borgmatic: add module

This commit is contained in:
Damien Cassou 2022-09-25 19:27:04 +02:00 committed by Robert Helgesson
parent da3b8049fd
commit 04f5399978
No known key found for this signature in database
GPG key ID: 36BDAA14C2797E89
12 changed files with 473 additions and 0 deletions

5
.github/CODEOWNERS vendored
View file

@ -343,6 +343,11 @@ Makefile @thiagokokada
/modules/services/betterlockscreen.nix @SebTM
/modules/programs/borgmatic.nix @DamienCassou
/modules/services/borgmatic.nix @DamienCassou
/tests/modules/programs/borgmatic @DamienCassou
/tests/modules/services/borgmatic @DamienCassou
/modules/services/caffeine.nix @uvNikita
/modules/services/cbatticon.nix @pmiddend

View file

@ -748,6 +748,20 @@ in
A new module is available: 'programs.discocss'.
'';
}
{
time = "2022-10-16T19:49:46+00:00";
condition = hostPlatform.isLinux;
message = ''
Two new modules are available:
- 'programs.borgmatic' and
- 'services.borgmatic'.
use the first to configure the borgmatic tool and the second if you
want to automatically run scheduled backups.
'';
}
];
};
}

View file

@ -57,6 +57,7 @@ let
./programs/bashmount.nix
./programs/bat.nix
./programs/beets.nix
./programs/borgmatic.nix
./programs/bottom.nix
./programs/broot.nix
./programs/browserpass.nix
@ -197,6 +198,7 @@ let
./services/barrier.nix
./services/betterlockscreen.nix
./services/blueman-applet.nix
./services/borgmatic.nix
./services/caffeine.nix
./services/cbatticon.nix
./services/clipmenu.nix

View file

@ -0,0 +1,196 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.programs.borgmatic;
mkNullableOption = args:
lib.mkOption (args // {
type = lib.types.nullOr args.type;
default = null;
});
mkRetentionOption = frequency:
mkNullableOption {
type = types.int;
description =
"Number of ${frequency} archives to keep. Use -1 for no limit.";
example = 3;
};
extraConfigOption = mkOption {
type = with types; attrsOf (oneOf [ str bool path int ]);
default = { };
description = "Extra settings.";
};
consistencyCheckModule = types.submodule {
options = {
name = mkOption {
type = types.enum [ "repository" "archives" "data" "extract" ];
description = "Name of consistency check to run.";
example = "repository";
};
frequency = mkNullableOption {
type = types.strMatching "([[:digit:]]+ .*)|always";
description = "Frequency of this type of check";
example = "2 weeks";
};
};
};
configModule = types.submodule {
options = {
location = {
sourceDirectories = mkOption {
type = types.listOf types.str;
description = "Directories to backup.";
example = literalExpression "[config.home.homeDirectory]";
};
repositories = mkOption {
type = types.listOf types.str;
description = "Paths to repositories.";
example =
literalExpression ''["ssh://myuser@myrepo.myserver.com/./repo"]'';
};
extraConfig = extraConfigOption;
};
storage = {
encryptionPasscommand = mkNullableOption {
type = types.str;
description = "Command writing the passphrase to standard output.";
example =
literalExpression ''"''${pkgs.password-store}/bin/pass borg-repo"'';
};
extraConfig = extraConfigOption;
};
retention = {
keepWithin = mkNullableOption {
type = types.strMatching "[[:digit:]]+[Hdwmy]";
description = "Keep all archives within this time interval.";
example = "2d";
};
keepSecondly = mkRetentionOption "secondly";
keepMinutely = mkRetentionOption "minutely";
keepHourly = mkRetentionOption "hourly";
keepDaily = mkRetentionOption "daily";
keepWeekly = mkRetentionOption "weekly";
keepMonthly = mkRetentionOption "monthly";
keepYearly = mkRetentionOption "yearly";
extraConfig = extraConfigOption;
};
consistency = {
checks = mkOption {
type = types.listOf consistencyCheckModule;
default = [ ];
description = "Consistency checks to run";
example = literalExpression ''
[
{
name = "repository";
frequency = "2 weeks";
}
{
name = "archives";
frequency = "4 weeks";
}
{
name = "data";
frequency = "6 weeks";
}
{
name = "extract";
frequency = "6 weeks";
}
];
'';
};
extraConfig = extraConfigOption;
};
};
};
removeNullValues = attrSet: filterAttrs (key: value: value != null) attrSet;
writeConfig = config:
generators.toYAML { } {
location = removeNullValues {
source_directories = config.location.sourceDirectories;
repositories = config.location.repositories;
} // config.location.extraConfig;
storage = removeNullValues {
encryption_passcommand = config.storage.encryptionPasscommand;
} // config.storage.extraConfig;
retention = removeNullValues {
keep_within = config.retention.keepWithin;
keep_secondly = config.retention.keepSecondly;
keep_minutely = config.retention.keepMinutely;
keep_hourly = config.retention.keepHourly;
keep_daily = config.retention.keepDaily;
keep_weekly = config.retention.keepWeekly;
keep_monthly = config.retention.keepMonthly;
keep_yearly = config.retention.keepYearly;
} // config.retention.extraConfig;
consistency = removeNullValues { checks = config.consistency.checks; }
// config.consistency.extraConfig;
};
in {
meta.maintainers = [ maintainers.DamienCassou ];
options = {
programs.borgmatic = {
enable = mkEnableOption "Borgmatic";
package = mkPackageOption pkgs "borgmatic" { };
backups = mkOption {
type = types.attrsOf configModule;
description = ''
Borgmatic allows for several named backup configurations,
each with its own source directories and repositories.
'';
example = literalExpression ''
{
personal = {
location = {
sourceDirectories = [ "/home/me/personal" ];
repositories = [ "ssh://myuser@myserver.com/./personal-repo" ];
};
};
work = {
location = {
sourceDirectories = [ "/home/me/work" ];
repositories = [ "ssh://myuser@myserver.com/./work-repo" ];
};
};
};
'';
};
};
};
config = mkIf cfg.enable {
assertions = [
(lib.hm.assertions.assertPlatform "programs.borgmatic" pkgs
lib.platforms.linux)
];
xdg.configFile = with lib.attrsets;
mapAttrs' (configName: config:
nameValuePair ("borgmatic.d/" + configName + ".yaml") {
text = writeConfig config;
}) cfg.backups;
home.packages = [ cfg.package ];
};
}

View file

@ -0,0 +1,87 @@
{ config, lib, pkgs, ... }:
with lib;
let
serviceConfig = config.services.borgmatic;
programConfig = config.programs.borgmatic;
in {
meta.maintainers = [ maintainers.DamienCassou ];
options = {
services.borgmatic = {
enable = mkEnableOption "Borgmatic service";
frequency = mkOption {
type = types.str;
default = "hourly";
description = ''
How often to run borgmatic when
<code language="nix">services.borgmatic.enable = true</code>.
This value is passed to the systemd timer configuration as
the onCalendar option. See
<citerefentry>
<refentrytitle>systemd.time</refentrytitle>
<manvolnum>7</manvolnum>
</citerefentry>
for more information about the format.
'';
};
};
};
config = mkIf serviceConfig.enable {
assertions = [
(lib.hm.assertions.assertPlatform "services.borgmatic" pkgs
lib.platforms.linux)
];
systemd.user = {
services.borgmatic = {
Unit = {
Description = "borgmatic backup";
# Prevent borgmatic from running unless the machine is
# plugged into power:
ConditionACPower = true;
};
Service = {
Type = "oneshot";
# Lower CPU and I/O priority:
Nice = 19;
CPUSchedulingPolicy = "batch";
IOSchedulingClass = "best-effort";
IOSchedulingPriority = 7;
IOWeight = 100;
Restart = "no";
LogRateLimitIntervalSec = 0;
# Delay start to prevent backups running during boot:
ExecStartPre = "sleep 3m";
ExecStart = ''
${pkgs.systemd}/bin/systemd-inhibit \
--who="borgmatic" \
--why="Prevent interrupting scheduled backup" \
${programConfig.package}/bin/borgmatic \
--stats \
--verbosity -1 \
--list \
--syslog-verbosity 1
'';
};
};
timers.borgmatic = {
Unit.Description = "Run borgmatic backup";
Timer = {
OnCalendar = serviceConfig.frequency;
Persistent = true;
RandomizedDelaySec = "10m";
};
Install.WantedBy = [ "timers.target" ];
};
};
};
}

View file

