Created
November 1, 2013 16:49
-
-
Save jlaska/7268285 to your computer and use it in GitHub Desktop.
[ansible/library/cloud/ec2] Support wait on terminate and enable tag idempotency
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--- 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