[PATCH 3/8] nixos: build the per-node disk image through the flake output

Daniel Gomez <[email protected]> Thu, 23 Apr 2026 00:48:41 +0200
Newsgroups dev.linux.lists.kdevops
Message-ID <20260423-kdevops-series-b-nixos-qemu-v1-3-209154ae54f8@samsung.com>
From: Daniel Gomez <[email protected]>

Let nix build resolve the nixos-qemu libvirt template's
packages.<system>.image output instead of stitching qcow2s
together through a jinja-rendered disk-image.nix plus a
hand-rolled make-image.nix wrapper. The template already encodes
everything the old wrapper patched on top of the closure (boot
loader, root filesystem, kernel modules, qemu-guest profile), so
keeping both would mean keeping kdevops' mkForce overrides in
sync with whatever nixos-qemu decides. Leaning on the flake
output makes the library authoritative and collapses the kdevops
side to building and copying.

Generated-by: Claude AI
Signed-off-by: Daniel Gomez <[email protected]>
(cherry picked from commit ea76a5645a6a2ca0cbcfca56ef348021bbf94d61)
---
 playbooks/nixos.yml          | 156 +++++--------------------------------------
 scripts/nixos_build_image.sh |  38 +++++++++++
 2 files changed, 54 insertions(+), 140 deletions(-)

diff --git a/playbooks/nixos.yml b/playbooks/nixos.yml
index 007c9033..7db0ab63 100644
--- a/playbooks/nixos.yml
+++ b/playbooks/nixos.yml
@@ -153,149 +153,25 @@
     - "{{ playbook_dir }}/../extra_vars.yaml"
   tags: build-vms
   tasks:
-    - name: Create disk image configuration
-      ansible.builtin.copy:
-        content: |
-          { config, lib, pkgs, ... }:
-
-          {
-            imports = [
-              ./configuration.nix
-            ];
-
-            # Ensure proper boot configuration for disk image
-            boot.loader.grub.device = lib.mkForce "/dev/vda";
-            boot.loader.grub.enable = lib.mkForce true;
-
-            fileSystems."/" = lib.mkForce {
-              device = "/dev/disk/by-label/nixos";
-              fsType = "ext4";
-              autoResize = true;
-            };
-          }
-        dest: "{{ nixos_generation_dir }}/disk-image.nix"
-
-    - name: Check if NixOS disk image already exists
-      ansible.builtin.stat:
-        path: "{{ nixos_storage_dir }}/nixos-image-result"
-      register: disk_image_exists
-
-    - name: Build NixOS disk image
-      ansible.builtin.shell: |
-        # Source nix profile and set PATH
-        export PATH="/nix/var/nix/profiles/default/bin:/usr/local/bin:/usr/bin:/bin:$PATH"
-        if [ -f /nix/var/nix/profiles/default/etc/profile.d/nix.sh ]; then
-          . /nix/var/nix/profiles/default/etc/profile.d/nix.sh
-        fi
-
-        # Configure Nix to use local mirror if available
-        {% if nixos_use_local_mirror is defined and nixos_use_local_mirror %}
-        # Dynamically detect mirror URL if not provided
-        {% if nixos_mirror_url is not defined or nixos_mirror_url == "" %}
-        DETECTED_MIRROR=$(bash {{ playbook_dir }}/../scripts/check_nix_mirror.sh NIX_CACHE_MIRROR_URL)
-        if [ -n "$DETECTED_MIRROR" ]; then
-          export NIX_CONFIG="substituters = $DETECTED_MIRROR https://cache.nixos.org"
-          echo "Using detected local Nix cache mirror: $DETECTED_MIRROR"
-        fi
-        {% else %}
-        export NIX_CONFIG="substituters = {{ nixos_mirror_url }} https://cache.nixos.org"
-        echo "Using configured local Nix cache mirror: {{ nixos_mirror_url }}"
-        {% endif %}
-        {% endif %}
-
-        cd {{ nixos_generation_dir }}
-
-        # Build a QCOW2 disk image with NixOS installed
-        echo "Building NixOS disk image (this may take a while)..."
-
-        # Create a wrapper expression for make-disk-image.nix
-        cat > make-image.nix <<'EOF'
-        let
-          pkgs = import <nixpkgs> {};
-          lib = pkgs.lib;
-
-          # Build a complete NixOS system configuration
-          nixosSystem = import "${pkgs.path}/nixos" {
-            configuration = {
-              imports = [
-                ./configuration.nix
-                ./disk-image.nix
-              ];
-
-              # Ensure we have a bootable system
-              boot.loader.grub.enable = lib.mkForce true;
-              boot.loader.grub.device = lib.mkForce "/dev/vda";
-              boot.loader.grub.configurationLimit = 1;
-
-              # Critical: ensure the system can boot
-              boot.kernelModules = [ "virtio_pci" "virtio_blk" "virtio_net" ];
-              boot.initrd.availableKernelModules = [ "virtio_pci" "virtio_blk" "virtio_net" ];
-
-              # Ensure networking works
-              networking.useDHCP = lib.mkDefault true;
-
-              # Make sure we have a working system
-              system.stateVersion = "24.05";
-
-              # Ensure SSH starts
-              systemd.services.sshd.wantedBy = [ "multi-user.target" ];
-            };
-          };
-        in
-        import "${pkgs.path}/nixos/lib/make-disk-image.nix" {
-          inherit pkgs lib;
-          config = nixosSystem.config;
-          diskSize = 20480;
-          format = "qcow2";
-          partitionTableType = "legacy";
-          # Important: include the bootloader!
-          installBootLoader = true;
-        }
-        EOF
-
-        # Force rebuild by clearing any cached result
-        rm -f {{ nixos_storage_dir }}/nixos-image-result
-
-        nix-build make-image.nix \
-          --no-out-link \
-          --arg forceRebuild true \
-          -o {{ nixos_storage_dir }}/nixos-image-result
-
-        # Return the path to the disk image
-        readlink -f {{ nixos_storage_dir }}/nixos-image-result/nixos.qcow2
-      register: build_result
-      changed_when: "'Building NixOS disk image' in build_result.stdout"
-      when: not disk_image_exists.stat.exists
-
-    - name: Get existing disk image path
-      ansible.builtin.shell: |
-        readlink -f {{ nixos_storage_dir }}/nixos-image-result/nixos.qcow2
-      register: existing_image_path
-      when: disk_image_exists.stat.exists
+    - name: Build per-node NixOS disk image via the flake
+      ansible.builtin.command:
+        cmd: "{{ playbook_dir }}/../scripts/nixos_build_image.sh {{ nixos_config_dir }}/{{ item }}"
+      environment:
+        NIXOS_MIRROR_URL: "{{ nixos_mirror_url | default('') if (nixos_use_local_mirror | default(false) | bool) else '' }}"
+      loop: "{{ groups['all'] | reject('equalto', 'localhost') | list }}"
+      loop_control:
+        label: "{{ item }}"
 
