diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 1d1995eda25a..4ce1bb6cd488 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -138,6 +138,7 @@
./programs/sway.nix
./programs/thefuck.nix
./programs/tmux.nix
+ ./programs/tsm-client.nix
./programs/udevil.nix
./programs/venus.nix
./programs/vim.nix
diff --git a/nixos/modules/programs/tsm-client.nix b/nixos/modules/programs/tsm-client.nix
new file mode 100644
index 000000000000..eb6f12475286
--- /dev/null
+++ b/nixos/modules/programs/tsm-client.nix
@@ -0,0 +1,287 @@
+{ 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 loaOf 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
+ server
+ directive in dsm.sys.
+ '';
+ };
+ 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
+ tcpserveraddress
+ directive in dsm.sys.
+ '';
+ };
+ 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
+ tcpport
+ directive in dsm.sys.
+ 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
+ nodename
+ directive in dsm.sys.
+ '';
+ };
+ options.genPasswd = mkEnableOption ''
+ automatic client password generation.
+ This option influences the
+ passwordaccess
+ directive in dsm.sys.
+ The password will be stored in the directory
+ given by the option .
+ Caution:
+ 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
+ passworddir
+ directive in dsm.sys.
+ '';
+ };
+ options.includeExclude = mkOption {
+ type = lines;
+ default = "";
+ example = ''
+ exclude.dir /nix/store
+ include.encrypt /home/.../*
+ '';
+ description = ''
+ include.* and
+ exclude.* directives to be
+ used when sending files to the IBM TSM server.
+ The lines will be written into a file that the
+ inclexcl
+ directive in dsm.sys 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 null
+ 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 can't
+ control the order of lines in the resulting stanza.
+ Note that the server
+ 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 = loaOf (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
+ ,
+ this option may be used to name a default
+ server stanza that IBM TSM uses in the absence of
+ a user-defined dsm.opt file.
+ This option translates to a
+ defaultserver 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 .override
+ 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 ];
+
+}