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:
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user