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
Empty file added src/core/__init__.py
Empty file.
80 changes: 80 additions & 0 deletions src/core/src/CoreMain.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
from src.bootstrap.Bootstrapper import Bootstrapper
from src.bootstrap.Constants import Constants


class CoreMain(object):
def __init__(self, argv):
"""The main entry point of patch operation execution"""
# Level 1 bootstrapping - bare minimum components to allow for diagnostics in further bootstrapping
bootstrapper = Bootstrapper(argv)
file_logger = bootstrapper.file_logger
composite_logger = bootstrapper.composite_logger
stdout_file_mirror = bootstrapper.stdout_file_mirror
lifecycle_manager = telemetry_writer = status_handler = None

# Init operation statuses
patch_operation_requested = Constants.UNKNOWN
patch_assessment_successful = False
patch_installation_successful = False

try:
# Level 2 bootstrapping
composite_logger.log_debug("Building out full container...")
container = bootstrapper.build_out_container()
lifecycle_manager, telemetry_writer, status_handler = bootstrapper.build_core_components(container)
composite_logger.log_debug("Completed building out full container.\n\n")

# Basic environment check
bootstrapper.bootstrap_splash_text()
bootstrapper.basic_environment_health_check()
lifecycle_manager.execution_start_check() # terminates if this instance shouldn't be running (redundant)

# Execution config retrieval
composite_logger.log_debug("Obtaining execution configuration...")
execution_config = container.get('execution_config')
patch_operation_requested = execution_config.operation.lower()
patch_assessor = container.get('patch_assessor')
patch_installer = container.get('patch_installer')

# Assessment happens no matter what
patch_assessment_successful = patch_assessor.start_assessment()

# Patching + additional assessment occurs if the operation is 'Installation'
if patch_operation_requested == Constants.INSTALLATION.lower():
patch_installation_successful = patch_installer.start_installation()
patch_assessment_successful = patch_assessor.start_assessment()

except Exception as error:
# Privileged operation handling for non-production use
if Constants.EnvLayer.PRIVILEGED_OP_MARKER in repr(error):
composite_logger.log_debug('\nPrivileged operation request intercepted: ' + repr(error))
raise

# General handling
composite_logger.log_error('\nEXCEPTION during patch operation: ' + repr(error))
composite_logger.log_error('TO TROUBLESHOOT, please save this file before the next invocation: ' + bootstrapper.log_file_path)

composite_logger.log_debug("Safely completing required operations after exception...")
if telemetry_writer is not None:
telemetry_writer.send_error_info("EXCEPTION: " + repr(error))
if status_handler is not None:
composite_logger.log_debug(' - Status handler pending writes flags [I=' + str(patch_installation_successful) + ', A=' + str(patch_assessment_successful) + ']')
if patch_operation_requested == Constants.INSTALLATION.lower() and not patch_installation_successful:
status_handler.set_installation_substatus_json(status=Constants.STATUS_ERROR)
composite_logger.log_debug(' -- Persisted failed installation substatus.')
if not patch_assessment_successful:
status_handler.set_assessment_substatus_json(status=Constants.STATUS_ERROR)
composite_logger.log_debug(' -- Persisted failed assessment substatus.')
else:
composite_logger.log_error(' - Status handler is not initialized, and status data cannot be written.')
composite_logger.log_debug("Completed exception handling.\n")

finally:
if lifecycle_manager is not None:
lifecycle_manager.update_core_sequence(completed=True)

telemetry_writer.send_runbook_state_info("Succeeded.")
telemetry_writer.close_transports()

stdout_file_mirror.stop()
file_logger.close(message_at_close="<End of output>")
Empty file added src/core/src/__init__.py
Empty file.
5 changes: 5 additions & 0 deletions src/core/src/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import sys
from CoreMain import CoreMain

if __name__ == "__main__":
CoreMain(sys.argv)
109 changes: 109 additions & 0 deletions src/core/src/bootstrap/Bootstrapper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
""" Environment Manager """
import base64
import json
import os
import sys
from src.bootstrap.ConfigurationFactory import ConfigurationFactory
from src.bootstrap.Constants import Constants
from src.bootstrap.Container import Container
from src.local_loggers.StdOutFileMirror import StdOutFileMirror


class Bootstrapper(object):
def __init__(self, argv):
# Environment awareness
self.current_env = self.get_current_env()
self.argv = argv
self.log_file_path, self.real_record_path = self.get_log_file_and_real_record_paths(argv)
self.recorder_enabled, self.emulator_enabled = self.get_recorder_emulator_flags(argv)

# Container initialization
print("Building bootstrap container configuration...")
self.configuration_factory = ConfigurationFactory(self.log_file_path, self.real_record_path, self.recorder_enabled, self.emulator_enabled)
self.container = Container()
self.container.build(self.configuration_factory.get_bootstrap_configuration(self.current_env))

# Environment layer capture
self.env_layer = self.container.get('env_layer')

# Logging initializations
self.file_logger = self.container.get('file_logger')
self.stdout_file_mirror = StdOutFileMirror(self.env_layer, self.file_logger)
self.composite_logger = self.container.get('composite_logger')
self.telemetry_writer = None

print("Completed building bootstrap container configuration.\n")

@staticmethod
def get_current_env():
""" Decides what environment to bootstrap with """
current_env = os.getenv(Constants.LPE_ENV_VARIABLE, Constants.PROD)
if str(current_env) not in [Constants.DEV, Constants.TEST, Constants.PROD]:
current_env = Constants.PROD
print("Bootstrap environment: " + str(current_env))
return current_env

def get_log_file_and_real_record_paths(self, argv):
""" Performs the minimum steps required to determine where to start logging """
sequence_number = self.get_value_from_argv(argv, Constants.ARG_SEQUENCE_NUMBER)
environment_settings = json.loads(base64.b64decode(self.get_value_from_argv(argv, Constants.ARG_ENVIRONMENT_SETTINGS)))
log_folder = environment_settings[Constants.EnvSettings.LOG_FOLDER] # can throw exception and that's okay (since we can't recover from this)
log_file_path = os.path.join(log_folder, str(sequence_number) + ".core.log")
real_rec_path = os.path.join(log_folder, str(sequence_number) + ".core.rec")
return log_file_path, real_rec_path

def get_recorder_emulator_flags(self, argv):
""" Determines if the recorder or emulator flags need to be changed from the defaults """
recorder_enabled = False
emulator_enabled = False
try:
recorder_enabled = bool(self.get_value_from_argv(argv, Constants.ARG_INTERNAL_RECORDER_ENABLED))
emulator_enabled = bool(self.get_value_from_argv(argv, Constants.ARG_INTERNAL_EMULATOR_ENABLED))
except Exception as error:
print("INFO: Default environment layer settings loaded.")
return recorder_enabled, emulator_enabled

@staticmethod
def get_value_from_argv(argv, key):
""" Discovers the value assigned to a given key based on the core contract on arguments """
for x in range(1, len(argv)):
if x % 2 == 1: # key checker
if str(argv[x]).lower() == key.lower() and x < len(argv):
return str(argv[x+1])
raise Exception("Unable to find key {0} in core arguments: {1}.".format(key, str(argv)))

def build_out_container(self):
# First output in a positive bootstrap
try:
# input parameter incorporation
arguments_config = self.configuration_factory.get_arguments_configuration(self.argv)
self.container.build(arguments_config)

# full configuration incorporation
self.container.build(self.configuration_factory.get_configuration(self.current_env, self.env_layer.get_package_manager()))

return self.container
except Exception as error:
self.composite_logger.log_error('\nEXCEPTION during patch management core bootstrap: ' + repr(error))
raise
pass

def build_core_components(self, container):
self.composite_logger.log_debug(" - Instantiating lifecycle manager.")
lifecycle_manager = container.get('lifecycle_manager')
self.composite_logger.log_debug(" - Instantiating telemetry writer.")
telemetry_writer = container.get('telemetry_writer')
self.composite_logger.log_debug(" - Instantiating progress status writer.")
status_handler = container.get('status_handler')
return lifecycle_manager, telemetry_writer, 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")

