From 950673e69cc106ab9d9508e693c11dbadb28e413 Mon Sep 17 00:00:00 2001 From: nnandigam Date: Sun, 26 Nov 2023 19:48:26 -0800 Subject: [PATCH] addressed comments --- azurelinuxagent/common/conf.py | 4 +- ...sions_goal_state_from_extensions_config.py | 4 +- .../extensions_goal_state_from_vm_settings.py | 4 +- azurelinuxagent/common/protocol/restapi.py | 4 +- azurelinuxagent/ga/agent_update_handler.py | 521 +++--------------- azurelinuxagent/ga/ga_version_updater.py | 157 ++++++ azurelinuxagent/ga/rsm_version_updater.py | 159 ++++++ .../ga/self_update_version_updater.py | 181 ++++++ azurelinuxagent/ga/update.py | 2 +- tests/ga/test_agent_update_handler.py | 32 +- tests/ga/test_update.py | 6 +- 11 files changed, 611 insertions(+), 463 deletions(-) create mode 100644 azurelinuxagent/ga/ga_version_updater.py create mode 100644 azurelinuxagent/ga/rsm_version_updater.py create mode 100644 azurelinuxagent/ga/self_update_version_updater.py diff --git a/azurelinuxagent/common/conf.py b/azurelinuxagent/common/conf.py index 57d6c9d28..e6e926355 100644 --- a/azurelinuxagent/common/conf.py +++ b/azurelinuxagent/common/conf.py @@ -479,8 +479,8 @@ def get_autoupdate_enabled(conf=__conf__): return conf.get_switch("AutoUpdate.Enabled", True) -def get_autoupdate_frequency(conf=__conf__): - return conf.get_int("Autoupdate.Frequency", 3600) +def get_agentupdate_frequency(conf=__conf__): + return conf.get_int("AgentUpdate.Frequency", 3600) def get_enable_overprovisioning(conf=__conf__): diff --git a/azurelinuxagent/common/protocol/extensions_goal_state_from_extensions_config.py b/azurelinuxagent/common/protocol/extensions_goal_state_from_extensions_config.py index f8a25c299..5894c5972 100644 --- a/azurelinuxagent/common/protocol/extensions_goal_state_from_extensions_config.py +++ b/azurelinuxagent/common/protocol/extensions_goal_state_from_extensions_config.py @@ -65,7 +65,9 @@ def _parse_extensions_config(self, xml_text, wire_client): is_vm_enabled_for_rsm_upgrades = findtext(ga_family, "IsVMEnabledForRSMUpgrades") uris_list = find(ga_family, "Uris") uris = findall(uris_list, "Uri") - family = VMAgentFamily(name, version) + family = VMAgentFamily(name) + if version is not None: + family.version = version if is_version_from_rsm is not None: family.is_version_from_rsm = is_version_from_rsm.lower() == "true" if is_vm_enabled_for_rsm_upgrades is not None: diff --git a/azurelinuxagent/common/protocol/extensions_goal_state_from_vm_settings.py b/azurelinuxagent/common/protocol/extensions_goal_state_from_vm_settings.py index afbb1b7f3..5f6fd61a6 100644 --- a/azurelinuxagent/common/protocol/extensions_goal_state_from_vm_settings.py +++ b/azurelinuxagent/common/protocol/extensions_goal_state_from_vm_settings.py @@ -274,7 +274,9 @@ def _parse_agent_manifests(self, vm_settings): uris = family.get("uris") if uris is None: uris = [] - agent_family = VMAgentFamily(name, version) + agent_family = VMAgentFamily(name) + if version is not None: + agent_family.version = version if is_version_from_rsm is not None: agent_family.is_version_from_rsm = is_version_from_rsm if is_vm_enabled_for_rsm_upgrades is not None: diff --git a/azurelinuxagent/common/protocol/restapi.py b/azurelinuxagent/common/protocol/restapi.py index 0ec34db86..e6524b6e4 100644 --- a/azurelinuxagent/common/protocol/restapi.py +++ b/azurelinuxagent/common/protocol/restapi.py @@ -68,10 +68,10 @@ def __init__(self): class VMAgentFamily(object): - def __init__(self, name, version): + def __init__(self, name): self.name = name # This is the version as specified by the Goal State - self.version = version + self.version = None # Set to None if the property not specified in the GS and later computed True/False based on previous state in agent update self.is_version_from_rsm = None # Set to None if this property not specified in the GS and later computed True/False based on previous state in agent update diff --git a/azurelinuxagent/ga/agent_update_handler.py b/azurelinuxagent/ga/agent_update_handler.py index b95195913..302cb3c7e 100644 --- a/azurelinuxagent/ga/agent_update_handler.py +++ b/azurelinuxagent/ga/agent_update_handler.py @@ -1,76 +1,74 @@ +# Microsoft Azure Linux Agent +# +# Copyright 2020 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.6+ and Openssl 1.0+ import datetime -import glob import json import os -import shutil from azurelinuxagent.common import conf, logger from azurelinuxagent.common.event import add_event, WALAEventOperation from azurelinuxagent.common.exception import AgentUpgradeExitException, AgentUpdateError from azurelinuxagent.common.future import ustr -from azurelinuxagent.common.protocol.extensions_goal_state import GoalStateSource from azurelinuxagent.common.protocol.restapi import VMAgentUpdateStatuses, VMAgentUpdateStatus, VERSION_0 -from azurelinuxagent.common.utils import fileutil, textutil, timeutil +from azurelinuxagent.common.utils import textutil from azurelinuxagent.common.utils.flexible_version import FlexibleVersion -from azurelinuxagent.common.version import get_daemon_version, CURRENT_VERSION, AGENT_NAME, AGENT_DIR_PATTERN -from azurelinuxagent.ga.guestagent import GuestAgent +from azurelinuxagent.common.version import get_daemon_version +from azurelinuxagent.ga.ga_version_updater import VMDisabledRSMUpdates, VMEnabledRSMUpdates +from azurelinuxagent.ga.rsm_version_updater import RSMVersionUpdater +from azurelinuxagent.ga.self_update_version_updater import SelfUpdateVersionUpdater def get_agent_update_handler(protocol): return AgentUpdateHandler(protocol) -class SelfUpdateType(object): - """ - Enum for different modes of Self updates - """ - Hotfix = "Hotfix" - Regular = "Regular" - - -class AgentUpdateHandlerUpdateState(object): - """ - This class is primarily used to maintain the in-memory persistent state for the agent updates. - This state will be persisted throughout the current service run. - """ - def __init__(self): - self.last_attempted_rsm_version_update_time = datetime.datetime.min - self.last_attempted_self_update_hotfix_time = datetime.datetime.min - self.last_attempted_self_update_regular_time = datetime.datetime.min - self.last_attempted_manifest_download_time = datetime.datetime.min - self.last_attempted_update_error_msg = "" - self.last_attempted_update_version = FlexibleVersion("0.0.0.0") - - class AgentUpdateHandler(object): """ - This class handles two type of agent updates and chooses the appropriate updater based on the below conditions: + This class handles two type of agent updates. Handler initializes the updater to SelfUpdateVersionUpdater and switch to appropriate updater based on below conditions: RSM update: This is the update requested by RSM. The contract between CRP and agent is we get following properties in the goal state: version: it will have what version to update isVersionFromRSM: True if the version is from RSM deployment. isVMEnabledForRSMUpgrades: True if the VM is enabled for RSM upgrades. if vm enabled for RSM upgrades, we use RSM update path. But if requested update is not by rsm deployment we ignore the update. - This update is allowed once per (as specified in the conf.get_autoupdate_frequency()) Self update: We fallback to this if above is condition not met. This update to the largest version available in the manifest - we allow update once per (as specified in the conf.get_self_update_hotfix_frequency() or conf.get_self_update_regular_frequency()) Note: Self-update don't support downgrade. + + Handler keeps the rsm state of last update is with RSM or not on every new goal state. Once handler decides which updater to use, then + does following steps: + 1. Retrieve the agent version from the goal state. + 2. Check if we allowed to update for that version. + 3. Log the update message. + 4. Purge the extra agents from disk. + 5. Download the new agent. + 6. Proceed with update. """ def __init__(self, protocol): self._protocol = protocol - self._ga_family = conf.get_autoupdate_gafamily() - self._autoupdate_enabled = conf.get_autoupdate_enabled() self._gs_id = "unknown" + self._ga_family_type = conf.get_autoupdate_gafamily() self._daemon_version = self._get_daemon_version_for_update() - self.update_state = AgentUpdateHandlerUpdateState() + self._last_attempted_update_error_msg = "" - # restore the state of rsm update - if not os.path.exists(self._get_rsm_version_state_file()): - self._is_version_from_rsm = False - self._is_vm_enabled_for_rsm_upgrades = False + # restore the state of rsm update. Default to self-update if last update is not with RSM. + if not self._get_is_last_update_with_rsm(): + self._updater = SelfUpdateVersionUpdater(self._gs_id, datetime.datetime.min) else: - self._is_version_from_rsm = self._get_is_version_from_rsm() - self._is_vm_enabled_for_rsm_upgrades = self._get_is_vm_enabled_for_rsm_upgrades() + self._updater = RSMVersionUpdater(self._gs_id, self._daemon_version, datetime.datetime.min) @staticmethod def _get_daemon_version_for_update(): @@ -82,103 +80,43 @@ def _get_daemon_version_for_update(): return FlexibleVersion("2.2.53") @staticmethod - def _get_rsm_version_state_file(): - # This file keeps the isversionfromrsm and isvmeabledforrsmupgrades of the most recent goal state. - return os.path.join(conf.get_lib_dir(), "rsm_version.json") + def _get_rsm_update_state_file(): + # This file keeps if last attempted update is rsm or not. + return os.path.join(conf.get_lib_dir(), "rsm_update.json") - def _save_rsm_version_state(self, isVersionFromRSM, isVMEnabledForRSMUpgrades, timestamp): + def _save_rsm_update_state(self, isLastUpdateWithRSM): """ - Save the rsm state to the file + Save the rsm state to the file when we switch between rsm and self-update """ try: - with open(self._get_rsm_version_state_file(), "w") as file_: - json.dump({"isVersionFromRSM": isVersionFromRSM, - "isVMEnabledForRSMUpgrades": isVMEnabledForRSMUpgrades, - "timestamp": timestamp}, file_) + with open(self._get_rsm_update_state_file(), "w") as file_: + json.dump({"isLastUpdateWithRSM": isLastUpdateWithRSM}, file_) except Exception as e: - logger.warn("Error updating the RSM version state ({0}): {1}", self._get_rsm_version_state_file(), ustr(e)) - - def _get_is_version_from_rsm(self): - """ - Returns isVersionFromRSM property of most recent goal state or False if the most recent - goal state was not added this property or set to False in gs. - """ - if not os.path.exists(self._get_rsm_version_state_file()): - return False - - try: - with open(self._get_rsm_version_state_file(), "r") as file_: - return json.load(file_)["isVersionFromRSM"] - except Exception as e: - logger.warn( - "Can't retrieve the is_version_from_rsm most recent rsm state ({0}), will assume it False. Error: {1}", - self._get_rsm_version_state_file(), ustr(e)) - return False + logger.warn("Error updating the RSM state ({0}): {1}", self._get_rsm_update_state_file(), ustr(e)) - def _get_is_vm_enabled_for_rsm_upgrades(self): + def _get_is_last_update_with_rsm(self): """ - Returns isVMEnabledForRSMUpgrades property of most recent goal state or False if the most recent - goal state was not added this property or set to False in gs. + Returns isLastUpdateWithRSM from the state file. """ - if not os.path.exists(self._get_rsm_version_state_file()): + if not os.path.exists(self._get_rsm_update_state_file()): return False try: - with open(self._get_rsm_version_state_file(), "r") as file_: - return json.load(file_)["isVMEnabledForRSMUpgrades"] + with open(self._get_rsm_update_state_file(), "r") as file_: + return json.load(file_)["isLastUpdateWithRSM"] except Exception as e: logger.warn( - "Can't retrieve the is_vm_enabled_for_rsm_upgrades most recent rsm state ({0}), will assume it False. Error: {1}", - self._get_rsm_version_state_file(), ustr(e)) + "Can't retrieve the isLastUpdateWithRSM from rsm state file ({0}), will assume it False. Error: {1}", + self._get_rsm_update_state_file(), ustr(e)) return False - def _get_rsm_state_used_gs_timestamp(self): - """ - Returns the timestamp of th goal state used for rsm state, or min if the most recent - goal state has not been invoked. - """ - if not os.path.exists(self._get_rsm_version_state_file()): - return timeutil.create_timestamp(datetime.datetime.min) - - try: - with open(self._get_rsm_version_state_file(), "r") as file_: - return json.load(file_)["timestamp"] - - except Exception as e: - logger.warn( - "Can't retrieve the timestamp of goal state used for rsm state ({0}), will assume the datetime.min time. Error: {1}", - self._get_rsm_version_state_file(), ustr(e)) - return timeutil.create_timestamp(datetime.datetime.min) - - def _update_rsm_version_state_if_changed(self, goalstate_timestamp, agent_family): - """ - Persisting state to address the issue when HGPA supported(properties present) to unsupported(properties not present) and also sync between Wireserver and HGAP. - Updates the isVrsionFromRSM and isVMEnabledForRSMUpgrades of the most recent goal state retrieved if - properties changed from last rsm state. - Timestamp is the timestamp of the goal state used to update the state. This timestamp helps ignore old goal states when it gets to the vm as a recent goal state. - """ - last_timestamp = self._get_rsm_state_used_gs_timestamp() - # update the state if the goal state is newer than the last goal state used to update the state. - if last_timestamp < goalstate_timestamp: - update_file = False - if agent_family.is_version_from_rsm is not None and self._is_version_from_rsm != agent_family.is_version_from_rsm: - self._is_version_from_rsm = agent_family.is_version_from_rsm - update_file = True - - if agent_family.is_vm_enabled_for_rsm_upgrades is not None and self._is_vm_enabled_for_rsm_upgrades != agent_family.is_vm_enabled_for_rsm_upgrades: - self._is_vm_enabled_for_rsm_upgrades = agent_family.is_vm_enabled_for_rsm_upgrades - update_file = True - - if update_file: - self._save_rsm_version_state(self._is_version_from_rsm, self._is_vm_enabled_for_rsm_upgrades, goalstate_timestamp) - def _get_agent_family_manifest(self, goal_state): """ Get the agent_family from last GS for the given family Returns: first entry of Manifest Exception if no manifests found in the last GS """ - family = self._ga_family + family = self._ga_family_type agent_families = goal_state.extensions_goal_state.agent_families family_found = False agent_family_manifests = [] @@ -194,51 +132,50 @@ def _get_agent_family_manifest(self, goal_state): if len(agent_family_manifests) == 0: raise AgentUpdateError( u"No manifest links found for agent family: {0} for incarnation: {1}, skipping agent update".format( - self._ga_family, self._gs_id)) + family, self._gs_id)) return agent_family_manifests[0] - @staticmethod - def _get_version_from_gs(agent_family): - """ - Get the version from agent family - Returns: version if supported and available in the GS - None if version is missing - """ - if agent_family.version is not None: - return FlexibleVersion(agent_family.version) - return None - def run(self, goal_state): try: # Ignore new agents if update is disabled. The latter flag only used in e2e tests. - if not self._autoupdate_enabled or not conf.get_download_new_agents(): + if not conf.get_autoupdate_enabled() or not conf.get_download_new_agents(): + return + + # verify if agent update is allowed this time (RSM checks 1 hr interval; self-update checks manifest download interval) + if not self._updater.is_update_allowed_this_time(): return agent_family = self._get_agent_family_manifest(goal_state) - version = self._get_version_from_gs(agent_family) gs_id = goal_state.extensions_goal_state.id - self._update_rsm_version_state_if_changed(goal_state.extensions_goal_state.created_on_timestamp, agent_family) - # if version is specified and vm is enabled for rsm upgrades, use rsm update path, else sef-update - if version is None and self._is_vm_enabled_for_rsm_upgrades and self._is_version_from_rsm: - raise AgentUpdateError("VM Enabled for RSM upgrades but version is missing in Goal state: {0}, so skipping agent update".format(gs_id)) - elif conf.get_enable_ga_versioning() and self._is_vm_enabled_for_rsm_upgrades: - updater = RSMVersionUpdater(gs_id, agent_family, None, version, self.update_state, self._is_version_from_rsm, self._daemon_version) - self.update_state.last_attempted_update_version = version - else: - updater = SelfUpdateVersionUpdater(gs_id, agent_family, None, None, self.update_state) + try: + # updater will raise exception if we need to switch to self-update or rsm update + self._updater.check_and_switch_updater_if_changed(agent_family, gs_id) + except VMDisabledRSMUpdates: + msg = "VM not enabled for RSM updates, switching to self-update mode" + logger.info(msg) + add_event(op=WALAEventOperation.AgentUpgrade, message=msg, log_event=False) + self._updater = SelfUpdateVersionUpdater(gs_id, datetime.datetime.now()) + self._save_rsm_update_state(isLastUpdateWithRSM=False) + except VMEnabledRSMUpdates: + msg = "VM enabled for RSM updates, switching to RSM update mode" + logger.info(msg) + add_event(op=WALAEventOperation.AgentUpgrade, message=msg, log_event=False) + self._updater = RSMVersionUpdater(gs_id, self._daemon_version, datetime.datetime.now()) + self._save_rsm_update_state(isLastUpdateWithRSM=True) + + self._updater.retrieve_agent_version(agent_family, goal_state) - # verify if agent update is allowed - if not updater.should_update_agent(goal_state): + if not self._updater.is_retrieved_version_allowed_to_update(agent_family): return - updater.log_new_agent_update_message() - updater.purge_extra_agents_from_disk() - agent = updater.download_and_get_new_agent(self._protocol, goal_state) + self._updater.log_new_agent_update_message() + self._updater.purge_extra_agents_from_disk() + agent = self._updater.download_and_get_new_agent(self._protocol, agent_family, goal_state) if agent.is_blacklisted or not agent.is_downloaded: msg = "Downloaded agent version is in bad state : {0} , skipping agent update".format( str(agent.version)) raise AgentUpdateError(msg) - updater.proceed_with_update() + self._updater.proceed_with_update() except Exception as err: if isinstance(err, AgentUpgradeExitException): @@ -249,308 +186,26 @@ def run(self, goal_state): error_msg = "Unable to update Agent: {0}".format(textutil.format_exception(err)) logger.warn(error_msg) add_event(op=WALAEventOperation.AgentUpgrade, is_success=False, message=error_msg, log_event=False) - self.update_state.last_attempted_update_error_msg = error_msg + self._last_attempted_update_error_msg = error_msg def get_vmagent_update_status(self): """ This function gets the VMAgent update status as per the last attempted update. Returns: None if fail to report or update never attempted with rsm version specified in GS + Note: We send the status regardless of updater type. Since we call this main loop, want to avoid fetching agent family to decide and send only if + vm enabled for rsm updates. """ try: - if conf.get_enable_ga_versioning() and self._is_vm_enabled_for_rsm_upgrades and self._is_version_from_rsm: - if not self.update_state.last_attempted_update_error_msg: + if conf.get_enable_ga_versioning(): + if not self._last_attempted_update_error_msg: status = VMAgentUpdateStatuses.Success code = 0 else: status = VMAgentUpdateStatuses.Error code = 1 - return VMAgentUpdateStatus(expected_version=str(self.update_state.last_attempted_update_version), status=status, code=code, message=self.update_state.last_attempted_update_error_msg) + return VMAgentUpdateStatus(expected_version=str(self._updater.version), status=status, code=code, message=self._last_attempted_update_error_msg) except Exception as err: msg = "Unable to report agent update status: {0}".format(textutil.format_exception(err)) logger.warn(msg) add_event(op=WALAEventOperation.AgentUpgrade, is_success=False, message=msg, log_event=True) return None - - -class GAVersionUpdater(object): - - def __init__(self, gs_id, agent_family, agent_manifest, version, update_state): - self._gs_id = gs_id - self._agent_family = agent_family - self._agent_manifest = agent_manifest - self._version = version - self._update_state = update_state - - def should_update_agent(self, goal_state): - """ - RSM version update: - update is allowed once per (as specified in the conf.get_autoupdate_frequency()) and - if new version not same as current version, not below than daemon version and if version is from rsm request - return false when we don't allow updates. - self-update: - 1) checks if we allowed download manifest as per manifest download frequency - 2) update is allowed once per (as specified in the conf.get_self_update_hotfix_frequency() or conf.get_self_update_regular_frequency()) - 3) not below than current version - return false when we don't allow updates. - """ - raise NotImplementedError - - def log_new_agent_update_message(self): - """ - This function logs the update message after we check agent allowed to update. - """ - raise NotImplementedError - - def purge_extra_agents_from_disk(self): - """ - RSM version update: - remove the agents( including rsm version if exists) from disk except current version. There is a chance that rsm version could exist and/or blacklisted - on previous update attempts. So we should remove it from disk in order to honor current rsm version update. - self-update: - remove the agents from disk except current version and new agent version if exists - """ - raise NotImplementedError - - def proceed_with_update(self): - """ - RSM version update: - upgrade/downgrade to the specified version. - Raises: AgentUpgradeExitException - self-update: - If largest version is found in manifest, upgrade to that version. Downgrade is not supported. - Raises: AgentUpgradeExitException - """ - raise NotImplementedError - - def download_and_get_new_agent(self, protocol, goal_state): - """ - This function downloads the new agent and returns the downloaded version. - """ - if self._agent_manifest is None: # Fetch agent manifest if it's not already done - self._agent_manifest = goal_state.fetch_agent_manifest(self._agent_family.name, self._agent_family.uris) - package_to_download = self._get_agent_package_to_download(self._agent_manifest, self._version) - is_fast_track_goal_state = goal_state.extensions_goal_state.source == GoalStateSource.FastTrack - agent = GuestAgent.from_agent_package(package_to_download, protocol, is_fast_track_goal_state) - return agent - - def _get_agent_package_to_download(self, agent_manifest, version): - """ - Returns the package of the given Version found in the manifest. If not found, returns exception - """ - for pkg in agent_manifest.pkg_list.versions: - if FlexibleVersion(pkg.version) == version: - # Found a matching package, only download that one - return pkg - - raise AgentUpdateError("No matching package found in the agent manifest for version: {0} in goal state incarnation: {1}, " - "skipping agent update".format(str(version), self._gs_id)) - - @staticmethod - def _purge_unknown_agents_from_disk(known_agents): - """ - Remove from disk all directories and .zip files of unknown agents - """ - path = os.path.join(conf.get_lib_dir(), "{0}-*".format(AGENT_NAME)) - - for agent_path in glob.iglob(path): - try: - name = fileutil.trim_ext(agent_path, "zip") - m = AGENT_DIR_PATTERN.match(name) - if m is not None and FlexibleVersion(m.group(1)) not in known_agents: - if os.path.isfile(agent_path): - logger.info(u"Purging outdated Agent file {0}", agent_path) - os.remove(agent_path) - else: - logger.info(u"Purging outdated Agent directory {0}", agent_path) - shutil.rmtree(agent_path) - except Exception as e: - logger.warn(u"Purging {0} raised exception: {1}", agent_path, ustr(e)) - - -class RSMVersionUpdater(GAVersionUpdater): - def __init__(self, gs_id, agent_family, agent_manifest, version, update_state, is_version_from_rsm, daemon_version): - super(RSMVersionUpdater, self).__init__(gs_id, agent_family, agent_manifest, version, update_state) - self._is_version_from_rsm = is_version_from_rsm - self._daemon_version = daemon_version - - @staticmethod - def _get_all_agents_on_disk(): - path = os.path.join(conf.get_lib_dir(), "{0}-*".format(AGENT_NAME)) - return [GuestAgent.from_installed_agent(path=agent_dir) for agent_dir in glob.iglob(path) if os.path.isdir(agent_dir)] - - def _get_available_agents_on_disk(self): - available_agents = [agent for agent in self._get_all_agents_on_disk() if agent.is_available] - return sorted(available_agents, key=lambda agent: agent.version, reverse=True) - - def _is_update_allowed_this_time(self): - """ - update is allowed once per (as specified in the conf.get_autoupdate_frequency()) - If update allowed, we update the last_attempted_rsm_version_update_time to current time. - """ - now = datetime.datetime.now() - - if self._update_state.last_attempted_rsm_version_update_time != datetime.datetime.min: - next_attempt_time = self._update_state.last_attempted_rsm_version_update_time + datetime.timedelta( - seconds=conf.get_autoupdate_frequency()) - else: - next_attempt_time = now - - if next_attempt_time > now: - return False - self._update_state.last_attempted_rsm_version_update_time = now - # The time limit elapsed for us to allow updates. - return True - - def should_update_agent(self, goal_state): - if not self._is_update_allowed_this_time(): - return False - - # we don't allow updates if version is not from RSM or downgrades below daemon version or if version is same as current version - if not self._is_version_from_rsm or self._version < self._daemon_version or self._version == CURRENT_VERSION: - return False - - return True - - def log_new_agent_update_message(self): - msg = "New agent version:{0} requested by RSM in Goal state {1}, will update the agent before processing the goal state.".format(str(self._version), self._gs_id) - logger.info(msg) - add_event(op=WALAEventOperation.AgentUpgrade, message=msg, log_event=False) - - def purge_extra_agents_from_disk(self): - known_agents = [CURRENT_VERSION] - self._purge_unknown_agents_from_disk(known_agents) - - def proceed_with_update(self): - if self._version < CURRENT_VERSION: - # In case of a downgrade, we mark the current agent as bad version to avoid starting it back up ever again - # (the expectation here being that if we get request to a downgrade, - # there's a good reason for not wanting the current version). - prefix = "downgrade" - try: - # We should always have an agent directory for the CURRENT_VERSION - agents_on_disk = self._get_available_agents_on_disk() - current_agent = next(agent for agent in agents_on_disk if agent.version == CURRENT_VERSION) - msg = "Marking the agent {0} as bad version since a downgrade was requested in the GoalState, " \ - "suggesting that we really don't want to execute any extensions using this version".format( - CURRENT_VERSION) - logger.info(msg) - add_event(op=WALAEventOperation.AgentUpgrade, message=msg, log_event=False) - current_agent.mark_failure(is_fatal=True, reason=msg) - except StopIteration: - logger.warn( - "Could not find a matching agent with current version {0} to blacklist, skipping it".format( - CURRENT_VERSION)) - else: - # In case of an upgrade, we don't need to exclude anything as the daemon will automatically - # start the next available highest version which would be the target version - prefix = "upgrade" - raise AgentUpgradeExitException( - "Agent completed all update checks, exiting current process to {0} to the new Agent version {1}".format(prefix, - self._version)) - - -class SelfUpdateVersionUpdater(GAVersionUpdater): - - @staticmethod - def _get_largest_version(agent_manifest): - """ - Get the largest version from the agent manifest - """ - largest_version = FlexibleVersion("0.0.0.0") - for pkg in agent_manifest.pkg_list.versions: - pkg_version = FlexibleVersion(pkg.version) - if pkg_version > largest_version: - largest_version = pkg_version - return largest_version - - @staticmethod - def _get_agent_upgrade_type(version): - # We follow semantic versioning for the agent, if .. is same, then has changed. - # In this case, we consider it as a Hotfix upgrade. Else we consider it a Regular upgrade. - if version.major == CURRENT_VERSION.major and version.minor == CURRENT_VERSION.minor and version.patch == CURRENT_VERSION.patch: - return SelfUpdateType.Hotfix - return SelfUpdateType.Regular - - def _get_next_upgrade_times(self, now): - """ - Get the next upgrade times - return: Next Hotfix Upgrade Time, Next Regular Upgrade Time - """ - - def get_next_process_time(last_val, frequency): - return now if last_val == datetime.datetime.min else last_val + datetime.timedelta(seconds=frequency) - - next_hotfix_time = get_next_process_time(self._update_state.last_attempted_self_update_hotfix_time, - conf.get_self_update_hotfix_frequency()) - next_regular_time = get_next_process_time(self._update_state.last_attempted_self_update_regular_time, - conf.get_self_update_regular_frequency()) - - return next_hotfix_time, next_regular_time - - def _is_update_allowed_this_time(self): - """ - This method ensure that update is allowed only once per (hotfix/Regular) upgrade frequency - """ - now = datetime.datetime.now() - next_hotfix_time, next_regular_time = self._get_next_upgrade_times(now) - upgrade_type = self._get_agent_upgrade_type(self._version) - - if (upgrade_type == SelfUpdateType.Hotfix and next_hotfix_time <= now) or ( - upgrade_type == SelfUpdateType.Regular and next_regular_time <= now): - # Update the last upgrade check time even if no new agent is available for upgrade - self._update_state.last_attempted_self_update_hotfix_time = now - self._update_state.last_attempted_self_update_regular_time = now - return True - return False - - def _should_agent_attempt_manifest_download(self): - """ - The agent should attempt to download the manifest if - the agent has not attempted to download the manifest in the last 1 hour - If we allow update, we update the last attempted manifest download time - """ - now = datetime.datetime.now() - - if self._update_state.last_attempted_manifest_download_time != datetime.datetime.min: - next_attempt_time = self._update_state.last_attempted_manifest_download_time + datetime.timedelta(seconds=conf.get_autoupdate_frequency()) - else: - next_attempt_time = now - - if next_attempt_time > now: - return False - self._update_state.last_attempted_manifest_download_time = now - return True - - def should_update_agent(self, goal_state): - # First we check if we allowed to download the manifest - if not self._should_agent_attempt_manifest_download(): - return False - - # Fetch agent manifest to find largest version - self._agent_manifest = goal_state.fetch_agent_manifest(self._agent_family.name, self._agent_family.uris) - largest_version = self._get_largest_version(self._agent_manifest) - self._version = largest_version - - if not self._is_update_allowed_this_time(): - return False - - if self._version <= CURRENT_VERSION: - return False - - return True - - def log_new_agent_update_message(self): - msg = "Self-update discovered new agent version:{0} in agent manifest for goal state {1}, will update the agent before processing the goal state.".format( - str(self._version), self._gs_id) - logger.info(msg) - add_event(op=WALAEventOperation.AgentUpgrade, message=msg, log_event=False) - - def purge_extra_agents_from_disk(self): - known_agents = [CURRENT_VERSION, self._version] - self._purge_unknown_agents_from_disk(known_agents) - - def proceed_with_update(self): - if self._version > CURRENT_VERSION: - # In case of an upgrade, we don't need to exclude anything as the daemon will automatically - # start the next available highest version which would be the target version - raise AgentUpgradeExitException("Agent completed all update checks, exiting current process to upgrade to the new Agent version {0}".format(self._version)) \ No newline at end of file diff --git a/azurelinuxagent/ga/ga_version_updater.py b/azurelinuxagent/ga/ga_version_updater.py new file mode 100644 index 000000000..710af732b --- /dev/null +++ b/azurelinuxagent/ga/ga_version_updater.py @@ -0,0 +1,157 @@ +# Microsoft Azure Linux Agent +# +# Copyright 2020 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.6+ and Openssl 1.0+ + +import glob +import os +import shutil + +from azurelinuxagent.common import conf, logger +from azurelinuxagent.common.exception import AgentUpdateError +from azurelinuxagent.common.future import ustr +from azurelinuxagent.common.protocol.extensions_goal_state import GoalStateSource +from azurelinuxagent.common.utils import fileutil +from azurelinuxagent.common.utils.flexible_version import FlexibleVersion +from azurelinuxagent.common.version import AGENT_NAME, AGENT_DIR_PATTERN +from azurelinuxagent.ga.guestagent import GuestAgent + + +class VMEnabledRSMUpdates(TypeError): + """ + Thrown when agent needs to switch to RSM update mode if vm turn on RSM updates + """ + + +class VMDisabledRSMUpdates(TypeError): + """ + Thrown when agent needs to switch to self update mode if vm turn off RSM updates + """ + + +class GAVersionUpdater(object): + + def __init__(self, gs_id): + self._gs_id = gs_id + self._version = FlexibleVersion("0.0.0.0") # Initialize to zero and retrieve from goal state later stage + self._agent_manifest = None # Initialize to None and fetch from goal state at different stage for different updater + + def is_update_allowed_this_time(self): + """ + This function checks if we allowed to update the agent. + return false when we don't allow updates. + """ + raise NotImplementedError + + def check_and_switch_updater_if_changed(self, agent_family, gs_id): + """ + checks and raise the updater exception if we need to switch to self-update from rsm update or vice versa + @param agent_family: goal state agent family + @param gs_id: incarnation of the goal state + @return: VMDisabledRSMUpdates: raise when agent need to stop rsm updates and switch to self-update + VMEnabledRSMUpdates: raise when agent need to switch to rsm update + """ + raise NotImplementedError + + def retrieve_agent_version(self, agent_family, goal_state): + """ + This function fetches the agent version from the goal state for the given family. + @param agent_family: goal state agent family + @param goal_state: goal state + """ + raise NotImplementedError + + def is_retrieved_version_allowed_to_update(self, goal_state): + """ + Checks all base condition if new version allow to update. + @param goal_state: goal state + @return: True if allowed to update else False + """ + raise NotImplementedError + + def log_new_agent_update_message(self): + """ + This function logs the update message after we check agent allowed to update. + """ + raise NotImplementedError + + def purge_extra_agents_from_disk(self): + """ + Method remove the extra agents from disk. + """ + raise NotImplementedError + + def proceed_with_update(self): + """ + performs upgrade/downgrade + @return: AgentUpgradeExitException + """ + raise NotImplementedError + + @property + def version(self): + """ + Return version + """ + return self._version + + def download_and_get_new_agent(self, protocol, agent_family, goal_state): + """ + Function downloads the new agent and returns the downloaded version. + @param protocol: protocol object + @param agent_family: agent family + @param goal_state: goal state + @return: GuestAgent: downloaded agent + """ + if self._agent_manifest is None: # Fetch agent manifest if it's not already done + self._agent_manifest = goal_state.fetch_agent_manifest(agent_family.name, agent_family.uris) + package_to_download = self._get_agent_package_to_download(self._agent_manifest, self._version) + is_fast_track_goal_state = goal_state.extensions_goal_state.source == GoalStateSource.FastTrack + agent = GuestAgent.from_agent_package(package_to_download, protocol, is_fast_track_goal_state) + return agent + + def _get_agent_package_to_download(self, agent_manifest, version): + """ + Returns the package of the given Version found in the manifest. If not found, returns exception + """ + for pkg in agent_manifest.pkg_list.versions: + if FlexibleVersion(pkg.version) == version: + # Found a matching package, only download that one + return pkg + + raise AgentUpdateError("No matching package found in the agent manifest for version: {0} in goal state incarnation: {1}, " + "skipping agent update".format(str(version), self._gs_id)) + + @staticmethod + def _purge_unknown_agents_from_disk(known_agents): + """ + Remove from disk all directories and .zip files of unknown agents + """ + path = os.path.join(conf.get_lib_dir(), "{0}-*".format(AGENT_NAME)) + + for agent_path in glob.iglob(path): + try: + name = fileutil.trim_ext(agent_path, "zip") + m = AGENT_DIR_PATTERN.match(name) + if m is not None and FlexibleVersion(m.group(1)) not in known_agents: + if os.path.isfile(agent_path): + logger.info(u"Purging outdated Agent file {0}", agent_path) + os.remove(agent_path) + else: + logger.info(u"Purging outdated Agent directory {0}", agent_path) + shutil.rmtree(agent_path) + except Exception as e: + logger.warn(u"Purging {0} raised exception: {1}", agent_path, ustr(e)) diff --git a/azurelinuxagent/ga/rsm_version_updater.py b/azurelinuxagent/ga/rsm_version_updater.py new file mode 100644 index 000000000..b6dc994bb --- /dev/null +++ b/azurelinuxagent/ga/rsm_version_updater.py @@ -0,0 +1,159 @@ +# Microsoft Azure Linux Agent +# +# Copyright 2020 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.6+ and Openssl 1.0+ + +import datetime +import glob +import os + +from azurelinuxagent.common import conf, logger +from azurelinuxagent.common.event import add_event, WALAEventOperation +from azurelinuxagent.common.exception import AgentUpgradeExitException, AgentUpdateError +from azurelinuxagent.common.utils.flexible_version import FlexibleVersion +from azurelinuxagent.common.version import CURRENT_VERSION, AGENT_NAME +from azurelinuxagent.ga.ga_version_updater import GAVersionUpdater, VMDisabledRSMUpdates +from azurelinuxagent.ga.guestagent import GuestAgent + + +class RSMVersionUpdater(GAVersionUpdater): + def __init__(self, gs_id, daemon_version, last_attempted_rsm_version_update_time): + super(RSMVersionUpdater, self).__init__(gs_id) + self._daemon_version = daemon_version + self._last_attempted_rsm_version_update_time = last_attempted_rsm_version_update_time + + @staticmethod + def _get_all_agents_on_disk(): + path = os.path.join(conf.get_lib_dir(), "{0}-*".format(AGENT_NAME)) + return [GuestAgent.from_installed_agent(path=agent_dir) for agent_dir in glob.iglob(path) if + os.path.isdir(agent_dir)] + + def _get_available_agents_on_disk(self): + available_agents = [agent for agent in self._get_all_agents_on_disk() if agent.is_available] + return sorted(available_agents, key=lambda agent: agent.version, reverse=True) + + def is_update_allowed_this_time(self): + """ + update is allowed once per (as specified in the conf.get_autoupdate_frequency()) + If update allowed, we update the last_attempted_rsm_version_update_time to current time. + """ + now = datetime.datetime.now() + + if self._last_attempted_rsm_version_update_time != datetime.datetime.min: + next_attempt_time = self._last_attempted_rsm_version_update_time + datetime.timedelta( + seconds=conf.get_agentupdate_frequency()) + else: + next_attempt_time = now + + if next_attempt_time > now: + return False + self._last_attempted_rsm_version_update_time = now + # The time limit elapsed for us to allow updates. + return True + + def check_and_switch_updater_if_changed(self, agent_family, gs_id): + """ + Checks if there is a new goal state and decide if we need to continue with rsm update or switch to self-update. + Firstly it checks agent supports GA versioning or not. If not, we raise exception to switch to self-update. + if vm is enabled for RSM updates and continue with rsm update, otherwise we raise exception to switch to self-update. + if either isVersionFromRSM or isVMEnabledForRSMUpgrades is missing in the goal state, we ignore the update as we consider it as invalid goal state. + """ + if self._gs_id != gs_id: + self._gs_id = gs_id + if not conf.get_enable_ga_versioning(): + raise VMDisabledRSMUpdates() + + if agent_family.is_vm_enabled_for_rsm_upgrades is None: + raise AgentUpdateError( + "Received invalid goal state:{0}, missing isVMEnabledForRSMUpgrades property. So, skipping agent update".format( + gs_id)) + elif not agent_family.is_vm_enabled_for_rsm_upgrades: + raise VMDisabledRSMUpdates() + else: + if agent_family.is_version_from_rsm is None: + raise AgentUpdateError( + "Received invalid goal state:{0}, missing isVersionFromRSM property. So, skipping agent update".format( + gs_id)) + + def retrieve_agent_version(self, agent_family, goal_state): + """ + Get the agent version from the goal state + """ + if agent_family.version is None and agent_family.is_vm_enabled_for_rsm_upgrades and agent_family.is_version_from_rsm: + raise AgentUpdateError( + "Received invalid goal state:{0}, missing version property. So, skipping agent update".format(self._gs_id)) + self._version = FlexibleVersion(agent_family.version) + + def is_retrieved_version_allowed_to_update(self, agent_family): + """ + Once version retrieved from goal state, we check if we allowed to update for that version + allow update If new version not same as current version, not below than daemon version and if version is from rsm request + """ + + if not agent_family.is_version_from_rsm or self._version < self._daemon_version or self._version == CURRENT_VERSION: + return False + + return True + + def log_new_agent_update_message(self): + """ + This function logs the update message after we check version allowed to update. + """ + msg = "New agent version:{0} requested by RSM in Goal state {1}, will update the agent before processing the goal state.".format( + str(self._version), self._gs_id) + logger.info(msg) + add_event(op=WALAEventOperation.AgentUpgrade, message=msg, log_event=False) + + def purge_extra_agents_from_disk(self): + """ + Remove the agents( including rsm version if exists) from disk except current version. There is a chance that rsm version could exist and/or blacklisted + on previous update attempts. So we should remove it from disk in order to honor current rsm version update. + """ + known_agents = [CURRENT_VERSION] + self._purge_unknown_agents_from_disk(known_agents) + + def proceed_with_update(self): + """ + upgrade/downgrade to the new version. + Raises: AgentUpgradeExitException + """ + if self._version < CURRENT_VERSION: + # In case of a downgrade, we mark the current agent as bad version to avoid starting it back up ever again + # (the expectation here being that if we get request to a downgrade, + # there's a good reason for not wanting the current version). + prefix = "downgrade" + try: + # We should always have an agent directory for the CURRENT_VERSION + agents_on_disk = self._get_available_agents_on_disk() + current_agent = next(agent for agent in agents_on_disk if agent.version == CURRENT_VERSION) + msg = "Marking the agent {0} as bad version since a downgrade was requested in the GoalState, " \ + "suggesting that we really don't want to execute any extensions using this version".format( + CURRENT_VERSION) + logger.info(msg) + add_event(op=WALAEventOperation.AgentUpgrade, message=msg, log_event=False) + current_agent.mark_failure(is_fatal=True, reason=msg) + except StopIteration: + logger.warn( + "Could not find a matching agent with current version {0} to blacklist, skipping it".format( + CURRENT_VERSION)) + else: + # In case of an upgrade, we don't need to exclude anything as the daemon will automatically + # start the next available highest version which would be the target version + prefix = "upgrade" + raise AgentUpgradeExitException( + "Agent completed all update checks, exiting current process to {0} to the new Agent version {1}".format( + prefix, + self._version)) diff --git a/azurelinuxagent/ga/self_update_version_updater.py b/azurelinuxagent/ga/self_update_version_updater.py new file mode 100644 index 000000000..712f90490 --- /dev/null +++ b/azurelinuxagent/ga/self_update_version_updater.py @@ -0,0 +1,181 @@ +# Microsoft Azure Linux Agent +# +# Copyright 2020 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.6+ and Openssl 1.0+ + +import datetime + +from azurelinuxagent.common import conf, logger +from azurelinuxagent.common.event import add_event, WALAEventOperation +from azurelinuxagent.common.exception import AgentUpgradeExitException, AgentUpdateError +from azurelinuxagent.common.utils.flexible_version import FlexibleVersion +from azurelinuxagent.common.version import CURRENT_VERSION +from azurelinuxagent.ga.ga_version_updater import GAVersionUpdater, VMEnabledRSMUpdates + + +class SelfUpdateType(object): + """ + Enum for different modes of Self updates + """ + Hotfix = "Hotfix" + Regular = "Regular" + + +class SelfUpdateVersionUpdater(GAVersionUpdater): + def __init__(self, gs_id, last_attempted_manifest_download_time): + super(SelfUpdateVersionUpdater, self).__init__(gs_id) + self._last_attempted_manifest_download_time = last_attempted_manifest_download_time + self._last_attempted_self_update_time = datetime.datetime.min + + @staticmethod + def _get_largest_version(agent_manifest): + """ + Get the largest version from the agent manifest + """ + largest_version = FlexibleVersion("0.0.0.0") + for pkg in agent_manifest.pkg_list.versions: + pkg_version = FlexibleVersion(pkg.version) + if pkg_version > largest_version: + largest_version = pkg_version + return largest_version + + @staticmethod + def _get_agent_upgrade_type(version): + # We follow semantic versioning for the agent, if .. is same, then has changed. + # In this case, we consider it as a Hotfix upgrade. Else we consider it a Regular upgrade. + if version.major == CURRENT_VERSION.major and version.minor == CURRENT_VERSION.minor and version.patch == CURRENT_VERSION.patch: + return SelfUpdateType.Hotfix + return SelfUpdateType.Regular + + def _get_next_upgrade_times(self, now): + """ + Get the next upgrade times + return: Next Hotfix Upgrade Time, Next Regular Upgrade Time + """ + def get_next_process_time(last_val, frequency): + return now if last_val == datetime.datetime.min else last_val + datetime.timedelta(seconds=frequency) + + next_hotfix_time = get_next_process_time(self._last_attempted_self_update_time, + conf.get_self_update_hotfix_frequency()) + next_regular_time = get_next_process_time(self._last_attempted_self_update_time, + conf.get_self_update_regular_frequency()) + + return next_hotfix_time, next_regular_time + + def _is_new_agent_allowed_update(self): + """ + This method ensure that update is allowed only once per (hotfix/Regular) upgrade frequency + """ + now = datetime.datetime.now() + next_hotfix_time, next_regular_time = self._get_next_upgrade_times(now) + upgrade_type = self._get_agent_upgrade_type(self._version) + + if (upgrade_type == SelfUpdateType.Hotfix and next_hotfix_time <= now) or ( + upgrade_type == SelfUpdateType.Regular and next_regular_time <= now): + # Update the last upgrade check time even if no new agent is available for upgrade + self._last_attempted_self_update_time = now + return True + return False + + def _should_agent_attempt_manifest_download(self): + """ + The agent should attempt to download the manifest if + the agent has not attempted to download the manifest in the last 1 hour + If we allow update, we update the last attempted manifest download time + """ + now = datetime.datetime.now() + + if self._last_attempted_manifest_download_time != datetime.datetime.min: + next_attempt_time = self._last_attempted_manifest_download_time + datetime.timedelta(seconds=conf.get_agentupdate_frequency()) + else: + next_attempt_time = now + + if next_attempt_time > now: + return False + self._last_attempted_manifest_download_time = now + return True + + def is_update_allowed_this_time(self): + """ + Checks if we allowed download manifest as per manifest download frequency + """ + if not self._should_agent_attempt_manifest_download(): + return False + return True + + def check_and_switch_updater_if_changed(self, agent_family, gs_id): + """ + Checks if there is a new goal state and decide if we need to continue with self-update or switch to rsm update. + if vm is not enabled for RSM updates or agent not supports GA versioning then we continue with self update, otherwise we raise exception to switch to rsm update. + if isVersionFromRSM is missing but isVMEnabledForRSMUpgrades is present in the goal state, we ignore the update as we consider it as invalid goal state. + """ + if self._gs_id != gs_id: + self._gs_id = gs_id + if conf.get_enable_ga_versioning() and agent_family.is_vm_enabled_for_rsm_upgrades: + if agent_family.is_version_from_rsm is None: + raise AgentUpdateError( + "Received invalid goal state:{0}, missing isVersionFromRSM property. So, skipping agent update".format( + gs_id)) + else: + raise VMEnabledRSMUpdates() + + def retrieve_agent_version(self, agent_family, goal_state): + """ + Get the largest version from the agent manifest + """ + self._agent_manifest = goal_state.fetch_agent_manifest(agent_family.name, agent_family.uris) + largest_version = self._get_largest_version(self._agent_manifest) + self._version = largest_version + + def is_retrieved_version_allowed_to_update(self, agent_family): + """ + checks update is spread per (as specified in the conf.get_self_update_hotfix_frequency() or conf.get_self_update_regular_frequency()) + or if version below than current version + return false when we don't allow updates. + """ + if not self._is_new_agent_allowed_update(): + return False + + if self._version <= CURRENT_VERSION: + return False + + return True + + def log_new_agent_update_message(self): + """ + This function logs the update message after we check version allowed to update. + """ + msg = "Self-update discovered new agent version:{0} in agent manifest for goal state {1}, will update the agent before processing the goal state.".format( + str(self._version), self._gs_id) + logger.info(msg) + add_event(op=WALAEventOperation.AgentUpgrade, message=msg, log_event=False) + + def purge_extra_agents_from_disk(self): + """ + Remove the agents from disk except current version and new agent version if exists + """ + known_agents = [CURRENT_VERSION, self._version] + self._purge_unknown_agents_from_disk(known_agents) + + def proceed_with_update(self): + """ + upgrade to largest version. Downgrade is not supported. + Raises: AgentUpgradeExitException + """ + if self._version > CURRENT_VERSION: + # In case of an upgrade, we don't need to exclude anything as the daemon will automatically + # start the next available highest version which would be the target version + raise AgentUpgradeExitException("Agent completed all update checks, exiting current process to upgrade to the new Agent version {0}".format(self._version)) diff --git a/azurelinuxagent/ga/update.py b/azurelinuxagent/ga/update.py index 147402709..0becb7804 100644 --- a/azurelinuxagent/ga/update.py +++ b/azurelinuxagent/ga/update.py @@ -756,7 +756,7 @@ def log_if_agent_versioning_feature_disabled(): log_if_int_changed_from_default("OS.EnableFirewallPeriod", conf.get_enable_firewall_period()) if conf.get_autoupdate_enabled(): - log_if_int_changed_from_default("Autoupdate.Frequency", conf.get_autoupdate_frequency()) + log_if_int_changed_from_default("Autoupdate.Frequency", conf.get_agentupdate_frequency()) if conf.get_enable_fast_track(): log_if_op_disabled("Debug.EnableFastTrack", conf.get_enable_fast_track()) diff --git a/tests/ga/test_agent_update_handler.py b/tests/ga/test_agent_update_handler.py index 91d40c2a4..959fbc619 100644 --- a/tests/ga/test_agent_update_handler.py +++ b/tests/ga/test_agent_update_handler.py @@ -27,7 +27,7 @@ def setUp(self): clear_singleton_instances(ProtocolUtil) @contextlib.contextmanager - def _get_agent_update_handler(self, test_data=None, autoupdate_frequency=0.001, autoupdate_enabled=True, protocol_get_error=False): + def _get_agent_update_handler(self, test_data=None, agentupdate_frequency=0.001, autoupdate_enabled=True, protocol_get_error=False): # Default to DATA_FILE of test_data parameter raises the pylint warning # W0102: Dangerous default value DATA_FILE (builtins.dict) as argument (dangerous-default-value) test_data = DATA_FILE if test_data is None else test_data @@ -54,10 +54,10 @@ def put_handler(url, *args, **_): protocol.set_http_handlers(http_get_handler=get_handler, http_put_handler=put_handler) with patch("azurelinuxagent.common.conf.get_autoupdate_enabled", return_value=autoupdate_enabled): - with patch("azurelinuxagent.common.conf.get_autoupdate_frequency", return_value=autoupdate_frequency): + with patch("azurelinuxagent.common.conf.get_agentupdate_frequency", return_value=agentupdate_frequency): with patch("azurelinuxagent.common.conf.get_autoupdate_gafamily", return_value="Prod"): with patch("azurelinuxagent.common.conf.get_enable_ga_versioning", return_value=True): - with patch("azurelinuxagent.ga.agent_update_handler.add_event") as mock_telemetry: + with patch("azurelinuxagent.common.event.EventLogger.add_event") as mock_telemetry: agent_update_handler = get_agent_update_handler(protocol) agent_update_handler._protocol = protocol yield agent_update_handler, mock_telemetry @@ -122,12 +122,12 @@ def test_it_should_update_to_largest_version_if_ga_versioning_disabled(self): self._assert_agent_directories_exist_and_others_dont_exist(versions=[str(CURRENT_VERSION), "99999.0.0.0"]) self._assert_agent_exit_process_telemetry_emitted(ustr(context.exception.reason)) - def test_it_should_update_to_largest_version_if_time_window_not_elapsed(self): + def test_it_should_not_update_to_largest_version_if_time_window_not_elapsed(self): self.prepare_agents(count=1) data_file = DATA_FILE.copy() data_file["ga_manifest"] = "wire/ga_manifest_no_uris.xml" - with self._get_agent_update_handler(test_data=data_file) as (agent_update_handler, _): + with self._get_agent_update_handler(test_data=data_file, agentupdate_frequency=10) as (agent_update_handler, _): agent_update_handler.run(agent_update_handler._protocol.get_goal_state()) self.assertFalse(os.path.exists(self.agent_dir("99999.0.0.0")), "New agent directory should not be found") @@ -171,7 +171,7 @@ def test_it_should_not_agent_update_if_last_attempted_update_time_not_elapsed(se data_file = DATA_FILE.copy() data_file["ext_conf"] = "wire/ext_conf_rsm_version.xml" version = "5.2.0.1" - with self._get_agent_update_handler(test_data=data_file, autoupdate_frequency=10) as (agent_update_handler, mock_telemetry): + with self._get_agent_update_handler(test_data=data_file, agentupdate_frequency=10) as (agent_update_handler, mock_telemetry): agent_update_handler._protocol.mock_wire_data.set_version_in_agent_family(version) agent_update_handler._protocol.mock_wire_data.set_incarnation(2) agent_update_handler._protocol.client.update_goal_state() @@ -200,7 +200,7 @@ def test_it_should_not_download_manifest_again_if_last_attempted_download_time_n self.prepare_agents(count=1) data_file = DATA_FILE.copy() data_file['ext_conf'] = "wire/ext_conf.xml" - with self._get_agent_update_handler(test_data=data_file, autoupdate_frequency=10, protocol_get_error=True) as (agent_update_handler, _): + with self._get_agent_update_handler(test_data=data_file, agentupdate_frequency=10, protocol_get_error=True) as (agent_update_handler, _): # making multiple agent update attempts agent_update_handler.run(agent_update_handler._protocol.get_goal_state()) agent_update_handler.run(agent_update_handler._protocol.get_goal_state()) @@ -214,7 +214,7 @@ def test_it_should_download_manifest_if_last_attempted_download_time_is_elapsed( data_file = DATA_FILE.copy() data_file['ext_conf'] = "wire/ext_conf.xml" - with self._get_agent_update_handler(test_data=data_file, autoupdate_frequency=0.00001, protocol_get_error=True) as (agent_update_handler, _): + with self._get_agent_update_handler(test_data=data_file, agentupdate_frequency=0.00001, protocol_get_error=True) as (agent_update_handler, _): # making multiple agent update attempts agent_update_handler.run(agent_update_handler._protocol.get_goal_state()) agent_update_handler.run(agent_update_handler._protocol.get_goal_state()) @@ -402,7 +402,7 @@ def test_it_should_report_update_status_with_missing_rsm_version_error(self): vm_agent_update_status = agent_update_handler.get_vmagent_update_status() self.assertEqual(VMAgentUpdateStatuses.Error, vm_agent_update_status.status) self.assertEqual(1, vm_agent_update_status.code) - self.assertIn("VM Enabled for RSM upgrades but version is missing in Goal state", vm_agent_update_status.message) + self.assertIn("missing version property. So, skipping agent update", vm_agent_update_status.message) def test_it_should_not_log_same_error_next_hours(self): data_file = DATA_FILE.copy() @@ -438,17 +438,13 @@ def test_it_should_save_rsm_state_of_the_most_recent_goal_state(self): with self.assertRaises(AgentUpgradeExitException): agent_update_handler.run(agent_update_handler._protocol.get_goal_state()) - state_file = os.path.join(conf.get_lib_dir(), "rsm_version.json") + state_file = os.path.join(conf.get_lib_dir(), "rsm_update.json") self.assertTrue(os.path.exists(state_file), "The rsm properties was not saved (can't find {0})".format(state_file)) with open(state_file, "r") as state_file_: state = json.load(state_file_) - self.assertTrue(state["isVersionFromRSM"], "{0} does not contain True".format(state_file)) - self.assertTrue(state["isVMEnabledForRSMUpgrades"], "{0} does not contain True".format(state_file)) - self.assertEqual(agent_update_handler._is_version_from_rsm, state["isVersionFromRSM"], "{0} does not contain the expected value".format(state_file)) - self.assertEqual(agent_update_handler._is_vm_enabled_for_rsm_upgrades, state["isVMEnabledForRSMUpgrades"], "{0} does not contain the expected value".format(state_file)) - self.assertEqual(agent_update_handler._protocol.get_goal_state().extensions_goal_state.created_on_timestamp, state["timestamp"], "{0} does not contain the expected value".format(state_file)) + self.assertTrue(state["isLastUpdateWithRSM"], "{0} does not contain True".format(state_file)) # check if state gets updated if most recent goal state has different values agent_update_handler._protocol.mock_wire_data.set_extension_config_is_vm_enabled_for_rsm_upgrades("False") @@ -461,8 +457,4 @@ def test_it_should_save_rsm_state_of_the_most_recent_goal_state(self): with open(state_file, "r") as state_file_: state = json.load(state_file_) - self.assertTrue(state["isVersionFromRSM"], "{0} does not contain True".format(state_file)) - self.assertFalse(state["isVMEnabledForRSMUpgrades"], "{0} does not contain False".format(state_file)) - self.assertEqual(agent_update_handler._is_version_from_rsm, state["isVersionFromRSM"], "{0} does not contain the expected value".format(state_file)) - self.assertEqual(agent_update_handler._is_vm_enabled_for_rsm_upgrades, state["isVMEnabledForRSMUpgrades"], "{0} does not contain the expected value".format(state_file)) - self.assertEqual(agent_update_handler._protocol.get_goal_state().extensions_goal_state.created_on_timestamp, state["timestamp"], "{0} does not contain the expected value".format(state_file)) \ No newline at end of file + self.assertFalse(state["isLastUpdateWithRSM"], "{0} does not contain False".format(state_file)) diff --git a/tests/ga/test_update.py b/tests/ga/test_update.py index 8ef6f4146..3aff3bc94 100644 --- a/tests/ga/test_update.py +++ b/tests/ga/test_update.py @@ -1288,7 +1288,7 @@ def update_goal_state_and_run_handler(autoupdate_enabled=True): update_status = protocol.aggregate_status['aggregateStatus']['guestAgentStatus']["updateStatus"] self.assertEqual(VMAgentUpdateStatuses.Error, update_status['status'], "Status should be an error") self.assertEqual(update_status['code'], 1, "incorrect code reported") - self.assertIn("VM Enabled for RSM upgrades but version is missing in Goal state", update_status['formattedMessage']['message'], "incorrect message reported") + self.assertIn("missing version property. So, skipping agent update", update_status['formattedMessage']['message'], "incorrect message reported") # Case 2: rsm version in GS == Current Version; updateStatus should be Success protocol.mock_wire_data.set_extension_config("wire/ext_conf_rsm_version.xml") @@ -1430,10 +1430,10 @@ def test_run_emits_restart_event(self): class TestAgentUpgrade(UpdateTestCase): @contextlib.contextmanager - def create_conf_mocks(self, autoupdate_frequency, hotfix_frequency, normal_frequency): + def create_conf_mocks(self, agentupdate_frequency, hotfix_frequency, normal_frequency): # Disabling extension processing to speed up tests as this class deals with testing agent upgrades with patch("azurelinuxagent.common.conf.get_extensions_enabled", return_value=False): - with patch("azurelinuxagent.common.conf.get_autoupdate_frequency", return_value=autoupdate_frequency): + with patch("azurelinuxagent.common.conf.get_agentupdate_frequency", return_value=agentupdate_frequency): with patch("azurelinuxagent.common.conf.get_self_update_hotfix_frequency", return_value=hotfix_frequency): with patch("azurelinuxagent.common.conf.get_self_update_regular_frequency", return_value=normal_frequency): with patch("azurelinuxagent.common.conf.get_autoupdate_gafamily", return_value="Prod"):