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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,17 @@ 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
)
Comment on lines 62 to +65
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extension_name is built from the raw version, but normalize_type_handler_version() strips/validates a sanitized version separately. If the runbook value has leading/trailing whitespace, the extension name can become invalid even though the normalized install version is fine. Consider stripping/validating version once up front, then use the sanitized value for both extension_name and later comparisons/logging.

Copilot uses AI. Check for mistakes.

# Remove any existing extension with the same handler type to avoid
# conflicts (Azure forbids two extensions with the same publisher+type
# but different versions on the same VM).
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(
Expand All @@ -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"
Expand Down
51 changes: 51 additions & 0 deletions lisa/sut_orchestrator/azure/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -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<major>\d+)\.(?P<minor>\d+)(?:\.(?P<patch>\d+))?$"
)

@classmethod
def create_setting(
Expand All @@ -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}'."
Comment on lines +3380 to +3381
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

normalize_type_handler_version() strips whitespace into requested_version, but callers (and the exception message) still use the unstripped version. This makes behavior inconsistent: e.g., a runbook value like "1.7.1 " will normalize correctly for install, but later comparisons / resource naming may still use the whitespace. Consider returning the stripped version (or stripping at the call site) and using the sanitized value consistently for extension name and any version comparisons.

Suggested change
"Invalid extension_version format. Expected 'Major.Minor' "
f"or 'Major.Minor.Patch', got '{version}'."
"Invalid extension_version format after trimming whitespace. "
"Expected 'Major.Minor' or 'Major.Minor.Patch', "
f"got '{requested_version}'. Verify the runbook value does not "
"contain extra characters and uses only numeric version parts."

Copilot uses AI. Check for mistakes.
)

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")
Comment on lines +3400 to +3401
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get_installed_type_handler_version() falls back to returning the string "unknown" when it can't find a version field. That can allow callers (especially the generic test when is_patch_version is false) to proceed without actually having an installed version, masking API/SDK issues. Prefer raising a LisaException when the installed version can't be determined (and include what fields were missing) so failures are explicit and actionable.

Suggested change
return str(
getattr(extension_obj, "type_handler_version_name", "unknown")
version = getattr(extension_obj, "type_handler_version_name", None)
if version:
return str(version)
missing_fields = [
"instance_view.type_handler_version",
"type_handler_version",
"type_handler_version_name",
]
raise LisaException(
f"Unable to determine installed type handler version for extension "
f"'{name}'. Checked fields {missing_fields}, but none were present "
f"or populated in the Azure extension response. instance_view "
f"{'was present' if instance_view is not None else 'was missing'}. "
f"Verify the extension exists and inspect the Azure API/SDK "
f"response for the extension instance view."

Copilot uses AI. Check for mistakes.
)

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

Comment on lines +3404 to +3414
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

assert_installed_type_handler_version() is newly added but appears unused in the repo (no references found). To reduce dead code and keep a single source of truth for the mismatch message/behavior, either use this helper from the test(s) or remove it until there’s a concrete caller.

Suggested change
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

Copilot uses AI. Check for mistakes.
def create_or_update(
self,
type_: str,
Expand Down
Loading