~cloud-init-dev/cloud-init/trunk

« back to all changes in this revision

Viewing changes to cloudinit/url_helper.py

  • Committer: Scott Moser
  • Date: 2015-08-07 19:45:01 UTC
  • mfrom: (1130.1.32 trunk.reporting)
  • Revision ID: smoser@ubuntu.com-20150807194501-z582089k6n21rmmp
Add initial reporting module and events

Show diffs side-by-side

added added

removed removed

Lines of Context:
25
25
import six
26
26
 
27
27
import requests
 
28
import oauthlib.oauth1 as oauth1
 
29
import os
 
30
import json
 
31
from functools import partial
28
32
from requests import exceptions
29
33
 
30
34
from six.moves.urllib.parse import (
147
151
 
148
152
 
149
153
class UrlError(IOError):
150
 
    def __init__(self, cause, code=None, headers=None):
 
154
    def __init__(self, cause, code=None, headers=None, url=None):
151
155
        IOError.__init__(self, str(cause))
152
156
        self.cause = cause
153
157
        self.code = code
154
158
        self.headers = headers
155
159
        if self.headers is None:
156
160
            self.headers = {}
 
161
        self.url = url
157
162
 
158
163
 
159
164
def _get_ssl_args(url, ssl_details):
247
252
                    and hasattr(e, 'response')  # This appeared in v 0.10.8
248
253
                    and hasattr(e.response, 'status_code')):
249
254
                excps.append(UrlError(e, code=e.response.status_code,
250
 
                                      headers=e.response.headers))
 
255
                                      headers=e.response.headers,
 
256
                                      url=url))
251
257
            else:
252
 
                excps.append(UrlError(e))
 
258
                excps.append(UrlError(e, url=url))
253
259
                if SSL_ENABLED and isinstance(e, exceptions.SSLError):
254
260
                    # ssl exceptions are not going to get fixed by waiting a
255
261
                    # few seconds
333
339
                if not response.contents:
334
340
                    reason = "empty response [%s]" % (response.code)
335
341
                    url_exc = UrlError(ValueError(reason), code=response.code,
336
 
                                       headers=response.headers)
 
342
                                       headers=response.headers, url=url)
337
343
                elif not response.ok():
338
344
                    reason = "bad status code [%s]" % (response.code)
339
345
                    url_exc = UrlError(ValueError(reason), code=response.code,
340
 
                                       headers=response.headers)
 
346
                                       headers=response.headers, url=url)
341
347
                else:
342
348
                    return url
343
349
            except UrlError as e:
368
374
        time.sleep(sleep_time)
369
375
 
370
376
    return False
 
377
 
 
378
 
 
379
class OauthUrlHelper(object):
 
380
    def __init__(self, consumer_key=None, token_key=None,
 
381
                 token_secret=None, consumer_secret=None,
 
382
                 skew_data_file="/run/oauth_skew.json"):
 
383
        self.consumer_key = consumer_key
 
384
        self.consumer_secret = consumer_secret or ""
 
385
        self.token_key = token_key
 
386
        self.token_secret = token_secret
 
387
        self.skew_data_file = skew_data_file
 
388
        self._do_oauth = True
 
389
        self.skew_change_limit = 5
 
390
        required = (self.token_key, self.token_secret, self.consumer_key)
 
391
        if not any(required):
 
392
            self._do_oauth = False
 
393
        elif not all(required):
 
394
            raise ValueError("all or none of token_key, token_secret, or "
 
395
                             "consumer_key can be set")
 
396
 
 
397
        old = self.read_skew_file()
 
398
        self.skew_data = old or {}
 
399
 
 
400
    def read_skew_file(self):
 
401
        if self.skew_data_file and os.path.isfile(self.skew_data_file):
 
402
            with open(self.skew_data_file, mode="r") as fp:
 
403
                return json.load(fp.read())
 
404
        return None
 
405
 
 
406
    def update_skew_file(self, host, value):
 
407
        # this is not atomic
 
