From 892fc411900fd6e26ba3b2ebabec1c497d12cd47 Mon Sep 17 00:00:00 2001 From: Daniel Shepherd Date: Fri, 26 Jan 2018 09:22:30 -0500 Subject: [PATCH] [cloud] New module: Amazon Egress-Only Internet Gateway (ec2_vpc_egress_igw) (#23941) * New module: ec2_vpc_egress_igw * minor pep8 fix and doc update * add test dir and files * add tests for gateway module * fix up return documentation per review * remove HAS_BOTO3 stuff as it is handled in AnsibleAWSModule per review * fix an error with failure message and add custom handler for non-existent vpc ID * add additional tests and update tests per review * ignore errors on clean up tasks * update module copyright to newer format * fix exception handling since BotoCoreError doesnt have response attribute * actually fix exception handling this time so it works with Py3 as well --- .../cloud/amazon/ec2_vpc_egress_igw.py | 193 ++++++++++++++++++ .../targets/ec2_vpc_egress_igw/aliases | 2 + .../targets/ec2_vpc_egress_igw/tasks/main.yml | 112 ++++++++++ 3 files changed, 307 insertions(+) create mode 100644 lib/ansible/modules/cloud/amazon/ec2_vpc_egress_igw.py create mode 100644 test/integration/targets/ec2_vpc_egress_igw/aliases create mode 100644 test/integration/targets/ec2_vpc_egress_igw/tasks/main.yml diff --git a/lib/ansible/modules/cloud/amazon/ec2_vpc_egress_igw.py b/lib/ansible/modules/cloud/amazon/ec2_vpc_egress_igw.py new file mode 100644 index 00000000000..61dcf42d845 --- /dev/null +++ b/lib/ansible/modules/cloud/amazon/ec2_vpc_egress_igw.py @@ -0,0 +1,193 @@ +#!/usr/bin/python +# Copyright (c) 2017 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = ''' +--- +module: ec2_vpc_egress_igw +short_description: Manage an AWS VPC Egress Only Internet gateway +description: + - Manage an AWS VPC Egress Only Internet gateway +version_added: "2.5" +author: Daniel Shepherd (@shepdelacreme) +options: + vpc_id: + description: + - The VPC ID for the VPC that this Egress Only Internet Gateway should be attached. + required: true + state: + description: + - Create or delete the EIGW + default: present + choices: [ 'present', 'absent' ] +extends_documentation_fragment: + - aws + - ec2 +''' + +EXAMPLES = ''' +# Note: These examples do not set authentication details, see the AWS Guide for details. + +# Ensure that the VPC has an Internet Gateway. +# The Internet Gateway ID is can be accessed via {{eigw.gateway_id}} for use in setting up NATs etc. +ec2_vpc_egress_igw: + vpc_id: vpc-abcdefgh + state: present +register: eigw + +''' + +RETURN = ''' +gateway_id: + description: The ID of the Egress Only Internet Gateway or Null. + returned: always + type: string + sample: eigw-0e00cf111ba5bc11e +vpc_id: + description: The ID of the VPC to attach or detach gateway from. + returned: always + type: string + sample: vpc-012345678 +''' + + +from ansible.module_utils.aws.core import AnsibleAWSModule +from ansible.module_utils.ec2 import ( + boto3_conn, + ec2_argument_spec, + get_aws_connection_info, + camel_dict_to_snake_dict +) + +try: + import botocore +except ImportError: + pass # will be picked up by HAS_BOTO3 in AnsibleAWSModule + + +def delete_eigw(module, conn, eigw_id): + """ + Delete EIGW. + + module : AnsibleModule object + conn : boto3 client connection object + eigw_id : ID of the EIGW to delete + """ + changed = False + + try: + response = conn.delete_egress_only_internet_gateway(DryRun=module.check_mode, EgressOnlyInternetGatewayId=eigw_id) + except botocore.exceptions.ClientError as e: + # When boto3 method is run with DryRun=True it returns an error on success + # We need to catch the error and return something valid + if e.response.get('Error', {}).get('Code') == "DryRunOperation": + changed = True + else: + module.fail_json_aws(e, msg="Could not delete Egress-Only Internet Gateway {0} from VPC {1}".format(eigw_id, module.vpc_id)) + except botocore.exceptions.BotoCoreError as e: + module.fail_json_aws(e, msg="Could not delete Egress-Only Internet Gateway {0} from VPC {1}".format(eigw_id, module.vpc_id)) + + if not module.check_mode: + changed = response.get('ReturnCode', False) + + return changed + + +def create_eigw(module, conn, vpc_id): + """ + Create EIGW. + + module : AnsibleModule object + conn : boto3 client connection object + vpc_id : ID of the VPC we are operating on + """ + gateway_id = None + changed = False + + try: + response = conn.create_egress_only_internet_gateway(DryRun=module.check_mode, VpcId=vpc_id) + except botocore.exceptions.ClientError as e: + # When boto3 method is run with DryRun=True it returns an error on success + # We need to catch the error and return something valid + if e.response.get('Error', {}).get('Code') == "DryRunOperation": + changed = True + elif e.response.get('Error', {}).get('Code') == "InvalidVpcID.NotFound": + module.fail_json_aws(e, msg="invalid vpc ID '{0}' provided".format(vpc_id)) + else: + module.fail_json_aws(e, msg="Could not create Egress-Only Internet Gateway for vpc ID {0}".format(vpc_id)) + except botocore.exceptions.BotoCoreError as e: + module.fail_json_aws(e, msg="Could not create Egress-Only Internet Gateway for vpc ID {0}".format(vpc_id)) + + if not module.check_mode: + gateway = response.get('EgressOnlyInternetGateway', {}) + state = gateway.get('Attachments', [{}])[0].get('State') + gateway_id = gateway.get('EgressOnlyInternetGatewayId') + + if gateway_id and state in ('attached', 'attaching'): + changed = True + else: + # EIGW gave back a bad attachment state or an invalid response so we error out + module.fail_json(msg='Unable to create and attach Egress Only Internet Gateway to VPCId: {0}. Bad or no state in response'.format(vpc_id), + **camel_dict_to_snake_dict(response)) + + return changed, gateway_id + + +def describe_eigws(module, conn, vpc_id): + """ + Describe EIGWs. + + module : AnsibleModule object + conn : boto3 client connection object + vpc_id : ID of the VPC we are operating on + """ + gateway_id = None + + try: + response = conn.describe_egress_only_internet_gateways() + except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: + module.fail_json_aws(e, msg="Could not get list of existing Egress-Only Internet Gateways") + + for eigw in response.get('EgressOnlyInternetGateways', []): + for attachment in eigw.get('Attachments', []): + if attachment.get('VpcId') == vpc_id and attachment.get('State') in ('attached', 'attaching'): + gateway_id = eigw.get('EgressOnlyInternetGatewayId') + + return gateway_id + + +def main(): + argument_spec = ec2_argument_spec() + argument_spec.update(dict( + vpc_id=dict(required=True), + state=dict(default='present', choices=['present', 'absent']) + )) + + module = AnsibleAWSModule(argument_spec=argument_spec, supports_check_mode=True) + + region, ec2_url, aws_connect_params = get_aws_connection_info(module, boto3=True) + connection = boto3_conn(module, conn_type='client', resource='ec2', region=region, endpoint=ec2_url, **aws_connect_params) + + vpc_id = module.params.get('vpc_id') + state = module.params.get('state') + + eigw_id = describe_eigws(module, connection, vpc_id) + + result = dict(gateway_id=eigw_id, vpc_id=vpc_id) + changed = False + + if state == 'present' and not eigw_id: + changed, result['gateway_id'] = create_eigw(module, connection, vpc_id) + elif state == 'absent' and eigw_id: + changed = delete_eigw(module, connection, eigw_id) + + module.exit_json(changed=changed, **result) + + +if __name__ == '__main__': + main() diff --git a/test/integration/targets/ec2_vpc_egress_igw/aliases b/test/integration/targets/ec2_vpc_egress_igw/aliases new file mode 100644 index 00000000000..e4e6bccbd6e --- /dev/null +++ b/test/integration/targets/ec2_vpc_egress_igw/aliases @@ -0,0 +1,2 @@ +cloud/aws +posix/ci/cloud/group4/aws \ No newline at end of file diff --git a/test/integration/targets/ec2_vpc_egress_igw/tasks/main.yml b/test/integration/targets/ec2_vpc_egress_igw/tasks/main.yml new file mode 100644 index 00000000000..ff47baf331c --- /dev/null +++ b/test/integration/targets/ec2_vpc_egress_igw/tasks/main.yml @@ -0,0 +1,112 @@ +--- +- block: + + # ============================================================ + - name: test failure with no parameters + ec2_vpc_egress_igw: + register: result + ignore_errors: true + + - name: assert failure with no parameters + assert: + that: + - 'result.failed' + - 'result.msg == "missing required arguments: vpc_id"' + + # ============================================================ + - name: set up aws connection info + set_fact: + aws_connection_info: &aws_connection_info + aws_access_key: "{{ aws_access_key }}" + aws_secret_key: "{{ aws_secret_key }}" + security_token: "{{ security_token }}" + region: "{{ aws_region }}" + no_log: yes + + # ============================================================ + - name: test failure with non-existent VPC ID + ec2_vpc_egress_igw: + state: present + vpc_id: vpc-012345678 + <<: *aws_connection_info + register: result + ignore_errors: true + + - name: assert failure with non-existent VPC ID + assert: + that: + - 'result.failed' + - 'result.error.code == "InvalidVpcID.NotFound"' + - '"invalid vpc ID" in result.msg' + + # ============================================================ + - name: create a VPC + ec2_vpc_net: + name: "{{ resource_prefix }}-vpc" + state: present + cidr_block: "10.232.232.128/26" + <<: *aws_connection_info + tags: + Name: "{{ resource_prefix }}-vpc" + Description: "Created by ansible-test" + register: vpc_result + + # ============================================================ + - name: create egress-only internet gateway (expected changed=true) + ec2_vpc_egress_igw: + state: present + vpc_id: "{{ vpc_result.vpc.id }}" + <<: *aws_connection_info + register: vpc_eigw_create + + - name: assert creation happened (expected changed=true) + assert: + that: + - 'vpc_eigw_create' + - 'vpc_eigw_create.gateway_id.startswith("eigw-")' + - 'vpc_eigw_create.vpc_id == vpc_result.vpc.id' + + # ============================================================ + - name: attempt to recreate egress-only internet gateway on VPC (expected changed=false) + ec2_vpc_egress_igw: + state: present + vpc_id: "{{ vpc_result.vpc.id }}" + <<: *aws_connection_info + register: vpc_eigw_recreate + + - name: assert recreation did nothing (expected changed=false) + assert: + that: + - 'vpc_eigw_recreate.changed == False' + - 'vpc_eigw_recreate.gateway_id == vpc_eigw_create.gateway_id' + - 'vpc_eigw_recreate.vpc_id == vpc_eigw_create.vpc_id' + + # ============================================================ + - name: test state=absent (expected changed=true) + ec2_vpc_egress_igw: + state: absent + vpc_id: "{{ vpc_result.vpc.id }}" + <<: *aws_connection_info + register: vpc_eigw_delete + + - name: assert state=absent (expected changed=true) + assert: + that: + - 'vpc_eigw_delete.changed' + + always: + # ============================================================ + - name: tidy up EIGW + ec2_vpc_egress_igw: + state: absent + vpc_id: "{{ vpc_result.vpc.id }}" + <<: *aws_connection_info + ignore_errors: true + + - name: tidy up VPC + ec2_vpc_net: + name: "{{ resource_prefix }}-vpc" + state: absent + cidr_block: "10.232.232.128/26" + <<: *aws_connection_info + ignore_errors: true