a862ff2d43
* Allow to specify collection_name separately for deprecation. * Use new functionality in Ansible. * Use new functionality in tests. * Update tagging/untagging functions. * Update pylint deprecated sanity test. * Update validate-modules. Missing are basic checks for version_added (validate semantic version format for collections). * Improve version validation. Re-add version_added validation. * Make sure collection names are added to return docs before schema validation. * Extra checks to avoid crashes on bad data. * Make C# module utils code work, and update/extend tests. * Add changelog fragment. * Stop extracting collection name from potentially tagged versions/dates. * Simplify C# code. * Update Windows modules docs. * Forgot semicolons.
1766 lines
65 KiB
Python
1766 lines
65 KiB
Python
#!/usr/bin/python
|
|
# This file is part of Ansible
|
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
|
|
from __future__ import absolute_import, division, print_function
|
|
__metaclass__ = type
|
|
|
|
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
|
'status': ['stableinterface'],
|
|
'supported_by': 'core'}
|
|
|
|
|
|
DOCUMENTATION = '''
|
|
---
|
|
module: ec2
|
|
short_description: create, terminate, start or stop an instance in ec2
|
|
description:
|
|
- Creates or terminates ec2 instances.
|
|
- >
|
|
Note: This module uses the older boto Python module to interact with the EC2 API.
|
|
M(ec2) will still receive bug fixes, but no new features.
|
|
Consider using the M(ec2_instance) module instead.
|
|
If M(ec2_instance) does not support a feature you need that is available in M(ec2), please
|
|
file a feature request.
|
|
version_added: "0.9"
|
|
options:
|
|
key_name:
|
|
description:
|
|
- Key pair to use on the instance.
|
|
- The SSH key must already exist in AWS in order to use this argument.
|
|
- Keys can be created / deleted using the M(ec2_key) module.
|
|
aliases: ['keypair']
|
|
type: str
|
|
id:
|
|
version_added: "1.1"
|
|
description:
|
|
- Identifier for this instance or set of instances, so that the module will be idempotent with respect to EC2 instances.
|
|
- This identifier is valid for at least 24 hours after the termination of the instance, and should not be reused for another call later on.
|
|
- For details, see the description of client token at U(https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Run_Instance_Idempotency.html).
|
|
type: str
|
|
group:
|
|
description:
|
|
- Security group (or list of groups) to use with the instance.
|
|
aliases: [ 'groups' ]
|
|
type: list
|
|
elements: str
|
|
group_id:
|
|
version_added: "1.1"
|
|
description:
|
|
- Security group id (or list of ids) to use with the instance.
|
|
type: list
|
|
elements: str
|
|
zone:
|
|
version_added: "1.2"
|
|
description:
|
|
- AWS availability zone in which to launch the instance.
|
|
aliases: [ 'aws_zone', 'ec2_zone' ]
|
|
type: str
|
|
instance_type:
|
|
description:
|
|
- Instance type to use for the instance, see U(https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-types.html).
|
|
- Required when creating a new instance.
|
|
type: str
|
|
aliases: ['type']
|
|
tenancy:
|
|
version_added: "1.9"
|
|
description:
|
|
- An instance with a tenancy of C(dedicated) runs on single-tenant hardware and can only be launched into a VPC.
|
|
- Note that to use dedicated tenancy you MUST specify a I(vpc_subnet_id) as well.
|
|
- Dedicated tenancy is not available for EC2 "micro" instances.
|
|
default: default
|
|
choices: [ "default", "dedicated" ]
|
|
type: str
|
|
spot_price:
|
|
version_added: "1.5"
|
|
description:
|
|
- Maximum spot price to bid. If not set, a regular on-demand instance is requested.
|
|
- A spot request is made with this maximum bid. When it is filled, the instance is started.
|
|
type: str
|
|
spot_type:
|
|
version_added: "2.0"
|
|
description:
|
|
- The type of spot request.
|
|
- After being interrupted a C(persistent) spot instance will be started once there is capacity to fill the request again.
|
|
default: "one-time"
|
|
choices: [ "one-time", "persistent" ]
|
|
type: str
|
|
image:
|
|
description:
|
|
- I(ami) ID to use for the instance.
|
|
- Required when I(state=present).
|
|
type: str
|
|
kernel:
|
|
description:
|
|
- Kernel eki to use for the instance.
|
|
type: str
|
|
ramdisk:
|
|
description:
|
|
- Ramdisk eri to use for the instance.
|
|
type: str
|
|
wait:
|
|
description:
|
|
- Wait for the instance to reach its desired state before returning.
|
|
- Does not wait for SSH, see the 'wait_for_connection' example for details.
|
|
type: bool
|
|
default: false
|
|
wait_timeout:
|
|
description:
|
|
- How long before wait gives up, in seconds.
|
|
default: 300
|
|
type: int
|
|
spot_wait_timeout:
|
|
version_added: "1.5"
|
|
description:
|
|
- How long to wait for the spot instance request to be fulfilled. Affects 'Request valid until' for setting spot request lifespan.
|
|
default: 600
|
|
type: int
|
|
count:
|
|
description:
|
|
- Number of instances to launch.
|
|
default: 1
|
|
type: int
|
|
monitoring:
|
|
version_added: "1.1"
|
|
description:
|
|
- Enable detailed monitoring (CloudWatch) for instance.
|
|
type: bool
|
|
default: false
|
|
user_data:
|
|
version_added: "0.9"
|
|
description:
|
|
- Opaque blob of data which is made available to the EC2 instance.
|
|
type: str
|
|
instance_tags:
|
|
version_added: "1.0"
|
|
description:
|
|
- A hash/dictionary of tags to add to the new instance or for starting/stopping instance by tag; '{"key":"value"}' and '{"key":"value","key":"value"}'.
|
|
type: dict
|
|
placement_group:
|
|
version_added: "1.3"
|
|
description:
|
|
- Placement group for the instance when using EC2 Clustered Compute.
|
|
type: str
|
|
vpc_subnet_id:
|
|
version_added: "1.1"
|
|
description:
|
|
- the subnet ID in which to launch the instance (VPC).
|
|
type: str
|
|
assign_public_ip:
|
|
version_added: "1.5"
|
|
description:
|
|
- When provisioning within vpc, assign a public IP address. Boto library must be 2.13.0+.
|
|
type: bool
|
|
private_ip:
|
|
version_added: "1.2"
|
|
description:
|
|
- The private ip address to assign the instance (from the vpc subnet).
|
|
type: str
|
|
instance_profile_name:
|
|
version_added: "1.3"
|
|
description:
|
|
- Name of the IAM instance profile (i.e. what the EC2 console refers to as an "IAM Role") to use. Boto library must be 2.5.0+.
|
|
type: str
|
|
instance_ids:
|
|
version_added: "1.3"
|
|
description:
|
|
- "list of instance ids, currently used for states: absent, running, stopped"
|
|
aliases: ['instance_id']
|
|
type: list
|
|
elements: str
|
|
source_dest_check:
|
|
version_added: "1.6"
|
|
description:
|
|
- Enable or Disable the Source/Destination checks (for NAT instances and Virtual Routers).
|
|
When initially creating an instance the EC2 API defaults this to C(True).
|
|
type: bool
|
|
termination_protection:
|
|
version_added: "2.0"
|
|
description:
|
|
- Enable or Disable the Termination Protection.
|
|
type: bool
|
|
default: false
|
|
instance_initiated_shutdown_behavior:
|
|
version_added: "2.2"
|
|
description:
|
|
- Set whether AWS will Stop or Terminate an instance on shutdown. This parameter is ignored when using instance-store.
|
|
images (which require termination on shutdown).
|
|
default: 'stop'
|
|
choices: [ "stop", "terminate" ]
|
|
type: str
|
|
state:
|
|
version_added: "1.3"
|
|
description:
|
|
- Create, terminate, start, stop or restart instances. The state 'restarted' was added in Ansible 2.2.
|
|
- When I(state=absent), I(instance_ids) is required.
|
|
- When I(state=running), I(state=stopped) or I(state=restarted) then either I(instance_ids) or I(instance_tags) is required.
|
|
default: 'present'
|
|
choices: ['absent', 'present', 'restarted', 'running', 'stopped']
|
|
type: str
|
|
volumes:
|
|
version_added: "1.5"
|
|
description:
|
|
- A list of hash/dictionaries of volumes to add to the new instance.
|
|
type: list
|
|
elements: dict
|
|
suboptions:
|
|
device_name:
|
|
type: str
|
|
required: true
|
|
description:
|
|
- A name for the device (For example C(/dev/sda)).
|
|
delete_on_termination:
|
|
type: bool
|
|
default: false
|
|
description:
|
|
- Whether the volume should be automatically deleted when the instance is terminated.
|
|
ephemeral:
|
|
type: str
|
|
description:
|
|
- Whether the volume should be ephemeral.
|
|
- Data on ephemeral volumes is lost when the instance is stopped.
|
|
- Mutually exclusive with the I(snapshot) parameter.
|
|
encrypted:
|
|
type: bool
|
|
default: false
|
|
description:
|
|
- Whether the volume should be encrypted using the 'aws/ebs' KMS CMK.
|
|
snapshot:
|
|
type: str
|
|
description:
|
|
- The ID of an EBS snapshot to copy when creating the volume.
|
|
- Mutually exclusive with the I(ephemeral) parameter.
|
|
volume_type:
|
|
type: str
|
|
description:
|
|
- The type of volume to create.
|
|
- See U(https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSVolumeTypes.html) for more information on the available volume types.
|
|
volume_size:
|
|
type: int
|
|
description:
|
|
- The size of the volume (in GiB).
|
|
iops:
|
|
type: int
|
|
description:
|
|
- The number of IOPS per second to provision for the volume.
|
|
- Required when I(volume_type=io1).
|
|
ebs_optimized:
|
|
version_added: "1.6"
|
|
description:
|
|
- Whether instance is using optimized EBS volumes, see U(https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSOptimized.html).
|
|
default: false
|
|
type: bool
|
|
exact_count:
|
|
version_added: "1.5"
|
|
description:
|
|
- An integer value which indicates how many instances that match the 'count_tag' parameter should be running.
|
|
Instances are either created or terminated based on this value.
|
|
type: int
|
|
count_tag:
|
|
version_added: "1.5"
|
|
description:
|
|
- Used with I(exact_count) to determine how many nodes based on a specific tag criteria should be running.
|
|
This can be expressed in multiple ways and is shown in the EXAMPLES section. For instance, one can request 25 servers
|
|
that are tagged with "class=webserver". The specified tag must already exist or be passed in as the I(instance_tags) option.
|
|
type: raw
|
|
network_interfaces:
|
|
version_added: "2.0"
|
|
description:
|
|
- A list of existing network interfaces to attach to the instance at launch. When specifying existing network interfaces,
|
|
none of the I(assign_public_ip), I(private_ip), I(vpc_subnet_id), I(group), or I(group_id) parameters may be used. (Those parameters are
|
|
for creating a new network interface at launch.)
|
|
aliases: ['network_interface']
|
|
type: list
|
|
elements: str
|
|
spot_launch_group:
|
|
version_added: "2.1"
|
|
description:
|
|
- Launch group for spot requests, see U(https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/how-spot-instances-work.html#spot-launch-group).
|
|
type: str
|
|
author:
|
|
- "Tim Gerla (@tgerla)"
|
|
- "Lester Wade (@lwade)"
|
|
- "Seth Vidal (@skvidal)"
|
|
extends_documentation_fragment:
|
|
- aws
|
|
- ec2
|
|
'''
|
|
|
|
EXAMPLES = '''
|
|
# Note: These examples do not set authentication details, see the AWS Guide for details.
|
|
|
|
# Basic provisioning example
|
|
- ec2:
|
|
key_name: mykey
|
|
instance_type: t2.micro
|
|
image: ami-123456
|
|
wait: yes
|
|
group: webserver
|
|
count: 3
|
|
vpc_subnet_id: subnet-29e63245
|
|
assign_public_ip: yes
|
|
|
|
# Advanced example with tagging and CloudWatch
|
|
- ec2:
|
|
key_name: mykey
|
|
group: databases
|
|
instance_type: t2.micro
|
|
image: ami-123456
|
|
wait: yes
|
|
wait_timeout: 500
|
|
count: 5
|
|
instance_tags:
|
|
db: postgres
|
|
monitoring: yes
|
|
vpc_subnet_id: subnet-29e63245
|
|
assign_public_ip: yes
|
|
|
|
# Single instance with additional IOPS volume from snapshot and volume delete on termination
|
|
- ec2:
|
|
key_name: mykey
|
|
group: webserver
|
|
instance_type: c3.medium
|
|
image: ami-123456
|
|
wait: yes
|
|
wait_timeout: 500
|
|
volumes:
|
|
- device_name: /dev/sdb
|
|
snapshot: snap-abcdef12
|
|
volume_type: io1
|
|
iops: 1000
|
|
volume_size: 100
|
|
delete_on_termination: true
|
|
monitoring: yes
|
|
vpc_subnet_id: subnet-29e63245
|
|
assign_public_ip: yes
|
|
|
|
# Single instance with ssd gp2 root volume
|
|
- ec2:
|
|
key_name: mykey
|
|
group: webserver
|
|
instance_type: c3.medium
|
|
image: ami-123456
|
|
wait: yes
|
|
wait_timeout: 500
|
|
volumes:
|
|
- device_name: /dev/xvda
|
|
volume_type: gp2
|
|
volume_size: 8
|
|
vpc_subnet_id: subnet-29e63245
|
|
assign_public_ip: yes
|
|
count_tag:
|
|
Name: dbserver
|
|
exact_count: 1
|
|
|
|
# Multiple groups example
|
|
- ec2:
|
|
key_name: mykey
|
|
group: ['databases', 'internal-services', 'sshable', 'and-so-forth']
|
|
instance_type: m1.large
|
|
image: ami-6e649707
|
|
wait: yes
|
|
wait_timeout: 500
|
|
count: 5
|
|
instance_tags:
|
|
db: postgres
|
|
monitoring: yes
|
|
vpc_subnet_id: subnet-29e63245
|
|
assign_public_ip: yes
|
|
|
|
# Multiple instances with additional volume from snapshot
|
|
- ec2:
|
|
key_name: mykey
|
|
group: webserver
|
|
instance_type: m1.large
|
|
image: ami-6e649707
|
|
wait: yes
|
|
wait_timeout: 500
|
|
count: 5
|
|
volumes:
|
|
- device_name: /dev/sdb
|
|
snapshot: snap-abcdef12
|
|
volume_size: 10
|
|
monitoring: yes
|
|
vpc_subnet_id: subnet-29e63245
|
|
assign_public_ip: yes
|
|
|
|
# Dedicated tenancy example
|
|
- local_action:
|
|
module: ec2
|
|
assign_public_ip: yes
|
|
group_id: sg-1dc53f72
|
|
key_name: mykey
|
|
image: ami-6e649707
|
|
instance_type: m1.small
|
|
tenancy: dedicated
|
|
vpc_subnet_id: subnet-29e63245
|
|
wait: yes
|
|
|
|
# Spot instance example
|
|
- ec2:
|
|
spot_price: 0.24
|
|
spot_wait_timeout: 600
|
|
keypair: mykey
|
|
group_id: sg-1dc53f72
|
|
instance_type: m1.small
|
|
image: ami-6e649707
|
|
wait: yes
|
|
vpc_subnet_id: subnet-29e63245
|
|
assign_public_ip: yes
|
|
spot_launch_group: report_generators
|
|
instance_initiated_shutdown_behavior: terminate
|
|
|
|
# Examples using pre-existing network interfaces
|
|
- ec2:
|
|
key_name: mykey
|
|
instance_type: t2.small
|
|
image: ami-f005ba11
|
|
network_interface: eni-deadbeef
|
|
|
|
- ec2:
|
|
key_name: mykey
|
|
instance_type: t2.small
|
|
image: ami-f005ba11
|
|
network_interfaces: ['eni-deadbeef', 'eni-5ca1ab1e']
|
|
|
|
# Launch instances, runs some tasks
|
|
# and then terminate them
|
|
|
|
- name: Create a sandbox instance
|
|
hosts: localhost
|
|
gather_facts: False
|
|
vars:
|
|
keypair: my_keypair
|
|
instance_type: m1.small
|
|
security_group: my_securitygroup
|
|
image: my_ami_id
|
|
region: us-east-1
|
|
tasks:
|
|
- name: Launch instance
|
|
ec2:
|
|
key_name: "{{ keypair }}"
|
|
group: "{{ security_group }}"
|
|
instance_type: "{{ instance_type }}"
|
|
image: "{{ image }}"
|
|
wait: true
|
|
region: "{{ region }}"
|
|
vpc_subnet_id: subnet-29e63245
|
|
assign_public_ip: yes
|
|
register: ec2
|
|
|
|
- name: Add new instance to host group
|
|
add_host:
|
|
hostname: "{{ item.public_ip }}"
|
|
groupname: launched
|
|
loop: "{{ ec2.instances }}"
|
|
|
|
- name: Wait for SSH to come up
|
|
delegate_to: "{{ item.public_dns_name }}"
|
|
wait_for_connection:
|
|
delay: 60
|
|
timeout: 320
|
|
loop: "{{ ec2.instances }}"
|
|
|
|
- name: Configure instance(s)
|
|
hosts: launched
|
|
become: True
|
|
gather_facts: True
|
|
roles:
|
|
- my_awesome_role
|
|
- my_awesome_test
|
|
|
|
- name: Terminate instances
|
|
hosts: localhost
|
|
tasks:
|
|
- name: Terminate instances that were previously launched
|
|
ec2:
|
|
state: 'absent'
|
|
instance_ids: '{{ ec2.instance_ids }}'
|
|
|
|
# Start a few existing instances, run some tasks
|
|
# and stop the instances
|
|
|
|
- name: Start sandbox instances
|
|
hosts: localhost
|
|
gather_facts: false
|
|
vars:
|
|
instance_ids:
|
|
- 'i-xxxxxx'
|
|
- 'i-xxxxxx'
|
|
- 'i-xxxxxx'
|
|
region: us-east-1
|
|
tasks:
|
|
- name: Start the sandbox instances
|
|
ec2:
|
|
instance_ids: '{{ instance_ids }}'
|
|
region: '{{ region }}'
|
|
state: running
|
|
wait: True
|
|
vpc_subnet_id: subnet-29e63245
|
|
assign_public_ip: yes
|
|
roles:
|
|
- do_neat_stuff
|
|
- do_more_neat_stuff
|
|
|
|
- name: Stop sandbox instances
|
|
hosts: localhost
|
|
gather_facts: false
|
|
vars:
|
|
instance_ids:
|
|
- 'i-xxxxxx'
|
|
- 'i-xxxxxx'
|
|
- 'i-xxxxxx'
|
|
region: us-east-1
|
|
tasks:
|
|
- name: Stop the sandbox instances
|
|
ec2:
|
|
instance_ids: '{{ instance_ids }}'
|
|
region: '{{ region }}'
|
|
state: stopped
|
|
wait: True
|
|
vpc_subnet_id: subnet-29e63245
|
|
assign_public_ip: yes
|
|
|
|
#
|
|
# Start stopped instances specified by tag
|
|
#
|
|
- local_action:
|
|
module: ec2
|
|
instance_tags:
|
|
Name: ExtraPower
|
|
state: running
|
|
|
|
#
|
|
# Restart instances specified by tag
|
|
#
|
|
- local_action:
|
|
module: ec2
|
|
instance_tags:
|
|
Name: ExtraPower
|
|
state: restarted
|
|
|
|
#
|
|
# Enforce that 5 instances with a tag "foo" are running
|
|
# (Highly recommended!)
|
|
#
|
|
|
|
- ec2:
|
|
key_name: mykey
|
|
instance_type: c1.medium
|
|
image: ami-40603AD1
|
|
wait: yes
|
|
group: webserver
|
|
instance_tags:
|
|
foo: bar
|
|
exact_count: 5
|
|
count_tag: foo
|
|
vpc_subnet_id: subnet-29e63245
|
|
assign_public_ip: yes
|
|
|
|
#
|
|
# Enforce that 5 running instances named "database" with a "dbtype" of "postgres"
|
|
#
|
|
|
|
- ec2:
|
|
key_name: mykey
|
|
instance_type: c1.medium
|
|
image: ami-40603AD1
|
|
wait: yes
|
|
group: webserver
|
|
instance_tags:
|
|
Name: database
|
|
dbtype: postgres
|
|
exact_count: 5
|
|
count_tag:
|
|
Name: database
|
|
dbtype: postgres
|
|
vpc_subnet_id: subnet-29e63245
|
|
assign_public_ip: yes
|
|
|
|
#
|
|
# count_tag complex argument examples
|
|
#
|
|
|
|
# instances with tag foo
|
|
- ec2:
|
|
count_tag:
|
|
foo:
|
|
|
|
# instances with tag foo=bar
|
|
- ec2:
|
|
count_tag:
|
|
foo: bar
|
|
|
|
# instances with tags foo=bar & baz
|
|
- ec2:
|
|
count_tag:
|
|
foo: bar
|
|
baz:
|
|
|
|
# instances with tags foo & bar & baz=bang
|
|
- ec2:
|
|
count_tag:
|
|
- foo
|
|
- bar
|
|
- baz: bang
|
|
|
|
'''
|
|
|
|
import time
|
|
import datetime
|
|
import traceback
|
|
from ast import literal_eval
|
|
from distutils.version import LooseVersion
|
|
|
|
from ansible.module_utils.basic import AnsibleModule
|
|
from ansible.module_utils.ec2 import get_aws_connection_info, ec2_argument_spec, ec2_connect
|
|
from ansible.module_utils.six import get_function_code, string_types
|
|
from ansible.module_utils._text import to_bytes, to_text
|
|
|
|
try:
|
|
import boto.ec2
|
|
from boto.ec2.blockdevicemapping import BlockDeviceType, BlockDeviceMapping
|
|
from boto.exception import EC2ResponseError
|
|
from boto import connect_ec2_endpoint
|
|
from boto import connect_vpc
|
|
HAS_BOTO = True
|
|
except ImportError:
|
|
HAS_BOTO = False
|
|
|
|
|
|
def find_running_instances_by_count_tag(module, ec2, vpc, count_tag, zone=None):
|
|
|
|
# get reservations for instances that match tag(s) and are in the desired state
|
|
state = module.params.get('state')
|
|
if state not in ['running', 'stopped']:
|
|
state = None
|
|
reservations = get_reservations(module, ec2, vpc, tags=count_tag, state=state, zone=zone)
|
|
|
|
instances = []
|
|
for res in reservations:
|
|
if hasattr(res, 'instances'):
|
|
for inst in res.instances:
|
|
if inst.state == 'terminated' or inst.state == 'shutting-down':
|
|
continue
|
|
instances.append(inst)
|
|
|
|
return reservations, instances
|
|
|
|
|
|
def _set_none_to_blank(dictionary):
|
|
result = dictionary
|
|
for k in result:
|
|
if isinstance(result[k], dict):
|
|
result[k] = _set_none_to_blank(result[k])
|
|
elif not result[k]:
|
|
result[k] = ""
|
|
return result
|
|
|
|
|
|
def get_reservations(module, ec2, vpc, tags=None, state=None, zone=None):
|
|
# TODO: filters do not work with tags that have underscores
|
|
filters = dict()
|
|
|
|
vpc_subnet_id = module.params.get('vpc_subnet_id')
|
|
vpc_id = None
|
|
if vpc_subnet_id:
|
|
filters.update({"subnet-id": vpc_subnet_id})
|
|
if vpc:
|
|
vpc_id = vpc.get_all_subnets(subnet_ids=[vpc_subnet_id])[0].vpc_id
|
|
|
|
if vpc_id:
|
|
filters.update({"vpc-id": vpc_id})
|
|
|
|
if tags is not None:
|
|
|
|
if isinstance(tags, str):
|
|
try:
|
|
tags = literal_eval(tags)
|
|
except Exception:
|
|
pass
|
|
|
|
# if not a string type, convert and make sure it's a text string
|
|
if isinstance(tags, int):
|
|
tags = to_text(tags)
|
|
|
|
# if string, we only care that a tag of that name exists
|
|
if isinstance(tags, str):
|
|
filters.update({"tag-key": tags})
|
|
|
|
# if list, append each item to filters
|
|
if isinstance(tags, list):
|
|
for x in tags:
|
|
if isinstance(x, dict):
|
|
x = _set_none_to_blank(x)
|
|
filters.update(dict(("tag:" + tn, tv) for (tn, tv) in x.items()))
|
|
else:
|
|
filters.update({"tag-key": x})
|
|
|
|
# if dict, add the key and value to the filter
|
|
if isinstance(tags, dict):
|
|
tags = _set_none_to_blank(tags)
|
|
filters.update(dict(("tag:" + tn, tv) for (tn, tv) in tags.items()))
|
|
|
|
# lets check to see if the filters dict is empty, if so then stop
|
|
if not filters:
|
|
module.fail_json(msg="Filters based on tag is empty => tags: %s" % (tags))
|
|
|
|
if state:
|
|
# http://stackoverflow.com/questions/437511/what-are-the-valid-instancestates-for-the-amazon-ec2-api
|
|
filters.update({'instance-state-name': state})
|
|
|
|
if zone:
|
|
filters.update({'availability-zone': zone})
|
|
|
|
if module.params.get('id'):
|
|
filters['client-token'] = module.params['id']
|
|
|
|
results = ec2.get_all_instances(filters=filters)
|
|
|
|
return results
|
|
|
|
|
|
def get_instance_info(inst):
|
|
"""
|
|
Retrieves instance information from an instance
|
|
ID and returns it as a dictionary
|
|
"""
|
|
instance_info = {'id': inst.id,
|
|
'ami_launch_index': inst.ami_launch_index,
|
|
'private_ip': inst.private_ip_address,
|
|
'private_dns_name': inst.private_dns_name,
|
|
'public_ip': inst.ip_address,
|
|
'dns_name': inst.dns_name,
|
|
'public_dns_name': inst.public_dns_name,
|
|
'state_code': inst.state_code,
|
|
'architecture': inst.architecture,
|
|
'image_id': inst.image_id,
|
|
'key_name': inst.key_name,
|
|
'placement': inst.placement,
|
|
'region': inst.placement[:-1],
|
|
'kernel': inst.kernel,
|
|
'ramdisk': inst.ramdisk,
|
|
'launch_time': inst.launch_time,
|
|
'instance_type': inst.instance_type,
|
|
'root_device_type': inst.root_device_type,
|
|
'root_device_name': inst.root_device_name,
|
|
'state': inst.state,
|
|
'hypervisor': inst.hypervisor,
|
|
'tags': inst.tags,
|
|
'groups': dict((group.id, group.name) for group in inst.groups),
|
|
}
|
|
try:
|
|
instance_info['virtualization_type'] = getattr(inst, 'virtualization_type')
|
|
except AttributeError:
|
|
instance_info['virtualization_type'] = None
|
|
|
|
try:
|
|
instance_info['ebs_optimized'] = getattr(inst, 'ebs_optimized')
|
|
except AttributeError:
|
|
instance_info['ebs_optimized'] = False
|
|
|
|
try:
|
|
bdm_dict = {}
|
|
bdm = getattr(inst, 'block_device_mapping')
|
|
for device_name in bdm.keys():
|
|
bdm_dict[device_name] = {
|
|
'status': bdm[device_name].status,
|
|
'volume_id': bdm[device_name].volume_id,
|
|
'delete_on_termination': bdm[device_name].delete_on_termination
|
|
}
|
|
instance_info['block_device_mapping'] = bdm_dict
|
|
except AttributeError:
|
|
instance_info['block_device_mapping'] = False
|
|
|
|
try:
|
|
instance_info['tenancy'] = getattr(inst, 'placement_tenancy')
|
|
except AttributeError:
|
|
instance_info['tenancy'] = 'default'
|
|
|
|
return instance_info
|
|
|
|
|
|
def boto_supports_associate_public_ip_address(ec2):
|
|
"""
|
|
Check if Boto library has associate_public_ip_address in the NetworkInterfaceSpecification
|
|
class. Added in Boto 2.13.0
|
|
|
|
ec2: authenticated ec2 connection object
|
|
|
|
Returns:
|
|
True if Boto library accepts associate_public_ip_address argument, else false
|
|
"""
|
|
|
|
try:
|
|
network_interface = boto.ec2.networkinterface.NetworkInterfaceSpecification()
|
|
getattr(network_interface, "associate_public_ip_address")
|
|
return True
|
|
except AttributeError:
|
|
return False
|
|
|
|
|
|
def boto_supports_profile_name_arg(ec2):
|
|
"""
|
|
Check if Boto library has instance_profile_name argument. instance_profile_name has been added in Boto 2.5.0
|
|
|
|
ec2: authenticated ec2 connection object
|
|
|
|
Returns:
|
|
True if Boto library accept instance_profile_name argument, else false
|
|
"""
|
|
run_instances_method = getattr(ec2, 'run_instances')
|
|
return 'instance_profile_name' in get_function_code(run_instances_method).co_varnames
|
|
|
|
|
|
def boto_supports_volume_encryption():
|
|
"""
|
|
Check if Boto library supports encryption of EBS volumes (added in 2.29.0)
|
|
|
|
Returns:
|
|
True if boto library has the named param as an argument on the request_spot_instances method, else False
|
|
"""
|
|
return hasattr(boto, 'Version') and LooseVersion(boto.Version) >= LooseVersion('2.29.0')
|
|
|
|
|
|
def create_block_device(module, ec2, volume):
|
|
# Not aware of a way to determine this programatically
|
|
# http://aws.amazon.com/about-aws/whats-new/2013/10/09/ebs-provisioned-iops-maximum-iops-gb-ratio-increased-to-30-1/
|
|
MAX_IOPS_TO_SIZE_RATIO = 30
|
|
|
|
volume_type = volume.get('volume_type')
|
|
|
|
if 'snapshot' not in volume and 'ephemeral' not in volume:
|
|
if 'volume_size' not in volume:
|
|
module.fail_json(msg='Size must be specified when creating a new volume or modifying the root volume')
|
|
if 'snapshot' in volume:
|
|
if volume_type == 'io1' and 'iops' not in volume:
|
|
module.fail_json(msg='io1 volumes must have an iops value set')
|
|
if 'iops' in volume:
|
|
snapshot = ec2.get_all_snapshots(snapshot_ids=[volume['snapshot']])[0]
|
|
size = volume.get('volume_size', snapshot.volume_size)
|
|
if int(volume['iops']) > MAX_IOPS_TO_SIZE_RATIO * size:
|
|
module.fail_json(msg='IOPS must be at most %d times greater than size' % MAX_IOPS_TO_SIZE_RATIO)
|
|
if 'ephemeral' in volume:
|
|
if 'snapshot' in volume:
|
|
module.fail_json(msg='Cannot set both ephemeral and snapshot')
|
|
if boto_supports_volume_encryption():
|
|
return BlockDeviceType(snapshot_id=volume.get('snapshot'),
|
|
ephemeral_name=volume.get('ephemeral'),
|
|
size=volume.get('volume_size'),
|
|
volume_type=volume_type,
|
|
delete_on_termination=volume.get('delete_on_termination', False),
|
|
iops=volume.get('iops'),
|
|
encrypted=volume.get('encrypted', None))
|
|
else:
|
|
return BlockDeviceType(snapshot_id=volume.get('snapshot'),
|
|
ephemeral_name=volume.get('ephemeral'),
|
|
size=volume.get('volume_size'),
|
|
volume_type=volume_type,
|
|
delete_on_termination=volume.get('delete_on_termination', False),
|
|
iops=volume.get('iops'))
|
|
|
|
|
|
def boto_supports_param_in_spot_request(ec2, param):
|
|
"""
|
|
Check if Boto library has a <param> in its request_spot_instances() method. For example, the placement_group parameter wasn't added until 2.3.0.
|
|
|
|
ec2: authenticated ec2 connection object
|
|
|
|
Returns:
|
|
True if boto library has the named param as an argument on the request_spot_instances method, else False
|
|
"""
|
|
method = getattr(ec2, 'request_spot_instances')
|
|
return param in get_function_code(method).co_varnames
|
|
|
|
|
|
def await_spot_requests(module, ec2, spot_requests, count):
|
|
"""
|
|
Wait for a group of spot requests to be fulfilled, or fail.
|
|
|
|
module: Ansible module object
|
|
ec2: authenticated ec2 connection object
|
|
spot_requests: boto.ec2.spotinstancerequest.SpotInstanceRequest object returned by ec2.request_spot_instances
|
|
count: Total number of instances to be created by the spot requests
|
|
|
|
Returns:
|
|
list of instance ID's created by the spot request(s)
|
|
"""
|
|
spot_wait_timeout = int(module.params.get('spot_wait_timeout'))
|
|
wait_complete = time.time() + spot_wait_timeout
|
|
|
|
spot_req_inst_ids = dict()
|
|
while time.time() < wait_complete:
|
|
reqs = ec2.get_all_spot_instance_requests()
|
|
for sirb in spot_requests:
|
|
if sirb.id in spot_req_inst_ids:
|
|
continue
|
|
for sir in reqs:
|
|
if sir.id != sirb.id:
|
|
continue # this is not our spot instance
|
|
if sir.instance_id is not None:
|
|
spot_req_inst_ids[sirb.id] = sir.instance_id
|
|
elif sir.state == 'open':
|
|
continue # still waiting, nothing to do here
|
|
elif sir.state == 'active':
|
|
continue # Instance is created already, nothing to do here
|
|
elif sir.state == 'failed':
|
|
module.fail_json(msg="Spot instance request %s failed with status %s and fault %s:%s" % (
|
|
sir.id, sir.status.code, sir.fault.code, sir.fault.message))
|
|
elif sir.state == 'cancelled':
|
|
module.fail_json(msg="Spot instance request %s was cancelled before it could be fulfilled." % sir.id)
|
|
elif sir.state == 'closed':
|
|
# instance is terminating or marked for termination
|
|
# this may be intentional on the part of the operator,
|
|
# or it may have been terminated by AWS due to capacity,
|
|
# price, or group constraints in this case, we'll fail
|
|
# the module if the reason for the state is anything
|
|
# other than termination by user. Codes are documented at
|
|
# https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/spot-bid-status.html
|
|
if sir.status.code == 'instance-terminated-by-user':
|
|
# do nothing, since the user likely did this on purpose
|
|
pass
|
|
else:
|
|
spot_msg = "Spot instance request %s was closed by AWS with the status %s and fault %s:%s"
|
|
module.fail_json(msg=spot_msg % (sir.id, sir.status.code, sir.fault.code, sir.fault.message))
|
|
|
|
if len(spot_req_inst_ids) < count:
|
|
time.sleep(5)
|
|
else:
|
|
return list(spot_req_inst_ids.values())
|
|
module.fail_json(msg="wait for spot requests timeout on %s" % time.asctime())
|
|
|
|
|
|
def enforce_count(module, ec2, vpc):
|
|
|
|
exact_count = module.params.get('exact_count')
|
|
count_tag = module.params.get('count_tag')
|
|
zone = module.params.get('zone')
|
|
|
|
# fail here if the exact count was specified without filtering
|
|
# on a tag, as this may lead to a undesired removal of instances
|
|
if exact_count and count_tag is None:
|
|
module.fail_json(msg="you must use the 'count_tag' option with exact_count")
|
|
|
|
reservations, instances = find_running_instances_by_count_tag(module, ec2, vpc, count_tag, zone)
|
|
|
|
changed = None
|
|
checkmode = False
|
|
instance_dict_array = []
|
|
changed_instance_ids = None
|
|
|
|
if len(instances) == exact_count:
|
|
changed = False
|
|
elif len(instances) < exact_count:
|
|
changed = True
|
|
to_create = exact_count - len(instances)
|
|
if not checkmode:
|
|
(instance_dict_array, changed_instance_ids, changed) \
|
|
= create_instances(module, ec2, vpc, override_count=to_create)
|
|
|
|
for inst in instance_dict_array:
|
|
instances.append(inst)
|
|
elif len(instances) > exact_count:
|
|
changed = True
|
|
to_remove = len(instances) - exact_count
|
|
if not checkmode:
|
|
all_instance_ids = sorted([x.id for x in instances])
|
|
remove_ids = all_instance_ids[0:to_remove]
|
|
|
|
instances = [x for x in instances if x.id not in remove_ids]
|
|
|
|
(changed, instance_dict_array, changed_instance_ids) \
|
|
= terminate_instances(module, ec2, remove_ids)
|
|
terminated_list = []
|
|
for inst in instance_dict_array:
|
|
inst['state'] = "terminated"
|
|
terminated_list.append(inst)
|
|
instance_dict_array = terminated_list
|
|
|
|
# ensure all instances are dictionaries
|
|
all_instances = []
|
|
for inst in instances:
|
|
|
|
if not isinstance(inst, dict):
|
|
warn_if_public_ip_assignment_changed(module, inst)
|
|
inst = get_instance_info(inst)
|
|
all_instances.append(inst)
|
|
|
|
return (all_instances, instance_dict_array, changed_instance_ids, changed)
|
|
|
|
|
|
def create_instances(module, ec2, vpc, override_count=None):
|
|
"""
|
|
Creates new instances
|
|
|
|
module : AnsibleModule object
|
|
ec2: authenticated ec2 connection object
|
|
|
|
Returns:
|
|
A list of dictionaries with instance information
|
|
about the instances that were launched
|
|
"""
|
|
|
|
key_name = module.params.get('key_name')
|
|
id = module.params.get('id')
|
|
group_name = module.params.get('group')
|
|
group_id = module.params.get('group_id')
|
|
zone = module.params.get('zone')
|
|
instance_type = module.params.get('instance_type')
|
|
tenancy = module.params.get('tenancy')
|
|
spot_price = module.params.get('spot_price')
|
|
spot_type = module.params.get('spot_type')
|
|
image = module.params.get('image')
|
|
if override_count:
|
|
count = override_count
|
|
else:
|
|
count = module.params.get('count')
|
|
monitoring = module.params.get('monitoring')
|
|
kernel = module.params.get('kernel')
|
|
ramdisk = module.params.get('ramdisk')
|
|
wait = module.params.get('wait')
|
|
wait_timeout = int(module.params.get('wait_timeout'))
|
|
spot_wait_timeout = int(module.params.get('spot_wait_timeout'))
|
|
placement_group = module.params.get('placement_group')
|
|
user_data = module.params.get('user_data')
|
|
instance_tags = module.params.get('instance_tags')
|
|
vpc_subnet_id = module.params.get('vpc_subnet_id')
|
|
assign_public_ip = module.boolean(module.params.get('assign_public_ip'))
|
|
private_ip = module.params.get('private_ip')
|
|
instance_profile_name = module.params.get('instance_profile_name')
|
|
volumes = module.params.get('volumes')
|
|
ebs_optimized = module.params.get('ebs_optimized')
|
|
exact_count = module.params.get('exact_count')
|
|
count_tag = module.params.get('count_tag')
|
|
source_dest_check = module.boolean(module.params.get('source_dest_check'))
|
|
termination_protection = module.boolean(module.params.get('termination_protection'))
|
|
network_interfaces = module.params.get('network_interfaces')
|
|
spot_launch_group = module.params.get('spot_launch_group')
|
|
instance_initiated_shutdown_behavior = module.params.get('instance_initiated_shutdown_behavior')
|
|
|
|
vpc_id = None
|
|
if vpc_subnet_id:
|
|
if not vpc:
|
|
module.fail_json(msg="region must be specified")
|
|
else:
|
|
vpc_id = vpc.get_all_subnets(subnet_ids=[vpc_subnet_id])[0].vpc_id
|
|
else:
|
|
vpc_id = None
|
|
|
|
try:
|
|
# Here we try to lookup the group id from the security group name - if group is set.
|
|
if group_name:
|
|
if vpc_id:
|
|
grp_details = ec2.get_all_security_groups(filters={'vpc_id': vpc_id})
|
|
else:
|
|
grp_details = ec2.get_all_security_groups()
|
|
if isinstance(group_name, string_types):
|
|
group_name = [group_name]
|
|
unmatched = set(group_name).difference(str(grp.name) for grp in grp_details)
|
|
if len(unmatched) > 0:
|
|
module.fail_json(msg="The following group names are not valid: %s" % ', '.join(unmatched))
|
|
group_id = [str(grp.id) for grp in grp_details if str(grp.name) in group_name]
|
|
# Now we try to lookup the group id testing if group exists.
|
|
elif group_id:
|
|
# wrap the group_id in a list if it's not one already
|
|
if isinstance(group_id, string_types):
|
|
group_id = [group_id]
|
|
grp_details = ec2.get_all_security_groups(group_ids=group_id)
|
|
group_name = [grp_item.name for grp_item in grp_details]
|
|
except boto.exception.NoAuthHandlerFound as e:
|
|
module.fail_json(msg=str(e))
|
|
|
|
# Lookup any instances that much our run id.
|
|
|
|
running_instances = []
|
|
count_remaining = int(count)
|
|
|
|
if id is not None:
|
|
filter_dict = {'client-token': id, 'instance-state-name': 'running'}
|
|
previous_reservations = ec2.get_all_instances(None, filter_dict)
|
|
for res in previous_reservations:
|
|
for prev_instance in res.instances:
|
|
running_instances.append(prev_instance)
|
|
count_remaining = count_remaining - len(running_instances)
|
|
|
|
# Both min_count and max_count equal count parameter. This means the launch request is explicit (we want count, or fail) in how many instances we want.
|
|
|
|
if count_remaining == 0:
|
|
changed = False
|
|
else:
|
|
changed = True
|
|
try:
|
|
params = {'image_id': image,
|
|
'key_name': key_name,
|
|
'monitoring_enabled': monitoring,
|
|
'placement': zone,
|
|
'instance_type': instance_type,
|
|
'kernel_id': kernel,
|
|
'ramdisk_id': ramdisk}
|
|
if user_data is not None:
|
|
params['user_data'] = to_bytes(user_data, errors='surrogate_or_strict')
|
|
|
|
if ebs_optimized:
|
|
params['ebs_optimized'] = ebs_optimized
|
|
|
|
# 'tenancy' always has a default value, but it is not a valid parameter for spot instance request
|
|
if not spot_price:
|
|
params['tenancy'] = tenancy
|
|
|
|
if boto_supports_profile_name_arg(ec2):
|
|
params['instance_profile_name'] = instance_profile_name
|
|
else:
|
|
if instance_profile_name is not None:
|
|
module.fail_json(
|
|
msg="instance_profile_name parameter requires Boto version 2.5.0 or higher")
|
|
|
|
if assign_public_ip is not None:
|
|
if not boto_supports_associate_public_ip_address(ec2):
|
|
module.fail_json(
|
|
msg="assign_public_ip parameter requires Boto version 2.13.0 or higher.")
|
|
elif not vpc_subnet_id:
|
|
module.fail_json(
|
|
msg="assign_public_ip only available with vpc_subnet_id")
|
|
|
|
else:
|
|
if private_ip:
|
|
interface = boto.ec2.networkinterface.NetworkInterfaceSpecification(
|
|
subnet_id=vpc_subnet_id,
|
|
private_ip_address=private_ip,
|
|
groups=group_id,
|
|
associate_public_ip_address=assign_public_ip)
|
|
else:
|
|
interface = boto.ec2.networkinterface.NetworkInterfaceSpecification(
|
|
subnet_id=vpc_subnet_id,
|
|
groups=group_id,
|
|
associate_public_ip_address=assign_public_ip)
|
|
interfaces = boto.ec2.networkinterface.NetworkInterfaceCollection(interface)
|
|
params['network_interfaces'] = interfaces
|
|
else:
|
|
if network_interfaces:
|
|
if isinstance(network_interfaces, string_types):
|
|
network_interfaces = [network_interfaces]
|
|
interfaces = []
|
|
for i, network_interface_id in enumerate(network_interfaces):
|
|
interface = boto.ec2.networkinterface.NetworkInterfaceSpecification(
|
|
network_interface_id=network_interface_id,
|
|
device_index=i)
|
|
interfaces.append(interface)
|
|
params['network_interfaces'] = \
|
|
boto.ec2.networkinterface.NetworkInterfaceCollection(*interfaces)
|
|
else:
|
|
params['subnet_id'] = vpc_subnet_id
|
|
if vpc_subnet_id:
|
|
params['security_group_ids'] = group_id
|
|
else:
|
|
params['security_groups'] = group_name
|
|
|
|
if volumes:
|
|
bdm = BlockDeviceMapping()
|
|
for volume in volumes:
|
|
if 'device_name' not in volume:
|
|
module.fail_json(msg='Device name must be set for volume')
|
|
# Minimum volume size is 1GiB. We'll use volume size explicitly set to 0
|
|
# to be a signal not to create this volume
|
|
if 'volume_size' not in volume or int(volume['volume_size']) > 0:
|
|
bdm[volume['device_name']] = create_block_device(module, ec2, volume)
|
|
|
|
params['block_device_map'] = bdm
|
|
|
|
# check to see if we're using spot pricing first before starting instances
|
|
if not spot_price:
|
|
if assign_public_ip is not None and private_ip:
|
|
params.update(
|
|
dict(
|
|
min_count=count_remaining,
|
|
max_count=count_remaining,
|
|
client_token=id,
|
|
placement_group=placement_group,
|
|
)
|
|
)
|
|
else:
|
|
params.update(
|
|
dict(
|
|
min_count=count_remaining,
|
|
max_count=count_remaining,
|
|
client_token=id,
|
|
placement_group=placement_group,
|
|
private_ip_address=private_ip,
|
|
)
|
|
)
|
|
|
|
# For ordinary (not spot) instances, we can select 'stop'
|
|
# (the default) or 'terminate' here.
|
|
params['instance_initiated_shutdown_behavior'] = instance_initiated_shutdown_behavior or 'stop'
|
|
|
|
try:
|
|
res = ec2.run_instances(**params)
|
|
except boto.exception.EC2ResponseError as e:
|
|
if (params['instance_initiated_shutdown_behavior'] != 'terminate' and
|
|
"InvalidParameterCombination" == e.error_code):
|
|
params['instance_initiated_shutdown_behavior'] = 'terminate'
|
|
res = ec2.run_instances(**params)
|
|
else:
|
|
raise
|
|
|
|
instids = [i.id for i in res.instances]
|
|
while True:
|
|
try:
|
|
ec2.get_all_instances(instids)
|
|
break
|
|
except boto.exception.EC2ResponseError as e:
|
|
if "<Code>InvalidInstanceID.NotFound</Code>" in str(e):
|
|
# there's a race between start and get an instance
|
|
continue
|
|
else:
|
|
module.fail_json(msg=str(e))
|
|
|
|
# The instances returned through ec2.run_instances above can be in
|
|
# terminated state due to idempotency. See commit 7f11c3d for a complete
|
|
# explanation.
|
|
terminated_instances = [
|
|
str(instance.id) for instance in res.instances if instance.state == 'terminated'
|
|
]
|
|
if terminated_instances:
|
|
module.fail_json(msg="Instances with id(s) %s " % terminated_instances +
|
|
"were created previously but have since been terminated - " +
|
|
"use a (possibly different) 'instanceid' parameter")
|
|
|
|
else:
|
|
if private_ip:
|
|
module.fail_json(
|
|
msg='private_ip only available with on-demand (non-spot) instances')
|
|
if boto_supports_param_in_spot_request(ec2, 'placement_group'):
|
|
params['placement_group'] = placement_group
|
|
elif placement_group:
|
|
module.fail_json(
|
|
msg="placement_group parameter requires Boto version 2.3.0 or higher.")
|
|
|
|
# You can't tell spot instances to 'stop'; they will always be
|
|
# 'terminate'd. For convenience, we'll ignore the latter value.
|
|
if instance_initiated_shutdown_behavior and instance_initiated_shutdown_behavior != 'terminate':
|
|
module.fail_json(
|
|
msg="instance_initiated_shutdown_behavior=stop is not supported for spot instances.")
|
|
|
|
if spot_launch_group and isinstance(spot_launch_group, string_types):
|
|
params['launch_group'] = spot_launch_group
|
|
|
|
params.update(dict(
|
|
count=count_remaining,
|
|
type=spot_type,
|
|
))
|
|
|
|
# Set spot ValidUntil
|
|
# ValidUntil -> (timestamp). The end date of the request, in
|
|
# UTC format (for example, YYYY -MM -DD T*HH* :MM :SS Z).
|
|
utc_valid_until = (
|
|
datetime.datetime.utcnow()
|
|
+ datetime.timedelta(seconds=spot_wait_timeout))
|
|
params['valid_until'] = utc_valid_until.strftime('%Y-%m-%dT%H:%M:%S.000Z')
|
|
|
|
res = ec2.request_spot_instances(spot_price, **params)
|
|
|
|
# Now we have to do the intermediate waiting
|
|
if wait:
|
|
instids = await_spot_requests(module, ec2, res, count)
|
|
else:
|
|
instids = []
|
|
except boto.exception.BotoServerError as e:
|
|
module.fail_json(msg="Instance creation failed => %s: %s" % (e.error_code, e.error_message))
|
|
|
|
# wait here until the instances are up
|
|
num_running = 0
|
|
wait_timeout = time.time() + wait_timeout
|
|
res_list = ()
|
|
while wait_timeout > time.time() and num_running < len(instids):
|
|
try:
|
|
res_list = ec2.get_all_instances(instids)
|
|
except boto.exception.BotoServerError as e:
|
|
if e.error_code == 'InvalidInstanceID.NotFound':
|
|
time.sleep(1)
|
|
continue
|
|
else:
|
|
raise
|
|
|
|
num_running = 0
|
|
for res in res_list:
|
|
num_running += len([i for i in res.instances if i.state == 'running'])
|
|
if len(res_list) <= 0:
|
|
# got a bad response of some sort, possibly due to
|
|
# stale/cached data. Wait a second and then try again
|
|
time.sleep(1)
|
|
continue
|
|
if wait and num_running < len(instids):
|
|
time.sleep(5)
|
|
else:
|
|
break
|
|
|
|
if wait and wait_timeout <= time.time():
|
|
# waiting took too long
|
|
module.fail_json(msg="wait for instances running timeout on %s" % time.asctime())
|
|
|
|
# We do this after the loop ends so that we end up with one list
|
|
for res in res_list:
|
|
running_instances.extend(res.instances)
|
|
|
|
# Enabled by default by AWS
|
|
if source_dest_check is False:
|
|
for inst in res.instances:
|
|
inst.modify_attribute('sourceDestCheck', False)
|
|
|
|
# Disabled by default by AWS
|
|
if termination_protection is True:
|
|
for inst in res.instances:
|
|
inst.modify_attribute('disableApiTermination', True)
|
|
|
|
# Leave this as late as possible to try and avoid InvalidInstanceID.NotFound
|
|
if instance_tags and instids:
|
|
try:
|
|
ec2.create_tags(instids, instance_tags)
|
|
except boto.exception.EC2ResponseError as e:
|
|
module.fail_json(msg="Instance tagging failed => %s: %s" % (e.error_code, e.error_message))
|
|
|
|
instance_dict_array = []
|
|
created_instance_ids = []
|
|
for inst in running_instances:
|
|
inst.update()
|
|
d = get_instance_info(inst)
|
|
created_instance_ids.append(inst.id)
|
|
instance_dict_array.append(d)
|
|
|
|
return (instance_dict_array, created_instance_ids, changed)
|
|
|
|
|
|
def terminate_instances(module, ec2, instance_ids):
|
|
"""
|
|
Terminates a list of instances
|
|
|
|
module: Ansible module object
|
|
ec2: authenticated ec2 connection object
|
|
termination_list: a list of instances to terminate in the form of
|
|
[ {id: <inst-id>}, ..]
|
|
|
|
Returns a dictionary of instance information
|
|
about the instances terminated.
|
|
|
|
If the instance to be terminated is running
|
|
"changed" will be set to False.
|
|
|
|
"""
|
|
|
|
# Whether to wait for termination to complete before returning
|
|
wait = module.params.get('wait')
|
|
wait_timeout = int(module.params.get('wait_timeout'))
|
|
|
|
changed = False
|
|
instance_dict_array = []
|
|
|
|
if not isinstance(instance_ids, list) or len(instance_ids) < 1:
|
|
module.fail_json(msg='instance_ids should be a list of instances, aborting')
|
|
|
|
terminated_instance_ids = []
|
|
for res in ec2.get_all_instances(instance_ids):
|
|
for inst in res.instances:
|
|
if inst.state == 'running' or inst.state == 'stopped':
|
|
terminated_instance_ids.append(inst.id)
|
|
instance_dict_array.append(get_instance_info(inst))
|
|
try:
|
|
ec2.terminate_instances([inst.id])
|
|
except EC2ResponseError as e:
|
|
module.fail_json(msg='Unable to terminate instance {0}, error: {1}'.format(inst.id, e))
|
|
changed = True
|
|
|
|
# wait here until the instances are 'terminated'
|
|
if wait:
|
|
num_terminated = 0
|
|
wait_timeout = time.time() + wait_timeout
|
|
while wait_timeout > time.time() and num_terminated < len(terminated_instance_ids):
|
|
response = ec2.get_all_instances(instance_ids=terminated_instance_ids,
|
|
filters={'instance-state-name': 'terminated'})
|
|
try:
|
|
num_terminated = sum([len(res.instances) for res in response])
|
|
except Exception as e:
|
|
# got a bad response of some sort, possibly due to
|
|
# stale/cached data. Wait a second and then try again
|
|
time.sleep(1)
|
|
continue
|
|
|
|
if num_terminated < len(terminated_instance_ids):
|
|
time.sleep(5)
|
|
|
|
# waiting took too long
|
|
if wait_timeout < time.time() and num_terminated < len(terminated_instance_ids):
|
|
module.fail_json(msg="wait for instance termination timeout on %s" % time.asctime())
|
|
# Lets get the current state of the instances after terminating - issue600
|
|
instance_dict_array = []
|
|
for res in ec2.get_all_instances(instance_ids=terminated_instance_ids, filters={'instance-state-name': 'terminated'}):
|
|
for inst in res.instances:
|
|
instance_dict_array.append(get_instance_info(inst))
|
|
|
|
return (changed, instance_dict_array, terminated_instance_ids)
|
|
|
|
|
|
def startstop_instances(module, ec2, instance_ids, state, instance_tags):
|
|
"""
|
|
Starts or stops a list of existing instances
|
|
|
|
module: Ansible module object
|
|
ec2: authenticated ec2 connection object
|
|
instance_ids: The list of instances to start in the form of
|
|
[ {id: <inst-id>}, ..]
|
|
instance_tags: A dict of tag keys and values in the form of
|
|
{key: value, ... }
|
|
state: Intended state ("running" or "stopped")
|
|
|
|
Returns a dictionary of instance information
|
|
about the instances started/stopped.
|
|
|
|
If the instance was not able to change state,
|
|
"changed" will be set to False.
|
|
|
|
Note that if instance_ids and instance_tags are both non-empty,
|
|
this method will process the intersection of the two
|
|
"""
|
|
|
|
wait = module.params.get('wait')
|
|
wait_timeout = int(module.params.get('wait_timeout'))
|
|
group_id = module.params.get('group_id')
|
|
group_name = module.params.get('group')
|
|
changed = False
|
|
instance_dict_array = []
|
|
|
|
if not isinstance(instance_ids, list) or len(instance_ids) < 1:
|
|
# Fail unless the user defined instance tags
|
|
if not instance_tags:
|
|
module.fail_json(msg='instance_ids should be a list of instances, aborting')
|
|
|
|
# To make an EC2 tag filter, we need to prepend 'tag:' to each key.
|
|
# An empty filter does no filtering, so it's safe to pass it to the
|
|
# get_all_instances method even if the user did not specify instance_tags
|
|
filters = {}
|
|
if instance_tags:
|
|
for key, value in instance_tags.items():
|
|
filters["tag:" + key] = value
|
|
|
|
if module.params.get('id'):
|
|
filters['client-token'] = module.params['id']
|
|
# Check that our instances are not in the state we want to take
|
|
|
|
# Check (and eventually change) instances attributes and instances state
|
|
existing_instances_array = []
|
|
for res in ec2.get_all_instances(instance_ids, filters=filters):
|
|
for inst in res.instances:
|
|
|
|
warn_if_public_ip_assignment_changed(module, inst)
|
|
|
|
changed = (check_source_dest_attr(module, inst, ec2) or
|
|
check_termination_protection(module, inst) or changed)
|
|
|
|
# Check security groups and if we're using ec2-vpc; ec2-classic security groups may not be modified
|
|
if inst.vpc_id and group_name:
|
|
grp_details = ec2.get_all_security_groups(filters={'vpc_id': inst.vpc_id})
|
|
if isinstance(group_name, string_types):
|
|
group_name = [group_name]
|
|
unmatched = set(group_name) - set(to_text(grp.name) for grp in grp_details)
|
|
if unmatched:
|
|
module.fail_json(msg="The following group names are not valid: %s" % ', '.join(unmatched))
|
|
group_ids = [to_text(grp.id) for grp in grp_details if to_text(grp.name) in group_name]
|
|
elif inst.vpc_id and group_id:
|
|
if isinstance(group_id, string_types):
|
|
group_id = [group_id]
|
|
grp_details = ec2.get_all_security_groups(group_ids=group_id)
|
|
group_ids = [grp_item.id for grp_item in grp_details]
|
|
if inst.vpc_id and (group_name or group_id):
|
|
if set(sg.id for sg in inst.groups) != set(group_ids):
|
|
changed = inst.modify_attribute('groupSet', group_ids)
|
|
|
|
# Check instance state
|
|
if inst.state != state:
|
|
instance_dict_array.append(get_instance_info(inst))
|
|
try:
|
|
if state == 'running':
|
|
inst.start()
|
|
else:
|
|
inst.stop()
|
|
except EC2ResponseError as e:
|
|
module.fail_json(msg='Unable to change state for instance {0}, error: {1}'.format(inst.id, e))
|
|
changed = True
|
|
existing_instances_array.append(inst.id)
|
|
|
|
instance_ids = list(set(existing_instances_array + (instance_ids or [])))
|
|
# Wait for all the instances to finish starting or stopping
|
|
wait_timeout = time.time() + wait_timeout
|
|
while wait and wait_timeout > time.time():
|
|
instance_dict_array = []
|
|
matched_instances = []
|
|
for res in ec2.get_all_instances(instance_ids):
|
|
for i in res.instances:
|
|
if i.state == state:
|
|
instance_dict_array.append(get_instance_info(i))
|
|
matched_instances.append(i)
|
|
if len(matched_instances) < len(instance_ids):
|
|
time.sleep(5)
|
|
else:
|
|
break
|
|
|
|
if wait and wait_timeout <= time.time():
|
|
# waiting took too long
|
|
module.fail_json(msg="wait for instances running timeout on %s" % time.asctime())
|
|
|
|
return (changed, instance_dict_array, instance_ids)
|
|
|
|
|
|
def restart_instances(module, ec2, instance_ids, state, instance_tags):
|
|
"""
|
|
Restarts a list of existing instances
|
|
|
|
module: Ansible module object
|
|
ec2: authenticated ec2 connection object
|
|
instance_ids: The list of instances to start in the form of
|
|
[ {id: <inst-id>}, ..]
|
|
instance_tags: A dict of tag keys and values in the form of
|
|
{key: value, ... }
|
|
state: Intended state ("restarted")
|
|
|
|
Returns a dictionary of instance information
|
|
about the instances.
|
|
|
|
If the instance was not able to change state,
|
|
"changed" will be set to False.
|
|
|
|
Wait will not apply here as this is a OS level operation.
|
|
|
|
Note that if instance_ids and instance_tags are both non-empty,
|
|
this method will process the intersection of the two.
|
|
"""
|
|
|
|
changed = False
|
|
instance_dict_array = []
|
|
|
|
if not isinstance(instance_ids, list) or len(instance_ids) < 1:
|
|
# Fail unless the user defined instance tags
|
|
if not instance_tags:
|
|
module.fail_json(msg='instance_ids should be a list of instances, aborting')
|
|
|
|
# To make an EC2 tag filter, we need to prepend 'tag:' to each key.
|
|
# An empty filter does no filtering, so it's safe to pass it to the
|
|
# get_all_instances method even if the user did not specify instance_tags
|
|
filters = {}
|
|
if instance_tags:
|
|
for key, value in instance_tags.items():
|
|
filters["tag:" + key] = value
|
|
if module.params.get('id'):
|
|
filters['client-token'] = module.params['id']
|
|
|
|
# Check that our instances are not in the state we want to take
|
|
|
|
# Check (and eventually change) instances attributes and instances state
|
|
for res in ec2.get_all_instances(instance_ids, filters=filters):
|
|
for inst in res.instances:
|
|
|
|
warn_if_public_ip_assignment_changed(module, inst)
|
|
|
|
changed = (check_source_dest_attr(module, inst, ec2) or
|
|
check_termination_protection(module, inst) or changed)
|
|
|
|
# Check instance state
|
|
if inst.state != state:
|
|
instance_dict_array.append(get_instance_info(inst))
|
|
try:
|
|
inst.reboot()
|
|
except EC2ResponseError as e:
|
|
module.fail_json(msg='Unable to change state for instance {0}, error: {1}'.format(inst.id, e))
|
|
changed = True
|
|
|
|
return (changed, instance_dict_array, instance_ids)
|
|
|
|
|
|
def check_termination_protection(module, inst):
|
|
"""
|
|
Check the instance disableApiTermination attribute.
|
|
|
|
module: Ansible module object
|
|
inst: EC2 instance object
|
|
|
|
returns: True if state changed None otherwise
|
|
"""
|
|
|
|
termination_protection = module.params.get('termination_protection')
|
|
|
|
if (inst.get_attribute('disableApiTermination')['disableApiTermination'] != termination_protection and termination_protection is not None):
|
|
inst.modify_attribute('disableApiTermination', termination_protection)
|
|
return True
|
|
|
|
|
|
def check_source_dest_attr(module, inst, ec2):
|
|
"""
|
|
Check the instance sourceDestCheck attribute.
|
|
|
|
module: Ansible module object
|
|
inst: EC2 instance object
|
|
|
|
returns: True if state changed None otherwise
|
|
"""
|
|
|
|
source_dest_check = module.params.get('source_dest_check')
|
|
|
|
if source_dest_check is not None:
|
|
try:
|
|
if inst.vpc_id is not None and inst.get_attribute('sourceDestCheck')['sourceDestCheck'] != source_dest_check:
|
|
inst.modify_attribute('sourceDestCheck', source_dest_check)
|
|
return True
|
|
except boto.exception.EC2ResponseError as exc:
|
|
# instances with more than one Elastic Network Interface will
|
|
# fail, because they have the sourceDestCheck attribute defined
|
|
# per-interface
|
|
if exc.code == 'InvalidInstanceID':
|
|
for interface in inst.interfaces:
|
|
if interface.source_dest_check != source_dest_check:
|
|
ec2.modify_network_interface_attribute(interface.id, "sourceDestCheck", source_dest_check)
|
|
return True
|
|
else:
|
|
module.fail_json(msg='Failed to handle source_dest_check state for instance {0}, error: {1}'.format(inst.id, exc),
|
|
exception=traceback.format_exc())
|
|
|
|
|
|
def warn_if_public_ip_assignment_changed(module, instance):
|
|
# This is a non-modifiable attribute.
|
|
assign_public_ip = module.params.get('assign_public_ip')
|
|
|
|
# Check that public ip assignment is the same and warn if not
|
|
public_dns_name = getattr(instance, 'public_dns_name', None)
|
|
if (assign_public_ip or public_dns_name) and (not public_dns_name or assign_public_ip is False):
|
|
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.id))
|
|
|
|
|
|
def main():
|
|
argument_spec = ec2_argument_spec()
|
|
argument_spec.update(
|
|
dict(
|
|
key_name=dict(aliases=['keypair']),
|
|
id=dict(),
|
|
group=dict(type='list', aliases=['groups']),
|
|
group_id=dict(type='list'),
|
|
zone=dict(aliases=['aws_zone', 'ec2_zone']),
|
|
instance_type=dict(aliases=['type']),
|
|
spot_price=dict(),
|
|
spot_type=dict(default='one-time', choices=["one-time", "persistent"]),
|
|
spot_launch_group=dict(),
|
|
image=dict(),
|
|
kernel=dict(),
|
|
count=dict(type='int', default='1'),
|
|
monitoring=dict(type='bool', default=False),
|
|
ramdisk=dict(),
|
|
wait=dict(type='bool', default=False),
|
|
wait_timeout=dict(type='int', default=300),
|
|
spot_wait_timeout=dict(type='int', default=600),
|
|
placement_group=dict(),
|
|
user_data=dict(),
|
|
instance_tags=dict(type='dict'),
|
|
vpc_subnet_id=dict(),
|
|
assign_public_ip=dict(type='bool'),
|
|
private_ip=dict(),
|
|
instance_profile_name=dict(),
|
|
instance_ids=dict(type='list', aliases=['instance_id']),
|
|
source_dest_check=dict(type='bool', default=None),
|
|
termination_protection=dict(type='bool', default=None),
|
|
state=dict(default='present', choices=['present', 'absent', 'running', 'restarted', 'stopped']),
|
|
instance_initiated_shutdown_behavior=dict(default='stop', choices=['stop', 'terminate']),
|
|
exact_count=dict(type='int', default=None),
|
|
count_tag=dict(type='raw'),
|
|
volumes=dict(type='list'),
|
|
ebs_optimized=dict(type='bool', default=False),
|
|
tenancy=dict(default='default', choices=['default', 'dedicated']),
|
|
network_interfaces=dict(type='list', aliases=['network_interface'])
|
|
)
|
|
)
|
|
|
|
module = AnsibleModule(
|
|
argument_spec=argument_spec,
|
|
mutually_exclusive=[
|
|
# Can be uncommented when we finish the deprecation cycle.
|
|
# ['group', 'group_id'],
|
|
['exact_count', 'count'],
|
|
['exact_count', 'state'],
|
|
['exact_count', 'instance_ids'],
|
|
['network_interfaces', 'assign_public_ip'],
|
|
['network_interfaces', 'group'],
|
|
['network_interfaces', 'group_id'],
|
|
['network_interfaces', 'private_ip'],
|
|
['network_interfaces', 'vpc_subnet_id'],
|
|
],
|
|
)
|
|
|
|
if module.params.get('group') and module.params.get('group_id'):
|
|
module.deprecate(
|
|
msg='Support for passing both group and group_id has been deprecated. '
|
|
'Currently group_id is ignored, in future passing both will result in an error',
|
|
version='2.14', collection_name='ansible.builtin')
|
|
|
|
if not HAS_BOTO:
|
|
module.fail_json(msg='boto required for this module')
|
|
|
|
try:
|
|
region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module)
|
|
if module.params.get('region') or not module.params.get('ec2_url'):
|
|
ec2 = ec2_connect(module)
|
|
elif module.params.get('ec2_url'):
|
|
ec2 = connect_ec2_endpoint(ec2_url, **aws_connect_kwargs)
|
|
|
|
if 'region' not in aws_connect_kwargs:
|
|
aws_connect_kwargs['region'] = ec2.region
|
|
|
|
vpc = connect_vpc(**aws_connect_kwargs)
|
|
except boto.exception.NoAuthHandlerFound as e:
|
|
module.fail_json(msg="Failed to get connection: %s" % e.message, exception=traceback.format_exc())
|
|
|
|
tagged_instances = []
|
|
|
|
state = module.params['state']
|
|
|
|
if state == 'absent':
|
|
instance_ids = module.params['instance_ids']
|
|
if not instance_ids:
|
|
module.fail_json(msg='instance_ids list is required for absent state')
|
|
|
|
(changed, instance_dict_array, new_instance_ids) = terminate_instances(module, ec2, instance_ids)
|
|
|
|
elif state in ('running', 'stopped'):
|
|
instance_ids = module.params.get('instance_ids')
|
|
instance_tags = module.params.get('instance_tags')
|
|
if not (isinstance(instance_ids, list) or isinstance(instance_tags, dict)):
|
|
module.fail_json(msg='running list needs to be a list of instances or set of tags to run: %s' % instance_ids)
|
|
|
|
(changed, instance_dict_array, new_instance_ids) = startstop_instances(module, ec2, instance_ids, state, instance_tags)
|
|
|
|
elif state in ('restarted'):
|
|
instance_ids = module.params.get('instance_ids')
|
|
instance_tags = module.params.get('instance_tags')
|
|
if not (isinstance(instance_ids, list) or isinstance(instance_tags, dict)):
|
|
module.fail_json(msg='running list needs to be a list of instances or set of tags to run: %s' % instance_ids)
|
|
|
|
(changed, instance_dict_array, new_instance_ids) = restart_instances(module, ec2, instance_ids, state, instance_tags)
|
|
|
|
elif state == 'present':
|
|
# Changed is always set to true when provisioning new instances
|
|
if not module.params.get('image'):
|
|
module.fail_json(msg='image parameter is required for new instance')
|
|
|
|
if module.params.get('exact_count') is None:
|
|
(instance_dict_array, new_instance_ids, changed) = create_instances(module, ec2, vpc)
|
|
else:
|
|
(tagged_instances, instance_dict_array, new_instance_ids, changed) = enforce_count(module, ec2, vpc)
|
|
|
|
# Always return instances in the same order
|
|
if new_instance_ids:
|
|
new_instance_ids.sort()
|
|
if instance_dict_array:
|
|
instance_dict_array.sort(key=lambda x: x['id'])
|
|
if tagged_instances:
|
|
tagged_instances.sort(key=lambda x: x['id'])
|
|
|
|
module.exit_json(changed=changed, instance_ids=new_instance_ids, instances=instance_dict_array, tagged_instances=tagged_instances)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|