Merge pull request #72060 from lopsided98/sanoid-init

sanoid: add package, NixOS module and test
This commit is contained in:
Silvan Mosberger
2020-02-10 01:28:41 +01:00
committed by GitHub
7 changed files with 551 additions and 0 deletions
+213
View File
@@ -0,0 +1,213 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.sanoid;
datasetSettingsType = with types;
(attrsOf (nullOr (oneOf [ str int bool (listOf str) ]))) // {
description = "dataset/template options";
};
# Default values from https://github.com/jimsalterjrs/sanoid/blob/master/sanoid.defaults.conf
commonOptions = {
hourly = mkOption {
description = "Number of hourly snapshots.";
type = types.ints.unsigned;
default = 48;
};
daily = mkOption {
description = "Number of daily snapshots.";
type = types.ints.unsigned;
default = 90;
};
monthly = mkOption {
description = "Number of monthly snapshots.";
type = types.ints.unsigned;
default = 6;
};
yearly = mkOption {
description = "Number of yearly snapshots.";
type = types.ints.unsigned;
default = 0;
};
autoprune = mkOption {
description = "Whether to automatically prune old snapshots.";
type = types.bool;
default = true;
};
autosnap = mkOption {
description = "Whether to automatically take snapshots.";
type = types.bool;
default = true;
};
settings = mkOption {
description = ''
Free-form settings for this template/dataset. See
<link xlink:href="https://github.com/jimsalterjrs/sanoid/blob/master/sanoid.defaults.conf"/>
for allowed values.
'';
type = datasetSettingsType;
};
};
commonConfig = config: {
settings = {
hourly = mkDefault config.hourly;
daily = mkDefault config.daily;
monthly = mkDefault config.monthly;
yearly = mkDefault config.yearly;
autoprune = mkDefault config.autoprune;
autosnap = mkDefault config.autosnap;
};
};
datasetOptions = {
useTemplate = mkOption {
description = "Names of the templates to use for this dataset.";
type = (types.listOf (types.enum (attrNames cfg.templates))) // {
description = "list of template names";
};
default = [];
};
recursive = mkOption {
description = "Whether to recursively snapshot dataset children.";
type = types.bool;
default = false;
};
processChildrenOnly = mkOption {
description = "Whether to only snapshot child datasets if recursing.";
type = types.bool;
default = false;
};
};
datasetConfig = config: {
settings = {
use_template = mkDefault config.useTemplate;
recursive = mkDefault config.recursive;
process_children_only = mkDefault config.processChildrenOnly;
};
};
# Extract pool names from configured datasets
pools = unique (map (d: head (builtins.match "([^/]+).*" d)) (attrNames cfg.datasets));
configFile = let
mkValueString = v:
if builtins.isList v then concatStringsSep "," v
else generators.mkValueStringDefault {} v;
mkKeyValue = k: v: if v == null then ""
else generators.mkKeyValueDefault { inherit mkValueString; } "=" k v;
in generators.toINI { inherit mkKeyValue; } cfg.settings;
configDir = pkgs.writeTextDir "sanoid.conf" configFile;
in {
# Interface
options.services.sanoid = {
enable = mkEnableOption "Sanoid ZFS snapshotting service";
interval = mkOption {
type = types.str;
default = "hourly";
example = "daily";
description = ''
Run sanoid at this interval. The default is to run hourly.
The format is described in
<citerefentry><refentrytitle>systemd.time</refentrytitle>
<manvolnum>7</manvolnum></citerefentry>.
'';
};
datasets = mkOption {
type = types.attrsOf (types.submodule ({ config, ... }: {
options = commonOptions // datasetOptions;
config = mkMerge [ (commonConfig config) (datasetConfig config) ];
}));
default = {};
description = "Datasets to snapshot.";
};
templates = mkOption {
type = types.attrsOf (types.submodule ({ config, ... }: {
options = commonOptions;
config = commonConfig config;
}));
default = {};
description = "Templates for datasets.";
};
settings = mkOption {
type = types.attrsOf datasetSettingsType;
description = ''
Free-form settings written directly to the config file. See
<link xlink:href="https://github.com/jimsalterjrs/sanoid/blob/master/sanoid.defaults.conf"/>
for allowed values.
'';
};
extraArgs = mkOption {
type = types.listOf types.str;
default = [];
example = [ "--verbose" "--readonly" "--debug" ];
description = ''
Extra arguments to pass to sanoid. See
<link xlink:href="https://github.com/jimsalterjrs/sanoid/#sanoid-command-line-options"/>
for allowed options.
'';
};
};
# Implementation
config = mkIf cfg.enable {
services.sanoid.settings = mkMerge [
(mapAttrs' (d: v: nameValuePair ("template_" + d) v.settings) cfg.templates)
(mapAttrs (d: v: v.settings) cfg.datasets)
];
systemd.services.sanoid = {
description = "Sanoid snapshot service";
serviceConfig = {
ExecStartPre = map (pool: lib.escapeShellArgs [
"+/run/booted-system/sw/bin/zfs" "allow"
"sanoid" "snapshot,mount,destroy" pool
]) pools;
ExecStart = lib.escapeShellArgs ([
"${pkgs.sanoid}/bin/sanoid"
"--cron"
"--configdir" configDir
] ++ cfg.extraArgs);
ExecStopPost = map (pool: lib.escapeShellArgs [
"+/run/booted-system/sw/bin/zfs" "unallow" "sanoid" pool
]) pools;
User = "sanoid";
Group = "sanoid";
DynamicUser = true;
RuntimeDirectory = "sanoid";
CacheDirectory = "sanoid";
};
# Prevents missing snapshots during DST changes
environment.TZ = "UTC";
after = [ "zfs.target" ];
startAt = cfg.interval;
};
};
meta.maintainers = with maintainers; [ lopsided98 ];
}
+168
View File
@@ -0,0 +1,168 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.syncoid;
in {
# Interface
options.services.syncoid = {
enable = mkEnableOption "Syncoid ZFS synchronization service";
interval = mkOption {
type = types.str;
default = "hourly";
example = "*-*-* *:15:00";
description = ''
Run syncoid at this interval. The default is to run hourly.
The format is described in
<citerefentry><refentrytitle>systemd.time</refentrytitle>
<manvolnum>7</manvolnum></citerefentry>.
'';
};
user = mkOption {
type = types.str;
default = "root";
example = "backup";
description = ''
The user for the service. Sudo or ZFS privilege delegation must be
configured to use a user other than root.
'';
};
sshKey = mkOption {
type = types.nullOr types.path;
# Prevent key from being copied to store
apply = mapNullable toString;
default = null;
description = ''
SSH private key file to use to login to the remote system. Can be
overridden in individual commands.
'';
};
commonArgs = mkOption {
type = types.listOf types.str;
default = [];
example = [ "--no-sync-snap" ];
description = ''
Arguments to add to every syncoid command, unless disabled for that
command. See
<link xlink:href="https://github.com/jimsalterjrs/sanoid/#syncoid-command-line-options"/>
for available options.
'';
};
commands = mkOption {
type = types.attrsOf (types.submodule ({ name, ... }: {
options = {
source = mkOption {
type = types.str;
example = "pool/dataset";
description = ''
Source ZFS dataset. Can be either local or remote. Defaults to
the attribute name.
'';
};
target = mkOption {
type = types.str;
example = "user@server:pool/dataset";
description = ''
Target ZFS dataset. Can be either local
(<replaceable>pool/dataset</replaceable>) or remote
(<replaceable>user@server:pool/dataset</replaceable>).
'';
};
recursive = mkOption {
type = types.bool;
default = false;
description = ''
Whether to also transfer child datasets.
'';
};
sshKey = mkOption {
type = types.nullOr types.path;
# Prevent key from being copied to store
apply = mapNullable toString;
description = ''
SSH private key file to use to login to the remote system.
Defaults to <option>services.syncoid.sshKey</option> option.
'';
};
sendOptions = mkOption {
type = types.separatedString " ";
default = "";
example = "Lc e";
description = ''
Advanced options to pass to zfs send. Options are specified
without their leading dashes and separated by spaces.
'';
};
recvOptions = mkOption {
type = types.separatedString " ";
default = "";
example = "ux recordsize o compression=lz4";
description = ''
Advanced options to pass to zfs recv. Options are specified
without their leading dashes and separated by spaces.
'';
};
useCommonArgs = mkOption {
type = types.bool;
default = true;
description = ''
Whether to add the configured common arguments to this command.
'';
};
extraArgs = mkOption {
type = types.listOf types.str;
default = [];
example = [ "--sshport 2222" ];
description = "Extra syncoid arguments for this command.";
};
};
config = {
source = mkDefault name;
sshKey = mkDefault cfg.sshKey;
};
}));
default = {};
example."pool/test".target = "root@target:pool/test";
description = "Syncoid commands to run.";
};
};
# Implementation
config = mkIf cfg.enable {
systemd.services.syncoid = {
description = "Syncoid ZFS synchronization service";
script = concatMapStringsSep "\n" (c: lib.escapeShellArgs
([ "${pkgs.sanoid}/bin/syncoid" ]
++ (optionals c.useCommonArgs cfg.commonArgs)
++ (optional c.recursive "-r")
++ (optionals (c.sshKey != null) [ "--sshkey" c.sshKey ])
++ c.extraArgs
++ [ "--sendoptions" c.sendOptions
"--recvoptions" c.recvOptions
c.source c.target
])) (attrValues cfg.commands);
after = [ "zfs.target" ];
serviceConfig.User = cfg.user;
startAt = cfg.interval;
};
};
meta.maintainers = with maintainers; [ lopsided98 ];
}