[cloud] New AWS module - elb_target (#26483)

* New module - elb_target

* Work to add deregister functionality

* Add jurajseffer work on waiting for status change

* List the set of statuses as a choices list

* default target_status_timeout is 60

* Add 'unavailable' as a target status choice per API docs

* Add support to remove targets that are in unused state

* add support for availability zone selection

Fix deregistering targets from used target groups

Fix waiting for unused state for targets that have started deregistering
This commit is contained in:
Rob 2018-02-07 23:45:35 +11:00 committed by Ryan Brown
parent fcd09e2f02
commit 9451212855
4 changed files with 803 additions and 0 deletions

View file

@ -0,0 +1,316 @@
#!/usr/bin/python
# Copyright (c) 2017 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
ANSIBLE_METADATA = {'status': ['preview'],
'supported_by': 'community',
'metadata_version': '1.1'}
DOCUMENTATION = '''
---
module: elb_target
short_description: Manage a target in a target group
description:
- Used to register or deregister a target in a target group
version_added: "2.5"
author: "Rob White (@wimnat)"
options:
deregister_unused:
description:
- The default behaviour for targets that are unused is to leave them registered. If instead you would like to remove them
set I(deregister_unused) to yes.
choices: [ 'yes', 'no' ]
target_az:
description:
- An Availability Zone or all. This determines whether the target receives traffic from the load balancer nodes in the specified
Availability Zone or from all enabled Availability Zones for the load balancer. This parameter is not supported if the target
type of the target group is instance.
target_group_arn:
description:
- The Amazon Resource Name (ARN) of the target group. Mutually exclusive of I(target_group_name).
target_group_name:
description:
- The name of the target group. Mutually exclusive of I(target_group_arn).
target_id:
description:
- The ID of the target.
required: true
target_port:
description:
- The port on which the target is listening. You can specify a port override. If a target is already registered,
you can register it again using a different port.
required: false
default: The default port for a target is the port for the target group.
target_status:
description:
- Blocks and waits for the target status to equal given value. For more detail on target status see
U(http://docs.aws.amazon.com/elasticloadbalancing/latest/application/target-group-health-checks.html#target-health-states)
required: false
choices: [ 'initial', 'healthy', 'unhealthy', 'unused', 'draining', 'unavailable' ]
target_status_timeout:
description:
- Maximum time in seconds to wait for target_status change
required: false
default: 60
state:
description:
- Register or deregister the target.
required: true
choices: [ 'present', 'absent' ]
extends_documentation_fragment:
- aws
- ec2
notes:
- If you specified a port override when you registered a target, you must specify both the target ID and the port when you deregister it.
'''
EXAMPLES = '''
# Note: These examples do not set authentication details, see the AWS Guide for details.
# Register an IP address target to a target group
- elb_target:
target_group_name: myiptargetgroup
target_id: 10.0.0.10
state: present
# Register an instance target to a target group
- elb_target:
target_group_name: mytargetgroup
target_id: i-1234567
state: present
# Deregister a target from a target group
- elb_target:
target_group_name: mytargetgroup
target_id: i-1234567
state: absent
# Modify a target to use a different port
# Register a target to a target group
- elb_target:
target_group_name: mytargetgroup
target_id: i-1234567
target_port: 8080
state: present
'''
RETURN = '''
'''
import traceback
from time import time, sleep
from ansible.module_utils._text import to_native
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.ec2 import (boto3_conn, camel_dict_to_snake_dict,
ec2_argument_spec, get_aws_connection_info)
try:
import boto3
from botocore.exceptions import ClientError, BotoCoreError
HAS_BOTO3 = True
except ImportError:
HAS_BOTO3 = False
def convert_tg_name_to_arn(connection, module, tg_name):
try:
response = connection.describe_target_groups(Names=[tg_name])
except ClientError as e:
module.fail_json(msg="Unable to describe target group {0}: {1}".format(tg_name, to_native(e)),
exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response))
except BotoCoreError as e:
module.fail_json(msg="Unable to describe target group {0}: {1}".format(tg_name, to_native(e)),
exception=traceback.format_exc())
tg_arn = response['TargetGroups'][0]['TargetGroupArn']
return tg_arn
def describe_targets(connection, module, tg_arn, target):
"""
Describe targets in a target group
:param module: ansible module object
:param connection: boto3 connection
:param tg_arn: target group arn
:param target: dictionary containing target id and port
:return:
"""
try:
targets = connection.describe_target_health(TargetGroupArn=tg_arn, Targets=target)['TargetHealthDescriptions']
if not targets:
return {}
return targets[0]
except ClientError as e:
module.fail_json(msg="Unable to describe target health for target {0}: {1}".format(target, to_native(e)),
exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response))
except BotoCoreError as e:
module.fail_json(msg="Unable to describe target health for target {0}: {1}".format(target, to_native(e)),
exception=traceback.format_exc())
def register_target(connection, module):
"""
Registers a target to a target group
:param module: ansible module object
:param connection: boto3 connection
:return:
"""
target_az = module.params.get("target_az")
target_group_arn = module.params.get("target_group_arn")
target_id = module.params.get("target_id")
target_port = module.params.get("target_port")
target_status = module.params.get("target_status")
target_status_timeout = module.params.get("target_status_timeout")
changed = False
if not target_group_arn:
target_group_arn = convert_tg_name_to_arn(connection, module, module.params.get("target_group_name"))
target = dict(Id=target_id)
if target_az:
target['AvailabilityZone'] = target_az
if target_port:
target['Port'] = target_port
target_description = describe_targets(connection, module, target_group_arn, [target])
if 'Reason' in target_description['TargetHealth']:
if target_description['TargetHealth']['Reason'] == "Target.NotRegistered":
try:
connection.register_targets(TargetGroupArn=target_group_arn, Targets=[target])
changed = True
if target_status:
target_status_check(connection, module, target_group_arn, target, target_status, target_status_timeout)
except ClientError as e:
module.fail_json(msg="Unable to deregister target {0}: {1}".format(target, to_native(e)),
exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response))
except BotoCoreError as e:
module.fail_json(msg="Unable to deregister target {0}: {1}".format(target, to_native(e)),
exception=traceback.format_exc())
# Get all targets for the target group
target_descriptions = describe_targets(connection, module, target_group_arn, [])
module.exit_json(changed=changed, target_health_descriptions=camel_dict_to_snake_dict(target_descriptions), target_group_arn=target_group_arn)
def deregister_target(connection, module):
"""
Deregisters a target to a target group
:param module: ansible module object
:param connection: boto3 connection
:return:
"""
deregister_unused = module.params.get("deregister_unused")
target_group_arn = module.params.get("target_group_arn")
target_id = module.params.get("target_id")
target_port = module.params.get("target_port")
target_status = module.params.get("target_status")
target_status_timeout = module.params.get("target_status_timeout")
changed = False
if not target_group_arn:
target_group_arn = convert_tg_name_to_arn(connection, module, module.params.get("target_group_name"))
target = dict(Id=target_id)
if target_port:
target['Port'] = target_port
target_description = describe_targets(connection, module, target_group_arn, [target])
current_target_state = target_description['TargetHealth']['State']
current_target_reason = target_description['TargetHealth'].get('Reason')
needs_deregister = False
if deregister_unused and current_target_state == 'unused':
if current_target_reason != 'Target.NotRegistered':
needs_deregister = True
elif current_target_state not in ['unused', 'draining']:
needs_deregister = True
if needs_deregister:
try:
connection.deregister_targets(TargetGroupArn=target_group_arn, Targets=[target])
changed = True
except ClientError as e:
module.fail_json(msg="Unable to deregister target {0}: {1}".format(target, to_native(e)),
exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response))
except BotoCoreError as e:
module.fail_json(msg="Unable to deregister target {0}: {1}".format(target, to_native(e)),
exception=traceback.format_exc())
else:
if current_target_reason != 'Target.NotRegistered' and current_target_state != 'draining':
module.warn(warning="Your specified target has an 'unused' state but is still registered to the target group. " +
"To force deregistration use the 'deregister_unused' option.")
if target_status:
target_status_check(connection, module, target_group_arn, target, target_status, target_status_timeout)
# Get all targets for the target group
target_descriptions = describe_targets(connection, module, target_group_arn, [])
module.exit_json(changed=changed, target_health_descriptions=camel_dict_to_snake_dict(target_descriptions), target_group_arn=target_group_arn)
def target_status_check(connection, module, target_group_arn, target, target_status, target_status_timeout):
reached_state = False
timeout = target_status_timeout + time()
while time() < timeout:
health_state = describe_targets(connection, module, target_group_arn, [target])['TargetHealth']['State']
if health_state == target_status:
reached_state = True
break
sleep(1)
if not reached_state:
module.fail_json(msg='Status check timeout of {0} exceeded, last status was {1}: '.format(target_status_timeout, health_state))
def main():
argument_spec = ec2_argument_spec()
argument_spec.update(
dict(
deregister_unused=dict(type='bool', default=False),
target_az=dict(type='str'),
target_group_arn=dict(type='str'),
target_group_name=dict(type='str'),
target_id=dict(type='str', required=True),
target_port=dict(type='int'),
target_status=dict(choices=['initial', 'healthy', 'unhealthy', 'unused', 'draining', 'unavailable'], type='str'),
target_status_timeout=dict(type='int', default=60),
state=dict(required=True, choices=['present', 'absent'], type='str'),
)
)
module = AnsibleModule(argument_spec=argument_spec,
mutually_exclusive=['target_group_arn', 'target_group_name']
)
if not HAS_BOTO3:
module.fail_json(msg='boto3 required for this module')
region, ec2_url, aws_connect_params = get_aws_connection_info(module, boto3=True)
connection = boto3_conn(module, conn_type='client', resource='elbv2', region=region, endpoint=ec2_url, **aws_connect_params)
state = module.params.get("state")
if state == 'present':
register_target(connection, module)
else:
deregister_target(connection, module)
if __name__ == '__main__':
main()

