···11+{ config, lib, pkgs, ... }:
22+33+let
44+55+ inherit (builtins) length map;
66+ inherit (lib.attrsets) attrNames filterAttrs hasAttr mapAttrs mapAttrsToList optionalAttrs;
77+ inherit (lib.modules) mkDefault mkIf;
88+ inherit (lib.options) literalExample mkEnableOption mkOption;
99+ inherit (lib.strings) concatStringsSep optionalString toLower;
1010+ inherit (lib.types) addCheck attrsOf lines loaOf nullOr package path port str strMatching submodule;
1111+1212+ # Checks if given list of strings contains unique
1313+ # elements when compared without considering case.
1414+ # Type: checkIUnique :: [string] -> bool
1515+ # Example: checkIUnique ["foo" "Foo"] => false
1616+ checkIUnique = lst:
1717+ let
1818+ lenUniq = l: length (lib.lists.unique l);
1919+ in
2020+ lenUniq lst == lenUniq (map toLower lst);
2121+2222+ # TSM rejects servername strings longer than 64 chars.
2323+ servernameType = strMatching ".{1,64}";
2424+2525+ serverOptions = { name, config, ... }: {
2626+ options.name = mkOption {
2727+ type = servernameType;
2828+ example = "mainTsmServer";
2929+ description = ''
3030+ Local name of the IBM TSM server,
3131+ must be uncapitalized and no longer than 64 chars.
3232+ The value will be used for the
3333+ <literal>server</literal>
3434+ directive in <filename>dsm.sys</filename>.
3535+ '';
3636+ };
3737+ options.server = mkOption {
3838+ type = strMatching ".+";
3939+ example = "tsmserver.company.com";
4040+ description = ''
4141+ Host/domain name or IP address of the IBM TSM server.
4242+ The value will be used for the
4343+ <literal>tcpserveraddress</literal>
4444+ directive in <filename>dsm.sys</filename>.
4545+ '';
4646+ };
4747+ options.port = mkOption {
4848+ type = addCheck port (p: p<=32767);
4949+ default = 1500; # official default
5050+ description = ''
5151+ TCP port of the IBM TSM server.
5252+ The value will be used for the
5353+ <literal>tcpport</literal>
5454+ directive in <filename>dsm.sys</filename>.
5555+ TSM does not support ports above 32767.
5656+ '';
5757+ };
5858+ options.node = mkOption {
5959+ type = strMatching ".+";
6060+ example = "MY-TSM-NODE";
6161+ description = ''
6262+ Target node name on the IBM TSM server.
6363+ The value will be used for the
6464+ <literal>nodename</literal>
6565+ directive in <filename>dsm.sys</filename>.
6666+ '';
6767+ };
6868+ options.genPasswd = mkEnableOption ''
6969+ automatic client password generation.
7070+ This option influences the
7171+ <literal>passwordaccess</literal>
7272+ directive in <filename>dsm.sys</filename>.
7373+ The password will be stored in the directory
7474+ given by the option <option>passwdDir</option>.
7575+ <emphasis>Caution</emphasis>:
7676+ If this option is enabled and the server forces
7777+ to renew the password (e.g. on first connection),
7878+ a random password will be generated and stored
7979+ '';
8080+ options.passwdDir = mkOption {
8181+ type = path;
8282+ example = "/home/alice/tsm-password";
8383+ description = ''
8484+ Directory that holds the TSM
8585+ node's password information.
8686+ The value will be used for the
8787+ <literal>passworddir</literal>
8888+ directive in <filename>dsm.sys</filename>.
8989+ '';
9090+ };
9191+ options.includeExclude = mkOption {
9292+ type = lines;
9393+ default = "";
9494+ example = ''
9595+ exclude.dir /nix/store
9696+ include.encrypt /home/.../*
9797+ '';
9898+ description = ''
9999+ <literal>include.*</literal> and
100100+ <literal>exclude.*</literal> directives to be
101101+ used when sending files to the IBM TSM server.
102102+ The lines will be written into a file that the
103103+ <literal>inclexcl</literal>
104104+ directive in <filename>dsm.sys</filename> points to.
105105+ '';
106106+ };
107107+ options.extraConfig = mkOption {
108108+ # TSM option keys are case insensitive;
109109+ # we have to ensure there are no keys that
110110+ # differ only by upper and lower case.
111111+ type = addCheck
112112+ (attrsOf (nullOr str))
113113+ (attrs: checkIUnique (attrNames attrs));
114114+ default = {};
115115+ example.compression = "yes";
116116+ example.passwordaccess = null;
117117+ description = ''
118118+ Additional key-value pairs for the server stanza.
119119+ Values must be strings, or <literal>null</literal>
120120+ for the key not to be used in the stanza
121121+ (e.g. to overrule values generated by other options).
122122+ '';
123123+ };
124124+ options.text = mkOption {
125125+ type = lines;
126126+ example = literalExample
127127+ ''lib.modules.mkAfter "compression no"'';
128128+ description = ''
129129+ Additional text lines for the server stanza.
130130+ This option can be used if certion configuration keys
131131+ must be used multiple times or ordered in a certain way
132132+ as the <option>extraConfig</option> option can't
133133+ control the order of lines in the resulting stanza.
134134+ Note that the <literal>server</literal>
135135+ line at the beginning of the stanza is
136136+ not part of this option's value.
137137+ '';
138138+ };
139139+ options.stanza = mkOption {
140140+ type = str;
141141+ internal = true;
142142+ visible = false;
143143+ description = "Server stanza text generated from the options.";
144144+ };
145145+ config.name = mkDefault name;
146146+ # Client system-options file directives are explained here:
147147+ # https://www.ibm.com/support/knowledgecenter/SSEQVQ_8.1.8/client/c_opt_usingopts.html
148148+ config.extraConfig =
149149+ mapAttrs (lib.trivial.const mkDefault) (
150150+ {
151151+ commmethod = "v6tcpip"; # uses v4 or v6, based on dns lookup result
152152+ tcpserveraddress = config.server;
153153+ tcpport = builtins.toString config.port;
154154+ nodename = config.node;
155155+ passwordaccess = if config.genPasswd then "generate" else "prompt";
156156+ passworddir = ''"${config.passwdDir}"'';
157157+ } // optionalAttrs (config.includeExclude!="") {
158158+ inclexcl = ''"${pkgs.writeText "inclexcl.dsm.sys" config.includeExclude}"'';
159159+ }
160160+ );
161161+ config.text =
162162+ let
163163+ attrset = filterAttrs (k: v: v!=null) config.extraConfig;
164164+ mkLine = k: v: k + optionalString (v!="") " ${v}";
165165+ lines = mapAttrsToList mkLine attrset;
166166+ in
167167+ concatStringsSep "\n" lines;
168168+ config.stanza = ''
169169+ server ${config.name}
170170+ ${config.text}
171171+ '';
172172+ };
173173+174174+ options.programs.tsmClient = {
175175+ enable = mkEnableOption ''
176176+ IBM Spectrum Protect (Tivoli Storage Manager, TSM)
177177+ client command line applications with a
178178+ client system-options file "dsm.sys"
179179+ '';
180180+ servers = mkOption {
181181+ type = loaOf (submodule [ serverOptions ]);
182182+ default = {};
183183+ example.mainTsmServer = {
184184+ server = "tsmserver.company.com";
185185+ node = "MY-TSM-NODE";
186186+ extraConfig.compression = "yes";
187187+ };
188188+ description = ''
189189+ Server definitions ("stanzas")
190190+ for the client system-options file.
191191+ '';
192192+ };
193193+ defaultServername = mkOption {
194194+ type = nullOr servernameType;
195195+ default = null;
196196+ example = "mainTsmServer";
197197+ description = ''
198198+ If multiple server stanzas are declared with
199199+ <option>programs.tsmClient.servers</option>,
200200+ this option may be used to name a default
201201+ server stanza that IBM TSM uses in the absence of
202202+ a user-defined <filename>dsm.opt</filename> file.
203203+ This option translates to a
204204+ <literal>defaultserver</literal> configuration line.
205205+ '';
206206+ };
207207+ dsmSysText = mkOption {
208208+ type = lines;
209209+ readOnly = true;
210210+ description = ''
211211+ This configuration key contains the effective text
212212+ of the client system-options file "dsm.sys".
213213+ It should not be changed, but may be
214214+ used to feed the configuration into other
215215+ TSM-depending packages used on the system.
216216+ '';
217217+ };
218218+ package = mkOption {
219219+ type = package;
220220+ default = pkgs.tsm-client;
221221+ defaultText = "pkgs.tsm-client";
222222+ example = literalExample "pkgs.tsm-client-withGui";
223223+ description = ''
224224+ The TSM client derivation to be
225225+ added to the system environment.
226226+ It will called with <literal>.override</literal>
227227+ to add paths to the client system-options file.
228228+ '';
229229+ };
230230+ wrappedPackage = mkOption {
231231+ type = package;
232232+ readOnly = true;
233233+ description = ''
234234+ The TSM client derivation, wrapped with the path
235235+ to the client system-options file "dsm.sys".
236236+ This option is to provide the effective derivation
237237+ for other modules that want to call TSM executables.
238238+ '';
239239+ };
240240+ };
241241+242242+ cfg = config.programs.tsmClient;
243243+244244+ assertions = [
245245+ {
246246+ assertion = checkIUnique (mapAttrsToList (k: v: v.name) cfg.servers);
247247+ message = ''
248248+ TSM servernames contain duplicate name
249249+ (note that case doesn't matter!)
250250+ '';
251251+ }
252252+ {
253253+ assertion = (cfg.defaultServername!=null)->(hasAttr cfg.defaultServername cfg.servers);
254254+ message = "TSM defaultServername not found in list of servers";
255255+ }
256256+ ];
257257+258258+ dsmSysText = ''
259259+ **** IBM Spectrum Protect (Tivoli Storage Manager)
260260+ **** client system-options file "dsm.sys".
261261+ **** Do not edit!
262262+ **** This file is generated by NixOS configuration.
263263+264264+ ${optionalString (cfg.defaultServername!=null) "defaultserver ${cfg.defaultServername}"}
265265+266266+ ${concatStringsSep "\n" (mapAttrsToList (k: v: v.stanza) cfg.servers)}
267267+ '';
268268+269269+in
270270+271271+{
272272+273273+ inherit options;
274274+275275+ config = mkIf cfg.enable {
276276+ inherit assertions;
277277+ programs.tsmClient.dsmSysText = dsmSysText;
278278+ programs.tsmClient.wrappedPackage = cfg.package.override rec {
279279+ dsmSysCli = pkgs.writeText "dsm.sys" cfg.dsmSysText;
280280+ dsmSysApi = dsmSysCli;
281281+ };
282282+ environment.systemPackages = [ cfg.wrappedPackage ];
283283+ };
284284+285285+ meta.maintainers = [ lib.maintainers.yarny ];
286286+287287+}
+106
nixos/modules/services/backup/tsm.nix
···11+{ config, lib, ... }:
22+33+let
44+55+ inherit (lib.attrsets) hasAttr;
66+ inherit (lib.modules) mkDefault mkIf;
77+ inherit (lib.options) mkEnableOption mkOption;
88+ inherit (lib.types) nullOr strMatching;
99+1010+ options.services.tsmBackup = {
1111+ enable = mkEnableOption ''
1212+ automatic backups with the
1313+ IBM Spectrum Protect (Tivoli Storage Manager, TSM) client.
1414+ This also enables
1515+ <option>programs.tsmClient.enable</option>
1616+ '';
1717+ command = mkOption {
1818+ type = strMatching ".+";
1919+ default = "backup";
2020+ example = "incr";
2121+ description = ''
2222+ The actual command passed to the
2323+ <literal>dsmc</literal> executable to start the backup.
2424+ '';
2525+ };
2626+ servername = mkOption {
2727+ type = strMatching ".+";
2828+ example = "mainTsmServer";
2929+ description = ''
3030+ Create a systemd system service
3131+ <literal>tsm-backup.service</literal> that starts
3232+ a backup based on the given servername's stanza.
3333+ Note that this server's
3434+ <option>passwdDir</option> will default to
3535+ <filename>/var/lib/tsm-backup/password</filename>
3636+ (but may be overridden);
3737+ also, the service will use
3838+ <filename>/var/lib/tsm-backup</filename> as
3939+ <literal>HOME</literal> when calling
4040+ <literal>dsmc</literal>.
4141+ '';
4242+ };
4343+ autoTime = mkOption {
4444+ type = nullOr (strMatching ".+");
4545+ default = null;
4646+ example = "12:00";
4747+ description = ''
4848+ The backup service will be invoked
4949+ automatically at the given date/time,
5050+ which must be in the format described in
5151+ <citerefentry><refentrytitle>systemd.time</refentrytitle><manvolnum>5</manvolnum></citerefentry>.
5252+ The default <literal>null</literal>
5353+ disables automatic backups.
5454+ '';
5555+ };
5656+ };
5757+5858+ cfg = config.services.tsmBackup;
5959+ cfgPrg = config.programs.tsmClient;
6060+6161+ assertions = [
6262+ {
6363+ assertion = hasAttr cfg.servername cfgPrg.servers;
6464+ message = "TSM service servername not found in list of servers";
6565+ }
6666+ {
6767+ assertion = cfgPrg.servers.${cfg.servername}.genPasswd;
6868+ message = "TSM service requires automatic password generation";
6969+ }
7070+ ];
7171+7272+in
7373+7474+{
7575+7676+ inherit options;
7777+7878+ config = mkIf cfg.enable {
7979+ inherit assertions;
8080+ programs.tsmClient.enable = true;
8181+ programs.tsmClient.servers."${cfg.servername}".passwdDir =
8282+ mkDefault "/var/lib/tsm-backup/password";
8383+ systemd.services.tsm-backup = {
8484+ description = "IBM Spectrum Protect (Tivoli Storage Manager) Backup";
8585+ # DSM_LOG needs a trailing slash to have it treated as a directory.
8686+ # `/var/log` would be littered with TSM log files otherwise.
8787+ environment.DSM_LOG = "/var/log/tsm-backup/";
8888+ # TSM needs a HOME dir to store certificates.
8989+ environment.HOME = "/var/lib/tsm-backup";
9090+ # for exit status description see
9191+ # https://www.ibm.com/support/knowledgecenter/en/SSEQVQ_8.1.8/client/c_sched_rtncode.html
9292+ serviceConfig.SuccessExitStatus = "4 8";
9393+ # The `-se` option must come after the command.
9494+ # The `-optfile` option suppresses a `dsm.opt`-not-found warning.
9595+ serviceConfig.ExecStart =
9696+ "${cfgPrg.wrappedPackage}/bin/dsmc ${cfg.command} -se='${cfg.servername}' -optfile=/dev/null";
9797+ serviceConfig.LogsDirectory = "tsm-backup";
9898+ serviceConfig.StateDirectory = "tsm-backup";
9999+ serviceConfig.StateDirectoryMode = "0750";
100100+ startAt = mkIf (cfg.autoTime!=null) cfg.autoTime;
101101+ };
102102+ };
103103+104104+ meta.maintainers = [ lib.maintainers.yarny ];
105105+106106+}
+165
pkgs/tools/backup/tsm-client/default.nix
···11+{ lib
22+, stdenv
33+, autoPatchelfHook
44+, buildEnv
55+, fetchurl
66+, makeWrapper
77+, procps
88+, zlib
99+# optional packages that enable certain features
1010+, acl ? null # EXT2/EXT3/XFS ACL support
1111+, jdk8 ? null # Java GUI
1212+, lvm2 ? null # LVM image backup and restore functions
1313+# path to `dsm.sys` configuration files
1414+, dsmSysCli ? "/etc/tsm-client/cli.dsm.sys"
1515+, dsmSysApi ? "/etc/tsm-client/api.dsm.sys"
1616+}:
1717+1818+1919+# For an explanation of optional packages
2020+# (features provided by them, version limits), see
2121+# https://www-01.ibm.com/support/docview.wss?uid=swg21052223#Version%208.1
2222+2323+2424+# IBM Tivoli Storage Manager Client uses a system-wide
2525+# client system-options file `dsm.sys` and expects it
2626+# to be located in a directory within the package.
2727+# Note that the command line client and the API use
2828+# different "dms.sys" files (located in different directories).
2929+# Since these files contain settings to be altered by the
3030+# admin user (e.g. TSM server name), we create symlinks
3131+# in place of the files that the client attempts to open.
3232+# Use the arguments `dsmSysCli` and `dsmSysApi` to
3333+# provide the location of the configuration files for
3434+# the command-line interface and the API, respectively.
3535+#
3636+# While the command-line interface contains wrappers
3737+# that help the executables find the configuration file,
3838+# packages that link against the API have to
3939+# set the environment variable `DSMI_DIR` to
4040+# point to this derivations `/dsmi_dir` directory symlink.
4141+# Other environment variables might be necessary,
4242+# depending on local configuration or usage; see:
4343+# https://www.ibm.com/support/knowledgecenter/en/SSEQVQ_8.1.8/client/c_cfg_sapiunix.html
4444+4545+4646+# The newest version of TSM client should be discoverable
4747+# by going the the `downloadPage` (see `meta` below),
4848+# there to "Client Latest Downloads",
4949+# "IBM Spectrum Protect Client Downloads and READMEs",
5050+# then to "Linux x86_64 Ubuntu client" (as of 2019-07-15).
5151+5252+5353+let
5454+5555+ meta = {
5656+ homepage = https://www.ibm.com/us-en/marketplace/data-protection-and-recovery;
5757+ downloadPage = https://www-01.ibm.com/support/docview.wss?uid=swg21239415;
5858+ platforms = [ "x86_64-linux" ];
5959+ license = lib.licenses.unfree;
6060+ maintainers = [ lib.maintainers.yarny ];
6161+ description = "IBM Spectrum Protect (Tivoli Storage Manager) CLI and API";
6262+ longDescription = ''
6363+ IBM Spectrum Protect (Tivoli Storage Manager) provides
6464+ a single point of control for backup and recovery.
6565+ This package contains the client software, that is,
6666+ a command line client and linkable libraries.
6767+6868+ Note that the software requires a system-wide
6969+ client system-options file (commonly named "dsm.sys").
7070+ This package allows to use separate files for
7171+ the command-line interface and for the linkable API.
7272+ The location of those files can
7373+ be provided as build parameters.
7474+ '';
7575+ };
7676+7777+ unwrapped = stdenv.mkDerivation rec {
7878+ name = "tsm-client-${version}-unwrapped";
7979+ version = "8.1.8.0";
8080+ src = fetchurl {
8181+ url = "ftp://public.dhe.ibm.com/storage/tivoli-storage-management/maintenance/client/v8r1/Linux/LinuxX86_DEB/BA/v818/${version}-TIV-TSMBAC-LinuxX86_DEB.tar";
8282+ sha256 = "0c1d0jm0i7qjd314nhj2vj8fs7sncm1x2n4d6dg4049jniyvjhpk";
8383+ };
8484+ inherit meta;
8585+8686+ nativeBuildInputs = [
8787+ autoPatchelfHook
8888+ ];
8989+ buildInputs = [
9090+ stdenv.cc.cc
9191+ zlib
9292+ ];
9393+ runtimeDependencies = [
9494+ lvm2
9595+ ];
9696+ sourceRoot = ".";
9797+9898+ postUnpack = ''
9999+ for debfile in *.deb
100100+ do
101101+ ar -x "$debfile"
102102+ tar --xz --extract --file=data.tar.xz
103103+ rm data.tar.xz
104104+ done
105105+ '';
106106+107107+ installPhase = ''
108108+ runHook preInstall
109109+ mkdir --parents $out
110110+ mv --target-directory=$out usr/* opt
111111+ runHook postInstall
112112+ '';
113113+114114+ # Fix relative symlinks after `/usr` was moved up one level
115115+ preFixup = ''
116116+ for link in $out/lib/* $out/bin/*
117117+ do
118118+ target=$(readlink "$link")
119119+ if [ "$(cut -b -6 <<< "$target")" != "../../" ]
120120+ then
121121+ echo "cannot fix this symlink: $link -> $target"
122122+ exit 1
123123+ fi
124124+ ln --symbolic --force --no-target-directory "$out/$(cut -b 7- <<< "$target")" "$link"
125125+ done
126126+ '';
127127+ };
128128+129129+in
130130+131131+buildEnv {
132132+ name = "tsm-client-${unwrapped.version}";
133133+ inherit meta;
134134+ passthru = { inherit unwrapped; };
135135+ paths = [ unwrapped ];
136136+ buildInputs = [ makeWrapper ];
137137+ pathsToLink = [
138138+ "/"
139139+ "/bin"
140140+ "/opt/tivoli/tsm/client/ba/bin"
141141+ "/opt/tivoli/tsm/client/api/bin64"
142142+ ];
143143+ # * Provide top-level symlinks `dsm_dir` and `dsmi_dir`
144144+ # to the so-called "installation directories"
145145+ # * Add symlinks to the "installation directories"
146146+ # that point to the `dsm.sys` configuration files
147147+ # * Drop the Java GUI executable unless `jdk` is present
148148+ # * Create wrappers for the command-line interface to
149149+ # prepare `PATH` and `DSM_DIR` environment variables
150150+ postBuild = ''
151151+ ln --symbolic --no-target-directory opt/tivoli/tsm/client/ba/bin $out/dsm_dir
152152+ ln --symbolic --no-target-directory opt/tivoli/tsm/client/api/bin64 $out/dsmi_dir
153153+ ln --symbolic --no-target-directory "${dsmSysCli}" $out/dsm_dir/dsm.sys
154154+ ln --symbolic --no-target-directory "${dsmSysApi}" $out/dsmi_dir/dsm.sys
155155+ ${lib.strings.optionalString (jdk8==null) "rm $out/bin/dsmj"}
156156+ for bin in $out/bin/*
157157+ do
158158+ target=$(readlink "$bin")
159159+ rm "$bin"
160160+ makeWrapper "$target" "$bin" \
161161+ --prefix PATH : "$out/dsm_dir:${lib.strings.makeBinPath [ procps acl jdk8 ]}" \
162162+ --set DSM_DIR $out/dsm_dir
163163+ done
164164+ '';
165165+}