From b2a2570b82936f04c7df7176c13f1f3ebb0fba5e Mon Sep 17 00:00:00 2001 From: Erin DeLong Date: Sun, 14 Apr 2024 18:24:56 -0700 Subject: [PATCH 1/6] added security group support to network load balancer --- meta/runtime.yml | 4 +- plugins/modules/elb_network_lb.py | 146 ++++++++++++++++-- .../tasks/test_creating_nlb_sg.yml | 72 +++++++++ 3 files changed, 209 insertions(+), 13 deletions(-) create mode 100644 tests/integration/targets/elb_network_lb/tasks/test_creating_nlb_sg.yml diff --git a/meta/runtime.yml b/meta/runtime.yml index 4c6bc72910d..2d17de54a9a 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -1,5 +1,5 @@ --- -requires_ansible: '>=2.12.0' +requires_ansible: ">=2.12.0" action_groups: aws: - accessanalyzer_validate_policy_info @@ -522,4 +522,4 @@ plugin_routing: redirect: amazon.aws.sts_assume_role module_utils: route53: - redirect: amazon.aws.route53 \ No newline at end of file + redirect: amazon.aws.route53 diff --git a/plugins/modules/elb_network_lb.py b/plugins/modules/elb_network_lb.py index 22e419328d9..9d9d737a684 100644 --- a/plugins/modules/elb_network_lb.py +++ b/plugins/modules/elb_network_lb.py @@ -108,6 +108,13 @@ - This parameter is mutually exclusive with I(subnet_mappings). type: list elements: str + security_groups: + description: + - A list of the names or IDs of the security groups to assign to the load balancer. + - Required if I(state=present). + - If C([]), the VPC's default security group will be used. + type: list + elements: str scheme: description: - Internet-facing or internal load balancer. An ELB scheme can not be modified after creation. @@ -320,6 +327,11 @@ returned: when state is present type: str sample: internal + security_groups: + description: The IDs of the security groups for the load balancer. + returned: when state is present + type: list + sample: ['sg-0011223344'] state: description: The state of the load balancer. returned: when state is present @@ -452,6 +464,117 @@ def delete_elb(elb_obj): elb_obj.module.exit_json(changed=elb_obj.changed) +#creating class to allow for security groups with load balancer + +class NetworkLoadBalancerWithSecurityGroups(NetworkLoadBalancer): + def __init__(self, connection, connection_ec2, module): + """ + + :param connection: boto3 connection + :param module: Ansible module + """ + super().__init__(connection, module) + + self.connection_ec2 = connection_ec2 + + # Ansible module parameters specific to NLBs + # self.type = "network" + # self.cross_zone_load_balancing = module.params.get("cross_zone_load_balancing") + + # if self.elb is not None and self.elb["Type"] != "network": + # self.module.fail_json( + # msg="The load balancer type you are trying to manage is not network. Try elb_application_lb module instead.", + # ) + + 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") + + def _elb_create_params(self): + params = super()._elb_create_params() + + if self.security_groups is not None: + params["SecurityGroups"] = self.security_groups + + # params["Scheme"] = self.scheme + + return params + + def modify_elb_attributes(self): + """ + Update Network ELB attributes if required + + :return: + """ + + update_attributes = [] + + if ( + self.cross_zone_load_balancing is not None + and str(self.cross_zone_load_balancing).lower() != self.elb_attributes["load_balancing_cross_zone_enabled"] + ): + update_attributes.append( + {"Key": "load_balancing.cross_zone.enabled", "Value": str(self.cross_zone_load_balancing).lower()} + ) + if ( + self.deletion_protection is not None + and str(self.deletion_protection).lower() != self.elb_attributes["deletion_protection_enabled"] + ): + update_attributes.append( + {"Key": "deletion_protection.enabled", "Value": str(self.deletion_protection).lower()} + ) + + 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 + + + def main(): argument_spec = dict( @@ -486,31 +609,32 @@ def main(): ) required_if = [ - ["state", "present", ["subnets", "subnet_mappings"], True], + ["state", "present", ["subnets", "subnet_mappings"], True], ] module = AnsibleAWSModule( - argument_spec=argument_spec, - required_if=required_if, - mutually_exclusive=[["subnets", "subnet_mappings"]], + argument_spec=argument_spec, + required_if=required_if, + mutually_exclusive=[["subnets", "subnet_mappings"]], ) # Check for subnets or subnet_mappings if state is present state = module.params.get("state") - # Quick check of listeners parameters + # Quick check of listeners parameters listeners = module.params.get("listeners") if listeners is not None: - for listener in listeners: - for key in listener.keys(): - protocols_list = ["TCP", "TLS", "UDP", "TCP_UDP"] - if key == "Protocol" and listener[key] not in protocols_list: - module.fail_json(msg="'Protocol' must be either " + ", ".join(protocols_list)) + for listener in listeners: + for key in listener.keys(): + protocols_list = ["TCP", "TLS", "UDP", "TCP_UDP"] + if key == "Protocol" and listener[key] not in protocols_list: + module.fail_json(msg="'Protocol' must be either " + ", ".join(protocols_list)) connection = module.client("elbv2") connection_ec2 = module.client("ec2") - elb = NetworkLoadBalancer(connection, connection_ec2, module) + # elb = NetworkLoadBalancer(connection, connection_ec2, module) + elb = NetworkLoadBalancerWithSecurityGroups(connection, connection_ec2, module) if state == "present": create_or_update_elb(elb) diff --git a/tests/integration/targets/elb_network_lb/tasks/test_creating_nlb_sg.yml b/tests/integration/targets/elb_network_lb/tasks/test_creating_nlb_sg.yml new file mode 100644 index 00000000000..b85558a9ebe --- /dev/null +++ b/tests/integration/targets/elb_network_lb/tasks/test_creating_nlb_sg.yml @@ -0,0 +1,72 @@ +- block: + - name: create NLB with listeners + elb_network_lb: + name: "{{ nlb_name }}" + subnets: "{{ nlb_subnets }}" + state: present + security_groups: "{{ sec_group.id }}" + listeners: + - Protocol: TCP + Port: 80 + DefaultActions: + - Type: forward + TargetGroupName: "{{ tg_name }}" + - Protocol: TLS + Port: 443 + Certificates: + - CertificateArn: "{{ cert.arn }}" + DefaultActions: + - Type: forward + TargetGroupName: "{{ tg_name }}" + - Protocol: UDP + Port: 13 + DefaultActions: + - Type: forward + TargetGroupName: "{{ tg_tcpudp_name }}" + - Protocol: TCP_UDP + Port: 17 + DefaultActions: + - Type: forward + TargetGroupName: "{{ tg_tcpudp_name }}" + register: nlb + + - assert: + that: + - nlb.changed + - nlb.listeners|length == 4 + + - name: test idempotence creating NLB with listeners + elb_network_lb: + name: "{{ nlb_name }}" + subnets: "{{ nlb_subnets }}" + state: present + security_groups: "{{ sec_group.id }}" + listeners: + - Protocol: TCP + Port: 80 + DefaultActions: + - Type: forward + TargetGroupName: "{{ tg_name }}" + - Protocol: TLS + Port: 443 + Certificates: + - CertificateArn: "{{ cert.arn }}" + DefaultActions: + - Type: forward + TargetGroupName: "{{ tg_name }}" + - Protocol: UDP + Port: 13 + DefaultActions: + - Type: forward + TargetGroupName: "{{ tg_tcpudp_name }}" + - Protocol: TCP_UDP + Port: 17 + DefaultActions: + - Type: forward + TargetGroupName: "{{ tg_tcpudp_name }}" + register: nlb + + - assert: + that: + - not nlb.changed + - nlb.listeners|length == 4 From 60a4d44955506f0ba46ccb2b399d3f92b7351d6a Mon Sep 17 00:00:00 2001 From: erinkdelong Date: Tue, 30 Apr 2024 21:17:28 -0700 Subject: [PATCH 2/6] Update plugins/modules/elb_network_lb.py Co-authored-by: Markus Bergholz --- plugins/modules/elb_network_lb.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/modules/elb_network_lb.py b/plugins/modules/elb_network_lb.py index 9d9d737a684..46df50bdf71 100644 --- a/plugins/modules/elb_network_lb.py +++ b/plugins/modules/elb_network_lb.py @@ -109,6 +109,7 @@ type: list elements: str security_groups: + version_added: 7.3.0 description: - A list of the names or IDs of the security groups to assign to the load balancer. - Required if I(state=present). From 329db9d30d92aa9f29a2041422705f0a208fe096 Mon Sep 17 00:00:00 2001 From: Erin DeLong Date: Tue, 30 Apr 2024 21:24:54 -0700 Subject: [PATCH 3/6] added changelog to fragment --- changelogs/fragments/2079-network_lb_security_groups.yml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 changelogs/fragments/2079-network_lb_security_groups.yml diff --git a/changelogs/fragments/2079-network_lb_security_groups.yml b/changelogs/fragments/2079-network_lb_security_groups.yml new file mode 100644 index 00000000000..eb954efb361 --- /dev/null +++ b/changelogs/fragments/2079-network_lb_security_groups.yml @@ -0,0 +1,2 @@ +minor_changes: + - elb_network_lb - add security groups parameter to network load balancer From 78f117b9fca31f23d670bcad2c01dbb92756c9e6 Mon Sep 17 00:00:00 2001 From: Erin DeLong Date: Tue, 30 Apr 2024 21:28:23 -0700 Subject: [PATCH 4/6] added pr url to changelog --- changelogs/fragments/2079-network_lb_security_groups.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelogs/fragments/2079-network_lb_security_groups.yml b/changelogs/fragments/2079-network_lb_security_groups.yml index eb954efb361..a3757ed44bf 100644 --- a/changelogs/fragments/2079-network_lb_security_groups.yml +++ b/changelogs/fragments/2079-network_lb_security_groups.yml @@ -1,2 +1,2 @@ minor_changes: - - elb_network_lb - add security groups parameter to network load balancer + - elb_network_lb - add security groups parameter to network load balancer (https://github.com/ansible-collections/community.aws/pull/2079) From 269f6b0373adc6b6d6ecc8c5d486152f8f5acfeb Mon Sep 17 00:00:00 2001 From: Erin DeLong Date: Wed, 1 May 2024 22:11:33 -0700 Subject: [PATCH 5/6] added third arg to constructor call --- plugins/modules/elb_network_lb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/elb_network_lb.py b/plugins/modules/elb_network_lb.py index 46df50bdf71..aa7124a3227 100644 --- a/plugins/modules/elb_network_lb.py +++ b/plugins/modules/elb_network_lb.py @@ -474,7 +474,7 @@ def __init__(self, connection, connection_ec2, module): :param connection: boto3 connection :param module: Ansible module """ - super().__init__(connection, module) + super().__init__(connection, connection_ec2, module) self.connection_ec2 = connection_ec2 From b68e452ff41226d7104f39e55d89689872f7d461 Mon Sep 17 00:00:00 2001 From: Erin DeLong Date: Thu, 9 May 2024 10:41:23 -0700 Subject: [PATCH 6/6] fixing spacing --- plugins/modules/elb_network_lb.py | 250 +++++++++++++++--------------- 1 file changed, 124 insertions(+), 126 deletions(-) diff --git a/plugins/modules/elb_network_lb.py b/plugins/modules/elb_network_lb.py index aa7124a3227..05f0f7d3c1b 100644 --- a/plugins/modules/elb_network_lb.py +++ b/plugins/modules/elb_network_lb.py @@ -460,139 +460,137 @@ def create_or_update_elb(elb_obj): def delete_elb(elb_obj): - if elb_obj.elb: - elb_obj.delete() + if elb_obj.elb: + elb_obj.delete() elb_obj.module.exit_json(changed=elb_obj.changed) -#creating class to allow for security groups with load balancer +# creating class to allow for security groups with load balancer + class NetworkLoadBalancerWithSecurityGroups(NetworkLoadBalancer): - def __init__(self, connection, connection_ec2, module): - """ + def __init__(self, connection, connection_ec2, module): + """ - :param connection: boto3 connection - :param module: Ansible module - """ - super().__init__(connection, connection_ec2, module) + :param connection: boto3 connection + :param module: Ansible module + """ + super().__init__(connection, connection_ec2, module) - self.connection_ec2 = connection_ec2 + self.connection_ec2 = connection_ec2 - # Ansible module parameters specific to NLBs - # self.type = "network" - # self.cross_zone_load_balancing = module.params.get("cross_zone_load_balancing") + # Ansible module parameters specific to NLBs + # self.type = "network" + # self.cross_zone_load_balancing = module.params.get("cross_zone_load_balancing") - # if self.elb is not None and self.elb["Type"] != "network": - # self.module.fail_json( - # msg="The load balancer type you are trying to manage is not network. Try elb_application_lb module instead.", - # ) + # if self.elb is not None and self.elb["Type"] != "network": + # self.module.fail_json( + # msg="The load balancer type you are trying to manage is not network. Try elb_application_lb module instead.", + # ) - if module.params.get("security_groups") is not None: - try: - self.security_groups = AWSRetry.jittered_backoff()(get_ec2_security_group_ids_from_names)( + 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") - - def _elb_create_params(self): - params = super()._elb_create_params() + ) + 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") - if self.security_groups is not None: - params["SecurityGroups"] = self.security_groups + def _elb_create_params(self): + params = super()._elb_create_params() - # params["Scheme"] = self.scheme + if self.security_groups is not None: + params["SecurityGroups"] = self.security_groups - return params + # params["Scheme"] = self.scheme - def modify_elb_attributes(self): - """ - Update Network ELB attributes if required + return params - :return: - """ + def modify_elb_attributes(self): + """ + Update Network ELB attributes if required - update_attributes = [] + :return: + """ - if ( - self.cross_zone_load_balancing is not None - and str(self.cross_zone_load_balancing).lower() != self.elb_attributes["load_balancing_cross_zone_enabled"] - ): - update_attributes.append( - {"Key": "load_balancing.cross_zone.enabled", "Value": str(self.cross_zone_load_balancing).lower()} - ) - if ( - self.deletion_protection is not None - and str(self.deletion_protection).lower() != self.elb_attributes["deletion_protection_enabled"] - ): - update_attributes.append( - {"Key": "deletion_protection.enabled", "Value": str(self.deletion_protection).lower()} - ) + update_attributes = [] + + if ( + self.cross_zone_load_balancing is not None + and str(self.cross_zone_load_balancing).lower() != self.elb_attributes["load_balancing_cross_zone_enabled"] + ): + update_attributes.append( + {"Key": "load_balancing.cross_zone.enabled", "Value": str(self.cross_zone_load_balancing).lower()} + ) + if ( + self.deletion_protection is not None + and str(self.deletion_protection).lower() != self.elb_attributes["deletion_protection_enabled"] + ): + update_attributes.append( + {"Key": "deletion_protection.enabled", "Value": str(self.deletion_protection).lower()} + ) + + 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 - 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) + :return: bool True if they match otherwise False + """ - self.changed = True + 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 + def main(): - argument_spec = dict( - cross_zone_load_balancing=dict(type="bool"), - deletion_protection=dict(type="bool"), - 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", elements="dict"), - DefaultActions=dict(type="list", required=True, elements="dict"), - AlpnPolicy=dict( - type="str", - choices=["HTTP1Only", "HTTP2Only", "HTTP2Optional", "HTTP2Preferred", "None"], + argument_spec = dict( + cross_zone_load_balancing=dict(type="bool"), + deletion_protection=dict(type="bool"), + 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", elements="dict"), + DefaultActions=dict(type="list", required=True, elements="dict"), + AlpnPolicy=dict( + type="str", + choices=["HTTP1Only", "HTTP2Only", "HTTP2Optional", "HTTP2Preferred", "None"], ), ), ), @@ -609,38 +607,38 @@ def main(): ip_address_type=dict(type="str", choices=["ipv4", "dualstack"]), ) - required_if = [ + required_if = [ ["state", "present", ["subnets", "subnet_mappings"], True], ] - module = AnsibleAWSModule( + module = AnsibleAWSModule( argument_spec=argument_spec, required_if=required_if, mutually_exclusive=[["subnets", "subnet_mappings"]], ) # Check for subnets or subnet_mappings if state is present - state = module.params.get("state") + state = module.params.get("state") # Quick check of listeners parameters - listeners = module.params.get("listeners") - if listeners is not None: - for listener in listeners: - for key in listener.keys(): - protocols_list = ["TCP", "TLS", "UDP", "TCP_UDP"] - if key == "Protocol" and listener[key] not in protocols_list: - module.fail_json(msg="'Protocol' must be either " + ", ".join(protocols_list)) + listeners = module.params.get("listeners") + if listeners is not None: + for listener in listeners: + for key in listener.keys(): + protocols_list = ["TCP", "TLS", "UDP", "TCP_UDP"] + if key == "Protocol" and listener[key] not in protocols_list: + module.fail_json(msg="'Protocol' must be either " + ", ".join(protocols_list)) - connection = module.client("elbv2") - connection_ec2 = module.client("ec2") + connection = module.client("elbv2") + connection_ec2 = module.client("ec2") # elb = NetworkLoadBalancer(connection, connection_ec2, module) - elb = NetworkLoadBalancerWithSecurityGroups(connection, connection_ec2, module) + elb = NetworkLoadBalancerWithSecurityGroups(connection, connection_ec2, module) - if state == "present": - create_or_update_elb(elb) - else: - delete_elb(elb) + if state == "present": + create_or_update_elb(elb) + else: + delete_elb(elb) if __name__ == "__main__":