Skip to content

Commit 9e1f638

Browse files
authored
Merge pull request #262 from xcp-ng/tools-windows
Add Windows PV tools installer tests
2 parents 9c87472 + 7057dfc commit 9e1f638

21 files changed

+945
-33
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ __pycache__
22
*/__pycache__
33
data.py
44
vm_data.py
5+
/scripts/guests/windows/id_rsa.pub

conftest.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,3 +463,27 @@ def second_network(pytestconfig, host):
463463
if network_uuid == host.management_network():
464464
pytest.fail("--second-network must NOT be the management network")
465465
return network_uuid
466+
467+
@pytest.fixture(scope='module')
468+
def nfs_iso_device_config():
469+
return global_config.sr_device_config("NFS_ISO_DEVICE_CONFIG", required=['location'])
470+
471+
@pytest.fixture(scope='module')
472+
def cifs_iso_device_config():
473+
return global_config.sr_device_config("CIFS_ISO_DEVICE_CONFIG")
474+
475+
@pytest.fixture(scope='module')
476+
def nfs_iso_sr(host, nfs_iso_device_config):
477+
""" A NFS ISO SR. """
478+
sr = host.sr_create('iso', "ISO-NFS-SR-test", nfs_iso_device_config, shared=True, verify=True)
479+
yield sr
480+
# teardown
481+
sr.forget()
482+
483+
@pytest.fixture(scope='module')
484+
def cifs_iso_sr(host, cifs_iso_device_config):
485+
""" A Samba/CIFS SR. """
486+
sr = host.sr_create('iso', "ISO-CIFS-SR-test", cifs_iso_device_config, shared=True, verify=True)
487+
yield sr
488+
# teardown
489+
sr.forget()

data.py-dist

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,57 @@ PXE_CONFIG_SERVER = 'pxe'
2626
# Default VM images location
2727
DEF_VM_URL = 'http://pxe/images/'
2828

