diff --git a/src/core/src/bootstrap/Constants.py b/src/core/src/bootstrap/Constants.py index ba4f10db..5db38abe 100644 --- a/src/core/src/bootstrap/Constants.py +++ b/src/core/src/bootstrap/Constants.py @@ -206,6 +206,7 @@ class PackageClassification(EnumBackport): UNCLASSIFIED = 'Unclassified' CRITICAL = 'Critical' SECURITY = 'Security' + SECURITY_ESM = 'Security-ESM' OTHER = 'Other' PKG_MGR_SETTING_FILTER_CRITSEC_ONLY = 'FilterCritSecOnly' @@ -264,6 +265,7 @@ class PatchOperationErrorCodes(EnumBackport): OPERATION_FAILED = "OPERATION_FAILED" PACKAGE_MANAGER_FAILURE = "PACKAGE_MANAGER_FAILURE" NEWER_OPERATION_SUPERSEDED = "NEWER_OPERATION_SUPERSEDED" + UA_ESM_REQUIRED = "UA_ESM_REQUIRED" ERROR_ADDED_TO_STATUS = "Error_added_to_status" @@ -312,8 +314,9 @@ class EnvLayer(EnumBackport): PackageClassificationOrderInStatusReporting = { PackageClassification.CRITICAL: 1, PackageClassification.SECURITY: 2, - PackageClassification.OTHER: 3, - PackageClassification.UNCLASSIFIED: 4 + PackageClassification.SECURITY_ESM: 3, + PackageClassification.OTHER: 4, + PackageClassification.UNCLASSIFIED: 5 } PatchStateOrderInStatusReporting = { @@ -330,4 +333,5 @@ class UbuntuProClientSettings(EnumBackport): FEATURE_ENABLED = True MINIMUM_PYTHON_VERSION_REQUIRED = (3, 5) # using tuple as we can compare this with sys.version_info. The comparison will happen in the same order. Major version checked first. Followed by Minor version. MAX_OS_MAJOR_VERSION_SUPPORTED = 18 + MINIMUM_CLIENT_VERSION = "27.14.4" diff --git a/src/core/src/core_logic/PackageFilter.py b/src/core/src/core_logic/PackageFilter.py index f704c0f9..efed752a 100644 --- a/src/core/src/core_logic/PackageFilter.py +++ b/src/core/src/core_logic/PackageFilter.py @@ -161,7 +161,7 @@ def is_msft_other_classification_only(self): def is_msft_all_classification_included(self): """Returns true if all classifications were individually selected *OR* (nothing was selected AND no inclusion list is present) -- business logic""" all_classifications = [key for key in Constants.PackageClassification.__dict__.keys() if not key.startswith('__')] - all_classifications_explicitly_selected = bool(len(self.installation_included_classifications) == (len(all_classifications) - 1)) + all_classifications_explicitly_selected = bool(len(self.installation_included_classifications) == (len(all_classifications) - 2)) # all_classifications has "UNCLASSIFIED" and "SECURITY-ESM" that should be ignored. Hence -2 no_classifications_selected = bool(len(self.installation_included_classifications) == 0) only_unclassified_selected = bool('Unclassified' in self.installation_included_classifications and len(self.installation_included_classifications) == 1) return all_classifications_explicitly_selected or ((no_classifications_selected or only_unclassified_selected) and not self.is_inclusion_list_present()) diff --git a/src/core/src/core_logic/PatchAssessor.py b/src/core/src/core_logic/PatchAssessor.py index ff0b236b..9e079fcf 100644 --- a/src/core/src/core_logic/PatchAssessor.py +++ b/src/core/src/core_logic/PatchAssessor.py @@ -78,7 +78,10 @@ def start_assessment(self): # Tag security updates self.telemetry_writer.write_event("Security assessment: " + str(sec_packages), Constants.TelemetryEventLevel.Verbose) - self.status_handler.set_package_assessment_status(sec_packages, sec_package_versions, "Security") + self.status_handler.set_package_assessment_status(sec_packages, sec_package_versions, Constants.PackageClassification.SECURITY) + + # Set the security-esm packages in status. + self.package_manager.set_security_esm_package_status(Constants.ASSESSMENT) # ensure reboot status is set reboot_pending = self.package_manager.is_reboot_pending() diff --git a/src/core/src/core_logic/PatchInstaller.py b/src/core/src/core_logic/PatchInstaller.py index 54cf2125..1f69a82f 100644 --- a/src/core/src/core_logic/PatchInstaller.py +++ b/src/core/src/core_logic/PatchInstaller.py @@ -46,6 +46,10 @@ def __init__(self, env_layer, execution_config, composite_logger, telemetry_writ self.attempted_parent_package_install_count = 0 self.successful_parent_package_install_count = 0 self.failed_parent_package_install_count = 0 + self.skipped_esm_packages = [] + self.skipped_esm_package_versions = [] + self.esm_packages_found_without_attach = False # Flag used to record if esm packages excluded as ubuntu vm not attached. + self.stopwatch = Stopwatch(self.env_layer, self.telemetry_writer, self.composite_logger) def start_installation(self, simulate=False): @@ -150,7 +154,14 @@ def install_updates(self, maintenance_window, package_manager, simulate=False): excluded_packages, excluded_package_versions = self.get_excluded_updates(package_manager, packages, package_versions) self.telemetry_writer.write_event("Excluded package list: " + str(excluded_packages), Constants.TelemetryEventLevel.Verbose) - packages, package_versions = self.filter_out_excluded_updates(packages, package_versions, excluded_packages) # Final, honoring exclusions + packages, package_versions = self.filter_out_excluded_updates(packages, package_versions, excluded_packages) # honoring exclusions + + # For ubuntu VMs, filter out esm_packages, if the VM is not attached. + # These packages will already be marked with version as 'UA_ESM_REQUIRED'. + # Esm packages will not be dependent packages to non-esm packages. This is confirmed by Canonical. So, once these are removed from processing, we need not worry about handling it in our batch / sequential patch processing logic. + # Adding this after filtering excluded packages, so we don`t un-intentionally mark excluded esm-package status as failed. + packages, package_versions, self.skipped_esm_packages, self.skipped_esm_package_versions, self.esm_packages_found_without_attach = package_manager.filter_out_esm_packages(packages, package_versions) + self.telemetry_writer.write_event("Final package list: " + str(packages), Constants.TelemetryEventLevel.Verbose) # Set initial statuses @@ -158,12 +169,16 @@ def install_updates(self, maintenance_window, package_manager, simulate=False): self.status_handler.set_package_install_status(not_included_packages, not_included_package_versions, Constants.NOT_SELECTED) self.status_handler.set_package_install_status(excluded_packages, excluded_package_versions, Constants.EXCLUDED) self.status_handler.set_package_install_status(packages, package_versions, Constants.PENDING) + self.status_handler.set_package_install_status(self.skipped_esm_packages, self.skipped_esm_package_versions, Constants.FAILED) self.composite_logger.log("\nList of packages to be updated: \n" + str(packages)) sec_packages, sec_package_versions = self.package_manager.get_security_updates() self.telemetry_writer.write_event("Security packages out of the final package list: " + str(sec_packages), Constants.TelemetryEventLevel.Verbose) self.status_handler.set_package_install_status_classification(sec_packages, sec_package_versions, classification="Security") + # Set the security-esm package status. + package_manager.set_security_esm_package_status(Constants.INSTALLATION) + self.composite_logger.log("\nNote: Packages that are neither included nor excluded may still be installed if an included package has a dependency on it.") # We will see this as packages going from NotSelected --> Installed. We could remove them preemptively from not_included_packages, but we're explicitly choosing not to. @@ -237,15 +252,11 @@ def install_updates(self, maintenance_window, package_manager, simulate=False): # point in time status progress_status = self.progress_template.format(str(datetime.timedelta(minutes=remaining_time)), 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), "Processing package: " + str(package) + " (" + str(version) + ")") - if version == Constants.UA_ESM_REQUIRED: - progress_status += "[Skipping - requires Ubuntu Advantage for Infrastructure with Extended Security Maintenance]" - self.composite_logger.log(progress_status) - self.status_handler.set_package_install_status(package_manager.get_product_name(package), str(version), Constants.NOT_SELECTED) # may be changed to Failed in the future - continue + self.composite_logger.log(progress_status) # include all dependencies (with specified versions) explicitly - # package_and_dependencies initially conains only one package. The dependencies are added in the list by method include_dependencies + # 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] @@ -435,14 +446,10 @@ def install_packages_in_batches(self, all_packages, all_package_versions, packag packages_in_batch = [] package_versions_in_batch = [] - skip_packages = [] already_installed_packages = [] for index in range(begin_index, end_index + 1): - if package_versions[index] == Constants.UA_ESM_REQUIRED: - skip_packages.append(packages[index]) - self.status_handler.set_package_install_status(package_manager.get_product_name(packages[index]), str(package_versions[index]), Constants.NOT_SELECTED) - elif packages[index] not in self.last_still_needed_packages: + if packages[index] not in self.last_still_needed_packages: # Could have got installed as dependent package of some other package. Package installation status could also have been set. already_installed_packages.append(packages[index]) self.attempted_parent_package_install_count += 1 @@ -454,9 +461,6 @@ def install_packages_in_batches(self, all_packages, all_package_versions, packag if len(already_installed_packages) > 0: self.composite_logger.log("Following packages are already installed. Could have got installed as dependent package of some other package " + str(already_installed_packages)) - if len(skip_packages) > 0: - self.composite_logger.log("[Skipping packages " + str(skip_packages) + " - requires Ubuntu Advantage for Infrastructure with Extended Security Maintenance]") - if len(packages_in_batch) == 0: continue @@ -553,10 +557,16 @@ def mark_installation_completed(self): # RebootNever is selected and pending, set status warning else success if self.reboot_manager.reboot_setting == Constants.REBOOT_NEVER and self.reboot_manager.is_reboot_pending(): + # Set error details inline with windows extension when setting warning status. This message will be shown in portal. + self.status_handler.add_error_to_status("Machine is Required to reboot. However, the customer-specified reboot setting doesn't allow reboots.", Constants.PatchOperationErrorCodes.DEFAULT_ERROR) self.status_handler.set_installation_substatus_json(status=Constants.STATUS_WARNING) else: self.status_handler.set_installation_substatus_json(status=Constants.STATUS_SUCCESS) + # If esm packages are found, set the status as warning. This will show up in portal along with the error message we already set. + if self.esm_packages_found_without_attach: + 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 # When available, HealthStoreId always takes precedence over the 'overriden' Maintenance Run Id that is being re-purposed for other reasons # In the future, maintenance run id will be completely deprecated for health store reporting. diff --git a/src/core/src/package_managers/AptitudePackageManager.py b/src/core/src/package_managers/AptitudePackageManager.py index 87bf34fd..9f35895b 100644 --- a/src/core/src/package_managers/AptitudePackageManager.py +++ b/src/core/src/package_managers/AptitudePackageManager.py @@ -72,6 +72,9 @@ def __init__(self, env_layer, execution_config, composite_logger, telemetry_writ self.ubuntu_pro_client = UbuntuProClient(env_layer, composite_logger) self.check_pro_client_prerequisites() + self.ubuntu_pro_client_all_updates_cached = [] + self.ubuntu_pro_client_all_updates_versions_cached = [] + def refresh_repo(self): self.composite_logger.log("\nRefreshing local repo...") self.invoke_package_manager(self.repo_refresh) @@ -131,20 +134,50 @@ def invoke_apt_cache(self, command): # region Classification-based (incl. All) update check def get_all_updates(self, cached=False): """Get all missing updates""" + all_updates = [] + all_updates_versions = [] + ubuntu_pro_client_all_updates_query_success = False + self.composite_logger.log_debug("\nDiscovering all packages...") - if cached and not len(self.all_updates_cached) == 0: - self.composite_logger.log_debug(" - Returning cached package data.") - return self.all_updates_cached, self.all_update_versions_cached # allows for high performance reuse in areas of the code explicitly aware of the cache + # use Ubuntu Pro Client cached list when the conditions are met. + if self.__pro_client_prereq_met and not len(self.ubuntu_pro_client_all_updates_cached) == 0: + all_updates = self.ubuntu_pro_client_all_updates_cached + all_updates_versions = self.ubuntu_pro_client_all_updates_versions_cached + + elif not self.__pro_client_prereq_met and not len(self.all_updates_cached) == 0: + all_updates = self.all_updates_cached + all_updates_versions = self.all_update_versions_cached + if cached and not len(all_updates) == 0: + self.composite_logger.log_debug("Get all updates : [Cached={0}][PackagesCount={1}]]".format(cached, len(all_updates))) + return all_updates, all_updates_versions + + # when cached is False, query both default way and using Ubuntu Pro Client. cmd = self.dist_upgrade_simulation_cmd_template.replace('', '') out = self.invoke_package_manager(cmd) self.all_updates_cached, self.all_update_versions_cached = self.extract_packages_and_versions(out) - self.composite_logger.log_debug("Discovered " + str(len(self.all_updates_cached)) + " package entries.") - return self.all_updates_cached, self.all_update_versions_cached + if self.__pro_client_prereq_met: + ubuntu_pro_client_all_updates_query_success, self.ubuntu_pro_client_all_updates_cached, self.ubuntu_pro_client_all_updates_versions_cached = self.ubuntu_pro_client.get_all_updates() + + self.composite_logger.log_debug("Get all updates : [DefaultAllPackagesCount={0}][UbuntuProClientQuerySuccess={1}][UbuntuProClientAllPackagesCount={2}]".format(len(self.all_updates_cached), ubuntu_pro_client_all_updates_query_success, len(self.ubuntu_pro_client_all_updates_cached))) + + # Get the list of updates that are present in only one of the two lists. + different_updates = list(set(self.all_updates_cached) - set(self.ubuntu_pro_client_all_updates_cached)) + list(set(self.ubuntu_pro_client_all_updates_cached) - set(self.all_updates_cached)) + self.composite_logger.log_debug("Get all updates : [DifferentUpdatesCount={0}][Updates={1}]".format(len(different_updates), different_updates)) + + # Prefer Ubuntu Pro Client output when available. + if ubuntu_pro_client_all_updates_query_success: + return self.ubuntu_pro_client_all_updates_cached, self.ubuntu_pro_client_all_updates_versions_cached + else: + return self.all_updates_cached, self.all_update_versions_cached def get_security_updates(self): """Get missing security updates""" + ubuntu_pro_client_security_updates_query_success = False + ubuntu_pro_client_security_packages = [] + ubuntu_pro_client_security_package_versions = [] + self.composite_logger.log("\nDiscovering 'security' packages...") code, out = self.env_layer.run_command_output(self.prep_security_sources_list_cmd, False, False) if code != 0: @@ -157,15 +190,37 @@ def get_security_updates(self): out = self.invoke_package_manager(cmd) security_packages, security_package_versions = self.extract_packages_and_versions(out) - self.composite_logger.log("Discovered " + str(len(security_packages)) + " 'security' package entries.") - return security_packages, security_package_versions + if self.__pro_client_prereq_met: + ubuntu_pro_client_security_updates_query_success, ubuntu_pro_client_security_packages, ubuntu_pro_client_security_package_versions = self.ubuntu_pro_client.get_security_updates() + + self.composite_logger.log_debug("Get Security Updates : [DefaultSecurityPackagesCount={0}][UbuntuProClientQuerySuccess={1}][UbuntuProClientSecurityPackagesCount={2}]".format(len(security_packages), ubuntu_pro_client_security_updates_query_success, len(ubuntu_pro_client_security_packages))) + + if ubuntu_pro_client_security_updates_query_success: + return ubuntu_pro_client_security_packages, ubuntu_pro_client_security_package_versions + else: + return security_packages, security_package_versions + + def get_security_esm_updates(self): + """Get missing security-esm updates.""" + ubuntu_pro_client_security_esm_updates_query_success = False + ubuntu_pro_client_security_esm_packages = [] + ubuntu_pro_client_security_package_esm_versions = [] + + if self.__pro_client_prereq_met: + ubuntu_pro_client_security_esm_updates_query_success, ubuntu_pro_client_security_esm_packages, ubuntu_pro_client_security_package_esm_versions = self.ubuntu_pro_client.get_security_esm_updates() + + self.composite_logger.log_debug("Get Security ESM updates : [UbuntuProClientQuerySuccess={0}][UbuntuProClientSecurityEsmPackagesCount={1}]".format(ubuntu_pro_client_security_esm_updates_query_success, len(ubuntu_pro_client_security_esm_packages))) + return ubuntu_pro_client_security_esm_updates_query_success, ubuntu_pro_client_security_esm_packages, ubuntu_pro_client_security_package_esm_versions def get_other_updates(self): """Get missing other updates""" - self.composite_logger.log("\nDiscovering 'other' packages...") + ubuntu_pro_client_other_updates_query_success = False + ubuntu_pro_client_other_packages = [] + ubuntu_pro_client_other_package_versions = [] other_packages = [] other_package_versions = [] + self.composite_logger.log("\nDiscovering 'other' packages...") all_packages, all_package_versions = self.get_all_updates(True) security_packages, security_package_versions = self.get_security_updates() @@ -174,8 +229,15 @@ def get_other_updates(self): other_packages.append(package) other_package_versions.append(all_package_versions[index]) - self.composite_logger.log("Discovered " + str(len(other_packages)) + " 'other' package entries.") - return other_packages, other_package_versions + if self.__pro_client_prereq_met: + ubuntu_pro_client_other_updates_query_success, ubuntu_pro_client_other_packages, ubuntu_pro_client_other_package_versions = self.ubuntu_pro_client.get_other_updates() + + self.composite_logger.log_debug("Get Other Updates : [DefaultOtherPackagesCount={0}][UbuntuProClientQuerySuccess={1}][UbuntuProClientOtherPackagesCount={2}]".format(len(other_packages), ubuntu_pro_client_other_updates_query_success, len(ubuntu_pro_client_other_packages))) + + if ubuntu_pro_client_other_updates_query_success: + return ubuntu_pro_client_other_packages, ubuntu_pro_client_other_package_versions + else: + return other_packages, other_package_versions # endregion # region Output Parser(s) @@ -581,6 +643,19 @@ def check_pro_client_prerequisites(self): self.composite_logger.log_debug("Ubuntu Pro Client pre-requisite checks:[IsFeatureEnabled={0}][IsOSVersionCompatible={1}][IsPythonCompatible={2}][Error={3}]".format(Constants.UbuntuProClientSettings.FEATURE_ENABLED, self.__get_os_major_version() <= Constants.UbuntuProClientSettings.MAX_OS_MAJOR_VERSION_SUPPORTED, self.__is_minimum_required_python_installed(), exception_error)) return self.__pro_client_prereq_met + def set_security_esm_package_status(self, operation): + """Set the security-ESM classification for the esm packages.""" + security_esm_update_query_success, security_esm_updates, security_esm_updates_versions = self.get_security_esm_updates() + if self.__pro_client_prereq_met and security_esm_update_query_success and len(security_esm_updates) > 0: + self.telemetry_writer.write_event("set Security-ESM package status:[Operation={0}][Updates={1}]".format(operation, str(security_esm_updates)), Constants.TelemetryEventLevel.Verbose) + if operation == Constants.ASSESSMENT: + self.status_handler.set_package_assessment_status(security_esm_updates, security_esm_updates_versions, Constants.PackageClassification.SECURITY_ESM) + # If the Ubuntu Pro Client is not attached, set the error with the code UA_ESM_REQUIRED. This will be used in portal to mark the VM as unattached to pro. + if not self.ubuntu_pro_client.is_ubuntu_pro_client_attached: + self.status_handler.add_error_to_status("{0} patches requires Ubuntu Pro for Infrastructure with Extended Security Maintenance".format(len(security_esm_updates)), Constants.PatchOperationErrorCodes.UA_ESM_REQUIRED) + elif operation == Constants.INSTALLATION: + self.status_handler.set_package_install_status_classification(security_esm_updates, security_esm_updates_versions, Constants.PackageClassification.SECURITY_ESM) + def __get_os_major_version(self): """get the OS major version""" # Sample output for linux_distribution(): @@ -599,3 +674,32 @@ def add_arch_dependencies(self, package_manager, package, packages, package_vers Only required for yum. No-op for apt and zypper. """ return + + def filter_out_esm_packages(self, packages, package_versions): + """ + Filter out packages from the list where the version matches the UA_ESM_REQUIRED string. + """ + non_esm_packages = [] + non_esm_package_versions = [] + esm_packages = [] + esm_package_versions = [] + esm_packages_found = False + + for pkg, version in zip(packages, package_versions): + if version != Constants.UA_ESM_REQUIRED: + non_esm_packages.append(pkg) + non_esm_package_versions.append(version) + continue + + # version is UA_ESM_REQUIRED. + esm_packages.append(pkg) + esm_package_versions.append(version) + + esm_packages_found = len(esm_packages) > 0 + if esm_packages_found: + self.status_handler.add_error_to_status("{0} patches requires Ubuntu Pro for Infrastructure with Extended Security Maintenance".format(len(esm_packages)), Constants.PatchOperationErrorCodes.UA_ESM_REQUIRED) # Set the error status with the esm_package details. Will be used in portal. + self.telemetry_writer.write_event("Filter esm packages [EsmPackagesCount={0}]".format(len(esm_packages)), Constants.TelemetryEventLevel.Informational) + + self.composite_logger.log_debug("Filter esm packages : [TotalPackagesCount={0}][EsmPackagesCount={1}]".format(len(packages), len(esm_packages))) + return non_esm_packages, non_esm_package_versions, esm_packages, esm_package_versions, esm_packages_found + diff --git a/src/core/src/package_managers/PackageManager.py b/src/core/src/package_managers/PackageManager.py index 19db14da..13ba3eff 100644 --- a/src/core/src/package_managers/PackageManager.py +++ b/src/core/src/package_managers/PackageManager.py @@ -200,12 +200,12 @@ def install_updates_fail_safe(self, excluded_packages): def install_update_and_dependencies(self, package_and_dependencies, package_and_dependency_versions, simulate=False): """ Install a list of packages along with dependencies (explicitly) - + Parameters: package_and_dependencies (List of strings): List of packages along with dependencies to install package_and_dependency_versions (List of strings): Versions of the packages in the list package_and_dependencies simulate (bool): Whether this function call is from test run. - + Returns: code (int): Output code of the command run to install packages out (string): Output string of the command run to install packages @@ -230,7 +230,7 @@ def install_update_and_dependencies(self, package_and_dependencies, package_and_ def get_installation_status(self, code, out, exec_cmd, package, version, simulate=False): """ Returns result of the package installation - + Parameters: code (int): Output code of the command run to install packages. out (string): Output string of the command run to install packages. @@ -238,7 +238,7 @@ def get_installation_status(self, code, out, exec_cmd, package, version, simulat package (string): Package name. version (string): Package version. simulate (bool): Whether this function call is from test run. - + Returns: install_result (string): Package installation result """ @@ -317,7 +317,7 @@ def install_update_and_dependencies_and_get_status(self, package_and_dependencie package_and_dependencies (List of strings): List of packages along with dependencies to install package_and_dependency_versions (List of strings): Versions of the packages in the list package_and_dependencies simulate (bool): Whether this function call is from test run. - + Returns: install_result (string): Package installation result """ @@ -440,15 +440,23 @@ def add_arch_dependencies(self, package_manager, package, packages, package_vers """ Add the packages with same name as that of input parameter package but with different architectures from packages list to the list package_and_dependencies. Only required for yum. No-op for apt and zypper. - + Parameters: package_manager (PackageManager): Package manager used. package (string): Input package for which same package name but different architecture need to be added in the list package_and_dependencies. packages (List of strings): List of all packages selected by user to install. package_versions (List of strings): Versions of packages in packages list. - package_and_dependencies (List of strings): List of packages along with dependencies. This function adds packages with same name as input parameter package + package_and_dependencies (List of strings): List of packages along with dependencies. This function adds packages with same name as input parameter package but different architecture in this list. package_and_dependency_versions (List of strings): Versions of packages in package_and_dependencies. """ pass + @abstractmethod + def set_security_esm_package_status(self, operation): + pass + + @abstractmethod + def filter_out_esm_packages(self, packages, package_versions): + pass + diff --git a/src/core/src/package_managers/UbuntuProClient.py b/src/core/src/package_managers/UbuntuProClient.py index 818c32ee..0fe82346 100644 --- a/src/core/src/package_managers/UbuntuProClient.py +++ b/src/core/src/package_managers/UbuntuProClient.py @@ -15,6 +15,8 @@ # Requires Python 3.5+ """This is the Ubuntu Pro Client implementation""" +import json +from core.src.bootstrap.Constants import Constants class UbuntuProClient: @@ -22,6 +24,9 @@ def __init__(self, env_layer, composite_logger): self.env_layer = env_layer self.composite_logger = composite_logger self.ubuntu_pro_client_install_cmd = 'sudo apt-get install ubuntu-advantage-tools -y' + self.ubuntu_pro_client_security_status_cmd = 'pro security-status --format=json' + self.security_esm_criteria_strings = ["esm-infra", "esm-apps"] + self.is_ubuntu_pro_client_attached = False def install_or_update_pro(self): """install/update pro(ubuntu-advantage-tools) to the latest version""" @@ -42,26 +47,99 @@ def is_pro_working(self): ubuntu_pro_client_exception = None is_ubuntu_pro_client_working = False ubuntu_pro_client_version = None + is_minimum_ubuntu_pro_version_installed = False try: from uaclient.api.u.pro.version.v1 import version + from distutils.version import LooseVersion # Importing this module here as there is conflict between "distutils.version" and "uaclient.api.u.pro.version.v1.version when 'LooseVersion' is called." version_result = version() ubuntu_pro_client_version = version_result.installed_version - if ubuntu_pro_client_version is not None: + is_minimum_ubuntu_pro_version_installed = LooseVersion(ubuntu_pro_client_version) >= LooseVersion(Constants.UbuntuProClientSettings.MINIMUM_CLIENT_VERSION) + if ubuntu_pro_client_version is not None and is_minimum_ubuntu_pro_version_installed: is_ubuntu_pro_client_working = True + self.is_ubuntu_pro_client_attached = self.log_ubuntu_pro_client_attached() except Exception as error: ubuntu_pro_client_exception = repr(error) - self.composite_logger.log_debug("Ubuntu Pro Client working: [Success={0}][UbuntuProClientVersion={1}][Error={2}]".format(is_ubuntu_pro_client_working, ubuntu_pro_client_version, ubuntu_pro_client_exception)) + self.composite_logger.log_debug("Is Ubuntu Pro Client working debug flags: [Success={0}][UbuntuProClientVersion={1}][UbuntuProClientMinimumVersionInstalled={2}][IsAttached={3}][Error={4}]".format(is_ubuntu_pro_client_working, ubuntu_pro_client_version, is_minimum_ubuntu_pro_version_installed, self.is_ubuntu_pro_client_attached, ubuntu_pro_client_exception)) return is_ubuntu_pro_client_working + def log_ubuntu_pro_client_attached(self): + """log the attachment status of the machine.""" + is_ubuntu_pro_client_attached = False + try: + code, output = self.env_layer.run_command_output(self.ubuntu_pro_client_security_status_cmd, False, False) + if code == 0: + is_ubuntu_pro_client_attached = json.loads(output)['summary']['ua']['attached'] + except Exception as error: + ubuntu_pro_client_exception = repr(error) + self.composite_logger.log_debug("Ubuntu Pro Client Attached Exception: [Exception={0}]".format(ubuntu_pro_client_exception)) + return is_ubuntu_pro_client_attached + + def extract_packages_and_versions(self, updates): + extracted_updates = [] + extracted_updates_versions = [] + + for update in updates: + extracted_updates.append(update.package) + if not self.is_ubuntu_pro_client_attached and update.provided_by in self.security_esm_criteria_strings: + extracted_updates_versions.append(Constants.UA_ESM_REQUIRED) + else: + extracted_updates_versions.append(update.version) + return extracted_updates, extracted_updates_versions + + def get_filtered_updates(self, filter_criteria): + """query Ubuntu Pro Client to get filtered updates.""" + updates_query_success = False + updates = [] + versions = [] + updates_exception = None + try: + ubuntu_pro_client_updates = self.get_ubuntu_pro_client_updates() + updates_query_success = True + if len(filter_criteria) > 0: # Filter the updates only when the criteria strings are passed. + filtered_updates = [update for update in ubuntu_pro_client_updates if update.provided_by in filter_criteria] + else: + filtered_updates = ubuntu_pro_client_updates + updates, versions = self.extract_packages_and_versions(filtered_updates) + except Exception as error: + updates_exception = repr(error) + + return updates_query_success, updates_exception, updates, versions + def get_security_updates(self): - pass + """query Ubuntu Pro Client to get security updates.""" + security_criteria = ["standard-security"] + security_updates_query_success, security_updates_exception, security_updates, security_updates_versions = self.get_filtered_updates(security_criteria) + + self.composite_logger.log_debug("Ubuntu Pro Client get security updates : [SecurityUpdatesCount={0}][error={1}]".format(len(security_updates), security_updates_exception)) + return security_updates_query_success, security_updates, security_updates_versions + + def get_security_esm_updates(self): + """query Ubuntu Pro Client to get security-esm updates.""" + security_esm_updates_query_success, security_esm_updates_exception, security_esm_updates, security_esm_updates_versions = self.get_filtered_updates(self.security_esm_criteria_strings) + + self.composite_logger.log_debug("Ubuntu Pro Client get security-esm updates : [SecurityEsmUpdatesCount={0}][error={1}]".format(len(security_esm_updates),security_esm_updates_exception)) + return security_esm_updates_query_success, security_esm_updates, security_esm_updates_versions def get_all_updates(self): - pass + """query Ubuntu Pro Client to get all updates.""" + filter_criteria = [] + all_updates_query_success, all_updates_exception, all_updates, all_updates_versions = self.get_filtered_updates(filter_criteria) + + self.composite_logger.log_debug("Ubuntu Pro Client get all updates: [AllUpdatesCount={0}][error={1}]".format(len(all_updates), all_updates_exception)) + return all_updates_query_success, all_updates, all_updates_versions + + def get_ubuntu_pro_client_updates(self): + from uaclient.api.u.pro.packages.updates.v1 import updates + return updates().updates def get_other_updates(self): - pass + """query Ubuntu Pro Client to get other updates.""" + other_criteria = ["standard-updates"] + other_updates_query_success, other_update_exception, other_updates, other_updates_versions = self.get_filtered_updates(other_criteria) + + self.composite_logger.log_debug("Ubuntu Pro Client get other updates: [OtherUpdatesCount={0}][error = {1}]".format(len(other_updates), other_update_exception)) + return other_updates_query_success, other_updates, other_updates_versions def is_reboot_pending(self): """query pro api to get the reboot status""" diff --git a/src/core/src/package_managers/YumPackageManager.py b/src/core/src/package_managers/YumPackageManager.py index c5a8814c..7c1f9268 100644 --- a/src/core/src/package_managers/YumPackageManager.py +++ b/src/core/src/package_managers/YumPackageManager.py @@ -943,13 +943,12 @@ def do_processes_require_restart(self): def add_arch_dependencies(self, package_manager, package, packages, package_versions, package_and_dependencies, package_and_dependency_versions): """ Add the packages with same name as that of input parameter package but with different architectures from packages list to the list package_and_dependencies. - Parameters: package_manager (PackageManager): Package manager used. package (string): Input package for which same package name but different architecture need to be added in the list package_and_dependencies. packages (List of strings): List of all packages selected by user to install. package_versions (List of strings): Versions of packages in packages list. - package_and_dependencies (List of strings): List of packages along with dependencies. This function adds packages with same name as input parameter package + package_and_dependencies (List of strings): List of packages along with dependencies. This function adds packages with same name as input parameter package but different architecture in this list. package_and_dependency_versions (List of strings): Versions of packages in package_and_dependencies. """ @@ -958,3 +957,21 @@ def add_arch_dependencies(self, package_manager, package, packages, package_vers if package_manager.get_product_name_without_arch(possible_arch_dependency) == package_name_without_arch and possible_arch_dependency not in package_and_dependencies: package_and_dependencies.append(possible_arch_dependency) package_and_dependency_versions.append(possible_arch_dependency_version) + + def set_security_esm_package_status(self, operation): + """ + Set the security-ESM classification for the esm packages. Only needed for apt. No-op for yum and zypper. + """ + pass + + def filter_out_esm_packages(self, packages, package_versions): + """ + Filter out packages from the list where the version matches the UA_ESM_REQUIRED string. + Only needed for apt. No-op for yum and zypper + """ + esm_packages = [] + esm_package_versions = [] + esm_packages_found = False + + return packages, package_versions, esm_packages, esm_package_versions, esm_packages_found + diff --git a/src/core/src/package_managers/ZypperPackageManager.py b/src/core/src/package_managers/ZypperPackageManager.py index e57d9f36..5e866b4e 100644 --- a/src/core/src/package_managers/ZypperPackageManager.py +++ b/src/core/src/package_managers/ZypperPackageManager.py @@ -812,3 +812,21 @@ def add_arch_dependencies(self, package_manager, package, packages, package_vers Only required for yum. No-op for apt and zypper. """ return + + def set_security_esm_package_status(self, operation): + """ + Set the security-ESM classification for the esm packages. Only needed for apt. No-op for yum and zypper. + """ + pass + + def filter_out_esm_packages(self, packages, package_versions): + """ + Filter out packages from the list where the version matches the UA_ESM_REQUIRED string. + Only needed for apt. No-op for yum and zypper + """ + esm_packages = [] + esm_package_versions = [] + esm_packages_found = False + + return packages, package_versions, esm_packages, esm_package_versions, esm_packages_found + diff --git a/src/core/src/service_interfaces/StatusHandler.py b/src/core/src/service_interfaces/StatusHandler.py index e5c790f6..d25233cd 100644 --- a/src/core/src/service_interfaces/StatusHandler.py +++ b/src/core/src/service_interfaces/StatusHandler.py @@ -323,7 +323,7 @@ def __new_assessment_summary_json(self, assessment_packages_json, status, code): other_patch_count = 0 for i in range(0, len(assessment_packages_json)): classifications = assessment_packages_json[i]['classifications'] - if "Critical" in classifications or "Security" in classifications: + if "Critical" in classifications or "Security" in classifications or "Security-ESM" in classifications: critsec_patch_count += 1 else: other_patch_count += 1 diff --git a/src/core/tests/Test_AptitudePackageManager.py b/src/core/tests/Test_AptitudePackageManager.py index e4f77afb..f15fb6dd 100644 --- a/src/core/tests/Test_AptitudePackageManager.py +++ b/src/core/tests/Test_AptitudePackageManager.py @@ -17,7 +17,7 @@ import os import unittest from core.src.bootstrap.Constants import Constants -from core.tests.Test_UbuntuProClient import MockVersionResult, MockRebootRequiredResult +from core.tests.Test_UbuntuProClient import MockVersionResult, MockRebootRequiredResult, MockUpdatesResult from core.tests.library.ArgumentComposer import ArgumentComposer from core.tests.library.LegacyEnvLayerExtensions import LegacyEnvLayerExtensions from core.tests.library.RuntimeCompositor import RuntimeCompositor @@ -59,6 +59,10 @@ def mock_is_reboot_pending_returns_False(self): def mock_os_path_isfile_raise_exception(self, file): raise Exception + + def mock_get_security_updates_return_empty_list(self): + return [], [] + #endregion Mocks def test_package_manager_no_updates(self): @@ -445,6 +449,66 @@ def test_package_manager_instance_created_even_when_exception_thrown_in_pro(self UbuntuProClient.UbuntuProClient.install_or_update_pro = backup_package_manager_ubuntu_pro_client_install_or_update_pro + def test_get_other_updates_success(self): + obj = MockVersionResult() + obj.mock_import_uaclient_version_module('version', 'mock_version') + updates_obj = MockUpdatesResult() + updates_obj.mock_import_uaclient_update_module('updates', 'mock_update_list_with_all_update_types') + runtime = RuntimeCompositor(ArgumentComposer().get_composed_arguments(), True, Constants.APT) + runtime.set_legacy_test_type('UA_ESM_Required') + + backup_AptitudePackageManager__pro_client_prereq_met = runtime.package_manager._AptitudePackageManager__pro_client_prereq_met + runtime.package_manager._AptitudePackageManager__pro_client_prereq_met = True + + packages, versions = runtime.package_manager.get_other_updates() + self.assertEqual(1, len(packages)) + + runtime.package_manager._AptitudePackageManager__pro_client_prereq_met = backup_AptitudePackageManager__pro_client_prereq_met + obj.mock_unimport_uaclient_version_module() + updates_obj.mock_unimport_uaclient_update_module() + + def test_get_other_updates_without_pro_success(self): + runtime = RuntimeCompositor(ArgumentComposer().get_composed_arguments(), True, Constants.APT) + runtime.set_legacy_test_type('UA_ESM_Required') + backup_package_manager_get_security_updates = runtime.package_manager.get_security_updates + runtime.package_manager.get_security_updates = self.mock_get_security_updates_return_empty_list + + packages, versions = runtime.package_manager.get_other_updates() + self.assertEqual(1, len(packages)) + + runtime.package_manager.get_security_updates = backup_package_manager_get_security_updates + + def test_set_security_esm_package_status_assessment(self): + obj = MockVersionResult() + obj.mock_import_uaclient_version_module('version', 'mock_version') + updates_obj = MockUpdatesResult() + updates_obj.mock_import_uaclient_update_module('updates', 'mock_update_list_with_all_update_types') + runtime = RuntimeCompositor(ArgumentComposer().get_composed_arguments(), True, Constants.APT) + runtime.set_legacy_test_type('UA_ESM_Required') + backup_aptitudepackagemanager__pro_client_prereq_met = runtime.package_manager._AptitudePackageManager__pro_client_prereq_met + runtime.package_manager._AptitudePackageManager__pro_client_prereq_met = True + + runtime.patch_assessor.start_assessment() + status = "" + error_set = False + with self.runtime.env_layer.file_system.open(self.runtime.execution_config.status_file_path, 'r') as file_handle: + status = json.load(file_handle) + self.assertEqual(status[0]["status"]["status"].lower(), Constants.STATUS_SUCCESS.lower()) + self.assertEqual(status[0]["status"]["substatus"][0]["name"], "PatchAssessmentSummary") + + # Parse the assessment data to check if we have logged the error details for esm_required. + assessment_data = status[0]["status"]["substatus"][0]["formattedMessage"]["message"] + error_list = json.loads(assessment_data)["errors"]["details"] + for error in error_list: + if error["code"] == Constants.PatchOperationErrorCodes.UA_ESM_REQUIRED: + error_set = True + break + self.assertTrue(error_set) + + runtime.package_manager._AptitudePackageManager__pro_client_prereq_met = backup_aptitudepackagemanager__pro_client_prereq_met + obj.mock_unimport_uaclient_version_module() + updates_obj.mock_unimport_uaclient_update_module() + def test_is_reboot_pending_pro_client_success(self): reboot_mock = MockRebootRequiredResult() reboot_mock.mock_import_uaclient_reboot_required_module('reboot_required', 'mock_reboot_required_return_no') diff --git a/src/core/tests/Test_PatchInstaller.py b/src/core/tests/Test_PatchInstaller.py index 2cfcd85f..12068df6 100644 --- a/src/core/tests/Test_PatchInstaller.py +++ b/src/core/tests/Test_PatchInstaller.py @@ -18,6 +18,7 @@ import json import unittest from core.src.bootstrap.Constants import Constants +from core.tests.Test_UbuntuProClient import MockUpdatesResult, MockVersionResult from core.tests.library.ArgumentComposer import ArgumentComposer from core.tests.library.RuntimeCompositor import RuntimeCompositor @@ -198,6 +199,64 @@ def test_apt_install_success(self): self.assertFalse(maintenance_window_exceeded) runtime.stop() + def test_apt_install_skips_esm_packages(self): + obj = MockUpdatesResult() + obj.mock_import_uaclient_update_module('updates', 'mock_update_list_with_one_esm_update') + version_obj = MockVersionResult() + version_obj.mock_import_uaclient_version_module('version', 'mock_version') + current_time = datetime.datetime.utcnow() + td = datetime.timedelta(hours=0, minutes=20) + job_start_time = (current_time - td).strftime("%Y-%m-%dT%H:%M:%S.9999Z") + argument_composer = ArgumentComposer() + argument_composer.maximum_duration = 'PT1H' + argument_composer.start_time = job_start_time + runtime = RuntimeCompositor(argument_composer.get_composed_arguments(), True, Constants.APT) + # Path change + runtime.set_legacy_test_type('UA_ESM_Required') + backup_package_manager_ubuntu_pro_client_attached = runtime.package_manager.ubuntu_pro_client.is_ubuntu_pro_client_attached + runtime.package_manager.ubuntu_pro_client.is_ubuntu_pro_client_attached = False + + # esm package should be skipped. + installed_update_count, update_run_successful, maintenance_window_exceeded = runtime.patch_installer.install_updates( + runtime.maintenance_window, runtime.package_manager, simulate=True) + self.assertEqual(0, installed_update_count) + self.assertTrue(update_run_successful) + self.assertFalse(maintenance_window_exceeded) + runtime.stop() + + runtime.package_manager.ubuntu_pro_client.is_ubuntu_pro_client_attached = backup_package_manager_ubuntu_pro_client_attached + obj.mock_unimport_uaclient_update_module() + version_obj.mock_unimport_uaclient_version_module() + + def test_mark_status_completed_esm_required(self): + obj = MockUpdatesResult() + obj.mock_import_uaclient_update_module('updates', 'mock_update_list_with_one_esm_update') + version_obj = MockVersionResult() + version_obj.mock_import_uaclient_version_module('version', 'mock_version') + current_time = datetime.datetime.utcnow() + td = datetime.timedelta(hours=0, minutes=20) + job_start_time = (current_time - td).strftime("%Y-%m-%dT%H:%M:%S.9999Z") + argument_composer = ArgumentComposer() + argument_composer.maximum_duration = 'PT1H' + argument_composer.start_time = job_start_time + runtime = RuntimeCompositor(argument_composer.get_composed_arguments(), True, Constants.APT) + # Path change + runtime.set_legacy_test_type('UA_ESM_Required') + backup_package_manager_ubuntu_pro_client_attached = runtime.package_manager.ubuntu_pro_client.is_ubuntu_pro_client_attached + runtime.package_manager.ubuntu_pro_client.is_ubuntu_pro_client_attached = False + + runtime.patch_installer.install_updates(runtime.maintenance_window, runtime.package_manager, simulate=True) + runtime.patch_installer.mark_installation_completed() + 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"] + + self.assertEqual('warning', substatus_file_data[0]['status']) + + runtime.stop() + runtime.package_manager.ubuntu_pro_client.is_ubuntu_pro_client_attached = backup_package_manager_ubuntu_pro_client_attached + obj.mock_unimport_uaclient_update_module() + version_obj.mock_unimport_uaclient_version_module() + def test_apt_install_success_not_enough_time_for_batch_patching(self): # total packages to install is 3, reboot_setting is 'Never', so cutoff time for batch = 3*5 = 15 # window size is 60 minutes, let time remain = 14 minutes so that not enough time to install in batch diff --git a/src/core/tests/Test_UbuntuProClient.py b/src/core/tests/Test_UbuntuProClient.py index 067b4dca..6f6dd5b9 100644 --- a/src/core/tests/Test_UbuntuProClient.py +++ b/src/core/tests/Test_UbuntuProClient.py @@ -45,12 +45,15 @@ def mock_unimport_module(self, module_name): class MockVersionResult(MockSystemModules): - def __init__(self, version='27.13.5~18.04.1'): + def __init__(self, version='27.14.4~18.04.1'): self.installed_version = version def mock_version(self): return MockVersionResult() + def mock_to_below_minimum_version(self): + return MockVersionResult('27.13.4~18.04.1') + def mock_version_raise_exception(self): raise @@ -97,6 +100,50 @@ def mock_unimport_uaclient_reboot_required_module(self): self.mock_unimport_module('uaclient.api.u.pro.security.status.reboot_required.v1') +class UpdateInfo: + def __init__(self, package, version, provided_by, origin): + self.version = version + self.provided_by = provided_by + self.package = package + self.origin = origin + + +class MockUpdatesResult(MockSystemModules): + + def __init__(self, updates = []): + self.updates = updates + + def mock_update_list_with_all_update_types(self): + return MockUpdatesResult(self.get_mock_updates_list_with_three_updates()) + + def mock_update_list_with_one_esm_update(self): + return MockUpdatesResult(self.get_mock_updates_list_with_one_esm_update()) + + @staticmethod + def get_mock_updates_list_with_three_updates(): + return [UpdateInfo(package='python3', provided_by='standard-security', origin='security.ubuntu.com', version='1.2.3-1ubuntu0.3'), + UpdateInfo(package='apt', provided_by='standard-updates', origin='security.ubuntu.com', version='1.2.35'), + UpdateInfo(package='cups', provided_by='esm-infra', origin='security.ubuntu.com', version='2.1.3-4ubuntu0.11+esm1')] + + @staticmethod + def get_mock_updates_list_with_one_esm_update(): + return [UpdateInfo(package='git-man', provided_by='esm-infra', origin='security.ubuntu.com', version='1:2.17.1-1ubuntu0.15')] + + def mock_import_uaclient_update_module(self, mock_name, method_name): + if sys.version_info[0] == 3: + sys.modules['uaclient.api.u.pro.packages.updates.v1'] = types.ModuleType('update_module') + mock_method = getattr(self, method_name) + setattr(sys.modules['uaclient.api.u.pro.packages.updates.v1'], mock_name, mock_method) + else: + update_module = imp.new_module('update_module') + mock_method = getattr(self, method_name) + setattr(update_module, mock_name, mock_method) + self.assign_sys_modules_with_mock_module('uaclient.api.u.pro.packages.updates.v1', update_module) + + def mock_unimport_uaclient_update_module(self): + self.mock_unimport_module('uaclient.api.u.pro.packages.updates.v1') + + class TestUbuntuProClient(unittest.TestCase): def setUp(self): self.runtime = RuntimeCompositor(ArgumentComposer().get_composed_arguments(), True, Constants.APT) @@ -108,6 +155,9 @@ def tearDown(self): def mock_run_command_output_raise_exception(self, cmd="", output=False, chk_err=False): raise Exception + def mock_get_ubuntu_pro_client_updates_raise_exception(self): + raise Exception + def test_install_or_update_pro_success(self): package_manager = self.container.get('package_manager') self.assertTrue(package_manager.ubuntu_pro_client.install_or_update_pro()) @@ -126,7 +176,6 @@ def test_install_or_update_pro_exception(self): package_manager.env_layer.run_command_output = backup_run_command_output - def test_is_pro_working_success(self): obj = MockVersionResult() obj.mock_import_uaclient_version_module('version', 'mock_version') @@ -136,6 +185,15 @@ def test_is_pro_working_success(self): obj.mock_unimport_uaclient_version_module() + def test_is_pro_working_failure_when_minimum_version_required_is_false(self): + obj = MockVersionResult() + obj.mock_import_uaclient_version_module('version', 'mock_to_below_minimum_version') + + package_manager = self.container.get('package_manager') + self.assertFalse(package_manager.ubuntu_pro_client.is_pro_working()) + + obj.mock_unimport_uaclient_version_module() + def test_is_pro_working_failure(self): obj = MockVersionResult() obj.mock_import_uaclient_version_module('version', 'mock_version_raise_exception') @@ -145,6 +203,54 @@ def test_is_pro_working_failure(self): obj.mock_unimport_uaclient_version_module() + def test_log_ubuntu_pro_client_attached_true(self): + package_manager = self.container.get('package_manager') + self.assertTrue(package_manager.ubuntu_pro_client.log_ubuntu_pro_client_attached()) + + def test_log_ubuntu_pro_client_attached_false(self): + self.runtime.set_legacy_test_type('SadPath') + package_manager = self.container.get('package_manager') + self.assertFalse(package_manager.ubuntu_pro_client.log_ubuntu_pro_client_attached()) + + def test_log_ubuntu_pro_client_attached_raises_exception(self): + package_manager = self.container.get('package_manager') + backup_run_command_output = package_manager.env_layer.run_command_output + package_manager.env_layer.run_command_output = self.mock_run_command_output_raise_exception + + self.assertFalse(package_manager.ubuntu_pro_client.log_ubuntu_pro_client_attached()) + + package_manager.env_layer.run_command_output = backup_run_command_output + + def test_extract_packages_and_versions_returns_zero_for_empty_updates(self): + package_manager = self.container.get('package_manager') + empty_updates = [] + updates, version = package_manager.ubuntu_pro_client.extract_packages_and_versions(empty_updates) + self.assertTrue(len(updates) == 0) + self.assertTrue(len(version) == 0) + + def test_extract_packages_and_versions_returns_correct_number_of_updates(self): + package_manager = self.container.get('package_manager') + mock_updates = MockUpdatesResult.get_mock_updates_list_with_three_updates() + updates, versions = package_manager.ubuntu_pro_client.extract_packages_and_versions(mock_updates) + self.assertTrue(len(updates) == len(mock_updates)) + self.assertTrue(len(versions) == len(mock_updates)) + + def test_extract_packages_and_versions_returns_correct_esm_package_count(self): + package_manager = self.container.get('package_manager') + mock_updates = MockUpdatesResult.get_mock_updates_list_with_three_updates() + updates, versions = package_manager.ubuntu_pro_client.extract_packages_and_versions(mock_updates) + expected_count = 0 + actual_count = 0 + for update in mock_updates: + if update.provided_by == 'esm-infra': + expected_count += 1 + + for version in versions: + if version == Constants.UA_ESM_REQUIRED: + actual_count += 1 + + self.assertEqual(expected_count, actual_count) + def test_is_reboot_pending_success(self): obj = MockRebootRequiredResult() obj.mock_import_uaclient_reboot_required_module('reboot_required', 'mock_reboot_required_return_yes') @@ -175,18 +281,116 @@ def test_is_reboot_pending_exception(self): obj.mock_unimport_uaclient_reboot_required_module() - def test_get_security_updates_is_None(self): + def test_get_security_updates_success(self): + obj = MockUpdatesResult() + obj.mock_import_uaclient_update_module('updates', 'mock_update_list_with_all_update_types') package_manager = self.container.get('package_manager') - self.assertIsNone(package_manager.ubuntu_pro_client.get_security_updates()) - def test_get_all_updates_is_None(self): + query_success, updates, versions = package_manager.ubuntu_pro_client.get_security_updates() + + self.assertTrue(query_success) + self.assertEqual(len(updates), 1) + self.assertEqual(len(versions), 1) + + obj.mock_unimport_uaclient_update_module() + + def test_get_security_updates_exception(self): + obj = MockUpdatesResult() + obj.mock_import_uaclient_update_module('updates', 'mock_update_list_with_all_update_types') package_manager = self.container.get('package_manager') - self.assertIsNone(package_manager.ubuntu_pro_client.get_all_updates()) + backup_get_ubuntu_pro_client_updates = package_manager.ubuntu_pro_client.get_ubuntu_pro_client_updates + package_manager.ubuntu_pro_client.get_ubuntu_pro_client_updates = self.mock_get_ubuntu_pro_client_updates_raise_exception + + query_success, updates, versions = package_manager.ubuntu_pro_client.get_security_updates() + self.assertFalse(query_success) + self.assertEqual(len(updates), 0) + self.assertEqual(len(versions), 0) - def test_get_other_updates_is_None(self): + package_manager.ubuntu_pro_client.get_ubuntu_pro_client_updates = backup_get_ubuntu_pro_client_updates + obj.mock_unimport_uaclient_update_module() + + def test_get_security_esm_updates_success(self): + obj = MockUpdatesResult() + obj.mock_import_uaclient_update_module('updates', 'mock_update_list_with_all_update_types') package_manager = self.container.get('package_manager') - self.assertIsNone(package_manager.ubuntu_pro_client.get_other_updates()) + query_success, updates, versions = package_manager.ubuntu_pro_client.get_security_esm_updates() + + self.assertTrue(query_success) + self.assertEqual(len(updates), 1) + self.assertEqual(len(versions), 1) + obj.mock_unimport_uaclient_update_module() + + def test_get_security_esm_updates_exception(self): + obj = MockUpdatesResult() + obj.mock_import_uaclient_update_module('updates', 'mock_update_list_with_all_update_types') + package_manager = self.container.get('package_manager') + backup_get_ubuntu_pro_client_updates = package_manager.ubuntu_pro_client.get_ubuntu_pro_client_updates + package_manager.ubuntu_pro_client.get_ubuntu_pro_client_updates = self.mock_get_ubuntu_pro_client_updates_raise_exception + + query_success, updates, versions = package_manager.ubuntu_pro_client.get_security_esm_updates() + self.assertFalse(query_success) + self.assertEqual(len(updates), 0) + self.assertEqual(len(versions), 0) + + package_manager.ubuntu_pro_client.get_ubuntu_pro_client_updates = backup_get_ubuntu_pro_client_updates + obj.mock_unimport_uaclient_update_module() + + def test_get_all_updates_success(self): + obj = MockUpdatesResult() + obj.mock_import_uaclient_update_module('updates', 'mock_update_list_with_all_update_types') + package_manager = self.container.get('package_manager') + + query_success, updates, versions = package_manager.ubuntu_pro_client.get_all_updates() + + self.assertTrue(query_success) + self.assertEqual(len(updates), 3) + self.assertEqual(len(versions), 3) + + obj.mock_unimport_uaclient_update_module() + + def test_get_all_updates_exception(self): + obj = MockUpdatesResult() + obj.mock_import_uaclient_update_module('updates', 'mock_update_list_with_all_update_types') + package_manager = self.container.get('package_manager') + backup_get_ubuntu_pro_client_updates = package_manager.ubuntu_pro_client.get_ubuntu_pro_client_updates + package_manager.ubuntu_pro_client.get_ubuntu_pro_client_updates = self.mock_get_ubuntu_pro_client_updates_raise_exception + + query_success, updates, versions = package_manager.ubuntu_pro_client.get_all_updates() + self.assertFalse(query_success) + self.assertEqual(len(updates), 0) + self.assertEqual(len(versions), 0) + + package_manager.ubuntu_pro_client.get_ubuntu_pro_client_updates = backup_get_ubuntu_pro_client_updates + obj.mock_unimport_uaclient_update_module() + + def test_get_other_updates_success(self): + obj = MockUpdatesResult() + obj.mock_import_uaclient_update_module('updates', 'mock_update_list_with_all_update_types') + package_manager = self.container.get('package_manager') + + query_success, updates, versions = package_manager.ubuntu_pro_client.get_other_updates() + + self.assertTrue(query_success) + self.assertEqual(len(updates), 1) + self.assertEqual(len(versions), 1) + + obj.mock_unimport_uaclient_update_module() + + def test_get_other_updates_exception(self): + obj = MockUpdatesResult() + obj.mock_import_uaclient_update_module('updates', 'mock_update_list_with_all_update_types') + package_manager = self.container.get('package_manager') + backup_get_ubuntu_pro_client_updates = package_manager.ubuntu_pro_client.get_ubuntu_pro_client_updates + package_manager.ubuntu_pro_client.get_ubuntu_pro_client_updates = self.mock_get_ubuntu_pro_client_updates_raise_exception + + query_success, updates, versions = package_manager.ubuntu_pro_client.get_other_updates() + self.assertFalse(query_success) + self.assertEqual(len(updates), 0) + self.assertEqual(len(versions), 0) + + package_manager.ubuntu_pro_client.get_ubuntu_pro_client_updates = backup_get_ubuntu_pro_client_updates + obj.mock_unimport_uaclient_update_module() if __name__ == '__main__': unittest.main() diff --git a/src/core/tests/library/LegacyEnvLayerExtensions.py b/src/core/tests/library/LegacyEnvLayerExtensions.py index 4eea7757..667077f5 100644 --- a/src/core/tests/library/LegacyEnvLayerExtensions.py +++ b/src/core/tests/library/LegacyEnvLayerExtensions.py @@ -532,6 +532,9 @@ def run_command_output(self, cmd, no_output=False, chk_err=True): output = "tmp file created" elif cmd.find('sudo apt-get install ubuntu-advantage-tools -y') > -1: code = 0 + elif cmd.find('pro security-status --format=json') > -1: + code = 0 + output = "{\"summary\":{\"ua\":{\"attached\":true}}}" elif self.legacy_test_type == 'SadPath': if cmd.find("cat /proc/cpuinfo | grep name") > -1: code = 0 @@ -542,6 +545,9 @@ def run_command_output(self, cmd, no_output=False, chk_err=True): elif self.legacy_package_manager_name is Constants.APT: if cmd.find('sudo apt-get install ubuntu-advantage-tools -y') > -1: code = 1 + elif cmd.find('pro security-status --format=json') > -1: + code = 0 + output = "{\"summary\":{\"ua\":{\"attached\":false}}}" elif self.legacy_package_manager_name is Constants.YUM: if cmd.find("microcode_ctl") > -1: code = 1 @@ -1072,8 +1078,34 @@ def run_command_output(self, cmd, no_output=False, chk_err=True): if self.legacy_package_manager_name is Constants.APT: if cmd.find("dist-upgrade") > -1: code = 0 - output = "Inst git-man [1:2.17.1-1ubuntu0.15] (UA_ESM_Required Ubuntu:18.04/bionic-updates, " \ - "Ubuntu:18.04/bionic-security [all])" + output = "Inst cups [1:2.17.1-1ubuntu0.15] (UA_ESM_Required Ubuntu:18.04/bionic-updates, " \ + "Ubuntu:18.04/bionic-updates [all])" + elif cmd.find("sudo dpkg -s python3") > -1: + code = 0 + output = "Package: python3\n" + \ + "Status: install ok installed\n" + \ + "Priority: optional\n" + \ + "Section: database\n" + \ + "Installed-Size: 107\n" + \ + "Maintainer: Ubuntu Developers \n" + \ + "Architecture: all\n" + \ + "Source: python3\n" + \ + "Version: 1:2.17.1-1ubuntu0.16\n" + \ + "Description: " + \ + "Homepage: http://dev.python3.com/\n" + elif cmd.find("sudo dpkg -s apt") > -1: + code = 0 + output = "Package: apt\n" + \ + "Status: install ok installed\n" + \ + "Priority: optional\n" + \ + "Section: database\n" + \ + "Installed-Size: 107\n" + \ + "Maintainer: Ubuntu Developers \n" + \ + "Architecture: all\n" + \ + "Source: apt\n" + \ + "Version: 2.06-2ubuntu14.1\n" + \ + "Description: " + \ + "Homepage: http://dev.apt.com/\n" elif self.legacy_test_type == 'ArchDependency': if self.legacy_package_manager_name is Constants.YUM: if cmd.find("check-update") > -1: