Skip to content

Commit

Permalink
Disable auto OS updates in YUM (#93)
Browse files Browse the repository at this point in the history
* Disabling auto OS updates in yum

* minor corrections

* Addressing review comments

* Resolving bugs in diable auto OS updates in yum and addresing some feedback comments

* Merging with master and adressing some more review comments

* Adding UTs and addressing some more PR comments

* updating extension version to 1.6.27
  • Loading branch information
rane-rajasi authored Sep 25, 2021
1 parent 3e68b1b commit aaa60bc
Show file tree
Hide file tree
Showing 12 changed files with 683 additions and 63 deletions.
9 changes: 8 additions & 1 deletion src/core/src/bootstrap/Constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def __iter__(self):
UNKNOWN = "Unknown"

# Extension version (todo: move to a different file)
EXT_VERSION = "1.6.26"
EXT_VERSION = "1.6.27"

# Runtime environments
TEST = 'Test'
Expand Down Expand Up @@ -112,6 +112,13 @@ class AutomaticOSPatchStates(EnumBackport):
DISABLED = "Disabled"
ENABLED = "Enabled"

# List of auto OS update services in Yum
# todo: move to yumpackagemanager
class YumAutoOSUpdateServices(EnumBackport):
YUM_CRON = "yum-cron"
DNF_AUTOMATIC = "dnf-automatic"
PACKAGEKIT = "packagekit"

# auto assessment states
class AutoAssessmentStates(EnumBackport):
UNKNOWN = "Unknown"
Expand Down
24 changes: 17 additions & 7 deletions src/core/src/bootstrap/EnvLayer.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ def resolve_path(self, requested_path):
else:
return requested_path

def open(self, file_path, mode):
def open(self, file_path, mode, raise_if_not_found=True):
""" Provides a file handle to the file_path requested using implicit redirection where required """
real_path = self.resolve_path(file_path)
for i in range(0, Constants.MAX_FILE_OPERATION_RETRY_COUNT):
Expand All @@ -284,24 +284,29 @@ def open(self, file_path, mode):
if i < Constants.MAX_FILE_OPERATION_RETRY_COUNT:
time.sleep(i + 1)
else:
raise Exception("Unable to open {0} (retries exhausted). Error: {1}.".format(str(real_path), repr(error)))
error_message = "Unable to open file (retries exhausted). [File={0}][Error={1}][RaiseIfNotFound={2}].".format(str(real_path), repr(error), str(raise_if_not_found))
if raise_if_not_found:
raise Exception(error_message)
else:
print(error_message)
return None

def __obtain_file_handle(self, file_path_or_handle, mode='a+'):
def __obtain_file_handle(self, file_path_or_handle, mode='a+', raise_if_not_found=True):
""" Pass-through for handle. For path, resolution and handle open with retry. """
is_path = False
if isinstance(file_path_or_handle, str) or not(hasattr(file_path_or_handle, 'read') and hasattr(file_path_or_handle, 'write')):
is_path = True
file_path_or_handle = self.open(file_path_or_handle, mode)
file_path_or_handle = self.open(file_path_or_handle, mode, raise_if_not_found)
file_handle = file_path_or_handle
return file_handle, is_path

def read_with_retry(self, file_path_or_handle):
def read_with_retry(self, file_path_or_handle, raise_if_not_found=True):
""" Reads all content from a given file path in a single operation """
operation = "FILE_READ"

# only fully emulate non_exclusive_files from the real recording; exclusive files can be redirected and handled in emulator scenarios
if not self.__emulator_enabled or (isinstance(file_path_or_handle, str) and os.path.basename(file_path_or_handle) not in self.__non_exclusive_files):
file_handle, was_path = self.__obtain_file_handle(file_path_or_handle, 'r')
file_handle, was_path = self.__obtain_file_handle(file_path_or_handle, 'r', raise_if_not_found)
for i in range(0, Constants.MAX_FILE_OPERATION_RETRY_COUNT):
try:
value = file_handle.read()
Expand All @@ -313,7 +318,12 @@ def read_with_retry(self, file_path_or_handle):
if i < Constants.MAX_FILE_OPERATION_RETRY_COUNT:
time.sleep(i + 1)
else:
raise Exception("Unable to read from {0} (retries exhausted). Error: {1}.".format(str(file_path_or_handle), repr(error)))
error_message = "Unable to read file (retries exhausted). [File={0}][Error={1}][RaiseIfNotFound={2}].".format(str(file_path_or_handle), repr(error), str(raise_if_not_found))
if raise_if_not_found:
raise Exception(error_message)
else:
print(error_message)
return None
else:
code, output = self.__read_record(operation)
return output
Expand Down
11 changes: 7 additions & 4 deletions src/core/src/core_logic/ConfigurePatchingProcessor.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,17 +65,20 @@ def __try_set_patch_mode(self):
try:
self.status_handler.set_current_operation(Constants.CONFIGURE_PATCHING)
self.current_auto_os_patch_state = self.package_manager.get_current_auto_os_patch_state()
self.composite_logger.log_debug("Current Auto OS Patch State is [State={0}]".format(str(self.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.ENABLED and self.execution_config.patch_mode == Constants.PatchModes.AUTOMATIC_BY_PLATFORM:
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()

self.current_auto_os_patch_state = self.package_manager.get_current_auto_os_patch_state()
self.composite_logger.log_debug("Current Auto OS Patch State is [State={0}]".format(str(self.current_auto_os_patch_state)))

self.__report_consolidated_configure_patch_status()
if 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.__report_consolidated_configure_patch_status(error="Extension attempted but could not disable some of the auto OS update service. Please check if the auto OS services are configured correctly")
else:
self.__report_consolidated_configure_patch_status()

self.composite_logger.log_debug("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)))
Expand Down
28 changes: 23 additions & 5 deletions src/core/src/package_managers/AptitudePackageManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -395,11 +395,14 @@ def get_current_auto_os_patch_state(self):
self.composite_logger.log("Fetching the current automatic OS patch state on the machine...")
self.__get_current_auto_os_updates_setting_on_machine()
if int(self.unattended_upgrade_value) == 0:
return Constants.AutomaticOSPatchStates.DISABLED
current_auto_os_patch_state = Constants.AutomaticOSPatchStates.DISABLED
elif int(self.unattended_upgrade_value) == 1:
return Constants.AutomaticOSPatchStates.ENABLED
current_auto_os_patch_state = Constants.AutomaticOSPatchStates.ENABLED
else:
return Constants.AutomaticOSPatchStates.UNKNOWN
current_auto_os_patch_state = Constants.AutomaticOSPatchStates.UNKNOWN

self.composite_logger.log_debug("Current Auto OS Patch State is [State={0}]".format(str(current_auto_os_patch_state)))
return current_auto_os_patch_state

def __get_current_auto_os_updates_setting_on_machine(self):
""" Gets all the update settings related to auto OS updates currently set on the machine """
Expand Down Expand Up @@ -437,7 +440,22 @@ def backup_image_default_patch_configuration_if_not_exists(self):
""" Records the default system settings for auto OS updates within patch extension artifacts for future reference.
We only log the default system settings a VM comes with, any subsequent updates will not be recorded"""
try:
if not self.image_default_patch_configuration_backup_exists():
image_default_patch_configuration_backup = {}

# read existing backup since it also contains backup from other update services. We need to preserve any existing data with backup file
if self.image_default_patch_configuration_backup_exists():
try:
image_default_patch_configuration_backup = json.loads(self.env_layer.file_system.read_with_retry(self.image_default_patch_configuration_backup_path))
except Exception as error:
self.composite_logger.log_error("Unable to read backup for default patch state. Will attempt to re-write. [Exception={0}]".format(repr(error)))

# verify if existing backup is valid if not, write to backup
is_backup_valid = self.is_image_default_patch_configuration_backup_valid(image_default_patch_configuration_backup)
if is_backup_valid:
self.composite_logger.log_debug("Since extension has a valid backup, no need to log the current settings again. [Default Auto OS update settings={0}] [File path={1}]"
.format(str(image_default_patch_configuration_backup), self.image_default_patch_configuration_backup_path))
else:
self.composite_logger.log_debug("Since the backup is invalid, will add a new backup with the current auto OS update settings")
self.__get_current_auto_os_updates_setting_on_machine()

backup_image_default_patch_configuration_json = {
Expand All @@ -462,7 +480,7 @@ def is_image_default_patch_configuration_backup_valid(self, image_default_patch_
self.composite_logger.log_error("Extension does not have a valid backup of the default system configuration settings for auto OS updates.")
return False

def update_os_patch_configuration_sub_setting(self, patch_configuration_sub_setting, value="0"):
def update_os_patch_configuration_sub_setting(self, patch_configuration_sub_setting, value="0", patch_configuration_sub_setting_pattern_to_match=""):
""" Updates (or adds if it doesn't exist) the given patch_configuration_sub_setting with the given value in os_patch_configuration_settings_file """
try:
# note: adding space between the patch_configuration_sub_setting and value since, we will have to do that if we have to add a patch_configuration_sub_setting that did not exist before
Expand Down
17 changes: 2 additions & 15 deletions src/core/src/package_managers/PackageManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -348,27 +348,14 @@ def image_default_patch_configuration_backup_exists(self):
self.composite_logger.log_debug("Default system configuration settings for auto OS updates aren't recorded in the extension")
return False

# verify if the existing backup is valid
try:
image_default_patch_configuration_backup = json.loads(self.env_layer.file_system.read_with_retry(self.image_default_patch_configuration_backup_path))
if self.is_image_default_patch_configuration_backup_valid(image_default_patch_configuration_backup):
self.composite_logger.log_debug("Since extension has a valid backup, no need to log the current settings again. "
"[Default Auto OS update settings={0}] [File path={1}]"
.format(str(image_default_patch_configuration_backup), self.image_default_patch_configuration_backup_path))
return True
else:
self.composite_logger.log_error("Since the backup is invalid, will add a new backup with the current auto OS update settings")
return False
except Exception as error:
self.composite_logger.log_error("Unable to read backup for default auto OS update settings. [Exception={0}]".format(repr(error)))
return False
return True

@abstractmethod
def is_image_default_patch_configuration_backup_valid(self, image_default_patch_configuration_backup):
pass

@abstractmethod
def update_os_patch_configuration_sub_setting(self, patch_configuration_sub_setting, value):
def update_os_patch_configuration_sub_setting(self, patch_configuration_sub_setting, value, patch_configuration_sub_setting_pattern_to_match):
pass
# endregion

Expand Down
Loading

0 comments on commit aaa60bc

Please sign in to comment.