implement backup cli for borgbackup

This commit is contained in:
lassulus
2023-12-04 17:05:37 +01:00
parent c9e640a74d
commit 83dd430d03
10 changed files with 125 additions and 53 deletions

View File

@@ -16,7 +16,7 @@
{ {
clanCore.machineName = "machine"; clanCore.machineName = "machine";
clanCore.clanDir = ./.; clanCore.clanDir = ./.;
clanCore.state."/etc/state" = { }; clanCore.state.testState.folders = [ "/etc/state" ];
environment.etc.state.text = "hello world"; environment.etc.state.text = "hello world";
clan.borgbackup = { clan.borgbackup = {
enable = true; enable = true;

View File

@@ -33,7 +33,7 @@ in
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
services.borgbackup.jobs = lib.mapAttrs services.borgbackup.jobs = lib.mapAttrs
(_: dest: { (_: dest: {
paths = map (state: state.folder) (lib.attrValues config.clanCore.state); paths = lib.flatten (map (state: state.folders) (lib.attrValues config.clanCore.state));
exclude = [ exclude = [
"*.pyc" "*.pyc"
]; ];
@@ -58,16 +58,23 @@ in
clanCore.backups.providers.borgbackup = { clanCore.backups.providers.borgbackup = {
list = '' list = ''
${lib.concatMapStringsSep "\n" (dest: '' ${lib.concatMapStringsSep "\n" (dest: ''
echo listing backups for ${dest} (
borg-job-${dest} list export BORG_REPO=${lib.escapeShellArg dest.repo}
'') cfg.destinations} export BORG_RSH=${lib.escapeShellArg dest.rsh}
${lib.getExe config.services.borgbackup.package} list
)
'') (lib.attrValues cfg.destinations)}
''; '';
start = '' start = ''
${lib.concatMapStringsSep "\n" (dest: '' ssh ${config.clan.networking.deploymentAddress} -- '
systemctl start borgbackup-job-${dest} ${lib.concatMapStringsSep "\n" (dest: ''
'') cfg.destinations} systemctl start borgbackup-job-${dest.name}
'') (lib.attrValues cfg.destinations)}
'
''; '';
restore = ''
'';
}; };
}; };
} }

View File

@@ -3,15 +3,22 @@
options.clanCore.state = lib.mkOption { options.clanCore.state = lib.mkOption {
default = { }; default = { };
type = lib.types.attrsOf type = lib.types.attrsOf
(lib.types.submodule ({ name, ... }: { (lib.types.submodule ({ ... }: {
options = { options = {
folder = lib.mkOption { folders = lib.mkOption {
type = lib.types.str; type = lib.types.listOf lib.types.str;
default = name;
description = '' description = ''
Folder where state resides in Folder where state resides in
''; '';
}; };
restoreScript = lib.mkOption {
type = lib.types.str;
default = ":";
description = ''
script to restore the service after the state dir was restored from a backup
'';
};
}; };
})); }));
}; };
@@ -32,10 +39,11 @@
script to list backups script to list backups
''; '';
}; };
delete = lib.mkOption { restore = lib.mkOption {
type = lib.types.str; type = lib.types.str;
description = '' description = ''
script to delete a backup script to restore a backup
should take an optional service name as argument
''; '';
}; };
start = lib.mkOption { start = lib.mkOption {

View File

@@ -147,6 +147,8 @@ in
--network-id "$facts/zerotier-network-id" --network-id "$facts/zerotier-network-id"
''; '';
}; };
clanCore.state.zerotier.folders = [ "/var/lib/zerotier-one" ];
environment.systemPackages = [ config.clanCore.clanPkgs.zerotier-members ]; environment.systemPackages = [ config.clanCore.clanPkgs.zerotier-members ];
}) })
(lib.mkIf (config.clanCore.secretsUploadDirectory != null && !cfg.controller.enable && cfg.networkId != null) { (lib.mkIf (config.clanCore.secretsUploadDirectory != null && !cfg.controller.enable && cfg.networkId != null) {

View File

@@ -5,7 +5,7 @@ from pathlib import Path
from types import ModuleType from types import ModuleType
from typing import Any, Optional, Sequence from typing import Any, Optional, Sequence
from . import config, flakes, machines, secrets, vms, webui, backups from . import backups, config, flakes, machines, secrets, vms, webui
from .custom_logger import setup_logging from .custom_logger import setup_logging
from .dirs import get_clan_flake_toplevel from .dirs import get_clan_flake_toplevel
from .ssh import cli as ssh_cli from .ssh import cli as ssh_cli

View File

@@ -1,8 +1,8 @@
# !/usr/bin/env python3 # !/usr/bin/env python3
import argparse import argparse
from .list import register_list_parser
from .create import register_create_parser from .create import register_create_parser
from .list import register_list_parser
from .restore import register_restore_parser from .restore import register_restore_parser

View File

@@ -1,34 +1,45 @@
import argparse import argparse
import pprint import json
from pathlib import Path import subprocess
from typing import Optional from typing import Optional
from ..errors import ClanError from ..errors import ClanError
from ..machines.machines import Machine
def create_backup(flake_dir: Path, machine: Optional[str] = None, provider: Optional[str] = None) -> None: def create_backup(machine: Machine, provider: Optional[str] = None) -> None:
if machine is None: backup_scripts = json.loads(
# TODO get all machines here machine.eval_nix(f"nixosConfigurations.{machine.name}.config.clanCore.backups")
machines = [ "machine1", "machine2" ] )
else:
machines = [ machine ]
if provider is None: if provider is None:
# TODO get all providers here for provider in backup_scripts["providers"]:
providers = [ "provider1", "provider2" ] proc = subprocess.run(
["bash", "-c", backup_scripts["providers"][provider]["start"]],
)
if proc.returncode != 0:
raise ClanError("failed to start backup")
else:
print("successfully started backup")
else: else:
providers = [ provider ] if provider not in backup_scripts["providers"]:
raise ClanError(f"provider {provider} not found")
print("would create backups for machines: ", machines, " with providers: ", providers) proc = subprocess.run(
["bash", "-c", backup_scripts["providers"][provider]["start"]],
)
if proc.returncode != 0:
raise ClanError("failed to start backup")
else:
print("successfully started backup")
def create_command(args: argparse.Namespace) -> None: def create_command(args: argparse.Namespace) -> None:
if args.flake is None: machine = Machine(name=args.machine, flake_dir=args.flake)
raise ClanError("Could not find clan flake toplevel directory") create_backup(machine=machine, provider=args.provider)
create_backup(Path(args.flake), machine=args.machine, provider=args.provider)
def register_create_parser(parser: argparse.ArgumentParser) -> None: def register_create_parser(parser: argparse.ArgumentParser) -> None:
parser.add_argument("--machine", type=str, help="machine in the flake to create backups of") parser.add_argument(
"machine", type=str, help="machine in the flake to create backups of"
)
parser.add_argument("--provider", type=str, help="backup provider to use") parser.add_argument("--provider", type=str, help="backup provider to use")
parser.set_defaults(func=create_command) parser.set_defaults(func=create_command)

View File

@@ -6,25 +6,27 @@ from typing import Optional
from ..errors import ClanError from ..errors import ClanError
def list_backups(flake_dir: Path, machine: Optional[str] = None, provider: Optional[str] = None) -> dict[str, dict[str, list[str]]]: def list_backups(
flake_dir: Path, machine: str, provider: Optional[str] = None
) -> dict[str, dict[str, list[dict[str, str]]]]:
dummy_data = { dummy_data = {
"testhostname": { "testhostname": {
"borgbackup": [ "borgbackup": [
"2021-01-01T00:00:00Z", {"date": "2021-01-01T00:00:00Z", "id": "1"},
"2022-01-01T00:00:00Z", {"date": "2022-01-01T00:00:00Z", "id": "2"},
"2023-01-01T00:00:00Z", {"date": "2023-01-01T00:00:00Z", "id": "3"},
], ],
"restic" : [ "restic": [
"2021-01-01T00:00:00Z", {"date": "2021-01-01T00:00:00Z", "id": "1"},
"2022-01-01T00:00:00Z", {"date": "2022-01-01T00:00:00Z", "id": "2"},
"2023-01-01T00:00:00Z", {"date": "2023-01-01T00:00:00Z", "id": "3"},
], ],
}, },
"another host": { "another host": {
"borgbackup": [ "borgbackup": [
"2021-01-01T00:00:00Z", {"date": "2021-01-01T00:00:00Z", "id": "1"},
"2022-01-01T00:00:00Z", {"date": "2022-01-01T00:00:00Z", "id": "2"},
"2023-01-01T00:00:00Z", {"date": "2023-01-01T00:00:00Z", "id": "3"},
], ],
}, },
} }
@@ -41,16 +43,21 @@ def list_backups(flake_dir: Path, machine: Optional[str] = None, provider: Optio
else: else:
return {machine: dummy_data[machine]} return {machine: dummy_data[machine]}
def list_command(args: argparse.Namespace) -> None: def list_command(args: argparse.Namespace) -> None:
if args.flake is None: if args.flake is None:
raise ClanError("Could not find clan flake toplevel directory") raise ClanError("Could not find clan flake toplevel directory")
backups = list_backups(Path(args.flake), machine=args.machine, provider=args.provider) backups = list_backups(
Path(args.flake), machine=args.machine, provider=args.provider
)
if len(backups) > 0: if len(backups) > 0:
pp = pprint.PrettyPrinter(depth=4) pp = pprint.PrettyPrinter(depth=4)
pp.pprint(backups) pp.pprint(backups)
def register_list_parser(parser: argparse.ArgumentParser) -> None: def register_list_parser(parser: argparse.ArgumentParser) -> None:
parser.add_argument("--machine", type=str, help="machine in the flake to show backups of") parser.add_argument(
"machine", type=str, help="machine in the flake to show backups of"
)
parser.add_argument("--provider", type=str, help="backup provider to filter by") parser.add_argument("--provider", type=str, help="backup provider to filter by")
parser.set_defaults(func=list_command) parser.set_defaults(func=list_command)

View File

@@ -1,26 +1,41 @@
import argparse import argparse
import pprint
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
from ..errors import ClanError from ..errors import ClanError
def restore_backup(flake_dir: Path, machine: str, provider: str, backup_id: str, service: Optional[str] = None) -> None: def restore_backup(
flake_dir: Path,
machine: str,
provider: str,
backup_id: str,
service: Optional[str] = None,
) -> None:
if service is None: if service is None:
print("would restore backup", machine, provider, backup_id) print("would restore backup", machine, provider, backup_id)
else: else:
print("would restore backup", machine, provider, backup_id, "of service:", service) print(
"would restore backup", machine, provider, backup_id, "of service:", service
)
def restore_command(args: argparse.Namespace) -> None: def restore_command(args: argparse.Namespace) -> None:
if args.flake is None: if args.flake is None:
raise ClanError("Could not find clan flake toplevel directory") raise ClanError("Could not find clan flake toplevel directory")
restore_backup(Path(args.flake), machine=args.machine, provider=args.provider, backup_id=args.backup_id, service=args.service) restore_backup(
Path(args.flake),
machine=args.machine,
provider=args.provider,
backup_id=args.backup_id,
service=args.service,
)
def register_restore_parser(parser: argparse.ArgumentParser) -> None: def register_restore_parser(parser: argparse.ArgumentParser) -> None:
parser.add_argument("machine", type=str, help="machine in the flake to create backups of") parser.add_argument(
"machine", type=str, help="machine in the flake to create backups of"
)
parser.add_argument("provider", type=str, help="backup provider to use") parser.add_argument("provider", type=str, help="backup provider to use")
parser.add_argument("backup_id", type=str, help="id of the backup to restore") parser.add_argument("backup_id", type=str, help="id of the backup to restore")
parser.add_argument("--service", type=str, help="name of the service to restore") parser.add_argument("--service", type=str, help="name of the service to restore")

View File

@@ -0,0 +1,22 @@
import logging
from cli import Cli
from fixtures_flakes import FlakeForTest
log = logging.getLogger(__name__)
def test_backups(
test_flake: FlakeForTest,
) -> None:
cli = Cli()
cli.run(
[
"--flake",
str(test_flake.path),
"backups",
"list",
"testhostname",
]
)