diff options
Diffstat (limited to 'nixos/modules/system/boot/systemd.nix')
-rw-r--r-- | nixos/modules/system/boot/systemd.nix | 678 |
1 files changed, 678 insertions, 0 deletions
diff --git a/nixos/modules/system/boot/systemd.nix b/nixos/modules/system/boot/systemd.nix new file mode 100644 index 000000000000..c1fb2c45165c --- /dev/null +++ b/nixos/modules/system/boot/systemd.nix @@ -0,0 +1,678 @@ +{ 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; + }; + }; + }; + + automountConfig = { name, config, ... }: { + config = { + automountConfig = + { Where = config.where; + }; + }; + }; + + 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} + ''; + }; + + automountToUnit = name: def: + { inherit (def) wantedBy requiredBy enable; + text = + '' + [Unit] + ${attrsToSection def.unitConfig} + + [Automount] + ${attrsToSection def.automountConfig} + ''; + }; + + 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 { + type = types.str; + description = "Text of this systemd unit."; + }; + enable = mkOption { + default = true; + type = 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 = []; + type = types.listOf types.string; + description = "Units that require (i.e. depend on and need to go down with) this unit."; + }; + wantedBy = mkOption { + default = []; + type = 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.automounts = mkOption { + default = []; + type = types.listOf types.optionSet; + options = [ automountOptions unitConfig automountConfig ]; + description = '' + Definition of systemd automount 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.str; + 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.str; + description = "If non-empty, write log messages to the specified TTY device."; + }; + + services.journald.rateLimitInterval = mkOption { + default = "10s"; + type = types.str; + 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.str; + 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) + // listToAttrs (map + (v: let n = escapeSystemdPath v.where; + in nameValuePair "${n}.automount" (automountToUnit n v)) cfg.automounts); + + 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; + + # Generate timer units for all services that have a ‘startAt’ value. + systemd.timers = + mapAttrs (name: service: + { wantedBy = [ "timers.target" ]; + timerConfig.OnCalendar = service.startAt; + }) + (filterAttrs (name: service: service.startAt != "") cfg.services); + + # 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"; + }; + }; + + }; +} |