Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -135,4 +135,4 @@ dmypy.json

# Linux Patch Extension specific
/src/core/tests/scratch/
/src/extension/tests/MsftLinuxPatchAutoAssess.sh
/src/extension/tests/AzGPSLinuxPatchAutoAssess.sh
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
PACKAGER_PATH = src//tools/Package-All.py
PACKAGER_PATH = src//tools//packager//Package-All.py
BUILD_PATH = build
ZIP_SRC_PATH = src//out//LinuxPatchExtension.zip
MANIFEST_PATH = src//extension//src//manifest.xml
Expand Down
2 changes: 2 additions & 0 deletions src/build.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@echo off
python tools\packager\Package-All.py
35 changes: 35 additions & 0 deletions src/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#!/usr/bin/env bash

# Copyright 2025 Microsoft Corporation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Requires Python 2.7+

COMMAND="tools/packager/Package-All.py"

function find_python(){
local python_exec_command=$1

# Check if there is python defined.
for p in python3 /usr/share/oem/python/bin/python3 python python2 /usr/libexec/platform-python /usr/share/oem/python/bin/python; do
if command -v "${p}" ; then
eval ${python_exec_command}=${p}
return
fi
done
}

find_python PYTHON

${PYTHON} "${COMMAND}"
2 changes: 1 addition & 1 deletion src/core/src/bootstrap/Bootstrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ def build_core_components(self, container):
return lifecycle_manager, status_handler

def bootstrap_splash_text(self):
self.composite_logger.log("\n\n[%exec_name%] \t -- \t Copyright (c) Microsoft Corporation. All rights reserved. \nApplication version: 3.0.[%exec_sub_ver%]\n\n")
self.composite_logger.log("\n\n[%exec_name%] \t -- \t Copyright (c) Microsoft Corporation. All rights reserved. \nBuild Date: [%exec_build_date%]\n\n")

