1#!/usr/bin/python
2# Copyright: Ansible Project
3# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
4
5from __future__ import absolute_import, division, print_function
6__metaclass__ = type
7
8ANSIBLE_METADATA = {'metadata_version': '1.1',
9                    'status': ['preview'],
10                    'supported_by': 'community'}
11
12
13DOCUMENTATION = '''
14---
15module: ecs_service_info
16short_description: list or describe services in ecs
17description:
18    - Lists or describes services in ecs.
19    - This module was called C(ecs_service_facts) before Ansible 2.9, returning C(ansible_facts).
20      Note that the M(ecs_service_info) module no longer returns C(ansible_facts)!
21version_added: "2.1"
22author:
23    - "Mark Chance (@Java1Guy)"
24    - "Darek Kaczynski (@kaczynskid)"
25requirements: [ json, botocore, boto3 ]
26options:
27    details:
28        description:
29            - Set this to true if you want detailed information about the services.
30        required: false
31        default: 'false'
32        type: bool
33    events:
34        description:
35            - Whether to return ECS service events. Only has an effect if C(details) is true.
36        required: false
37        default: 'true'
38        type: bool
39        version_added: "2.6"
40    cluster:
41        description:
42            - The cluster ARNS in which to list the services.
43        required: false
44        default: 'default'
45    service:
46        description:
47            - One or more services to get details for
48        required: false
49extends_documentation_fragment:
50    - aws
51    - ec2
52'''
53
54EXAMPLES = '''
55# Note: These examples do not set authentication details, see the AWS Guide for details.
56
57# Basic listing example
58- ecs_service_info:
59    cluster: test-cluster
60    service: console-test-service
61    details: true
62  register: output
63
64# Basic listing example
65- ecs_service_info:
66    cluster: test-cluster
67  register: output
68'''
69
70RETURN = '''
71services:
72    description: When details is false, returns an array of service ARNs, otherwise an array of complex objects as described below.
73    returned: success
74    type: complex
75    contains:
76        clusterArn:
77            description: The Amazon Resource Name (ARN) of the of the cluster that hosts the service.
78            returned: always
79            type: str
80        desiredCount:
81            description: The desired number of instantiations of the task definition to keep running on the service.
82            returned: always
83            type: int
84        loadBalancers:
85            description: A list of load balancer objects
86            returned: always
87            type: complex
88            contains:
89                loadBalancerName:
90                    description: the name
91                    returned: always
92                    type: str
93                containerName:
94                    description: The name of the container to associate with the load balancer.
95                    returned: always
96                    type: str
97                containerPort:
98                    description: The port on the container to associate with the load balancer.
99                    returned: always
100                    type: int
101        pendingCount:
102            description: The number of tasks in the cluster that are in the PENDING state.
103            returned: always
104            type: int
105        runningCount:
106            description: The number of tasks in the cluster that are in the RUNNING state.
107            returned: always
108            type: int
109        serviceArn:
110            description: The Amazon Resource Name (ARN) that identifies the service. The ARN contains the arn:aws:ecs namespace, followed by the region of the service, the AWS account ID of the service owner, the service namespace, and then the service name. For example, arn:aws:ecs:region :012345678910 :service/my-service .
111            returned: always
112            type: str
113        serviceName:
114            description: A user-generated string used to identify the service
115            returned: always
116            type: str
117        status:
118            description: The valid values are ACTIVE, DRAINING, or INACTIVE.
119            returned: always
120            type: str
121        taskDefinition:
122            description: The ARN of a task definition to use for tasks in the service.
123            returned: always
124            type: str
125        deployments:
126            description: list of service deployments
127            returned: always
128            type: list of complex
129        events:
130            description: list of service events
131            returned: when events is true
132            type: list of complex
133'''  # NOQA
134
135try:
136    import botocore
137except ImportError:
138    pass  # handled by AnsibleAWSModule
139
140from ansible.module_utils.aws.core import AnsibleAWSModule
141from ansible.module_utils.ec2 import ec2_argument_spec, AWSRetry
142
143
144class EcsServiceManager:
145    """Handles ECS Services"""
146
147    def __init__(self, module):
148        self.module = module
149        self.ecs = module.client('ecs')
150
151    @AWSRetry.backoff(tries=5, delay=5, backoff=2.0)
152    def list_services_with_backoff(self, **kwargs):
153        paginator = self.ecs.get_paginator('list_services')
154        try:
155            return paginator.paginate(**kwargs).build_full_result()
156        except botocore.exceptions.ClientError as e:
157            if e.response['Error']['Code'] == 'ClusterNotFoundException':
158                self.module.fail_json_aws(e, "Could not find cluster to list services")
159            else:
160                raise
161
162    @AWSRetry.backoff(tries=5, delay=5, backoff=2.0)
163    def describe_services_with_backoff(self, **kwargs):
164        return self.ecs.describe_services(**kwargs)
165
166    def list_services(self, cluster):
167        fn_args = dict()
168        if cluster and cluster is not None:
169            fn_args['cluster'] = cluster
170        try:
171            response = self.list_services_with_backoff(**fn_args)
172        except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
173            self.module.fail_json_aws(e, msg="Couldn't list ECS services")
174        relevant_response = dict(services=response['serviceArns'])
175        return relevant_response
176
177    def describe_services(self, cluster, services):
178        fn_args = dict()
179        if cluster and cluster is not None:
180            fn_args['cluster'] = cluster
181        fn_args['services'] = services
182        try:
183            response = self.describe_services_with_backoff(**fn_args)
184        except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
185            self.module.fail_json_aws(e, msg="Couldn't describe ECS services")
186        running_services = [self.extract_service_from(service) for service in response.get('services', [])]
187        services_not_running = response.get('failures', [])
188        return running_services, services_not_running
189
190    def extract_service_from(self, service):
191        # some fields are datetime which is not JSON serializable
192        # make them strings
193        if 'deployments' in service:
194            for d in service['deployments']:
195                if 'createdAt' in d:
196                    d['createdAt'] = str(d['createdAt'])
197                if 'updatedAt' in d:
198                    d['updatedAt'] = str(d['updatedAt'])
199        if 'events' in service:
200            if not self.module.params['events']:
201                del service['events']
202            else:
203                for e in service['events']:
204                    if 'createdAt' in e:
205                        e['createdAt'] = str(e['createdAt'])
206        return service
207
208
209def chunks(l, n):
210    """Yield successive n-sized chunks from l."""
211    """ https://stackoverflow.com/a/312464 """
212    for i in range(0, len(l), n):
213        yield l[i:i + n]
214
215
216def main():
217
218    argument_spec = ec2_argument_spec()
219    argument_spec.update(dict(
220        details=dict(type='bool', default=False),
221        events=dict(type='bool', default=True),
222        cluster=dict(),
223        service=dict(type='list')
224    ))
225
226    module = AnsibleAWSModule(argument_spec=argument_spec, supports_check_mode=True)
227    is_old_facts = module._name == 'ecs_service_facts'
228    if is_old_facts:
229        module.deprecate("The 'ecs_service_facts' module has been renamed to 'ecs_service_info', "
230                         "and the renamed one no longer returns ansible_facts", version='2.13')
231
232    show_details = module.params.get('details')
233
234    task_mgr = EcsServiceManager(module)
235    if show_details:
236        if module.params['service']:
237            services = module.params['service']
238        else:
239            services = task_mgr.list_services(module.params['cluster'])['services']
240        ecs_info = dict(services=[], services_not_running=[])
241        for chunk in chunks(services, 10):
242            running_services, services_not_running = task_mgr.describe_services(module.params['cluster'], chunk)
243            ecs_info['services'].extend(running_services)
244            ecs_info['services_not_running'].extend(services_not_running)
245    else:
246        ecs_info = task_mgr.list_services(module.params['cluster'])
247
248    if is_old_facts:
249        module.exit_json(changed=False, ansible_facts=ecs_info, **ecs_info)
250    else:
251        module.exit_json(changed=False, **ecs_info)
252
253
254if __name__ == '__main__':
255    main()
256