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