def basic_environment_health_check(self):
self.composite_logger.log("Python version: " + " ".join(sys.version.splitlines()))
self.composite_logger.log("Linux distribution: " + str(self.env_layer.platform.linux_distribution()) + "\n")

# Ensure sudo works in the environment
sudo_check_result = self.env_layer.check_sudo_status()
self.composite_logger.log_debug("Sudo status check: " + str(sudo_check_result) + "\n")
196 changes: 196 additions & 0 deletions src/core/src/bootstrap/ConfigurationFactory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
""" Configure factory. This module populates configuration based on package manager and environment, e.g. TEST/DEV/PROD"""
from __future__ import print_function
import os
from src.bootstrap.Constants import Constants
from src.bootstrap.EnvLayer import EnvLayer

from src.core_logic.ExecutionConfig import ExecutionConfig
from src.core_logic.MaintenanceWindow import MaintenanceWindow
from src.core_logic.PackageFilter import PackageFilter
from src.core_logic.RebootManager import RebootManager
from src.core_logic.PatchAssessor import PatchAssessor
from src.core_logic.PatchInstaller import PatchInstaller

from src.local_loggers.FileLogger import FileLogger
from src.local_loggers.CompositeLogger import CompositeLogger

from src.package_managers.AptitudePackageManager import AptitudePackageManager
from src.package_managers.YumPackageManager import YumPackageManager
from src.package_managers.ZypperPackageManager import ZypperPackageManager

from src.service_interfaces.LifecycleManager import LifecycleManager
from src.service_interfaces.StatusHandler import StatusHandler
from src.service_interfaces.TelemetryWriter import TelemetryWriter


class ConfigurationFactory(object):
""" Class for generating module definitions. Configuration is list of key value pairs. Please DON'T change key name.
DI container relies on the key name to find and resolve dependencies. If you do need change it, please make sure to
update the key name in all places that reference it. """
def __init__(self, log_file_path, real_record_path, recorder_enabled, emulator_enabled):
self.bootstrap_configurations = {
'prod_config': self.new_bootstrap_configuration(Constants.PROD, log_file_path, real_record_path, recorder_enabled, emulator_enabled),
'dev_config': self.new_bootstrap_configuration(Constants.DEV, log_file_path, real_record_path, recorder_enabled, emulator_enabled),
'test_config': self.new_bootstrap_configuration(Constants.TEST, log_file_path, real_record_path, recorder_enabled, emulator_enabled)
}

self.configurations = {
'apt_prod_config': self.new_prod_configuration(Constants.APT, AptitudePackageManager),
'yum_prod_config': self.new_prod_configuration(Constants.YUM, YumPackageManager),
'zypper_prod_config': self.new_prod_configuration(Constants.ZYPPER, ZypperPackageManager),

'apt_dev_config': self.new_dev_configuration(Constants.APT, AptitudePackageManager),
'yum_dev_config': self.new_dev_configuration(Constants.YUM, YumPackageManager),
'zypper_dev_config': self.new_dev_configuration(Constants.ZYPPER, ZypperPackageManager),

'apt_test_config': self.new_test_configuration(Constants.APT, AptitudePackageManager),
'yum_test_config': self.new_test_configuration(Constants.YUM, YumPackageManager),
'zypper_test_config': self.new_test_configuration(Constants.ZYPPER, ZypperPackageManager)
}

# region - Configuration Getters
def get_bootstrap_configuration(self, env):
""" Get core configuration for bootstrapping the application. """
if str(env) not in [Constants.DEV, Constants.TEST, Constants.PROD]:
print ("Error: Environment configuration not supported - " + str(env))
return None

configuration_key = str.lower('{0}_config'.format(str(env)))
return self.bootstrap_configurations[configuration_key]

@staticmethod
def get_arguments_configuration(argv):
""" Composes the configuration with the passed in arguments. """
arguments_config = {
'execution_arguments': str(argv),
'execution_config': {
'component': ExecutionConfig,
'component_args': ['env_layer', 'composite_logger'],
'component_kwargs': {
'execution_parameters': str(argv)
}
}
}
return arguments_config

