diff --git a/lisa/microsoft/testsuites/core/vm_resize.py b/lisa/microsoft/testsuites/core/vm_resize.py index 178b4f3a0f..335d5f9379 100644 --- a/lisa/microsoft/testsuites/core/vm_resize.py +++ b/lisa/microsoft/testsuites/core/vm_resize.py @@ -127,9 +127,12 @@ def _verify_vm_resize( start_stop.stop() retry = 1 maxretry = 20 + expected_vm_capability: Optional[NodeSpace] = None + origin_vm_size: str = "" + final_vm_size: str = "" + last_error: str = "" while retry < maxretry: try: - expected_vm_capability: Optional[NodeSpace] = None expected_vm_capability, origin_vm_size, final_vm_size = resize.resize( resize_action ) @@ -150,6 +153,7 @@ def _verify_vm_resize( in str(e) or "Following SKUs have failed for Capacity Restrictions" in str(e) ): + last_error = str(e) retry = retry + 1 else: raise e @@ -157,7 +161,17 @@ def _verify_vm_resize( finally: if not hot_resize: start_stop.start() - assert expected_vm_capability, "fail to find proper vm size" + if not expected_vm_capability: + # The retry loop exhausted without a successful resize. This is an + # environment limitation (no compatible candidate happened to be + # picked across all retries), not a defect in the system under + # test, so surface it as SKIPPED rather than the misleading + # "fail to find proper vm size" assertion. + raise SkippedException( + f"no resize-compatible VM size succeeded after {maxretry - 1} " + f"attempts for action {resize_action}. last Azure error: " + f"{last_error or 'unknown'}" + ) test_result.information["final_vm_size"] = final_vm_size test_result.information["origin_vm_size"] = origin_vm_size diff --git a/lisa/sut_orchestrator/azure/features.py b/lisa/sut_orchestrator/azure/features.py index 5e243d6141..d9ac2ce0c7 100644 --- a/lisa/sut_orchestrator/azure/features.py +++ b/lisa/sut_orchestrator/azure/features.py @@ -2546,12 +2546,68 @@ def _check_actual_disk_controller_type( return True + def _get_actual_security_profile(self) -> Optional[SecurityProfileType]: + # Read the security profile the source VM was actually deployed with + # (a concrete value), as opposed to what the SKU advertises in its + # capability SetSpace. Resizing a CVM-deployed VM to a SKU whose + # advertised SetSpace includes CVM AND Standard is fine; resizing + # to a SKU that only advertises Standard will be rejected by Azure + # with PropertyChangeNotAllowed. + if not self._node.capability.features: + return None + for feature in self._node.capability.features: + if feature.type != SecurityProfileSettings.type: + continue + if not isinstance(feature, SecurityProfileSettings): + continue + sp = feature.security_profile + if isinstance(sp, SecurityProfileType): + return sp + if isinstance(sp, search_space.SetSpace): + items = list(sp) + if len(items) == 1: + return items[0] + return None + return None + + def _compare_security_profile( + self, + candidate_size: "AzureCapability", + actual_security_profile: Optional[SecurityProfileType], + ) -> bool: + # The candidate SKU must support the source VM's deployed security + # profile (CVM, TrustedLaunch/SecureBoot, Stateless, Standard). + # Without this filter, the random selector can pick a non-CVM SKU as + # a resize target for a CVM VM, and every retry will fail with + # PropertyChangeNotAllowed. + if not actual_security_profile: + return True + assert candidate_size.capability + assert candidate_size.capability.features + candidate_sp = next( + ( + feature + for feature in candidate_size.capability.features + if feature.type == SecurityProfileSettings.type + ), + None, + ) + if not isinstance(candidate_sp, SecurityProfileSettings): + return True + cand_profiles = candidate_sp.security_profile + if isinstance(cand_profiles, search_space.SetSpace): + return actual_security_profile in cand_profiles + if isinstance(cand_profiles, SecurityProfileType): + return cand_profiles == actual_security_profile + return True + def _is_candidate_size_valid( self, candidate_size: "AzureCapability", current_vm_size: "AzureCapability", resize_action: ResizeAction, actual_disk_controller_type: Optional[schema.DiskControllerType], + actual_security_profile: Optional[SecurityProfileType], ) -> bool: return ( self._compare_architecture(candidate_size, current_vm_size) @@ -2562,6 +2618,7 @@ def _is_candidate_size_valid( and self._check_actual_disk_controller_type( candidate_size, actual_disk_controller_type ) + and self._compare_security_profile(candidate_size, actual_security_profile) and self._compare_size_generation(candidate_size, current_vm_size) and self._compare_network_interface(candidate_size, current_vm_size) and self._compare_core_count(candidate_size, current_vm_size, resize_action) @@ -2626,6 +2683,7 @@ def _select_vm_size( assert current_vm_size.capability.features actual_disk_controller_type = self._get_actual_disk_controller_type() + actual_security_profile = self._get_actual_security_profile() # Filter candidate vm sizes that can't be resized to avail_eligible_intersect = [ @@ -2636,6 +2694,7 @@ def _select_vm_size( current_vm_size, resize_action, actual_disk_controller_type, + actual_security_profile, ) ]