408
        if not self.skew_data_file:
 
409
            return
 
410
        cur = self.read_skew_file()
 
411
        cur[host] = value
 
412
        with open(self.skew_data_file, mode="w") as fp:
 
413
            fp.write(json.dumps(cur))
 
414
 
 
415
    def exception_cb(self, msg, exception):
 
416
        if not (isinstance(exception, UrlError) and
 
417
                (exception.code == 403 or exception.code == 401)):
 
418
            return
 
419
 
 
420
        if 'date' not in exception.headers:
 
421
            LOG.warn("Missing header 'date' in %s response", exception.code)
 
422
            return
 
423
 
 
424
        date = exception.headers['date']
 
425
        try:
 
426
            remote_time = time.mktime(parsedate(date))
 
427
        except Exception as e:
 
428
            LOG.warn("Failed to convert datetime '%s': %s", date, e)
 
429
            return
 
430
 
 
431
        skew = int(remote_time - time.time())
 
432
        host = urlparse(exception.url).netloc
 
433
        old_skew = self.skew_data.get(host, 0)
 
434
        if abs(old_skew - skew) > self.skew_change_limit:
 
435
            self.update_skew_file(host, skew)
 
436
            LOG.warn("Setting oauth clockskew for %s to %d", host, skew)
 
437
        skew_data[host] = skew
 
438
 
 
439
        return
 
440
 
 
441
    def headers_cb(self, url):
 
442
        if not self._do_oauth:
 
443
            return {}
 
444
 
 
445
        timestamp = None
 
446
        host = urlparse(url).netloc
 
447
        if self.skew_data and host in self.skew_data:
 
448
            timestamp = int(time.time()) + self.skew_data[host]
 
449
 
 
450
        return oauth_headers(
 
451
            url=url, consumer_key=self.consumer_key,
 
452
            token_key=self.token_key, token_secret=self.token_secret,
 
453
            consumer_secret=self.consumer_secret, timestamp=timestamp)
 
454
 
 
455
    def _wrapped(self, wrapped_func, args, kwargs):
 
456
        kwargs['headers_cb'] = partial(
 
457
            self._headers_cb, kwargs.get('headers_cb'))
 
458
        kwargs['exception_cb'] = partial(
 
459
            self._exception_cb, kwargs.get('exception_cb'))
 
460
        return wrapped_func(*args, **kwargs)
 
461
 
 
462
    def wait_for_url(self, *args, **kwargs):
 
463
        return self._wrapped(wait_for_url, args, kwargs)
 
464
 
 
465
    def readurl(self, *args, **kwargs):
 
466
        return self._wrapped(readurl, args, kwargs)
 
467
 
 
468
    def _exception_cb(self, extra_exception_cb, msg, exception):
 
469
        ret = None
 
470
        try:
 
471
            if extra_exception_cb:
 
472
                ret = extra_exception_cb(msg, exception)
 
473
        finally:
 
474
                self.exception_cb(msg, exception)
 
475
        return ret
 
476
 
 
477
    def _headers_cb(self, extra_headers_cb, url):
 
478
        headers = {}
 
479
        if extra_headers_cb:
 
480
            headers = extra_headers_cb(url)
 
481
        headers.update(self.headers_cb(url))
 
482
        return headers
 
483
 
 
484
 
 
485
def oauth_headers(url, consumer_key, token_key, token_secret, consumer_secret,
 
486
                  timestamp=None):
 
487
    if timestamp:
 
488
        timestamp = str(timestamp)
 
489
    else:
 
490
        timestamp = None
 
491
 
 
492
    client = oauth1.Client(
 
493
        consumer_key,
 
494
        client_secret=consumer_secret,
 
495
        resource_owner_key=token_key,
 
496
        resource_owner_secret=token_secret,
 
497
        signature_method=oauth1.SIGNATURE_PLAINTEXT,
 
498
        timestamp=timestamp)
 
499
    uri, signed_headers, body = client.sign(url)
 
500
    return signed_headers