From 53d49a3138b144bb7c90dd06850c73f5262262b7 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Tue, 7 May 2024 15:59:23 +0200 Subject: [PATCH 01/75] host.main_sr: don't return "" as UUID on missing default-SR Signed-off-by: Yann Dirson --- lib/host.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/host.py b/lib/host.py index 712c2053..1c7b694d 100644 --- a/lib/host.py +++ b/lib/host.py @@ -480,6 +480,7 @@ def main_sr(self): else: 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}" + assert sr_uuid != "" return sr_uuid def hostname(self): From 1a67de4dc89fa1a1c4be1189621eb7f151484e95 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Mon, 13 May 2024 11:45:19 +0200 Subject: [PATCH 02/75] Host: rename main_sr to main_sr_uuid Other methods to get SRs return `SR` objects, this was inconsistent. Signed-off-by: Yann Dirson --- conftest.py | 2 +- lib/host.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/conftest.py b/conftest.py index 1e6c0bfc..721f4d45 100644 --- a/conftest.py +++ b/conftest.py @@ -314,7 +314,7 @@ def imported_vm(host, vm_ref): name = vm_orig.name() logging.info(">> Reuse VM %s (%s) on host %s" % (vm_ref, name, host)) else: - vm_orig = host.import_vm(vm_ref, host.main_sr(), use_cache=CACHE_IMPORTED_VM) + vm_orig = host.import_vm(vm_ref, host.main_sr_uuid(), use_cache=CACHE_IMPORTED_VM) if CACHE_IMPORTED_VM: # Clone the VM before running tests, so that the original VM remains untouched diff --git a/lib/host.py b/lib/host.py index 1c7b694d..15dce97e 100644 --- a/lib/host.py +++ b/lib/host.py @@ -460,7 +460,7 @@ def local_vm_srs(self): srs.append(sr) return srs - def main_sr(self): + def main_sr_uuid(self): """ Main SR is either the default SR, or the first local SR, depending on data.py's DEFAULT_SR. """ try: from data import DEFAULT_SR From e356b4fdaf3793c7916f389df4c0d56e5d083f6f Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Tue, 14 May 2024 10:51:31 +0200 Subject: [PATCH 03/75] local_cmd: log command to be run, like ssh does Signed-off-by: Yann Dirson --- lib/commands.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/commands.py b/lib/commands.py index 841f52e3..71d69ca9 100644 --- a/lib/commands.py +++ b/lib/commands.py @@ -205,6 +205,7 @@ def sftp(hostname_or_ip, cmds, check=True, suppress_fingerprint_warnings=True): def local_cmd(cmd, check=True, decode=True): """ Run a command locally on tester end. """ + logging.debug("[local] %s", (cmd,)) res = subprocess.run( cmd, stdout=subprocess.PIPE, From 60db45b1f6f186477cdbd055ddfae3665d8ab151 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Mon, 13 May 2024 14:58:47 +0200 Subject: [PATCH 04/75] VM: fix name of method dealing with CD insertion, provide a generic API "vm-cd-insert" does not mount anything, as the need for a subsequent "mount" in the only test using it shows. Signed-off-by: Yann Dirson --- lib/vm.py | 9 ++++++--- tests/guest-tools/unix/test_guest_tools_unix.py | 4 ++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/vm.py b/lib/vm.py index f21c0d5c..62d1f6c1 100644 --- a/lib/vm.py +++ b/lib/vm.py @@ -334,10 +334,13 @@ def detect_package_manager(self): else: return PackageManagerEnum.UNKNOWN - def mount_guest_tools_iso(self): - self.host.xe('vm-cd-insert', {'uuid': self.uuid, 'cd-name': 'guest-tools.iso'}) + def insert_cd(self, vdi_name): + self.host.xe('vm-cd-insert', {'uuid': self.uuid, 'cd-name': vdi_name}) - def unmount_guest_tools_iso(self): + def insert_guest_tools_iso(self): + self.insert_cd('guest-tools.iso') + + def eject_cd(self): self.host.xe('vm-cd-eject', {'uuid': self.uuid}) # *** Common reusable test fragments diff --git a/tests/guest-tools/unix/test_guest_tools_unix.py b/tests/guest-tools/unix/test_guest_tools_unix.py index 51664777..29e4ce60 100644 --- a/tests/guest-tools/unix/test_guest_tools_unix.py +++ b/tests/guest-tools/unix/test_guest_tools_unix.py @@ -62,7 +62,7 @@ def test_install(self, running_vm, state): # mount ISO logging.info("Mount guest tools ISO") - vm.mount_guest_tools_iso() + vm.insert_guest_tools_iso() tmp_mnt = vm.ssh(['mktemp', '-d']) time.sleep(2) # wait a small amount of time just to ensure the device is available vm.ssh(['mount', '-t', 'iso9660', '/dev/cdrom', tmp_mnt]) @@ -80,7 +80,7 @@ def test_install(self, running_vm, state): # unmount ISO logging.info("Unmount guest tools ISO") vm.ssh(['umount', tmp_mnt]) - vm.unmount_guest_tools_iso() + vm.eject_cd() # check that xe-daemon is running wait_for(lambda: vm.ssh_with_result(['pgrep', '-f', 'xe-daemon']).returncode == 0, From b9012fc70a544fbbb55d842481098a8f31af057f Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Mon, 13 May 2024 15:16:21 +0200 Subject: [PATCH 05/75] VM: add logging for CD insert/eject Signed-off-by: Yann Dirson --- lib/vm.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/vm.py b/lib/vm.py index 62d1f6c1..80349fd2 100644 --- a/lib/vm.py +++ b/lib/vm.py @@ -335,12 +335,14 @@ def detect_package_manager(self): return PackageManagerEnum.UNKNOWN def insert_cd(self, vdi_name): + logging.info("Insert CD %r in VM %s", vdi_name, self.uuid) self.host.xe('vm-cd-insert', {'uuid': self.uuid, 'cd-name': vdi_name}) def insert_guest_tools_iso(self): self.insert_cd('guest-tools.iso') def eject_cd(self): + logging.info("Ejecting CD from VM %s", self.uuid) self.host.xe('vm-cd-eject', {'uuid': self.uuid}) # *** Common reusable test fragments From 38b3ae25065d9108794527d3290ce92417cefd00 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Tue, 14 May 2024 17:51:38 +0200 Subject: [PATCH 06/75] _ssh: remove duplicate default values The entrypoint is ssh(), and the default values are already there. Signed-off-by: Yann Dirson --- lib/commands.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/commands.py b/lib/commands.py index 71d69ca9..c5a54bc5 100644 --- a/lib/commands.py +++ b/lib/commands.py @@ -65,8 +65,8 @@ def _ellide_log_lines(log): OUPUT_LOGGER.addHandler(OUTPUT_HANDLER) OUTPUT_HANDLER.setFormatter(logging.Formatter('%(message)s')) -def _ssh(hostname_or_ip, cmd, check=True, simple_output=True, suppress_fingerprint_warnings=True, - background=False, target_os='linux', decode=True, options=[]): +def _ssh(hostname_or_ip, cmd, check, simple_output, suppress_fingerprint_warnings, + background, target_os, decode, options): opts = list(options) opts.append('-o "BatchMode yes"') if suppress_fingerprint_warnings: From b58c666c02929d7c5a214d8969bc0695c3137423 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Wed, 15 May 2024 11:06:07 +0200 Subject: [PATCH 07/75] Remove redundant logs on VM creation Since f9b53659ca2936ae8b5014b5a9b9dbada6e215a3 the VM ctor takes care of reporting this info. Signed-off-by: Yann Dirson --- lib/host.py | 1 - lib/vm.py | 1 - 2 files changed, 2 deletions(-) diff --git a/lib/host.py b/lib/host.py index 15dce97e..73848a98 100644 --- a/lib/host.py +++ b/lib/host.py @@ -227,7 +227,6 @@ def import_vm(self, uri, sr_uuid=None, use_cache=False): params['sr-uuid'] = sr_uuid logging.info(msg) vm_uuid = self.xe('vm-import', params) - logging.info("VM UUID: %s" % vm_uuid) vm_name = prefix_object_name(self.xe('vm-param-get', {'uuid': vm_uuid, 'param-name': 'name-label'})) vm = VM(vm_uuid, self) vm.param_set('name-label', vm_name) diff --git a/lib/vm.py b/lib/vm.py index 80349fd2..c2f0b165 100644 --- a/lib/vm.py +++ b/lib/vm.py @@ -481,7 +481,6 @@ def clone(self): name = self.name() + '_clone_for_tests' logging.info("Clone VM") uuid = self.host.xe('vm-clone', {'uuid': self.uuid, 'new-name-label': name}) - logging.info("New VM: %s (%s)" % (uuid, name)) return VM(uuid, self.host) def install_uefi_certs(self, auths): From 6d85067f3512b80bb468fe36ad057996323abebf Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Wed, 29 May 2024 14:04:35 +0200 Subject: [PATCH 08/75] host.import_vm: report when VM not found in cache Signed-off-by: Yann Dirson --- lib/host.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/host.py b/lib/host.py index 73848a98..ea7944b1 100644 --- a/lib/host.py +++ b/lib/host.py @@ -215,6 +215,7 @@ def import_vm(self, uri, sr_uuid=None, use_cache=False): if not vm.vdi_uuids() or vm.get_sr().uuid == sr_uuid: logging.info(f"Reusing cached VM {vm.uuid} for {uri}") return vm + logging.info("Could not find a vm in cache with key %r", cache_key) params = {} msg = "Import VM %s" % uri From fbf6711e8e67d81c2addab4eb3239aab78b0a4eb Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Thu, 30 May 2024 11:16:05 +0200 Subject: [PATCH 09/75] host: split cached_vm identification from import_vm This will be useful to the plugin that allows not rerunning a cached dependency: it needs to probe the cache. Signed-off-by: Yann Dirson --- lib/host.py | 38 ++++++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/lib/host.py b/lib/host.py index ea7944b1..034b6439 100644 --- a/lib/host.py +++ b/lib/host.py @@ -200,22 +200,31 @@ def xo_server_reconnect(self): # is not enough to guarantee that the host object exists yet. wait_for(lambda: xo_object_exists(self.uuid), "Wait for XO to know about HOST %s" % self.uuid) + @staticmethod + def vm_cache_key(uri): + return f"[Cache for {uri}]" + + def cached_vm(self, uri, sr_uuid): + assert sr_uuid, "A SR UUID is necessary to use import cache" + cache_key = self.vm_cache_key(uri) + # Look for an existing cache VM + vm_uuids = safe_split(self.xe('vm-list', {'name-description': cache_key}, minimal=True), ',') + + for vm_uuid in vm_uuids: + vm = VM(vm_uuid, self) + # Make sure the VM is on the wanted SR. + # Assumption: if the first disk is on the SR, the VM is. + # If there's no VDI at all, then it is virtually on any SR. + if not vm.vdi_uuids() or vm.get_sr().uuid == sr_uuid: + logging.info(f"Reusing cached VM {vm.uuid} for {uri}") + return vm + logging.info("Could not find a VM in cache with key %r", cache_key) + def import_vm(self, uri, sr_uuid=None, use_cache=False): if use_cache: - assert sr_uuid, "A SR UUID is necessary to use import cache" - cache_key = f"[Cache for {uri}]" - # Look for an existing cache VM - vm_uuids = safe_split(self.xe('vm-list', {'name-description': cache_key}, minimal=True), ',') - - for vm_uuid in vm_uuids: - vm = VM(vm_uuid, self) - # Make sure the VM is on the wanted SR. - # Assumption: if the first disk is on the SR, the VM is. - # If there's no VDI at all, then it is virtually on any SR. - if not vm.vdi_uuids() or vm.get_sr().uuid == sr_uuid: - logging.info(f"Reusing cached VM {vm.uuid} for {uri}") - return vm - logging.info("Could not find a vm in cache with key %r", cache_key) + vm = self.cached_vm(uri, sr_uuid) + if vm: + return vm params = {} msg = "Import VM %s" % uri @@ -235,6 +244,7 @@ def import_vm(self, uri, sr_uuid=None, use_cache=False): for vif in vm.vifs(): vif.move(self.management_network()) if use_cache: + cache_key = self.vm_cache_key(uri) logging.info(f"Marking VM {vm.uuid} as cached") vm.param_set('name-description', cache_key) return vm From 523c60adced9a2b7b0a43f6fe87738d34cc04280 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Wed, 19 Jun 2024 13:49:13 +0200 Subject: [PATCH 10/75] requirements: cleanup cruft, avoid duplication in README Signed-off-by: Yann Dirson --- README.md | 7 +++---- requirements/base.txt | 11 ----------- requirements/dev.txt | 7 ------- 3 files changed, 3 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 497771d8..ba61dad1 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,9 @@ Note: this is a perpertual work in progress. If you encounter any obstacles or b ## Main requirements * python >= 3.5 -* pytest >= 5.4 (python3 version) -* xo-cli >= 0.17.0 installed, in the PATH, and registered to an instance of XO that will be used during the tests +* packages as listed in requirements/base.txt +* extra test-specific requirements are documented in the test file + "Requirements" header ### Quick install (python requirements) @@ -13,14 +14,12 @@ Install the python requirements using pip: ``` $ pip install -r requirements/base.txt - ``` Additionally, for dev dependencies (things like the linter / style checker): ``` $ pip install -r requirements/dev.txt - ``` ## Other requirements diff --git a/requirements/base.txt b/requirements/base.txt index 34fb262e..facd2a48 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,14 +1,3 @@ -attrs>=20.3.0 -cffi>=1.14.4 cryptography>=3.3.1 -importlib-metadata>=2.1.1 -more-itertools>=8.6.0 packaging>=20.7 -pathlib2>=2.3.5 -pluggy>=0.13.1 -py>=1.9.0 -pyparsing>=2.4.7 pytest>=5.4.0 -six>=1.15.0 -wcwidth>=0.2.5 -zipp>=1.2.0 diff --git a/requirements/dev.txt b/requirements/dev.txt index 0853008d..3b8d7bb1 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,13 +1,6 @@ # All requirements + those only used in development ansible>=5.0.1 -ansible-core>=2.12.1 -beautifulsoup4>=4.10.0 bs4>=0.0.1 -Jinja2>=3.0.3 -MarkupSafe>=2.0.1 pycodestyle>=2.6.0 -pycparser>=2.21 PyYAML>=6.0 -resolvelib>=0.5.4 -soupsieve>=2.3.1 -r base.txt From 26a5a8f3f18cc37643768a66a6e24b4f9bc5a767 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Wed, 19 Jun 2024 16:05:19 +0200 Subject: [PATCH 11/75] try_get_and_store_ip: raise boot timeout for nested tests When testing in a nested setup, 2 min is not enough to boot a Debian 12 to the point the XS agent has published to xenstore. 5 min should be enough for everyone. Signed-off-by: Yann Dirson --- conftest.py | 2 +- lib/vm.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/conftest.py b/conftest.py index 721f4d45..b7183b3b 100644 --- a/conftest.py +++ b/conftest.py @@ -339,7 +339,7 @@ def running_vm(imported_vm): if not vm.is_running(): vm.start() wait_for(vm.is_running, '> Wait for VM running') - wait_for(vm.try_get_and_store_ip, "> Wait for VM IP") + wait_for(vm.try_get_and_store_ip, "> Wait for VM IP", timeout_secs=5 * 60) wait_for(vm.is_ssh_up, "> Wait for VM SSH up") return vm # no teardown diff --git a/lib/vm.py b/lib/vm.py index c2f0b165..51752b65 100644 --- a/lib/vm.py +++ b/lib/vm.py @@ -122,7 +122,7 @@ def wait_for_os_booted(self): # waiting for the IP: # - allows to make sure the OS actually started (on VMs that have the management agent) # - allows to store the IP for future use in the VM object - wait_for(self.try_get_and_store_ip, "Wait for VM IP") + wait_for(self.try_get_and_store_ip, "Wait for VM IP", timeout_secs=5 * 60) # now wait also for the management agent to have started wait_for(self.is_management_agent_up, "Wait for management agent up") From 80da462e1ec5956af383d45b991a12c662f45eea Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Wed, 19 Jun 2024 16:24:47 +0200 Subject: [PATCH 12/75] test_guest_tools_unix: make requirements more explicit Signed-off-by: Yann Dirson --- tests/guest-tools/unix/test_guest_tools_unix.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/guest-tools/unix/test_guest_tools_unix.py b/tests/guest-tools/unix/test_guest_tools_unix.py index 29e4ce60..a0235e0e 100644 --- a/tests/guest-tools/unix/test_guest_tools_unix.py +++ b/tests/guest-tools/unix/test_guest_tools_unix.py @@ -9,6 +9,7 @@ # - hostA2: Second member of the pool. Can have any local SR. No need to specify it on CLI. # From --vm parameter # - A VM to import, supported by the Linux/install.sh script of the guest tools ISO +# (without this flag you get an alpine, and that is not suitable) class State: def __init__(self): From a8832247323b00725e64f3f40c08c2519e798d49 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Wed, 19 Jun 2024 14:20:03 +0200 Subject: [PATCH 13/75] test_basic_without_ssh: split start/stop test from those needing a running VM. There is no other inter-test dependency in this file, this allows to stop using @pytest.mark.incremental here. Signed-off-by: Yann Dirson --- conftest.py | 10 +++++--- tests/misc/test_basic_without_ssh.py | 34 +++++++++++++--------------- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/conftest.py b/conftest.py index b7183b3b..a243a70f 100644 --- a/conftest.py +++ b/conftest.py @@ -332,18 +332,22 @@ def imported_vm(host, vm_ref): vm.destroy(verify=True) @pytest.fixture(scope="module") -def running_vm(imported_vm): +def started_vm(imported_vm): vm = imported_vm - # may be already running if we skipped the import to use an existing VM if not vm.is_running(): vm.start() wait_for(vm.is_running, '> Wait for VM running') wait_for(vm.try_get_and_store_ip, "> Wait for VM IP", timeout_secs=5 * 60) - wait_for(vm.is_ssh_up, "> Wait for VM SSH up") return vm # no teardown +@pytest.fixture(scope="module") +def running_vm(started_vm): + vm = started_vm + wait_for(vm.is_ssh_up, "> Wait for VM SSH up") + return vm + @pytest.fixture(scope='module') def unix_vm(imported_vm): vm = imported_vm diff --git a/tests/misc/test_basic_without_ssh.py b/tests/misc/test_basic_without_ssh.py index 52587df7..1463cc2c 100644 --- a/tests/misc/test_basic_without_ssh.py +++ b/tests/misc/test_basic_without_ssh.py @@ -16,8 +16,6 @@ # - when using an existing VM, the VM can be on any host of the pool, # the local SR or shared SR: the test will adapt itself. # Note however that an existing VM will be left on a different SR after the tests. -# -# This test suite is meant to be run entirely. There's no guarantee that cherry-picking tests will work. @pytest.fixture(scope='session') def existing_shared_sr(host): @@ -25,21 +23,25 @@ def existing_shared_sr(host): assert sr is not None, "A shared SR on the pool is required" return sr -@pytest.mark.incremental # tests depend on each other. If one test fails, don't execute the others @pytest.mark.multi_vms # run them on a variety of VMs @pytest.mark.big_vm # and also on a really big VM ideally -class TestBasicNoSSH: - def test_start(self, imported_vm): - vm = imported_vm - # if VM already running, stop it - if (vm.is_running()): - logging.info("VM already running, shutting it down first") - vm.shutdown(verify=True) - vm.start() - # this also tests the guest tools at the same time since they are used - # for retrieving the IP address and management agent status. - vm.wait_for_os_booted() +def test_vm_start_stop(imported_vm): + vm = imported_vm + # if VM already running, stop it + if (vm.is_running()): + logging.info("VM already running, shutting it down first") + vm.shutdown(verify=True) + vm.start() + # this also tests the guest tools at the same time since they are used + # for retrieving the IP address and management agent status. + vm.wait_for_os_booted() + + vm.shutdown(verify=True) +@pytest.mark.multi_vms # run them on a variety of VMs +@pytest.mark.big_vm # and also on a really big VM ideally +@pytest.mark.usefixtures("started_vm") +class TestBasicNoSSH: def test_pause(self, imported_vm): vm = imported_vm vm.pause(verify=True) @@ -104,7 +106,3 @@ def live_migrate(vm, dest_host, dest_sr, check_vdis=False): else: logging.info("* Preparing for live migration without storage motion *") live_migrate(vm, host1, existing_shared_sr) - - def test_shutdown(self, imported_vm): - vm = imported_vm - vm.shutdown(verify=True) From ce3bff2ea11149cb785c6ab53f9fcc03f86492c5 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Fri, 9 Aug 2024 10:51:25 +0200 Subject: [PATCH 14/75] test_basic_without_ssh: clarify requirements a bit Signed-off-by: Yann Dirson --- tests/misc/test_basic_without_ssh.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/misc/test_basic_without_ssh.py b/tests/misc/test_basic_without_ssh.py index 1463cc2c..de291561 100644 --- a/tests/misc/test_basic_without_ssh.py +++ b/tests/misc/test_basic_without_ssh.py @@ -9,8 +9,9 @@ # because the VM may not have SSH installed, which is needed for more advanced scenarios. # # Requirements: -# - a two-host XCP-ng pool >= 8.1. -# - the pool must have 1 shared SR +# - XCP-ng >= 8.1. +# - a two-host pool with 1 shared SR (for test_live_migrate) +# - the pool must have `suspend-image-SR` set (for suspend and checkpoint) # - each host must have a local SR # - any VM with guest tools installed. No SSH required for this test suite. # - when using an existing VM, the VM can be on any host of the pool, From 5c7e175e382dfe3ecec88abf7110abbd57867710 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Wed, 19 Jun 2024 16:25:56 +0200 Subject: [PATCH 15/75] test_guest_tools_unix: use a fixture instead of expecting test_install Same as for test_basic_without_ssh, and this allows to stop using @pytest.mark.incremental here too. Signed-off-by: Yann Dirson --- tests/guest-tools/unix/test_guest_tools_unix.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/guest-tools/unix/test_guest_tools_unix.py b/tests/guest-tools/unix/test_guest_tools_unix.py index a0235e0e..4bb82bc0 100644 --- a/tests/guest-tools/unix/test_guest_tools_unix.py +++ b/tests/guest-tools/unix/test_guest_tools_unix.py @@ -16,7 +16,6 @@ def __init__(self): self.tools_version = None self.vm_distro = None -@pytest.mark.incremental # tests depend on each other. If one test fails, don't execute the others @pytest.mark.multi_vms @pytest.mark.usefixtures("unix_vm") class TestGuestToolsUnix: @@ -34,7 +33,8 @@ def _check_os_info(self, vm, vm_distro): detected_distro = vm.distro() assert detected_distro == vm_distro - def test_install(self, running_vm, state): + @pytest.fixture(scope="class", autouse=True) + def vm_install(self, running_vm, state): vm = running_vm # skip test for some unixes From 4376ce1a1dfcc7a0ca83076efff7c389074969de Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Wed, 19 Jun 2024 16:28:44 +0200 Subject: [PATCH 16/75] Drop "incremental" marker. Previous commits removed its two only uses, replaced by an autouse fixture. Other possible uses would be covered (with a more fine-grained approach) by pytest-dependency. This frees the global pytest_runtest_makereport hook for potential reuse. Signed-off-by: Yann Dirson --- conftest.py | 15 --------------- pytest.ini | 1 - 2 files changed, 16 deletions(-) diff --git a/conftest.py b/conftest.py index a243a70f..7ca337b7 100644 --- a/conftest.py +++ b/conftest.py @@ -19,21 +19,6 @@ # need to import them in the global conftest.py so that they are recognized as fixtures. from pkgfixtures import formatted_and_mounted_ext4_disk, sr_disk_wiped -# *** Support for incremental tests in test classes *** -# From https://stackoverflow.com/questions/12411431/how-to-skip-the-rest-of-tests-in-the-class-if-one-has-failed -def pytest_runtest_makereport(item, call): - if "incremental" in item.keywords: - if call.excinfo is not None: - parent = item.parent - parent._previousfailed = item - -def pytest_runtest_setup(item): - previousfailed = getattr(item.parent, "_previousfailed", None) - if previousfailed is not None: - pytest.skip("previous test failed (%s)" % previousfailed.name) - -# *** End of: Support for incremental tests *** - def pytest_addoption(parser): parser.addoption( "--hosts", diff --git a/pytest.ini b/pytest.ini index 4aae1f6d..c8cc20da 100644 --- a/pytest.ini +++ b/pytest.ini @@ -3,7 +3,6 @@ addopts = -ra --maxfail=1 -s markers = # *** Markers that change test behaviour *** default_vm: mark a test with a default VM in case no --vm parameter was given. - incremental: mark a class so that any test failure skips the rest of the tests in the class. # *** Markers used to select tests at collect stage *** From 048f021aaa351b3375cd91c0d9cb83383ab6c27a Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Wed, 19 Jun 2024 16:48:54 +0200 Subject: [PATCH 17/75] conftest: regroup all pytest hooks together, separated from fixtures Signed-off-by: Yann Dirson --- conftest.py | 72 ++++++++++++++++++++++++++++------------------------- 1 file changed, 38 insertions(+), 34 deletions(-) diff --git a/conftest.py b/conftest.py index 7ca337b7..2d3c0b5a 100644 --- a/conftest.py +++ b/conftest.py @@ -19,6 +19,8 @@ # need to import them in the global conftest.py so that they are recognized as fixtures. from pkgfixtures import formatted_and_mounted_ext4_disk, sr_disk_wiped +# pytest hooks + def pytest_addoption(parser): parser.addoption( "--hosts", @@ -70,6 +72,42 @@ def pytest_configure(config): global_config.ignore_ssh_banner = config.getoption('--ignore-ssh-banner') global_config.ssh_output_max_lines = int(config.getoption('--ssh-output-max-lines')) +def pytest_generate_tests(metafunc): + if "vm_ref" in metafunc.fixturenames: + vms = metafunc.config.getoption("vm") + if not vms: + vms = [None] # no --vm parameter does not mean skip the test, for us, it means use the default + metafunc.parametrize("vm_ref", vms, indirect=True, scope="module") + +def pytest_collection_modifyitems(items, config): + # Automatically mark tests based on fixtures they require. + # Check pytest.ini or pytest --markers for marker descriptions. + + markable_fixtures = [ + 'uefi_vm', + 'unix_vm', + 'windows_vm', + 'hostA2', + 'hostB1', + 'sr_disk', + 'sr_disk_4k' + ] + + for item in items: + fixturenames = getattr(item, 'fixturenames', ()) + for fixturename in markable_fixtures: + if fixturename in fixturenames: + item.add_marker(fixturename) + + if 'vm_ref' not in fixturenames: + item.add_marker('no_vm') + + if item.get_closest_marker('multi_vms'): + # multi_vms implies small_vm + item.add_marker('small_vm') + +# fixtures + def setup_host(hostname_or_ip): pool = Pool(hostname_or_ip) h = pool.master @@ -404,37 +442,3 @@ def second_network(pytestconfig, host): if network_uuid == host.management_network(): pytest.fail("--second-network must NOT be the management network") return network_uuid - -def pytest_generate_tests(metafunc): - if "vm_ref" in metafunc.fixturenames: - vms = metafunc.config.getoption("vm") - if not vms: - vms = [None] # no --vm parameter does not mean skip the test, for us, it means use the default - metafunc.parametrize("vm_ref", vms, indirect=True, scope="module") - -def pytest_collection_modifyitems(items, config): - # Automatically mark tests based on fixtures they require. - # Check pytest.ini or pytest --markers for marker descriptions. - - markable_fixtures = [ - 'uefi_vm', - 'unix_vm', - 'windows_vm', - 'hostA2', - 'hostB1', - 'sr_disk', - 'sr_disk_4k' - ] - - for item in items: - fixturenames = getattr(item, 'fixturenames', ()) - for fixturename in markable_fixtures: - if fixturename in fixturenames: - item.add_marker(fixturename) - - if 'vm_ref' not in fixturenames: - item.add_marker('no_vm') - - if item.get_closest_marker('multi_vms'): - # multi_vms implies small_vm - item.add_marker('small_vm') From ae4fc4f4852a07e9cb6149b18fb88879d68b5c54 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Fri, 19 Jul 2024 14:46:40 +0200 Subject: [PATCH 18/75] CACHE_IMPORTED_VM: parse only once We will need this value in other places. Signed-off-by: Yann Dirson --- conftest.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/conftest.py b/conftest.py index 2d3c0b5a..0a65879b 100644 --- a/conftest.py +++ b/conftest.py @@ -19,6 +19,13 @@ # need to import them in the global conftest.py so that they are recognized as fixtures. from pkgfixtures import formatted_and_mounted_ext4_disk, sr_disk_wiped +# Do we cache VMs? +try: + from data import CACHE_IMPORTED_VM +except ImportError: + CACHE_IMPORTED_VM = False +assert CACHE_IMPORTED_VM in [True, False] + # pytest hooks def pytest_addoption(parser): @@ -325,13 +332,6 @@ def vm_ref(request): @pytest.fixture(scope="module") def imported_vm(host, vm_ref): - # Do we cache VMs? - try: - from data import CACHE_IMPORTED_VM - except ImportError: - CACHE_IMPORTED_VM = False - assert CACHE_IMPORTED_VM in [True, False] - if is_uuid(vm_ref): vm_orig = VM(vm_ref, host) name = vm_orig.name() From 8de40b4a427a3c59d584da1091cfb5bafe80cc60 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Tue, 23 Jul 2024 11:11:28 +0200 Subject: [PATCH 19/75] BaseVM: add missing param_* methods And make the whole easier to copypaste. Signed-off-by: Yann Dirson --- lib/basevm.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/lib/basevm.py b/lib/basevm.py index 951c6c59..09ebb466 100644 --- a/lib/basevm.py +++ b/lib/basevm.py @@ -2,7 +2,7 @@ import lib.commands as commands -from lib.common import _param_get, _param_remove, _param_set +from lib.common import _param_add, _param_clear, _param_get, _param_remove, _param_set from lib.sr import SR class BaseVM: @@ -16,13 +16,24 @@ def __init__(self, uuid, host): self.host = host def param_get(self, param_name, key=None, accept_unknown_key=False): - return _param_get(self.host, BaseVM.xe_prefix, self.uuid, param_name, key, accept_unknown_key) + return _param_get(self.host, self.xe_prefix, self.uuid, + param_name, key, accept_unknown_key) def param_set(self, param_name, value, key=None): - _param_set(self.host, BaseVM.xe_prefix, self.uuid, param_name, value, key) + _param_set(self.host, self.xe_prefix, self.uuid, + param_name, value, key) def param_remove(self, param_name, key, accept_unknown_key=False): - _param_remove(self.host, BaseVM.xe_prefix, self.uuid, param_name, key, accept_unknown_key) + _param_remove(self.host, self.xe_prefix, self.uuid, + param_name, key, accept_unknown_key) + + def param_add(self, param_name, value, key=None): + _param_add(self.host, self.xe_prefix, self.uuid, + param_name, value, key) + + def param_clear(self, param_name): + _param_clear(self.host, self.xe_prefix, self.uuid, + param_name) def name(self): return self.param_get('name-label') From e621f022f9b66b5e65468c8eb65aa50d0f60edb9 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Thu, 4 Jul 2024 18:19:59 +0200 Subject: [PATCH 20/75] vm: remove duplicate file_exists() Reported by mypy. Signed-off-by: Yann Dirson --- lib/vm.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/vm.py b/lib/vm.py index 51752b65..def89bf8 100644 --- a/lib/vm.py +++ b/lib/vm.py @@ -322,7 +322,7 @@ def tools_version(self): return "{major}.{minor}.{micro}-{build}".format(**version_dict) def file_exists(self, filepath): - """ Test that the file at filepath exists. """ + """Returns True if the file exists, otherwise returns False.""" return self.ssh_with_result(['test', '-f', filepath]).returncode == 0 def detect_package_manager(self): @@ -434,10 +434,6 @@ def clear_uefi_variables(self): """ self.param_remove('NVRAM', 'EFI-variables') - def file_exists(self, filepath): - """Returns True if the file exists, otherwise returns False.""" - return self.ssh_with_result(['test', '-f', filepath]).returncode == 0 - def sign_bins(self): for f in self.get_all_efi_bins(): self.sign(f) From 8e27d00fb5574fc07373e2aafcef8999f09a796e Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Mon, 8 Jul 2024 16:06:08 +0200 Subject: [PATCH 21/75] Host.join_pool(): update the host's pool after new pool is joined Signed-off-by: Yann Dirson --- lib/host.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/host.py b/lib/host.py index 034b6439..474ac0cb 100644 --- a/lib/host.py +++ b/lib/host.py @@ -520,6 +520,7 @@ def join_pool(self, pool): lambda: master.xe('host-param-get', {'uuid': self.uuid, 'param-name': 'enabled'}), f"Wait for pool {master} to see joined host {self} as enabled." ) + self.pool = pool def activate_smapi_driver(self, driver): sm_plugins = self.ssh(['grep', '[[:space:]]*sm-plugins[[:space:]]*=[[:space:]]*', XAPI_CONF_FILE]).splitlines() From cb3b8e44689715a72c64234fad98a2776edfa05f Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Thu, 25 Jul 2024 14:28:57 +0200 Subject: [PATCH 22/75] pytest: do not restrict timestamping to CLI We want to have it in log-file. Signed-off-by: Yann Dirson --- pytest.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pytest.ini b/pytest.ini index c8cc20da..10b632df 100644 --- a/pytest.ini +++ b/pytest.ini @@ -30,7 +30,7 @@ markers = quicktest: runs `quicktest`. log_cli = 1 log_cli_level = info -log_cli_format = %(asctime)s %(levelname)s %(message)s -log_cli_date_format = %b %d %H:%M:%S +log_format = %(asctime)s %(levelname)s %(message)s +log_date_format = %b %d %H:%M:%S filterwarnings = error xfail_strict=true From 286c4ef7f5594885550b596401d8488f20f46072 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Thu, 25 Jul 2024 16:05:36 +0200 Subject: [PATCH 23/75] pytest: add milliseconds to log timestamp Signed-off-by: Yann Dirson --- pytest.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytest.ini b/pytest.ini index 10b632df..786e9cc6 100644 --- a/pytest.ini +++ b/pytest.ini @@ -30,7 +30,7 @@ markers = quicktest: runs `quicktest`. log_cli = 1 log_cli_level = info -log_format = %(asctime)s %(levelname)s %(message)s +log_format = %(asctime)s.%(msecs)03d %(levelname)s %(message)s log_date_format = %b %d %H:%M:%S filterwarnings = error xfail_strict=true From fd2ca15958e88899badeced8de2c78179f03f2c7 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Wed, 31 Jul 2024 16:52:16 +0200 Subject: [PATCH 24/75] LocalCommandFailed: fix ctor upcall Signed-off-by: Yann Dirson --- lib/commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/commands.py b/lib/commands.py index c5a54bc5..534be408 100644 --- a/lib/commands.py +++ b/lib/commands.py @@ -26,7 +26,7 @@ def __init__(self, returncode, stdout, cmd): class LocalCommandFailed(BaseCommandFailed): def __init__(self, returncode, stdout, cmd): msg_end = f": {stdout}" if stdout else "." - super(SSHCommandFailed, self).__init__( + super(LocalCommandFailed, self).__init__( returncode, stdout, cmd, f'Local command ({cmd}) failed with return code {returncode}{msg_end}' ) From b4b8fe1aa25f1792e2a1992988be329acba7caad Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Tue, 11 Jun 2024 17:03:24 +0200 Subject: [PATCH 25/75] vm cache: shorten vm description by stripping .xva suffix With install tests the description becomes really long, use chars sparingly. Signed-off-by: Yann Dirson --- lib/common.py | 15 +++++++++++++++ lib/host.py | 4 ++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/lib/common.py b/lib/common.py index 66c8e4b0..d840304f 100644 --- a/lib/common.py +++ b/lib/common.py @@ -1,6 +1,7 @@ import getpass import inspect import logging +import sys import time import traceback from enum import Enum @@ -75,6 +76,20 @@ def safe_split(text, sep=','): """ A split function that returns an empty list if the input string is empty. """ return text.split(sep) if len(text) > 0 else [] +def strip_prefix(string, prefix): + if sys.version_info >= (3, 9): + return string.removeprefix(prefix) + if string.startswith(prefix): + return string[len(prefix):] + return string + +def strip_suffix(string, suffix): + if sys.version_info >= (3, 9): + return string.removesuffix(suffix) + if string.endswith(suffix): + return string[:-len(suffix)] + return string + def setup_formatted_and_mounted_disk(host, sr_disk, fs_type, mountpoint): if fs_type == 'ext4': option_force = '-F' diff --git a/lib/host.py b/lib/host.py index 474ac0cb..80d16ff9 100644 --- a/lib/host.py +++ b/lib/host.py @@ -8,7 +8,7 @@ import lib.commands as commands -from lib.common import _param_get, safe_split, to_xapi_bool, wait_for, wait_for_not +from lib.common import _param_get, safe_split, strip_suffix, to_xapi_bool, wait_for, wait_for_not from lib.common import prefix_object_name from lib.netutil import wrap_ip from lib.sr import SR @@ -202,7 +202,7 @@ def xo_server_reconnect(self): @staticmethod def vm_cache_key(uri): - return f"[Cache for {uri}]" + return f"[Cache for {strip_suffix(uri, '.xva')}]" def cached_vm(self, uri, sr_uuid): assert sr_uuid, "A SR UUID is necessary to use import cache" From 74253bd9b0be45b905f997ba07708d36c3dbabf6 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Wed, 19 Jun 2024 17:56:00 +0200 Subject: [PATCH 26/75] ssh: don't use a separate logger The logger setup was confusing when using --log-file-level=DEBUG, showing this debug-level output inside default --log-cli-level=INFO output, but without the debug context that only went into the logfile, and without any hint of what kind of output it was. This makes it use the same logger as all other debug output, preventing it from appearing in the wrong place, and adds a marker to hint it is output data - at the same time fixing a potential formatting issue, where arbitrary ssh output lines were interpreted as *format strings*. Signed-off-by: Yann Dirson --- lib/commands.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/lib/commands.py b/lib/commands.py index 534be408..78891fbd 100644 --- a/lib/commands.py +++ b/lib/commands.py @@ -59,12 +59,6 @@ def _ellide_log_lines(log): reduced_message.append("(...)") return "\n{}".format("\n".join(reduced_message)) -OUPUT_LOGGER = logging.getLogger('output') -OUPUT_LOGGER.propagate = False -OUTPUT_HANDLER = logging.StreamHandler() -OUPUT_LOGGER.addHandler(OUTPUT_HANDLER) -OUTPUT_HANDLER.setFormatter(logging.Formatter('%(message)s')) - def _ssh(hostname_or_ip, cmd, check, simple_output, suppress_fingerprint_warnings, background, target_os, decode, options): opts = list(options) @@ -111,7 +105,7 @@ def _ssh(hostname_or_ip, cmd, check, simple_output, suppress_fingerprint_warning for line in iter(process.stdout.readline, b''): readable_line = line.decode(errors='replace').strip() stdout.append(line) - OUPUT_LOGGER.debug(readable_line) + logging.debug("> %s", readable_line) _, stderr = process.communicate() res = subprocess.CompletedProcess(ssh_cmd, process.returncode, b''.join(stdout), stderr) From 917183f6297b055e7a47494b6771948cc612c085 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Thu, 23 May 2024 14:05:33 +0200 Subject: [PATCH 27/75] WIP vm.clone: avoid foo_clone_for_tests_clone_for_tests_clone_for_tests FIXME: replace with explicit clone naming --- lib/vm.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/vm.py b/lib/vm.py index def89bf8..532fb4a7 100644 --- a/lib/vm.py +++ b/lib/vm.py @@ -474,7 +474,9 @@ def destroy_vtpm(self): return self.host.xe('vtpm-destroy', {'uuid': vtpm_uuid}, force=True) def clone(self): - name = self.name() + '_clone_for_tests' + name = self.name() + if not name.endswith('_clone_for_tests'): + name += '_clone_for_tests' logging.info("Clone VM") uuid = self.host.xe('vm-clone', {'uuid': self.uuid, 'new-name-label': name}) return VM(uuid, self.host) From a49d5e2580d99a9a1d2d7d9ec1effea40fcc1e10 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Thu, 13 Jun 2024 17:17:34 +0200 Subject: [PATCH 28/75] host.main_sr_uuid: don't assume $HOSTNAME is the XAPI name-label If the host's name-label was changed using XO after installation, but the UNIX hostname was not, $HOSTNAME is wrong. Signed-off-by: Yann Dirson --- lib/host.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/host.py b/lib/host.py index 80d16ff9..327dc7fe 100644 --- a/lib/host.py +++ b/lib/host.py @@ -480,9 +480,11 @@ def main_sr_uuid(self): sr_uuid = None if DEFAULT_SR == 'local': + hostname = self.xe('host-param-get', {'uuid': self.uuid, + 'param-name': 'name-label'}) local_sr_uuids = safe_split( # xe sr-list doesn't support filtering by host UUID! - self.ssh(['xe sr-list host=$HOSTNAME content-type=user minimal=true']), + self.xe('sr-list', {'host': hostname, 'content-type': 'user', 'minimal': 'true'}), ',' ) assert local_sr_uuids, f"DEFAULT_SR=='local' so there must be a local SR on host {self}" From 78c64ca7037996b94c828070009ae74438c494cf Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Thu, 8 Aug 2024 13:40:59 +0200 Subject: [PATCH 29/75] Add VBD object --- lib/vbd.py | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 lib/vbd.py diff --git a/lib/vbd.py b/lib/vbd.py new file mode 100644 index 00000000..fb535cac --- /dev/null +++ b/lib/vbd.py @@ -0,0 +1,44 @@ +import logging + +from lib.common import _param_add, _param_clear, _param_get, _param_remove, _param_set + +class VBD: + xe_prefix = "vbd" + + def __init__(self, uuid, vm, device): + self.uuid = uuid + self.vm = vm + self.device = device + + def plug(self): + self.vm.host.xe("vbd-plug", {'uuid': self.uuid}) + + def unplug(self): + self.vm.host.xe("vbd-unplug", {'uuid': self.uuid}) + + def param_get(self, param_name, key=None, accept_unknown_key=False): + return _param_get(self.vm.host, self.xe_prefix, self.uuid, + param_name, key, accept_unknown_key) + + def param_set(self, param_name, value, key=None): + _param_set(self.vm.host, self.xe_prefix, self.uuid, + param_name, value, key) + + def param_remove(self, param_name, key, accept_unknown_key=False): + _param_remove(self.vm.host, self.xe_prefix, self.uuid, + param_name, key, accept_unknown_key) + + def param_add(self, param_name, value, key=None): + _param_add(self.vm.host, self.xe_prefix, self.uuid, + param_name, value, key) + + def param_clear(self, param_name): + _param_clear(self.vm.host, self.xe_prefix, self.uuid, + param_name) + + def destroy(self): + logging.info("Destroy %s", self) + self.vm.host.pool.master.xe('vbd-destroy', {'uuid': self.uuid}) + + def __str__(self): + return f"VBD {self.uuid} for {self.device} of VM {self.vm.uuid}" From f686df5b08bac5bf5ce5bd21e707632bc01b1ed0 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Thu, 8 Aug 2024 14:00:00 +0200 Subject: [PATCH 30/75] Add PIF object Signed-off-by: Yann Dirson --- lib/pif.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 lib/pif.py diff --git a/lib/pif.py b/lib/pif.py new file mode 100644 index 00000000..5991f391 --- /dev/null +++ b/lib/pif.py @@ -0,0 +1,30 @@ +import lib.commands as commands + +from lib.common import _param_add, _param_clear, _param_get, _param_remove, _param_set + +class PIF: + xe_prefix = "pif" + + def __init__(self, uuid, host): + self.uuid = uuid + self.host = host + + def param_get(self, param_name, key=None, accept_unknown_key=False): + return _param_get(self.host, self.xe_prefix, self.uuid, + param_name, key, accept_unknown_key) + + def param_set(self, param_name, value, key=None): + _param_set(self.host, self.xe_prefix, self.uuid, + param_name, value, key) + + def param_remove(self, param_name, key, accept_unknown_key=False): + _param_remove(self.host, self.xe_prefix, self.uuid, + param_name, key, accept_unknown_key) + + def param_add(self, param_name, value, key=None): + _param_add(self.host, self.xe_prefix, self.uuid, + param_name, value, key) + + def param_clear(self, param_name): + _param_clear(self.host, self.xe_prefix, self.uuid, + param_name) From 1cf9e6db62b7e190c937e272531734b370fb37e7 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Thu, 8 Aug 2024 14:02:36 +0200 Subject: [PATCH 31/75] Host: add missing param_* methods Signed-off-by: Yann Dirson --- lib/host.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/lib/host.py b/lib/host.py index 327dc7fe..dad43cb2 100644 --- a/lib/host.py +++ b/lib/host.py @@ -8,7 +8,8 @@ import lib.commands as commands -from lib.common import _param_get, safe_split, strip_suffix, to_xapi_bool, wait_for, wait_for_not +from lib.common import _param_add, _param_clear, _param_get, _param_remove, _param_set +from lib.common import safe_split, strip_suffix, to_xapi_bool, wait_for, wait_for_not from lib.common import prefix_object_name from lib.netutil import wrap_ip from lib.sr import SR @@ -97,7 +98,24 @@ def stringify(key, value): return result def param_get(self, param_name, key=None, accept_unknown_key=False): - return _param_get(self, Host.xe_prefix, self.uuid, param_name, key, accept_unknown_key) + return _param_get(self, self.xe_prefix, self.uuid, + param_name, key, accept_unknown_key) + + def param_set(self, param_name, value, key=None): + _param_set(self, self.xe_prefix, self.uuid, + param_name, value, key) + + def param_remove(self, param_name, key, accept_unknown_key=False): + _param_remove(self, self.xe_prefix, self.uuid, + param_name, key, accept_unknown_key) + + def param_add(self, param_name, value, key=None): + _param_add(self, self.xe_prefix, self.uuid, + param_name, value, key) + + def param_clear(self, param_name): + _param_clear(self, self.xe_prefix, self.uuid, + param_name) def create_file(self, filename, text): with tempfile.NamedTemporaryFile('w') as file: From b0965390ccde63aaf963be48a7553961d4d5c04b Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Thu, 1 Aug 2024 14:22:00 +0200 Subject: [PATCH 32/75] VDI: add param_* APIs Signed-off-by: Yann Dirson --- lib/vdi.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/lib/vdi.py b/lib/vdi.py index e9f82b18..c0a59534 100644 --- a/lib/vdi.py +++ b/lib/vdi.py @@ -1,6 +1,10 @@ import logging +from lib.common import _param_add, _param_clear, _param_get, _param_remove, _param_set + class VDI: + xe_prefix = "vdi" + def __init__(self, sr, uuid): self.uuid = uuid # TODO: use a different approach when migration is possible @@ -12,3 +16,23 @@ def destroy(self): def __str__(self): return f"VDI {self.uuid} on SR {self.sr.uuid}" + + def param_get(self, param_name, key=None, accept_unknown_key=False): + return _param_get(self.sr.pool.master, self.xe_prefix, self.uuid, + param_name, key, accept_unknown_key) + + def param_set(self, param_name, value, key=None): + _param_set(self.sr.pool.master, self.xe_prefix, self.uuid, + param_name, value, key) + + def param_add(self, param_name, value, key=None): + _param_add(self.sr.pool.master, self.xe_prefix, self.uuid, + param_name, value, key) + + def param_clear(self, param_name): + _param_clear(self.sr.pool.master, self.xe_prefix, self.uuid, + param_name) + + def param_remove(self, param_name, key, accept_unknown_key=False): + _param_remove(self.sr.pool.master, self.xe_prefix, self.uuid, + param_name, key, accept_unknown_key) From 62329ea811d2fa06812f82eb0dd46012316d7a6c Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Thu, 1 Aug 2024 14:23:21 +0200 Subject: [PATCH 33/75] VDI: allow constructing an object when knowing just the Host This class was written for a very specific use-case, make it more largely useful. Signed-off-by: Yann Dirson --- lib/sr.py | 2 +- lib/vdi.py | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/sr.py b/lib/sr.py index c36462ba..7dd8e142 100644 --- a/lib/sr.py +++ b/lib/sr.py @@ -157,7 +157,7 @@ def create_vdi(self, name_label, virtual_size=64): 'virtual-size': str(virtual_size), 'sr-uuid': self.uuid }) - return VDI(self, vdi_uuid) + return VDI(vdi_uuid, sr=self) def run_quicktest(self): logging.info(f"Run quicktest on SR {self.uuid}") diff --git a/lib/vdi.py b/lib/vdi.py index c0a59534..092f7bc4 100644 --- a/lib/vdi.py +++ b/lib/vdi.py @@ -5,10 +5,17 @@ class VDI: xe_prefix = "vdi" - def __init__(self, sr, uuid): + def __init__(self, uuid, *, host=None, sr=None): self.uuid = uuid # TODO: use a different approach when migration is possible - self.sr = sr + if sr is None: + sr_uuid = host.get_vdi_sr_uuid(uuid) + # avoid circular import + # FIXME should get it from Host instead + from lib.sr import SR + self.sr = SR(sr_uuid, host.pool) + else: + self.sr = sr def destroy(self): logging.info("Destroy %s", self) From eca3178727d02c0a83bfdbaf7864a982f9f7fdbe Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Thu, 1 Aug 2024 14:32:34 +0200 Subject: [PATCH 34/75] VDI: add more methods Signed-off-by: Yann Dirson --- lib/vdi.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/vdi.py b/lib/vdi.py index 092f7bc4..3b331731 100644 --- a/lib/vdi.py +++ b/lib/vdi.py @@ -21,6 +21,13 @@ def destroy(self): logging.info("Destroy %s", self) self.sr.pool.master.xe('vdi-destroy', {'uuid': self.uuid}) + def clone(self): + uuid = self.sr.pool.master.xe('vdi-clone', {'uuid': self.uuid}) + return VDI(uuid, sr=self.sr) + + def readonly(self): + return self.param_get("read-only") == "true" + def __str__(self): return f"VDI {self.uuid} on SR {self.sr.uuid}" From a7701aeb6a0df72b82ce6d6f6c7a58838cb6ffd3 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Thu, 1 Aug 2024 14:03:37 +0200 Subject: [PATCH 35/75] WIP get_vdi_sr_uuid: move from BaseVM to Host This has nothing to do with the VM, which prevents usage from places with no VM. FIXME: should this be in Pool instead? Signed-off-by: Yann Dirson --- lib/basevm.py | 11 ++++------- lib/host.py | 3 +++ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/basevm.py b/lib/basevm.py index 09ebb466..ea1a9d4d 100644 --- a/lib/basevm.py +++ b/lib/basevm.py @@ -49,7 +49,7 @@ def vdi_uuids(self, sr_uuid=None): vdis_on_sr = [] for vdi in vdis: - if self.get_vdi_sr_uuid(vdi) == sr_uuid: + if self.host.get_vdi_sr_uuid(vdi) == sr_uuid: vdis_on_sr.append(vdi) return vdis_on_sr @@ -62,19 +62,16 @@ def destroy(self): self.destroy_vdi(vdi_uuid) self._destroy() - def get_vdi_sr_uuid(self, vdi_uuid): - return self.host.xe('vdi-param-get', {'uuid': vdi_uuid, 'param-name': 'sr-uuid'}) - def all_vdis_on_host(self, host): for vdi_uuid in self.vdi_uuids(): - sr = SR(self.get_vdi_sr_uuid(vdi_uuid), self.host.pool) + sr = SR(self.host.get_vdi_sr_uuid(vdi_uuid), self.host.pool) if not sr.attached_to_host(host): return False return True def all_vdis_on_sr(self, sr): for vdi_uuid in self.vdi_uuids(): - if self.get_vdi_sr_uuid(vdi_uuid) != sr.uuid: + if self.host.get_vdi_sr_uuid(vdi_uuid) != sr.uuid: return False return True @@ -82,7 +79,7 @@ def get_sr(self): # in this method we assume the SR of the first VDI is the VM SR vdis = self.vdi_uuids() assert len(vdis) > 0, "Don't ask for the SR of a VM without VDIs!" - sr = SR(self.get_vdi_sr_uuid(vdis[0]), self.host.pool) + sr = SR(self.host.get_vdi_sr_uuid(vdis[0]), self.host.pool) assert sr.attached_to_host(self.host) return sr diff --git a/lib/host.py b/lib/host.py index dad43cb2..1c2e9234 100644 --- a/lib/host.py +++ b/lib/host.py @@ -566,3 +566,6 @@ def enable_hsts_header(self): def disable_hsts_header(self): self.ssh(['rm', '-f', f'{XAPI_CONF_DIR}/00-XCP-ng-tests-enable-hsts-header.conf']) self.restart_toolstack(verify=True) + + def get_vdi_sr_uuid(self, vdi_uuid): + return self.xe('vdi-param-get', {'uuid': vdi_uuid, 'param-name': 'sr-uuid'}) From 4309f01ae14ae6e24a8c0c02ab8977c55aae51f4 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Wed, 19 Jun 2024 16:54:16 +0200 Subject: [PATCH 36/75] conftest: add pytest hook to make test results visible from fixtures Comes from official pytest examples, with included example: https://docs.pytest.org/en/latest/example/simple.html#making-test-result-information-available-in-fixtures This was made possible by dropping the "incremental" marker. This uses new-style hook wrappers, which require pluggy>=1.1, and likely pytest 8.0, which both require python>=3.8. Signed-off-by: Yann Dirson --- README.md | 2 +- conftest.py | 21 +++++++++++++++++++++ requirements/base.txt | 3 ++- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ba61dad1..97c1d7ef 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ Note: this is a perpertual work in progress. If you encounter any obstacles or bugs, let us know! ## Main requirements -* python >= 3.5 +* python >= 3.8 * packages as listed in requirements/base.txt * extra test-specific requirements are documented in the test file "Requirements" header diff --git a/conftest.py b/conftest.py index 0a65879b..3c176a49 100644 --- a/conftest.py +++ b/conftest.py @@ -113,6 +113,27 @@ def pytest_collection_modifyitems(items, config): # multi_vms implies small_vm item.add_marker('small_vm') +# BEGIN make test results visible from fixtures +# from https://docs.pytest.org/en/latest/example/simple.html#making-test-result-information-available-in-fixtures + +# FIXME we may have to move this into lib/ if fixtures in sub-packages +# want to make use of this feature + +PHASE_REPORT_KEY = pytest.StashKey[dict[str, pytest.CollectReport]]() +@pytest.hookimpl(wrapper=True, tryfirst=True) +def pytest_runtest_makereport(item, call): + # execute all other hooks to obtain the report object + rep = yield + + # store test results for each phase of a call, which can + # be "setup", "call", "teardown" + item.stash.setdefault(PHASE_REPORT_KEY, {})[rep.when] = rep + + return rep + +# END make test results visible from fixtures + + # fixtures def setup_host(hostname_or_ip): diff --git a/requirements/base.txt b/requirements/base.txt index facd2a48..3df8b072 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,3 +1,4 @@ cryptography>=3.3.1 packaging>=20.7 -pytest>=5.4.0 +pytest>=8.0.0 +pluggy>=1.1.0 From c766e65bffae9771f3cc255492c838e18934166f Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Mon, 13 May 2024 17:15:15 +0200 Subject: [PATCH 37/75] WIP move generic pxe-boot code to new pxe.py improvements: - PXE_CONFIG_SERVER is checked only once at parse time - don't over-handle exceptions FIXME: - test - find a proper way to get "restore" not to block reboot (disabled the workaround to understand the problem) --- lib/pxe.py | 50 +++++++++++++++++++++++++++++++++++++ scripts/install_xcpng.py | 53 ++++++---------------------------------- 2 files changed, 57 insertions(+), 46 deletions(-) create mode 100644 lib/pxe.py diff --git a/lib/pxe.py b/lib/pxe.py new file mode 100644 index 00000000..232efd5b --- /dev/null +++ b/lib/pxe.py @@ -0,0 +1,50 @@ +from lib.commands import ssh, scp, SSHCommandFailed + +PXE_CONFIG_DIR = "/pxe/configs/custom" + +try: + from data import PXE_CONFIG_SERVER + assert PXE_CONFIG_SERVER +except ImportError: + raise Exception('No address for the PXE server found in data.py (`PXE_CONFIG_SERVER`)') + +def generate_boot_conf(directory, installer): + if True: + rt = "" + else: + # (because of the restore case), we disable the text ui from + # the installer completely, to workaround a bug that leaves us + # stuck on a confirmation dialog at the end of the operation. + rt = 'rt=1' + with open(f'{directory}/boot.conf', 'w') as bootfile: + bootfile.write(f""" +answerfile=custom +installer={installer} +is_default=1 +{rt} +""") + +def server_push_config(mac_address, tmp_local_path): + remote_dir = f'{PXE_CONFIG_DIR}/{mac_address}/' + server_remove_config(mac_address) + ssh(PXE_CONFIG_SERVER, ['mkdir', '-p', remote_dir]) + scp(PXE_CONFIG_SERVER, f'{tmp_local_path}/boot.conf', remote_dir) + scp(PXE_CONFIG_SERVER, f'{tmp_local_path}/answerfile.xml', remote_dir) + +def server_remove_config(mac_address): + assert mac_address # protection against deleting the whole parent dir! + remote_dir = f'{PXE_CONFIG_DIR}/{mac_address}/' + ssh(PXE_CONFIG_SERVER, ['rm', '-rf', remote_dir]) + +def server_remove_bootconf(mac_address): + assert mac_address + distant_file = f'{PXE_CONFIG_DIR}/{mac_address}/boot.conf' + ssh(PXE_CONFIG_SERVER, ['rm', '-rf', distant_file]) + +def arp_addresses_for(mac_address): + output = ssh( + PXE_CONFIG_SERVER, + ['arp', '-n', '|', 'grep', mac_address, '|', 'awk', '\'{ print $1 }\''] + ) + candidate_ips = output.splitlines() + return candidate_ips diff --git a/scripts/install_xcpng.py b/scripts/install_xcpng.py index 47a716f2..7af91437 100755 --- a/scripts/install_xcpng.py +++ b/scripts/install_xcpng.py @@ -15,6 +15,7 @@ from packaging import version sys.path.append(f"{os.path.abspath(os.path.dirname(__file__))}/..") # noqa +from lib import pxe from lib.commands import ssh, scp, SSHCommandFailed from lib.common import wait_for, is_uuid from lib.host import host_data @@ -23,25 +24,8 @@ logging.basicConfig(format='[%(levelname)s] %(message)s', level=logging.INFO) -PXE_CONFIG_DIR = "/pxe/configs/custom" - def pxe_address(): - try: - from data import PXE_CONFIG_SERVER - return PXE_CONFIG_SERVER - except ImportError: - raise Exception('No address for the PXE server found in data.py (`PXE_CONFIG_SERVER`)') - -def generate_boot_conf(directory, installer, action): - # in case of restore, we disable the text ui from the installer completely, - # to workaround a bug that leaves us stuck on a confirmation dialog at the end of the operation. - rt = 'rt=1' if action == 'restore' else '' - with open(f'{directory}/boot.conf', 'w') as bootfile: - bootfile.write(f"""answerfile=custom -installer={installer} -is_default=1 -{rt} -""") + return pxe.PXE_CONFIG_SERVER def generate_answerfile(directory, installer, hostname_or_ip, target_hostname, action, hdd, netinstall_gpg_check): pxe = pxe_address() @@ -89,37 +73,16 @@ def generate_answerfile(directory, installer, hostname_or_ip, target_hostname, a raise Exception(f"Unknown action: `{action}`") def copy_files_to_pxe(mac_address, tmp_local_path): - assert mac_address - pxe = pxe_address() - remote_dir = f'{PXE_CONFIG_DIR}/{mac_address}/' - clean_files_on_pxe(mac_address) - ssh(pxe, ['mkdir', '-p', remote_dir]) - scp(pxe, f'{tmp_local_path}/boot.conf', remote_dir) - scp(pxe, f'{tmp_local_path}/answerfile.xml', remote_dir) + pxe.server_push_config(mac_address, tmp_local_path) def clean_files_on_pxe(mac_address): - assert mac_address # protection against deleting the whole parent dir! - pxe = pxe_address() - remote_dir = f'{PXE_CONFIG_DIR}/{mac_address}/' - ssh(pxe, ['rm', '-rf', remote_dir]) + pxe.server_remove_config(mac_address) def clean_bootconf_on_pxe(mac_address): - assert mac_address - pxe = pxe_address() - distant_file = f'{PXE_CONFIG_DIR}/{mac_address}/boot.conf' - try: - ssh(pxe, ['rm', '-rf', distant_file]) - except SSHCommandFailed as e: - raise Exception('ERROR: failed to clean the boot.conf file.' + e) + pxe.server_remove_bootconf(mac_address) def get_candidate_ips(mac_address): - pxe = pxe_address() - output = ssh( - pxe, - ['arp', '-n', '|', 'grep', mac_address, '|', 'awk', '\'{ print $1 }\''] - ) - candidate_ips = output.splitlines() - return candidate_ips + return pxe.arp_addresses_for(mac_address) def is_ip_active(ip): return not os.system(f"ping -c 3 -W 10 {ip} > /dev/null 2>&1") @@ -191,8 +154,6 @@ def main(): # *** "fail early" checks - pxe = pxe_address() # raises if not defined - if not is_uuid(args.vm_uuid): raise Exception(f'The provided VM UUID is invalid: {args.vm_uuid}') @@ -237,7 +198,7 @@ def main(): hdd = 'nvme0n1' if vm.is_uefi else 'sda' generate_answerfile(tmp_local_path, installer, args.host, args.target_hostname, args.action, hdd, netinstall_gpg_check) - generate_boot_conf(tmp_local_path, installer, args.action) + pxe.generate_boot_conf(tmp_local_path, installer) logging.info('Copy files to the pxe server') copy_files_to_pxe(mac_address, tmp_local_path) atexit.register(lambda: clean_files_on_pxe(mac_address)) From ba3db19a2917257239851ab0fb175abe10d46b4c Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Tue, 7 May 2024 15:58:16 +0200 Subject: [PATCH 38/75] install 1/n: fixture to create VMs from template Signed-off-by: Yann Dirson --- conftest.py | 108 ++++++++++++++++++++++++++++++++++++++ data.py-dist | 4 ++ lib/common.py | 21 ++++++++ lib/host.py | 9 ++++ lib/installer.py | 4 ++ lib/vm.py | 30 +++++++++++ pytest.ini | 3 ++ tests/install/__init__.py | 0 tests/install/test.py | 29 ++++++++++ 9 files changed, 208 insertions(+) create mode 100644 lib/installer.py create mode 100644 tests/install/__init__.py create mode 100644 tests/install/test.py diff --git a/conftest.py b/conftest.py index 3c176a49..ba286700 100644 --- a/conftest.py +++ b/conftest.py @@ -7,10 +7,12 @@ import lib.config as global_config +from lib.common import callable_marker from lib.common import wait_for, vm_image, is_uuid from lib.common import setup_formatted_and_mounted_disk, teardown_formatted_and_mounted_disk from lib.netutil import is_ipv6 from lib.pool import Pool +from lib.sr import SR from lib.vm import VM from lib.xo import xo_cli @@ -375,6 +377,112 @@ def imported_vm(host, vm_ref): logging.info("<< Destroy VM") vm.destroy(verify=True) +@pytest.fixture(scope="function") +def create_vms(request, host): + """ + Returns list of VM objects created from `vm_definitions` marker. + + `vm_definitions` marker test author to specify one or more VMs, + using one `dict` per VM. + + Mandatory keys: + - `name`: name of the VM to create (str) + - `template`: name (or UUID) of template to use (str) + + Optional keys: see example below + + Example: + ------- + > @pytest.mark.vm_definitions( + > dict(name="vm1", template="Other install media"), + > dict(name="vm2", + > template="CentOS 7", + > params=( + > dict(param_name="memory-static-max", value="4GiB"), + > dict(param_name="HVM-boot-params", key="order", value="dcn"), + > ), + > vdis=[dict(name="vm 2 system disk", + > size="100GiB", + > device="xvda", + > userdevice="0", + > )], + > cd_vbd=dict(device="xvdd", userdevice="3"), + > vifs=[dict(index=0, network_uuid=NETWORKS["MGMT"])], + > )) + > def test_foo(create_vms): + > ... + + """ + marker = request.node.get_closest_marker("vm_definitions") + if marker is None: + raise Exception("No vm_definitions marker specified.") + param_mapping = marker.kwargs.get("param_mapping", {}) + + vm_defs = [] + for vm_def in marker.args: + vm_def = callable_marker(vm_def, request, param_mapping=param_mapping) + assert "name" in vm_def + assert "template" in vm_def + # FIXME should check optional vdis contents + # FIXME should check for extra args + vm_defs.append(vm_def) + + try: + vms = [] + vdis = [] + vbds = [] + for vm_def in vm_defs: + _create_vm(vm_def, host, vms, vdis, vbds) + yield vms + + except Exception: + logging.error("exception caught...") + raise + + finally: + for vbd in vbds: + logging.info("<< Destroy VBD %s", vbd.uuid) + vbd.destroy() + for vdi in vdis: + logging.info("<< Destroy VDI %s", vdi.uuid) + vdi.destroy() + for vm in vms: + logging.info("<< Destroy VM %s", vm.uuid) + vm.destroy(verify=True) + +def _create_vm(vm_def, host, vms, vdis, vbds): + vm_name = vm_def["name"] + vm_template = vm_def["template"] + + logging.info(">> Install VM %r from template %r", vm_name, vm_template) + + vm = host.vm_from_template(vm_name, vm_template) + + # VM is now created, make sure we clean it up on any subsequent failure + vms.append(vm) + + if "vdis" in vm_def: + for vdi_def in vm_def["vdis"]: + sr = SR(host.main_sr_uuid(), host.pool) + vdi = sr.create_vdi(vdi_def["name"], vdi_def["size"]) + vdis.append(vdi) + # connect to VM + vbd = vm.create_vbd(vdi_def["device"], vdi.uuid) + vbds.append(vbd) + vbd.param_set(param_name="userdevice", value=vdi_def["userdevice"]) + + if "cd_vbd" in vm_def: + vm.create_cd_vbd(**vm_def["cd_vbd"]) + + if "vifs" in vm_def: + for vif_def in vm_def["vifs"]: + vm.create_vif(vif_def["index"], vif_def["network_uuid"]) + + if "params" in vm_def: + for param_def in vm_def["params"]: + logging.info("Setting param %s", param_def) + vm.param_set(**param_def) + @pytest.fixture(scope="module") def started_vm(imported_vm): vm = imported_vm diff --git a/data.py-dist b/data.py-dist index a1bc8d3a..17d68760 100644 --- a/data.py-dist +++ b/data.py-dist @@ -20,6 +20,10 @@ HOSTS = { # "testhost1": {"user": "root", "password": "", 'skip_xo_config': True}, } +NETWORKS = { +# "MGMT": "7a48464c-1e70-4e2e-a449-9f2514564381", +} + # PXE config server for automated XCP-ng installation PXE_CONFIG_SERVER = 'pxe' diff --git a/lib/common.py b/lib/common.py index d840304f..6d2f1acb 100644 --- a/lib/common.py +++ b/lib/common.py @@ -7,6 +7,8 @@ from enum import Enum from uuid import UUID +import pytest + import lib.commands as commands class PackageManagerEnum(Enum): @@ -30,6 +32,25 @@ def prefix_object_name(label): name_prefix = f"[{getpass.getuser()}]" return f"{name_prefix} {label}" +def callable_marker(value, request, param_mapping): + """Process value optionally generated by fixture-dependent callable. + + Typically useful for fixtures using pytest markers on parametrized tests. + + Such markers take 2 parameters: + - one or more value(s), or callable(s) that will return a value + - a mapping of parameters to test fixture names, to pass as named arguments + to the callable + """ + if callable(value): + try: + params = { arg_name: request.getfixturevalue(fixture_name) + for arg_name, fixture_name in param_mapping.items() } + except pytest.FixtureLookupError as e: + raise RuntimeError("fixture in mapping not found on test") from e + value = value(**params) + return value + def wait_for(fn, msg=None, timeout_secs=2 * 60, retry_delay_secs=2, invert=False): if msg is not None: logging.info(msg) diff --git a/lib/host.py b/lib/host.py index 1c2e9234..6a5543fb 100644 --- a/lib/host.py +++ b/lib/host.py @@ -267,6 +267,15 @@ def import_vm(self, uri, sr_uuid=None, use_cache=False): vm.param_set('name-description', cache_key) return vm + def vm_from_template(self, name, template): + params = { + "new-name-label": prefix_object_name(name), + "template": template, + "sr-uuid": self.main_sr_uuid(), + } + vm_uuid = self.xe('vm-install', params) + return VM(vm_uuid, self) + 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 diff --git a/lib/installer.py b/lib/installer.py new file mode 100644 index 00000000..e37f0ba0 --- /dev/null +++ b/lib/installer.py @@ -0,0 +1,4 @@ +def perform_install(*, iso, host_vm): + host_vm.insert_cd(iso) + + host_vm.eject_cd() diff --git a/lib/vm.py b/lib/vm.py index 532fb4a7..6dcf6894 100644 --- a/lib/vm.py +++ b/lib/vm.py @@ -9,6 +9,7 @@ from lib.basevm import BaseVM from lib.common import PackageManagerEnum, parse_xe_dict, safe_split, wait_for, wait_for_not from lib.snapshot import Snapshot +from lib.vbd import VBD from lib.vif import VIF class VM(BaseVM): @@ -247,6 +248,14 @@ def vifs(self): _vifs.append(VIF(vif_uuid, self)) return _vifs + # FIXME: use network_name instead? + def create_vif(self, vif_num, network_uuid): + logging.info("Create VIF %d to network %r on VM %s", vif_num, network_uuid, self.uuid) + self.host.xe('vif-create', {'vm-uuid': self.uuid, + 'device': str(vif_num), + 'network-uuid': network_uuid, + }) + def is_running_on_host(self, host): return self.is_running() and self.param_get('resident-on') == host.uuid @@ -473,6 +482,27 @@ def destroy_vtpm(self): logging.info("Destroying vTPM %s" % vtpm_uuid) return self.host.xe('vtpm-destroy', {'uuid': vtpm_uuid}, force=True) + def create_vbd(self, device, vdi_uuid): + logging.info("Create VBD %r for VDI %r on VM %s", device, vdi_uuid, self.uuid) + vbd_uuid = self.host.xe('vbd-create', {'vm-uuid': self.uuid, + 'device': device, + 'vdi-uuid': vdi_uuid, + }) + logging.info("New VBD %s", vbd_uuid) + return VBD(vbd_uuid, self, device) + + def create_cd_vbd(self, device, userdevice): + logging.info("Create CD VBD %r on VM %s", device, self.uuid) + vbd_uuid = self.host.xe('vbd-create', {'vm-uuid': self.uuid, + 'device': device, + 'type': 'CD', + 'mode': 'RO', + }) + vbd = VBD(vbd_uuid, self, device) + vbd.param_set(param_name="userdevice", value=userdevice) + logging.info("New VBD %s", vbd_uuid) + return vbd + def clone(self): name = self.name() if not name.endswith('_clone_for_tests'): diff --git a/pytest.ini b/pytest.ini index 786e9cc6..61aa3945 100644 --- a/pytest.ini +++ b/pytest.ini @@ -18,6 +18,9 @@ markers = unix_vm: tests that require a unix/linux VM to run. windows_vm: tests that require a Windows VM to run. + # * VM-related markers to give parameters to fixtures + vm_definitions: dicts of VM defs for create_vms fixture. + # * Test targets related to VMs small_vm: tests that it is enough to run just once, using the smallest possible VM. big_vm: tests that it would be good to run with a big VM. diff --git a/tests/install/__init__.py b/tests/install/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/install/test.py b/tests/install/test.py new file mode 100644 index 00000000..94193ee8 --- /dev/null +++ b/tests/install/test.py @@ -0,0 +1,29 @@ +import logging +import pytest + +from lib import installer + +from data import NETWORKS +assert "MGMT" in NETWORKS + +class TestNested: + @pytest.mark.vm_definitions( + dict(name="vm1", + template="Other install media", + params=( + # dict(param_name="", value=""), + dict(param_name="memory-static-max", value="4GiB"), + dict(param_name="memory-dynamic-max", value="4GiB"), + dict(param_name="memory-dynamic-min", value="4GiB"), + dict(param_name="platform", key="exp-nested-hvm", value="true"), # FIXME < 8.3 host? + dict(param_name="HVM-boot-params", key="firmware", value="uefi"), + dict(param_name="HVM-boot-params", key="order", value="dc"), + dict(param_name="platform", key="device-model", value="qemu-upstream-uefi"), + ), + vdis=[dict(name="vm1 system disk", size="100GiB", device="xvda", userdevice="0")], + cd_vbd=dict(device="xvdd", userdevice="3"), + vifs=[dict(index=0, network_uuid=NETWORKS["MGMT"])], + )) + def test_install(self, create_vms): + assert len(create_vms) == 1 + installer.perform_install(iso="xcp-ng-8.2.1-20231130.iso", host_vm=create_vms[0]) From 442662f7c7b21aac432c5078aea26f1f3d1d281a Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Thu, 25 Jul 2024 16:43:50 +0200 Subject: [PATCH 39/75] install 2/n: use iso-remaster to plug an hardcoded answerfile This is a first step for answerfile handling, to be able to run a first real installation. Signed-off-by: Yann Dirson --- data.py-dist | 20 +++++++++++ pytest.ini | 3 ++ tests/install/conftest.py | 70 +++++++++++++++++++++++++++++++++++++++ tests/install/test.py | 5 +-- 4 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 tests/install/conftest.py diff --git a/data.py-dist b/data.py-dist index 17d68760..a6d6d9ca 100644 --- a/data.py-dist +++ b/data.py-dist @@ -30,12 +30,32 @@ PXE_CONFIG_SERVER = 'pxe' # Default VM images location DEF_VM_URL = 'http://pxe/images/' +ANSWERFILE_URL = f"http://{PXE_CONFIG_SERVER}/configs/custom/ydi/install-8.2-uefi-iso-ext.xml" # FIXME + +# Default shared ISO SR +ISOSR_SRV = "nfs-server" +ISOSR_PATH = "/srv/iso-sr" + +# Tools +TOOLS = { +# "iso-remaster": "/home/user/src/xcpng/xcp/scripts/iso-remaster/iso-remaster.sh", +} + # 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', 'mini-linux-x86_64-uefi': 'alpine-uefi-minimal-3.12.0.xva' } +# FIXME: use URLs and an optional cache? +ISO_IMAGES = { +# 'xcpng-8.3-beta2': {'path': "/home/user/iso/xcp-ng-8.3.0-beta2.iso"}, +# 'xcpng-8.3-beta2-net': {'path': "/home/user/iso/xcp-ng-8.3.0-beta2-netinstall.iso"}, +# 'xcpng-8.2.1-2023': {'path': "/home/user/iso/xcp-ng-8.2.1-20231130.iso"}, +# 'xcpng-8.2.1': {'path': "/home/user/iso/xcp-ng-8.2.1.iso"}, +# 'xcpng-8.2.0': {'path': "/home/user/iso/xcp-ng-8.2.0.iso"}, +} + # In some cases, we may prefer to favour a local SR to store test VM disks, # to avoid latency or unstabilities related to network or shared file servers. # However it's not good practice to make a local SR the default SR for a pool of several hosts. diff --git a/pytest.ini b/pytest.ini index 61aa3945..09bc027f 100644 --- a/pytest.ini +++ b/pytest.ini @@ -21,6 +21,9 @@ markers = # * VM-related markers to give parameters to fixtures vm_definitions: dicts of VM defs for create_vms fixture. + # * installation-related markers to customize installer run + installer_iso: key of data.ISO_IMAGES identifying an installer ISO + # * Test targets related to VMs small_vm: tests that it is enough to run just once, using the smallest possible VM. big_vm: tests that it would be good to run with a big VM. diff --git a/tests/install/conftest.py b/tests/install/conftest.py new file mode 100644 index 00000000..ac444bb8 --- /dev/null +++ b/tests/install/conftest.py @@ -0,0 +1,70 @@ +import logging +import os +import pytest +import tempfile + +from lib.common import callable_marker +from lib.commands import local_cmd, scp, ssh + +@pytest.fixture(scope='function') +def iso_remaster(request): + marker = request.node.get_closest_marker("installer_iso") + assert marker is not None, "iso_remaster fixture requires 'installer_iso' marker" + param_mapping = marker.kwargs.get("param_mapping", {}) + iso_key = callable_marker(marker.args[0], request, param_mapping=param_mapping) + + from data import ANSWERFILE_URL, ISO_IMAGES, ISOSR_SRV, ISOSR_PATH, TOOLS + assert "iso-remaster" in TOOLS + iso_remaster = TOOLS["iso-remaster"] + assert os.access(iso_remaster, os.X_OK) + + assert iso_key in ISO_IMAGES, f"ISO_IMAGES does not have a value for {iso_key}" + SOURCE_ISO = ISO_IMAGES[iso_key]['path'] + + with tempfile.TemporaryDirectory() as isotmp: + remastered_iso = os.path.join(isotmp, "image.iso") + iso_patcher_script = os.path.join(isotmp, "iso-patcher") + + logging.info("Remastering %s to %s", SOURCE_ISO, remastered_iso) + + # generate iso-patcher script + with open(iso_patcher_script, "xt") as patcher_fd: + passwd = "passw0rd" # FIXME use invalid hash + print(f"""#!/bin/bash +set -ex +ISODIR="$1" +SED_COMMANDS=(-e "s@/vmlinuz@/vmlinuz sshpassword={passwd} atexit=shell@") +SED_COMMANDS+=(-e "s@/vmlinuz@/vmlinuz install answerfile={ANSWERFILE_URL} network_device=all@") + +sed -i "${{SED_COMMANDS[@]}}" \ + "$ISODIR"/*/*/grub*.cfg \ + "$ISODIR"/boot/isolinux/isolinux.cfg +""", + file=patcher_fd) + os.chmod(patcher_fd.fileno(), 0o755) + + # do remaster + local_cmd([iso_remaster, + "--iso-patcher", iso_patcher_script, + SOURCE_ISO, remastered_iso + ]) + + # unique filename on server, has to work on FreeBSD-based NAS + # too, and even v14 has no tool allowing mktemp suffixes + remote_iso = ssh(ISOSR_SRV, + ["python3", "-c", + '"import os, tempfile; ' + f"f = tempfile.mkstemp(suffix='.iso', dir='{ISOSR_PATH}')[1];" + "os.chmod(f, 0o644);" + 'print(f);"' + ]) + logging.info("Uploading to ISO-SR server remastered %s as %s", + remastered_iso, os.path.basename(remote_iso)) + scp(ISOSR_SRV, remastered_iso, remote_iso) + # FIXME: is sr-scan ever needed? + + try: + yield os.path.basename(remote_iso) + finally: + logging.info("Removing %s from ISO-SR server", os.path.basename(remote_iso)) + ssh(ISOSR_SRV, ["rm", remote_iso]) diff --git a/tests/install/test.py b/tests/install/test.py index 94193ee8..874a96e0 100644 --- a/tests/install/test.py +++ b/tests/install/test.py @@ -24,6 +24,7 @@ class TestNested: cd_vbd=dict(device="xvdd", userdevice="3"), vifs=[dict(index=0, network_uuid=NETWORKS["MGMT"])], )) - def test_install(self, create_vms): + @pytest.mark.installer_iso("xcpng-8.2.1-2023") + def test_install(self, create_vms, iso_remaster): assert len(create_vms) == 1 - installer.perform_install(iso="xcp-ng-8.2.1-20231130.iso", host_vm=create_vms[0]) + installer.perform_install(iso=iso_remaster, host_vm=create_vms[0]) From c2874aaff0e3d96c5b112b7f370bff49d1a99f0e Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Thu, 25 Jul 2024 16:54:11 +0200 Subject: [PATCH 40/75] WIP install 3/n: run installer Currently relies on the installer fetching the answerfile from the PXE server, to ensure the latter's ARP tables are populated with the obtained DHCP address. FIXME: - encapsulate machine boot as fixture - specification of answerfile contents and dom0 cmdline is tightly linked, e.g. we rely on atexit=shell - repeatedly polling with grep is junk --- data.py-dist | 5 +++ lib/installer.py | 75 ++++++++++++++++++++++++++++++++++++++- tests/install/conftest.py | 26 +++++++++++++- 3 files changed, 104 insertions(+), 2 deletions(-) diff --git a/data.py-dist b/data.py-dist index a6d6d9ca..a5d5490b 100644 --- a/data.py-dist +++ b/data.py-dist @@ -6,6 +6,11 @@ HOST_DEFAULT_USER = "root" HOST_DEFAULT_PASSWORD = "" +# Public key for a private key available to the test runner +TEST_SSH_PUBKEY = """ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMnN/wVdQqHA8KsndfrLS7fktH/IEgxoa533efuXR6rw XCP-ng CI +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDKz9uQOoxq6Q0SQ0XTzQHhDolvuo/7EyrDZsYQbRELhcPJG8MT/o5u3HyJFhIP2+HqBSXXgmqRPJUkwz9wUwb2sUwf44qZm/pyPUWOoxyVtrDXzokU/uiaNKUMhbnfaXMz6Ogovtjua63qld2+ZRXnIgrVtYKtYBeu/qKGVSnf4FTOUKl1w3uKkr59IUwwAO8ay3wVnxXIHI/iJgq6JBgQNHbn3C/SpYU++nqL9G7dMyqGD36QPFuqH/cayL8TjNZ67TgAzsPX8OvmRSqjrv3KFbeSlpS/R4enHkSemhgfc8Z2f49tE7qxWZ6x4Uyp5E6ur37FsRf/tEtKIUJGMRXN XCP-ng CI +""" + # The following prefix will be added to the `name-label` parameter of XAPI objects # that the tests will create or import, such as VMs and SRs. # Default value: [your login/user] diff --git a/lib/installer.py b/lib/installer.py index e37f0ba0..3bbce7ee 100644 --- a/lib/installer.py +++ b/lib/installer.py @@ -1,4 +1,77 @@ +import logging +from lib import commands, pxe +from lib.commands import ssh +from lib.common import wait_for + +def poweroff(ip): + try: + ssh(ip, ["poweroff"]) + except commands.SSHCommandFailed as e: + # ignore connection closed by reboot + if e.returncode == 255 and "closed by remote host" in e.stdout: + logging.info("sshd closed the connection") + pass + else: + raise + +def monitor_install(*, ip): + # wait for "yum install" phase to finish + wait_for(lambda: ssh(ip, ["grep", + "'DISPATCH: NEW PHASE: Completing installation'", + "/tmp/install-log"], + check=False, simple_output=False, + ).returncode == 0, + "Wait for rpm installation to succeed", + timeout_secs=40*60) # FIXME too big + + # wait for install to finish + wait_for(lambda: ssh(ip, ["grep", + "'The installation completed successfully'", + "/tmp/install-log"], + check=False, simple_output=False, + ).returncode == 0, + "Wait for system installation to succeed", + timeout_secs=40*60) # FIXME too big + + wait_for(lambda: ssh(ip, ["ps a|grep '[0-9]. python /opt/xensource/installer/init'"], + check=False, simple_output=False, + ).returncode == 1, + "Wait for installer to terminate") + def perform_install(*, iso, host_vm): + vif = host_vm.vifs()[0] + mac_address = vif.param_get('MAC') + logging.info("Host VM has MAC %s", mac_address) + host_vm.insert_cd(iso) - host_vm.eject_cd() + try: + host_vm.start() + wait_for(host_vm.is_running, "Wait for host VM running") + + # catch host-vm IP address + wait_for(lambda: pxe.arp_addresses_for(mac_address), + "Wait for DHCP server to see Host VM in ARP tables", + timeout_secs=10*60) + ips = pxe.arp_addresses_for(mac_address) + logging.info("Host VM has IPs %s", ips) + assert len(ips) == 1 + host_vm.ip = ips[0] + + monitor_install(ip=host_vm.ip) + + logging.info("Shutting down Host VM after successful installation") + poweroff(host_vm.ip) + wait_for(host_vm.is_halted, "Wait for host VM halted") + host_vm.eject_cd() + + except Exception as e: + logging.critical("caught exception %s", e) + # wait_for(lambda: False, 'Wait "forever"', timeout_secs=100*60) + host_vm.shutdown(force=True) + raise + except KeyboardInterrupt: + logging.warning("keyboard interrupt") + # wait_for(lambda: False, 'Wait "forever"', timeout_secs=100*60) + host_vm.shutdown(force=True) + raise diff --git a/tests/install/conftest.py b/tests/install/conftest.py index ac444bb8..b7c3f67e 100644 --- a/tests/install/conftest.py +++ b/tests/install/conftest.py @@ -13,7 +13,7 @@ def iso_remaster(request): param_mapping = marker.kwargs.get("param_mapping", {}) iso_key = callable_marker(marker.args[0], request, param_mapping=param_mapping) - from data import ANSWERFILE_URL, ISO_IMAGES, ISOSR_SRV, ISOSR_PATH, TOOLS + from data import ANSWERFILE_URL, ISO_IMAGES, ISOSR_SRV, ISOSR_PATH, TEST_SSH_PUBKEY, TOOLS assert "iso-remaster" in TOOLS iso_remaster = TOOLS["iso-remaster"] assert os.access(iso_remaster, os.X_OK) @@ -23,10 +23,33 @@ def iso_remaster(request): with tempfile.TemporaryDirectory() as isotmp: remastered_iso = os.path.join(isotmp, "image.iso") + img_patcher_script = os.path.join(isotmp, "img-patcher") iso_patcher_script = os.path.join(isotmp, "iso-patcher") logging.info("Remastering %s to %s", SOURCE_ISO, remastered_iso) + # generate install.img-patcher script + with open(img_patcher_script, "xt") as patcher_fd: + print(f"""#!/bin/bash +set -ex +INSTALLIMG="$1" + +mkdir -p "$INSTALLIMG/root/.ssh" +echo "{TEST_SSH_PUBKEY}" > "$INSTALLIMG/root/.ssh/authorized_keys" + +cat > "$INSTALLIMG/root/postinstall.sh" <> "\\$ROOT/root/.ssh/authorized_keys" +EOF +""", + file=patcher_fd) + os.chmod(patcher_fd.fileno(), 0o755) + # generate iso-patcher script with open(iso_patcher_script, "xt") as patcher_fd: passwd = "passw0rd" # FIXME use invalid hash @@ -45,6 +68,7 @@ def iso_remaster(request): # do remaster local_cmd([iso_remaster, + "--install-patcher", img_patcher_script, "--iso-patcher", iso_patcher_script, SOURCE_ISO, remastered_iso ]) From d4d09dc823d80e721d7b0d1dc84b807b5adb4fef Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Tue, 25 Jun 2024 13:04:16 +0200 Subject: [PATCH 41/75] install 4/n: make sure installer host appears in PXE ARP tables Detection of host IP till now relies on the fact we download the answerfile from PXE server. Once we take this file from the ISO this network traffic won't happen so we need some other mechanism to fill the server's ARP tables. test-pingpxe.service is installed in install.img by iso-remaster. Since it is difficult to wait until the IP has been assigned before launching the service, make it ping continuously until we can reach the PXE server. One the installed host we will set a static IP instead. Signed-off-by: Yann Dirson --- tests/install/conftest.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/tests/install/conftest.py b/tests/install/conftest.py index b7c3f67e..20ca1a89 100644 --- a/tests/install/conftest.py +++ b/tests/install/conftest.py @@ -13,7 +13,7 @@ def iso_remaster(request): param_mapping = marker.kwargs.get("param_mapping", {}) iso_key = callable_marker(marker.args[0], request, param_mapping=param_mapping) - from data import ANSWERFILE_URL, ISO_IMAGES, ISOSR_SRV, ISOSR_PATH, TEST_SSH_PUBKEY, TOOLS + from data import ANSWERFILE_URL, ISO_IMAGES, ISOSR_SRV, ISOSR_PATH, PXE_CONFIG_SERVER, TEST_SSH_PUBKEY, TOOLS assert "iso-remaster" in TOOLS iso_remaster = TOOLS["iso-remaster"] assert os.access(iso_remaster, os.X_OK) @@ -37,6 +37,28 @@ def iso_remaster(request): mkdir -p "$INSTALLIMG/root/.ssh" echo "{TEST_SSH_PUBKEY}" > "$INSTALLIMG/root/.ssh/authorized_keys" +cat > "$INSTALLIMG/usr/local/sbin/test-pingpxe.sh" << 'EOF' +#! /bin/bash +set -eE +set -o pipefail + +ping -c1 "$1" +EOF +chmod +x "$INSTALLIMG/usr/local/sbin/test-pingpxe.sh" + +cat > "$INSTALLIMG/etc/systemd/system/test-pingpxe.service" < "$INSTALLIMG/root/postinstall.sh" < Date: Fri, 2 Aug 2024 14:18:50 +0200 Subject: [PATCH 42/75] Install test-pingpxe service on host This is necessary to get rid of old ARP cache entries that would match our IP to the MAC used by the VM clone in a previous test. Signed-off-by: Yann Dirson --- tests/install/conftest.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/install/conftest.py b/tests/install/conftest.py index 20ca1a89..36eaf71d 100644 --- a/tests/install/conftest.py +++ b/tests/install/conftest.py @@ -42,6 +42,19 @@ def iso_remaster(request): set -eE set -o pipefail +ether_of () {{ + ifconfig "$1" | grep ether | sed 's/.*ether \\([^ ]*\\).*/\\1/' +}} + +# on installed system, avoid xapi-project/xen-api#5799 +if ! [ -e /opt/xensource/installer ]; then + eth_mac=$(ether_of eth0) + br_mac=$(ether_of xenbr0) + + # wait for bridge MAC to be fixed + test "$eth_mac" = "$br_mac" +fi + ping -c1 "$1" EOF chmod +x "$INSTALLIMG/usr/local/sbin/test-pingpxe.sh" @@ -65,6 +78,10 @@ def iso_remaster(request): ROOT="\\$1" +cp /etc/systemd/system/test-pingpxe.service "\\$ROOT/etc/systemd/system/test-pingpxe.service" +cp /usr/local/sbin/test-pingpxe.sh "\\$ROOT/usr/local/sbin/test-pingpxe.sh" +systemctl --root="\\$ROOT" enable test-pingpxe.service + mkdir -p "\\$ROOT/root/.ssh" echo "{TEST_SSH_PUBKEY}" >> "\\$ROOT/root/.ssh/authorized_keys" EOF From eddb2d7e8485eb625a08eb98e311385c79b346df Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Mon, 29 Jul 2024 10:55:55 +0200 Subject: [PATCH 43/75] WIP install 5/n: answerfile generation This process has several steps: - building of a data structure holding all of the answerfile data, from a customizable base in data.py and from tests-specific items - serialization as XML to be read by host-installer - necessary changes to the ISO for host-installer to use it We now have to explicitly enable the network during access (was implied by the use of a remote answerfile). Similarly we now rely on the test-pingpxe service, as nothing else would otherwise populate the server's ARP table. This is needed so: - different tests can use different parameters without the need for provisionning every answerfile to be used - tests can dynamically add contents for their own needs, before the XML gets actualy written FIXME: - doc --- data.py-dist | 30 ++++++++++++++++-- lib/installer.py | 58 ++++++++++++++++++++++++++++++++++ pytest.ini | 1 + tests/install/conftest.py | 53 ++++++++++++++++++++++++++++--- tests/install/test.py | 6 ++++ tests/install/test_fixtures.py | 20 ++++++++++++ 6 files changed, 162 insertions(+), 6 deletions(-) create mode 100644 tests/install/test_fixtures.py diff --git a/data.py-dist b/data.py-dist index a5d5490b..9f582531 100644 --- a/data.py-dist +++ b/data.py-dist @@ -5,6 +5,7 @@ # You need to have an SSH key into the hosts' /root/.ssh/authorized_keys. HOST_DEFAULT_USER = "root" HOST_DEFAULT_PASSWORD = "" +HOST_DEFAULT_PASSWORD_HASH = "" # FIXME # Public key for a private key available to the test runner TEST_SSH_PUBKEY = """ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMnN/wVdQqHA8KsndfrLS7fktH/IEgxoa533efuXR6rw XCP-ng CI @@ -35,12 +36,22 @@ PXE_CONFIG_SERVER = 'pxe' # Default VM images location DEF_VM_URL = 'http://pxe/images/' -ANSWERFILE_URL = f"http://{PXE_CONFIG_SERVER}/configs/custom/ydi/install-8.2-uefi-iso-ext.xml" # FIXME - # Default shared ISO SR ISOSR_SRV = "nfs-server" ISOSR_PATH = "/srv/iso-sr" +# IP addresses for hosts to be installed +# NOTE: do NOT set an IP for host1, it is assumed to use DEFAULT +HOSTS_IP_CONFIG = { + 'HOSTS': { +# 'DEFAULT': '192.16.0.1', +# 'host2': '192.16.0.2', + }, +# 'NETMASK': '255.255.0.0', +# 'GATEWAY': '192.16.0.254', +# 'DNS': '192.16.0.254', +} + # Tools TOOLS = { # "iso-remaster": "/home/user/src/xcpng/xcp/scripts/iso-remaster/iso-remaster.sh", @@ -127,6 +138,21 @@ LVMOISCSI_DEVICE_CONFIG = { # 'SCSIid': 'id' } +BASE_ANSWERFILES = dict( + INSTALL={ + "TAG": "installation", + "CONTENTS": ( + {"TAG": "root-password", + "type": "hash", + "CONTENTS": HOST_DEFAULT_PASSWORD_HASH}, + {"TAG": "timezone", + "CONTENTS": "Europe/Paris"}, + {"TAG": "keymap", + "CONTENTS": "us"}, + ), + }, +) + # compatibility settings for older tests DEFAULT_NFS_DEVICE_CONFIG = NFS_DEVICE_CONFIG DEFAULT_NFS4_DEVICE_CONFIG = NFS4_DEVICE_CONFIG diff --git a/lib/installer.py b/lib/installer.py index 3bbce7ee..d33ba889 100644 --- a/lib/installer.py +++ b/lib/installer.py @@ -1,8 +1,66 @@ import logging +import xml.etree.ElementTree as ET + from lib import commands, pxe from lib.commands import ssh from lib.common import wait_for +class AnswerFile: + def __init__(self, kind, /): + from data import BASE_ANSWERFILES + defn = BASE_ANSWERFILES[kind] + self.defn = self._normalize_structure(defn) + + def write_xml(self, filename): + logging.info("generating answerfile %s", filename) + etree = ET.ElementTree(self._defn_to_xml_et(self.defn)) + etree.write(filename) + + # chainable mutators for lambdas + + def top_append(self, *defs): + for defn in defs: + self.defn['CONTENTS'].append(self._normalize_structure(defn)) + return self + + def top_setattr(self, attrs): + assert 'CONTENTS' not in attrs + self.defn.update(attrs) + return self + + # makes a mutable deep copy of all `contents` + @staticmethod + def _normalize_structure(defn): + assert isinstance(defn, dict) + assert 'TAG' in defn + defn = dict(defn) + if 'CONTENTS' not in defn: + defn['CONTENTS'] = [] + if not isinstance(defn['CONTENTS'], str): + defn['CONTENTS'] = [AnswerFile._normalize_structure(item) + for item in defn['CONTENTS']] + return defn + + # convert to a ElementTree.Element tree suitable for further + # modification before we serialize it to XML + @staticmethod + def _defn_to_xml_et(defn, /, *, parent=None): + assert isinstance(defn, dict) + defn = dict(defn) + name = defn.pop('TAG') + assert isinstance(name, str) + contents = defn.pop('CONTENTS', ()) + assert isinstance(contents, (str, list)) + element = ET.Element(name, **defn) + if parent is not None: + parent.append(element) + if isinstance(contents, str): + element.text = contents + else: + for contents in contents: + AnswerFile._defn_to_xml_et(contents, parent=element) + return element + def poweroff(ip): try: ssh(ip, ["poweroff"]) diff --git a/pytest.ini b/pytest.ini index 09bc027f..2e73d975 100644 --- a/pytest.ini +++ b/pytest.ini @@ -22,6 +22,7 @@ markers = vm_definitions: dicts of VM defs for create_vms fixture. # * installation-related markers to customize installer run + answerfile: dict defining an answerfile installer_iso: key of data.ISO_IMAGES identifying an installer ISO # * Test targets related to VMs diff --git a/tests/install/conftest.py b/tests/install/conftest.py index 36eaf71d..a695827f 100644 --- a/tests/install/conftest.py +++ b/tests/install/conftest.py @@ -1,19 +1,51 @@ +from copy import deepcopy import logging import os import pytest import tempfile +import xml.etree.ElementTree as ET +from lib.installer import AnswerFile from lib.common import callable_marker from lib.commands import local_cmd, scp, ssh @pytest.fixture(scope='function') -def iso_remaster(request): +def answerfile(request): + marker = request.node.get_closest_marker("answerfile") + + if marker is None: + yield None # no answerfile to generate + return + + # construct answerfile definition from option "base", and explicit bits + param_mapping = marker.kwargs.get("param_mapping", {}) + answerfile_def = callable_marker(marker.args[0], request, param_mapping=param_mapping) + assert isinstance(answerfile_def, AnswerFile) + + from data import HOSTS_IP_CONFIG + answerfile_def.top_append( + dict(TAG="admin-interface", + name="eth0", + proto="static", + CONTENTS=( + dict(TAG='ipaddr', CONTENTS=HOSTS_IP_CONFIG['HOSTS']['DEFAULT']), + dict(TAG='subnet', CONTENTS=HOSTS_IP_CONFIG['NETMASK']), + dict(TAG='gateway', CONTENTS=HOSTS_IP_CONFIG['GATEWAY']), + )), + dict(TAG="name-server", + CONTENTS=HOSTS_IP_CONFIG['DNS']), + ) + + yield answerfile_def + +@pytest.fixture(scope='function') +def iso_remaster(request, answerfile): marker = request.node.get_closest_marker("installer_iso") assert marker is not None, "iso_remaster fixture requires 'installer_iso' marker" param_mapping = marker.kwargs.get("param_mapping", {}) iso_key = callable_marker(marker.args[0], request, param_mapping=param_mapping) - from data import ANSWERFILE_URL, ISO_IMAGES, ISOSR_SRV, ISOSR_PATH, PXE_CONFIG_SERVER, TEST_SSH_PUBKEY, TOOLS + from data import ISO_IMAGES, ISOSR_SRV, ISOSR_PATH, PXE_CONFIG_SERVER, TEST_SSH_PUBKEY, TOOLS assert "iso-remaster" in TOOLS iso_remaster = TOOLS["iso-remaster"] assert os.access(iso_remaster, os.X_OK) @@ -25,6 +57,15 @@ def iso_remaster(request): remastered_iso = os.path.join(isotmp, "image.iso") img_patcher_script = os.path.join(isotmp, "img-patcher") iso_patcher_script = os.path.join(isotmp, "iso-patcher") + answerfile_xml = os.path.join(isotmp, "answerfile.xml") + + if answerfile: + logging.info("generating answerfile %s", answerfile_xml) + answerfile.top_append(dict(TAG="script", stage="filesystem-populated", + type="url", CONTENTS="file:///root/postinstall.sh")) + answerfile.write_xml(answerfile_xml) + else: + logging.info("no answerfile") logging.info("Remastering %s to %s", SOURCE_ISO, remastered_iso) @@ -37,6 +78,9 @@ def iso_remaster(request): mkdir -p "$INSTALLIMG/root/.ssh" echo "{TEST_SSH_PUBKEY}" > "$INSTALLIMG/root/.ssh/authorized_keys" +test ! -e "{answerfile_xml}" || + cp "{answerfile_xml}" "$INSTALLIMG/root/answerfile.xml" + cat > "$INSTALLIMG/usr/local/sbin/test-pingpxe.sh" << 'EOF' #! /bin/bash set -eE @@ -95,8 +139,9 @@ def iso_remaster(request): print(f"""#!/bin/bash set -ex ISODIR="$1" -SED_COMMANDS=(-e "s@/vmlinuz@/vmlinuz sshpassword={passwd} atexit=shell@") -SED_COMMANDS+=(-e "s@/vmlinuz@/vmlinuz install answerfile={ANSWERFILE_URL} network_device=all@") +SED_COMMANDS=(-e "s@/vmlinuz@/vmlinuz sshpassword={passwd} atexit=shell network_device=all@") +test ! -e "{answerfile_xml}" || + SED_COMMANDS+=(-e "s@/vmlinuz@/vmlinuz install answerfile=file:///root/answerfile.xml@") sed -i "${{SED_COMMANDS[@]}}" \ "$ISODIR"/*/*/grub*.cfg \ diff --git a/tests/install/test.py b/tests/install/test.py index 874a96e0..cb5d8ca9 100644 --- a/tests/install/test.py +++ b/tests/install/test.py @@ -2,6 +2,7 @@ import pytest from lib import installer +from lib.installer import AnswerFile from data import NETWORKS assert "MGMT" in NETWORKS @@ -24,6 +25,11 @@ class TestNested: cd_vbd=dict(device="xvdd", userdevice="3"), vifs=[dict(index=0, network_uuid=NETWORKS["MGMT"])], )) + @pytest.mark.answerfile(lambda: AnswerFile("INSTALL") \ + .top_append( + {"TAG": "source", "type": "local"}, + {"TAG": "primary-disk", "CONTENTS": "nvme0n1"}, + )) @pytest.mark.installer_iso("xcpng-8.2.1-2023") def test_install(self, create_vms, iso_remaster): assert len(create_vms) == 1 diff --git a/tests/install/test_fixtures.py b/tests/install/test_fixtures.py new file mode 100644 index 00000000..486ba9cc --- /dev/null +++ b/tests/install/test_fixtures.py @@ -0,0 +1,20 @@ +import logging +import pytest + +from lib.installer import AnswerFile + +# test the answerfile fixture can run on 2 parametrized instances +# of the test in one run +@pytest.mark.answerfile(lambda: AnswerFile("INSTALL").top_append( + {"TAG": "source", "type": "local"}, + {"TAG": "primary-disk", "text": "nvme0n1"}, +)) +@pytest.mark.parametrize("parm", [ + 1, + pytest.param(2, marks=[ + pytest.mark.dependency(depends=["TestFixtures::test_parametrized_answerfile[1]"]), + ]), +]) +@pytest.mark.dependency +def test_parametrized_answerfile(answerfile, parm): + logging.debug("test_parametrized_answerfile with parm=%s", parm) From 04f798c857e90fe535f7ce0927a0ed05d1512104 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Tue, 25 Jun 2024 09:49:13 +0200 Subject: [PATCH 44/75] WIP install 6/n: export to XVA after install FIXME: - settle for a location to save XVA? --- conftest.py | 41 ++++++++++++++++++++++++++++++++++++----- lib/common.py | 27 +++++++++++++++++++++++++++ lib/vm.py | 11 +++++++++++ requirements/base.txt | 1 + tests/install/test.py | 1 + 5 files changed, 76 insertions(+), 5 deletions(-) diff --git a/conftest.py b/conftest.py index ba286700..5f9f59bc 100644 --- a/conftest.py +++ b/conftest.py @@ -7,13 +7,13 @@ import lib.config as global_config -from lib.common import callable_marker +from lib.common import callable_marker, shortened_nodeid from lib.common import wait_for, vm_image, is_uuid from lib.common import setup_formatted_and_mounted_disk, teardown_formatted_and_mounted_disk from lib.netutil import is_ipv6 from lib.pool import Pool from lib.sr import SR -from lib.vm import VM +from lib.vm import VM, xva_name_from_def from lib.xo import xo_cli # Import package-scoped fixtures. Although we need to define them in a separate file so that we can @@ -412,6 +412,13 @@ def create_vms(request, host): > def test_foo(create_vms): > ... + Example: + ------- + > @pytest.mark.dependency(depends=["test_foo"]) + > @pytest.mark.vm_definitions(dict(name="vm1", image_test="test_foo", image_vm="vm2")) + > def test_bar(create_vms): + > ... + """ marker = request.node.get_closest_marker("vm_definitions") if marker is None: @@ -422,8 +429,10 @@ def create_vms(request, host): for vm_def in marker.args: vm_def = callable_marker(vm_def, request, param_mapping=param_mapping) assert "name" in vm_def - assert "template" in vm_def - # FIXME should check optional vdis contents + assert "template" in vm_def or "image_test" in vm_def + if "template" in vm_def: + assert "image_test" not in vm_def + # FIXME should check optional vdis contents # FIXME should check for extra args vm_defs.append(vm_def) @@ -432,9 +441,26 @@ def create_vms(request, host): vdis = [] vbds = [] for vm_def in vm_defs: - _create_vm(vm_def, host, vms, vdis, vbds) + if "template" in vm_def: + _create_vm(vm_def, host, vms, vdis, vbds) + elif "image_test" in vm_def: + _import_vm(request, vm_def, host, vms) yield vms + # request.node is an "item" because this fixture has "function" scope + report = request.node.stash[PHASE_REPORT_KEY] + if report["setup"].failed: + logging.warning("setting up a test failed or skipped: not exporting VMs") + elif ("call" not in report) or report["call"].failed: + logging.warning("executing test failed or skipped: not exporting VMs") + else: + # record this state + for vm_def, vm in zip(vm_defs, vms): + # FIXME where to store? + xva_name = f"{shortened_nodeid(request.node.nodeid)}-{vm_def['name']}.xva" + host.ssh(["rm -f", xva_name]) + vm.export(xva_name, "zstd") + except Exception: logging.error("exception caught...") raise @@ -483,6 +509,11 @@ def _create_vm(vm_def, host, vms, vdis, vbds): logging.info("Setting param %s", param_def) vm.param_set(**param_def) +def _import_vm(request, vm_def, host, vms): + vm_image = xva_name_from_def(vm_def, request.node.nodeid) + vm = host.import_vm(vm_image) + vms.append(vm) + @pytest.fixture(scope="module") def started_vm(imported_vm): vm = imported_vm diff --git a/lib/common.py b/lib/common.py index 6d2f1acb..52d6a893 100644 --- a/lib/common.py +++ b/lib/common.py @@ -1,5 +1,6 @@ import getpass import inspect +import itertools import logging import sys import time @@ -32,6 +33,32 @@ def prefix_object_name(label): name_prefix = f"[{getpass.getuser()}]" return f"{name_prefix} {label}" +def shortened_nodeid(nodeid): + components = nodeid.split("::") + # module + components[0] = strip_prefix(components[0], "tests/") + components[0] = strip_suffix(components[0], ".py") + components[0] = components[0].replace("/", ".") + # function + components[-1] = strip_prefix(components[-1], "test_") + # class + if len(components) > 2: + components[1] = strip_prefix(components[1], "Test") + + return "::".join(components) + +def expand_scope_relative_nodeid(scoped_nodeid, scope, ref_nodeid): + if scope == 'session' or scope == 'package': + base = () + elif scope == 'module': + base = ref_nodeid.split("::", 1)[:1] + elif scope == 'class': + base = ref_nodeid.split("::", 2)[:2] + else: + raise RuntimeError(f"Internal error: invalid scope {scope!r}") + logging.debug("scope: %r base: %r relative: %r", scope, base, scoped_nodeid) + return "::".join(itertools.chain(base, (scoped_nodeid,))) + def callable_marker(value, request, param_mapping): """Process value optionally generated by fixture-dependent callable. diff --git a/lib/vm.py b/lib/vm.py index 6dcf6894..17753f51 100644 --- a/lib/vm.py +++ b/lib/vm.py @@ -8,6 +8,7 @@ from lib.basevm import BaseVM from lib.common import PackageManagerEnum, parse_xe_dict, safe_split, wait_for, wait_for_not +from lib.common import shortened_nodeid, expand_scope_relative_nodeid from lib.snapshot import Snapshot from lib.vbd import VBD from lib.vif import VIF @@ -618,3 +619,13 @@ 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 xva_name_from_def(vm_def, ref_nodeid): + vm_name = vm_def["name"] + image_test = vm_def["image_test"] + image_vm = vm_def.get("image_vm", vm_name) + image_scope = vm_def.get("image_scope", "module") + return "{}-{}.xva".format(shortened_nodeid( + expand_scope_relative_nodeid(image_test, image_scope, ref_nodeid)), + image_vm) diff --git a/requirements/base.txt b/requirements/base.txt index 3df8b072..1cd12156 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -2,3 +2,4 @@ cryptography>=3.3.1 packaging>=20.7 pytest>=8.0.0 pluggy>=1.1.0 +pytest-dependency diff --git a/tests/install/test.py b/tests/install/test.py index cb5d8ca9..4f97a686 100644 --- a/tests/install/test.py +++ b/tests/install/test.py @@ -7,6 +7,7 @@ from data import NETWORKS assert "MGMT" in NETWORKS +@pytest.mark.dependency() class TestNested: @pytest.mark.vm_definitions( dict(name="vm1", From 1443ff5a9332b4df0a0fe948ff8d66f5da46adb8 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Thu, 25 Jul 2024 17:25:43 +0200 Subject: [PATCH 45/75] WIP install 7/n: add firstboot test FIXME - setup firstboot as an autouse fixture (or not, given the messy consequences exposed in https://github.com/pytest-dev/pytest/discussions/12541) - encapsulate machine boot as fixture - Pool construction on firstboot is problematic (causing flaky test) --- tests/install/test.py | 98 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 97 insertions(+), 1 deletion(-) diff --git a/tests/install/test.py b/tests/install/test.py index 4f97a686..146613a9 100644 --- a/tests/install/test.py +++ b/tests/install/test.py @@ -1,8 +1,11 @@ import logging import pytest +import time -from lib import installer +from lib import commands, installer, pxe +from lib.common import wait_for from lib.installer import AnswerFile +from lib.pool import Pool from data import NETWORKS assert "MGMT" in NETWORKS @@ -35,3 +38,96 @@ class TestNested: def test_install(self, create_vms, iso_remaster): assert len(create_vms) == 1 installer.perform_install(iso=iso_remaster, host_vm=create_vms[0]) + + + @pytest.mark.dependency(depends=["TestNested::test_install"]) + @pytest.mark.vm_definitions( + dict(name="vm1", image_test="TestNested::test_install")) + def test_firstboot(self, create_vms): + host_vm = create_vms[0] + vif = host_vm.vifs()[0] + mac_address = vif.param_get('MAC') + logging.info("Host VM has MAC %s", mac_address) + + try: + # FIXME: evict MAC from ARP cache first? + host_vm.start() + wait_for(host_vm.is_running, "Wait for host VM running") + + # catch host-vm IP address + wait_for(lambda: pxe.arp_addresses_for(mac_address), + "Wait for DHCP server to see Host VM in ARP tables", + timeout_secs=10*60) + ips = pxe.arp_addresses_for(mac_address) + logging.info("Host VM has IPs %s", ips) + assert len(ips) == 1 + host_vm.ip = ips[0] + + wait_for( + lambda: commands.local_cmd( + ["nc", "-zw5", host_vm.ip, "22"], check=False).returncode == 0, + "Wait for ssh back up on Host VM", retry_delay_secs=5, timeout_secs=4 * 60) + + # wait for XAPI startup to be done, which avoids: + # - waiting for XAPI to start listening to its socket + # - waiting for host and pool objects to be populated after install + wait_for(lambda: commands.ssh(host_vm.ip, ['xapi-wait-init-complete', '60'], + check=False, simple_output=False).returncode == 0, + "Wait for XAPI init to be complete", + timeout_secs=30 * 60) + # FIXME: after this all wait_for should be instant - replace with immediate tests? + + # pool master must be reachable here + pool = Pool(host_vm.ip) + + # wait for XAPI + wait_for(pool.master.is_enabled, "Wait for XAPI to be ready", timeout_secs=30 * 60) + + # check for firstboot issues + # FIXME: flaky, must check logs extraction on failure + for service in ["control-domain-params-init", + "network-init", + "storage-init", + "generate-iscsi-iqn", + "create-guest-templates", + ]: + try: + wait_for(lambda: pool.master.ssh(["test", "-e", f"/var/lib/misc/ran-{service}"], + check=False, simple_output=False, + ).returncode == 0, + f"Wait for ran-{service} stamp") + except TimeoutError: + logging.warning("investigating lack of %s service stamp", service) + out = pool.master.ssh(["systemctl", "status", service], check=False) + logging.warning("service status: %s", out) + out = pool.master.ssh(["grep", "-r", service, "/var/log"], check=False) + logging.warning("in logs: %s", out) + raise + + #wait_for(lambda: False, 'Wait "forever"', timeout_secs=100*60) + logging.info("Powering off pool master") + try: + # use "poweroff" because "reboot" would cause ARP and + # SSH to be checked before host is down, and require + # ssh retries + pool.master.ssh(["poweroff"]) + except commands.SSHCommandFailed as e: + # ignore connection closed by reboot + if e.returncode == 255 and "closed by remote host" in e.stdout: + logging.info("sshd closed the connection") + pass + else: + raise + + wait_for(host_vm.is_halted, "Wait for host VM halted") + + except Exception as e: + logging.critical("caught exception %s", e) + # wait_for(lambda: False, 'Wait "forever"', timeout_secs=100*60) + host_vm.shutdown(force=True) + raise + except KeyboardInterrupt: + logging.warning("keyboard interrupt") + # wait_for(lambda: False, 'Wait "forever"', timeout_secs=100*60) + host_vm.shutdown(force=True) + raise From f643c2755ed750164b2b368cbd34894daac47422 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Mon, 24 Jun 2024 13:50:36 +0200 Subject: [PATCH 46/75] VM caching on export/import --- conftest.py | 17 +++++++++++++---- lib/basevm.py | 23 +++++++++++++++-------- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/conftest.py b/conftest.py index 5f9f59bc..9ccbfb1b 100644 --- a/conftest.py +++ b/conftest.py @@ -444,7 +444,7 @@ def create_vms(request, host): if "template" in vm_def: _create_vm(vm_def, host, vms, vdis, vbds) elif "image_test" in vm_def: - _import_vm(request, vm_def, host, vms) + _import_vm(request, vm_def, host, vms, use_cache=CACHE_IMPORTED_VM) yield vms # request.node is an "item" because this fixture has "function" scope @@ -459,7 +459,7 @@ def create_vms(request, host): # FIXME where to store? xva_name = f"{shortened_nodeid(request.node.nodeid)}-{vm_def['name']}.xva" host.ssh(["rm -f", xva_name]) - vm.export(xva_name, "zstd") + vm.export(xva_name, "zstd", use_cache=CACHE_IMPORTED_VM) except Exception: logging.error("exception caught...") @@ -509,9 +509,18 @@ def _create_vm(vm_def, host, vms, vdis, vbds): logging.info("Setting param %s", param_def) vm.param_set(**param_def) -def _import_vm(request, vm_def, host, vms): +def _import_vm(request, vm_def, host, vms, *, use_cache): vm_image = xva_name_from_def(vm_def, request.node.nodeid) - vm = host.import_vm(vm_image) + base_vm = host.import_vm(vm_image, sr_uuid=host.main_sr_uuid(), use_cache=use_cache) + + if use_cache: + # Clone the VM before running tests, so that the original VM remains untouched + logging.info(">> Clone cached VM before running tests") + vm = base_vm.clone() + # Remove the description, which may contain a cache identifier + vm.param_set('name-description', "") + else: + vm = base_vm vms.append(vm) @pytest.fixture(scope="module") diff --git a/lib/basevm.py b/lib/basevm.py index ea1a9d4d..dcd7ae8a 100644 --- a/lib/basevm.py +++ b/lib/basevm.py @@ -83,11 +83,18 @@ def get_sr(self): assert sr.attached_to_host(self.host) return sr - def export(self, filepath, compress='none'): - logging.info("Export VM %s to %s with compress=%s" % (self.uuid, filepath, compress)) - params = { - 'uuid': self.uuid, - 'compress': compress, - 'filename': filepath - } - self.host.xe('vm-export', params) + def export(self, filepath, compress='none', use_cache=False): + + if use_cache: + logging.info("Export VM %s to cache for %r as a clone" % (self.uuid, filepath)) + clone = self.clone() + logging.info(f"Marking VM {clone.uuid} as cached") + clone.param_set('name-description', self.host.vm_cache_key(filepath)) + else: + logging.info("Export VM %s to %s with compress=%s" % (self.uuid, filepath, compress)) + params = { + 'uuid': self.uuid, + 'compress': compress, + 'filename': filepath + } + self.host.xe('vm-export', params) From ac206aac1151e9aa7d4ff7920234187b8e1eaf33 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Wed, 24 Jul 2024 12:03:50 +0200 Subject: [PATCH 47/75] WIP Image caching: include commit hash in caching key This protects against using results from incompatible test by mistake. FIXME: get hash / check dirty at startup only Signed-off-by: Yann Dirson --- conftest.py | 13 +++++++++---- lib/vm.py | 9 +++++---- requirements/base.txt | 1 + 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/conftest.py b/conftest.py index 9ccbfb1b..607b59a5 100644 --- a/conftest.py +++ b/conftest.py @@ -420,6 +420,10 @@ def create_vms(request, host): > ... """ + import git + test_repo = git.Repo(".") + assert not test_repo.is_dirty(), "test repo must not be dirty to cache images" + marker = request.node.get_closest_marker("vm_definitions") if marker is None: raise Exception("No vm_definitions marker specified.") @@ -444,7 +448,7 @@ def create_vms(request, host): if "template" in vm_def: _create_vm(vm_def, host, vms, vdis, vbds) elif "image_test" in vm_def: - _import_vm(request, vm_def, host, vms, use_cache=CACHE_IMPORTED_VM) + _import_vm(request, vm_def, host, vms, test_repo, use_cache=CACHE_IMPORTED_VM) yield vms # request.node is an "item" because this fixture has "function" scope @@ -457,7 +461,8 @@ def create_vms(request, host): # record this state for vm_def, vm in zip(vm_defs, vms): # FIXME where to store? - xva_name = f"{shortened_nodeid(request.node.nodeid)}-{vm_def['name']}.xva" + gitref = test_repo.head.commit.hexsha + xva_name = f"{shortened_nodeid(request.node.nodeid)}-{vm_def['name']}-{gitref}.xva" host.ssh(["rm -f", xva_name]) vm.export(xva_name, "zstd", use_cache=CACHE_IMPORTED_VM) @@ -509,8 +514,8 @@ def _create_vm(vm_def, host, vms, vdis, vbds): logging.info("Setting param %s", param_def) vm.param_set(**param_def) -def _import_vm(request, vm_def, host, vms, *, use_cache): - vm_image = xva_name_from_def(vm_def, request.node.nodeid) +def _import_vm(request, vm_def, host, vms, test_repo, *, use_cache): + vm_image = xva_name_from_def(vm_def, request.node.nodeid, test_repo.head.commit.hexsha) base_vm = host.import_vm(vm_image, sr_uuid=host.main_sr_uuid(), use_cache=use_cache) if use_cache: diff --git a/lib/vm.py b/lib/vm.py index 17753f51..322a0b46 100644 --- a/lib/vm.py +++ b/lib/vm.py @@ -621,11 +621,12 @@ def is_cert_present(vm, key): return res.returncode == 0 -def xva_name_from_def(vm_def, ref_nodeid): +def xva_name_from_def(vm_def, ref_nodeid, test_gitref): vm_name = vm_def["name"] image_test = vm_def["image_test"] image_vm = vm_def.get("image_vm", vm_name) image_scope = vm_def.get("image_scope", "module") - return "{}-{}.xva".format(shortened_nodeid( - expand_scope_relative_nodeid(image_test, image_scope, ref_nodeid)), - image_vm) + return "{}-{}-{}.xva".format( + shortened_nodeid(expand_scope_relative_nodeid(image_test, image_scope, ref_nodeid)), + image_vm, + test_gitref) diff --git a/requirements/base.txt b/requirements/base.txt index 1cd12156..5d218bec 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,4 +1,5 @@ cryptography>=3.3.1 +GitPython packaging>=20.7 pytest>=8.0.0 pluggy>=1.1.0 From 02424f7571654a778bddb8d841fed87af651fe61 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Tue, 9 Jul 2024 09:23:08 +0200 Subject: [PATCH 48/75] Image caching: allow to declare image equivalence The key used to locate a VM image in the VM cache depends on the test repo commit hash, to protect against using results from incompatible test by mistake. But the commit hash can change for many reasons that do not influence the parent tests, so this provides a way to use known-equivalent test outputs. Signed-off-by: Yann Dirson --- data.py-dist | 4 ++++ lib/vm.py | 7 ++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/data.py-dist b/data.py-dist index 9f582531..c2833c83 100644 --- a/data.py-dist +++ b/data.py-dist @@ -153,6 +153,10 @@ BASE_ANSWERFILES = dict( }, ) +IMAGE_EQUIVS = { +# 'install.test::Nested::install[bios-83rc1-ext]-vm1-607cea0c825a4d578fa5fab56978627d8b2e28bb': 'install.test::Nested::install[bios-83rc1-ext]-vm1-addb4ead4da49856e1d2fb3ddf4e31027c6b693b', +} + # compatibility settings for older tests DEFAULT_NFS_DEVICE_CONFIG = NFS_DEVICE_CONFIG DEFAULT_NFS4_DEVICE_CONFIG = NFS4_DEVICE_CONFIG diff --git a/lib/vm.py b/lib/vm.py index 322a0b46..e9c533b8 100644 --- a/lib/vm.py +++ b/lib/vm.py @@ -626,7 +626,12 @@ def xva_name_from_def(vm_def, ref_nodeid, test_gitref): image_test = vm_def["image_test"] image_vm = vm_def.get("image_vm", vm_name) image_scope = vm_def.get("image_scope", "module") - return "{}-{}-{}.xva".format( + + image_key = "{}-{}-{}".format( shortened_nodeid(expand_scope_relative_nodeid(image_test, image_scope, ref_nodeid)), image_vm, test_gitref) + + from data import IMAGE_EQUIVS + image_key = IMAGE_EQUIVS.get(image_key, image_key) + return f"{image_key}.xva" From ec1c9f85ef74123d564061652a38e688f1bc10b5 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Fri, 21 Jun 2024 18:06:25 +0200 Subject: [PATCH 49/75] install: use xcpng_chained/continuation_of to simplify dependency spec This will avoid duplication of logic for selecting parent test. Since dependencies are now generated by fixtures, they are not taken into account by pytest-order any more. Looks like some heavy surgery would be necessary to get that back. --- pytest.ini | 3 ++- tests/install/conftest.py | 20 ++++++++++++++++++++ tests/install/test.py | 6 +++--- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/pytest.ini b/pytest.ini index 2e73d975..93a51f56 100644 --- a/pytest.ini +++ b/pytest.ini @@ -19,7 +19,8 @@ markers = windows_vm: tests that require a Windows VM to run. # * VM-related markers to give parameters to fixtures - vm_definitions: dicts of VM defs for create_vms fixture. + vm_definitions: dicts of VM nick to VM defs for create_vms fixture. + continuation_of: dicts of VM nick to test (and soon VM nick) from which to start # * installation-related markers to customize installer run answerfile: dict defining an answerfile diff --git a/tests/install/conftest.py b/tests/install/conftest.py index a695827f..2f51ddde 100644 --- a/tests/install/conftest.py +++ b/tests/install/conftest.py @@ -2,6 +2,7 @@ import logging import os import pytest +import pytest_dependency import tempfile import xml.etree.ElementTree as ET @@ -176,3 +177,22 @@ def iso_remaster(request, answerfile): finally: logging.info("Removing %s from ISO-SR server", os.path.basename(remote_iso)) ssh(ISOSR_SRV, ["rm", remote_iso]) + +@pytest.fixture(scope='function') +def xcpng_chained(request): + # take test name from mark + marker = request.node.get_closest_marker("continuation_of") + assert marker is not None, "xcpng_chained fixture requires 'continuation_of' marker" + param_mapping = marker.kwargs.get("param_mapping", {}) + continuation_of = callable_marker(marker.args[0], request, param_mapping=param_mapping) + + vm_defs = [dict(name=vm_spec['vm'], + image_test=vm_spec['image_test'], + image_vm=vm_spec.get("image_vm", vm_spec['vm']), + image_scope=vm_spec.get("scope", "module"), + ) + for vm_spec in continuation_of] + + depends = [vm_spec['image_test'] for vm_spec in continuation_of] + pytest_dependency.depends(request, depends) + request.applymarker(pytest.mark.vm_definitions(*vm_defs)) diff --git a/tests/install/test.py b/tests/install/test.py index 146613a9..04b10c1b 100644 --- a/tests/install/test.py +++ b/tests/install/test.py @@ -40,9 +40,9 @@ def test_install(self, create_vms, iso_remaster): installer.perform_install(iso=iso_remaster, host_vm=create_vms[0]) - @pytest.mark.dependency(depends=["TestNested::test_install"]) - @pytest.mark.vm_definitions( - dict(name="vm1", image_test="TestNested::test_install")) + @pytest.mark.usefixtures("xcpng_chained") + @pytest.mark.continuation_of([dict(vm="vm1", + image_test="TestNested::test_install")]) def test_firstboot(self, create_vms): host_vm = create_vms[0] vif = host_vm.vifs()[0] From 7e20f6dfe7d396041dd372949af0b77d06efc162 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Thu, 20 Jun 2024 18:00:45 +0200 Subject: [PATCH 50/75] install: add "version" test parameter, and 8.3b2 as a version --- tests/install/test-sequences/82.lst | 2 ++ tests/install/test-sequences/83.lst | 2 ++ tests/install/test.py | 27 ++++++++++++++++++++++----- 3 files changed, 26 insertions(+), 5 deletions(-) create mode 100644 tests/install/test-sequences/82.lst create mode 100644 tests/install/test-sequences/83.lst diff --git a/tests/install/test-sequences/82.lst b/tests/install/test-sequences/82.lst new file mode 100644 index 00000000..115fbf3a --- /dev/null +++ b/tests/install/test-sequences/82.lst @@ -0,0 +1,2 @@ +tests/install/test.py::TestNested::test_install[821.1] +tests/install/test.py::TestNested::test_firstboot[821.1] diff --git a/tests/install/test-sequences/83.lst b/tests/install/test-sequences/83.lst new file mode 100644 index 00000000..3d6968a4 --- /dev/null +++ b/tests/install/test-sequences/83.lst @@ -0,0 +1,2 @@ +tests/install/test.py::TestNested::test_install[83b2] +tests/install/test.py::TestNested::test_firstboot[83b2] diff --git a/tests/install/test.py b/tests/install/test.py index 04b10c1b..d9e42346 100644 --- a/tests/install/test.py +++ b/tests/install/test.py @@ -12,6 +12,10 @@ @pytest.mark.dependency() class TestNested: + @pytest.mark.parametrize("iso_version", ( + "83b2", + "821.1", + )) @pytest.mark.vm_definitions( dict(name="vm1", template="Other install media", @@ -29,21 +33,34 @@ class TestNested: cd_vbd=dict(device="xvdd", userdevice="3"), vifs=[dict(index=0, network_uuid=NETWORKS["MGMT"])], )) + @pytest.mark.installer_iso( + lambda version: { + "83b2": "xcpng-8.3-beta2", + "821.1": "xcpng-8.2.1-2023", + }[version], + param_mapping={"version": "iso_version"}) @pytest.mark.answerfile(lambda: AnswerFile("INSTALL") \ .top_append( {"TAG": "source", "type": "local"}, {"TAG": "primary-disk", "CONTENTS": "nvme0n1"}, )) - @pytest.mark.installer_iso("xcpng-8.2.1-2023") - def test_install(self, create_vms, iso_remaster): + def test_install(self, create_vms, iso_remaster, + iso_version): assert len(create_vms) == 1 installer.perform_install(iso=iso_remaster, host_vm=create_vms[0]) @pytest.mark.usefixtures("xcpng_chained") - @pytest.mark.continuation_of([dict(vm="vm1", - image_test="TestNested::test_install")]) - def test_firstboot(self, create_vms): + @pytest.mark.parametrize("mode", ( + "83b2", + "821.1", + )) + @pytest.mark.continuation_of( + lambda params: [dict(vm="vm1", + image_test=f"TestNested::test_install[{params}]")], + param_mapping={"params": "mode"}) + def test_firstboot(self, create_vms, + mode): host_vm = create_vms[0] vif = host_vm.vifs()[0] mac_address = vif.param_get('MAC') From 8888cef5e1e73c659f377c2acbe19b97461732b2 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Thu, 25 Jul 2024 19:08:54 +0200 Subject: [PATCH 51/75] Add upgrade test 8.3b2 does not support upgrading from any 8.3. Signed-off-by: Yann Dirson --- data.py-dist | 4 ++ lib/installer.py | 80 +++++++++++++++++++++++++- tests/install/test-sequences/82-83.lst | 4 ++ tests/install/test-sequences/82.lst | 2 - tests/install/test.py | 36 +++++++++++- 5 files changed, 122 insertions(+), 4 deletions(-) create mode 100644 tests/install/test-sequences/82-83.lst delete mode 100644 tests/install/test-sequences/82.lst diff --git a/data.py-dist b/data.py-dist index c2833c83..f14a75d8 100644 --- a/data.py-dist +++ b/data.py-dist @@ -151,6 +151,10 @@ BASE_ANSWERFILES = dict( "CONTENTS": "us"}, ), }, + UPGRADE={ + "TAG": "installation", + "mode": "upgrade", + }, ) IMAGE_EQUIVS = { diff --git a/lib/installer.py b/lib/installer.py index d33ba889..5b6a225f 100644 --- a/lib/installer.py +++ b/lib/installer.py @@ -2,7 +2,7 @@ import xml.etree.ElementTree as ET from lib import commands, pxe -from lib.commands import ssh +from lib.commands import local_cmd, ssh from lib.common import wait_for class AnswerFile: @@ -133,3 +133,81 @@ def perform_install(*, iso, host_vm): # wait_for(lambda: False, 'Wait "forever"', timeout_secs=100*60) host_vm.shutdown(force=True) raise + +def monitor_upgrade(*, ip): + # wait for "yum install" phase to start + wait_for(lambda: ssh(ip, ["grep", + "'DISPATCH: NEW PHASE: Reading package information'", + "/tmp/install-log"], + check=False, simple_output=False, + ).returncode == 0, + "Wait for upgrade preparations to finish", + timeout_secs=40*60) # FIXME too big + + # wait for "yum install" phase to finish + wait_for(lambda: ssh(ip, ["grep", + "'DISPATCH: NEW PHASE: Completing installation'", + "/tmp/install-log"], + check=False, simple_output=False, + ).returncode == 0, + "Wait for rpm installation to succeed", + timeout_secs=40*60) # FIXME too big + + # wait for install to finish + wait_for(lambda: ssh(ip, ["grep", + "'The installation completed successfully'", + "/tmp/install-log"], + check=False, simple_output=False, + ).returncode == 0, + "Wait for system installation to succeed", + timeout_secs=40*60) # FIXME too big + + wait_for(lambda: ssh(ip, ["ps a|grep '[0-9]. python /opt/xensource/installer/init'"], + check=False, simple_output=False, + ).returncode == 1, + "Wait for installer to terminate") + +def perform_upgrade(*, iso, host_vm): + vif = host_vm.vifs()[0] + mac_address = vif.param_get('MAC') + logging.info("Host VM has MAC %s", mac_address) + + host_vm.insert_cd(iso) + + try: + host_vm.start() + wait_for(host_vm.is_running, "Wait for host VM running") + + # catch host-vm IP address + wait_for(lambda: pxe.arp_addresses_for(mac_address), + "Wait for DHCP server to see Host VM in ARP tables", + timeout_secs=10*60) + ips = pxe.arp_addresses_for(mac_address) + logging.info("Host VM has IPs %s", ips) + assert len(ips) == 1 + host_vm.ip = ips[0] + + # host may not be up if ARP cache was filled + wait_for(lambda: local_cmd(["ping", "-c1", host_vm.ip], check=False), + "Wait for host up", timeout_secs=10 * 60, retry_delay_secs=10) + wait_for(lambda: local_cmd(["nc", "-zw5", host_vm.ip, "22"], check=False), + "Wait for ssh up on host", timeout_secs=10 * 60, retry_delay_secs=5) + + monitor_upgrade(ip=host_vm.ip) + + logging.info("Shutting down Host VM after successful upgrade") + poweroff(host_vm.ip) + wait_for(host_vm.is_halted, "Wait for host VM halted") + + except Exception as e: + logging.critical("caught exception %s", e) + # wait_for(lambda: False, 'Wait "forever"', timeout_secs=100*60) + host_vm.shutdown(force=True) + raise + except KeyboardInterrupt: + logging.warning("keyboard interrupt") + # wait_for(lambda: False, 'Wait "forever"', timeout_secs=100*60) + host_vm.shutdown(force=True) + raise + + host_vm.eject_cd() diff --git a/tests/install/test-sequences/82-83.lst b/tests/install/test-sequences/82-83.lst new file mode 100644 index 00000000..8cdc2393 --- /dev/null +++ b/tests/install/test-sequences/82-83.lst @@ -0,0 +1,4 @@ +tests/install/test.py::TestNested::test_install[821.1] +tests/install/test.py::TestNested::test_firstboot[821.1] +tests/install/test.py::TestNested::test_upgrade[821.1-83b2] +tests/install/test.py::TestNested::test_firstboot[821.1-83b2] diff --git a/tests/install/test-sequences/82.lst b/tests/install/test-sequences/82.lst deleted file mode 100644 index 115fbf3a..00000000 --- a/tests/install/test-sequences/82.lst +++ /dev/null @@ -1,2 +0,0 @@ -tests/install/test.py::TestNested::test_install[821.1] -tests/install/test.py::TestNested::test_firstboot[821.1] diff --git a/tests/install/test.py b/tests/install/test.py index d9e42346..b472d49a 100644 --- a/tests/install/test.py +++ b/tests/install/test.py @@ -53,11 +53,19 @@ def test_install(self, create_vms, iso_remaster, @pytest.mark.usefixtures("xcpng_chained") @pytest.mark.parametrize("mode", ( "83b2", + #"83b2-83b2", # 8.3b2 disabled the upgrade from 8.3 + "821.1-83b2", "821.1", + "821.1-821.1", )) @pytest.mark.continuation_of( lambda params: [dict(vm="vm1", - image_test=f"TestNested::test_install[{params}]")], + image_test=(f"TestNested::{{}}[{params}]".format( + { + 1: "test_install", + 2: "test_upgrade", + }[len(params.split("-"))] + )))], param_mapping={"params": "mode"}) def test_firstboot(self, create_vms, mode): @@ -148,3 +156,29 @@ def test_firstboot(self, create_vms, # wait_for(lambda: False, 'Wait "forever"', timeout_secs=100*60) host_vm.shutdown(force=True) raise + + @pytest.mark.usefixtures("xcpng_chained") + @pytest.mark.parametrize(("orig_version", "iso_version"), [ + #("83b2", "83b2"), # 8.3b2 disabled the upgrade from 8.3 + ("821.1", "83b2"), + ("821.1", "821.1"), + ]) + @pytest.mark.continuation_of( + lambda params: [dict(vm="vm1", + image_test=f"TestNested::test_firstboot[{params}]")], + param_mapping={"params": "orig_version"}) + @pytest.mark.installer_iso( + lambda version: { + "821.1": "xcpng-8.2.1-2023", + "83b2": "xcpng-8.3-beta2", + }[version], + param_mapping={"version": "iso_version"}) + @pytest.mark.answerfile( + lambda firmware: AnswerFile("UPGRADE").top_append( + {"TAG": "source", "type": "local"}, + {"TAG": "existing-installation", "CONTENTS": "nvme0n1"}, + ), + param_mapping={"firmware": "firmware"}) + def test_upgrade(self, create_vms, iso_remaster, + orig_version, iso_version): + installer.perform_upgrade(iso=iso_remaster, host_vm=create_vms[0]) From 8a22b453bb9c9087ca95bdfa07fb169e880b5334 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Wed, 24 Jul 2024 11:38:40 +0200 Subject: [PATCH 52/75] install: add a "firmware" parameter --- tests/install/test-sequences/82-83-bios.lst | 4 + tests/install/test-sequences/82-83-uefi.lst | 4 + tests/install/test-sequences/82-83.lst | 4 - tests/install/test-sequences/83-bios.lst | 2 + tests/install/test-sequences/83-uefi.lst | 2 + tests/install/test-sequences/83.lst | 2 - tests/install/test.py | 85 ++++++++++++--------- 7 files changed, 60 insertions(+), 43 deletions(-) create mode 100644 tests/install/test-sequences/82-83-bios.lst create mode 100644 tests/install/test-sequences/82-83-uefi.lst delete mode 100644 tests/install/test-sequences/82-83.lst create mode 100644 tests/install/test-sequences/83-bios.lst create mode 100644 tests/install/test-sequences/83-uefi.lst delete mode 100644 tests/install/test-sequences/83.lst diff --git a/tests/install/test-sequences/82-83-bios.lst b/tests/install/test-sequences/82-83-bios.lst new file mode 100644 index 00000000..172deb6b --- /dev/null +++ b/tests/install/test-sequences/82-83-bios.lst @@ -0,0 +1,4 @@ +tests/install/test.py::TestNested::test_install[bios-821.1] +tests/install/test.py::TestNested::test_firstboot[bios-821.1] +tests/install/test.py::TestNested::test_upgrade[bios-821.1-83b2] +tests/install/test.py::TestNested::test_firstboot[bios-821.1-83b2] diff --git a/tests/install/test-sequences/82-83-uefi.lst b/tests/install/test-sequences/82-83-uefi.lst new file mode 100644 index 00000000..506f62ce --- /dev/null +++ b/tests/install/test-sequences/82-83-uefi.lst @@ -0,0 +1,4 @@ +tests/install/test.py::TestNested::test_install[uefi-821.1] +tests/install/test.py::TestNested::test_firstboot[uefi-821.1] +tests/install/test.py::TestNested::test_upgrade[uefi-821.1-83b2] +tests/install/test.py::TestNested::test_firstboot[uefi-821.1-83b2] diff --git a/tests/install/test-sequences/82-83.lst b/tests/install/test-sequences/82-83.lst deleted file mode 100644 index 8cdc2393..00000000 --- a/tests/install/test-sequences/82-83.lst +++ /dev/null @@ -1,4 +0,0 @@ -tests/install/test.py::TestNested::test_install[821.1] -tests/install/test.py::TestNested::test_firstboot[821.1] -tests/install/test.py::TestNested::test_upgrade[821.1-83b2] -tests/install/test.py::TestNested::test_firstboot[821.1-83b2] diff --git a/tests/install/test-sequences/83-bios.lst b/tests/install/test-sequences/83-bios.lst new file mode 100644 index 00000000..47fbcceb --- /dev/null +++ b/tests/install/test-sequences/83-bios.lst @@ -0,0 +1,2 @@ +tests/install/test.py::TestNested::test_install[bios-83b2] +tests/install/test.py::TestNested::test_firstboot[bios-83b2] diff --git a/tests/install/test-sequences/83-uefi.lst b/tests/install/test-sequences/83-uefi.lst new file mode 100644 index 00000000..2e75c5ed --- /dev/null +++ b/tests/install/test-sequences/83-uefi.lst @@ -0,0 +1,2 @@ +tests/install/test.py::TestNested::test_install[uefi-83b2] +tests/install/test.py::TestNested::test_firstboot[uefi-83b2] diff --git a/tests/install/test-sequences/83.lst b/tests/install/test-sequences/83.lst deleted file mode 100644 index 3d6968a4..00000000 --- a/tests/install/test-sequences/83.lst +++ /dev/null @@ -1,2 +0,0 @@ -tests/install/test.py::TestNested::test_install[83b2] -tests/install/test.py::TestNested::test_firstboot[83b2] diff --git a/tests/install/test.py b/tests/install/test.py index b472d49a..6a599301 100644 --- a/tests/install/test.py +++ b/tests/install/test.py @@ -16,36 +16,44 @@ class TestNested: "83b2", "821.1", )) - @pytest.mark.vm_definitions( - dict(name="vm1", - template="Other install media", - params=( - # dict(param_name="", value=""), - dict(param_name="memory-static-max", value="4GiB"), - dict(param_name="memory-dynamic-max", value="4GiB"), - dict(param_name="memory-dynamic-min", value="4GiB"), - dict(param_name="platform", key="exp-nested-hvm", value="true"), # FIXME < 8.3 host? - dict(param_name="HVM-boot-params", key="firmware", value="uefi"), - dict(param_name="HVM-boot-params", key="order", value="dc"), - dict(param_name="platform", key="device-model", value="qemu-upstream-uefi"), - ), - vdis=[dict(name="vm1 system disk", size="100GiB", device="xvda", userdevice="0")], - cd_vbd=dict(device="xvdd", userdevice="3"), - vifs=[dict(index=0, network_uuid=NETWORKS["MGMT"])], - )) + @pytest.mark.parametrize("firmware", ("uefi", "bios")) + @pytest.mark.vm_definitions(lambda firmware: dict( + name="vm1", + template="Other install media", + params=( + # dict(param_name="", value=""), + dict(param_name="memory-static-max", value="4GiB"), + dict(param_name="memory-dynamic-max", value="4GiB"), + dict(param_name="memory-dynamic-min", value="4GiB"), + dict(param_name="platform", key="exp-nested-hvm", value="true"), # FIXME < 8.3 host? + dict(param_name="HVM-boot-params", key="order", value="dc"), + ) + { + "uefi": ( + dict(param_name="HVM-boot-params", key="firmware", value="uefi"), + dict(param_name="platform", key="device-model", value="qemu-upstream-uefi"), + ), + "bios": (), + }[firmware], + vdis=[dict(name="vm1 system disk", size="100GiB", device="xvda", userdevice="0")], + cd_vbd=dict(device="xvdd", userdevice="3"), + vifs=[dict(index=0, network_uuid=NETWORKS["MGMT"])], + ), + param_mapping={"firmware": "firmware"}) @pytest.mark.installer_iso( lambda version: { "83b2": "xcpng-8.3-beta2", "821.1": "xcpng-8.2.1-2023", }[version], param_mapping={"version": "iso_version"}) - @pytest.mark.answerfile(lambda: AnswerFile("INSTALL") \ + @pytest.mark.answerfile(lambda firmware: AnswerFile("INSTALL") \ .top_append( {"TAG": "source", "type": "local"}, - {"TAG": "primary-disk", "CONTENTS": "nvme0n1"}, - )) + {"TAG": "primary-disk", + "CONTENTS": {"uefi": "nvme0n1", "bios": "sda"}[firmware]}, + ), + param_mapping={"firmware": "firmware"}) def test_install(self, create_vms, iso_remaster, - iso_version): + firmware, iso_version): assert len(create_vms) == 1 installer.perform_install(iso=iso_remaster, host_vm=create_vms[0]) @@ -58,17 +66,18 @@ def test_install(self, create_vms, iso_remaster, "821.1", "821.1-821.1", )) - @pytest.mark.continuation_of( - lambda params: [dict(vm="vm1", - image_test=(f"TestNested::{{}}[{params}]".format( - { - 1: "test_install", - 2: "test_upgrade", - }[len(params.split("-"))] - )))], - param_mapping={"params": "mode"}) + @pytest.mark.parametrize("firmware", ("uefi", "bios")) + @pytest.mark.continuation_of(lambda params, firmware: [dict( + vm="vm1", + image_test=(f"TestNested::{{}}[{firmware}-{params}]".format( + { + 1: "test_install", + 2: "test_upgrade", + }[len(params.split("-"))] + )))], + param_mapping={"params": "mode", "firmware": "firmware"}) def test_firstboot(self, create_vms, - mode): + firmware, mode): host_vm = create_vms[0] vif = host_vm.vifs()[0] mac_address = vif.param_get('MAC') @@ -163,10 +172,11 @@ def test_firstboot(self, create_vms, ("821.1", "83b2"), ("821.1", "821.1"), ]) - @pytest.mark.continuation_of( - lambda params: [dict(vm="vm1", - image_test=f"TestNested::test_firstboot[{params}]")], - param_mapping={"params": "orig_version"}) + @pytest.mark.parametrize("firmware", ("uefi", "bios")) + @pytest.mark.continuation_of(lambda firmware, params: [dict( + vm="vm1", + image_test=f"TestNested::test_firstboot[{firmware}-{params}]")], + param_mapping={"params": "orig_version", "firmware": "firmware"}) @pytest.mark.installer_iso( lambda version: { "821.1": "xcpng-8.2.1-2023", @@ -176,9 +186,10 @@ def test_firstboot(self, create_vms, @pytest.mark.answerfile( lambda firmware: AnswerFile("UPGRADE").top_append( {"TAG": "source", "type": "local"}, - {"TAG": "existing-installation", "CONTENTS": "nvme0n1"}, + {"TAG": "existing-installation", + "CONTENTS": {"uefi": "nvme0n1", "bios": "sda"}[firmware]}, ), param_mapping={"firmware": "firmware"}) def test_upgrade(self, create_vms, iso_remaster, - orig_version, iso_version): + firmware, orig_version, iso_version): installer.perform_upgrade(iso=iso_remaster, host_vm=create_vms[0]) From fee03e8695e173fbc55d2c646bbf102877171827 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Wed, 24 Jul 2024 11:34:04 +0200 Subject: [PATCH 53/75] install: add "restore" test using 8.3 ISO --- data.py-dist | 3 + tests/install/test-sequences/82-83-bios.lst | 2 + tests/install/test-sequences/82-83-uefi.lst | 2 + tests/install/test.py | 100 ++++++++++++++++++++ 4 files changed, 107 insertions(+) diff --git a/data.py-dist b/data.py-dist index f14a75d8..adf5a25c 100644 --- a/data.py-dist +++ b/data.py-dist @@ -155,6 +155,9 @@ BASE_ANSWERFILES = dict( "TAG": "installation", "mode": "upgrade", }, + RESTORE={ + "TAG": "restore", + }, ) IMAGE_EQUIVS = { diff --git a/tests/install/test-sequences/82-83-bios.lst b/tests/install/test-sequences/82-83-bios.lst index 172deb6b..d7562855 100644 --- a/tests/install/test-sequences/82-83-bios.lst +++ b/tests/install/test-sequences/82-83-bios.lst @@ -2,3 +2,5 @@ tests/install/test.py::TestNested::test_install[bios-821.1] tests/install/test.py::TestNested::test_firstboot[bios-821.1] tests/install/test.py::TestNested::test_upgrade[bios-821.1-83b2] tests/install/test.py::TestNested::test_firstboot[bios-821.1-83b2] +tests/install/test.py::TestNested::test_restore[bios-821.1-83b2-83b2] +tests/install/test.py::TestNested::test_firstboot[bios-821.1-83b2-83b2] diff --git a/tests/install/test-sequences/82-83-uefi.lst b/tests/install/test-sequences/82-83-uefi.lst index 506f62ce..a716e633 100644 --- a/tests/install/test-sequences/82-83-uefi.lst +++ b/tests/install/test-sequences/82-83-uefi.lst @@ -2,3 +2,5 @@ tests/install/test.py::TestNested::test_install[uefi-821.1] tests/install/test.py::TestNested::test_firstboot[uefi-821.1] tests/install/test.py::TestNested::test_upgrade[uefi-821.1-83b2] tests/install/test.py::TestNested::test_firstboot[uefi-821.1-83b2] +tests/install/test.py::TestNested::test_restore[uefi-821.1-83b2-83b2] +tests/install/test.py::TestNested::test_firstboot[uefi-821.1-83b2-83b2] diff --git a/tests/install/test.py b/tests/install/test.py index 6a599301..57436336 100644 --- a/tests/install/test.py +++ b/tests/install/test.py @@ -63,6 +63,7 @@ def test_install(self, create_vms, iso_remaster, "83b2", #"83b2-83b2", # 8.3b2 disabled the upgrade from 8.3 "821.1-83b2", + "821.1-83b2-83b2", "821.1", "821.1-821.1", )) @@ -73,6 +74,7 @@ def test_install(self, create_vms, iso_remaster, { 1: "test_install", 2: "test_upgrade", + 3: "test_restore", }[len(params.split("-"))] )))], param_mapping={"params": "mode", "firmware": "firmware"}) @@ -193,3 +195,101 @@ def test_firstboot(self, create_vms, def test_upgrade(self, create_vms, iso_remaster, firmware, orig_version, iso_version): installer.perform_upgrade(iso=iso_remaster, host_vm=create_vms[0]) + + @pytest.mark.usefixtures("xcpng_chained") + @pytest.mark.parametrize(("orig_version", "iso_version"), [ + ("821.1-83b2", "83b2"), + ]) + @pytest.mark.parametrize("firmware", ("uefi", "bios")) + @pytest.mark.continuation_of(lambda firmware, params: [dict( + vm="vm1", + image_test=f"TestNested::test_firstboot[{firmware}-{params}]")], + param_mapping={"params": "orig_version", "firmware": "firmware"}) + @pytest.mark.installer_iso( + lambda version: { + "821.1": "xcpng-8.2.1-2023", + "83b2": "xcpng-8.3-beta2", + }[version], + param_mapping={"version": "iso_version"}) + @pytest.mark.answerfile(lambda firmware: AnswerFile("RESTORE").top_append( + {"TAG": "backup-disk", + "CONTENTS": {"uefi": "nvme0n1", "bios": "sda"}[firmware]}, + ), + param_mapping={"firmware": "firmware"}) + def test_restore(self, create_vms, iso_remaster, + firmware, orig_version, iso_version): + host_vm = create_vms[0] + vif = host_vm.vifs()[0] + mac_address = vif.param_get('MAC') + logging.info("Host VM has MAC %s", mac_address) + + host_vm.insert_cd(iso_remaster) + + try: + host_vm.start() + wait_for(host_vm.is_running, "Wait for host VM running") + + # catch host-vm IP address + wait_for(lambda: pxe.arp_addresses_for(mac_address), + "Wait for DHCP server to see Host VM in ARP tables", + timeout_secs=10*60) + ips = pxe.arp_addresses_for(mac_address) + logging.info("Host VM has IPs %s", ips) + assert len(ips) == 1 + host_vm.ip = ips[0] + + # wait for "yum install" phase to start + wait_for(lambda: host_vm.ssh(["grep", + "'Restoring backup'", + "/tmp/install-log"], + check=False, simple_output=False, + ).returncode == 0, + "Wait for data restoration to start", + timeout_secs=40*60) # FIXME too big + + # wait for "yum install" phase to finish + wait_for(lambda: host_vm.ssh(["grep", + "'Data restoration complete. About to re-install bootloader.'", + "/tmp/install-log"], + check=False, simple_output=False, + ).returncode == 0, + "Wait for data restoration to complete", + timeout_secs=40*60) # FIXME too big + + # The installer will not terminate in restore mode, it + # requires human interaction and does not even log it, so + # wait for last known action log (tested with 8.3b2) + wait_for(lambda: host_vm.ssh(["grep", + "'ran .*swaplabel.*rc 0'", + "/tmp/install-log"], + check=False, simple_output=False, + ).returncode == 0, + "Wait for installer to hopefully finish", + timeout_secs=40*60) # FIXME too big + + # "wait a bit to be extra sure". Yuck. + time.sleep(30) + + logging.info("Shutting down Host VM after successful restore") + try: + host_vm.ssh(["poweroff"]) + except commands.SSHCommandFailed as e: + # ignore connection closed by reboot + if e.returncode == 255 and "closed by remote host" in e.stdout: + logging.info("sshd closed the connection") + pass + else: + raise + wait_for(host_vm.is_halted, "Wait for host VM halted") + host_vm.eject_cd() + + except Exception as e: + logging.critical("caught exception %s", e) + # wait_for(lambda: False, 'Wait "forever"', timeout_secs=100*60) + host_vm.shutdown(force=True) + raise + except KeyboardInterrupt: + logging.warning("keyboard interrupt") + # wait_for(lambda: False, 'Wait "forever"', timeout_secs=100*60) + host_vm.shutdown(force=True) + raise From bb35e37925e7478cb112f29e8d36ac4351a344a2 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Mon, 17 Jun 2024 14:43:13 +0200 Subject: [PATCH 54/75] install/firstboot: check installed version --- tests/install/test.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/install/test.py b/tests/install/test.py index 57436336..7a1741de 100644 --- a/tests/install/test.py +++ b/tests/install/test.py @@ -85,6 +85,20 @@ def test_firstboot(self, create_vms, mac_address = vif.param_get('MAC') logging.info("Host VM has MAC %s", mac_address) + # determine version info from `mode` + expected_dist = "XCP-ng" + # succession of insta/upg/rst operations + split_mode = mode.split("-") + if len(split_mode) == 3: + # restore: back to 1st installed version + expected_rel_id = split_mode[0] + else: + expected_rel_id = split_mode[-1] + expected_rel = { + "821.1": "8.2.1", + "83b2": "8.3.0", + }[expected_rel_id] + try: # FIXME: evict MAC from ARP cache first? host_vm.start() @@ -104,6 +118,11 @@ def test_firstboot(self, create_vms, ["nc", "-zw5", host_vm.ip, "22"], check=False).returncode == 0, "Wait for ssh back up on Host VM", retry_delay_secs=5, timeout_secs=4 * 60) + logging.info("Checking installed version") + lsb_dist = commands.ssh(host_vm.ip, ["lsb_release", "-si"]) + lsb_rel = commands.ssh(host_vm.ip, ["lsb_release", "-sr"]) + assert (lsb_dist, lsb_rel) == (expected_dist, expected_rel) + # wait for XAPI startup to be done, which avoids: # - waiting for XAPI to start listening to its socket # - waiting for host and pool objects to be populated after install From 62fda98eed062f0ca1f081c7e7cf9ce6d8b8ce52 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Mon, 17 Jun 2024 16:08:14 +0200 Subject: [PATCH 55/75] install: add XS/CH support --- data.py-dist | 3 +++ tests/install/test.py | 17 ++++++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/data.py-dist b/data.py-dist index adf5a25c..c65e70a0 100644 --- a/data.py-dist +++ b/data.py-dist @@ -70,6 +70,9 @@ ISO_IMAGES = { # 'xcpng-8.2.1-2023': {'path': "/home/user/iso/xcp-ng-8.2.1-20231130.iso"}, # 'xcpng-8.2.1': {'path': "/home/user/iso/xcp-ng-8.2.1.iso"}, # 'xcpng-8.2.0': {'path': "/home/user/iso/xcp-ng-8.2.0.iso"}, +# 'ch-8.2.1': {'path': "/home/user/iso/CitrixHypervisor-8.2.1-install-cd.iso"}, +# 'ch-8.2.1-23': {'path': "/home/user/iso/CitrixHypervisor-8.2.1-2306-install-cd.iso"}, +# 'xs8-2024-03': {'path': "/home/user/iso/XenServer8_2024-03-18.iso"}, } # In some cases, we may prefer to favour a local SR to store test VM disks, diff --git a/tests/install/test.py b/tests/install/test.py index 7a1741de..4675e3a9 100644 --- a/tests/install/test.py +++ b/tests/install/test.py @@ -15,6 +15,7 @@ class TestNested: @pytest.mark.parametrize("iso_version", ( "83b2", "821.1", + "xs8", "ch821.1", )) @pytest.mark.parametrize("firmware", ("uefi", "bios")) @pytest.mark.vm_definitions(lambda firmware: dict( @@ -43,6 +44,8 @@ class TestNested: lambda version: { "83b2": "xcpng-8.3-beta2", "821.1": "xcpng-8.2.1-2023", + "xs8": "xs8-2024-03", + "ch821.1": "ch-8.2.1-23", }[version], param_mapping={"version": "iso_version"}) @pytest.mark.answerfile(lambda firmware: AnswerFile("INSTALL") \ @@ -64,8 +67,11 @@ def test_install(self, create_vms, iso_remaster, #"83b2-83b2", # 8.3b2 disabled the upgrade from 8.3 "821.1-83b2", "821.1-83b2-83b2", + "ch821.1-83b2", + "ch821.1-83b2-83b2", "821.1", "821.1-821.1", + "ch821.1", "xs8", )) @pytest.mark.parametrize("firmware", ("uefi", "bios")) @pytest.mark.continuation_of(lambda params, firmware: [dict( @@ -86,7 +92,12 @@ def test_firstboot(self, create_vms, logging.info("Host VM has MAC %s", mac_address) # determine version info from `mode` - expected_dist = "XCP-ng" + if mode.startswith("xs"): + expected_dist = "XenServer" + elif mode.startswith("ch"): + expected_dist = "CitrixHypervisor" + else: + expected_dist = "XCP-ng" # succession of insta/upg/rst operations split_mode = mode.split("-") if len(split_mode) == 3: @@ -95,6 +106,8 @@ def test_firstboot(self, create_vms, else: expected_rel_id = split_mode[-1] expected_rel = { + "ch821.1": "8.2.1", + "xs8": "8.4.0", "821.1": "8.2.1", "83b2": "8.3.0", }[expected_rel_id] @@ -191,6 +204,7 @@ def test_firstboot(self, create_vms, @pytest.mark.parametrize(("orig_version", "iso_version"), [ #("83b2", "83b2"), # 8.3b2 disabled the upgrade from 8.3 ("821.1", "83b2"), + ("ch821.1", "83b2"), ("821.1", "821.1"), ]) @pytest.mark.parametrize("firmware", ("uefi", "bios")) @@ -218,6 +232,7 @@ def test_upgrade(self, create_vms, iso_remaster, @pytest.mark.usefixtures("xcpng_chained") @pytest.mark.parametrize(("orig_version", "iso_version"), [ ("821.1-83b2", "83b2"), + ("ch821.1-83b2", "83b2"), ]) @pytest.mark.parametrize("firmware", ("uefi", "bios")) @pytest.mark.continuation_of(lambda firmware, params: [dict( From fc1ae463fac378a3c040b688dc39f28eba939911 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Fri, 26 Jul 2024 11:18:52 +0200 Subject: [PATCH 56/75] install: add installation of xcp-ng 8.0 and 8.1, upgrades to 8.3b2 --- data.py-dist | 2 ++ tests/install/test.py | 56 +++++++++++++++++++++++++++++++++++-------- 2 files changed, 48 insertions(+), 10 deletions(-) diff --git a/data.py-dist b/data.py-dist index c65e70a0..57b83454 100644 --- a/data.py-dist +++ b/data.py-dist @@ -70,6 +70,8 @@ ISO_IMAGES = { # 'xcpng-8.2.1-2023': {'path': "/home/user/iso/xcp-ng-8.2.1-20231130.iso"}, # 'xcpng-8.2.1': {'path': "/home/user/iso/xcp-ng-8.2.1.iso"}, # 'xcpng-8.2.0': {'path': "/home/user/iso/xcp-ng-8.2.0.iso"}, +# 'xcpng-8.1': {'path': "/home/user/iso/xcp-ng-8.1.0-2.iso"}, +# 'xcpng-8.0': {'path': "/home/user/iso/xcp-ng-8.0.0.iso"}, # 'ch-8.2.1': {'path': "/home/user/iso/CitrixHypervisor-8.2.1-install-cd.iso"}, # 'ch-8.2.1-23': {'path': "/home/user/iso/CitrixHypervisor-8.2.1-2306-install-cd.iso"}, # 'xs8-2024-03': {'path': "/home/user/iso/XenServer8_2024-03-18.iso"}, diff --git a/tests/install/test.py b/tests/install/test.py index 4675e3a9..370a0fd5 100644 --- a/tests/install/test.py +++ b/tests/install/test.py @@ -15,6 +15,7 @@ class TestNested: @pytest.mark.parametrize("iso_version", ( "83b2", "821.1", + "81", "80", "xs8", "ch821.1", )) @pytest.mark.parametrize("firmware", ("uefi", "bios")) @@ -44,6 +45,8 @@ class TestNested: lambda version: { "83b2": "xcpng-8.3-beta2", "821.1": "xcpng-8.2.1-2023", + "81": "xcpng-8.1", + "80": "xcpng-8.0", "xs8": "xs8-2024-03", "ch821.1": "ch-8.2.1-23", }[version], @@ -67,10 +70,13 @@ def test_install(self, create_vms, iso_remaster, #"83b2-83b2", # 8.3b2 disabled the upgrade from 8.3 "821.1-83b2", "821.1-83b2-83b2", + "81-83b2", "81-83b2-83b2", + "80-83b2", "80-83b2-83b2", "ch821.1-83b2", "ch821.1-83b2-83b2", "821.1", "821.1-821.1", + "81", "80", "ch821.1", "xs8", )) @pytest.mark.parametrize("firmware", ("uefi", "bios")) @@ -108,6 +114,8 @@ def test_firstboot(self, create_vms, expected_rel = { "ch821.1": "8.2.1", "xs8": "8.4.0", + "80": "8.0.0", + "81": "8.1.0", "821.1": "8.2.1", "83b2": "8.3.0", }[expected_rel_id] @@ -151,26 +159,50 @@ def test_firstboot(self, create_vms, # wait for XAPI wait_for(pool.master.is_enabled, "Wait for XAPI to be ready", timeout_secs=30 * 60) - # check for firstboot issues - # FIXME: flaky, must check logs extraction on failure - for service in ["control-domain-params-init", + if lsb_rel in ["8.2.1", "8.3.0", "8.4.0"]: + SERVICES =["control-domain-params-init", "network-init", "storage-init", "generate-iscsi-iqn", "create-guest-templates", - ]: - try: - wait_for(lambda: pool.master.ssh(["test", "-e", f"/var/lib/misc/ran-{service}"], + ] + STAMPS_DIR = "/var/lib/misc" + STAMPS = [f"ran-{service}" for service in SERVICES] + elif lsb_rel in ["8.0.0", "8.1.0"]: + SERVICES = ["xs-firstboot"] + STAMPS_DIR = "/etc/firstboot.d/state" + STAMPS = [ + "05-prepare-networking", + "10-prepare-storage", + "15-set-default-storage", + "20-udev-storage", + "25-multipath", + "40-generate-iscsi-iqn", + "50-prepare-control-domain-params", + "60-import-keys", + "60-upgrade-likewise-to-pbis", + "62-create-guest-templates", + "80-common-criteria", + "90-flush-pool-db", + "95-legacy-logrotate", + "99-remove-firstboot-flag", + ] + # check for firstboot issues + # FIXME: flaky, must check logs extraction on failure + try: + for stamp in STAMPS: + wait_for(lambda: pool.master.ssh(["test", "-e", f"{STAMPS_DIR}/{stamp}"], check=False, simple_output=False, ).returncode == 0, - f"Wait for ran-{service} stamp") - except TimeoutError: - logging.warning("investigating lack of %s service stamp", service) + f"Wait for {stamp} stamp") + except TimeoutError: + logging.warning("investigating lack of %s service stamp", stamp) + for service in SERVICES: out = pool.master.ssh(["systemctl", "status", service], check=False) logging.warning("service status: %s", out) out = pool.master.ssh(["grep", "-r", service, "/var/log"], check=False) logging.warning("in logs: %s", out) - raise + raise #wait_for(lambda: False, 'Wait "forever"', timeout_secs=100*60) logging.info("Powering off pool master") @@ -204,6 +236,8 @@ def test_firstboot(self, create_vms, @pytest.mark.parametrize(("orig_version", "iso_version"), [ #("83b2", "83b2"), # 8.3b2 disabled the upgrade from 8.3 ("821.1", "83b2"), + ("81", "83b2"), + ("80", "83b2"), ("ch821.1", "83b2"), ("821.1", "821.1"), ]) @@ -232,6 +266,8 @@ def test_upgrade(self, create_vms, iso_remaster, @pytest.mark.usefixtures("xcpng_chained") @pytest.mark.parametrize(("orig_version", "iso_version"), [ ("821.1-83b2", "83b2"), + ("80-83b2", "83b2"), + ("81-83b2", "83b2"), ("ch821.1-83b2", "83b2"), ]) @pytest.mark.parametrize("firmware", ("uefi", "bios")) From 4ebf89a29c61d7501e55e1f918f8595d9f00c5aa Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Fri, 14 Jun 2024 13:57:10 +0200 Subject: [PATCH 57/75] install: 7.5 and 7.6 --- data.py-dist | 2 ++ tests/install/test.py | 20 +++++++++++++++++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/data.py-dist b/data.py-dist index 57b83454..a6fa5ccd 100644 --- a/data.py-dist +++ b/data.py-dist @@ -72,6 +72,8 @@ ISO_IMAGES = { # 'xcpng-8.2.0': {'path': "/home/user/iso/xcp-ng-8.2.0.iso"}, # 'xcpng-8.1': {'path': "/home/user/iso/xcp-ng-8.1.0-2.iso"}, # 'xcpng-8.0': {'path': "/home/user/iso/xcp-ng-8.0.0.iso"}, +# 'xcpng-7.6': {'path': "/home/user/iso/xcp-ng-7.6.0.iso"}, +# 'xcpng-7.5': {'path': "/home/user/iso/xcp-ng-7.5.0-2.iso"}, # 'ch-8.2.1': {'path': "/home/user/iso/CitrixHypervisor-8.2.1-install-cd.iso"}, # 'ch-8.2.1-23': {'path': "/home/user/iso/CitrixHypervisor-8.2.1-2306-install-cd.iso"}, # 'xs8-2024-03': {'path': "/home/user/iso/XenServer8_2024-03-18.iso"}, diff --git a/tests/install/test.py b/tests/install/test.py index 370a0fd5..3617ecbe 100644 --- a/tests/install/test.py +++ b/tests/install/test.py @@ -15,7 +15,7 @@ class TestNested: @pytest.mark.parametrize("iso_version", ( "83b2", "821.1", - "81", "80", + "81", "80", "76", "75", "xs8", "ch821.1", )) @pytest.mark.parametrize("firmware", ("uefi", "bios")) @@ -47,6 +47,8 @@ class TestNested: "821.1": "xcpng-8.2.1-2023", "81": "xcpng-8.1", "80": "xcpng-8.0", + "76": "xcpng-7.6", + "75": "xcpng-7.5", "xs8": "xs8-2024-03", "ch821.1": "ch-8.2.1-23", }[version], @@ -72,11 +74,14 @@ def test_install(self, create_vms, iso_remaster, "821.1-83b2-83b2", "81-83b2", "81-83b2-83b2", "80-83b2", "80-83b2-83b2", + "76-83b2", "76-83b2-83b2", + "75-83b2", "75-83b2-83b2", "ch821.1-83b2", "ch821.1-83b2-83b2", "821.1", "821.1-821.1", "81", "80", + "76", "75", "ch821.1", "xs8", )) @pytest.mark.parametrize("firmware", ("uefi", "bios")) @@ -114,6 +119,8 @@ def test_firstboot(self, create_vms, expected_rel = { "ch821.1": "8.2.1", "xs8": "8.4.0", + "75": "7.5.0", + "76": "7.6.0", "80": "8.0.0", "81": "8.1.0", "821.1": "8.2.1", @@ -168,7 +175,7 @@ def test_firstboot(self, create_vms, ] STAMPS_DIR = "/var/lib/misc" STAMPS = [f"ran-{service}" for service in SERVICES] - elif lsb_rel in ["8.0.0", "8.1.0"]: + elif lsb_rel in ["7.5.0", "7.6.0", "8.0.0", "8.1.0"]: SERVICES = ["xs-firstboot"] STAMPS_DIR = "/etc/firstboot.d/state" STAMPS = [ @@ -182,11 +189,14 @@ def test_firstboot(self, create_vms, "60-import-keys", "60-upgrade-likewise-to-pbis", "62-create-guest-templates", - "80-common-criteria", "90-flush-pool-db", "95-legacy-logrotate", "99-remove-firstboot-flag", ] + if lsb_rel in ["8.0.0", "8.1.0"]: + STAMPS += [ + "80-common-criteria", + ] # check for firstboot issues # FIXME: flaky, must check logs extraction on failure try: @@ -238,6 +248,8 @@ def test_firstboot(self, create_vms, ("821.1", "83b2"), ("81", "83b2"), ("80", "83b2"), + ("76", "83b2"), + ("75", "83b2"), ("ch821.1", "83b2"), ("821.1", "821.1"), ]) @@ -266,6 +278,8 @@ def test_upgrade(self, create_vms, iso_remaster, @pytest.mark.usefixtures("xcpng_chained") @pytest.mark.parametrize(("orig_version", "iso_version"), [ ("821.1-83b2", "83b2"), + ("75-83b2", "83b2"), + ("76-83b2", "83b2"), ("80-83b2", "83b2"), ("81-83b2", "83b2"), ("ch821.1-83b2", "83b2"), From b44ecf4906fc6060c2626820737b77ce97acffd8 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Tue, 2 Jul 2024 11:43:53 +0200 Subject: [PATCH 58/75] WIP Separate firstboot of install and that of upgrade/restore We want to be able to change the resulting host UUID after install, and thus to produce different hosts based on a new parameter, but all of this only after initial install. FIXME: restore is even more different Signed-off-by: Yann Dirson --- tests/install/test-sequences/82-83-bios.lst | 6 +- tests/install/test-sequences/82-83-uefi.lst | 6 +- tests/install/test-sequences/83-bios.lst | 2 +- tests/install/test-sequences/83-uefi.lst | 2 +- tests/install/test.py | 84 ++++++++++++--------- 5 files changed, 57 insertions(+), 43 deletions(-) diff --git a/tests/install/test-sequences/82-83-bios.lst b/tests/install/test-sequences/82-83-bios.lst index d7562855..74417498 100644 --- a/tests/install/test-sequences/82-83-bios.lst +++ b/tests/install/test-sequences/82-83-bios.lst @@ -1,6 +1,6 @@ tests/install/test.py::TestNested::test_install[bios-821.1] -tests/install/test.py::TestNested::test_firstboot[bios-821.1] +tests/install/test.py::TestNested::test_firstboot_install[bios-821.1] tests/install/test.py::TestNested::test_upgrade[bios-821.1-83b2] -tests/install/test.py::TestNested::test_firstboot[bios-821.1-83b2] +tests/install/test.py::TestNested::test_firstboot_noninst[bios-821.1-83b2] tests/install/test.py::TestNested::test_restore[bios-821.1-83b2-83b2] -tests/install/test.py::TestNested::test_firstboot[bios-821.1-83b2-83b2] +tests/install/test.py::TestNested::test_firstboot_noninst[bios-821.1-83b2-83b2] diff --git a/tests/install/test-sequences/82-83-uefi.lst b/tests/install/test-sequences/82-83-uefi.lst index a716e633..6bff41b7 100644 --- a/tests/install/test-sequences/82-83-uefi.lst +++ b/tests/install/test-sequences/82-83-uefi.lst @@ -1,6 +1,6 @@ tests/install/test.py::TestNested::test_install[uefi-821.1] -tests/install/test.py::TestNested::test_firstboot[uefi-821.1] +tests/install/test.py::TestNested::test_firstboot_install[uefi-821.1] tests/install/test.py::TestNested::test_upgrade[uefi-821.1-83b2] -tests/install/test.py::TestNested::test_firstboot[uefi-821.1-83b2] +tests/install/test.py::TestNested::test_firstboot_noninst[uefi-821.1-83b2] tests/install/test.py::TestNested::test_restore[uefi-821.1-83b2-83b2] -tests/install/test.py::TestNested::test_firstboot[uefi-821.1-83b2-83b2] +tests/install/test.py::TestNested::test_firstboot_noninst[uefi-821.1-83b2-83b2] diff --git a/tests/install/test-sequences/83-bios.lst b/tests/install/test-sequences/83-bios.lst index 47fbcceb..a7ff3c7a 100644 --- a/tests/install/test-sequences/83-bios.lst +++ b/tests/install/test-sequences/83-bios.lst @@ -1,2 +1,2 @@ tests/install/test.py::TestNested::test_install[bios-83b2] -tests/install/test.py::TestNested::test_firstboot[bios-83b2] +tests/install/test.py::TestNested::test_firstboot_install[bios-83b2] diff --git a/tests/install/test-sequences/83-uefi.lst b/tests/install/test-sequences/83-uefi.lst index 2e75c5ed..b232e6ed 100644 --- a/tests/install/test-sequences/83-uefi.lst +++ b/tests/install/test-sequences/83-uefi.lst @@ -1,2 +1,2 @@ tests/install/test.py::TestNested::test_install[uefi-83b2] -tests/install/test.py::TestNested::test_firstboot[uefi-83b2] +tests/install/test.py::TestNested::test_firstboot_install[uefi-83b2] diff --git a/tests/install/test.py b/tests/install/test.py index 3617ecbe..0942db1a 100644 --- a/tests/install/test.py +++ b/tests/install/test.py @@ -65,39 +65,7 @@ def test_install(self, create_vms, iso_remaster, assert len(create_vms) == 1 installer.perform_install(iso=iso_remaster, host_vm=create_vms[0]) - - @pytest.mark.usefixtures("xcpng_chained") - @pytest.mark.parametrize("mode", ( - "83b2", - #"83b2-83b2", # 8.3b2 disabled the upgrade from 8.3 - "821.1-83b2", - "821.1-83b2-83b2", - "81-83b2", "81-83b2-83b2", - "80-83b2", "80-83b2-83b2", - "76-83b2", "76-83b2-83b2", - "75-83b2", "75-83b2-83b2", - "ch821.1-83b2", - "ch821.1-83b2-83b2", - "821.1", - "821.1-821.1", - "81", "80", - "76", "75", - "ch821.1", "xs8", - )) - @pytest.mark.parametrize("firmware", ("uefi", "bios")) - @pytest.mark.continuation_of(lambda params, firmware: [dict( - vm="vm1", - image_test=(f"TestNested::{{}}[{firmware}-{params}]".format( - { - 1: "test_install", - 2: "test_upgrade", - 3: "test_restore", - }[len(params.split("-"))] - )))], - param_mapping={"params": "mode", "firmware": "firmware"}) - def test_firstboot(self, create_vms, - firmware, mode): - host_vm = create_vms[0] + def _test_firstboot(self, host_vm, mode): vif = host_vm.vifs()[0] mac_address = vif.param_get('MAC') logging.info("Host VM has MAC %s", mac_address) @@ -242,6 +210,52 @@ def test_firstboot(self, create_vms, host_vm.shutdown(force=True) raise + @pytest.mark.usefixtures("xcpng_chained") + @pytest.mark.parametrize("version", ( + "83b2", + "821.1", + "81", "80", + "76", "75", + "ch821.1", "xs8", + )) + @pytest.mark.parametrize("firmware", ("uefi", "bios")) + @pytest.mark.continuation_of(lambda version, firmware: [dict( + vm="vm1", + image_test=f"TestNested::test_install[{firmware}-{version}]")], + param_mapping={"version": "version", "firmware": "firmware"}) + def test_firstboot_install(self, create_vms, + firmware, version): + host_vm = create_vms[0] + self._test_firstboot(host_vm, version) + + @pytest.mark.usefixtures("xcpng_chained") + @pytest.mark.parametrize("mode", ( + #"83b2-83b2", # 8.3b2 disabled the upgrade from 8.3 + "821.1-83b2", + "821.1-83b2-83b2", + "81-83b2", "81-83b2-83b2", + "80-83b2", "80-83b2-83b2", + "76-83b2", "76-83b2-83b2", + "75-83b2", "75-83b2-83b2", + "ch821.1-83b2", + "ch821.1-83b2-83b2", + "821.1-821.1", + )) + @pytest.mark.parametrize("firmware", ("uefi", "bios")) + @pytest.mark.continuation_of(lambda params, firmware: [dict( + vm="vm1", + image_test=(f"TestNested::{{}}[{firmware}-{params}]".format( + { + 2: "test_upgrade", + 3: "test_restore", + }[len(params.split("-"))] + )))], + param_mapping={"params": "mode", "firmware": "firmware"}) + def test_firstboot_noninst(self, create_vms, + firmware, mode): + host_vm = create_vms[0] + self._test_firstboot(host_vm, mode) + @pytest.mark.usefixtures("xcpng_chained") @pytest.mark.parametrize(("orig_version", "iso_version"), [ #("83b2", "83b2"), # 8.3b2 disabled the upgrade from 8.3 @@ -256,7 +270,7 @@ def test_firstboot(self, create_vms, @pytest.mark.parametrize("firmware", ("uefi", "bios")) @pytest.mark.continuation_of(lambda firmware, params: [dict( vm="vm1", - image_test=f"TestNested::test_firstboot[{firmware}-{params}]")], + image_test=f"TestNested::test_firstboot_install[{firmware}-{params}]")], param_mapping={"params": "orig_version", "firmware": "firmware"}) @pytest.mark.installer_iso( lambda version: { @@ -287,7 +301,7 @@ def test_upgrade(self, create_vms, iso_remaster, @pytest.mark.parametrize("firmware", ("uefi", "bios")) @pytest.mark.continuation_of(lambda firmware, params: [dict( vm="vm1", - image_test=f"TestNested::test_firstboot[{firmware}-{params}]")], + image_test=f"TestNested::test_firstboot_noninst[{firmware}-{params}]")], param_mapping={"params": "orig_version", "firmware": "firmware"}) @pytest.mark.installer_iso( lambda version: { From 97cff7b921aff49222e22cf2b082faf22c9933b9 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Mon, 1 Jul 2024 18:36:52 +0200 Subject: [PATCH 59/75] Add a first-boot service to make UUIDs unique The installer generates UUID for host and its dom0 in xensource-inventory, so the result of a costly install test could not be reused for multiple hosts in a same pool. This service will be run once and before xapi ever starts, to override those UUIDs with brand new random ones during firstboot. --- tests/install/conftest.py | 18 ++++++++++++++++++ tests/install/test.py | 2 ++ 2 files changed, 20 insertions(+) diff --git a/tests/install/conftest.py b/tests/install/conftest.py index 2f51ddde..7553662a 100644 --- a/tests/install/conftest.py +++ b/tests/install/conftest.py @@ -46,6 +46,8 @@ def iso_remaster(request, answerfile): param_mapping = marker.kwargs.get("param_mapping", {}) iso_key = callable_marker(marker.args[0], request, param_mapping=param_mapping) + gen_unique_uuid = marker.kwargs.get("gen_unique_uuid", False) + from data import ISO_IMAGES, ISOSR_SRV, ISOSR_PATH, PXE_CONFIG_SERVER, TEST_SSH_PUBKEY, TOOLS assert "iso-remaster" in TOOLS iso_remaster = TOOLS["iso-remaster"] @@ -117,6 +119,17 @@ def iso_remaster(request, answerfile): systemctl --root="$INSTALLIMG" enable test-pingpxe.service +cat > "$INSTALLIMG/root/test-unique-uuids.service" < "$INSTALLIMG/root/postinstall.sh" <> "\\$ROOT/root/.ssh/authorized_keys" EOF diff --git a/tests/install/test.py b/tests/install/test.py index 0942db1a..82757487 100644 --- a/tests/install/test.py +++ b/tests/install/test.py @@ -52,6 +52,7 @@ class TestNested: "xs8": "xs8-2024-03", "ch821.1": "ch-8.2.1-23", }[version], + gen_unique_uuid=True, param_mapping={"version": "iso_version"}) @pytest.mark.answerfile(lambda firmware: AnswerFile("INSTALL") \ .top_append( @@ -130,6 +131,7 @@ def _test_firstboot(self, host_vm, mode): # pool master must be reachable here pool = Pool(host_vm.ip) + logging.info("Host uuid: %s", pool.master.uuid) # wait for XAPI wait_for(pool.master.is_enabled, "Wait for XAPI to be ready", timeout_secs=30 * 60) From 9f0532262bae82efc82c7bda4cd31f02bbfb4c43 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Tue, 2 Jul 2024 17:28:36 +0200 Subject: [PATCH 60/75] WIP Replace 8.3beta2 tests with upcoming 8.3rc1 beta2 did not have support for upgrading from 8.3, so more combos get activated. FIXME: test upgrade from beta2? --- data.py-dist | 3 +- tests/install/test-sequences/82-83-bios.lst | 8 +-- tests/install/test-sequences/82-83-uefi.lst | 8 +-- tests/install/test-sequences/83-bios.lst | 8 ++- tests/install/test-sequences/83-uefi.lst | 8 ++- tests/install/test.py | 56 ++++++++++++--------- 6 files changed, 53 insertions(+), 38 deletions(-) diff --git a/data.py-dist b/data.py-dist index a6fa5ccd..646914d0 100644 --- a/data.py-dist +++ b/data.py-dist @@ -65,8 +65,9 @@ VM_IMAGES = { # FIXME: use URLs and an optional cache? ISO_IMAGES = { +# 'xcpng-8.3-rc1': {'path': "/home/user/iso/xcp-ng-8.3.0-rc1.iso"}, +# 'xcpng-8.3-rc1-net': {'path': "/home/user/iso/xcp-ng-8.3.0-rc1-netinstall.iso"}, # 'xcpng-8.3-beta2': {'path': "/home/user/iso/xcp-ng-8.3.0-beta2.iso"}, -# 'xcpng-8.3-beta2-net': {'path': "/home/user/iso/xcp-ng-8.3.0-beta2-netinstall.iso"}, # 'xcpng-8.2.1-2023': {'path': "/home/user/iso/xcp-ng-8.2.1-20231130.iso"}, # 'xcpng-8.2.1': {'path': "/home/user/iso/xcp-ng-8.2.1.iso"}, # 'xcpng-8.2.0': {'path': "/home/user/iso/xcp-ng-8.2.0.iso"}, diff --git a/tests/install/test-sequences/82-83-bios.lst b/tests/install/test-sequences/82-83-bios.lst index 74417498..47fd2dc5 100644 --- a/tests/install/test-sequences/82-83-bios.lst +++ b/tests/install/test-sequences/82-83-bios.lst @@ -1,6 +1,6 @@ tests/install/test.py::TestNested::test_install[bios-821.1] tests/install/test.py::TestNested::test_firstboot_install[bios-821.1] -tests/install/test.py::TestNested::test_upgrade[bios-821.1-83b2] -tests/install/test.py::TestNested::test_firstboot_noninst[bios-821.1-83b2] -tests/install/test.py::TestNested::test_restore[bios-821.1-83b2-83b2] -tests/install/test.py::TestNested::test_firstboot_noninst[bios-821.1-83b2-83b2] +tests/install/test.py::TestNested::test_upgrade[bios-821.1-83rc1] +tests/install/test.py::TestNested::test_firstboot_noninst[bios-821.1-83rc1] +tests/install/test.py::TestNested::test_restore[bios-821.1-83rc1-83rc1] +tests/install/test.py::TestNested::test_firstboot_noninst[bios-821.1-83rc1-83rc1] diff --git a/tests/install/test-sequences/82-83-uefi.lst b/tests/install/test-sequences/82-83-uefi.lst index 6bff41b7..83ce5f8d 100644 --- a/tests/install/test-sequences/82-83-uefi.lst +++ b/tests/install/test-sequences/82-83-uefi.lst @@ -1,6 +1,6 @@ tests/install/test.py::TestNested::test_install[uefi-821.1] tests/install/test.py::TestNested::test_firstboot_install[uefi-821.1] -tests/install/test.py::TestNested::test_upgrade[uefi-821.1-83b2] -tests/install/test.py::TestNested::test_firstboot_noninst[uefi-821.1-83b2] -tests/install/test.py::TestNested::test_restore[uefi-821.1-83b2-83b2] -tests/install/test.py::TestNested::test_firstboot_noninst[uefi-821.1-83b2-83b2] +tests/install/test.py::TestNested::test_upgrade[uefi-821.1-83rc1] +tests/install/test.py::TestNested::test_firstboot_noninst[uefi-821.1-83rc1] +tests/install/test.py::TestNested::test_restore[uefi-821.1-83rc1-83rc1] +tests/install/test.py::TestNested::test_firstboot_noninst[uefi-821.1-83rc1-83rc1] diff --git a/tests/install/test-sequences/83-bios.lst b/tests/install/test-sequences/83-bios.lst index a7ff3c7a..37e6089c 100644 --- a/tests/install/test-sequences/83-bios.lst +++ b/tests/install/test-sequences/83-bios.lst @@ -1,2 +1,6 @@ -tests/install/test.py::TestNested::test_install[bios-83b2] -tests/install/test.py::TestNested::test_firstboot_install[bios-83b2] +tests/install/test.py::TestNested::test_install[bios-83rc1] +tests/install/test.py::TestNested::test_firstboot_install[bios-83rc1] +tests/install/test.py::TestNested::test_upgrade[bios-83rc1-83rc1] +tests/install/test.py::TestNested::test_firstboot_noninst[bios-83rc1-83rc1] +tests/install/test.py::TestNested::test_restore[bios-83rc1-83rc1-83rc1] +tests/install/test.py::TestNested::test_firstboot_noninst[bios-83rc1-83rc1-83rc1] diff --git a/tests/install/test-sequences/83-uefi.lst b/tests/install/test-sequences/83-uefi.lst index b232e6ed..0c8df17a 100644 --- a/tests/install/test-sequences/83-uefi.lst +++ b/tests/install/test-sequences/83-uefi.lst @@ -1,2 +1,6 @@ -tests/install/test.py::TestNested::test_install[uefi-83b2] -tests/install/test.py::TestNested::test_firstboot_install[uefi-83b2] +tests/install/test.py::TestNested::test_install[uefi-83rc1] +tests/install/test.py::TestNested::test_firstboot_install[uefi-83rc1] +tests/install/test.py::TestNested::test_upgrade[uefi-83rc1-83rc1] +tests/install/test.py::TestNested::test_firstboot_noninst[uefi-83rc1-83rc1] +tests/install/test.py::TestNested::test_restore[uefi-83rc1-83rc1-83rc1] +tests/install/test.py::TestNested::test_firstboot_noninst[uefi-83rc1-83rc1-83rc1] diff --git a/tests/install/test.py b/tests/install/test.py index 82757487..550a4466 100644 --- a/tests/install/test.py +++ b/tests/install/test.py @@ -13,7 +13,7 @@ @pytest.mark.dependency() class TestNested: @pytest.mark.parametrize("iso_version", ( - "83b2", + "83rc1", "83b2", "821.1", "81", "80", "76", "75", "xs8", "ch821.1", @@ -43,6 +43,7 @@ class TestNested: param_mapping={"firmware": "firmware"}) @pytest.mark.installer_iso( lambda version: { + "83rc1": "xcpng-8.3-rc1", "83b2": "xcpng-8.3-beta2", "821.1": "xcpng-8.2.1-2023", "81": "xcpng-8.1", @@ -94,6 +95,7 @@ def _test_firstboot(self, host_vm, mode): "81": "8.1.0", "821.1": "8.2.1", "83b2": "8.3.0", + "83rc1": "8.3.0", }[expected_rel_id] try: @@ -214,6 +216,7 @@ def _test_firstboot(self, host_vm, mode): @pytest.mark.usefixtures("xcpng_chained") @pytest.mark.parametrize("version", ( + "83rc1", "83b2", "821.1", "81", "80", @@ -232,15 +235,16 @@ def test_firstboot_install(self, create_vms, @pytest.mark.usefixtures("xcpng_chained") @pytest.mark.parametrize("mode", ( - #"83b2-83b2", # 8.3b2 disabled the upgrade from 8.3 - "821.1-83b2", - "821.1-83b2-83b2", - "81-83b2", "81-83b2-83b2", - "80-83b2", "80-83b2-83b2", - "76-83b2", "76-83b2-83b2", - "75-83b2", "75-83b2-83b2", - "ch821.1-83b2", - "ch821.1-83b2-83b2", + "83rc1-83rc1", "83rc1-83rc1-83rc1", + "83b2-83rc1", + "821.1-83rc1", + "821.1-83rc1-83rc1", + "81-83rc1", "81-83rc1-83rc1", + "80-83rc1", "80-83rc1-83rc1", + "76-83rc1", "76-83rc1-83rc1", + "75-83rc1", "75-83rc1-83rc1", + "ch821.1-83rc1", + "ch821.1-83rc1-83rc1", "821.1-821.1", )) @pytest.mark.parametrize("firmware", ("uefi", "bios")) @@ -260,13 +264,14 @@ def test_firstboot_noninst(self, create_vms, @pytest.mark.usefixtures("xcpng_chained") @pytest.mark.parametrize(("orig_version", "iso_version"), [ - #("83b2", "83b2"), # 8.3b2 disabled the upgrade from 8.3 - ("821.1", "83b2"), - ("81", "83b2"), - ("80", "83b2"), - ("76", "83b2"), - ("75", "83b2"), - ("ch821.1", "83b2"), + ("83rc1", "83rc1"), + ("83b2", "83rc1"), + ("821.1", "83rc1"), + ("81", "83rc1"), + ("80", "83rc1"), + ("76", "83rc1"), + ("75", "83rc1"), + ("ch821.1", "83rc1"), ("821.1", "821.1"), ]) @pytest.mark.parametrize("firmware", ("uefi", "bios")) @@ -277,7 +282,7 @@ def test_firstboot_noninst(self, create_vms, @pytest.mark.installer_iso( lambda version: { "821.1": "xcpng-8.2.1-2023", - "83b2": "xcpng-8.3-beta2", + "83rc1": "xcpng-8.3-rc1", }[version], param_mapping={"version": "iso_version"}) @pytest.mark.answerfile( @@ -293,12 +298,13 @@ def test_upgrade(self, create_vms, iso_remaster, @pytest.mark.usefixtures("xcpng_chained") @pytest.mark.parametrize(("orig_version", "iso_version"), [ - ("821.1-83b2", "83b2"), - ("75-83b2", "83b2"), - ("76-83b2", "83b2"), - ("80-83b2", "83b2"), - ("81-83b2", "83b2"), - ("ch821.1-83b2", "83b2"), + ("83rc1-83rc1", "83rc1"), + ("821.1-83rc1", "83rc1"), + ("75-83rc1", "83rc1"), + ("76-83rc1", "83rc1"), + ("80-83rc1", "83rc1"), + ("81-83rc1", "83rc1"), + ("ch821.1-83rc1", "83rc1"), ]) @pytest.mark.parametrize("firmware", ("uefi", "bios")) @pytest.mark.continuation_of(lambda firmware, params: [dict( @@ -308,7 +314,7 @@ def test_upgrade(self, create_vms, iso_remaster, @pytest.mark.installer_iso( lambda version: { "821.1": "xcpng-8.2.1-2023", - "83b2": "xcpng-8.3-beta2", + "83rc1": "xcpng-8.3-rc1", }[version], param_mapping={"version": "iso_version"}) @pytest.mark.answerfile(lambda firmware: AnswerFile("RESTORE").top_append( From 3e11018706d2fd0130f206228da3eca81cde24e4 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Wed, 3 Jul 2024 17:17:04 +0200 Subject: [PATCH 61/75] install: produce several hosts from single install This is just base infra, different machines have not differences yet. --- tests/install/test-sequences/82-83-bios.lst | 2 +- tests/install/test-sequences/82-83-uefi.lst | 2 +- tests/install/test-sequences/83-bios.lst | 2 +- tests/install/test-sequences/83-uefi.lst | 2 +- tests/install/test.py | 5 +++-- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/install/test-sequences/82-83-bios.lst b/tests/install/test-sequences/82-83-bios.lst index 47fd2dc5..4314fbd9 100644 --- a/tests/install/test-sequences/82-83-bios.lst +++ b/tests/install/test-sequences/82-83-bios.lst @@ -1,5 +1,5 @@ tests/install/test.py::TestNested::test_install[bios-821.1] -tests/install/test.py::TestNested::test_firstboot_install[bios-821.1] +tests/install/test.py::TestNested::test_firstboot_install[bios-821.1-host1] tests/install/test.py::TestNested::test_upgrade[bios-821.1-83rc1] tests/install/test.py::TestNested::test_firstboot_noninst[bios-821.1-83rc1] tests/install/test.py::TestNested::test_restore[bios-821.1-83rc1-83rc1] diff --git a/tests/install/test-sequences/82-83-uefi.lst b/tests/install/test-sequences/82-83-uefi.lst index 83ce5f8d..8eee04ca 100644 --- a/tests/install/test-sequences/82-83-uefi.lst +++ b/tests/install/test-sequences/82-83-uefi.lst @@ -1,5 +1,5 @@ tests/install/test.py::TestNested::test_install[uefi-821.1] -tests/install/test.py::TestNested::test_firstboot_install[uefi-821.1] +tests/install/test.py::TestNested::test_firstboot_install[uefi-821.1-host1] tests/install/test.py::TestNested::test_upgrade[uefi-821.1-83rc1] tests/install/test.py::TestNested::test_firstboot_noninst[uefi-821.1-83rc1] tests/install/test.py::TestNested::test_restore[uefi-821.1-83rc1-83rc1] diff --git a/tests/install/test-sequences/83-bios.lst b/tests/install/test-sequences/83-bios.lst index 37e6089c..51f6ed80 100644 --- a/tests/install/test-sequences/83-bios.lst +++ b/tests/install/test-sequences/83-bios.lst @@ -1,5 +1,5 @@ tests/install/test.py::TestNested::test_install[bios-83rc1] -tests/install/test.py::TestNested::test_firstboot_install[bios-83rc1] +tests/install/test.py::TestNested::test_firstboot_install[bios-83rc1-host1] tests/install/test.py::TestNested::test_upgrade[bios-83rc1-83rc1] tests/install/test.py::TestNested::test_firstboot_noninst[bios-83rc1-83rc1] tests/install/test.py::TestNested::test_restore[bios-83rc1-83rc1-83rc1] diff --git a/tests/install/test-sequences/83-uefi.lst b/tests/install/test-sequences/83-uefi.lst index 0c8df17a..6dfe5744 100644 --- a/tests/install/test-sequences/83-uefi.lst +++ b/tests/install/test-sequences/83-uefi.lst @@ -1,5 +1,5 @@ tests/install/test.py::TestNested::test_install[uefi-83rc1] -tests/install/test.py::TestNested::test_firstboot_install[uefi-83rc1] +tests/install/test.py::TestNested::test_firstboot_install[uefi-83rc1-host1] tests/install/test.py::TestNested::test_upgrade[uefi-83rc1-83rc1] tests/install/test.py::TestNested::test_firstboot_noninst[uefi-83rc1-83rc1] tests/install/test.py::TestNested::test_restore[uefi-83rc1-83rc1-83rc1] diff --git a/tests/install/test.py b/tests/install/test.py index 550a4466..da31ea7d 100644 --- a/tests/install/test.py +++ b/tests/install/test.py @@ -215,6 +215,7 @@ def _test_firstboot(self, host_vm, mode): raise @pytest.mark.usefixtures("xcpng_chained") + @pytest.mark.parametrize("machine", ("host1", "host2")) @pytest.mark.parametrize("version", ( "83rc1", "83b2", @@ -229,7 +230,7 @@ def _test_firstboot(self, host_vm, mode): image_test=f"TestNested::test_install[{firmware}-{version}]")], param_mapping={"version": "version", "firmware": "firmware"}) def test_firstboot_install(self, create_vms, - firmware, version): + firmware, version, machine): host_vm = create_vms[0] self._test_firstboot(host_vm, version) @@ -277,7 +278,7 @@ def test_firstboot_noninst(self, create_vms, @pytest.mark.parametrize("firmware", ("uefi", "bios")) @pytest.mark.continuation_of(lambda firmware, params: [dict( vm="vm1", - image_test=f"TestNested::test_firstboot_install[{firmware}-{params}]")], + image_test=f"TestNested::test_firstboot_install[{firmware}-{params}-host1]")], param_mapping={"params": "orig_version", "firmware": "firmware"}) @pytest.mark.installer_iso( lambda version: { From 7235bca8307af52aaa865a2578f5933b6c1b4d01 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Thu, 8 Aug 2024 18:39:51 +0200 Subject: [PATCH 62/75] install: adjust host IP and name in firstboot data before booting Uses a helper VM to modify firstboot data in installed disk --- tests/install/test-sequences/82-83-bios.lst | 1 + tests/install/test-sequences/82-83-uefi.lst | 1 + tests/install/test-sequences/83-bios.lst | 1 + tests/install/test-sequences/83-uefi.lst | 1 + tests/install/test.py | 91 +++++++++++++++++---- 5 files changed, 79 insertions(+), 16 deletions(-) diff --git a/tests/install/test-sequences/82-83-bios.lst b/tests/install/test-sequences/82-83-bios.lst index 4314fbd9..da749201 100644 --- a/tests/install/test-sequences/82-83-bios.lst +++ b/tests/install/test-sequences/82-83-bios.lst @@ -1,4 +1,5 @@ tests/install/test.py::TestNested::test_install[bios-821.1] +tests/install/test.py::TestNested::test_tune_firstboot[None-bios-821.1-host1] tests/install/test.py::TestNested::test_firstboot_install[bios-821.1-host1] tests/install/test.py::TestNested::test_upgrade[bios-821.1-83rc1] tests/install/test.py::TestNested::test_firstboot_noninst[bios-821.1-83rc1] diff --git a/tests/install/test-sequences/82-83-uefi.lst b/tests/install/test-sequences/82-83-uefi.lst index 8eee04ca..40942a12 100644 --- a/tests/install/test-sequences/82-83-uefi.lst +++ b/tests/install/test-sequences/82-83-uefi.lst @@ -1,4 +1,5 @@ tests/install/test.py::TestNested::test_install[uefi-821.1] +tests/install/test.py::TestNested::test_tune_firstboot[None-uefi-821.1-host1] tests/install/test.py::TestNested::test_firstboot_install[uefi-821.1-host1] tests/install/test.py::TestNested::test_upgrade[uefi-821.1-83rc1] tests/install/test.py::TestNested::test_firstboot_noninst[uefi-821.1-83rc1] diff --git a/tests/install/test-sequences/83-bios.lst b/tests/install/test-sequences/83-bios.lst index 51f6ed80..93161b64 100644 --- a/tests/install/test-sequences/83-bios.lst +++ b/tests/install/test-sequences/83-bios.lst @@ -1,4 +1,5 @@ tests/install/test.py::TestNested::test_install[bios-83rc1] +tests/install/test.py::TestNested::test_tune_firstboot[None-bios-83rc1-host1] tests/install/test.py::TestNested::test_firstboot_install[bios-83rc1-host1] tests/install/test.py::TestNested::test_upgrade[bios-83rc1-83rc1] tests/install/test.py::TestNested::test_firstboot_noninst[bios-83rc1-83rc1] diff --git a/tests/install/test-sequences/83-uefi.lst b/tests/install/test-sequences/83-uefi.lst index 6dfe5744..7280abaa 100644 --- a/tests/install/test-sequences/83-uefi.lst +++ b/tests/install/test-sequences/83-uefi.lst @@ -1,4 +1,5 @@ tests/install/test.py::TestNested::test_install[uefi-83rc1] +tests/install/test.py::TestNested::test_tune_firstboot[None-uefi-83rc1-host1] tests/install/test.py::TestNested::test_firstboot_install[uefi-83rc1-host1] tests/install/test.py::TestNested::test_upgrade[uefi-83rc1-83rc1] tests/install/test.py::TestNested::test_firstboot_noninst[uefi-83rc1-83rc1] diff --git a/tests/install/test.py b/tests/install/test.py index da31ea7d..65834c5d 100644 --- a/tests/install/test.py +++ b/tests/install/test.py @@ -3,12 +3,15 @@ import time from lib import commands, installer, pxe -from lib.common import wait_for +from lib.common import safe_split, wait_for from lib.installer import AnswerFile +from lib.pif import PIF from lib.pool import Pool +from lib.vdi import VDI -from data import NETWORKS +from data import HOSTS_IP_CONFIG, NETWORKS assert "MGMT" in NETWORKS +assert "HOSTS" in HOSTS_IP_CONFIG @pytest.mark.dependency() class TestNested: @@ -67,7 +70,67 @@ def test_install(self, create_vms, iso_remaster, assert len(create_vms) == 1 installer.perform_install(iso=iso_remaster, host_vm=create_vms[0]) - def _test_firstboot(self, host_vm, mode): + @pytest.fixture + @staticmethod + def helper_vm_with_plugged_disk(imported_vm, create_vms): + helper_vm = imported_vm + host_vm, = create_vms + + helper_vm.start() + helper_vm.wait_for_vm_running_and_ssh_up() + + all_vdis = [VDI(uuid, host=host_vm.host) for uuid in host_vm.vdi_uuids()] + disk_vdis = [vdi for vdi in all_vdis if not vdi.readonly()] + vdi, = disk_vdis + + vbd = helper_vm.create_vbd("1", vdi.uuid) + try: + vbd.plug() + + yield helper_vm + + finally: + vbd.unplug() + vbd.destroy() + + @pytest.mark.usefixtures("xcpng_chained") + @pytest.mark.parametrize("local_sr", ("nosr", "ext", "lvm")) + @pytest.mark.parametrize("machine", ("host1", "host2")) + @pytest.mark.parametrize("version", ( + "83rc1", + "83b2", + "821.1", + "81", "80", + "76", "75", + "ch821.1", "xs8", + )) + @pytest.mark.parametrize("firmware", ("uefi", "bios")) + @pytest.mark.continuation_of(lambda version, firmware, local_sr: [dict( + vm="vm1", + image_test=f"TestNested::test_install[{firmware}-{version}-{local_sr}]")], + param_mapping={"version": "version", "firmware": "firmware", + "local_sr": "local_sr"}) + @pytest.mark.small_vm + def test_tune_firstboot(self, create_vms, helper_vm_with_plugged_disk, + firmware, version, machine, local_sr): + helper_vm = helper_vm_with_plugged_disk + + helper_vm.ssh(["mount /dev/xvdb1 /mnt"]) + try: + # hostname + logging.info("Setting hostname to %r", machine) + helper_vm.ssh(["echo > /mnt/etc/hostname", machine]) + # management IP + if machine in HOSTS_IP_CONFIG['HOSTS']: + ip = HOSTS_IP_CONFIG['HOSTS'][machine] + logging.info("Changing IP to %s", ip) + + helper_vm.ssh([f"sed -i s/^IP=.*/IP='{ip}'/", + "/mnt/etc/firstboot.d/data/management.conf"]) + finally: + helper_vm.ssh(["umount /dev/xvdb1"]) + + def _test_firstboot(self, host_vm, mode, *, machine='DEFAULT'): vif = host_vm.vifs()[0] mac_address = vif.param_get('MAC') logging.info("Host VM has MAC %s", mac_address) @@ -99,18 +162,12 @@ def _test_firstboot(self, host_vm, mode): }[expected_rel_id] try: - # FIXME: evict MAC from ARP cache first? host_vm.start() wait_for(host_vm.is_running, "Wait for host VM running") - # catch host-vm IP address - wait_for(lambda: pxe.arp_addresses_for(mac_address), - "Wait for DHCP server to see Host VM in ARP tables", - timeout_secs=10*60) - ips = pxe.arp_addresses_for(mac_address) - logging.info("Host VM has IPs %s", ips) - assert len(ips) == 1 - host_vm.ip = ips[0] + host_vm.ip = HOSTS_IP_CONFIG['HOSTS'].get(machine, + HOSTS_IP_CONFIG['HOSTS']['DEFAULT']) + logging.info("Expecting host VM to have IP %s", host_vm.ip) wait_for( lambda: commands.local_cmd( @@ -225,14 +282,16 @@ def _test_firstboot(self, host_vm, mode): "ch821.1", "xs8", )) @pytest.mark.parametrize("firmware", ("uefi", "bios")) - @pytest.mark.continuation_of(lambda version, firmware: [dict( + @pytest.mark.continuation_of(lambda version, firmware, machine: [dict( vm="vm1", - image_test=f"TestNested::test_install[{firmware}-{version}]")], - param_mapping={"version": "version", "firmware": "firmware"}) + image_test=f"TestNested::test_tune_firstboot[None-{firmware}-{version}-{machine}]")], + param_mapping={"version": "version", "firmware": "firmware", + "machine": "machine"}) def test_firstboot_install(self, create_vms, firmware, version, machine): host_vm = create_vms[0] - self._test_firstboot(host_vm, version) + + self._test_firstboot(host_vm, version, machine=machine) @pytest.mark.usefixtures("xcpng_chained") @pytest.mark.parametrize("mode", ( From f18d98c3ae3dccd647006953e00f38988139b8a8 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Wed, 3 Jul 2024 18:32:32 +0200 Subject: [PATCH 63/75] New test: join 2 hosts into a pool Signed-off-by: Yann Dirson --- lib/host.py | 11 ++++ tests/install/test-sequences/82-83-rpu.lst | 4 ++ tests/install/test_pool.py | 68 ++++++++++++++++++++++ 3 files changed, 83 insertions(+) create mode 100644 tests/install/test-sequences/82-83-rpu.lst create mode 100644 tests/install/test_pool.py diff --git a/lib/host.py b/lib/host.py index 6a5543fb..6b44eb47 100644 --- a/lib/host.py +++ b/lib/host.py @@ -410,6 +410,17 @@ def yum_restore_saved_state(self): self.saved_packages_list = None self.saved_rollback_id = None + def shutdown(self, verify=False): + logging.info("Shutdown host %s" % self) + try: + self.ssh(['shutdown']) + except commands.SSHCommandFailed as e: + # ssh connection may get killed by the shutdown and terminate with an error code + if "closed by remote host" not in e.stdout: + raise + if verify: + wait_for_not(self.is_enabled, "Wait for host down") + def reboot(self, verify=False): logging.info("Reboot host %s" % self) try: diff --git a/tests/install/test-sequences/82-83-rpu.lst b/tests/install/test-sequences/82-83-rpu.lst new file mode 100644 index 00000000..fe762118 --- /dev/null +++ b/tests/install/test-sequences/82-83-rpu.lst @@ -0,0 +1,4 @@ +tests/install/test.py::TestNested::test_install[uefi-821.1] +tests/install/test.py::TestNested::test_firstboot_install[uefi-821.1-host1] +tests/install/test.py::TestNested::test_firstboot_install[uefi-821.1-host2] +tests/install/test_pool.py::test_join_pool[uefi-821.1] diff --git a/tests/install/test_pool.py b/tests/install/test_pool.py new file mode 100644 index 00000000..ca13dbd8 --- /dev/null +++ b/tests/install/test_pool.py @@ -0,0 +1,68 @@ +import logging +import os +import pytest + +from lib import pxe +from lib.common import wait_for +from lib.pool import Pool + +@pytest.mark.usefixtures("xcpng_chained") +@pytest.mark.parametrize("mode", ( + "821.1", +)) +@pytest.mark.parametrize("firmware", ("uefi", "bios")) +@pytest.mark.continuation_of(lambda params, firmware: [ + dict(vm="vm1", + image_test=f"tests/install/test.py::TestNested::test_firstboot_install[{firmware}-{params}-host1]", + scope="session"), + dict(vm="vm2", + image_vm="vm1", + image_test=f"tests/install/test.py::TestNested::test_firstboot_install[{firmware}-{params}-host2]", + scope="session"), +], + param_mapping={"params": "mode", "firmware": "firmware"}) +def test_join_pool(firmware, mode, create_vms): + (master_vm, slave_vm) = create_vms + master_mac = master_vm.vifs()[0].param_get('MAC') + logging.info("Master VM has MAC %s", master_mac) + slave_mac = slave_vm.vifs()[0].param_get('MAC') + logging.info("Slave VM has MAC %s", slave_mac) + + pxe.arp_clear_for(master_mac) + master_vm.start() + pxe.arp_clear_for(slave_mac) + slave_vm.start() + wait_for(master_vm.is_running, "Wait for master VM running") + wait_for(slave_vm.is_running, "Wait for slave VM running") + + # catch host-vm IP address + wait_for(lambda: pxe.arp_addresses_for(master_mac), + "Wait for DHCP server to see Master VM in ARP tables", + timeout_secs=10*60) + ips = pxe.arp_addresses_for(master_mac) + logging.info("Master VM has IPs %s", ips) + assert len(ips) == 1 + master_vm.ip = ips[0] + + wait_for(lambda: pxe.arp_addresses_for(slave_mac), + "Wait for DHCP server to see Slave VM in ARP tables", + timeout_secs=10*60) + ips = pxe.arp_addresses_for(slave_mac) + logging.info("Slave VM has IPs %s", ips) + assert len(ips) == 1 + slave_vm.ip = ips[0] + + wait_for(lambda: not os.system(f"nc -zw5 {master_vm.ip} 22"), + "Wait for ssh up on Master VM", retry_delay_secs=5) + wait_for(lambda: not os.system(f"nc -zw5 {slave_vm.ip} 22"), + "Wait for ssh up on Slave VM", retry_delay_secs=5) + + pool = Pool(master_vm.ip) + slave = Pool(slave_vm.ip).master + slave.join_pool(pool) + + slave.shutdown() + pool.master.shutdown() + + wait_for(lambda: slave_vm.is_halted(), "Wait for Slave VM to be halted") + wait_for(lambda: master_vm.is_halted(), "Wait for Master VM to be halted") From 47e4c025b56839e67673de270aad44c706a150de Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Thu, 4 Jul 2024 11:59:50 +0200 Subject: [PATCH 64/75] New test: create VMs on shared SR in pool --- lib/installer.py | 2 + lib/pxe.py | 7 ++ tests/install/test-sequences/82-83-rpu.lst | 2 +- tests/install/test_pool.py | 133 ++++++++++++++++++--- 4 files changed, 124 insertions(+), 20 deletions(-) diff --git a/lib/installer.py b/lib/installer.py index 5b6a225f..4e598391 100644 --- a/lib/installer.py +++ b/lib/installer.py @@ -175,6 +175,8 @@ def perform_upgrade(*, iso, host_vm): host_vm.insert_cd(iso) try: + pxe.arp_clear_for(mac_address) + host_vm.start() wait_for(host_vm.is_running, "Wait for host VM running") diff --git a/lib/pxe.py b/lib/pxe.py index 232efd5b..7a720521 100644 --- a/lib/pxe.py +++ b/lib/pxe.py @@ -48,3 +48,10 @@ def arp_addresses_for(mac_address): ) candidate_ips = output.splitlines() return candidate_ips + +def arp_clear_for(mac_address): + for stray_ip in arp_addresses_for(mac_address): + output = ssh( + PXE_CONFIG_SERVER, + ['arp', '-d', stray_ip] + ) diff --git a/tests/install/test-sequences/82-83-rpu.lst b/tests/install/test-sequences/82-83-rpu.lst index fe762118..9c97fa94 100644 --- a/tests/install/test-sequences/82-83-rpu.lst +++ b/tests/install/test-sequences/82-83-rpu.lst @@ -1,4 +1,4 @@ tests/install/test.py::TestNested::test_install[uefi-821.1] tests/install/test.py::TestNested::test_firstboot_install[uefi-821.1-host1] tests/install/test.py::TestNested::test_firstboot_install[uefi-821.1-host2] -tests/install/test_pool.py::test_join_pool[uefi-821.1] +tests/install/test_pool.py::test_pool_rpu[uefi-821.1-83rc1] diff --git a/tests/install/test_pool.py b/tests/install/test_pool.py index ca13dbd8..9298baaa 100644 --- a/tests/install/test_pool.py +++ b/tests/install/test_pool.py @@ -2,14 +2,19 @@ import os import pytest -from lib import pxe -from lib.common import wait_for +from lib import commands, installer, pxe +from lib.common import wait_for, vm_image +from lib.installer import AnswerFile from lib.pool import Pool +from data import HOSTS_IP_CONFIG, NFS_DEVICE_CONFIG + +# FIXME without --ignore-unknown-dependency, SKIPPED +# "because it depends on tests/install/test.py::TestNested::test_firstboot_install[uefi-821.1-host1]" @pytest.mark.usefixtures("xcpng_chained") -@pytest.mark.parametrize("mode", ( - "821.1", -)) +@pytest.mark.parametrize(("orig_version", "iso_version"), [ + ("821.1", "83rc1"), +]) @pytest.mark.parametrize("firmware", ("uefi", "bios")) @pytest.mark.continuation_of(lambda params, firmware: [ dict(vm="vm1", @@ -20,22 +25,93 @@ image_test=f"tests/install/test.py::TestNested::test_firstboot_install[{firmware}-{params}-host2]", scope="session"), ], - param_mapping={"params": "mode", "firmware": "firmware"}) -def test_join_pool(firmware, mode, create_vms): + param_mapping={"params": "orig_version", "firmware": "firmware"}) +@pytest.mark.installer_iso( + lambda version: { + "83rc1": "xcpng-8.3-rc1", + }[version], + param_mapping={"version": "iso_version"}) +@pytest.mark.answerfile( + lambda firmware: AnswerFile("UPGRADE").top_append( + {"TAG": "source", "type": "local"}, + {"TAG": "existing-installation", "CONTENTS": {"uefi": "nvme0n1", "bios": "sda"}[firmware]}, + ), + param_mapping={"firmware": "firmware"}) +def test_pool_rpu(firmware, orig_version, iso_version, iso_remaster, create_vms): (master_vm, slave_vm) = create_vms master_mac = master_vm.vifs()[0].param_get('MAC') logging.info("Master VM has MAC %s", master_mac) slave_mac = slave_vm.vifs()[0].param_get('MAC') logging.info("Slave VM has MAC %s", slave_mac) - pxe.arp_clear_for(master_mac) master_vm.start() - pxe.arp_clear_for(slave_mac) slave_vm.start() wait_for(master_vm.is_running, "Wait for master VM running") wait_for(slave_vm.is_running, "Wait for slave VM running") - # catch host-vm IP address + master_vm.ip = HOSTS_IP_CONFIG['HOSTS']['DEFAULT'] + logging.info("Expecting master VM to have IP %s", master_vm.ip) + + slave_vm.ip = HOSTS_IP_CONFIG['HOSTS']['host2'] + logging.info("Expecting slave VM to have IP %s", slave_vm.ip) + + wait_for(lambda: not os.system(f"nc -zw5 {master_vm.ip} 22"), + "Wait for ssh up on Master VM", retry_delay_secs=5) + wait_for(lambda: not os.system(f"nc -zw5 {slave_vm.ip} 22"), + "Wait for ssh up on Slave VM", retry_delay_secs=5) + + wait_for(lambda: commands.ssh(master_vm.ip, ['xapi-wait-init-complete', '60'], + check=False, simple_output=False).returncode == 0, + "Wait for master XAPI init to be complete", + timeout_secs=30 * 60) + pool = Pool(master_vm.ip) + + # create pool with shared SR + + wait_for(lambda: commands.ssh(slave_vm.ip, ['xapi-wait-init-complete', '60'], + check=False, simple_output=False).returncode == 0, + "Wait for slave XAPI init to be complete", + timeout_secs=30 * 60) + slave = Pool(slave_vm.ip).master + slave.join_pool(pool) + + sr = pool.master.sr_create("nfs", "NFS Shared SR", NFS_DEVICE_CONFIG, + shared=True, verify=True) + + # create and start VMs + vms = ( + pool.master.import_vm(vm_image('mini-linux-x86_64-bios'), sr_uuid=sr.uuid), + pool.master.import_vm(vm_image('mini-linux-x86_64-bios'), sr_uuid=sr.uuid), + ) + + for vm in vms: + vm.start() + + wait_for(lambda: all(vm.is_running() for vm in vms), "Wait for VMs running") + wait_for(lambda: all(vm.try_get_and_store_ip() for vm in vms), + "Wait for VM IPs", timeout_secs=5*60) + wait_for(lambda: all(vm.is_management_agent_up() for vm in vms), + "Wait for management agents up") + + logging.info("VMs dispatched as %s", [vm.get_residence_host().uuid for vm in vms]) + + ## do RPU + + # evacuate master + vms_to_migrate = [vm for vm in vms if vm.get_residence_host().uuid == pool.master.uuid] + logging.info("Expecting migration of %s", ([vm.uuid for vm in vms_to_migrate],)) + pool.master.xe("host-evacuate", {"host": pool.master.uuid}) + wait_for(lambda: all(vm.get_residence_host().uuid != pool.master.uuid for vm in vms_to_migrate), + "Wait for VM migration") + + # upgrade master + pool.master.shutdown() + wait_for(lambda: master_vm.is_halted(), "Wait for Master VM to be halted", timeout_secs=5*60) + installer.perform_upgrade(iso=iso_remaster, host_vm=master_vm) + pxe.arp_clear_for(master_mac) + master_vm.start() + wait_for(master_vm.is_running, "Wait for Master VM running") + wait_for(lambda: pxe.arp_addresses_for(master_mac), "Wait for DHCP server to see Master VM in ARP tables", timeout_secs=10*60) @@ -44,6 +120,25 @@ def test_join_pool(firmware, mode, create_vms): assert len(ips) == 1 master_vm.ip = ips[0] + wait_for(lambda: not os.system(f"nc -zw5 {master_vm.ip} 22"), + "Wait for ssh back up on Master VM", retry_delay_secs=5) + wait_for(pool.master.is_enabled, "Wait for XAPI to be ready", timeout_secs=30 * 60) + + # evacuate slave + vms_to_migrate = vms + logging.info("Expecting migration of %s", ([vm.uuid for vm in vms_to_migrate],)) + pool.master.xe("host-evacuate", {"host": slave.uuid}) + wait_for(lambda: all(vm.get_residence_host().uuid != slave.uuid for vm in vms), + "Wait for VM migration") + + # upgrade slave + slave.shutdown() + wait_for(lambda: slave_vm.is_halted(), "Wait for Slave VM to be halted", timeout_secs=5*60) + installer.perform_upgrade(iso=iso_remaster, host_vm=slave_vm) + pxe.arp_clear_for(slave_mac) + slave_vm.start() + wait_for(slave_vm.is_running, "Wait for Slave VM running") + wait_for(lambda: pxe.arp_addresses_for(slave_mac), "Wait for DHCP server to see Slave VM in ARP tables", timeout_secs=10*60) @@ -52,17 +147,17 @@ def test_join_pool(firmware, mode, create_vms): assert len(ips) == 1 slave_vm.ip = ips[0] - wait_for(lambda: not os.system(f"nc -zw5 {master_vm.ip} 22"), - "Wait for ssh up on Master VM", retry_delay_secs=5) wait_for(lambda: not os.system(f"nc -zw5 {slave_vm.ip} 22"), - "Wait for ssh up on Slave VM", retry_delay_secs=5) + "Wait for ssh back up on Slave VM", retry_delay_secs=5) + wait_for(slave.is_enabled, "Wait for XAPI to be ready", timeout_secs=30 * 60) - pool = Pool(master_vm.ip) - slave = Pool(slave_vm.ip).master - slave.join_pool(pool) + logging.info("Migrating a VM back to slave") + vms[1].migrate(slave) + + ## cleanup slave.shutdown() pool.master.shutdown() - - wait_for(lambda: slave_vm.is_halted(), "Wait for Slave VM to be halted") - wait_for(lambda: master_vm.is_halted(), "Wait for Master VM to be halted") + wait_for(lambda: slave_vm.is_halted(), "Wait for Slave VM to be halted", timeout_secs=5*60) + wait_for(lambda: master_vm.is_halted(), "Wait for Master VM to be halted", timeout_secs=5*60) + # FIXME destroy shared SR contents From c8a414d96939d843f438461332f541b81f00ec5c Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Tue, 6 Aug 2024 15:34:39 +0200 Subject: [PATCH 65/75] Add local_sr parameter to test_install --- .../{82-83-bios.lst => 82-83-bios-ext.lst} | 14 +++--- .../install/test-sequences/82-83-bios-lvm.lst | 7 +++ tests/install/test-sequences/82-83-rpu.lst | 8 ++-- .../{82-83-uefi.lst => 82-83-uefi-ext.lst} | 14 +++--- .../install/test-sequences/82-83-uefi-lvm.lst | 7 +++ .../{83-bios.lst => 83-bios-ext.lst} | 14 +++--- tests/install/test-sequences/83-bios-lvm.lst | 7 +++ .../{83-uefi.lst => 83-uefi-ext.lst} | 14 +++--- tests/install/test-sequences/83-uefi-lvm.lst | 7 +++ tests/install/test.py | 48 +++++++++++-------- tests/install/test_pool.py | 4 +- 11 files changed, 92 insertions(+), 52 deletions(-) rename tests/install/test-sequences/{82-83-bios.lst => 82-83-bios-ext.lst} (71%) create mode 100644 tests/install/test-sequences/82-83-bios-lvm.lst rename tests/install/test-sequences/{82-83-uefi.lst => 82-83-uefi-ext.lst} (71%) create mode 100644 tests/install/test-sequences/82-83-uefi-lvm.lst rename tests/install/test-sequences/{83-bios.lst => 83-bios-ext.lst} (71%) create mode 100644 tests/install/test-sequences/83-bios-lvm.lst rename tests/install/test-sequences/{83-uefi.lst => 83-uefi-ext.lst} (71%) create mode 100644 tests/install/test-sequences/83-uefi-lvm.lst diff --git a/tests/install/test-sequences/82-83-bios.lst b/tests/install/test-sequences/82-83-bios-ext.lst similarity index 71% rename from tests/install/test-sequences/82-83-bios.lst rename to tests/install/test-sequences/82-83-bios-ext.lst index da749201..56ba5d7d 100644 --- a/tests/install/test-sequences/82-83-bios.lst +++ b/tests/install/test-sequences/82-83-bios-ext.lst @@ -1,7 +1,7 @@ -tests/install/test.py::TestNested::test_install[bios-821.1] -tests/install/test.py::TestNested::test_tune_firstboot[None-bios-821.1-host1] -tests/install/test.py::TestNested::test_firstboot_install[bios-821.1-host1] -tests/install/test.py::TestNested::test_upgrade[bios-821.1-83rc1] -tests/install/test.py::TestNested::test_firstboot_noninst[bios-821.1-83rc1] -tests/install/test.py::TestNested::test_restore[bios-821.1-83rc1-83rc1] -tests/install/test.py::TestNested::test_firstboot_noninst[bios-821.1-83rc1-83rc1] +tests/install/test.py::TestNested::test_install[bios-821.1-ext] +tests/install/test.py::TestNested::test_tune_firstboot[None-bios-821.1-host1-ext] +tests/install/test.py::TestNested::test_firstboot_install[bios-821.1-host1-ext] +tests/install/test.py::TestNested::test_upgrade[bios-821.1-83rc1-ext] +tests/install/test.py::TestNested::test_firstboot_noninst[bios-821.1-83rc1-ext] +tests/install/test.py::TestNested::test_restore[bios-821.1-83rc1-83rc1-ext] +tests/install/test.py::TestNested::test_firstboot_noninst[bios-821.1-83rc1-83rc1-ext] diff --git a/tests/install/test-sequences/82-83-bios-lvm.lst b/tests/install/test-sequences/82-83-bios-lvm.lst new file mode 100644 index 00000000..55e1446e --- /dev/null +++ b/tests/install/test-sequences/82-83-bios-lvm.lst @@ -0,0 +1,7 @@ +tests/install/test.py::TestNested::test_install[bios-821.1-lvm] +tests/install/test.py::TestNested::test_tune_firstboot[None-bios-821.1-host1-lvm] +tests/install/test.py::TestNested::test_firstboot_install[bios-821.1-host1-lvm] +tests/install/test.py::TestNested::test_upgrade[bios-821.1-83rc1-lvm] +tests/install/test.py::TestNested::test_firstboot_noninst[bios-821.1-83rc1-lvm] +tests/install/test.py::TestNested::test_restore[bios-821.1-83rc1-83rc1-lvm] +tests/install/test.py::TestNested::test_firstboot_noninst[bios-821.1-83rc1-83rc1-lvm] diff --git a/tests/install/test-sequences/82-83-rpu.lst b/tests/install/test-sequences/82-83-rpu.lst index 9c97fa94..24d53b17 100644 --- a/tests/install/test-sequences/82-83-rpu.lst +++ b/tests/install/test-sequences/82-83-rpu.lst @@ -1,4 +1,6 @@ -tests/install/test.py::TestNested::test_install[uefi-821.1] -tests/install/test.py::TestNested::test_firstboot_install[uefi-821.1-host1] -tests/install/test.py::TestNested::test_firstboot_install[uefi-821.1-host2] +tests/install/test.py::TestNested::test_install[uefi-821.1-nosr] +tests/install/test.py::TestNested::test_tune_firstboot[None-uefi-821.1-host1-nosr] +tests/install/test.py::TestNested::test_firstboot_install[uefi-821.1-host1-nosr] +tests/install/test.py::TestNested::test_tune_firstboot[None-uefi-821.1-host2-nosr] +tests/install/test.py::TestNested::test_firstboot_install[uefi-821.1-host2-nosr] tests/install/test_pool.py::test_pool_rpu[uefi-821.1-83rc1] diff --git a/tests/install/test-sequences/82-83-uefi.lst b/tests/install/test-sequences/82-83-uefi-ext.lst similarity index 71% rename from tests/install/test-sequences/82-83-uefi.lst rename to tests/install/test-sequences/82-83-uefi-ext.lst index 40942a12..c00a72c6 100644 --- a/tests/install/test-sequences/82-83-uefi.lst +++ b/tests/install/test-sequences/82-83-uefi-ext.lst @@ -1,7 +1,7 @@ -tests/install/test.py::TestNested::test_install[uefi-821.1] -tests/install/test.py::TestNested::test_tune_firstboot[None-uefi-821.1-host1] -tests/install/test.py::TestNested::test_firstboot_install[uefi-821.1-host1] -tests/install/test.py::TestNested::test_upgrade[uefi-821.1-83rc1] -tests/install/test.py::TestNested::test_firstboot_noninst[uefi-821.1-83rc1] -tests/install/test.py::TestNested::test_restore[uefi-821.1-83rc1-83rc1] -tests/install/test.py::TestNested::test_firstboot_noninst[uefi-821.1-83rc1-83rc1] +tests/install/test.py::TestNested::test_install[uefi-821.1-ext] +tests/install/test.py::TestNested::test_tune_firstboot[None-uefi-821.1-host1-ext] +tests/install/test.py::TestNested::test_firstboot_install[uefi-821.1-host1-ext] +tests/install/test.py::TestNested::test_upgrade[uefi-821.1-83rc1-ext] +tests/install/test.py::TestNested::test_firstboot_noninst[uefi-821.1-83rc1-ext] +tests/install/test.py::TestNested::test_restore[uefi-821.1-83rc1-83rc1-ext] +tests/install/test.py::TestNested::test_firstboot_noninst[uefi-821.1-83rc1-83rc1-ext] diff --git a/tests/install/test-sequences/82-83-uefi-lvm.lst b/tests/install/test-sequences/82-83-uefi-lvm.lst new file mode 100644 index 00000000..28dbcb7b --- /dev/null +++ b/tests/install/test-sequences/82-83-uefi-lvm.lst @@ -0,0 +1,7 @@ +tests/install/test.py::TestNested::test_install[uefi-821.1-lvm] +tests/install/test.py::TestNested::test_tune_firstboot[None-uefi-821.1-host1-lvm] +tests/install/test.py::TestNested::test_firstboot_install[uefi-821.1-host1-lvm] +tests/install/test.py::TestNested::test_upgrade[uefi-821.1-83rc1-lvm] +tests/install/test.py::TestNested::test_firstboot_noninst[uefi-821.1-83rc1-lvm] +tests/install/test.py::TestNested::test_restore[uefi-821.1-83rc1-83rc1-lvm] +tests/install/test.py::TestNested::test_firstboot_noninst[uefi-821.1-83rc1-83rc1-lvm] diff --git a/tests/install/test-sequences/83-bios.lst b/tests/install/test-sequences/83-bios-ext.lst similarity index 71% rename from tests/install/test-sequences/83-bios.lst rename to tests/install/test-sequences/83-bios-ext.lst index 93161b64..5711cba6 100644 --- a/tests/install/test-sequences/83-bios.lst +++ b/tests/install/test-sequences/83-bios-ext.lst @@ -1,7 +1,7 @@ -tests/install/test.py::TestNested::test_install[bios-83rc1] -tests/install/test.py::TestNested::test_tune_firstboot[None-bios-83rc1-host1] -tests/install/test.py::TestNested::test_firstboot_install[bios-83rc1-host1] -tests/install/test.py::TestNested::test_upgrade[bios-83rc1-83rc1] -tests/install/test.py::TestNested::test_firstboot_noninst[bios-83rc1-83rc1] -tests/install/test.py::TestNested::test_restore[bios-83rc1-83rc1-83rc1] -tests/install/test.py::TestNested::test_firstboot_noninst[bios-83rc1-83rc1-83rc1] +tests/install/test.py::TestNested::test_install[bios-83rc1-ext] +tests/install/test.py::TestNested::test_tune_firstboot[None-bios-83rc1-host1-ext] +tests/install/test.py::TestNested::test_firstboot_install[bios-83rc1-host1-ext] +tests/install/test.py::TestNested::test_upgrade[bios-83rc1-83rc1-ext] +tests/install/test.py::TestNested::test_firstboot_noninst[bios-83rc1-83rc1-ext] +tests/install/test.py::TestNested::test_restore[bios-83rc1-83rc1-83rc1-ext] +tests/install/test.py::TestNested::test_firstboot_noninst[bios-83rc1-83rc1-83rc1-ext] diff --git a/tests/install/test-sequences/83-bios-lvm.lst b/tests/install/test-sequences/83-bios-lvm.lst new file mode 100644 index 00000000..dce9e8a5 --- /dev/null +++ b/tests/install/test-sequences/83-bios-lvm.lst @@ -0,0 +1,7 @@ +tests/install/test.py::TestNested::test_install[bios-83rc1-lvm] +tests/install/test.py::TestNested::test_tune_firstboot[None-bios-83rc1-host1-lvm] +tests/install/test.py::TestNested::test_firstboot_install[bios-83rc1-host1-lvm] +tests/install/test.py::TestNested::test_upgrade[bios-83rc1-83rc1-lvm] +tests/install/test.py::TestNested::test_firstboot_noninst[bios-83rc1-83rc1-lvm] +tests/install/test.py::TestNested::test_restore[bios-83rc1-83rc1-83rc1-lvm] +tests/install/test.py::TestNested::test_firstboot_noninst[bios-83rc1-83rc1-83rc1-lvm] diff --git a/tests/install/test-sequences/83-uefi.lst b/tests/install/test-sequences/83-uefi-ext.lst similarity index 71% rename from tests/install/test-sequences/83-uefi.lst rename to tests/install/test-sequences/83-uefi-ext.lst index 7280abaa..57a77f29 100644 --- a/tests/install/test-sequences/83-uefi.lst +++ b/tests/install/test-sequences/83-uefi-ext.lst @@ -1,7 +1,7 @@ -tests/install/test.py::TestNested::test_install[uefi-83rc1] -tests/install/test.py::TestNested::test_tune_firstboot[None-uefi-83rc1-host1] -tests/install/test.py::TestNested::test_firstboot_install[uefi-83rc1-host1] -tests/install/test.py::TestNested::test_upgrade[uefi-83rc1-83rc1] -tests/install/test.py::TestNested::test_firstboot_noninst[uefi-83rc1-83rc1] -tests/install/test.py::TestNested::test_restore[uefi-83rc1-83rc1-83rc1] -tests/install/test.py::TestNested::test_firstboot_noninst[uefi-83rc1-83rc1-83rc1] +tests/install/test.py::TestNested::test_install[uefi-83rc1-ext] +tests/install/test.py::TestNested::test_tune_firstboot[None-uefi-83rc1-host1-ext] +tests/install/test.py::TestNested::test_firstboot_install[uefi-83rc1-host1-ext] +tests/install/test.py::TestNested::test_upgrade[uefi-83rc1-83rc1-ext] +tests/install/test.py::TestNested::test_firstboot_noninst[uefi-83rc1-83rc1-ext] +tests/install/test.py::TestNested::test_restore[uefi-83rc1-83rc1-83rc1-ext] +tests/install/test.py::TestNested::test_firstboot_noninst[uefi-83rc1-83rc1-83rc1-ext] diff --git a/tests/install/test-sequences/83-uefi-lvm.lst b/tests/install/test-sequences/83-uefi-lvm.lst new file mode 100644 index 00000000..c355685e --- /dev/null +++ b/tests/install/test-sequences/83-uefi-lvm.lst @@ -0,0 +1,7 @@ +tests/install/test.py::TestNested::test_install[uefi-83rc1-lvm] +tests/install/test.py::TestNested::test_tune_firstboot[None-uefi-83rc1-host1-lvm] +tests/install/test.py::TestNested::test_firstboot_install[uefi-83rc1-host1-lvm] +tests/install/test.py::TestNested::test_upgrade[uefi-83rc1-83rc1-lvm] +tests/install/test.py::TestNested::test_firstboot_noninst[uefi-83rc1-83rc1-lvm] +tests/install/test.py::TestNested::test_restore[uefi-83rc1-83rc1-83rc1-lvm] +tests/install/test.py::TestNested::test_firstboot_noninst[uefi-83rc1-83rc1-83rc1-lvm] diff --git a/tests/install/test.py b/tests/install/test.py index 65834c5d..6baa882d 100644 --- a/tests/install/test.py +++ b/tests/install/test.py @@ -15,6 +15,7 @@ @pytest.mark.dependency() class TestNested: + @pytest.mark.parametrize("local_sr", ("nosr", "ext", "lvm")) @pytest.mark.parametrize("iso_version", ( "83rc1", "83b2", "821.1", @@ -58,15 +59,17 @@ class TestNested: }[version], gen_unique_uuid=True, param_mapping={"version": "iso_version"}) - @pytest.mark.answerfile(lambda firmware: AnswerFile("INSTALL") \ + @pytest.mark.answerfile(lambda firmware, local_sr: AnswerFile("INSTALL") \ + .top_setattr({} if local_sr == "nosr" else {"sr-type": local_sr}) \ .top_append( {"TAG": "source", "type": "local"}, {"TAG": "primary-disk", + "guest-storage": "no" if local_sr == "nosr" else "yes", "CONTENTS": {"uefi": "nvme0n1", "bios": "sda"}[firmware]}, ), - param_mapping={"firmware": "firmware"}) + param_mapping={"firmware": "firmware", "local_sr": "local_sr"}) def test_install(self, create_vms, iso_remaster, - firmware, iso_version): + firmware, iso_version, local_sr): assert len(create_vms) == 1 installer.perform_install(iso=iso_remaster, host_vm=create_vms[0]) @@ -272,6 +275,7 @@ def _test_firstboot(self, host_vm, mode, *, machine='DEFAULT'): raise @pytest.mark.usefixtures("xcpng_chained") + @pytest.mark.parametrize("local_sr", ("nosr", "ext", "lvm")) @pytest.mark.parametrize("machine", ("host1", "host2")) @pytest.mark.parametrize("version", ( "83rc1", @@ -282,18 +286,19 @@ def _test_firstboot(self, host_vm, mode, *, machine='DEFAULT'): "ch821.1", "xs8", )) @pytest.mark.parametrize("firmware", ("uefi", "bios")) - @pytest.mark.continuation_of(lambda version, firmware, machine: [dict( + @pytest.mark.continuation_of(lambda version, firmware, machine, local_sr: [dict( vm="vm1", - image_test=f"TestNested::test_tune_firstboot[None-{firmware}-{version}-{machine}]")], + image_test=f"TestNested::test_tune_firstboot[None-{firmware}-{version}-{machine}-{local_sr}]")], param_mapping={"version": "version", "firmware": "firmware", - "machine": "machine"}) + "machine": "machine", "local_sr": "local_sr"}) def test_firstboot_install(self, create_vms, - firmware, version, machine): + firmware, version, machine, local_sr): host_vm = create_vms[0] self._test_firstboot(host_vm, version, machine=machine) @pytest.mark.usefixtures("xcpng_chained") + @pytest.mark.parametrize("local_sr", ("nosr", "ext", "lvm")) @pytest.mark.parametrize("mode", ( "83rc1-83rc1", "83rc1-83rc1-83rc1", "83b2-83rc1", @@ -308,21 +313,23 @@ def test_firstboot_install(self, create_vms, "821.1-821.1", )) @pytest.mark.parametrize("firmware", ("uefi", "bios")) - @pytest.mark.continuation_of(lambda params, firmware: [dict( + @pytest.mark.continuation_of(lambda params, firmware, local_sr: [dict( vm="vm1", - image_test=(f"TestNested::{{}}[{firmware}-{params}]".format( + image_test=(f"TestNested::{{}}[{firmware}-{params}-{local_sr}]".format( { 2: "test_upgrade", 3: "test_restore", }[len(params.split("-"))] )))], - param_mapping={"params": "mode", "firmware": "firmware"}) + param_mapping={"params": "mode", "firmware": "firmware", + "local_sr": "local_sr"}) def test_firstboot_noninst(self, create_vms, - firmware, mode): + firmware, mode, local_sr): host_vm = create_vms[0] self._test_firstboot(host_vm, mode) @pytest.mark.usefixtures("xcpng_chained") + @pytest.mark.parametrize("local_sr", ("nosr", "ext", "lvm")) @pytest.mark.parametrize(("orig_version", "iso_version"), [ ("83rc1", "83rc1"), ("83b2", "83rc1"), @@ -335,10 +342,11 @@ def test_firstboot_noninst(self, create_vms, ("821.1", "821.1"), ]) @pytest.mark.parametrize("firmware", ("uefi", "bios")) - @pytest.mark.continuation_of(lambda firmware, params: [dict( + @pytest.mark.continuation_of(lambda firmware, params, local_sr: [dict( vm="vm1", - image_test=f"TestNested::test_firstboot_install[{firmware}-{params}-host1]")], - param_mapping={"params": "orig_version", "firmware": "firmware"}) + image_test=f"TestNested::test_firstboot_install[{firmware}-{params}-host1-{local_sr}]")], + param_mapping={"params": "orig_version", "firmware": "firmware", + "local_sr": "local_sr"}) @pytest.mark.installer_iso( lambda version: { "821.1": "xcpng-8.2.1-2023", @@ -353,10 +361,11 @@ def test_firstboot_noninst(self, create_vms, ), param_mapping={"firmware": "firmware"}) def test_upgrade(self, create_vms, iso_remaster, - firmware, orig_version, iso_version): + firmware, orig_version, iso_version, local_sr): installer.perform_upgrade(iso=iso_remaster, host_vm=create_vms[0]) @pytest.mark.usefixtures("xcpng_chained") + @pytest.mark.parametrize("local_sr", ("nosr", "ext", "lvm")) @pytest.mark.parametrize(("orig_version", "iso_version"), [ ("83rc1-83rc1", "83rc1"), ("821.1-83rc1", "83rc1"), @@ -367,10 +376,11 @@ def test_upgrade(self, create_vms, iso_remaster, ("ch821.1-83rc1", "83rc1"), ]) @pytest.mark.parametrize("firmware", ("uefi", "bios")) - @pytest.mark.continuation_of(lambda firmware, params: [dict( + @pytest.mark.continuation_of(lambda firmware, params, local_sr: [dict( vm="vm1", - image_test=f"TestNested::test_firstboot_noninst[{firmware}-{params}]")], - param_mapping={"params": "orig_version", "firmware": "firmware"}) + image_test=f"TestNested::test_firstboot_noninst[{firmware}-{params}-{local_sr}]")], + param_mapping={"params": "orig_version", "firmware": "firmware", + "local_sr": "local_sr"}) @pytest.mark.installer_iso( lambda version: { "821.1": "xcpng-8.2.1-2023", @@ -383,7 +393,7 @@ def test_upgrade(self, create_vms, iso_remaster, ), param_mapping={"firmware": "firmware"}) def test_restore(self, create_vms, iso_remaster, - firmware, orig_version, iso_version): + firmware, orig_version, iso_version, local_sr): host_vm = create_vms[0] vif = host_vm.vifs()[0] mac_address = vif.param_get('MAC') diff --git a/tests/install/test_pool.py b/tests/install/test_pool.py index 9298baaa..92571694 100644 --- a/tests/install/test_pool.py +++ b/tests/install/test_pool.py @@ -18,11 +18,11 @@ @pytest.mark.parametrize("firmware", ("uefi", "bios")) @pytest.mark.continuation_of(lambda params, firmware: [ dict(vm="vm1", - image_test=f"tests/install/test.py::TestNested::test_firstboot_install[{firmware}-{params}-host1]", + image_test=f"tests/install/test.py::TestNested::test_firstboot_install[{firmware}-{params}-host1-nosr]", scope="session"), dict(vm="vm2", image_vm="vm1", - image_test=f"tests/install/test.py::TestNested::test_firstboot_install[{firmware}-{params}-host2]", + image_test=f"tests/install/test.py::TestNested::test_firstboot_install[{firmware}-{params}-host2-nosr]", scope="session"), ], param_mapping={"params": "orig_version", "firmware": "firmware"}) From ed6351e214283a87bb5c5cefdf0fad715bb6ea37 Mon Sep 17 00:00:00 2001 From: Guillaume Date: Wed, 10 Jul 2024 11:24:04 +0200 Subject: [PATCH 66/75] WIP Rescan the SR When the ISO is copied to the SR it happens that it is not available right now. So rescan the SR to ensure that it is available. Signed-off-by: Guillaume FIXME: rescan after copying rather than on each use --- lib/vm.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/vm.py b/lib/vm.py index e9c533b8..bcf8d9ae 100644 --- a/lib/vm.py +++ b/lib/vm.py @@ -344,8 +344,16 @@ def detect_package_manager(self): else: return PackageManagerEnum.UNKNOWN - def insert_cd(self, vdi_name): + def insert_cd(self, vdi_name, use_cache=False): logging.info("Insert CD %r in VM %s", vdi_name, self.uuid) + # FIXME: rescan only the right ISO (if at all possible) + # FIXME: rescan after copying rather than on each use (this + # function can be used also for standard ISOs not generated by our tests) + if not use_cache: + sr_iso_uuids = self.host.xe('sr-list', {'type': 'iso'}, minimal=True).split(',') + for uuid in sr_iso_uuids: + logging.info("scanning SR %s", uuid) + self.host.xe('sr-scan', {'uuid': uuid}) self.host.xe('vm-cd-insert', {'uuid': self.uuid, 'cd-name': vdi_name}) def insert_guest_tools_iso(self): From a5df441b3a71a293ef2e81a7055962fe46823746 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Mon, 15 Jul 2024 12:10:13 +0200 Subject: [PATCH 67/75] import_vm: add clone:// and clone+start:// URIs clone+start:// will be used to implement--hosts=cache://... clone:// itself is not yet used directly, but as the "base" protocol upon which clone+start build, it seems logical (and basically free) to implement. Signed-off-by: Yann Dirson --- lib/host.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/lib/host.py b/lib/host.py index 6b44eb47..6318a248 100644 --- a/lib/host.py +++ b/lib/host.py @@ -239,10 +239,25 @@ def cached_vm(self, uri, sr_uuid): logging.info("Could not find a VM in cache with key %r", cache_key) def import_vm(self, uri, sr_uuid=None, use_cache=False): + vm = None if use_cache: - vm = self.cached_vm(uri, sr_uuid) + if '://' in uri and uri.startswith("clone"): + protocol, rest = uri.split(":", 1) + assert rest.startswith("//") + filename = rest[2:] # strip "//" + base_vm = self.cached_vm(filename, sr_uuid) + if base_vm: + vm = base_vm.clone() + vm.param_clear('name-description') + if uri.startswith("clone+start"): + vm.start() + wait_for(vm.is_running, "Wait for VM running") + else: + vm = self.cached_vm(uri, sr_uuid) if vm: return vm + else: + assert not ('://' in uri and uri.startswith("clone")), "clone URIs require cache enabled" params = {} msg = "Import VM %s" % uri From 3aafae5b8a348e2b109ec1d763eb050e4164aca0 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Tue, 23 Jul 2024 11:12:57 +0200 Subject: [PATCH 68/75] Add support for --nest=... --hosts=cache://... Signed-off-by: Yann Dirson --- conftest.py | 49 +++++++++++++++++++++++++++++++++++++++++++++++-- lib/host.py | 1 + 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/conftest.py b/conftest.py index 607b59a5..66f2ca14 100644 --- a/conftest.py +++ b/conftest.py @@ -1,5 +1,6 @@ import itertools import logging +import os import pytest import tempfile @@ -7,6 +8,7 @@ import lib.config as global_config +from lib import pxe from lib.common import callable_marker, shortened_nodeid from lib.common import wait_for, vm_image, is_uuid from lib.common import setup_formatted_and_mounted_disk, teardown_formatted_and_mounted_disk @@ -31,6 +33,12 @@ # pytest hooks def pytest_addoption(parser): + parser.addoption( + "--nest", + action="store", + default=None, + help="XCP-ng or XS master of pool to use for nesting hosts under test", + ) parser.addoption( "--hosts", action="append", @@ -138,9 +146,40 @@ def pytest_runtest_makereport(item, call): # fixtures -def setup_host(hostname_or_ip): +def setup_host(hostname_or_ip, *, config=None): + host_vm = None + if hostname_or_ip.startswith("cache://"): + nest_hostname = config.getoption("nest") + if not nest_hostname: + pytest.fail("--hosts=cache://... requires --nest parameter") + nest = Pool(nest_hostname).master + + protocol, rest = hostname_or_ip.split(":", 1) + host_vm = nest.import_vm(f"clone+start:{rest}", nest.main_sr_uuid(), + use_cache=CACHE_IMPORTED_VM) + + vif = host_vm.vifs()[0] + mac_address = vif.param_get('MAC') + logging.info("Nested host has MAC %s", mac_address) + + # catch host-vm IP address + wait_for(lambda: pxe.arp_addresses_for(mac_address), + "Wait for DHCP server to see nested host in ARP tables", + timeout_secs=10 * 60) + ips = pxe.arp_addresses_for(mac_address) + logging.info("Nested host has IPs %s", ips) + assert len(ips) == 1 + host_vm.ip = ips[0] + + wait_for(lambda: not os.system(f"nc -zw5 {host_vm.ip} 22"), + "Wait for ssh up on nested host", retry_delay_secs=5) + + hostname_or_ip = host_vm.ip + pool = Pool(hostname_or_ip) h = pool.master + if host_vm: + h.nested = host_vm return h @pytest.fixture(scope='session') @@ -149,11 +188,17 @@ def hosts(pytestconfig): hosts_args = pytestconfig.getoption("hosts") hosts_split = [hostlist.split(',') for hostlist in hosts_args] hostname_list = list(itertools.chain(*hosts_split)) - host_list = [setup_host(hostname_or_ip) for hostname_or_ip in hostname_list] + host_list = [setup_host(hostname_or_ip, config=pytestconfig) + for hostname_or_ip in hostname_list] if not host_list: pytest.fail("This test requires at least one --hosts parameter") yield host_list + for host in host_list: + if host.nested: + logging.info("Destroying nested host's VM %s", host.nested) + host.nested.destroy(verify=True) + @pytest.fixture(scope='session') def registered_xo_cli(): # The fixture is not responsible for establishing the connection. diff --git a/lib/host.py b/lib/host.py index 6318a248..86b1023a 100644 --- a/lib/host.py +++ b/lib/host.py @@ -32,6 +32,7 @@ class Host: xe_prefix = "host" def __init__(self, pool, hostname_or_ip): + self.nested = None # if running nested, the VM object for this host self.pool = pool self.hostname_or_ip = hostname_or_ip self.inventory = None From 2702675942cc69329fc7ec2452d7f8d2edc7ba5a Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Fri, 26 Jul 2024 17:59:53 +0200 Subject: [PATCH 69/75] firstboot: wait for stunnel and other certs to be generated --- tests/install/test.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/install/test.py b/tests/install/test.py index 6baa882d..45c3ab3a 100644 --- a/tests/install/test.py +++ b/tests/install/test.py @@ -182,6 +182,8 @@ def _test_firstboot(self, host_vm, mode, *, machine='DEFAULT'): lsb_rel = commands.ssh(host_vm.ip, ["lsb_release", "-sr"]) assert (lsb_dist, lsb_rel) == (expected_dist, expected_rel) + lsb_rel_tuple = tuple(int(x) for x in lsb_rel.split(".")) + # wait for XAPI startup to be done, which avoids: # - waiting for XAPI to start listening to its socket # - waiting for host and pool objects to be populated after install @@ -246,6 +248,20 @@ def _test_firstboot(self, host_vm, mode, *, machine='DEFAULT'): logging.warning("in logs: %s", out) raise + if lsb_rel_tuple >= (8, 3): + # Those certs take time to get generated in install case, + # so make sure they are. On upgrade they must be + # preserved (fails on 8.3.0-rc1). + for certfile in ( + "/etc/stunnel/xapi-stunnel-ca-bundle.pem", + "/etc/stunnel/xapi-pool-ca-bundle.pem", + "/etc/xensource/xapi-pool-tls.pem", + ): + wait_for(lambda: pool.master.ssh(["test", "-e", certfile], + check=False, simple_output=False, + ).returncode == 0, + f"Wait for {certfile} certificate file") + #wait_for(lambda: False, 'Wait "forever"', timeout_secs=100*60) logging.info("Powering off pool master") try: From 59dd504c0bfa7ff9bc9d284db78f39a8c19ef632 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Mon, 5 Aug 2024 17:03:31 +0200 Subject: [PATCH 70/75] WIP drop redundant version mapping from installer_iso marker FIXME: - installer_iso fixture could just use iso_version param ... except for gen_unique_uuid => turn into optional iso_remaster marker? --- data.py-dist | 28 +++++++++++++++------------- tests/install/test.py | 28 ++++++---------------------- 2 files changed, 21 insertions(+), 35 deletions(-) diff --git a/data.py-dist b/data.py-dist index 646914d0..8389de50 100644 --- a/data.py-dist +++ b/data.py-dist @@ -65,19 +65,21 @@ VM_IMAGES = { # FIXME: use URLs and an optional cache? ISO_IMAGES = { -# 'xcpng-8.3-rc1': {'path': "/home/user/iso/xcp-ng-8.3.0-rc1.iso"}, -# 'xcpng-8.3-rc1-net': {'path': "/home/user/iso/xcp-ng-8.3.0-rc1-netinstall.iso"}, -# 'xcpng-8.3-beta2': {'path': "/home/user/iso/xcp-ng-8.3.0-beta2.iso"}, -# 'xcpng-8.2.1-2023': {'path': "/home/user/iso/xcp-ng-8.2.1-20231130.iso"}, -# 'xcpng-8.2.1': {'path': "/home/user/iso/xcp-ng-8.2.1.iso"}, -# 'xcpng-8.2.0': {'path': "/home/user/iso/xcp-ng-8.2.0.iso"}, -# 'xcpng-8.1': {'path': "/home/user/iso/xcp-ng-8.1.0-2.iso"}, -# 'xcpng-8.0': {'path': "/home/user/iso/xcp-ng-8.0.0.iso"}, -# 'xcpng-7.6': {'path': "/home/user/iso/xcp-ng-7.6.0.iso"}, -# 'xcpng-7.5': {'path': "/home/user/iso/xcp-ng-7.5.0-2.iso"}, -# 'ch-8.2.1': {'path': "/home/user/iso/CitrixHypervisor-8.2.1-install-cd.iso"}, -# 'ch-8.2.1-23': {'path': "/home/user/iso/CitrixHypervisor-8.2.1-2306-install-cd.iso"}, -# 'xs8-2024-03': {'path': "/home/user/iso/XenServer8_2024-03-18.iso"}, +# '83rc1': {'path': "/home/user/iso/xcp-ng-8.3.0-rc1.iso"}, +# '83rc1net': {'path': "/home/user/iso/xcp-ng-8.3.0-rc1-netinstall.iso"}, +# '83b2': {'path': "/home/user/iso/xcp-ng-8.3.0-beta2.iso"}, +# '821.1': {'path': "/home/user/iso/xcp-ng-8.2.1-20231130.iso"}, +# '821': {'path': "/home/user/iso/xcp-ng-8.2.1.iso"}, +# '820': {'path': "/home/user/iso/xcp-ng-8.2.0.iso"}, +# '81': {'path': "/home/user/iso/xcp-ng-8.1.0-2.iso"}, +# '80': {'path': "/home/user/iso/xcp-ng-8.0.0.iso"}, +# '76': {'path': "/home/user/iso/xcp-ng-7.6.0.iso"}, +# '75': {'path': "/home/user/iso/xcp-ng-7.5.0-2.iso"}, +# +# 'xs8': {'path': "/home/user/iso/XenServer8_2024-03-18.iso"}, +# #'xs8': {'path': "/home/user/iso/XenServer8_2023-08-22.iso"}, +# 'ch821.1': {'path': "/home/user/iso/CitrixHypervisor-8.2.1-2306-install-cd.iso"}, +# 'ch821': {'path': "/home/user/iso/CitrixHypervisor-8.2.1-install-cd.iso"}, } # In some cases, we may prefer to favour a local SR to store test VM disks, diff --git a/tests/install/test.py b/tests/install/test.py index 45c3ab3a..683b5865 100644 --- a/tests/install/test.py +++ b/tests/install/test.py @@ -46,19 +46,9 @@ class TestNested: ), param_mapping={"firmware": "firmware"}) @pytest.mark.installer_iso( - lambda version: { - "83rc1": "xcpng-8.3-rc1", - "83b2": "xcpng-8.3-beta2", - "821.1": "xcpng-8.2.1-2023", - "81": "xcpng-8.1", - "80": "xcpng-8.0", - "76": "xcpng-7.6", - "75": "xcpng-7.5", - "xs8": "xs8-2024-03", - "ch821.1": "ch-8.2.1-23", - }[version], + lambda iso_version: iso_version, gen_unique_uuid=True, - param_mapping={"version": "iso_version"}) + param_mapping={"iso_version": "iso_version"}) @pytest.mark.answerfile(lambda firmware, local_sr: AnswerFile("INSTALL") \ .top_setattr({} if local_sr == "nosr" else {"sr-type": local_sr}) \ .top_append( @@ -364,11 +354,8 @@ def test_firstboot_noninst(self, create_vms, param_mapping={"params": "orig_version", "firmware": "firmware", "local_sr": "local_sr"}) @pytest.mark.installer_iso( - lambda version: { - "821.1": "xcpng-8.2.1-2023", - "83rc1": "xcpng-8.3-rc1", - }[version], - param_mapping={"version": "iso_version"}) + lambda iso_version: iso_version, + param_mapping={"iso_version": "iso_version"}) @pytest.mark.answerfile( lambda firmware: AnswerFile("UPGRADE").top_append( {"TAG": "source", "type": "local"}, @@ -398,11 +385,8 @@ def test_upgrade(self, create_vms, iso_remaster, param_mapping={"params": "orig_version", "firmware": "firmware", "local_sr": "local_sr"}) @pytest.mark.installer_iso( - lambda version: { - "821.1": "xcpng-8.2.1-2023", - "83rc1": "xcpng-8.3-rc1", - }[version], - param_mapping={"version": "iso_version"}) + lambda iso_version: iso_version, + param_mapping={"iso_version": "iso_version"}) @pytest.mark.answerfile(lambda firmware: AnswerFile("RESTORE").top_append( {"TAG": "backup-disk", "CONTENTS": {"uefi": "nvme0n1", "bios": "sda"}[firmware]}, From 100524abe5e62ba7119cce41b8bb29178a282bd8 Mon Sep 17 00:00:00 2001 From: Guillaume Date: Fri, 26 Jul 2024 15:57:26 +0200 Subject: [PATCH 71/75] WIP Add support for netinstall Add a new parameter to allow booting using netinstall. If an ISO only supports netinstall you can specify it in the data.py by setting the option `net-only` to True. This option is set to False by default. FIXME: - ISO_IMAGES should not require 'net-url' for every ISO Signed-off-by: Guillaume Signed-off-by: Yann Dirson --- data.py-dist | 13 +++- pytest.ini | 1 + tests/install/conftest.py | 38 +++++++++- ...83-bios-ext.lst => 82-83-bios-iso-ext.lst} | 14 ++-- ...83-bios-lvm.lst => 82-83-bios-iso-lvm.lst} | 14 ++-- tests/install/test-sequences/82-83-rpu.lst | 12 ++-- ...83-uefi-ext.lst => 82-83-uefi-iso-ext.lst} | 14 ++-- ...83-uefi-lvm.lst => 82-83-uefi-iso-lvm.lst} | 14 ++-- .../{83-bios-ext.lst => 83-bios-iso-ext.lst} | 14 ++-- .../{83-bios-lvm.lst => 83-bios-iso-lvm.lst} | 14 ++-- .../{83-uefi-ext.lst => 83-uefi-iso-ext.lst} | 14 ++-- .../{83-uefi-lvm.lst => 83-uefi-iso-lvm.lst} | 14 ++-- .../test-sequences/83-uefi-net-ext.lst | 7 ++ tests/install/test.py | 70 ++++++++++++------- tests/install/test_pool.py | 8 ++- 15 files changed, 165 insertions(+), 96 deletions(-) rename tests/install/test-sequences/{82-83-bios-ext.lst => 82-83-bios-iso-ext.lst} (67%) rename tests/install/test-sequences/{82-83-bios-lvm.lst => 82-83-bios-iso-lvm.lst} (67%) rename tests/install/test-sequences/{82-83-uefi-ext.lst => 82-83-uefi-iso-ext.lst} (67%) rename tests/install/test-sequences/{82-83-uefi-lvm.lst => 82-83-uefi-iso-lvm.lst} (67%) rename tests/install/test-sequences/{83-bios-ext.lst => 83-bios-iso-ext.lst} (67%) rename tests/install/test-sequences/{83-bios-lvm.lst => 83-bios-iso-lvm.lst} (67%) rename tests/install/test-sequences/{83-uefi-ext.lst => 83-uefi-iso-ext.lst} (67%) rename tests/install/test-sequences/{83-uefi-lvm.lst => 83-uefi-iso-lvm.lst} (67%) create mode 100644 tests/install/test-sequences/83-uefi-net-ext.lst diff --git a/data.py-dist b/data.py-dist index 8389de50..4b1a2b63 100644 --- a/data.py-dist +++ b/data.py-dist @@ -64,10 +64,17 @@ VM_IMAGES = { } # FIXME: use URLs and an optional cache? +# +# If 'net-only' is set to 'True' only source of type URL will be possible. +# By default the parameter is set to False. ISO_IMAGES = { -# '83rc1': {'path': "/home/user/iso/xcp-ng-8.3.0-rc1.iso"}, -# '83rc1net': {'path': "/home/user/iso/xcp-ng-8.3.0-rc1-netinstall.iso"}, -# '83b2': {'path': "/home/user/iso/xcp-ng-8.3.0-beta2.iso"}, +# '83rc1': {'path': "/home/user/iso/xcp-ng-8.3.0-rc1.iso", +# 'net-url': "http://server/installers/xcp-ng/8.3-rc1"}, +# '83rc1net': {'path': "/home/user/iso/xcp-ng-8.3.0-rc1-netinstall.iso", +# 'net-url': "http://server/installers/xcp-ng/8.3-rc1", +# 'net-only': True}, +# '83b2': {'path': "/home/user/iso/xcp-ng-8.3.0-beta2.iso", +# 'net-url': "http://server/installers/xcp-ng/8.3-beta2"}, # '821.1': {'path': "/home/user/iso/xcp-ng-8.2.1-20231130.iso"}, # '821': {'path': "/home/user/iso/xcp-ng-8.2.1.iso"}, # '820': {'path': "/home/user/iso/xcp-ng-8.2.0.iso"}, diff --git a/pytest.ini b/pytest.ini index 93a51f56..4cae98f3 100644 --- a/pytest.ini +++ b/pytest.ini @@ -36,6 +36,7 @@ markers = flaky: flaky tests. Usually pass, but sometimes fail unexpectedly. complex_prerequisites: tests whose prerequisites are complex and may require special attention. quicktest: runs `quicktest`. + source_type: generate the source type. log_cli = 1 log_cli_level = info log_format = %(asctime)s.%(msecs)03d %(levelname)s %(message)s diff --git a/tests/install/conftest.py b/tests/install/conftest.py index 7553662a..b3985796 100644 --- a/tests/install/conftest.py +++ b/tests/install/conftest.py @@ -1,4 +1,5 @@ from copy import deepcopy +import importlib import logging import os import pytest @@ -10,6 +11,32 @@ from lib.common import callable_marker from lib.commands import local_cmd, scp, ssh +ISO_IMAGES = getattr(importlib.import_module('data', package=None), 'ISO_IMAGES', {}) + +# Return true if the version of the ISO doesn't support the source type. +# Note: this is a quick-win hack, to avoid explicit enumeration of supported +# source_type values for each ISO. +def skip_source_type(version, source_type): + if version not in ISO_IMAGES.keys(): + return True, "version of ISO {} is unknown".format(version) + + if source_type == "iso": + if ISO_IMAGES[version].get('net-only', False): + return True, "ISO image is net-only while source_type is local" + + return False, "do not skip" + + if source_type == "net": + # Net install is not valid if there is no netinstall URL + # FIXME: ISO includes a default URL so we should be able to omit net-url + if 'net-url' not in ISO_IMAGES[version].keys(): + return True, "net-url required for netinstall was not found for {}".format(version) + + return False, "do not skip" + + # If we don't know the source type then it is invalid + return True, "unknown source type {}".format(source_type) + @pytest.fixture(scope='function') def answerfile(request): marker = request.node.get_closest_marker("answerfile") @@ -46,9 +73,18 @@ def iso_remaster(request, answerfile): param_mapping = marker.kwargs.get("param_mapping", {}) iso_key = callable_marker(marker.args[0], request, param_mapping=param_mapping) + try: + source_type = request.getfixturevalue("source_type") + except pytest.FixtureLookupError as e: + raise RuntimeError("iso_remaster fixture requires 'source_type' parameter") from e + gen_unique_uuid = marker.kwargs.get("gen_unique_uuid", False) - from data import ISO_IMAGES, ISOSR_SRV, ISOSR_PATH, PXE_CONFIG_SERVER, TEST_SSH_PUBKEY, TOOLS + skip, reason = skip_source_type(iso_key, source_type) + if skip: + pytest.skip(reason) + + from data import ISOSR_SRV, ISOSR_PATH, PXE_CONFIG_SERVER, TEST_SSH_PUBKEY, TOOLS assert "iso-remaster" in TOOLS iso_remaster = TOOLS["iso-remaster"] assert os.access(iso_remaster, os.X_OK) diff --git a/tests/install/test-sequences/82-83-bios-ext.lst b/tests/install/test-sequences/82-83-bios-iso-ext.lst similarity index 67% rename from tests/install/test-sequences/82-83-bios-ext.lst rename to tests/install/test-sequences/82-83-bios-iso-ext.lst index 56ba5d7d..5d33bc1a 100644 --- a/tests/install/test-sequences/82-83-bios-ext.lst +++ b/tests/install/test-sequences/82-83-bios-iso-ext.lst @@ -1,7 +1,7 @@ -tests/install/test.py::TestNested::test_install[bios-821.1-ext] -tests/install/test.py::TestNested::test_tune_firstboot[None-bios-821.1-host1-ext] -tests/install/test.py::TestNested::test_firstboot_install[bios-821.1-host1-ext] -tests/install/test.py::TestNested::test_upgrade[bios-821.1-83rc1-ext] -tests/install/test.py::TestNested::test_firstboot_noninst[bios-821.1-83rc1-ext] -tests/install/test.py::TestNested::test_restore[bios-821.1-83rc1-83rc1-ext] -tests/install/test.py::TestNested::test_firstboot_noninst[bios-821.1-83rc1-83rc1-ext] +tests/install/test.py::TestNested::test_install[bios-821.1-iso-ext] +tests/install/test.py::TestNested::test_tune_firstboot[None-bios-821.1-host1-iso-ext] +tests/install/test.py::TestNested::test_firstboot_install[bios-821.1-host1-iso-ext] +tests/install/test.py::TestNested::test_upgrade[bios-821.1-83rc1-iso-ext] +tests/install/test.py::TestNested::test_firstboot_noninst[bios-821.1-83rc1-iso-ext] +tests/install/test.py::TestNested::test_restore[bios-821.1-83rc1-83rc1-iso-ext] +tests/install/test.py::TestNested::test_firstboot_noninst[bios-821.1-83rc1-83rc1-iso-ext] diff --git a/tests/install/test-sequences/82-83-bios-lvm.lst b/tests/install/test-sequences/82-83-bios-iso-lvm.lst similarity index 67% rename from tests/install/test-sequences/82-83-bios-lvm.lst rename to tests/install/test-sequences/82-83-bios-iso-lvm.lst index 55e1446e..e02b9f33 100644 --- a/tests/install/test-sequences/82-83-bios-lvm.lst +++ b/tests/install/test-sequences/82-83-bios-iso-lvm.lst @@ -1,7 +1,7 @@ -tests/install/test.py::TestNested::test_install[bios-821.1-lvm] -tests/install/test.py::TestNested::test_tune_firstboot[None-bios-821.1-host1-lvm] -tests/install/test.py::TestNested::test_firstboot_install[bios-821.1-host1-lvm] -tests/install/test.py::TestNested::test_upgrade[bios-821.1-83rc1-lvm] -tests/install/test.py::TestNested::test_firstboot_noninst[bios-821.1-83rc1-lvm] -tests/install/test.py::TestNested::test_restore[bios-821.1-83rc1-83rc1-lvm] -tests/install/test.py::TestNested::test_firstboot_noninst[bios-821.1-83rc1-83rc1-lvm] +tests/install/test.py::TestNested::test_install[bios-821.1-iso-lvm] +tests/install/test.py::TestNested::test_tune_firstboot[None-bios-821.1-host1-iso-lvm] +tests/install/test.py::TestNested::test_firstboot_install[bios-821.1-host1-iso-lvm] +tests/install/test.py::TestNested::test_upgrade[bios-821.1-83rc1-iso-lvm] +tests/install/test.py::TestNested::test_firstboot_noninst[bios-821.1-83rc1-iso-lvm] +tests/install/test.py::TestNested::test_restore[bios-821.1-83rc1-83rc1-iso-lvm] +tests/install/test.py::TestNested::test_firstboot_noninst[bios-821.1-83rc1-83rc1-iso-lvm] diff --git a/tests/install/test-sequences/82-83-rpu.lst b/tests/install/test-sequences/82-83-rpu.lst index 24d53b17..5c1ad22e 100644 --- a/tests/install/test-sequences/82-83-rpu.lst +++ b/tests/install/test-sequences/82-83-rpu.lst @@ -1,6 +1,6 @@ -tests/install/test.py::TestNested::test_install[uefi-821.1-nosr] -tests/install/test.py::TestNested::test_tune_firstboot[None-uefi-821.1-host1-nosr] -tests/install/test.py::TestNested::test_firstboot_install[uefi-821.1-host1-nosr] -tests/install/test.py::TestNested::test_tune_firstboot[None-uefi-821.1-host2-nosr] -tests/install/test.py::TestNested::test_firstboot_install[uefi-821.1-host2-nosr] -tests/install/test_pool.py::test_pool_rpu[uefi-821.1-83rc1] +tests/install/test.py::TestNested::test_install[uefi-821.1-iso-nosr] +tests/install/test.py::TestNested::test_tune_firstboot[None-uefi-821.1-host1-iso-nosr] +tests/install/test.py::TestNested::test_firstboot_install[uefi-821.1-host1-iso-nosr] +tests/install/test.py::TestNested::test_tune_firstboot[None-uefi-821.1-host2-iso-nosr] +tests/install/test.py::TestNested::test_firstboot_install[uefi-821.1-host2-iso-nosr] +tests/install/test_pool.py::test_pool_rpu[uefi-821.1-83rc1-iso] diff --git a/tests/install/test-sequences/82-83-uefi-ext.lst b/tests/install/test-sequences/82-83-uefi-iso-ext.lst similarity index 67% rename from tests/install/test-sequences/82-83-uefi-ext.lst rename to tests/install/test-sequences/82-83-uefi-iso-ext.lst index c00a72c6..e6c075e5 100644 --- a/tests/install/test-sequences/82-83-uefi-ext.lst +++ b/tests/install/test-sequences/82-83-uefi-iso-ext.lst @@ -1,7 +1,7 @@ -tests/install/test.py::TestNested::test_install[uefi-821.1-ext] -tests/install/test.py::TestNested::test_tune_firstboot[None-uefi-821.1-host1-ext] -tests/install/test.py::TestNested::test_firstboot_install[uefi-821.1-host1-ext] -tests/install/test.py::TestNested::test_upgrade[uefi-821.1-83rc1-ext] -tests/install/test.py::TestNested::test_firstboot_noninst[uefi-821.1-83rc1-ext] -tests/install/test.py::TestNested::test_restore[uefi-821.1-83rc1-83rc1-ext] -tests/install/test.py::TestNested::test_firstboot_noninst[uefi-821.1-83rc1-83rc1-ext] +tests/install/test.py::TestNested::test_install[uefi-821.1-iso-ext] +tests/install/test.py::TestNested::test_tune_firstboot[None-uefi-821.1-host1-iso-ext] +tests/install/test.py::TestNested::test_firstboot_install[uefi-821.1-host1-iso-ext] +tests/install/test.py::TestNested::test_upgrade[uefi-821.1-83rc1-iso-ext] +tests/install/test.py::TestNested::test_firstboot_noninst[uefi-821.1-83rc1-iso-ext] +tests/install/test.py::TestNested::test_restore[uefi-821.1-83rc1-83rc1-iso-ext] +tests/install/test.py::TestNested::test_firstboot_noninst[uefi-821.1-83rc1-83rc1-iso-ext] diff --git a/tests/install/test-sequences/82-83-uefi-lvm.lst b/tests/install/test-sequences/82-83-uefi-iso-lvm.lst similarity index 67% rename from tests/install/test-sequences/82-83-uefi-lvm.lst rename to tests/install/test-sequences/82-83-uefi-iso-lvm.lst index 28dbcb7b..5b5edf88 100644 --- a/tests/install/test-sequences/82-83-uefi-lvm.lst +++ b/tests/install/test-sequences/82-83-uefi-iso-lvm.lst @@ -1,7 +1,7 @@ -tests/install/test.py::TestNested::test_install[uefi-821.1-lvm] -tests/install/test.py::TestNested::test_tune_firstboot[None-uefi-821.1-host1-lvm] -tests/install/test.py::TestNested::test_firstboot_install[uefi-821.1-host1-lvm] -tests/install/test.py::TestNested::test_upgrade[uefi-821.1-83rc1-lvm] -tests/install/test.py::TestNested::test_firstboot_noninst[uefi-821.1-83rc1-lvm] -tests/install/test.py::TestNested::test_restore[uefi-821.1-83rc1-83rc1-lvm] -tests/install/test.py::TestNested::test_firstboot_noninst[uefi-821.1-83rc1-83rc1-lvm] +tests/install/test.py::TestNested::test_install[uefi-821.1-iso-lvm] +tests/install/test.py::TestNested::test_tune_firstboot[None-uefi-821.1-host1-iso-lvm] +tests/install/test.py::TestNested::test_firstboot_install[uefi-821.1-host1-iso-lvm] +tests/install/test.py::TestNested::test_upgrade[uefi-821.1-83rc1-iso-lvm] +tests/install/test.py::TestNested::test_firstboot_noninst[uefi-821.1-83rc1-iso-lvm] +tests/install/test.py::TestNested::test_restore[uefi-821.1-83rc1-83rc1-iso-lvm] +tests/install/test.py::TestNested::test_firstboot_noninst[uefi-821.1-83rc1-83rc1-iso-lvm] diff --git a/tests/install/test-sequences/83-bios-ext.lst b/tests/install/test-sequences/83-bios-iso-ext.lst similarity index 67% rename from tests/install/test-sequences/83-bios-ext.lst rename to tests/install/test-sequences/83-bios-iso-ext.lst index 5711cba6..4b8c2d4b 100644 --- a/tests/install/test-sequences/83-bios-ext.lst +++ b/tests/install/test-sequences/83-bios-iso-ext.lst @@ -1,7 +1,7 @@ -tests/install/test.py::TestNested::test_install[bios-83rc1-ext] -tests/install/test.py::TestNested::test_tune_firstboot[None-bios-83rc1-host1-ext] -tests/install/test.py::TestNested::test_firstboot_install[bios-83rc1-host1-ext] -tests/install/test.py::TestNested::test_upgrade[bios-83rc1-83rc1-ext] -tests/install/test.py::TestNested::test_firstboot_noninst[bios-83rc1-83rc1-ext] -tests/install/test.py::TestNested::test_restore[bios-83rc1-83rc1-83rc1-ext] -tests/install/test.py::TestNested::test_firstboot_noninst[bios-83rc1-83rc1-83rc1-ext] +tests/install/test.py::TestNested::test_install[bios-83rc1-iso-ext] +tests/install/test.py::TestNested::test_tune_firstboot[None-bios-83rc1-host1-iso-ext] +tests/install/test.py::TestNested::test_firstboot_install[bios-83rc1-host1-iso-ext] +tests/install/test.py::TestNested::test_upgrade[bios-83rc1-83rc1-iso-ext] +tests/install/test.py::TestNested::test_firstboot_noninst[bios-83rc1-83rc1-iso-ext] +tests/install/test.py::TestNested::test_restore[bios-83rc1-83rc1-83rc1-iso-ext] +tests/install/test.py::TestNested::test_firstboot_noninst[bios-83rc1-83rc1-83rc1-iso-ext] diff --git a/tests/install/test-sequences/83-bios-lvm.lst b/tests/install/test-sequences/83-bios-iso-lvm.lst similarity index 67% rename from tests/install/test-sequences/83-bios-lvm.lst rename to tests/install/test-sequences/83-bios-iso-lvm.lst index dce9e8a5..e0756d7e 100644 --- a/tests/install/test-sequences/83-bios-lvm.lst +++ b/tests/install/test-sequences/83-bios-iso-lvm.lst @@ -1,7 +1,7 @@ -tests/install/test.py::TestNested::test_install[bios-83rc1-lvm] -tests/install/test.py::TestNested::test_tune_firstboot[None-bios-83rc1-host1-lvm] -tests/install/test.py::TestNested::test_firstboot_install[bios-83rc1-host1-lvm] -tests/install/test.py::TestNested::test_upgrade[bios-83rc1-83rc1-lvm] -tests/install/test.py::TestNested::test_firstboot_noninst[bios-83rc1-83rc1-lvm] -tests/install/test.py::TestNested::test_restore[bios-83rc1-83rc1-83rc1-lvm] -tests/install/test.py::TestNested::test_firstboot_noninst[bios-83rc1-83rc1-83rc1-lvm] +tests/install/test.py::TestNested::test_install[bios-83rc1-iso-lvm] +tests/install/test.py::TestNested::test_tune_firstboot[None-bios-83rc1-host1-iso-lvm] +tests/install/test.py::TestNested::test_firstboot_install[bios-83rc1-host1-iso-lvm] +tests/install/test.py::TestNested::test_upgrade[bios-83rc1-83rc1-iso-lvm] +tests/install/test.py::TestNested::test_firstboot_noninst[bios-83rc1-83rc1-iso-lvm] +tests/install/test.py::TestNested::test_restore[bios-83rc1-83rc1-83rc1-iso-lvm] +tests/install/test.py::TestNested::test_firstboot_noninst[bios-83rc1-83rc1-83rc1-iso-lvm] diff --git a/tests/install/test-sequences/83-uefi-ext.lst b/tests/install/test-sequences/83-uefi-iso-ext.lst similarity index 67% rename from tests/install/test-sequences/83-uefi-ext.lst rename to tests/install/test-sequences/83-uefi-iso-ext.lst index 57a77f29..d79a18c0 100644 --- a/tests/install/test-sequences/83-uefi-ext.lst +++ b/tests/install/test-sequences/83-uefi-iso-ext.lst @@ -1,7 +1,7 @@ -tests/install/test.py::TestNested::test_install[uefi-83rc1-ext] -tests/install/test.py::TestNested::test_tune_firstboot[None-uefi-83rc1-host1-ext] -tests/install/test.py::TestNested::test_firstboot_install[uefi-83rc1-host1-ext] -tests/install/test.py::TestNested::test_upgrade[uefi-83rc1-83rc1-ext] -tests/install/test.py::TestNested::test_firstboot_noninst[uefi-83rc1-83rc1-ext] -tests/install/test.py::TestNested::test_restore[uefi-83rc1-83rc1-83rc1-ext] -tests/install/test.py::TestNested::test_firstboot_noninst[uefi-83rc1-83rc1-83rc1-ext] +tests/install/test.py::TestNested::test_install[uefi-83rc1-iso-ext] +tests/install/test.py::TestNested::test_tune_firstboot[None-uefi-83rc1-host1-iso-ext] +tests/install/test.py::TestNested::test_firstboot_install[uefi-83rc1-host1-iso-ext] +tests/install/test.py::TestNested::test_upgrade[uefi-83rc1-83rc1-iso-ext] +tests/install/test.py::TestNested::test_firstboot_noninst[uefi-83rc1-83rc1-iso-ext] +tests/install/test.py::TestNested::test_restore[uefi-83rc1-83rc1-83rc1-iso-ext] +tests/install/test.py::TestNested::test_firstboot_noninst[uefi-83rc1-83rc1-83rc1-iso-ext] diff --git a/tests/install/test-sequences/83-uefi-lvm.lst b/tests/install/test-sequences/83-uefi-iso-lvm.lst similarity index 67% rename from tests/install/test-sequences/83-uefi-lvm.lst rename to tests/install/test-sequences/83-uefi-iso-lvm.lst index c355685e..51b383f7 100644 --- a/tests/install/test-sequences/83-uefi-lvm.lst +++ b/tests/install/test-sequences/83-uefi-iso-lvm.lst @@ -1,7 +1,7 @@ -tests/install/test.py::TestNested::test_install[uefi-83rc1-lvm] -tests/install/test.py::TestNested::test_tune_firstboot[None-uefi-83rc1-host1-lvm] -tests/install/test.py::TestNested::test_firstboot_install[uefi-83rc1-host1-lvm] -tests/install/test.py::TestNested::test_upgrade[uefi-83rc1-83rc1-lvm] -tests/install/test.py::TestNested::test_firstboot_noninst[uefi-83rc1-83rc1-lvm] -tests/install/test.py::TestNested::test_restore[uefi-83rc1-83rc1-83rc1-lvm] -tests/install/test.py::TestNested::test_firstboot_noninst[uefi-83rc1-83rc1-83rc1-lvm] +tests/install/test.py::TestNested::test_install[uefi-83rc1-iso-lvm] +tests/install/test.py::TestNested::test_tune_firstboot[None-uefi-83rc1-host1-iso-lvm] +tests/install/test.py::TestNested::test_firstboot_install[uefi-83rc1-host1-iso-lvm] +tests/install/test.py::TestNested::test_upgrade[uefi-83rc1-83rc1-iso-lvm] +tests/install/test.py::TestNested::test_firstboot_noninst[uefi-83rc1-83rc1-iso-lvm] +tests/install/test.py::TestNested::test_restore[uefi-83rc1-83rc1-83rc1-iso-lvm] +tests/install/test.py::TestNested::test_firstboot_noninst[uefi-83rc1-83rc1-83rc1-iso-lvm] diff --git a/tests/install/test-sequences/83-uefi-net-ext.lst b/tests/install/test-sequences/83-uefi-net-ext.lst new file mode 100644 index 00000000..d5814da7 --- /dev/null +++ b/tests/install/test-sequences/83-uefi-net-ext.lst @@ -0,0 +1,7 @@ +tests/install/test.py::TestNested::test_install[uefi-83rc1net-net-ext] +tests/install/test.py::TestNested::test_tune_firstboot[None-uefi-83rc1net-host1-net-ext] +tests/install/test.py::TestNested::test_firstboot_install[uefi-83rc1net-host1-net-ext] +tests/install/test.py::TestNested::test_upgrade[uefi-83rc1net-83rc1net-net-ext] +tests/install/test.py::TestNested::test_firstboot_noninst[uefi-83rc1net-83rc1net-net-ext] +tests/install/test.py::TestNested::test_restore[uefi-83rc1net-83rc1net-83rc1net-net-ext] +tests/install/test.py::TestNested::test_firstboot_noninst[uefi-83rc1net-83rc1net-83rc1net-net-ext] diff --git a/tests/install/test.py b/tests/install/test.py index 683b5865..e7d77212 100644 --- a/tests/install/test.py +++ b/tests/install/test.py @@ -9,15 +9,16 @@ from lib.pool import Pool from lib.vdi import VDI -from data import HOSTS_IP_CONFIG, NETWORKS +from data import HOSTS_IP_CONFIG, ISO_IMAGES, NETWORKS assert "MGMT" in NETWORKS assert "HOSTS" in HOSTS_IP_CONFIG @pytest.mark.dependency() class TestNested: @pytest.mark.parametrize("local_sr", ("nosr", "ext", "lvm")) + @pytest.mark.parametrize("source_type", ("iso", "net")) @pytest.mark.parametrize("iso_version", ( - "83rc1", "83b2", + "83rc1", "83rc1net", "83b2", "821.1", "81", "80", "76", "75", "xs8", "ch821.1", @@ -49,17 +50,22 @@ class TestNested: lambda iso_version: iso_version, gen_unique_uuid=True, param_mapping={"iso_version": "iso_version"}) - @pytest.mark.answerfile(lambda firmware, local_sr: AnswerFile("INSTALL") \ + @pytest.mark.answerfile(lambda firmware, local_sr, source_type, version: AnswerFile("INSTALL") \ .top_setattr({} if local_sr == "nosr" else {"sr-type": local_sr}) \ .top_append( - {"TAG": "source", "type": "local"}, + {'iso': {"TAG": "source", "type": "local"}, + 'net': {"TAG": "source", "type": "url", + # FIXME evaluation requires 'net-url' for every ISO + "CONTENTS": ISO_IMAGES[version]['net-url']}}[source_type], {"TAG": "primary-disk", "guest-storage": "no" if local_sr == "nosr" else "yes", "CONTENTS": {"uefi": "nvme0n1", "bios": "sda"}[firmware]}, ), - param_mapping={"firmware": "firmware", "local_sr": "local_sr"}) + param_mapping={"firmware": "firmware", "local_sr": "local_sr", + "source_type": "source_type", "version": "iso_version", + }) def test_install(self, create_vms, iso_remaster, - firmware, iso_version, local_sr): + firmware, iso_version, source_type, local_sr): assert len(create_vms) == 1 installer.perform_install(iso=iso_remaster, host_vm=create_vms[0]) @@ -88,6 +94,7 @@ def helper_vm_with_plugged_disk(imported_vm, create_vms): @pytest.mark.usefixtures("xcpng_chained") @pytest.mark.parametrize("local_sr", ("nosr", "ext", "lvm")) + @pytest.mark.parametrize("source_type", ("iso", "net")) @pytest.mark.parametrize("machine", ("host1", "host2")) @pytest.mark.parametrize("version", ( "83rc1", @@ -98,14 +105,14 @@ def helper_vm_with_plugged_disk(imported_vm, create_vms): "ch821.1", "xs8", )) @pytest.mark.parametrize("firmware", ("uefi", "bios")) - @pytest.mark.continuation_of(lambda version, firmware, local_sr: [dict( + @pytest.mark.continuation_of(lambda version, firmware, local_sr, source_type: [dict( vm="vm1", - image_test=f"TestNested::test_install[{firmware}-{version}-{local_sr}]")], + image_test=f"TestNested::test_install[{firmware}-{version}-{source_type}-{local_sr}]")], param_mapping={"version": "version", "firmware": "firmware", - "local_sr": "local_sr"}) + "local_sr": "local_sr", "source_type": "source_type"}) @pytest.mark.small_vm def test_tune_firstboot(self, create_vms, helper_vm_with_plugged_disk, - firmware, version, machine, local_sr): + firmware, version, machine, local_sr, source_type): helper_vm = helper_vm_with_plugged_disk helper_vm.ssh(["mount /dev/xvdb1 /mnt"]) @@ -152,6 +159,7 @@ def _test_firstboot(self, host_vm, mode, *, machine='DEFAULT'): "821.1": "8.2.1", "83b2": "8.3.0", "83rc1": "8.3.0", + "83rc1net": "8.3.0", }[expected_rel_id] try: @@ -282,9 +290,10 @@ def _test_firstboot(self, host_vm, mode, *, machine='DEFAULT'): @pytest.mark.usefixtures("xcpng_chained") @pytest.mark.parametrize("local_sr", ("nosr", "ext", "lvm")) + @pytest.mark.parametrize("source_type", ("iso", "net")) @pytest.mark.parametrize("machine", ("host1", "host2")) @pytest.mark.parametrize("version", ( - "83rc1", + "83rc1", "83rc1net", "83b2", "821.1", "81", "80", @@ -292,21 +301,24 @@ def _test_firstboot(self, host_vm, mode, *, machine='DEFAULT'): "ch821.1", "xs8", )) @pytest.mark.parametrize("firmware", ("uefi", "bios")) - @pytest.mark.continuation_of(lambda version, firmware, machine, local_sr: [dict( + @pytest.mark.continuation_of(lambda version, firmware, machine, local_sr, source_type: [dict( vm="vm1", - image_test=f"TestNested::test_tune_firstboot[None-{firmware}-{version}-{machine}-{local_sr}]")], + image_test=f"TestNested::test_tune_firstboot[None-{firmware}-{version}-{machine}-{source_type}-{local_sr}]")], param_mapping={"version": "version", "firmware": "firmware", - "machine": "machine", "local_sr": "local_sr"}) + "machine": "machine", "local_sr": "local_sr", + "source_type": "source_type"}) def test_firstboot_install(self, create_vms, - firmware, version, machine, local_sr): + firmware, version, machine, source_type, local_sr): host_vm = create_vms[0] self._test_firstboot(host_vm, version, machine=machine) @pytest.mark.usefixtures("xcpng_chained") @pytest.mark.parametrize("local_sr", ("nosr", "ext", "lvm")) + @pytest.mark.parametrize("source_type", ("iso", "net")) @pytest.mark.parametrize("mode", ( "83rc1-83rc1", "83rc1-83rc1-83rc1", + "83rc1net-83rc1net", "83rc1net-83rc1net-83rc1net", "83b2-83rc1", "821.1-83rc1", "821.1-83rc1-83rc1", @@ -319,25 +331,27 @@ def test_firstboot_install(self, create_vms, "821.1-821.1", )) @pytest.mark.parametrize("firmware", ("uefi", "bios")) - @pytest.mark.continuation_of(lambda params, firmware, local_sr: [dict( + @pytest.mark.continuation_of(lambda params, firmware, local_sr, source_type: [dict( vm="vm1", - image_test=(f"TestNested::{{}}[{firmware}-{params}-{local_sr}]".format( + image_test=(f"TestNested::{{}}[{firmware}-{params}-{source_type}-{local_sr}]".format( { 2: "test_upgrade", 3: "test_restore", }[len(params.split("-"))] )))], param_mapping={"params": "mode", "firmware": "firmware", - "local_sr": "local_sr"}) + "local_sr": "local_sr", "source_type": "source_type"}) def test_firstboot_noninst(self, create_vms, - firmware, mode, local_sr): + firmware, mode, source_type, local_sr): host_vm = create_vms[0] self._test_firstboot(host_vm, mode) @pytest.mark.usefixtures("xcpng_chained") @pytest.mark.parametrize("local_sr", ("nosr", "ext", "lvm")) + @pytest.mark.parametrize("source_type", ("iso", "net")) @pytest.mark.parametrize(("orig_version", "iso_version"), [ ("83rc1", "83rc1"), + ("83rc1net", "83rc1net"), ("83b2", "83rc1"), ("821.1", "83rc1"), ("81", "83rc1"), @@ -348,11 +362,11 @@ def test_firstboot_noninst(self, create_vms, ("821.1", "821.1"), ]) @pytest.mark.parametrize("firmware", ("uefi", "bios")) - @pytest.mark.continuation_of(lambda firmware, params, local_sr: [dict( + @pytest.mark.continuation_of(lambda firmware, params, local_sr, source_type: [dict( vm="vm1", - image_test=f"TestNested::test_firstboot_install[{firmware}-{params}-host1-{local_sr}]")], + image_test=f"TestNested::test_firstboot_install[{firmware}-{params}-host1-{source_type}-{local_sr}]")], param_mapping={"params": "orig_version", "firmware": "firmware", - "local_sr": "local_sr"}) + "local_sr": "local_sr", "source_type": "source_type"}) @pytest.mark.installer_iso( lambda iso_version: iso_version, param_mapping={"iso_version": "iso_version"}) @@ -364,13 +378,15 @@ def test_firstboot_noninst(self, create_vms, ), param_mapping={"firmware": "firmware"}) def test_upgrade(self, create_vms, iso_remaster, - firmware, orig_version, iso_version, local_sr): + firmware, orig_version, iso_version, source_type, local_sr): installer.perform_upgrade(iso=iso_remaster, host_vm=create_vms[0]) @pytest.mark.usefixtures("xcpng_chained") @pytest.mark.parametrize("local_sr", ("nosr", "ext", "lvm")) + @pytest.mark.parametrize("source_type", ("iso", "net")) @pytest.mark.parametrize(("orig_version", "iso_version"), [ ("83rc1-83rc1", "83rc1"), + ("83rc1net-83rc1net", "83rc1net"), ("821.1-83rc1", "83rc1"), ("75-83rc1", "83rc1"), ("76-83rc1", "83rc1"), @@ -379,11 +395,11 @@ def test_upgrade(self, create_vms, iso_remaster, ("ch821.1-83rc1", "83rc1"), ]) @pytest.mark.parametrize("firmware", ("uefi", "bios")) - @pytest.mark.continuation_of(lambda firmware, params, local_sr: [dict( + @pytest.mark.continuation_of(lambda firmware, params, local_sr, source_type: [dict( vm="vm1", - image_test=f"TestNested::test_firstboot_noninst[{firmware}-{params}-{local_sr}]")], + image_test=f"TestNested::test_firstboot_noninst[{firmware}-{params}-{source_type}-{local_sr}]")], param_mapping={"params": "orig_version", "firmware": "firmware", - "local_sr": "local_sr"}) + "local_sr": "local_sr", "source_type": "source_type"}) @pytest.mark.installer_iso( lambda iso_version: iso_version, param_mapping={"iso_version": "iso_version"}) @@ -393,7 +409,7 @@ def test_upgrade(self, create_vms, iso_remaster, ), param_mapping={"firmware": "firmware"}) def test_restore(self, create_vms, iso_remaster, - firmware, orig_version, iso_version, local_sr): + firmware, orig_version, iso_version, source_type, local_sr): host_vm = create_vms[0] vif = host_vm.vifs()[0] mac_address = vif.param_get('MAC') diff --git a/tests/install/test_pool.py b/tests/install/test_pool.py index 92571694..af164031 100644 --- a/tests/install/test_pool.py +++ b/tests/install/test_pool.py @@ -12,17 +12,18 @@ # FIXME without --ignore-unknown-dependency, SKIPPED # "because it depends on tests/install/test.py::TestNested::test_firstboot_install[uefi-821.1-host1]" @pytest.mark.usefixtures("xcpng_chained") +@pytest.mark.parametrize("source_type", ("iso", "net")) @pytest.mark.parametrize(("orig_version", "iso_version"), [ ("821.1", "83rc1"), ]) @pytest.mark.parametrize("firmware", ("uefi", "bios")) @pytest.mark.continuation_of(lambda params, firmware: [ dict(vm="vm1", - image_test=f"tests/install/test.py::TestNested::test_firstboot_install[{firmware}-{params}-host1-nosr]", + image_test=f"tests/install/test.py::TestNested::test_firstboot_install[{firmware}-{params}-host1-iso-nosr]", scope="session"), dict(vm="vm2", image_vm="vm1", - image_test=f"tests/install/test.py::TestNested::test_firstboot_install[{firmware}-{params}-host2-nosr]", + image_test=f"tests/install/test.py::TestNested::test_firstboot_install[{firmware}-{params}-host2-iso-nosr]", scope="session"), ], param_mapping={"params": "orig_version", "firmware": "firmware"}) @@ -37,7 +38,8 @@ {"TAG": "existing-installation", "CONTENTS": {"uefi": "nvme0n1", "bios": "sda"}[firmware]}, ), param_mapping={"firmware": "firmware"}) -def test_pool_rpu(firmware, orig_version, iso_version, iso_remaster, create_vms): +def test_pool_rpu(create_vms, iso_remaster, + firmware, orig_version, iso_version, source_type): (master_vm, slave_vm) = create_vms master_mac = master_vm.vifs()[0].param_get('MAC') logging.info("Master VM has MAC %s", master_mac) From 07de1f705af6b4f4fe693ad30d6ed87be7fae8d7 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Thu, 1 Aug 2024 13:44:03 +0200 Subject: [PATCH 72/75] WIP modify VDI --- tests/install/test_fixtures.py | 41 ++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/tests/install/test_fixtures.py b/tests/install/test_fixtures.py index 486ba9cc..d38c59dd 100644 --- a/tests/install/test_fixtures.py +++ b/tests/install/test_fixtures.py @@ -1,7 +1,10 @@ import logging +import os import pytest +from lib.common import wait_for from lib.installer import AnswerFile +from lib.vdi import VDI # test the answerfile fixture can run on 2 parametrized instances # of the test in one run @@ -18,3 +21,41 @@ @pytest.mark.dependency def test_parametrized_answerfile(answerfile, parm): logging.debug("test_parametrized_answerfile with parm=%s", parm) + +@pytest.fixture +def cloned_disk(imported_vm): + vm = imported_vm + all_vdis = [VDI(uuid, host=vm.host) for uuid in vm.vdi_uuids()] + disk_vdis = [vdi for vdi in all_vdis if not vdi.readonly()] + base_vdi, = disk_vdis + + clone = base_vdi.clone() + yield clone + + clone.destroy() + +@pytest.fixture +def vm_with_plugged_disk(imported_vm, cloned_disk): + vm = imported_vm + test_vdi = cloned_disk + + vm.start() + vm.wait_for_vm_running_and_ssh_up() + + vbd = vm.create_vbd("1", test_vdi.uuid) + try: + vbd.plug() + + yield vm + + finally: + logging.info("cleaning up") + vbd.unplug() + vbd.destroy() + +@pytest.mark.small_vm +def test_vdi_modify(vm_with_plugged_disk): + vm = vm_with_plugged_disk + vm.ssh(["mount /dev/xvdb3 /mnt"]) + vm.ssh(["touch /tmp/foo"]) + vm.ssh(["umount /dev/xvdb3"]) From c952b7ebe5c6f696b2aa1356743526939c47f8e3 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Fri, 5 Jul 2024 11:32:41 +0200 Subject: [PATCH 73/75] WIP allow to inspect VMs on exception when using --pdb FIXME: shutdowns should instead be part of fixtures, so that --pdb allows to inspect them --- lib/installer.py | 8 ++++---- tests/install/test.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/installer.py b/lib/installer.py index 4e598391..f4a0a4e2 100644 --- a/lib/installer.py +++ b/lib/installer.py @@ -126,12 +126,12 @@ def perform_install(*, iso, host_vm): except Exception as e: logging.critical("caught exception %s", e) # wait_for(lambda: False, 'Wait "forever"', timeout_secs=100*60) - host_vm.shutdown(force=True) + #host_vm.shutdown(force=True) raise except KeyboardInterrupt: logging.warning("keyboard interrupt") # wait_for(lambda: False, 'Wait "forever"', timeout_secs=100*60) - host_vm.shutdown(force=True) + #host_vm.shutdown(force=True) raise def monitor_upgrade(*, ip): @@ -204,12 +204,12 @@ def perform_upgrade(*, iso, host_vm): except Exception as e: logging.critical("caught exception %s", e) # wait_for(lambda: False, 'Wait "forever"', timeout_secs=100*60) - host_vm.shutdown(force=True) + #host_vm.shutdown(force=True) raise except KeyboardInterrupt: logging.warning("keyboard interrupt") # wait_for(lambda: False, 'Wait "forever"', timeout_secs=100*60) - host_vm.shutdown(force=True) + #host_vm.shutdown(force=True) raise host_vm.eject_cd() diff --git a/tests/install/test.py b/tests/install/test.py index e7d77212..af1e14d4 100644 --- a/tests/install/test.py +++ b/tests/install/test.py @@ -280,12 +280,12 @@ def _test_firstboot(self, host_vm, mode, *, machine='DEFAULT'): except Exception as e: logging.critical("caught exception %s", e) # wait_for(lambda: False, 'Wait "forever"', timeout_secs=100*60) - host_vm.shutdown(force=True) + #host_vm.shutdown(force=True) raise except KeyboardInterrupt: logging.warning("keyboard interrupt") # wait_for(lambda: False, 'Wait "forever"', timeout_secs=100*60) - host_vm.shutdown(force=True) + #host_vm.shutdown(force=True) raise @pytest.mark.usefixtures("xcpng_chained") @@ -478,10 +478,10 @@ def test_restore(self, create_vms, iso_remaster, except Exception as e: logging.critical("caught exception %s", e) # wait_for(lambda: False, 'Wait "forever"', timeout_secs=100*60) - host_vm.shutdown(force=True) + #host_vm.shutdown(force=True) raise except KeyboardInterrupt: logging.warning("keyboard interrupt") # wait_for(lambda: False, 'Wait "forever"', timeout_secs=100*60) - host_vm.shutdown(force=True) + #host_vm.shutdown(force=True) raise From e86dd1d589c34834169da698112e5d8d765c4c95 Mon Sep 17 00:00:00 2001 From: Guillaume Date: Mon, 12 Aug 2024 17:35:01 +0200 Subject: [PATCH 74/75] add support for pxe Signed-off-by: Guillaume --- data.py-dist | 1 + lib/installer.py | 109 ++++++++++++++++++++++++++++++-------- tests/install/conftest.py | 8 ++- tests/install/test.py | 27 ++++++---- 4 files changed, 112 insertions(+), 33 deletions(-) diff --git a/data.py-dist b/data.py-dist index 4b1a2b63..ddea97f2 100644 --- a/data.py-dist +++ b/data.py-dist @@ -69,6 +69,7 @@ VM_IMAGES = { # By default the parameter is set to False. ISO_IMAGES = { # '83rc1': {'path': "/home/user/iso/xcp-ng-8.3.0-rc1.iso", +# 'pxe-url': "http://server/installers/xcp-ng/8.3-rc1", # 'net-url': "http://server/installers/xcp-ng/8.3-rc1"}, # '83rc1net': {'path': "/home/user/iso/xcp-ng-8.3.0-rc1-netinstall.iso", # 'net-url': "http://server/installers/xcp-ng/8.3-rc1", diff --git a/lib/installer.py b/lib/installer.py index f4a0a4e2..9d936e17 100644 --- a/lib/installer.py +++ b/lib/installer.py @@ -1,10 +1,57 @@ +import atexit import logging +import tempfile +import time import xml.etree.ElementTree as ET from lib import commands, pxe from lib.commands import local_cmd, ssh from lib.common import wait_for +from data import HOST_DEFAULT_PASSWORD_HASH +from data import ISO_IMAGES + +def clean_files_on_pxe(mac_address): + logging.info('cleanning file for mac {}'.format(mac_address)) + pxe.server_remove_config(mac_address) + +def setup_pxe_boot(vm, mac_address, version): + with tempfile.TemporaryDirectory(suffix=mac_address) as tmp_local_path: + logging.info('Generate answerfile.xml for {}'.format(mac_address)) + + hdd = 'nvme0n1' if vm.is_uefi else 'sda' + encrypted_password = HOST_DEFAULT_PASSWORD_HASH + try: + installer = ISO_IMAGES[version]['pxe-url'] + except KeyError: + pytest.skip(f"cannot found pxe-url for {version}") + pxe_addr = pxe.PXE_CONFIG_SERVER + + with open(f'{tmp_local_path}/answerfile.xml', 'w') as answerfile: + answerfile.write(f""" + + fr + {hdd} + {hdd} + {encrypted_password} + {installer} + + Europe/Paris + + + """) + + logging.info('Generate boot.conf for {}'.format(mac_address)) + pxe.generate_boot_conf(tmp_local_path, installer) + + logging.info('Copy files to {}'.format(pxe_addr)) + pxe.server_push_config(mac_address, tmp_local_path) + + logging.info('Register cleanup files for PXE') + atexit.register(lambda: clean_files_on_pxe(mac_address)) + class AnswerFile: def __init__(self, kind, /): from data import BASE_ANSWERFILES @@ -72,36 +119,56 @@ def poweroff(ip): else: raise -def monitor_install(*, ip): - # wait for "yum install" phase to finish - wait_for(lambda: ssh(ip, ["grep", - "'DISPATCH: NEW PHASE: Completing installation'", - "/tmp/install-log"], - check=False, simple_output=False, - ).returncode == 0, - "Wait for rpm installation to succeed", - timeout_secs=40*60) # FIXME too big - - # wait for install to finish - wait_for(lambda: ssh(ip, ["grep", - "'The installation completed successfully'", - "/tmp/install-log"], - check=False, simple_output=False, - ).returncode == 0, - "Wait for system installation to succeed", - timeout_secs=40*60) # FIXME too big +def monitor_install(*, ip, is_pxe=False): + if is_pxe: + # When using PXE boot the IP is available but ssh is not so we just + # need to wait. + while True: + try: + ssh(ip, ["ls"], check=False) + except commands.SSHCommandFailed as e: + if e.returncode == 255: + logging.debug("ssh connexion refused, wait 10s...") + time.sleep(10) + else: + # Raise all other errors + raise e + else: + logging.debug("ssh works") + break + else: + # wait for "yum install" phase to finish + wait_for(lambda: ssh(ip, ["grep", + "'DISPATCH: NEW PHASE: Completing installation'", + "/tmp/install-log"], + check=False, simple_output=False, + ).returncode == 0, + "Wait for rpm installation to succeed", + timeout_secs=40*60) # FIXME too big + + # wait for install to finish + wait_for(lambda: ssh(ip, ["grep", + "'The installation completed successfully'", + "/tmp/install-log"], + check=False, simple_output=False, + ).returncode == 0, + "Wait for system installation to succeed", + timeout_secs=40*60) # FIXME too big wait_for(lambda: ssh(ip, ["ps a|grep '[0-9]. python /opt/xensource/installer/init'"], check=False, simple_output=False, ).returncode == 1, "Wait for installer to terminate") -def perform_install(*, iso, host_vm): +def perform_install(*, iso, host_vm, version=None): vif = host_vm.vifs()[0] mac_address = vif.param_get('MAC') logging.info("Host VM has MAC %s", mac_address) - host_vm.insert_cd(iso) + if iso: + host_vm.insert_cd(iso) + else: + setup_pxe_boot(host_vm, mac_address, version) try: host_vm.start() @@ -116,7 +183,7 @@ def perform_install(*, iso, host_vm): assert len(ips) == 1 host_vm.ip = ips[0] - monitor_install(ip=host_vm.ip) + monitor_install(ip=host_vm.ip, is_pxe = False if iso else True) logging.info("Shutting down Host VM after successful installation") poweroff(host_vm.ip) diff --git a/tests/install/conftest.py b/tests/install/conftest.py index b3985796..6c78e8e1 100644 --- a/tests/install/conftest.py +++ b/tests/install/conftest.py @@ -71,7 +71,7 @@ def iso_remaster(request, answerfile): marker = request.node.get_closest_marker("installer_iso") assert marker is not None, "iso_remaster fixture requires 'installer_iso' marker" param_mapping = marker.kwargs.get("param_mapping", {}) - iso_key = callable_marker(marker.args[0], request, param_mapping=param_mapping) + (iso_key, source_type) = callable_marker(marker.args[0], request, param_mapping=param_mapping) try: source_type = request.getfixturevalue("source_type") @@ -80,6 +80,12 @@ def iso_remaster(request, answerfile): gen_unique_uuid = marker.kwargs.get("gen_unique_uuid", False) + if source_type == "pxe": + # ISO remastering is not needed when booting with PXE so we just return + logging.info("iso_remaster not needed with PXE") + yield None + return + skip, reason = skip_source_type(iso_key, source_type) if skip: pytest.skip(reason) diff --git a/tests/install/test.py b/tests/install/test.py index af1e14d4..1431bab7 100644 --- a/tests/install/test.py +++ b/tests/install/test.py @@ -3,7 +3,7 @@ import time from lib import commands, installer, pxe -from lib.common import safe_split, wait_for +from lib.common import safe_split, wait_for, callable_marker from lib.installer import AnswerFile from lib.pif import PIF from lib.pool import Pool @@ -16,7 +16,7 @@ @pytest.mark.dependency() class TestNested: @pytest.mark.parametrize("local_sr", ("nosr", "ext", "lvm")) - @pytest.mark.parametrize("source_type", ("iso", "net")) + @pytest.mark.parametrize("source_type", ("iso", "net", "pxe")) @pytest.mark.parametrize("iso_version", ( "83rc1", "83rc1net", "83b2", "821.1", @@ -33,7 +33,7 @@ class TestNested: dict(param_name="memory-dynamic-max", value="4GiB"), dict(param_name="memory-dynamic-min", value="4GiB"), dict(param_name="platform", key="exp-nested-hvm", value="true"), # FIXME < 8.3 host? - dict(param_name="HVM-boot-params", key="order", value="dc"), + dict(param_name="HVM-boot-params", key="order", value="dcn"), ) + { "uefi": ( dict(param_name="HVM-boot-params", key="firmware", value="uefi"), @@ -47,16 +47,19 @@ class TestNested: ), param_mapping={"firmware": "firmware"}) @pytest.mark.installer_iso( - lambda iso_version: iso_version, + lambda iso_version, source_type: (iso_version, source_type), gen_unique_uuid=True, - param_mapping={"iso_version": "iso_version"}) + param_mapping={"iso_version": "iso_version", "source_type": "source_type"}) @pytest.mark.answerfile(lambda firmware, local_sr, source_type, version: AnswerFile("INSTALL") \ .top_setattr({} if local_sr == "nosr" else {"sr-type": local_sr}) \ .top_append( {'iso': {"TAG": "source", "type": "local"}, 'net': {"TAG": "source", "type": "url", # FIXME evaluation requires 'net-url' for every ISO - "CONTENTS": ISO_IMAGES[version]['net-url']}}[source_type], + "CONTENTS": ISO_IMAGES[version]['net-url']}, + 'pxe': {"TAG": "source", "type": "url", + # FIXME evaluation requires 'pxe-url' for every ISO + "CONTENTS": ISO_IMAGES[version]['pxe-url']}}[source_type], {"TAG": "primary-disk", "guest-storage": "no" if local_sr == "nosr" else "yes", "CONTENTS": {"uefi": "nvme0n1", "bios": "sda"}[firmware]}, @@ -67,7 +70,9 @@ class TestNested: def test_install(self, create_vms, iso_remaster, firmware, iso_version, source_type, local_sr): assert len(create_vms) == 1 - installer.perform_install(iso=iso_remaster, host_vm=create_vms[0]) + + iso_tool = iso_remaster if source_type != "pxe" else None + installer.perform_install(iso=iso_tool, host_vm=create_vms[0], version=iso_version) @pytest.fixture @staticmethod @@ -368,8 +373,8 @@ def test_firstboot_noninst(self, create_vms, param_mapping={"params": "orig_version", "firmware": "firmware", "local_sr": "local_sr", "source_type": "source_type"}) @pytest.mark.installer_iso( - lambda iso_version: iso_version, - param_mapping={"iso_version": "iso_version"}) + lambda iso_version, source_type: (iso_version, source_type), + param_mapping={"iso_version": "iso_version", "source_type": "source_type"}) @pytest.mark.answerfile( lambda firmware: AnswerFile("UPGRADE").top_append( {"TAG": "source", "type": "local"}, @@ -401,8 +406,8 @@ def test_upgrade(self, create_vms, iso_remaster, param_mapping={"params": "orig_version", "firmware": "firmware", "local_sr": "local_sr", "source_type": "source_type"}) @pytest.mark.installer_iso( - lambda iso_version: iso_version, - param_mapping={"iso_version": "iso_version"}) + lambda iso_version, source_type: (iso_version, source_type), + param_mapping={"iso_version": "iso_version", "source_type": "source_type"}) @pytest.mark.answerfile(lambda firmware: AnswerFile("RESTORE").top_append( {"TAG": "backup-disk", "CONTENTS": {"uefi": "nvme0n1", "bios": "sda"}[firmware]}, From 9f023950805c88624497d0060db76c98f3d3da82 Mon Sep 17 00:00:00 2001 From: Guillaume Date: Tue, 13 Aug 2024 10:04:30 +0200 Subject: [PATCH 75/75] fixup! add support for pxe --- lib/installer.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/installer.py b/lib/installer.py index 9d936e17..1f0ac398 100644 --- a/lib/installer.py +++ b/lib/installer.py @@ -188,7 +188,8 @@ def perform_install(*, iso, host_vm, version=None): logging.info("Shutting down Host VM after successful installation") poweroff(host_vm.ip) wait_for(host_vm.is_halted, "Wait for host VM halted") - host_vm.eject_cd() + if iso: + host_vm.eject_cd() except Exception as e: logging.critical("caught exception %s", e)