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 "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)
));

View file

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

View file

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

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 {
freeformType = anything;
options = {
user = mkOption {
description = "Unix user doing the backups.";

View file

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

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