Skip to content

Commit f4a58ec

Browse files
benankbenank
andauthored
Fix SUSE error codes 6 and 106 (#111)
* Add mitigations for codes 106 and 6 * Add UTs * Update position and naming of retriable exit codes * Adjust exception throwing * Also adjust refresh_repo to only raise if error persists after reboot Co-authored-by: benank <[email protected]>
1 parent 7f6c6b5 commit f4a58ec

File tree

3 files changed

+97
-6
lines changed

3 files changed

+97
-6
lines changed

src/core/src/package_managers/ZypperPackageManager.py

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ def __init__(self, env_layer, execution_config, composite_logger, telemetry_writ
4040
# Repo refresh
4141
self.repo_clean = 'sudo zypper clean -a'
4242
self.repo_refresh = 'sudo zypper refresh'
43+
self.repo_refresh_services = 'sudo zypper refresh --services'
4344

4445
# Support to get updates and their dependencies
4546
self.zypper_check = 'sudo LANG=en_US.UTF8 zypper list-updates'
@@ -54,11 +55,14 @@ def __init__(self, env_layer, execution_config, composite_logger, telemetry_writ
5455

5556
# Package manager exit code(s)
5657
self.zypper_exitcode_ok = 0
58+
self.zypper_exitcode_zypp_lib_exit_err = 4
59+
self.zypper_exitcode_no_repos = 6
60+
self.zypper_exitcode_zypp_locked = 7
5761
self.zypper_exitcode_reboot_required = 102
5862
self.zypper_exitcode_zypper_updated = 103
59-
self.zypper_exitcode_zypp_locked = 7
60-
self.zypper_exitcode_zypp_lib_exit_err = 4
63+
self.zypper_exitcode_repos_skipped = 106
6164
self.zypper_success_exit_codes = [self.zypper_exitcode_ok, self.zypper_exitcode_zypper_updated, self.zypper_exitcode_reboot_required]
65+
self.zypper_retriable_exit_codes = [self.zypper_exitcode_zypp_locked, self.zypper_exitcode_zypp_lib_exit_err, self.zypper_exitcode_repos_skipped]
6266

6367
# Support to check for processes requiring restart
6468
self.zypper_ps = "sudo zypper ps -s"
@@ -67,7 +71,6 @@ def __init__(self, env_layer, execution_config, composite_logger, telemetry_writ
6771
self.set_package_manager_setting(Constants.PKG_MGR_SETTING_IDENTITY, Constants.ZYPPER)
6872
self.zypper_get_process_tree_cmd = 'ps --forest -o pid,cmd -g $(ps -o sid= -p {})'
6973
self.package_manager_max_retries = 5
70-
self.error_codes_to_retry = [self.zypper_exitcode_zypp_locked, self.zypper_exitcode_zypp_lib_exit_err]
7174
self.zypp_lock_timeout_backup = None
7275

7376
# auto OS updates
@@ -89,11 +92,24 @@ def refresh_repo(self):
8992
# Reboot if not already done
9093
if self.status_handler.get_installation_reboot_status() == Constants.RebootStatus.COMPLETED:
9194
self.composite_logger.log_warning("Unable to refresh repo (retries exhausted after reboot).")
95+
raise
9296
else:
9397
self.composite_logger.log_warning("Setting force_reboot flag to True.")
9498
self.force_reboot = True
95-
96-
raise
99+
100+
def __refresh_repo_services(self):
101+
""" Similar to refresh_repo, but refreshes services in case no repos are defined. """
102+
self.composite_logger.log("Refreshing local repo services...")
103+
try:
104+
self.invoke_package_manager(self.repo_refresh_services)
105+
except Exception as error:
106+
# Reboot if not already done
107+
if self.status_handler.get_installation_reboot_status() == Constants.RebootStatus.COMPLETED:
108+
self.composite_logger.log_warning("Unable to refresh repo services (retries exhausted after reboot).")
109+
raise
110+
else:
111+
self.composite_logger.log_warning("Setting force_reboot flag to True after refreshing repo services.")
112+
self.force_reboot = True
97113

98114
# region Get Available Updates
99115
def invoke_package_manager(self, command):
@@ -106,12 +122,18 @@ def invoke_package_manager(self, command):
106122
self.restore_original_lock_timeout()
107123

108124
if code not in self.zypper_success_exit_codes: # more known return codes should be added as appropriate
125+
# Refresh repo services if no repos are defined
126+
if code == self.zypper_exitcode_no_repos and command != self.repo_refresh_services:
127+
self.composite_logger.log_warning("Warning: no repos defined on command: {0}".format(str(command)))
128+
self.__refresh_repo_services()
129+
continue
130+
109131
self.log_errors_on_invoke(command, out, code)
110132
error_msg = 'Unexpected return code (' + str(code) + ') from package manager on command: ' + command
111133
self.status_handler.add_error_to_status(error_msg, Constants.PatchOperationErrorCodes.PACKAGE_MANAGER_FAILURE)
112134

113135
# Not a retriable error code, so raise an exception
114-
if code not in self.error_codes_to_retry:
136+
if code not in self.zypper_retriable_exit_codes:
115137
raise Exception(error_msg, "[{0}]".format(Constants.ERROR_ADDED_TO_STATUS))
116138

117139
# Retriable error code, so check number of retries and wait then retry if applicable; otherwise, raise error after max retries

src/core/tests/Test_ZypperPackageManager.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,66 @@ def mock_run_command_output(cmd, no_output=False, chk_err=False):
482482

483483
package_manager.env_layer.run_command_output = backup_mocked_method
484484

485+
def test_package_manager_no_repos(self):
486+
package_manager = self.container.get('package_manager')
487+
# Setting operation to assessment to add all errors under assessment substatus
488+
self.runtime.status_handler.set_current_operation(Constants.ASSESSMENT)
489+
cmd_to_run = 'sudo zypper refresh'
490+
491+
# Wrap count in a mutable container to modify in mocked method to keep track of retries
492+
counter = [0]
493+
backup_mocked_method = package_manager.env_layer.run_command_output
494+
495+
def mock_run_command_output(cmd, no_output=False, chk_err=False):
496+
# Only check for refresh services cmd
497+
if cmd == 'sudo zypper refresh --services':
498+
# After refreshing, allow it to succeed
499+
self.runtime.set_legacy_test_type('HappyPath')
500+
elif cmd == 'sudo zypper refresh':
501+
counter[0] += 1
502+
return backup_mocked_method(cmd, no_output, chk_err)
503+
504+
package_manager.env_layer.run_command_output = mock_run_command_output
505+
506+
# Case 1: AnotherSadPath to HappyPath (no repos defined -> repos defined)
507+
508+
# AnotherSadPath uses return code 6
509+
self.runtime.set_legacy_test_type('AnotherSadPath')
510+
511+
# Invoke should not raise an exception here
512+
try:
513+
package_manager.invoke_package_manager(cmd_to_run)
514+
except Exception as error:
515+
self.fail(repr(error))
516+
517+
# Should try twice: once fail, fix repos, then try again and succeed
518+
self.assertEqual(counter[0], 2)
519+
self.assertFalse(self.is_string_in_status_file('Unexpected return code (6) from package manager on command'))
520+
521+
# Case 2: AnotherSadPath (no repos defined -> still no repos defined)
522+
counter = [0]
523+
524+
# AnotherSadPath uses return code 6
525+
self.runtime.set_legacy_test_type('AnotherSadPath')
526+
527+
def mock_run_command_output(cmd, no_output=False, chk_err=False):
528+
# Only count the number of command invocations and do not change to HappyPath
529+
if cmd == 'sudo zypper refresh':
530+
counter[0] += 1
531+
return backup_mocked_method(cmd, no_output, chk_err)
532+
533+
package_manager.env_layer.run_command_output = mock_run_command_output
534+
535+
# Invoke should raise an exception here
536+
try:
537+
package_manager.invoke_package_manager(cmd_to_run)
538+
except Exception as error:
539+
# Should only try once since this is not a retriable error code
540+
self.assertEqual(counter[0], 1)
541+
self.assertTrue(self.is_string_in_status_file('Unexpected return code (6) from package manager on command: sudo zypper refresh'))
542+
self.assertTrue('Unexpected return code (6) from package manager on command: sudo zypper refresh' in repr(error))
543+
544+
package_manager.env_layer.run_command_output = backup_mocked_method
485545

486546
if __name__ == '__main__':
487547
unittest.main()

src/core/tests/library/LegacyEnvLayerExtensions.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,10 @@ def run_command_output(self, cmd, no_output=False, chk_err=True):
231231
"7984 | \_ /usr/bin/python3 /usr/bin/azuremetadata --api latest --subscriptionId --billingTag --attestedData --signature\n" + \
232232
"7986 \_ python3 package_test.py\n" + \
233233
"8298 \_ sudo LANG=en_US.UTF8 zypper --non-interactive update --dry-run grub2-i386-pc\n"
234+
elif cmd.find('sudo zypper refresh --services') > -1:
235+
code = 0
236+
output = "Refreshing service \'Web_and_Scripting_Module_x86_64\'." + \
237+
"All services have been refreshed."
234238
elif cmd.find('sudo zypper refresh') > -1:
235239
code = 0
236240
output = "Retrieving repository 'SLE-Module-Basesystem15-SP3-Pool' metadata ................................................................[done]\n" + \
@@ -570,6 +574,11 @@ def run_command_output(self, cmd, no_output=False, chk_err=True):
570574
if cmd.find('sudo zypper refresh') > -1:
571575
code = 999999
572576
output = 'Unexpected return code (100) from package manager on command: LANG=en_US.UTF8 sudo apt-get -s dist-upgrade'
577+
elif self.legacy_test_type == 'AnotherSadPath':
578+
if self.legacy_package_manager_name is Constants.ZYPPER:
579+
if cmd.find('sudo zypper refresh') > -1:
580+
code = 6
581+
output = 'Warning: There are no enabled repositories defined. | Use \'zypper addrepo\' or \'zypper modifyrepo\' commands to add or enable repositories.'
573582
elif self.legacy_test_type == 'ExceptionPath':
574583
code = -1
575584
output = ''

0 commit comments

Comments
 (0)