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