Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(7)

Side by Side Diff: infra_libs/httplib2_utils.py

Issue 2213143002: Add infra_libs as a bootstrap dependency. (Closed) Base URL: https://chromium.googlesource.com/infra/infra.git@master
Patch Set: Removed the ugly import hack Created 4 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 # Copyright 2015 The Chromium Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file.
4
5 import collections
6 import copy
7 import json
8 import logging
9 import os
10 import re
11 import socket
12 import sys
13 import time
14
15 import httplib2
16 import oauth2client.client
17
18 from googleapiclient import errors
19 from infra_libs.ts_mon.common import http_metrics
20
21 DEFAULT_SCOPES = ['email']
22
23 # default timeout for http requests, in seconds
24 DEFAULT_TIMEOUT = 30
25
26 # This is part of the API.
27 if sys.platform.startswith('win'): # pragma: no cover
28 SERVICE_ACCOUNTS_CREDS_ROOT = 'C:\\creds\\service_accounts'
29 else:
30 SERVICE_ACCOUNTS_CREDS_ROOT = '/creds/service_accounts'
31
32
33 class AuthError(Exception):
34 pass
35
36
37 def load_service_account_credentials(credentials_filename,
38 service_accounts_creds_root=None):
39 """Loads and validate a credential JSON file.
40
41 Example of a well-formatted file:
42 {
43 "private_key_id": "4168d274cdc7a1eaef1c59f5b34bdf255",
44 "private_key": ("-----BEGIN PRIVATE KEY-----\nMIIhkiG9w0BAQEFAASCAmEwsd"
45 "sdfsfFd\ngfxFChctlOdTNm2Wrr919Nx9q+sPV5ibyaQt5Dgn89fKV"
46 "jftrO3AMDS3sMjaE4Ib\nZwJgy90wwBbMT7/YOzCgf5PZfivUe8KkB"
47 -----END PRIVATE KEY-----\n",
48 "client_email": "234243-rjstu8hi95iglc8at3@developer.gserviceaccount.com",
49 "client_id": "234243-rjstu8hi95iglc8at3.apps.googleusercontent.com",
50 "type": "service_account"
51 }
52
53 Args:
54 credentials_filename (str): path to a .json file containing credentials
55 for a Cloud platform service account.
56
57 Keyword Args:
58 service_accounts_creds_root (str or None): location where all service
59 account credentials are stored. ``credentials_filename`` is relative
60 to this path. None means 'use default location'.
61
62 Raises:
63 AuthError: if the file content is invalid.
64 """
65 service_accounts_creds_root = (service_accounts_creds_root
66 or SERVICE_ACCOUNTS_CREDS_ROOT)
67
68 service_account_file = os.path.join(service_accounts_creds_root,
69 credentials_filename)
70 try:
71 with open(service_account_file, 'r') as f:
72 key = json.load(f)
73 except ValueError as e:
74 raise AuthError('Parsing of file as JSON failed (%s): %s',
75 e, service_account_file)
76
77 if key.get('type') != 'service_account':
78 msg = ('Credentials type must be for a service_account, got %s.'
79 ' Check content of %s' % (key.get('type'), service_account_file))
80 logging.error(msg)
81 raise AuthError(msg)
82
83 if not key.get('client_email'):
84 msg = ('client_email field missing in credentials json file. '
85 ' Check content of %s' % service_account_file)
86 logging.error(msg)
87 raise AuthError(msg)
88
89 if not key.get('private_key'):
90 msg = ('private_key field missing in credentials json. '
91 ' Check content of %s' % service_account_file)
92 logging.error(msg)
93 raise AuthError(msg)
94
95 return key
96
97
98 def get_signed_jwt_assertion_credentials(credentials_filename,
99 scope=None,
100 service_accounts_creds_root=None):
101 """Factory for SignedJwtAssertionCredentials
102
103 Reads and validate the json credential file.
104
105 Args:
106 credentials_filename (str): path to the service account key file.
107 See load_service_account_credentials() docstring for the file format.
108
109 Keyword Args:
110 scope (str|list of str): scope(s) of the credentials being
111 requested. Defaults to https://www.googleapis.com/auth/userinfo.email.
112 service_accounts_creds_root (str or None): location where all service
113 account credentials are stored. ``credentials_filename`` is relative
114 to this path. None means 'use default location'.
115 """
116 scope = scope or DEFAULT_SCOPES
117 if isinstance(scope, basestring):
118 scope = [scope]
119 assert all(isinstance(s, basestring) for s in scope)
120
121 key = load_service_account_credentials(
122 credentials_filename,
123 service_accounts_creds_root=service_accounts_creds_root)
124
125 return oauth2client.client.SignedJwtAssertionCredentials(
126 key['client_email'], key['private_key'], scope)
127
128
129 def get_authenticated_http(credentials_filename,
130 scope=None,
131 service_accounts_creds_root=None,
132 http_identifier=None,
133 timeout=DEFAULT_TIMEOUT):
134 """Creates an httplib2.Http wrapped with a service account authenticator.
135
136 Args:
137 credentials_filename (str): relative path to the file containing
138 credentials in json format. Path is relative to the default
139 location where credentials are stored (platform-dependent).
140
141 Keyword Args:
142 scope (str|list of str): scope(s) of the credentials being
143 requested. Defaults to https://www.googleapis.com/auth/userinfo.email.
144 service_accounts_creds_root (str or None): location where all service
145 account credentials are stored. ``credentials_filename`` is relative
146 to this path. None means 'use default location'.
147 http_identifier (str): if provided, returns an instrumented http request
148 and use this string to identify it to ts_mon.
149 timeout (int): timeout passed to httplib2.Http, in seconds.
150
151 Returns:
152 httplib2.Http authenticated with master's service account.
153 """
154 creds = get_signed_jwt_assertion_credentials(
155 credentials_filename,
156 scope=scope,
157 service_accounts_creds_root=service_accounts_creds_root)
158
159 if http_identifier:
160 http = InstrumentedHttp(http_identifier, timeout=timeout)
161 else:
162 http = httplib2.Http(timeout=timeout)
163 return creds.authorize(http)
164
165 class RetriableHttp(object):
166 """A httplib2.Http object that retries on failure."""
167
168 def __init__(self, http, max_tries=5, backoff_time=1,
169 retrying_statuses_fn=None):
170 """
171 Args:
172 http: an httplib2.Http instance
173 max_tries: a number of maximum tries
174 backoff_time: a number of seconds to sleep between retries
175 retrying_statuses_fn: a function that returns True if a given status
176 should be retried
177 """
178 self._http = http
179 self._max_tries = max_tries
180 self._backoff_time = backoff_time
181 self._retrying_statuses_fn = retrying_statuses_fn or \
182 set(range(500,599)).__contains__
183
184 def request(self, uri, method='GET', body=None, *args, **kwargs):
185 for i in range(1, self._max_tries + 1):
186 try:
187 response, content = self._http.request(uri, method, body, *args,
188 **kwargs)
189
190 if self._retrying_statuses_fn(response.status):
191 logging.info('RetriableHttp: attempt %d receiving status %d, %s',
192 i, response.status,
193 'final attempt' if i == self._max_tries else \
194 'will retry')
195 else:
196 break
197 except (ValueError, errors.Error,
198 socket.timeout, socket.error, socket.herror, socket.gaierror,
199 httplib2.HttpLib2Error) as error:
200 logging.info('RetriableHttp: attempt %d received exception: %s, %s',
201 i, error, 'final attempt' if i == self._max_tries else \
202 'will retry')
203 if i == self._max_tries:
204 raise
205 time.sleep(self._backoff_time)
206
207 return response, content
208
209 def __getattr__(self, name):
210 return getattr(self._http, name)
211
212 def __setattr__(self, name, value):
213 if name in ('request', '_http', '_max_tries', '_backoff_time',
214 '_retrying_statuses_fn'):
215 self.__dict__[name] = value
216 else:
217 setattr(self._http, name, value)
218
219 class InstrumentedHttp(httplib2.Http):
220 """A httplib2.Http object that reports ts_mon metrics about its requests."""
221
222 def __init__(self, name, time_fn=time.time, timeout=DEFAULT_TIMEOUT,
223 **kwargs):
224 """
225 Args:
226 name: An identifier for the HTTP requests made by this object.
227 time_fn: Function returning the current time in seconds. Use for testing
228 purposes only.
229 """
230
231 super(InstrumentedHttp, self).__init__(timeout=timeout, **kwargs)
232 self.fields = {'name': name, 'client': 'httplib2'}
233 self.time_fn = time_fn
234
235 def _update_metrics(self, status, start_time):
236 status_fields = {'status': status}
237 status_fields.update(self.fields)
238 http_metrics.response_status.increment(fields=status_fields)
239
240 duration_msec = (self.time_fn() - start_time) * 1000
241 http_metrics.durations.add(duration_msec, fields=self.fields)
242
243 def request(self, uri, method="GET", body=None, *args, **kwargs):
244 request_bytes = 0
245 if body is not None:
246 request_bytes = len(body)
247 http_metrics.request_bytes.add(request_bytes, fields=self.fields)
248
249 start_time = self.time_fn()
250 try:
251 response, content = super(InstrumentedHttp, self).request(
252 uri, method, body, *args, **kwargs)
253 except socket.timeout:
254 self._update_metrics(http_metrics.STATUS_TIMEOUT, start_time)
255 raise
256 except (socket.error, socket.herror, socket.gaierror):
257 self._update_metrics(http_metrics.STATUS_ERROR, start_time)
258 raise
259 except httplib2.HttpLib2Error:
260 self._update_metrics(http_metrics.STATUS_EXCEPTION, start_time)
261 raise
262 http_metrics.response_bytes.add(len(content), fields=self.fields)
263
264 self._update_metrics(response.status, start_time)
265
266 return response, content
267
268
269 class HttpMock(object):
270 """Mock of httplib2.Http"""
271 HttpCall = collections.namedtuple('HttpCall', ('uri', 'method', 'body',
272 'headers'))
273
274 def __init__(self, uris):
275 """
276 Args:
277 uris(dict): list of (uri, headers, body). `uri` is a regexp for
278 matching the requested uri, (headers, body) gives the values returned
279 by the mock. Uris are tested in the order from `uris`.
280 `headers` is a dict mapping headers to value. The 'status' key is
281 mandatory. `body` is a string.
282 Ex: [('.*', {'status': 200}, 'nicely done.')]
283 """
284 self._uris = []
285 self.requests_made = []
286
287 for value in uris:
288 if not isinstance(value, (list, tuple)) or len(value) != 3:
289 raise ValueError("'uris' must be a sequence of (uri, headers, body)")
290 uri, headers, body = value
291 compiled_uri = re.compile(uri)
292 if not isinstance(headers, dict):
293 raise TypeError("'headers' must be a dict")
294 if not 'status' in headers:
295 raise ValueError("'headers' must have 'status' as a key")
296
297 new_headers = copy.copy(headers)
298 new_headers['status'] = int(new_headers['status'])
299
300 if not isinstance(body, basestring):
301 raise TypeError("'body' must be a string, got %s" % type(body))
302 self._uris.append((compiled_uri, new_headers, body))
303
304 # pylint: disable=unused-argument
305 def request(self, uri,
306 method='GET',
307 body=None,
308 headers=None,
309 redirections=1,
310 connection_type=None):
311 self.requests_made.append(self.HttpCall(uri, method, body, headers))
312 headers = None
313 body = None
314 for candidate in self._uris:
315 if candidate[0].match(uri):
316 _, headers, body = candidate
317 break
318 if not headers:
319 raise AssertionError("Unexpected request to %s" % uri)
320 return httplib2.Response(headers), body
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698