mirror of
https://github.com/jamesstringer90/appsandbox.git
synced 2026-10-21 10:40:25 -05:00
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).
86 lines
3.9 KiB
Python
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])
|