29+
# Guest tools ISO download location
30+
ISO_DOWNLOAD_URL = 'http://pxe/isos/'
31+
32+
# Definitions of Windows guest tool ISOs to be tested
33+
WIN_GUEST_TOOLS_ISOS = {
34+
"stable": {
35+
# ISO name on SR or subpath of ISO_DOWNLOAD_URL
36+
"name": "guest-tools-win.iso",
37+
# Whether ISO should be downloaded from ISO_DOWNLOAD_URL
38+
"download": True,
39+
# ISO-relative path of MSI file to be installed
40+
"package": "package\\XenDrivers-x64.msi",
41+
# ISO-relative path of XenClean script
42+
"xenclean_path": "package\\XenClean\\x64\\Invoke-XenClean.ps1",
43+
# ISO-relative path of root cert file to be installed before guest tools (optional)
44+
"testsign_cert": "testsign\\XCP-ng_Test_Signer.crt",
45+
},
46+
# Add more guest tool ISOs here as needed
47+
}
48+
49+
# Definition of ISO containing other guest tools to be tested
50+
OTHER_GUEST_TOOLS_ISO = {
51+
"name": "other-guest-tools-win.iso",
52+
"download": False,
53+
}
54+
55+
# Definitions of other guest tools contained in OTHER_GUEST_TOOLS_ISO
56+
OTHER_GUEST_TOOLS = {
57+
"xcp-ng-9.0.9000": {
58+
# Whether we are installing MSI files ("msi"), bare .inf drivers ("inf")
59+
# or nothing in case of Windows Update (absent or null)
60+
"type": "msi",
61+
# ISO-relative path of this guest tool
62+
"path": "xcp-ng-9.0.9000",
63+
# "path"-relative path of MSI or driver files to be installed
64+
"package": "package\\XenDrivers-x64.msi",
65+
# Relative path of root cert file (optional)
66+
"testsign_cert": "testsign\\XCP-ng_Test_Signer.crt",
67+
# Whether this guest tool version wants vendor device to be activated (optional, defaults to False)
68+
# Note: other guest tools may not install correctly with this setting enabled
69+
"vendor_device": False,
70+
71+
# Can we upgrade automatically from this guest tool to our tools?
72+
"upgradable": True,
73+
},
74+
"vendor": {
75+
"vendor_device": True,
76+
"upgradable": False,
77+
},
78+
}
79+
2980
# Values can be either full URLs or only partial URLs that will be automatically appended to DEF_VM_URL
3081
VM_IMAGES = {
3182
'mini-linux-x86_64-bios': 'alpine-minimal-3.12.0.xva',
@@ -40,6 +91,7 @@ VM_IMAGES = {
4091
# Possible values:
4192
# - 'default': keep using the pool's default SR
4293
# - 'local': use the first local SR found instead
94+
# - A UUID of the SR to be used
4395
DEFAULT_SR = 'default'
4496

4597
# Whether to cache VMs on the test host, that is import them only if not already

jobs.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,19 @@
350350
"paths": ["tests/guest-tools/unix"],
351351
"markers": "multi_vms",
352352
},
353+
"tools-windows": {
354+
"description": "tests our windows guest tools on a variety of VMs",
355+
"requirements": [
356+
"A pool >= 8.2. One host is enough.",
357+
"A variety of windows VMs supported by our tools installer.",
358+
],
359+
"nb_pools": 1,
360+
"params": {
361+
"--vm[]": "multi/tools_windows",
362+
},
363+
"paths": ["tests/guest-tools/win"],
364+
"markers": "multi_vms",
365+
},
353366
"xen": {
354367
"description": "Testing of the Xen hypervisor itself",
355368
"requirements": [

lib/commands.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import base64
12
import logging
23
import shlex
34
import subprocess
@@ -71,7 +72,10 @@ def _ssh(hostname_or_ip, cmd, check, simple_output, suppress_fingerprint_warning
7172
opts.append('-o "LogLevel ERROR"')
7273
opts.append('-o "UserKnownHostsFile /dev/null"')
7374

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

224228
return LocalCommandResult(res.returncode, output)
229+
230+
def encode_powershell_command(cmd: str):
231+
return base64.b64encode(cmd.encode("utf-16-le")).decode("ascii")

lib/host.py

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import os
44
import shlex
55
import tempfile
6+
import uuid
67

78
from packaging import version
89

@@ -14,6 +15,7 @@
1415
from lib.common import prefix_object_name
1516
from lib.netutil import wrap_ip
1617
from lib.sr import SR
18+
from lib.vdi import VDI
1719
from lib.vm import VM
1820
from lib.xo import xo_cli, xo_object_exists
1921

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

273+
def import_iso(self, uri, sr: SR):
274+
random_name = str(uuid.uuid4())
275+
276+
vdi_uuid = self.xe(
277+
"vdi-create",
278+
{
279+
"sr-uuid": sr.uuid,
280+
"name-label": random_name,
281+
"virtual-size": "0",
282+
},
283+
)
284+
285+
try:
286+
params = {'uuid': vdi_uuid}
287+
if '://' in uri:
288+
logging.info(f"Download ISO {uri}")
289+
download_path = f'/tmp/{vdi_uuid}'
290+
self.ssh(f"curl -o '{download_path}' '{uri}'")
291+
params['filename'] = download_path
292+
else:
293+
download_path = None
294+
params['filename'] = uri
295+
logging.info(f"Import ISO {uri}: name {random_name}, uuid {vdi_uuid}")
296+
297+
self.xe('vdi-import', params)
298+
finally:
299+
if download_path:
300+
self.ssh(f"rm -f '{download_path}'")
301+
302+
return VDI(vdi_uuid, sr=sr)
303+
271304
def pool_has_vm(self, vm_uuid, vm_type='vm'):
272305
if vm_type == 'snapshot':
273306
return self.xe('snapshot-list', {'uuid': vm_uuid}, minimal=True) == vm_uuid
@@ -494,12 +527,11 @@ def local_vm_srs(self):
494527
return srs
495528

496529
def main_sr_uuid(self):
497-
""" Main SR is either the default SR, or the first local SR, depending on data.py's DEFAULT_SR. """
530+
""" Main SR is the default SR, the first local SR, or a specific SR depending on data.py's DEFAULT_SR. """
498531
try:
499532
from data import DEFAULT_SR
500533
except ImportError:
501534
DEFAULT_SR = 'default'
502-
assert DEFAULT_SR in ['default', 'local']
503535

504536
sr_uuid = None
505537
if DEFAULT_SR == 'local':
@@ -512,9 +544,12 @@ def main_sr_uuid(self):
512544
)
513545
assert local_sr_uuids, f"DEFAULT_SR=='local' so there must be a local SR on host {self}"
514546
sr_uuid = local_sr_uuids[0]
515-
else:
547+
elif DEFAULT_SR == 'default':
516548
sr_uuid = self.pool.param_get('default-SR')
517549
assert sr_uuid, f"DEFAULT_SR='default' so there must be a default SR on the pool of host {self}"
550+
else:
551+
sr_uuid = DEFAULT_SR
552+
assert self.xe('sr-list', {'uuid': sr_uuid}), f"cannot find SR with UUID {sr_uuid} on host {self}"
518553
assert sr_uuid != "<not in database>"
519554
return sr_uuid
520555

lib/vdi.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ def __init__(self, uuid, *, host=None, sr=None):
1717
else:
1818
self.sr = sr
1919

20+
def name(self):
21+
return self.param_get('name-label')
22+
2023
def destroy(self):
2124
logging.info("Destroy %s", self)
2225
self.sr.pool.master.xe('vdi-destroy', {'uuid': self.uuid})

lib/vm.py

Lines changed: 91 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import lib.efi as efi
88

99
from lib.basevm import BaseVM
10-
from lib.common import PackageManagerEnum, parse_xe_dict, safe_split, wait_for, wait_for_not
10+
from lib.common import PackageManagerEnum, parse_xe_dict, safe_split, strtobool, wait_for, wait_for_not
1111
from lib.snapshot import Snapshot
1212
from lib.vif import VIF
1313

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

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

328329
def detect_package_manager(self):
329330
""" Heuristic to determine the package manager on a unix distro. """
@@ -587,3 +588,90 @@ def is_cert_present(vm, key):
587588
res = vm.host.ssh(['varstore-get', vm.uuid, efi.get_secure_boot_guid(key).as_str(), key],
588589
check=False, simple_output=False, decode=False)
589590
return res.returncode == 0
591+
592+
def execute_powershell_script(
593+
self,
594+
script_contents: str,
595+
simple_output=True,
596+
prepend="$ProgressPreference = 'SilentlyContinue';"):
597+
# ProgressPreference is needed to suppress any clixml progress output,
598+
# as it's not filtered away from stdout by default, and we're grabbing stdout.
599+
assert self.is_windows
600+
if prepend is not None:
601+
script_contents = prepend + script_contents
602+
cmd = commands.encode_powershell_command(script_contents)
603+
return self.ssh(
604+
f"powershell.exe -nologo -noprofile -noninteractive -encodedcommand {cmd}",
605+
simple_output=simple_output,
606+
)
607+
608+
def run_powershell_command(self, program: str, args: str):
609+
"""
610+
Run command under powershell to retrieve exit codes higher than 255.
611+
612+
Backslash-safe.
613+
"""
614+
assert self.is_windows
615+
output = self.execute_powershell_script(
616+
f"Write-Output (Start-Process -Wait -PassThru {program} -ArgumentList '{args}').ExitCode")
617+
return int(output)
618+
619+
def start_background_powershell(self, cmd: str):
620+
"""
621+
Run command under powershell in the background.
622+
623+
Backslash-safe.
624+
"""
625+
assert self.is_windows
626+
encoded_command = commands.encode_powershell_command(cmd)
627+
self.ssh(
628+
"powershell.exe -noprofile -noninteractive Invoke-WmiMethod -Class Win32_Process -Name Create "
629+
f"-ArgumentList \\'powershell.exe -noprofile -noninteractive -encodedcommand {encoded_command}\\'"
630+
)
631+
632+
def is_windows_pv_device_installed(self):
633+
"""Checks for the install state of **any** Xen/XenServer PV devices."""
634+
output = self.execute_powershell_script(
635+
r"""Get-PnpDevice -PresentOnly |
636+
Where-Object CompatibleID -icontains 'PCI\VEN_5853' |
637+
Select-Object -ExpandProperty Problem"""
638+
)
639+
# There may be multiple platform PCI devices (e.g. one default, one vendor).
640+
# In some cases (e.g. installing our tools on VMs with vendor devices), the default and vendor
641+
# devices may have different statuses (default = installed, vendor = not installed).
642+
# For now, make sure all of them share the same status since our tools do not support vendor devices anyway.
643+
statuses = output.splitlines()
644+
logging.debug(f"Installed Xen device status: {statuses}")
645+
if all(x == "CM_PROB_NONE" for x in statuses):
646+
return True
647+
elif all(x == "CM_PROB_FAILED_INSTALL" for x in statuses):
648+
return False
649+
else:
650+
raise Exception(f"Unknown problem status {statuses}")
651+
652+
def are_windows_services_present(self):
653+
"""Checks for the presence of **any** Xen/XenServer PV services."""
654+
output = self.execute_powershell_script(
655+
r"""$null -ne (Get-Service xenagent,xenbus,xenbus_monitor,xencons,xencons_monitor,xendisk,xenfilt,xenhid,
656+
xeniface,XenInstall,xennet,XenSvc,xenvbd,xenvif,xenvkbd -ErrorAction SilentlyContinue)""")
657+
return strtobool(output)
658+
659+
def are_windows_drivers_present(self):
660+
"""Checks for the presence of **any** installed PV drivers, activated or not."""
661+
output = self.execute_powershell_script(
662+
r"""$null -ne (Get-ChildItem $env:windir\INF\oem*.inf |
663+
ForEach-Object {Get-Content $_} |
664+
Select-String "AddService=(xenbus|xencons|xendisk|xenfilt|xenhid|xeniface|xennet|xenvbd|xenvif|xenvkbd)")""")
665+
return strtobool(output)
666+
667+
def are_windows_tools_working(self):
668+
assert self.is_windows
669+
return self.is_windows_pv_device_installed() and self.param_get("PV-drivers-detected")
670+
671+
def are_windows_tools_uninstalled(self):
672+
assert self.is_windows
673+
return (
674+
not self.is_windows_pv_device_installed()
675+
and not self.are_windows_services_present()
676+
and not self.are_windows_drivers_present()
677+
)

0 commit comments

Comments
 (0)