def basic_environment_health_check(self):
self.composite_logger.log("Python version: " + " ".join(sys.version.splitlines()))
Expand Down
19 changes: 19 additions & 0 deletions src/core/src/bootstrap/ConfigurationFactory.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,25 @@ def new_prod_configuration(self, package_manager_name, package_manager_component
'component_args': ['env_layer', 'execution_config', 'composite_logger', 'telemetry_writer', 'service_info'],
'component_kwargs': {}
},
'service_info_legacy': {
'component': ServiceInfo,
'component_args': [],
'component_kwargs': {
'service_name': Constants.AUTO_ASSESSMENT_SERVICE_NAME_LEGACY,
'service_desc': Constants.AUTO_ASSESSMENT_SERVICE_DESC,
'service_exec_path': os.path.join(os.path.dirname(os.path.realpath(__file__)), Constants.CORE_AUTO_ASSESS_SH_FILE_NAME_LEGACY)
}
},
'auto_assess_service_manager_legacy': {
'component': ServiceManager,
'component_args': ['env_layer', 'execution_config', 'composite_logger', 'telemetry_writer', 'service_info'],
'component_kwargs': {}
},
'auto_assess_timer_manager_legacy': {
'component': TimerManager,
'component_args': ['env_layer', 'execution_config', 'composite_logger', 'telemetry_writer', 'service_info'],
'component_kwargs': {}
},
'configure_patching_processor': {
'component': ConfigurePatchingProcessor,
'component_args': ['env_layer', 'execution_config', 'composite_logger', 'telemetry_writer', 'status_handler', 'package_manager', 'auto_assess_service_manager', 'auto_assess_timer_manager', 'lifecycle_manager'],
Expand Down
6 changes: 4 additions & 2 deletions src/core/src/bootstrap/Constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,10 @@ class EulaSettings(EnumBackport):
IMAGE_DEFAULT_PATCH_CONFIGURATION_BACKUP_PATH = "ImageDefaultPatchConfiguration.bak"

# Auto assessment shell script name
CORE_AUTO_ASSESS_SH_FILE_NAME = "MsftLinuxPatchAutoAssess.sh"
AUTO_ASSESSMENT_SERVICE_NAME = "MsftLinuxPatchAutoAssess"
CORE_AUTO_ASSESS_SH_FILE_NAME = "AzGPSLinuxPatchAutoAssess.sh"
AUTO_ASSESSMENT_SERVICE_NAME = "AzGPSLinuxPatchAutoAssess"
CORE_AUTO_ASSESS_SH_FILE_NAME_LEGACY = "MsftLinuxPatchAutoAssess.sh"
AUTO_ASSESSMENT_SERVICE_NAME_LEGACY = "MsftLinuxPatchAutoAssess"
AUTO_ASSESSMENT_SERVICE_DESC = "Microsoft Azure Linux Patch Extension - Auto Assessment"

# Operations
Expand Down
55 changes: 44 additions & 11 deletions src/core/src/core_logic/ConfigurePatchingProcessor.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,19 @@
""" Configure Patching """
from core.src.bootstrap.Constants import Constants

from core.src.bootstrap.EnvLayer import EnvLayer
from core.src.core_logic.ExecutionConfig import ExecutionConfig
from core.src.local_loggers.CompositeLogger import CompositeLogger
from core.src.service_interfaces.StatusHandler import StatusHandler
from core.src.package_managers.PackageManager import PackageManager
from core.src.core_logic.ServiceManager import ServiceManager
from core.src.core_logic.TimerManager import TimerManager


class ConfigurePatchingProcessor(object):
def __init__(self, env_layer, execution_config, composite_logger, telemetry_writer, status_handler, package_manager, auto_assess_service_manager, auto_assess_timer_manager, lifecycle_manager):
def __init__(self, env_layer, execution_config, composite_logger, telemetry_writer, status_handler, package_manager, auto_assess_service_manager, auto_assess_timer_manager, lifecycle_manager,
auto_assess_service_manager_legacy=None, auto_assess_timer_manager_legacy=None):
# type: (EnvLayer, ExecutionConfig, CompositeLogger, TelemetryWriter, StatusHandler, PackageManager, ServiceManager, TimerManager, ServiceManager, TimerManager) -> None
self.env_layer = env_layer
self.execution_config = execution_config

Expand All @@ -30,6 +40,8 @@
self.package_manager = package_manager
self.auto_assess_service_manager = auto_assess_service_manager
self.auto_assess_timer_manager = auto_assess_timer_manager
self.auto_assess_service_manager_legacy = auto_assess_service_manager_legacy
self.auto_assess_timer_manager_legacy = auto_assess_timer_manager_legacy
self.lifecycle_manager = lifecycle_manager

self.current_auto_os_patch_state = Constants.AutomaticOSPatchStates.UNKNOWN
Expand All @@ -40,7 +52,7 @@
def start_configure_patching(self):
""" Start configure patching """
try:
self.composite_logger.log("\nStarting configure patching... [MachineId: " + self.env_layer.platform.vm_name() + "][ActivityId: " + self.execution_config.activity_id + "][StartTime: " + self.execution_config.start_time + "]")
self.composite_logger.log("[CPP] Starting configure patching... [MachineId: " + self.env_layer.platform.vm_name() + "][ActivityId: " + self.execution_config.activity_id + "][StartTime: " + self.execution_config.start_time + "]")
self.status_handler.set_current_operation(Constants.CONFIGURE_PATCHING)
self.__raise_if_telemetry_unsupported()

Expand Down Expand Up @@ -73,23 +85,22 @@
def __try_set_patch_mode(self):
""" Set the patch mode for the VM """
try:
self.composite_logger.log_verbose("[CPP] Processing patch mode configuration...")
self.status_handler.set_current_operation(Constants.CONFIGURE_PATCHING)
self.current_auto_os_patch_state = self.package_manager.get_current_auto_os_patch_state()

# disable auto OS updates if VM is configured for platform updates only.
# NOTE: this condition will be false for Assessment operations, since patchMode is not sent in the API request
if self.current_auto_os_patch_state != Constants.AutomaticOSPatchStates.DISABLED and self.execution_config.patch_mode == Constants.PatchModes.AUTOMATIC_BY_PLATFORM:
self.package_manager.disable_auto_os_update()
elif self.current_auto_os_patch_state == Constants.AutomaticOSPatchStates.DISABLED and self.execution_config.patch_mode == Constants.PatchModes.IMAGE_DEFAULT:
self.package_manager.revert_auto_os_update_to_system_default()

self.current_auto_os_patch_state = self.package_manager.get_current_auto_os_patch_state()

if self.execution_config.patch_mode == Constants.PatchModes.AUTOMATIC_BY_PLATFORM and self.current_auto_os_patch_state == Constants.AutomaticOSPatchStates.UNKNOWN:
# NOTE: only sending details in error objects for customer visibility on why patch state is unknown, overall configurepatching status will remain successful
self.configure_patching_exception_error = "Could not disable one or more automatic OS update services. Please check if they are configured correctly"
self.configure_patching_exception_error = "Could not disable one or more automatic OS update services. Please check if they are configured correctly."

self.composite_logger.log_debug("Completed processing patch mode configuration.")
self.composite_logger.log_debug("[CPP] Completed processing patch mode configuration.")
except Exception as error:
self.composite_logger.log_error("Error while processing patch mode configuration. [Error={0}]".format(repr(error)))
self.configure_patching_exception_error = error
Expand All @@ -98,28 +109,35 @@
def __try_set_auto_assessment_mode(self):
""" Sets the preferred auto-assessment mode for the VM """
try:
self.composite_logger.log_verbose("[CPP] Processing assessment mode configuration...")
self.status_handler.set_current_operation(Constants.CONFIGURE_PATCHING_AUTO_ASSESSMENT)
self.composite_logger.log_debug("Systemd information: {0}".format(str(self.auto_assess_service_manager.get_version()))) # proactive support telemetry

if self.execution_config.assessment_mode is None:
self.composite_logger.log_debug("No assessment mode config was present. No configuration changes will occur.")
elif self.execution_config.assessment_mode == Constants.AssessmentModes.AUTOMATIC_BY_PLATFORM:
self.composite_logger.log_debug("Enabling platform-based automatic assessment.")

if not self.auto_assess_service_manager.systemd_exists():
raise Exception("Systemd is not available on this system, and platform-based auto-assessment cannot be configured.")

self.__erase_auto_assess_config_if_any("legacy", self.auto_assess_service_manager_legacy, self.auto_assess_timer_manager_legacy)
self.auto_assess_service_manager.create_and_set_service_idem()
self.auto_assess_timer_manager.create_and_set_timer_idem()

self.current_auto_assessment_state = Constants.AutoAssessmentStates.ENABLED
elif self.execution_config.assessment_mode == Constants.AssessmentModes.IMAGE_DEFAULT:
self.composite_logger.log_debug("Disabling platform-based automatic assessment.")
self.auto_assess_timer_manager.remove_timer()
self.auto_assess_service_manager.remove_service()

self.__erase_auto_assess_config_if_any(Constants.AUTO_ASSESSMENT_SERVICE_NAME, self.auto_assess_service_manager, self.auto_assess_timer_manager)
self.__erase_auto_assess_config_if_any(Constants.AUTO_ASSESSMENT_SERVICE_NAME_LEGACY, self.auto_assess_service_manager_legacy, self.auto_assess_timer_manager_legacy)

Check warning on line 133 in src/core/src/core_logic/ConfigurePatchingProcessor.py

View check run for this annotation

Codecov / codecov/patch

src/core/src/core_logic/ConfigurePatchingProcessor.py#L132-L133

Added lines #L132 - L133 were not covered by tests

self.current_auto_assessment_state = Constants.AutoAssessmentStates.DISABLED
else:
raise Exception("Unknown assessment mode specified. [AssessmentMode={0}]".format(self.execution_config.assessment_mode))

self.__report_consolidated_configure_patch_status()
self.composite_logger.log_debug("Completed processing automatic assessment mode configuration.")
self.composite_logger.log_debug("[CPP] Completed processing automatic assessment mode configuration.")
except Exception as error:
# deliberately not setting self.configure_patching_exception_error here as it does not feed into the parent object. Not a bug, if you're thinking about it.
self.composite_logger.log_error("Error while processing automatic assessment mode configuration. [Error={0}]".format(repr(error)))
Expand All @@ -130,9 +148,24 @@
self.composite_logger.log_debug("Restoring status handler operation to {0}.".format(Constants.CONFIGURE_PATCHING))
self.status_handler.set_current_operation(Constants.CONFIGURE_PATCHING)

def __erase_auto_assess_config_if_any(self, service_name, service_manager, timer_manager):
""" Cleans up the legacy auto-assess service """
try:
if service_manager is not None and not service_manager.service_exists():
self.composite_logger.log_debug("[CPP] Cleaning up the {0} service.".format(service_name))
service_manager.remove_service()

if timer_manager is not None and not timer_manager.timer_exists():
self.composite_logger.log_debug("[CPP] Cleaning up the {0} timer.".format(service_name))
timer_manager.remove_timer()
except Exception as error:
self.composite_logger.log_warning("[CPP] Retriable error while cleaning up auto-assess config. [Error={0}]".format(repr(error)))
self.configure_patching_successful &= False

def __report_consolidated_configure_patch_status(self, status=Constants.STATUS_TRANSITIONING, error=Constants.DEFAULT_UNSPECIFIED_VALUE):
""" Reports """
self.composite_logger.log_debug("Reporting consolidated current configure patch status. [OSPatchState={0}][AssessmentState={1}]".format(self.current_auto_os_patch_state, self.current_auto_assessment_state))
# type: (str, any) -> None
""" Reports the consolidated configure patching status """
self.composite_logger.log_debug("[CPP] Reporting consolidated current configure patch status. [OSPatchState={0}][AssessmentState={1}]".format(self.current_auto_os_patch_state, self.current_auto_assessment_state))

# report error if specified
if error != Constants.DEFAULT_UNSPECIFIED_VALUE:
Expand Down
11 changes: 9 additions & 2 deletions src/core/src/core_logic/ServiceManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,14 @@ def __init__(self, env_layer, execution_config, composite_logger, telemetry_writ
self.service_is_active_cmd = "sudo systemctl is-active {0}.service"

# region - Service Creation / Removal
def service_exists(self):
# type: () -> bool
""" Check if the service exists """
service_path = self.__systemd_service_unit_path.format(self.service_name)
return os.path.exists(service_path)

def remove_service(self):
""" Remove the service if it exists """
service_path = self.__systemd_service_unit_path.format(self.service_name)
if os.path.exists(service_path):
self.stop_service()
Expand All @@ -58,7 +65,7 @@ def start_service(self):
code, out = self.invoke_systemctl(self.service_start_cmd.format(self.service_name), "Starting the service.")
return code == 0

def stop_service(self):
def stop_service(self, service_name=str()):
code, out = self.invoke_systemctl(self.service_stop_cmd.format(self.service_name), "Stopping the service.")
return code == 0

Expand All @@ -74,7 +81,7 @@ def enable_service(self):
code, out = self.invoke_systemctl(self.service_enable_cmd.format(self.service_name), "Enabling the service.")
return code == 0

def disable_service(self):
def disable_service(self, service_name=str()):
code, out = self.invoke_systemctl(self.service_disable_cmd.format(self.service_name), "Disabling the service.")
return code == 0

Expand Down
6 changes: 6 additions & 0 deletions src/core/src/core_logic/TimerManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ def __init__(self, env_layer, execution_config, composite_logger, telemetry_writ
self.timer_is_active_cmd = "sudo systemctl is-active {0}.timer"

# region - Time Creation / Removal
def timer_exists(self):
# type: () -> bool
""" Check if the timer exists """
timer_path = self.__systemd_timer_unit_path.format(self.service_name)
return os.path.exists(timer_path)

def remove_timer(self):
timer_path = self.__systemd_timer_unit_path.format(self.service_name)
if os.path.exists(timer_path):
Expand Down
Loading
Loading