-    - name: Store disk image path
-      ansible.builtin.set_fact:
-        nixos_disk_image_path: >-
-          {{
-            build_result.stdout_lines | last | trim
-            if (build_result.stdout_lines is defined)
-            else existing_image_path.stdout | trim
-          }}
-
-    - name: Copy NixOS disk image for each VM
+    - name: Copy per-node NixOS disk image into libvirt storage
       ansible.builtin.shell: |
-        SOURCE_IMAGE="{{ nixos_disk_image_path | default(nixos_storage_dir + '/nixos-image-result/nixos.qcow2') }}"
-        TARGET_IMAGE="{{ nixos_storage_dir }}/{{ item }}.qcow2"
-
-        # Remove target if it exists and copy fresh
-        if [ -f "$TARGET_IMAGE" ]; then
-          rm -f "$TARGET_IMAGE"
-        fi
-
-        cp "$SOURCE_IMAGE" "$TARGET_IMAGE"
-        chmod u+w "$TARGET_IMAGE"
+        SRC="{{ nixos_config_dir }}/{{ item }}/result/nixos.qcow2"
+        DST="{{ nixos_storage_dir }}/{{ item }}.qcow2"
+        rm -f "$DST"
+        cp -- "$SRC" "$DST"
+        chmod u+w "$DST"
       loop: "{{ groups['all'] | reject('equalto', 'localhost') | list }}"
-      when: nixos_disk_image_path is defined
+      loop_control:
+        label: "{{ item }}"
 
     - name: Generate VM wrapper scripts
       ansible.builtin.template:
diff --git a/scripts/nixos_build_image.sh b/scripts/nixos_build_image.sh
new file mode 100755
index 00000000..a4a715fc
--- /dev/null
+++ b/scripts/nixos_build_image.sh
@@ -0,0 +1,38 @@
+#!/bin/bash
+# SPDX-License-Identifier: copyleft-next-0.3.1
+#
+# Build a per-node NixOS disk image from a generated kdevops flake.
+# Invoked from playbooks/nixos.yml's build-vms play with a single
+# argument: the per-node flake directory.
+#
+# Resolves the flake with a path: URL so nix does not filter the
+# flake directory through git. The per-node directory lives under
+# scripts/nixos-qemu/configurations/ which the nixos-qemu subtree's
+# own .gitignore excludes; a plain "." or "./" flake-ref would be
+# treated as git+file: and come back empty.
+#
+# Optional: if NIXOS_MIRROR_URL is set in the environment, the nix
+# build invocation routes through that substituter in addition to
+# cache.nixos.org.
+set -euo pipefail
+
+if [ $# -ne 1 ]; then
+    echo "Usage: $0 <per-node-flake-directory>" >&2
+    exit 1
+fi
+
+NODE_DIR="$1"
+
+if [ -f /nix/var/nix/profiles/default/etc/profile.d/nix.sh ]; then
+    . /nix/var/nix/profiles/default/etc/profile.d/nix.sh
+fi
+export PATH="/nix/var/nix/profiles/default/bin:${PATH:-/usr/local/bin:/usr/bin:/bin}"
+
+if [ -n "${NIXOS_MIRROR_URL:-}" ]; then
+    export NIX_CONFIG="substituters = ${NIXOS_MIRROR_URL} https://cache.nixos.org"
+fi
+
+cd "$NODE_DIR"
+rm -f result
+nix build "path:$PWD#image" -o result
+readlink -f result/nixos.qcow2

-- 
2.53.0