From 7733c3f2bef303ba59e89107848c03d310e3c901 Mon Sep 17 00:00:00 2001 From: Robert Estelle Date: Mon, 1 Dec 2014 14:43:58 -0500 Subject: [PATCH] ec2_vpc_subnet - resource_tags is not required. --- .../extras/cloud/amazon/ec2_vpc_subnet.py | 292 ++++++++++++++++++ 1 file changed, 292 insertions(+) create mode 100644 lib/ansible/modules/extras/cloud/amazon/ec2_vpc_subnet.py diff --git a/lib/ansible/modules/extras/cloud/amazon/ec2_vpc_subnet.py b/lib/ansible/modules/extras/cloud/amazon/ec2_vpc_subnet.py new file mode 100644 index 00000000000..5c7fc8596a2 --- /dev/null +++ b/lib/ansible/modules/extras/cloud/amazon/ec2_vpc_subnet.py @@ -0,0 +1,292 @@ +#!/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: ec2_vpc_subnet +short_description: Configure subnets in AWS virtual private clouds. +description: + - Create or removes AWS subnets in a VPC. This module has a''' +''' dependency on python-boto. +version_added: "1.8" +options: + vpc_id: + description: + - A VPC id in which the subnet resides + required: false + default: null + aliases: [] + resource_tags: + description: + - 'A dictionary array of resource tags of the form: { tag1: value1,''' +''' tag2: value2 }. This module identifies a subnet by CIDR and will update''' +''' the subnet's tags to match. Tags not in this list will be ignored. + required: false + default: null + aliases: [] + cidr: + description: + - "The cidr block for the subnet, e.g. 10.0.0.0/16" + required: false, unless state=present + az: + description: + - "The availability zone for the subnet" + required: false, unless state=present + region: + description: + - region in which the resource exists. + required: false + default: null + aliases: ['aws_region', 'ec2_region'] + state: + description: + - Create or remove the subnet + required: true + default: present + aliases: [] + aws_secret_key: + description: + - AWS secret key. If not set then the value of the AWS_SECRET_KEY''' +''' environment variable is used. + required: false + default: None + aliases: ['ec2_secret_key', 'secret_key' ] + aws_access_key: + description: + - AWS access key. If not set then the value of the AWS_ACCESS_KEY''' +''' environment variable is used. + required: false + default: None + aliases: ['ec2_access_key', 'access_key' ] + validate_certs: + description: + - When set to "no", SSL certificates will not be validated for''' +''' boto versions >= 2.6.0. + required: false + default: "yes" + choices: ["yes", "no"] + aliases: [] + +requirements: ["boto"] +author: Robert Estelle +''' + +EXAMPLES = ''' +# Note: None of these examples set aws_access_key, aws_secret_key, or region. +# It is assumed that their matching environment variables are set. + +# Basic creation example: +- name: Set up the subnet for database servers + local_action: + module: ec2_vpc_subnet + state: present + vpc_id: vpc-123456 + region: us-west-1 + cidr: 10.0.1.16/28 + resource_tags: + Name: Database Subnet + register: database_subnet + +# Removal of a VPC by id +- name: Set up the subnet for database servers + local_action: + module: ec2_vpc + state: absent + vpc_id: vpc-123456 + region: us-west-1 + cidr: 10.0.1.16/28 +''' + + +import sys # noqa +import time + +try: + import boto.ec2 + import boto.vpc + from boto.exception import EC2ResponseError + HAS_BOTO = True +except ImportError: + HAS_BOTO = False + if __name__ != '__main__': + raise + + +class AnsibleVPCSubnetException(Exception): + pass + + +class AnsibleVPCSubnetCreationException(AnsibleVPCSubnetException): + pass + + +class AnsibleVPCSubnetDeletionException(AnsibleVPCSubnetException): + pass + + +class AnsibleTagCreationException(AnsibleVPCSubnetException): + pass + + +def subnet_exists(vpc_conn, subnet_id): + filters = {'subnet-id': subnet_id} + return len(vpc_conn.get_all_subnets(filters=filters)) > 0 + + +def create_subnet(vpc_conn, vpc_id, cidr, az): + try: + new_subnet = vpc_conn.create_subnet(vpc_id, cidr, az) + # Sometimes AWS takes its time to create a subnet and so using + # new subnets's id to do things like create tags results in + # exception. boto doesn't seem to refresh 'state' of the newly + # created subnet, i.e.: it's always 'pending'. + while not subnet_exists(vpc_conn, new_subnet.id): + time.sleep(0.1) + except EC2ResponseError as e: + raise AnsibleVPCSubnetCreationException( + 'Unable to create subnet {0}, error: {1}'.format(cidr, e)) + return new_subnet + + +def get_resource_tags(vpc_conn, resource_id): + return {t.name: t.value for t in + vpc_conn.get_all_tags(filters={'resource-id': resource_id})} + + +def ensure_tags(vpc_conn, resource_id, tags, add_only, dry_run): + try: + cur_tags = get_resource_tags(vpc_conn, resource_id) + if cur_tags == tags: + return {'changed': False, 'tags': cur_tags} + + to_delete = {k: cur_tags[k] for k in cur_tags if k not in tags} + if to_delete and not add_only: + vpc_conn.delete_tags(resource_id, to_delete, dry_run=dry_run) + + to_add = {k: tags[k] for k in tags if k not in cur_tags} + if to_add: + vpc_conn.create_tags(resource_id, to_add, dry_run=dry_run) + + latest_tags = get_resource_tags(vpc_conn, resource_id) + return {'changed': True, 'tags': latest_tags} + except EC2ResponseError as e: + raise AnsibleTagCreationException( + 'Unable to update tags for {0}, error: {1}'.format(resource_id, e)) + + +def get_matching_subnet(vpc_conn, vpc_id, cidr): + subnets = vpc_conn.get_all_subnets(filters={'vpc_id': vpc_id}) + return next((s for s in subnets if s.cidr_block == cidr), None) + + +def ensure_subnet_present(vpc_conn, vpc_id, cidr, az, tags, check_mode): + subnet = get_matching_subnet(vpc_conn, vpc_id, cidr) + changed = False + if subnet is None: + if check_mode: + return {'changed': True, 'subnet_id': None, 'subnet': {}} + + subnet = create_subnet(vpc_conn, vpc_id, cidr, az) + changed = True + + if tags is not None: + tag_result = ensure_tags(vpc_conn, subnet.id, tags, add_only=True, + dry_run=check_mode) + tags = tag_result['tags'] + changed = changed or tag_result['changed'] + else: + tags = get_resource_tags(vpc_conn, subnet.id) + + return { + 'changed': changed, + 'subnet_id': subnet.id, + 'subnet': { + 'tags': tags, + 'cidr': subnet.cidr_block, + 'az': subnet.availability_zone, + 'id': subnet.id, + } + } + + +def ensure_subnet_absent(vpc_conn, vpc_id, cidr, check_mode): + subnet = get_matching_subnet(vpc_conn, vpc_id, cidr) + if subnet is None: + return {'changed': False} + elif check_mode: + return {'changed': True} + + try: + vpc_conn.delete_subnet(subnet.id) + return {'changed': True} + except EC2ResponseError as e: + raise AnsibleVPCSubnetDeletionException( + 'Unable to delete subnet {0}, error: {1}' + .format(subnet.cidr_block, e)) + + +def main(): + argument_spec = ec2_argument_spec() + argument_spec.update({ + 'vpc_id': {'required': True}, + 'resource_tags': {'type': 'dict', 'required': False}, + 'cidr': {'required': True}, + 'az': {}, + 'state': {'choices': ['present', 'absent'], 'default': 'present'}, + }) + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + if not HAS_BOTO: + module.fail_json(msg='boto is required for this module') + + ec2_url, aws_access_key, aws_secret_key, region = get_ec2_creds(module) + if not region: + module.fail_json(msg='Region must be specified') + + try: + vpc_conn = boto.vpc.connect_to_region( + region, + aws_access_key_id=aws_access_key, + aws_secret_access_key=aws_secret_key + ) + except boto.exception.NoAuthHandlerFound as e: + module.fail_json(msg=str(e)) + + vpc_id = module.params.get('vpc_id') + tags = module.params.get('resource_tags') + cidr = module.params.get('cidr') + az = module.params.get('az') + state = module.params.get('state', 'present') + + try: + if state == 'present': + result = ensure_subnet_present(vpc_conn, vpc_id, cidr, az, tags, + check_mode=module.check_mode) + elif state == 'absent': + result = ensure_subnet_absent(vpc_conn, vpc_id, cidr, + check_mode=module.check_mode) + except AnsibleVPCSubnetException as e: + module.fail_json(msg=str(e)) + + module.exit_json(**result) + +from ansible.module_utils.basic import * # noqa +from ansible.module_utils.ec2 import * # noqa + +if __name__ == '__main__': + main()