Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Windows PV tools installer tests #262

Merged
merged 12 commits into from
Jan 27, 2025
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ __pycache__
*/__pycache__
data.py
vm_data.py
/scripts/guests/windows/id_rsa.pub
24 changes: 24 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -463,3 +463,27 @@ def second_network(pytestconfig, host):
if network_uuid == host.management_network():
pytest.fail("--second-network must NOT be the management network")
return network_uuid

@pytest.fixture(scope='module')
def nfs_iso_device_config():
return global_config.sr_device_config("NFS_ISO_DEVICE_CONFIG", required=['location'])

@pytest.fixture(scope='module')
def cifs_iso_device_config():
return global_config.sr_device_config("CIFS_ISO_DEVICE_CONFIG")

@pytest.fixture(scope='module')
def nfs_iso_sr(host, nfs_iso_device_config):
""" A NFS ISO SR. """
sr = host.sr_create('iso', "ISO-NFS-SR-test", nfs_iso_device_config, shared=True, verify=True)
yield sr
# teardown
sr.forget()

@pytest.fixture(scope='module')
def cifs_iso_sr(host, cifs_iso_device_config):
""" A Samba/CIFS SR. """
sr = host.sr_create('iso', "ISO-CIFS-SR-test", cifs_iso_device_config, shared=True, verify=True)
yield sr
# teardown
sr.forget()
52 changes: 52 additions & 0 deletions data.py-dist
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,57 @@ PXE_CONFIG_SERVER = 'pxe'
# Default VM images location
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.iso",
"download": False,
}

# Definitions of other guest tools contained in OTHER_GUEST_TOOLS_ISO
OTHER_GUEST_TOOLS = {
"xcp-ng-9.0.9000": {
# Whether we are installing MSI files ("msi"), bare .inf drivers ("inf")
# or nothing in case of Windows Update (absent or null)
"type": "msi",
# 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",
# 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)
# Note: other guest tools may not install correctly with this setting enabled
"vendor_device": False,

# Can we upgrade automatically from this guest tool to our tools?
"upgradable": True,
},
"vendor": {
"vendor_device": True,
"upgradable": False,
},
}

# 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 All @@ -40,6 +91,7 @@ VM_IMAGES = {
# Possible values:
# - 'default': keep using the pool's default SR
# - 'local': use the first local SR found instead
# - A UUID of the SR to be used
DEFAULT_SR = 'default'

