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 9DOCUMENTATION = ''' 10--- 11module: ec2_vpc_subnet 12version_added: 1.0.0 13short_description: Manage subnets in AWS virtual private clouds 14description: 15 - Manage subnets in AWS virtual private clouds. 16author: 17- Robert Estelle (@erydo) 18- Brad Davidson (@brandond) 19requirements: [ boto3 ] 20options: 21 az: 22 description: 23 - "The availability zone for the subnet." 24 type: str 25 cidr: 26 description: 27 - "The CIDR block for the subnet. E.g. 192.0.2.0/24." 28 type: str 29 required: true 30 ipv6_cidr: 31 description: 32 - "The IPv6 CIDR block for the subnet. The VPC must have a /56 block assigned and this value must be a valid IPv6 /64 that falls in the VPC range." 33 - "Required if I(assign_instances_ipv6=true)" 34 type: str 35 tags: 36 description: 37 - "A dict of tags to apply to the subnet. Any tags currently applied to the subnet and not present here will be removed." 38 aliases: [ 'resource_tags' ] 39 type: dict 40 state: 41 description: 42 - "Create or remove the subnet." 43 default: present 44 choices: [ 'present', 'absent' ] 45 type: str 46 vpc_id: 47 description: 48 - "VPC ID of the VPC in which to create or delete the subnet." 49 required: true 50 type: str 51 map_public: 52 description: 53 - "Specify C(yes) to indicate that instances launched into the subnet should be assigned public IP address by default." 54 type: bool 55 default: 'no' 56 assign_instances_ipv6: 57 description: 58 - "Specify C(yes) to indicate that instances launched into the subnet should be automatically assigned an IPv6 address." 59 type: bool 60 default: false 61 wait: 62 description: 63 - "When I(wait=true) and I(state=present), module will wait for subnet to be in available state before continuing." 64 type: bool 65 default: true 66 wait_timeout: 67 description: 68 - "Number of seconds to wait for subnet to become available I(wait=True)." 69 default: 300 70 type: int 71 purge_tags: 72 description: 73 - Whether or not to remove tags that do not appear in the I(tags) list. 74 type: bool 75 default: true 76extends_documentation_fragment: 77- amazon.aws.aws 78- amazon.aws.ec2 79 80''' 81 82EXAMPLES = ''' 83# Note: These examples do not set authentication details, see the AWS Guide for details. 84 85- name: Create subnet for database servers 86 amazon.aws.ec2_vpc_subnet: 87 state: present 88 vpc_id: vpc-123456 89 cidr: 10.0.1.16/28 90 tags: 91 Name: Database Subnet 92 register: database_subnet 93 94- name: Remove subnet for database servers 95 amazon.aws.ec2_vpc_subnet: 96 state: absent 97 vpc_id: vpc-123456 98 cidr: 10.0.1.16/28 99 100- name: Create subnet with IPv6 block assigned 101 amazon.aws.ec2_vpc_subnet: 102 state: present 103 vpc_id: vpc-123456 104 cidr: 10.1.100.0/24 105 ipv6_cidr: 2001:db8:0:102::/64 106 107- name: Remove IPv6 block assigned to subnet 108 amazon.aws.ec2_vpc_subnet: 109 state: present 110 vpc_id: vpc-123456 111 cidr: 10.1.100.0/24 112 ipv6_cidr: '' 113''' 114 115RETURN = ''' 116subnet: 117 description: Dictionary of subnet values 118 returned: I(state=present) 119 type: complex 120 contains: 121 id: 122 description: Subnet resource id 123 returned: I(state=present) 124 type: str 125 sample: subnet-b883b2c4 126 cidr_block: 127 description: The IPv4 CIDR of the Subnet 128 returned: I(state=present) 129 type: str 130 sample: "10.0.0.0/16" 131 ipv6_cidr_block: 132 description: The IPv6 CIDR block actively associated with the Subnet 133 returned: I(state=present) 134 type: str 135 sample: "2001:db8:0:102::/64" 136 availability_zone: 137 description: Availability zone of the Subnet 138 returned: I(state=present) 139 type: str 140 sample: us-east-1a 141 state: 142 description: state of the Subnet 143 returned: I(state=present) 144 type: str 145 sample: available 146 tags: 147 description: tags attached to the Subnet, includes name 148 returned: I(state=present) 149 type: dict 150 sample: {"Name": "My Subnet", "env": "staging"} 151 map_public_ip_on_launch: 152 description: whether public IP is auto-assigned to new instances 153 returned: I(state=present) 154 type: bool 155 sample: false 156 assign_ipv6_address_on_creation: 157 description: whether IPv6 address is auto-assigned to new instances 158 returned: I(state=present) 159 type: bool 160 sample: false 161 vpc_id: 162 description: the id of the VPC where this Subnet exists 163 returned: I(state=present) 164 type: str 165 sample: vpc-67236184 166 available_ip_address_count: 167 description: number of available IPv4 addresses 168 returned: I(state=present) 169 type: str 170 sample: 251 171 default_for_az: 172 description: indicates whether this is the default Subnet for this Availability Zone 173 returned: I(state=present) 174 type: bool 175 sample: false 176 ipv6_association_id: 177 description: The IPv6 association ID for the currently associated CIDR 178 returned: I(state=present) 179 type: str 180 sample: subnet-cidr-assoc-b85c74d2 181 ipv6_cidr_block_association_set: 182 description: An array of IPv6 cidr block association set information. 183 returned: I(state=present) 184 type: complex 185 contains: 186 association_id: 187 description: The association ID 188 returned: always 189 type: str 190 ipv6_cidr_block: 191 description: The IPv6 CIDR block that is associated with the subnet. 192 returned: always 193 type: str 194 ipv6_cidr_block_state: 195 description: A hash/dict that contains a single item. The state of the cidr block association. 196 returned: always 197 type: dict 198 contains: 199 state: 200 description: The CIDR block association state. 201 returned: always 202 type: str 203''' 204 205 206import time 207 208try: 209 import botocore 210except ImportError: 211 pass # caught by AnsibleAWSModule 212 213from ansible.module_utils._text import to_text 214from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict 215 216from ..module_utils.core import AnsibleAWSModule 217from ..module_utils.ec2 import AWSRetry 218from ..module_utils.ec2 import ansible_dict_to_boto3_filter_list 219from ..module_utils.ec2 import ansible_dict_to_boto3_tag_list 220from ..module_utils.ec2 import boto3_tag_list_to_ansible_dict 221from ..module_utils.ec2 import compare_aws_tags 222from ..module_utils.ec2 import describe_ec2_tags 223from ..module_utils.ec2 import ensure_ec2_tags 224from ..module_utils.waiters import get_waiter 225 226 227def get_subnet_info(subnet): 228 if 'Subnets' in subnet: 229 return [get_subnet_info(s) for s in subnet['Subnets']] 230 elif 'Subnet' in subnet: 231 subnet = camel_dict_to_snake_dict(subnet['Subnet']) 232 else: 233 subnet = camel_dict_to_snake_dict(subnet) 234 235 if 'tags' in subnet: 236 subnet['tags'] = boto3_tag_list_to_ansible_dict(subnet['tags']) 237 else: 238 subnet['tags'] = dict() 239 240 if 'subnet_id' in subnet: 241 subnet['id'] = subnet['subnet_id'] 242 del subnet['subnet_id'] 243 244 subnet['ipv6_cidr_block'] = '' 245 subnet['ipv6_association_id'] = '' 246 ipv6set = subnet.get('ipv6_cidr_block_association_set') 247 if ipv6set: 248 for item in ipv6set: 249 if item.get('ipv6_cidr_block_state', {}).get('state') in ('associated', 'associating'): 250 subnet['ipv6_cidr_block'] = item['ipv6_cidr_block'] 251 subnet['ipv6_association_id'] = item['association_id'] 252 253 return subnet 254 255 256def waiter_params(module, params, start_time): 257 if not module.botocore_at_least("1.7.0"): 258 remaining_wait_timeout = int(module.params['wait_timeout'] + start_time - time.time()) 259 params['WaiterConfig'] = {'Delay': 5, 'MaxAttempts': remaining_wait_timeout // 5} 260 return params 261 262 263def handle_waiter(conn, module, waiter_name, params, start_time): 264 try: 265 get_waiter(conn, waiter_name).wait( 266 **waiter_params(module, params, start_time) 267 ) 268 except botocore.exceptions.WaiterError as e: 269 module.fail_json_aws(e, "Failed to wait for updates to complete") 270 except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: 271 module.fail_json_aws(e, "An exception happened while trying to wait for updates") 272 273 274def create_subnet(conn, module, vpc_id, cidr, ipv6_cidr=None, az=None, start_time=None): 275 wait = module.params['wait'] 276 wait_timeout = module.params['wait_timeout'] 277 278 params = dict(VpcId=vpc_id, 279 CidrBlock=cidr) 280 281 if ipv6_cidr: 282 params['Ipv6CidrBlock'] = ipv6_cidr 283 284 if az: 285 params['AvailabilityZone'] = az 286 287 try: 288 subnet = get_subnet_info(conn.create_subnet(aws_retry=True, **params)) 289 except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: 290 module.fail_json_aws(e, msg="Couldn't create subnet") 291 292 # Sometimes AWS takes its time to create a subnet and so using 293 # new subnets's id to do things like create tags results in 294 # exception. 295 if wait and subnet.get('state') != 'available': 296 handle_waiter(conn, module, 'subnet_exists', {'SubnetIds': [subnet['id']]}, start_time) 297 handle_waiter(conn, module, 'subnet_available', {'SubnetIds': [subnet['id']]}, start_time) 298 subnet['state'] = 'available' 299 300 return subnet 301 302 303def ensure_tags(conn, module, subnet, tags, purge_tags, start_time): 304 305 changed = ensure_ec2_tags( 306 conn, module, subnet['id'], 307 resource_type='subnet', 308 purge_tags=purge_tags, 309 tags=tags, 310 retry_codes=['InvalidSubnetID.NotFound']) 311 312 if module.params['wait'] and not module.check_mode: 313 # Wait for tags to be updated 314 filters = [{'Name': 'tag:{0}'.format(k), 'Values': [v]} for k, v in tags.items()] 315 handle_waiter(conn, module, 'subnet_exists', 316 {'SubnetIds': [subnet['id']], 'Filters': filters}, start_time) 317 318 return changed 319 320 321def ensure_map_public(conn, module, subnet, map_public, check_mode, start_time): 322 if check_mode: 323 return 324 try: 325 conn.modify_subnet_attribute(aws_retry=True, SubnetId=subnet['id'], 326 MapPublicIpOnLaunch={'Value': map_public}) 327 except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: 328 module.fail_json_aws(e, msg="Couldn't modify subnet attribute") 329 330 331def ensure_assign_ipv6_on_create(conn, module, subnet, assign_instances_ipv6, check_mode, start_time): 332 if check_mode: 333 return 334 try: 335 conn.modify_subnet_attribute(aws_retry=True, SubnetId=subnet['id'], 336 AssignIpv6AddressOnCreation={'Value': assign_instances_ipv6}) 337 except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: 338 module.fail_json_aws(e, msg="Couldn't modify subnet attribute") 339 340 341def disassociate_ipv6_cidr(conn, module, subnet, start_time): 342 if subnet.get('assign_ipv6_address_on_creation'): 343 ensure_assign_ipv6_on_create(conn, module, subnet, False, False, start_time) 344 345 try: 346 conn.disassociate_subnet_cidr_block(aws_retry=True, AssociationId=subnet['ipv6_association_id']) 347 except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: 348 module.fail_json_aws(e, msg="Couldn't disassociate ipv6 cidr block id {0} from subnet {1}" 349 .format(subnet['ipv6_association_id'], subnet['id'])) 350 351 # Wait for cidr block to be disassociated 352 if module.params['wait']: 353 filters = ansible_dict_to_boto3_filter_list( 354 {'ipv6-cidr-block-association.state': ['disassociated'], 355 'vpc-id': subnet['vpc_id']} 356 ) 357 handle_waiter(conn, module, 'subnet_exists', 358 {'SubnetIds': [subnet['id']], 'Filters': filters}, start_time) 359 360 361def ensure_ipv6_cidr_block(conn, module, subnet, ipv6_cidr, check_mode, start_time): 362 wait = module.params['wait'] 363 changed = False 364 365 if subnet['ipv6_association_id'] and not ipv6_cidr: 366 if not check_mode: 367 disassociate_ipv6_cidr(conn, module, subnet, start_time) 368 changed = True 369 370 if ipv6_cidr: 371 filters = ansible_dict_to_boto3_filter_list({'ipv6-cidr-block-association.ipv6-cidr-block': ipv6_cidr, 372 'vpc-id': subnet['vpc_id']}) 373 374 try: 375 _subnets = conn.describe_subnets(aws_retry=True, Filters=filters) 376 check_subnets = get_subnet_info(_subnets) 377 except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: 378 module.fail_json_aws(e, msg="Couldn't get subnet info") 379 380 if check_subnets and check_subnets[0]['ipv6_cidr_block']: 381 module.fail_json(msg="The IPv6 CIDR '{0}' conflicts with another subnet".format(ipv6_cidr)) 382 383 if subnet['ipv6_association_id']: 384 if not check_mode: 385 disassociate_ipv6_cidr(conn, module, subnet, start_time) 386 changed = True 387 388 try: 389 if not check_mode: 390 associate_resp = conn.associate_subnet_cidr_block(aws_retry=True, SubnetId=subnet['id'], 391 Ipv6CidrBlock=ipv6_cidr) 392 changed = True 393 except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: 394 module.fail_json_aws(e, msg="Couldn't associate ipv6 cidr {0} to {1}".format(ipv6_cidr, subnet['id'])) 395 else: 396 if not check_mode and wait: 397 filters = ansible_dict_to_boto3_filter_list( 398 {'ipv6-cidr-block-association.state': ['associated'], 399 'vpc-id': subnet['vpc_id']} 400 ) 401 handle_waiter(conn, module, 'subnet_exists', 402 {'SubnetIds': [subnet['id']], 'Filters': filters}, start_time) 403 404 if associate_resp.get('Ipv6CidrBlockAssociation', {}).get('AssociationId'): 405 subnet['ipv6_association_id'] = associate_resp['Ipv6CidrBlockAssociation']['AssociationId'] 406 subnet['ipv6_cidr_block'] = associate_resp['Ipv6CidrBlockAssociation']['Ipv6CidrBlock'] 407 if subnet['ipv6_cidr_block_association_set']: 408 subnet['ipv6_cidr_block_association_set'][0] = camel_dict_to_snake_dict(associate_resp['Ipv6CidrBlockAssociation']) 409 else: 410 subnet['ipv6_cidr_block_association_set'].append(camel_dict_to_snake_dict(associate_resp['Ipv6CidrBlockAssociation'])) 411 412 return changed 413 414 415def get_matching_subnet(conn, module, vpc_id, cidr): 416 filters = ansible_dict_to_boto3_filter_list({'vpc-id': vpc_id, 'cidr-block': cidr}) 417 try: 418 _subnets = conn.describe_subnets(aws_retry=True, Filters=filters) 419 subnets = get_subnet_info(_subnets) 420 except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: 421 module.fail_json_aws(e, msg="Couldn't get matching subnet") 422 423 if subnets: 424 return subnets[0] 425 426 return None 427 428 429def ensure_subnet_present(conn, module): 430 subnet = get_matching_subnet(conn, module, module.params['vpc_id'], module.params['cidr']) 431 changed = False 432 433 # Initialize start so max time does not exceed the specified wait_timeout for multiple operations 434 start_time = time.time() 435 436 if subnet is None: 437 if not module.check_mode: 438 subnet = create_subnet(conn, module, module.params['vpc_id'], module.params['cidr'], 439 ipv6_cidr=module.params['ipv6_cidr'], az=module.params['az'], start_time=start_time) 440 changed = True 441 # Subnet will be None when check_mode is true 442 if subnet is None: 443 return { 444 'changed': changed, 445 'subnet': {} 446 } 447 if module.params['wait']: 448 handle_waiter(conn, module, 'subnet_exists', {'SubnetIds': [subnet['id']]}, start_time) 449 450 if module.params['ipv6_cidr'] != subnet.get('ipv6_cidr_block'): 451 if ensure_ipv6_cidr_block(conn, module, subnet, module.params['ipv6_cidr'], module.check_mode, start_time): 452 changed = True 453 454 if module.params['map_public'] != subnet['map_public_ip_on_launch']: 455 ensure_map_public(conn, module, subnet, module.params['map_public'], module.check_mode, start_time) 456 changed = True 457 458 if module.params['assign_instances_ipv6'] != subnet.get('assign_ipv6_address_on_creation'): 459 ensure_assign_ipv6_on_create(conn, module, subnet, module.params['assign_instances_ipv6'], module.check_mode, start_time) 460 changed = True 461 462 if module.params['tags'] != subnet['tags']: 463 stringified_tags_dict = dict((to_text(k), to_text(v)) for k, v in module.params['tags'].items()) 464 if ensure_tags(conn, module, subnet, stringified_tags_dict, module.params['purge_tags'], start_time): 465 changed = True 466 467 subnet = get_matching_subnet(conn, module, module.params['vpc_id'], module.params['cidr']) 468 if not module.check_mode and module.params['wait']: 469 # GET calls are not monotonic for map_public_ip_on_launch and assign_ipv6_address_on_creation 470 # so we only wait for those if necessary just before returning the subnet 471 subnet = ensure_final_subnet(conn, module, subnet, start_time) 472 473 return { 474 'changed': changed, 475 'subnet': subnet 476 } 477 478 479def ensure_final_subnet(conn, module, subnet, start_time): 480 for rewait in range(0, 30): 481 map_public_correct = False 482 assign_ipv6_correct = False 483 484 if module.params['map_public'] == subnet['map_public_ip_on_launch']: 485 map_public_correct = True 486 else: 487 if module.params['map_public']: 488 handle_waiter(conn, module, 'subnet_has_map_public', {'SubnetIds': [subnet['id']]}, start_time) 489 else: 490 handle_waiter(conn, module, 'subnet_no_map_public', {'SubnetIds': [subnet['id']]}, start_time) 491 492 if module.params['assign_instances_ipv6'] == subnet.get('assign_ipv6_address_on_creation'): 493 assign_ipv6_correct = True 494 else: 495 if module.params['assign_instances_ipv6']: 496 handle_waiter(conn, module, 'subnet_has_assign_ipv6', {'SubnetIds': [subnet['id']]}, start_time) 497 else: 498 handle_waiter(conn, module, 'subnet_no_assign_ipv6', {'SubnetIds': [subnet['id']]}, start_time) 499 500 if map_public_correct and assign_ipv6_correct: 501 break 502 503 time.sleep(5) 504 subnet = get_matching_subnet(conn, module, module.params['vpc_id'], module.params['cidr']) 505 506 return subnet 507 508 509def ensure_subnet_absent(conn, module): 510 subnet = get_matching_subnet(conn, module, module.params['vpc_id'], module.params['cidr']) 511 if subnet is None: 512 return {'changed': False} 513 514 try: 515 if not module.check_mode: 516 conn.delete_subnet(aws_retry=True, SubnetId=subnet['id']) 517 if module.params['wait']: 518 handle_waiter(conn, module, 'subnet_deleted', {'SubnetIds': [subnet['id']]}, time.time()) 519 return {'changed': True} 520 except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: 521 module.fail_json_aws(e, msg="Couldn't delete subnet") 522 523 524def main(): 525 argument_spec = dict( 526 az=dict(default=None, required=False), 527 cidr=dict(required=True), 528 ipv6_cidr=dict(default='', required=False), 529 state=dict(default='present', choices=['present', 'absent']), 530 tags=dict(default={}, required=False, type='dict', aliases=['resource_tags']), 531 vpc_id=dict(required=True), 532 map_public=dict(default=False, required=False, type='bool'), 533 assign_instances_ipv6=dict(default=False, required=False, type='bool'), 534 wait=dict(type='bool', default=True), 535 wait_timeout=dict(type='int', default=300, required=False), 536 purge_tags=dict(default=True, type='bool') 537 ) 538 539 required_if = [('assign_instances_ipv6', True, ['ipv6_cidr'])] 540 541 module = AnsibleAWSModule(argument_spec=argument_spec, supports_check_mode=True, required_if=required_if) 542 543 if module.params.get('assign_instances_ipv6') and not module.params.get('ipv6_cidr'): 544 module.fail_json(msg="assign_instances_ipv6 is True but ipv6_cidr is None or an empty string") 545 546 if not module.botocore_at_least("1.7.0"): 547 module.warn("botocore >= 1.7.0 is required to use wait_timeout for custom wait times") 548 549 retry_decorator = AWSRetry.jittered_backoff(retries=10) 550 connection = module.client('ec2', retry_decorator=retry_decorator) 551 552 state = module.params.get('state') 553 554 try: 555 if state == 'present': 556 result = ensure_subnet_present(connection, module) 557 elif state == 'absent': 558 result = ensure_subnet_absent(connection, module) 559 except botocore.exceptions.ClientError as e: 560 module.fail_json_aws(e) 561 562 module.exit_json(**result) 563 564 565if __name__ == '__main__': 566 main() 567