From 10aeda45b32645542d03cd42bd830558a6354495 Mon Sep 17 00:00:00 2001
From: Daniel Watkins <daniel.watkins@canonical.com>
Date: Tue, 17 Feb 2015 16:33:23 +0000
Subject: Clean up imports in DataSourceCloudStack.py.

---
 cloudinit/sources/DataSourceCloudStack.py | 7 +++----
 1 file changed, 3 insertions(+), 4 deletions(-)

diff --git a/cloudinit/sources/DataSourceCloudStack.py b/cloudinit/sources/DataSourceCloudStack.py
index 1bbeca59..b8974dc1 100644
--- a/cloudinit/sources/DataSourceCloudStack.py
+++ b/cloudinit/sources/DataSourceCloudStack.py
@@ -26,14 +26,13 @@
 
 import os
 import time
+from socket import inet_ntoa
+from struct import pack
 
 from cloudinit import ec2_utils as ec2
 from cloudinit import log as logging
-from cloudinit import sources
 from cloudinit import url_helper as uhelp
-from cloudinit import util
-from socket import inet_ntoa
-from struct import pack
+from cloudinit import sources, util
 
 LOG = logging.getLogger(__name__)
 
-- 
cgit v1.2.3


From e626359a6ea47880f0c17add03502513ee3a6792 Mon Sep 17 00:00:00 2001
From: Daniel Watkins <daniel.watkins@canonical.com>
Date: Tue, 17 Feb 2015 16:33:23 +0000
Subject: Fetch and use passwords from CloudStack virtual router.

---
 cloudinit/sources/DataSourceCloudStack.py | 36 ++++++++++++++++++++++++++++---
 1 file changed, 33 insertions(+), 3 deletions(-)

diff --git a/cloudinit/sources/DataSourceCloudStack.py b/cloudinit/sources/DataSourceCloudStack.py
index b8974dc1..0377d940 100644
--- a/cloudinit/sources/DataSourceCloudStack.py
+++ b/cloudinit/sources/DataSourceCloudStack.py
@@ -29,6 +29,8 @@ import time
 from socket import inet_ntoa
 from struct import pack
 
+from six.moves import http_client
+
 from cloudinit import ec2_utils as ec2
 from cloudinit import log as logging
 from cloudinit import url_helper as uhelp
@@ -44,10 +46,11 @@ class DataSourceCloudStack(sources.DataSource):
         # Cloudstack has its metadata/userdata URLs located at
         # http://<virtual-router-ip>/latest/
         self.api_ver = 'latest'
-        vr_addr = get_vr_address()
-        if not vr_addr:
+        self.vr_addr = get_vr_address()
+        if not self.vr_addr:
             raise RuntimeError("No virtual router found!")
-        self.metadata_address = "http://%s/" % (vr_addr)
+        self.metadata_address = "http://%s/" % (self.vr_addr,)
+        self.cfg = {}
 
     def _get_url_settings(self):
         mcfg = self.ds_cfg
@@ -92,6 +95,9 @@ class DataSourceCloudStack(sources.DataSource):
 
         return bool(url)
 
+    def get_config_obj(self):
+        return self.cfg
+
     def get_data(self):
         seed_ret = {}
         if util.read_optional_seed(seed_ret, base=(self.seed_dir + "/")):
@@ -109,12 +115,36 @@ class DataSourceCloudStack(sources.DataSource):
                                                       self.metadata_address)
             LOG.debug("Crawl of metadata service took %s seconds",
                       int(time.time() - start_time))
+            set_password = self.get_password()
+            if set_password:
+                self.cfg = {
+                    'ssh_pwauth': True,
+                    'password': set_password,
+                    'chpasswd': {
+                        'expire': False,
+                    },
+                }
             return True
         except Exception:
             util.logexc(LOG, 'Failed fetching from metadata service %s',
                         self.metadata_address)
             return False
 
+    def get_password(self):
+        def _do_request(req_string):
+            conn = http_client.HTTPConnection(self.vr_addr, 8080)
+            conn.request('GET', '', headers={'DomU_Request': req_string})
+            output = conn.sock.recv(1024).decode('utf-8').strip()
+            conn.close()
+            return output
+        password = _do_request('send_my_password')
+        if password in ['', 'saved_password']:
+            return None
+        if password == 'bad_request':
+            raise RuntimeError('Error when attempting to fetch root password.')
+        _do_request('saved_password')
+        return password
+
     def get_instance_id(self):
         return self.metadata['instance-id']
 