def get_configuration(self, env, package_manager_name):
""" Gets the final configuration for a given env and package manager. """
if str(env) not in [Constants.DEV, Constants.TEST, Constants.PROD]:
print ("Error: Environment configuration not supported - " + str(env))
return None

if str(package_manager_name) not in [Constants.APT, Constants.YUM, Constants.ZYPPER]:
print ("Error: Package manager configuration not supported - " + str(package_manager_name))
return None

configuration_key = str.lower('{0}_{1}_config'.format(str(package_manager_name), str(env)))
selected_configuration = self.configurations[configuration_key]
return selected_configuration
# endregion

# region - Configuration Builders
@staticmethod
def new_bootstrap_configuration(config_env, log_file_path, real_record_path, recorder_enabled, emulator_enabled):
""" Core configuration definition. """
configuration = {
'config_env': config_env,
'env_layer': {
'component': EnvLayer,
'component_args': [],
'component_kwargs': {
'real_record_path': real_record_path,
'recorder_enabled': recorder_enabled,
'emulator_enabled': emulator_enabled
}
},
'file_logger': {
'component': FileLogger,
'component_args': ['env_layer'],
'component_kwargs': {
'log_file': log_file_path
}
},
'composite_logger': {
'component': CompositeLogger,
'component_args': ['file_logger'],
'component_kwargs': {
'current_env': config_env
}
},

}

if config_env is Constants.DEV or config_env is Constants.TEST:
pass # modify config as desired

return configuration

def new_prod_configuration(self, package_manager_name, package_manager_component):
""" Base configuration for Prod V2. """
configuration = {
'config_env': Constants.PROD,
'package_manager_name': package_manager_name,
'lifecycle_manager': {
'component': LifecycleManager,
'component_args': ['env_layer', 'execution_config', 'composite_logger', 'telemetry_writer'],
'component_kwargs': {}
},
'status_handler': {
'component': StatusHandler,
'component_args': ['env_layer', 'execution_config', 'composite_logger', 'telemetry_writer', 'package_manager'],
'component_kwargs': {}
},
'telemetry_writer': {
'component': TelemetryWriter,
'component_args': ['env_layer', 'execution_config'],
'component_kwargs': {}
},
'package_manager': {
'component': package_manager_component,
'component_args': ['env_layer', 'composite_logger', 'telemetry_writer'],
'component_kwargs': {}
},
'reboot_manager': {
'component': RebootManager,
'component_args': ['env_layer', 'execution_config', 'composite_logger', 'status_handler', 'package_manager'],
'component_kwargs': {
'default_reboot_setting': 'IfRequired'
}
},
'package_filter': {
'component': PackageFilter,
'component_args': ['execution_config', 'composite_logger'],
'component_kwargs': {}
},
'patch_assessor': {
'component': PatchAssessor,
'component_args': ['env_layer', 'execution_config', 'composite_logger', 'telemetry_writer', 'status_handler', 'package_manager'],
'component_kwargs': {}
},
'patch_installer': {
'component': PatchInstaller,
'component_args': ['env_layer', 'execution_config', 'composite_logger', 'telemetry_writer', 'status_handler', 'lifecycle_manager', 'package_manager', 'package_filter', 'maintenance_window', 'reboot_manager'],
'component_kwargs': {}
},
'maintenance_window': {
'component': MaintenanceWindow,
'component_args': ['env_layer', 'execution_config', 'composite_logger'],
'component_kwargs': {}
}
}
return configuration

def new_dev_configuration(self, package_manager_name, package_manager_component):
""" Base configuration definition for dev. It derives from the production configuration. """
configuration = self.new_prod_configuration(package_manager_name, package_manager_component)
configuration['config_env'] = Constants.DEV
# perform desired modifications to configuration
return configuration

def new_test_configuration(self, package_manager_name, package_manager_component):
""" Base configuration definition for test. It derives from the production configuration. """
configuration = self.new_prod_configuration(package_manager_name, package_manager_component)
configuration['config_env'] = Constants.TEST
# perform desired modifications to configuration
return configuration
# endregion
Loading