Skip to content

Instantly share code, notes, and snippets.

@jlaska
Created November 1, 2013 16:49
Show Gist options
  • Save jlaska/7268285 to your computer and use it in GitHub Desktop.
Save jlaska/7268285 to your computer and use it in GitHub Desktop.
[ansible/library/cloud/ec2] Support wait on terminate and enable tag idempotency
--- library/cloud/ec2 2013-11-01 07:44:09.000000000 -0400
+++ library/cloud/ec2 2013-11-01 08:40:04.000000000 -0400
@@ -34,6 +34,12 @@
required: false
default: null
aliases: []
+ idempotency_attribute:
+ description:
+ - Specify an alternative attribute to enforce idempotency.
+ required: false
+ default: null
+ aliases: []
group:
description:
- security group (or list of groups) to use with the instance
@@ -121,7 +127,7 @@
required: False
default: 1
aliases: []
- monitoring:
+ monitor:
version_added: "1.1"
description:
- enable detailed monitoring (CloudWatch) for instance
@@ -213,8 +219,7 @@
wait: yes
wait_timeout: 500
count: 5
- instance_tags: '{"db":"postgres"}'
- monitoring=yes
+ instance_tags: '{"db":"postgres"}' monitoring=yes'
# Multiple groups example
local_action:
@@ -226,8 +231,7 @@
wait: yes
wait_timeout: 500
count: 5
- instance_tags: '{"db":"postgres"}'
- monitoring=yes
+ instance_tags: '{"db":"postgres"}' monitoring=yes'
# VPC example
- local_action:
@@ -349,6 +353,58 @@
return 'instance_profile_name' in run_instances_method.func_code.co_varnames
+def get_instances(module, ec2, **kwargs):
+
+ # Gather kwargs
+ state = kwargs.get('state', None)
+ id = kwargs.get('id', None)
+ idempotency_attribute = kwargs.get('idempotency_attribute', None)
+ instance_ids = kwargs.get('instance_ids', [])
+ instance_tags = kwargs.get('instance_tags')
+ image = kwargs.get('image', None)
+ group_name = kwargs.get('group_name', None)
+ group_id = kwargs.get('group_id', None)
+
+ # initialize filters dict
+ filters = dict()
+
+ # Filter by state?
+ if state is not None:
+ filters.update({'instance-state-name': state})
+
+ # Filter by instance_id?
+ if id is not None:
+ filters.update({'client-token': id})
+
+ # Filter by a list of instance_ids
+ elif instance_ids:
+ filters.update({'instance-id': instance_ids})
+
+ # Filter by other attribute
+ elif idempotency_attribute is not None:
+ # Custom attribute idempotency requested
+ if idempotency_attribute == "tags":
+ if instance_tags is None:
+ module.fail_json(msg = "Idempotency attribute set to id, so a value for the id option must also be provided.")
+ else:
+ filters.update(dict(("tag:"+tn, tv) for (tn,tv) in instance_tags.iteritems()))
+
+ elif idempotency_attribute == "image-id":
+ filters.update({"image-id": image})
+ elif idempotency_attribute == "group-name":
+ filters.update({"group-name": group_name})
+ elif idempotency_attribute == "group_id":
+ filters.update({"group-id": group_id})
+ elif idempotency_attribute == "client-token":
+ if id is None:
+ module.fail_json(msg = "Idempotency attribute set to id, so a value for the id option must also be provided.")
+ else:
+ filters.update({"client-token": id})
+ else:
+ module.fail_json(msg = "Idempotency attribute '%s' not supported." % idempotency_attribute)
+
+ return ec2.get_all_instances(filters=filters)
+
def create_instances(module, ec2):
"""
Creates new instances
@@ -363,6 +419,8 @@
key_name = module.params.get('key_name')
id = module.params.get('id')
+ idempotency_attribute = module.params.get('idempotency_attribute')
+ instance_ids = module.params.get('instance_ids')
group_name = module.params.get('group')
group_id = module.params.get('group_id')
zone = module.params.get('zone')
@@ -381,7 +439,6 @@
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)"))
@@ -392,8 +449,6 @@
if group_name:
grp_details = ec2.get_all_security_groups()
if type(group_name) == list:
- # FIXME: this should be a nice list comprehension
- # also not py 2.4 compliant
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:
@@ -416,9 +471,18 @@
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)
+ filter_args = {
+ 'id': id,
+ 'idempotency_attribute': idempotency_attribute,
+ 'instance_ids': instance_ids,
+ 'instance_tags': module.params.get('instance_tags'),
+ 'image': module.params.get('image'),
+ 'group_name': module.params.get('group_name'),
+ 'group_id': module.params.get('group_id'),}
+
+ # Idempotency?
+ if any([id, idempotency_attribute, instance_ids]):
+ previous_reservations = get_instances(module, ec2, **dict(state='running', **filter_args))
for res in previous_reservations:
for prev_instance in res.instances:
running_instances.append(prev_instance)
@@ -481,15 +545,15 @@
module.fail_json(msg = "%s: %s" % (e.error_code, e.error_message))
# wait here until the instances are up
- this_res = []
+ running = []
num_running = 0
wait_timeout = time.time() + wait_timeout
while wait_timeout > time.time() and num_running < len(instids):
- res_list = res.connection.get_all_instances(instids)
- if len(res_list) > 0:
- this_res = res_list[0]
- num_running = len([ i for i in this_res.instances if i.state=='running' ])
- else:
+ response = res.connection.get_all_instances(instids)
+ try:
+ running = response.pop()
+ num_running = len([ i for i in running.instances if i.state=='running' ])
+ except (IndexError, Exception), e:
# got a bad response of some sort, possibly due to
# stale/cached data. Wait a second and then try again
time.sleep(1)
@@ -503,7 +567,7 @@
# waiting took too long
module.fail_json(msg = "wait for instances running timeout on %s" % time.asctime())
- for inst in this_res.instances:
+ for inst in running.instances:
running_instances.append(inst)
instance_dict_array = []
@@ -516,14 +580,13 @@
return (instance_dict_array, created_instance_ids, changed)
-def terminate_instances(module, ec2, instance_ids):
+def terminate_instances(module, ec2, instance_termination_filter):
"""
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>}, ..]
+ instance_termination_filter: a dictionary used to determine which intances to terminate
Returns a dictionary of instance information
about the instances terminated.
@@ -536,11 +599,12 @@
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')
+ # Whether to wait for termination to complete before returning
+ wait = module.params.get('wait')
+ wait_timeout = int(module.params.get('wait_timeout'))
terminated_instance_ids = []
- for res in ec2.get_all_instances(instance_ids):
+ for res in get_instances(module, ec2, **instance_termination_filter):
for inst in res.instances:
if inst.state == 'running':
terminated_instance_ids.append(inst.id)
@@ -551,8 +615,31 @@
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)
+ # wait here until the instances are 'terminated'
+ terminated = []
+ num_terminated = 0
+ wait_timeout = time.time() + wait_timeout
+ while wait_timeout > time.time() and num_terminated < len(terminated_instance_ids):
+ response = get_instances(module, ec2, **dict(state='terminated', **instance_termination_filter))
+ try:
+ terminated = response.pop()
+ num_terminated = len(terminated.instances)
+ except (IndexError, Exception), 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 wait and num_terminated < len(terminated_instance_ids):
+ time.sleep(5)
+ else:
+ break
+ if wait and wait_timeout <= time.time():
+ # waiting took too long
+ module.fail_json(msg = "wait for instance termination timeout on %s" % time.asctime())
+
+ return (changed, instance_dict_array, terminated_instance_ids)
def main():
@@ -560,24 +647,25 @@
argument_spec = dict(
key_name = dict(aliases = ['keypair']),
id = dict(),
+ idempotency_attribute = dict(type='str', default='client-token', choices=['client-token', 'image-id', 'group-name', 'group-id', 'tags']),
group = dict(type='list'),
- group_id = dict(type='list'),
+ group_id = dict(),
region = dict(aliases=['aws_region', 'ec2_region'], choices=AWS_REGIONS),
zone = dict(aliases=['aws_zone', 'ec2_zone']),
instance_type = dict(aliases=['type']),
image = dict(),
kernel = dict(),
count = dict(default='1'),
- monitoring = dict(type='bool', default=False),
+ monitoring = dict(choices=BOOLEANS, default=False),
ramdisk = dict(),
- wait = dict(type='bool', default=False),
+ wait = dict(choices=BOOLEANS, default=False),
wait_timeout = dict(default=300),
ec2_url = dict(),
aws_secret_key = dict(aliases=['ec2_secret_key', 'secret_key'], no_log=True),
aws_access_key = dict(aliases=['ec2_access_key', 'access_key']),
placement_group = dict(),
user_data = dict(),
- instance_tags = dict(type='dict'),
+ instance_tags = dict(type='dict', default={}),
vpc_subnet_id = dict(),
private_ip = dict(),
instance_profile_name = dict(),
@@ -591,7 +679,6 @@
aws_access_key = module.params.get('aws_access_key')
region = module.params.get('region')
-
# 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']
@@ -630,11 +717,15 @@
module.fail_json(msg="Either region or ec2_url must be specified")
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')
+ instance_termination_filter = {
+ 'instance_ids': module.params.get('instance_ids'),
+ 'idempotency_attribute': module.params.get('idempotency_attribute'),
+ 'instance_tags': module.params.get('instance_tags'),
+ 'image': module.params.get('image'),
+ 'group_name': module.params.get('group_name'),
+ 'group_id': module.params.get('group_id'),}
- (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_termination_filter)
elif module.params.get('state') == 'present':
# Changed is always set to true when provisioning new instances
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment