ansible-later/env_27/lib/python2.7/site-packages/ansible/modules/cloud/amazon/elb_target_facts.py
2019-04-11 13:00:36 +02:00

435 lines
16 KiB
Python

#!/usr/bin/python
# Copyright: (c) 2018, Yaakov Kuperman <ykuperman@gmail.com>
# GNU General Public License v3.0+ # (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
ANSIBLE_METADATA = {"metadata_version": "1.1",
"status": ["preview"],
"supported_by": "community"}
DOCUMENTATION = """
---
module: elb_target_facts
short_description: Gathers which target groups a target is associated with.
description:
- This module will search through every target group in a region to find
which ones have registered a given instance ID or IP.
version_added: "2.7"
author: "Yaakov Kuperman (@yaakov-github)"
options:
instance_id:
description:
- What instance ID to get facts for.
type: str
required: true
get_unused_target_groups:
description:
- Whether or not to get target groups not used by any load balancers.
type: bool
default: true
requirements:
- boto3
- botocore
extends_documentation_fragment:
- aws
- ec2
"""
EXAMPLES = """
# practical use case - dynamically deregistering and reregistering nodes
- name: Get EC2 Metadata
action: ec2_metadata_facts
- name: Get initial list of target groups
delegate_to: localhost
elb_target_facts:
instance_id: "{{ ansible_ec2_instance_id }}"
region: "{{ ansible_ec2_placement_region }}"
register: target_facts
- name: save fact for later
set_fact:
original_tgs: "{{ target_facts.instance_target_groups }}"
- name: Deregister instance from all target groups
delegate_to: localhost
elb_target:
target_group_arn: "{{ item.0.target_group_arn }}"
target_port: "{{ item.1.target_port }}"
target_az: "{{ item.1.target_az }}"
target_id: "{{ item.1.target_id }}"
state: absent
target_status: "draining"
region: "{{ ansible_ec2_placement_region }}"
with_subelements:
- "{{ original_tgs }}"
- "targets"
# This avoids having to wait for 'elb_target' to serially deregister each
# target group. An alternative would be to run all of the 'elb_target'
# tasks async and wait for them to finish.
- name: wait for all targets to deregister simultaneously
delegate_to: localhost
elb_target_facts:
get_unused_target_groups: false
instance_id: "{{ ansible_ec2_instance_id }}"
region: "{{ ansible_ec2_placement_region }}"
register: target_facts
until: (target_facts.instance_target_groups | length) == 0
retries: 60
delay: 10
- name: reregister in elbv2s
elb_target:
region: "{{ ansible_ec2_placement_region }}"
target_group_arn: "{{ item.0.target_group_arn }}"
target_port: "{{ item.1.target_port }}"
target_az: "{{ item.1.target_az }}"
target_id: "{{ item.1.target_id }}"
state: present
target_status: "initial"
with_subelements:
- "{{ original_tgs }}"
- "targets"
# wait until all groups associated with this instance are 'healthy' or
# 'unused'
- name: wait for registration
elb_target_facts:
get_unused_target_groups: false
instance_id: "{{ ansible_ec2_instance_id }}"
region: "{{ ansible_ec2_placement_region }}"
register: target_facts
until: (target_facts.instance_target_groups |
map(attribute='targets') |
flatten |
map(attribute='target_health') |
rejectattr('state', 'equalto', 'healthy') |
rejectattr('state', 'equalto', 'unused') |
list |
length) == 0
retries: 61
delay: 10
# using the target groups to generate AWS CLI commands to reregister the
# instance - useful in case the playbook fails mid-run and manual
# rollback is required
- name: "reregistration commands: ELBv2s"
debug:
msg: >
aws --region {{ansible_ec2_placement_region}} elbv2
register-targets --target-group-arn {{item.target_group_arn}}
--targets{%for target in item.targets%}
Id={{target.target_id}},
Port={{target.target_port}}{%if target.target_az%},AvailabilityZone={{target.target_az}}
{%endif%}
{%endfor%}
with_items: "{{target_facts.instance_target_groups}}"
"""
RETURN = """
instance_target_groups:
description: a list of target groups to which the instance is registered to
returned: always
type: complex
contains:
target_group_arn:
description: The ARN of the target group
type: string
returned: always
sample:
- "arn:aws:elasticloadbalancing:eu-west-1:111111111111:targetgroup/target-group/deadbeefdeadbeef"
target_group_type:
description: Which target type is used for this group
returned: always
type: string
sample:
- ip
- instance
targets:
description: A list of targets that point to this instance ID
returned: always
type: complex
contains:
target_id:
description: the target ID referiing to this instance
type: str
returned: always
sample:
- i-deadbeef
- 1.2.3.4
target_port:
description: which port this target is listening on
type: str
returned: always
sample:
- 80
target_az:
description: which availability zone is explicitly
associated with this target
type: str
returned: when an AZ is associated with this instance
sample:
- us-west-2a
target_health:
description: the target health description
(see U(https://boto3.readthedocs.io/en/latest/
reference/services/elbv2.html#ElasticLoadBalancingv2.Client.describe_target_health))
for all possible values
returned: always
type: complex
contains:
description:
description: description of target health
returned: if I(state!=present)
sample:
- "Target desregistration is in progress"
reason:
description: reason code for target health
returned: if I(state!=healthy)
sample:
- "Target.Deregistration in progress"
state:
description: health state
returned: always
sample:
- "healthy"
- "draining"
- "initial"
- "unhealthy"
- "unused"
- "unavailable"
"""
__metaclass__ = type
try:
from botocore.exceptions import ClientError, BotoCoreError
except ImportError:
# we can handle the lack of boto3 based on the ec2 module
pass
from ansible.module_utils.aws.core import AnsibleAWSModule
from ansible.module_utils.ec2 import (HAS_BOTO3, camel_dict_to_snake_dict,
AWSRetry)
class Target(object):
"""Models a target in a target group"""
def __init__(self, target_id, port, az, raw_target_health):
self.target_port = port
self.target_id = target_id
self.target_az = az
self.target_health = self.convert_target_health(raw_target_health)
def convert_target_health(self, raw_target_health):
return camel_dict_to_snake_dict(raw_target_health)
class TargetGroup(object):
"""Models an elbv2 target group"""
def __init__(self, **kwargs):
self.target_group_type = kwargs["target_group_type"]
self.target_group_arn = kwargs["target_group_arn"]
# the relevant targets associated with this group
self.targets = []
def add_target(self, target_id, target_port, target_az, raw_target_health):
self.targets.append(Target(target_id,
target_port,
target_az,
raw_target_health))
def to_dict(self):
object_dict = vars(self)
object_dict["targets"] = [vars(each) for each in self.get_targets()]
return object_dict
def get_targets(self):
return list(self.targets)
class TargetFactsGatherer(object):
def __init__(self, module, instance_id, get_unused_target_groups):
self.module = module
try:
self.ec2 = self.module.client(
"ec2",
retry_decorator=AWSRetry.jittered_backoff(retries=10)
)
except (ClientError, BotoCoreError) as e:
self.module.fail_json_aws(e,
msg="Couldn't connect to ec2"
)
try:
self.elbv2 = self.module.client(
"elbv2",
retry_decorator=AWSRetry.jittered_backoff(retries=10)
)
except (BotoCoreError, ClientError) as e:
self.module.fail_json_aws(e,
msg="Could not connect to elbv2"
)
self.instance_id = instance_id
self.get_unused_target_groups = get_unused_target_groups
self.tgs = self._get_target_groups()
def _get_instance_ips(self):
"""Fetch all IPs associated with this instance so that we can determine
whether or not an instance is in an IP-based target group"""
try:
# get ahold of the instance in the API
reservations = self.ec2.describe_instances(
InstanceIds=[self.instance_id],
aws_retry=True
)["Reservations"]
except (BotoCoreError, ClientError) as e:
# typically this will happen if the instance doesn't exist
self.module.fail_json_aws(e,
msg="Could not get instance info" +
" for instance '%s'" %
(self.instance_id)
)
if len(reservations) < 1:
self.module.fail_json(
msg="Instance ID %s could not be found" % self.instance_id
)
instance = reservations[0]["Instances"][0]
# IPs are represented in a few places in the API, this should
# account for all of them
ips = set()
ips.add(instance["PrivateIpAddress"])
for nic in instance["NetworkInterfaces"]:
ips.add(nic["PrivateIpAddress"])
for ip in nic["PrivateIpAddresses"]:
ips.add(ip["PrivateIpAddress"])
return list(ips)
def _get_target_group_objects(self):
"""helper function to build a list of TargetGroup objects based on
the AWS API"""
try:
paginator = self.elbv2.get_paginator(
"describe_target_groups"
)
tg_response = paginator.paginate().build_full_result()
except (BotoCoreError, ClientError) as e:
self.module.fail_json_aws(e,
msg="Could not describe target" +
" groups"
)
# build list of TargetGroup objects representing every target group in
# the system
target_groups = []
for each_tg in tg_response["TargetGroups"]:
if not self.get_unused_target_groups and \
len(each_tg["LoadBalancerArns"]) < 1:
# only collect target groups that actually are connected
# to LBs
continue
target_groups.append(
TargetGroup(target_group_arn=each_tg["TargetGroupArn"],
target_group_type=each_tg["TargetType"],
)
)
return target_groups
def _get_target_descriptions(self, target_groups):
"""Helper function to build a list of all the target descriptions
for this target in a target group"""
# Build a list of all the target groups pointing to this instance
# based on the previous list
tgs = set()
# Loop through all the target groups
for tg in target_groups:
try:
# Get the list of targets for that target group
response = self.elbv2.describe_target_health(
TargetGroupArn=tg.target_group_arn,
aws_retry=True
)
except (BotoCoreError, ClientError) as e:
self.module.fail_json_aws(e,
msg="Could not describe target " +
"health for target group %s" %
tg.target_group_arn
)
for t in response["TargetHealthDescriptions"]:
# If the target group has this instance as a target, add to
# list. This logic also accounts for the possibility of a
# target being in the target group multiple times with
# overridden ports
if t["Target"]["Id"] == self.instance_id or \
t["Target"]["Id"] in self.instance_ips:
# The 'AvailabilityZone' parameter is a weird one, see the
# API docs for more. Basically it's only supposed to be
# there under very specific circumstances, so we need
# to account for that
az = t["Target"]["AvailabilityZone"] \
if "AvailabilityZone" in t["Target"] \
else None
tg.add_target(t["Target"]["Id"],
t["Target"]["Port"],
az,
t["TargetHealth"])
# since tgs is a set, each target group will be added only
# once, even though we call add on each successful match
tgs.add(tg)
return list(tgs)
def _get_target_groups(self):
# do this first since we need the IPs later on in this function
self.instance_ips = self._get_instance_ips()
# build list of target groups
target_groups = self._get_target_group_objects()
return self._get_target_descriptions(target_groups)
def main():
argument_spec = dict(
instance_id={"required": True, "type": "str"},
get_unused_target_groups={"required": False,
"default": True, "type": "bool"}
)
module = AnsibleAWSModule(
argument_spec=argument_spec,
supports_check_mode=True,
)
instance_id = module.params["instance_id"]
get_unused_target_groups = module.params["get_unused_target_groups"]
tg_gatherer = TargetFactsGatherer(module,
instance_id,
get_unused_target_groups
)
instance_target_groups = [each.to_dict() for each in tg_gatherer.tgs]
module.exit_json(instance_target_groups=instance_target_groups)
if __name__ == "__main__":
main()