From f802fc2ce31b66a2c0b59519cbf531343f19fcf3 Mon Sep 17 00:00:00 2001 From: Mark Chance Date: Wed, 9 Sep 2015 17:08:48 -0600 Subject: [PATCH 1/3] cloud amazon ECS cluster module --- cloud/amazon/ecs_cluster.py | 240 ++++++++++++++++++++++++++++++ cloud/amazon/ecs_cluster_facts.py | 173 +++++++++++++++++++++ 2 files changed, 413 insertions(+) create mode 100644 cloud/amazon/ecs_cluster.py create mode 100644 cloud/amazon/ecs_cluster_facts.py diff --git a/cloud/amazon/ecs_cluster.py b/cloud/amazon/ecs_cluster.py new file mode 100644 index 00000000000..f5bd1e42bc1 --- /dev/null +++ b/cloud/amazon/ecs_cluster.py @@ -0,0 +1,240 @@ +#!/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 . + +DOCUMENTATION = ''' +--- +module: ecs_cluster +short_description: create or terminate ecs clusters +notes: + - When deleting a cluster, the information returned is the state of the cluster prior to deletion. + - It will also wait for a cluster to have instances registered to it. +description: + - Creates or terminates ecs clusters. +version_added: "1.9" +requirements: [ json, time, boto, boto3 ] +options: + state=dict(required=True, choices=['present', 'absent', 'has_instances'] ), + name=dict(required=True, type='str' ), + delay=dict(required=False, type='int', default=10), + repeat=dict(required=False, type='int', default=10) + + state: + description: + - The desired state of the cluster + required: true + choices: ['present', 'absent', 'has_instances'] + name: + description: + - The cluster name + required: true + delay: + description: + - Number of seconds to wait + required: true + name: + description: + - The cluster name + required: true +''' + +EXAMPLES = ''' +# Note: These examples do not set authentication details, see the AWS Guide for details. + +# Cluster creation +- ecs_cluster: + name: default + state: present + +# Cluster deletion +- ecs_cluster: + name: default + state: absent + +- name: Wait for register + ecs_cluster: + name: "{{ new_cluster }}" + state: has_instances + delay: 10 + repeat: 10 + register: task_output + +''' +RETURN = ''' +activeServicesCount: + description: how many services are active in this cluster + returned: 0 if a new cluster + type: int +clusterArn: + description: the ARN of the cluster just created + type: string (ARN) + sample: arn:aws:ecs:us-west-2:172139249013:cluster/test-cluster-mfshcdok +clusterName: + description: name of the cluster just created (should match the input argument) + type: string + sample: test-cluster-mfshcdok +pendingTasksCount: + description: how many tasks are waiting to run in this cluster + returned: 0 if a new cluster + type: int +registeredContainerInstancesCount: + description: how many container instances are available in this cluster + returned: 0 if a new cluster + type: int +runningTasksCount: + description: how many tasks are running in this cluster + returned: 0 if a new cluster + type: int +status: + description: the status of the new cluster + returned: ACTIVE + type: string +''' +try: + import json, time + import boto + HAS_BOTO = True +except ImportError: + HAS_BOTO = False + +try: + import boto3 + HAS_BOTO3 = True +except ImportError: + HAS_BOTO3 = False + +class EcsClusterManager: + """Handles ECS Clusters""" + + def __init__(self, module): + self.module = module + + try: + # self.ecs = boto3.client('ecs') + region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) + if not region: + module.fail_json(msg="Region must be specified as a parameter, in EC2_REGION or AWS_REGION environment variables or in boto configuration file") + self.ecs = boto3_conn(module, conn_type='client', resource='ecs', region=region, endpoint=ec2_url, **aws_connect_kwargs) + except boto.exception.NoAuthHandlerFound, e: + self.module.fail_json(msg=str(e)) + + def find_in_array(self, array_of_clusters, cluster_name, field_name='clusterArn'): + for c in array_of_clusters: + if c[field_name].endswith(cluster_name): + return c + return None + + def describe_cluster(self, cluster_name): + response = self.ecs.describe_clusters(clusters=[ + cluster_name + ]) + if len(response['failures'])>0: + c = self.find_in_array(response['failures'], cluster_name, 'arn') + if c and c['reason']=='MISSING': + return None + # fall thru and look through found ones + if len(response['clusters'])>0: + c = self.find_in_array(response['clusters'], cluster_name) + if c: + return c + raise StandardError("Unknown problem describing cluster %s." % cluster_name) + + def create_cluster(self, clusterName = 'default'): + response = self.ecs.create_cluster(clusterName=clusterName) + return response['cluster'] + + def delete_cluster(self, clusterName): + return self.ecs.delete_cluster(cluster=clusterName) + +def main(): + + argument_spec = ec2_argument_spec() + argument_spec.update(dict( + state=dict(required=True, choices=['present', 'absent', 'has_instances'] ), + name=dict(required=True, type='str' ), + delay=dict(required=False, type='int', default=10), + repeat=dict(required=False, type='int', default=10) + )) + required_together = ( ['state', 'name'] ) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True, required_together=required_together) + + if not HAS_BOTO: + module.fail_json(msg='boto is required.') + + if not HAS_BOTO3: + module.fail_json(msg='boto3 is required.') + + cluster_name = module.params['name'] + + cluster_mgr = EcsClusterManager(module) + try: + existing = cluster_mgr.describe_cluster(module.params['name']) + except Exception, e: + module.fail_json(msg=str(e)) + + results = dict(changed=False) + if module.params['state'] == 'present': + if existing and 'status' in existing and existing['status']=="ACTIVE": + results['cluster']=existing + else: + if not module.check_mode: + # doesn't exist. create it. + results['cluster'] = cluster_mgr.create_cluster(module.params['name']) + results['changed'] = True + + # delete the cluster + elif module.params['state'] == 'absent': + if not existing: + pass + else: + # it exists, so we should delete it and mark changed. + # return info about the cluster deleted + results['cluster'] = existing + if 'status' in existing and existing['status']=="INACTIVE": + results['changed'] = False + else: + if not module.check_mode: + cluster_mgr.delete_cluster(module.params['name']) + results['changed'] = True + elif module.params['state'] == 'has_instances': + if not existing: + module.fail_json(msg="Cluster '"+module.params['name']+" not found.") + return + # it exists, so we should delete it and mark changed. + # return info about the cluster deleted + delay = module.params['delay'] + repeat = module.params['repeat'] + time.sleep(delay) + count = 0 + for i in range(repeat): + existing = cluster_mgr.describe_cluster(module.params['name']) + count = existing['registeredContainerInstancesCount'] + if count > 0: + results['changed'] = True + break + time.sleep(delay) + if count == 0 and i is repeat-1: + module.fail_json(msg="Cluster instance count still zero after "+str(repeat)+" tries of "+str(delay)+" seconds each.") + return + + module.exit_json(**results) + +# import module snippets +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * + +if __name__ == '__main__': + main() diff --git a/cloud/amazon/ecs_cluster_facts.py b/cloud/amazon/ecs_cluster_facts.py new file mode 100644 index 00000000000..4dac2f42daa --- /dev/null +++ b/cloud/amazon/ecs_cluster_facts.py @@ -0,0 +1,173 @@ +#!/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 . + +DOCUMENTATION = ''' +--- +module: ecs_cluster_facts +short_description: list or describe clusters or their instances in ecs +description: + - Lists or describes clusters or cluster instances in ecs. +version_added: 1.9 +options: + details: + description: + - Set this to true if you want detailed information. + required: false + default: false + cluster: + description: + - The cluster ARNS to list. + required: false + default: 'default' + instances: + description: + - The instance ARNS to list. + required: false + default: None (returns all) + option: + description: + - Whether to return information about clusters or their instances + required: false + choices: ['clusters', 'instances'] + default: 'clusters' +''' + +EXAMPLES = ''' +# Note: These examples do not set authentication details, see the AWS Guide for details. + +# Basic listing example +- ecs_task: + cluster=test-cluster + task_list=123456789012345678901234567890123456 + +# Basic example of deregistering task +- ecs_task: + state: absent + family: console-test-tdn + revision: 1 +''' +RETURN = ''' +clusters: + description: + - array of cluster ARNs when details is false + - array of dicts when details is true + sample: [ "arn:aws:ecs:us-west-2:172139249013:cluster/test-cluster" ] +''' +try: + import json, os + import boto + import botocore + HAS_BOTO = True +except ImportError: + HAS_BOTO = False + +try: + import boto3 + # import module snippets + from ansible.module_utils.ec2 import ec2_argument_spec, get_aws_connection_info, boto3_conn + HAS_BOTO3 = True +except ImportError: + HAS_BOTO3 = False + +class EcsClusterManager: + """Handles ECS Clusters""" + + def __init__(self, module): + self.module = module + + try: + region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) + if not region: + module.fail_json(msg="Region must be specified as a parameter, in EC2_REGION or AWS_REGION environment variables or in boto configuration file") + self.ecs = boto3_conn(module, conn_type='client', resource='ecs', region=region, endpoint=ec2_url, **aws_connect_kwargs) + except boto.exception.NoAuthHandlerFound, e: + self.module.fail_json(msg=str(e)) + + def list_container_instances(self, cluster): + response = self.ecs.list_container_instances(cluster=cluster) + relevant_response = dict(instances = response['containerInstanceArns']) + return relevant_response + + def describe_container_instances(self, cluster, instances): + response = self.ecs.describe_container_instances( + clusters=cluster, + containerInstances=instances.split(",") if instances else [] + ) + relevant_response = dict() + if 'containerInstances' in response and len(response['containerInstances'])>0: + relevant_response['instances'] = response['containerInstances'] + if 'failures' in response and len(response['failures'])>0: + relevant_response['instances_not_running'] = response['failures'] + return relevant_response + + def list_clusters(self): + response = self.ecs.list_clusters() + relevant_response = dict(clusters = response['clusterArns']) + return relevant_response + + def describe_clusters(self, cluster): + response = self.ecs.describe_clusters( + clusters=cluster.split(",") if cluster else [] + ) + relevant_response = dict() + if 'clusters' in response and len(response['clusters'])>0: + relevant_response['clusters'] = response['clusters'] + if 'failures' in response and len(response['failures'])>0: + relevant_response['clusters_not_running'] = response['failures'] + return relevant_response + +def main(): + + argument_spec = ec2_argument_spec() + argument_spec.update(dict( + details=dict(required=False, type='bool' ), + cluster=dict(required=False, type='str' ), + instances=dict(required=False, type='str' ), + option=dict(required=False, choices=['clusters', 'instances'], default='clusters') + )) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + if not HAS_BOTO: + module.fail_json(msg='boto is required.') + + if not HAS_BOTO3: + module.fail_json(msg='boto3 is required.') + + show_details = False + if 'details' in module.params and module.params['details']: + show_details = True + + task_mgr = EcsClusterManager(module) + if module.params['option']=='clusters': + if show_details: + ecs_facts = task_mgr.describe_clusters(module.params['cluster']) + else: + ecs_facts = task_mgr.list_clusters() + if module.params['option']=='instances': + if show_details: + ecs_facts = task_mgr.describe_container_instances(module.params['cluster'], module.params['instances']) + else: + ecs_facts = task_mgr.list_container_instances(module.params['cluster']) + ecs_facts_result = dict(changed=False, ansible_facts=ecs_facts) + module.exit_json(**ecs_facts_result) + +# import module snippets +from ansible.module_utils.basic import * +from ansible.module_utils.urls import * + +if __name__ == '__main__': + main() From de95580f665a628ac4fff2af95d043413aac6334 Mon Sep 17 00:00:00 2001 From: Mark Chance Date: Thu, 10 Sep 2015 08:02:24 -0600 Subject: [PATCH 2/3] fix docs, enhance fail msgs --- cloud/amazon/ecs_cluster.py | 19 +++++++------------ cloud/amazon/ecs_cluster_facts.py | 4 ++-- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/cloud/amazon/ecs_cluster.py b/cloud/amazon/ecs_cluster.py index f5bd1e42bc1..9dc49860384 100644 --- a/cloud/amazon/ecs_cluster.py +++ b/cloud/amazon/ecs_cluster.py @@ -23,14 +23,9 @@ notes: - It will also wait for a cluster to have instances registered to it. description: - Creates or terminates ecs clusters. -version_added: "1.9" +version_added: "2.0" requirements: [ json, time, boto, boto3 ] options: - state=dict(required=True, choices=['present', 'absent', 'has_instances'] ), - name=dict(required=True, type='str' ), - delay=dict(required=False, type='int', default=10), - repeat=dict(required=False, type='int', default=10) - state: description: - The desired state of the cluster @@ -43,11 +38,11 @@ options: delay: description: - Number of seconds to wait - required: true - name: + required: false + repeat: description: - - The cluster name - required: true + - The number of times to wait for the cluster to have an instance + required: false ''' EXAMPLES = ''' @@ -128,7 +123,7 @@ class EcsClusterManager: module.fail_json(msg="Region must be specified as a parameter, in EC2_REGION or AWS_REGION environment variables or in boto configuration file") self.ecs = boto3_conn(module, conn_type='client', resource='ecs', region=region, endpoint=ec2_url, **aws_connect_kwargs) except boto.exception.NoAuthHandlerFound, e: - self.module.fail_json(msg=str(e)) + self.module.fail_json(msg="Can't authorize connection - "+str(e)) def find_in_array(self, array_of_clusters, cluster_name, field_name='clusterArn'): for c in array_of_clusters: @@ -183,7 +178,7 @@ def main(): try: existing = cluster_mgr.describe_cluster(module.params['name']) except Exception, e: - module.fail_json(msg=str(e)) + module.fail_json(msg="Exception describing cluster '"+module.params['name']+"': "+str(e)) results = dict(changed=False) if module.params['state'] == 'present': diff --git a/cloud/amazon/ecs_cluster_facts.py b/cloud/amazon/ecs_cluster_facts.py index 4dac2f42daa..ec1a9209ef7 100644 --- a/cloud/amazon/ecs_cluster_facts.py +++ b/cloud/amazon/ecs_cluster_facts.py @@ -20,7 +20,7 @@ module: ecs_cluster_facts short_description: list or describe clusters or their instances in ecs description: - Lists or describes clusters or cluster instances in ecs. -version_added: 1.9 +version_added: "2.0" options: details: description: @@ -94,7 +94,7 @@ class EcsClusterManager: module.fail_json(msg="Region must be specified as a parameter, in EC2_REGION or AWS_REGION environment variables or in boto configuration file") self.ecs = boto3_conn(module, conn_type='client', resource='ecs', region=region, endpoint=ec2_url, **aws_connect_kwargs) except boto.exception.NoAuthHandlerFound, e: - self.module.fail_json(msg=str(e)) + self.module.fail_json(msg="Can't authorize connection - "+str(e)) def list_container_instances(self, cluster): response = self.ecs.list_container_instances(cluster=cluster) From ff4c0004515b2e7fde3a248141c7cd572379f5f7 Mon Sep 17 00:00:00 2001 From: Mark Chance Date: Mon, 21 Sep 2015 09:56:32 -0600 Subject: [PATCH 3/3] add author tag in doc --- cloud/amazon/ecs_cluster.py | 1 + cloud/amazon/ecs_cluster_facts.py | 1 + 2 files changed, 2 insertions(+) diff --git a/cloud/amazon/ecs_cluster.py b/cloud/amazon/ecs_cluster.py index 9dc49860384..9dac7cd5bad 100644 --- a/cloud/amazon/ecs_cluster.py +++ b/cloud/amazon/ecs_cluster.py @@ -24,6 +24,7 @@ notes: description: - Creates or terminates ecs clusters. version_added: "2.0" +author: Mark Chance(@Java1Guy) requirements: [ json, time, boto, boto3 ] options: state: diff --git a/cloud/amazon/ecs_cluster_facts.py b/cloud/amazon/ecs_cluster_facts.py index ec1a9209ef7..c4dff2706ad 100644 --- a/cloud/amazon/ecs_cluster_facts.py +++ b/cloud/amazon/ecs_cluster_facts.py @@ -21,6 +21,7 @@ short_description: list or describe clusters or their instances in ecs description: - Lists or describes clusters or cluster instances in ecs. version_added: "2.0" +author: Mark Chance(@Java1Guy) options: details: description: