Repository: Azure/WALinuxAgent
Pull request #2382 (2021-10-18 21:34:04)

Parse statusUploadBlob in vmSettings

codecov[bot]: # [Codecov](https://codecov.io/gh/Azure/WALinuxAgent/pull/2382?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azure) Report
> Merging [#2382](https://codecov.io/gh/Azure/WALinuxAgent/pull/2382?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azure) (9928b8d) into [develop](https://codecov.io/gh/Azure/WALinuxAgent/commit/dbe9b30e9594f429264c68b7124d59c45cc3c098?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azure) (dbe9b30) will **increase** coverage by `0.19%`.
> The diff coverage is `82.40%`.

[![Impacted file tree graph](https://codecov.io/gh/Azure/WALinuxAgent/pull/2382/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/2382?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azure)

```diff
@@             Coverage Diff             @@
##           develop    #2382      +/-   ##
===========================================
+ Coverage    70.94%   71.14%   +0.19%     
===========================================
  Files           97       97              
  Lines        14142    14374     +232     
  Branches      2034     2074      +40     
===========================================
+ Hits         10033    10226     +193     
- Misses        3657     3688      +31     
- Partials       452      460       +8     
```


| [Impacted Files](https://codecov.io/gh/Azure/WALinuxAgent/pull/2382?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azure) | Coverage Δ | |
|---|---|---|
| [...inuxagent/common/protocol/extensions\_goal\_state.py](https://codecov.io/gh/Azure/WALinuxAgent/pull/2382/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azure#diff-YXp1cmVsaW51eGFnZW50L2NvbW1vbi9wcm90b2NvbC9leHRlbnNpb25zX2dvYWxfc3RhdGUucHk=) | `87.86% <80.20%> (-2.22%)` | :arrow_down: |
| [azurelinuxagent/common/exception.py](https://codecov.io/gh/Azure/WALinuxAgent/pull/2382/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azure#diff-YXp1cmVsaW51eGFnZW50L2NvbW1vbi9leGNlcHRpb24ucHk=) | `97.00% <100.00%> (+0.06%)` | :arrow_up: |
| [azurelinuxagent/common/protocol/wire.py](https://codecov.io/gh/Azure/WALinuxAgent/pull/2382/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azure#diff-YXp1cmVsaW51eGFnZW50L2NvbW1vbi9wcm90b2NvbC93aXJlLnB5) | `80.44% <100.00%> (+1.92%)` | :arrow_up: |
| [azurelinuxagent/ga/exthandlers.py](https://codecov.io/gh/Azure/WALinuxAgent/pull/2382/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azure#diff-YXp1cmVsaW51eGFnZW50L2dhL2V4dGhhbmRsZXJzLnB5) | `85.74% <100.00%> (-0.51%)` | :arrow_down: |
| [azurelinuxagent/common/cgroupconfigurator.py](https://codecov.io/gh/Azure/WALinuxAgent/pull/2382/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azure#diff-YXp1cmVsaW51eGFnZW50L2NvbW1vbi9jZ3JvdXBjb25maWd1cmF0b3IucHk=) | `69.95% <0.00%> (-0.25%)` | :arrow_down: |
| [azurelinuxagent/common/cgroupstelemetry.py](https://codecov.io/gh/Azure/WALinuxAgent/pull/2382/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azure#diff-YXp1cmVsaW51eGFnZW50L2NvbW1vbi9jZ3JvdXBzdGVsZW1ldHJ5LnB5) | `100.00% <0.00%> (ø)` | |
| [azurelinuxagent/common/conf.py](https://codecov.io/gh/Azure/WALinuxAgent/pull/2382/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azure#diff-YXp1cmVsaW51eGFnZW50L2NvbW1vbi9jb25mLnB5) | `78.76% <0.00%> (+0.12%)` | :arrow_up: |
| [azurelinuxagent/ga/update.py](https://codecov.io/gh/Azure/WALinuxAgent/pull/2382/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azure#diff-YXp1cmVsaW51eGFnZW50L2dhL3VwZGF0ZS5weQ==) | `88.70% <0.00%> (+0.39%)` | :arrow_up: |
| [azurelinuxagent/common/cgroupapi.py](https://codecov.io/gh/Azure/WALinuxAgent/pull/2382/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azure#diff-YXp1cmVsaW51eGFnZW50L2NvbW1vbi9jZ3JvdXBhcGkucHk=) | `81.39% <0.00%> (+0.78%)` | :arrow_up: |
| ... and [1 more](https://codecov.io/gh/Azure/WALinuxAgent/pull/2382/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azure) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/Azure/WALinuxAgent/pull/2382?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/2382?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azure). Last update [dbe9b30...9928b8d](https://codecov.io/gh/Azure/WALinuxAgent/pull/2382?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/exception.py b/azurelinuxagent/common/exception.py
index c0421d00..e4c6c9aa 100644
--- a/azurelinuxagent/common/exception.py
+++ b/azurelinuxagent/common/exception.py
@@ -103,9 +103,21 @@ class ExtensionDownloadError(ExtensionError):
     """
 
 
-class ExtensionConfigError(ExtensionError):
+class ExtensionsGoalStateError(ExtensionError):
     """
-    Error raised when extension config file is malformed
+    Error raised when the ExtensionsGoalState is malformed
+    """
+
+
+class ExtensionsConfigError(ExtensionsGoalStateError):
+    """
+    Error raised when the ExtensionsConfig is malformed
+    """
+
+
+class VmSettingsError(ExtensionsGoalStateError):
+    """
+    Error raised when the VmSettings are malformed
     """
 
 
diff --git a/azurelinuxagent/common/protocol/extensions_goal_state.py b/azurelinuxagent/common/protocol/extensions_goal_state.py
index c02f29e6..0c5e9abd 100644
--- a/azurelinuxagent/common/protocol/extensions_goal_state.py
+++ b/azurelinuxagent/common/protocol/extensions_goal_state.py
@@ -18,13 +18,14 @@
 
 import json
 import re
+import sys
 
 from collections import defaultdict
 from operator import attrgetter
 
 import azurelinuxagent.common.logger as logger
 from azurelinuxagent.common.event import add_event, WALAEventOperation
-from azurelinuxagent.common.exception import ExtensionConfigError
+from azurelinuxagent.common.exception import ExtensionsConfigError, VmSettingsError
 from azurelinuxagent.common.future import ustr
 from azurelinuxagent.common.protocol.restapi import Extension, ExtHandler, ExtHandlerList, ExtHandlerVersionUri, VMAgentManifest, \
     VMAgentManifestList, VMAgentManifestUri, InVMGoalStateMetaData, ExtensionState
@@ -45,10 +46,10 @@ class ExtensionsGoalState(object):
         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._status_upload_blob = None
+        self._status_upload_blob_type = None
         self.artifacts_profile_blob = None
-        self.required_features = []
+        self._required_features = []
 
     @staticmethod
     def create_empty():
@@ -71,10 +72,19 @@ class ExtensionsGoalState(object):
 
     def get_redacted_text(self):
         """
-        Returns the raw text (either the ExtensionsConfig or the vmSettings) with any confidential data removed, or None for empty goal states.
+        Returns the raw text (either the ExtensionsConfig or the vmSettings) with any confidential data removed, or an empty string for empty goal states.
         """
         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):
         """
@@ -91,13 +101,25 @@ class ExtensionsGoalState(object):
             logger.info("[GoalStateMismatch] {0}", message)
             add_event(op=WALAEventOperation.GoalStateMismatch, is_success=False, message=message)
 
-        if from_extensions_config.required_features != from_vm_settings.required_features:
-            report_difference("required_features")
+        if from_extensions_config.get_status_upload_blob() != from_vm_settings.get_status_upload_blob():
+            report_difference("_status_upload_blob")
+        if from_extensions_config.get_status_upload_blob_type() != from_vm_settings.get_status_upload_blob_type():
+            report_difference("_status_upload_blob_type")
+        if from_extensions_config.get_required_features() != from_vm_settings.get_required_features():
+            report_difference("_required_features")
+
+    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 None
+        return ''
 
 
 class _ExtensionsGoalStateFromExtensionsConfig(ExtensionsGoalState):
@@ -106,6 +128,13 @@ class _ExtensionsGoalStateFromExtensionsConfig(ExtensionsGoalState):
         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")
@@ -128,12 +157,12 @@ class _ExtensionsGoalStateFromExtensionsConfig(ExtensionsGoalState):
         if required_features_list is not None:
             self._parse_required_features(required_features_list)
 
-        self.status_upload_blob = findtext(xml_doc, "StatusUploadBlob")
+        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._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"))
 
@@ -149,7 +178,7 @@ class _ExtensionsGoalStateFromExtensionsConfig(ExtensionsGoalState):
         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)
+            self._required_features.append(feature_name)
 
     def __parse_plugins_and_settings_and_populate_ext_handlers(self, xml_doc):
         """
@@ -200,7 +229,7 @@ class _ExtensionsGoalStateFromExtensionsConfig(ExtensionsGoalState):
             try:
                 _ExtensionsGoalStateFromExtensionsConfig._parse_plugin(ext_handler, plugin)
                 _ExtensionsGoalStateFromExtensionsConfig._parse_plugin_settings(ext_handler, plugin_settings)
-            except ExtensionConfigError as error:
+            except ExtensionsConfigError as error:
                 ext_handler.invalid_setting_reason = ustr(error)
 
             self.ext_handlers.extHandlers.append(ext_handler)
@@ -243,7 +272,7 @@ class _ExtensionsGoalStateFromExtensionsConfig(ExtensionsGoalState):
                                                             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")
+            raise ExtensionsConfigError("Received empty Extensions.Plugins.Plugin.state, failing Handler")
 
         def getattrib_wrapped_in_list(node, attr_name):
             attr = getattrib(node, attr_name)
@@ -317,12 +346,12 @@ class _ExtensionsGoalStateFromExtensionsConfig(ExtensionsGoalState):
                 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)
+            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 ExtensionConfigError(msg)
+            raise ExtensionsConfigError(msg)
 
         plugin_settings_node = settings[0]
         runtime_settings_nodes = findall(plugin_settings_node, "RuntimeSettings")
@@ -332,13 +361,13 @@ class _ExtensionsGoalStateFromExtensionsConfig(ExtensionsGoalState):
             # 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)
+            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 ExtensionConfigError(msg)
+                raise ExtensionsConfigError(msg)
             # Only Runtime settings available, parse that
             _ExtensionsGoalStateFromExtensionsConfig.__parse_runtime_settings(plugin_settings_node, runtime_settings_nodes[0], handler_name,
                                                       ext_handler)
@@ -386,7 +415,7 @@ class _ExtensionsGoalStateFromExtensionsConfig(ExtensionsGoalState):
         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)
+            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,
@@ -449,7 +478,7 @@ class _ExtensionsGoalStateFromExtensionsConfig(ExtensionsGoalState):
         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!")
+                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
@@ -459,7 +488,7 @@ class _ExtensionsGoalStateFromExtensionsConfig(ExtensionsGoalState):
             # 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!")
+                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
@@ -471,7 +500,7 @@ class _ExtensionsGoalStateFromExtensionsConfig(ExtensionsGoalState):
     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))
+            raise ExtensionsConfigError("SeqNo not specified for the Extension: {0}".format(name))
 
         try:
             runtime_settings = json.loads(gettext(settings_node))
@@ -507,22 +536,114 @@ class _ExtensionsGoalStateFromVmSettings(ExtensionsGoalState):
 
         try:
             self._parse_vm_settings(json_text)
+            self._do_common_validations()
         except Exception as e:
-            # TODO: Review code to determine whether we can rename ExtensionConfigError to InvalidExtensionsGoalStateError or
-            #       if we need to differentiate between errors in ExtensionsConfig and vmSettings.
-            raise ExtensionConfigError("Error parsing vmSettings (etag: {0}): {1}\n{2}".format(etag, format_exception(e), json_text))
+            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 = json.loads(json_text)
+        vm_settings = _CaseFoldedDict.from_dict(json.loads(json_text))
+        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")
-            self.required_features.extend([feature["name"] for feature in required_features])
+
+            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)
+
+
+#
+# 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/wire.py b/azurelinuxagent/common/protocol/wire.py
index 78670007..d7a33f9d 100644
--- a/azurelinuxagent/common/protocol/wire.py
+++ b/azurelinuxagent/common/protocol/wire.py
@@ -127,7 +127,7 @@ class WireProtocol(DataContract):
         return self.client.get_extensions_goal_state().in_vm_gs_metadata
 
     def get_required_features(self):
-        return self.client.get_extensions_goal_state().required_features
+        return self.client.get_extensions_goal_state().get_required_features()
 
     def get_vmagent_manifests(self):
         goal_state = self.client.get_goal_state()
@@ -897,13 +897,13 @@ class WireClient(object):
             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
-                if text is not None:
+                if text != '':
                     self._save_cache(text, EXT_CONF_FILE_NAME.format(self._extensions_goal_state.get_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
-                if text is not None:
+                if text != '':
                     self._save_cache(text, VM_SETTINGS_FILE_NAME.format(self._extensions_goal_state_from_vm_settings.get_id()))
             # END TODO
 
@@ -1111,18 +1111,15 @@ class WireClient(object):
     def upload_status_blob(self):
         extensions_goal_state = self.get_extensions_goal_state()
 
-        if extensions_goal_state.status_upload_blob is None:
+        if extensions_goal_state.get_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.status_upload_blob is None:
+        if extensions_goal_state.get_status_upload_blob() is None:
             raise ProtocolNotFoundError("Status upload uri is missing")
 
-        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")
+        blob_type = extensions_goal_state.get_status_upload_blob_type()
 
         try:
             self.status_blob.prepare(blob_type)
@@ -1140,7 +1137,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.status_upload_blob, extensions_goal_state.status_upload_blob_type)
+            host.put_vm_status(self.status_blob, extensions_goal_state.get_status_upload_blob(), extensions_goal_state.get_status_upload_blob_type())
             return
         except ResourceGoneError:
             # refresh the host plugin client and try again on the next iteration of the main loop
@@ -1152,7 +1149,7 @@ class WireClient(object):
             self.report_status_event(msg, is_success=True)
 
         try:
-            if self.status_blob.upload(extensions_goal_state.status_upload_blob):
+            if self.status_blob.upload(extensions_goal_state.get_status_upload_blob()):
                 return
         except Exception as e:
             msg = "Exception uploading status blob: {0}".format(ustr(e))
diff --git a/azurelinuxagent/ga/exthandlers.py b/azurelinuxagent/ga/exthandlers.py
index 414c9c57..247a96f6 100644
--- a/azurelinuxagent/ga/exthandlers.py
+++ b/azurelinuxagent/ga/exthandlers.py
@@ -43,7 +43,7 @@ from azurelinuxagent.common.errorstate import ErrorState
 from azurelinuxagent.common.event import add_event, elapsed_milliseconds, WALAEventOperation, \
     add_periodic, EVENTS_DIRECTORY
 from azurelinuxagent.common.exception import ExtensionDownloadError, ExtensionError, ExtensionErrorCodes, \
-    ExtensionOperationError, ExtensionUpdateError, ProtocolError, ProtocolNotFoundError, ExtensionConfigError, \
+    ExtensionOperationError, ExtensionUpdateError, ProtocolError, ProtocolNotFoundError, ExtensionsGoalStateError, \
     GoalStateAggregateStatusCodes, MultiConfigExtensionEnableError
 from azurelinuxagent.common.future import ustr, is_file_not_found_error
 from azurelinuxagent.common.protocol.restapi import ExtensionStatus, ExtensionSubStatus, ExtHandler, ExtHandlerStatus, \
@@ -621,7 +621,7 @@ class ExtHandlersHandler(object):
         try:
             # Ensure the extension config was valid
             if ext_handler_i.ext_handler.is_invalid_setting:
-                raise ExtensionConfigError(ext_handler_i.ext_handler.invalid_setting_reason)
+                raise ExtensionsGoalStateError(ext_handler_i.ext_handler.invalid_setting_reason)
 
             handler_state = ext_handler_i.ext_handler.properties.state
 
@@ -660,7 +660,7 @@ class ExtHandlersHandler(object):
                                                           operation=ext_handler_i.operation, message=err_msg)
             add_event(name=ext_name, version=ext_handler_i.ext_handler.properties.version, op=ext_handler_i.operation,
                       is_success=False, log_event=True, message=err_msg)
-        except ExtensionConfigError as error:
+        except ExtensionsGoalStateError as error:
             # Catch and report Invalid ExtensionConfig errors here to fail fast rather than timing out after 90 min
             err_msg = "Ran into config errors: {0}. \nPlease retry again as another operation with updated settings".format(
                 ustr(error))
@@ -763,7 +763,7 @@ class ExtHandlersHandler(object):
             else:
                 ext_handler_i.logger.info("Extension already disabled, not doing anything")
         else:
-            raise ExtensionConfigError(
+            raise ExtensionsGoalStateError(
                 "Unknown requested state for Extension {0}: {1}".format(extension.name, extension.state))
 
     @staticmethod
@@ -1038,7 +1038,7 @@ class ExtHandlersHandler(object):
         ext_handler_statuses = []
         # For MultiConfig, we need to report status per extension even for Handler level failures.
         # If we have HandlerStatus for a MultiConfig handler and GS is requesting for it, we would report status per
-        #  extension even if HandlerState == NotInstalled (Sample scenario: ExtensionConfigError, DecideVersionError, etc)
+        # extension even if HandlerState == NotInstalled (Sample scenario: ExtensionsGoalStateError, DecideVersionError, etc)
         if handler_state != ExtHandlerState.NotInstalled or ext_handler.supports_multi_config:
 
             # Since we require reading the Manifest for reading the heartbeat, this would fail if HandlerManifest not found.
@@ -1357,7 +1357,7 @@ class ExtHandlerInstance(object):
         # CRP can wrongfully request send a Multi-Config GoalState even if the Handler supports only Single Config.
         # Checking this only if HandlerState == Enable. In case of Uninstall, we dont care.
         if self.supports_multi_config and not self.load_manifest().supports_multiple_extensions():
-            raise ExtensionConfigError(
+            raise ExtensionsGoalStateError(
                 "Handler {0} does not support MultiConfig but CRP expects it, failing due to inconsistent data".format(
                     self.ext_handler.name))
 
@@ -1433,7 +1433,7 @@ class ExtHandlerInstance(object):
                 }
             ]
             # Create status directory if not exists. This is needed in the case where the Handler fails before even
-            # initializing the directories (ExtensionConfigError, Version deleted from PIR error, etc)
+            # initializing the directories (ExtensionsGoalStateError, Version deleted from PIR error, etc)
             if not os.path.exists(os.path.dirname(status_path)):
                 fileutil.mkdir(os.path.dirname(status_path), mode=0o700)
             self.logger.info("Creating a placeholder status file {0} with status: {1}".format(status_path, status))


Test patch:
diff --git a/tests/data/hostgaplugin/ext_conf.xml b/tests/data/hostgaplugin/ext_conf.xml
index 6ba5fc6f..700cf110 100644
--- a/tests/data/hostgaplugin/ext_conf.xml
+++ b/tests/data/hostgaplugin/ext_conf.xml
@@ -1,81 +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://ardfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Prod_uscentraleuap_manifest.xml</Uri>
-        <Uri>https://zrdfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Prod_uscentraleuap_manifest.xml</Uri>
-      </Uris>
-      <Version>2.4.0.1</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.4.0.1</Version>
-    </GAFamily>
-  </GAFamilies>
-  <Location>CentralUSEUAP</Location>
-  <ServiceName>CRP</ServiceName>
-</GuestAgentExtension>
-<RequiredFeatures>
-  <RequiredFeature>
-    <Name>MultipleExtensionsPerHandler</Name>
-  </RequiredFeature>
-</RequiredFeatures>
-<StatusUploadBlob statusBlobType="BlockBlob">https://dcrcf5h3g6.blob.core.windows.net/$system/edpymvazsh.ba323e38-2145-4cb9-8cbf-778c7824bc9e.status?sv=2018-03-28&amp;sr=b&amp;sk=system-1&amp;sig=xYQPQ6IHQh%2b57h2OA6u4CUr85wzaxQH7WF2qHi3Hu98%3d&amp;se=9999-01-01T00%3a00%3a00Z&amp;sp=w</StatusUploadBlob>
-<Plugins>
-  <Plugin name="Microsoft.Azure.Extensions.CustomScript" version="2.1.3" location="https://zrdfepirv2cdm03prdstr01a.blob.core.windows.net/708cdbbfb9ad4802ac80a012992930b1/Microsoft.Azure.Extensions_CustomScript_uscentraleuap_manifest.xml" state="enabled" autoUpgrade="true" failoverlocation="https://ardfepirv2cdm03prdstr01a.blob.core.windows.net/708cdbbfb9ad4802ac80a012992930b1/Microsoft.Azure.Extensions_CustomScript_uscentraleuap_manifest.xml" runAsStartupTask="false" isJson="true" useExactVersion="true" />
-  <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.3">
-    <RuntimeSettings seqNo="0">{
-  "runtimeSettings": [
-    {
-      "handlerSettings": {
-        "publicSettings": {"commandToExecute":"echo '26c8d804-6d2e-4da1-ab5b-629ad733b3bb'"}
+<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="BlockBlob">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 '571bbcda-2992-4417-a65c-eaad1832c1f9'"}}
+      </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 '58d6ac9d-9ef3-400a-99d7-1c3b5451eef6'"}}
+      </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>
-    <ExtensionRuntimeSettings seqNo="0" name="MCExt3" state="enabled">{
-  "runtimeSettings": [
-    {
-      "handlerSettings": {
-        "publicSettings": {"source":{"script":"echo '834cad38-9eec-483e-946d-f713cafc3da8'"}}
-      }
-    }
-  ]
-}</ExtensionRuntimeSettings>
-  </Plugin>
-</PluginSettings>
-<InVMArtifactsProfileBlob>https://dcrcf5h3g6.blob.core.windows.net/$system/edpymvazsh.ba323e38-2145-4cb9-8cbf-778c7824bc9e.vmSettings?sv=2018-03-28&amp;sr=b&amp;sk=system-1&amp;sig=y40xBuoKCIheR%2fzq2P7ocWSRmsWNB9TizIojGwm0A2A%3d&amp;se=9999-01-01T00%3a00%3a00Z&amp;sp=r</InVMArtifactsProfileBlob>
-<InVMGoalStateMetaData inSvdSeqNo="10" createdOnTicks="637632862319659103" activityId="c86d8064-6f11-4145-8178-e0587e0584de" correlationId="6609dcb1-fccc-474a-9a59-8bd71ba06983" /></Extensions>
\ No newline at end of file
+      </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-protected_settings.json b/tests/data/hostgaplugin/vm_settings-protected_settings.json
index 0742c8c3..3b21e416 100644
--- a/tests/data/hostgaplugin/vm_settings-protected_settings.json
+++ b/tests/data/hostgaplugin/vm_settings-protected_settings.json
@@ -2,6 +2,11 @@
     "activityId": "b3f493e2-cab8-487d-820a-5718504f2f3f",
     "correlationId": "4f4939fb-35e6-411f-98f1-aeaf8796ed30",
     "extensionsLastModifiedTickCount": 637629365237217593,
+    "extensionGoalStatesSource": "Fabric",
+    "StatusUploadBlob": {
+        "statusBlobType": "BlockBlob",
+        "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": "a53f7094-a16c-47af-abe4-b05c05d0d79a",
         "resourceGroupName": "narrieta-rg",
diff --git a/tests/data/hostgaplugin/vm_settings.json b/tests/data/hostgaplugin/vm_settings.json
index d486d618..0bf9236e 100644
--- a/tests/data/hostgaplugin/vm_settings.json
+++ b/tests/data/hostgaplugin/vm_settings.json
@@ -1,13 +1,18 @@
 {
-    "activityId": "c86d8064-6f11-4145-8178-e0587e0584de",
-    "correlationId": "6609dcb1-fccc-474a-9a59-8bd71ba06983",
-    "extensionsLastModifiedTickCount": 637632862319659103,
+    "activityId": "2e7f8b5d-f637-4721-b757-cb190d49b4e9",
+    "correlationId": "1bef4c48-044e-4225-8f42-1d1eac1eb158",
+    "extensionsLastModifiedTickCount": 637693267431616449,
+    "extensionGoalStatesSource": "Fabric",
+    "StatusUploadBlob": {
+        "statusBlobType": "BlockBlob",
+        "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-ht3rnxw",
-        "vmName": "edpymvazsh",
+        "resourceGroupName": "rg-dc-qphvx25",
+        "vmName": "edpxmal5j1",
         "location": "CentralUSEUAP",
-        "vmId": "ba323e38-2145-4cb9-8cbf-778c7824bc9e",
+        "vmId": "058b176d-445b-4e75-bd97-4911511b7d96",
         "vmSize": "Standard_D2s_v3",
         "osType": "Linux"
     },
@@ -18,28 +23,41 @@
     ],
     "gaFamilies": [
         {
-            "name": "Prod",
-            "version": "2.4.0.1"
+            "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.4.0.1"
+            "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.3",
-            "location": "https://zrdfepirv2cdm03prdstr01a.blob.core.windows.net/708cdbbfb9ad4802ac80a012992930b1/Microsoft.Azure.Extensions_CustomScript_uscentraleuap_manifest.xml",
+            "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 '26c8d804-6d2e-4da1-ab5b-629ad733b3bb'\"}"
+                    "publicSettings": "{\"commandToExecute\":\"echo '09cd27e9-fbd6-48ad-be86-55f3783e0a23'\"}"
                 }
             ]
         },
@@ -47,26 +65,32 @@
             "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 '571bbcda-2992-4417-a65c-eaad1832c1f9'\"}}",
+                    "publicSettings": "{\"source\":{\"script\":\"echo '737fa9f1-e9bf-4c3e-ab1f-9e03cd0b5b40'\"}}",
                     "seqNo": 0,
                     "extensionName": "MCExt1",
                     "extensionState": "enabled"
                 },
                 {
-                    "publicSettings": "{\"source\":{\"script\":\"echo '58d6ac9d-9ef3-400a-99d7-1c3b5451eef6'\"}}",
+                    "publicSettings": "{\"source\":{\"script\":\"echo 'f6a0a405-c028-4e68-bd77-5d491fbbd9cf'\"}}",
                     "seqNo": 0,
                     "extensionName": "MCExt2",
                     "extensionState": "enabled"
                 },
                 {
-                    "publicSettings": "{\"source\":{\"script\":\"echo '834cad38-9eec-483e-946d-f713cafc3da8'\"}}",
+                    "publicSettings": "{\"source\":{\"script\":\"echo '338e316a-01bf-4513-8ae1-b603b09ad155'\"}}",
                     "seqNo": 0,
                     "extensionName": "MCExt3",
                     "extensionState": "enabled"
diff --git a/tests/protocol/test_extensions_goal_state.py b/tests/protocol/test_extensions_goal_state.py
index c070ddde..0663d843 100644
--- a/tests/protocol/test_extensions_goal_state.py
+++ b/tests/protocol/test_extensions_goal_state.py
@@ -1,11 +1,13 @@
 # Copyright (c) Microsoft Corporation. All rights reserved.
 # Licensed under the Apache License.
+import json
 import os.path
+import re
 
 from azurelinuxagent.common.event import WALAEventOperation
 from azurelinuxagent.common.future import httpclient
 from azurelinuxagent.common.protocol import hostplugin
-from azurelinuxagent.common.protocol.extensions_goal_state import ExtensionsGoalState
+from azurelinuxagent.common.protocol.extensions_goal_state import ExtensionsGoalState, _CaseFoldedDict
 from azurelinuxagent.common.utils import restutil, fileutil
 from tests.protocol.mocks import mock_wire_protocol
 from tests.protocol import mockwiredata
@@ -61,10 +63,82 @@ class ExtensionsGoalStateTestCase(HttpRequestPredicates, AgentTestCase):
             self.assertTrue(len(mismatch_events) == 0, "Did not expect any GoalStateMismatch messages, got: {0}".format(mismatch_events))
 
         with patch("azurelinuxagent.common.protocol.extensions_goal_state.add_event") as add_event_patcher:
-            from_vm_settings.required_features = ['FORCE_A_MISMATCH_FEATURE']
+            from_vm_settings._required_features = ['FORCE_A_MISMATCH_FEATURE']
 
             ExtensionsGoalState.compare(from_extensions_config, from_vm_settings)
 
             mismatch_events = [kw["message"] for _, kw in add_event_patcher.call_args_list if kw['op'] == WALAEventOperation.GoalStateMismatch]
-            self.assertTrue(len(mismatch_events) == 1 and 'required_features' in mismatch_events[0],
+            self.assertTrue(len(mismatch_events) == 1 and '_required_features' in mismatch_events[0],
                 "Expected 1 difference in RequiredFeatures, got: {0}".format(mismatch_events))
+
+    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')
+
+    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)
+
+        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")
diff --git a/tests/protocol/test_hostplugin.py b/tests/protocol/test_hostplugin.py
index 73d874c2..8966d3f5 100644
--- a/tests/protocol/test_hostplugin.py
+++ b/tests/protocol/test_hostplugin.py
@@ -154,8 +154,8 @@ class TestHostPlugin(HttpRequestPredicates, AgentTestCase):
             # 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
+            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 32350e84..5c23271c 100644
--- a/tests/protocol/test_wire.py
+++ b/tests/protocol/test_wire.py
@@ -75,8 +75,8 @@ def create_mock_protocol(artifacts_profile_blob=None, status_upload_blob=None, s
         # 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
+        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_extensions_goal_state().status_upload_blob,
+            self.assertEqual(protocol.client.get_extensions_goal_state().get_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().status_upload_blob_type, u'BlockBlob')
+            self.assertEqual(protocol.client.get_extensions_goal_state().get_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:
@@ -252,19 +252,6 @@ class TestWireProtocol(AgentTestCase):
                         patch_http.assert_called_once_with(testurl, protocol.client.status_blob)
                         self.assertFalse(HostPluginProtocol.is_default_channel)
 
-    @patch("azurelinuxagent.common.protocol.hostplugin.HostPluginProtocol.ensure_initialized")
-    def test_upload_status_blob_unknown_type_assumes_block(self, *_):
-        with create_mock_protocol(status_upload_blob=testurl, status_upload_blob_type="NotALegalType") as protocol:
-            protocol.client.status_blob.vm_status = VMStatus(message="Ready", status="Ready")
-
-            with patch.object(StatusBlob, "prepare") as patch_prepare:
-                with patch.object(StatusBlob, "upload") as patch_default_upload:
-                    HostPluginProtocol.is_default_channel = False
-                    protocol.client.upload_status_blob()
-
-                    patch_prepare.assert_called_once_with("BlockBlob")
-                    patch_default_upload.assert_called_once_with(testurl)
-
     def test_upload_status_blob_reports_prepare_error(self, *_):
         with create_mock_protocol(status_upload_blob=testurl, status_upload_blob_type=testtype) as protocol:
             protocol.client.status_blob.vm_status = VMStatus(message="Ready", status="Ready")
@@ -506,9 +493,9 @@ 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.status_upload_blob,
+            self.assertIsNone(extensions_goal_state.get_status_upload_blob(),
                               "Status upload blob in the extension config is expected to be None")
-            self.assertIsNone(extensions_goal_state.status_upload_blob_type,
+            self.assertIsNone(extensions_goal_state.get_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")
@@ -528,8 +515,8 @@ 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.status_upload_blob, "Unexpected value for status upload blob URI")
-            self.assertEqual("BlockBlob", extensions_goal_state.status_upload_blob_type,
+                             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(),
                              "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")