Files
James Stringer 4087baa4bf Add headless --headless daemon, HTTP/JSON API + Python SDK, and SSH key deploy
Headless mode: `appsandbox.exe --headless` runs a single-owner daemon that hosts
the core over a loopback HTTP/JSON API (http.sys) with a host.json discovery
file. A dependency-free Python SDK, examples, and a self-contained regression
suite live under tools/headless-api/ (PLAN.md = design rationale).

The API mirrors the GUI: create/start/shutdown/stop/delete/edit, snapshots and
branches (incl. base branches via the new asb_snap_base_branch_count), templates,
sshInfo, an SSE /v1/events stream, GUI-mirrored input validation, a startup
fail-fast (admin / Win11 build / VirtualMachinePlatform), and a single-instance
mutex shared with the GUI so only one of GUI/headless runs at a time.

SSH key deploy: sshDeployKey (requires sshEnabled) has the core generate an
AppSandbox ed25519 keypair once and the guest agent write the public key into
authorized_keys -- Windows administrators_authorized_keys (+ ACL), Linux/macOS
~/.ssh -- reporting a new ssh_key_deployed status (sshState 4). The GUI SSH
button and the SDK use the key for password-less login. Verified end-to-end on
Windows.

The Linux guest builds its agent/driver source from this branch's GitHub repo;
asb_core.c points the prefetch at "headless-api" (revert to "main" before merge,
see PLAN.md).
2026-06-09 20:12:18 -04:00

86 lines
3.9 KiB
Python

"""
End-to-end tour of the SDK: create a VM, wait for it to install, read and edit
its config while stopped, take/rename/delete a snapshot, branch from a snapshot,
read SSH info, then delete it. Self-contained -- it makes its own VM and removes
it at the end, so it assumes no existing configuration.
python examples/full_lifecycle.py [linux|windows]
A fresh Linux guest installs in a few minutes; Windows can take ~15-20. Point
imagePath at an ISO you have. Run from an elevated shell (the daemon is too).
"""
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import asb
OS_TYPE = "Windows" if (len(sys.argv) > 1 and sys.argv[1].lower().startswith("win")) else "Linux"
NAME = "demo-vm"
ISO = (r"C:\ISOs\ubuntu-24.04.iso" if OS_TYPE == "Linux"
else r"C:\ISOs\Win11_24H2.iso") # <-- edit to a real ISO path
c = asb.connect()
print("daemon:", c.version().get("version"), "| host free:", c.host()["freeGb"], "GB")
if NAME in {v["name"] for v in c.list()}:
sys.exit("a VM named %r already exists -- delete it first" % NAME)
# --- create: validated exactly like the GUI; async + auto-starts ---
code, body = c.create(name=NAME, osType=OS_TYPE, imagePath=ISO,
ramMb=4096, hddGb=24, cpuCores=2,
gpuMode=1, networkMode=1, # 1 = paravirtual GPU, NAT
adminUser="user", adminPass="test123",
testMode=True, sshEnabled=False)
print("create ->", code, body.get("error") or "ok")
if code not in (200, 202):
sys.exit("create rejected")
try:
# --- wait through build -> install -> agent online ---
print("waiting for the guest to install + boot (this is the slow part)...")
s = c.wait_online(NAME, timeout=2400)
print("online: state=%s ram=%dMB cpu=%d gpu=%d net=%d"
% (s["state"], s["ramMb"], s["cpuCores"], s["gpuMode"], s["networkMode"]))
# --- ssh info (loopback-forwarded; populated once the agent is up) ---
ssh = c.ssh_info(NAME)
print("ssh: %s:%s user=%s sshState=%s" % (ssh.get("host"), ssh.get("port"),
ssh.get("user"), ssh.get("sshState")))
# --- take a snapshot of the running VM ---
print("snap take ->", c.snap_take(NAME, "clean-install")[0])
snaps = c.snapshots(NAME)
print("snapshots:", [(x["index"], x["name"]) for x in snaps])
sidx = snaps[-1]["index"]
print("snap rename ->", c.snap_rename(NAME, sidx, "baseline")[0])
# --- config edits require the VM stopped; graceful shutdown first ---
print("graceful shutdown ->", c.shutdown(NAME)[0])
c.wait(NAME, "stopped", timeout=600)
print("edit ram=6144,cpu=4 ->", c.edit(NAME, ramMb=6144, cpuCores=4)[0])
s = c.status(NAME)
print("now:", s["ramMb"], "MB,", s["cpuCores"], "cores")
# --- branch: boot from the snapshot onto a new branch (the GUI's only way) ---
print("start from snapshot %d on branch 'experiment' ->" % sidx,
c.start(NAME, snap_index=sidx, branch_name="experiment")[0])
c.wait(NAME, asb.RUNNING_STATES, timeout=300)
full = c.snapshots_full(NAME)
print("branches on snap %d:" % sidx,
next(x["branchCount"] for x in full["snapshots"] if x["index"] == sidx))
# --- clean up the snapshot tree: drop the branch we made, then the snapshot ---
c.shutdown(NAME); c.wait(NAME, "stopped", timeout=600)
snap = next(x for x in c.snapshots_full(NAME)["snapshots"] if x["index"] == sidx)
branch = next((b for b in snap.get("branches", []) if b["name"] == "experiment"), None)
if branch:
print("snap delete branch %d ->" % branch["index"],
c.snap_delete_branch(NAME, sidx, branch["index"])[0])
print("snap delete ->", c.snap_delete(NAME, sidx)[0])
finally:
# --- always delete the VM we made (force-stop if still running) ---
if c.status(NAME).get("running"):
c.stop(NAME); c.wait(NAME, "stopped", timeout=120)
print("delete ->", c.delete_vm(NAME)[0])