python-botocore/botocore/waiter.py
2015-10-08 11:15:31 -07:00

172 lines
6.7 KiB
Python

# Copyright 2012-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 jmespath
import logging
import time
from .exceptions import WaiterError
logger = logging.getLogger(__name__)
class Waiter(object):
"""Wait for a resource to reach a certain state.
In addition to creating this class manually, you can
also use ``botocore.service.Service.get_waiter`` to
create an instance of ``Waiter```.
The typical usage pattern is from a ``Service`` object::
ec2 = session.get_service('ec2')
p = ec2.get_operation('RunInstances').call(endpoint, **kwargs)[1]
instance_running = ec2.get_waiter('InstanceRunning')
instance_id = p['Reservations'][0]['Instances'][0]['InstanceId']
# This will block until the instance reaches a 'running' state.
instance_running.wait(instance_ids=[instance_id])
"""
def __init__(self, name, operation, config):
"""
:type name: str
:param name: The name of the waiter.
:type operation: ``botocore.operation.Operation``
:param operation: The operation associated with the waiter.
This is specified in the waiter configuration as the
``operation`` key.
:type config: dict
:param config: The waiter configuration.
"""
self.name = name
self.operation = operation
self.sleep_time = config['interval']
self.max_attempts = config['max_attempts']
self.success = self._process_config(config.get('success'))
self.failure = self._process_config(config.get('failure'))
def _process_config(self, acceptor_config):
if acceptor_config is None:
return {}
new_config = acceptor_config.copy()
if new_config['type'] == 'output' and \
new_config.get('path') is not None:
new_config['path'] = jmespath.compile(acceptor_config['path'])
return new_config
def wait(self, endpoint, **kwargs):
"""Wait until a resource reaches its success state.
Calling this method will block until the waiter reaches its
desired state. If the failure state is reached, a ``WaiterError``
is raised.
The ``**kwargs`` passed to this method will be forwarded to the
operation associated with the waiter.
:param endpoint: An instance of ``botocore.endpoint.Endpoint``.
"""
logger.debug("Waiter %s waiting.", self.name)
num_attempts = 0
while num_attempts < self.max_attempts:
http_response, parsed = self.operation.call(endpoint, **kwargs)
if self.success:
if self._matches_acceptor_state(self.success,
http_response, parsed):
# For the success state, if the acceptor matches then we
# break the loop.
break
if self.failure:
if self._matches_acceptor_state(self.failure,
http_response, parsed):
# For the failure state, if the acceptor matches then we
# raise an exception.
raise WaiterError(
name=self.name,
reason='Failure state matched one of: %s' %
', '.join(self.failure['value']))
logger.debug("No acceptor state reached for waiter %s, "
"attempt %s/%s, sleeping for: %s",
self.name, num_attempts, self.max_attempts,
self.sleep_time)
num_attempts += 1
time.sleep(self.sleep_time)
else:
error_msg = ("Max attempts (%s) exceeded for waiter %s without "
"reaching a terminal state."
% (self.max_attempts, self.name))
logger.debug(error_msg)
raise WaiterError(name=self.name, reason=error_msg)
def _matches_acceptor_state(self, acceptor, http_response, parsed):
if acceptor['type'] == 'output':
return self._matches_acceptor_output_type(acceptor, http_response,
parsed)
elif acceptor['type'] == 'error':
return self._matches_acceptor_error_type(acceptor, http_response,
parsed)
def _matches_acceptor_output_type(self, acceptor, http_response, parsed):
if 'path' not in acceptor and not self._get_error_codes_from_response(parsed):
# If there's no path specified, then a successful response means
# that we've matched the acceptor.
return True
match = acceptor['path'].search(parsed)
return self._path_matches_value(match, acceptor['value'])
def _path_matches_value(self, match, value):
# Determine if the matched data matches the config value.
if match is None:
return False
elif not isinstance(match, list):
# If match is not a list, then we need to perform an exact match,
# this is something like Table.TableStatus == 'CREATING'
return self._single_value_match(match, value)
elif isinstance(match, list):
# If ``match`` is a list, then we need to ensure that every element
# in ``match`` matches something in the ``value`` list.
return all(self._single_value_match(element, value)
for element in match)
else:
return False
def _single_value_match(self, match, value):
for v in value:
if match == v:
return True
else:
return False
def _matches_acceptor_error_type(self, acceptor, http_response, parsed):
if http_response.status_code >= 400 and 'Errors' in parsed:
error_codes = self._get_error_codes_from_response(parsed)
for v in acceptor['value']:
if v in error_codes:
return True
return False
def _get_error_codes_from_response(self, parsed):
errors = set()
for error in parsed.get('Errors', []):
if 'Code' in error:
errors.add(error['Code'])
return errors