Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
9a9b48a
add logic to set installation warning when all pkgs are installed
feng-j678 Feb 24, 2025
b6e7003
add print in statushandler to help with debug
feng-j678 Feb 26, 2025
091af98
add print in patchinstaller
feng-j678 Feb 26, 2025
3f77ecd
add unit test to test install successful on retry
feng-j678 Feb 26, 2025
a9b23f8
remove print, revert linebreak
feng-j678 Feb 26, 2025
68421aa
correct spelling
feng-j678 Feb 26, 2025
1f757ef
remove comments
feng-j678 Feb 27, 2025
f4dd3d5
Merge branch 'master' of https://github.com/Azure/LinuxPatchExtension…
feng-j678 Mar 13, 2025
67fb578
Merge branch 'feature/pkg_failed_retry_set_status_warning' of https:/…
feng-j678 Mar 13, 2025
4be150a
revert format changes
feng-j678 Mar 13, 2025
9dff776
Merge branch 'master' of https://github.com/Azure/LinuxPatchExtension…
feng-j678 Mar 19, 2025
1d830c7
Merge branch 'feature/pkg_failed_retry_set_status_warning' of https:/…
feng-j678 Mar 19, 2025
34bd06a
create fake comment to reset file format
feng-j678 Mar 19, 2025
37dd5d5
Merge branch 'tmp/resolve_installer_format' into feature/pkg_failed_r…
feng-j678 Mar 19, 2025
0fdab66
create conflict
feng-j678 Mar 19, 2025
000dba1
create conflict
feng-j678 Mar 19, 2025
abeb3d2
Merge branch 'tmp/A' into tmp/B
feng-j678 Mar 19, 2025
f2bc6d8
remove print
feng-j678 Mar 19, 2025
ead5296
add fake comment
feng-j678 Mar 19, 2025
7e57abc
reset format
feng-j678 Mar 19, 2025
cf01d5b
Merge branch 'tmp/A' into tmp/B
feng-j678 Mar 19, 2025
2171a61
remove add trailing space
feng-j678 Mar 19, 2025
1182105
B
feng-j678 Mar 26, 2025
e64f078
refactor functions and comments
feng-j678 Mar 26, 2025
c3f80b3
add extra line btw imports and class
feng-j678 Mar 26, 2025
008e919
remove extra spaces
feng-j678 Mar 27, 2025
4a4dcc3
extract log metric into a function
feng-j678 Mar 28, 2025
cc28d0e
Bi
feng-j678 Apr 3, 2025
8922159
apply code refactor
feng-j678 Apr 7, 2025
e12f2d4
alter logic to check all packages are installed instead critical/securit
feng-j678 Apr 7, 2025
30f867e
refactor extra lines
feng-j678 Apr 7, 2025
7930d79
Merge branch 'master' of https://github.com/Azure/LinuxPatchExtension…
feng-j678 Apr 7, 2025
7a5b139
Merge branch 'master' of https://github.com/Azure/LinuxPatchExtension…
feng-j678 Apr 15, 2025
c57be77
refactor __check_installation_status_can_set_to_warning
feng-j678 Apr 15, 2025
12a1c2b
add extra lines
feng-j678 Apr 15, 2025
2de4b07
refactor __send_metadata_to_health_store
feng-j678 Apr 17, 2025
7d909f0
Merge branch 'master' of https://github.com/Azure/LinuxPatchExtension…
feng-j678 Apr 18, 2025
f9ae07e
PR feedback explanation
rane-rajasi Apr 18, 2025
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
5 changes: 4 additions & 1 deletion src/core/src/CoreMain.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,14 +93,17 @@ def __init__(self, argv):
# setting current operation here, to include patch_installer init within installation actions, ensuring any exceptions during patch_installer init are added in installation summary errors object
status_handler.set_current_operation(Constants.INSTALLATION)
patch_installer = container.get('patch_installer')
patch_installation_successful = patch_installer.start_installation()
patch_installation_successful, maintenance_window_exceeded = patch_installer.start_installation()
patch_assessment_successful = False
patch_assessment_successful = patch_assessor.start_assessment()

