Repository: Azure/WALinuxAgent
Pull request #2314 (2021-07-23 00:44:11)

Merge fast-track into develop


Patch:
diff --git a/azurelinuxagent/common/conf.py b/azurelinuxagent/common/conf.py
index 67798a47..f79a80b2 100644
--- a/azurelinuxagent/common/conf.py
+++ b/azurelinuxagent/common/conf.py
@@ -118,6 +118,7 @@ __SWITCH_OPTIONS__ = {
     "Debug.CgroupLogMetrics": False,
     "Debug.CgroupDisableOnProcessCheckFailure": True,
     "Debug.CgroupDisableOnQuotaCheckFailure": True,
+    "Debug.FetchVmSettings": False,
 }
 
 
@@ -507,3 +508,11 @@ def get_cgroup_disable_on_quota_check_failure(conf=__conf__):
     NOTE: This option is experimental and may be removed in later versions of the Agent.
     """
     return conf.get_switch("Debug.CgroupDisableOnQuotaCheckFailure", True)
+
+def get_fetch_vm_settings(conf=__conf__):
+    """
+    If True, the agent will query the extensions goal state (via the vmSettings API)
+
+    NOTE: This option is experimental and may be removed in later versions of the Agent.
+    """
+    return conf.get_switch("Debug.FetchVmSettings", False)
diff --git a/azurelinuxagent/common/logcollector_manifests.py b/azurelinuxagent/common/logcollector_manifests.py
index f022c656..27eba6ed 100644
--- a/azurelinuxagent/common/logcollector_manifests.py
+++ b/azurelinuxagent/common/logcollector_manifests.py
@@ -40,6 +40,7 @@ echo,
 
 echo,### Gathering Extension Files ###
 copy,$LIB_DIR/*.xml
+copy,$LIB_DIR/VmSettings.*.json
 copy,$LIB_DIR/waagent_status.*.json
 copy,$LIB_DIR/*/status/*.status
 copy,$LIB_DIR/*/config/*.settings
diff --git a/azurelinuxagent/common/protocol/extensions_goal_state.py b/azurelinuxagent/common/protocol/extensions_goal_state.py
new file mode 100644
index 00000000..39ca5d42
--- /dev/null
+++ b/azurelinuxagent/common/protocol/extensions_goal_state.py
@@ -0,0 +1,29 @@
+# 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 re
+
+
+class ExtensionsGoalState(object):
+    def __init__(self, etag, vm_settings):
+        self.etag = etag
+        self.vm_settings = vm_settings
+
+    def get_redacted_vm_settings(self):
+        return re.sub(r'("protectedSettings"\s*:\s*)"[^"]+"', r'\1"***REDACTED***"', self.vm_settings, flags=re.IGNORECASE)
+
diff --git a/azurelinuxagent/common/protocol/hostplugin.py b/azurelinuxagent/common/protocol/hostplugin.py
index 4d23a205..7b94fba7 100644
--- a/azurelinuxagent/common/protocol/hostplugin.py
+++ b/azurelinuxagent/common/protocol/hostplugin.py
@@ -36,6 +36,7 @@ from azurelinuxagent.common.version import AGENT_NAME, AGENT_VERSION, PY_VERSION
 HOST_PLUGIN_PORT = 32526
 
 URI_FORMAT_GET_API_VERSIONS = "http://{0}:{1}/versions"
+URI_FORMAT_VM_SETTINGS = "http://{0}:{1}/vmSettings"
 URI_FORMAT_GET_EXTENSION_ARTIFACT = "http://{0}:{1}/extensionArtifact"
 URI_FORMAT_PUT_VM_STATUS = "http://{0}:{1}/status"
 URI_FORMAT_PUT_LOG = "http://{0}:{1}/vmAgentLog"
@@ -140,6 +141,21 @@ class HostPluginProtocol(object):
 
         return return_val
 
+    def get_vm_settings_request(self, correlation_id):
+        if not self.ensure_initialized():
+            raise ProtocolError("HostGAPlugin: Host plugin channel is not available")
+
+        url = URI_FORMAT_VM_SETTINGS.format(self.endpoint, HOST_PLUGIN_PORT)
+
+        headers = {
+            _HEADER_VERSION: API_VERSION,
+           _HEADER_CONTAINER_ID: self.container_id,
+           _HEADER_HOST_CONFIG_NAME: self.role_config_name,
+           _HEADER_CORRELATION_ID: correlation_id
+        }
+
+        return url, headers
+
     def get_artifact_request(self, artifact_url, artifact_manifest_url=None):
         if not self.ensure_initialized():
             raise ProtocolError("HostGAPlugin: Host plugin channel is not available")
diff --git a/azurelinuxagent/common/protocol/wire.py b/azurelinuxagent/common/protocol/wire.py
index eb8476a5..be6d75e6 100644
--- a/azurelinuxagent/common/protocol/wire.py
+++ b/azurelinuxagent/common/protocol/wire.py
@@ -20,6 +20,7 @@ import json
 import os
 import random
 import time
+import uuid
 import xml.sax.saxutils as saxutils
 from collections import defaultdict
 from datetime import datetime, timedelta
@@ -34,6 +35,7 @@ from azurelinuxagent.common.event import add_event, WALAEventOperation, report_e
 from azurelinuxagent.common.exception import ProtocolNotFoundError, \
     ResourceGoneError, ExtensionDownloadError, InvalidContainerError, ProtocolError, HttpError
 from azurelinuxagent.common.future import httpclient, bytebuffer, ustr
+from azurelinuxagent.common.protocol.extensions_goal_state import ExtensionsGoalState
 from azurelinuxagent.common.protocol.goal_state import GoalState, TRANSPORT_CERT_FILE_NAME, TRANSPORT_PRV_FILE_NAME
 from azurelinuxagent.common.protocol.hostplugin import HostPluginProtocol
 from azurelinuxagent.common.protocol.restapi import DataContract, ExtHandlerPackage, \
@@ -54,6 +56,7 @@ TELEMETRY_URI = "http://{0}/machine?comp=telemetrydata"
 WIRE_SERVER_ADDR_FILE_NAME = "WireServer"
 INCARNATION_FILE_NAME = "Incarnation"
 GOAL_STATE_FILE_NAME = "GoalState.{0}.xml"
+VM_SETTINGS_FILE_NAME = "VmSettings.{0}.json"
 HOSTING_ENV_FILE_NAME = "HostingEnvironmentConfig.xml"
 SHARED_CONF_FILE_NAME = "SharedConfig.xml"
 REMOTE_ACCESS_FILE_NAME = "RemoteAccess.{0}.xml"
@@ -92,12 +95,12 @@ class WireProtocol(DataContract):
         logger.info('Initializing goal state during protocol detection')
         self.client.update_goal_state(forced=True)
 
+    def update_extensions_goal_state(self):
+        self.client.update_extensions_goal_state()
+
     def update_goal_state(self):
         self.client.update_goal_state()
 
-    def try_update_goal_state(self):
-        return self.client.try_update_goal_state()
-
     def update_host_plugin_from_goal_state(self):
         self.client.update_host_plugin_from_goal_state()
 
@@ -123,6 +126,9 @@ class WireProtocol(DataContract):
     def get_incarnation(self):
         return self.client.get_goal_state().incarnation
 
+    def get_etag(self):
+        return self.client.get_etag()
+
     def get_in_vm_gs_metadata(self):
         return self.client.get_ext_conf().in_vm_gs_metadata
 
@@ -574,7 +580,7 @@ class WireClient(object):
         logger.info("Wire server endpoint:{0}", endpoint)
         self._endpoint = endpoint
         self._goal_state = None
-        self._last_try_update_goal_state_failed = False
+        self._extensions_goal_state = None
         self._host_plugin = None
         self.status_blob = StatusBlob(self)
         self.goal_state_flusher = StateFlusher(conf.get_lib_dir())
@@ -645,7 +651,7 @@ class WireClient(object):
     def fetch_manifest_through_host(self, uri):
         host = self.get_host_plugin()
         uri, headers = host.get_artifact_request(uri)
-        response = self.fetch(uri, headers, use_proxy=False, max_retry=1)
+        response, _ = self.fetch(uri, headers, use_proxy=False, max_retry=1)
         return response
 
     def fetch_manifest(self, version_uris, timeout_in_minutes=5, timeout_in_ms=0):
@@ -668,7 +674,7 @@ class WireClient(object):
                 logger.verbose('The specified manifest URL is empty, ignored.')
                 continue
 
-            direct_func = lambda: self.fetch(version.uri, max_retry=1)  # pylint: disable=W0640
+            direct_func = lambda: self.fetch(version.uri, max_retry=1)[0]  # pylint: disable=W0640
             # NOTE: the host_func may be called after refreshing the goal state, be careful about any goal state data
             # in the lambda.
             host_func = lambda: self.fetch_manifest_through_host(version.uri)  # pylint: disable=W0640
@@ -709,19 +715,21 @@ class WireClient(object):
 
         return success
 
-    def fetch(self, uri, headers=None, use_proxy=None, decode=True, max_retry=None):
+    def fetch(self, uri, headers=None, use_proxy=None, decode=True, max_retry=None, ok_codes=None):
         """
         max_retry indicates the maximum number of retries for the HTTP request; None indicates that the default value should be used
