Harden default environment pull behavior

Fix the default one-shot install path so empty bundled profile directories no longer win over OCI-backed environment pulls or leave broken cached symlinks behind.

Treat cached installs as valid only when the manifest and boot artifacts are all present, repair invalid installs on the next pull, and add human-mode phase markers for env pull and run without changing JSON output.

Align the Python lifecycle example and public docs with the current exec_vm/vm_exec auto-clean semantics, and validate the slice with focused pytest coverage, make check, make dist-check, and a real default-path pull/inspect/run smoke.
This commit is contained in:
Thales Maciel 2026-03-11 19:27:09 -03:00
parent 694be0730b
commit 6e16e74fd5
16 changed files with 384 additions and 91 deletions

View file

@ -68,6 +68,19 @@ def _fake_runtime_paths(tmp_path: Path) -> RuntimePaths:
)
def _write_local_profile(
runtime_paths: RuntimePaths,
profile_name: str,
*,
kernel: str = "kernel\n",
rootfs: str = "rootfs\n",
) -> None:
profile_dir = runtime_paths.artifacts_dir / profile_name
profile_dir.mkdir(parents=True, exist_ok=True)
(profile_dir / "vmlinux").write_text(kernel, encoding="utf-8")
(profile_dir / "rootfs.ext4").write_text(rootfs, encoding="utf-8")
def _sha256_digest(payload: bytes) -> str:
return f"sha256:{hashlib.sha256(payload).hexdigest()}"
@ -108,7 +121,9 @@ def test_get_environment_rejects_unknown() -> None:
def test_environment_store_installs_from_local_runtime_source(tmp_path: Path) -> None:
store = EnvironmentStore(runtime_paths=resolve_runtime_paths(), cache_dir=tmp_path / "cache")
runtime_paths = _fake_runtime_paths(tmp_path)
_write_local_profile(runtime_paths, "debian-git")
store = EnvironmentStore(runtime_paths=runtime_paths, cache_dir=tmp_path / "cache")
installed = store.ensure_installed("debian:12")
assert installed.kernel_image.exists()
@ -117,7 +132,9 @@ def test_environment_store_installs_from_local_runtime_source(tmp_path: Path) ->
def test_environment_store_pull_and_cached_inspect(tmp_path: Path) -> None:
store = EnvironmentStore(runtime_paths=resolve_runtime_paths(), cache_dir=tmp_path / "cache")
runtime_paths = _fake_runtime_paths(tmp_path)
_write_local_profile(runtime_paths, "debian-git")
store = EnvironmentStore(runtime_paths=runtime_paths, cache_dir=tmp_path / "cache")
before = store.inspect_environment("debian:12")
assert before["installed"] is False
@ -145,7 +162,7 @@ def test_environment_store_uses_env_override_for_default_cache_dir(
def test_environment_store_installs_from_archive_when_runtime_source_missing(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
runtime_paths = resolve_runtime_paths()
runtime_paths = _fake_runtime_paths(tmp_path)
source_environment = get_environment("debian:12-base", runtime_paths=runtime_paths)
archive_dir = tmp_path / "archive"
@ -157,30 +174,6 @@ def test_environment_store_installs_from_archive_when_runtime_source_missing(
archive.add(archive_dir / "vmlinux", arcname="vmlinux")
archive.add(archive_dir / "rootfs.ext4", arcname="rootfs.ext4")
missing_bundle = tmp_path / "bundle"
platform_root = missing_bundle / "linux-x86_64"
platform_root.mkdir(parents=True, exist_ok=True)
(missing_bundle / "NOTICE").write_text(
runtime_paths.notice_path.read_text(encoding="utf-8"),
encoding="utf-8",
)
(platform_root / "manifest.json").write_text(
runtime_paths.manifest_path.read_text(encoding="utf-8"),
encoding="utf-8",
)
(platform_root / "bin").mkdir(parents=True, exist_ok=True)
(platform_root / "bin" / "firecracker").write_bytes(runtime_paths.firecracker_bin.read_bytes())
(platform_root / "bin" / "jailer").write_bytes(runtime_paths.jailer_bin.read_bytes())
guest_agent_path = runtime_paths.guest_agent_path
if guest_agent_path is None:
raise AssertionError("expected guest agent path")
(platform_root / "guest").mkdir(parents=True, exist_ok=True)
(platform_root / "guest" / "pyro_guest_agent.py").write_text(
guest_agent_path.read_text(encoding="utf-8"),
encoding="utf-8",
)
monkeypatch.setenv("PYRO_RUNTIME_BUNDLE_DIR", str(missing_bundle))
monkeypatch.setattr(
"pyro_mcp.vm_environments.CATALOG",
{
@ -200,7 +193,7 @@ def test_environment_store_installs_from_archive_when_runtime_source_missing(
},
)
store = EnvironmentStore(
runtime_paths=resolve_runtime_paths(verify_checksums=False),
runtime_paths=runtime_paths,
cache_dir=tmp_path / "cache",
)
installed = store.ensure_installed("debian:12-base")
@ -209,6 +202,91 @@ def test_environment_store_installs_from_archive_when_runtime_source_missing(
assert installed.rootfs_image.read_text(encoding="utf-8") == "rootfs\n"
def test_environment_store_skips_empty_local_source_dir_and_uses_archive(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
runtime_paths = _fake_runtime_paths(tmp_path)
source_environment = get_environment("debian:12-base", runtime_paths=runtime_paths)
(runtime_paths.artifacts_dir / source_environment.source_profile).mkdir(
parents=True,
exist_ok=True,
)
archive_dir = tmp_path / "archive"
archive_dir.mkdir(parents=True, exist_ok=True)
(archive_dir / "vmlinux").write_text("kernel\n", encoding="utf-8")
(archive_dir / "rootfs.ext4").write_text("rootfs\n", encoding="utf-8")
archive_path = tmp_path / "environment.tgz"
with tarfile.open(archive_path, "w:gz") as archive:
archive.add(archive_dir / "vmlinux", arcname="vmlinux")
archive.add(archive_dir / "rootfs.ext4", arcname="rootfs.ext4")
monkeypatch.setattr(
"pyro_mcp.vm_environments.CATALOG",
{
"debian:12-base": source_environment.__class__(
name=source_environment.name,
version=source_environment.version,
description=source_environment.description,
default_packages=source_environment.default_packages,
distribution=source_environment.distribution,
distribution_version=source_environment.distribution_version,
source_profile=source_environment.source_profile,
platform=source_environment.platform,
source_url=archive_path.resolve().as_uri(),
source_digest=source_environment.source_digest,
compatibility=source_environment.compatibility,
)
},
)
store = EnvironmentStore(runtime_paths=runtime_paths, cache_dir=tmp_path / "cache")
installed = store.ensure_installed("debian:12-base")
assert installed.kernel_image.read_text(encoding="utf-8") == "kernel\n"
assert installed.rootfs_image.read_text(encoding="utf-8") == "rootfs\n"
def test_environment_store_marks_broken_symlink_install_uninstalled_and_repairs_it(
tmp_path: Path,
) -> None:
runtime_paths = _fake_runtime_paths(tmp_path)
_write_local_profile(
runtime_paths,
"debian-git",
kernel="kernel-fixed\n",
rootfs="rootfs-fixed\n",
)
store = EnvironmentStore(runtime_paths=runtime_paths, cache_dir=tmp_path / "cache")
spec = get_environment("debian:12", runtime_paths=runtime_paths)
install_dir = store.cache_dir / "linux-x86_64" / "debian_12-1.0.0"
install_dir.mkdir(parents=True, exist_ok=True)
(install_dir / "environment.json").write_text(
json.dumps(
{
"catalog_version": "2.0.0",
"name": spec.name,
"version": spec.version,
"source": "bundled-runtime-source",
"source_digest": spec.source_digest,
"installed_at": 0,
}
),
encoding="utf-8",
)
(install_dir / "vmlinux").symlink_to("missing-vmlinux")
(install_dir / "rootfs.ext4").symlink_to("missing-rootfs.ext4")
inspected_before = store.inspect_environment("debian:12")
assert inspected_before["installed"] is False
pulled = store.pull_environment("debian:12")
assert pulled["installed"] is True
assert Path(str(pulled["kernel_image"])).read_text(encoding="utf-8") == "kernel-fixed\n"
assert Path(str(pulled["rootfs_image"])).read_text(encoding="utf-8") == "rootfs-fixed\n"
def test_environment_store_prunes_stale_entries(tmp_path: Path) -> None:
store = EnvironmentStore(runtime_paths=resolve_runtime_paths(), cache_dir=tmp_path / "cache")
platform_dir = store.cache_dir / "linux-x86_64"