From fb6045182081d228bd90fc0b8ebd676fc52d7461 Mon Sep 17 00:00:00 2001 From: ibizaman Date: Sun, 10 Nov 2024 16:52:33 +0100 Subject: [PATCH] use contract test for backup --- flake.nix | 1 + modules/blocks/restic.nix | 16 ++-- modules/contracts/backup.nix | 78 ++++++++++------- modules/contracts/backup/test.nix | 121 +++++++++++++++++++++++++++ modules/contracts/databasebackup.nix | 1 + modules/contracts/default.nix | 1 + test/contracts/backup.nix | 43 ++++++++++ 7 files changed, 224 insertions(+), 37 deletions(-) create mode 100644 modules/contracts/backup/test.nix create mode 100644 test/contracts/backup.nix diff --git a/flake.nix b/flake.nix index 3b16bba..d72ff8c 100644 --- a/flake.nix +++ b/flake.nix @@ -136,6 +136,7 @@ // (vm_test "restic" ./test/blocks/restic.nix) // (vm_test "ssl" ./test/blocks/ssl.nix) + // (vm_test "contracts-backup" ./test/contracts/backup.nix) // (vm_test "contracts-databasebackup" ./test/contracts/databasebackup.nix) // (vm_test "contracts-secret" ./test/contracts/secret.nix) )); diff --git a/modules/blocks/restic.nix b/modules/blocks/restic.nix index 6a04443..cc1cc18 100644 --- a/modules/blocks/restic.nix +++ b/modules/blocks/restic.nix @@ -333,9 +333,17 @@ in environment.systemPackages = let mkResticBinary = name: instance: pkgs.writeShellScriptBin (fullName name instance.settings.repository) '' + set -euo pipefail + export $(grep -v '^#' "/run/secrets_restic_env/${fullName name instance.settings.repository}" \ | xargs -d '\n') - ${pkgs.restic}/bin/restic $@ + + if ! [ "$1" = "restore" ]; then + sudo --preserve-env -u ${instance.request.user} ${pkgs.restic}/bin/restic $@ + else + shift + sudo --preserve-env -u ${instance.request.user} sh -c "${pkgs.restic}/bin/restic restore $@ --target /" + fi ''; in flatten (mapAttrsToList mkResticBinary cfg.instances); @@ -346,15 +354,11 @@ in pkgs.writeShellScriptBin (fullName name instance.settings.repository) '' set -euo pipefail - ls /run/secrets_restic_env/${fullName name instance.settings.repository} - export $(grep -v '^#' "/run/secrets_restic_env/${fullName name instance.settings.repository}" \ | xargs -d '\n') - set -x - if ! [ "$1" = "restore" ]; then - sudo -u ${instance.request.user} ${pkgs.restic}/bin/restic $@ + sudo --preserve-env -u ${instance.request.user} ${pkgs.restic}/bin/restic $@ else shift sudo --preserve-env -u ${instance.request.user} sh -c "${pkgs.restic}/bin/restic dump $@ ${instance.request.backupFile} | ${instance.request.restoreCmd}" diff --git a/modules/contracts/backup.nix b/modules/contracts/backup.nix index cee22ca..8f89ad3 100644 --- a/modules/contracts/backup.nix +++ b/modules/contracts/backup.nix @@ -1,46 +1,62 @@ { lib, ... }: let inherit (lib) mkOption; - inherit (lib.types) anything listOf nonEmptyListOf nullOr submodule str; + inherit (lib.types) anything listOf nonEmptyListOf submodule str; in -submodule { - freeformType = anything; +{ + request = submodule { + freeformType = anything; - options = { - user = mkOption { - description = "Unix user doing the backups."; - type = str; - }; + options = { + user = mkOption { + description = "Unix user doing the backups."; + type = str; + }; - sourceDirectories = mkOption { - description = "Directories to backup."; - type = nonEmptyListOf str; - }; + sourceDirectories = mkOption { + description = "Directories to backup."; + type = nonEmptyListOf str; + }; - excludePatterns = mkOption { - description = "Patterns to exclude."; - type = listOf str; - default = []; - }; + excludePatterns = mkOption { + description = "Patterns to exclude."; + type = listOf str; + default = []; + }; - hooks = mkOption { - description = "Hooks to run around the backup."; - default = {}; - type = submodule { - options = { - before_backup = mkOption { - description = "Hooks to run before backup"; - type = listOf str; - default = []; - }; + hooks = mkOption { + description = "Hooks to run around the backup."; + default = {}; + type = submodule { + options = { + before_backup = mkOption { + description = "Hooks to run before backup"; + type = listOf str; + default = []; + }; - after_backup = mkOption { - description = "Hooks to run after backup"; - type = listOf str; - default = []; + after_backup = mkOption { + description = "Hooks to run after backup"; + type = listOf str; + default = []; + }; }; }; }; }; }; + + result = submodule { + options = { + restoreScript = mkOption { + description = "Name of script that can restore the database."; + type = str; + }; + + backupService = mkOption { + description = "Name of service backing up the database."; + type = str; + }; + }; + }; } diff --git a/modules/contracts/backup/test.nix b/modules/contracts/backup/test.nix new file mode 100644 index 0000000..b8b5996 --- /dev/null +++ b/modules/contracts/backup/test.nix @@ -0,0 +1,121 @@ +{ pkgs, lib, ... }: +let + pkgs' = pkgs; + + testLib = pkgs.callPackage ../../../test/common.nix {}; + + inherit (lib) concatStringsSep concatMapStringsSep getAttrFromPath mkIf optionalAttrs setAttrByPath; + inherit (testLib) indent; +in +{ name, + providerRoot, + modules ? [], + username ? "me", + sourceDirectories ? [ + "/opt/files/A" + "/opt/files/B" + ], + settings, # repository -> attrset +}: pkgs.testers.runNixOSTest { + inherit name; + + nodes.machine = { config, ... }: { + imports = ( testLib.baseImports pkgs' ) ++ modules; + + config = lib.mkMerge [ + (setAttrByPath providerRoot { + request = { + inherit sourceDirectories; + user = username; + }; + settings = settings "/opt/repos/${name}"; + }) + (mkIf (username != "root") { + users.users.${username} = { + isSystemUser = true; + extraGroups = [ "sudoers" ]; + group = "root"; + }; + }) + ]; + }; + + extraPythonPackages = p: [ p.dictdiffer ]; + skipTypeCheck = true; + + testScript = { nodes, ... }: let + provider = getAttrFromPath providerRoot nodes.machine; + backupService = provider.result.backupService; + restoreScript = provider.result.restoreScript; + onAllSourceDirectories = f: concatMapStringsSep "\n" (path: indent 4 (f path)) sourceDirectories; + in '' + from dictdiffer import diff + + username = "${username}" + sourceDirectories = [ ${concatMapStringsSep ", " (x: ''"${x}"'') sourceDirectories} ] + + def list_files(dir): + files_and_content = {} + + files = machine.succeed(f"""find {dir} -type f""").split("\n")[:-1] + + for f in files: + content = machine.succeed(f"""cat {f}""").strip() + files_and_content[f] = content + + return files_and_content + + def assert_files(dir, files): + result = list(diff(list_files(dir), files)) + if len(result) > 0: + raise Exception("Unexpected files:", result) + + with subtest("Create initial content"): + for path in sourceDirectories: + machine.succeed(f""" + mkdir -p {path} + echo repo_fileA_1 > {path}/fileA + echo repo_fileB_1 > {path}/fileB + + chown {username}: -R {path} + chmod go-rwx -R {path} + """) + + for path in sourceDirectories: + assert_files(path, { + f'{path}/fileA': 'repo_fileA_1', + f'{path}/fileB': 'repo_fileB_1', + }) + + with subtest("First backup in repo"): + print(machine.succeed("systemctl cat ${backupService}")) + machine.succeed("systemctl start ${backupService}") + + with subtest("New content"): + for path in sourceDirectories: + machine.succeed(f""" + echo repo_fileA_2 > {path}/fileA + echo repo_fileB_2 > {path}/fileB + """) + + assert_files(path, { + f'{path}/fileA': 'repo_fileA_2', + f'{path}/fileB': 'repo_fileB_2', + }) + + with subtest("Delete content"): + for path in sourceDirectories: + machine.succeed(f"""rm -r {path}/*""") + + assert_files(path, {}) + + with subtest("Restore initial content from repo"): + machine.succeed("""${restoreScript} restore latest""") + + for path in sourceDirectories: + assert_files(path, { + f'{path}/fileA': 'repo_fileA_1', + f'{path}/fileB': 'repo_fileB_1', + }) + ''; +} diff --git a/modules/contracts/databasebackup.nix b/modules/contracts/databasebackup.nix index b7ed733..417ea2c 100644 --- a/modules/contracts/databasebackup.nix +++ b/modules/contracts/databasebackup.nix @@ -6,6 +6,7 @@ in { request = submodule { freeformType = anything; + options = { user = mkOption { description = "Unix user doing the backups."; diff --git a/modules/contracts/default.nix b/modules/contracts/default.nix index ee326af..d5a2873 100644 --- a/modules/contracts/default.nix +++ b/modules/contracts/default.nix @@ -8,5 +8,6 @@ test = { secret = import ./secret/test.nix { inherit pkgs lib; }; databasebackup = import ./databasebackup/test.nix { inherit pkgs lib; }; + backup = import ./backup/test.nix { inherit pkgs lib; }; }; } diff --git a/test/contracts/backup.nix b/test/contracts/backup.nix new file mode 100644 index 0000000..3a40186 --- /dev/null +++ b/test/contracts/backup.nix @@ -0,0 +1,43 @@ +{ pkgs, ... }: +let + contracts = pkgs.callPackage ../../modules/contracts {}; +in +{ + restic_root = contracts.test.backup { + name = "restic_root"; + username = "root"; + providerRoot = [ "shb" "restic" "instances" "mytest" ]; + modules = [ + ../../modules/blocks/restic.nix + ]; + settings = repository: { + enable = true; + passphraseFile = toString (pkgs.writeText "passphrase" "PassPhrase"); + repository = { + path = repository; + timerConfig = { + OnCalendar = "00:00:00"; + }; + }; + }; + }; + + restic_me = contracts.test.backup { + name = "restic_me"; + username = "me"; + providerRoot = [ "shb" "restic" "instances" "mytest" ]; + modules = [ + ../../modules/blocks/restic.nix + ]; + settings = repository: { + enable = true; + passphraseFile = toString (pkgs.writeText "passphrase" "PassPhrase"); + repository = { + path = repository; + timerConfig = { + OnCalendar = "00:00:00"; + }; + }; + }; + }; +}