+
+        Returns a tuple with the content and headers of the response. The headers are a list of (name, value) tuples.
         """
         logger.verbose("Fetch [{0}] with headers [{1}]", uri, headers)
         content = None
-        response = self._fetch_response(uri, headers, use_proxy, max_retry=max_retry)
+        response = self._fetch_response(uri, headers, use_proxy, max_retry=max_retry, ok_codes=ok_codes)
         if response is not None and not restutil.request_failed(response):
             response_content = response.read()
             content = self.decode_config(response_content) if decode else response_content
-        return content
+        return content, response.getheaders()
 
-    def _fetch_response(self, uri, headers=None, use_proxy=None, max_retry=None):
+    def _fetch_response(self, uri, headers=None, use_proxy=None, max_retry=None, ok_codes=None):
         """
         max_retry indicates the maximum number of retries for the HTTP request; None indicates that the default value should be used
         """
@@ -736,7 +744,7 @@ class WireClient(object):
 
             host_plugin = self.get_host_plugin()
 
-            if restutil.request_failed(resp):
+            if restutil.request_failed(resp, ok_codes=ok_codes):
                 error_response = restutil.read_response_error(resp)
                 msg = "Fetch failed from [{0}]: {1}".format(uri, error_response)
                 logger.warn(msg)
@@ -784,35 +792,39 @@ class WireClient(object):
 
             if new_goal_state is not None:
                 self._goal_state = new_goal_state
+                if conf.get_fetch_vm_settings():
+                    self.update_extensions_goal_state()
                 self._save_goal_state()
                 self._update_host_plugin(new_goal_state.container_id, new_goal_state.role_config_name)
 
         except Exception as exception:
-            raise ProtocolError("Error processing goal state: {0}".format(ustr(exception)))
+            raise ProtocolError("Error fetching goal state: {0}".format(ustr(exception)))
 
-    def try_update_goal_state(self):
-        """
-        Attempts to update the goal state and returns True on success or False on failure, sending telemetry events about the failures.
-        """
+    def update_extensions_goal_state(self):
         try:
-            self.update_goal_state()
+            correlation_id = str(uuid.uuid4())
+            url, headers = self.get_host_plugin().get_vm_settings_request(correlation_id)
+            etag = self.get_etag()
+            if etag is not None:
+                headers['if-none-match'] = etag
+
+            vm_settings, response_headers = self.fetch(url, headers, ok_codes=[httpclient.OK, httpclient.NOT_MODIFIED])
+
+            # The response includes an etag if and only if the VM settings change.
+            response_etag = None
+            for h in response_headers:
+                if h[0].lower() == 'etag':
+                    response_etag = h[1]
+                    break
+            if response_etag is None:
+                return
 
-            if self._last_try_update_goal_state_failed:
-                self._last_try_update_goal_state_failed = False
-                message = u"Retrieving the goal state recovered from previous errors"
-                add_event(AGENT_NAME, op=WALAEventOperation.FetchGoalState, version=CURRENT_VERSION, is_success=True, message=message, log_event=False)
-                logger.info(message)
-        except Exception as e:
-            if not self._last_try_update_goal_state_failed:
-                self._last_try_update_goal_state_failed = True
-                message = u"An error occurred while retrieving the goal state: {0}".format(ustr(e))
-                add_event(AGENT_NAME, op=WALAEventOperation.FetchGoalState, version=CURRENT_VERSION, is_success=False, message=message, log_event=False)
-                message = u"An error occurred while retrieving the goal state: {0}".format(textutil.format_exception(e))
-                logger.warn(message)
-            message = u"Attempts to retrieve the goal state are failing: {0}".format(ustr(e))
-            logger.periodic_warn(logger.EVERY_SIX_HOURS, "[PERIODIC] {0}".format(message))
-            return False
-        return True
+            self._extensions_goal_state = ExtensionsGoalState(response_etag, vm_settings)
+
+        except Exception as exception:
+            # TODO: report HostGAPlugin failure
+            # TODO: raise ProtocolError instead of logging a warning
+            logger.warn("Error fetching extension goal state (correlation id: {0}): {1}".format(correlation_id, ustr(exception)))
 
     def _update_host_plugin(self, container_id, role_config_name):
         if self._host_plugin is not None:
@@ -839,6 +851,9 @@ class WireClient(object):
             if self._goal_state.ext_conf is not None and self._goal_state.ext_conf.xml_text is not None:
                 self._save_cache(self._goal_state.ext_conf.get_redacted_xml_text(), EXT_CONF_FILE_NAME.format(self._goal_state.incarnation))
 
+            if self._extensions_goal_state is not None:
+                self._save_cache(self._extensions_goal_state.get_redacted_vm_settings(), VM_SETTINGS_FILE_NAME.format(self._extensions_goal_state.etag))  # TODO: need to redact settings
+
         except Exception as e:
             logger.warn("Failed to save the goal state to disk: {0}", ustr(e))
 
@@ -847,6 +862,19 @@ class WireClient(object):
             logger.warn("Setting empty Host Plugin object!")
         self._host_plugin = new_host_plugin
 
+    def get_etag(self):
+        """
+        Returns the Etag of the current ExtensionsGoalState, or None, if the goal state has not been retrieved.
+        """
+        if self._extensions_goal_state is None:
+            return None
+        return self._extensions_goal_state.etag
+
+    def get_extensions_goal_state(self):
+        if self._extensions_goal_state is None:
+            raise ProtocolError("Trying to fetch the extensions goal state before initialization!")
+        return self._extensions_goal_state
+
     def get_goal_state(self):
         if self._goal_state is None:
             raise ProtocolError("Trying to fetch goal state before initialization!")
@@ -1045,7 +1073,7 @@ class WireClient(object):
 
         if ext_conf.status_upload_blob is None:
             # the status upload blob is in ExtensionsConfig so force a full goal state refresh
-            self.update_goal_state(forced=True)
+            self.update_goal_state(forced=True)  # FT: This will come from the ExtensionsGoalState
             ext_conf = self.get_ext_conf()
 
         if ext_conf.status_upload_blob is None:
@@ -1263,7 +1291,7 @@ class WireClient(object):
     def get_artifacts_profile_through_host(self, blob):
         host = self.get_host_plugin()
         uri, headers = host.get_artifact_request(blob)
-        profile = self.fetch(uri, headers, use_proxy=False)
+        profile, _ = self.fetch(uri, headers, use_proxy=False)
         return profile
 
     def get_artifacts_profile(self):
@@ -1271,7 +1299,7 @@ class WireClient(object):
 
         if self.has_artifacts_profile_blob():
             blob = self.get_ext_conf().artifacts_profile_blob
-            direct_func = lambda: self.fetch(blob)
+            direct_func = lambda: self.fetch(blob)[0]
             # NOTE: the host_func may be called after refreshing the goal state, be careful about any goal state data
             # in the lambda.
             host_func = lambda: self.get_artifacts_profile_through_host(blob)
diff --git a/azurelinuxagent/common/utils/archive.py b/azurelinuxagent/common/utils/archive.py
index 2ef80103..8e484577 100644
--- a/azurelinuxagent/common/utils/archive.py
+++ b/azurelinuxagent/common/utils/archive.py
@@ -41,6 +41,7 @@ ARCHIVE_DIRECTORY_NAME = 'history'
 _MAX_ARCHIVED_STATES = 50
 
 _CACHE_PATTERNS = [
+    re.compile(r"^VmSettings.\d+\.json$"),
     re.compile(r"^(.*)\.(\d+)\.(agentsManifest)$", re.IGNORECASE),
     re.compile(r"^(.*)\.(\d+)\.(manifest\.xml)$", re.IGNORECASE),
     re.compile(r"^(.*)\.(\d+)\.(xml)$", re.IGNORECASE),
diff --git a/azurelinuxagent/daemon/main.py b/azurelinuxagent/daemon/main.py
index bc97b440..7c397dc5 100644
--- a/azurelinuxagent/daemon/main.py
+++ b/azurelinuxagent/daemon/main.py
@@ -29,7 +29,6 @@ from azurelinuxagent.common.event import add_event, WALAEventOperation, initiali
 from azurelinuxagent.common.future import ustr
 from azurelinuxagent.common.osutil import get_osutil
 from azurelinuxagent.common.protocol.util import get_protocol_util
-from azurelinuxagent.common.protocol.wire import WireProtocol
 from azurelinuxagent.common.rdma import setup_rdma_device
 from azurelinuxagent.common.utils import textutil
 from azurelinuxagent.common.version import AGENT_NAME, AGENT_LONG_NAME, \
@@ -160,8 +159,6 @@ class DaemonHandler(object):
                 #   incarnation number. A forced update ensures the most
                 #   current values.
                 protocol = self.protocol_util.get_protocol()
-                if type(protocol) is not WireProtocol:
-                    raise Exception("Attempt to setup RDMA without Wireserver")
 
                 protocol.client.update_goal_state(forced=True)
 
diff --git a/azurelinuxagent/ga/update.py b/azurelinuxagent/ga/update.py
index 47433536..2c660c81 100644
--- a/azurelinuxagent/ga/update.py
+++ b/azurelinuxagent/ga/update.py
@@ -115,6 +115,7 @@ class UpdateHandler(object):
         self._heartbeat_id = str(uuid.uuid4()).upper()
         self._heartbeat_counter = 0
         self._heartbeat_update_goal_state_error_count = 0
+        self._last_try_update_goal_state_failed = False
 
         self.last_incarnation = None
 
@@ -350,8 +351,32 @@ class UpdateHandler(object):
                 logger.warn("{0} thread died, restarting".format(thread_handler.get_thread_name()))
                 thread_handler.start()
 
+    def _try_update_goal_state(self, protocol):
+        """
+        Attempts to update the goal state and returns True on success or False on failure, sending telemetry events about the failures.
+        """
+        try:
+            protocol.update_goal_state()
+
+            if self._last_try_update_goal_state_failed:
+                self._last_try_update_goal_state_failed = False
+                message = u"Retrieving the goal state recovered from previous errors"
+                add_event(AGENT_NAME, op=WALAEventOperation.FetchGoalState, version=CURRENT_VERSION, is_success=True, message=message, log_event=False)
+                logger.info(message)
+
+        except Exception as e:
+            if not self._last_try_update_goal_state_failed:
+                self._last_try_update_goal_state_failed = True
+                message = u"An error occurred while retrieving the goal state: {0}".format(textutil.format_exception(e))
+                logger.warn(message)
+                add_event(AGENT_NAME, op=WALAEventOperation.FetchGoalState, version=CURRENT_VERSION, is_success=False, message=message, log_event=False)
+            message = u"Attempts to retrieve the goal state are failing: {0}".format(ustr(e))
+            logger.periodic_warn(logger.EVERY_SIX_HOURS, "[PERIODIC] {0}".format(message))
+            return False
+        return True
+
     def _process_goal_state(self, protocol, exthandlers_handler, remote_access_handler):
-        if not protocol.try_update_goal_state():
+        if not self._try_update_goal_state(protocol):
             self._heartbeat_update_goal_state_error_count += 1
             return
 
@@ -366,8 +391,8 @@ class UpdateHandler(object):
         incarnation = protocol.get_incarnation()
 
         try:
-            if incarnation != self.last_incarnation:
-                exthandlers_handler.run()
+            if incarnation != self.last_incarnation:  # TODO: This check should be based in the etag for the extensions goal state
+                exthandlers_handler.run()  # TODO: The info for the extensions within run() should be based on the extensions goal state
 
             # report status always, even if the goal state did not change
             # do it before processing the remote access, since that operation can take a long time
diff --git a/azurelinuxagent/pa/provision/cloudinit.py b/azurelinuxagent/pa/provision/cloudinit.py
index b3980065..79316212 100644
--- a/azurelinuxagent/pa/provision/cloudinit.py
+++ b/azurelinuxagent/pa/provision/cloudinit.py
@@ -50,7 +50,7 @@ class CloudInitProvisionHandler(ProvisionHandler):
             utc_start = datetime.utcnow()
             logger.info("Running CloudInit provisioning handler")
             self.wait_for_ovfenv()
-            self.protocol_util.get_protocol()
+            self.protocol_util.get_protocol()  # Trigger protocol detection
             self.report_not_ready("Provisioning", "Starting")
 
             thumbprint = self.wait_for_ssh_host_key()  # pylint: disable=W0612
diff --git a/azurelinuxagent/pa/provision/default.py b/azurelinuxagent/pa/provision/default.py
index 1baa4097..91fe04ed 100644
--- a/azurelinuxagent/pa/provision/default.py
+++ b/azurelinuxagent/pa/provision/default.py
@@ -78,7 +78,7 @@ class ProvisionHandler(object):
             logger.info("Copying ovf-env.xml")
             ovf_env = self.protocol_util.copy_ovf_env()
 
-            self.protocol_util.get_protocol()
+            self.protocol_util.get_protocol()  # Trigger protocol detection
             self.report_not_ready("Provisioning", "Starting")
             logger.info("Starting provisioning")
 


Test patch:
diff --git a/tests/data/hostgaplugin/vm_settings.json b/tests/data/hostgaplugin/vm_settings.json
new file mode 100644
index 00000000..a2d71eff
--- /dev/null
+++ b/tests/data/hostgaplugin/vm_settings.json
@@ -0,0 +1,58 @@
+{
+    "activityId": "785cb703-7468-4015-b5b8-3eb060b25a81",
+    "correlationId": "38e83195-ad71-4194-91d0-def8af9c4405",
+    "extensionsLastModifiedTickCount": 637607673150222321,
+    "inVMMetadata": {
+        "subscriptionId": "a53f7094-a16c-47af-abe4-b05c05d0d79a",
+        "resourceGroupName": "narrieta-rg",
+        "vmName": "nam-canary",
+        "location": "EastUS2EUAP",
+        "vmId": "fc9e847a-29fd-4ca8-a5ae-59e581425ca7",
+        "vmSize": "Standard_DS1_v2",
+        "osType": "Linux",
+        "vmImage": {
+            "publisher": "Canonical",
+            "offer": "UbuntuServer",
+            "sku": "18.04-LTS",
+            "version": "18.04.202104150"
+        }
+    },
+    "extensionGoalStates": [
+        {
+            "name": "Microsoft.Azure.Monitor.AzureMonitorLinuxAgent",
+            "version": "1.9.1",
+            "location": "https://zrdfepirv2cbn09pr02a.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": "C6E8BB9560A41FDF7B90894738AE82AFA3440EE8",
+                    "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://zrdfepirv2cbn09pr02a.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": "C6E8BB9560A41FDF7B90894738AE82AFA3440EE8",
+                    "protectedSettings": "MIIBsAYJKoZIhvcNAQcDoIIBoTCCAZ0CAQAxggFpMIIBZQIBADBNMDkxNzA1BgoJkiaJk/IsZAEZFidXaW5kb3dzIEF6dXJlIENSUCBDZXJ0aWZpY2F0ZSBHZW5lcmF0b3ICEFpB/HKM/7evRk+DBz754wUwDQYJKoZIhvcNAQEBBQAEggEAqBkgNZ6L81eyQ3UmHqdWTmq4Ya4r2JCjJbHnQiKeRDd+qUw0WZRKFiQ2ki0QjZeNoVAcjL9WqmrcsU83hb7j5Y/uqIcgpBHCjJ4h7dr5ZyrioONhwtcAPH7fWW1lSi37RYxJ2JrimoGdCLJWEfiN4DjLvfHbNMLkTpUck7SrJFPvnNwxJVN337GhY9rpjaL314M9eAri0f3GLN6DFheBJe++AYH6LXqVvn6WU7DOoFU9T7Fz4oBRzUAxCwPqCDbHxmjJN7zH5Bd0Y2KAt1A2J2o2Dp8lrorVu/XXpahUTz9JcBnZbsnmJh9zjo4cAp1mlxrkrcbFndpwStqcYOudoDArBgkqhkiG9w0BBwEwFAYIKoZIhvcNAwcECANcY8gd0y1AgAi90+baxPAWvg==",
+                    "publicSettings": "{\"enableGenevaUpload\":true}"
+                }
+            ]
+        }
+    ]
+}
\ No newline at end of file
diff --git a/tests/ga/test_update.py b/tests/ga/test_update.py
index e700f43d..8e790a24 100644
--- a/tests/ga/test_update.py
+++ b/tests/ga/test_update.py
@@ -7,6 +7,7 @@ import contextlib
 import glob
 import json
 import os
+import re
 import shutil
 import stat
 import subprocess
@@ -22,7 +23,7 @@ from mock import PropertyMock
 
 from azurelinuxagent.common import conf
 from azurelinuxagent.common.event import EVENTS_DIRECTORY
-from azurelinuxagent.common.exception import ProtocolError, UpdateError, ResourceGoneError
+from azurelinuxagent.common.exception import ProtocolError, UpdateError, ResourceGoneError, HttpError
 from azurelinuxagent.common.future import ustr
 from azurelinuxagent.common.persist_firewall_rules import PersistFirewallRulesHandler
 from azurelinuxagent.common.protocol.goal_state import ExtensionsConfig
@@ -44,7 +45,9 @@ from azurelinuxagent.ga.update import GuestAgent, GuestAgentError, MAX_FAILURE,
 from tests.protocol.mocks import mock_wire_protocol
 from tests.protocol.mockwiredata import DATA_FILE, DATA_FILE_MULTIPLE_EXT
 from tests.tools import AgentTestCase, data_dir, DEFAULT, patch, load_bin_data, load_data, Mock, MagicMock, \
-    clear_singleton_instances, mock_sleep
+    clear_singleton_instances, mock_sleep, skip_if_predicate_true
+from tests.protocol import mockwiredata
+from tests.protocol.mocks import HttpRequestPredicates
 
 NO_ERROR = {
     "last_failure": 0.0,
@@ -1892,6 +1895,137 @@ class TimeMock(Mock):
         return current_time
 
 
+class TryUpdateGoalStateTestCase(HttpRequestPredicates, AgentTestCase):
+    """
+    Tests for UpdateHandler._try_update_goal_state()
+    """
+    def test_it_should_return_true_on_success(self):
+        update_handler = get_update_handler()
+        with mock_wire_protocol(mockwiredata.DATA_FILE) as protocol:
+            self.assertTrue(update_handler._try_update_goal_state(protocol), "try_update_goal_state should have succeeded")
+
+    def test_it_should_return_false_on_failure(self):
+        with mock_wire_protocol(mockwiredata.DATA_FILE) as protocol:
+            def http_get_handler(url, *_, **__):
+                if self.is_goal_state_request(url):
+                    return HttpError('Exception to fake an error retrieving the goal state')
+                return None
+            protocol.set_http_handlers(http_get_handler=http_get_handler)
+
+            update_handler = get_update_handler()
+            self.assertFalse(update_handler._try_update_goal_state(protocol), "try_update_goal_state should have failed")
+
+    @skip_if_predicate_true(lambda: True, "TODO: Enable this test once we start retrieving the ExtensionsGoalState (vmSettings)")
+    def test_it_should_update_the_goal_state(self):
+        update_handler = get_update_handler()
+        with mock_wire_protocol(mockwiredata.DATA_FILE) as protocol:
+            protocol.mock_wire_data.set_incarnation(12345)
+            protocol.mock_wire_data.set_etag('54321')
+
+            # the first goal state should produce an update
+            update_handler._try_update_goal_state(protocol)
+            self.assertEqual(protocol.get_incarnation(), '12345', "The goal state was not updated (received unexpected incarnation)")
+            self.assertEqual(protocol.get_etag(), '54321', "The extensions goal state was not updated (received unexpected ETag)")
+
+            # no changes in the goal state should not produce an update
+            update_handler._try_update_goal_state(protocol)
+            self.assertEqual(protocol.get_incarnation(), '12345', "The goal state should not be updated (received unexpected incarnation)")
+            self.assertEqual(protocol.get_etag(), '54321', "The extensions goal state should not be updated (received unexpected ETag)")
+
+            # a new  goal state should produce an update
+            protocol.mock_wire_data.set_incarnation(6789)
+            protocol.mock_wire_data.set_etag('9876')
+            update_handler._try_update_goal_state(protocol)
+            self.assertEqual(protocol.get_incarnation(), '6789', "The goal state was not updated (received unexpected incarnation)")
+            self.assertEqual(protocol.get_etag(), '9876', "The extensions goal state was not updated (received unexpected ETag)")
+
+    def test_it_should_log_errors_only_when_the_error_state_changes(self):
+        with mock_wire_protocol(mockwiredata.DATA_FILE) as protocol:
+            def http_get_handler(url, *_, **__):
+                if self.is_goal_state_request(url):
+                    if fail_goal_state_request:
+                        return HttpError('Exception to fake an error retrieving the goal state')
+                return None
+
+            protocol.set_http_handlers(http_get_handler=http_get_handler)
+
+            @contextlib.contextmanager
+            def create_log_and_telemetry_mocks():
+                with patch("azurelinuxagent.ga.update.logger", autospec=True) as logger_patcher:
+                    with patch("azurelinuxagent.ga.update.add_event") as add_event_patcher:
+                        yield logger_patcher, add_event_patcher
+
+            calls_to_strings = lambda calls: (str(c) for c in calls)
+            filter_calls = lambda calls, regex=None: (c for c in calls_to_strings(calls) if regex is None or re.match(regex, c))
+            logger_calls = lambda regex=None: [m for m in filter_calls(logger.method_calls, regex)]  # pylint: disable=used-before-assignment,unnecessary-comprehension
+            warnings = lambda: logger_calls(r'call.warn\(.*An error occurred while retrieving the goal state.*')
+            periodic_warnings = lambda: logger_calls(r'call.periodic_warn\(.*Attempts to retrieve the goal state are failing.*')
+            success_messages = lambda: logger_calls(r'call.info\(.*Retrieving the goal state recovered from previous errors.*')
+            telemetry_calls = lambda regex=None: [m for m in filter_calls(add_event.mock_calls, regex)]  # pylint: disable=used-before-assignment,unnecessary-comprehension
+            goal_state_events = lambda: telemetry_calls(r".*op='FetchGoalState'.*")
+
+            #
+            # Initially calls to retrieve the goal state are successful...
+            #
+            update_handler = get_update_handler()
+            fail_goal_state_request = False
+            with create_log_and_telemetry_mocks() as (logger, add_event):
+                update_handler._try_update_goal_state(protocol)
+
+                lc = logger_calls()
+                self.assertTrue(len(lc) == 0, "A successful call should not produce any log messages: [{0}]".format(lc))
+
+                tc = telemetry_calls()
+                self.assertTrue(len(tc) == 0, "A successful call should not produce any telemetry events: [{0}]".format(tc))
+
+            #
+            # ... then an error happens...
+            #
+            fail_goal_state_request = True
+            with create_log_and_telemetry_mocks() as (logger, add_event):
+                update_handler._try_update_goal_state(protocol)
+
+                w = warnings()
+                pw = periodic_warnings()
+                self.assertEqual(1, len(w), "A failure should have produced a warning: [{0}]".format(w))
+                self.assertEqual(1, len(pw), "A failure should have produced a periodic warning: [{0}]".format(pw))
+
+                gs = goal_state_events()
+                self.assertTrue(len(gs) == 1 and 'is_success=False' in gs[0], "A failure should produce a telemetry event (success=false): [{0}]".format(gs))
+
+            #
+            # ... and errors continue happening...
+            #
+            with create_log_and_telemetry_mocks() as (logger, add_event):
+                update_handler._try_update_goal_state(protocol)
+                update_handler._try_update_goal_state(protocol)
+                update_handler._try_update_goal_state(protocol)
+
+                w = warnings()
+                pw = periodic_warnings()
+                self.assertTrue(len(w) == 0, "Subsequent failures should not produce warnings: [{0}]".format(w))
+                self.assertEqual(len(pw), 3, "Subsequent failures should produce periodic warnings: [{0}]".format(pw))
+
+                tc = telemetry_calls()
+                self.assertTrue(len(tc) == 0, "Subsequent failures should not produce any telemetry events: [{0}]".format(tc))
+
+            #
+            # ... until we finally succeed
+            #
+            fail_goal_state_request = False
+            with create_log_and_telemetry_mocks() as (logger, add_event):
+                update_handler._try_update_goal_state(protocol)
+
+                s = success_messages()
+                w = warnings()
+                pw = periodic_warnings()
+                self.assertEqual(len(s), 1, "Recovering after failures should have produced an info message: [{0}]".format(s))
+                self.assertTrue(len(w) == 0 and len(pw) == 0, "Recovering after failures should have not produced any warnings: [{0}] [{1}]".format(w, pw))
+
+                gs = goal_state_events()
+                self.assertTrue(len(gs) == 1 and 'is_success=True' in gs[0], "Recovering after failures should produce a telemetry event (success=true): [{0}]".format(gs))
+
+
 class TestProcessGoalState(AgentTestCase):
     """
     Tests for UpdateHandler._process_goal_state
