Repository: Azure/WALinuxAgent
Pull request #692 (2017-05-02 19:34:45)

Addressed issues #687, #604, and #531
Address issues #687, #604, and #531.
Signed-off-by: Brendan Dixon <brendandixon@me.com>

brendandixon: @hglkrijger Not sure. This will preserve the Ubuntu behavior and lay the groundwork for the next distribution. Issues will come out in testing / design.

Patch:
diff --git a/README.md b/README.md
index 36f6b881..fd160737 100644
--- a/README.md
+++ b/README.md
@@ -170,6 +170,7 @@ waagent. A sample configuration file is shown below:
 
 ```
 Provisioning.Enabled=y
+Provisioning.UseCloudInit=n
 Provisioning.DeleteRootPassword=n
 Provisioning.RegenerateSshHostKeyPair=y
 Provisioning.SshHostKeyPairType=rsa
@@ -187,7 +188,9 @@ ResourceDisk.SwapSizeMB=0
 LBProbeResponder=y
 Logs.Verbose=n
 OS.RootDeviceScsiTimeout=300
+OS.FIPSEnabled=n
 OS.OpensslPath=None
+OS.SshDir=/etc/ssh
 HttpProxy.Host=None
 HttpProxy.Port=None
 ```
@@ -208,6 +211,16 @@ agent. Valid values are "y" or "n". If provisioning is disabled, SSH host and
 user keys in the image are preserved and any configuration specified in the
 Azure provisioning API is ignored.
 
+# __Provisioning.UseCloudInit__
+_Type: Boolean_
+_Default: n_
+
+This options enables / disables support for provisioning by means of cloud-init.
+When true ("y"), the agent will wait for cloud-init to complete before installing
+extensions and processing the latest goal state. _Provisioning.Enabled_ must be
+disabled ("n") for this option to have an effect. Setting _Provisioning.Enabled_ to
+true ("y") overrides this option and runs the built-in agent provisioning code.
+
 * __Provisioning.DeleteRootPassword__  
 _Type: Boolean_   
 _Default: n_  
@@ -340,6 +353,15 @@ _Default: n_
 If set, the agent will attempt to install and then load an RDMA kernel driver
 that matches the version of the firmware on the underlying hardware.
 
+* __OS.FIPSEnabled__
+_Type: Boolean_
+_Default: n_
+
+If set, the agent will emit into the environment "OPENSSL_FIPS=1" when executing
+OpenSSL commands. This signals OpenSSL to use any installed FIPS-compliant libraries.
+Note that the agent itself has no FIPS-specific code. _If no FIPS-compliant are
+installed, then enabling this option will cause all OpenSSL commands to fail._
+
 * __OS.RootDeviceScsiTimeout__   
 _Type: Integer_    
 _Default: 300_   
@@ -354,6 +376,13 @@ _Default: None_
 This can be used to specify an alternate path for the openssl binary to use for
 cryptographic operations.
 
+* __OS.SshDir__
+_Type: String_
+_Default: "/etc/ssh"_
+
+This option can be used to override the normal location of the SSH configuration
+directory.
+
 * __HttpProxy.Host, HttpProxy.Port__   
 _Type: String_   
 _Default: None_   
diff --git a/azurelinuxagent/agent.py b/azurelinuxagent/agent.py
index bd6dd20c..e5900e20 100644
--- a/azurelinuxagent/agent.py
+++ b/azurelinuxagent/agent.py
@@ -21,6 +21,8 @@
 Module agent
 """
 
+from __future__ import print_function
+
 import os
 import sys
 import re
@@ -37,7 +39,7 @@ from azurelinuxagent.common.version import AGENT_NAME, AGENT_LONG_VERSION, \
 from azurelinuxagent.common.osutil import get_osutil
 
 class Agent(object):
-    def __init__(self, verbose):
+    def __init__(self, verbose, conf_file_path=None):
         """
         Initialize agent running environment.
         """
@@ -47,7 +49,8 @@ class Agent(object):
         logger.add_logger_appender(logger.AppenderType.STDOUT, level)
 
         #Init config
-        conf_file_path = self.osutil.get_agent_conf_file_path()
+        if conf_file_path is None:
+            conf_file_path = self.osutil.get_agent_conf_file_path()
         conf.load_conf_from_file(conf_file_path)
 
         #Init log
@@ -113,7 +116,7 @@ def main(args=[]):
     """
     if len(args) <= 0:
         args = sys.argv[1:]
-    command, force, verbose = parse_args(args)
+    command, force, verbose, conf_file_path = parse_args(args)
     if command == "version":
         version()
     elif command == "help":
@@ -122,7 +125,7 @@ def main(args=[]):
         start()
     else:
         try:
-            agent = Agent(verbose)
+            agent = Agent(verbose, conf_file_path=conf_file_path)
             if command == "deprovision+user":
                 agent.deprovision(force, deluser=True)
             elif command == "deprovision":
@@ -147,8 +150,18 @@ def parse_args(sys_args):
     cmd = "help"
     force = False
     verbose = False
+    conf_file_path = None
     for a in sys_args:
-        if re.match("^([-/]*)deprovision\\+user", a):
+        m = re.match("^(?:[-/]*)configuration-path:([\w/\.\-_]+)", a)
+        if not m is None:
+            conf_file_path = m.group(1)
+            if not os.path.exists(conf_file_path):
+                print("Error: Configuration file {0} does not exist".format(
+                        conf_file_path), file=sys.stderr)
+                usage()
+                sys.exit(1)
+        
+        elif re.match("^([-/]*)deprovision\\+user", a):
             cmd = "deprovision+user"
         elif re.match("^([-/]*)deprovision", a):
             cmd = "deprovision"
@@ -171,7 +184,7 @@ def parse_args(sys_args):
         else:
             cmd = "help"
             break
-    return cmd, force, verbose
+    return cmd, force, verbose, conf_file_path
 
 def version():
     """
@@ -191,6 +204,7 @@ def usage():
     """
     print("")
     print((("usage: {0} [-verbose] [-force] [-help] "
+           "-configuration-path:<path to configuration file>"
            "-deprovision[+user]|-register-service|-version|-daemon|-start|"
            "-run-exthandlers]"
            "").format(sys.argv[0])))
diff --git a/azurelinuxagent/common/conf.py b/azurelinuxagent/common/conf.py
index 11b0aad4..a8a22172 100644
--- a/azurelinuxagent/common/conf.py
+++ b/azurelinuxagent/common/conf.py
@@ -21,13 +21,15 @@
 Module conf loads and parses configuration file
 """
 import os
+import os.path
+
 import azurelinuxagent.common.utils.fileutil as fileutil
 from azurelinuxagent.common.exception import AgentConfigError
 
 
 class ConfigurationProvider(object):
     """
