Chromium Code Reviews| 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): | |
|
Sergiy Byelozyorov
2015/09/22 11:49:49
I recall writing this code. Can you please referen
dnj
2015/09/25 17:40:55
This is standalone, so I don't think referencing t
| |
| 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() | |
|
Sergiy Byelozyorov
2015/09/25 15:36:20
Please add 5 retries inside out outside this call.
dnj
2015/09/25 17:40:55
I'm fairly reluctant to do 5 retries in all circum
Sergiy Byelozyorov
2015/09/25 20:06:34
If you think 5 is too much, feel free to reduce it
| |
| 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 p = urlparse.urlparse(url) | |
| 156 c = GetConnectionClass(protocol=p.scheme)(p.netloc) | |
| 157 c.request('GET', url, **kwargs) | |
| 158 return c.getresponse() | |
| 159 | |
| 160 @classmethod | |
| 161 def _get_token_dict(cls): | |
| 162 if cls._token_cache: | |
| 163 # If it expires within 25 seconds, refresh. | |
| 164 if cls._token_expiration > time.time() - 25: | |
| 165 return cls._token_cache | |
|
Sergiy Byelozyorov
2015/09/25 15:36:20
You actually return the token only if it expires w
dnj
2015/09/25 17:40:55
lol good catch
| |
| 166 | |
| 167 resp = cls._get(cls._ACQUIRE_URL, headers=cls._ACQUIRE_HEADERS) | |
|
Sergiy Byelozyorov
2015/09/25 15:36:20
Please add 5 retries around this. Metadata endpoin
dnj
2015/09/25 17:40:55
Done.
| |
| 168 if resp.status != httplib.OK: | |
| 169 return None | |
| 170 cls._token_cache = token_dict = json.load(resp) | |
|
Sergiy Byelozyorov
2015/09/25 15:36:20
why not just assign to cls._token_cache and use th
dnj
2015/09/25 17:40:55
It avoids one more dictionary getattr hit *shrug*.
| |
| 171 cls._token_expiration = token_dict['expires_in'] + time.time() | |
| 172 return cls._token_cache | |
| 173 | |
| 174 def get_auth_header(self, _host): | |
| 175 token_dict = self._get_token_dict() | |
| 176 if not token_dict: | |
| 177 return None | |
| 178 return '%(token_type)s %(access_token)s' % token_dict | |
| 179 | |
| 180 | |
| 181 | |
| 87 def CreateHttpConn(host, path, reqtype='GET', headers=None, body=None): | 182 def CreateHttpConn(host, path, reqtype='GET', headers=None, body=None): |
| 88 """Opens an https connection to a gerrit service, and sends a request.""" | 183 """Opens an https connection to a gerrit service, and sends a request.""" |
| 89 headers = headers or {} | 184 headers = headers or {} |
| 90 bare_host = host.partition(':')[0] | 185 bare_host = host.partition(':')[0] |
| 91 auth = NETRC.authenticators(bare_host) | |
| 92 | 186 |
| 187 auth = Authenticator.get().get_auth_header(bare_host) | |
| 93 if auth: | 188 if auth: |
| 94 headers.setdefault('Authorization', 'Basic %s' % ( | 189 headers.setdefault('Authorization', auth) |
| 95 base64.b64encode('%s:%s' % (auth[0], auth[2])))) | |
| 96 else: | 190 else: |
| 97 LOGGER.debug('No authorization found in netrc for %s.' % bare_host) | 191 LOGGER.debug('No authorization found for %s.' % bare_host) |
| 98 | 192 |
| 99 if 'Authorization' in headers and not path.startswith('a/'): | 193 if 'Authorization' in headers and not path.startswith('a/'): |
| 100 url = '/a/%s' % path | 194 url = '/a/%s' % path |
| 101 else: | 195 else: |
| 102 url = '/%s' % path | 196 url = '/%s' % path |
| 103 | 197 |
| 104 if body: | 198 if body: |
| 105 body = json.JSONEncoder().encode(body) | 199 body = json.JSONEncoder().encode(body) |
| 106 headers.setdefault('Content-Type', 'application/json') | 200 headers.setdefault('Content-Type', 'application/json') |
| 107 if LOGGER.isEnabledFor(logging.DEBUG): | 201 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', '')) | 559 username = review.get('email', jmsg.get('name', '')) |
| 466 raise GerritError(200, 'Unable to set %s label for user "%s"' | 560 raise GerritError(200, 'Unable to set %s label for user "%s"' |
| 467 ' on change %s.' % (label, username, change)) | 561 ' on change %s.' % (label, username, change)) |
| 468 jmsg = GetChangeCurrentRevision(host, change) | 562 jmsg = GetChangeCurrentRevision(host, change) |
| 469 if not jmsg: | 563 if not jmsg: |
| 470 raise GerritError( | 564 raise GerritError( |
| 471 200, 'Could not get review information for change "%s"' % change) | 565 200, 'Could not get review information for change "%s"' % change) |
| 472 elif jmsg[0]['current_revision'] != revision: | 566 elif jmsg[0]['current_revision'] != revision: |
| 473 raise GerritError(200, 'While resetting labels on change "%s", ' | 567 raise GerritError(200, 'While resetting labels on change "%s", ' |
| 474 'a new patchset was uploaded.' % change) | 568 'a new patchset was uploaded.' % change) |
| OLD | NEW |