diff --git a/lisa/microsoft/testsuites/vm_extensions/generic_vm_extension.py b/lisa/microsoft/testsuites/vm_extensions/generic_vm_extension.py index 3797f52367..be248bf980 100644 --- a/lisa/microsoft/testsuites/vm_extensions/generic_vm_extension.py +++ b/lisa/microsoft/testsuites/vm_extensions/generic_vm_extension.py @@ -60,6 +60,9 @@ def verify_vm_extension_install_uninstall( extension = node.features[AzureExtension] extension_name = f"{publisher}.{type_}-{version}" + install_version, is_patch_version = extension.normalize_type_handler_version( + version + ) # Remove any existing extension with the same handler type to avoid # conflicts (Azure forbids two extensions with the same publisher+type @@ -67,7 +70,7 @@ def verify_vm_extension_install_uninstall( self._cleanup_existing_extensions(extension, publisher, type_, log) extension_result = self._install_extension( - extension, extension_name, publisher, type_, version + extension, extension_name, publisher, type_, install_version ) assert_that(extension_result["provisioning_state"]).described_as( @@ -78,6 +81,20 @@ def verify_vm_extension_install_uninstall( "Expected VM extension to exist after installation" ).is_true() + installed_version = extension.get_installed_type_handler_version( + extension_name + ) + if is_patch_version: + assert_that(installed_version).described_as( + f"Installed extension '{extension_name}' version mismatch: expected " + f"'{version}', actual '{installed_version}'. Please double confirm if " + f"the Azure platform supports {installed_version}" + ).is_equal_to(version) + log.info( + f"Installed extension '{extension_name}' " + f"version: {installed_version}" + ) + # Verify the VM is still reachable after extension operations. assert_that(node.test_connection()).described_as( "Expected VM to be reachable via SSH after extension installation" diff --git a/lisa/sut_orchestrator/azure/features.py b/lisa/sut_orchestrator/azure/features.py index f0ad814780..90b332dffc 100644 --- a/lisa/sut_orchestrator/azure/features.py +++ b/lisa/sut_orchestrator/azure/features.py @@ -3340,6 +3340,9 @@ def delete_share(self) -> None: class AzureExtension(AzureFeatureMixin, Feature): RESOURCE_NOT_FOUND = re.compile(r"ResourceNotFound", re.M) + _TYPE_HANDLER_VERSION_PATTERN = re.compile( + r"^(?P\d+)\.(?P\d+)(?:\.(?P\d+))?$" + ) @classmethod def create_setting( @@ -3361,6 +3364,54 @@ def get( ) return extension + def normalize_type_handler_version(self, version: str) -> Tuple[str, bool]: + """ + Normalize a requested extension version for installation. + + Returns: + Tuple[str, bool]: + - normalized Major.Minor version for installation + - True if the original version was Major.Minor.Patch + """ + requested_version = version.strip() + matched = self._TYPE_HANDLER_VERSION_PATTERN.fullmatch(requested_version) + if not matched: + raise LisaException( + "Invalid extension_version format. Expected 'Major.Minor' " + f"or 'Major.Minor.Patch', got '{version}'." + ) + + normalized_version = f"{matched.group('major')}.{matched.group('minor')}" + is_patch_version = matched.group("patch") is not None + return normalized_version, is_patch_version + + def get_installed_type_handler_version(self, name: str) -> str: + extension_obj = self.get(name=name) + + instance_view = getattr(extension_obj, "instance_view", None) + version = getattr(instance_view, "type_handler_version", None) + if version: + return str(version) + + version = getattr(extension_obj, "type_handler_version", None) + if version: + return str(version) + + return str( + getattr(extension_obj, "type_handler_version_name", "unknown") + ) + + def assert_installed_type_handler_version( + self, name: str, expected_version: str + ) -> str: + actual_version = self.get_installed_type_handler_version(name) + if actual_version != expected_version: + raise LisaException( + f"Installed extension '{name}' version mismatch: " + f"expected '{expected_version}', actual '{actual_version}'." + ) + return actual_version + def create_or_update( self, type_: str,