Merge lp:~harlowja/cloud-init/boto-ec2-replace into lp:~cloud-init-dev/cloud-init/trunk
- boto-ec2-replace
- Merge into trunk
Proposed by
Joshua Harlow
Status: | Merged |
---|---|
Merged at revision: | 910 |
Proposed branch: | lp:~harlowja/cloud-init/boto-ec2-replace |
Merge into: | lp:~cloud-init-dev/cloud-init/trunk |
Diff against target: |
366 lines (+296/-48) 3 files modified
Requires (+3/-3) cloudinit/ec2_utils.py (+160/-45) tests/unittests/test_ec2_util.py (+133/-0) |
To merge this branch: | bzr merge lp:~harlowja/cloud-init/boto-ec2-replace |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
cloud-init Commiters | Pending | ||
Review via email: mp+201269@code.launchpad.net |
Commit message
Description of the change
To post a comment you must log in.
- 906. By Joshua Harlow
-
Add ec2 utils tests and httpretty requirement for http mocking
- 907. By Joshua Harlow
-
Add a maybe_json helper function
- 908. By Joshua Harlow
-
Only check for json objects instead of also arrays
- 909. By Joshua Harlow
-
Updated non-json message
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'Requires' |
2 | --- Requires 2013-09-09 05:36:28 +0000 |
3 | +++ Requires 2014-01-11 01:39:20 +0000 |
4 | @@ -29,8 +29,8 @@ |
5 | # Requests handles ssl correctly! |
6 | requests |
7 | |
8 | -# Boto for ec2 |
9 | -boto |
10 | - |
11 | # For patching pieces of cloud-config together |
12 | jsonpatch |
13 | + |
14 | +# For http testing (only needed for testing) |
15 | +httpretty>=0.7.1 |
16 | |
17 | === modified file 'cloudinit/ec2_utils.py' |
18 | --- cloudinit/ec2_utils.py 2013-03-20 12:34:19 +0000 |
19 | +++ cloudinit/ec2_utils.py 2014-01-11 01:39:20 +0000 |
20 | @@ -16,48 +16,163 @@ |
21 | # You should have received a copy of the GNU General Public License |
22 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
23 | |
24 | -import boto.utils as boto_utils |
25 | - |
26 | -# Versions of boto >= 2.6.0 (and possibly 2.5.2) |
27 | -# try to lazily load the metadata backing, which |
28 | -# doesn't work so well in cloud-init especially |
29 | -# since the metadata is serialized and actions are |
30 | -# performed where the metadata server may be blocked |
31 | -# (thus the datasource will start failing) resulting |
32 | -# in url exceptions when fields that do exist (or |
33 | -# would have existed) do not exist due to the blocking |
34 | -# that occurred. |
35 | - |
36 | -# TODO(harlowja): https://github.com/boto/boto/issues/1401 |
37 | -# When boto finally moves to using requests, we should be able |
38 | -# to provide it ssl details, it does not yet, so we can't provide them... |
39 | - |
40 | - |
41 | -def _unlazy_dict(mp): |
42 | - if not isinstance(mp, (dict)): |
43 | - return mp |
44 | - # Walk over the keys/values which |
45 | - # forces boto to unlazy itself and |
46 | - # has no effect on dictionaries that |
47 | - # already have there items. |
48 | - for (_k, v) in mp.items(): |
49 | - _unlazy_dict(v) |
50 | - return mp |
51 | - |
52 | - |
53 | -def get_instance_userdata(api_version, metadata_address): |
54 | - # Note: boto.utils.get_instance_metadata returns '' for empty string |
55 | - # so the change from non-true to '' is not specifically necessary, but |
56 | - # this way cloud-init will get consistent behavior even if boto changed |
57 | - # in the future to return a None on "no user-data provided". |
58 | - ud = boto_utils.get_instance_userdata(api_version, None, metadata_address) |
59 | - if not ud: |
60 | - ud = '' |
61 | - return ud |
62 | - |
63 | - |
64 | -def get_instance_metadata(api_version, metadata_address): |
65 | - metadata = boto_utils.get_instance_metadata(api_version, metadata_address) |
66 | - if not isinstance(metadata, (dict)): |
67 | - metadata = {} |
68 | - return _unlazy_dict(metadata) |
69 | +from urlparse import (urlparse, urlunparse) |
70 | + |
71 | +import functools |
72 | +import json |
73 | +import urllib |
74 | + |
75 | +from cloudinit import log as logging |
76 | +from cloudinit import util |
77 | + |
78 | +LOG = logging.getLogger(__name__) |
79 | + |
80 | + |
81 | +def maybe_json_object(text): |
82 | + if not text: |
83 | + return False |
84 | + text = text.strip() |
85 | + if text.startswith("{") and text.endswith("}"): |
86 | + return True |
87 | + return False |
88 | + |
89 | + |
90 | +def combine_url(base, add_on): |
91 | + base_parsed = list(urlparse(base)) |
92 | + path = base_parsed[2] |
93 | + if path and not path.endswith("/"): |
94 | + path += "/" |
95 | + path += urllib.quote(str(add_on), safe="/:") |
96 | + base_parsed[2] = path |
97 | + return urlunparse(base_parsed) |
98 | + |
99 | + |
100 | +# See: http://bit.ly/TyoUQs |
101 | +# |
102 | +# Since boto metadata reader uses the old urllib which does not |
103 | +# support ssl, we need to ahead and create our own reader which |
104 | +# works the same as the boto one (for now). |
105 | +class MetadataMaterializer(object): |
106 | + def __init__(self, blob, base_url, caller): |
107 | + self._blob = blob |
108 | + self._md = None |
109 | + self._base_url = base_url |
110 | + self._caller = caller |
111 | + |
112 | + def _parse(self, blob): |
113 | + leaves = {} |
114 | + children = [] |
115 | + if not blob: |
116 | + return (leaves, children) |
117 | + |
118 | + def has_children(item): |
119 | + if item.endswith("/"): |
120 | + return True |
121 | + else: |
122 | + return False |
123 | + |
124 | + def get_name(item): |
125 | + if item.endswith("/"): |
126 | + return item.rstrip("/") |
127 | + return item |
128 | + |
129 | + for field in blob.splitlines(): |
130 | + field = field.strip() |
131 | + field_name = get_name(field) |
132 | + if not field or not field_name: |
133 | + continue |
134 | + if has_children(field): |
135 | + if field_name not in children: |
136 | + children.append(field_name) |
137 | + else: |
138 | + contents = field.split("=", 1) |
139 | + resource = field_name |
140 | + if len(contents) > 1: |
141 | + # What a PITA... |
142 | + (ident, sub_contents) = contents |
143 | + ident = util.safe_int(ident) |
144 | + if ident is not None: |
145 | + resource = "%s/openssh-key" % (ident) |
146 | + field_name = sub_contents |
147 | + leaves[field_name] = resource |
148 | + return (leaves, children) |
149 | + |
150 | + def materialize(self): |
151 | + if self._md is not None: |
152 | + return self._md |
153 | + self._md = self._materialize(self._blob, self._base_url) |
154 | + return self._md |
155 | + |
156 | + def _decode_leaf_blob(self, field, blob): |
157 | + if not blob: |
158 | + return blob |
159 | + if maybe_json_object(blob): |
160 | + try: |
161 | + # Assume it's json, unless it fails parsing... |
162 | + return json.loads(blob) |
163 | + except (ValueError, TypeError) as e: |
164 | + LOG.warn("Field %s looked like a json object, but it was" |
165 | + " not: %s", field, e) |
166 | + if blob.find("\n") != -1: |
167 | + return blob.splitlines() |
168 | + return blob |
169 | + |
170 | + def _materialize(self, blob, base_url): |
171 | + (leaves, children) = self._parse(blob) |
172 | + child_contents = {} |
173 | + for c in children: |
174 | + child_url = combine_url(base_url, c) |
175 | + if not child_url.endswith("/"): |
176 | + child_url += "/" |
177 | + child_blob = str(self._caller(child_url)) |
178 | + child_contents[c] = self._materialize(child_blob, child_url) |
179 | + leaf_contents = {} |
180 | + for (field, resource) in leaves.items(): |
181 | + leaf_url = combine_url(base_url, resource) |
182 | + leaf_blob = str(self._caller(leaf_url)) |
183 | + leaf_contents[field] = self._decode_leaf_blob(field, leaf_blob) |
184 | + joined = {} |
185 | + joined.update(child_contents) |
186 | + for field in leaf_contents.keys(): |
187 | + if field in joined: |
188 | + LOG.warn("Duplicate key found in results from %s", base_url) |
189 | + else: |
190 | + joined[field] = leaf_contents[field] |
191 | + return joined |
192 | + |
193 | + |
194 | +def get_instance_userdata(api_version='latest', |
195 | + metadata_address='http://169.254.169.254', |
196 | + ssl_details=None, timeout=5, retries=5): |
197 | + ud_url = combine_url(metadata_address, api_version) |
198 | + ud_url = combine_url(ud_url, 'user-data') |
199 | + try: |
200 | + response = util.read_file_or_url(ud_url, |
201 | + ssl_details=ssl_details, |
202 | + timeout=timeout, |
203 | + retries=retries) |
204 | + return str(response) |
205 | + except Exception: |
206 | + util.logexc(LOG, "Failed fetching userdata from url %s", ud_url) |
207 | + return '' |
208 | + |
209 | + |
210 | +def get_instance_metadata(api_version='latest', |
211 | + metadata_address='http://169.254.169.254', |
212 | + ssl_details=None, timeout=5, retries=5): |
213 | + md_url = combine_url(metadata_address, api_version) |
214 | + md_url = combine_url(md_url, 'meta-data') |
215 | + caller = functools.partial(util.read_file_or_url, |
216 | + ssl_details=ssl_details, timeout=timeout, |
217 | + retries=retries) |
218 | + |
219 | + try: |
220 | + response = caller(md_url) |
221 | + materializer = MetadataMaterializer(str(response), md_url, caller) |
222 | + md = materializer.materialize() |
223 | + if not isinstance(md, (dict)): |
224 | + md = {} |
225 | + return md |
226 | + except Exception: |
227 | + util.logexc(LOG, "Failed fetching metadata from url %s", md_url) |
228 | + return {} |
229 | |
230 | === added file 'tests/unittests/test_ec2_util.py' |
231 | --- tests/unittests/test_ec2_util.py 1970-01-01 00:00:00 +0000 |
232 | +++ tests/unittests/test_ec2_util.py 2014-01-11 01:39:20 +0000 |
233 | @@ -0,0 +1,133 @@ |
234 | +from tests.unittests import helpers |
235 | + |
236 | +from cloudinit import ec2_utils as eu |
237 | + |
238 | +import httpretty as hp |
239 | + |
240 | + |
241 | +class TestEc2Util(helpers.TestCase): |
242 | + VERSION = 'latest' |
243 | + |
244 | + @hp.activate |
245 | + def test_userdata_fetch(self): |
246 | + hp.register_uri(hp.GET, |
247 | + 'http://169.254.169.254/%s/user-data' % (self.VERSION), |
248 | + body='stuff', |
249 | + status=200) |
250 | + userdata = eu.get_instance_userdata(self.VERSION) |
251 | + self.assertEquals('stuff', userdata) |
252 | + |
253 | + @hp.activate |
254 | + def test_userdata_fetch_fail_not_found(self): |
255 | + hp.register_uri(hp.GET, |
256 | + 'http://169.254.169.254/%s/user-data' % (self.VERSION), |
257 | + status=404) |
258 | + userdata = eu.get_instance_userdata(self.VERSION, retries=0) |
259 | + self.assertEquals('', userdata) |
260 | + |
261 | + |
262 | + @hp.activate |
263 | + def test_userdata_fetch_fail_server_dead(self): |
264 | + hp.register_uri(hp.GET, |
265 | + 'http://169.254.169.254/%s/user-data' % (self.VERSION), |
266 | + status=500) |
267 | + userdata = eu.get_instance_userdata(self.VERSION, retries=0) |
268 | + self.assertEquals('', userdata) |
269 | + |
270 | + @hp.activate |
271 | + def test_metadata_fetch_no_keys(self): |
272 | + base_url = 'http://169.254.169.254/%s/meta-data' % (self.VERSION) |
273 | + hp.register_uri(hp.GET, base_url, status=200, |
274 | + body="\n".join(['hostname', |
275 | + 'instance-id', |
276 | + 'ami-launch-index'])) |
277 | + hp.register_uri(hp.GET, eu.combine_url(base_url, 'hostname'), |
278 | + status=200, body='ec2.fake.host.name.com') |
279 | + hp.register_uri(hp.GET, eu.combine_url(base_url, 'instance-id'), |
280 | + status=200, body='123') |
281 | + hp.register_uri(hp.GET, eu.combine_url(base_url, 'ami-launch-index'), |
282 | + status=200, body='1') |
283 | + md = eu.get_instance_metadata(self.VERSION, retries=0) |
284 | + self.assertEquals(md['hostname'], 'ec2.fake.host.name.com') |
285 | + self.assertEquals(md['instance-id'], '123') |
286 | + self.assertEquals(md['ami-launch-index'], '1') |
287 | + |
288 | + @hp.activate |
289 | + def test_metadata_fetch_key(self): |
290 | + base_url = 'http://169.254.169.254/%s/meta-data' % (self.VERSION) |
291 | + hp.register_uri(hp.GET, base_url, status=200, |
292 | + body="\n".join(['hostname', |
293 | + 'instance-id', |
294 | + 'public-keys/'])) |
295 | + hp.register_uri(hp.GET, eu.combine_url(base_url, 'hostname'), |
296 | + status=200, body='ec2.fake.host.name.com') |
297 | + hp.register_uri(hp.GET, eu.combine_url(base_url, 'instance-id'), |
298 | + status=200, body='123') |
299 | + hp.register_uri(hp.GET, eu.combine_url(base_url, 'public-keys/'), |
300 | + status=200, body='0=my-public-key') |
301 | + hp.register_uri(hp.GET, eu.combine_url(base_url, 'public-keys/'), |
302 | + status=200, body='0=my-public-key') |
303 | + hp.register_uri(hp.GET, |
304 | + eu.combine_url(base_url, 'public-keys/0/openssh-key'), |
305 | + status=200, body='ssh-rsa AAAA.....wZEf my-public-key') |
306 | + md = eu.get_instance_metadata(self.VERSION, retries=0, timeout=0.1) |
307 | + self.assertEquals(md['hostname'], 'ec2.fake.host.name.com') |
308 | + self.assertEquals(md['instance-id'], '123') |
309 | + self.assertEquals(1, len(md['public-keys'])) |
310 | + |
311 | + @hp.activate |
312 | + def test_metadata_fetch_key(self): |
313 | + base_url = 'http://169.254.169.254/%s/meta-data' % (self.VERSION) |
314 | + hp.register_uri(hp.GET, base_url, status=200, |
315 | + body="\n".join(['hostname', |
316 | + 'instance-id', |
317 | + 'public-keys/'])) |
318 | + hp.register_uri(hp.GET, eu.combine_url(base_url, 'hostname'), |
319 | + status=200, body='ec2.fake.host.name.com') |
320 | + hp.register_uri(hp.GET, eu.combine_url(base_url, 'instance-id'), |
321 | + status=200, body='123') |
322 | + hp.register_uri(hp.GET, eu.combine_url(base_url, 'public-keys/'), |
323 | + status=200, |
324 | + body="\n".join(['0=my-public-key', '1=my-other-key'])) |
325 | + hp.register_uri(hp.GET, |
326 | + eu.combine_url(base_url, 'public-keys/0/openssh-key'), |
327 | + status=200, body='ssh-rsa AAAA.....wZEf my-public-key') |
328 | + hp.register_uri(hp.GET, |
329 | + eu.combine_url(base_url, 'public-keys/1/openssh-key'), |
330 | + status=200, body='ssh-rsa AAAA.....wZEf my-other-key') |
331 | + md = eu.get_instance_metadata(self.VERSION, retries=0, timeout=0.1) |
332 | + self.assertEquals(md['hostname'], 'ec2.fake.host.name.com') |
333 | + self.assertEquals(md['instance-id'], '123') |
334 | + self.assertEquals(2, len(md['public-keys'])) |
335 | + |
336 | + @hp.activate |
337 | + def test_metadata_fetch_bdm(self): |
338 | + base_url = 'http://169.254.169.254/%s/meta-data' % (self.VERSION) |
339 | + hp.register_uri(hp.GET, base_url, status=200, |
340 | + body="\n".join(['hostname', |
341 | + 'instance-id', |
342 | + 'block-device-mapping/'])) |
343 | + hp.register_uri(hp.GET, eu.combine_url(base_url, 'hostname'), |
344 | + status=200, body='ec2.fake.host.name.com') |
345 | + hp.register_uri(hp.GET, eu.combine_url(base_url, 'instance-id'), |
346 | + status=200, body='123') |
347 | + hp.register_uri(hp.GET, |
348 | + eu.combine_url(base_url, 'block-device-mapping/'), |
349 | + status=200, |
350 | + body="\n".join(['ami', 'ephemeral0'])) |
351 | + hp.register_uri(hp.GET, |
352 | + eu.combine_url(base_url, 'block-device-mapping/ami'), |
353 | + status=200, |
354 | + body="sdb") |
355 | + hp.register_uri(hp.GET, |
356 | + eu.combine_url(base_url, |
357 | + 'block-device-mapping/ephemeral0'), |
358 | + status=200, |
359 | + body="sdc") |
360 | + md = eu.get_instance_metadata(self.VERSION, retries=0, timeout=0.1) |
361 | + self.assertEquals(md['hostname'], 'ec2.fake.host.name.com') |
362 | + self.assertEquals(md['instance-id'], '123') |
363 | + bdm = md['block-device-mapping'] |
364 | + self.assertEquals(2, len(bdm)) |
365 | + self.assertEquals(bdm['ami'], 'sdb') |
366 | + self.assertEquals(bdm['ephemeral0'], 'sdc') |