add postgresql backup hooks
This commit is contained in:
parent
f71295e640
commit
6dec2a9222
|
@ -44,6 +44,7 @@
|
||||||
zt-tcp-relay = import ./zt-tcp-relay nixosTestArgs;
|
zt-tcp-relay = import ./zt-tcp-relay nixosTestArgs;
|
||||||
borgbackup = import ./borgbackup nixosTestArgs;
|
borgbackup = import ./borgbackup nixosTestArgs;
|
||||||
syncthing = import ./syncthing nixosTestArgs;
|
syncthing = import ./syncthing nixosTestArgs;
|
||||||
|
postgresql = import ./postgresql nixosTestArgs;
|
||||||
wayland-proxy-virtwl = import ./wayland-proxy-virtwl nixosTestArgs;
|
wayland-proxy-virtwl = import ./wayland-proxy-virtwl nixosTestArgs;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
22
checks/postgresql/default.nix
Normal file
22
checks/postgresql/default.nix
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
(import ../lib/container-test.nix) (
|
||||||
|
{
|
||||||
|
name = "postgresql";
|
||||||
|
|
||||||
|
nodes.machine = { self, ... }:
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
self.nixosModules.clanCore
|
||||||
|
self.clanModules.postgresql
|
||||||
|
self.clanModules.localbackup
|
||||||
|
];
|
||||||
|
clan.postgresl.databases = [ "test" ];
|
||||||
|
clan.localbackup.targets.hdd.directory = "/mnt/external-disk";
|
||||||
|
};
|
||||||
|
testScript = ''
|
||||||
|
start_all()
|
||||||
|
machine.succeed("systemctl status postgresql")
|
||||||
|
machine.wait_for_unit("postgresql")
|
||||||
|
machine.succeed("localbackup-create")
|
||||||
|
machine.succeed("ls -la /var/backups/postgresql")
|
||||||
|
'';
|
||||||
|
})
|
|
@ -51,7 +51,9 @@ in
|
||||||
|
|
||||||
config = lib.mkIf (cfg.destinations != { }) {
|
config = lib.mkIf (cfg.destinations != { }) {
|
||||||
services.borgbackup.jobs = lib.mapAttrs (_: dest: {
|
services.borgbackup.jobs = lib.mapAttrs (_: dest: {
|
||||||
paths = lib.flatten (map (state: state.folders) (lib.attrValues config.clanCore.state));
|
paths = lib.unique (
|
||||||
|
lib.flatten (map (state: state.folders) (lib.attrValues config.clanCore.state))
|
||||||
|
);
|
||||||
exclude = [ "*.pyc" ];
|
exclude = [ "*.pyc" ];
|
||||||
repo = dest.repo;
|
repo = dest.repo;
|
||||||
environment.BORG_RSH = dest.rsh;
|
environment.BORG_RSH = dest.rsh;
|
||||||
|
@ -60,6 +62,23 @@ in
|
||||||
persistentTimer = true;
|
persistentTimer = true;
|
||||||
preHook = ''
|
preHook = ''
|
||||||
set -x
|
set -x
|
||||||
|
declare -A preCommandErrors
|
||||||
|
${lib.concatMapStringsSep "\n" (state: ''
|
||||||
|
echo "Running pre-backup command for ${state.name}"
|
||||||
|
if ! ${state.preBackupCommand} then
|
||||||
|
preCommandErrors["${state.name}"]=1
|
||||||
|
fi
|
||||||
|
'') (lib.attrValues config.clanCore.state)}
|
||||||
|
'';
|
||||||
|
postPrune = ''
|
||||||
|
# report any preBackupCommand errors
|
||||||
|
if [[ ''${#preCommandErrors[@]} -gt 0 ]]; then
|
||||||
|
echo "PreBackupCommand failed for the following services:"
|
||||||
|
for state in "''${!preCommandErrors[@]}"; do
|
||||||
|
echo " $state"
|
||||||
|
done
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
'';
|
'';
|
||||||
|
|
||||||
encryption = {
|
encryption = {
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
localsend = ./localsend;
|
localsend = ./localsend;
|
||||||
matrix-synapse = ./matrix-synapse;
|
matrix-synapse = ./matrix-synapse;
|
||||||
moonlight = ./moonlight;
|
moonlight = ./moonlight;
|
||||||
|
postgresql = ./postgresql;
|
||||||
root-password = ./root-password;
|
root-password = ./root-password;
|
||||||
sshd = ./sshd;
|
sshd = ./sshd;
|
||||||
sunshine = ./sunshine;
|
sunshine = ./sunshine;
|
||||||
|
|
|
@ -6,7 +6,8 @@
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
cfg = config.clan.localbackup;
|
cfg = config.clan.localbackup;
|
||||||
rsnapshotConfig = target: states: ''
|
uniqueFolders = lib.unique (lib.flatten (lib.mapAttrsToList (name: state: state.folders) config.clanCore.state));
|
||||||
|
rsnapshotConfig = target: ''
|
||||||
config_version 1.2
|
config_version 1.2
|
||||||
snapshot_root ${target.directory}
|
snapshot_root ${target.directory}
|
||||||
sync_first 1
|
sync_first 1
|
||||||
|
@ -21,6 +22,13 @@ let
|
||||||
cmd_preexec ${pkgs.writeShellScript "preexec.sh" ''
|
cmd_preexec ${pkgs.writeShellScript "preexec.sh" ''
|
||||||
set -efu -o pipefail
|
set -efu -o pipefail
|
||||||
${target.preBackupHook}
|
${target.preBackupHook}
|
||||||
|
|
||||||
|
# FIXME: we currently fail the backup if the pre-backup command fails
|
||||||
|
# This is not ideal, but at least most of the time we run backup commands in foreground.
|
||||||
|
${lib.concatMapStringsSep "\n" (state: ''
|
||||||
|
echo "Running pre-backup command for ${state.name}"
|
||||||
|
${state.preBackupCommand}
|
||||||
|
'') (lib.attrValues config.clanCore.state)}
|
||||||
''}
|
''}
|
||||||
''}
|
''}
|
||||||
|
|
||||||
|
@ -31,11 +39,9 @@ let
|
||||||
''}
|
''}
|
||||||
''}
|
''}
|
||||||
retain snapshot ${builtins.toString config.clan.localbackup.snapshots}
|
retain snapshot ${builtins.toString config.clan.localbackup.snapshots}
|
||||||
${lib.concatMapStringsSep "\n" (state: ''
|
|
||||||
${lib.concatMapStringsSep "\n" (folder: ''
|
${lib.concatMapStringsSep "\n" (folder: ''
|
||||||
backup ${folder} ${config.networking.hostName}/
|
backup ${folder} ${config.networking.hostName}/
|
||||||
'') state.folders}
|
'') uniqueFolders}
|
||||||
'') states}
|
|
||||||
'';
|
'';
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
@ -132,8 +138,8 @@ in
|
||||||
(
|
(
|
||||||
${mountHook target}
|
${mountHook target}
|
||||||
echo "Creating backup '${target.name}'"
|
echo "Creating backup '${target.name}'"
|
||||||
rsnapshot -c "${pkgs.writeText "rsnapshot.conf" (rsnapshotConfig target (lib.attrValues config.clanCore.state))}" sync
|
rsnapshot -c "${pkgs.writeText "rsnapshot.conf" (rsnapshotConfig target)}" sync
|
||||||
rsnapshot -c "${pkgs.writeText "rsnapshot.conf" (rsnapshotConfig target (lib.attrValues config.clanCore.state))}" snapshot
|
rsnapshot -c "${pkgs.writeText "rsnapshot.conf" (rsnapshotConfig target)}" snapshot
|
||||||
)
|
)
|
||||||
'') (builtins.attrValues cfg.targets)}
|
'') (builtins.attrValues cfg.targets)}
|
||||||
'')
|
'')
|
||||||
|
|
|
@ -8,7 +8,8 @@ let
|
||||||
cfg = config.clan.matrix-synapse;
|
cfg = config.clan.matrix-synapse;
|
||||||
nginx-vhost = "matrix.${config.clan.matrix-synapse.domain}";
|
nginx-vhost = "matrix.${config.clan.matrix-synapse.domain}";
|
||||||
element-web =
|
element-web =
|
||||||
pkgs.runCommand "element-web-with-config" { nativeBuildInputs = [ pkgs.buildPackages.jq ]; } ''
|
pkgs.runCommand "element-web-with-config" { nativeBuildInputs = [ pkgs.buildPackages.jq ]; }
|
||||||
|
''
|
||||||
cp -r ${pkgs.element-web} $out
|
cp -r ${pkgs.element-web} $out
|
||||||
chmod -R u+w $out
|
chmod -R u+w $out
|
||||||
jq '."default_server_config"."m.homeserver" = { "base_url": "https://${nginx-vhost}:443", "server_name": "${config.clan.matrix-synapse.domain}" }' \
|
jq '."default_server_config"."m.homeserver" = { "base_url": "https://${nginx-vhost}:443", "server_name": "${config.clan.matrix-synapse.domain}" }' \
|
||||||
|
@ -71,7 +72,8 @@ in
|
||||||
};
|
};
|
||||||
systemd.tmpfiles.settings."synapse" = {
|
systemd.tmpfiles.settings."synapse" = {
|
||||||
"/run/synapse-registration-shared-secret.yaml" = {
|
"/run/synapse-registration-shared-secret.yaml" = {
|
||||||
C.argument = config.clanCore.facts.services.matrix-synapse.secret.synapse-registration_shared_secret.path;
|
C.argument =
|
||||||
|
config.clanCore.facts.services.matrix-synapse.secret.synapse-registration_shared_secret.path;
|
||||||
z = {
|
z = {
|
||||||
mode = "0400";
|
mode = "0400";
|
||||||
user = "matrix-synapse";
|
user = "matrix-synapse";
|
||||||
|
|
2
clanModules/postgresql/README.md
Normal file
2
clanModules/postgresql/README.md
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
A free and open-source relational database management system (RDBMS) emphasizing extensibility and SQL compliance.
|
||||||
|
---
|
52
clanModules/postgresql/default.nix
Normal file
52
clanModules/postgresql/default.nix
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
{
|
||||||
|
pkgs,
|
||||||
|
lib,
|
||||||
|
config,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
createDatatbaseState =
|
||||||
|
db:
|
||||||
|
let
|
||||||
|
folder = "/var/backup/postgresql/${db}";
|
||||||
|
curFile = "${folder}/dump.sql.zstd";
|
||||||
|
prevFile = "${folder}/dump.sql.prev.zstd";
|
||||||
|
inProgressFile = "${folder}/dump.sql.in-progress.zstd";
|
||||||
|
in
|
||||||
|
{
|
||||||
|
folders = [ folder ];
|
||||||
|
preBackupCommand = ''
|
||||||
|
(
|
||||||
|
umask 0077 # ensure backup is only readable by postgres user
|
||||||
|
if [ -e ${curFile} ]; then
|
||||||
|
mv ${curFile} ${prevFile}
|
||||||
|
fi
|
||||||
|
pg_dump -C ${db} | \
|
||||||
|
${pkgs.zstd}/bin/zstd --rsyncable | \
|
||||||
|
> ${inProgressFile}
|
||||||
|
mv ${inProgressFile} ${curFile}
|
||||||
|
)
|
||||||
|
'';
|
||||||
|
postRestoreCommand = ''
|
||||||
|
if [[ -f ${prevFile} ]]; then
|
||||||
|
zstd --decompress --stdout ${prevFile} | psql -d ${db}
|
||||||
|
fi
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.clan.postgresl = {
|
||||||
|
databases = lib.mkOption {
|
||||||
|
type = lib.types.listOf lib.types.str;
|
||||||
|
default = [ "clan" ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
config = {
|
||||||
|
services.postgresql.enable = true;
|
||||||
|
clanCore.state = lib.listToAttrs (
|
||||||
|
builtins.map (
|
||||||
|
db: lib.nameValuePair "postgresql-${db}" (createDatatbaseState db)
|
||||||
|
) config.clan.postgresl.databases
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
|
@ -60,6 +60,7 @@ nav:
|
||||||
- reference/clanModules/localsend.md
|
- reference/clanModules/localsend.md
|
||||||
- reference/clanModules/matrix-synapse.md
|
- reference/clanModules/matrix-synapse.md
|
||||||
- reference/clanModules/moonlight.md
|
- reference/clanModules/moonlight.md
|
||||||
|
- reference/clanModules/postgresql.md
|
||||||
- reference/clanModules/root-password.md
|
- reference/clanModules/root-password.md
|
||||||
- reference/clanModules/sshd.md
|
- reference/clanModules/sshd.md
|
||||||
- reference/clanModules/static-hosts.md
|
- reference/clanModules/static-hosts.md
|
||||||
|
|
|
@ -17,6 +17,15 @@
|
||||||
Folder where state resides in
|
Folder where state resides in
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
preBackupCommand = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = null;
|
||||||
|
description = ''
|
||||||
|
script to run before backing up the state dir
|
||||||
|
This is for example useful for services that require an export of their state
|
||||||
|
e.g. a database dump
|
||||||
|
'';
|
||||||
|
};
|
||||||
preRestoreCommand = lib.mkOption {
|
preRestoreCommand = lib.mkOption {
|
||||||
type = lib.types.nullOr lib.types.str;
|
type = lib.types.nullOr lib.types.str;
|
||||||
default = null;
|
default = null;
|
||||||
|
|
|
@ -17,7 +17,7 @@ def restore_service(machine: Machine, name: str, provider: str, service: str) ->
|
||||||
folders = backup_folders[service]["folders"]
|
folders = backup_folders[service]["folders"]
|
||||||
env = {}
|
env = {}
|
||||||
env["NAME"] = name
|
env["NAME"] = name
|
||||||
env["FOLDERS"] = ":".join(folders)
|
env["FOLDERS"] = ":".join(set(folders))
|
||||||
|
|
||||||
if pre_restore := backup_folders[service]["preRestoreCommand"]:
|
if pre_restore := backup_folders[service]["preRestoreCommand"]:
|
||||||
proc = machine.target_host.run(
|
proc = machine.target_host.run(
|
||||||
|
@ -58,12 +58,23 @@ def restore_backup(
|
||||||
name: str,
|
name: str,
|
||||||
service: str | None = None,
|
service: str | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
errors = []
|
||||||
if service is None:
|
if service is None:
|
||||||
backup_folders = json.loads(machine.eval_nix("config.clanCore.state"))
|
backup_folders = json.loads(machine.eval_nix("config.clanCore.state"))
|
||||||
for _service in backup_folders:
|
for _service in backup_folders:
|
||||||
|
try:
|
||||||
restore_service(machine, name, provider, _service)
|
restore_service(machine, name, provider, _service)
|
||||||
|
except ClanError as e:
|
||||||
|
errors.append(f"{_service}: {e}")
|
||||||
else:
|
else:
|
||||||
|
try:
|
||||||
restore_service(machine, name, provider, service)
|
restore_service(machine, name, provider, service)
|
||||||
|
except ClanError as e:
|
||||||
|
errors.append(f"{service}: {e}")
|
||||||
|
if errors:
|
||||||
|
raise ClanError(
|
||||||
|
"Restore failed for the following services:\n" + "\n".join(errors)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def restore_command(args: argparse.Namespace) -> None:
|
def restore_command(args: argparse.Namespace) -> None:
|
||||||
|
|
Loading…
Reference in New Issue
Block a user