206 lines
7 KiB
Nix
206 lines
7 KiB
Nix
# 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
|
|
'';
|
|
};
|
|
};
|
|
}
|