c73d717f71
First sub-PR of chunk 2: rewrite the renderer chunk 1 shipped to match smolvm 0.8.0's actual Smolfile shape, delete the dead gvproxy renderer + its tests, simplify the prepare flow now that there's no gvproxy socket + no loopback-port allocation. Smolfile renderer: - Old shape (under the abandoned gvproxy design): name = ..., command = [...], [[net]] attachment = "unixgram", socket = "...". - New shape (smolvm 0.8.0): env = [...] (sorted K=V pairs), [network] allow_cidrs = ["<bundle-ip>/32"]. Nothing else. image / entrypoint / cmd come from the .smolmachine artifact built in chunk 2b; cpus / memory left at smolvm defaults. - Tests assert no leakage of TSI's --outbound-localhost-only or the old gvproxy/unixgram keys. util.py: - smolmachines_gvproxy_subnet → smolmachines_bundle_subnet, returning (subnet, gateway, bundle_ip). bundle_ip is always at .2 (gateway .1); subnet is /24, third octet derived from the slug hash, skipping the docker-default 17 to avoid the common 192.168.17.x collision. - allocate_loopback_port: deleted. The bundle gets a pinned docker IP now; the agent dials that IP directly through TSI. - smolmachines_preflight: dropped the gvproxy check; only smolvm is required. prepare.py: - Drops the gvproxy.yaml render + the loopback port allocation + the gvproxy_socket field on the plan. - Derives subnet / gateway / bundle_ip from the slug and populates the new SmolmachinesBottlePlan fields. - Agent env now uses IP-literal URLs (http://<bundle-ip>:8888 etc) since the guest will have no DNS resolver inside TSI's allowlist. bottle_plan.py: - Old fields: gvproxy_config_path, gvproxy_socket, gvproxy_subnet, gvproxy_gateway, host_port_map. - New fields: bundle_subnet, bundle_gateway, bundle_ip, smolfile_path. (smolmachine artifact path lands in chunk 2b.) Net: -410 lines. Full unit suite: 516 passing. The VM lifecycle + bundle bringup + launch wiring + smoke tests land in chunk 2b. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
49 lines
1.7 KiB
Python
49 lines
1.7 KiB
Python
"""Slug / preflight / subnet helpers for the smolmachines backend
|
|
(PRD 0023). Kept in its own module so the renderers can be
|
|
unit-tested without importing the docker subprocess paths."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import shutil
|
|
|
|
from ...log import die
|
|
|
|
|
|
def smolmachines_preflight() -> None:
|
|
"""Ensure `smolvm` is on PATH before the launch flow runs.
|
|
Called from `_resolve_plan`; gives the operator a clear
|
|
install pointer rather than a cryptic FileNotFoundError
|
|
later. `gvproxy` is no longer required — see the PRD's design
|
|
pivot section."""
|
|
if shutil.which("smolvm") is not None:
|
|
return
|
|
die(
|
|
"CLAUDE_BOTTLE_BACKEND=smolmachines requires `smolvm` on "
|
|
"PATH. Install with: "
|
|
"curl -sSL https://smolmachines.com/install.sh | sh"
|
|
)
|
|
|
|
|
|
def smolmachines_bundle_subnet(slug: str) -> tuple[str, str, str]:
|
|
"""Derive a per-bottle docker subnet + gateway IP + bundle IP
|
|
from the slug.
|
|
|
|
Returns `(subnet_cidr, gateway_ip, bundle_ip)`. The third
|
|
octet comes from SHA-256 of the slug mod 254 (skipping 17 to
|
|
avoid the docker-default bridge), so parallel bottles get
|
|
distinct /24s and `resume` reuses the same /24. The bundle
|
|
container always lands at `.2`; gateway is `.1`; the smolvm
|
|
Smolfile's `allow_cidrs` is `<bundle_ip>/32`."""
|
|
digest = hashlib.sha256(slug.encode("utf-8")).digest()
|
|
octet = (digest[0] % 254) + 1
|
|
# Skip the docker-default bridge to dodge the most common
|
|
# collision (operators with `docker0` at 172.17.x.x or a
|
|
# 192.168.17.x VPN client).
|
|
if octet == 17:
|
|
octet = 18
|
|
subnet = f"192.168.{octet}.0/24"
|
|
gateway = f"192.168.{octet}.1"
|
|
bundle_ip = f"192.168.{octet}.2"
|
|
return subnet, gateway, bundle_ip
|