Merge lp:~harlowja/cloud-init/url-ssl-fixings into lp:~cloud-init-dev/cloud-init/trunk
- url-ssl-fixings
- Merge into trunk
Status: | Merged | ||||
---|---|---|---|---|---|
Merged at revision: | 801 | ||||
Proposed branch: | lp:~harlowja/cloud-init/url-ssl-fixings | ||||
Merge into: | lp:~cloud-init-dev/cloud-init/trunk | ||||
Diff against target: |
890 lines (+316/-163) 15 files modified
Requires (+6/-5) cloudinit/config/cc_phone_home.py (+3/-2) cloudinit/config/cc_power_state_change.py (+1/-1) cloudinit/distros/parsers/resolv_conf.py (+2/-2) cloudinit/ec2_utils.py (+5/-0) cloudinit/sources/DataSourceCloudStack.py (+6/-5) cloudinit/sources/DataSourceEc2.py (+7/-6) cloudinit/sources/DataSourceMAAS.py (+26/-19) cloudinit/url_helper.py (+159/-98) cloudinit/user_data.py (+3/-2) cloudinit/util.py (+79/-10) setup.py (+3/-2) tests/unittests/test__init__.py (+5/-5) tests/unittests/test_datasource/test_maas.py (+9/-6) tests/unittests/test_util.py (+2/-0) |
||||
To merge this branch: | bzr merge lp:~harlowja/cloud-init/url-ssl-fixings | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Scott Moser | Needs Fixing | ||
Review via email: mp+149481@code.launchpad.net |
Commit message
Description of the change
- 695. By harlowja <email address hidden>
-
More work on requests integration.
- 696. By harlowja <email address hidden>
-
Update to code on trunk.
- 697. By harlowja <email address hidden>
-
Why did this file showup.
- 698. By harlowja <email address hidden>
-
Get tests working and further adjustments.
- 699. By harlowja <email address hidden>
-
Fix the maas callback mechanism now that requests is used.
Joshua Harlow (harlowja) wrote : | # |
Scott Moser (smoser) wrote : | # |
I'm sorry this is such a pain.
$ python -c 'from cloudinit import util; util.read_
Traceback (most recent call last):
File "<string>", line 1, in <module>
File "cloudinit/
ssl_
File "cloudinit/
r = requests.
File "/usr/lib/
return session.
TypeError: request() got an unexpected keyword argument 'config'
$ dpkg-query --show python-requests
python-requests 1.1.0-1
See https:/
precise: 0.8.2-1
quantal: 0.12.1-1ubuntu6
raring: 1.1.0-1
latest-release: 1.1.0
Your committed code does work on precise and quantal.
Scott Moser (smoser) wrote : | # |
Also:
$ python -c 'from cloudinit import util; import sys; print util.read_
Traceback (most recent call last):
File "<string>", line 1, in <module>
File "cloudinit/
ssl_
File "cloudinit/
if isinstance(e, (exceptions.
AttributeError: 'HTTPError' object has no attribute 'response'
$ dpkg-query --show python-requests
python-requests 0.8.2-1
- 700. By Joshua Harlow
-
Update to handle requests >= 1.0 which doesn't use the config dict.
- 701. By Joshua Harlow
-
Fix how the http error doesn't always have the response attached
in earlier versions of requests (pre 0.10.8).
Joshua Harlow (harlowja) wrote : | # |
Ok dokie, think those are all fixed by more checks on the requests version and the http error exception object.
- 702. By Scott Moser
-
merge from trunk at revno 799
- 703. By Scott Moser
-
merge from trunk rev 800
- 704. By Scott Moser
-
appease pylint and pep8
* cloudinit/
distros/ parsers/ resolv_ conf.py
added some pylint overrides with 'plXXXXX' syntax.
example: # pl51222 pylint: disable=E0102The pl51222 there means: http://
www.logilab. org/ticket/ 51222 This specific issue is present in 12.04 pylint, but not 13.04.
* pylint doesn't like the requests special handling we have.
which makes sense as it is only checking versus one specific version.
* general pep8 and pylint cleanups. - 705. By Scott Moser
-
do not bother retrying on ssl errors
if the error is an ssl error, its extremely unlikely that it would be fixed by
waiting a few seconds and trying again. - 706. By Scott Moser
-
set 'allow_redirects' to True by default
the previous implementation of url_helper.
readurl( ) would default
to allow_redirects being true.So, for backwards compat, we should keep that behavior.
- 707. By Scott Moser
-
make get_instance_
userdata and get_instance_ metadata more like botos this shouldn't change anything, only the signatures of the methods.
- 708. By Scott Moser
-
fix typo
- 709. By Joshua Harlow
-
Move back to using boto for now.
- 710. By Joshua Harlow
-
Add doc about issue 1401 in boto.
Preview Diff
1 | === modified file 'Requires' |
2 | --- Requires 2012-07-09 20:41:45 +0000 |
3 | +++ Requires 2013-03-19 22:58:20 +0000 |
4 | @@ -10,11 +10,6 @@ |
5 | # datasource is removed, this is no longer needed |
6 | oauth |
7 | |
8 | -# This is used to fetch the ec2 metadata into a easily |
9 | -# parseable format, instead of having to have cloud-init perform |
10 | -# those same fetchs and decodes and signing (...) that ec2 requires. |
11 | -boto |
12 | - |
13 | # This is only needed for places where we need to support configs in a manner |
14 | # that the built-in config parser is not sufficent (ie |
15 | # when we need to preserve comments, or do not have a top-level |
16 | @@ -26,3 +21,9 @@ |
17 | |
18 | # The new main entrypoint uses argparse instead of optparse |
19 | argparse |
20 | + |
21 | +# Requests handles ssl correctly! |
22 | +requests |
23 | + |
24 | +# Boto for ec2 |
25 | +boto |
26 | |
27 | === modified file 'cloudinit/config/cc_phone_home.py' |
28 | --- cloudinit/config/cc_phone_home.py 2012-10-28 02:25:48 +0000 |
29 | +++ cloudinit/config/cc_phone_home.py 2013-03-19 22:58:20 +0000 |
30 | @@ -19,7 +19,6 @@ |
31 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
32 | |
33 | from cloudinit import templater |
34 | -from cloudinit import url_helper as uhelp |
35 | from cloudinit import util |
36 | |
37 | from cloudinit.settings import PER_INSTANCE |
38 | @@ -112,7 +111,9 @@ |
39 | } |
40 | url = templater.render_string(url, url_params) |
41 | try: |
42 | - uhelp.readurl(url, data=real_submit_keys, retries=tries, sec_between=3) |
43 | + util.read_file_or_url(url, data=real_submit_keys, |
44 | + retries=tries, sec_between=3, |
45 | + ssl_details=util.fetch_ssl_details(cloud.paths)) |
46 | except: |
47 | util.logexc(log, ("Failed to post phone home data to" |
48 | " %s in %s tries"), url, tries) |
49 | |
50 | === modified file 'cloudinit/config/cc_power_state_change.py' |
51 | --- cloudinit/config/cc_power_state_change.py 2013-03-07 19:54:25 +0000 |
52 | +++ cloudinit/config/cc_power_state_change.py 2013-03-19 22:58:20 +0000 |
53 | @@ -100,7 +100,7 @@ |
54 | proc = subprocess.Popen(exe_args, stdin=subprocess.PIPE, |
55 | stdout=output, stderr=subprocess.STDOUT) |
56 | proc.communicate(data_in) |
57 | - ret = proc.returncode |
58 | + ret = proc.returncode # pylint: disable=E1101 |
59 | except Exception: |
60 | doexit(EXIT_FAIL) |
61 | doexit(ret) |
62 | |
63 | === modified file 'cloudinit/distros/parsers/resolv_conf.py' |
64 | --- cloudinit/distros/parsers/resolv_conf.py 2012-11-12 22:30:08 +0000 |
65 | +++ cloudinit/distros/parsers/resolv_conf.py 2013-03-19 22:58:20 +0000 |
66 | @@ -137,8 +137,8 @@ |
67 | self._contents.append(('option', ['search', s_list, ''])) |
68 | return flat_sds |
69 | |
70 | - @local_domain.setter |
71 | - def local_domain(self, domain): |
72 | + @local_domain.setter # pl51222 pylint: disable=E1101 |
73 | + def local_domain(self, domain): # pl51222 pylint: disable=E0102 |
74 | self.parse() |
75 | self._remove_option('domain') |
76 | self._contents.append(('option', ['domain', str(domain), ''])) |
77 | |
78 | === modified file 'cloudinit/ec2_utils.py' |
79 | --- cloudinit/ec2_utils.py 2012-11-13 19:02:03 +0000 |
80 | +++ cloudinit/ec2_utils.py 2013-03-19 22:58:20 +0000 |
81 | @@ -28,6 +28,10 @@ |
82 | # would have existed) do not exist due to the blocking |
83 | # that occurred. |
84 | |
85 | +# TODO: https://github.com/boto/boto/issues/1401 |
86 | +# When boto finally moves to using requests, we should be able |
87 | +# to provide it ssl details, it does not yet, so we can't provide them... |
88 | + |
89 | |
90 | def _unlazy_dict(mp): |
91 | if not isinstance(mp, (dict)): |
92 | @@ -57,3 +61,4 @@ |
93 | if not isinstance(metadata, (dict)): |
94 | metadata = {} |
95 | return _unlazy_dict(metadata) |
96 | + |
97 | |
98 | === modified file 'cloudinit/sources/DataSourceCloudStack.py' |
99 | --- cloudinit/sources/DataSourceCloudStack.py 2013-03-07 21:27:47 +0000 |
100 | +++ cloudinit/sources/DataSourceCloudStack.py 2013-03-19 22:58:20 +0000 |
101 | @@ -25,7 +25,7 @@ |
102 | import os |
103 | import time |
104 | |
105 | -from cloudinit import ec2_utils as ec2 |
106 | +from cloudinit import ec2_utils |
107 | from cloudinit import log as logging |
108 | from cloudinit import sources |
109 | from cloudinit import url_helper as uhelp |
110 | @@ -101,10 +101,11 @@ |
111 | if not self.wait_for_metadata_service(): |
112 | return False |
113 | start_time = time.time() |
114 | - self.userdata_raw = ec2.get_instance_userdata(self.api_ver, |
115 | - self.metadata_address) |
116 | - self.metadata = ec2.get_instance_metadata(self.api_ver, |
117 | - self.metadata_address) |
118 | + md_addr = self.metadata_address |
119 | + self.userdata_raw = ec2_utils.get_instance_userdata(self.api_ver, |
120 | + md_addr) |
121 | + self.metadata = ec2_utils.get_instance_metadata(self.api_ver, |
122 | + md_addr) |
123 | LOG.debug("Crawl of metadata service took %s seconds", |
124 | int(time.time() - start_time)) |
125 | return True |
126 | |
127 | === modified file 'cloudinit/sources/DataSourceEc2.py' |
128 | --- cloudinit/sources/DataSourceEc2.py 2013-03-07 03:24:05 +0000 |
129 | +++ cloudinit/sources/DataSourceEc2.py 2013-03-19 22:58:20 +0000 |
130 | @@ -23,7 +23,7 @@ |
131 | import os |
132 | import time |
133 | |
134 | -from cloudinit import ec2_utils as ec2 |
135 | +from cloudinit import ec2_utils |
136 | from cloudinit import log as logging |
137 | from cloudinit import sources |
138 | from cloudinit import url_helper as uhelp |
139 | @@ -61,10 +61,11 @@ |
140 | if not self.wait_for_metadata_service(): |
141 | return False |
142 | start_time = time.time() |
143 | - self.userdata_raw = ec2.get_instance_userdata(self.api_ver, |
144 | - self.metadata_address) |
145 | - self.metadata = ec2.get_instance_metadata(self.api_ver, |
146 | - self.metadata_address) |
147 | + md_addr = self.metadata_address |
148 | + self.userdata_raw = ec2_utils.get_instance_userdata(self.api_ver, |
149 | + md_addr) |
150 | + self.metadata = ec2_utils.get_instance_metadata(self.api_ver, |
151 | + md_addr) |
152 | LOG.debug("Crawl of metadata service took %s seconds", |
153 | int(time.time() - start_time)) |
154 | return True |
155 | @@ -133,7 +134,7 @@ |
156 | |
157 | start_time = time.time() |
158 | url = uhelp.wait_for_url(urls=urls, max_wait=max_wait, |
159 | - timeout=timeout, status_cb=LOG.warn) |
160 | + timeout=timeout, status_cb=LOG.warn) |
161 | |
162 | if url: |
163 | LOG.debug("Using metadata source: '%s'", url2base[url]) |
164 | |
165 | === modified file 'cloudinit/sources/DataSourceMAAS.py' |
166 | --- cloudinit/sources/DataSourceMAAS.py 2013-03-07 03:24:05 +0000 |
167 | +++ cloudinit/sources/DataSourceMAAS.py 2013-03-19 22:58:20 +0000 |
168 | @@ -27,7 +27,7 @@ |
169 | |
170 | from cloudinit import log as logging |
171 | from cloudinit import sources |
172 | -from cloudinit import url_helper as uhelp |
173 | +from cloudinit import url_helper |
174 | from cloudinit import util |
175 | |
176 | LOG = logging.getLogger(__name__) |
177 | @@ -80,7 +80,8 @@ |
178 | self.base_url = url |
179 | |
180 | (userdata, metadata) = read_maas_seed_url(self.base_url, |
181 | - self.md_headers) |
182 | + self._md_headers, |
183 | + paths=self.paths) |
184 | self.userdata_raw = userdata |
185 | self.metadata = metadata |
186 | return True |
187 | @@ -88,7 +89,7 @@ |
188 | util.logexc(LOG, "Failed fetching metadata from url %s", url) |
189 | return False |
190 | |
191 | - def md_headers(self, url): |
192 | + def _md_headers(self, url): |
193 | mcfg = self.ds_cfg |
194 | |
195 | # If we are missing token_key, token_secret or consumer_key |
196 | @@ -132,36 +133,37 @@ |
197 | starttime = time.time() |
198 | check_url = "%s/%s/meta-data/instance-id" % (url, MD_VERSION) |
199 | urls = [check_url] |
200 | - url = uhelp.wait_for_url(urls=urls, max_wait=max_wait, |
201 | - timeout=timeout, exception_cb=self._except_cb, |
202 | - headers_cb=self.md_headers) |
203 | + url = url_helper.wait_for_url(urls=urls, max_wait=max_wait, |
204 | + timeout=timeout, |
205 | + exception_cb=self._except_cb, |
206 | + headers_cb=self._md_headers) |
207 | |
208 | if url: |
209 | LOG.debug("Using metadata source: '%s'", url) |
210 | else: |
211 | LOG.critical("Giving up on md from %s after %i seconds", |
212 | - urls, int(time.time() - starttime)) |
213 | + urls, int(time.time() - starttime)) |
214 | |
215 | return bool(url) |
216 | |
217 | def _except_cb(self, msg, exception): |
218 | - if not (isinstance(exception, urllib2.HTTPError) and |
219 | + if not (isinstance(exception, url_helper.UrlError) and |
220 | (exception.code == 403 or exception.code == 401)): |
221 | return |
222 | + |
223 | if 'date' not in exception.headers: |
224 | - LOG.warn("date field not in %d headers" % exception.code) |
225 | + LOG.warn("Missing header 'date' in %s response", exception.code) |
226 | return |
227 | |
228 | date = exception.headers['date'] |
229 | - |
230 | try: |
231 | ret_time = time.mktime(parsedate(date)) |
232 | - except: |
233 | - LOG.warn("failed to convert datetime '%s'") |
234 | + except Exception as e: |
235 | + LOG.warn("Failed to convert datetime '%s': %s", date, e) |
236 | return |
237 | |
238 | self.oauth_clockskew = int(ret_time - time.time()) |
239 | - LOG.warn("set oauth clockskew to %d" % self.oauth_clockskew) |
240 | + LOG.warn("Setting oauth clockskew to %d", self.oauth_clockskew) |
241 | return |
242 | |
243 | |
244 | @@ -189,11 +191,11 @@ |
245 | |
246 | |
247 | def read_maas_seed_url(seed_url, header_cb=None, timeout=None, |
248 | - version=MD_VERSION): |
249 | + version=MD_VERSION, paths=None): |
250 | """ |
251 | Read the maas datasource at seed_url. |
252 | - header_cb is a method that should return a headers dictionary that will |
253 | - be given to urllib2.Request() |
254 | + - header_cb is a method that should return a headers dictionary for |
255 | + a given url |
256 | |
257 | Expected format of seed_url is are the following files: |
258 | * <seed_url>/<version>/meta-data/instance-id |
259 | @@ -221,13 +223,17 @@ |
260 | else: |
261 | headers = {} |
262 | try: |
263 | - resp = uhelp.readurl(url, headers=headers, timeout=timeout) |
264 | + ssl_details = util.fetch_ssl_details(paths) |
265 | + resp = util.read_file_or_url(url, |
266 | + headers=headers, |
267 | + timeout=timeout, |
268 | + ssl_details=ssl_details) |
269 | if resp.ok(): |
270 | md[name] = str(resp) |
271 | else: |
272 | LOG.warn(("Fetching from %s resulted in" |
273 | " an invalid http code %s"), url, resp.code) |
274 | - except urllib2.HTTPError as e: |
275 | + except url_helper.UrlError as e: |
276 | if e.code != 404: |
277 | raise |
278 | return check_seed_contents(md, seed_url) |
279 | @@ -370,7 +376,8 @@ |
280 | if args.subcmd == "check-seed": |
281 | if args.url.startswith("http"): |
282 | (userdata, metadata) = read_maas_seed_url(args.url, |
283 | - header_cb=my_headers, version=args.apiver) |
284 | + header_cb=my_headers, |
285 | + version=args.apiver) |
286 | else: |
287 | (userdata, metadata) = read_maas_seed_url(args.url) |
288 | print "=== userdata ===" |
289 | |
290 | === modified file 'cloudinit/url_helper.py' |
291 | --- cloudinit/url_helper.py 2012-09-24 21:13:38 +0000 |
292 | +++ cloudinit/url_helper.py 2013-03-19 22:58:20 +0000 |
293 | @@ -20,43 +20,55 @@ |
294 | # You should have received a copy of the GNU General Public License |
295 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
296 | |
297 | -from contextlib import closing |
298 | - |
299 | -import errno |
300 | -import socket |
301 | import time |
302 | -import urllib |
303 | -import urllib2 |
304 | + |
305 | +import requests |
306 | +from requests import exceptions |
307 | + |
308 | +from urlparse import (urlparse, urlunparse) |
309 | |
310 | from cloudinit import log as logging |
311 | from cloudinit import version |
312 | |
313 | LOG = logging.getLogger(__name__) |
314 | |
315 | +# Check if requests has ssl support (added in requests >= 0.8.8) |
316 | +SSL_ENABLED = False |
317 | +CONFIG_ENABLED = False # This was added in 0.7 (but taken out in >=1.0) |
318 | +try: |
319 | + from distutils.version import LooseVersion |
320 | + import pkg_resources |
321 | + _REQ = pkg_resources.get_distribution('requests') |
322 | + _REQ_VER = LooseVersion(_REQ.version) # pylint: disable=E1103 |
323 | + if _REQ_VER >= LooseVersion('0.8.8'): |
324 | + SSL_ENABLED = True |
325 | + if _REQ_VER >= LooseVersion('0.7.0') and _REQ_VER < LooseVersion('1.0.0'): |
326 | + CONFIG_ENABLED = True |
327 | +except: |
328 | + pass |
329 | + |
330 | + |
331 | +def _cleanurl(url): |
332 | + parsed_url = list(urlparse(url, scheme='http')) # pylint: disable=E1123 |
333 | + if not parsed_url[1] and parsed_url[2]: |
334 | + # Swap these since this seems to be a common |
335 | + # occurrence when given urls like 'www.google.com' |
336 | + parsed_url[1] = parsed_url[2] |
337 | + parsed_url[2] = '' |
338 | + return urlunparse(parsed_url) |
339 | + |
340 | |
341 | class UrlResponse(object): |
342 | - def __init__(self, status_code, contents=None, headers=None): |
343 | - self._status_code = status_code |
344 | - self._contents = contents |
345 | - self._headers = headers |
346 | - |
347 | - @property |
348 | - def code(self): |
349 | - return self._status_code |
350 | + def __init__(self, response): |
351 | + self._response = response |
352 | |
353 | @property |
354 | def contents(self): |
355 | - return self._contents |
356 | + return self._response.content |
357 | |
358 | @property |
359 | - def headers(self): |
360 | - return self._headers |
361 | - |
362 | - def __str__(self): |
363 | - if not self.contents: |
364 | - return '' |
365 | - else: |
366 | - return str(self.contents) |
367 | + def url(self): |
368 | + return self._response.url |
369 | |
370 | def ok(self, redirects_ok=False): |
371 | upper = 300 |
372 | @@ -67,72 +79,117 @@ |
373 | else: |
374 | return False |
375 | |
376 | - |
377 | -def readurl(url, data=None, timeout=None, |
378 | - retries=0, sec_between=1, headers=None): |
379 | - |
380 | - req_args = {} |
381 | - req_args['url'] = url |
382 | - if data is not None: |
383 | - req_args['data'] = urllib.urlencode(data) |
384 | - |
385 | + @property |
386 | + def headers(self): |
387 | + return self._response.headers |
388 | + |
389 | + @property |
390 | + def code(self): |
391 | + return self._response.status_code |
392 | + |
393 | + def __str__(self): |
394 | + return self.contents |
395 | + |
396 | + |
397 | +class UrlError(IOError): |
398 | + def __init__(self, cause, code=None, headers=None): |
399 | + IOError.__init__(self, str(cause)) |
400 | + self.cause = cause |
401 | + self.code = code |
402 | + self.headers = headers |
403 | + if self.headers is None: |
404 | + self.headers = {} |
405 | + |
406 | + |
407 | +def readurl(url, data=None, timeout=None, retries=0, sec_between=1, |
408 | + headers=None, ssl_details=None, check_status=True, |
409 | + allow_redirects=True): |
410 | + url = _cleanurl(url) |
411 | + req_args = { |
412 | + 'url': url, |
413 | + } |
414 | + scheme = urlparse(url).scheme # pylint: disable=E1101 |
415 | + if scheme == 'https' and ssl_details: |
416 | + if not SSL_ENABLED: |
417 | + LOG.warn("SSL is not enabled, cert. verification can not occur!") |
418 | + else: |
419 | + if 'ca_certs' in ssl_details and ssl_details['ca_certs']: |
420 | + req_args['verify'] = ssl_details['ca_certs'] |
421 | + else: |
422 | + req_args['verify'] = True |
423 | + if 'cert_file' in ssl_details and 'key_file' in ssl_details: |
424 | + req_args['cert'] = [ssl_details['cert_file'], |
425 | + ssl_details['key_file']] |
426 | + elif 'cert_file' in ssl_details: |
427 | + req_args['cert'] = str(ssl_details['cert_file']) |
428 | + |
429 | + req_args['allow_redirects'] = allow_redirects |
430 | + req_args['method'] = 'GET' |
431 | + if timeout is not None: |
432 | + req_args['timeout'] = max(float(timeout), 0) |
433 | + if data: |
434 | + req_args['method'] = 'POST' |
435 | + # It doesn't seem like config |
436 | + # was added in older library versions (or newer ones either), thus we |
437 | + # need to manually do the retries if it wasn't... |
438 | + if CONFIG_ENABLED: |
439 | + req_config = { |
440 | + 'store_cookies': False, |
441 | + } |
442 | + # Don't use the retry support built-in |
443 | + # since it doesn't allow for 'sleep_times' |
444 | + # in between tries.... |
445 | + # if retries: |
446 | + # req_config['max_retries'] = max(int(retries), 0) |
447 | + req_args['config'] = req_config |
448 | + manual_tries = 1 |
449 | + if retries: |
450 | + manual_tries = max(int(retries) + 1, 1) |
451 | if not headers: |
452 | headers = { |
453 | 'User-Agent': 'Cloud-Init/%s' % (version.version_string()), |
454 | } |
455 | - |
456 | req_args['headers'] = headers |
457 | - req = urllib2.Request(**req_args) |
458 | - |
459 | - retries = max(retries, 0) |
460 | - attempts = retries + 1 |
461 | - |
462 | - excepts = [] |
463 | - LOG.debug(("Attempting to open '%s' with %s attempts" |
464 | - " (%s retries, timeout=%s) to be performed"), |
465 | - url, attempts, retries, timeout) |
466 | - open_args = {} |
467 | - if timeout is not None: |
468 | - open_args['timeout'] = int(timeout) |
469 | - for i in range(0, attempts): |
470 | + LOG.debug("Attempting to open '%s' with %s configuration", url, req_args) |
471 | + if data: |
472 | + # Do this after the log (it might be large) |
473 | + req_args['data'] = data |
474 | + if sec_between is None: |
475 | + sec_between = -1 |
476 | + excps = [] |
477 | + # Handle retrying ourselves since the built-in support |
478 | + # doesn't handle sleeping between tries... |
479 | + for i in range(0, manual_tries): |
480 | try: |
481 | - with closing(urllib2.urlopen(req, **open_args)) as rh: |
482 | - content = rh.read() |
483 | - status = rh.getcode() |
484 | - if status is None: |
485 | - # This seems to happen when files are read... |
486 | - status = 200 |
487 | - headers = {} |
488 | - if rh.headers: |
489 | - headers = dict(rh.headers) |
490 | - LOG.debug("Read from %s (%s, %sb) after %s attempts", |
491 | - url, status, len(content), (i + 1)) |
492 | - return UrlResponse(status, content, headers) |
493 | - except urllib2.HTTPError as e: |
494 | - excepts.append(e) |
495 | - except urllib2.URLError as e: |
496 | - # This can be a message string or |
497 | - # another exception instance |
498 | - # (socket.error for remote URLs, OSError for local URLs). |
499 | - if (isinstance(e.reason, (OSError)) and |
500 | - e.reason.errno == errno.ENOENT): |
501 | - excepts.append(e.reason) |
502 | + r = requests.request(**req_args) |
503 | + if check_status: |
504 | + r.raise_for_status() # pylint: disable=E1103 |
505 | + LOG.debug("Read from %s (%s, %sb) after %s attempts", url, |
506 | + r.status_code, len(r.content), # pylint: disable=E1103 |
507 | + (i + 1)) |
508 | + # Doesn't seem like we can make it use a different |
509 | + # subclass for responses, so add our own backward-compat |
510 | + # attrs |
511 | + return UrlResponse(r) |
512 | + except exceptions.RequestException as e: |
513 | + if (isinstance(e, (exceptions.HTTPError)) |
514 | + and hasattr(e, 'response') # This appeared in v 0.10.8 |
515 | + and e.response): |
516 | + excps.append(UrlError(e, code=e.response.status_code, |
517 | + headers=e.response.headers)) |
518 | else: |
519 | - excepts.append(e) |
520 | - except Exception as e: |
521 | - excepts.append(e) |
522 | - if i + 1 < attempts: |
523 | - LOG.debug("Please wait %s seconds while we wait to try again", |
524 | - sec_between) |
525 | - time.sleep(sec_between) |
526 | - |
527 | - # Didn't work out |
528 | - LOG.debug("Failed reading from %s after %s attempts", url, attempts) |
529 | - |
530 | - # It must of errored at least once for code |
531 | - # to get here so re-raise the last error |
532 | - LOG.debug("%s errors occured, re-raising the last one", len(excepts)) |
533 | - raise excepts[-1] |
534 | + excps.append(UrlError(e)) |
535 | + if SSL_ENABLED and isinstance(e, exceptions.SSLError): |
536 | + # ssl exceptions are not going to get fixed by waiting a |
537 | + # few seconds |
538 | + break |
539 | + if i + 1 < manual_tries and sec_between > 0: |
540 | + LOG.debug("Please wait %s seconds while we wait to try again", |
541 | + sec_between) |
542 | + time.sleep(sec_between) |
543 | + if excps: |
544 | + raise excps[-1] |
545 | + return None # Should throw before this... |
546 | |
547 | |
548 | def wait_for_url(urls, max_wait=None, timeout=None, |
549 | @@ -143,7 +200,7 @@ |
550 | max_wait: roughly the maximum time to wait before giving up |
551 | The max time is *actually* len(urls)*timeout as each url will |
552 | be tried once and given the timeout provided. |
553 | - timeout: the timeout provided to urllib2.urlopen |
554 | + timeout: the timeout provided to urlopen |
555 | status_cb: call method with string message when a url is not available |
556 | headers_cb: call method with single argument of url to get headers |
557 | for request. |
558 | @@ -190,36 +247,40 @@ |
559 | timeout = int((start_time + max_wait) - now) |
560 | |
561 | reason = "" |
562 | + e = None |
563 | try: |
564 | if headers_cb is not None: |
565 | headers = headers_cb(url) |
566 | else: |
567 | headers = {} |
568 | |
569 | - resp = readurl(url, headers=headers, timeout=timeout) |
570 | - if not resp.contents: |
571 | - reason = "empty response [%s]" % (resp.code) |
572 | - e = ValueError(reason) |
573 | - elif not resp.ok(): |
574 | - reason = "bad status code [%s]" % (resp.code) |
575 | - e = ValueError(reason) |
576 | + response = readurl(url, headers=headers, timeout=timeout, |
577 | + check_status=False) |
578 | + if not response.contents: |
579 | + reason = "empty response [%s]" % (response.code) |
580 | + e = UrlError(ValueError(reason), |
581 | + code=response.code, headers=response.headers) |
582 | + elif not response.ok(): |
583 | + reason = "bad status code [%s]" % (response.code) |
584 | + e = UrlError(ValueError(reason), |
585 | + code=response.code, headers=response.headers) |
586 | else: |
587 | return url |
588 | - except urllib2.HTTPError as e: |
589 | - reason = "http error [%s]" % e.code |
590 | - except urllib2.URLError as e: |
591 | - reason = "url error [%s]" % e.reason |
592 | - except socket.timeout as e: |
593 | - reason = "socket timeout [%s]" % e |
594 | + except UrlError as e: |
595 | + reason = "request error [%s]" % e |
596 | except Exception as e: |
597 | reason = "unexpected error [%s]" % e |
598 | |
599 | time_taken = int(time.time() - start_time) |
600 | status_msg = "Calling '%s' failed [%s/%ss]: %s" % (url, |
601 | - time_taken, |
602 | - max_wait, reason) |
603 | + time_taken, |
604 | + max_wait, |
605 | + reason) |
606 | status_cb(status_msg) |
607 | if exception_cb: |
608 | + # This can be used to alter the headers that will be sent |
609 | + # in the future, for example this is what the MAAS datasource |
610 | + # does. |
611 | exception_cb(msg=status_msg, exception=e) |
612 | |
613 | if timeup(max_wait, start_time): |
614 | |
615 | === modified file 'cloudinit/user_data.py' |
616 | --- cloudinit/user_data.py 2012-10-10 16:27:28 +0000 |
617 | +++ cloudinit/user_data.py 2013-03-19 22:58:20 +0000 |
618 | @@ -29,7 +29,6 @@ |
619 | |
620 | from cloudinit import handlers |
621 | from cloudinit import log as logging |
622 | -from cloudinit import url_helper |
623 | from cloudinit import util |
624 | |
625 | LOG = logging.getLogger(__name__) |
626 | @@ -60,6 +59,7 @@ |
627 | class UserDataProcessor(object): |
628 | def __init__(self, paths): |
629 | self.paths = paths |
630 | + self.ssl_details = util.fetch_ssl_details(paths) |
631 | |
632 | def process(self, blob): |
633 | accumulating_msg = MIMEMultipart() |
634 | @@ -173,7 +173,8 @@ |
635 | if include_once_on and os.path.isfile(include_once_fn): |
636 | content = util.load_file(include_once_fn) |
637 | else: |
638 | - resp = url_helper.readurl(include_url) |
639 | + resp = util.read_file_or_url(include_url, |
640 | + ssl_details=self.ssl_details) |
641 | if include_once_on and resp.ok(): |
642 | util.write_file(include_once_fn, str(resp), mode=0600) |
643 | if resp.ok(): |
644 | |
645 | === modified file 'cloudinit/util.py' |
646 | --- cloudinit/util.py 2013-03-13 14:43:40 +0000 |
647 | +++ cloudinit/util.py 2013-03-19 22:58:20 +0000 |
648 | @@ -52,7 +52,7 @@ |
649 | from cloudinit import mergers |
650 | from cloudinit import safeyaml |
651 | from cloudinit import type_utils |
652 | -from cloudinit import url_helper as uhelp |
653 | +from cloudinit import url_helper |
654 | from cloudinit import version |
655 | |
656 | from cloudinit.settings import (CFG_BUILTIN) |
657 | @@ -71,6 +71,31 @@ |
658 | CONTAINER_TESTS = ['running-in-container', 'lxc-is-container'] |
659 | |
660 | |
661 | +# Made to have same accessors as UrlResponse so that the |
662 | +# read_file_or_url can return this or that object and the |
663 | +# 'user' of those objects will not need to know the difference. |
664 | +class StringResponse(object): |
665 | + def __init__(self, contents, code=200): |
666 | + self.code = code |
667 | + self.headers = {} |
668 | + self.contents = contents |
669 | + self.url = None |
670 | + |
671 | + def ok(self, *args, **kwargs): # pylint: disable=W0613 |
672 | + if self.code != 200: |
673 | + return False |
674 | + return True |
675 | + |
676 | + def __str__(self): |
677 | + return self.contents |
678 | + |
679 | + |
680 | +class FileResponse(StringResponse): |
681 | + def __init__(self, path, contents, code=200): |
682 | + StringResponse.__init__(self, contents, code=code) |
683 | + self.url = path |
684 | + |
685 | + |
686 | class ProcessExecutionError(IOError): |
687 | |
688 | MESSAGE_TMPL = ('%(description)s\n' |
689 | @@ -462,7 +487,7 @@ |
690 | new_fp = open(arg, owith) |
691 | elif mode == "|": |
692 | proc = subprocess.Popen(arg, shell=True, stdin=subprocess.PIPE) |
693 | - new_fp = proc.stdin |
694 | + new_fp = proc.stdin # pylint: disable=E1101 |
695 | else: |
696 | raise TypeError("Invalid type for output format: %s" % outfmt) |
697 | |
698 | @@ -484,7 +509,7 @@ |
699 | new_fp = open(arg, owith) |
700 | elif mode == "|": |
701 | proc = subprocess.Popen(arg, shell=True, stdin=subprocess.PIPE) |
702 | - new_fp = proc.stdin |
703 | + new_fp = proc.stdin # pylint: disable=E1101 |
704 | else: |
705 | raise TypeError("Invalid type for error format: %s" % errfmt) |
706 | |
707 | @@ -606,18 +631,62 @@ |
708 | fill['user-data'] = ud |
709 | fill['meta-data'] = md |
710 | return True |
711 | - except OSError as e: |
712 | + except IOError as e: |
713 | if e.errno == errno.ENOENT: |
714 | return False |
715 | raise |
716 | |
717 | |
718 | -def read_file_or_url(url, timeout=5, retries=10, file_retries=0): |
719 | +def fetch_ssl_details(paths=None): |
720 | + ssl_details = {} |
721 | + # Lookup in these locations for ssl key/cert files |
722 | + ssl_cert_paths = [ |
723 | + '/var/lib/cloud/data/ssl', |
724 | + '/var/lib/cloud/instance/data/ssl', |
725 | + ] |
726 | + if paths: |
727 | + ssl_cert_paths.extend([ |
728 | + os.path.join(paths.get_ipath_cur('data'), 'ssl'), |
729 | + os.path.join(paths.get_cpath('data'), 'ssl'), |
730 | + ]) |
731 | + ssl_cert_paths = uniq_merge(ssl_cert_paths) |
732 | + ssl_cert_paths = [d for d in ssl_cert_paths if d and os.path.isdir(d)] |
733 | + cert_file = None |
734 | + for d in ssl_cert_paths: |
735 | + if os.path.isfile(os.path.join(d, 'cert.pem')): |
736 | + cert_file = os.path.join(d, 'cert.pem') |
737 | + break |
738 | + key_file = None |
739 | + for d in ssl_cert_paths: |
740 | + if os.path.isfile(os.path.join(d, 'key.pem')): |
741 | + key_file = os.path.join(d, 'key.pem') |
742 | + break |
743 | + if cert_file and key_file: |
744 | + ssl_details['cert_file'] = cert_file |
745 | + ssl_details['key_file'] = key_file |
746 | + elif cert_file: |
747 | + ssl_details['cert_file'] = cert_file |
748 | + return ssl_details |
749 | + |
750 | + |
751 | +def read_file_or_url(url, timeout=5, retries=10, |
752 | + headers=None, data=None, sec_between=1, ssl_details=None): |
753 | + url = url.lstrip() |
754 | if url.startswith("/"): |
755 | url = "file://%s" % url |
756 | - if url.startswith("file://"): |
757 | - retries = file_retries |
758 | - return uhelp.readurl(url, timeout=timeout, retries=retries) |
759 | + if url.lower().startswith("file://"): |
760 | + if data: |
761 | + LOG.warn("Unable to post data to file resource %s", url) |
762 | + file_path = url[len("file://"):] |
763 | + return FileResponse(file_path, contents=load_file(file_path)) |
764 | + else: |
765 | + return url_helper.readurl(url, |
766 | + timeout=timeout, |
767 | + retries=retries, |
768 | + headers=headers, |
769 | + data=data, |
770 | + sec_between=sec_between, |
771 | + ssl_details=ssl_details) |
772 | |
773 | |
774 | def load_yaml(blob, default=None, allowed=(dict,)): |
775 | @@ -834,7 +903,7 @@ |
776 | if not url: |
777 | return (None, None, None) |
778 | |
779 | - resp = uhelp.readurl(url) |
780 | + resp = read_file_or_url(url) |
781 | if resp.contents.startswith(starts) and resp.ok(): |
782 | return (key, url, str(resp)) |
783 | |
784 | @@ -1409,7 +1478,7 @@ |
785 | (out, err) = sp.communicate(data) |
786 | except OSError as e: |
787 | raise ProcessExecutionError(cmd=args, reason=e) |
788 | - rc = sp.returncode |
789 | + rc = sp.returncode # pylint: disable=E1101 |
790 | if rc not in rcs: |
791 | raise ProcessExecutionError(stdout=out, stderr=err, |
792 | exit_code=rc, |
793 | |
794 | === modified file 'setup.py' |
795 | --- setup.py 2013-02-21 14:07:54 +0000 |
796 | +++ setup.py 2013-03-19 22:58:20 +0000 |
797 | @@ -61,9 +61,10 @@ |
798 | sp = subprocess.Popen(cmd, stdout=stdout, |
799 | stderr=stderr, stdin=None) |
800 | (out, err) = sp.communicate() |
801 | - if sp.returncode not in [0]: |
802 | + ret = sp.returncode # pylint: disable=E1101 |
803 | + if ret not in [0]: |
804 | raise RuntimeError("Failed running %s [rc=%s] (%s, %s)" |
805 | - % (cmd, sp.returncode, out, err)) |
806 | + % (cmd, ret, out, err)) |
807 | return (out, err) |
808 | |
809 | |
810 | |
811 | === modified file 'tests/unittests/test__init__.py' |
812 | --- tests/unittests/test__init__.py 2013-03-07 22:13:05 +0000 |
813 | +++ tests/unittests/test__init__.py 2013-03-19 22:58:20 +0000 |
814 | @@ -195,8 +195,8 @@ |
815 | |
816 | mock_readurl = self.mocker.replace(url_helper.readurl, |
817 | passthrough=False) |
818 | - mock_readurl(url) |
819 | - self.mocker.result(url_helper.UrlResponse(200, payload)) |
820 | + mock_readurl(url, ARGS, KWARGS) |
821 | + self.mocker.result(util.StringResponse(payload)) |
822 | self.mocker.replay() |
823 | |
824 | self.assertEqual((key, url, None), |
825 | @@ -211,8 +211,8 @@ |
826 | |
827 | mock_readurl = self.mocker.replace(url_helper.readurl, |
828 | passthrough=False) |
829 | - mock_readurl(url) |
830 | - self.mocker.result(url_helper.UrlResponse(200, payload)) |
831 | + mock_readurl(url, ARGS, KWARGS) |
832 | + self.mocker.result(util.StringResponse(payload)) |
833 | self.mocker.replay() |
834 | |
835 | self.assertEqual((key, url, payload), |
836 | @@ -225,7 +225,7 @@ |
837 | cmdline = "ro %s=%s bar=1" % (key, url) |
838 | |
839 | self.mocker.replace(url_helper.readurl, passthrough=False) |
840 | - self.mocker.result(url_helper.UrlResponse(400)) |
841 | + self.mocker.result(util.StringResponse("")) |
842 | self.mocker.replay() |
843 | |
844 | self.assertEqual((None, None, None), |
845 | |
846 | === modified file 'tests/unittests/test_datasource/test_maas.py' |
847 | --- tests/unittests/test_datasource/test_maas.py 2013-02-07 17:08:30 +0000 |
848 | +++ tests/unittests/test_datasource/test_maas.py 2013-03-19 22:58:20 +0000 |
849 | @@ -3,12 +3,13 @@ |
850 | |
851 | from cloudinit.sources import DataSourceMAAS |
852 | from cloudinit import url_helper |
853 | +from cloudinit import util |
854 | from tests.unittests.helpers import populate_dir |
855 | |
856 | -from mocker import MockerTestCase |
857 | - |
858 | - |
859 | -class TestMAASDataSource(MockerTestCase): |
860 | +import mocker |
861 | + |
862 | + |
863 | +class TestMAASDataSource(mocker.MockerTestCase): |
864 | |
865 | def setUp(self): |
866 | super(TestMAASDataSource, self).setUp() |
867 | @@ -115,9 +116,11 @@ |
868 | |
869 | for key in valid_order: |
870 | url = "%s/%s/%s" % (my_seed, my_ver, key) |
871 | - mock_request(url, headers=my_headers, timeout=None) |
872 | + mock_request(url, headers=my_headers, timeout=mocker.ANY, |
873 | + data=mocker.ANY, sec_between=mocker.ANY, |
874 | + ssl_details=mocker.ANY, retries=mocker.ANY) |
875 | resp = valid.get(key) |
876 | - self.mocker.result(url_helper.UrlResponse(200, resp)) |
877 | + self.mocker.result(util.StringResponse(resp)) |
878 | self.mocker.replay() |
879 | |
880 | (userdata, metadata) = DataSourceMAAS.read_maas_seed_url(my_seed, |
881 | |
882 | === modified file 'tests/unittests/test_util.py' |
883 | --- tests/unittests/test_util.py 2013-03-11 02:47:46 +0000 |
884 | +++ tests/unittests/test_util.py 2013-03-19 22:58:20 +0000 |
885 | @@ -1,3 +1,5 @@ |
886 | +# pylint: disable=C0301 |
887 | +# the mountinfo data lines are too long |
888 | import os |
889 | import stat |
890 | import yaml |
So I looked into the requests code, they are using in a underlying layer the HTTP[d]Connection objects and the regular ssl object, this should allow it to use the system certificates as a fallback. So thats good news!