From a5083a4a17599787f897e3b99185dc2453344e87 Mon Sep 17 00:00:00 2001 From: Mark Chance Date: Wed, 9 Sep 2015 17:07:04 -0600 Subject: [PATCH] cloud amazon ECS task modules --- cloud/amazon/ecs_task.py | 268 +++++++++++++++++++++++++++++++++ cloud/amazon/ecs_task_facts.py | 204 +++++++++++++++++++++++++ 2 files changed, 472 insertions(+) create mode 100644 cloud/amazon/ecs_task.py create mode 100644 cloud/amazon/ecs_task_facts.py diff --git a/cloud/amazon/ecs_task.py b/cloud/amazon/ecs_task.py new file mode 100644 index 00000000000..ce9fa2e85a8 --- /dev/null +++ b/cloud/amazon/ecs_task.py @@ -0,0 +1,268 @@ +#!/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_task +short_description: run, start or stop a task in ecs +description: + - Creates or deletes instances of task definitions. +version_added: "2.0" +options: + operation: + description: + - Which task operation to execute + required: True + choices: ['run', 'start', 'stop'] + cluster: + description: + - The name of the cluster to run the task on + required: False + task_definition: + description: + - The task definition to start or run + required: False + overrides: + description: + - A dictionary of values to pass to the new instances + required: False + count: + description: + - How many new instances to start + required: False + task: + description: + - The task to stop + required: False + container_instances: + description: + - The list of container instances on which to deploy the task + required: False + started_by: + description: + - A value showing who or what started the task (for informational purposes) + required: False +''' + +EXAMPLES = ''' +# Simple example of run task +- name: Run task + ecs_task: + operation: run + cluster: console-sample-app-static-cluster + task_definition: console-sample-app-static-taskdef + count: 1 + started_by: ansible_user + register: task_output + +# Simple example of start task + +- name: Start a task + ecs_task: + operation: start + cluster: console-sample-app-static-cluster + task_definition: console-sample-app-static-taskdef + task: "arn:aws:ecs:us-west-2:172139249013:task/3f8353d1-29a8-4689-bbf6-ad79937ffe8a" + container_instances: + - arn:aws:ecs:us-west-2:172139249013:container-instance/79c23f22-876c-438a-bddf-55c98a3538a8 + started_by: ansible_user + register: task_output + +- name: Stop a task + ecs_task: + operation: stop + cluster: console-sample-app-static-cluster + task_definition: console-sample-app-static-taskdef + task: "arn:aws:ecs:us-west-2:172139249013:task/3f8353d1-29a8-4689-bbf6-ad79937ffe8a" +''' +RETURN = ''' +task: + description: details about the tast that was started + type: array of dict + sample: [ clusterArn, containerInstanceArn, containers[], desiredStatus, lastStatus + overrides, startedBy, taskArn, taskDefinitionArn ] +''' +try: + import json + import boto + import botocore + HAS_BOTO = True +except ImportError: + HAS_BOTO = False + +try: + import boto3 + HAS_BOTO3 = True +except ImportError: + HAS_BOTO3 = False + +class EcsExecManager: + """Handles ECS Tasks""" + + 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_tasks(self, cluster_name, service_name, status): + response = self.ecs.list_tasks( + cluster=cluster_name, + family=service_name, + desiredStatus=status + ) + if len(response['taskArns'])>0: + for c in response['taskArns']: + if c.endswith(service_name): + return c + return None + + def run_task(self, cluster, task_definition, overrides, count, startedBy): + if overrides is None: + overrides = dict() + response = self.ecs.run_task( + cluster=cluster, + taskDefinition=task_definition, + overrides=overrides, + count=count, + startedBy=startedBy) + # include tasks and failures + return response['tasks'] + + def start_task(self, cluster, task_definition, overrides, container_instances, startedBy): + args = dict() + if cluster: + args['cluster'] = cluster + if task_definition: + args['taskDefinition']=task_definition + if overrides: + args['overrides']=overrides + if container_instances: + args['containerInstances']=container_instances + if startedBy: + args['startedBy']=startedBy + response = self.ecs.start_task(**args) + # include tasks and failures + return response['tasks'] + + def stop_task(self, cluster, task): + response = self.ecs.stop_task(cluster=cluster, task=task) + return response['task'] + +def main(): + argument_spec = ec2_argument_spec() + argument_spec.update(dict( + operation=dict(required=True, choices=['run', 'start', 'stop'] ), + cluster=dict(required=False, type='str' ), # R S P + task_definition=dict(required=False, type='str' ), # R* S* + overrides=dict(required=False, type='dict'), # R S + count=dict(required=False, type='int' ), # R + task=dict(required=False, type='str' ), # P* + container_instances=dict(required=False, type='list'), # S* + started_by=dict(required=False, type='str' ) # R S + )) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + # Validate Requirements + if not HAS_BOTO: + module.fail_json(msg='boto is required.') + + if not HAS_BOTO3: + module.fail_json(msg='boto3 is required.') + + # Validate Inputs + if module.params['operation'] == 'run': + if not 'task_definition' in module.params and module.params['task_definition'] is None: + module.fail_json(msg="To run a task, a task_definition must be specified") + task_to_list = module.params['task_definition'] + status_type = "RUNNING" + + if module.params['operation'] == 'start': + if not 'task_definition' in module.params and module.params['task_definition'] is None: + module.fail_json(msg="To start a task, a task_definition must be specified") + if not 'container_instances' in module.params and module.params['container_instances'] is None: + module.fail_json(msg="To start a task, container instances must be specified") + task_to_list = module.params['task'] + status_type = "RUNNING" + + if module.params['operation'] == 'stop': + if not 'task' in module.params and module.params['task'] is None: + module.fail_json(msg="To stop a task, a task must be specified") + if not 'task_definition' in module.params and module.params['task_definition'] is None: + module.fail_json(msg="To stop a task, a task definition must be specified") + task_to_list = module.params['task_definition'] + status_type = "STOPPED" + + service_mgr = EcsExecManager(module) + existing = service_mgr.list_tasks(module.params['cluster'], task_to_list, status_type) + + results = dict(changed=False) + if module.params['operation'] == 'run': + if existing: + # TBD - validate the rest of the details + results['task']=existing + else: + if not module.check_mode: + results['task'] = service_mgr.run_task( + module.params['cluster'], + module.params['task_definition'], + module.params['overrides'], + module.params['count'], + module.params['started_by']) + results['changed'] = True + + elif module.params['operation'] == 'start': + if existing: + # TBD - validate the rest of the details + results['task']=existing + else: + if not module.check_mode: + results['task'] = service_mgr.start_task( + module.params['cluster'], + module.params['task_definition'], + module.params['overrides'], + module.params['container_instances'], + module.params['started_by'] + ) + results['changed'] = True + + elif module.params['operation'] == 'stop': + if existing: + results['task']=existing + else: + if not module.check_mode: + # it exists, so we should delete it and mark changed. + # return info about the cluster deleted + results['task'] = service_mgr.stop_task( + module.params['cluster'], + module.params['task'] + ) + results['changed'] = True + + 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_task_facts.py b/cloud/amazon/ecs_task_facts.py new file mode 100644 index 00000000000..541bfaee3c1 --- /dev/null +++ b/cloud/amazon/ecs_task_facts.py @@ -0,0 +1,204 @@ +#!/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_task_facts +short_description: return facts about tasks in ecs +description: + - Describes or lists tasks. +version_added: 1.9 +options: + details: + description: + - Set this to true if you want detailed information about the tasks. + required: false + default: false + type: bool + cluster: + description: + - The cluster in which to list tasks if other than the 'default'. + required: false + default: 'default' + type: str + task_list: + description: + - Set this to a list of task identifiers. If 'details' is false, this is required. + required: false + family: + required: False + type: str + + container_instance: + required: False + type: 'str' + max_results: + required: False + type: 'int' + started_by: + required: False + type: 'str' + service_name: + required: False + type: 'str' + desired_status: + required: False + choices=['RUNNING', 'PENDING', 'STOPPED'] + +''' + +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 = ''' +cache_updated: + description: if the cache was updated or not + returned: success, in some cases + type: boolean + sample: True +cache_update_time: + description: time of the last cache update (0 if unknown) + returned: success, in some cases + type: datetime + sample: 1425828348000 +stdout: + description: output from apt + returned: success, when needed + type: string + sample: "Reading package lists...\nBuilding dependency tree...\nReading state information...\nThe following extra packages will be installed:\n apache2-bin ..." +stderr: + description: error output from apt + returned: success, when needed + type: string + sample: "AH00558: apache2: Could not reliably determine the server's fully qualified domain name, using 127.0.1.1. Set the 'ServerName' directive globally to ..." +''' +try: + import json, os + import boto + import botocore + # import module snippets + from ansible.module_utils.basic import * + from ansible.module_utils.ec2 import * + HAS_BOTO = True +except ImportError: + HAS_BOTO = False + +try: + import boto3 + HAS_BOTO3 = True +except ImportError: + HAS_BOTO3 = False + +class EcsTaskManager: + """Handles ECS Tasks""" + + 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 transmogrify(self, params, field, dictionary, arg_name): + if field in params and params[field] is not None: + dictionary[arg_name] = params[field] + + def list_tasks(self, params): + fn_args = dict() + self.transmogrify(params, 'cluster', fn_args, 'cluster') + self.transmogrify(params, 'container_instance', fn_args, 'containerInstance') + self.transmogrify(params, 'family', fn_args, 'family') + self.transmogrify(params, 'max_results', fn_args, 'maxResults') + self.transmogrify(params, 'started_by', fn_args, 'startedBy') + self.transmogrify(params, 'service_name', fn_args, 'startedBy') + self.transmogrify(params, 'desired_status', fn_args, 'desiredStatus') + relevant_response = dict() + try: + response = self.ecs.list_tasks(**fn_args) + relevant_response['tasks'] = response['taskArns'] + except botocore.exceptions.ClientError: + relevant_response['tasks'] = [] + return relevant_response + + def describe_tasks(self, cluster_name, tasks): + response = self.ecs.describe_tasks( + cluster=cluster_name if cluster_name else '', + tasks=tasks.split(",") if tasks else [] + ) + relevant_response = dict( + tasks = response['tasks'], + tasks_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' ), + task_list = dict(required=False, type='str'), + family=dict(required= False, type='str' ), + container_instance=dict(required=False, type='str' ), + max_results=dict(required=False, type='int' ), + started_by=dict(required=False, type='str' ), + service_name=dict(required=False, type='str' ), + desired_status=dict(required=False, choices=['RUNNING', 'PENDING', 'STOPPED']) + )) + + 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.') + + task_to_describe = module.params['family'] + show_details = False + if 'details' in module.params and module.params['details']: + show_details = True + + task_mgr = EcsTaskManager(module) + if show_details: + if 'task_list' not in module.params or not module.params['task_list']: + module.fail_json(msg="task_list must be specified for ecs_task_facts") + ecs_facts = task_mgr.describe_tasks(module.params['cluster'], module.params['task_list']) + else: + ecs_facts = task_mgr.list_tasks(module.params) + 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()