Repository: Azure/WALinuxAgent
Pull request #1289 (2018-08-07 21:03:21)

Agentless
## Description

Addresses #838

This introduces two new modes of operation for the agent:
- extensions disabled
- agent disabled

### Extensions disabled

This allows an end user to reduce the agent overhead when extensions are not required. This serves customers who have provisioned VMs already and cannot re-provision without the agent. To enable this mode, a new config option `Extensions.Enabled=y|n` is provided, which the user may set. 

When `y` (default), the agent runs without any changes.

When `n`, the goal state interval is reduced to 5 minutes, and extension configuration is not processed. This should help customers who do not want to run the agent, but still need the VM to report status, handle JIT requests, and so on.

### Agent disabled

As of Azure.Compute 2018-06-01, a new option is provided in the ARM API to disable the Linux guest agent completely. Documentation on that is forthcoming. This API will end up setting the `ProvisionGuestAgent` flag in `ovf-env.xml`, which the agent will honor with this change. 

If that flag is set to `false`, the agent will write `/var/lib/waagent/disable_agent`, and if that file exists, sleep forever. No status reports or goal state handling of any kind are processed. 

Once cloud-init supports this flag, the agent service will be disabled and/or removed on provision.

---

### PR information
- [x] The title of the PR is clear and informative.
- [x] 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).
- [x] 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.
- [x] 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).

hglkrijger: /cc @danielsollondon

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/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..cdfa825a 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")
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/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