diff --git a/azurelinuxagent/common/conf.py b/azurelinuxagent/common/conf.py index 167d520d09..57d6c9d280 100644 --- a/azurelinuxagent/common/conf.py +++ b/azurelinuxagent/common/conf.py @@ -622,20 +622,20 @@ def get_etp_collection_period(conf=__conf__): return conf.get_int("Debug.EtpCollectionPeriod", 300) -def get_hotfix_upgrade_frequency(conf=__conf__): +def get_self_update_hotfix_frequency(conf=__conf__): """ - Determines the frequency to check for Hotfix upgrades (. version changed in new upgrades). + Determines the frequency to check for Hotfix upgrades ( version changed in new upgrades). NOTE: This option is experimental and may be removed in later versions of the Agent. """ - return conf.get_int("Debug.AutoUpdateHotfixFrequency", 4 * 60 * 60) + return conf.get_int("Debug.SelfUpdateHotfixFrequency", 4 * 60 * 60) -def get_normal_upgrade_frequency(conf=__conf__): +def get_self_update_regular_frequency(conf=__conf__): """ - Determines the frequency to check for Normal upgrades (. version changed in new upgrades). + Determines the frequency to check for regular upgrades (.. version changed in new upgrades). NOTE: This option is experimental and may be removed in later versions of the Agent. """ - return conf.get_int("Debug.AutoUpdateNormalFrequency", 24 * 60 * 60) + return conf.get_int("Debug.SelfUpdateRegularFrequency", 24 * 60 * 60) def get_enable_ga_versioning(conf=__conf__): @@ -643,7 +643,7 @@ def get_enable_ga_versioning(conf=__conf__): If True, the agent looks for rsm updates(checking requested version in GS) otherwise it will fall back to self-update and finds the highest version from PIR. NOTE: This option is experimental and may be removed in later versions of the Agent. """ - return conf.get_switch("Debug.EnableGAVersioning", False) + return conf.get_switch("Debug.EnableGAVersioning", True) def get_firewall_rules_log_period(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 a8bfa25054..2b98819a2a 100644 --- a/azurelinuxagent/common/protocol/extensions_goal_state_from_extensions_config.py +++ b/azurelinuxagent/common/protocol/extensions_goal_state_from_extensions_config.py @@ -61,9 +61,16 @@ def _parse_extensions_config(self, xml_text, wire_client): for ga_family in ga_families: name = findtext(ga_family, "Name") version = findtext(ga_family, "Version") + is_version_from_rsm = findtext(ga_family, "IsVersionFromRSM") + 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) + family.version = version + if is_version_from_rsm is not None: # checking None because converting string to lowercase + family.is_version_from_rsm = is_version_from_rsm.lower() == "true" + if is_vm_enabled_for_rsm_upgrades is not None: # checking None because converting string to lowercase + family.is_vm_enabled_for_rsm_upgrades = is_vm_enabled_for_rsm_upgrades.lower() == "true" for uri in uris: family.uris.append(gettext(uri)) self._agent_families.append(family) 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 664d1e868f..041ddedcda 100644 --- a/azurelinuxagent/common/protocol/extensions_goal_state_from_vm_settings.py +++ b/azurelinuxagent/common/protocol/extensions_goal_state_from_vm_settings.py @@ -243,6 +243,8 @@ def _parse_agent_manifests(self, vm_settings): # { # "name": "Prod", # "version": "9.9.9.9", + # "isVersionFromRSM": true, + # "isVMEnabledForRSMUpgrades": true, # "uris": [ # "https://zrdfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Prod_uscentraleuap_manifest.xml", # "https://ardfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Prod_uscentraleuap_manifest.xml" @@ -267,10 +269,15 @@ def _parse_agent_manifests(self, vm_settings): for family in families: name = family["name"] version = family.get("version") + is_version_from_rsm = family.get("isVersionFromRSM") + is_vm_enabled_for_rsm_upgrades = family.get("isVMEnabledForRSMUpgrades") uris = family.get("uris") if uris is None: uris = [] - agent_family = VMAgentFamily(name, version) + agent_family = VMAgentFamily(name) + agent_family.version = version + agent_family.is_version_from_rsm = is_version_from_rsm + agent_family.is_vm_enabled_for_rsm_upgrades = is_vm_enabled_for_rsm_upgrades for u in uris: agent_family.uris.append(u) self._agent_families.append(agent_family) diff --git a/azurelinuxagent/common/protocol/restapi.py b/azurelinuxagent/common/protocol/restapi.py index 725e2d7bb4..35b40cf13c 100644 --- a/azurelinuxagent/common/protocol/restapi.py +++ b/azurelinuxagent/common/protocol/restapi.py @@ -22,7 +22,6 @@ from azurelinuxagent.common.datacontract import DataContract, DataContractList from azurelinuxagent.common.future import ustr -from azurelinuxagent.common.utils.flexible_version import FlexibleVersion from azurelinuxagent.common.utils.textutil import getattrib from azurelinuxagent.common.version import DISTRO_VERSION, DISTRO_NAME, CURRENT_VERSION @@ -69,23 +68,16 @@ def __init__(self): class VMAgentFamily(object): - def __init__(self, name, version=None): + def __init__(self, name): self.name = name - # This is the Requested version as specified by the Goal State, it defaults to 0.0.0.0 if not specified in GS - self.requested_version_string = VERSION_0 if version is None else version - self.uris = [] - - @property - def requested_version(self): - return FlexibleVersion(self.requested_version_string) + # Two-state: None, string. Set to None if version not specified in the GS + self.version = None + # Tri-state: None, True, False. Set to None if this property not specified in the GS. + self.is_version_from_rsm = None + # Tri-state: None, True, False. Set to None if this property not specified in the GS. + self.is_vm_enabled_for_rsm_upgrades = None - @property - def is_requested_version_specified(self): - """ - If we don't get any requested_version from the GS, we default it to 0.0.0.0. - This property identifies if a requested Version was passed in the GS or not. - """ - return self.requested_version > FlexibleVersion(VERSION_0) + self.uris = [] def __repr__(self): return self.__str__() diff --git a/azurelinuxagent/ga/agent_update_handler.py b/azurelinuxagent/ga/agent_update_handler.py index a650f110ac..ed157bdf5b 100644 --- a/azurelinuxagent/ga/agent_update_handler.py +++ b/azurelinuxagent/ga/agent_update_handler.py @@ -1,146 +1,125 @@ +# 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 -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.logger import LogLevel -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 +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, GAUpdateReportState +from azurelinuxagent.common.version import get_daemon_version +from azurelinuxagent.ga.ga_version_updater import RSMUpdates +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 AgentUpgradeType(object): - """ - Enum for different modes of Agent Upgrade - """ - Hotfix = "Hotfix" - Normal = "Normal" - - -class AgentUpdateHandlerUpdateState(object): +class AgentUpdateHandler(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. + 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. + Self update: We fallback to this if above is condition not met. This update to the largest version available in the manifest + 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. + + [Note: 1.0.8.147 is the minimum supported version of HGPA which will have the isVersionFromRSM and isVMEnabledForRSMUpgrades properties in vmsettings.] """ - def __init__(self): - self.last_attempted_requested_version_update_time = datetime.datetime.min - self.last_attempted_hotfix_update_time = datetime.datetime.min - self.last_attempted_normal_update_time = datetime.datetime.min - self.last_attempted_manifest_download_time = datetime.datetime.min - - -class AgentUpdateHandler(object): - 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._is_requested_version_update = True # This is to track the current update type(requested version or self update) - self.update_state = AgentUpdateHandlerUpdateState() - - def __check_if_agent_update_allowed_and_update_next_upgrade_times(self, requested_version): - """ - requested version update: - update is allowed once per (as specified in the conf.get_autoupdate_frequency()) - return false when we don't allow updates. - largest version update(self-update): - update is allowed once per (as specified in the conf.get_hotfix_upgrade_frequency() or conf.get_normal_upgrade_frequency()) - return false when we don't allow updates. - """ - now = datetime.datetime.now() - - if self._is_requested_version_update: - if self.update_state.last_attempted_requested_version_update_time != datetime.datetime.min: - next_attempt_time = self.update_state.last_attempted_requested_version_update_time + datetime.timedelta(seconds=conf.get_autoupdate_frequency()) - else: - next_attempt_time = now - - if next_attempt_time > now: - return False - # The time limit elapsed for us to allow updates. - self.update_state.last_attempted_requested_version_update_time = now - return True - else: - next_hotfix_time, next_normal_time = self.__get_next_upgrade_times(now) - upgrade_type = self.__get_agent_upgrade_type(requested_version) - - if (upgrade_type == AgentUpgradeType.Hotfix and next_hotfix_time <= now) or ( - upgrade_type == AgentUpgradeType.Normal and next_normal_time <= now): - # Update the last upgrade check time even if no new agent is available for upgrade - self.update_state.last_attempted_hotfix_update_time = now - self.update_state.last_attempted_normal_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 - """ - now = datetime.datetime.now() + self._ga_family_type = conf.get_autoupdate_gafamily() + self._daemon_version = self._get_daemon_version_for_update() + self._last_attempted_update_error_msg = "" - 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()) + # 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: - next_attempt_time = now - - if next_attempt_time > now: - return False - self.update_state.last_attempted_manifest_download_time = now - return True - - @staticmethod - def __get_agent_upgrade_type(requested_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 Normal upgrade. - if requested_version.major == CURRENT_VERSION.major and requested_version.minor == CURRENT_VERSION.minor and requested_version.patch == CURRENT_VERSION.patch: - return AgentUpgradeType.Hotfix - return AgentUpgradeType.Normal + self._updater = RSMVersionUpdater(self._gs_id, self._daemon_version) @staticmethod - def __get_daemon_version_for_update(): + def _get_daemon_version_for_update(): daemon_version = get_daemon_version() if daemon_version != FlexibleVersion(VERSION_0): return daemon_version - # We return 0.0.0.0 if we failed to retrieve daemon version. In that case, + # We return 0.0.0.0 if daemon version is not specified. In that case, # use the min version as 2.2.53 as we started setting the daemon version starting 2.2.53. return FlexibleVersion("2.2.53") - def __get_next_upgrade_times(self, now): + @staticmethod + def _get_rsm_update_state_file(): """ - Get the next upgrade times - return: Next Hotfix Upgrade Time, Next Normal Upgrade Time + This file keeps if last attempted update is rsm or not. """ + return os.path.join(conf.get_lib_dir(), "rsm_update.json") - def get_next_process_time(last_val, frequency): - return now if last_val == datetime.datetime.min else last_val + datetime.timedelta(seconds=frequency) + def _save_rsm_update_state(self): + """ + Save the rsm state empty file when we switch to RSM + """ + try: + with open(self._get_rsm_update_state_file(), "w"): + pass + except Exception as e: + logger.warn("Error creating the RSM state ({0}): {1}", self._get_rsm_update_state_file(), ustr(e)) - next_hotfix_time = get_next_process_time(self.update_state.last_attempted_hotfix_update_time, - conf.get_hotfix_upgrade_frequency()) - next_normal_time = get_next_process_time(self.update_state.last_attempted_normal_update_time, - conf.get_normal_upgrade_frequency()) + def _remove_rsm_update_state(self): + """ + Remove the rsm state file when we switch to self-update + """ + try: + if os.path.exists(self._get_rsm_update_state_file()): + os.remove(self._get_rsm_update_state_file()) + except Exception as e: + logger.warn("Error removing the RSM state ({0}): {1}", self._get_rsm_update_state_file(), ustr(e)) - return next_hotfix_time, next_normal_time + def _get_is_last_update_with_rsm(self): + """ + Returns True if state file exists as this consider as last update with RSM is true + """ + return os.path.exists(self._get_rsm_update_state_file()) - def __get_agent_family_manifests(self, goal_state): + 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 = [] @@ -151,211 +130,57 @@ def __get_agent_family_manifests(self, goal_state): agent_family_manifests.append(m) if not family_found: - raise AgentUpdateError(u"Agent family: {0} not found in the goal state, skipping agent update".format(family)) + raise AgentUpdateError(u"Agent family: {0} not found in the goal state: {1}, skipping agent update".format(family, self._gs_id)) 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)) + u"No manifest links found for agent family: {0} for goal state: {1}, skipping agent update".format( + family, self._gs_id)) return agent_family_manifests[0] - @staticmethod - def __get_requested_version(agent_family): - """ - Get the requested version from agent family - Returns: Requested version if supported and available in the GS - None if requested version missing or GA versioning not enabled - """ - if conf.get_enable_ga_versioning() and agent_family.is_requested_version_specified: - if agent_family.requested_version is not None: - return FlexibleVersion(agent_family.requested_version) - return None - - @staticmethod - def __get_largest_version(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 + def run(self, goal_state, ext_gs_updated): - def __download_and_get_agent(self, goal_state, agent_family, agent_manifest, requested_version): - """ - This function downloads the new agent(requested version) and returns the downloaded version. - """ - if agent_manifest is None: # Fetch agent manifest if it's not already done - agent_manifest = goal_state.fetch_agent_manifest(agent_family.name, agent_family.uris) - package_to_download = self.__get_agent_package_to_download(agent_manifest, requested_version) - is_fast_track_goal_state = goal_state.extensions_goal_state.source == GoalStateSource.FastTrack - agent = GuestAgent.from_agent_package(package_to_download, self._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 requested version: {0} in goal state incarnation: {1}, " - "skipping agent update".format(str(version), self._gs_id)) - - @staticmethod - def __purge_extra_agents_from_disk(current_version, known_agents): - """ - Remove from disk all directories and .zip files of unknown agents - (without removing the current, running agent). - """ - path = os.path.join(conf.get_lib_dir(), "{0}-*".format(AGENT_NAME)) - - known_versions = [agent.version for agent in known_agents] - known_versions.append(current_version) - - 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_versions: - 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)) - - def __proceed_with_update(self, requested_version): - """ - If requested version is specified, upgrade/downgrade to the specified version. - Raises: AgentUpgradeExitException - """ - if requested_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 = AgentUpdateHandler.__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) - self.__log_event(LogLevel.INFO, msg) - 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 update found, exiting current process to {0} to the new Agent version {1}".format(prefix, requested_version)) - - @staticmethod - def __get_available_agents_on_disk(): - available_agents = [agent for agent in AgentUpdateHandler.__get_all_agents_on_disk() if agent.is_available] - return sorted(available_agents, key=lambda agent: agent.version, reverse=True) - - @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 __check_if_downgrade_is_requested_and_allowed(self, requested_version): - """ - Don't allow downgrades for self-update version - Note: The intention of this check is to keep the original behavior of self-update as it is. - """ - if not self._is_requested_version_update: - if requested_version < CURRENT_VERSION: - return False - return True - - @staticmethod - def __log_event(level, msg, success=True): - if level == LogLevel.INFO: - logger.info(msg) - elif level == LogLevel.WARNING: - logger.warn(msg) - elif level == LogLevel.ERROR: - logger.error(msg) - add_event(op=WALAEventOperation.AgentUpgrade, is_success=success, message=msg, log_event=False) - - 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 - self._gs_id = goal_state.extensions_goal_state.id - agent_family = self.__get_agent_family_manifests(goal_state) - requested_version = self.__get_requested_version(agent_family) - agent_manifest = None # This is to make sure fetch agent manifest once per update - warn_msg = "" - if requested_version is None: - # Do not proceed with update if self-update needs to download the manifest again with in an hour - if not self.__should_agent_attempt_manifest_download(): - return - if conf.get_enable_ga_versioning(): # log the warning only when ga versioning is enabled - # TODO: Need to revisit this msg when version is missing in Goal state. We may need to handle better way to report the error - warn_msg = "Missing requested version in agent family: {0} for incarnation: {1}, fallback to largest version update".format(self._ga_family, self._gs_id) - GAUpdateReportState.report_error_msg = warn_msg - agent_manifest = goal_state.fetch_agent_manifest(agent_family.name, agent_family.uris) - requested_version = self.__get_largest_version(agent_manifest) - self._is_requested_version_update = False - else: - self._is_requested_version_update = True - # Save the requested version to report back - GAUpdateReportState.report_expected_version = requested_version - # Remove the missing requested version warning once requested version becomes available - if "Missing requested version" in GAUpdateReportState.report_error_msg: - GAUpdateReportState.report_error_msg = "" - - # Check if an update is allowed and update next upgrade times even if no new agent is available for upgrade - if not self.__check_if_agent_update_allowed_and_update_next_upgrade_times(requested_version): + # verify if agent update is allowed this time (RSM checks new goal state; self-update checks manifest download interval) + if not self._updater.is_update_allowed_this_time(ext_gs_updated): return - if requested_version == CURRENT_VERSION: - return + self._gs_id = goal_state.extensions_goal_state.id + agent_family = self._get_agent_family_manifest(goal_state) - if warn_msg != "": - self.__log_event(LogLevel.WARNING, warn_msg) + # updater will return RSM enabled or disabled if we need to switch to self-update or rsm update + updater_mode = self._updater.check_and_switch_updater_if_changed(agent_family, self._gs_id, ext_gs_updated) - # Downgrades are not allowed for self-update version - if not self.__check_if_downgrade_is_requested_and_allowed(requested_version): - return + if updater_mode == RSMUpdates.Disabled: + 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(self._gs_id, datetime.datetime.now()) + self._remove_rsm_update_state() - daemon_version = self.__get_daemon_version_for_update() - if requested_version < daemon_version: - # Don't process the update if the requested version is less than daemon version, - # as historically we don't support downgrades below daemon versions. So daemon will not pickup that requested version rather start with - # installed latest version again. When that happens agent go into loop of downloading the requested version, exiting and start again with same version. - # - raise AgentUpdateError("The Agent received a request to downgrade to version {0}, but downgrading to a version less than " - "the Agent installed on the image ({1}) is not supported. Skipping downgrade.".format(requested_version, daemon_version)) + if updater_mode == RSMUpdates.Enabled: + 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(self._gs_id, self._daemon_version) + self._save_rsm_update_state() - # Todo: Need to update the message when we fix RSM stuff - 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(requested_version), self._gs_id) - self.__log_event(LogLevel.INFO, msg) - - agent = self.__download_and_get_agent(goal_state, agent_family, agent_manifest, requested_version) + self._updater.retrieve_agent_version(agent_family, goal_state) + if not self._updater.is_retrieved_version_allowed_to_update(agent_family): + return + 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)) - self.__log_event(LogLevel.WARNING, msg) - return - - # We delete the directory and the zip package from the filesystem except current version and target version - self.__purge_extra_agents_from_disk(CURRENT_VERSION, known_agents=[agent]) - self.__proceed_with_update(requested_version) - + raise AgentUpdateError(msg) + self._updater.proceed_with_update() except Exception as err: if isinstance(err, AgentUpgradeExitException): @@ -364,25 +189,28 @@ def run(self, goal_state): error_msg = ustr(err) else: error_msg = "Unable to update Agent: {0}".format(textutil.format_exception(err)) - self.__log_event(LogLevel.WARNING, error_msg, success=False) - if "Missing requested version" not in GAUpdateReportState.report_error_msg: - GAUpdateReportState.report_error_msg = error_msg + logger.warn(error_msg) + add_event(op=WALAEventOperation.AgentUpgrade, is_success=False, message=error_msg, log_event=False) + 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 requested version + 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(): - if not GAUpdateReportState.report_error_msg: + if not self._last_attempted_update_error_msg: status = VMAgentUpdateStatuses.Success code = 0 else: status = VMAgentUpdateStatuses.Error code = 1 - return VMAgentUpdateStatus(expected_version=str(GAUpdateReportState.report_expected_version), status=status, code=code, message=GAUpdateReportState.report_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: - self.__log_event(LogLevel.WARNING, "Unable to report agent update status: {0}".format( - textutil.format_exception(err)), success=False) + 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 diff --git a/azurelinuxagent/ga/ga_version_updater.py b/azurelinuxagent/ga/ga_version_updater.py new file mode 100644 index 0000000000..0d3f639f25 --- /dev/null +++ b/azurelinuxagent/ga/ga_version_updater.py @@ -0,0 +1,156 @@ +# 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 RSMUpdates(object): + """ + Enum for switching between RSM updates and self updates + """ + Enabled = "Enabled" + Disabled = "Disabled" + + +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, ext_gs_updated): + """ + This function checks if we allowed to update the agent. + @param ext_gs_updated: True if extension goal state updated else False + @return false when we don't allow updates. + """ + raise NotImplementedError + + def check_and_switch_updater_if_changed(self, agent_family, gs_id, ext_gs_updated): + """ + checks and raise the updater exception if we need to switch to self-update from rsm update or vice versa + @param agent_family: agent family + @param gs_id: incarnation of the goal state + @param ext_gs_updated: True if extension goal state updated else False + @return: RSMUpdates.Disabled: return when agent need to stop rsm updates and switch to self-update + RSMUpdates.Enabled: return when agent need to switch to rsm update + None: return when no need to switch + """ + 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: agent family + @param goal_state: goal state + """ + raise NotImplementedError + + def is_retrieved_version_allowed_to_update(self, agent_family): + """ + Checks all base condition if new version allow to update. + @param agent_family: agent family + @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/guestagent.py b/azurelinuxagent/ga/guestagent.py index 56f3142447..35472c74a6 100644 --- a/azurelinuxagent/ga/guestagent.py +++ b/azurelinuxagent/ga/guestagent.py @@ -18,15 +18,6 @@ MAX_FAILURE = 3 # Max failure allowed for agent before declare bad agent -class GAUpdateReportState(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 and might be modified by external classes. - """ - report_error_msg = "" - report_expected_version = FlexibleVersion("0.0.0.0") - - class GuestAgent(object): def __init__(self, path, pkg, protocol, is_fast_track_goal_state): """ @@ -74,8 +65,6 @@ def __init__(self, path, pkg, protocol, is_fast_track_goal_state): msg = u"Agent {0} install failed with exception:".format( self.name) detailed_msg = '{0} {1}'.format(msg, textutil.format_exception(e)) - if "Missing requested version" not in GAUpdateReportState.report_error_msg: - GAUpdateReportState.report_error_msg = detailed_msg # capture the download errors to report back add_event( AGENT_NAME, version=self.version, diff --git a/azurelinuxagent/ga/rsm_version_updater.py b/azurelinuxagent/ga/rsm_version_updater.py new file mode 100644 index 0000000000..dc972c1c7a --- /dev/null +++ b/azurelinuxagent/ga/rsm_version_updater.py @@ -0,0 +1,147 @@ +# 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 + +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, RSMUpdates +from azurelinuxagent.ga.guestagent import GuestAgent + + +class RSMVersionUpdater(GAVersionUpdater): + def __init__(self, gs_id, daemon_version): + super(RSMVersionUpdater, self).__init__(gs_id) + 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, ext_gs_updated): + """ + RSM update allowed if we have a new goal state + """ + return ext_gs_updated + + def check_and_switch_updater_if_changed(self, agent_family, gs_id, ext_gs_updated): + """ + 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 return rsm updates disabled to switch to self-update. + if vm is enabled for RSM updates and continue with rsm update, otherwise we return rsm updates disabled to switch to self-update. + if either isVersionFromRSM or isVMEnabledForRSMUpgrades or version is missing in the goal state, we ignore the update as we consider it as invalid goal state. + """ + if ext_gs_updated: + self._gs_id = gs_id + if not conf.get_enable_ga_versioning(): + return RSMUpdates.Disabled + + 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( + self._gs_id)) + elif not agent_family.is_vm_enabled_for_rsm_upgrades: + return RSMUpdates.Disabled + 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( + self._gs_id)) + if agent_family.version is None: + raise AgentUpdateError( + "Received invalid goal state:{0}, missing version property. So, skipping agent update".format( + self._gs_id)) + + return None + + def retrieve_agent_version(self, agent_family, goal_state): + """ + Get the agent version from the goal state + """ + 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 0000000000..2b04fd5c22 --- /dev/null +++ b/azurelinuxagent/ga/self_update_version_updater.py @@ -0,0 +1,186 @@ +# 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, RSMUpdates + + +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 + + @staticmethod + def _get_next_process_time(last_val, frequency, now): + """ + Get the next upgrade time + """ + return now if last_val == datetime.datetime.min else last_val + datetime.timedelta(seconds=frequency) + + 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() + upgrade_type = self._get_agent_upgrade_type(self._version) + if upgrade_type == SelfUpdateType.Hotfix: + next_update_time = self._get_next_process_time(self._last_attempted_self_update_time, + conf.get_self_update_hotfix_frequency(), now) + else: + next_update_time = self._get_next_process_time(self._last_attempted_self_update_time, + conf.get_self_update_regular_frequency(), now) + + if next_update_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_autoupdate_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, ext_gs_updated): + """ + 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, ext_gs_updated): + """ + 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 rsm enabled 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 ext_gs_updated: + self._gs_id = gs_id + if conf.get_enable_ga_versioning() and agent_family.is_vm_enabled_for_rsm_upgrades is not None 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( + self._gs_id)) + else: + if agent_family.version is None: + raise AgentUpdateError( + "Received invalid goal state:{0}, missing version property. So, skipping agent update".format( + self._gs_id)) + return RSMUpdates.Enabled + + return None + + 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 6e7b5b917c..88267b75e2 100644 --- a/azurelinuxagent/ga/update.py +++ b/azurelinuxagent/ga/update.py @@ -547,21 +547,20 @@ def _processing_new_extensions_goal_state(self): """ True if we are currently processing a new extensions goal state """ - egs = self._goal_state.extensions_goal_state - return self._goal_state is not None and egs.id != self._last_extensions_gs_id and not egs.is_outdated + return self._goal_state is not None and self._goal_state.extensions_goal_state.id != self._last_extensions_gs_id and not self._goal_state.extensions_goal_state.is_outdated def _process_goal_state(self, exthandlers_handler, remote_access_handler, agent_update_handler): protocol = exthandlers_handler.protocol # update self._goal_state if not self._try_update_goal_state(protocol): - agent_update_handler.run(self._goal_state) + agent_update_handler.run(self._goal_state, self._processing_new_extensions_goal_state()) # status reporting should be done even when the goal state is not updated self._report_status(exthandlers_handler, agent_update_handler) return # check for agent updates - agent_update_handler.run(self._goal_state) + agent_update_handler.run(self._goal_state, self._processing_new_extensions_goal_state()) try: if self._processing_new_extensions_goal_state(): diff --git a/tests/common/protocol/test_extensions_goal_state_from_extensions_config.py b/tests/common/protocol/test_extensions_goal_state_from_extensions_config.py index 61380a46f7..2a9acff659 100644 --- a/tests/common/protocol/test_extensions_goal_state_from_extensions_config.py +++ b/tests/common/protocol/test_extensions_goal_state_from_extensions_config.py @@ -60,3 +60,43 @@ def test_its_source_channel_should_be_wire_server(self): extensions_goal_state = protocol.get_goal_state().extensions_goal_state self.assertEqual(GoalStateChannel.WireServer, extensions_goal_state.channel, "The channel is incorrect") + + def test_it_should_parse_is_version_from_rsm_properly(self): + with mock_wire_protocol(wire_protocol_data.DATA_FILE) as protocol: + agent_families = protocol.get_goal_state().extensions_goal_state.agent_families + for family in agent_families: + self.assertIsNone(family.is_version_from_rsm, "is_version_from_rsm should be None") + + data_file = wire_protocol_data.DATA_FILE.copy() + data_file["ext_conf"] = "hostgaplugin/ext_conf-agent_family_version.xml" + with mock_wire_protocol(data_file) as protocol: + agent_families = protocol.get_goal_state().extensions_goal_state.agent_families + for family in agent_families: + self.assertTrue(family.is_version_from_rsm, "is_version_from_rsm should be True") + + data_file = wire_protocol_data.DATA_FILE.copy() + data_file["ext_conf"] = "hostgaplugin/ext_conf-rsm_version_properties_false.xml" + with mock_wire_protocol(data_file) as protocol: + agent_families = protocol.get_goal_state().extensions_goal_state.agent_families + for family in agent_families: + self.assertFalse(family.is_version_from_rsm, "is_version_from_rsm should be False") + + def test_it_should_parse_is_vm_enabled_for_rsm_upgrades(self): + with mock_wire_protocol(wire_protocol_data.DATA_FILE) as protocol: + agent_families = protocol.get_goal_state().extensions_goal_state.agent_families + for family in agent_families: + self.assertIsNone(family.is_vm_enabled_for_rsm_upgrades, "is_vm_enabled_for_rsm_upgrades should be None") + + data_file = wire_protocol_data.DATA_FILE.copy() + data_file["ext_conf"] = "hostgaplugin/ext_conf-agent_family_version.xml" + with mock_wire_protocol(data_file) as protocol: + agent_families = protocol.get_goal_state().extensions_goal_state.agent_families + for family in agent_families: + self.assertTrue(family.is_vm_enabled_for_rsm_upgrades, "is_vm_enabled_for_rsm_upgrades should be True") + + data_file = wire_protocol_data.DATA_FILE.copy() + data_file["ext_conf"] = "hostgaplugin/ext_conf-rsm_version_properties_false.xml" + with mock_wire_protocol(data_file) as protocol: + agent_families = protocol.get_goal_state().extensions_goal_state.agent_families + for family in agent_families: + self.assertFalse(family.is_vm_enabled_for_rsm_upgrades, "is_vm_enabled_for_rsm_upgrades should be False") diff --git a/tests/common/protocol/test_extensions_goal_state_from_vm_settings.py b/tests/common/protocol/test_extensions_goal_state_from_vm_settings.py index bea1063f70..771fa22068 100644 --- a/tests/common/protocol/test_extensions_goal_state_from_vm_settings.py +++ b/tests/common/protocol/test_extensions_goal_state_from_vm_settings.py @@ -53,16 +53,66 @@ def test_it_should_parse_requested_version_properly(self): goal_state = GoalState(protocol.client) families = goal_state.extensions_goal_state.agent_families for family in families: - self.assertEqual(family.requested_version_string, "0.0.0.0", "Version should be None") + self.assertIsNone(family.version, "Version should be None") data_file = wire_protocol_data.DATA_FILE_VM_SETTINGS.copy() - data_file["vm_settings"] = "hostgaplugin/vm_settings-requested_version.json" + data_file["vm_settings"] = "hostgaplugin/vm_settings-agent_family_version.json" with mock_wire_protocol(data_file) as protocol: protocol.mock_wire_data.set_etag(888) goal_state = GoalState(protocol.client) families = goal_state.extensions_goal_state.agent_families for family in families: - self.assertEqual(family.requested_version_string, "9.9.9.9", "Version should be 9.9.9.9") + self.assertEqual(family.version, "9.9.9.9", "Version should be 9.9.9.9") + + def test_it_should_parse_is_version_from_rsm_properly(self): + with mock_wire_protocol(wire_protocol_data.DATA_FILE_VM_SETTINGS) as protocol: + goal_state = GoalState(protocol.client) + families = goal_state.extensions_goal_state.agent_families + for family in families: + self.assertIsNone(family.is_version_from_rsm, "is_version_from_rsm should be None") + + data_file = wire_protocol_data.DATA_FILE_VM_SETTINGS.copy() + data_file["vm_settings"] = "hostgaplugin/vm_settings-agent_family_version.json" + with mock_wire_protocol(data_file) as protocol: + protocol.mock_wire_data.set_etag(888) + goal_state = GoalState(protocol.client) + families = goal_state.extensions_goal_state.agent_families + for family in families: + self.assertTrue(family.is_version_from_rsm, "is_version_from_rsm should be True") + + data_file = wire_protocol_data.DATA_FILE_VM_SETTINGS.copy() + data_file["vm_settings"] = "hostgaplugin/vm_settings-requested_version_properties_false.json" + with mock_wire_protocol(data_file) as protocol: + protocol.mock_wire_data.set_etag(888) + goal_state = GoalState(protocol.client) + families = goal_state.extensions_goal_state.agent_families + for family in families: + self.assertFalse(family.is_version_from_rsm, "is_version_from_rsm should be False") + + def test_it_should_parse_is_vm_enabled_for_rsm_upgrades_properly(self): + with mock_wire_protocol(wire_protocol_data.DATA_FILE_VM_SETTINGS) as protocol: + goal_state = GoalState(protocol.client) + families = goal_state.extensions_goal_state.agent_families + for family in families: + self.assertIsNone(family.is_vm_enabled_for_rsm_upgrades, "is_vm_enabled_for_rsm_upgrades should be None") + + data_file = wire_protocol_data.DATA_FILE_VM_SETTINGS.copy() + data_file["vm_settings"] = "hostgaplugin/vm_settings-agent_family_version.json" + with mock_wire_protocol(data_file) as protocol: + protocol.mock_wire_data.set_etag(888) + goal_state = GoalState(protocol.client) + families = goal_state.extensions_goal_state.agent_families + for family in families: + self.assertTrue(family.is_vm_enabled_for_rsm_upgrades, "is_vm_enabled_for_rsm_upgrades should be True") + + data_file = wire_protocol_data.DATA_FILE_VM_SETTINGS.copy() + data_file["vm_settings"] = "hostgaplugin/vm_settings-requested_version_properties_false.json" + with mock_wire_protocol(data_file) as protocol: + protocol.mock_wire_data.set_etag(888) + goal_state = GoalState(protocol.client) + families = goal_state.extensions_goal_state.agent_families + for family in families: + self.assertFalse(family.is_vm_enabled_for_rsm_upgrades, "is_vm_enabled_for_rsm_upgrades should be False") def test_it_should_parse_missing_status_upload_blob_as_none(self): data_file = wire_protocol_data.DATA_FILE_VM_SETTINGS.copy() diff --git a/tests/data/hostgaplugin/ext_conf-requested_version.xml b/tests/data/hostgaplugin/ext_conf-agent_family_version.xml similarity index 97% rename from tests/data/hostgaplugin/ext_conf-requested_version.xml rename to tests/data/hostgaplugin/ext_conf-agent_family_version.xml index 48cc95cc9f..5c9e0028fe 100644 --- a/tests/data/hostgaplugin/ext_conf-requested_version.xml +++ b/tests/data/hostgaplugin/ext_conf-agent_family_version.xml @@ -4,6 +4,8 @@ Prod 9.9.9.10 + true + true https://zrdfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Prod_uscentraleuap_manifest.xml https://ardfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Prod_uscentraleuap_manifest.xml @@ -12,6 +14,8 @@ Test 9.9.9.10 + true + true https://zrdfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Test_uscentraleuap_manifest.xml https://ardfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Test_uscentraleuap_manifest.xml diff --git a/tests/data/hostgaplugin/ext_conf-rsm_version_properties_false.xml b/tests/data/hostgaplugin/ext_conf-rsm_version_properties_false.xml new file mode 100644 index 0000000000..e1f1d6ba8c --- /dev/null +++ b/tests/data/hostgaplugin/ext_conf-rsm_version_properties_false.xml @@ -0,0 +1,152 @@ + + + + + Prod + 9.9.9.10 + false + false + + https://zrdfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Prod_uscentraleuap_manifest.xml + https://ardfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Prod_uscentraleuap_manifest.xml + + + + Test + 9.9.9.10 + false + false + + https://zrdfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Test_uscentraleuap_manifest.xml + https://ardfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Test_uscentraleuap_manifest.xml + + + + CentralUSEUAP + CRP + + + + MultipleExtensionsPerHandler + + +https://dcrcl3a0xs.blob.core.windows.net/$system/edp0plkw2b.86f4ae0a-61f8-48ae-9199-40f402d56864.status?sv=2018-03-28&sr=b&sk=system-1&sig=KNWgC2%3d&se=9999-01-01T00%3a00%3a00Z&sp=w + + + + https://zrdfepirv2cbn09pr02a.blob.core.windows.net/a47f0806d764480a8d989d009c75007d/Microsoft.Azure.Monitor_AzureMonitorLinuxAgent_useast2euap_manifest.xml + + + + + https://zrdfepirv2cbn06prdstr01a.blob.core.windows.net/4ef06ad957494df49c807a5334f2b5d2/Microsoft.Azure.Security.Monitoring_AzureSecurityLinuxAgent_useast2euap_manifest.xml + + + + + https://umsanh4b5rfz0q0p4pwm.blob.core.windows.net/5237dd14-0aad-f051-0fad-1e33e1b63091/5237dd14-0aad-f051-0fad-1e33e1b63091_manifest.xml + + + + + https://umsawqtlsshtn5v2nfgh.blob.core.windows.net/f4086d41-69f9-3103-78e0-8a2c7e789d0f/f4086d41-69f9-3103-78e0-8a2c7e789d0f_manifest.xml + + + + + https://umsah3cwjlctnmhsvzqv.blob.core.windows.net/2bbece4f-0283-d415-b034-cc0adc6997a1/2bbece4f-0283-d415-b034-cc0adc6997a1_manifest.xml + + + + + + { + "runtimeSettings": [ + { + "handlerSettings": { + "protectedSettingsCertThumbprint": "BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F", + "protectedSettings": "MIIBsAYJKoZIhvcNAQcDoIIBoTCCAZ0CAQAxggFpMIIBZQIBADBNMDkxNzA1BgoJkiaJk/IsZAEZFidXaW5kb3dzIEF6dXJlIENSUCBDZXJ0aWZpY2F0ZSBHZW5lcmF0b3ICEFpB/HKM/7evRk+DBz754wUwDQYJKoZIhvcNAQEBBQAEggEADPJwniDeIUXzxNrZCloitFdscQ59Bz1dj9DLBREAiM8jmxM0LLicTJDUv272Qm/4ZQgdqpFYBFjGab/9MX+Ih2x47FkVY1woBkckMaC/QOFv84gbboeQCmJYZC/rZJdh8rCMS+CEPq3uH1PVrvtSdZ9uxnaJ+E4exTPPviIiLIPtqWafNlzdbBt8HZjYaVw+SSe+CGzD2pAQeNttq3Rt/6NjCzrjG8ufKwvRoqnrInMs4x6nnN5/xvobKIBSv4/726usfk8Ug+9Q6Benvfpmre2+1M5PnGTfq78cO3o6mI3cPoBUjp5M0iJjAMGeMt81tyHkimZrEZm6pLa4NQMOEjArBgkqhkiG9w0BBwEwFAYIKoZIhvcNAwcECC5nVaiJaWt+gAhgeYvxUOYHXw==", + "publicSettings": {"GCS_AUTO_CONFIG":true} + } + } + ] +} + + + { + "runtimeSettings": [ + { + "handlerSettings": { + "protectedSettingsCertThumbprint": "BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F", + "protectedSettings": "MIIBsAYJKoZIhvcNAQcDoIIBoTCCAZ0CAQAxggFpMIIBZQIBADBNMDkxNzA1BgoJkiaJk/IsZAEZFidXaW5kb3dzIEF6dXJlIENSUCBDZXJ0aWZpY2F0ZSBHZW5lcmF0b3ICEFpB/HKM/7evRk+DBz754wUwDQYJKoZIhvcNAQEBBQAEggEADPJwniDeIUXzxNrZCloitFdscQ59Bz1dj9DLBREAiM8jmxM0LLicTJDUv272Qm/4ZQgdqpFYBFjGab/9MX+Ih2x47FkVY1woBkckMaC/QOFv84gbboeQCmJYZC/rZJdh8rCMS+CEPq3uH1PVrvtSdZ9uxnaJ+E4exTPPviIiLIPtqWafNlzdbBt8HZjYaVw+SSe+CGzD2pAQeNttq3Rt/6NjCzrjG8ufKwvRoqnrInMs4x6nnN5/xvobKIBSv4/726usfk8Ug+9Q6Benvfpmre2+1M5PnGTfq78cO3o6mI3cPoBUjp5M0iJjAMGeMt81tyHkimZrEZm6pLa4NQMOEjArBgkqhkiG9w0BBwEwFAYIKoZIhvcNAwcECC5nVaiJaWt+gAhgeYvxUOYHXw==", + "publicSettings": {"enableGenevaUpload":true} + } + } + ] +} + + + + + + { + "runtimeSettings": [ + { + "handlerSettings": { + "publicSettings": {"commandToExecute":"echo 'cee174d4-4daa-4b07-9958-53b9649445c2'"} + } + } + ] +} + + + + + + + + + + { + "runtimeSettings": [ + { + "handlerSettings": { + "publicSettings": {"source":{"script":"echo '4abb1e88-f349-41f8-8442-247d9fdfcac5'"}} + } + } + ] +} + { + "runtimeSettings": [ + { + "handlerSettings": { + "publicSettings": {"source":{"script":"echo 'e865c9bc-a7b3-42c6-9a79-cfa98a1ee8b3'"}} + } + } + ] +} + { + "runtimeSettings": [ + { + "handlerSettings": { + "publicSettings": {"source":{"script":"echo 'f923e416-0340-485c-9243-8b84fb9930c6'"}} + } + } + ] +} + + + { + "runtimeSettings": [ + { + "handlerSettings": { + "protectedSettingsCertThumbprint": "59A10F50FFE2A0408D3F03FE336C8FD5716CF25C", + "protectedSettings": "*** REDACTED ***" + } + } + ] +} + + +https://dcrcl3a0xs.blob.core.windows.net/$system/edp0plkw2b.86f4ae0a-61f8-48ae-9199-40f402d56864.vmSettings?sv=2018-03-28&sr=b&sk=system-1&sig=PaiLic%3d&se=9999-01-01T00%3a00%3a00Z&sp=r + diff --git a/tests/data/hostgaplugin/vm_settings-requested_version.json b/tests/data/hostgaplugin/vm_settings-agent_family_version.json similarity index 97% rename from tests/data/hostgaplugin/vm_settings-requested_version.json rename to tests/data/hostgaplugin/vm_settings-agent_family_version.json index 0f73cb255e..734cc8147b 100644 --- a/tests/data/hostgaplugin/vm_settings-requested_version.json +++ b/tests/data/hostgaplugin/vm_settings-agent_family_version.json @@ -29,6 +29,8 @@ { "name": "Prod", "version": "9.9.9.9", + "isVersionFromRSM": true, + "isVMEnabledForRSMUpgrades": true, "uris": [ "https://zrdfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Prod_uscentraleuap_manifest.xml", "https://ardfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Prod_uscentraleuap_manifest.xml" @@ -37,6 +39,8 @@ { "name": "Test", "version": "9.9.9.9", + "isVersionFromRSM": true, + "isVMEnabledForRSMUpgrades": true, "uris": [ "https://zrdfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Test_uscentraleuap_manifest.xml", "https://ardfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Test_uscentraleuap_manifest.xml" diff --git a/tests/data/hostgaplugin/vm_settings-requested_version_properties_false.json b/tests/data/hostgaplugin/vm_settings-requested_version_properties_false.json new file mode 100644 index 0000000000..3a6eb8b1a5 --- /dev/null +++ b/tests/data/hostgaplugin/vm_settings-requested_version_properties_false.json @@ -0,0 +1,145 @@ +{ + "hostGAPluginVersion": "1.0.8.133", + "vmSettingsSchemaVersion": "0.0", + "activityId": "a33f6f53-43d6-4625-b322-1a39651a00c9", + "correlationId": "9a47a2a2-e740-4bfc-b11b-4f2f7cfe7d2e", + "inSvdSeqNo": 1, + "extensionsLastModifiedTickCount": 637726699999999999, + "extensionGoalStatesSource": "FastTrack", + "onHold": true, + "statusUploadBlob": { + "statusBlobType": "BlockBlob", + "value": "https://dcrcl3a0xs.blob.core.windows.net/$system/edp0plkw2b.86f4ae0a-61f8-48ae-9199-40f402d56864.status?sv=2018-03-28&sr=b&sk=system-1&sig=KNWgC2%3d&se=9999-01-01T00%3a00%3a00Z&sp=w" + }, + "inVMMetadata": { + "subscriptionId": "8e037ad4-618f-4466-8bc8-5099d41ac15b", + "resourceGroupName": "rg-dc-86fjzhp", + "vmName": "edp0plkw2b", + "location": "CentralUSEUAP", + "vmId": "86f4ae0a-61f8-48ae-9199-40f402d56864", + "vmSize": "Standard_B2s", + "osType": "Linux" + }, + "requiredFeatures": [ + { + "name": "MultipleExtensionsPerHandler" + } + ], + "gaFamilies": [ + { + "name": "Prod", + "version": "9.9.9.9", + "isVersionFromRSM": false, + "isVMEnabledForRSMUpgrades": false, + "uris": [ + "https://zrdfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Prod_uscentraleuap_manifest.xml", + "https://ardfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Prod_uscentraleuap_manifest.xml" + ] + }, + { + "name": "Test", + "version": "9.9.9.9", + "isVersionFromRSM": false, + "isVMEnabledForRSMUpgrades": false, + "uris": [ + "https://zrdfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Test_uscentraleuap_manifest.xml", + "https://ardfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Test_uscentraleuap_manifest.xml" + ] + } + ], + "extensionGoalStates": [ + { + "name": "Microsoft.Azure.Monitor.AzureMonitorLinuxAgent", + "version": "1.9.1", + "location": "https://zrdfepirv2cbn04prdstr01a.blob.core.windows.net/a47f0806d764480a8d989d009c75007d/Microsoft.Azure.Monitor_AzureMonitorLinuxAgent_useast2euap_manifest.xml", + "state": "enabled", + "autoUpgrade": true, + "runAsStartupTask": false, + "isJson": true, + "useExactVersion": true, + "settingsSeqNo": 0, + "settings": [ + { + "protectedSettingsCertThumbprint": "BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F", + "protectedSettings": "MIIBsAYJKoZIhvcNAQcDoIIBoTCCAZ0CAQAxggFpMIIBZQIBADBNMDkxNzA1BgoJkiaJk/IsZAEZFidXaW5kb3dzIEF6dXJlIENSUCBDZXJ0aWZpY2F0ZSBHZW5lcmF0b3ICEFpB/HKM/7evRk+DBz754wUwDQYJKoZIhvcNAQEBBQAEggEADPJwniDeIUXzxNrZCloitFdscQ59Bz1dj9DLBREAiM8jmxM0LLicTJDUv272Qm/4ZQgdqpFYBFjGab/9MX+Ih2x47FkVY1woBkckMaC/QOFv84gbboeQCmJYZC/rZJdh8rCMS+CEPq3uH1PVrvtSdZ9uxnaJ+E4exTPPviIiLIPtqWafNlzdbBt8HZjYaVw+SSe+CGzD2pAQeNttq3Rt/6NjCzrjG8ufKwvRoqnrInMs4x6nnN5/xvobKIBSv4/726usfk8Ug+9Q6Benvfpmre2+1M5PnGTfq78cO3o6mI3cPoBUjp5M0iJjAMGeMt81tyHkimZrEZm6pLa4NQMOEjArBgkqhkiG9w0BBwEwFAYIKoZIhvcNAwcECC5nVaiJaWt+gAhgeYvxUOYHXw==", + "publicSettings": "{\"GCS_AUTO_CONFIG\":true}" + } + ] + }, + { + "name": "Microsoft.Azure.Security.Monitoring.AzureSecurityLinuxAgent", + "version": "2.15.112", + "location": "https://zrdfepirv2cbn04prdstr01a.blob.core.windows.net/4ef06ad957494df49c807a5334f2b5d2/Microsoft.Azure.Security.Monitoring_AzureSecurityLinuxAgent_useast2euap_manifest.xml", + "state": "enabled", + "autoUpgrade": true, + "runAsStartupTask": false, + "isJson": true, + "useExactVersion": true, + "settingsSeqNo": 0, + "settings": [ + { + "protectedSettingsCertThumbprint": "BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F", + "protectedSettings": "MIIBsAYJKoZIhvcNAQcDoIIBoTCCAZ0CAQAxggFpMIIBZQIBADBNMDkxNzA1BgoJkiaJk/IsZAEZFidXaW5kb3dzIEF6dXJlIENSUCBDZXJ0aWZpY2F0ZSBHZW5lcmF0b3ICEFpB/HKM/7evRk+DBz754wUwDQYJKoZIhvcNAQEBBQAEggEADPJwniDeIUXzxNrZCloitFdscQ59Bz1dj9DLBREAiM8jmxM0LLicTJDUv272Qm/4ZQgdqpFYBFjGab/9MX+Ih2x47FkVY1woBkckMaC/QOFv84gbboeQCmJYZC/rZJdh8rCMS+CEPq3uH1PVrvtSdZ9uxnaJ+E4exTPPviIiLIPtqWafNlzdbBt8HZjYaVw+SSe+CGzD2pAQeNttq3Rt/6NjCzrjG8ufKwvRoqnrInMs4x6nnN5/xvobKIBSv4/726usfk8Ug+9Q6Benvfpmre2+1M5PnGTfq78cO3o6mI3cPoBUjp5M0iJjAMGeMt81tyHkimZrEZm6pLa4NQMOEjArBgkqhkiG9w0BBwEwFAYIKoZIhvcNAwcECC5nVaiJaWt+gAhgeYvxUOYHXw==", + "publicSettings": "{\"enableGenevaUpload\":true}" + } + ] + }, + { + "name": "Microsoft.Azure.Extensions.CustomScript", + "version": "2.1.6", + "location": "https://umsavwggj2v40kvqhc0w.blob.core.windows.net/5237dd14-0aad-f051-0fad-1e33e1b63091/5237dd14-0aad-f051-0fad-1e33e1b63091_manifest.xml", + "failoverlocation": "https://umsafwzhkbm1rfrhl0ws.blob.core.windows.net/5237dd14-0aad-f051-0fad-1e33e1b63091/5237dd14-0aad-f051-0fad-1e33e1b63091_manifest.xml", + "additionalLocations": [ + "https://umsanh4b5rfz0q0p4pwm.blob.core.windows.net/5237dd14-0aad-f051-0fad-1e33e1b63091/5237dd14-0aad-f051-0fad-1e33e1b63091_manifest.xml" + ], + "state": "enabled", + "autoUpgrade": true, + "runAsStartupTask": false, + "isJson": true, + "useExactVersion": true, + "settingsSeqNo": 0, + "isMultiConfig": false, + "settings": [ + { + "publicSettings": "{\"commandToExecute\":\"echo 'cee174d4-4daa-4b07-9958-53b9649445c2'\"}" + } + ] + }, + { + "name": "Microsoft.CPlat.Core.RunCommandHandlerLinux", + "version": "1.2.0", + "location": "https://umsavbvncrpzbnxmxzmr.blob.core.windows.net/f4086d41-69f9-3103-78e0-8a2c7e789d0f/f4086d41-69f9-3103-78e0-8a2c7e789d0f_manifest.xml", + "failoverlocation": "https://umsajbjtqrb3zqjvgb2z.blob.core.windows.net/f4086d41-69f9-3103-78e0-8a2c7e789d0f/f4086d41-69f9-3103-78e0-8a2c7e789d0f_manifest.xml", + "additionalLocations": [ + "https://umsawqtlsshtn5v2nfgh.blob.core.windows.net/f4086d41-69f9-3103-78e0-8a2c7e789d0f/f4086d41-69f9-3103-78e0-8a2c7e789d0f_manifest.xml" + ], + "state": "enabled", + "autoUpgrade": true, + "runAsStartupTask": false, + "isJson": true, + "useExactVersion": true, + "settingsSeqNo": 0, + "isMultiConfig": true, + "settings": [ + { + "publicSettings": "{\"source\":{\"script\":\"echo '4abb1e88-f349-41f8-8442-247d9fdfcac5'\"}}", + "seqNo": 0, + "extensionName": "MCExt1", + "extensionState": "enabled" + }, + { + "publicSettings": "{\"source\":{\"script\":\"echo 'e865c9bc-a7b3-42c6-9a79-cfa98a1ee8b3'\"}}", + "seqNo": 0, + "extensionName": "MCExt2", + "extensionState": "enabled" + }, + { + "publicSettings": "{\"source\":{\"script\":\"echo 'f923e416-0340-485c-9243-8b84fb9930c6'\"}}", + "seqNo": 0, + "extensionName": "MCExt3", + "extensionState": "enabled" + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/data/wire/ext_conf_requested_version.xml b/tests/data/wire/ext_conf_rsm_version.xml similarity index 89% rename from tests/data/wire/ext_conf_requested_version.xml rename to tests/data/wire/ext_conf_rsm_version.xml index d12352c297..806063541a 100644 --- a/tests/data/wire/ext_conf_requested_version.xml +++ b/tests/data/wire/ext_conf_rsm_version.xml @@ -3,6 +3,8 @@ Prod 9.9.9.10 + True + True http://mock-goal-state/manifest_of_ga.xml @@ -10,6 +12,8 @@ Test 9.9.9.10 + True + True http://mock-goal-state/manifest_of_ga.xml diff --git a/tests/data/wire/ext_conf_version_missing_in_agent_family.xml b/tests/data/wire/ext_conf_version_missing_in_agent_family.xml new file mode 100644 index 0000000000..3f81ed1195 --- /dev/null +++ b/tests/data/wire/ext_conf_version_missing_in_agent_family.xml @@ -0,0 +1,31 @@ + + + + Prod + True + True + + http://mock-goal-state/manifest_of_ga.xml + + + + Test + True + True + + http://mock-goal-state/manifest_of_ga.xml + + + + + + + + + + {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} + + + +https://test.blob.core.windows.net/vhds/test-cs12.test-cs12.test-cs12.status?sr=b&sp=rw&se=9999-01-01&sk=key1&sv=2014-02-14&sig=hfRh7gzUE7sUtYwke78IOlZOrTRCYvkec4hGZ9zZzXo + diff --git a/tests/data/wire/ext_conf_requested_version_missing_in_manifest.xml b/tests/data/wire/ext_conf_version_missing_in_manifest.xml similarity index 89% rename from tests/data/wire/ext_conf_requested_version_missing_in_manifest.xml rename to tests/data/wire/ext_conf_version_missing_in_manifest.xml index 84043e2d75..c750d5d3a2 100644 --- a/tests/data/wire/ext_conf_requested_version_missing_in_manifest.xml +++ b/tests/data/wire/ext_conf_version_missing_in_manifest.xml @@ -4,6 +4,8 @@ Prod 5.2.1.0 + True + True http://mock-goal-state/manifest_of_ga.xml @@ -11,6 +13,8 @@ Test 5.2.1.0 + True + True http://mock-goal-state/manifest_of_ga.xml diff --git a/tests/data/wire/ext_conf_version_not_from_rsm.xml b/tests/data/wire/ext_conf_version_not_from_rsm.xml new file mode 100644 index 0000000000..9da8f5da72 --- /dev/null +++ b/tests/data/wire/ext_conf_version_not_from_rsm.xml @@ -0,0 +1,33 @@ + + + + Prod + 9.9.9.10 + False + True + + http://mock-goal-state/manifest_of_ga.xml + + + + Test + 9.9.9.10 + False + True + + http://mock-goal-state/manifest_of_ga.xml + + + + + + + + + + {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} + + + +https://test.blob.core.windows.net/vhds/test-cs12.test-cs12.test-cs12.status?sr=b&sp=rw&se=9999-01-01&sk=key1&sv=2014-02-14&sig=hfRh7gzUE7sUtYwke78IOlZOrTRCYvkec4hGZ9zZzXo + diff --git a/tests/data/wire/ext_conf_vm_not_enabled_for_rsm_upgrades.xml b/tests/data/wire/ext_conf_vm_not_enabled_for_rsm_upgrades.xml new file mode 100644 index 0000000000..384723f461 --- /dev/null +++ b/tests/data/wire/ext_conf_vm_not_enabled_for_rsm_upgrades.xml @@ -0,0 +1,33 @@ + + + + Prod + 9.9.9.10 + False + False + + http://mock-goal-state/manifest_of_ga.xml + + + + Test + 9.9.9.10 + False + False + + http://mock-goal-state/manifest_of_ga.xml + + + + + + + + + + {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} + + + +https://test.blob.core.windows.net/vhds/test-cs12.test-cs12.test-cs12.status?sr=b&sp=rw&se=9999-01-01&sk=key1&sv=2014-02-14&sig=hfRh7gzUE7sUtYwke78IOlZOrTRCYvkec4hGZ9zZzXo + diff --git a/tests/ga/test_agent_update_handler.py b/tests/ga/test_agent_update_handler.py index d91cbb8019..6c069bfc84 100644 --- a/tests/ga/test_agent_update_handler.py +++ b/tests/ga/test_agent_update_handler.py @@ -11,7 +11,6 @@ from azurelinuxagent.common.protocol.util import ProtocolUtil from azurelinuxagent.common.version import CURRENT_VERSION from azurelinuxagent.ga.agent_update_handler import get_agent_update_handler -from azurelinuxagent.ga.guestagent import GAUpdateReportState from tests.ga.test_update import UpdateTestCase from tests.lib.http_request_predicates import HttpRequestPredicates from tests.lib.mock_wire_protocol import mock_wire_protocol, MockHttpResponse @@ -28,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, autoupdate_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 @@ -58,74 +57,84 @@ def put_handler(url, *args, **_): with patch("azurelinuxagent.common.conf.get_autoupdate_frequency", return_value=autoupdate_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 - - def __assert_agent_directories_available(self, versions): + def _assert_agent_directories_available(self, versions): for version in versions: self.assertTrue(os.path.exists(self.agent_dir(version)), "Agent directory {0} not found".format(version)) - def __assert_agent_directories_exist_and_others_dont_exist(self, versions): - self.__assert_agent_directories_available(versions=versions) + def _assert_agent_directories_exist_and_others_dont_exist(self, versions): + self._assert_agent_directories_available(versions=versions) other_agents = [agent_dir for agent_dir in self.agent_dirs() if agent_dir not in [self.agent_dir(version) for version in versions]] self.assertFalse(any(other_agents), "All other agents should be purged from agent dir: {0}".format(other_agents)) - def __assert_agent_requested_version_in_goal_state(self, mock_telemetry, inc=1, version="9.9.9.10"): + def _assert_agent_rsm_version_in_goal_state(self, mock_telemetry, inc=1, version="9.9.9.10"): upgrade_event_msgs = [kwarg['message'] for _, kwarg in mock_telemetry.call_args_list if - 'discovered new agent version:{0} in agent manifest for goal state incarnation_{1}'.format(version, inc) in kwarg['message'] and kwarg[ + 'New agent version:{0} requested by RSM in Goal state incarnation_{1}'.format(version, inc) in kwarg['message'] and kwarg[ 'op'] == WALAEventOperation.AgentUpgrade] self.assertEqual(1, len(upgrade_event_msgs), "Did not find the event indicating that the agent requested version found. Got: {0}".format( mock_telemetry.call_args_list)) - def __assert_no_agent_package_telemetry_emitted(self, mock_telemetry, version="9.9.9.10"): + def _assert_update_discovered_from_agent_manifest(self, mock_telemetry, inc=1, version="9.9.9.10"): + upgrade_event_msgs = [kwarg['message'] for _, kwarg in mock_telemetry.call_args_list if + 'Self-update discovered new agent version:{0} in agent manifest for goal state incarnation_{1}'.format(version, inc) in kwarg['message'] and kwarg[ + 'op'] == WALAEventOperation.AgentUpgrade] + self.assertEqual(1, len(upgrade_event_msgs), + "Did not find the event indicating that the new version found. Got: {0}".format( + mock_telemetry.call_args_list)) + + def _assert_no_agent_package_telemetry_emitted(self, mock_telemetry, version="9.9.9.10"): upgrade_event_msgs = [kwarg['message'] for _, kwarg in mock_telemetry.call_args_list if - 'No matching package found in the agent manifest for requested version: {0}'.format(version) in kwarg['message'] and kwarg[ + 'No matching package found in the agent manifest for version: {0}'.format(version) in kwarg['message'] and kwarg[ 'op'] == WALAEventOperation.AgentUpgrade] self.assertEqual(1, len(upgrade_event_msgs), "Did not find the event indicating that the agent package not found. Got: {0}".format( mock_telemetry.call_args_list)) + def _assert_agent_exit_process_telemetry_emitted(self, message): + self.assertIn("Agent completed all update checks, exiting current process", message) + def test_it_should_not_update_when_autoupdate_disabled(self): self.prepare_agents(count=1) - with self.__get_agent_update_handler(autoupdate_enabled=False) as (agent_update_handler, mock_telemetry): - agent_update_handler.run(agent_update_handler._protocol.get_goal_state()) - self.__assert_agent_directories_exist_and_others_dont_exist(versions=[str(CURRENT_VERSION)]) + with self._get_agent_update_handler(autoupdate_enabled=False) as (agent_update_handler, mock_telemetry): + agent_update_handler.run(agent_update_handler._protocol.get_goal_state(), True) + self._assert_agent_directories_exist_and_others_dont_exist(versions=[str(CURRENT_VERSION)]) self.assertEqual(0, len([kwarg['message'] for _, kwarg in mock_telemetry.call_args_list if "requesting a new agent version" in kwarg['message'] and kwarg[ - 'op'] == WALAEventOperation.AgentUpgrade]), "should not check for requested version") + 'op'] == WALAEventOperation.AgentUpgrade]), "should not check for rsm version") def test_it_should_update_to_largest_version_if_ga_versioning_disabled(self): self.prepare_agents(count=1) data_file = DATA_FILE.copy() - data_file["ext_conf"] = "wire/ext_conf_requested_version.xml" - with self.__get_agent_update_handler(test_data=data_file) as (agent_update_handler, mock_telemetry): + data_file["ext_conf"] = "wire/ext_conf_rsm_version.xml" + with self._get_agent_update_handler(test_data=data_file) as (agent_update_handler, mock_telemetry): with patch.object(conf, "get_enable_ga_versioning", return_value=False): with self.assertRaises(AgentUpgradeExitException) as context: - agent_update_handler.run(agent_update_handler._protocol.get_goal_state()) - self.__assert_agent_requested_version_in_goal_state(mock_telemetry, inc=2, version="99999.0.0.0") - self.__assert_agent_directories_exist_and_others_dont_exist(versions=[str(CURRENT_VERSION), "99999.0.0.0"]) - self.assertIn("Agent update found, exiting current process", ustr(context.exception.reason)) + agent_update_handler.run(agent_update_handler._protocol.get_goal_state(), True) + self._assert_update_discovered_from_agent_manifest(mock_telemetry, version="99999.0.0.0") + 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, _): - agent_update_handler.run(agent_update_handler._protocol.get_goal_state()) + with self._get_agent_update_handler(test_data=data_file, autoupdate_frequency=10) as (agent_update_handler, _): + agent_update_handler.run(agent_update_handler._protocol.get_goal_state(), True) self.assertFalse(os.path.exists(self.agent_dir("99999.0.0.0")), "New agent directory should not be found") agent_update_handler._protocol.mock_wire_data.set_ga_manifest("wire/ga_manifest.xml") agent_update_handler._protocol.mock_wire_data.set_incarnation(2) agent_update_handler._protocol.client.update_goal_state() - agent_update_handler.run(agent_update_handler._protocol.get_goal_state()) + agent_update_handler.run(agent_update_handler._protocol.get_goal_state(), True) self.assertFalse(os.path.exists(self.agent_dir("99999.0.0.0")), "New agent directory should not be found") @@ -134,68 +143,50 @@ def test_it_should_update_to_largest_version_if_time_window_elapsed(self): data_file = DATA_FILE.copy() data_file["ga_manifest"] = "wire/ga_manifest_no_uris.xml" - with patch("azurelinuxagent.common.conf.get_hotfix_upgrade_frequency", return_value=0.001): - with patch("azurelinuxagent.common.conf.get_normal_upgrade_frequency", return_value=0.001): - with self.__get_agent_update_handler(test_data=data_file) as (agent_update_handler, mock_telemetry): + with patch("azurelinuxagent.common.conf.get_self_update_hotfix_frequency", return_value=0.001): + with patch("azurelinuxagent.common.conf.get_self_update_regular_frequency", return_value=0.001): + with self._get_agent_update_handler(test_data=data_file) as (agent_update_handler, mock_telemetry): with self.assertRaises(AgentUpgradeExitException) as context: - agent_update_handler.run(agent_update_handler._protocol.get_goal_state()) + agent_update_handler.run(agent_update_handler._protocol.get_goal_state(), True) self.assertFalse(os.path.exists(self.agent_dir("99999.0.0.0")), "New agent directory should not be found") agent_update_handler._protocol.mock_wire_data.set_ga_manifest("wire/ga_manifest.xml") agent_update_handler._protocol.mock_wire_data.set_incarnation(2) agent_update_handler._protocol.client.update_goal_state() - agent_update_handler.run(agent_update_handler._protocol.get_goal_state()) - self.__assert_agent_requested_version_in_goal_state(mock_telemetry, inc=2, version="99999.0.0.0") - self.__assert_agent_directories_exist_and_others_dont_exist(versions=[str(CURRENT_VERSION), "99999.0.0.0"]) - self.assertIn("Agent update found, exiting current process", ustr(context.exception.reason)) + agent_update_handler.run(agent_update_handler._protocol.get_goal_state(), True) + self._assert_update_discovered_from_agent_manifest(mock_telemetry, inc=2, version="99999.0.0.0") + 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_not_allow_update_if_largest_version_below_current_version(self): self.prepare_agents(count=1) data_file = DATA_FILE.copy() data_file["ga_manifest"] = "wire/ga_manifest_no_upgrade.xml" - with self.__get_agent_update_handler(test_data=data_file) as (agent_update_handler, _): - agent_update_handler.run(agent_update_handler._protocol.get_goal_state()) - self.__assert_agent_directories_exist_and_others_dont_exist(versions=[str(CURRENT_VERSION)]) - - def test_it_should_not_agent_update_if_last_attempted_update_time_not_elapsed(self): - self.prepare_agents(count=1) - data_file = DATA_FILE.copy() - data_file["ext_conf"] = "wire/ext_conf_requested_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): - agent_update_handler._protocol.mock_wire_data.set_extension_config_requested_version(version) - agent_update_handler._protocol.mock_wire_data.set_incarnation(2) - agent_update_handler._protocol.client.update_goal_state() - agent_update_handler.run(agent_update_handler._protocol.get_goal_state()) - - self.__assert_agent_requested_version_in_goal_state(mock_telemetry, inc=2, version=version) - self.__assert_no_agent_package_telemetry_emitted(mock_telemetry, version=version) - # Now we shouldn't check for download if update not allowed.This run should not add new logs - agent_update_handler.run(agent_update_handler._protocol.get_goal_state()) - self.__assert_agent_requested_version_in_goal_state(mock_telemetry, inc=2, version=version) - self.__assert_no_agent_package_telemetry_emitted(mock_telemetry, version=version) + with self._get_agent_update_handler(test_data=data_file) as (agent_update_handler, _): + agent_update_handler.run(agent_update_handler._protocol.get_goal_state(), True) + self._assert_agent_directories_exist_and_others_dont_exist(versions=[str(CURRENT_VERSION)]) - def test_it_should_update_to_largest_version_if_requested_version_not_available(self): + def test_it_should_update_to_largest_version_if_rsm_version_not_available(self): 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) as (agent_update_handler, mock_telemetry): + with self._get_agent_update_handler(test_data=data_file) as (agent_update_handler, mock_telemetry): with self.assertRaises(AgentUpgradeExitException) as context: - agent_update_handler.run(agent_update_handler._protocol.get_goal_state()) - self.__assert_agent_requested_version_in_goal_state(mock_telemetry, inc=2, version="99999.0.0.0") - self.__assert_agent_directories_exist_and_others_dont_exist(versions=[str(CURRENT_VERSION), "99999.0.0.0"]) - self.assertIn("Agent update found, exiting current process", ustr(context.exception.reason)) + agent_update_handler.run(agent_update_handler._protocol.get_goal_state(), True) + self._assert_update_discovered_from_agent_manifest(mock_telemetry, version="99999.0.0.0") + 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_not_download_manifest_again_if_last_attempted_download_time_not_elapsed(self): 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, autoupdate_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()) - agent_update_handler.run(agent_update_handler._protocol.get_goal_state()) + agent_update_handler.run(agent_update_handler._protocol.get_goal_state(), True) + agent_update_handler.run(agent_update_handler._protocol.get_goal_state(), True) + agent_update_handler.run(agent_update_handler._protocol.get_goal_state(), True) mock_wire_data = agent_update_handler._protocol.mock_wire_data self.assertEqual(1, mock_wire_data.call_counts['manifest_of_ga.xml'], "Agent manifest should not be downloaded again") @@ -205,53 +196,53 @@ 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, autoupdate_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()) - agent_update_handler.run(agent_update_handler._protocol.get_goal_state()) + agent_update_handler.run(agent_update_handler._protocol.get_goal_state(), True) + agent_update_handler.run(agent_update_handler._protocol.get_goal_state(), True) + agent_update_handler.run(agent_update_handler._protocol.get_goal_state(), True) mock_wire_data = agent_update_handler._protocol.mock_wire_data self.assertEqual(3, mock_wire_data.call_counts['manifest_of_ga.xml'], "Agent manifest should be downloaded in all attempts") - def test_it_should_not_agent_update_if_requested_version_is_same_as_current_version(self): + def test_it_should_not_agent_update_if_rsm_version_is_same_as_current_version(self): data_file = DATA_FILE.copy() - data_file["ext_conf"] = "wire/ext_conf_requested_version.xml" + data_file["ext_conf"] = "wire/ext_conf_rsm_version.xml" # Set the test environment by adding 20 random agents to the agent directory self.prepare_agents() self.assertEqual(20, self.agent_count(), "Agent directories not set properly") - with self.__get_agent_update_handler(test_data=data_file) as (agent_update_handler, mock_telemetry): - agent_update_handler._protocol.mock_wire_data.set_extension_config_requested_version( + with self._get_agent_update_handler(test_data=data_file) as (agent_update_handler, mock_telemetry): + agent_update_handler._protocol.mock_wire_data.set_version_in_agent_family( str(CURRENT_VERSION)) agent_update_handler._protocol.mock_wire_data.set_incarnation(2) agent_update_handler._protocol.client.update_goal_state() - agent_update_handler.run(agent_update_handler._protocol.get_goal_state()) + agent_update_handler.run(agent_update_handler._protocol.get_goal_state(), True) self.assertEqual(0, len([kwarg['message'] for _, kwarg in mock_telemetry.call_args_list if "requesting a new agent version" in kwarg['message'] and kwarg[ - 'op'] == WALAEventOperation.AgentUpgrade]), "requested version should be same as current version") + 'op'] == WALAEventOperation.AgentUpgrade]), "rsm version should be same as current version") self.assertFalse(os.path.exists(self.agent_dir("99999.0.0.0")), "New agent directory should not be found") - def test_it_should_upgrade_agent_if_requested_version_is_available_greater_than_current_version(self): + def test_it_should_upgrade_agent_if_rsm_version_is_available_greater_than_current_version(self): data_file = DATA_FILE.copy() - data_file["ext_conf"] = "wire/ext_conf_requested_version.xml" + data_file["ext_conf"] = "wire/ext_conf_rsm_version.xml" # Set the test environment by adding 20 random agents to the agent directory self.prepare_agents() self.assertEqual(20, self.agent_count(), "Agent directories not set properly") - with self.__get_agent_update_handler(test_data=data_file) as (agent_update_handler, mock_telemetry): + with self._get_agent_update_handler(test_data=data_file) as (agent_update_handler, mock_telemetry): with self.assertRaises(AgentUpgradeExitException) as context: - agent_update_handler.run(agent_update_handler._protocol.get_goal_state()) - self.__assert_agent_requested_version_in_goal_state(mock_telemetry, version="9.9.9.10") - self.__assert_agent_directories_exist_and_others_dont_exist(versions=["9.9.9.10", str(CURRENT_VERSION)]) - self.assertIn("Agent update found, exiting current process", ustr(context.exception.reason)) + agent_update_handler.run(agent_update_handler._protocol.get_goal_state(), True) + self._assert_agent_rsm_version_in_goal_state(mock_telemetry, version="9.9.9.10") + self._assert_agent_directories_exist_and_others_dont_exist(versions=["9.9.9.10", str(CURRENT_VERSION)]) + self._assert_agent_exit_process_telemetry_emitted(ustr(context.exception.reason)) - def test_it_should_downgrade_agent_if_requested_version_is_available_less_than_current_version(self): + def test_it_should_downgrade_agent_if_rsm_version_is_available_less_than_current_version(self): data_file = DATA_FILE.copy() - data_file["ext_conf"] = "wire/ext_conf_requested_version.xml" + data_file["ext_conf"] = "wire/ext_conf_rsm_version.xml" # Set the test environment by adding 20 random agents to the agent directory self.prepare_agents() @@ -259,20 +250,38 @@ def test_it_should_downgrade_agent_if_requested_version_is_available_less_than_c downgraded_version = "2.5.0" - with self.__get_agent_update_handler(test_data=data_file) as (agent_update_handler, mock_telemetry): - agent_update_handler._protocol.mock_wire_data.set_extension_config_requested_version(downgraded_version) + with self._get_agent_update_handler(test_data=data_file) as (agent_update_handler, mock_telemetry): + agent_update_handler._protocol.mock_wire_data.set_version_in_agent_family(downgraded_version) agent_update_handler._protocol.mock_wire_data.set_incarnation(2) agent_update_handler._protocol.client.update_goal_state() with self.assertRaises(AgentUpgradeExitException) as context: - agent_update_handler.run(agent_update_handler._protocol.get_goal_state()) - self.__assert_agent_requested_version_in_goal_state(mock_telemetry, inc=2, version=downgraded_version) - self.__assert_agent_directories_exist_and_others_dont_exist( + agent_update_handler.run(agent_update_handler._protocol.get_goal_state(), True) + self._assert_agent_rsm_version_in_goal_state(mock_telemetry, inc=2, version=downgraded_version) + self._assert_agent_directories_exist_and_others_dont_exist( versions=[downgraded_version, str(CURRENT_VERSION)]) - self.assertIn("Agent update found, exiting current process", ustr(context.exception.reason)) + self._assert_agent_exit_process_telemetry_emitted(ustr(context.exception.reason)) + + def test_it_should_not_do_rsm_update_if_gs_not_updated_in_next_attempt(self): + self.prepare_agents(count=1) + 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): + 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() + agent_update_handler.run(agent_update_handler._protocol.get_goal_state(), True) + + self._assert_agent_rsm_version_in_goal_state(mock_telemetry, inc=2, version=version) + self._assert_no_agent_package_telemetry_emitted(mock_telemetry, version=version) + # Now we shouldn't check for download if update not allowed(GS not updated).This run should not add new logs + agent_update_handler.run(agent_update_handler._protocol.get_goal_state(), False) + self._assert_agent_rsm_version_in_goal_state(mock_telemetry, inc=2, version=version) + self._assert_no_agent_package_telemetry_emitted(mock_telemetry, version=version) def test_it_should_not_downgrade_below_daemon_version(self): data_file = DATA_FILE.copy() - data_file["ext_conf"] = "wire/ext_conf_requested_version.xml" + data_file["ext_conf"] = "wire/ext_conf_rsm_version.xml" # Set the test environment by adding 20 random agents to the agent directory self.prepare_agents() @@ -280,21 +289,45 @@ def test_it_should_not_downgrade_below_daemon_version(self): downgraded_version = "1.2.0" - with self.__get_agent_update_handler(test_data=data_file) as (agent_update_handler, mock_telemetry): - agent_update_handler._protocol.mock_wire_data.set_extension_config_requested_version(downgraded_version) + with self._get_agent_update_handler(test_data=data_file) as (agent_update_handler, _): + agent_update_handler._protocol.mock_wire_data.set_version_in_agent_family(downgraded_version) agent_update_handler._protocol.mock_wire_data.set_incarnation(2) agent_update_handler._protocol.client.update_goal_state() - agent_update_handler.run(agent_update_handler._protocol.get_goal_state()) + agent_update_handler.run(agent_update_handler._protocol.get_goal_state(), True) self.assertFalse(os.path.exists(self.agent_dir(downgraded_version)), "New agent directory should not be found") - self.assertEqual(1, len([kwarg['message'] for _, kwarg in mock_telemetry.call_args_list if - "The Agent received a request to downgrade to version" in kwarg[ - 'message'] and kwarg[ - 'op'] == WALAEventOperation.AgentUpgrade]), "We should allow downgrade above daemon version") - def test_handles_if_requested_version_not_found_in_pkgs_to_download(self): + def test_it_should_update_to_largest_version_if_vm_not_enabled_for_rsm_upgrades(self): + self.prepare_agents(count=1) + + data_file = DATA_FILE.copy() + data_file['ext_conf'] = "wire/ext_conf_vm_not_enabled_for_rsm_upgrades.xml" + with self._get_agent_update_handler(test_data=data_file) as (agent_update_handler, mock_telemetry): + with self.assertRaises(AgentUpgradeExitException) as context: + agent_update_handler.run(agent_update_handler._protocol.get_goal_state(), True) + self._assert_update_discovered_from_agent_manifest(mock_telemetry, version="99999.0.0.0") + 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_not_update_to_version_if_version_not_from_rsm(self): + self.prepare_agents(count=1) data_file = DATA_FILE.copy() - data_file["ext_conf"] = "wire/ext_conf_requested_version.xml" + data_file["ext_conf"] = "wire/ext_conf_version_not_from_rsm.xml" + downgraded_version = "2.5.0" + + with self._get_agent_update_handler(test_data=data_file) as (agent_update_handler, _): + agent_update_handler._protocol.mock_wire_data.set_version_in_agent_family(downgraded_version) + agent_update_handler._protocol.mock_wire_data.set_incarnation(2) + agent_update_handler._protocol.client.update_goal_state() + agent_update_handler.run(agent_update_handler._protocol.get_goal_state(), True) + self._assert_agent_directories_exist_and_others_dont_exist( + versions=[str(CURRENT_VERSION)]) + self.assertFalse(os.path.exists(self.agent_dir(downgraded_version)), + "New agent directory should not be found") + + def test_handles_if_rsm_version_not_found_in_pkgs_to_download(self): + data_file = DATA_FILE.copy() + data_file["ext_conf"] = "wire/ext_conf_rsm_version.xml" # Set the test environment by adding 20 random agents to the agent directory self.prepare_agents() @@ -302,17 +335,17 @@ def test_handles_if_requested_version_not_found_in_pkgs_to_download(self): version = "5.2.0.4" - with self.__get_agent_update_handler(test_data=data_file) as (agent_update_handler, mock_telemetry): - agent_update_handler._protocol.mock_wire_data.set_extension_config_requested_version(version) + with self._get_agent_update_handler(test_data=data_file) 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() - agent_update_handler.run(agent_update_handler._protocol.get_goal_state()) + agent_update_handler.run(agent_update_handler._protocol.get_goal_state(), True) - self.__assert_agent_requested_version_in_goal_state(mock_telemetry, inc=2, version=version) + self._assert_agent_rsm_version_in_goal_state(mock_telemetry, inc=2, version=version) self.assertFalse(os.path.exists(self.agent_dir(version)), "New agent directory should not be found") - self.__assert_no_agent_package_telemetry_emitted(mock_telemetry, version=version) + self._assert_no_agent_package_telemetry_emitted(mock_telemetry, version=version) def test_handles_missing_agent_family(self): data_file = DATA_FILE.copy() @@ -322,8 +355,8 @@ def test_handles_missing_agent_family(self): self.prepare_agents() self.assertEqual(20, self.agent_count(), "Agent directories not set properly") - with self.__get_agent_update_handler(test_data=data_file) as (agent_update_handler, mock_telemetry): - agent_update_handler.run(agent_update_handler._protocol.get_goal_state()) + with self._get_agent_update_handler(test_data=data_file) as (agent_update_handler, mock_telemetry): + agent_update_handler.run(agent_update_handler._protocol.get_goal_state(), True) self.assertFalse(os.path.exists(self.agent_dir("99999.0.0.0")), "New agent directory should not be found") @@ -335,15 +368,14 @@ def test_handles_missing_agent_family(self): def test_it_should_report_update_status_with_success(self): data_file = DATA_FILE.copy() - data_file["ext_conf"] = "wire/ext_conf_requested_version.xml" + data_file["ext_conf"] = "wire/ext_conf_rsm_version.xml" - with self.__get_agent_update_handler(test_data=data_file) as (agent_update_handler, _): - GAUpdateReportState.report_error_msg = "" - agent_update_handler._protocol.mock_wire_data.set_extension_config_requested_version( + with self._get_agent_update_handler(test_data=data_file) as (agent_update_handler, _): + agent_update_handler._protocol.mock_wire_data.set_version_in_agent_family( str(CURRENT_VERSION)) agent_update_handler._protocol.mock_wire_data.set_incarnation(2) agent_update_handler._protocol.client.update_goal_state() - agent_update_handler.run(agent_update_handler._protocol.get_goal_state()) + agent_update_handler.run(agent_update_handler._protocol.get_goal_state(), True) vm_agent_update_status = agent_update_handler.get_vmagent_update_status() self.assertEqual(VMAgentUpdateStatuses.Success, vm_agent_update_status.status) self.assertEqual(0, vm_agent_update_status.code) @@ -351,28 +383,26 @@ def test_it_should_report_update_status_with_success(self): def test_it_should_report_update_status_with_error_on_download_fail(self): data_file = DATA_FILE.copy() - data_file["ext_conf"] = "wire/ext_conf_requested_version.xml" + data_file["ext_conf"] = "wire/ext_conf_rsm_version.xml" - with self.__get_agent_update_handler(test_data=data_file, protocol_get_error=True) as (agent_update_handler, _): - GAUpdateReportState.report_error_msg = "" - agent_update_handler.run(agent_update_handler._protocol.get_goal_state()) + with self._get_agent_update_handler(test_data=data_file, protocol_get_error=True) as (agent_update_handler, _): + agent_update_handler.run(agent_update_handler._protocol.get_goal_state(), True) 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.assertEqual("9.9.9.10", vm_agent_update_status.expected_version) - self.assertIn("Unable to download Agent", vm_agent_update_status.message) + self.assertIn("Downloaded agent version is in bad state", vm_agent_update_status.message) - def test_it_should_report_update_status_with_missing_requested_version_error(self): + def test_it_should_report_update_status_with_missing_rsm_version_error(self): data_file = DATA_FILE.copy() - data_file['ext_conf'] = "wire/ext_conf.xml" + data_file['ext_conf'] = "wire/ext_conf_version_missing_in_agent_family.xml" - with self.__get_agent_update_handler(test_data=data_file, protocol_get_error=True) as (agent_update_handler, _): - GAUpdateReportState.report_error_msg = "" - agent_update_handler.run(agent_update_handler._protocol.get_goal_state()) + with self._get_agent_update_handler(test_data=data_file, protocol_get_error=True) as (agent_update_handler, _): + agent_update_handler.run(agent_update_handler._protocol.get_goal_state(), True) 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("Missing requested version", 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() @@ -382,8 +412,8 @@ def test_it_should_not_log_same_error_next_hours(self): self.prepare_agents() self.assertEqual(20, self.agent_count(), "Agent directories not set properly") - with self.__get_agent_update_handler(test_data=data_file) as (agent_update_handler, mock_telemetry): - agent_update_handler.run(agent_update_handler._protocol.get_goal_state()) + with self._get_agent_update_handler(test_data=data_file) as (agent_update_handler, mock_telemetry): + agent_update_handler.run(agent_update_handler._protocol.get_goal_state(), True) self.assertFalse(os.path.exists(self.agent_dir("99999.0.0.0")), "New agent directory should not be found") @@ -393,9 +423,29 @@ def test_it_should_not_log_same_error_next_hours(self): 'message'] and kwarg[ 'op'] == WALAEventOperation.AgentUpgrade]), "Agent manifest should not be in GS") - agent_update_handler.run(agent_update_handler._protocol.get_goal_state()) + agent_update_handler.run(agent_update_handler._protocol.get_goal_state(), True) self.assertEqual(1, len([kwarg['message'] for _, kwarg in mock_telemetry.call_args_list if "No manifest links found for agent family" in kwarg[ 'message'] and kwarg[ - 'op'] == WALAEventOperation.AgentUpgrade]), "Agent manifest should not be in GS") \ No newline at end of file + 'op'] == WALAEventOperation.AgentUpgrade]), "Agent manifest should not be in GS") + + def test_it_should_save_rsm_state_of_the_most_recent_goal_state(self): + data_file = DATA_FILE.copy() + data_file["ext_conf"] = "wire/ext_conf_rsm_version.xml" + + with self._get_agent_update_handler(test_data=data_file) as (agent_update_handler, _): + with self.assertRaises(AgentUpgradeExitException): + agent_update_handler.run(agent_update_handler._protocol.get_goal_state(), True) + + state_file = os.path.join(conf.get_lib_dir(), "rsm_update.json") + self.assertTrue(os.path.exists(state_file), "The rsm state file was not saved (can't find {0})".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") + agent_update_handler._protocol.mock_wire_data.set_incarnation(2) + agent_update_handler._protocol.client.update_goal_state() + with self.assertRaises(AgentUpgradeExitException): + agent_update_handler.run(agent_update_handler._protocol.get_goal_state(), True) + + self.assertFalse(os.path.exists(state_file), "The rsm file should be removed (file: {0})".format(state_file)) diff --git a/tests/ga/test_update.py b/tests/ga/test_update.py index 8bac67746c..286dfb0b70 100644 --- a/tests/ga/test_update.py +++ b/tests/ga/test_update.py @@ -21,7 +21,7 @@ from datetime import datetime, timedelta from threading import current_thread from azurelinuxagent.ga.guestagent import GuestAgent, GuestAgentError, \ - AGENT_ERROR_FILE, GAUpdateReportState + AGENT_ERROR_FILE from tests.common.osutil.test_default import TestOSUtil import azurelinuxagent.common.osutil.default as osutil @@ -1268,12 +1268,11 @@ def put_handler(url, *args, **_): protocol.aggregate_status = json.loads(args[0]) return MockHttpResponse(status=201) - def update_goal_state_and_run_handler(autoupdate_enabled = True): + def update_goal_state_and_run_handler(autoupdate_enabled=True): protocol.incarnation += 1 protocol.mock_wire_data.set_incarnation(protocol.incarnation) self._add_write_permission_to_goal_state_files() with _get_update_handler(iterations=1, protocol=protocol, autoupdate_enabled=autoupdate_enabled) as (update_handler, _): - GAUpdateReportState.report_error_msg = "" update_handler.run(debug=True) self.assertEqual(0, update_handler.get_exit_code(), "Exit code should be 0; List of all warnings logged by the agent: {0}".format( @@ -1281,20 +1280,19 @@ def update_goal_state_and_run_handler(autoupdate_enabled = True): protocol.set_http_handlers(http_get_handler=get_handler, http_put_handler=put_handler) - # Case 1: Requested version removed in GS; report missing requested version errr - protocol.mock_wire_data.set_extension_config("wire/ext_conf.xml") - protocol.mock_wire_data.reload() + # Case 1: rsm version missing in GS when vm opt-in for rsm upgrades; report missing rsm version error + protocol.mock_wire_data.set_extension_config("wire/ext_conf_version_missing_in_agent_family.xml") update_goal_state_and_run_handler() self.assertTrue("updateStatus" in protocol.aggregate_status['aggregateStatus']['guestAgentStatus'], "updateStatus should be reported") 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("Missing requested version", 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: Requested version in GS == Current Version; updateStatus should be Success - protocol.mock_wire_data.set_extension_config("wire/ext_conf_requested_version.xml") - protocol.mock_wire_data.set_extension_config_requested_version(str(CURRENT_VERSION)) + # 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") + protocol.mock_wire_data.set_version_in_agent_family(str(CURRENT_VERSION)) update_goal_state_and_run_handler() self.assertTrue("updateStatus" in protocol.aggregate_status['aggregateStatus']['guestAgentStatus'], "updateStatus should be reported if asked in GS") @@ -1303,9 +1301,9 @@ def update_goal_state_and_run_handler(autoupdate_enabled = True): self.assertEqual(update_status['expectedVersion'], str(CURRENT_VERSION), "incorrect version reported") self.assertEqual(update_status['code'], 0, "incorrect code reported") - # Case 3: Requested version in GS != Current Version; update fail and report error - protocol.mock_wire_data.set_extension_config("wire/ext_conf_requested_version.xml") - protocol.mock_wire_data.set_extension_config_requested_version("5.2.0.1") + # Case 3: rsm version in GS != Current Version; update fail and report error + protocol.mock_wire_data.set_extension_config("wire/ext_conf_rsm_version.xml") + protocol.mock_wire_data.set_version_in_agent_family("5.2.0.1") update_goal_state_and_run_handler() self.assertTrue("updateStatus" in protocol.aggregate_status['aggregateStatus']['guestAgentStatus'], "updateStatus should be in status blob. Warns: {0}".format(patch_warn.call_args_list)) @@ -1436,8 +1434,8 @@ def create_conf_mocks(self, autoupdate_frequency, hotfix_frequency, normal_frequ # 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_hotfix_upgrade_frequency", return_value=hotfix_frequency): - with patch("azurelinuxagent.common.conf.get_normal_upgrade_frequency", return_value=normal_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"): with patch("azurelinuxagent.common.conf.get_enable_ga_versioning", return_value=True): yield @@ -1480,7 +1478,7 @@ def __assert_exit_code_successful(self, update_handler): def __assert_upgrade_telemetry_emitted(self, mock_telemetry, upgrade=True, version="9.9.9.10"): upgrade_event_msgs = [kwarg['message'] for _, kwarg in mock_telemetry.call_args_list if - 'Agent update found, exiting current process to {0} to the new Agent version {1}'.format( + 'Agent completed all update checks, exiting current process to {0} to the new Agent version {1}'.format( "upgrade" if upgrade else "downgrade", version) in kwarg['message'] and kwarg[ 'op'] == WALAEventOperation.AgentUpgrade] self.assertEqual(1, len(upgrade_event_msgs), @@ -1507,7 +1505,7 @@ def __assert_ga_version_in_status(self, aggregate_status, version=str(CURRENT_VE def test_it_should_upgrade_agent_on_process_start_if_auto_upgrade_enabled(self): data_file = wire_protocol_data.DATA_FILE.copy() - data_file["ext_conf"] = "wire/ext_conf_requested_version.xml" + data_file["ext_conf"] = "wire/ext_conf_rsm_version.xml" with self.__get_update_handler(test_data=data_file, iterations=10) as (update_handler, mock_telemetry): update_handler.run(debug=True) @@ -1516,16 +1514,17 @@ def test_it_should_upgrade_agent_on_process_start_if_auto_upgrade_enabled(self): self.__assert_agent_directories_available(versions=["9.9.9.10"]) self.__assert_upgrade_telemetry_emitted(mock_telemetry) - def test_it_should_not_update_agent_if_last_update_time_not_permitted(self): + def test_it_should_not_update_agent_with_rsm_if_gs_not_updated_in_next_attempts(self): no_of_iterations = 10 data_file = DATA_FILE.copy() - data_file['ext_conf'] = "wire/ext_conf_requested_version.xml" + data_file['ext_conf'] = "wire/ext_conf_rsm_version.xml" self.prepare_agents(1) test_frequency = 10 with self.__get_update_handler(iterations=no_of_iterations, test_data=data_file, autoupdate_frequency=test_frequency) as (update_handler, _): - update_handler._protocol.mock_wire_data.set_ga_manifest_version_version("5.2.0.1") + # Given version which will fail on first attempt, then rsm shouldn't make any futher attempts since GS is not updated + update_handler._protocol.mock_wire_data.set_version_in_agent_family("5.2.1.0") update_handler._protocol.mock_wire_data.set_incarnation(2) update_handler.run(debug=True) @@ -1533,6 +1532,8 @@ def test_it_should_not_update_agent_if_last_update_time_not_permitted(self): self.assertEqual(no_of_iterations, update_handler.get_iterations(), "Update handler should've run its course") self.assertFalse(os.path.exists(self.agent_dir("5.2.0.1")), "New agent directory should not be found") + self.assertGreaterEqual(update_handler._protocol.mock_wire_data.call_counts["manifest_of_ga.xml"], 1, + "only 1 agent manifest call should've been made - 1 per incarnation") def test_it_should_not_auto_upgrade_if_auto_update_disabled(self): with self.__get_update_handler(iterations=10) as (update_handler, _): @@ -1544,9 +1545,9 @@ def test_it_should_not_auto_upgrade_if_auto_update_disabled(self): self.assertFalse(os.path.exists(self.agent_dir("99999.0.0.0")), "New agent directory should not be found") - def test_it_should_download_only_requested_version_if_available(self): + def test_it_should_download_only_rsm_version_if_available(self): data_file = wire_protocol_data.DATA_FILE.copy() - data_file["ext_conf"] = "wire/ext_conf_requested_version.xml" + data_file["ext_conf"] = "wire/ext_conf_rsm_version.xml" with self.__get_update_handler(test_data=data_file) as (update_handler, mock_telemetry): update_handler.run(debug=True) @@ -1556,7 +1557,7 @@ def test_it_should_download_only_requested_version_if_available(self): def test_it_should_download_largest_version_if_ga_versioning_disabled(self): data_file = wire_protocol_data.DATA_FILE.copy() - data_file["ext_conf"] = "wire/ext_conf_requested_version.xml" + data_file["ext_conf"] = "wire/ext_conf_rsm_version.xml" with self.__get_update_handler(test_data=data_file) as (update_handler, mock_telemetry): with patch.object(conf, "get_enable_ga_versioning", return_value=False): update_handler.run(debug=True) @@ -1565,9 +1566,9 @@ def test_it_should_download_largest_version_if_ga_versioning_disabled(self): self.__assert_upgrade_telemetry_emitted(mock_telemetry, version="99999.0.0.0") self.__assert_agent_directories_exist_and_others_dont_exist(versions=["99999.0.0.0"]) - def test_it_should_cleanup_all_agents_except_requested_version_and_current_version(self): + def test_it_should_cleanup_all_agents_except_rsm_version_and_current_version(self): data_file = wire_protocol_data.DATA_FILE.copy() - data_file["ext_conf"] = "wire/ext_conf_requested_version.xml" + data_file["ext_conf"] = "wire/ext_conf_rsm_version.xml" # Set the test environment by adding 20 random agents to the agent directory self.prepare_agents() @@ -1580,10 +1581,10 @@ def test_it_should_cleanup_all_agents_except_requested_version_and_current_versi self.__assert_upgrade_telemetry_emitted(mock_telemetry, version="9.9.9.10") self.__assert_agent_directories_exist_and_others_dont_exist(versions=["9.9.9.10", str(CURRENT_VERSION)]) - def test_it_should_not_update_if_requested_version_not_found_in_manifest(self): + def test_it_should_not_update_if_rsm_version_not_found_in_manifest(self): self.prepare_agents(1) data_file = wire_protocol_data.DATA_FILE.copy() - data_file["ext_conf"] = "wire/ext_conf_requested_version_missing_in_manifest.xml" + data_file["ext_conf"] = "wire/ext_conf_version_missing_in_manifest.xml" with self.__get_update_handler(test_data=data_file) as (update_handler, mock_telemetry): update_handler.run(debug=True) @@ -1592,18 +1593,18 @@ def test_it_should_not_update_if_requested_version_not_found_in_manifest(self): agent_msgs = [kwarg for _, kwarg in mock_telemetry.call_args_list if kwarg['op'] in (WALAEventOperation.AgentUpgrade, WALAEventOperation.Download)] # This will throw if corresponding message not found so not asserting on that - requested_version_found = next(kwarg for kwarg in agent_msgs if - "discovered new agent version:5.2.1.0 in agent manifest for goal state incarnation_1, will update the agent before processing the goal state" in kwarg['message']) - self.assertTrue(requested_version_found['is_success'], - "The requested version found op should be reported as a success") + rsm_version_found = next(kwarg for kwarg in agent_msgs if + "New agent version:5.2.1.0 requested by RSM in Goal state incarnation_1, will update the agent before processing the goal state" in kwarg['message']) + self.assertTrue(rsm_version_found['is_success'], + "The rsm version found op should be reported as a success") skipping_update = next(kwarg for kwarg in agent_msgs if - "No matching package found in the agent manifest for requested version: 5.2.1.0 in goal state incarnation: incarnation_1, skipping agent update" in kwarg['message']) + "No matching package found in the agent manifest for version: 5.2.1.0 in goal state incarnation: incarnation_1, skipping agent update" in kwarg['message']) self.assertEqual(skipping_update['version'], str(CURRENT_VERSION), "The not found message should be reported from current agent version") self.assertFalse(skipping_update['is_success'], "The not found op should be reported as a failure") - def test_it_should_try_downloading_requested_version_on_new_incarnation(self): + def test_it_should_try_downloading_rsm_version_on_new_incarnation(self): no_of_iterations = 1000 # Set the test environment by adding 20 random agents to the agent directory @@ -1620,8 +1621,8 @@ def reload_conf(url, protocol): # Ensure we didn't try to download any agents except during the incarnation change self.__assert_agent_directories_available(versions=[str(CURRENT_VERSION)]) - # Update the requested version to "99999.0.0.0" - update_handler._protocol.mock_wire_data.set_extension_config_requested_version("99999.0.0.0") + # Update the rsm version to "99999.0.0.0" + update_handler._protocol.mock_wire_data.set_version_in_agent_family("99999.0.0.0") reload_conf.call_count += 1 self._add_write_permission_to_goal_state_files() reload_conf.incarnation += 1 @@ -1631,9 +1632,9 @@ def reload_conf(url, protocol): reload_conf.incarnation = 2 data_file = wire_protocol_data.DATA_FILE.copy() - data_file["ext_conf"] = "wire/ext_conf_requested_version.xml" + data_file["ext_conf"] = "wire/ext_conf_rsm_version.xml" with self.__get_update_handler(iterations=no_of_iterations, test_data=data_file, reload_conf=reload_conf) as (update_handler, mock_telemetry): - update_handler._protocol.mock_wire_data.set_extension_config_requested_version(str(CURRENT_VERSION)) + update_handler._protocol.mock_wire_data.set_version_in_agent_family(str(CURRENT_VERSION)) update_handler._protocol.mock_wire_data.set_incarnation(2) update_handler.run(debug=True) @@ -1646,7 +1647,7 @@ def reload_conf(url, protocol): self.assertGreaterEqual(update_handler._protocol.mock_wire_data.call_counts["manifest_of_ga.xml"], 1, "only 1 agent manifest call should've been made - 1 per incarnation") - def test_it_should_update_to_largest_version_if_requested_version_not_available(self): + def test_it_should_update_to_largest_version_if_rsm_version_not_available(self): no_of_iterations = 100 # Set the test environment by adding 20 random agents to the agent directory @@ -1661,12 +1662,12 @@ def reload_conf(url, protocol): "goalstate"] >= 5: reload_conf.call_count += 1 - # By this point, the GS with requested version should've been executed. Verify that + # By this point, the GS with rsm version should've been executed. Verify that self.__assert_agent_directories_available(versions=[str(CURRENT_VERSION)]) - # Update the ext-conf and incarnation and remove requested versions from GS, - # this should download all versions requested in config - mock_wire_data.data_files["ext_conf"] = "wire/ext_conf.xml" + # Update the ga_manifest and incarnation to send largest version manifest + # this should download largest version requested in config + mock_wire_data.data_files["ga_manifest"] = "wire/ga_manifest.xml" mock_wire_data.reload() self._add_write_permission_to_goal_state_files() reload_conf.incarnation += 1 @@ -1676,9 +1677,9 @@ def reload_conf(url, protocol): reload_conf.incarnation = 2 data_file = wire_protocol_data.DATA_FILE.copy() - data_file["ext_conf"] = "wire/ext_conf_requested_version.xml" + data_file["ext_conf"] = "wire/ext_conf.xml" + data_file["ga_manifest"] = "wire/ga_manifest_no_upgrade.xml" with self.__get_update_handler(iterations=no_of_iterations, test_data=data_file, reload_conf=reload_conf) as (update_handler, mock_telemetry): - update_handler._protocol.mock_wire_data.set_extension_config_requested_version(str(CURRENT_VERSION)) update_handler._protocol.mock_wire_data.set_incarnation(2) update_handler.run(debug=True) @@ -1766,16 +1767,16 @@ def reload_conf(url, protocol): self.__assert_upgrade_telemetry_emitted(mock_telemetry, version="99999.0.0.0") self.__assert_agent_directories_exist_and_others_dont_exist(versions=["99999.0.0.0", str(CURRENT_VERSION)]) - def test_it_should_not_download_anything_if_requested_version_is_current_version(self): + def test_it_should_not_download_anything_if_rsm_version_is_current_version(self): data_file = wire_protocol_data.DATA_FILE.copy() - data_file["ext_conf"] = "wire/ext_conf_requested_version.xml" + data_file["ext_conf"] = "wire/ext_conf_rsm_version.xml" # Set the test environment by adding 20 random agents to the agent directory self.prepare_agents() self.assertEqual(20, self.agent_count(), "Agent directories not set properly") with self.__get_update_handler(test_data=data_file) as (update_handler, _): - update_handler._protocol.mock_wire_data.set_extension_config_requested_version(str(CURRENT_VERSION)) + update_handler._protocol.mock_wire_data.set_version_in_agent_family(str(CURRENT_VERSION)) update_handler._protocol.mock_wire_data.set_incarnation(2) update_handler.run(debug=True) @@ -1783,7 +1784,7 @@ def test_it_should_not_download_anything_if_requested_version_is_current_version self.assertFalse(os.path.exists(self.agent_dir("99999.0.0.0")), "New agent directory should not be found") - def test_it_should_skip_wait_to_update_if_requested_version_available(self): + def test_it_should_skip_wait_to_update_if_rsm_version_available(self): no_of_iterations = 100 def reload_conf(url, protocol): @@ -1796,8 +1797,8 @@ def reload_conf(url, protocol): # Assert GA version from status to ensure agent is running fine from the current version self.__assert_ga_version_in_status(protocol.aggregate_status) - # Update the ext-conf and incarnation and add requested version from GS - mock_wire_data.data_files["ext_conf"] = "wire/ext_conf_requested_version.xml" + # Update the ext-conf and incarnation and add rsm version from GS + mock_wire_data.data_files["ext_conf"] = "wire/ext_conf_rsm_version.xml" data_file['ga_manifest'] = "wire/ga_manifest.xml" mock_wire_data.reload() self._add_write_permission_to_goal_state_files() @@ -1814,7 +1815,7 @@ def reload_conf(url, protocol): self.assertGreater(reload_conf.call_count, 0, "Reload conf not updated") self.assertLess(update_handler.get_iterations(), no_of_iterations, - "The code should've exited as soon as requested version was found") + "The code should've exited as soon as rsm version was found") self.__assert_exit_code_successful(update_handler) self.__assert_upgrade_telemetry_emitted(mock_telemetry, version="9.9.9.10") @@ -1827,9 +1828,9 @@ def test_it_should_mark_current_agent_as_bad_version_on_downgrade(self): downgraded_version = "2.5.0" data_file = wire_protocol_data.DATA_FILE.copy() - data_file["ext_conf"] = "wire/ext_conf_requested_version.xml" + data_file["ext_conf"] = "wire/ext_conf_rsm_version.xml" with self.__get_update_handler(test_data=data_file) as (update_handler, mock_telemetry): - update_handler._protocol.mock_wire_data.set_extension_config_requested_version(downgraded_version) + update_handler._protocol.mock_wire_data.set_version_in_agent_family(downgraded_version) update_handler._protocol.mock_wire_data.set_incarnation(2) update_handler.run(debug=True) @@ -1843,6 +1844,43 @@ def test_it_should_mark_current_agent_as_bad_version_on_downgrade(self): "Invalid reason specified for blacklisting agent") self.__assert_agent_directories_exist_and_others_dont_exist(versions=[downgraded_version, str(CURRENT_VERSION)]) + def test_it_should_do_self_update_if_vm_opt_out_rsm_upgrades_later(self): + no_of_iterations = 100 + + # Set the test environment by adding 20 random agents to the agent directory + self.prepare_agents() + self.assertEqual(20, self.agent_count(), "Agent directories not set properly") + def reload_conf(url, protocol): + mock_wire_data = protocol.mock_wire_data + + # This function reloads the conf mid-run to mimic an actual customer scenario + if HttpRequestPredicates.is_goal_state_request(url) and mock_wire_data.call_counts["goalstate"] >= 5: + reload_conf.call_count += 1 + + # Assert GA version from status to ensure agent is running fine from the current version + self.__assert_ga_version_in_status(protocol.aggregate_status) + + # Update is_vm_enabled_for_rsm_upgrades flag to False + update_handler._protocol.mock_wire_data.set_extension_config_is_vm_enabled_for_rsm_upgrades("False") + self._add_write_permission_to_goal_state_files() + mock_wire_data.set_incarnation(2) + + reload_conf.call_count = 0 + + data_file = wire_protocol_data.DATA_FILE.copy() + data_file['ext_conf'] = "wire/ext_conf_rsm_version.xml" + with self.__get_update_handler(iterations=no_of_iterations, test_data=data_file, reload_conf=reload_conf) as (update_handler, mock_telemetry): + update_handler._protocol.mock_wire_data.set_version_in_agent_family(str(CURRENT_VERSION)) + update_handler._protocol.mock_wire_data.set_incarnation(20) + update_handler.run(debug=True) + + self.assertGreater(reload_conf.call_count, 0, "Reload conf not updated") + self.assertLess(update_handler.get_iterations(), no_of_iterations, + "The code should've exited as soon as version was found") + self.__assert_exit_code_successful(update_handler) + self.__assert_upgrade_telemetry_emitted(mock_telemetry, version="99999.0.0.0") + self.__assert_agent_directories_exist_and_others_dont_exist(versions=["99999.0.0.0", str(CURRENT_VERSION)]) + @patch('azurelinuxagent.ga.update.get_collect_telemetry_events_handler') @patch('azurelinuxagent.ga.update.get_send_telemetry_events_handler') @@ -2539,4 +2577,4 @@ def test_inequality_operator_should_return_false_on_items_with_same_value(self): if __name__ == '__main__': - unittest.main() + unittest.main() \ No newline at end of file diff --git a/tests/lib/wire_protocol_data.py b/tests/lib/wire_protocol_data.py index 2bc18e34f1..9502a64133 100644 --- a/tests/lib/wire_protocol_data.py +++ b/tests/lib/wire_protocol_data.py @@ -463,8 +463,11 @@ def set_extension_config(self, ext_conf_file): def set_ga_manifest(self, ga_manifest): self.ga_manifest = load_data(ga_manifest) - def set_extension_config_requested_version(self, version): + def set_version_in_agent_family(self, version): self.ext_conf = WireProtocolData.replace_xml_element_value(self.ext_conf, "Version", version) + def set_extension_config_is_vm_enabled_for_rsm_upgrades(self, is_vm_enabled_for_rsm_upgrades): + self.ext_conf = WireProtocolData.replace_xml_element_value(self.ext_conf, "IsVMEnabledForRSMUpgrades", is_vm_enabled_for_rsm_upgrades) + def set_ga_manifest_version_version(self, version): self.ga_manifest = WireProtocolData.replace_xml_element_value(self.ga_manifest, "Version", version) diff --git a/tests_e2e/tests/scripts/agent_update-wait_for_rsm_gs.py b/tests_e2e/tests/scripts/agent_update-wait_for_rsm_gs.py index 016bcd8c62..c65047903a 100755 --- a/tests_e2e/tests/scripts/agent_update-wait_for_rsm_gs.py +++ b/tests_e2e/tests/scripts/agent_update-wait_for_rsm_gs.py @@ -35,8 +35,8 @@ def get_requested_version(gs: GoalState) -> str: raise Exception( u"No manifest links found for agent family Test, skipping agent update verification") manifest = agent_family_manifests[0] - if manifest.is_requested_version_specified and manifest.requested_version is not None: - return str(manifest.requested_version) + if manifest.is_requested_version_specified and manifest.version is not None: + return str(manifest.version) return ""