Skip to content
Merged
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,7 @@ dmypy.json

# Pyre type checker
.pyre/

# JetBrains Rider
.idea/
*.sln.iml
Empty file added src/extension/__init__.py
Empty file.
76 changes: 76 additions & 0 deletions src/extension/src/ActionHandler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import datetime
from src.Constants import Constants
from src.EnableCommandHandler import EnableCommandHandler
from src.InstallCommandHandler import InstallCommandHandler


class ActionHandler(object):
"""Responsible for identifying the action to perform based on the user input"""
def __init__(self, logger, utility, runtime_context_handler, json_file_handler, ext_env_handler, ext_config_settings_handler, core_state_handler, ext_state_handler, ext_output_status_handler, process_handler, cmd_exec_start_time, seq_no):
self.logger = logger
self.utility = utility
self.runtime_context_handler = runtime_context_handler
self.json_file_handler = json_file_handler
self.ext_env_handler = ext_env_handler
self.ext_config_settings_handler = ext_config_settings_handler
self.core_state_handler = core_state_handler
self.ext_state_handler = ext_state_handler
self.ext_output_status_handler = ext_output_status_handler
self.process_handler = process_handler
self.cmd_exec_start_time = cmd_exec_start_time
self.seq_no = seq_no

def determine_operation(self, command):
switcher = {
"-install": self.install,
"-uninstall": self.uninstall,
"-disable": self.disable,
"-enable": self.enable,
"-update": self.update,
"-reset": self.reset
}
try:
return switcher[command]()
except KeyError as e:
raise e

def install(self):
self.logger.log("Extension installation started")
install_command_handler = InstallCommandHandler(self.logger, self.ext_env_handler)
return install_command_handler.execute_handler_action()

def update(self):
""" as per the extension user guide, upon update request, Azure agent calls
1. disable on the prev version
2. update on the new version
3. uninstall on the prev version
4. install (if updateMode is UpdateWithInstall)
5. enable on the new version
on uninstall the agent deletes removes configuration files"""
# todo: in the test run verify if CoreState.json, ExtState.json and the .status files are deleted, if yes, move them to a separate location
self.logger.log("Extension updated")
return Constants.ExitCode.Okay

def uninstall(self):
# ToDo: verify if the agent deletes config files. And find out from the extension/agent team if we need to delete older logs
self.logger.log("Extension uninstalled")
return Constants.ExitCode.Okay

def enable(self):
self.logger.log("Enable triggered on extension")
enable_command_handler = EnableCommandHandler(self.logger, self.utility, self.runtime_context_handler, self.ext_env_handler, self.ext_config_settings_handler, self.core_state_handler, self.ext_state_handler, self.ext_output_status_handler, self.process_handler, self.cmd_exec_start_time, self.seq_no)
return enable_command_handler.execute_handler_action()

def disable(self):
self.logger.log("Disable triggered on extension")
prev_patch_max_end_time = self.cmd_exec_start_time + datetime.timedelta(hours=0, minutes=Constants.DISABLE_MAX_RUNTIME)
self.runtime_context_handler.process_previous_patch_operation(self.core_state_handler, self.process_handler, prev_patch_max_end_time, core_state_content=None)
self.logger.log("Extension disabled successfully")
return Constants.ExitCode.Okay

def reset(self):
#ToDo: do we have to delete log and status files? and raise error if delete fails?
self.logger.log("Reset triggered on extension, deleting CoreState and ExtState files")
self.utility.delete_file(self.core_state_handler.dir_path, self.core_state_handler.file, raise_if_not_found=False)
self.utility.delete_file(self.ext_state_handler.dir_path, self.ext_state_handler.file, raise_if_not_found=False)
return Constants.ExitCode.Okay
132 changes: 132 additions & 0 deletions src/extension/src/Constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import os


class Constants(object):
"""Static class contains all constant variables"""

class EnumBackport(object):
class __metaclass__(type):
def __iter__(self):
for item in self.__dict__:
if item == self.__dict__[item]:
yield item

# Runtime environments
TEST = 'Test'
DEV = 'Dev'
PROD = 'Prod' # Azure Native Patch Management
UNKNOWN_ENV = 'Unknown' # Non-functional code placeholder prior to compile

# File Constants
HANDLER_ENVIRONMENT_FILE = 'HandlerEnvironment.json'
HANDLER_MANIFEST_FILE = 'HandlerManifest.json'
CORE_STATE_FILE = 'CoreState.json'
EXT_STATE_FILE = 'ExtState.json'
HANDLER_ENVIRONMENT_FILE_PATH = os.getcwd()
CONFIG_SETTINGS_FILE_EXTENSION = '.settings'
STATUS_FILE_EXTENSION = '.status'
CORE_CODE_FILE_NAME = 'MsftLinuxPatchCore.py'
LOG_FILE_EXTENSION = '.log'
LOG_FILES_TO_RETAIN = 10
MAX_LOG_FILES_ALLOWED = 40

# Environment variables
SEQ_NO_ENVIRONMENT_VAR = "ConfigSequenceNumber"

