···1+{ config, lib, pkgs, ... }:
2+3+let
4+5+ inherit (builtins) length map;
6+ inherit (lib.attrsets) attrNames filterAttrs hasAttr mapAttrs mapAttrsToList optionalAttrs;
7+ inherit (lib.modules) mkDefault mkIf;
8+ inherit (lib.options) literalExample mkEnableOption mkOption;
9+ inherit (lib.strings) concatStringsSep optionalString toLower;
10+ inherit (lib.types) addCheck attrsOf lines loaOf nullOr package path port str strMatching submodule;
11+12+ # Checks if given list of strings contains unique
13+ # elements when compared without considering case.
14+ # Type: checkIUnique :: [string] -> bool
15+ # Example: checkIUnique ["foo" "Foo"] => false
16+ checkIUnique = lst:
17+ let
18+ lenUniq = l: length (lib.lists.unique l);
19+ in
20+ lenUniq lst == lenUniq (map toLower lst);
21+22+ # TSM rejects servername strings longer than 64 chars.
23+ servernameType = strMatching ".{1,64}";
24+25+ serverOptions = { name, config, ... }: {
26+ options.name = mkOption {
27+ type = servernameType;
28+ example = "mainTsmServer";
29+ description = ''
30+ Local name of the IBM TSM server,
31+ must be uncapitalized and no longer than 64 chars.
32+ The value will be used for the
33+ <literal>server</literal>
34+ directive in <filename>dsm.sys</filename>.
35+ '';
36+ };
37+ options.server = mkOption {
38+ type = strMatching ".+";
39+ example = "tsmserver.company.com";
40+ description = ''
41+ Host/domain name or IP address of the IBM TSM server.
42+ The value will be used for the
43+ <literal>tcpserveraddress</literal>
44+ directive in <filename>dsm.sys</filename>.
45+ '';
46+ };
47+ options.port = mkOption {
48+ type = addCheck port (p: p<=32767);
49+ default = 1500; # official default
50+ description = ''
51+ TCP port of the IBM TSM server.
52+ The value will be used for the
53+ <literal>tcpport</literal>
54+ directive in <filename>dsm.sys</filename>.
55+ TSM does not support ports above 32767.
56+ '';
57+ };
58+ options.node = mkOption {
59+ type = strMatching ".+";
60+ example = "MY-TSM-NODE";
61+ description = ''
62+ Target node name on the IBM TSM server.
63+ The value will be used for the
64+ <literal>nodename</literal>
65+ directive in <filename>dsm.sys</filename>.
66+ '';
67+ };
68+ options.genPasswd = mkEnableOption ''
69+ automatic client password generation.
70+ This option influences the
71+ <literal>passwordaccess</literal>
72+ directive in <filename>dsm.sys</filename>.
73+ The password will be stored in the directory
74+ given by the option <option>passwdDir</option>.
75+ <emphasis>Caution</emphasis>:
76+ If this option is enabled and the server forces
77+ to renew the password (e.g. on first connection),
78+ a random password will be generated and stored
79+ '';
80+ options.passwdDir = mkOption {
81+ type = path;
82+ example = "/home/alice/tsm-password";
83+ description = ''
84+ Directory that holds the TSM
85+ node's password information.
86+ The value will be used for the
87+ <literal>passworddir</literal>
88+ directive in <filename>dsm.sys</filename>.
89+ '';
90+ };
91+ options.includeExclude = mkOption {
92+ type = lines;
93+ default = "";
94+ example = ''
95+ exclude.dir /nix/store
96+ include.encrypt /home/.../*
97+ '';
98+ description = ''
99+ <literal>include.*</literal> and
100+ <literal>exclude.*</literal> directives to be
101+ used when sending files to the IBM TSM server.
102+ The lines will be written into a file that the
103+ <literal>inclexcl</literal>
104+ directive in <filename>dsm.sys</filename> points to.
105+ '';
106+ };
107+ options.extraConfig = mkOption {
108+ # TSM option keys are case insensitive;
109+ # we have to ensure there are no keys that
110+ # differ only by upper and lower case.
111+ type = addCheck
112+ (attrsOf (nullOr str))
113+ (attrs: checkIUnique (attrNames attrs));
114+ default = {};
115+ example.compression = "yes";
116+ example.passwordaccess = null;
117+ description = ''
118+ Additional key-value pairs for the server stanza.
119+ Values must be strings, or <literal>null</literal>
120+ for the key not to be used in the stanza
121+ (e.g. to overrule values generated by other options).
122+ '';
123+ };
124+ options.text = mkOption {
125+ type = lines;
126+ example = literalExample
127+ ''lib.modules.mkAfter "compression no"'';
128+ description = ''
129+ Additional text lines for the server stanza.
130+ This option can be used if certion configuration keys
131+ must be used multiple times or ordered in a certain way
132+ as the <option>extraConfig</option> option can't
133+ control the order of lines in the resulting stanza.
134+ Note that the <literal>server</literal>
135+ line at the beginning of the stanza is
136+ not part of this option's value.
137+ '';
138+ };
139+ options.stanza = mkOption {
140+ type = str;
141+ internal = true;
142+ visible = false;
143+ description = "Server stanza text generated from the options.";
144+ };
145+ config.name = mkDefault name;
146+ # Client system-options file directives are explained here:
147+ # https://www.ibm.com/support/knowledgecenter/SSEQVQ_8.1.8/client/c_opt_usingopts.html
148+ config.extraConfig =
149+ mapAttrs (lib.trivial.const mkDefault) (
150+ {
151+ commmethod = "v6tcpip"; # uses v4 or v6, based on dns lookup result
152+ tcpserveraddress = config.server;
153+ tcpport = builtins.toString config.port;
154+ nodename = config.node;
155+ passwordaccess = if config.genPasswd then "generate" else "prompt";
156+ passworddir = ''"${config.passwdDir}"'';
157+ } // optionalAttrs (config.includeExclude!="") {
158+ inclexcl = ''"${pkgs.writeText "inclexcl.dsm.sys" config.includeExclude}"'';
159+ }
160+ );
161+ config.text =
162+ let
163+ attrset = filterAttrs (k: v: v!=null) config.extraConfig;
164+ mkLine = k: v: k + optionalString (v!="") " ${v}";
165+ lines = mapAttrsToList mkLine attrset;
166+ in
167+ concatStringsSep "\n" lines;
168+ config.stanza = ''
169+ server ${config.name}
170+ ${config.text}
171+ '';
172+ };
173+174+ options.programs.tsmClient = {
175+ enable = mkEnableOption ''
176+ IBM Spectrum Protect (Tivoli Storage Manager, TSM)
177+ client command line applications with a
178+ client system-options file "dsm.sys"
179+ '';
180+ servers = mkOption {
181+ type = loaOf (submodule [ serverOptions ]);
182+ default = {};
183+ example.mainTsmServer = {
184+ server = "tsmserver.company.com";
185+ node = "MY-TSM-NODE";
186+ extraConfig.compression = "yes";
187+ };
188+ description = ''
189+ Server definitions ("stanzas")
190+ for the client system-options file.
191+ '';
192+ };
193+ defaultServername = mkOption {
194+ type = nullOr servernameType;
195+ default = null;
196+ example = "mainTsmServer";
197+ description = ''
198+ If multiple server stanzas are declared with
199+ <option>programs.tsmClient.servers</option>,
200+ this option may be used to name a default
201+ server stanza that IBM TSM uses in the absence of
202+ a user-defined <filename>dsm.opt</filename> file.
203+ This option translates to a
204+ <literal>defaultserver</literal> configuration line.
205+ '';
206+ };
207+ dsmSysText = mkOption {
208+ type = lines;
209+ readOnly = true;
210+ description = ''
211+ This configuration key contains the effective text
212+ of the client system-options file "dsm.sys".
213+ It should not be changed, but may be
214+ used to feed the configuration into other
215+ TSM-depending packages used on the system.
216+ '';
217+ };
218+ package = mkOption {
219+ type = package;
220+ default = pkgs.tsm-client;
221+ defaultText = "pkgs.tsm-client";
222+ example = literalExample "pkgs.tsm-client-withGui";
223+ description = ''
224+ The TSM client derivation to be
225+ added to the system environment.
226+ It will called with <literal>.override</literal>
227+ to add paths to the client system-options file.
228+ '';
229+ };
230+ wrappedPackage = mkOption {
231+ type = package;
232+ readOnly = true;
233+ description = ''
234+ The TSM client derivation, wrapped with the path
235+ to the client system-options file "dsm.sys".
236+ This option is to provide the effective derivation
237+ for other modules that want to call TSM executables.
238+ '';
239+ };
240+ };
241+242+ cfg = config.programs.tsmClient;
243+244+ assertions = [
245+ {
246+ assertion = checkIUnique (mapAttrsToList (k: v: v.name) cfg.servers);
247+ message = ''
248+ TSM servernames contain duplicate name
249+ (note that case doesn't matter!)
250+ '';
251+ }
252+ {
253+ assertion = (cfg.defaultServername!=null)->(hasAttr cfg.defaultServername cfg.servers);
254+ message = "TSM defaultServername not found in list of servers";
255+ }
256+ ];
257+258+ dsmSysText = ''
259+ **** IBM Spectrum Protect (Tivoli Storage Manager)
260+ **** client system-options file "dsm.sys".
261+ **** Do not edit!
262+ **** This file is generated by NixOS configuration.
263+264+ ${optionalString (cfg.defaultServername!=null) "defaultserver ${cfg.defaultServername}"}
265+266+ ${concatStringsSep "\n" (mapAttrsToList (k: v: v.stanza) cfg.servers)}
267+ '';
268+269+in
270+271+{
272+273+ inherit options;
274+275+ config = mkIf cfg.enable {
276+ inherit assertions;
277+ programs.tsmClient.dsmSysText = dsmSysText;
278+ programs.tsmClient.wrappedPackage = cfg.package.override rec {
279+ dsmSysCli = pkgs.writeText "dsm.sys" cfg.dsmSysText;
280+ dsmSysApi = dsmSysCli;
281+ };
282+ environment.systemPackages = [ cfg.wrappedPackage ];
283+ };
284+285+ meta.maintainers = [ lib.maintainers.yarny ];
286+287+}
···1+{ lib
2+, stdenv
3+, autoPatchelfHook
4+, buildEnv
5+, fetchurl
6+, makeWrapper
7+, procps
8+, zlib
9+# optional packages that enable certain features
10+, acl ? null # EXT2/EXT3/XFS ACL support
11+, jdk8 ? null # Java GUI
12+, lvm2 ? null # LVM image backup and restore functions
13+# path to `dsm.sys` configuration files
14+, dsmSysCli ? "/etc/tsm-client/cli.dsm.sys"
15+, dsmSysApi ? "/etc/tsm-client/api.dsm.sys"
16+}:
17+18+19+# For an explanation of optional packages
20+# (features provided by them, version limits), see
21+# https://www-01.ibm.com/support/docview.wss?uid=swg21052223#Version%208.1
22+23+24+# IBM Tivoli Storage Manager Client uses a system-wide
25+# client system-options file `dsm.sys` and expects it
26+# to be located in a directory within the package.
27+# Note that the command line client and the API use
28+# different "dms.sys" files (located in different directories).
29+# Since these files contain settings to be altered by the
30+# admin user (e.g. TSM server name), we create symlinks
31+# in place of the files that the client attempts to open.
32+# Use the arguments `dsmSysCli` and `dsmSysApi` to
33+# provide the location of the configuration files for
34+# the command-line interface and the API, respectively.
35+#
36+# While the command-line interface contains wrappers
37+# that help the executables find the configuration file,
38+# packages that link against the API have to
39+# set the environment variable `DSMI_DIR` to
40+# point to this derivations `/dsmi_dir` directory symlink.
41+# Other environment variables might be necessary,
42+# depending on local configuration or usage; see:
43+# https://www.ibm.com/support/knowledgecenter/en/SSEQVQ_8.1.8/client/c_cfg_sapiunix.html
44+45+46+# The newest version of TSM client should be discoverable
47+# by going the the `downloadPage` (see `meta` below),
48+# there to "Client Latest Downloads",
49+# "IBM Spectrum Protect Client Downloads and READMEs",
50+# then to "Linux x86_64 Ubuntu client" (as of 2019-07-15).
51+52+53+let
54+55+ meta = {
56+ homepage = https://www.ibm.com/us-en/marketplace/data-protection-and-recovery;
57+ downloadPage = https://www-01.ibm.com/support/docview.wss?uid=swg21239415;
58+ platforms = [ "x86_64-linux" ];
59+ license = lib.licenses.unfree;
60+ maintainers = [ lib.maintainers.yarny ];
61+ description = "IBM Spectrum Protect (Tivoli Storage Manager) CLI and API";
62+ longDescription = ''
63+ IBM Spectrum Protect (Tivoli Storage Manager) provides
64+ a single point of control for backup and recovery.
65+ This package contains the client software, that is,
66+ a command line client and linkable libraries.
67+68+ Note that the software requires a system-wide
69+ client system-options file (commonly named "dsm.sys").
70+ This package allows to use separate files for
71+ the command-line interface and for the linkable API.
72+ The location of those files can
73+ be provided as build parameters.
74+ '';
75+ };
76+77+ unwrapped = stdenv.mkDerivation rec {
78+ name = "tsm-client-${version}-unwrapped";
79+ version = "8.1.8.0";
80+ src = fetchurl {
81+ url = "ftp://public.dhe.ibm.com/storage/tivoli-storage-management/maintenance/client/v8r1/Linux/LinuxX86_DEB/BA/v818/${version}-TIV-TSMBAC-LinuxX86_DEB.tar";
82+ sha256 = "0c1d0jm0i7qjd314nhj2vj8fs7sncm1x2n4d6dg4049jniyvjhpk";
83+ };
84+ inherit meta;
85+86+ nativeBuildInputs = [
87+ autoPatchelfHook
88+ ];
89+ buildInputs = [
90+ stdenv.cc.cc
91+ zlib
92+ ];
93+ runtimeDependencies = [
94+ lvm2
95+ ];
96+ sourceRoot = ".";
97+98+ postUnpack = ''
99+ for debfile in *.deb
100+ do
101+ ar -x "$debfile"
102+ tar --xz --extract --file=data.tar.xz
103+ rm data.tar.xz
104+ done
105+ '';
106+107+ installPhase = ''
108+ runHook preInstall
109+ mkdir --parents $out
110+ mv --target-directory=$out usr/* opt
111+ runHook postInstall
112+ '';
113+114+ # Fix relative symlinks after `/usr` was moved up one level
115+ preFixup = ''
116+ for link in $out/lib/* $out/bin/*
117+ do
118+ target=$(readlink "$link")
119+ if [ "$(cut -b -6 <<< "$target")" != "../../" ]
120+ then
121+ echo "cannot fix this symlink: $link -> $target"
122+ exit 1
123+ fi
124+ ln --symbolic --force --no-target-directory "$out/$(cut -b 7- <<< "$target")" "$link"
125+ done
126+ '';
127+ };
128+129+in
130+131+buildEnv {
132+ name = "tsm-client-${unwrapped.version}";
133+ inherit meta;
134+ passthru = { inherit unwrapped; };
135+ paths = [ unwrapped ];
136+ buildInputs = [ makeWrapper ];
137+ pathsToLink = [
138+ "/"
139+ "/bin"
140+ "/opt/tivoli/tsm/client/ba/bin"
141+ "/opt/tivoli/tsm/client/api/bin64"
142+ ];
143+ # * Provide top-level symlinks `dsm_dir` and `dsmi_dir`
144+ # to the so-called "installation directories"
145+ # * Add symlinks to the "installation directories"
146+ # that point to the `dsm.sys` configuration files
147+ # * Drop the Java GUI executable unless `jdk` is present
148+ # * Create wrappers for the command-line interface to
149+ # prepare `PATH` and `DSM_DIR` environment variables
150+ postBuild = ''
151+ ln --symbolic --no-target-directory opt/tivoli/tsm/client/ba/bin $out/dsm_dir
152+ ln --symbolic --no-target-directory opt/tivoli/tsm/client/api/bin64 $out/dsmi_dir
153+ ln --symbolic --no-target-directory "${dsmSysCli}" $out/dsm_dir/dsm.sys
154+ ln --symbolic --no-target-directory "${dsmSysApi}" $out/dsmi_dir/dsm.sys
155+ ${lib.strings.optionalString (jdk8==null) "rm $out/bin/dsmj"}
156+ for bin in $out/bin/*
157+ do
158+ target=$(readlink "$bin")
159+ rm "$bin"
160+ makeWrapper "$target" "$bin" \
161+ --prefix PATH : "$out/dsm_dir:${lib.strings.makeBinPath [ procps acl jdk8 ]}" \
162+ --set DSM_DIR $out/dsm_dir
163+ done
164+ '';
165+}