summary refs log blame commit diff
path: root/nixos/modules/programs/tsm-client.nix
blob: 7ac4086d5f09472c43fea204f49d78e549f999ab (plain) (tree)
1
2
3
4
5
6
7
8
9








                                                                                             
                                                                                                









































































































































































                                                                                          
                                                   









































































































                                                                                             
{ 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 ];

}