mkHypervisor has no disko module — host disk layout cannot be declarative #12

Closed
opened 2026-07-27 22:12:36 +01:00 by vnprc-agent · 1 comment

The hypervisor builder cannot evaluate a declarative disk layout, so a host's disk topology has to stay a hand-maintained UUID module while every VM archetype already gets disko. Bring the hypervisor archetype up to the same standard as the VM archetypes.

Primary goals:

  • Reach disko without a second pin — follow the existing vm/disko pin rather than adding an independent one, so the framework keeps one source of truth for the disko revision.
  • Import the module in the hypervisor buildermkHypervisor gains disko.nixosModules.disko so a consumer's hardware module can declare disko.devices.
  • Stay inert until something is declared — with no disko.devices in the evaluated configuration this must be a zero-diff change, provable by comparing built system closures.
  • Unblock declarative host disk layout — consumers can then express host disk topology declaratively instead of copying generated UUID modules between machines.

Current state

mkHypervisor (flake.nix:232) assembles the host module list, and at flake.nix:248 it currently reads:

modules = [
  nexus.nixosModules.host
  machines.${name}.hardware
  agenix.nixosModules.default
  ./modules/netrc.nix
  # ...
] ++ definition.nixosModules;

There is no disko module in that list, and flake.nix declares no disko input. Disko is reachable in the wider stack only through allod/vm, which pins it at vm/flake.nix:18-21.

vm.nixosModules.qemuGuest is not reusable here. It welds three things into one bundle at vm/flake.nix:30-31disko.nixosModules.disko, ./disk.nix (the VM layout, hardcoded to /dev/vda with no encryption), and modules/qemu-guest.nix. Importing it on a hypervisor would drag in the VM disk layout and the QEMU guest configuration.

The result is an asymmetry: VM archetypes get a declarative disk through qemuGuest, and the hypervisor archetype gets nothing, so host disk topology stays imperative and machine-bound.

Proposed change

Add the input, following the existing pin:

inputs.disko.follows = "vm/disko";

Add disko to the outputs argument list, then add the module ahead of the rest in mkHypervisor:

modules = [
  disko.nixosModules.disko
  nexus.nixosModules.host
  machines.${name}.hardware
  # ...unchanged...
];

Validation

A disko module with no disko.devices declared should produce no configuration, so the evaluated system must be unchanged. Compare derivation paths rather than building — identical .drv proves identical output, and building a full host closure is expensive and unnecessary for this:

nix eval --raw '<deploy-flake>#nixosConfigurations.<host>.config.system.build.toplevel.drvPath' \
  --override-input archetypes path:<local-archetypes-checkout>

Run it before and after the change, using the same --override-input on both sides so the only difference is the code. Identical drvPaths prove the change inert and it can land with no risk to a running host. If they differ, diff the two derivations and explain the difference before merging rather than accepting it.

Pair this with a non-vacuity check, so an identical drvPath cannot be mistaken for "the module never loaded": config.disko.devices should fail to evaluate before the change and resolve to the module's empty-device attrset after it.

nix flake check should pass unchanged.

Scope

In scope: the disko input wiring and the module import in the hypervisor builder, plus the store-path equivalence check.

Out of scope: any actual disko.devices declaration. Disk topology is machine data and belongs in a consumer's inventory hardware module, not in the framework. Also out of scope: converting an existing host from a UUID hardware module to a disko declaration, which is consumer-side migration work with its own rollback requirements.

The hypervisor builder cannot evaluate a declarative disk layout, so a host's disk topology has to stay a hand-maintained UUID module while every VM archetype already gets disko. Bring the hypervisor archetype up to the same standard as the VM archetypes. Primary goals: - **Reach disko without a second pin** — follow the existing `vm/disko` pin rather than adding an independent one, so the framework keeps one source of truth for the disko revision. - **Import the module in the hypervisor builder** — `mkHypervisor` gains `disko.nixosModules.disko` so a consumer's hardware module can declare `disko.devices`. - **Stay inert until something is declared** — with no `disko.devices` in the evaluated configuration this must be a zero-diff change, provable by comparing built system closures. - **Unblock declarative host disk layout** — consumers can then express host disk topology declaratively instead of copying generated UUID modules between machines. ### Current state `mkHypervisor` (`flake.nix:232`) assembles the host module list, and at `flake.nix:248` it currently reads: ```nix modules = [ nexus.nixosModules.host machines.${name}.hardware agenix.nixosModules.default ./modules/netrc.nix # ... ] ++ definition.nixosModules; ``` There is no disko module in that list, and `flake.nix` declares no `disko` input. Disko is reachable in the wider stack only through `allod/vm`, which pins it at `vm/flake.nix:18-21`. `vm.nixosModules.qemuGuest` is not reusable here. It welds three things into one bundle at `vm/flake.nix:30-31` — `disko.nixosModules.disko`, `./disk.nix` (the VM layout, hardcoded to `/dev/vda` with no encryption), and `modules/qemu-guest.nix`. Importing it on a hypervisor would drag in the VM disk layout and the QEMU guest configuration. The result is an asymmetry: VM archetypes get a declarative disk through `qemuGuest`, and the hypervisor archetype gets nothing, so host disk topology stays imperative and machine-bound. ### Proposed change Add the input, following the existing pin: ```nix inputs.disko.follows = "vm/disko"; ``` Add `disko` to the `outputs` argument list, then add the module ahead of the rest in `mkHypervisor`: ```nix modules = [ disko.nixosModules.disko nexus.nixosModules.host machines.${name}.hardware # ...unchanged... ]; ``` ### Validation A disko module with no `disko.devices` declared should produce no configuration, so the evaluated system must be unchanged. Compare derivation paths rather than building — identical `.drv` proves identical output, and building a full host closure is expensive and unnecessary for this: ```bash nix eval --raw '<deploy-flake>#nixosConfigurations.<host>.config.system.build.toplevel.drvPath' \ --override-input archetypes path:<local-archetypes-checkout> ``` Run it before and after the change, using the same `--override-input` on both sides so the only difference is the code. Identical drvPaths prove the change inert and it can land with no risk to a running host. If they differ, diff the two derivations and explain the difference before merging rather than accepting it. Pair this with a non-vacuity check, so an identical drvPath cannot be mistaken for "the module never loaded": `config.disko.devices` should fail to evaluate before the change and resolve to the module's empty-device attrset after it. `nix flake check` should pass unchanged. ### Scope In scope: the `disko` input wiring and the module import in the hypervisor builder, plus the store-path equivalence check. Out of scope: any actual `disko.devices` declaration. Disk topology is machine data and belongs in a consumer's inventory hardware module, not in the framework. Also out of scope: converting an existing host from a UUID hardware module to a disko declaration, which is consumer-side migration work with its own rollback requirements.
Author

Landed on master as 1420e8f. Verified inert: config.system.build.toplevel.drvPath is identical before and after against a consumer configuration that declares no devices, with the same --override-input on both sides. Non-vacuity confirmed — config.disko.devices fails to evaluate before the change and resolves to the module's empty-device attrset after, so the unchanged derivation is not simply the module failing to load. nix flake check --no-build passes. The lock gained only a follows entry, no second disko node.

Landed on master as 1420e8f. Verified inert: `config.system.build.toplevel.drvPath` is identical before and after against a consumer configuration that declares no devices, with the same `--override-input` on both sides. Non-vacuity confirmed — `config.disko.devices` fails to evaluate before the change and resolves to the module's empty-device attrset after, so the unchanged derivation is not simply the module failing to load. `nix flake check --no-build` passes. The lock gained only a `follows` entry, no second disko node.
Sign in to join this conversation.
No description provided.