-    Parse amd store key:values in /etc/waagent.conf.
+    Parse and store key:values in /etc/waagent.conf.
     """
 
     def __init__(self):
@@ -119,34 +121,43 @@ def get_fips_enabled(conf=__conf__):
 def get_openssl_cmd(conf=__conf__):
     return conf.get("OS.OpensslPath", "/usr/bin/openssl")
 
+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 conf.get("OS.SshdConfigPath", "/etc/ssh/sshd_config")
+    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__):
     return conf.get("Provisioning.SshHostKeyPairType", "rsa")
 
-
 def get_provision_enabled(conf=__conf__):
     return conf.get_switch("Provisioning.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)
diff --git a/azurelinuxagent/pa/deprovision/default.py b/azurelinuxagent/pa/deprovision/default.py
index ced87eef..d2a00898 100644
--- a/azurelinuxagent/pa/deprovision/default.py
+++ b/azurelinuxagent/pa/deprovision/default.py
@@ -17,13 +17,16 @@
 # Requires Python 2.4+ and Openssl 1.0+
 #
 
+import os.path
 import signal
 import sys
+
 import azurelinuxagent.common.conf as conf
-from azurelinuxagent.common.exception import ProtocolError
-from azurelinuxagent.common.future import read_input
 import azurelinuxagent.common.utils.fileutil as fileutil
 import azurelinuxagent.common.utils.shellutil as shellutil
+
+from azurelinuxagent.common.exception import ProtocolError
+from azurelinuxagent.common.future import read_input
 from azurelinuxagent.common.osutil import get_osutil
 from azurelinuxagent.common.protocol import get_protocol_util
 
@@ -68,7 +71,7 @@ class DeprovisionHandler(object):
     def regen_ssh_host_key(self, warnings, actions):
         warnings.append("WARNING! All SSH host key pairs will be deleted.")
         actions.append(DeprovisionAction(fileutil.rm_files,
-                                         ['/etc/ssh/ssh_host_*key*']))
+                        [conf.get_ssh_key_glob()]))
 
     def stop_agent_service(self, warnings, actions):
         warnings.append("WARNING! The waagent service will be stopped.")
diff --git a/azurelinuxagent/pa/provision/ubuntu.py b/azurelinuxagent/pa/provision/cloudinit.py
similarity index 90%
rename from azurelinuxagent/pa/provision/ubuntu.py
rename to azurelinuxagent/pa/provision/cloudinit.py
index 359c52bf..69b0bc3f 100644
--- a/azurelinuxagent/pa/provision/ubuntu.py
+++ b/azurelinuxagent/pa/provision/cloudinit.py
@@ -23,27 +23,24 @@ import time
 import azurelinuxagent.common.conf as conf
 import azurelinuxagent.common.logger as logger
 import azurelinuxagent.common.utils.fileutil as fileutil
+
 from azurelinuxagent.common.exception import ProvisionError, ProtocolError
 from azurelinuxagent.common.future import ustr
 from azurelinuxagent.common.protocol import OVF_FILE_NAME
 from azurelinuxagent.common.protocol.ovfenv import OvfEnv
 from azurelinuxagent.pa.provision.default import ProvisionHandler
 
-"""
-On ubuntu image, provision could be disabled.
-"""
-
 
-class UbuntuProvisionHandler(ProvisionHandler):
+class CloudInitProvisionHandler(ProvisionHandler):
     def __init__(self):
-        super(UbuntuProvisionHandler, self).__init__()
+        super(CloudInitProvisionHandler, self).__init__()
 
     def run(self):
         # If provision is enabled, run default provision handler
         if conf.get_provision_enabled():
-            logger.warn("Provisioning flag is enabled, this is not typical"
-                        "in Ubuntu, please ensure your config is correct.")
-            super(UbuntuProvisionHandler, self).run()
+            logger.warn("Provisioning flag is enabled, which overrides using "
+                        "cloud-init; running the default provisioning code")
+            super(CloudInitProvisionHandler, self).run()
             return
 
         provisioned = os.path.join(conf.get_lib_dir(), "provisioned")
@@ -51,7 +48,7 @@ class UbuntuProvisionHandler(ProvisionHandler):
             logger.info("Provisioning already completed, skipping.")
             return
 
-        logger.info("Running Ubuntu provisioning handler")
+        logger.info("Running CloudInit provisioning handler")
         self.wait_for_ovfenv()
         self.protocol_util.get_protocol()
         self.report_not_ready("Provisioning", "Starting")
@@ -102,12 +99,12 @@ class UbuntuProvisionHandler(ProvisionHandler):
         Wait for cloud-init to generate ssh host key
         """
         keypair_type = conf.get_ssh_host_keypair_type()
-        path = '/etc/ssh/ssh_host_{0}_key.pub'.format(keypair_type)
+        path = conf.get_ssh_key_public_path()
         for retry in range(0, max_retry):
             if os.path.isfile(path):
                 logger.info("ssh host key found at: {0}".format(path))
                 try:
-                    thumbprint = self.get_ssh_host_key_thumbprint(keypair_type, chk_err=False)
+                    thumbprint = self.get_ssh_host_key_thumbprint(chk_err=False)
                     logger.info("Thumbprint obtained from : {0}".format(path))
                     return thumbprint
                 except ProvisionError:
