From fd919062cb0dba383384fc0f7380acb48151d599 Mon Sep 17 00:00:00 2001 From: James Tanner Date: Thu, 6 Feb 2014 20:22:45 -0500 Subject: [PATCH 1/3] Add exact_count and count_tag to the ec2 module. --- library/cloud/ec2 | 118 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 115 insertions(+), 3 deletions(-) diff --git a/library/cloud/ec2 b/library/cloud/ec2 index c28c4b51576..f8e48948c03 100644 --- a/library/cloud/ec2 +++ b/library/cloud/ec2 @@ -376,6 +376,7 @@ local_action: import sys import time +from ast import literal_eval try: import boto.ec2 @@ -385,6 +386,68 @@ except ImportError: print "failed=True msg='boto required for this module'" 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): """ @@ -473,7 +536,45 @@ def create_block_device(module, ec2, volume): delete_on_termination=volume.get('delete_on_termination', False), iops=volume.get('iops')) -def create_instances(module, ec2): + +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) + 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] + (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 + + return (instance_dict_array, changed_instance_ids, changed) + + +def create_instances(module, ec2, override_count=None): """ Creates new instances @@ -492,7 +593,10 @@ def create_instances(module, ec2): zone = module.params.get('zone') instance_type = module.params.get('instance_type') image = module.params.get('image') - count = module.params.get('count') + 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') @@ -506,6 +610,8 @@ def create_instances(module, ec2): private_ip = module.params.get('private_ip') instance_profile_name = module.params.get('instance_profile_name') volumes = module.params.get('volumes') + exact_count = module.params.get('exact_count') + count_tag = module.params.get('count_tag') # group_id and group_name are exclusive of each other if group_id and group_name: @@ -832,6 +938,8 @@ def main(): instance_profile_name = dict(), instance_ids = dict(type='list'), state = dict(default='present'), + exact_count = dict(type='int'), + count_tag = dict(), volumes = dict(type='list'), ) ) @@ -857,7 +965,11 @@ def main(): # 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') - (instance_dict_array, new_instance_ids, changed) = create_instances(module, ec2) + + if module.params.get('exact_count'): + (instance_dict_array, new_instance_ids, changed) = enforce_count(module, ec2) + else: + (instance_dict_array, new_instance_ids, changed) = create_instances(module, ec2) module.exit_json(changed=changed, instance_ids=new_instance_ids, instances=instance_dict_array) From 005ef837d934eb4e75b6cba9f37c6c345f91a966 Mon Sep 17 00:00:00 2001 From: James Tanner Date: Fri, 7 Feb 2014 10:34:45 -0500 Subject: [PATCH 2/3] Update docstrings --- library/cloud/ec2 | 86 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 80 insertions(+), 6 deletions(-) diff --git a/library/cloud/ec2 b/library/cloud/ec2 index f8e48948c03..bc037265563 100644 --- a/library/cloud/ec2 +++ b/library/cloud/ec2 @@ -198,6 +198,21 @@ options: required: false default: null aliases: [] + 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. + 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 + default: null + aliases: [] + requirements: [ "boto" ] author: Seth Vidal, Tim Gerla, Lester Wade @@ -227,8 +242,9 @@ EXAMPLES = ''' wait: yes wait_timeout: 500 count: 5 - instance_tags: '{"db":"postgres"}' - monitoring=yes + instance_tags: + db: postgres + monitoring: yes # Single instance with additional IOPS volume from snapshot local_action: @@ -245,7 +261,7 @@ local_action: device_type: io1 iops: 1000 volume_size: 100 - monitoring=yes + monitoring: yes # Multiple groups example local_action: @@ -257,8 +273,9 @@ local_action: wait: yes wait_timeout: 500 count: 5 - instance_tags: '{"db":"postgres"}' - monitoring=yes + instance_tags: + db: postgres + monitoring: yes # Multiple instances with additional volume from snapshot local_action: @@ -274,7 +291,7 @@ local_action: - device_name: /dev/sdb snapshot: snap-abcdef12 volume_size: 10 - monitoring=yes + monitoring: yes # VPC example - local_action: @@ -372,6 +389,63 @@ local_action: region: '{{ region }}' state: stopped wait: True + +# +# Enforce that 5 instances with a tag "foo" are running +# + +- local_action: + module: ec2 + keypair: 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 instances with a tag "foo" that has a value "bar" +# + +- local_action: + module: ec2 + keypair: mykey + instance_type: c1.medium + image: emi-40603AD1 + wait: yes + group: webserver + instance_tags: + foo: bar + exact_count: 5 + count_tag: + foo: bar + +# +# 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 From f101768a9dcb1ea9a3a414bbb08d491118d17284 Mon Sep 17 00:00:00 2001 From: James Tanner Date: Fri, 7 Feb 2014 10:49:13 -0500 Subject: [PATCH 3/3] Make an example that sets the instance Names --- library/cloud/ec2 | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/library/cloud/ec2 b/library/cloud/ec2 index bc037265563..585b70280de 100644 --- a/library/cloud/ec2 +++ b/library/cloud/ec2 @@ -407,7 +407,7 @@ local_action: count_tag: foo # -# Enforce that 5 instances with a tag "foo" that has a value "bar" +# Enforce that 5 running instances named "database" with a "dbtype" of "postgres" # - local_action: @@ -418,10 +418,12 @@ local_action: wait: yes group: webserver instance_tags: - foo: bar + Name: database + dbtype: postgres exact_count: 5 count_tag: - foo: bar + Name: database + dbtype: postgres # # count_tag complex argument examples