Merge lp:~harlowja/cloud-init/url-ssl-fixings into lp:~cloud-init-dev/cloud-init/trunk

Proposed by Joshua Harlow
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
Reviewer Review Type Date Requested Status
Scott Moser Needs Fixing
Review via email: mp+149481@code.launchpad.net
To post a comment you must log in.
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.

Revision history for this message
Joshua Harlow (harlowja) wrote :

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!

Revision history for this message
Scott Moser (smoser) wrote :

I'm sorry this is such a pain.
$ python -c 'from cloudinit import util; util.read_file_or_url("http://brickies.net")'
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "cloudinit/util.py", line 689, in read_file_or_url
    ssl_details=ssl_details)
  File "cloudinit/url_helper.py", line 163, in readurl
    r = requests.request(**req_args)
  File "/usr/lib/python2.7/dist-packages/requests/api.py", line 44, in request
    return session.request(method=method, url=url, **kwargs)
TypeError: request() got an unexpected keyword argument 'config'

$ dpkg-query --show python-requests
python-requests 1.1.0-1

See https://launchpad.net/ubuntu/+source/requests for all the ubuntu versions:
 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.

review: Needs Fixing
Revision history for this message
Scott Moser (smoser) wrote :

Also:
$ python -c 'from cloudinit import util; import sys; print util.read_file_or_url(sys.argv[1])' https://stackoverflow.com/
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "cloudinit/util.py", line 701, in read_file_or_url
    ssl_details=ssl_details)
  File "cloudinit/url_helper.py", line 173, in readurl
    if isinstance(e, (exceptions.HTTPError)) and e.response:
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).

Revision history for this message
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=E0102

  The 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

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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