# OpenClaw VM — Ubuntu 24.04 QEMU guest for interactive `openclaw onboard` setup. # Runs alongside the Podman container (openclaw.nix on .97.60) on a separate IP (.97.61). # The container serves the gateway/webchat; this VM is for the daemon/onboarding workflow. { config, pkgs, lib, ... }: let vmName = "openclaw-vm"; vmStateDir = "/var/lib/${vmName}"; vmMac = "02:00:00:00:03:01"; vmIp = "${config.networkPrefix}.97.61"; gateway = "${config.networkPrefix}.97.1"; tapDevice = "vm-openclaw"; sshAuthorizedKeys = [ "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDN/2SAFm50kraB1fepAizox/QRXxB7WbqVbH+5OPalDT47VIJGNKOKhixQoqhABHxEoLxdf/C83wxlCVlPV9poLfDgVkA3Lyt5r3tSFQ6QjjOJAgchWamMsxxyGBedhKvhiEzcr/Lxytnoz3kjDG8fqQJwEpdqMmJoMUfyL2Rqp16u+FQ7d5aJtwO8EUqovhMaNO7rggjPpV/uMOg+tBxxmscliN7DLuP4EMTA/FwXVzcFNbOx3K9BdpMRAaSJt4SWcJO2cS2KHA5n/H+PQI7nz5KN3Yr/upJN5fROhi/SHvK39QOx12Pv7FCuWlc+oR68vLaoCKYhnkl3DnCfc7A7" "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIRQuPqH5fdX3KEw7DXzWEdO3AlUn1oSmtJtHB71ICoH Generated By Termius" ]; gitRepoUrl = "https://git.cloonar.com/openclawd/config.git"; # Cloud-init user-data userData = pkgs.writeText "user-data" '' #cloud-config users: - name: openclaw shell: /bin/bash sudo: ALL=(ALL) NOPASSWD:ALL # needed for openclaw onboard --install-daemon; VM is internal-only, SSH-key-only groups: [sudo] ssh_authorized_keys: ${lib.concatMapStringsSep "\n " (k: "- ${k}") sshAuthorizedKeys} package_update: true package_upgrade: true packages: - git - curl - build-essential runcmd: - [bash, /var/lib/cloud/scripts/setup-openclaw.sh] write_files: - path: /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg content: | network: {config: disabled} - path: /var/lib/cloud/scripts/setup-openclaw.sh permissions: "0755" content: | #!/bin/bash set -euxo pipefail # Wait for network to be fully ready for i in $(seq 1 30); do if curl -fsS --max-time 5 https://nodejs.org/ >/dev/null 2>&1; then break fi echo "Waiting for network... ($i/30)" sleep 2 done # Install Node.js 22 from official binary tarball NODE_MAJOR=22 NODE_VERSION=$(curl -fsSL "https://nodejs.org/dist/latest-v''${NODE_MAJOR}.x/" | grep -oP 'node-v\K[0-9.]+' | head -1) curl -fsSL "https://nodejs.org/dist/v''${NODE_VERSION}/node-v''${NODE_VERSION}-linux-x64.tar.xz" \ | tar -xJf - -C /usr/local --strip-components=1 # Verify node and npm are available node --version npm --version # Install OpenClaw globally npm install -g openclaw@latest # Clone the repo as openclaw user mkdir -p /home/openclaw/.openclaw su - openclaw -c 'git clone ${gitRepoUrl} /home/openclaw/.openclaw/workspace' ''; # Cloud-init meta-data metaData = pkgs.writeText "meta-data" '' instance-id: ${vmName} local-hostname: ${vmName} ''; # Cloud-init network-config (netplan v2, MAC-based match) networkConfig = pkgs.writeText "network-config" '' network: version: 2 ethernets: id0: match: macaddress: "${vmMac}" addresses: [${vmIp}/24] routes: - to: default via: ${gateway} nameservers: addresses: [${gateway}] ''; ubuntuImageUrl = "https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img"; ubuntuImageName = "noble-server-cloudimg-amd64.img"; in { # Ensure KVM is available boot.kernelModules = [ "kvm-intel" ]; # State directory systemd.tmpfiles.rules = [ "d ${vmStateDir} 0755 root root - -" ]; # Init service: download cloud image, create disk, generate seed ISO systemd.services."${vmName}-init" = { description = "Initialize ${vmName} disk and cloud-init seed"; wantedBy = [ "multi-user.target" ]; before = [ "${vmName}.service" ]; requiredBy = [ "${vmName}.service" ]; path = with pkgs; [ curl qemu_kvm cdrkit ]; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; }; script = '' set -euo pipefail mkdir -p "${vmStateDir}" # Download Ubuntu cloud image if not present if [ ! -f "${vmStateDir}/${ubuntuImageName}" ]; then echo "Downloading Ubuntu 24.04 cloud image..." curl -fSL -o "${vmStateDir}/${ubuntuImageName}" "${ubuntuImageUrl}" fi # Create qcow2 disk from cloud image if not present if [ ! -f "${vmStateDir}/disk.qcow2" ]; then echo "Creating qcow2 disk from cloud image..." qemu-img convert -f qcow2 -O qcow2 "${vmStateDir}/${ubuntuImageName}" "${vmStateDir}/disk.qcow2" qemu-img resize "${vmStateDir}/disk.qcow2" 20G fi # Always regenerate seed ISO (picks up config changes) # Cloud-init expects files named user-data, meta-data, network-config echo "Generating cloud-init seed ISO..." tmpdir=$(mktemp -d) cp ${userData} "$tmpdir/user-data" cp ${metaData} "$tmpdir/meta-data" cp ${networkConfig} "$tmpdir/network-config" genisoimage -output "${vmStateDir}/seed.iso" \ -volid cidata -joliet -rock \ "$tmpdir/user-data" "$tmpdir/meta-data" "$tmpdir/network-config" rm -rf "$tmpdir" ''; }; # QEMU VM service systemd.services."${vmName}" = { description = "OpenClaw QEMU VM"; wantedBy = [ "multi-user.target" ]; after = [ "${vmName}-init.service" "network-online.target" ]; requires = [ "${vmName}-init.service" ]; wants = [ "network-online.target" ]; path = with pkgs; [ iproute2 qemu_kvm ]; serviceConfig = { Type = "simple"; Restart = "on-failure"; RestartSec = 10; ExecStartPre = pkgs.writeShellScript "${vmName}-tap-setup" '' set -euo pipefail # Clean up stale TAP device if it exists if ip link show ${tapDevice} &>/dev/null; then ip link set ${tapDevice} down || true ip link delete ${tapDevice} || true fi # Create TAP device and attach to server bridge ip tuntap add dev ${tapDevice} mode tap ip link set ${tapDevice} master server ip link set ${tapDevice} up ''; ExecStart = lib.concatStringsSep " " [ "${pkgs.qemu_kvm}/bin/qemu-system-x86_64" "-machine type=q35,accel=kvm" "-cpu host" "-smp 2" "-m 4096" "-drive file=${vmStateDir}/disk.qcow2,format=qcow2,if=virtio" "-drive file=${vmStateDir}/seed.iso,format=raw,if=virtio,media=cdrom" "-netdev tap,id=net0,ifname=${tapDevice},script=no,downscript=no" "-device virtio-net-pci,netdev=net0,mac=${vmMac}" "-nographic" "-serial mon:stdio" ]; ExecStopPost = pkgs.writeShellScript "${vmName}-tap-cleanup" '' if ip link show ${tapDevice} &>/dev/null; then ip link set ${tapDevice} down || true ip link delete ${tapDevice} || true fi ''; }; }; }