use contract test for databasebackup
This commit is contained in:
parent
6c9aa47184
commit
bcc219a111
7 changed files with 171 additions and 142 deletions
|
|
@ -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-databasebackup" ./test/contracts/databasebackup.nix)
|
||||||
// (vm_test "contracts-secret" ./test/contracts/secret.nix)
|
// (vm_test "contracts-secret" ./test/contracts/secret.nix)
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -73,12 +73,12 @@ in
|
||||||
backupFile = "postgres.sql";
|
backupFile = "postgres.sql";
|
||||||
|
|
||||||
backupCmd = ''
|
backupCmd = ''
|
||||||
${pkgs.postgresql}/bin/pg_dumpall | ${pkgs.gzip}/bin/gzip --rsyncable
|
${pkgs.postgresql}/bin/pg_dumpall | ${pkgs.gzip}/bin/gzip --rsyncable
|
||||||
'';
|
'';
|
||||||
|
|
||||||
restoreCmd = ''
|
restoreCmd = ''
|
||||||
${pkgs.gzip}/bin/gunzip | ${pkgs.postgresql}/bin/psql postgres
|
${pkgs.gzip}/bin/gunzip | ${pkgs.postgresql}/bin/psql postgres
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,9 @@ let
|
||||||
type = path;
|
type = path;
|
||||||
};
|
};
|
||||||
|
|
||||||
repositories = mkOption {
|
repository = mkOption {
|
||||||
description = "Repositories to back this instance to.";
|
description = "Repositories to back this instance to.";
|
||||||
type = nonEmptyListOf (submodule {
|
type = submodule {
|
||||||
options = {
|
options = {
|
||||||
path = mkOption {
|
path = mkOption {
|
||||||
type = str;
|
type = str;
|
||||||
|
|
@ -60,7 +60,7 @@ let
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
});
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
retention = mkOption {
|
retention = mkOption {
|
||||||
|
|
@ -98,9 +98,9 @@ in
|
||||||
{
|
{
|
||||||
options.shb.restic = {
|
options.shb.restic = {
|
||||||
instances = mkOption {
|
instances = mkOption {
|
||||||
description = "Each instance is backing up some directories to one or more repositories.";
|
description = "Each instance is backing up some directories to one repository.";
|
||||||
default = {};
|
default = {};
|
||||||
type = attrsOf (submodule {
|
type = attrsOf (submodule ({ name, config, ... }: {
|
||||||
options = {
|
options = {
|
||||||
request = mkOption {
|
request = mkOption {
|
||||||
type = contracts.backup.request;
|
type = contracts.backup.request;
|
||||||
|
|
@ -109,14 +109,22 @@ in
|
||||||
settings = mkOption {
|
settings = mkOption {
|
||||||
type = commonOptions;
|
type = commonOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
result = mkOption {
|
||||||
|
type = contracts.databasebackup.result;
|
||||||
|
default = {
|
||||||
|
restoreScript = fullName name config.settings.repository;
|
||||||
|
backupService = "${fullName name config.settings.repository}.service";
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
});
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
databases = mkOption {
|
databases = mkOption {
|
||||||
description = "Each item is backing up some database to one or more repositories.";
|
description = "Each item is backing up some database to one repository.";
|
||||||
default = {};
|
default = {};
|
||||||
type = attrsOf (submodule ({ options, ... }: {
|
type = attrsOf (submodule ({ name, config, ... }: {
|
||||||
options = {
|
options = {
|
||||||
request = mkOption {
|
request = mkOption {
|
||||||
type = contracts.databasebackup.request;
|
type = contracts.databasebackup.request;
|
||||||
|
|
@ -129,8 +137,8 @@ in
|
||||||
result = mkOption {
|
result = mkOption {
|
||||||
type = contracts.databasebackup.result;
|
type = contracts.databasebackup.result;
|
||||||
default = {
|
default = {
|
||||||
restoreScript = fullName options.name options.repository;
|
restoreScript = fullName name config.settings.repository;
|
||||||
backupService = "${fullName options.name options.repository}.service";
|
backupService = "${fullName name config.settings.repository}.service";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -175,22 +183,20 @@ in
|
||||||
# Create repository if it is a local path.
|
# Create repository if it is a local path.
|
||||||
systemd.tmpfiles.rules =
|
systemd.tmpfiles.rules =
|
||||||
let
|
let
|
||||||
mkRepositorySettings = name: instance: repository: optionals (hasPrefix "/" repository.path) [
|
mkSettings = name: instance: optionals (hasPrefix "/" instance.settings.repository.path) [
|
||||||
"d '${repository.path}' 0750 ${instance.request.user} root - -"
|
"d '${instance.settings.repository.path}' 0750 ${instance.request.user} root - -"
|
||||||
];
|
];
|
||||||
|
|
||||||
mkSettings = name: instance: builtins.map (mkRepositorySettings name instance) instance.settings.repositories;
|
|
||||||
in
|
in
|
||||||
flatten (mapAttrsToList mkSettings (cfg.instances // cfg.databases));
|
flatten (mapAttrsToList mkSettings (cfg.instances // cfg.databases));
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
services.restic.backups =
|
services.restic.backups =
|
||||||
let
|
let
|
||||||
mkRepositorySettings = name: instance: repository: {
|
mkSettings = name: instance: {
|
||||||
"${name}_${repoSlugName repository.path}" = {
|
"${name}_${repoSlugName instance.settings.repository.path}" = {
|
||||||
inherit (instance.request) user;
|
inherit (instance.request) user;
|
||||||
|
|
||||||
repository = repository.path;
|
repository = instance.settings.repository.path;
|
||||||
|
|
||||||
paths = instance.request.sourceDirectories;
|
paths = instance.request.sourceDirectories;
|
||||||
|
|
||||||
|
|
@ -198,7 +204,7 @@ in
|
||||||
|
|
||||||
initialize = true;
|
initialize = true;
|
||||||
|
|
||||||
inherit (repository) timerConfig;
|
inherit (instance.settings.repository) timerConfig;
|
||||||
|
|
||||||
pruneOpts = mapAttrsToList (name: value:
|
pruneOpts = mapAttrsToList (name: value:
|
||||||
"--${builtins.replaceStrings ["_"] ["-"] name} ${builtins.toString value}"
|
"--${builtins.replaceStrings ["_"] ["-"] name} ${builtins.toString value}"
|
||||||
|
|
@ -219,19 +225,17 @@ in
|
||||||
exclude = instance.request.excludePatterns;
|
exclude = instance.request.excludePatterns;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
mkSettings = name: instance: builtins.map (mkRepositorySettings name instance) instance.settings.repositories;
|
|
||||||
in
|
in
|
||||||
mkMerge (flatten (mapAttrsToList mkSettings enabledInstances));
|
mkMerge (flatten (mapAttrsToList mkSettings enabledInstances));
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
services.restic.backups =
|
services.restic.backups =
|
||||||
let
|
let
|
||||||
mkRepositorySettings = name: instance: repository: {
|
mkSettings = name: instance: {
|
||||||
"${name}_${repoSlugName repository.path}" = {
|
"${name}_${repoSlugName instance.settings.repository.path}" = {
|
||||||
inherit (instance.request) user;
|
inherit (instance.request) user;
|
||||||
|
|
||||||
repository = repository.path;
|
repository = instance.settings.repository.path;
|
||||||
|
|
||||||
dynamicFilesFrom = "echo";
|
dynamicFilesFrom = "echo";
|
||||||
|
|
||||||
|
|
@ -239,7 +243,7 @@ in
|
||||||
|
|
||||||
initialize = true;
|
initialize = true;
|
||||||
|
|
||||||
inherit (repository) timerConfig;
|
inherit (instance.settings.repository) timerConfig;
|
||||||
|
|
||||||
pruneOpts = mapAttrsToList (name: value:
|
pruneOpts = mapAttrsToList (name: value:
|
||||||
"--${builtins.replaceStrings ["_"] ["-"] name} ${builtins.toString value}"
|
"--${builtins.replaceStrings ["_"] ["-"] name} ${builtins.toString value}"
|
||||||
|
|
@ -261,17 +265,15 @@ in
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
mkSettings = name: instance: builtins.map (mkRepositorySettings name instance) instance.settings.repositories;
|
|
||||||
in
|
in
|
||||||
mkMerge (flatten (mapAttrsToList mkSettings enabledDatabases));
|
mkMerge (flatten (mapAttrsToList mkSettings enabledDatabases));
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
systemd.services =
|
systemd.services =
|
||||||
let
|
let
|
||||||
mkRepositorySettings = name: instance: repository:
|
mkSettings = name: instance:
|
||||||
let
|
let
|
||||||
serviceName = fullName name repository;
|
serviceName = fullName name instance.settings.repository;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
${serviceName} = mkMerge [
|
${serviceName} = mkMerge [
|
||||||
|
|
@ -283,7 +285,7 @@ in
|
||||||
# BindReadOnlyPaths = instance.sourceDirectories;
|
# BindReadOnlyPaths = instance.sourceDirectories;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
(optionalAttrs (repository.secrets != {})
|
(optionalAttrs (instance.settings.repository.secrets != {})
|
||||||
{
|
{
|
||||||
serviceConfig.EnvironmentFile = [
|
serviceConfig.EnvironmentFile = [
|
||||||
"/run/secrets_restic/${serviceName}"
|
"/run/secrets_restic/${serviceName}"
|
||||||
|
|
@ -293,10 +295,10 @@ in
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
|
||||||
"${serviceName}-pre" = mkIf (repository.secrets != {})
|
"${serviceName}-pre" = mkIf (instance.settings.repository.secrets != {})
|
||||||
(let
|
(let
|
||||||
script = shblib.genConfigOutOfBandSystemd {
|
script = shblib.genConfigOutOfBandSystemd {
|
||||||
config = repository.secrets;
|
config = instance.settings.repository.secrets;
|
||||||
configLocation = "/run/secrets_restic/${serviceName}";
|
configLocation = "/run/secrets_restic/${serviceName}";
|
||||||
generator = name: v: pkgs.writeText "template" (generators.toINIWithGlobalSection {} { globalSection = v; });
|
generator = name: v: pkgs.writeText "template" (generators.toINIWithGlobalSection {} { globalSection = v; });
|
||||||
user = instance.request.user;
|
user = instance.request.user;
|
||||||
|
|
@ -308,48 +310,45 @@ in
|
||||||
serviceConfig.LoadCredential = script.loadCredentials;
|
serviceConfig.LoadCredential = script.loadCredentials;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
mkSettings = name: instance: builtins.map (mkRepositorySettings name instance) instance.settings.repositories;
|
|
||||||
in
|
in
|
||||||
mkMerge (flatten (mapAttrsToList mkSettings (enabledInstances // enabledDatabases)));
|
mkMerge (flatten (mapAttrsToList mkSettings (enabledInstances // enabledDatabases)));
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
system.activationScripts = let
|
system.activationScripts = let
|
||||||
mkEnv = name: instance: repository:
|
mkEnv = name: instance:
|
||||||
nameValuePair "${fullName name repository}_gen"
|
nameValuePair "${fullName name instance.settings.repository}_gen"
|
||||||
(shblib.replaceSecrets {
|
(shblib.replaceSecrets {
|
||||||
userConfig = repository.secrets // {
|
userConfig = instance.settings.repository.secrets // {
|
||||||
RESTIC_PASSWORD_FILE = instance.settings.passphraseFile;
|
RESTIC_PASSWORD_FILE = instance.settings.passphraseFile;
|
||||||
RESTIC_REPOSITORY = repository.path;
|
RESTIC_REPOSITORY = instance.settings.repository.path;
|
||||||
};
|
};
|
||||||
resultPath = "/run/secrets_restic_env/${fullName name repository}";
|
resultPath = "/run/secrets_restic_env/${fullName name instance.settings.repository}";
|
||||||
generator = name: v: pkgs.writeText (fullName name repository) (generators.toINIWithGlobalSection {} { globalSection = v; });
|
generator = name: v: pkgs.writeText (fullName name instance.settings.repository) (generators.toINIWithGlobalSection {} { globalSection = v; });
|
||||||
user = instance.request.user;
|
user = instance.request.user;
|
||||||
});
|
});
|
||||||
mkSettings = name: instance: builtins.map (mkEnv name instance) instance.settings.repositories;
|
|
||||||
in
|
in
|
||||||
listToAttrs (flatten (mapAttrsToList mkSettings (cfg.instances // cfg.databases)));
|
listToAttrs (flatten (mapAttrsToList mkEnv (cfg.instances // cfg.databases)));
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
environment.systemPackages = let
|
environment.systemPackages = let
|
||||||
mkResticBinary = name: instance: repository:
|
mkResticBinary = name: instance:
|
||||||
pkgs.writeShellScriptBin (fullName name repository) ''
|
pkgs.writeShellScriptBin (fullName name instance.settings.repository) ''
|
||||||
export $(grep -v '^#' "/run/secrets_restic_env/${fullName name repository}" \
|
export $(grep -v '^#' "/run/secrets_restic_env/${fullName name instance.settings.repository}" \
|
||||||
| xargs -d '\n')
|
| xargs -d '\n')
|
||||||
${pkgs.restic}/bin/restic $@
|
${pkgs.restic}/bin/restic $@
|
||||||
'';
|
'';
|
||||||
mkSettings = name: instance: builtins.map (mkResticBinary name instance) instance.settings.repositories;
|
|
||||||
in
|
in
|
||||||
flatten (mapAttrsToList mkSettings cfg.instances);
|
flatten (mapAttrsToList mkResticBinary cfg.instances);
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
environment.systemPackages = let
|
environment.systemPackages = let
|
||||||
mkResticBinary = name: instance: repository:
|
mkResticBinary = name: instance:
|
||||||
pkgs.writeShellScriptBin (fullName name repository) ''
|
pkgs.writeShellScriptBin (fullName name instance.settings.repository) ''
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
ls /run/secrets_restic_env/${fullName name repository}
|
ls /run/secrets_restic_env/${fullName name instance.settings.repository}
|
||||||
|
|
||||||
export $(grep -v '^#' "/run/secrets_restic_env/${fullName name repository}" \
|
export $(grep -v '^#' "/run/secrets_restic_env/${fullName name instance.settings.repository}" \
|
||||||
| xargs -d '\n')
|
| xargs -d '\n')
|
||||||
|
|
||||||
set -x
|
set -x
|
||||||
|
|
@ -361,9 +360,8 @@ in
|
||||||
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}"
|
||||||
fi
|
fi
|
||||||
'';
|
'';
|
||||||
mkSettings = name: instance: builtins.map (mkResticBinary name instance) instance.settings.repositories;
|
|
||||||
in
|
in
|
||||||
flatten (mapAttrsToList mkSettings cfg.databases);
|
flatten (mapAttrsToList mkResticBinary cfg.databases);
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
84
modules/contracts/databasebackup/test.nix
Normal file
84
modules/contracts/databasebackup/test.nix
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
{ pkgs, lib, ... }:
|
||||||
|
let
|
||||||
|
pkgs' = pkgs;
|
||||||
|
|
||||||
|
testLib = pkgs.callPackage ../../../test/common.nix {};
|
||||||
|
|
||||||
|
inherit (lib) getAttrFromPath mkIf optionalAttrs setAttrByPath;
|
||||||
|
in
|
||||||
|
{ name,
|
||||||
|
requesterRoot,
|
||||||
|
providerRoot,
|
||||||
|
providerExtraConfig ? null, # { username, database } -> attrset
|
||||||
|
modules ? [],
|
||||||
|
username ? "me",
|
||||||
|
database ? "me",
|
||||||
|
settings, # repository -> attrset
|
||||||
|
}: pkgs.testers.runNixOSTest {
|
||||||
|
inherit name;
|
||||||
|
|
||||||
|
nodes.machine = { config, ... }: {
|
||||||
|
imports = ( testLib.baseImports pkgs' ) ++ modules;
|
||||||
|
config = lib.mkMerge [
|
||||||
|
(setAttrByPath providerRoot {
|
||||||
|
request = (getAttrFromPath requesterRoot config).backup;
|
||||||
|
settings = settings "/opt/repos/database";
|
||||||
|
})
|
||||||
|
(mkIf (username != "root") {
|
||||||
|
users.users.${username} = {
|
||||||
|
isSystemUser = true;
|
||||||
|
extraGroups = [ "sudoers" ];
|
||||||
|
group = "root";
|
||||||
|
};
|
||||||
|
})
|
||||||
|
(optionalAttrs (providerExtraConfig != null) (providerExtraConfig { inherit username database; }))
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
testScript = { nodes, ... }: let
|
||||||
|
provider = (getAttrFromPath providerRoot nodes.machine).result;
|
||||||
|
in ''
|
||||||
|
import csv
|
||||||
|
|
||||||
|
start_all()
|
||||||
|
machine.wait_for_unit("postgresql.service")
|
||||||
|
machine.wait_for_open_port(5432)
|
||||||
|
|
||||||
|
def peer_cmd(cmd, db="me"):
|
||||||
|
return "sudo -u me psql -U me {db} --csv --command \"{cmd}\"".format(cmd=cmd, db=db)
|
||||||
|
|
||||||
|
def query(query):
|
||||||
|
res = machine.succeed(peer_cmd(query))
|
||||||
|
return list(dict(l) for l in csv.DictReader(res.splitlines()))
|
||||||
|
|
||||||
|
def cmp_tables(a, b):
|
||||||
|
for i in range(max(len(a), len(b))):
|
||||||
|
diff = set(a[i]) ^ set(b[i])
|
||||||
|
if len(diff) > 0:
|
||||||
|
raise Exception(i, diff)
|
||||||
|
|
||||||
|
table = [{'name': 'car', 'count': '1'}, {'name': 'lollipop', 'count': '2'}]
|
||||||
|
|
||||||
|
with subtest("create fixture"):
|
||||||
|
machine.succeed(peer_cmd("CREATE TABLE test (name text, count int)"))
|
||||||
|
machine.succeed(peer_cmd("INSERT INTO test VALUES ('car', 1), ('lollipop', 2)"))
|
||||||
|
|
||||||
|
res = query("SELECT * FROM test")
|
||||||
|
cmp_tables(res, table)
|
||||||
|
|
||||||
|
with subtest("backup"):
|
||||||
|
print(machine.succeed("systemctl cat ${provider.backupService}"))
|
||||||
|
machine.succeed("systemctl start ${provider.backupService}")
|
||||||
|
|
||||||
|
with subtest("drop database"):
|
||||||
|
machine.succeed(peer_cmd("DROP DATABASE me", db="postgres"))
|
||||||
|
|
||||||
|
with subtest("restore"):
|
||||||
|
print(machine.succeed("readlink -f $(type ${provider.restoreScript})"))
|
||||||
|
machine.succeed("${provider.restoreScript} restore latest ")
|
||||||
|
|
||||||
|
with subtest("check restoration"):
|
||||||
|
res = query("SELECT * FROM test")
|
||||||
|
cmp_tables(res, table)
|
||||||
|
'';
|
||||||
|
}
|
||||||
|
|
@ -7,5 +7,6 @@
|
||||||
ssl = import ./ssl.nix { inherit lib; };
|
ssl = import ./ssl.nix { inherit lib; };
|
||||||
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; };
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -179,91 +179,4 @@ in
|
||||||
machine.fail(tcpip_cmd("me", "me", "5432", "oops"), timeout=10)
|
machine.fail(tcpip_cmd("me", "me", "5432", "oops"), timeout=10)
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
backup = pkgs.testers.runNixOSTest {
|
|
||||||
name = "postgresql-backup";
|
|
||||||
|
|
||||||
nodes.machine = { config, pkgs, ... }: {
|
|
||||||
imports = [
|
|
||||||
(pkgs'.path + "/nixos/modules/profiles/headless.nix")
|
|
||||||
(pkgs'.path + "/nixos/modules/profiles/qemu-guest.nix")
|
|
||||||
../../modules/blocks/postgresql.nix
|
|
||||||
../../modules/blocks/restic.nix
|
|
||||||
];
|
|
||||||
|
|
||||||
users.users.me = {
|
|
||||||
isSystemUser = true;
|
|
||||||
group = "me";
|
|
||||||
extraGroups = [ "sudoers" ];
|
|
||||||
};
|
|
||||||
users.groups.me = {};
|
|
||||||
|
|
||||||
services.postgresql.enable = true;
|
|
||||||
shb.postgresql.ensures = [
|
|
||||||
{
|
|
||||||
username = "me";
|
|
||||||
database = "me";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
shb.restic.databases."postgres".request = config.shb.postgresql.backup;
|
|
||||||
shb.restic.databases."postgres".settings = {
|
|
||||||
enable = true;
|
|
||||||
|
|
||||||
passphraseFile = toString (pkgs.writeText "passphrase" "PassPhrase");
|
|
||||||
repositories = [
|
|
||||||
{
|
|
||||||
path = "/opt/repos/postgres";
|
|
||||||
timerConfig = {
|
|
||||||
OnCalendar = "00:00:00";
|
|
||||||
};
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
testScript = { nodes, ... }: ''
|
|
||||||
import csv
|
|
||||||
|
|
||||||
start_all()
|
|
||||||
machine.wait_for_unit("postgresql.service")
|
|
||||||
machine.wait_for_open_port(5432)
|
|
||||||
|
|
||||||
def peer_cmd(cmd, db="me"):
|
|
||||||
return "sudo -u me psql -U me {db} --csv --command \"{cmd}\"".format(cmd=cmd, db=db)
|
|
||||||
|
|
||||||
def query(query):
|
|
||||||
res = machine.succeed(peer_cmd(query))
|
|
||||||
return list(dict(l) for l in csv.DictReader(res.splitlines()))
|
|
||||||
|
|
||||||
def cmp_tables(a, b):
|
|
||||||
for i in range(max(len(a), len(b))):
|
|
||||||
diff = set(a[i]) ^ set(b[i])
|
|
||||||
if len(diff) > 0:
|
|
||||||
raise Exception(i, diff)
|
|
||||||
|
|
||||||
table = [{'name': 'car', 'count': '1'}, {'name': 'lollipop', 'count': '2'}]
|
|
||||||
|
|
||||||
with subtest("create fixture"):
|
|
||||||
machine.succeed(peer_cmd("CREATE TABLE test (name text, count int)"))
|
|
||||||
machine.succeed(peer_cmd("INSERT INTO test VALUES ('car', 1), ('lollipop', 2)"))
|
|
||||||
|
|
||||||
res = query("SELECT * FROM test")
|
|
||||||
cmp_tables(res, table)
|
|
||||||
|
|
||||||
with subtest("backup"):
|
|
||||||
print(machine.succeed("systemctl cat restic-backups-postgres_opt_repos_postgres.service"))
|
|
||||||
machine.succeed("systemctl start restic-backups-postgres_opt_repos_postgres.service")
|
|
||||||
|
|
||||||
with subtest("drop database"):
|
|
||||||
machine.succeed(peer_cmd("DROP DATABASE me", db="postgres"))
|
|
||||||
|
|
||||||
with subtest("restore"):
|
|
||||||
print(machine.succeed("readlink -f $(type restic-backups-postgres_opt_repos_postgres)"))
|
|
||||||
machine.succeed("restic-backups-postgres_opt_repos_postgres restore latest ")
|
|
||||||
|
|
||||||
with subtest("check restoration"):
|
|
||||||
res = query("SELECT * FROM test")
|
|
||||||
cmp_tables(res, table)
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
32
test/contracts/databasebackup.nix
Normal file
32
test/contracts/databasebackup.nix
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
{ pkgs, ... }:
|
||||||
|
let
|
||||||
|
contracts = pkgs.callPackage ../../modules/contracts {};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
restic_postgres = contracts.test.databasebackup {
|
||||||
|
name = "restic_postgres";
|
||||||
|
requesterRoot = [ "shb" "postgresql" ];
|
||||||
|
providerRoot = [ "shb" "restic" "databases" "postgresql" ];
|
||||||
|
modules = [
|
||||||
|
../../modules/blocks/postgresql.nix
|
||||||
|
../../modules/blocks/restic.nix
|
||||||
|
];
|
||||||
|
settings = repository: {
|
||||||
|
enable = true;
|
||||||
|
passphraseFile = toString (pkgs.writeText "passphrase" "PassPhrase");
|
||||||
|
repository = {
|
||||||
|
path = repository;
|
||||||
|
timerConfig = {
|
||||||
|
OnCalendar = "00:00:00";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
providerExtraConfig = { username, database, ... }: {
|
||||||
|
shb.postgresql.ensures = [
|
||||||
|
{
|
||||||
|
inherit username database;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue