diff options
Diffstat (limited to 'nixos/modules/system/boot')
28 files changed, 4176 insertions, 0 deletions
diff --git a/nixos/modules/system/boot/kernel.nix b/nixos/modules/system/boot/kernel.nix new file mode 100644 index 00000000000..4ceabb20df5 --- /dev/null +++ b/nixos/modules/system/boot/kernel.nix @@ -0,0 +1,304 @@ +{ config, pkgs, ... }: + +with pkgs.lib; + +let + + kernel = config.boot.kernelPackages.kernel; + + kernelModulesConf = pkgs.writeText "nixos.conf" + '' + ${concatStringsSep "\n" config.boot.kernelModules} + ''; + +in + +{ + + ###### interface + + options = { + + boot.kernelPackages = mkOption { + default = pkgs.linuxPackages; + # We don't want to evaluate all of linuxPackages for the manual + # - some of it might not even evaluate correctly. + defaultText = "pkgs.linuxPackages"; + example = "pkgs.linuxPackages_2_6_25"; + description = '' + This option allows you to override the Linux kernel used by + NixOS. Since things like external kernel module packages are + tied to the kernel you're using, it also overrides those. + This option is a function that takes Nixpkgs as an argument + (as a convenience), and returns an attribute set containing at + the very least an attribute <varname>kernel</varname>. + Additional attributes may be needed depending on your + configuration. For instance, if you use the NVIDIA X driver, + then it also needs to contain an attribute + <varname>nvidia_x11</varname>. + ''; + }; + + boot.kernelParams = mkOption { + default = [ ]; + description = '' + The kernel parameters. If you want to add additional + parameters, it's best to set + <option>boot.extraKernelParams</option>. + ''; + }; + + boot.extraKernelParams = mkOption { + default = [ ]; + example = [ "boot.trace" ]; + description = "Additional user-defined kernel parameters."; + }; + + boot.consoleLogLevel = mkOption { + type = types.int; + default = 4; + description = '' + The kernel console log level. Only log messages with a + priority numerically less than this will appear on the + console. + ''; + }; + + boot.vesa = mkOption { + default = false; + description = '' + Whether to activate VESA video mode on boot. + ''; + }; + + boot.extraModulePackages = mkOption { + default = []; + # !!! example = [pkgs.nvidia_x11]; + description = "A list of additional packages supplying kernel modules."; + }; + + boot.kernelModules = mkOption { + default = []; + description = '' + The set of kernel modules to be loaded in the second stage of + the boot process. Note that modules that are needed to + mount the root file system should be added to + <option>boot.initrd.availableKernelModules</option> or + <option>boot.initrd.kernelModules</option>. + ''; + }; + + boot.initrd.availableKernelModules = mkOption { + default = []; + example = [ "sata_nv" "ext3" ]; + description = '' + The set of kernel modules in the initial ramdisk used during the + boot process. This set must include all modules necessary for + mounting the root device. That is, it should include modules + for the physical device (e.g., SCSI drivers) and for the file + system (e.g., ext3). The set specified here is automatically + closed under the module dependency relation, i.e., all + dependencies of the modules list here are included + automatically. The modules listed here are available in the + initrd, but are only loaded on demand (e.g., the ext3 module is + loaded automatically when an ext3 filesystem is mounted, and + modules for PCI devices are loaded when they match the PCI ID + of a device in your system). To force a module to be loaded, + include it in <option>boot.initrd.kernelModules</option>. + ''; + }; + + boot.initrd.kernelModules = mkOption { + default = []; + description = "List of modules that are always loaded by the initrd."; + }; + + system.modulesTree = mkOption { + internal = true; + default = []; + description = '' + Tree of kernel modules. This includes the kernel, plus modules + built outside of the kernel. Combine these into a single tree of + symlinks because modprobe only supports one directory. + ''; + merge = mergeListOption; + # Convert the list of path to only one path. + apply = pkgs.aggregateModules; + }; + + system.requiredKernelConfig = mkOption { + default = []; + example = literalExample '' + with config.lib.kernelConfig; [ + (isYes "MODULES") + (isEnabled "FB_CON_DECOR") + (isEnabled "BLK_DEV_INITRD") + ] + ''; + internal = true; + type = types.listOf types.attrs; + description = '' + This option allows modules to specify the kernel config options that + must be set (or unset) for the module to work. Please use the + lib.kernelConfig functions to build list elements. + ''; + }; + + }; + + + ###### implementation + + config = { + + system.build = { inherit kernel; }; + + system.modulesTree = [ kernel ] ++ config.boot.extraModulePackages; + + # Implement consoleLogLevel both in early boot and using sysctl + # (so you don't need to reboot to have changes take effect). + boot.kernelParams = + [ "loglevel=${toString config.boot.consoleLogLevel}" ] ++ + optionals config.boot.vesa [ "vga=0x317" ]; + + boot.kernel.sysctl."kernel.printk" = config.boot.consoleLogLevel; + + boot.kernelModules = [ "loop" ]; + + boot.initrd.availableKernelModules = + [ # Note: most of these (especially the SATA/PATA modules) + # shouldn't be included by default since nixos-hardware-scan + # detects them, but I'm keeping them for now for backwards + # compatibility. + + # Some SATA/PATA stuff. + "ahci" + "sata_nv" + "sata_via" + "sata_sis" + "sata_uli" + "ata_piix" + "pata_marvell" + + # Standard SCSI stuff. + "sd_mod" + "sr_mod" + + # Standard IDE stuff. + "ide_cd" + "ide_disk" + "ide_generic" + + # Support USB keyboards, in case the boot fails and we only have + # a USB keyboard. + "uhci_hcd" + "ehci_hcd" + "ehci_pci" + "ohci_hcd" + "xhci_hcd" + "usbhid" + "hid_generic" + + # Unix domain sockets (needed by udev). + "unix" + + # Misc. stuff. + "pcips2" "xtkbd" + + # To wait for SCSI devices to appear. + "scsi_wait_scan" + ]; + + boot.initrd.kernelModules = + [ # For LVM. + "dm_mod" + ]; + + # The Linux kernel >= 2.6.27 provides firmware. + hardware.firmware = [ "${kernel}/lib/firmware" ]; + + # Create /etc/modules-load.d/nixos.conf, which is read by + # systemd-modules-load.service to load required kernel modules. + # FIXME: ensure that systemd-modules-load.service is restarted if + # this file changes. + environment.etc = singleton + { target = "modules-load.d/nixos.conf"; + source = kernelModulesConf; + }; + + # Sigh. This overrides systemd's systemd-modules-load.service + # just so we can set a restart trigger. Also make + # multi-user.target pull it in so that it gets started if it + # failed earlier. + systemd.services."systemd-modules-load" = + { description = "Load Kernel Modules"; + wantedBy = [ "sysinit.target" "multi-user.target" ]; + before = [ "sysinit.target" "shutdown.target" ]; + unitConfig = + { DefaultDependencies = "no"; + Conflicts = "shutdown.target"; + }; + serviceConfig = + { Type = "oneshot"; + RemainAfterExit = true; + ExecStart = "${config.systemd.package}/lib/systemd/systemd-modules-load"; + # Ignore failed module loads. Typically some of the + # modules in ‘boot.kernelModules’ are "nice to have but + # not required" (e.g. acpi-cpufreq), so we don't want to + # barf on those. + SuccessExitStatus = "0 1"; + }; + restartTriggers = [ kernelModulesConf ]; + }; + + lib.kernelConfig = { + isYes = option: { + assertion = config: config.isYes option; + message = "CONFIG_${option} is not yes!"; + configLine = "CONFIG_${option}=y"; + }; + + isNo = option: { + assertion = config: config.isNo option; + message = "CONFIG_${option} is not no!"; + configLine = "CONFIG_${option}=n"; + }; + + isModule = option: { + assertion = config: config.isModule option; + message = "CONFIG_${option} is not built as a module!"; + configLine = "CONFIG_${option}=m"; + }; + + ### Usually you will just want to use these two + # True if yes or module + isEnabled = option: { + assertion = config: config.isEnabled option; + message = "CONFIG_${option} is not enabled!"; + configLine = "CONFIG_${option}=y"; + }; + + # True if no or omitted + isDisabled = option: { + assertion = config: config.isDisabled option; + message = "CONFIG_${option} is not disabled!"; + configLine = "CONFIG_${option}=n"; + }; + }; + + # The config options that all modules can depend upon + system.requiredKernelConfig = with config.lib.kernelConfig; [ + # !!! Should this really be needed? + (isYes "MODULES") + (isYes "BINFMT_ELF") + ]; + + # nixpkgs kernels are assumed to have all required features + assertions = if config.boot.kernelPackages.kernel ? features then [] else + let cfg = config.boot.kernelPackages.kernel.config; in map (attrs: + { assertion = attrs.assertion cfg; inherit (attrs) message; } + ) config.system.requiredKernelConfig; + + }; + +} diff --git a/nixos/modules/system/boot/kexec.nix b/nixos/modules/system/boot/kexec.nix new file mode 100644 index 00000000000..b7821f9509f --- /dev/null +++ b/nixos/modules/system/boot/kexec.nix @@ -0,0 +1,21 @@ +{ config, pkgs, ... }: + +{ + environment.systemPackages = [ pkgs.kexectools ]; + + systemd.services."prepare-kexec" = + { description = "Preparation for kexec"; + wantedBy = [ "kexec.target" ]; + before = [ "systemd-kexec.service" ]; + unitConfig.DefaultDependencies = false; + serviceConfig.Type = "oneshot"; + path = [ pkgs.kexectools ]; + script = + '' + p=$(readlink -f /nix/var/nix/profiles/system) + if ! [ -d $p ]; then exit 1; fi + exec kexec --load $p/kernel --initrd=$p/initrd --append="$(cat $p/kernel-params) init=$p/init" + ''; + }; + +} \ No newline at end of file diff --git a/nixos/modules/system/boot/loader/efi-boot-stub/efi-boot-stub-builder.sh b/nixos/modules/system/boot/loader/efi-boot-stub/efi-boot-stub-builder.sh new file mode 100644 index 00000000000..2f550c98428 --- /dev/null +++ b/nixos/modules/system/boot/loader/efi-boot-stub/efi-boot-stub-builder.sh @@ -0,0 +1,131 @@ +#! @bash@/bin/sh -e + +shopt -s nullglob + +export PATH=/empty +for i in @path@; do PATH=$PATH:$i/bin:$i/sbin; done + +default=$1 +if test -z "$1"; then + echo "Syntax: efi-boot-stub-builder.sh <DEFAULT-CONFIG>" + exit 1 +fi + +echo "updating the efi system partition..." + +# Convert a path to a file in the Nix store such as +# /nix/store/<hash>-<name>/file to <hash>-<name>-<file>. +# Also, efi executables need the .efi extension +cleanName() { + local path="$1" + echo "$path" | sed 's|^/nix/store/||' | sed 's|/|-|g' | sed 's|@kernelFile@$|@kernelFile@.efi|' +} + +# Copy a file from the Nix store to the EFI system partition +declare -A filesCopied + +copyToKernelsDir() { + local src="$1" + local dst="@efiSysMountPoint@/efi/nixos/$(cleanName $src)" + # Don't copy the file if $dst already exists. This means that we + # have to create $dst atomically to prevent partially copied + # kernels or initrd if this script is ever interrupted. + if ! test -e $dst; then + local dstTmp=$dst.tmp.$$ + cp $src $dstTmp + mv $dstTmp $dst + fi + filesCopied[$dst]=1 + result=$dst +} + +# Copy its kernel, initrd, and startup script to the efi system partition +# Add the efibootmgr entry if requested +addEntry() { + local path="$1" + local generation="$2" + + if ! test -e $path/kernel -a -e $path/initrd; then + return + fi + + local kernel=$(readlink -f $path/kernel) + local initrd=$(readlink -f $path/initrd) + copyToKernelsDir $kernel; kernel=$result + copyToKernelsDir $initrd; initrd=$result + + local startup="@efiSysMountPoint@/efi/nixos/generation-$generation-startup.nsh" + if ! test -e $startup; then + local dstTmp=$startup.tmp.$$ + echo "$(echo $kernel | sed 's|@efiSysMountPoint@||' | sed 's|/|\\|g') systemConfig=$(readlink -f $path) init=$(readlink -f $path/init) initrd=$(echo $initrd | sed 's|@efiSysMountPoint@||' | sed 's|/|\\|g') $(cat $path/kernel-params)" > $dstTmp + mv $dstTmp $startup + fi + filesCopied[$startup]=1 + + if test -n "@runEfibootmgr@"; then + set +e + efibootmgr -c -d "@efiDisk@" -g -l $(echo $kernel | sed 's|@efiSysMountPoint@||' | sed 's|/|\\|g') -L "NixOS $generation Generation" -p "@efiPartition@" \ + -u systemConfig=$(readlink -f $path) init=$(readlink -f $path/init) initrd=$(echo $initrd | sed 's|@efiSysMountPoint@||' | sed 's|/|\\|g') $(cat $path/kernel-params) > /dev/null 2>&1 + set -e + fi + + if test $(readlink -f "$path") = "$default"; then + if test -n "@runEfibootmgr@"; then + set +e + defaultbootnum=$(efibootmgr | grep "NixOS $generation Generation" | sed 's/Boot//' | sed 's/\*.*//') + set -e + fi + + if test -n "@installStartupNsh@"; then + sed 's|.*@kernelFile@.efi|@kernelFile@.efi|' < $startup > "@efiSysMountPoint@/startup.nsh" + cp $kernel "@efiSysMountPoint@/@kernelFile@.efi" + fi + fi +} + +mkdir -p "@efiSysMountPoint@/efi/nixos/" + +# Remove all old boot manager entries +if test -n "@runEfibootmgr@"; then + set +e + modprobe efivars > /dev/null 2>&1 + for bootnum in $(efibootmgr | grep "NixOS" | grep "Generation" | sed 's/Boot//' | sed 's/\*.*//'); do + efibootmgr -B -b "$bootnum" > /dev/null 2>&1 + done + set -e +fi + +# Add all generations of the system profile to the system partition, in reverse +# (most recent to least recent) order. +for generation in $( + (cd /nix/var/nix/profiles && ls -d system-*-link) \ + | sed 's/system-\([0-9]\+\)-link/\1/' \ + | sort -n -r); do + link=/nix/var/nix/profiles/system-$generation-link + addEntry $link $generation +done + +if test -n "@runEfibootmgr@"; then + set +e + efibootmgr -o $defaultbootnum > /dev/null 2>&1 + set -e +fi + +if test -n "@efiShell@"; then + mkdir -pv "@efiSysMountPoint@"/efi/boot + cp "@efiShell@" "@efiSysMountPoint@"/efi/boot/boot"@targetArch@".efi +fi + +# Remove obsolete files from the EFI system partition +for fn in "@efiSysMountPoint@/efi/nixos/"*; do + if ! test "${filesCopied[$fn]}" = 1; then + rm -vf -- "$fn" + fi +done + +# Run any extra commands users may need +if test -n "@runEfibootmgr@"; then + set +e + @postEfiBootMgrCommands@ + set -e +fi diff --git a/nixos/modules/system/boot/loader/efi-boot-stub/efi-boot-stub.nix b/nixos/modules/system/boot/loader/efi-boot-stub/efi-boot-stub.nix new file mode 100644 index 00000000000..735784327bc --- /dev/null +++ b/nixos/modules/system/boot/loader/efi-boot-stub/efi-boot-stub.nix @@ -0,0 +1,98 @@ +{pkgs, config, ...}: + +with pkgs.lib; + +let + efiBootStubBuilder = pkgs.substituteAll { + src = ./efi-boot-stub-builder.sh; + isExecutable = true; + inherit (pkgs) bash; + path = [pkgs.coreutils pkgs.gnused pkgs.gnugrep pkgs.glibc] ++ (pkgs.stdenv.lib.optionals config.boot.loader.efi.canTouchEfiVariables [pkgs.efibootmgr pkgs.module_init_tools]); + inherit (config.boot.loader.efiBootStub) installStartupNsh; + + inherit (config.boot.loader.efi) efiSysMountPoint; + + inherit (config.boot.loader.efi.efibootmgr) efiDisk efiPartition postEfiBootMgrCommands; + + runEfibootmgr = config.boot.loader.efi.canTouchEfiVariables; + + efiShell = if config.boot.loader.efiBootStub.installShell then + if pkgs.stdenv.isi686 then + pkgs.fetchurl { + url = "https://edk2.svn.sourceforge.net/svnroot/edk2/trunk/edk2/EdkShellBinPkg/FullShell/Ia32/Shell_Full.efi"; + sha256 = "1gv6kyaspczdp7x8qnx5x76ilriaygkfs99ay7ihhdi6riclkhfl"; + } + else + pkgs.fetchurl { + url = "https://edk2.svn.sourceforge.net/svnroot/edk2/trunk/edk2/EdkShellBinPkg/FullShell/X64/Shell_Full.efi"; + sha256 = "1g18z84rlavxr5gsrh2g942rfr6znv9fs3fqww5m7dhmnysgyv8p"; + } + else + null; + + kernelFile = platform.kernelTarget; + targetArch = if pkgs.stdenv.isi686 then + "IA32" + else if pkgs.stdenv.isx86_64 then + "X64" + else + throw "Unsupported architecture"; + }; + + # Temporary check, for nixos to cope both with nixpkgs stdenv-updates and trunk + platform = pkgs.stdenv.platform; +in +{ + options = { + boot = { + loader = { + efiBootStub = { + + enable = mkOption { + default = false; + description = '' + Whether to use the linux kernel as an EFI bootloader. + When enabled, the kernel, initrd, and an EFI shell script + to boot the system are copied to the EFI system partition. + ''; + }; + + installStartupNsh = mkOption { + default = false; + description = '' + Whether to install a startup.nsh in the root of the EFI system partition. + For now, it will just boot the latest version when run, the eventual goal + is to have a basic menu-type interface. + ''; + }; + + installShell = mkOption { + default = false; + description = '' + Whether to install an EFI shell in \EFI\BOOT. + This _should_ only be needed for removable devices + (CDs, usb sticks, etc.), but it may be an option for broken + systems where efibootmgr doesn't work. Particularly useful in + conjunction with installStartupNsh + ''; + }; + + }; + }; + }; + }; + + config = mkIf config.boot.loader.efiBootStub.enable { + assertions = [ { assertion = ! config.boot.kernelPackages.kernel ? features || config.boot.kernelPackages.kernel.features ? efiBootStub; message = "This kernel does not support the EFI boot stub"; } ]; + + system = { + build.installBootLoader = efiBootStubBuilder; + boot.loader.id = "efiBootStub"; + boot.loader.kernelFile = platform.kernelTarget; + requiredKernelConfig = with config.lib.kernelConfig; [ + (isYes "EFI_STUB") + ]; + }; + }; + +} diff --git a/nixos/modules/system/boot/loader/efi.nix b/nixos/modules/system/boot/loader/efi.nix new file mode 100644 index 00000000000..827b3e39122 --- /dev/null +++ b/nixos/modules/system/boot/loader/efi.nix @@ -0,0 +1,49 @@ +{ pkgs, ... }: + +with pkgs.lib; + +{ + options.boot.loader.efi = { + canTouchEfiVariables = mkOption { + default = false; + + type = types.bool; + + description = "Whether or not the installation process should modify efi boot variables."; + }; + + efibootmgr = { + efiDisk = mkOption { + default = "/dev/sda"; + + type = types.string; + + description = "The disk that contains the EFI system partition."; + }; + + efiPartition = mkOption { + default = "1"; + description = "The partition number of the EFI system partition."; + }; + + postEfiBootMgrCommands = mkOption { + default = ""; + type = types.string; + description = '' + Shell commands to be executed immediately after efibootmgr has setup the system EFI. + Some systems do not follow the EFI specifications properly and insert extra entries. + Others will brick (fix by removing battery) on boot when it finds more than X entries. + This hook allows for running a few extra efibootmgr commands to combat these issues. + ''; + }; + }; + + efiSysMountPoint = mkOption { + default = "/boot"; + + type = types.string; + + description = "Where the EFI System Partition is mounted."; + }; + }; +} diff --git a/nixos/modules/system/boot/loader/generations-dir/generations-dir-builder.sh b/nixos/modules/system/boot/loader/generations-dir/generations-dir-builder.sh new file mode 100644 index 00000000000..e723b9eb7cb --- /dev/null +++ b/nixos/modules/system/boot/loader/generations-dir/generations-dir-builder.sh @@ -0,0 +1,106 @@ +#! @bash@/bin/sh -e + +shopt -s nullglob + +export PATH=/empty +for i in @path@; do PATH=$PATH:$i/bin; done + +default=$1 +if test -z "$1"; then + echo "Syntax: generations-dir-builder.sh <DEFAULT-CONFIG>" + exit 1 +fi + +echo "updating the boot generations directory..." + +mkdir -p /boot + +rm -Rf /boot/system* || true + +target=/boot/grub/menu.lst +tmp=$target.tmp + +# Convert a path to a file in the Nix store such as +# /nix/store/<hash>-<name>/file to <hash>-<name>-<file>. +cleanName() { + local path="$1" + echo "$path" | sed 's|^/nix/store/||' | sed 's|/|-|g' +} + +# Copy a file from the Nix store to /boot/kernels. +declare -A filesCopied + +copyToKernelsDir() { + local src="$1" + local dst="/boot/kernels/$(cleanName $src)" + # Don't copy the file if $dst already exists. This means that we + # have to create $dst atomically to prevent partially copied + # kernels or initrd if this script is ever interrupted. + if ! test -e $dst; then + local dstTmp=$dst.tmp.$$ + cp $src $dstTmp + mv $dstTmp $dst + fi + filesCopied[$dst]=1 + result=$dst +} + + +# Copy its kernel and initrd to /boot/kernels. +addEntry() { + local path="$1" + local generation="$2" + local outdir=/boot/system-$generation + + if ! test -e $path/kernel -a -e $path/initrd; then + return + fi + + local kernel=$(readlink -f $path/kernel) + local initrd=$(readlink -f $path/initrd) + + if test -n "@copyKernels@"; then + copyToKernelsDir $kernel; kernel=$result + copyToKernelsDir $initrd; initrd=$result + fi + + mkdir -p $outdir + ln -sf $(readlink -f $path) $outdir/system + ln -sf $(readlink -f $path/init) $outdir/init + ln -sf $initrd $outdir/initrd + ln -sf $kernel $outdir/kernel + + if test $(readlink -f "$path") = "$default"; then + cp "$kernel" /boot/nixos-kernel + cp "$initrd" /boot/nixos-initrd + cp "$(readlink -f "$path/init")" /boot/nixos-init + + mkdir -p /boot/default + # ln -sfT: overrides target even if it exists. + ln -sfT $(readlink -f $path) /boot/default/system + ln -sfT $(readlink -f $path/init) /boot/default/init + ln -sfT $initrd /boot/default/initrd + ln -sfT $kernel /boot/default/kernel + fi +} + +if test -n "@copyKernels@"; then + mkdir -p /boot/kernels +fi + +# Add all generations of the system profile to the menu, in reverse +# (most recent to least recent) order. +for generation in $( + (cd /nix/var/nix/profiles && ls -d system-*-link) \ + | sed 's/system-\([0-9]\+\)-link/\1/' \ + | sort -n -r); do + link=/nix/var/nix/profiles/system-$generation-link + addEntry $link $generation +done + +# Remove obsolete files from /boot/kernels. +for fn in /boot/kernels/*; do + if ! test "${filesCopied[$fn]}" = 1; then + rm -vf -- "$fn" + fi +done diff --git a/nixos/modules/system/boot/loader/generations-dir/generations-dir.nix b/nixos/modules/system/boot/loader/generations-dir/generations-dir.nix new file mode 100644 index 00000000000..9855c8c19dd --- /dev/null +++ b/nixos/modules/system/boot/loader/generations-dir/generations-dir.nix @@ -0,0 +1,63 @@ +{ config, pkgs, ... }: + +with pkgs.lib; + +let + + generationsDirBuilder = pkgs.substituteAll { + src = ./generations-dir-builder.sh; + isExecutable = true; + inherit (pkgs) bash; + path = [pkgs.coreutils pkgs.gnused pkgs.gnugrep]; + inherit (config.boot.loader.generationsDir) copyKernels; + }; + + # Temporary check, for nixos to cope both with nixpkgs stdenv-updates and trunk + platform = pkgs.stdenv.platform; + +in + +{ + options = { + + boot.loader.generationsDir = { + + enable = mkOption { + default = false; + description = '' + Whether to create symlinks to the system generations under + <literal>/boot</literal>. When enabled, + <literal>/boot/default/kernel</literal>, + <literal>/boot/default/initrd</literal>, etc., are updated to + point to the current generation's kernel image, initial RAM + disk, and other bootstrap files. + + This optional is not necessary with boot loaders such as GNU GRUB + for which the menu is updated to point to the latest bootstrap + files. However, it is needed for U-Boot on platforms where the + boot command line is stored in flash memory rather than in a + menu file. + ''; + }; + + copyKernels = mkOption { + default = false; + description = " + Whether copy the necessary boot files into /boot, so + /nix/store is not needed by the boot loader. + "; + }; + + }; + + }; + + + config = mkIf config.boot.loader.generationsDir.enable { + + system.build.installBootLoader = generationsDirBuilder; + system.boot.loader.id = "generationsDir"; + system.boot.loader.kernelFile = platform.kernelTarget; + + }; +} diff --git a/nixos/modules/system/boot/loader/grub/grub.nix b/nixos/modules/system/boot/loader/grub/grub.nix new file mode 100644 index 00000000000..8e9f3253f87 --- /dev/null +++ b/nixos/modules/system/boot/loader/grub/grub.nix @@ -0,0 +1,261 @@ +{ config, pkgs, ... }: + +with pkgs.lib; + +let + + cfg = config.boot.loader.grub; + + realGrub = if cfg.version == 1 then pkgs.grub else pkgs.grub2; + + grub = + # Don't include GRUB if we're only generating a GRUB menu (e.g., + # in EC2 instances). + if cfg.devices == ["nodev"] + then null + else realGrub; + + f = x: if x == null then "" else "" + x; + + grubConfig = pkgs.writeText "grub-config.xml" (builtins.toXML + { splashImage = f config.boot.loader.grub.splashImage; + grub = f grub; + shell = "${pkgs.stdenv.shell}"; + fullVersion = (builtins.parseDrvName realGrub.name).version; + inherit (cfg) + version extraConfig extraPerEntryConfig extraEntries + extraEntriesBeforeNixOS extraPrepareConfig configurationLimit copyKernels timeout + default devices; + path = (makeSearchPath "bin" [ + pkgs.coreutils pkgs.gnused pkgs.gnugrep pkgs.findutils pkgs.diffutils + ]) + ":" + (makeSearchPath "sbin" [ + pkgs.mdadm + ]); + }); + +in + +{ + + ###### interface + + options = { + + boot.loader.grub = { + + enable = mkOption { + default = true; + type = types.bool; + description = '' + Whether to enable the GNU GRUB boot loader. + ''; + }; + + version = mkOption { + default = 2; + example = 1; + type = types.int; + description = '' + The version of GRUB to use: <literal>1</literal> for GRUB + Legacy (versions 0.9x), or <literal>2</literal> (the + default) for GRUB 2. + ''; + }; + + device = mkOption { + default = ""; + example = "/dev/hda"; + type = types.uniq types.string; + description = '' + The device on which the GRUB boot loader will be installed. + The special value <literal>nodev</literal> means that a GRUB + boot menu will be generated, but GRUB itself will not + actually be installed. To install GRUB on multiple devices, + use <literal>boot.loader.grub.devices</literal>. + ''; + }; + + devices = mkOption { + default = []; + example = [ "/dev/hda" ]; + type = types.listOf types.string; + description = '' + The devices on which the boot loader, GRUB, will be + installed. Can be used instead of <literal>device</literal> to + install grub into multiple devices (e.g., if as softraid arrays holding /boot). + ''; + }; + + # !!! How can we mark options as obsolete? + bootDevice = mkOption { + default = ""; + description = "Obsolete."; + }; + + configurationName = mkOption { + default = ""; + example = "Stable 2.6.21"; + type = types.uniq types.string; + description = '' + GRUB entry name instead of default. + ''; + }; + + extraPrepareConfig = mkOption { + default = ""; + type = types.lines; + description = '' + Additional bash commands to be run at the script that + prepares the grub menu entries. + ''; + }; + + extraConfig = mkOption { + default = ""; + example = "serial; terminal_output.serial"; + type = types.lines; + description = '' + Additional GRUB commands inserted in the configuration file + just before the menu entries. + ''; + }; + + extraPerEntryConfig = mkOption { + default = ""; + example = "root (hd0)"; + type = types.lines; + description = '' + Additional GRUB commands inserted in the configuration file + at the start of each NixOS menu entry. + ''; + }; + + extraEntries = mkOption { + default = ""; + type = types.lines; + example = '' + # GRUB 1 example (not GRUB 2 compatible) + title Windows + chainloader (hd0,1)+1 + + # GRUB 2 example + menuentry "Windows7" { + title Windows7 + insmod ntfs + set root='(hd1,1)' + chainloader +1 + } + ''; + description = '' + Any additional entries you want added to the GRUB boot menu. + ''; + }; + + extraEntriesBeforeNixOS = mkOption { + default = false; + type = types.bool; + description = '' + Whether extraEntries are included before the default option. + ''; + }; + + extraFiles = mkOption { + default = {}; + example = literalExample '' + { "memtest.bin" = "${pkgs.memtest86plus}/memtest.bin"; } + ''; + description = '' + A set of files to be copied to <filename>/boot</filename>. + Each attribute name denotes the destination file name in + <filename>/boot</filename>, while the corresponding + attribute value specifies the source file. + ''; + }; + + splashImage = mkOption { + default = + if cfg.version == 1 + then pkgs.fetchurl { + url = http://www.gnome-look.org/CONTENT/content-files/36909-soft-tux.xpm.gz; + sha256 = "14kqdx2lfqvh40h6fjjzqgff1mwk74dmbjvmqphi6azzra7z8d59"; + } + # GRUB 1.97 doesn't support gzipped XPMs. + else ./winkler-gnu-blue-640x480.png; + example = null; + description = '' + Background image used for GRUB. It must be a 640x480, + 14-colour image in XPM format, optionally compressed with + <command>gzip</command> or <command>bzip2</command>. Set to + <literal>null</literal> to run GRUB in text mode. + ''; + }; + + configurationLimit = mkOption { + default = 100; + example = 120; + type = types.int; + description = '' + Maximum of configurations in boot menu. GRUB has problems when + there are too many entries. + ''; + }; + + copyKernels = mkOption { + default = false; + type = types.bool; + description = '' + Whether the GRUB menu builder should copy kernels and initial + ramdisks to /boot. This is done automatically if /boot is + on a different partition than /. + ''; + }; + + timeout = mkOption { + default = 5; + type = types.int; + description = '' + Timeout (in seconds) until GRUB boots the default menu item. + ''; + }; + + default = mkOption { + default = 0; + type = types.int; + description = '' + Index of the default menu item to be booted. + ''; + }; + + }; + + }; + + + ###### implementation + + config = mkIf cfg.enable { + + boot.loader.grub.devices = optional (cfg.device != "") cfg.device; + + system.build = mkAssert (cfg.devices != []) + "You must set the ‘boot.loader.grub.device’ option to make the system bootable." + { installBootLoader = + "PERL5LIB=${makePerlPath [ pkgs.perlPackages.XMLLibXML pkgs.perlPackages.XMLSAX ]} " + + "${pkgs.perl}/bin/perl ${./install-grub.pl} ${grubConfig}"; + inherit grub; + }; + + # Common attribute for boot loaders so only one of them can be + # set at once. + system.boot.loader.id = "grub"; + + environment.systemPackages = [ grub ]; + + boot.loader.grub.extraPrepareConfig = + concatStrings (mapAttrsToList (n: v: '' + ${pkgs.coreutils}/bin/cp -pf "${v}" "/boot/${n}" + '') config.boot.loader.grub.extraFiles); + + }; + +} diff --git a/nixos/modules/system/boot/loader/grub/install-grub.pl b/nixos/modules/system/boot/loader/grub/install-grub.pl new file mode 100644 index 00000000000..a83733db63b --- /dev/null +++ b/nixos/modules/system/boot/loader/grub/install-grub.pl @@ -0,0 +1,265 @@ +use strict; +use warnings; +use XML::LibXML; +use File::Basename; +use File::Path; +use File::stat; +use File::Copy; +use POSIX; +use Cwd; + +my $defaultConfig = $ARGV[1] or die; + +my $dom = XML::LibXML->load_xml(location => $ARGV[0]); + +sub get { my ($name) = @_; return $dom->findvalue("/expr/attrs/attr[\@name = '$name']/*/\@value"); } + +sub readFile { + my ($fn) = @_; local $/ = undef; + open FILE, "<$fn" or return undef; my $s = <FILE>; close FILE; + local $/ = "\n"; chomp $s; return $s; +} + +sub writeFile { + my ($fn, $s) = @_; + open FILE, ">$fn" or die "cannot create $fn: $!\n"; + print FILE $s or die; + close FILE or die; +} + +my $grub = get("grub"); +my $grubVersion = int(get("version")); +my $extraConfig = get("extraConfig"); +my $extraPrepareConfig = get("extraPrepareConfig"); +my $extraPerEntryConfig = get("extraPerEntryConfig"); +my $extraEntries = get("extraEntries"); +my $extraEntriesBeforeNixOS = get("extraEntriesBeforeNixOS") eq "true"; +my $splashImage = get("splashImage"); +my $configurationLimit = int(get("configurationLimit")); +my $copyKernels = get("copyKernels") eq "true"; +my $timeout = int(get("timeout")); +my $defaultEntry = int(get("default")); +$ENV{'PATH'} = get("path"); + +die "unsupported GRUB version\n" if $grubVersion != 1 && $grubVersion != 2; + +print STDERR "updating GRUB $grubVersion menu...\n"; + +mkpath("/boot/grub", 0, 0700); + + +# Discover whether /boot is on the same filesystem as / and +# /nix/store. If not, then all kernels and initrds must be copied to +# /boot, and all paths in the GRUB config file must be relative to the +# root of the /boot filesystem. `$bootRoot' is the path to be +# prepended to paths under /boot. +my $bootRoot = "/boot"; +if (stat("/")->dev != stat("/boot")->dev) { + $bootRoot = ""; + $copyKernels = 1; +} elsif (stat("/boot")->dev != stat("/nix/store")->dev) { + $copyKernels = 1; +} + + +# Generate the header. +my $conf .= "# Automatically generated. DO NOT EDIT THIS FILE!\n"; + +if ($grubVersion == 1) { + $conf .= " + default $defaultEntry + timeout $timeout + "; + if ($splashImage) { + copy $splashImage, "/boot/background.xpm.gz" or die "cannot copy $splashImage to /boot\n"; + $conf .= "splashimage $bootRoot/background.xpm.gz\n"; + } +} + +else { + $conf .= " + if [ -s \$prefix/grubenv ]; then + load_env + fi + + # ‘grub-reboot’ sets a one-time saved entry, which we process here and + # then delete. + if [ \"\${saved_entry}\" ]; then + # The next line *has* to look exactly like this, otherwise KDM's + # reboot feature won't work properly with GRUB 2. + set default=\"\${saved_entry}\" + set saved_entry= + set prev_saved_entry= + save_env saved_entry + save_env prev_saved_entry + set timeout=1 + else + set default=$defaultEntry + set timeout=$timeout + fi + + if loadfont $bootRoot/grub/fonts/unicode.pf2; then + set gfxmode=640x480 + insmod gfxterm + insmod vbe + terminal_output gfxterm + fi + "; + + if ($splashImage) { + # FIXME: GRUB 1.97 doesn't resize the background image if it + # doesn't match the video resolution. + copy $splashImage, "/boot/background.png" or die "cannot copy $splashImage to /boot\n"; + $conf .= " + insmod png + if background_image $bootRoot/background.png; then + set color_normal=white/black + set color_highlight=black/white + else + set menu_color_normal=cyan/blue + set menu_color_highlight=white/blue + fi + "; + } +} + +$conf .= "$extraConfig\n"; + + +# Generate the menu entries. +$conf .= "\n"; + +my %copied; +mkpath("/boot/kernels", 0, 0755) if $copyKernels; + +sub copyToKernelsDir { + my ($path) = @_; + return $path unless $copyKernels; + $path =~ /\/nix\/store\/(.*)/ or die; + my $name = $1; $name =~ s/\//-/g; + my $dst = "/boot/kernels/$name"; + # Don't copy the file if $dst already exists. This means that we + # have to create $dst atomically to prevent partially copied + # kernels or initrd if this script is ever interrupted. + if (! -e $dst) { + my $tmp = "$dst.tmp"; + copy $path, $tmp or die "cannot copy $path to $tmp\n"; + rename $tmp, $dst or die "cannot rename $tmp to $dst\n"; + } + $copied{$dst} = 1; + return "$bootRoot/kernels/$name"; +} + +sub addEntry { + my ($name, $path) = @_; + return unless -e "$path/kernel" && -e "$path/initrd"; + + my $kernel = copyToKernelsDir(Cwd::abs_path("$path/kernel")); + my $initrd = copyToKernelsDir(Cwd::abs_path("$path/initrd")); + my $xen = -e "$path/xen.gz" ? copyToKernelsDir(Cwd::abs_path("$path/xen.gz")) : undef; + + # FIXME: $confName + + my $kernelParams = + "systemConfig=" . Cwd::abs_path($path) . " " . + "init=" . Cwd::abs_path("$path/init") . " " . + readFile("$path/kernel-params"); + my $xenParams = $xen && -e "$path/xen-params" ? readFile("$path/xen-params") : ""; + + if ($grubVersion == 1) { + $conf .= "title $name\n"; + $conf .= " $extraPerEntryConfig\n" if $extraPerEntryConfig; + $conf .= " kernel $xen $xenParams\n" if $xen; + $conf .= " " . ($xen ? "module" : "kernel") . " $kernel $kernelParams\n"; + $conf .= " " . ($xen ? "module" : "initrd") . " $initrd\n\n"; + } else { + $conf .= "menuentry \"$name\" {\n"; + $conf .= " $extraPerEntryConfig\n" if $extraPerEntryConfig; + $conf .= " multiboot $xen $xenParams\n" if $xen; + $conf .= " " . ($xen ? "module" : "linux") . " $kernel $kernelParams\n"; + $conf .= " " . ($xen ? "module" : "initrd") . " $initrd\n"; + $conf .= "}\n\n"; + } +} + + +# Add default entries. +$conf .= "$extraEntries\n" if $extraEntriesBeforeNixOS; + +addEntry("NixOS - Default", $defaultConfig); + +$conf .= "$extraEntries\n" unless $extraEntriesBeforeNixOS; + +# extraEntries could refer to @bootRoot@, which we have to substitute +$conf =~ s/\@bootRoot\@/$bootRoot/g; + +# Emit submenus for all system profiles. +sub addProfile { + my ($profile, $description) = @_; + + # Add entries for all generations of this profile. + $conf .= "submenu \"$description\" {\n" if $grubVersion == 2; + + sub nrFromGen { my ($x) = @_; $x =~ /\/\w+-(\d+)-link/; return $1; } + + my @links = sort + { nrFromGen($b) <=> nrFromGen($a) } + (glob "$profile-*-link"); + + my $curEntry = 0; + foreach my $link (@links) { + last if $curEntry++ >= $configurationLimit; + my $date = strftime("%F", localtime(lstat($link)->mtime)); + my $version = + -e "$link/nixos-version" + ? readFile("$link/nixos-version") + : basename((glob(dirname(Cwd::abs_path("$link/kernel")) . "/lib/modules/*"))[0]); + addEntry("NixOS - Configuration " . nrFromGen($link) . " ($date - $version)", $link); + } + + $conf .= "}\n" if $grubVersion == 2; +} + +addProfile "/nix/var/nix/profiles/system", "NixOS - All configurations"; + +if ($grubVersion == 2) { + for my $profile (glob "/nix/var/nix/profiles/system-profiles/*") { + my $name = basename($profile); + next unless $name =~ /^\w+$/; + addProfile $profile, "NixOS - Profile '$name'"; + } +} + +# Run extraPrepareConfig in sh +if ($extraPrepareConfig ne "") { + system((get("shell"), "-c", $extraPrepareConfig)); +} + +# Atomically update the GRUB config. +my $confFile = $grubVersion == 1 ? "/boot/grub/menu.lst" : "/boot/grub/grub.cfg"; +my $tmpFile = $confFile . ".tmp"; +writeFile($tmpFile, $conf); +rename $tmpFile, $confFile or die "cannot rename $tmpFile to $confFile\n"; + + +# Remove obsolete files from /boot/kernels. +foreach my $fn (glob "/boot/kernels/*") { + next if defined $copied{$fn}; + print STDERR "removing obsolete file $fn\n"; + unlink $fn; +} + + +# Install GRUB if the version changed from the last time we installed +# it. FIXME: shouldn't we reinstall if ‘devices’ changed? +my $prevVersion = readFile("/boot/grub/version") // ""; +if (($ENV{'NIXOS_INSTALL_GRUB'} // "") eq "1" || get("fullVersion") ne $prevVersion) { + foreach my $dev ($dom->findnodes('/expr/attrs/attr[@name = "devices"]/list/string/@value')) { + $dev = $dev->findvalue(".") or die; + next if $dev eq "nodev"; + print STDERR "installing the GRUB $grubVersion boot loader on $dev...\n"; + system("$grub/sbin/grub-install", "--recheck", Cwd::abs_path($dev)) == 0 + or die "$0: installation of GRUB on $dev failed\n"; + } + writeFile("/boot/grub/version", get("fullVersion")); +} diff --git a/nixos/modules/system/boot/loader/grub/memtest.nix b/nixos/modules/system/boot/loader/grub/memtest.nix new file mode 100644 index 00000000000..a0726c01e20 --- /dev/null +++ b/nixos/modules/system/boot/loader/grub/memtest.nix @@ -0,0 +1,39 @@ +# This module adds Memtest86+ to the GRUB boot menu. + +{ config, pkgs, ... }: + +with pkgs.lib; + +let + memtest86 = pkgs.memtest86plus; +in + +{ + options = { + + boot.loader.grub.memtest86 = mkOption { + default = false; + type = types.bool; + description = '' + Make Memtest86+, a memory testing program, available from the + GRUB boot menu. + ''; + }; + }; + + config = mkIf config.boot.loader.grub.memtest86 { + + boot.loader.grub.extraEntries = mkFixStrictness ( + if config.boot.loader.grub.version == 2 then + '' + menuentry "Memtest86+" { + linux16 @bootRoot@/memtest.bin + } + '' + else + throw "Memtest86+ is not supported with GRUB 1."); + + boot.loader.grub.extraFiles."memtest.bin" = "${memtest86}/memtest.bin"; + + }; +} diff --git a/nixos/modules/system/boot/loader/grub/winkler-gnu-blue-640x480.png b/nixos/modules/system/boot/loader/grub/winkler-gnu-blue-640x480.png new file mode 100644 index 00000000000..35bbb57b51e --- /dev/null +++ b/nixos/modules/system/boot/loader/grub/winkler-gnu-blue-640x480.png Binary files differdiff --git a/nixos/modules/system/boot/loader/grub/winkler-gnu-blue.README b/nixos/modules/system/boot/loader/grub/winkler-gnu-blue.README new file mode 100644 index 00000000000..9616362dce2 --- /dev/null +++ b/nixos/modules/system/boot/loader/grub/winkler-gnu-blue.README @@ -0,0 +1,6 @@ +This is a resized version of + + http://www.gnu.org/graphics/winkler-gnu-blue.png + +by Kyle Winkler and released under the Free Art License +(http://artlibre.org/licence.php/lalgb.html). diff --git a/nixos/modules/system/boot/loader/gummiboot/gummiboot-builder.py b/nixos/modules/system/boot/loader/gummiboot/gummiboot-builder.py new file mode 100644 index 00000000000..9ea224b51f6 --- /dev/null +++ b/nixos/modules/system/boot/loader/gummiboot/gummiboot-builder.py @@ -0,0 +1,114 @@ +#! @python@/bin/python +import argparse +import shutil +import os +import errno +import subprocess +import glob +import tempfile +import errno + +def copy_if_not_exists(source, dest): + known_paths.append(dest) + if not os.path.exists(dest): + shutil.copyfile(source, dest) + +system_dir = lambda generation: "/nix/var/nix/profiles/system-%d-link" % (generation) + +def write_entry(generation, kernel, initrd): + entry_file = "@efiSysMountPoint@/loader/entries/nixos-generation-%d.conf" % (generation) + generation_dir = os.readlink(system_dir(generation)) + tmp_path = "%s.tmp" % (entry_file) + kernel_params = "systemConfig=%s init=%s/init " % (generation_dir, generation_dir) + with open("%s/kernel-params" % (generation_dir)) as params_file: + kernel_params = kernel_params + params_file.read() + with open(tmp_path, 'w') as f: + print >> f, "title NixOS" + print >> f, "version Generation %d" % (generation) + if machine_id is not None: print >> f, "machine-id %s" % (machine_id) + print >> f, "linux %s" % (kernel) + print >> f, "initrd %s" % (initrd) + print >> f, "options %s" % (kernel_params) + os.rename(tmp_path, entry_file) + +def write_loader_conf(generation): + with open("@efiSysMountPoint@/loader/loader.conf.tmp", 'w') as f: + if "@timeout@" != "": + print >> f, "timeout @timeout@" + print >> f, "default nixos-generation-%d" % (generation) + os.rename("@efiSysMountPoint@/loader/loader.conf.tmp", "@efiSysMountPoint@/loader/loader.conf") + +def copy_from_profile(generation, name): + store_file_path = os.readlink("%s/%s" % (system_dir(generation), name)) + suffix = os.path.basename(store_file_path) + store_dir = os.path.basename(os.path.dirname(store_file_path)) + efi_file_path = "/efi/nixos/%s-%s.efi" % (store_dir, suffix) + copy_if_not_exists(store_file_path, "@efiSysMountPoint@%s" % (efi_file_path)) + return efi_file_path + +def add_entry(generation): + efi_kernel_path = copy_from_profile(generation, "kernel") + efi_initrd_path = copy_from_profile(generation, "initrd") + write_entry(generation, efi_kernel_path, efi_initrd_path) + +def mkdir_p(path): + try: + os.makedirs(path) + except OSError as e: + if e.errno != errno.EEXIST or not os.path.isdir(path): + raise + +def get_generations(profile): + gen_list = subprocess.check_output([ + "@nix@/bin/nix-env", + "--list-generations", + "-p", + "/nix/var/nix/profiles/%s" % (profile) + ]) + gen_lines = gen_list.split('\n') + gen_lines.pop() + return [ int(line.split()[0]) for line in gen_lines ] + +def remove_old_entries(gens): + slice_start = len("@efiSysMountPoint@/loader/entries/nixos-generation-") + slice_end = -1 * len(".conf") + for path in glob.iglob("@efiSysMountPoint@/loader/entries/nixos-generation-[1-9]*.conf"): + try: + gen = int(path[slice_start:slice_end]) + if not gen in gens: + os.unlink(path) + except ValueError: + pass + for path in glob.iglob("@efiSysMountPoint@/efi/nixos/*"): + if not path in known_paths: + os.unlink(path) + +parser = argparse.ArgumentParser(description='Update NixOS-related gummiboot files') +parser.add_argument('default_config', metavar='DEFAULT-CONFIG', help='The default NixOS config to boot') +args = parser.parse_args() + +# We deserve our own env var! +if os.getenv("NIXOS_INSTALL_GRUB") == "1": + if "@canTouchEfiVariables@" == "1": + subprocess.check_call(["@gummiboot@/bin/gummiboot", "--path=@efiSysMountPoint@", "install"]) + else: + subprocess.check_call(["@gummiboot@/bin/gummiboot", "--path=@efiSysMountPoint@", "--no-variables", "install"]) + +known_paths = [] +mkdir_p("@efiSysMountPoint@/efi/nixos") +mkdir_p("@efiSysMountPoint@/loader/entries") +try: + with open("/etc/machine-id") as machine_file: + machine_id = machine_file.readlines()[0] +except IOError as e: + if e.errno != errno.ENOENT: + raise + machine_id = None + +gens = get_generations("system") +for gen in gens: + add_entry(gen) + if os.readlink(system_dir(gen)) == args.default_config: + write_loader_conf(gen) + +remove_old_entries(gens) diff --git a/nixos/modules/system/boot/loader/gummiboot/gummiboot.nix b/nixos/modules/system/boot/loader/gummiboot/gummiboot.nix new file mode 100644 index 00000000000..9193cd3bc53 --- /dev/null +++ b/nixos/modules/system/boot/loader/gummiboot/gummiboot.nix @@ -0,0 +1,67 @@ +{ config, pkgs, ... }: + +with pkgs.lib; + +let + cfg = config.boot.loader.gummiboot; + + efi = config.boot.loader.efi; + + gummibootBuilder = pkgs.substituteAll { + src = ./gummiboot-builder.py; + + isExecutable = true; + + inherit (pkgs) python gummiboot; + + inherit (config.environment) nix; + + inherit (cfg) timeout; + + inherit (efi) efiSysMountPoint canTouchEfiVariables; + }; +in { + options.boot.loader.gummiboot = { + enable = mkOption { + default = false; + + type = types.bool; + + description = "Whether to enable the gummiboot UEFI boot manager"; + }; + + timeout = mkOption { + default = null; + + example = 4; + + type = types.nullOr types.int; + + description = '' + Timeout (in seconds) for how long to show the menu (null if none). + Note that even with no timeout the menu can be forced if the space + key is pressed during bootup + ''; + }; + }; + + config = mkIf cfg.enable { + assertions = [ + { + assertion = (config.boot.kernelPackages.kernel.features or { efiBootStub = true; }) ? efiBootStub; + + message = "This kernel does not support the EFI boot stub"; + } + ]; + + system = { + build.installBootLoader = gummibootBuilder; + + boot.loader.id = "gummiboot"; + + requiredKernelConfig = with config.lib.kernelConfig; [ + (isYes "EFI_STUB") + ]; + }; + }; +} diff --git a/nixos/modules/system/boot/loader/init-script/init-script-builder.sh b/nixos/modules/system/boot/loader/init-script/init-script-builder.sh new file mode 100644 index 00000000000..502b3b63af2 --- /dev/null +++ b/nixos/modules/system/boot/loader/init-script/init-script-builder.sh @@ -0,0 +1,88 @@ +#! @bash@/bin/sh -e + +shopt -s nullglob + +export PATH=/empty +for i in @path@; do PATH=$PATH:$i/bin; done + +if test $# -ne 1; then + echo "Usage: init-script-builder.sh DEFAULT-CONFIG" + exit 1 +fi + +defaultConfig="$1" + + +[ "$(stat -f -c '%i' /)" = "$(stat -f -c '%i' /boot)" ] || { + # see grub-menu-builder.sh + echo "WARNING: /boot being on a different filesystem not supported by init-script-builder.sh" +} + + + +target="/sbin/init" +targetOther="/boot/init-other-configurations-contents.txt" + +tmp="$target.tmp" +tmpOther="$targetOther.tmp" + + +configurationCounter=0 +numAlienEntries=`cat <<EOF | egrep '^[[:space:]]*title' | wc -l +@extraEntries@ +EOF` + + + + +# Add an entry to $targetOther +addEntry() { + local name="$1" + local path="$2" + local shortSuffix="$3" + + configurationCounter=$((configurationCounter + 1)) + + local stage2=$path/init + + content="$( + echo "#!/bin/sh" + echo "# $name" + echo "# created by init-script-builder.sh" + echo "export systemConfig=$(readlink -f $path)" + echo "exec $stage2" + )" + + [ "$path" != "$defaultConfig" ] || { + echo "$content" > $tmp + echo "# older configurations: $targetOther" >> $tmp + chmod +x $tmp + } + + echo -e "$content\n\n" >> $tmpOther +} + + +mkdir -p /boot /sbin + +addEntry "NixOS - Default" $defaultConfig "" + +# Add all generations of the system profile to the menu, in reverse +# (most recent to least recent) order. +for link in $((ls -d $defaultConfig/fine-tune/* ) | sort -n); do + date=$(stat --printf="%y\n" $link | sed 's/\..*//') + addEntry "NixOS - variation" $link "" +done + +for generation in $( + (cd /nix/var/nix/profiles && ls -d system-*-link) \ + | sed 's/system-\([0-9]\+\)-link/\1/' \ + | sort -n -r); do + link=/nix/var/nix/profiles/system-$generation-link + date=$(stat --printf="%y\n" $link | sed 's/\..*//') + kernelVersion=$(cd $(dirname $(readlink -f $link/kernel))/lib/modules && echo *) + addEntry "NixOS - Configuration $generation ($date - $kernelVersion)" $link "$generation ($date)" +done + +mv $tmpOther $targetOther +mv $tmp $target diff --git a/nixos/modules/system/boot/loader/init-script/init-script.nix b/nixos/modules/system/boot/loader/init-script/init-script.nix new file mode 100644 index 00000000000..4b0fcd85b4b --- /dev/null +++ b/nixos/modules/system/boot/loader/init-script/init-script.nix @@ -0,0 +1,50 @@ +{ config, pkgs, ... }: + +with pkgs.lib; + +let + + initScriptBuilder = pkgs.substituteAll { + src = ./init-script-builder.sh; + isExecutable = true; + inherit (pkgs) bash; + path = [pkgs.coreutils pkgs.gnused pkgs.gnugrep]; + }; + +in + +{ + + ###### interface + + options = { + + boot.loader.initScript = { + + enable = mkOption { + default = false; + description = '' + Some systems require a /sbin/init script which is started. + Or having it makes starting NixOS easier. + This applies to some kind of hosting services and user mode linux. + + Additionally this script will create + /boot/init-other-configurations-contents.txt containing + contents of remaining configurations. You can copy paste them into + /sbin/init manually running a rescue system or such. + ''; + }; + }; + + }; + + + ###### implementation + + config = mkIf config.boot.loader.initScript.enable { + + system.build.installBootLoader = initScriptBuilder; + + }; + +} diff --git a/nixos/modules/system/boot/loader/raspberrypi/builder.sh b/nixos/modules/system/boot/loader/raspberrypi/builder.sh new file mode 100644 index 00000000000..f6ccfe493d8 --- /dev/null +++ b/nixos/modules/system/boot/loader/raspberrypi/builder.sh @@ -0,0 +1,109 @@ +#! @bash@/bin/sh -e + +shopt -s nullglob + +export PATH=/empty +for i in @path@; do PATH=$PATH:$i/bin; done + +default=$1 +if test -z "$1"; then + echo "Syntax: builder.sh <DEFAULT-CONFIG>" + exit 1 +fi + +echo "updating the boot generations directory..." + +mkdir -p /boot/old + +# Convert a path to a file in the Nix store such as +# /nix/store/<hash>-<name>/file to <hash>-<name>-<file>. +cleanName() { + local path="$1" + echo "$path" | sed 's|^/nix/store/||' | sed 's|/|-|g' +} + +# Copy a file from the Nix store to /boot/kernels. +declare -A filesCopied + +copyToKernelsDir() { + local src="$1" + local dst="/boot/old/$(cleanName $src)" + # Don't copy the file if $dst already exists. This means that we + # have to create $dst atomically to prevent partially copied + # kernels or initrd if this script is ever interrupted. + if ! test -e $dst; then + local dstTmp=$dst.tmp.$$ + cp $src $dstTmp + mv $dstTmp $dst + fi + filesCopied[$dst]=1 + result=$dst +} + +copyForced() { + local src="$1" + local dst="$2" + cp $src $dst.tmp + mv $dst.tmp $dst +} + +outdir=/boot/old +mkdir -p $outdir || true + +# Copy its kernel and initrd to /boot/kernels. +addEntry() { + local path="$1" + local generation="$2" + + if ! test -e $path/kernel -a -e $path/initrd; then + return + fi + + local kernel=$(readlink -f $path/kernel) + # local initrd=$(readlink -f $path/initrd) + + if test -n "@copyKernels@"; then + copyToKernelsDir $kernel; kernel=$result + # copyToKernelsDir $initrd; initrd=$result + fi + + echo $(readlink -f $path) > $outdir/$generation-system + echo $(readlink -f $path/init) > $outdir/$generation-init + cp $path/kernel-params $outdir/$generation-cmdline.txt + # echo $initrd > $outdir/$generation-initrd + echo $kernel > $outdir/$generation-kernel + + if test $(readlink -f "$path") = "$default"; then + copyForced $kernel /boot/kernel.img + # copyForced $initrd /boot/initrd + cp "$(readlink -f "$path/init")" /boot/nixos-init + echo "`cat $path/kernel-params` init=$path/init" >/boot/cmdline.txt + + echo "$2" > /boot/defaultgeneration + fi +} + +# Add all generations of the system profile to the menu, in reverse +# (most recent to least recent) order. +for generation in $( + (cd /nix/var/nix/profiles && ls -d system-*-link) \ + | sed 's/system-\([0-9]\+\)-link/\1/' \ + | sort -n -r); do + link=/nix/var/nix/profiles/system-$generation-link + addEntry $link $generation +done + +# Add the firmware files +fwdir=@firmware@/share/raspberrypi/boot/ +copyForced $fwdir/bootcode.bin /boot/bootcode.bin +copyForced $fwdir/fixup.dat /boot/fixup.dat +copyForced $fwdir/fixup_cd.dat /boot/fixup_cd.dat +copyForced $fwdir/start.elf /boot/start.elf +copyForced $fwdir/start_cd.elf /boot/start_cd.elf + +# Remove obsolete files from /boot/old. +for fn in /boot/old/*linux* /boot/old/*initrd*; do + if ! test "${filesCopied[$fn]}" = 1; then + rm -vf -- "$fn" + fi +done diff --git a/nixos/modules/system/boot/loader/raspberrypi/raspberrypi.nix b/nixos/modules/system/boot/loader/raspberrypi/raspberrypi.nix new file mode 100644 index 00000000000..5bc856c3df0 --- /dev/null +++ b/nixos/modules/system/boot/loader/raspberrypi/raspberrypi.nix @@ -0,0 +1,38 @@ +{ config, pkgs, ... }: + +with pkgs.lib; + +let + + builder = pkgs.substituteAll { + src = ./builder.sh; + isExecutable = true; + inherit (pkgs) bash; + path = [pkgs.coreutils pkgs.gnused pkgs.gnugrep]; + firmware = pkgs.raspberrypifw; + }; + + platform = pkgs.stdenv.platform; + +in + +{ + options = { + + boot.loader.raspberryPi.enable = mkOption { + default = false; + description = '' + Whether to create files with the system generations in + <literal>/boot</literal>. + <literal>/boot/old</literal> will hold files from old generations. + ''; + }; + + }; + + config = mkIf config.boot.loader.raspberryPi.enable { + system.build.installBootLoader = builder; + system.boot.loader.id = "raspberrypi"; + system.boot.loader.kernelFile = platform.kernelTarget; + }; +} diff --git a/nixos/modules/system/boot/luksroot.nix b/nixos/modules/system/boot/luksroot.nix new file mode 100644 index 00000000000..29f5eb4fd77 --- /dev/null +++ b/nixos/modules/system/boot/luksroot.nix @@ -0,0 +1,176 @@ +{ config, pkgs, ... }: + +with pkgs.lib; + +let + luks = config.boot.initrd.luks; + + openCommand = { name, device, keyFile, keyFileSize, allowDiscards, ... }: '' + # Wait for luksRoot to appear, e.g. if on a usb drive. + # XXX: copied and adapted from stage-1-init.sh - should be + # available as a function. + if ! test -e ${device}; then + echo -n "waiting 10 seconds for device ${device} to appear..." + for try in $(seq 10); do + sleep 1 + if test -e ${device}; then break; fi + echo -n . + done + echo "ok" + fi + + ${optionalString (keyFile != null) '' + if ! test -e ${keyFile}; then + echo -n "waiting 10 seconds for key file ${keyFile} to appear..." + for try in $(seq 10); do + sleep 1 + if test -e ${keyFile}; then break; fi + echo -n . + done + echo "ok" + fi + ''} + + # open luksRoot and scan for logical volumes + cryptsetup luksOpen ${device} ${name} ${optionalString allowDiscards "--allow-discards"} \ + ${optionalString (keyFile != null) "--key-file=${keyFile} ${optionalString (keyFileSize != null) "--keyfile-size=${toString keyFileSize}"}"} + ''; + + isPreLVM = f: f.preLVM; + preLVM = filter isPreLVM luks.devices; + postLVM = filter (f: !(isPreLVM f)) luks.devices; + +in +{ + + options = { + boot.initrd.luks.enable = mkOption { + default = false; + description = "Obsolete."; + }; + + boot.initrd.luks.mitigateDMAAttacks = mkOption { + default = true; + description = '' + Unless enabled, encryption keys can be easily recovered by an attacker with physical + access to any machine with PCMCIA, ExpressCard, ThunderBolt or FireWire port. + More information: http://en.wikipedia.org/wiki/DMA_attack + + This option blacklists FireWire drivers, but doesn't remove them. You can manually + load the drivers if you need to use a FireWire device, but don't forget to unload them! + ''; + }; + + boot.initrd.luks.cryptoModules = mkOption { + default = + [ "aes" "aes_generic" "blowfish" "twofish" + "serpent" "cbc" "xts" "lrw" "sha1" "sha256" "sha512" + (if pkgs.stdenv.system == "x86_64-linux" then "aes_x86_64" else "aes_i586") + ]; + description = '' + A list of cryptographic kernel modules needed to decrypt the root device(s). + The default includes all common modules. + ''; + }; + + boot.initrd.luks.devices = mkOption { + default = [ ]; + example = [ { name = "luksroot"; device = "/dev/sda3"; preLVM = true; } ]; + description = '' + The list of devices that should be decrypted using LUKS before trying to mount the + root partition. This works for both LVM-over-LUKS and LUKS-over-LVM setups. + + The devices are decrypted to the device mapper names defined. + + Make sure that initrd has the crypto modules needed for decryption. + ''; + + type = types.listOf types.optionSet; + + options = { + + name = mkOption { + example = "luksroot"; + type = types.string; + description = "Named to be used for the generated device in /dev/mapper."; + }; + + device = mkOption { + example = "/dev/sda2"; + type = types.string; + description = "Path of the underlying block device."; + }; + + keyFile = mkOption { + default = null; + example = "/dev/sdb1"; + type = types.nullOr types.string; + description = '' + The name of the file (can be a raw device or a partition) that + should be used as the decryption key for the encrypted device. If + not specified, you will be prompted for a passphrase instead. + ''; + }; + + keyFileSize = mkOption { + default = null; + example = 4096; + type = types.nullOr types.int; + description = '' + The size of the key file. Use this if only the beginning of the + key file should be used as a key (often the case if a raw device + or partition is used as key file). If not specified, the whole + <literal>keyFile</literal> will be used decryption, instead of just + the first <literal>keyFileSize</literal> bytes. + ''; + }; + + preLVM = mkOption { + default = true; + type = types.bool; + description = "Whether the luksOpen will be attempted before LVM scan or after it."; + }; + + allowDiscards = mkOption { + default = false; + type = types.bool; + description = '' + Whether to allow TRIM requests to the underlying device. This option + has security implications, please read the LUKS documentation before + activating in. + ''; + }; + + }; + }; + }; + + config = mkIf (luks.devices != []) { + + # actually, sbp2 driver is the one enabling the DMA attack, but this needs to be tested + boot.blacklistedKernelModules = optionals luks.mitigateDMAAttacks + ["firewire_ohci" "firewire_core" "firewire_sbp2"]; + + # Some modules that may be needed for mounting anything ciphered + boot.initrd.availableKernelModules = [ "dm_mod" "dm_crypt" "cryptd" ] ++ luks.cryptoModules; + + # copy the cryptsetup binary and it's dependencies + boot.initrd.extraUtilsCommands = '' + cp -pdv ${pkgs.cryptsetup}/sbin/cryptsetup $out/bin + # XXX: do we have a function that does this? + for lib in $(ldd $out/bin/cryptsetup |grep '=>' |grep /nix/store/ |cut -d' ' -f3); do + cp -pdvn $lib $out/lib + cp -pvn $(readlink -f $lib) $out/lib + done + ''; + + boot.initrd.extraUtilsCommandsTest = '' + $out/bin/cryptsetup --version + ''; + + boot.initrd.preLVMCommands = concatMapStrings openCommand preLVM; + boot.initrd.postDeviceCommands = concatMapStrings openCommand postLVM; + + environment.systemPackages = [ pkgs.cryptsetup ]; + }; +} diff --git a/nixos/modules/system/boot/modprobe.nix b/nixos/modules/system/boot/modprobe.nix new file mode 100644 index 00000000000..8b2762e2526 --- /dev/null +++ b/nixos/modules/system/boot/modprobe.nix @@ -0,0 +1,112 @@ +{ config, pkgs, ... }: + +with pkgs.lib; + +{ + + ###### interface + + options = { + + system.sbin.modprobe = mkOption { + internal = true; + default = pkgs.writeTextFile { + name = "modprobe"; + destination = "/sbin/modprobe"; + executable = true; + text = + '' + #! ${pkgs.stdenv.shell} + export MODULE_DIR=/run/current-system/kernel-modules/lib/modules + + # Fall back to the kernel modules used at boot time if the + # modules in the current configuration don't match the + # running kernel. + if [ ! -d "$MODULE_DIR/$(${pkgs.coreutils}/bin/uname -r)" ]; then + MODULE_DIR=/run/booted-system/kernel-modules/lib/modules/ + fi + + exec ${pkgs.kmod}/sbin/modprobe "$@" + ''; + }; + description = '' + Wrapper around modprobe that sets the path to the modules + tree. + ''; + }; + + boot.blacklistedKernelModules = mkOption { + default = []; + example = [ "cirrusfb" "i2c_piix4" ]; + description = '' + List of names of kernel modules that should not be loaded + automatically by the hardware probing code. + ''; + }; + + boot.extraModprobeConfig = mkOption { + default = ""; + example = + '' + options parport_pc io=0x378 irq=7 dma=1 + ''; + description = '' + Any additional configuration to be appended to the generated + <filename>modprobe.conf</filename>. This is typically used to + specify module options. See + <citerefentry><refentrytitle>modprobe.conf</refentrytitle> + <manvolnum>5</manvolnum></citerefentry> for details. + ''; + type = types.lines; + }; + + }; + + + ###### implementation + + config = { + + environment.etc = singleton + { source = pkgs.writeText "modprobe.conf" + '' + ${flip concatMapStrings config.boot.blacklistedKernelModules (name: '' + blacklist ${name} + '')} + ${config.boot.extraModprobeConfig} + ''; + target = "modprobe.d/nixos.conf"; + }; + + environment.systemPackages = [ config.system.sbin.modprobe pkgs.kmod ]; + + boot.blacklistedKernelModules = + [ # This module is for debugging and generates gigantic amounts + # of log output, so it should never be loaded automatically. + "evbug" + + # This module causes ALSA to occassionally select the wrong + # default sound device, and is little more than an annoyance + # on modern machines. + "snd_pcsp" + + # The cirrusfb module prevents X11 from starting. FIXME: + # Ubuntu blacklists all framebuffer devices because they're + # "buggy" and cause suspend problems. Maybe we should too? + "cirrusfb" + ]; + + system.activationScripts.modprobe = + '' + # Allow the kernel to find our wrapped modprobe (which searches + # in the right location in the Nix store for kernel modules). + # We need this when the kernel (or some module) auto-loads a + # module. + echo ${config.system.sbin.modprobe}/sbin/modprobe > /proc/sys/kernel/modprobe + ''; + + environment.variables.MODULE_DIR = "/run/current-system/kernel-modules/lib/modules"; + + }; + +} diff --git a/nixos/modules/system/boot/readonly-mountpoint.c b/nixos/modules/system/boot/readonly-mountpoint.c new file mode 100644 index 00000000000..27b66687382 --- /dev/null +++ b/nixos/modules/system/boot/readonly-mountpoint.c @@ -0,0 +1,20 @@ +#include <sys/statvfs.h> +#include <stdio.h> +#include <stdlib.h> + +int main(int argc, char ** argv) { + struct statvfs stat; + if (argc != 2) { + fprintf(stderr, "Usage: %s PATH", argv[0]); + exit(2); + } + if (statvfs(argv[1], &stat) != 0) { + perror("statvfs"); + exit(3); + } + if (stat.f_flag & ST_RDONLY) + exit(0); + else + exit(1); +} + diff --git a/nixos/modules/system/boot/shutdown.nix b/nixos/modules/system/boot/shutdown.nix new file mode 100644 index 00000000000..ad71a2e816e --- /dev/null +++ b/nixos/modules/system/boot/shutdown.nix @@ -0,0 +1,27 @@ +{ config, pkgs, ... }: + +with pkgs.lib; + +{ + + # This unit saves the value of the system clock to the hardware + # clock on shutdown. + systemd.units."save-hwclock.service" = + { wantedBy = [ "shutdown.target" ]; + + text = + '' + [Unit] + Description=Save Hardware Clock + DefaultDependencies=no + Before=shutdown.target + + [Service] + Type=oneshot + ExecStart=${pkgs.utillinux}/sbin/hwclock --systohc ${if config.time.hardwareClockInLocalTime then "--localtime" else "--utc"} + ''; + }; + + boot.kernel.sysctl."kernel.poweroff_cmd" = "${config.systemd.package}/sbin/poweroff"; + +} diff --git a/nixos/modules/system/boot/stage-1-init.sh b/nixos/modules/system/boot/stage-1-init.sh new file mode 100644 index 00000000000..e3e07c08580 --- /dev/null +++ b/nixos/modules/system/boot/stage-1-init.sh @@ -0,0 +1,374 @@ +#! @shell@ + +targetRoot=/mnt-root +console=tty1 + +export LD_LIBRARY_PATH=@extraUtils@/lib +export PATH=@extraUtils@/bin:@extraUtils@/sbin + + +fail() { + if [ -n "$panicOnFail" ]; then exit 1; fi + + # If starting stage 2 failed, allow the user to repair the problem + # in an interactive shell. + cat <<EOF + +An error occured in stage 1 of the boot process, which must mount the +root filesystem on \`$targetRoot' and then start stage 2. Press one +of the following keys: + +EOF + if [ -n "$allowShell" ]; then cat <<EOF + i) to launch an interactive shell + f) to start an interactive shell having pid 1 (needed if you want to + start stage 2's init manually) +EOF + fi + cat <<EOF + r) to reboot immediately + *) to ignore the error and continue +EOF + + read reply + + if [ -n "$allowShell" -a "$reply" = f ]; then + exec setsid @shell@ -c "@shell@ < /dev/$console >/dev/$console 2>/dev/$console" + elif [ -n "$allowShell" -a "$reply" = i ]; then + echo "Starting interactive shell..." + setsid @shell@ -c "@shell@ < /dev/$console >/dev/$console 2>/dev/$console" || fail + elif [ "$reply" = r ]; then + echo "Rebooting..." + reboot -f + else + echo "Continuing..." + fi +} + +trap 'fail' 0 + + +# Print a greeting. +echo +echo "[1;32m<<< NixOS Stage 1 >>>[0m" +echo + + +# Mount special file systems. +mkdir -p /etc +touch /etc/fstab # to shut up mount +touch /etc/mtab # to shut up mke2fs +mkdir -p /proc +mount -t proc none /proc +mkdir -p /sys +mount -t sysfs none /sys +mount -t devtmpfs -o "size=@devSize@" none /dev +mkdir -p /run +mount -t tmpfs -o "mode=0755,size=@runSize@" none /run + + +# Process the kernel command line. +export stage2Init=/init +for o in $(cat /proc/cmdline); do + case $o in + console=*) + set -- $(IFS==; echo $o) + params=$2 + set -- $(IFS=,; echo $params) + console=$1 + ;; + init=*) + set -- $(IFS==; echo $o) + stage2Init=$2 + ;; + boot.trace|debugtrace) + # Show each command. + set -x + ;; + boot.shell_on_fail) + allowShell=1 + ;; + boot.debug1|debug1) # stop right away + allowShell=1 + fail + ;; + boot.debug1devices) # stop after loading modules and creating device nodes + allowShell=1 + debug1devices=1 + ;; + boot.debug1mounts) # stop after mounting file systems + allowShell=1 + debug1mounts=1 + ;; + boot.panic_on_fail|stage1panic=1) + panicOnFail=1 + ;; + root=*) + # If a root device is specified on the kernel command + # line, make it available through the symlink /dev/root. + # Recognise LABEL= and UUID= to support UNetbootin. + set -- $(IFS==; echo $o) + if [ $2 = "LABEL" ]; then + root="/dev/disk/by-label/$3" + elif [ $2 = "UUID" ]; then + root="/dev/disk/by-uuid/$3" + else + root=$2 + fi + ln -s "$root" /dev/root + ;; + esac +done + + +# Load the required kernel modules. +mkdir -p /lib +ln -s @modulesClosure@/lib/modules /lib/modules +echo @extraUtils@/bin/modprobe > /proc/sys/kernel/modprobe +for i in @kernelModules@; do + echo "loading module $(basename $i)..." + modprobe $i || true +done + + +# Create device nodes in /dev. +echo "running udev..." +mkdir -p /etc/udev +ln -sfn @udevRules@ /etc/udev/rules.d +mkdir -p /dev/.mdadm +systemd-udevd --daemon +udevadm trigger --action=add +udevadm settle || true +modprobe scsi_wait_scan || true +udevadm settle || true + + +# Load boot-time keymap before any LVM/LUKS initialization +@extraUtils@/bin/busybox loadkmap < "@busyboxKeymap@" + + +# XXX: Use case usb->lvm will still fail, usb->luks->lvm is covered +@preLVMCommands@ + + +echo "starting device mapper and LVM..." +lvm vgchange -ay + +if test -n "$debug1devices"; then fail; fi + + +@postDeviceCommands@ + + +# Try to resume - all modules are loaded now, and devices exist +if test -e /sys/power/tuxonice/resume; then + if test -n "$(cat /sys/power/tuxonice/resume)"; then + echo 0 > /sys/power/tuxonice/user_interface/enabled + echo 1 > /sys/power/tuxonice/do_resume || echo "failed to resume..." + fi +fi + +if test -e /sys/power/resume -a -e /sys/power/disk; then + echo "@resumeDevice@" > /sys/power/resume 2> /dev/null || echo "failed to resume..." + echo shutdown > /sys/power/disk +fi + + +# Return true if the machine is on AC power, or if we can't determine +# whether it's on AC power. +onACPower() { + ! test -d "/proc/acpi/battery" || + ! ls /proc/acpi/battery/BAT[0-9]* > /dev/null 2>&1 || + ! cat /proc/acpi/battery/BAT*/state | grep "^charging state" | grep -q "discharg" +} + + +# Check the specified file system, if appropriate. +checkFS() { + local device="$1" + local fsType="$2" + + # Only check block devices. + if [ ! -b "$device" ]; then return 0; fi + + # Don't check ROM filesystems. + if [ "$fsType" = iso9660 -o "$fsType" = udf ]; then return 0; fi + + # If we couldn't figure out the FS type, then skip fsck. + if [ "$fsType" = auto ]; then + echo 'cannot check filesystem with type "auto"!' + return 0 + fi + + # Optionally, skip fsck on journaling filesystems. This option is + # a hack - it's mostly because e2fsck on ext3 takes much longer to + # recover the journal than the ext3 implementation in the kernel + # does (minutes versus seconds). + if test -z "@checkJournalingFS@" -a \ + \( "$fsType" = ext3 -o "$fsType" = ext4 -o "$fsType" = reiserfs \ + -o "$fsType" = xfs -o "$fsType" = jfs \) + then + return 0 + fi + + # Don't run `fsck' if the machine is on battery power. !!! Is + # this a good idea? + if ! onACPower; then + echo "on battery power, so no \`fsck' will be performed on \`$device'" + return 0 + fi + + echo "checking $device..." + + fsckFlags= + if test "$fsType" != "btrfs"; then + fsckFlags="-V -a" + fi + fsck $fsckFlags "$device" + fsckResult=$? + + if test $(($fsckResult | 2)) = $fsckResult; then + echo "fsck finished, rebooting..." + sleep 3 + reboot -f + fi + + if test $(($fsckResult | 4)) = $fsckResult; then + echo "$device has unrepaired errors, please fix them manually." + fail + fi + + if test $fsckResult -ge 8; then + echo "fsck on $device failed." + fail + fi + + return 0 +} + + +# Function for mounting a file system. +mountFS() { + local device="$1" + local mountPoint="$2" + local options="$3" + local fsType="$4" + + if [ "$fsType" = auto ]; then + fsType=$(blkid -o value -s TYPE "$device") + if [ -z "$fsType" ]; then fsType=auto; fi + fi + + echo "$device /mnt-root$mountPoint $fsType $options" >> /etc/fstab + + checkFS "$device" "$fsType" + + echo "mounting $device on $mountPoint..." + + mkdir -p "/mnt-root$mountPoint" || true + + # For CIFS mounts, retry a few times before giving up. + local n=0 + while true; do + mount "/mnt-root$mountPoint" && break + if [ "$fsType" != cifs -o "$n" -ge 10 ]; then fail; break; fi + echo "retrying..." + n=$((n + 1)) + done +} + + +# Try to find and mount the root device. +mkdir /mnt-root + +exec 3< @fsInfo@ + +while read -u 3 mountPoint; do + read -u 3 device + read -u 3 fsType + read -u 3 options + + # !!! Really quick hack to support bind mounts, i.e., where the + # "device" should be taken relative to /mnt-root, not /. Assume + # that every device that starts with / but doesn't start with /dev + # is a bind mount. + pseudoDevice= + case $device in + /dev/*) + ;; + //*) + # Don't touch SMB/CIFS paths. + pseudoDevice=1 + ;; + /*) + device=/mnt-root$device + ;; + *) + # Not an absolute path; assume that it's a pseudo-device + # like an NFS path (e.g. "server:/path"). + pseudoDevice=1 + ;; + esac + + # USB storage devices tend to appear with some delay. It would be + # great if we had a way to synchronously wait for them, but + # alas... So just wait for a few seconds for the device to + # appear. If it doesn't appear, try to mount it anyway (and + # probably fail). This is a fallback for non-device "devices" + # that we don't properly recognise. + if test -z "$pseudoDevice" -a ! -e $device; then + echo -n "waiting for device $device to appear..." + for try in $(seq 1 20); do + sleep 1 + if test -e $device; then break; fi + echo -n "." + done + echo + fi + + # Wait once more for the udev queue to empty, just in case it's + # doing something with $device right now. + udevadm settle || true + + mountFS "$device" "$mountPoint" "$options" "$fsType" +done + +exec 3>&- + + +@postMountCommands@ + + +# Stop udevd. +udevadm control --exit || true + +# Kill any remaining processes, just to be sure we're not taking any +# with us into stage 2. unionfs-fuse mounts require the unionfs process. +pkill -9 -v '(1|unionfs)' + + +if test -n "$debug1mounts"; then fail; fi + + +# Restore /proc/sys/kernel/modprobe to its original value. +echo /sbin/modprobe > /proc/sys/kernel/modprobe + + +# Start stage 2. `switch_root' deletes all files in the ramfs on the +# current root. Note that $stage2Init might be an absolute symlink, +# in which case "-e" won't work because we're not in the chroot yet. +if ! test -e "$targetRoot/$stage2Init" -o -L "$targetRoot/$stage2Init"; then + echo "stage 2 init script ($targetRoot/$stage2Init) not found" + fail +fi + +mkdir -m 0755 -p $targetRoot/proc $targetRoot/sys $targetRoot/dev $targetRoot/run + +mount --move /proc $targetRoot/proc +mount --move /sys $targetRoot/sys +mount --move /dev $targetRoot/dev +mount --move /run $targetRoot/run + +exec env -i $(type -P switch_root) "$targetRoot" "$stage2Init" + +fail # should never be reached diff --git a/nixos/modules/system/boot/stage-1.nix b/nixos/modules/system/boot/stage-1.nix new file mode 100644 index 00000000000..3836d639513 --- /dev/null +++ b/nixos/modules/system/boot/stage-1.nix @@ -0,0 +1,343 @@ +# This module builds the initial ramdisk, which contains an init +# script that performs the first stage of booting the system: it loads +# the modules necessary to mount the root file system, then calls the +# init in the root file system to start the second boot stage. + +{ config, pkgs, ... }: + +with pkgs.lib; + +let + + udev = config.systemd.package; + + kernelPackages = config.boot.kernelPackages; + modulesTree = config.system.modulesTree; + + + # Determine the set of modules that we need to mount the root FS. + modulesClosure = pkgs.makeModulesClosure { + rootModules = config.boot.initrd.availableKernelModules ++ config.boot.initrd.kernelModules; + kernel = modulesTree; + allowMissing = true; + }; + + + needsCifsUtils = kernelPackages.kernel ? features + && kernelPackages.kernel.features ? needsCifsUtils + && kernelPackages.kernel.features.needsCifsUtils + && any (fs: fs.fsType == "cifs") fileSystems; + + busybox = + if needsCifsUtils + then pkgs.busybox.override { + extraConfig = '' + CONFIG_FEATURE_MOUNT_CIFS n + CONFIG_FEATURE_MOUNT_HELPERS y + ''; + } + else pkgs.busybox; + + + # Some additional utilities needed in stage 1, like mount, lvm, fsck + # etc. We don't want to bring in all of those packages, so we just + # copy what we need. Instead of using statically linked binaries, + # we just copy what we need from Glibc and use patchelf to make it + # work. + extraUtils = pkgs.runCommand "extra-utils" + { buildInputs = [pkgs.nukeReferences]; + allowedReferences = [ "out" ]; # prevent accidents like glibc being included in the initrd + doublePatchelf = pkgs.stdenv.isArm; + } + '' + mkdir -p $out/bin $out/lib + + # Copy what we need from Glibc. + cp -pv ${pkgs.glibc}/lib/ld*.so.? $out/lib + cp -pv ${pkgs.glibc}/lib/libc.so.* $out/lib + cp -pv ${pkgs.glibc}/lib/libm.so.* $out/lib + cp -pv ${pkgs.glibc}/lib/libpthread.so.* $out/lib + cp -pv ${pkgs.glibc}/lib/librt.so.* $out/lib + cp -pv ${pkgs.glibc}/lib/libdl.so.* $out/lib + cp -pv ${pkgs.gcc.gcc}/lib*/libgcc_s.so.* $out/lib + + # Copy BusyBox. + cp -rvd ${busybox}/{bin,sbin} $out/ + chmod -R u+w $out + + # Copy some utillinux stuff. + cp -v ${pkgs.utillinux}/sbin/blkid $out/bin + cp -pdv ${pkgs.utillinux}/lib/libblkid*.so.* $out/lib + cp -pdv ${pkgs.utillinux}/lib/libuuid*.so.* $out/lib + + # Copy dmsetup and lvm. + cp -v ${pkgs.lvm2}/sbin/dmsetup $out/bin/dmsetup + cp -v ${pkgs.lvm2}/sbin/lvm $out/bin/lvm + cp -v ${pkgs.lvm2}/lib/libdevmapper.so.*.* $out/lib + cp -v ${pkgs.systemd}/lib/libsystemd-daemon.so.* $out/lib + + # Add RAID mdadm tool. + cp -v ${pkgs.mdadm}/sbin/mdadm $out/bin/mdadm + + # Copy udev. + cp -v ${udev}/lib/systemd/systemd-udevd ${udev}/bin/udevadm $out/bin + cp -v ${udev}/lib/udev/*_id $out/bin + cp -pdv ${udev}/lib/libudev.so.* $out/lib + cp -v ${pkgs.kmod}/lib/libkmod.so.* $out/lib + cp -v ${pkgs.acl}/lib/libacl.so.* $out/lib + cp -v ${pkgs.attr}/lib/libattr.so.* $out/lib + + # Copy modprobe. + cp -v ${pkgs.kmod}/bin/kmod $out/bin/ + ln -s kmod $out/bin/modprobe + + # Maybe copy cifs utils + ${optionalString needsCifsUtils '' + cp -v ${pkgs.cifs_utils}/sbin/mount.cifs $out/bin + ''} + + ${config.boot.initrd.extraUtilsCommands} + + # Strip binaries further than normal. + chmod -R u+w $out + stripDirs "lib bin" "-s" + + # Run patchelf to make the programs refer to the copied libraries. + for i in $out/bin/* $out/lib/*; do if ! test -L $i; then nuke-refs $i; fi; done + + for i in $out/bin/*; do + if ! test -L $i; then + echo "patching $i..." + patchelf --set-interpreter $out/lib/ld*.so.? --set-rpath $out/lib $i || true + if [ -n "$doublePatchelf" ]; then + patchelf --set-interpreter $out/lib/ld*.so.? --set-rpath $out/lib $i || true + fi + fi + done + + # Make sure that the patchelf'ed binaries still work. + echo "testing patched programs..." + $out/bin/ash -c 'echo hello world' | grep "hello world" + export LD_LIBRARY_PATH=$out/lib + $out/bin/mount --help 2>&1 | grep "BusyBox" + $out/bin/udevadm --version + $out/bin/dmsetup --version 2>&1 | tee -a log | grep "version:" + LVM_SYSTEM_DIR=$out $out/bin/lvm version 2>&1 | tee -a log | grep "LVM" + $out/bin/mdadm --version + + ${config.boot.initrd.extraUtilsCommandsTest} + ''; # */ + + + # The initrd only has to mount / or any FS marked as necessary for + # booting (such as the FS containing /nix/store, or an FS needed for + # mounting /, like / on a loopback). + fileSystems = filter + (fs: fs.neededForBoot || elem fs.mountPoint [ "/" "/nix" "/nix/store" "/var" "/var/log" "/var/lib" "/etc" ]) + (attrValues config.fileSystems); + + + udevRules = pkgs.stdenv.mkDerivation { + name = "udev-rules"; + buildCommand = '' + ensureDir $out + + echo 'ENV{LD_LIBRARY_PATH}="${extraUtils}/lib"' > $out/00-env.rules + + cp -v ${udev}/lib/udev/rules.d/60-cdrom_id.rules $out/ + cp -v ${udev}/lib/udev/rules.d/60-persistent-storage.rules $out/ + cp -v ${udev}/lib/udev/rules.d/80-drivers.rules $out/ + cp -v ${pkgs.lvm2}/lib/udev/rules.d/*.rules $out/ + cp -v ${pkgs.mdadm}/lib/udev/rules.d/*.rules $out/ + + for i in $out/*.rules; do + substituteInPlace $i \ + --replace ata_id ${extraUtils}/bin/ata_id \ + --replace scsi_id ${extraUtils}/bin/scsi_id \ + --replace cdrom_id ${extraUtils}/bin/cdrom_id \ + --replace ${pkgs.utillinux}/sbin/blkid ${extraUtils}/bin/blkid \ + --replace /sbin/blkid ${extraUtils}/bin/blkid \ + --replace ${pkgs.lvm2}/sbin ${extraUtils}/bin \ + --replace /sbin/mdadm ${extraUtils}/bin/mdadm + done + + # Work around a bug in QEMU, which doesn't implement the "READ + # DISC INFORMATION" SCSI command: + # https://bugzilla.redhat.com/show_bug.cgi?id=609049 + # As a result, `cdrom_id' doesn't print + # ID_CDROM_MEDIA_TRACK_COUNT_DATA, which in turn prevents the + # /dev/disk/by-label symlinks from being created. We need these + # in the NixOS installation CD, so use ID_CDROM_MEDIA in the + # corresponding udev rules for now. This was the behaviour in + # udev <= 154. See also + # http://www.spinics.net/lists/hotplug/msg03935.html + substituteInPlace $out/60-persistent-storage.rules \ + --replace ID_CDROM_MEDIA_TRACK_COUNT_DATA ID_CDROM_MEDIA + ''; # */ + }; + + + # The binary keymap for busybox to load at boot. + busyboxKeymap = pkgs.runCommand "boottime-keymap" + { preferLocalBuild = true; } + '' + ${pkgs.kbd}/bin/loadkeys -qb "${config.i18n.consoleKeyMap}" > $out || + ${pkgs.kbd}/bin/loadkeys -qbu "${config.i18n.consoleKeyMap}" > $out + ''; + + + # The init script of boot stage 1 (loading kernel modules for + # mounting the root FS). + bootStage1 = pkgs.substituteAll { + src = ./stage-1-init.sh; + + shell = "${extraUtils}/bin/ash"; + + isExecutable = true; + + inherit udevRules extraUtils modulesClosure busyboxKeymap; + + inherit (config.boot) resumeDevice devSize runSize; + + inherit (config.boot.initrd) checkJournalingFS + preLVMCommands postDeviceCommands postMountCommands kernelModules; + + fsInfo = + let f = fs: [ fs.mountPoint (if fs.device != null then fs.device else "/dev/disk/by-label/${fs.label}") fs.fsType fs.options ]; + in pkgs.writeText "initrd-fsinfo" (concatStringsSep "\n" (concatMap f fileSystems)); + }; + + + # The closure of the init script of boot stage 1 is what we put in + # the initial RAM disk. + initialRamdisk = pkgs.makeInitrd { + inherit (config.boot.initrd) compressor; + + contents = + [ { object = bootStage1; + symlink = "/init"; + } + { object = pkgs.writeText "mdadm.conf" config.boot.initrd.mdadmConf; + symlink = "/etc/mdadm.conf"; + } + ]; + }; + +in + +{ + options = { + + boot.resumeDevice = mkOption { + default = ""; + example = "0:0"; + description = " + Device for manual resume attempt during boot. Looks like + major:minor. ls -l /dev/SWAP_PARTION shows them. + "; + }; + + boot.initrd.checkJournalingFS = mkOption { + default = true; + type = types.bool; + description = '' + Whether to run fsck on journaling filesystems such as ext3. + ''; + }; + + boot.initrd.mdadmConf = mkOption { + default = ""; + type = with types; string; + description = '' + Contents of /etc/mdadm.conf at initrd. + ''; + }; + + boot.initrd.preLVMCommands = mkOption { + default = ""; + type = with types; string; + description = '' + Shell commands to be executed immediately before lvm discovery. + ''; + }; + + boot.initrd.postDeviceCommands = mkOption { + default = ""; + type = with types; string; + description = '' + Shell commands to be executed immediately after stage 1 of the + boot has loaded kernel modules and created device nodes in + /dev. + ''; + }; + + boot.initrd.postMountCommands = mkOption { + default = ""; + type = with types; string; + description = '' + Shell commands to be executed immediately after the stage 1 + filesystems have been mounted. + ''; + }; + + boot.initrd.extraUtilsCommands = mkOption { + internal = true; + default = ""; + type = with types; string; + description = '' + Shell commands to be executed in the builder of the + extra-utils derivation. This can be used to provide + additional utilities in the initial ramdisk. + ''; + }; + + boot.initrd.extraUtilsCommandsTest = mkOption { + internal = true; + default = ""; + type = with types; string; + description = '' + Shell commands to be executed in the builder of the + extra-utils derivation after patchelf has done its + job. This can be used to test additional utilities + copied in extraUtilsCommands. + ''; + }; + + boot.initrd.compressor = mkOption { + default = "gzip -9"; + + type = types.string; + + description = "The compressor to use on the initrd"; + + example = "xz"; + }; + + fileSystems = mkOption { + options.neededForBoot = mkOption { + default = false; + type = types.bool; + description = '' + If set, this file system will be mounted in the initial + ramdisk. By default, this applies to the root file system + and to the file system containing + <filename>/nix/store</filename>. + ''; + }; + }; + + }; + + config = { + + system.build.bootStage1 = bootStage1; + system.build.initialRamdisk = initialRamdisk; + system.build.extraUtils = extraUtils; + + system.requiredKernelConfig = with config.lib.kernelConfig; [ + (isYes "TMPFS") + (isYes "BLK_DEV_INITRD") + ]; + + }; +} diff --git a/nixos/modules/system/boot/stage-2-init.sh b/nixos/modules/system/boot/stage-2-init.sh new file mode 100644 index 00000000000..2fadd3de1f0 --- /dev/null +++ b/nixos/modules/system/boot/stage-2-init.sh @@ -0,0 +1,173 @@ +#! @shell@ + +systemConfig=@systemConfig@ + +export HOME=/root + + +# Print a greeting. +echo +echo -e "\e[1;32m<<< NixOS Stage 2 >>>\e[0m" +echo + + +# Set the PATH. +setPath() { + local dirs="$1" + export PATH=/empty + for i in $dirs; do + PATH=$PATH:$i/bin + if test -e $i/sbin; then + PATH=$PATH:$i/sbin + fi + done +} + +setPath "@path@" + + +# Normally, stage 1 mounts the root filesystem read/writable. +# However, in some environments, stage 2 is executed directly, and the +# root is read-only. So make it writable here. +mount -n -o remount,rw / + + +# Likewise, stage 1 mounts /proc, /dev and /sys, so if we don't have a +# stage 1, we need to do that here. +if [ ! -e /proc/1 ]; then + mkdir -m 0755 -p /proc + mount -n -t proc none /proc + mkdir -m 0755 -p /dev + mount -t devtmpfs none /dev +fi + + +echo "booting system configuration $systemConfig" > /dev/kmsg + + +# Make /nix/store a read-only bind mount to enforce immutability of +# the Nix store. Note that we can't use "chown root:nixbld" here +# because users/groups might not exist yet. +chown 0:30000 /nix/store +chmod 1775 /nix/store +if [ -n "@readOnlyStore@" ]; then + if ! readonly-mountpoint /nix/store; then + mount --bind /nix/store /nix/store + mount -o remount,ro,bind /nix/store + fi +fi + + +# Provide a /etc/mtab. +mkdir -m 0755 -p /etc +test -e /etc/fstab || touch /etc/fstab # to shut up mount +rm -f /etc/mtab* # not that we care about stale locks +ln -s /proc/mounts /etc/mtab + + +# Process the kernel command line. +for o in $(cat /proc/cmdline); do + case $o in + boot.debugtrace) + # Show each command. + set -x + ;; + resume=*) + set -- $(IFS==; echo $o) + resumeDevice=$2 + ;; + esac +done + + +# More special file systems, initialise required directories. +mkdir -m 0755 /dev/shm +mount -t tmpfs -o "rw,nosuid,nodev,size=@devShmSize@" tmpfs /dev/shm +mkdir -m 0755 -p /dev/pts +[ -e /proc/bus/usb ] && mount -t usbfs none /proc/bus/usb # UML doesn't have USB by default +mkdir -m 01777 -p /tmp +mkdir -m 0755 -p /var /var/log /var/lib /var/db +mkdir -m 0755 -p /nix/var +mkdir -m 0700 -p /root +mkdir -m 0755 -p /bin # for the /bin/sh symlink +mkdir -m 0755 -p /home +mkdir -m 0755 -p /etc/nixos + + +# Miscellaneous boot time cleanup. +rm -rf /var/run /var/lock +rm -f /etc/resolv.conf +touch /etc/resolv.conf +rm -f /etc/{group,passwd,shadow}.lock + +if test -n "@cleanTmpDir@"; then + echo -n "cleaning \`/tmp'..." + find /tmp -maxdepth 1 -mindepth 1 -print0 | xargs -0r rm -rf --one-file-system + echo " done" +else + # Get rid of ICE locks... + rm -rf /tmp/.ICE-unix +fi + +# ... and ensure that it's owned by root. +mkdir -m 1777 /tmp/.ICE-unix + +# This is a good time to clean up /nix/var/nix/chroots. Doing an `rm +# -rf' on it isn't safe in general because it can contain bind mounts +# to /nix/store and other places. But after rebooting these are all +# gone, of course. +rm -rf /nix/var/nix/chroots # recreated in activate-configuration.sh + + +# Also get rid of temporary GC roots. +rm -rf /nix/var/nix/gcroots/tmp /nix/var/nix/temproots + + +# Create a tmpfs on /run to hold runtime state for programs such as +# udev (if stage 1 hasn't already done so). +if ! mountpoint -q /run; then + rm -rf /run + mkdir -m 0755 -p /run + mount -t tmpfs -o "mode=0755,size=@runSize@" none /run +fi + +mkdir -m 0755 -p /run/lock + + +# For backwards compatibility, symlink /var/run to /run, and /var/lock +# to /run/lock. +ln -s /run /var/run +ln -s /run/lock /var/lock + + +# Clear the resume device. +if test -n "$resumeDevice"; then + mkswap "$resumeDevice" || echo 'Failed to clear saved image.' +fi + + +# Run the script that performs all configuration activation that does +# not have to be done at boot time. +echo "running activation script..." +$systemConfig/activate + + +# Record the boot configuration. +ln -sfn "$systemConfig" /run/booted-system + +# Prevent the booted system form being garbage-collected If it weren't +# a gcroot, if we were running a different kernel, switched system, +# and garbage collected all, we could not load kernel modules anymore. +ln -sfn /run/booted-system /nix/var/nix/gcroots/booted-system + + +# Run any user-specified commands. +@shell@ @postBootCommands@ + + +# Start systemd. +echo "starting systemd..." +PATH=/run/current-system/systemd/lib/systemd \ + MODULE_DIR=/run/booted-system/kernel-modules/lib/modules \ + LOCALE_ARCHIVE=/run/current-system/sw/lib/locale/locale-archive \ + exec systemd --log-target=journal # --log-level=debug --log-target=console --crash-shell diff --git a/nixos/modules/system/boot/stage-2.nix b/nixos/modules/system/boot/stage-2.nix new file mode 100644 index 00000000000..ff17535e418 --- /dev/null +++ b/nixos/modules/system/boot/stage-2.nix @@ -0,0 +1,100 @@ +{ config, pkgs, ... }: + +with pkgs.lib; + +let + + kernel = config.boot.kernelPackages.kernel; + activateConfiguration = config.system.activationScripts.script; + + readonlyMountpoint = pkgs.runCommand "readonly-mountpoint" {} '' + mkdir -p $out/bin + cc -O3 ${./readonly-mountpoint.c} -o $out/bin/readonly-mountpoint + strip -s $out/bin/readonly-mountpoint + ''; + + bootStage2 = pkgs.substituteAll { + src = ./stage-2-init.sh; + shellDebug = "${pkgs.bashInteractive}/bin/bash"; + isExecutable = true; + inherit (config.boot) devShmSize runSize cleanTmpDir; + inherit (config.nix) readOnlyStore; + ttyGid = config.ids.gids.tty; + path = + [ pkgs.coreutils + pkgs.utillinux + pkgs.sysvtools + ] ++ (optional config.boot.cleanTmpDir pkgs.findutils) + ++ optional config.nix.readOnlyStore readonlyMountpoint; + postBootCommands = pkgs.writeText "local-cmds" + '' + ${config.boot.postBootCommands} + ${config.powerManagement.powerUpCommands} + ''; + }; + +in + +{ + options = { + + boot = { + + postBootCommands = mkOption { + default = ""; + example = "rm -f /var/log/messages"; + type = types.string; + description = '' + Shell commands to be executed just before systemd is started. + ''; + }; + + devSize = mkOption { + default = "5%"; + example = "32m"; + type = types.uniq types.string; + description = '' + Size limit for the /dev tmpfs. Look at mount(8), tmpfs size option, + for the accepted syntax. + ''; + }; + + devShmSize = mkOption { + default = "50%"; + example = "256m"; + type = types.uniq types.string; + description = '' + Size limit for the /dev/shm tmpfs. Look at mount(8), tmpfs size option, + for the accepted syntax. + ''; + }; + + runSize = mkOption { + default = "25%"; + example = "256m"; + type = types.uniq types.string; + description = '' + Size limit for the /run tmpfs. Look at mount(8), tmpfs size option, + for the accepted syntax. + ''; + }; + + cleanTmpDir = mkOption { + default = false; + example = true; + description = '' + Delete all files in /tmp/ during boot. + ''; + }; + + }; + + }; + + + config = { + + system.build.bootStage2 = bootStage2; + + }; +} diff --git a/nixos/modules/system/boot/systemd-unit-options.nix b/nixos/modules/system/boot/systemd-unit-options.nix new file mode 100644 index 00000000000..dfb9036ab4d --- /dev/null +++ b/nixos/modules/system/boot/systemd-unit-options.nix @@ -0,0 +1,364 @@ +{ config, pkgs }: + +with pkgs.lib; + +rec { + + unitOptions = { + + 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. + ''; + }; + + description = mkOption { + default = ""; + types = types.uniq types.string; + description = "Description of this unit used in systemd messages and progress indicators."; + }; + + requires = mkOption { + default = []; + types = types.listOf types.string; + description = '' + Start the specified units when this unit is started, and stop + this unit when the specified units are stopped or fail. + ''; + }; + + wants = mkOption { + default = []; + types = types.listOf types.string; + description = '' + Start the specified units when this unit is started. + ''; + }; + + after = mkOption { + default = []; + types = types.listOf types.string; + description = '' + If the specified units are started at the same time as + this unit, delay this unit until they have started. + ''; + }; + + before = mkOption { + default = []; + types = types.listOf types.string; + description = '' + If the specified units are started at the same time as + this unit, delay them until this unit has started. + ''; + }; + + bindsTo = mkOption { + default = []; + types = types.listOf types.string; + description = '' + Like ‘requires’, but in addition, if the specified units + unexpectedly disappear, this unit will be stopped as well. + ''; + }; + + partOf = mkOption { + default = []; + types = types.listOf types.string; + description = '' + If the specified units are stopped or restarted, then this + unit is stopped or restarted as well. + ''; + }; + + conflicts = mkOption { + default = []; + types = types.listOf types.string; + description = '' + If the specified units are started, then this unit is stopped + and vice versa. + ''; + }; + + 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."; + }; + + unitConfig = mkOption { + default = {}; + example = { RequiresMountsFor = "/data"; }; + type = types.attrs; + description = '' + Each attribute in this set specifies an option in the + <literal>[Unit]</literal> section of the unit. See + <citerefentry><refentrytitle>systemd.unit</refentrytitle> + <manvolnum>5</manvolnum></citerefentry> for details. + ''; + }; + + restartTriggers = mkOption { + default = []; + description = '' + An arbitrary list of items such as derivations. If any item + in the list changes between reconfigurations, the service will + be restarted. + ''; + }; + + }; + + + serviceOptions = unitOptions // { + + environment = mkOption { + default = {}; + type = types.attrs; + example = { PATH = "/foo/bar/bin"; LANG = "nl_NL.UTF-8"; }; + description = "Environment variables passed to the service's processes."; + }; + + path = mkOption { + default = []; + apply = ps: "${makeSearchPath "bin" ps}:${makeSearchPath "sbin" ps}"; + description = '' + Packages added to the service's <envar>PATH</envar> + environment variable. Both the <filename>bin</filename> + and <filename>sbin</filename> subdirectories of each + package are added. + ''; + }; + + serviceConfig = mkOption { + default = {}; + example = + { StartLimitInterval = 10; + RestartSec = 5; + }; + type = types.attrs; + description = '' + Each attribute in this set specifies an option in the + <literal>[Service]</literal> section of the unit. See + <citerefentry><refentrytitle>systemd.service</refentrytitle> + <manvolnum>5</manvolnum></citerefentry> for details. + ''; + + check = v: + let assertValueOneOf = name: values: attr: + let val = getAttr name attr; + in optional ( hasAttr name attr && !elem val values) "${name} ${val} not known to systemd"; + checkType = assertValueOneOf "Type" ["simple" "forking" "oneshot" "dbus" "notify" "idle"]; + checkRestart = assertValueOneOf "Restart" ["no" "on-success" "on-failure" "on-abort" "always"]; + errors = concatMap (c: c v) [checkType checkRestart]; + in if errors == [] then true + else builtins.trace (concatStringsSep "\n" errors) false; + }; + + script = mkOption { + type = types.uniq types.string; + default = ""; + description = "Shell commands executed as the service's main process."; + }; + + scriptArgs = mkOption { + type = types.uniq types.string; + default = ""; + description = "Arguments passed to the main process script."; + }; + + preStart = mkOption { + type = types.string; + default = ""; + description = '' + Shell commands executed before the service's main process + is started. + ''; + }; + + postStart = mkOption { + type = types.string; + default = ""; + description = '' + Shell commands executed after the service's main process + is started. + ''; + }; + + postStop = mkOption { + type = types.string; + default = ""; + description = '' + Shell commands executed after the service's main process + has exited. + ''; + }; + + restartIfChanged = mkOption { + type = types.bool; + default = true; + description = '' + Whether the service should be restarted during a NixOS + configuration switch if its definition has changed. + ''; + }; + + stopIfChanged = mkOption { + type = types.bool; + default = true; + description = '' + If set, a changed unit is restarted by calling + <command>systemctl stop</command> in the old configuration, + then <command>systemctl start</command> in the new one. + Otherwise, it is restarted in a single step using + <command>systemctl restart</command> in the new configuration. + The latter is less correct because it runs the + <literal>ExecStop</literal> commands from the new + configuration. + ''; + }; + + startAt = mkOption { + type = types.uniq types.string; + default = ""; + example = "Sun 14:00:00"; + description = '' + Automatically start this unit at the given date/time, which + must be in the format described in + <citerefentry><refentrytitle>systemd.time</refentrytitle> + <manvolnum>5</manvolnum></citerefentry>. This is equivalent + to adding a corresponding timer unit with + <option>OnCalendar</option> set to the value given here. + ''; + }; + + }; + + + socketOptions = unitOptions // { + + listenStreams = mkOption { + default = []; + types = types.listOf types.string; + example = [ "0.0.0.0:993" "/run/my-socket" ]; + description = '' + For each item in this list, a <literal>ListenStream</literal> + option in the <literal>[Socket]</literal> section will be created. + ''; + }; + + socketConfig = mkOption { + default = {}; + example = { ListenStream = "/run/my-socket"; }; + type = types.attrs; + description = '' + Each attribute in this set specifies an option in the + <literal>[Socket]</literal> section of the unit. See + <citerefentry><refentrytitle>systemd.socket</refentrytitle> + <manvolnum>5</manvolnum></citerefentry> for details. + ''; + }; + + }; + + + timerOptions = unitOptions // { + + timerConfig = mkOption { + default = {}; + example = { OnCalendar = "Sun 14:00:00"; Unit = "foo.service"; }; + type = types.attrs; + description = '' + Each attribute in this set specifies an option in the + <literal>[Timer]</literal> section of the unit. See + <citerefentry><refentrytitle>systemd.timer</refentrytitle> + <manvolnum>5</manvolnum></citerefentry> and + <citerefentry><refentrytitle>systemd.time</refentrytitle> + <manvolnum>5</manvolnum></citerefentry> for details. + ''; + }; + + }; + + + mountOptions = unitOptions // { + + what = mkOption { + example = "/dev/sda1"; + type = types.uniq types.string; + description = "Absolute path of device node, file or other resource. (Mandatory)"; + }; + + where = mkOption { + example = "/mnt"; + type = types.uniq types.string; + description = '' + Absolute path of a directory of the mount point. + Will be created if it doesn't exist. (Mandatory) + ''; + }; + + type = mkOption { + default = ""; + example = "ext4"; + type = types.uniq types.string; + description = "File system type."; + }; + + options = mkOption { + default = ""; + example = "noatime"; + type = types.string; + merge = concatStringsSep ","; + description = "Options used to mount the file system."; + }; + + mountConfig = mkOption { + default = {}; + example = { DirectoryMode = "0775"; }; + type = types.attrs; + description = '' + Each attribute in this set specifies an option in the + <literal>[Mount]</literal> section of the unit. See + <citerefentry><refentrytitle>systemd.mount</refentrytitle> + <manvolnum>5</manvolnum></citerefentry> for details. + ''; + }; + }; + + automountOptions = unitOptions // { + + where = mkOption { + example = "/mnt"; + type = types.uniq types.string; + description = '' + Absolute path of a directory of the mount point. + Will be created if it doesn't exist. (Mandatory) + ''; + }; + + automountConfig = mkOption { + default = {}; + example = { DirectoryMode = "0775"; }; + type = types.attrs; + description = '' + Each attribute in this set specifies an option in the + <literal>[Automount]</literal> section of the unit. See + <citerefentry><refentrytitle>systemd.automount</refentrytitle> + <manvolnum>5</manvolnum></citerefentry> for details. + ''; + }; + }; + +} diff --git a/nixos/modules/system/boot/systemd.nix b/nixos/modules/system/boot/systemd.nix new file mode 100644 index 00000000000..a5e1165574c --- /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 { + 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.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.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) + // 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"; + }; + }; + + }; +} |