1{ config, lib, pkgs, ... }:
2
3let
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) literalExpression mkEnableOption mkOption;
9 inherit (lib.strings) concatLines optionalString toLower;
10 inherit (lib.types) addCheck attrsOf lines nonEmptyStr 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 = lib.mdDoc ''
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 `server`
34 directive in {file}`dsm.sys`.
35 '';
36 };
37 options.server = mkOption {
38 type = nonEmptyStr;
39 example = "tsmserver.company.com";
40 description = lib.mdDoc ''
41 Host/domain name or IP address of the IBM TSM server.
42 The value will be used for the
43 `tcpserveraddress`
44 directive in {file}`dsm.sys`.
45 '';
46 };
47 options.port = mkOption {
48 type = addCheck port (p: p<=32767);
49 default = 1500; # official default
50 description = lib.mdDoc ''
51 TCP port of the IBM TSM server.
52 The value will be used for the
53 `tcpport`
54 directive in {file}`dsm.sys`.
55 TSM does not support ports above 32767.
56 '';
57 };
58 options.node = mkOption {
59 type = nonEmptyStr;
60 example = "MY-TSM-NODE";
61 description = lib.mdDoc ''
62 Target node name on the IBM TSM server.
63 The value will be used for the
64 `nodename`
65 directive in {file}`dsm.sys`.
66 '';
67 };
68 options.genPasswd = mkEnableOption (lib.mdDoc ''
69 automatic client password generation.
70 This option influences the
71 `passwordaccess`
72 directive in {file}`dsm.sys`.
73 The password will be stored in the directory
74 given by the option {option}`passwdDir`.
75 *Caution*:
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 = lib.mdDoc ''
84 Directory that holds the TSM
85 node's password information.
86 The value will be used for the
87 `passworddir`
88 directive in {file}`dsm.sys`.
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 = lib.mdDoc ''
99 `include.*` and
100 `exclude.*` 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 `inclexcl`
104 directive in {file}`dsm.sys` 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 = lib.mdDoc ''
118 Additional key-value pairs for the server stanza.
119 Values must be strings, or `null`
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 = literalExpression
127 ''lib.modules.mkAfter "compression no"'';
128 description = lib.mdDoc ''
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 can't
133 control the order of lines in the resulting stanza.
134 Note that the `server`
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 = lib.mdDoc "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/docs/en/spectrum-protect/8.1.13?topic=commands-processing-options
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 concatLines lines;
168 config.stanza = ''
169 server ${config.name}
170 ${config.text}
171 '';
172 };
173
174 options.programs.tsmClient = {
175 enable = mkEnableOption (lib.mdDoc ''
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 = attrsOf (submodule [ serverOptions ]);
182 default = {};
183 example.mainTsmServer = {
184 server = "tsmserver.company.com";
185 node = "MY-TSM-NODE";
186 extraConfig.compression = "yes";
187 };
188 description = lib.mdDoc ''
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 = lib.mdDoc ''
198 If multiple server stanzas are declared with
199 {option}`programs.tsmClient.servers`,
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 {file}`dsm.opt` file.
203 This option translates to a
204 `defaultserver` configuration line.
205 '';
206 };
207 dsmSysText = mkOption {
208 type = lines;
209 readOnly = true;
210 description = lib.mdDoc ''
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 = literalExpression "pkgs.tsm-client";
222 example = literalExpression "pkgs.tsm-client-withGui";
223 description = lib.mdDoc ''
224 The TSM client derivation to be
225 added to the system environment.
226 It will be used with `.override`
227 to add paths to the client system-options file.
228 '';
229 };
230 wrappedPackage = mkOption {
231 type = package;
232 readOnly = true;
233 description = lib.mdDoc ''
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 ${concatLines (mapAttrsToList (k: v: v.stanza) cfg.servers)}
267 '';
268
269in
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}