diff --git a/lib/host.py b/lib/host.py index 29ffd9b07..c41dbc8ad 100644 --- a/lib/host.py +++ b/lib/host.py @@ -359,9 +359,10 @@ def pool_has_vm(self, vm_uuid, vm_type='vm'): else: return self.xe('vm-list', {'uuid': vm_uuid}, minimal=True) == vm_uuid - def install_updates(self): + def install_updates(self, enablerepo=None): logging.info("Install updates on host %s" % self) - return self.ssh(['yum', 'update', '-y']) + enablerepo_cmd = ['--enablerepo=%s' % enablerepo] if enablerepo is not None else [] + return self.ssh(['yum', 'update', '-y'] + enablerepo_cmd) def restart_toolstack(self, verify=False): logging.info("Restart toolstack on host %s" % self) @@ -376,10 +377,11 @@ def is_enabled(self) -> bool: # If XAPI is not ready yet, or the host is down, this will throw. We return False in that case. return False - def has_updates(self): + def has_updates(self, enablerepo=None): + enablerepo_cmd = ['--enablerepo=%s' % enablerepo] if enablerepo is not None else [] try: # yum check-update returns 100 if there are updates, 1 if there's an error, 0 if no updates - self.ssh(['yum', 'check-update']) + self.ssh(['yum', 'check-update'] + enablerepo_cmd) # returned 0, else there would have been a SSHCommandFailed return False except commands.SSHCommandFailed as e: diff --git a/pytest.ini b/pytest.ini index fe93b353b..76515c050 100644 --- a/pytest.ini +++ b/pytest.ini @@ -5,6 +5,7 @@ markers = default_vm: mark a test with a default VM in case no --vm parameter was given. # *** Markers used to select tests at collect stage *** + upgrade_test: mark a test which will upgrade packages from testing repo # * Host-related markers, automatically set based on fixtures hostA2: a second member in the first pool. diff --git a/tests/storage/linstor/conftest.py b/tests/storage/linstor/conftest.py index afb09da4d..788f7c44b 100644 --- a/tests/storage/linstor/conftest.py +++ b/tests/storage/linstor/conftest.py @@ -49,9 +49,20 @@ def storage_pool_name(provisioning_type): def provisioning_type(request): return request.param +def pytest_configure(config): + config._linstor_upgrade_test = False + +def pytest_collection_modifyitems(config, items): + for item in items: + if item.get_closest_marker("upgrade_test"): + config._linstor_upgrade_test = True + break + @pytest.fixture(scope='package') -def pool_with_linstor(hostA2, lvm_disks, pool_with_saved_yum_state): +def pool_with_linstor(hostA2, lvm_disks, pool_with_saved_yum_state, request): import concurrent.futures + + dont_use_testing_repo = request.config._linstor_upgrade_test pool = pool_with_saved_yum_state def check_linstor_installed(host): @@ -66,7 +77,10 @@ def check_linstor_installed(host): def install_linstor(host): logging.info(f"Installing {LINSTOR_PACKAGE} on host {host}...") host.yum_install([LINSTOR_RELEASE_PACKAGE]) - host.yum_install([LINSTOR_PACKAGE], enablerepo="xcp-ng-linstor-testing") + if dont_use_testing_repo: + host.yum_install([LINSTOR_PACKAGE]) + else: + host.yum_install([LINSTOR_PACKAGE], enablerepo="xcp-ng-linstor-testing") # Needed because the linstor driver is not in the xapi sm-plugins list # before installing the LINSTOR packages. host.ssh(["systemctl", "restart", "multipathd"]) diff --git a/tests/storage/linstor/test_linstor_sr.py b/tests/storage/linstor/test_linstor_sr.py index 7dc6f4597..bad7b2e37 100644 --- a/tests/storage/linstor/test_linstor_sr.py +++ b/tests/storage/linstor/test_linstor_sr.py @@ -131,6 +131,56 @@ def test_linstor_missing(self, linstor_sr, host): if not linstor_installed: host.yum_install([LINSTOR_PACKAGE]) + @pytest.mark.reboot + @pytest.mark.small_vm + @pytest.mark.upgrade_test + def test_linstor_sr_pool_update(self, linstor_sr, vm_on_linstor_sr): + """ + Perform update on the Linstor SR pool hosts while ensuring VM availability. + 1. Identify all hosts in the SR pool and order them with the master first. + 2. Update all hosts if updates are available. + 3. Reboot all hosts. + 4. Sequentially ensure that the VM can start on all hosts. + """ + import concurrent.futures, threading + + sr = linstor_sr + vm = vm_on_linstor_sr + updates_applied = [] + updates_lock = threading.Lock() + + # Sort hosts so that pool master is first (optional) + hosts = sorted(sr.pool.hosts, key=lambda h: h != sr.pool.master) + + # RPU is disabled for pools with XOSTOR SRs. + # LINSTOR expects that we always use satellites and controllers with the same version on all hosts. + def install_updates_on(host): + logging.info("Checking on host %s", host.hostname_or_ip) + if host.has_updates(enablerepo="xcp-ng-linstor-testing"): + host.install_updates(enablerepo="xcp-ng-linstor-testing") + with updates_lock: + updates_applied.append(host) + else: + logging.info("No updates available for host %s", host.hostname_or_ip) + + with concurrent.futures.ThreadPoolExecutor() as executor: + executor.map(install_updates_on, hosts) + + # Reboot updated hosts + def reboot_updated(host): + host.reboot(verify=True) + + with concurrent.futures.ThreadPoolExecutor() as executor: + executor.map(reboot_updated, updates_applied) + + # Ensure VM is able to boot on all the hosts + for h in hosts: + vm.start(on=h.uuid) + vm.wait_for_os_booted() + vm.shutdown(verify=True) + + sr.scan() + # *** End of tests with reboots # --- Test diskless resources --------------------------------------------------