Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
e8486a9
AzureLinux: Strict SDP support
rane-rajasi Apr 30, 2025
ecd7409
Merge branch 'master' into rarane/azlinux/strict_sdp
rane-rajasi Apr 30, 2025
a592072
AzureLinux Strict SDP support: Addressing PR feedback #1
rane-rajasi Apr 30, 2025
77f92d3
AzureLinux Strict SDP support: Minor changes
rane-rajasi May 1, 2025
bc0f961
Merge branch 'master' into rarane/azlinux/strict_sdp
rane-rajasi May 12, 2025
5740839
AzureLinux Strict SDP support: intermediary commit explaining approach
rane-rajasi May 13, 2025
f27c3e3
AzureLinux Strict SDP support: Added pseudo code to discuss approach
rane-rajasi Jun 19, 2025
f9edc65
AzureLinux Strict SDP support: Resolving merge conflicts
rane-rajasi Jun 19, 2025
4b75049
AzureLinux Strict SDP support: Modifying strict sdp logic to follow
rane-rajasi Jul 10, 2025
528bb89
AzureLinux Strict SDP support: Addressing PR comments #2
rane-rajasi Jul 10, 2025
af021ce
Merge branch 'master' into rarane/azlinux/strict_sdp
rane-rajasi Jul 10, 2025
0d40748
AzureLinux Strict SDP support: Adding minimum requirements check
rane-rajasi Jul 23, 2025
c078f90
AzureLinux Strict SDP support: Addressing PR comments #3
rane-rajasi Jul 23, 2025
b0de63b
AzureLinux Strict SDP support: Adding UTs
rane-rajasi Jul 24, 2025
813350e
AzureLinux Strict SDP support: Fixing posix time computation to alway…
rane-rajasi Jul 24, 2025
103f34b
AzureLinux Strict SDP support: Addressing PR comments #4
rane-rajasi Jul 31, 2025
3f56dc0
AzureLinux Strict SDP support: Addressing PR comments #5
rane-rajasi Aug 22, 2025
91b6a84
Merge branch 'master' into rarane/azlinux/strict_sdp
rane-rajasi Aug 22, 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
25 changes: 25 additions & 0 deletions src/core/src/bootstrap/EnvLayer.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
# Requires Python 2.7+

from __future__ import print_function

import datetime
import glob
import os
Expand Down Expand Up @@ -46,6 +47,15 @@ def __init__(self):
def is_distro_azure_linux(distro_name):
return any(x in distro_name for x in Constants.AZURE_LINUX)

def is_distro_azure_linux_3_or_beyond(self):
# type: () -> bool
""" Checks if the current distro is Azure Linux 3 """
if self.is_distro_azure_linux(self.platform.linux_distribution()):
version = distro.os_release_attr('version')
major = version.split('.')[0] if version else None
return major is not None and int(major) >= 3
return False

def get_package_manager(self):
# type: () -> str
""" Detects package manager type """
Expand Down Expand Up @@ -242,6 +252,7 @@ def cpu_arch(): # architecture
@staticmethod
def vm_name(): # machine name
return platform.node()

# endregion - Platform extensions

# region - File system extensions
Expand Down Expand Up @@ -396,4 +407,18 @@ def utc_to_standard_datetime(utc_datetime):
def standard_datetime_to_utc(std_datetime):
""" Converts datetime object to string of format '"%Y-%m-%dT%H:%M:%SZ"' """
return std_datetime.strftime("%Y-%m-%dT%H:%M:%SZ")

@staticmethod
def datetime_string_to_posix_time(datetime_string, format_string):
# type: (str, str) -> int
""" Converts string of given format to posix datetime string. """
# eg: Input: datetime_string: 20241220T000000Z (str), format_string: '%Y%m%dT%H%M%SZ' -> Output: 1734681600 (str)

# Parse the datetime string
dt = datetime.datetime.strptime(datetime_string, format_string)

