ebcli/lib/aws.py (294 lines of code) (raw):
# Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file 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.
import os
import random
import time
import warnings
import botocore
import botocore.exceptions
import botocore.parsers
import botocore.session
from botocore.config import Config
from botocore.loaders import Loader
from cement.utils.misc import minimal_logger
from ebcli import __version__
from ebcli.core import fileoperations
from ebcli.lib.botopatch import apply_patches
from ebcli.lib.utils import static_var
from ebcli.objects.exceptions import ServiceError, NotAuthorizedError, \
CredentialsError, NoRegionError, ValidationError, \
InvalidProfileError, ConnectionError, AlreadyExistsError, NotFoundError, \
NotAuthorizedInRegionError
from ebcli.resources.strings import strings
LOG = minimal_logger(__name__)
BOTOCORE_DATA_FOLDER_NAME = 'botocoredata'
_api_clients = {}
_profile = None
_profile_env_var = 'AWS_EB_PROFILE'
_id = None
_key = None
_region_name = None
_verify_ssl = True
_endpoint_url = None
_debug = False
apply_patches()
def _flush():
# Should be used for resetting tests only
global _api_clients, _profile, _id, _key, _region_name, _verify_ssl
_api_clients = {}
_get_botocore_session.botocore_session = None
_profile = None
_id = None
_key = None
_region_name = None
_verify_ssl = True
def set_session_creds(id, key):
global _api_clients, _id, _key
_id = id
_key = key
# invalidate all old clients
_api_clients = {}
def set_profile(profile):
global _profile, _api_clients
_profile = profile
# Invalidate session and old clients
_get_botocore_session.botocore_session = None
_api_clients = {}
def get_profile():
if _profile is not None:
return _profile
from ebcli.operations import commonops
return commonops.get_default_profile(require_default=True)
def set_region(region_name):
global _region_name
_region_name = region_name
# Invalidate session and old clients
_get_botocore_session.botocore_session = None
def set_endpoint_url(endpoint_url):
global _endpoint_url
_endpoint_url = endpoint_url
def no_verify_ssl():
global _verify_ssl
_verify_ssl = False
def set_profile_override(profile):
global _profile_env_var
set_profile(profile)
_profile_env_var = None
def set_debug():
global _debug
_debug = True
def _set_user_agent_for_session(session):
session.user_agent_name = 'eb-cli'
session.user_agent_version = __version__
def _get_data_loader():
# Creates a botocore data loader that loads custom data files
# FIRST, creating a precedence for custom files.
data_folder = os.path.join(os.path.dirname(os.path.realpath(__file__)),
BOTOCORE_DATA_FOLDER_NAME)
return Loader(extra_search_paths=[data_folder, Loader.BUILTIN_DATA_PATH],
include_default_search_paths=False)
def _get_client(service_name):
aws_access_key_id = _id
aws_secret_key = _key
if service_name in _api_clients:
return _api_clients[service_name]
session = _get_botocore_session()
if service_name == 'elasticbeanstalk':
endpoint_url = _endpoint_url
else:
endpoint_url = None
try:
LOG.debug('Creating new Botocore Client for ' + str(service_name))
client = session.create_client(service_name,
endpoint_url=endpoint_url,
aws_access_key_id=aws_access_key_id,
aws_secret_access_key=aws_secret_key,
verify=_verify_ssl,
config=Config(signature_version='s3v4'))
except botocore.exceptions.ProfileNotFound as e:
raise InvalidProfileError(e)
LOG.debug('Successfully created session for ' + service_name)
_api_clients[service_name] = client
return client
@static_var('botocore_session', None)
def _get_botocore_session():
if _get_botocore_session.botocore_session is None:
LOG.debug('Creating new Botocore Session')
LOG.debug('Botocore version: {0}'.format(botocore.__version__))
session = botocore.session.get_session({
'profile': (None, _profile_env_var, _profile, None),
})
session.set_config_variable('region', _region_name)
session.set_config_variable('profile', _profile)
session.register_component('data_loader', _get_data_loader())
_set_user_agent_for_session(session)
_get_botocore_session.botocore_session = session
if _debug:
session.set_debug_logger()
return _get_botocore_session.botocore_session
def get_region_name():
return _region_name
def get_credentials():
client_creds = _get_client('elasticbeanstalk')._request_signer._credentials
return botocore.credentials.Credentials(
access_key=client_creds.access_key,
secret_key=client_creds.secret_key,
)
def make_api_call(service_name, operation_name, **operation_options):
operation = _set_operation(service_name, operation_name)
aggregated_error_message = []
max_attempts = 10
region = _region_name
if not region:
region = 'default'
if region == 'placeholder':
set_region(fileoperations.get_config_setting('global', 'default_region'))
attempt = 0
while True:
attempt += 1
if attempt > 1:
LOG.debug('Retrying -- attempt #' + str(attempt))
_sleep(_get_delay(attempt))
try:
LOG.debug('Making api call: (' +
service_name + ', ' + operation_name +
') to region: ' + region + ' with args:' + str(operation_options))
response_data = operation(**operation_options)
status = response_data['ResponseMetadata']['HTTPStatusCode']
LOG.debug('API call finished, status = ' + str(status))
if response_data:
LOG.debug('Response: ' + str(response_data))
return response_data
except botocore.exceptions.ClientError as e:
_handle_response_code(e.response, attempt, aggregated_error_message)
except botocore.parsers.ResponseParserError as e:
LOG.debug('Botocore could not parse response received')
if attempt > max_attempts:
raise MaxRetriesError(
'Max retries exceeded for ResponseParserErrors'
+ os.linesep.join(aggregated_error_message)
)
aggregated_error_message.insert(attempt, str(e))
except botocore.exceptions.NoCredentialsError:
LOG.debug('No credentials found')
raise CredentialsError('Operation Denied. You appear to have no'
' credentials')
except botocore.exceptions.PartialCredentialsError as e:
LOG.debug('Credentials incomplete')
raise CredentialsError(str(e))
except (botocore.exceptions.ValidationError,
botocore.exceptions.ParamValidationError) as e:
raise ValidationError(str(e))
except botocore.exceptions.EndpointConnectionError as e:
LOG.debug(e)
raise
except botocore.exceptions.BotoCoreError:
LOG.error('Botocore Error')
raise
except IOError as error:
if hasattr(error.args[0], 'reason') and str(error.args[0].reason) == \
'[Errno -2] Name or service not known':
raise ConnectionError()
LOG.error('Error while contacting Elastic Beanstalk Service')
LOG.debug('error:' + str(error))
raise ServiceError(error)
def _handle_response_code(response_data, attempt, aggregated_error_message):
max_attempts = 10
LOG.debug('Response: ' + str(response_data))
status = response_data['ResponseMetadata']['HTTPStatusCode']
LOG.debug('API call finished, status = ' + str(status))
try:
message = str(response_data['Error']['Message'])
except KeyError:
message = ""
if status == 400:
error = _get_400_error(response_data, message)
if isinstance(error, ThrottlingError):
LOG.debug('Received throttling error')
if attempt > max_attempts:
raise MaxRetriesError('Max retries exceeded for '
'throttling error')
else:
raise error
elif status == 403:
LOG.debug('Received a 403')
if not message:
message = 'Are your permissions correct?'
if _region_name == 'cn-north-1':
raise NotAuthorizedInRegionError('Operation Denied. ' + message +
'\n' +
strings['region.china.credentials'])
else:
raise NotAuthorizedError('Operation Denied. ' + message)
elif status == 404:
LOG.debug('Received a 404')
raise NotFoundError(message)
elif status == 409:
LOG.debug('Received a 409')
raise AlreadyExistsError(message)
elif status in (500, 503, 504):
LOG.debug('Received 5XX error')
retry_failure_message = \
'Received 5XX error during attempt #{0}\n {1}\n'.format(
str(attempt),
message
)
aggregated_error_message.insert(attempt, retry_failure_message)
if attempt > max_attempts:
_handle_500_error(('\n').join(aggregated_error_message))
else:
raise ServiceError('API Call unsuccessful. '
'Status code returned ' + str(status))
def _set_operation(service_name, operation_name):
try:
client = _get_client(service_name)
except botocore.exceptions.UnknownEndpointError as e:
raise NoRegionError(e)
except botocore.exceptions.PartialCredentialsError as e:
LOG.debug('Credentials incomplete')
raise CredentialsError('Your credentials are not complete. Error: {0}'
.format(e))
except botocore.exceptions.NoRegionError:
raise NoRegionError()
if not _verify_ssl:
warnings.filterwarnings("ignore")
return getattr(client, operation_name)
def _get_delay(attempt_number):
if attempt_number == 1:
return 0
# Exponential backoff
rand_int = random.randrange(0, 2**attempt_number)
delay = rand_int * 0.05 # delay time is 50 ms
LOG.debug('Sleeping for ' + str(delay) + ' seconds.')
return delay
def _get_400_error(response_data, message):
code = response_data['Error']['Code']
LOG.debug('Received a 400 Error')
if code == 'InvalidParameterValue':
return InvalidParameterValueError(message)
elif code == 'InvalidQueryParameter':
return InvalidQueryParameterError(message)
elif code.startswith('Throttling'):
return ThrottlingError(message)
elif code.startswith('ResourceNotFound'):
return NotFoundError(message)
elif code.startswith('TooManyPlatformsException'):
return TooManyPlatformsError(message)
elif code.startswith('TooManyConfigurationTemplatesException'):
message = [
'Your request cannot be completed. You have reached the maximum',
'number of saved configuration templates. Learn more about service',
'limits: http://docs.aws.amazon.com/general/latest/gr/aws_service_limits.html'
]
return TooManyConfigurationTemplatesException(' '.join(message))
else:
return ServiceError(message, code=code)
def _handle_500_error(aggregated_error_message):
raise MaxRetriesError('Max retries exceeded for '
'service error (5XX)\n' +
aggregated_error_message)
def _sleep(delay):
time.sleep(delay)
class InvalidParameterValueError(ServiceError):
pass
class InvalidQueryParameterError(ServiceError):
pass
class ThrottlingError(ServiceError):
pass
class MaxRetriesError(ServiceError):
pass
class TooManyPlatformsError(ServiceError):
pass
class TooManyConfigurationTemplatesException(ServiceError):
pass