Merge lp:~daniel-thewatkins/cloud-init/cloudstack-passwords into lp:~cloud-init-dev/cloud-init/trunk

Proposed by Dan Watkins on 2015-02-17
Status: Merged
Merged at revision: 1062
Proposed branch: lp:~daniel-thewatkins/cloud-init/cloudstack-passwords
Merge into: lp:~cloud-init-dev/cloud-init/trunk
Diff against target: 244 lines (+167/-12)
2 files modified
cloudinit/sources/DataSourceCloudStack.py (+81/-12)
tests/unittests/test_datasource/test_cloudstack.py (+86/-0)
To merge this branch: bzr merge lp:~daniel-thewatkins/cloud-init/cloudstack-passwords
Reviewer Review Type Date Requested Status
Ben Howard 2015-02-18 Pending
Scott Moser 2015-02-17 Pending
Review via email: mp+250028@code.launchpad.net

Description of the Change

CloudStack supports setting passwords; this MP introduces support for fetching these from the virtual router.

To post a comment you must log in.
1060. By Dan Watkins on 2015-02-17

Clean up imports in DataSourceCloudStack.py.

1061. By Dan Watkins on 2015-02-17

Fetch and use passwords from CloudStack virtual router.

1062. By Dan Watkins on 2015-02-17

Add explanatory comment.

1063. By Dan Watkins on 2015-02-18

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.

1064. By Dan Watkins on 2015-02-18

Set an explicit timeout when fetching CloudStack passwords.

Joshua Harlow (harlowja) :
1065. By Dan Watkins on 2015-02-20

Add automated tests for CloudStack passwords.

1066. By Dan Watkins on 2015-02-20

Minor formatting clean-up in CloudStack DS.

1067. By Dan Watkins on 2015-02-20

Split CloudStack password handling out to separate class.

Joshua Harlow (harlowja) wrote :

Just a few more adjusments and looks ok to me.

1068. By Dan Watkins on 2015-02-23

Always close the password server connection, even on failure.

1069. By Dan Watkins on 2015-02-23

Add documentation about upstream CloudStack HTTP fix.

Joshua Harlow (harlowja) wrote :

Looks ok to me, cloudstack is scary, ha.

Joshua Harlow (harlowja) wrote :