View file

@ -0,0 +1 @@
cloud/aws

View file

@ -0,0 +1,4 @@
---
ec2_ami_image:
us-east-1: ami-8c1be5f6
us-east-2: ami-c5062ba0

View file

@ -0,0 +1,482 @@
---
- name: set up elb_target test prerequisites
block:
- name:
debug: msg="********** Setting up elb_target test dependencies **********"
# ============================================================
- name: set up aws connection info
set_fact:
aws_connection_info: &aws_connection_info
aws_access_key: "{{ aws_access_key }}"
aws_secret_key: "{{ aws_secret_key }}"
security_token: "{{ security_token }}"
region: "{{ aws_region }}"
no_log: yes
# ============================================================
- name: create target group name
set_fact:
tg_name: "ansible-test-{{ resource_prefix | regex_search('([0-9]+)$') }}-tg"
- name: create application load balancer name
set_fact:
lb_name: "ansible-test-{{ resource_prefix | regex_search('([0-9]+)$') }}-lb"
# ============================================================
- name: set up testing VPC
ec2_vpc_net:
name: "{{ resource_prefix }}-vpc"
state: present
cidr_block: 20.0.0.0/16
<<: *aws_connection_info
tags:
Name: "{{ resource_prefix }}-vpc"
Description: "Created by ansible-test"
register: vpc
- name: set up testing internet gateway
ec2_vpc_igw:
vpc_id: "{{ vpc.vpc.id }}"
state: present
<<: *aws_connection_info
register: igw
- name: set up testing subnet
ec2_vpc_subnet:
state: present
vpc_id: "{{ vpc.vpc.id }}"
cidr: 20.0.0.0/18
az: "{{ aws_region }}a"
resource_tags:
Name: "{{ resource_prefix }}-subnet"
<<: *aws_connection_info
register: subnet_1
- name: set up testing subnet
ec2_vpc_subnet:
state: present
vpc_id: "{{ vpc.vpc.id }}"
cidr: 20.0.64.0/18
az: "{{ aws_region }}b"
resource_tags:
Name: "{{ resource_prefix }}-subnet"
<<: *aws_connection_info
register: subnet_2
- name: create routing rules
ec2_vpc_route_table:
vpc_id: "{{ vpc.vpc.id }}"
tags:
created: "{{ resource_prefix }}-route"
routes:
- dest: 0.0.0.0/0
gateway_id: "{{ igw.gateway_id }}"
subnets:
- "{{ subnet_1.subnet.id }}"
- "{{ subnet_2.subnet.id }}"
<<: *aws_connection_info
register: route_table
- name: create testing security group
ec2_group:
name: "{{ resource_prefix }}-sg"
description: a security group for ansible tests
vpc_id: "{{ vpc.vpc.id }}"
rules:
- proto: tcp
from_port: 80
to_port: 80
cidr_ip: 0.0.0.0/0
- proto: tcp
from_port: 22
to_port: 22
cidr_ip: 0.0.0.0/0
<<: *aws_connection_info
register: sg
- name: set up testing target group (type=instance)
elb_target_group:
name: "{{ tg_name }}"
health_check_port: 80
protocol: http
port: 80
vpc_id: '{{ vpc.vpc.id }}'
state: present
target_type: instance
tags:
Description: "Created by {{ resource_prefix }}"
<<: *aws_connection_info
- name: set up testing target group for ALB (type=instance)
elb_target_group:
name: "{{ tg_name }}-used"
health_check_port: 80
protocol: http
port: 80
vpc_id: '{{ vpc.vpc.id }}'
state: present
target_type: instance
tags:
Description: "Created by {{ resource_prefix }}"
<<: *aws_connection_info
- name: set up ec2 instance to use as a target
ec2:
group_id: "{{ sg.group_id }}"
instance_type: t2.micro
image: "{{ ec2_ami_image[aws_region] }}"
vpc_subnet_id: "{{ subnet_2.subnet.id }}"
instance_tags:
Name: "{{ resource_prefix }}-inst"
exact_count: 1
count_tag:
Name: "{{ resource_prefix }}-inst"
assign_public_ip: true
volumes: []
wait: true
ebs_optimized: false
user_data: |
#cloud-config
package_upgrade: true
package_update: true
packages:
- httpd
runcmd:
- "service httpd start"
- echo "HELLO ANSIBLE" > /var/www/html/index.html
<<: *aws_connection_info
register: ec2
- name: create an application load balancer
elb_application_lb:
name: "{{ lb_name }}"
security_groups:
- "{{ sg.group_id }}"
subnets:
- "{{ subnet_1.subnet.id }}"
- "{{ subnet_2.subnet.id }}"
listeners:
- Protocol: HTTP
Port: 80
DefaultActions:
- Type: forward
TargetGroupName: "{{ tg_name }}-used"
state: present
<<: *aws_connection_info
# ============================================================
- name:
debug: msg="********** Running elb_target integration tests **********"
# ============================================================
- name: register an instance to unused target group
elb_target:
target_group_name: "{{ tg_name }}"
target_id: "{{ ec2.instance_ids[0] }}"
state: present
<<: *aws_connection_info
register: result
- name: target is registered
assert:
that:
- result.changed
- result.target_group_arn
- "'{{ result.target_health_descriptions.target.id }}' == '{{ ec2.instance_ids[0] }}'"
# ============================================================
- name: test idempotence
elb_target:
target_group_name: "{{ tg_name }}"
target_id: "{{ ec2.instance_ids[0] }}"
state: present
<<: *aws_connection_info
register: result
- name: target was already registered
assert:
that:
- not result.changed
# ============================================================
- name: remove an unused target
elb_target:
target_group_name: "{{ tg_name }}"
target_id: "{{ ec2.instance_ids[0] }}"
state: absent
deregister_unused: true
<<: *aws_connection_info
register: result
- name: target group was deleted
assert:
that:
- result.changed
- not result.target_health_descriptions
# ============================================================
- name: register an instance to used target group and wait until healthy
elb_target:
target_group_name: "{{ tg_name }}-used"
target_id: "{{ ec2.instance_ids[0] }}"
state: present
target_status: healthy
target_status_timeout: 200
<<: *aws_connection_info
register: result
- name: target is registered
assert:
that:
- result.changed
- result.target_group_arn
- "'{{ result.target_health_descriptions.target.id }}' == '{{ ec2.instance_ids[0] }}'"
- "{{ result.target_health_descriptions.target_health }} == {'state': 'healthy'}"
# ============================================================
- name: remove a target from used target group
elb_target:
target_group_name: "{{ tg_name }}-used"
target_id: "{{ ec2.instance_ids[0] }}"
state: absent
target_status: unused
target_status_timeout: 400
<<: *aws_connection_info
register: result
- name: target was deregistered
assert:
that:
- result.changed
# ============================================================
- name: test idempotence
elb_target:
target_group_name: "{{ tg_name }}-used"
target_id: "{{ ec2.instance_ids[0] }}"
state: absent
<<: *aws_connection_info
register: result
- name: target was already deregistered
assert:
that:
- not result.changed
# ============================================================
- name: register an instance to used target group and wait until healthy again to test deregistering differently
elb_target:
target_group_name: "{{ tg_name }}-used"
target_id: "{{ ec2.instance_ids[0] }}"
state: present
target_status: healthy
target_status_timeout: 200
<<: *aws_connection_info
register: result
- name: target is registered
assert:
that:
- result.changed
- result.target_group_arn
- "'{{ result.target_health_descriptions.target.id }}' == '{{ ec2.instance_ids[0] }}'"
- "{{ result.target_health_descriptions.target_health }} == {'state': 'healthy'}"
- name: start deregisteration but don't wait
elb_target:
target_group_name: "{{ tg_name }}-used"
target_id: "{{ ec2.instance_ids[0] }}"
state: absent
<<: *aws_connection_info
register: result
- name: target is starting to deregister
assert:
that:
- result.changed
- result.target_health_descriptions.target_health.reason == "Target.DeregistrationInProgress"
- name: now wait for target to finish deregistering
elb_target:
target_group_name: "{{ tg_name }}-used"
target_id: "{{ ec2.instance_ids[0] }}"
state: absent
target_status: unused
target_status_timeout: 400
<<: *aws_connection_info
register: result
- name: target was deregistered already and now has finished
assert:
that:
- not result.changed
- not result.target_health_descriptions
# ============================================================
always:
- name:
debug: msg="********** Tearing down elb_target test dependencies **********"
- name: remove ec2 instance
ec2:
group_id: "{{ sg.group_id }}"
instance_type: t2.micro
image: "{{ ec2_ami_image[aws_region] }}"
vpc_subnet_id: "{{ subnet_2.subnet.id }}"
instance_tags:
Name: "{{ resource_prefix }}-inst"
exact_count: 0
count_tag:
Name: "{{ resource_prefix }}-inst"
assign_public_ip: true
volumes: []
wait: true
ebs_optimized: false
<<: *aws_connection_info
ignore_errors: true
- name: remove testing target groups
elb_target_group:
name: "{{ item }}"
health_check_port: 80
protocol: http
port: 80
vpc_id: '{{ vpc.vpc.id }}'
state: absent
target_type: instance
tags:
Description: "Created by {{ resource_prefix }}"
wait: true
wait_timeout: 200
<<: *aws_connection_info
register: removed
retries: 10
until: removed is not failed
with_items:
- "{{ tg_name }}"
- "{{ tg_name }}-used"
ignore_errors: true
- name: remove application load balancer
elb_application_lb:
name: "{{ lb_name }}"
security_groups:
- "{{ sg.group_id }}"
subnets:
- "{{ subnet_1.subnet.id }}"
- "{{ subnet_2.subnet.id }}"
listeners:
- Protocol: HTTP
Port: 80
DefaultActions:
- Type: forward
TargetGroupName: "{{ tg_name }}-used"
state: absent
wait: true
wait_timeout: 200
<<: *aws_connection_info
register: removed
retries: 10
until: removed is not failed
ignore_errors: true
- name: remove testing security group
ec2_group:
state: absent
name: "{{ resource_prefix }}-sg"
description: a security group for ansible tests
vpc_id: "{{ vpc.vpc.id }}"
rules:
- proto: tcp
from_port: 80
to_port: 80
cidr_ip: 0.0.0.0/0
- proto: tcp
from_port: 22
to_port: 22
cidr_ip: 0.0.0.0/0
<<: *aws_connection_info
register: removed
retries: 10
until: removed is not failed
ignore_errors: true
- name: remove routing rules
ec2_vpc_route_table:
state: absent
lookup: id
route_table_id: "{{ route_table.route_table.id }}"
<<: *aws_connection_info
register: removed
retries: 10
until: removed is not failed
ignore_errors: true
- name: remove testing subnet
ec2_vpc_subnet:
state: absent
vpc_id: "{{ vpc.vpc.id }}"
cidr: 20.0.0.0/18
az: "{{ aws_region }}a"
resource_tags:
Name: "{{ resource_prefix }}-subnet"
<<: *aws_connection_info
register: removed
retries: 10
until: removed is not failed
ignore_errors: true
- name: remove testing subnet
ec2_vpc_subnet:
state: absent
vpc_id: "{{ vpc.vpc.id }}"
cidr: 20.0.64.0/18
az: "{{ aws_region }}b"
resource_tags:
Name: "{{ resource_prefix }}-subnet"
<<: *aws_connection_info
register: removed
retries: 10
until: removed is not failed
ignore_errors: true
- name: remove testing internet gateway
ec2_vpc_igw:
vpc_id: "{{ vpc.vpc.id }}"
state: absent
<<: *aws_connection_info
register: removed
retries: 10
until: removed is not failed
ignore_errors: true
- name: remove testing VPC
ec2_vpc_net:
name: "{{ resource_prefix }}-vpc"
state: absent
cidr_block: 20.0.0.0/16
tags:
Name: "{{ resource_prefix }}-vpc"
Description: "Created by ansible-test"
<<: *aws_connection_info
register: removed
retries: 10
until: removed is not failed
# ============================================================