blob: 9f851dab6d1bf12d851b02782444dca217f6b710 [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.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070016"""Base Cloud API Client.
17
18BasicCloudApiCliend does basic setup for a cloud API.
19"""
20import httplib
21import logging
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070022import socket
23import ssl
24
Kevin Cheng3031f8a2018-05-16 13:21:51 -070025# pylint: disable=import-error
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070026from apiclient import errors as gerrors
27from apiclient.discovery import build
28import apiclient.http
29import httplib2
30from oauth2client import client
31
Sam Chiu7de3b232018-12-06 19:45:52 +080032from acloud import errors
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070033from acloud.internal.lib import utils
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070034
35logger = logging.getLogger(__name__)
36
37
38class BaseCloudApiClient(object):
39 """A class that does basic setup for a cloud API."""
40
41 # To be overriden by subclasses.
42 API_NAME = ""
43 API_VERSION = "v1"
44 SCOPE = ""
45
46 # Defaults for retry.
47 RETRY_COUNT = 5
48 RETRY_BACKOFF_FACTOR = 1.5
49 RETRY_SLEEP_MULTIPLIER = 2
50 RETRY_HTTP_CODES = [
51 # 403 is to retry the "Rate Limit Exceeded" error.
52 # We could retry on a finer-grained error message later if necessary.
53 403,
54 500, # Internal Server Error
55 502, # Bad Gateway
56 503, # Service Unavailable
57 ]
58 RETRIABLE_ERRORS = (httplib.HTTPException, httplib2.HttpLib2Error,
59 socket.error, ssl.SSLError)
60 RETRIABLE_AUTH_ERRORS = (client.AccessTokenRefreshError, )
61
62 def __init__(self, oauth2_credentials):
63 """Initialize.
64
65 Args:
66 oauth2_credentials: An oauth2client.OAuth2Credentials instance.
67 """
68 self._service = self.InitResourceHandle(oauth2_credentials)
69
70 @classmethod
71 def InitResourceHandle(cls, oauth2_credentials):
72 """Authenticate and initialize a Resource object.
73
74 Authenticate http and create a Resource object with methods
75 for interacting with the service.
76
77 Args:
78 oauth2_credentials: An oauth2client.OAuth2Credentials instance.
79
80 Returns:
81 An apiclient.discovery.Resource object
82 """
83 http_auth = oauth2_credentials.authorize(httplib2.Http())
Fang Dengf24be082018-02-10 10:09:55 -080084 return utils.RetryExceptionType(
Kevin Cheng3031f8a2018-05-16 13:21:51 -070085 exception_types=cls.RETRIABLE_AUTH_ERRORS,
86 max_retries=cls.RETRY_COUNT,
87 functor=build,
88 sleep_multiplier=cls.RETRY_SLEEP_MULTIPLIER,
89 retry_backoff_factor=cls.RETRY_BACKOFF_FACTOR,
90 serviceName=cls.API_NAME,
91 version=cls.API_VERSION,
xingdai8a00d462018-07-30 14:24:48 -070092 # This is workaround for a known issue of some veriosn
93 # of api client.
94 # https://github.com/google/google-api-python-client/issues/435
95 cache_discovery=False,
Kevin Cheng3031f8a2018-05-16 13:21:51 -070096 http=http_auth)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070097
Kevin Cheng3031f8a2018-05-16 13:21:51 -070098 @staticmethod
99 def _ShouldRetry(exception, retry_http_codes,
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700100 other_retriable_errors):
101 """Check if exception is retriable.
102
103 Args:
104 exception: An instance of Exception.
105 retry_http_codes: a list of integers, retriable HTTP codes of
106 HttpError
107 other_retriable_errors: a tuple of error types to retry other than
108 HttpError.
109
110 Returns:
111 Boolean, True if retriable, False otherwise.
112 """
113 if isinstance(exception, other_retriable_errors):
114 return True
115
116 if isinstance(exception, errors.HttpError):
117 if exception.code in retry_http_codes:
118 return True
119 else:
120 logger.debug("_ShouldRetry: Exception code %s not in %s: %s",
121 exception.code, retry_http_codes, str(exception))
122
Kevin Cheng3031f8a2018-05-16 13:21:51 -0700123 logger.debug("_ShouldRetry: Exception %s is not one of %s: %s",
124 type(exception),
125 list(other_retriable_errors) + [errors.HttpError],
126 str(exception))
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700127 return False
128
Kevin Cheng3031f8a2018-05-16 13:21:51 -0700129 @staticmethod
130 def _TranslateError(exception):
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700131 """Translate the exception to a desired type.
132
133 Args:
134 exception: An instance of Exception.
135
136 Returns:
137 gerrors.HttpError will be translated to errors.HttpError.
138 If the error code is errors.HTTP_NOT_FOUND_CODE, it will
139 be translated to errors.ResourceNotFoundError.
140 Unrecognized error type will not be translated and will
141 be returned as is.
142 """
143 if isinstance(exception, gerrors.HttpError):
144 exception = errors.HttpError.CreateFromHttpError(exception)
145 if exception.code == errors.HTTP_NOT_FOUND_CODE:
Kevin Cheng3031f8a2018-05-16 13:21:51 -0700146 exception = errors.ResourceNotFoundError(
147 exception.code, str(exception))
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700148 return exception
149
150 def ExecuteOnce(self, api):
151 """Execute an api and parse the errors.
152
153 Args:
154 api: An apiclient.http.HttpRequest, representing the api to execute.
155
156 Returns:
157 Execution result of the api.
158
159 Raises:
160 errors.ResourceNotFoundError: For 404 error.
161 errors.HttpError: For other types of http error.
162 """
163 try:
164 return api.execute()
165 except gerrors.HttpError as e:
166 raise self._TranslateError(e)
167
168 def Execute(self,
169 api,
170 retry_http_codes=None,
171 max_retry=None,
172 sleep=None,
173 backoff_factor=None,
174 other_retriable_errors=None):
175 """Execute an api with retry.
176
177 Call ExecuteOnce and retry on http error with given codes.
178
179 Args:
180 api: An apiclient.http.HttpRequest, representing the api to execute:
181 retry_http_codes: A list of http codes to retry.
Fang Dengf24be082018-02-10 10:09:55 -0800182 max_retry: See utils.Retry.
183 sleep: See utils.Retry.
184 backoff_factor: See utils.Retry.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700185 other_retriable_errors: A tuple of error types that should be retried
186 other than errors.HttpError.
187
188 Returns:
189 Execution result of the api.
190
191 Raises:
192 See ExecuteOnce.
193 """
Kevin Cheng3031f8a2018-05-16 13:21:51 -0700194 retry_http_codes = (self.RETRY_HTTP_CODES
195 if retry_http_codes is None else retry_http_codes)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700196 max_retry = (self.RETRY_COUNT if max_retry is None else max_retry)
197 sleep = (self.RETRY_SLEEP_MULTIPLIER if sleep is None else sleep)
Kevin Cheng3031f8a2018-05-16 13:21:51 -0700198 backoff_factor = (self.RETRY_BACKOFF_FACTOR
199 if backoff_factor is None else backoff_factor)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700200 other_retriable_errors = (self.RETRIABLE_ERRORS
201 if other_retriable_errors is None else
202 other_retriable_errors)
203
204 def _Handler(exc):
205 """Check if |exc| is a retriable exception.
206
207 Args:
208 exc: An exception.
209
210 Returns:
211 True if exc is an errors.HttpError and code exists in |retry_http_codes|
212 False otherwise.
213 """
214 if self._ShouldRetry(exc, retry_http_codes,
215 other_retriable_errors):
216 logger.debug("Will retry error: %s", str(exc))
217 return True
218 return False
219
Fang Dengf24be082018-02-10 10:09:55 -0800220 return utils.Retry(
Kevin Cheng3031f8a2018-05-16 13:21:51 -0700221 _Handler,
222 max_retries=max_retry,
223 functor=self.ExecuteOnce,
224 sleep_multiplier=sleep,
225 retry_backoff_factor=backoff_factor,
226 api=api)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700227
228 def BatchExecuteOnce(self, requests):
229 """Execute requests in a batch.
230
231 Args:
232 requests: A dictionary where key is request id and value
233 is an http request.
234
235 Returns:
236 results, a dictionary in the following format
237 {request_id: (response, exception)}
238 request_ids are those from requests; response
239 is the http response for the request or None on error;
240 exception is an instance of DriverError or None if no error.
241 """
242 results = {}
243
244 def _CallBack(request_id, response, exception):
245 results[request_id] = (response, self._TranslateError(exception))
246
247 batch = apiclient.http.BatchHttpRequest()
248 for request_id, request in requests.iteritems():
Kevin Cheng3031f8a2018-05-16 13:21:51 -0700249 batch.add(
250 request=request, callback=_CallBack, request_id=request_id)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700251 batch.execute()
252 return results
253
254 def BatchExecute(self,
255 requests,
256 retry_http_codes=None,
257 max_retry=None,
258 sleep=None,
259 backoff_factor=None,
260 other_retriable_errors=None):
261 """Batch execute multiple requests with retry.
262
263 Call BatchExecuteOnce and retry on http error with given codes.
264
265 Args:
266 requests: A dictionary where key is request id picked by caller,
267 and value is a apiclient.http.HttpRequest.
268 retry_http_codes: A list of http codes to retry.
Fang Dengf24be082018-02-10 10:09:55 -0800269 max_retry: See utils.Retry.
270 sleep: See utils.Retry.
271 backoff_factor: See utils.Retry.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700272 other_retriable_errors: A tuple of error types that should be retried
273 other than errors.HttpError.
274
275 Returns:
276 results, a dictionary in the following format
277 {request_id: (response, exception)}
278 request_ids are those from requests; response
279 is the http response for the request or None on error;
280 exception is an instance of DriverError or None if no error.
281 """
282 executor = utils.BatchHttpRequestExecutor(
283 self.BatchExecuteOnce,
284 requests=requests,
285 retry_http_codes=retry_http_codes or self.RETRY_HTTP_CODES,
286 max_retry=max_retry or self.RETRY_COUNT,
287 sleep=sleep or self.RETRY_SLEEP_MULTIPLIER,
288 backoff_factor=backoff_factor or self.RETRY_BACKOFF_FACTOR,
Kevin Cheng3031f8a2018-05-16 13:21:51 -0700289 other_retriable_errors=other_retriable_errors
290 or self.RETRIABLE_ERRORS)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700291 executor.Execute()
292 return executor.GetResults()
293
294 def ListWithMultiPages(self, api_resource, *args, **kwargs):
295 """Call an api that list a type of resource.
296
297 Multiple google services support listing a type of
298 resource (e.g list gce instances, list storage objects).
299 The querying pattern is similar --
300 Step 1: execute the api and get a response object like,
301 {
302 "items": [..list of resource..],
303 # The continuation token that can be used
304 # to get the next page.
305 "nextPageToken": "A String",
306 }
307 Step 2: execute the api again with the nextPageToken to
308 retrieve more pages and get a response object.
309
310 Step 3: Repeat Step 2 until no more page.
311
312 This method encapsulates the generic logic of
313 calling such listing api.
314
315 Args:
316 api_resource: An apiclient.discovery.Resource object
317 used to create an http request for the listing api.
318 *args: Arguments used to create the http request.
319 **kwargs: Keyword based arguments to create the http
320 request.
321
322 Returns:
323 A list of items.
324 """
325 items = []
326 next_page_token = None
327 while True:
328 api = api_resource(pageToken=next_page_token, *args, **kwargs)
329 response = self.Execute(api)
330 items.extend(response.get("items", []))
331 next_page_token = response.get("nextPageToken")
332 if not next_page_token:
333 break
334 return items
335
336 @property
337 def service(self):
338 """Return self._service as a property."""
339 return self._service