use contract test for backup

This commit is contained in:
ibizaman 2024-11-10 16:52:33 +01:00 committed by Pierre Penninckx
parent bcc219a111
commit fb60451820
7 changed files with 224 additions and 37 deletions

View file

@ -136,6 +136,7 @@
// (vm_test "restic" ./test/blocks/restic.nix) // (vm_test "restic" ./test/blocks/restic.nix)
// (vm_test "ssl" ./test/blocks/ssl.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-databasebackup" ./test/contracts/databasebackup.nix)
// (vm_test "contracts-secret" ./test/contracts/secret.nix) // (vm_test "contracts-secret" ./test/contracts/secret.nix)
)); ));

View file

@ -333,9 +333,17 @@ in
environment.systemPackages = let environment.systemPackages = let
mkResticBinary = name: instance: mkResticBinary = name: instance:
pkgs.writeShellScriptBin (fullName name instance.settings.repository) '' pkgs.writeShellScriptBin (fullName name instance.settings.repository) ''
set -euo pipefail
export $(grep -v '^#' "/run/secrets_restic_env/${fullName name instance.settings.repository}" \ export $(grep -v '^#' "/run/secrets_restic_env/${fullName name instance.settings.repository}" \
| xargs -d '\n') | 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 in
flatten (mapAttrsToList mkResticBinary cfg.instances); flatten (mapAttrsToList mkResticBinary cfg.instances);
@ -346,15 +354,11 @@ in
pkgs.writeShellScriptBin (fullName name instance.settings.repository) '' pkgs.writeShellScriptBin (fullName name instance.settings.repository) ''
set -euo pipefail 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}" \ export $(grep -v '^#' "/run/secrets_restic_env/${fullName name instance.settings.repository}" \
| xargs -d '\n') | xargs -d '\n')
set -x
if ! [ "$1" = "restore" ]; then 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 else
shift shift
sudo --preserve-env -u ${instance.request.user} sh -c "${pkgs.restic}/bin/restic dump $@ ${instance.request.backupFile} | ${instance.request.restoreCmd}" sudo --preserve-env -u ${instance.request.user} sh -c "${pkgs.restic}/bin/restic dump $@ ${instance.request.backupFile} | ${instance.request.restoreCmd}"

View file

@ -1,9 +1,10 @@
{ lib, ... }: { lib, ... }:
let let
inherit (lib) mkOption; inherit (lib) mkOption;
inherit (lib.types) anything listOf nonEmptyListOf nullOr submodule str; inherit (lib.types) anything listOf nonEmptyListOf submodule str;
in in
submodule { {
request = submodule {
freeformType = anything; freeformType = anything;
options = { options = {
@ -43,4 +44,19 @@ submodule {
}; };
}; };
}; };
};
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;
};
};
};
} }

View file

@ -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',
})
'';
}

View file

@ -6,6 +6,7 @@ in
{ {
request = submodule { request = submodule {
freeformType = anything; freeformType = anything;
options = { options = {
user = mkOption { user = mkOption {
description = "Unix user doing the backups."; description = "Unix user doing the backups.";

View file

@ -8,5 +8,6 @@
test = { test = {
secret = import ./secret/test.nix { inherit pkgs lib; }; secret = import ./secret/test.nix { inherit pkgs lib; };
databasebackup = import ./databasebackup/test.nix { inherit pkgs lib; }; databasebackup = import ./databasebackup/test.nix { inherit pkgs lib; };
backup = import ./backup/test.nix { inherit pkgs lib; };
}; };
} }

43
test/contracts/backup.nix Normal file
View file

@ -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";
};
};
};
};
}