1#!/usr/bin/env python3
2import glob
3import hashlib
4import json
5import os
6from pathlib import Path
7import secrets
8import shutil
9import subprocess
10import sys
11import urllib.request
12import xml.etree.ElementTree as ET
13
14
15data = json.load(sys.stdin)
16root = Path(data["hostRoot"])
17if root.name != "openclaw":
18 raise ValueError("OpenClaw's dedicated VM is production-only")
19source = Path(__file__).parent
20vm = root / "vm"
21vm.mkdir(mode=0o700, exist_ok=True)
22cfg = root / "cfg"
23cfg.mkdir(mode=0o700, exist_ok=True)
24key = json.loads(subprocess.check_output(
25 ["nomad", "var", "get", "-out=json", "nomad/jobs/local-ai"],
26 text=True, stderr=subprocess.DEVNULL,
27))["Items"]["api_key"]
28if not key.isalnum():
29 raise ValueError("Invalid model API credential")
30nginx = source.joinpath("nginx.conf").read_text().replace("MODEL_API_KEY", key)
31pending = cfg / ".nginx.conf.pending"
32pending.write_text(nginx)
33pending.chmod(0o600)
34os.chown(pending, data["uid"], data["uid"])
35pending.replace(cfg / "nginx.conf")
36os.chown(cfg, data["uid"], data["uid"])
37
38if not (vm / "ssh_key").exists():
39 subprocess.run(["ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-f", str(vm / "ssh_key")], check=True)
40
41catalog = json.loads(source.parent.joinpath("local-ai/catalog.json").read_text())["models"][0]
42config = json.loads(source.joinpath("openclaw.json").read_text())
43# Browser control needs its own password under trusted-proxy auth.
44previous = json.loads((vm / "openclaw.json").read_text()) if (vm / "openclaw.json").exists() else {}
45config["gateway"]["auth"]["password"] = previous.get("gateway", {}).get("auth", {}).get("password") or secrets.token_hex(24)
46model = config["models"]["providers"]["snowglobe"]["models"][0]
47model.update(id=catalog["slug"], name=catalog["display_name"], contextWindow=catalog["context_window"])
48config["agents"]["defaults"]["model"]["primary"] = "snowglobe/" + catalog["slug"]
49(vm / "openclaw.json").write_text(json.dumps(config, indent=2) + "\n")
50
51if not (vm / "disk.qcow2").exists():
52 image = vm / "ubuntu.img"
53 expected = "6a81c37564db9b1ee84e141922625e1d7c5b389b99bb3c572e0243607d5bb4d2"
54 if not image.exists():
55 urllib.request.urlretrieve(
56 "https://cloud-images.ubuntu.com/noble/20260926/noble-server-cloudimg-amd64.img", image,
57 )
58 with image.open("rb") as stream:
59 if hashlib.file_digest(stream, "sha256").hexdigest() != expected:
60 raise ValueError("Ubuntu image checksum mismatch")
61 subprocess.run(["qemu-img", "convert", "-O", "qcow2", str(image), str(vm / ".disk.pending")], check=True)
62 subprocess.run(["qemu-img", "resize", str(vm / ".disk.pending"), "32G"], check=True)
63
64 writes = [
65 {"path": "/etc/apt/apt.conf.d/90proxy", "content": 'Acquire::http::Proxy "http://10.0.2.100:3128";\nAcquire::https::Proxy "http://10.0.2.100:3128";\n'},
66 {"path": "/etc/systemd/system/docker.service.d/proxy.conf", "content": "[Service]\nEnvironment=HTTP_PROXY=http://10.0.2.100:3128 HTTPS_PROXY=http://10.0.2.100:3128 NO_PROXY=localhost,127.0.0.1,10.0.2.101\n"},
67 {"path": "/opt/openclaw/openclaw.json", "content": json.dumps(config, indent=2), "permissions": "0600"},
68 {"path": "/opt/openclaw/Dockerfile", "content": source.joinpath("Dockerfile.guest").read_text()},
69 {"path": "/opt/openclaw/resolv.conf", "content": source.joinpath("resolv.conf").read_text()},
70 {"path": "/etc/systemd/system/openclaw.service", "content": source.joinpath("openclaw.service").read_text()},
71 {"path": "/usr/local/sbin/openclaw-install", "content": source.joinpath("install.sh").read_text(), "permissions": "0755"},
72 {"path": "/etc/systemd/system/openclaw-install.service", "content": source.joinpath("openclaw-install.service").read_text()},
73 ]
74 cloud = {
75 "hostname": "openclaw", "manage_etc_hosts": True, "ssh_pwauth": False,
76 "disable_root": True, "users": [{"name": "ocadmin", "lock_passwd": True,
77 "shell": "/bin/bash", "sudo": "ALL=(ALL) NOPASSWD:ALL",
78 "ssh_authorized_keys": [(vm / "ssh_key.pub").read_text().strip()]}],
79 "write_files": writes,
80 "bootcmd": [["systemctl", "mask", "--now", "snapd.service", "snapd.socket", "snapd.seeded.service", "snapd.autoimport.service"]],
81 "runcmd": [["systemctl", "daemon-reload"], ["systemctl", "enable", "--now", "openclaw-install"]],
82 }
83 (vm / "user-data").write_text("#cloud-config\n" + json.dumps(cloud))
84 (vm / "meta-data").write_text("instance-id: openclaw-trial\nlocal-hostname: openclaw\n")
85 iso = shutil.which("genisoimage") or sorted(glob.glob("/nix/store/*-cdrkit-*/bin/genisoimage"))[0]
86 subprocess.run([iso, "-quiet", "-output", str(vm / "seed.iso"), "-volid", "cidata", "-joliet", "-rock", str(vm / "user-data"), str(vm / "meta-data")], check=True)
87 (vm / ".disk.pending").replace(vm / "disk.qcow2")
88 image.unlink()
89
90qemu = "http://libvirt.org/schemas/domain/qemu/1.0"
91units = Path("/etc/systemd/system.control")
92units.mkdir(exist_ok=True)
93shutil.copyfile(source / "machine-openclaw.slice", units / "machine-openclaw.slice")
94dependency = units / "libvirtd.service.d"
95dependency.mkdir(exist_ok=True)
96(dependency / "openclaw.conf").write_text("[Unit]\nRequires=machine-openclaw.slice\nAfter=machine-openclaw.slice\n")
97subprocess.run(["systemctl", "daemon-reload"], check=True)
98subprocess.run(["systemctl", "start", "machine-openclaw.slice"], check=True)
99ET.register_namespace("qemu", qemu)
100domain = ET.fromstring(f"""<domain type="kvm">
101 <name>openclaw</name><memory unit="MiB">6144</memory><vcpu>4</vcpu>
102 <resource><partition>/machine/openclaw</partition></resource>
103 <os><type arch="x86_64" machine="q35">hvm</type><boot dev="hd"/></os>
104 <features><acpi/><apic/></features><cpu mode="host-passthrough"/>
105 <on_poweroff>destroy</on_poweroff><on_reboot>restart</on_reboot><on_crash>restart</on_crash>
106 <devices>
107 <disk type="file" device="disk"><driver name="qemu" type="qcow2"/><source file="{vm}/disk.qcow2"/><target dev="vda" bus="virtio"/></disk>
108 <disk type="file" device="cdrom"><driver name="qemu" type="raw"/><source file="{vm}/seed.iso"/><target dev="sda" bus="sata"/><readonly/></disk>
109 <serial type="pty"><target port="0"/></serial>
110 <memballoon model="virtio"/><rng model="virtio"><backend model="random">/dev/urandom</backend></rng>
111 </devices>
112</domain>""")
113existing = subprocess.run(["virsh", "domuuid", "openclaw"], capture_output=True, text=True)
114if existing.returncode == 0:
115 ET.SubElement(domain, "uuid").text = existing.stdout.strip()
116args = ET.SubElement(domain, "{" + qemu + "}commandline")
117capabilities = ET.SubElement(domain, "{" + qemu + "}capabilities")
118# Libvirt's spawn/setsid filters block the fixed per-connection proxy commands.
119ET.SubElement(capabilities, "{" + qemu + "}del", capability="seccomp-sandbox")
120for value in [
121 "-sandbox", "on,obsolete=deny,elevateprivileges=allow,spawn=allow,resourcecontrol=deny",
122 "-netdev", "user,id=ocnet,restrict=on,ipv6=off,hostfwd=tcp:127.0.0.1:18890-:18789,hostfwd=tcp:127.0.0.1:18892-:22,guestfwd=tcp:10.0.2.100:3128-cmd:/run/current-system/sw/bin/nc 127.0.0.1 18882,guestfwd=tcp:10.0.2.101:8080-cmd:/run/current-system/sw/bin/nc 127.0.0.1 18881,guestfwd=tcp:10.0.2.102:53-cmd:/run/current-system/sw/bin/nc 1.1.1.1 53",
123 "-device", "virtio-net-pci,netdev=ocnet,bus=pcie.0,addr=0x10",
124]:
125 ET.SubElement(args, "{" + qemu + "}arg", value=value)
126(vm / "domain.xml").write_text(ET.tostring(domain, encoding="unicode"))
127subprocess.run(["virsh", "define", str(vm / "domain.xml")], check=True)
128subprocess.run(["virsh", "autostart", "openclaw"], check=True)
129if subprocess.check_output(["virsh", "domstate", "openclaw"], text=True).strip() != "running":
130 subprocess.run(["virsh", "start", "openclaw"], check=True)