16a9bcfe81
This is mainly useful for specifying mounts that depend on other units. For example sshfs or davfs need network (and possibly nameservices). While systemd makes a distinction between local and remote filesystems, this only works for in-kernel filesystems such as nfs and cifs. fuse-based filesystems (such as sshfs and davs) are classified as local, so they fail without networking. By explicitly declaring these mounts as full systemd units (as opposed to having systemd generate them automatically from /etc/fstab), dependencies can be specified as on every other unit. In the future, we can probably port NixOS' filesystems handling to use these native systemd.mount units and skip /etc/fstab altogether, but this probably requires additional changes, such as starting systemd even earlier during boot (stage 1).
568 lines
16 KiB
Nix
568 lines
16 KiB
Nix
{ config, pkgs, ... }:
|
||
|
||
with pkgs.lib;
|
||
with import ./systemd-unit-options.nix { inherit config pkgs; };
|
||
|
||
let
|
||
|
||
cfg = config.boot.systemd;
|
||
|
||
systemd = pkgs.systemd;
|
||
|
||
makeUnit = name: unit:
|
||
pkgs.runCommand "unit" { inherit (unit) text; }
|
||
(if unit.enable then ''
|
||
mkdir -p $out
|
||
echo -n "$text" > $out/${name}
|
||
'' else ''
|
||
mkdir -p $out
|
||
ln -s /dev/null $out/${name}
|
||
'');
|
||
|
||
upstreamUnits =
|
||
[ # Targets.
|
||
"basic.target"
|
||
#"sysinit.target"
|
||
"sockets.target"
|
||
"graphical.target"
|
||
"multi-user.target"
|
||
"getty.target"
|
||
"rescue.target"
|
||
"network.target"
|
||
"nss-lookup.target"
|
||
"nss-user-lookup.target"
|
||
"syslog.target"
|
||
"time-sync.target"
|
||
#"cryptsetup.target"
|
||
"sigpwr.target"
|
||
|
||
# Udev.
|
||
"systemd-udevd-control.socket"
|
||
"systemd-udevd-kernel.socket"
|
||
"systemd-udevd.service"
|
||
"systemd-udev-settle.service"
|
||
"systemd-udev-trigger.service"
|
||
|
||
# Hardware (started by udev when a relevant device is plugged in).
|
||
"sound.target"
|
||
"bluetooth.target"
|
||
"printer.target"
|
||
"smartcard.target"
|
||
|
||
# Login stuff.
|
||
"systemd-logind.service"
|
||
"autovt@.service"
|
||
#"systemd-vconsole-setup.service"
|
||
"systemd-user-sessions.service"
|
||
"dbus-org.freedesktop.login1.service"
|
||
"user@.service"
|
||
|
||
# Journal.
|
||
"systemd-journald.socket"
|
||
"systemd-journald.service"
|
||
"systemd-journal-flush.service"
|
||
"syslog.socket"
|
||
|
||
# SysV init compatibility.
|
||
"systemd-initctl.socket"
|
||
"systemd-initctl.service"
|
||
"runlevel0.target"
|
||
"runlevel1.target"
|
||
"runlevel2.target"
|
||
"runlevel3.target"
|
||
"runlevel4.target"
|
||
"runlevel5.target"
|
||
"runlevel6.target"
|
||
|
||
# Random seed.
|
||
"systemd-random-seed-load.service"
|
||
"systemd-random-seed-save.service"
|
||
|
||
# Utmp maintenance.
|
||
"systemd-update-utmp-runlevel.service"
|
||
"systemd-update-utmp-shutdown.service"
|
||
|
||
# Kernel module loading.
|
||
#"systemd-modules-load.service"
|
||
|
||
# Filesystems.
|
||
"systemd-fsck@.service"
|
||
"systemd-fsck-root.service"
|
||
"systemd-remount-fs.service"
|
||
"local-fs.target"
|
||
"local-fs-pre.target"
|
||
"remote-fs.target"
|
||
"remote-fs-pre.target"
|
||
"swap.target"
|
||
"dev-hugepages.mount"
|
||
"dev-mqueue.mount"
|
||
"sys-fs-fuse-connections.mount"
|
||
"sys-kernel-config.mount"
|
||
"sys-kernel-debug.mount"
|
||
|
||
# Hibernate / suspend.
|
||
"hibernate.target"
|
||
"suspend.target"
|
||
"sleep.target"
|
||
"systemd-hibernate.service"
|
||
"systemd-suspend.service"
|
||
"systemd-shutdownd.socket"
|
||
"systemd-shutdownd.service"
|
||
|
||
# Reboot stuff.
|
||
"reboot.target"
|
||
"systemd-reboot.service"
|
||
"poweroff.target"
|
||
"systemd-poweroff.service"
|
||
"halt.target"
|
||
"systemd-halt.service"
|
||
"ctrl-alt-del.target"
|
||
"shutdown.target"
|
||
"umount.target"
|
||
"final.target"
|
||
"kexec.target"
|
||
|
||
# Password entry.
|
||
"systemd-ask-password-console.path"
|
||
"systemd-ask-password-console.service"
|
||
"systemd-ask-password-wall.path"
|
||
"systemd-ask-password-wall.service"
|
||
];
|
||
|
||
upstreamWants =
|
||
[ "basic.target.wants"
|
||
"sysinit.target.wants"
|
||
"sockets.target.wants"
|
||
"local-fs.target.wants"
|
||
"multi-user.target.wants"
|
||
"shutdown.target.wants"
|
||
];
|
||
|
||
rescueService =
|
||
''
|
||
[Unit]
|
||
Description=Rescue Shell
|
||
DefaultDependencies=no
|
||
Conflicts=shutdown.target
|
||
After=sysinit.target
|
||
Before=shutdown.target
|
||
|
||
[Service]
|
||
Environment=HOME=/root
|
||
WorkingDirectory=/root
|
||
ExecStartPre=-${pkgs.coreutils}/bin/echo 'Welcome to rescue mode. Use "systemctl default" or ^D to enter default mode.'
|
||
#ExecStart=-/sbin/sulogin
|
||
ExecStart=-${pkgs.bashInteractive}/bin/bash --login
|
||
ExecStopPost=-${systemd}/bin/systemctl --fail --no-block default
|
||
Type=idle
|
||
StandardInput=tty-force
|
||
StandardOutput=inherit
|
||
StandardError=inherit
|
||
KillMode=process
|
||
|
||
# Bash ignores SIGTERM, so we send SIGHUP instead, to ensure that bash
|
||
# terminates cleanly.
|
||
KillSignal=SIGHUP
|
||
'';
|
||
|
||
makeJobScript = name: text:
|
||
let x = pkgs.writeTextFile { name = "unit-script"; executable = true; destination = "/bin/${name}"; inherit text; };
|
||
in "${x}/bin/${name}";
|
||
|
||
unitConfig = { name, config, ... }: {
|
||
config = {
|
||
unitConfig =
|
||
{ Requires = concatStringsSep " " config.requires;
|
||
Wants = concatStringsSep " " config.wants;
|
||
After = concatStringsSep " " config.after;
|
||
Before = concatStringsSep " " config.before;
|
||
BindsTo = concatStringsSep " " config.bindsTo;
|
||
PartOf = concatStringsSep " " config.partOf;
|
||
"X-Restart-Triggers" = toString config.restartTriggers;
|
||
} // optionalAttrs (config.description != "") {
|
||
Description = config.description;
|
||
};
|
||
};
|
||
};
|
||
|
||
serviceConfig = { name, config, ... }: {
|
||
config = {
|
||
# Default path for systemd services. Should be quite minimal.
|
||
path =
|
||
[ pkgs.coreutils
|
||
pkgs.findutils
|
||
pkgs.gnugrep
|
||
pkgs.gnused
|
||
systemd
|
||
];
|
||
};
|
||
};
|
||
|
||
mountConfig = { name, config, ... }: {
|
||
config = {
|
||
mountConfig =
|
||
{ What = config.what;
|
||
Where = config.where;
|
||
} // optionalAttrs (config.type != "") {
|
||
Type = config.type;
|
||
} // optionalAttrs (config.options != "") {
|
||
Options = config.options;
|
||
};
|
||
};
|
||
};
|
||
|
||
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));
|
||
|
||
targetToUnit = name: def:
|
||
{ inherit (def) wantedBy enable;
|
||
text =
|
||
''
|
||
[Unit]
|
||
${attrsToSection def.unitConfig}
|
||
'';
|
||
};
|
||
|
||
serviceToUnit = name: def:
|
||
{ inherit (def) wantedBy enable;
|
||
text =
|
||
''
|
||
[Unit]
|
||
${attrsToSection def.unitConfig}
|
||
|
||
[Service]
|
||
Environment=PATH=${def.path}
|
||
${let env = cfg.globalEnvironment // def.environment;
|
||
in concatMapStrings (n: "Environment=${n}=${getAttr n env}\n") (attrNames env)}
|
||
${optionalString (!def.restartIfChanged) "X-RestartIfChanged=false"}
|
||
|
||
${optionalString (def.preStart != "") ''
|
||
ExecStartPre=${makeJobScript "${name}-pre-start" ''
|
||
#! ${pkgs.stdenv.shell} -e
|
||
${def.preStart}
|
||
''}
|
||
''}
|
||
|
||
${optionalString (def.script != "") ''
|
||
ExecStart=${makeJobScript "${name}-start" ''
|
||
#! ${pkgs.stdenv.shell} -e
|
||
${def.script}
|
||
''}
|
||
''}
|
||
|
||
${optionalString (def.postStart != "") ''
|
||
ExecStartPost=${makeJobScript "${name}-post-start" ''
|
||
#! ${pkgs.stdenv.shell} -e
|
||
${def.postStart}
|
||
''}
|
||
''}
|
||
|
||
${optionalString (def.postStop != "") ''
|
||
ExecStopPost=${makeJobScript "${name}-post-stop" ''
|
||
#! ${pkgs.stdenv.shell} -e
|
||
${def.postStop}
|
||
''}
|
||
''}
|
||
|
||
${attrsToSection def.serviceConfig}
|
||
'';
|
||
};
|
||
|
||
socketToUnit = name: def:
|
||
{ inherit (def) wantedBy enable;
|
||
text =
|
||
''
|
||
[Unit]
|
||
${attrsToSection def.unitConfig}
|
||
|
||
[Socket]
|
||
${attrsToSection def.socketConfig}
|
||
'';
|
||
};
|
||
|
||
# this is by no means the full escaping-logic systemd uses
|
||
# so feel free to extend this further.
|
||
mountName = path:
|
||
let escaped = replaceChars [ "-" " " "/" ]
|
||
[ "\x2d" "\x20" "-" ] (toString path);
|
||
in if (substring 0 1 escaped == "-")
|
||
then substring 1 (sub (stringLength escaped) 1) escaped
|
||
else escaped;
|
||
|
||
mountToUnit = name: def:
|
||
assert def.mountConfig.What != "";
|
||
assert def.mountConfig.Where != "";
|
||
{ inherit (def) wantedBy enable;
|
||
text =
|
||
''
|
||
[Unit]
|
||
${attrsToSection def.unitConfig}
|
||
|
||
[Mount]
|
||
${attrsToSection def.mountConfig}
|
||
'';
|
||
};
|
||
|
||
nixosUnits = mapAttrsToList makeUnit cfg.units;
|
||
|
||
units = pkgs.runCommand "units" { preferLocalBuild = true; }
|
||
''
|
||
mkdir -p $out
|
||
for i in ${toString upstreamUnits}; do
|
||
fn=${systemd}/example/systemd/system/$i
|
||
if ! [ -e $fn ]; then echo "missing $fn"; false; fi
|
||
if [ -L $fn ]; then
|
||
cp -pd $fn $out/
|
||
else
|
||
ln -s $fn $out/
|
||
fi
|
||
done
|
||
|
||
for i in ${toString upstreamWants}; do
|
||
fn=${systemd}/example/systemd/system/$i
|
||
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 -v $y; fi
|
||
done
|
||
done
|
||
|
||
for i in ${toString nixosUnits}; do
|
||
ln -s $i/* $out/
|
||
done
|
||
|
||
for i in ${toString cfg.packages}; do
|
||
ln -s $i/etc/systemd/system/* $out/
|
||
done
|
||
|
||
${concatStrings (mapAttrsToList (name: unit:
|
||
concatMapStrings (name2: ''
|
||
mkdir -p $out/${name2}.wants
|
||
ln -sfn ../${name} $out/${name2}.wants/
|
||
'') unit.wantedBy) cfg.units)}
|
||
|
||
ln -s ${cfg.defaultUnit} $out/default.target
|
||
|
||
#ln -s ../getty@tty1.service $out/multi-user.target.wants/
|
||
ln -s ../local-fs.target ../remote-fs.target ../network.target ../nss-lookup.target \
|
||
../nss-user-lookup.target ../swap.target $out/multi-user.target.wants/
|
||
''; # */
|
||
|
||
in
|
||
|
||
{
|
||
|
||
###### interface
|
||
|
||
options = {
|
||
|
||
boot.systemd.units = mkOption {
|
||
description = "Definition of systemd units.";
|
||
default = {};
|
||
type = types.attrsOf types.optionSet;
|
||
options = {
|
||
text = mkOption {
|
||
types = types.uniq types.string;
|
||
description = "Text of this systemd unit.";
|
||
};
|
||
enable = mkOption {
|
||
default = true;
|
||
types = types.bool;
|
||
description = ''
|
||
If set to false, this unit will be a symlink to
|
||
/dev/null. This is primarily useful to prevent specific
|
||
template instances (e.g. <literal>serial-getty@ttyS0</literal>)
|
||
from being started.
|
||
'';
|
||
};
|
||
wantedBy = mkOption {
|
||
default = [];
|
||
types = types.listOf types.string;
|
||
description = "Units that want (i.e. depend on) this unit.";
|
||
};
|
||
};
|
||
};
|
||
|
||
boot.systemd.packages = mkOption {
|
||
default = [];
|
||
type = types.listOf types.package;
|
||
description = "Packages providing systemd units.";
|
||
};
|
||
|
||
boot.systemd.targets = mkOption {
|
||
default = {};
|
||
type = types.attrsOf types.optionSet;
|
||
options = [ unitOptions unitConfig ];
|
||
description = "Definition of systemd target units.";
|
||
};
|
||
|
||
boot.systemd.services = mkOption {
|
||
default = {};
|
||
type = types.attrsOf types.optionSet;
|
||
options = [ serviceOptions unitConfig serviceConfig ];
|
||
description = "Definition of systemd service units.";
|
||
};
|
||
|
||
boot.systemd.sockets = mkOption {
|
||
default = {};
|
||
type = types.attrsOf types.optionSet;
|
||
options = [ socketOptions unitConfig ];
|
||
description = "Definition of systemd socket units.";
|
||
};
|
||
|
||
boot.systemd.mounts = mkOption {
|
||
default = [];
|
||
type = types.listOf types.optionSet;
|
||
options = [ mountOptions unitConfig mountConfig ];
|
||
description = ''
|
||
Definition of systemd mount units.
|
||
This is a list instead of an attrSet, because systemd mandates the names to be derived from
|
||
the 'where' attribute.
|
||
'';
|
||
};
|
||
|
||
boot.systemd.defaultUnit = mkOption {
|
||
default = "multi-user.target";
|
||
type = types.uniq types.string;
|
||
description = "Default unit started when the system boots.";
|
||
};
|
||
|
||
boot.systemd.globalEnvironment = mkOption {
|
||
type = types.attrs;
|
||
default = {};
|
||
example = { TZ = "CET"; };
|
||
description = ''
|
||
Environment variables passed to <emphasis>all</emphasis> systemd units.
|
||
'';
|
||
};
|
||
|
||
services.journald.console = mkOption {
|
||
default = "";
|
||
type = types.uniq types.string;
|
||
description = "If non-empty, write log messages to the specified TTY device.";
|
||
};
|
||
|
||
services.journald.rateLimitInterval = mkOption {
|
||
default = "10s";
|
||
type = types.uniq types.string;
|
||
description = ''
|
||
Configures the rate limiting interval that is applied to all
|
||
messages generated on the system. This rate limiting is applied
|
||
per-service, so that two services which log do not interfere with
|
||
each other's limit. The value may be specified in the following
|
||
units: s, min, h, ms, us. To turn off any kind of rate limiting,
|
||
set either value to 0.
|
||
'';
|
||
};
|
||
|
||
services.journald.rateLimitBurst = mkOption {
|
||
default = 100;
|
||
type = types.uniq types.int;
|
||
description = ''
|
||
Configures the rate limiting burst limit (number of messages per
|
||
interval) that is applied to all messages generated on the system.
|
||
This rate limiting is applied per-service, so that two services
|
||
which log do not interfere with each other's limit.
|
||
'';
|
||
};
|
||
|
||
};
|
||
|
||
|
||
###### implementation
|
||
|
||
config = {
|
||
|
||
system.build.systemd = systemd;
|
||
|
||
system.build.units = units;
|
||
|
||
environment.systemPackages = [ systemd ];
|
||
|
||
environment.etc =
|
||
[ { source = units;
|
||
target = "systemd/system";
|
||
}
|
||
{ source = pkgs.writeText "systemd.conf"
|
||
''
|
||
[Manager]
|
||
'';
|
||
target = "systemd/system.conf";
|
||
}
|
||
{ source = pkgs.writeText "journald.conf"
|
||
''
|
||
[Journal]
|
||
RateLimitInterval=${config.services.journald.rateLimitInterval}
|
||
RateLimitBurst=${toString config.services.journald.rateLimitBurst}
|
||
${optionalString (config.services.journald.console != "") ''
|
||
ForwardToConsole=yes
|
||
TTYPath=${config.services.journald.console}
|
||
''}
|
||
'';
|
||
target = "systemd/journald.conf";
|
||
}
|
||
];
|
||
|
||
system.activationScripts.systemd =
|
||
''
|
||
mkdir -p /var/lib/udev -m 0755
|
||
|
||
# Regenerate the hardware database /var/lib/udev/hwdb.bin
|
||
# whenever systemd changes.
|
||
if [ ! -e /var/lib/udev/prev-systemd -o "$(readlink /var/lib/udev/prev-systemd)" != ${systemd} ]; then
|
||
echo "regenerating udev hardware database..."
|
||
${systemd}/bin/udevadm hwdb --update && ln -sfn ${systemd} /var/lib/udev/prev-systemd
|
||
fi
|
||
'';
|
||
|
||
# Target for ‘charon send-keys’ to hook into.
|
||
boot.systemd.targets.keys =
|
||
{ description = "Security Keys";
|
||
};
|
||
|
||
# This is like the upstream sysinit.target, except that it doesn't
|
||
# depend on local-fs.target and swap.target. If services need to
|
||
# be started after some filesystem (local or otherwise) has been
|
||
# mounted, they should use the RequiresMountsFor option.
|
||
boot.systemd.targets.sysinit =
|
||
{ description = "System Initialization";
|
||
after = [ "emergency.service" "emergency.target" ];
|
||
unitConfig.Conflicts = "emergency.service emergency.target";
|
||
unitConfig.RefuseManualStart = true;
|
||
};
|
||
|
||
boot.systemd.units =
|
||
{ "rescue.service".text = rescueService; }
|
||
// mapAttrs' (n: v: nameValuePair "${n}.target" (targetToUnit n v)) cfg.targets
|
||
// mapAttrs' (n: v: nameValuePair "${n}.service" (serviceToUnit n v)) cfg.services
|
||
// mapAttrs' (n: v: nameValuePair "${n}.socket" (socketToUnit n v)) cfg.sockets
|
||
// listToAttrs (map
|
||
(v: let n = mountName v.where;
|
||
in nameValuePair "${n}.mount" (mountToUnit n v)) cfg.mounts);
|
||
|
||
system.requiredKernelConfig = map config.lib.kernelConfig.isEnabled [
|
||
"CGROUPS" "AUTOFS4_FS" "DEVTMPFS"
|
||
];
|
||
|
||
environment.shellAliases =
|
||
{ start = "systemctl start";
|
||
stop = "systemctl stop";
|
||
restart = "systemctl restart";
|
||
status = "systemctl status";
|
||
};
|
||
|
||
};
|
||
}
|