diff --git a/src/core/src/bootstrap/EnvLayer.py b/src/core/src/bootstrap/EnvLayer.py index 3da5554f..d2ebb25b 100644 --- a/src/core/src/bootstrap/EnvLayer.py +++ b/src/core/src/bootstrap/EnvLayer.py @@ -81,6 +81,14 @@ def is_distro_rhel_10(self, distro_name): """ 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_dnf_version(self): + code, out = self.run_command_output('dnf --version', False, False) + # Output : dnf5 version 5.2.18.0 + if code != 0 or not out: + return code, out, None + version = str(out).split()[-1].split('.')[0] + return code, out, version + def get_package_manager(self): # type: () -> str """ Detects package manager type """ @@ -99,11 +107,14 @@ def get_package_manager(self): # Check for Azure Linux 4 or Above( uses dnf5) if self.is_distro_azure_linux_4(str(os_name)): - code, out = self.run_command_output('which dnf', False, False) - if code == 0: + code, out, version = self.__get_dnf_version() + if code == 0 and version == '5': return Constants.DNF5 + elif code == 0 and version != '5': + print("Error: Expected dnf version not found on this Azure Linux4 VM. [Expected={0}][Found={1}]".format("5", str(version))) + return str() else: - print("Error: Expected package manager dnf5 not found on this Azure Linux4 VM.") + print("Error: Expected package manager dnf5 not found on this Azure Linux4 VM") return str() # Check for Azure Linux (3 and below use TDNF) diff --git a/src/core/src/package_managers/Dnf5PackageManager.py b/src/core/src/package_managers/Dnf5PackageManager.py index c077714e..2ced1186 100644 --- a/src/core/src/package_managers/Dnf5PackageManager.py +++ b/src/core/src/package_managers/Dnf5PackageManager.py @@ -263,9 +263,9 @@ def get_dependent_list(self, packages): code, output = self.env_layer.run_command_output(cmd, False, False) self.composite_logger.log_verbose("[DNF5] Dependency simulation. [Command={0}][Code={1}]".format(cmd, str(code))) if code not in self.dnf5_simulation_valid_exit_codes: - self.composite_logger.log_error("[DNF5] Unexpected failure. [Command={0}][Code={1}][Output={2}]".format(cmd, str(code), output)) + self.composite_logger.log_error("[DNF5] Unexpected failure during dependency simulation. [Command={0}][Code={1}][Output={2}]".format(cmd, str(code), output)) error_msg = "DNF5 dependency simulation failed. Investigate and resolve unexpected return code({0}) from package manager on command: {1} ".format(str(code), cmd) - self.status_handler.add_error_to_status(error_msg, Constants.PatchOperationErrorCodes.DEFAULT_ERROR) + self.status_handler.add_error_to_status(error_msg, Constants.PatchOperationErrorCodes.PACKAGE_MANAGER_FAILURE) raise Exception(error_msg, "[{0}]".format(Constants.ERROR_ADDED_TO_STATUS)) dependencies = self.extract_dependencies(output, packages) @@ -727,3 +727,5 @@ def is_reboot_required_before_cert_update(self): """ Checks if a reboot is required before updating certificates """ return False # endregion + + diff --git a/src/core/tests/Test_EnvLayer.py b/src/core/tests/Test_EnvLayer.py index c9041752..2b51f9dd 100644 --- a/src/core/tests/Test_EnvLayer.py +++ b/src/core/tests/Test_EnvLayer.py @@ -45,6 +45,9 @@ def mock_platform_system_windows(self): def mock_linux_distribution(self): return ['test', 'test', 'test'] + def mock_linux_distribution_to_return_azure_linux_4(self): + return ['Microsoft Azure Linux', '4.0', ''] + def mock_linux_distribution_to_return_azure_linux_3(self): return ['Microsoft Azure Linux', '3.0', ''] @@ -71,6 +74,38 @@ def mock_run_command_for_tdnf(self, cmd, no_output=False, chk_err=False): return 0, '' return -1, '' + def mock_run_command_for_dnf5(self, cmd, no_output=False, chk_err=False): + if "dnf --version" in cmd: + return 0, 'dnf5 version 5.2.18.0' + return -1, '' + + def mock_run_command_for_dnf_not_found(self, cmd, no_output=False, chk_err=False): + return -1, '' + + def mock_run_command_for_dnf_wrong_version(self, cmd, no_output=False, chk_err=False): + return self.mock_run_command_for_dnf_version_command(cmd, no_output, chk_err, "wrong_version") + + def mock_run_command_for_dnf_version_command_failure(self, cmd, no_output=False, chk_err=False): + return self.mock_run_command_for_dnf_version_command(cmd, no_output, chk_err, "version_command_failure") + + def mock_run_command_for_dnf_version_command(self, cmd, no_output=False, chk_err=False, usecase="success"): + code = -1 + out = "" + if "dnf --version" in cmd: + if usecase == "wrong_version": + code = 0 + out = "dnf version 4.14.0" + elif usecase == "version_command_failure": + code = -1 + out = "dnf version command failure" + else: + code = 0 + out = "dnf version 5.2.6" + return code, out + + def mock_distro_os_release_attr_return_azure_linux_4(self, attribute): + return '4.0.0' + def mock_distro_os_release_attr_return_azure_linux_3(self, attribute): return '3.0.0' @@ -126,20 +161,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, 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_dnf5, self.mock_linux_distribution_to_return_azure_linux_4, self.mock_distro_os_release_attr_return_azure_linux_4, Constants.DNF5], + [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_none, 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_dnf_not_found, self.mock_linux_distribution_to_return_azure_linux_4, self.mock_distro_os_release_attr_return_azure_linux_4, str()], + [self.mock_run_command_for_dnf_wrong_version, self.mock_linux_distribution_to_return_azure_linux_4, self.mock_distro_os_release_attr_return_azure_linux_4, str()], + [self.mock_run_command_for_dnf_version_command_failure, self.mock_linux_distribution_to_return_azure_linux_4, self.mock_distro_os_release_attr_return_azure_linux_4, str()], + [self.mock_run_command_for_dnf_version_command, self.mock_linux_distribution_to_return_azure_linux_4, self.mock_distro_os_release_attr_return_azure_linux_4, Constants.DNF5], ] 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.assertEqual(package_manager, row[3]) # test for Windows platform.system = self.mock_platform_system_windows @@ -148,6 +189,7 @@ def test_get_package_manager(self): # restore original methods self.envlayer.run_command_output = self.backup_run_command_output self.envlayer.platform.linux_distribution = self.backup_linux_distribution + distro.os_release_attr = self.backup_distro_os_release_attr platform.system = self.backup_platform_system def test_is_distro_azure_linux_3(self): @@ -168,6 +210,23 @@ def test_is_distro_azure_linux_3(self): # restore original methods distro.os_release_attr = self.backup_envlayer_distro_os_release_attr + def test_is_distro_azure_linux_4(self): + self.backup_envlayer_distro_os_release_attr = distro.os_release_attr + + test_input_output_table = [ + [self.mock_linux_distribution_to_return_azure_linux_4, self.mock_distro_os_release_attr_return_azure_linux_4, True], + [self.mock_linux_distribution_to_return_azure_linux_4, 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.os_release_attr = row[1] + result = self.envlayer.is_distro_azure_linux_4(distro_name) + self.assertEqual(result, row[2]) + + # restore original methods + distro.os_release_attr = self.backup_envlayer_distro_os_release_attr + def test_detect_confidential_vm_by_fde(self): backup_run_command_output = self.envlayer.run_command_output backup_read_with_retry = self.envlayer.file_system.read_with_retry @@ -255,7 +314,7 @@ def test_platform(self): self.envlayer.platform.cpu_arch() self.envlayer.platform.vm_name() - def test_get_package_manager_azure_linux_4_and_rhel10_not_supported(self): + def test_get_package_manager_rhel10_not_supported(self): """Test for RHEL 10 log unsupported message""" self.backup_platform_system = platform.system self.backup_linux_distribution = self.envlayer.platform.linux_distribution @@ -280,6 +339,23 @@ def test_get_package_manager_azure_linux_4_and_rhel10_not_supported(self): # restore self.__restore_mocks() + def test_mock_command_fallback_paths(self): + """Test that mock commands return -1 for unexpected commands""" + code, out = self.mock_run_command_for_apt('which apt') + self.assertEqual(code, -1) + + code, out = self.mock_run_command_for_dnf5('which not-dnf') + self.assertEqual(code, -1) + + code, out = self.mock_run_command_for_dnf_version_command('dnf --v', "wrong_version") + self.assertEqual(code, -1) + + code, out = self.mock_run_command_for_dnf_version_command('dnf --v', "version_command_failure") + self.assertEqual(code, -1) + + code, out = self.mock_run_command_for_tdnf('which not-tdnf') + self.assertEqual(code, -1) + def __restore_mocks(self): """Restore backed up mocks to their original state""" distro.os_release_attr = self.backup_distro_os_release_attr