From ef9ed1ebea29410e3dc208304ccfa5d054aa3825 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Wed, 19 Jun 2024 11:42:14 +0200 Subject: [PATCH] clan.core.state: wrap all commands in shell scripts Otherwise we cannot execute them via ssh and also have nix store dependencies. --- checks/backups/flake-module.nix | 20 +++---- checks/postgresql/default.nix | 4 +- clanModules/borgbackup/default.nix | 4 +- clanModules/localbackup/default.nix | 3 +- clanModules/postgresql/default.nix | 35 +++++++++++- nixosModules/clanCore/state.nix | 82 +++++++++++++++++++++++++---- 6 files changed, 117 insertions(+), 31 deletions(-) diff --git a/checks/backups/flake-module.nix b/checks/backups/flake-module.nix index b4822942..6c6dad17 100644 --- a/checks/backups/flake-module.nix +++ b/checks/backups/flake-module.nix @@ -70,15 +70,7 @@ }; clan.core.facts.secretStore = "vm"; - environment.systemPackages = [ - self.packages.${pkgs.system}.clan-cli - (pkgs.writeShellScriptBin "pre-restore-command" '' - touch /var/test-service/pre-restore-command - '') - (pkgs.writeShellScriptBin "post-restore-command" '' - touch /var/test-service/post-restore-command - '') - ]; + environment.systemPackages = [ self.packages.${pkgs.system}.clan-cli ]; environment.etc.install-closure.source = "${closureInfo}/store-paths"; nix.settings = { substituters = lib.mkForce [ ]; @@ -90,11 +82,15 @@ clan.core.state.test-backups.folders = [ "/var/test-backups" ]; clan.core.state.test-service = { - preBackupCommand = '' + preBackupScript = '' touch /var/test-service/pre-backup-command ''; - preRestoreCommand = "pre-restore-command"; - postRestoreCommand = "post-restore-command"; + preRestoreScript = '' + touch /var/test-service/pre-restore-command + ''; + postRestoreScript = '' + touch /var/test-service/post-restore-command + ''; folders = [ "/var/test-service" ]; }; clan.borgbackup.destinations.test-backup.repo = "borg@machine:."; diff --git a/checks/postgresql/default.nix b/checks/postgresql/default.nix index 4c0182ab..eb1c7e4c 100644 --- a/checks/postgresql/default.nix +++ b/checks/postgresql/default.nix @@ -50,7 +50,7 @@ machine.succeed(""" set -x - ${nodes.machine.clan.core.state.postgresql-test.postRestoreCommand} + ${nodes.machine.clan.core.state.test.postRestoreCommand} """) machine.succeed("runuser -u postgres -- /run/current-system/sw/bin/psql -l >&2") machine.succeed("runuser -u postgres -- /run/current-system/sw/bin/psql -d test -c '\dt' >&2") @@ -66,7 +66,7 @@ # check if restore works if the database does not exist machine.succeed("runuser -u postgres -- dropdb test") - machine.succeed("${nodes.machine.clanCore.state.postgresql-test.postRestoreCommand}") + machine.succeed("${nodes.machine.clan.core.state.test.postRestoreCommand}") machine.succeed("runuser -u postgres -- /run/current-system/sw/bin/psql -d test -c '\dt' >&2") ''; }) diff --git a/clanModules/borgbackup/default.nix b/clanModules/borgbackup/default.nix index 30a28b44..a625b83f 100644 --- a/clanModules/borgbackup/default.nix +++ b/clanModules/borgbackup/default.nix @@ -13,14 +13,14 @@ let state: lib.optionalString (state.preBackupCommand != null) '' echo "Running pre-backup command for ${state.name}" - if ! ( ${state.preBackupCommand} ) then + if ! /run/current-system/sw/bin/${state.preBackupCommand}; then preCommandErrors["${state.name}"]=1 fi '' ) (lib.attrValues config.clan.core.state)} if [[ ''${#preCommandErrors[@]} -gt 0 ]]; then - echo "PreBackupCommand failed for the following services:" + echo "pre-backup commands failed for the following services:" for state in "''${!preCommandErrors[@]}"; do echo " $state" done diff --git a/clanModules/localbackup/default.nix b/clanModules/localbackup/default.nix index 6281a69f..0adc46fd 100644 --- a/clanModules/localbackup/default.nix +++ b/clanModules/localbackup/default.nix @@ -125,7 +125,6 @@ in } ${lib.concatMapStringsSep "\n" (target: '' ${mountHook target} - set -x echo "Creating backup '${target.name}'" ${lib.optionalString (target.preBackupHook != null) '' @@ -139,7 +138,7 @@ in state: lib.optionalString (state.preBackupCommand != null) '' echo "Running pre-backup command for ${state.name}" - if ! ( ${state.preBackupCommand} ) then + if ! /run/current-system/sw/bin/${state.preBackupCommand}; then preCommandErrors["${state.name}"]=1 fi '' diff --git a/clanModules/postgresql/default.nix b/clanModules/postgresql/default.nix index 82469bb6..589aa081 100644 --- a/clanModules/postgresql/default.nix +++ b/clanModules/postgresql/default.nix @@ -14,7 +14,7 @@ let in { folders = [ folder ]; - preBackupCommand = '' + preBackupScript = '' export PATH=${ lib.makeBinPath [ config.services.postgresql.package @@ -32,7 +32,38 @@ let runuser -u postgres -- pg_dump ${compression} --dbname=${db.name} -Fc -c > "${current}.tmp" mv "${current}.tmp" ${current} ''; - postRestoreCommand = "postgres-db-restore-command-${db.name}"; + postRestoreScript = '' + export PATH=${ + lib.makeBinPath [ + config.services.postgresql.package + config.systemd.package + pkgs.coreutils + pkgs.util-linux + pkgs.zstd + ] + } + while [[ "$(systemctl is-active postgresql)" == activating ]]; do + sleep 1 + done + echo "Waiting for postgres to be ready..." + while ! runuser -u postgres -- psql --port=${builtins.toString config.services.postgresql.settings.port} -d postgres -c "" ; do + if ! systemctl is-active postgresql; then exit 1; fi + sleep 0.1 + done + + if [[ -e "${current}" ]]; then + ( + systemctl stop ${lib.concatStringsSep " " db.restore.stopOnRestore} + trap "systemctl start ${lib.concatStringsSep " " db.restore.stopOnRestore}" EXIT + + mkdir -p "${folder}" + runuser -u postgres -- dropdb "${db.name}" + runuser -u postgres -- pg_restore -C -d postgres "${current}" + ) + else + echo No database backup found, skipping restore + fi + ''; }; createDatabase = db: '' diff --git a/nixosModules/clanCore/state.nix b/nixosModules/clanCore/state.nix index 6750988b..5298ccd9 100644 --- a/nixosModules/clanCore/state.nix +++ b/nixosModules/clanCore/state.nix @@ -1,18 +1,20 @@ -{ lib, ... }: { - # defaults - config.clan.core.state.HOME.folders = [ "/home" ]; - + lib, + pkgs, + config, + ... +}: +{ # interface options.clan.core.state = lib.mkOption { default = { }; type = lib.types.attrsOf ( lib.types.submodule ( - { name, ... }: + { name, config, ... }: { options = { name = lib.mkOption { - type = lib.types.str; + type = lib.types.strMatching "^[a-zA-Z0-9_-]+$"; default = name; description = '' Name of the state @@ -24,8 +26,9 @@ Folder where state resides in ''; }; - preBackupCommand = lib.mkOption { - type = lib.types.nullOr lib.types.str; + + preBackupScript = lib.mkOption { + type = lib.types.nullOr lib.types.lines; default = null; description = '' script to run before backing up the state dir @@ -34,6 +37,15 @@ ''; }; + preBackupCommand = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = if config.preBackupScript == null then null else "pre-backup-${name}"; + readOnly = true; + description = '' + Use this command in backup providers. It contains the content of preBackupScript. + ''; + }; + # TODO: implement this #stopOnRestore = lib.mkOption { # type = lib.types.listOf lib.types.str; @@ -45,8 +57,8 @@ # ''; #}; - preRestoreCommand = lib.mkOption { - type = lib.types.nullOr lib.types.str; + preRestoreScript = lib.mkOption { + type = lib.types.nullOr lib.types.lines; default = null; description = '' script to run before restoring the state dir from a backup @@ -55,8 +67,18 @@ ''; }; - postRestoreCommand = lib.mkOption { + preRestoreCommand = lib.mkOption { type = lib.types.nullOr lib.types.str; + default = if config.preRestoreScript == null then null else "pre-restore-${name}"; + readOnly = true; + description = '' + This command can be called to restore the state dir from a backup. + It contains the content of preRestoreScript. + ''; + }; + + postRestoreScript = lib.mkOption { + type = lib.types.nullOr lib.types.lines; default = null; description = '' script to restore the service after the state dir was restored from a backup @@ -64,9 +86,47 @@ Utilize this to start services which were previously stopped ''; }; + + postRestoreCommand = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = if config.postRestoreScript == null then null else "post-restore-${name}"; + readOnly = true; + description = '' + This command is called after a restore of the state dir from a backup. + + It contains the content of postRestoreScript. + ''; + }; }; } ) ); }; + + # defaults + config.clan.core.state.HOME.folders = [ "/home" ]; + config.environment.systemPackages = lib.optional (config.clan.core.state != { }) ( + pkgs.runCommand "state-commands" { } '' + ${builtins.concatStringsSep "\n" ( + builtins.map (state: '' + writeShellScript() { + local name=$1 + local content=$2 + printf "#!${pkgs.runtimeShell}\nset -eu -o pipefail\n%s" "$content" > $out/bin/$name + } + mkdir -p $out/bin/ + ${lib.optionalString (state.preBackupCommand != null) '' + writeShellScript ${lib.escapeShellArg state.preBackupCommand} ${lib.escapeShellArg state.preBackupScript} + ''} + ${lib.optionalString (state.preRestoreCommand != null) '' + writeShellScript ${lib.escapeShellArg state.preRestoreCommand} ${lib.escapeShellArg state.preRestoreScript} + ''} + ${lib.optionalString (state.postRestoreCommand != null) '' + writeShellScript ${lib.escapeShellArg state.postRestoreCommand} ${lib.escapeShellArg state.postRestoreScript} + ''} + find $out/bin/ -type f -print0 | xargs --no-run-if-empty -0 chmod 755 + '') (builtins.attrValues config.clan.core.state) + )} + '' + ); }