"""Turning a catalog cloud image (qcow2/raw) into a VMDK vSphere can import.""" from __future__ import annotations import hashlib import shutil import subprocess import pytest from napalm_vmware.provision import image as image_mod from napalm_vmware.provision.image import ImageCache URL = "https://cloud.example.org/debian-13-genericcloud-amd64.qcow2" CONTENT = b"qcow2-bytes" SHA = hashlib.sha256(CONTENT).hexdigest() class _Fake: def __init__(self, payloads=(CONTENT,)): self.payloads = list(payloads) self.fetched = 0 self.converted = 0 def fetch(self, url, dest, timeout): dest.write_bytes(self.payloads[min(self.fetched, len(self.payloads) - 1)]) self.fetched += 1 def convert(self, src, dst): dst.write_bytes(b"vmdk:" + src.read_bytes()) self.converted += 1 def size(self, path): return 2 * 1024**3 def _cache(tmp_path, fake): return ImageCache(tmp_path, fetch=fake.fetch, convert=fake.convert, virtual_size=fake.size) def test_first_use_downloads_and_converts(tmp_path): fake = _Fake() vmdk, size = _cache(tmp_path, fake).vmdk(URL, f"sha256:{SHA}", timeout=60) assert vmdk.read_bytes() == b"vmdk:" + CONTENT assert size == 2 * 1024**3 assert (fake.fetched, fake.converted) == (1, 1) def test_second_use_is_served_from_the_cache(tmp_path): fake = _Fake() _cache(tmp_path, fake).vmdk(URL, None, timeout=60) vmdk, size = _cache(tmp_path, fake).vmdk(URL, None, timeout=60) assert (fake.fetched, fake.converted) == (1, 1) assert size == 2 * 1024**3 def test_the_download_is_not_kept_after_conversion(tmp_path): _cache(tmp_path, _Fake()).vmdk(URL, None, timeout=60) assert [p.suffix for p in tmp_path.iterdir()] and not list(tmp_path.glob("*.download")) def test_a_corrupt_download_is_retried_once(tmp_path): fake = _Fake(payloads=(b"truncated", CONTENT)) vmdk, _ = _cache(tmp_path, fake).vmdk(URL, f"sha256:{SHA}", timeout=60) assert fake.fetched == 2 assert vmdk.read_bytes() == b"vmdk:" + CONTENT def test_a_persistent_mismatch_is_an_error(tmp_path): fake = _Fake(payloads=(b"wrong",)) with pytest.raises(RuntimeError, match="Checksum mismatch"): _cache(tmp_path, fake).vmdk(URL, f"sha256:{SHA}", timeout=60) assert fake.converted == 0 assert not list(tmp_path.glob("*.vmdk")) def test_bare_hex_checksum_means_sha256(tmp_path): _cache(tmp_path, _Fake()).vmdk(URL, SHA, timeout=60) def test_different_urls_do_not_share_a_cache_entry(tmp_path): fake = _Fake() cache = _cache(tmp_path, fake) cache.vmdk(URL, None, timeout=60) cache.vmdk(URL + "?v=2", None, timeout=60) assert fake.converted == 2 def test_missing_qemu_img_is_explained(monkeypatch, tmp_path): monkeypatch.setattr(image_mod.shutil, "which", lambda name: None) with pytest.raises(RuntimeError, match="qemu-img"): image_mod.convert_to_vmdk(tmp_path / "a", tmp_path / "b") @pytest.mark.skipif(not shutil.which("qemu-img"), reason="qemu-img not installed") def test_real_conversion_round_trip(tmp_path): raw = tmp_path / "disk.raw" subprocess.run( ["qemu-img", "create", "-f", "raw", str(raw), "64M"], check=True, capture_output=True ) vmdk = tmp_path / "disk.vmdk" image_mod.convert_to_vmdk(raw, vmdk) info = subprocess.run( ["qemu-img", "info", str(vmdk)], check=True, capture_output=True, text=True ).stdout assert "streamOptimized" in info assert image_mod.virtual_size(raw) == 64 * 1024**2