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

189 lines
7.3 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 BaseWaiter(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 _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'])
elif new_config['type'] == 'output' and len(new_config) == 1:
# There are some cases where a waiter config erroneously defines
# an acceptor type of "output" which cascades to the failure type,
# but there is no path or value specified for failures. While
# the model is eventually going to be corrected, for now, we
# need to ignore this case and just return an empty dict
# indicating that this particular acceptor state should be ignored.
return {}
return new_config
def wait(self, **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:
parsed = self._make_api_call(**kwargs)
if self.success:
if self._matches_acceptor_state(self.success, 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, 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, parsed):
if acceptor['type'] == 'output':
return self._matches_acceptor_output_type(acceptor, parsed)
elif acceptor['type'] == 'error':
return self._matches_acceptor_error_type(acceptor, parsed)
def _matches_acceptor_output_type(self, acceptor, 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, parsed):
if 'Error' in parsed:
error_code = parsed['Error']['Code']
for v in acceptor['value']:
if v == error_code:
return True
return False
class LegacyWaiter(BaseWaiter):
def __init__(self, name, operation, endpoint, 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.endpoint = endpoint
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 _make_api_call(self, **kwargs):
parsed = self.operation.call(self.endpoint, **kwargs)[1]
return parsed
class Waiter(BaseWaiter):
def __init__(self, name, operation_method, config):
self.name = name
self.operation_method = operation_method
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 _make_api_call(self, **kwargs):
return self.operation_method(**kwargs)