diff --git a/tests/protocol/mockwiredata.py b/tests/protocol/mockwiredata.py
index 337b678d..b85b793d 100644
--- a/tests/protocol/mockwiredata.py
+++ b/tests/protocol/mockwiredata.py
@@ -36,7 +36,9 @@ DATA_FILE = {
         "trans_cert": "wire/trans_cert",
         "test_ext": "ext/sample_ext-1.3.0.zip",
         "remote_access": None,
-        "in_vm_artifacts_profile": None
+        "in_vm_artifacts_profile": None,
+        "vm_settings": "hostgaplugin/vm_settings.json",
+        "ETag": "1"
 }
 
 DATA_FILE_IN_VM_ARTIFACTS_PROFILE = DATA_FILE.copy()
@@ -131,7 +133,8 @@ class WireProtocolData(object):
             "manifest.xml": 0,
             "manifest_of_ga.xml": 0,
             "ExampleHandlerLinux": 0,
-            "in_vm_artifacts_profile": 0
+            "in_vm_artifacts_profile": 0,
+            "vm_settings": 0
         }
         self.data_files = data_files
         self.version_info = None
@@ -147,6 +150,8 @@ class WireProtocolData(object):
         self.ext = None
         self.remote_access = None
         self.in_vm_artifacts_profile = None
+        self.vm_settings = None
+        self.etag = None
 
         self.reload()
 
@@ -164,6 +169,8 @@ class WireProtocolData(object):
         self.trans_prv = load_data(self.data_files.get("trans_prv"))
         self.trans_cert = load_data(self.data_files.get("trans_cert"))
         self.ext = load_bin_data(self.data_files.get("test_ext"))
+        self.vm_settings = load_data(self.data_files.get("vm_settings"))
+        self.etag = self.data_files.get("ETag")
 
         remote_access_data_file = self.data_files.get("remote_access")
         if remote_access_data_file is not None:
@@ -175,6 +182,7 @@ class WireProtocolData(object):
 
     def mock_http_get(self, url, *args, **kwargs):  # pylint: disable=unused-argument
         content = None
+        response_headers = []
 
         resp = MagicMock()
         resp.status = httpclient.OK
@@ -209,6 +217,10 @@ class WireProtocolData(object):
         elif ".vmSettings" in url or ".settings" in url:
             content = self.in_vm_artifacts_profile
             self.call_counts["in_vm_artifacts_profile"] += 1
+        elif "/vmSettings" in url:
+            content = self.vm_settings
+            response_headers = [('ETag', self.etag)]
+            self.call_counts["vm_settings"] += 1
 
         else:
             # A stale GoalState results in a 400 from the HostPlugin
@@ -249,6 +261,7 @@ class WireProtocolData(object):
                 raise Exception("Bad url {0}".format(url))
 
         resp.read = Mock(return_value=content.encode("utf-8"))
+        resp.getheaders = Mock(return_value=response_headers)
         return resp
 
     def mock_http_post(self, url, *args, **kwargs):  # pylint: disable=unused-argument
@@ -334,6 +347,12 @@ class WireProtocolData(object):
             raise Exception("Could not match attribute '{0}' of element '{1}'".format(attribute_name, element_name))
         return new_xml_document
 
+    def set_etag(self, etag):
+        '''
+        Sets the ETag for the mock response
+        '''
+        self.etag = etag
+
     def set_incarnation(self, incarnation):
         '''
         Sets the incarnation in the goal state, but not on its subcomponents (e.g. hosting env, shared config)
diff --git a/tests/protocol/test_goal_state.py b/tests/protocol/test_goal_state.py
new file mode 100644
index 00000000..22fc59e5
--- /dev/null
+++ b/tests/protocol/test_goal_state.py
@@ -0,0 +1,22 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the Apache License.
+
+from azurelinuxagent.common.exception import IncompleteGoalStateError
+from azurelinuxagent.common.protocol.goal_state import GoalState
+from tests.protocol.mocks import mock_wire_protocol
+from tests.protocol import mockwiredata
+from tests.protocol.mocks import HttpRequestPredicates
+from tests.tools import AgentTestCase
+
+class GoalStateTestCase(HttpRequestPredicates, AgentTestCase):
+    def test_incomplete_gs_should_fail(self):
+        with mock_wire_protocol(mockwiredata.DATA_FILE) as protocol:
+            GoalState.fetch_full_goal_state(protocol.client)
+
+            protocol.mock_wire_data.data_files = mockwiredata.DATA_FILE_NOOP_GS
+            protocol.mock_wire_data.reload()
+            protocol.mock_wire_data.set_incarnation(2)
+
+            with self.assertRaises(IncompleteGoalStateError):
+                GoalState.fetch_full_goal_state_if_incarnation_different_than(protocol.client, 1)
+
diff --git a/tests/protocol/test_wire.py b/tests/protocol/test_wire.py
index d85ed095..0ca57934 100644
--- a/tests/protocol/test_wire.py
+++ b/tests/protocol/test_wire.py
@@ -31,7 +31,7 @@ from azurelinuxagent.common.agent_supported_feature import SupportedFeatureNames
     get_agent_supported_features_list_for_crp
 from azurelinuxagent.common.exception import ResourceGoneError, ProtocolError, \
     ExtensionDownloadError, HttpError
-from azurelinuxagent.common.protocol.goal_state import ExtensionsConfig, GoalState
+from azurelinuxagent.common.protocol.goal_state import ExtensionsConfig
 from azurelinuxagent.common.protocol.hostplugin import HostPluginProtocol
 from azurelinuxagent.common.protocol.restapi import VMAgentManifestUri
 from azurelinuxagent.common.protocol.wire import WireProtocol, WireClient, \
@@ -39,7 +39,6 @@ from azurelinuxagent.common.protocol.wire import WireProtocol, WireClient, \
 from azurelinuxagent.common.telemetryevent import GuestAgentExtensionEventsSchema, \
     TelemetryEventParam, TelemetryEvent
 from azurelinuxagent.common.utils import restutil
-from azurelinuxagent.common.exception import IncompleteGoalStateError
 from azurelinuxagent.common.version import CURRENT_VERSION, DISTRO_NAME, DISTRO_VERSION
 from azurelinuxagent.ga.exthandlers import get_exthandlers_handler
 from tests.ga.test_monitor import random_generator
@@ -1205,123 +1204,6 @@ class UpdateGoalStateTestCase(AgentTestCase):
                     "Could not find the expected number of redacted settings. Expected {0}.\n{1}".format(len(protected_settings), extensions_config))
 
 
-class TryUpdateGoalStateTestCase(HttpRequestPredicates, AgentTestCase):
-    """
-    Tests for WireClient.try_update_goal_state()
-    """
-    def test_it_should_return_true_on_success(self):
-        with mock_wire_protocol(mockwiredata.DATA_FILE) as protocol:
-            self.assertTrue(protocol.client.try_update_goal_state(), "try_update_goal_state should have succeeded")
-
-    def test_incomplete_gs_should_fail(self):
-
-        with mock_wire_protocol(mockwiredata.DATA_FILE) as protocol:
-            GoalState.fetch_full_goal_state(protocol.client)
-
-            protocol.mock_wire_data.data_files = mockwiredata.DATA_FILE_NOOP_GS
-            protocol.mock_wire_data.reload()
-            protocol.mock_wire_data.set_incarnation(2)
-
-            with self.assertRaises(IncompleteGoalStateError):
-                GoalState.fetch_full_goal_state_if_incarnation_different_than(protocol.client, 1)
-
-    def test_it_should_return_false_on_failure(self):
-        with mock_wire_protocol(mockwiredata.DATA_FILE) as protocol:
-            def http_get_handler(url, *_, **__):
-                if self.is_goal_state_request(url):
-                    return HttpError('Exception to fake an error retrieving the goal state')
-                return None
-
-            protocol.set_http_handlers(http_get_handler=http_get_handler)
-
-            self.assertFalse(protocol.client.try_update_goal_state(), "try_update_goal_state should have failed")
-
-    def test_it_should_log_errors_only_when_the_error_state_changes(self):
-        with mock_wire_protocol(mockwiredata.DATA_FILE) as protocol:
-            def http_get_handler(url, *_, **__):
-                if self.is_goal_state_request(url):
-                    if fail_goal_state_request:
-                        return HttpError('Exception to fake an error retrieving the goal state')
-                return None
-
-            protocol.set_http_handlers(http_get_handler=http_get_handler)
-
-            @contextlib.contextmanager
-            def create_log_and_telemetry_mocks():
-                with patch("azurelinuxagent.common.protocol.wire.logger", autospec=True) as logger_patcher:
-                    with patch("azurelinuxagent.common.protocol.wire.add_event") as add_event_patcher:
-                        yield logger_patcher, add_event_patcher
-
-            calls_to_strings = lambda calls: (str(c) for c in calls)
-            filter_calls = lambda calls, regex=None: (c for c in calls_to_strings(calls) if regex is None or re.match(regex, c))
-            logger_calls = lambda regex=None: [m for m in filter_calls(logger.method_calls, regex)]  # pylint: disable=used-before-assignment,unnecessary-comprehension
-            warnings = lambda: logger_calls(r'call.warn\(.*An error occurred while retrieving the goal state.*')
-            periodic_warnings = lambda: logger_calls(r'call.periodic_warn\(.*Attempts to retrieve the goal state are failing.*')
-            success_messages = lambda: logger_calls(r'call.info\(.*Retrieving the goal state recovered from previous errors.*')
-            telemetry_calls = lambda regex=None: [m for m in filter_calls(add_event.mock_calls, regex)]  # pylint: disable=used-before-assignment,unnecessary-comprehension
-            goal_state_events = lambda: telemetry_calls(r".*op='FetchGoalState'.*")
-
-            #
-            # Initially calls to retrieve the goal state are successful...
-            #
-            fail_goal_state_request = False
-            with create_log_and_telemetry_mocks() as (logger, add_event):
-                protocol.client.try_update_goal_state()
-
-                lc = logger_calls()
-                self.assertTrue(len(lc) == 0, "A successful call should not produce any log messages: [{0}]".format(lc))
-
-                tc = telemetry_calls()
-                self.assertTrue(len(tc) == 0, "A successful call should not produce any telemetry events: [{0}]".format(tc))
-
-            #
-            # ... then an error happens...
-            #
-            fail_goal_state_request = True
-            with create_log_and_telemetry_mocks() as (logger, add_event):
-                protocol.client.try_update_goal_state()
-
-                w = warnings()
-                pw = periodic_warnings()
-                self.assertEqual(len(w), 1, "A failure should have produced a warning: [{0}]".format(w))
-                self.assertEqual(len(pw), 1, "A failure should have produced a periodic warning: [{0}]".format(pw))
-
-                gs = goal_state_events()
-                self.assertTrue(len(gs) == 1 and 'is_success=False' in gs[0], "A failure should produce a telemetry event (success=false): [{0}]".format(gs))
-
-            #
-            # ... and errors continue happening...
-            #
-            with create_log_and_telemetry_mocks() as (logger, add_event):
-                protocol.client.try_update_goal_state()
-                protocol.client.try_update_goal_state()
-                protocol.client.try_update_goal_state()
-
-                w = warnings()
-                pw = periodic_warnings()
-                self.assertTrue(len(w) == 0, "Subsequent failures should not produce warnings: [{0}]".format(w))
-                self.assertEqual(len(pw), 3, "Subsequent failures should produce periodic warnings: [{0}]".format(pw))
-
-                tc = telemetry_calls()
-                self.assertTrue(len(tc) == 0, "Subsequent failures should not produce any telemetry events: [{0}]".format(tc))
-
-            #
-            # ... until we finally succeed
-            #
-            fail_goal_state_request = False
-            with create_log_and_telemetry_mocks() as (logger, add_event):
-                protocol.client.try_update_goal_state()
-
-                s = success_messages()
-                w = warnings()
-                pw = periodic_warnings()
-                self.assertEqual(len(s), 1, "Recovering after failures should have produced an info message: [{0}]".format(s))
-                self.assertTrue(len(w) == 0 and len(pw) == 0, "Recovering after failures should have not produced any warnings: [{0}] [{1}]".format(w, pw))
-
-                gs = goal_state_events()
-                self.assertTrue(len(gs) == 1 and 'is_success=True' in gs[0], "Recovering after failures should produce a telemetry event (success=true): [{0}]".format(gs))
-
-
 class UpdateHostPluginFromGoalStateTestCase(AgentTestCase):
     """
     Tests for WireClient.update_host_plugin_from_goal_state()
@@ -1368,6 +1250,8 @@ class MockResponse:
     def read(self, *_):
         return self.body
 
+    def getheaders(self):
+        return []
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/tests/test_agent.py b/tests/test_agent.py
index 68f46d11..01e50a4c 100644
--- a/tests/test_agent.py
+++ b/tests/test_agent.py
@@ -33,6 +33,7 @@ Debug.CgroupCheckPeriod = 300
 Debug.CgroupDisableOnProcessCheckFailure = True
 Debug.CgroupDisableOnQuotaCheckFailure = True
 Debug.CgroupLogMetrics = False
+Debug.FetchVmSettings = False
 DetectScvmmEnv = False
 EnableOverProvisioning = True
 Extension.LogDir = /var/log/azure