Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -469,7 +469,7 @@ def imported_vm(host, vm_ref):

yield vm
# teardown
if not is_uuid(vm_ref):
if CACHE_IMPORTED_VM or not is_uuid(vm_ref):
logging.info("<< Destroy VM")
vm.destroy(verify=True)

Expand Down
3 changes: 3 additions & 0 deletions data.py-dist
Original file line number Diff line number Diff line change
Expand Up @@ -253,3 +253,6 @@ IMAGE_EQUIVS: dict[str, str] = {
# 'install.test::Nested::install[bios-830-ext]-vm1-607cea0c825a4d578fa5fab56978627d8b2e28bb':
# 'install.test::Nested::install[bios-830-ext]-vm1-addb4ead4da49856e1d2fb3ddf4e31027c6b693b',
}

# This should be a working DNS server that's not used by any VM images.
TEST_DNS_SERVER = "1.1.1.1"
6 changes: 3 additions & 3 deletions lib/host.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import lib.commands as commands
import lib.pif as pif

from typing import TYPE_CHECKING, Dict, List, Literal, Optional, TypedDict, Union, overload
from typing import TYPE_CHECKING, Dict, List, Literal, Mapping, Optional, TypedDict, Union, overload

if TYPE_CHECKING:
from lib.pool import Pool
Expand Down Expand Up @@ -136,12 +136,12 @@ def scp(self, src, dest, check=True, suppress_fingerprint_warnings=True, local_d
)