# Convert to POSIX time assuming UTC
epoch = datetime.datetime(1970, 1, 1)
return int((dt - epoch).total_seconds())

# endregion - DateTime emulator and extensions
2 changes: 1 addition & 1 deletion src/core/src/core_logic/PatchInstaller.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ def start_installation(self, simulate=False):
if self.execution_config.max_patch_publish_date != str():
self.package_manager.set_max_patch_publish_date(self.execution_config.max_patch_publish_date)

if self.package_manager.max_patch_publish_date != str():
if self.package_manager.max_patch_publish_date != str() and self.package_manager.try_meet_azgps_coordinated_requirements():
""" Strict SDP with the package manager that supports it """
installed_update_count, update_run_successful, maintenance_window_exceeded = self.install_updates_azgps_coordinated(maintenance_window, package_manager, simulate)
package_manager.set_package_manager_setting(Constants.PACKAGE_MGR_SETTING_REPEAT_PATCH_OPERATION, bool(not update_run_successful))
Expand Down
4 changes: 4 additions & 0 deletions src/core/src/package_managers/AptitudePackageManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,10 @@ def install_security_updates_azgps_coordinated(self):
command = self.__generate_command_with_custom_sources(self.install_security_updates_azgps_coordinated_cmd, source_parts=source_parts, source_list=source_list)
out, code = self.invoke_package_manager_advanced(command, raise_on_exception=False)
return code, out

def try_meet_azgps_coordinated_requirements(self):
# type: () -> bool
return True
# endregion

# region Package Information
Expand Down
6 changes: 6 additions & 0 deletions src/core/src/package_managers/PackageManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,12 @@ def install_update_and_dependencies_and_get_status(self, package_and_dependencie
@abstractmethod
def install_security_updates_azgps_coordinated(self):
pass

@abstractmethod
def try_meet_azgps_coordinated_requirements(self):
# type: () -> bool
""" Returns true if the package manager meets the requirements for azgps coordinated security updates """
return False
# endregion

# region Package Information
Expand Down
131 changes: 109 additions & 22 deletions src/core/src/package_managers/TdnfPackageManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,14 @@ def __init__(self, env_layer, execution_config, composite_logger, telemetry_writ
self.cmd_repo_refresh = "sudo tdnf -q list updates"

# Support to get updates and their dependencies
self.tdnf_check = 'sudo tdnf -q list updates'
self.single_package_check_versions = 'sudo tdnf list available <PACKAGE-NAME>'
self.single_package_check_installed = 'sudo tdnf list installed <PACKAGE-NAME>'
self.tdnf_check = 'sudo tdnf -q list updates <SNAPSHOTTIME>'
self.single_package_check_versions = 'sudo tdnf list available <PACKAGE-NAME> '
self.single_package_check_installed = 'sudo tdnf list installed <PACKAGE-NAME> '
self.single_package_upgrade_simulation_cmd = 'sudo tdnf install --assumeno --skip-broken '

# Install update
self.single_package_upgrade_cmd = 'sudo tdnf -y install --skip-broken '
self.install_security_updates_azgps_coordinated_cmd = 'sudo tdnf -y upgrade --skip-broken <SNAPSHOTTIME>'

# Package manager exit code(s)
self.tdnf_exitcode_ok = 0
Expand Down Expand Up @@ -70,12 +71,15 @@ def __init__(self, env_layer, execution_config, composite_logger, telemetry_writ
# commands for DNF Automatic updates service
self.__init_constants_for_dnf_automatic()

# AzLinux3 Package Manager.
self.azl3_tdnf_packagemanager = self.AzL3TdnfPackageManager()

# Miscellaneous
self.set_package_manager_setting(Constants.PKG_MGR_SETTING_IDENTITY, Constants.TDNF)
self.STR_TOTAL_DOWNLOAD_SIZE = "Total download size: "
self.version_comparator = VersionComparator()

# if an Auto Patching request comes in on a Azure Linux machine with Security and/or Critical classifications selected, we need to install all patches, since classifications aren't available in Azure Linux repository
# if an Auto Patching request comes in on an Azure Linux machine with Security and/or Critical classifications selected, we need to install all patches, since classifications aren't available in Azure Linux repository
installation_included_classifications = [] if execution_config.included_classifications_list is None else execution_config.included_classifications_list
if execution_config.health_store_id is not str() and execution_config.operation.lower() == Constants.INSTALLATION.lower() \
and (env_layer.is_distro_azure_linux(str(env_layer.platform.linux_distribution()))) \
Expand All @@ -90,6 +94,16 @@ def refresh_repo(self):
self.invoke_package_manager(self.cmd_clean_cache)
self.invoke_package_manager(self.cmd_repo_refresh)

# region Strict SDP using SnapshotTime
@staticmethod
def __generate_command_with_snapshotposixtime_if_specified(command_template, snapshot_posix_time=str()):
# type: (str, str) -> str
if snapshot_posix_time == str():
return command_template.replace('<SNAPSHOTTIME>', str())
else:
return command_template.replace('<SNAPSHOTTIME>', ('--snapshottime={0}'.format(str(snapshot_posix_time))))
# endregion

# region Get Available Updates
def invoke_package_manager_advanced(self, command, raise_on_exception=True):
"""Get missing updates using the command input"""
Expand Down Expand Up @@ -117,32 +131,28 @@ def get_all_updates(self, cached=False):
self.composite_logger.log_debug("[TDNF] Get all updates : [Cached={0}][PackagesCount={1}]]".format(str(cached), len(self.all_updates_cached)))
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

out = self.invoke_package_manager(self.tdnf_check)
out = self.invoke_package_manager(self.__generate_command_with_snapshotposixtime_if_specified(self.tdnf_check, self.max_patch_publish_date))
self.all_updates_cached, self.all_update_versions_cached = self.extract_packages_and_versions(out)

self.composite_logger.log_debug("[TDNF] Get all updates : [Cached={0}][PackagesCount={1}]]".format(str(False), len(self.all_updates_cached)))
return self.all_updates_cached, self.all_update_versions_cached

def get_security_updates(self):
"""Get missing security updates. NOTE: Classification based categorization of patches is not available in Azure Linux as of now"""
self.composite_logger.log_verbose("[TDNF] Discovering 'security' packages...")
security_packages, security_package_versions = [], []
"""Get missing security updates. NOTE: Classification based categorization of patches is not available in TDNF as of now"""
self.composite_logger.log_verbose("[TDNF] Discovering all packages as 'security' packages, since TDNF does not support package classification...")
security_packages, security_package_versions = self.get_all_updates(cached=False)
self.composite_logger.log_debug("[TDNF] Discovered 'security' packages. [Count={0}]".format(len(security_packages)))
return security_packages, security_package_versions

def get_other_updates(self):
"""Get missing other updates.
NOTE: This function will return all available packages since Azure Linux does not support package classification in it's repository"""
"""Get missing other updates."""
self.composite_logger.log_verbose("[TDNF] Discovering 'other' packages...")
other_packages, other_package_versions = [], []

all_packages, all_package_versions = self.get_all_updates(True)

self.composite_logger.log_debug("[TDNF] Discovered 'other' packages. [Count={0}]".format(len(other_packages)))
return all_packages, all_package_versions
return [], []

def set_max_patch_publish_date(self, max_patch_publish_date=str()):
pass
"""Set the max patch publish date in POSIX time for strict SDP"""
self.composite_logger.log_debug("[TDNF] Setting max patch publish date. [MaxPatchPublishDate={0}]".format(str(max_patch_publish_date)))
self.max_patch_publish_date = str(self.env_layer.datetime.datetime_string_to_posix_time(max_patch_publish_date, '%Y%m%dT%H%M%SZ')) if max_patch_publish_date != str() else max_patch_publish_date
self.composite_logger.log_debug("[TDNF] Set max patch publish date. [MaxPatchPublishDatePosixTime={0}]".format(str(self.max_patch_publish_date)))
# endregion

# region Output Parser(s)
Expand Down Expand Up @@ -215,7 +225,78 @@ def install_updates_fail_safe(self, excluded_packages):
return

def install_security_updates_azgps_coordinated(self):
pass
"""Install security updates in Azure Linux following strict SDP"""
command = self.__generate_command_with_snapshotposixtime_if_specified(self.install_security_updates_azgps_coordinated_cmd, self.max_patch_publish_date)
out, code = self.invoke_package_manager_advanced(command, raise_on_exception=False)
return code, out

def try_meet_azgps_coordinated_requirements(self):
# type: () -> bool
""" Check if the system meets the requirements for Azure Linux strict safe deployment and attempt to update TDNF if necessary """
self.composite_logger.log_debug("[TDNF] Checking if system meets Azure Linux security updates requirements...")
# Check if the system is Azure Linux 3.0 or beyond
if not self.env_layer.is_distro_azure_linux_3_or_beyond():
self.composite_logger.log_error("[TDNF] The system does not meet minimum Azure Linux requirement of 3.0 or above for strict safe deployment. Defaulting to regular upgrades.")
self.set_max_patch_publish_date() # fall-back
return False
else:
if self.is_minimum_tdnf_version_for_strict_sdp_installed():
self.composite_logger.log_debug("[TDNF] Minimum tdnf version for strict safe deployment is installed.")
return True
else:
if not self.try_tdnf_update_to_meet_strict_sdp_requirements():
error_msg = "Failed to meet minimum TDNF version requirement for strict safe deployment. Defaulting to regular upgrades."
self.composite_logger.log_error(error_msg + "[Error={0}]".format(repr(error_msg)))
self.status_handler.add_error_to_status(error_msg)
self.set_max_patch_publish_date() # fall-back
return False
return True

def is_minimum_tdnf_version_for_strict_sdp_installed(self):
# type: () -> bool
"""Check if at least the minimum required version of TDNF is installed"""
self.composite_logger.log_debug("[TDNF] Checking if minimum TDNF version required for strict safe deployment is installed...")
tdnf_version = self.get_tdnf_version()
minimum_tdnf_version_for_strict_sdp = self.azl3_tdnf_packagemanager.TDNF_MINIMUM_VERSION_FOR_STRICT_SDP
distro_from_minimum_tdnf_version_for_strict_sdp = re.match(r".*-\d+\.([a-zA-Z0-9]+)$", minimum_tdnf_version_for_strict_sdp).group(1)
if tdnf_version is None:
self.composite_logger.log_error("[TDNF] Failed to get TDNF version. Cannot proceed with strict safe deployment. Defaulting to regular upgrades.")
return False
elif re.match(r".*-\d+\.([a-zA-Z0-9]+)$", tdnf_version).group(1) != distro_from_minimum_tdnf_version_for_strict_sdp:
self.composite_logger.log_warning("[TDNF] TDNF version installed is not from the same Azure Linux distribution as the minimum required version for strict SDP. [InstalledVersion={0}][MinimumRequiredVersion={1}]".format(tdnf_version, self.azl3_tdnf_packagemanager.TDNF_MINIMUM_VERSION_FOR_STRICT_SDP))
return False
elif not self.version_comparator.compare_versions(tdnf_version, minimum_tdnf_version_for_strict_sdp) >= 0:
self.composite_logger.log_warning("[TDNF] TDNF version installed is less than the minimum required version for strict SDP. [InstalledVersion={0}][MinimumRequiredVersion={1}]".format(tdnf_version, self.azl3_tdnf_packagemanager.TDNF_MINIMUM_VERSION_FOR_STRICT_SDP))
return False
return True

def get_tdnf_version(self):
# type: () -> any
"""Get the version of TDNF installed on the system"""
self.composite_logger.log_debug("[TDNF] Getting tdnf version...")
cmd = "rpm -q tdnf | sed -E 's/^tdnf-([0-9]+\\.[0-9]+\\.[0-9]+-[0-9]+\\.[a-zA-Z0-9]+).*/\\1/'"
code, output = self.env_layer.run_command_output(cmd, False, False)
if code == 0:
# Sample output: 3.5.8-3-azl3
version = output.split()[0] if output else None
self.composite_logger.log_debug("[TDNF] TDNF version detected. [Version={0}]".format(version))
return version
else:
self.composite_logger.log_error("[TDNF] Failed to get TDNF version. [Command={0}][Code={1}][Output={2}]".format(cmd, code, output))
return None

def try_tdnf_update_to_meet_strict_sdp_requirements(self):
# type: () -> bool
"""Attempt to update TDNF to meet the minimum version required for strict SDP"""
self.composite_logger.log_debug("[TDNF] Attempting to update TDNF to meet strict safe deployment requirements...")
cmd = "sudo tdnf -y install tdnf-" + self.azl3_tdnf_packagemanager.TDNF_MINIMUM_VERSION_FOR_STRICT_SDP
code, output = self.env_layer.run_command_output(cmd, no_output=True, chk_err=False)
if code == 0:
self.composite_logger.log_debug("[TDNF] Successfully updated TDNF for Strict SDP. [Command={0}][Code={1}]".format(cmd, code))
return True
else:
self.composite_logger.log_error("[TDNF] Failed to update TDNF for Strict SDP. [Command={0}][Code={1}][Output={2}]".format(cmd, code, output))
return False
# endregion

# region Package Information
Expand Down Expand Up @@ -325,10 +406,10 @@ def get_dependent_list(self, packages):
package_names += ' '
package_names += package

self.composite_logger.log_verbose("[TDNF] Resolving dependencies. [Command={0}]".format(str(self.single_package_upgrade_simulation_cmd + package_names)))
output = self.invoke_package_manager(self.single_package_upgrade_simulation_cmd + package_names)
cmd = self.single_package_upgrade_simulation_cmd + package_names
output = self.invoke_package_manager(cmd)
dependencies = self.extract_dependencies(output, packages)
self.composite_logger.log_verbose("[TDNF] Resolved dependencies. [Packages={0}][DependencyCount={1}]".format(str(packages), len(dependencies)))
self.composite_logger.log_verbose("[TDNF] Resolved dependencies. [Command={0}][Packages={1}][DependencyCount={2}]".format(str(cmd),str(packages), len(dependencies)))
return dependencies

def get_product_name(self, package_name):
Expand Down Expand Up @@ -748,3 +829,9 @@ def separate_out_esm_packages(self, packages, package_versions):
def get_package_install_expected_avg_time_in_seconds(self):
return self.package_install_expected_avg_time_in_seconds

# region - AzLinux specializations
class AzL3TdnfPackageManager(object):
"""AzLinux Package Manager class for TDNF package manager."""
def __init__(self):
self.TDNF_MINIMUM_VERSION_FOR_STRICT_SDP = "3.5.8-3.azl3" # minimum version of tdnf required to support Strict SDP in Azure Linux

4 changes: 4 additions & 0 deletions src/core/src/package_managers/YumPackageManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,10 @@ def install_updates_fail_safe(self, excluded_packages):

def install_security_updates_azgps_coordinated(self):
pass

def try_meet_azgps_coordinated_requirements(self):
# type: () -> bool
return False
# endregion

# region Package Information
Expand Down
4 changes: 4 additions & 0 deletions src/core/src/package_managers/ZypperPackageManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,10 @@ def install_updates_fail_safe(self, excluded_packages):

def install_security_updates_azgps_coordinated(self):
pass

def try_meet_azgps_coordinated_requirements(self):
# type: () -> bool
return False
# endregion

# region Package Information
Expand Down
Loading
Loading