nixos-configs/modules/borgbackup/module.nix

279 lines
8 KiB
Nix
Raw Permalink Normal View History

2024-03-02 22:04:23 -05:00
{
config,
lib,
pkgs,
...
}: let
inherit (lib) mkIf mkOption types;
inherit (lib.lists) all any filter findSingle length;
inherit (lib.attrsets) attrValues hasAttr listToAttrs mapAttrs removeAttrs;
2024-03-02 22:04:23 -05:00
inherit (config.sops) secrets;
inherit (config.networking) hostName;
2024-03-02 22:04:23 -05:00
cfg = config.services.borgbackup;
in {
options.services.borgbackup = {
configs = mkOption {
# Taken from https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/services/backup/borgbackup.nix
type = types.attrsOf (types.submodule (
let
globalConfig = config;
in
{
name,
config,
...
}: {
options = {
paths = mkOption {
2024-05-05 23:07:06 -04:00
type = types.nullOr (types.coercedTo types.str lib.singleton (types.listOf types.str));
2024-03-02 22:04:23 -05:00
default = null;
};
dumpCommand = mkOption {
type = with types; nullOr path;
default = null;
};
repo = mkOption {
type = types.str;
default = name;
};
removableDevice = mkOption {
type = types.bool;
default = false;
};
archiveBaseName = mkOption {
type = types.nullOr (types.strMatching "[^/{}]+");
default = "${globalConfig.networking.hostName}-${name}";
};
dateFormat = mkOption {
type = types.str;
default = "+%Y-%m-%dT%H:%M:%S";
};
startAt = mkOption {
type = with types; either str (listOf str);
# Run every 3 hours
default = "00/3:00";
};
persistentTimer = mkOption {
default = false;
type = types.bool;
};
inhibitsSleep = mkOption {
default = false;
type = types.bool;
};
user = mkOption {
type = types.str;
default = "root";
};
group = mkOption {
type = types.str;
default = "root";
};
encryption.mode = mkOption {
type = types.enum [
"repokey"
"keyfile"
"repokey-blake2"
"keyfile-blake2"
"authenticated"
"authenticated-blake2"
"none"
];
default = "none";
};
encryption.passCommand = mkOption {
type = with types; nullOr str;
default = null;
};
encryption.passphrase = mkOption {
type = with types; nullOr str;
default = null;
};
compression = mkOption {
type = types.strMatching "none|(auto,)?(lz4|zstd|zlib|lzma)(,[[:digit:]]{1,2})?";
default = "auto,lz4";
};
exclude = mkOption {
type = with types; listOf str;
default = [];
};
patterns = mkOption {
type = with types; listOf str;
default = [];
};
readWritePaths = mkOption {
type = with types; listOf path;
default = [];
};
privateTmp = mkOption {
type = types.bool;
default = true;
};
doInit = mkOption {
type = types.bool;
default = true;
};
appendFailedSuffix = mkOption {
type = types.bool;
default = true;
};
prune.keep = mkOption {
type = with types; attrsOf (either int (strMatching "[[:digit:]]+[Hdwmy]"));
default = {};
};
prune.prefix = mkOption {
type = types.nullOr (types.str);
default = config.archiveBaseName;
};
environment = mkOption {
type = with types; attrsOf str;
default = {};
};
preHook = mkOption {
type = types.lines;
default = "";
};
postInit = mkOption {
type = types.lines;
default = "";
};
postCreate = mkOption {
type = types.lines;
default = "";
};
postPrune = mkOption {
type = types.lines;
default = "";
};
postHook = mkOption {
type = types.lines;
default = "";
};
extraArgs = mkOption {
type = types.str;
default = "";
};
extraInitArgs = mkOption {
type = types.str;
default = "";
};
extraCreateArgs = mkOption {
type = types.str;
default = "";
};
extraPruneArgs = mkOption {
type = types.str;
default = "";
};
extraCompactArgs = mkOption {
type = types.str;
default = "";
};
};
}
));
default = {};
};
existingRepos = mkOption {
2024-05-05 23:07:06 -04:00
type = types.listOf (types.submodule {
options = {
name = mkOption {
type = types.str;
};
host = mkOption {
type = types.str;
};
2024-05-05 23:07:06 -04:00
authorizedKeys = mkOption {
type = types.listOf types.str;
default = [];
2024-03-02 22:04:23 -05:00
};
2024-05-05 23:07:06 -04:00
};
});
2024-03-02 22:04:23 -05:00
default = [];
};
};
config = mkIf (cfg.configs != {}) {
assertions = [
{
assertion = all (
conf:
any (repo: conf.repo == repo.name) cfg.existingRepos
) (attrValues cfg.configs);
message = ''
The repo you want to backup to needs to exist.
'';
}
];
services.borgbackup = let
backupDir = {
nos = "/data/borgbackups";
servivi = "/home/backups";
};
2024-03-02 22:04:23 -05:00
in {
repos =
mkIf (length cfg.existingRepos > 0)
2024-03-02 22:04:23 -05:00
(listToAttrs (map (r: {
inherit (r) name;
value = {
authorizedKeysAppendOnly = r.authorizedKeys;
path = "${backupDir.${hostName}}/${r.name}";
2024-03-02 22:04:23 -05:00
};
})
(filter (x: x.host == hostName) cfg.existingRepos)));
2024-03-02 22:04:23 -05:00
jobs = mapAttrs (n: v: let
existingRepo = findSingle (x: x.name == v.repo) null null cfg.existingRepos;
2024-03-02 22:04:23 -05:00
otherAttrs = removeAttrs v [
"environment"
"paths"
"preHook"
"postHook"
"repo"
];
pathPrefix = "/root/snaps";
snapPath = "${pathPrefix}/${n}";
in
{
environment =
v.environment
// (mkIf (hasAttr "borg-ssh" secrets) {
BORG_RSH = "ssh -o 'StrictHostKeyChecking=no' -i ${secrets.borg-ssh.path}";
});
2024-03-02 22:04:23 -05:00
paths = map (x: snapPath + x) v.paths;
preHook =
v.preHook
+ ''
if [[ ! -d ${pathPrefix} ]]; then
mkdir -p ${pathPrefix}
fi
${pkgs.btrfs-progs}/bin/btrfs subvolume snapshot -r / ${snapPath}
'';
postHook =
v.postHook
+ ''
${pkgs.btrfs-progs}/bin/btrfs subvolume delete ${snapPath}
'';
repo =
if (hostName != existingRepo.host)
then "ssh://borg@${existingRepo.host}${backupDir.${existingRepo.host}}/${v.repo}"
else "${backupDir.${existingRepo.host}}/${v.repo}";
2024-03-02 22:04:23 -05:00
}
// otherAttrs)
cfg.configs;
};
};
# For accurate stack trace
_file = ./module.nix;
2024-03-02 22:04:23 -05:00
}