# PatchInstallationSummary to be marked as completed successfully only after the implicit (i.e. 2nd) assessment is completed, as per CRP's restrictions
if patch_assessment_successful and patch_installation_successful:
patch_installer.mark_installation_completed()
overall_patch_installation_operation_successful = True
elif patch_installer.should_patch_installation_status_be_set_to_warning(patch_installation_successful, maintenance_window_exceeded):
patch_installer.mark_installation_completed_with_warning()
overall_patch_installation_operation_successful = True
self.update_patch_substatus_if_pending(patch_operation_requested, overall_patch_installation_operation_successful, patch_assessment_successful, configure_patching_successful, status_handler, composite_logger)
except Exception as error:
# Privileged operation handling for non-production use
Expand Down
1 change: 1 addition & 0 deletions src/core/src/bootstrap/Constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,7 @@ class PatchOperationErrorCodes(EnumBackport):
NEWER_OPERATION_SUPERSEDED = "NEWER_OPERATION_SUPERSEDED"
UA_ESM_REQUIRED = "UA_ESM_REQUIRED"
TRUNCATION = "PACKAGE_LIST_TRUNCATED"
PACKAGES_RETRY_SUCCEEDED = "PACKAGES_RETRY_SUCCEEDED"

ERROR_ADDED_TO_STATUS = "Error_added_to_status"

Expand Down
63 changes: 46 additions & 17 deletions src/core/src/core_logic/PatchInstaller.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from core.src.bootstrap.Constants import Constants
from core.src.core_logic.Stopwatch import Stopwatch


class PatchInstaller(object):
"""" Wrapper class for a single patch installation operation """
def __init__(self, env_layer, execution_config, composite_logger, telemetry_writer, status_handler, lifecycle_manager, package_manager, package_filter, maintenance_window, reboot_manager):
Expand Down Expand Up @@ -123,7 +124,7 @@
overall_patch_installation_successful = bool(update_run_successful and not maintenance_window_exceeded)
# NOTE: Not updating installation substatus at this point because we need to wait for the implicit/second assessment to complete first, as per CRP's instructions

return overall_patch_installation_successful
return overall_patch_installation_successful, maintenance_window_exceeded

def write_installer_perf_logs(self, patch_operation_successful, installed_patch_count, retry_count, maintenance_window, maintenance_window_exceeded, task_status, error_msg):
perc_maintenance_window_used = -1
Expand Down Expand Up @@ -324,7 +325,7 @@
# package_and_dependencies initially contains only one package. The dependencies are added in the list by method include_dependencies
package_and_dependencies = [package]
package_and_dependency_versions = [version]

self.include_dependencies(package_manager, [package], [version], all_packages, all_package_versions, packages, package_versions, package_and_dependencies, package_and_dependency_versions)

# parent package install (+ dependencies) and parent package result management
Expand Down Expand Up @@ -379,6 +380,7 @@

self.composite_logger.log_debug("\nPerforming final system state reconciliation...")
installed_update_count += self.perform_status_reconciliation_conditionally(package_manager, True)

self.log_final_metrics(maintenance_window, patch_installation_successful, maintenance_window_exceeded, installed_update_count)

install_update_count_in_sequential_patching = installed_update_count - install_update_count_in_batch_patching
Expand All @@ -398,16 +400,14 @@
def log_final_metrics(self, maintenance_window, patch_installation_successful, maintenance_window_exceeded, installed_update_count):
"""
logs the final metrics.

Parameters:
maintenance_window (MaintenanceWindow): Maintenance window for the job.
patch_installation_successful (bool): Whether patch installation succeeded.
maintenance_window_exceeded (bool): Whether maintenance window exceeded.
installed_update_count (int): Number of updates installed.
"""
progress_status = self.progress_template.format(str(datetime.timedelta(minutes=maintenance_window.get_remaining_time_in_minutes())), str(self.attempted_parent_package_install_count), str(self.successful_parent_package_install_count), str(self.failed_parent_package_install_count), str(installed_update_count - self.successful_parent_package_install_count),
"Completed processing packages!")
self.composite_logger.log(progress_status)

self.__log_progress_status(maintenance_window, installed_update_count)

if not patch_installation_successful or maintenance_window_exceeded:
message = "\n\nOperation status was marked as failed because: "
Expand All @@ -416,10 +416,16 @@
self.status_handler.add_error_to_status(message, Constants.PatchOperationErrorCodes.OPERATION_FAILED)
self.composite_logger.log_error(message)

def log_final_warning_metric(self, maintenance_window, installed_update_count):
""" Log the final metrics for warning installation status. """
self.__log_progress_status(maintenance_window, installed_update_count)
message = "All requested package(s) are installed. Any patch errors marked are from previous attempts."
self.status_handler.add_error_to_status(message, Constants.PatchOperationErrorCodes.PACKAGES_RETRY_SUCCEEDED)
self.composite_logger.log_error(message)

Check warning on line 424 in src/core/src/core_logic/PatchInstaller.py

