fix(template): clean snapshot chain on deploy --force; seal cloud-init; harden first-boot IP

deploy-linux --force reused the VM directory but left any prior snapshot
chain in place, so vmware-vdiskmanager refused the Step 4 resize with
'disk is part of a snapshot chain'. Add _clean_force_artifacts to remove
snapshot deltas (.vmdk), .vmsn, .vmsd and stale .lck before resize.

prepare-linux Step 7d seals the image: drop the stale cloud-init netplan
(leaving only the MAC-agnostic 99-ci-dhcp-allnics), regenerate networkd
config, and disable cloud-init so it never re-runs on linked clones.

deploy user-data sets package_update/upgrade: false so cloud-init does
not hold the dpkg lock at first boot and delay open-vm-tools past the
IP-detection poll; the step-7 IP timeout is raised 300s -> 600s as a
safety net for slow first boots.

Adds unit tests for _clean_force_artifacts (snapshot chain, lock dir,
clean-dir no-op) and updates prepare-linux stubs for the seal step.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-06 12:55:34 +02:00
parent 2b0e98774f
commit e91e55f20e
2 changed files with 140 additions and 2 deletions
+57
View File
@@ -1854,6 +1854,9 @@ def _stub_prepare(
return 0, "", ""
if "echo connected" in cmd:
return 0, "connected", ""
# Step 7d cloud-init seal verification.
if "cloud-init.disabled" in cmd and "echo DISABLED" in cmd:
return 0, "DISABLED\nNO50\n", ""
# validation script
return 0, val_output, ""
@@ -2063,6 +2066,8 @@ def test_prepare_linux_happy_path_skip_flags(
return 0, "", ""
if "echo connected" in cmd:
return 0, "connected", ""
if "cloud-init.disabled" in cmd and "echo DISABLED" in cmd:
return 0, "DISABLED\nNO50\n", ""
return 0, "UserSudo:OK\nSudoNoPass:OK\n", ""
def fake_pl_stream(client: object, cmd: str) -> int:
@@ -2119,3 +2124,55 @@ def test_prepare_linux_existing_snapshot_deleted(
cmds = [" ".join(c) for c in run_calls]
assert any("deleteSnapshot" in c for c in cmds)
assert any("snapshot" in c and "deleteSnapshot" not in c for c in cmds)
# ── _clean_force_artifacts (deploy-linux --force snapshot-chain cleanup) ────────
def test_clean_force_artifacts_removes_snapshot_chain(tmp_path: Path) -> None:
"""Snapshot delta/.vmsn/.vmsd/.lck are removed; base disk + vmx kept."""
(tmp_path / "LinuxBuild2404.vmdk").write_text("base")
(tmp_path / "LinuxBuild2404.vmx").write_text("vmx")
(tmp_path / "LinuxBuild2404-000001.vmdk").write_text("delta")
(tmp_path / "LinuxBuild2404-000002.vmdk").write_text("delta2")
(tmp_path / "LinuxBuild2404-Snapshot7.vmsn").write_text("snap")
(tmp_path / "LinuxBuild2404.vmsd").write_text("snapdb")
(tmp_path / "LinuxBuild2404.vmdk.lck").write_text("lock")
removed = tmpl_mod._clean_force_artifacts(tmp_path, "LinuxBuild2404")
assert set(removed) == {
"LinuxBuild2404-000001.vmdk",
"LinuxBuild2404-000002.vmdk",
"LinuxBuild2404-Snapshot7.vmsn",
"LinuxBuild2404.vmsd",
"LinuxBuild2404.vmdk.lck",
}
# Base disk and vmx survive (Step 4/5 overwrite them).
assert (tmp_path / "LinuxBuild2404.vmdk").is_file()
assert (tmp_path / "LinuxBuild2404.vmx").is_file()
# Chain artifacts gone.
assert not (tmp_path / "LinuxBuild2404-000001.vmdk").exists()
assert not (tmp_path / "LinuxBuild2404.vmsd").exists()
def test_clean_force_artifacts_removes_lock_directory(tmp_path: Path) -> None:
"""Lock entries may be directories (VMware writes .lck dirs)."""
lck = tmp_path / "LinuxBuild2404.vmx.lck"
lck.mkdir()
(lck / "M12345.lck").write_text("host")
removed = tmpl_mod._clean_force_artifacts(tmp_path, "LinuxBuild2404")
assert removed == ["LinuxBuild2404.vmx.lck"]
assert not lck.exists()
def test_clean_force_artifacts_noop_on_clean_dir(tmp_path: Path) -> None:
"""A clean directory yields no removals and leaves the base disk."""
(tmp_path / "LinuxBuild2404.vmdk").write_text("base")
removed = tmpl_mod._clean_force_artifacts(tmp_path, "LinuxBuild2404")
assert removed == []
assert (tmp_path / "LinuxBuild2404.vmdk").is_file()