# Whether to cache VMs on the test host, that is import them only if not already
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.2. One host is enough.",
"A variety of windows VMs supported by our tools installer.",
],
"nb_pools": 1,
"params": {
"--vm[]": "multi/tools_windows",
dinhngtu marked this conversation as resolved.
Show resolved Hide resolved
},
"paths": ["tests/guest-tools/win"],
"markers": "multi_vms",
},
"xen": {
"description": "Testing of the Xen hypervisor itself",
"requirements": [
Expand Down
9 changes: 8 additions & 1 deletion lib/commands.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import base64
import logging
import shlex
import subprocess
Expand Down Expand Up @@ -71,7 +72,10 @@ def _ssh(hostname_or_ip, cmd, check, simple_output, suppress_fingerprint_warning
opts.append('-o "LogLevel ERROR"')
opts.append('-o "UserKnownHostsFile /dev/null"')

command = " ".join(cmd)
if isinstance(cmd, str):
command = cmd
else:
command = " ".join(cmd)
if background and target_os != "windows":
# https://stackoverflow.com/questions/29142/getting-ssh-to-execute-a-command-in-the-background-on-target-machine
# ... and run the command through a bash shell so that output redirection both works on Linux and FreeBSD.
Expand Down Expand Up @@ -222,3 +226,6 @@ def local_cmd(cmd, check=True, decode=True):
raise LocalCommandFailed(res.returncode, output_for_logs, command)

return LocalCommandResult(res.returncode, output)

def encode_powershell_command(cmd: str):
return base64.b64encode(cmd.encode("utf-16-le")).decode("ascii")
41 changes: 38 additions & 3 deletions lib/host.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import os
import shlex
import tempfile
import uuid

from packaging import version

Expand All @@ -14,6 +15,7 @@
from lib.common import prefix_object_name
from lib.netutil import wrap_ip
from lib.sr import SR
from lib.vdi import VDI
from lib.vm import VM
from lib.xo import xo_cli, xo_object_exists

Expand Down Expand Up @@ -268,6 +270,37 @@ def import_vm(self, uri, sr_uuid=None, use_cache=False):
vm.param_set('name-description', cache_key)
return vm

def import_iso(self, uri, sr: SR):
random_name = str(uuid.uuid4())

vdi_uuid = self.xe(
"vdi-create",
{
"sr-uuid": sr.uuid,
"name-label": random_name,
"virtual-size": "0",
},
)

try:
params = {'uuid': vdi_uuid}
if '://' in uri:
logging.info(f"Download ISO {uri}")
download_path = f'/tmp/{vdi_uuid}'
self.ssh(f"curl -o '{download_path}' '{uri}'")
params['filename'] = download_path
else:
download_path = None
params['filename'] = uri
logging.info(f"Import ISO {uri}: name {random_name}, uuid {vdi_uuid}")

self.xe('vdi-import', params)
finally:
if download_path:
self.ssh(f"rm -f '{download_path}'")

return VDI(vdi_uuid, sr=sr)

def pool_has_vm(self, vm_uuid, vm_type='vm'):
if vm_type == 'snapshot':
return self.xe('snapshot-list', {'uuid': vm_uuid}, minimal=True) == vm_uuid
Expand Down Expand Up @@ -494,12 +527,11 @@ def local_vm_srs(self):
return srs

def main_sr_uuid(self):
""" Main SR is either the default SR, or the first local SR, depending on data.py's DEFAULT_SR. """
""" Main SR is the default SR, the first local SR, or a specific SR depending on data.py's DEFAULT_SR. """
try:
from data import DEFAULT_SR
except ImportError:
DEFAULT_SR = 'default'
assert DEFAULT_SR in ['default', 'local']

sr_uuid = None
if DEFAULT_SR == 'local':
Expand All @@ -512,9 +544,12 @@ def main_sr_uuid(self):
)
assert local_sr_uuids, f"DEFAULT_SR=='local' so there must be a local SR on host {self}"
sr_uuid = local_sr_uuids[0]
else:
elif DEFAULT_SR == 'default':
sr_uuid = self.pool.param_get('default-SR')
assert sr_uuid, f"DEFAULT_SR='default' so there must be a default SR on the pool of host {self}"
else:
sr_uuid = DEFAULT_SR
assert self.xe('sr-list', {'uuid': sr_uuid}), f"cannot find SR with UUID {sr_uuid} on host {self}"
assert sr_uuid != "<not in database>"
return sr_uuid

Expand Down
3 changes: 3 additions & 0 deletions lib/vdi.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ def __init__(self, uuid, *, host=None, sr=None):
else:
self.sr = sr

def name(self):
return self.param_get('name-label')

def destroy(self):
logging.info("Destroy %s", self)
self.sr.pool.master.xe('vdi-destroy', {'uuid': self.uuid})
Expand Down
94 changes: 91 additions & 3 deletions lib/vm.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import lib.efi as efi

from lib.basevm import BaseVM
from lib.common import PackageManagerEnum, parse_xe_dict, safe_split, wait_for, wait_for_not
from lib.common import PackageManagerEnum, parse_xe_dict, safe_split, strtobool, wait_for, wait_for_not
from lib.snapshot import Snapshot
from lib.vif import VIF

Expand Down Expand Up @@ -321,9 +321,10 @@ def tools_version(self):
version_dict = self.tools_version_dict()
return "{major}.{minor}.{micro}-{build}".format(**version_dict)

def file_exists(self, filepath):
def file_exists(self, filepath, regular_file=True):
"""Returns True if the file exists, otherwise returns False."""
return self.ssh_with_result(['test', '-f', filepath]).returncode == 0
option = '-f' if regular_file else '-e'
return self.ssh_with_result(['test', option, filepath]).returncode == 0

def detect_package_manager(self):
""" Heuristic to determine the package manager on a unix distro. """
Expand Down Expand Up @@ -587,3 +588,90 @@ def is_cert_present(vm, key):
res = vm.host.ssh(['varstore-get', vm.uuid, efi.get_secure_boot_guid(key).as_str(), key],
check=False, simple_output=False, decode=False)
return res.returncode == 0

def execute_powershell_script(
self,
script_contents: str,
simple_output=True,
prepend="$ProgressPreference = 'SilentlyContinue';"):
# ProgressPreference is needed to suppress any clixml progress output,
# as it's not filtered away from stdout by default, and we're grabbing stdout.
assert self.is_windows
if prepend is not None:
script_contents = prepend + script_contents
cmd = commands.encode_powershell_command(script_contents)
return self.ssh(
f"powershell.exe -nologo -noprofile -noninteractive -encodedcommand {cmd}",
simple_output=simple_output,
)

def run_powershell_command(self, program: str, args: str):
"""
Run command under powershell to retrieve exit codes higher than 255.

Backslash-safe.
"""
assert self.is_windows
output = self.execute_powershell_script(
f"Write-Output (Start-Process -Wait -PassThru {program} -ArgumentList '{args}').ExitCode")
return int(output)

def start_background_powershell(self, cmd: str):
"""
Run command under powershell in the background.

Backslash-safe.
"""
assert self.is_windows
encoded_command = commands.encode_powershell_command(cmd)
self.ssh(
"powershell.exe -noprofile -noninteractive Invoke-WmiMethod -Class Win32_Process -Name Create "
f"-ArgumentList \\'powershell.exe -noprofile -noninteractive -encodedcommand {encoded_command}\\'"
)

def is_windows_pv_device_installed(self):
"""Checks for the install state of **any** Xen/XenServer PV devices."""
output = self.execute_powershell_script(
r"""Get-PnpDevice -PresentOnly |
Where-Object CompatibleID -icontains 'PCI\VEN_5853' |
Select-Object -ExpandProperty Problem"""
)
# There may be multiple platform PCI devices (e.g. one default, one vendor).
# In some cases (e.g. installing our tools on VMs with vendor devices), the default and vendor
# devices may have different statuses (default = installed, vendor = not installed).
# For now, make sure all of them share the same status since our tools do not support vendor devices anyway.
statuses = output.splitlines()
logging.debug(f"Installed Xen device status: {statuses}")
if all(x == "CM_PROB_NONE" for x in statuses):
return True
elif all(x == "CM_PROB_FAILED_INSTALL" for x in statuses):
return False
else:
raise Exception(f"Unknown problem status {statuses}")

def are_windows_services_present(self):
"""Checks for the presence of **any** Xen/XenServer PV services."""
output = self.execute_powershell_script(
r"""$null -ne (Get-Service xenagent,xenbus,xenbus_monitor,xencons,xencons_monitor,xendisk,xenfilt,xenhid,
xeniface,XenInstall,xennet,XenSvc,xenvbd,xenvif,xenvkbd -ErrorAction SilentlyContinue)""")
return strtobool(output)

def are_windows_drivers_present(self):
"""Checks for the presence of **any** installed PV drivers, activated or not."""
output = self.execute_powershell_script(
r"""$null -ne (Get-ChildItem $env:windir\INF\oem*.inf |
ForEach-Object {Get-Content $_} |
Select-String "AddService=(xenbus|xencons|xendisk|xenfilt|xenhid|xeniface|xennet|xenvbd|xenvif|xenvkbd)")""")
return strtobool(output)

def are_windows_tools_working(self):
assert self.is_windows
return self.is_windows_pv_device_installed() and self.param_get("PV-drivers-detected")

def are_windows_tools_uninstalled(self):
assert self.is_windows
return (
not self.is_windows_pv_device_installed()
and not self.are_windows_services_present()
and not self.are_windows_drivers_present()
)
Loading
Loading