| #!/usr/bin/env python |
| # |
| # Copyright 2016 - The Android Open Source Project |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| """Base Cloud API Client. |
| |
| BasicCloudApiCliend does basic setup for a cloud API. |
| """ |
| import httplib |
| import logging |
| import os |
| import socket |
| import ssl |
| |
| from apiclient import errors as gerrors |
| from apiclient.discovery import build |
| import apiclient.http |
| import httplib2 |
| from oauth2client import client |
| |
| from acloud.internal.lib import utils |
| from acloud.public import errors |
| |
| logger = logging.getLogger(__name__) |
| |
| |
| class BaseCloudApiClient(object): |
| """A class that does basic setup for a cloud API.""" |
| |
| # To be overriden by subclasses. |
| API_NAME = "" |
| API_VERSION = "v1" |
| SCOPE = "" |
| |
| # Defaults for retry. |
| RETRY_COUNT = 5 |
| RETRY_BACKOFF_FACTOR = 1.5 |
| RETRY_SLEEP_MULTIPLIER = 2 |
| RETRY_HTTP_CODES = [ |
| # 403 is to retry the "Rate Limit Exceeded" error. |
| # We could retry on a finer-grained error message later if necessary. |
| 403, |
| 500, # Internal Server Error |
| 502, # Bad Gateway |
| 503, # Service Unavailable |
| ] |
| RETRIABLE_ERRORS = (httplib.HTTPException, httplib2.HttpLib2Error, |
| socket.error, ssl.SSLError) |
| RETRIABLE_AUTH_ERRORS = (client.AccessTokenRefreshError, ) |
| |
| def __init__(self, oauth2_credentials): |
| """Initialize. |
| |
| Args: |
| oauth2_credentials: An oauth2client.OAuth2Credentials instance. |
| """ |
| self._service = self.InitResourceHandle(oauth2_credentials) |
| |
| @classmethod |
| def InitResourceHandle(cls, oauth2_credentials): |
| """Authenticate and initialize a Resource object. |
| |
| Authenticate http and create a Resource object with methods |
| for interacting with the service. |
| |
| Args: |
| oauth2_credentials: An oauth2client.OAuth2Credentials instance. |
| |
| Returns: |
| An apiclient.discovery.Resource object |
| """ |
| http_auth = oauth2_credentials.authorize(httplib2.Http()) |
| return utils.RetryExceptionType( |
| exception_types=cls.RETRIABLE_AUTH_ERRORS, |
| max_retries=cls.RETRY_COUNT, |
| functor=build, |
| sleep_multiplier=cls.RETRY_SLEEP_MULTIPLIER, |
| retry_backoff_factor=cls.RETRY_BACKOFF_FACTOR, |
| serviceName=cls.API_NAME, |
| version=cls.API_VERSION, |
| http=http_auth) |
| |
| def _ShouldRetry(self, exception, retry_http_codes, |
| other_retriable_errors): |
| """Check if exception is retriable. |
| |
| Args: |
| exception: An instance of Exception. |
| retry_http_codes: a list of integers, retriable HTTP codes of |
| HttpError |
| other_retriable_errors: a tuple of error types to retry other than |
| HttpError. |
| |
| Returns: |
| Boolean, True if retriable, False otherwise. |
| """ |
| if isinstance(exception, other_retriable_errors): |
| return True |
| |
| if isinstance(exception, errors.HttpError): |
| if exception.code in retry_http_codes: |
| return True |
| else: |
| logger.debug("_ShouldRetry: Exception code %s not in %s: %s", |
| exception.code, retry_http_codes, str(exception)) |
| |
| logger.debug( |
| "_ShouldRetry: Exception %s is not one of %s: %s", type(exception), |
| list(other_retriable_errors) + [errors.HttpError], str(exception)) |
| return False |
| |
| def _TranslateError(self, exception): |
| """Translate the exception to a desired type. |
| |
| Args: |
| exception: An instance of Exception. |
| |
| Returns: |
| gerrors.HttpError will be translated to errors.HttpError. |
| If the error code is errors.HTTP_NOT_FOUND_CODE, it will |
| be translated to errors.ResourceNotFoundError. |
| Unrecognized error type will not be translated and will |
| be returned as is. |
| """ |
| if isinstance(exception, gerrors.HttpError): |
| exception = errors.HttpError.CreateFromHttpError(exception) |
| if exception.code == errors.HTTP_NOT_FOUND_CODE: |
| exception = errors.ResourceNotFoundError(exception.code, |
| str(exception)) |
| return exception |
| |
| def ExecuteOnce(self, api): |
| """Execute an api and parse the errors. |
| |
| Args: |
| api: An apiclient.http.HttpRequest, representing the api to execute. |
| |
| Returns: |
| Execution result of the api. |
| |
| Raises: |
| errors.ResourceNotFoundError: For 404 error. |
| errors.HttpError: For other types of http error. |
| """ |
| try: |
| return api.execute() |
| except gerrors.HttpError as e: |
| raise self._TranslateError(e) |
| |
| def Execute(self, |
| api, |
| retry_http_codes=None, |
| max_retry=None, |
| sleep=None, |
| backoff_factor=None, |
| other_retriable_errors=None): |
| """Execute an api with retry. |
| |
| Call ExecuteOnce and retry on http error with given codes. |
| |
| Args: |
| api: An apiclient.http.HttpRequest, representing the api to execute: |
| retry_http_codes: A list of http codes to retry. |
| max_retry: See utils.Retry. |
| sleep: See utils.Retry. |
| backoff_factor: See utils.Retry. |
| other_retriable_errors: A tuple of error types that should be retried |
| other than errors.HttpError. |
| |
| Returns: |
| Execution result of the api. |
| |
| Raises: |
| See ExecuteOnce. |
| """ |
| retry_http_codes = (self.RETRY_HTTP_CODES if retry_http_codes is None |
| else retry_http_codes) |
| max_retry = (self.RETRY_COUNT if max_retry is None else max_retry) |
| sleep = (self.RETRY_SLEEP_MULTIPLIER if sleep is None else sleep) |
| backoff_factor = (self.RETRY_BACKOFF_FACTOR if backoff_factor is None |
| else backoff_factor) |
| other_retriable_errors = (self.RETRIABLE_ERRORS |
| if other_retriable_errors is None else |
| other_retriable_errors) |
| |
| def _Handler(exc): |
| """Check if |exc| is a retriable exception. |
| |
| Args: |
| exc: An exception. |
| |
| Returns: |
| True if exc is an errors.HttpError and code exists in |retry_http_codes| |
| False otherwise. |
| """ |
| if self._ShouldRetry(exc, retry_http_codes, |
| other_retriable_errors): |
| logger.debug("Will retry error: %s", str(exc)) |
| return True |
| return False |
| |
| return utils.Retry( |
| _Handler, max_retries=max_retry, functor=self.ExecuteOnce, |
| sleep_multiplier=sleep, retry_backoff_factor=backoff_factor, |
| api=api) |
| |
| def BatchExecuteOnce(self, requests): |
| """Execute requests in a batch. |
| |
| Args: |
| requests: A dictionary where key is request id and value |
| is an http request. |
| |
| Returns: |
| results, a dictionary in the following format |
| {request_id: (response, exception)} |
| request_ids are those from requests; response |
| is the http response for the request or None on error; |
| exception is an instance of DriverError or None if no error. |
| """ |
| results = {} |
| |
| def _CallBack(request_id, response, exception): |
| results[request_id] = (response, self._TranslateError(exception)) |
| |
| batch = apiclient.http.BatchHttpRequest() |
| for request_id, request in requests.iteritems(): |
| batch.add(request=request, |
| callback=_CallBack, |
| request_id=request_id) |
| batch.execute() |
| return results |
| |
| def BatchExecute(self, |
| requests, |
| retry_http_codes=None, |
| max_retry=None, |
| sleep=None, |
| backoff_factor=None, |
| other_retriable_errors=None): |
| """Batch execute multiple requests with retry. |
| |
| Call BatchExecuteOnce and retry on http error with given codes. |
| |
| Args: |
| requests: A dictionary where key is request id picked by caller, |
| and value is a apiclient.http.HttpRequest. |
| retry_http_codes: A list of http codes to retry. |
| max_retry: See utils.Retry. |
| sleep: See utils.Retry. |
| backoff_factor: See utils.Retry. |
| other_retriable_errors: A tuple of error types that should be retried |
| other than errors.HttpError. |
| |
| Returns: |
| results, a dictionary in the following format |
| {request_id: (response, exception)} |
| request_ids are those from requests; response |
| is the http response for the request or None on error; |
| exception is an instance of DriverError or None if no error. |
| """ |
| executor = utils.BatchHttpRequestExecutor( |
| self.BatchExecuteOnce, |
| requests=requests, |
| retry_http_codes=retry_http_codes or self.RETRY_HTTP_CODES, |
| max_retry=max_retry or self.RETRY_COUNT, |
| sleep=sleep or self.RETRY_SLEEP_MULTIPLIER, |
| backoff_factor=backoff_factor or self.RETRY_BACKOFF_FACTOR, |
| other_retriable_errors=other_retriable_errors or |
| self.RETRIABLE_ERRORS) |
| executor.Execute() |
| return executor.GetResults() |
| |
| def ListWithMultiPages(self, api_resource, *args, **kwargs): |
| """Call an api that list a type of resource. |
| |
| Multiple google services support listing a type of |
| resource (e.g list gce instances, list storage objects). |
| The querying pattern is similar -- |
| Step 1: execute the api and get a response object like, |
| { |
| "items": [..list of resource..], |
| # The continuation token that can be used |
| # to get the next page. |
| "nextPageToken": "A String", |
| } |
| Step 2: execute the api again with the nextPageToken to |
| retrieve more pages and get a response object. |
| |
| Step 3: Repeat Step 2 until no more page. |
| |
| This method encapsulates the generic logic of |
| calling such listing api. |
| |
| Args: |
| api_resource: An apiclient.discovery.Resource object |
| used to create an http request for the listing api. |
| *args: Arguments used to create the http request. |
| **kwargs: Keyword based arguments to create the http |
| request. |
| |
| Returns: |
| A list of items. |
| """ |
| items = [] |
| next_page_token = None |
| while True: |
| api = api_resource(pageToken=next_page_token, *args, **kwargs) |
| response = self.Execute(api) |
| items.extend(response.get("items", [])) |
| next_page_token = response.get("nextPageToken") |
| if not next_page_token: |
| break |
| return items |
| |
| @property |
| def service(self): |
| """Return self._service as a property.""" |
| return self._service |