diff options
Diffstat (limited to 'test/support')
7 files changed, 0 insertions, 4590 deletions
diff --git a/test/support/integration/plugins/modules/ec2_eni.py b/test/support/integration/plugins/modules/ec2_eni.py deleted file mode 100644 index 8b6dbd1c32..0000000000 --- a/test/support/integration/plugins/modules/ec2_eni.py +++ /dev/null @@ -1,633 +0,0 @@ -#!/usr/bin/python -# -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: ec2_eni -short_description: Create and optionally attach an Elastic Network Interface (ENI) to an instance -description: - - Create and optionally attach an Elastic Network Interface (ENI) to an instance. If an ENI ID or private_ip is - provided, the existing ENI (if any) will be modified. The 'attached' parameter controls the attachment status - of the network interface. -version_added: "2.0" -author: "Rob White (@wimnat)" -options: - eni_id: - description: - - The ID of the ENI (to modify). - - If I(eni_id=None) and I(state=present), a new eni will be created. - type: str - instance_id: - description: - - Instance ID that you wish to attach ENI to. - - Since version 2.2, use the I(attached) parameter to attach or detach an ENI. Prior to 2.2, to detach an ENI from an instance, use C(None). - type: str - private_ip_address: - description: - - Private IP address. - type: str - subnet_id: - description: - - ID of subnet in which to create the ENI. - type: str - description: - description: - - Optional description of the ENI. - type: str - security_groups: - description: - - List of security groups associated with the interface. Only used when I(state=present). - - Since version 2.2, you can specify security groups by ID or by name or a combination of both. Prior to 2.2, you can specify only by ID. - type: list - elements: str - state: - description: - - Create or delete ENI. - default: present - choices: [ 'present', 'absent' ] - type: str - device_index: - description: - - The index of the device for the network interface attachment on the instance. - default: 0 - type: int - attached: - description: - - Specifies if network interface should be attached or detached from instance. If omitted, attachment status - won't change - version_added: 2.2 - type: bool - force_detach: - description: - - Force detachment of the interface. This applies either when explicitly detaching the interface by setting I(instance_id=None) - or when deleting an interface with I(state=absent). - default: false - type: bool - delete_on_termination: - description: - - Delete the interface when the instance it is attached to is terminated. You can only specify this flag when the - interface is being modified, not on creation. - required: false - type: bool - source_dest_check: - description: - - By default, interfaces perform source/destination checks. NAT instances however need this check to be disabled. - You can only specify this flag when the interface is being modified, not on creation. - required: false - type: bool - secondary_private_ip_addresses: - description: - - A list of IP addresses to assign as secondary IP addresses to the network interface. - This option is mutually exclusive of I(secondary_private_ip_address_count) - required: false - version_added: 2.2 - type: list - elements: str - purge_secondary_private_ip_addresses: - description: - - To be used with I(secondary_private_ip_addresses) to determine whether or not to remove any secondary IP addresses other than those specified. - - Set I(secondary_private_ip_addresses=[]) to purge all secondary addresses. - default: false - type: bool - version_added: 2.5 - secondary_private_ip_address_count: - description: - - The number of secondary IP addresses to assign to the network interface. This option is mutually exclusive of I(secondary_private_ip_addresses) - required: false - version_added: 2.2 - type: int - allow_reassignment: - description: - - Indicates whether to allow an IP address that is already assigned to another network interface or instance - to be reassigned to the specified network interface. - required: false - default: false - type: bool - version_added: 2.7 -extends_documentation_fragment: - - aws - - ec2 -notes: - - This module identifies and ENI based on either the I(eni_id), a combination of I(private_ip_address) and I(subnet_id), - or a combination of I(instance_id) and I(device_id). Any of these options will let you specify a particular ENI. -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -# Create an ENI. As no security group is defined, ENI will be created in default security group -- ec2_eni: - private_ip_address: 172.31.0.20 - subnet_id: subnet-xxxxxxxx - state: present - -# Create an ENI and attach it to an instance -- ec2_eni: - instance_id: i-xxxxxxx - device_index: 1 - private_ip_address: 172.31.0.20 - subnet_id: subnet-xxxxxxxx - state: present - -# Create an ENI with two secondary addresses -- ec2_eni: - subnet_id: subnet-xxxxxxxx - state: present - secondary_private_ip_address_count: 2 - -# Assign a secondary IP address to an existing ENI -# This will purge any existing IPs -- ec2_eni: - subnet_id: subnet-xxxxxxxx - eni_id: eni-yyyyyyyy - state: present - secondary_private_ip_addresses: - - 172.16.1.1 - -# Remove any secondary IP addresses from an existing ENI -- ec2_eni: - subnet_id: subnet-xxxxxxxx - eni_id: eni-yyyyyyyy - state: present - secondary_private_ip_address_count: 0 - -# Destroy an ENI, detaching it from any instance if necessary -- ec2_eni: - eni_id: eni-xxxxxxx - force_detach: true - state: absent - -# Update an ENI -- ec2_eni: - eni_id: eni-xxxxxxx - description: "My new description" - state: present - -# Update an ENI identifying it by private_ip_address and subnet_id -- ec2_eni: - subnet_id: subnet-xxxxxxx - private_ip_address: 172.16.1.1 - description: "My new description" - -# Detach an ENI from an instance -- ec2_eni: - eni_id: eni-xxxxxxx - instance_id: None - state: present - -### Delete an interface on termination -# First create the interface -- ec2_eni: - instance_id: i-xxxxxxx - device_index: 1 - private_ip_address: 172.31.0.20 - subnet_id: subnet-xxxxxxxx - state: present - register: eni - -# Modify the interface to enable the delete_on_terminaton flag -- ec2_eni: - eni_id: "{{ eni.interface.id }}" - delete_on_termination: true - -''' - - -RETURN = ''' -interface: - description: Network interface attributes - returned: when state != absent - type: complex - contains: - description: - description: interface description - type: str - sample: Firewall network interface - groups: - description: list of security groups - type: list - elements: dict - sample: [ { "sg-f8a8a9da": "default" } ] - id: - description: network interface id - type: str - sample: "eni-1d889198" - mac_address: - description: interface's physical address - type: str - sample: "00:00:5E:00:53:23" - owner_id: - description: aws account id - type: str - sample: 812381371 - private_ip_address: - description: primary ip address of this interface - type: str - sample: 10.20.30.40 - private_ip_addresses: - description: list of all private ip addresses associated to this interface - type: list - elements: dict - sample: [ { "primary_address": true, "private_ip_address": "10.20.30.40" } ] - source_dest_check: - description: value of source/dest check flag - type: bool - sample: True - status: - description: network interface status - type: str - sample: "pending" - subnet_id: - description: which vpc subnet the interface is bound - type: str - sample: subnet-b0a0393c - vpc_id: - description: which vpc this network interface is bound - type: str - sample: vpc-9a9a9da - -''' - -import time -import re - -try: - import boto.ec2 - import boto.vpc - from boto.exception import BotoServerError - HAS_BOTO = True -except ImportError: - HAS_BOTO = False - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import (AnsibleAWSError, connect_to_aws, - ec2_argument_spec, get_aws_connection_info, - get_ec2_security_group_ids_from_names) - - -def get_eni_info(interface): - - # Private addresses - private_addresses = [] - for ip in interface.private_ip_addresses: - private_addresses.append({'private_ip_address': ip.private_ip_address, 'primary_address': ip.primary}) - - interface_info = {'id': interface.id, - 'subnet_id': interface.subnet_id, - 'vpc_id': interface.vpc_id, - 'description': interface.description, - 'owner_id': interface.owner_id, - 'status': interface.status, - 'mac_address': interface.mac_address, - 'private_ip_address': interface.private_ip_address, - 'source_dest_check': interface.source_dest_check, - 'groups': dict((group.id, group.name) for group in interface.groups), - 'private_ip_addresses': private_addresses - } - - if interface.attachment is not None: - interface_info['attachment'] = {'attachment_id': interface.attachment.id, - 'instance_id': interface.attachment.instance_id, - 'device_index': interface.attachment.device_index, - 'status': interface.attachment.status, - 'attach_time': interface.attachment.attach_time, - 'delete_on_termination': interface.attachment.delete_on_termination, - } - - return interface_info - - -def wait_for_eni(eni, status): - - while True: - time.sleep(3) - eni.update() - # If the status is detached we just need attachment to disappear - if eni.attachment is None: - if status == "detached": - break - else: - if status == "attached" and eni.attachment.status == "attached": - break - - -def create_eni(connection, vpc_id, module): - - instance_id = module.params.get("instance_id") - attached = module.params.get("attached") - if instance_id == 'None': - instance_id = None - device_index = module.params.get("device_index") - subnet_id = module.params.get('subnet_id') - private_ip_address = module.params.get('private_ip_address') - description = module.params.get('description') - security_groups = get_ec2_security_group_ids_from_names(module.params.get('security_groups'), connection, vpc_id=vpc_id, boto3=False) - secondary_private_ip_addresses = module.params.get("secondary_private_ip_addresses") - secondary_private_ip_address_count = module.params.get("secondary_private_ip_address_count") - changed = False - - try: - eni = connection.create_network_interface(subnet_id, private_ip_address, description, security_groups) - if attached and instance_id is not None: - try: - eni.attach(instance_id, device_index) - except BotoServerError: - eni.delete() - raise - # Wait to allow creation / attachment to finish - wait_for_eni(eni, "attached") - eni.update() - - if secondary_private_ip_address_count is not None: - try: - connection.assign_private_ip_addresses(network_interface_id=eni.id, secondary_private_ip_address_count=secondary_private_ip_address_count) - except BotoServerError: - eni.delete() - raise - - if secondary_private_ip_addresses is not None: - try: - connection.assign_private_ip_addresses(network_interface_id=eni.id, private_ip_addresses=secondary_private_ip_addresses) - except BotoServerError: - eni.delete() - raise - - changed = True - - except BotoServerError as e: - module.fail_json(msg=e.message) - - module.exit_json(changed=changed, interface=get_eni_info(eni)) - - -def modify_eni(connection, vpc_id, module, eni): - - instance_id = module.params.get("instance_id") - attached = module.params.get("attached") - do_detach = module.params.get('state') == 'detached' - device_index = module.params.get("device_index") - description = module.params.get('description') - security_groups = module.params.get('security_groups') - force_detach = module.params.get("force_detach") - source_dest_check = module.params.get("source_dest_check") - delete_on_termination = module.params.get("delete_on_termination") - secondary_private_ip_addresses = module.params.get("secondary_private_ip_addresses") - purge_secondary_private_ip_addresses = module.params.get("purge_secondary_private_ip_addresses") - secondary_private_ip_address_count = module.params.get("secondary_private_ip_address_count") - allow_reassignment = module.params.get("allow_reassignment") - changed = False - - try: - if description is not None: - if eni.description != description: - connection.modify_network_interface_attribute(eni.id, "description", description) - changed = True - if len(security_groups) > 0: - groups = get_ec2_security_group_ids_from_names(security_groups, connection, vpc_id=vpc_id, boto3=False) - if sorted(get_sec_group_list(eni.groups)) != sorted(groups): - connection.modify_network_interface_attribute(eni.id, "groupSet", groups) - changed = True - if source_dest_check is not None: - if eni.source_dest_check != source_dest_check: - connection.modify_network_interface_attribute(eni.id, "sourceDestCheck", source_dest_check) - changed = True - if delete_on_termination is not None and eni.attachment is not None: - if eni.attachment.delete_on_termination is not delete_on_termination: - connection.modify_network_interface_attribute(eni.id, "deleteOnTermination", delete_on_termination, eni.attachment.id) - changed = True - - current_secondary_addresses = [i.private_ip_address for i in eni.private_ip_addresses if not i.primary] - if secondary_private_ip_addresses is not None: - secondary_addresses_to_remove = list(set(current_secondary_addresses) - set(secondary_private_ip_addresses)) - if secondary_addresses_to_remove and purge_secondary_private_ip_addresses: - connection.unassign_private_ip_addresses(network_interface_id=eni.id, - private_ip_addresses=list(set(current_secondary_addresses) - - set(secondary_private_ip_addresses)), - dry_run=False) - changed = True - - secondary_addresses_to_add = list(set(secondary_private_ip_addresses) - set(current_secondary_addresses)) - if secondary_addresses_to_add: - connection.assign_private_ip_addresses(network_interface_id=eni.id, - private_ip_addresses=secondary_addresses_to_add, - secondary_private_ip_address_count=None, - allow_reassignment=allow_reassignment, dry_run=False) - changed = True - if secondary_private_ip_address_count is not None: - current_secondary_address_count = len(current_secondary_addresses) - - if secondary_private_ip_address_count > current_secondary_address_count: - connection.assign_private_ip_addresses(network_interface_id=eni.id, - private_ip_addresses=None, - secondary_private_ip_address_count=(secondary_private_ip_address_count - - current_secondary_address_count), - allow_reassignment=allow_reassignment, dry_run=False) - changed = True - elif secondary_private_ip_address_count < current_secondary_address_count: - # How many of these addresses do we want to remove - secondary_addresses_to_remove_count = current_secondary_address_count - secondary_private_ip_address_count - connection.unassign_private_ip_addresses(network_interface_id=eni.id, - private_ip_addresses=current_secondary_addresses[:secondary_addresses_to_remove_count], - dry_run=False) - - if attached is True: - if eni.attachment and eni.attachment.instance_id != instance_id: - detach_eni(eni, module) - eni.attach(instance_id, device_index) - wait_for_eni(eni, "attached") - changed = True - if eni.attachment is None: - eni.attach(instance_id, device_index) - wait_for_eni(eni, "attached") - changed = True - elif attached is False: - detach_eni(eni, module) - - except BotoServerError as e: - module.fail_json(msg=e.message) - - eni.update() - module.exit_json(changed=changed, interface=get_eni_info(eni)) - - -def delete_eni(connection, module): - - eni_id = module.params.get("eni_id") - force_detach = module.params.get("force_detach") - - try: - eni_result_set = connection.get_all_network_interfaces(eni_id) - eni = eni_result_set[0] - - if force_detach is True: - if eni.attachment is not None: - eni.detach(force_detach) - # Wait to allow detachment to finish - wait_for_eni(eni, "detached") - eni.update() - eni.delete() - changed = True - else: - eni.delete() - changed = True - - module.exit_json(changed=changed) - except BotoServerError as e: - regex = re.compile('The networkInterface ID \'.*\' does not exist') - if regex.search(e.message) is not None: - module.exit_json(changed=False) - else: - module.fail_json(msg=e.message) - - -def detach_eni(eni, module): - - attached = module.params.get("attached") - - force_detach = module.params.get("force_detach") - if eni.attachment is not None: - eni.detach(force_detach) - wait_for_eni(eni, "detached") - if attached: - return - eni.update() - module.exit_json(changed=True, interface=get_eni_info(eni)) - else: - module.exit_json(changed=False, interface=get_eni_info(eni)) - - -def uniquely_find_eni(connection, module): - - eni_id = module.params.get("eni_id") - private_ip_address = module.params.get('private_ip_address') - subnet_id = module.params.get('subnet_id') - instance_id = module.params.get('instance_id') - device_index = module.params.get('device_index') - attached = module.params.get('attached') - - try: - filters = {} - - # proceed only if we're univocally specifying an ENI - if eni_id is None and private_ip_address is None and (instance_id is None and device_index is None): - return None - - if private_ip_address and subnet_id: - filters['private-ip-address'] = private_ip_address - filters['subnet-id'] = subnet_id - - if not attached and instance_id and device_index: - filters['attachment.instance-id'] = instance_id - filters['attachment.device-index'] = device_index - - if eni_id is None and len(filters) == 0: - return None - - eni_result = connection.get_all_network_interfaces(eni_id, filters=filters) - if len(eni_result) == 1: - return eni_result[0] - else: - return None - - except BotoServerError as e: - module.fail_json(msg=e.message) - - return None - - -def get_sec_group_list(groups): - - # Build list of remote security groups - remote_security_groups = [] - for group in groups: - remote_security_groups.append(group.id.encode()) - - return remote_security_groups - - -def _get_vpc_id(connection, module, subnet_id): - - try: - return connection.get_all_subnets(subnet_ids=[subnet_id])[0].vpc_id - except BotoServerError as e: - module.fail_json(msg=e.message) - - -def main(): - argument_spec = ec2_argument_spec() - argument_spec.update( - dict( - eni_id=dict(default=None, type='str'), - instance_id=dict(default=None, type='str'), - private_ip_address=dict(type='str'), - subnet_id=dict(type='str'), - description=dict(type='str'), - security_groups=dict(default=[], type='list'), - device_index=dict(default=0, type='int'), - state=dict(default='present', choices=['present', 'absent']), - force_detach=dict(default='no', type='bool'), - source_dest_check=dict(default=None, type='bool'), - delete_on_termination=dict(default=None, type='bool'), - secondary_private_ip_addresses=dict(default=None, type='list'), - purge_secondary_private_ip_addresses=dict(default=False, type='bool'), - secondary_private_ip_address_count=dict(default=None, type='int'), - allow_reassignment=dict(default=False, type='bool'), - attached=dict(default=None, type='bool') - ) - ) - - module = AnsibleModule(argument_spec=argument_spec, - mutually_exclusive=[ - ['secondary_private_ip_addresses', 'secondary_private_ip_address_count'] - ], - required_if=([ - ('state', 'absent', ['eni_id']), - ('attached', True, ['instance_id']), - ('purge_secondary_private_ip_addresses', True, ['secondary_private_ip_addresses']) - ]) - ) - - if not HAS_BOTO: - module.fail_json(msg='boto required for this module') - - region, ec2_url, aws_connect_params = get_aws_connection_info(module) - - if region: - try: - connection = connect_to_aws(boto.ec2, region, **aws_connect_params) - vpc_connection = connect_to_aws(boto.vpc, region, **aws_connect_params) - except (boto.exception.NoAuthHandlerFound, AnsibleAWSError) as e: - module.fail_json(msg=str(e)) - else: - module.fail_json(msg="region must be specified") - - state = module.params.get("state") - - if state == 'present': - eni = uniquely_find_eni(connection, module) - if eni is None: - subnet_id = module.params.get("subnet_id") - if subnet_id is None: - module.fail_json(msg="subnet_id is required when creating a new ENI") - - vpc_id = _get_vpc_id(vpc_connection, module, subnet_id) - create_eni(connection, vpc_id, module) - else: - vpc_id = eni.vpc_id - modify_eni(connection, vpc_id, module, eni) - - elif state == 'absent': - delete_eni(connection, module) - - -if __name__ == '__main__': - main() diff --git a/test/support/integration/plugins/modules/ec2_eni_info.py b/test/support/integration/plugins/modules/ec2_eni_info.py deleted file mode 100644 index 1e281a4938..0000000000 --- a/test/support/integration/plugins/modules/ec2_eni_info.py +++ /dev/null @@ -1,276 +0,0 @@ -#!/usr/bin/python -# -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: ec2_eni_info -short_description: Gather information about ec2 ENI interfaces in AWS -description: - - Gather information about ec2 ENI interfaces in AWS. - - This module was called C(ec2_eni_facts) before Ansible 2.9. The usage did not change. -version_added: "2.0" -author: "Rob White (@wimnat)" -requirements: [ boto3 ] -options: - filters: - description: - - A dict of filters to apply. Each dict item consists of a filter key and a filter value. - See U(https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeNetworkInterfaces.html) for possible filters. - type: dict -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -# Gather information about all ENIs -- ec2_eni_info: - -# Gather information about a particular ENI -- ec2_eni_info: - filters: - network-interface-id: eni-xxxxxxx - -''' - -RETURN = ''' -network_interfaces: - description: List of matching elastic network interfaces - returned: always - type: complex - contains: - association: - description: Info of associated elastic IP (EIP) - returned: always, empty dict if no association exists - type: dict - sample: { - allocation_id: "eipalloc-5sdf123", - association_id: "eipassoc-8sdf123", - ip_owner_id: "4415120123456", - public_dns_name: "ec2-52-1-0-63.compute-1.amazonaws.com", - public_ip: "52.1.0.63" - } - attachment: - description: Info about attached ec2 instance - returned: always, empty dict if ENI is not attached - type: dict - sample: { - attach_time: "2017-08-05T15:25:47+00:00", - attachment_id: "eni-attach-149d21234", - delete_on_termination: false, - device_index: 1, - instance_id: "i-15b8d3cadbafa1234", - instance_owner_id: "4415120123456", - status: "attached" - } - availability_zone: - description: Availability zone of ENI - returned: always - type: str - sample: "us-east-1b" - description: - description: Description text for ENI - returned: always - type: str - sample: "My favourite network interface" - groups: - description: List of attached security groups - returned: always - type: list - sample: [ - { - group_id: "sg-26d0f1234", - group_name: "my_ec2_security_group" - } - ] - id: - description: The id of the ENI (alias for network_interface_id) - returned: always - type: str - sample: "eni-392fsdf" - interface_type: - description: Type of the network interface - returned: always - type: str - sample: "interface" - ipv6_addresses: - description: List of IPv6 addresses for this interface - returned: always - type: list - sample: [] - mac_address: - description: MAC address of the network interface - returned: always - type: str - sample: "0a:f8:10:2f:ab:a1" - network_interface_id: - description: The id of the ENI - returned: always - type: str - sample: "eni-392fsdf" - owner_id: - description: AWS account id of the owner of the ENI - returned: always - type: str - sample: "4415120123456" - private_dns_name: - description: Private DNS name for the ENI - returned: always - type: str - sample: "ip-172-16-1-180.ec2.internal" - private_ip_address: - description: Private IP address for the ENI - returned: always - type: str - sample: "172.16.1.180" - private_ip_addresses: - description: List of private IP addresses attached to the ENI - returned: always - type: list - sample: [] - requester_id: - description: The ID of the entity that launched the ENI - returned: always - type: str - sample: "AIDAIONYVJQNIAZFT3ABC" - requester_managed: - description: Indicates whether the network interface is being managed by an AWS service. - returned: always - type: bool - sample: false - source_dest_check: - description: Indicates whether the network interface performs source/destination checking. - returned: always - type: bool - sample: false - status: - description: Indicates if the network interface is attached to an instance or not - returned: always - type: str - sample: "in-use" - subnet_id: - description: Subnet ID the ENI is in - returned: always - type: str - sample: "subnet-7bbf01234" - tag_set: - description: Dictionary of tags added to the ENI - returned: always - type: dict - sample: {} - vpc_id: - description: ID of the VPC the network interface it part of - returned: always - type: str - sample: "vpc-b3f1f123" -''' - -try: - from botocore.exceptions import ClientError, NoCredentialsError - HAS_BOTO3 = True -except ImportError: - HAS_BOTO3 = False - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import ansible_dict_to_boto3_filter_list, boto3_conn -from ansible.module_utils.ec2 import boto3_tag_list_to_ansible_dict, camel_dict_to_snake_dict -from ansible.module_utils.ec2 import ec2_argument_spec, get_aws_connection_info - - -def list_eni(connection, module): - - if module.params.get("filters") is None: - filters = [] - else: - filters = ansible_dict_to_boto3_filter_list(module.params.get("filters")) - - try: - network_interfaces_result = connection.describe_network_interfaces(Filters=filters)['NetworkInterfaces'] - except (ClientError, NoCredentialsError) as e: - module.fail_json(msg=e.message) - - # Modify boto3 tags list to be ansible friendly dict and then camel_case - camel_network_interfaces = [] - for network_interface in network_interfaces_result: - network_interface['TagSet'] = boto3_tag_list_to_ansible_dict(network_interface['TagSet']) - # Added id to interface info to be compatible with return values of ec2_eni module: - network_interface['Id'] = network_interface['NetworkInterfaceId'] - camel_network_interfaces.append(camel_dict_to_snake_dict(network_interface)) - - module.exit_json(network_interfaces=camel_network_interfaces) - - -def get_eni_info(interface): - - # Private addresses - private_addresses = [] - for ip in interface.private_ip_addresses: - private_addresses.append({'private_ip_address': ip.private_ip_address, 'primary_address': ip.primary}) - - interface_info = {'id': interface.id, - 'subnet_id': interface.subnet_id, - 'vpc_id': interface.vpc_id, - 'description': interface.description, - 'owner_id': interface.owner_id, - 'status': interface.status, - 'mac_address': interface.mac_address, - 'private_ip_address': interface.private_ip_address, - 'source_dest_check': interface.source_dest_check, - 'groups': dict((group.id, group.name) for group in interface.groups), - 'private_ip_addresses': private_addresses - } - - if hasattr(interface, 'publicDnsName'): - interface_info['association'] = {'public_ip_address': interface.publicIp, - 'public_dns_name': interface.publicDnsName, - 'ip_owner_id': interface.ipOwnerId - } - - if interface.attachment is not None: - interface_info['attachment'] = {'attachment_id': interface.attachment.id, - 'instance_id': interface.attachment.instance_id, - 'device_index': interface.attachment.device_index, - 'status': interface.attachment.status, - 'attach_time': interface.attachment.attach_time, - 'delete_on_termination': interface.attachment.delete_on_termination, - } - - return interface_info - - -def main(): - argument_spec = ec2_argument_spec() - argument_spec.update( - dict( - filters=dict(default=None, type='dict') - ) - ) - - module = AnsibleModule(argument_spec=argument_spec) - if module._name == 'ec2_eni_facts': - module.deprecate("The 'ec2_eni_facts' module has been renamed to 'ec2_eni_info'", - version='2.13', collection_name='ansible.builtin') - - if not HAS_BOTO3: - module.fail_json(msg='boto3 required for this module') - - 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) - - list_eni(connection, module) - - -if __name__ == '__main__': - main() diff --git a/test/support/integration/plugins/modules/ec2_instance.py b/test/support/integration/plugins/modules/ec2_instance.py deleted file mode 100644 index 7a587fb941..0000000000 --- a/test/support/integration/plugins/modules/ec2_instance.py +++ /dev/null @@ -1,1805 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = ''' ---- -module: ec2_instance -short_description: Create & manage EC2 instances -description: - - Create and manage AWS EC2 instances. - - > - Note: This module does not support creating - L(EC2 Spot instances,https://aws.amazon.com/ec2/spot/). The M(ec2) module - can create and manage spot instances. -version_added: "2.5" -author: - - Ryan Scott Brown (@ryansb) -requirements: [ "boto3", "botocore" ] -options: - instance_ids: - description: - - If you specify one or more instance IDs, only instances that have the specified IDs are returned. - type: list - state: - description: - - Goal state for the instances. - choices: [present, terminated, running, started, stopped, restarted, rebooted, absent] - default: present - type: str - wait: - description: - - Whether or not to wait for the desired state (use wait_timeout to customize this). - default: true - type: bool - wait_timeout: - description: - - How long to wait (in seconds) for the instance to finish booting/terminating. - default: 600 - type: int - instance_type: - description: - - Instance type to use for the instance, see U(https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-types.html) - Only required when instance is not already present. - default: t2.micro - type: str - user_data: - description: - - Opaque blob of data which is made available to the ec2 instance - type: str - tower_callback: - description: - - Preconfigured user-data to enable an instance to perform a Tower callback (Linux only). - - Mutually exclusive with I(user_data). - - For Windows instances, to enable remote access via Ansible set I(tower_callback.windows) to true, and optionally set an admin password. - - If using 'windows' and 'set_password', callback to Tower will not be performed but the instance will be ready to receive winrm connections from Ansible. - type: dict - suboptions: - tower_address: - description: - - IP address or DNS name of Tower server. Must be accessible via this address from the VPC that this instance will be launched in. - type: str - job_template_id: - description: - - Either the integer ID of the Tower Job Template, or the name (name supported only for Tower 3.2+). - type: str - host_config_key: - description: - - Host configuration secret key generated by the Tower job template. - type: str - tags: - description: - - A hash/dictionary of tags to add to the new instance or to add/remove from an existing one. - type: dict - purge_tags: - description: - - Delete any tags not specified in the task that are on the instance. - This means you have to specify all the desired tags on each task affecting an instance. - default: false - type: bool - image: - description: - - An image to use for the instance. The M(ec2_ami_info) module may be used to retrieve images. - One of I(image) or I(image_id) are required when instance is not already present. - type: dict - suboptions: - id: - description: - - The AMI ID. - type: str - ramdisk: - description: - - Overrides the AMI's default ramdisk ID. - type: str - kernel: - description: - - a string AKI to override the AMI kernel. - image_id: - description: - - I(ami) ID to use for the instance. One of I(image) or I(image_id) are required when instance is not already present. - - This is an alias for I(image.id). - type: str - security_groups: - description: - - A list of security group IDs or names (strings). Mutually exclusive with I(security_group). - type: list - security_group: - description: - - A security group ID or name. Mutually exclusive with I(security_groups). - type: str - name: - description: - - The Name tag for the instance. - type: str - vpc_subnet_id: - description: - - The subnet ID in which to launch the instance (VPC) - If none is provided, ec2_instance will chose the default zone of the default VPC. - aliases: ['subnet_id'] - type: str - network: - description: - - Either a dictionary containing the key 'interfaces' corresponding to a list of network interface IDs or - containing specifications for a single network interface. - - Use the ec2_eni module to create ENIs with special settings. - type: dict - suboptions: - interfaces: - description: - - a list of ENI IDs (strings) or a list of objects containing the key I(id). - type: list - assign_public_ip: - description: - - when true assigns a public IP address to the interface - type: bool - private_ip_address: - description: - - an IPv4 address to assign to the interface - type: str - ipv6_addresses: - description: - - a list of IPv6 addresses to assign to the network interface - type: list - source_dest_check: - description: - - controls whether source/destination checking is enabled on the interface - type: bool - description: - description: - - a description for the network interface - type: str - private_ip_addresses: - description: - - a list of IPv4 addresses to assign to the network interface - type: list - subnet_id: - description: - - the subnet to connect the network interface to - type: str - delete_on_termination: - description: - - Delete the interface when the instance it is attached to is - terminated. - type: bool - device_index: - description: - - The index of the interface to modify - type: int - groups: - description: - - a list of security group IDs to attach to the interface - type: list - volumes: - description: - - A list of block device mappings, by default this will always use the AMI root device so the volumes option is primarily for adding more storage. - - A mapping contains the (optional) keys device_name, virtual_name, ebs.volume_type, ebs.volume_size, ebs.kms_key_id, - ebs.iops, and ebs.delete_on_termination. - - For more information about each parameter, see U(https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_BlockDeviceMapping.html). - type: list - launch_template: - description: - - The EC2 launch template to base instance configuration on. - type: dict - suboptions: - id: - description: - - the ID of the launch template (optional if name is specified). - type: str - name: - description: - - the pretty name of the launch template (optional if id is specified). - type: str - version: - description: - - the specific version of the launch template to use. If unspecified, the template default is chosen. - key_name: - description: - - Name of the SSH access key to assign to the instance - must exist in the region the instance is created. - type: str - availability_zone: - description: - - Specify an availability zone to use the default subnet it. Useful if not specifying the I(vpc_subnet_id) parameter. - - If no subnet, ENI, or availability zone is provided, the default subnet in the default VPC will be used in the first AZ (alphabetically sorted). - type: str - instance_initiated_shutdown_behavior: - description: - - Whether to stop or terminate an instance upon shutdown. - choices: ['stop', 'terminate'] - type: str - tenancy: - description: - - What type of tenancy to allow an instance to use. Default is shared tenancy. Dedicated tenancy will incur additional charges. - choices: ['dedicated', 'default'] - type: str - termination_protection: - description: - - Whether to enable termination protection. - This module will not terminate an instance with termination protection active, it must be turned off first. - type: bool - cpu_credit_specification: - description: - - For T series instances, choose whether to allow increased charges to buy CPU credits if the default pool is depleted. - - Choose I(unlimited) to enable buying additional CPU credits. - choices: ['unlimited', 'standard'] - type: str - cpu_options: - description: - - Reduce the number of vCPU exposed to the instance. - - Those parameters can only be set at instance launch. The two suboptions threads_per_core and core_count are mandatory. - - See U(https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-optimize-cpu.html) for combinations available. - - Requires botocore >= 1.10.16 - version_added: 2.7 - type: dict - suboptions: - threads_per_core: - description: - - Select the number of threads per core to enable. Disable or Enable Intel HT. - choices: [1, 2] - required: true - type: int - core_count: - description: - - Set the number of core to enable. - required: true - type: int - detailed_monitoring: - description: - - Whether to allow detailed cloudwatch metrics to be collected, enabling more detailed alerting. - type: bool - ebs_optimized: - description: - - Whether instance is should use optimized EBS volumes, see U(https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSOptimized.html). - type: bool - filters: - description: - - A dict of filters to apply when deciding whether existing instances match and should be altered. Each dict item - consists of a filter key and a filter value. See - U(https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeInstances.html). - for possible filters. Filter names and values are case sensitive. - - By default, instances are filtered for counting by their "Name" tag, base AMI, state (running, by default), and - subnet ID. Any queryable filter can be used. Good candidates are specific tags, SSH keys, or security groups. - type: dict - instance_role: - description: - - The ARN or name of an EC2-enabled instance role to be used. If a name is not provided in arn format - then the ListInstanceProfiles permission must also be granted. - U(https://docs.aws.amazon.com/IAM/latest/APIReference/API_ListInstanceProfiles.html) If no full ARN is provided, - the role with a matching name will be used from the active AWS account. - type: str - placement_group: - description: - - The placement group that needs to be assigned to the instance - version_added: 2.8 - type: str - -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -# Terminate every running instance in a region. Use with EXTREME caution. -- ec2_instance: - state: absent - filters: - instance-state-name: running - -# restart a particular instance by its ID -- ec2_instance: - state: restarted - instance_ids: - - i-12345678 - -# start an instance with a public IP address -- ec2_instance: - name: "public-compute-instance" - key_name: "prod-ssh-key" - vpc_subnet_id: subnet-5ca1ab1e - instance_type: c5.large - security_group: default - network: - assign_public_ip: true - image_id: ami-123456 - tags: - Environment: Testing - -# start an instance and Add EBS -- ec2_instance: - name: "public-withebs-instance" - vpc_subnet_id: subnet-5ca1ab1e - instance_type: t2.micro - key_name: "prod-ssh-key" - security_group: default - volumes: - - device_name: /dev/sda1 - ebs: - volume_size: 16 - delete_on_termination: true - -# start an instance with a cpu_options -- ec2_instance: - name: "public-cpuoption-instance" - vpc_subnet_id: subnet-5ca1ab1e - tags: - Environment: Testing - instance_type: c4.large - volumes: - - device_name: /dev/sda1 - ebs: - delete_on_termination: true - cpu_options: - core_count: 1 - threads_per_core: 1 - -# start an instance and have it begin a Tower callback on boot -- ec2_instance: - name: "tower-callback-test" - key_name: "prod-ssh-key" - vpc_subnet_id: subnet-5ca1ab1e - security_group: default - tower_callback: - # IP or hostname of tower server - tower_address: 1.2.3.4 - job_template_id: 876 - host_config_key: '[secret config key goes here]' - network: - assign_public_ip: true - image_id: ami-123456 - cpu_credit_specification: unlimited - tags: - SomeThing: "A value" - -# start an instance with ENI (An existing ENI ID is required) -- ec2_instance: - name: "public-eni-instance" - key_name: "prod-ssh-key" - vpc_subnet_id: subnet-5ca1ab1e - network: - interfaces: - - id: "eni-12345" - tags: - Env: "eni_on" - volumes: - - device_name: /dev/sda1 - ebs: - delete_on_termination: true - instance_type: t2.micro - image_id: ami-123456 - -# add second ENI interface -- ec2_instance: - name: "public-eni-instance" - network: - interfaces: - - id: "eni-12345" - - id: "eni-67890" - image_id: ami-123456 - tags: - Env: "eni_on" - instance_type: t2.micro -''' - -RETURN = ''' -instances: - description: a list of ec2 instances - returned: when wait == true - type: complex - contains: - ami_launch_index: - description: The AMI launch index, which can be used to find this instance in the launch group. - returned: always - type: int - sample: 0 - architecture: - description: The architecture of the image - returned: always - type: str - sample: x86_64 - block_device_mappings: - description: Any block device mapping entries for the instance. - returned: always - type: complex - contains: - device_name: - description: The device name exposed to the instance (for example, /dev/sdh or xvdh). - returned: always - type: str - sample: /dev/sdh - ebs: - description: Parameters used to automatically set up EBS volumes when the instance is launched. - returned: always - type: complex - contains: - attach_time: - description: The time stamp when the attachment initiated. - returned: always - type: str - sample: "2017-03-23T22:51:24+00:00" - delete_on_termination: - description: Indicates whether the volume is deleted on instance termination. - returned: always - type: bool - sample: true - status: - description: The attachment state. - returned: always - type: str - sample: attached - volume_id: - description: The ID of the EBS volume - returned: always - type: str - sample: vol-12345678 - client_token: - description: The idempotency token you provided when you launched the instance, if applicable. - returned: always - type: str - sample: mytoken - ebs_optimized: - description: Indicates whether the instance is optimized for EBS I/O. - returned: always - type: bool - sample: false - hypervisor: - description: The hypervisor type of the instance. - returned: always - type: str - sample: xen - iam_instance_profile: - description: The IAM instance profile associated with the instance, if applicable. - returned: always - type: complex - contains: - arn: - description: The Amazon Resource Name (ARN) of the instance profile. - returned: always - type: str - sample: "arn:aws:iam::000012345678:instance-profile/myprofile" - id: - description: The ID of the instance profile - returned: always - type: str - sample: JFJ397FDG400FG9FD1N - image_id: - description: The ID of the AMI used to launch the instance. - returned: always - type: str - sample: ami-0011223344 - instance_id: - description: The ID of the instance. - returned: always - type: str - sample: i-012345678 - instance_type: - description: The instance type size of the running instance. - returned: always - type: str - sample: t2.micro - key_name: - description: The name of the key pair, if this instance was launched with an associated key pair. - returned: always - type: str - sample: my-key - launch_time: - description: The time the instance was launched. - returned: always - type: str - sample: "2017-03-23T22:51:24+00:00" - monitoring: - description: The monitoring for the instance. - returned: always - type: complex - contains: - state: - description: Indicates whether detailed monitoring is enabled. Otherwise, basic monitoring is enabled. - returned: always - type: str - sample: disabled - network_interfaces: - description: One or more network interfaces for the instance. - returned: always - type: complex - contains: - association: - description: The association information for an Elastic IPv4 associated with the network interface. - returned: always - type: complex - contains: - ip_owner_id: - description: The ID of the owner of the Elastic IP address. - returned: always - type: str - sample: amazon - public_dns_name: - description: The public DNS name. - returned: always - type: str - sample: "" - public_ip: - description: The public IP address or Elastic IP address bound to the network interface. - returned: always - type: str - sample: 1.2.3.4 - attachment: - description: The network interface attachment. - returned: always - type: complex - contains: - attach_time: - description: The time stamp when the attachment initiated. - returned: always - type: str - sample: "2017-03-23T22:51:24+00:00" - attachment_id: - description: The ID of the network interface attachment. - returned: always - type: str - sample: eni-attach-3aff3f - delete_on_termination: - description: Indicates whether the network interface is deleted when the instance is terminated. - returned: always - type: bool - sample: true - device_index: - description: The index of the device on the instance for the network interface attachment. - returned: always - type: int - sample: 0 - status: - description: The attachment state. - returned: always - type: str - sample: attached - description: - description: The description. - returned: always - type: str - sample: My interface - groups: - description: One or more security groups. - returned: always - type: list - elements: dict - contains: - group_id: - description: The ID of the security group. - returned: always - type: str - sample: sg-abcdef12 - group_name: - description: The name of the security group. - returned: always - type: str - sample: mygroup - ipv6_addresses: - description: One or more IPv6 addresses associated with the network interface. - returned: always - type: list - elements: dict - contains: - ipv6_address: - description: The IPv6 address. - returned: always - type: str - sample: "2001:0db8:85a3:0000:0000:8a2e:0370:7334" - mac_address: - description: The MAC address. - returned: always - type: str - sample: "00:11:22:33:44:55" - network_interface_id: - description: The ID of the network interface. - returned: always - type: str - sample: eni-01234567 - owner_id: - description: The AWS account ID of the owner of the network interface. - returned: always - type: str - sample: 01234567890 - private_ip_address: - description: The IPv4 address of the network interface within the subnet. - returned: always - type: str - sample: 10.0.0.1 - private_ip_addresses: - description: The private IPv4 addresses associated with the network interface. - returned: always - type: list - elements: dict - contains: - association: - description: The association information for an Elastic IP address (IPv4) associated with the network interface. - returned: always - type: complex - contains: - ip_owner_id: - description: The ID of the owner of the Elastic IP address. - returned: always - type: str - sample: amazon - public_dns_name: - description: The public DNS name. - returned: always - type: str - sample: "" - public_ip: - description: The public IP address or Elastic IP address bound to the network interface. - returned: always - type: str - sample: 1.2.3.4 - primary: - description: Indicates whether this IPv4 address is the primary private IP address of the network interface. - returned: always - type: bool - sample: true - private_ip_address: - description: The private IPv4 address of the network interface. - returned: always - type: str - sample: 10.0.0.1 - source_dest_check: - description: Indicates whether source/destination checking is enabled. - returned: always - type: bool - sample: true - status: - description: The status of the network interface. - returned: always - type: str - sample: in-use - subnet_id: - description: The ID of the subnet for the network interface. - returned: always - type: str - sample: subnet-0123456 - vpc_id: - description: The ID of the VPC for the network interface. - returned: always - type: str - sample: vpc-0123456 - placement: - description: The location where the instance launched, if applicable. - returned: always - type: complex - contains: - availability_zone: - description: The Availability Zone of the instance. - returned: always - type: str - sample: ap-southeast-2a - group_name: - description: The name of the placement group the instance is in (for cluster compute instances). - returned: always - type: str - sample: "" - tenancy: - description: The tenancy of the instance (if the instance is running in a VPC). - returned: always - type: str - sample: default - private_dns_name: - description: The private DNS name. - returned: always - type: str - sample: ip-10-0-0-1.ap-southeast-2.compute.internal - private_ip_address: - description: The IPv4 address of the network interface within the subnet. - returned: always - type: str - sample: 10.0.0.1 - product_codes: - description: One or more product codes. - returned: always - type: list - elements: dict - contains: - product_code_id: - description: The product code. - returned: always - type: str - sample: aw0evgkw8ef3n2498gndfgasdfsd5cce - product_code_type: - description: The type of product code. - returned: always - type: str - sample: marketplace - public_dns_name: - description: The public DNS name assigned to the instance. - returned: always - type: str - sample: - public_ip_address: - description: The public IPv4 address assigned to the instance - returned: always - type: str - sample: 52.0.0.1 - root_device_name: - description: The device name of the root device - returned: always - type: str - sample: /dev/sda1 - root_device_type: - description: The type of root device used by the AMI. - returned: always - type: str - sample: ebs - security_groups: - description: One or more security groups for the instance. - returned: always - type: list - elements: dict - contains: - group_id: - description: The ID of the security group. - returned: always - type: str - sample: sg-0123456 - group_name: - description: The name of the security group. - returned: always - type: str - sample: my-security-group - network.source_dest_check: - description: Indicates whether source/destination checking is enabled. - returned: always - type: bool - sample: true - state: - description: The current state of the instance. - returned: always - type: complex - contains: - code: - description: The low byte represents the state. - returned: always - type: int - sample: 16 - name: - description: The name of the state. - returned: always - type: str - sample: running - state_transition_reason: - description: The reason for the most recent state transition. - returned: always - type: str - sample: - subnet_id: - description: The ID of the subnet in which the instance is running. - returned: always - type: str - sample: subnet-00abcdef - tags: - description: Any tags assigned to the instance. - returned: always - type: dict - sample: - virtualization_type: - description: The type of virtualization of the AMI. - returned: always - type: str - sample: hvm - vpc_id: - description: The ID of the VPC the instance is in. - returned: always - type: dict - sample: vpc-0011223344 -''' - -import re -import uuid -import string -import textwrap -import time -from collections import namedtuple - -try: - import boto3 - import botocore.exceptions -except ImportError: - pass # caught by AnsibleAWSModule - -from ansible.module_utils.six import text_type, string_types -from ansible.module_utils.six.moves.urllib import parse as urlparse -from ansible.module_utils._text import to_bytes, to_native -import ansible.module_utils.ec2 as ec2_utils -from ansible.module_utils.ec2 import (AWSRetry, - ansible_dict_to_boto3_filter_list, - compare_aws_tags, - boto3_tag_list_to_ansible_dict, - ansible_dict_to_boto3_tag_list, - camel_dict_to_snake_dict) - -from ansible.module_utils.aws.core import AnsibleAWSModule - -module = None - - -def tower_callback_script(tower_conf, windows=False, passwd=None): - script_url = 'https://raw.githubusercontent.com/ansible/ansible/devel/examples/scripts/ConfigureRemotingForAnsible.ps1' - if windows and passwd is not None: - script_tpl = """<powershell> - $admin = [adsi]("WinNT://./administrator, user") - $admin.PSBase.Invoke("SetPassword", "{PASS}") - Invoke-Expression ((New-Object System.Net.Webclient).DownloadString('{SCRIPT}')) - </powershell> - """ - return to_native(textwrap.dedent(script_tpl).format(PASS=passwd, SCRIPT=script_url)) - elif windows and passwd is None: - script_tpl = """<powershell> - $admin = [adsi]("WinNT://./administrator, user") - Invoke-Expression ((New-Object System.Net.Webclient).DownloadString('{SCRIPT}')) - </powershell> - """ - return to_native(textwrap.dedent(script_tpl).format(PASS=passwd, SCRIPT=script_url)) - elif not windows: - for p in ['tower_address', 'job_template_id', 'host_config_key']: - if p not in tower_conf: - module.fail_json(msg="Incomplete tower_callback configuration. tower_callback.{0} not set.".format(p)) - - if isinstance(tower_conf['job_template_id'], string_types): - tower_conf['job_template_id'] = urlparse.quote(tower_conf['job_template_id']) - tpl = string.Template(textwrap.dedent("""#!/bin/bash - set -x - - retry_attempts=10 - attempt=0 - while [[ $attempt -lt $retry_attempts ]] - do - status_code=`curl --max-time 10 -v -k -s -i \ - --data "host_config_key=${host_config_key}" \ - 'https://${tower_address}/api/v2/job_templates/${template_id}/callback/' \ - | head -n 1 \ - | awk '{print $2}'` - if [[ $status_code == 404 ]] - then - status_code=`curl --max-time 10 -v -k -s -i \ - --data "host_config_key=${host_config_key}" \ - 'https://${tower_address}/api/v1/job_templates/${template_id}/callback/' \ - | head -n 1 \ - | awk '{print $2}'` - # fall back to using V1 API for Tower 3.1 and below, since v2 API will always 404 - fi - if [[ $status_code == 201 ]] - then - exit 0 - fi - attempt=$(( attempt + 1 )) - echo "$${status_code} received... retrying in 1 minute. (Attempt $${attempt})" - sleep 60 - done - exit 1 - """)) - return tpl.safe_substitute(tower_address=tower_conf['tower_address'], - template_id=tower_conf['job_template_id'], - host_config_key=tower_conf['host_config_key']) - raise NotImplementedError("Only windows with remote-prep or non-windows with tower job callback supported so far.") - - -@AWSRetry.jittered_backoff() -def manage_tags(match, new_tags, purge_tags, ec2): - changed = False - old_tags = boto3_tag_list_to_ansible_dict(match['Tags']) - tags_to_set, tags_to_delete = compare_aws_tags( - old_tags, new_tags, - purge_tags=purge_tags, - ) - if tags_to_set: - ec2.create_tags( - Resources=[match['InstanceId']], - Tags=ansible_dict_to_boto3_tag_list(tags_to_set)) - changed |= True - if tags_to_delete: - delete_with_current_values = dict((k, old_tags.get(k)) for k in tags_to_delete) - ec2.delete_tags( - Resources=[match['InstanceId']], - Tags=ansible_dict_to_boto3_tag_list(delete_with_current_values)) - changed |= True - return changed - - -def build_volume_spec(params): - volumes = params.get('volumes') or [] - for volume in volumes: - if 'ebs' in volume: - for int_value in ['volume_size', 'iops']: - if int_value in volume['ebs']: - volume['ebs'][int_value] = int(volume['ebs'][int_value]) - return [ec2_utils.snake_dict_to_camel_dict(v, capitalize_first=True) for v in volumes] - - -def add_or_update_instance_profile(instance, desired_profile_name): - instance_profile_setting = instance.get('IamInstanceProfile') - if instance_profile_setting and desired_profile_name: - if desired_profile_name in (instance_profile_setting.get('Name'), instance_profile_setting.get('Arn')): - # great, the profile we asked for is what's there - return False - else: - desired_arn = determine_iam_role(desired_profile_name) - if instance_profile_setting.get('Arn') == desired_arn: - return False - # update association - ec2 = module.client('ec2') - try: - association = ec2.describe_iam_instance_profile_associations(Filters=[{'Name': 'instance-id', 'Values': [instance['InstanceId']]}]) - except botocore.exceptions.ClientError as e: - # check for InvalidAssociationID.NotFound - module.fail_json_aws(e, "Could not find instance profile association") - try: - resp = ec2.replace_iam_instance_profile_association( - AssociationId=association['IamInstanceProfileAssociations'][0]['AssociationId'], - IamInstanceProfile={'Arn': determine_iam_role(desired_profile_name)} - ) - return True - except botocore.exceptions.ClientError as e: - module.fail_json_aws(e, "Could not associate instance profile") - - if not instance_profile_setting and desired_profile_name: - # create association - ec2 = module.client('ec2') - try: - resp = ec2.associate_iam_instance_profile( - IamInstanceProfile={'Arn': determine_iam_role(desired_profile_name)}, - InstanceId=instance['InstanceId'] - ) - return True - except botocore.exceptions.ClientError as e: - module.fail_json_aws(e, "Could not associate new instance profile") - - return False - - -def build_network_spec(params, ec2=None): - """ - Returns list of interfaces [complex] - Interface type: { - 'AssociatePublicIpAddress': True|False, - 'DeleteOnTermination': True|False, - 'Description': 'string', - 'DeviceIndex': 123, - 'Groups': [ - 'string', - ], - 'Ipv6AddressCount': 123, - 'Ipv6Addresses': [ - { - 'Ipv6Address': 'string' - }, - ], - 'NetworkInterfaceId': 'string', - 'PrivateIpAddress': 'string', - 'PrivateIpAddresses': [ - { - 'Primary': True|False, - 'PrivateIpAddress': 'string' - }, - ], - 'SecondaryPrivateIpAddressCount': 123, - 'SubnetId': 'string' - }, - """ - if ec2 is None: - ec2 = module.client('ec2') - - interfaces = [] - network = params.get('network') or {} - if not network.get('interfaces'): - # they only specified one interface - spec = { - 'DeviceIndex': 0, - } - if network.get('assign_public_ip') is not None: - spec['AssociatePublicIpAddress'] = network['assign_public_ip'] - - if params.get('vpc_subnet_id'): - spec['SubnetId'] = params['vpc_subnet_id'] - else: - default_vpc = get_default_vpc(ec2) - if default_vpc is None: - raise module.fail_json( - msg="No default subnet could be found - you must include a VPC subnet ID (vpc_subnet_id parameter) to create an instance") - else: - sub = get_default_subnet(ec2, default_vpc) - spec['SubnetId'] = sub['SubnetId'] - - if network.get('private_ip_address'): - spec['PrivateIpAddress'] = network['private_ip_address'] - - if params.get('security_group') or params.get('security_groups'): - groups = discover_security_groups( - group=params.get('security_group'), - groups=params.get('security_groups'), - subnet_id=spec['SubnetId'], - ec2=ec2 - ) - spec['Groups'] = [g['GroupId'] for g in groups] - if network.get('description') is not None: - spec['Description'] = network['description'] - # TODO more special snowflake network things - - return [spec] - - # handle list of `network.interfaces` options - for idx, interface_params in enumerate(network.get('interfaces', [])): - spec = { - 'DeviceIndex': idx, - } - - if isinstance(interface_params, string_types): - # naive case where user gave - # network_interfaces: [eni-1234, eni-4567, ....] - # put into normal data structure so we don't dupe code - interface_params = {'id': interface_params} - - if interface_params.get('id') is not None: - # if an ID is provided, we don't want to set any other parameters. - spec['NetworkInterfaceId'] = interface_params['id'] - interfaces.append(spec) - continue - - spec['DeleteOnTermination'] = interface_params.get('delete_on_termination', True) - - if interface_params.get('ipv6_addresses'): - spec['Ipv6Addresses'] = [{'Ipv6Address': a} for a in interface_params.get('ipv6_addresses', [])] - - if interface_params.get('private_ip_address'): - spec['PrivateIpAddress'] = interface_params.get('private_ip_address') - - if interface_params.get('description'): - spec['Description'] = interface_params.get('description') - - if interface_params.get('subnet_id', params.get('vpc_subnet_id')): - spec['SubnetId'] = interface_params.get('subnet_id', params.get('vpc_subnet_id')) - elif not spec.get('SubnetId') and not interface_params['id']: - # TODO grab a subnet from default VPC - raise ValueError('Failed to assign subnet to interface {0}'.format(interface_params)) - - interfaces.append(spec) - return interfaces - - -def warn_if_public_ip_assignment_changed(instance): - # This is a non-modifiable attribute. - assign_public_ip = (module.params.get('network') or {}).get('assign_public_ip') - if assign_public_ip is None: - return - - # Check that public ip assignment is the same and warn if not - public_dns_name = instance.get('PublicDnsName') - if (public_dns_name and not assign_public_ip) or (assign_public_ip and not public_dns_name): - module.warn( - "Unable to modify public ip assignment to {0} for instance {1}. " - "Whether or not to assign a public IP is determined during instance creation.".format( - assign_public_ip, instance['InstanceId'])) - - -def warn_if_cpu_options_changed(instance): - # This is a non-modifiable attribute. - cpu_options = module.params.get('cpu_options') - if cpu_options is None: - return - - # Check that the CpuOptions set are the same and warn if not - core_count_curr = instance['CpuOptions'].get('CoreCount') - core_count = cpu_options.get('core_count') - threads_per_core_curr = instance['CpuOptions'].get('ThreadsPerCore') - threads_per_core = cpu_options.get('threads_per_core') - if core_count_curr != core_count: - module.warn( - "Unable to modify core_count from {0} to {1}. " - "Assigning a number of core is determinted during instance creation".format( - core_count_curr, core_count)) - - if threads_per_core_curr != threads_per_core: - module.warn( - "Unable to modify threads_per_core from {0} to {1}. " - "Assigning a number of threads per core is determined during instance creation.".format( - threads_per_core_curr, threads_per_core)) - - -def discover_security_groups(group, groups, parent_vpc_id=None, subnet_id=None, ec2=None): - if ec2 is None: - ec2 = module.client('ec2') - - if subnet_id is not None: - try: - sub = ec2.describe_subnets(SubnetIds=[subnet_id]) - except botocore.exceptions.ClientError as e: - if e.response['Error']['Code'] == 'InvalidGroup.NotFound': - module.fail_json( - "Could not find subnet {0} to associate security groups. Please check the vpc_subnet_id and security_groups parameters.".format( - subnet_id - ) - ) - module.fail_json_aws(e, msg="Error while searching for subnet {0} parent VPC.".format(subnet_id)) - except botocore.exceptions.BotoCoreError as e: - module.fail_json_aws(e, msg="Error while searching for subnet {0} parent VPC.".format(subnet_id)) - parent_vpc_id = sub['Subnets'][0]['VpcId'] - - vpc = { - 'Name': 'vpc-id', - 'Values': [parent_vpc_id] - } - - # because filter lists are AND in the security groups API, - # make two separate requests for groups by ID and by name - id_filters = [vpc] - name_filters = [vpc] - - if group: - name_filters.append( - dict( - Name='group-name', - Values=[group] - ) - ) - if group.startswith('sg-'): - id_filters.append( - dict( - Name='group-id', - Values=[group] - ) - ) - if groups: - name_filters.append( - dict( - Name='group-name', - Values=groups - ) - ) - if [g for g in groups if g.startswith('sg-')]: - id_filters.append( - dict( - Name='group-id', - Values=[g for g in groups if g.startswith('sg-')] - ) - ) - - found_groups = [] - for f_set in (id_filters, name_filters): - if len(f_set) > 1: - found_groups.extend(ec2.get_paginator( - 'describe_security_groups' - ).paginate( - Filters=f_set - ).search('SecurityGroups[]')) - return list(dict((g['GroupId'], g) for g in found_groups).values()) - - -def build_top_level_options(params): - spec = {} - if params.get('image_id'): - spec['ImageId'] = params['image_id'] - elif isinstance(params.get('image'), dict): - image = params.get('image', {}) - spec['ImageId'] = image.get('id') - if 'ramdisk' in image: - spec['RamdiskId'] = image['ramdisk'] - if 'kernel' in image: - spec['KernelId'] = image['kernel'] - if not spec.get('ImageId') and not params.get('launch_template'): - module.fail_json(msg="You must include an image_id or image.id parameter to create an instance, or use a launch_template.") - - if params.get('key_name') is not None: - spec['KeyName'] = params.get('key_name') - if params.get('user_data') is not None: - spec['UserData'] = to_native(params.get('user_data')) - elif params.get('tower_callback') is not None: - spec['UserData'] = tower_callback_script( - tower_conf=params.get('tower_callback'), - windows=params.get('tower_callback').get('windows', False), - passwd=params.get('tower_callback').get('set_password'), - ) - - if params.get('launch_template') is not None: - spec['LaunchTemplate'] = {} - if not params.get('launch_template').get('id') or params.get('launch_template').get('name'): - module.fail_json(msg="Could not create instance with launch template. Either launch_template.name or launch_template.id parameters are required") - - if params.get('launch_template').get('id') is not None: - spec['LaunchTemplate']['LaunchTemplateId'] = params.get('launch_template').get('id') - if params.get('launch_template').get('name') is not None: - spec['LaunchTemplate']['LaunchTemplateName'] = params.get('launch_template').get('name') - if params.get('launch_template').get('version') is not None: - spec['LaunchTemplate']['Version'] = to_native(params.get('launch_template').get('version')) - - if params.get('detailed_monitoring', False): - spec['Monitoring'] = {'Enabled': True} - if params.get('cpu_credit_specification') is not None: - spec['CreditSpecification'] = {'CpuCredits': params.get('cpu_credit_specification')} - if params.get('tenancy') is not None: - spec['Placement'] = {'Tenancy': params.get('tenancy')} - if params.get('placement_group'): - if 'Placement' in spec: - spec['Placement']['GroupName'] = str(params.get('placement_group')) - else: - spec.setdefault('Placement', {'GroupName': str(params.get('placement_group'))}) - if params.get('ebs_optimized') is not None: - spec['EbsOptimized'] = params.get('ebs_optimized') - if params.get('instance_initiated_shutdown_behavior'): - spec['InstanceInitiatedShutdownBehavior'] = params.get('instance_initiated_shutdown_behavior') - if params.get('termination_protection') is not None: - spec['DisableApiTermination'] = params.get('termination_protection') - if params.get('cpu_options') is not None: - spec['CpuOptions'] = {} - spec['CpuOptions']['ThreadsPerCore'] = params.get('cpu_options').get('threads_per_core') - spec['CpuOptions']['CoreCount'] = params.get('cpu_options').get('core_count') - return spec - - -def build_instance_tags(params, propagate_tags_to_volumes=True): - tags = params.get('tags', {}) - if params.get('name') is not None: - if tags is None: - tags = {} - tags['Name'] = params.get('name') - return [ - { - 'ResourceType': 'volume', - 'Tags': ansible_dict_to_boto3_tag_list(tags), - }, - { - 'ResourceType': 'instance', - 'Tags': ansible_dict_to_boto3_tag_list(tags), - }, - ] - - -def build_run_instance_spec(params, ec2=None): - if ec2 is None: - ec2 = module.client('ec2') - - spec = dict( - ClientToken=uuid.uuid4().hex, - MaxCount=1, - MinCount=1, - ) - # network parameters - spec['NetworkInterfaces'] = build_network_spec(params, ec2) - spec['BlockDeviceMappings'] = build_volume_spec(params) - spec.update(**build_top_level_options(params)) - spec['TagSpecifications'] = build_instance_tags(params) - - # IAM profile - if params.get('instance_role'): - spec['IamInstanceProfile'] = dict(Arn=determine_iam_role(params.get('instance_role'))) - - spec['InstanceType'] = params['instance_type'] - return spec - - -def await_instances(ids, state='OK'): - if not module.params.get('wait', True): - # the user asked not to wait for anything - return - - if module.check_mode: - # In check mode, there is no change even if you wait. - return - - state_opts = { - 'OK': 'instance_status_ok', - 'STOPPED': 'instance_stopped', - 'TERMINATED': 'instance_terminated', - 'EXISTS': 'instance_exists', - 'RUNNING': 'instance_running', - } - if state not in state_opts: - module.fail_json(msg="Cannot wait for state {0}, invalid state".format(state)) - waiter = module.client('ec2').get_waiter(state_opts[state]) - try: - waiter.wait( - InstanceIds=ids, - WaiterConfig={ - 'Delay': 15, - 'MaxAttempts': module.params.get('wait_timeout', 600) // 15, - } - ) - except botocore.exceptions.WaiterConfigError as e: - module.fail_json(msg="{0}. Error waiting for instances {1} to reach state {2}".format( - to_native(e), ', '.join(ids), state)) - except botocore.exceptions.WaiterError as e: - module.warn("Instances {0} took too long to reach state {1}. {2}".format( - ', '.join(ids), state, to_native(e))) - - -def diff_instance_and_params(instance, params, ec2=None, skip=None): - """boto3 instance obj, module params""" - if ec2 is None: - ec2 = module.client('ec2') - - if skip is None: - skip = [] - - changes_to_apply = [] - id_ = instance['InstanceId'] - - ParamMapper = namedtuple('ParamMapper', ['param_key', 'instance_key', 'attribute_name', 'add_value']) - - def value_wrapper(v): - return {'Value': v} - - param_mappings = [ - ParamMapper('ebs_optimized', 'EbsOptimized', 'ebsOptimized', value_wrapper), - ParamMapper('termination_protection', 'DisableApiTermination', 'disableApiTermination', value_wrapper), - # user data is an immutable property - # ParamMapper('user_data', 'UserData', 'userData', value_wrapper), - ] - - for mapping in param_mappings: - if params.get(mapping.param_key) is not None and mapping.instance_key not in skip: - value = AWSRetry.jittered_backoff()(ec2.describe_instance_attribute)(Attribute=mapping.attribute_name, InstanceId=id_) - if params.get(mapping.param_key) is not None and value[mapping.instance_key]['Value'] != params.get(mapping.param_key): - arguments = dict( - InstanceId=instance['InstanceId'], - # Attribute=mapping.attribute_name, - ) - arguments[mapping.instance_key] = mapping.add_value(params.get(mapping.param_key)) - changes_to_apply.append(arguments) - - if (params.get('network') or {}).get('source_dest_check') is not None: - # network.source_dest_check is nested, so needs to be treated separately - check = bool(params.get('network').get('source_dest_check')) - if instance['SourceDestCheck'] != check: - changes_to_apply.append(dict( - InstanceId=instance['InstanceId'], - SourceDestCheck={'Value': check}, - )) - - return changes_to_apply - - -def change_network_attachments(instance, params, ec2): - if (params.get('network') or {}).get('interfaces') is not None: - new_ids = [] - for inty in params.get('network').get('interfaces'): - if isinstance(inty, dict) and 'id' in inty: - new_ids.append(inty['id']) - elif isinstance(inty, string_types): - new_ids.append(inty) - # network.interfaces can create the need to attach new interfaces - old_ids = [inty['NetworkInterfaceId'] for inty in instance['NetworkInterfaces']] - to_attach = set(new_ids) - set(old_ids) - for eni_id in to_attach: - ec2.attach_network_interface( - DeviceIndex=new_ids.index(eni_id), - InstanceId=instance['InstanceId'], - NetworkInterfaceId=eni_id, - ) - return bool(len(to_attach)) - return False - - -def find_instances(ec2, ids=None, filters=None): - paginator = ec2.get_paginator('describe_instances') - if ids: - return list(paginator.paginate( - InstanceIds=ids, - ).search('Reservations[].Instances[]')) - elif filters is None: - module.fail_json(msg="No filters provided when they were required") - elif filters is not None: - for key in list(filters.keys()): - if not key.startswith("tag:"): - filters[key.replace("_", "-")] = filters.pop(key) - return list(paginator.paginate( - Filters=ansible_dict_to_boto3_filter_list(filters) - ).search('Reservations[].Instances[]')) - return [] - - -@AWSRetry.jittered_backoff() -def get_default_vpc(ec2): - vpcs = ec2.describe_vpcs(Filters=ansible_dict_to_boto3_filter_list({'isDefault': 'true'})) - if len(vpcs.get('Vpcs', [])): - return vpcs.get('Vpcs')[0] - return None - - -@AWSRetry.jittered_backoff() -def get_default_subnet(ec2, vpc, availability_zone=None): - subnets = ec2.describe_subnets( - Filters=ansible_dict_to_boto3_filter_list({ - 'vpc-id': vpc['VpcId'], - 'state': 'available', - 'default-for-az': 'true', - }) - ) - if len(subnets.get('Subnets', [])): - if availability_zone is not None: - subs_by_az = dict((subnet['AvailabilityZone'], subnet) for subnet in subnets.get('Subnets')) - if availability_zone in subs_by_az: - return subs_by_az[availability_zone] - - # to have a deterministic sorting order, we sort by AZ so we'll always pick the `a` subnet first - # there can only be one default-for-az subnet per AZ, so the AZ key is always unique in this list - by_az = sorted(subnets.get('Subnets'), key=lambda s: s['AvailabilityZone']) - return by_az[0] - return None - - -def ensure_instance_state(state, ec2=None): - if ec2 is None: - module.client('ec2') - if state in ('running', 'started'): - changed, failed, instances, failure_reason = change_instance_state(filters=module.params.get('filters'), desired_state='RUNNING') - - if failed: - module.fail_json( - msg="Unable to start instances: {0}".format(failure_reason), - reboot_success=list(changed), - reboot_failed=failed) - - module.exit_json( - msg='Instances started', - reboot_success=list(changed), - changed=bool(len(changed)), - reboot_failed=[], - instances=[pretty_instance(i) for i in instances], - ) - elif state in ('restarted', 'rebooted'): - changed, failed, instances, failure_reason = change_instance_state( - filters=module.params.get('filters'), - desired_state='STOPPED') - changed, failed, instances, failure_reason = change_instance_state( - filters=module.params.get('filters'), - desired_state='RUNNING') - - if failed: - module.fail_json( - msg="Unable to restart instances: {0}".format(failure_reason), - reboot_success=list(changed), - reboot_failed=failed) - - module.exit_json( - msg='Instances restarted', - reboot_success=list(changed), - changed=bool(len(changed)), - reboot_failed=[], - instances=[pretty_instance(i) for i in instances], - ) - elif state in ('stopped',): - changed, failed, instances, failure_reason = change_instance_state( - filters=module.params.get('filters'), - desired_state='STOPPED') - - if failed: - module.fail_json( - msg="Unable to stop instances: {0}".format(failure_reason), - stop_success=list(changed), - stop_failed=failed) - - module.exit_json( - msg='Instances stopped', - stop_success=list(changed), - changed=bool(len(changed)), - stop_failed=[], - instances=[pretty_instance(i) for i in instances], - ) - elif state in ('absent', 'terminated'): - terminated, terminate_failed, instances, failure_reason = change_instance_state( - filters=module.params.get('filters'), - desired_state='TERMINATED') - - if terminate_failed: - module.fail_json( - msg="Unable to terminate instances: {0}".format(failure_reason), - terminate_success=list(terminated), - terminate_failed=terminate_failed) - module.exit_json( - msg='Instances terminated', - terminate_success=list(terminated), - changed=bool(len(terminated)), - terminate_failed=[], - instances=[pretty_instance(i) for i in instances], - ) - - -@AWSRetry.jittered_backoff() -def change_instance_state(filters, desired_state, ec2=None): - """Takes STOPPED/RUNNING/TERMINATED""" - if ec2 is None: - ec2 = module.client('ec2') - - changed = set() - instances = find_instances(ec2, filters=filters) - to_change = set(i['InstanceId'] for i in instances if i['State']['Name'].upper() != desired_state) - unchanged = set() - failure_reason = "" - - for inst in instances: - try: - if desired_state == 'TERMINATED': - if module.check_mode: - changed.add(inst['InstanceId']) - continue - - # TODO use a client-token to prevent double-sends of these start/stop/terminate commands - # https://docs.aws.amazon.com/AWSEC2/latest/APIReference/Run_Instance_Idempotency.html - resp = ec2.terminate_instances(InstanceIds=[inst['InstanceId']]) - [changed.add(i['InstanceId']) for i in resp['TerminatingInstances']] - if desired_state == 'STOPPED': - if inst['State']['Name'] in ('stopping', 'stopped'): - unchanged.add(inst['InstanceId']) - continue - - if module.check_mode: - changed.add(inst['InstanceId']) - continue - - resp = ec2.stop_instances(InstanceIds=[inst['InstanceId']]) - [changed.add(i['InstanceId']) for i in resp['StoppingInstances']] - if desired_state == 'RUNNING': - if module.check_mode: - changed.add(inst['InstanceId']) - continue - - resp = ec2.start_instances(InstanceIds=[inst['InstanceId']]) - [changed.add(i['InstanceId']) for i in resp['StartingInstances']] - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - try: - failure_reason = to_native(e.message) - except AttributeError: - failure_reason = to_native(e) - - if changed: - await_instances(ids=list(changed) + list(unchanged), state=desired_state) - - change_failed = list(to_change - changed) - instances = find_instances(ec2, ids=list(i['InstanceId'] for i in instances)) - return changed, change_failed, instances, failure_reason - - -def pretty_instance(i): - instance = camel_dict_to_snake_dict(i, ignore_list=['Tags']) - instance['tags'] = boto3_tag_list_to_ansible_dict(i['Tags']) - return instance - - -def determine_iam_role(name_or_arn): - if re.match(r'^arn:aws:iam::\d+:instance-profile/[\w+=/,.@-]+$', name_or_arn): - return name_or_arn - iam = module.client('iam', retry_decorator=AWSRetry.jittered_backoff()) - try: - role = iam.get_instance_profile(InstanceProfileName=name_or_arn, aws_retry=True) - return role['InstanceProfile']['Arn'] - except botocore.exceptions.ClientError as e: - if e.response['Error']['Code'] == 'NoSuchEntity': - module.fail_json_aws(e, msg="Could not find instance_role {0}".format(name_or_arn)) - module.fail_json_aws(e, msg="An error occurred while searching for instance_role {0}. Please try supplying the full ARN.".format(name_or_arn)) - - -def handle_existing(existing_matches, changed, ec2, state): - if state in ('running', 'started') and [i for i in existing_matches if i['State']['Name'] != 'running']: - ins_changed, failed, instances, failure_reason = change_instance_state(filters=module.params.get('filters'), desired_state='RUNNING') - if failed: - module.fail_json(msg="Couldn't start instances: {0}. Failure reason: {1}".format(instances, failure_reason)) - module.exit_json( - changed=bool(len(ins_changed)) or changed, - instances=[pretty_instance(i) for i in instances], - instance_ids=[i['InstanceId'] for i in instances], - ) - changes = diff_instance_and_params(existing_matches[0], module.params) - for c in changes: - AWSRetry.jittered_backoff()(ec2.modify_instance_attribute)(**c) - changed |= bool(changes) - changed |= add_or_update_instance_profile(existing_matches[0], module.params.get('instance_role')) - changed |= change_network_attachments(existing_matches[0], module.params, ec2) - altered = find_instances(ec2, ids=[i['InstanceId'] for i in existing_matches]) - module.exit_json( - changed=bool(len(changes)) or changed, - instances=[pretty_instance(i) for i in altered], - instance_ids=[i['InstanceId'] for i in altered], - changes=changes, - ) - - -def ensure_present(existing_matches, changed, ec2, state): - if len(existing_matches): - try: - handle_existing(existing_matches, changed, ec2, state) - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - module.fail_json_aws( - e, msg="Failed to handle existing instances {0}".format(', '.join([i['InstanceId'] for i in existing_matches])), - # instances=[pretty_instance(i) for i in existing_matches], - # instance_ids=[i['InstanceId'] for i in existing_matches], - ) - try: - instance_spec = build_run_instance_spec(module.params) - # If check mode is enabled,suspend 'ensure function'. - if module.check_mode: - module.exit_json( - changed=True, - spec=instance_spec, - ) - instance_response = run_instances(ec2, **instance_spec) - instances = instance_response['Instances'] - instance_ids = [i['InstanceId'] for i in instances] - - for ins in instances: - changes = diff_instance_and_params(ins, module.params, skip=['UserData', 'EbsOptimized']) - for c in changes: - try: - AWSRetry.jittered_backoff()(ec2.modify_instance_attribute)(**c) - except botocore.exceptions.ClientError as e: - module.fail_json_aws(e, msg="Could not apply change {0} to new instance.".format(str(c))) - - if not module.params.get('wait'): - module.exit_json( - changed=True, - instance_ids=instance_ids, - spec=instance_spec, - ) - await_instances(instance_ids) - instances = ec2.get_paginator('describe_instances').paginate( - InstanceIds=instance_ids - ).search('Reservations[].Instances[]') - - module.exit_json( - changed=True, - instances=[pretty_instance(i) for i in instances], - instance_ids=instance_ids, - spec=instance_spec, - ) - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - module.fail_json_aws(e, msg="Failed to create new EC2 instance") - - -@AWSRetry.jittered_backoff() -def run_instances(ec2, **instance_spec): - try: - return ec2.run_instances(**instance_spec) - except botocore.exceptions.ClientError as e: - if e.response['Error']['Code'] == 'InvalidParameterValue' and "Invalid IAM Instance Profile ARN" in e.response['Error']['Message']: - # If the instance profile has just been created, it takes some time to be visible by ec2 - # So we wait 10 second and retry the run_instances - time.sleep(10) - return ec2.run_instances(**instance_spec) - else: - raise e - - -def main(): - global module - argument_spec = dict( - state=dict(default='present', choices=['present', 'started', 'running', 'stopped', 'restarted', 'rebooted', 'terminated', 'absent']), - wait=dict(default=True, type='bool'), - wait_timeout=dict(default=600, type='int'), - # count=dict(default=1, type='int'), - image=dict(type='dict'), - image_id=dict(type='str'), - instance_type=dict(default='t2.micro', type='str'), - user_data=dict(type='str'), - tower_callback=dict(type='dict'), - ebs_optimized=dict(type='bool'), - vpc_subnet_id=dict(type='str', aliases=['subnet_id']), - availability_zone=dict(type='str'), - security_groups=dict(default=[], type='list'), - security_group=dict(type='str'), - instance_role=dict(type='str'), - name=dict(type='str'), - tags=dict(type='dict'), - purge_tags=dict(type='bool', default=False), - filters=dict(type='dict', default=None), - launch_template=dict(type='dict'), - key_name=dict(type='str'), - cpu_credit_specification=dict(type='str', choices=['standard', 'unlimited']), - cpu_options=dict(type='dict', options=dict( - core_count=dict(type='int', required=True), - threads_per_core=dict(type='int', choices=[1, 2], required=True) - )), - tenancy=dict(type='str', choices=['dedicated', 'default']), - placement_group=dict(type='str'), - instance_initiated_shutdown_behavior=dict(type='str', choices=['stop', 'terminate']), - termination_protection=dict(type='bool'), - detailed_monitoring=dict(type='bool'), - instance_ids=dict(default=[], type='list'), - network=dict(default=None, type='dict'), - volumes=dict(default=None, type='list'), - ) - # running/present are synonyms - # as are terminated/absent - module = AnsibleAWSModule( - argument_spec=argument_spec, - mutually_exclusive=[ - ['security_groups', 'security_group'], - ['availability_zone', 'vpc_subnet_id'], - ['tower_callback', 'user_data'], - ['image_id', 'image'], - ], - supports_check_mode=True - ) - - if module.params.get('network'): - if module.params.get('network').get('interfaces'): - if module.params.get('security_group'): - module.fail_json(msg="Parameter network.interfaces can't be used with security_group") - if module.params.get('security_groups'): - module.fail_json(msg="Parameter network.interfaces can't be used with security_groups") - - state = module.params.get('state') - ec2 = module.client('ec2') - if module.params.get('filters') is None: - filters = { - # all states except shutting-down and terminated - 'instance-state-name': ['pending', 'running', 'stopping', 'stopped'] - } - if state == 'stopped': - # only need to change instances that aren't already stopped - filters['instance-state-name'] = ['stopping', 'pending', 'running'] - - if isinstance(module.params.get('instance_ids'), string_types): - filters['instance-id'] = [module.params.get('instance_ids')] - elif isinstance(module.params.get('instance_ids'), list) and len(module.params.get('instance_ids')): - filters['instance-id'] = module.params.get('instance_ids') - else: - if not module.params.get('vpc_subnet_id'): - if module.params.get('network'): - # grab AZ from one of the ENIs - ints = module.params.get('network').get('interfaces') - if ints: - filters['network-interface.network-interface-id'] = [] - for i in ints: - if isinstance(i, dict): - i = i['id'] - filters['network-interface.network-interface-id'].append(i) - else: - sub = get_default_subnet(ec2, get_default_vpc(ec2), availability_zone=module.params.get('availability_zone')) - filters['subnet-id'] = sub['SubnetId'] - else: - filters['subnet-id'] = [module.params.get('vpc_subnet_id')] - - if module.params.get('name'): - filters['tag:Name'] = [module.params.get('name')] - - if module.params.get('image_id'): - filters['image-id'] = [module.params.get('image_id')] - elif (module.params.get('image') or {}).get('id'): - filters['image-id'] = [module.params.get('image', {}).get('id')] - - module.params['filters'] = filters - - if module.params.get('cpu_options') and not module.botocore_at_least('1.10.16'): - module.fail_json(msg="cpu_options is only supported with botocore >= 1.10.16") - - existing_matches = find_instances(ec2, filters=module.params.get('filters')) - changed = False - - if state not in ('terminated', 'absent') and existing_matches: - for match in existing_matches: - warn_if_public_ip_assignment_changed(match) - warn_if_cpu_options_changed(match) - tags = module.params.get('tags') or {} - name = module.params.get('name') - if name: - tags['Name'] = name - changed |= manage_tags(match, tags, module.params.get('purge_tags', False), ec2) - - if state in ('present', 'running', 'started'): - ensure_present(existing_matches=existing_matches, changed=changed, ec2=ec2, state=state) - elif state in ('restarted', 'rebooted', 'stopped', 'absent', 'terminated'): - if existing_matches: - ensure_instance_state(state, ec2) - else: - module.exit_json( - msg='No matching instances found', - changed=False, - instances=[], - ) - else: - module.fail_json(msg="We don't handle the state {0}".format(state)) - - -if __name__ == '__main__': - main() diff --git a/test/support/integration/plugins/modules/ec2_instance_info.py b/test/support/integration/plugins/modules/ec2_instance_info.py deleted file mode 100644 index 0836ef3b80..0000000000 --- a/test/support/integration/plugins/modules/ec2_instance_info.py +++ /dev/null @@ -1,572 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = ''' ---- -module: ec2_instance_info -short_description: Gather information about ec2 instances in AWS -description: - - Gather information about ec2 instances in AWS - - This module was called C(ec2_instance_facts) before Ansible 2.9. The usage did not change. -version_added: "2.4" -author: - - Michael Schuett (@michaeljs1990) - - Rob White (@wimnat) -requirements: [ "boto3", "botocore" ] -options: - instance_ids: - description: - - If you specify one or more instance IDs, only instances that have the specified IDs are returned. - required: false - version_added: 2.4 - type: list - filters: - description: - - A dict of filters to apply. Each dict item consists of a filter key and a filter value. See - U(https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeInstances.html) for possible filters. Filter - names and values are case sensitive. - required: false - default: {} - type: dict - -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -# Gather information about all instances -- ec2_instance_info: - -# Gather information about all instances in AZ ap-southeast-2a -- ec2_instance_info: - filters: - availability-zone: ap-southeast-2a - -# Gather information about a particular instance using ID -- ec2_instance_info: - instance_ids: - - i-12345678 - -# Gather information about any instance with a tag key Name and value Example -- ec2_instance_info: - filters: - "tag:Name": Example - -# Gather information about any instance in states "shutting-down", "stopping", "stopped" -- ec2_instance_info: - filters: - instance-state-name: [ "shutting-down", "stopping", "stopped" ] - -''' - -RETURN = ''' -instances: - description: a list of ec2 instances - returned: always - type: complex - contains: - ami_launch_index: - description: The AMI launch index, which can be used to find this instance in the launch group. - returned: always - type: int - sample: 0 - architecture: - description: The architecture of the image - returned: always - type: str - sample: x86_64 - block_device_mappings: - description: Any block device mapping entries for the instance. - returned: always - type: complex - contains: - device_name: - description: The device name exposed to the instance (for example, /dev/sdh or xvdh). - returned: always - type: str - sample: /dev/sdh - ebs: - description: Parameters used to automatically set up EBS volumes when the instance is launched. - returned: always - type: complex - contains: - attach_time: - description: The time stamp when the attachment initiated. - returned: always - type: str - sample: "2017-03-23T22:51:24+00:00" - delete_on_termination: - description: Indicates whether the volume is deleted on instance termination. - returned: always - type: bool - sample: true - status: - description: The attachment state. - returned: always - type: str - sample: attached - volume_id: - description: The ID of the EBS volume - returned: always - type: str - sample: vol-12345678 - cpu_options: - description: The CPU options set for the instance. - returned: always if botocore version >= 1.10.16 - type: complex - contains: - core_count: - description: The number of CPU cores for the instance. - returned: always - type: int - sample: 1 - threads_per_core: - description: The number of threads per CPU core. On supported instance, a value of 1 means Intel Hyper-Threading Technology is disabled. - returned: always - type: int - sample: 1 - client_token: - description: The idempotency token you provided when you launched the instance, if applicable. - returned: always - type: str - sample: mytoken - ebs_optimized: - description: Indicates whether the instance is optimized for EBS I/O. - returned: always - type: bool - sample: false - hypervisor: - description: The hypervisor type of the instance. - returned: always - type: str - sample: xen - iam_instance_profile: - description: The IAM instance profile associated with the instance, if applicable. - returned: always - type: complex - contains: - arn: - description: The Amazon Resource Name (ARN) of the instance profile. - returned: always - type: str - sample: "arn:aws:iam::000012345678:instance-profile/myprofile" - id: - description: The ID of the instance profile - returned: always - type: str - sample: JFJ397FDG400FG9FD1N - image_id: - description: The ID of the AMI used to launch the instance. - returned: always - type: str - sample: ami-0011223344 - instance_id: - description: The ID of the instance. - returned: always - type: str - sample: i-012345678 - instance_type: - description: The instance type size of the running instance. - returned: always - type: str - sample: t2.micro - key_name: - description: The name of the key pair, if this instance was launched with an associated key pair. - returned: always - type: str - sample: my-key - launch_time: - description: The time the instance was launched. - returned: always - type: str - sample: "2017-03-23T22:51:24+00:00" - monitoring: - description: The monitoring for the instance. - returned: always - type: complex - contains: - state: - description: Indicates whether detailed monitoring is enabled. Otherwise, basic monitoring is enabled. - returned: always - type: str - sample: disabled - network_interfaces: - description: One or more network interfaces for the instance. - returned: always - type: complex - contains: - association: - description: The association information for an Elastic IPv4 associated with the network interface. - returned: always - type: complex - contains: - ip_owner_id: - description: The ID of the owner of the Elastic IP address. - returned: always - type: str - sample: amazon - public_dns_name: - description: The public DNS name. - returned: always - type: str - sample: "" - public_ip: - description: The public IP address or Elastic IP address bound to the network interface. - returned: always - type: str - sample: 1.2.3.4 - attachment: - description: The network interface attachment. - returned: always - type: complex - contains: - attach_time: - description: The time stamp when the attachment initiated. - returned: always - type: str - sample: "2017-03-23T22:51:24+00:00" - attachment_id: - description: The ID of the network interface attachment. - returned: always - type: str - sample: eni-attach-3aff3f - delete_on_termination: - description: Indicates whether the network interface is deleted when the instance is terminated. - returned: always - type: bool - sample: true - device_index: - description: The index of the device on the instance for the network interface attachment. - returned: always - type: int - sample: 0 - status: - description: The attachment state. - returned: always - type: str - sample: attached - description: - description: The description. - returned: always - type: str - sample: My interface - groups: - description: One or more security groups. - returned: always - type: list - elements: dict - contains: - group_id: - description: The ID of the security group. - returned: always - type: str - sample: sg-abcdef12 - group_name: - description: The name of the security group. - returned: always - type: str - sample: mygroup - ipv6_addresses: - description: One or more IPv6 addresses associated with the network interface. - returned: always - type: list - elements: dict - contains: - ipv6_address: - description: The IPv6 address. - returned: always - type: str - sample: "2001:0db8:85a3:0000:0000:8a2e:0370:7334" - mac_address: - description: The MAC address. - returned: always - type: str - sample: "00:11:22:33:44:55" - network_interface_id: - description: The ID of the network interface. - returned: always - type: str - sample: eni-01234567 - owner_id: - description: The AWS account ID of the owner of the network interface. - returned: always - type: str - sample: 01234567890 - private_ip_address: - description: The IPv4 address of the network interface within the subnet. - returned: always - type: str - sample: 10.0.0.1 - private_ip_addresses: - description: The private IPv4 addresses associated with the network interface. - returned: always - type: list - elements: dict - contains: - association: - description: The association information for an Elastic IP address (IPv4) associated with the network interface. - returned: always - type: complex - contains: - ip_owner_id: - description: The ID of the owner of the Elastic IP address. - returned: always - type: str - sample: amazon - public_dns_name: - description: The public DNS name. - returned: always - type: str - sample: "" - public_ip: - description: The public IP address or Elastic IP address bound to the network interface. - returned: always - type: str - sample: 1.2.3.4 - primary: - description: Indicates whether this IPv4 address is the primary private IP address of the network interface. - returned: always - type: bool - sample: true - private_ip_address: - description: The private IPv4 address of the network interface. - returned: always - type: str - sample: 10.0.0.1 - source_dest_check: - description: Indicates whether source/destination checking is enabled. - returned: always - type: bool - sample: true - status: - description: The status of the network interface. - returned: always - type: str - sample: in-use - subnet_id: - description: The ID of the subnet for the network interface. - returned: always - type: str - sample: subnet-0123456 - vpc_id: - description: The ID of the VPC for the network interface. - returned: always - type: str - sample: vpc-0123456 - placement: - description: The location where the instance launched, if applicable. - returned: always - type: complex - contains: - availability_zone: - description: The Availability Zone of the instance. - returned: always - type: str - sample: ap-southeast-2a - group_name: - description: The name of the placement group the instance is in (for cluster compute instances). - returned: always - type: str - sample: "" - tenancy: - description: The tenancy of the instance (if the instance is running in a VPC). - returned: always - type: str - sample: default - private_dns_name: - description: The private DNS name. - returned: always - type: str - sample: ip-10-0-0-1.ap-southeast-2.compute.internal - private_ip_address: - description: The IPv4 address of the network interface within the subnet. - returned: always - type: str - sample: 10.0.0.1 - product_codes: - description: One or more product codes. - returned: always - type: list - elements: dict - contains: - product_code_id: - description: The product code. - returned: always - type: str - sample: aw0evgkw8ef3n2498gndfgasdfsd5cce - product_code_type: - description: The type of product code. - returned: always - type: str - sample: marketplace - public_dns_name: - description: The public DNS name assigned to the instance. - returned: always - type: str - sample: - public_ip_address: - description: The public IPv4 address assigned to the instance - returned: always - type: str - sample: 52.0.0.1 - root_device_name: - description: The device name of the root device - returned: always - type: str - sample: /dev/sda1 - root_device_type: - description: The type of root device used by the AMI. - returned: always - type: str - sample: ebs - security_groups: - description: One or more security groups for the instance. - returned: always - type: list - elements: dict - contains: - group_id: - description: The ID of the security group. - returned: always - type: str - sample: sg-0123456 - group_name: - description: The name of the security group. - returned: always - type: str - sample: my-security-group - source_dest_check: - description: Indicates whether source/destination checking is enabled. - returned: always - type: bool - sample: true - state: - description: The current state of the instance. - returned: always - type: complex - contains: - code: - description: The low byte represents the state. - returned: always - type: int - sample: 16 - name: - description: The name of the state. - returned: always - type: str - sample: running - state_transition_reason: - description: The reason for the most recent state transition. - returned: always - type: str - sample: - subnet_id: - description: The ID of the subnet in which the instance is running. - returned: always - type: str - sample: subnet-00abcdef - tags: - description: Any tags assigned to the instance. - returned: always - type: dict - sample: - virtualization_type: - description: The type of virtualization of the AMI. - returned: always - type: str - sample: hvm - vpc_id: - description: The ID of the VPC the instance is in. - returned: always - type: dict - sample: vpc-0011223344 -''' - -import traceback - -try: - import boto3 - from botocore.exceptions import ClientError - HAS_BOTO3 = True -except ImportError: - HAS_BOTO3 = False - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import (ansible_dict_to_boto3_filter_list, - boto3_conn, boto3_tag_list_to_ansible_dict, camel_dict_to_snake_dict, - ec2_argument_spec, get_aws_connection_info) - - -def list_ec2_instances(connection, module): - - instance_ids = module.params.get("instance_ids") - filters = ansible_dict_to_boto3_filter_list(module.params.get("filters")) - - try: - reservations_paginator = connection.get_paginator('describe_instances') - reservations = reservations_paginator.paginate(InstanceIds=instance_ids, Filters=filters).build_full_result() - except ClientError as e: - module.fail_json(msg=e.message, exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) - - # Get instances from reservations - instances = [] - for reservation in reservations['Reservations']: - instances = instances + reservation['Instances'] - - # Turn the boto3 result in to ansible_friendly_snaked_names - snaked_instances = [camel_dict_to_snake_dict(instance) for instance in instances] - - # Turn the boto3 result in to ansible friendly tag dictionary - for instance in snaked_instances: - instance['tags'] = boto3_tag_list_to_ansible_dict(instance.get('tags', []), 'key', 'value') - - module.exit_json(instances=snaked_instances) - - -def main(): - - argument_spec = ec2_argument_spec() - argument_spec.update( - dict( - instance_ids=dict(default=[], type='list'), - filters=dict(default={}, type='dict') - ) - ) - - module = AnsibleModule(argument_spec=argument_spec, - mutually_exclusive=[ - ['instance_ids', 'filters'] - ], - supports_check_mode=True - ) - if module._name == 'ec2_instance_facts': - module.deprecate("The 'ec2_instance_facts' module has been renamed to 'ec2_instance_info'", - version='2.13', collection_name='ansible.builtin') - - if not HAS_BOTO3: - module.fail_json(msg='boto3 required for this module') - - region, ec2_url, aws_connect_params = get_aws_connection_info(module, boto3=True) - - if region: - connection = boto3_conn(module, conn_type='client', resource='ec2', region=region, endpoint=ec2_url, **aws_connect_params) - else: - module.fail_json(msg="region must be specified") - - list_ec2_instances(connection, module) - - -if __name__ == '__main__': - main() diff --git a/test/support/integration/plugins/modules/ec2_key.py b/test/support/integration/plugins/modules/ec2_key.py deleted file mode 100644 index de67af8bc0..0000000000 --- a/test/support/integration/plugins/modules/ec2_key.py +++ /dev/null @@ -1,271 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['stableinterface'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: ec2_key -version_added: "1.5" -short_description: create or delete an ec2 key pair -description: - - create or delete an ec2 key pair. -options: - name: - description: - - Name of the key pair. - required: true - type: str - key_material: - description: - - Public key material. - required: false - type: str - force: - description: - - Force overwrite of already existing key pair if key has changed. - required: false - default: true - type: bool - version_added: "2.3" - state: - description: - - create or delete keypair - required: false - choices: [ present, absent ] - default: 'present' - type: str - wait: - description: - - This option has no effect since version 2.5 and will be removed in 2.14. - version_added: "1.6" - type: bool - wait_timeout: - description: - - This option has no effect since version 2.5 and will be removed in 2.14. - version_added: "1.6" - type: int - required: false - -extends_documentation_fragment: - - aws - - ec2 -requirements: [ boto3 ] -author: - - "Vincent Viallet (@zbal)" - - "Prasad Katti (@prasadkatti)" -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -- name: create a new ec2 key pair, returns generated private key - ec2_key: - name: my_keypair - -- name: create key pair using provided key_material - ec2_key: - name: my_keypair - key_material: 'ssh-rsa AAAAxyz...== me@example.com' - -- name: create key pair using key_material obtained using 'file' lookup plugin - ec2_key: - name: my_keypair - key_material: "{{ lookup('file', '/path/to/public_key/id_rsa.pub') }}" - -# try creating a key pair with the name of an already existing keypair -# but don't overwrite it even if the key is different (force=false) -- name: try creating a key pair with name of an already existing keypair - ec2_key: - name: my_existing_keypair - key_material: 'ssh-rsa AAAAxyz...== me@example.com' - force: false - -- name: remove key pair by name - ec2_key: - name: my_keypair - state: absent -''' - -RETURN = ''' -changed: - description: whether a keypair was created/deleted - returned: always - type: bool - sample: true -msg: - description: short message describing the action taken - returned: always - type: str - sample: key pair created -key: - description: details of the keypair (this is set to null when state is absent) - returned: always - type: complex - contains: - fingerprint: - description: fingerprint of the key - returned: when state is present - type: str - sample: 'b0:22:49:61:d9:44:9d:0c:7e:ac:8a:32:93:21:6c:e8:fb:59:62:43' - name: - description: name of the keypair - returned: when state is present - type: str - sample: my_keypair - private_key: - description: private key of a newly created keypair - returned: when a new keypair is created by AWS (key_material is not provided) - type: str - sample: '-----BEGIN RSA PRIVATE KEY----- - MIIEowIBAAKC... - -----END RSA PRIVATE KEY-----' -''' - -import uuid - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils._text import to_bytes - -try: - from botocore.exceptions import ClientError -except ImportError: - pass # caught by AnsibleAWSModule - - -def extract_key_data(key): - - data = { - 'name': key['KeyName'], - 'fingerprint': key['KeyFingerprint'] - } - if 'KeyMaterial' in key: - data['private_key'] = key['KeyMaterial'] - return data - - -def get_key_fingerprint(module, ec2_client, key_material): - ''' - EC2's fingerprints are non-trivial to generate, so push this key - to a temporary name and make ec2 calculate the fingerprint for us. - http://blog.jbrowne.com/?p=23 - https://forums.aws.amazon.com/thread.jspa?messageID=352828 - ''' - - # find an unused name - name_in_use = True - while name_in_use: - random_name = "ansible-" + str(uuid.uuid4()) - name_in_use = find_key_pair(module, ec2_client, random_name) - - temp_key = import_key_pair(module, ec2_client, random_name, key_material) - delete_key_pair(module, ec2_client, random_name, finish_task=False) - return temp_key['KeyFingerprint'] - - -def find_key_pair(module, ec2_client, name): - - try: - key = ec2_client.describe_key_pairs(KeyNames=[name])['KeyPairs'][0] - except ClientError as err: - if err.response['Error']['Code'] == "InvalidKeyPair.NotFound": - return None - module.fail_json_aws(err, msg="error finding keypair") - except IndexError: - key = None - return key - - -def create_key_pair(module, ec2_client, name, key_material, force): - - key = find_key_pair(module, ec2_client, name) - if key: - if key_material and force: - if not module.check_mode: - new_fingerprint = get_key_fingerprint(module, ec2_client, key_material) - if key['KeyFingerprint'] != new_fingerprint: - delete_key_pair(module, ec2_client, name, finish_task=False) - key = import_key_pair(module, ec2_client, name, key_material) - key_data = extract_key_data(key) - module.exit_json(changed=True, key=key_data, msg="key pair updated") - else: - # Assume a change will be made in check mode since a comparison can't be done - module.exit_json(changed=True, key=extract_key_data(key), msg="key pair updated") - key_data = extract_key_data(key) - module.exit_json(changed=False, key=key_data, msg="key pair already exists") - else: - # key doesn't exist, create it now - key_data = None - if not module.check_mode: - if key_material: - key = import_key_pair(module, ec2_client, name, key_material) - else: - try: - key = ec2_client.create_key_pair(KeyName=name) - except ClientError as err: - module.fail_json_aws(err, msg="error creating key") - key_data = extract_key_data(key) - module.exit_json(changed=True, key=key_data, msg="key pair created") - - -def import_key_pair(module, ec2_client, name, key_material): - - try: - key = ec2_client.import_key_pair(KeyName=name, PublicKeyMaterial=to_bytes(key_material)) - except ClientError as err: - module.fail_json_aws(err, msg="error importing key") - return key - - -def delete_key_pair(module, ec2_client, name, finish_task=True): - - key = find_key_pair(module, ec2_client, name) - if key: - if not module.check_mode: - try: - ec2_client.delete_key_pair(KeyName=name) - except ClientError as err: - module.fail_json_aws(err, msg="error deleting key") - if not finish_task: - return - module.exit_json(changed=True, key=None, msg="key deleted") - module.exit_json(key=None, msg="key did not exist") - - -def main(): - - argument_spec = dict( - name=dict(required=True), - key_material=dict(), - force=dict(type='bool', default=True), - state=dict(default='present', choices=['present', 'absent']), - wait=dict(type='bool', removed_in_version='2.14'), - wait_timeout=dict(type='int', removed_in_version='2.14') - ) - - module = AnsibleAWSModule(argument_spec=argument_spec, supports_check_mode=True) - - ec2_client = module.client('ec2') - - name = module.params['name'] - state = module.params.get('state') - key_material = module.params.get('key_material') - force = module.params.get('force') - - if state == 'absent': - delete_key_pair(module, ec2_client, name) - elif state == 'present': - create_key_pair(module, ec2_client, name, key_material, force) - - -if __name__ == '__main__': - main() diff --git a/test/support/integration/plugins/modules/ec2_vpc_igw.py b/test/support/integration/plugins/modules/ec2_vpc_igw.py deleted file mode 100644 index 5198527af7..0000000000 --- a/test/support/integration/plugins/modules/ec2_vpc_igw.py +++ /dev/null @@ -1,283 +0,0 @@ -#!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['stableinterface'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: ec2_vpc_igw -short_description: Manage an AWS VPC Internet gateway -description: - - Manage an AWS VPC Internet gateway -version_added: "2.0" -author: Robert Estelle (@erydo) -options: - vpc_id: - description: - - The VPC ID for the VPC in which to manage the Internet Gateway. - required: true - type: str - tags: - description: - - "A dict of tags to apply to the internet gateway. Any tags currently applied to the internet gateway and not present here will be removed." - aliases: [ 'resource_tags' ] - version_added: "2.4" - type: dict - state: - description: - - Create or terminate the IGW - default: present - choices: [ 'present', 'absent' ] - type: str -extends_documentation_fragment: - - aws - - ec2 -requirements: - - botocore - - boto3 -''' - -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 {{igw.gateway_id}} for use in setting up NATs etc. -ec2_vpc_igw: - vpc_id: vpc-abcdefgh - state: present -register: igw - -''' - -RETURN = ''' -changed: - description: If any changes have been made to the Internet Gateway. - type: bool - returned: always - sample: - changed: false -gateway_id: - description: The unique identifier for the Internet Gateway. - type: str - returned: I(state=present) - sample: - gateway_id: "igw-XXXXXXXX" -tags: - description: The tags associated the Internet Gateway. - type: dict - returned: I(state=present) - sample: - tags: - "Ansible": "Test" -vpc_id: - description: The VPC ID associated with the Internet Gateway. - type: str - returned: I(state=present) - sample: - vpc_id: "vpc-XXXXXXXX" -''' - -try: - import botocore -except ImportError: - pass # caught by AnsibleAWSModule - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.aws.waiters import get_waiter -from ansible.module_utils.ec2 import ( - AWSRetry, - camel_dict_to_snake_dict, - boto3_tag_list_to_ansible_dict, - ansible_dict_to_boto3_filter_list, - ansible_dict_to_boto3_tag_list, - compare_aws_tags -) -from ansible.module_utils.six import string_types - - -class AnsibleEc2Igw(object): - - def __init__(self, module, results): - self._module = module - self._results = results - self._connection = self._module.client('ec2') - self._check_mode = self._module.check_mode - - def process(self): - vpc_id = self._module.params.get('vpc_id') - state = self._module.params.get('state', 'present') - tags = self._module.params.get('tags') - - if state == 'present': - self.ensure_igw_present(vpc_id, tags) - elif state == 'absent': - self.ensure_igw_absent(vpc_id) - - def get_matching_igw(self, vpc_id): - filters = ansible_dict_to_boto3_filter_list({'attachment.vpc-id': vpc_id}) - igws = [] - try: - response = self._connection.describe_internet_gateways(Filters=filters) - igws = response.get('InternetGateways', []) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - self._module.fail_json_aws(e) - - igw = None - if len(igws) > 1: - self._module.fail_json( - msg='EC2 returned more than one Internet Gateway for VPC {0}, aborting'.format(vpc_id)) - elif igws: - igw = camel_dict_to_snake_dict(igws[0]) - - return igw - - def check_input_tags(self, tags): - nonstring_tags = [k for k, v in tags.items() if not isinstance(v, string_types)] - if nonstring_tags: - self._module.fail_json(msg='One or more tags contain non-string values: {0}'.format(nonstring_tags)) - - def ensure_tags(self, igw_id, tags, add_only): - final_tags = [] - - filters = ansible_dict_to_boto3_filter_list({'resource-id': igw_id, 'resource-type': 'internet-gateway'}) - cur_tags = None - try: - cur_tags = self._connection.describe_tags(Filters=filters) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - self._module.fail_json_aws(e, msg="Couldn't describe tags") - - purge_tags = bool(not add_only) - to_update, to_delete = compare_aws_tags(boto3_tag_list_to_ansible_dict(cur_tags.get('Tags')), tags, purge_tags) - final_tags = boto3_tag_list_to_ansible_dict(cur_tags.get('Tags')) - - if to_update: - try: - if self._check_mode: - # update tags - final_tags.update(to_update) - else: - AWSRetry.exponential_backoff()(self._connection.create_tags)( - Resources=[igw_id], - Tags=ansible_dict_to_boto3_tag_list(to_update) - ) - - self._results['changed'] = True - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - self._module.fail_json_aws(e, msg="Couldn't create tags") - - if to_delete: - try: - if self._check_mode: - # update tags - for key in to_delete: - del final_tags[key] - else: - tags_list = [] - for key in to_delete: - tags_list.append({'Key': key}) - - AWSRetry.exponential_backoff()(self._connection.delete_tags)(Resources=[igw_id], Tags=tags_list) - - self._results['changed'] = True - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - self._module.fail_json_aws(e, msg="Couldn't delete tags") - - if not self._check_mode and (to_update or to_delete): - try: - response = self._connection.describe_tags(Filters=filters) - final_tags = boto3_tag_list_to_ansible_dict(response.get('Tags')) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - self._module.fail_json_aws(e, msg="Couldn't describe tags") - - return final_tags - - @staticmethod - def get_igw_info(igw): - return { - 'gateway_id': igw['internet_gateway_id'], - 'tags': igw['tags'], - 'vpc_id': igw['vpc_id'] - } - - def ensure_igw_absent(self, vpc_id): - igw = self.get_matching_igw(vpc_id) - if igw is None: - return self._results - - if self._check_mode: - self._results['changed'] = True - return self._results - - try: - self._results['changed'] = True - self._connection.detach_internet_gateway(InternetGatewayId=igw['internet_gateway_id'], VpcId=vpc_id) - self._connection.delete_internet_gateway(InternetGatewayId=igw['internet_gateway_id']) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - self._module.fail_json_aws(e, msg="Unable to delete Internet Gateway") - - return self._results - - def ensure_igw_present(self, vpc_id, tags): - self.check_input_tags(tags) - - igw = self.get_matching_igw(vpc_id) - - if igw is None: - if self._check_mode: - self._results['changed'] = True - self._results['gateway_id'] = None - return self._results - - try: - response = self._connection.create_internet_gateway() - - # Ensure the gateway exists before trying to attach it or add tags - waiter = get_waiter(self._connection, 'internet_gateway_exists') - waiter.wait(InternetGatewayIds=[response['InternetGateway']['InternetGatewayId']]) - - igw = camel_dict_to_snake_dict(response['InternetGateway']) - self._connection.attach_internet_gateway(InternetGatewayId=igw['internet_gateway_id'], VpcId=vpc_id) - self._results['changed'] = True - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - self._module.fail_json_aws(e, msg='Unable to create Internet Gateway') - - igw['vpc_id'] = vpc_id - - igw['tags'] = self.ensure_tags(igw_id=igw['internet_gateway_id'], tags=tags, add_only=False) - - igw_info = self.get_igw_info(igw) - self._results.update(igw_info) - - return self._results - - -def main(): - argument_spec = dict( - vpc_id=dict(required=True), - state=dict(default='present', choices=['present', 'absent']), - tags=dict(default=dict(), required=False, type='dict', aliases=['resource_tags']) - ) - - module = AnsibleAWSModule( - argument_spec=argument_spec, - supports_check_mode=True, - ) - results = dict( - changed=False - ) - igw_manager = AnsibleEc2Igw(module=module, results=results) - igw_manager.process() - - module.exit_json(**results) - - -if __name__ == '__main__': - main() diff --git a/test/support/integration/plugins/modules/ec2_vpc_route_table.py b/test/support/integration/plugins/modules/ec2_vpc_route_table.py deleted file mode 100644 index 96c9b2d04d..0000000000 --- a/test/support/integration/plugins/modules/ec2_vpc_route_table.py +++ /dev/null @@ -1,750 +0,0 @@ -#!/usr/bin/python -# -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['stableinterface'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: ec2_vpc_route_table -short_description: Manage route tables for AWS virtual private clouds -description: - - Manage route tables for AWS virtual private clouds -version_added: "2.0" -author: -- Robert Estelle (@erydo) -- Rob White (@wimnat) -- Will Thames (@willthames) -options: - lookup: - description: Look up route table by either tags or by route table ID. Non-unique tag lookup will fail. - If no tags are specified then no lookup for an existing route table is performed and a new - route table will be created. To change tags of a route table you must look up by id. - default: tag - choices: [ 'tag', 'id' ] - type: str - propagating_vgw_ids: - description: Enable route propagation from virtual gateways specified by ID. - type: list - elements: str - purge_routes: - version_added: "2.3" - description: Purge existing routes that are not found in routes. - type: bool - default: 'yes' - purge_subnets: - version_added: "2.3" - description: Purge existing subnets that are not found in subnets. Ignored unless the subnets option is supplied. - default: 'true' - type: bool - purge_tags: - version_added: "2.5" - description: Purge existing tags that are not found in route table. - type: bool - default: 'no' - route_table_id: - description: - - The ID of the route table to update or delete. - - Required when I(lookup=id). - type: str - routes: - description: List of routes in the route table. - Routes are specified as dicts containing the keys 'dest' and one of 'gateway_id', - 'instance_id', 'network_interface_id', or 'vpc_peering_connection_id'. - If 'gateway_id' is specified, you can refer to the VPC's IGW by using the value 'igw'. - Routes are required for present states. - type: list - elements: dict - state: - description: Create or destroy the VPC route table. - default: present - choices: [ 'present', 'absent' ] - type: str - subnets: - description: An array of subnets to add to this route table. Subnets may be specified - by either subnet ID, Name tag, or by a CIDR such as '10.0.0.0/24'. - type: list - elements: str - tags: - description: > - A dictionary of resource tags of the form: C({ tag1: value1, tag2: value2 }). Tags are - used to uniquely identify route tables within a VPC when the route_table_id is not supplied. - aliases: [ "resource_tags" ] - type: dict - vpc_id: - description: - - VPC ID of the VPC in which to create the route table. - - Required when I(state=present) or I(lookup=tag). - type: str -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -# Basic creation example: -- name: Set up public subnet route table - ec2_vpc_route_table: - vpc_id: vpc-1245678 - region: us-west-1 - tags: - Name: Public - subnets: - - "{{ jumpbox_subnet.subnet.id }}" - - "{{ frontend_subnet.subnet.id }}" - - "{{ vpn_subnet.subnet_id }}" - routes: - - dest: 0.0.0.0/0 - gateway_id: "{{ igw.gateway_id }}" - register: public_route_table - -- name: Set up NAT-protected route table - ec2_vpc_route_table: - vpc_id: vpc-1245678 - region: us-west-1 - tags: - Name: Internal - subnets: - - "{{ application_subnet.subnet.id }}" - - 'Database Subnet' - - '10.0.0.0/8' - routes: - - dest: 0.0.0.0/0 - instance_id: "{{ nat.instance_id }}" - register: nat_route_table - -- name: delete route table - ec2_vpc_route_table: - vpc_id: vpc-1245678 - region: us-west-1 - route_table_id: "{{ route_table.id }}" - lookup: id - state: absent -''' - -RETURN = ''' -route_table: - description: Route Table result - returned: always - type: complex - contains: - associations: - description: List of subnets associated with the route table - returned: always - type: complex - contains: - main: - description: Whether this is the main route table - returned: always - type: bool - sample: false - route_table_association_id: - description: ID of association between route table and subnet - returned: always - type: str - sample: rtbassoc-ab47cfc3 - route_table_id: - description: ID of the route table - returned: always - type: str - sample: rtb-bf779ed7 - subnet_id: - description: ID of the subnet - returned: always - type: str - sample: subnet-82055af9 - id: - description: ID of the route table (same as route_table_id for backwards compatibility) - returned: always - type: str - sample: rtb-bf779ed7 - propagating_vgws: - description: List of Virtual Private Gateways propagating routes - returned: always - type: list - sample: [] - route_table_id: - description: ID of the route table - returned: always - type: str - sample: rtb-bf779ed7 - routes: - description: List of routes in the route table - returned: always - type: complex - contains: - destination_cidr_block: - description: CIDR block of destination - returned: always - type: str - sample: 10.228.228.0/22 - gateway_id: - description: ID of the gateway - returned: when gateway is local or internet gateway - type: str - sample: local - instance_id: - description: ID of a NAT instance - returned: when the route is via an EC2 instance - type: str - sample: i-abcd123456789 - instance_owner_id: - description: AWS account owning the NAT instance - returned: when the route is via an EC2 instance - type: str - sample: 123456789012 - nat_gateway_id: - description: ID of the NAT gateway - returned: when the route is via a NAT gateway - type: str - sample: local - origin: - description: mechanism through which the route is in the table - returned: always - type: str - sample: CreateRouteTable - state: - description: state of the route - returned: always - type: str - sample: active - tags: - description: Tags applied to the route table - returned: always - type: dict - sample: - Name: Public route table - Public: 'true' - vpc_id: - description: ID for the VPC in which the route lives - returned: always - type: str - sample: vpc-6e2d2407 -''' - -import re -from time import sleep -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.aws.waiters import get_waiter -from ansible.module_utils.ec2 import ansible_dict_to_boto3_filter_list -from ansible.module_utils.ec2 import camel_dict_to_snake_dict, snake_dict_to_camel_dict -from ansible.module_utils.ec2 import ansible_dict_to_boto3_tag_list, boto3_tag_list_to_ansible_dict -from ansible.module_utils.ec2 import compare_aws_tags, AWSRetry - - -try: - import botocore -except ImportError: - pass # caught by AnsibleAWSModule - - -CIDR_RE = re.compile(r'^(\d{1,3}\.){3}\d{1,3}/\d{1,2}$') -SUBNET_RE = re.compile(r'^subnet-[A-z0-9]+$') -ROUTE_TABLE_RE = re.compile(r'^rtb-[A-z0-9]+$') - - -@AWSRetry.exponential_backoff() -def describe_subnets_with_backoff(connection, **params): - return connection.describe_subnets(**params)['Subnets'] - - -def find_subnets(connection, module, vpc_id, identified_subnets): - """ - Finds a list of subnets, each identified either by a raw ID, a unique - 'Name' tag, or a CIDR such as 10.0.0.0/8. - - Note that this function is duplicated in other ec2 modules, and should - potentially be moved into a shared module_utils - """ - subnet_ids = [] - subnet_names = [] - subnet_cidrs = [] - for subnet in (identified_subnets or []): - if re.match(SUBNET_RE, subnet): - subnet_ids.append(subnet) - elif re.match(CIDR_RE, subnet): - subnet_cidrs.append(subnet) - else: - subnet_names.append(subnet) - - subnets_by_id = [] - if subnet_ids: - filters = ansible_dict_to_boto3_filter_list({'vpc-id': vpc_id}) - try: - subnets_by_id = describe_subnets_with_backoff(connection, SubnetIds=subnet_ids, Filters=filters) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't find subnet with id %s" % subnet_ids) - - subnets_by_cidr = [] - if subnet_cidrs: - filters = ansible_dict_to_boto3_filter_list({'vpc-id': vpc_id, 'cidr': subnet_cidrs}) - try: - subnets_by_cidr = describe_subnets_with_backoff(connection, Filters=filters) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't find subnet with cidr %s" % subnet_cidrs) - - subnets_by_name = [] - if subnet_names: - filters = ansible_dict_to_boto3_filter_list({'vpc-id': vpc_id, 'tag:Name': subnet_names}) - try: - subnets_by_name = describe_subnets_with_backoff(connection, Filters=filters) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't find subnet with names %s" % subnet_names) - - for name in subnet_names: - matching_count = len([1 for s in subnets_by_name for t in s.get('Tags', []) if t['Key'] == 'Name' and t['Value'] == name]) - if matching_count == 0: - module.fail_json(msg='Subnet named "{0}" does not exist'.format(name)) - elif matching_count > 1: - module.fail_json(msg='Multiple subnets named "{0}"'.format(name)) - - return subnets_by_id + subnets_by_cidr + subnets_by_name - - -def find_igw(connection, module, vpc_id): - """ - Finds the Internet gateway for the given VPC ID. - """ - filters = ansible_dict_to_boto3_filter_list({'attachment.vpc-id': vpc_id}) - try: - igw = connection.describe_internet_gateways(Filters=filters)['InternetGateways'] - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg='No IGW found for VPC {0}'.format(vpc_id)) - if len(igw) == 1: - return igw[0]['InternetGatewayId'] - elif len(igw) == 0: - module.fail_json(msg='No IGWs found for VPC {0}'.format(vpc_id)) - else: - module.fail_json(msg='Multiple IGWs found for VPC {0}'.format(vpc_id)) - - -@AWSRetry.exponential_backoff() -def describe_tags_with_backoff(connection, resource_id): - filters = ansible_dict_to_boto3_filter_list({'resource-id': resource_id}) - paginator = connection.get_paginator('describe_tags') - tags = paginator.paginate(Filters=filters).build_full_result()['Tags'] - return boto3_tag_list_to_ansible_dict(tags) - - -def tags_match(match_tags, candidate_tags): - return all((k in candidate_tags and candidate_tags[k] == v - for k, v in match_tags.items())) - - -def ensure_tags(connection=None, module=None, resource_id=None, tags=None, purge_tags=None, check_mode=None): - try: - cur_tags = describe_tags_with_backoff(connection, resource_id) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg='Unable to list tags for VPC') - - to_add, to_delete = compare_aws_tags(cur_tags, tags, purge_tags) - - if not to_add and not to_delete: - return {'changed': False, 'tags': cur_tags} - if check_mode: - if not purge_tags: - tags = cur_tags.update(tags) - return {'changed': True, 'tags': tags} - - if to_delete: - try: - connection.delete_tags(Resources=[resource_id], Tags=[{'Key': k} for k in to_delete]) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't delete tags") - if to_add: - try: - connection.create_tags(Resources=[resource_id], Tags=ansible_dict_to_boto3_tag_list(to_add)) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't create tags") - - try: - latest_tags = describe_tags_with_backoff(connection, resource_id) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg='Unable to list tags for VPC') - return {'changed': True, 'tags': latest_tags} - - -@AWSRetry.exponential_backoff() -def describe_route_tables_with_backoff(connection, **params): - try: - return connection.describe_route_tables(**params)['RouteTables'] - except botocore.exceptions.ClientError as e: - if e.response['Error']['Code'] == 'InvalidRouteTableID.NotFound': - return None - else: - raise - - -def get_route_table_by_id(connection, module, route_table_id): - - route_table = None - try: - route_tables = describe_route_tables_with_backoff(connection, RouteTableIds=[route_table_id]) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't get route table") - if route_tables: - route_table = route_tables[0] - - return route_table - - -def get_route_table_by_tags(connection, module, vpc_id, tags): - count = 0 - route_table = None - filters = ansible_dict_to_boto3_filter_list({'vpc-id': vpc_id}) - try: - route_tables = describe_route_tables_with_backoff(connection, Filters=filters) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't get route table") - for table in route_tables: - this_tags = describe_tags_with_backoff(connection, table['RouteTableId']) - if tags_match(tags, this_tags): - route_table = table - count += 1 - - if count > 1: - module.fail_json(msg="Tags provided do not identify a unique route table") - else: - return route_table - - -def route_spec_matches_route(route_spec, route): - if route_spec.get('GatewayId') and 'nat-' in route_spec['GatewayId']: - route_spec['NatGatewayId'] = route_spec.pop('GatewayId') - if route_spec.get('GatewayId') and 'vpce-' in route_spec['GatewayId']: - if route_spec.get('DestinationCidrBlock', '').startswith('pl-'): - route_spec['DestinationPrefixListId'] = route_spec.pop('DestinationCidrBlock') - - return set(route_spec.items()).issubset(route.items()) - - -def route_spec_matches_route_cidr(route_spec, route): - return route_spec['DestinationCidrBlock'] == route.get('DestinationCidrBlock') - - -def rename_key(d, old_key, new_key): - d[new_key] = d.pop(old_key) - - -def index_of_matching_route(route_spec, routes_to_match): - for i, route in enumerate(routes_to_match): - if route_spec_matches_route(route_spec, route): - return "exact", i - elif 'Origin' in route_spec and route_spec['Origin'] != 'EnableVgwRoutePropagation': - if route_spec_matches_route_cidr(route_spec, route): - return "replace", i - - -def ensure_routes(connection=None, module=None, route_table=None, route_specs=None, - propagating_vgw_ids=None, check_mode=None, purge_routes=None): - routes_to_match = [route for route in route_table['Routes']] - route_specs_to_create = [] - route_specs_to_recreate = [] - for route_spec in route_specs: - match = index_of_matching_route(route_spec, routes_to_match) - if match is None: - if route_spec.get('DestinationCidrBlock'): - route_specs_to_create.append(route_spec) - else: - module.warn("Skipping creating {0} because it has no destination cidr block. " - "To add VPC endpoints to route tables use the ec2_vpc_endpoint module.".format(route_spec)) - else: - if match[0] == "replace": - if route_spec.get('DestinationCidrBlock'): - route_specs_to_recreate.append(route_spec) - else: - module.warn("Skipping recreating route {0} because it has no destination cidr block.".format(route_spec)) - del routes_to_match[match[1]] - - routes_to_delete = [] - if purge_routes: - for r in routes_to_match: - if not r.get('DestinationCidrBlock'): - module.warn("Skipping purging route {0} because it has no destination cidr block. " - "To remove VPC endpoints from route tables use the ec2_vpc_endpoint module.".format(r)) - continue - if r['Origin'] == 'CreateRoute': - routes_to_delete.append(r) - - changed = bool(routes_to_delete or route_specs_to_create or route_specs_to_recreate) - if changed and not check_mode: - for route in routes_to_delete: - try: - connection.delete_route(RouteTableId=route_table['RouteTableId'], DestinationCidrBlock=route['DestinationCidrBlock']) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't delete route") - - for route_spec in route_specs_to_recreate: - try: - connection.replace_route(RouteTableId=route_table['RouteTableId'], - **route_spec) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't recreate route") - - for route_spec in route_specs_to_create: - try: - connection.create_route(RouteTableId=route_table['RouteTableId'], - **route_spec) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't create route") - - return {'changed': bool(changed)} - - -def ensure_subnet_association(connection=None, module=None, vpc_id=None, route_table_id=None, subnet_id=None, - check_mode=None): - filters = ansible_dict_to_boto3_filter_list({'association.subnet-id': subnet_id, 'vpc-id': vpc_id}) - try: - route_tables = describe_route_tables_with_backoff(connection, Filters=filters) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't get route tables") - for route_table in route_tables: - if route_table['RouteTableId'] is None: - continue - for a in route_table['Associations']: - if a['Main']: - continue - if a['SubnetId'] == subnet_id: - if route_table['RouteTableId'] == route_table_id: - return {'changed': False, 'association_id': a['RouteTableAssociationId']} - else: - if check_mode: - return {'changed': True} - try: - connection.disassociate_route_table(AssociationId=a['RouteTableAssociationId']) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't disassociate subnet from route table") - - try: - association_id = connection.associate_route_table(RouteTableId=route_table_id, SubnetId=subnet_id) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't associate subnet with route table") - return {'changed': True, 'association_id': association_id} - - -def ensure_subnet_associations(connection=None, module=None, route_table=None, subnets=None, - check_mode=None, purge_subnets=None): - current_association_ids = [a['RouteTableAssociationId'] for a in route_table['Associations'] if not a['Main']] - new_association_ids = [] - changed = False - for subnet in subnets: - result = ensure_subnet_association(connection=connection, module=module, vpc_id=route_table['VpcId'], - route_table_id=route_table['RouteTableId'], subnet_id=subnet['SubnetId'], check_mode=check_mode) - changed = changed or result['changed'] - if changed and check_mode: - return {'changed': True} - new_association_ids.append(result['association_id']) - - if purge_subnets: - to_delete = [a_id for a_id in current_association_ids - if a_id not in new_association_ids] - - for a_id in to_delete: - changed = True - if not check_mode: - try: - connection.disassociate_route_table(AssociationId=a_id) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't disassociate subnet from route table") - - return {'changed': changed} - - -def ensure_propagation(connection=None, module=None, route_table=None, propagating_vgw_ids=None, - check_mode=None): - changed = False - gateways = [gateway['GatewayId'] for gateway in route_table['PropagatingVgws']] - to_add = set(propagating_vgw_ids) - set(gateways) - if to_add: - changed = True - if not check_mode: - for vgw_id in to_add: - try: - connection.enable_vgw_route_propagation(RouteTableId=route_table['RouteTableId'], - GatewayId=vgw_id) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't enable route propagation") - - return {'changed': changed} - - -def ensure_route_table_absent(connection, module): - - lookup = module.params.get('lookup') - route_table_id = module.params.get('route_table_id') - tags = module.params.get('tags') - vpc_id = module.params.get('vpc_id') - purge_subnets = module.params.get('purge_subnets') - - if lookup == 'tag': - if tags is not None: - route_table = get_route_table_by_tags(connection, module, vpc_id, tags) - else: - route_table = None - elif lookup == 'id': - route_table = get_route_table_by_id(connection, module, route_table_id) - - if route_table is None: - return {'changed': False} - - # disassociate subnets before deleting route table - if not module.check_mode: - ensure_subnet_associations(connection=connection, module=module, route_table=route_table, - subnets=[], check_mode=False, purge_subnets=purge_subnets) - try: - connection.delete_route_table(RouteTableId=route_table['RouteTableId']) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Error deleting route table") - - return {'changed': True} - - -def get_route_table_info(connection, module, route_table): - result = get_route_table_by_id(connection, module, route_table['RouteTableId']) - try: - result['Tags'] = describe_tags_with_backoff(connection, route_table['RouteTableId']) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't get tags for route table") - result = camel_dict_to_snake_dict(result, ignore_list=['Tags']) - # backwards compatibility - result['id'] = result['route_table_id'] - return result - - -def create_route_spec(connection, module, vpc_id): - routes = module.params.get('routes') - - for route_spec in routes: - rename_key(route_spec, 'dest', 'destination_cidr_block') - - if route_spec.get('gateway_id') and route_spec['gateway_id'].lower() == 'igw': - igw = find_igw(connection, module, vpc_id) - route_spec['gateway_id'] = igw - if route_spec.get('gateway_id') and route_spec['gateway_id'].startswith('nat-'): - rename_key(route_spec, 'gateway_id', 'nat_gateway_id') - - return snake_dict_to_camel_dict(routes, capitalize_first=True) - - -def ensure_route_table_present(connection, module): - - lookup = module.params.get('lookup') - propagating_vgw_ids = module.params.get('propagating_vgw_ids') - purge_routes = module.params.get('purge_routes') - purge_subnets = module.params.get('purge_subnets') - purge_tags = module.params.get('purge_tags') - route_table_id = module.params.get('route_table_id') - subnets = module.params.get('subnets') - tags = module.params.get('tags') - vpc_id = module.params.get('vpc_id') - routes = create_route_spec(connection, module, vpc_id) - - changed = False - tags_valid = False - - if lookup == 'tag': - if tags is not None: - try: - route_table = get_route_table_by_tags(connection, module, vpc_id, tags) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Error finding route table with lookup 'tag'") - else: - route_table = None - elif lookup == 'id': - try: - route_table = get_route_table_by_id(connection, module, route_table_id) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Error finding route table with lookup 'id'") - - # If no route table returned then create new route table - if route_table is None: - changed = True - if not module.check_mode: - try: - route_table = connection.create_route_table(VpcId=vpc_id)['RouteTable'] - # try to wait for route table to be present before moving on - get_waiter( - connection, 'route_table_exists' - ).wait( - RouteTableIds=[route_table['RouteTableId']], - ) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Error creating route table") - else: - route_table = {"id": "rtb-xxxxxxxx", "route_table_id": "rtb-xxxxxxxx", "vpc_id": vpc_id} - module.exit_json(changed=changed, route_table=route_table) - - if routes is not None: - result = ensure_routes(connection=connection, module=module, route_table=route_table, - route_specs=routes, propagating_vgw_ids=propagating_vgw_ids, - check_mode=module.check_mode, purge_routes=purge_routes) - changed = changed or result['changed'] - - if propagating_vgw_ids is not None: - result = ensure_propagation(connection=connection, module=module, route_table=route_table, - propagating_vgw_ids=propagating_vgw_ids, check_mode=module.check_mode) - changed = changed or result['changed'] - - if not tags_valid and tags is not None: - result = ensure_tags(connection=connection, module=module, resource_id=route_table['RouteTableId'], tags=tags, - purge_tags=purge_tags, check_mode=module.check_mode) - route_table['Tags'] = result['tags'] - changed = changed or result['changed'] - - if subnets is not None: - associated_subnets = find_subnets(connection, module, vpc_id, subnets) - - result = ensure_subnet_associations(connection=connection, module=module, route_table=route_table, - subnets=associated_subnets, check_mode=module.check_mode, - purge_subnets=purge_subnets) - changed = changed or result['changed'] - - if changed: - # pause to allow route table routes/subnets/associations to be updated before exiting with final state - sleep(5) - module.exit_json(changed=changed, route_table=get_route_table_info(connection, module, route_table)) - - -def main(): - argument_spec = dict( - lookup=dict(default='tag', choices=['tag', 'id']), - propagating_vgw_ids=dict(type='list'), - purge_routes=dict(default=True, type='bool'), - purge_subnets=dict(default=True, type='bool'), - purge_tags=dict(default=False, type='bool'), - route_table_id=dict(), - routes=dict(default=[], type='list'), - state=dict(default='present', choices=['present', 'absent']), - subnets=dict(type='list'), - tags=dict(type='dict', aliases=['resource_tags']), - vpc_id=dict() - ) - - module = AnsibleAWSModule(argument_spec=argument_spec, - required_if=[['lookup', 'id', ['route_table_id']], - ['lookup', 'tag', ['vpc_id']], - ['state', 'present', ['vpc_id']]], - supports_check_mode=True) - - connection = module.client('ec2') - - state = module.params.get('state') - - if state == 'present': - result = ensure_route_table_present(connection, module) - elif state == 'absent': - result = ensure_route_table_absent(connection, module) - - module.exit_json(**result) - - -if __name__ == '__main__': - main() |