-- 
cgit v1.2.3


From e01795dac74cd31bd6054e3185c2dba6203690ca Mon Sep 17 00:00:00 2001
From: Daniel Watkins <daniel.watkins@canonical.com>
Date: Tue, 17 Feb 2015 16:33:23 +0000
Subject: Add explanatory comment.

---
 cloudinit/sources/DataSourceCloudStack.py | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/cloudinit/sources/DataSourceCloudStack.py b/cloudinit/sources/DataSourceCloudStack.py
index 0377d940..5eda10a5 100644
--- a/cloudinit/sources/DataSourceCloudStack.py
+++ b/cloudinit/sources/DataSourceCloudStack.py
@@ -132,6 +132,9 @@ class DataSourceCloudStack(sources.DataSource):
 
     def get_password(self):
         def _do_request(req_string):
+            # We have to provide a valid HTTP request, but a valid HTTP
+            # response is not returned. This means that getresponse() chokes,
+            # so we use the socket directly to read off the password.
             conn = http_client.HTTPConnection(self.vr_addr, 8080)
             conn.request('GET', '', headers={'DomU_Request': req_string})
             output = conn.sock.recv(1024).decode('utf-8').strip()
-- 
cgit v1.2.3


From 5e864eb373ead67d2bc29a19d970f9d3d94c53df Mon Sep 17 00:00:00 2001
From: Daniel Watkins <daniel.watkins@canonical.com>
Date: Wed, 18 Feb 2015 18:09:34 +0000
Subject: Failing to fetch a CloudStack password should never fail the whole
 DS.

There might be some CloudStack deployments without the :8080 password
server, and there's no reason the rest of the data source can't be used
for them.
---
 cloudinit/sources/DataSourceCloudStack.py | 24 +++++++++++++++---------
 1 file changed, 15 insertions(+), 9 deletions(-)

diff --git a/cloudinit/sources/DataSourceCloudStack.py b/cloudinit/sources/DataSourceCloudStack.py
index 5eda10a5..a8f8daec 100644
--- a/cloudinit/sources/DataSourceCloudStack.py
+++ b/cloudinit/sources/DataSourceCloudStack.py
@@ -115,15 +115,21 @@ class DataSourceCloudStack(sources.DataSource):
                                                       self.metadata_address)
             LOG.debug("Crawl of metadata service took %s seconds",
                       int(time.time() - start_time))
