diff options
author | Eelco Dolstra <eelco.dolstra@logicblox.com> | 2013-10-10 13:28:20 +0200 |
---|---|---|
committer | Eelco Dolstra <eelco.dolstra@logicblox.com> | 2013-10-10 13:28:20 +0200 |
commit | 5c1f8cbc70cd5e6867ef6a2a06d27a40daa07010 (patch) | |
tree | a6c0f605be6de3f372ae69905b331f9f75452da7 /nixos/modules/system | |
parent | 6070bc016bd2fd945b04347e25cfd3738622d2ac (diff) | |
download | nixpkgs-5c1f8cbc70cd5e6867ef6a2a06d27a40daa07010.tar nixpkgs-5c1f8cbc70cd5e6867ef6a2a06d27a40daa07010.tar.gz nixpkgs-5c1f8cbc70cd5e6867ef6a2a06d27a40daa07010.tar.bz2 nixpkgs-5c1f8cbc70cd5e6867ef6a2a06d27a40daa07010.tar.lz nixpkgs-5c1f8cbc70cd5e6867ef6a2a06d27a40daa07010.tar.xz nixpkgs-5c1f8cbc70cd5e6867ef6a2a06d27a40daa07010.tar.zst nixpkgs-5c1f8cbc70cd5e6867ef6a2a06d27a40daa07010.zip |
Move all of NixOS to nixos/ in preparation of the repository merge
Diffstat (limited to 'nixos/modules/system')
36 files changed, 5411 insertions, 0 deletions
diff --git a/nixos/modules/system/activation/activation-script.nix b/nixos/modules/system/activation/activation-script.nix new file mode 100644 index 00000000000..dc017563217 --- /dev/null +++ b/nixos/modules/system/activation/activation-script.nix @@ -0,0 +1,155 @@ +# generate the script used to activate the configuration. +{ config, pkgs, ... }: + +with pkgs.lib; + +let + + addAttributeName = mapAttrs (a: v: v // { + text = '' + #### Activation script snippet ${a}: + ${v.text} + ''; + }); + + path = + [ pkgs.coreutils pkgs.gnugrep pkgs.findutils + pkgs.glibc # needed for getent + pkgs.shadow + pkgs.nettools # needed for hostname + ]; + +in + +{ + + ###### interface + + options = { + + system.activationScripts = mkOption { + default = {}; + + example = { + stdio = { + text = '' + # Needed by some programs. + ln -sfn /proc/self/fd /dev/fd + ln -sfn /proc/self/fd/0 /dev/stdin + ln -sfn /proc/self/fd/1 /dev/stdout + ln -sfn /proc/self/fd/2 /dev/stderr + ''; + deps = []; + }; + }; + + description = '' + Activate the new configuration (i.e., update /etc, make accounts, + and so on). + ''; + + merge = mergeTypedOption "script" builtins.isAttrs (fold mergeAttrs {}); + + apply = set: { + script = + '' + #! ${pkgs.stdenv.shell} + + systemConfig=@out@ + + export PATH=/empty + for i in ${toString path}; do + PATH=$PATH:$i/bin:$i/sbin + done + + # Ensure a consistent umask. + umask 0022 + + ${ + let + set' = mapAttrs (n: v: if builtins.isString v then noDepEntry v else v) set; + withHeadlines = addAttributeName set'; + in textClosureMap id (withHeadlines) (attrNames withHeadlines) + } + + # Make this configuration the current configuration. + # The readlink is there to ensure that when $systemConfig = /system + # (which is a symlink to the store), /run/current-system is still + # used as a garbage collection root. + ln -sfn "$(readlink -f "$systemConfig")" /run/current-system + + # Prevent the current configuration from being garbage-collected. + ln -sfn /run/current-system /nix/var/nix/gcroots/current-system + ''; + }; + + }; + + }; + + + ###### implementation + + config = { + + system.activationScripts.stdio = + '' + # Needed by some programs. + ln -sfn /proc/self/fd /dev/fd + ln -sfn /proc/self/fd/0 /dev/stdin + ln -sfn /proc/self/fd/1 /dev/stdout + ln -sfn /proc/self/fd/2 /dev/stderr + ''; + + system.activationScripts.var = + '' + # Various log/runtime directories. + + touch /var/run/utmp # must exist + chgrp ${toString config.ids.gids.utmp} /var/run/utmp + chmod 664 /var/run/utmp + + mkdir -m 0755 -p /var/run/nix/current-load # for distributed builds + mkdir -m 0700 -p /var/run/nix/remote-stores + + # Directory holding symlinks to currently running Upstart + # jobs. Used to determine which jobs need to be restarted + # when switching to a new configuration. + mkdir -m 0700 -p /var/run/upstart-jobs + + mkdir -m 0755 -p /var/log + + touch /var/log/wtmp # must exist + chmod 644 /var/log/wtmp + + touch /var/log/lastlog + chmod 644 /var/log/lastlog + + mkdir -m 1777 -p /var/tmp + + # Empty, read-only home directory of many system accounts. + mkdir -m 0555 -p /var/empty + ''; + + system.activationScripts.media = + '' + mkdir -m 0755 -p /media + ''; + + system.activationScripts.usrbinenv = + '' + mkdir -m 0755 -p /usr/bin + ln -sfn ${pkgs.coreutils}/bin/env /usr/bin/.env.tmp + mv /usr/bin/.env.tmp /usr/bin/env # atomically replace /usr/bin/env + ''; + + system.activationScripts.tmpfs = + '' + ${pkgs.utillinux}/bin/mount -o "remount,size=${config.boot.devSize}" none /dev + ${pkgs.utillinux}/bin/mount -o "remount,size=${config.boot.devShmSize}" none /dev/shm + ${pkgs.utillinux}/bin/mount -o "remount,size=${config.boot.runSize}" none /run + ''; + + }; + +} diff --git a/nixos/modules/system/activation/no-clone.nix b/nixos/modules/system/activation/no-clone.nix new file mode 100644 index 00000000000..f15809e4d8b --- /dev/null +++ b/nixos/modules/system/activation/no-clone.nix @@ -0,0 +1,11 @@ +# This configuration is not made to figure inside the module-list.nix to +# allow clone of the first level. +{pkgs, ...}: + +with pkgs.lib; + +{ + boot.loader.grub.device = mkOverrideTemplate 0 {} "nodev"; + nesting.children = mkOverrideTemplate 0 {} []; + nesting.clone = mkOverrideTemplate 0 {} []; +} diff --git a/nixos/modules/system/activation/switch-to-configuration.pl b/nixos/modules/system/activation/switch-to-configuration.pl new file mode 100644 index 00000000000..33ae3aef9fc --- /dev/null +++ b/nixos/modules/system/activation/switch-to-configuration.pl @@ -0,0 +1,362 @@ +#! @perl@ + +use strict; +use warnings; +use File::Basename; +use File::Slurp; +use Sys::Syslog qw(:standard :macros); +use Cwd 'abs_path'; + +my $out = "@out@"; + +my $startListFile = "/run/systemd/start-list"; +my $restartListFile = "/run/systemd/restart-list"; +my $reloadListFile = "/run/systemd/reload-list"; + +my $action = shift @ARGV; + +if (!defined $action || ($action ne "switch" && $action ne "boot" && $action ne "test")) { + print STDERR <<EOF; +Usage: $0 [switch|boot|test] + +switch: make the configuration the boot default and activate now +boot: make the configuration the boot default +test: activate the configuration, but don\'t make it the boot default +EOF + exit 1; +} + +die "This is not a NixOS installation (/etc/NIXOS is missing)!\n" unless -f "/etc/NIXOS"; + +openlog("nixos", "", LOG_USER); + +# Install or update the bootloader. +if ($action eq "switch" || $action eq "boot") { + system("@installBootLoader@ $out") == 0 or exit 1; +} + +# Just in case the new configuration hangs the system, do a sync now. +system("@coreutils@/bin/sync") unless ($ENV{"NIXOS_NO_SYNC"} // "") eq "1"; + +exit 0 if $action eq "boot"; + +# Check if we can activate the new configuration. +my $oldVersion = read_file("/run/current-system/init-interface-version", err_mode => 'quiet') // ""; +my $newVersion = read_file("$out/init-interface-version"); + +if ($newVersion ne $oldVersion) { + print STDERR <<EOF; +Warning: the new NixOS configuration has an ‘init’ that is +incompatible with the current configuration. The new configuration +won\'t take effect until you reboot the system. +EOF + exit 100; +} + +syslog(LOG_NOTICE, "switching to system configuration $out"); + +# Ignore SIGHUP so that we're not killed if we're running on (say) +# virtual console 1 and we restart the "tty1" unit. +$SIG{PIPE} = "IGNORE"; + +sub getActiveUnits { + # FIXME: use D-Bus or whatever to query this, since parsing the + # output of list-units is likely to break. + my $lines = `@systemd@/bin/systemctl list-units --full`; + my $res = {}; + foreach my $line (split '\n', $lines) { + chomp $line; + last if $line eq ""; + $line =~ /^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s/ or next; + next if $1 eq "UNIT"; + $res->{$1} = { load => $2, state => $3, substate => $4 }; + } + return $res; +} + +sub parseFstab { + my ($filename) = @_; + my ($fss, $swaps); + foreach my $line (read_file($filename, err_mode => 'quiet')) { + chomp $line; + $line =~ s/^\s*#.*//; + next if $line =~ /^\s*$/; + my @xs = split / /, $line; + if ($xs[2] eq "swap") { + $swaps->{$xs[0]} = { options => $xs[3] // "" }; + } else { + $fss->{$xs[1]} = { device => $xs[0], fsType => $xs[2], options => $xs[3] // "" }; + } + } + return ($fss, $swaps); +} + +sub parseUnit { + my ($filename) = @_; + my $info = {}; + foreach my $line (read_file($filename)) { + # FIXME: not quite correct. + $line =~ /^([^=]+)=(.*)$/ or next; + $info->{$1} = $2; + } + return $info; +} + +sub boolIsTrue { + my ($s) = @_; + return $s eq "yes" || $s eq "true"; +} + +# Stop all services that no longer exist or have changed in the new +# configuration. +my (@unitsToStop, @unitsToSkip); +my $activePrev = getActiveUnits; +while (my ($unit, $state) = each %{$activePrev}) { + my $baseUnit = $unit; + + # Recognise template instances. + $baseUnit = "$1\@.$2" if $unit =~ /^(.*)@[^\.]*\.(.*)$/; + my $prevUnitFile = "/etc/systemd/system/$baseUnit"; + my $newUnitFile = "$out/etc/systemd/system/$baseUnit"; + + my $baseName = $baseUnit; + $baseName =~ s/\.[a-z]*$//; + + if (-e $prevUnitFile && ($state->{state} eq "active" || $state->{state} eq "activating")) { + if (! -e $newUnitFile) { + push @unitsToStop, $unit; + } + + elsif ($unit =~ /\.target$/) { + my $unitInfo = parseUnit($newUnitFile); + + # Cause all active target units to be restarted below. + # This should start most changed units we stop here as + # well as any new dependencies (including new mounts and + # swap devices). FIXME: the suspend target is sometimes + # active after the system has resumed, which probably + # should not be the case. Just ignore it. + if ($unit ne "suspend.target" && $unit ne "hibernate.target" && $unit ne "hybrid-sleep.target") { + unless (boolIsTrue($unitInfo->{'RefuseManualStart'} // "no")) { + write_file($startListFile, { append => 1 }, "$unit\n"); + } + } + + # Stop targets that have X-StopOnReconfiguration set. + # This is necessary to respect dependency orderings + # involving targets: if unit X starts after target Y and + # target Y starts after unit Z, then if X and Z have both + # changed, then X should be restarted after Z. However, + # if target Y is in the "active" state, X and Z will be + # restarted at the same time because X's dependency on Y + # is already satisfied. Thus, we need to stop Y first. + # Stopping a target generally has no effect on other units + # (unless there is a PartOf dependency), so this is just a + # bookkeeping thing to get systemd to do the right thing. + if (boolIsTrue($unitInfo->{'X-StopOnReconfiguration'} // "no")) { + push @unitsToStop, $unit; + } + } + + elsif (abs_path($prevUnitFile) ne abs_path($newUnitFile)) { + if ($unit eq "sysinit.target" || $unit eq "basic.target" || $unit eq "multi-user.target" || $unit eq "graphical.target") { + # Do nothing. These cannot be restarted directly. + } elsif ($unit =~ /\.mount$/) { + # Reload the changed mount unit to force a remount. + write_file($reloadListFile, { append => 1 }, "$unit\n"); + } elsif ($unit =~ /\.socket$/ || $unit =~ /\.path$/) { + # FIXME: do something? + } else { + my $unitInfo = parseUnit($newUnitFile); + if (!boolIsTrue($unitInfo->{'X-RestartIfChanged'} // "yes")) { + push @unitsToSkip, $unit; + } else { + # If this unit is socket-activated, then stop the + # socket unit(s) as well, and restart the + # socket(s) instead of the service. + my $socketActivated = 0; + if ($unit =~ /\.service$/) { + my @sockets = split / /, ($unitInfo->{Sockets} // ""); + if (scalar @sockets == 0) { + @sockets = ("$baseName.socket"); + } + foreach my $socket (@sockets) { + if (defined $activePrev->{$socket}) { + push @unitsToStop, $socket; + write_file($startListFile, { append => 1 }, "$socket\n"); + $socketActivated = 1; + } + } + } + + if (!boolIsTrue($unitInfo->{'X-StopIfChanged'} // "yes")) { + + # This unit should be restarted instead of + # stopped and started. + write_file($restartListFile, { append => 1 }, "$unit\n"); + + } else { + + # If the unit is not socket-activated, record + # that this unit needs to be started below. + # We write this to a file to ensure that the + # service gets restarted if we're interrupted. + if (!$socketActivated) { + write_file($startListFile, { append => 1 }, "$unit\n"); + } + + push @unitsToStop, $unit; + + } + } + } + } + } +} + +sub pathToUnitName { + my ($path) = @_; + die unless substr($path, 0, 1) eq "/"; + return "-" if $path eq "/"; + $path = substr($path, 1); + $path =~ s/\//-/g; + # FIXME: handle - and unprintable characters. + return $path; +} + +sub unique { + my %seen; + my @res; + foreach my $name (@_) { + next if $seen{$name}; + $seen{$name} = 1; + push @res, $name; + } + return @res; +} + +# Compare the previous and new fstab to figure out which filesystems +# need a remount or need to be unmounted. New filesystems are mounted +# automatically by starting local-fs.target. FIXME: might be nicer if +# we generated units for all mounts; then we could unify this with the +# unit checking code above. +my ($prevFss, $prevSwaps) = parseFstab "/etc/fstab"; +my ($newFss, $newSwaps) = parseFstab "$out/etc/fstab"; +foreach my $mountPoint (keys %$prevFss) { + my $prev = $prevFss->{$mountPoint}; + my $new = $newFss->{$mountPoint}; + my $unit = pathToUnitName($mountPoint) . ".mount"; + if (!defined $new) { + # Filesystem entry disappeared, so unmount it. + push @unitsToStop, $unit; + } elsif ($prev->{fsType} ne $new->{fsType} || $prev->{device} ne $new->{device}) { + # Filesystem type or device changed, so unmount and mount it. + write_file($startListFile, { append => 1 }, "$unit\n"); + push @unitsToStop, $unit; + } elsif ($prev->{options} ne $new->{options}) { + # Mount options changes, so remount it. + write_file($reloadListFile, { append => 1 }, "$unit\n"); + } +} + +# Also handles swap devices. +foreach my $device (keys %$prevSwaps) { + my $prev = $prevSwaps->{$device}; + my $new = $newSwaps->{$device}; + if (!defined $new) { + # Swap entry disappeared, so turn it off. Can't use + # "systemctl stop" here because systemd has lots of alias + # units that prevent a stop from actually calling + # "swapoff". + print STDERR "stopping swap device: $device\n"; + system("@utillinux@/sbin/swapoff", $device); + } + # FIXME: update swap options (i.e. its priority). +} + +if (scalar @unitsToStop > 0) { + @unitsToStop = unique(@unitsToStop); + print STDERR "stopping the following units: ", join(", ", sort(@unitsToStop)), "\n"; + system("@systemd@/bin/systemctl", "stop", "--", @unitsToStop); # FIXME: ignore errors? +} + +print STDERR "NOT restarting the following units: ", join(", ", sort(@unitsToSkip)), "\n" + if scalar @unitsToSkip > 0; + +# Activate the new configuration (i.e., update /etc, make accounts, +# and so on). +my $res = 0; +print STDERR "activating the configuration...\n"; +system("$out/activate", "$out") == 0 or $res = 2; + +# Restart systemd if necessary. +if (abs_path("/proc/1/exe") ne abs_path("@systemd@/lib/systemd/systemd")) { + print STDERR "restarting systemd...\n"; + system("@systemd@/bin/systemctl", "daemon-reexec") == 0 or $res = 2; +} + +# Forget about previously failed services. +system("@systemd@/bin/systemctl", "reset-failed"); + +# Make systemd reload its units. +system("@systemd@/bin/systemctl", "daemon-reload") == 0 or $res = 3; + +# Restart changed services (those that have to be restarted rather +# than stopped and started). +my @restart = unique(split('\n', read_file($restartListFile, err_mode => 'quiet') // "")); +if (scalar @restart > 0) { + print STDERR "restarting the following units: ", join(", ", sort(@restart)), "\n"; + system("@systemd@/bin/systemctl", "restart", "--", @restart) == 0 or $res = 4; + unlink($restartListFile); +} + +# Start all active targets, as well as changed units we stopped above. +# The latter is necessary because some may not be dependencies of the +# targets (i.e., they were manually started). FIXME: detect units +# that are symlinks to other units. We shouldn't start both at the +# same time because we'll get a "Failed to add path to set" error from +# systemd. +my @start = unique("default.target", "timers.target", split('\n', read_file($startListFile, err_mode => 'quiet') // "")); +print STDERR "starting the following units: ", join(", ", sort(@start)), "\n"; +system("@systemd@/bin/systemctl", "start", "--", @start) == 0 or $res = 4; +unlink($startListFile); + +# Reload units that need it. This includes remounting changed mount +# units. +my @reload = unique(split '\n', read_file($reloadListFile, err_mode => 'quiet') // ""); +if (scalar @reload > 0) { + print STDERR "reloading the following units: ", join(", ", sort(@reload)), "\n"; + system("@systemd@/bin/systemctl", "reload", "--", @reload) == 0 or $res = 4; + unlink($reloadListFile); +} + +# Signal dbus to reload its configuration. +system("@systemd@/bin/systemctl", "reload", "dbus.service"); + +# Print failed and new units. +my (@failed, @new, @restarting); +my $activeNew = getActiveUnits; +while (my ($unit, $state) = each %{$activeNew}) { + push @failed, $unit if $state->{state} eq "failed" || $state->{substate} eq "auto-restart"; + push @new, $unit if $state->{state} ne "failed" && !defined $activePrev->{$unit}; +} + +print STDERR "the following new units were started: ", join(", ", sort(@new)), "\n" + if scalar @new > 0; + +if (scalar @failed > 0) { + print STDERR "warning: the following units failed: ", join(", ", sort(@failed)), "\n"; + foreach my $unit (@failed) { + print STDERR "\n"; + system("COLUMNS=1000 @systemd@/bin/systemctl status --no-pager '$unit' >&2"); + } + $res = 4; +} + +if ($res == 0) { + syslog(LOG_NOTICE, "finished switching to system configuration $out"); +} else { + syslog(LOG_ERR, "switching to system configuration $out failed (status $res)"); +} + +exit $res; diff --git a/nixos/modules/system/activation/top-level.nix b/nixos/modules/system/activation/top-level.nix new file mode 100644 index 00000000000..32157e41985 --- /dev/null +++ b/nixos/modules/system/activation/top-level.nix @@ -0,0 +1,194 @@ +{ config, pkgs, modules, baseModules, ... }: + +with pkgs.lib; + +let + + + # This attribute is responsible for creating boot entries for + # child configuration. They are only (directly) accessible + # when the parent configuration is boot default. For example, + # you can provide an easy way to boot the same configuration + # as you use, but with another kernel + # !!! fix this + cloner = inheritParent: list: with pkgs.lib; + map (childConfig: + (import ../../../lib/eval-config.nix { + inherit baseModules; + modules = + (optionals inheritParent modules) + ++ [ ./no-clone.nix ] + ++ [ childConfig ]; + }).config.system.build.toplevel + ) list; + + children = + cloner false config.nesting.children + ++ cloner true config.nesting.clone; + + + systemBuilder = + let + kernelPath = "${config.boot.kernelPackages.kernel}/" + + "${config.system.boot.loader.kernelFile}"; + in '' + mkdir $out + + if [ ! -f ${kernelPath} ]; then + echo "The bootloader cannot find the proper kernel image." + echo "(Expecting ${kernelPath})" + false + fi + + ln -s ${kernelPath} $out/kernel + ln -s ${config.system.modulesTree} $out/kernel-modules + + ln -s ${config.system.build.initialRamdisk}/initrd $out/initrd + + echo "$activationScript" > $out/activate + substituteInPlace $out/activate --subst-var out + chmod u+x $out/activate + unset activationScript + + cp ${config.system.build.bootStage2} $out/init + substituteInPlace $out/init --subst-var-by systemConfig $out + + ln -s ${config.system.build.etc}/etc $out/etc + ln -s ${config.system.path} $out/sw + ln -s "$systemd" $out/systemd + ln -s ${config.hardware.firmware} $out/firmware + + echo -n "$kernelParams" > $out/kernel-params + echo -n "$configurationName" > $out/configuration-name + echo -n "systemd ${toString config.systemd.package.interfaceVersion}" > $out/init-interface-version + echo -n "$nixosVersion" > $out/nixos-version + + mkdir $out/fine-tune + childCount=0 + for i in $children; do + childCount=$(( childCount + 1 )) + ln -s $i $out/fine-tune/child-$childCount + done + + mkdir $out/bin + substituteAll ${./switch-to-configuration.pl} $out/bin/switch-to-configuration + chmod +x $out/bin/switch-to-configuration + + ${config.system.extraSystemBuilderCmds} + ''; + + + # Putting it all together. This builds a store path containing + # symlinks to the various parts of the built configuration (the + # kernel, the Upstart services, the init scripts, etc.) as well as a + # script `switch-to-configuration' that activates the configuration + # and makes it bootable. + system = pkgs.stdenv.mkDerivation { + name = "nixos-${config.system.nixosVersion}"; + preferLocalBuild = true; + buildCommand = systemBuilder; + + inherit (pkgs) utillinux coreutils; + systemd = config.systemd.package; + + inherit children; + kernelParams = + config.boot.kernelParams ++ config.boot.extraKernelParams; + installBootLoader = + config.system.build.installBootLoader + or "echo 'Warning: do not know how to make this configuration bootable; please enable a boot loader.' 1>&2; true"; + activationScript = config.system.activationScripts.script; + nixosVersion = config.system.nixosVersion; + + jobs = map (j: j.name) (attrValues config.jobs); + + # Pass the names of all Upstart tasks to the activation script. + tasks = attrValues (mapAttrs (n: v: if v.task then ["[${v.name}]=1"] else []) config.jobs); + + # Pass the names of all Upstart jobs that shouldn't be restarted + # to the activation script. + noRestartIfChanged = attrValues (mapAttrs (n: v: if v.restartIfChanged then [] else ["[${v.name}]=1"]) config.jobs); + + configurationName = config.boot.loader.grub.configurationName; + + # Needed by switch-to-configuration. + perl = "${pkgs.perl}/bin/perl -I${pkgs.perlPackages.FileSlurp}/lib/perl5/site_perl"; + }; + + +in + +{ + options = { + + system.build = mkOption { + default = {}; + description = '' + Attribute set of derivations used to setup the system. + ''; + }; + + nesting.children = mkOption { + default = []; + description = '' + Additional configurations to build. + ''; + }; + + nesting.clone = mkOption { + default = []; + description = '' + Additional configurations to build based on the current + configuration which is has a lower priority. + ''; + }; + + system.boot.loader.id = mkOption { + default = ""; + description = '' + Id string of the used bootloader. + ''; + }; + + system.boot.loader.kernelFile = mkOption { + default = pkgs.stdenv.platform.kernelTarget; + type = types.uniq types.string; + description = '' + Name of the kernel file to be passed to the bootloader. + ''; + }; + + system.copySystemConfiguration = mkOption { + default = false; + description = '' + If enabled, copies the NixOS configuration file + <literal>$NIXOS_CONFIG</literal> (usually + <filename>/etc/nixos/configuration.nix</filename>) + to the system store path. + ''; + }; + + system.extraSystemBuilderCmds = mkOption { + default = ""; + internal = true; + merge = concatStringsSep "\n"; + description = '' + This code will be added to the builder creating the system store path. + ''; + }; + + }; + + + config = { + + system.extraSystemBuilderCmds = + optionalString + config.system.copySystemConfiguration + "cp ${maybeEnv "NIXOS_CONFIG" "/etc/nixos/configuration.nix"} $out"; + + system.build.toplevel = system; + + }; + +} 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"; + }; + }; + + }; +} diff --git a/nixos/modules/system/etc/etc.nix b/nixos/modules/system/etc/etc.nix new file mode 100644 index 00000000000..91fcdcf2435 --- /dev/null +++ b/nixos/modules/system/etc/etc.nix @@ -0,0 +1,117 @@ +# Management of static files in /etc. + +{ config, pkgs, ... }: + +with pkgs.lib; + +let + + etc' = filter (f: f.enable) (attrValues config.environment.etc); + + etc = pkgs.stdenv.mkDerivation { + name = "etc"; + + builder = ./make-etc.sh; + + preferLocalBuild = true; + + /* !!! Use toXML. */ + sources = map (x: x.source) etc'; + targets = map (x: x.target) etc'; + modes = map (x: x.mode) etc'; + }; + +in + +{ + + ###### interface + + options = { + + environment.etc = mkOption { + type = types.loaOf types.optionSet; + default = {}; + example = + { hosts = + { source = "/nix/store/.../etc/dir/file.conf.example"; + mode = "0440"; + }; + "default/useradd".text = "GROUP=100 ..."; + }; + description = '' + Set of files that have to be linked in <filename>/etc</filename>. + ''; + + options = singleton ({ name, config, ... }: + { options = { + + enable = mkOption { + type = types.bool; + default = true; + description = '' + Whether this /etc file should be generated. This + option allows specific /etc files to be disabled. + ''; + }; + + target = mkOption { + description = '' + Name of symlink (relative to + <filename>/etc</filename>). Defaults to the attribute + name. + ''; + }; + + text = mkOption { + default = null; + type = types.nullOr types.string; + description = "Text of the file."; + }; + + source = mkOption { + types = types.path; + description = "Path of the source file."; + }; + + mode = mkOption { + default = "symlink"; + example = "0600"; + description = '' + If set to something else than <literal>symlink</literal>, + the file is copied instead of symlinked, with the given + file mode. + ''; + }; + + }; + + config = { + target = mkDefault name; + source = mkIf (config.text != null) + (mkDefault (pkgs.writeText "etc-file" config.text)); + }; + + }); + + }; + + }; + + + ###### implementation + + config = { + + system.build.etc = etc; + + system.activationScripts.etc = stringAfter [ "stdio" ] + '' + # Set up the statically computed bits of /etc. + echo "setting up /etc..." + ${pkgs.perl}/bin/perl ${./setup-etc.pl} ${etc}/etc + ''; + + }; + +} diff --git a/nixos/modules/system/etc/make-etc.sh b/nixos/modules/system/etc/make-etc.sh new file mode 100644 index 00000000000..7cf68db9ddc --- /dev/null +++ b/nixos/modules/system/etc/make-etc.sh @@ -0,0 +1,42 @@ +source $stdenv/setup + +mkdir -p $out/etc + +set -f +sources_=($sources) +targets_=($targets) +modes_=($modes) +set +f + +for ((i = 0; i < ${#targets_[@]}; i++)); do + source="${sources_[$i]}" + target="${targets_[$i]}" + + if [[ "$source" =~ '*' ]]; then + + # If the source name contains '*', perform globbing. + mkdir -p $out/etc/$target + for fn in $source; do + ln -s "$fn" $out/etc/$target/ + done + + else + + mkdir -p $out/etc/$(dirname $target) + if ! [ -e $out/etc/$target ]; then + ln -s $source $out/etc/$target + else + echo "duplicate entry $target -> $source" + if test "$(readlink $out/etc/$target)" != "$source"; then + echo "mismatched duplicate entry $(readlink $out/etc/$target) <-> $source" + exit 1 + fi + fi + + if test "${modes_[$i]}" != symlink; then + echo "${modes_[$i]}" > $out/etc/$target.mode + fi + + fi +done + diff --git a/nixos/modules/system/etc/setup-etc.pl b/nixos/modules/system/etc/setup-etc.pl new file mode 100644 index 00000000000..7cb6d2a6a45 --- /dev/null +++ b/nixos/modules/system/etc/setup-etc.pl @@ -0,0 +1,68 @@ +use strict; +use File::Find; +use File::Copy; +use File::Path; +use File::Basename; + +my $etc = $ARGV[0] or die; +my $static = "/etc/static"; + +sub atomicSymlink { + my ($source, $target) = @_; + my $tmp = "$target.tmp"; + unlink $tmp; + symlink $source, $tmp or return 1; + rename $tmp, $target or return 1; + return 1; +} + + +# Atomically update /etc/static to point at the etc files of the +# current configuration. +atomicSymlink $etc, $static or die; + + +# Remove dangling symlinks that point to /etc/static. These are +# configuration files that existed in a previous configuration but not +# in the current one. For efficiency, don't look under /etc/nixos +# (where all the NixOS sources live). +sub cleanup { + if ($File::Find::name eq "/etc/nixos") { + $File::Find::prune = 1; + return; + } + if (-l $_) { + my $target = readlink $_; + if (substr($target, 0, length $static) eq $static) { + my $x = "/etc/static/" . substr($File::Find::name, length "/etc/"); + unless (-l $x) { + print STDERR "removing obsolete symlink ‘$File::Find::name’...\n"; + unlink "$_"; + } + } + } +} + +find(\&cleanup, "/etc"); + + +# For every file in the etc tree, create a corresponding symlink in +# /etc to /etc/static. The indirection through /etc/static is to make +# switching to a new configuration somewhat more atomic. +sub link { + my $fn = substr $File::Find::name, length($etc) + 1 or next; + my $target = "/etc/$fn"; + File::Path::make_path(dirname $target); + if (-e "$_.mode") { + open MODE, "<$_.mode"; + my $mode = <MODE>; chomp $mode; + close MODE; + copy "$static/$fn", "$target.tmp" or warn; + chmod oct($mode), "$target.tmp" or warn; + rename "$target.tmp", $target or warn; + } elsif (-l "$_") { + atomicSymlink "$static/$fn", $target or warn; + } +} + +find(\&link, $etc); diff --git a/nixos/modules/system/upstart/upstart.nix b/nixos/modules/system/upstart/upstart.nix new file mode 100644 index 00000000000..5d5139b7a57 --- /dev/null +++ b/nixos/modules/system/upstart/upstart.nix @@ -0,0 +1,286 @@ +{ config, pkgs, ... }: + +with pkgs.lib; +with import ../boot/systemd-unit-options.nix { inherit config pkgs; }; + +let + + userExists = u: + (u == "") || any (uu: uu.name == u) (attrValues config.users.extraUsers); + + groupExists = g: + (g == "") || any (gg: gg.name == g) (attrValues config.users.extraGroups); + + makeJobScript = name: content: "${pkgs.writeScriptBin name content}/bin/${name}"; + + # From a job description, generate an systemd unit file. + makeUnit = job: + + let + hasMain = job.script != "" || job.exec != ""; + + env = job.environment; + + preStartScript = makeJobScript "${job.name}-pre-start" + '' + #! ${pkgs.stdenv.shell} -e + ${job.preStart} + ''; + + startScript = makeJobScript "${job.name}-start" + '' + #! ${pkgs.stdenv.shell} -e + ${if job.script != "" then job.script else '' + exec ${job.exec} + ''} + ''; + + postStartScript = makeJobScript "${job.name}-post-start" + '' + #! ${pkgs.stdenv.shell} -e + ${job.postStart} + ''; + + preStopScript = makeJobScript "${job.name}-pre-stop" + '' + #! ${pkgs.stdenv.shell} -e + ${job.preStop} + ''; + + postStopScript = makeJobScript "${job.name}-post-stop" + '' + #! ${pkgs.stdenv.shell} -e + ${job.postStop} + ''; + in { + + inherit (job) description requires before partOf environment path restartIfChanged unitConfig; + + after = + (if job.startOn == "stopped udevtrigger" then [ "systemd-udev-settle.service" ] else + if job.startOn == "started udev" then [ "systemd-udev.service" ] else + if job.startOn == "started network-interfaces" then [ "network-interfaces.target" ] else + if job.startOn == "started networking" then [ "network.target" ] else + if job.startOn == "ip-up" then [] else + if job.startOn == "" || job.startOn == "startup" then [] else + builtins.trace "Warning: job ‘${job.name}’ has unknown startOn value ‘${job.startOn}’." [] + ) ++ job.after; + + wants = + (if job.startOn == "stopped udevtrigger" then [ "systemd-udev-settle.service" ] else [] + ) ++ job.wants; + + wantedBy = + (if job.startOn == "" then [] else + if job.startOn == "ip-up" then [ "ip-up.target" ] else + [ "multi-user.target" ]) ++ job.wantedBy; + + serviceConfig = + job.serviceConfig + // optionalAttrs (job.preStart != "" && (job.script != "" || job.exec != "")) + { ExecStartPre = preStartScript; } + // optionalAttrs (job.preStart != "" && job.script == "" && job.exec == "") + { ExecStart = preStartScript; } + // optionalAttrs (job.script != "" || job.exec != "") + { ExecStart = startScript; } + // optionalAttrs (job.postStart != "") + { ExecStartPost = postStartScript; } + // optionalAttrs (job.preStop != "") + { ExecStop = preStopScript; } + // optionalAttrs (job.postStop != "") + { ExecStopPost = postStopScript; } + // (if job.script == "" && job.exec == "" then { Type = "oneshot"; RemainAfterExit = true; } else + if job.daemonType == "fork" || job.daemonType == "daemon" then { Type = "forking"; GuessMainPID = true; } else + if job.daemonType == "none" then { } else + throw "invalid daemon type `${job.daemonType}'") + // optionalAttrs (!job.task && job.respawn) + { Restart = "always"; } + // optionalAttrs job.task + { Type = "oneshot"; RemainAfterExit = false; }; + }; + + + jobOptions = serviceOptions // { + + name = mkOption { + # !!! The type should ensure that this could be a filename. + type = types.string; + example = "sshd"; + description = '' + Name of the Upstart job. + ''; + }; + + startOn = mkOption { + # !!! Re-enable this once we're on Upstart >= 0.6. + #type = types.string; + default = ""; + description = '' + The Upstart event that triggers this job to be started. + If empty, the job will not start automatically. + ''; + }; + + stopOn = mkOption { + type = types.string; + default = "starting shutdown"; + description = '' + The Upstart event that triggers this job to be stopped. + ''; + }; + + postStart = mkOption { + type = types.string; + default = ""; + description = '' + Shell commands executed after the job is started (i.e. after + the job's main process is started), but before the job is + considered “running”. + ''; + }; + + preStop = mkOption { + type = types.string; + default = ""; + description = '' + Shell commands executed before the job is stopped + (i.e. before Upstart kills the job's main process). This can + be used to cleanly shut down a daemon. + ''; + }; + + postStop = mkOption { + type = types.string; + default = ""; + description = '' + Shell commands executed after the job has stopped + (i.e. after the job's main process has terminated). + ''; + }; + + exec = mkOption { + type = types.string; + default = ""; + description = '' + Command to start the job's main process. If empty, the + job has no main process, but can still have pre/post-start + and pre/post-stop scripts, and is considered “running” + until it is stopped. + ''; + }; + + respawn = mkOption { + type = types.bool; + default = true; + description = '' + Whether to restart the job automatically if its process + ends unexpectedly. + ''; + }; + + task = mkOption { + type = types.bool; + default = false; + description = '' + Whether this job is a task rather than a service. Tasks + are executed only once, while services are restarted when + they exit. + ''; + }; + + daemonType = mkOption { + type = types.string; + default = "none"; + description = '' + Determines how Upstart detects when a daemon should be + considered “running”. The value <literal>none</literal> means + that the daemon is considered ready immediately. The value + <literal>fork</literal> means that the daemon will fork once. + The value <literal>daemon</literal> means that the daemon will + fork twice. The value <literal>stop</literal> means that the + daemon will raise the SIGSTOP signal to indicate readiness. + ''; + }; + + setuid = mkOption { + type = types.string; + check = userExists; + default = ""; + description = '' + Run the daemon as a different user. + ''; + }; + + setgid = mkOption { + type = types.string; + check = groupExists; + default = ""; + description = '' + Run the daemon as a different group. + ''; + }; + + path = mkOption { + default = []; + description = '' + Packages added to the job's <envar>PATH</envar> environment variable. + Both the <filename>bin</filename> and <filename>sbin</filename> + subdirectories of each package are added. + ''; + }; + + }; + + + upstartJob = { name, config, ... }: { + + options = { + + unit = mkOption { + default = makeUnit config; + description = "Generated definition of the systemd unit corresponding to this job."; + }; + + }; + + config = { + + # The default name is the name extracted from the attribute path. + name = mkDefaultValue name; + + }; + + }; + +in + +{ + + ###### interface + + options = { + + jobs = mkOption { + default = {}; + description = '' + This option defines the system jobs started and managed by the + Upstart daemon. + ''; + type = types.loaOf types.optionSet; + options = [ jobOptions upstartJob ]; + }; + + }; + + + ###### implementation + + config = { + + systemd.services = + flip mapAttrs' config.jobs (name: job: + nameValuePair job.name job.unit); + + }; + +} |