@overload
def xe(self, action: str, args: Dict[str, Union[str, bool]] = {}, *, check: bool = ...,
def xe(self, action: str, args: Mapping[str, Union[str, bool]] = {}, *, check: bool = ...,
simple_output: Literal[True] = ..., minimal: bool = ..., force: bool = ...) -> str:
...

@overload
def xe(self, action: str, args: Dict[str, Union[str, bool]] = {}, *, check: bool = ...,
def xe(self, action: str, args: Mapping[str, Union[str, bool]] = {}, *, check: bool = ...,
simple_output: Literal[False], minimal: bool = ..., force: bool = ...) -> commands.SSHResult:
...

Expand Down
6 changes: 6 additions & 0 deletions lib/vif.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ def __init__(self, uuid, vm):
self.uuid = uuid
self.vm = vm

def plug(self):
self.vm.host.xe("vif-plug", {'uuid': self.uuid})

def unplug(self):
self.vm.host.xe("vif-unplug", {'uuid': self.uuid})

def param_get(self, param_name, key=None, accept_unknown_key=False):
return _param_get(self.vm.host, VIF.xe_prefix, self.uuid, param_name, key, accept_unknown_key)

Expand Down
37 changes: 37 additions & 0 deletions lib/vm.py
Original file line number Diff line number Diff line change
Expand Up @@ -855,6 +855,43 @@ def save_to_cache(self, cache_id):
logging.info(f"Marking VM {clone.uuid} as cached")
clone.param_set('name-description', self.host.vm_cache_key(cache_id))

def set_memory_limits(
self,
*,
static_min: int | str | None = None,
static_max: int | str | None = None,
dynamic_min: int | str | None = None,
dynamic_max: int | str | None = None,
):
# Take both int and str for the memory limits since the latter is what param_get() returns.
if static_min is None:
static_min = self.param_get("memory-static-min")
if static_max is None:
static_max = self.param_get("memory-static-max")
if dynamic_min is None:
dynamic_min = self.param_get("memory-dynamic-min")
if dynamic_max is None:
dynamic_max = self.param_get("memory-dynamic-max")
params = {
"uuid": self.uuid,
"static-min": str(static_min),
"static-max": str(static_max),
"dynamic-min": str(dynamic_min),
"dynamic-max": str(dynamic_max),
}
logging.info(
f"Updating memory limits for vm {self.uuid}: "
f"static min={static_min} "
f"max={static_max} "
f"dynamic min={dynamic_min} "
f"max={dynamic_max}"
)
return self.host.xe('vm-memory-limits-set', params)

def set_memory_target(self, target: int | str):
logging.info(f"Setting memory target for vm {self.uuid} to {target}")
return self.host.xe('vm-memory-target-set', {"uuid": self.uuid, "target": str(target)})


def vm_cache_key_from_def(vm_def, ref_nodeid, test_gitref):
vm_name = vm_def["name"]
Expand Down
178 changes: 178 additions & 0 deletions lib/windows/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import enum
import logging
import re
import time

from data import ISO_DOWNLOAD_URL, TEST_DNS_SERVER
from lib.commands import SSHCommandFailed
from lib.common import strtobool, wait_for
from lib.host import Host
from lib.sr import SR
from lib.vif import VIF
from lib.vm import VM

from typing import Any, Dict, List, Union

# HACK: I originally thought that using Stop-Computer -Force would cause the SSH session to sometimes fail.
# I could never confirm this in the end, but use a slightly delayed shutdown just to be safe anyway.
WINDOWS_SHUTDOWN_COMMAND = "shutdown.exe -s -f -t 5"


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


def iso_create(host: Host, sr: SR, param: Dict[str, Any]):
if param["download"]:
vdi = host.import_iso(ISO_DOWNLOAD_URL + param["name"], sr)
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
res_host = vm.get_residence_host()
report = res_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")


def enable_testsign(vm: VM, rootcert: Union[str, None]):
if rootcert is not None:
vm.execute_powershell_script(
f"""certutil -addstore -f Root '{rootcert}';
if ($LASTEXITCODE -ne 0) {{throw}}
certutil -addstore -f TrustedPublisher '{rootcert}';
if ($LASTEXITCODE -ne 0) {{throw}}"""
)
vm.execute_powershell_script(
"bcdedit /set testsigning on; if ($LASTEXITCODE -ne 0) {throw};" + WINDOWS_SHUTDOWN_COMMAND
)
wait_for(vm.is_halted, "Wait for VM halted")
vm.start()
wait_for_vm_running_and_ssh_up_without_tools(vm)


def insert_cd_safe(vm: VM, vdi_name: str, cd_path="D:/", retries=2):
"""
Insert a CD with retry.

HACK: Windows VM may not detect the CD drive in some cases.
If this happens, reboot the VM in hopes that it will be detected.
"""
for _attempt in range(retries):
# Eject the existing CD just in case.
try:
vm.eject_cd()
# Leave some time for the guest to realize its CD got ejected.
time.sleep(5)
except SSHCommandFailed:
pass

vm.insert_cd(vdi_name)
if not vm.is_running():
vm.start()
# wait_for(vm.file_exists) doesn't handle SSHCommandFailed;
# we need to check for it via wait_for(vm.is_ssh_up).
wait_for_vm_running_and_ssh_up_without_tools(vm)

try:
wait_for(lambda: vm.file_exists(cd_path, regular_file=False), timeout_secs=60)
return
except TimeoutError:
logging.warning(f"Waiting for CD at {cd_path} failed, retrying by rebooting VM")
# There might be no VM tools so use SSH instead.
vm.ssh(WINDOWS_SHUTDOWN_COMMAND)
wait_for(vm.is_halted, "Wait for VM halted")

raise TimeoutError(f"Waiting for CD at {cd_path} failed")


def vif_get_mac_without_separator(vif: VIF):
mac = vif.param_get("MAC")
assert mac is not None
return mac.replace(":", "")


def vif_has_rss(vif: VIF):
# Even if the Xenvif hash setting request fails, Windows can still report the NIC as having RSS enabled as long as
# the relevant OIDs are supported (Get-NetAdapterRss reports Enabled as True and Profile as Default).
# We need to explicitly check MaxProcessors to see if the hash setting request has really succeeded.
mac = vif_get_mac_without_separator(vif)
return strtobool(
vif.vm.execute_powershell_script(
rf"""(Get-NetAdapter |
Where-Object {{$_.PnPDeviceID -notlike 'root\kdnic\*' -and $_.PermanentAddress -eq '{mac}'}} |
Get-NetAdapterRss).MaxProcessors -gt 0"""
)
)


def vif_get_dns(vif: VIF):
mac = vif_get_mac_without_separator(vif)
return vif.vm.execute_powershell_script(
rf"""Import-Module DnsClient; Get-NetAdapter |
Where-Object {{$_.PnPDeviceID -notlike 'root\kdnic\*' -and $_.PermanentAddress -eq '{mac}'}} |
Get-DnsClientServerAddress -AddressFamily IPv4 |
Select-Object -ExpandProperty ServerAddresses"""
).splitlines()


def vif_set_dns(vif: VIF, nameservers: List[str]):
mac = vif_get_mac_without_separator(vif)
vif.vm.execute_powershell_script(
rf"""Import-Module DnsClient; Get-NetAdapter |
Where-Object {{$_.PnPDeviceID -notlike 'root\kdnic\*' -and $_.PermanentAddress -eq '{mac}'}} |
Get-DnsClientServerAddress -AddressFamily IPv4 |
Set-DnsClientServerAddress -ServerAddresses {",".join(nameservers)}"""
)


def wait_for_vm_xenvif_offboard(vm: VM):
# Xenvif offboard will reset the NIC, so need to wait for it to disappear first
wait_for(
lambda: strtobool(
vm.execute_powershell_script(
r'$null -eq (Get-ScheduledTask "Copy-XenVifSettings" -ErrorAction SilentlyContinue)', simple_output=True
)
),
timeout_secs=300,
retry_delay_secs=30,
)


def set_vm_dns(vm: VM):
logging.info(f"Set VM DNS to {TEST_DNS_SERVER}")
vif = vm.vifs()[0]
assert TEST_DNS_SERVER not in vif_get_dns(vif)
vif_set_dns(vif, [TEST_DNS_SERVER])


def check_vm_dns(vm: VM):
# The restore task takes time to fire so wait for it
vif = vm.vifs()[0]
wait_for(
lambda: TEST_DNS_SERVER in vif_get_dns(vif),
f"Check VM DNS contains {TEST_DNS_SERVER}",
timeout_secs=300,
retry_delay_secs=30,
)
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
enable_testsign,
insert_cd_safe,
wait_for_vm_running_and_ssh_up_without_tools,
wait_for_vm_xenvif_offboard,
)

from typing import Any, Dict
Expand Down Expand Up @@ -57,7 +58,7 @@ def install_guest_tools(vm: VM, guest_tools_iso: Dict[str, Any], action: PowerAc
install_cmd += WINDOWS_SHUTDOWN_COMMAND
vm.start_background_powershell(install_cmd)
if action != PowerAction.Nothing:
wait_for(vm.is_halted, "Wait for VM halted")
wait_for(vm.is_halted, "Wait for VM halted", timeout_secs=600)
if action == PowerAction.Reboot:
vm.start()
wait_for_vm_running_and_ssh_up_without_tools(vm)
Expand All @@ -75,7 +76,8 @@ def uninstall_guest_tools(vm: VM, action: PowerAction):
uninstall_cmd += WINDOWS_SHUTDOWN_COMMAND
vm.start_background_powershell(uninstall_cmd)
if action != PowerAction.Nothing:
wait_for(vm.is_halted, "Wait for VM halted")
wait_for(vm.is_halted, "Wait for VM halted", timeout_secs=600)
if action == PowerAction.Reboot:
vm.start()
wait_for_vm_running_and_ssh_up_without_tools(vm)
wait_for_vm_xenvif_offboard(vm)
File renamed without changes.
Loading