1{
2 config,
3 lib,
4 options,
5 pkgs,
6 ...
7}: # XXX migration code for freeform settings: `options` can be removed in 2025
8let
9 optionsGlobal = options;
10in
11
12let
13
14 inherit (lib.attrsets)
15 attrNames
16 attrValues
17 mapAttrsToList
18 removeAttrs
19 ;
20 inherit (lib.lists)
21 all
22 allUnique
23 concatLists
24 concatMap
25 elem
26 isList
27 map
28 ;
29 inherit (lib.modules) mkDefault mkIf;
30 inherit (lib.options) mkEnableOption mkOption mkPackageOption;
31 inherit (lib.strings)
32 concatLines
33 match
34 optionalString
35 toLower
36 ;
37 inherit (lib.trivial) isInt;
38 inherit (lib.types)
39 addCheck
40 attrsOf
41 coercedTo
42 either
43 enum
44 int
45 lines
46 listOf
47 nonEmptyStr
48 nullOr
49 oneOf
50 path
51 port
52 singleLineStr
53 strMatching
54 submodule
55 ;
56
57 scalarType =
58 # see the option's description below for the
59 # handling/transformation of each possible type
60 oneOf [
61 (enum [
62 true
63 null
64 ])
65 int
66 path
67 singleLineStr
68 ];
69
70 # TSM rejects servername strings longer than 64 chars.
71 servernameType = strMatching "[^[:space:]]{1,64}";
72
73 serverOptions =
74 { name, config, ... }:
75 {
76 freeformType = attrsOf (either scalarType (listOf scalarType));
77 # Client system-options file directives are explained here:
78 # https://www.ibm.com/docs/en/storage-protect/8.1.25?topic=commands-processing-options
79 options.servername = mkOption {
80 type = servernameType;
81 default = name;
82 example = "mainTsmServer";
83 description = ''
84 Local name of the IBM TSM server,
85 must not contain space or more than 64 chars.
86 '';
87 };
88 options.tcpserveraddress = mkOption {
89 type = nonEmptyStr;
90 example = "tsmserver.company.com";
91 description = ''
92 Host/domain name or IP address of the IBM TSM server.
93 '';
94 };
95 options.tcpport = mkOption {
96 type = addCheck port (p: p <= 32767);
97 default = 1500; # official default
98 description = ''
99 TCP port of the IBM TSM server.
100 TSM does not support ports above 32767.
101 '';
102 };
103 options.nodename = mkOption {
104 type = nonEmptyStr;
105 example = "MY-TSM-NODE";
106 description = ''
107 Target node name on the IBM TSM server.
108 '';
109 };
110 options.genPasswd = mkEnableOption ''
111 automatic client password generation.
112 This option does *not* cause a line in
113 {file}`dsm.sys` by itself, but generates a
114 corresponding `passwordaccess` directive.
115 The password will be stored in the directory
116 given by the option {option}`passworddir`.
117 *Caution*:
118 If this option is enabled and the server forces
119 to renew the password (e.g. on first connection),
120 a random password will be generated and stored
121 '';
122 options.passwordaccess = mkOption {
123 type = enum [
124 "generate"
125 "prompt"
126 ];
127 visible = false;
128 };
129 options.passworddir = mkOption {
130 type = nullOr path;
131 default = null;
132 example = "/home/alice/tsm-password";
133 description = ''
134 Directory that holds the TSM
135 node's password information.
136 '';
137 };
138 options.inclexcl = mkOption {
139 type = coercedTo lines (pkgs.writeText "inclexcl.dsm.sys") (nullOr path);
140 default = null;
141 example = ''
142 exclude.dir /nix/store
143 include.encrypt /home/.../*
144 '';
145 description = ''
146 Text lines with `include.*` and `exclude.*` directives
147 to be used when sending files to the IBM TSM server,
148 or an absolute path pointing to a file with such lines.
149 '';
150 };
151 config.commmethod = mkDefault "v6tcpip"; # uses v4 or v6, based on dns lookup result
152 config.passwordaccess = if config.genPasswd then "generate" else "prompt";
153 # XXX migration code for freeform settings, these can be removed in 2025:
154 options.warnings = optionsGlobal.warnings;
155 options.assertions = optionsGlobal.assertions;
156 imports =
157 let
158 inherit (lib.modules) mkRemovedOptionModule mkRenamedOptionModule;
159 in
160 [
161 (mkRemovedOptionModule [ "extraConfig" ]
162 "Please just add options directly to the server attribute set, cf. the description of `programs.tsmClient.servers`."
163 )
164 (mkRemovedOptionModule [ "text" ]
165 "Please just add options directly to the server attribute set, cf. the description of `programs.tsmClient.servers`."
166 )
167 (mkRenamedOptionModule [ "name" ] [ "servername" ])
168 (mkRenamedOptionModule [ "server" ] [ "tcpserveraddress" ])
169 (mkRenamedOptionModule [ "port" ] [ "tcpport" ])
170 (mkRenamedOptionModule [ "node" ] [ "nodename" ])
171 (mkRenamedOptionModule [ "passwdDir" ] [ "passworddir" ])
172 (mkRenamedOptionModule [ "includeExclude" ] [ "inclexcl" ])
173 ];
174 };
175
176 options.programs.tsmClient = {
177 enable = mkEnableOption ''
178 IBM Storage Protect (Tivoli Storage Manager, TSM)
179 client command line applications with a
180 client system-options file "dsm.sys"
181 '';
182 servers = mkOption {
183 type = attrsOf (submodule serverOptions);
184 default = { };
185 example.mainTsmServer = {
186 tcpserveraddress = "tsmserver.company.com";
187 nodename = "MY-TSM-NODE";
188 compression = "yes";
189 };
190 description = ''
191 Server definitions ("stanzas")
192 for the client system-options file.
193 The name of each entry will be used for
194 the internal `servername` by default.
195 Each attribute will be transformed into a line
196 with a key-value pair within the server's stanza.
197 Integers as values will be
198 canonically turned into strings.
199 The boolean value `true` will be turned
200 into a line with just the attribute's name.
201 The value `null` will not generate a line.
202 A list as values generates an entry for
203 each value, according to the rules above.
204 '';
205 };
206 defaultServername = mkOption {
207 type = nullOr servernameType;
208 default = null;
209 example = "mainTsmServer";
210 description = ''
211 If multiple server stanzas are declared with
212 {option}`programs.tsmClient.servers`,
213 this option may be used to name a default
214 server stanza that IBM TSM uses in the absence of
215 a user-defined {file}`dsm.opt` file.
216 This option translates to a
217 `defaultserver` configuration line.
218 '';
219 };
220 dsmSysText = mkOption {
221 type = lines;
222 readOnly = true;
223 description = ''
224 This configuration key contains the effective text
225 of the client system-options file "dsm.sys".
226 It should not be changed, but may be
227 used to feed the configuration into other
228 TSM-depending packages used on the system.
229 '';
230 };
231 package = mkPackageOption pkgs "tsm-client" {
232 example = "tsm-client-withGui";
233 extraDescription = ''
234 It will be used with `.override`
235 to add paths to the client system-options file.
236 '';
237 };
238 wrappedPackage =
239 mkPackageOption pkgs "tsm-client" {
240 default = null;
241 extraDescription = ''
242 This option is to provide the effective derivation,
243 wrapped with the path to the
244 client system-options file "dsm.sys".
245 It should not be changed, but exists
246 for other modules that want to call TSM executables.
247 '';
248 }
249 // {
250 readOnly = true;
251 };
252 };
253
254 cfg = config.programs.tsmClient;
255 servernames = map (s: s.servername) (attrValues cfg.servers);
256
257 assertions =
258 [
259 {
260 assertion = allUnique (map toLower servernames);
261 message = ''
262 TSM server names
263 (option `programs.tsmClient.servers`)
264 contain duplicate name
265 (note that server names are case insensitive).
266 '';
267 }
268 {
269 assertion = (cfg.defaultServername != null) -> (elem cfg.defaultServername servernames);
270 message = ''
271 TSM default server name
272 `programs.tsmClient.defaultServername="${cfg.defaultServername}"`
273 not found in server names in
274 `programs.tsmClient.servers`.
275 '';
276 }
277 ]
278 ++ (mapAttrsToList (name: serverCfg: {
279 assertion = all (key: null != match "[^[:space:]]+" key) (attrNames serverCfg);
280 message = ''
281 TSM server setting names in
282 `programs.tsmClient.servers.${name}.*`
283 contain spaces, but that's not allowed.
284 '';
285 }) cfg.servers)
286 ++ (mapAttrsToList (name: serverCfg: {
287 assertion = allUnique (map toLower (attrNames serverCfg));
288 message = ''
289 TSM server setting names in
290 `programs.tsmClient.servers.${name}.*`
291 contain duplicate names
292 (note that setting names are case insensitive).
293 '';
294 }) cfg.servers)
295 # XXX migration code for freeform settings, this can be removed in 2025:
296 ++ (enrichMigrationInfos "assertions" (
297 addText:
298 { assertion, message }:
299 {
300 inherit assertion;
301 message = addText message;
302 }
303 ));
304
305 makeDsmSysLines =
306 key: value:
307 # Turn a key-value pair from the server options attrset
308 # into zero (value==null), one (scalar value) or
309 # more (value is list) configuration stanza lines.
310 if isList value then
311 concatMap (makeDsmSysLines key) value
312 # recurse into list
313 else if value == null then
314 [ ]
315 # skip `null` value
316 else
317 [
318 (
319 " ${key}${
320 if value == true then
321 ""
322 # just output key if value is `true`
323 else if isInt value then
324 " ${builtins.toString value}"
325 else if path.check value then
326 " \"${value}\""
327 # enclose path in ".."
328 else if singleLineStr.check value then
329 " ${value}"
330 else
331 throw "assertion failed: cannot convert type" # should never happen
332 }"
333 )
334 ];
335
336 makeDsmSysStanza =
337 { servername, ... }@serverCfg:
338 let
339 # drop special values that should not go into server config block
340 attrs = removeAttrs serverCfg [
341 "servername"
342 "genPasswd"
343 # XXX migration code for freeform settings, these can be removed in 2025:
344 "assertions"
345 "warnings"
346 "extraConfig"
347 "text"
348 "name"
349 "server"
350 "port"
351 "node"
352 "passwdDir"
353 "includeExclude"
354 ];
355 in
356 ''
357 servername ${servername}
358 ${concatLines (concatLists (mapAttrsToList makeDsmSysLines attrs))}
359 '';
360
361 dsmSysText = ''
362 **** IBM Storage Protect (Tivoli Storage Manager)
363 **** client system-options file "dsm.sys".
364 **** Do not edit!
365 **** This file is generated by NixOS configuration.
366
367 ${optionalString (cfg.defaultServername != null) "defaultserver ${cfg.defaultServername}"}
368
369 ${concatLines (map makeDsmSysStanza (attrValues cfg.servers))}
370 '';
371
372 # XXX migration code for freeform settings, this can be removed in 2025:
373 enrichMigrationInfos =
374 what: how:
375 concatLists (
376 mapAttrsToList (
377 name: serverCfg:
378 map (how (text: "In `programs.tsmClient.servers.${name}`: ${text}")) serverCfg."${what}"
379 ) cfg.servers
380 );
381
382in
383
384{
385
386 inherit options;
387
388 config = mkIf cfg.enable {
389 inherit assertions;
390 programs.tsmClient.dsmSysText = dsmSysText;
391 programs.tsmClient.wrappedPackage = cfg.package.override rec {
392 dsmSysCli = pkgs.writeText "dsm.sys" cfg.dsmSysText;
393 dsmSysApi = dsmSysCli;
394 };
395 environment.systemPackages = [ cfg.wrappedPackage ];
396 # XXX migration code for freeform settings, this can be removed in 2025:
397 warnings = enrichMigrationInfos "warnings" (addText: addText);
398 };
399
400 meta.maintainers = [ lib.maintainers.yarny ];
401
402}