View check run for this annotation

Codecov / codecov/patch

src/core/src/core_logic/PatchInstaller.py#L421-L424

Added lines #L421 - L424 were not covered by tests

def include_dependencies(self, package_manager, packages_in_batch, package_versions_in_batch, all_packages, all_package_versions, packages, package_versions, package_and_dependencies, package_and_dependency_versions):
"""
Add dependent packages in the list of packages to install i.e. package_and_dependencies.

Parameters:
package_manager (PackageManager): Package manager used.
packages_in_batch (List of strings): List of packages to be installed in the current batch.
Expand Down Expand Up @@ -509,9 +515,7 @@
def install_packages_in_batches(self, all_packages, all_package_versions, packages, package_versions, maintenance_window, package_manager, max_batch_size_for_packages, simulate=False):
"""
Install packages in batches.

Parameters:

all_packages (List of strings): List of all available packages to install.
all_package_versions (List of strings): Versions of the packages in the list all_packages.
packages (List of strings): List of all packages selected by user to install.
Expand All @@ -520,7 +524,6 @@
package_manager (PackageManager): Package manager used.
max_batch_size_for_packages (Integer): Maximum batch size.
simulate (bool): Whether this function is called from a test run.

Returns:
installed_update_count (int): Number of packages installed through installing packages in batches.
patch_installation_successful (bool): Whether package installation succeeded for all attempted packages.
Expand All @@ -529,7 +532,6 @@
not_attempted_and_failed_packages (List of strings): List of packages which are (a) Not attempted due to not enough time in maintenance window to install in batch.
(b) Failed to install in batch patching.
not_attempted_and_failed_package_versions (List of strings): Versions of packages in the list not_attempted_and_failed_packages.