# Max runtime for specific commands in minutes
ENABLE_MAX_RUNTIME = 3
DISABLE_MAX_RUNTIME = 13

# Todo: will be implemented later
# Telemetry Categories
TelemetryExtState = "State"
TelemetryConfig = "Config"
TelemetryError = "Error"
TelemetryWarning = "Warning"
TelemetryInfo = "Info"
TelemetryDebug = "Debug"

# Re-try limit for file operations
MAX_IO_RETRIES = 5

# Operations
NOOPERATION = "NoOperation"
PATCH_NOOPERATION_SUMMARY = "PatchNoOperationSummary"

# HandlerEnvironment constants
class EnvSettingsFields(EnumBackport):
version = "version"
settings_parent_key = "handlerEnvironment"
log_folder = "logFolder"
config_folder = "configFolder"
status_folder = "statusFolder"

# Config Settings json keys
RUNTIME_SETTINGS = "runtimeSettings"
HANDLER_SETTINGS = "handlerSettings"
PUBLIC_SETTINGS = "publicSettings"

# Public Settings within Config Settings
class ConfigPublicSettingsFields(EnumBackport):
operation = "operation"
activity_id = "activityId"
start_time = "startTime"
maximum_duration = "maximumDuration"
reboot_setting = "rebootSetting"
include_classifications = "classificationsToInclude"
include_patches = "patchesToInclude"
exclude_patches = "patchesToExclude"
internal_settings = "internalSettings"

# ExtState.json keys
class ExtStateFields(EnumBackport):
ext_seq = "extensionSequence"
ext_seq_number = "number"
ext_seq_achieve_enable_by = "achieveEnableBy"
ext_seq_operation = "operation"

# <SequenceNumber>.status keys
class StatusFileFields(EnumBackport):
version = "version"
timestamp_utc = "timestampUTC"
status = "status"
status_name = "name"
status_operation = "operation"
status_status = "status"
status_code = "code"
status_formatted_message = "formattedMessage"
status_formatted_message_lang = "lang"
status_formatted_message_message = "message"
status_substatus = "substatus"

# CoreState.json keys
class CoreStateFields(EnumBackport):
parent_key = "coreSequence"
number = "number"
action = "action"
completed = "completed"
last_heartbeat = "lastHeartbeat"
process_ids = "processIds"

# Status values
class Status(EnumBackport):
Transitioning = "Transitioning"
Error = "Error"
Success = "Success"
Warning = "Warning"

class ExitCode(EnumBackport):
Okay = 0
HandlerFailed = -1
MissingConfig = -2
BadConfig = -3
UnsupportedOperatingSystem = 51
MissingDependency = 52
ConfigurationError = 53
BadHandlerEnvironmentFile = 3560
UnableToReadStatusFile = 3561
CreateFileLoggerFailure = 3562
ReadingAndDeserializingConfigFileFailure = 3563
InvalidConfigSettingPropertyValue = 3564
CreateLoggerFailure = 3565
CreateStatusWriterFailure = 3566
104 changes: 104 additions & 0 deletions src/extension/src/EnableCommandHandler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import datetime
from src.Constants import Constants


class EnableCommandHandler(object):
""" Responsible for executing the action for enable command """
def __init__(self, logger, utility, runtime_context_handler, ext_env_handler, ext_config_settings_handler, core_state_handler, ext_state_handler, ext_output_status_handler, process_handler, cmd_exec_start_time, seq_no):
self.logger = logger
self.utility = utility
self.runtime_context_handler = runtime_context_handler
self.ext_env_handler = ext_env_handler
self.ext_config_settings_handler = ext_config_settings_handler
self.core_state_handler = core_state_handler
self.ext_state_handler = ext_state_handler
self.ext_output_status_handler = ext_output_status_handler
self.process_handler = process_handler
self.cmd_exec_start_time = cmd_exec_start_time
self.seq_no = seq_no
self.config_public_settings = Constants.ConfigPublicSettingsFields
self.core_state_fields = Constants.CoreStateFields
self.status = Constants.Status

def execute_handler_action(self):
""" Responsible for taking appropriate action for enable command as per the request sent in Handler Configuration file by user """
try:
config_settings = self.ext_config_settings_handler.read_file(self.seq_no)
prev_patch_max_end_time = self.cmd_exec_start_time + datetime.timedelta(hours=0, minutes=Constants.ENABLE_MAX_RUNTIME)
self.ext_state_handler.create_file(self.seq_no, config_settings.__getattribute__(self.config_public_settings.operation), prev_patch_max_end_time)
core_state_content = self.core_state_handler.read_file()

