From f4fe6ac65603134bb7732eff67204335830ca0be Mon Sep 17 00:00:00 2001 From: Yashna Parikh Date: Tue, 12 May 2026 16:08:58 -0400 Subject: [PATCH 1/6] Raise Exception for Azure Linux4 and RHEL10 not supoorted yet --- src/core/src/bootstrap/EnvLayer.py | 22 ++++++++++++-- src/core/tests/Test_EnvLayer.py | 49 ++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/src/core/src/bootstrap/EnvLayer.py b/src/core/src/bootstrap/EnvLayer.py index f79f7b2c..a09c5d70 100644 --- a/src/core/src/bootstrap/EnvLayer.py +++ b/src/core/src/bootstrap/EnvLayer.py @@ -49,7 +49,7 @@ def is_distro_azure_linux(distro_name): def is_distro_azure_linux_3_or_beyond(self): # type: () -> bool - """ Checks if the current distro is Azure Linux 3 """ + """ Checks if the current distro is Azure Linux 3 or beyond """ if self.is_distro_azure_linux(self.platform.linux_distribution()): version = distro.os_release_attr('version') major = version.split('.')[0] if version else None @@ -62,7 +62,19 @@ def get_package_manager(self): if platform.system() == 'Windows': return Constants.APT + version = distro.os_release_attr('version') + major = version.split('.')[0] if version else None + os_id = distro.id() + + # Check for Azure Linux if self.is_distro_azure_linux(str(self.platform.linux_distribution())): + # Azure Linux 4 not yet supported + if major is not None and int(major) >= 4: + error_msg = "Azure Linux 4 is not yet supported in your region. Please review aka.ms/LinuxPatchExtension for more information." + print("Error: {0}".format(error_msg)) + raise Exception(error_msg) + + # Azure Linux 3 and below use TDNF code, out = self.run_command_output('which tdnf', False, False) if code == 0: return Constants.TDNF @@ -70,7 +82,13 @@ def get_package_manager(self): print("Error: Expected package manager tdnf not found on this Azure Linux VM.") return str() - # choose default package manager + # Check for RHEL 10 (not yet supported) + if os_id == "rhel" and major is not None and int(major) >= 10: + error_msg = "RHEL 10 is not yet supported in your region. Please review aka.ms/LinuxPatchExtension for more information." + print("Error: {0}".format(error_msg)) + raise Exception(error_msg) + + # Choose default package manager package_manager_map = (('apt-get', Constants.APT), ('yum', Constants.YUM), ('zypper', Constants.ZYPPER)) diff --git a/src/core/tests/Test_EnvLayer.py b/src/core/tests/Test_EnvLayer.py index 37db4055..28c32cfe 100644 --- a/src/core/tests/Test_EnvLayer.py +++ b/src/core/tests/Test_EnvLayer.py @@ -71,6 +71,18 @@ def mock_distro_os_release_attr_return_azure_linux_2(self, attribute): def mock_distro_os_release_attr_return_none(self, attribute): return None + + def mock_distro_os_release_attr_return_azure_linux_4(self, attribute): + return '4.0.2' + + def mock_distro_os_release_attr_return_rhel_10(self, attribute): + return '10.0' + + def mock_linux_distribution_to_return_azure_linux_4(self): + return ['Microsoft Azure Linux', '4.0', ''] + + def mock_distro_id_return_rhel(self): + return 'rhel' # endregion def test_get_package_manager(self): @@ -139,6 +151,43 @@ def test_platform(self): self.envlayer.platform.cpu_arch() self.envlayer.platform.vm_name() + def test_get_package_manager_azure_linux_4_not_supported(self): + """Test that Azure Linux 4 raises an exception""" + self.backup_linux_distribution = self.envlayer.platform.linux_distribution + self.backup_distro_os_release_attr = distro.os_release_attr + + platform.system = self.mock_platform_system + self.envlayer.platform.linux_distribution = self.mock_linux_distribution_to_return_azure_linux_4 + distro.os_release_attr = self.mock_distro_os_release_attr_return_azure_linux_4 + + with self.assertRaises(Exception) as context: + self.envlayer.get_package_manager() + self.assertEqual(str(context.exception), "Azure Linux 4 is not yet supported in your region. Please review aka.ms/LinuxPatchExtension for more information.") + + # restore + distro.os_release_attr = self.backup_distro_os_release_attr + self.envlayer.platform.linux_distribution = self.backup_linux_distribution + + def test_get_package_manager_rhel_10_not_supported(self): + """Test that RHEL 10 raises an exception""" + self.backup_linux_distribution = self.envlayer.platform.linux_distribution + self.backup_distro_os_release_attr = distro.os_release_attr + self.backup_distro_id = distro.id + + platform.system = self.mock_platform_system + self.envlayer.platform.linux_distribution = self.mock_linux_distribution + distro.os_release_attr = self.mock_distro_os_release_attr_return_rhel_10 + distro.id = self.mock_distro_id_return_rhel + + with self.assertRaises(Exception) as context: + self.envlayer.get_package_manager() + self.assertEqual(str(context.exception), "RHEL 10 is not yet supported in your region. Please review aka.ms/LinuxPatchExtension for more information.") + + # restore + distro.id = self.backup_distro_id + distro.os_release_attr = self.backup_distro_os_release_attr + self.envlayer.platform.linux_distribution = self.backup_linux_distribution + if __name__ == '__main__': unittest.main() \ No newline at end of file From f9caaf7c8fcf612ceec50c57c9f3a8c52a221b00 Mon Sep 17 00:00:00 2001 From: Yashna Parikh Date: Tue, 12 May 2026 16:53:03 -0400 Subject: [PATCH 2/6] Address Copilot comments --- src/core/src/bootstrap/EnvLayer.py | 6 +++--- src/core/tests/Test_EnvLayer.py | 24 ++++++++++++++++-------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/core/src/bootstrap/EnvLayer.py b/src/core/src/bootstrap/EnvLayer.py index a09c5d70..50ba6b17 100644 --- a/src/core/src/bootstrap/EnvLayer.py +++ b/src/core/src/bootstrap/EnvLayer.py @@ -49,7 +49,7 @@ def is_distro_azure_linux(distro_name): def is_distro_azure_linux_3_or_beyond(self): # type: () -> bool - """ Checks if the current distro is Azure Linux 3 or beyond """ + """ Checks if the current distro is Azure Linux 3 """ if self.is_distro_azure_linux(self.platform.linux_distribution()): version = distro.os_release_attr('version') major = version.split('.')[0] if version else None @@ -69,7 +69,7 @@ def get_package_manager(self): # Check for Azure Linux if self.is_distro_azure_linux(str(self.platform.linux_distribution())): # Azure Linux 4 not yet supported - if major is not None and int(major) >= 4: + if major is not None and int(major) == 4: error_msg = "Azure Linux 4 is not yet supported in your region. Please review aka.ms/LinuxPatchExtension for more information." print("Error: {0}".format(error_msg)) raise Exception(error_msg) @@ -83,7 +83,7 @@ def get_package_manager(self): return str() # Check for RHEL 10 (not yet supported) - if os_id == "rhel" and major is not None and int(major) >= 10: + if os_id == "rhel" and major is not None and int(major) == 10: error_msg = "RHEL 10 is not yet supported in your region. Please review aka.ms/LinuxPatchExtension for more information." print("Error: {0}".format(error_msg)) raise Exception(error_msg) diff --git a/src/core/tests/Test_EnvLayer.py b/src/core/tests/Test_EnvLayer.py index 28c32cfe..710085d4 100644 --- a/src/core/tests/Test_EnvLayer.py +++ b/src/core/tests/Test_EnvLayer.py @@ -91,28 +91,32 @@ def test_get_package_manager(self): self.backup_linux_distribution = self.envlayer.platform.linux_distribution self.envlayer.platform.linux_distribution = self.mock_linux_distribution self.backup_run_command_output = self.envlayer.run_command_output + self.backup_distro_os_release_attr = distro.os_release_attr test_input_output_table = [ - [self.mock_run_command_for_apt, self.mock_linux_distribution, Constants.APT], - [self.mock_run_command_for_tdnf, self.mock_linux_distribution_to_return_azure_linux_3, Constants.TDNF], - [self.mock_run_command_for_yum, self.mock_linux_distribution_to_return_azure_linux_3, str()], # check for Azure Linux machine which does not have tdnf - [self.mock_run_command_for_tdnf, self.mock_linux_distribution_to_return_azure_linux_2, Constants.TDNF], - [self.mock_run_command_for_yum, self.mock_linux_distribution, Constants.YUM], - [self.mock_run_command_for_zypper, self.mock_linux_distribution, Constants.ZYPPER], - [lambda cmd, no_output, chk_err: (-1, ''), self.mock_linux_distribution, str()], # no matches for any package manager + [self.mock_run_command_for_apt, self.mock_linux_distribution, self.mock_distro_os_release_attr_return_none, Constants.APT], + [self.mock_run_command_for_tdnf, self.mock_linux_distribution_to_return_azure_linux_3, self.mock_distro_os_release_attr_return_azure_linux_3, Constants.TDNF], + [self.mock_run_command_for_yum, self.mock_linux_distribution_to_return_azure_linux_3, self.mock_distro_os_release_attr_return_azure_linux_3, str()], # check for Azure Linux machine which does not have tdnf + [self.mock_run_command_for_tdnf, self.mock_linux_distribution_to_return_azure_linux_2, self.mock_distro_os_release_attr_return_azure_linux_2, Constants.TDNF], + [self.mock_run_command_for_yum, self.mock_linux_distribution, self.mock_distro_os_release_attr_return_none, Constants.YUM], + [self.mock_run_command_for_zypper, self.mock_linux_distribution, self.mock_distro_os_release_attr_return_none, Constants.ZYPPER], + [lambda cmd, no_output, chk_err: (-1, ''), self.mock_linux_distribution, self.mock_distro_os_release_attr_return_none, str()], # no matches for any package manager ] for row in test_input_output_table: self.envlayer.run_command_output = row[0] self.envlayer.platform.linux_distribution = row[1] + distro.os_release_attr = row[2] package_manager = self.envlayer.get_package_manager() - self.assertTrue(package_manager is row[2]) + self.assertTrue(package_manager is row[3]) # test for Windows platform.system = self.mock_platform_system_windows + distro.os_release_attr = self.mock_distro_os_release_attr_return_none self.assertEqual(self.envlayer.get_package_manager(), Constants.APT) # restore original methods + distro.os_release_attr = self.backup_distro_os_release_attr self.envlayer.run_command_output = self.backup_run_command_output self.envlayer.platform.linux_distribution = self.backup_linux_distribution platform.system = self.backup_platform_system @@ -153,6 +157,7 @@ def test_platform(self): def test_get_package_manager_azure_linux_4_not_supported(self): """Test that Azure Linux 4 raises an exception""" + self.backup_platform_system = platform.system self.backup_linux_distribution = self.envlayer.platform.linux_distribution self.backup_distro_os_release_attr = distro.os_release_attr @@ -167,9 +172,11 @@ def test_get_package_manager_azure_linux_4_not_supported(self): # restore distro.os_release_attr = self.backup_distro_os_release_attr self.envlayer.platform.linux_distribution = self.backup_linux_distribution + platform.system = self.backup_platform_system def test_get_package_manager_rhel_10_not_supported(self): """Test that RHEL 10 raises an exception""" + self.backup_platform_system = platform.system self.backup_linux_distribution = self.envlayer.platform.linux_distribution self.backup_distro_os_release_attr = distro.os_release_attr self.backup_distro_id = distro.id @@ -187,6 +194,7 @@ def test_get_package_manager_rhel_10_not_supported(self): distro.id = self.backup_distro_id distro.os_release_attr = self.backup_distro_os_release_attr self.envlayer.platform.linux_distribution = self.backup_linux_distribution + platform.system = self.backup_platform_system if __name__ == '__main__': From dfa21149fa7ddd037bbe3e5292193a8737b179db Mon Sep 17 00:00:00 2001 From: Yashna Parikh Date: Thu, 14 May 2026 12:24:02 -0400 Subject: [PATCH 3/6] Address code review --- src/core/src/bootstrap/Constants.py | 2 +- src/core/src/bootstrap/EnvLayer.py | 75 ++++++++++++++++++++--------- src/core/tests/Test_EnvLayer.py | 40 ++++++++++++--- 3 files changed, 85 insertions(+), 32 deletions(-) diff --git a/src/core/src/bootstrap/Constants.py b/src/core/src/bootstrap/Constants.py index 51da02c5..8c7c8c73 100644 --- a/src/core/src/bootstrap/Constants.py +++ b/src/core/src/bootstrap/Constants.py @@ -197,7 +197,7 @@ class StatusTruncationConfig(EnumBackport): RED_HAT = 'Red Hat' SUSE = 'SUSE' CENTOS = 'CentOS' - AZURE_LINUX = ['Microsoft Azure Linux', 'Common Base Linux Mariner'] + AZURE_LINUX = ['Azure Linux', 'Microsoft Azure Linux', 'Common Base Linux Mariner'] # Package Managers APT = 'apt' diff --git a/src/core/src/bootstrap/EnvLayer.py b/src/core/src/bootstrap/EnvLayer.py index 50ba6b17..1ebec087 100644 --- a/src/core/src/bootstrap/EnvLayer.py +++ b/src/core/src/bootstrap/EnvLayer.py @@ -47,13 +47,21 @@ def __init__(self): def is_distro_azure_linux(distro_name): return any(x in distro_name for x in Constants.AZURE_LINUX) + @staticmethod + def _get_major_version(): + # type: () -> str + """ Extracts major version from distro.os_release_attr('version'). + Returns major version as string or None if not available. """ + version = distro.os_release_attr('version') + major = version.split('.')[0] if version else None + return major + def is_distro_azure_linux_3_or_beyond(self): # type: () -> bool """ Checks if the current distro is Azure Linux 3 """ if self.is_distro_azure_linux(self.platform.linux_distribution()): - version = distro.os_release_attr('version') - major = version.split('.')[0] if version else None - return major is not None and int(major) >= 3 + major = self._get_major_version() + return major is not None and int(major) == 3 return False def get_package_manager(self): @@ -62,31 +70,20 @@ def get_package_manager(self): if platform.system() == 'Windows': return Constants.APT - version = distro.os_release_attr('version') - major = version.split('.')[0] if version else None + major = self._get_major_version() os_id = distro.id() # Check for Azure Linux if self.is_distro_azure_linux(str(self.platform.linux_distribution())): - # Azure Linux 4 not yet supported - if major is not None and int(major) == 4: - error_msg = "Azure Linux 4 is not yet supported in your region. Please review aka.ms/LinuxPatchExtension for more information." - print("Error: {0}".format(error_msg)) - raise Exception(error_msg) - - # Azure Linux 3 and below use TDNF - code, out = self.run_command_output('which tdnf', False, False) - if code == 0: - return Constants.TDNF - else: - print("Error: Expected package manager tdnf not found on this Azure Linux VM.") - return str() + return self._get_azure_linux_package_manager(major) - # Check for RHEL 10 (not yet supported) - if os_id == "rhel" and major is not None and int(major) == 10: - error_msg = "RHEL 10 is not yet supported in your region. Please review aka.ms/LinuxPatchExtension for more information." - print("Error: {0}".format(error_msg)) - raise Exception(error_msg) + # Check for RHEL + if os_id == "rhel": + package_manager = self._get_rhel_package_manager(major) + # Helper will raise exception for unsupported versions (e.g., RHEL 10). + # If it returns a value, use it; otherwise fall through to default detect + if package_manager is not None: + return package_manager # Choose default package manager package_manager_map = (('apt-get', Constants.APT), @@ -99,6 +96,38 @@ def get_package_manager(self): return str() + def _get_azure_linux_package_manager(self, major): + # type: (str) -> str + """ Determines the package manager for Azure Linux based on version. + Azure Linux 3 and below use TDNF. Azure Linux 4 use DNF (not yet supported). + Logs error message if version is unsupported. """ + # Azure Linux 4 not yet supported + if major is not None and int(major) == 4: + error_msg = "Azure Linux 4 is not yet supported in your region. Please review https://aka.ms/VMGuestPatchingCompatibility for more information." + print("Error: {0}".format(error_msg)) + return str() + + # Azure Linux 3 and below use TDNF + code, out = self.run_command_output('which tdnf', False, False) + if code == 0: + return Constants.TDNF + else: + print("Error: Expected package manager tdnf not found on this Azure Linux VM.") + return str() + + def _get_rhel_package_manager(self, major): + # type: (str) -> str + """ Determines the package manager for RHEL based on version. + Returns DNF when RHEL 10+ is supported. Logs error message if unsupported. """ + # RHEL 10 not yet supported + if major is not None and int(major) == 10: + error_msg = "RHEL 10 is not yet supported in your region. Please review https://aka.ms/VMGuestPatchingCompatibility for more information." + print("Error: {0}".format(error_msg)) + return str() + + # return None to allow default package manager detection + return None + def set_env_var(self, var_name, var_value=str(), raise_if_not_success=False): # type: (str, str, bool) -> None """ Sets an environment variable with var_name and var_value in /etc/environment. If it already exists, it is overwritten. """ diff --git a/src/core/tests/Test_EnvLayer.py b/src/core/tests/Test_EnvLayer.py index 710085d4..0dc5d07f 100644 --- a/src/core/tests/Test_EnvLayer.py +++ b/src/core/tests/Test_EnvLayer.py @@ -13,7 +13,9 @@ # limitations under the License. # # Requires Python 2.7+ +import io import platform +import sys import unittest from core.src.bootstrap.EnvLayer import EnvLayer from core.src.bootstrap.Constants import Constants @@ -156,7 +158,7 @@ def test_platform(self): self.envlayer.platform.vm_name() def test_get_package_manager_azure_linux_4_not_supported(self): - """Test that Azure Linux 4 raises an exception""" + """Test that Azure Linux 4 logs unsupported message""" self.backup_platform_system = platform.system self.backup_linux_distribution = self.envlayer.platform.linux_distribution self.backup_distro_os_release_attr = distro.os_release_attr @@ -165,9 +167,20 @@ def test_get_package_manager_azure_linux_4_not_supported(self): self.envlayer.platform.linux_distribution = self.mock_linux_distribution_to_return_azure_linux_4 distro.os_release_attr = self.mock_distro_os_release_attr_return_azure_linux_4 - with self.assertRaises(Exception) as context: - self.envlayer.get_package_manager() - self.assertEqual(str(context.exception), "Azure Linux 4 is not yet supported in your region. Please review aka.ms/LinuxPatchExtension for more information.") + # Capture stdout to check for log message + captured_output = io.StringIO() + sys.stdout = captured_output + + result = self.envlayer.get_package_manager() + + sys.stdout = sys.__stdout__ + output = captured_output.getvalue() + + # Verify exact error message is logged + expected_msg = "Error: Azure Linux 4 is not yet supported in your region. Please review https://aka.ms/VMGuestPatchingCompatibility for more information." + self.assertEqual(expected_msg in output, True) + # Verify empty string is returned + self.assertEqual(result, "") # restore distro.os_release_attr = self.backup_distro_os_release_attr @@ -175,7 +188,7 @@ def test_get_package_manager_azure_linux_4_not_supported(self): platform.system = self.backup_platform_system def test_get_package_manager_rhel_10_not_supported(self): - """Test that RHEL 10 raises an exception""" + """Test that RHEL 10 logs unsupported message""" self.backup_platform_system = platform.system self.backup_linux_distribution = self.envlayer.platform.linux_distribution self.backup_distro_os_release_attr = distro.os_release_attr @@ -186,9 +199,20 @@ def test_get_package_manager_rhel_10_not_supported(self): distro.os_release_attr = self.mock_distro_os_release_attr_return_rhel_10 distro.id = self.mock_distro_id_return_rhel - with self.assertRaises(Exception) as context: - self.envlayer.get_package_manager() - self.assertEqual(str(context.exception), "RHEL 10 is not yet supported in your region. Please review aka.ms/LinuxPatchExtension for more information.") + # Capture stdout to check for log message + captured_output = io.StringIO() + sys.stdout = captured_output + + result = self.envlayer.get_package_manager() + + sys.stdout = sys.__stdout__ + output = captured_output.getvalue() + + # Verify exact error message is logged + expected_msg = "Error: RHEL 10 is not yet supported in your region. Please review https://aka.ms/VMGuestPatchingCompatibility for more information." + self.assertEqual(expected_msg in output, True) + # Verify empty string is returned + self.assertEqual(result, "") # restore distro.id = self.backup_distro_id From 73da72b9db3c366c7280c81a4f0fe10960f119f1 Mon Sep 17 00:00:00 2001 From: Yashna Parikh Date: Mon, 18 May 2026 12:47:45 -0400 Subject: [PATCH 4/6] Address Code Review comments --- src/core/src/bootstrap/EnvLayer.py | 98 +++++++++---------- .../AzL3TdnfPackageManager.py | 3 +- src/core/tests/Test_EnvLayer.py | 63 ++++++------ 3 files changed, 76 insertions(+), 88 deletions(-) diff --git a/src/core/src/bootstrap/EnvLayer.py b/src/core/src/bootstrap/EnvLayer.py index 1ebec087..1618e366 100644 --- a/src/core/src/bootstrap/EnvLayer.py +++ b/src/core/src/bootstrap/EnvLayer.py @@ -48,21 +48,40 @@ def is_distro_azure_linux(distro_name): return any(x in distro_name for x in Constants.AZURE_LINUX) @staticmethod - def _get_major_version(): - # type: () -> str - """ Extracts major version from distro.os_release_attr('version'). + def __try_get_major_version(): + # type: () -> str or None + """ Attempts to get major version from distro.os_release_attr('version'). Returns major version as string or None if not available. """ version = distro.os_release_attr('version') major = version.split('.')[0] if version else None return major - def is_distro_azure_linux_3_or_beyond(self): - # type: () -> bool + + def __is_matching_distro_and_version(self, distro_name, distro_to_match, version_to_match): + # type: (str, str, int) -> bool + """ Checks if distro name matches and version matches the expected version. + Returns True if both distro and version match, False otherwise. """ + if distro_to_match == Constants.AZURE_LINUX and not self.is_distro_azure_linux(str(distro_name)): + return False + elif distro_to_match == Constants.RED_HAT and not Constants.RED_HAT.lower() in str(distro_name).lower(): + return False + major = self.__try_get_major_version() + return major is not None and int(major) == version_to_match + + def is_distro_azure_linux_3(self, distro_name): + # type: (str) -> bool """ Checks if the current distro is Azure Linux 3 """ - if self.is_distro_azure_linux(self.platform.linux_distribution()): - major = self._get_major_version() - return major is not None and int(major) == 3 - return False + return self.__is_matching_distro_and_version(distro_name, Constants.AZURE_LINUX, version_to_match=3) + + def is_distro_azure_linux_4(self, distro_name): + # type: (str) -> bool + """ Checks if the current distro is Azure Linux 4 """ + return self.__is_matching_distro_and_version(distro_name, Constants.AZURE_LINUX, version_to_match=4) + + def is_distro_rhel_10(self, distro_name): + # type: (str) -> bool + """ Checks if the current distro is RHEL 10 """ + return self.__is_matching_distro_and_version(distro_name, Constants.RED_HAT, version_to_match=10) def get_package_manager(self): # type: () -> str @@ -70,20 +89,25 @@ def get_package_manager(self): if platform.system() == 'Windows': return Constants.APT - major = self._get_major_version() - os_id = distro.id() + # platform.linux_distribution() returns a tuple with distro_name, version and a code + # Example: ['Azure Linux', '4.0', ''] + os_name, os_version, os_code = self.platform.linux_distribution() - # Check for Azure Linux - if self.is_distro_azure_linux(str(self.platform.linux_distribution())): - return self._get_azure_linux_package_manager(major) + # Check for unsupported distros + if self.is_distro_azure_linux_4(os_name) or self.is_distro_rhel_10(os_name): + error_msg = "This distro is not yet supported in your region. Please review https://aka.ms/VMGuestPatchingCompatibility for more information. [Distro={0}]".format(str(os_name)) + print("Error: {0}".format(error_msg)) + return str() + + # Check for Azure Linux (3 and below use TDNF) + if self.is_distro_azure_linux(str(os_name)): + code, out = self.run_command_output('which tdnf', False, False) + if code == 0: + return Constants.TDNF + else: + print("Error: Expected package manager tdnf not found on this Azure Linux VM.") + return str() - # Check for RHEL - if os_id == "rhel": - package_manager = self._get_rhel_package_manager(major) - # Helper will raise exception for unsupported versions (e.g., RHEL 10). - # If it returns a value, use it; otherwise fall through to default detect - if package_manager is not None: - return package_manager # Choose default package manager package_manager_map = (('apt-get', Constants.APT), @@ -96,38 +120,6 @@ def get_package_manager(self): return str() - def _get_azure_linux_package_manager(self, major): - # type: (str) -> str - """ Determines the package manager for Azure Linux based on version. - Azure Linux 3 and below use TDNF. Azure Linux 4 use DNF (not yet supported). - Logs error message if version is unsupported. """ - # Azure Linux 4 not yet supported - if major is not None and int(major) == 4: - error_msg = "Azure Linux 4 is not yet supported in your region. Please review https://aka.ms/VMGuestPatchingCompatibility for more information." - print("Error: {0}".format(error_msg)) - return str() - - # Azure Linux 3 and below use TDNF - code, out = self.run_command_output('which tdnf', False, False) - if code == 0: - return Constants.TDNF - else: - print("Error: Expected package manager tdnf not found on this Azure Linux VM.") - return str() - - def _get_rhel_package_manager(self, major): - # type: (str) -> str - """ Determines the package manager for RHEL based on version. - Returns DNF when RHEL 10+ is supported. Logs error message if unsupported. """ - # RHEL 10 not yet supported - if major is not None and int(major) == 10: - error_msg = "RHEL 10 is not yet supported in your region. Please review https://aka.ms/VMGuestPatchingCompatibility for more information." - print("Error: {0}".format(error_msg)) - return str() - - # return None to allow default package manager detection - return None - def set_env_var(self, var_name, var_value=str(), raise_if_not_success=False): # type: (str, str, bool) -> None """ Sets an environment variable with var_name and var_value in /etc/environment. If it already exists, it is overwritten. """ diff --git a/src/core/src/package_managers/AzL3TdnfPackageManager.py b/src/core/src/package_managers/AzL3TdnfPackageManager.py index 0a9cface..72c431a1 100644 --- a/src/core/src/package_managers/AzL3TdnfPackageManager.py +++ b/src/core/src/package_managers/AzL3TdnfPackageManager.py @@ -71,7 +71,8 @@ def try_meet_azgps_coordinated_requirements(self): """ Check if the system meets the requirements for Azure Linux strict safe deployment and attempt to update TDNF if necessary """ self.composite_logger.log_debug("[AzL3TDNF] Checking if system meets Azure Linux security updates requirements...") # Check if the system is Azure Linux 3.0 or beyond - if not self.env_layer.is_distro_azure_linux_3_or_beyond(): + distro_name = str(self.env_layer.platform.linux_distribution()[0]) + if not self.env_layer.is_distro_azure_linux_3(distro_name): self.composite_logger.log_error("[AzL3TDNF] The system does not meet minimum Azure Linux requirement of 3.0 or above for strict safe deployment. Defaulting to regular upgrades.") self.set_max_patch_publish_date() # fall-back return False diff --git a/src/core/tests/Test_EnvLayer.py b/src/core/tests/Test_EnvLayer.py index 0dc5d07f..78e72a8e 100644 --- a/src/core/tests/Test_EnvLayer.py +++ b/src/core/tests/Test_EnvLayer.py @@ -74,17 +74,17 @@ def mock_distro_os_release_attr_return_azure_linux_2(self, attribute): def mock_distro_os_release_attr_return_none(self, attribute): return None + def mock_linux_distribution_to_return_azure_linux_4(self): + return ['Microsoft Azure Linux', '4.0', ''] + def mock_distro_os_release_attr_return_azure_linux_4(self, attribute): return '4.0.2' + def mock_linux_distribution_to_return_rhel_10(self): + return ['Red Hat', '10.0', ''] + def mock_distro_os_release_attr_return_rhel_10(self, attribute): return '10.0' - - def mock_linux_distribution_to_return_azure_linux_4(self): - return ['Microsoft Azure Linux', '4.0', ''] - - def mock_distro_id_return_rhel(self): - return 'rhel' # endregion def test_get_package_manager(self): @@ -96,29 +96,26 @@ def test_get_package_manager(self): self.backup_distro_os_release_attr = distro.os_release_attr test_input_output_table = [ - [self.mock_run_command_for_apt, self.mock_linux_distribution, self.mock_distro_os_release_attr_return_none, Constants.APT], - [self.mock_run_command_for_tdnf, self.mock_linux_distribution_to_return_azure_linux_3, self.mock_distro_os_release_attr_return_azure_linux_3, Constants.TDNF], - [self.mock_run_command_for_yum, self.mock_linux_distribution_to_return_azure_linux_3, self.mock_distro_os_release_attr_return_azure_linux_3, str()], # check for Azure Linux machine which does not have tdnf - [self.mock_run_command_for_tdnf, self.mock_linux_distribution_to_return_azure_linux_2, self.mock_distro_os_release_attr_return_azure_linux_2, Constants.TDNF], - [self.mock_run_command_for_yum, self.mock_linux_distribution, self.mock_distro_os_release_attr_return_none, Constants.YUM], - [self.mock_run_command_for_zypper, self.mock_linux_distribution, self.mock_distro_os_release_attr_return_none, Constants.ZYPPER], - [lambda cmd, no_output, chk_err: (-1, ''), self.mock_linux_distribution, self.mock_distro_os_release_attr_return_none, str()], # no matches for any package manager + [self.mock_run_command_for_apt, self.mock_linux_distribution, Constants.APT], + [self.mock_run_command_for_tdnf, self.mock_linux_distribution_to_return_azure_linux_3, Constants.TDNF], + [self.mock_run_command_for_yum, self.mock_linux_distribution_to_return_azure_linux_3, str()], # check for Azure Linux machine which does not have tdnf + [self.mock_run_command_for_tdnf, self.mock_linux_distribution_to_return_azure_linux_2, Constants.TDNF], + [self.mock_run_command_for_yum, self.mock_linux_distribution, Constants.YUM], + [self.mock_run_command_for_zypper, self.mock_linux_distribution, Constants.ZYPPER], + [lambda cmd, no_output, chk_err: (-1, ''), self.mock_linux_distribution, str()], # no matches for any package manager ] for row in test_input_output_table: self.envlayer.run_command_output = row[0] self.envlayer.platform.linux_distribution = row[1] - distro.os_release_attr = row[2] package_manager = self.envlayer.get_package_manager() - self.assertTrue(package_manager is row[3]) + self.assertTrue(package_manager is row[2]) # test for Windows platform.system = self.mock_platform_system_windows - distro.os_release_attr = self.mock_distro_os_release_attr_return_none self.assertEqual(self.envlayer.get_package_manager(), Constants.APT) # restore original methods - distro.os_release_attr = self.backup_distro_os_release_attr self.envlayer.run_command_output = self.backup_run_command_output self.envlayer.platform.linux_distribution = self.backup_linux_distribution platform.system = self.backup_platform_system @@ -128,20 +125,19 @@ def test_is_distro_azure_linux_3_or_beyond(self): self.backup_envlayer_distro_os_release_attr = distro.os_release_attr test_input_output_table = [ - [self.mock_linux_distribution_to_return_azure_linux_3, self.mock_distro_os_release_attr_return_azure_linux_3, True], - [self.mock_linux_distribution_to_return_azure_linux_2, self.mock_distro_os_release_attr_return_azure_linux_2, False], - [self.mock_linux_distribution_to_return_azure_linux_3, self.mock_distro_os_release_attr_return_none, False] + [self.mock_linux_distribution_to_return_azure_linux_3(), self.mock_distro_os_release_attr_return_azure_linux_3, True], + [self.mock_linux_distribution_to_return_azure_linux_2(), self.mock_distro_os_release_attr_return_azure_linux_2, False], + [self.mock_linux_distribution_to_return_azure_linux_3(), self.mock_distro_os_release_attr_return_none, False] ] for row in test_input_output_table: - self.envlayer.platform.linux_distribution = row[0] + distro_name = row[0][0] # Extract distro name from tuple (first element) distro.os_release_attr = row[1] - result = self.envlayer.is_distro_azure_linux_3_or_beyond() + result = self.envlayer.is_distro_azure_linux_3(distro_name) self.assertEqual(result, row[2]) # restore original methods distro.os_release_attr = self.backup_envlayer_distro_os_release_attr - self.envlayer.platform.linux_distribution = self.backup_linux_distribution def test_filesystem(self): # only validates if these invocable without exceptions @@ -177,27 +173,23 @@ def test_get_package_manager_azure_linux_4_not_supported(self): output = captured_output.getvalue() # Verify exact error message is logged - expected_msg = "Error: Azure Linux 4 is not yet supported in your region. Please review https://aka.ms/VMGuestPatchingCompatibility for more information." - self.assertEqual(expected_msg in output, True) + expected_msg = "Error: This distro is not yet supported in your region. Please review https://aka.ms/VMGuestPatchingCompatibility for more information. [Distro=Microsoft Azure Linux]" + self.assertIn(expected_msg, output) # Verify empty string is returned self.assertEqual(result, "") # restore - distro.os_release_attr = self.backup_distro_os_release_attr - self.envlayer.platform.linux_distribution = self.backup_linux_distribution - platform.system = self.backup_platform_system + self._restore_mocks() def test_get_package_manager_rhel_10_not_supported(self): """Test that RHEL 10 logs unsupported message""" self.backup_platform_system = platform.system self.backup_linux_distribution = self.envlayer.platform.linux_distribution self.backup_distro_os_release_attr = distro.os_release_attr - self.backup_distro_id = distro.id platform.system = self.mock_platform_system - self.envlayer.platform.linux_distribution = self.mock_linux_distribution + self.envlayer.platform.linux_distribution = self.mock_linux_distribution_to_return_rhel_10 distro.os_release_attr = self.mock_distro_os_release_attr_return_rhel_10 - distro.id = self.mock_distro_id_return_rhel # Capture stdout to check for log message captured_output = io.StringIO() @@ -209,13 +201,16 @@ def test_get_package_manager_rhel_10_not_supported(self): output = captured_output.getvalue() # Verify exact error message is logged - expected_msg = "Error: RHEL 10 is not yet supported in your region. Please review https://aka.ms/VMGuestPatchingCompatibility for more information." - self.assertEqual(expected_msg in output, True) + expected_msg = "Error: This distro is not yet supported in your region. Please review https://aka.ms/VMGuestPatchingCompatibility for more information. [Distro=Red Hat]" + self.assertIn(expected_msg, output) # Verify empty string is returned self.assertEqual(result, "") # restore - distro.id = self.backup_distro_id + self._restore_mocks() + + def _restore_mocks(self): + """Restore backed up mocks to their original state""" distro.os_release_attr = self.backup_distro_os_release_attr self.envlayer.platform.linux_distribution = self.backup_linux_distribution platform.system = self.backup_platform_system From 6e07a3c474733c5b5d8183e02ba93c1ae1bf0b7f Mon Sep 17 00:00:00 2001 From: Yashna Parikh Date: Tue, 19 May 2026 09:55:12 -0400 Subject: [PATCH 5/6] Address code review comments_2 --- src/core/src/bootstrap/EnvLayer.py | 7 +-- src/core/tests/Test_EnvLayer.py | 75 +++++++++--------------------- 2 files changed, 23 insertions(+), 59 deletions(-) diff --git a/src/core/src/bootstrap/EnvLayer.py b/src/core/src/bootstrap/EnvLayer.py index 1618e366..35c9254d 100644 --- a/src/core/src/bootstrap/EnvLayer.py +++ b/src/core/src/bootstrap/EnvLayer.py @@ -50,13 +50,11 @@ def is_distro_azure_linux(distro_name): @staticmethod def __try_get_major_version(): # type: () -> str or None - """ Attempts to get major version from distro.os_release_attr('version'). - Returns major version as string or None if not available. """ + """ Attempts to get major version from distro.os_release_attr('version').Returns major version as string or None if not available. """ version = distro.os_release_attr('version') major = version.split('.')[0] if version else None return major - def __is_matching_distro_and_version(self, distro_name, distro_to_match, version_to_match): # type: (str, str, int) -> bool """ Checks if distro name matches and version matches the expected version. @@ -108,8 +106,7 @@ def get_package_manager(self): print("Error: Expected package manager tdnf not found on this Azure Linux VM.") return str() - - # Choose default package manager + # choose default package manager package_manager_map = (('apt-get', Constants.APT), ('yum', Constants.YUM), ('zypper', Constants.ZYPPER)) diff --git a/src/core/tests/Test_EnvLayer.py b/src/core/tests/Test_EnvLayer.py index 78e72a8e..6dad9f90 100644 --- a/src/core/tests/Test_EnvLayer.py +++ b/src/core/tests/Test_EnvLayer.py @@ -120,18 +120,17 @@ def test_get_package_manager(self): self.envlayer.platform.linux_distribution = self.backup_linux_distribution platform.system = self.backup_platform_system - def test_is_distro_azure_linux_3_or_beyond(self): - self.backup_linux_distribution = self.envlayer.platform.linux_distribution + def test_is_distro_azure_linux_3(self): self.backup_envlayer_distro_os_release_attr = distro.os_release_attr test_input_output_table = [ - [self.mock_linux_distribution_to_return_azure_linux_3(), self.mock_distro_os_release_attr_return_azure_linux_3, True], - [self.mock_linux_distribution_to_return_azure_linux_2(), self.mock_distro_os_release_attr_return_azure_linux_2, False], - [self.mock_linux_distribution_to_return_azure_linux_3(), self.mock_distro_os_release_attr_return_none, False] + [self.mock_linux_distribution_to_return_azure_linux_3, self.mock_distro_os_release_attr_return_azure_linux_3, True], + [self.mock_linux_distribution_to_return_azure_linux_2, self.mock_distro_os_release_attr_return_azure_linux_2, False], + [self.mock_linux_distribution_to_return_azure_linux_3, self.mock_distro_os_release_attr_return_none, False] ] for row in test_input_output_table: - distro_name = row[0][0] # Extract distro name from tuple (first element) + distro_name = row[0]()[0] # Extract distro name from tuple (first element) distro.os_release_attr = row[1] result = self.envlayer.is_distro_azure_linux_3(distro_name) self.assertEqual(result, row[2]) @@ -153,68 +152,36 @@ def test_platform(self): self.envlayer.platform.cpu_arch() self.envlayer.platform.vm_name() - def test_get_package_manager_azure_linux_4_not_supported(self): - """Test that Azure Linux 4 logs unsupported message""" - self.backup_platform_system = platform.system - self.backup_linux_distribution = self.envlayer.platform.linux_distribution - self.backup_distro_os_release_attr = distro.os_release_attr - - platform.system = self.mock_platform_system - self.envlayer.platform.linux_distribution = self.mock_linux_distribution_to_return_azure_linux_4 - distro.os_release_attr = self.mock_distro_os_release_attr_return_azure_linux_4 - - # Capture stdout to check for log message - captured_output = io.StringIO() - sys.stdout = captured_output - - result = self.envlayer.get_package_manager() - - sys.stdout = sys.__stdout__ - output = captured_output.getvalue() - - # Verify exact error message is logged - expected_msg = "Error: This distro is not yet supported in your region. Please review https://aka.ms/VMGuestPatchingCompatibility for more information. [Distro=Microsoft Azure Linux]" - self.assertIn(expected_msg, output) - # Verify empty string is returned - self.assertEqual(result, "") - - # restore - self._restore_mocks() - - def test_get_package_manager_rhel_10_not_supported(self): - """Test that RHEL 10 logs unsupported message""" + def test_get_package_manager_azure_linux_4_and_rhel10_not_supported(self): + """Test that Azure Linux 4 and RHEL 10 log unsupported message""" self.backup_platform_system = platform.system self.backup_linux_distribution = self.envlayer.platform.linux_distribution self.backup_distro_os_release_attr = distro.os_release_attr platform.system = self.mock_platform_system - self.envlayer.platform.linux_distribution = self.mock_linux_distribution_to_return_rhel_10 - distro.os_release_attr = self.mock_distro_os_release_attr_return_rhel_10 - - # Capture stdout to check for log message - captured_output = io.StringIO() - sys.stdout = captured_output - - result = self.envlayer.get_package_manager() + test_input_output_table = [ + [self.mock_linux_distribution_to_return_azure_linux_4, self.mock_distro_os_release_attr_return_azure_linux_4, "Error: This distro is not yet supported in your region. Please review https://aka.ms/VMGuestPatchingCompatibility for more information. [Distro=Microsoft Azure Linux]"], + [self.mock_linux_distribution_to_return_rhel_10, self.mock_distro_os_release_attr_return_rhel_10, "Error: This distro is not yet supported in your region. Please review https://aka.ms/VMGuestPatchingCompatibility for more information. [Distro=Red Hat]"], + ] - sys.stdout = sys.__stdout__ - output = captured_output.getvalue() + for row in test_input_output_table: + self.envlayer.platform.linux_distribution = row[0] + distro.os_release_attr = row[1] - # Verify exact error message is logged - expected_msg = "Error: This distro is not yet supported in your region. Please review https://aka.ms/VMGuestPatchingCompatibility for more information. [Distro=Red Hat]" - self.assertIn(expected_msg, output) - # Verify empty string is returned - self.assertEqual(result, "") + captured_output = io.StringIO() + sys.stdout = captured_output + result = self.envlayer.get_package_manager() + sys.stdout = sys.__stdout__ + self.assertEqual(result, "") # restore - self._restore_mocks() + self.__restore_mocks() - def _restore_mocks(self): + def __restore_mocks(self): """Restore backed up mocks to their original state""" distro.os_release_attr = self.backup_distro_os_release_attr self.envlayer.platform.linux_distribution = self.backup_linux_distribution platform.system = self.backup_platform_system - if __name__ == '__main__': unittest.main() \ No newline at end of file From f8871ea1974b2efc219d4cebeaca5131ad8495d6 Mon Sep 17 00:00:00 2001 From: Yashna Parikh Date: Tue, 19 May 2026 17:21:36 -0400 Subject: [PATCH 6/6] Address code review comments_3 --- src/core/src/bootstrap/EnvLayer.py | 2 +- src/core/tests/Test_EnvLayer.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/core/src/bootstrap/EnvLayer.py b/src/core/src/bootstrap/EnvLayer.py index 35c9254d..238da5db 100644 --- a/src/core/src/bootstrap/EnvLayer.py +++ b/src/core/src/bootstrap/EnvLayer.py @@ -93,7 +93,7 @@ def get_package_manager(self): # Check for unsupported distros if self.is_distro_azure_linux_4(os_name) or self.is_distro_rhel_10(os_name): - error_msg = "This distro is not yet supported in your region. Please review https://aka.ms/VMGuestPatchingCompatibility for more information. [Distro={0}]".format(str(os_name)) + error_msg = "This distro is not yet supported in your region. Please review https://aka.ms/VMGuestPatchingCompatibility for more information. [Distro={0}][Version={1}][Code={2}]".format(str(os_name), os_version, os_code) print("Error: {0}".format(error_msg)) return str() diff --git a/src/core/tests/Test_EnvLayer.py b/src/core/tests/Test_EnvLayer.py index 6dad9f90..3559ec04 100644 --- a/src/core/tests/Test_EnvLayer.py +++ b/src/core/tests/Test_EnvLayer.py @@ -81,7 +81,7 @@ def mock_distro_os_release_attr_return_azure_linux_4(self, attribute): return '4.0.2' def mock_linux_distribution_to_return_rhel_10(self): - return ['Red Hat', '10.0', ''] + return ['Red Hat', '10.0', 'abc'] def mock_distro_os_release_attr_return_rhel_10(self, attribute): return '10.0' @@ -160,8 +160,8 @@ def test_get_package_manager_azure_linux_4_and_rhel10_not_supported(self): platform.system = self.mock_platform_system test_input_output_table = [ - [self.mock_linux_distribution_to_return_azure_linux_4, self.mock_distro_os_release_attr_return_azure_linux_4, "Error: This distro is not yet supported in your region. Please review https://aka.ms/VMGuestPatchingCompatibility for more information. [Distro=Microsoft Azure Linux]"], - [self.mock_linux_distribution_to_return_rhel_10, self.mock_distro_os_release_attr_return_rhel_10, "Error: This distro is not yet supported in your region. Please review https://aka.ms/VMGuestPatchingCompatibility for more information. [Distro=Red Hat]"], + [self.mock_linux_distribution_to_return_azure_linux_4, self.mock_distro_os_release_attr_return_azure_linux_4, "Error: This distro is not yet supported in your region. Please review https://aka.ms/VMGuestPatchingCompatibility for more information. [Distro=Microsoft Azure Linux][Version=4.0][Code=]\n"], + [self.mock_linux_distribution_to_return_rhel_10, self.mock_distro_os_release_attr_return_rhel_10, "Error: This distro is not yet supported in your region. Please review https://aka.ms/VMGuestPatchingCompatibility for more information. [Distro=Red Hat][Version=10.0][Code=abc]\n"], ] for row in test_input_output_table: @@ -172,6 +172,7 @@ def test_get_package_manager_azure_linux_4_and_rhel10_not_supported(self): sys.stdout = captured_output result = self.envlayer.get_package_manager() sys.stdout = sys.__stdout__ + self.assertEqual(row[2], captured_output.getvalue()) self.assertEqual(result, "") # restore