Server IP : 85.214.239.14 / Your IP : 3.133.109.141 Web Server : Apache/2.4.62 (Debian) System : Linux h2886529.stratoserver.net 4.9.0 #1 SMP Tue Jan 9 19:45:01 MSK 2024 x86_64 User : www-data ( 33) PHP Version : 7.4.18 Disable Function : pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,pcntl_unshare, MySQL : OFF | cURL : OFF | WGET : ON | Perl : ON | Python : ON | Sudo : ON | Pkexec : OFF Directory : /usr/lib/python3/dist-packages/ansible_collections/amazon/aws/plugins/modules/ |
Upload File : |
#!/usr/bin/python # Copyright: Ansible Project # 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 __metaclass__ = type DOCUMENTATION = ''' --- module: elb_classic_lb version_added: 1.0.0 description: - Creates, updates or destroys an Amazon Elastic Load Balancer (ELB). - This module was renamed from C(amazon.aws.ec2_elb_lb) to M(amazon.aws.elb_classic_lb) in version 2.1.0 of the amazon.aws collection. short_description: Creates, updates or destroys an Amazon ELB author: - "Jim Dalton (@jsdalton)" - "Mark Chappell (@tremble)" options: state: description: - Create or destroy the ELB. type: str choices: [ absent, present ] required: true name: description: - The name of the ELB. - The name of an ELB must be less than 32 characters and unique per-region per-account. type: str required: true listeners: description: - List of ports/protocols for this ELB to listen on (see examples). - Required when I(state=present) and the ELB doesn't exist. type: list elements: dict suboptions: load_balancer_port: description: - The port on which the load balancer will listen. type: int required: True instance_port: description: - The port on which the instance is listening. type: int required: True ssl_certificate_id: description: - The Amazon Resource Name (ARN) of the SSL certificate. type: str protocol: description: - The transport protocol to use for routing. - Valid values are C(HTTP), C(HTTPS), C(TCP), or C(SSL). type: str required: True instance_protocol: description: - The protocol to use for routing traffic to instances. - Valid values are C(HTTP), C(HTTPS), C(TCP), or C(SSL), type: str proxy_protocol: description: - Enable proxy protocol for the listener. - Beware, ELB controls for the proxy protocol are based on the I(instance_port). If you have multiple listeners talking to the same I(instance_port), this will affect all of them. type: bool purge_listeners: description: - Purge existing listeners on ELB that are not found in listeners. type: bool default: true instance_ids: description: - List of instance ids to attach to this ELB. type: list elements: str purge_instance_ids: description: - Purge existing instance ids on ELB that are not found in I(instance_ids). type: bool default: false zones: description: - List of availability zones to enable on this ELB. - Mutually exclusive with I(subnets). type: list elements: str purge_zones: description: - Purge existing availability zones on ELB that are not found in I(zones). type: bool default: false security_group_ids: description: - A list of security groups to apply to the ELB. type: list elements: str security_group_names: description: - A list of security group names to apply to the ELB. type: list elements: str health_check: description: - A dictionary of health check configuration settings (see examples). type: dict suboptions: ping_protocol: description: - The protocol which the ELB health check will use when performing a health check. - Valid values are C('HTTP'), C('HTTPS'), C('TCP') and C('SSL'). required: true type: str ping_path: description: - The URI path which the ELB health check will query when performing a health check. - Required when I(ping_protocol=HTTP) or I(ping_protocol=HTTPS). required: false type: str ping_port: description: - The TCP port to which the ELB will connect when performing a health check. required: true type: int interval: description: - The approximate interval, in seconds, between health checks of an individual instance. required: true type: int timeout: description: - The amount of time, in seconds, after which no response means a failed health check. aliases: ['response_timeout'] required: true type: int unhealthy_threshold: description: - The number of consecutive health check failures required before moving the instance to the Unhealthy state. required: true type: int healthy_threshold: description: - The number of consecutive health checks successes required before moving the instance to the Healthy state. required: true type: int access_logs: description: - A dictionary of access logs configuration settings (see examples). type: dict suboptions: enabled: description: - When set to C(True) will configure delivery of access logs to an S3 bucket. - When set to C(False) will disable delivery of access logs. required: false type: bool default: true s3_location: description: - The S3 bucket to deliver access logs to. - See U(https://docs.aws.amazon.com/elasticloadbalancing/latest/classic/enable-access-logs.html) for more information about the necessary S3 bucket policies. - Required when I(enabled=True). required: false type: str s3_prefix: description: - Where in the S3 bucket to deliver the logs. - If the prefix is not provided or set to C(""), the log is placed at the root level of the bucket. required: false type: str default: "" interval: description: - The interval for publishing the access logs to S3. required: false type: int default: 60 choices: [ 5, 60 ] subnets: description: - A list of VPC subnets to use when creating the ELB. - Mutually exclusive with I(zones). type: list elements: str purge_subnets: description: - Purge existing subnets on the ELB that are not found in I(subnets). - Because it is not permitted to add multiple subnets from the same availability zone, subnets to be purged will be removed before new subnets are added. This may cause a brief outage if you try to replace all subnets at once. type: bool default: false scheme: description: - The scheme to use when creating the ELB. - For a private VPC-visible ELB use C(internal). - If you choose to update your scheme with a different value the ELB will be destroyed and a new ELB created. - Defaults to I(scheme=internet-facing). type: str choices: ["internal", "internet-facing"] connection_draining_timeout: description: - Wait a specified timeout allowing connections to drain before terminating an instance. - Set to C(0) to disable connection draining. type: int idle_timeout: description: - ELB connections from clients and to servers are timed out after this amount of time. type: int cross_az_load_balancing: description: - Distribute load across all configured Availability Zones. - Defaults to C(false). type: bool stickiness: description: - A dictionary of stickiness policy settings. - Policy will be applied to all listeners (see examples). type: dict suboptions: type: description: - The type of stickiness policy to apply. - Required if I(enabled=true). - Ignored if I(enabled=false). required: false type: 'str' choices: ['application','loadbalancer'] enabled: description: - When I(enabled=false) session stickiness will be disabled for all listeners. required: false type: bool default: true cookie: description: - The name of the application cookie used for stickiness. - Required if I(enabled=true) and I(type=application). - Ignored if I(enabled=false). required: false type: str expiration: description: - The time period, in seconds, after which the cookie should be considered stale. - If this parameter is not specified, the stickiness session lasts for the duration of the browser session. - Ignored if I(enabled=false). required: false type: int wait: description: - When creating, deleting, or adding instances to an ELB, if I(wait=true) Ansible will wait for both the load balancer and related network interfaces to finish creating/deleting. - Support for waiting when adding instances was added in release 2.1.0. type: bool default: false wait_timeout: description: - Used in conjunction with wait. Number of seconds to wait for the ELB to be terminated. - A maximum of 600 seconds (10 minutes) is allowed. type: int default: 180 notes: - The ec2_elb fact previously set by this module was deprecated in release 2.1.0 and since release 4.0.0 is no longer set. - Support for I(purge_tags) was added in release 2.1.0. extends_documentation_fragment: - amazon.aws.aws - amazon.aws.ec2 - amazon.aws.tags - amazon.aws.boto3 ''' EXAMPLES = """ # Note: None of these examples set aws_access_key, aws_secret_key, or region. # It is assumed that their matching environment variables are set. # Basic provisioning example (non-VPC) - amazon.aws.elb_classic_lb: name: "test-please-delete" state: present zones: - us-east-1a - us-east-1d listeners: - protocol: http # options are http, https, ssl, tcp load_balancer_port: 80 instance_port: 80 proxy_protocol: True - protocol: https load_balancer_port: 443 instance_protocol: http # optional, defaults to value of protocol setting instance_port: 80 # ssl certificate required for https or ssl ssl_certificate_id: "arn:aws:iam::123456789012:server-certificate/company/servercerts/ProdServerCert" # Internal ELB example - amazon.aws.elb_classic_lb: name: "test-vpc" scheme: internal state: present instance_ids: - i-abcd1234 purge_instance_ids: true subnets: - subnet-abcd1234 - subnet-1a2b3c4d listeners: - protocol: http # options are http, https, ssl, tcp load_balancer_port: 80 instance_port: 80 # Configure a health check and the access logs - amazon.aws.elb_classic_lb: name: "test-please-delete" state: present zones: - us-east-1d listeners: - protocol: http load_balancer_port: 80 instance_port: 80 health_check: ping_protocol: http # options are http, https, ssl, tcp ping_port: 80 ping_path: "/index.html" # not required for tcp or ssl response_timeout: 5 # seconds interval: 30 # seconds unhealthy_threshold: 2 healthy_threshold: 10 access_logs: interval: 5 # minutes (defaults to 60) s3_location: "my-bucket" # This value is required if access_logs is set s3_prefix: "logs" # Ensure ELB is gone - amazon.aws.elb_classic_lb: name: "test-please-delete" state: absent # Ensure ELB is gone and wait for check (for default timeout) - amazon.aws.elb_classic_lb: name: "test-please-delete" state: absent wait: true # Ensure ELB is gone and wait for check with timeout value - amazon.aws.elb_classic_lb: name: "test-please-delete" state: absent wait: true wait_timeout: 600 # Normally, this module will purge any listeners that exist on the ELB # but aren't specified in the listeners parameter. If purge_listeners is # false it leaves them alone - amazon.aws.elb_classic_lb: name: "test-please-delete" state: present zones: - us-east-1a - us-east-1d listeners: - protocol: http load_balancer_port: 80 instance_port: 80 purge_listeners: false # Normally, this module will leave availability zones that are enabled # on the ELB alone. If purge_zones is true, then any extraneous zones # will be removed - amazon.aws.elb_classic_lb: name: "test-please-delete" state: present zones: - us-east-1a - us-east-1d listeners: - protocol: http load_balancer_port: 80 instance_port: 80 purge_zones: true # Creates a ELB and assigns a list of subnets to it. - amazon.aws.elb_classic_lb: state: present name: 'New ELB' security_group_ids: 'sg-123456, sg-67890' subnets: 'subnet-123456,subnet-67890' purge_subnets: true listeners: - protocol: http load_balancer_port: 80 instance_port: 80 # Create an ELB with connection draining, increased idle timeout and cross availability # zone load balancing - amazon.aws.elb_classic_lb: name: "New ELB" state: present connection_draining_timeout: 60 idle_timeout: 300 cross_az_load_balancing: "yes" zones: - us-east-1a - us-east-1d listeners: - protocol: http load_balancer_port: 80 instance_port: 80 # Create an ELB with load balancer stickiness enabled - amazon.aws.elb_classic_lb: name: "New ELB" state: present zones: - us-east-1a - us-east-1d listeners: - protocol: http load_balancer_port: 80 instance_port: 80 stickiness: type: loadbalancer enabled: true expiration: 300 # Create an ELB with application stickiness enabled - amazon.aws.elb_classic_lb: name: "New ELB" state: present zones: - us-east-1a - us-east-1d listeners: - protocol: http load_balancer_port: 80 instance_port: 80 stickiness: type: application enabled: true cookie: SESSIONID # Create an ELB and add tags - amazon.aws.elb_classic_lb: name: "New ELB" state: present zones: - us-east-1a - us-east-1d listeners: - protocol: http load_balancer_port: 80 instance_port: 80 tags: Name: "New ELB" stack: "production" client: "Bob" # Delete all tags from an ELB - amazon.aws.elb_classic_lb: name: "New ELB" state: present zones: - us-east-1a - us-east-1d listeners: - protocol: http load_balancer_port: 80 instance_port: 80 tags: {} """ RETURN = ''' elb: description: Load Balancer attributes returned: always type: dict contains: app_cookie_policy: description: The name of the policy used to control if the ELB is using a application cookie stickiness policy. type: str sample: ec2-elb-lb-AppCookieStickinessPolicyType returned: when state is not 'absent' backends: description: A description of the backend policy applied to the ELB (instance-port:policy-name). type: str sample: 8181:ProxyProtocol-policy returned: when state is not 'absent' connection_draining_timeout: description: The maximum time, in seconds, to keep the existing connections open before deregistering the instances. type: int sample: 25 returned: when state is not 'absent' cross_az_load_balancing: description: Either C('yes') if cross-AZ load balancing is enabled, or C('no') if cross-AZ load balancing is disabled. type: str sample: 'yes' returned: when state is not 'absent' dns_name: description: The DNS name of the ELB. type: str sample: internal-ansible-test-935c585850ac-1516306744.us-east-1.elb.amazonaws.com returned: when state is not 'absent' health_check: description: A dictionary describing the health check used for the ELB. type: dict returned: when state is not 'absent' contains: healthy_threshold: description: The number of consecutive successful health checks before marking an instance as healthy. type: int sample: 2 interval: description: The time, in seconds, between each health check. type: int sample: 10 target: description: The Protocol, Port, and for HTTP(S) health checks the path tested by the health check. type: str sample: TCP:22 timeout: description: The time, in seconds, after which an in progress health check is considered failed due to a timeout. type: int sample: 5 unhealthy_threshold: description: The number of consecutive failed health checks before marking an instance as unhealthy. type: int sample: 2 hosted_zone_id: description: The ID of the Amazon Route 53 hosted zone for the load balancer. type: str sample: Z35SXDOTRQ7X7K returned: when state is not 'absent' hosted_zone_name: description: The DNS name of the load balancer when using a custom hostname. type: str sample: 'ansible-module.example' returned: when state is not 'absent' idle_timeout: description: The length of of time before an idle connection is dropped by the ELB. type: int sample: 50 returned: when state is not 'absent' in_service_count: description: The number of instances attached to the ELB in an in-service state. type: int sample: 1 returned: when state is not 'absent' instance_health: description: A list of dictionaries describing the health of each instance attached to the ELB. type: list elements: dict returned: when state is not 'absent' contains: description: description: A human readable description of why the instance is not in service. type: str sample: N/A returned: when state is not 'absent' instance_id: description: The ID of the instance. type: str sample: i-03dcc8953a03d6435 returned: when state is not 'absent' reason_code: description: A code describing why the instance is not in service. type: str sample: N/A returned: when state is not 'absent' state: description: The current service state of the instance. type: str sample: InService returned: when state is not 'absent' instances: description: A list of the IDs of instances attached to the ELB. type: list elements: str sample: ['i-03dcc8953a03d6435'] returned: when state is not 'absent' lb_cookie_policy: description: The name of the policy used to control if the ELB is using a cookie stickiness policy. type: str sample: ec2-elb-lb-LBCookieStickinessPolicyType returned: when state is not 'absent' listeners: description: - A list of lists describing the listeners attached to the ELB. - The nested list contains the listener port, the instance port, the listener protoco, the instance port, and where appropriate the ID of the SSL certificate for the port. type: list elements: list sample: [[22, 22, 'TCP', 'TCP'], [80, 8181, 'HTTP', 'HTTP']] returned: when state is not 'absent' name: description: The name of the ELB. This name is unique per-region, per-account. type: str sample: ansible-test-935c585850ac returned: when state is not 'absent' out_of_service_count: description: The number of instances attached to the ELB in an out-of-service state. type: int sample: 0 returned: when state is not 'absent' proxy_policy: description: The name of the policy used to control if the ELB operates using the Proxy protocol. type: str sample: ProxyProtocol-policy returned: when the proxy protocol policy exists. region: description: The AWS region in which the ELB is running. type: str sample: us-east-1 returned: always scheme: description: Whether the ELB is an C('internal') or a C('internet-facing') load balancer. type: str sample: internal returned: when state is not 'absent' security_group_ids: description: A list of the IDs of the Security Groups attached to the ELB. type: list elements: str sample: ['sg-0c12ebd82f2fb97dc', 'sg-01ec7378d0c7342e6'] returned: when state is not 'absent' status: description: A minimal description of the current state of the ELB. Valid values are C('exists'), C('gone'), C('deleted'), C('created'). type: str sample: exists returned: always subnets: description: A list of the subnet IDs attached to the ELB. type: list elements: str sample: ['subnet-00d9d0f70c7e5f63c', 'subnet-03fa5253586b2d2d5'] returned: when state is not 'absent' tags: description: A dictionary describing the tags attached to the ELB. type: dict sample: {'Name': 'ansible-test-935c585850ac', 'ExampleTag': 'Example Value'} returned: when state is not 'absent' unknown_instance_state_count: description: The number of instances attached to the ELB in an unknown state. type: int sample: 0 returned: when state is not 'absent' zones: description: A list of the AWS regions in which the ELB is running. type: list elements: str sample: ['us-east-1b', 'us-east-1a'] returned: when state is not 'absent' ''' try: import botocore except ImportError: pass # Taken care of by AnsibleAWSModule from ansible_collections.amazon.aws.plugins.module_utils.core import AnsibleAWSModule from ansible_collections.amazon.aws.plugins.module_utils.core import is_boto3_error_code from ansible_collections.amazon.aws.plugins.module_utils.core import scrub_none_parameters from ansible_collections.amazon.aws.plugins.module_utils.ec2 import AWSRetry from ansible_collections.amazon.aws.plugins.module_utils.ec2 import ansible_dict_to_boto3_filter_list from ansible_collections.amazon.aws.plugins.module_utils.ec2 import ansible_dict_to_boto3_tag_list from ansible_collections.amazon.aws.plugins.module_utils.ec2 import boto3_tag_list_to_ansible_dict from ansible_collections.amazon.aws.plugins.module_utils.ec2 import camel_dict_to_snake_dict from ansible_collections.amazon.aws.plugins.module_utils.ec2 import compare_aws_tags from ansible_collections.amazon.aws.plugins.module_utils.ec2 import snake_dict_to_camel_dict from ansible_collections.amazon.aws.plugins.module_utils.ec2 import get_ec2_security_group_ids_from_names from ansible_collections.amazon.aws.plugins.module_utils.waiters import get_waiter class ElbManager(object): """Handles ELB creation and destruction""" def __init__(self, module): self.module = module self.name = module.params['name'] self.listeners = module.params['listeners'] self.purge_listeners = module.params['purge_listeners'] self.instance_ids = module.params['instance_ids'] self.purge_instance_ids = module.params['purge_instance_ids'] self.zones = module.params['zones'] self.purge_zones = module.params['purge_zones'] self.health_check = module.params['health_check'] self.access_logs = module.params['access_logs'] self.subnets = module.params['subnets'] self.purge_subnets = module.params['purge_subnets'] self.scheme = module.params['scheme'] self.connection_draining_timeout = module.params['connection_draining_timeout'] self.idle_timeout = module.params['idle_timeout'] self.cross_az_load_balancing = module.params['cross_az_load_balancing'] self.stickiness = module.params['stickiness'] self.wait = module.params['wait'] self.wait_timeout = module.params['wait_timeout'] self.tags = module.params['tags'] self.purge_tags = module.params['purge_tags'] self.changed = False self.status = 'gone' retry_decorator = AWSRetry.jittered_backoff() self.client = self.module.client('elb', retry_decorator=retry_decorator) self.ec2_client = self.module.client('ec2', retry_decorator=retry_decorator) security_group_names = module.params['security_group_names'] self.security_group_ids = module.params['security_group_ids'] self._update_descriptions() if security_group_names: # Use the subnets attached to the VPC to find which VPC we're in and # limit the search if self.elb and self.elb.get('Subnets', None): subnets = set(self.elb.get('Subnets') + list(self.subnets or [])) else: subnets = set(self.subnets) if subnets: vpc_id = self._get_vpc_from_subnets(subnets) else: vpc_id = None try: self.security_group_ids = self._get_ec2_security_group_ids_from_names( sec_group_list=security_group_names, vpc_id=vpc_id) except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: module.fail_json_aws(e, msg="Failed to convert security group names to IDs, try using security group IDs rather than names") def _update_descriptions(self): try: self.elb = self._get_elb() except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: self.module.fail_json_aws(e, msg='Unable to describe load balancer') try: self.elb_attributes = self._get_elb_attributes() except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: self.module.fail_json_aws(e, msg='Unable to describe load balancer attributes') try: self.elb_policies = self._get_elb_policies() except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: self.module.fail_json_aws(e, msg='Unable to describe load balancer policies') try: self.elb_health = self._get_elb_instance_health() except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: self.module.fail_json_aws(e, msg='Unable to describe load balancer instance health') # We have a number of complex parameters which can't be validated by # AnsibleModule or are only required if the ELB doesn't exist. def validate_params(self, state=None): problem_found = False # Validate that protocol is one of the permitted values problem_found |= self._validate_listeners(self.listeners) problem_found |= self._validate_health_check(self.health_check) problem_found |= self._validate_stickiness(self.stickiness) if state == 'present': # When creating a new ELB problem_found |= self._validate_creation_requirements() problem_found |= self._validate_access_logs(self.access_logs) # Pass check_mode down through to the module @property def check_mode(self): return self.module.check_mode def _get_elb_policies(self): try: attributes = self.client.describe_load_balancer_policies(LoadBalancerName=self.name) except is_boto3_error_code(['LoadBalancerNotFound', 'LoadBalancerAttributeNotFoundException']): return {} except is_boto3_error_code('AccessDenied'): # pylint: disable=duplicate-except # Be forgiving if we can't see the attributes # Note: This will break idempotency if someone has set but not describe self.module.warn('Access Denied trying to describe load balancer policies') return {} return attributes['PolicyDescriptions'] def _get_elb_instance_health(self): try: instance_health = self.client.describe_instance_health(LoadBalancerName=self.name) except is_boto3_error_code(['LoadBalancerNotFound', 'LoadBalancerAttributeNotFoundException']): return [] except is_boto3_error_code('AccessDenied'): # pylint: disable=duplicate-except # Be forgiving if we can't see the attributes # Note: This will break idempotency if someone has set but not describe self.module.warn('Access Denied trying to describe instance health') return [] return instance_health['InstanceStates'] def _get_elb_attributes(self): try: attributes = self.client.describe_load_balancer_attributes(LoadBalancerName=self.name) except is_boto3_error_code(['LoadBalancerNotFound', 'LoadBalancerAttributeNotFoundException']): return {} except is_boto3_error_code('AccessDenied'): # pylint: disable=duplicate-except # Be forgiving if we can't see the attributes # Note: This will break idempotency if someone has set but not describe self.module.warn('Access Denied trying to describe load balancer attributes') return {} return attributes['LoadBalancerAttributes'] def _get_elb(self): try: elbs = self._describe_loadbalancer(self.name) except is_boto3_error_code('LoadBalancerNotFound'): return None # Shouldn't happen, but Amazon could change the rules on us... if len(elbs) > 1: self.module.fail_json('Found multiple ELBs with name {0}'.format(self.name)) self.status = 'exists' if self.status == 'gone' else self.status return elbs[0] def _delete_elb(self): # True if succeeds, exception raised if not try: if not self.check_mode: self.client.delete_load_balancer(aws_retry=True, LoadBalancerName=self.name) self.changed = True self.status = 'deleted' except is_boto3_error_code('LoadBalancerNotFound'): return False return True def _create_elb(self): listeners = list(self._format_listener(l) for l in self.listeners) if not self.scheme: self.scheme = 'internet-facing' params = dict( LoadBalancerName=self.name, AvailabilityZones=self.zones, SecurityGroups=self.security_group_ids, Subnets=self.subnets, Listeners=listeners, Scheme=self.scheme) params = scrub_none_parameters(params) if self.tags: params['Tags'] = ansible_dict_to_boto3_tag_list(self.tags) if not self.check_mode: self.client.create_load_balancer(aws_retry=True, **params) # create_load_balancer only returns the DNS name self.elb = self._get_elb() self.changed = True self.status = 'created' return True def _format_listener(self, listener, inject_protocol=False): """Formats listener into the format needed by the ELB API""" listener = scrub_none_parameters(listener) for protocol in ['protocol', 'instance_protocol']: if protocol in listener: listener[protocol] = listener[protocol].upper() if inject_protocol and 'instance_protocol' not in listener: listener['instance_protocol'] = listener['protocol'] # Remove proxy_protocol, it has to be handled as a policy listener.pop('proxy_protocol', None) ssl_id = listener.pop('ssl_certificate_id', None) formatted_listener = snake_dict_to_camel_dict(listener, True) if ssl_id: formatted_listener['SSLCertificateId'] = ssl_id return formatted_listener def _format_healthcheck_target(self): """Compose target string from healthcheck parameters""" protocol = self.health_check['ping_protocol'].upper() path = "" if protocol in ['HTTP', 'HTTPS'] and 'ping_path' in self.health_check: path = self.health_check['ping_path'] return "%s:%s%s" % (protocol, self.health_check['ping_port'], path) def _format_healthcheck(self): return dict( Target=self._format_healthcheck_target(), Timeout=self.health_check['timeout'], Interval=self.health_check['interval'], UnhealthyThreshold=self.health_check['unhealthy_threshold'], HealthyThreshold=self.health_check['healthy_threshold'], ) def ensure_ok(self): """Create the ELB""" if not self.elb: try: self._create_elb() except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: self.module.fail_json_aws(e, msg="Failed to create load balancer") try: self.elb_attributes = self._get_elb_attributes() except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: self.module.fail_json_aws(e, msg='Unable to describe load balancer attributes') self._wait_created() # Some attributes are configured on creation, others need to be updated # after creation. Skip updates for those set on creation else: if self._check_scheme(): # XXX We should probably set 'None' parameters based on the # current state prior to deletion # the only way to change the scheme is by recreating the resource self.ensure_gone() # We need to wait for it to be gone-gone self._wait_gone(True) try: self._create_elb() except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: self.module.fail_json_aws(e, msg="Failed to recreate load balancer") try: self.elb_attributes = self._get_elb_attributes() except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: self.module.fail_json_aws(e, msg='Unable to describe load balancer attributes') else: self._set_subnets() self._set_zones() self._set_security_groups() self._set_elb_listeners() self._set_tags() self._set_health_check() self._set_elb_attributes() self._set_backend_policies() self._set_stickiness_policies() self._set_instance_ids() # if self._check_attribute_support('access_log'): # self._set_access_log() def ensure_gone(self): """Destroy the ELB""" if self.elb: try: self._delete_elb() except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: self.module.fail_json_aws(e, msg="Failed to delete load balancer") self._wait_gone() def _wait_gone(self, wait=None): if not wait and not self.wait: return try: self._wait_for_elb_removed() # Unfortunately even though the ELB itself is removed quickly # the interfaces take longer so reliant security groups cannot # be deleted until the interface has registered as removed. self._wait_for_elb_interface_removed() except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: self.module.fail_json_aws(e, msg="Failed while waiting for load balancer deletion") def _wait_created(self, wait=False): if not wait and not self.wait: return try: self._wait_for_elb_created() # Can take longer than creation self._wait_for_elb_interface_created() except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: self.module.fail_json_aws(e, msg="Failed while waiting for load balancer deletion") def get_load_balancer(self): self._update_descriptions() elb = dict(self.elb or {}) if not elb: return {} elb['LoadBalancerAttributes'] = self.elb_attributes elb['LoadBalancerPolicies'] = self.elb_policies load_balancer = camel_dict_to_snake_dict(elb) try: load_balancer['tags'] = self._get_tags() except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: self.module.fail_json_aws(e, msg="Failed to get load balancer tags") return load_balancer def get_info(self): self._update_descriptions() if not self.elb: return dict( name=self.name, status=self.status, region=self.module.region ) check_elb = dict(self.elb) check_elb_attrs = dict(self.elb_attributes or {}) check_policies = check_elb.get('Policies', {}) try: lb_cookie_policy = check_policies['LBCookieStickinessPolicies'][0]['PolicyName'] except (KeyError, IndexError): lb_cookie_policy = None try: app_cookie_policy = check_policies['AppCookieStickinessPolicies'][0]['PolicyName'] except (KeyError, IndexError): app_cookie_policy = None health_check = camel_dict_to_snake_dict(check_elb.get('HealthCheck', {})) backend_policies = list() for port, policies in self._get_backend_policies().items(): for policy in policies: backend_policies.append("{0}:{1}".format(port, policy)) info = dict( name=check_elb.get('LoadBalancerName'), dns_name=check_elb.get('DNSName'), zones=check_elb.get('AvailabilityZones'), security_group_ids=check_elb.get('SecurityGroups'), status=self.status, subnets=check_elb.get('Subnets'), scheme=check_elb.get('Scheme'), hosted_zone_name=check_elb.get('CanonicalHostedZoneName'), hosted_zone_id=check_elb.get('CanonicalHostedZoneNameID'), lb_cookie_policy=lb_cookie_policy, app_cookie_policy=app_cookie_policy, proxy_policy=self._get_proxy_protocol_policy(), backends=backend_policies, instances=self._get_instance_ids(), out_of_service_count=0, in_service_count=0, unknown_instance_state_count=0, region=self.module.region, health_check=health_check, ) instance_health = camel_dict_to_snake_dict(dict(InstanceHealth=self.elb_health)) info.update(instance_health) # instance state counts: InService or OutOfService if info['instance_health']: for instance_state in info['instance_health']: if instance_state['state'] == "InService": info['in_service_count'] += 1 elif instance_state['state'] == "OutOfService": info['out_of_service_count'] += 1 else: info['unknown_instance_state_count'] += 1 listeners = check_elb.get('ListenerDescriptions', []) if listeners: info['listeners'] = list( self._api_listener_as_tuple(l['Listener']) for l in listeners ) else: info['listeners'] = [] try: info['connection_draining_timeout'] = check_elb_attrs['ConnectionDraining']['Timeout'] except KeyError: pass try: info['idle_timeout'] = check_elb_attrs['ConnectionSettings']['IdleTimeout'] except KeyError: pass try: is_enabled = check_elb_attrs['CrossZoneLoadBalancing']['Enabled'] info['cross_az_load_balancing'] = 'yes' if is_enabled else 'no' except KeyError: pass # # return stickiness info? try: info['tags'] = self._get_tags() except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: self.module.fail_json_aws(e, msg="Failed to get load balancer tags") return info @property def _waiter_config(self): delay = min(10, self.wait_timeout) max_attempts = (self.wait_timeout // delay) return {'Delay': delay, 'MaxAttempts': max_attempts} def _wait_for_elb_created(self): if self.check_mode: return True waiter = get_waiter(self.client, 'load_balancer_created') try: waiter.wait( WaiterConfig=self._waiter_config, LoadBalancerNames=[self.name], ) except botocore.exceptions.WaiterError as e: self.module.fail_json_aws(e, 'Timeout waiting for ELB removal') return True def _wait_for_elb_interface_created(self): if self.check_mode: return True waiter = get_waiter(self.ec2_client, 'network_interface_available') filters = ansible_dict_to_boto3_filter_list( {'requester-id': 'amazon-elb', 'description': 'ELB {0}'.format(self.name)} ) try: waiter.wait( WaiterConfig=self._waiter_config, Filters=filters, ) except botocore.exceptions.WaiterError as e: self.module.fail_json_aws(e, 'Timeout waiting for ELB Interface removal') return True def _wait_for_elb_removed(self): if self.check_mode: return True waiter = get_waiter(self.client, 'load_balancer_deleted') try: waiter.wait( WaiterConfig=self._waiter_config, LoadBalancerNames=[self.name], ) except botocore.exceptions.WaiterError as e: self.module.fail_json_aws(e, 'Timeout waiting for ELB removal') return True def _wait_for_elb_interface_removed(self): if self.check_mode: return True waiter = get_waiter(self.ec2_client, 'network_interface_deleted') filters = ansible_dict_to_boto3_filter_list( {'requester-id': 'amazon-elb', 'description': 'ELB {0}'.format(self.name)} ) try: waiter.wait( WaiterConfig=self._waiter_config, Filters=filters, ) except botocore.exceptions.WaiterError as e: self.module.fail_json_aws(e, 'Timeout waiting for ELB Interface removal') return True def _wait_for_instance_state(self, waiter_name, instances): if not instances: return False if self.check_mode: return True waiter = get_waiter(self.client, waiter_name) instance_list = list(dict(InstanceId=instance) for instance in instances) try: waiter.wait( WaiterConfig=self._waiter_config, LoadBalancerName=self.name, Instances=instance_list, ) except botocore.exceptions.WaiterError as e: self.module.fail_json_aws(e, 'Timeout waiting for ELB Instance State') return True def _create_elb_listeners(self, listeners): """Takes a list of listener definitions and creates them""" if not listeners: return False self.changed = True if self.check_mode: return True self.client.create_load_balancer_listeners( aws_retry=True, LoadBalancerName=self.name, Listeners=listeners, ) return True def _delete_elb_listeners(self, ports): """Takes a list of listener ports and deletes them from the ELB""" if not ports: return False self.changed = True if self.check_mode: return True self.client.delete_load_balancer_listeners( aws_retry=True, LoadBalancerName=self.name, LoadBalancerPorts=ports, ) return True def _set_elb_listeners(self): """ Creates listeners specified by self.listeners; overwrites existing listeners on these ports; removes extraneous listeners """ if not self.listeners: return False # We can't use sets here: dicts aren't hashable, so convert to the boto3 # format and use a generator to filter new_listeners = list(self._format_listener(l, True) for l in self.listeners) existing_listeners = list(l['Listener'] for l in self.elb['ListenerDescriptions']) listeners_to_remove = list(l for l in existing_listeners if l not in new_listeners) listeners_to_add = list(l for l in new_listeners if l not in existing_listeners) changed = False if self.purge_listeners: ports_to_remove = list(l['LoadBalancerPort'] for l in listeners_to_remove) else: old_ports = set(l['LoadBalancerPort'] for l in listeners_to_remove) new_ports = set(l['LoadBalancerPort'] for l in listeners_to_add) # If we're not purging, then we need to remove Listeners # where the full definition doesn't match, but the port does ports_to_remove = list(old_ports & new_ports) # Update is a delete then add, so do the deletion first try: changed |= self._delete_elb_listeners(ports_to_remove) except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: self.module.fail_json_aws(e, msg="Failed to remove listeners from load balancer") try: changed |= self._create_elb_listeners(listeners_to_add) except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: self.module.fail_json_aws(e, msg="Failed to remove listeners from load balancer") return changed def _api_listener_as_tuple(self, listener): """Adds ssl_certificate_id to ELB API tuple if present""" base_tuple = [ listener.get('LoadBalancerPort'), listener.get('InstancePort'), listener.get('Protocol'), listener.get('InstanceProtocol'), ] if listener.get('SSLCertificateId', False): base_tuple.append(listener.get('SSLCertificateId')) return tuple(base_tuple) def _attach_subnets(self, subnets): if not subnets: return False self.changed = True if self.check_mode: return True self.client.attach_load_balancer_to_subnets( aws_retry=True, LoadBalancerName=self.name, Subnets=subnets) return True def _detach_subnets(self, subnets): if not subnets: return False self.changed = True if self.check_mode: return True self.client.detach_load_balancer_from_subnets( aws_retry=True, LoadBalancerName=self.name, Subnets=subnets) return True def _set_subnets(self): """Determine which subnets need to be attached or detached on the ELB""" # Subnets parameter not set, nothing to change if self.subnets is None: return False changed = False if self.purge_subnets: subnets_to_detach = list(set(self.elb['Subnets']) - set(self.subnets)) else: subnets_to_detach = list() subnets_to_attach = list(set(self.subnets) - set(self.elb['Subnets'])) # You can't add multiple subnets from the same AZ. Remove first, then # add. try: changed |= self._detach_subnets(subnets_to_detach) except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: self.module.fail_json_aws(e, msg="Failed to detach subnets from load balancer") try: changed |= self._attach_subnets(subnets_to_attach) except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: self.module.fail_json_aws(e, msg="Failed to attach subnets to load balancer") return changed def _check_scheme(self): """Determine if the current scheme is different than the scheme of the ELB""" if self.scheme: if self.elb['Scheme'] != self.scheme: return True return False def _enable_zones(self, zones): if not zones: return False self.changed = True if self.check_mode: return True try: self.client.enable_availability_zones_for_load_balancer( aws_retry=True, LoadBalancerName=self.name, AvailabilityZones=zones, ) except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: self.module.fail_json_aws(e, msg='Failed to enable zones for load balancer') return True def _disable_zones(self, zones): if not zones: return False self.changed = True if self.check_mode: return True try: self.client.disable_availability_zones_for_load_balancer( aws_retry=True, LoadBalancerName=self.name, AvailabilityZones=zones, ) except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: self.module.fail_json_aws(e, msg='Failed to disable zones for load balancer') return True def _set_zones(self): """Determine which zones need to be enabled or disabled on the ELB""" # zones parameter not set, nothing to changeA if self.zones is None: return False changed = False if self.purge_zones: zones_to_disable = list(set(self.elb['AvailabilityZones']) - set(self.zones)) else: zones_to_disable = list() zones_to_enable = list(set(self.zones) - set(self.elb['AvailabilityZones'])) # Add before we remove to reduce the chance of an outage if someone # replaces all zones at once try: changed |= self._enable_zones(zones_to_enable) except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: self.module.fail_json_aws(e, msg="Failed to enable zone on load balancer") try: changed |= self._disable_zones(zones_to_disable) except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: self.module.fail_json_aws(e, msg="Failed to attach zone to load balancer") return changed def _set_security_groups(self): if not self.security_group_ids: return False # Security Group Names should already by converted to IDs by this point. if set(self.elb['SecurityGroups']) == set(self.security_group_ids): return False self.changed = True if self.check_mode: return True try: self.client.apply_security_groups_to_load_balancer( aws_retry=True, LoadBalancerName=self.name, SecurityGroups=self.security_group_ids, ) except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: self.module.fail_json_aws(e, msg="Failed to apply security groups to load balancer") return True def _set_health_check(self): if not self.health_check: return False """Set health check values on ELB as needed""" health_check_config = self._format_healthcheck() if self.elb and health_check_config == self.elb['HealthCheck']: return False self.changed = True if self.check_mode: return True try: self.client.configure_health_check( aws_retry=True, LoadBalancerName=self.name, HealthCheck=health_check_config, ) except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: self.module.fail_json_aws(e, msg="Failed to apply healthcheck to load balancer") return True def _set_elb_attributes(self): attributes = {} if self.cross_az_load_balancing is not None: attr = dict(Enabled=self.cross_az_load_balancing) if not self.elb_attributes.get('CrossZoneLoadBalancing', None) == attr: attributes['CrossZoneLoadBalancing'] = attr if self.idle_timeout is not None: attr = dict(IdleTimeout=self.idle_timeout) if not self.elb_attributes.get('ConnectionSettings', None) == attr: attributes['ConnectionSettings'] = attr if self.connection_draining_timeout is not None: curr_attr = dict(self.elb_attributes.get('ConnectionDraining', {})) if self.connection_draining_timeout == 0: attr = dict(Enabled=False) curr_attr.pop('Timeout', None) else: attr = dict(Enabled=True, Timeout=self.connection_draining_timeout) if not curr_attr == attr: attributes['ConnectionDraining'] = attr if self.access_logs is not None: curr_attr = dict(self.elb_attributes.get('AccessLog', {})) # For disabling we only need to compare and pass 'Enabled' if not self.access_logs.get('enabled'): curr_attr = dict(Enabled=curr_attr.get('Enabled', False)) attr = dict(Enabled=self.access_logs.get('enabled')) else: attr = dict( Enabled=True, S3BucketName=self.access_logs['s3_location'], S3BucketPrefix=self.access_logs.get('s3_prefix', ''), EmitInterval=self.access_logs.get('interval', 60), ) if not curr_attr == attr: attributes['AccessLog'] = attr if not attributes: return False self.changed = True if self.check_mode: return True try: self.client.modify_load_balancer_attributes( aws_retry=True, LoadBalancerName=self.name, LoadBalancerAttributes=attributes ) except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: self.module.fail_json_aws(e, msg="Failed to apply load balancer attrbutes") def _proxy_policy_name(self): return 'ProxyProtocol-policy' def _policy_name(self, policy_type): return 'ec2-elb-lb-{0}'.format(policy_type) def _get_listener_policies(self): """Get a list of listener policies mapped to the LoadBalancerPort""" if not self.elb: return {} listener_descriptions = self.elb.get('ListenerDescriptions', []) policies = {l['LoadBalancerPort']: l['PolicyNames'] for l in listener_descriptions} return policies def _set_listener_policies(self, port, policies): self.changed = True if self.check_mode: return True try: self.client.set_load_balancer_policies_of_listener( aws_retry=True, LoadBalancerName=self.name, LoadBalancerPort=port, PolicyNames=list(policies), ) except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: self.module.fail_json_aws(e, msg="Failed to set load balancer listener policies", port=port, policies=policies) return True def _get_stickiness_policies(self): """Get a list of AppCookieStickinessPolicyType and LBCookieStickinessPolicyType policies""" return list(p['PolicyName'] for p in self.elb_policies if p['PolicyTypeName'] in ['AppCookieStickinessPolicyType', 'LBCookieStickinessPolicyType']) def _get_app_stickness_policy_map(self): """Get a mapping of App Cookie Stickiness policy names to their definitions""" policies = self.elb.get('Policies', {}).get('AppCookieStickinessPolicies', []) return {p['PolicyName']: p for p in policies} def _get_lb_stickness_policy_map(self): """Get a mapping of LB Cookie Stickiness policy names to their definitions""" policies = self.elb.get('Policies', {}).get('LBCookieStickinessPolicies', []) return {p['PolicyName']: p for p in policies} def _purge_stickiness_policies(self): """Removes all stickiness policies from all Load Balancers""" # Used when purging stickiness policies or updating a policy (you can't # update a policy while it's connected to a Listener) stickiness_policies = set(self._get_stickiness_policies()) listeners = self.elb['ListenerDescriptions'] changed = False for listener in listeners: port = listener['Listener']['LoadBalancerPort'] policies = set(listener['PolicyNames']) new_policies = set(policies - stickiness_policies) if policies != new_policies: changed |= self._set_listener_policies(port, new_policies) return changed def _set_stickiness_policies(self): if self.stickiness is None: return False # Make sure that the list of policies and listeners is up to date, we're # going to make changes to all listeners self._update_descriptions() if not self.stickiness['enabled']: return self._purge_stickiness_policies() if self.stickiness['type'] == 'loadbalancer': policy_name = self._policy_name('LBCookieStickinessPolicyType') expiration = self.stickiness.get('expiration') if not expiration: expiration = 0 policy_description = dict( PolicyName=policy_name, CookieExpirationPeriod=expiration, ) existing_policies = self._get_lb_stickness_policy_map() add_method = self.client.create_lb_cookie_stickiness_policy elif self.stickiness['type'] == 'application': policy_name = self._policy_name('AppCookieStickinessPolicyType') policy_description = dict( PolicyName=policy_name, CookieName=self.stickiness.get('cookie', 0) ) existing_policies = self._get_app_stickness_policy_map() add_method = self.client.create_app_cookie_stickiness_policy else: # We shouldn't get here... self.module.fail_json( msg='Unknown stickiness policy {0}'.format( self.stickiness['type'] ) ) changed = False # To update a policy we need to delete then re-add, and we can only # delete if the policy isn't attached to a listener if policy_name in existing_policies: if existing_policies[policy_name] != policy_description: changed |= self._purge_stickiness_policies() if changed: self._update_descriptions() changed |= self._set_stickiness_policy( method=add_method, description=policy_description, existing_policies=existing_policies, ) listeners = self.elb['ListenerDescriptions'] for listener in listeners: changed |= self._set_lb_stickiness_policy( listener=listener, policy=policy_name ) return changed def _delete_loadbalancer_policy(self, policy_name): self.changed = True if self.check_mode: return True try: self.client.delete_load_balancer_policy( LoadBalancerName=self.name, PolicyName=policy_name, ) except is_boto3_error_code('InvalidConfigurationRequest'): # Already deleted return False except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: # pylint: disable=duplicate-except self.module.fail_json_aws(e, msg="Failed to load balancer policy {0}".format(policy_name)) return True def _set_stickiness_policy(self, method, description, existing_policies=None): changed = False if existing_policies: policy_name = description['PolicyName'] if policy_name in existing_policies: if existing_policies[policy_name] == description: return False if existing_policies[policy_name] != description: changed |= self._delete_loadbalancer_policy(policy_name) self.changed = True changed = True if self.check_mode: return changed # This needs to be in place for comparisons, but not passed to the # method. if not description.get('CookieExpirationPeriod', None): description.pop('CookieExpirationPeriod', None) try: method( aws_retry=True, LoadBalancerName=self.name, **description ) except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: self.module.fail_json_aws(e, msg="Failed to create load balancer stickiness policy", description=description) return changed def _set_lb_stickiness_policy(self, listener, policy): port = listener['Listener']['LoadBalancerPort'] stickiness_policies = set(self._get_stickiness_policies()) changed = False policies = set(listener['PolicyNames']) new_policies = list(policies - stickiness_policies) new_policies.append(policy) if policies != set(new_policies): changed |= self._set_listener_policies(port, new_policies) return changed def _get_backend_policies(self): """Get a list of backend policies mapped to the InstancePort""" if not self.elb: return {} server_descriptions = self.elb.get('BackendServerDescriptions', []) policies = {b['InstancePort']: b['PolicyNames'] for b in server_descriptions} return policies def _get_proxy_protocol_policy(self): """Returns the name of the name of the ProxyPolicy if created""" all_proxy_policies = self._get_proxy_policies() if not all_proxy_policies: return None if len(all_proxy_policies) == 1: return all_proxy_policies[0] return all_proxy_policies def _get_proxy_policies(self): """Get a list of ProxyProtocolPolicyType policies""" return list(p['PolicyName'] for p in self.elb_policies if p['PolicyTypeName'] == 'ProxyProtocolPolicyType') def _get_policy_map(self): """Get a mapping of Policy names to their definitions""" return {p['PolicyName']: p for p in self.elb_policies} def _set_backend_policies(self): """Sets policies for all backends""" # Currently only supports setting ProxyProtocol policies if not self.listeners: return False backend_policies = self._get_backend_policies() proxy_policies = set(self._get_proxy_policies()) proxy_ports = dict() for listener in self.listeners: proxy_protocol = listener.get('proxy_protocol', None) # Only look at the listeners for which proxy_protocol is defined if proxy_protocol is None: next instance_port = listener.get('instance_port') if proxy_ports.get(instance_port, None) is not None: if proxy_ports[instance_port] != proxy_protocol: self.module.fail_json_aws( 'proxy_protocol set to conflicting values for listeners' ' on port {0}'.format(instance_port)) proxy_ports[instance_port] = proxy_protocol if not proxy_ports: return False changed = False # If anyone's set proxy_protocol to true, make sure we have our policy # in place. proxy_policy_name = self._proxy_policy_name() if any(proxy_ports.values()): changed |= self._set_proxy_protocol_policy(proxy_policy_name) for port in proxy_ports: current_policies = set(backend_policies.get(port, [])) new_policies = list(current_policies - proxy_policies) if proxy_ports[port]: new_policies.append(proxy_policy_name) changed |= self._set_backend_policy(port, new_policies) return changed def _set_backend_policy(self, port, policies): backend_policies = self._get_backend_policies() current_policies = set(backend_policies.get(port, [])) if current_policies == set(policies): return False self.changed = True if self.check_mode: return True try: self.client.set_load_balancer_policies_for_backend_server( aws_retry=True, LoadBalancerName=self.name, InstancePort=port, PolicyNames=policies, ) except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: self.module.fail_json_aws(e, msg="Failed to set load balancer backend policies", port=port, policies=policies) return True def _set_proxy_protocol_policy(self, policy_name): """Install a proxy protocol policy if needed""" policy_map = self._get_policy_map() policy_attributes = [dict(AttributeName='ProxyProtocol', AttributeValue='true')] proxy_policy = dict( PolicyName=policy_name, PolicyTypeName='ProxyProtocolPolicyType', PolicyAttributeDescriptions=policy_attributes, ) existing_policy = policy_map.get(policy_name) if proxy_policy == existing_policy: return False if existing_policy is not None: self.module.fail_json( msg="Unable to configure ProxyProtocol policy. " "Policy with name {0} already exists and doesn't match.".format(policy_name), policy=proxy_policy, existing_policy=existing_policy, ) proxy_policy['PolicyAttributes'] = proxy_policy.pop('PolicyAttributeDescriptions') proxy_policy['LoadBalancerName'] = self.name self.changed = True if self.check_mode: return True try: self.client.create_load_balancer_policy( aws_retry=True, **proxy_policy ) except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: self.module.fail_json_aws(e, msg="Failed to create load balancer policy", policy=proxy_policy) return True def _get_instance_ids(self): """Get the current list of instance ids installed in the elb""" elb = self.elb or {} return list(i['InstanceId'] for i in elb.get('Instances', [])) def _change_instances(self, method, instances): if not instances: return False self.changed = True if self.check_mode: return True instance_id_list = list({'InstanceId': i} for i in instances) try: method( aws_retry=True, LoadBalancerName=self.name, Instances=instance_id_list, ) except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: self.module.fail_json_aws(e, msg="Failed to change instance registration", instances=instance_id_list, name=self.name) return True def _set_instance_ids(self): """Register or deregister instances from an lb instance""" new_instances = self.instance_ids or [] existing_instances = self._get_instance_ids() instances_to_add = set(new_instances) - set(existing_instances) if self.purge_instance_ids: instances_to_remove = set(existing_instances) - set(new_instances) else: instances_to_remove = [] changed = False changed |= self._change_instances(self.client.register_instances_with_load_balancer, instances_to_add) if self.wait: self._wait_for_instance_state('instance_in_service', list(instances_to_add)) changed |= self._change_instances(self.client.deregister_instances_from_load_balancer, instances_to_remove) if self.wait: self._wait_for_instance_state('instance_deregistered', list(instances_to_remove)) return changed def _get_tags(self): tags = self.client.describe_tags(aws_retry=True, LoadBalancerNames=[self.name]) if not tags: return {} try: tags = tags['TagDescriptions'][0]['Tags'] except (KeyError, TypeError): return {} return boto3_tag_list_to_ansible_dict(tags) def _add_tags(self, tags_to_set): if not tags_to_set: return False self.changed = True if self.check_mode: return True tags_to_add = ansible_dict_to_boto3_tag_list(tags_to_set) self.client.add_tags(LoadBalancerNames=[self.name], Tags=tags_to_add) return True def _remove_tags(self, tags_to_unset): if not tags_to_unset: return False self.changed = True if self.check_mode: return True tags_to_remove = [dict(Key=tagkey) for tagkey in tags_to_unset] self.client.remove_tags(LoadBalancerNames=[self.name], Tags=tags_to_remove) return True def _set_tags(self): """Add/Delete tags""" if self.tags is None: return False try: current_tags = self._get_tags() except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: self.module.fail_json_aws(e, msg="Failed to get load balancer tags") tags_to_set, tags_to_unset = compare_aws_tags(current_tags, self.tags, self.purge_tags) changed = False try: changed |= self._remove_tags(tags_to_unset) except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: self.module.fail_json_aws(e, msg="Failed to remove load balancer tags") try: changed |= self._add_tags(tags_to_set) except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: self.module.fail_json_aws(e, msg="Failed to add load balancer tags") return changed def _validate_stickiness(self, stickiness): problem_found = False if not stickiness: return problem_found if not stickiness['enabled']: return problem_found if stickiness['type'] == 'application': if not stickiness.get('cookie'): problem_found = True self.module.fail_json( msg='cookie must be specified when stickiness type is "application"', stickiness=stickiness, ) if stickiness.get('expiration'): self.warn( msg='expiration is ignored when stickiness type is "application"',) if stickiness['type'] == 'loadbalancer': if stickiness.get('cookie'): self.warn( msg='cookie is ignored when stickiness type is "loadbalancer"',) return problem_found def _validate_access_logs(self, access_logs): problem_found = False if not access_logs: return problem_found if not access_logs['enabled']: return problem_found if not access_logs.get('s3_location', None): problem_found = True self.module.fail_json( msg='s3_location must be provided when access_logs.state is "present"') return problem_found def _validate_creation_requirements(self): if self.elb: return False problem_found = False if not self.subnets and not self.zones: problem_found = True self.module.fail_json( msg='One of subnets or zones must be provided when creating an ELB') if not self.listeners: problem_found = True self.module.fail_json( msg='listeners must be provided when creating an ELB') return problem_found def _validate_listeners(self, listeners): if not listeners: return False return any(self._validate_listener(listener) for listener in listeners) def _validate_listener(self, listener): problem_found = False if not listener: return problem_found for protocol in ['instance_protocol', 'protocol']: value = listener.get(protocol, None) problem = self._validate_protocol(value) problem_found |= problem if problem: self.module.fail_json( msg='Invalid protocol ({0}) in listener'.format(value), listener=listener) return problem_found def _validate_health_check(self, health_check): if not health_check: return False protocol = health_check['ping_protocol'] if self._validate_protocol(protocol): self.module.fail_json( msg='Invalid protocol ({0}) defined in health check'.format(protocol), health_check=health_check,) if protocol.upper() in ['HTTP', 'HTTPS']: if not health_check['ping_path']: self.module.fail_json( msg='For HTTP and HTTPS health checks a ping_path must be provided', health_check=health_check,) return False def _validate_protocol(self, protocol): if not protocol: return False return protocol.upper() not in ['HTTP', 'HTTPS', 'TCP', 'SSL'] @AWSRetry.jittered_backoff() def _describe_loadbalancer(self, lb_name): paginator = self.client.get_paginator('describe_load_balancers') return paginator.paginate(LoadBalancerNames=[lb_name]).build_full_result()['LoadBalancerDescriptions'] def _get_vpc_from_subnets(self, subnets): if not subnets: return None subnet_details = self._describe_subnets(list(subnets)) vpc_ids = set(subnet['VpcId'] for subnet in subnet_details) if not vpc_ids: return None if len(vpc_ids) > 1: self.module.fail_json("Subnets for an ELB may not span multiple VPCs", subnets=subnet_details, vpc_ids=vpc_ids) return vpc_ids.pop() @AWSRetry.jittered_backoff() def _describe_subnets(self, subnet_ids): paginator = self.ec2_client.get_paginator('describe_subnets') return paginator.paginate(SubnetIds=subnet_ids).build_full_result()['Subnets'] # Wrap it so we get the backoff @AWSRetry.jittered_backoff() def _get_ec2_security_group_ids_from_names(self, **params): return get_ec2_security_group_ids_from_names(ec2_connection=self.ec2_client, **params) def main(): access_log_spec = dict( enabled=dict(required=False, type='bool', default=True), s3_location=dict(required=False, type='str'), s3_prefix=dict(required=False, type='str', default=""), interval=dict(required=False, type='int', default=60, choices=[5, 60]), ) stickiness_spec = dict( type=dict(required=False, type='str', choices=['application', 'loadbalancer']), enabled=dict(required=False, type='bool', default=True), cookie=dict(required=False, type='str'), expiration=dict(required=False, type='int') ) healthcheck_spec = dict( ping_protocol=dict(required=True, type='str'), ping_path=dict(required=False, type='str'), ping_port=dict(required=True, type='int'), interval=dict(required=True, type='int'), timeout=dict(aliases=['response_timeout'], required=True, type='int'), unhealthy_threshold=dict(required=True, type='int'), healthy_threshold=dict(required=True, type='int'), ) listeners_spec = dict( load_balancer_port=dict(required=True, type='int'), instance_port=dict(required=True, type='int'), ssl_certificate_id=dict(required=False, type='str'), protocol=dict(required=True, type='str'), instance_protocol=dict(required=False, type='str'), proxy_protocol=dict(required=False, type='bool'), ) argument_spec = dict( state=dict(required=True, choices=['present', 'absent']), name=dict(required=True), listeners=dict(type='list', elements='dict', options=listeners_spec), purge_listeners=dict(default=True, type='bool'), instance_ids=dict(type='list', elements='str'), purge_instance_ids=dict(default=False, type='bool'), zones=dict(type='list', elements='str'), purge_zones=dict(default=False, type='bool'), security_group_ids=dict(type='list', elements='str'), security_group_names=dict(type='list', elements='str'), health_check=dict(type='dict', options=healthcheck_spec), subnets=dict(type='list', elements='str'), purge_subnets=dict(default=False, type='bool'), scheme=dict(choices=['internal', 'internet-facing']), connection_draining_timeout=dict(type='int'), idle_timeout=dict(type='int'), cross_az_load_balancing=dict(type='bool'), stickiness=dict(type='dict', options=stickiness_spec), access_logs=dict(type='dict', options=access_log_spec), wait=dict(default=False, type='bool'), wait_timeout=dict(default=180, type='int'), tags=dict(type='dict', aliases=['resource_tags']), purge_tags=dict(default=True, type='bool'), ) module = AnsibleAWSModule( argument_spec=argument_spec, mutually_exclusive=[ ['security_group_ids', 'security_group_names'], ['zones', 'subnets'], ], supports_check_mode=True, ) wait_timeout = module.params['wait_timeout'] state = module.params['state'] if wait_timeout > 600: module.fail_json(msg='wait_timeout maximum is 600 seconds') elb_man = ElbManager(module) elb_man.validate_params(state) if state == 'present': elb_man.ensure_ok() # original boto style elb = elb_man.get_info() # boto3 style lb = elb_man.get_load_balancer() ec2_result = dict(elb=elb, load_balancer=lb) elif state == 'absent': elb_man.ensure_gone() # original boto style elb = elb_man.get_info() ec2_result = dict(elb=elb) module.exit_json( changed=elb_man.changed, **ec2_result, ) if __name__ == '__main__': main()