099936da22
Add some details in the docs about how the id parameter of the ec2 module behaves.
596 lines
19 KiB
Python
596 lines
19 KiB
Python
#!/usr/bin/python
|
|
# This file is part of Ansible
|
|
#
|
|
# Ansible is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# Ansible is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
DOCUMENTATION = '''
|
|
---
|
|
module: ec2
|
|
short_description: create or terminate an instance in ec2, return instanceid
|
|
description:
|
|
- Creates or terminates ec2 instances. When created optionally waits for it to be 'running'. This module has a dependency on python-boto >= 2.5
|
|
version_added: "0.9"
|
|
options:
|
|
key_name:
|
|
description:
|
|
- key pair to use on the instance
|
|
required: true
|
|
default: null
|
|
aliases: ['keypair']
|
|
id:
|
|
description:
|
|
- identifier for this instance or set of instances, so that the module will be idempotent with respect to EC2 instances.
|
|
- Note: 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(http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Run_Instance_Idempotency.html).
|
|
required: false
|
|
default: null
|
|
aliases: []
|
|
group:
|
|
description:
|
|
- security group (or list of groups) to use with the instance
|
|
required: false
|
|
default: null
|
|
aliases: [ 'groups' ]
|
|
group_id:
|
|
version_added: "1.1"
|
|
description:
|
|
- security group id (or list of ids) to use with the instance
|
|
required: false
|
|
default: null
|
|
aliases: []
|
|
region:
|
|
version_added: "1.2"
|
|
description:
|
|
- the EC2 region to use
|
|
required: false
|
|
default: null
|
|
aliases: []
|
|
zone:
|
|
version_added: "1.2"
|
|
description:
|
|
- availability zone in which to launch the instance
|
|
required: false
|
|
default: null
|
|
aliases: []
|
|
instance_type:
|
|
description:
|
|
- instance type to use for the instance
|
|
required: true
|
|
default: null
|
|
aliases: []
|
|
image:
|
|
description:
|
|
- I(emi) (or I(ami)) to use for the instance
|
|
required: true
|
|
default: null
|
|
aliases: []
|
|
kernel:
|
|
description:
|
|
- kernel I(eki) to use for the instance
|
|
required: false
|
|
default: null
|
|
aliases: []
|
|
ramdisk:
|
|
description:
|
|
- ramdisk I(eri) to use for the instance
|
|
required: false
|
|
default: null
|
|
aliases: []
|
|
wait:
|
|
description:
|
|
- wait for the instance to be in state 'running' before returning
|
|
required: false
|
|
default: "no"
|
|
choices: [ "yes", "no" ]
|
|
aliases: []
|
|
wait_timeout:
|
|
description:
|
|
- how long before wait gives up, in seconds
|
|
default: 300
|
|
aliases: []
|
|
ec2_url:
|
|
description:
|
|
- url to use to connect to EC2 or your Eucalyptus cloud (by default the module will use EC2 endpoints)
|
|
required: false
|
|
default: null
|
|
aliases: []
|
|
ec2_secret_key:
|
|
description:
|
|
- ec2 secret key
|
|
required: false
|
|
default: null
|
|
aliases: []
|
|
ec2_access_key:
|
|
description:
|
|
- ec2 access key
|
|
required: false
|
|
default: null
|
|
aliases: []
|
|
count:
|
|
description:
|
|
- number of instances to launch
|
|
required: False
|
|
default: 1
|
|
aliases: []
|
|
monitor:
|
|
version_added: "1.1"
|
|
description:
|
|
- enable detailed monitoring (CloudWatch) for instance
|
|
required: false
|
|
default: null
|
|
aliases: []
|
|
user_data:
|
|
version_added: "0.9"
|
|
description:
|
|
- opaque blob of data which is made available to the ec2 instance
|
|
required: false
|
|
default: null
|
|
aliases: []
|
|
instance_tags:
|
|
version_added: "1.0"
|
|
description:
|
|
- a hash/dictionary of tags to add to the new instance; '{"key":"value"}' and '{"key":"value","key":"value"}'
|
|
required: false
|
|
default: null
|
|
aliases: []
|
|
placement_group:
|
|
version_added: "1.3"
|
|
description:
|
|
- placement group for the instance when using EC2 Clustered Compute
|
|
required: false
|
|
default: null
|
|
aliases: []
|
|
vpc_subnet_id:
|
|
version_added: "1.1"
|
|
description:
|
|
- the subnet ID in which to launch the instance (VPC)
|
|
required: false
|
|
default: null
|
|
aliases: []
|
|
private_ip:
|
|
version_added: "1.2"
|
|
description:
|
|
- the private ip address to assign the instance (from the vpc subnet)
|
|
required: false
|
|
defualt: null
|
|
aliases: []
|
|
instance_profile_name:
|
|
version_added: "1.3"
|
|
description:
|
|
- Name of the IAM instance profile to use
|
|
required: false
|
|
default: null
|
|
aliases: []
|
|
instance_ids:
|
|
version_added: "1.3"
|
|
description:
|
|
- list of instance ids, currently only used when state='absent'
|
|
required: false
|
|
default: null
|
|
aliases: []
|
|
state:
|
|
version_added: "1.3"
|
|
description:
|
|
- create or terminate instances
|
|
required: false
|
|
default: 'present'
|
|
aliases: []
|
|
|
|
requirements: [ "boto" ]
|
|
author: Seth Vidal, Tim Gerla, Lester Wade
|
|
'''
|
|
|
|
EXAMPLES = '''
|
|
# Basic provisioning example
|
|
- local_action:
|
|
module: ec2
|
|
keypair: mykey
|
|
instance_type: c1.medium
|
|
image: emi-40603AD1
|
|
wait: yes
|
|
group: webserver
|
|
count: 3
|
|
|
|
# Advanced example with tagging and CloudWatch
|
|
- local_action:
|
|
module: ec2
|
|
keypair: mykey
|
|
group: databases
|
|
instance_type: m1.large
|
|
image: ami-6e649707
|
|
wait: yes
|
|
wait_timeout: 500
|
|
count: 5
|
|
instance_tags: '{"db":"postgres"}' monitoring=yes'
|
|
|
|
# Multiple groups example
|
|
local_action:
|
|
module: ec2
|
|
keypair: 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 example
|
|
- local_action:
|
|
module: ec2
|
|
keypair: mykey
|
|
group_id: sg-1dc53f72
|
|
instance_type: m1.small
|
|
image: ami-6e649707
|
|
wait: yes
|
|
vpc_subnet_id: subnet-29e63245
|
|
|
|
|
|
# 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
|
|
local_action: ec2 keypair={{ keypair }} group={{ security_group }} instance_type={{ instance_type }} image={{ image }} wait=true region={{ region }}
|
|
register: ec2
|
|
- name: Add new instance to host group
|
|
local_action: add_host hostname={{ item.public_ip }} groupname=launched
|
|
with_items: ec2.instances
|
|
- name: Wait for SSH to come up
|
|
local_action: wait_for host={{ item.public_dns_name }} port=22 delay=60 timeout=320 state=started
|
|
with_items: ec2.instances
|
|
|
|
- name: Configure instance(s)
|
|
hosts: launched
|
|
sudo: True
|
|
gather_facts: True
|
|
roles:
|
|
- my_awesome_role
|
|
- my_awesome_test
|
|
|
|
- name: Terminate instances
|
|
hosts: localhost
|
|
connection: local
|
|
tasks:
|
|
- name: Terminate instances that were previously launched
|
|
local_action:
|
|
module: ec2
|
|
state: 'absent'
|
|
instance_ids: {{ec2.instance_ids}}
|
|
|
|
'''
|
|
|
|
import sys
|
|
import time
|
|
|
|
try:
|
|
import boto.ec2
|
|
from boto.exception import EC2ResponseError
|
|
except ImportError:
|
|
print "failed=True msg='boto required for this module'"
|
|
sys.exit(1)
|
|
|
|
|
|
def get_instance_info(inst):
|
|
"""
|
|
Retrieves instance information from an instance
|
|
ID and returns it as a dictionary
|
|
"""
|
|
|
|
return({
|
|
'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,
|
|
'virtualization_type': inst.virtualization_type,
|
|
'placement': inst.placement,
|
|
'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
|
|
})
|
|
|
|
|
|
def create_instances(module, ec2):
|
|
"""
|
|
Creates new instances
|
|
|
|
module : AnsbileModule 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')
|
|
image = module.params.get('image')
|
|
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'))
|
|
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')
|
|
private_ip = module.params.get('private_ip')
|
|
instance_profile_name = module.params.get('instance_profile_name')
|
|
|
|
|
|
# group_id and group_name are exclusive of each other
|
|
if group_id and group_name:
|
|
module.fail_json(msg = str("Use only one type of parameter (group_name) or (group_id)"))
|
|
sys.exit(1)
|
|
|
|
try:
|
|
# Here we try to lookup the group id from the security group name - if group is set.
|
|
if group_name:
|
|
grp_details = ec2.get_all_security_groups()
|
|
if type(group_name) == list:
|
|
group_id = list(filter(lambda grp: str(grp.id) if str(tmp) in str(grp) else None, grp_details) for tmp in group_name)
|
|
elif type(group_name) == str:
|
|
for grp in grp_details:
|
|
if str(group_name) in str(grp):
|
|
group_id = [str(grp.id)]
|
|
group_name = [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 type(group_id) == str:
|
|
group_id = [group_id]
|
|
grp_details = ec2.get_all_security_groups(group_ids=group_id)
|
|
grp_item = grp_details[0]
|
|
group_name = [grp_item.name]
|
|
except boto.exception.NoAuthHandlerFound, e:
|
|
module.fail_json(msg = str(e))
|
|
|
|
# Lookup any instances that much our run id.
|
|
|
|
running_instances = []
|
|
count_remaining = int(count)
|
|
|
|
if id != 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:
|
|
try:
|
|
params = {'image_id': image,
|
|
'key_name': key_name,
|
|
'client_token': id,
|
|
'min_count': count_remaining,
|
|
'max_count': count_remaining,
|
|
'monitoring_enabled': monitoring,
|
|
'placement': zone,
|
|
'placement_group': placement_group,
|
|
'instance_type': instance_type,
|
|
'kernel_id': kernel,
|
|
'ramdisk_id': ramdisk,
|
|
'subnet_id': vpc_subnet_id,
|
|
'private_ip_address': private_ip,
|
|
'user_data': user_data,
|
|
'instance_profile_name': instance_profile_name}
|
|
|
|
if vpc_subnet_id:
|
|
params['security_group_ids'] = group_id
|
|
else:
|
|
params['security_groups'] = group_name
|
|
|
|
res = ec2.run_instances(**params)
|
|
except boto.exception.BotoServerError, e:
|
|
module.fail_json(msg = "%s: %s" % (e.error_code, e.error_message))
|
|
|
|
instids = [ i.id for i in res.instances ]
|
|
while True:
|
|
try:
|
|
res.connection.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))
|
|
|
|
if instance_tags:
|
|
try:
|
|
ec2.create_tags(instids, module.from_json(instance_tags))
|
|
except boto.exception.EC2ResponseError as e:
|
|
module.fail_json(msg = "%s: %s" % (e.error_code, e.error_message))
|
|
|
|
# wait here until the instances are up
|
|
res_list = res.connection.get_all_instances(instids)
|
|
this_res = res_list[0]
|
|
num_running = 0
|
|
wait_timeout = time.time() + wait_timeout
|
|
while wait and wait_timeout > time.time() and num_running < len(instids):
|
|
res_list = res.connection.get_all_instances(instids)
|
|
this_res = res_list[0]
|
|
num_running = len([ i for i in this_res.instances if i.state=='running' ])
|
|
time.sleep(5)
|
|
if wait and wait_timeout <= time.time():
|
|
# waiting took too long
|
|
module.fail_json(msg = "wait for instances running timeout on %s" % time.asctime())
|
|
|
|
for inst in this_res.instances:
|
|
running_instances.append(inst)
|
|
|
|
instance_dict_array = []
|
|
created_instance_ids = []
|
|
for inst in running_instances:
|
|
d = get_instance_info(inst)
|
|
created_instance_ids.append(inst.id)
|
|
instance_dict_array.append(d)
|
|
|
|
return (instance_dict_array, created_instance_ids)
|
|
|
|
|
|
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.
|
|
|
|
"""
|
|
|
|
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':
|
|
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
|
|
|
|
return (changed, instance_dict_array, terminated_instance_ids)
|
|
|
|
|
|
|
|
def main():
|
|
module = AnsibleModule(
|
|
argument_spec = dict(
|
|
key_name = dict(aliases = ['keypair']),
|
|
id = dict(),
|
|
group = dict(type='list'),
|
|
group_id = dict(),
|
|
region = dict(choices=['eu-west-1', 'sa-east-1', 'us-east-1', 'ap-northeast-1', 'us-west-2', 'us-west-1', 'ap-southeast-1', 'ap-southeast-2']),
|
|
zone = dict(),
|
|
instance_type = dict(aliases=['type']),
|
|
image = dict(),
|
|
kernel = dict(),
|
|
count = dict(default='1'),
|
|
monitoring = dict(choices=BOOLEANS, default=False),
|
|
ramdisk = dict(),
|
|
wait = dict(choices=BOOLEANS, default=False),
|
|
wait_timeout = dict(default=300),
|
|
ec2_url = dict(aliases=['EC2_URL']),
|
|
ec2_secret_key = dict(aliases=['EC2_SECRET_KEY'], no_log=True),
|
|
ec2_access_key = dict(aliases=['EC2_ACCESS_KEY']),
|
|
placement_group = dict(),
|
|
user_data = dict(),
|
|
instance_tags = dict(),
|
|
vpc_subnet_id = dict(),
|
|
private_ip = dict(),
|
|
instance_profile_name = dict(),
|
|
instance_ids = dict(type='list'),
|
|
state = dict(default='present'),
|
|
)
|
|
)
|
|
|
|
ec2_url = module.params.get('ec2_url')
|
|
ec2_secret_key = module.params.get('ec2_secret_key')
|
|
ec2_access_key = module.params.get('ec2_access_key')
|
|
region = module.params.get('region')
|
|
termination_list = module.params.get('termination_list')
|
|
|
|
|
|
# allow eucarc environment variables to be used if ansible vars aren't set
|
|
if not ec2_url and 'EC2_URL' in os.environ:
|
|
ec2_url = os.environ['EC2_URL']
|
|
if not ec2_secret_key and 'EC2_SECRET_KEY' in os.environ:
|
|
ec2_secret_key = os.environ['EC2_SECRET_KEY']
|
|
if not ec2_access_key and 'EC2_ACCESS_KEY' in os.environ:
|
|
ec2_access_key = os.environ['EC2_ACCESS_KEY']
|
|
|
|
# If we have a region specified, connect to its endpoint.
|
|
if region:
|
|
try:
|
|
ec2 = boto.ec2.connect_to_region(region, aws_access_key_id=ec2_access_key, aws_secret_access_key=ec2_secret_key)
|
|
except boto.exception.NoAuthHandlerFound, e:
|
|
module.fail_json(msg = str(e))
|
|
# Otherwise, no region so we fallback to the old connection method
|
|
else:
|
|
try:
|
|
if ec2_url: # if we have an URL set, connect to the specified endpoint
|
|
ec2 = boto.connect_ec2_endpoint(ec2_url, ec2_access_key, ec2_secret_key)
|
|
else: # otherwise it's Amazon.
|
|
ec2 = boto.connect_ec2(ec2_access_key, ec2_secret_key)
|
|
except boto.exception.NoAuthHandlerFound, e:
|
|
module.fail_json(msg = str(e))
|
|
|
|
if module.params.get('state') == 'absent':
|
|
instance_ids = module.params.get('instance_ids')
|
|
if not isinstance(instance_ids, list):
|
|
module.fail_json(msg='termination_list needs to be a list of instances to terminate')
|
|
|
|
(changed, instance_dict_array, new_instance_ids) = terminate_instances(module, ec2, instance_ids)
|
|
|
|
elif module.params.get('state') == 'present':
|
|
# Changed is always set to true when provisioning new instances
|
|
changed = True
|
|
if not module.params.get('key_name'):
|
|
module.fail_json(msg='key_name parameter is required for new instance')
|
|
if not module.params.get('image'):
|
|
module.fail_json(msg='image parameter is required for new instance')
|
|
(instance_dict_array, new_instance_ids) = create_instances(module, ec2)
|
|
|
|
module.exit_json(changed=True, instance_ids=new_instance_ids, instances=instance_dict_array)
|
|
|
|
|
|
# this is magic, see lib/ansible/module_common.py
|
|
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
|
|
|
|
main()
|