Improve details and events results for ecs_service_facts (#37983)

* Use AnsibleAWSModule to simplify AWS connection
* Add Exception handling, pagination, retries and backoff
* Allow events to be switched off
* Allow details to be obtained without having to specify services
This commit is contained in:
Will Thames 2018-04-03 01:26:23 +10:00 committed by Sloane Hertel
parent 50761bef0a
commit 423b0e0f58
2 changed files with 108 additions and 48 deletions

View file

@ -14,8 +14,6 @@ DOCUMENTATION = '''
--- ---
module: ecs_service_facts module: ecs_service_facts
short_description: list or describe services in ecs short_description: list or describe services in ecs
notes:
- for details of the parameters and returns see U(http://boto3.readthedocs.org/en/latest/reference/services/ecs.html)
description: description:
- Lists or describes services in ecs. - Lists or describes services in ecs.
version_added: "2.1" version_added: "2.1"
@ -30,6 +28,13 @@ options:
required: false required: false
default: 'false' default: 'false'
choices: ['true', 'false'] choices: ['true', 'false']
events:
description:
- Whether to return ECS service events. Only has an effect if C(details) is true.
required: false
default: 'true'
choices: ['true', 'false']
version_added: "2.6"
cluster: cluster:
description: description:
- The cluster ARNS in which to list the services. - The cluster ARNS in which to list the services.
@ -37,7 +42,7 @@ options:
default: 'default' default: 'default'
service: service:
description: description:
- The service to get details for (required if details is true) - One or more services to get details for
required: false required: false
extends_documentation_fragment: extends_documentation_fragment:
- aws - aws
@ -118,19 +123,18 @@ services:
returned: always returned: always
type: list of complex type: list of complex
events: events:
description: lost of service events description: list of service events
returned: always returned: when events is true
type: list of complex type: list of complex
''' # NOQA ''' # NOQA
try: try:
import botocore import botocore
HAS_BOTO3 = True
except ImportError: except ImportError:
HAS_BOTO3 = False pass # handled by AnsibleAWSModule
from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.aws.core import AnsibleAWSModule
from ansible.module_utils.ec2 import boto3_conn, ec2_argument_spec, get_aws_connection_info from ansible.module_utils.ec2 import ec2_argument_spec, AWSRetry
class EcsServiceManager: class EcsServiceManager:
@ -138,26 +142,31 @@ class EcsServiceManager:
def __init__(self, module): def __init__(self, module):
self.module = module self.module = module
self.ecs = module.client('ecs')
# self.ecs = boto3.client('ecs') @AWSRetry.backoff(tries=5, delay=5, backoff=2.0)
region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) def list_services_with_backoff(self, **kwargs):
self.ecs = boto3_conn(module, conn_type='client', resource='ecs', region=region, endpoint=ec2_url, **aws_connect_kwargs) paginator = self.ecs.get_paginator('list_services')
try:
return paginator.paginate(**kwargs).build_full_result()
except botocore.exceptions.ClientError as e:
if e.response['Error']['Code'] == 'ClusterNotFoundException':
self.module.fail_json_aws(e, "Could not find cluster to list services")
else:
raise
# def list_clusters(self): @AWSRetry.backoff(tries=5, delay=5, backoff=2.0)
# return self.client.list_clusters() def describe_services_with_backoff(self, **kwargs):
# {'failures': [], return self.ecs.describe_services(**kwargs)
# 'ResponseMetadata': {'HTTPStatusCode': 200, 'RequestId': 'ce7b5880-1c41-11e5-8a31-47a93a8a98eb'},
# 'clusters': [{'activeServicesCount': 0, 'clusterArn': 'arn:aws:ecs:us-west-2:777110527155:cluster/default',
# 'status': 'ACTIVE', 'pendingTasksCount': 0, 'runningTasksCount': 0, 'registeredContainerInstancesCount': 0, 'clusterName': 'default'}]}
# {'failures': [{'arn': 'arn:aws:ecs:us-west-2:777110527155:cluster/bogus', 'reason': 'MISSING'}],
# 'ResponseMetadata': {'HTTPStatusCode': 200, 'RequestId': '0f66c219-1c42-11e5-8a31-47a93a8a98eb'},
# 'clusters': []}
def list_services(self, cluster): def list_services(self, cluster):
fn_args = dict() fn_args = dict()
if cluster and cluster is not None: if cluster and cluster is not None:
fn_args['cluster'] = cluster fn_args['cluster'] = cluster
response = self.ecs.list_services(**fn_args) try:
response = self.list_services_with_backoff(**fn_args)
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
self.module.fail_json_aws(e, msg="Couldn't list ECS services")
relevant_response = dict(services=response['serviceArns']) relevant_response = dict(services=response['serviceArns'])
return relevant_response return relevant_response
@ -165,14 +174,14 @@ class EcsServiceManager:
fn_args = dict() fn_args = dict()
if cluster and cluster is not None: if cluster and cluster is not None:
fn_args['cluster'] = cluster fn_args['cluster'] = cluster
fn_args['services'] = services.split(",") fn_args['services'] = services
response = self.ecs.describe_services(**fn_args) try:
relevant_response = {'services': []} response = self.describe_services_with_backoff(**fn_args)
for service in response.get('services', []): except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
relevant_response['services'].append(self.extract_service_from(service)) self.module.fail_json_aws(e, msg="Couldn't describe ECS services")
if 'failures' in response and len(response['failures']) > 0: running_services = [self.extract_service_from(service) for service in response.get('services', [])]
relevant_response['services_not_running'] = response['failures'] services_not_running = response.get('failures', [])
return relevant_response return running_services, services_not_running
def extract_service_from(self, service): def extract_service_from(self, service):
# some fields are datetime which is not JSON serializable # some fields are datetime which is not JSON serializable
@ -184,38 +193,51 @@ class EcsServiceManager:
if 'updatedAt' in d: if 'updatedAt' in d:
d['updatedAt'] = str(d['updatedAt']) d['updatedAt'] = str(d['updatedAt'])
if 'events' in service: if 'events' in service:
for e in service['events']: if not self.module.params['events']:
if 'createdAt' in e: del service['events']
e['createdAt'] = str(e['createdAt']) else:
for e in service['events']:
if 'createdAt' in e:
e['createdAt'] = str(e['createdAt'])
return service return service
def chunks(l, n):
"""Yield successive n-sized chunks from l."""
""" https://stackoverflow.com/a/312464 """
for i in range(0, len(l), n):
yield l[i:i + n]
def main(): def main():
argument_spec = ec2_argument_spec() argument_spec = ec2_argument_spec()
argument_spec.update(dict( argument_spec.update(dict(
details=dict(required=False, type='bool', default=False), details=dict(type='bool', default=False),
cluster=dict(required=False, type='str'), events=dict(type='bool', default=True),
service=dict(required=False, type='str') cluster=dict(),
service=dict(type='list')
)) ))
module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) module = AnsibleAWSModule(argument_spec=argument_spec, supports_check_mode=True)
if not HAS_BOTO3: show_details = module.params.get('details')
module.fail_json(msg='boto3 is required.')
show_details = module.params.get('details', False)
task_mgr = EcsServiceManager(module) task_mgr = EcsServiceManager(module)
if show_details: if show_details:
if 'service' not in module.params or not module.params['service']: if module.params['service']:
module.fail_json(msg="service must be specified for ecs_service_facts") services = module.params['service']
ecs_facts = task_mgr.describe_services(module.params['cluster'], module.params['service']) else:
services = task_mgr.list_services(module.params['cluster'])['services']
ecs_facts = dict(services=[], services_not_running=[])
for chunk in chunks(services, 10):
running_services, services_not_running = task_mgr.describe_services(module.params['cluster'], chunk)
ecs_facts['services'].extend(running_services)
ecs_facts['services_not_running'].extend(services_not_running)
else: else:
ecs_facts = task_mgr.list_services(module.params['cluster']) ecs_facts = task_mgr.list_services(module.params['cluster'])
ecs_facts_result = dict(changed=False, ansible_facts=ecs_facts) module.exit_json(changed=False, ansible_facts=ecs_facts, **ecs_facts)
module.exit_json(**ecs_facts_result)
if __name__ == '__main__': if __name__ == '__main__':

