From b5cffe8ced3c06c5c1542e37c382c74d5f61f3eb Mon Sep 17 00:00:00 2001 From: Rob Date: Sat, 5 May 2018 06:22:00 +1000 Subject: [PATCH] [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 --- lib/ansible/module_utils/aws/elb_utils.py | 110 +++ lib/ansible/module_utils/aws/elbv2.py | 757 ++++++++++++++++++ .../cloud/amazon/elb_application_lb.py | 736 +++-------------- .../targets/elb_application_lb/aliases | 2 + .../elb_application_lb/defaults/main.yml | 6 + .../targets/elb_application_lb/meta/main.yml | 3 + .../targets/elb_application_lb/tasks/main.yml | 204 +++++ .../tasks/test_alb_bad_listener_options.yml | 71 ++ .../tasks/test_alb_tags.yml | 93 +++ .../tasks/test_alb_with_asg.yml | 88 ++ .../tasks/test_creating_alb.yml | 52 ++ .../tasks/test_deleting_alb.yml | 52 ++ test/sanity/validate-modules/ignore.txt | 2 - .../cloud/amazon/test_elb_application_lb.py | 154 ---- 14 files changed, 1568 insertions(+), 762 deletions(-) create mode 100644 lib/ansible/module_utils/aws/elb_utils.py create mode 100644 lib/ansible/module_utils/aws/elbv2.py create mode 100644 test/integration/targets/elb_application_lb/aliases create mode 100644 test/integration/targets/elb_application_lb/defaults/main.yml create mode 100644 test/integration/targets/elb_application_lb/meta/main.yml create mode 100644 test/integration/targets/elb_application_lb/tasks/main.yml create mode 100644 test/integration/targets/elb_application_lb/tasks/test_alb_bad_listener_options.yml create mode 100644 test/integration/targets/elb_application_lb/tasks/test_alb_tags.yml create mode 100644 test/integration/targets/elb_application_lb/tasks/test_alb_with_asg.yml create mode 100644 test/integration/targets/elb_application_lb/tasks/test_creating_alb.yml create mode 100644 test/integration/targets/elb_application_lb/tasks/test_deleting_alb.yml delete mode 100644 test/units/modules/cloud/amazon/test_elb_application_lb.py diff --git a/lib/ansible/module_utils/aws/elb_utils.py b/lib/ansible/module_utils/aws/elb_utils.py new file mode 100644 index 00000000000..5af0f25b169 --- /dev/null +++ b/lib/ansible/module_utils/aws/elb_utils.py @@ -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 diff --git a/lib/ansible/module_utils/aws/elbv2.py b/lib/ansible/module_utils/aws/elbv2.py new file mode 100644 index 00000000000..2f4d97e7a60 --- /dev/null +++ b/lib/ansible/module_utils/aws/elbv2.py @@ -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 diff --git a/lib/ansible/modules/cloud/amazon/elb_application_lb.py b/lib/ansible/modules/cloud/amazon/elb_application_lb.py index 3485af2a8c9..9675dacd598 100644 --- a/lib/ansible/modules/cloud/amazon/elb_application_lb.py +++ b/lib/ansible/modules/cloud/amazon/elb_application_lb.py @@ -101,6 +101,17 @@ options: description: - A dictionary of one or more tags to assign to the load balancer. 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: - aws - ec2 @@ -342,623 +353,132 @@ vpc_id: type: string 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 +from ansible.module_utils.aws.core import AnsibleAWSModule +from ansible.module_utils.ec2 import boto3_conn, get_aws_connection_info, camel_dict_to_snake_dict, ec2_argument_spec, \ + 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 +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 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: - if current_condition['Field'] == condition['Field'] and current_condition['Values'][0] == condition['Values'][0]: - condition_found = True - break - - return condition_found - - -def compare_rule(current_rule, new_rule): - """ - 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 - - return listeners_to_add, listeners_to_modify, listeners_to_delete - - -def compare_rules(connection, module, current_listeners, listener): - """ - 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 - :param module: Ansible module object - :param current_listeners: list of listeners currently associated with the ELB - :param listener: dict object of a listener passed by the user - :return: - """ - - # Run through listeners looking for a match (by port) to get the ARN - for current_listener in current_listeners: - if current_listener['Port'] == listener['Port']: - 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: - current_rules = [] - - 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 - - -def create_or_update_elb_listeners(connection, module, elb): - """Create or update ELB listeners. Return true if changed, else false""" - - listener_changed = False - # 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? - current_listeners = get_elb_listeners(connection, module, elb['LoadBalancerArn']) - - listeners_to_add, listeners_to_modify, listeners_to_delete = compare_listeners(connection, module, current_listeners, deepcopy(listeners), purge_listeners) - - # Add listeners - for listener_to_add in listeners_to_add: - try: - listener_to_add['LoadBalancerArn'] = elb['LoadBalancerArn'] - # Rules is not a valid parameter for create_listener - 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 - for listener_to_modify in listeners_to_modify: - try: - # Rules is not a valid parameter for modify_listener - if 'Rules' in listener_to_modify: - 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 - for listener_to_delete in listeners_to_delete: - try: - 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 - for listener in deepcopy(listeners): - if 'Rules' in listener: - # Ensure rules are using Target Group ARN not name - 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 - looked_up_listener = get_listener(connection, module, elb['LoadBalancerArn'], listener['Port']) - - # Delete rules - for rule in rules_to_delete: - try: - connection.delete_rule(RuleArn=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)) - - # Add rules - for rule in rules_to_add: - try: - rule['ListenerArn'] = looked_up_listener['ListenerArn'] - rule['Priority'] = int(rule['Priority']) - 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 - for rule in rules_to_modify: - try: - del rule['Priority'] - connection.modify_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)) - - return listener_changed - - -def create_or_update_elb(connection, connection_ec2, module): +def create_or_update_elb(elb_obj): """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: + if elb_obj.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 + if not elb_obj.compare_subnets(): + elb_obj.modify_subnets() # 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 + if not elb_obj.compare_security_groups(): + elb_obj.modify_security_groups() # 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)) + if elb_obj.tags is not None: # 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) + tags_need_modify, tags_to_delete = compare_aws_tags(boto3_tag_list_to_ansible_dict(elb_obj.elb['tags']), + boto3_tag_list_to_ansible_dict(elb_obj.tags), elb_obj.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 + elb_obj.delete_tags(tags_to_delete) # 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 + elb_obj.modify_tags() 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)) + # Create load balancer + elb_obj.create_elb() - if module.params.get("wait"): - status_achieved, new_elb = wait_for_status(connection, module, elb['LoadBalancerArn'], 'active') + # ELB attributes + elb_obj.update_elb_attributes() + elb_obj.modify_elb_attributes() - # Now set ELB attributes. Use try statement here so we can remove the ELB if this stage fails - update_attributes = [] + # Listeners + listeners_obj = ELBListeners(elb_obj.connection, elb_obj.module, elb_obj.elb['LoadBalancerArn']) - # Get current attributes - current_elb_attributes = get_elb_attributes(connection, module, elb['LoadBalancerArn']) + listeners_to_add, listeners_to_modify, listeners_to_delete = listeners_obj.compare_listeners() - 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)}) + # Delete listeners + for listener_to_delete in listeners_to_delete: + listener_obj = ELBListener(elb_obj.connection, elb_obj.module, listener_to_delete, elb_obj.elb['LoadBalancerArn']) + listener_obj.delete() + listeners_obj.changed = True - 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)) + # Add listeners + for listener_to_add in listeners_to_add: + listener_obj = ELBListener(elb_obj.connection, elb_obj.module, listener_to_add, elb_obj.elb['LoadBalancerArn']) + listener_obj.add() + listeners_obj.changed = True - # 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)) + # Modify listeners + for listener_to_modify in listeners_to_modify: + listener_obj = ELBListener(elb_obj.connection, elb_obj.module, listener_to_modify, elb_obj.elb['LoadBalancerArn']) + listener_obj.modify() + listeners_obj.changed = True + + # If listeners changed, mark ELB as changed + if listeners_obj.changed: + elb_obj.changed = True + + # Rules of each listener + for listener in listeners_obj.listeners: + if 'Rules' in listener: + rules_obj = ELBListenerRules(elb_obj.connection, elb_obj.module, elb_obj.elb['LoadBalancerArn'], listener['Rules'], listener['Port']) + + rules_to_add, rules_to_modify, rules_to_delete = rules_obj.compare_rules() + + # Delete rules + for rule in rules_to_delete: + rule_obj = ELBListenerRule(elb_obj.connection, elb_obj.module, {'RuleArn': rule}, rules_obj.listener_arn) + rule_obj.delete() + elb_obj.changed = True + + # Add rules + for rule in rules_to_add: + rule_obj = ELBListenerRule(elb_obj.connection, elb_obj.module, rule, rules_obj.listener_arn) + rule_obj.create() + elb_obj.changed = True + + # Modify rules + for rule in rules_to_modify: + rule_obj = ELBListenerRule(elb_obj.connection, elb_obj.module, rule, rules_obj.listener_arn) + rule_obj.modify() + elb_obj.changed = True # Get the ELB again - elb = get_elb(connection, module) + elb_obj.update() # Get the ELB listeners again - elb['listeners'] = get_elb_listeners(connection, module, elb['LoadBalancerArn']) + listeners_obj.update() - # For each listener, get listener rules - for listener in elb['listeners']: - listener['rules'] = get_listener_rules(connection, module, listener['ListenerArn']) + # Update the ELB attributes + elb_obj.update_elb_attributes() - # Get the ELB attributes again - elb.update(get_elb_attributes(connection, module, elb['LoadBalancerArn'])) + # Convert to snake_case and merge in everything we want to return to the user + snaked_elb = camel_dict_to_snake_dict(elb_obj.elb) + snaked_elb.update(camel_dict_to_snake_dict(elb_obj.elb_attributes)) + snaked_elb['listeners'] = [] + for listener in listeners_obj.current_listeners: + # 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)) - # Convert to snake_case - snaked_elb = camel_dict_to_snake_dict(elb) + # Change tags to ansible friendly dict + snaked_elb['tags'] = boto3_tag_list_to_ansible_dict(snaked_elb['tags']) - # Get the tags of the ELB - elb_tags = connection.describe_tags(ResourceArns=[elb['LoadBalancerArn']])['TagDescriptions'][0]['Tags'] - snaked_elb['tags'] = boto3_tag_list_to_ansible_dict(elb_tags) - - module.exit_json(changed=changed, **snaked_elb) + elb_obj.module.exit_json(changed=elb_obj.changed, **snaked_elb) -def delete_elb(connection, module): +def delete_elb(elb_obj): - changed = False - elb = get_elb(connection, module) + if elb_obj.elb: + elb_obj.delete() - if elb: - try: - 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(): @@ -969,9 +489,19 @@ def main(): access_logs_enabled=dict(type='bool'), access_logs_s3_bucket=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'), - 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'), purge_listeners=dict(default=True, type='bool'), purge_tags=dict(default=True, type='bool'), @@ -981,48 +511,42 @@ def main(): state=dict(choices=['present', 'absent'], type='str'), tags=dict(default={}, type='dict'), wait_timeout=dict(type='int'), - wait=dict(type='bool') + wait=dict(default=False, type='bool') ) ) - module = AnsibleModule(argument_spec=argument_spec, - required_if=[ - ('state', 'present', ['subnets', 'security_groups']) - ], - required_together=( - ['access_logs_enabled', 'access_logs_s3_bucket', 'access_logs_s3_prefix'] - ) - ) + module = AnsibleAWSModule(argument_spec=argument_spec, + required_if=[ + ('state', 'present', ['subnets', 'security_groups']) + ], + required_together=( + ['access_logs_enabled', 'access_logs_s3_bucket', 'access_logs_s3_prefix'] + ) + ) # Quick check of listeners parameters listeners = module.params.get("listeners") if listeners is not None: for listener in listeners: for key in listener.keys(): - if key not in ['Protocol', 'Port', 'SslPolicy', 'Certificates', 'DefaultActions', 'Rules']: - module.fail_json(msg="listeners parameter contains invalid dict keys. Should be one of 'Protocol', " - "'Port', 'SslPolicy', 'Certificates', 'DefaultActions', 'Rules'.") - # Make sure Port is always an integer - elif key == 'Port': - listener[key] = int(listener[key]) + if key == 'Protocol' and listener[key] == 'HTTPS': + if listener.get('SslPolicy') is None: + module.fail_json(msg="'SslPolicy' is a required listener dict key when Protocol = HTTPS") - if not HAS_BOTO3: - module.fail_json(msg='boto3 required for this module') + if listener.get('Certificates') is None: + module.fail_json(msg="'Certificates' is a required listener dict key when Protocol = HTTPS") - region, ec2_url, aws_connect_params = get_aws_connection_info(module, boto3=True) - - if region: - connection = boto3_conn(module, conn_type='client', resource='elbv2', region=region, endpoint=ec2_url, **aws_connect_params) - 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") + connection = module.client('elbv2') + connection_ec2 = module.client('ec2') state = module.params.get("state") + elb = ApplicationLoadBalancer(connection, connection_ec2, module) + if state == 'present': - create_or_update_elb(connection, connection_ec2, module) + create_or_update_elb(elb) else: - delete_elb(connection, module) + delete_elb(elb) if __name__ == '__main__': main() diff --git a/test/integration/targets/elb_application_lb/aliases b/test/integration/targets/elb_application_lb/aliases new file mode 100644 index 00000000000..56927195182 --- /dev/null +++ b/test/integration/targets/elb_application_lb/aliases @@ -0,0 +1,2 @@ +cloud/aws +unsupported diff --git a/test/integration/targets/elb_application_lb/defaults/main.yml b/test/integration/targets/elb_application_lb/defaults/main.yml new file mode 100644 index 00000000000..8100bd55ed0 --- /dev/null +++ b/test/integration/targets/elb_application_lb/defaults/main.yml @@ -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]+)$') }}" diff --git a/test/integration/targets/elb_application_lb/meta/main.yml b/test/integration/targets/elb_application_lb/meta/main.yml new file mode 100644 index 00000000000..1f64f1169a9 --- /dev/null +++ b/test/integration/targets/elb_application_lb/meta/main.yml @@ -0,0 +1,3 @@ +dependencies: + - prepare_tests + - setup_ec2 diff --git a/test/integration/targets/elb_application_lb/tasks/main.yml b/test/integration/targets/elb_application_lb/tasks/main.yml new file mode 100644 index 00000000000..48e1a0d1ea6 --- /dev/null +++ b/test/integration/targets/elb_application_lb/tasks/main.yml @@ -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 diff --git a/test/integration/targets/elb_application_lb/tasks/test_alb_bad_listener_options.yml b/test/integration/targets/elb_application_lb/tasks/test_alb_bad_listener_options.yml new file mode 100644 index 00000000000..821ad36d767 --- /dev/null +++ b/test/integration/targets/elb_application_lb/tasks/test_alb_bad_listener_options.yml @@ -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" diff --git a/test/integration/targets/elb_application_lb/tasks/test_alb_tags.yml b/test/integration/targets/elb_application_lb/tasks/test_alb_tags.yml new file mode 100644 index 00000000000..b7942fa7368 --- /dev/null +++ b/test/integration/targets/elb_application_lb/tasks/test_alb_tags.yml @@ -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 diff --git a/test/integration/targets/elb_application_lb/tasks/test_alb_with_asg.yml b/test/integration/targets/elb_application_lb/tasks/test_alb_with_asg.yml new file mode 100644 index 00000000000..11c13845a7b --- /dev/null +++ b/test/integration/targets/elb_application_lb/tasks/test_alb_with_asg.yml @@ -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 diff --git a/test/integration/targets/elb_application_lb/tasks/test_creating_alb.yml b/test/integration/targets/elb_application_lb/tasks/test_creating_alb.yml new file mode 100644 index 00000000000..ee932d4eded --- /dev/null +++ b/test/integration/targets/elb_application_lb/tasks/test_creating_alb.yml @@ -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 diff --git a/test/integration/targets/elb_application_lb/tasks/test_deleting_alb.yml b/test/integration/targets/elb_application_lb/tasks/test_deleting_alb.yml new file mode 100644 index 00000000000..34e278cb9f6 --- /dev/null +++ b/test/integration/targets/elb_application_lb/tasks/test_deleting_alb.yml @@ -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 diff --git a/test/sanity/validate-modules/ignore.txt b/test/sanity/validate-modules/ignore.txt index 00941d6ea25..c1b580dcbc0 100644 --- a/test/sanity/validate-modules/ignore.txt +++ b/test/sanity/validate-modules/ignore.txt @@ -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_parameter_group.py E326 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 E325 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_target.py E327 diff --git a/test/units/modules/cloud/amazon/test_elb_application_lb.py b/test/units/modules/cloud/amazon/test_elb_application_lb.py deleted file mode 100644 index 7c92633af12..00000000000 --- a/test/units/modules/cloud/amazon/test_elb_application_lb.py +++ /dev/null @@ -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