From 7c199cad252468c90a512bf735336285e893a200 Mon Sep 17 00:00:00 2001 From: Alan Loi Date: Sat, 16 May 2015 21:53:27 +1000 Subject: [PATCH 1/5] Add dynamodb_table module --- cloud/amazon/dynamodb_table | 261 ++++++++++++++++++++++++++++++++++++ 1 file changed, 261 insertions(+) create mode 100644 cloud/amazon/dynamodb_table diff --git a/cloud/amazon/dynamodb_table b/cloud/amazon/dynamodb_table new file mode 100644 index 00000000000..7a200a3b271 --- /dev/null +++ b/cloud/amazon/dynamodb_table @@ -0,0 +1,261 @@ +#!/usr/bin/python +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = """ +--- +module: dynamodb_table +short_description: Create, update or delete AWS Dynamo DB tables. +description: + - Create or delete AWS Dynamo DB tables. + - Can update the provisioned throughput on existing tables. + - Returns the status of the specified table. +author: Alan Loi (@loia) +requirements: + - "boto >= 2.13.2" +options: + state: + description: + - Create or delete the table + required: false + choices: ['present', 'absent'] + default: 'present' + name: + description: + - Name of the table. + required: true + hash_key_name: + description: + - Name of the hash key. + - Required when state=present. + required: false + hash_key_type: + description: + - Type of the hash key. + required: false + choices: ['STRING', 'NUMBER', 'BINARY'] + default: 'STRING' + range_key_name: + description: + - Name of the range key. + required: false + range_key_type: + description: + - Type of the range key. + required: false + choices: ['STRING', 'NUMBER', 'BINARY'] + default: 'STRING' + read_capacity: + description: + - Read throughput capacity (units) to provision. + required: false + default: 1 + write_capacity: + description: + - Write throughput capacity (units) to provision. + required: false + default: 1 + region: + description: + - The AWS region to use. If not specified then the value of the EC2_REGION environment variable, if any, is used. + required: false + aliases: ['aws_region', 'ec2_region'] + +extends_documentation_fragment: aws +""" + +EXAMPLES = ''' +# Create dynamo table with hash and range primary key +- dynamodb_table: + name: my-table + region: us-east-1 + hash_key_name: id + hash_key_type: STRING + range_key_name: create_time + range_key_type: NUMBER + read_capacity: 2 + write_capacity: 2 + +# Update capacity on existing dynamo table +- dynamodb_table: + name: my-table + region: us-east-1 + read_capacity: 10 + write_capacity: 10 + +# Delete dynamo table +- dynamodb_table: + name: my-table + region: us-east-1 + state: absent +''' + +try: + import boto + import boto.dynamodb2 + from boto.dynamodb2.table import Table + from boto.dynamodb2.fields import HashKey, RangeKey + from boto.dynamodb2.types import STRING, NUMBER, BINARY + from boto.exception import BotoServerError, JSONResponseError + +except ImportError: + print "failed=True msg='boto required for this module'" + sys.exit(1) + + +DYNAMO_TYPE_MAP = { + 'STRING': STRING, + 'NUMBER': NUMBER, + 'BINARY': BINARY +} + + +def create_or_update_dynamo_table(connection, module): + table_name = module.params.get('name') + hash_key_name = module.params.get('hash_key_name') + hash_key_type = module.params.get('hash_key_type') + range_key_name = module.params.get('range_key_name') + range_key_type = module.params.get('range_key_type') + read_capacity = module.params.get('read_capacity') + write_capacity = module.params.get('write_capacity') + + schema = [ + HashKey(hash_key_name, map_dynamo_type(hash_key_type)), + RangeKey(range_key_name, map_dynamo_type(range_key_type)) + ] + throughput = { + 'read': read_capacity, + 'write': write_capacity + } + + result = dict( + region=module.params.get('region'), + table_name=table_name, + hash_key_name=hash_key_name, + hash_key_type=hash_key_type, + range_key_name=range_key_name, + range_key_type=range_key_type, + read_capacity=read_capacity, + write_capacity=write_capacity, + ) + + try: + table = Table(table_name, connection=connection) + + if dynamo_table_exists(table): + changed = update_dynamo_table(table, throughput=throughput) + else: + Table.create(table_name, connection=connection, schema=schema, throughput=throughput) + changed = True + + result['table_status'] = table.describe()['Table']['TableStatus'] + result['changed'] = changed + + except BotoServerError: + result['msg'] = 'Failed to create/update dynamo table due to error: ' + traceback.format_exc() + module.fail_json(**result) + else: + module.exit_json(**result) + + +def delete_dynamo_table(connection, module): + table_name = module.params.get('table_name') + + result = dict( + region=module.params.get('region'), + table_name=table_name, + ) + + try: + changed = False + table = Table(table_name, connection=connection) + + if dynamo_table_exists(table): + table.delete() + changed = True + + result['changed'] = changed + + except BotoServerError: + result['msg'] = 'Failed to delete dynamo table due to error: ' + traceback.format_exc() + module.fail_json(**result) + else: + module.exit_json(**result) + + +def dynamo_table_exists(table): + try: + table.describe() + return True + + except JSONResponseError, e: + if e.message and e.message.startswith('Requested resource not found'): + return False + else: + raise e + + +def update_dynamo_table(table, throughput=None): + table.describe() # populate table details + + # AWS complains if the throughput hasn't changed + if has_throughput_changed(table, throughput): + return table.update(throughput=throughput) + + return False + + +def has_throughput_changed(table, new_throughput): + if not new_throughput: + return False + + return new_throughput['read'] != table.throughput['read'] or \ + new_throughput['write'] != table.throughput['write'] + + +def map_dynamo_type(dynamo_type): + return DYNAMO_TYPE_MAP.get(dynamo_type) + + +def main(): + argument_spec = ec2_argument_spec() + argument_spec.update(dict( + state=dict(default='present', choices=['present', 'absent']), + name=dict(required=True, type='str'), + hash_key_name=dict(required=True, type='str'), + hash_key_type=dict(default='STRING', type='str', choices=['STRING', 'NUMBER', 'BINARY']), + range_key_name=dict(type='str'), + range_key_type=dict(default='STRING', type='str', choices=['STRING', 'NUMBER', 'BINARY']), + read_capacity=dict(default=1, type='int'), + write_capacity=dict(default=1, type='int'), + )) + + module = AnsibleModule(argument_spec=argument_spec) + + region, ec2_url, aws_connect_params = get_aws_connection_info(module) + connection = boto.dynamodb2.connect_to_region(region) + + state = module.params.get('state') + if state == 'present': + create_or_update_dynamo_table(connection, module) + elif state == 'absent': + delete_dynamo_table(connection, module) + + +# import module snippets +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * + +main() From 268104fca321a777e279ed20d252e43da23a2b9a Mon Sep 17 00:00:00 2001 From: Alan Loi Date: Sat, 20 Jun 2015 21:24:36 +1000 Subject: [PATCH 2/5] Added check_mode support to dynamodb_table module. --- cloud/amazon/dynamodb_table | 51 ++++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/cloud/amazon/dynamodb_table b/cloud/amazon/dynamodb_table index 7a200a3b271..b59280a2e23 100644 --- a/cloud/amazon/dynamodb_table +++ b/cloud/amazon/dynamodb_table @@ -39,7 +39,7 @@ options: hash_key_name: description: - Name of the hash key. - - Required when state=present. + - Required when C(state=present). required: false hash_key_type: description: @@ -109,10 +109,10 @@ try: from boto.dynamodb2.fields import HashKey, RangeKey from boto.dynamodb2.types import STRING, NUMBER, BINARY from boto.exception import BotoServerError, JSONResponseError + HAS_BOTO = True except ImportError: - print "failed=True msg='boto required for this module'" - sys.exit(1) + HAS_BOTO = False DYNAMO_TYPE_MAP = { @@ -132,8 +132,8 @@ def create_or_update_dynamo_table(connection, module): write_capacity = module.params.get('write_capacity') schema = [ - HashKey(hash_key_name, map_dynamo_type(hash_key_type)), - RangeKey(range_key_name, map_dynamo_type(range_key_type)) + HashKey(hash_key_name, DYNAMO_TYPE_MAP.get(hash_key_type)), + RangeKey(range_key_name, DYNAMO_TYPE_MAP.get(range_key_type)) ] throughput = { 'read': read_capacity, @@ -155,13 +155,14 @@ def create_or_update_dynamo_table(connection, module): table = Table(table_name, connection=connection) if dynamo_table_exists(table): - changed = update_dynamo_table(table, throughput=throughput) + result['changed'] = update_dynamo_table(table, throughput=throughput, check_mode=module.check_mode) else: - Table.create(table_name, connection=connection, schema=schema, throughput=throughput) - changed = True + if not module.check_mode: + Table.create(table_name, connection=connection, schema=schema, throughput=throughput) + result['changed'] = True - result['table_status'] = table.describe()['Table']['TableStatus'] - result['changed'] = changed + if not module.check_mode: + result['table_status'] = table.describe()['Table']['TableStatus'] except BotoServerError: result['msg'] = 'Failed to create/update dynamo table due to error: ' + traceback.format_exc() @@ -171,7 +172,7 @@ def create_or_update_dynamo_table(connection, module): def delete_dynamo_table(connection, module): - table_name = module.params.get('table_name') + table_name = module.params.get('name') result = dict( region=module.params.get('region'), @@ -179,14 +180,15 @@ def delete_dynamo_table(connection, module): ) try: - changed = False table = Table(table_name, connection=connection) if dynamo_table_exists(table): - table.delete() - changed = True + if not module.check_mode: + table.delete() + result['changed'] = True - result['changed'] = changed + else: + result['changed'] = False except BotoServerError: result['msg'] = 'Failed to delete dynamo table due to error: ' + traceback.format_exc() @@ -207,12 +209,14 @@ def dynamo_table_exists(table): raise e -def update_dynamo_table(table, throughput=None): +def update_dynamo_table(table, throughput=None, check_mode=False): table.describe() # populate table details - # AWS complains if the throughput hasn't changed if has_throughput_changed(table, throughput): - return table.update(throughput=throughput) + if not check_mode: + return table.update(throughput=throughput) + else: + return True return False @@ -225,10 +229,6 @@ def has_throughput_changed(table, new_throughput): new_throughput['write'] != table.throughput['write'] -def map_dynamo_type(dynamo_type): - return DYNAMO_TYPE_MAP.get(dynamo_type) - - def main(): argument_spec = ec2_argument_spec() argument_spec.update(dict( @@ -242,7 +242,12 @@ def main(): write_capacity=dict(default=1, type='int'), )) - module = AnsibleModule(argument_spec=argument_spec) + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True) + + if not HAS_BOTO: + module.fail_json(msg='boto required for this module') region, ec2_url, aws_connect_params = get_aws_connection_info(module) connection = boto.dynamodb2.connect_to_region(region) From 011fef5f3275b5a1cf55a9c578c61d2dde0d3f99 Mon Sep 17 00:00:00 2001 From: Alan Loi Date: Sat, 20 Jun 2015 21:34:27 +1000 Subject: [PATCH 3/5] Added return value documentation to dynamodb_table module. --- cloud/amazon/dynamodb_table | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/cloud/amazon/dynamodb_table b/cloud/amazon/dynamodb_table index b59280a2e23..89a7e0fbb2e 100644 --- a/cloud/amazon/dynamodb_table +++ b/cloud/amazon/dynamodb_table @@ -102,6 +102,14 @@ EXAMPLES = ''' state: absent ''' +RETURN = ''' +table_status: + description: The current status of the table. + returned: success + type: string + sample: ACTIVE +''' + try: import boto import boto.dynamodb2 From ac09e609146c3f8c8ef46dc22ab75834aa5d20dc Mon Sep 17 00:00:00 2001 From: Alan Loi Date: Sun, 21 Jun 2015 08:40:57 +1000 Subject: [PATCH 4/5] Add .py file extension to dynamodb_table module. --- cloud/amazon/{dynamodb_table => dynamodb_table.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename cloud/amazon/{dynamodb_table => dynamodb_table.py} (100%) diff --git a/cloud/amazon/dynamodb_table b/cloud/amazon/dynamodb_table.py similarity index 100% rename from cloud/amazon/dynamodb_table rename to cloud/amazon/dynamodb_table.py From 1a914128f6d172da7ea349d6b070758e1ebbff9c Mon Sep 17 00:00:00 2001 From: Alan Loi Date: Mon, 22 Jun 2015 20:23:11 +1000 Subject: [PATCH 5/5] Fix aws connection to use params. --- cloud/amazon/dynamodb_table.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/amazon/dynamodb_table.py b/cloud/amazon/dynamodb_table.py index 89a7e0fbb2e..130fae44721 100644 --- a/cloud/amazon/dynamodb_table.py +++ b/cloud/amazon/dynamodb_table.py @@ -258,7 +258,7 @@ def main(): module.fail_json(msg='boto required for this module') region, ec2_url, aws_connect_params = get_aws_connection_info(module) - connection = boto.dynamodb2.connect_to_region(region) + connection = connect_to_aws(boto.dynamodb2, region, **aws_connect_params) state = module.params.get('state') if state == 'present':