View file

@ -253,11 +253,49 @@
# FIXME: fixed in #32876 # FIXME: fixed in #32876
ignore_errors: yes ignore_errors: yes
- name: obtain ECS service facts - name: obtain facts for all ECS services in the cluster
ecs_service_facts: ecs_service_facts:
service: "{{ ecs_service_name }}"
cluster: "{{ ecs_cluster_name }}" cluster: "{{ ecs_cluster_name }}"
details: yes
events: no
<<: *aws_connection_info <<: *aws_connection_info
register: ecs_service_facts
- name: assert that facts are useful
assert:
that:
- "'services' in ecs_service_facts"
- ecs_service_facts.services | length > 0
- "'events' not in ecs_service_facts.services[0]"
- name: obtain facts for existing service in the cluster
ecs_service_facts:
cluster: "{{ ecs_cluster_name }}"
service: "{{ ecs_service_name }}"
details: yes
events: no
<<: *aws_connection_info
register: ecs_service_facts
- name: assert that existing service is available and running
assert:
that:
- "ecs_service_facts.services|length == 1"
- "ecs_service_facts.services_not_running|length == 0"
- name: obtain facts for non-existent service in the cluster
ecs_service_facts:
cluster: "{{ ecs_cluster_name }}"
service: madeup
details: yes
events: no
<<: *aws_connection_info
register: ecs_service_facts
- name: assert that non-existent service is missing
assert:
that:
- "ecs_service_facts.services_not_running[0].reason == 'MISSING'"
- name: attempt to get facts from missing task definition - name: attempt to get facts from missing task definition
ecs_taskdefinition_facts: ecs_taskdefinition_facts: