fix(smolmachines): commit via exec-tar instead of stop→pack

smolvm pack create --from-vm requires the VM to be stopped, and stopping
a smolmachines VM terminates any running interactive session.

Instead, mirror the macos-container approach: exec into the running VM as
root and stream the root filesystem via tar (smolvm machine exec -- tar),
build a Docker image from the archive, push to an ephemeral local registry,
and run smolvm pack create --image to produce the .smolmachine artifact.
The VM stays running throughout the commit.

Remove the stop-confirm prompt and machine_is_running check that were
added in the previous commit — neither is needed when we no longer stop.
This commit is contained in:
2026-06-23 08:57:06 +00:00
committed by didericis
parent d11e3940fa
commit eb64a52ffa
2 changed files with 94 additions and 78 deletions
+83 -34
View File
@@ -1,52 +1,101 @@
"""SmolmachinesFreezer — snapshot a smolmachines bottle via smolvm pack.""" """SmolmachinesFreezer — snapshot a smolmachines bottle.
`smolvm pack create --from-vm` requires the VM to be stopped, and smolvm
removes VMs when stopped (same issue as Apple Container). Instead, exec
into the running VM as root and stream the root filesystem via tar, build
a Docker image from the archive, convert it to a smolmachine artifact via
the existing registry pipeline, and record the sidecar path. The VM stays
running throughout."""
from __future__ import annotations from __future__ import annotations
import sys import subprocess
import tempfile
from pathlib import Path
from .. import ActiveAgent from .. import ActiveAgent
from ..freeze import CommitCancelled, Freezer from ..freeze import Freezer
from .smolvm import machine_is_running, machine_stop, pack_create_from_vm from ..docker import util as docker_mod
from .local_registry import crane_push_tarball, ephemeral_registry
from .smolvm import pack_create
from ...bottle_state import bottle_state_dir from ...bottle_state import bottle_state_dir
from ...log import info from ...log import die, info
def _read_tty_line() -> str:
"""Read one line from /dev/tty, falling back to stdin."""
try:
with open("/dev/tty", "r", encoding="utf-8") as tty:
return tty.readline().rstrip("\n")
except OSError:
return sys.stdin.readline().rstrip("\n")
class SmolmachinesFreezer(Freezer): class SmolmachinesFreezer(Freezer):
"""Freezes a smolmachines bottle via `smolvm pack create --from-vm`. """Freezes a smolmachines bottle via exec-tar + Docker image + smolmachine pack.
`smolvm pack create --from-vm` requires the VM to be stopped first. The VM is NOT stopped. smolvm machine exec streams the root filesystem
If the VM is running the freezer prompts the user to confirm the stop via tar; we build a Docker image from it and run the same image→registry→
before proceeding. The VM remains stopped after commit; use pack_create pipeline that _ensure_smolmachine uses for fresh builds."""
`./cli.py resume` to restart."""
backend_name = "smolmachines" backend_name = "smolmachines"
def _freeze(self, agent: ActiveAgent) -> str: def _freeze(self, agent: ActiveAgent) -> str:
machine = f"bot-bottle-{agent.slug}" machine = f"bot-bottle-{agent.slug}"
output = bottle_state_dir(agent.slug) / "committed-smolmachine" image_ref = f"bot-bottle-committed-{agent.slug}:latest"
output.parent.mkdir(parents=True, exist_ok=True) output_dir = bottle_state_dir(agent.slug)
if machine_is_running(machine): output_dir.mkdir(parents=True, exist_ok=True)
sys.stderr.write( binary = output_dir / "committed-smolmachine"
f"bot-bottle: bottle {agent.slug!r} is running; " sidecar = output_dir / "committed-smolmachine.smolmachine"
"commit will stop it. Continue? [y/N] " _snapshot_running_vm(machine, image_ref, binary)
) return str(sidecar)
sys.stderr.flush()
reply = _read_tty_line().strip().lower()
if reply not in ("y", "yes"):
raise CommitCancelled
machine_stop(machine)
pack_create_from_vm(machine, output)
artifact = output.with_name(f"{output.name}.smolmachine")
return str(artifact)
def _export_hint(self, slug: str, image_ref: str) -> None: def _export_hint(self, slug: str, image_ref: str) -> None:
info(f"to export for migration: cp {image_ref} {slug}.smolmachine") info(f"to export for migration: cp {image_ref} {slug}.smolmachine")
def _snapshot_running_vm(machine: str, image_ref: str, binary: Path) -> None:
"""Exec-tar the running VM, build a Docker image, and pack to a smolmachine.
binary: destination for the launcher (sibling .smolmachine is the artifact
that machine_create --from consumes, same convention as pack_create).
"""
with tempfile.TemporaryDirectory(prefix="bot-bottle-vm-commit.") as tmp:
tmp_path = Path(tmp)
rootfs_tar = tmp_path / "rootfs.tar"
dockerfile = tmp_path / "Dockerfile"
with open(rootfs_tar, "wb") as tar_out:
result = subprocess.run(
[
"smolvm", "machine", "exec",
"--name", machine, "--",
"tar", "--create",
"--exclude=./proc",
"--exclude=./sys",
"--exclude=./dev",
"--exclude=./run",
"--file=-",
"--directory=/",
".",
],
stdout=tar_out,
stderr=subprocess.PIPE,
check=False,
)
if result.returncode != 0:
die(
f"smolvm exec tar {machine!r} failed: "
f"{(result.stderr or b'').decode().strip() or '<no stderr>'}"
)
dockerfile.write_text(
"FROM scratch\n"
"ADD rootfs.tar /\n"
"USER node\n"
"WORKDIR /home/node\n"
)
docker_mod.build_image(image_ref, str(tmp_path), dockerfile=str(dockerfile))
image_tarball = binary.parent / "committed.image.tar"
docker_mod.save(image_ref, str(image_tarball))
try:
with ephemeral_registry() as handle:
digest = docker_mod.image_id(image_ref).split(":", 1)[-1][:16]
push_ref = f"{handle.push_endpoint}/bot-bottle-committed:{digest}"
pack_ref = f"{handle.pull_endpoint}/bot-bottle-committed:{digest}"
crane_push_tarball(handle, str(image_tarball), push_ref)
pack_create(pack_ref, binary)
finally:
image_tarball.unlink(missing_ok=True)
+11 -44
View File
@@ -189,61 +189,28 @@ class TestSmolmachinesFreezer(_FakeHomeMixin, unittest.TestCase):
started_at="t", backend="smolmachines", started_at="t", backend="smolmachines",
)) ))
def test_packs_stopped_vm_directly(self): def test_snapshots_running_vm_without_stopping(self):
"""Commit should exec-tar the running VM, not stop it."""
slug = "dev-abc12" slug = "dev-abc12"
self._write_meta(slug) self._write_meta(slug)
freezer = SmolmachinesFreezer() freezer = SmolmachinesFreezer()
agent = _make_agent(slug, "smolmachines") agent = _make_agent(slug, "smolmachines")
with patch("bot_bottle.backend.smolmachines.freezer.machine_is_running", with patch("bot_bottle.backend.smolmachines.freezer._snapshot_running_vm") as mock_snap, \
return_value=False), \
patch("bot_bottle.backend.smolmachines.freezer.pack_create_from_vm") as mock_pack, \
patch("bot_bottle.backend.freeze.info"), \ patch("bot_bottle.backend.freeze.info"), \
patch("bot_bottle.backend.smolmachines.freezer.info"): patch("bot_bottle.backend.smolmachines.freezer.info"):
freezer.commit(agent) freezer.commit(agent)
expected_output = bottle_state.bottle_state_dir(slug) / "committed-smolmachine" expected_binary = bottle_state.bottle_state_dir(slug) / "committed-smolmachine"
mock_pack.assert_called_once_with(f"bot-bottle-{slug}", expected_output) mock_snap.assert_called_once_with(
expected_artifact = str(expected_output.with_name("committed-smolmachine.smolmachine")) f"bot-bottle-{slug}",
self.assertEqual(expected_artifact, bottle_state.read_committed_image(slug)) f"bot-bottle-committed-{slug}:latest",
expected_binary,
)
expected_sidecar = str(expected_binary.with_suffix(".smolmachine"))
self.assertEqual(expected_sidecar, bottle_state.read_committed_image(slug))
self.assertTrue(bottle_state.is_preserved(slug)) self.assertTrue(bottle_state.is_preserved(slug))
def test_stops_running_vm_on_yes(self):
slug = "dev-abc12"
self._write_meta(slug)
freezer = SmolmachinesFreezer()
agent = _make_agent(slug, "smolmachines")
with patch("bot_bottle.backend.smolmachines.freezer.machine_is_running",
return_value=True), \
patch("bot_bottle.backend.smolmachines.freezer._read_tty_line", return_value="y"), \
patch("bot_bottle.backend.smolmachines.freezer.machine_stop") as mock_stop, \
patch("bot_bottle.backend.smolmachines.freezer.pack_create_from_vm") as mock_pack, \
patch("bot_bottle.backend.freeze.info"), \
patch("bot_bottle.backend.smolmachines.freezer.info"):
freezer.commit(agent)
mock_stop.assert_called_once_with(f"bot-bottle-{slug}")
mock_pack.assert_called_once()
def test_raises_commit_cancelled_on_no(self):
slug = "dev-abc12"
self._write_meta(slug)
freezer = SmolmachinesFreezer()
agent = _make_agent(slug, "smolmachines")
with patch("bot_bottle.backend.smolmachines.freezer.machine_is_running",
return_value=True), \
patch("bot_bottle.backend.smolmachines.freezer._read_tty_line", return_value="n"), \
patch("bot_bottle.backend.smolmachines.freezer.machine_stop") as mock_stop, \
patch("bot_bottle.backend.smolmachines.freezer.pack_create_from_vm") as mock_pack:
from bot_bottle.backend.freeze import CommitCancelled
with self.assertRaises(CommitCancelled):
freezer.commit(agent)
mock_stop.assert_not_called()
mock_pack.assert_not_called()
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()