2016 lines
81 KiB
Python
2016 lines
81 KiB
Python
#!/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
|
|
|
|
|
|
DOCUMENTATION = r'''
|
|
---
|
|
module: ec2_instance
|
|
version_added: 1.0.0
|
|
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(amazon.aws.ec2) module
|
|
can create and manage spot instances.
|
|
author:
|
|
- Ryan Scott Brown (@ryansb)
|
|
options:
|
|
instance_ids:
|
|
description:
|
|
- If you specify one or more instance IDs, only instances that have the specified IDs are returned.
|
|
- Mutually exclusive with I(exact_count).
|
|
type: list
|
|
elements: str
|
|
state:
|
|
description:
|
|
- Goal state for the instances.
|
|
- "I(state=present): ensures instances exist, but does not guarantee any state (e.g. running). Newly-launched instances will be run by EC2."
|
|
- "I(state=running): I(state=present) + ensures the instances are running"
|
|
- "I(state=started): I(state=running) + waits for EC2 status checks to report OK if I(wait=true)"
|
|
- "I(state=stopped): ensures an existing instance is stopped."
|
|
- "I(state=rebooted): convenience alias for I(state=stopped) immediately followed by I(state=running)"
|
|
- "I(state=restarted): convenience alias for I(state=stopped) immediately followed by I(state=started)"
|
|
- "I(state=terminated): ensures an existing instance is terminated."
|
|
- "I(state=absent): alias for I(state=terminated)"
|
|
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
|
|
count:
|
|
description:
|
|
- Number of instances to launch.
|
|
- Setting this value will result in always launching new instances.
|
|
- Mutually exclusive with I(exact_count).
|
|
type: int
|
|
version_added: 2.2.0
|
|
exact_count:
|
|
description:
|
|
- An integer value which indicates how many instances that match the I(filters) parameter should be running.
|
|
- Instances are either created or terminated based on this value.
|
|
- If termination takes place, least recently created instances will be terminated based on Launch Time.
|
|
- Mutually exclusive with I(count), I(instance_ids).
|
|
type: int
|
|
version_added: 2.2.0
|
|
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(amazon.aws.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
|
|
elements: str
|
|
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, M(amazon.aws.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 M(amazon.aws.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.
|
|
- Set ebs.throughput value requires botocore>=1.19.27.
|
|
- For more information about each parameter, see U(https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_BlockDeviceMapping.html).
|
|
type: list
|
|
elements: dict
|
|
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.
|
|
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
|
|
type: str
|
|
metadata_options:
|
|
description:
|
|
- Modify the metadata options for the instance.
|
|
- See U(https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html) for more information.
|
|
- The two suboptions I(http_endpoint) and I(http_tokens) are supported.
|
|
type: dict
|
|
version_added: 2.0.0
|
|
suboptions:
|
|
http_endpoint:
|
|
description:
|
|
- Enables or disables the HTTP metadata endpoint on instances.
|
|
- If specified a value of disabled, metadata of the instance will not be accessible.
|
|
choices: [enabled, disabled]
|
|
default: enabled
|
|
type: str
|
|
http_tokens:
|
|
description:
|
|
- Set the state of token usage for instance metadata requests.
|
|
- If the state is optional (v1 and v2), instance metadata can be retrieved with or without a signed token header on request.
|
|
- If the state is required (v2), a signed token header must be sent with any instance metadata retrieval requests.
|
|
choices: [optional, required]
|
|
default: optional
|
|
type: str
|
|
|
|
extends_documentation_fragment:
|
|
- amazon.aws.aws
|
|
- amazon.aws.ec2
|
|
|
|
'''
|
|
|
|
EXAMPLES = '''
|
|
# Note: These examples do not set authentication details, see the AWS Guide for details.
|
|
|
|
- name: Terminate every running instance in a region. Use with EXTREME caution.
|
|
amazon.aws.ec2_instance:
|
|
state: absent
|
|
filters:
|
|
instance-state-name: running
|
|
|
|
- name: restart a particular instance by its ID
|
|
amazon.aws.ec2_instance:
|
|
state: restarted
|
|
instance_ids:
|
|
- i-12345678
|
|
|
|
- name: start an instance with a public IP address
|
|
amazon.aws.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
|
|
|
|
- name: start an instance and Add EBS
|
|
amazon.aws.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
|
|
|
|
- name: start an instance with a cpu_options
|
|
amazon.aws.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
|
|
|
|
- name: start an instance and have it begin a Tower callback on boot
|
|
amazon.aws.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"
|
|
|
|
- name: start an instance with ENI (An existing ENI ID is required)
|
|
amazon.aws.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
|
|
|
|
- name: add second ENI interface
|
|
amazon.aws.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
|
|
|
|
- name: start an instance with metadata options
|
|
amazon.aws.ec2_instance:
|
|
name: "public-metadataoptions-instance"
|
|
vpc_subnet_id: subnet-5calable
|
|
instance_type: t3.small
|
|
image_id: ami-123456
|
|
tags:
|
|
Environment: Testing
|
|
metadata_options:
|
|
http_endpoint: enabled
|
|
http_tokens: optional
|
|
|
|
# ensure number of instances running with a tag matches exact_count
|
|
- name: start multiple instances
|
|
amazon.aws.ec2_instance:
|
|
instance_type: t3.small
|
|
image_id: ami-123456
|
|
exact_count: 5
|
|
region: us-east-2
|
|
vpc_subnet_id: subnet-0123456
|
|
network:
|
|
assign_public_ip: yes
|
|
security_group: default
|
|
tags:
|
|
foo: bar
|
|
|
|
# launches multiple instances - specific number of instances
|
|
- name: start specific number of multiple instances
|
|
amazon.aws.ec2_instance:
|
|
instance_type: t3.small
|
|
image_id: ami-123456
|
|
count: 3
|
|
region: us-east-2
|
|
network:
|
|
assign_public_ip: yes
|
|
security_group: default
|
|
vpc_subnet_id: subnet-0123456
|
|
state: present
|
|
tags:
|
|
foo: bar
|
|
'''
|
|
|
|
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
|
|
'''
|
|
|
|
from collections import namedtuple
|
|
import re
|
|
import string
|
|
import textwrap
|
|
import time
|
|
import uuid
|
|
|
|
try:
|
|
import botocore
|
|
except ImportError:
|
|
pass # caught by AnsibleAWSModule
|
|
|
|
|
|
from ansible.module_utils._text import to_native
|
|
from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict
|
|
from ansible.module_utils.common.dict_transformations import snake_dict_to_camel_dict
|
|
from ansible.module_utils.six import string_types
|
|
from ansible.module_utils.six.moves.urllib import parse as urlparse
|
|
|
|
from ansible_collections.amazon.aws.plugins.module_utils.core import AnsibleAWSModule
|
|
from ansible_collections.amazon.aws.plugins.module_utils.core import is_boto3_error_code
|
|
from ansible_collections.amazon.aws.plugins.module_utils.core import is_boto3_error_message
|
|
from ansible_collections.amazon.aws.plugins.module_utils.ec2 import AWSRetry
|
|
from ansible_collections.amazon.aws.plugins.module_utils.ec2 import ansible_dict_to_boto3_filter_list
|
|
from ansible_collections.amazon.aws.plugins.module_utils.ec2 import ensure_ec2_tags
|
|
from ansible_collections.amazon.aws.plugins.module_utils.ec2 import get_ec2_security_group_ids_from_names
|
|
from ansible_collections.amazon.aws.plugins.module_utils.tagging import boto3_tag_list_to_ansible_dict
|
|
from ansible_collections.amazon.aws.plugins.module_utils.tagging import boto3_tag_specifications
|
|
|
|
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.")
|
|
|
|
|
|
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])
|
|
if 'volume_type' in volume['ebs'] and volume['ebs']['volume_type'] == 'gp3':
|
|
if not volume['ebs'].get('iops'):
|
|
volume['ebs']['iops'] = 3000
|
|
if 'throughput' in volume['ebs']:
|
|
module.require_botocore_at_least('1.19.27', reason='to set throughtput value')
|
|
volume['ebs']['throughput'] = int(volume['ebs']['throughput'])
|
|
else:
|
|
volume['ebs']['throughput'] = 125
|
|
|
|
return [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
|
|
try:
|
|
association = client.describe_iam_instance_profile_associations(
|
|
aws_retry=True,
|
|
Filters=[{'Name': 'instance-id', 'Values': [instance['InstanceId']]}])
|
|
except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e:
|
|
# check for InvalidAssociationID.NotFound
|
|
module.fail_json_aws(e, "Could not find instance profile association")
|
|
try:
|
|
resp = client.replace_iam_instance_profile_association(
|
|
aws_retry=True,
|
|
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
|
|
try:
|
|
resp = client.associate_iam_instance_profile(
|
|
aws_retry=True,
|
|
IamInstanceProfile={'Arn': determine_iam_role(desired_profile_name)},
|
|
InstanceId=instance['InstanceId']
|
|
)
|
|
return True
|
|
except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e:
|
|
module.fail_json_aws(e, "Could not associate new instance profile")
|
|
|
|
return False
|
|
|
|
|
|
def build_network_spec(params):
|
|
"""
|
|
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'
|
|
},
|
|
"""
|
|
|
|
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()
|
|
if default_vpc is None:
|
|
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(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'],
|
|
)
|
|
spec['Groups'] = 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):
|
|
|
|
if subnet_id is not None:
|
|
try:
|
|
sub = client.describe_subnets(aws_retry=True, SubnetIds=[subnet_id])
|
|
except is_boto3_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
|
|
)
|
|
)
|
|
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: # pylint: disable=duplicate-except
|
|
module.fail_json_aws(e, msg="Error while searching for subnet {0} parent VPC.".format(subnet_id))
|
|
parent_vpc_id = sub['Subnets'][0]['VpcId']
|
|
|
|
if group:
|
|
return get_ec2_security_group_ids_from_names(group, client, vpc_id=parent_vpc_id)
|
|
if groups:
|
|
return get_ec2_security_group_ids_from_names(groups, client, vpc_id=parent_vpc_id)
|
|
return []
|
|
|
|
|
|
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')
|
|
if params.get('metadata_options'):
|
|
spec['MetadataOptions'] = {}
|
|
spec['MetadataOptions']['HttpEndpoint'] = params.get(
|
|
'metadata_options').get('http_endpoint')
|
|
spec['MetadataOptions']['HttpTokens'] = params.get(
|
|
'metadata_options').get('http_tokens')
|
|
return spec
|
|
|
|
|
|
def build_instance_tags(params, propagate_tags_to_volumes=True):
|
|
tags = params.get('tags') or {}
|
|
if params.get('name') is not None:
|
|
tags['Name'] = params.get('name')
|
|
specs = boto3_tag_specifications(tags, ['volume', 'instance'])
|
|
return specs
|
|
|
|
|
|
def build_run_instance_spec(params):
|
|
|
|
spec = dict(
|
|
ClientToken=uuid.uuid4().hex,
|
|
MaxCount=1,
|
|
MinCount=1,
|
|
)
|
|
# network parameters
|
|
spec['NetworkInterfaces'] = build_network_spec(params)
|
|
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')))
|
|
|
|
if module.params.get('exact_count'):
|
|
spec['MaxCount'] = module.params.get('to_launch')
|
|
spec['MinCount'] = module.params.get('to_launch')
|
|
|
|
if module.params.get('count'):
|
|
spec['MaxCount'] = module.params.get('count')
|
|
spec['MinCount'] = module.params.get('count')
|
|
|
|
spec['InstanceType'] = params['instance_type']
|
|
return spec
|
|
|
|
|
|
def await_instances(ids, desired_module_state='present', force_wait=False):
|
|
if not module.params.get('wait', True) and not force_wait:
|
|
# 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
|
|
|
|
# Map ansible state to boto3 waiter type
|
|
state_to_boto3_waiter = {
|
|
'present': 'instance_exists',
|
|
'started': 'instance_status_ok',
|
|
'running': 'instance_running',
|
|
'stopped': 'instance_stopped',
|
|
'restarted': 'instance_status_ok',
|
|
'rebooted': 'instance_running',
|
|
'terminated': 'instance_terminated',
|
|
'absent': 'instance_terminated',
|
|
}
|
|
if desired_module_state not in state_to_boto3_waiter:
|
|
module.fail_json(msg="Cannot wait for state {0}, invalid state".format(desired_module_state))
|
|
boto3_waiter_type = state_to_boto3_waiter[desired_module_state]
|
|
waiter = client.get_waiter(boto3_waiter_type)
|
|
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), boto3_waiter_type))
|
|
except botocore.exceptions.WaiterError as e:
|
|
module.warn("Instances {0} took too long to reach state {1}. {2}".format(
|
|
', '.join(ids), boto3_waiter_type, to_native(e)))
|
|
|
|
|
|
def diff_instance_and_params(instance, params, skip=None):
|
|
"""boto3 instance obj, module params"""
|
|
|
|
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 None:
|
|
continue
|
|
if mapping.instance_key in skip:
|
|
continue
|
|
|
|
try:
|
|
value = client.describe_instance_attribute(aws_retry=True, Attribute=mapping.attribute_name, InstanceId=id_)
|
|
except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e:
|
|
module.fail_json_aws(e, msg="Could not describe attribute {0} for instance {1}".format(mapping.attribute_name, id_))
|
|
if 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('security_group') or params.get('security_groups'):
|
|
try:
|
|
value = client.describe_instance_attribute(aws_retry=True, Attribute="groupSet", InstanceId=id_)
|
|
except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e:
|
|
module.fail_json_aws(e, msg="Could not describe attribute groupSet for instance {0}".format(id_))
|
|
# managing security groups
|
|
if params.get('vpc_subnet_id'):
|
|
subnet_id = params.get('vpc_subnet_id')
|
|
else:
|
|
default_vpc = get_default_vpc()
|
|
if default_vpc is None:
|
|
module.fail_json(
|
|
msg="No default subnet could be found - you must include a VPC subnet ID (vpc_subnet_id parameter) to modify security groups.")
|
|
else:
|
|
sub = get_default_subnet(default_vpc)
|
|
subnet_id = sub['SubnetId']
|
|
|
|
groups = discover_security_groups(
|
|
group=params.get('security_group'),
|
|
groups=params.get('security_groups'),
|
|
subnet_id=subnet_id,
|
|
)
|
|
expected_groups = groups
|
|
instance_groups = [g['GroupId'] for g in value['Groups']]
|
|
if set(instance_groups) != set(expected_groups):
|
|
changes_to_apply.append(dict(
|
|
Groups=expected_groups,
|
|
InstanceId=instance['InstanceId']
|
|
))
|
|
|
|
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):
|
|
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:
|
|
try:
|
|
client.attach_network_interface(
|
|
aws_retry=True,
|
|
DeviceIndex=new_ids.index(eni_id),
|
|
InstanceId=instance['InstanceId'],
|
|
NetworkInterfaceId=eni_id,
|
|
)
|
|
except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e:
|
|
module.fail_json_aws(e, msg="Could not attach interface {0} to instance {1}".format(eni_id, instance['InstanceId']))
|
|
return bool(len(to_attach))
|
|
return False
|
|
|
|
|
|
def find_instances(ids=None, filters=None):
|
|
paginator = client.get_paginator('describe_instances')
|
|
if ids:
|
|
params = dict(InstanceIds=ids)
|
|
elif filters is None:
|
|
module.fail_json(msg="No filters provided when they were required")
|
|
else:
|
|
for key in list(filters.keys()):
|
|
if not key.startswith("tag:"):
|
|
filters[key.replace("_", "-")] = filters.pop(key)
|
|
params = dict(Filters=ansible_dict_to_boto3_filter_list(filters))
|
|
|
|
try:
|
|
results = _describe_instances(**params)
|
|
except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e:
|
|
module.fail_json_aws(e, msg="Could not describe instances")
|
|
retval = list(results)
|
|
return retval
|
|
|
|
|
|
@AWSRetry.jittered_backoff()
|
|
def _describe_instances(**params):
|
|
paginator = client.get_paginator('describe_instances')
|
|
return paginator.paginate(**params).search('Reservations[].Instances[]')
|
|
|
|
|
|
def get_default_vpc():
|
|
try:
|
|
vpcs = client.describe_vpcs(
|
|
aws_retry=True,
|
|
Filters=ansible_dict_to_boto3_filter_list({'isDefault': 'true'}))
|
|
except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e:
|
|
module.fail_json_aws(e, msg="Could not describe default VPC")
|
|
if len(vpcs.get('Vpcs', [])):
|
|
return vpcs.get('Vpcs')[0]
|
|
return None
|
|
|
|
|
|
def get_default_subnet(vpc, availability_zone=None):
|
|
try:
|
|
subnets = client.describe_subnets(
|
|
aws_retry=True,
|
|
Filters=ansible_dict_to_boto3_filter_list({
|
|
'vpc-id': vpc['VpcId'],
|
|
'state': 'available',
|
|
'default-for-az': 'true',
|
|
})
|
|
)
|
|
except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e:
|
|
module.fail_json_aws(e, msg="Could not describe default subnets for VPC {0}".format(vpc['VpcId']))
|
|
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(desired_module_state):
|
|
"""
|
|
Sets return keys depending on the desired instance state
|
|
"""
|
|
results = dict()
|
|
changed = False
|
|
if desired_module_state in ('running', 'started'):
|
|
_changed, failed, instances, failure_reason = change_instance_state(
|
|
filters=module.params.get('filters'), desired_module_state=desired_module_state)
|
|
changed |= bool(len(_changed))
|
|
|
|
if failed:
|
|
module.fail_json(
|
|
msg="Unable to start instances: {0}".format(failure_reason),
|
|
reboot_success=list(_changed),
|
|
reboot_failed=failed)
|
|
|
|
results = dict(
|
|
msg='Instances started',
|
|
start_success=list(_changed),
|
|
start_failed=[],
|
|
# Avoid breaking things 'reboot' is wrong but used to be returned
|
|
reboot_success=list(_changed),
|
|
reboot_failed=[],
|
|
changed=changed,
|
|
instances=[pretty_instance(i) for i in instances],
|
|
)
|
|
elif desired_module_state in ('restarted', 'rebooted'):
|
|
# https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-reboot.html
|
|
# The Ansible behaviour of issuing a stop/start has a minor impact on user billing
|
|
# This will need to be changelogged if we ever change to client.reboot_instance
|
|
_changed, failed, instances, failure_reason = change_instance_state(
|
|
filters=module.params.get('filters'),
|
|
desired_module_state='stopped',
|
|
)
|
|
changed |= bool(len(_changed))
|
|
_changed, failed, instances, failure_reason = change_instance_state(
|
|
filters=module.params.get('filters'),
|
|
desired_module_state=desired_module_state,
|
|
)
|
|
changed |= bool(len(_changed))
|
|
|
|
if failed:
|
|
module.fail_json(
|
|
msg="Unable to restart instances: {0}".format(failure_reason),
|
|
reboot_success=list(_changed),
|
|
reboot_failed=failed)
|
|
|
|
results = dict(
|
|
msg='Instances restarted',
|
|
reboot_success=list(_changed),
|
|
changed=changed,
|
|
reboot_failed=[],
|
|
instances=[pretty_instance(i) for i in instances],
|
|
)
|
|
elif desired_module_state in ('stopped',):
|
|
_changed, failed, instances, failure_reason = change_instance_state(
|
|
filters=module.params.get('filters'),
|
|
desired_module_state=desired_module_state,
|
|
)
|
|
changed |= bool(len(_changed))
|
|
|
|
if failed:
|
|
module.fail_json(
|
|
msg="Unable to stop instances: {0}".format(failure_reason),
|
|
stop_success=list(_changed),
|
|
stop_failed=failed)
|
|
|
|
results = dict(
|
|
msg='Instances stopped',
|
|
stop_success=list(_changed),
|
|
changed=changed,
|
|
stop_failed=[],
|
|
instances=[pretty_instance(i) for i in instances],
|
|
)
|
|
elif desired_module_state in ('absent', 'terminated'):
|
|
terminated, terminate_failed, instances, failure_reason = change_instance_state(
|
|
filters=module.params.get('filters'),
|
|
desired_module_state=desired_module_state,
|
|
)
|
|
|
|
if terminate_failed:
|
|
module.fail_json(
|
|
msg="Unable to terminate instances: {0}".format(failure_reason),
|
|
terminate_success=list(terminated),
|
|
terminate_failed=terminate_failed)
|
|
results = dict(
|
|
msg='Instances terminated',
|
|
terminate_success=list(terminated),
|
|
changed=bool(len(terminated)),
|
|
terminate_failed=[],
|
|
instances=[pretty_instance(i) for i in instances],
|
|
)
|
|
return results
|
|
|
|
|
|
def change_instance_state(filters, desired_module_state):
|
|
|
|
# Map ansible state to ec2 state
|
|
ec2_instance_states = {
|
|
'present': 'running',
|
|
'started': 'running',
|
|
'running': 'running',
|
|
'stopped': 'stopped',
|
|
'restarted': 'running',
|
|
'rebooted': 'running',
|
|
'terminated': 'terminated',
|
|
'absent': 'terminated',
|
|
}
|
|
desired_ec2_state = ec2_instance_states[desired_module_state]
|
|
changed = set()
|
|
instances = find_instances(filters=filters)
|
|
to_change = set(i['InstanceId'] for i in instances if i['State']['Name'] != desired_ec2_state)
|
|
unchanged = set()
|
|
failure_reason = ""
|
|
|
|
for inst in instances:
|
|
try:
|
|
if desired_ec2_state == 'terminated':
|
|
# Before terminating an instance we need for them to leave
|
|
# 'pending' or 'stopping' (if they're in those states)
|
|
if inst['State']['Name'] == 'stopping':
|
|
await_instances([inst['InstanceId']], desired_module_state='stopped', force_wait=True)
|
|
elif inst['State']['Name'] == 'pending':
|
|
await_instances([inst['InstanceId']], desired_module_state='running', force_wait=True)
|
|
|
|
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 = client.terminate_instances(aws_retry=True, InstanceIds=[inst['InstanceId']])
|
|
[changed.add(i['InstanceId']) for i in resp['TerminatingInstances']]
|
|
if desired_ec2_state == 'stopped':
|
|
# Before stopping an instance we need for them to leave
|
|
# 'pending'
|
|
if inst['State']['Name'] == 'pending':
|
|
await_instances([inst['InstanceId']], desired_module_state='running', force_wait=True)
|
|
# Already moving to the relevant state
|
|
elif inst['State']['Name'] in ('stopping', 'stopped'):
|
|
unchanged.add(inst['InstanceId'])
|
|
continue
|
|
|
|
if module.check_mode:
|
|
changed.add(inst['InstanceId'])
|
|
continue
|
|
resp = client.stop_instances(aws_retry=True, InstanceIds=[inst['InstanceId']])
|
|
[changed.add(i['InstanceId']) for i in resp['StoppingInstances']]
|
|
if desired_ec2_state == 'running':
|
|
if inst['State']['Name'] in ('pending', 'running'):
|
|
unchanged.add(inst['InstanceId'])
|
|
continue
|
|
elif inst['State']['Name'] == 'stopping':
|
|
await_instances([inst['InstanceId']], desired_module_state='stopped', force_wait=True)
|
|
|
|
if module.check_mode:
|
|
changed.add(inst['InstanceId'])
|
|
continue
|
|
|
|
resp = client.start_instances(aws_retry=True, 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), desired_module_state=desired_module_state)
|
|
|
|
change_failed = list(to_change - changed)
|
|
|
|
if instances:
|
|
instances = find_instances(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.get('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 is_boto3_error_code('NoSuchEntity') as e:
|
|
module.fail_json_aws(e, msg="Could not find instance_role {0}".format(name_or_arn))
|
|
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: # pylint: disable=duplicate-except
|
|
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, state):
|
|
tags = dict(module.params.get('tags') or {})
|
|
name = module.params.get('name')
|
|
purge_tags = module.params.get('purge_tags', False)
|
|
if name:
|
|
tags['Name'] = name
|
|
|
|
changed = False
|
|
all_changes = list()
|
|
|
|
for instance in existing_matches:
|
|
changed |= ensure_ec2_tags(client, module, instance['InstanceId'], tags=tags, purge_tags=purge_tags)
|
|
changes = diff_instance_and_params(instance, module.params)
|
|
for c in changes:
|
|
if not module.check_mode:
|
|
try:
|
|
client.modify_instance_attribute(aws_retry=True, **c)
|
|
except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e:
|
|
module.fail_json_aws(e, msg="Could not apply change {0} to existing instance.".format(str(c)))
|
|
all_changes.extend(changes)
|
|
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)
|
|
|
|
altered = find_instances(ids=[i['InstanceId'] for i in existing_matches])
|
|
alter_config_result = dict(
|
|
changed=changed,
|
|
instances=[pretty_instance(i) for i in altered],
|
|
instance_ids=[i['InstanceId'] for i in altered],
|
|
changes=changes,
|
|
)
|
|
|
|
state_results = ensure_instance_state(state)
|
|
|
|
result = {**state_results, **alter_config_result}
|
|
|
|
return result
|
|
|
|
|
|
def enforce_count(existing_matches, module, desired_module_state):
|
|
exact_count = module.params.get('exact_count')
|
|
|
|
try:
|
|
current_count = len(existing_matches)
|
|
if current_count == exact_count:
|
|
module.exit_json(
|
|
changed=False,
|
|
msg='{0} instances already running, nothing to do.'.format(exact_count)
|
|
)
|
|
|
|
elif current_count < exact_count:
|
|
to_launch = exact_count - current_count
|
|
module.params['to_launch'] = to_launch
|
|
# launch instances
|
|
try:
|
|
ensure_present(existing_matches=existing_matches, desired_module_state=desired_module_state)
|
|
except botocore.exceptions.ClientError as e:
|
|
module.fail_json(e, msg='Unable to launch instances')
|
|
elif current_count > exact_count:
|
|
to_terminate = current_count - exact_count
|
|
# sort the instances from least recent to most recent based on launch time
|
|
existing_matches = sorted(existing_matches, key=lambda inst: inst['LaunchTime'])
|
|
# get the instance ids of instances with the count tag on them
|
|
all_instance_ids = [x['InstanceId'] for x in existing_matches]
|
|
terminate_ids = all_instance_ids[0:to_terminate]
|
|
if module.check_mode:
|
|
module.exit_json(changed=True, msg='Would have terminated following instances if not in check mode {0}'.format(terminate_ids))
|
|
# terminate instances
|
|
try:
|
|
result = client.terminate_instances(InstanceIds=terminate_ids)
|
|
await_instances(terminate_ids, desired_module_state='terminated', force_wait=True)
|
|
except botocore.exceptions.ClientError as e:
|
|
module.fail_json(e, msg='Unable to terminate instances')
|
|
module.exit_json(
|
|
changed=True,
|
|
msg='Successfully terminated instances.',
|
|
terminated_ids=terminate_ids,
|
|
)
|
|
|
|
except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e:
|
|
module.fail_json_aws(e, msg="Failed to enforce instance count")
|
|
|
|
|
|
def ensure_present(existing_matches, desired_module_state):
|
|
tags = dict(module.params.get('tags') or {})
|
|
name = module.params.get('name')
|
|
if name:
|
|
tags['Name'] = name
|
|
|
|
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,
|
|
msg='Would have launched instances if not in check_mode.',
|
|
)
|
|
instance_response = run_instances(**instance_spec)
|
|
instances = instance_response['Instances']
|
|
instance_ids = [i['InstanceId'] for i in instances]
|
|
|
|
# Wait for instances to exist in the EC2 API before
|
|
# attempting to modify them
|
|
await_instances(instance_ids, desired_module_state='present', force_wait=True)
|
|
|
|
for ins in instances:
|
|
# Wait for instances to exist (don't check state)
|
|
try:
|
|
AWSRetry.jittered_backoff(
|
|
catch_extra_error_codes=['InvalidInstanceID.NotFound'],
|
|
)(
|
|
client.describe_instance_status
|
|
)(
|
|
InstanceIds=[ins['InstanceId']],
|
|
IncludeAllInstances=True,
|
|
)
|
|
except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e:
|
|
module.fail_json_aws(e, msg="Failed to fetch status of new EC2 instance")
|
|
changes = diff_instance_and_params(ins, module.params, skip=['UserData', 'EbsOptimized'])
|
|
for c in changes:
|
|
try:
|
|
client.modify_instance_attribute(aws_retry=True, **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, desired_module_state=desired_module_state)
|
|
instances = find_instances(ids=instance_ids)
|
|
|
|
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")
|
|
|
|
|
|
def run_instances(**instance_spec):
|
|
try:
|
|
return client.run_instances(**instance_spec)
|
|
except is_boto3_error_message('Invalid IAM Instance Profile ARN'):
|
|
# 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 client.run_instances(**instance_spec)
|
|
|
|
|
|
def build_filters():
|
|
filters = {
|
|
# all states except shutting-down and terminated
|
|
'instance-state-name': ['pending', 'running', 'stopping', 'stopped'],
|
|
}
|
|
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(get_default_vpc(), 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')]
|
|
elif module.params.get('tags'):
|
|
name_tag = module.params.get('tags').get('Name', None)
|
|
if name_tag:
|
|
filters['tag:Name'] = [name_tag]
|
|
|
|
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')]
|
|
return filters
|
|
|
|
|
|
def main():
|
|
global module
|
|
global client
|
|
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(type='int'),
|
|
exact_count=dict(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', elements='str'),
|
|
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', elements='str'),
|
|
network=dict(default=None, type='dict'),
|
|
volumes=dict(default=None, type='list', elements='dict'),
|
|
metadata_options=dict(type='dict', options=dict(
|
|
http_endpoint=dict(type='str', choices=['enabled', 'disabled'], default='enabled'),
|
|
http_tokens=dict(type='str', choices=['optional', 'required'], default='optional'))),
|
|
)
|
|
# 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'],
|
|
['exact_count', 'count'],
|
|
['exact_count', 'instance_ids'],
|
|
],
|
|
supports_check_mode=True
|
|
)
|
|
result = dict()
|
|
|
|
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')
|
|
|
|
retry_decorator = AWSRetry.jittered_backoff(
|
|
catch_extra_error_codes=[
|
|
'IncorrectState',
|
|
]
|
|
)
|
|
client = module.client('ec2', retry_decorator=retry_decorator)
|
|
|
|
if module.params.get('filters') is None:
|
|
module.params['filters'] = build_filters()
|
|
|
|
existing_matches = find_instances(filters=module.params.get('filters'))
|
|
|
|
if state in ('terminated', 'absent'):
|
|
if existing_matches:
|
|
result = ensure_instance_state(state)
|
|
else:
|
|
result = dict(
|
|
msg='No matching instances found',
|
|
changed=False,
|
|
)
|
|
elif module.params.get('exact_count'):
|
|
enforce_count(existing_matches, module, desired_module_state=state)
|
|
elif existing_matches and not module.params.get('count'):
|
|
for match in existing_matches:
|
|
warn_if_public_ip_assignment_changed(match)
|
|
warn_if_cpu_options_changed(match)
|
|
result = handle_existing(existing_matches, state)
|
|
else:
|
|
result = ensure_present(existing_matches=existing_matches, desired_module_state=state)
|
|
|
|
module.exit_json(**result)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|