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