nixpkgs/modules/system/boot/systemd.nix
Eelco Dolstra f19065c283 Fix broken systemd shutdown services
The services systemd-random-seed-save.service and
systemd-update-utmp-shutdown.service from systemd 203 don't work,
because they have a RequiresMountFor dependency on /var/lib and
/var/log.  Such a dependency produces a cycle, causing systemd to drop
those services:

  Fixing conflicting jobs by deleting job poweroff.target/stop
  Deleting job systemd-poweroff.service/stop as dependency of job poweroff.target/stop
  Deleting job umount.target/stop as dependency of job systemd-poweroff.service/stop
  Deleting job -.mount/start as dependency of job umount.target/stop
  Deleting job systemd-random-seed-save.service/start as dependency of job -.mount/start
  Deleting job systemd-update-utmp-shutdown.service/start as dependency of job -.mount/start

Dropping the RequiresMountFor doesn't work either, because then
/var/... may have meen unmounted or remounted read-only.

Upstream systemd fixes this by doing the actual work from an ExecStop
action in an already-running service than conflicts with
shutdown.target.  So I backported that here.  The main consequence is
that wtmp shutdown records now get written properly.

The main lesson: shutdown.target is useless for pulling in services
during shutdown if they need to write anywhere.
2013-09-22 21:39:05 +02:00

