Skip to content

Commit

Permalink
Expose waiters in the CLI
Browse files Browse the repository at this point in the history
Add a ``wait`` command to all services that have waiters. For each type of
waiter, a subcommand representing that waiter was added. For example, to wait
for an ec2 instance to reach the running state, the wait command would be
specified as ``aws ec2 wait instance-running``.

Conflicts:
	awscli/handlers.py
  • Loading branch information
kyleknap authored and jamesls committed Nov 10, 2014
1 parent b854792 commit e5c3639
Show file tree
Hide file tree
Showing 8 changed files with 608 additions and 6 deletions.
11 changes: 10 additions & 1 deletion awscli/clidocs.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,16 @@ def doc_subitems_start(self, help_command, **kwargs):

def doc_subitem(self, command_name, help_command, **kwargs):
doc = help_command.doc
doc.style.tocitem(command_name)
subcommand = help_command.command_table[command_name]
subcommand_table = getattr(subcommand, 'subcommand_table', {})
# If the subcommand table has commands in it,
# direct the subitem to the command's index because
# it has more subcommands to be documented.
if (len(subcommand_table) > 0):
file_name = '%s/index' % command_name
doc.style.tocitem(command_name, file_name=file_name)
else:
doc.style.tocitem(command_name)


class OperationDocumentEventHandler(CLIDocumentEventHandler):
Expand Down
3 changes: 2 additions & 1 deletion awscli/clidriver.py
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,8 @@ def _create_command_table(self):
service_object=service_object)
self.session.emit('building-command-table.%s' % self._name,
command_table=command_table,
session=self.session)
session=self.session,
service_object=service_object)
return command_table

def create_help_command(self):
Expand Down
216 changes: 216 additions & 0 deletions awscli/customizations/waiters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
# 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.
from botocore import xform_name

from awscli.clidriver import ServiceOperation
from awscli.customizations.commands import BasicCommand, BasicHelp, \
BasicDocHandler


def register_add_waiters(cli):
cli.register('building-command-table', add_waiters)


def add_waiters(command_table, session, service_object=None, **kwargs):
# If a service object was passed in, try to add a wait command.
if service_object is not None:
# Get a client out of the service object.
client = translate_service_object_to_client(service_object)
# Find all of the waiters for that client.
waiters = client.waiter_names
# If there are waiters make a wait command.
if waiters:
command_table['wait'] = WaitCommand(client, service_object)


def translate_service_object_to_client(service_object):
# Create a client from a service object.
session = service_object.session
return session.create_client(service_object.service_name)


class WaitCommand(BasicCommand):
NAME = 'wait'
DESCRIPTION = 'Wait until a particular condition is satisfied.'

def __init__(self, client, service_object):
self._client = client
self._service_object = service_object
self.waiter_cmd_builder = WaiterStateCommandBuilder(
client=self._client,
service_object=self._service_object
)
super(WaitCommand, self).__init__(self._service_object.session)

def _run_main(self, parsed_args, parsed_globals):
if parsed_args.subcommand is None:
raise ValueError("usage: aws [options] <command> <subcommand> "
"[parameters]\naws: error: too few arguments")

def _build_subcommand_table(self):
subcommand_table = super(WaitCommand, self)._build_subcommand_table()
self.waiter_cmd_builder.build_all_waiter_state_cmds(subcommand_table)
return subcommand_table

def create_help_command(self):
return BasicHelp(self._session, self,
command_table=self.subcommand_table,
arg_table=self.arg_table,
event_handler_class=WaiterCommandDocHandler)


class WaiterStateCommandBuilder(object):
def __init__(self, client, service_object):
self._client = client
self._service_object = service_object

def build_all_waiter_state_cmds(self, subcommand_table):
"""This adds waiter state commands to the subcommand table passed in.
This is the method that adds waiter state commands like
``instance-running`` to ``ec2 wait``.
"""
waiters = self._client.waiter_names
for waiter_name in waiters:
waiter_cli_name = waiter_name.replace('_', '-')
subcommand_table[waiter_cli_name] = \
self._build_waiter_state_cmd(waiter_name)

def _build_waiter_state_cmd(self, waiter_name):
# Get the waiter
waiter = self._client.get_waiter(waiter_name)

# Create the cli name for the waiter operation
waiter_cli_name = waiter_name.replace('_', '-')

# Obtain the name of the service operation that is used to implement
# the specified waiter.
operation_name = waiter.config.operation

# Create an operation object to make a command for the waiter. The
# operation object is used to generate the arguments for the waiter
# state command.
operation_object = self._service_object.get_operation(operation_name)
waiter_state_command = WaiterStateCommand(
name=waiter_cli_name, parent_name='wait',
operation_object=operation_object,
operation_caller=WaiterCaller(self._client, waiter),
service_object=self._service_object
)
# Build the top level description for the waiter state command.
# Most waiters do not have a description so they need to be generated
# using the waiter configuration.
waiter_state_doc_builder = WaiterStateDocBuilder(waiter.config)
description = waiter_state_doc_builder.build_waiter_state_description()
waiter_state_command.DESCRIPTION = description
return waiter_state_command


class WaiterStateDocBuilder(object):
SUCCESS_DESCRIPTIONS = {
'error': u'%s is thrown ',
'path': u'%s ',
'pathAll': u'%s for all elements ',
'pathAny': u'%s for any element ',
'status': u'%s response is received '
}

def __init__(self, waiter_config):
self._waiter_config = waiter_config