-            set_password = self.get_password()
-            if set_password:
-                self.cfg = {
-                    'ssh_pwauth': True,
-                    'password': set_password,
-                    'chpasswd': {
-                        'expire': False,
-                    },
-                }
+            try:
+                set_password = self.get_password()
+            except Exception:
+                util.logexc(LOG,
+                            'Failed to fetch password from virtual router %s',
+                            self.vr_addr)
+            else:
+                if set_password:
+                    self.cfg = {
+                        'ssh_pwauth': True,
+                        'password': set_password,
+                        'chpasswd': {
+                            'expire': False,
+                        },
+                    }
             return True
         except Exception:
             util.logexc(LOG, 'Failed fetching from metadata service %s',
-- 
cgit v1.2.3


From d3d44a3efaf22c91d342f2cb81470745b7be0658 Mon Sep 17 00:00:00 2001
From: Daniel Watkins <daniel.watkins@canonical.com>
Date: Wed, 18 Feb 2015 18:10:15 +0000
Subject: Set an explicit timeout when fetching CloudStack passwords.

---
 cloudinit/sources/DataSourceCloudStack.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/cloudinit/sources/DataSourceCloudStack.py b/cloudinit/sources/DataSourceCloudStack.py
index a8f8daec..89f58e1e 100644
--- a/cloudinit/sources/DataSourceCloudStack.py
+++ b/cloudinit/sources/DataSourceCloudStack.py
@@ -143,6 +143,7 @@ class DataSourceCloudStack(sources.DataSource):
             # so we use the socket directly to read off the password.
             conn = http_client.HTTPConnection(self.vr_addr, 8080)
             conn.request('GET', '', headers={'DomU_Request': req_string})
+            conn.sock.settimeout(30)
             output = conn.sock.recv(1024).decode('utf-8').strip()
             conn.close()
             return output
-- 
cgit v1.2.3


From 385b48ae2b82b9c267710d41b9f470104ea248b7 Mon Sep 17 00:00:00 2001
From: Daniel Watkins <daniel.watkins@canonical.com>
Date: Fri, 20 Feb 2015 10:30:36 +0000
Subject: Add automated tests for CloudStack passwords.

---
 tests/unittests/test_datasource/test_cloudstack.py | 86 ++++++++++++++++++++++
 1 file changed, 86 insertions(+)
 create mode 100644 tests/unittests/test_datasource/test_cloudstack.py

diff --git a/tests/unittests/test_datasource/test_cloudstack.py b/tests/unittests/test_datasource/test_cloudstack.py
new file mode 100644
index 00000000..959d78ae
--- /dev/null
+++ b/tests/unittests/test_datasource/test_cloudstack.py
@@ -0,0 +1,86 @@
+from cloudinit import helpers
+from cloudinit.sources.DataSourceCloudStack import DataSourceCloudStack
+from ..helpers import TestCase
+
+try:
+    from unittest import mock
+except ImportError:
+    import mock
+try:
+    from contextlib import ExitStack
+except ImportError:
+    from contextlib2 import ExitStack
+
+
+class TestCloudStackPasswordFetching(TestCase):
+
+    def setUp(self):
+        super(TestCloudStackPasswordFetching, self).setUp()
+        self.patches = ExitStack()
+        self.addCleanup(self.patches.close)
+        mod_name = 'cloudinit.sources.DataSourceCloudStack'
+        self.patches.enter_context(mock.patch('{0}.ec2'.format(mod_name)))
+        self.patches.enter_context(mock.patch('{0}.uhelp'.format(mod_name)))
+
+    def _set_password_server_response(self, response_string):
+        http_client = mock.MagicMock()
+        http_client.HTTPConnection.return_value.sock.recv.return_value = \
+            response_string.encode('utf-8')
+        self.patches.enter_context(
+            mock.patch('cloudinit.sources.DataSourceCloudStack.http_client',
+                       http_client))
+        return http_client
+
+    def test_empty_password_doesnt_create_config(self):
+        self._set_password_server_response('')
+        ds = DataSourceCloudStack({}, None, helpers.Paths({}))
+        ds.get_data()
+        self.assertEqual({}, ds.get_config_obj())
+
+    def test_saved_password_doesnt_create_config(self):
+        self._set_password_server_response('saved_password')
+        ds = DataSourceCloudStack({}, None, helpers.Paths({}))
+        ds.get_data()
+        self.assertEqual({}, ds.get_config_obj())
+
+    def test_password_sets_password(self):
+        password = 'SekritSquirrel'
+        self._set_password_server_response(password)
+        ds = DataSourceCloudStack({}, None, helpers.Paths({}))
+        ds.get_data()
+        self.assertEqual(password, ds.get_config_obj()['password'])
+
+    def test_bad_request_doesnt_stop_ds_from_working(self):
+        self._set_password_server_response('bad_request')
+        ds = DataSourceCloudStack({}, None, helpers.Paths({}))
+        self.assertTrue(ds.get_data())
+
+    def assertRequestTypesSent(self, http_client, expected_request_types):
+        request_types = [
+            kwargs['headers']['DomU_Request']
+            for _, kwargs
+            in http_client.HTTPConnection.return_value.request.call_args_list]
+        self.assertEqual(expected_request_types, request_types)
+
+    def test_valid_response_means_password_marked_as_saved(self):
+        password = 'SekritSquirrel'
+        http_client = self._set_password_server_response(password)
+        ds = DataSourceCloudStack({}, None, helpers.Paths({}))
+        ds.get_data()
+        self.assertRequestTypesSent(http_client,
+                                    ['send_my_password', 'saved_password'])
+
+    def _check_password_not_saved_for(self, response_string):
+        http_client = self._set_password_server_response(response_string)
+        ds = DataSourceCloudStack({}, None, helpers.Paths({}))
+        ds.get_data()
+        self.assertRequestTypesSent(http_client, ['send_my_password'])
+
+    def test_password_not_saved_if_empty(self):
+        self._check_password_not_saved_for('')
+
+    def test_password_not_saved_if_already_saved(self):
+        self._check_password_not_saved_for('saved_password')
+
+    def test_password_not_saved_if_bad_request(self):
+        self._check_password_not_saved_for('bad_request')
-- 
cgit v1.2.3


From b57c6a109491f344fa6e6fc2593ab2e60ca65249 Mon Sep 17 00:00:00 2001
From: Daniel Watkins <daniel.watkins@canonical.com>
Date: Fri, 20 Feb 2015 10:57:06 +0000
Subject: Minor formatting clean-up in CloudStack DS.

---
 cloudinit/sources/DataSourceCloudStack.py | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/cloudinit/sources/DataSourceCloudStack.py b/cloudinit/sources/DataSourceCloudStack.py
index 89f58e1e..85f20c23 100644
--- a/cloudinit/sources/DataSourceCloudStack.py
+++ b/cloudinit/sources/DataSourceCloudStack.py
@@ -84,14 +84,14 @@ class DataSourceCloudStack(sources.DataSource):
                                   'latest/meta-data/instance-id')]
         start_time = time.time()
         url = uhelp.wait_for_url(urls=urls, max_wait=max_wait,
-                                timeout=timeout, status_cb=LOG.warn)
+                                 timeout=timeout, status_cb=LOG.warn)
 
         if url:
             LOG.debug("Using metadata source: '%s'", url)
         else:
             LOG.critical(("Giving up on waiting for the metadata from %s"
                           " after %s seconds"),
-                          urls, int(time.time() - start_time))
+                         urls, int(time.time() - start_time))
 
         return bool(url)
 
