Skip to content

Commit

Permalink
Add Windows tools install/uninstall/upgrade tests
Browse files Browse the repository at this point in the history
Signed-off-by: Tu Dinh <[email protected]>
  • Loading branch information
Tu Dinh committed Nov 28, 2024
1 parent 485df19 commit 25cea38
Show file tree
Hide file tree
Showing 8 changed files with 404 additions and 0 deletions.
48 changes: 48 additions & 0 deletions data.py-dist
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,54 @@ DEF_VM_URL = 'http://pxe/images/'
# Guest tools ISO download location
ISO_DOWNLOAD_URL = 'http://pxe/isos/'

# Definitions of Windows guest tool ISOs to be tested
WIN_GUEST_TOOLS_ISOS = {
"stable": {
# ISO name on SR or subpath of ISO_DOWNLOAD_URL
"name": "guest-tools-win.iso",
# Whether ISO should be downloaded from ISO_DOWNLOAD_URL
"download": True,
# ISO-relative path of MSI file to be installed
"package": "package\\XenDrivers-x64.msi",
# ISO-relative path of XenClean script
"xenclean_path": "package\\XenClean\\x64\\Invoke-XenClean.ps1",
# ISO-relative path of root cert file to be installed before guest tools (optional)
"testsign_cert": "testsign\\XCP-ng_Test_Signer.crt",
},
# Add more guest tool ISOs here as needed
}

# Definition of ISO containing other guest tools to be tested
OTHER_GUEST_TOOLS_ISO = {
"name": "other-guest-tools-win-bak.iso",
"download": False,
}

# Definitions of other guest tools contained in OTHER_GUEST_TOOLS_ISO
OTHER_GUEST_TOOLS = {
"xcp-ng-9.0.9000": {
# ISO-relative path of this guest tool
"path": "xcp-ng-9.0.9000",
# "path"-relative path of MSI or driver files to be installed
"package": "package\\XenDrivers-x64.msi",
# Is the above path a MSI file path (set to False for path to driver files)
"is_msi": True,
# Can we upgrade automatically from this guest tool to our tools?
"upgradable": True,
# Relative path of root cert file (optional)
"testsign_cert": "testsign\\XCP-ng_Test_Signer.crt",
# Whether this guest tool version wants vendor device to be activated (optional, defaults to False)
"vendor_device": False,
},
"citrix-9.4.0-vendor": {
"path": "citrix-9.4.0",
"is_msi": False,
"package": "",
"upgradable": False,
"vendor_device": True,
},
}

