blob: 67e26b196e10de657381dc601e21d61b5f420666 [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
32from acloud.internal.lib import utils
33from acloud.public import errors
34
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,
92 http=http_auth)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070093
Kevin Cheng3031f8a2018-05-16 13:21:51 -070094 @staticmethod
95 def _ShouldRetry(exception, retry_http_codes,
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070096 other_retriable_errors):
97 """Check if exception is retriable.
98
99 Args:
100 exception: An instance of Exception.
101 retry_http_codes: a list of integers, retriable HTTP codes of
102 HttpError
103 other_retriable_errors: a tuple of error types to retry other than
104 HttpError.
105
106 Returns:
107 Boolean, True if retriable, False otherwise.
108 """
109 if isinstance(exception, other_retriable_errors):
110 return True
111
112 if isinstance(exception, errors.HttpError):
113 if exception.code in retry_http_codes:
114 return True
115 else:
116 logger.debug("_ShouldRetry: Exception code %s not in %s: %s",
117 exception.code, retry_http_codes, str(exception))
118
Kevin Cheng3031f8a2018-05-16 13:21:51 -0700119 logger.debug("_ShouldRetry: Exception %s is not one of %s: %s",
120 type(exception),
121 list(other_retriable_errors) + [errors.HttpError],
122 str(exception))
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700123 return False
124
Kevin Cheng3031f8a2018-05-16 13:21:51 -0700125 @staticmethod
126 def _TranslateError(exception):
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700127 """Translate the exception to a desired type.
128
129 Args:
130 exception: An instance of Exception.
131
132 Returns:
133 gerrors.HttpError will be translated to errors.HttpError.
134 If the error code is errors.HTTP_NOT_FOUND_CODE, it will
135 be translated to errors.ResourceNotFoundError.
136 Unrecognized error type will not be translated and will
137 be returned as is.
138 """
139 if isinstance(exception, gerrors.HttpError):
140 exception = errors.HttpError.CreateFromHttpError(exception)
141 if exception.code == errors.HTTP_NOT_FOUND_CODE:
Kevin Cheng3031f8a2018-05-16 13:21:51 -0700142 exception = errors.ResourceNotFoundError(
143 exception.code, str(exception))
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700144 return exception
145
146 def ExecuteOnce(self, api):
147 """Execute an api and parse the errors.
148
149 Args:
150 api: An apiclient.http.HttpRequest, representing the api to execute.
151
152 Returns:
153 Execution result of the api.
154
155 Raises:
156 errors.ResourceNotFoundError: For 404 error.
157 errors.HttpError: For other types of http error.
158 """
159 try:
160 return api.execute()
161 except gerrors.HttpError as e:
162 raise self._TranslateError(e)
163
164 def Execute(self,
165 api,
166 retry_http_codes=None,
167 max_retry=None,
168 sleep=None,
169 backoff_factor=None,
170 other_retriable_errors=None):
171 """Execute an api with retry.
172
173 Call ExecuteOnce and retry on http error with given codes.
174
175 Args:
176 api: An apiclient.http.HttpRequest, representing the api to execute:
177 retry_http_codes: A list of http codes to retry.
Fang Dengf24be082018-02-10 10:09:55 -0800178 max_retry: See utils.Retry.
179 sleep: See utils.Retry.
180 backoff_factor: See utils.Retry.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700181 other_retriable_errors: A tuple of error types that should be retried
182 other than errors.HttpError.
183
184 Returns:
185 Execution result of the api.
186
187 Raises:
188 See ExecuteOnce.
189 """
Kevin Cheng3031f8a2018-05-16 13:21:51 -0700190 retry_http_codes = (self.RETRY_HTTP_CODES
191 if retry_http_codes is None else retry_http_codes)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700192 max_retry = (self.RETRY_COUNT if max_retry is None else max_retry)
193 sleep = (self.RETRY_SLEEP_MULTIPLIER if sleep is None else sleep)
Kevin Cheng3031f8a2018-05-16 13:21:51 -0700194 backoff_factor = (self.RETRY_BACKOFF_FACTOR
195 if backoff_factor is None else backoff_factor)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700196 other_retriable_errors = (self.RETRIABLE_ERRORS
197 if other_retriable_errors is None else
198 other_retriable_errors)
199
200 def _Handler(exc):
201 """Check if |exc| is a retriable exception.
202
203 Args:
204 exc: An exception.
205
206 Returns:
207 True if exc is an errors.HttpError and code exists in |retry_http_codes|
208 False otherwise.
209 """
210 if self._ShouldRetry(exc, retry_http_codes,
211 other_retriable_errors):
212 logger.debug("Will retry error: %s", str(exc))
213 return True
214 return False
215
Fang Dengf24be082018-02-10 10:09:55 -0800216 return utils.Retry(
Kevin Cheng3031f8a2018-05-16 13:21:51 -0700217 _Handler,
218 max_retries=max_retry,
219 functor=self.ExecuteOnce,
220 sleep_multiplier=sleep,
221 retry_backoff_factor=backoff_factor,
222 api=api)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700223
224 def BatchExecuteOnce(self, requests):
225 """Execute requests in a batch.
226
227 Args:
228 requests: A dictionary where key is request id and value
229 is an http request.
230
231 Returns:
232 results, a dictionary in the following format
233 {request_id: (response, exception)}
234 request_ids are those from requests; response
235 is the http response for the request or None on error;
236 exception is an instance of DriverError or None if no error.
237 """
238 results = {}
239
240 def _CallBack(request_id, response, exception):
241 results[request_id] = (response, self._TranslateError(exception))
242
243 batch = apiclient.http.BatchHttpRequest()
244 for request_id, request in requests.iteritems():
Kevin Cheng3031f8a2018-05-16 13:21:51 -0700245 batch.add(
246 request=request, callback=_CallBack, request_id=request_id)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700247 batch.execute()
248 return results
249
250 def BatchExecute(self,
251 requests,
252 retry_http_codes=None,
253 max_retry=None,
254 sleep=None,
255 backoff_factor=None,
256 other_retriable_errors=None):
257 """Batch execute multiple requests with retry.
258
259 Call BatchExecuteOnce and retry on http error with given codes.
260
261 Args:
262 requests: A dictionary where key is request id picked by caller,
263 and value is a apiclient.http.HttpRequest.
264 retry_http_codes: A list of http codes to retry.
Fang Dengf24be082018-02-10 10:09:55 -0800265 max_retry: See utils.Retry.
266 sleep: See utils.Retry.
267 backoff_factor: See utils.Retry.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700268 other_retriable_errors: A tuple of error types that should be retried
269 other than errors.HttpError.
270
271 Returns:
272 results, a dictionary in the following format
273 {request_id: (response, exception)}
274 request_ids are those from requests; response
275 is the http response for the request or None on error;
276 exception is an instance of DriverError or None if no error.
277 """
278 executor = utils.BatchHttpRequestExecutor(
279 self.BatchExecuteOnce,
280 requests=requests,
281 retry_http_codes=retry_http_codes or self.RETRY_HTTP_CODES,
282 max_retry=max_retry or self.RETRY_COUNT,
283 sleep=sleep or self.RETRY_SLEEP_MULTIPLIER,
284 backoff_factor=backoff_factor or self.RETRY_BACKOFF_FACTOR,
Kevin Cheng3031f8a2018-05-16 13:21:51 -0700285 other_retriable_errors=other_retriable_errors
286 or self.RETRIABLE_ERRORS)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700287 executor.Execute()
288 return executor.GetResults()
289
290 def ListWithMultiPages(self, api_resource, *args, **kwargs):
291 """Call an api that list a type of resource.
292
293 Multiple google services support listing a type of
294 resource (e.g list gce instances, list storage objects).
295 The querying pattern is similar --
296 Step 1: execute the api and get a response object like,
297 {
298 "items": [..list of resource..],
299 # The continuation token that can be used
300 # to get the next page.
301 "nextPageToken": "A String",
302 }
303 Step 2: execute the api again with the nextPageToken to
304 retrieve more pages and get a response object.
305
306 Step 3: Repeat Step 2 until no more page.
307
308 This method encapsulates the generic logic of
309 calling such listing api.
310
311 Args:
312 api_resource: An apiclient.discovery.Resource object
313 used to create an http request for the listing api.
314 *args: Arguments used to create the http request.
315 **kwargs: Keyword based arguments to create the http
316 request.
317
318 Returns:
319 A list of items.
320 """
321 items = []
322 next_page_token = None
323 while True:
324 api = api_resource(pageToken=next_page_token, *args, **kwargs)
325 response = self.Execute(api)
326 items.extend(response.get("items", []))
327 next_page_token = response.get("nextPageToken")
328 if not next_page_token:
329 break
330 return items
331
332 @property
333 def service(self):
334 """Return self._service as a property."""
335 return self._service