@@ -109,8 +109,8 @@ class DataSourceCloudStack(sources.DataSource):
             if not self.wait_for_metadata_service():
                 return False
             start_time = time.time()
-            self.userdata_raw = ec2.get_instance_userdata(self.api_ver,
-                self.metadata_address)
+            self.userdata_raw = ec2.get_instance_userdata(
+                self.api_ver, self.metadata_address)
             self.metadata = ec2.get_instance_metadata(self.api_ver,
                                                       self.metadata_address)
             LOG.debug("Crawl of metadata service took %s seconds",
@@ -231,7 +231,7 @@ def get_vr_address():
 
 # Used to match classes to dependencies
 datasources = [
-  (DataSourceCloudStack, (sources.DEP_FILESYSTEM, sources.DEP_NETWORK)),
+    (DataSourceCloudStack, (sources.DEP_FILESYSTEM, sources.DEP_NETWORK)),
 ]
 
 
-- 
cgit v1.2.3


From f8d9ebbe3743bcada75bc1a980b49f493e2da2f1 Mon Sep 17 00:00:00 2001
From: Daniel Watkins <daniel.watkins@canonical.com>
Date: Fri, 20 Feb 2015 10:57:18 +0000
Subject: Split CloudStack password handling out to separate class.

---
 cloudinit/sources/DataSourceCloudStack.py | 65 +++++++++++++++++++++----------
 1 file changed, 45 insertions(+), 20 deletions(-)

diff --git a/cloudinit/sources/DataSourceCloudStack.py b/cloudinit/sources/DataSourceCloudStack.py
index 85f20c23..0c3c51c0 100644
--- a/cloudinit/sources/DataSourceCloudStack.py
+++ b/cloudinit/sources/DataSourceCloudStack.py
@@ -39,6 +39,49 @@ from cloudinit import sources, util
 LOG = logging.getLogger(__name__)
 
 
+class CloudStackPasswordServerClient(object):
+    """
+    Implements password fetching from the CloudStack password server.
+
+    http://cloudstack-administration.readthedocs.org/en/latest/templates.html#adding-password-management-to-your-templates
+    has documentation about the system.  This implementation is following that
+    found at
+    https://github.com/shankerbalan/cloudstack-scripts/blob/master/cloud-set-guest-password-debian
+
+    The CloudStack password server is, essentially, a broken HTTP
+    server. It requires us to provide a valid HTTP request (including a
+    DomU_Request header, which is the meat of the request), but just
+    writes the text of its response on to the socket, without a status
+    line or any HTTP headers.  This makes HTTP libraries sad, which
+    explains the screwiness of the implementation of this class.
+    """
+
+    def __init__(self, virtual_router_address):
+        self.virtual_router_address = virtual_router_address
+
+    def _do_request(self, domu_request):
+        # We have to provide a valid HTTP request, but a valid HTTP
+        # response is not returned. This means that getresponse() chokes,
+        # so we use the socket directly to read off the response.
+        # Because we're reading off the socket directly, we can't re-use the
+        # connection.
+        conn = http_client.HTTPConnection(self.virtual_router_address, 8080)
+        conn.request('GET', '', headers={'DomU_Request': domu_request})
+        conn.sock.settimeout(30)
+        output = conn.sock.recv(1024).decode('utf-8').strip()
+        conn.close()
+        return output
+
+    def get_password(self):
+        password = self._do_request('send_my_password')
+        if password in ['', 'saved_password']:
+            return None
+        if password == 'bad_request':
+            raise RuntimeError('Error when attempting to fetch root password.')
+        self._do_request('saved_password')
+        return password
+
+
 class DataSourceCloudStack(sources.DataSource):
     def __init__(self, sys_cfg, distro, paths):
         sources.DataSource.__init__(self, sys_cfg, distro, paths)
@@ -115,8 +158,9 @@ class DataSourceCloudStack(sources.DataSource):
                                                       self.metadata_address)
             LOG.debug("Crawl of metadata service took %s seconds",
                       int(time.time() - start_time))
