{ config, lib, pkgs, ... }: let inherit (builtins) length map; inherit (lib.attrsets) attrNames filterAttrs hasAttr mapAttrs mapAttrsToList optionalAttrs; inherit (lib.modules) mkDefault mkIf; inherit (lib.options) literalExample mkEnableOption mkOption; inherit (lib.strings) concatStringsSep optionalString toLower; inherit (lib.types) addCheck attrsOf lines nullOr package path port str strMatching submodule; # Checks if given list of strings contains unique # elements when compared without considering case. # Type: checkIUnique :: [string] -> bool # Example: checkIUnique ["foo" "Foo"] => false checkIUnique = lst: let lenUniq = l: length (lib.lists.unique l); in lenUniq lst == lenUniq (map toLower lst); # TSM rejects servername strings longer than 64 chars. servernameType = strMatching ".{1,64}"; serverOptions = { name, config, ... }: { options.name = mkOption { type = servernameType; example = "mainTsmServer"; description = '' Local name of the IBM TSM server, must be uncapitalized and no longer than 64 chars. The value will be used for the <literal>server</literal> directive in <filename>dsm.sys</filename>. ''; }; options.server = mkOption { type = strMatching ".+"; example = "tsmserver.company.com"; description = '' Host/domain name or IP address of the IBM TSM server. The value will be used for the <literal>tcpserveraddress</literal> directive in <filename>dsm.sys</filename>. ''; }; options.port = mkOption { type = addCheck port (p: p<=32767); default = 1500; # official default description = '' TCP port of the IBM TSM server. The value will be used for the <literal>tcpport</literal> directive in <filename>dsm.sys</filename>. TSM does not support ports above 32767. ''; }; options.node = mkOption { type = strMatching ".+"; example = "MY-TSM-NODE"; description = '' Target node name on the IBM TSM server. The value will be used for the <literal>nodename</literal> directive in <filename>dsm.sys</filename>. ''; }; options.genPasswd = mkEnableOption '' automatic client password generation. This option influences the <literal>passwordaccess</literal> directive in <filename>dsm.sys</filename>. The password will be stored in the directory given by the option <option>passwdDir</option>. <emphasis>Caution</emphasis>: 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 ''; options.passwdDir = mkOption { type = path; example = "/home/alice/tsm-password"; description = '' Directory that holds the TSM node's password information. The value will be used for the <literal>passworddir</literal> directive in <filename>dsm.sys</filename>. ''; }; options.includeExclude = mkOption { type = lines; default = ""; example = '' exclude.dir /nix/store include.encrypt /home/.../* ''; description = '' <literal>include.*</literal> and <literal>exclude.*</literal> directives to be used when sending files to the IBM TSM server. The lines will be written into a file that the <literal>inclexcl</literal> directive in <filename>dsm.sys</filename> points to. ''; }; options.extraConfig = mkOption { # TSM option keys are case insensitive; # we have to ensure there are no keys that # differ only by upper and lower case. type = addCheck (attrsOf (nullOr str)) (attrs: checkIUnique (attrNames attrs)); default = {}; example.compression = "yes"; example.passwordaccess = null; description = '' Additional key-value pairs for the server stanza. Values must be strings, or <literal>null</literal> for the key not to be used in the stanza (e.g. to overrule values generated by other options). ''; }; options.text = mkOption { type = lines; example = literalExample ''lib.modules.mkAfter "compression no"''; description = '' Additional text lines for the server stanza. This option can be used if certion configuration keys must be used multiple times or ordered in a certain way as the <option>extraConfig</option> option can't control the order of lines in the resulting stanza. Note that the <literal>server</literal> line at the beginning of the stanza is not part of this option's value. ''; }; options.stanza = mkOption { type = str; internal = true; visible = false; description = "Server stanza text generated from the options."; }; config.name = mkDefault name; # Client system-options file directives are explained here: # https://www.ibm.com/support/knowledgecenter/SSEQVQ_8.1.8/client/c_opt_usingopts.html config.extraConfig = mapAttrs (lib.trivial.const mkDefault) ( { commmethod = "v6tcpip"; # uses v4 or v6, based on dns lookup result tcpserveraddress = config.server; tcpport = builtins.toString config.port; nodename = config.node; passwordaccess = if config.genPasswd then "generate" else "prompt"; passworddir = ''"${config.passwdDir}"''; } // optionalAttrs (config.includeExclude!="") { inclexcl = ''"${pkgs.writeText "inclexcl.dsm.sys" config.includeExclude}"''; } ); config.text = let attrset = filterAttrs (k: v: v!=null) config.extraConfig; mkLine = k: v: k + optionalString (v!="") " ${v}"; lines = mapAttrsToList mkLine attrset; in concatStringsSep "\n" lines; config.stanza = '' server ${config.name} ${config.text} ''; }; options.programs.tsmClient = { enable = mkEnableOption '' IBM Spectrum Protect (Tivoli Storage Manager, TSM) client command line applications with a client system-options file "dsm.sys" ''; servers = mkOption { type = attrsOf (submodule [ serverOptions ]); default = {}; example.mainTsmServer = { server = "tsmserver.company.com"; node = "MY-TSM-NODE"; extraConfig.compression = "yes"; }; description = '' Server definitions ("stanzas") for the client system-options file. ''; }; defaultServername = mkOption { type = nullOr servernameType; default = null; example = "mainTsmServer"; description = '' If multiple server stanzas are declared with <option>programs.tsmClient.servers</option>, this option may be used to name a default server stanza that IBM TSM uses in the absence of a user-defined <filename>dsm.opt</filename> file. This option translates to a <literal>defaultserver</literal> configuration line. ''; }; dsmSysText = mkOption { type = lines; readOnly = true; description = '' This configuration key contains the effective text of the client system-options file "dsm.sys". It should not be changed, but may be used to feed the configuration into other TSM-depending packages used on the system. ''; }; package = mkOption { type = package; default = pkgs.tsm-client; defaultText = "pkgs.tsm-client"; example = literalExample "pkgs.tsm-client-withGui"; description = '' The TSM client derivation to be added to the system environment. It will called with <literal>.override</literal> to add paths to the client system-options file. ''; }; wrappedPackage = mkOption { type = package; readOnly = true; description = '' The TSM client derivation, wrapped with the path to the client system-options file "dsm.sys". This option is to provide the effective derivation for other modules that want to call TSM executables. ''; }; }; cfg = config.programs.tsmClient; assertions = [ { assertion = checkIUnique (mapAttrsToList (k: v: v.name) cfg.servers); message = '' TSM servernames contain duplicate name (note that case doesn't matter!) ''; } { assertion = (cfg.defaultServername!=null)->(hasAttr cfg.defaultServername cfg.servers); message = "TSM defaultServername not found in list of servers"; } ]; dsmSysText = '' **** IBM Spectrum Protect (Tivoli Storage Manager) **** client system-options file "dsm.sys". **** Do not edit! **** This file is generated by NixOS configuration. ${optionalString (cfg.defaultServername!=null) "defaultserver ${cfg.defaultServername}"} ${concatStringsSep "\n" (mapAttrsToList (k: v: v.stanza) cfg.servers)} ''; in { inherit options; config = mkIf cfg.enable { inherit assertions; programs.tsmClient.dsmSysText = dsmSysText; programs.tsmClient.wrappedPackage = cfg.package.override rec { dsmSysCli = pkgs.writeText "dsm.sys" cfg.dsmSysText; dsmSysApi = dsmSysCli; }; environment.systemPackages = [ cfg.wrappedPackage ]; }; meta.maintainers = [ lib.maintainers.yarny ]; }