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
8
9ANSIBLE_METADATA = {'metadata_version': '1.1',
10                    'status': ['preview'],
11                    'supported_by': 'community'}
12
13
14DOCUMENTATION = '''
15---
16module: ec2_asg_info
17short_description: Gather information about ec2 Auto Scaling Groups (ASGs) in AWS
18description:
19  - Gather information about ec2 Auto Scaling Groups (ASGs) in AWS
20  - This module was called C(ec2_asg_facts) before Ansible 2.9. The usage did not change.
21version_added: "2.2"
22requirements: [ boto3 ]
23author: "Rob White (@wimnat)"
24options:
25  name:
26    description:
27      - The prefix or name of the auto scaling group(s) you are searching for.
28      - "Note: This is a regular expression match with implicit '^' (beginning of string). Append '$' for a complete name match."
29    required: false
30  tags:
31    description:
32      - >
33        A dictionary/hash of tags in the format { tag1_name: 'tag1_value', tag2_name: 'tag2_value' } to match against the auto scaling
34        group(s) you are searching for.
35    required: false
36extends_documentation_fragment:
37    - aws
38    - ec2
39'''
40
41EXAMPLES = '''
42# Note: These examples do not set authentication details, see the AWS Guide for details.
43
44# Find all groups
45- ec2_asg_info:
46  register: asgs
47
48# Find a group with matching name/prefix
49- ec2_asg_info:
50    name: public-webserver-asg
51  register: asgs
52
53# Find a group with matching tags
54- ec2_asg_info:
55    tags:
56      project: webapp
57      env: production
58  register: asgs
59
60# Find a group with matching name/prefix and tags
61- ec2_asg_info:
62    name: myproject
63    tags:
64      env: production
65  register: asgs
66
67# Fail if no groups are found
68- ec2_asg_info:
69    name: public-webserver-asg
70  register: asgs
71  failed_when: "{{ asgs.results | length == 0 }}"
72
73# Fail if more than 1 group is found
74- ec2_asg_info:
75    name: public-webserver-asg
76  register: asgs
77  failed_when: "{{ asgs.results | length > 1 }}"
78'''
79
80RETURN = '''
81---
82auto_scaling_group_arn:
83    description: The Amazon Resource Name of the ASG
84    returned: success
85    type: str
86    sample: "arn:aws:autoscaling:us-west-2:1234567890:autoScalingGroup:10787c52-0bcb-427d-82ba-c8e4b008ed2e:autoScalingGroupName/public-webapp-production-1"
87auto_scaling_group_name:
88    description: Name of autoscaling group
89    returned: success
90    type: str
91    sample: "public-webapp-production-1"
92availability_zones:
93    description: List of Availability Zones that are enabled for this ASG.
94    returned: success
95    type: list
96    sample: ["us-west-2a", "us-west-2b", "us-west-2a"]
97created_time:
98    description: The date and time this ASG was created, in ISO 8601 format.
99    returned: success
100    type: str
101    sample: "2015-11-25T00:05:36.309Z"
102default_cooldown:
103    description: The default cooldown time in seconds.
104    returned: success
105    type: int
106    sample: 300
107desired_capacity:
108    description: The number of EC2 instances that should be running in this group.
109    returned: success
110    type: int
111    sample: 3
112health_check_period:
113    description: Length of time in seconds after a new EC2 instance comes into service that Auto Scaling starts checking its health.
114    returned: success
115    type: int
116    sample: 30
117health_check_type:
118    description: The service you want the health status from, one of "EC2" or "ELB".
119    returned: success
120    type: str
121    sample: "ELB"
122instances:
123    description: List of EC2 instances and their status as it relates to the ASG.
124    returned: success
125    type: list
126    sample: [
127        {
128            "availability_zone": "us-west-2a",
129            "health_status": "Healthy",
130            "instance_id": "i-es22ad25",
131            "launch_configuration_name": "public-webapp-production-1",
132            "lifecycle_state": "InService",
133            "protected_from_scale_in": "false"
134        }
135    ]
136launch_config_name:
137    description: >
138      Name of launch configuration associated with the ASG. Same as launch_configuration_name,
139      provided for compatibility with ec2_asg module.
140    returned: success
141    type: str
142    sample: "public-webapp-production-1"
143launch_configuration_name:
144    description: Name of launch configuration associated with the ASG.
145    returned: success
146    type: str
147    sample: "public-webapp-production-1"
148load_balancer_names:
149    description: List of load balancers names attached to the ASG.
150    returned: success
151    type: list
152    sample: ["elb-webapp-prod"]
153max_size:
154    description: Maximum size of group
155    returned: success
156    type: int
157    sample: 3
158min_size:
159    description: Minimum size of group
160    returned: success
161    type: int
162    sample: 1
163new_instances_protected_from_scale_in:
164    description: Whether or not new instances a protected from automatic scaling in.
165    returned: success
166    type: bool
167    sample: "false"
168placement_group:
169    description: Placement group into which instances are launched, if any.
170    returned: success
171    type: str
172    sample: None
173status:
174    description: The current state of the group when DeleteAutoScalingGroup is in progress.
175    returned: success
176    type: str
177    sample: None
178tags:
179    description: List of tags for the ASG, and whether or not each tag propagates to instances at launch.
180    returned: success
181    type: list
182    sample: [
183        {
184            "key": "Name",
185            "value": "public-webapp-production-1",
186            "resource_id": "public-webapp-production-1",
187            "resource_type": "auto-scaling-group",
188            "propagate_at_launch": "true"
189        },
190        {
191            "key": "env",
192            "value": "production",
193            "resource_id": "public-webapp-production-1",
194            "resource_type": "auto-scaling-group",
195            "propagate_at_launch": "true"
196        }
197    ]
198target_group_arns:
199    description: List of ARNs of the target groups that the ASG populates
200    returned: success
201    type: list
202    sample: [
203        "arn:aws:elasticloadbalancing:ap-southeast-2:123456789012:targetgroup/target-group-host-hello/1a2b3c4d5e6f1a2b",
204        "arn:aws:elasticloadbalancing:ap-southeast-2:123456789012:targetgroup/target-group-path-world/abcd1234abcd1234"
205    ]
206target_group_names:
207    description: List of names of the target groups that the ASG populates
208    returned: success
209    type: list
210    sample: [
211        "target-group-host-hello",
212        "target-group-path-world"
213    ]
214termination_policies:
215    description: A list of termination policies for the group.
216    returned: success
217    type: str
218    sample: ["Default"]
219'''
220
221import re
222
223try:
224    from botocore.exceptions import ClientError
225except ImportError:
226    pass  # caught by imported HAS_BOTO3
227
228from ansible.module_utils.basic import AnsibleModule
229from ansible.module_utils.ec2 import (get_aws_connection_info, boto3_conn, ec2_argument_spec,
230                                      camel_dict_to_snake_dict, HAS_BOTO3)
231
232
233def match_asg_tags(tags_to_match, asg):
234    for key, value in tags_to_match.items():
235        for tag in asg['Tags']:
236            if key == tag['Key'] and value == tag['Value']:
237                break
238        else:
239            return False
240    return True
241
242
243def find_asgs(conn, module, name=None, tags=None):
244    """
245    Args:
246        conn (boto3.AutoScaling.Client): Valid Boto3 ASG client.
247        name (str): Optional name of the ASG you are looking for.
248        tags (dict): Optional dictionary of tags and values to search for.
249
250    Basic Usage:
251        >>> name = 'public-webapp-production'
252        >>> tags = { 'env': 'production' }
253        >>> conn = boto3.client('autoscaling', region_name='us-west-2')
254        >>> results = find_asgs(name, conn)
255
256    Returns:
257        List
258        [
259            {
260                "auto_scaling_group_arn": (
261                    "arn:aws:autoscaling:us-west-2:275977225706:autoScalingGroup:58abc686-9783-4528-b338-3ad6f1cbbbaf:"
262                    "autoScalingGroupName/public-webapp-production"
263                ),
264                "auto_scaling_group_name": "public-webapp-production",
265                "availability_zones": ["us-west-2c", "us-west-2b", "us-west-2a"],
266                "created_time": "2016-02-02T23:28:42.481000+00:00",
267                "default_cooldown": 300,
268                "desired_capacity": 2,
269                "enabled_metrics": [],
270                "health_check_grace_period": 300,
271                "health_check_type": "ELB",
272                "instances":
273                [
274                    {
275                        "availability_zone": "us-west-2c",
276                        "health_status": "Healthy",
277                        "instance_id": "i-047a12cb",
278                        "launch_configuration_name": "public-webapp-production-1",
279                        "lifecycle_state": "InService",
280                        "protected_from_scale_in": false
281                    },
282                    {
283                        "availability_zone": "us-west-2a",
284                        "health_status": "Healthy",
285                        "instance_id": "i-7a29df2c",
286                        "launch_configuration_name": "public-webapp-production-1",
287                        "lifecycle_state": "InService",
288                        "protected_from_scale_in": false
289                    }
290                ],
291                "launch_config_name": "public-webapp-production-1",
292                "launch_configuration_name": "public-webapp-production-1",
293                "load_balancer_names": ["public-webapp-production-lb"],
294                "max_size": 4,
295                "min_size": 2,
296                "new_instances_protected_from_scale_in": false,
297                "placement_group": None,
298                "status": None,
299                "suspended_processes": [],
300                "tags":
301                [
302                    {
303                        "key": "Name",
304                        "propagate_at_launch": true,
305                        "resource_id": "public-webapp-production",
306                        "resource_type": "auto-scaling-group",
307                        "value": "public-webapp-production"
308                    },
309                    {
310                        "key": "env",
311                        "propagate_at_launch": true,
312                        "resource_id": "public-webapp-production",
313                        "resource_type": "auto-scaling-group",
314                        "value": "production"
315                    }
316                ],
317                "target_group_names": [],
318                "target_group_arns": [],
319                "termination_policies":
320                [
321                    "Default"
322                ],
323                "vpc_zone_identifier":
324                [
325                    "subnet-a1b1c1d1",
326                    "subnet-a2b2c2d2",
327                    "subnet-a3b3c3d3"
328                ]
329            }
330        ]
331    """
332
333    try:
334        asgs_paginator = conn.get_paginator('describe_auto_scaling_groups')
335        asgs = asgs_paginator.paginate().build_full_result()
336    except ClientError as e:
337        module.fail_json(msg=e.message, **camel_dict_to_snake_dict(e.response))
338
339    if not asgs:
340        return asgs
341    try:
342        region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True)
343        elbv2 = boto3_conn(module, conn_type='client', resource='elbv2', region=region, endpoint=ec2_url, **aws_connect_kwargs)
344    except ClientError as e:
345        # This is nice to have, not essential
346        elbv2 = None
347    matched_asgs = []
348
349    if name is not None:
350        # if the user didn't specify a name
351        name_prog = re.compile(r'^' + name)
352
353    for asg in asgs['AutoScalingGroups']:
354        if name:
355            matched_name = name_prog.search(asg['AutoScalingGroupName'])
356        else:
357            matched_name = True
358
359        if tags:
360            matched_tags = match_asg_tags(tags, asg)
361        else:
362            matched_tags = True
363
364        if matched_name and matched_tags:
365            asg = camel_dict_to_snake_dict(asg)
366            # compatibility with ec2_asg module
367            if 'launch_configuration_name' in asg:
368                asg['launch_config_name'] = asg['launch_configuration_name']
369            # workaround for https://github.com/ansible/ansible/pull/25015
370            if 'target_group_ar_ns' in asg:
371                asg['target_group_arns'] = asg['target_group_ar_ns']
372                del(asg['target_group_ar_ns'])
373            if asg.get('target_group_arns'):
374                if elbv2:
375                    try:
376                        tg_paginator = elbv2.get_paginator('describe_target_groups')
377                        tg_result = tg_paginator.paginate(TargetGroupArns=asg['target_group_arns']).build_full_result()
378                        asg['target_group_names'] = [tg['TargetGroupName'] for tg in tg_result['TargetGroups']]
379                    except ClientError as e:
380                        if e.response['Error']['Code'] == 'TargetGroupNotFound':
381                            asg['target_group_names'] = []
382            else:
383                asg['target_group_names'] = []
384            matched_asgs.append(asg)
385
386    return matched_asgs
387
388
389def main():
390
391    argument_spec = ec2_argument_spec()
392    argument_spec.update(
393        dict(
394            name=dict(type='str'),
395            tags=dict(type='dict'),
396        )
397    )
398    module = AnsibleModule(argument_spec=argument_spec)
399    if module._name == 'ec2_asg_facts':
400        module.deprecate("The 'ec2_asg_facts' module has been renamed to 'ec2_asg_info'", version='2.13')
401
402    if not HAS_BOTO3:
403        module.fail_json(msg='boto3 required for this module')
404
405    asg_name = module.params.get('name')
406    asg_tags = module.params.get('tags')
407
408    try:
409        region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True)
410        autoscaling = boto3_conn(module, conn_type='client', resource='autoscaling', region=region, endpoint=ec2_url, **aws_connect_kwargs)
411    except ClientError as e:
412        module.fail_json(msg=e.message, **camel_dict_to_snake_dict(e.response))
413
414    results = find_asgs(autoscaling, module, name=asg_name, tags=asg_tags)
415    module.exit_json(results=results)
416
417
418if __name__ == '__main__':
419    main()
420