blob: 5566793a2de37a8bd7c88166a08ccf2cbf5c31dd [file] [log] [blame]
Keun Soo Yimb293fdb2016-09-21 16:03:44 -07001#!/usr/bin/env python
2#
3# Copyright 2016 - The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17"""Base Cloud API Client.
18
19BasicCloudApiCliend does basic setup for a cloud API.
20"""
21import httplib
22import logging
23import os
24import socket
25import ssl
26
27import google3
28
29from apiclient import errors as gerrors
30from apiclient.discovery import build
31import apiclient.http
32import httplib2
33from oauth2client import client
34
35from acloud.internal.lib import utils
36from acloud.public import errors
37
38logger = logging.getLogger(__name__)
39
40
41class BaseCloudApiClient(object):
42 """A class that does basic setup for a cloud API."""
43
44 # To be overriden by subclasses.
45 API_NAME = ""
46 API_VERSION = "v1"
47 SCOPE = ""
48
49 # Defaults for retry.
50 RETRY_COUNT = 5
51 RETRY_BACKOFF_FACTOR = 1.5
52 RETRY_SLEEP_MULTIPLIER = 2
53 RETRY_HTTP_CODES = [
54 # 403 is to retry the "Rate Limit Exceeded" error.
55 # We could retry on a finer-grained error message later if necessary.
56 403,
57 500, # Internal Server Error
58 502, # Bad Gateway
59 503, # Service Unavailable
60 ]
61 RETRIABLE_ERRORS = (httplib.HTTPException, httplib2.HttpLib2Error,
62 socket.error, ssl.SSLError)
63 RETRIABLE_AUTH_ERRORS = (client.AccessTokenRefreshError, )
64
65 def __init__(self, oauth2_credentials):
66 """Initialize.
67
68 Args:
69 oauth2_credentials: An oauth2client.OAuth2Credentials instance.
70 """
71 self._service = self.InitResourceHandle(oauth2_credentials)
72
73 @classmethod
74 def InitResourceHandle(cls, oauth2_credentials):
75 """Authenticate and initialize a Resource object.
76
77 Authenticate http and create a Resource object with methods
78 for interacting with the service.
79
80 Args:
81 oauth2_credentials: An oauth2client.OAuth2Credentials instance.
82
83 Returns:
84 An apiclient.discovery.Resource object
85 """
86 http_auth = oauth2_credentials.authorize(httplib2.Http())
87 return utils.RetryException(exc_retry=cls.RETRIABLE_AUTH_ERRORS,
88 max_retry=cls.RETRY_COUNT,
89 functor=build,
90 sleep=cls.RETRY_SLEEP_MULTIPLIER,
91 backoff_factor=cls.RETRY_BACKOFF_FACTOR,
92 serviceName=cls.API_NAME,
93 version=cls.API_VERSION,
94 http=http_auth)
95
96 def _ShouldRetry(self, exception, retry_http_codes,
97 other_retriable_errors):
98 """Check if exception is retriable.
99
100 Args:
101 exception: An instance of Exception.
102 retry_http_codes: a list of integers, retriable HTTP codes of
103 HttpError
104 other_retriable_errors: a tuple of error types to retry other than
105 HttpError.
106
107 Returns:
108 Boolean, True if retriable, False otherwise.
109 """
110 if isinstance(exception, other_retriable_errors):
111 return True
112
113 if isinstance(exception, errors.HttpError):
114 if exception.code in retry_http_codes:
115 return True
116 else:
117 logger.debug("_ShouldRetry: Exception code %s not in %s: %s",
118 exception.code, retry_http_codes, str(exception))
119
120 logger.debug(
121 "_ShouldRetry: Exception %s is not one of %s: %s", type(exception),
122 list(other_retriable_errors) + [errors.HttpError], str(exception))
123 return False
124
125 def _TranslateError(self, exception):
126 """Translate the exception to a desired type.
127
128 Args:
129 exception: An instance of Exception.
130
131 Returns:
132 gerrors.HttpError will be translated to errors.HttpError.
133 If the error code is errors.HTTP_NOT_FOUND_CODE, it will
134 be translated to errors.ResourceNotFoundError.
135 Unrecognized error type will not be translated and will
136 be returned as is.
137 """
138 if isinstance(exception, gerrors.HttpError):
139 exception = errors.HttpError.CreateFromHttpError(exception)
140 if exception.code == errors.HTTP_NOT_FOUND_CODE:
141 exception = errors.ResourceNotFoundError(exception.code,
142 str(exception))
143 return exception
144
145 def ExecuteOnce(self, api):
146 """Execute an api and parse the errors.
147
148 Args:
149 api: An apiclient.http.HttpRequest, representing the api to execute.
150
151 Returns:
152 Execution result of the api.
153
154 Raises:
155 errors.ResourceNotFoundError: For 404 error.
156 errors.HttpError: For other types of http error.
157 """
158 try:
159 return api.execute()
160 except gerrors.HttpError as e:
161 raise self._TranslateError(e)
162
163 def Execute(self,
164 api,
165 retry_http_codes=None,
166 max_retry=None,
167 sleep=None,
168 backoff_factor=None,
169 other_retriable_errors=None):
170 """Execute an api with retry.
171
172 Call ExecuteOnce and retry on http error with given codes.
173
174 Args:
175 api: An apiclient.http.HttpRequest, representing the api to execute:
176 retry_http_codes: A list of http codes to retry.
177 max_retry: See utils.GenericRetry.
178 sleep: See utils.GenericRetry.
179 backoff_factor: See utils.GenericRetry.
180 other_retriable_errors: A tuple of error types that should be retried
181 other than errors.HttpError.
182
183 Returns:
184 Execution result of the api.
185
186 Raises:
187 See ExecuteOnce.
188 """
189 retry_http_codes = (self.RETRY_HTTP_CODES if retry_http_codes is None
190 else retry_http_codes)
191 max_retry = (self.RETRY_COUNT if max_retry is None else max_retry)
192 sleep = (self.RETRY_SLEEP_MULTIPLIER if sleep is None else sleep)
193 backoff_factor = (self.RETRY_BACKOFF_FACTOR if backoff_factor is None
194 else backoff_factor)
195 other_retriable_errors = (self.RETRIABLE_ERRORS
196 if other_retriable_errors is None else
197 other_retriable_errors)
198
199 def _Handler(exc):
200 """Check if |exc| is a retriable exception.
201
202 Args:
203 exc: An exception.
204
205 Returns:
206 True if exc is an errors.HttpError and code exists in |retry_http_codes|
207 False otherwise.
208 """
209 if self._ShouldRetry(exc, retry_http_codes,
210 other_retriable_errors):
211 logger.debug("Will retry error: %s", str(exc))
212 return True
213 return False
214
215 return utils.GenericRetry(_Handler,
216 max_retry=max_retry,
217 functor=self.ExecuteOnce,
218 sleep=sleep,
219 backoff_factor=backoff_factor,
220 api=api)
221
222 def BatchExecuteOnce(self, requests):
223 """Execute requests in a batch.
224
225 Args:
226 requests: A dictionary where key is request id and value
227 is an http request.
228
229 Returns:
230 results, a dictionary in the following format
231 {request_id: (response, exception)}
232 request_ids are those from requests; response
233 is the http response for the request or None on error;
234 exception is an instance of DriverError or None if no error.
235 """
236 results = {}
237
238 def _CallBack(request_id, response, exception):
239 results[request_id] = (response, self._TranslateError(exception))
240
241 batch = apiclient.http.BatchHttpRequest()
242 for request_id, request in requests.iteritems():
243 batch.add(request=request,
244 callback=_CallBack,
245 request_id=request_id)
246 batch.execute()
247 return results
248
249 def BatchExecute(self,
250 requests,
251 retry_http_codes=None,
252 max_retry=None,
253 sleep=None,
254 backoff_factor=None,
255 other_retriable_errors=None):
256 """Batch execute multiple requests with retry.
257
258 Call BatchExecuteOnce and retry on http error with given codes.
259
260 Args:
261 requests: A dictionary where key is request id picked by caller,
262 and value is a apiclient.http.HttpRequest.
263 retry_http_codes: A list of http codes to retry.
264 max_retry: See utils.GenericRetry.
265 sleep: See utils.GenericRetry.
266 backoff_factor: See utils.GenericRetry.
267 other_retriable_errors: A tuple of error types that should be retried
268 other than errors.HttpError.
269
270 Returns:
271 results, a dictionary in the following format
272 {request_id: (response, exception)}
273 request_ids are those from requests; response
274 is the http response for the request or None on error;
275 exception is an instance of DriverError or None if no error.
276 """
277 executor = utils.BatchHttpRequestExecutor(
278 self.BatchExecuteOnce,
279 requests=requests,
280 retry_http_codes=retry_http_codes or self.RETRY_HTTP_CODES,
281 max_retry=max_retry or self.RETRY_COUNT,
282 sleep=sleep or self.RETRY_SLEEP_MULTIPLIER,
283 backoff_factor=backoff_factor or self.RETRY_BACKOFF_FACTOR,
284 other_retriable_errors=other_retriable_errors or
285 self.RETRIABLE_ERRORS)
286 executor.Execute()
287 return executor.GetResults()
288
289 def ListWithMultiPages(self, api_resource, *args, **kwargs):
290 """Call an api that list a type of resource.
291
292 Multiple google services support listing a type of
293 resource (e.g list gce instances, list storage objects).
294 The querying pattern is similar --
295 Step 1: execute the api and get a response object like,
296 {
297 "items": [..list of resource..],
298 # The continuation token that can be used
299 # to get the next page.
300 "nextPageToken": "A String",
301 }
302 Step 2: execute the api again with the nextPageToken to
303 retrieve more pages and get a response object.
304
305 Step 3: Repeat Step 2 until no more page.
306
307 This method encapsulates the generic logic of
308 calling such listing api.
309
310 Args:
311 api_resource: An apiclient.discovery.Resource object
312 used to create an http request for the listing api.
313 *args: Arguments used to create the http request.
314 **kwargs: Keyword based arguments to create the http
315 request.
316
317 Returns:
318 A list of items.
319 """
320 items = []
321 next_page_token = None
322 while True:
323 api = api_resource(pageToken=next_page_token, *args, **kwargs)
324 response = self.Execute(api)
325 items.extend(response.get("items", []))
326 next_page_token = response.get("nextPageToken")
327 if not next_page_token:
328 break
329 return items
330
331 @property
332 def service(self):
333 """Return self._service as a property."""
334 return self._service