Files
local-ci-cd-system/tests/python/test_commands_deploy_win.py
T
Simone b83a7fa624 feat(template): add deploy-win — UUP download + ISO patch + deploy WS2025
New `ci template deploy-win` builds the Windows Server 2025 template VM on
the Linux host, mirroring the legacy Deploy-WinBuild2025.ps1 ten-step flow
in Python:

- winimage/uup.py: resolve the newest (or pinned) WS2025 build via the
  uupdump API and build a cached, MS-signed ISO via the uup converter.
- winimage/autounattend.py + assets/: render autounattend.xml and the
  guest post-install.ps1 (ported from the PS heredoc; VMware Tools now
  mounted as a 2nd CD, scanned at first logon).
- winimage/iso_patch.py: xorriso repack injecting the answer files at the
  ISO root and swapping the EFI boot image to efisys_noprompt.bin
  (no "press any key" prompt — essential for an unattended headless install).
- commands/template.py: the deploy-win command + _winbuild_vmx_lines
  (the 82-key VMX), _remove_vmx_lines, _wait_install_flag, _vmrun_soft_stop.
  Acquire → patch → VMDK → VMX → install → flag wait → strip media +
  NIC e1000e→vmxnet3 → verify IP → cold snapshot.

Deterministic by build (--build), with --win-iso to bypass UUP. Tests
mirror the prepare-win mocking style; total coverage 93%.
2026-06-08 23:21:02 +02:00

213 lines
8.4 KiB
Python

"""Tests for ``template deploy-win`` (Windows Server 2025, Linux host)."""
from __future__ import annotations
import subprocess
import types
from pathlib import Path
from typing import Any
import pytest
from click.testing import CliRunner
import ci_orchestrator.commands.template as tmpl_mod
from ci_orchestrator.__main__ import cli
from ci_orchestrator.winimage import uup
def _make_config(root: Path) -> types.SimpleNamespace:
paths = types.SimpleNamespace(
root=root,
templates=root / "templates",
build_vms=root / "build-vms",
artifacts=root / "artifacts",
keys=root / "keys",
)
return types.SimpleNamespace(vmrun_path=None, paths=paths, ssh_key_path=None)
# ── helper unit tests ───────────────────────────────────────────────────────
def test_winbuild_vmx_lines() -> None:
lines = tmpl_mod._winbuild_vmx_lines(
vm_name="WinBuild2025", mem_mb=6144, vcpu_count=4, cores_per_socket=2,
vmdk_name="WinBuild2025.vmdk", install_iso="/iso/in.iso", tools_iso="/iso/tools.iso",
)
text = "\n".join(lines)
assert 'guestOS = "windows2019srvnext-64"' in text
assert 'firmware = "efi"' in text
assert 'sata0:0.fileName = "/iso/in.iso"' in text
assert 'sata0:1.fileName = "/iso/tools.iso"' in text
assert 'ethernet0.virtualDev = "e1000e"' in text
assert 'nvme0:0.fileName = "WinBuild2025.vmdk"' in text
def test_remove_vmx_lines(tmp_path: Path) -> None:
vmx = tmp_path / "x.vmx"
vmx.write_text(
'nvme0:0.present = "TRUE"\n'
'sata0:0.present = "TRUE"\n'
'sata0:1.fileName = "t.iso"\n'
'usb.present = "TRUE"\n'
'ethernet0.virtualDev = "e1000e"\n',
encoding="ascii",
)
assert tmpl_mod._remove_vmx_lines(vmx, "sata0") == 2
assert tmpl_mod._remove_vmx_lines(vmx, "usb") == 1
remaining = vmx.read_text(encoding="ascii")
assert "sata0" not in remaining and "usb" not in remaining
assert "nvme0:0" in remaining and "ethernet0" in remaining
def test_wait_install_flag_ok(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(tmpl_mod.time, "sleep", lambda _s: None)
def fake_run(argv: list[str], **_kw: Any) -> subprocess.CompletedProcess[str]:
if "checkToolsState" in argv:
return subprocess.CompletedProcess(argv, 0, "running\n", "")
if "fileExistsInGuest" in argv:
return subprocess.CompletedProcess(argv, 0, "The file exists.\n", "")
return subprocess.CompletedProcess(argv, 0, "", "")
monkeypatch.setattr(tmpl_mod.subprocess, "run", fake_run)
assert tmpl_mod._wait_install_flag(
"vmrun", "x.vmx", admin_user="a", admin_password="p",
flag_path="C:\\f.flag", timeout_s=10.0, poll_s=0.0,
) is True
def test_wait_install_flag_timeout(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(tmpl_mod.time, "sleep", lambda _s: None)
monkeypatch.setattr(
tmpl_mod.subprocess, "run",
lambda argv, **_k: subprocess.CompletedProcess(argv, 0, "not-running", ""),
)
assert tmpl_mod._wait_install_flag(
"vmrun", "x.vmx", admin_user="a", admin_password="p",
flag_path="C:\\f.flag", timeout_s=-1.0, poll_s=0.0,
) is False
# ── CLI flow ────────────────────────────────────────────────────────────────
def _patch_deploy(
monkeypatch: pytest.MonkeyPatch, root: Path, *, snapshot: str = "PostInstall"
) -> dict[str, Any]:
"""Stub all external effects; return a dict capturing key calls."""
seen: dict[str, Any] = {"patched": False, "vmdk": False}
vdisk = root / "vdiskmgr"
vdisk.write_bytes(b"")
monkeypatch.setattr(tmpl_mod, "load_config", lambda: _make_config(root))
monkeypatch.setattr(tmpl_mod, "_find_vdisk_manager", lambda _e: str(vdisk))
monkeypatch.setattr(tmpl_mod.shutil, "which", lambda name: f"/usr/bin/{name}")
monkeypatch.setattr(tmpl_mod.time, "sleep", lambda _s: None)
monkeypatch.setattr(tmpl_mod, "_wait_install_flag", lambda *a, **k: True)
monkeypatch.setattr(tmpl_mod, "_poll_guest_ip_deploy", lambda *a, **k: "192.168.79.50")
monkeypatch.setattr(tmpl_mod, "_vmrun_soft_stop", lambda *a, **k: None)
def fake_patch_iso(base: Path, *, out_iso: Path, **_k: Any) -> Path:
out_iso.write_bytes(b"iso")
seen["patched"] = True
return out_iso
monkeypatch.setattr(tmpl_mod.win_iso_patch, "patch_iso", fake_patch_iso)
def fake_run(argv: list[str], **_kw: Any) -> subprocess.CompletedProcess[str]:
if "listSnapshots" in argv:
return subprocess.CompletedProcess(argv, 0, f"Total: 1\n{snapshot}\n", "")
if "vdiskmgr" in str(argv[0]):
seen["vmdk"] = True
return subprocess.CompletedProcess(argv, 0, "", "")
monkeypatch.setattr(tmpl_mod.subprocess, "run", fake_run)
monkeypatch.setattr(tmpl_mod.subprocess, "Popen", lambda *a, **k: types.SimpleNamespace())
return seen
def test_deploy_win_winiso_happy(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
root = tmp_path
(root / "iso").mkdir(parents=True)
tools = root / "iso" / "VMware-tools-windows.iso"
tools.write_bytes(b"tools")
base = root / "iso" / "ws.iso"
base.write_bytes(b"win")
seen = _patch_deploy(monkeypatch, root)
result = CliRunner().invoke(
cli,
["template", "deploy-win", "--win-iso", str(base), "--nogui"],
)
assert result.exit_code == 0, result.output
assert seen["patched"] and seen["vmdk"]
assert "deploy-win complete" in result.output
vmx = root / "templates" / "WinBuild2025" / "WinBuild2025.vmx"
assert vmx.is_file()
text = vmx.read_text(encoding="ascii")
# Step 9 stripped install media + swapped NIC
assert "sata0" not in text
assert 'ethernet0.virtualDev = "vmxnet3"' in text
def test_deploy_win_uup_happy(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
root = tmp_path
(root / "iso").mkdir(parents=True)
(root / "iso" / "VMware-tools-windows.iso").write_bytes(b"tools")
_patch_deploy(monkeypatch, root)
monkeypatch.setattr(tmpl_mod.win_uup, "check_tools", lambda: [])
build = uup.UupBuild(uuid="u", build="26100.32860", title="t", arch="amd64")
monkeypatch.setattr(tmpl_mod.win_uup, "resolve_build", lambda **_k: build)
def fake_build_iso(_b: uup.UupBuild, *, dest_dir: Path, **_k: Any) -> Path:
p = dest_dir / "ws2025-26100.32860.iso"
p.write_bytes(b"iso")
return p
monkeypatch.setattr(tmpl_mod.win_uup, "build_iso", fake_build_iso)
result = CliRunner().invoke(cli, ["template", "deploy-win", "--nogui"])
assert result.exit_code == 0, result.output
assert "26100.32860" in result.output
def test_deploy_win_bad_computer_name(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
root = tmp_path
(root / "iso").mkdir(parents=True)
(root / "iso" / "VMware-tools-windows.iso").write_bytes(b"t")
base = root / "iso" / "ws.iso"
base.write_bytes(b"w")
_patch_deploy(monkeypatch, root)
result = CliRunner().invoke(
cli,
["template", "deploy-win", "--win-iso", str(base), "--computer-name", "bad name!"],
)
assert result.exit_code != 0
assert "invalid computer name" in result.output
def test_deploy_win_missing_tools_iso(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
root = tmp_path
(root / "iso").mkdir(parents=True)
base = root / "iso" / "ws.iso"
base.write_bytes(b"w")
_patch_deploy(monkeypatch, root) # tools iso intentionally absent
result = CliRunner().invoke(
cli, ["template", "deploy-win", "--win-iso", str(base)]
)
assert result.exit_code != 0
assert "VMware Tools ISO" in result.output
def test_deploy_win_flag_timeout(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
root = tmp_path
(root / "iso").mkdir(parents=True)
(root / "iso" / "VMware-tools-windows.iso").write_bytes(b"t")
base = root / "iso" / "ws.iso"
base.write_bytes(b"w")
_patch_deploy(monkeypatch, root)
monkeypatch.setattr(tmpl_mod, "_wait_install_flag", lambda *a, **k: False)
result = CliRunner().invoke(
cli, ["template", "deploy-win", "--win-iso", str(base), "--nogui"]
)
assert result.exit_code != 0
assert "install_complete.flag not seen" in result.output