From bde7dd352c07d43bd5b8245e6c39074a391fdd46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Tue, 27 Feb 2024 20:41:22 +0100 Subject: [PATCH] add disko-install command disko-install combines disko and nixos-install into a single command. Example usage: sudo ./disko-install --flake /someflake#eva --disk sda /dev/zvol/zroot/test --- disko-install | 169 ++++++++++++++++++++++++++ disko-install.nix | 26 ++++ flake.nix | 17 ++- install-cli.nix | 43 +++++++ tests/disko-install/configuration.nix | 23 ++++ tests/disko-install/default.nix | 45 +++++++ 6 files changed, 322 insertions(+), 1 deletion(-) create mode 100755 disko-install create mode 100644 disko-install.nix create mode 100644 install-cli.nix create mode 100644 tests/disko-install/configuration.nix create mode 100644 tests/disko-install/default.nix diff --git a/disko-install b/disko-install new file mode 100755 index 0000000..c703167 --- /dev/null +++ b/disko-install @@ -0,0 +1,169 @@ +#!/usr/bin/env bash + +set -euo pipefail + +showUsage() { + cat <&2 + exit 1 + fi + case $2 in + format) + diskoAttr=diskoScript + ;; + mount) + diskoAttr=mountScript + ;; + *) + echo "Invalid mode: $2" >&2 + echo "Valid modes are: format, mount" >&2 + exit 1 + ;; + esac + mode=$2 + shift + ;; + --option) + if [[ $# -lt 3 ]]; then + echo "Option $1 requires an argument" >&2 + exit 1 + fi + nix_args+=(--option "$2" "$3") + shift + shift + ;; + --disk) + if [[ $# -lt 3 ]]; then + echo "Option $1 requires an argument" >&2 + exit 1 + fi + # shellcheck disable=SC2034 + diskMappings[$2]=$3 + shift + shift + ;; + *) + echo "Unknown option: $1" >&2 + showUsage + exit 1 + ;; + esac + shift + done +} + +cleanupMountPoint() { + mountPoint=$1 + if mountpoint -q "${mountPoint}"; then + umount -R "${mountPoint}" + fi + rmdir "${mountPoint}" +} + +main() { + parseArgs "$@" + + if [[ -z ${flake-} ]]; then + echo "Please specify the flake-uri with --flake to use for installation." >&2 + exit 1 + fi + + if [[ $flake =~ ^(.*)\#([^\#\"]*)$ ]]; then + flake="${BASH_REMATCH[1]}" + flakeAttr="${BASH_REMATCH[2]}" + fi + if [[ -z ${flakeAttr-} ]]; then + echo "Please specify the name of the NixOS configuration to be installed, as a URI fragment in the flake-uri." >&2 + echo 'For example, to use the output nixosConfigurations.foo from the flake.nix, append "#foo" to the flake-uri.' >&2 + exit 1 + fi + + mountPoint=$(mktemp -d) + chmod 755 "${mountPoint}" # bchachefs wants 755 + escapeMountPoint=$(printf '%q' "$mountPoint") + # shellcheck disable=SC2064 + trap "cleanupMountPoint ${escapeMountPoint}" EXIT + + IFS=$'\n' mapfile -t artifacts < <(nix-build "${libexec_dir}"/install-cli.nix \ + "${nix_args[@]}" \ + --no-out-link \ + --impure \ + --argstr flake "$flake" \ + --argstr flakeAttr "$flakeAttr" \ + --argstr rootMountPoint "$mountPoint" \ + --arg diskMappings "$(serialiaseArrayToNix diskMappings)" \ + -A installToplevel \ + -A "$diskoAttr") + nixos_system=${artifacts[0]} + disko_script=${artifacts[1]} + + if [[ -n ${dry_run-} ]]; then + echo "Would run: $disko_script" + echo "Would run: nixos-install --system '$nixos_system' --root '$mountPoint'" + exit 0 + fi + + "$disko_script" + nixos-install --no-root-password --system "$nixos_system" --root "$mountPoint" +} + +main "$@" diff --git a/disko-install.nix b/disko-install.nix new file mode 100644 index 0000000..48ff6ac --- /dev/null +++ b/disko-install.nix @@ -0,0 +1,26 @@ +{ stdenvNoCC, makeWrapper, lib }: + +stdenvNoCC.mkDerivation { + name = "disko-install"; + src = ./.; + nativeBuildInputs = [ + makeWrapper + ]; + installPhase = '' + mkdir -p $out/bin $out/share/disko + cp -r install-cli.nix $out/share/disko + sed \ + -e "s|libexec_dir=\".*\"|libexec_dir=\"$out/share/disko\"|" \ + -e "s|#!/usr/bin/env.*|#!/usr/bin/env bash|" \ + disko-install > $out/bin/disko-install + chmod 755 $out/bin/disko-install + wrapProgram $out/bin/disko-install + ''; + meta = with lib; { + description = "Disko and nixos-install in one command"; + homepage = "https://github.com/nix-community/disko"; + license = licenses.mit; + maintainers = with maintainers; [ lassulus ]; + platforms = platforms.linux; + }; +} diff --git a/flake.nix b/flake.nix index 1543270..168350c 100644 --- a/flake.nix +++ b/flake.nix @@ -28,6 +28,7 @@ in { disko = pkgs.callPackage ./package.nix { }; + disko-install = pkgs.callPackage ./disko-install.nix { }; default = self.packages.${system}.disko; } // pkgs.lib.optionalAttrs (!pkgs.buildPlatform.isRiscV64) { disko-doc = pkgs.callPackage ./doc.nix { }; @@ -43,6 +44,11 @@ makeTest = import (pkgs.path + "/nixos/tests/make-test-python.nix"); eval-config = import (pkgs.path + "/nixos/lib/eval-config.nix"); }); + + disko-install = pkgs.callPackage ./tests/disko-install { + inherit self; + }; + shellcheck = pkgs.runCommand "shellcheck" { nativeBuildInputs = [ pkgs.shellcheck ]; } '' cd ${./.} shellcheck disk-deactivate/disk-deactivate disko @@ -50,8 +56,17 @@ ''; in # FIXME: aarch64-linux seems to hang on boot - nixpkgs.lib.optionalAttrs pkgs.hostPlatform.isx86_64 nixosTests // + nixpkgs.lib.optionalAttrs pkgs.hostPlatform.isx86_64 (nixosTests // { inherit disko-install; }) // pkgs.lib.optionalAttrs (!pkgs.buildPlatform.isRiscV64 && !pkgs.hostPlatform.isx86_32) { inherit shellcheck; }); + + nixosConfigurations.testmachine = nixpkgs.lib.nixosSystem { + system = "x86_64-linux"; + modules = [ + ./tests/disko-install/configuration.nix + ./example/hybrid.nix + ./module.nix + ]; + }; formatter = forAllSystems (system: let pkgs = nixpkgs.legacyPackages.${system}; diff --git a/install-cli.nix b/install-cli.nix new file mode 100644 index 0000000..dec7394 --- /dev/null +++ b/install-cli.nix @@ -0,0 +1,43 @@ +{ flake +, flakeAttr +, diskMappings +, canTouchEfiVariables ? false +, rootMountPoint ? "/mnt" +}: +let + originalSystem = (builtins.getFlake "${flake}").nixosConfigurations."${flakeAttr}"; + diskoSystem = + let + lib = originalSystem.lib; + + modifiedDisks = builtins.mapAttrs + (name: value: let + dev = if diskMappings ? ${name} then + diskMappings.${name} + else + throw "No device passed for disk '${name}'"; + in value // { + device = dev; + content = value.content // { device = dev; }; + }) + originalSystem.config.disko.devices.disk; + + cleanedDisks = lib.filterAttrsRecursive (n: _: !lib.hasPrefix "_" n) modifiedDisks; + in + originalSystem.extendModules { + modules = [{ + disko.rootMountPoint = rootMountPoint; + disko.devices.disk = lib.mkVMOverride cleanedDisks; + }]; + }; + installSystem = originalSystem.extendModules { + modules = [({ lib, ... }: { + boot.loader.efi.canTouchEfiVariables = lib.mkVMOverride canTouchEfiVariables; + boot.loader.grub.devices = lib.mkVMOverride diskoSystem.config.boot.loader.grub.devices; + })]; + }; +in +{ + installToplevel = installSystem.config.system.build.toplevel; + inherit (diskoSystem.config.system.build) formatScript mountScript diskoScript; +} diff --git a/tests/disko-install/configuration.nix b/tests/disko-install/configuration.nix new file mode 100644 index 0000000..41e33e0 --- /dev/null +++ b/tests/disko-install/configuration.nix @@ -0,0 +1,23 @@ +{ lib, pkgs, modulesPath, ... }: { + imports = [ + (modulesPath + "/testing/test-instrumentation.nix") + (modulesPath + "/profiles/qemu-guest.nix") + (modulesPath + "/profiles/minimal.nix") + ]; + + networking.hostName = "disko-machine"; + + # do not try to fetch stuff from the internet + nix.settings = { + substituters = lib.mkForce [ ]; + hashed-mirrors = null; + connect-timeout = 3; + flake-registry = pkgs.writeText "flake-registry" ''{"flakes":[],"version":2}''; + experimental-features = [ "nix-command" "flakes" ]; + }; + services.openssh.enable = true; + boot.kernelParams = [ "console=tty0" ] ++ + (lib.optional (pkgs.stdenv.hostPlatform.isAarch) "ttyAMA0,115200") ++ + (lib.optional (pkgs.stdenv.hostPlatform.isRiscV64) "ttySIF0,115200") ++ + [ "console=ttyS0,115200" ]; +} diff --git a/tests/disko-install/default.nix b/tests/disko-install/default.nix new file mode 100644 index 0000000..fad18fd --- /dev/null +++ b/tests/disko-install/default.nix @@ -0,0 +1,45 @@ +{ pkgs ? import { }, self }: +let + disko-install = pkgs.callPackage ../../disko-install.nix { }; + toplevel = self.nixosConfigurations.testmachine.config.system.build.toplevel; + + dependencies = [ + pkgs.stdenv.drvPath + toplevel + self.nixosConfigurations.testmachine.config.system.build.diskoScript + ] ++ builtins.map (i: i.outPath) (builtins.attrValues self.inputs); + + closureInfo = pkgs.closureInfo { rootPaths = dependencies; }; +in +pkgs.nixosTest { + name = "disko-test"; + nodes.machine = { + virtualisation.emptyDiskImages = [ 4096 ]; + virtualisation.memorySize = 3000; + environment.etc."install-closure".source = "${closureInfo}/store-paths"; + }; + + testScript = '' + def create_test_machine(oldmachine=None, args={}): # taken from + machine = create_machine({ + "qemuFlags": + '-cpu max -m 1024 -virtfs local,path=/nix/store,security_model=none,mount_tag=nix-store,' + f' -drive file={oldmachine.state_dir}/empty0.qcow2,id=drive1,if=none,index=1,werror=report' + f' -device virtio-blk-pci,drive=drive1', + } | args) + driver.machines.append(machine) + return machine + machine.succeed("lsblk >&2") + + print(machine.succeed("tty")) + machine.succeed("${disko-install}/bin/disko-install --disk main /dev/vdb --flake ${../..}#testmachine") + # test idempotency + machine.succeed("${disko-install}/bin/disko-install --mode mount --disk main /dev/vdb --flake ${../..}#testmachine") + machine.shutdown() + + new_machine = create_test_machine(oldmachine=machine, args={ "name": "after_install" }) + new_machine.start() + name = new_machine.succeed("hostname").strip() + assert name == "disko-machine", f"expected hostname 'disko-machine', got {name}" + ''; +}