···
1
-
{ config, lib, pkgs, ... }:
1
+
{ config, lib, options, pkgs, ... }: # XXX migration code for freeform settings: `options` can be removed in 2025
2
+
let optionsGlobal = options; in
5
-
inherit (builtins) length map;
6
-
inherit (lib.attrsets) attrNames filterAttrs hasAttr mapAttrs mapAttrsToList optionalAttrs;
6
+
inherit (lib.attrsets) attrNames attrValues mapAttrsToList removeAttrs;
7
+
inherit (lib.lists) all allUnique concatLists elem isList map;
inherit (lib.modules) mkDefault mkIf;
8
-
inherit (lib.options) literalExpression mkEnableOption mkOption mkPackageOption;
9
-
inherit (lib.strings) concatLines optionalString toLower;
10
-
inherit (lib.types) addCheck attrsOf lines nonEmptyStr nullOr package path port str strMatching submodule;
9
+
inherit (lib.options) mkEnableOption mkOption mkPackageOption;
10
+
inherit (lib.strings) concatLines match optionalString toLower;
11
+
inherit (lib.trivial) isInt;
12
+
inherit (lib.types) addCheck attrsOf coercedTo either enum int lines listOf nonEmptyStr nullOr oneOf path port singleLineStr strMatching submodule;
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
18
-
lenUniq = l: length (lib.lists.unique l);
20
-
lenUniq lst == lenUniq (map toLower lst);
15
+
# see the option's description below for the
16
+
# handling/transformation of each possible type
17
+
oneOf [ (enum [ true null ]) int path singleLineStr ];
# TSM rejects servername strings longer than 64 chars.
23
-
servernameType = strMatching ".{1,64}";
20
+
servernameType = strMatching "[^[:space:]]{1,64}";
serverOptions = { name, config, ... }: {
26
-
options.name = mkOption {
23
+
freeformType = attrsOf (either scalarType (listOf scalarType));
24
+
# Client system-options file directives are explained here:
25
+
# https://www.ibm.com/docs/en/storage-protect/8.1.20?topic=commands-processing-options
26
+
options.servername = mkOption {
example = "mainTsmServer";
description = lib.mdDoc ''
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
34
-
directive in {file}`dsm.sys`.
32
+
must not contain space or more than 64 chars.
37
-
options.server = mkOption {
35
+
options.tcpserveraddress = mkOption {
example = "tsmserver.company.com";
description = lib.mdDoc ''
Host/domain name or IP address of the IBM TSM server.
42
-
The value will be used for the
44
-
directive in {file}`dsm.sys`.
47
-
options.port = mkOption {
42
+
options.tcpport = mkOption {
type = addCheck port (p: p<=32767);
default = 1500; # official default
description = lib.mdDoc ''
TCP port of the IBM TSM server.
52
-
The value will be used for the
54
-
directive in {file}`dsm.sys`.
TSM does not support ports above 32767.
58
-
options.node = mkOption {
50
+
options.nodename = mkOption {
description = lib.mdDoc ''
Target node name on the IBM TSM server.
63
-
The value will be used for the
65
-
directive in {file}`dsm.sys`.
options.genPasswd = mkEnableOption (lib.mdDoc ''
automatic client password generation.
70
-
This option influences the
72
-
directive in {file}`dsm.sys`.
59
+
This option does *not* cause a line in
60
+
{file}`dsm.sys` by itself, but generates a
61
+
corresponding `passwordaccess` directive.
The password will be stored in the directory
74
-
given by the option {option}`passwdDir`.
63
+
given by the option {option}`passworddir`.
If this option is enabled and the server forces
to renew the password (e.g. on first connection),
a random password will be generated and stored
80
-
options.passwdDir = mkOption {
69
+
options.passwordaccess = mkOption {
70
+
type = enum [ "generate" "prompt" ];
73
+
options.passworddir = mkOption {
example = "/home/alice/tsm-password";
description = lib.mdDoc ''
Directory that holds the TSM
node's password information.
86
-
The value will be used for the
88
-
directive in {file}`dsm.sys`.
91
-
options.includeExclude = mkOption {
82
+
options.inclexcl = mkOption {
83
+
type = coercedTo lines
84
+
(pkgs.writeText "inclexcl.dsm.sys")
include.encrypt /home/.../*
description = lib.mdDoc ''
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
104
-
directive in {file}`dsm.sys` points to.
92
+
Text lines with `include.*` and `exclude.*` directives
93
+
to be used when sending files to the IBM TSM server,
94
+
or an absolute path pointing to a file with such lines.
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.
112
-
(attrsOf (nullOr str))
113
-
(attrs: checkIUnique (attrNames attrs));
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).
124
-
options.text = mkOption {
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.
139
-
options.stanza = mkOption {
143
-
description = lib.mdDoc "Server stanza text generated from the options.";
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) (
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}"'';
163
-
attrset = filterAttrs (k: v: v!=null) config.extraConfig;
164
-
mkLine = k: v: k + optionalString (v!="") " ${v}";
165
-
lines = mapAttrsToList mkLine attrset;
169
-
server ${config.name}
97
+
config.commmethod = mkDefault "v6tcpip"; # uses v4 or v6, based on dns lookup result
98
+
config.passwordaccess = if config.genPasswd then "generate" else "prompt";
99
+
# XXX migration code for freeform settings, these can be removed in 2025:
100
+
options.warnings = optionsGlobal.warnings;
101
+
options.assertions = optionsGlobal.assertions;
102
+
imports = let inherit (lib.modules) mkRemovedOptionModule mkRenamedOptionModule; in [
103
+
(mkRemovedOptionModule [ "extraConfig" ] "Please just add options directly to the server attribute set, cf. the description of `programs.tsmClient.servers`.")
104
+
(mkRemovedOptionModule [ "text" ] "Please just add options directly to the server attribute set, cf. the description of `programs.tsmClient.servers`.")
105
+
(mkRenamedOptionModule [ "name" ] [ "servername" ])
106
+
(mkRenamedOptionModule [ "server" ] [ "tcpserveraddress" ])
107
+
(mkRenamedOptionModule [ "port" ] [ "tcpport" ])
108
+
(mkRenamedOptionModule [ "node" ] [ "nodename" ])
109
+
(mkRenamedOptionModule [ "passwdDir" ] [ "passworddir" ])
110
+
(mkRenamedOptionModule [ "includeExclude" ] [ "inclexcl" ])
options.programs.tsmClient = {
enable = mkEnableOption (lib.mdDoc ''
176
-
IBM Spectrum Protect (Tivoli Storage Manager, TSM)
116
+
IBM Storage Protect (Tivoli Storage Manager, TSM)
client command line applications with a
client system-options file "dsm.sys"
181
-
type = attrsOf (submodule [ serverOptions ]);
121
+
type = attrsOf (submodule serverOptions);
example.mainTsmServer = {
184
-
server = "tsmserver.company.com";
185
-
node = "MY-TSM-NODE";
186
-
extraConfig.compression = "yes";
124
+
tcpserveraddress = "tsmserver.company.com";
125
+
nodename = "MY-TSM-NODE";
126
+
compression = "yes";
description = lib.mdDoc ''
Server definitions ("stanzas")
for the client system-options file.
131
+
The name of each entry will be used for
132
+
the internal `servername` by default.
133
+
Each attribute will be transformed into a line
134
+
with a key-value pair within the server's stanza.
135
+
Integers as values will be
136
+
canonically turned into strings.
137
+
The boolean value `true` will be turned
138
+
into a line with just the attribute's name.
139
+
The value `null` will not generate a line.
140
+
A list as values generates an entry for
141
+
each value, according to the rules above.
defaultServername = mkOption {
···
to add paths to the client system-options file.
225
-
wrappedPackage = mkOption {
228
-
description = lib.mdDoc ''
229
-
The TSM client derivation, wrapped with the path
230
-
to the client system-options file "dsm.sys".
231
-
This option is to provide the effective derivation
176
+
wrappedPackage = mkPackageOption pkgs "tsm-client" {
178
+
extraDescription = ''
179
+
This option is to provide the effective derivation,
180
+
wrapped with the path to the
181
+
client system-options file "dsm.sys".
182
+
It should not be changed, but exists
for other modules that want to call TSM executables.
185
+
} // { readOnly = true; };
cfg = config.programs.tsmClient;
189
+
servernames = map (s: s.servername) (attrValues cfg.servers);
241
-
assertion = checkIUnique (mapAttrsToList (k: v: v.name) cfg.servers);
194
+
assertion = allUnique (map toLower servernames);
197
+
(option `programs.tsmClient.servers`)
198
+
contain duplicate name
199
+
(note that server names are case insensitive).
203
+
assertion = (cfg.defaultServername!=null)->(elem cfg.defaultServername servernames);
205
+
TSM default server name
206
+
`programs.tsmClient.defaultServername="${cfg.defaultServername}"`
207
+
not found in server names in
208
+
`programs.tsmClient.servers`.
211
+
] ++ (mapAttrsToList (name: serverCfg: {
212
+
assertion = all (key: null != match "[^[:space:]]+" key) (attrNames serverCfg);
214
+
TSM server setting names in
215
+
`programs.tsmClient.servers.${name}.*`
216
+
contain spaces, but that's not allowed.
218
+
}) cfg.servers) ++ (mapAttrsToList (name: serverCfg: {
219
+
assertion = allUnique (map toLower (attrNames serverCfg));
243
-
TSM servernames contain duplicate name
244
-
(note that case doesn't matter!)
221
+
TSM server setting names in
222
+
`programs.tsmClient.servers.${name}.*`
223
+
contain duplicate names
224
+
(note that setting names are case insensitive).
248
-
assertion = (cfg.defaultServername!=null)->(hasAttr cfg.defaultServername cfg.servers);
249
-
message = "TSM defaultServername not found in list of servers";
227
+
# XXX migration code for freeform settings, this can be removed in 2025:
228
+
++ (enrichMigrationInfos "assertions" (addText: { assertion, message }: { inherit assertion; message = addText message; }));
230
+
makeDsmSysLines = key: value:
231
+
# Turn a key-value pair from the server options attrset
232
+
# into zero (value==null), one (scalar value) or
233
+
# more (value is list) configuration stanza lines.
234
+
if isList value then map (makeDsmSysLines key) value else # recurse into list
235
+
if value == null then [ ] else # skip `null` value
237
+
if value == true then "" else # just output key if value is `true`
238
+
if isInt value then " ${builtins.toString value}" else
239
+
if path.check value then " \"${value}\"" else # enclose path in ".."
240
+
if singleLineStr.check value then " ${value}" else
241
+
throw "assertion failed: cannot convert type" # should never happen
244
+
makeDsmSysStanza = {servername, ... }@serverCfg:
246
+
# drop special values that should not go into server config block
247
+
attrs = removeAttrs serverCfg [ "servername" "genPasswd"
248
+
# XXX migration code for freeform settings, these can be removed in 2025:
249
+
"assertions" "warnings"
250
+
"extraConfig" "text"
251
+
"name" "server" "port" "node" "passwdDir" "includeExclude"
255
+
servername ${servername}
256
+
${concatLines (concatLists (mapAttrsToList makeDsmSysLines attrs))}
254
-
**** IBM Spectrum Protect (Tivoli Storage Manager)
260
+
**** IBM Storage Protect (Tivoli Storage Manager)
**** client system-options file "dsm.sys".
**** This file is generated by NixOS configuration.
${optionalString (cfg.defaultServername!=null) "defaultserver ${cfg.defaultServername}"}
261
-
${concatLines (mapAttrsToList (k: v: v.stanza) cfg.servers)}
267
+
${concatLines (map makeDsmSysStanza (attrValues cfg.servers))}
270
+
# XXX migration code for freeform settings, this can be removed in 2025:
271
+
enrichMigrationInfos = what: how: concatLists (
273
+
(name: serverCfg: map (how (text: "In `programs.tsmClient.servers.${name}`: ${text}")) serverCfg."${what}")
···
environment.systemPackages = [ cfg.wrappedPackage ];
291
+
# XXX migration code for freeform settings, this can be removed in 2025:
292
+
warnings = enrichMigrationInfos "warnings" (addText: addText);
meta.maintainers = [ lib.maintainers.yarny ];