Unverified Commit 6b0b1559 authored by Rick van Schijndel's avatar Rick van Schijndel Committed by GitHub
Browse files

nixos/restic: add command option (#432329)

parents a0c53ddf 73f8c1e6
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -215,6 +215,8 @@

- Immich now has support for [VectorChord](https://github.com/tensorchord/VectorChord) when using the PostgreSQL configuration provided by `services.immich.database.enable`, which replaces `pgvecto-rs`. VectorChord support can be toggled with the option `services.immich.database.enableVectorChord`. Additionally, `pgvecto-rs` support is now disabled from NixOS 25.11 onwards using the option `services.immich.database.enableVectors`. This option will be removed fully in the future once Immich drops support for `pgvecto-rs` fully. See [Immich migration instructions](#module-services-immich-vectorchord-migration)

- `services.restic.backups` now includes a `command` option for passing a command to the [--stdin-from-command](https://github.com/restic/restic/pull/4410) flag.

- `services.postsrsd` now automatically integrates with the local Postfix instance, when enabled. This behavior can disabled using the [services.postsrsd.configurePostfix](#opt-services.postsrsd.configurePostfix) option.

- `services.pfix-srsd` now automatically integrates with the local Postfix instance, when enabled. This behavior can disabled using the [services.pfix-srsd.configurePostfix](#opt-services.pfix-srsd.configurePostfix) option.
+65 −14
Original line number Diff line number Diff line
@@ -146,6 +146,21 @@ in
              ];
            };

            command = lib.mkOption {
              type = lib.types.listOf lib.types.str;
              default = [ ];
              description = ''
                Command to pass to --stdin-from-command. If null or an empty array, and `paths`/`dynamicFilesFrom`
                are also null, no backup command will be run.
              '';
              example = [
                "sudo"
                "-u"
                "postgres"
                "pg_dumpall"
              ];
            };

            exclude = lib.mkOption {
              type = lib.types.listOf lib.types.str;
              default = [ ];
@@ -238,7 +253,7 @@ in

            runCheck = lib.mkOption {
              type = lib.types.bool;
              default = (builtins.length config.services.restic.backups.${name}.checkOpts > 0);
              default = builtins.length config.services.restic.backups.${name}.checkOpts > 0;
              defaultText = lib.literalExpression ''builtins.length config.services.backups.${name}.checkOpts > 0'';
              description = "Whether to run the `check` command with the provided `checkOpts` options.";
              example = true;
@@ -327,14 +342,44 @@ in
          RandomizedDelaySec = "5h";
        };
      };
      commandbackup = {
        command = [
          "\${lib.getExe pkgs.sudo}"
          "-u postgres"
          "\${pkgs.postgresql}/bin/pg_dumpall"
        ];
        extraBackupArgs = [ "--tag database" ];
        repository = "s3:example.com/mybucket";
        passwordFile = "/etc/nixos/secrets/restic-password";
        environmentFile = "/etc/nixos/secrets/restic-environment";
        pruneOpts = [
          "--keep-daily 14"
          "--keep-weekly 4"
          "--keep-monthly 2"
          "--group-by tags"
        ];
      };
    };
  };

  config = {
    assertions = lib.mapAttrsToList (n: v: {
      assertion = (v.repository == null) != (v.repositoryFile == null);
      message = "services.restic.backups.${n}: exactly one of repository or repositoryFile should be set";
    }) config.services.restic.backups;
    assertions = lib.flatten (
      lib.mapAttrsToList (name: backup: [
        {
          assertion = (backup.repository == null) != (backup.repositoryFile == null);
          message = "services.restic.backups.${name}: exactly one of repository or repositoryFile should be set";
        }
        {
          assertion =
            let
              fileBackup = (backup.paths != null && backup.paths != [ ]) || backup.dynamicFilesFrom != null;
              commandBackup = backup.command != [ ];
            in
            !(fileBackup && commandBackup);
          message = "services.restic.backups.${name}: cannot do both a command backup and a file backup at the same time.";
        }
      ]) config.services.restic.backups
    );
    systemd.services = lib.mapAttrs' (
      name: backup:
      let
@@ -351,7 +396,9 @@ in
          backup.exclude != [ ]
        ) "--exclude-file=${pkgs.writeText "exclude-patterns" (lib.concatStringsSep "\n" backup.exclude)}";
        filesFromTmpFile = "/run/restic-backups-${name}/includes";
        doBackup = (backup.dynamicFilesFrom != null) || (backup.paths != null && backup.paths != [ ]);
        fileBackup = (backup.dynamicFilesFrom != null) || (backup.paths != null && backup.paths != [ ]);
        commandBackup = backup.command != [ ];
        doBackup = fileBackup || commandBackup;
        pruneCmd = lib.optionals (builtins.length backup.pruneOpts > 0) [
          (resticCmd + " unlock")
          (resticCmd + " forget --prune " + (lib.concatStringsSep " " backup.pruneOpts))
@@ -397,11 +444,15 @@ in
          serviceConfig = {
            Type = "oneshot";
            ExecStart =
              (lib.optionals doBackup [
              lib.optionals doBackup [
                "${resticCmd} backup ${
                  lib.concatStringsSep " " (backup.extraBackupArgs ++ excludeFlags)
                } --files-from=${filesFromTmpFile}"
              ])
                  lib.concatStringsSep " " (
                    backup.extraBackupArgs
                    ++ lib.optionals fileBackup (excludeFlags ++ [ "--files-from=${filesFromTmpFile}" ])
                    ++ lib.optionals commandBackup ([ "--stdin-from-command=true --" ] ++ backup.command)
                  )
                }"
              ]
              ++ pruneCmd
              ++ checkCmd;
            User = backup.user;
@@ -419,7 +470,7 @@ in
            ${lib.optionalString (backup.backupPrepareCommand != null) ''
              ${pkgs.writeScript "backupPrepareCommand" backup.backupPrepareCommand}
            ''}
            ${lib.optionalString (backup.initialize) ''
            ${lib.optionalString backup.initialize ''
              ${resticCmd} cat config > /dev/null || ${resticCmd} init
            ''}
            ${lib.optionalString (backup.paths != null && backup.paths != [ ]) ''
@@ -435,7 +486,7 @@ in
            ${lib.optionalString (backup.backupCleanupCommand != null) ''
              ${pkgs.writeScript "backupCleanupCommand" backup.backupCleanupCommand}
            ''}
            ${lib.optionalString doBackup ''
            ${lib.optionalString fileBackup ''
              rm ${filesFromTmpFile}
            ''}
          '';
@@ -446,7 +497,7 @@ in
      name: backup:
      lib.nameValuePair "restic-backups-${name}" {
        wantedBy = [ "timers.target" ];
        timerConfig = backup.timerConfig;
        inherit (backup) timerConfig;
      }
    ) (lib.filterAttrs (_: backup: backup.timerConfig != null) config.services.restic.backups);

@@ -464,7 +515,7 @@ in
        ${lib.pipe config.systemd.services."restic-backups-${name}".environment [
          (lib.filterAttrs (n: v: v != null && n != "PATH"))
          (lib.mapAttrs (_: v: "${v}"))
          (lib.toShellVars)
          lib.toShellVars
        ]}
        PATH=${config.systemd.services."restic-backups-${name}".environment.PATH}:$PATH

+21 −1
Original line number Diff line number Diff line
{ pkgs, ... }:

let
  inherit (import ./ssh-keys.nix pkgs)
    snakeOilEd25519PrivateKey
@@ -8,6 +7,7 @@ let

  remoteRepository = "/root/restic-backup";
  remoteFromFileRepository = "/root/restic-backup-from-file";
  remoteFromCommandRepository = "/root/restic-backup-from-command";
  remoteInhibitTestRepository = "/root/restic-backup-inhibit-test";
  remoteNoInitRepository = "/root/restic-backup-no-init";
  rcloneRepository = "rclone:local:/root/restic-rclone-backup";
@@ -45,6 +45,12 @@ let
    "--keep-monthly 1"
    "--keep-yearly 99"
  ];
  commandString = "testing";
  command = [
    "echo"
    "-n"
    commandString
  ];
in
{
  name = "restic";
@@ -127,6 +133,15 @@ in
              find /opt -mindepth 1 -maxdepth 1 ! -name a_dir # all files in /opt except for a_dir
            '';
          };
          remote-from-command-backup = {
            inherit
              passwordFile
              pruneOpts
              command
              ;
            initialize = true;
            repository = remoteFromCommandRepository;
          };
          inhibit-test = {
            inherit
              passwordFile
@@ -267,6 +282,11 @@ in
        "${pkgs.restic}/bin/restic -r ${remoteRepository} -p ${passwordFile} restore latest -t /tmp/restore-3",
        "diff -ru ${testDir} /tmp/restore-3/opt",

        # test that remote-from-command-backup produces a snapshot, with the expected contents
        "systemctl start restic-backups-remote-from-command-backup.service",
        'restic-remote-from-command-backup snapshots --json | ${pkgs.jq}/bin/jq "length | . == 1"',
        '[[ $(restic-remote-from-command-backup dump --path /stdin latest stdin) == ${commandString} ]]',

        # test that rclonebackup produces a snapshot
        "systemctl start restic-backups-rclonebackup.service",
        'restic-rclonebackup snapshots --json | ${pkgs.jq}/bin/jq "length | . == 1"',