1#!/usr/local/bin/python3.8
2# Copyright: Ansible Project
3# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
4
5from __future__ import absolute_import, division, print_function
6__metaclass__ = type
7
8
9ANSIBLE_METADATA = {'metadata_version': '1.1',
10                    'status': ['stableinterface'],
11                    'supported_by': 'core'}
12
13
14DOCUMENTATION = '''
15---
16module: ec2_vpc_net
17short_description: Configure AWS virtual private clouds
18description:
19    - Create, modify, and terminate AWS virtual private clouds.
20version_added: "2.0"
21author:
22  - Jonathan Davila (@defionscode)
23  - Sloane Hertel (@s-hertel)
24options:
25  name:
26    description:
27      - The name to give your VPC. This is used in combination with C(cidr_block) to determine if a VPC already exists.
28    required: yes
29    type: str
30  cidr_block:
31    description:
32      - The primary CIDR of the VPC. After 2.5 a list of CIDRs can be provided. The first in the list will be used as the primary CIDR
33        and is used in conjunction with the C(name) to ensure idempotence.
34    required: yes
35    type: list
36    elements: str
37  ipv6_cidr:
38    description:
39      - Request an Amazon-provided IPv6 CIDR block with /56 prefix length.  You cannot specify the range of IPv6 addresses,
40        or the size of the CIDR block.
41    default: False
42    type: bool
43    version_added: '2.10'
44  purge_cidrs:
45    description:
46      - Remove CIDRs that are associated with the VPC and are not specified in C(cidr_block).
47    default: no
48    type: bool
49    version_added: '2.5'
50  tenancy:
51    description:
52      - Whether to be default or dedicated tenancy. This cannot be changed after the VPC has been created.
53    default: default
54    choices: [ 'default', 'dedicated' ]
55    type: str
56  dns_support:
57    description:
58      - Whether to enable AWS DNS support.
59    default: yes
60    type: bool
61  dns_hostnames:
62    description:
63      - Whether to enable AWS hostname support.
64    default: yes
65    type: bool
66  dhcp_opts_id:
67    description:
68      - The id of the DHCP options to use for this VPC.
69    type: str
70  tags:
71    description:
72      - The tags you want attached to the VPC. This is independent of the name value, note if you pass a 'Name' key it would override the Name of
73        the VPC if it's different.
74    aliases: [ 'resource_tags' ]
75    type: dict
76  state:
77    description:
78      - The state of the VPC. Either absent or present.
79    default: present
80    choices: [ 'present', 'absent' ]
81    type: str
82  multi_ok:
83    description:
84      - By default the module will not create another VPC if there is another VPC with the same name and CIDR block. Specify this as true if you want
85        duplicate VPCs created.
86    type: bool
87    default: false
88requirements:
89    - boto3
90    - botocore
91extends_documentation_fragment:
92    - aws
93    - ec2
94'''
95
96EXAMPLES = '''
97# Note: These examples do not set authentication details, see the AWS Guide for details.
98
99- name: create a VPC with dedicated tenancy and a couple of tags
100  ec2_vpc_net:
101    name: Module_dev2
102    cidr_block: 10.10.0.0/16
103    region: us-east-1
104    tags:
105      module: ec2_vpc_net
106      this: works
107    tenancy: dedicated
108
109- name: create a VPC with dedicated tenancy and request an IPv6 CIDR
110  ec2_vpc_net:
111    name: Module_dev2
112    cidr_block: 10.10.0.0/16
113    ipv6_cidr: True
114    region: us-east-1
115    tenancy: dedicated
116'''
117
118RETURN = '''
119vpc:
120  description: info about the VPC that was created or deleted
121  returned: always
122  type: complex
123  contains:
124    cidr_block:
125      description: The CIDR of the VPC
126      returned: always
127      type: str
128      sample: 10.0.0.0/16
129    cidr_block_association_set:
130      description: IPv4 CIDR blocks associated with the VPC
131      returned: success
132      type: list
133      sample:
134        "cidr_block_association_set": [
135            {
136                "association_id": "vpc-cidr-assoc-97aeeefd",
137                "cidr_block": "20.0.0.0/24",
138                "cidr_block_state": {
139                    "state": "associated"
140                }
141            }
142        ]
143    classic_link_enabled:
144      description: indicates whether ClassicLink is enabled
145      returned: always
146      type: bool
147      sample: false
148    dhcp_options_id:
149      description: the id of the DHCP options associated with this VPC
150      returned: always
151      type: str
152      sample: dopt-0fb8bd6b
153    id:
154      description: VPC resource id
155      returned: always
156      type: str
157      sample: vpc-c2e00da5
158    instance_tenancy:
159      description: indicates whether VPC uses default or dedicated tenancy
160      returned: always
161      type: str
162      sample: default
163    ipv6_cidr_block_association_set:
164      description: IPv6 CIDR blocks associated with the VPC
165      returned: success
166      type: list
167      sample:
168        "ipv6_cidr_block_association_set": [
169            {
170                "association_id": "vpc-cidr-assoc-97aeeefd",
171                "ipv6_cidr_block": "2001:db8::/56",
172                "ipv6_cidr_block_state": {
173                    "state": "associated"
174                }
175            }
176        ]
177    is_default:
178      description: indicates whether this is the default VPC
179      returned: always
180      type: bool
181      sample: false
182    state:
183      description: state of the VPC
184      returned: always
185      type: str
186      sample: available
187    tags:
188      description: tags attached to the VPC, includes name
189      returned: always
190      type: complex
191      contains:
192        Name:
193          description: name tag for the VPC
194          returned: always
195          type: str
196          sample: pk_vpc4
197'''
198
199try:
200    import botocore
201except ImportError:
202    pass  # Handled by AnsibleAWSModule
203
204from time import sleep, time
205from ansible.module_utils.aws.core import AnsibleAWSModule
206from ansible.module_utils.ec2 import (AWSRetry, camel_dict_to_snake_dict, compare_aws_tags,
207                                      ansible_dict_to_boto3_tag_list, boto3_tag_list_to_ansible_dict)
208from ansible.module_utils.six import string_types
209from ansible.module_utils._text import to_native
210from ansible.module_utils.network.common.utils import to_subnet
211
212
213def vpc_exists(module, vpc, name, cidr_block, multi):
214    """Returns None or a vpc object depending on the existence of a VPC. When supplied
215    with a CIDR, it will check for matching tags to determine if it is a match
216    otherwise it will assume the VPC does not exist and thus return None.
217    """
218    try:
219        matching_vpcs = vpc.describe_vpcs(Filters=[{'Name': 'tag:Name', 'Values': [name]}, {'Name': 'cidr-block', 'Values': cidr_block}])['Vpcs']
220        # If an exact matching using a list of CIDRs isn't found, check for a match with the first CIDR as is documented for C(cidr_block)
221        if not matching_vpcs:
222            matching_vpcs = vpc.describe_vpcs(Filters=[{'Name': 'tag:Name', 'Values': [name]}, {'Name': 'cidr-block', 'Values': [cidr_block[0]]}])['Vpcs']
223    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
224        module.fail_json_aws(e, msg="Failed to describe VPCs")
225
226    if multi:
227        return None
228    elif len(matching_vpcs) == 1:
229        return matching_vpcs[0]['VpcId']
230    elif len(matching_vpcs) > 1:
231        module.fail_json(msg='Currently there are %d VPCs that have the same name and '
232                             'CIDR block you specified. If you would like to create '
233                             'the VPC anyway please pass True to the multi_ok param.' % len(matching_vpcs))
234    return None
235
236
237@AWSRetry.backoff(delay=3, tries=8, catch_extra_error_codes=['InvalidVpcID.NotFound'])
238def get_classic_link_with_backoff(connection, vpc_id):
239    try:
240        return connection.describe_vpc_classic_link(VpcIds=[vpc_id])['Vpcs'][0].get('ClassicLinkEnabled')
241    except botocore.exceptions.ClientError as e:
242        if e.response["Error"]["Message"] == "The functionality you requested is not available in this region.":
243            return False
244        else:
245            raise
246
247
248def get_vpc(module, connection, vpc_id):
249    # wait for vpc to be available
250    try:
251        connection.get_waiter('vpc_available').wait(VpcIds=[vpc_id])
252    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
253        module.fail_json_aws(e, msg="Unable to wait for VPC {0} to be available.".format(vpc_id))
254
255    try:
256        vpc_obj = connection.describe_vpcs(VpcIds=[vpc_id], aws_retry=True)['Vpcs'][0]
257    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
258        module.fail_json_aws(e, msg="Failed to describe VPCs")
259    try:
260        vpc_obj['ClassicLinkEnabled'] = get_classic_link_with_backoff(connection, vpc_id)
261    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
262        module.fail_json_aws(e, msg="Failed to describe VPCs")
263
264    return vpc_obj
265
266
267def update_vpc_tags(connection, module, vpc_id, tags, name):
268    if tags is None:
269        tags = dict()
270
271    tags.update({'Name': name})
272    tags = dict((k, to_native(v)) for k, v in tags.items())
273    try:
274        current_tags = dict((t['Key'], t['Value']) for t in connection.describe_tags(Filters=[{'Name': 'resource-id', 'Values': [vpc_id]}])['Tags'])
275        tags_to_update, dummy = compare_aws_tags(current_tags, tags, False)
276        if tags_to_update:
277            if not module.check_mode:
278                tags = ansible_dict_to_boto3_tag_list(tags_to_update)
279                vpc_obj = connection.create_tags(Resources=[vpc_id], Tags=tags, aws_retry=True)
280
281                # Wait for tags to be updated
282                expected_tags = boto3_tag_list_to_ansible_dict(tags)
283                filters = [{'Name': 'tag:{0}'.format(key), 'Values': [value]} for key, value in expected_tags.items()]
284                connection.get_waiter('vpc_available').wait(VpcIds=[vpc_id], Filters=filters)
285
286            return True
287        else:
288            return False
289    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
290        module.fail_json_aws(e, msg="Failed to update tags")
291
292
293def update_dhcp_opts(connection, module, vpc_obj, dhcp_id):
294    if vpc_obj['DhcpOptionsId'] != dhcp_id:
295        if not module.check_mode:
296            try:
297                connection.associate_dhcp_options(DhcpOptionsId=dhcp_id, VpcId=vpc_obj['VpcId'])
298            except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
299                module.fail_json_aws(e, msg="Failed to associate DhcpOptionsId {0}".format(dhcp_id))
300
301            try:
302                # Wait for DhcpOptionsId to be updated
303                filters = [{'Name': 'dhcp-options-id', 'Values': [dhcp_id]}]
304                connection.get_waiter('vpc_available').wait(VpcIds=[vpc_obj['VpcId']], Filters=filters)
305            except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
306                module.fail_json(msg="Failed to wait for DhcpOptionsId to be updated")
307
308        return True
309    else:
310        return False
311
312
313def create_vpc(connection, module, cidr_block, tenancy):
314    try:
315        if not module.check_mode:
316            vpc_obj = connection.create_vpc(CidrBlock=cidr_block, InstanceTenancy=tenancy)
317        else:
318            module.exit_json(changed=True)
319    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
320        module.fail_json_aws(e, "Failed to create the VPC")
321
322    # wait for vpc to exist
323    try:
324        connection.get_waiter('vpc_exists').wait(VpcIds=[vpc_obj['Vpc']['VpcId']])
325    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
326        module.fail_json_aws(e, msg="Unable to wait for VPC {0} to be created.".format(vpc_obj['Vpc']['VpcId']))
327
328    return vpc_obj['Vpc']['VpcId']
329
330
331def wait_for_vpc_attribute(connection, module, vpc_id, attribute, expected_value):
332    start_time = time()
333    updated = False
334    while time() < start_time + 300:
335        current_value = connection.describe_vpc_attribute(
336            Attribute=attribute,
337            VpcId=vpc_id
338        )['{0}{1}'.format(attribute[0].upper(), attribute[1:])]['Value']
339        if current_value != expected_value:
340            sleep(3)
341        else:
342            updated = True
343            break
344    if not updated:
345        module.fail_json(msg="Failed to wait for {0} to be updated".format(attribute))
346
347
348def get_cidr_network_bits(module, cidr_block):
349    fixed_cidrs = []
350    for cidr in cidr_block:
351        split_addr = cidr.split('/')
352        if len(split_addr) == 2:
353            # this_ip is a IPv4 CIDR that may or may not have host bits set
354            # Get the network bits.
355            valid_cidr = to_subnet(split_addr[0], split_addr[1])
356            if cidr != valid_cidr:
357                module.warn("One of your CIDR addresses ({0}) has host bits set. To get rid of this warning, "
358                            "check the network mask and make sure that only network bits are set: {1}.".format(cidr, valid_cidr))
359            fixed_cidrs.append(valid_cidr)
360        else:
361            # let AWS handle invalid CIDRs
362            fixed_cidrs.append(cidr)
363    return fixed_cidrs
364
365
366def main():
367    argument_spec = dict(
368        name=dict(required=True),
369        cidr_block=dict(type='list', required=True),
370        ipv6_cidr=dict(type='bool', default=False),
371        tenancy=dict(choices=['default', 'dedicated'], default='default'),
372        dns_support=dict(type='bool', default=True),
373        dns_hostnames=dict(type='bool', default=True),
374        dhcp_opts_id=dict(),
375        tags=dict(type='dict', aliases=['resource_tags']),
376        state=dict(choices=['present', 'absent'], default='present'),
377        multi_ok=dict(type='bool', default=False),
378        purge_cidrs=dict(type='bool', default=False),
379    )
380
381    module = AnsibleAWSModule(
382        argument_spec=argument_spec,
383        supports_check_mode=True
384    )
385
386    name = module.params.get('name')
387    cidr_block = get_cidr_network_bits(module, module.params.get('cidr_block'))
388    ipv6_cidr = module.params.get('ipv6_cidr')
389    purge_cidrs = module.params.get('purge_cidrs')
390    tenancy = module.params.get('tenancy')
391    dns_support = module.params.get('dns_support')
392    dns_hostnames = module.params.get('dns_hostnames')
393    dhcp_id = module.params.get('dhcp_opts_id')
394    tags = module.params.get('tags')
395    state = module.params.get('state')
396    multi = module.params.get('multi_ok')
397
398    changed = False
399
400    connection = module.client(
401        'ec2',
402        retry_decorator=AWSRetry.jittered_backoff(
403            retries=8, delay=3, catch_extra_error_codes=['InvalidVpcID.NotFound']
404        )
405    )
406
407    if dns_hostnames and not dns_support:
408        module.fail_json(msg='In order to enable DNS Hostnames you must also enable DNS support')
409
410    if state == 'present':
411
412        # Check if VPC exists
413        vpc_id = vpc_exists(module, connection, name, cidr_block, multi)
414
415        if vpc_id is None:
416            vpc_id = create_vpc(connection, module, cidr_block[0], tenancy)
417            changed = True
418
419        vpc_obj = get_vpc(module, connection, vpc_id)
420
421        associated_cidrs = dict((cidr['CidrBlock'], cidr['AssociationId']) for cidr in vpc_obj.get('CidrBlockAssociationSet', [])
422                                if cidr['CidrBlockState']['State'] != 'disassociated')
423        to_add = [cidr for cidr in cidr_block if cidr not in associated_cidrs]
424        to_remove = [associated_cidrs[cidr] for cidr in associated_cidrs if cidr not in cidr_block]
425        expected_cidrs = [cidr for cidr in associated_cidrs if associated_cidrs[cidr] not in to_remove] + to_add
426
427        if len(cidr_block) > 1:
428            for cidr in to_add:
429                changed = True
430                try:
431                    connection.associate_vpc_cidr_block(CidrBlock=cidr, VpcId=vpc_id)
432                except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
433                    module.fail_json_aws(e, "Unable to associate CIDR {0}.".format(ipv6_cidr))
434        if ipv6_cidr:
435            if 'Ipv6CidrBlockAssociationSet' in vpc_obj.keys():
436                module.warn("Only one IPv6 CIDR is permitted per VPC, {0} already has CIDR {1}".format(
437                    vpc_id,
438                    vpc_obj['Ipv6CidrBlockAssociationSet'][0]['Ipv6CidrBlock']))
439            else:
440                try:
441                    connection.associate_vpc_cidr_block(AmazonProvidedIpv6CidrBlock=ipv6_cidr, VpcId=vpc_id)
442                    changed = True
443                except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
444                    module.fail_json_aws(e, "Unable to associate CIDR {0}.".format(ipv6_cidr))
445
446        if purge_cidrs:
447            for association_id in to_remove:
448                changed = True
449                try:
450                    connection.disassociate_vpc_cidr_block(AssociationId=association_id)
451                except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
452                    module.fail_json_aws(e, "Unable to disassociate {0}. You must detach or delete all gateways and resources that "
453                                         "are associated with the CIDR block before you can disassociate it.".format(association_id))
454
455        if dhcp_id is not None:
456            try:
457                if update_dhcp_opts(connection, module, vpc_obj, dhcp_id):
458                    changed = True
459            except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
460                module.fail_json_aws(e, "Failed to update DHCP options")
461
462        if tags is not None or name is not None:
463            try:
464                if update_vpc_tags(connection, module, vpc_id, tags, name):
465                    changed = True
466            except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
467                module.fail_json_aws(e, msg="Failed to update tags")
468
469        current_dns_enabled = connection.describe_vpc_attribute(Attribute='enableDnsSupport', VpcId=vpc_id, aws_retry=True)['EnableDnsSupport']['Value']
470        current_dns_hostnames = connection.describe_vpc_attribute(Attribute='enableDnsHostnames', VpcId=vpc_id, aws_retry=True)['EnableDnsHostnames']['Value']
471        if current_dns_enabled != dns_support:
472            changed = True
473            if not module.check_mode:
474                try:
475                    connection.modify_vpc_attribute(VpcId=vpc_id, EnableDnsSupport={'Value': dns_support})
476                except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
477                    module.fail_json_aws(e, "Failed to update enabled dns support attribute")
478        if current_dns_hostnames != dns_hostnames:
479            changed = True
480            if not module.check_mode:
481                try:
482                    connection.modify_vpc_attribute(VpcId=vpc_id, EnableDnsHostnames={'Value': dns_hostnames})
483                except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
484                    module.fail_json_aws(e, "Failed to update enabled dns hostnames attribute")
485
486        # wait for associated cidrs to match
487        if to_add or to_remove:
488            try:
489                connection.get_waiter('vpc_available').wait(
490                    VpcIds=[vpc_id],
491                    Filters=[{'Name': 'cidr-block-association.cidr-block', 'Values': expected_cidrs}]
492                )
493            except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
494                module.fail_json_aws(e, "Failed to wait for CIDRs to update")
495
496        # try to wait for enableDnsSupport and enableDnsHostnames to match
497        wait_for_vpc_attribute(connection, module, vpc_id, 'enableDnsSupport', dns_support)
498        wait_for_vpc_attribute(connection, module, vpc_id, 'enableDnsHostnames', dns_hostnames)
499
500        final_state = camel_dict_to_snake_dict(get_vpc(module, connection, vpc_id))
501        final_state['tags'] = boto3_tag_list_to_ansible_dict(final_state.get('tags', []))
502        final_state['id'] = final_state.pop('vpc_id')
503
504        module.exit_json(changed=changed, vpc=final_state)
505
506    elif state == 'absent':
507
508        # Check if VPC exists
509        vpc_id = vpc_exists(module, connection, name, cidr_block, multi)
510
511        if vpc_id is not None:
512            try:
513                if not module.check_mode:
514                    connection.delete_vpc(VpcId=vpc_id)
515                changed = True
516            except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
517                module.fail_json_aws(e, msg="Failed to delete VPC {0} You may want to use the ec2_vpc_subnet, ec2_vpc_igw, "
518                                     "and/or ec2_vpc_route_table modules to ensure the other components are absent.".format(vpc_id))
519
520        module.exit_json(changed=changed, vpc={})
521
522
523if __name__ == '__main__':
524    main()
525