# if NoOperation is requested, terminate all running processes from previous operation and update status file
if config_settings.__getattribute__(self.config_public_settings.operation) == Constants.NOOPERATION:
self.logger.log("NoOperation requested. Terminating older patch operation, if still in progress.")
self.process_nooperation(config_settings, core_state_content)
else:
# if any of the other operations are requested, verify if request is a new request or a re-enable, by comparing sequence number from the prev request and current one
if core_state_content is None or core_state_content.__getattribute__(self.core_state_fields.number) is None:
# first patch request for the VM
self.logger.log("No state information was found for any previous patch operation. Launching a new patch operation.")
self.launch_new_process(config_settings, create_status_output_file=True)
else:
if int(core_state_content.__getattribute__(self.core_state_fields.number)) != int(self.seq_no):
# new request
self.process_enable_request(config_settings, prev_patch_max_end_time, core_state_content)
else:
# re-enable request
self.process_reenable_request(config_settings, core_state_content)
except Exception as error:
self.logger.log_error("Failed to execute enable. [Exception={0}]".format(repr(error)))
raise

def process_enable_request(self, config_settings, prev_patch_max_end_time, core_state_content):
""" Called when the current request is different from the one before. Identifies and waits for the previous request action to complete, if required before addressing the current request """
self.logger.log("Terminating older patch operation, if still in progress, as per it's completion duration and triggering the new requested patch opertaion.")
self.runtime_context_handler.process_previous_patch_operation(self.core_state_handler, self.process_handler, prev_patch_max_end_time, core_state_content)
self.utility.delete_file(self.core_state_handler.dir_path, self.core_state_handler.file)
self.launch_new_process(config_settings, create_status_output_file=True)

def process_reenable_request(self, config_settings, core_state_content):
""" Called when the current request has the same config as the one before it. Restarts the operation if the previous request has errors, no action otherwise """
self.logger.log("This is the same request as the previous patch operation. Checking previous request's status")
if core_state_content.__getattribute__(self.core_state_fields.completed).lower() == 'false':
running_process_ids = self.process_handler.identify_running_processes(core_state_content.__getattribute__(self.core_state_fields.process_ids))
if len(running_process_ids) == 0:
self.logger.log("Re-triggering the patch operation as the previous patch operation was not running and hadn't marked completion either.")
self.utility.delete_file(self.core_state_handler.dir_path, self.core_state_handler.file)
self.launch_new_process(config_settings, create_status_output_file=False)
else:
self.logger.log("Patch operation is in progress from the previous request. [Operation={0}]".format(config_settings.__getattribute__(self.config_public_settings.operation)))
exit(Constants.ExitCode.Okay)

else:
self.logger.log("Patch operation already completed in the previous request. [Operation={0}]".format(config_settings.__getattribute__(self.config_public_settings.operation)))
exit(Constants.ExitCode.Okay)

def launch_new_process(self, config_settings, create_status_output_file):
""" Creates <sequence number>.status to report the current request's status and launches core code to handle the requested operation """
# create Status file
if create_status_output_file:
self.ext_output_status_handler.write_status_file(self.seq_no, self.ext_env_handler.status_folder, config_settings.__getattribute__(self.config_public_settings.operation), substatus_json=[], status=self.status.Transitioning.lower())
else:
self.ext_output_status_handler.update_file(self.seq_no, self.ext_env_handler.status_folder)
# launch core code in a process and exit extension handler
process = self.process_handler.start_daemon(self.seq_no, config_settings, self.ext_env_handler)
self.logger.log("exiting extension handler")
exit(Constants.ExitCode.Okay)

def process_nooperation(self, config_settings, core_state_content):
activity_id = config_settings.__getattribute__(self.config_public_settings.activity_id)
operation = config_settings.__getattribute__(self.config_public_settings.operation)
start_time = config_settings.__getattribute__(self.config_public_settings.start_time)
try:
self.ext_output_status_handler.set_nooperation_substatus_json(self.seq_no, self.ext_env_handler.status_folder, operation, activity_id, start_time, status=Constants.Status.Transitioning)
self.runtime_context_handler.terminate_processes_from_previous_operation(self.process_handler, core_state_content)
self.utility.delete_file(self.core_state_handler.dir_path, self.core_state_handler.file, raise_if_not_found=False)
# ToDo: log prev activity id later
self.ext_output_status_handler.set_nooperation_substatus_json(self.seq_no, self.ext_env_handler.status_folder, operation, activity_id, start_time, status=Constants.Status.Success)
self.logger.log("exiting extension handler")
exit(Constants.ExitCode.Okay)
except Exception as error:
self.logger.log("Error executing NoOperation.")
self.ext_output_status_handler.set_nooperation_substatus_json(self.seq_no, self.ext_env_handler.status_folder, operation, activity_id, start_time, status=Constants.Status.Error)


16 changes: 16 additions & 0 deletions src/extension/src/HandlerManifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[
{
"version": 1.0,
"handlerManifest": {
"disableCommand": "MsftLinuxPatchExtShim.sh -d",
"enableCommand": "MsftLinuxPatchExtShim.sh -e",
"installCommand": "MsftLinuxPatchExtShim.sh -i",
"uninstallCommand": "MsftLinuxPatchExtShim.sh -u",
"updateCommand": "MsftLinuxPatchExtShim.sh -p",
"resetStateCommand": "MsftLinuxPatchExtShim.sh -r",
"rebootAfterInstall": false,
"reportHeartbeat": false,
"updateMode": "UpdateWithoutInstall"
}
}
]
Loading