at 25.11-pre 14 kB view raw
1# Management of static files in /etc. 2{ 3 config, 4 lib, 5 pkgs, 6 ... 7}: 8let 9 10 etc' = lib.filter (f: f.enable) (lib.attrValues config.environment.etc); 11 12 etc = 13 pkgs.runCommandLocal "etc" 14 { 15 # This is needed for the systemd module 16 passthru.targets = map (x: x.target) etc'; 17 } # sh 18 '' 19 set -euo pipefail 20 21 makeEtcEntry() { 22 src="$1" 23 target="$2" 24 mode="$3" 25 user="$4" 26 group="$5" 27 28 if [[ "$src" = *'*'* ]]; then 29 # If the source name contains '*', perform globbing. 30 mkdir -p "$out/etc/$target" 31 for fn in $src; do 32 ln -s "$fn" "$out/etc/$target/" 33 done 34 else 35 36 mkdir -p "$out/etc/$(dirname "$target")" 37 if ! [ -e "$out/etc/$target" ]; then 38 ln -s "$src" "$out/etc/$target" 39 else 40 echo "duplicate entry $target -> $src" 41 if [ "$(readlink "$out/etc/$target")" != "$src" ]; then 42 echo "mismatched duplicate entry $(readlink "$out/etc/$target") <-> $src" 43 ret=1 44 fi 45 fi 46 47 if [ "$mode" != symlink ]; then 48 echo "$mode" > "$out/etc/$target.mode" 49 echo "$user" > "$out/etc/$target.uid" 50 echo "$group" > "$out/etc/$target.gid" 51 fi 52 fi 53 } 54 55 mkdir -p "$out/etc" 56 ${lib.concatMapStringsSep "\n" ( 57 etcEntry: 58 lib.escapeShellArgs [ 59 "makeEtcEntry" 60 # Force local source paths to be added to the store 61 "${etcEntry.source}" 62 etcEntry.target 63 etcEntry.mode 64 etcEntry.user 65 etcEntry.group 66 ] 67 ) etc'} 68 ''; 69 70 etcHardlinks = lib.filter (f: f.mode != "symlink" && f.mode != "direct-symlink") etc'; 71 72in 73 74{ 75 76 imports = [ ../build.nix ]; 77 78 ###### interface 79 80 options = { 81 82 system.etc.overlay = { 83 enable = lib.mkOption { 84 type = lib.types.bool; 85 default = false; 86 description = '' 87 Mount `/etc` as an overlayfs instead of generating it via a perl script. 88 89 Note: This is currently experimental. Only enable this option if you're 90 confident that you can recover your system if it breaks. 91 ''; 92 }; 93 94 mutable = lib.mkOption { 95 type = lib.types.bool; 96 default = true; 97 description = '' 98 Whether to mount `/etc` mutably (i.e. read-write) or immutably (i.e. read-only). 99 100 If this is false, only the immutable lowerdir is mounted. If it is 101 true, a writable upperdir is mounted on top. 102 ''; 103 }; 104 }; 105 106 environment.etc = lib.mkOption { 107 default = { }; 108 example = lib.literalExpression '' 109 { example-configuration-file = 110 { source = "/nix/store/.../etc/dir/file.conf.example"; 111 mode = "0440"; 112 }; 113 "default/useradd".text = "GROUP=100 ..."; 114 } 115 ''; 116 description = '' 117 Set of files that have to be linked in {file}`/etc`. 118 ''; 119 120 type = 121 with lib.types; 122 attrsOf ( 123 submodule ( 124 { 125 name, 126 config, 127 options, 128 ... 129 }: 130 { 131 options = { 132 133 enable = lib.mkOption { 134 type = lib.types.bool; 135 default = true; 136 description = '' 137 Whether this /etc file should be generated. This 138 option allows specific /etc files to be disabled. 139 ''; 140 }; 141 142 target = lib.mkOption { 143 type = lib.types.str; 144 description = '' 145 Name of symlink (relative to 146 {file}`/etc`). Defaults to the attribute 147 name. 148 ''; 149 }; 150 151 text = lib.mkOption { 152 default = null; 153 type = lib.types.nullOr lib.types.lines; 154 description = "Text of the file."; 155 }; 156 157 source = lib.mkOption { 158 type = lib.types.path; 159 description = "Path of the source file."; 160 }; 161 162 mode = lib.mkOption { 163 type = lib.types.str; 164 default = "symlink"; 165 example = "0600"; 166 description = '' 167 If set to something else than `symlink`, 168 the file is copied instead of symlinked, with the given 169 file mode. 170 ''; 171 }; 172 173 uid = lib.mkOption { 174 default = 0; 175 type = lib.types.int; 176 description = '' 177 UID of created file. Only takes effect when the file is 178 copied (that is, the mode is not 'symlink'). 179 ''; 180 }; 181 182 gid = lib.mkOption { 183 default = 0; 184 type = lib.types.int; 185 description = '' 186 GID of created file. Only takes effect when the file is 187 copied (that is, the mode is not 'symlink'). 188 ''; 189 }; 190 191 user = lib.mkOption { 192 default = "+${toString config.uid}"; 193 type = lib.types.str; 194 description = '' 195 User name of file owner. 196 197 Only takes effect when the file is copied (that is, the 198 mode is not `symlink`). 199 200 When `services.userborn.enable`, this option has no effect. 201 You have to assign a `uid` instead. Otherwise this option 202 takes precedence over `uid`. 203 ''; 204 }; 205 206 group = lib.mkOption { 207 default = "+${toString config.gid}"; 208 type = lib.types.str; 209 description = '' 210 Group name of file owner. 211 212 Only takes effect when the file is copied (that is, the 213 mode is not `symlink`). 214 215 When `services.userborn.enable`, this option has no effect. 216 You have to assign a `gid` instead. Otherwise this option 217 takes precedence over `gid`. 218 ''; 219 }; 220 221 }; 222 223 config = { 224 target = lib.mkDefault name; 225 source = lib.mkIf (config.text != null) ( 226 let 227 name' = "etc-" + lib.replaceStrings [ "/" ] [ "-" ] name; 228 in 229 lib.mkDerivedConfig options.text (pkgs.writeText name') 230 ); 231 }; 232 233 } 234 ) 235 ); 236 237 }; 238 239 }; 240 241 ###### implementation 242 243 config = { 244 245 system.build.etc = etc; 246 system.build.etcActivationCommands = 247 let 248 etcOverlayOptions = lib.concatStringsSep "," ( 249 [ 250 "relatime" 251 "redirect_dir=on" 252 "metacopy=on" 253 ] 254 ++ lib.optionals config.system.etc.overlay.mutable [ 255 "upperdir=/.rw-etc/upper" 256 "workdir=/.rw-etc/work" 257 ] 258 ); 259 in 260 if config.system.etc.overlay.enable then 261 #bash 262 '' 263 # This script atomically remounts /etc when switching configuration. 264 # On a (re-)boot this should not run because /etc is mounted via a 265 # systemd mount unit instead. 266 # The activation script can also be called in cases where we didn't have 267 # an initrd though, like for instance when using nixos-enter, 268 # so we cannot assume that /etc has already been mounted. 269 # 270 # To a large extent this mimics what composefs does. Because 271 # it's relatively simple, however, we avoid the composefs dependency. 272 # Since this script is not idempotent, it should not run when etc hasn't 273 # changed. 274 if [[ ! $IN_NIXOS_SYSTEMD_STAGE1 ]] && [[ "${config.system.build.etc}/etc" != "$(readlink -f /run/current-system/etc)" ]]; then 275 echo "remounting /etc..." 276 277 ${lib.optionalString config.system.etc.overlay.mutable '' 278 # These directories are usually created in initrd, 279 # but we need to create them here when we're called directly, 280 # for instance by nixos-enter 281 mkdir --parents /.rw-etc/upper /.rw-etc/work 282 chmod 0755 /.rw-etc /.rw-etc/upper /.rw-etc/work 283 ''} 284 285 tmpMetadataMount=$(TMPDIR="/run" mktemp --directory -t nixos-etc-metadata.XXXXXXXXXX) 286 mount --type erofs -o ro ${config.system.build.etcMetadataImage} $tmpMetadataMount 287 288 # There was no previous /etc mounted. This happens when we're called 289 # directly without an initrd, like with nixos-enter. 290 if ! mountpoint -q /etc; then 291 mount --type overlay overlay \ 292 --options lowerdir=$tmpMetadataMount::${config.system.build.etcBasedir},${etcOverlayOptions} \ 293 /etc 294 else 295 # Mount the new /etc overlay to a temporary private mount. 296 # This needs the indirection via a private bind mount because you 297 # cannot move shared mounts. 298 tmpEtcMount=$(TMPDIR="/run" mktemp --directory -t nixos-etc.XXXXXXXXXX) 299 mount --bind --make-private $tmpEtcMount $tmpEtcMount 300 mount --type overlay overlay \ 301 --options lowerdir=$tmpMetadataMount::${config.system.build.etcBasedir},${etcOverlayOptions} \ 302 $tmpEtcMount 303 304 # Before moving the new /etc overlay under the old /etc, we have to 305 # move mounts on top of /etc to the new /etc mountpoint. 306 findmnt /etc --submounts --list --noheading --kernel --output TARGET | while read -r mountPoint; do 307 if [[ "$mountPoint" = "/etc" ]]; then 308 continue 309 fi 310 311 tmpMountPoint="$tmpEtcMount/''${mountPoint:5}" 312 ${ 313 if config.system.etc.overlay.mutable then 314 '' 315 if [[ -f "$mountPoint" ]]; then 316 touch "$tmpMountPoint" 317 elif [[ -d "$mountPoint" ]]; then 318 mkdir -p "$tmpMountPoint" 319 fi 320 '' 321 else 322 '' 323 if [[ ! -e "$tmpMountPoint" ]]; then 324 echo "Skipping undeclared mountpoint in environment.etc: $mountPoint" 325 continue 326 fi 327 '' 328 } 329 mount --bind "$mountPoint" "$tmpMountPoint" 330 done 331 332 # Move the new temporary /etc mount underneath the current /etc mount. 333 # 334 # This should eventually use util-linux to perform this move beneath, 335 # however, this functionality is not yet in util-linux. See this 336 # tracking issue: https://github.com/util-linux/util-linux/issues/2604 337 ${pkgs.move-mount-beneath}/bin/move-mount --move --beneath $tmpEtcMount /etc 338 339 # Unmount the top /etc mount to atomically reveal the new mount. 340 umount --lazy --recursive /etc 341 342 # Unmount the temporary mount 343 umount --lazy "$tmpEtcMount" 344 rmdir "$tmpEtcMount" 345 fi 346 347 # Unmount old metadata mounts 348 # For some reason, `findmnt /tmp --submounts` does not show the nested 349 # mounts. So we'll just find all mounts of type erofs and filter on the 350 # name of the mountpoint. 351 findmnt --type erofs --list --kernel --output TARGET | while read -r mountPoint; do 352 if [[ ("$mountPoint" =~ ^/run/nixos-etc-metadata\..{10}$ || "$mountPoint" =~ ^/run/nixos-etc-metadata$ ) && 353 "$mountPoint" != "$tmpMetadataMount" ]]; then 354 umount --lazy "$mountPoint" 355 rmdir "$mountPoint" 356 fi 357 done 358 fi 359 '' 360 else 361 '' 362 # Set up the statically computed bits of /etc. 363 echo "setting up /etc..." 364 ${pkgs.perl.withPackages (p: [ p.FileSlurp ])}/bin/perl ${./setup-etc.pl} ${etc}/etc 365 ''; 366 367 system.build.etcBasedir = pkgs.runCommandLocal "etc-lowerdir" { } '' 368 set -euo pipefail 369 370 makeEtcEntry() { 371 src="$1" 372 target="$2" 373 374 mkdir -p "$out/$(dirname "$target")" 375 cp "$src" "$out/$target" 376 } 377 378 mkdir -p "$out" 379 ${lib.concatMapStringsSep "\n" ( 380 etcEntry: 381 lib.escapeShellArgs [ 382 "makeEtcEntry" 383 # Force local source paths to be added to the store 384 "${etcEntry.source}" 385 etcEntry.target 386 ] 387 ) etcHardlinks} 388 ''; 389 390 system.build.etcMetadataImage = 391 let 392 etcJson = pkgs.writeText "etc-json" (builtins.toJSON etc'); 393 etcDump = pkgs.runCommand "etc-dump" { } '' 394 ${lib.getExe pkgs.buildPackages.python3} ${./build-composefs-dump.py} ${etcJson} > $out 395 ''; 396 in 397 pkgs.runCommand "etc-metadata.erofs" 398 { 399 nativeBuildInputs = with pkgs.buildPackages; [ 400 composefs 401 erofs-utils 402 ]; 403 } 404 '' 405 mkcomposefs --from-file ${etcDump} $out 406 fsck.erofs $out 407 ''; 408 409 }; 410 411}