Include patched btrbk module
This commit is contained in:
parent
ff683258c7
commit
b5b8ff8598
3 changed files with 337 additions and 24 deletions
86
flake.nix
86
flake.nix
|
@ -22,6 +22,7 @@
|
|||
let
|
||||
sharedModule = import ./modules/shared;
|
||||
btrbkModule = import ./modules/btrbk;
|
||||
btrbkPatchedModule = import ./modules/btrbk-patched;
|
||||
dockerModule = import ./modules/docker;
|
||||
subvolumesModule = import ./modules/subvolumes;
|
||||
k3sModule = import ./modules/k3s;
|
||||
|
@ -36,6 +37,7 @@
|
|||
modules = [
|
||||
sops-nix.nixosModules.sops
|
||||
sharedModule
|
||||
btrbkPatchedModule
|
||||
{ networking.hostName = hostname; }
|
||||
] ++ extraModules;
|
||||
};
|
||||
|
@ -557,7 +559,7 @@
|
|||
makeQuartz64ABackupConfig = makeBoardBackupConfig sd-images.packages.${system}.Quartz64A.modules;
|
||||
|
||||
makePostgresConfig = system:
|
||||
{ hostname, selfIp, macAddress, keyFile, primaryIp ? null }:
|
||||
{ hostname, selfIp, macAddress, keyFile, primaryIp ? null, unlockMounts ? true, mountVolumes ? true, luksDevice ? "/dev/sda1" }:
|
||||
let
|
||||
device = "/dev/mapper/cryptdrive1";
|
||||
mountDir = "/btrfs/ssd";
|
||||
|
@ -569,35 +571,72 @@
|
|||
extraModules = sd-images.packages.${system}.Rock64.modules ++ [
|
||||
dockerModule
|
||||
(networkModule { inherit macAddress selfIp; })
|
||||
(btrbkModule {
|
||||
instances = [{ inherit mountDir primaryIp subvolumes; }];
|
||||
})
|
||||
(if primaryIp == null then
|
||||
(if unlockMounts && mountVolumes then
|
||||
(btrbkModule {
|
||||
instances = [{ inherit mountDir primaryIp subvolumes; }];
|
||||
}) else { })
|
||||
(if primaryIp == null && unlockMounts && mountVolumes then
|
||||
(subvolumesModule { inherit device subvolumes; })
|
||||
else
|
||||
{ })
|
||||
({ config, lib, ... }:
|
||||
let keyFilePath = config.sops.secrets."${keyFile}".path;
|
||||
in {
|
||||
({ config, lib, pkgs, ... }:
|
||||
let
|
||||
keyFilePath = config.sops.secrets.${keyFile}.path;
|
||||
prepareNvme = ''
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
DEVICE=$1
|
||||
|
||||
if [ "$DEVICE" == "" ]; then
|
||||
echo "Must provide device"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "YES" | cryptsetup luksFormat "$DEVICE" -d ${keyFilePath} --label=LUKS_SSD
|
||||
cryptsetup luksOpen "$DEVICE" cryptdrive1 -d ${keyFilePath}
|
||||
|
||||
mkfs.btrfs /dev/mapper/cryptdrive1
|
||||
|
||||
mkdir -p /btrfs/nvme
|
||||
mount /dev/mapper/cryptdrive1 ${mountDir}
|
||||
|
||||
btrfs subvolume create ${mountDir}/@postgres
|
||||
btrfs subvolume create ${mountDir}/@postgres-cfg
|
||||
btrfs subvolume create ${mountDir}/@snapshots
|
||||
|
||||
umount ${mountDir}
|
||||
|
||||
cryptsetup luksClose cryptdrive1
|
||||
'';
|
||||
in
|
||||
{
|
||||
sops.secrets.${keyFile} = {
|
||||
format = "binary";
|
||||
sopsFile = ./secrets/${keyFile}.bin;
|
||||
};
|
||||
|
||||
environment.systemPackages = with pkgs;
|
||||
[ (writeShellScriptBin "prepare-nvme" prepareNvme) ];
|
||||
|
||||
environment.etc.crypttab = {
|
||||
enable = true;
|
||||
enable = unlockMounts;
|
||||
text = ''
|
||||
cryptdrive1 /dev/sda1 ${keyFilePath} luks
|
||||
cryptdrive1 ${luksDevice} ${keyFilePath} luks
|
||||
'';
|
||||
};
|
||||
|
||||
fileSystems = {
|
||||
"${mountDir}" = {
|
||||
inherit device;
|
||||
fsType = "btrfs";
|
||||
options = [ "defaults" "compress=zstd" "rw" ];
|
||||
};
|
||||
};
|
||||
fileSystems =
|
||||
if unlockMounts && mountVolumes then
|
||||
{
|
||||
"${mountDir}" = {
|
||||
inherit device;
|
||||
fsType = "btrfs";
|
||||
options = [ "defaults" "compress=zstd" "rw" ];
|
||||
};
|
||||
}
|
||||
else { };
|
||||
})
|
||||
];
|
||||
};
|
||||
|
@ -611,7 +650,8 @@
|
|||
selfIp = "192.168.20.23";
|
||||
macAddress = "02:fe:30:d8:cf:64";
|
||||
keyFile = "redtailKeyFile";
|
||||
# primaryIp = "192.168.20.24";
|
||||
primaryIp = "192.168.20.24";
|
||||
luksDevice = "/dev/disk/by-label/LUKS_SSD";
|
||||
};
|
||||
|
||||
redtail2 = makePostgresConfig system {
|
||||
|
@ -619,7 +659,7 @@
|
|||
selfIp = "192.168.20.24";
|
||||
macAddress = "02:8a:70:2a:a8:5e";
|
||||
keyFile = "redtailKeyFile";
|
||||
primaryIp = "192.168.20.23";
|
||||
# primaryIp = "192.168.20.23";
|
||||
};
|
||||
|
||||
whitestorm1 = makePostgresConfig system {
|
||||
|
@ -929,8 +969,8 @@
|
|||
name = "whitestorm1";
|
||||
}
|
||||
{
|
||||
ip = "192.168.20.23";
|
||||
name = "redtail1";
|
||||
ip = "192.168.20.24";
|
||||
name = "redtail2";
|
||||
}
|
||||
]);
|
||||
};
|
||||
|
@ -1064,8 +1104,8 @@
|
|||
name = "whitestorm1";
|
||||
}
|
||||
{
|
||||
ip = "192.168.20.23";
|
||||
name = "redtail1";
|
||||
ip = "192.168.20.24";
|
||||
name = "redtail2";
|
||||
}
|
||||
]);
|
||||
};
|
||||
|
|
273
modules/btrbk-patched/default.nix
Normal file
273
modules/btrbk-patched/default.nix
Normal file
|
@ -0,0 +1,273 @@
|
|||
{ config, pkgs, lib, ... }:
|
||||
let
|
||||
inherit (lib)
|
||||
concatLists
|
||||
concatMap
|
||||
concatMapStringsSep
|
||||
concatStringsSep
|
||||
filterAttrs
|
||||
isAttrs
|
||||
literalExpression
|
||||
mapAttrs'
|
||||
mapAttrsToList
|
||||
mkIf
|
||||
mkOption
|
||||
optionalString
|
||||
sort
|
||||
types
|
||||
;
|
||||
|
||||
# The priority of an option or section.
|
||||
# The configurations format are order-sensitive. Pairs are added as children of
|
||||
# the last sections if possible, otherwise, they start a new section.
|
||||
# We sort them in topological order:
|
||||
# 1. Leaf pairs.
|
||||
# 2. Sections that may contain (1).
|
||||
# 3. Sections that may contain (1) or (2).
|
||||
# 4. Etc.
|
||||
prioOf = { name, value }:
|
||||
if !isAttrs value then 0 # Leaf options.
|
||||
else {
|
||||
target = 1; # Contains: options.
|
||||
subvolume = 2; # Contains: options, target.
|
||||
volume = 3; # Contains: options, target, subvolume.
|
||||
}.${name} or (throw "Unknow section '${name}'");
|
||||
|
||||
genConfig' = set: concatStringsSep "\n" (genConfig set);
|
||||
genConfig = set:
|
||||
let
|
||||
pairs = mapAttrsToList (name: value: { inherit name value; }) set;
|
||||
sortedPairs = sort (a: b: prioOf a < prioOf b) pairs;
|
||||
in
|
||||
concatMap genPair sortedPairs;
|
||||
genSection = sec: secName: value:
|
||||
[ "${sec} ${secName}" ] ++ map (x: " " + x) (genConfig value);
|
||||
genPair = { name, value }:
|
||||
if !isAttrs value
|
||||
then [ "${name} ${value}" ]
|
||||
else concatLists (mapAttrsToList (genSection name) value);
|
||||
|
||||
sudo_doas =
|
||||
if config.security.sudo.enable then "sudo"
|
||||
else if config.security.doas.enable then "doas"
|
||||
else throw "The btrbk nixos module needs either sudo or doas enabled in the configuration";
|
||||
|
||||
addDefaults = settings: { backend = "btrfs-progs-${sudo_doas}"; } // settings;
|
||||
|
||||
mkConfigFile = name: settings: pkgs.writeTextFile {
|
||||
name = "btrbk-${name}.conf";
|
||||
text = genConfig' (addDefaults settings);
|
||||
checkPhase = ''
|
||||
set +e
|
||||
${pkgs.btrbk}/bin/btrbk -c $out dryrun
|
||||
# According to btrbk(1), exit status 2 means parse error
|
||||
# for CLI options or the config file.
|
||||
if [[ $? == 2 ]]; then
|
||||
echo "Btrbk configuration is invalid:"
|
||||
cat $out
|
||||
exit 1
|
||||
fi
|
||||
set -e
|
||||
'';
|
||||
};
|
||||
|
||||
cfg = config.services.btrbk-patched;
|
||||
sshEnabled = cfg.sshAccess != [ ];
|
||||
serviceEnabled = cfg.instances != { };
|
||||
in
|
||||
{
|
||||
meta.maintainers = with lib.maintainers; [ oxalica ];
|
||||
|
||||
options = {
|
||||
services.btrbk-patched = {
|
||||
extraPackages = mkOption {
|
||||
description = lib.mdDoc "Extra packages for btrbk, like compression utilities for `stream_compress`";
|
||||
type = types.listOf types.package;
|
||||
default = [ ];
|
||||
example = literalExpression "[ pkgs.xz ]";
|
||||
};
|
||||
niceness = mkOption {
|
||||
description = lib.mdDoc "Niceness for local instances of btrbk. Also applies to remote ones connecting via ssh when positive.";
|
||||
type = types.ints.between (-20) 19;
|
||||
default = 10;
|
||||
};
|
||||
ioSchedulingClass = mkOption {
|
||||
description = lib.mdDoc "IO scheduling class for btrbk (see ionice(1) for a quick description). Applies to local instances, and remote ones connecting by ssh if set to idle.";
|
||||
type = types.enum [ "idle" "best-effort" "realtime" ];
|
||||
default = "best-effort";
|
||||
};
|
||||
instances = mkOption {
|
||||
description = lib.mdDoc "Set of btrbk instances. The instance named `btrbk` is the default one.";
|
||||
type = with types;
|
||||
attrsOf (
|
||||
submodule {
|
||||
options = {
|
||||
onCalendar = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = "daily";
|
||||
description = lib.mdDoc ''
|
||||
How often this btrbk instance is started. See systemd.time(7) for more information about the format.
|
||||
Setting it to null disables the timer, thus this instance can only be started manually.
|
||||
'';
|
||||
};
|
||||
settings = mkOption {
|
||||
type = let t = types.attrsOf (types.either types.str (t // { description = "instances of this type recursively"; })); in t;
|
||||
default = { };
|
||||
example = {
|
||||
snapshot_preserve_min = "2d";
|
||||
snapshot_preserve = "14d";
|
||||
volume = {
|
||||
"/mnt/btr_pool" = {
|
||||
target = "/mnt/btr_backup/mylaptop";
|
||||
subvolume = {
|
||||
"rootfs" = { };
|
||||
"home" = { snapshot_create = "always"; };
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
description = lib.mdDoc "configuration options for btrbk. Nested attrsets translate to subsections.";
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
default = { };
|
||||
};
|
||||
sshAccess = mkOption {
|
||||
description = lib.mdDoc "SSH keys that should be able to make or push snapshots on this system remotely with btrbk";
|
||||
type = with types; listOf (
|
||||
submodule {
|
||||
options = {
|
||||
key = mkOption {
|
||||
type = str;
|
||||
description = lib.mdDoc "SSH public key allowed to login as user `btrbk` to run remote backups.";
|
||||
};
|
||||
roles = mkOption {
|
||||
type = listOf (enum [ "info" "source" "target" "delete" "snapshot" "send" "receive" ]);
|
||||
example = [ "source" "info" "send" ];
|
||||
description = lib.mdDoc "What actions can be performed with this SSH key. See ssh_filter_btrbk(1) for details";
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
default = [ ];
|
||||
};
|
||||
};
|
||||
|
||||
};
|
||||
config = mkIf (sshEnabled || serviceEnabled) {
|
||||
environment.systemPackages = [ pkgs.btrbk ] ++ cfg.extraPackages;
|
||||
security.sudo = mkIf (sudo_doas == "sudo") {
|
||||
extraRules = [
|
||||
{
|
||||
users = [ "btrbk" ];
|
||||
commands = [
|
||||
{ command = "${pkgs.btrfs-progs}/bin/btrfs"; options = [ "NOPASSWD" ]; }
|
||||
{ command = "${pkgs.coreutils}/bin/mkdir"; options = [ "NOPASSWD" ]; }
|
||||
{ command = "${pkgs.coreutils}/bin/readlink"; options = [ "NOPASSWD" ]; }
|
||||
# for ssh, they are not the same than the one hard coded in ${pkgs.btrbk}
|
||||
{ command = "/run/current-system/sw/bin/btrfs"; options = [ "NOPASSWD" ]; }
|
||||
{ command = "/run/current-system/sw/bin/mkdir"; options = [ "NOPASSWD" ]; }
|
||||
{ command = "/run/current-system/sw/bin/readlink"; options = [ "NOPASSWD" ]; }
|
||||
];
|
||||
}
|
||||
];
|
||||
};
|
||||
security.doas = mkIf (sudo_doas == "doas") {
|
||||
extraRules = let
|
||||
doasCmdNoPass = cmd: { users = [ "btrbk" ]; cmd = cmd; noPass = true; };
|
||||
in
|
||||
[
|
||||
(doasCmdNoPass "${pkgs.btrfs-progs}/bin/btrfs")
|
||||
(doasCmdNoPass "${pkgs.coreutils}/bin/mkdir")
|
||||
(doasCmdNoPass "${pkgs.coreutils}/bin/readlink")
|
||||
# for ssh, they are not the same than the one hard coded in ${pkgs.btrbk}
|
||||
(doasCmdNoPass "/run/current-system/sw/bin/btrfs")
|
||||
(doasCmdNoPass "/run/current-system/sw/bin/mkdir")
|
||||
(doasCmdNoPass "/run/current-system/sw/bin/readlink")
|
||||
|
||||
# doas matches command, not binary
|
||||
(doasCmdNoPass "btrfs")
|
||||
(doasCmdNoPass "mkdir")
|
||||
(doasCmdNoPass "readlink")
|
||||
];
|
||||
};
|
||||
users.users.btrbk = {
|
||||
isSystemUser = true;
|
||||
# ssh needs a home directory
|
||||
home = "/var/lib/btrbk";
|
||||
createHome = true;
|
||||
shell = "${pkgs.bash}/bin/bash";
|
||||
group = "btrbk";
|
||||
openssh.authorizedKeys.keys = map
|
||||
(
|
||||
v:
|
||||
let
|
||||
options = concatMapStringsSep " " (x: "--" + x) v.roles;
|
||||
ioniceClass = {
|
||||
"idle" = 3;
|
||||
"best-effort" = 2;
|
||||
"realtime" = 1;
|
||||
}.${cfg.ioSchedulingClass};
|
||||
sudo_doas_flag = "--${sudo_doas}";
|
||||
in
|
||||
''command="${pkgs.util-linux}/bin/ionice -t -c ${toString ioniceClass} ${optionalString (cfg.niceness >= 1) "${pkgs.coreutils}/bin/nice -n ${toString cfg.niceness}"} ${pkgs.btrbk}/share/btrbk/scripts/ssh_filter_btrbk.sh ${sudo_doas_flag} ${options}" ${v.key}''
|
||||
)
|
||||
cfg.sshAccess;
|
||||
};
|
||||
users.groups.btrbk = { };
|
||||
systemd.tmpfiles.rules = [
|
||||
"d /var/lib/btrbk 0750 btrbk btrbk"
|
||||
"d /var/lib/btrbk/.ssh 0700 btrbk btrbk"
|
||||
"f /var/lib/btrbk/.ssh/config 0700 btrbk btrbk - StrictHostKeyChecking=accept-new"
|
||||
];
|
||||
environment.etc = mapAttrs'
|
||||
(
|
||||
name: instance: {
|
||||
name = "btrbk/${name}.conf";
|
||||
value.source = mkConfigFile name instance.settings;
|
||||
}
|
||||
)
|
||||
cfg.instances;
|
||||
systemd.services = mapAttrs'
|
||||
(
|
||||
name: _: {
|
||||
name = "btrbk-${name}";
|
||||
value = {
|
||||
description = "Takes BTRFS snapshots and maintains retention policies.";
|
||||
unitConfig.Documentation = "man:btrbk(1)";
|
||||
path = [ "/run/wrappers" ] ++ cfg.extraPackages;
|
||||
serviceConfig = {
|
||||
User = "btrbk";
|
||||
Group = "btrbk";
|
||||
Type = "oneshot";
|
||||
ExecStart = "${pkgs.btrbk}/bin/btrbk -c /etc/btrbk/${name}.conf run";
|
||||
Nice = cfg.niceness;
|
||||
IOSchedulingClass = cfg.ioSchedulingClass;
|
||||
StateDirectory = "btrbk";
|
||||
};
|
||||
};
|
||||
}
|
||||
)
|
||||
cfg.instances;
|
||||
|
||||
systemd.timers = mapAttrs'
|
||||
(
|
||||
name: instance: {
|
||||
name = "btrbk-${name}";
|
||||
value = {
|
||||
description = "Timer to take BTRFS snapshots and maintain retention policies.";
|
||||
wantedBy = [ "timers.target" ];
|
||||
timerConfig = {
|
||||
OnCalendar = instance.onCalendar;
|
||||
AccuracySec = "10min";
|
||||
Persistent = true;
|
||||
};
|
||||
};
|
||||
}
|
||||
)
|
||||
(filterAttrs (name: instance: instance.onCalendar != null)
|
||||
cfg.instances);
|
||||
};
|
||||
|
||||
}
|
|
@ -96,7 +96,7 @@ in
|
|||
else
|
||||
[ ]);
|
||||
|
||||
services.btrbk = {
|
||||
services.btrbk-patched = {
|
||||
sshAccess = [{
|
||||
key =
|
||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHTqU3EvTgY5/e9m6YyQWypQPK58t9iPmPnPYAvnODGB asonix@lionheart";
|
||||
|
|
Loading…
Reference in a new issue