1
0
forked from clan/clan-core

backups: support services for restore

This commit is contained in:
lassulus 2023-12-08 18:40:18 +01:00
parent 57bded996b
commit dec431e69f
5 changed files with 147 additions and 89 deletions

View File

@ -66,27 +66,24 @@ in
}; };
clanCore.backups.providers.borgbackup = { clanCore.backups.providers.borgbackup = {
# TODO list needs to run locally or on the remote machine
list = '' list = ''
ssh ${config.clan.networking.deploymentAddress} <<EOF
${lib.concatMapStringsSep "\n" (dest: '' ${lib.concatMapStringsSep "\n" (dest: ''
borg-job-${dest.name} list --json | jq -r '. + {"job-name": "${dest.name}"}' # we need yes here to skip the changed url verification
yes y | borg-job-${dest.name} list --json | jq -r '. + {"job-name": "${dest.name}"}'
'') (lib.attrValues cfg.destinations)} '') (lib.attrValues cfg.destinations)}
EOF
''; '';
start = '' create = ''
ssh ${config.clan.networking.deploymentAddress} -- '
${lib.concatMapStringsSep "\n" (dest: '' ${lib.concatMapStringsSep "\n" (dest: ''
systemctl start borgbackup-job-${dest.name} systemctl start borgbackup-job-${dest.name}
'') (lib.attrValues cfg.destinations)} '') (lib.attrValues cfg.destinations)}
'
''; '';
restore = '' restore = ''
ssh ${config.clan.networking.deploymentAddress} -- LOCATION="$LOCATION" ARCHIVE="$ARCHIVE_ID" JOB="$JOB" ' set -efu
set -efux
cd / cd /
borg-job-"$JOB" extract --list --dry-run "$LOCATION"::"$ARCHIVE" IFS=';' read -ra FOLDER <<< "$FOLDERS"
' yes y | borg-job-"$JOB" extract --list --dry-run "$LOCATION"::"$ARCHIVE_ID" "''${FOLDER[@]}"
''; '';
}; };
}; };

View File

@ -50,9 +50,14 @@
description = '' description = ''
script to restore a backup script to restore a backup
should take an optional service name as argument should take an optional service name as argument
gets ARCHIVE_ID, LOCATION, JOB and FOLDERS as environment variables
ARCHIVE_ID is the id of the backup
LOCATION is the remote identifier of the backup
JOB is the job name of the backup
FOLDERS is a colon separated list of folders to restore
''; '';
}; };
start = lib.mkOption { create = lib.mkOption {
type = lib.types.str; type = lib.types.str;
description = '' description = ''
script to start a backup script to start a backup

View File

@ -1,6 +1,5 @@
import argparse import argparse
import json import json
import subprocess
from ..errors import ClanError from ..errors import ClanError
from ..machines.machines import Machine from ..machines.machines import Machine
@ -12,8 +11,8 @@ def create_backup(machine: Machine, provider: str | None = None) -> None:
) )
if provider is None: if provider is None:
for provider in backup_scripts["providers"]: for provider in backup_scripts["providers"]:
proc = subprocess.run( proc = machine.host.run(
["bash", "-c", backup_scripts["providers"][provider]["start"]], ["bash", "-c", backup_scripts["providers"][provider]["create"]],
) )
if proc.returncode != 0: if proc.returncode != 0:
raise ClanError("failed to start backup") raise ClanError("failed to start backup")
@ -22,8 +21,8 @@ def create_backup(machine: Machine, provider: str | None = None) -> None:
else: else:
if provider not in backup_scripts["providers"]: if provider not in backup_scripts["providers"]:
raise ClanError(f"provider {provider} not found") raise ClanError(f"provider {provider} not found")
proc = subprocess.run( proc = machine.host.run(
["bash", "-c", backup_scripts["providers"][provider]["start"]], ["bash", "-c", backup_scripts["providers"][provider]["create"]],
) )
if proc.returncode != 0: if proc.returncode != 0:
raise ClanError("failed to start backup") raise ClanError("failed to start backup")

View File

@ -1,47 +1,68 @@
import argparse import argparse
import json import json
import subprocess import subprocess
from typing import Any from dataclasses import dataclass
from ..errors import ClanError from ..errors import ClanError
from ..machines.machines import Machine from ..machines.machines import Machine
def list_backups(machine: Machine, provider: str | None = None) -> list[dict[str, Any]]: @dataclass
backup_scripts = json.loads( class Backup:
archive_id: str
date: str
provider: str
remote_path: str
job_name: str
def list_provider(machine: Machine, provider: str) -> list[Backup]:
results = []
backup_metadata = json.loads(
machine.eval_nix(f"nixosConfigurations.{machine.name}.config.clanCore.backups")
)
proc = machine.host.run(
["bash", "-c", backup_metadata["providers"][provider]["list"]],
stdout=subprocess.PIPE,
check=False,
)
if proc.returncode != 0:
# TODO this should be a warning, only raise exception if no providers succeed
ClanError(f"failed to list backups for provider {provider}")
else:
parsed_json = json.loads(proc.stdout)
# TODO move borg specific code to borgbackup.nix
for archive in parsed_json["archives"]:
backup = Backup(
archive_id=archive["archive"],
date=archive["time"],
provider=provider,
remote_path=parsed_json["repository"]["location"],
job_name=parsed_json["job-name"],
)
results.append(backup)
return results
def list_backups(machine: Machine, provider: str | None = None) -> list[Backup]:
backup_metadata = json.loads(
machine.eval_nix(f"nixosConfigurations.{machine.name}.config.clanCore.backups") machine.eval_nix(f"nixosConfigurations.{machine.name}.config.clanCore.backups")
) )
results = [] results = []
if provider is None: if provider is None:
for provider in backup_scripts["providers"]: for _provider in backup_metadata["providers"]:
proc = subprocess.run( results += list_provider(machine, _provider)
["bash", "-c", backup_scripts["providers"][provider]["list"]],
stdout=subprocess.PIPE,
)
if proc.returncode != 0:
# TODO this should be a warning, only raise exception if no providers succeed
raise ClanError("failed to list backups")
else:
results.append(proc.stdout)
else:
if provider not in backup_scripts["providers"]:
raise ClanError(f"provider {provider} not found")
proc = subprocess.run(
["bash", "-c", backup_scripts["providers"][provider]["list"]],
stdout=subprocess.PIPE,
)
if proc.returncode != 0:
raise ClanError("failed to list backup")
else:
results.append(proc.stdout)
return list(map(json.loads, results)) else:
results += list_provider(machine, provider)
return results
def list_command(args: argparse.Namespace) -> None: def list_command(args: argparse.Namespace) -> None:
machine = Machine(name=args.machine, flake_dir=args.flake) machine = Machine(name=args.machine, flake_dir=args.flake)
backups_data = list_backups(machine=machine, provider=args.provider) backups = list_backups(machine=machine, provider=args.provider)
print(json.dumps(list(backups_data))) print(backups)
def register_list_parser(parser: argparse.ArgumentParser) -> None: def register_list_parser(parser: argparse.ArgumentParser) -> None:

View File

@ -2,64 +2,100 @@ import argparse
import json import json
import os import os
import subprocess import subprocess
from typing import Any
from ..errors import ClanError from ..errors import ClanError
from ..machines.machines import Machine from ..machines.machines import Machine
from .list import list_backups from .list import Backup, list_backups
def restore_backup( def restore_service(
backup_data: list[dict[str, Any]], machine: Machine, backup: Backup, provider: str, service: str
machine: Machine,
provider: str,
archive_id: str,
service: str | None = None,
) -> None: ) -> None:
backup_scripts = json.loads( backup_metadata = json.loads(
machine.eval_nix(f"nixosConfigurations.{machine.name}.config.clanCore.backups") machine.eval_nix(f"nixosConfigurations.{machine.name}.config.clanCore.backups")
) )
backup_folders = json.loads( backup_folders = json.loads(
machine.eval_nix(f"nixosConfigurations.{machine.name}.config.clanCore.state") machine.eval_nix(f"nixosConfigurations.{machine.name}.config.clanCore.state")
) )
if service is None: folders = backup_folders[service]["folders"]
for backup in backup_data:
for archive in backup["archives"]:
if archive["archive"] == archive_id:
env = os.environ.copy() env = os.environ.copy()
env["ARCHIVE_ID"] = archive_id env["ARCHIVE_ID"] = backup.archive_id
env["LOCATION"] = backup["repository"]["location"] env["LOCATION"] = backup.remote_path
env["JOB"] = backup["job-name"] env["JOB"] = backup.job_name
proc = subprocess.run( env["FOLDERS"] = ":".join(folders)
proc = machine.host.run(
[ [
"bash", "bash",
"-c", "-c",
backup_scripts["providers"][provider]["restore"], backup_folders[service]["preRestoreScript"],
], ],
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
env=env, extra_env=env,
) )
if proc.returncode != 0: if proc.returncode != 0:
# TODO this should be a warning, only raise exception if no providers succeed raise ClanError(
raise ClanError("failed to restore backup") f"failed to run preRestoreScript: {backup_folders[service]['preRestoreScript']}, error was: {proc.stdout}"
else:
print(
"would restore backup",
machine,
provider,
archive_id,
"of service:",
service,
) )
print(backup_folders)
proc = machine.host.run(
[
"bash",
"-c",
backup_metadata["providers"][provider]["restore"],
],
stdout=subprocess.PIPE,
extra_env=env,
)
if proc.returncode != 0:
raise ClanError(
f"failed to restore backup: {backup_metadata['providers'][provider]['restore']}"
)
proc = machine.host.run(
[
"bash",
"-c",
backup_folders[service]["postRestoreScript"],
],
stdout=subprocess.PIPE,
extra_env=env,
)
if proc.returncode != 0:
raise ClanError(
f"failed to run postRestoreScript: {backup_folders[service]['postRestoreScript']}, error was: {proc.stdout}"
)
def restore_backup(
machine: Machine,
backups: list[Backup],
provider: str,
archive_id: str,
service: str | None = None,
) -> None:
if service is None:
for backup in backups:
if backup.archive_id == archive_id:
backup_folders = json.loads(
machine.eval_nix(
f"nixosConfigurations.{machine.name}.config.clanCore.state"
)
)
for _service in backup_folders:
restore_service(machine, backup, provider, _service)
else:
for backup in backups:
if backup.archive_id == archive_id:
restore_service(machine, backup, provider, service)
def restore_command(args: argparse.Namespace) -> None: def restore_command(args: argparse.Namespace) -> None:
machine = Machine(name=args.machine, flake_dir=args.flake) machine = Machine(name=args.machine, flake_dir=args.flake)
backup_data = list_backups(machine=machine, provider=args.provider) backups = list_backups(machine=machine, provider=args.provider)
restore_backup( restore_backup(
backup_data=backup_data,
machine=machine, machine=machine,
backups=backups,
provider=args.provider, provider=args.provider,
archive_id=args.archive_id, archive_id=args.archive_id,
service=args.service, service=args.service,