diff --git a/jobs.py b/jobs.py index c4bd391e2..811e0bc20 100755 --- a/jobs.py +++ b/jobs.py @@ -350,6 +350,19 @@ "paths": ["tests/guest-tools/unix"], "markers": "multi_vms", }, + "tools-windows": { + "description": "tests our windows guest tools on a variety of VMs", + "requirements": [ + "A pool >= 8.3. One host is enough.", + "A variety of windows VMs supported by our tools installer.", + ], + "nb_pools": 1, + "params": { + "--vm[]": "multi/tools_windows", + }, + "paths": ["tests/guest-tools/win"], + "markers": "multi_vms and windows_vm", + }, "xen": { "description": "Testing of the Xen hypervisor itself", "requirements": [ diff --git a/tests/guest-tools/win/__init__.py b/tests/guest-tools/win/__init__.py new file mode 100644 index 000000000..8c58fcd22 --- /dev/null +++ b/tests/guest-tools/win/__init__.py @@ -0,0 +1,24 @@ +import logging +import re +from lib.common import wait_for +from lib.vm import VM + + +def try_get_and_store_vm_ip_serial(vm: VM, timeout: int): + domid = vm.param_get("dom-id") + logging.debug(f"Domain ID {domid}") + command = f"xl console -t serial {domid} | grep '~xcp-ng-tests~.*~end~' | head -n 1" + if timeout > 0: + command = f"timeout {timeout} " + command + report = vm.host.ssh(command) + logging.debug(f"Got report: {report}") + match = re.match("~xcp-ng-tests~(.*)=(.*)~end~", report) + if not match: + return False + vm.ip = match[2] + return True + + +def wait_for_vm_running_and_ssh_up_without_tools(vm: VM): + wait_for(vm.is_running, "Wait for VM running") + wait_for(vm.is_ssh_up, "Wait for SSH up") diff --git a/tests/guest-tools/win/test_guest_tools_win.py b/tests/guest-tools/win/test_guest_tools_win.py new file mode 100644 index 000000000..120690408 --- /dev/null +++ b/tests/guest-tools/win/test_guest_tools_win.py @@ -0,0 +1,305 @@ +import enum +import logging +import pytest +from lib.common import wait_for +from lib.host import Host +from lib.snapshot import Snapshot +from lib.vdi import VDI +from lib.vm import VM +from . import * + +from data import ISO_DOWNLOAD_URL + +# Requirements: +# - XCP-ng >= 8.3. +# +# From --vm parameter: +# - A Windows VM with the following requirements: +# - Xen PV tools not installed +# - Testsigning enabled +# - Reports its IP via serial console on boot in this format: +# "~xcp-ng-tests~=~end~\r\n" +# The VM should report its IP frequently since the test script acquires the VM's IP based on a timeout. +# - Git Bash +# - OpenSSH: +# - Server enabled and allowed by firewall +# - SSH key installed into %ProgramData%\ssh\administrators_authorized_keys with appropriate permissions +# - Registry value "HKLM\SOFTWARE\OpenSSH DefaultShell" set to Git Bash +# +# Specific configuration: +# - ISO image of guest tools under test with the following structure: +# guest-tools-win.iso +# ├───package +# │ └───XenDrivers-x64.msi +# └───testsign +# └───*.crt +# - ISO image of other guest tools with the following structure: +# other-guest-tools-win.iso +# ├───citrix-9.4.0 +# │ ├───XenBus +# │ ├───XenIface +# │ ├───XenNet +# │ ├───XenVbd +# │ ├───XenVif +# │ └───managementagent-9.4.0-x64.msi +# ├───xcp-ng-8.2.2.200 +# │ └───managementagentx64.msi +# ├───xcp-ng-9.0.9000 +# │ ├───package +# │ │ └───XenDrivers-x64.msi +# │ └───testsign +# │ └───*.crt +# └───install-drivers.ps1 + +ERROR_SUCCESS = 0 +ERROR_INSTALL_FAILURE = 1603 +ERROR_SUCCESS_REBOOT_INITIATED = 1641 +ERROR_SUCCESS_REBOOT_REQUIRED = 3010 + + +class PowerAction(enum.Enum): + Nothing = "nothing" + Shutdown = "shutdown" + Reboot = "reboot" + + +@pytest.fixture(scope="module") +def guest_tools_iso(host: Host): + vdi = host.import_iso(ISO_DOWNLOAD_URL + "guest-tools-win.iso", host.iso_sr_uuid()) + yield vdi + vdi.destroy() + + +@pytest.fixture(scope="module") +def other_tools_iso(host: Host): + vdi = host.import_iso(ISO_DOWNLOAD_URL + "other-guest-tools-win.iso", host.iso_sr_uuid()) + yield vdi + vdi.destroy() + + +def install_guest_tools(vm: VM, action: PowerAction, check: bool = True): + msiexec_args = "-i C:\\XenDrivers-x64.msi -log C:\\tools_install.log -passive -norestart" + + if action == PowerAction.Nothing: + exitcode = vm.run_powershell_command("msiexec.exe", msiexec_args) + else: + if check: + raise Exception(f"Cannot check exit code with {action} action") + # when powershell runs msiexec it doesn't wait for it to end unlike ssh + # it only waits for stdin closing so we need Start-Process -Wait here + install_cmd = f"Start-Process -Wait msiexec.exe -ArgumentList '{msiexec_args}'" + if action != PowerAction.Nothing: + install_cmd += ";Stop-Computer -Force" + vm.start_background_powershell(install_cmd) + if action != PowerAction.Nothing: + wait_for(vm.is_halted, "Wait for VM halted") + if action == PowerAction.Reboot: + vm.start() + wait_for_vm_running_and_ssh_up_without_tools(vm) + exitcode = None + + if check: + assert exitcode in [ERROR_SUCCESS, ERROR_SUCCESS_REBOOT_INITIATED, ERROR_SUCCESS_REBOOT_REQUIRED] + return exitcode + + +def uninstall_guest_tools(vm: VM, action: PowerAction): + uninstall_cmd = ( + "Start-Process -Wait msiexec.exe -ArgumentList " + "'/x C:\\XenDrivers-x64.msi /l* C:\\tools_uninstall.log /passive /norestart'" + ) + if action != PowerAction.Nothing: + uninstall_cmd += ";Stop-Computer -Force" + vm.start_background_powershell(uninstall_cmd) + if action != PowerAction.Nothing: + wait_for(vm.is_halted, "Wait for VM halted") + if action == PowerAction.Reboot: + vm.start() + wait_for_vm_running_and_ssh_up_without_tools(vm) + + +def install_cert_and_tools(vm: VM, guest_tools_iso: VDI, action: PowerAction, check: bool = True): + vm.insert_cd(guest_tools_iso.name()) + wait_for(lambda: vm.path_exists("D:/")) + + logging.info("Install VM root certs") + vm.ssh("certutil -addstore -f Root D:/testsign/XCP-ng_Test_Signer.crt") + vm.ssh("certutil -addstore -f TrustedPublisher D:/testsign/XCP-ng_Test_Signer.crt") + + logging.info("Copy Windows PV drivers to VM") + vm.ssh( + "powershell.exe -noprofile -noninteractive Copy-Item -Force D:/package/XenDrivers-x64.msi C:/XenDrivers-x64.msi" + ) + + vm.eject_cd() + logging.info("Install Windows PV drivers") + return install_guest_tools(vm, action=action, check=check) + + +def install_other_drivers(vm: VM, other_tools_iso: VDI, driver_name: str, is_msi: bool): + vm.insert_cd(other_tools_iso.name()) + wait_for(lambda: vm.path_exists("D:/")) + + install_cmd = "D:\\install-drivers.ps1 -Shutdown " + if is_msi: + logging.info(f"Install {driver_name} MSI drivers") + install_cmd += f'-MsiPath "D:\\{driver_name}" ' + else: + logging.info(f"Install {driver_name} drivers") + install_cmd += f'-DriverPath "D:\\{driver_name}" ' + install_cmd += ">C:\\othertools.log" + vm.start_background_powershell(install_cmd) + wait_for(vm.is_halted, "Shutdown VM") + + vm.eject_cd() + vm.start() + wait_for_vm_running_and_ssh_up_without_tools(vm) + + +@pytest.fixture(scope="module") +def running_windows_vm_without_tools(imported_vm: VM) -> VM: + vm = imported_vm + if not vm.is_running(): + vm.start() + wait_for(vm.is_running, "Wait for VM running") + # whenever the guest changes its serial port config, xl console will drop out + # retry several times to force xl console to refresh + wait_for(lambda: try_get_and_store_vm_ip_serial(vm, timeout=10), "Wait for VM IP", 300) + logging.info(f"VM IP: {vm.ip}") + wait_for(vm.is_ssh_up, "Wait for VM SSH up") + return vm + # no teardown + + +@pytest.fixture(scope="module") +def unsealed_windows_vm_and_snapshot(running_windows_vm_without_tools: VM): + """Unseal VM and get its IP, then shut it down. Cache the unsealed state in a snapshot to save time.""" + vm = running_windows_vm_without_tools + # vm shutdown is not usable yet (there's no tools) + vm.ssh(["powershell.exe", "-noprofile", "-noninteractive", "Stop-Computer", "-Force"]) + wait_for(vm.is_halted, "Shutdown VM") + snapshot = vm.snapshot() + yield vm, snapshot + snapshot.destroy(verify=True) + + +@pytest.fixture +def running_unsealed_windows_vm(unsealed_windows_vm_and_snapshot: tuple[VM, Snapshot]): + vm, snapshot = unsealed_windows_vm_and_snapshot + vm.start() + wait_for_vm_running_and_ssh_up_without_tools(vm) + yield vm + snapshot.revert() + + +@pytest.mark.multi_vms +@pytest.mark.usefixtures("windows_vm") +class TestGuestToolsWindows: + @pytest.fixture(scope="class") + def vm_install_test_tools(self, unsealed_windows_vm_and_snapshot, guest_tools_iso): + vm, snapshot = unsealed_windows_vm_and_snapshot + vm.start() + wait_for_vm_running_and_ssh_up_without_tools(vm) + install_cert_and_tools(vm, guest_tools_iso, PowerAction.Reboot, check=False) + yield vm + snapshot.revert() + + def test_tools_after_reboot(self, vm_install_test_tools: VM): + vm = vm_install_test_tools + assert vm.are_windows_drivers_installed() + + def test_drivers_detected(self, vm_install_test_tools: VM): + vm = vm_install_test_tools + assert vm.param_get("PV-drivers-detected") + + +@pytest.mark.multi_vms +@pytest.mark.usefixtures("windows_vm") +class TestGuestToolsWindowsDestructive: + @pytest.fixture + def vm_install_test_tools(self, running_unsealed_windows_vm: VM, guest_tools_iso): + install_cert_and_tools(running_unsealed_windows_vm, guest_tools_iso, PowerAction.Nothing) + return running_unsealed_windows_vm + + @pytest.fixture + def vm_install_citrix_tools(self, running_unsealed_windows_vm: VM, other_tools_iso: VDI): + install_other_drivers( + running_unsealed_windows_vm, other_tools_iso, "citrix-9.4.0\\managementagent-9.4.0-x64.msi", is_msi=True + ) + return running_unsealed_windows_vm + + @pytest.fixture + def vm_install_xcpng_v8_tools(self, running_unsealed_windows_vm: VM, other_tools_iso: VDI): + install_other_drivers( + running_unsealed_windows_vm, other_tools_iso, "xcp-ng-8.2.2.200\\managementagentx64.msi", is_msi=True + ) + return running_unsealed_windows_vm + + @pytest.fixture + def vm_install_xcpng_v9_tools(self, running_unsealed_windows_vm: VM, other_tools_iso: VDI): + vm = running_unsealed_windows_vm + + vm.insert_cd(other_tools_iso.name()) + wait_for(lambda: vm.path_exists("D:/")) + + logging.info("Install VM root certs") + vm.ssh("certutil -addstore -f Root D:/xcp-ng-9.0.9000/testsign/XCP-ng_Test_Signer.crt") + vm.ssh("certutil -addstore -f TrustedPublisher D:/xcp-ng-9.0.9000/testsign/XCP-ng_Test_Signer.crt") + + vm.eject_cd() + + install_other_drivers(vm, other_tools_iso, "xcp-ng-9.0.9000\\package\\XenDrivers-x64.msi", is_msi=True) + return vm + + @pytest.fixture + def vm_install_citrix_vendor_drivers(self, unsealed_windows_vm_and_snapshot: VM, other_tools_iso: VDI): + vm, snapshot = unsealed_windows_vm_and_snapshot + assert not vm.param_get("has-vendor-device") + vm.param_set("has-vendor-device", True) + vm.start() + wait_for_vm_running_and_ssh_up_without_tools(vm) + install_other_drivers(vm, other_tools_iso, "citrix-9.4.0", is_msi=False) + yield vm + snapshot.revert() + + def test_uninstall_tools(self, vm_install_test_tools: VM): + vm = vm_install_test_tools + vm.reboot() + wait_for_vm_running_and_ssh_up_without_tools(vm) + logging.info("Uninstall Windows PV drivers") + uninstall_guest_tools(vm, action=PowerAction.Reboot) + assert not vm.are_windows_drivers_installed() + + def test_uninstall_tools_early(self, vm_install_test_tools: VM): + vm = vm_install_test_tools + logging.info("Uninstall Windows PV drivers before rebooting") + uninstall_guest_tools(vm, action=PowerAction.Reboot) + assert not vm.are_windows_drivers_installed() + + def test_reinstall_tools_early(self, vm_install_test_tools: VM): + vm = vm_install_test_tools + vm.reboot() + wait_for_vm_running_and_ssh_up_without_tools(vm) + logging.info("Uninstall Windows PV drivers before reinstalling") + uninstall_guest_tools(vm, action=PowerAction.Nothing) + install_guest_tools(vm, action=PowerAction.Reboot, check=False) + assert vm.are_windows_drivers_installed() + + def test_install_with_citrix_tools(self, vm_install_citrix_tools: VM, guest_tools_iso: VDI): + exitcode = install_cert_and_tools(vm_install_citrix_tools, guest_tools_iso, PowerAction.Nothing, check=False) + assert exitcode == ERROR_INSTALL_FAILURE + + def test_install_with_xcpng_v8_tools(self, vm_install_xcpng_v8_tools: VM, guest_tools_iso: VDI): + exitcode = install_cert_and_tools(vm_install_xcpng_v8_tools, guest_tools_iso, PowerAction.Nothing, check=False) + assert exitcode == ERROR_INSTALL_FAILURE + + def test_upgrade_with_xcpng_v9_tools(self, vm_install_xcpng_v9_tools: VM, guest_tools_iso: VDI): + vm = vm_install_xcpng_v9_tools + install_cert_and_tools(vm, guest_tools_iso, PowerAction.Reboot, check=False) + assert vm.are_windows_drivers_installed() + + def test_install_with_citrix_vendor_drivers(self, vm_install_citrix_vendor_drivers: VM, guest_tools_iso: VDI): + exitcode = install_cert_and_tools( + vm_install_citrix_vendor_drivers, guest_tools_iso, PowerAction.Nothing, check=False + ) + assert exitcode == ERROR_INSTALL_FAILURE diff --git a/vm_data.py-dist b/vm_data.py-dist index e43140bab..9d460c6c1 100644 --- a/vm_data.py-dist +++ b/vm_data.py-dist @@ -38,6 +38,8 @@ VMS = { "small_vm_windows": "", # Debian VM (UEFI, no GUI) "debian_uefi_vm": "", + # "small" Windows VM with testsign enabled + "small_vm_windows_testsign": "", }, "multi": { # all VMs we want to run "multi_vms" tests on @@ -48,6 +50,8 @@ VMS = { "uefi_unix": [], # UEFI Windows VMs "uefi_windows": [], + # Testsign UEFI Windows VMs + "tools_windows": [], } }