OLD | NEW |
1 # Copyright (c) 2013 The Chromium OS Authors. All rights reserved. | 1 # Copyright (c) 2013 The Chromium OS Authors. All rights reserved. |
2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
4 | 4 |
5 """ | 5 """ |
6 Utilities for requesting information for a gerrit server via https. | 6 Utilities for requesting information for a gerrit server via https. |
7 | 7 |
8 https://gerrit-review.googlesource.com/Documentation/rest-api.html | 8 https://gerrit-review.googlesource.com/Documentation/rest-api.html |
9 """ | 9 """ |
10 | 10 |
11 import base64 | 11 import base64 |
12 import httplib | 12 import httplib |
13 import json | 13 import json |
14 import logging | 14 import logging |
15 import netrc | 15 import netrc |
16 import os | 16 import os |
17 import re | 17 import re |
| 18 import socket |
18 import stat | 19 import stat |
19 import sys | 20 import sys |
20 import time | 21 import time |
21 import urllib | 22 import urllib |
| 23 import urlparse |
22 from cStringIO import StringIO | 24 from cStringIO import StringIO |
23 | 25 |
24 _netrc_file = '_netrc' if sys.platform.startswith('win') else '.netrc' | |
25 _netrc_file = os.path.join(os.environ['HOME'], _netrc_file) | |
26 try: | |
27 NETRC = netrc.netrc(_netrc_file) | |
28 except IOError: | |
29 print >> sys.stderr, 'WARNING: Could not read netrc file %s' % _netrc_file | |
30 NETRC = netrc.netrc(os.devnull) | |
31 except netrc.NetrcParseError as e: | |
32 _netrc_stat = os.stat(e.filename) | |
33 if _netrc_stat.st_mode & (stat.S_IRWXG | stat.S_IRWXO): | |
34 print >> sys.stderr, ( | |
35 'WARNING: netrc file %s cannot be used because its file permissions ' | |
36 'are insecure. netrc file permissions should be 600.' % _netrc_file) | |
37 else: | |
38 print >> sys.stderr, ('ERROR: Cannot use netrc file %s due to a parsing ' | |
39 'error.' % _netrc_file) | |
40 raise | |
41 del _netrc_stat | |
42 NETRC = netrc.netrc(os.devnull) | |
43 del _netrc_file | |
44 | 26 |
45 LOGGER = logging.getLogger() | 27 LOGGER = logging.getLogger() |
46 TRY_LIMIT = 5 | 28 TRY_LIMIT = 5 |
47 | 29 |
| 30 |
48 # Controls the transport protocol used to communicate with gerrit. | 31 # Controls the transport protocol used to communicate with gerrit. |
49 # This is parameterized primarily to enable GerritTestCase. | 32 # This is parameterized primarily to enable GerritTestCase. |
50 GERRIT_PROTOCOL = 'https' | 33 GERRIT_PROTOCOL = 'https' |
51 | 34 |
52 | 35 |
53 class GerritError(Exception): | 36 class GerritError(Exception): |
54 """Exception class for errors commuicating with the gerrit-on-borg service.""" | 37 """Exception class for errors commuicating with the gerrit-on-borg service.""" |
55 def __init__(self, http_status, *args, **kwargs): | 38 def __init__(self, http_status, *args, **kwargs): |
56 super(GerritError, self).__init__(*args, **kwargs) | 39 super(GerritError, self).__init__(*args, **kwargs) |
57 self.http_status = http_status | 40 self.http_status = http_status |
(...skipping 19 matching lines...) Expand all Loading... |
77 protocol = GERRIT_PROTOCOL | 60 protocol = GERRIT_PROTOCOL |
78 if protocol == 'https': | 61 if protocol == 'https': |
79 return httplib.HTTPSConnection | 62 return httplib.HTTPSConnection |
80 elif protocol == 'http': | 63 elif protocol == 'http': |
81 return httplib.HTTPConnection | 64 return httplib.HTTPConnection |
82 else: | 65 else: |
83 raise RuntimeError( | 66 raise RuntimeError( |
84 "Don't know how to work with protocol '%s'" % protocol) | 67 "Don't know how to work with protocol '%s'" % protocol) |
85 | 68 |
86 | 69 |
| 70 class Authenticator(object): |
| 71 """Base authenticator class for authenticator implementations to subclass.""" |
| 72 |
| 73 def get_auth_header(self, host): |
| 74 raise NotImplementedError() |
| 75 |
| 76 @staticmethod |
| 77 def get(): |
| 78 """Returns: (Authenticator) The identified Authenticator to use. |
| 79 |
| 80 Probes the local system and its environment and identifies the |
| 81 Authenticator instance to use. |
| 82 """ |
| 83 if GceAuthenticator.is_gce(): |
| 84 return GceAuthenticator() |
| 85 return NetrcAuthenticator() |
| 86 |
| 87 |
| 88 class NetrcAuthenticator(Authenticator): |
| 89 """Authenticator implementation that uses ".netrc" for token. |
| 90 """ |
| 91 |
| 92 def __init__(self): |
| 93 self.netrc = self._get_netrc() |
| 94 |
| 95 @staticmethod |
| 96 def _get_netrc(): |
| 97 path = '_netrc' if sys.platform.startswith('win') else '.netrc' |
| 98 path = os.path.join(os.environ['HOME'], path) |
| 99 try: |
| 100 return netrc.netrc(path) |
| 101 except IOError: |
| 102 print >> sys.stderr, 'WARNING: Could not read netrc file %s' % path |
| 103 return netrc.netrc(os.devnull) |
| 104 except netrc.NetrcParseError as e: |
| 105 st = os.stat(e.path) |
| 106 if st.st_mode & (stat.S_IRWXG | stat.S_IRWXO): |
| 107 print >> sys.stderr, ( |
| 108 'WARNING: netrc file %s cannot be used because its file ' |
| 109 'permissions are insecure. netrc file permissions should be ' |
| 110 '600.' % path) |
| 111 else: |
| 112 print >> sys.stderr, ('ERROR: Cannot use netrc file %s due to a ' |
| 113 'parsing error.' % path) |
| 114 raise |
| 115 return netrc.netrc(os.devnull) |
| 116 |
| 117 def get_auth_header(self, host): |
| 118 auth = self.netrc.authenticators(host) |
| 119 if auth: |
| 120 return 'Basic %s' % (base64.b64encode('%s:%s' % (auth[0], auth[2]))) |
| 121 return None |
| 122 |
| 123 |
| 124 class GceAuthenticator(Authenticator): |
| 125 """Authenticator implementation that uses GCE metadata service for token. |
| 126 """ |
| 127 |
| 128 _INFO_URL = 'http://metadata.google.internal' |
| 129 _ACQUIRE_URL = ('http://metadata/computeMetadata/v1/instance/' |
| 130 'service-accounts/default/token') |
| 131 _ACQUIRE_HEADERS = {"Metadata-Flavor": "Google"} |
| 132 |
| 133 _cache_is_gce = None |
| 134 _token_cache = None |
| 135 _token_expiration = None |
| 136 |
| 137 @classmethod |
| 138 def is_gce(cls): |
| 139 if cls._cache_is_gce is None: |
| 140 cls._cache_is_gce = cls._test_is_gce() |
| 141 return cls._cache_is_gce |
| 142 |
| 143 @classmethod |
| 144 def _test_is_gce(cls): |
| 145 # Based on https://cloud.google.com/compute/docs/metadata#runninggce |
| 146 try: |
| 147 resp = cls._get(cls._INFO_URL) |
| 148 except socket.error: |
| 149 # Could not resolve URL. |
| 150 return False |
| 151 return resp.getheader('Metadata-Flavor', None) == 'Google' |
| 152 |
| 153 @staticmethod |
| 154 def _get(url, **kwargs): |
| 155 next_delay_sec = 1 |
| 156 for i in xrange(TRY_LIMIT): |
| 157 if i > 0: |
| 158 # Retry server error status codes. |
| 159 LOGGER.info('Encountered server error; retrying after %d second(s).', |
| 160 next_delay_sec) |
| 161 time.sleep(next_delay_sec) |
| 162 next_delay_sec *= 2 |
| 163 |
| 164 p = urlparse.urlparse(url) |
| 165 c = GetConnectionClass(protocol=p.scheme)(p.netloc) |
| 166 c.request('GET', url, **kwargs) |
| 167 resp = c.getresponse() |
| 168 LOGGER.debug('GET [%s] #%d/%d (%d)', url, i+1, TRY_LIMIT, resp.status) |
| 169 if resp.status < httplib.INTERNAL_SERVER_ERROR: |
| 170 return resp |
| 171 |
| 172 |
| 173 @classmethod |
| 174 def _get_token_dict(cls): |
| 175 if cls._token_cache: |
| 176 # If it expires within 25 seconds, refresh. |
| 177 if cls._token_expiration < time.time() - 25: |
| 178 return cls._token_cache |
| 179 |
| 180 resp = cls._get(cls._ACQUIRE_URL, headers=cls._ACQUIRE_HEADERS) |
| 181 if resp.status != httplib.OK: |
| 182 return None |
| 183 cls._token_cache = json.load(resp) |
| 184 cls._token_expiration = cls._token_cache['expires_in'] + time.time() |
| 185 return cls._token_cache |
| 186 |
| 187 def get_auth_header(self, _host): |
| 188 token_dict = self._get_token_dict() |
| 189 if not token_dict: |
| 190 return None |
| 191 return '%(token_type)s %(access_token)s' % token_dict |
| 192 |
| 193 |
| 194 |
87 def CreateHttpConn(host, path, reqtype='GET', headers=None, body=None): | 195 def CreateHttpConn(host, path, reqtype='GET', headers=None, body=None): |
88 """Opens an https connection to a gerrit service, and sends a request.""" | 196 """Opens an https connection to a gerrit service, and sends a request.""" |
89 headers = headers or {} | 197 headers = headers or {} |
90 bare_host = host.partition(':')[0] | 198 bare_host = host.partition(':')[0] |
91 auth = NETRC.authenticators(bare_host) | |
92 | 199 |
| 200 auth = Authenticator.get().get_auth_header(bare_host) |
93 if auth: | 201 if auth: |
94 headers.setdefault('Authorization', 'Basic %s' % ( | 202 headers.setdefault('Authorization', auth) |
95 base64.b64encode('%s:%s' % (auth[0], auth[2])))) | |
96 else: | 203 else: |
97 LOGGER.debug('No authorization found in netrc for %s.' % bare_host) | 204 LOGGER.debug('No authorization found for %s.' % bare_host) |
98 | 205 |
99 if 'Authorization' in headers and not path.startswith('a/'): | 206 if 'Authorization' in headers and not path.startswith('a/'): |
100 url = '/a/%s' % path | 207 url = '/a/%s' % path |
101 else: | 208 else: |
102 url = '/%s' % path | 209 url = '/%s' % path |
103 | 210 |
104 if body: | 211 if body: |
105 body = json.JSONEncoder().encode(body) | 212 body = json.JSONEncoder().encode(body) |
106 headers.setdefault('Content-Type', 'application/json') | 213 headers.setdefault('Content-Type', 'application/json') |
107 if LOGGER.isEnabledFor(logging.DEBUG): | 214 if LOGGER.isEnabledFor(logging.DEBUG): |
(...skipping 357 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
465 username = review.get('email', jmsg.get('name', '')) | 572 username = review.get('email', jmsg.get('name', '')) |
466 raise GerritError(200, 'Unable to set %s label for user "%s"' | 573 raise GerritError(200, 'Unable to set %s label for user "%s"' |
467 ' on change %s.' % (label, username, change)) | 574 ' on change %s.' % (label, username, change)) |
468 jmsg = GetChangeCurrentRevision(host, change) | 575 jmsg = GetChangeCurrentRevision(host, change) |
469 if not jmsg: | 576 if not jmsg: |
470 raise GerritError( | 577 raise GerritError( |
471 200, 'Could not get review information for change "%s"' % change) | 578 200, 'Could not get review information for change "%s"' % change) |
472 elif jmsg[0]['current_revision'] != revision: | 579 elif jmsg[0]['current_revision'] != revision: |
473 raise GerritError(200, 'While resetting labels on change "%s", ' | 580 raise GerritError(200, 'While resetting labels on change "%s", ' |
474 'a new patchset was uploaded.' % change) | 581 'a new patchset was uploaded.' % change) |
OLD | NEW |