From 4700198654affcca0a76915e083b857e7e605cf5 Mon Sep 17 00:00:00 2001 From: nikstur Date: Thu, 19 Jan 2023 20:04:29 +0100 Subject: [PATCH] nixos/systemd-repart: init --- nixos/modules/module-list.nix | 1 + nixos/modules/system/boot/systemd/repart.nix | 101 +++++++++++++++++ nixos/tests/all-tests.nix | 1 + nixos/tests/systemd-repart.nix | 108 +++++++++++++++++++ 4 files changed, 211 insertions(+) create mode 100644 nixos/modules/system/boot/systemd/repart.nix create mode 100644 nixos/tests/systemd-repart.nix diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index dce6e878540d..35185834a6cb 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1301,6 +1301,7 @@ ./system/boot/systemd/logind.nix ./system/boot/systemd/nspawn.nix ./system/boot/systemd/oomd.nix + ./system/boot/systemd/repart.nix ./system/boot/systemd/shutdown.nix ./system/boot/systemd/tmpfiles.nix ./system/boot/systemd/user.nix diff --git a/nixos/modules/system/boot/systemd/repart.nix b/nixos/modules/system/boot/systemd/repart.nix new file mode 100644 index 000000000000..33f1b247c5ed --- /dev/null +++ b/nixos/modules/system/boot/systemd/repart.nix @@ -0,0 +1,101 @@ +{ config, pkgs, lib, ... }: + +let + cfg = config.boot.initrd.systemd.repart; + + writeDefinition = name: partitionConfig: pkgs.writeText + "${name}.conf" + (lib.generators.toINI { } { Partition = partitionConfig; }); + + listOfDefinitions = lib.mapAttrsToList + writeDefinition + (lib.filterAttrs (k: _: !(lib.hasPrefix "_" k)) cfg.partitions); + + # Create a directory in the store that contains a copy of all definition + # files. This is then passed to systemd-repart in the initrd so it can access + # the definition files after the sysroot has been mounted but before + # activation. This needs a hard copy of the files and not just symlinks + # because otherwise the files do not show up in the sysroot. + definitionsDirectory = pkgs.runCommand "systemd-repart-definitions" { } '' + mkdir -p $out + ${(lib.concatStringsSep "\n" + (map (pkg: "cp ${pkg} $out/${pkg.name}") listOfDefinitions) + )} + ''; +in +{ + options.boot.initrd.systemd.repart = { + enable = lib.mkEnableOption (lib.mdDoc "systemd-repart") // { + description = lib.mdDoc '' + Grow and add partitions to a partition table a boot time in the initrd. + systemd-repart only works with GPT partition tables. + ''; + }; + + partitions = lib.mkOption { + type = with lib.types; attrsOf (attrsOf (oneOf [ str int bool ])); + default = { }; + example = { + "10-root" = { + Type = "root"; + }; + "20-home" = { + Type = "home"; + SizeMinBytes = "512M"; + SizeMaxBytes = "2G"; + }; + }; + description = lib.mdDoc '' + Specify partitions as a set of the names of the definition files as the + key and the partition configuration as its value. The partition + configuration can use all upstream options. See + for all available options. + ''; + }; + }; + + config = lib.mkIf cfg.enable { + # Link the definitions into /etc so that they are included in the + # /nix/store of the sysroot. This also allows the user to run the + # systemd-repart binary after activation manually while automatically + # picking up the definition files. + environment.etc."repart.d".source = definitionsDirectory; + + boot.initrd.systemd = { + additionalUpstreamUnits = [ + "systemd-repart.service" + ]; + + storePaths = [ + "${config.boot.initrd.systemd.package}/bin/systemd-repart" + ]; + + # Override defaults in upstream unit. + services.systemd-repart = { + # Unset the coniditions as they cannot be met before activation because + # the definition files are not stored in the expected locations. + unitConfig.ConditionDirectoryNotEmpty = [ + " " # required to unset the previous value. + ]; + serviceConfig = { + # systemd-repart runs before the activation script. Thus we cannot + # rely on them being linked in /etc already. Instead we have to + # explicitly pass their location in the sysroot to the binary. + ExecStart = [ + " " # required to unset the previous value. + ''${config.boot.initrd.systemd.package}/bin/systemd-repart \ + --definitions=/sysroot${definitionsDirectory} \ + --dry-run=no + '' + ]; + }; + # Because the initrd does not have the `initrd-usr-fs.target` the + # upestream unit runs too early in the boot process, before the sysroot + # is available. However, systemd-repart needs access to the sysroot to + # find the definition files. + after = [ "sysroot.mount" ]; + }; + }; + }; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 9bf85cd0b97d..fd21f7a0deea 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -656,6 +656,7 @@ in { systemd-nspawn = handleTest ./systemd-nspawn.nix {}; systemd-oomd = handleTest ./systemd-oomd.nix {}; systemd-portabled = handleTest ./systemd-portabled.nix {}; + systemd-repart = handleTest ./systemd-repart.nix {}; systemd-shutdown = handleTest ./systemd-shutdown.nix {}; systemd-timesyncd = handleTest ./systemd-timesyncd.nix {}; systemd-user-tmpfiles-rules = handleTest ./systemd-user-tmpfiles-rules.nix {}; diff --git a/nixos/tests/systemd-repart.nix b/nixos/tests/systemd-repart.nix new file mode 100644 index 000000000000..92cc1fb04edc --- /dev/null +++ b/nixos/tests/systemd-repart.nix @@ -0,0 +1,108 @@ +{ system ? builtins.currentSystem +, config ? { } +, pkgs ? import ../.. { inherit system config; } +}: + +with import ../lib/testing-python.nix { inherit system pkgs; }; +with pkgs.lib; + +let + # A testScript fragment that prepares a disk with some empty, unpartitioned + # space. and uses it to boot the test with. Takes a single argument `machine` + # from which the diskImage is extraced. + useDiskImage = machine: '' + import os + import shutil + import subprocess + import tempfile + + tmp_disk_image = tempfile.NamedTemporaryFile() + + shutil.copyfile("${machine.system.build.diskImage}/nixos.img", tmp_disk_image.name) + + subprocess.run([ + "${pkgs.qemu}/bin/qemu-img", + "resize", + "-f", + "raw", + tmp_disk_image.name, + "+32M", + ]) + + # Fix the GPT table by moving the backup table to the end of the enlarged + # disk image. This is necessary because we increased the size of the disk + # before. The disk needs to be a raw disk because sgdisk can only run on + # raw images. + subprocess.run([ + "${pkgs.gptfdisk}/bin/sgdisk", + "--move-second-header", + tmp_disk_image.name, + ]) + + # Set NIX_DISK_IMAGE so that the qemu script finds the right disk image. + os.environ['NIX_DISK_IMAGE'] = tmp_disk_image.name + ''; + + common = { config, pkgs, lib, ... }: { + virtualisation.useDefaultFilesystems = false; + virtualisation.fileSystems = { + "/" = { + device = "/dev/vda2"; + fsType = "ext4"; + }; + }; + + boot.initrd.systemd.enable = true; + boot.initrd.systemd.repart.enable = true; + + # systemd-repart operates on disks with a partition table. The qemu module, + # however, creates separate filesystem images without a partition table, so + # we have to create a disk image manually. + # + # This creates two partitions, an ESP mounted on /dev/vda1 and the root + # partition mounted on /dev/vda2 + system.build.diskImage = import ../lib/make-disk-image.nix { + inherit config pkgs lib; + # Use a raw format disk so that it can be resized before starting the + # test VM. + format = "raw"; + # Keep the image as small as possible but leave some room for changes. + bootSize = "32M"; + additionalSpace = "0M"; + # GPT with an EFI System Partition is the typical use case for + # systemd-repart because it does not support MBR. + partitionTableType = "efi"; + # We do not actually care much about the content of the partitions, so we + # do not need a bootloader installed. + installBootLoader = false; + # Improve determinism by not copying a channel. + copyChannel = false; + }; + }; +in +{ + basic = makeTest { + name = "systemd-repart"; + meta.maintainers = with maintainers; [ nikstur ]; + + nodes.machine = { config, pkgs, ... }: { + imports = [ common ]; + + boot.initrd.systemd.repart.partitions = { + "10-root" = { + Type = "linux-generic"; + }; + }; + }; + + testScript = { nodes, ... }: '' + ${useDiskImage nodes.machine} + + machine.start() + machine.wait_for_unit("multi-user.target") + + systemd_repart_logs = machine.succeed("journalctl --boot --unit systemd-repart.service") + assert "Growing existing partition 1." in systemd_repart_logs + ''; + }; +}