diff options
Diffstat (limited to 'test/support/integration/plugins/modules/ec2_instance.py')
-rw-r--r-- | test/support/integration/plugins/modules/ec2_instance.py | 1805 |
1 files changed, 0 insertions, 1805 deletions
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() |