2020-04-24 23:36:52 +00:00
|
|
|
{ config, pkgs, lib, ... }:
|
|
|
|
|
|
|
|
with lib;
|
|
|
|
|
|
|
|
let
|
|
|
|
cfg = config.services.snapper;
|
2023-05-24 13:37:59 +00:00
|
|
|
|
|
|
|
mkValue = v:
|
|
|
|
if isList v then "\"${concatMapStringsSep " " (escape [ "\\" " " ]) v}\""
|
|
|
|
else if v == true then "yes"
|
|
|
|
else if v == false then "no"
|
|
|
|
else if isString v then "\"${v}\""
|
|
|
|
else builtins.toJSON v;
|
|
|
|
|
|
|
|
mkKeyValue = k: v: "${k}=${mkValue v}";
|
|
|
|
|
|
|
|
# "it's recommended to always specify the filesystem type" -- man snapper-configs
|
|
|
|
defaultOf = k: if k == "FSTYPE" then null else configOptions.${k}.default or null;
|
|
|
|
|
|
|
|
safeStr = types.strMatching "[^\n\"]*" // {
|
|
|
|
description = "string without line breaks or quotes";
|
|
|
|
descriptionClass = "conjunction";
|
|
|
|
};
|
|
|
|
|
|
|
|
configOptions = {
|
|
|
|
SUBVOLUME = mkOption {
|
|
|
|
type = types.path;
|
|
|
|
description = lib.mdDoc ''
|
|
|
|
Path of the subvolume or mount point.
|
|
|
|
This path is a subvolume and has to contain a subvolume named
|
|
|
|
.snapshots.
|
|
|
|
See also man:snapper(8) section PERMISSIONS.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
FSTYPE = mkOption {
|
|
|
|
type = types.enum [ "btrfs" ];
|
|
|
|
default = "btrfs";
|
|
|
|
description = lib.mdDoc ''
|
|
|
|
Filesystem type. Only btrfs is stable and tested.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
ALLOW_GROUPS = mkOption {
|
|
|
|
type = types.listOf safeStr;
|
|
|
|
default = [];
|
|
|
|
description = lib.mdDoc ''
|
|
|
|
List of groups allowed to operate with the config.
|
|
|
|
|
|
|
|
Also see the PERMISSIONS section in man:snapper(8).
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
ALLOW_USERS = mkOption {
|
|
|
|
type = types.listOf safeStr;
|
|
|
|
default = [];
|
|
|
|
example = [ "alice" ];
|
|
|
|
description = lib.mdDoc ''
|
|
|
|
List of users allowed to operate with the config. "root" is always
|
|
|
|
implicitly included.
|
|
|
|
|
|
|
|
Also see the PERMISSIONS section in man:snapper(8).
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
TIMELINE_CLEANUP = mkOption {
|
|
|
|
type = types.bool;
|
|
|
|
default = false;
|
|
|
|
description = lib.mdDoc ''
|
|
|
|
Defines whether the timeline cleanup algorithm should be run for the config.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
TIMELINE_CREATE = mkOption {
|
|
|
|
type = types.bool;
|
|
|
|
default = false;
|
|
|
|
description = lib.mdDoc ''
|
|
|
|
Defines whether hourly snapshots should be created.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
};
|
2020-04-24 23:36:52 +00:00
|
|
|
in
|
|
|
|
|
|
|
|
{
|
|
|
|
options.services.snapper = {
|
|
|
|
|
2021-09-28 08:13:01 +00:00
|
|
|
snapshotRootOnBoot = mkOption {
|
|
|
|
type = types.bool;
|
|
|
|
default = false;
|
2022-08-12 12:06:08 +00:00
|
|
|
description = lib.mdDoc ''
|
2021-09-28 08:13:01 +00:00
|
|
|
Whether to snapshot root on boot
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
2020-04-24 23:36:52 +00:00
|
|
|
snapshotInterval = mkOption {
|
|
|
|
type = types.str;
|
|
|
|
default = "hourly";
|
2022-08-21 13:32:41 +00:00
|
|
|
description = lib.mdDoc ''
|
2020-04-24 23:36:52 +00:00
|
|
|
Snapshot interval.
|
|
|
|
|
|
|
|
The format is described in
|
2022-08-21 13:32:41 +00:00
|
|
|
{manpage}`systemd.time(7)`.
|
2020-04-24 23:36:52 +00:00
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
cleanupInterval = mkOption {
|
|
|
|
type = types.str;
|
|
|
|
default = "1d";
|
2022-08-21 13:32:41 +00:00
|
|
|
description = lib.mdDoc ''
|
2020-04-24 23:36:52 +00:00
|
|
|
Cleanup interval.
|
|
|
|
|
|
|
|
The format is described in
|
2022-08-21 13:32:41 +00:00
|
|
|
{manpage}`systemd.time(7)`.
|
2020-04-24 23:36:52 +00:00
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
filters = mkOption {
|
|
|
|
type = types.nullOr types.lines;
|
|
|
|
default = null;
|
2022-08-12 12:06:08 +00:00
|
|
|
description = lib.mdDoc ''
|
2020-04-24 23:36:52 +00:00
|
|
|
Global display difference filter. See man:snapper(8) for more details.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
configs = mkOption {
|
|
|
|
default = { };
|
2021-10-06 13:57:05 +00:00
|
|
|
example = literalExpression ''
|
|
|
|
{
|
|
|
|
home = {
|
2023-05-24 13:37:59 +00:00
|
|
|
SUBVOLUME = "/home";
|
|
|
|
ALLOW_USERS = [ "alice" ];
|
|
|
|
TIMELINE_CREATE = true;
|
|
|
|
TIMELINE_CLEANUP = true;
|
2021-10-06 13:57:05 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
'';
|
2020-04-24 23:36:52 +00:00
|
|
|
|
2022-08-12 12:06:08 +00:00
|
|
|
description = lib.mdDoc ''
|
2023-05-24 13:37:59 +00:00
|
|
|
Subvolume configuration. Any option mentioned in man:snapper-configs(5)
|
|
|
|
is valid here, even if NixOS doesn't document it.
|
2020-04-24 23:36:52 +00:00
|
|
|
'';
|
|
|
|
|
|
|
|
type = types.attrsOf (types.submodule {
|
2023-05-24 13:37:59 +00:00
|
|
|
freeformType = types.attrsOf (types.oneOf [ (types.listOf safeStr) types.bool safeStr types.number ]);
|
2020-04-24 23:36:52 +00:00
|
|
|
|
2023-05-24 13:37:59 +00:00
|
|
|
options = configOptions;
|
2020-04-24 23:36:52 +00:00
|
|
|
});
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
config = mkIf (cfg.configs != {}) (let
|
|
|
|
documentation = [ "man:snapper(8)" "man:snapper-configs(5)" ];
|
|
|
|
in {
|
|
|
|
|
|
|
|
environment = {
|
|
|
|
|
|
|
|
systemPackages = [ pkgs.snapper ];
|
|
|
|
|
|
|
|
# Note: snapper/config-templates/default is only needed for create-config
|
|
|
|
# which is not the NixOS way to configure.
|
|
|
|
etc = {
|
|
|
|
|
|
|
|
"sysconfig/snapper".text = ''
|
|
|
|
SNAPPER_CONFIGS="${lib.concatStringsSep " " (builtins.attrNames cfg.configs)}"
|
|
|
|
'';
|
|
|
|
|
|
|
|
}
|
|
|
|
// (mapAttrs' (name: subvolume: nameValuePair "snapper/configs/${name}" ({
|
2023-05-24 13:37:59 +00:00
|
|
|
text = lib.generators.toKeyValue { inherit mkKeyValue; } (filterAttrs (k: v: v != defaultOf k) subvolume);
|
2020-04-24 23:36:52 +00:00
|
|
|
})) cfg.configs)
|
|
|
|
// (lib.optionalAttrs (cfg.filters != null) {
|
|
|
|
"snapper/filters/default.txt".text = cfg.filters;
|
|
|
|
});
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
services.dbus.packages = [ pkgs.snapper ];
|
|
|
|
|
2020-10-11 12:50:04 +00:00
|
|
|
systemd.services.snapperd = {
|
|
|
|
description = "DBus interface for snapper";
|
|
|
|
inherit documentation;
|
|
|
|
serviceConfig = {
|
|
|
|
Type = "dbus";
|
|
|
|
BusName = "org.opensuse.Snapper";
|
|
|
|
ExecStart = "${pkgs.snapper}/bin/snapperd";
|
2021-09-28 08:13:01 +00:00
|
|
|
CapabilityBoundingSet = "CAP_DAC_OVERRIDE CAP_FOWNER CAP_CHOWN CAP_FSETID CAP_SETFCAP CAP_SYS_ADMIN CAP_SYS_MODULE CAP_IPC_LOCK CAP_SYS_NICE";
|
|
|
|
LockPersonality = true;
|
|
|
|
NoNewPrivileges = false;
|
|
|
|
PrivateNetwork = true;
|
|
|
|
ProtectHostname = true;
|
|
|
|
RestrictAddressFamilies = "AF_UNIX";
|
|
|
|
RestrictRealtime = true;
|
2020-10-11 12:50:04 +00:00
|
|
|
};
|
|
|
|
};
|
|
|
|
|
2020-04-24 23:36:52 +00:00
|
|
|
systemd.services.snapper-timeline = {
|
|
|
|
description = "Timeline of Snapper Snapshots";
|
|
|
|
inherit documentation;
|
2021-09-28 08:13:01 +00:00
|
|
|
requires = [ "local-fs.target" ];
|
2020-04-24 23:36:52 +00:00
|
|
|
serviceConfig.ExecStart = "${pkgs.snapper}/lib/snapper/systemd-helper --timeline";
|
2021-09-28 08:13:01 +00:00
|
|
|
startAt = cfg.snapshotInterval;
|
2020-04-24 23:36:52 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
systemd.services.snapper-cleanup = {
|
|
|
|
description = "Cleanup of Snapper Snapshots";
|
|
|
|
inherit documentation;
|
|
|
|
serviceConfig.ExecStart = "${pkgs.snapper}/lib/snapper/systemd-helper --cleanup";
|
|
|
|
};
|
|
|
|
|
|
|
|
systemd.timers.snapper-cleanup = {
|
|
|
|
description = "Cleanup of Snapper Snapshots";
|
|
|
|
inherit documentation;
|
2021-09-28 08:13:01 +00:00
|
|
|
wantedBy = [ "timers.target" ];
|
|
|
|
requires = [ "local-fs.target" ];
|
2020-04-24 23:36:52 +00:00
|
|
|
timerConfig.OnBootSec = "10m";
|
|
|
|
timerConfig.OnUnitActiveSec = cfg.cleanupInterval;
|
|
|
|
};
|
2021-09-28 08:13:01 +00:00
|
|
|
|
|
|
|
systemd.services.snapper-boot = lib.optionalAttrs cfg.snapshotRootOnBoot {
|
|
|
|
description = "Take snapper snapshot of root on boot";
|
|
|
|
inherit documentation;
|
|
|
|
serviceConfig.ExecStart = "${pkgs.snapper}/bin/snapper --config root create --cleanup-algorithm number --description boot";
|
2023-01-20 10:41:00 +00:00
|
|
|
serviceConfig.Type = "oneshot";
|
2021-09-28 08:13:01 +00:00
|
|
|
requires = [ "local-fs.target" ];
|
|
|
|
wantedBy = [ "multi-user.target" ];
|
|
|
|
unitConfig.ConditionPathExists = "/etc/snapper/configs/root";
|
|
|
|
};
|
|
|
|
|
2023-05-24 13:37:59 +00:00
|
|
|
assertions =
|
|
|
|
concatMap
|
|
|
|
(name:
|
|
|
|
let
|
|
|
|
sub = cfg.configs.${name};
|
|
|
|
in
|
|
|
|
[ { assertion = !(sub ? extraConfig);
|
|
|
|
message = ''
|
|
|
|
The option definition `services.snapper.configs.${name}.extraConfig' no longer has any effect; please remove it.
|
|
|
|
The contents of this option should be migrated to attributes on `services.snapper.configs.${name}'.
|
|
|
|
'';
|
|
|
|
}
|
|
|
|
] ++
|
|
|
|
map
|
|
|
|
(attr: {
|
|
|
|
assertion = !(hasAttr attr sub);
|
|
|
|
message = ''
|
|
|
|
The option definition `services.snapper.configs.${name}.${attr}' has been renamed to `services.snapper.configs.${name}.${toUpper attr}'.
|
|
|
|
'';
|
|
|
|
})
|
|
|
|
[ "fstype" "subvolume" ]
|
|
|
|
)
|
|
|
|
(attrNames cfg.configs);
|
2020-04-24 23:36:52 +00:00
|
|
|
});
|
|
|
|
}
|