diff --git a/nixos/modules/programs/tsm-client.nix b/nixos/modules/programs/tsm-client.nix index 65d4db7834ff..28db96253875 100644 --- a/nixos/modules/programs/tsm-client.nix +++ b/nixos/modules/programs/tsm-client.nix @@ -7,7 +7,7 @@ let inherit (lib.modules) mkDefault mkIf; inherit (lib.options) literalExpression mkEnableOption mkOption; inherit (lib.strings) concatStringsSep optionalString toLower; - inherit (lib.types) addCheck attrsOf lines nullOr package path port str strMatching submodule; + inherit (lib.types) addCheck attrsOf lines nonEmptyStr nullOr package path port str strMatching submodule; # Checks if given list of strings contains unique # elements when compared without considering case. @@ -35,7 +35,7 @@ let ''; }; options.server = mkOption { - type = strMatching ".+"; + type = nonEmptyStr; example = "tsmserver.company.com"; description = '' Host/domain name or IP address of the IBM TSM server. @@ -56,7 +56,7 @@ let ''; }; options.node = mkOption { - type = strMatching ".+"; + type = nonEmptyStr; example = "MY-TSM-NODE"; description = '' Target node name on the IBM TSM server. @@ -144,7 +144,7 @@ let }; 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 + # https://www.ibm.com/docs/en/spectrum-protect/8.1.13?topic=commands-processing-options config.extraConfig = mapAttrs (lib.trivial.const mkDefault) ( { diff --git a/nixos/modules/services/backup/tsm.nix b/nixos/modules/services/backup/tsm.nix index 6c238745797e..4e690ac6ecda 100644 --- a/nixos/modules/services/backup/tsm.nix +++ b/nixos/modules/services/backup/tsm.nix @@ -5,7 +5,7 @@ let inherit (lib.attrsets) hasAttr; inherit (lib.modules) mkDefault mkIf; inherit (lib.options) mkEnableOption mkOption; - inherit (lib.types) nullOr strMatching; + inherit (lib.types) nonEmptyStr nullOr; options.services.tsmBackup = { enable = mkEnableOption '' @@ -15,7 +15,7 @@ let ''; command = mkOption { - type = strMatching ".+"; + type = nonEmptyStr; default = "backup"; example = "incr"; description = '' @@ -24,7 +24,7 @@ let ''; }; servername = mkOption { - type = strMatching ".+"; + type = nonEmptyStr; example = "mainTsmServer"; description = '' Create a systemd system service @@ -41,7 +41,7 @@ let ''; }; autoTime = mkOption { - type = nullOr (strMatching ".+"); + type = nullOr nonEmptyStr; default = null; example = "12:00"; description = '' @@ -87,16 +87,35 @@ in environment.DSM_LOG = "/var/log/tsm-backup/"; # TSM needs a HOME dir to store certificates. environment.HOME = "/var/lib/tsm-backup"; - # for exit status description see - # https://www.ibm.com/support/knowledgecenter/en/SSEQVQ_8.1.8/client/c_sched_rtncode.html - serviceConfig.SuccessExitStatus = "4 8"; - # The `-se` option must come after the command. - # The `-optfile` option suppresses a `dsm.opt`-not-found warning. - serviceConfig.ExecStart = - "${cfgPrg.wrappedPackage}/bin/dsmc ${cfg.command} -se='${cfg.servername}' -optfile=/dev/null"; - serviceConfig.LogsDirectory = "tsm-backup"; - serviceConfig.StateDirectory = "tsm-backup"; - serviceConfig.StateDirectoryMode = "0750"; + serviceConfig = { + # for exit status description see + # https://www.ibm.com/docs/en/spectrum-protect/8.1.13?topic=clients-client-return-codes + SuccessExitStatus = "4 8"; + # The `-se` option must come after the command. + # The `-optfile` option suppresses a `dsm.opt`-not-found warning. + ExecStart = + "${cfgPrg.wrappedPackage}/bin/dsmc ${cfg.command} -se='${cfg.servername}' -optfile=/dev/null"; + LogsDirectory = "tsm-backup"; + StateDirectory = "tsm-backup"; + StateDirectoryMode = "0750"; + # systemd sandboxing + LockPersonality = true; + NoNewPrivileges = true; + PrivateDevices = true; + #PrivateTmp = true; # would break backup of {/var,}/tmp + #PrivateUsers = true; # would block backup of /home/* + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = "read-only"; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "noaccess"; + ProtectSystem = "strict"; + RestrictNamespaces = true; + RestrictSUIDSGID = true; + }; startAt = mkIf (cfg.autoTime!=null) cfg.autoTime; }; }; diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 2bc34d6fd786..bbf6de7b6cfd 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -490,6 +490,7 @@ in trezord = handleTest ./trezord.nix {}; trickster = handleTest ./trickster.nix {}; trilium-server = handleTestOn ["x86_64-linux"] ./trilium-server.nix {}; + tsm-client-gui = handleTest ./tsm-client-gui.nix {}; txredisapi = handleTest ./txredisapi.nix {}; tuptime = handleTest ./tuptime.nix {}; turbovnc-headless-server = handleTest ./turbovnc-headless-server.nix {}; diff --git a/nixos/tests/tsm-client-gui.nix b/nixos/tests/tsm-client-gui.nix new file mode 100644 index 000000000000..e4bcd344a895 --- /dev/null +++ b/nixos/tests/tsm-client-gui.nix @@ -0,0 +1,57 @@ +# The tsm-client GUI first tries to connect to a server. +# We can't simulate a server, so we just check if +# it reports the correct connection failure error. +# After that the test persuades the GUI +# to show its main application window +# and verifies some configuration information. + +import ./make-test-python.nix ({ lib, pkgs, ... }: { + name = "tsm-client"; + + enableOCR = true; + + machine = { pkgs, ... }: { + imports = [ ./common/x11.nix ]; + programs.tsmClient = { + enable = true; + package = pkgs.tsm-client-withGui; + defaultServername = "testserver"; + servers.testserver = { + # 192.0.0.8 is a "dummy address" according to RFC 7600 + server = "192.0.0.8"; + node = "SOME-NODE"; + passwdDir = "/tmp"; + }; + }; + }; + + testScript = '' + machine.succeed("which dsmj") # fail early if this is missing + machine.wait_for_x() + machine.execute("DSM_LOG=/tmp dsmj -optfile=/dev/null >&2 &") + + # does it report the "TCP/IP connection failure" error code? + machine.wait_for_window("IBM Spectrum Protect") + machine.wait_for_text("ANS2610S") + machine.send_key("esc") + + # it asks to continue to restore a local backupset now; + # "yes" (return) leads to the main application window + machine.wait_for_text("backupset") + machine.send_key("ret") + + # main window: navigate to "Connection Information" + machine.wait_for_text("Welcome") + machine.send_key("alt-f") # "File" menu + machine.send_key("c") # "Connection Information" + + # "Connection Information" dialog box + machine.wait_for_window("Connection Information") + machine.wait_for_text("SOME-NODE") + machine.wait_for_text("${pkgs.tsm-client.passthru.unwrapped.version}") + + machine.shutdown() + ''; + + meta.maintainers = [ lib.maintainers.yarny ]; +}) diff --git a/pkgs/tools/backup/tsm-client/default.nix b/pkgs/tools/backup/tsm-client/default.nix index e298751facab..c684b34ec4e5 100644 --- a/pkgs/tools/backup/tsm-client/default.nix +++ b/pkgs/tools/backup/tsm-client/default.nix @@ -1,15 +1,20 @@ { lib +, callPackage +, nixosTests , stdenv -, autoPatchelfHook -, buildEnv , fetchurl -, makeWrapper -, procps +, autoPatchelfHook +, rpmextract +, openssl , zlib -# optional packages that enable certain features -, acl ? null # EXT2/EXT3/XFS ACL support -, jdk8 ? null # Java GUI -, lvm2 ? null # LVM image backup and restore functions +, lvm2 # LVM image backup and restore functions (optional) +, acl # EXT2/EXT3/XFS ACL support (optional) +, gnugrep +, procps +, jdk8 # Java GUI (needed for `enableGui`) +, buildEnv +, makeWrapper +, enableGui ? false # enables Java GUI `dsmj` # path to `dsm.sys` configuration files , dsmSysCli ? "/etc/tsm-client/cli.dsm.sys" , dsmSysApi ? "/etc/tsm-client/api.dsm.sys" @@ -18,7 +23,7 @@ # For an explanation of optional packages # (features provided by them, version limits), see -# https://www-01.ibm.com/support/docview.wss?uid=swg21052223#Version%208.1 +# https://www.ibm.com/support/pages/node/660813#Version%208.1 # IBM Tivoli Storage Manager Client uses a system-wide @@ -40,22 +45,33 @@ # point to this derivations `/dsmi_dir` directory symlink. # Other environment variables might be necessary, # depending on local configuration or usage; see: -# https://www.ibm.com/support/knowledgecenter/en/SSEQVQ_8.1.8/client/c_cfg_sapiunix.html +# https://www.ibm.com/docs/en/spectrum-protect/8.1.13?topic=solaris-set-api-environment-variables -# The newest version of TSM client should be discoverable -# by going the the `downloadPage` (see `meta` below), -# there to "Client Latest Downloads", -# "IBM Spectrum Protect Client Downloads and READMEs", -# then to "Linux x86_64 Ubuntu client" (as of 2019-07-15). +# The newest version of TSM client should be discoverable by +# going to the `downloadPage` (see `meta` below). +# Find the "Backup-archive client" table on that page. +# Look for "Download Documents" of the latest release. +# Here, two links must be checked: +# * "IBM Spectrum Protect Client ... Downloads and READMEs": +# In the table at the page's bottom, +# check the date of the "Linux x86_64 client" +# * "IBM Spectrum Protect BA client ... interim fix downloads" +# Look for the "Linux x86_64 client" rows +# in the table # at the bottom of each page. +# Follow the "HTTPS" link of the row with the latest date stamp. +# In the directory listing to show up, pick the big `.tar` file. +# +# (as of 2021-12-18) let meta = { - homepage = "https://www.ibm.com/us-en/marketplace/data-protection-and-recovery"; - downloadPage = "https://www-01.ibm.com/support/docview.wss?uid=swg21239415"; + homepage = "https://www.ibm.com/products/data-protection-and-recovery"; + downloadPage = "https://www.ibm.com/support/pages/ibm-spectrum-protect-downloads-latest-fix-packs-and-interim-fixes"; platforms = [ "x86_64-linux" ]; + mainProgram = "dsmc"; license = lib.licenses.unfree; maintainers = [ lib.maintainers.yarny ]; description = "IBM Spectrum Protect (Tivoli Storage Manager) CLI and API"; @@ -74,34 +90,53 @@ let ''; }; + passthru.tests = { + test-cli = callPackage ./test-cli.nix {}; + test-gui = nixosTests.tsm-client-gui; + }; + + mkSrcUrl = version: + let + major = lib.versions.major version; + minor = lib.versions.minor version; + patch = lib.versions.patch version; + fixup = lib.lists.elemAt (lib.versions.splitVersion version) 3; + in + "https://public.dhe.ibm.com/storage/tivoli-storage-management/${if fixup=="0" then "maintenance" else "patches"}/client/v${major}r${minor}/Linux/LinuxX86/BA/v${major}${minor}${patch}/${version}-TIV-TSMBAC-LinuxX86.tar"; + unwrapped = stdenv.mkDerivation rec { name = "tsm-client-${version}-unwrapped"; - version = "8.1.8.0"; + version = "8.1.13.3"; src = fetchurl { - url = "ftp://public.dhe.ibm.com/storage/tivoli-storage-management/maintenance/client/v8r1/Linux/LinuxX86_DEB/BA/v818/${version}-TIV-TSMBAC-LinuxX86_DEB.tar"; - sha256 = "0c1d0jm0i7qjd314nhj2vj8fs7sncm1x2n4d6dg4049jniyvjhpk"; + url = mkSrcUrl version; + sha256 = "1dwczf236drdaf4jcfzz5154vdwvxf5zraxhrhiddl6n80hnvbcd"; }; - inherit meta; + inherit meta passthru; nativeBuildInputs = [ autoPatchelfHook + rpmextract ]; buildInputs = [ + openssl stdenv.cc.cc zlib ]; runtimeDependencies = [ - lvm2 + (lib.attrsets.getLib lvm2) ]; sourceRoot = "."; postUnpack = '' - for debfile in *.deb - do - ar -x "$debfile" - tar --xz --extract --file=data.tar.xz - rm data.tar.xz - done + rpmextract TIVsm-API64.x86_64.rpm + rpmextract TIVsm-APIcit.x86_64.rpm + rpmextract TIVsm-BA.x86_64.rpm + rpmextract TIVsm-BAcit.x86_64.rpm + rpmextract TIVsm-BAhdw.x86_64.rpm + rpmextract TIVsm-JBB.x86_64.rpm + # use globbing so that version updates don't break the build: + rpmextract gskcrypt64-*.linux.x86_64.rpm + rpmextract gskssl64-*.linux.x86_64.rpm ''; installPhase = '' @@ -113,7 +148,7 @@ let # Fix relative symlinks after `/usr` was moved up one level preFixup = '' - for link in $out/lib/* $out/bin/* + for link in $out/lib{,64}/* $out/bin/* do target=$(readlink "$link") if [ "$(cut -b -6 <<< "$target")" != "../../" ] @@ -126,14 +161,19 @@ let ''; }; + binPath = lib.makeBinPath ([ acl gnugrep procps ] + ++ lib.optional enableGui jdk8); + in buildEnv { name = "tsm-client-${unwrapped.version}"; - inherit meta; - passthru = { inherit unwrapped; }; + meta = meta // lib.attrsets.optionalAttrs enableGui { + mainProgram = "dsmj"; + }; + passthru = passthru // { inherit unwrapped; }; paths = [ unwrapped ]; - buildInputs = [ makeWrapper ]; + nativeBuildInputs = [ makeWrapper ]; pathsToLink = [ "/" "/bin" @@ -144,7 +184,7 @@ buildEnv { # to the so-called "installation directories" # * Add symlinks to the "installation directories" # that point to the `dsm.sys` configuration files - # * Drop the Java GUI executable unless `jdk` is present + # * Drop the Java GUI executable unless `enableGui` is set # * Create wrappers for the command-line interface to # prepare `PATH` and `DSM_DIR` environment variables postBuild = '' @@ -152,13 +192,13 @@ buildEnv { ln --symbolic --no-target-directory opt/tivoli/tsm/client/api/bin64 $out/dsmi_dir ln --symbolic --no-target-directory "${dsmSysCli}" $out/dsm_dir/dsm.sys ln --symbolic --no-target-directory "${dsmSysApi}" $out/dsmi_dir/dsm.sys - ${lib.optionalString (jdk8==null) "rm $out/bin/dsmj"} + ${lib.optionalString (!enableGui) "rm $out/bin/dsmj"} for bin in $out/bin/* do target=$(readlink "$bin") rm "$bin" makeWrapper "$target" "$bin" \ - --prefix PATH : "$out/dsm_dir:${lib.strings.makeBinPath [ procps acl jdk8 ]}" \ + --prefix PATH : "$out/dsm_dir:${binPath}" \ --set DSM_DIR $out/dsm_dir done ''; diff --git a/pkgs/tools/backup/tsm-client/test-cli.nix b/pkgs/tools/backup/tsm-client/test-cli.nix new file mode 100644 index 000000000000..0858083c9f90 --- /dev/null +++ b/pkgs/tools/backup/tsm-client/test-cli.nix @@ -0,0 +1,58 @@ +{ lib +, writeText +, runCommand +, tsm-client +}: + +# Let the client try to connect to a server. +# We can't simulate a server, so there's no more to test. + +let + + # 192.0.0.8 is a "dummy address" according to RFC 7600 + dsmSysCli = writeText "cli.dsm.sys" '' + defaultserver testserver + server testserver + commmethod v6tcpip + tcpserveraddress 192.0.0.8 + nodename ARBITRARYNODENAME + ''; + + tsm-client_ = tsm-client.override { inherit dsmSysCli; }; + + env.nativeBuildInputs = [ tsm-client_ ]; + + versionString = + let + inherit (tsm-client_.passthru.unwrapped) version; + major = lib.versions.major version; + minor = lib.versions.minor version; + patch = lib.versions.patch version; + fixup = lib.lists.elemAt (lib.versions.splitVersion version) 3; + in + "Client Version ${major}, Release ${minor}, Level ${patch}.${fixup}"; + +in + +runCommand "${tsm-client.name}-test-cli" env '' + set -o nounset + set -o pipefail + + export DSM_LOG=$(mktemp -d ./dsm_log.XXXXXXXXXXX) + + { dsmc -optfile=/dev/null || true; } | tee dsmc-stdout + + # does it report the correct version? + grep --fixed-strings '${versionString}' dsmc-stdout + + # does it use the provided dsm.sys config file? + # if it does, it states the node's name + grep ARBITRARYNODENAME dsmc-stdout + + # does it try (and fail) to connect to the server? + # if it does, it reports the "TCP/IP connection failure" error code + grep ANS1017E dsmc-stdout + grep ANS1017E $DSM_LOG/dsmerror.log + + touch $out +'' diff --git a/pkgs/top-level/all-packages.nix b/pkgs/top-level/all-packages.nix index 366a7078d87e..582c6921259f 100644 --- a/pkgs/top-level/all-packages.nix +++ b/pkgs/top-level/all-packages.nix @@ -4927,8 +4927,8 @@ with pkgs; timeline = callPackage ../applications/office/timeline { }; - tsm-client = callPackage ../tools/backup/tsm-client { jdk8 = null; }; - tsm-client-withGui = callPackage ../tools/backup/tsm-client { }; + tsm-client = callPackage ../tools/backup/tsm-client { }; + tsm-client-withGui = callPackage ../tools/backup/tsm-client { enableGui = true; }; tracker = callPackage ../development/libraries/tracker { };