diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index d8e8dd37af76..80c8618df193 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -139,6 +139,7 @@ ./programs/sway.nix ./programs/thefuck.nix ./programs/tmux.nix + ./programs/tsm-client.nix ./programs/udevil.nix ./programs/venus.nix ./programs/vim.nix @@ -210,6 +211,7 @@ ./services/backup/restic-rest-server.nix ./services/backup/rsnapshot.nix ./services/backup/tarsnap.nix + ./services/backup/tsm.nix ./services/backup/znapzend.nix ./services/cluster/hadoop/default.nix ./services/cluster/kubernetes/addons/dns.nix diff --git a/nixos/modules/programs/tsm-client.nix b/nixos/modules/programs/tsm-client.nix new file mode 100644 index 000000000000..eb6f12475286 --- /dev/null +++ b/nixos/modules/programs/tsm-client.nix @@ -0,0 +1,287 @@ +{ config, lib, pkgs, ... }: + +let + + inherit (builtins) length map; + inherit (lib.attrsets) attrNames filterAttrs hasAttr mapAttrs mapAttrsToList optionalAttrs; + inherit (lib.modules) mkDefault mkIf; + inherit (lib.options) literalExample mkEnableOption mkOption; + inherit (lib.strings) concatStringsSep optionalString toLower; + inherit (lib.types) addCheck attrsOf lines loaOf nullOr package path port str strMatching submodule; + + # Checks if given list of strings contains unique + # elements when compared without considering case. + # Type: checkIUnique :: [string] -> bool + # Example: checkIUnique ["foo" "Foo"] => false + checkIUnique = lst: + let + lenUniq = l: length (lib.lists.unique l); + in + lenUniq lst == lenUniq (map toLower lst); + + # TSM rejects servername strings longer than 64 chars. + servernameType = strMatching ".{1,64}"; + + serverOptions = { name, config, ... }: { + options.name = mkOption { + type = servernameType; + example = "mainTsmServer"; + description = '' + Local name of the IBM TSM server, + must be uncapitalized and no longer than 64 chars. + The value will be used for the + server + directive in dsm.sys. + ''; + }; + options.server = mkOption { + type = strMatching ".+"; + example = "tsmserver.company.com"; + description = '' + Host/domain name or IP address of the IBM TSM server. + The value will be used for the + tcpserveraddress + directive in dsm.sys. + ''; + }; + options.port = mkOption { + type = addCheck port (p: p<=32767); + default = 1500; # official default + description = '' + TCP port of the IBM TSM server. + The value will be used for the + tcpport + directive in dsm.sys. + TSM does not support ports above 32767. + ''; + }; + options.node = mkOption { + type = strMatching ".+"; + example = "MY-TSM-NODE"; + description = '' + Target node name on the IBM TSM server. + The value will be used for the + nodename + directive in dsm.sys. + ''; + }; + options.genPasswd = mkEnableOption '' + automatic client password generation. + This option influences the + passwordaccess + directive in dsm.sys. + The password will be stored in the directory + given by the option . + Caution: + If this option is enabled and the server forces + to renew the password (e.g. on first connection), + a random password will be generated and stored + ''; + options.passwdDir = mkOption { + type = path; + example = "/home/alice/tsm-password"; + description = '' + Directory that holds the TSM + node's password information. + The value will be used for the + passworddir + directive in dsm.sys. + ''; + }; + options.includeExclude = mkOption { + type = lines; + default = ""; + example = '' + exclude.dir /nix/store + include.encrypt /home/.../* + ''; + description = '' + include.* and + exclude.* directives to be + used when sending files to the IBM TSM server. + The lines will be written into a file that the + inclexcl + directive in dsm.sys points to. + ''; + }; + options.extraConfig = mkOption { + # TSM option keys are case insensitive; + # we have to ensure there are no keys that + # differ only by upper and lower case. + type = addCheck + (attrsOf (nullOr str)) + (attrs: checkIUnique (attrNames attrs)); + default = {}; + example.compression = "yes"; + example.passwordaccess = null; + description = '' + Additional key-value pairs for the server stanza. + Values must be strings, or null + for the key not to be used in the stanza + (e.g. to overrule values generated by other options). + ''; + }; + options.text = mkOption { + type = lines; + example = literalExample + ''lib.modules.mkAfter "compression no"''; + description = '' + Additional text lines for the server stanza. + This option can be used if certion configuration keys + must be used multiple times or ordered in a certain way + as the option can't + control the order of lines in the resulting stanza. + Note that the server + line at the beginning of the stanza is + not part of this option's value. + ''; + }; + options.stanza = mkOption { + type = str; + internal = true; + visible = false; + description = "Server stanza text generated from the options."; + }; + config.name = mkDefault name; + # Client system-options file directives are explained here: + # https://www.ibm.com/support/knowledgecenter/SSEQVQ_8.1.8/client/c_opt_usingopts.html + config.extraConfig = + mapAttrs (lib.trivial.const mkDefault) ( + { + commmethod = "v6tcpip"; # uses v4 or v6, based on dns lookup result + tcpserveraddress = config.server; + tcpport = builtins.toString config.port; + nodename = config.node; + passwordaccess = if config.genPasswd then "generate" else "prompt"; + passworddir = ''"${config.passwdDir}"''; + } // optionalAttrs (config.includeExclude!="") { + inclexcl = ''"${pkgs.writeText "inclexcl.dsm.sys" config.includeExclude}"''; + } + ); + config.text = + let + attrset = filterAttrs (k: v: v!=null) config.extraConfig; + mkLine = k: v: k + optionalString (v!="") " ${v}"; + lines = mapAttrsToList mkLine attrset; + in + concatStringsSep "\n" lines; + config.stanza = '' + server ${config.name} + ${config.text} + ''; + }; + + options.programs.tsmClient = { + enable = mkEnableOption '' + IBM Spectrum Protect (Tivoli Storage Manager, TSM) + client command line applications with a + client system-options file "dsm.sys" + ''; + servers = mkOption { + type = loaOf (submodule [ serverOptions ]); + default = {}; + example.mainTsmServer = { + server = "tsmserver.company.com"; + node = "MY-TSM-NODE"; + extraConfig.compression = "yes"; + }; + description = '' + Server definitions ("stanzas") + for the client system-options file. + ''; + }; + defaultServername = mkOption { + type = nullOr servernameType; + default = null; + example = "mainTsmServer"; + description = '' + If multiple server stanzas are declared with + , + this option may be used to name a default + server stanza that IBM TSM uses in the absence of + a user-defined dsm.opt file. + This option translates to a + defaultserver configuration line. + ''; + }; + dsmSysText = mkOption { + type = lines; + readOnly = true; + description = '' + This configuration key contains the effective text + of the client system-options file "dsm.sys". + It should not be changed, but may be + used to feed the configuration into other + TSM-depending packages used on the system. + ''; + }; + package = mkOption { + type = package; + default = pkgs.tsm-client; + defaultText = "pkgs.tsm-client"; + example = literalExample "pkgs.tsm-client-withGui"; + description = '' + The TSM client derivation to be + added to the system environment. + It will called with .override + to add paths to the client system-options file. + ''; + }; + wrappedPackage = mkOption { + type = package; + readOnly = true; + description = '' + The TSM client derivation, wrapped with the path + to the client system-options file "dsm.sys". + This option is to provide the effective derivation + for other modules that want to call TSM executables. + ''; + }; + }; + + cfg = config.programs.tsmClient; + + assertions = [ + { + assertion = checkIUnique (mapAttrsToList (k: v: v.name) cfg.servers); + message = '' + TSM servernames contain duplicate name + (note that case doesn't matter!) + ''; + } + { + assertion = (cfg.defaultServername!=null)->(hasAttr cfg.defaultServername cfg.servers); + message = "TSM defaultServername not found in list of servers"; + } + ]; + + dsmSysText = '' + **** IBM Spectrum Protect (Tivoli Storage Manager) + **** client system-options file "dsm.sys". + **** Do not edit! + **** This file is generated by NixOS configuration. + + ${optionalString (cfg.defaultServername!=null) "defaultserver ${cfg.defaultServername}"} + + ${concatStringsSep "\n" (mapAttrsToList (k: v: v.stanza) cfg.servers)} + ''; + +in + +{ + + inherit options; + + config = mkIf cfg.enable { + inherit assertions; + programs.tsmClient.dsmSysText = dsmSysText; + programs.tsmClient.wrappedPackage = cfg.package.override rec { + dsmSysCli = pkgs.writeText "dsm.sys" cfg.dsmSysText; + dsmSysApi = dsmSysCli; + }; + environment.systemPackages = [ cfg.wrappedPackage ]; + }; + + meta.maintainers = [ lib.maintainers.yarny ]; + +} diff --git a/nixos/modules/services/backup/tsm.nix b/nixos/modules/services/backup/tsm.nix new file mode 100644 index 000000000000..3b2bb37491b5 --- /dev/null +++ b/nixos/modules/services/backup/tsm.nix @@ -0,0 +1,106 @@ +{ config, lib, ... }: + +let + + inherit (lib.attrsets) hasAttr; + inherit (lib.modules) mkDefault mkIf; + inherit (lib.options) mkEnableOption mkOption; + inherit (lib.types) nullOr strMatching; + + options.services.tsmBackup = { + enable = mkEnableOption '' + automatic backups with the + IBM Spectrum Protect (Tivoli Storage Manager, TSM) client. + This also enables + + ''; + command = mkOption { + type = strMatching ".+"; + default = "backup"; + example = "incr"; + description = '' + The actual command passed to the + dsmc executable to start the backup. + ''; + }; + servername = mkOption { + type = strMatching ".+"; + example = "mainTsmServer"; + description = '' + Create a systemd system service + tsm-backup.service that starts + a backup based on the given servername's stanza. + Note that this server's + will default to + /var/lib/tsm-backup/password + (but may be overridden); + also, the service will use + /var/lib/tsm-backup as + HOME when calling + dsmc. + ''; + }; + autoTime = mkOption { + type = nullOr (strMatching ".+"); + default = null; + example = "12:00"; + description = '' + The backup service will be invoked + automatically at the given date/time, + which must be in the format described in + systemd.time5. + The default null + disables automatic backups. + ''; + }; + }; + + cfg = config.services.tsmBackup; + cfgPrg = config.programs.tsmClient; + + assertions = [ + { + assertion = hasAttr cfg.servername cfgPrg.servers; + message = "TSM service servername not found in list of servers"; + } + { + assertion = cfgPrg.servers.${cfg.servername}.genPasswd; + message = "TSM service requires automatic password generation"; + } + ]; + +in + +{ + + inherit options; + + config = mkIf cfg.enable { + inherit assertions; + programs.tsmClient.enable = true; + programs.tsmClient.servers."${cfg.servername}".passwdDir = + mkDefault "/var/lib/tsm-backup/password"; + systemd.services.tsm-backup = { + description = "IBM Spectrum Protect (Tivoli Storage Manager) Backup"; + # DSM_LOG needs a trailing slash to have it treated as a directory. + # `/var/log` would be littered with TSM log files otherwise. + environment.DSM_LOG = "/var/log/tsm-backup/"; + # TSM needs a HOME dir to store certificates. + environment.HOME = "/var/lib/tsm-backup"; + # for exit status description see + # https://www.ibm.com/support/knowledgecenter/en/SSEQVQ_8.1.8/client/c_sched_rtncode.html + serviceConfig.SuccessExitStatus = "4 8"; + # The `-se` option must come after the command. + # The `-optfile` option suppresses a `dsm.opt`-not-found warning. + serviceConfig.ExecStart = + "${cfgPrg.wrappedPackage}/bin/dsmc ${cfg.command} -se='${cfg.servername}' -optfile=/dev/null"; + serviceConfig.LogsDirectory = "tsm-backup"; + serviceConfig.StateDirectory = "tsm-backup"; + serviceConfig.StateDirectoryMode = "0750"; + startAt = mkIf (cfg.autoTime!=null) cfg.autoTime; + }; + }; + + meta.maintainers = [ lib.maintainers.yarny ]; + +} diff --git a/pkgs/tools/backup/tsm-client/default.nix b/pkgs/tools/backup/tsm-client/default.nix new file mode 100644 index 000000000000..a1f7b1aba84e --- /dev/null +++ b/pkgs/tools/backup/tsm-client/default.nix @@ -0,0 +1,165 @@ +{ lib +, stdenv +, autoPatchelfHook +, buildEnv +, fetchurl +, makeWrapper +, procps +, zlib +# optional packages that enable certain features +, acl ? null # EXT2/EXT3/XFS ACL support +, jdk8 ? null # Java GUI +, lvm2 ? null # LVM image backup and restore functions +# path to `dsm.sys` configuration files +, dsmSysCli ? "/etc/tsm-client/cli.dsm.sys" +, dsmSysApi ? "/etc/tsm-client/api.dsm.sys" +}: + + +# For an explanation of optional packages +# (features provided by them, version limits), see +# https://www-01.ibm.com/support/docview.wss?uid=swg21052223#Version%208.1 + + +# IBM Tivoli Storage Manager Client uses a system-wide +# client system-options file `dsm.sys` and expects it +# to be located in a directory within the package. +# Note that the command line client and the API use +# different "dms.sys" files (located in different directories). +# Since these files contain settings to be altered by the +# admin user (e.g. TSM server name), we create symlinks +# in place of the files that the client attempts to open. +# Use the arguments `dsmSysCli` and `dsmSysApi` to +# provide the location of the configuration files for +# the command-line interface and the API, respectively. +# +# While the command-line interface contains wrappers +# that help the executables find the configuration file, +# packages that link against the API have to +# set the environment variable `DSMI_DIR` to +# point to this derivations `/dsmi_dir` directory symlink. +# Other environment variables might be necessary, +# depending on local configuration or usage; see: +# https://www.ibm.com/support/knowledgecenter/en/SSEQVQ_8.1.8/client/c_cfg_sapiunix.html + + +# The newest version of TSM client should be discoverable +# by going the the `downloadPage` (see `meta` below), +# there to "Client Latest Downloads", +# "IBM Spectrum Protect Client Downloads and READMEs", +# then to "Linux x86_64 Ubuntu client" (as of 2019-07-15). + + +let + + meta = { + homepage = https://www.ibm.com/us-en/marketplace/data-protection-and-recovery; + downloadPage = https://www-01.ibm.com/support/docview.wss?uid=swg21239415; + platforms = [ "x86_64-linux" ]; + license = lib.licenses.unfree; + maintainers = [ lib.maintainers.yarny ]; + description = "IBM Spectrum Protect (Tivoli Storage Manager) CLI and API"; + longDescription = '' + IBM Spectrum Protect (Tivoli Storage Manager) provides + a single point of control for backup and recovery. + This package contains the client software, that is, + a command line client and linkable libraries. + + Note that the software requires a system-wide + client system-options file (commonly named "dsm.sys"). + This package allows to use separate files for + the command-line interface and for the linkable API. + The location of those files can + be provided as build parameters. + ''; + }; + + unwrapped = stdenv.mkDerivation rec { + name = "tsm-client-${version}-unwrapped"; + version = "8.1.8.0"; + src = fetchurl { + url = "ftp://public.dhe.ibm.com/storage/tivoli-storage-management/maintenance/client/v8r1/Linux/LinuxX86_DEB/BA/v818/${version}-TIV-TSMBAC-LinuxX86_DEB.tar"; + sha256 = "0c1d0jm0i7qjd314nhj2vj8fs7sncm1x2n4d6dg4049jniyvjhpk"; + }; + inherit meta; + + nativeBuildInputs = [ + autoPatchelfHook + ]; + buildInputs = [ + stdenv.cc.cc + zlib + ]; + runtimeDependencies = [ + lvm2 + ]; + sourceRoot = "."; + + postUnpack = '' + for debfile in *.deb + do + ar -x "$debfile" + tar --xz --extract --file=data.tar.xz + rm data.tar.xz + done + ''; + + installPhase = '' + runHook preInstall + mkdir --parents $out + mv --target-directory=$out usr/* opt + runHook postInstall + ''; + + # Fix relative symlinks after `/usr` was moved up one level + preFixup = '' + for link in $out/lib/* $out/bin/* + do + target=$(readlink "$link") + if [ "$(cut -b -6 <<< "$target")" != "../../" ] + then + echo "cannot fix this symlink: $link -> $target" + exit 1 + fi + ln --symbolic --force --no-target-directory "$out/$(cut -b 7- <<< "$target")" "$link" + done + ''; + }; + +in + +buildEnv { + name = "tsm-client-${unwrapped.version}"; + inherit meta; + passthru = { inherit unwrapped; }; + paths = [ unwrapped ]; + buildInputs = [ makeWrapper ]; + pathsToLink = [ + "/" + "/bin" + "/opt/tivoli/tsm/client/ba/bin" + "/opt/tivoli/tsm/client/api/bin64" + ]; + # * Provide top-level symlinks `dsm_dir` and `dsmi_dir` + # to the so-called "installation directories" + # * Add symlinks to the "installation directories" + # that point to the `dsm.sys` configuration files + # * Drop the Java GUI executable unless `jdk` is present + # * Create wrappers for the command-line interface to + # prepare `PATH` and `DSM_DIR` environment variables + postBuild = '' + ln --symbolic --no-target-directory opt/tivoli/tsm/client/ba/bin $out/dsm_dir + ln --symbolic --no-target-directory opt/tivoli/tsm/client/api/bin64 $out/dsmi_dir + ln --symbolic --no-target-directory "${dsmSysCli}" $out/dsm_dir/dsm.sys + ln --symbolic --no-target-directory "${dsmSysApi}" $out/dsmi_dir/dsm.sys + ${lib.strings.optionalString (jdk8==null) "rm $out/bin/dsmj"} + for bin in $out/bin/* + do + target=$(readlink "$bin") + rm "$bin" + makeWrapper "$target" "$bin" \ + --prefix PATH : "$out/dsm_dir:${lib.strings.makeBinPath [ procps acl jdk8 ]}" \ + --set DSM_DIR $out/dsm_dir + done + ''; +} diff --git a/pkgs/top-level/all-packages.nix b/pkgs/top-level/all-packages.nix index bde0167c94d0..e881e2f758fd 100644 --- a/pkgs/top-level/all-packages.nix +++ b/pkgs/top-level/all-packages.nix @@ -2682,6 +2682,9 @@ in teamocil = callPackage ../tools/misc/teamocil { }; + tsm-client = callPackage ../tools/backup/tsm-client { jdk8 = null; }; + tsm-client-withGui = callPackage ../tools/backup/tsm-client { }; + tridactyl-native = callPackage ../tools/networking/tridactyl-native { }; trompeloeil = callPackage ../development/libraries/trompeloeil { };