From fefc7bd20db473db1104150f2631b365ffaeb159 Mon Sep 17 00:00:00 2001 From: Katherina Walshe-Grey Date: Tue, 4 Mar 2025 14:43:03 +0000 Subject: [PATCH] backup: init as a reusable module --- flake.nix | 15 ++++ flake/backup.nix | 126 ++++++++++++++++++++++++++++++++++ flake/default.nix | 1 + hosts/elucredassa/default.nix | 20 +----- hosts/orm/default.nix | 13 ---- 5 files changed, 144 insertions(+), 31 deletions(-) create mode 100644 flake/backup.nix diff --git a/flake.nix b/flake.nix index 307a812..5562b16 100644 --- a/flake.nix +++ b/flake.nix @@ -102,6 +102,21 @@ "qenya@shaw".imports = [ ./hosts/shaw/home.nix ]; }; + fountain.backup = { + keys = { + elucredassa = [ "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOFa3hjej6KGmS2aQ4s46Y7U8pN4yyR2FuMofpHRwXNk syncoid@elucredassa" ]; + }; + sync = { + "orm-state" = { + dataset = "state"; + sourceHost = "orm"; + targetHost = "elucredassa"; + source = "rpool_orm"; + target = "rpool_elucredassa/backup/orm"; + }; + }; + }; + flake.colmena = { meta = { nixpkgs = import nixpkgs-unstable { diff --git a/flake/backup.nix b/flake/backup.nix new file mode 100644 index 0000000..30bc5bc --- /dev/null +++ b/flake/backup.nix @@ -0,0 +1,126 @@ +{ config, lib, pkgs, ... }: +let + cfg = config.fountain.backup; + + syncOptions = { + dataset = lib.mkOption { + type = lib.types.str; + description = '' + The name of the dataset to be synced (not including its parent + datasets, if any). This will be the same on the source and target. + It must already exist on the source, defined with the + {option}`randomcat.services.zfs` module, and not exist on the target. + ''; + }; + sourceHost = lib.mkOption { + type = lib.types.str; + description = '' + The host from which the dataset should be synced. Must be an entry in + {option}`flake.colmena`. + ''; + }; + targetHost = lib.mkOption { + type = lib.types.str; + description = '' + The host to which the dataset should be synced. Must be an entry in + {option}`flake.colmena`. + ''; + }; + source = lib.mkOption { + type = lib.types.str; + description = '' + The path to the synced dataset in the ZFS namespace on the source host, + excluding the component that is the name of the dataset itself. + ''; + }; + target = lib.mkOption { + type = lib.types.str; + description = '' + The path to the synced dataset in the ZFS namespace on the target host, + excluding the component that is the name of the dataset itself. It must + already exist, defined with the {option}`randomcat.services.zfs` + module. + ''; + }; + }; +in +{ + options.fountain.backup = { + keys = lib.mkOption { + type = lib.types.attrsOf (lib.types.listOf lib.types.singleLineStr); + default = { }; + description = '' + Lists of verbatim OpenSSH public keys that may be used to identify the + syncoid user on each target host. The key to each list must be the + host's hostname, as listed in {option}`flake.colmena`. + ''; + example = { + host = [ "ssh-rsa AAAAB3NzaC1yc2etc/etc/etcjwrsh8e596z6J0l7 example@host" ]; + bar = [ "ssh-ed25519 AAAAC3NzaCetcetera/etceteraJZMfk3QPfQ foo@bar" ]; + }; + }; + + sync = lib.mkOption { + type = lib.types.attrsOf (lib.types.submodule { options = syncOptions; }); + default = { }; + description = '' + Details of ZFS datasets whose snapshots should be synced from machine + to machine using syncoid. Syncoid will run hourly at 15 past the hour + and copy all ZFS snapshots from the source dataset to the target + dataset (recursing into child datasets). + + See descriptions for the individual options for more details. The name + of each attribute in this set is arbitrary and used to generate systemd + unit names. + + This module does not actually cause snapshots to be taken; sanoid must + be configured separately to do this. + ''; + example = { + "orm-state" = { + dataset = "state"; + sourceHost = "orm"; + targetHost = "elucredassa"; + source = "rpool_orm"; + target = "rpool_elucredassa/backup/orm"; + }; + }; + }; + }; + + # TODO: add some assertions to verify the options + + config.flake.colmena = lib.mkMerge (lib.mapAttrsToList + (name: sync: { + ${sync.sourceHost} = { pkgs, ... }: { + randomcat.services.zfs.datasets."${sync.source}/${sync.dataset}".zfsPermissions.users.backup = [ "hold" "send" ]; + users.users.backup = { + group = "backup"; + isSystemUser = true; + useDefaultShell = true; + openssh.authorizedKeys.keys = cfg.keys.${sync.targetHost}; + packages = with pkgs; [ mbuffer lzop ]; # syncoid uses these if available but doesn't pull them in automatically + }; + users.groups.backup = { }; + }; + + ${sync.targetHost} = { + randomcat.services.zfs.datasets."${sync.target}".zfsPermissions.users.syncoid = [ "mount" "create" "receive" "recordsize" ]; + services.syncoid = { + enable = true; + interval = "*-*-* *:15:00"; + commonArgs = [ "--no-sync-snap" ]; + commands = { + ${name} = { + source = "backup@${config.flake.nixosConfigurations.${sync.sourceHost}.config.networking.fqdn}:${sync.source}/${sync.dataset}"; + target = "${sync.target}/${sync.dataset}"; + recursive = true; + recvOptions = "ux recordsize o compression=lz4"; + }; + }; + }; + }; + }) + cfg.sync + ); +} diff --git a/flake/default.nix b/flake/default.nix index 65b028d..b567fab 100644 --- a/flake/default.nix +++ b/flake/default.nix @@ -1,5 +1,6 @@ { imports = [ + ./backup.nix ./colmena.nix ]; } diff --git a/hosts/elucredassa/default.nix b/hosts/elucredassa/default.nix index 147af4d..23c5421 100644 --- a/hosts/elucredassa/default.nix +++ b/hosts/elucredassa/default.nix @@ -23,26 +23,10 @@ in console.keyMap = "uk"; services.xserver.xkb.layout = "gb"; - # TODO: modularise this + # These are populated by fountain.backup randomcat.services.zfs.datasets = { "rpool_elucredassa/backup" = { mountpoint = "none"; }; - "rpool_elucredassa/backup/orm" = { - mountpoint = "none"; - zfsPermissions.users.syncoid = [ "mount" "create" "receive" "recordsize" ]; - }; - }; - services.syncoid = { - enable = true; - interval = "*-*-* *:15:00"; - commonArgs = [ "--no-sync-snap" ]; - commands = { - "testing1" = { - source = "backup@10.127.1.2:rpool_orm/state"; - target = "rpool_elucredassa/backup/orm/state"; - recursive = true; - recvOptions = "ux recordsize o compression=lz4"; - }; - }; + "rpool_elucredassa/backup/orm" = { mountpoint = "none"; }; }; qenya.services.distributed-builds = { diff --git a/hosts/orm/default.nix b/hosts/orm/default.nix index c892c20..0db3632 100644 --- a/hosts/orm/default.nix +++ b/hosts/orm/default.nix @@ -31,19 +31,6 @@ useTemplate = [ "production" ]; recursive = "zfs"; }; - - # TODO: modularise this - randomcat.services.zfs.datasets."rpool_orm/state".zfsPermissions.users.backup = [ "hold" "send" ]; - users.users.backup = { - group = "backup"; - isSystemUser = true; - useDefaultShell = true; - openssh.authorizedKeys.keys = [ - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOFa3hjej6KGmS2aQ4s46Y7U8pN4yyR2FuMofpHRwXNk syncoid@elucredassa" - ]; - packages = with pkgs; [ mbuffer lzop ]; # syncoid uses these if available but doesn't pull them in automatically - }; - users.groups.backup = { }; qenya.services.actual = { enable = true;