"""
number_of_batches = int(math.ceil(len(packages) / float(max_batch_size_for_packages)))
self.composite_logger.log("\nDividing package install in batches. \nNumber of packages to be installed: " + str(len(packages)) + "\nBatch Size: " + str(max_batch_size_for_packages) + "\nNumber of batches: " + str(number_of_batches))
Expand Down Expand Up @@ -683,12 +685,20 @@
self.status_handler.set_installation_substatus_json(status=Constants.STATUS_WARNING)

# Update patch metadata in status for auto patching request, to be reported to healthStore
self.composite_logger.log_debug("[PI] Reviewing final healthstore record write. [HealthStoreId={0}][MaintenanceRunId={1}]".format(str(self.execution_config.health_store_id), str(self.execution_config.maintenance_run_id)))
if self.execution_config.health_store_id is not None:
self.status_handler.set_patch_metadata_for_healthstore_substatus_json(
patch_version=self.execution_config.health_store_id,
report_to_healthstore=True,
wait_after_update=False)
self.__send_metadata_to_health_store()

def mark_installation_completed_with_warning(self):
""" Marks Installation operation as warning by updating the status of PatchInstallationSummary as warning and patch metadata to be sent to healthstore.
This is set outside of start_installation function due to a restriction in CRP, where installation substatus should be marked as warning only after the implicit (2nd) assessment operation
and all customer requested packages are installed. """

message = "All requested package(s) are installed. Any patch errors marked are from previous attempts."
self.composite_logger.log_error(message)
self.status_handler.add_error_to_status(message, Constants.PatchOperationErrorCodes.PACKAGES_RETRY_SUCCEEDED)
self.status_handler.set_installation_substatus_json(status=Constants.STATUS_WARNING)

# Update patch metadata in status for auto patching request, to be reported to healthStore
self.__send_metadata_to_health_store()

# region Installation Progress support
def perform_status_reconciliation_conditionally(self, package_manager, condition=True):
Expand Down Expand Up @@ -801,3 +811,22 @@

return max_batch_size_for_packages

def __log_progress_status(self, maintenance_window, installed_update_count):
progress_status = self.progress_template.format(str(datetime.timedelta(minutes=maintenance_window.get_remaining_time_in_minutes())),
str(self.attempted_parent_package_install_count),
str(self.successful_parent_package_install_count),
str(self.failed_parent_package_install_count),
str(installed_update_count - self.successful_parent_package_install_count),
"Completed processing packages!")
self.composite_logger.log(progress_status)

def __send_metadata_to_health_store(self):
self.composite_logger.log_debug("[PI] Reviewing final healthstore record write. [HealthStoreId={0}][MaintenanceRunId={1}]".format(str(self.execution_config.health_store_id), str(self.execution_config.maintenance_run_id)))
if self.execution_config.health_store_id is not None:
self.status_handler.set_patch_metadata_for_healthstore_substatus_json(
patch_version=self.execution_config.health_store_id,
report_to_healthstore=True,
wait_after_update=False)

def should_patch_installation_status_be_set_to_warning(self, patch_installation_successful, maintenance_window_exceeded):
return not patch_installation_successful and not maintenance_window_exceeded and self.status_handler.are_all_requested_packgaes_installed()
11 changes: 11 additions & 0 deletions src/core/src/service_interfaces/StatusHandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,17 @@ def get_os_name_and_version(self):
except Exception as error:
self.composite_logger.log_error("Unable to determine platform information: {0}".format(repr(error)))
return "unknownDist_unknownVer"

def are_all_requested_packgaes_installed(self):
# type (none) -> bool
Comment on lines +266 to +267
Copy link

Copilot AI Apr 18, 2025

Choose a reason for hiding this comment

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

Typo in method name 'are_all_requested_packgaes_installed'. Consider renaming it to 'are_all_requested_packages_installed' for clarity and consistency.

Suggested change
def are_all_requested_packgaes_installed(self):
# type (none) -> bool
def are_all_requested_packages_installed(self):

Copilot uses AI. Check for mistakes.
""" Check if all requested package(s) are installed. """

for package in self.__installation_packages:
if package['patchInstallationState'] != Constants.INSTALLED:
return False

# All requested package(s) are installed
return True
# endregion

# region - Installation Reboot Status
Expand Down
97 changes: 97 additions & 0 deletions src/core/tests/Test_CoreMain.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,17 @@
def mock_os_path_exists(self, patch_to_validate):
return False

def mock_batch_patching_with_packages(self, all_packages, all_package_versions, packages, package_versions, maintenance_window, package_manager):
"""Mock batch_patching to simulate package installation failure, return some packages """
return packages, package_versions, 0, False

def mock_batch_patching_with_no_packages(self, all_packages, all_package_versions, packages, package_versions, maintenance_window, package_manager):
"""Mock batch_patching to simulate package installation failure, return no packages"""
return [], [], 0, False

Check warning on line 65 in src/core/tests/Test_CoreMain.py

View check run for this annotation

Codecov / codecov/patch

src/core/tests/Test_CoreMain.py#L65

Added line #L65 was not covered by tests

def mock_check_all_requested_packages_install_state(self):
return True

Check warning on line 68 in src/core/tests/Test_CoreMain.py

View check run for this annotation

Codecov / codecov/patch

src/core/tests/Test_CoreMain.py#L68

Added line #L68 was not covered by tests

def test_operation_fail_for_non_autopatching_request(self):
# Test for non auto patching request
argument_composer = ArgumentComposer()
Expand Down Expand Up @@ -1275,6 +1286,69 @@
os.remove = self.backup_os_remove
runtime.stop()

def test_warning_status_when_packages_initially_fail_but_succeed_on_retry(self):
"""
Tests installation status set warning when:
1. Packages initially fail installation
2. Package manager indicates retry is needed
3. On retry, all supposed packages are installed successfully
4. Batch_patching returns some packages
"""
argument_composer = ArgumentComposer()
argument_composer.operation = Constants.INSTALLATION
runtime = RuntimeCompositor(argument_composer.get_composed_arguments(), True, Constants.ZYPPER)

runtime.set_legacy_test_type('PackageRetrySuccessPath')

# Store original methods
original_batch_patching = runtime.patch_installer.batch_patching

# Mock batch_patching with packages to return false
runtime.patch_installer.batch_patching = self.mock_batch_patching_with_packages

# Run CoreMain to execute the installation
try:
CoreMain(argument_composer.get_composed_arguments())
self.__assertion_pkg_succeed_on_retry(runtime)

finally:
# reset mock
runtime.patch_installer.batch_patching = original_batch_patching
runtime.stop()

def test_warning_status_when_packages_initially_fail_but_succeed_on_retry_no_batch_packages(self):
"""
Tests installation status set warning when:
1. Packages initially fail installation
2. Package manager indicates retry is needed
3. On retry, all supposed packages are installed successfully
4. Batch_patching returns no packages
"""
argument_composer = ArgumentComposer()
argument_composer.operation = Constants.INSTALLATION
runtime = RuntimeCompositor(argument_composer.get_composed_arguments(), True, Constants.ZYPPER)

runtime.set_legacy_test_type('PackageRetrySuccessPath')

# Store original methods
original_batch_patching = runtime.patch_installer.batch_patching
original_check_all_requested_packages_install_state= runtime.status_handler.check_all_requested_packages_install_state

# Mock batch_patching with packages to return [], [], false
runtime.patch_installer.batch_patching = self.mock_batch_patching_with_no_packages
runtime.status_handler.check_all_requested_packages_install_state = self.mock_check_all_requested_packages_install_state

Check warning on line 1339 in src/core/tests/Test_CoreMain.py

View check run for this annotation

Codecov / codecov/patch

src/core/tests/Test_CoreMain.py#L1338-L1339

Added lines #L1338 - L1339 were not covered by tests

# Run CoreMain to execute the installation
try:
CoreMain(argument_composer.get_composed_arguments())
self.__assertion_pkg_succeed_on_retry(runtime)

Check warning on line 1344 in src/core/tests/Test_CoreMain.py

View check run for this annotation

Codecov / codecov/patch

src/core/tests/Test_CoreMain.py#L1342-L1344

Added lines #L1342 - L1344 were not covered by tests

finally:
# reset mock
runtime.patch_installer.batch_patching = original_batch_patching
runtime.status_handler.get_installation_packages_list = original_check_all_requested_packages_install_state
Copy link

Copilot AI Apr 18, 2025

Choose a reason for hiding this comment

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

The reset for the mock method appears to assign to 'get_installation_packages_list' instead of 'check_all_requested_packages_install_state'. Verify and correct the attribute name to properly restore the original method.

Suggested change
runtime.status_handler.get_installation_packages_list = original_check_all_requested_packages_install_state
runtime.status_handler.check_all_requested_packages_install_state = original_check_all_requested_packages_install_state

Copilot uses AI. Check for mistakes.
runtime.stop()

Check warning on line 1350 in src/core/tests/Test_CoreMain.py

View check run for this annotation

Codecov / codecov/patch

src/core/tests/Test_CoreMain.py#L1348-L1350

Added lines #L1348 - L1350 were not covered by tests

def __check_telemetry_events(self, runtime):
all_events = os.listdir(runtime.telemetry_writer.events_folder_path)
self.assertTrue(len(all_events) > 0)
Expand All @@ -1285,6 +1359,29 @@
self.assertTrue('Core' in events[0]['TaskName'])
f.close()

def __assertion_pkg_succeed_on_retry(self, runtime):
# Check telemetry events
self.__check_telemetry_events(runtime)

# If true, set installation status to warning
self.assertTrue(runtime.patch_installer.set_patch_installation_status_to_warning_from_failed())

# Check status file
with runtime.env_layer.file_system.open(runtime.execution_config.status_file_path, 'r') as file_handle:
substatus_file_data = json.load(file_handle)[0]["status"]["substatus"]

Check warning on line 1371 in src/core/tests/Test_CoreMain.py

View check run for this annotation

Codecov / codecov/patch

src/core/tests/Test_CoreMain.py#L1370-L1371

Added lines #L1370 - L1371 were not covered by tests

self.assertEqual(len(substatus_file_data), 3)
self.assertTrue(substatus_file_data[0]["name"] == Constants.PATCH_ASSESSMENT_SUMMARY)
self.assertTrue(substatus_file_data[0]["status"].lower() == Constants.STATUS_SUCCESS.lower())

Check warning on line 1375 in src/core/tests/Test_CoreMain.py

View check run for this annotation

Codecov / codecov/patch

src/core/tests/Test_CoreMain.py#L1373-L1375

Added lines #L1373 - L1375 were not covered by tests

# Check installation status is WARNING
self.assertTrue(substatus_file_data[1]["name"] == Constants.PATCH_INSTALLATION_SUMMARY)
self.assertTrue(substatus_file_data[1]["status"].lower() == Constants.STATUS_WARNING.lower())

Check warning on line 1379 in src/core/tests/Test_CoreMain.py

View check run for this annotation

Codecov / codecov/patch

src/core/tests/Test_CoreMain.py#L1378-L1379

Added lines #L1378 - L1379 were not covered by tests

# Verify at least one error detail about package retry
error_details = json.loads(substatus_file_data[1]["formattedMessage"]["message"])["errors"]["details"]
self.assertTrue(any("package(s) are installed" in detail["message"] for detail in error_details))

Check warning on line 1383 in src/core/tests/Test_CoreMain.py

View check run for this annotation

Codecov / codecov/patch

src/core/tests/Test_CoreMain.py#L1382-L1383

Added lines #L1382 - L1383 were not covered by tests


if __name__ == '__main__':
unittest.main()
Loading
Loading