637 lines
18 KiB
Nix
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{ config, pkgs, utils, ... }:
with pkgs.lib;
with utils;
with import ./systemd-unit-options.nix { inherit config pkgs; };
let
cfg = config.systemd;
systemd = cfg.package;
makeUnit = name: unit:
pkgs.runCommand "unit" { inherit (unit) text; preferLocalBuild = true; }
(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"
"network.target"
"network-online.target"
"nss-lookup.target"
"nss-user-lookup.target"
"time-sync.target"
#"cryptsetup.target"
"sigpwr.target"
"timers.target"
"paths.target"
# Rescue mode.
"rescue.target"
"rescue.service"
# 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"
# 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"
"hybrid-sleep.target"
"systemd-hibernate.service"
"systemd-suspend.service"
"systemd-hybrid-sleep.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"
"systemd-kexec.service"
# Password entry.
"systemd-ask-password-console.path"
"systemd-ask-password-console.service"
"systemd-ask-password-wall.path"
"systemd-ask-password-wall.service"
]
++ optionals cfg.enableEmergencyMode [
"emergency.target"
"emergency.service"
];
upstreamWants =
[ #"basic.target.wants"
"sysinit.target.wants"
"sockets.target.wants"
"local-fs.target.wants"
"multi-user.target.wants"
"shutdown.target.wants"
"timers.target.wants"
];
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;
Conflicts = concatStringsSep " " config.conflicts;
"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 requiredBy enable;
text =
''
[Unit]
${attrsToSection def.unitConfig}
'';
};
serviceToUnit = name: def:
{ inherit (def) wantedBy requiredBy enable;
text =
''
[Unit]
${attrsToSection def.unitConfig}
[Service]
Environment=PATH=${def.path}
Environment=LD_LIBRARY_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.stopIfChanged) "X-StopIfChanged=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}
''} ${def.scriptArgs}
''}
${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 requiredBy enable;
text =
''
[Unit]
${attrsToSection def.unitConfig}
[Socket]
${attrsToSection def.socketConfig}
${concatStringsSep "\n" (map (s: "ListenStream=${s}") def.listenStreams)}
'';
};
timerToUnit = name: def:
{ inherit (def) wantedBy requiredBy enable;
text =
''
[Unit]
${attrsToSection def.unitConfig}
[Timer]
${attrsToSection def.timerConfig}
'';
};
mountToUnit = name: def:
{ inherit (def) wantedBy requiredBy 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)}
${concatStrings (mapAttrsToList (name: unit:
concatMapStrings (name2: ''
mkdir -p $out/${name2}.requires
ln -sfn ../${name} $out/${name2}.requires/
'') unit.requiredBy) cfg.units)}
ln -s ${cfg.defaultUnit} $out/default.target
ln -s rescue.target $out/kbrequest.target
mkdir -p $out/getty.target.wants/
ln -s ../getty@tty1.service $out/getty.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 = {
systemd.package = mkOption {
default = pkgs.systemd;
type = types.package;
description = "The systemd package.";
};
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.
'';
};
requiredBy = mkOption {
default = [];
types = types.listOf types.string;
description = "Units that require (i.e. depend on and need to go down with) this unit.";
};
wantedBy = mkOption {
default = [];
types = types.listOf types.string;
description = "Units that want (i.e. depend on) this unit.";
};
};
};
systemd.packages = mkOption {
default = [];
type = types.listOf types.package;
description = "Packages providing systemd units.";
};
systemd.targets = mkOption {
default = {};
type = types.attrsOf types.optionSet;
options = [ unitOptions unitConfig ];
description = "Definition of systemd target units.";
};
systemd.services = mkOption {
default = {};
type = types.attrsOf types.optionSet;
options = [ serviceOptions unitConfig serviceConfig ];
description = "Definition of systemd service units.";
};
systemd.sockets = mkOption {
default = {};
type = types.attrsOf types.optionSet;
options = [ socketOptions unitConfig ];
description = "Definition of systemd socket units.";
};
systemd.timers = mkOption {
default = {};
type = types.attrsOf types.optionSet;
options = [ timerOptions unitConfig ];
description = "Definition of systemd timer units.";
};
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.
'';
};
systemd.defaultUnit = mkOption {
default = "multi-user.target";
type = types.uniq types.string;
description = "Default unit started when the system boots.";
};
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.
'';
};
services.logind.extraConfig = mkOption {
default = "";
type = types.uniq types.string;
example = "HandleLidSwitch=ignore";
description = ''
Extra config options for systemd-logind. See man logind.conf for
available options.
'';
};
systemd.enableEmergencyMode = mkOption {
default = true;
type = types.bool;
description = ''
Whether to enable emergency mode, which is an
<command>sulogin</command> shell started on the console if
mounting a filesystem fails. Since some machines (like EC2
instances) have no console of any kind, emergency mode doesn't
make sense, and it's better to continue with the boot insofar
as possible.
'';
};
};
###### implementation
config = {
system.build.units = units;
environment.systemPackages = [ systemd ];
environment.etc."systemd/system".source = units;
environment.etc."systemd/system.conf".text =
''
[Manager]
'';
environment.etc."systemd/journald.conf".text =
''
[Journal]
RateLimitInterval=${config.services.journald.rateLimitInterval}
RateLimitBurst=${toString config.services.journald.rateLimitBurst}
${optionalString (config.services.journald.console != "") ''
ForwardToConsole=yes
TTYPath=${config.services.journald.console}
''}
'';
environment.etc."systemd/logind.conf".text =
''
[Login]
${config.services.logind.extraConfig}
'';
environment.etc."systemd/sleep.conf".text =
''
[Sleep]
'';
system.activationScripts.systemd = stringAfter [ "groups" ]
''
mkdir -m 0755 -p /var/lib/udev
mkdir -p /var/log/journal
chmod 0755 /var/log/journal
# 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
# Make all journals readable to users in the wheel and adm
# groups, in addition to those in the systemd-journal group.
# Users can always read their own journals.
${pkgs.acl}/bin/setfacl -nm g:wheel:rx,d:g:wheel:rx,g:adm:rx,d:g:adm:rx /var/log/journal
'';
# Target for charon send-keys to hook into.
systemd.targets.keys =
{ description = "Security Keys";
};
systemd.units =
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
// mapAttrs' (n: v: nameValuePair "${n}.timer" (timerToUnit n v)) cfg.timers
// listToAttrs (map
(v: let n = escapeSystemdPath 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";
};
users.extraGroups.systemd-journal.gid = config.ids.gids.systemd-journal;
# FIXME: These are borrowed from upstream systemd.
systemd.services."systemd-update-utmp" =
{ description = "Update UTMP about System Reboot/Shutdown";
wantedBy = [ "sysinit.target" ];
after = [ "systemd-remount-fs.service" ];
before = [ "sysinit.target" "shutdown.target" ];
conflicts = [ "shutdown.target" ];
unitConfig = {
DefaultDependencies = false;
RequiresMountsFor = "/var/log";
};
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = "${systemd}/lib/systemd/systemd-update-utmp reboot";
ExecStop = "${systemd}/lib/systemd/systemd-update-utmp shutdown";
};
restartIfChanged = false;
};
systemd.services."systemd-random-seed" =
{ description = "Load/Save Random Seed";
wantedBy = [ "sysinit.target" "multi-user.target" ];
after = [ "systemd-remount-fs.service" ];
before = [ "sysinit.target" "shutdown.target" ];
conflicts = [ "shutdown.target" ];
unitConfig = {
DefaultDependencies = false;
RequiresMountsFor = "/var/lib";
};
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = "${systemd}/lib/systemd/systemd-random-seed load";
ExecStop = "${systemd}/lib/systemd/systemd-random-seed save";
};
};
};
}