# Values can be either full URLs or only partial URLs that will be automatically appended to DEF_VM_URL
VM_IMAGES = {
'mini-linux-x86_64-bios': 'alpine-minimal-3.12.0.xva',
Expand Down
13 changes: 13 additions & 0 deletions jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down
45 changes: 45 additions & 0 deletions tests/guest-tools/win/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import enum
import logging
import re
from typing import Any
from data import ISO_DOWNLOAD_URL
from lib.common import wait_for
from lib.host import Host
from lib.vm import VM


class PowerAction(enum.Enum):
Nothing = "nothing"
Shutdown = "shutdown"
Reboot = "reboot"


def iso_create(host: Host, param: dict[str, Any]):
if param["download"]:
vdi = host.import_iso(ISO_DOWNLOAD_URL + param["name"], host.iso_sr_uuid())
new_param = param.copy()
new_param["name"] = vdi.name()
yield new_param
vdi.destroy()
else:
yield param


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")
91 changes: 91 additions & 0 deletions tests/guest-tools/win/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import logging
from typing import Any
import pytest

from data import OTHER_GUEST_TOOLS, OTHER_GUEST_TOOLS_ISO, WIN_GUEST_TOOLS_ISOS
from lib.common import wait_for
from lib.host import Host
from lib.snapshot import Snapshot
from lib.vm import VM
from . import PowerAction, iso_create, try_get_and_store_vm_ip_serial, wait_for_vm_running_and_ssh_up_without_tools
from .guest_tools import install_guest_tools
from .other_tools import install_other_drivers


@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.fixture(scope="class")
def vm_install_test_tools_per_test_class(unsealed_windows_vm_and_snapshot, guest_tools_iso: dict[str, Any]):
vm, snapshot = unsealed_windows_vm_and_snapshot
vm.start()
wait_for_vm_running_and_ssh_up_without_tools(vm)
install_guest_tools(vm, guest_tools_iso, PowerAction.Reboot, check=False)
yield vm
snapshot.revert()


@pytest.fixture
def vm_install_test_tools(running_unsealed_windows_vm: VM, guest_tools_iso: dict[str, Any]):
install_guest_tools(running_unsealed_windows_vm, guest_tools_iso, PowerAction.Nothing)
return running_unsealed_windows_vm


@pytest.fixture(
scope="module",
ids=list(WIN_GUEST_TOOLS_ISOS.keys()),
params=list(WIN_GUEST_TOOLS_ISOS.values()),
)
def guest_tools_iso(host: Host, request: pytest.FixtureRequest):
yield from iso_create(host, request.param)


@pytest.fixture(scope="module")
def other_tools_iso(host: Host):
yield from iso_create(host, OTHER_GUEST_TOOLS_ISO)


@pytest.fixture(ids=list(OTHER_GUEST_TOOLS.keys()), params=list(OTHER_GUEST_TOOLS.values()))
def vm_install_other_drivers(
unsealed_windows_vm_and_snapshot: tuple[VM, Snapshot],
other_tools_iso: dict[str, Any],
request: pytest.FixtureRequest,
):
vm, snapshot = unsealed_windows_vm_and_snapshot
param = request.param
install_other_drivers(vm, other_tools_iso["name"], param)
yield vm, param
snapshot.revert()
70 changes: 70 additions & 0 deletions tests/guest-tools/win/guest_tools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import logging
from pathlib import PureWindowsPath
from typing import Any

from lib.common import wait_for
from lib.vm import VM
from . import PowerAction, wait_for_vm_running_and_ssh_up_without_tools


ERROR_SUCCESS = 0
ERROR_INSTALL_FAILURE = 1603
ERROR_SUCCESS_REBOOT_INITIATED = 1641
ERROR_SUCCESS_REBOOT_REQUIRED = 3010

GUEST_TOOLS_COPY_PATH = "C:\\package.msi"


def install_guest_tools(vm: VM, guest_tools_iso: dict[str, Any], action: PowerAction, check: bool = True):
vm.insert_cd(guest_tools_iso["name"])
wait_for(lambda: vm.path_exists("D:/"))

if guest_tools_iso.get("testsign_cert"):
logging.info("Install VM root certs")
rootcert = PureWindowsPath("D:\\") / guest_tools_iso["testsign_cert"]
vm.execute_powershell_script(f"certutil -addstore -f Root '{rootcert}'")
vm.execute_powershell_script(f"certutil -addstore -f TrustedPublisher '{rootcert}'")

logging.info("Copy Windows PV drivers to VM")
package_path = PureWindowsPath("D:\\") / guest_tools_iso["package"]
vm.execute_powershell_script(f"Copy-Item -Force '{package_path}' '{GUEST_TOOLS_COPY_PATH}'")

vm.eject_cd()

logging.info("Install Windows PV drivers")
msiexec_args = f"/i {GUEST_TOOLS_COPY_PATH} /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):
msiexec_args = f"/x {GUEST_TOOLS_COPY_PATH} /log C:\\tools_uninstall.log /passive /norestart"
uninstall_cmd = f"Start-Process -Wait msiexec.exe -ArgumentList '{msiexec_args}'"
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)
41 changes: 41 additions & 0 deletions tests/guest-tools/win/other_tools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import logging
from pathlib import PureWindowsPath
from typing import Any

from lib.common import wait_for
from lib.vm import VM
from . import wait_for_vm_running_and_ssh_up_without_tools


def install_other_drivers(vm: VM, other_tools_iso_name: str, param: dict[str, Any]):
if param.get("vendor_device"):
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)

vm.insert_cd(other_tools_iso_name)
wait_for(lambda: vm.path_exists("D:/"))

if param.get("testsign_cert"):
logging.info("Install VM root certs")
rootcert = PureWindowsPath("D:\\") / param["path"] / param["testsign_cert"]
vm.execute_powershell_script(f"certutil -addstore -f Root '{rootcert}'")
vm.execute_powershell_script(f"certutil -addstore -f TrustedPublisher '{rootcert}'")

package_path = PureWindowsPath("D:\\") / param["path"] / param["package"]
install_cmd = "D:\\install-drivers.ps1 -Shutdown "
if param["is_msi"]:
logging.info(f"Install MSI drivers: {package_path}")
install_cmd += f"-MsiPath '{package_path}' "
else:
logging.info(f"Install drivers: {package_path}")
install_cmd += f"-DriverPath '{package_path}' "
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)
Loading

0 comments on commit 25cea38

Please sign in to comment.