summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorScott Moser <smoser@ubuntu.com>2015-02-24 09:15:27 -0500
committerScott Moser <smoser@ubuntu.com>2015-02-24 09:15:27 -0500
commita37b3e0c1dcf1137744f0a123a5a535a93f799e7 (patch)
tree0028d62656452b964a8f20b75c3158347901d8fe
parent3165ab2a9b9d5f90f40f4458b58d3e5dba8dc53e (diff)
parent3be2a3aaf66601f1218676050f08010c5465ef7e (diff)
downloadvyos-cloud-init-a37b3e0c1dcf1137744f0a123a5a535a93f799e7.tar.gz
vyos-cloud-init-a37b3e0c1dcf1137744f0a123a5a535a93f799e7.zip
CloudStack: support fetching password from virtual router
LP: #1422388
-rw-r--r--ChangeLog2
-rw-r--r--cloudinit/sources/DataSourceCloudStack.py93
-rw-r--r--tests/unittests/test_datasource/test_cloudstack.py86
3 files changed, 169 insertions, 12 deletions
diff --git a/ChangeLog b/ChangeLog
index 94d901ac..e74b69b2 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -24,6 +24,8 @@
- python3 support [Barry Warsaw, Daniel Watkins, Josh Harlow] (LP: #1247132)
- support managing gpt partitions in disk config [Daniel Watkins]
- Azure: utilze gpt support for ephemeral formating [Daniel Watkins]
+ - CloudStack: support fetching password from virtual router [Daniel Watkins]
+ (LP: #1422388)
0.7.6:
- open 0.7.6
- Enable vendordata on CloudSigma datasource (LP: #1303986)
diff --git a/cloudinit/sources/DataSourceCloudStack.py b/cloudinit/sources/DataSourceCloudStack.py
index 1bbeca59..7b32e1fa 100644
--- a/cloudinit/sources/DataSourceCloudStack.py
+++ b/cloudinit/sources/DataSourceCloudStack.py
@@ -26,18 +26,67 @@
import os
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 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__)
+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.
+
+ This should be fixed in CloudStack by commit
+ a72f14ea9cb832faaac946b3cf9f56856b50142a in December 2014.
+ """
+
+ 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)
+ 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):
+ 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)
@@ -45,10 +94,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
@@ -82,17 +132,20 @@ 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)
+ 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 + "/")):
@@ -104,12 +157,28 @@ 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",
int(time.time() - start_time))
+ password_client = CloudStackPasswordServerClient(self.vr_addr)
+ try:
+ set_password = password_client.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',
@@ -192,7 +261,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)),
]
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')