diff options
Diffstat (limited to 'nixos/modules/system/etc')
-rw-r--r-- | nixos/modules/system/etc/etc-activation.nix | 12 | ||||
-rw-r--r-- | nixos/modules/system/etc/etc.nix | 201 | ||||
-rw-r--r-- | nixos/modules/system/etc/setup-etc.pl | 146 | ||||
-rw-r--r-- | nixos/modules/system/etc/test.nix | 70 |
4 files changed, 429 insertions, 0 deletions
diff --git a/nixos/modules/system/etc/etc-activation.nix b/nixos/modules/system/etc/etc-activation.nix new file mode 100644 index 00000000000..78010495018 --- /dev/null +++ b/nixos/modules/system/etc/etc-activation.nix @@ -0,0 +1,12 @@ +{ config, lib, ... }: +let + inherit (lib) stringAfter; +in { + + imports = [ ./etc.nix ]; + + config = { + system.activationScripts.etc = + stringAfter [ "users" "groups" ] config.system.build.etcActivationCommands; + }; +} diff --git a/nixos/modules/system/etc/etc.nix b/nixos/modules/system/etc/etc.nix new file mode 100644 index 00000000000..ed552fecec5 --- /dev/null +++ b/nixos/modules/system/etc/etc.nix @@ -0,0 +1,201 @@ +# Management of static files in /etc. + +{ config, lib, pkgs, ... }: + +with lib; + +let + + etc' = filter (f: f.enable) (attrValues config.environment.etc); + + etc = pkgs.runCommandLocal "etc" { + # This is needed for the systemd module + passthru.targets = map (x: x.target) etc'; + } /* sh */ '' + set -euo pipefail + + makeEtcEntry() { + src="$1" + target="$2" + mode="$3" + user="$4" + group="$5" + + if [[ "$src" = *'*'* ]]; then + # If the source name contains '*', perform globbing. + mkdir -p "$out/etc/$target" + for fn in $src; do + ln -s "$fn" "$out/etc/$target/" + done + else + + mkdir -p "$out/etc/$(dirname "$target")" + if ! [ -e "$out/etc/$target" ]; then + ln -s "$src" "$out/etc/$target" + else + echo "duplicate entry $target -> $src" + if [ "$(readlink "$out/etc/$target")" != "$src" ]; then + echo "mismatched duplicate entry $(readlink "$out/etc/$target") <-> $src" + ret=1 + + continue + fi + fi + + if [ "$mode" != symlink ]; then + echo "$mode" > "$out/etc/$target.mode" + echo "$user" > "$out/etc/$target.uid" + echo "$group" > "$out/etc/$target.gid" + fi + fi + } + + mkdir -p "$out/etc" + ${concatMapStringsSep "\n" (etcEntry: escapeShellArgs [ + "makeEtcEntry" + # Force local source paths to be added to the store + "${etcEntry.source}" + etcEntry.target + etcEntry.mode + etcEntry.user + etcEntry.group + ]) etc'} + ''; + +in + +{ + + imports = [ ../build.nix ]; + + ###### interface + + options = { + + environment.etc = mkOption { + default = {}; + example = literalExpression '' + { example-configuration-file = + { 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>. + ''; + + type = with types; attrsOf (submodule ( + { name, config, options, ... }: + { 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 { + type = types.str; + description = '' + Name of symlink (relative to + <filename>/etc</filename>). Defaults to the attribute + name. + ''; + }; + + text = mkOption { + default = null; + type = types.nullOr types.lines; + description = "Text of the file."; + }; + + source = mkOption { + type = types.path; + description = "Path of the source file."; + }; + + mode = mkOption { + type = types.str; + 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. + ''; + }; + + uid = mkOption { + default = 0; + type = types.int; + description = '' + UID of created file. Only takes effect when the file is + copied (that is, the mode is not 'symlink'). + ''; + }; + + gid = mkOption { + default = 0; + type = types.int; + description = '' + GID of created file. Only takes effect when the file is + copied (that is, the mode is not 'symlink'). + ''; + }; + + user = mkOption { + default = "+${toString config.uid}"; + type = types.str; + description = '' + User name of created file. + Only takes effect when the file is copied (that is, the mode is not 'symlink'). + Changing this option takes precedence over <literal>uid</literal>. + ''; + }; + + group = mkOption { + default = "+${toString config.gid}"; + type = types.str; + description = '' + Group name of created file. + Only takes effect when the file is copied (that is, the mode is not 'symlink'). + Changing this option takes precedence over <literal>gid</literal>. + ''; + }; + + }; + + config = { + target = mkDefault name; + source = mkIf (config.text != null) ( + let name' = "etc-" + baseNameOf name; + in mkDerivedConfig options.text (pkgs.writeText name') + ); + }; + + })); + + }; + + }; + + + ###### implementation + + config = { + + system.build.etc = etc; + system.build.etcActivationCommands = + '' + # Set up the statically computed bits of /etc. + echo "setting up /etc..." + ${pkgs.perl.withPackages (p: [ p.FileSlurp ])}/bin/perl ${./setup-etc.pl} ${etc}/etc + ''; + }; + +} diff --git a/nixos/modules/system/etc/setup-etc.pl b/nixos/modules/system/etc/setup-etc.pl new file mode 100644 index 00000000000..be6b2d9ae71 --- /dev/null +++ b/nixos/modules/system/etc/setup-etc.pl @@ -0,0 +1,146 @@ +use strict; +use File::Find; +use File::Copy; +use File::Path; +use File::Basename; +use File::Slurp; + +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 0; + rename $tmp, $target or return 0; + return 1; +} + + +# Atomically update /etc/static to point at the etc files of the +# current configuration. +atomicSymlink $etc, $static or die; + +# Returns 1 if the argument points to the files in /etc/static. That +# means either argument is a symlink to a file in /etc/static or a +# directory with all children being static. +sub isStatic { + my $path = shift; + + if (-l $path) { + my $target = readlink $path; + return substr($target, 0, length "/etc/static/") eq "/etc/static/"; + } + + if (-d $path) { + opendir DIR, "$path" or return 0; + my @names = readdir DIR or die; + closedir DIR; + + foreach my $name (@names) { + next if $name eq "." || $name eq ".."; + unless (isStatic("$path/$name")) { + return 0; + } + } + return 1; + } + + return 0; +} + +# 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"); + + +# Use /etc/.clean to keep track of copied files. +my @oldCopied = read_file("/etc/.clean", chomp => 1, err_mode => 'quiet'); +open CLEAN, ">>/etc/.clean"; + + +# 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. +my %created; +my @copied; + +sub link { + my $fn = substr $File::Find::name, length($etc) + 1 or next; + my $target = "/etc/$fn"; + File::Path::make_path(dirname $target); + $created{$fn} = 1; + + # Rename doesn't work if target is directory. + if (-l $_ && -d $target) { + if (isStatic $target) { + rmtree $target or warn; + } else { + warn "$target directory contains user files. Symlinking may fail."; + } + } + + if (-e "$_.mode") { + my $mode = read_file("$_.mode"); chomp $mode; + if ($mode eq "direct-symlink") { + atomicSymlink readlink("$static/$fn"), $target or warn; + } else { + my $uid = read_file("$_.uid"); chomp $uid; + my $gid = read_file("$_.gid"); chomp $gid; + copy "$static/$fn", "$target.tmp" or warn; + $uid = getpwnam $uid unless $uid =~ /^\+/; + $gid = getgrnam $gid unless $gid =~ /^\+/; + chown int($uid), int($gid), "$target.tmp" or warn; + chmod oct($mode), "$target.tmp" or warn; + rename "$target.tmp", $target or warn; + } + push @copied, $fn; + print CLEAN "$fn\n"; + } elsif (-l "$_") { + atomicSymlink "$static/$fn", $target or warn; + } +} + +find(\&link, $etc); + + +# Delete files that were copied in a previous version but not in the +# current. +foreach my $fn (@oldCopied) { + if (!defined $created{$fn}) { + $fn = "/etc/$fn"; + print STDERR "removing obsolete file ‘$fn’...\n"; + unlink "$fn"; + } +} + + +# Rewrite /etc/.clean. +close CLEAN; +write_file("/etc/.clean", map { "$_\n" } @copied); + +# Create /etc/NIXOS tag if not exists. +# When /etc is not on a persistent filesystem, it will be wiped after reboot, +# so we need to check and re-create it during activation. +open TAG, ">>/etc/NIXOS"; +close TAG; diff --git a/nixos/modules/system/etc/test.nix b/nixos/modules/system/etc/test.nix new file mode 100644 index 00000000000..5e43b155038 --- /dev/null +++ b/nixos/modules/system/etc/test.nix @@ -0,0 +1,70 @@ +{ lib +, coreutils +, fakechroot +, fakeroot +, evalMinimalConfig +, pkgsModule +, runCommand +, util-linux +, vmTools +, writeText +}: +let + node = evalMinimalConfig ({ config, ... }: { + imports = [ pkgsModule ../etc/etc.nix ]; + environment.etc."passwd" = { + text = passwdText; + }; + environment.etc."hosts" = { + text = hostsText; + mode = "0751"; + }; + }); + passwdText = '' + root:x:0:0:System administrator:/root:/run/current-system/sw/bin/bash + ''; + hostsText = '' + 127.0.0.1 localhost + ::1 localhost + # testing... + ''; +in +lib.recurseIntoAttrs { + test-etc-vm = + vmTools.runInLinuxVM (runCommand "test-etc-vm" { } '' + mkdir -p /etc + ${node.config.system.build.etcActivationCommands} + set -x + [[ -L /etc/passwd ]] + diff /etc/passwd ${writeText "expected-passwd" passwdText} + [[ 751 = $(stat --format %a /etc/hosts) ]] + diff /etc/hosts ${writeText "expected-hosts" hostsText} + set +x + touch $out + ''); + + # fakeroot is behaving weird + test-etc-fakeroot = + runCommand "test-etc" + { + nativeBuildInputs = [ + fakeroot + fakechroot + # for chroot + coreutils + # fakechroot needs getopt, which is provided by util-linux + util-linux + ]; + fakeRootCommands = '' + mkdir -p /etc + ${node.config.system.build.etcActivationCommands} + diff /etc/hosts ${writeText "expected-hosts" hostsText} + touch $out + ''; + } '' + mkdir fake-root + export FAKECHROOT_EXCLUDE_PATH=/dev:/proc:/sys:${builtins.storeDir}:$out + fakechroot fakeroot chroot $PWD/fake-root bash -c 'source $stdenv/setup; eval "$fakeRootCommands"' + ''; + +} |