Scary code commit linked :-P

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'cloudinit/sources/DataSourceCloudStack.py'
2--- cloudinit/sources/DataSourceCloudStack.py 2014-08-21 17:46:41 +0000
3+++ cloudinit/sources/DataSourceCloudStack.py 2015-02-23 09:36:47 +0000
4@@ -26,18 +26,67 @@
5
6 import os
7 import time
8+from socket import inet_ntoa
9+from struct import pack
10+
11+from six.moves import http_client
12
13 from cloudinit import ec2_utils as ec2
14 from cloudinit import log as logging
15-from cloudinit import sources
16 from cloudinit import url_helper as uhelp
17-from cloudinit import util
18-from socket import inet_ntoa
19-from struct import pack
20+from cloudinit import sources, util
21
22 LOG = logging.getLogger(__name__)
23
24
25+class CloudStackPasswordServerClient(object):
26+ """
27+ Implements password fetching from the CloudStack password server.
28+
29+ http://cloudstack-administration.readthedocs.org/en/latest/templates.html#adding-password-management-to-your-templates
30+ has documentation about the system. This implementation is following that
31+ found at
32+ https://github.com/shankerbalan/cloudstack-scripts/blob/master/cloud-set-guest-password-debian
33+
34+ The CloudStack password server is, essentially, a broken HTTP
35+ server. It requires us to provide a valid HTTP request (including a
36+ DomU_Request header, which is the meat of the request), but just
37+ writes the text of its response on to the socket, without a status
38+ line or any HTTP headers. This makes HTTP libraries sad, which
39+ explains the screwiness of the implementation of this class.
40+
41+ This should be fixed in CloudStack by commit
42+ a72f14ea9cb832faaac946b3cf9f56856b50142a in December 2014.
43+ """
44+
45+ def __init__(self, virtual_router_address):
46+ self.virtual_router_address = virtual_router_address
47+
48+ def _do_request(self, domu_request):
49+ # We have to provide a valid HTTP request, but a valid HTTP
50+ # response is not returned. This means that getresponse() chokes,
51+ # so we use the socket directly to read off the response.
52+ # Because we're reading off the socket directly, we can't re-use the
53+ # connection.
54+ conn = http_client.HTTPConnection(self.virtual_router_address, 8080)
55+ try:
56+ conn.request('GET', '', headers={'DomU_Request': domu_request})
57+ conn.sock.settimeout(30)
58+ output = conn.sock.recv(1024).decode('utf-8').strip()
59+ finally:
60+ conn.close()
61+ return output
62+
63+ def get_password(self):
64+ password = self._do_request('send_my_password')
65+ if password in ['', 'saved_password']:
66+ return None
67+ if password == 'bad_request':
68+ raise RuntimeError('Error when attempting to fetch root password.')
69+ self._do_request('saved_password')
70+ return password
71+
72+
73 class DataSourceCloudStack(sources.DataSource):
74 def __init__(self, sys_cfg, distro, paths):
75 sources.DataSource.__init__(self, sys_cfg, distro, paths)
76@@ -45,10 +94,11 @@
77 # Cloudstack has its metadata/userdata URLs located at
78 # http://<virtual-router-ip>/latest/
79 self.api_ver = 'latest'
80- vr_addr = get_vr_address()
81- if not vr_addr:
82+ self.vr_addr = get_vr_address()
83+ if not self.vr_addr:
84 raise RuntimeError("No virtual router found!")
85- self.metadata_address = "http://%s/" % (vr_addr)
86+ self.metadata_address = "http://%s/" % (self.vr_addr,)
87+ self.cfg = {}
88
89 def _get_url_settings(self):
90 mcfg = self.ds_cfg
91@@ -82,17 +132,20 @@
92 'latest/meta-data/instance-id')]
93 start_time = time.time()
94 url = uhelp.wait_for_url(urls=urls, max_wait=max_wait,
95- timeout=timeout, status_cb=LOG.warn)
96+ timeout=timeout, status_cb=LOG.warn)
97
98 if url:
99 LOG.debug("Using metadata source: '%s'", url)
100 else:
101 LOG.critical(("Giving up on waiting for the metadata from %s"
102 " after %s seconds"),
103- urls, int(time.time() - start_time))
104+ urls, int(time.time() - start_time))
105
106 return bool(url)
107
108+ def get_config_obj(self):
109+ return self.cfg
110+
111 def get_data(self):
112 seed_ret = {}
113 if util.read_optional_seed(seed_ret, base=(self.seed_dir + "/")):
114@@ -104,12 +157,28 @@
115 if not self.wait_for_metadata_service():
116 return False
117 start_time = time.time()
118- self.userdata_raw = ec2.get_instance_userdata(self.api_ver,
119- self.metadata_address)
120+ self.userdata_raw = ec2.get_instance_userdata(
121+ self.api_ver, self.metadata_address)
122 self.metadata = ec2.get_instance_metadata(self.api_ver,
123 self.metadata_address)
124 LOG.debug("Crawl of metadata service took %s seconds",
125 int(time.time() - start_time))
126+ password_client = CloudStackPasswordServerClient(self.vr_addr)
127+ try:
128+ set_password = password_client.get_password()
129+ except Exception:
130+ util.logexc(LOG,
131+ 'Failed to fetch password from virtual router %s',
132+ self.vr_addr)
133+ else:
134+ if set_password:
135+ self.cfg = {
136+ 'ssh_pwauth': True,
137+ 'password': set_password,
138+ 'chpasswd': {
139+ 'expire': False,
140+ },
141+ }
142 return True
143 except Exception:
144 util.logexc(LOG, 'Failed fetching from metadata service %s',
145@@ -192,7 +261,7 @@
146
147 # Used to match classes to dependencies
148 datasources = [
149- (DataSourceCloudStack, (sources.DEP_FILESYSTEM, sources.DEP_NETWORK)),
150+ (DataSourceCloudStack, (sources.DEP_FILESYSTEM, sources.DEP_NETWORK)),
151 ]
152
153
154
155=== added file 'tests/unittests/test_datasource/test_cloudstack.py'
156--- tests/unittests/test_datasource/test_cloudstack.py 1970-01-01 00:00:00 +0000
157+++ tests/unittests/test_datasource/test_cloudstack.py 2015-02-23 09:36:47 +0000
158@@ -0,0 +1,86 @@
159+from cloudinit import helpers
160+from cloudinit.sources.DataSourceCloudStack import DataSourceCloudStack
161+from ..helpers import TestCase
162+
163+try:
164+ from unittest import mock
165+except ImportError:
166+ import mock
167+try:
168+ from contextlib import ExitStack
169+except ImportError:
170+ from contextlib2 import ExitStack
171+
172+
173+class TestCloudStackPasswordFetching(TestCase):
174+
175+ def setUp(self):
176+ super(TestCloudStackPasswordFetching, self).setUp()
177+ self.patches = ExitStack()
178+ self.addCleanup(self.patches.close)
179+ mod_name = 'cloudinit.sources.DataSourceCloudStack'
180+ self.patches.enter_context(mock.patch('{0}.ec2'.format(mod_name)))
181+ self.patches.enter_context(mock.patch('{0}.uhelp'.format(mod_name)))
182+
183+ def _set_password_server_response(self, response_string):
184+ http_client = mock.MagicMock()
185+ http_client.HTTPConnection.return_value.sock.recv.return_value = \
186+ response_string.encode('utf-8')
187+ self.patches.enter_context(
188+ mock.patch('cloudinit.sources.DataSourceCloudStack.http_client',
189+ http_client))
190+ return http_client
191+
192+ def test_empty_password_doesnt_create_config(self):
193+ self._set_password_server_response('')
194+ ds = DataSourceCloudStack({}, None, helpers.Paths({}))
195+ ds.get_data()
196+ self.assertEqual({}, ds.get_config_obj())
197+
198+ def test_saved_password_doesnt_create_config(self):
199+ self._set_password_server_response('saved_password')
200+ ds = DataSourceCloudStack({}, None, helpers.Paths({}))
201+ ds.get_data()
202+ self.assertEqual({}, ds.get_config_obj())
203+
204+ def test_password_sets_password(self):
205+ password = 'SekritSquirrel'
206+ self._set_password_server_response(password)
207+ ds = DataSourceCloudStack({}, None, helpers.Paths({}))
208+ ds.get_data()
209+ self.assertEqual(password, ds.get_config_obj()['password'])
210+
211+ def test_bad_request_doesnt_stop_ds_from_working(self):
212+ self._set_password_server_response('bad_request')
213+ ds = DataSourceCloudStack({}, None, helpers.Paths({}))
214+ self.assertTrue(ds.get_data())
215+
216+ def assertRequestTypesSent(self, http_client, expected_request_types):
217+ request_types = [
218+ kwargs['headers']['DomU_Request']
219+ for _, kwargs
220+ in http_client.HTTPConnection.return_value.request.call_args_list]
221+ self.assertEqual(expected_request_types, request_types)
222+
223+ def test_valid_response_means_password_marked_as_saved(self):
224+ password = 'SekritSquirrel'
225+ http_client = self._set_password_server_response(password)
226+ ds = DataSourceCloudStack({}, None, helpers.Paths({}))
227+ ds.get_data()
228+ self.assertRequestTypesSent(http_client,
229+ ['send_my_password', 'saved_password'])
230+
231+ def _check_password_not_saved_for(self, response_string):
232+ http_client = self._set_password_server_response(response_string)
233+ ds = DataSourceCloudStack({}, None, helpers.Paths({}))
234+ ds.get_data()
235+ self.assertRequestTypesSent(http_client, ['send_my_password'])
236+
237+ def test_password_not_saved_if_empty(self):
238+ self._check_password_not_saved_for('')
239+
240+ def test_password_not_saved_if_already_saved(self):
241+ self._check_password_not_saved_for('saved_password')
242+
243+ def test_password_not_saved_if_bad_request(self):
244+ self._check_password_not_saved_for('bad_request')