Merge pull request #131118 from etu/sanoid-syncoid-improvements
nixos/{syncoid,sanoid}: Improve ZFS permission delegation
This commit is contained in:
commit
699ea65439
|
@ -702,6 +702,19 @@
|
||||||
option.
|
option.
|
||||||
</para>
|
</para>
|
||||||
</listitem>
|
</listitem>
|
||||||
|
<listitem>
|
||||||
|
<para>
|
||||||
|
The
|
||||||
|
<link xlink:href="options.html#opt-services.syncoid.enable">services.syncoid.enable</link>
|
||||||
|
module now properly drops ZFS permissions after usage. Before
|
||||||
|
it delegated permissions to whole pools instead of datasets
|
||||||
|
and didn’t clean up after execution. You can manually look
|
||||||
|
this up for your pools by running
|
||||||
|
<literal>zfs allow your-pool-name</literal> and use
|
||||||
|
<literal>zfs unallow syncoid your-pool-name</literal> to clean
|
||||||
|
this up.
|
||||||
|
</para>
|
||||||
|
</listitem>
|
||||||
</itemizedlist>
|
</itemizedlist>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
@ -183,3 +183,5 @@ pt-services.clipcat.enable).
|
||||||
- NSS modules which should come after `dns` should use mkAfter.
|
- NSS modules which should come after `dns` should use mkAfter.
|
||||||
|
|
||||||
- The [networking.wireless.iwd](options.html#opt-networking.wireless.iwd.enable) module has a new [networking.wireless.iwd.settings](options.html#opt-networking.wireless.iwd.settings) option.
|
- The [networking.wireless.iwd](options.html#opt-networking.wireless.iwd.enable) module has a new [networking.wireless.iwd.settings](options.html#opt-networking.wireless.iwd.settings) option.
|
||||||
|
|
||||||
|
- The [services.syncoid.enable](options.html#opt-services.syncoid.enable) module now properly drops ZFS permissions after usage. Before it delegated permissions to whole pools instead of datasets and didn't clean up after execution. You can manually look this up for your pools by running `zfs allow your-pool-name` and use `zfs unallow syncoid your-pool-name` to clean this up.
|
||||||
|
|
|
@ -52,7 +52,7 @@ let
|
||||||
use_template = mkOption {
|
use_template = mkOption {
|
||||||
description = "Names of the templates to use for this dataset.";
|
description = "Names of the templates to use for this dataset.";
|
||||||
type = types.listOf (types.enum (attrNames cfg.templates));
|
type = types.listOf (types.enum (attrNames cfg.templates));
|
||||||
default = [];
|
default = [ ];
|
||||||
};
|
};
|
||||||
useTemplate = use_template;
|
useTemplate = use_template;
|
||||||
|
|
||||||
|
@ -70,116 +70,127 @@ let
|
||||||
processChildrenOnly = process_children_only;
|
processChildrenOnly = process_children_only;
|
||||||
};
|
};
|
||||||
|
|
||||||
# Extract pool names from configured datasets
|
# Extract unique dataset names
|
||||||
pools = unique (map (d: head (builtins.match "([^/]+).*" d)) (attrNames cfg.datasets));
|
datasets = unique (attrNames cfg.datasets);
|
||||||
|
|
||||||
configFile = let
|
# Function to build "zfs allow" and "zfs unallow" commands for the
|
||||||
mkValueString = v:
|
# filesystems we've delegated permissions to.
|
||||||
if builtins.isList v then concatStringsSep "," v
|
buildAllowCommand = zfsAction: permissions: dataset: lib.escapeShellArgs [
|
||||||
else generators.mkValueStringDefault {} v;
|
# Here we explicitly use the booted system to guarantee the stable API needed by ZFS
|
||||||
|
"-+/run/booted-system/sw/bin/zfs"
|
||||||
|
zfsAction
|
||||||
|
"sanoid"
|
||||||
|
(concatStringsSep "," permissions)
|
||||||
|
dataset
|
||||||
|
];
|
||||||
|
|
||||||
mkKeyValue = k: v: if v == null then ""
|
configFile =
|
||||||
else if k == "processChildrenOnly" then ""
|
let
|
||||||
else if k == "useTemplate" then ""
|
mkValueString = v:
|
||||||
else generators.mkKeyValueDefault { inherit mkValueString; } "=" k v;
|
if builtins.isList v then concatStringsSep "," v
|
||||||
in generators.toINI { inherit mkKeyValue; } cfg.settings;
|
else generators.mkValueStringDefault { } v;
|
||||||
|
|
||||||
in {
|
mkKeyValue = k: v:
|
||||||
|
if v == null then ""
|
||||||
|
else if k == "processChildrenOnly" then ""
|
||||||
|
else if k == "useTemplate" then ""
|
||||||
|
else generators.mkKeyValueDefault { inherit mkValueString; } "=" k v;
|
||||||
|
in
|
||||||
|
generators.toINI { inherit mkKeyValue; } cfg.settings;
|
||||||
|
|
||||||
# Interface
|
in
|
||||||
|
{
|
||||||
|
|
||||||
options.services.sanoid = {
|
# Interface
|
||||||
enable = mkEnableOption "Sanoid ZFS snapshotting service";
|
|
||||||
|
|
||||||
interval = mkOption {
|
options.services.sanoid = {
|
||||||
type = types.str;
|
enable = mkEnableOption "Sanoid ZFS snapshotting service";
|
||||||
default = "hourly";
|
|
||||||
example = "daily";
|
|
||||||
description = ''
|
|
||||||
Run sanoid at this interval. The default is to run hourly.
|
|
||||||
|
|
||||||
The format is described in
|
interval = mkOption {
|
||||||
<citerefentry><refentrytitle>systemd.time</refentrytitle>
|
type = types.str;
|
||||||
<manvolnum>7</manvolnum></citerefentry>.
|
default = "hourly";
|
||||||
'';
|
example = "daily";
|
||||||
};
|
description = ''
|
||||||
|
Run sanoid at this interval. The default is to run hourly.
|
||||||
|
|
||||||
datasets = mkOption {
|
The format is described in
|
||||||
type = types.attrsOf (types.submodule ({config, options, ...}: {
|
<citerefentry><refentrytitle>systemd.time</refentrytitle>
|
||||||
freeformType = datasetSettingsType;
|
<manvolnum>7</manvolnum></citerefentry>.
|
||||||
options = commonOptions // datasetOptions;
|
'';
|
||||||
config.use_template = mkAliasDefinitions (mkDefault options.useTemplate or {});
|
|
||||||
config.process_children_only = mkAliasDefinitions (mkDefault options.processChildrenOnly or {});
|
|
||||||
}));
|
|
||||||
default = {};
|
|
||||||
description = "Datasets to snapshot.";
|
|
||||||
};
|
|
||||||
|
|
||||||
templates = mkOption {
|
|
||||||
type = types.attrsOf (types.submodule {
|
|
||||||
freeformType = datasetSettingsType;
|
|
||||||
options = commonOptions;
|
|
||||||
});
|
|
||||||
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
|
datasets = mkOption {
|
||||||
|
type = types.attrsOf (types.submodule ({ config, options, ... }: {
|
||||||
config = mkIf cfg.enable {
|
freeformType = datasetSettingsType;
|
||||||
services.sanoid.settings = mkMerge [
|
options = commonOptions // datasetOptions;
|
||||||
(mapAttrs' (d: v: nameValuePair ("template_" + d) v) cfg.templates)
|
config.use_template = mkAliasDefinitions (mkDefault options.useTemplate or { });
|
||||||
(mapAttrs (d: v: v) cfg.datasets)
|
config.process_children_only = mkAliasDefinitions (mkDefault options.processChildrenOnly or { });
|
||||||
];
|
}));
|
||||||
|
default = { };
|
||||||
systemd.services.sanoid = {
|
description = "Datasets to snapshot.";
|
||||||
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" (pkgs.writeTextDir "sanoid.conf" configFile)
|
|
||||||
] ++ 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 ];
|
templates = mkOption {
|
||||||
}
|
type = types.attrsOf (types.submodule {
|
||||||
|
freeformType = datasetSettingsType;
|
||||||
|
options = commonOptions;
|
||||||
|
});
|
||||||
|
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) cfg.templates)
|
||||||
|
(mapAttrs (d: v: v) cfg.datasets)
|
||||||
|
];
|
||||||
|
|
||||||
|
systemd.services.sanoid = {
|
||||||
|
description = "Sanoid snapshot service";
|
||||||
|
serviceConfig = {
|
||||||
|
ExecStartPre = (map (buildAllowCommand "allow" [ "snapshot" "mount" "destroy" ]) datasets);
|
||||||
|
ExecStopPost = (map (buildAllowCommand "unallow" [ "snapshot" "mount" "destroy" ]) datasets);
|
||||||
|
ExecStart = lib.escapeShellArgs ([
|
||||||
|
"${pkgs.sanoid}/bin/sanoid"
|
||||||
|
"--cron"
|
||||||
|
"--configdir"
|
||||||
|
(pkgs.writeTextDir "sanoid.conf" configFile)
|
||||||
|
] ++ cfg.extraArgs);
|
||||||
|
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 ];
|
||||||
|
}
|
||||||
|
|
|
@ -5,226 +5,243 @@ with lib;
|
||||||
let
|
let
|
||||||
cfg = config.services.syncoid;
|
cfg = config.services.syncoid;
|
||||||
|
|
||||||
# Extract the pool name of a local dataset (any dataset not containing "@")
|
# Extract local dasaset names (so no datasets containing "@")
|
||||||
localPoolName = d: optionals (d != null) (
|
localDatasetName = d: optionals (d != null) (
|
||||||
let m = builtins.match "([^/@]+)[^@]*" d; in
|
let m = builtins.match "([^/@]+[^@]*)" d; in
|
||||||
optionals (m != null) m);
|
optionals (m != null) m
|
||||||
|
);
|
||||||
|
|
||||||
# Escape as required by: https://www.freedesktop.org/software/systemd/man/systemd.unit.html
|
# Escape as required by: https://www.freedesktop.org/software/systemd/man/systemd.unit.html
|
||||||
escapeUnitName = name:
|
escapeUnitName = name:
|
||||||
lib.concatMapStrings (s: if lib.isList s then "-" else s)
|
lib.concatMapStrings (s: if lib.isList s then "-" else s)
|
||||||
(builtins.split "[^a-zA-Z0-9_.\\-]+" name);
|
(builtins.split "[^a-zA-Z0-9_.\\-]+" name);
|
||||||
in {
|
|
||||||
|
|
||||||
# Interface
|
# Function to build "zfs allow" and "zfs unallow" commands for the
|
||||||
|
# filesystems we've delegated permissions to.
|
||||||
|
buildAllowCommand = zfsAction: permissions: dataset: lib.escapeShellArgs [
|
||||||
|
# Here we explicitly use the booted system to guarantee the stable API needed by ZFS
|
||||||
|
"-+/run/booted-system/sw/bin/zfs"
|
||||||
|
zfsAction
|
||||||
|
cfg.user
|
||||||
|
(concatStringsSep "," permissions)
|
||||||
|
dataset
|
||||||
|
];
|
||||||
|
in
|
||||||
|
{
|
||||||
|
|
||||||
options.services.syncoid = {
|
# Interface
|
||||||
enable = mkEnableOption "Syncoid ZFS synchronization service";
|
|
||||||
|
|
||||||
interval = mkOption {
|
options.services.syncoid = {
|
||||||
type = types.str;
|
enable = mkEnableOption "Syncoid ZFS synchronization service";
|
||||||
default = "hourly";
|
|
||||||
example = "*-*-* *:15:00";
|
|
||||||
description = ''
|
|
||||||
Run syncoid at this interval. The default is to run hourly.
|
|
||||||
|
|
||||||
The format is described in
|
interval = mkOption {
|
||||||
<citerefentry><refentrytitle>systemd.time</refentrytitle>
|
type = types.str;
|
||||||
<manvolnum>7</manvolnum></citerefentry>.
|
default = "hourly";
|
||||||
'';
|
example = "*-*-* *:15:00";
|
||||||
};
|
description = ''
|
||||||
|
Run syncoid at this interval. The default is to run hourly.
|
||||||
|
|
||||||
user = mkOption {
|
The format is described in
|
||||||
type = types.str;
|
<citerefentry><refentrytitle>systemd.time</refentrytitle>
|
||||||
default = "syncoid";
|
<manvolnum>7</manvolnum></citerefentry>.
|
||||||
example = "backup";
|
'';
|
||||||
description = ''
|
};
|
||||||
The user for the service. ZFS privilege delegation will be
|
|
||||||
automatically configured for any local pools used by syncoid if this
|
|
||||||
option is set to a user other than root. The user will be given the
|
|
||||||
"hold" and "send" privileges on any pool that has datasets being sent
|
|
||||||
and the "create", "mount", "receive", and "rollback" privileges on
|
|
||||||
any pool that has datasets being received.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
group = mkOption {
|
user = mkOption {
|
||||||
type = types.str;
|
type = types.str;
|
||||||
default = "syncoid";
|
default = "syncoid";
|
||||||
example = "backup";
|
example = "backup";
|
||||||
description = "The group for the service.";
|
description = ''
|
||||||
};
|
The user for the service. ZFS privilege delegation will be
|
||||||
|
automatically configured for any local pools used by syncoid if this
|
||||||
|
option is set to a user other than root. The user will be given the
|
||||||
|
"hold" and "send" privileges on any pool that has datasets being sent
|
||||||
|
and the "create", "mount", "receive", and "rollback" privileges on
|
||||||
|
any pool that has datasets being received.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
sshKey = mkOption {
|
group = mkOption {
|
||||||
type = types.nullOr types.path;
|
type = types.str;
|
||||||
# Prevent key from being copied to store
|
default = "syncoid";
|
||||||
apply = mapNullable toString;
|
example = "backup";
|
||||||
default = null;
|
description = "The group for the service.";
|
||||||
description = ''
|
};
|
||||||
SSH private key file to use to login to the remote system. Can be
|
|
||||||
overridden in individual commands.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
commonArgs = mkOption {
|
sshKey = mkOption {
|
||||||
type = types.listOf types.str;
|
type = types.nullOr types.path;
|
||||||
default = [];
|
# Prevent key from being copied to store
|
||||||
example = [ "--no-sync-snap" ];
|
apply = mapNullable toString;
|
||||||
description = ''
|
default = null;
|
||||||
Arguments to add to every syncoid command, unless disabled for that
|
description = ''
|
||||||
command. See
|
SSH private key file to use to login to the remote system. Can be
|
||||||
<link xlink:href="https://github.com/jimsalterjrs/sanoid/#syncoid-command-line-options"/>
|
overridden in individual commands.
|
||||||
for available options.
|
'';
|
||||||
'';
|
};
|
||||||
};
|
|
||||||
|
|
||||||
service = mkOption {
|
commonArgs = mkOption {
|
||||||
type = types.attrs;
|
type = types.listOf types.str;
|
||||||
default = {};
|
default = [ ];
|
||||||
description = ''
|
example = [ "--no-sync-snap" ];
|
||||||
Systemd configuration common to all syncoid services.
|
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 {
|
service = mkOption {
|
||||||
type = types.attrsOf (types.submodule ({ name, ... }: {
|
type = types.attrs;
|
||||||
options = {
|
default = { };
|
||||||
source = mkOption {
|
description = ''
|
||||||
type = types.str;
|
Systemd configuration common to all syncoid services.
|
||||||
example = "pool/dataset";
|
'';
|
||||||
description = ''
|
};
|
||||||
Source ZFS dataset. Can be either local or remote. Defaults to
|
|
||||||
the attribute name.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
target = mkOption {
|
commands = mkOption {
|
||||||
type = types.str;
|
type = types.attrsOf (types.submodule ({ name, ... }: {
|
||||||
example = "user@server:pool/dataset";
|
options = {
|
||||||
description = ''
|
source = mkOption {
|
||||||
Target ZFS dataset. Can be either local
|
type = types.str;
|
||||||
(<replaceable>pool/dataset</replaceable>) or remote
|
example = "pool/dataset";
|
||||||
(<replaceable>user@server:pool/dataset</replaceable>).
|
description = ''
|
||||||
'';
|
Source ZFS dataset. Can be either local or remote. Defaults to
|
||||||
};
|
the attribute name.
|
||||||
|
'';
|
||||||
recursive = mkEnableOption ''the transfer of 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.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
service = mkOption {
|
|
||||||
type = types.attrs;
|
|
||||||
default = {};
|
|
||||||
description = ''
|
|
||||||
Systemd configuration specific to this syncoid service.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
extraArgs = mkOption {
|
|
||||||
type = types.listOf types.str;
|
|
||||||
default = [];
|
|
||||||
example = [ "--sshport 2222" ];
|
|
||||||
description = "Extra syncoid arguments for this command.";
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
config = {
|
|
||||||
source = mkDefault name;
|
target = mkOption {
|
||||||
sshKey = mkDefault cfg.sshKey;
|
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>).
|
||||||
|
'';
|
||||||
};
|
};
|
||||||
}));
|
|
||||||
default = {};
|
recursive = mkEnableOption ''the transfer of child datasets'';
|
||||||
example = literalExample ''
|
|
||||||
{
|
sshKey = mkOption {
|
||||||
"pool/test".target = "root@target:pool/test";
|
type = types.nullOr types.path;
|
||||||
}
|
# Prevent key from being copied to store
|
||||||
'';
|
apply = mapNullable toString;
|
||||||
description = "Syncoid commands to run.";
|
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.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
service = mkOption {
|
||||||
|
type = types.attrs;
|
||||||
|
default = { };
|
||||||
|
description = ''
|
||||||
|
Systemd configuration specific to this syncoid service.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
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 = literalExample ''
|
||||||
|
{
|
||||||
|
"pool/test".target = "root@target:pool/test";
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
description = "Syncoid commands to run.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# Implementation
|
||||||
|
|
||||||
|
config = mkIf cfg.enable {
|
||||||
|
users = {
|
||||||
|
users = mkIf (cfg.user == "syncoid") {
|
||||||
|
syncoid = {
|
||||||
|
group = cfg.group;
|
||||||
|
isSystemUser = true;
|
||||||
|
# For syncoid to be able to create /var/lib/syncoid/.ssh/
|
||||||
|
# and to use custom ssh_config or known_hosts.
|
||||||
|
home = "/var/lib/syncoid";
|
||||||
|
createHome = false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
groups = mkIf (cfg.group == "syncoid") {
|
||||||
|
syncoid = { };
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
# Implementation
|
systemd.services = mapAttrs'
|
||||||
|
(name: c:
|
||||||
config = mkIf cfg.enable {
|
|
||||||
users = {
|
|
||||||
users = mkIf (cfg.user == "syncoid") {
|
|
||||||
syncoid = {
|
|
||||||
group = cfg.group;
|
|
||||||
isSystemUser = true;
|
|
||||||
# For syncoid to be able to create /var/lib/syncoid/.ssh/
|
|
||||||
# and to use custom ssh_config or known_hosts.
|
|
||||||
home = "/var/lib/syncoid";
|
|
||||||
createHome = false;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
groups = mkIf (cfg.group == "syncoid") {
|
|
||||||
syncoid = {};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
systemd.services = mapAttrs' (name: c:
|
|
||||||
nameValuePair "syncoid-${escapeUnitName name}" (mkMerge [
|
nameValuePair "syncoid-${escapeUnitName name}" (mkMerge [
|
||||||
{ description = "Syncoid ZFS synchronization from ${c.source} to ${c.target}";
|
{
|
||||||
|
description = "Syncoid ZFS synchronization from ${c.source} to ${c.target}";
|
||||||
after = [ "zfs.target" ];
|
after = [ "zfs.target" ];
|
||||||
startAt = cfg.interval;
|
startAt = cfg.interval;
|
||||||
# syncoid may need zpool to get feature@extensible_dataset
|
# syncoid may need zpool to get feature@extensible_dataset
|
||||||
path = [ "/run/booted-system/sw/bin/" ];
|
path = [ "/run/booted-system/sw/bin/" ];
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
ExecStartPre =
|
ExecStartPre =
|
||||||
map (pool: lib.escapeShellArgs [
|
# Permissions snapshot and destroy are in case --no-sync-snap is not used
|
||||||
"+/run/booted-system/sw/bin/zfs" "allow"
|
(map (buildAllowCommand "allow" [ "bookmark" "hold" "send" "snapshot" "destroy" ]) (localDatasetName c.source)) ++
|
||||||
cfg.user "bookmark,hold,send,snapshot,destroy" pool
|
(map (buildAllowCommand "allow" [ "create" "mount" "receive" "rollback" ]) (localDatasetName c.target));
|
||||||
# Permissions snapshot and destroy are in case --no-sync-snap is not used
|
ExecStopPost =
|
||||||
]) (localPoolName c.source) ++
|
# Permissions snapshot and destroy are in case --no-sync-snap is not used
|
||||||
map (pool: lib.escapeShellArgs [
|
(map (buildAllowCommand "unallow" [ "bookmark" "hold" "send" "snapshot" "destroy" ]) (localDatasetName c.source)) ++
|
||||||
"+/run/booted-system/sw/bin/zfs" "allow"
|
(map (buildAllowCommand "unallow" [ "create" "mount" "receive" "rollback" ]) (localDatasetName c.target));
|
||||||
cfg.user "create,mount,receive,rollback" pool
|
|
||||||
]) (localPoolName c.target);
|
|
||||||
ExecStart = lib.escapeShellArgs ([ "${pkgs.sanoid}/bin/syncoid" ]
|
ExecStart = lib.escapeShellArgs ([ "${pkgs.sanoid}/bin/syncoid" ]
|
||||||
++ optionals c.useCommonArgs cfg.commonArgs
|
++ optionals c.useCommonArgs cfg.commonArgs
|
||||||
++ optional c.recursive "-r"
|
++ optional c.recursive "-r"
|
||||||
++ optionals (c.sshKey != null) [ "--sshkey" c.sshKey ]
|
++ optionals (c.sshKey != null) [ "--sshkey" c.sshKey ]
|
||||||
++ c.extraArgs
|
++ c.extraArgs
|
||||||
++ [ "--sendoptions" c.sendOptions
|
++ [
|
||||||
"--recvoptions" c.recvOptions
|
"--sendoptions"
|
||||||
"--no-privilege-elevation"
|
c.sendOptions
|
||||||
c.source c.target
|
"--recvoptions"
|
||||||
]);
|
c.recvOptions
|
||||||
|
"--no-privilege-elevation"
|
||||||
|
c.source
|
||||||
|
c.target
|
||||||
|
]);
|
||||||
User = cfg.user;
|
User = cfg.user;
|
||||||
Group = cfg.group;
|
Group = cfg.group;
|
||||||
StateDirectory = [ "syncoid" ];
|
StateDirectory = [ "syncoid" ];
|
||||||
|
@ -240,7 +257,7 @@ in {
|
||||||
# systemd-analyze security | grep syncoid-'*'
|
# systemd-analyze security | grep syncoid-'*'
|
||||||
AmbientCapabilities = "";
|
AmbientCapabilities = "";
|
||||||
CapabilityBoundingSet = "";
|
CapabilityBoundingSet = "";
|
||||||
DeviceAllow = ["/dev/zfs"];
|
DeviceAllow = [ "/dev/zfs" ];
|
||||||
LockPersonality = true;
|
LockPersonality = true;
|
||||||
MemoryDenyWriteExecute = true;
|
MemoryDenyWriteExecute = true;
|
||||||
NoNewPrivileges = true;
|
NoNewPrivileges = true;
|
||||||
|
@ -266,7 +283,7 @@ in {
|
||||||
BindPaths = [ "/dev/zfs" ];
|
BindPaths = [ "/dev/zfs" ];
|
||||||
BindReadOnlyPaths = [ builtins.storeDir "/etc" "/run" "/bin/sh" ];
|
BindReadOnlyPaths = [ builtins.storeDir "/etc" "/run" "/bin/sh" ];
|
||||||
# Avoid useless mounting of RootDirectory= in the own RootDirectory= of ExecStart='s mount namespace.
|
# Avoid useless mounting of RootDirectory= in the own RootDirectory= of ExecStart='s mount namespace.
|
||||||
InaccessiblePaths = ["-+/run/syncoid/${escapeUnitName name}"];
|
InaccessiblePaths = [ "-+/run/syncoid/${escapeUnitName name}" ];
|
||||||
MountAPIVFS = true;
|
MountAPIVFS = true;
|
||||||
# Create RootDirectory= in the host's mount namespace.
|
# Create RootDirectory= in the host's mount namespace.
|
||||||
RuntimeDirectory = [ "syncoid/${escapeUnitName name}" ];
|
RuntimeDirectory = [ "syncoid/${escapeUnitName name}" ];
|
||||||
|
@ -277,8 +294,14 @@ in {
|
||||||
# perf stat -x, 2>perf.log -e 'syscalls:sys_enter_*' syncoid …
|
# perf stat -x, 2>perf.log -e 'syscalls:sys_enter_*' syncoid …
|
||||||
# awk >perf.syscalls -F "," '$1 > 0 {sub("syscalls:sys_enter_","",$3); print $3}' perf.log
|
# awk >perf.syscalls -F "," '$1 > 0 {sub("syscalls:sys_enter_","",$3); print $3}' perf.log
|
||||||
# systemd-analyze syscall-filter | grep -v -e '#' | sed -e ':loop; /^[^ ]/N; s/\n //; t loop' | grep $(printf ' -e \\<%s\\>' $(cat perf.syscalls)) | cut -f 1 -d ' '
|
# systemd-analyze syscall-filter | grep -v -e '#' | sed -e ':loop; /^[^ ]/N; s/\n //; t loop' | grep $(printf ' -e \\<%s\\>' $(cat perf.syscalls)) | cut -f 1 -d ' '
|
||||||
"~@aio" "~@chown" "~@keyring" "~@memlock" "~@privileged"
|
"~@aio"
|
||||||
"~@resources" "~@setuid" "~@sync" "~@timer"
|
"~@chown"
|
||||||
|
"~@keyring"
|
||||||
|
"~@memlock"
|
||||||
|
"~@privileged"
|
||||||
|
"~@resources"
|
||||||
|
"~@setuid"
|
||||||
|
"~@timer"
|
||||||
];
|
];
|
||||||
SystemCallArchitectures = "native";
|
SystemCallArchitectures = "native";
|
||||||
# This is for BindPaths= and BindReadOnlyPaths=
|
# This is for BindPaths= and BindReadOnlyPaths=
|
||||||
|
@ -288,8 +311,9 @@ in {
|
||||||
}
|
}
|
||||||
cfg.service
|
cfg.service
|
||||||
c.service
|
c.service
|
||||||
])) cfg.commands;
|
]))
|
||||||
};
|
cfg.commands;
|
||||||
|
};
|
||||||
|
|
||||||
meta.maintainers = with maintainers; [ julm lopsided98 ];
|
meta.maintainers = with maintainers; [ julm lopsided98 ];
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,10 +85,18 @@ in {
|
||||||
"chown -R syncoid:syncoid /var/lib/syncoid/",
|
"chown -R syncoid:syncoid /var/lib/syncoid/",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
assert len(source.succeed("zfs allow pool")) == 0, "Pool shouldn't have delegated permissions set before snapshotting"
|
||||||
|
assert len(source.succeed("zfs allow pool/sanoid")) == 0, "Sanoid dataset shouldn't have delegated permissions set before snapshotting"
|
||||||
|
assert len(source.succeed("zfs allow pool/syncoid")) == 0, "Syncoid dataset shouldn't have delegated permissions set before snapshotting"
|
||||||
|
|
||||||
# Take snapshot with sanoid
|
# Take snapshot with sanoid
|
||||||
source.succeed("touch /mnt/pool/sanoid/test.txt")
|
source.succeed("touch /mnt/pool/sanoid/test.txt")
|
||||||
source.systemctl("start --wait sanoid.service")
|
source.systemctl("start --wait sanoid.service")
|
||||||
|
|
||||||
|
assert len(source.succeed("zfs allow pool")) == 0, "Pool shouldn't have delegated permissions set after snapshotting"
|
||||||
|
assert len(source.succeed("zfs allow pool/sanoid")) == 0, "Sanoid dataset shouldn't have delegated permissions set after snapshotting"
|
||||||
|
assert len(source.succeed("zfs allow pool/syncoid")) == 0, "Syncoid dataset shouldn't have delegated permissions set after snapshotting"
|
||||||
|
|
||||||
# Sync snapshots
|
# Sync snapshots
|
||||||
target.wait_for_open_port(22)
|
target.wait_for_open_port(22)
|
||||||
source.succeed("touch /mnt/pool/syncoid/test.txt")
|
source.succeed("touch /mnt/pool/syncoid/test.txt")
|
||||||
|
@ -96,5 +104,9 @@ in {
|
||||||
target.succeed("cat /mnt/pool/sanoid/test.txt")
|
target.succeed("cat /mnt/pool/sanoid/test.txt")
|
||||||
source.systemctl("start --wait syncoid-pool-syncoid.service")
|
source.systemctl("start --wait syncoid-pool-syncoid.service")
|
||||||
target.succeed("cat /mnt/pool/syncoid/test.txt")
|
target.succeed("cat /mnt/pool/syncoid/test.txt")
|
||||||
|
|
||||||
|
assert len(source.succeed("zfs allow pool")) == 0, "Pool shouldn't have delegated permissions set after syncing snapshots"
|
||||||
|
assert len(source.succeed("zfs allow pool/sanoid")) == 0, "Sanoid dataset shouldn't have delegated permissions set after syncing snapshots"
|
||||||
|
assert len(source.succeed("zfs allow pool/syncoid")) == 0, "Syncoid dataset shouldn't have delegated permissions set after syncing snapshots"
|
||||||
'';
|
'';
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{ lib, stdenv, fetchFromGitHub, makeWrapper, zfs
|
{ lib, stdenv, fetchFromGitHub, nixosTests, makeWrapper, zfs
|
||||||
, perlPackages, procps, which, openssh, mbuffer, pv, lzop, gzip, pigz }:
|
, perlPackages, procps, which, openssh, mbuffer, pv, lzop, gzip, pigz }:
|
||||||
|
|
||||||
with lib;
|
with lib;
|
||||||
|
@ -17,6 +17,8 @@ stdenv.mkDerivation rec {
|
||||||
nativeBuildInputs = [ makeWrapper ];
|
nativeBuildInputs = [ makeWrapper ];
|
||||||
buildInputs = with perlPackages; [ perl ConfigIniFiles CaptureTiny ];
|
buildInputs = with perlPackages; [ perl ConfigIniFiles CaptureTiny ];
|
||||||
|
|
||||||
|
passthru.tests = nixosTests.sanoid;
|
||||||
|
|
||||||
installPhase = ''
|
installPhase = ''
|
||||||
runHook preInstall
|
runHook preInstall
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue