Commit 4e8c0d1e by Carson Gee

Merge pull request #1190 from edx/cg/new_ec2

Updating local ec2 module
parents 6b704b90 803bd8bf
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
DOCUMENTATION = ''' DOCUMENTATION = '''
--- ---
module: ec2 module: ec2
short_description: create or terminate an instance in ec2, return instanceid short_description: create, terminate, start or stop an instance in ec2, return instanceid
description: 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 - 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" version_added: "0.9"
...@@ -25,7 +25,7 @@ options: ...@@ -25,7 +25,7 @@ options:
key_name: key_name:
description: description:
- key pair to use on the instance - key pair to use on the instance
required: true required: false
default: null default: null
aliases: ['keypair'] aliases: ['keypair']
id: id:
...@@ -67,6 +67,13 @@ options: ...@@ -67,6 +67,13 @@ options:
required: true required: true
default: null default: null
aliases: [] aliases: []
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.
required: false
default: null
aliases: []
image: image:
description: description:
- I(emi) (or I(ami)) to use for the instance - I(emi) (or I(ami)) to use for the instance
...@@ -97,24 +104,12 @@ options: ...@@ -97,24 +104,12 @@ options:
- how long before wait gives up, in seconds - how long before wait gives up, in seconds
default: 300 default: 300
aliases: [] aliases: []
ec2_url: spot_wait_timeout:
version_added: "1.5"
description: description:
- Url to use to connect to EC2 or your Eucalyptus cloud (by default the module will use EC2 endpoints). Must be specified if region is not used. If not set then the value of the EC2_URL environment variable, if any, is used - how long to wait for the spot instance request to be fulfilled
required: false default: 600
default: null
aliases: [] aliases: []
aws_secret_key:
description:
- AWS secret key. If not set then the value of the AWS_SECRET_KEY environment variable is used.
required: false
default: null
aliases: [ 'ec2_secret_key', 'secret_key' ]
aws_access_key:
description:
- AWS access key. If not set then the value of the AWS_ACCESS_KEY environment variable is used.
required: false
default: null
aliases: [ 'ec2_access_key', 'access_key' ]
count: count:
description: description:
- number of instances to launch - number of instances to launch
...@@ -156,6 +151,13 @@ options: ...@@ -156,6 +151,13 @@ options:
required: false required: false
default: null default: null
aliases: [] aliases: []
assign_public_ip:
version_added: "1.5"
description:
- when provisioning within vpc, assign a public IP address. Boto library must be 2.13.0+
required: false
default: null
aliases: []
private_ip: private_ip:
version_added: "1.2" version_added: "1.2"
description: description:
...@@ -173,10 +175,16 @@ options: ...@@ -173,10 +175,16 @@ options:
instance_ids: instance_ids:
version_added: "1.3" version_added: "1.3"
description: description:
- list of instance ids, currently only used when state='absent' - "list of instance ids, currently used for states: absent, running, stopped"
required: false required: false
default: null default: null
aliases: [] aliases: []
source_dest_check:
version_added: "1.6"
description:
- Enable or Disable the Source/Destination checks (for NAT instances and Virtual Routers)
required: false
default: true
state: state:
version_added: "1.3" version_added: "1.3"
description: description:
...@@ -184,16 +192,36 @@ options: ...@@ -184,16 +192,36 @@ options:
required: false required: false
default: 'present' default: 'present'
aliases: [] aliases: []
root_ebs_size: volumes:
version_added: "1.5"
description:
- a list of volume dicts, each containing device name and optionally ephemeral id or snapshot id. Size and type (and number of iops for io device type) must be specified for a new volume or a root volume, and may be passed for a snapshot volume. For any volume, a volume size less than 1 will be interpreted as a request not to create the volume.
required: false
default: null
aliases: []
ebs_optimized:
version_added: "1.6"
description:
- whether instance is using optimized EBS volumes, see U(http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSOptimized.html)
required: false
default: false
exact_count:
version_added: "1.5" version_added: "1.5"
desription: description:
- size of the root volume in gigabytes - 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.
required: false
default: null
aliases: []
count_tag:
version_added: "1.5"
description:
- Used with '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".
required: false required: false
default: null default: null
aliases: [] aliases: []
requirements: [ "boto" ]
author: Seth Vidal, Tim Gerla, Lester Wade author: Seth Vidal, Tim Gerla, Lester Wade
extends_documentation_fragment: aws
''' '''
EXAMPLES = ''' EXAMPLES = '''
...@@ -203,7 +231,7 @@ EXAMPLES = ''' ...@@ -203,7 +231,7 @@ EXAMPLES = '''
# Basic provisioning example # Basic provisioning example
- local_action: - local_action:
module: ec2 module: ec2
keypair: mykey key_name: mykey
instance_type: c1.medium instance_type: c1.medium
image: emi-40603AD1 image: emi-40603AD1
wait: yes wait: yes
...@@ -213,39 +241,88 @@ EXAMPLES = ''' ...@@ -213,39 +241,88 @@ EXAMPLES = '''
# Advanced example with tagging and CloudWatch # Advanced example with tagging and CloudWatch
- local_action: - local_action:
module: ec2 module: ec2
keypair: mykey key_name: mykey
group: databases group: databases
instance_type: m1.large instance_type: m1.large
image: ami-6e649707 image: ami-6e649707
wait: yes wait: yes
wait_timeout: 500 wait_timeout: 500
count: 5 count: 5
instance_tags: '{"db":"postgres"}' instance_tags:
monitoring=yes db: postgres
monitoring: yes
# Single instance with additional IOPS volume from snapshot and volume delete on termination
local_action:
module: ec2
key_name: mykey
group: webserver
instance_type: m1.large
image: ami-6e649707
wait: yes
wait_timeout: 500
volumes:
- device_name: /dev/sdb
snapshot: snap-abcdef12
device_type: io1
iops: 1000
volume_size: 100
delete_on_termination: true
monitoring: yes
# Multiple groups example # Multiple groups example
local_action: local_action:
module: ec2 module: ec2
keypair: mykey key_name: mykey
group: ['databases', 'internal-services', 'sshable', 'and-so-forth'] group: ['databases', 'internal-services', 'sshable', 'and-so-forth']
instance_type: m1.large instance_type: m1.large
image: ami-6e649707 image: ami-6e649707
wait: yes wait: yes
wait_timeout: 500 wait_timeout: 500
count: 5 count: 5
instance_tags: '{"db":"postgres"}' instance_tags:
monitoring=yes db: postgres
monitoring: yes
# Multiple instances with additional volume from snapshot
local_action:
module: 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 example # VPC example
- local_action: - local_action:
module: ec2 module: ec2
keypair: mykey key_name: mykey
group_id: sg-1dc53f72 group_id: sg-1dc53f72
instance_type: m1.small instance_type: m1.small
image: ami-6e649707 image: ami-6e649707
wait: yes wait: yes
vpc_subnet_id: subnet-29e63245 vpc_subnet_id: subnet-29e63245
assign_public_ip: yes
# Spot instance example
- local_action:
module: 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
# Launch instances, runs some tasks # Launch instances, runs some tasks
# and then terminate them # and then terminate them
...@@ -255,14 +332,14 @@ local_action: ...@@ -255,14 +332,14 @@ local_action:
hosts: localhost hosts: localhost
gather_facts: False gather_facts: False
vars: vars:
keypair: my_keypair key_name: my_keypair
instance_type: m1.small instance_type: m1.small
security_group: my_securitygroup security_group: my_securitygroup
image: my_ami_id image: my_ami_id
region: us-east-1 region: us-east-1
tasks: tasks:
- name: Launch instance - name: Launch instance
local_action: ec2 keypair={{ keypair }} group={{ security_group }} instance_type={{ instance_type }} image={{ image }} wait=true region={{ region }} local_action: ec2 key_name={{ keypair }} group={{ security_group }} instance_type={{ instance_type }} image={{ image }} wait=true region={{ region }}
register: ec2 register: ec2
- name: Add new instance to host group - name: Add new instance to host group
local_action: add_host hostname={{ item.public_ip }} groupname=launched local_action: add_host hostname={{ item.public_ip }} groupname=launched
...@@ -287,29 +364,186 @@ local_action: ...@@ -287,29 +364,186 @@ local_action:
local_action: local_action:
module: ec2 module: ec2
state: 'absent' state: 'absent'
instance_ids: {{ec2.instance_ids}} 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
connection: local
vars:
instance_ids:
- 'i-xxxxxx'
- 'i-xxxxxx'
- 'i-xxxxxx'
region: us-east-1
tasks:
- name: Start the sandbox instances
local_action:
module: ec2
instance_ids: '{{ instance_ids }}'
region: '{{ region }}'
state: running
wait: True
role:
- do_neat_stuff
- do_more_neat_stuff
- name: Stop sandbox instances
hosts: localhost
gather_facts: false
connection: local
vars:
instance_ids:
- 'i-xxxxxx'
- 'i-xxxxxx'
- 'i-xxxxxx'
region: us-east-1
tasks:
- name: Stop the sanbox instances
local_action:
module: ec2
instance_ids: '{{ instance_ids }}'
region: '{{ region }}'
state: stopped
wait: True
#
# Enforce that 5 instances with a tag "foo" are running
#
- local_action:
module: ec2
key_name: mykey
instance_type: c1.medium
image: emi-40603AD1
wait: yes
group: webserver
instance_tags:
foo: bar
exact_count: 5
count_tag: foo
#
# Enforce that 5 running instances named "database" with a "dbtype" of "postgres"
#
- local_action:
module: ec2
key_name: mykey
instance_type: c1.medium
image: emi-40603AD1
wait: yes
group: webserver
instance_tags:
Name: database
dbtype: postgres
exact_count: 5
count_tag:
Name: database
dbtype: postgres
#
# count_tag complex argument examples
#
# instances with tag foo
count_tag:
foo:
# instances with tag foo=bar
count_tag:
foo: bar
# instances with tags foo=bar & baz
count_tag:
foo: bar
baz:
# instances with tags foo & bar & baz=bang
count_tag:
- foo
- bar
- baz: bang
''' '''
import sys import sys
import time import time
from ast import literal_eval
AWS_REGIONS = ['ap-northeast-1',
'ap-southeast-1',
'ap-southeast-2',
'eu-west-1',
'sa-east-1',
'us-east-1',
'us-west-1',
'us-west-2']
try: try:
import boto.ec2 import boto.ec2
from boto.ec2.blockdevicemapping import BlockDeviceType, BlockDeviceMapping
from boto.exception import EC2ResponseError from boto.exception import EC2ResponseError
except ImportError: except ImportError:
print "failed=True msg='boto required for this module'" print "failed=True msg='boto required for this module'"
sys.exit(1) sys.exit(1)
def find_running_instances_by_count_tag(module, ec2, count_tag):
# get reservations for instances that match tag(s) and are running
reservations = get_reservations(module, ec2, tags=count_tag, state="running")
instances = []
for res in reservations:
if hasattr(res, 'instances'):
for inst in res.instances:
instances.append(inst)
return reservations, instances
def _set_none_to_blank(dictionary):
result = dictionary
for k in result.iterkeys():
if type(result[k]) == dict:
result[k] = _set_non_to_blank(result[k])
elif not result[k]:
result[k] = ""
return result
def get_reservations(module, ec2, tags=None, state=None):
# TODO: filters do not work with tags that have underscores
filters = dict()
if tags is not None:
if type(tags) is str:
try:
tags = literal_eval(tags)
except:
pass
# if string, we only care that a tag of that name exists
if type(tags) is str:
filters.update({"tag-key": tags})
# if list, append each item to filters
if type(tags) is list:
for x in tags:
if type(x) is dict:
x = _set_none_to_blank(x)
filters.update(dict(("tag:"+tn, tv) for (tn,tv) in x.iteritems()))
else:
filters.update({"tag-key": x})
# if dict, add the key and value to the filter
if type(tags) is dict:
tags = _set_none_to_blank(tags)
filters.update(dict(("tag:"+tn, tv) for (tn,tv) in tags.iteritems()))
if state:
# http://stackoverflow.com/questions/437511/what-are-the-valid-instancestates-for-the-amazon-ec2-api
filters.update({'instance-state-name': state})
results = ec2.get_all_instances(filters=filters)
return results
def get_instance_info(inst): def get_instance_info(inst):
""" """
...@@ -328,6 +562,7 @@ def get_instance_info(inst): ...@@ -328,6 +562,7 @@ def get_instance_info(inst):
'image_id': inst.image_id, 'image_id': inst.image_id,
'key_name': inst.key_name, 'key_name': inst.key_name,
'placement': inst.placement, 'placement': inst.placement,
'region': inst.placement[:-1],
'kernel': inst.kernel, 'kernel': inst.kernel,
'ramdisk': inst.ramdisk, 'ramdisk': inst.ramdisk,
'launch_time': inst.launch_time, 'launch_time': inst.launch_time,
...@@ -335,7 +570,8 @@ def get_instance_info(inst): ...@@ -335,7 +570,8 @@ def get_instance_info(inst):
'root_device_type': inst.root_device_type, 'root_device_type': inst.root_device_type,
'root_device_name': inst.root_device_name, 'root_device_name': inst.root_device_name,
'state': inst.state, 'state': inst.state,
'hypervisor': inst.hypervisor} 'hypervisor': inst.hypervisor,
'ebs_optimized': inst.ebs_optimized}
try: try:
instance_info['virtualization_type'] = getattr(inst,'virtualization_type') instance_info['virtualization_type'] = getattr(inst,'virtualization_type')
except AttributeError: except AttributeError:
...@@ -343,6 +579,24 @@ def get_instance_info(inst): ...@@ -343,6 +579,24 @@ def get_instance_info(inst):
return instance_info 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): 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 Check if Boto library has instance_profile_name argument. instance_profile_name has been added in Boto 2.5.0
...@@ -355,8 +609,94 @@ def boto_supports_profile_name_arg(ec2): ...@@ -355,8 +609,94 @@ def boto_supports_profile_name_arg(ec2):
run_instances_method = getattr(ec2, 'run_instances') run_instances_method = getattr(ec2, 'run_instances')
return 'instance_profile_name' in run_instances_method.func_code.co_varnames return 'instance_profile_name' in run_instances_method.func_code.co_varnames
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
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 'device_type' in volume and volume.get('device_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')
return BlockDeviceType(snapshot_id=volume.get('snapshot'),
ephemeral_name=volume.get('ephemeral'),
size=volume.get('volume_size'),
volume_type=volume.get('device_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 method.func_code.co_varnames
def enforce_count(module, ec2):
exact_count = module.params.get('exact_count')
count_tag = module.params.get('count_tag')
reservations, instances = find_running_instances_by_count_tag(module, ec2, count_tag)
changed = None
checkmode = False
instance_dict_array = None
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, 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 type(inst) is not dict:
inst = get_instance_info(inst)
all_instances.append(inst)
return (all_instances, instance_dict_array, changed_instance_ids, changed)
def create_instances(module, ec2):
def create_instances(module, ec2, override_count=None):
""" """
Creates new instances Creates new instances
...@@ -374,29 +714,30 @@ def create_instances(module, ec2): ...@@ -374,29 +714,30 @@ def create_instances(module, ec2):
group_id = module.params.get('group_id') group_id = module.params.get('group_id')
zone = module.params.get('zone') zone = module.params.get('zone')
instance_type = module.params.get('instance_type') instance_type = module.params.get('instance_type')
spot_price = module.params.get('spot_price')
image = module.params.get('image') image = module.params.get('image')
if override_count:
count = override_count
else:
count = module.params.get('count') count = module.params.get('count')
monitoring = module.params.get('monitoring') monitoring = module.params.get('monitoring')
kernel = module.params.get('kernel') kernel = module.params.get('kernel')
ramdisk = module.params.get('ramdisk') ramdisk = module.params.get('ramdisk')
wait = module.params.get('wait') wait = module.params.get('wait')
wait_timeout = int(module.params.get('wait_timeout')) 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') placement_group = module.params.get('placement_group')
user_data = module.params.get('user_data') user_data = module.params.get('user_data')
instance_tags = module.params.get('instance_tags') instance_tags = module.params.get('instance_tags')
vpc_subnet_id = module.params.get('vpc_subnet_id') 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') private_ip = module.params.get('private_ip')
instance_profile_name = module.params.get('instance_profile_name') instance_profile_name = module.params.get('instance_profile_name')
root_ebs_size = module.params.get('root_ebs_size') volumes = module.params.get('volumes')
ebs_optimized = module.params.get('ebs_optimized')
if root_ebs_size: exact_count = module.params.get('exact_count')
dev_sda1 = boto.ec2.blockdevicemapping.EBSBlockDeviceType() count_tag = module.params.get('count_tag')
dev_sda1.size = root_ebs_size source_dest_check = module.boolean(module.params.get('source_dest_check'))
bdm = boto.ec2.blockdevicemapping.BlockDeviceMapping()
bdm['/dev/sda1'] = dev_sda1
else:
bdm = None
# group_id and group_name are exclusive of each other # group_id and group_name are exclusive of each other
if group_id and group_name: if group_id and group_name:
...@@ -447,19 +788,15 @@ def create_instances(module, ec2): ...@@ -447,19 +788,15 @@ def create_instances(module, ec2):
try: try:
params = {'image_id': image, params = {'image_id': image,
'key_name': key_name, 'key_name': key_name,
'client_token': id,
'min_count': count_remaining,
'max_count': count_remaining,
'monitoring_enabled': monitoring, 'monitoring_enabled': monitoring,
'placement': zone, 'placement': zone,
'placement_group': placement_group,
'instance_type': instance_type, 'instance_type': instance_type,
'kernel_id': kernel, 'kernel_id': kernel,
'ramdisk_id': ramdisk, 'ramdisk_id': ramdisk,
'subnet_id': vpc_subnet_id, 'user_data': user_data}
'private_ip_address': private_ip,
'user_data': user_data, if ebs_optimized:
'block_device_map': bdm} params['ebs_optimized'] = ebs_optimized
if boto_supports_profile_name_arg(ec2): if boto_supports_profile_name_arg(ec2):
params['instance_profile_name'] = instance_profile_name params['instance_profile_name'] = instance_profile_name
...@@ -468,19 +805,70 @@ def create_instances(module, ec2): ...@@ -468,19 +805,70 @@ def create_instances(module, ec2):
module.fail_json( module.fail_json(
msg="instance_profile_name parameter requires Boto version 2.5.0 or higher") msg="instance_profile_name parameter requires Boto version 2.5.0 or higher")
if assign_public_ip:
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:
params['subnet_id'] = vpc_subnet_id
if vpc_subnet_id: if vpc_subnet_id:
params['security_group_ids'] = group_id params['security_group_ids'] = group_id
else: else:
params['security_groups'] = group_name params['security_groups'] = group_name
res = ec2.run_instances(**params) if volumes:
except boto.exception.BotoServerError, e: bdm = BlockDeviceMapping()
module.fail_json(msg = "%s: %s" % (e.error_code, e.error_message)) 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 1GB. 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 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,
))
res = ec2.run_instances(**params)
instids = [ i.id for i in res.instances ] instids = [ i.id for i in res.instances ]
while True: while True:
try: try:
res.connection.get_all_instances(instids) ec2.get_all_instances(instids)
break break
except boto.exception.EC2ResponseError as e: except boto.exception.EC2ResponseError as e:
if "<Code>InvalidInstanceID.NotFound</Code>" in str(e): if "<Code>InvalidInstanceID.NotFound</Code>" in str(e):
...@@ -488,23 +876,58 @@ def create_instances(module, ec2): ...@@ -488,23 +876,58 @@ def create_instances(module, ec2):
continue continue
else: else:
module.fail_json(msg = str(e)) module.fail_json(msg = str(e))
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.")
params.update(dict(
count = count_remaining,
))
res = ec2.request_spot_instances(spot_price, **params)
# Now we have to do the intermediate waiting
if wait:
spot_req_inst_ids = dict()
spot_wait_timeout = time.time() + spot_wait_timeout
while spot_wait_timeout > time.time():
reqs = ec2.get_all_spot_instance_requests()
for sirb in res:
if sirb.id in spot_req_inst_ids:
continue
for sir in reqs:
if sir.id == sirb.id and sir.instance_id is not None:
spot_req_inst_ids[sirb.id] = sir.instance_id
if len(spot_req_inst_ids) < count:
time.sleep(5)
else:
break
if spot_wait_timeout <= time.time():
module.fail_json(msg = "wait for spot requests timeout on %s" % time.asctime())
instids = spot_req_inst_ids.values()
except boto.exception.BotoServerError, e:
module.fail_json(msg = "Instance creation failed => %s: %s" % (e.error_code, e.error_message))
if instance_tags: if instance_tags:
try: try:
ec2.create_tags(instids, instance_tags) ec2.create_tags(instids, instance_tags)
except boto.exception.EC2ResponseError as e: except boto.exception.EC2ResponseError, e:
module.fail_json(msg = "%s: %s" % (e.error_code, e.error_message)) module.fail_json(msg = "Instance tagging failed => %s: %s" % (e.error_code, e.error_message))
# wait here until the instances are up # wait here until the instances are up
this_res = []
num_running = 0 num_running = 0
wait_timeout = time.time() + wait_timeout wait_timeout = time.time() + wait_timeout
while wait_timeout > time.time() and num_running < len(instids): while wait_timeout > time.time() and num_running < len(instids):
res_list = res.connection.get_all_instances(instids) res_list = ec2.get_all_instances(instids)
if len(res_list) > 0: num_running = 0
this_res = res_list[0] for res in res_list:
num_running = len([ i for i in this_res.instances if i.state=='running' ]) num_running += len([ i for i in res.instances if i.state=='running' ])
else: if len(res_list) <= 0:
# got a bad response of some sort, possibly due to # got a bad response of some sort, possibly due to
# stale/cached data. Wait a second and then try again # stale/cached data. Wait a second and then try again
time.sleep(1) time.sleep(1)
...@@ -518,8 +941,14 @@ def create_instances(module, ec2): ...@@ -518,8 +941,14 @@ def create_instances(module, ec2):
# waiting took too long # waiting took too long
module.fail_json(msg = "wait for instances running timeout on %s" % time.asctime()) module.fail_json(msg = "wait for instances running timeout on %s" % time.asctime())
for inst in this_res.instances: #We do this after the loop ends so that we end up with one list
running_instances.append(inst) for res in res_list:
running_instances.extend(res.instances)
# Enabled by default by Amazon
if not source_dest_check:
for inst in res.instances:
inst.modify_attribute('sourceDestCheck', False)
instance_dict_array = [] instance_dict_array = []
created_instance_ids = [] created_instance_ids = []
...@@ -561,12 +990,12 @@ def terminate_instances(module, ec2, instance_ids): ...@@ -561,12 +990,12 @@ def terminate_instances(module, ec2, instance_ids):
terminated_instance_ids = [] terminated_instance_ids = []
for res in ec2.get_all_instances(instance_ids): for res in ec2.get_all_instances(instance_ids):
for inst in res.instances: for inst in res.instances:
if inst.state == 'running': if inst.state == 'running' or inst.state == 'stopped':
terminated_instance_ids.append(inst.id) terminated_instance_ids.append(inst.id)
instance_dict_array.append(get_instance_info(inst)) instance_dict_array.append(get_instance_info(inst))
try: try:
ec2.terminate_instances([inst.id]) ec2.terminate_instances([inst.id])
except EC2ResponseError as e: except EC2ResponseError, e:
module.fail_json(msg='Unable to terminate instance {0}, error: {1}'.format(inst.id, e)) module.fail_json(msg='Unable to terminate instance {0}, error: {1}'.format(inst.id, e))
changed = True changed = True
...@@ -596,73 +1025,143 @@ def terminate_instances(module, ec2, instance_ids): ...@@ -596,73 +1025,143 @@ def terminate_instances(module, ec2, instance_ids):
return (changed, instance_dict_array, terminated_instance_ids) return (changed, instance_dict_array, terminated_instance_ids)
def startstop_instances(module, ec2, instance_ids, state):
"""
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>}, ..]
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.
"""
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')
# Check that our instances are not in the state we want to take them to
# and change them to our desired state
running_instances_array = []
for res in ec2.get_all_instances(instance_ids):
for inst in res.instances:
if inst.state != state:
instance_dict_array.append(get_instance_info(inst))
try:
if state == 'running':
inst.start()
else:
inst.stop()
except EC2ResponseError, e:
module.fail_json(msg='Unable to change state for instance {0}, error: {1}'.format(inst.id, e))
changed = True
## Wait for all the instances to finish starting or stopping
wait_timeout = time.time() + wait_timeout
while wait and wait_timeout > time.time():
matched_instances = []
for res in ec2.get_all_instances(instance_ids):
for i in res.instances:
if i.state == state:
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 main(): def main():
module = AnsibleModule( argument_spec = ec2_argument_spec()
argument_spec = dict( argument_spec.update(dict(
key_name = dict(aliases = ['keypair']), key_name = dict(aliases = ['keypair']),
id = dict(), id = dict(),
group = dict(type='list'), group = dict(type='list'),
group_id = dict(type='list'), group_id = dict(type='list'),
region = dict(aliases=['aws_region', 'ec2_region'], choices=AWS_REGIONS),
zone = dict(aliases=['aws_zone', 'ec2_zone']), zone = dict(aliases=['aws_zone', 'ec2_zone']),
instance_type = dict(aliases=['type']), instance_type = dict(aliases=['type']),
spot_price = dict(),
image = dict(), image = dict(),
kernel = dict(), kernel = dict(),
count = dict(default='1'), count = dict(type='int', default='1'),
monitoring = dict(type='bool', default=False), monitoring = dict(type='bool', default=False),
ramdisk = dict(), ramdisk = dict(),
wait = dict(type='bool', default=False), wait = dict(type='bool', default=False),
wait_timeout = dict(default=300), wait_timeout = dict(default=300),
ec2_url = dict(), spot_wait_timeout = dict(default=600),
ec2_secret_key = dict(aliases=['aws_secret_key', 'secret_key'], no_log=True),
ec2_access_key = dict(aliases=['aws_access_key', 'access_key']),
placement_group = dict(), placement_group = dict(),
user_data = dict(), user_data = dict(),
instance_tags = dict(type='dict'), instance_tags = dict(type='dict'),
vpc_subnet_id = dict(), vpc_subnet_id = dict(),
assign_public_ip = dict(type='bool', default=False),
private_ip = dict(), private_ip = dict(),
instance_profile_name = dict(), instance_profile_name = dict(),
instance_ids = dict(type='list'), instance_ids = dict(type='list'),
source_dest_check = dict(type='bool', default=True),
state = dict(default='present'), state = dict(default='present'),
root_ebs_size = dict(default=None), exact_count = dict(type='int', default=None),
count_tag = dict(),
volumes = dict(type='list'),
ebs_optimized = dict(),
) )
) )
# def get_ec2_creds(module): module = AnsibleModule(
# return ec2_url, ec2_access_key, ec2_secret_key, region argument_spec=argument_spec,
ec2_url, aws_access_key, aws_secret_key, region = get_ec2_creds(module) mutually_exclusive = [
['exact_count', 'count'],
['exact_count', 'state'],
['exact_count', 'instance_ids']
],
)
# If we have a region specified, connect to its endpoint. ec2 = ec2_connect(module)
if region:
try: tagged_instances = []
ec2 = boto.ec2.connect_to_region(region, aws_access_key_id=aws_access_key, aws_secret_access_key=aws_secret_key)
except boto.exception.NoAuthHandlerFound, e:
module.fail_json(msg = str(e))
# If we specified an ec2_url then try connecting to it
elif ec2_url:
try:
ec2 = boto.connect_ec2_endpoint(ec2_url, aws_access_key, aws_secret_key)
except boto.exception.NoAuthHandlerFound, e:
module.fail_json(msg = str(e))
else:
module.fail_json(msg="Either region or ec2_url must be specified")
if module.params.get('state') == 'absent': state = module.params.get('state')
if state == 'absent':
instance_ids = module.params.get('instance_ids') instance_ids = module.params.get('instance_ids')
if not isinstance(instance_ids, list): if not isinstance(instance_ids, list):
module.fail_json(msg='termination_list needs to be a list of instances to terminate') 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) (changed, instance_dict_array, new_instance_ids) = terminate_instances(module, ec2, instance_ids)
elif module.params.get('state') == 'present': elif state in ('running', 'stopped'):
instance_ids = module.params.get('instance_ids')
if not isinstance(instance_ids, list):
module.fail_json(msg='running list needs to be a list of instances to run: %s' % instance_ids)
(changed, instance_dict_array, new_instance_ids) = startstop_instances(module, ec2, instance_ids, state)
elif state == 'present':
# Changed is always set to true when provisioning new instances # Changed is always set to true when provisioning new instances
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'): if not module.params.get('image'):
module.fail_json(msg='image parameter is required for new instance') 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) (instance_dict_array, new_instance_ids, changed) = create_instances(module, ec2)
else:
(tagged_instances, instance_dict_array, new_instance_ids, changed) = enforce_count(module, ec2)
module.exit_json(changed=changed, instance_ids=new_instance_ids, instances=instance_dict_array) module.exit_json(changed=changed, instance_ids=new_instance_ids, instances=instance_dict_array, tagged_instances=tagged_instances)
# import module snippets # import module snippets
from ansible.module_utils.basic import * from ansible.module_utils.basic import *
......
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
- name: terminating single instance - name: terminating single instance
local_action: local_action:
module: ec2_local module: ec2
state: 'absent' state: 'absent'
region: "{{ region }}" region: "{{ region }}"
instance_ids: ${tag_lookup.instance_ids} instance_ids: ${tag_lookup.instance_ids}
...@@ -36,7 +36,7 @@ ...@@ -36,7 +36,7 @@
- name: Launch ec2 instance - name: Launch ec2 instance
local_action: local_action:
module: ec2_local module: ec2
keypair: "{{ keypair }}" keypair: "{{ keypair }}"
group: "{{ security_group }}" group: "{{ security_group }}"
instance_type: "{{ instance_type }}" instance_type: "{{ instance_type }}"
...@@ -44,7 +44,9 @@ ...@@ -44,7 +44,9 @@
wait: true wait: true
region: "{{ region }}" region: "{{ region }}"
instance_tags: "{{instance_tags}}" instance_tags: "{{instance_tags}}"
root_ebs_size: "{{ root_ebs_size }}" volumes:
- device_name: /dev/sda1
volume_size: "{{ root_ebs_size }}"
zone: "{{ zone }}" zone: "{{ zone }}"
instance_profile_name: "{{ instance_profile_name }}" instance_profile_name: "{{ instance_profile_name }}"
register: ec2 register: ec2
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment