Repository: Azure/WALinuxAgent
Pull request #2369 (2021-09-29 23:27:34)

Refactor ExtensionsConfig into ExtensionsGoalState
* Moved the instance of ExtensionsConfig in GoalState into the ExtensionsGoalState
* Added a placeholder function ExtensionsGoalState.from_vm_settings to parse the vmSettings into an ExtensionsGoalState
* Added skeleton code in WireClient.update_goal_state to show how those instances will be used

codecov[bot]: # [Codecov](https://codecov.io/gh/Azure/WALinuxAgent/pull/2369?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azure) Report
> Merging [#2369](https://codecov.io/gh/Azure/WALinuxAgent/pull/2369?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azure) (acaf5ab) into [develop](https://codecov.io/gh/Azure/WALinuxAgent/commit/f0a948ce53be34da11328be0fbf2182bcad2c1fa?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azure) (f0a948c) will **increase** coverage by `0.02%`.
> The diff coverage is `89.31%`.

> :exclamation: Current head acaf5ab differs from pull request most recent head afbaf43. Consider uploading reports for the commit afbaf43 to get more accurate results
[![Impacted file tree graph](https://codecov.io/gh/Azure/WALinuxAgent/pull/2369/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/2369?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azure)

```diff
@@             Coverage Diff             @@
##           develop    #2369      +/-   ##
===========================================
+ Coverage    70.86%   70.88%   +0.02%     
===========================================
  Files           97       97              
  Lines        14100    14105       +5     
  Branches      2031     2029       -2     
===========================================
+ Hits          9992     9999       +7     
+ Misses        3659     3657       -2     
  Partials       449      449              
```


| [Impacted Files](https://codecov.io/gh/Azure/WALinuxAgent/pull/2369?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azure) | Coverage Δ | |
|---|---|---|
| [azurelinuxagent/common/protocol/wire.py](https://codecov.io/gh/Azure/WALinuxAgent/pull/2369/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azure#diff-YXp1cmVsaW51eGFnZW50L2NvbW1vbi9wcm90b2NvbC93aXJlLnB5) | `78.59% <87.50%> (+0.53%)` | :arrow_up: |
| [...inuxagent/common/protocol/extensions\_goal\_state.py](https://codecov.io/gh/Azure/WALinuxAgent/pull/2369/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azure#diff-YXp1cmVsaW51eGFnZW50L2NvbW1vbi9wcm90b2NvbC9leHRlbnNpb25zX2dvYWxfc3RhdGUucHk=) | `89.52% <89.47%> (-10.48%)` | :arrow_down: |
| [azurelinuxagent/common/protocol/goal\_state.py](https://codecov.io/gh/Azure/WALinuxAgent/pull/2369/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azure#diff-YXp1cmVsaW51eGFnZW50L2NvbW1vbi9wcm90b2NvbC9nb2FsX3N0YXRlLnB5) | `93.25% <100.00%> (+1.77%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/Azure/WALinuxAgent/pull/2369?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/2369?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azure). Last update [f0a948c...afbaf43](https://codecov.io/gh/Azure/WALinuxAgent/pull/2369?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/protocol/extensions_goal_state.py b/azurelinuxagent/common/protocol/extensions_goal_state.py
index 049cf00f..2d0a940f 100644
--- a/azurelinuxagent/common/protocol/extensions_goal_state.py
+++ b/azurelinuxagent/common/protocol/extensions_goal_state.py
@@ -16,14 +16,432 @@
 #
 # Requires Python 2.6+ and Openssl 1.0+
 
-import re
+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 ExtensionConfigError
+from azurelinuxagent.common.future import ustr
+from azurelinuxagent.common.protocol.restapi import Extension, ExtHandler, ExtHandlerList, ExtHandlerVersionUri, VMAgentManifest, \
+    VMAgentManifestList, VMAgentManifestUri, InVMGoalStateMetaData, RequiredFeature, ExtensionState
+from azurelinuxagent.common.utils.textutil import parse_doc, findall, find, findtext, getattrib, gettext
 
 
 class ExtensionsGoalState(object):
-    def __init__(self, etag, vm_settings):
-        self.etag = etag
-        self.vm_settings = vm_settings
+    def __init__(self):
+        self.xml_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 from_extensions_config(xml_text):
+        extensions_goal_state = ExtensionsGoalState()
+        extensions_goal_state.xml_text = xml_text
+
+        xml_doc = parse_doc(extensions_goal_state.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)
+            extensions_goal_state.vmagent_manifests.vmAgentManifests.append(manifest)
+
+        extensions_goal_state.__parse_plugins_and_settings_and_populate_ext_handlers(xml_doc)
+
+        required_features_list = find(xml_doc, "RequiredFeatures")
+        if required_features_list is not None:
+            extensions_goal_state._parse_required_features(required_features_list)
+
+        extensions_goal_state.status_upload_blob = findtext(xml_doc, "StatusUploadBlob")
+        extensions_goal_state.artifacts_profile_blob = findtext(xml_doc, "InVMArtifactsProfileBlob")
+
+        status_upload_node = find(xml_doc, "StatusUploadBlob")
+        extensions_goal_state.status_upload_blob_type = getattrib(status_upload_node, "statusBlobType")
+        logger.verbose("Extension config shows status blob type as [{0}]", extensions_goal_state.status_upload_blob_type)
+
+        extensions_goal_state.in_vm_gs_metadata.parse_node(find(xml_doc, "InVMGoalStateMetaData"))
+
+        return extensions_goal_state
+
+    @staticmethod
+    def from_vm_settings(json_text):
+        raise NotImplementedError()  # TODO: Implement parsing of vmSettings
+
+    def _parse_required_features(self, required_features_list):
+        for required_feature in findall(required_features_list, "RequiredFeature"):
+            feature_name = findtext(required_feature, "Name")
+            feature_value = findtext(required_feature, "Value")
+            self.required_features.append(RequiredFeature(name=feature_name, value=feature_value))
+
+    def get_redacted_xml_text(self):
+        if self.xml_text is None:
+            return "<None/>"
+        xml_text = self.xml_text
+        for ext_handler in self.ext_handlers.extHandlers:
+            for extension in ext_handler.properties.extensions:
+                if extension.protectedSettings is not None:
+                    xml_text = xml_text.replace(extension.protectedSettings, "*** REDACTED ***")
+        return xml_text
+
+    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:
+                ExtensionsGoalState._parse_plugin(ext_handler, plugin)
+                ExtensionsGoalState._parse_plugin_settings(ext_handler, plugin_settings)
+            except ExtensionConfigError 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 ExtensionConfigError("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 ExtensionConfigError(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 ExtensionConfigError(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 ExtensionConfigError(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 ExtensionConfigError(msg)
+            # Only Runtime settings available, parse that
+            ExtensionsGoalState.__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
+            ExtensionsGoalState.__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 ExtensionConfigError(msg)
+        depends_on_node = depends_on_nodes[0] if depends_on_nodes else None
+        depends_on_level = ExtensionsGoalState.__get_dependency_level_from_node(depends_on_node, handler_name)
+        ExtensionsGoalState.__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 ExtensionConfigError("No Name not specified for DependsOn object in ExtensionRuntimeSettings for MultiConfig!")
+
+            dependency_level = ExtensionsGoalState.__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 ExtensionConfigError("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
+            ExtensionsGoalState.__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 ExtensionConfigError("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
 
-    def get_redacted_vm_settings(self):
-        return re.sub(r'("protectedSettings"\s*:\s*)"[^"]+"', r'\1"*** REDACTED ***"', self.vm_settings)
+        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)
 
diff --git a/azurelinuxagent/common/protocol/goal_state.py b/azurelinuxagent/common/protocol/goal_state.py
index 78bbf2b6..196ba134 100644
--- a/azurelinuxagent/common/protocol/goal_state.py
+++ b/azurelinuxagent/common/protocol/goal_state.py
@@ -16,27 +16,21 @@
 #
 # Requires Python 2.6+ and Openssl 1.0+
 
-import json
 import os
 import re
 import time
-from collections import defaultdict
 
 import azurelinuxagent.common.conf as conf
 import azurelinuxagent.common.logger as logger
 from azurelinuxagent.common.AgentGlobals import AgentGlobals
 from azurelinuxagent.common.datacontract import set_properties
-from azurelinuxagent.common.event import add_event, WALAEventOperation
 from azurelinuxagent.common.exception import IncompleteGoalStateError
-from azurelinuxagent.common.exception import ProtocolError, ExtensionConfigError
+from azurelinuxagent.common.exception import ProtocolError
 from azurelinuxagent.common.future import ustr
-from azurelinuxagent.common.protocol.restapi import Cert, CertList, Extension, ExtHandler, ExtHandlerList, \
-    ExtHandlerVersionUri, RemoteAccessUser, RemoteAccessUsersList, VMAgentManifest, VMAgentManifestList, \
-    VMAgentManifestUri, InVMGoalStateMetaData, RequiredFeature, ExtensionState
+from azurelinuxagent.common.protocol.restapi import Cert, CertList, RemoteAccessUser, RemoteAccessUsersList
 from azurelinuxagent.common.utils import fileutil
 from azurelinuxagent.common.utils.cryptutil import CryptUtil
-from azurelinuxagent.common.utils.textutil import parse_doc, findall, find, findtext, getattrib, gettext
-from azurelinuxagent.common.version import AGENT_NAME
+from azurelinuxagent.common.utils.textutil import parse_doc, findall, find, findtext, getattrib
 
 GOAL_STATE_URI = "http://{0}/machine/?comp=goalstate"
 CERTS_FILE_NAME = "Certificates.xml"
@@ -86,8 +80,7 @@ class GoalState(object):
             self.shared_conf = None
             self._certs_uri = findtext(xml_doc, "Certificates")
             self.certs = None
-            self._ext_conf_uri = findtext(xml_doc, "ExtensionsConfig")
-            self.ext_conf = None
+            self.extensions_config_uri = findtext(xml_doc, "ExtensionsConfig")
             self._remote_access_uri = findtext(container, "RemoteAccessInfo")
             self.remote_access = None
 
@@ -109,12 +102,6 @@ class GoalState(object):
                 xml_text = wire_client.fetch_config(self._certs_uri, wire_client.get_header_for_cert())
                 self.certs = Certificates(xml_text)
 
-            if self._ext_conf_uri is None:
-                self.ext_conf = ExtensionsConfig(None)
-            else:
-                xml_text = wire_client.fetch_config(self._ext_conf_uri, wire_client.get_header())
-                self.ext_conf = ExtensionsConfig(xml_text)
-
             if self._remote_access_uri is not None:
                 xml_text = wire_client.fetch_config(self._remote_access_uri, wire_client.get_header_for_cert())
                 self.remote_access = RemoteAccess(xml_text)
@@ -249,415 +236,6 @@ class Certificates(object):
         return file_name
 
 
-class ExtensionsConfig(object):
-    def __init__(self, xml_text):
-        self.xml_text = xml_text
-        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 = []
-
-        if xml_text is None:
-            return
-
-        xml_doc = parse_doc(self.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 _parse_required_features(self, required_features_list):
-        for required_feature in findall(required_features_list, "RequiredFeature"):
-            feature_name = findtext(required_feature, "Name")
-            feature_value = findtext(required_feature, "Value")
-            self.required_features.append(RequiredFeature(name=feature_name, value=feature_value))
-
-    def get_redacted_xml_text(self):
-        if self.xml_text is None:
-            return "<None/>"
-        xml_text = self.xml_text
-        for ext_handler in self.ext_handlers.extHandlers:
-            for extension in ext_handler.properties.extensions:
-                if extension.protectedSettings is not None:
-                    xml_text = xml_text.replace(extension.protectedSettings, "*** REDACTED ***")
-        return xml_text
-
-    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:
-                ExtensionsConfig._parse_plugin(ext_handler, plugin)
-                ExtensionsConfig._parse_plugin_settings(ext_handler, plugin_settings)
-            except ExtensionConfigError 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(AGENT_NAME, 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 ExtensionConfigError("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(AGENT_NAME, op=WALAEventOperation.PluginSettingsVersionMismatch, message=msg, log_event=True,
-                      is_success=False)
-            raise ExtensionConfigError(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 ExtensionConfigError(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 ExtensionConfigError(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 ExtensionConfigError(msg)
-            # Only Runtime settings available, parse that
-            ExtensionsConfig.__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
-            ExtensionsConfig.__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 ExtensionConfigError(msg)
-        depends_on_node = depends_on_nodes[0] if depends_on_nodes else None
-        depends_on_level = ExtensionsConfig.__get_dependency_level_from_node(depends_on_node, handler_name)
-        ExtensionsConfig.__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 ExtensionConfigError("No Name not specified for DependsOn object in ExtensionRuntimeSettings for MultiConfig!")
-
-            dependency_level = ExtensionsConfig.__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 ExtensionConfigError("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
-            ExtensionsConfig.__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 ExtensionConfigError("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 RemoteAccess(object):
     """
     Object containing information about user accounts
diff --git a/azurelinuxagent/common/protocol/wire.py b/azurelinuxagent/common/protocol/wire.py
index 26211fd9..91fa1da0 100644
--- a/azurelinuxagent/common/protocol/wire.py
+++ b/azurelinuxagent/common/protocol/wire.py
@@ -19,6 +19,7 @@
 import json
 import os
 import random
+import re
 import time
 import uuid
 import xml.sax.saxutils as saxutils
@@ -127,14 +128,14 @@ class WireProtocol(DataContract):
         return self.client.get_etag()
 
     def get_in_vm_gs_metadata(self):
-        return self.client.get_ext_conf().in_vm_gs_metadata
+        return self.client.get_extensions_goal_state().in_vm_gs_metadata
 
     def get_required_features(self):
-        return self.client.get_ext_conf().required_features
+        return self.client.get_extensions_goal_state().required_features
 
     def get_vmagent_manifests(self):
         goal_state = self.client.get_goal_state()
-        ext_conf = self.client.get_ext_conf()
+        ext_conf = self.client.get_extensions_goal_state()
         return ext_conf.vmagent_manifests, goal_state.incarnation
 
     def get_vmagent_pkgs(self, vmagent_manifest):
@@ -146,9 +147,9 @@ class WireProtocol(DataContract):
     def get_ext_handlers(self):
         logger.verbose("Get extension handler config")
         goal_state = self.client.get_goal_state()
-        ext_conf = self.client.get_ext_conf()
+        extensions_goal_state = self.client.get_extensions_goal_state()
         # In wire protocol, incarnation is equivalent to ETag
-        return ext_conf.ext_handlers, goal_state.incarnation
+        return extensions_goal_state.ext_handlers, goal_state.incarnation
 
     def get_ext_handler_pkgs(self, ext_handler):
         logger.verbose("Get extension handler package")
@@ -578,6 +579,8 @@ class WireClient(object):
         self._endpoint = endpoint
         self._goal_state = None
         self._extensions_goal_state = None
+        self._vm_settings_text = None
+        self._vm_settings_etag = None
         self._host_plugin = None
         self.status_blob = StatusBlob(self)
         self.goal_state_flusher = StateFlusher(conf.get_lib_dir())
@@ -787,25 +790,47 @@ class WireClient(object):
             # in the goal state (e.g. report status)
             self._update_host_plugin(goal_state.container_id, goal_state.role_config_name)
 
+            #
+            # TODO: This value should be set by querying the HostGAPlugin to check whether it supports Fast Track
+            #
+            fast_track_supported = True
+
+            use_fast_track = conf.get_enable_fast_track() and fast_track_supported
+
             if force_update:
                 logger.info("Forcing an update of the goal state..")
 
             if force_update or self._goal_state is None or self._goal_state.incarnation != goal_state.incarnation:
                 goal_state.fetch_full_goal_state(self)
                 self._goal_state = goal_state
+                #
+                # TODO: When Fast Track is fully enabled we should get the ExtensionsGoalState from ExtensionsConfig only
+                #       when not using fast track
+                # if not use_fast_track:
+                if self._goal_state.extensions_config_uri is None:
+                    self._extensions_goal_state = ExtensionsGoalState()
+                else:
+                    xml_text = self.fetch_config(self._goal_state.extensions_config_uri, self.get_header())
+                    self._extensions_goal_state = ExtensionsGoalState.from_extensions_config(xml_text)
+                # END TODO
                 updated = True
 
-            if conf.get_enable_fast_track():
-                if self._update_extensions_goal_state(force_update):
+            if use_fast_track:
+                #
+                # TODO: When Fast Track is fully enabled we should populate self._extensions_goal_state using the VmSettings
+                #
+                if self._update_vm_settings(force_update):
                     updated = True
 
+                # TODO: Create an ExtensionsGoalState from the vmSettings and compare against self._extensions_goal_state
+
             if updated:
                 self._save_goal_state()
 
         except Exception as exception:
             raise ProtocolError("Error fetching goal state: {0}".format(ustr(exception)))
 
-    def _update_extensions_goal_state(self, force_update):
+    def _update_vm_settings(self, force_update):
         correlation_id = str(uuid.uuid4())
         etag = None if force_update else self.get_etag()
 
@@ -837,7 +862,8 @@ class WireClient(object):
                 return False
 
             logger.info("Fetched new vmSettings [correlation ID: {0} New eTag: {1}]", correlation_id, response_etag)
-            self._extensions_goal_state = ExtensionsGoalState(response_etag, vm_settings)
+            self._vm_settings_text = vm_settings
+            self._vm_settings_etag = response_etag
             return True
 
         except Exception as exception:
@@ -865,11 +891,12 @@ class WireClient(object):
             save_if_not_none(self._goal_state.hosting_env, HOSTING_ENV_FILE_NAME)
             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._goal_state.ext_conf is not None and self._goal_state.ext_conf.xml_text is not None:
-                self._save_cache(self._goal_state.ext_conf.get_redacted_xml_text(), EXT_CONF_FILE_NAME.format(self._goal_state.incarnation))
+            if self._extensions_goal_state is not None and self._extensions_goal_state.xml_text is not None:
+                self._save_cache(self._extensions_goal_state.get_redacted_xml_text(), EXT_CONF_FILE_NAME.format(self._goal_state.incarnation))
 
-            if self._extensions_goal_state is not None:
-                self._save_cache(self._extensions_goal_state.get_redacted_vm_settings(), VM_SETTINGS_FILE_NAME.format(self._extensions_goal_state.etag))
+            if self._vm_settings_text is not None:
+                redacted_vm_settings_text = re.sub(r'("protectedSettings"\s*:\s*)"[^"]+"', r'\1"*** REDACTED ***"', self._vm_settings_text)
+                self._save_cache(redacted_vm_settings_text, VM_SETTINGS_FILE_NAME.format(self._vm_settings_etag))
 
         except Exception as e:
             logger.warn("Failed to save the goal state to disk: {0}", ustr(e))
@@ -881,16 +908,11 @@ class WireClient(object):
 
     def get_etag(self):
         """
-        Returns the Etag of the current ExtensionsGoalState, or None, if the goal state has not been retrieved.
+        Returns the Etag of the current vmSettings, or None, if the vmSettings have not been retrieved.
         """
-        if self._extensions_goal_state is None:
+        if self._vm_settings_text is None:
             return None
-        return self._extensions_goal_state.etag
-
-    def get_extensions_goal_state(self):
-        if self._extensions_goal_state is None:
-            raise ProtocolError("Trying to fetch the extensions goal state before initialization!")
-        return self._extensions_goal_state
+        return self._vm_settings_etag
 
     def get_goal_state(self):
         if self._goal_state is None:
@@ -912,10 +934,10 @@ class WireClient(object):
             raise ProtocolError("Trying to fetch Certificates before initialization!")
         return self._goal_state.certs
 
-    def get_ext_conf(self):
-        if self._goal_state is None:
-            raise ProtocolError("Trying to fetch Extension Conf before initialization!")
-        return self._goal_state.ext_conf
+    def get_extensions_goal_state(self):
+        if self._extensions_goal_state is None:
+            raise ProtocolError("Trying to fetch ExtensioalState before initialization!")
+        return self._extensions_goal_state
 
     def get_ext_manifest(self, ext_handler):
         if self._goal_state is None:
@@ -1086,17 +1108,17 @@ class WireClient(object):
         return ret
 
     def upload_status_blob(self):
-        ext_conf = self.get_ext_conf()
+        extensions_goal_state = self.get_extensions_goal_state()
 
-        if ext_conf.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)
-            ext_conf = self.get_ext_conf()
+            extensions_goal_state = self.get_extensions_goal_state()
 
-        if ext_conf.status_upload_blob is None:
+        if extensions_goal_state.status_upload_blob is None:
             raise ProtocolNotFoundError("Status upload uri is missing")
 
-        blob_type = ext_conf.status_upload_blob_type
+        blob_type = extensions_goal_state.status_upload_blob_type
         if blob_type not in ["BlockBlob", "PageBlob"]:
             blob_type = "BlockBlob"
             logger.verbose("Status Blob type is unspecified, assuming BlockBlob")
@@ -1117,7 +1139,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, ext_conf.status_upload_blob, ext_conf.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
@@ -1129,7 +1151,7 @@ class WireClient(object):
             self.report_status_event(msg, is_success=True)
 
         try:
-            if self.status_blob.upload(ext_conf.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))
@@ -1301,9 +1323,9 @@ class WireClient(object):
         return self._host_plugin
 
     def has_artifacts_profile_blob(self):
-        ext_conf = self.get_ext_conf()
-        return ext_conf and not \
-            textutil.is_str_none_or_whitespace(ext_conf.artifacts_profile_blob)
+        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()
@@ -1315,7 +1337,7 @@ class WireClient(object):
         artifacts_profile = None
 
         if self.has_artifacts_profile_blob():
-            blob = self.get_ext_conf().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.


Test patch:
diff --git a/tests/ga/test_exthandlers.py b/tests/ga/test_exthandlers.py
index d2ee2de7..c4bb2159 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.goal_state.add_event") as mock_add_event:
+            with patch("azurelinuxagent.common.protocol.extensions_goal_state.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 32fe64d1..d8625b19 100644
--- a/tests/ga/test_update.py
+++ b/tests/ga/test_update.py
@@ -28,7 +28,7 @@ from azurelinuxagent.common.event import EVENTS_DIRECTORY
 from azurelinuxagent.common.exception import ProtocolError, UpdateError, ResourceGoneError, HttpError
 from azurelinuxagent.common.future import ustr
 from azurelinuxagent.common.persist_firewall_rules import PersistFirewallRulesHandler
-from azurelinuxagent.common.protocol.goal_state import ExtensionsConfig
+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, \
@@ -1413,10 +1413,10 @@ class TestUpdate(UpdateTestCase):
         self.assertTrue(self._test_upgrade_available())
 
     def test_upgrade_available_handles_missing_family(self):
-        extensions_config = ExtensionsConfig(load_data("wire/ext_conf_missing_family.xml"))
+        extensions_goal_state = ExtensionsGoalState.from_extensions_config(load_data("wire/ext_conf_missing_family.xml"))
         protocol = ProtocolMock()
         protocol.family = "Prod"
-        protocol.agent_manifests = extensions_config.vmagent_manifests  # pylint: disable=attribute-defined-outside-init
+        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):
diff --git a/tests/protocol/test_goal_state.py b/tests/protocol/test_goal_state.py
index 6f5491c1..d2568145 100644
--- a/tests/protocol/test_goal_state.py
+++ b/tests/protocol/test_goal_state.py
@@ -7,7 +7,8 @@ import os
 from azurelinuxagent.common.future import httpclient
 from azurelinuxagent.common import conf
 from azurelinuxagent.common.exception import IncompleteGoalStateError
-from azurelinuxagent.common.protocol.goal_state import GoalState, ExtensionsConfig, _NUM_GS_FETCH_RETRIES
+from azurelinuxagent.common.protocol.extensions_goal_state import ExtensionsGoalState
+from azurelinuxagent.common.protocol.goal_state import GoalState, _NUM_GS_FETCH_RETRIES
 from azurelinuxagent.common.protocol import hostplugin
 from azurelinuxagent.common.utils import restutil
 from tests.protocol.mocks import mock_wire_protocol
@@ -51,9 +52,9 @@ class GoalStateTestCase(HttpRequestPredicates, AgentTestCase):
             self.assertTrue(os.path.exists(f), "{0} was not saved".format(f))
 
         with open(extensions_config_file, "r") as file_:
-            extensions_config = ExtensionsConfig(file_.read())
-        self.assertEqual(4, len(extensions_config.ext_handlers.extHandlers), "Expected 4 extensions in the test ExtensionsConfig")
-        for e in extensions_config.ext_handlers.extHandlers:
+            extensions_goal_state = ExtensionsGoalState.from_extensions_config(file_.read())
+        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))
 
         # TODO: Use azurelinuxagent.common.protocol.ExtensionsGoalState once it implements parsing
@@ -64,7 +65,7 @@ class GoalStateTestCase(HttpRequestPredicates, AgentTestCase):
         for e in extensions:
             self.assertEqual(e["settings"][0]["protectedSettings"], "*** REDACTED ***", "The protected settings for {0} were not redacted".format(e["name"]))
 
-    def test_update_extensions_goal_state_should_should_retry_on_resource_gone_error(self, _):
+    def test_update_vm_settings_should_should_retry_on_resource_gone_error(self, _):
         """
         Requests to the hostgaplugin incude the Container ID and the RoleConfigName as headers; when the hostgaplugin returns GONE (HTTP status 410) the agent
         needs to get a new goal state and retry the request with updated values for the Container ID and RoleConfigName headers.
@@ -92,7 +93,7 @@ class GoalStateTestCase(HttpRequestPredicates, AgentTestCase):
                 return MockHttpResponse(status=httpclient.NOT_MODIFIED)
 
             with patch("azurelinuxagent.common.utils.restutil._http_request", side_effect=http_get_vm_settings):
-                protocol.client._update_extensions_goal_state(force_update=False)
+                protocol.client._update_vm_settings(force_update=False)
 
             self.assertEqual(2, len(request_headers), "We expected 2 requests for vmSettings: the original request and the retry request")
             self.assertEqual("GET_VM_SETTINGS_TEST_CONTAINER_ID", request_headers[1][hostplugin._HEADER_CONTAINER_ID], "The retry request did not include the expected header for the ContainerId")
diff --git a/tests/protocol/test_hostplugin.py b/tests/protocol/test_hostplugin.py
index 641cb7d7..73d874c2 100644
--- a/tests/protocol/test_hostplugin.py
+++ b/tests/protocol/test_hostplugin.py
@@ -153,9 +153,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.
-            ext_conf = protocol.client._goal_state.ext_conf
-            ext_conf.status_upload_blob = sas_url
-            ext_conf.status_upload_blob_type = page_blob_type
+            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
 
             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 309a66c0..ac6bcdf9 100644
--- a/tests/protocol/test_wire.py
+++ b/tests/protocol/test_wire.py
@@ -31,7 +31,7 @@ from azurelinuxagent.common.agent_supported_feature import SupportedFeatureNames
     get_agent_supported_features_list_for_crp
 from azurelinuxagent.common.exception import ResourceGoneError, ProtocolError, \
     ExtensionDownloadError, HttpError
-from azurelinuxagent.common.protocol.goal_state import ExtensionsConfig
+from azurelinuxagent.common.protocol.extensions_goal_state import ExtensionsGoalState
 from azurelinuxagent.common.protocol.hostplugin import HostPluginProtocol
 from azurelinuxagent.common.protocol.restapi import VMAgentManifestUri
 from azurelinuxagent.common.protocol.wire import WireProtocol, WireClient, \
@@ -73,11 +73,11 @@ def create_mock_protocol(artifacts_profile_blob=None, status_upload_blob=None, s
     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.
-        ext_conf = ExtensionsConfig(None)
-        ext_conf.artifacts_profile_blob = artifacts_profile_blob
-        ext_conf.status_upload_blob = status_upload_blob
-        ext_conf.status_upload_blob_type = status_upload_blob_type
-        protocol.client._goal_state.ext_conf = ext_conf
+        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
 
         yield protocol
 
@@ -212,11 +212,11 @@ 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_ext_conf().status_upload_blob,
+            self.assertEqual(protocol.client.get_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_ext_conf().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:
@@ -276,7 +276,7 @@ class TestWireProtocol(AgentTestCase):
     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._goal_state.ext_conf = ExtensionsConfig(None)
+            protocol.client._extensions_goal_state = ExtensionsGoalState()
 
             self.assertEqual(None, protocol.client.get_artifacts_profile())
 
@@ -494,44 +494,44 @@ class TestWireProtocol(AgentTestCase):
 
 class TestWireClient(HttpRequestPredicates, AgentTestCase):
     def test_get_ext_conf_without_extensions_should_retrieve_vmagent_manifests_info(self, *args):  # pylint: disable=unused-argument
-        # Basic test for get_ext_conf() when extensions are not present in the config. The test verifies that
-        # get_ext_conf() fetches the correct data by comparing the returned data with the test data provided the
+        # Basic test for get_extensions_goal_state() when extensions are not present in the config. The test verifies that
+        # get_extensions_goal_state() fetches the correct data by comparing the returned data with the test data provided the
         # mock_wire_protocol.
         with mock_wire_protocol(mockwiredata.DATA_FILE_NO_EXT) as protocol:
-            ext_conf = protocol.client.get_ext_conf()
+            extensions_goal_state = protocol.client.get_extensions_goal_state()
 
-            ext_handlers_names = [ext_handler.name for ext_handler in ext_conf.ext_handlers.extHandlers]
-            self.assertEqual(0, len(ext_conf.ext_handlers.extHandlers),
+            ext_handlers_names = [ext_handler.name for ext_handler in extensions_goal_state.ext_handlers.extHandlers]
+            self.assertEqual(0, len(extensions_goal_state.ext_handlers.extHandlers),
                              "Unexpected number of extension handlers in the extension config: [{0}]".format(ext_handlers_names))
-            vmagent_manifests = [manifest.family for manifest in ext_conf.vmagent_manifests.vmAgentManifests]
-            self.assertEqual(0, len(ext_conf.vmagent_manifests.vmAgentManifests),
+            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(ext_conf.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(ext_conf.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(ext_conf.artifacts_profile_blob,
+            self.assertIsNone(extensions_goal_state.artifacts_profile_blob,
                               "Artifacts profile blob in the extensions config is expected to be None")
 
     def test_get_ext_conf_with_extensions_should_retrieve_ext_handlers_and_vmagent_manifests_info(self):
-        # Basic test for get_ext_conf() when extensions are present in the config. The test verifies that get_ext_conf()
+        # Basic test for get_extensions_goal_state() when extensions are present in the config. The test verifies that get_extensions_goal_state()
         # fetches the correct data by comparing the returned data with the test data provided the mock_wire_protocol.
         with mock_wire_protocol(mockwiredata.DATA_FILE) as protocol:
             wire_protocol_client = protocol.client
-            ext_conf = wire_protocol_client.get_ext_conf()
+            extensions_goal_state = wire_protocol_client.get_extensions_goal_state()
 
-            ext_handlers_names = [ext_handler.name for ext_handler in ext_conf.ext_handlers.extHandlers]
-            self.assertEqual(1, len(ext_conf.ext_handlers.extHandlers),
+            ext_handlers_names = [ext_handler.name for ext_handler in extensions_goal_state.ext_handlers.extHandlers]
+            self.assertEqual(1, len(extensions_goal_state.ext_handlers.extHandlers),
                              "Unexpected number of extension handlers in the extension config: [{0}]".format(ext_handlers_names))
-            vmagent_manifests = [manifest.family for manifest in ext_conf.vmagent_manifests.vmAgentManifests]
-            self.assertEqual(2, len(ext_conf.vmagent_manifests.vmAgentManifests),
+            vmagent_manifests = [manifest.family for manifest in extensions_goal_state.vmagent_manifests.vmAgentManifests]
+            self.assertEqual(2, len(extensions_goal_state.vmagent_manifests.vmAgentManifests),
                              "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",
-                             ext_conf.status_upload_blob, "Unexpected value for status upload blob URI")
-            self.assertEqual("BlockBlob", ext_conf.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, ext_conf.artifacts_profile_blob,
+            self.assertEqual(None, extensions_goal_state.artifacts_profile_blob,
                              "Artifacts profile blob in the extension config should have been None")
 
     def test_download_ext_handler_pkg_should_not_invoke_host_channel_when_direct_channel_succeeds(self):
@@ -1079,7 +1079,7 @@ class UpdateGoalStateTestCase(AgentTestCase):
                 else:
                     protocol.client.update_goal_state()
 
-                sequence_number = protocol.client.get_ext_conf().ext_handlers.extHandlers[0].properties.extensions[0].sequenceNumber
+                sequence_number = protocol.client.get_extensions_goal_state().ext_handlers.extHandlers[0].properties.extensions[0].sequenceNumber
 
                 self.assertEqual(protocol.client.get_goal_state().incarnation, new_incarnation)
                 self.assertEqual(protocol.client.get_hosting_env().deployment_name, new_hosting_env_deployment_name)
@@ -1178,9 +1178,10 @@ class UpdateGoalStateTestCase(AgentTestCase):
         with mock_wire_protocol(mockwiredata.DATA_FILE_MULTIPLE_EXT) as protocol:
             # instantiating the protocol fetches the goal state, so there is no need to do another call to update_goal_state()
             goal_state = protocol.client.get_goal_state()
+            extensions_goal_state = protocol.client.get_extensions_goal_state()
 
             protected_settings = []
-            for ext_handler in goal_state.ext_conf.ext_handlers.extHandlers:
+            for ext_handler in extensions_goal_state.ext_handlers.extHandlers:
                 for extension in ext_handler.properties.extensions:
                     if extension.protectedSettings is not None:
                         protected_settings.append(extension.protectedSettings)