+            password_client = CloudStackPasswordServerClient(self.vr_addr)
             try:
-                set_password = self.get_password()
+                set_password = password_client.get_password()
             except Exception:
                 util.logexc(LOG,
                             'Failed to fetch password from virtual router %s',
@@ -136,25 +180,6 @@ class DataSourceCloudStack(sources.DataSource):
                         self.metadata_address)
             return False
 
-    def get_password(self):
-        def _do_request(req_string):
-            # We have to provide a valid HTTP request, but a valid HTTP
-            # response is not returned. This means that getresponse() chokes,
-            # so we use the socket directly to read off the password.
-            conn = http_client.HTTPConnection(self.vr_addr, 8080)
-            conn.request('GET', '', headers={'DomU_Request': req_string})
-            conn.sock.settimeout(30)
-            output = conn.sock.recv(1024).decode('utf-8').strip()
-            conn.close()
-            return output
-        password = _do_request('send_my_password')
-        if password in ['', 'saved_password']:
-            return None
-        if password == 'bad_request':
-            raise RuntimeError('Error when attempting to fetch root password.')
-        _do_request('saved_password')
-        return password
-
     def get_instance_id(self):
         return self.metadata['instance-id']
 
-- 
cgit v1.2.3


From ef84bd214a1d5e0b922c0dd38096f694f8ff406e Mon Sep 17 00:00:00 2001
From: Daniel Watkins <daniel.watkins@canonical.com>
Date: Mon, 23 Feb 2015 09:22:50 +0000
Subject: Always close the password server connection, even on failure.

---
 cloudinit/sources/DataSourceCloudStack.py | 10 ++++++----
 1 file changed, 6 insertions(+), 4 deletions(-)

diff --git a/cloudinit/sources/DataSourceCloudStack.py b/cloudinit/sources/DataSourceCloudStack.py
index 0c3c51c0..996076b1 100644
--- a/cloudinit/sources/DataSourceCloudStack.py
+++ b/cloudinit/sources/DataSourceCloudStack.py
@@ -66,10 +66,12 @@ class CloudStackPasswordServerClient(object):
         # Because we're reading off the socket directly, we can't re-use the
         # connection.
         conn = http_client.HTTPConnection(self.virtual_router_address, 8080)
-        conn.request('GET', '', headers={'DomU_Request': domu_request})
-        conn.sock.settimeout(30)
-        output = conn.sock.recv(1024).decode('utf-8').strip()
-        conn.close()
+        try:
+            conn.request('GET', '', headers={'DomU_Request': domu_request})
+            conn.sock.settimeout(30)
+            output = conn.sock.recv(1024).decode('utf-8').strip()
+        finally:
+            conn.close()
         return output
 
     def get_password(self):
-- 
cgit v1.2.3


From 9ab6bbab42ffb5cadbe0afb36aa6967ed94459c3 Mon Sep 17 00:00:00 2001
From: Daniel Watkins <daniel.watkins@canonical.com>
Date: Mon, 23 Feb 2015 09:36:36 +0000
Subject: Add documentation about upstream CloudStack HTTP fix.

---
 cloudinit/sources/DataSourceCloudStack.py | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/cloudinit/sources/DataSourceCloudStack.py b/cloudinit/sources/DataSourceCloudStack.py
index 996076b1..7b32e1fa 100644
--- a/cloudinit/sources/DataSourceCloudStack.py
+++ b/cloudinit/sources/DataSourceCloudStack.py
@@ -54,6 +54,9 @@ class CloudStackPasswordServerClient(object):
     writes the text of its response on to the socket, without a status
     line or any HTTP headers.  This makes HTTP libraries sad, which
     explains the screwiness of the implementation of this class.
+
+    This should be fixed in CloudStack by commit
+    a72f14ea9cb832faaac946b3cf9f56856b50142a in December 2014.
     """
 
     def __init__(self, virtual_router_address):
-- 
cgit v1.2.3