@ -141,6 +141,7 @@ import nmt {
./modules/misc/xsession
./modules/programs/abook
./modules/programs/autorandr
./modules/programs/borgmatic
./modules/programs/firefox
./modules/programs/foot
./modules/programs/getmail
@ -160,6 +161,7 @@ import nmt {
./modules/programs/xmobar
./modules/programs/yt-dlp
./modules/services/barrier
./modules/services/borgmatic
./modules/services/devilspie2
./modules/services/dropbox
./modules/services/emacs

View file

@ -0,0 +1,110 @@
{ config, pkgs, ... }:
let
boolToString = bool: if bool then "true" else "false";
backups = config.programs.borgmatic.backups;
in {
config = {
programs.borgmatic = {
enable = true;
backups = {
main = {
location = {
sourceDirectories = [ "/my-stuff-to-backup" ];
repositories = [ "/mnt/disk1" "/mnt/disk2" ];
extraConfig = { one_file_system = true; };
};
storage = {
encryptionPasscommand = "fetch-the-password.sh";
extraConfig = { checkpoint_interval = 200; };
};
retention = {
keepWithin = "14d";
keepSecondly = 12;
extraConfig = { prefix = "hostname"; };
};
consistency = {
checks = [
{
name = "repository";
frequency = "2 weeks";
}
{
name = "archives";
frequency = "4 weeks";
}
];
extraConfig = { prefix = "hostname"; };
};
};
};
};
test.stubs.borgmatic = { };
nmt.script = ''
config_file=$TESTED/home-files/.config/borgmatic.d/main.yaml
assertFileExists $config_file
declare -A expectations
expectations[location.source_directories[0]]="${
builtins.elemAt backups.main.location.sourceDirectories 0
}"
expectations[location.repositories[0]]="${
builtins.elemAt backups.main.location.repositories 0
}"
expectations[location.repositories[1]]="${
builtins.elemAt backups.main.location.repositories 1
}"
expectations[location.one_file_system]="${
boolToString backups.main.location.extraConfig.one_file_system
}"
expectations[storage.encryption_passcommand]="${backups.main.storage.encryptionPasscommand}"
expectations[storage.checkpoint_interval]="${
toString backups.main.storage.extraConfig.checkpoint_interval
}"
expectations[retention.keep_within]="${backups.main.retention.keepWithin}"
expectations[retention.keep_secondly]="${
toString backups.main.retention.keepSecondly
}"
expectations[retention.prefix]="${backups.main.retention.extraConfig.prefix}"
expectations[consistency.checks[0].name]="${
(builtins.elemAt backups.main.consistency.checks 0).name
}"
expectations[consistency.checks[0].frequency]="${
(builtins.elemAt backups.main.consistency.checks 0).frequency
}"
expectations[consistency.checks[1].name]="${
(builtins.elemAt backups.main.consistency.checks 1).name
}"
expectations[consistency.checks[1].frequency]="${
(builtins.elemAt backups.main.consistency.checks 1).frequency
}"
expectations[consistency.prefix]="${backups.main.consistency.extraConfig.prefix}"
yq=${pkgs.yq-go}/bin/yq
for filter in "''${!expectations[@]}"; do
expected_value="''${expectations[$filter]}"
actual_value="$($yq ".$filter" $config_file)"
if [[ "$actual_value" != "$expected_value" ]]; then
fail "Expected '$filter' to be '$expected_value' but was '$actual_value'"
fi
done
one_file_system=$($yq ".location.one_file_system" $config_file)
if [[ $one_file_system != "true" ]]; then
fail "Expected one_file_system to be true but it was $one_file_system"
fi
'';
};
}

View file

@ -0,0 +1 @@
{ borgmatic-program-basic-configuration = ./basic-configuration.nix; }

View file

@ -0,0 +1,22 @@
{ config, pkgs, ... }:
{
config = {
services.borgmatic = {
enable = true;
frequency = "weekly";
};
test.stubs.borgmatic = { };
nmt.script = ''
assertFileContent \
$(normalizeStorePaths home-files/.config/systemd/user/borgmatic.service) \
${./basic-configuration.service}
assertFileContent \
home-files/.config/systemd/user/borgmatic.timer \
${./basic-configuration.timer}
'';
};
}

View file

@ -0,0 +1,23 @@
[Service]
CPUSchedulingPolicy=batch
ExecStart=/nix/store/00000000000000000000000000000000-systemd/bin/systemd-inhibit \
--who="borgmatic" \
--why="Prevent interrupting scheduled backup" \
@borgmatic@/bin/borgmatic \
--stats \
--verbosity -1 \
--list \
--syslog-verbosity 1
ExecStartPre=sleep 3m
IOSchedulingClass=best-effort
IOSchedulingPriority=7
IOWeight=100
LogRateLimitIntervalSec=0
Nice=19
Restart=no
Type=oneshot
[Unit]
ConditionACPower=true
Description=borgmatic backup

View file

@ -0,0 +1,10 @@
[Install]
WantedBy=timers.target
[Timer]
OnCalendar=weekly
Persistent=true
RandomizedDelaySec=10m
[Unit]
Description=Run borgmatic backup

View file

@ -0,0 +1 @@
{ borgmatic-service-basic-configuration = ./basic-configuration.nix; }