[aws] Create classes for Application Load Balancer (#33769)

* Create classes for Application Load Balancer
* Add unsupported CI alias
* Add AWSRetry
* Add integration tests using the ALB
pull/38152/merge
Rob 7 years ago committed by Ryan Brown
parent 8ac69b0a5f
commit b5cffe8ced

@ -0,0 +1,110 @@
#!/usr/bin/env python
# Copyright (c) 2017 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from ansible.module_utils.ec2 import AWSRetry
# Non-ansible imports
try:
from botocore.exceptions import BotoCoreError, ClientError
except ImportError:
pass
def get_elb(connection, module, elb_name):
"""
Get an ELB based on name. If not found, return None.
:param connection: AWS boto3 elbv2 connection
:param module: Ansible module
:param elb_name: Name of load balancer to get
:return: boto3 ELB dict or None if not found
"""
try:
return _get_elb(connection, module, elb_name)
except (BotoCoreError, ClientError) as e:
module.fail_json_aws(e)
@AWSRetry.jittered_backoff()
def _get_elb(connection, module, elb_name):
"""
Get an ELB based on name using AWSRetry. If not found, return None.
:param connection: AWS boto3 elbv2 connection
:param module: Ansible module
:param elb_name: Name of load balancer to get
:return: boto3 ELB dict or None if not found
"""
try:
load_balancer_paginator = connection.get_paginator('describe_load_balancers')
return (load_balancer_paginator.paginate(Names=[elb_name]).build_full_result())['LoadBalancers'][0]
except (BotoCoreError, ClientError) as e:
if e.response['Error']['Code'] == 'LoadBalancerNotFound':
return None
else:
raise e
def get_elb_listener(connection, module, elb_arn, listener_port):
"""
Get an ELB listener based on the port provided. If not found, return None.
:param connection: AWS boto3 elbv2 connection
:param module: Ansible module
:param elb_arn: ARN of the ELB to look at
:param listener_port: Port of the listener to look for
:return: boto3 ELB listener dict or None if not found
"""
try:
listener_paginator = connection.get_paginator('describe_listeners')
listeners = (AWSRetry.jittered_backoff()(listener_paginator.paginate)(LoadBalancerArn=elb_arn).build_full_result())['Listeners']
except (BotoCoreError, ClientError) as e:
module.fail_json_aws(e)
l = None
for listener in listeners:
if listener['Port'] == listener_port:
l = listener
break
return l
def get_elb_listener_rules(connection, module, listener_arn):
"""
Get rules for a particular ELB listener using the listener ARN.
:param connection: AWS boto3 elbv2 connection
:param module: Ansible module
:param listener_arn: ARN of the ELB listener
:return: boto3 ELB rules list
"""
try:
return AWSRetry.jittered_backoff()(connection.describe_rules)(ListenerArn=listener_arn)['Rules']
except (BotoCoreError, ClientError) as e:
module.fail_json_aws(e)
def convert_tg_name_to_arn(connection, module, tg_name):
"""
Get ARN of a target group using the target group's name
:param connection: AWS boto3 elbv2 connection
:param module: Ansible module
:param tg_name: Name of the target group
:return: target group ARN string
"""
try:
response = AWSRetry.jittered_backoff()(connection.describe_target_groups)(Names=[tg_name])
except (BotoCoreError, ClientError) as e:
module.fail_json_aws(e)
tg_arn = response['TargetGroups'][0]['TargetGroupArn']
return tg_arn

@ -0,0 +1,757 @@
#!/usr/bin/env 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 imports
from ansible.module_utils.ec2 import camel_dict_to_snake_dict, get_ec2_security_group_ids_from_names, \
ansible_dict_to_boto3_tag_list, boto3_tag_list_to_ansible_dict, compare_policies as compare_dicts, \
AWSRetry
from ansible.module_utils.aws.elb_utils import get_elb, get_elb_listener, convert_tg_name_to_arn
# Non-ansible imports
try:
from botocore.exceptions import BotoCoreError, ClientError
except ImportError:
pass
import traceback
import time
from copy import deepcopy
class ElasticLoadBalancerV2(object):
def __init__(self, connection, module):
self.connection = connection
self.module = module
self.changed = False
self.new_load_balancer = False
self.scheme = module.params.get("scheme")
self.name = module.params.get("name")
self.subnet_mappings = module.params.get("subnet_mappings")
self.subnets = module.params.get("subnets")
self.deletion_protection = module.params.get("deletion_protection")
self.wait = module.params.get("wait")
if module.params.get("tags") is not None:
self.tags = ansible_dict_to_boto3_tag_list(module.params.get("tags"))
else:
self.tags = None
self.purge_tags = module.params.get("purge_tags")
self.elb = get_elb(connection, module, self.name)
if self.elb is not None:
self.elb_attributes = self.get_elb_attributes()
self.elb['tags'] = self.get_elb_tags()
else:
self.elb_attributes = None
def wait_for_status(self, elb_arn):
"""
Wait for load balancer to reach 'active' status
:param elb_arn: The load balancer ARN
:return:
"""
try:
waiter = self.connection.get_waiter('load_balancer_available')
waiter.wait(LoadBalancerArns=[elb_arn])
except (BotoCoreError, ClientError) as e:
self.module.fail_json_aws(e)
def get_elb_attributes(self):
"""
Get load balancer attributes
:return:
"""
try:
attr_list = AWSRetry.jittered_backoff()(
self.connection.describe_load_balancer_attributes
)(LoadBalancerArn=self.elb['LoadBalancerArn'])['Attributes']
elb_attributes = boto3_tag_list_to_ansible_dict(attr_list)
except (BotoCoreError, ClientError) as e:
self.module.fail_json_aws(e)
# Replace '.' with '_' in attribute key names to make it more Ansibley
return dict((k.replace('.', '_'), v) for k, v in elb_attributes.items())
def update_elb_attributes(self):
"""
Update the elb_attributes parameter
:return:
"""
self.elb_attributes = self.get_elb_attributes()
def get_elb_tags(self):
"""
Get load balancer tags
:return:
"""
try:
return AWSRetry.jittered_backoff()(
self.connection.describe_tags
)(ResourceArns=[self.elb['LoadBalancerArn']])['TagDescriptions'][0]['Tags']
except (BotoCoreError, ClientError) as e:
self.module.fail_json_aws(e)
def delete_tags(self, tags_to_delete):
"""
Delete elb tags
:return:
"""
try:
AWSRetry.jittered_backoff()(
self.connection.remove_tags
)(ResourceArns=[self.elb['LoadBalancerArn']], TagKeys=tags_to_delete)
except (BotoCoreError, ClientError) as e:
self.module.fail_json_aws(e)
self.changed = True
def modify_tags(self):
"""
Modify elb tags
:return:
"""
try:
AWSRetry.jittered_backoff()(
self.connection.add_tags
)(ResourceArns=[self.elb['LoadBalancerArn']], Tags=self.tags)
except (BotoCoreError, ClientError) as e:
self.module.fail_json_aws(e)
self.changed = True
def delete(self):
"""
Delete elb
:return:
"""
try:
AWSRetry.jittered_backoff()(
self.connection.delete_load_balancer
)(LoadBalancerArn=self.elb['LoadBalancerArn'])
except (BotoCoreError, ClientError) as e:
self.module.fail_json_aws(e)
self.changed = True
def compare_subnets(self):
"""
Compare user subnets with current ELB subnets
:return: bool True if they match otherwise False
"""
subnet_id_list = []
subnets = []
# Check if we're dealing with subnets or subnet_mappings
if self.subnets is not None:
# We need to first get the subnet ID from the list
subnets = self.subnets
if self.subnet_mappings is not None:
# Make a list from the subnet_mappings dict
subnets_from_mappings = []
for subnet_mapping in self.subnet_mappings:
subnets.append(subnet_mapping['SubnetId'])
for subnet in self.elb['AvailabilityZones']:
subnet_id_list.append(subnet['SubnetId'])
if set(subnet_id_list) != set(subnets):
return False
else:
return True
def modify_subnets(self):
"""
Modify elb subnets to match module parameters
:return:
"""
try:
AWSRetry.jittered_backoff()(
self.connection.set_subnets
)(LoadBalancerArn=self.elb['LoadBalancerArn'], Subnets=self.subnets)
except (BotoCoreError, ClientError) as e:
self.module.fail_json_aws(e)
self.changed = True
def update(self):
"""
Update the elb from AWS
:return:
"""
self.elb = get_elb(self.connection, self.module, self.module.params.get("name"))
self.elb['tags'] = self.get_elb_tags()
class ApplicationLoadBalancer(ElasticLoadBalancerV2):
def __init__(self, connection, connection_ec2, module):
"""
:param connection: boto3 connection
:param module: Ansible module
"""
super(ApplicationLoadBalancer, self).__init__(connection, module)
self.connection_ec2 = connection_ec2
# Ansible module parameters specific to ALBs
self.type = 'application'
if module.params.get('security_groups') is not None:
try:
self.security_groups = AWSRetry.jittered_backoff()(
get_ec2_security_group_ids_from_names
)(module.params.get('security_groups'), self.connection_ec2, boto3=True)
except ValueError as e:
self.module.fail_json(msg=str(e), exception=traceback.format_exc())
except (BotoCoreError, ClientError) as e:
self.module.fail_json_aws(e)
else:
self.security_groups = module.params.get('security_groups')
self.access_logs_enabled = module.params.get("access_logs_enabled")
self.access_logs_s3_bucket = module.params.get("access_logs_s3_bucket")
self.access_logs_s3_prefix = module.params.get("access_logs_s3_prefix")
self.idle_timeout = module.params.get("idle_timeout")
def create_elb(self):
"""
Create a load balancer
:return:
"""
# Required parameters
params = dict()
params['Name'] = self.name
params['Type'] = self.type
# Other parameters
if self.subnets is not None:
params['Subnets'] = self.subnets
if self.security_groups is not None:
params['SecurityGroups'] = self.security_groups
params['Scheme'] = self.scheme
if self.tags:
params['Tags'] = self.tags
try:
self.elb = AWSRetry.jittered_backoff()(self.connection.create_load_balancer)(**params)['LoadBalancers'][0]
self.changed = True
self.new_load_balancer = True
except (BotoCoreError, ClientError) as e:
self.module.fail_json_aws(e)
if self.wait:
self.wait_for_status(self.elb['LoadBalancerArn'])
def modify_elb_attributes(self):
"""
Update ELB attributes if required
:return:
"""
update_attributes = []
if self.access_logs_enabled and self.elb_attributes['access_logs_s3_enabled'] != "true":
update_attributes.append({'Key': 'access_logs.s3.enabled', 'Value': "true"})
if not self.access_logs_enabled and self.elb_attributes['access_logs_s3_enabled'] != "false":
update_attributes.append({'Key': 'access_logs.s3.enabled', 'Value': 'false'})
if self.access_logs_s3_bucket is not None and self.access_logs_s3_bucket != self.elb_attributes['access_logs_s3_bucket']:
update_attributes.append({'Key': 'access_logs.s3.bucket', 'Value': self.access_logs_s3_bucket})
if self.access_logs_s3_prefix is not None and self.access_logs_s3_prefix != self.elb_attributes['access_logs_s3_prefix']:
update_attributes.append({'Key': 'access_logs.s3.prefix', 'Value': self.access_logs_s3_prefix})
if self.deletion_protection and self.elb_attributes['deletion_protection_enabled'] != "true":
update_attributes.append({'Key': 'deletion_protection.enabled', 'Value': "true"})
if self.deletion_protection is not None and not self.deletion_protection and self.elb_attributes['deletion_protection_enabled'] != "false":
update_attributes.append({'Key': 'deletion_protection.enabled', 'Value': "false"})
if self.idle_timeout is not None and str(self.idle_timeout) != self.elb_attributes['idle_timeout_timeout_seconds']:
update_attributes.append({'Key': 'idle_timeout.timeout_seconds', 'Value': str(self.idle_timeout)})
if update_attributes:
try:
AWSRetry.jittered_backoff()(
self.connection.modify_load_balancer_attributes
)(LoadBalancerArn=self.elb['LoadBalancerArn'], Attributes=update_attributes)
self.changed = True
except (BotoCoreError, ClientError) as e:
# Something went wrong setting attributes. If this ELB was created during this task, delete it to leave a consistent state
if self.new_load_balancer:
AWSRetry.jittered_backoff()(self.connection.delete_load_balancer)(LoadBalancerArn=self.elb['LoadBalancerArn'])
self.module.fail_json_aws(e)
def compare_security_groups(self):
"""
Compare user security groups with current ELB security groups
:return: bool True if they match otherwise False
"""
if set(self.elb['SecurityGroups']) != set(self.security_groups):
return False
else:
return True
def modify_security_groups(self):
"""
Modify elb security groups to match module parameters
:return:
"""
try:
AWSRetry.jittered_backoff()(
self.connection.set_security_groups
)(LoadBalancerArn=self.elb['LoadBalancerArn'], SecurityGroups=self.security_groups)
except (BotoCoreError, ClientError) as e:
self.module.fail_json_aws(e)
self.changed = True
class NetworkLoadBalancer(ElasticLoadBalancerV2):
def __init__(self, connection, connection_ec2, module):
"""
:param connection: boto3 connection
:param module: Ansible module
"""
super(NetworkLoadBalancer, self).__init__(connection, module)
self.connection_ec2 = connection_ec2
# Ansible module parameters specific to NLBs
self.type = 'network'
def create_elb(self):
"""
Create a load balancer
:return:
"""
# Required parameters
params = dict()
params['Name'] = self.name
params['Type'] = self.type
# Other parameters
if self.subnets is not None:
params['Subnets'] = self.subnets
params['Scheme'] = self.scheme
if self.tags is not None:
params['Tags'] = self.tags
try:
self.elb = AWSRetry.jittered_backoff()(self.connection.create_load_balancer)(**params)['LoadBalancers'][0]
self.changed = True
self.new_load_balancer = True
except (BotoCoreError, ClientError) as e:
self.module.fail_json_aws(e)
if self.wait:
self.wait_for_status(self.elb['LoadBalancerArn'])
def modify_elb_attributes(self):
"""
Update ELB attributes if required
:return:
"""
update_attributes = []
if self.deletion_protection and self.elb_attributes['deletion_protection_enabled'] != "true":
update_attributes.append({'Key': 'deletion_protection.enabled', 'Value': "true"})
if self.deletion_protection is not None and not self.deletion_protection and self.elb_attributes['deletion_protection_enabled'] != "false":
update_attributes.append({'Key': 'deletion_protection.enabled', 'Value': "false"})
if update_attributes:
try:
AWSRetry.jittered_backoff()(
self.connection.modify_load_balancer_attributes
)(LoadBalancerArn=self.elb['LoadBalancerArn'], Attributes=update_attributes)
self.changed = True
except (BotoCoreError, ClientError) as e:
# Something went wrong setting attributes. If this ELB was created during this task, delete it to leave a consistent state
if self.new_load_balancer:
AWSRetry.jittered_backoff()(self.connection.delete_load_balancer)(LoadBalancerArn=self.elb['LoadBalancerArn'])
self.module.fail_json_aws(e)
class ELBListeners(object):
def __init__(self, connection, module, elb_arn):
self.connection = connection
self.module = module
self.elb_arn = elb_arn
listeners = module.params.get("listeners")
if listeners is not None:
# Remove suboption argspec defaults of None from each listener
listeners = [dict((x, listener_dict[x]) for x in listener_dict if listener_dict[x] is not None) for listener_dict in listeners]
self.listeners = self._ensure_listeners_default_action_has_arn(listeners)
self.current_listeners = self._get_elb_listeners()
self.purge_listeners = module.params.get("purge_listeners")
self.changed = False
def update(self):
"""
Update the listeners for the ELB
:return:
"""
self.current_listeners = self._get_elb_listeners()
def _get_elb_listeners(self):
"""
Get ELB listeners
:return:
"""
try:
listener_paginator = self.connection.get_paginator('describe_listeners')
return (AWSRetry.jittered_backoff()(listener_paginator.paginate)(LoadBalancerArn=self.elb_arn).build_full_result())['Listeners']
except (BotoCoreError, ClientError) as e:
self.module.fail_json_aws(e)
def _ensure_listeners_default_action_has_arn(self, listeners):
"""
If a listener DefaultAction has been passed with a Target Group Name instead of ARN, lookup the ARN and
replace the name.
:param listeners: a list of listener dicts
:return: the same list of dicts ensuring that each listener DefaultActions dict has TargetGroupArn key. If a TargetGroupName key exists, it is removed.
"""
if not listeners:
listeners = []
for listener in listeners:
if 'TargetGroupName' in listener['DefaultActions'][0]:
listener['DefaultActions'][0]['TargetGroupArn'] = convert_tg_name_to_arn(self.connection, self.module,
listener['DefaultActions'][0]['TargetGroupName'])
del listener['DefaultActions'][0]['TargetGroupName']
return listeners
def compare_listeners(self):
"""
:return:
"""
listeners_to_modify = []
listeners_to_delete = []
listeners_to_add = deepcopy(self.listeners)
# Check each current listener port to see if it's been passed to the module
for current_listener in self.current_listeners:
current_listener_passed_to_module = False
for new_listener in self.listeners[:]:
new_listener['Port'] = int(new_listener['Port'])
if current_listener['Port'] == new_listener['Port']:
current_listener_passed_to_module = True
# Remove what we match so that what is left can be marked as 'to be added'
listeners_to_add.remove(new_listener)
modified_listener = self._compare_listener(current_listener, new_listener)
if modified_listener:
modified_listener['Port'] = current_listener['Port']
modified_listener['ListenerArn'] = current_listener['ListenerArn']
listeners_to_modify.append(modified_listener)
break
# If the current listener was not matched against passed listeners and purge is True, mark for removal
if not current_listener_passed_to_module and self.purge_listeners:
listeners_to_delete.append(current_listener['ListenerArn'])
return listeners_to_add, listeners_to_modify, listeners_to_delete
def _compare_listener(self, current_listener, new_listener):
"""
Compare two listeners.
:param current_listener:
:param new_listener:
:return:
"""
modified_listener = {}
# Port
if current_listener['Port'] != new_listener['Port']:
modified_listener['Port'] = new_listener['Port']
# Protocol
if current_listener['Protocol'] != new_listener['Protocol']:
modified_listener['Protocol'] = new_listener['Protocol']
# If Protocol is HTTPS, check additional attributes
if current_listener['Protocol'] == 'HTTPS' and new_listener['Protocol'] == 'HTTPS':
# Cert
if current_listener['SslPolicy'] != new_listener['SslPolicy']:
modified_listener['SslPolicy'] = new_listener['SslPolicy']
if current_listener['Certificates'][0]['CertificateArn'] != new_listener['Certificates'][0]['CertificateArn']:
modified_listener['Certificates'] = []
modified_listener['Certificates'].append({})
modified_listener['Certificates'][0]['CertificateArn'] = new_listener['Certificates'][0]['CertificateArn']
elif current_listener['Protocol'] != 'HTTPS' and new_listener['Protocol'] == 'HTTPS':
modified_listener['SslPolicy'] = new_listener['SslPolicy']
modified_listener['Certificates'] = []
modified_listener['Certificates'].append({})
modified_listener['Certificates'][0]['CertificateArn'] = new_listener['Certificates'][0]['CertificateArn']
# Default action
# We wont worry about the Action Type because it is always 'forward'
if current_listener['DefaultActions'][0]['TargetGroupArn'] != new_listener['DefaultActions'][0]['TargetGroupArn']:
modified_listener['DefaultActions'] = []
modified_listener['DefaultActions'].append({})
modified_listener['DefaultActions'][0]['TargetGroupArn'] = new_listener['DefaultActions'][0]['TargetGroupArn']
modified_listener['DefaultActions'][0]['Type'] = 'forward'
if modified_listener:
return modified_listener
else:
return None
class ELBListener(object):
def __init__(self, connection, module, listener, elb_arn):
"""
:param connection:
:param module:
:param listener:
:param elb_arn:
"""
self.connection = connection
self.module = module
self.listener = listener
self.elb_arn = elb_arn
def add(self):
try:
# Rules is not a valid parameter for create_listener
if 'Rules' in self.listener:
self.listener.pop('Rules')
AWSRetry.jittered_backoff()(self.connection.create_listener)(LoadBalancerArn=self.elb_arn, **self.listener)
except (BotoCoreError, ClientError) as e:
self.module.fail_json_aws(e)
def modify(self):
try:
# Rules is not a valid parameter for modify_listener
if 'Rules' in self.listener:
self.listener.pop('Rules')
AWSRetry.jittered_backoff()(self.connection.modify_listener)(**self.listener)
except (BotoCoreError, ClientError) as e:
self.module.fail_json_aws(e)
def delete(self):
try:
AWSRetry.jittered_backoff()(self.connection.delete_listener)(ListenerArn=self.listener)
except (BotoCoreError, ClientError) as e:
self.module.fail_json_aws(e)
class ELBListenerRules(object):
def __init__(self, connection, module, elb_arn, listener_rules, listener_port):
self.connection = connection
self.module = module
self.elb_arn = elb_arn
self.rules = self._ensure_rules_action_has_arn(listener_rules)
self.changed = False
# Get listener based on port so we can use ARN
self.current_listener = get_elb_listener(connection, module, elb_arn, listener_port)
self.listener_arn = self.current_listener['ListenerArn']
self.rules_to_add = deepcopy(self.rules)
self.rules_to_modify = []
self.rules_to_delete = []
# If the listener exists (i.e. has an ARN) get rules for the listener
if 'ListenerArn' in self.current_listener:
self.current_rules = self._get_elb_listener_rules()
else:
self.current_rules = []
def _ensure_rules_action_has_arn(self, rules):
"""
If a rule Action has been passed with a Target Group Name instead of ARN, lookup the ARN and
replace the name.
:param rules: a list of rule dicts
:return: the same list of dicts ensuring that each rule Actions dict has TargetGroupArn key. If a TargetGroupName key exists, it is removed.
"""
for rule in rules:
if 'TargetGroupName' in rule['Actions'][0]:
rule['Actions'][0]['TargetGroupArn'] = convert_tg_name_to_arn(self.connection, self.module, rule['Actions'][0]['TargetGroupName'])
del rule['Actions'][0]['TargetGroupName']
return rules
def _get_elb_listener_rules(self):
try:
return AWSRetry.jittered_backoff()(self.connection.describe_rules)(ListenerArn=self.current_listener['ListenerArn'])['Rules']
except (BotoCoreError, ClientError) as e:
self.module.fail_json_aws(e)
def _compare_condition(self, current_conditions, condition):
"""
:param current_conditions:
:param condition:
:return:
"""
condition_found = False
for current_condition in current_conditions:
if current_condition['Field'] == condition['Field'] and current_condition['Values'][0] == condition['Values'][0]:
condition_found = True
break
return condition_found
def _compare_rule(self, current_rule, new_rule):
"""
:return:
"""
modified_rule = {}
# Priority
if current_rule['Priority'] != new_rule['Priority']:
modified_rule['Priority'] = new_rule['Priority']
# Actions
# We wont worry about the Action Type because it is always 'forward'
if current_rule['Actions'][0]['TargetGroupArn'] != new_rule['Actions'][0]['TargetGroupArn']:
modified_rule['Actions'] = []
modified_rule['Actions'].append({})
modified_rule['Actions'][0]['TargetGroupArn'] = new_rule['Actions'][0]['TargetGroupArn']
modified_rule['Actions'][0]['Type'] = 'forward'
# Conditions
modified_conditions = []
for condition in new_rule['Conditions']:
if not self._compare_condition(current_rule['Conditions'], condition):
modified_conditions.append(condition)
if modified_conditions:
modified_rule['Conditions'] = modified_conditions
return modified_rule
def compare_rules(self):
"""
:return:
"""
rules_to_modify = []
rules_to_delete = []
rules_to_add = deepcopy(self.rules)
for current_rule in self.current_rules:
current_rule_passed_to_module = False
for new_rule in self.rules[:]:
if current_rule['Priority'] == new_rule['Priority']:
current_rule_passed_to_module = True
# Remove what we match so that what is left can be marked as 'to be added'
rules_to_add.remove(new_rule)
modified_rule = self._compare_rule(current_rule, new_rule)
if modified_rule:
modified_rule['Priority'] = int(current_rule['Priority'])
modified_rule['RuleArn'] = current_rule['RuleArn']
modified_rule['Actions'] = new_rule['Actions']
modified_rule['Conditions'] = new_rule['Conditions']
rules_to_modify.append(modified_rule)
break
# If the current rule was not matched against passed rules, mark for removal
if not current_rule_passed_to_module and not current_rule['IsDefault']:
rules_to_delete.append(current_rule['RuleArn'])
return rules_to_add, rules_to_modify, rules_to_delete
class ELBListenerRule(object):
def __init__(self, connection, module, rule, listener_arn):
self.connection = connection
self.module = module
self.rule = rule
self.listener_arn = listener_arn
self.changed = False
def create(self):
"""
Create a listener rule
:return:
"""
try:
self.rule['ListenerArn'] = self.listener_arn
self.rule['Priority'] = int(self.rule['Priority'])
AWSRetry.jittered_backoff()(self.connection.create_rule)(**self.rule)
except (BotoCoreError, ClientError) as e:
self.module.fail_json_aws(e)
self.changed = True
def modify(self):
"""
Modify a listener rule
:return:
"""
try:
del self.rule['Priority']
AWSRetry.jittered_backoff()(self.connection.modify_rule)(**self.rule)
except (BotoCoreError, ClientError) as e:
self.module.fail_json_aws(e)
self.changed = True
def delete(self):
"""
Delete a listener rule
:return:
"""
try:
AWSRetry.jittered_backoff()(self.connection.delete_rule)(RuleArn=self.rule['RuleArn'])
except (BotoCoreError, ClientError) as e:
self.module.fail_json_aws(e)
self.changed = True

@ -101,6 +101,17 @@ options:
description: description:
- A dictionary of one or more tags to assign to the load balancer. - A dictionary of one or more tags to assign to the load balancer.
required: false required: false
wait:
description:
- Wait for the load balancer to have a state of 'active' before completing. A status check is
performed every 15 seconds until a successful state is reached. An error is returned after 40 failed checks.
default: no
type: bool
version_added: 2.6
wait_timeout:
description:
- The time in seconds to use in conjunction with I(wait).
version_added: 2.6
extends_documentation_fragment: extends_documentation_fragment:
- aws - aws
- ec2 - ec2
@ -342,623 +353,132 @@ vpc_id:
type: string type: string
sample: vpc-0011223344 sample: vpc-0011223344
''' '''
import time
import collections
from copy import deepcopy
import traceback
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.six import string_types
from ansible.module_utils.ec2 import boto3_conn, get_aws_connection_info, camel_dict_to_snake_dict, ec2_argument_spec, get_ec2_security_group_ids_from_names, \
ansible_dict_to_boto3_tag_list, boto3_tag_list_to_ansible_dict, compare_aws_tags, HAS_BOTO3
try:
import boto3
from botocore.exceptions import ClientError, NoCredentialsError
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=e.message, exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response))
tg_arn = response['TargetGroups'][0]['TargetGroupArn']
return tg_arn
def wait_for_status(connection, module, elb_arn, status):
polling_increment_secs = 15
max_retries = module.params.get('wait_timeout') // polling_increment_secs
status_achieved = False
for x in range(0, max_retries):
try:
response = connection.describe_load_balancers(LoadBalancerArns=[elb_arn])
if response['LoadBalancers'][0]['State']['Code'] == status:
status_achieved = True
break
else:
time.sleep(polling_increment_secs)
except ClientError as e:
module.fail_json(msg=e.message, exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response))
result = response
return status_achieved, result
def _get_subnet_ids_from_subnet_list(subnet_list):
subnet_id_list = []
for subnet in subnet_list:
subnet_id_list.append(subnet['SubnetId'])
return subnet_id_list
def get_elb_listeners(connection, module, elb_arn):
try:
listener_paginator = connection.get_paginator('describe_listeners')
return (listener_paginator.paginate(LoadBalancerArn=elb_arn).build_full_result())['Listeners']
except ClientError as e:
module.fail_json(msg=e.message, exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response))
def get_elb_attributes(connection, module, elb_arn):
try:
elb_attributes = boto3_tag_list_to_ansible_dict(connection.describe_load_balancer_attributes(LoadBalancerArn=elb_arn)['Attributes'])
except ClientError as e:
module.fail_json(msg=e.message, exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response))
# Replace '.' with '_' in attribute key names to make it more Ansibley
return dict((k.replace('.', '_'), v) for k, v in elb_attributes.items())
def get_listener(connection, module, elb_arn, listener_port):
"""
Get a listener based on the port provided.
:param connection: ELBv2 boto3 connection
:param module: Ansible module object
:param listener_port:
:return:
"""
try:
listener_paginator = connection.get_paginator('describe_listeners')
listeners = (listener_paginator.paginate(LoadBalancerArn=elb_arn).build_full_result())['Listeners']
except ClientError as e:
module.fail_json(msg=e.message, exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response))
l = None
for listener in listeners:
if listener['Port'] == listener_port:
l = listener
break
return l
def get_elb(connection, module):
"""
Get an application load balancer based on name. If not found, return None
:param connection: ELBv2 boto3 connection
:param module: Ansible module object
:return: Dict of load balancer attributes or None if not found
"""
try:
load_balancer_paginator = connection.get_paginator('describe_load_balancers')
return (load_balancer_paginator.paginate(Names=[module.params.get("name")]).build_full_result())['LoadBalancers'][0]
except ClientError as e:
if e.response['Error']['Code'] == 'LoadBalancerNotFound':
return None
else:
module.fail_json(msg=e.message, exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response))
def get_listener_rules(connection, module, listener_arn):
try:
return connection.describe_rules(ListenerArn=listener_arn)['Rules']
except ClientError as e:
module.fail_json(msg=e.message, exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response))
def ensure_listeners_default_action_has_arn(connection, module, listeners):
"""
If a listener DefaultAction has been passed with a Target Group Name instead of ARN, lookup the ARN and
replace the name.
:param connection: ELBv2 boto3 connection
:param module: Ansible module object
:param listeners: a list of listener dicts
:return: the same list of dicts ensuring that each listener DefaultActions dict has TargetGroupArn key. If a TargetGroupName key exists, it is removed.
"""
if not listeners:
listeners = []
for listener in listeners:
if 'TargetGroupName' in listener['DefaultActions'][0]:
listener['DefaultActions'][0]['TargetGroupArn'] = convert_tg_name_to_arn(connection, module, listener['DefaultActions'][0]['TargetGroupName'])
del listener['DefaultActions'][0]['TargetGroupName']
return listeners
def ensure_rules_action_has_arn(connection, module, rules):
"""
If a rule Action has been passed with a Target Group Name instead of ARN, lookup the ARN and
replace the name.
:param connection: ELBv2 boto3 connection
:param module: Ansible module object
:param rules: a list of rule dicts
:return: the same list of dicts ensuring that each rule Actions dict has TargetGroupArn key. If a TargetGroupName key exists, it is removed.
"""
for rule in rules:
if 'TargetGroupName' in rule['Actions'][0]:
rule['Actions'][0]['TargetGroupArn'] = convert_tg_name_to_arn(connection, module, rule['Actions'][0]['TargetGroupName'])
del rule['Actions'][0]['TargetGroupName']
return rules
def compare_listener(current_listener, new_listener):
"""
Compare two listeners.
:param current_listener:
:param new_listener:
:return:
"""
modified_listener = {}
# Port
if current_listener['Port'] != new_listener['Port']:
modified_listener['Port'] = new_listener['Port']
# Protocol
if current_listener['Protocol'] != new_listener['Protocol']:
modified_listener['Protocol'] = new_listener['Protocol']
# If Protocol is HTTPS, check additional attributes
if current_listener['Protocol'] == 'HTTPS' and new_listener['Protocol'] == 'HTTPS':
# Cert
if current_listener['SslPolicy'] != new_listener['SslPolicy']:
modified_listener['SslPolicy'] = new_listener['SslPolicy']
if current_listener['Certificates'][0]['CertificateArn'] != new_listener['Certificates'][0]['CertificateArn']:
modified_listener['Certificates'] = []
modified_listener['Certificates'].append({})
modified_listener['Certificates'][0]['CertificateArn'] = new_listener['Certificates'][0]['CertificateArn']
elif current_listener['Protocol'] != 'HTTPS' and new_listener['Protocol'] == 'HTTPS':
modified_listener['SslPolicy'] = new_listener['SslPolicy']
modified_listener['Certificates'] = []
modified_listener['Certificates'].append({})
modified_listener['Certificates'][0]['CertificateArn'] = new_listener['Certificates'][0]['CertificateArn']
# Default action
# We wont worry about the Action Type because it is always 'forward'
if current_listener['DefaultActions'][0]['TargetGroupArn'] != new_listener['DefaultActions'][0]['TargetGroupArn']:
modified_listener['DefaultActions'] = []
modified_listener['DefaultActions'].append({})
modified_listener['DefaultActions'][0]['TargetGroupArn'] = new_listener['DefaultActions'][0]['TargetGroupArn']
modified_listener['DefaultActions'][0]['Type'] = 'forward'
if modified_listener:
return modified_listener
else:
return None
def compare_condition(current_conditions, condition):
"""
:param current_conditions:
:param condition:
:return:
"""
condition_found = False
for current_condition in current_conditions: from ansible.module_utils.aws.core import AnsibleAWSModule
if current_condition['Field'] == condition['Field'] and current_condition['Values'][0] == condition['Values'][0]: from ansible.module_utils.ec2 import boto3_conn, get_aws_connection_info, camel_dict_to_snake_dict, ec2_argument_spec, \
condition_found = True boto3_tag_list_to_ansible_dict, compare_aws_tags, HAS_BOTO3
break
return condition_found from ansible.module_utils.aws.elbv2 import ApplicationLoadBalancer, ELBListeners, ELBListener, ELBListenerRules, ELBListenerRule
from ansible.module_utils.aws.elb_utils import get_elb_listener_rules
def compare_rule(current_rule, new_rule): def create_or_update_elb(elb_obj):
""" """Create ELB or modify main attributes. json_exit here"""
Compare two rules.
:param current_rule:
:param new_rule:
:return:
"""
modified_rule = {}
# Priority
if current_rule['Priority'] != new_rule['Priority']:
modified_rule['Priority'] = new_rule['Priority']
# Actions
# We wont worry about the Action Type because it is always 'forward'
if current_rule['Actions'][0]['TargetGroupArn'] != new_rule['Actions'][0]['TargetGroupArn']:
modified_rule['Actions'] = []
modified_rule['Actions'].append({})
modified_rule['Actions'][0]['TargetGroupArn'] = new_rule['Actions'][0]['TargetGroupArn']
modified_rule['Actions'][0]['Type'] = 'forward'
# Conditions
modified_conditions = []
for condition in new_rule['Conditions']:
if not compare_condition(current_rule['Conditions'], condition):
modified_conditions.append(condition)
if modified_conditions:
modified_rule['Conditions'] = modified_conditions
return modified_rule
def compare_listeners(connection, module, current_listeners, new_listeners, purge_listeners):
"""
Compare listeners and return listeners to add, listeners to modify and listeners to remove
Listeners are compared based on port
:param connection: ELBv2 boto3 connection
:param module: Ansible module object
:param current_listeners:
:param new_listeners:
:param purge_listeners:
:return:
"""
listeners_to_modify = []
listeners_to_delete = []
# Check each current listener port to see if it's been passed to the module
for current_listener in current_listeners:
current_listener_passed_to_module = False
for new_listener in new_listeners[:]:
new_listener['Port'] = int(new_listener['Port'])
if current_listener['Port'] == new_listener['Port']:
current_listener_passed_to_module = True
# Remove what we match so that what is left can be marked as 'to be added'
new_listeners.remove(new_listener)
modified_listener = compare_listener(current_listener, new_listener)
if modified_listener:
modified_listener['Port'] = current_listener['Port']
modified_listener['ListenerArn'] = current_listener['ListenerArn']
listeners_to_modify.append(modified_listener)
break
# If the current listener was not matched against passed listeners and purge is True, mark for removal
if not current_listener_passed_to_module and purge_listeners:
listeners_to_delete.append(current_listener['ListenerArn'])
listeners_to_add = new_listeners if elb_obj.elb:
# ELB exists so check subnets, security groups and tags match what has been passed
return listeners_to_add, listeners_to_modify, listeners_to_delete # Subnets
if not elb_obj.compare_subnets():
elb_obj.modify_subnets()
# Security Groups
if not elb_obj.compare_security_groups():
elb_obj.modify_security_groups()
def compare_rules(connection, module, current_listeners, listener): # Tags - only need to play with tags if tags parameter has been set to something
""" if elb_obj.tags is not None:
Compare rules and return rules to add, rules to modify and rules to remove
Rules are compared based on priority
:param connection: ELBv2 boto3 connection # Delete necessary tags
:param module: Ansible module object tags_need_modify, tags_to_delete = compare_aws_tags(boto3_tag_list_to_ansible_dict(elb_obj.elb['tags']),
:param current_listeners: list of listeners currently associated with the ELB boto3_tag_list_to_ansible_dict(elb_obj.tags), elb_obj.purge_tags)
:param listener: dict object of a listener passed by the user if tags_to_delete:
:return: elb_obj.delete_tags(tags_to_delete)
"""
# Run through listeners looking for a match (by port) to get the ARN # Add/update tags
for current_listener in current_listeners: if tags_need_modify:
if current_listener['Port'] == listener['Port']: elb_obj.modify_tags()
listener['ListenerArn'] = current_listener['ListenerArn']
break
# If the listener exists (i.e. has an ARN) get rules for the listener
if 'ListenerArn' in listener:
current_rules = get_listener_rules(connection, module, listener['ListenerArn'])
else: else:
current_rules = [] # Create load balancer
elb_obj.create_elb()
rules_to_modify = []
rules_to_delete = []
for current_rule in current_rules:
current_rule_passed_to_module = False
for new_rule in listener['Rules'][:]:
if current_rule['Priority'] == new_rule['Priority']:
current_rule_passed_to_module = True
# Remove what we match so that what is left can be marked as 'to be added'
listener['Rules'].remove(new_rule)
modified_rule = compare_rule(current_rule, new_rule)
if modified_rule:
modified_rule['Priority'] = int(current_rule['Priority'])
modified_rule['RuleArn'] = current_rule['RuleArn']
modified_rule['Actions'] = new_rule['Actions']
modified_rule['Conditions'] = new_rule['Conditions']
rules_to_modify.append(modified_rule)
break
# If the current rule was not matched against passed rules, mark for removal
if not current_rule_passed_to_module and not current_rule['IsDefault']:
rules_to_delete.append(current_rule['RuleArn'])
rules_to_add = listener['Rules']
return rules_to_add, rules_to_modify, rules_to_delete
# ELB attributes
elb_obj.update_elb_attributes()
elb_obj.modify_elb_attributes()
def create_or_update_elb_listeners(connection, module, elb): # Listeners
"""Create or update ELB listeners. Return true if changed, else false""" listeners_obj = ELBListeners(elb_obj.connection, elb_obj.module, elb_obj.elb['LoadBalancerArn'])
listener_changed = False listeners_to_add, listeners_to_modify, listeners_to_delete = listeners_obj.compare_listeners()
# Ensure listeners are using Target Group ARN not name
listeners = ensure_listeners_default_action_has_arn(connection, module, module.params.get("listeners"))
purge_listeners = module.params.get("purge_listeners")
# Does the ELB have any listeners exist? # Delete listeners
current_listeners = get_elb_listeners(connection, module, elb['LoadBalancerArn']) for listener_to_delete in listeners_to_delete:
listener_obj = ELBListener(elb_obj.connection, elb_obj.module, listener_to_delete, elb_obj.elb['LoadBalancerArn'])
listeners_to_add, listeners_to_modify, listeners_to_delete = compare_listeners(connection, module, current_listeners, deepcopy(listeners), purge_listeners) listener_obj.delete()
listeners_obj.changed = True
# Add listeners # Add listeners
for listener_to_add in listeners_to_add: for listener_to_add in listeners_to_add:
try: listener_obj = ELBListener(elb_obj.connection, elb_obj.module, listener_to_add, elb_obj.elb['LoadBalancerArn'])
listener_to_add['LoadBalancerArn'] = elb['LoadBalancerArn'] listener_obj.add()
# Rules is not a valid parameter for create_listener listeners_obj.changed = True
if 'Rules' in listener_to_add:
listener_to_add.pop('Rules')
response = connection.create_listener(**listener_to_add)
# Add the new listener
current_listeners.append(response['Listeners'][0])
listener_changed = True
except ClientError as e:
module.fail_json(msg=e.message, exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response))
# Modify listeners # Modify listeners
for listener_to_modify in listeners_to_modify: for listener_to_modify in listeners_to_modify:
try: listener_obj = ELBListener(elb_obj.connection, elb_obj.module, listener_to_modify, elb_obj.elb['LoadBalancerArn'])
# Rules is not a valid parameter for modify_listener listener_obj.modify()
if 'Rules' in listener_to_modify: listeners_obj.changed = True
listener_to_modify.pop('Rules')
connection.modify_listener(**listener_to_modify)
listener_changed = True
except ClientError as e:
module.fail_json(msg=e.message, exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response))
# Delete listeners # If listeners changed, mark ELB as changed
for listener_to_delete in listeners_to_delete: if listeners_obj.changed:
try: elb_obj.changed = True
connection.delete_listener(ListenerArn=listener_to_delete)
listener_changed = True
except ClientError as e:
module.fail_json(msg=e.message, exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response))
# For each listener, check rules # Rules of each listener
for listener in deepcopy(listeners): for listener in listeners_obj.listeners:
if 'Rules' in listener: if 'Rules' in listener:
# Ensure rules are using Target Group ARN not name rules_obj = ELBListenerRules(elb_obj.connection, elb_obj.module, elb_obj.elb['LoadBalancerArn'], listener['Rules'], listener['Port'])
listener['Rules'] = ensure_rules_action_has_arn(connection, module, listener['Rules'])
rules_to_add, rules_to_modify, rules_to_delete = compare_rules(connection, module, current_listeners, listener)
# Get listener based on port so we can use ARN rules_to_add, rules_to_modify, rules_to_delete = rules_obj.compare_rules()
looked_up_listener = get_listener(connection, module, elb['LoadBalancerArn'], listener['Port'])
# Delete rules # Delete rules
for rule in rules_to_delete: for rule in rules_to_delete:
try: rule_obj = ELBListenerRule(elb_obj.connection, elb_obj.module, {'RuleArn': rule}, rules_obj.listener_arn)
connection.delete_rule(RuleArn=rule) rule_obj.delete()
listener_changed = True elb_obj.changed = True
except ClientError as e:
module.fail_json(msg=e.message, exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response))
# Add rules # Add rules
for rule in rules_to_add: for rule in rules_to_add:
try: rule_obj = ELBListenerRule(elb_obj.connection, elb_obj.module, rule, rules_obj.listener_arn)
rule['ListenerArn'] = looked_up_listener['ListenerArn'] rule_obj.create()
rule['Priority'] = int(rule['Priority']) elb_obj.changed = True
connection.create_rule(**rule)
listener_changed = True
except ClientError as e:
module.fail_json(msg=e.message, exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response))
# Modify rules # Modify rules
for rule in rules_to_modify: for rule in rules_to_modify:
try: rule_obj = ELBListenerRule(elb_obj.connection, elb_obj.module, rule, rules_obj.listener_arn)
del rule['Priority'] rule_obj.modify()
connection.modify_rule(**rule) elb_obj.changed = True
listener_changed = True
except ClientError as e:
module.fail_json(msg=e.message, exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response))
return listener_changed
def create_or_update_elb(connection, connection_ec2, module):
"""Create ELB or modify main attributes. json_exit here"""
changed = False
new_load_balancer = False
params = dict()
params['Name'] = module.params.get("name")
params['Subnets'] = module.params.get("subnets")
try:
params['SecurityGroups'] = get_ec2_security_group_ids_from_names(module.params.get('security_groups'), connection_ec2, boto3=True)
except ValueError as e:
module.fail_json(msg=str(e), exception=traceback.format_exc())
except ClientError as e:
module.fail_json(msg=e.message, exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response))
except NoCredentialsError as e:
module.fail_json(msg="AWS authentication problem. " + e.message, exception=traceback.format_exc())
params['Scheme'] = module.params.get("scheme")
if module.params.get("tags"):
params['Tags'] = ansible_dict_to_boto3_tag_list(module.params.get("tags"))
purge_tags = module.params.get("purge_tags")
access_logs_enabled = module.params.get("access_logs_enabled")
access_logs_s3_bucket = module.params.get("access_logs_s3_bucket")
access_logs_s3_prefix = module.params.get("access_logs_s3_prefix")
deletion_protection = module.params.get("deletion_protection")
idle_timeout = module.params.get("idle_timeout")
# Does the ELB currently exist?
elb = get_elb(connection, module)
if elb:
# ELB exists so check subnets, security groups and tags match what has been passed
# Subnets
if set(_get_subnet_ids_from_subnet_list(elb['AvailabilityZones'])) != set(params['Subnets']):
try:
connection.set_subnets(LoadBalancerArn=elb['LoadBalancerArn'], Subnets=params['Subnets'])
except ClientError as e:
module.fail_json(msg=e.message, exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response))
changed = True
# Security Groups
if set(elb['SecurityGroups']) != set(params['SecurityGroups']):
try:
connection.set_security_groups(LoadBalancerArn=elb['LoadBalancerArn'], SecurityGroups=params['SecurityGroups'])
except ClientError as e:
module.fail_json(msg=e.message, exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response))
changed = True
# Tags - only need to play with tags if tags parameter has been set to something
if module.params.get("tags"):
try:
elb_tags = connection.describe_tags(ResourceArns=[elb['LoadBalancerArn']])['TagDescriptions'][0]['Tags']
except ClientError as e:
module.fail_json(msg=e.message, exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response))
# Delete necessary tags
tags_need_modify, tags_to_delete = compare_aws_tags(boto3_tag_list_to_ansible_dict(elb_tags), boto3_tag_list_to_ansible_dict(params['Tags']),
purge_tags)
if tags_to_delete:
try:
connection.remove_tags(ResourceArns=[elb['LoadBalancerArn']], TagKeys=tags_to_delete)
except ClientError as e:
module.fail_json(msg=e.message, exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response))
changed = True
# Add/update tags
if tags_need_modify:
try:
connection.add_tags(ResourceArns=[elb['LoadBalancerArn']], Tags=params['Tags'])
except ClientError as e:
module.fail_json(msg=e.message, exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response))
changed = True
else:
try:
elb = connection.create_load_balancer(**params)['LoadBalancers'][0]
changed = True
new_load_balancer = True
except ClientError as e:
module.fail_json(msg=e.message, exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response))
if module.params.get("wait"):
status_achieved, new_elb = wait_for_status(connection, module, elb['LoadBalancerArn'], 'active')
# Now set ELB attributes. Use try statement here so we can remove the ELB if this stage fails
update_attributes = []
# Get current attributes
current_elb_attributes = get_elb_attributes(connection, module, elb['LoadBalancerArn'])
if access_logs_enabled and current_elb_attributes['access_logs_s3_enabled'] != "true":
update_attributes.append({'Key': 'access_logs.s3.enabled', 'Value': "true"})
if not access_logs_enabled and current_elb_attributes['access_logs_s3_enabled'] != "false":
update_attributes.append({'Key': 'access_logs.s3.enabled', 'Value': 'false'})
if access_logs_s3_bucket is not None and access_logs_s3_bucket != current_elb_attributes['access_logs_s3_bucket']:
update_attributes.append({'Key': 'access_logs.s3.bucket', 'Value': access_logs_s3_bucket})
if access_logs_s3_prefix is not None and access_logs_s3_prefix != current_elb_attributes['access_logs_s3_prefix']:
update_attributes.append({'Key': 'access_logs.s3.prefix', 'Value': access_logs_s3_prefix})
if deletion_protection and current_elb_attributes['deletion_protection_enabled'] != "true":
update_attributes.append({'Key': 'deletion_protection.enabled', 'Value': "true"})
if not deletion_protection and current_elb_attributes['deletion_protection_enabled'] != "false":
update_attributes.append({'Key': 'deletion_protection.enabled', 'Value': "false"})
if idle_timeout is not None and str(idle_timeout) != current_elb_attributes['idle_timeout_timeout_seconds']:
update_attributes.append({'Key': 'idle_timeout.timeout_seconds', 'Value': str(idle_timeout)})
if update_attributes:
try:
connection.modify_load_balancer_attributes(LoadBalancerArn=elb['LoadBalancerArn'], Attributes=update_attributes)
changed = True
except ClientError as e:
# Something went wrong setting attributes. If this ELB was created during this task, delete it to leave a consistent state
if new_load_balancer:
connection.delete_load_balancer(LoadBalancerArn=elb['LoadBalancerArn'])
module.fail_json(msg=e.message, exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response))
# Now, if required, set ELB listeners. Use try statement here so we can remove the ELB if this stage fails
try:
listener_changed = create_or_update_elb_listeners(connection, module, elb)
if listener_changed:
changed = True
except ClientError as e:
# Something went wrong setting listeners. If this ELB was created during this task, delete it to leave a consistent state
if new_load_balancer:
connection.delete_load_balancer(LoadBalancerArn=elb['LoadBalancerArn'])
module.fail_json(msg=e.message, exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response))
# Get the ELB again # Get the ELB again
elb = get_elb(connection, module) elb_obj.update()
# Get the ELB listeners again # Get the ELB listeners again
elb['listeners'] = get_elb_listeners(connection, module, elb['LoadBalancerArn']) listeners_obj.update()
# For each listener, get listener rules # Update the ELB attributes
for listener in elb['listeners']: elb_obj.update_elb_attributes()
listener['rules'] = get_listener_rules(connection, module, listener['ListenerArn'])
# Get the ELB attributes again
elb.update(get_elb_attributes(connection, module, elb['LoadBalancerArn']))
# Convert to snake_case # Convert to snake_case and merge in everything we want to return to the user
snaked_elb = camel_dict_to_snake_dict(elb) snaked_elb = camel_dict_to_snake_dict(elb_obj.elb)
snaked_elb.update(camel_dict_to_snake_dict(elb_obj.elb_attributes))
# Get the tags of the ELB snaked_elb['listeners'] = []
elb_tags = connection.describe_tags(ResourceArns=[elb['LoadBalancerArn']])['TagDescriptions'][0]['Tags'] for listener in listeners_obj.current_listeners:
snaked_elb['tags'] = boto3_tag_list_to_ansible_dict(elb_tags) # For each listener, get listener rules
listener['rules'] = get_elb_listener_rules(elb_obj.connection, elb_obj.module, listener['ListenerArn'])
snaked_elb['listeners'].append(camel_dict_to_snake_dict(listener))
module.exit_json(changed=changed, **snaked_elb) # Change tags to ansible friendly dict
snaked_elb['tags'] = boto3_tag_list_to_ansible_dict(snaked_elb['tags'])
elb_obj.module.exit_json(changed=elb_obj.changed, **snaked_elb)
def delete_elb(connection, module):
changed = False def delete_elb(elb_obj):
elb = get_elb(connection, module)
if elb: if elb_obj.elb:
try: elb_obj.delete()
connection.delete_load_balancer(LoadBalancerArn=elb['LoadBalancerArn'])
changed = True
except ClientError as e:
module.fail_json(msg=e.message, exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response))
except NoCredentialsError as e:
module.fail_json(msg="AWS authentication problem. " + e.message, exception=traceback.format_exc())
module.exit_json(changed=changed) elb_obj.module.exit_json(changed=elb_obj.changed)
def main(): def main():
@ -969,9 +489,19 @@ def main():
access_logs_enabled=dict(type='bool'), access_logs_enabled=dict(type='bool'),
access_logs_s3_bucket=dict(type='str'), access_logs_s3_bucket=dict(type='str'),
access_logs_s3_prefix=dict(type='str'), access_logs_s3_prefix=dict(type='str'),
deletion_protection=dict(default=False, type='bool'), deletion_protection=dict(type='bool'),
idle_timeout=dict(type='int'), idle_timeout=dict(type='int'),
listeners=dict(type='list'), listeners=dict(type='list',
elements='dict',
options=dict(
Protocol=dict(type='str', required=True),
Port=dict(type='int', required=True),
SslPolicy=dict(type='str'),
Certificates=dict(type='list'),
DefaultActions=dict(type='list', required=True),
Rules=dict(type='list')
)
),
name=dict(required=True, type='str'), name=dict(required=True, type='str'),
purge_listeners=dict(default=True, type='bool'), purge_listeners=dict(default=True, type='bool'),
purge_tags=dict(default=True, type='bool'), purge_tags=dict(default=True, type='bool'),
@ -981,11 +511,11 @@ def main():
state=dict(choices=['present', 'absent'], type='str'), state=dict(choices=['present', 'absent'], type='str'),
tags=dict(default={}, type='dict'), tags=dict(default={}, type='dict'),
wait_timeout=dict(type='int'), wait_timeout=dict(type='int'),
wait=dict(type='bool') wait=dict(default=False, type='bool')
) )
) )
module = AnsibleModule(argument_spec=argument_spec, module = AnsibleAWSModule(argument_spec=argument_spec,
required_if=[ required_if=[
('state', 'present', ['subnets', 'security_groups']) ('state', 'present', ['subnets', 'security_groups'])
], ],
@ -999,30 +529,24 @@ def main():
if listeners is not None: if listeners is not None:
for listener in listeners: for listener in listeners:
for key in listener.keys(): for key in listener.keys():
if key not in ['Protocol', 'Port', 'SslPolicy', 'Certificates', 'DefaultActions', 'Rules']: if key == 'Protocol' and listener[key] == 'HTTPS':
module.fail_json(msg="listeners parameter contains invalid dict keys. Should be one of 'Protocol', " if listener.get('SslPolicy') is None:
"'Port', 'SslPolicy', 'Certificates', 'DefaultActions', 'Rules'.") module.fail_json(msg="'SslPolicy' is a required listener dict key when Protocol = HTTPS")
# Make sure Port is always an integer
elif key == 'Port':
listener[key] = int(listener[key])
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) if listener.get('Certificates') is None:
module.fail_json(msg="'Certificates' is a required listener dict key when Protocol = HTTPS")
if region: connection = module.client('elbv2')
connection = boto3_conn(module, conn_type='client', resource='elbv2', region=region, endpoint=ec2_url, **aws_connect_params) connection_ec2 = module.client('ec2')
connection_ec2 = boto3_conn(module, conn_type='client', resource='ec2', region=region, endpoint=ec2_url, **aws_connect_params)
else:
module.fail_json(msg="region must be specified")
state = module.params.get("state") state = module.params.get("state")
elb = ApplicationLoadBalancer(connection, connection_ec2, module)
if state == 'present': if state == 'present':
create_or_update_elb(connection, connection_ec2, module) create_or_update_elb(elb)
else: else:
delete_elb(connection, module) delete_elb(elb)
if __name__ == '__main__': if __name__ == '__main__':
main() main()

@ -0,0 +1,6 @@
---
# load balancer and target group names have to be less than 32 characters
# the 8 digit identifier at the end of resource_prefix helps determine during which test something
# was created and allows tests to be run in parallel
alb_name: "my-alb-{{ resource_prefix | regex_search('([0-9]+)$') }}"
tg_name: "my-tg-{{ resource_prefix | regex_search('([0-9]+)$') }}"

@ -0,0 +1,3 @@
dependencies:
- prepare_tests
- setup_ec2

@ -0,0 +1,204 @@
- block:
- name: set connection information for all tasks
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 VPC
ec2_vpc_net:
cidr_block: 10.228.228.0/22
name: "{{ resource_prefix }}_vpc"
state: present
<<: *aws_connection_info
register: vpc
- name: create internet gateway
ec2_vpc_igw:
vpc_id: "{{ vpc.vpc.id }}"
state: present
tags:
Name: "{{ resource_prefix }}"
<<: *aws_connection_info
register: igw
- name: create public subnet
ec2_vpc_subnet:
cidr: "{{ item.cidr }}"
az: "{{ aws_region}}{{ item.az }}"
vpc_id: "{{ vpc.vpc.id }}"
state: present
tags:
Public: "{{ item.public|string }}"
Name: "{{ item.public|ternary('public', 'private') }}-{{ item.az }}"
<<: *aws_connection_info
with_items:
- cidr: 10.228.228.0/24
az: "a"
public: "True"
- cidr: 10.228.229.0/24
az: "b"
public: "True"
- cidr: 10.228.230.0/24
az: "a"
public: "False"
- cidr: 10.228.231.0/24
az: "b"
public: "False"
register: subnets
- ec2_vpc_subnet_facts:
filters:
vpc-id: "{{ vpc.vpc.id }}"
<<: *aws_connection_info
register: vpc_subnets
- name: create list of subnet ids
set_fact:
alb_subnets: "{{ vpc_subnets|json_query('subnets[?tags.Public == `True`].id') }}"
private_subnets: "{{ vpc_subnets|json_query('subnets[?tags.Public != `True`].id') }}"
- name: create a route table
ec2_vpc_route_table:
vpc_id: "{{ vpc.vpc.id }}"
<<: *aws_connection_info
tags:
Name: igw-route
Created: "{{ resource_prefix }}"
subnets: "{{ alb_subnets + private_subnets }}"
routes:
- dest: 0.0.0.0/0
gateway_id: "{{ igw.gateway_id }}"
register: route_table
- ec2_group:
name: "{{ resource_prefix }}"
description: "security group for Ansible ALB integration tests"
state: present
vpc_id: "{{ vpc.vpc.id }}"
rules:
- proto: tcp
from_port: 1
to_port: 65535
cidr_ip: 0.0.0.0/0
<<: *aws_connection_info
register: sec_group
- name: create a target group for testing
elb_target_group:
name: "{{ tg_name }}"
protocol: http
port: 80
vpc_id: "{{ vpc.vpc.id }}"
state: present
<<: *aws_connection_info
register: tg
- include_tasks: test_alb_bad_listener_options.yml
- include_tasks: test_alb_tags.yml
- include_tasks: test_creating_alb.yml
- include_tasks: test_alb_with_asg.yml
- include_tasks: test_modifying_alb_listeners.yml
- include_tasks: test_deleting_alb.yml
always:
#############################################################################
# TEAR DOWN STARTS HERE
#############################################################################
- name: destroy ALB
elb_application_lb:
name: "{{ alb_name }}"
state: absent
wait: yes
wait_timeout: 600
<<: *aws_connection_info
ignore_errors: yes
- name: destroy target group if it was created
elb_target_group:
name: "{{ tg_name }}"
protocol: http
port: 80
vpc_id: "{{ vpc.vpc.id }}"
state: absent
wait: yes
wait_timeout: 600
<<: *aws_connection_info
register: remove_tg
retries: 5
delay: 3
until: remove_tg is success
when: tg is defined
ignore_errors: yes
- name: destroy sec group
ec2_group:
name: "{{ sec_group.group_name }}"
description: "security group for Ansible ALB integration tests"
state: absent
vpc_id: "{{ vpc.vpc.id }}"
<<: *aws_connection_info
register: remove_sg
retries: 10
delay: 5
until: remove_sg is success
ignore_errors: yes
- name: remove route table
ec2_vpc_route_table:
vpc_id: "{{ vpc.vpc.id }}"
route_table_id: "{{ route_table.route_table.route_table_id }}"
lookup: id
state: absent
<<: *aws_connection_info
register: remove_rt
retries: 10
delay: 5
until: remove_rt is success
ignore_errors: yes
- name: destroy subnets
ec2_vpc_subnet:
cidr: "{{ item.cidr }}"
vpc_id: "{{ vpc.vpc.id }}"
state: absent
<<: *aws_connection_info
register: remove_subnet
retries: 10
delay: 5
until: remove_subnet is success
with_items:
- cidr: 10.228.228.0/24
- cidr: 10.228.229.0/24
- cidr: 10.228.230.0/24
- cidr: 10.228.231.0/24
ignore_errors: yes
- name: destroy internet gateway
ec2_vpc_igw:
vpc_id: "{{ vpc.vpc.id }}"
tags:
Name: "{{ resource_prefix }}"
state: absent
<<: *aws_connection_info
register: remove_igw
retries: 10
delay: 5
until: remove_igw is success
ignore_errors: yes
- name: destroy VPC
ec2_vpc_net:
cidr_block: 10.228.228.0/22
name: "{{ resource_prefix }}_vpc"
state: absent
<<: *aws_connection_info
register: remove_vpc
retries: 10
delay: 5
until: remove_vpc is success
ignore_errors: yes

@ -0,0 +1,71 @@
- block:
- name: set connection information for all tasks
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: test creating an ALB with invalid listener options
elb_application_lb:
name: "{{ alb_name }}"
subnets: "{{ alb_subnets }}"
security_groups: "{{ sec_group.group_id }}"
state: present
listeners:
- Protocol: HTTPS
Port: 80
DefaultActions:
- Type: forward
TargetGroupName: "{{ tg_name }}"
<<: *aws_connection_info
ignore_errors: yes
register: alb
- assert:
that:
- alb is failed
- alb.msg.startswith("'SslPolicy' is a required listener dict key when Protocol = HTTPS")
- name: test creating an ALB without providing required listener options
elb_application_lb:
name: "{{ alb_name }}"
subnets: "{{ alb_subnets }}"
security_groups: "{{ sec_group.group_id }}"
state: present
listeners:
- Port: 80
<<: *aws_connection_info
ignore_errors: yes
register: alb
- assert:
that:
- alb is failed
- '"missing required arguments" in alb.msg'
- '"Protocol" in alb.msg'
- '"DefaultActions" in alb.msg'
- name: test creating an ALB providing an invalid listener option type
elb_application_lb:
name: "{{ alb_name }}"
subnets: "{{ alb_subnets }}"
security_groups: "{{ sec_group.group_id }}"
state: present
listeners:
- Protocol: HTTP
Port: "bad type"
DefaultActions:
- Type: forward
TargetGroupName: "{{ tg_name }}"
<<: *aws_connection_info
ignore_errors: yes
register: alb
- assert:
that:
- alb is failed
- "'unable to convert to int' in alb.msg"

@ -0,0 +1,93 @@
- block:
- name: set connection information for all tasks
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 ALB with no listeners
elb_application_lb:
name: "{{ alb_name }}"
subnets: "{{ alb_subnets }}"
security_groups: "{{ sec_group.group_id }}"
state: present
<<: *aws_connection_info
register: alb
- assert:
that:
- alb.changed
- name: re-create ALB with no listeners
elb_application_lb:
name: "{{ alb_name }}"
subnets: "{{ alb_subnets }}"
security_groups: "{{ sec_group.group_id }}"
state: present
<<: *aws_connection_info
register: alb
- assert:
that:
- not alb.changed
- name: add tags to ALB
elb_application_lb:
name: "{{ alb_name }}"
subnets: "{{ alb_subnets }}"
security_groups: "{{ sec_group.group_id }}"
state: present
tags:
created_by: "ALB test {{ resource_prefix }}"
<<: *aws_connection_info
register: alb
- assert:
that:
- alb.changed
- 'alb.tags == {"created_by": "ALB test {{ resource_prefix }}"}'
- name: remove tags from ALB
elb_application_lb:
name: "{{ alb_name }}"
subnets: "{{ alb_subnets }}"
security_groups: "{{ sec_group.group_id }}"
state: present
tags: {}
<<: *aws_connection_info
register: alb
- assert:
that:
- alb.changed
- not alb.tags
- name: test idempotence
elb_application_lb:
name: "{{ alb_name }}"
subnets: "{{ alb_subnets }}"
security_groups: "{{ sec_group.group_id }}"
state: present
tags: {}
<<: *aws_connection_info
register: alb
- assert:
that:
- not alb.changed
- not alb.tags
- name: destroy ALB with no listeners
elb_application_lb:
name: "{{ alb_name }}"
state: absent
<<: *aws_connection_info
register: alb
- assert:
that:
- alb.changed

@ -0,0 +1,88 @@
- block:
- name: set connection information for all tasks
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
- ec2_ami_facts:
<<: *aws_connection_info
filters:
architecture: x86_64
virtualization-type: hvm
root-device-type: ebs
name: "amzn-ami-hvm*"
register: amis
- set_fact:
latest_amazon_linux: "{{ amis.images | sort(attribute='creation_date') | last }}"
- ec2_asg:
<<: *aws_connection_info
state: absent
name: "{{ resource_prefix }}-webservers"
wait_timeout: 900
- ec2_lc:
<<: *aws_connection_info
name: "{{ resource_prefix }}-web-lcfg"
state: absent
- name: Create launch config for testing
ec2_lc:
<<: *aws_connection_info
name: "{{ resource_prefix }}-web-lcfg"
assign_public_ip: true
image_id: "{{ latest_amazon_linux.image_id }}"
security_groups: "{{ sec_group.group_id }}"
instance_type: t2.medium
user_data: |
#!/bin/bash
set -x
yum update -y --nogpgcheck
yum install -y --nogpgcheck httpd
echo "Hello Ansiblings!" >> /var/www/html/index.html
service httpd start
volumes:
- device_name: /dev/xvda
volume_size: 10
volume_type: gp2
delete_on_termination: true
- name: Create autoscaling group for app server fleet
ec2_asg:
<<: *aws_connection_info
name: "{{ resource_prefix }}-webservers"
vpc_zone_identifier: "{{ alb_subnets }}"
launch_config_name: "{{ resource_prefix }}-web-lcfg"
termination_policies:
- OldestLaunchConfiguration
- Default
health_check_period: 600
health_check_type: EC2
replace_all_instances: true
min_size: 0
max_size: 2
desired_capacity: 1
wait_for_instances: true
target_group_arns:
- "{{ tg.target_group_arn }}"
always:
- ec2_asg:
<<: *aws_connection_info
state: absent
name: "{{ resource_prefix }}-webservers"
wait_timeout: 900
ignore_errors: yes
- ec2_lc:
<<: *aws_connection_info
name: "{{ resource_prefix }}-web-lcfg"
state: absent
ignore_errors: yes

@ -0,0 +1,52 @@
- block:
- name: set connection information for all tasks
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 ALB with a listener
elb_application_lb:
name: "{{ alb_name }}"
subnets: "{{ alb_subnets }}"
security_groups: "{{ sec_group.group_id }}"
state: present
listeners:
- Protocol: HTTP
Port: 80
DefaultActions:
- Type: forward
TargetGroupName: "{{ tg_name }}"
<<: *aws_connection_info
register: alb
- assert:
that:
- alb.changed
- alb.listeners|length == 1
- alb.listeners[0].rules|length == 1
- name: test idempotence creating ALB with a listener
elb_application_lb:
name: "{{ alb_name }}"
subnets: "{{ alb_subnets }}"
security_groups: "{{ sec_group.group_id }}"
state: present
listeners:
- Protocol: HTTP
Port: 80
DefaultActions:
- Type: forward
TargetGroupName: "{{ tg_name }}"
<<: *aws_connection_info
register: alb
- assert:
that:
- not alb.changed
- alb.listeners|length == 1
- alb.listeners[0].rules|length == 1

@ -0,0 +1,52 @@
- block:
- name: set connection information for all tasks
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: destroy ALB with listener
elb_application_lb:
name: "{{ alb_name }}"
subnets: "{{ alb_subnets }}"
security_groups: "{{ sec_group.group_id }}"
state: absent
listeners:
- Protocol: HTTP
Port: 80
DefaultActions:
- Type: forward
TargetGroupName: "{{ tg_name }}"
<<: *aws_connection_info
wait: yes
wait_timeout: 300
register: alb
- assert:
that:
- alb.changed
- name: test idempotence
elb_application_lb:
name: "{{ alb_name }}"
subnets: "{{ alb_subnets }}"
security_groups: "{{ sec_group.group_id }}"
state: absent
listeners:
- Protocol: HTTP
Port: 80
DefaultActions:
- Type: forward
TargetGroupName: "{{ tg_name }}"
<<: *aws_connection_info
wait: yes
wait_timeout: 300
register: alb
- assert:
that:
- not alb.changed

@ -92,9 +92,7 @@ lib/ansible/modules/cloud/amazon/elasticache.py E324
lib/ansible/modules/cloud/amazon/elasticache.py E326 lib/ansible/modules/cloud/amazon/elasticache.py E326
lib/ansible/modules/cloud/amazon/elasticache_parameter_group.py E326 lib/ansible/modules/cloud/amazon/elasticache_parameter_group.py E326
lib/ansible/modules/cloud/amazon/elasticache_subnet_group.py E324 lib/ansible/modules/cloud/amazon/elasticache_subnet_group.py E324
lib/ansible/modules/cloud/amazon/elb_application_lb.py E322
lib/ansible/modules/cloud/amazon/elb_application_lb.py E324 lib/ansible/modules/cloud/amazon/elb_application_lb.py E324
lib/ansible/modules/cloud/amazon/elb_application_lb.py E325
lib/ansible/modules/cloud/amazon/elb_classic_lb_facts.py E323 lib/ansible/modules/cloud/amazon/elb_classic_lb_facts.py E323
lib/ansible/modules/cloud/amazon/elb_instance.py E326 lib/ansible/modules/cloud/amazon/elb_instance.py E326
lib/ansible/modules/cloud/amazon/elb_target.py E327 lib/ansible/modules/cloud/amazon/elb_target.py E327

@ -1,154 +0,0 @@
#
# (c) 2017 Michael Tinning
#
# 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)
import json
from copy import deepcopy
import pytest
from ansible.module_utils._text import to_bytes
from ansible.module_utils import basic
from ansible.module_utils.ec2 import HAS_BOTO3
if not HAS_BOTO3:
pytestmark = pytest.mark.skip("test_elb_application_lb.py requires the `boto3` and `botocore` modules")
import ansible.modules.cloud.amazon.elb_application_lb as elb_module
@pytest.fixture
def listener():
return {
'Protocol': 'HTTP',
'Port': 80,
'DefaultActions': [{
'Type': 'forward',
'TargetGroupName': 'target-group'
}],
'Rules': [{
'Conditions': [{
'Field': 'host-header',
'Values': [
'www.example.com'
]
}],
'Priority': 1,
'Actions': [{
'TargetGroupName': 'other-target-group',
'Type': 'forward'
}]
}]
}
@pytest.fixture
def compare_listeners(mocker):
return mocker.Mock()
@pytest.fixture
def ensure_listeners(mocker):
ensure_listeners_mock = mocker.Mock()
ensure_listeners_mock.return_value = []
return ensure_listeners_mock
@pytest.fixture
def compare_rules(mocker):
compare_rules_mock = mocker.Mock()
compare_rules_mock.return_value = ([], [], [])
return compare_rules_mock
@pytest.fixture
def get_elb_listeners(mocker):
get_elb_listeners_mock = mocker.Mock()
get_elb_listeners_mock.return_value = []
return get_elb_listeners_mock
@pytest.fixture
def elb(mocker, monkeypatch, compare_listeners, ensure_listeners, compare_rules, get_elb_listeners):
monkeypatch.setattr(elb_module, "ensure_listeners_default_action_has_arn", ensure_listeners)
monkeypatch.setattr(elb_module, "get_elb_listeners", get_elb_listeners)
monkeypatch.setattr(elb_module, "ensure_rules_action_has_arn", mocker.Mock())
monkeypatch.setattr(elb_module, "get_listener", mocker.Mock())
monkeypatch.setattr(elb_module, "compare_rules", compare_rules)
monkeypatch.setattr(elb_module, "compare_listeners", compare_listeners)
return elb_module
@pytest.fixture
def created_listener(mocker, listener):
return {
'Port': listener['Port'],
'ListenerArn': 'new-listener-arn'
}
@pytest.fixture
def connection(mocker, created_listener):
connection_mock = mocker.Mock()
connection_mock.create_listener.return_value = {
'Listeners': [created_listener]
}
return connection_mock
@pytest.fixture
def existing_elb():
return {'LoadBalancerArn': 'fake'}
def test_create_listeners_called_with_correct_args(mocker, connection, listener, elb, compare_listeners, existing_elb):
compare_listeners.return_value = ([listener], [], [])
elb.create_or_update_elb_listeners(connection, mocker.Mock(), existing_elb)
connection.create_listener.assert_called_once_with(
Protocol=listener['Protocol'],
Port=listener['Port'],
DefaultActions=listener['DefaultActions'],
LoadBalancerArn=existing_elb['LoadBalancerArn']
)
def test_modify_listeners_called_with_correct_args(mocker, connection, listener, elb, compare_listeners, existing_elb):
# In the case of modify listener, LoadBalancerArn is set in compare_listeners
listener['LoadBalancerArn'] = existing_elb['LoadBalancerArn']
compare_listeners.return_value = ([], [listener], [])
elb.create_or_update_elb_listeners(connection, mocker.Mock(), existing_elb)
connection.modify_listener.assert_called_once_with(
Protocol=listener['Protocol'],
Port=listener['Port'],
DefaultActions=listener['DefaultActions'],
LoadBalancerArn=existing_elb['LoadBalancerArn']
)
def test_compare_rules_called_with_new_listener(
mocker,
connection,
listener,
elb,
compare_listeners,
ensure_listeners,
compare_rules,
existing_elb,
created_listener
):
compare_listeners.return_value = ([listener], [], [])
listener_from_ensure_listeners = deepcopy(listener)
ensure_listeners.return_value = [listener_from_ensure_listeners]
elb.create_or_update_elb_listeners(connection, mocker.Mock(), existing_elb)
(_conn, _module, current_listeners, _listener), _kwargs = compare_rules.call_args
assert created_listener in current_listeners
Loading…
Cancel
Save