diff --git a/azurelinuxagent/pa/provision/default.py b/azurelinuxagent/pa/provision/default.py
index d11e409e..af27967c 100644
--- a/azurelinuxagent/pa/provision/default.py
+++ b/azurelinuxagent/pa/provision/default.py
@@ -20,19 +20,20 @@ Provision handler
 """
 
 import os
-
+import os.path
 import re
 
+import azurelinuxagent.common.conf as conf
 import azurelinuxagent.common.logger as logger
+import azurelinuxagent.common.utils.shellutil as shellutil
+import azurelinuxagent.common.utils.fileutil as fileutil
+
 from azurelinuxagent.common.future import ustr
-import azurelinuxagent.common.conf as conf
 from azurelinuxagent.common.event import add_event, WALAEventOperation
 from azurelinuxagent.common.exception import ProvisionError, ProtocolError, \
     OSUtilError
-from azurelinuxagent.common.protocol.restapi import ProvisionStatus
-import azurelinuxagent.common.utils.shellutil as shellutil
-import azurelinuxagent.common.utils.fileutil as fileutil
 from azurelinuxagent.common.osutil import get_osutil
+from azurelinuxagent.common.protocol.restapi import ProvisionStatus
 from azurelinuxagent.common.protocol import get_protocol_util
 
 CUSTOM_DATA_FILE = "CustomData"
@@ -104,14 +105,14 @@ class ProvisionHandler(object):
     def reg_ssh_host_key(self):
         keypair_type = conf.get_ssh_host_keypair_type()
         if conf.get_regenerate_ssh_host_key():
-            fileutil.rm_files("/etc/ssh/ssh_host_*key*")
-            keygen_cmd = "ssh-keygen -N '' -t {0} -f /etc/ssh/ssh_host_{1}_key"
-            shellutil.run(keygen_cmd.format(keypair_type, keypair_type))
-        thumbprint = self.get_ssh_host_key_thumbprint(keypair_type)
-        return thumbprint
-
-    def get_ssh_host_key_thumbprint(self, keypair_type, chk_err=True):
-        cmd = "ssh-keygen -lf /etc/ssh/ssh_host_{0}_key.pub".format(keypair_type)
+            fileutil.rm_files(conf.get_ssh_key_glob())
+            keygen_cmd = "ssh-keygen -N '' -t {0} -f {1}"
+            shellutil.run(keygen_cmd.format(keypair_type,
+                        conf.get_ssh_key_private_path()))
+        return self.get_ssh_host_key_thumbprint()
+
+    def get_ssh_host_key_thumbprint(self, chk_err=True):
+        cmd = "ssh-keygen -lf {0}".format(conf.get_ssh_key_public_path())
         ret = shellutil.run_get_output(cmd, chk_err=chk_err)
         if ret[0] == 0:
             return ret[1].rstrip().split()[1].replace(':', '')
diff --git a/azurelinuxagent/pa/provision/factory.py b/azurelinuxagent/pa/provision/factory.py
index 9bbe35cc..d87765f3 100644
--- a/azurelinuxagent/pa/provision/factory.py
+++ b/azurelinuxagent/pa/provision/factory.py
@@ -15,18 +15,22 @@
 # Requires Python 2.4+ and Openssl 1.0+
 #
 
+import azurelinuxagent.common.conf as conf
 import azurelinuxagent.common.logger as logger
+
 from azurelinuxagent.common.utils.textutil import Version
 from azurelinuxagent.common.version import DISTRO_NAME, DISTRO_VERSION, \
                                      DISTRO_FULL_NAME
+
 from .default import ProvisionHandler
-from .ubuntu import UbuntuProvisionHandler
+from .cloudinit import CloudInitProvisionHandler
 
 def get_provision_handler(distro_name=DISTRO_NAME, 
                             distro_version=DISTRO_VERSION,
                             distro_full_name=DISTRO_FULL_NAME):
-    if distro_name == "ubuntu":
-        return UbuntuProvisionHandler()
+
+    if conf.get_provision_cloudinit():
+        return CloudInitProvisionHandler()
 
     return ProvisionHandler()
 
diff --git a/config/alpine/waagent.conf b/config/alpine/waagent.conf
index 3be79f95..95004c37 100644
--- a/config/alpine/waagent.conf
+++ b/config/alpine/waagent.conf
@@ -5,6 +5,9 @@
 # Enable instance creation
 Provisioning.Enabled=y
 
+# Rely on cloud-init to provision
+Provisioning.UseCloudInit=n
+
 # Password authentication for root account will be unavailable.
 Provisioning.DeleteRootPassword=y
 
@@ -65,6 +68,9 @@ OS.RootDeviceScsiTimeout=300
 # If "None", the system default version is used.
 OS.OpensslPath=None
 
+# Set the path to SSH keys and configuration files
+OS.SshDir=/etc/ssh
+
 # Enable or disable goal state processing auto-update, default is enabled
 # AutoUpdate.Enabled=y
 
diff --git a/config/arch/waagent.conf b/config/arch/waagent.conf
index 826cfd99..88ee0f8b 100644
--- a/config/arch/waagent.conf
+++ b/config/arch/waagent.conf
@@ -5,6 +5,9 @@
 # Enable instance creation
 Provisioning.Enabled=y
 
+# Rely on cloud-init to provision
+Provisioning.UseCloudInit=n
+
 # Password authentication for root account will be unavailable.
 Provisioning.DeleteRootPassword=n
 
@@ -66,6 +69,9 @@ OS.RootDeviceScsiTimeout=300
 # If "None", the system default version is used.
 OS.OpensslPath=None
 
+# Set the path to SSH keys and configuration files
+OS.SshDir=/etc/ssh
+
 # If set, agent will use proxy server to access internet
 #HttpProxy.Host=None
 #HttpProxy.Port=None
diff --git a/config/bigip/waagent.conf b/config/bigip/waagent.conf
index 33ea9cb2..65271e7d 100644
--- a/config/bigip/waagent.conf
+++ b/config/bigip/waagent.conf
@@ -16,6 +16,9 @@ Role.TopologyConsumer=None
 # Enable instance creation
 Provisioning.Enabled=y
 
+# Rely on cloud-init to provision
+Provisioning.UseCloudInit=n
+
 # Password authentication for root account will be unavailable.
 Provisioning.DeleteRootPassword=y
 
@@ -60,6 +63,9 @@ OS.RootDeviceScsiTimeout=300
 # If "None", the system default version is used.
 OS.OpensslPath=None
 
+# Set the path to SSH keys and configuration files
+OS.SshDir=/etc/ssh
+
 # Specify location of waagent lib dir on BIG-IP
 Lib.Dir=/shared/vadc/azure/waagent/
 
diff --git a/config/clearlinux/waagent.conf b/config/clearlinux/waagent.conf
index 6a4f1feb..87d3f540 100644
--- a/config/clearlinux/waagent.conf
+++ b/config/clearlinux/waagent.conf
@@ -16,6 +16,9 @@ Role.TopologyConsumer=None
 # Enable instance creation
 Provisioning.Enabled=y
 
+# Rely on cloud-init to provision
+Provisioning.UseCloudInit=n
+
 # Password authentication for root account will be unavailable.
 Provisioning.DeleteRootPassword=y
 
@@ -65,6 +68,9 @@ OS.RootDeviceScsiTimeout=300
 # If "None", the system default version is used.
 OS.OpensslPath=None
 
+# Set the path to SSH keys and configuration files
+OS.SshDir=/etc/ssh
+
 # Enable or disable self-update, default is enabled
 AutoUpdate.Enabled=y
 AutoUpdate.GAFamily=Prod
diff --git a/config/coreos/waagent.conf b/config/coreos/waagent.conf
index 3d93c269..2c73e395 100644
--- a/config/coreos/waagent.conf
+++ b/config/coreos/waagent.conf
@@ -5,6 +5,9 @@
 # Enable instance creation
 Provisioning.Enabled=y
 
+# Rely on cloud-init to provision
+Provisioning.UseCloudInit=n
+
 # Password authentication for root account will be unavailable.
 Provisioning.DeleteRootPassword=n
 
@@ -60,6 +63,9 @@ Logs.Verbose=n
 # Is FIPS enabled
 OS.FIPSEnabled=n
 
+# Set the path to SSH keys and configuration files
+OS.SshDir=/etc/ssh
+
 # Root device timeout in seconds.
 OS.RootDeviceScsiTimeout=300
 
diff --git a/config/freebsd/waagent.conf b/config/freebsd/waagent.conf
index 7312667e..60772939 100644
--- a/config/freebsd/waagent.conf
+++ b/config/freebsd/waagent.conf
@@ -5,6 +5,9 @@
 # Enable instance creation
 Provisioning.Enabled=y
 
+# Rely on cloud-init to provision
+Provisioning.UseCloudInit=n
+
 # Password authentication for root account will be unavailable.
 Provisioning.DeleteRootPassword=y
 
@@ -60,6 +63,9 @@ OS.RootDeviceScsiTimeout=300
 # If "None", the system default version is used.
 OS.OpensslPath=None
 
+# Set the path to SSH keys and configuration files
+OS.SshDir=/etc/ssh
+
 OS.PasswordPath=/etc/master.passwd
 
 OS.SudoersDir=/usr/local/etc/sudoers.d
diff --git a/config/suse/waagent.conf b/config/suse/waagent.conf
index edb1ce3a..813990ae 100644
--- a/config/suse/waagent.conf
+++ b/config/suse/waagent.conf
@@ -5,6 +5,9 @@
 # Enable instance creation
 Provisioning.Enabled=y
 
+# Rely on cloud-init to provision
+Provisioning.UseCloudInit=n
+
 # Password authentication for root account will be unavailable.
 Provisioning.DeleteRootPassword=y
 
@@ -66,6 +69,9 @@ OS.RootDeviceScsiTimeout=300
 # If "None", the system default version is used.
 OS.OpensslPath=None
 
+# Set the path to SSH keys and configuration files
+OS.SshDir=/etc/ssh
+
 # If set, agent will use proxy server to access internet
 #HttpProxy.Host=None
 #HttpProxy.Port=None
diff --git a/config/ubuntu/waagent.conf b/config/ubuntu/waagent.conf
index 3ad1e89a..0c92daa2 100644
--- a/config/ubuntu/waagent.conf
+++ b/config/ubuntu/waagent.conf
@@ -5,6 +5,9 @@
 # Enable instance creation
 Provisioning.Enabled=n
 
+# Rely on cloud-init to provision
+Provisioning.UseCloudInit=y
+
 # Password authentication for root account will be unavailable.
 Provisioning.DeleteRootPassword=y
 
@@ -66,6 +69,9 @@ OS.RootDeviceScsiTimeout=300
 # If "None", the system default version is used.
 OS.OpensslPath=None
 
+# Set the path to SSH keys and configuration files
+OS.SshDir=/etc/ssh
+
 # If set, agent will use proxy server to access internet
 #HttpProxy.Host=None
 #HttpProxy.Port=None
diff --git a/config/waagent.conf b/config/waagent.conf
index 02b4bed0..bf853511 100644
--- a/config/waagent.conf
+++ b/config/waagent.conf
@@ -5,6 +5,9 @@
 # Enable instance creation
 Provisioning.Enabled=y
 
+# Rely on cloud-init to provision
+Provisioning.UseCloudInit=n
+
 # Password authentication for root account will be unavailable.
 Provisioning.DeleteRootPassword=y
 
@@ -63,6 +66,9 @@ OS.RootDeviceScsiTimeout=300
 # If "None", the system default version is used.
 OS.OpensslPath=None
 
+# Set the path to SSH keys and configuration files
+OS.SshDir=/etc/ssh
+
 # If set, agent will use proxy server to access internet
 #HttpProxy.Host=None
 #HttpProxy.Port=None


Test patch:
diff --git a/tests/common/test_conf.py b/tests/common/test_conf.py
new file mode 100644
index 00000000..03cf290d
--- /dev/null
+++ b/tests/common/test_conf.py
@@ -0,0 +1,57 @@
+# Copyright 2014 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.4+ and Openssl 1.0+
+#
+
+import mock
+import os.path
+
+from azurelinuxagent.common.conf import *
+
+from tests.tools import *
+
+
+class TestConf(AgentTestCase):
+    def setUp(self):
+        AgentTestCase.setUp(self)
+        self.conf = ConfigurationProvider()
+        load_conf_from_file(
+                os.path.join(data_dir, "test_waagent.conf"),
+                self.conf)
+
+    def test_get_ssh_dir(self):
+        self.assertTrue(get_ssh_dir(self.conf).startswith("/notareal/path"))
+
+    def test_get_sshd_conf_file_path(self):
+        self.assertTrue(get_sshd_conf_file_path(
+            self.conf).startswith("/notareal/path"))
+
+    def test_get_ssh_key_glob(self):
+        self.assertTrue(get_ssh_key_glob(
+            self.conf).startswith("/notareal/path"))
+
+    def test_get_ssh_key_private_path(self):
+        self.assertTrue(get_ssh_key_private_path(
+            self.conf).startswith("/notareal/path"))
+
+    def test_get_ssh_key_public_path(self):
+        self.assertTrue(get_ssh_key_public_path(
+            self.conf).startswith("/notareal/path"))
+
+    def test_get_fips_enabled(self):
+        self.assertTrue(get_fips_enabled(self.conf))
+
+    def test_get_provision_cloudinit(self):
+        self.assertTrue(get_provision_cloudinit(self.conf))
diff --git a/tests/data/test_waagent.conf b/tests/data/test_waagent.conf
new file mode 100644
index 00000000..30f94b0c
--- /dev/null
+++ b/tests/data/test_waagent.conf
@@ -0,0 +1,106 @@
+#
+# Microsoft Azure Linux Agent Configuration
+#
+
+# Enable instance creation
+Provisioning.Enabled=y
+
+# Rely on cloud-init to provision
+Provisioning.UseCloudInit=y
+
+# Password authentication for root account will be unavailable.
+Provisioning.DeleteRootPassword=y
+
+# Generate fresh host key pair.
+Provisioning.RegenerateSshHostKeyPair=y
+
+# Supported values are "rsa", "dsa" and "ecdsa".
+Provisioning.SshHostKeyPairType=rsa
+
+# Monitor host name changes and publish changes via DHCP requests.
+Provisioning.MonitorHostName=y
+
+# Decode CustomData from Base64.
+Provisioning.DecodeCustomData=n
+
+# Execute CustomData after provisioning.
+Provisioning.ExecuteCustomData=n
+
+# Algorithm used by crypt when generating password hash.
+#Provisioning.PasswordCryptId=6
+
+# Length of random salt used when generating password hash.
+#Provisioning.PasswordCryptSaltLength=10
+
+# Allow reset password of sys user
+Provisioning.AllowResetSysUser=n
+
+# Format if unformatted. If 'n', resource disk will not be mounted.
+ResourceDisk.Format=y
+
+# File system on the resource disk
+# Typically ext3 or ext4. FreeBSD images should use 'ufs2' here.
+ResourceDisk.Filesystem=ext4
+
+# Mount point for the resource disk
+ResourceDisk.MountPoint=/mnt/resource
+
+# Create and use swapfile on resource disk.
+ResourceDisk.EnableSwap=n
+
+# Size of the swapfile.
+ResourceDisk.SwapSizeMB=0
+
+# Comma-seperated list of mount options. See man(8) for valid options.
+ResourceDisk.MountOptions=None
+
+# Enable verbose logging (y|n)
+Logs.Verbose=n
+
+# Is FIPS enabled
+OS.FIPSEnabled=y
+
+# Root device timeout in seconds.
+OS.RootDeviceScsiTimeout=300
+
+# If "None", the system default version is used.
+OS.OpensslPath=None
+
+# Set the path to SSH keys and configuration files
+OS.SshDir=/notareal/path
+
+# If set, agent will use proxy server to access internet
+#HttpProxy.Host=None
+#HttpProxy.Port=None
+
+# Detect Scvmm environment, default is n
+# DetectScvmmEnv=n
+
+#
+# Lib.Dir=/var/lib/waagent
+
+#
+# DVD.MountPoint=/mnt/cdrom/secure
+
+#
+# Pid.File=/var/run/waagent.pid
+
+#
+# Extension.LogDir=/var/log/azure
+
+#
+# Home.Dir=/home
+
+# Enable RDMA management and set up, should only be used in HPC images
+# OS.EnableRDMA=y
+
+# Enable or disable goal state processing auto-update, default is enabled
+# AutoUpdate.Enabled=y
+
+# Determine the update family, this should not be changed
+# AutoUpdate.GAFamily=Prod
+
+# Determine if the overprovisioning feature is enabled. If yes, hold extension
+# handling until inVMArtifactsProfile.OnHold is false.
+# Default is disabled
+# EnableOverProvisioning=n
diff --git a/tests/test_agent.py b/tests/test_agent.py
new file mode 100644
index 00000000..a8c9ce14
--- /dev/null
+++ b/tests/test_agent.py
@@ -0,0 +1,73 @@
+# Copyright 2014 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.4+ and Openssl 1.0+
+#
+
+import mock
+import os.path
+
+from azurelinuxagent.agent import *
+from azurelinuxagent.common.conf import *
+
+from tests.tools import *
+
+
+class TestAgent(AgentTestCase):
+
+    def test_accepts_configuration_path(self):
+        conf_path = os.path.join(data_dir, "test_waagent.conf")
+        c, f, v, cfp = parse_args(["-configuration-path:" + conf_path])
+        self.assertEqual(cfp, conf_path)
+
+    @patch("os.path.exists", return_value=True)
+    def test_checks_configuration_path(self, mock_exists):
+        conf_path = "/foo/bar-baz/something.conf"
+        c, f, v, cfp = parse_args(["-configuration-path:"+conf_path])
+        self.assertEqual(cfp, conf_path)
+        self.assertEqual(mock_exists.call_count, 1)
+
+    @patch("sys.stderr")
+    @patch("os.path.exists", return_value=False)
+    @patch("sys.exit", side_effect=Exception)
+    def test_rejects_missing_configuration_path(self, mock_exit, mock_exists, mock_stderr):
+        try:
+            c, f, v, cfp = parse_args(["-configuration-path:/foo/bar.conf"])
+            self.assertTrue(False)
+        except Exception:
+            self.assertEqual(mock_exit.call_count, 1)
+
+    def test_configuration_path_defaults_to_none(self):
+        c, f, v, cfp = parse_args([])
+        self.assertEqual(cfp, None)
+
+    def test_agent_accepts_configuration_path(self):
+        Agent(False,
+                conf_file_path=os.path.join(data_dir, "test_waagent.conf"))
+        self.assertTrue(conf.get_fips_enabled())
+
+    @patch("azurelinuxagent.common.conf.load_conf_from_file")
+    def test_agent_uses_default_configuration_path(self, mock_load):
+        Agent(False)
+        mock_load.assert_called_once_with("/etc/waagent.conf")
+
+    @patch("azurelinuxagent.agent.Agent.run_exthandlers")
+    @patch("os.path.exists", return_value=True)
+    @patch("azurelinuxagent.common.conf.load_conf_from_file")
+    def test_main_passes_configuration_path(self,
+                mock_load, mock_exists, mock_handlers):
+        main(["-configuration-path:/foo/bar.conf", "-run-exthandlers"])
+        mock_handlers.assert_called_once()
+        mock_exists.assert_called_once_with("/foo/bar.conf")
+        mock_load.assert_called_once_with("/foo/bar.conf")