Repository: Azure/WALinuxAgent
Pull request #1287 (2018-08-07 05:15:00)

Add a UDEV rule in azure disk encryption on Linux
Recently we are trying to do azure disk encryption on Ubuntu & RedHat and found that a UDEV rule is missing, the new UDEV rule is like following in `/etc/udev/rules.d/66-azure-storage.rules`
```
ATTRS{device_id}=="?00000001-0001-*", ENV{fabric_name}="BEK", GOTO="azure_names"
```
After disk encryption:
 - Without this UDEV rule
```
$ sudo tree /dev/disk/azure
âââ resource -> ../../sdb
âââ resource-part1 -> ../../sdb1
âââ root -> ../../sda
âââ root-part1 -> ../../sda1
âââ scsi1
    âââ lun0 -> ../../../sdd
```
 - With this UDEV rule
```
$ sudo tree /dev/disk/azure
âââ BEK -> ../../sdc
âââ resource -> ../../sdb
âââ resource-part1 -> ../../sdb1
âââ root -> ../../sda
âââ root-part1 -> ../../sda1
âââ scsi1
    âââ lun0 -> ../../../sdd
```

All azure os, resource and bek disks should be under `/dev/disk/azure` directory, that's a rule.
The item `BEK -> ../../sdc` is very important in kubernetes, without this item, new data disk could not be recognized, details info could be found here: https://github.com/kubernetes/kubernetes/issues/66443#issuecomment-406765240

<!-- DO NOT DELETE THIS TEMPLATE -->

## Description

Issue # <!-- if any -->
<!--
Please add an informative description that covers that changes made by the pull request. 
This checklist is used to make sure that common issues in a pull request are addressed.
This will expedite the process of getting your pull request merged and avoid extra work on your part to fix issues discovered during the review process.
-->

---

### PR information
- [x] The title of the PR is clear and informative.
- [ ] There are a small number of commits, each of which has an informative message. This means that previously merged commits do not appear in the history of the PR. For information on cleaning up the commits in your pull request, [see this page](https://github.com/Azure/azure-powershell/blob/master/documentation/development-docs/cleaning-up-commits.md).
- [ ] Except for special cases involving multiple contributors, the PR is started from a fork of the main repository, not a branch.
- [x] If applicable, the PR references the bug/issue that it fixes in the description.
- [ ] New Unit tests were added for the changes made and Travis.CI is passing.

### Quality of Code and Contribution Guidelines
- [x] I have read the [contribution guidelines](https://github.com/Azure/WALinuxAgent/blob/master/.github/CONTRIBUTING.md).

andyzhangx: /assign @paulmey

Patch:
diff --git a/README.md b/README.md
index 66672ab2..67409de7 100644
--- a/README.md
+++ b/README.md
@@ -20,7 +20,7 @@ OpenLogic CentOS 6.9|![badge](https://dcrbadges.blob.core.windows.net/scenarios/
 OpenLogic CentOS 7.4|![badge](https://dcrbadges.blob.core.windows.net/scenarios/OpenLogic_CentOS_7.4__agent--bvt.svg)
 RedHat RHEL 6.9|![badge](https://dcrbadges.blob.core.windows.net/scenarios/RedHat_RHEL_6.9__agent--bvt.svg)
 RedHat RHEL 7-RAW|![badge](https://dcrbadges.blob.core.windows.net/scenarios/RedHat_RHEL_7-RAW__agent--bvt.svg)
-SUSE SLES 12-SP2|![badge](https://dcrbadges.blob.core.windows.net/scenarios/SUSE_SLES_12-SP2__agent--bvt.svg)
+SUSE SLES 12-SP3|![badge](https://dcrbadges.blob.core.windows.net/scenarios/SUSE_SLES_12-SP3__agent--bvt.svg)
 
 ## Introduction
 
@@ -179,6 +179,7 @@ A configuration file (/etc/waagent.conf) controls the actions of waagent. Blank
 A sample configuration file is shown below:
 
 ```yml
+Extensions.Enabled=y
 Provisioning.Enabled=y
 Provisioning.UseCloudInit=n
 Provisioning.DeleteRootPassword=n
@@ -213,6 +214,23 @@ may be used for some string type configuration entries as detailed below.
 
 ### Configuration File Options
 
+#### __Extensions.Enabled__
+
+_Type: Boolean_  
+_Default: y_
+
+This allows the user to enable or disable the extension handling functionality in the
+agent. Valid values are "y" or "n". If extension handling is disabled, the goal state 
+will still be processed and VM status is still reported, but only every 5 minutes. 
+Extension config within the goal state will be ignored. Note that functionality such
+as password reset, ssh key updates and backups depend on extensions. Only disable this
+if you do not need extensions at all.
+
+_Note_: disabling extensions in this manner is not the same as running completely 
+without the agent. In order to do that, the `provisionVMAgent` flag must be set at
+provisioning time, via whichever API is being used. We will provide more details on
+this on our wiki when it is generally available. 
+
 #### __Provisioning.Enabled__
 
 _Type: Boolean_  
diff --git a/azurelinuxagent/common/conf.py b/azurelinuxagent/common/conf.py
index a0f7c89d..4b7972bb 100644
--- a/azurelinuxagent/common/conf.py
+++ b/azurelinuxagent/common/conf.py
@@ -26,6 +26,8 @@ import os.path
 import azurelinuxagent.common.utils.fileutil as fileutil
 from azurelinuxagent.common.exception import AgentConfigError
 
+DISABLE_AGENT_FILE = 'disable_agent'
+
 
 class ConfigurationProvider(object):
     """
@@ -85,57 +87,59 @@ def load_conf_from_file(conf_file_path, conf=__conf__):
         raise AgentConfigError(("Failed to load conf file:{0}, {1}"
                                 "").format(conf_file_path, err))
 
+
 __SWITCH_OPTIONS__ = {
-    "OS.AllowHTTP" : False,
-    "OS.EnableFirewall" : False,
-    "OS.EnableFIPS" : False,
-    "OS.EnableRDMA" : False,
-    "OS.UpdateRdmaDriver" : False,
-    "OS.CheckRdmaDriver" : False,
-    "Logs.Verbose" : False,
-    "Provisioning.Enabled" : True,
-    "Provisioning.UseCloudInit" : False,
-    "Provisioning.AllowResetSysUser" : False,
-    "Provisioning.RegenerateSshHostKeyPair" : False,
-    "Provisioning.DeleteRootPassword" : False,
-    "Provisioning.DecodeCustomData" : False,
-    "Provisioning.ExecuteCustomData" : False,
-    "Provisioning.MonitorHostName" : False,
-    "DetectScvmmEnv" : False,
-    "ResourceDisk.Format" : False,
-    "DetectScvmmEnv" : False,
-    "ResourceDisk.Format" : False,
-    "ResourceDisk.EnableSwap" : False,
-    "AutoUpdate.Enabled" : True,
-    "EnableOverProvisioning" : True
+    "OS.AllowHTTP": False,
+    "OS.EnableFirewall": False,
+    "OS.EnableFIPS": False,
+    "OS.EnableRDMA": False,
+    "OS.UpdateRdmaDriver": False,
+    "OS.CheckRdmaDriver": False,
+    "Logs.Verbose": False,
+    "Extensions.Enabled": True,
+    "Provisioning.Enabled": True,
+    "Provisioning.UseCloudInit": False,
+    "Provisioning.AllowResetSysUser": False,
+    "Provisioning.RegenerateSshHostKeyPair": False,
+    "Provisioning.DeleteRootPassword": False,
+    "Provisioning.DecodeCustomData": False,
+    "Provisioning.ExecuteCustomData": False,
+    "Provisioning.MonitorHostName": False,
+    "DetectScvmmEnv": False,
+    "ResourceDisk.Format": False,
+    "ResourceDisk.EnableSwap": False,
+    "AutoUpdate.Enabled": True,
+    "EnableOverProvisioning": True
 }
 
+
 __STRING_OPTIONS__ = {
-    "Lib.Dir" : "/var/lib/waagent",
-    "DVD.MountPoint" : "/mnt/cdrom/secure",
-    "Pid.File" : "/var/run/waagent.pid",
-    "Extension.LogDir" : "/var/log/azure",
-    "OS.OpensslPath" : "/usr/bin/openssl",
-    "OS.SshDir" : "/etc/ssh",
-    "OS.HomeDir" : "/home",
-    "OS.PasswordPath" : "/etc/shadow",
-    "OS.SudoersDir" : "/etc/sudoers.d",
-    "OS.RootDeviceScsiTimeout" : None,
-    "Provisioning.SshHostKeyPairType" : "rsa",
-    "Provisioning.PasswordCryptId" : "6",
-    "HttpProxy.Host" : None,
-    "ResourceDisk.MountPoint" : "/mnt/resource",
-    "ResourceDisk.MountOptions" : None,
-    "ResourceDisk.Filesystem" : "ext3",
-    "AutoUpdate.GAFamily" : "Prod"
+    "Lib.Dir": "/var/lib/waagent",
+    "DVD.MountPoint": "/mnt/cdrom/secure",
+    "Pid.File": "/var/run/waagent.pid",
+    "Extension.LogDir": "/var/log/azure",
+    "OS.OpensslPath": "/usr/bin/openssl",
+    "OS.SshDir": "/etc/ssh",
+    "OS.HomeDir": "/home",
+    "OS.PasswordPath": "/etc/shadow",
+    "OS.SudoersDir": "/etc/sudoers.d",
+    "OS.RootDeviceScsiTimeout": None,
+    "Provisioning.SshHostKeyPairType": "rsa",
+    "Provisioning.PasswordCryptId": "6",
+    "HttpProxy.Host": None,
+    "ResourceDisk.MountPoint": "/mnt/resource",
+    "ResourceDisk.MountOptions": None,
+    "ResourceDisk.Filesystem": "ext3",
+    "AutoUpdate.GAFamily": "Prod"
 }
 
+
 __INTEGER_OPTIONS__ = {
-    "OS.SshClientAliveInterval" : 180,
-    "Provisioning.PasswordCryptSaltLength" : 10,
-    "HttpProxy.Port" : None,
-    "ResourceDisk.SwapSizeMB" : 0,
-    "Autoupdate.Frequency" : 3600
+    "OS.SshClientAliveInterval": 180,
+    "Provisioning.PasswordCryptSaltLength": 10,
+    "HttpProxy.Port": None,
+    "ResourceDisk.SwapSizeMB": 0,
+    "Autoupdate.Frequency": 3600
 }
 
 
@@ -152,17 +156,21 @@ def get_configuration(conf=__conf__):
 
     return options
 
+
 def enable_firewall(conf=__conf__):
     return conf.get_switch("OS.EnableFirewall", False)
 
+
 def enable_rdma(conf=__conf__):
     return conf.get_switch("OS.EnableRDMA", False) or \
            conf.get_switch("OS.UpdateRdmaDriver", False) or \
            conf.get_switch("OS.CheckRdmaDriver", False)
 
+
 def enable_rdma_update(conf=__conf__):
     return conf.get_switch("OS.UpdateRdmaDriver", False)
 
+
 def get_logs_verbose(conf=__conf__):
     return conf.get_switch("Logs.Verbose", False)
 
@@ -186,44 +194,57 @@ def get_agent_pid_file_path(conf=__conf__):
 def get_ext_log_dir(conf=__conf__):
     return conf.get("Extension.LogDir", "/var/log/azure")
 
+
 def get_fips_enabled(conf=__conf__):
     return conf.get_switch("OS.EnableFIPS", False)
 
+
 def get_openssl_cmd(conf=__conf__):
     return conf.get("OS.OpensslPath", "/usr/bin/openssl")
 
+
 def get_ssh_client_alive_interval(conf=__conf__):
     return conf.get("OS.SshClientAliveInterval", 180)
 
+
 def get_ssh_dir(conf=__conf__):
     return conf.get("OS.SshDir", "/etc/ssh")
 
+
 def get_home_dir(conf=__conf__):
     return conf.get("OS.HomeDir", "/home")
 
+
 def get_passwd_file_path(conf=__conf__):
     return conf.get("OS.PasswordPath", "/etc/shadow")
 
+
 def get_sudoers_dir(conf=__conf__):
     return conf.get("OS.SudoersDir", "/etc/sudoers.d")
 
+
 def get_sshd_conf_file_path(conf=__conf__):
     return os.path.join(get_ssh_dir(conf), "sshd_config")
 
+
 def get_ssh_key_glob(conf=__conf__):
     return os.path.join(get_ssh_dir(conf), 'ssh_host_*key*')
 
+
 def get_ssh_key_private_path(conf=__conf__):
     return os.path.join(get_ssh_dir(conf),
         'ssh_host_{0}_key'.format(get_ssh_host_keypair_type(conf)))
 
+
 def get_ssh_key_public_path(conf=__conf__):
     return os.path.join(get_ssh_dir(conf),
         'ssh_host_{0}_key.pub'.format(get_ssh_host_keypair_type(conf)))
 
+
 def get_root_device_scsi_timeout(conf=__conf__):
     return conf.get("OS.RootDeviceScsiTimeout", None)
 
+
 def get_ssh_host_keypair_type(conf=__conf__):
     keypair_type = conf.get("Provisioning.SshHostKeyPairType", "rsa")
     if keypair_type == "auto":
@@ -234,15 +255,23 @@ def get_ssh_host_keypair_type(conf=__conf__):
         return "rsa"
     return keypair_type
 
+
 def get_ssh_host_keypair_mode(conf=__conf__):
     return conf.get("Provisioning.SshHostKeyPairType", "rsa")
 
+
 def get_provision_enabled(conf=__conf__):
     return conf.get_switch("Provisioning.Enabled", True)
 
+
+def get_extensions_enabled(conf=__conf__):
+    return conf.get_switch("Extensions.Enabled", True)
+
+
 def get_provision_cloudinit(conf=__conf__):
     return conf.get_switch("Provisioning.UseCloudInit", False)
 
+
 def get_allow_reset_sys_user(conf=__conf__):
     return conf.get_switch("Provisioning.AllowResetSysUser", False)
 
@@ -322,8 +351,14 @@ def get_autoupdate_enabled(conf=__conf__):
 def get_autoupdate_frequency(conf=__conf__):
     return conf.get_int("Autoupdate.Frequency", 3600)
 
+
 def get_enable_overprovisioning(conf=__conf__):
     return conf.get_switch("EnableOverProvisioning", True)
 
+
 def get_allow_http(conf=__conf__):
     return conf.get_switch("OS.AllowHTTP", False)
+
+
+def get_disable_agent_file_path(conf=__conf__):
+    return os.path.join(get_lib_dir(conf), DISABLE_AGENT_FILE)
diff --git a/azurelinuxagent/common/osutil/default.py b/azurelinuxagent/common/osutil/default.py
index 6611424a..b3fc3b9b 100644
--- a/azurelinuxagent/common/osutil/default.py
+++ b/azurelinuxagent/common/osutil/default.py
@@ -985,7 +985,8 @@ class DefaultOSUtil(object):
         return endpoint
 
     def is_missing_default_route(self):
-        routes = shellutil.run_get_output("route -n")[1]
+        route_cmd = "ip route show"
+        routes = shellutil.run_get_output(route_cmd)[1]
         for route in routes.split("\n"):
             if route.startswith("0.0.0.0 ") or route.startswith("default "):
                return False
@@ -1005,11 +1006,14 @@ class DefaultOSUtil(object):
         return self.get_first_if()[1]
 
     def set_route_for_dhcp_broadcast(self, ifname):
-        return shellutil.run("route add 255.255.255.255 dev {0}".format(ifname),
+        route_cmd = "ip route add"
+        return shellutil.run("{0} 255.255.255.255 dev {1}".format(
+            route_cmd, ifname),
                              chk_err=False)
 
     def remove_route_for_dhcp_broadcast(self, ifname):
-        shellutil.run("route del 255.255.255.255 dev {0}".format(ifname),
+        route_cmd = "ip route del"
+        shellutil.run("{0} 255.255.255.255 dev {1}".format(route_cmd, ifname),
                       chk_err=False)
 
     def is_dhcp_available(self):
@@ -1044,10 +1048,9 @@ class DefaultOSUtil(object):
 
     def route_add(self, net, mask, gateway):
         """
-        Add specified route using /sbin/route add -net.
+        Add specified route 
         """
-        cmd = ("/sbin/route add -net "
-               "{0} netmask {1} gw {2}").format(net, mask, gateway)
+        cmd = "ip route add {0} via {1}".format(net, gateway)
         return shellutil.run(cmd, chk_err=False)
 
     def get_dhcp_pid(self):
diff --git a/azurelinuxagent/common/utils/processutil.py b/azurelinuxagent/common/utils/processutil.py
index 9c0eb24b..b6f30ee7 100644
--- a/azurelinuxagent/common/utils/processutil.py
+++ b/azurelinuxagent/common/utils/processutil.py
@@ -100,7 +100,7 @@ def _destroy_process(process, signal_to_send=signal.SIGKILL):
         pass    # If the process is already gone, that's fine
 
 
-def capture_from_process_poll(process, cmd, timeout):
+def capture_from_process_poll(process, cmd, timeout, code):
     """
     If the process forks, we cannot capture anything we block until the process tree completes
     """
@@ -114,12 +114,12 @@ def capture_from_process_poll(process, cmd, timeout):
         os.killpg(os.getpgid(process.pid), signal.SIGKILL)
         stdout, stderr = process.communicate()
         msg = format_stdout_stderr(sanitize(stdout), sanitize(stderr))
-        raise ExtensionError("Timeout({0}): {1}\n{2}".format(timeout, cmd, msg))
+        raise ExtensionError("Timeout({0}): {1}\n{2}".format(timeout, cmd, msg), code=code)
 
     # process completed or forked
     return_code = process.wait()
     if return_code != 0:
-        raise ExtensionError("Non-zero exit code: {0}, {1}".format(return_code, cmd))
+        raise ExtensionError("Non-zero exit code: {0}, {1}".format(return_code, cmd), code=code)
 
     stderr = b''
     stdout = b'cannot collect stdout'
@@ -155,20 +155,20 @@ def capture_from_process_poll(process, cmd, timeout):
     return stdout, stderr
 
 
-def capture_from_process_no_timeout(process, cmd):
+def capture_from_process_no_timeout(process, cmd, code):
     try:
         stdout, stderr = process.communicate()
     except OSError as e:
         _destroy_process(process, signal.SIGKILL)
-        raise ExtensionError("Error while running '{0}': {1}".format(cmd, e.strerror))
+        raise ExtensionError("Error while running '{0}': {1}".format(cmd, e.strerror), code=code)
     except Exception as e:
         _destroy_process(process, signal.SIGKILL)
-        raise ExtensionError("Exception while running '{0}': {1}".format(cmd, e))
+        raise ExtensionError("Exception while running '{0}': {1}".format(cmd, e), code=code)
 
     return stdout, stderr
 
 
-def capture_from_process_raw(process, cmd, timeout):
+def capture_from_process_raw(process, cmd, timeout, code):
     """
     Captures stdout and stderr from an already-created process.
 
@@ -180,18 +180,18 @@ def capture_from_process_raw(process, cmd, timeout):
     :raises ExtensionError: if a timeout occurred or if anything was raised by Popen.communicate()
     """
     if not timeout:
-        stdout, stderr = capture_from_process_no_timeout(process, cmd)
+        stdout, stderr = capture_from_process_no_timeout(process, cmd, code)
     else:
         if os.getpgid(process.pid) != process.pid:
             _destroy_process(process, signal.SIGKILL)
-            raise ExtensionError("Subprocess was not root of its own process group")
+            raise ExtensionError("Subprocess was not root of its own process group", code=code)
 
-        stdout, stderr = capture_from_process_poll(process, cmd, timeout)
+        stdout, stderr = capture_from_process_poll(process, cmd, timeout, code)
 
     return stdout, stderr
 
 
-def capture_from_process(process, cmd, timeout=0):
+def capture_from_process(process, cmd, timeout=0, code=-1):
     """
     Captures stdout and stderr from an already-created process. The output is "cooked"
     into a string of reasonable length.
@@ -203,5 +203,5 @@ def capture_from_process(process, cmd, timeout=0):
     :rtype: (str, str)
     :raises ExtensionError: if a timeout occurred or if anything was raised by Popen.communicate()
     """
-    stdout, stderr = capture_from_process_raw(process, cmd, timeout)
+    stdout, stderr = capture_from_process_raw(process, cmd, timeout, code)
     return format_stdout_stderr(sanitize(stdout), sanitize(stderr))
diff --git a/azurelinuxagent/common/utils/shellutil.py b/azurelinuxagent/common/utils/shellutil.py
index 97e6e989..8b25b814 100644
--- a/azurelinuxagent/common/utils/shellutil.py
+++ b/azurelinuxagent/common/utils/shellutil.py
@@ -57,6 +57,11 @@ if not hasattr(subprocess, 'check_output'):
 Shell command util functions
 """
 
+def has_command(cmd):
+    """
+    Return True if the given command is on the path
+    """
+    return not run(cmd, False)
 
 def run(cmd, chk_err=True):
     """
diff --git a/azurelinuxagent/daemon/main.py b/azurelinuxagent/daemon/main.py
index d44af469..12531761 100644
--- a/azurelinuxagent/daemon/main.py
+++ b/azurelinuxagent/daemon/main.py
@@ -99,6 +99,17 @@ class DaemonHandler(object):
 
         fileutil.write_file(pid_file, ustr(os.getpid()))
 
+    def sleep_if_disabled(self):
+        agent_disabled_file_path = conf.get_disable_agent_file_path()
+        if os.path.exists(agent_disabled_file_path):
+            import threading
+            logger.warn("Disabling the guest agent by sleeping forever; "
+                        "to re-enable, remove {0} and restart"
+                        .format(agent_disabled_file_path))
+            self.running = False
+            disable_event = threading.Event()
+            disable_event.wait()
+
     def initialize_environment(self):
         # Create lib dir
         if not os.path.isdir(conf.get_lib_dir()):
@@ -149,5 +160,7 @@ class DaemonHandler(object):
         else:
             logger.info("RDMA capabilities are not enabled, skipping")
 
+        self.sleep_if_disabled()
+
         while self.running:
             self.update_handler.run_latest(child_args=child_args)
diff --git a/azurelinuxagent/ga/exthandlers.py b/azurelinuxagent/ga/exthandlers.py
index a66aacbd..add289e5 100644
--- a/azurelinuxagent/ga/exthandlers.py
+++ b/azurelinuxagent/ga/exthandlers.py
@@ -294,6 +294,10 @@ class ExtHandlersHandler(object):
                     logger.warn("Failed to remove extension package {0}: {1}".format(pkg, e.strerror))
    
     def handle_ext_handlers(self, etag=None):
+        if not conf.get_extensions_enabled():
+            logger.verbose("Extension handling is disabled")
+            return
+
         if self.ext_handlers.extHandlers is None or \
                 len(self.ext_handlers.extHandlers) == 0:
             logger.verbose("No extension handler config found")
@@ -479,14 +483,14 @@ class ExtHandlersHandler(object):
                 active_exts = ext_handler_i.report_ext_status()
                 handler_status.extensions.extend(active_exts)
             except ExtensionError as e:
-                ext_handler_i.set_handler_status(message=ustr(e), code=-1)
+                ext_handler_i.set_handler_status(message=ustr(e), code=e.code)
 
             try:
                 heartbeat = ext_handler_i.collect_heartbeat()
                 if heartbeat is not None:
                     handler_status.status = heartbeat.get('status')
             except ExtensionError as e:
-                ext_handler_i.set_handler_status(message=ustr(e), code=-1)
+                ext_handler_i.set_handler_status(message=ustr(e), code=e.code)
 
         vm_status.vmAgent.extensionHandlers.append(handler_status)
 
@@ -660,7 +664,7 @@ class ExtHandlerInstance(object):
                 logger.warn("Error while downloading extension: {0}", e)
         
         if package is None or chosen_uri is None:
-            raise ExtensionError("Failed to download extension")
+            raise ExtensionError("Failed to download extension", code=1001)
 
         self.logger.verbose("Unpack extension package")
         self.pkg_file = os.path.join(conf.get_lib_dir(), os.path.basename(chosen_uri.uri) + ".zip")
@@ -671,7 +675,7 @@ class ExtHandlerInstance(object):
         except IOError as e:
             fileutil.clean_ioerror(e,
                 paths=[self.get_base_dir(), self.pkg_file])
-            raise ExtensionError(u"Failed to write and unzip plugin", e)
+            raise ExtensionError(u"Failed to write and unzip plugin", e, code=1001)
 
         # Add user execute permission to all files under the base dir
         for file in fileutil.get_all_files(self.get_base_dir()):
@@ -843,7 +847,11 @@ class ExtHandlerInstance(object):
             ext_status.message = u"Failed to get status file {0}".format(e)
             ext_status.code = -1
             ext_status.status = "error"
-        except (ExtensionError, ValueError) as e:
+        except ExtensionError as e:
+            ext_status.message = u"Malformed status file {0}".format(e)
+            ext_status.code = e.code
+            ext_status.status = "error"
+        except ValueError as e:
             ext_status.message = u"Malformed status file {0}".format(e)
             ext_status.code = -1
             ext_status.status = "error"
@@ -900,7 +908,7 @@ class ExtHandlerInstance(object):
         last_update = int(time.time() - os.stat(heartbeat_file).st_mtime)
         return last_update <= 600
 
-    def launch_command(self, cmd, timeout=300, extension_error_code=-1, env=None):
+    def launch_command(self, cmd, timeout=300, extension_error_code=1000, env=None):
         begin_utc = datetime.datetime.utcnow()
         self.logger.verbose("Launch command: [{0}]", cmd)
         base_dir = self.get_base_dir()
@@ -938,7 +946,7 @@ class ExtHandlerInstance(object):
 
         cg = CGroups.for_extension(self.ext_handler.name)
         CGroupsTelemetry.track_extension(self.ext_handler.name, cg)
-        msg = capture_from_process(process, cmd, timeout)
+        msg = capture_from_process(process, cmd, timeout, extension_error_code)
 
         ret = process.poll()
         if ret is None:
@@ -956,9 +964,9 @@ class ExtHandlerInstance(object):
         try:
             data = json.loads(fileutil.read_file(man_file))
         except (IOError, OSError) as e:
-            raise ExtensionError('Failed to load manifest file ({0}): {1}'.format(man_file, e.strerror))
+            raise ExtensionError('Failed to load manifest file ({0}): {1}'.format(man_file, e.strerror), code=1002)
         except ValueError:
-            raise ExtensionError('Malformed manifest file ({0}).'.format(man_file))
+            raise ExtensionError('Malformed manifest file ({0}).'.format(man_file), code=1003)
 
         return HandlerManifest(data[0])
 
diff --git a/azurelinuxagent/ga/update.py b/azurelinuxagent/ga/update.py
index 4292fc26..d3c39c17 100644
--- a/azurelinuxagent/ga/update.py
+++ b/azurelinuxagent/ga/update.py
@@ -72,6 +72,7 @@ CHILD_POLL_INTERVAL = 60
 MAX_FAILURE = 3 # Max failure allowed for agent before blacklisted
 
 GOAL_STATE_INTERVAL = 3
+GOAL_STATE_INTERVAL_DISABLED = 5 * 60
 
 ORPHAN_WAIT_INTERVAL = 15 * 60
 
@@ -274,6 +275,10 @@ class UpdateHandler(object):
             self._ensure_partition_assigned()
             self._ensure_readonly_files()
 
+            goal_state_interval = GOAL_STATE_INTERVAL \
+                if conf.get_extensions_enabled() \
+                else GOAL_STATE_INTERVAL_DISABLED
+
             while self.running:
                 if self._is_orphaned:
                     logger.info("Agent {0} is an orphan -- exiting",
@@ -320,7 +325,7 @@ class UpdateHandler(object):
                         duration=duration,
                         message="Incarnation {0}".format(exthandlers_handler.last_etag))
 
-                time.sleep(GOAL_STATE_INTERVAL)
+                time.sleep(goal_state_interval)
 
         except Exception as e:
             msg = u"Agent {0} failed with exception: {1}".format(CURRENT_AGENT, ustr(e))
diff --git a/azurelinuxagent/pa/provision/cloudinit.py b/azurelinuxagent/pa/provision/cloudinit.py
index fb366999..9609d7da 100644
--- a/azurelinuxagent/pa/provision/cloudinit.py
+++ b/azurelinuxagent/pa/provision/cloudinit.py
@@ -83,10 +83,7 @@ class CloudInitProvisionHandler(ProvisionHandler):
             if os.path.isfile(ovf_file_path):
                 try:
                     ovf_env = OvfEnv(fileutil.read_file(ovf_file_path))
-                    self.report_event(message=ovf_env.provision_guest_agent,
-                                      is_success=True,
-                                      duration=0,
-                                      operation=WALAEventOperation.ProvisionGuestAgent)
+                    self.handle_provision_guest_agent(ovf_env.provision_guest_agent)
                     return
                 except ProtocolError as pe:
                     raise ProvisionError("OVF xml could not be parsed "
diff --git a/azurelinuxagent/pa/provision/default.py b/azurelinuxagent/pa/provision/default.py
index ab9bb13b..a6e50824 100644
--- a/azurelinuxagent/pa/provision/default.py
+++ b/azurelinuxagent/pa/provision/default.py
@@ -92,17 +92,14 @@ class ProvisionHandler(object):
                 is_success=True,
                 duration=elapsed_milliseconds(utc_start))
 
-            self.report_event(message=ovf_env.provision_guest_agent,
-                              is_success=True,
-                              duration=0,
-                              operation=WALAEventOperation.ProvisionGuestAgent)
+            self.handle_provision_guest_agent(ovf_env.provision_guest_agent)
 
             self.report_ready(thumbprint)
             logger.info("Provisioning complete")
 
         except (ProtocolError, ProvisionError) as e:
             self.report_not_ready("ProvisioningFailed", ustr(e))
-            self.report_event(ustr(e))
+            self.report_event(ustr(e), is_success=False)
             logger.error("Provisioning failed: {0}", ustr(e))
             return
 
@@ -205,6 +202,19 @@ class ProvisionHandler(object):
             self.provisioned_file_path(),
             get_osutil().get_instance_id())
 
+    @staticmethod
+    def write_agent_disabled():
+        logger.warn("Disabling guest agent in accordance with ovf-env.xml")
+        fileutil.write_file(conf.get_disable_agent_file_path(), '')
+
+    def handle_provision_guest_agent(self, provision_guest_agent):
+        self.report_event(message=provision_guest_agent,
+                          is_success=True,
+                          duration=0,
+                          operation=WALAEventOperation.ProvisionGuestAgent)
+        if provision_guest_agent and provision_guest_agent.lower() == 'false':
+            self.write_agent_disabled()
+
     def provision(self, ovfenv):
         logger.info("Handle ovf-env.xml.")
         try:
diff --git a/config/66-azure-storage.rules b/config/66-azure-storage.rules
index 5b2b799f..5fb36930 100644
--- a/config/66-azure-storage.rules
+++ b/config/66-azure-storage.rules
@@ -6,6 +6,7 @@ LABEL="azure_disk"
 # The resource/resource has GUID of 0001 as the second value
 ATTRS{device_id}=="?00000000-0000-*", ENV{fabric_name}="root", GOTO="azure_names"
 ATTRS{device_id}=="?00000000-0001-*", ENV{fabric_name}="resource", GOTO="azure_names"
+ATTRS{device_id}=="?00000001-0001-*", ENV{fabric_name}="BEK", GOTO="azure_names"
 # Wellknown SCSI controllers
 ATTRS{device_id}=="{f8b3781a-1e82-4818-a1c3-63d806ec15bb}", ENV{fabric_scsi_controller}="scsi0", GOTO="azure_datadisk"
 ATTRS{device_id}=="{f8b3781b-1e82-4818-a1c3-63d806ec15bb}", ENV{fabric_scsi_controller}="scsi1", GOTO="azure_datadisk"
diff --git a/config/alpine/waagent.conf b/config/alpine/waagent.conf
index 5c620e70..f45ac1fc 100644
--- a/config/alpine/waagent.conf
+++ b/config/alpine/waagent.conf
@@ -5,6 +5,10 @@
 # Enable instance creation
 Provisioning.Enabled=y
 
+# Enable extension handling. Do not disable this unless you do not need password reset,
+# backup, monitoring, or any extension handling whatsoever.
+Extensions.Enabled=y
+
 # Rely on cloud-init to provision
 Provisioning.UseCloudInit=n
 
diff --git a/config/bigip/waagent.conf b/config/bigip/waagent.conf
index 501292e5..bdf7a815 100644
--- a/config/bigip/waagent.conf
+++ b/config/bigip/waagent.conf
@@ -16,6 +16,10 @@ Role.TopologyConsumer=None
 # Enable instance creation
 Provisioning.Enabled=y
 
+# Enable extension handling. Do not disable this unless you do not need password reset,
+# backup, monitoring, or any extension handling whatsoever.
+Extensions.Enabled=y
+
 # Rely on cloud-init to provision
 Provisioning.UseCloudInit=n
 
diff --git a/config/coreos/waagent.conf b/config/coreos/waagent.conf
index a58c9985..942d922b 100644
--- a/config/coreos/waagent.conf
+++ b/config/coreos/waagent.conf
@@ -5,6 +5,10 @@
 # Enable instance creation
 Provisioning.Enabled=y
 
+# Enable extension handling. Do not disable this unless you do not need password reset,
+# backup, monitoring, or any extension handling whatsoever.
+Extensions.Enabled=y
+
 # Rely on cloud-init to provision
 Provisioning.UseCloudInit=n
 
diff --git a/config/debian/waagent.conf b/config/debian/waagent.conf
index 0c49d87b..60c44375 100644
--- a/config/debian/waagent.conf
+++ b/config/debian/waagent.conf
@@ -5,6 +5,10 @@
 # Enable instance creation
 Provisioning.Enabled=y
 
+# Enable extension handling. Do not disable this unless you do not need password reset,
+# backup, monitoring, or any extension handling whatsoever.
+Extensions.Enabled=y
+
 # Rely on cloud-init to provision
 Provisioning.UseCloudInit=n
 
diff --git a/config/freebsd/waagent.conf b/config/freebsd/waagent.conf
index 6e611d10..62c66a6c 100644
--- a/config/freebsd/waagent.conf
+++ b/config/freebsd/waagent.conf
@@ -5,6 +5,10 @@
 # Enable instance creation
 Provisioning.Enabled=y
 
+# Enable extension handling. Do not disable this unless you do not need password reset,
+# backup, monitoring, or any extension handling whatsoever.
+Extensions.Enabled=y
+
 # Rely on cloud-init to provision
 Provisioning.UseCloudInit=n
 
diff --git a/config/gaia/waagent.conf b/config/gaia/waagent.conf
index 168beda0..83d01628 100644
--- a/config/gaia/waagent.conf
+++ b/config/gaia/waagent.conf
@@ -5,6 +5,10 @@
 # Enable instance creation
 Provisioning.Enabled=y
 
+# Enable extension handling. Do not disable this unless you do not need password reset,
+# backup, monitoring, or any extension handling whatsoever.
+Extensions.Enabled=y
+
 # Rely on cloud-init to provision
 Provisioning.UseCloudInit=n
 
diff --git a/config/suse/waagent.conf b/config/suse/waagent.conf
index 2a514584..242aa614 100644
--- a/config/suse/waagent.conf
+++ b/config/suse/waagent.conf
@@ -5,6 +5,10 @@
 # Enable instance creation
 Provisioning.Enabled=y
 
+# Enable extension handling. Do not disable this unless you do not need password reset,
+# backup, monitoring, or any extension handling whatsoever.
+Extensions.Enabled=y
+
 # Rely on cloud-init to provision
 Provisioning.UseCloudInit=n
 
diff --git a/config/ubuntu/waagent.conf b/config/ubuntu/waagent.conf
index e67cdfe7..ff4ce8d0 100644
--- a/config/ubuntu/waagent.conf
+++ b/config/ubuntu/waagent.conf
@@ -5,6 +5,10 @@
 # Enable instance creation
 Provisioning.Enabled=n
 
+# Enable extension handling. Do not disable this unless you do not need password reset,
+# backup, monitoring, or any extension handling whatsoever.
+Extensions.Enabled=y
+
 # Rely on cloud-init to provision
 Provisioning.UseCloudInit=y
 
diff --git a/config/waagent.conf b/config/waagent.conf
index 90efa375..448c26c2 100644
--- a/config/waagent.conf
+++ b/config/waagent.conf
@@ -5,6 +5,10 @@
 # Enable instance creation
 Provisioning.Enabled=y
 
+# Enable extension handling. Do not disable this unless you do not need password reset,
+# backup, monitoring, or any extension handling whatsoever.
+Extensions.Enabled=y
+
 # Rely on cloud-init to provision
 Provisioning.UseCloudInit=n
 


Test patch:
diff --git a/tests/common/test_conf.py b/tests/common/test_conf.py
index c38a8afd..be935db3 100644
--- a/tests/common/test_conf.py
+++ b/tests/common/test_conf.py
@@ -27,45 +27,46 @@ class TestConf(AgentTestCase):
     # Note:
     # -- These values *MUST* match those from data/test_waagent.conf
     EXPECTED_CONFIGURATION = {
-        "Provisioning.Enabled" : True,
-        "Provisioning.UseCloudInit" : True,
-        "Provisioning.DeleteRootPassword" : True,
-        "Provisioning.RegenerateSshHostKeyPair" : True,
-        "Provisioning.SshHostKeyPairType" : "rsa",
-        "Provisioning.MonitorHostName" : True,
-        "Provisioning.DecodeCustomData" : False,
-        "Provisioning.ExecuteCustomData" : False,
-        "Provisioning.PasswordCryptId" : '6',
-        "Provisioning.PasswordCryptSaltLength" : 10,
-        "Provisioning.AllowResetSysUser" : False,
-        "ResourceDisk.Format" : True,
-        "ResourceDisk.Filesystem" : "ext4",
-        "ResourceDisk.MountPoint" : "/mnt/resource",
-        "ResourceDisk.EnableSwap" : False,
-        "ResourceDisk.SwapSizeMB" : 0,
-        "ResourceDisk.MountOptions" : None,
-        "Logs.Verbose" : False,
-        "OS.EnableFIPS" : True,
-        "OS.RootDeviceScsiTimeout" : '300',
-        "OS.OpensslPath" : '/usr/bin/openssl',
-        "OS.SshClientAliveInterval" : 42,
-        "OS.SshDir" : "/notareal/path",
-        "HttpProxy.Host" : None,
-        "HttpProxy.Port" : None,
-        "DetectScvmmEnv" : False,
-        "Lib.Dir" : "/var/lib/waagent",
-        "DVD.MountPoint" : "/mnt/cdrom/secure",
-        "Pid.File" : "/var/run/waagent.pid",
-        "Extension.LogDir" : "/var/log/azure",
-        "OS.HomeDir" : "/home",
-        "OS.EnableRDMA" : False,
-        "OS.UpdateRdmaDriver" : False,
-        "OS.CheckRdmaDriver" : False,
-        "AutoUpdate.Enabled" : True,
-        "AutoUpdate.GAFamily" : "Prod",
-        "EnableOverProvisioning" : True,
-        "OS.AllowHTTP" : False,
-        "OS.EnableFirewall" : False
+        "Extensions.Enabled": True,
+        "Provisioning.Enabled": True,
+        "Provisioning.UseCloudInit": True,
+        "Provisioning.DeleteRootPassword": True,
+        "Provisioning.RegenerateSshHostKeyPair": True,
+        "Provisioning.SshHostKeyPairType": "rsa",
+        "Provisioning.MonitorHostName": True,
+        "Provisioning.DecodeCustomData": False,
+        "Provisioning.ExecuteCustomData": False,
+        "Provisioning.PasswordCryptId": '6',
+        "Provisioning.PasswordCryptSaltLength": 10,
+        "Provisioning.AllowResetSysUser": False,
+        "ResourceDisk.Format": True,
+        "ResourceDisk.Filesystem": "ext4",
+        "ResourceDisk.MountPoint": "/mnt/resource",
+        "ResourceDisk.EnableSwap": False,
+        "ResourceDisk.SwapSizeMB": 0,
+        "ResourceDisk.MountOptions": None,
+        "Logs.Verbose": False,
+        "OS.EnableFIPS": True,
+        "OS.RootDeviceScsiTimeout": '300',
+        "OS.OpensslPath": '/usr/bin/openssl',
+        "OS.SshClientAliveInterval": 42,
+        "OS.SshDir": "/notareal/path",
+        "HttpProxy.Host": None,
+        "HttpProxy.Port": None,
+        "DetectScvmmEnv": False,
+        "Lib.Dir": "/var/lib/waagent",
+        "DVD.MountPoint": "/mnt/cdrom/secure",
+        "Pid.File": "/var/run/waagent.pid",
+        "Extension.LogDir": "/var/log/azure",
+        "OS.HomeDir": "/home",
+        "OS.EnableRDMA": False,
+        "OS.UpdateRdmaDriver": False,
+        "OS.CheckRdmaDriver": False,
+        "AutoUpdate.Enabled": True,
+        "AutoUpdate.GAFamily": "Prod",
+        "EnableOverProvisioning": True,
+        "OS.AllowHTTP": False,
+        "OS.EnableFirewall": False
     }
 
     def setUp(self):
@@ -113,3 +114,23 @@ class TestConf(AgentTestCase):
                 TestConf.EXPECTED_CONFIGURATION[k],
                 configuration[k],
                 k)
+
+    def test_get_agent_disabled_file_path(self):
+        self.assertEqual(get_disable_agent_file_path(self.conf),
+                         os.path.join(self.tmp_dir, DISABLE_AGENT_FILE))
+
+    def test_write_agent_disabled(self):
+        """
+        Test writing disable_agent is empty
+        """
+        from azurelinuxagent.pa.provision.default import ProvisionHandler
+
+        disable_file_path = get_disable_agent_file_path(self.conf)
+        self.assertFalse(os.path.exists(disable_file_path))
+        ProvisionHandler.write_agent_disabled()
+        self.assertTrue(os.path.exists(disable_file_path))
+        self.assertEqual('', fileutil.read_file(disable_file_path))
+
+    def test_get_extensions_enabled(self):
+        self.assertTrue(get_extensions_enabled(self.conf))
+
diff --git a/tests/daemon/test_daemon.py b/tests/daemon/test_daemon.py
index 6f9f8b1a..eb51d0e3 100644
--- a/tests/daemon/test_daemon.py
+++ b/tests/daemon/test_daemon.py
@@ -14,9 +14,11 @@
 #
 # Requires Python 2.6+ and Openssl 1.0+
 #
+from multiprocessing import Process
 
 from azurelinuxagent.daemon import *
 from azurelinuxagent.daemon.main import OPENSSL_FIPS_ENVIRONMENT
+from azurelinuxagent.pa.provision.default import ProvisionHandler
 from tests.tools import *
 
 
@@ -27,16 +29,17 @@ class MockDaemonCall(object):
 
     def __call__(self, *args, **kw):
         self.count = self.count - 1
-        #Stop daemon after restarting for n times
+        # Stop daemon after restarting for n times
         if self.count <= 0:
             self.daemon_handler.running = False
         raise Exception("Mock unhandled exception")
 
+
 class TestDaemon(AgentTestCase):
     
     @patch("time.sleep")
     def test_daemon_restart(self, mock_sleep):
-        #Mock daemon function
+        # Mock daemon function
         daemon_handler = get_daemon_handler()
         mock_daemon = Mock(side_effect=MockDaemonCall(daemon_handler, 2))
         daemon_handler.daemon = mock_daemon
@@ -51,7 +54,7 @@ class TestDaemon(AgentTestCase):
     @patch("time.sleep")
     @patch("azurelinuxagent.daemon.main.conf")
     @patch("azurelinuxagent.daemon.main.sys.exit")
-    def test_check_pid(self, mock_exit, mock_conf, mock_sleep):
+    def test_check_pid(self, mock_exit, mock_conf, _):
         daemon_handler = get_daemon_handler()
 
         mock_pid_file = os.path.join(self.tmp_dir, "pid")
@@ -65,7 +68,7 @@ class TestDaemon(AgentTestCase):
 
     @patch("azurelinuxagent.daemon.main.DaemonHandler.check_pid")
     @patch("azurelinuxagent.common.conf.get_fips_enabled", return_value=True)
-    def test_set_openssl_fips(self, mock_conf, mock_daemon):
+    def test_set_openssl_fips(self, _, __):
         daemon_handler = get_daemon_handler()
         daemon_handler.running = False
         with patch.dict("os.environ"):
@@ -75,13 +78,58 @@ class TestDaemon(AgentTestCase):
 
     @patch("azurelinuxagent.daemon.main.DaemonHandler.check_pid")
     @patch("azurelinuxagent.common.conf.get_fips_enabled", return_value=False)
-    def test_does_not_set_openssl_fips(self, mock_conf, mock_daemon):
+    def test_does_not_set_openssl_fips(self, _, __):
         daemon_handler = get_daemon_handler()
         daemon_handler.running = False
         with patch.dict("os.environ"):
             daemon_handler.run()
             self.assertFalse(OPENSSL_FIPS_ENVIRONMENT in os.environ)
-   
+
+    @patch('azurelinuxagent.ga.update.UpdateHandler.run_latest')
+    @patch('azurelinuxagent.pa.provision.default.ProvisionHandler.run')
+    @patch('azurelinuxagent.pa.provision.get_provision_handler', return_value=ProvisionHandler())
+    def test_daemon_agent_enabled(self, _, patch_run_provision, patch_run_latest):
+        """
+        Agent should run normally when no disable_agent is found
+        """
+
+        self.assertFalse(os.path.exists(conf.get_disable_agent_file_path()))
+        daemon_handler = get_daemon_handler()
+
+        def stop_daemon(child_args):
+            daemon_handler.running = False
+
+        patch_run_latest.side_effect = stop_daemon
+        daemon_handler.run()
+
+        self.assertEqual(1, patch_run_provision.call_count)
+        self.assertEqual(1, patch_run_latest.call_count)
+
+    @patch('azurelinuxagent.ga.update.UpdateHandler.run_latest', side_effect=AgentTestCase.fail)
+    @patch('azurelinuxagent.pa.provision.default.ProvisionHandler.run', side_effect=ProvisionHandler.write_agent_disabled)
+    @patch('azurelinuxagent.pa.provision.get_provision_handler', return_value=ProvisionHandler())
+    def test_daemon_agent_disabled(self, _, __, patch_run_latest):
+        """
+        Agent should provision, then sleep forever when disable_agent is found
+        """
+
+        # file is created by provisioning handler
+        self.assertFalse(os.path.exists(conf.get_disable_agent_file_path()))
+        daemon_handler = get_daemon_handler()
+
+        # we need to assert this thread will sleep forever, so fork it
+        daemon = Process(target=daemon_handler.run)
+        daemon.start()
+        daemon.join(timeout=5)
+
+        self.assertTrue(daemon.is_alive())
+        daemon.terminate()
+
+        # disable_agent was written, run_latest was not called
+        self.assertTrue(os.path.exists(conf.get_disable_agent_file_path()))
+        self.assertEqual(0, patch_run_latest.call_count)
+
+
 if __name__ == '__main__':
     unittest.main()
 
diff --git a/tests/data/ovf-env-2.xml b/tests/data/ovf-env-2.xml
new file mode 100644
index 00000000..5281d84b
--- /dev/null
+++ b/tests/data/ovf-env-2.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Environment xmlns="http://schemas.dmtf.org/ovf/environment/1" xmlns:oe="http://schemas.dmtf.org/ovf/environment/1" xmlns:wa="http://schemas.microsoft.com/windowsazure" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+    <wa:ProvisioningSection>
+      <wa:Version>1.0</wa:Version>
+      <LinuxProvisioningConfigurationSet xmlns="http://schemas.microsoft.com/windowsazure" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
+        <ConfigurationSetType>LinuxProvisioningConfiguration</ConfigurationSetType>
+        <HostName>HostName</HostName>
+        <UserName>UserName</UserName>
+        <UserPassword>UserPassword</UserPassword>
+        <DisableSshPasswordAuthentication>false</DisableSshPasswordAuthentication>
+        <SSH>
+          <PublicKeys>
+            <PublicKey>
+              <Fingerprint>EB0C0AB4B2D5FC35F2F0658D19F44C8283E2DD62</Fingerprint>
+              <Path>$HOME/UserName/.ssh/authorized_keys</Path>
+              <Value>ssh-rsa AAAANOTAREALKEY== foo@bar.local</Value>
+            </PublicKey>
+          </PublicKeys>
+          <KeyPairs>
+            <KeyPair>
+              <Fingerprint>EB0C0AB4B2D5FC35F2F0658D19F44C8283E2DD62</Fingerprint>
+              <Path>$HOME/UserName/.ssh/id_rsa</Path>
+            </KeyPair>
+          </KeyPairs>
+        </SSH>
+        <CustomData>CustomData</CustomData>
+      </LinuxProvisioningConfigurationSet>
+    </wa:ProvisioningSection>
+    <wa:PlatformSettingsSection>
+		<wa:Version>1.0</wa:Version>
+		<wa:PlatformSettings>
+			<wa:KmsServerHostname>kms.core.windows.net</wa:KmsServerHostname>
+			<wa:ProvisionGuestAgent>true</wa:ProvisionGuestAgent>
+			<wa:GuestAgentPackageName xsi:nil="true"/>
+			<wa:RetainWindowsPEPassInUnattend>true</wa:RetainWindowsPEPassInUnattend>
+			<wa:RetainOfflineServicingPassInUnattend>true</wa:RetainOfflineServicingPassInUnattend>
+			<wa:PreprovisionedVm>false</wa:PreprovisionedVm>
+		</wa:PlatformSettings>
+	</wa:PlatformSettingsSection>
+ </Environment>
diff --git a/tests/data/ovf-env-3.xml b/tests/data/ovf-env-3.xml
new file mode 100644
index 00000000..78129a0c
--- /dev/null
+++ b/tests/data/ovf-env-3.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Environment xmlns="http://schemas.dmtf.org/ovf/environment/1" xmlns:oe="http://schemas.dmtf.org/ovf/environment/1" xmlns:wa="http://schemas.microsoft.com/windowsazure" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+    <wa:ProvisioningSection>
+      <wa:Version>1.0</wa:Version>
+      <LinuxProvisioningConfigurationSet xmlns="http://schemas.microsoft.com/windowsazure" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
+        <ConfigurationSetType>LinuxProvisioningConfiguration</ConfigurationSetType>
+        <HostName>HostName</HostName>
+        <UserName>UserName</UserName>
+        <UserPassword>UserPassword</UserPassword>
+        <DisableSshPasswordAuthentication>false</DisableSshPasswordAuthentication>
+        <SSH>
+          <PublicKeys>
+            <PublicKey>
+              <Fingerprint>EB0C0AB4B2D5FC35F2F0658D19F44C8283E2DD62</Fingerprint>
+              <Path>$HOME/UserName/.ssh/authorized_keys</Path>
+              <Value>ssh-rsa AAAANOTAREALKEY== foo@bar.local</Value>
+            </PublicKey>
+          </PublicKeys>
+          <KeyPairs>
+            <KeyPair>
+              <Fingerprint>EB0C0AB4B2D5FC35F2F0658D19F44C8283E2DD62</Fingerprint>
+              <Path>$HOME/UserName/.ssh/id_rsa</Path>
+            </KeyPair>
+          </KeyPairs>
+        </SSH>
+        <CustomData>CustomData</CustomData>
+      </LinuxProvisioningConfigurationSet>
+    </wa:ProvisioningSection>
+    <wa:PlatformSettingsSection>
+		<wa:Version>1.0</wa:Version>
+		<wa:PlatformSettings>
+			<wa:KmsServerHostname>kms.core.windows.net</wa:KmsServerHostname>
+			<wa:ProvisionGuestAgent></wa:ProvisionGuestAgent>
+			<wa:GuestAgentPackageName xsi:nil="true"/>
+			<wa:RetainWindowsPEPassInUnattend>true</wa:RetainWindowsPEPassInUnattend>
+			<wa:RetainOfflineServicingPassInUnattend>true</wa:RetainOfflineServicingPassInUnattend>
+			<wa:PreprovisionedVm>false</wa:PreprovisionedVm>
+		</wa:PlatformSettings>
+	</wa:PlatformSettingsSection>
+ </Environment>
diff --git a/tests/data/ovf-env-4.xml b/tests/data/ovf-env-4.xml
new file mode 100644
index 00000000..4a22f5de
--- /dev/null
+++ b/tests/data/ovf-env-4.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Environment xmlns="http://schemas.dmtf.org/ovf/environment/1" xmlns:oe="http://schemas.dmtf.org/ovf/environment/1" xmlns:wa="http://schemas.microsoft.com/windowsazure" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+    <wa:ProvisioningSection>
+      <wa:Version>1.0</wa:Version>
+      <LinuxProvisioningConfigurationSet xmlns="http://schemas.microsoft.com/windowsazure" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
+        <ConfigurationSetType>LinuxProvisioningConfiguration</ConfigurationSetType>
+        <HostName>HostName</HostName>
+        <UserName>UserName</UserName>
+        <UserPassword>UserPassword</UserPassword>
+        <DisableSshPasswordAuthentication>false</DisableSshPasswordAuthentication>
+        <SSH>
+          <PublicKeys>
+            <PublicKey>
+              <Fingerprint>EB0C0AB4B2D5FC35F2F0658D19F44C8283E2DD62</Fingerprint>
+              <Path>$HOME/UserName/.ssh/authorized_keys</Path>
+              <Value>ssh-rsa AAAANOTAREALKEY== foo@bar.local</Value>
+            </PublicKey>
+          </PublicKeys>
+          <KeyPairs>
+            <KeyPair>
+              <Fingerprint>EB0C0AB4B2D5FC35F2F0658D19F44C8283E2DD62</Fingerprint>
+              <Path>$HOME/UserName/.ssh/id_rsa</Path>
+            </KeyPair>
+          </KeyPairs>
+        </SSH>
+        <CustomData>CustomData</CustomData>
+      </LinuxProvisioningConfigurationSet>
+    </wa:ProvisioningSection>
+    <wa:PlatformSettingsSection>
+		<wa:Version>1.0</wa:Version>
+		<wa:PlatformSettings>
+			<wa:KmsServerHostname>kms.core.windows.net</wa:KmsServerHostname>
+			<wa:ProvisionGuestAgent>bad data</wa:ProvisionGuestAgent>
+			<wa:GuestAgentPackageName xsi:nil="true"/>
+			<wa:RetainWindowsPEPassInUnattend>true</wa:RetainWindowsPEPassInUnattend>
+			<wa:RetainOfflineServicingPassInUnattend>true</wa:RetainOfflineServicingPassInUnattend>
+			<wa:PreprovisionedVm>false</wa:PreprovisionedVm>
+		</wa:PlatformSettings>
+	</wa:PlatformSettingsSection>
+ </Environment>
diff --git a/tests/data/test_waagent.conf b/tests/data/test_waagent.conf
index b779efc6..101cd320 100644
--- a/tests/data/test_waagent.conf
+++ b/tests/data/test_waagent.conf
@@ -11,6 +11,9 @@ FauxKey3=delalloc,rw,noatime,nobarrier,users,mode=777
 # Enable instance creation
 Provisioning.Enabled=y
 
+# Enable extension handling
+Extensions.Enabled=y
+
 # Rely on cloud-init to provision
 Provisioning.UseCloudInit=y
 
diff --git a/tests/ga/test_extension.py b/tests/ga/test_extension.py
index 9e6f3a44..6fc6b8c1 100644
--- a/tests/ga/test_extension.py
+++ b/tests/ga/test_extension.py
@@ -713,6 +713,20 @@ class TestExtension(AgentTestCase):
             ext_handler_instance.decide_version()
             self.assertEqual(expected_version, ext_handler.properties.version)
 
+    @patch('azurelinuxagent.common.conf.get_extensions_enabled', return_value=False)
+    def test_extensions_disabled(self, _, *args):
+        # test status is reported for no extensions
+        test_data = WireProtocolData(DATA_FILE_NO_EXT)
+        exthandlers_handler, protocol = self._create_mock(test_data, *args)
+        exthandlers_handler.run()
+        self._assert_no_handler_status(protocol.report_vm_status)
+
+        # test status is reported, but extensions are not processed
+        test_data = WireProtocolData(DATA_FILE)
+        exthandlers_handler, protocol = self._create_mock(test_data, *args)
+        exthandlers_handler.run()
+        self._assert_no_handler_status(protocol.report_vm_status)
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/tests/ga/test_update.py b/tests/ga/test_update.py
index a3aa7ae9..ac5039f6 100644
--- a/tests/ga/test_update.py
+++ b/tests/ga/test_update.py
@@ -1194,7 +1194,7 @@ class TestUpdate(UpdateTestCase):
         self._test_run_latest()
         self.assertEqual(0, mock_signal.call_count)
 
-    def _test_run(self, invocations=1, calls=[call.run()], enable_updates=False):
+    def _test_run(self, invocations=1, calls=[call.run()], enable_updates=False, sleep_interval=(3,)):
         conf.get_autoupdate_enabled = Mock(return_value=enable_updates)
 
         # Note:
@@ -1229,6 +1229,8 @@ class TestUpdate(UpdateTestCase):
                                 self.assertEqual(1, mock_ra_handler.call_count)
                                 self.assertEqual(mock_ra_handler.return_value.method_calls, calls)
                                 self.assertEqual(invocations, mock_sleep.call_count)
+                                if invocations > 0:
+                                    self.assertEqual(sleep_interval, mock_sleep.call_args[0])
                                 self.assertEqual(1, mock_monitor.call_count)
                                 self.assertEqual(1, mock_env.call_count)
                                 self.assertEqual(1, mock_exit.call_count)
@@ -1480,6 +1482,24 @@ class TestUpdate(UpdateTestCase):
         self.assertTrue(ga_manifest_2.allowed_versions[0] == '2.2.13')
         self.assertTrue(ga_manifest_2.allowed_versions[1] == '2.2.14')
 
+    @patch('azurelinuxagent.common.conf.get_extensions_enabled', return_value=False)
+    def test_update_happens_when_extensions_disabled(self, _):
+        """
+        Although the extension enabled config will not get checked
+        before an update is found, this test attempts to ensure that
+        behavior never changes.
+        """
+        self.update_handler._upgrade_available = Mock(return_value=True)
+        self._test_run(invocations=0, calls=[], enable_updates=True, sleep_interval=(300,))
+
+    @patch('azurelinuxagent.common.conf.get_extensions_enabled', return_value=False)
+    def test_interval_changes_when_extensions_disabled(self, _):
+        """
+        When extension processing is disabled, the goal state interval should be larger.
+        """
+        self.update_handler._upgrade_available = Mock(return_value=False)
+        self._test_run(invocations=15, calls=[call.run()] * 15, sleep_interval=(300,))
+
 
 class MonitorThreadTest(AgentTestCase):
     def setUp(self):
diff --git a/tests/pa/test_provision.py b/tests/pa/test_provision.py
index f62e9d42..0335bc9c 100644
--- a/tests/pa/test_provision.py
+++ b/tests/pa/test_provision.py
@@ -113,10 +113,77 @@ class TestProvision(AgentTestCase):
         self.assertEqual(1, deprovision_handler.run_changed_unique_id.call_count)
 
     @distros()
+    def test_provision_telemetry_pga_false(self,
+                                           distro_name,
+                                           distro_version,
+                                           distro_full_name):
+        """
+        ProvisionGuestAgent flag is 'false'
+        """
+        self._provision_test(distro_name,
+                             distro_version,
+                             distro_full_name,
+                             OVF_FILE_NAME,
+                             'false',
+                             True)
+
+    @distros()
+    def test_provision_telemetry_pga_true(self,
+                                          distro_name,
+                                          distro_version,
+                                          distro_full_name):
+        """
+        ProvisionGuestAgent flag is 'true'
+        """
+        self._provision_test(distro_name,
+                             distro_version,
+                             distro_full_name,
+                             'ovf-env-2.xml',
+                             'true',
+                             True)
+
+    @distros()
+    def test_provision_telemetry_pga_empty(self,
+                                           distro_name,
+                                           distro_version,
+                                           distro_full_name):
+        """
+        ProvisionGuestAgent flag is ''
+        """
+        self._provision_test(distro_name,
+                             distro_version,
+                             distro_full_name,
+                             'ovf-env-3.xml',
+                             'true',
+                             False)
+
+    @distros()
+    def test_provision_telemetry_pga_bad(self,
+                                         distro_name,
+                                         distro_version,
+                                         distro_full_name):
+        """
+        ProvisionGuestAgent flag is 'bad data'
+        """
+        self._provision_test(distro_name,
+                             distro_version,
+                             distro_full_name,
+                             'ovf-env-4.xml',
+                             'bad data',
+                             True)
+
     @patch('azurelinuxagent.common.osutil.default.DefaultOSUtil.get_instance_id',
-        return_value='B9F3C233-9913-9F42-8EB3-BA656DF32502')
-    def test_provision_telemetry_success(self, mock_util, distro_name, distro_version,
-                       distro_full_name):
+           return_value='B9F3C233-9913-9F42-8EB3-BA656DF32502')
+    @patch('azurelinuxagent.pa.provision.default.ProvisionHandler.write_agent_disabled')
+    def _provision_test(self,
+                        distro_name,
+                        distro_version,
+                        distro_full_name,
+                        ovf_file,
+                        provisionMessage,
+                        expect_success,
+                        patch_write_agent_disabled,
+                        patch_get_instance_id):
         """
         Assert that the agent issues two telemetry messages as part of a
         successful provisioning.
@@ -124,7 +191,8 @@ class TestProvision(AgentTestCase):
          1. Provision
          2. GuestState
         """
-        ph = get_provision_handler(distro_name, distro_version,
+        ph = get_provision_handler(distro_name,
+                                   distro_version,
                                    distro_full_name)
         ph.report_event = MagicMock()
         ph.reg_ssh_host_key = MagicMock(return_value='--thumprint--')
@@ -138,30 +206,43 @@ class TestProvision(AgentTestCase):
 
         conf.get_dvd_mount_point = Mock(return_value=self.tmp_dir)
         ovfenv_file = os.path.join(self.tmp_dir, OVF_FILE_NAME)
-        ovfenv_data = load_data("ovf-env.xml")
+        ovfenv_data = load_data(ovf_file)
         fileutil.write_file(ovfenv_file, ovfenv_data)
 
         ph.run()
 
-        self.assertEqual(2, ph.report_event.call_count)
-        positional_args, kw_args = ph.report_event.call_args_list[0]
-        # [call('Provisioning succeeded (146473.68s)', duration=65, is_success=True)]
-        self.assertTrue(re.match(r'Provisioning succeeded \(\d+\.\d+s\)', positional_args[0]) is not None)
-        self.assertTrue(isinstance(kw_args['duration'], int))
-        self.assertTrue(kw_args['is_success'])
-
-        positional_args, kw_args = ph.report_event.call_args_list[1]
-        self.assertTrue(kw_args['operation'] == 'ProvisionGuestAgent')
-        self.assertTrue(kw_args['message'] == 'false')
-        self.assertTrue(kw_args['is_success'])
+        if expect_success:
+            self.assertEqual(2, ph.report_event.call_count)
+            positional_args, kw_args = ph.report_event.call_args_list[0]
+            # [call('Provisioning succeeded (146473.68s)', duration=65, is_success=True)]
+            self.assertTrue(re.match(r'Provisioning succeeded \(\d+\.\d+s\)', positional_args[0]) is not None)
+            self.assertTrue(isinstance(kw_args['duration'], int))
+            self.assertTrue(kw_args['is_success'])
+
+            positional_args, kw_args = ph.report_event.call_args_list[1]
+            self.assertTrue(kw_args['operation'] == 'ProvisionGuestAgent')
+            self.assertTrue(kw_args['message'] == provisionMessage)
+            self.assertTrue(kw_args['is_success'])
+
+            expected_disabled = True if provisionMessage == 'false' else False
+            self.assertTrue(patch_write_agent_disabled.call_count == expected_disabled)
+
+        else:
+            self.assertEqual(1, ph.report_event.call_count)
+            positional_args, kw_args = ph.report_event.call_args_list[0]
+            # [call(u'[ProtocolError] Failed to validate OVF: ProvisionGuestAgent not found')]
+            self.assertTrue('Failed to validate OVF: ProvisionGuestAgent not found' in positional_args[0])
+            self.assertFalse(kw_args['is_success'])
 
     @distros()
     @patch(
         'azurelinuxagent.common.osutil.default.DefaultOSUtil.get_instance_id',
         return_value='B9F3C233-9913-9F42-8EB3-BA656DF32502')
-    def test_provision_telemetry_fail(self, mock_util, distro_name,
-                                         distro_version,
-                                         distro_full_name):
+    def test_provision_telemetry_fail(self,
+                                      mock_util,
+                                      distro_name,
+                                      distro_version,
+                                      distro_full_name):
         """
         Assert that the agent issues one telemetry message as part of a
         failed provisioning.
@@ -188,7 +269,48 @@ class TestProvision(AgentTestCase):
 
         ph.run()
         ph.report_event.assert_called_once_with(
-            "[ProvisionError] --unit-test--")
+            '[ProvisionError] --unit-test--', is_success=False)
+
+    @patch('azurelinuxagent.pa.provision.default.ProvisionHandler.write_agent_disabled')
+    @distros()
+    def test_handle_provision_guest_agent(self,
+                                          patch_write_agent_disabled,
+                                          distro_name,
+                                          distro_version,
+                                          distro_full_name):
+        ph = get_provision_handler(distro_name,
+                                   distro_version,
+                                   distro_full_name)
+
+        patch_write_agent_disabled.call_count = 0
+
+        ph.handle_provision_guest_agent(provision_guest_agent='false')
+        self.assertEqual(1, patch_write_agent_disabled.call_count)
+
+        ph.handle_provision_guest_agent(provision_guest_agent='False')
+        self.assertEqual(2, patch_write_agent_disabled.call_count)
+
+        ph.handle_provision_guest_agent(provision_guest_agent='FALSE')
+        self.assertEqual(3, patch_write_agent_disabled.call_count)
+
+        ph.handle_provision_guest_agent(provision_guest_agent='')
+        self.assertEqual(3, patch_write_agent_disabled.call_count)
+
+        ph.handle_provision_guest_agent(provision_guest_agent=' ')
+        self.assertEqual(3, patch_write_agent_disabled.call_count)
+
+        ph.handle_provision_guest_agent(provision_guest_agent=None)
+        self.assertEqual(3, patch_write_agent_disabled.call_count)
+
+        ph.handle_provision_guest_agent(provision_guest_agent='true')
+        self.assertEqual(3, patch_write_agent_disabled.call_count)
+
+        ph.handle_provision_guest_agent(provision_guest_agent='True')
+        self.assertEqual(3, patch_write_agent_disabled.call_count)
+
+        ph.handle_provision_guest_agent(provision_guest_agent='TRUE')
+        self.assertEqual(3, patch_write_agent_disabled.call_count)
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/tests/test_agent.py b/tests/test_agent.py
index 0f71259a..81e64bf0 100644
--- a/tests/test_agent.py
+++ b/tests/test_agent.py
@@ -30,6 +30,7 @@ DVD.MountPoint = /mnt/cdrom/secure
 DetectScvmmEnv = False
 EnableOverProvisioning = True
 Extension.LogDir = /var/log/azure
+Extensions.Enabled = True
 HttpProxy.Host = None
 HttpProxy.Port = None
 Lib.Dir = /var/lib/waagent
diff --git a/tests/utils/test_process_util.py b/tests/utils/test_process_util.py
index a950e556..55d65440 100644
--- a/tests/utils/test_process_util.py
+++ b/tests/utils/test_process_util.py
@@ -26,6 +26,7 @@ import sys
 process_target = "{0}/process_target.sh".format(os.path.abspath(os.path.join(__file__, os.pardir)))
 process_cmd_template = "{0} -o '{1}' -e '{2}'"
 
+EXTENSION_ERROR_CODE = 1000
 
 class TestProcessUtils(AgentTestCase):
     def test_format_stdout_stderr00(self):
@@ -138,13 +139,14 @@ class TestProcessUtils(AgentTestCase):
                                    preexec_fn=os.setsid)
 
         try:
-            capture_from_process(process, 'sleep 20', 10)
+            capture_from_process(process, 'sleep 20', 10, EXTENSION_ERROR_CODE)
             self.fail('Timeout exception was expected')
         except ExtensionError as e:
             body = str(e)
             self.assertTrue('Timeout(10)' in body)
             self.assertTrue('Iteration 9' in body)
             self.assertFalse('Iteration 11' in body)
+            self.assertEqual(EXTENSION_ERROR_CODE, e.code)
         except Exception as gen_ex:
             self.fail('Unexpected exception: {0}'.format(gen_ex))
 
@@ -240,10 +242,10 @@ class TestProcessUtils(AgentTestCase):
                                    env=os.environ)
 
         if sys.version_info < (2, 7):
-            self.assertRaises(ExtensionError, capture_from_process, process, cmd, 10)
+            self.assertRaises(ExtensionError, capture_from_process, process, cmd, 10, EXTENSION_ERROR_CODE)
         else:
             with self.assertRaises(ExtensionError) as ee:
-                capture_from_process(process, cmd, 10)
+                capture_from_process(process, cmd, 10, EXTENSION_ERROR_CODE)
 
             body = str(ee.exception)
             if sys.version_info >= (3, 2):
@@ -251,6 +253,8 @@ class TestProcessUtils(AgentTestCase):
             else:
                 self.assertRegexpMatches(body, "process group")
 
+            self.assertEqual(EXTENSION_ERROR_CODE, ee.exception.code)
+
 
 if __name__ == '__main__':
     unittest.main()