2015-04-20 10:31:17 +01:00
|
|
|
|
{ config, lib, pkgs }:
|
|
|
|
|
|
|
|
|
|
with lib;
|
|
|
|
|
|
2018-01-03 10:57:29 +00:00
|
|
|
|
let
|
|
|
|
|
cfg = config.systemd;
|
2021-10-21 02:55:36 +01:00
|
|
|
|
lndir = "${pkgs.buildPackages.xorg.lndir}/bin/lndir";
|
2022-03-13 15:09:36 +00:00
|
|
|
|
systemd = cfg.package;
|
2018-01-03 10:57:29 +00:00
|
|
|
|
in rec {
|
2015-04-20 10:31:17 +01:00
|
|
|
|
|
2022-12-12 01:36:03 +00:00
|
|
|
|
shellEscape = s: (replaceStrings [ "\\" ] [ "\\\\" ] s);
|
2015-04-20 10:31:17 +01:00
|
|
|
|
|
2022-12-12 01:36:03 +00:00
|
|
|
|
mkPathSafeName = lib.replaceStrings ["@" ":" "\\" "[" "]"] ["-" "-" "-" "" ""];
|
nixos: Add 'chroot' options to systemd.services
Currently, if you want to properly chroot a systemd service, you could
do it using BindReadOnlyPaths=/nix/store (which is not what I'd call
"properly", because the whole store is still accessible) or use a
separate derivation that gathers the runtime closure of the service you
want to chroot. The former is the easier method and there is also a
method directly offered by systemd, called ProtectSystem, which still
leaves the whole store accessible. The latter however is a bit more
involved, because you need to bind-mount each store path of the runtime
closure of the service you want to chroot.
This can be achieved using pkgs.closureInfo and a small derivation that
packs everything into a systemd unit, which later can be added to
systemd.packages. That's also what I did several times[1][2] in the
past.
However, this process got a bit tedious, so I decided that it would be
generally useful for NixOS, so this very implementation was born.
Now if you want to chroot a systemd service, all you need to do is:
{
systemd.services.yourservice = {
description = "My Shiny Service";
wantedBy = [ "multi-user.target" ];
chroot.enable = true;
serviceConfig.ExecStart = "${pkgs.myservice}/bin/myservice";
};
}
If more than the dependencies for the ExecStart* and ExecStop* (which
btw. also includes "script" and {pre,post}Start) need to be in the
chroot, it can be specified using the chroot.packages option. By
default (which uses the "full-apivfs"[3] confinement mode), a user
namespace is set up as well and /proc, /sys and /dev are mounted
appropriately.
In addition - and by default - a /bin/sh executable is provided as well,
which is useful for most programs that use the system() C library call
to execute commands via shell. The shell providing /bin/sh is dash
instead of the default in NixOS (which is bash), because it's way more
lightweight and after all we're chrooting because we want to lower the
attack surface and it should be only used for "/bin/sh -c something".
Prior to submitting this here, I did a first implementation of this
outside[4] of nixpkgs, which duplicated the "pathSafeName" functionality
from systemd-lib.nix, just because it's only a single line.
However, I decided to just re-use the one from systemd here and
subsequently made it available when importing systemd-lib.nix, so that
the systemd-chroot implementation also benefits from fixes to that
functionality (which is now a proper function).
Unfortunately, we do have a few limitations as well. The first being
that DynamicUser doesn't work in conjunction with tmpfs, because it
already sets up a tmpfs in a different path and simply ignores the one
we define. We could probably solve this by detecting it and try to
bind-mount our paths to that different path whenever DynamicUser is
enabled.
The second limitation/issue is that RootDirectoryStartOnly doesn't work
right now, because it only affects the RootDirectory option and not the
individual bind mounts or our tmpfs. It would be helpful if systemd
would have a way to disable specific bind mounts as well or at least
have some way to ignore failures for the bind mounts/tmpfs setup.
Another quirk we do have right now is that systemd tries to create a
/usr directory within the chroot, which subsequently fails. Fortunately,
this is just an ugly error and not a hard failure.
[1]: https://github.com/headcounter/shabitica/blob/3bb01728a0237ad5e7/default.nix#L43-L62
[2]: https://github.com/aszlig/avonc/blob/dedf29e092481a33dc/nextcloud.nix#L103-L124
[3]: The reason this is called "full-apivfs" instead of just "full" is
to make room for a *real* "full" confinement mode, which is more
restrictive even.
[4]: https://github.com/aszlig/avonc/blob/92a20bece4df54625e/systemd-chroot.nix
Signed-off-by: aszlig <aszlig@nix.build>
2019-03-10 11:21:55 +00:00
|
|
|
|
|
2021-12-27 12:00:00 +00:00
|
|
|
|
# a type for options that take a unit name
|
|
|
|
|
unitNameType = types.strMatching "[a-zA-Z0-9@%:_.\\-]+[.](service|socket|device|mount|automount|swap|target|path|timer|scope|slice)";
|
|
|
|
|
|
2015-04-20 10:31:17 +01:00
|
|
|
|
makeUnit = name: unit:
|
|
|
|
|
if unit.enable then
|
nixos: Add 'chroot' options to systemd.services
Currently, if you want to properly chroot a systemd service, you could
do it using BindReadOnlyPaths=/nix/store (which is not what I'd call
"properly", because the whole store is still accessible) or use a
separate derivation that gathers the runtime closure of the service you
want to chroot. The former is the easier method and there is also a
method directly offered by systemd, called ProtectSystem, which still
leaves the whole store accessible. The latter however is a bit more
involved, because you need to bind-mount each store path of the runtime
closure of the service you want to chroot.
This can be achieved using pkgs.closureInfo and a small derivation that
packs everything into a systemd unit, which later can be added to
systemd.packages. That's also what I did several times[1][2] in the
past.
However, this process got a bit tedious, so I decided that it would be
generally useful for NixOS, so this very implementation was born.
Now if you want to chroot a systemd service, all you need to do is:
{
systemd.services.yourservice = {
description = "My Shiny Service";
wantedBy = [ "multi-user.target" ];
chroot.enable = true;
serviceConfig.ExecStart = "${pkgs.myservice}/bin/myservice";
};
}
If more than the dependencies for the ExecStart* and ExecStop* (which
btw. also includes "script" and {pre,post}Start) need to be in the
chroot, it can be specified using the chroot.packages option. By
default (which uses the "full-apivfs"[3] confinement mode), a user
namespace is set up as well and /proc, /sys and /dev are mounted
appropriately.
In addition - and by default - a /bin/sh executable is provided as well,
which is useful for most programs that use the system() C library call
to execute commands via shell. The shell providing /bin/sh is dash
instead of the default in NixOS (which is bash), because it's way more
lightweight and after all we're chrooting because we want to lower the
attack surface and it should be only used for "/bin/sh -c something".
Prior to submitting this here, I did a first implementation of this
outside[4] of nixpkgs, which duplicated the "pathSafeName" functionality
from systemd-lib.nix, just because it's only a single line.
However, I decided to just re-use the one from systemd here and
subsequently made it available when importing systemd-lib.nix, so that
the systemd-chroot implementation also benefits from fixes to that
functionality (which is now a proper function).
Unfortunately, we do have a few limitations as well. The first being
that DynamicUser doesn't work in conjunction with tmpfs, because it
already sets up a tmpfs in a different path and simply ignores the one
we define. We could probably solve this by detecting it and try to
bind-mount our paths to that different path whenever DynamicUser is
enabled.
The second limitation/issue is that RootDirectoryStartOnly doesn't work
right now, because it only affects the RootDirectory option and not the
individual bind mounts or our tmpfs. It would be helpful if systemd
would have a way to disable specific bind mounts as well or at least
have some way to ignore failures for the bind mounts/tmpfs setup.
Another quirk we do have right now is that systemd tries to create a
/usr directory within the chroot, which subsequently fails. Fortunately,
this is just an ugly error and not a hard failure.
[1]: https://github.com/headcounter/shabitica/blob/3bb01728a0237ad5e7/default.nix#L43-L62
[2]: https://github.com/aszlig/avonc/blob/dedf29e092481a33dc/nextcloud.nix#L103-L124
[3]: The reason this is called "full-apivfs" instead of just "full" is
to make room for a *real* "full" confinement mode, which is more
restrictive even.
[4]: https://github.com/aszlig/avonc/blob/92a20bece4df54625e/systemd-chroot.nix
Signed-off-by: aszlig <aszlig@nix.build>
2019-03-10 11:21:55 +00:00
|
|
|
|
pkgs.runCommand "unit-${mkPathSafeName name}"
|
2015-07-07 14:01:36 +01:00
|
|
|
|
{ preferLocalBuild = true;
|
|
|
|
|
allowSubstitutes = false;
|
|
|
|
|
inherit (unit) text;
|
|
|
|
|
}
|
2015-04-20 10:31:17 +01:00
|
|
|
|
''
|
2021-08-11 19:28:30 +01:00
|
|
|
|
name=${shellEscape name}
|
2023-03-20 00:11:23 +00:00
|
|
|
|
mkdir -p "$out/$(dirname -- "$name")"
|
2021-08-11 19:28:30 +01:00
|
|
|
|
echo -n "$text" > "$out/$name"
|
2015-04-20 10:31:17 +01:00
|
|
|
|
''
|
|
|
|
|
else
|
nixos: Add 'chroot' options to systemd.services
Currently, if you want to properly chroot a systemd service, you could
do it using BindReadOnlyPaths=/nix/store (which is not what I'd call
"properly", because the whole store is still accessible) or use a
separate derivation that gathers the runtime closure of the service you
want to chroot. The former is the easier method and there is also a
method directly offered by systemd, called ProtectSystem, which still
leaves the whole store accessible. The latter however is a bit more
involved, because you need to bind-mount each store path of the runtime
closure of the service you want to chroot.
This can be achieved using pkgs.closureInfo and a small derivation that
packs everything into a systemd unit, which later can be added to
systemd.packages. That's also what I did several times[1][2] in the
past.
However, this process got a bit tedious, so I decided that it would be
generally useful for NixOS, so this very implementation was born.
Now if you want to chroot a systemd service, all you need to do is:
{
systemd.services.yourservice = {
description = "My Shiny Service";
wantedBy = [ "multi-user.target" ];
chroot.enable = true;
serviceConfig.ExecStart = "${pkgs.myservice}/bin/myservice";
};
}
If more than the dependencies for the ExecStart* and ExecStop* (which
btw. also includes "script" and {pre,post}Start) need to be in the
chroot, it can be specified using the chroot.packages option. By
default (which uses the "full-apivfs"[3] confinement mode), a user
namespace is set up as well and /proc, /sys and /dev are mounted
appropriately.
In addition - and by default - a /bin/sh executable is provided as well,
which is useful for most programs that use the system() C library call
to execute commands via shell. The shell providing /bin/sh is dash
instead of the default in NixOS (which is bash), because it's way more
lightweight and after all we're chrooting because we want to lower the
attack surface and it should be only used for "/bin/sh -c something".
Prior to submitting this here, I did a first implementation of this
outside[4] of nixpkgs, which duplicated the "pathSafeName" functionality
from systemd-lib.nix, just because it's only a single line.
However, I decided to just re-use the one from systemd here and
subsequently made it available when importing systemd-lib.nix, so that
the systemd-chroot implementation also benefits from fixes to that
functionality (which is now a proper function).
Unfortunately, we do have a few limitations as well. The first being
that DynamicUser doesn't work in conjunction with tmpfs, because it
already sets up a tmpfs in a different path and simply ignores the one
we define. We could probably solve this by detecting it and try to
bind-mount our paths to that different path whenever DynamicUser is
enabled.
The second limitation/issue is that RootDirectoryStartOnly doesn't work
right now, because it only affects the RootDirectory option and not the
individual bind mounts or our tmpfs. It would be helpful if systemd
would have a way to disable specific bind mounts as well or at least
have some way to ignore failures for the bind mounts/tmpfs setup.
Another quirk we do have right now is that systemd tries to create a
/usr directory within the chroot, which subsequently fails. Fortunately,
this is just an ugly error and not a hard failure.
[1]: https://github.com/headcounter/shabitica/blob/3bb01728a0237ad5e7/default.nix#L43-L62
[2]: https://github.com/aszlig/avonc/blob/dedf29e092481a33dc/nextcloud.nix#L103-L124
[3]: The reason this is called "full-apivfs" instead of just "full" is
to make room for a *real* "full" confinement mode, which is more
restrictive even.
[4]: https://github.com/aszlig/avonc/blob/92a20bece4df54625e/systemd-chroot.nix
Signed-off-by: aszlig <aszlig@nix.build>
2019-03-10 11:21:55 +00:00
|
|
|
|
pkgs.runCommand "unit-${mkPathSafeName name}-disabled"
|
2015-07-07 14:01:36 +01:00
|
|
|
|
{ preferLocalBuild = true;
|
|
|
|
|
allowSubstitutes = false;
|
|
|
|
|
}
|
2015-04-20 10:31:17 +01:00
|
|
|
|
''
|
2021-08-11 19:28:30 +01:00
|
|
|
|
name=${shellEscape name}
|
|
|
|
|
mkdir -p "$out/$(dirname "$name")"
|
|
|
|
|
ln -s /dev/null "$out/$name"
|
2015-04-20 10:31:17 +01:00
|
|
|
|
'';
|
|
|
|
|
|
2015-04-30 05:44:25 +01:00
|
|
|
|
boolValues = [true false "yes" "no"];
|
|
|
|
|
|
|
|
|
|
digits = map toString (range 0 9);
|
|
|
|
|
|
|
|
|
|
isByteFormat = s:
|
|
|
|
|
let
|
|
|
|
|
l = reverseList (stringToCharacters s);
|
|
|
|
|
suffix = head l;
|
|
|
|
|
nums = tail l;
|
|
|
|
|
in elem suffix (["K" "M" "G" "T"] ++ digits)
|
|
|
|
|
&& all (num: elem num digits) nums;
|
|
|
|
|
|
|
|
|
|
assertByteFormat = name: group: attr:
|
|
|
|
|
optional (attr ? ${name} && ! isByteFormat attr.${name})
|
|
|
|
|
"Systemd ${group} field `${name}' must be in byte format [0-9]+[KMGT].";
|
|
|
|
|
|
|
|
|
|
hexChars = stringToCharacters "0123456789abcdefABCDEF";
|
|
|
|
|
|
|
|
|
|
isMacAddress = s: stringLength s == 17
|
|
|
|
|
&& flip all (splitString ":" s) (bytes:
|
|
|
|
|
all (byte: elem byte hexChars) (stringToCharacters bytes)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
assertMacAddress = name: group: attr:
|
|
|
|
|
optional (attr ? ${name} && ! isMacAddress attr.${name})
|
nixos/networkd: support netdev MAC addresses
According to systemd.netdev manpage:
```
MACAddress=
Specifies the MAC address to use for the device, or takes the special value "none". When "none", systemd-networkd does not request the MAC address for
the device, and the kernel will assign a random MAC address. For "tun", "tap", or "l2tp" devices, the MACAddress= setting in the [NetDev] section is
not supported and will be ignored. Please specify it in the [Link] section of the corresponding systemd.network(5) file. If this option is not set,
"vlan" device inherits the MAC address of the master interface. For other kind of netdevs, if this option is not set, then the MAC address is
generated based on the interface name and the machine-id(5).
Note, even if "none" is specified, systemd-udevd will assign the persistent MAC address for the device, as 99-default.link has
MACAddressPolicy=persistent. So, it is also necessary to create a custom .link file for the device, if the MAC address assignment is not desired.
```
Therefore, `none` is an acceptable value.
2023-07-04 00:21:35 +01:00
|
|
|
|
"Systemd ${group} field `${name}' must be a valid MAC address.";
|
|
|
|
|
|
|
|
|
|
assertNetdevMacAddress = name: group: attr:
|
2023-07-20 10:03:46 +01:00
|
|
|
|
optional (attr ? ${name} && (! isMacAddress attr.${name} && attr.${name} != "none"))
|
nixos/networkd: support netdev MAC addresses
According to systemd.netdev manpage:
```
MACAddress=
Specifies the MAC address to use for the device, or takes the special value "none". When "none", systemd-networkd does not request the MAC address for
the device, and the kernel will assign a random MAC address. For "tun", "tap", or "l2tp" devices, the MACAddress= setting in the [NetDev] section is
not supported and will be ignored. Please specify it in the [Link] section of the corresponding systemd.network(5) file. If this option is not set,
"vlan" device inherits the MAC address of the master interface. For other kind of netdevs, if this option is not set, then the MAC address is
generated based on the interface name and the machine-id(5).
Note, even if "none" is specified, systemd-udevd will assign the persistent MAC address for the device, as 99-default.link has
MACAddressPolicy=persistent. So, it is also necessary to create a custom .link file for the device, if the MAC address assignment is not desired.
```
Therefore, `none` is an acceptable value.
2023-07-04 00:21:35 +01:00
|
|
|
|
"Systemd ${group} field `${name}` must be a valid MAC address or the special value `none`.";
|
|
|
|
|
|
2015-04-30 05:44:25 +01:00
|
|
|
|
|
2020-02-29 17:17:27 +00:00
|
|
|
|
isPort = i: i >= 0 && i <= 65535;
|
|
|
|
|
|
|
|
|
|
assertPort = name: group: attr:
|
|
|
|
|
optional (attr ? ${name} && ! isPort attr.${name})
|
|
|
|
|
"Error on the systemd ${group} field `${name}': ${attr.name} is not a valid port number.";
|
2015-04-30 05:44:25 +01:00
|
|
|
|
|
|
|
|
|
assertValueOneOf = name: values: group: attr:
|
|
|
|
|
optional (attr ? ${name} && !elem attr.${name} values)
|
2018-09-20 12:40:50 +01:00
|
|
|
|
"Systemd ${group} field `${name}' cannot have value `${toString attr.${name}}'.";
|
2015-04-30 05:44:25 +01:00
|
|
|
|
|
|
|
|
|
assertHasField = name: group: attr:
|
|
|
|
|
optional (!(attr ? ${name}))
|
|
|
|
|
"Systemd ${group} field `${name}' must exist.";
|
|
|
|
|
|
|
|
|
|
assertRange = name: min: max: group: attr:
|
|
|
|
|
optional (attr ? ${name} && !(min <= attr.${name} && max >= attr.${name}))
|
|
|
|
|
"Systemd ${group} field `${name}' is outside the range [${toString min},${toString max}]";
|
|
|
|
|
|
2018-08-28 23:31:45 +01:00
|
|
|
|
assertMinimum = name: min: group: attr:
|
|
|
|
|
optional (attr ? ${name} && attr.${name} < min)
|
|
|
|
|
"Systemd ${group} field `${name}' must be greater than or equal to ${toString min}";
|
|
|
|
|
|
2015-04-30 05:44:25 +01:00
|
|
|
|
assertOnlyFields = fields: group: attr:
|
|
|
|
|
let badFields = filter (name: ! elem name fields) (attrNames attr); in
|
|
|
|
|
optional (badFields != [ ])
|
|
|
|
|
"Systemd ${group} has extra fields [${concatStringsSep " " badFields}].";
|
|
|
|
|
|
2018-08-28 23:31:45 +01:00
|
|
|
|
assertInt = name: group: attr:
|
|
|
|
|
optional (attr ? ${name} && !isInt attr.${name})
|
|
|
|
|
"Systemd ${group} field `${name}' is not an integer";
|
|
|
|
|
|
2018-06-04 14:34:21 +01:00
|
|
|
|
checkUnitConfig = group: checks: attrs: let
|
|
|
|
|
# We're applied at the top-level type (attrsOf unitOption), so the actual
|
2021-02-11 21:18:21 +00:00
|
|
|
|
# unit options might contain attributes from mkOverride and mkIf that we need to
|
2018-06-04 14:34:21 +01:00
|
|
|
|
# convert into single values before checking them.
|
|
|
|
|
defs = mapAttrs (const (v:
|
2021-02-11 21:18:21 +00:00
|
|
|
|
if v._type or "" == "override" then v.content
|
|
|
|
|
else if v._type or "" == "if" then v.content
|
|
|
|
|
else v
|
2018-06-04 14:34:21 +01:00
|
|
|
|
)) attrs;
|
|
|
|
|
errors = concatMap (c: c group defs) checks;
|
|
|
|
|
in if errors == [] then true
|
|
|
|
|
else builtins.trace (concatStringsSep "\n" errors) false;
|
2015-04-30 05:44:25 +01:00
|
|
|
|
|
|
|
|
|
toOption = x:
|
|
|
|
|
if x == true then "true"
|
|
|
|
|
else if x == false then "false"
|
|
|
|
|
else toString x;
|
|
|
|
|
|
|
|
|
|
attrsToSection = as:
|
|
|
|
|
concatStrings (concatLists (mapAttrsToList (name: value:
|
|
|
|
|
map (x: ''
|
|
|
|
|
${name}=${toOption x}
|
|
|
|
|
'')
|
|
|
|
|
(if isList value then value else [value]))
|
|
|
|
|
as));
|
|
|
|
|
|
2022-03-20 02:58:38 +00:00
|
|
|
|
generateUnits = { allowCollisions ? true, type, units, upstreamUnits, upstreamWants, packages ? cfg.packages, package ? cfg.package }:
|
|
|
|
|
let
|
|
|
|
|
typeDir = ({
|
|
|
|
|
system = "system";
|
|
|
|
|
initrd = "system";
|
|
|
|
|
user = "user";
|
|
|
|
|
nspawn = "nspawn";
|
|
|
|
|
}).${type};
|
|
|
|
|
in pkgs.runCommand "${type}-units"
|
2015-07-07 14:01:36 +01:00
|
|
|
|
{ preferLocalBuild = true;
|
|
|
|
|
allowSubstitutes = false;
|
|
|
|
|
} ''
|
2015-04-20 10:31:17 +01:00
|
|
|
|
mkdir -p $out
|
|
|
|
|
|
|
|
|
|
# Copy the upstream systemd units we're interested in.
|
|
|
|
|
for i in ${toString upstreamUnits}; do
|
2022-03-20 02:58:38 +00:00
|
|
|
|
fn=${package}/example/systemd/${typeDir}/$i
|
2015-04-20 10:31:17 +01:00
|
|
|
|
if ! [ -e $fn ]; then echo "missing $fn"; false; fi
|
|
|
|
|
if [ -L $fn ]; then
|
|
|
|
|
target="$(readlink "$fn")"
|
|
|
|
|
if [ ''${target:0:3} = ../ ]; then
|
|
|
|
|
ln -s "$(readlink -f "$fn")" $out/
|
|
|
|
|
else
|
|
|
|
|
cp -pd $fn $out/
|
|
|
|
|
fi
|
|
|
|
|
else
|
|
|
|
|
ln -s $fn $out/
|
|
|
|
|
fi
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
# Copy .wants links, but only those that point to units that
|
|
|
|
|
# we're interested in.
|
|
|
|
|
for i in ${toString upstreamWants}; do
|
2022-03-20 02:58:38 +00:00
|
|
|
|
fn=${package}/example/systemd/${typeDir}/$i
|
2015-04-20 10:31:17 +01:00
|
|
|
|
if ! [ -e $fn ]; then echo "missing $fn"; false; fi
|
|
|
|
|
x=$out/$(basename $fn)
|
|
|
|
|
mkdir $x
|
|
|
|
|
for i in $fn/*; do
|
|
|
|
|
y=$x/$(basename $i)
|
|
|
|
|
cp -pd $i $y
|
|
|
|
|
if ! [ -e $y ]; then rm $y; fi
|
|
|
|
|
done
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
# Symlink all units provided listed in systemd.packages.
|
2022-03-20 02:58:38 +00:00
|
|
|
|
packages="${toString packages}"
|
2020-01-08 02:41:28 +00:00
|
|
|
|
|
|
|
|
|
# Filter duplicate directories
|
|
|
|
|
declare -A unique_packages
|
|
|
|
|
for k in $packages ; do unique_packages[$k]=1 ; done
|
|
|
|
|
|
|
|
|
|
for i in ''${!unique_packages[@]}; do
|
2022-03-20 02:58:38 +00:00
|
|
|
|
for fn in $i/etc/systemd/${typeDir}/* $i/lib/systemd/${typeDir}/*; do
|
2015-04-20 10:31:17 +01:00
|
|
|
|
if ! [[ "$fn" =~ .wants$ ]]; then
|
2018-01-03 10:57:29 +00:00
|
|
|
|
if [[ -d "$fn" ]]; then
|
|
|
|
|
targetDir="$out/$(basename "$fn")"
|
|
|
|
|
mkdir -p "$targetDir"
|
|
|
|
|
${lndir} "$fn" "$targetDir"
|
|
|
|
|
else
|
|
|
|
|
ln -s $fn $out/
|
|
|
|
|
fi
|
2015-04-20 10:31:17 +01:00
|
|
|
|
fi
|
|
|
|
|
done
|
|
|
|
|
done
|
|
|
|
|
|
2022-08-06 21:29:56 +01:00
|
|
|
|
# Symlink units defined by systemd.units where override strategy
|
|
|
|
|
# shall be automatically detected. If these are also provided by
|
|
|
|
|
# systemd or systemd.packages, then add them as
|
2015-04-20 10:31:17 +01:00
|
|
|
|
# <unit-name>.d/overrides.conf, which makes them extend the
|
|
|
|
|
# upstream unit.
|
2022-08-06 21:29:56 +01:00
|
|
|
|
for i in ${toString (mapAttrsToList
|
|
|
|
|
(n: v: v.unit)
|
|
|
|
|
(lib.filterAttrs (n: v: (attrByPath [ "overrideStrategy" ] "asDropinIfExists" v) == "asDropinIfExists") units))}; do
|
2015-04-20 10:31:17 +01:00
|
|
|
|
fn=$(basename $i/*)
|
2021-03-19 08:03:49 +00:00
|
|
|
|
if [ -e $out/$fn ]; then
|
2015-04-20 10:31:17 +01:00
|
|
|
|
if [ "$(readlink -f $i/$fn)" = /dev/null ]; then
|
|
|
|
|
ln -sfn /dev/null $out/$fn
|
|
|
|
|
else
|
2020-04-04 20:11:21 +01:00
|
|
|
|
${if allowCollisions then ''
|
|
|
|
|
mkdir -p $out/$fn.d
|
|
|
|
|
ln -s $i/$fn $out/$fn.d/overrides.conf
|
|
|
|
|
'' else ''
|
|
|
|
|
echo "Found multiple derivations configuring $fn!"
|
|
|
|
|
exit 1
|
|
|
|
|
''}
|
2015-04-20 10:31:17 +01:00
|
|
|
|
fi
|
|
|
|
|
else
|
|
|
|
|
ln -fs $i/$fn $out/
|
|
|
|
|
fi
|
|
|
|
|
done
|
|
|
|
|
|
2022-08-06 21:29:56 +01:00
|
|
|
|
# Symlink units defined by systemd.units which shall be
|
|
|
|
|
# treated as drop-in file.
|
|
|
|
|
for i in ${toString (mapAttrsToList
|
|
|
|
|
(n: v: v.unit)
|
|
|
|
|
(lib.filterAttrs (n: v: v ? overrideStrategy && v.overrideStrategy == "asDropin") units))}; do
|
|
|
|
|
fn=$(basename $i/*)
|
|
|
|
|
mkdir -p $out/$fn.d
|
|
|
|
|
ln -s $i/$fn $out/$fn.d/overrides.conf
|
|
|
|
|
done
|
|
|
|
|
|
2017-02-01 21:51:16 +00:00
|
|
|
|
# Create service aliases from aliases option.
|
|
|
|
|
${concatStrings (mapAttrsToList (name: unit:
|
|
|
|
|
concatMapStrings (name2: ''
|
|
|
|
|
ln -sfn '${name}' $out/'${name2}'
|
2022-10-23 14:57:55 +01:00
|
|
|
|
'') (unit.aliases or [])) units)}
|
2017-02-01 21:51:16 +00:00
|
|
|
|
|
|
|
|
|
# Create .wants and .requires symlinks from the wantedBy and
|
2015-04-20 10:31:17 +01:00
|
|
|
|
# requiredBy options.
|
|
|
|
|
${concatStrings (mapAttrsToList (name: unit:
|
|
|
|
|
concatMapStrings (name2: ''
|
|
|
|
|
mkdir -p $out/'${name2}.wants'
|
|
|
|
|
ln -sfn '../${name}' $out/'${name2}.wants'/
|
2022-10-23 14:57:55 +01:00
|
|
|
|
'') (unit.wantedBy or [])) units)}
|
2015-04-20 10:31:17 +01:00
|
|
|
|
|
|
|
|
|
${concatStrings (mapAttrsToList (name: unit:
|
|
|
|
|
concatMapStrings (name2: ''
|
|
|
|
|
mkdir -p $out/'${name2}.requires'
|
|
|
|
|
ln -sfn '../${name}' $out/'${name2}.requires'/
|
2022-10-23 14:57:55 +01:00
|
|
|
|
'') (unit.requiredBy or [])) units)}
|
2015-04-20 10:31:17 +01:00
|
|
|
|
|
|
|
|
|
${optionalString (type == "system") ''
|
|
|
|
|
# Stupid misc. symlinks.
|
|
|
|
|
ln -s ${cfg.defaultUnit} $out/default.target
|
2016-07-19 08:42:53 +01:00
|
|
|
|
ln -s ${cfg.ctrlAltDelUnit} $out/ctrl-alt-del.target
|
2015-04-20 10:31:17 +01:00
|
|
|
|
ln -s rescue.target $out/kbrequest.target
|
|
|
|
|
|
|
|
|
|
mkdir -p $out/getty.target.wants/
|
|
|
|
|
ln -s ../autovt@tty1.service $out/getty.target.wants/
|
|
|
|
|
|
2021-04-28 18:50:08 +01:00
|
|
|
|
ln -s ../remote-fs.target $out/multi-user.target.wants/
|
2015-04-20 10:31:17 +01:00
|
|
|
|
''}
|
|
|
|
|
''; # */
|
|
|
|
|
|
2022-03-13 15:09:36 +00:00
|
|
|
|
makeJobScript = name: text:
|
|
|
|
|
let
|
2022-12-12 01:36:03 +00:00
|
|
|
|
scriptName = replaceStrings [ "\\" "@" ] [ "-" "_" ] (shellEscape name);
|
2022-03-13 15:09:36 +00:00
|
|
|
|
out = (pkgs.writeShellScriptBin scriptName ''
|
|
|
|
|
set -e
|
|
|
|
|
${text}
|
|
|
|
|
'').overrideAttrs (_: {
|
|
|
|
|
# The derivation name is different from the script file name
|
|
|
|
|
# to keep the script file name short to avoid cluttering logs.
|
|
|
|
|
name = "unit-script-${scriptName}";
|
|
|
|
|
});
|
|
|
|
|
in "${out}/bin/${scriptName}";
|
|
|
|
|
|
|
|
|
|
unitConfig = { config, options, ... }: {
|
|
|
|
|
config = {
|
|
|
|
|
unitConfig =
|
|
|
|
|
optionalAttrs (config.requires != [])
|
|
|
|
|
{ Requires = toString config.requires; }
|
|
|
|
|
// optionalAttrs (config.wants != [])
|
|
|
|
|
{ Wants = toString config.wants; }
|
|
|
|
|
// optionalAttrs (config.after != [])
|
|
|
|
|
{ After = toString config.after; }
|
|
|
|
|
// optionalAttrs (config.before != [])
|
|
|
|
|
{ Before = toString config.before; }
|
|
|
|
|
// optionalAttrs (config.bindsTo != [])
|
|
|
|
|
{ BindsTo = toString config.bindsTo; }
|
|
|
|
|
// optionalAttrs (config.partOf != [])
|
|
|
|
|
{ PartOf = toString config.partOf; }
|
|
|
|
|
// optionalAttrs (config.conflicts != [])
|
|
|
|
|
{ Conflicts = toString config.conflicts; }
|
|
|
|
|
// optionalAttrs (config.requisite != [])
|
|
|
|
|
{ Requisite = toString config.requisite; }
|
2022-04-01 09:44:08 +01:00
|
|
|
|
// optionalAttrs (config ? restartTriggers && config.restartTriggers != [])
|
2023-05-11 10:38:34 +01:00
|
|
|
|
{ X-Restart-Triggers = "${pkgs.writeText "X-Restart-Triggers" (toString config.restartTriggers)}"; }
|
2022-04-01 09:44:08 +01:00
|
|
|
|
// optionalAttrs (config ? reloadTriggers && config.reloadTriggers != [])
|
2023-05-11 10:38:34 +01:00
|
|
|
|
{ X-Reload-Triggers = "${pkgs.writeText "X-Reload-Triggers" (toString config.reloadTriggers)}"; }
|
2022-03-13 15:09:36 +00:00
|
|
|
|
// optionalAttrs (config.description != "") {
|
|
|
|
|
Description = config.description; }
|
|
|
|
|
// optionalAttrs (config.documentation != []) {
|
|
|
|
|
Documentation = toString config.documentation; }
|
|
|
|
|
// optionalAttrs (config.onFailure != []) {
|
|
|
|
|
OnFailure = toString config.onFailure; }
|
2022-04-07 12:21:58 +01:00
|
|
|
|
// optionalAttrs (config.onSuccess != []) {
|
|
|
|
|
OnSuccess = toString config.onSuccess; }
|
2022-03-13 15:09:36 +00:00
|
|
|
|
// optionalAttrs (options.startLimitIntervalSec.isDefined) {
|
|
|
|
|
StartLimitIntervalSec = toString config.startLimitIntervalSec;
|
|
|
|
|
} // optionalAttrs (options.startLimitBurst.isDefined) {
|
|
|
|
|
StartLimitBurst = toString config.startLimitBurst;
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
2022-04-01 09:44:08 +01:00
|
|
|
|
serviceConfig = { config, ... }: {
|
|
|
|
|
config.environment.PATH = mkIf (config.path != []) "${makeBinPath config.path}:${makeSearchPathOutput "bin" "sbin" config.path}";
|
2022-03-13 15:09:36 +00:00
|
|
|
|
};
|
|
|
|
|
|
2022-04-01 08:39:56 +01:00
|
|
|
|
stage2ServiceConfig = {
|
|
|
|
|
imports = [ serviceConfig ];
|
|
|
|
|
# Default path for systemd services. Should be quite minimal.
|
|
|
|
|
config.path = mkAfter [
|
|
|
|
|
pkgs.coreutils
|
|
|
|
|
pkgs.findutils
|
|
|
|
|
pkgs.gnugrep
|
|
|
|
|
pkgs.gnused
|
|
|
|
|
systemd
|
|
|
|
|
];
|
|
|
|
|
};
|
2022-03-20 03:00:06 +00:00
|
|
|
|
|
2022-04-01 08:39:56 +01:00
|
|
|
|
stage1ServiceConfig = serviceConfig;
|
2022-03-20 03:00:06 +00:00
|
|
|
|
|
2022-03-13 15:09:36 +00:00
|
|
|
|
mountConfig = { config, ... }: {
|
|
|
|
|
config = {
|
|
|
|
|
mountConfig =
|
|
|
|
|
{ What = config.what;
|
|
|
|
|
Where = config.where;
|
|
|
|
|
} // optionalAttrs (config.type != "") {
|
|
|
|
|
Type = config.type;
|
|
|
|
|
} // optionalAttrs (config.options != "") {
|
|
|
|
|
Options = config.options;
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
automountConfig = { config, ... }: {
|
|
|
|
|
config = {
|
|
|
|
|
automountConfig =
|
|
|
|
|
{ Where = config.where;
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
commonUnitText = def: ''
|
|
|
|
|
[Unit]
|
|
|
|
|
${attrsToSection def.unitConfig}
|
|
|
|
|
'';
|
|
|
|
|
|
|
|
|
|
targetToUnit = name: def:
|
2022-08-06 21:29:56 +01:00
|
|
|
|
{ inherit (def) aliases wantedBy requiredBy enable overrideStrategy;
|
2022-03-13 15:09:36 +00:00
|
|
|
|
text =
|
|
|
|
|
''
|
|
|
|
|
[Unit]
|
|
|
|
|
${attrsToSection def.unitConfig}
|
|
|
|
|
'';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
serviceToUnit = name: def:
|
2022-08-06 21:29:56 +01:00
|
|
|
|
{ inherit (def) aliases wantedBy requiredBy enable overrideStrategy;
|
2022-03-13 15:09:36 +00:00
|
|
|
|
text = commonUnitText def +
|
|
|
|
|
''
|
|
|
|
|
[Service]
|
|
|
|
|
${let env = cfg.globalEnvironment // def.environment;
|
|
|
|
|
in concatMapStrings (n:
|
|
|
|
|
let s = optionalString (env.${n} != null)
|
|
|
|
|
"Environment=${builtins.toJSON "${n}=${env.${n}}"}\n";
|
|
|
|
|
# systemd max line length is now 1MiB
|
|
|
|
|
# https://github.com/systemd/systemd/commit/e6dde451a51dc5aaa7f4d98d39b8fe735f73d2af
|
|
|
|
|
in if stringLength s >= 1048576 then throw "The value of the environment variable ‘${n}’ in systemd service ‘${name}.service’ is too long." else s) (attrNames env)}
|
2022-04-01 09:44:08 +01:00
|
|
|
|
${if def ? reloadIfChanged && def.reloadIfChanged then ''
|
2022-03-13 15:09:36 +00:00
|
|
|
|
X-ReloadIfChanged=true
|
2022-04-01 09:44:08 +01:00
|
|
|
|
'' else if (def ? restartIfChanged && !def.restartIfChanged) then ''
|
2022-03-13 15:09:36 +00:00
|
|
|
|
X-RestartIfChanged=false
|
|
|
|
|
'' else ""}
|
2022-04-01 09:44:08 +01:00
|
|
|
|
${optionalString (def ? stopIfChanged && !def.stopIfChanged) "X-StopIfChanged=false"}
|
2022-03-13 15:09:36 +00:00
|
|
|
|
${attrsToSection def.serviceConfig}
|
|
|
|
|
'';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
socketToUnit = name: def:
|
2022-08-06 21:29:56 +01:00
|
|
|
|
{ inherit (def) aliases wantedBy requiredBy enable overrideStrategy;
|
2022-03-13 15:09:36 +00:00
|
|
|
|
text = commonUnitText def +
|
|
|
|
|
''
|
|
|
|
|
[Socket]
|
|
|
|
|
${attrsToSection def.socketConfig}
|
|
|
|
|
${concatStringsSep "\n" (map (s: "ListenStream=${s}") def.listenStreams)}
|
|
|
|
|
${concatStringsSep "\n" (map (s: "ListenDatagram=${s}") def.listenDatagrams)}
|
|
|
|
|
'';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
timerToUnit = name: def:
|
2022-08-06 21:29:56 +01:00
|
|
|
|
{ inherit (def) aliases wantedBy requiredBy enable overrideStrategy;
|
2022-03-13 15:09:36 +00:00
|
|
|
|
text = commonUnitText def +
|
|
|
|
|
''
|
|
|
|
|
[Timer]
|
|
|
|
|
${attrsToSection def.timerConfig}
|
|
|
|
|
'';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
pathToUnit = name: def:
|
2022-08-06 21:29:56 +01:00
|
|
|
|
{ inherit (def) aliases wantedBy requiredBy enable overrideStrategy;
|
2022-03-13 15:09:36 +00:00
|
|
|
|
text = commonUnitText def +
|
|
|
|
|
''
|
|
|
|
|
[Path]
|
|
|
|
|
${attrsToSection def.pathConfig}
|
|
|
|
|
'';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
mountToUnit = name: def:
|
2022-08-06 21:29:56 +01:00
|
|
|
|
{ inherit (def) aliases wantedBy requiredBy enable overrideStrategy;
|
2022-03-13 15:09:36 +00:00
|
|
|
|
text = commonUnitText def +
|
|
|
|
|
''
|
|
|
|
|
[Mount]
|
|
|
|
|
${attrsToSection def.mountConfig}
|
|
|
|
|
'';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
automountToUnit = name: def:
|
2022-08-06 21:29:56 +01:00
|
|
|
|
{ inherit (def) aliases wantedBy requiredBy enable overrideStrategy;
|
2022-03-13 15:09:36 +00:00
|
|
|
|
text = commonUnitText def +
|
|
|
|
|
''
|
|
|
|
|
[Automount]
|
|
|
|
|
${attrsToSection def.automountConfig}
|
|
|
|
|
'';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
sliceToUnit = name: def:
|
2022-08-06 21:29:56 +01:00
|
|
|
|
{ inherit (def) aliases wantedBy requiredBy enable overrideStrategy;
|
2022-03-13 15:09:36 +00:00
|
|
|
|
text = commonUnitText def +
|
|
|
|
|
''
|
|
|
|
|
[Slice]
|
|
|
|
|
${attrsToSection def.sliceConfig}
|
|
|
|
|
'';
|
|
|
|
|
};
|
2023-07-26 22:30:08 +01:00
|
|
|
|
|
|
|
|
|
# Create a directory that contains systemd definition files from an attrset
|
|
|
|
|
# that contains the file names as keys and the content as values. The values
|
|
|
|
|
# in that attrset are determined by the supplied format.
|
|
|
|
|
definitions = directoryName: format: definitionAttrs:
|
|
|
|
|
let
|
|
|
|
|
listOfDefinitions = lib.mapAttrsToList
|
|
|
|
|
(name: format.generate "${name}.conf")
|
|
|
|
|
definitionAttrs;
|
|
|
|
|
in
|
|
|
|
|
pkgs.runCommand directoryName { } ''
|
|
|
|
|
mkdir -p $out
|
|
|
|
|
${(lib.concatStringsSep "\n"
|
|
|
|
|
(map (pkg: "cp ${pkg} $out/${pkg.name}") listOfDefinitions)
|
|
|
|
|
)}
|
|
|
|
|
'';
|
|
|
|
|
|
2015-04-20 10:31:17 +01:00
|
|
|
|
}
|