Repository: Azure/WALinuxAgent
Pull request #2411 (2021-11-15 17:55:14)

Fast Track: Parse and compare in-vm metadata, status upload blob and artifacts profile

codecov[bot]: # [Codecov](https://codecov.io/gh/Azure/WALinuxAgent/pull/2411?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azure) Report
> Merging [#2411](https://codecov.io/gh/Azure/WALinuxAgent/pull/2411?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azure) (7714abb) into [develop](https://codecov.io/gh/Azure/WALinuxAgent/commit/6f1601374d636ca235a41fd1c3458ca81719d643?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azure) (6f16013) will **increase** coverage by `0.27%`.
> The diff coverage is `89.88%`.

[![Impacted file tree graph](https://codecov.io/gh/Azure/WALinuxAgent/pull/2411/graphs/tree.svg?width=650&height=150&src=pr&token=ae90E2KYCw&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azure)](https://codecov.io/gh/Azure/WALinuxAgent/pull/2411?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azure)

```diff
@@             Coverage Diff             @@
##           develop    #2411      +/-   ##
===========================================
+ Coverage    71.15%   71.43%   +0.27%     
===========================================
  Files           97      100       +3     
  Lines        14429    14569     +140     
  Branches      2082     2081       -1     
===========================================
+ Hits         10267    10407     +140     
+ Misses        3703     3699       -4     
- Partials       459      463       +4     
```


| [Impacted Files](https://codecov.io/gh/Azure/WALinuxAgent/pull/2411?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azure) | Coverage Δ | |
|---|---|---|
| [...protocol/extensions\_goal\_state\_from\_vm\_settings.py](https://codecov.io/gh/Azure/WALinuxAgent/pull/2411/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azure#diff-YXp1cmVsaW51eGFnZW50L2NvbW1vbi9wcm90b2NvbC9leHRlbnNpb25zX2dvYWxfc3RhdGVfZnJvbV92bV9zZXR0aW5ncy5weQ==) | `85.38% <85.38%> (ø)` | |
| [...inuxagent/common/protocol/extensions\_goal\_state.py](https://codecov.io/gh/Azure/WALinuxAgent/pull/2411/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azure#diff-YXp1cmVsaW51eGFnZW50L2NvbW1vbi9wcm90b2NvbC9leHRlbnNpb25zX2dvYWxfc3RhdGUucHk=) | `87.50% <86.36%> (+0.96%)` | :arrow_up: |
| [...ol/extensions\_goal\_state\_from\_extensions\_config.py](https://codecov.io/gh/Azure/WALinuxAgent/pull/2411/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azure#diff-YXp1cmVsaW51eGFnZW50L2NvbW1vbi9wcm90b2NvbC9leHRlbnNpb25zX2dvYWxfc3RhdGVfZnJvbV9leHRlbnNpb25zX2NvbmZpZy5weQ==) | `92.13% <92.13%> (ø)` | |
| [azurelinuxagent/common/protocol/wire.py](https://codecov.io/gh/Azure/WALinuxAgent/pull/2411/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azure#diff-YXp1cmVsaW51eGFnZW50L2NvbW1vbi9wcm90b2NvbC93aXJlLnB5) | `79.29% <94.44%> (-0.37%)` | :arrow_down: |
| [...t/common/protocol/extensions\_goal\_state\_factory.py](https://codecov.io/gh/Azure/WALinuxAgent/pull/2411/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azure#diff-YXp1cmVsaW51eGFnZW50L2NvbW1vbi9wcm90b2NvbC9leHRlbnNpb25zX2dvYWxfc3RhdGVfZmFjdG9yeS5weQ==) | `100.00% <100.00%> (ø)` | |
| [azurelinuxagent/common/protocol/restapi.py](https://codecov.io/gh/Azure/WALinuxAgent/pull/2411/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azure#diff-YXp1cmVsaW51eGFnZW50L2NvbW1vbi9wcm90b2NvbC9yZXN0YXBpLnB5) | `98.31% <100.00%> (+0.93%)` | :arrow_up: |
| [azurelinuxagent/ga/exthandlers.py](https://codecov.io/gh/Azure/WALinuxAgent/pull/2411/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azure#diff-YXp1cmVsaW51eGFnZW50L2dhL2V4dGhhbmRsZXJzLnB5) | `85.52% <100.00%> (+0.06%)` | :arrow_up: |
| [azurelinuxagent/ga/collect\_telemetry\_events.py](https://codecov.io/gh/Azure/WALinuxAgent/pull/2411/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azure#diff-YXp1cmVsaW51eGFnZW50L2dhL2NvbGxlY3RfdGVsZW1ldHJ5X2V2ZW50cy5weQ==) | `90.41% <0.00%> (+0.68%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/Azure/WALinuxAgent/pull/2411?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azure).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azure)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/Azure/WALinuxAgent/pull/2411?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azure). Last update [6f16013...7714abb](https://codecov.io/gh/Azure/WALinuxAgent/pull/2411?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azure). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azure).


Patch:
diff --git a/azurelinuxagent/common/AgentGlobals.py b/azurelinuxagent/common/AgentGlobals.py
index 268404f9..dbfda92d 100644
--- a/azurelinuxagent/common/AgentGlobals.py
+++ b/azurelinuxagent/common/AgentGlobals.py
@@ -22,11 +22,13 @@ class AgentGlobals(object):
     This class is used for setting AgentGlobals which can be used all throughout the Agent.
     """
 
+    GUID_ZERO = "00000000-0000-0000-0000-000000000000"
+
     #
     # Some modules (e.g. telemetry) require an up-to-date container ID. We update this variable each time we
     # fetch the goal state.
     #
-    _container_id = "00000000-0000-0000-0000-000000000000"
+    _container_id = GUID_ZERO
 
     @staticmethod
     def get_container_id():
diff --git a/azurelinuxagent/common/protocol/extensions_goal_state.py b/azurelinuxagent/common/protocol/extensions_goal_state.py
index 6f6200d9..2da74031 100644
--- a/azurelinuxagent/common/protocol/extensions_goal_state.py
+++ b/azurelinuxagent/common/protocol/extensions_goal_state.py
@@ -15,21 +15,13 @@
 # limitations under the License.
 #
 # Requires Python 2.6+ and Openssl 1.0+
-
-import json
-import re
-import sys
-
-from collections import defaultdict
-from operator import attrgetter
+import datetime
 
 import azurelinuxagent.common.logger as logger
-from azurelinuxagent.common.event import add_event, WALAEventOperation
-from azurelinuxagent.common.exception import ExtensionsConfigError, VmSettingsError, AgentError
-from azurelinuxagent.common.future import ustr
-from azurelinuxagent.common.protocol.restapi import Extension, ExtHandler, ExtHandlerList, ExtHandlerVersionUri, VMAgentManifest, \
-    VMAgentManifestList, VMAgentManifestUri, InVMGoalStateMetaData, ExtensionState
-from azurelinuxagent.common.utils.textutil import parse_doc, findall, find, findtext, getattrib, gettext, format_exception
+from azurelinuxagent.common.AgentGlobals import AgentGlobals
+from azurelinuxagent.common.exception import AgentError
+from azurelinuxagent.common.utils import textutil
+from azurelinuxagent.common.protocol.restapi import ExtHandlerList, VMAgentManifestList
 
 
 class GoalStateMismatchError(AgentError):
@@ -43,37 +35,50 @@ class ExtensionsGoalState(object):
     ExtensionsConfig when the goal state is retrieved from the WireServe or from vmSettings when it is retrieved from
     the HostGAPlugin.
 
-    NOTE: Do not instantiate this class directly, use one of the create_* methods instead.
+    NOTE: Do not instantiate this class directly, use the ExtensionsGoalStateFactory instead.
     """
     def __init__(self):
-        self._id = None
-        self._text = None
         self.ext_handlers = ExtHandlerList()
         self.vmagent_manifests = VMAgentManifestList()
-        self.in_vm_gs_metadata = InVMGoalStateMetaData()
-        self._status_upload_blob = None
-        self._status_upload_blob_type = None
-        self.artifacts_profile_blob = None
-        self._required_features = []
-
-    @staticmethod
-    def create_empty():
-        return _EmptyExtensionsGoalState()
-
-    @staticmethod
-    def create_from_extensions_config(incarnation, xml_text):
-        return _ExtensionsGoalStateFromExtensionsConfig(incarnation, xml_text)
 
-    @staticmethod
-    def create_from_vm_settings(etag, json_text):
-        return _ExtensionsGoalStateFromVmSettings(etag, json_text)
-
-    def get_id(self):
+    @property
+    def id(self):
         """
         Returns the incarnation number if the ExtensionsGoalState was created from ExtensionsConfig, or the etag if it
         was created from vmSettings.
         """
-        return self._id
+        raise NotImplementedError()
+
+    @property
+    def activity_id(self):
+        raise NotImplementedError()
+
+    @property
+    def correlation_id(self):
+        raise NotImplementedError()
+
+    @property
+    def created_on_timestamp(self):
+        raise NotImplementedError()
+
+    @property
+    def status_upload_blob(self):
+        raise NotImplementedError()
+
+    @property
+    def status_upload_blob_type(self):
+        raise NotImplementedError()
+
+    def _set_status_upload_blob_type(self, value):
+        raise NotImplementedError()
+
+    @property
+    def required_features(self):
+        raise NotImplementedError()
+
+    @property
+    def on_hold(self):
+        raise NotImplementedError()
 
     def get_redacted_text(self):
         """
@@ -81,15 +86,6 @@ class ExtensionsGoalState(object):
         """
         raise NotImplementedError()
 
-    def get_required_features(self):
-        return self._required_features
-
-    def get_status_upload_blob(self):
-        return self._status_upload_blob
-
-    def get_status_upload_blob_type(self):
-        return self._status_upload_blob_type
-
     @staticmethod
     def compare(from_extensions_config, from_vm_settings):
         """
@@ -97,561 +93,98 @@ class ExtensionsGoalState(object):
 
         NOTE: The order of the two instances is important for the debug info to be logged correctly (ExtensionsConfig first, vmSettings second)
         """
-        def raise_error(attribute):
-            message = "Mismatch in ExtensionsConfig (incarnation {0}) and vmSettings (etag {1}).\nAttribute: {2}\n{3} != {4}".format(
-                from_extensions_config.get_id(), from_vm_settings.get_id(),
-                attribute,
-                attrgetter(attribute)(from_extensions_config), attrgetter(attribute)(from_vm_settings)
-            )
-            raise GoalStateMismatchError(message)
-
-        # TODO: HostGAPlugin 112 does not include the status blob; enable these checks once we can ensure a newer version is available. Also enable parsing
-        #       in the _ExtensionsGoalStateFromVmSettings.compare() method
-        # if from_extensions_config.get_status_upload_blob() != from_vm_settings.get_status_upload_blob():
-        #     raise_error("_status_upload_blob")
-        # if from_extensions_config.get_status_upload_blob_type() != from_vm_settings.get_status_upload_blob_type():
-        #     raise_error("_status_upload_blob_type")
-        if from_extensions_config.get_required_features() != from_vm_settings.get_required_features():
-            raise_error("_required_features")
+        def compare_attribute(attribute):
+            from_extensions_config_value = getattr(from_extensions_config, attribute)
+            from_vm_settings_value = getattr(from_vm_settings, attribute)
+
+            if from_extensions_config_value != from_vm_settings_value:
+                message = "Mismatch in ExtensionsConfig (incarnation {0}) and vmSettings (etag {1}).\nAttribute: {2}()\n{3} != {4}".format(
+                    from_extensions_config.id, from_vm_settings.id,
+                    attribute,
+                    from_extensions_config_value, from_vm_settings_value
+                )
+                raise GoalStateMismatchError(message)
+
+        compare_attribute("activity_id")
+        compare_attribute("correlation_id")
+        compare_attribute("created_on_timestamp")
+        compare_attribute("status_upload_blob")
+        compare_attribute("status_upload_blob_type")
+        compare_attribute("required_features")
+        compare_attribute("on_hold")
 
     def _do_common_validations(self):
         """
         Does validations common to vmSettings and ExtensionsConfig
         """
-        if self._status_upload_blob_type not in ["BlockBlob", "PageBlob"]:
-            logger.info("Status Blob type '{0}' is not valid, assuming BlockBlob", self._status_upload_blob)
-            self._status_upload_blob_type = "BlockBlob"
-
-
-class _EmptyExtensionsGoalState(ExtensionsGoalState):
-    def get_redacted_text(self):
-        return ''
-
-
-class _ExtensionsGoalStateFromExtensionsConfig(ExtensionsGoalState):
-    def __init__(self, incarnation, xml_text):
-        super(_ExtensionsGoalStateFromExtensionsConfig, self).__init__()
-        self._id = incarnation
-        self._text = xml_text
-
-        try:
-            self._parse_extensions_config(xml_text)
-            self._do_common_validations()
-        except Exception as e:
-            raise ExtensionsConfigError("Error parsing ExtensionsConfig (incarnation: {0}): {1}\n{2}".format(incarnation, format_exception(e), self.get_redacted_text()))
-
-    def _parse_extensions_config(self, xml_text):
-        xml_doc = parse_doc(xml_text)
-
-        ga_families_list = find(xml_doc, "GAFamilies")
-        ga_families = findall(ga_families_list, "GAFamily")
-
-        for ga_family in ga_families:
-            family = findtext(ga_family, "Name")
-            uris_list = find(ga_family, "Uris")
-            uris = findall(uris_list, "Uri")
-            manifest = VMAgentManifest()
-            manifest.family = family
-            for uri in uris:
-                manifest_uri = VMAgentManifestUri(uri=gettext(uri))
-                manifest.versionsManifestUris.append(manifest_uri)
-            self.vmagent_manifests.vmAgentManifests.append(manifest)
-
-        self.__parse_plugins_and_settings_and_populate_ext_handlers(xml_doc)
-
-        required_features_list = find(xml_doc, "RequiredFeatures")
-        if required_features_list is not None:
-            self._parse_required_features(required_features_list)
-
-        self._status_upload_blob = findtext(xml_doc, "StatusUploadBlob")
-        self.artifacts_profile_blob = findtext(xml_doc, "InVMArtifactsProfileBlob")
-
-        status_upload_node = find(xml_doc, "StatusUploadBlob")
-        self._status_upload_blob_type = getattrib(status_upload_node, "statusBlobType")
-        logger.verbose("Extension config shows status blob type as [{0}]", self._status_upload_blob_type)
-
-        self.in_vm_gs_metadata.parse_node(find(xml_doc, "InVMGoalStateMetaData"))
-
-    def get_redacted_text(self):
-        text = self._text
-        for ext_handler in self.ext_handlers.extHandlers:
-            for extension in ext_handler.properties.extensions:
-                if extension.protectedSettings is not None:
-                    text = text.replace(extension.protectedSettings, "*** REDACTED ***")
-        return text
-
-    def _parse_required_features(self, required_features_list):
-        for required_feature in findall(required_features_list, "RequiredFeature"):
-            feature_name = findtext(required_feature, "Name")
-            # per the documentation, RequiredFeatures also have a "Value" attribute but currently it is not being populated
-            self._required_features.append(feature_name)
-
-    def __parse_plugins_and_settings_and_populate_ext_handlers(self, xml_doc):
-        """
-        Sample ExtensionConfig Plugin and PluginSettings:
-
-        <Plugins>
-          <Plugin name="Microsoft.CPlat.Core.NullSeqB" version="2.0.1" location="https://zrdfepirv2cbn04prdstr01a.blob.core.windows.net/f72653efd9e349ed9842c8b99e4c1712/Microsoft.CPlat.Core_NullSeqB_useast2euap_manifest.xml" state="enabled" autoUpgrade="false" failoverlocation="https://zrdfepirv2cbz06prdstr01a.blob.core.windows.net/f72653efd9e349ed9842c8b99e4c1712/Microsoft.CPlat.Core_NullSeqB_useast2euap_manifest.xml" runAsStartupTask="false" isJson="true" useExactVersion="true" />
-          <Plugin name="Microsoft.CPlat.Core.NullSeqA" version="2.0.1" location="https://zrdfepirv2cbn04prdstr01a.blob.core.windows.net/f72653efd9e349ed9842c8b99e4c1712/Microsoft.CPlat.Core_NullSeqA_useast2euap_manifest.xml" state="enabled" autoUpgrade="false" failoverlocation="https://zrdfepirv2cbn06prdstr01a.blob.core.windows.net/f72653efd9e349ed9842c8b99e4c1712/Microsoft.CPlat.Core_NullSeqA_useast2euap_manifest.xml" runAsStartupTask="false" isJson="true" useExactVersion="true" />
-        </Plugins>
-        <PluginSettings>
-          <Plugin name="Microsoft.CPlat.Core.NullSeqA" version="2.0.1">
-            <DependsOn dependencyLevel="1">
-              <DependsOnExtension handler="Microsoft.CPlat.Core.NullSeqB" />
-            </DependsOn>
-            <RuntimeSettings seqNo="0">{
-              "runtimeSettings": [
-                {
-                  "handlerSettings": {
-                    "publicSettings": {"01_add_extensions_with_dependency":"ff2a3da6-8e12-4ab6-a4ca-4e3a473ab385"}
-                  }
-                }
-              ]
-            }
-            </RuntimeSettings>
-          </Plugin>
-          <Plugin name="Microsoft.CPlat.Core.NullSeqB" version="2.0.1">
-            <RuntimeSettings seqNo="0">{
-              "runtimeSettings": [
-                {
-                  "handlerSettings": {
-                    "publicSettings": {"01_add_extensions_with_dependency":"2e837740-cf7e-4528-b3a4-241002618f05"}
-                  }
-                }
-              ]
-            }
-            </RuntimeSettings>
-          </Plugin>
-        </PluginSettings>
-        """
-
-        plugins_list = find(xml_doc, "Plugins")
-        plugins = findall(plugins_list, "Plugin")
-        plugin_settings_list = find(xml_doc, "PluginSettings")
-        plugin_settings = findall(plugin_settings_list, "Plugin")
-
-        for plugin in plugins:
-            ext_handler = ExtHandler()
-            try:
-                _ExtensionsGoalStateFromExtensionsConfig._parse_plugin(ext_handler, plugin)
-                _ExtensionsGoalStateFromExtensionsConfig._parse_plugin_settings(ext_handler, plugin_settings)
-            except ExtensionsConfigError as error:
-                ext_handler.invalid_setting_reason = ustr(error)
-
-            self.ext_handlers.extHandlers.append(ext_handler)
-
-    @staticmethod
-    def _parse_plugin(ext_handler, plugin):
-        """
-        Sample config:
-
-        <Plugins>
-          <Plugin name="Microsoft.CPlat.Core.NullSeqB" version="2.0.1" location="https://zrdfepirv2cbn04prdstr01a.blob.core.windows.net/f72653efd9e349ed9842c8b99e4c1712/Microsoft.CPlat.Core_NullSeqB_useast2euap_manifest.xml" state="enabled" autoUpgrade="false" failoverlocation="https://zrdfepirv2cbz06prdstr01a.blob.core.windows.net/f72653efd9e349ed9842c8b99e4c1712/Microsoft.CPlat.Core_NullSeqB_useast2euap_manifest.xml" runAsStartupTask="false" isJson="true" useExactVersion="true"><Plugin name="Microsoft.Azure.Extensions.CustomScript" version="1.0" location="https://rdfecurrentuswestcache.blob.core.test-cint.azure-test.net/0e53c53ef0be4178bacb0a1fecf12a74/Microsoft.Azure.Extensions_CustomScript_usstagesc_manifest.xml" state="enabled" autoUpgrade="false" failoverlocation="https://rdfecurrentuswestcache2.blob.core.test-cint.azure-test.net/0e53c53ef0be4178bacb0a1fecf12a74/Microsoft.Azure.Extensions_CustomScript_usstagesc_manifest.xml" runAsStartupTask="false" isJson="true" useExactVersion="true">
-            <additionalLocations>
-              <additionalLocation>https://rdfecurrentuswestcache3.blob.core.test-cint.azure-test.net/0e53c53ef0be4178bacb0a1fecf12a74/Microsoft.Azure.Extensions_CustomScript_usstagesc_manifest.xml</additionalLocation>
-              <additionalLocation>https://rdfecurrentuswestcache4.blob.core.test-cint.azure-test.net/0e53c53ef0be4178bacb0a1fecf12a74/Microsoft.Azure.Extensions_CustomScript_usstagesc_manifest.xml</additionalLocation>
-            </additionalLocations>
-          </Plugin>
-          <Plugin name="Microsoft.CPlat.Core.NullSeqA" version="2.0.1" location="https://zrdfepirv2cbn04prdstr01a.blob.core.windows.net/f72653efd9e349ed9842c8b99e4c1712/Microsoft.CPlat.Core_NullSeqA_useast2euap_manifest.xml" state="enabled" autoUpgrade="false" failoverlocation="https://zrdfepirv2cbn06prdstr01a.blob.core.windows.net/f72653efd9e349ed9842c8b99e4c1712/Microsoft.CPlat.Core_NullSeqA_useast2euap_manifest.xml" runAsStartupTask="false" isJson="true" useExactVersion="true" />
-        </Plugins>
-
-
-        Note that the `additionalLocations` subnode is populated with links
-        generated by PIR for resiliency. In regions with this feature enabled,
-        CRP will provide any extra links in the format above. If no extra links
-        are provided, the subnode will not exist.
-        """
-
-        def _log_error_if_none(attr_name, value):
-            # Plugin Name and Version are very essential fields, without them we wont be able to even report back to CRP
-            # about that handler. For those cases we need to fail the GoalState completely but currently we dont support
-            # reporting status at a GoalState level (we only report at a handler level).
-            # Once that functionality is added to the GA, we would raise here rather than just report error in our logs.
-            if value in (None, ""):
-                add_event(op=WALAEventOperation.InvalidExtensionConfig,
-                          message="{0} is None for ExtensionConfig, logging error".format(attr_name),
-                          log_event=True, is_success=False)
-            return value
-
-        ext_handler.name = _log_error_if_none("Extensions.Plugins.Plugin.name", getattrib(plugin, "name"))
-        ext_handler.properties.version = _log_error_if_none("Extensions.Plugins.Plugin.version",
-                                                            getattrib(plugin, "version"))
-        ext_handler.properties.state = getattrib(plugin, "state")
-        if ext_handler.properties.state in (None, ""):
-            raise ExtensionsConfigError("Received empty Extensions.Plugins.Plugin.state, failing Handler")
-
-        def getattrib_wrapped_in_list(node, attr_name):
-            attr = getattrib(node, attr_name)
-            return [attr] if attr not in (None, "") else []
-
-        location = getattrib_wrapped_in_list(plugin, "location")
-        failover_location = getattrib_wrapped_in_list(plugin, "failoverlocation")
-
-        locations = location + failover_location
-
-        additional_location_node = find(plugin, "additionalLocations")
-        if additional_location_node is not None:
-            nodes_list = findall(additional_location_node, "additionalLocation")
-            locations += [gettext(node) for node in nodes_list]
-
-        for uri in locations:
-            version_uri = ExtHandlerVersionUri()
-            version_uri.uri = uri
-            ext_handler.versionUris.append(version_uri)
+        if self.status_upload_blob_type not in ["BlockBlob", "PageBlob"]:
+            logger.info("Status Blob type '{0}' is not valid, assuming BlockBlob", self.status_upload_blob)
+            self._set_status_upload_blob_type("BlockBlob")
 
     @staticmethod
-    def _parse_plugin_settings(ext_handler, plugin_settings):
+    def _ticks_to_utc_timestamp(ticks_string):
         """
-        Sample config:
-
-        <PluginSettings>
-            <Plugin name="Microsoft.CPlat.Core.NullSeqA" version="2.0.1">
-                <DependsOn dependencyLevel="1">
-                  <DependsOnExtension handler="Microsoft.CPlat.Core.NullSeqB" />
-                </DependsOn>
-                <RuntimeSettings seqNo="0">{
-                  "runtimeSettings": [
-                    {
-                      "handlerSettings": {
-                        "publicSettings": {"01_add_extensions_with_dependency":"ff2a3da6-8e12-4ab6-a4ca-4e3a473ab385"}
-                      }
-                    }
-                  ]
-                }
-                </RuntimeSettings>
-            </Plugin>
-            <Plugin name="Microsoft.CPlat.Core.RunCommandHandlerWindows" version="2.0.2">
-                <ExtensionRuntimeSettings seqNo="4" name="firstRunCommand" state="enabled">{
-                  "runtimeSettings": [
-                    {
-                      "handlerSettings": {
-                        "publicSettings": {"source":{"script":"Write-Host First: Hello World TestTry2!"},"parameters":[{"name":"extensionName","value":"firstRunCommand"}],"timeoutInSeconds":120}
-                      }
-                    }
-                  ]
-                }
-                </ExtensionRuntimeSettings>
-            </Plugin>
-        </PluginSettings>
+        Takes 'ticks', a string indicating the number of ticks since midnight 0001-01-01 00:00:00, and
+        returns a UTC timestamp  (every tick is 1/10000000 of a second).
         """
-        if plugin_settings is None:
-            return
-
-        handler_name = ext_handler.name
-        version = ext_handler.properties.version
-
-        def to_lower(str_to_change): return str_to_change.lower() if str_to_change is not None else None
-
-        ext_handler_plugin_settings = [x for x in plugin_settings if to_lower(getattrib(x, "name")) == to_lower(handler_name)]
-        if not ext_handler_plugin_settings:
-            return
-
-        settings = [x for x in ext_handler_plugin_settings if getattrib(x, "version") == version]
-        if len(settings) != len(ext_handler_plugin_settings):
-            msg = "ExtHandler PluginSettings Version Mismatch! Expected PluginSettings version: {0} for Handler: {1} but found versions: ({2})".format(
-                version, handler_name, ', '.join(set([getattrib(x, "version") for x in ext_handler_plugin_settings])))
-            add_event(op=WALAEventOperation.PluginSettingsVersionMismatch, message=msg, log_event=True,
-                      is_success=False)
-            raise ExtensionsConfigError(msg)
-
-        if len(settings) > 1:
-            msg = "Multiple plugin settings found for the same handler: {0} and version: {1} (Expected: 1; Available: {2})".format(
-                handler_name, version, len(settings))
-            raise ExtensionsConfigError(msg)
-
-        plugin_settings_node = settings[0]
-        runtime_settings_nodes = findall(plugin_settings_node, "RuntimeSettings")
-        extension_runtime_settings_nodes = findall(plugin_settings_node, "ExtensionRuntimeSettings")
-
-        if any(runtime_settings_nodes) and any(extension_runtime_settings_nodes):
-            # There can only be a single RuntimeSettings node or multiple ExtensionRuntimeSettings nodes per Plugin
-            msg = "Both RuntimeSettings and ExtensionRuntimeSettings found for the same handler: {0} and version: {1}".format(
-                handler_name, version)
-            raise ExtensionsConfigError(msg)
-
-        if runtime_settings_nodes:
-            if len(runtime_settings_nodes) > 1:
-                msg = "Multiple RuntimeSettings found for the same handler: {0} and version: {1} (Expected: 1; Available: {2})".format(
-                    handler_name, version, len(runtime_settings_nodes))
-                raise ExtensionsConfigError(msg)
-            # Only Runtime settings available, parse that
-            _ExtensionsGoalStateFromExtensionsConfig.__parse_runtime_settings(plugin_settings_node, runtime_settings_nodes[0], handler_name,
-                                                      ext_handler)
-        elif extension_runtime_settings_nodes:
-            # Parse the ExtensionRuntime settings for the given extension
-            _ExtensionsGoalStateFromExtensionsConfig.__parse_extension_runtime_settings(plugin_settings_node, extension_runtime_settings_nodes,
-                                                                ext_handler)
-
-    @staticmethod
-    def __get_dependency_level_from_node(depends_on_node, name):
-        depends_on_level = 0
-        if depends_on_node is not None:
+        minimum = datetime.datetime(1900, 1, 1, 0, 0)  # min value accepted by datetime.strftime()
+        as_date_time = minimum
+        if ticks_string not in (None, ""):
             try:
-                depends_on_level = int(getattrib(depends_on_node, "dependencyLevel"))
-            except (ValueError, TypeError):
-                logger.warn("Could not parse dependencyLevel for handler {0}. Setting it to 0".format(name))
-                depends_on_level = 0
-        return depends_on_level
+                as_date_time = datetime.datetime.min + datetime.timedelta(seconds=float(ticks_string) / 10 ** 7)
+            except Exception as exception:
+                logger.verbose("Can't parse ticks: {0}", textutil.format_exception(exception))
+        as_date_time = max(as_date_time, minimum)
 
-    @staticmethod
-    def __parse_runtime_settings(plugin_settings_node, runtime_settings_node, handler_name, ext_handler):
-        """
-        Sample Plugin in PluginSettings containing DependsOn and RuntimeSettings (single settings per extension) -
-
-        <Plugin name="Microsoft.Compute.VMAccessAgent" version="2.4.7">
-        <DependsOn dependencyLevel="2">
-          <DependsOnExtension extension="firstRunCommand" handler="Microsoft.CPlat.Core.RunCommandHandlerWindows" />
-          <DependsOnExtension handler="Microsoft.Compute.CustomScriptExtension" />
-        </DependsOn>
-        <RuntimeSettings seqNo="1">{
-              "runtimeSettings": [
-                {
-                  "handlerSettings": {
-                    "protectedSettingsCertThumbprint": "<Redacted>",
-                    "protectedSettings": "<Redacted>",
-                    "publicSettings": {"UserName":"test1234"}
-                  }
-                }
-              ]
-            }
-        </RuntimeSettings>
-        </Plugin>
-        """
-        depends_on_nodes = findall(plugin_settings_node, "DependsOn")
-        if len(depends_on_nodes) > 1:
-            msg = "Extension Handler can only have a single dependsOn node for Single config extensions. Found: {0}".format(
-                len(depends_on_nodes))
-            raise ExtensionsConfigError(msg)
-        depends_on_node = depends_on_nodes[0] if depends_on_nodes else None
-        depends_on_level = _ExtensionsGoalStateFromExtensionsConfig.__get_dependency_level_from_node(depends_on_node, handler_name)
-        _ExtensionsGoalStateFromExtensionsConfig.__parse_and_add_extension_settings(runtime_settings_node, handler_name, ext_handler,
-                                                            depends_on_level)
+        return as_date_time.strftime(logger.Logger.LogTimeFormatInUTC)
 
     @staticmethod
-    def __parse_extension_runtime_settings(plugin_settings_node, extension_runtime_settings_nodes, ext_handler):
+    def _string_to_id(id_string):
         """
-        Sample PluginSettings containing DependsOn and ExtensionRuntimeSettings -
-
-        <Plugin name="Microsoft.CPlat.Core.RunCommandHandlerWindows" version="2.0.2">
-        <DependsOn dependencyLevel="3" name="secondRunCommand">
-          <DependsOnExtension extension="firstRunCommand" handler="Microsoft.CPlat.Core.RunCommandHandlerWindows" />
-          <DependsOnExtension handler="Microsoft.Compute.CustomScriptExtension" />
-          <DependsOnExtension handler="Microsoft.Compute.VMAccessAgent" />
-        </DependsOn>
-        <DependsOn dependencyLevel="4" name="thirdRunCommand">
-          <DependsOnExtension extension="firstRunCommand" handler="Microsoft.CPlat.Core.RunCommandHandlerWindows" />
-          <DependsOnExtension extension="secondRunCommand" handler="Microsoft.CPlat.Core.RunCommandHandlerWindows" />
-          <DependsOnExtension handler="Microsoft.Compute.CustomScriptExtension" />
-          <DependsOnExtension handler="Microsoft.Compute.VMAccessAgent" />
-        </DependsOn>
-        <ExtensionRuntimeSettings seqNo="2" name="firstRunCommand" state="enabled">
-            {
-              "runtimeSettings": [
-                {
-                  "handlerSettings": {
-                    "publicSettings": {"source":{"script":"Write-Host First: Hello World 1234!"}}
-                  }
-                }
-              ]
-            }
-        </ExtensionRuntimeSettings>
-        <ExtensionRuntimeSettings seqNo="2" name="secondRunCommand" state="enabled">
-            {
-              "runtimeSettings": [
-                {
-                  "handlerSettings": {
-                    "publicSettings": {"source":{"script":"Write-Host First: Hello World 1234!"}}
-                  }
-                }
-              ]
-            }
-        </ExtensionRuntimeSettings>
-        <ExtensionRuntimeSettings seqNo="1" name="thirdRunCommand" state="enabled">
-            {
-              "runtimeSettings": [
-                {
-                  "handlerSettings": {
-                    "publicSettings": {"source":{"script":"Write-Host Third: Hello World 3!"}}
-                  }
-                }
-              ]
-            }
-        </ExtensionRuntimeSettings>
-      </Plugin>
+        Takes 'id', a string indicating an ID, and returns a null GUID if the string is None or empty; otherwise
+        return 'id' unchanged
         """
-        # Parse and cache the Dependencies for each extension first
-        dependency_levels = defaultdict(int)
-        for depends_on_node in findall(plugin_settings_node, "DependsOn"):
-            extension_name = getattrib(depends_on_node, "name")
-            if extension_name in (None, ""):
-                raise ExtensionsConfigError("No Name not specified for DependsOn object in ExtensionRuntimeSettings for MultiConfig!")
-
-            dependency_level = _ExtensionsGoalStateFromExtensionsConfig.__get_dependency_level_from_node(depends_on_node, extension_name)
-            dependency_levels[extension_name] = dependency_level
-
-        ext_handler.supports_multi_config = True
-        for extension_runtime_setting_node in extension_runtime_settings_nodes:
-            # Name and State will only be set for ExtensionRuntimeSettings for Multi-Config
-            extension_name = getattrib(extension_runtime_setting_node, "name")
-            if extension_name in (None, ""):
-                raise ExtensionsConfigError("Extension Name not specified for ExtensionRuntimeSettings for MultiConfig!")
-            # State can either be `ExtensionState.Enabled` (default) or `ExtensionState.Disabled`
-            state = getattrib(extension_runtime_setting_node, "state")
-            state = ustr(state.lower()) if state not in (None, "") else ExtensionState.Enabled
-            _ExtensionsGoalStateFromExtensionsConfig.__parse_and_add_extension_settings(extension_runtime_setting_node, extension_name,
-                                                                ext_handler, dependency_levels[extension_name],
-                                                                state=state)
+        if id_string in (None, ""):
+            return AgentGlobals.GUID_ZERO
+        return id_string
 
-    @staticmethod
-    def __parse_and_add_extension_settings(settings_node, name, ext_handler, depends_on_level, state=ExtensionState.Enabled):
-        seq_no = getattrib(settings_node, "seqNo")
-        if seq_no in (None, ""):
-            raise ExtensionsConfigError("SeqNo not specified for the Extension: {0}".format(name))
-
-        try:
-            runtime_settings = json.loads(gettext(settings_node))
-        except ValueError as error:
-            logger.error("Invalid extension settings: {0}", ustr(error))
-            # Incase of invalid/no settings, add the name and seqNo of the Extension and treat it as an extension with
-            # no settings since we were able to successfully parse those data properly. Without this, we wont report
-            # anything for that sequence number and CRP would eventually have to timeout rather than fail fast.
-            ext_handler.properties.extensions.append(
-                Extension(name=name, sequenceNumber=seq_no, state=state, dependencyLevel=depends_on_level))
-            return
-
-        for plugin_settings_list in runtime_settings["runtimeSettings"]:
-            handler_settings = plugin_settings_list["handlerSettings"]
-            ext = Extension()
-            # There is no "extension name" for single Handler Settings. Use HandlerName for those
-            ext.name = name
-            ext.state = state
-            ext.sequenceNumber = seq_no
-            ext.publicSettings = handler_settings.get("publicSettings")
-            ext.protectedSettings = handler_settings.get("protectedSettings")
-            ext.dependencyLevel = depends_on_level
-            thumbprint = handler_settings.get("protectedSettingsCertThumbprint")
-            ext.certificateThumbprint = thumbprint
-            ext_handler.properties.extensions.append(ext)
-
-
-class _ExtensionsGoalStateFromVmSettings(ExtensionsGoalState):
-    def __init__(self, etag, json_text):
-        super(_ExtensionsGoalStateFromVmSettings, self).__init__()
-        self._id = etag
-        self._text = json_text
-
-        try:
-            self._parse_vm_settings(json_text)
-            self._do_common_validations()
-        except Exception as e:
-            raise VmSettingsError("Error parsing vmSettings (etag: {0}): {1}\n{2}".format(etag, format_exception(e), self.get_redacted_text()))
-
-    def _parse_vm_settings(self, json_text):
-        vm_settings = _CaseFoldedDict.from_dict(json.loads(json_text))
-        # TODO: HostGAPlugin 112 does not include the status blob; enable parsing once we can ensure a newer version is available. Also enable the check on
-        #       the ExtensionsGoalState.compare() method
-        # self._parse_status_upload_blob(vm_settings)
-        self._parse_required_features(vm_settings)
-        # TODO: Parse all atttributes
-
-    def _parse_status_upload_blob(self, vm_settings):
-        status_upload_blob = vm_settings.get("statusUploadBlob")
-        if status_upload_blob is None:
-            raise Exception("Missing statusUploadBlob")
-        self._status_upload_blob = status_upload_blob.get("value")
-        if self._status_upload_blob is None:
-            raise Exception("Missing statusUploadBlob.value")
-        self._status_upload_blob_type = status_upload_blob.get("statusBlobType")
-        if self._status_upload_blob is None:
-            raise Exception("Missing statusUploadBlob.statusBlobType")
-
-    def _parse_required_features(self, vm_settings):
-        required_features = vm_settings.get("requiredFeatures")
-        if required_features is not None:
-            if not isinstance(required_features, list):
-                raise Exception("requiredFeatures should be an array")
-
-            def get_required_features_names():
-                for feature in required_features:
-                    name = feature.get("name")
-                    if name is None:
-                        raise Exception("A required feature is missing the 'name' property")
-                    yield name
-
-            self._required_features.extend(get_required_features_names())
 
-    def get_redacted_text(self):
-        return re.sub(r'("protectedSettings"\s*:\s*)"[^"]+"', r'\1"*** REDACTED ***"', self._text)
+class EmptyExtensionsGoalState(ExtensionsGoalState):
+    @property
+    def id(self):
+        return self._string_to_id(None)
 
+    @property
+    def activity_id(self):
+        return AgentGlobals.GUID_ZERO
 
-#
-# TODO: The current implementation of the vmSettings API uses inconsistent cases on the names of the json items it returns.
-#       To work around that, we use _CaseFoldedDict to query those json items in a case-insensitive matter, Do not use
-#       _CaseFoldedDict for other purposes. Remove it once the vmSettings API is updated.
-#
-class _CaseFoldedDict(dict):
-    @staticmethod
-    def from_dict(dictionary):
-        case_folded = _CaseFoldedDict()
-        for key, value in dictionary.items():
-            case_folded[key] = _CaseFoldedDict._to_case_folded_dict_item(value)
-        return case_folded
-
-    def get(self, key):
-        return super(_CaseFoldedDict, self).get(_casefold(key))
+    @property
+    def correlation_id(self):
+        return AgentGlobals.GUID_ZERO
 
-    def has_key(self, key):
-        return super(_CaseFoldedDict, self).get(_casefold(key))
+    @property
+    def created_on_timestamp(self):
+        return datetime.datetime.min
 
-    def __getitem__(self, key):
-        return super(_CaseFoldedDict, self).__getitem__(_casefold(key))
+    @property
+    def status_upload_blob(self):
+        return None
 
-    def __setitem__(self, key, value):
-        return super(_CaseFoldedDict, self).__setitem__(_casefold(key), value)
-
-    def __contains__(self, key):
-        return  super(_CaseFoldedDict, self).__contains__(_casefold(key))
-
-    @staticmethod
-    def _to_case_folded_dict_item(item):
-        if isinstance(item, dict):
-            case_folded_dict = _CaseFoldedDict()
-            for key, value in item.items():
-                case_folded_dict[_casefold(key)] = _CaseFoldedDict._to_case_folded_dict_item(value)
-            return case_folded_dict
-        if isinstance(item, list):
-            return [_CaseFoldedDict._to_case_folded_dict_item(list_item) for list_item in item]
-        return item
-
-    def copy(self):
-        raise NotImplementedError()
-
-    @staticmethod
-    def fromkeys(*args, **kwargs):
-        raise NotImplementedError()
-
-    def pop(self, key, default=None):
-        raise NotImplementedError()
-
-    def setdefault(self, key, default=None):
-        raise NotImplementedError()
-
-    def update(self, E=None, **F):  # known special case of dict.update
-        raise NotImplementedError()
-
-    def __delitem__(self, *args, **kwargs):
-        raise NotImplementedError()
-
-
-# casefold() does not exist on Python 2 so we use lower() there
-def _casefold(string):
-    if sys.version_info[0] == 2:
-        return type(string).lower(string)  # the type of "string" can be unicode or str
-    # Class 'str' has no 'casefold' member (no-member) -- Disabled: This warning shows up on Python 2.7 pylint runs
-    # but this code is actually not executed on Python 2.
-    return str.casefold(string)  # pylint: disable=no-member
+    @property
+    def status_upload_blob_type(self):
+        return None
 
+    def _set_status_upload_blob_type(self, value):
+        raise TypeError("EmptyExtensionsGoalState is immutable; cannot change the value of the status upload blob")
 
+    @property
+    def required_features(self):
+        return []
 
+    @property
+    def on_hold(self):
+        return False
 
+    def get_redacted_text(self):
+        return ''
 
diff --git a/azurelinuxagent/common/protocol/extensions_goal_state_factory.py b/azurelinuxagent/common/protocol/extensions_goal_state_factory.py
new file mode 100644
index 00000000..f3c8dcff
--- /dev/null
+++ b/azurelinuxagent/common/protocol/extensions_goal_state_factory.py
@@ -0,0 +1,36 @@
+# 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+
+
+from azurelinuxagent.common.protocol.extensions_goal_state import EmptyExtensionsGoalState
+from azurelinuxagent.common.protocol.extensions_goal_state_from_extensions_config import ExtensionsGoalStateFromExtensionsConfig
+from azurelinuxagent.common.protocol.extensions_goal_state_from_vm_settings import ExtensionsGoalStateFromVmSettings
+
+
+class ExtensionsGoalStateFactory(object):
+    @staticmethod
+    def create_empty():
+        return EmptyExtensionsGoalState()
+
+    @staticmethod
+    def create_from_extensions_config(incarnation, xml_text, wire_client):
+        return ExtensionsGoalStateFromExtensionsConfig(incarnation, xml_text, wire_client)
+
+    @staticmethod
+    def create_from_vm_settings(etag, json_text):
+        return ExtensionsGoalStateFromVmSettings(etag, json_text)
+
diff --git a/azurelinuxagent/common/protocol/extensions_goal_state_from_extensions_config.py b/azurelinuxagent/common/protocol/extensions_goal_state_from_extensions_config.py
new file mode 100644
index 00000000..d908dfa6
--- /dev/null
+++ b/azurelinuxagent/common/protocol/extensions_goal_state_from_extensions_config.py
@@ -0,0 +1,559 @@
+# 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 json
+
+from collections import defaultdict
+
+import azurelinuxagent.common.logger as logger
+from azurelinuxagent.common.event import add_event, WALAEventOperation
+from azurelinuxagent.common.exception import ExtensionsConfigError
+from azurelinuxagent.common.future import ustr
+from azurelinuxagent.common.protocol.extensions_goal_state import ExtensionsGoalState
+from azurelinuxagent.common.protocol.restapi import Extension, ExtHandler, ExtHandlerVersionUri, VMAgentManifest, \
+    VMAgentManifestUri, ExtensionState, ExtHandlerList, VMAgentManifestList, InVMGoalStateMetaData
+from azurelinuxagent.common.utils.textutil import parse_doc, parse_json, findall, find, findtext, getattrib, gettext, format_exception, \
+    is_str_none_or_whitespace, is_str_empty
+
+
+class ExtensionsGoalStateFromExtensionsConfig(ExtensionsGoalState):
+    def __init__(self, incarnation, xml_text, wire_client=None):
+        super(ExtensionsGoalStateFromExtensionsConfig, self).__init__()
+        self._id = incarnation
+        self._text = xml_text
+        self._status_upload_blob = None
+        self._status_upload_blob_type = None
+        self._required_features = []
+        self._on_hold = False
+        self._activity_id = None
+        self._correlation_id = None
+        self._created_on_timestamp = None
+
+        self.ext_handlers = ExtHandlerList()
+        self.vmagent_manifests = VMAgentManifestList()
+
+        try:
+            self._parse_extensions_config(xml_text, wire_client)
+            self._do_common_validations()
+        except Exception as e:
+            raise ExtensionsConfigError("Error parsing ExtensionsConfig (incarnation: {0}): {1}\n{2}".format(incarnation, format_exception(e), self.get_redacted_text()))
+
+    def _parse_extensions_config(self, xml_text, wire_client):
+        xml_doc = parse_doc(xml_text)
+
+        ga_families_list = find(xml_doc, "GAFamilies")
+        ga_families = findall(ga_families_list, "GAFamily")
+
+        for ga_family in ga_families:
+            family = findtext(ga_family, "Name")
+            uris_list = find(ga_family, "Uris")
+            uris = findall(uris_list, "Uri")
+            manifest = VMAgentManifest()
+            manifest.family = family
+            for uri in uris:
+                manifest_uri = VMAgentManifestUri(uri=gettext(uri))
+                manifest.versionsManifestUris.append(manifest_uri)
+            self.vmagent_manifests.vmAgentManifests.append(manifest)
+
+        self.__parse_plugins_and_settings_and_populate_ext_handlers(xml_doc)
+
+        required_features_list = find(xml_doc, "RequiredFeatures")
+        if required_features_list is not None:
+            self._parse_required_features(required_features_list)
+
+        self._status_upload_blob = findtext(xml_doc, "StatusUploadBlob")
+
+        status_upload_node = find(xml_doc, "StatusUploadBlob")
+        self._status_upload_blob_type = getattrib(status_upload_node, "statusBlobType")
+        logger.verbose("Extension config shows status blob type as [{0}]", self._status_upload_blob_type)
+
+        self._on_hold = self._fetch_extensions_on_hold(xml_doc, wire_client)
+
+        in_vm_gs_metadata = InVMGoalStateMetaData(find(xml_doc, "InVMGoalStateMetaData"))
+        self._activity_id = self._string_to_id(in_vm_gs_metadata.activity_id)
+        self._correlation_id = self._string_to_id(in_vm_gs_metadata.correlation_id)
+        self._created_on_timestamp = self._ticks_to_utc_timestamp(in_vm_gs_metadata.created_on_ticks)
+
+    def _fetch_extensions_on_hold(self, xml_doc, wire_client):
+        artifacts_profile_blob = findtext(xml_doc, "InVMArtifactsProfileBlob")
+        if is_str_none_or_whitespace(artifacts_profile_blob):
+            return False
+
+        def fetch_direct():
+            content, _ = wire_client.fetch(artifacts_profile_blob)
+            return content
+
+        def fetch_through_host():
+            host = wire_client.get_host_plugin()
+            uri, headers = host.get_artifact_request(artifacts_profile_blob)
+            content, _ = wire_client.fetch(uri, headers, use_proxy=False)
+            return content
+
+        logger.verbose("Retrieving the artifacts profile")
+
+        try:
+            profile = wire_client.send_request_using_appropriate_channel(fetch_direct, fetch_through_host)
+            if profile is None:
+                logger.warn("Failed to fetch artifacts profile from blob {0}", artifacts_profile_blob)
+                return False
+        except Exception as error:
+            logger.warn("Exception retrieving artifacts profile from blob {0}. Error: {1}".format(artifacts_profile_blob, ustr(error)))
+            return False
+
+        if is_str_empty(profile):
+            return False
+
+        logger.verbose("Artifacts profile downloaded")
+
+        try:
+            artifacts_profile = _InVMArtifactsProfile(profile)
+        except Exception:
+            logger.warn("Could not parse artifacts profile blob")
+            msg = "Content: [{0}]".format(profile)
+            logger.verbose(msg)
+            add_event(op=WALAEventOperation.ArtifactsProfileBlob, is_success=False, message=msg, log_event=False)
+            return False
+
+        return artifacts_profile.get_on_hold()
+
+    @property
+    def id(self):
+        return self._id
+
+    @property
+    def activity_id(self):
+        return self._activity_id
+
+    @property
+    def correlation_id(self):
+        return self._correlation_id
+
+    @property
+    def created_on_timestamp(self):
+        return self._created_on_timestamp
+
+    @property
+    def status_upload_blob(self):
+        return self._status_upload_blob
+
+    @property
+    def status_upload_blob_type(self):
+        return self._status_upload_blob_type
+
+    def _set_status_upload_blob_type(self, value):
+        self._status_upload_blob_type = value
+
+    @property
+    def required_features(self):
+        return self._required_features
+
+    @property
+    def on_hold(self):
+        return self._on_hold
+
+    def get_redacted_text(self):
+        text = self._text
+        for ext_handler in self.ext_handlers.extHandlers:
+            for extension in ext_handler.properties.extensions:
+                if extension.protectedSettings is not None:
+                    text = text.replace(extension.protectedSettings, "*** REDACTED ***")
+        return text
+
+    def _parse_required_features(self, required_features_list):
+        for required_feature in findall(required_features_list, "RequiredFeature"):
+            feature_name = findtext(required_feature, "Name")
+            # per the documentation, RequiredFeatures also have a "Value" attribute but currently it is not being populated
+            self._required_features.append(feature_name)
+
+    def __parse_plugins_and_settings_and_populate_ext_handlers(self, xml_doc):
+        """
+        Sample ExtensionConfig Plugin and PluginSettings:
+
+        <Plugins>
+          <Plugin name="Microsoft.CPlat.Core.NullSeqB" version="2.0.1" location="https://zrdfepirv2cbn04prdstr01a.blob.core.windows.net/f72653efd9e349ed9842c8b99e4c1712/Microsoft.CPlat.Core_NullSeqB_useast2euap_manifest.xml" state="enabled" autoUpgrade="false" failoverlocation="https://zrdfepirv2cbz06prdstr01a.blob.core.windows.net/f72653efd9e349ed9842c8b99e4c1712/Microsoft.CPlat.Core_NullSeqB_useast2euap_manifest.xml" runAsStartupTask="false" isJson="true" useExactVersion="true" />
+          <Plugin name="Microsoft.CPlat.Core.NullSeqA" version="2.0.1" location="https://zrdfepirv2cbn04prdstr01a.blob.core.windows.net/f72653efd9e349ed9842c8b99e4c1712/Microsoft.CPlat.Core_NullSeqA_useast2euap_manifest.xml" state="enabled" autoUpgrade="false" failoverlocation="https://zrdfepirv2cbn06prdstr01a.blob.core.windows.net/f72653efd9e349ed9842c8b99e4c1712/Microsoft.CPlat.Core_NullSeqA_useast2euap_manifest.xml" runAsStartupTask="false" isJson="true" useExactVersion="true" />
+        </Plugins>
+        <PluginSettings>
+          <Plugin name="Microsoft.CPlat.Core.NullSeqA" version="2.0.1">
+            <DependsOn dependencyLevel="1">
+              <DependsOnExtension handler="Microsoft.CPlat.Core.NullSeqB" />
+            </DependsOn>
+            <RuntimeSettings seqNo="0">{
+              "runtimeSettings": [
+                {
+                  "handlerSettings": {
+                    "publicSettings": {"01_add_extensions_with_dependency":"ff2a3da6-8e12-4ab6-a4ca-4e3a473ab385"}
+                  }
+                }
+              ]
+            }
+            </RuntimeSettings>
+          </Plugin>
+          <Plugin name="Microsoft.CPlat.Core.NullSeqB" version="2.0.1">
+            <RuntimeSettings seqNo="0">{
+              "runtimeSettings": [
+                {
+                  "handlerSettings": {
+                    "publicSettings": {"01_add_extensions_with_dependency":"2e837740-cf7e-4528-b3a4-241002618f05"}
+                  }
+                }
+              ]
+            }
+            </RuntimeSettings>
+          </Plugin>
+        </PluginSettings>
+        """
+
+        plugins_list = find(xml_doc, "Plugins")
+        plugins = findall(plugins_list, "Plugin")
+        plugin_settings_list = find(xml_doc, "PluginSettings")
+        plugin_settings = findall(plugin_settings_list, "Plugin")
+
+        for plugin in plugins:
+            ext_handler = ExtHandler()
+            try:
+                ExtensionsGoalStateFromExtensionsConfig._parse_plugin(ext_handler, plugin)
+                ExtensionsGoalStateFromExtensionsConfig._parse_plugin_settings(ext_handler, plugin_settings)
+            except ExtensionsConfigError as error:
+                ext_handler.invalid_setting_reason = ustr(error)
+
+            self.ext_handlers.extHandlers.append(ext_handler)
+
+    @staticmethod
+    def _parse_plugin(ext_handler, plugin):
+        """
+        Sample config:
+
+        <Plugins>
+          <Plugin name="Microsoft.CPlat.Core.NullSeqB" version="2.0.1" location="https://zrdfepirv2cbn04prdstr01a.blob.core.windows.net/f72653efd9e349ed9842c8b99e4c1712/Microsoft.CPlat.Core_NullSeqB_useast2euap_manifest.xml" state="enabled" autoUpgrade="false" failoverlocation="https://zrdfepirv2cbz06prdstr01a.blob.core.windows.net/f72653efd9e349ed9842c8b99e4c1712/Microsoft.CPlat.Core_NullSeqB_useast2euap_manifest.xml" runAsStartupTask="false" isJson="true" useExactVersion="true"><Plugin name="Microsoft.Azure.Extensions.CustomScript" version="1.0" location="https://rdfecurrentuswestcache.blob.core.test-cint.azure-test.net/0e53c53ef0be4178bacb0a1fecf12a74/Microsoft.Azure.Extensions_CustomScript_usstagesc_manifest.xml" state="enabled" autoUpgrade="false" failoverlocation="https://rdfecurrentuswestcache2.blob.core.test-cint.azure-test.net/0e53c53ef0be4178bacb0a1fecf12a74/Microsoft.Azure.Extensions_CustomScript_usstagesc_manifest.xml" runAsStartupTask="false" isJson="true" useExactVersion="true">
+            <additionalLocations>
+              <additionalLocation>https://rdfecurrentuswestcache3.blob.core.test-cint.azure-test.net/0e53c53ef0be4178bacb0a1fecf12a74/Microsoft.Azure.Extensions_CustomScript_usstagesc_manifest.xml</additionalLocation>
+              <additionalLocation>https://rdfecurrentuswestcache4.blob.core.test-cint.azure-test.net/0e53c53ef0be4178bacb0a1fecf12a74/Microsoft.Azure.Extensions_CustomScript_usstagesc_manifest.xml</additionalLocation>
+            </additionalLocations>
+          </Plugin>
+          <Plugin name="Microsoft.CPlat.Core.NullSeqA" version="2.0.1" location="https://zrdfepirv2cbn04prdstr01a.blob.core.windows.net/f72653efd9e349ed9842c8b99e4c1712/Microsoft.CPlat.Core_NullSeqA_useast2euap_manifest.xml" state="enabled" autoUpgrade="false" failoverlocation="https://zrdfepirv2cbn06prdstr01a.blob.core.windows.net/f72653efd9e349ed9842c8b99e4c1712/Microsoft.CPlat.Core_NullSeqA_useast2euap_manifest.xml" runAsStartupTask="false" isJson="true" useExactVersion="true" />
+        </Plugins>
+
+
+        Note that the `additionalLocations` subnode is populated with links
+        generated by PIR for resiliency. In regions with this feature enabled,
+        CRP will provide any extra links in the format above. If no extra links
+        are provided, the subnode will not exist.
+        """
+
+        def _log_error_if_none(attr_name, value):
+            # Plugin Name and Version are very essential fields, without them we wont be able to even report back to CRP
+            # about that handler. For those cases we need to fail the GoalState completely but currently we dont support
+            # reporting status at a GoalState level (we only report at a handler level).
+            # Once that functionality is added to the GA, we would raise here rather than just report error in our logs.
+            if value in (None, ""):
+                add_event(op=WALAEventOperation.InvalidExtensionConfig,
+                          message="{0} is None for ExtensionConfig, logging error".format(attr_name),
+                          log_event=True, is_success=False)
+            return value
+
+        ext_handler.name = _log_error_if_none("Extensions.Plugins.Plugin.name", getattrib(plugin, "name"))
+        ext_handler.properties.version = _log_error_if_none("Extensions.Plugins.Plugin.version",
+                                                            getattrib(plugin, "version"))
+        ext_handler.properties.state = getattrib(plugin, "state")
+        if ext_handler.properties.state in (None, ""):
+            raise ExtensionsConfigError("Received empty Extensions.Plugins.Plugin.state, failing Handler")
+
+        def getattrib_wrapped_in_list(node, attr_name):
+            attr = getattrib(node, attr_name)
+            return [attr] if attr not in (None, "") else []
+
+        location = getattrib_wrapped_in_list(plugin, "location")
+        failover_location = getattrib_wrapped_in_list(plugin, "failoverlocation")
+
+        locations = location + failover_location
+
+        additional_location_node = find(plugin, "additionalLocations")
+        if additional_location_node is not None:
+            nodes_list = findall(additional_location_node, "additionalLocation")
+            locations += [gettext(node) for node in nodes_list]
+
+        for uri in locations:
+            version_uri = ExtHandlerVersionUri()
+            version_uri.uri = uri
+            ext_handler.versionUris.append(version_uri)
+
+    @staticmethod
+    def _parse_plugin_settings(ext_handler, plugin_settings):
+        """
+        Sample config:
+
+        <PluginSettings>
+            <Plugin name="Microsoft.CPlat.Core.NullSeqA" version="2.0.1">
+                <DependsOn dependencyLevel="1">
+                  <DependsOnExtension handler="Microsoft.CPlat.Core.NullSeqB" />
+                </DependsOn>
+                <RuntimeSettings seqNo="0">{
+                  "runtimeSettings": [
+                    {
+                      "handlerSettings": {
+                        "publicSettings": {"01_add_extensions_with_dependency":"ff2a3da6-8e12-4ab6-a4ca-4e3a473ab385"}
+                      }
+                    }
+                  ]
+                }
+                </RuntimeSettings>
+            </Plugin>
+            <Plugin name="Microsoft.CPlat.Core.RunCommandHandlerWindows" version="2.0.2">
+                <ExtensionRuntimeSettings seqNo="4" name="firstRunCommand" state="enabled">{
+                  "runtimeSettings": [
+                    {
+                      "handlerSettings": {
+                        "publicSettings": {"source":{"script":"Write-Host First: Hello World TestTry2!"},"parameters":[{"name":"extensionName","value":"firstRunCommand"}],"timeoutInSeconds":120}
+                      }
+                    }
+                  ]
+                }
+                </ExtensionRuntimeSettings>
+            </Plugin>
+        </PluginSettings>
+        """
+        if plugin_settings is None:
+            return
+
+        handler_name = ext_handler.name
+        version = ext_handler.properties.version
+
+        def to_lower(str_to_change): return str_to_change.lower() if str_to_change is not None else None
+
+        ext_handler_plugin_settings = [x for x in plugin_settings if to_lower(getattrib(x, "name")) == to_lower(handler_name)]
+        if not ext_handler_plugin_settings:
+            return
+
+        settings = [x for x in ext_handler_plugin_settings if getattrib(x, "version") == version]
+        if len(settings) != len(ext_handler_plugin_settings):
+            msg = "ExtHandler PluginSettings Version Mismatch! Expected PluginSettings version: {0} for Handler: {1} but found versions: ({2})".format(
+                version, handler_name, ', '.join(set([getattrib(x, "version") for x in ext_handler_plugin_settings])))
+            add_event(op=WALAEventOperation.PluginSettingsVersionMismatch, message=msg, log_event=True,
+                      is_success=False)
+            raise ExtensionsConfigError(msg)
+
+        if len(settings) > 1:
+            msg = "Multiple plugin settings found for the same handler: {0} and version: {1} (Expected: 1; Available: {2})".format(
+                handler_name, version, len(settings))
+            raise ExtensionsConfigError(msg)
+
+        plugin_settings_node = settings[0]
+        runtime_settings_nodes = findall(plugin_settings_node, "RuntimeSettings")
+        extension_runtime_settings_nodes = findall(plugin_settings_node, "ExtensionRuntimeSettings")
+
+        if any(runtime_settings_nodes) and any(extension_runtime_settings_nodes):
+            # There can only be a single RuntimeSettings node or multiple ExtensionRuntimeSettings nodes per Plugin
+            msg = "Both RuntimeSettings and ExtensionRuntimeSettings found for the same handler: {0} and version: {1}".format(
+                handler_name, version)
+            raise ExtensionsConfigError(msg)
+
+        if runtime_settings_nodes:
+            if len(runtime_settings_nodes) > 1:
+                msg = "Multiple RuntimeSettings found for the same handler: {0} and version: {1} (Expected: 1; Available: {2})".format(
+                    handler_name, version, len(runtime_settings_nodes))
+                raise ExtensionsConfigError(msg)
+            # Only Runtime settings available, parse that
+            ExtensionsGoalStateFromExtensionsConfig.__parse_runtime_settings(plugin_settings_node, runtime_settings_nodes[0], handler_name,
+                                                      ext_handler)
+        elif extension_runtime_settings_nodes:
+            # Parse the ExtensionRuntime settings for the given extension
+            ExtensionsGoalStateFromExtensionsConfig.__parse_extension_runtime_settings(plugin_settings_node, extension_runtime_settings_nodes,
+                                                                ext_handler)
+
+    @staticmethod
+    def __get_dependency_level_from_node(depends_on_node, name):
+        depends_on_level = 0
+        if depends_on_node is not None:
+            try:
+                depends_on_level = int(getattrib(depends_on_node, "dependencyLevel"))
+            except (ValueError, TypeError):
+                logger.warn("Could not parse dependencyLevel for handler {0}. Setting it to 0".format(name))
+                depends_on_level = 0
+        return depends_on_level
+
+    @staticmethod
+    def __parse_runtime_settings(plugin_settings_node, runtime_settings_node, handler_name, ext_handler):
+        """
+        Sample Plugin in PluginSettings containing DependsOn and RuntimeSettings (single settings per extension) -
+
+        <Plugin name="Microsoft.Compute.VMAccessAgent" version="2.4.7">
+        <DependsOn dependencyLevel="2">
+          <DependsOnExtension extension="firstRunCommand" handler="Microsoft.CPlat.Core.RunCommandHandlerWindows" />
+          <DependsOnExtension handler="Microsoft.Compute.CustomScriptExtension" />
+        </DependsOn>
+        <RuntimeSettings seqNo="1">{
+              "runtimeSettings": [
+                {
+                  "handlerSettings": {
+                    "protectedSettingsCertThumbprint": "<Redacted>",
+                    "protectedSettings": "<Redacted>",
+                    "publicSettings": {"UserName":"test1234"}
+                  }
+                }
+              ]
+            }
+        </RuntimeSettings>
+        </Plugin>
+        """
+        depends_on_nodes = findall(plugin_settings_node, "DependsOn")
+        if len(depends_on_nodes) > 1:
+            msg = "Extension Handler can only have a single dependsOn node for Single config extensions. Found: {0}".format(
+                len(depends_on_nodes))
+            raise ExtensionsConfigError(msg)
+        depends_on_node = depends_on_nodes[0] if depends_on_nodes else None
+        depends_on_level = ExtensionsGoalStateFromExtensionsConfig.__get_dependency_level_from_node(depends_on_node, handler_name)
+        ExtensionsGoalStateFromExtensionsConfig.__parse_and_add_extension_settings(runtime_settings_node, handler_name, ext_handler,
+                                                            depends_on_level)
+
+    @staticmethod
+    def __parse_extension_runtime_settings(plugin_settings_node, extension_runtime_settings_nodes, ext_handler):
+        """
+        Sample PluginSettings containing DependsOn and ExtensionRuntimeSettings -
+
+        <Plugin name="Microsoft.CPlat.Core.RunCommandHandlerWindows" version="2.0.2">
+        <DependsOn dependencyLevel="3" name="secondRunCommand">
+          <DependsOnExtension extension="firstRunCommand" handler="Microsoft.CPlat.Core.RunCommandHandlerWindows" />
+          <DependsOnExtension handler="Microsoft.Compute.CustomScriptExtension" />
+          <DependsOnExtension handler="Microsoft.Compute.VMAccessAgent" />
+        </DependsOn>
+        <DependsOn dependencyLevel="4" name="thirdRunCommand">
+          <DependsOnExtension extension="firstRunCommand" handler="Microsoft.CPlat.Core.RunCommandHandlerWindows" />
+          <DependsOnExtension extension="secondRunCommand" handler="Microsoft.CPlat.Core.RunCommandHandlerWindows" />
+          <DependsOnExtension handler="Microsoft.Compute.CustomScriptExtension" />
+          <DependsOnExtension handler="Microsoft.Compute.VMAccessAgent" />
+        </DependsOn>
+        <ExtensionRuntimeSettings seqNo="2" name="firstRunCommand" state="enabled">
+            {
+              "runtimeSettings": [
+                {
+                  "handlerSettings": {
+                    "publicSettings": {"source":{"script":"Write-Host First: Hello World 1234!"}}
+                  }
+                }
+              ]
+            }
+        </ExtensionRuntimeSettings>
+        <ExtensionRuntimeSettings seqNo="2" name="secondRunCommand" state="enabled">
+            {
+              "runtimeSettings": [
+                {
+                  "handlerSettings": {
+                    "publicSettings": {"source":{"script":"Write-Host First: Hello World 1234!"}}
+                  }
+                }
+              ]
+            }
+        </ExtensionRuntimeSettings>
+        <ExtensionRuntimeSettings seqNo="1" name="thirdRunCommand" state="enabled">
+            {
+              "runtimeSettings": [
+                {
+                  "handlerSettings": {
+                    "publicSettings": {"source":{"script":"Write-Host Third: Hello World 3!"}}
+                  }
+                }
+              ]
+            }
+        </ExtensionRuntimeSettings>
+      </Plugin>
+        """
+        # Parse and cache the Dependencies for each extension first
+        dependency_levels = defaultdict(int)
+        for depends_on_node in findall(plugin_settings_node, "DependsOn"):
+            extension_name = getattrib(depends_on_node, "name")
+            if extension_name in (None, ""):
+                raise ExtensionsConfigError("No Name not specified for DependsOn object in ExtensionRuntimeSettings for MultiConfig!")
+
+            dependency_level = ExtensionsGoalStateFromExtensionsConfig.__get_dependency_level_from_node(depends_on_node, extension_name)
+            dependency_levels[extension_name] = dependency_level
+
+        ext_handler.supports_multi_config = True
+        for extension_runtime_setting_node in extension_runtime_settings_nodes:
+            # Name and State will only be set for ExtensionRuntimeSettings for Multi-Config
+            extension_name = getattrib(extension_runtime_setting_node, "name")
+            if extension_name in (None, ""):
+                raise ExtensionsConfigError("Extension Name not specified for ExtensionRuntimeSettings for MultiConfig!")
+            # State can either be `ExtensionState.Enabled` (default) or `ExtensionState.Disabled`
+            state = getattrib(extension_runtime_setting_node, "state")
+            state = ustr(state.lower()) if state not in (None, "") else ExtensionState.Enabled
+            ExtensionsGoalStateFromExtensionsConfig.__parse_and_add_extension_settings(extension_runtime_setting_node, extension_name,
+                                                                ext_handler, dependency_levels[extension_name],
+                                                                state=state)
+
+    @staticmethod
+    def __parse_and_add_extension_settings(settings_node, name, ext_handler, depends_on_level, state=ExtensionState.Enabled):
+        seq_no = getattrib(settings_node, "seqNo")
+        if seq_no in (None, ""):
+            raise ExtensionsConfigError("SeqNo not specified for the Extension: {0}".format(name))
+
+        try:
+            runtime_settings = json.loads(gettext(settings_node))
+        except ValueError as error:
+            logger.error("Invalid extension settings: {0}", ustr(error))
+            # Incase of invalid/no settings, add the name and seqNo of the Extension and treat it as an extension with
+            # no settings since we were able to successfully parse those data properly. Without this, we wont report
+            # anything for that sequence number and CRP would eventually have to timeout rather than fail fast.
+            ext_handler.properties.extensions.append(
+                Extension(name=name, sequenceNumber=seq_no, state=state, dependencyLevel=depends_on_level))
+            return
+
+        for plugin_settings_list in runtime_settings["runtimeSettings"]:
+            handler_settings = plugin_settings_list["handlerSettings"]
+            ext = Extension()
+            # There is no "extension name" for single Handler Settings. Use HandlerName for those
+            ext.name = name
+            ext.state = state
+            ext.sequenceNumber = seq_no
+            ext.publicSettings = handler_settings.get("publicSettings")
+            ext.protectedSettings = handler_settings.get("protectedSettings")
+            ext.dependencyLevel = depends_on_level
+            thumbprint = handler_settings.get("protectedSettingsCertThumbprint")
+            ext.certificateThumbprint = thumbprint
+            ext_handler.properties.extensions.append(ext)
+
+
+# Do not extend this class
+class _InVMArtifactsProfile(object):
+    """
+    deserialized json string of InVMArtifactsProfile.
+    It is expected to contain the following fields:
+    * inVMArtifactsProfileBlobSeqNo
+    * profileId (optional)
+    * onHold (optional)
+    * certificateThumbprint (optional)
+    * encryptedHealthChecks (optional)
+    * encryptedApplicationProfile (optional)
+    """
+
+    def __init__(self, artifacts_profile_json):
+        self._on_hold = False
+        artifacts_profile = parse_json(artifacts_profile_json)
+        on_hold = artifacts_profile.get('onHold')
+        if on_hold is not None:
+            # accept both bool and str values
+            on_hold_normalized = str(on_hold).lower()
+            if on_hold_normalized == "true":
+                self._on_hold = True
+            elif on_hold_normalized == "false":
+                self._on_hold = False
+            else:
+                raise Exception("Invalid value for onHold: {0}".format(on_hold))
+
+    def get_on_hold(self):
+        return self._on_hold
+
diff --git a/azurelinuxagent/common/protocol/extensions_goal_state_from_vm_settings.py b/azurelinuxagent/common/protocol/extensions_goal_state_from_vm_settings.py
new file mode 100644
index 00000000..95a3a813
--- /dev/null
+++ b/azurelinuxagent/common/protocol/extensions_goal_state_from_vm_settings.py
@@ -0,0 +1,197 @@
+# 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 json
+import re
+import sys
+
+from azurelinuxagent.common.exception import VmSettingsError
+from azurelinuxagent.common.protocol.extensions_goal_state import ExtensionsGoalState
+from azurelinuxagent.common.utils.textutil import format_exception
+
+
+class ExtensionsGoalStateFromVmSettings(ExtensionsGoalState):
+    def __init__(self, etag, json_text):
+        super(ExtensionsGoalStateFromVmSettings, self).__init__()
+        self._id = etag
+        self._host_ga_plugin_version = None
+        self._schema_version = None
+        self._text = json_text
+        self._activity_id = None
+        self._correlation_id = None
+        self._created_on_timestamp = None
+        self._status_upload_blob = None
+        self._status_upload_blob_type = None
+        self._required_features = []
+        self._on_hold = False
+
+        try:
+            self._parse_vm_settings(json_text)
+            self._do_common_validations()
+        except Exception as e:
+            raise VmSettingsError("Error parsing vmSettings (etag: {0}): {1}\n{2}".format(etag, format_exception(e), self.get_redacted_text()))
+
+    @property
+    def id(self):
+        return self._id
+
+    @property
+    def activity_id(self):
+        return self._activity_id
+
+    @property
+    def correlation_id(self):
+        return self._correlation_id
+
+    @property
+    def created_on_timestamp(self):
+        return self._created_on_timestamp
+
+    @property
+    def status_upload_blob(self):
+        return self._status_upload_blob
+
+    @property
+    def status_upload_blob_type(self):
+        return self._status_upload_blob_type
+
+    def _set_status_upload_blob_type(self, value):
+        self._status_upload_blob_type = value
+
+    @property
+    def required_features(self):
+        return self._required_features
+
+    @property
+    def on_hold(self):
+        return self._on_hold
+
+    def get_redacted_text(self):
+        return re.sub(r'("protectedSettings"\s*:\s*)"[^"]+"', r'\1"*** REDACTED ***"', self._text)
+
+    def _parse_vm_settings(self, json_text):
+        vm_settings = _CaseFoldedDict.from_dict(json.loads(json_text))
+        self._parse_simple_attributes(vm_settings)
+        self._parse_status_upload_blob(vm_settings)
+        self._parse_required_features(vm_settings)
+        # TODO: Parse all atttributes
+
+    def _parse_simple_attributes(self, vm_settings):
+        self._host_ga_plugin_version = vm_settings.get("hostGAPluginVersion")
+        self._schema_version = vm_settings.get("vmSettingsSchemaVersion")
+        self._activity_id = self._string_to_id(vm_settings.get("activityId"))
+        self._correlation_id = self._string_to_id(vm_settings.get("correlationId"))
+        self._created_on_timestamp = self._ticks_to_utc_timestamp(vm_settings.get("extensionsLastModifiedTickCount"))
+
+    def _parse_status_upload_blob(self, vm_settings):
+        status_upload_blob = vm_settings.get("statusUploadBlob")
+        if status_upload_blob is None:
+            raise Exception("Missing statusUploadBlob")
+        self._status_upload_blob = status_upload_blob.get("value")
+        if self._status_upload_blob is None:
+            raise Exception("Missing statusUploadBlob.value")
+        self._status_upload_blob_type = status_upload_blob.get("statusBlobType")
+        if self._status_upload_blob is None:
+            raise Exception("Missing statusUploadBlob.statusBlobType")
+
+    def _parse_required_features(self, vm_settings):
+        required_features = vm_settings.get("requiredFeatures")
+        if required_features is not None:
+            if not isinstance(required_features, list):
+                raise Exception("requiredFeatures should be an array")
+
+            def get_required_features_names():
+                for feature in required_features:
+                    name = feature.get("name")
+                    if name is None:
+                        raise Exception("A required feature is missing the 'name' property")
+                    yield name
+
+            self._required_features.extend(get_required_features_names())
+
+
+#
+# TODO: The current implementation of the vmSettings API uses inconsistent cases on the names of the json items it returns.
+#       To work around that, we use _CaseFoldedDict to query those json items in a case-insensitive matter, Do not use
+#       _CaseFoldedDict for other purposes. Remove it once the vmSettings API is updated.
+#
+class _CaseFoldedDict(dict):
+    @staticmethod
+    def from_dict(dictionary):
+        case_folded = _CaseFoldedDict()
+        for key, value in dictionary.items():
+            case_folded[key] = _CaseFoldedDict._to_case_folded_dict_item(value)
+        return case_folded
+
+    def get(self, key):
+        return super(_CaseFoldedDict, self).get(_casefold(key))
+
+    def has_key(self, key):
+        return super(_CaseFoldedDict, self).get(_casefold(key))
+
+    def __getitem__(self, key):
+        return super(_CaseFoldedDict, self).__getitem__(_casefold(key))
+
+    def __setitem__(self, key, value):
+        return super(_CaseFoldedDict, self).__setitem__(_casefold(key), value)
+
+    def __contains__(self, key):
+        return  super(_CaseFoldedDict, self).__contains__(_casefold(key))
+
+    @staticmethod
+    def _to_case_folded_dict_item(item):
+        if isinstance(item, dict):
+            case_folded_dict = _CaseFoldedDict()
+            for key, value in item.items():
+                case_folded_dict[_casefold(key)] = _CaseFoldedDict._to_case_folded_dict_item(value)
+            return case_folded_dict
+        if isinstance(item, list):
+            return [_CaseFoldedDict._to_case_folded_dict_item(list_item) for list_item in item]
+        return item
+
+    def copy(self):
+        raise NotImplementedError()
+
+    @staticmethod
+    def fromkeys(*args, **kwargs):
+        raise NotImplementedError()
+
+    def pop(self, key, default=None):
+        raise NotImplementedError()
+
+    def setdefault(self, key, default=None):
+        raise NotImplementedError()
+
+    def update(self, E=None, **F):  # known special case of dict.update
+        raise NotImplementedError()
+
+    def __delitem__(self, *args, **kwargs):
+        raise NotImplementedError()
+
+
+# casefold() does not exist on Python 2 so we use lower() there
+def _casefold(string):
+    if sys.version_info[0] == 2:
+        return type(string).lower(string)  # the type of "string" can be unicode or str
+    # Class 'str' has no 'casefold' member (no-member) -- Disabled: This warning shows up on Python 2.7 pylint runs
+    # but this code is actually not executed on Python 2.
+    return str.casefold(string)  # pylint: disable=no-member
+
+
+
+
+
diff --git a/azurelinuxagent/common/protocol/restapi.py b/azurelinuxagent/common/protocol/restapi.py
index 5b1fc0c9..4928e4b0 100644
--- a/azurelinuxagent/common/protocol/restapi.py
+++ b/azurelinuxagent/common/protocol/restapi.py
@@ -19,7 +19,6 @@
 
 import socket
 import time
-from datetime import datetime, timedelta
 
 from azurelinuxagent.common.datacontract import DataContract, DataContractList
 from azurelinuxagent.common.future import ustr
@@ -190,28 +189,10 @@ class InVMGoalStateMetaData(DataContract):
     Object for parsing the GoalState MetaData received from CRP
     Eg: <InVMGoalStateMetaData inSvdSeqNo="2" createdOnTicks="637405409304121230" activityId="555e551c-600e-4fb4-90ba-8ab8ec28eccc" correlationId="400de90b-522e-491f-9d89-ec944661f531" />
     """
-    def __init__(self):
-        self.in_svd_seq_no = None
-        self.created_on_ticks = None
-        self.activity_id = None
-        self.correlation_id = None
-
-    def parse_node(self, in_vm_metadata_node):
-
-        def __ticks_to_datetime(ticks):
-            if ticks in (None, ""):
-                return None
-            try:
-                # C# ticks is a number of ticks since midnight 0001-01-01 00:00:00 (every tick is 1/10000000 of second)
-                # and UNIX timestamp is number of seconds since beginning of the UNIX epoch (1970-01-01 01:00:00).
-                # This function converts the ticks to datetime object that Python recognises.
-                return datetime.min + timedelta(seconds=float(ticks) / 10 ** 7)
-            except Exception:
-                return None
-
+    def __init__(self, in_vm_metadata_node):
         self.correlation_id = getattrib(in_vm_metadata_node, "correlationId")
         self.activity_id = getattrib(in_vm_metadata_node, "activityId")
-        self.created_on_ticks = __ticks_to_datetime(getattrib(in_vm_metadata_node, "createdOnTicks"))
+        self.created_on_ticks = getattrib(in_vm_metadata_node, "createdOnTicks")
         self.in_svd_seq_no = getattrib(in_vm_metadata_node, "inSvdSeqNo")
 
 
diff --git a/azurelinuxagent/common/protocol/wire.py b/azurelinuxagent/common/protocol/wire.py
index c6211ffa..06ccc539 100644
--- a/azurelinuxagent/common/protocol/wire.py
+++ b/azurelinuxagent/common/protocol/wire.py
@@ -36,6 +36,7 @@ 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.extensions_goal_state_factory import ExtensionsGoalStateFactory
 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, \
@@ -123,12 +124,6 @@ class WireProtocol(DataContract):
     def get_incarnation(self):
         return self.client.get_goal_state().incarnation
 
-    def get_in_vm_gs_metadata(self):
-        return self.client.get_extensions_goal_state().in_vm_gs_metadata
-
-    def get_required_features(self):
-        return self.client.get_extensions_goal_state().get_required_features()
-
     def get_vmagent_manifests(self):
         goal_state = self.client.get_goal_state()
         ext_conf = self.client.get_extensions_goal_state()
@@ -152,9 +147,8 @@ class WireProtocol(DataContract):
         man = self.client.get_ext_manifest(ext_handler)
         return man.pkg_list
 
-    def get_artifacts_profile(self):
-        logger.verbose("Get In-VM Artifacts Profile")
-        return self.client.get_artifacts_profile()
+    def get_extensions_goal_state(self):
+        return self.client.get_extensions_goal_state()
 
     def _download_ext_handler_pkg_through_host(self, uri, destination):
         host = self.client.get_host_plugin()
@@ -807,20 +801,20 @@ class WireClient(object):
                 goal_state.fetch_full_goal_state(self)
                 self._goal_state = goal_state
                 if self._goal_state.extensions_config_uri is None:
-                    self._extensions_goal_state = ExtensionsGoalState.create_empty()
+                    self._extensions_goal_state = ExtensionsGoalStateFactory.create_empty()
                 else:
                     xml_text = self.fetch_config(self._goal_state.extensions_config_uri, self.get_header())
-                    self._extensions_goal_state = ExtensionsGoalState.create_from_extensions_config(goal_state.incarnation, xml_text)
+                    self._extensions_goal_state = ExtensionsGoalStateFactory.create_from_extensions_config(goal_state.incarnation, xml_text, self)
                 updated = True
 
             if conf.get_enable_fast_track():
                 # Since currently we query vmSettings only to exercise the HostGAPlugin, errors in this section of the code should not
                 # prevent the agent from processing the goal state; we simply report a summary of those errors.
                 try:
-                    request_etag = None if force_update or self._extensions_goal_state_from_vm_settings is None else self._extensions_goal_state_from_vm_settings.get_id()
+                    request_etag = None if force_update or self._extensions_goal_state_from_vm_settings is None else self._extensions_goal_state_from_vm_settings.id
                     response_etag, vm_settings = self._fetch_vm_settings(request_etag)
                     if vm_settings is not None:  # vmSettings is None when there has not been a new goal state, or the HostGAPlugin does not support FastTrack
-                        self._extensions_goal_state_from_vm_settings = ExtensionsGoalState.create_from_vm_settings(response_etag, vm_settings)
+                        self._extensions_goal_state_from_vm_settings = ExtensionsGoalStateFactory.create_from_vm_settings(response_etag, vm_settings)
                         updated = True
                     # If either goal state was updated, compare them
                     if updated and self._extensions_goal_state_from_vm_settings is not None:
@@ -920,16 +914,14 @@ class WireClient(object):
             save_if_not_none(self._goal_state.shared_conf, SHARED_CONF_FILE_NAME)
             save_if_not_none(self._goal_state.remote_access, REMOTE_ACCESS_FILE_NAME.format(self._goal_state.incarnation))
             if self._extensions_goal_state is not None:
-                # disable=assignment-from-none: E1128: Assigning result of a function call, where the function returns None
-                text = self._extensions_goal_state.get_redacted_text()  # pylint: disable=assignment-from-none
+                text = self._extensions_goal_state.get_redacted_text()
                 if text != '':
-                    self._save_cache(text, EXT_CONF_FILE_NAME.format(self._extensions_goal_state.get_id()))
+                    self._save_cache(text, EXT_CONF_FILE_NAME.format(self._extensions_goal_state.id))
             # TODO: When Fast Track is fully enabled self._extensions_goal_state_from_vm_settings will go away and this can be deleted
             if self._extensions_goal_state_from_vm_settings is not None:
-                # disable=assignment-from-none: E1128: Assigning result of a function call, where the function returns None
-                text = self._extensions_goal_state_from_vm_settings.get_redacted_text()  # pylint: disable=assignment-from-none
+                text = self._extensions_goal_state_from_vm_settings.get_redacted_text()
                 if text != '':
-                    self._save_cache(text, VM_SETTINGS_FILE_NAME.format(self._extensions_goal_state_from_vm_settings.get_id()))
+                    self._save_cache(text, VM_SETTINGS_FILE_NAME.format(self._extensions_goal_state_from_vm_settings.id))
             # END TODO
 
         except Exception as e:
@@ -1136,15 +1128,15 @@ class WireClient(object):
     def upload_status_blob(self):
         extensions_goal_state = self.get_extensions_goal_state()
 
-        if extensions_goal_state.get_status_upload_blob() is None:
+        if extensions_goal_state.status_upload_blob is None:
             # the status upload blob is in ExtensionsConfig so force a full goal state refresh
             self.update_goal_state(force_update=True)
             extensions_goal_state = self.get_extensions_goal_state()
 
-        if extensions_goal_state.get_status_upload_blob() is None:
+        if extensions_goal_state.status_upload_blob is None:
             raise ProtocolNotFoundError("Status upload uri is missing")
 
-        blob_type = extensions_goal_state.get_status_upload_blob_type()
+        blob_type = extensions_goal_state.status_upload_blob_type
 
         try:
             self.status_blob.prepare(blob_type)
@@ -1162,7 +1154,7 @@ class WireClient(object):
         # wrong. This is why we try HostPlugin then direct.
         try:
             host = self.get_host_plugin()
-            host.put_vm_status(self.status_blob, extensions_goal_state.get_status_upload_blob(), extensions_goal_state.get_status_upload_blob_type())
+            host.put_vm_status(self.status_blob, extensions_goal_state.status_upload_blob, extensions_goal_state.status_upload_blob_type)
             return
         except ResourceGoneError:
             # refresh the host plugin client and try again on the next iteration of the main loop
@@ -1174,7 +1166,7 @@ class WireClient(object):
             self.report_status_event(msg, is_success=True)
 
         try:
-            if self.status_blob.upload(extensions_goal_state.get_status_upload_blob()):
+            if self.status_blob.upload(extensions_goal_state.status_upload_blob):
                 return
         except Exception as e:
             msg = "Exception uploading status blob: {0}".format(ustr(e))
@@ -1345,53 +1337,8 @@ class WireClient(object):
                                                      goal_state.role_config_name))
         return self._host_plugin
 
-    def has_artifacts_profile_blob(self):
-        extensions_goal_state = self.get_extensions_goal_state()
-        return extensions_goal_state and not \
-            textutil.is_str_none_or_whitespace(extensions_goal_state.artifacts_profile_blob)
-
-    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)
-        return profile
-
-    def get_artifacts_profile(self):
-        artifacts_profile = None
-
-        if self.has_artifacts_profile_blob():
-            blob = self.get_extensions_goal_state().artifacts_profile_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)
-
-            logger.verbose("Retrieving the artifacts profile")
-
-            try:
-                profile = self.send_request_using_appropriate_channel(direct_func, host_func)
-                if profile is None:
-                    logger.warn("Failed to fetch artifacts profile from blob {0}", blob)
-                    return None
-            except Exception as error:
-                logger.warn("Exception retrieving artifacts profile from blob {0}. Error: {1}".format(blob, ustr(error)))
-                return None
-
-            if not textutil.is_str_empty(profile):
-                logger.verbose("Artifacts profile downloaded")
-                try:
-                    artifacts_profile = InVMArtifactsProfile(profile)
-                except Exception:
-                    logger.warn("Could not parse artifacts profile blob")
-                    msg = "Content: [{0}]".format(profile)
-                    logger.verbose(msg)
-
-                    report_event(op=WALAEventOperation.ArtifactsProfileBlob,
-                                 is_success=False,
-                                 message=msg,
-                                 log_event=False)
-
-        return artifacts_profile
+    def get_on_hold(self):
+        return self.get_extensions_goal_state().on_hold
 
     def upload_logs(self, content):
         host_func = lambda: self._upload_logs_through_host(content)
diff --git a/azurelinuxagent/ga/exthandlers.py b/azurelinuxagent/ga/exthandlers.py
index 247a96f6..50ee467e 100644
--- a/azurelinuxagent/ga/exthandlers.py
+++ b/azurelinuxagent/ga/exthandlers.py
@@ -293,36 +293,6 @@ class ExtHandlersHandler(object):
                self.__gs_aggregate_status.status == GoalStateStatus.Failed and \
                self.__gs_aggregate_status.code == GoalStateAggregateStatusCodes.GoalStateUnsupportedRequiredFeatures
 
-    def get_goal_state_debug_metadata(self):
-        """
-        This function fetches metadata fetched from the GoalState for better debuggability
-        :return: Tuple (activity_id, correlation_id, gs_created_timestamp) or "NA" for any property that's not available
-        """
-
-        def format_value(parse_fn, value):
-
-            try:
-                if value not in (None, ""):
-                    return parse_fn(value)
-            except Exception as e:
-                # A failure here isn't a fatal error, because the info we're
-                # trying to retrieve is debug only on linux.
-                error_msg = u"Couldn't parse debug metadata value: {0}".format(e)
-                logger.verbose(error_msg)
-
-            return "NA"
-
-        to_utc = lambda time: time.strftime(logger.Logger.LogTimeFormatInUTC)
-        identity = lambda value: value
-
-        in_vm_gs_metadata = self.protocol.get_in_vm_gs_metadata()
-
-        gs_creation_time = format_value(to_utc, in_vm_gs_metadata.created_on_ticks)
-        activity_id = format_value(identity, in_vm_gs_metadata.activity_id)
-        correlation_id = format_value(identity, in_vm_gs_metadata.correlation_id)
-
-        return activity_id, correlation_id, gs_creation_time
-
     def run(self):
         etag, activity_id, correlation_id, gs_creation_time = None, None, None, None
 
@@ -333,7 +303,11 @@ class ExtHandlersHandler(object):
             if not self._extension_processing_allowed():
                 return
 
-            activity_id, correlation_id, gs_creation_time = self.get_goal_state_debug_metadata()
+            extensions_goal_state = self.protocol.get_extensions_goal_state()
+
+            gs_creation_time = extensions_goal_state.created_on_timestamp
+            activity_id = extensions_goal_state.activity_id
+            correlation_id = extensions_goal_state.correlation_id
         except Exception as error:
             msg = u"ProcessExtensionsInGoalState - Exception processing extension handlers:{0}".format(textutil.format_exception(error))
             logger.warn(msg)
@@ -367,7 +341,7 @@ class ExtHandlersHandler(object):
             add_event(op=WALAEventOperation.ExtensionProcessing, is_success=(error is None), message=message, log_event=False, duration=duration)
 
     def __get_unsupported_features(self):
-        required_features = self.protocol.get_required_features()
+        required_features = self.protocol.client.get_extensions_goal_state().required_features
         supported_features = get_agent_supported_features_list_for_crp()
         return [feature for feature in required_features if feature not in supported_features]
 
@@ -476,8 +450,7 @@ class ExtHandlersHandler(object):
             return False
 
         if conf.get_enable_overprovisioning():
-            artifacts_profile = self.protocol.get_artifacts_profile()
-            if artifacts_profile and artifacts_profile.is_on_hold():
+            if self.protocol.get_extensions_goal_state().on_hold:
                 logger.info("Extension handling is on hold")
                 return False
 


Test patch:
diff --git a/tests/data/hostgaplugin/ext_conf-invalid_blob_type.xml b/tests/data/hostgaplugin/ext_conf-invalid_blob_type.xml
new file mode 100644
index 00000000..ac4384ce
--- /dev/null
+++ b/tests/data/hostgaplugin/ext_conf-invalid_blob_type.xml
@@ -0,0 +1,94 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Extensions version="1.0.0.0" goalStateIncarnation="10">
+  <GuestAgentExtension xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
+    <GAFamilies>
+      <GAFamily>
+        <Name>Prod</Name>
+        <Uris>
+          <Uri>https://zrdfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Prod_uscentraleuap_manifest.xml</Uri>
+          <Uri>https://ardfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Prod_uscentraleuap_manifest.xml</Uri>
+        </Uris>
+        <Version>2.5.0.2</Version>
+      </GAFamily>
+      <GAFamily>
+        <Name>Test</Name>
+        <Uris>
+          <Uri>https://zrdfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Test_uscentraleuap_manifest.xml</Uri>
+          <Uri>https://ardfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Test_uscentraleuap_manifest.xml</Uri>
+        </Uris>
+        <Version>2.5.0.2</Version>
+      </GAFamily>
+    </GAFamilies>
+    <Location>CentralUSEUAP</Location>
+    <ServiceName>CRP</ServiceName>
+  </GuestAgentExtension>
+  <RequiredFeatures>
+    <RequiredFeature>
+      <Name>MultipleExtensionsPerHandler</Name>
+    </RequiredFeature>
+  </RequiredFeatures>
+  <StatusUploadBlob statusBlobType="INVALID_BLOB_TYPE">https://dcrcqabsr1.blob.core.windows.net/$system/edpxmal5j1.058b176d-445b-4e75-bd97-4911511b7d96.status?sv=2018-03-28&amp;sr=b&amp;sk=system-1&amp;sig=U4KaLxlyYfgQ%2fie8RCwgMBSXa3E4vlW0ozPYOEHikoc%3d&amp;se=9999-01-01T00%3a00%3a00Z&amp;sp=w</StatusUploadBlob>
+  <Plugins>
+    <Plugin 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" state="enabled" autoUpgrade="true" failoverlocation="https://umsanh4b5rfz0q0p4pwm.blob.core.windows.net/5237dd14-0aad-f051-0fad-1e33e1b63091/5237dd14-0aad-f051-0fad-1e33e1b63091_manifest.xml" runAsStartupTask="false" isJson="true" useExactVersion="true">
+      <additionalLocations>
+        <additionalLocation>https://umsafwzhkbm1rfrhl0ws.blob.core.windows.net/5237dd14-0aad-f051-0fad-1e33e1b63091/5237dd14-0aad-f051-0fad-1e33e1b63091_manifest.xml</additionalLocation>
+      </additionalLocations>
+    </Plugin>
+    <Plugin 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" state="enabled" autoUpgrade="true" failoverlocation="https://umsawqtlsshtn5v2nfgh.blob.core.windows.net/f4086d41-69f9-3103-78e0-8a2c7e789d0f/f4086d41-69f9-3103-78e0-8a2c7e789d0f_manifest.xml" runAsStartupTask="false" isJson="true" useExactVersion="true">
+      <additionalLocations>
+        <additionalLocation>https://umsajbjtqrb3zqjvgb2z.blob.core.windows.net/f4086d41-69f9-3103-78e0-8a2c7e789d0f/f4086d41-69f9-3103-78e0-8a2c7e789d0f_manifest.xml</additionalLocation>
+      </additionalLocations>
+    </Plugin>
+  </Plugins>
+  <PluginSettings>
+    <Plugin name="Microsoft.Azure.Extensions.CustomScript" version="2.1.6">
+      <RuntimeSettings seqNo="0">
+        {
+          "runtimeSettings": [
+            {
+              "handlerSettings": {
+                "publicSettings": {"commandToExecute":"echo '09cd27e9-fbd6-48ad-be86-55f3783e0a23'"}
+              }
+            }
+          ]
+      }
+      </RuntimeSettings>
+    </Plugin>
+    <Plugin name="Microsoft.CPlat.Core.RunCommandHandlerLinux" version="1.2.0">
+      <ExtensionRuntimeSettings seqNo="0" name="MCExt1" state="enabled">
+        {
+          "runtimeSettings": [
+            {
+              "handlerSettings": {
+                "publicSettings": {"source":{"script":"echo '737fa9f1-e9bf-4c3e-ab1f-9e03cd0b5b40'"}}
+              }
+            }
+          ]
+      }
+      </ExtensionRuntimeSettings>
+      <ExtensionRuntimeSettings seqNo="0" name="MCExt2" state="enabled">
+        {
+          "runtimeSettings": [
+            {
+              "handlerSettings": {
+                "publicSettings": {"source":{"script":"echo 'f6a0a405-c028-4e68-bd77-5d491fbbd9cf'"}}
+              }
+            }
+          ]
+      }</ExtensionRuntimeSettings>
+      <ExtensionRuntimeSettings seqNo="0" name="MCExt3" state="enabled">
+        {
+          "runtimeSettings": [
+            {
+              "handlerSettings": {
+                "publicSettings": {"source":{"script":"echo '338e316a-01bf-4513-8ae1-b603b09ad155'"}}
+              }
+            }
+          ]
+      }
+      </ExtensionRuntimeSettings>
+    </Plugin>
+  </PluginSettings>
+  <InVMArtifactsProfileBlob>https://dcrcqabsr1.blob.core.windows.net/$system/edpxmal5j1.058b176d-445b-4e75-bd97-4911511b7d96.vmSettings?sv=2018-03-28&amp;sr=b&amp;sk=system-1&amp;sig=mOMtcUyao4oNPMtcVhQjzMK%2bmGSJS3Y1MIKOJPjqzus%3d&amp;se=9999-01-01T00%3a00%3a00Z&amp;sp=r</InVMArtifactsProfileBlob>
+  <InVMGoalStateMetaData inSvdSeqNo="10" createdOnTicks="637693267431616449" activityId="2e7f8b5d-f637-4721-b757-cb190d49b4e9" correlationId="1bef4c48-044e-4225-8f42-1d1eac1eb158" />
+</Extensions>
\ No newline at end of file
diff --git a/tests/data/hostgaplugin/vm_settings-invalid_blob_type.json b/tests/data/hostgaplugin/vm_settings-invalid_blob_type.json
new file mode 100644
index 00000000..c49f2047
--- /dev/null
+++ b/tests/data/hostgaplugin/vm_settings-invalid_blob_type.json
@@ -0,0 +1,101 @@
+{
+    "activityId": "2e7f8b5d-f637-4721-b757-cb190d49b4e9",
+    "correlationId": "1bef4c48-044e-4225-8f42-1d1eac1eb158",
+    "extensionsLastModifiedTickCount": 637693267431616449,
+    "extensionGoalStatesSource": "Fabric",
+    "StatusUploadBlob": {
+        "statusBlobType": "INVALID_BLOB_TYPE",
+        "value": "https://dcrcqabsr1.blob.core.windows.net/$system/edpxmal5j1.058b176d-445b-4e75-bd97-4911511b7d96.status?sv=2018-03-28&sr=b&sk=system-1&sig=U4KaLxlyYfgQ%2fie8RCwgMBSXa3E4vlW0ozPYOEHikoc%3d&se=9999-01-01T00%3a00%3a00Z&sp=w"
+    },
+    "inVMMetadata": {
+        "subscriptionId": "8e037ad4-618f-4466-8bc8-5099d41ac15b",
+        "resourceGroupName": "rg-dc-qphvx25",
+        "vmName": "edpxmal5j1",
+        "location": "CentralUSEUAP",
+        "vmId": "058b176d-445b-4e75-bd97-4911511b7d96",
+        "vmSize": "Standard_D2s_v3",
+        "osType": "Linux"
+    },
+    "requiredFeatures": [
+        {
+            "name": "MultipleExtensionsPerHandler"
+        }
+    ],
+    "gaFamilies": [
+        {
+            "Name": "Prod",
+            "Version": "2.5.0.2",
+            "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": "2.5.0.2",
+            "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.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://umsanh4b5rfz0q0p4pwm.blob.core.windows.net/5237dd14-0aad-f051-0fad-1e33e1b63091/5237dd14-0aad-f051-0fad-1e33e1b63091_manifest.xml",
+            "additionalLocations": [
+                "https://umsafwzhkbm1rfrhl0ws.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 '09cd27e9-fbd6-48ad-be86-55f3783e0a23'\"}"
+                }
+            ]
+        },
+        {
+            "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://umsawqtlsshtn5v2nfgh.blob.core.windows.net/f4086d41-69f9-3103-78e0-8a2c7e789d0f/f4086d41-69f9-3103-78e0-8a2c7e789d0f_manifest.xml",
+            "additionalLocations": [
+                "https://umsajbjtqrb3zqjvgb2z.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 '737fa9f1-e9bf-4c3e-ab1f-9e03cd0b5b40'\"}}",
+                    "seqNo": 0,
+                    "extensionName": "MCExt1",
+                    "extensionState": "enabled"
+                },
+                {
+                    "publicSettings": "{\"source\":{\"script\":\"echo 'f6a0a405-c028-4e68-bd77-5d491fbbd9cf'\"}}",
+                    "seqNo": 0,
+                    "extensionName": "MCExt2",
+                    "extensionState": "enabled"
+                },
+                {
+                    "publicSettings": "{\"source\":{\"script\":\"echo '338e316a-01bf-4513-8ae1-b603b09ad155'\"}}",
+                    "seqNo": 0,
+                    "extensionName": "MCExt3",
+                    "extensionState": "enabled"
+                }
+            ]
+        }
+    ]
+}
\ No newline at end of file
diff --git a/tests/data/wire/ext_conf_in_vm_empty_artifacts_profile.xml b/tests/data/wire/ext_conf_in_vm_empty_artifacts_profile.xml
new file mode 100644
index 00000000..345ff075
--- /dev/null
+++ b/tests/data/wire/ext_conf_in_vm_empty_artifacts_profile.xml
@@ -0,0 +1,28 @@
+<Extensions version="1.0.0.0" goalStateIncarnation="9">
+  <GuestAgentExtension xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
+    <GAFamilies>
+      <GAFamily>
+        <Name>Prod</Name>
+        <Uris>
+            <Uri>http://mock-goal-state/manifest_of_ga.xml</Uri>
+        </Uris>
+      </GAFamily>
+      <GAFamily>
+        <Name>Test</Name>
+        <Uris>
+            <Uri>http://mock-goal-state/manifest_of_ga.xml</Uri>
+          </Uris>
+      </GAFamily>
+    </GAFamilies>
+  </GuestAgentExtension>
+  <Plugins>
+    <Plugin name="OSTCExtensions.ExampleHandlerLinux" version="1.0.0" location="http://mock-goal-state/rdfepirv2hknprdstr03.blob.core.windows.net/b01058962be54ceca550a390fa5ff064/Microsoft.OSTCExtensions_ExampleHandlerLinux_asiaeast_manifest.xml" config="" state="enabled" autoUpgrade="false" failoverlocation="http://mock-goal-state/rdfepirv2hknprdstr04.blob.core.windows.net/b01058962be54ceca550a390fa5ff064/Microsoft.OSTCExtensions_ExampleHandlerLinux_asiaeast_manifest.xml" runAsStartupTask="false" isJson="true" />
+  </Plugins>
+  <PluginSettings>
+    <Plugin name="OSTCExtensions.ExampleHandlerLinux" version="1.0.0">
+      <RuntimeSettings seqNo="0">{"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]}</RuntimeSettings>
+    </Plugin>
+  </PluginSettings>
+  <StatusUploadBlob statusBlobType="BlockBlob">https://test.blob.core.windows.net/vhds/test-cs12.test-cs12.test-cs12.status?sr=b&amp;sp=rw&amp;se=9999-01-01&amp;sk=key1&amp;sv=2014-02-14&amp;sig=hfRh7gzUE7sUtYwke78IOlZOrTRCYvkec4hGZ9zZzXo</StatusUploadBlob>
+  <InVMArtifactsProfileBlob statusBlobType="BlockBlob">   </InVMArtifactsProfileBlob>
+</Extensions>
diff --git a/tests/data/wire/ext_conf_invalid_vm_metadata.xml b/tests/data/wire/ext_conf_invalid_vm_metadata.xml
index 3c407021..081e734e 100644
--- a/tests/data/wire/ext_conf_invalid_vm_metadata.xml
+++ b/tests/data/wire/ext_conf_invalid_vm_metadata.xml
@@ -25,5 +25,5 @@
       </Plugin>
    </PluginSettings>
    <StatusUploadBlob statusBlobType="BlockBlob">https://test.blob.core.windows.net/vhds/test-cs12.test-cs12.test-cs12.status?sr=b&amp;sp=rw&amp;se=9999-01-01&amp;sk=key1&amp;sv=2014-02-14&amp;sig=hfRh7gzUE7sUtYwke78IOlZOrTRCYvkec4hGZ9zZzXo</StatusUploadBlob>
-   <InVMGoalStateMetaData inSvdSeqNo="2" createdOnTicks="0" activityId="555e551c-600e-4fb4-90ba-8ab8ec28eccc" correlationId="400de90b-522e-491f-9d89-ec944661f531" />
+   <InVMGoalStateMetaData inSvdSeqNo="2" createdOnTicks="0" activityId="" correlationId="" />
 </Extensions>
\ No newline at end of file
diff --git a/tests/ga/test_extension.py b/tests/ga/test_extension.py
index 8d5b341c..70249bcd 100644
--- a/tests/ga/test_extension.py
+++ b/tests/ga/test_extension.py
@@ -435,14 +435,7 @@ class TestHandlerStateMigration(AgentTestCase):
             self.assertTrue(False, "Unexpected exception: {0}".format(str(e)))  # pylint: disable=redundant-unittest-assert
         return
 
-# Deprecated. New tests should be added to the TestExtension class
-@patch('time.sleep', side_effect=lambda _: mock_sleep(0.001))
-@patch("azurelinuxagent.common.protocol.wire.CryptUtil")
-@patch("azurelinuxagent.common.utils.restutil.http_get")
-class TestExtension_Deprecated(AgentTestCase):
-    def setUp(self):
-        AgentTestCase.setUp(self)
-
+class TestExtensionBase(AgentTestCase):
     def _assert_handler_status(self, report_vm_status, expected_status,
                                expected_ext_count, version,
                                expected_handler_name="OSTCExtensions.ExampleHandlerLinux", expected_msg=None):
@@ -461,6 +454,15 @@ class TestExtension_Deprecated(AgentTestCase):
         if expected_msg is not None:
             self.assertIn(expected_msg, handler_status.message)
 
+
+# Deprecated. New tests should be added to the TestExtension class
+@patch('time.sleep', side_effect=lambda _: mock_sleep(0.001))
+@patch("azurelinuxagent.common.protocol.wire.CryptUtil")
+@patch("azurelinuxagent.common.utils.restutil.http_get")
+class TestExtension_Deprecated(TestExtensionBase):
+    def setUp(self):
+        AgentTestCase.setUp(self)
+
     def _assert_ext_pkg_file_status(self, expected_to_be_present=True, extension_version="1.0.0",
                                     extension_handler_name="OSTCExtensions.ExampleHandlerLinux"):
         zip_file_format = "{0}__{1}.zip"
@@ -1379,149 +1381,6 @@ class TestExtension_Deprecated(AgentTestCase):
         exthandlers_handler.run()
         exthandlers_handler.report_ext_handlers_status()
 
-
-    def test_it_should_process_extensions_only_if_allowed(self, mock_get, mock_crypt, *args):
-
-        def assert_extensions_called(exthandlers_handler, expected_call_count=0):
-            extension_name = 'OSTCExtensions.ExampleHandlerLinux'
-            extension_calls = []
-            original_popen = subprocess.Popen
-
-            def mock_popen(*args, **kwargs):
-                if extension_name in args[0]:
-                    extension_calls.append(args[0])
-                return original_popen(*args, **kwargs)
-
-            with patch('subprocess.Popen', side_effect=mock_popen):
-                exthandlers_handler.run()
-                exthandlers_handler.report_ext_handlers_status()
-
-                self.assertEqual(expected_call_count, len(extension_calls), "Call counts dont match")
-
-        test_data = mockwiredata.WireProtocolData(mockwiredata.DATA_FILE)
-        exthandlers_handler, protocol = self._create_mock(test_data, mock_get, mock_crypt, *args)
-
-        # Extension called once for Install and once for Enable
-        assert_extensions_called(exthandlers_handler, expected_call_count=2)
-        self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.0.0")
-
-        # Update GoalState
-        test_data.set_incarnation(2)
-        protocol.update_goal_state()
-
-        with patch.object(conf, 'get_extensions_enabled', return_value=False):
-            assert_extensions_called(exthandlers_handler, expected_call_count=0)
-
-        # Disabled over-provisioning in configuration
-        # In this case we should process GoalState as incarnation changed
-        with patch.object(conf, 'get_extensions_enabled', return_value=True):
-            with patch.object(conf, 'get_enable_overprovisioning', return_value=False):
-                # 1 expected call count for Enable command
-                assert_extensions_called(exthandlers_handler, expected_call_count=1)
-
-        # Update GoalState
-        test_data.set_incarnation(3)
-        protocol.update_goal_state()
-
-        # Enabled on_hold property in artifact_blob
-        with patch.object(conf, 'get_extensions_enabled', return_value=True):
-            with patch.object(conf, "get_enable_overprovisioning", return_value=True):
-                with patch.object(protocol, "get_artifacts_profile",
-                                  return_value=InVMArtifactsProfile(json.dumps({'onHold': True}))):
-                    assert_extensions_called(exthandlers_handler, expected_call_count=0)
-
-        # Disabled on_hold property in artifact_blob
-        with patch.object(conf, 'get_extensions_enabled', return_value=True):
-            with patch.object(conf, "get_enable_overprovisioning", return_value=True):
-                with patch.object(protocol, "get_artifacts_profile",
-                                  return_value=InVMArtifactsProfile(json.dumps({'onHold': False}))):
-                    # 1 expected call count for Enable command
-                    assert_extensions_called(exthandlers_handler, expected_call_count=1)
-
-    def test_it_should_process_extensions_appropriately_on_artifact_hold(self, mock_get, mock_crypt, *args):
-        test_data = mockwiredata.WireProtocolData(mockwiredata.DATA_FILE)
-        exthandlers_handler, protocol = self._create_mock(test_data, mock_get, mock_crypt, *args)
-
-        # enable extension handling blocking
-        conf.get_enable_overprovisioning = Mock(return_value=True)
-        mock_in_vm_artifacts_profile = InVMArtifactsProfile(MagicMock())
-
-        # Test when is_on_hold returns True - should not work
-        mock_in_vm_artifacts_profile.is_on_hold = Mock(return_value=True)
-        protocol.get_artifacts_profile = Mock(return_value=mock_in_vm_artifacts_profile)
-
-        exthandlers_handler.run()
-        exthandlers_handler.report_ext_handlers_status()
-
-        vm_agent_status = protocol.report_vm_status.call_args[0][0].vmAgent
-        self.assertEqual(vm_agent_status.status, "Ready", "Agent should report ready")
-        self.assertEqual(0, len(vm_agent_status.extensionHandlers),
-                         "No extensions should be reported as on_hold is True")
-        self.assertIsNone(vm_agent_status.vm_artifacts_aggregate_status.goal_state_aggregate_status,
-                          "No GS Aggregate status should be reported")
-
-        # Test when is_on_hold returns False - should work
-        mock_in_vm_artifacts_profile.is_on_hold = Mock(return_value=False)
-        protocol.get_artifacts_profile = Mock(return_value=mock_in_vm_artifacts_profile)
-        exthandlers_handler.run()
-        exthandlers_handler.report_ext_handlers_status()
-
-        self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.0.0")
-        self.assertEqual("1", protocol.report_vm_status.call_args[0][
-            0].vmAgent.vm_artifacts_aggregate_status.goal_state_aggregate_status.in_svd_seq_no, "Incarnation mismatch")
-
-        # Test when in_vm_artifacts_profile is not available - Should work
-        protocol.get_artifacts_profile = Mock(return_value=None)
-        # Update GoalState
-        test_data.set_incarnation(2)
-        protocol.update_goal_state()
-
-        exthandlers_handler.run()
-        exthandlers_handler.report_ext_handlers_status()
-
-        self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.0.0")
-        self.assertEqual("2", protocol.report_vm_status.call_args[0][
-            0].vmAgent.vm_artifacts_aggregate_status.goal_state_aggregate_status.in_svd_seq_no, "Incarnation mismatch")
-
-    def test_it_should_parse_valid_in_vm_metadata_properly(self, mock_get, mock_crypt, *args):
-
-        test_data = mockwiredata.WireProtocolData(mockwiredata.DATA_FILE_IN_VM_META_DATA)
-        exthandlers_handler, protocol = self._create_mock(test_data, mock_get, mock_crypt, *args)
-
-        exthandlers_handler.run()
-        exthandlers_handler.report_ext_handlers_status()
-
-        self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.0.0")
-        activity_id, correlation_id, gs_creation_time = exthandlers_handler.get_goal_state_debug_metadata()
-        self.assertEqual(activity_id, "555e551c-600e-4fb4-90ba-8ab8ec28eccc", "Incorrect activity Id")
-        self.assertEqual(correlation_id, "400de90b-522e-491f-9d89-ec944661f531", "Incorrect correlation Id")
-        self.assertEqual(gs_creation_time, '2020-11-09T17:48:50.412125Z', "Incorrect GS Creation time")
-
-    def test_it_should_process_goal_state_even_if_metadata_missing(self, mock_get, mock_crypt, *args):
-
-        # If the data is not provided in ExtensionConfig, it should just be None
-        test_data = mockwiredata.WireProtocolData(mockwiredata.DATA_FILE)
-        exthandlers_handler, protocol = self._create_mock(test_data, mock_get, mock_crypt, *args)
-
-        exthandlers_handler.run()
-        exthandlers_handler.report_ext_handlers_status()
-
-        self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.0.0")
-        activity_id, correlation_id, gs_creation_time = exthandlers_handler.get_goal_state_debug_metadata()
-        self.assertEqual(activity_id, "NA", "Activity Id should be NA")
-        self.assertEqual(correlation_id, "NA", "Correlation Id should be NA")
-        self.assertEqual(gs_creation_time, "NA", "GS Creation time should be NA")
-
-    def test_it_should_process_goal_state_even_if_metadata_invalid(self, mock_get, mock_crypt, *args):
-
-        test_data = mockwiredata.WireProtocolData(mockwiredata.DATA_FILE_INVALID_VM_META_DATA)
-        exthandlers_handler, protocol = self._create_mock(test_data, mock_get, mock_crypt, *args)
-
-        exthandlers_handler.run()
-        exthandlers_handler.report_ext_handlers_status()
-
-        self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.0.0")
-
     def _assert_ext_status(self, vm_agent_status, expected_status,
                            expected_seq_no, expected_handler_name="OSTCExtensions.ExampleHandlerLinux", expected_msg=None):
 
@@ -2589,7 +2448,7 @@ class TestExtension_Deprecated(AgentTestCase):
         test_data = mockwiredata.WireProtocolData(mockwiredata.DATA_FILE_REQUIRED_FEATURES)
         _, protocol = self._create_mock(test_data, mock_get, mock_crypt_util, *args)
 
-        required_features = protocol.get_required_features()
+        required_features = protocol.get_extensions_goal_state().required_features
         self.assertEqual(3, len(required_features), "Incorrect features parsed")
         for i, feature in enumerate(required_features):
             self.assertEqual(feature, "TestRequiredFeature{0}".format(i+1), "Name mismatch")
@@ -2625,7 +2484,6 @@ class TestExtensionSequencing(AgentTestCase):
         protocol = WireProtocol(KNOWN_WIRESERVER_IP)
         protocol.detect()
         protocol.report_vm_status = MagicMock()
-        protocol.get_artifacts_profile = MagicMock()
 
         handler = get_exthandlers_handler(protocol)
         handler.ext_handlers, handler.last_etag = protocol.get_ext_handlers()
@@ -3458,7 +3316,16 @@ class TestAdditionalLocationsExtensions(AgentTestCase):
 
 
 # New test cases should be added here.This class uses mock_wire_protocol
-class TestExtension(AgentTestCase):
+class TestExtension(TestExtensionBase, HttpRequestPredicates):
+    @classmethod
+    def setUpClass(cls):
+        AgentTestCase.setUpClass()
+        cls.mock_sleep = patch('time.sleep', side_effect=lambda _: mock_sleep(0.001))
+        cls.mock_sleep.start()
+
+    @classmethod
+    def tearDownClass(cls):
+        cls.mock_sleep.stop()
 
     def setUp(self):
         AgentTestCase.setUp(self)
@@ -3610,6 +3477,104 @@ class TestExtension(AgentTestCase):
             test_subject.archive()
             self.assertEqual(2, len(os.listdir(os.path.join(self.tmp_dir, "history"))))
 
+    def test_it_should_process_extensions_only_if_allowed(self):
+        def assert_extensions_called(exthandlers_handler, expected_call_count=0):
+            extension_name = 'OSTCExtensions.ExampleHandlerLinux'
+            extension_calls = []
+            original_popen = subprocess.Popen
+
+            def mock_popen(*args, **kwargs):
+                if extension_name in args[0]:
+                    extension_calls.append(args[0])
+                return original_popen(*args, **kwargs)
+
+            with patch('subprocess.Popen', side_effect=mock_popen):
+                exthandlers_handler.run()
+                exthandlers_handler.report_ext_handlers_status()
+
+                self.assertEqual(expected_call_count, len(extension_calls), "Call counts dont match")
+
+        with patch('time.sleep', side_effect=lambda _: mock_sleep(0.001)):
+            def http_get_handler(url, *_, **kwargs):
+                if self.is_in_vm_artifacts_profile_request(url) or self.is_host_plugin_in_vm_artifacts_profile_request(url, kwargs):
+                    return mock_in_vm_artifacts_profile_response
+                return None
+
+            mock_in_vm_artifacts_profile_response = MockHttpResponse(200, body='{ "onHold": false }'.encode('utf-8'))
+
+            with mock_wire_protocol(mockwiredata.DATA_FILE_IN_VM_ARTIFACTS_PROFILE, http_get_handler=http_get_handler) as protocol:
+                protocol.report_vm_status = MagicMock()
+                exthandlers_handler = get_exthandlers_handler(protocol)
+
+                # Extension called once for Install and once for Enable
+                assert_extensions_called(exthandlers_handler, expected_call_count=2)
+                self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.0.0")
+
+                # Update GoalState
+                protocol.mock_wire_data.set_incarnation(2)
+                protocol.update_goal_state()
+
+                with patch.object(conf, 'get_extensions_enabled', return_value=False):
+                    assert_extensions_called(exthandlers_handler, expected_call_count=0)
+
+                # Disabled over-provisioning in configuration
+                # In this case we should process GoalState as incarnation changed
+                with patch.object(conf, 'get_extensions_enabled', return_value=True):
+                    with patch.object(conf, 'get_enable_overprovisioning', return_value=False):
+                        # 1 expected call count for Enable command
+                        assert_extensions_called(exthandlers_handler, expected_call_count=1)
+
+                # Enabled on_hold property in artifact_blob
+                mock_in_vm_artifacts_profile_response = MockHttpResponse(200, body='{ "onHold": true }'.encode('utf-8'))
+                protocol.client.update_goal_state(force_update=True)
+
+                with patch.object(conf, 'get_extensions_enabled', return_value=True):
+                    with patch.object(conf, "get_enable_overprovisioning", return_value=True):
+                        assert_extensions_called(exthandlers_handler, expected_call_count=0)
+
+                # Disabled on_hold property in artifact_blob
+                mock_in_vm_artifacts_profile_response = MockHttpResponse(200, body='{ "onHold": false }'.encode('utf-8'))
+                protocol.client.update_goal_state(force_update=True)
+
+                with patch.object(conf, 'get_extensions_enabled', return_value=True):
+                    with patch.object(conf, "get_enable_overprovisioning", return_value=True):
+                        # 1 expected call count for Enable command
+                        assert_extensions_called(exthandlers_handler, expected_call_count=1)
+
+    def test_it_should_process_extensions_appropriately_on_artifact_hold(self):
+        with patch('time.sleep', side_effect=lambda _: mock_sleep(0.001)):
+            with patch("azurelinuxagent.common.conf.get_enable_overprovisioning", return_value=True):
+                with mock_wire_protocol(mockwiredata.DATA_FILE_IN_VM_ARTIFACTS_PROFILE) as protocol:
+                    protocol.report_vm_status = MagicMock()
+                    exthandlers_handler = get_exthandlers_handler(protocol)
+                    #
+                    # The test data sets onHold to True; extensions should not be processed
+                    #
+                    exthandlers_handler.run()
+                    exthandlers_handler.report_ext_handlers_status()
+
+                    vm_agent_status = protocol.report_vm_status.call_args[0][0].vmAgent
+                    self.assertEqual(vm_agent_status.status, "Ready", "Agent should report ready")
+                    self.assertEqual(0, len(vm_agent_status.extensionHandlers), "No extensions should be reported as on_hold is True")
+                    self.assertIsNone(vm_agent_status.vm_artifacts_aggregate_status.goal_state_aggregate_status, "No GS Aggregate status should be reported")
+
+                    #
+                    # Now force onHold to False; extensions should be processed
+                    #
+                    def http_get_handler(url, *_, **kwargs):
+                        if self.is_in_vm_artifacts_profile_request(url) or self.is_host_plugin_in_vm_artifacts_profile_request(url, kwargs):
+                            return MockHttpResponse(200, body='{ "onHold": false }'.encode('utf-8'))
+                        return None
+                    protocol.set_http_handlers(http_get_handler=http_get_handler)
+
+                    protocol.client.update_goal_state(force_update=True)
+
+                    exthandlers_handler.run()
+                    exthandlers_handler.report_ext_handlers_status()
+
+                    self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.0.0")
+                    self.assertEqual("1", protocol.report_vm_status.call_args[0][0].vmAgent.vm_artifacts_aggregate_status.goal_state_aggregate_status.in_svd_seq_no, "Incarnation mismatch")
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/tests/ga/test_exthandlers.py b/tests/ga/test_exthandlers.py
index c4bb2159..14b376bb 100644
--- a/tests/ga/test_exthandlers.py
+++ b/tests/ga/test_exthandlers.py
@@ -251,7 +251,7 @@ class TestExtHandlers(AgentTestCase):
 
     def test_it_should_report_error_if_plugin_settings_version_mismatch(self):
         with mock_wire_protocol(mockwiredata.DATA_FILE_PLUGIN_SETTINGS_MISMATCH) as protocol:
-            with patch("azurelinuxagent.common.protocol.extensions_goal_state.add_event") as mock_add_event:
+            with patch("azurelinuxagent.common.protocol.extensions_goal_state_from_extensions_config.add_event") as mock_add_event:
                 # Forcing update of GoalState to allow the ExtConfig to report an event
                 protocol.mock_wire_data.set_incarnation(2)
                 protocol.client.update_goal_state()
diff --git a/tests/ga/test_update.py b/tests/ga/test_update.py
index 7e43b55b..f07ad428 100644
--- a/tests/ga/test_update.py
+++ b/tests/ga/test_update.py
@@ -28,7 +28,6 @@ from azurelinuxagent.common.event import EVENTS_DIRECTORY, WALAEventOperation
 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.extensions_goal_state import ExtensionsGoalState
 from azurelinuxagent.common.protocol.hostplugin import URI_FORMAT_GET_API_VERSIONS, HOST_PLUGIN_PORT, \
     URI_FORMAT_GET_EXTENSION_ARTIFACT, HostPluginProtocol
 from azurelinuxagent.common.protocol.restapi import ExtHandlerPackageUri, VMAgentManifest, VMAgentManifestUri, \
@@ -47,7 +46,7 @@ from azurelinuxagent.ga.update import GuestAgent, GuestAgentError, MAX_FAILURE,
     READONLY_FILE_GLOBS, ExtensionsSummary, AgentUpgradeType
 from tests.protocol.mocks import mock_wire_protocol
 from tests.protocol.mockwiredata import DATA_FILE, DATA_FILE_MULTIPLE_EXT, is_ga_manifest_request
-from tests.tools import AgentTestCase, data_dir, DEFAULT, patch, load_bin_data, load_data, Mock, MagicMock, \
+from tests.tools import AgentTestCase, data_dir, DEFAULT, patch, load_bin_data, Mock, MagicMock, \
     clear_singleton_instances, mock_sleep, skip_if_predicate_true
 from tests.protocol import mockwiredata
 from tests.protocol.mocks import HttpRequestPredicates
@@ -1480,15 +1479,15 @@ class TestUpdate(UpdateTestCase):
         self.assertTrue(self._test_upgrade_available())
 
     def test_upgrade_available_handles_missing_family(self):
-        extensions_goal_state = ExtensionsGoalState.create_from_extensions_config(123, load_data("wire/ext_conf_missing_family.xml"))
-        protocol = ProtocolMock()
-        protocol.family = "Prod"
-        protocol.agent_manifests = extensions_goal_state.vmagent_manifests  # pylint: disable=attribute-defined-outside-init
-        self.update_handler.protocol_util = protocol
-        with patch('azurelinuxagent.common.logger.warn') as mock_logger:
-            with patch('tests.ga.test_update.ProtocolMock.get_vmagent_pkgs', side_effect=ProtocolError):
-                self.assertFalse(self.update_handler._check_and_download_agent_if_upgrade_available(protocol, base_version=CURRENT_VERSION))
-                self.assertEqual(0, mock_logger.call_count)
+        data_file = mockwiredata.DATA_FILE.copy()
+        data_file["ext_conf"] = "wire/ext_conf_missing_family.xml"
+
+        with mock_wire_protocol(data_file) as protocol:
+            self.update_handler.protocol_util = protocol
+            with patch('azurelinuxagent.common.logger.warn') as mock_logger:
+                with patch('tests.ga.test_update.ProtocolMock.get_vmagent_pkgs', side_effect=ProtocolError):
+                    self.assertFalse(self.update_handler._check_and_download_agent_if_upgrade_available(protocol, base_version=CURRENT_VERSION))
+                    self.assertEqual(0, mock_logger.call_count)
 
     def test_upgrade_available_includes_old_agents(self):
         self.prepare_agents()
diff --git a/tests/protocol/test_extensions_goal_state.py b/tests/protocol/test_extensions_goal_state.py
index 59c6d9bf..7b998de0 100644
--- a/tests/protocol/test_extensions_goal_state.py
+++ b/tests/protocol/test_extensions_goal_state.py
@@ -1,93 +1,43 @@
 # Copyright (c) Microsoft Corporation. All rights reserved.
 # Licensed under the Apache License.
-import json
-import os.path
 import re
 
-from azurelinuxagent.common.protocol.extensions_goal_state import ExtensionsGoalState, GoalStateMismatchError, _CaseFoldedDict
-from azurelinuxagent.common.utils import fileutil
-from tests.protocol.mocks import HttpRequestPredicates
-from tests.tools import AgentTestCase, data_dir
+from azurelinuxagent.common.protocol.extensions_goal_state import ExtensionsGoalState, GoalStateMismatchError
+from tests.protocol.mocks import mockwiredata, mock_wire_protocol
+from tests.tools import AgentTestCase, PropertyMock, patch
 
 
-class ExtensionsGoalStateTestCase(HttpRequestPredicates, AgentTestCase):
+class ExtensionsGoalStateTestCase(AgentTestCase):
     def test_compare_should_report_mismatches_between_extensions_config_and_vm_settings(self):
-        from_vm_settings = ExtensionsGoalState.create_from_vm_settings("123", fileutil.read_file(os.path.join(data_dir, "hostgaplugin/vm_settings.json")))
-        from_extensions_config = ExtensionsGoalState.create_from_extensions_config("123", fileutil.read_file(os.path.join(data_dir, "hostgaplugin/ext_conf.xml")))
-
-        from_vm_settings._required_features = ['FORCE_A_MISMATCH_FEATURE']
-        with self.assertRaisesRegexCM(GoalStateMismatchError, "MultipleExtensionsPerHandler.*!=.*FORCE_A_MISMATCH_FEATURE"):
-            ExtensionsGoalState.compare(from_extensions_config, from_vm_settings)
+        with mock_wire_protocol(mockwiredata.DATA_FILE_VM_SETTINGS) as protocol:
+            from_extensions_config = protocol.client.get_extensions_goal_state()
+            from_vm_settings = protocol.client._extensions_goal_state_from_vm_settings
+
+            def test_property(name, value):
+                full_name = "azurelinuxagent.common.protocol.extensions_goal_state_from_vm_settings.ExtensionsGoalStateFromVmSettings.{0}".format(name)
+                with patch(full_name, new_callable=PropertyMock) as property_mock:
+                    property_mock.return_value = value
+                    with self.assertRaisesRegexCM(GoalStateMismatchError, r"Attribute: {0}.*{1}".format(name, value), re.DOTALL):
+                        ExtensionsGoalState.compare(from_extensions_config, from_vm_settings)
+
+            test_property("activity_id",             'MOCK_ACTIVITY_ID')
+            test_property("correlation_id",          'MOCK_CORRELATION_ID')
+            test_property("created_on_timestamp",    'MOCK_TIMESTAMP')
+            test_property("status_upload_blob",      'MOCK_UPLOAD_BLOB')
+            test_property("status_upload_blob_type", 'MOCK_UPLOAD_BLOB_TYPE')
+            test_property("required_features",       ['MOCK_REQUIRED_FEATURE'])
+            test_property("on_hold",                 True)
 
     def test_create_from_extensions_config_should_assume_block_when_blob_type_is_not_valid(self):
-        extensions_config_text = fileutil.read_file(os.path.join(data_dir, "hostgaplugin/ext_conf.xml"))
-        extensions_config_text = re.sub(r'statusBlobType.*=.*"BlockBlob"', 'statusBlobType="INVALID_BLOB_TYPE"', extensions_config_text)
-        if "INVALID_BLOB_TYPE" not in extensions_config_text:
-            raise Exception("Failed to inject an invalid blob type in the test data")
-        extensions_config = ExtensionsGoalState.create_from_extensions_config("123", extensions_config_text)
-
-        actual = extensions_config.get_status_upload_blob_type()
-        self.assertEqual("BlockBlob", actual, 'Expected BlockBob for an invalid statusBlobType')
+        data_file = mockwiredata.DATA_FILE.copy()
+        data_file["ext_conf"] = "hostgaplugin/ext_conf-invalid_blob_type.xml"
+        with mock_wire_protocol(data_file) as protocol:
+            actual = protocol.client.get_extensions_goal_state().status_upload_blob_type
+            self.assertEqual("BlockBlob", actual, 'Expected BlockBob for an invalid statusBlobType')
 
     def test_create_from_vm_settings_should_assume_block_when_blob_type_is_not_valid(self):
-        vm_settings_text = fileutil.read_file(os.path.join(data_dir, "hostgaplugin/vm_settings.json"))
-        vm_settings_text =  re.sub(r'"statusBlobType".*:.*"BlockBlob"', '"statusBlobType": "INVALID_BLOB_TYPE"', vm_settings_text)
-        if "INVALID_BLOB_TYPE" not in vm_settings_text:
-            raise Exception("Failed to inject an invalid blob type in the test data")
-        vm_settings = ExtensionsGoalState.create_from_vm_settings("123", vm_settings_text)
-
-        actual = vm_settings.get_status_upload_blob_type()
-        self.assertEqual("BlockBlob", actual, 'Expected BlockBob for an invalid statusBlobType')
-
-    def test_create_from_vm_settings_should_parse_vm_settiings(self):
-        vm_settings_text = fileutil.read_file(os.path.join(data_dir, "hostgaplugin/vm_settings.json"))
-        vm_settings = ExtensionsGoalState.create_from_vm_settings("123", vm_settings_text)
-
-        # TODO: HostGAPlugin 112 does not include the status blob; see TODO in ExtensionsGoalState
-        # self.assertEqual("https://dcrcqabsr1.blob.core.windows.net/$system/edpxmal5j1.058b176d-445b-4e75-bd97-4911511b7d96.status?sv=2018-03-28&sr=b&sk=system-1&sig=U4KaLxlyYfgQ%2fie8RCwgMBSXa3E4vlW0ozPYOEHikoc%3d&se=9999-01-01T00%3a00%3a00Z&sp=w", vm_settings.get_status_upload_blob(), 'statusUploadBlob.value was not parsed correctly')
-        # self.assertEqual("BlockBlob", vm_settings.get_status_upload_blob_type(), 'statusBlobType was not parsed correctly')
-        self.assertEqual(["MultipleExtensionsPerHandler"], vm_settings.get_required_features(), 'requiredFeatures was not parsed correctly')
-
-
-class CaseFoldedDictionaryTestCase(AgentTestCase):
-    def test_it_should_retrieve_items_ignoring_case(self):
-        dictionary = json.loads('''{
-            "activityId": "2e7f8b5d-f637-4721-b757-cb190d49b4e9",
-            "StatusUploadBlob": {
-                "statusBlobType": "BlockBlob",
-                "value": "https://dcrcqabsr1.blob.core.windows.net/$system/edpxmal5j1.058b176d-445b-4e75-bd97-4911511b7d96.status"
-            },
-            "gaFamilies": [
-                {
-                    "Name": "Prod",
-                    "Version": "2.5.0.2",
-                    "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"
-                    ]
-                }
-            ]
-         }''')
-
-        case_folded = _CaseFoldedDict.from_dict(dictionary)
-
-        def test_retrieve_item(key, expected_value):
-            """
-            Test for operators [] and in, and methods get() and has_key()
-            """
-            try:
-                self.assertEqual(expected_value, case_folded[key], "Operator [] retrieved incorrect value for '{0}'".format(key))
-            except KeyError:
-                self.fail("Operator [] failed to retrieve '{0}'".format(key))
-
-            self.assertTrue(case_folded.has_key(key), "Method has_key() did not find '{0}'".format(key))
-
-            self.assertEqual(expected_value, case_folded.get(key), "Method get() retrieved incorrect value for '{0}'".format(key))
-            self.assertTrue(key in case_folded, "Operator in did not find key '{0}'".format(key))
-
-        test_retrieve_item("activityId", "2e7f8b5d-f637-4721-b757-cb190d49b4e9")
-        test_retrieve_item("activityid", "2e7f8b5d-f637-4721-b757-cb190d49b4e9")
-        test_retrieve_item("ACTIVITYID", "2e7f8b5d-f637-4721-b757-cb190d49b4e9")
-
-        self.assertEqual("BlockBlob", case_folded["statusuploadblob"]["statusblobtype"], "Failed to retrieve item in nested dictionary")
-        self.assertEqual("Prod", case_folded["gafamilies"][0]["name"], "Failed to retrieve item in nested array")
+        data_file = mockwiredata.DATA_FILE.copy()
+        data_file["vm_settings"] = "hostgaplugin/vm_settings-invalid_blob_type.json"
+        with mock_wire_protocol(data_file) as protocol:
+            actual = protocol.client._extensions_goal_state_from_vm_settings.status_upload_blob_type
+            self.assertEqual("BlockBlob", actual, 'Expected BlockBob for an invalid statusBlobType')
diff --git a/tests/protocol/test_extensions_goal_state_from_extensions_config.py b/tests/protocol/test_extensions_goal_state_from_extensions_config.py
new file mode 100644
index 00000000..b792edab
--- /dev/null
+++ b/tests/protocol/test_extensions_goal_state_from_extensions_config.py
@@ -0,0 +1,29 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the Apache License.
+from azurelinuxagent.common.AgentGlobals import AgentGlobals
+from tests.protocol.mocks import mockwiredata, mock_wire_protocol
+from tests.tools import AgentTestCase
+
+
+class ExtensionsGoalStateFromExtensionsConfigTestCase(AgentTestCase):
+    def test_it_should_parse_in_vm_metadata(self):
+        with mock_wire_protocol(mockwiredata.DATA_FILE_IN_VM_META_DATA) as protocol:
+            extensions_goal_state = protocol.get_extensions_goal_state()
+            self.assertEqual("555e551c-600e-4fb4-90ba-8ab8ec28eccc", extensions_goal_state.activity_id, "Incorrect activity Id")
+            self.assertEqual("400de90b-522e-491f-9d89-ec944661f531", extensions_goal_state.correlation_id, "Incorrect correlation Id")
+            self.assertEqual('2020-11-09T17:48:50.412125Z', extensions_goal_state.created_on_timestamp, "Incorrect GS Creation time")
+
+    def test_it_should_use_default_values_when_in_vm_metadata_is_missing(self):
+        with mock_wire_protocol(mockwiredata.DATA_FILE) as protocol:
+            extensions_goal_state = protocol.get_extensions_goal_state()
+            self.assertEqual(AgentGlobals.GUID_ZERO, extensions_goal_state.activity_id, "Incorrect activity Id")
+            self.assertEqual(AgentGlobals.GUID_ZERO, extensions_goal_state.correlation_id, "Incorrect correlation Id")
+            self.assertEqual('1900-01-01T00:00:00.000000Z', extensions_goal_state.created_on_timestamp, "Incorrect GS Creation time")
+
+    def test_it_should_use_default_values_when_in_vm_metadata_is_invalid(self):
+        with mock_wire_protocol(mockwiredata.DATA_FILE_INVALID_VM_META_DATA) as protocol:
+            extensions_goal_state = protocol.get_extensions_goal_state()
+            self.assertEqual(AgentGlobals.GUID_ZERO, extensions_goal_state.activity_id, "Incorrect activity Id")
+            self.assertEqual(AgentGlobals.GUID_ZERO, extensions_goal_state.correlation_id, "Incorrect correlation Id")
+            self.assertEqual('1900-01-01T00:00:00.000000Z', extensions_goal_state.created_on_timestamp, "Incorrect GS Creation time")
+
diff --git a/tests/protocol/test_extensions_goal_state_from_vm_settings.py b/tests/protocol/test_extensions_goal_state_from_vm_settings.py
new file mode 100644
index 00000000..cd416da4
--- /dev/null
+++ b/tests/protocol/test_extensions_goal_state_from_vm_settings.py
@@ -0,0 +1,70 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the Apache License.
+import json
+import os.path
+
+from azurelinuxagent.common.protocol.extensions_goal_state_factory import ExtensionsGoalStateFactory
+from azurelinuxagent.common.protocol.extensions_goal_state_from_vm_settings import _CaseFoldedDict
+from azurelinuxagent.common.utils import fileutil
+from tests.tools import AgentTestCase, data_dir
+
+
+class ExtensionsGoalStateFromVmSettingsTestCase(AgentTestCase):
+    def test_create_from_vm_settings_should_parse_vm_settings(self):
+        vm_settings_text = fileutil.read_file(os.path.join(data_dir, "hostgaplugin/vm_settings.json"))
+        vm_settings = ExtensionsGoalStateFactory.create_from_vm_settings("123", vm_settings_text)
+
+        def assert_property(name, value):
+            self.assertEqual(value, getattr(vm_settings, name), '{0} was not parsed correctly'.format(name))
+
+        assert_property("activity_id", "2e7f8b5d-f637-4721-b757-cb190d49b4e9")
+        assert_property("correlation_id", "1bef4c48-044e-4225-8f42-1d1eac1eb158")
+        assert_property("created_on_timestamp", "2021-10-08T21:52:23.161652Z")
+        assert_property("status_upload_blob", "https://dcrcqabsr1.blob.core.windows.net/$system/edpxmal5j1.058b176d-445b-4e75-bd97-4911511b7d96.status?sv=2018-03-28&sr=b&sk=system-1&sig=U4KaLxlyYfgQ%2fie8RCwgMBSXa3E4vlW0ozPYOEHikoc%3d&se=9999-01-01T00%3a00%3a00Z&sp=w")
+        assert_property("status_upload_blob_type", "BlockBlob")
+        assert_property("required_features", ["MultipleExtensionsPerHandler"])
+        assert_property("on_hold", False)
+
+
+class CaseFoldedDictionaryTestCase(AgentTestCase):
+    def test_it_should_retrieve_items_ignoring_case(self):
+        dictionary = json.loads('''{
+            "activityId": "2e7f8b5d-f637-4721-b757-cb190d49b4e9",
+            "StatusUploadBlob": {
+                "statusBlobType": "BlockBlob",
+                "value": "https://dcrcqabsr1.blob.core.windows.net/$system/edpxmal5j1.058b176d-445b-4e75-bd97-4911511b7d96.status"
+            },
+            "gaFamilies": [
+                {
+                    "Name": "Prod",
+                    "Version": "2.5.0.2",
+                    "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"
+                    ]
+                }
+            ]
+         }''')
+
+        case_folded = _CaseFoldedDict.from_dict(dictionary)
+
+        def test_retrieve_item(key, expected_value):
+            """
+            Test for operators [] and in, and methods get() and has_key()
+            """
+            try:
+                self.assertEqual(expected_value, case_folded[key], "Operator [] retrieved incorrect value for '{0}'".format(key))
+            except KeyError:
+                self.fail("Operator [] failed to retrieve '{0}'".format(key))
+
+            self.assertTrue(case_folded.has_key(key), "Method has_key() did not find '{0}'".format(key))
+
+            self.assertEqual(expected_value, case_folded.get(key), "Method get() retrieved incorrect value for '{0}'".format(key))
+            self.assertTrue(key in case_folded, "Operator in did not find key '{0}'".format(key))
+
+        test_retrieve_item("activityId", "2e7f8b5d-f637-4721-b757-cb190d49b4e9")
+        test_retrieve_item("activityid", "2e7f8b5d-f637-4721-b757-cb190d49b4e9")
+        test_retrieve_item("ACTIVITYID", "2e7f8b5d-f637-4721-b757-cb190d49b4e9")
+
+        self.assertEqual("BlockBlob", case_folded["statusuploadblob"]["statusblobtype"], "Failed to retrieve item in nested dictionary")
+        self.assertEqual("Prod", case_folded["gafamilies"][0]["name"], "Failed to retrieve item in nested array")
diff --git a/tests/protocol/test_hostplugin.py b/tests/protocol/test_hostplugin.py
index b87693b3..05540fde 100644
--- a/tests/protocol/test_hostplugin.py
+++ b/tests/protocol/test_hostplugin.py
@@ -34,7 +34,7 @@ from azurelinuxagent.common.utils import restutil
 from azurelinuxagent.common.version import AGENT_VERSION, AGENT_NAME
 from tests.protocol.mocks import mock_wire_protocol, HttpRequestPredicates, MockHttpResponse
 from tests.protocol.mockwiredata import DATA_FILE, DATA_FILE_NO_EXT
-from tests.tools import AgentTestCase, PY_VERSION_MAJOR, Mock, patch
+from tests.tools import AgentTestCase, PY_VERSION_MAJOR, Mock, PropertyMock, patch
 
 
 hostplugin_status_url = "http://168.63.129.16:32526/status"
@@ -152,9 +152,9 @@ class TestHostPlugin(HttpRequestPredicates, AgentTestCase):
         with mock_wire_protocol(DATA_FILE_NO_EXT) as protocol:
             # These tests use mock wire data that don't have any extensions (extension config will be empty).
             # Populate the upload blob and set an initial empty status before returning the protocol.
-            extensions_goal_state = protocol.client.get_extensions_goal_state()
-            extensions_goal_state._status_upload_blob = sas_url
-            extensions_goal_state._status_upload_blob_type = page_blob_type
+            protocol.client._extensions_goal_state = Mock(wraps=protocol.client._extensions_goal_state)
+            type(protocol.client._extensions_goal_state).status_upload_blob = PropertyMock(return_value=sas_url)
+            type(protocol.client._extensions_goal_state).status_upload_blob_type = PropertyMock(return_value=page_blob_type)
 
             status = restapi.VMStatus(status="Ready", message="Guest Agent is running")
             protocol.client.status_blob.set_vm_status(status)
diff --git a/tests/protocol/test_wire.py b/tests/protocol/test_wire.py
index 20042140..fae7a7c0 100644
--- a/tests/protocol/test_wire.py
+++ b/tests/protocol/test_wire.py
@@ -30,14 +30,17 @@ from azurelinuxagent.common import conf
 from azurelinuxagent.common.agent_supported_feature import SupportedFeatureNames, get_supported_feature_by_name, \
     get_agent_supported_features_list_for_crp
 from azurelinuxagent.common.future import httpclient
+from azurelinuxagent.common.event import WALAEventOperation
 from azurelinuxagent.common.exception import ResourceGoneError, ProtocolError, \
     ExtensionDownloadError, HttpError
 from azurelinuxagent.common.protocol import hostplugin
-from azurelinuxagent.common.protocol.extensions_goal_state import ExtensionsGoalState, GoalStateMismatchError
+from azurelinuxagent.common.protocol.extensions_goal_state import GoalStateMismatchError
+from azurelinuxagent.common.protocol.extensions_goal_state_factory import ExtensionsGoalStateFactory
+from azurelinuxagent.common.protocol.extensions_goal_state_from_extensions_config import ExtensionsGoalStateFromExtensionsConfig
 from azurelinuxagent.common.protocol.hostplugin import HostPluginProtocol
 from azurelinuxagent.common.protocol.restapi import VMAgentManifestUri
 from azurelinuxagent.common.protocol.wire import WireProtocol, WireClient, \
-    InVMArtifactsProfile, StatusBlob, VMStatus, EXT_CONF_FILE_NAME, _ErrorReporter
+    StatusBlob, VMStatus, EXT_CONF_FILE_NAME, _ErrorReporter
 from azurelinuxagent.common.telemetryevent import GuestAgentExtensionEventsSchema, \
     TelemetryEventParam, TelemetryEvent
 from azurelinuxagent.common.utils import restutil, textutil
@@ -48,7 +51,7 @@ from tests.protocol import mockwiredata
 from tests.protocol.mocks import mock_wire_protocol, HttpRequestPredicates, MockHttpResponse
 from tests.protocol.mockwiredata import DATA_FILE_NO_EXT, DATA_FILE
 from tests.protocol.mockwiredata import WireProtocolData
-from tests.tools import Mock, patch, AgentTestCase
+from tests.tools import Mock, PropertyMock, patch, AgentTestCase
 
 data_with_bom = b'\xef\xbb\xbfhehe'
 testurl = 'http://foo'
@@ -71,15 +74,13 @@ def get_event(message, duration=30000, evt_type="", is_internal=False, is_succes
 
 
 @contextlib.contextmanager
-def create_mock_protocol(artifacts_profile_blob=None, status_upload_blob=None, status_upload_blob_type=None):
+def create_mock_protocol(status_upload_blob=None, status_upload_blob_type=None):
     with mock_wire_protocol(DATA_FILE_NO_EXT) as protocol:
         # These tests use mock wire data that dont have any extensions (extension config will be empty).
-        # Populate the upload blob and artifacts profile blob.
-        extensions_goal_state = ExtensionsGoalState()
-        extensions_goal_state.artifacts_profile_blob = artifacts_profile_blob
-        extensions_goal_state._status_upload_blob = status_upload_blob
-        extensions_goal_state._status_upload_blob_type = status_upload_blob_type
-        protocol.client._extensions_goal_state = extensions_goal_state
+        # Mock the upload blob and artifacts profile blob.
+        protocol.client._extensions_goal_state = Mock(wraps=protocol.client._extensions_goal_state)
+        type(protocol.client._extensions_goal_state).status_upload_blob = PropertyMock(return_value=status_upload_blob)
+        type(protocol.client._extensions_goal_state).status_upload_blob_type = PropertyMock(return_value=status_upload_blob_type)
 
         yield protocol
 
@@ -87,7 +88,7 @@ def create_mock_protocol(artifacts_profile_blob=None, status_upload_blob=None, s
 @patch("time.sleep")
 @patch("azurelinuxagent.common.protocol.wire.CryptUtil")
 @patch("azurelinuxagent.common.protocol.healthservice.HealthService._report")
-class TestWireProtocol(AgentTestCase):
+class TestWireProtocol(AgentTestCase, HttpRequestPredicates):
 
     def setUp(self):
         super(TestWireProtocol, self).setUp()
@@ -214,11 +215,13 @@ class TestWireProtocol(AgentTestCase):
 
     def test_status_blob_parsing(self, *args):  # pylint: disable=unused-argument
         with mock_wire_protocol(mockwiredata.DATA_FILE) as protocol:
-            self.assertEqual(protocol.client.get_extensions_goal_state().get_status_upload_blob(),
+            extensions_goal_state = protocol.client.get_extensions_goal_state()
+            self.assertIsInstance(extensions_goal_state, ExtensionsGoalStateFromExtensionsConfig)
+            self.assertEqual(extensions_goal_state.status_upload_blob,
                              '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')
-            self.assertEqual(protocol.client.get_extensions_goal_state().get_status_upload_blob_type(), u'BlockBlob')
+            self.assertEqual(protocol.client.get_extensions_goal_state().status_upload_blob_type, u'BlockBlob')
 
     def test_get_host_ga_plugin(self, *args):  # pylint: disable=unused-argument
         with mock_wire_protocol(mockwiredata.DATA_FILE) as protocol:
@@ -263,60 +266,49 @@ class TestWireProtocol(AgentTestCase):
                 self.assertEqual(1, mock_prepare.call_count)
 
     def test_get_in_vm_artifacts_profile_blob_not_available(self, *_):
-        # Test when artifacts_profile_blob is null/None
-        with mock_wire_protocol(DATA_FILE_NO_EXT) as protocol:
-            protocol.client._extensions_goal_state = ExtensionsGoalState()
-
-            self.assertEqual(None, protocol.client.get_artifacts_profile())
-
-        # Test when artifacts_profile_blob is whitespace
-        with create_mock_protocol(artifacts_profile_blob="  ") as protocol:
-            self.assertEqual(None, protocol.client.get_artifacts_profile())
-
-    def test_get_in_vm_artifacts_profile_response_body_not_valid(self, *_):
-        with create_mock_protocol(artifacts_profile_blob=testurl) as protocol:
-            with patch.object(HostPluginProtocol, "get_artifact_request", return_value=['dummy_url', {}]) as host_plugin_get_artifact_url_and_headers:
-                # Test when response body is None
-                protocol.client.call_storage_service = Mock(return_value=MockHttpResponse(200, body=None))
-                in_vm_artifacts_profile = protocol.client.get_artifacts_profile()
-                self.assertTrue(in_vm_artifacts_profile is None)
-
-                # Test when response body is None
-                protocol.client.call_storage_service = Mock(return_value=MockHttpResponse(200, '   '.encode('utf-8')))
-                in_vm_artifacts_profile = protocol.client.get_artifacts_profile()
-                self.assertTrue(in_vm_artifacts_profile is None)
-
-                # Test when response body is None
-                protocol.client.call_storage_service = Mock(return_value=MockHttpResponse(200, '{ }'.encode('utf-8')))
-                in_vm_artifacts_profile = protocol.client.get_artifacts_profile()
-                self.assertEqual(dict(), in_vm_artifacts_profile.__dict__,
-                                 'If artifacts_profile_blob has empty json dictionary, in_vm_artifacts_profile '
-                                 'should contain nothing')
-
-                host_plugin_get_artifact_url_and_headers.assert_called_with(testurl)
-
-    @patch("azurelinuxagent.common.event.add_event")
-    def test_artifacts_profile_json_parsing(self, patch_event, *args):  # pylint: disable=unused-argument
-        with create_mock_protocol(artifacts_profile_blob=testurl) as protocol:
-            # response is invalid json
-            protocol.client.call_storage_service = Mock(return_value=MockHttpResponse(200, "invalid json".encode('utf-8')))
-            in_vm_artifacts_profile = protocol.client.get_artifacts_profile()
-
-            # ensure response is empty
-            self.assertEqual(None, in_vm_artifacts_profile)
-
-            # ensure event is logged
-            self.assertEqual(1, patch_event.call_count)
-            self.assertFalse(patch_event.call_args[1]['is_success'])
-            self.assertTrue('invalid json' in patch_event.call_args[1]['message'])
-            self.assertEqual('ArtifactsProfileBlob', patch_event.call_args[1]['op'])
-
-    def test_get_in_vm_artifacts_profile_default(self, *args):  # pylint: disable=unused-argument
-        with create_mock_protocol(artifacts_profile_blob=testurl) as protocol:
-            protocol.client.call_storage_service = Mock(return_value=MockHttpResponse(200, '{"onHold": "true"}'.encode('utf-8')))
-            in_vm_artifacts_profile = protocol.client.get_artifacts_profile()
-            self.assertEqual(dict(onHold='true'), in_vm_artifacts_profile.__dict__)
-            self.assertTrue(in_vm_artifacts_profile.is_on_hold())
+        data_file = mockwiredata.DATA_FILE.copy()
+        data_file["ext_conf"] = "wire/ext_conf_in_vm_empty_artifacts_profile.xml"
+
+        with mock_wire_protocol(data_file) as protocol:
+            self.assertFalse(protocol.get_extensions_goal_state().on_hold)
+
+    def test_it_should_set_on_hold_to_false_when_the_in_vm_artifacts_profile_is_not_valid(self, *_):
+        with mock_wire_protocol(mockwiredata.DATA_FILE_IN_VM_ARTIFACTS_PROFILE) as protocol:
+            extensions_on_hold = protocol.get_extensions_goal_state().on_hold
+            self.assertTrue(extensions_on_hold, "Extensions should be on hold in the test data")
+
+            def http_get_handler(url, *_, **kwargs):
+                if self.is_in_vm_artifacts_profile_request(url) or self.is_host_plugin_in_vm_artifacts_profile_request(url, kwargs):
+                    return mock_response
+                return None
+            protocol.set_http_handlers(http_get_handler=http_get_handler)
+
+            mock_response = MockHttpResponse(200, body=None)
+            protocol.client.update_goal_state(force_update=True)
+            extensions_on_hold = protocol.get_extensions_goal_state().on_hold
+            self.assertFalse(extensions_on_hold, "Extensions should not be on hold when the in-vm artifacts profile response body is None")
+
+            mock_response = MockHttpResponse(200, '   '.encode('utf-8'))
+            protocol.client.update_goal_state(force_update=True)
+            extensions_on_hold = protocol.get_extensions_goal_state().on_hold
+            self.assertFalse(extensions_on_hold, "Extensions should not be on hold when the in-vm artifacts profile response is an empty string")
+
+            mock_response = MockHttpResponse(200, '{ }'.encode('utf-8'))
+            protocol.client.update_goal_state(force_update=True)
+            extensions_on_hold = protocol.get_extensions_goal_state().on_hold
+            self.assertFalse(extensions_on_hold, "Extensions should not be on hold when the in-vm artifacts profile response is an empty json object")
+
+            with patch("azurelinuxagent.common.protocol.extensions_goal_state_from_extensions_config.add_event") as add_event:
+                mock_response = MockHttpResponse(200, 'invalid json'.encode('utf-8'))
+                protocol.client.update_goal_state(force_update=True)
+
+                extensions_on_hold = protocol.get_extensions_goal_state().on_hold
+                self.assertFalse(extensions_on_hold, "Extensions should not be on hold when the in-vm artifacts profile response is not valid json")
+
+                events = [kwargs for _, kwargs in add_event.call_args_list if kwargs['op'] == WALAEventOperation.ArtifactsProfileBlob]
+                self.assertEqual(1, len(events), "Expected 1 event for operation ArtifactsProfileBlob. Got: {0}".format(events))
+                self.assertFalse(events[0]['is_success'], "Expected ArtifactsProfileBlob's success to be False")
+                self.assertTrue('invalid json' in events[0]['message'], "Expected 'invalid json' as the reason for the operation failure. Got: {0}".format(events[0]['message']))
 
     @patch("socket.gethostname", return_value="hostname")
     @patch("time.gmtime", return_value=time.localtime(1485543256))
@@ -495,12 +487,12 @@ class TestWireClient(HttpRequestPredicates, AgentTestCase):
             vmagent_manifests = [manifest.family for manifest in extensions_goal_state.vmagent_manifests.vmAgentManifests]
             self.assertEqual(0, len(extensions_goal_state.vmagent_manifests.vmAgentManifests),
                              "Unexpected number of vmagent manifests in the extension config: [{0}]".format(vmagent_manifests))
-            self.assertIsNone(extensions_goal_state.get_status_upload_blob(),
+            self.assertIsNone(extensions_goal_state.status_upload_blob,
                               "Status upload blob in the extension config is expected to be None")
-            self.assertIsNone(extensions_goal_state.get_status_upload_blob_type(),
+            self.assertIsNone(extensions_goal_state.status_upload_blob_type,
                               "Type of status upload blob in the extension config is expected to be None")
-            self.assertIsNone(extensions_goal_state.artifacts_profile_blob,
-                              "Artifacts profile blob in the extensions config is expected to be None")
+            self.assertFalse(extensions_goal_state.on_hold,
+                              "Extensions On Hold is expected to be False")
 
     def test_get_ext_conf_with_extensions_should_retrieve_ext_handlers_and_vmagent_manifests_info(self):
         # Basic test for get_extensions_goal_state() when extensions are present in the config. The test verifies that get_extensions_goal_state()
@@ -517,11 +509,11 @@ class TestWireClient(HttpRequestPredicates, AgentTestCase):
                              "Unexpected number of vmagent manifests in the extension config: [{0}]".format(vmagent_manifests))
             self.assertEqual("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",
-                             extensions_goal_state.get_status_upload_blob(), "Unexpected value for status upload blob URI")
-            self.assertEqual("BlockBlob", extensions_goal_state.get_status_upload_blob_type(),
+                             extensions_goal_state.status_upload_blob, "Unexpected value for status upload blob URI")
+            self.assertEqual("BlockBlob", extensions_goal_state.status_upload_blob_type,
                              "Unexpected status upload blob type in the extension config")
-            self.assertEqual(None, extensions_goal_state.artifacts_profile_blob,
-                             "Artifacts profile blob in the extension config should have been None")
+            self.assertFalse(extensions_goal_state.on_hold,
+                              "Extensions On Hold is expected to be False")
 
     def test_download_ext_handler_pkg_should_not_invoke_host_channel_when_direct_channel_succeeds(self):
         extension_url = 'https://fake_host/fake_extension.zip'
@@ -766,12 +758,12 @@ class TestWireClient(HttpRequestPredicates, AgentTestCase):
                 protocol.track_url(url)
             return None
 
-        with mock_wire_protocol(mockwiredata.DATA_FILE_IN_VM_ARTIFACTS_PROFILE, http_get_handler=http_get_handler) as protocol:
+        with mock_wire_protocol(mockwiredata.DATA_FILE_IN_VM_ARTIFACTS_PROFILE) as protocol:
+            protocol.set_http_handlers(http_get_handler=http_get_handler)
             HostPluginProtocol.is_default_channel = False
 
-            return_value = protocol.client.get_artifacts_profile()
+            protocol.client.update_goal_state(force_update=True)
 
-            self.assertIsInstance(return_value, InVMArtifactsProfile, 'The request did not return a valid artifacts profile: {0}'.format(return_value))
             urls = protocol.get_tracked_urls()
             self.assertEqual(len(urls), 1, "Unexpected HTTP requests: [{0}]".format(urls))
             self.assertFalse(HostPluginProtocol.is_default_channel, "The host should not have been set as the default channel")
@@ -785,16 +777,12 @@ class TestWireClient(HttpRequestPredicates, AgentTestCase):
             return None
 
         with mock_wire_protocol(mockwiredata.DATA_FILE_IN_VM_ARTIFACTS_PROFILE) as protocol:
-            HostPluginProtocol.is_default_channel = False
+            protocol.set_http_handlers(http_get_handler=http_get_handler)
 
+            HostPluginProtocol.is_default_channel = False
             try:
-                protocol.set_http_handlers(http_get_handler=http_get_handler)
+                protocol.client.update_goal_state(force_update=True)
 
-                return_value = protocol.client.get_artifacts_profile()
-
-                self.assertIsNotNone(return_value, "The artifacts profile request should have succeeded")
-                self.assertIsInstance(return_value, InVMArtifactsProfile, 'The request did not return a valid artifacts profile: {0}'.format(return_value))
-                self.assertTrue(return_value.onHold, 'The OnHold property should be True')  # pylint: disable=no-member
                 urls = protocol.get_tracked_urls()
                 self.assertEqual(len(urls), 2, "Invalid number of requests: [{0}]".format(urls))
                 self.assertTrue(self.is_in_vm_artifacts_profile_request(urls[0]), "The first request should have been over the direct channel")
@@ -812,7 +800,7 @@ class TestWireClient(HttpRequestPredicates, AgentTestCase):
                     http_get_handler.host_plugin_calls += 1
                     return ResourceGoneError("Exception to fake a stale goal state")
                 protocol.track_url(url)
-            if self.is_goal_state_request(url):
+            if self.is_goal_state_request(url) and http_get_handler.host_plugin_calls == 1:
                 protocol.track_url(url)
             return None
         http_get_handler.host_plugin_calls = 0
@@ -826,11 +814,8 @@ class TestWireClient(HttpRequestPredicates, AgentTestCase):
 
                 protocol.set_http_handlers(http_get_handler=http_get_handler)
 
-                return_value = protocol.client.get_artifacts_profile()
+                protocol.client.update_goal_state(force_update=True)
 
-                self.assertIsNotNone(return_value, "The artifacts profile request should have succeeded")
-                self.assertIsInstance(return_value, InVMArtifactsProfile, 'The request did not return a valid artifacts profile: {0}'.format(return_value))
-                self.assertTrue(return_value.onHold, 'The OnHold property should be True')  # pylint: disable=no-member
                 urls = protocol.get_tracked_urls()
                 self.assertEqual(len(urls), 4, "Invalid number of requests: [{0}]".format(urls))
                 self.assertTrue(self.is_in_vm_artifacts_profile_request(urls[0]), "The first request should have been over the direct channel")
@@ -846,10 +831,12 @@ class TestWireClient(HttpRequestPredicates, AgentTestCase):
             if self.is_in_vm_artifacts_profile_request(url):
                 return HttpError("Exception to fake an error on the direct channel")
             if self.is_host_plugin_in_vm_artifacts_profile_request(url, kwargs):
+                http_get_handler.host_plugin_calls += 1
                 return ResourceGoneError("Exception to fake a stale goal state")
-            if self.is_goal_state_request(url):
+            if self.is_goal_state_request(url) and http_get_handler.host_plugin_calls == 1:
                 protocol.track_url(url)
             return None
+        http_get_handler.host_plugin_calls = 0
 
         with mock_wire_protocol(mockwiredata.DATA_FILE_IN_VM_ARTIFACTS_PROFILE) as protocol:
             HostPluginProtocol.is_default_channel = False
@@ -859,9 +846,8 @@ class TestWireClient(HttpRequestPredicates, AgentTestCase):
 
             protocol.set_http_handlers(http_get_handler=http_get_handler)
 
-            return_value = protocol.client.get_artifacts_profile()
+            protocol.client.update_goal_state(force_update=True)
 
-            self.assertIsNone(return_value, "The artifacts profile request should have failed")
             urls = protocol.get_tracked_urls()
             self.assertEqual(len(urls), 4, "Invalid number of requests: [{0}]".format(urls))
             self.assertTrue(self.is_in_vm_artifacts_profile_request(urls[0]), "The first request should have been over the direct channel")
@@ -1216,7 +1202,7 @@ class UpdateGoalStateTestCase(HttpRequestPredicates, AgentTestCase):
                 self.assertTrue(os.path.exists(f), "{0} was not saved".format(f))
 
             with open(extensions_config_file, "r") as file_:
-                extensions_goal_state = ExtensionsGoalState.create_from_extensions_config(123, file_.read())
+                extensions_goal_state = ExtensionsGoalStateFactory.create_from_extensions_config(123, file_.read(), protocol)
             self.assertEqual(4, len(extensions_goal_state.ext_handlers.extHandlers), "Expected 4 extensions in the test ExtensionsConfig")
             for e in extensions_goal_state.ext_handlers.extHandlers:
                 self.assertEqual(e.properties.extensions[0].protectedSettings, "*** REDACTED ***", "The protected settings for {0} were not redacted".format(e.name))
diff --git a/tests/tools.py b/tests/tools.py
index fba2a98b..5df9ae6d 100644
--- a/tests/tools.py
+++ b/tests/tools.py
@@ -239,10 +239,11 @@ class AgentTestCase(unittest.TestCase):
         self.fail(msg)
 
     class _AssertRaisesContextManager(object):
-        def __init__(self, expected_exception_type, test_case, match_regex=None):
+        def __init__(self, expected_exception_type, test_case, match_regex=None, regex_flags=0):
             self._expected_exception_type = expected_exception_type
             self._test_case = test_case
             self._match_regex = match_regex
+            self._regex_flags = regex_flags
             self.exception = None
 
         def __enter__(self):
@@ -262,7 +263,7 @@ class AgentTestCase(unittest.TestCase):
                 self._test_case.fail("Raised '{0}', but expected '{1}'".format(raised, expected))
             if self._match_regex is not None:
                 exception_text = str(exception)
-                if re.search(self._match_regex, exception_text) is None:
+                if re.search(self._match_regex, exception_text, flags=self._regex_flags) is None:
                     self._test_case.fail("The exception did not match the expected pattern. Expected: r'{0}' Got: '{1}'".format(self._match_regex, exception_text))
             self.exception = exception
             return True
@@ -287,11 +288,11 @@ class AgentTestCase(unittest.TestCase):
                     exception_type, regex, str(e)))
         self.fail("No exception was thrown.  Expected exception {0} matching {1}".format(exception_type, regex))
 
-    def assertRaisesRegexCM(self, exception_type, regex):
+    def assertRaisesRegexCM(self, exception_type, regex, flags=0):
         """
         Similar to assertRaisesRegex, but returns a context manager (mostly needed for Python 2.*, which does not have a assertRaisesRegex)
         """
-        return AgentTestCase._AssertRaisesContextManager(exception_type, self, match_regex=regex)
+        return AgentTestCase._AssertRaisesContextManager(exception_type, self, match_regex=regex, regex_flags=flags)
 
     def emulate_assertDictEqual(self, first, second, msg=None):
         def fail(message):