refactor(deploy-win): drop UUP dump, take ISO via --iso-url / --win-iso

On a Linux host the UUP converter only produces the RTM install.wim (build
26100.1), so Windows Update had to install the entire cumulative anyway —
the UUP download/convert machinery added a slow ~5 GB step and several host
tools (aria2/wimtools/cabextract/chntpw) for no benefit.

Replace it: deploy-win now takes the install ISO directly via `--iso-url`
(urllib download + cache by file name under <CI_ROOT>/iso/, optional
--iso-sha256) or a local `--win-iso` (exactly one required). Patch + deploy
flow is unchanged. Supply an already-updated ISO (e.g. a massgrave link) to
skip the WU step entirely.

- Remove winimage/uup.py and its tests.
- Trim setup-host-linux.sh deps to p7zip-full + xorriso + genisoimage.
- Update RUNBOOK §5 and the Windows-host port plan. Coverage 93.6%.
This commit is contained in:
2026-06-09 18:42:22 +02:00
parent cd88dec15d
commit c257c931c3
8 changed files with 108 additions and 445 deletions
+3 -134
View File
@@ -1,4 +1,4 @@
"""Unit tests for the winimage subpackage (uup, autounattend, iso_patch)."""
"""Unit tests for the winimage subpackage (autounattend, iso_patch)."""
from __future__ import annotations
@@ -11,142 +11,10 @@ import pytest
from ci_orchestrator.winimage import autounattend as au
from ci_orchestrator.winimage import iso_patch as ip
from ci_orchestrator.winimage import uup
# ── uup ────────────────────────────────────────────────────────────────────
_FAKE_BUILDS: dict[str, Any] = {
"response": {
"builds": {
"1": {"title": "Windows Server 2025 (26100.2033)", "build": "26100.2033",
"arch": "amd64", "uuid": "old"},
"2": {"title": "Windows Server 2025 (26100.32860)", "build": "26100.32860",
"arch": "amd64", "uuid": "new"},
"3": {"title": "Windows Server, version 23H2 (25398.1)", "build": "25398.1",
"arch": "amd64", "uuid": "h2"},
"4": {"title": "Windows Server 2025 (26100.1)", "build": "26100.1",
"arch": "arm64", "uuid": "arm"},
}
}
}
@pytest.fixture()
def _fake_api(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(uup, "_http_json", lambda *_a, **_k: _FAKE_BUILDS)
def test_build_sort_key() -> None:
assert uup._build_sort_key("26100.32860") == (26100, 32860)
assert uup._build_sort_key("26100.x") == (26100, 0)
def test_list_filters_arch_and_h2(_fake_api: None) -> None:
builds = uup.list_ws2025_builds(arch="amd64")
names = [b.build for b in builds]
assert names == ["26100.32860", "26100.2033"] # newest first, no 23H2, no arm64
def test_resolve_latest(_fake_api: None) -> None:
assert uup.resolve_build().uuid == "new"
def test_resolve_pinned(_fake_api: None) -> None:
assert uup.resolve_build(build="26100.2033").uuid == "old"
def test_resolve_pinned_missing(_fake_api: None) -> None:
with pytest.raises(click.ClickException, match="not found"):
uup.resolve_build(build="99999.1")
def test_resolve_none_available(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(uup, "_http_json", lambda *_a, **_k: {"response": {"builds": {}}})
with pytest.raises(click.ClickException, match="No Windows Server 2025"):
uup.resolve_build()
def test_check_tools_all_present(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(uup.shutil, "which", lambda _n: "/usr/bin/x")
assert uup.check_tools() == []
def test_check_tools_missing(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(uup.shutil, "which", lambda _n: None)
missing = uup.check_tools()
assert "aria2c" in missing and "genisoimage|mkisofs" in missing
def test_cache_path() -> None:
b = uup.UupBuild(uuid="u", build="26100.1", title="t", arch="amd64")
assert uup.cache_path(Path("/iso"), b) == Path("/iso/ws2025-26100.1.iso")
def test_build_iso_uses_cache(tmp_path: Path) -> None:
b = uup.UupBuild(uuid="u", build="26100.1", title="t", arch="amd64")
cached = uup.cache_path(tmp_path, b)
cached.write_bytes(b"iso")
assert uup.build_iso(b, dest_dir=tmp_path) == cached
def test_build_iso_missing_tools(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(uup, "check_tools", lambda: ["aria2c"])
b = uup.UupBuild(uuid="u", build="26100.9", title="t", arch="amd64")
with pytest.raises(click.ClickException, match="Missing UUP"):
uup.build_iso(b, dest_dir=tmp_path)
def test_build_iso_full_flow(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
b = uup.UupBuild(uuid="u", build="26100.5", title="t", arch="amd64")
monkeypatch.setattr(uup, "check_tools", lambda: [])
def fake_download(_b: uup.UupBuild, _l: str, _e: str, dest: Path, **_k: Any) -> None:
import zipfile
with zipfile.ZipFile(dest, "w") as zf:
zf.writestr("uup_download_linux.sh", "#!/bin/bash\ntrue\n")
monkeypatch.setattr(uup, "_download_package", fake_download)
def fake_run(argv: list[str], **kw: Any) -> subprocess.CompletedProcess[str]:
# simulate the converter producing an ISO in the work dir
work = Path(kw["cwd"])
(work / "26100.5_SERVER.iso").write_bytes(b"x" * 500)
return subprocess.CompletedProcess(argv, 0, "", "")
monkeypatch.setattr(uup.subprocess, "run", fake_run)
out = uup.build_iso(b, dest_dir=tmp_path)
assert out == uup.cache_path(tmp_path, b)
assert out.is_file()
def test_build_iso_converter_fails(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
b = uup.UupBuild(uuid="u", build="26100.6", title="t", arch="amd64")
monkeypatch.setattr(uup, "check_tools", lambda: [])
def fake_download(_b: uup.UupBuild, _l: str, _e: str, dest: Path, **_k: Any) -> None:
import zipfile
with zipfile.ZipFile(dest, "w") as zf:
zf.writestr("uup_download_linux.sh", "#!/bin/bash\nfalse\n")
monkeypatch.setattr(uup, "_download_package", fake_download)
monkeypatch.setattr(
uup.subprocess, "run",
lambda argv, **kw: subprocess.CompletedProcess(argv, 1, "", ""),
)
with pytest.raises(click.ClickException, match="UUP conversion failed"):
uup.build_iso(b, dest_dir=tmp_path)
def test_find_produced_iso(tmp_path: Path) -> None:
(tmp_path / "small.iso").write_bytes(b"a")
big = tmp_path / "big.iso"
big.write_bytes(b"a" * 100)
assert uup._find_produced_iso(tmp_path) == big
assert uup._find_produced_iso(tmp_path / "empty") is None or True # nonexistent dir → no match
# ── autounattend ───────────────────────────────────────────────────────────
def test_render_autounattend_no_placeholder() -> None:
xml = au.render_autounattend(au.WinUnattendParams())
assert "{{" not in xml and "}}" not in xml
@@ -176,6 +44,7 @@ def test_apply_detects_leftover() -> None:
# ── iso_patch ──────────────────────────────────────────────────────────────
def test_read_volid_fallback(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
monkeypatch.setattr(ip.shutil, "which", lambda _n: None)
iso = tmp_path / "x.iso"