Merge pull request 'Migrate localbackup to clanServices' (#4656) from migrate-localbackup into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4656
This commit is contained in:
@@ -1,208 +0,0 @@
|
|||||||
{ self, ... }:
|
|
||||||
{
|
|
||||||
clan.machines.test-backup = {
|
|
||||||
imports = [ self.nixosModules.test-backup ];
|
|
||||||
fileSystems."/".device = "/dev/null";
|
|
||||||
boot.loader.grub.device = "/dev/null";
|
|
||||||
};
|
|
||||||
clan.inventory.services = {
|
|
||||||
borgbackup.test-backup = {
|
|
||||||
roles.client.machines = [ "test-backup" ];
|
|
||||||
roles.server.machines = [ "test-backup" ];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
flake.nixosModules = {
|
|
||||||
test-backup =
|
|
||||||
{
|
|
||||||
pkgs,
|
|
||||||
lib,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
let
|
|
||||||
dependencies = [
|
|
||||||
pkgs.stdenv.drvPath
|
|
||||||
]
|
|
||||||
++ builtins.map (i: i.outPath) (builtins.attrValues (builtins.removeAttrs self.inputs [ "self" ]));
|
|
||||||
closureInfo = pkgs.closureInfo { rootPaths = dependencies; };
|
|
||||||
in
|
|
||||||
{
|
|
||||||
imports = [
|
|
||||||
# Do not import inventory modules. They should be configured via 'clan.inventory'
|
|
||||||
#
|
|
||||||
# TODO: Configure localbackup via inventory
|
|
||||||
self.clanModules.localbackup
|
|
||||||
];
|
|
||||||
# Borgbackup overrides
|
|
||||||
services.borgbackup.repos.test-backups = {
|
|
||||||
path = "/var/lib/borgbackup/test-backups";
|
|
||||||
authorizedKeys = [ (builtins.readFile ../assets/ssh/pubkey) ];
|
|
||||||
};
|
|
||||||
clan.borgbackup.destinations.test-backup.repo = lib.mkForce "borg@machine:.";
|
|
||||||
|
|
||||||
clan.core.networking.targetHost = "machine";
|
|
||||||
networking.hostName = "machine";
|
|
||||||
|
|
||||||
programs.ssh.knownHosts = {
|
|
||||||
machine.hostNames = [ "machine" ];
|
|
||||||
machine.publicKey = builtins.readFile ../assets/ssh/pubkey;
|
|
||||||
};
|
|
||||||
|
|
||||||
services.openssh = {
|
|
||||||
enable = true;
|
|
||||||
settings.UsePAM = false;
|
|
||||||
settings.UseDns = false;
|
|
||||||
hostKeys = [
|
|
||||||
{
|
|
||||||
path = "/root/.ssh/id_ed25519";
|
|
||||||
type = "ed25519";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
users.users.root.openssh.authorizedKeys.keyFiles = [ ../assets/ssh/pubkey ];
|
|
||||||
|
|
||||||
# This is needed to unlock the user for sshd
|
|
||||||
# Because we use sshd without setuid binaries
|
|
||||||
users.users.borg.initialPassword = "hello";
|
|
||||||
|
|
||||||
systemd.tmpfiles.settings."vmsecrets" = {
|
|
||||||
"/root/.ssh/id_ed25519" = {
|
|
||||||
C.argument = "${../assets/ssh/privkey}";
|
|
||||||
z = {
|
|
||||||
mode = "0400";
|
|
||||||
user = "root";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
"/etc/secrets/ssh.id_ed25519" = {
|
|
||||||
C.argument = "${../assets/ssh/privkey}";
|
|
||||||
z = {
|
|
||||||
mode = "0400";
|
|
||||||
user = "root";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
"/etc/secrets/borgbackup/borgbackup.ssh" = {
|
|
||||||
C.argument = "${../assets/ssh/privkey}";
|
|
||||||
z = {
|
|
||||||
mode = "0400";
|
|
||||||
user = "root";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
"/etc/secrets/borgbackup/borgbackup.repokey" = {
|
|
||||||
C.argument = builtins.toString (pkgs.writeText "repokey" "repokey12345");
|
|
||||||
z = {
|
|
||||||
mode = "0400";
|
|
||||||
user = "root";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
clan.core.facts.secretStore = "vm";
|
|
||||||
clan.core.vars.settings.secretStore = "vm";
|
|
||||||
|
|
||||||
environment.systemPackages = [ self.packages.${pkgs.system}.clan-cli ];
|
|
||||||
environment.etc.install-closure.source = "${closureInfo}/store-paths";
|
|
||||||
nix.settings = {
|
|
||||||
substituters = lib.mkForce [ ];
|
|
||||||
hashed-mirrors = null;
|
|
||||||
connect-timeout = lib.mkForce 3;
|
|
||||||
flake-registry = pkgs.writeText "flake-registry" ''{"flakes":[],"version":2}'';
|
|
||||||
};
|
|
||||||
system.extraDependencies = dependencies;
|
|
||||||
clan.core.state.test-backups.folders = [ "/var/test-backups" ];
|
|
||||||
|
|
||||||
clan.core.state.test-service = {
|
|
||||||
preBackupScript = ''
|
|
||||||
touch /var/test-service/pre-backup-command
|
|
||||||
'';
|
|
||||||
preRestoreScript = ''
|
|
||||||
touch /var/test-service/pre-restore-command
|
|
||||||
'';
|
|
||||||
postRestoreScript = ''
|
|
||||||
touch /var/test-service/post-restore-command
|
|
||||||
'';
|
|
||||||
folders = [ "/var/test-service" ];
|
|
||||||
};
|
|
||||||
|
|
||||||
fileSystems."/mnt/external-disk" = {
|
|
||||||
device = "/dev/vdb"; # created in tests with virtualisation.emptyDisks
|
|
||||||
autoFormat = true;
|
|
||||||
fsType = "ext4";
|
|
||||||
options = [
|
|
||||||
"defaults"
|
|
||||||
"noauto"
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
clan.localbackup.targets.hdd = {
|
|
||||||
directory = "/mnt/external-disk";
|
|
||||||
preMountHook = ''
|
|
||||||
touch /run/mount-external-disk
|
|
||||||
'';
|
|
||||||
postUnmountHook = ''
|
|
||||||
touch /run/unmount-external-disk
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
perSystem =
|
|
||||||
{ pkgs, ... }:
|
|
||||||
let
|
|
||||||
clanCore = self.checks.x86_64-linux.clan-core-for-checks;
|
|
||||||
in
|
|
||||||
{
|
|
||||||
checks = pkgs.lib.mkIf pkgs.stdenv.isLinux {
|
|
||||||
nixos-test-backups = self.clanLib.test.containerTest {
|
|
||||||
name = "nixos-test-backups";
|
|
||||||
nodes.machine = {
|
|
||||||
imports = [
|
|
||||||
self.nixosModules.clanCore
|
|
||||||
# Some custom overrides for the backup tests
|
|
||||||
self.nixosModules.test-backup
|
|
||||||
]
|
|
||||||
++
|
|
||||||
# import the inventory generated nixosModules
|
|
||||||
self.clan.clanInternals.inventoryClass.machines.test-backup.machineImports;
|
|
||||||
clan.core.settings.directory = ./.;
|
|
||||||
};
|
|
||||||
|
|
||||||
testScript = ''
|
|
||||||
import json
|
|
||||||
start_all()
|
|
||||||
|
|
||||||
# dummy data
|
|
||||||
machine.succeed("mkdir -p /var/test-backups /var/test-service")
|
|
||||||
machine.succeed("echo testing > /var/test-backups/somefile")
|
|
||||||
|
|
||||||
# create
|
|
||||||
machine.succeed("clan backups create --debug --flake ${clanCore} test-backup")
|
|
||||||
machine.wait_until_succeeds("! systemctl is-active borgbackup-job-test-backup >&2")
|
|
||||||
machine.succeed("test -f /run/mount-external-disk")
|
|
||||||
machine.succeed("test -f /run/unmount-external-disk")
|
|
||||||
|
|
||||||
# list
|
|
||||||
backup_id = json.loads(machine.succeed("borg-job-test-backup list --json"))["archives"][0]["archive"]
|
|
||||||
out = machine.succeed("clan backups list --debug --flake ${clanCore} test-backup").strip()
|
|
||||||
print(out)
|
|
||||||
assert backup_id in out, f"backup {backup_id} not found in {out}"
|
|
||||||
localbackup_id = "hdd::/mnt/external-disk/snapshot.0"
|
|
||||||
assert localbackup_id in out, "localbackup not found in {out}"
|
|
||||||
|
|
||||||
## borgbackup restore
|
|
||||||
machine.succeed("rm -f /var/test-backups/somefile")
|
|
||||||
machine.succeed(f"clan backups restore --debug --flake ${clanCore} test-backup borgbackup 'test-backup::borg@machine:.::{backup_id}' >&2")
|
|
||||||
assert machine.succeed("cat /var/test-backups/somefile").strip() == "testing", "restore failed"
|
|
||||||
machine.succeed("test -f /var/test-service/pre-restore-command")
|
|
||||||
machine.succeed("test -f /var/test-service/post-restore-command")
|
|
||||||
machine.succeed("test -f /var/test-service/pre-backup-command")
|
|
||||||
|
|
||||||
## localbackup restore
|
|
||||||
machine.succeed("rm -rf /var/test-backups/somefile /var/test-service/ && mkdir -p /var/test-service")
|
|
||||||
machine.succeed(f"clan backups restore --debug --flake ${clanCore} test-backup localbackup '{localbackup_id}' >&2")
|
|
||||||
assert machine.succeed("cat /var/test-backups/somefile").strip() == "testing", "restore failed"
|
|
||||||
machine.succeed("test -f /var/test-service/pre-restore-command")
|
|
||||||
machine.succeed("test -f /var/test-service/post-restore-command")
|
|
||||||
machine.succeed("test -f /var/test-service/pre-backup-command")
|
|
||||||
'';
|
|
||||||
} { inherit pkgs self; };
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -33,7 +33,6 @@ in
|
|||||||
in
|
in
|
||||||
getClanCoreTestModules
|
getClanCoreTestModules
|
||||||
++ filter pathExists [
|
++ filter pathExists [
|
||||||
./backups/flake-module.nix
|
|
||||||
./devshell/flake-module.nix
|
./devshell/flake-module.nix
|
||||||
./flash/flake-module.nix
|
./flash/flake-module.nix
|
||||||
./impure/flake-module.nix
|
./impure/flake-module.nix
|
||||||
|
|||||||
35
clanServices/localbackup/README.md
Normal file
35
clanServices/localbackup/README.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
## Features
|
||||||
|
|
||||||
|
- Creates incremental snapshots using rsnapshot
|
||||||
|
- Supports multiple backup targets
|
||||||
|
- Mount/unmount hooks for external storage
|
||||||
|
- Pre/post backup hooks for custom scripts
|
||||||
|
- Configurable snapshot retention
|
||||||
|
- Automatic state folder detection
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Enable the localbackup service and configure backup targets:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
instances = {
|
||||||
|
localbackup = {
|
||||||
|
module.name = "@clan/localbackup";
|
||||||
|
module.input = "self";
|
||||||
|
roles.default.machines."machine".settings = {
|
||||||
|
targets.external= {
|
||||||
|
directory = "/mnt/backup";
|
||||||
|
mountpoint = "/mnt/backup";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
The service provides these commands:
|
||||||
|
|
||||||
|
- `localbackup-create`: Create a new backup
|
||||||
|
- `localbackup-list`: List available backups
|
||||||
|
- `localbackup-restore`: Restore from backup (requires NAME and FOLDERS environment variables)
|
||||||
267
clanServices/localbackup/default.nix
Normal file
267
clanServices/localbackup/default.nix
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
{ ... }:
|
||||||
|
{
|
||||||
|
_class = "clan.service";
|
||||||
|
manifest.name = "localbackup";
|
||||||
|
manifest.description = "Automatically backups current machine to local directory.";
|
||||||
|
manifest.categories = [ "System" ];
|
||||||
|
manifest.readme = builtins.readFile ./README.md;
|
||||||
|
|
||||||
|
roles.default = {
|
||||||
|
interface =
|
||||||
|
{ lib, ... }:
|
||||||
|
{
|
||||||
|
|
||||||
|
options = {
|
||||||
|
|
||||||
|
targets = lib.mkOption {
|
||||||
|
type = lib.types.attrsOf (
|
||||||
|
lib.types.submodule (
|
||||||
|
{ name, ... }:
|
||||||
|
{
|
||||||
|
options = {
|
||||||
|
name = lib.mkOption {
|
||||||
|
type = lib.types.strMatching "^[a-zA-Z0-9._-]+$";
|
||||||
|
default = name;
|
||||||
|
description = "the name of the backup job";
|
||||||
|
};
|
||||||
|
directory = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
description = "the directory to backup";
|
||||||
|
};
|
||||||
|
mountpoint = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = null;
|
||||||
|
description = "mountpoint of the directory to backup. If set, the directory will be mounted before the backup and unmounted afterwards";
|
||||||
|
};
|
||||||
|
preMountHook = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.lines;
|
||||||
|
default = null;
|
||||||
|
description = "Shell commands to run before the directory is mounted";
|
||||||
|
};
|
||||||
|
postMountHook = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.lines;
|
||||||
|
default = null;
|
||||||
|
description = "Shell commands to run after the directory is mounted";
|
||||||
|
};
|
||||||
|
preUnmountHook = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.lines;
|
||||||
|
default = null;
|
||||||
|
description = "Shell commands to run before the directory is unmounted";
|
||||||
|
};
|
||||||
|
postUnmountHook = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.lines;
|
||||||
|
default = null;
|
||||||
|
description = "Shell commands to run after the directory is unmounted";
|
||||||
|
};
|
||||||
|
preBackupHook = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.lines;
|
||||||
|
default = null;
|
||||||
|
description = "Shell commands to run before the backup";
|
||||||
|
};
|
||||||
|
postBackupHook = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.lines;
|
||||||
|
default = null;
|
||||||
|
description = "Shell commands to run after the backup";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
# default = { };
|
||||||
|
description = "List of directories where backups are stored";
|
||||||
|
};
|
||||||
|
|
||||||
|
snapshots = lib.mkOption {
|
||||||
|
type = lib.types.int;
|
||||||
|
default = 20;
|
||||||
|
description = "Number of snapshots to keep";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
perInstance =
|
||||||
|
{
|
||||||
|
settings,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
{
|
||||||
|
nixosModule =
|
||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
|
||||||
|
let
|
||||||
|
mountHook = target: ''
|
||||||
|
if [[ -x /run/current-system/sw/bin/localbackup-mount-${target.name} ]]; then
|
||||||
|
/run/current-system/sw/bin/localbackup-mount-${target.name}
|
||||||
|
fi
|
||||||
|
if [[ -x /run/current-system/sw/bin/localbackup-unmount-${target.name} ]]; then
|
||||||
|
trap "/run/current-system/sw/bin/localbackup-unmount-${target.name}" EXIT
|
||||||
|
fi
|
||||||
|
'';
|
||||||
|
|
||||||
|
uniqueFolders = lib.unique (
|
||||||
|
lib.flatten (lib.mapAttrsToList (_name: state: state.folders) config.clan.core.state)
|
||||||
|
);
|
||||||
|
|
||||||
|
rsnapshotConfig = target: ''
|
||||||
|
config_version 1.2
|
||||||
|
snapshot_root ${target.directory}
|
||||||
|
sync_first 1
|
||||||
|
cmd_cp ${pkgs.coreutils}/bin/cp
|
||||||
|
cmd_rm ${pkgs.coreutils}/bin/rm
|
||||||
|
cmd_rsync ${pkgs.rsync}/bin/rsync
|
||||||
|
cmd_ssh ${pkgs.openssh}/bin/ssh
|
||||||
|
cmd_logger ${pkgs.inetutils}/bin/logger
|
||||||
|
cmd_du ${pkgs.coreutils}/bin/du
|
||||||
|
cmd_rsnapshot_diff ${pkgs.rsnapshot}/bin/rsnapshot-diff
|
||||||
|
|
||||||
|
${lib.optionalString (target.postBackupHook != null) ''
|
||||||
|
cmd_postexec ${pkgs.writeShellScript "postexec.sh" ''
|
||||||
|
set -efu -o pipefail
|
||||||
|
${target.postBackupHook}
|
||||||
|
''}
|
||||||
|
''}
|
||||||
|
retain snapshot ${builtins.toString settings.snapshots}
|
||||||
|
${lib.concatMapStringsSep "\n" (folder: ''
|
||||||
|
backup ${folder} ${config.networking.hostName}/
|
||||||
|
'') uniqueFolders}
|
||||||
|
'';
|
||||||
|
in
|
||||||
|
|
||||||
|
{
|
||||||
|
|
||||||
|
environment.systemPackages = [
|
||||||
|
(pkgs.writeShellScriptBin "localbackup-create" ''
|
||||||
|
set -efu -o pipefail
|
||||||
|
export PATH=${
|
||||||
|
lib.makeBinPath [
|
||||||
|
pkgs.rsnapshot
|
||||||
|
pkgs.coreutils
|
||||||
|
pkgs.util-linux
|
||||||
|
]
|
||||||
|
}
|
||||||
|
${lib.concatMapStringsSep "\n" (target: ''
|
||||||
|
${mountHook target}
|
||||||
|
echo "Creating backup '${target.name}'"
|
||||||
|
|
||||||
|
${lib.optionalString (target.preBackupHook != null) ''
|
||||||
|
(
|
||||||
|
${target.preBackupHook}
|
||||||
|
)
|
||||||
|
''}
|
||||||
|
|
||||||
|
declare -A preCommandErrors
|
||||||
|
${lib.concatMapStringsSep "\n" (
|
||||||
|
state:
|
||||||
|
lib.optionalString (state.preBackupCommand != null) ''
|
||||||
|
echo "Running pre-backup command for ${state.name}"
|
||||||
|
if ! /run/current-system/sw/bin/${state.preBackupCommand}; then
|
||||||
|
preCommandErrors["${state.name}"]=1
|
||||||
|
fi
|
||||||
|
''
|
||||||
|
) (builtins.attrValues config.clan.core.state)}
|
||||||
|
|
||||||
|
rsnapshot -c "${pkgs.writeText "rsnapshot.conf" (rsnapshotConfig target)}" sync
|
||||||
|
rsnapshot -c "${pkgs.writeText "rsnapshot.conf" (rsnapshotConfig target)}" snapshot
|
||||||
|
'') (builtins.attrValues settings.targets)}'')
|
||||||
|
(pkgs.writeShellScriptBin "localbackup-list" ''
|
||||||
|
set -efu -o pipefail
|
||||||
|
export PATH=${
|
||||||
|
lib.makeBinPath [
|
||||||
|
pkgs.jq
|
||||||
|
pkgs.findutils
|
||||||
|
pkgs.coreutils
|
||||||
|
pkgs.util-linux
|
||||||
|
]
|
||||||
|
}
|
||||||
|
(${
|
||||||
|
lib.concatMapStringsSep "\n" (target: ''
|
||||||
|
(
|
||||||
|
${mountHook target}
|
||||||
|
find ${lib.escapeShellArg target.directory} -mindepth 1 -maxdepth 1 -name "snapshot.*" -print0 -type d \
|
||||||
|
| jq -Rs 'split("\u0000") | .[] | select(. != "") | { "name": ("${target.name}::" + .)}'
|
||||||
|
)
|
||||||
|
'') (builtins.attrValues settings.targets)
|
||||||
|
}) | jq -s .
|
||||||
|
'')
|
||||||
|
(pkgs.writeShellScriptBin "localbackup-restore" ''
|
||||||
|
set -efu -o pipefail
|
||||||
|
export PATH=${
|
||||||
|
lib.makeBinPath [
|
||||||
|
pkgs.rsync
|
||||||
|
pkgs.coreutils
|
||||||
|
pkgs.util-linux
|
||||||
|
pkgs.gawk
|
||||||
|
]
|
||||||
|
}
|
||||||
|
if [[ "''${NAME:-}" == "" ]]; then
|
||||||
|
echo "No backup name given via NAME environment variable"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ "''${FOLDERS:-}" == "" ]]; then
|
||||||
|
echo "No folders given via FOLDERS environment variable"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
name=$(awk -F'::' '{print $1}' <<< $NAME)
|
||||||
|
backupname=''${NAME#$name::}
|
||||||
|
|
||||||
|
if command -v localbackup-mount-$name; then
|
||||||
|
localbackup-mount-$name
|
||||||
|
fi
|
||||||
|
if command -v localbackup-unmount-$name; then
|
||||||
|
trap "localbackup-unmount-$name" EXIT
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -d $backupname ]]; then
|
||||||
|
echo "No backup found $backupname"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
IFS=':' read -ra FOLDER <<< "''$FOLDERS"
|
||||||
|
for folder in "''${FOLDER[@]}"; do
|
||||||
|
mkdir -p "$folder"
|
||||||
|
rsync -a "$backupname/${config.networking.hostName}$folder/" "$folder"
|
||||||
|
done
|
||||||
|
'')
|
||||||
|
]
|
||||||
|
++ (lib.mapAttrsToList (
|
||||||
|
name: target:
|
||||||
|
pkgs.writeShellScriptBin ("localbackup-mount-" + name) ''
|
||||||
|
set -efu -o pipefail
|
||||||
|
${lib.optionalString (target.preMountHook != null) target.preMountHook}
|
||||||
|
${lib.optionalString (target.mountpoint != null) ''
|
||||||
|
if ! ${pkgs.util-linux}/bin/mountpoint -q ${lib.escapeShellArg target.mountpoint}; then
|
||||||
|
${pkgs.util-linux}/bin/mount -o X-mount.mkdir ${lib.escapeShellArg target.mountpoint}
|
||||||
|
fi
|
||||||
|
''}
|
||||||
|
${lib.optionalString (target.postMountHook != null) target.postMountHook}
|
||||||
|
''
|
||||||
|
) settings.targets)
|
||||||
|
++ lib.mapAttrsToList (
|
||||||
|
name: target:
|
||||||
|
pkgs.writeShellScriptBin ("localbackup-unmount-" + name) ''
|
||||||
|
set -efu -o pipefail
|
||||||
|
${lib.optionalString (target.preUnmountHook != null) target.preUnmountHook}
|
||||||
|
${lib.optionalString (
|
||||||
|
target.mountpoint != null
|
||||||
|
) "${pkgs.util-linux}/bin/umount ${lib.escapeShellArg target.mountpoint}"}
|
||||||
|
${lib.optionalString (target.postUnmountHook != null) target.postUnmountHook}
|
||||||
|
''
|
||||||
|
) settings.targets;
|
||||||
|
|
||||||
|
clan.core.backups.providers.localbackup = {
|
||||||
|
# TODO list needs to run locally or on the remote machine
|
||||||
|
list = "localbackup-list";
|
||||||
|
create = "localbackup-create";
|
||||||
|
restore = "localbackup-restore";
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
16
clanServices/localbackup/flake-module.nix
Normal file
16
clanServices/localbackup/flake-module.nix
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{ lib, ... }:
|
||||||
|
let
|
||||||
|
module = lib.modules.importApply ./default.nix { };
|
||||||
|
in
|
||||||
|
{
|
||||||
|
clan.modules.localbackup = module;
|
||||||
|
perSystem =
|
||||||
|
{ ... }:
|
||||||
|
{
|
||||||
|
clan.nixosTests.localbackup = {
|
||||||
|
imports = [ ./tests/vm/default.nix ];
|
||||||
|
|
||||||
|
clan.modules."@clan/localbackup" = module;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
62
clanServices/localbackup/tests/vm/default.nix
Normal file
62
clanServices/localbackup/tests/vm/default.nix
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
{ ... }:
|
||||||
|
{
|
||||||
|
name = "service-localbackup";
|
||||||
|
|
||||||
|
clan = {
|
||||||
|
directory = ./.;
|
||||||
|
test.useContainers = true;
|
||||||
|
inventory = {
|
||||||
|
|
||||||
|
machines.machine = { };
|
||||||
|
|
||||||
|
instances = {
|
||||||
|
localbackup = {
|
||||||
|
module.name = "@clan/localbackup";
|
||||||
|
module.input = "self";
|
||||||
|
roles.default.machines."machine".settings = {
|
||||||
|
|
||||||
|
targets.hdd = {
|
||||||
|
directory = "/mnt/external-disk";
|
||||||
|
preMountHook = ''
|
||||||
|
touch /run/mount-external-disk
|
||||||
|
'';
|
||||||
|
postUnmountHook = ''
|
||||||
|
touch /run/unmount-external-disk
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
nodes.machine = {
|
||||||
|
clan.core.state.test-backups.folders = [ "/var/test-backups" ];
|
||||||
|
};
|
||||||
|
|
||||||
|
testScript = ''
|
||||||
|
import json
|
||||||
|
start_all()
|
||||||
|
|
||||||
|
machine.systemctl("start network-online.target")
|
||||||
|
machine.wait_for_unit("network-online.target")
|
||||||
|
|
||||||
|
# dummy data
|
||||||
|
machine.succeed("mkdir -p /var/test-backups")
|
||||||
|
machine.succeed("echo testing > /var/test-backups/somefile")
|
||||||
|
|
||||||
|
# create
|
||||||
|
machine.succeed("localbackup-create >&2")
|
||||||
|
machine.wait_until_succeeds("! systemctl is-active localbackup-job-serverone >&2")
|
||||||
|
|
||||||
|
# list
|
||||||
|
snapshot_list = machine.succeed("localbackup-list").strip()
|
||||||
|
assert json.loads(snapshot_list)[0]["name"].strip() == "hdd::/mnt/external-disk/snapshot.0"
|
||||||
|
|
||||||
|
# borgbackup restore
|
||||||
|
machine.succeed("rm -f /var/test-backups/somefile")
|
||||||
|
|
||||||
|
machine.succeed("NAME=/mnt/external-disk/snapshot.0 FOLDERS=/var/test-backups /run/current-system/sw/bin/localbackup-restore >&2")
|
||||||
|
assert machine.succeed("cat /var/test-backups/somefile").strip() == "testing", "restore failed"
|
||||||
|
'';
|
||||||
|
}
|
||||||
@@ -144,6 +144,7 @@ nav:
|
|||||||
- reference/clanModules/heisenbridge.md
|
- reference/clanModules/heisenbridge.md
|
||||||
- reference/clanModules/importer.md
|
- reference/clanModules/importer.md
|
||||||
- reference/clanModules/iwd.md
|
- reference/clanModules/iwd.md
|
||||||
|
- reference/clanServices/localbackup.md
|
||||||
- reference/clanModules/localbackup.md
|
- reference/clanModules/localbackup.md
|
||||||
- reference/clanModules/localsend.md
|
- reference/clanModules/localsend.md
|
||||||
- reference/clanModules/matrix-synapse.md
|
- reference/clanModules/matrix-synapse.md
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{ self, ... }:
|
{ ... }:
|
||||||
{
|
{
|
||||||
perSystem =
|
perSystem =
|
||||||
{ ... }:
|
{ ... }:
|
||||||
@@ -22,28 +22,11 @@
|
|||||||
roles.default.extraModules = [
|
roles.default.extraModules = [
|
||||||
{
|
{
|
||||||
|
|
||||||
imports = [
|
|
||||||
# self.nixosModules.clanCore
|
|
||||||
self.clanModules.localbackup
|
|
||||||
];
|
|
||||||
|
|
||||||
clan.core.postgresql.enable = true;
|
clan.core.postgresql.enable = true;
|
||||||
clan.core.postgresql.users.test = { };
|
clan.core.postgresql.users.test = { };
|
||||||
clan.core.postgresql.databases.test.create.options.OWNER = "test";
|
clan.core.postgresql.databases.test.create.options.OWNER = "test";
|
||||||
clan.core.postgresql.databases.test.restore.stopOnRestore = [ "sample-service" ];
|
|
||||||
clan.localbackup.targets.hdd.directory = "/mnt/external-disk";
|
|
||||||
clan.core.settings.directory = ./.;
|
clan.core.settings.directory = ./.;
|
||||||
|
|
||||||
systemd.services.sample-service = {
|
|
||||||
wantedBy = [ "multi-user.target" ];
|
|
||||||
script = ''
|
|
||||||
while true; do
|
|
||||||
echo "Hello, world!"
|
|
||||||
sleep 5
|
|
||||||
done
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
@@ -53,53 +36,14 @@
|
|||||||
# TODO: Broken. Use instead of importer after fixing.
|
# TODO: Broken. Use instead of importer after fixing.
|
||||||
# nodes.machine = { };
|
# nodes.machine = { };
|
||||||
|
|
||||||
testScript =
|
testScript = ''
|
||||||
|
|
||||||
{ nodes, ... }:
|
|
||||||
|
|
||||||
''
|
|
||||||
start_all()
|
start_all()
|
||||||
machine.wait_for_unit("postgresql")
|
machine.wait_for_unit("postgresql")
|
||||||
machine.wait_for_unit("sample-service")
|
|
||||||
# Create a test table
|
# Create a test table
|
||||||
machine.succeed("runuser -u postgres -- /run/current-system/sw/bin/psql -c 'CREATE TABLE test (id serial PRIMARY KEY);' test")
|
machine.succeed("runuser -u postgres -- /run/current-system/sw/bin/psql -c 'CREATE TABLE test (id serial PRIMARY KEY);' test")
|
||||||
|
|
||||||
machine.succeed("/run/current-system/sw/bin/localbackup-create >&2")
|
|
||||||
timestamp_before = int(machine.succeed("systemctl show --property=ExecMainStartTimestampMonotonic sample-service | cut -d= -f2").strip())
|
|
||||||
|
|
||||||
# import time
|
|
||||||
# time.sleep(5400000)
|
|
||||||
|
|
||||||
machine.succeed("test -e /mnt/external-disk/snapshot.0/machine/var/backup/postgres/test/pg-dump || { echo 'pg-dump not found'; exit 1; }")
|
|
||||||
machine.succeed("runuser -u postgres -- /run/current-system/sw/bin/psql -d test -c 'INSERT INTO test DEFAULT VALUES;'")
|
machine.succeed("runuser -u postgres -- /run/current-system/sw/bin/psql -d test -c 'INSERT INTO test DEFAULT VALUES;'")
|
||||||
machine.succeed("runuser -u postgres -- /run/current-system/sw/bin/psql -d test -c 'DROP TABLE test;'")
|
|
||||||
machine.succeed("test -e /var/backup/postgres/test/pg-dump || { echo 'pg-dump not found'; exit 1; }")
|
|
||||||
|
|
||||||
machine.succeed("rm -rf /var/backup/postgres")
|
|
||||||
|
|
||||||
machine.succeed("NAME=/mnt/external-disk/snapshot.0 FOLDERS=/var/backup/postgres/test /run/current-system/sw/bin/localbackup-restore >&2")
|
|
||||||
machine.succeed("test -e /var/backup/postgres/test/pg-dump || { echo 'pg-dump not found'; exit 1; }")
|
|
||||||
|
|
||||||
machine.succeed("""
|
|
||||||
set -x
|
|
||||||
${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")
|
|
||||||
|
|
||||||
timestamp_after = int(machine.succeed("systemctl show --property=ExecMainStartTimestampMonotonic sample-service | cut -d= -f2").strip())
|
|
||||||
assert timestamp_before < timestamp_after, f"{timestamp_before} >= {timestamp_after}: expected sample-service to be restarted after restore"
|
|
||||||
|
|
||||||
# Check that the table is still there
|
|
||||||
machine.succeed("runuser -u postgres -- /run/current-system/sw/bin/psql -d test -c 'SELECT * FROM test;'")
|
machine.succeed("runuser -u postgres -- /run/current-system/sw/bin/psql -d test -c 'SELECT * FROM test;'")
|
||||||
output = machine.succeed("runuser -u postgres -- /run/current-system/sw/bin/psql --csv -c \"SELECT datdba::regrole FROM pg_database WHERE datname = 'test'\"")
|
|
||||||
owner = output.split("\n")[1]
|
|
||||||
assert owner == "test", f"Expected database owner to be 'test', got '{owner}'"
|
|
||||||
|
|
||||||
# check if restore works if the database does not exist
|
|
||||||
machine.succeed("runuser -u postgres -- dropdb test")
|
|
||||||
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")
|
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user