Skip to content

Commit

Permalink
guest-tools/win: refactor remote execution, add tests with existing t…
Browse files Browse the repository at this point in the history
…ools
  • Loading branch information
Tu Dinh committed Oct 31, 2024
1 parent 3874aac commit aacbed8
Show file tree
Hide file tree
Showing 2 changed files with 175 additions and 51 deletions.
33 changes: 32 additions & 1 deletion tests/guest-tools/win/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import base64
import logging
import re
from typing import List
from lib.common import wait_for
from lib.vm import VM

Expand Down Expand Up @@ -40,10 +41,11 @@ def is_drivers_installed(vm: VM):
output = vm.ssh(
[
"powershell.exe",
"-noprofile",
"-noninteractive",
"-encodedcommand",
encode_ps_command(
r"""
$ProgressPreference = 'SilentlyContinue';
Get-PnpDevice -PresentOnly |
Where-Object CompatibleID -icontains 'PCI\VEN_5853' |
Select-Object -ExpandProperty Problem
Expand All @@ -60,11 +62,40 @@ def is_drivers_installed(vm: VM):
raise Exception(f"Unknown problem status {output}")


def run_command_powershell(vm: VM, program: str, args: List[str]):
"""
Run command under powershell to retrieve exit codes higher than 255.
Backslash-safe.
"""
# ProgressPreference is needed to suppress any clixml progress output as it's not filtered away from stdout by default, and we're grabbing stdout
cmd = f"$ProgressPreference = 'SilentlyContinue'; Write-Output (Start-Process -Wait -PassThru {program} -ArgumentList '{" ".join(args)}').ExitCode"
return int(
vm.ssh(
[
"powershell.exe",
# suppress the "Preparing modules for first use" output
"-noprofile",
"-noninteractive",
"-encodedcommand",
encode_ps_command(cmd),
]
)
)


def start_background_powershell(vm: VM, cmd: str):
"""
Run command under powershell in the background.
Backslash-safe.
"""
async_cmd = f"\\'powershell.exe -encodedcommand {encode_ps_command(cmd)}\\'"
vm.ssh(
[
"powershell.exe",
"-noprofile",
"-noninteractive",
"Invoke-WmiMethod",
"-Class",
"Win32_Process",
Expand Down
193 changes: 143 additions & 50 deletions tests/guest-tools/win/test_guest_tools_win.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,39 @@
import enum
import logging
from typing import Generator
import pytest
import time
from conftest import imported_vm
from lib.common import wait_for
from lib.host import Host
from lib.snapshot import Snapshot
from lib.vm import VM
from . import *


def install_guest_tools(vm: VM):
# msiexec always fails when the install requires reboot so don't check here
# it also doesn't like forward slashes in msi filenames so use backslashes instead
# thus the ridiculous backslashing
vm.ssh(
ERROR_SUCCESS = 0
ERROR_INSTALL_FAILURE = 1603
ERROR_SUCCESS_REBOOT_INITIATED = 1641
ERROR_SUCCESS_REBOOT_REQUIRED = 3010


def install_guest_tools(vm: VM, check: bool = True):
exitcode = run_command_powershell(
vm,
"msiexec.exe",
[
"msiexec.exe",
"-i",
"C:\\\\\\\\XenDrivers-x64.msi",
"C:\\XenDrivers-x64.msi",
"-log",
"C:/tools_install.log",
"C:\\tools_install.log",
"-passive",
"-norestart",
],
check=False,
)
if check:
assert exitcode in [
ERROR_SUCCESS,
ERROR_SUCCESS_REBOOT_INITIATED,
ERROR_SUCCESS_REBOOT_REQUIRED,
]
return exitcode


class PowerAction(enum.Enum):
Expand All @@ -35,9 +42,11 @@ class PowerAction(enum.Enum):
Reboot = "reboot"


# Unlike install_guest_tools, uninstall_guest_tools is asynchronous since it disconnects the VM's networking.
# For that reason, you must specify whether to reboot or shutdown after installing.
def uninstall_guest_tools(vm: VM, action: PowerAction):
"""
Unlike install_guest_tools, uninstall_guest_tools is asynchronous since it disconnects the VM's networking.
For that reason, you must specify whether to reboot or shutdown after installing.
"""
uninstall_cmd = [
"Start-Sleep -Seconds 5",
# when powershell runs msiexec it doesn't wait for it to end unlike ssh
Expand All @@ -58,30 +67,7 @@ def uninstall_guest_tools(vm: VM, action: PowerAction):
wait_for_guest_start(vm)


@pytest.fixture(scope="module")
def running_windows_vm(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
for _ in range(30):
try:
logging.info("Wait for VM IP")
vm.ip = try_get_and_store_ip_serial(vm, timeout=10)
break
except:
pass
if not vm.ip:
raise Exception("Cannot find VM IP")
logging.info(f"VM IP: {vm.ip}")
wait_for(vm.is_ssh_up, "Wait for VM SSH up")
return vm
# no teardown


def install_tools_noreboot(vm: VM):
def install_tools_noreboot(vm: VM, check: bool = True):
vm.insert_cd("guest-tools-win.iso")
# wait a small amount of time just to ensure the device is available
time.sleep(5)
Expand Down Expand Up @@ -110,6 +96,8 @@ def install_tools_noreboot(vm: VM):
vm.ssh(
[
"powershell.exe",
"-noprofile",
"-noninteractive",
"Copy-Item",
"-Force",
"D:/package/XenDrivers-x64.msi",
Expand All @@ -118,11 +106,60 @@ def install_tools_noreboot(vm: VM):
)

vm.eject_cd()

logging.info("Install Windows PV drivers")
install_guest_tools(vm)
return install_guest_tools(vm, check=check)


def install_other_drivers(vm: VM, name: str, is_msi: bool):
vm.insert_cd("other-guest-tools-win.iso")
time.sleep(5)

install_cmd = [
"D:\\install-drivers.ps1",
"-Shutdown",
]
if is_msi:
logging.info(f"Install {name} MSI drivers")
install_cmd += [
"-MsiPath",
f"D:\\{name}",
]
else:
logging.info(f"Install {name} drivers")
install_cmd += [
"-DriverPath",
f"D:\\{name}",
]
install_cmd += [">C:\\othertools.log"]
start_background_powershell(vm, " ".join(install_cmd))
wait_for(vm.is_halted, "Shutdown VM")

vm.eject_cd()
vm.start()
wait_for_guest_start(vm)


@pytest.fixture(scope="module")
def running_windows_vm(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
for _ in range(30):
try:
logging.info("Wait for VM IP")
vm.ip = try_get_and_store_ip_serial(vm, timeout=10)
break
except:
pass
if not vm.ip:
raise Exception("Cannot find VM IP")
logging.info(f"VM IP: {vm.ip}")
wait_for(vm.is_ssh_up, "Wait for VM SSH up")
return vm
# no teardown


@pytest.mark.multi_vms
Expand All @@ -136,11 +173,11 @@ def vm_install(self, running_windows_vm):
wait_for_guest_start(vm)
return vm

def test_check_tools_after_reboot(self, vm_install: VM):
def test_tools_after_reboot(self, vm_install: VM):
vm = vm_install
assert is_drivers_installed(vm)

def test_check_drivers_detected(self, vm_install: VM):
def test_drivers_detected(self, vm_install: VM):
vm = vm_install
assert vm.param_get("PV-drivers-detected")

Expand All @@ -150,39 +187,83 @@ def test_check_drivers_detected(self, vm_install: VM):
class TestGuestToolsWindowsDestructive:
@pytest.fixture(scope="class")
def vm_prepared(self, running_windows_vm: VM):
"""Unseal VM and get its IP. Cache the unsealed state in a snapshot to save time."""
"""Unseal VM and get its IP, then shut it down. Cache the unsealed state in a snapshot to save time."""
vm = running_windows_vm
# vm shutdown is not usable yet (there's no tools)
vm.ssh(["powershell.exe", "Stop-Computer", "-Force"])
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(scope="function")
def vm_install(self, vm_prepared: tuple[VM, Snapshot]):
@pytest.fixture
def vm_instance(self, vm_prepared: tuple[VM, Snapshot]):
(vm, snapshot) = vm_prepared
vm.start()
wait_for_guest_start(vm)
install_tools_noreboot(vm)
yield vm
snapshot.revert()

def test_check_uninstall_tools(self, vm_install: VM):
@pytest.fixture
def vm_install(self, vm_instance: VM):
install_tools_noreboot(vm_instance)
return vm_instance

@pytest.fixture
def vm_install_citrix_tools(self, vm_instance: VM):
install_other_drivers(
vm_instance,
"citrix-9.4.0\\managementagent-9.4.0-x64.msi",
is_msi=True,
)
return vm_instance

@pytest.fixture
def vm_install_xcpng_tools(self, vm_instance: VM):
install_other_drivers(
vm_instance,
"xcp-ng-8.2.2.200\\managementagentx64.msi",
is_msi=True,
)
return vm_instance

@pytest.fixture
def vm_install_wu_drivers(self, vm_prepared: VM):
(vm, snapshot) = vm_prepared
vm.param_set("has-vendor-device", True)
vm.start()
wait_for_guest_start(vm)
install_other_drivers(
vm,
"citrix-9.4.0",
is_msi=False,
)
yield vm
snapshot.revert()

def test_uninstall_tools(self, vm_install: VM):
vm = vm_install
vm.reboot()
wait_for_guest_start(vm)
logging.info("Uninstall Windows PV drivers")
uninstall_guest_tools(vm, action=PowerAction.Reboot)
assert not is_drivers_installed(vm)

def test_check_uninstall_tools_early(self, vm_install: VM):
def test_uninstall_tools_early(self, vm_install: VM):
vm = vm_install
logging.info("Uninstall Windows PV drivers before rebooting")
uninstall_guest_tools(vm, action=PowerAction.Reboot)
assert not is_drivers_installed(vm)

def test_check_reinstall_tools_early(self, vm_install: VM):
def test_reinstall_tools_early(self, vm_install: VM):
vm = vm_install
vm.reboot()
wait_for_guest_start(vm)
Expand All @@ -192,3 +273,15 @@ def test_check_reinstall_tools_early(self, vm_install: VM):
vm.reboot()
wait_for_guest_start(vm)
assert is_drivers_installed(vm)

def test_install_with_citrix_tools(self, vm_install_citrix_tools: VM):
exitcode = install_tools_noreboot(vm_install_citrix_tools, check=False)
assert exitcode == ERROR_INSTALL_FAILURE

def test_install_with_xcpng_tools(self, vm_install_xcpng_tools: VM):
exitcode = install_tools_noreboot(vm_install_xcpng_tools, check=False)
assert exitcode == ERROR_INSTALL_FAILURE

def test_install_with_citrix_wu(self, vm_install_wu_drivers: VM):
exitcode = install_tools_noreboot(vm_install_wu_drivers, check=False)
assert exitcode == ERROR_INSTALL_FAILURE

0 comments on commit aacbed8

Please sign in to comment.