def build_waiter_state_description(self):
description = self._waiter_config.description
# Use the description provided in the waiter config file. If no
# description is provided, use a heuristic to generate a description
# for the waiter.
if not description:
description = u'Wait until '
# Look at all of the acceptors and find the success state
# acceptor.
for acceptor in self._waiter_config.acceptors:
# Build the description off of the success acceptor.
if acceptor.state == 'success':
description += self._build_success_description(acceptor)
break
# Include what operation is being used.
description += self._build_operation_description(
self._waiter_config.operation)
return description

def _build_success_description(self, acceptor):
matcher = acceptor.matcher
# Pick the description template to use based on what the matcher is.
success_description = self.SUCCESS_DESCRIPTIONS[matcher]
resource_description = None
# If success is based off of the state of a resource include the
# description about what resource is looked at.
if matcher in ['path', 'pathAny', 'pathAll']:
resource_description = u'JMESPath query %s returns ' % \
acceptor.argument
# Prepend the resource description to the template description
success_description = resource_description + success_description
# Complete the description by filling in the expected success state.
full_success_description = success_description % acceptor.expected
return full_success_description

def _build_operation_description(self, operation):
operation_name = xform_name(operation).replace('_', '-')
return u'when polling with ``%s``.' % operation_name


class WaiterCaller(object):
def __init__(self, client, waiter):
self._client = client
self._waiter = waiter

def invoke(self, operation_object, parameters, parsed_globals):
# Create the endpoint based on the parsed globals
endpoint = operation_object.service.get_endpoint(
region_name=parsed_globals.region,
endpoint_url=parsed_globals.endpoint_url,
verify=parsed_globals.verify_ssl)
# Change the client's endpoint using the newly configured endpoint
self._client._endpoint = endpoint
# Call the waiter's wait method.
self._waiter.wait(**parameters)
return 0


class WaiterStateCommand(ServiceOperation):
DESCRIPTION = ''

def create_help_command(self):
help_command = super(WaiterStateCommand, self).create_help_command()
# Change the operation object's description by changing it to the
# description for a waiter state command.
self._operation_object.documentation = self.DESCRIPTION
# Change the output shape because waiters provide no output.
self._operation_object.model.output_shape = None
return help_command


class WaiterCommandDocHandler(BasicDocHandler):
def doc_synopsis_start(self, help_command, **kwargs):
pass

def doc_synopsis_option(self, arg_name, help_command, **kwargs):
pass

def doc_synopsis_end(self, help_command, **kwargs):
pass

def doc_options_start(self, help_command, **kwargs):
pass

def doc_option(self, arg_name, help_command, **kwargs):
pass
2 changes: 2 additions & 0 deletions awscli/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
from awscli.customizations.generatecliskeleton import \
register_generate_cli_skeleton
from awscli.customizations.assumerole import register_assume_role_provider
from awscli.customizations.waiters import register_add_waiters


def awscli_initialize(event_handlers):
Expand Down Expand Up @@ -107,3 +108,4 @@ def awscli_initialize(event_handlers):
register_s3_endpoint(event_handlers)
register_generate_cli_skeleton(event_handlers)
register_assume_role_provider(event_handlers)
register_add_waiters(event_handlers)
17 changes: 14 additions & 3 deletions doc/source/htmlgen
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@ def do_operation(driver, service_path, operation_name, operation_command):
help_command(None, None)


def do_service(driver, ref_path, service_name, service_command):
print('...%s' % service_name)
def do_service(driver, ref_path, service_name, service_command,
is_top_level_service=True):
if is_top_level_service:
print('...%s' % service_name)
service_path = os.path.join(ref_path, service_name)
if not os.path.isdir(service_path):
os.mkdir(service_path)
Expand All @@ -50,7 +52,16 @@ def do_service(driver, ref_path, service_name, service_command):
if operation_name == 'help':
continue
operation_command = help_command.command_table[operation_name]
do_operation(driver, service_path, operation_name, operation_command)
subcommand_table = getattr(operation_command, 'subcommand_table', {})
# If the operation command has a subcommand table with commands
# in it, treat it as a service command as opposed to an operation
# command.
if (len(subcommand_table) > 0):
do_service(driver, service_path, operation_name,
operation_command, False)
else:
do_operation(driver, service_path, operation_name,
operation_command)


def do_provider(driver):
Expand Down
44 changes: 44 additions & 0 deletions tests/integration/customizations/test_waiters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# 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 botocore.session
import random

from awscli.testutils import unittest, aws


class TestDynamoDBWait(unittest.TestCase):
def setUp(self):
self.session = botocore.session.get_session()
self.client = self.session.create_client('dynamodb', 'us-west-2')

def test_wait_table_exists(self):
# Create a table.
table_name = 'awscliddb-%s' % random.randint(1, 10000)
self.client.create_table(
TableName=table_name,
ProvisionedThroughput={"ReadCapacityUnits": 5,
"WriteCapacityUnits": 5},
KeySchema=[{"AttributeName": "foo", "KeyType": "HASH"}],
AttributeDefinitions=[{"AttributeName": "foo",
"AttributeType": "S"}])
self.addCleanup(self.client.delete_table, TableName=table_name)

# Wait for the table to be active.
p = aws(
'dynamodb wait table-exists --table-name %s --region us-west-2' %
table_name)
self.assertEqual(p.rc, 0)

# Make sure the table is active.
parsed = self.client.describe_table(TableName=table_name)
self.assertEqual(parsed['Table']['TableStatus'], 'ACTIVE')
Loading

0 comments on commit e5c3639

Please sign in to comment.