1#!/usr/bin/python 2# Copyright: Ansible Project 3# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 4 5from __future__ import absolute_import, division, print_function 6__metaclass__ = type 7 8 9ANSIBLE_METADATA = {'metadata_version': '1.1', 10 'status': ['stableinterface'], 11 'supported_by': 'community'} 12 13 14DOCUMENTATION = """ 15--- 16module: ec2_elb_lb 17description: 18 - Returns information about the load balancer. 19 - Will be marked changed when called only if state is changed. 20short_description: Creates, updates or destroys an Amazon ELB. 21version_added: "1.5" 22author: 23 - "Jim Dalton (@jsdalton)" 24options: 25 state: 26 description: 27 - Create or destroy the ELB 28 type: str 29 choices: [ absent, present ] 30 required: true 31 name: 32 description: 33 - The name of the ELB 34 type: str 35 required: true 36 listeners: 37 description: 38 - List of ports/protocols for this ELB to listen on (see example) 39 type: list 40 purge_listeners: 41 description: 42 - Purge existing listeners on ELB that are not found in listeners 43 type: bool 44 default: yes 45 instance_ids: 46 description: 47 - List of instance ids to attach to this ELB 48 type: list 49 version_added: "2.1" 50 purge_instance_ids: 51 description: 52 - Purge existing instance ids on ELB that are not found in instance_ids 53 type: bool 54 default: no 55 version_added: "2.1" 56 zones: 57 description: 58 - List of availability zones to enable on this ELB 59 type: list 60 purge_zones: 61 description: 62 - Purge existing availability zones on ELB that are not found in zones 63 type: bool 64 default: no 65 security_group_ids: 66 description: 67 - A list of security groups to apply to the elb 68 type: list 69 version_added: "1.6" 70 security_group_names: 71 description: 72 - A list of security group names to apply to the elb 73 type: list 74 version_added: "2.0" 75 health_check: 76 description: 77 - An associative array of health check configuration settings (see example) 78 type: dict 79 access_logs: 80 description: 81 - An associative array of access logs configuration settings (see example) 82 type: dict 83 version_added: "2.0" 84 subnets: 85 description: 86 - A list of VPC subnets to use when creating ELB. Zones should be empty if using this. 87 type: list 88 version_added: "1.7" 89 purge_subnets: 90 description: 91 - Purge existing subnet on ELB that are not found in subnets 92 type: bool 93 default: no 94 version_added: "1.7" 95 scheme: 96 description: 97 - The scheme to use when creating the ELB. For a private VPC-visible ELB use 'internal'. 98 If you choose to update your scheme with a different value the ELB will be destroyed and 99 recreated. To update scheme you must use the option wait. 100 type: str 101 choices: ["internal", "internet-facing"] 102 default: 'internet-facing' 103 version_added: "1.7" 104 validate_certs: 105 description: 106 - When set to C(no), SSL certificates will not be validated for boto versions >= 2.6.0. 107 type: bool 108 default: yes 109 version_added: "1.5" 110 connection_draining_timeout: 111 description: 112 - Wait a specified timeout allowing connections to drain before terminating an instance 113 type: int 114 version_added: "1.8" 115 idle_timeout: 116 description: 117 - ELB connections from clients and to servers are timed out after this amount of time 118 type: int 119 version_added: "2.0" 120 cross_az_load_balancing: 121 description: 122 - Distribute load across all configured Availability Zones 123 type: bool 124 default: no 125 version_added: "1.8" 126 stickiness: 127 description: 128 - An associative array of stickiness policy settings. Policy will be applied to all listeners ( see example ) 129 type: dict 130 version_added: "2.0" 131 wait: 132 description: 133 - When specified, Ansible will check the status of the load balancer to ensure it has been successfully 134 removed from AWS. 135 type: bool 136 default: no 137 version_added: "2.1" 138 wait_timeout: 139 description: 140 - Used in conjunction with wait. Number of seconds to wait for the elb to be terminated. 141 A maximum of 600 seconds (10 minutes) is allowed. 142 type: int 143 default: 60 144 version_added: "2.1" 145 tags: 146 description: 147 - An associative array of tags. To delete all tags, supply an empty dict. 148 type: dict 149 version_added: "2.1" 150 151extends_documentation_fragment: 152 - aws 153 - ec2 154""" 155 156EXAMPLES = """ 157# Note: None of these examples set aws_access_key, aws_secret_key, or region. 158# It is assumed that their matching environment variables are set. 159 160# Basic provisioning example (non-VPC) 161 162- local_action: 163 module: ec2_elb_lb 164 name: "test-please-delete" 165 state: present 166 zones: 167 - us-east-1a 168 - us-east-1d 169 listeners: 170 - protocol: http # options are http, https, ssl, tcp 171 load_balancer_port: 80 172 instance_port: 80 173 proxy_protocol: True 174 - protocol: https 175 load_balancer_port: 443 176 instance_protocol: http # optional, defaults to value of protocol setting 177 instance_port: 80 178 # ssl certificate required for https or ssl 179 ssl_certificate_id: "arn:aws:iam::123456789012:server-certificate/company/servercerts/ProdServerCert" 180 181# Internal ELB example 182 183- local_action: 184 module: ec2_elb_lb 185 name: "test-vpc" 186 scheme: internal 187 state: present 188 instance_ids: 189 - i-abcd1234 190 purge_instance_ids: true 191 subnets: 192 - subnet-abcd1234 193 - subnet-1a2b3c4d 194 listeners: 195 - protocol: http # options are http, https, ssl, tcp 196 load_balancer_port: 80 197 instance_port: 80 198 199# Configure a health check and the access logs 200- local_action: 201 module: ec2_elb_lb 202 name: "test-please-delete" 203 state: present 204 zones: 205 - us-east-1d 206 listeners: 207 - protocol: http 208 load_balancer_port: 80 209 instance_port: 80 210 health_check: 211 ping_protocol: http # options are http, https, ssl, tcp 212 ping_port: 80 213 ping_path: "/index.html" # not required for tcp or ssl 214 response_timeout: 5 # seconds 215 interval: 30 # seconds 216 unhealthy_threshold: 2 217 healthy_threshold: 10 218 access_logs: 219 interval: 5 # minutes (defaults to 60) 220 s3_location: "my-bucket" # This value is required if access_logs is set 221 s3_prefix: "logs" 222 223# Ensure ELB is gone 224- local_action: 225 module: ec2_elb_lb 226 name: "test-please-delete" 227 state: absent 228 229# Ensure ELB is gone and wait for check (for default timeout) 230- local_action: 231 module: ec2_elb_lb 232 name: "test-please-delete" 233 state: absent 234 wait: yes 235 236# Ensure ELB is gone and wait for check with timeout value 237- local_action: 238 module: ec2_elb_lb 239 name: "test-please-delete" 240 state: absent 241 wait: yes 242 wait_timeout: 600 243 244# Normally, this module will purge any listeners that exist on the ELB 245# but aren't specified in the listeners parameter. If purge_listeners is 246# false it leaves them alone 247- local_action: 248 module: ec2_elb_lb 249 name: "test-please-delete" 250 state: present 251 zones: 252 - us-east-1a 253 - us-east-1d 254 listeners: 255 - protocol: http 256 load_balancer_port: 80 257 instance_port: 80 258 purge_listeners: no 259 260# Normally, this module will leave availability zones that are enabled 261# on the ELB alone. If purge_zones is true, then any extraneous zones 262# will be removed 263- local_action: 264 module: ec2_elb_lb 265 name: "test-please-delete" 266 state: present 267 zones: 268 - us-east-1a 269 - us-east-1d 270 listeners: 271 - protocol: http 272 load_balancer_port: 80 273 instance_port: 80 274 purge_zones: yes 275 276# Creates a ELB and assigns a list of subnets to it. 277- local_action: 278 module: ec2_elb_lb 279 state: present 280 name: 'New ELB' 281 security_group_ids: 'sg-123456, sg-67890' 282 region: us-west-2 283 subnets: 'subnet-123456,subnet-67890' 284 purge_subnets: yes 285 listeners: 286 - protocol: http 287 load_balancer_port: 80 288 instance_port: 80 289 290# Create an ELB with connection draining, increased idle timeout and cross availability 291# zone load balancing 292- local_action: 293 module: ec2_elb_lb 294 name: "New ELB" 295 state: present 296 connection_draining_timeout: 60 297 idle_timeout: 300 298 cross_az_load_balancing: "yes" 299 region: us-east-1 300 zones: 301 - us-east-1a 302 - us-east-1d 303 listeners: 304 - protocol: http 305 load_balancer_port: 80 306 instance_port: 80 307 308# Create an ELB with load balancer stickiness enabled 309- local_action: 310 module: ec2_elb_lb 311 name: "New ELB" 312 state: present 313 region: us-east-1 314 zones: 315 - us-east-1a 316 - us-east-1d 317 listeners: 318 - protocol: http 319 load_balancer_port: 80 320 instance_port: 80 321 stickiness: 322 type: loadbalancer 323 enabled: yes 324 expiration: 300 325 326# Create an ELB with application stickiness enabled 327- local_action: 328 module: ec2_elb_lb 329 name: "New ELB" 330 state: present 331 region: us-east-1 332 zones: 333 - us-east-1a 334 - us-east-1d 335 listeners: 336 - protocol: http 337 load_balancer_port: 80 338 instance_port: 80 339 stickiness: 340 type: application 341 enabled: yes 342 cookie: SESSIONID 343 344# Create an ELB and add tags 345- local_action: 346 module: ec2_elb_lb 347 name: "New ELB" 348 state: present 349 region: us-east-1 350 zones: 351 - us-east-1a 352 - us-east-1d 353 listeners: 354 - protocol: http 355 load_balancer_port: 80 356 instance_port: 80 357 tags: 358 Name: "New ELB" 359 stack: "production" 360 client: "Bob" 361 362# Delete all tags from an ELB 363- local_action: 364 module: ec2_elb_lb 365 name: "New ELB" 366 state: present 367 region: us-east-1 368 zones: 369 - us-east-1a 370 - us-east-1d 371 listeners: 372 - protocol: http 373 load_balancer_port: 80 374 instance_port: 80 375 tags: {} 376""" 377 378import random 379import time 380import traceback 381 382try: 383 import boto 384 import boto.ec2.elb 385 import boto.ec2.elb.attributes 386 import boto.vpc 387 from boto.ec2.elb.healthcheck import HealthCheck 388 from boto.ec2.tag import Tag 389 HAS_BOTO = True 390except ImportError: 391 HAS_BOTO = False 392 393from ansible.module_utils.basic import AnsibleModule 394from ansible.module_utils.ec2 import ec2_argument_spec, connect_to_aws, AnsibleAWSError, get_aws_connection_info 395from ansible.module_utils.six import string_types 396from ansible.module_utils._text import to_native 397 398 399def _throttleable_operation(max_retries): 400 def _operation_wrapper(op): 401 def _do_op(*args, **kwargs): 402 retry = 0 403 while True: 404 try: 405 return op(*args, **kwargs) 406 except boto.exception.BotoServerError as e: 407 if retry < max_retries and e.code in \ 408 ("Throttling", "RequestLimitExceeded"): 409 retry = retry + 1 410 time.sleep(min(random.random() * (2 ** retry), 300)) 411 continue 412 else: 413 raise 414 return _do_op 415 return _operation_wrapper 416 417 418def _get_vpc_connection(module, region, aws_connect_params): 419 try: 420 return connect_to_aws(boto.vpc, region, **aws_connect_params) 421 except (boto.exception.NoAuthHandlerFound, AnsibleAWSError) as e: 422 module.fail_json(msg=str(e)) 423 424 425_THROTTLING_RETRIES = 5 426 427 428class ElbManager(object): 429 """Handles ELB creation and destruction""" 430 431 def __init__(self, module, name, listeners=None, purge_listeners=None, 432 zones=None, purge_zones=None, security_group_ids=None, 433 health_check=None, subnets=None, purge_subnets=None, 434 scheme="internet-facing", connection_draining_timeout=None, 435 idle_timeout=None, 436 cross_az_load_balancing=None, access_logs=None, 437 stickiness=None, wait=None, wait_timeout=None, tags=None, 438 region=None, 439 instance_ids=None, purge_instance_ids=None, **aws_connect_params): 440 441 self.module = module 442 self.name = name 443 self.listeners = listeners 444 self.purge_listeners = purge_listeners 445 self.instance_ids = instance_ids 446 self.purge_instance_ids = purge_instance_ids 447 self.zones = zones 448 self.purge_zones = purge_zones 449 self.security_group_ids = security_group_ids 450 self.health_check = health_check 451 self.subnets = subnets 452 self.purge_subnets = purge_subnets 453 self.scheme = scheme 454 self.connection_draining_timeout = connection_draining_timeout 455 self.idle_timeout = idle_timeout 456 self.cross_az_load_balancing = cross_az_load_balancing 457 self.access_logs = access_logs 458 self.stickiness = stickiness 459 self.wait = wait 460 self.wait_timeout = wait_timeout 461 self.tags = tags 462 463 self.aws_connect_params = aws_connect_params 464 self.region = region 465 466 self.changed = False 467 self.status = 'gone' 468 self.elb_conn = self._get_elb_connection() 469 470 try: 471 self.elb = self._get_elb() 472 except boto.exception.BotoServerError as e: 473 module.fail_json(msg='unable to get all load balancers: %s' % e.message, exception=traceback.format_exc()) 474 475 self.ec2_conn = self._get_ec2_connection() 476 477 @_throttleable_operation(_THROTTLING_RETRIES) 478 def ensure_ok(self): 479 """Create the ELB""" 480 if not self.elb: 481 # Zones and listeners will be added at creation 482 self._create_elb() 483 else: 484 if self._get_scheme(): 485 # the only way to change the scheme is by recreating the resource 486 self.ensure_gone() 487 self._create_elb() 488 else: 489 self._set_zones() 490 self._set_security_groups() 491 self._set_elb_listeners() 492 self._set_subnets() 493 self._set_health_check() 494 # boto has introduced support for some ELB attributes in 495 # different versions, so we check first before trying to 496 # set them to avoid errors 497 if self._check_attribute_support('connection_draining'): 498 self._set_connection_draining_timeout() 499 if self._check_attribute_support('connecting_settings'): 500 self._set_idle_timeout() 501 if self._check_attribute_support('cross_zone_load_balancing'): 502 self._set_cross_az_load_balancing() 503 if self._check_attribute_support('access_log'): 504 self._set_access_log() 505 # add sticky options 506 self.select_stickiness_policy() 507 508 # ensure backend server policies are correct 509 self._set_backend_policies() 510 # set/remove instance ids 511 self._set_instance_ids() 512 513 self._set_tags() 514 515 def ensure_gone(self): 516 """Destroy the ELB""" 517 if self.elb: 518 self._delete_elb() 519 if self.wait: 520 elb_removed = self._wait_for_elb_removed() 521 # Unfortunately even though the ELB itself is removed quickly 522 # the interfaces take longer so reliant security groups cannot 523 # be deleted until the interface has registered as removed. 524 elb_interface_removed = self._wait_for_elb_interface_removed() 525 if not (elb_removed and elb_interface_removed): 526 self.module.fail_json(msg='Timed out waiting for removal of load balancer.') 527 528 def get_info(self): 529 try: 530 check_elb = self.elb_conn.get_all_load_balancers(self.name)[0] 531 except Exception: 532 check_elb = None 533 534 if not check_elb: 535 info = { 536 'name': self.name, 537 'status': self.status, 538 'region': self.region 539 } 540 else: 541 try: 542 lb_cookie_policy = check_elb.policies.lb_cookie_stickiness_policies[0].__dict__['policy_name'] 543 except Exception: 544 lb_cookie_policy = None 545 try: 546 app_cookie_policy = check_elb.policies.app_cookie_stickiness_policies[0].__dict__['policy_name'] 547 except Exception: 548 app_cookie_policy = None 549 550 info = { 551 'name': check_elb.name, 552 'dns_name': check_elb.dns_name, 553 'zones': check_elb.availability_zones, 554 'security_group_ids': check_elb.security_groups, 555 'status': self.status, 556 'subnets': self.subnets, 557 'scheme': check_elb.scheme, 558 'hosted_zone_name': check_elb.canonical_hosted_zone_name, 559 'hosted_zone_id': check_elb.canonical_hosted_zone_name_id, 560 'lb_cookie_policy': lb_cookie_policy, 561 'app_cookie_policy': app_cookie_policy, 562 'proxy_policy': self._get_proxy_protocol_policy(), 563 'backends': self._get_backend_policies(), 564 'instances': [instance.id for instance in check_elb.instances], 565 'out_of_service_count': 0, 566 'in_service_count': 0, 567 'unknown_instance_state_count': 0, 568 'region': self.region 569 } 570 571 # status of instances behind the ELB 572 if info['instances']: 573 info['instance_health'] = [dict( 574 instance_id=instance_state.instance_id, 575 reason_code=instance_state.reason_code, 576 state=instance_state.state 577 ) for instance_state in self.elb_conn.describe_instance_health(self.name)] 578 else: 579 info['instance_health'] = [] 580 581 # instance state counts: InService or OutOfService 582 if info['instance_health']: 583 for instance_state in info['instance_health']: 584 if instance_state['state'] == "InService": 585 info['in_service_count'] += 1 586 elif instance_state['state'] == "OutOfService": 587 info['out_of_service_count'] += 1 588 else: 589 info['unknown_instance_state_count'] += 1 590 591 if check_elb.health_check: 592 info['health_check'] = { 593 'target': check_elb.health_check.target, 594 'interval': check_elb.health_check.interval, 595 'timeout': check_elb.health_check.timeout, 596 'healthy_threshold': check_elb.health_check.healthy_threshold, 597 'unhealthy_threshold': check_elb.health_check.unhealthy_threshold, 598 } 599 600 if check_elb.listeners: 601 info['listeners'] = [self._api_listener_as_tuple(l) 602 for l in check_elb.listeners] 603 elif self.status == 'created': 604 # When creating a new ELB, listeners don't show in the 605 # immediately returned result, so just include the 606 # ones that were added 607 info['listeners'] = [self._listener_as_tuple(l) 608 for l in self.listeners] 609 else: 610 info['listeners'] = [] 611 612 if self._check_attribute_support('connection_draining'): 613 info['connection_draining_timeout'] = int(self.elb_conn.get_lb_attribute(self.name, 'ConnectionDraining').timeout) 614 615 if self._check_attribute_support('connecting_settings'): 616 info['idle_timeout'] = self.elb_conn.get_lb_attribute(self.name, 'ConnectingSettings').idle_timeout 617 618 if self._check_attribute_support('cross_zone_load_balancing'): 619 is_cross_az_lb_enabled = self.elb_conn.get_lb_attribute(self.name, 'CrossZoneLoadBalancing') 620 if is_cross_az_lb_enabled: 621 info['cross_az_load_balancing'] = 'yes' 622 else: 623 info['cross_az_load_balancing'] = 'no' 624 625 # return stickiness info? 626 627 info['tags'] = self.tags 628 629 return info 630 631 @_throttleable_operation(_THROTTLING_RETRIES) 632 def _wait_for_elb_removed(self): 633 polling_increment_secs = 15 634 max_retries = (self.wait_timeout // polling_increment_secs) 635 status_achieved = False 636 637 for x in range(0, max_retries): 638 try: 639 self.elb_conn.get_all_lb_attributes(self.name) 640 except (boto.exception.BotoServerError, Exception) as e: 641 if "LoadBalancerNotFound" in e.code: 642 status_achieved = True 643 break 644 else: 645 time.sleep(polling_increment_secs) 646 647 return status_achieved 648 649 @_throttleable_operation(_THROTTLING_RETRIES) 650 def _wait_for_elb_interface_removed(self): 651 polling_increment_secs = 15 652 max_retries = (self.wait_timeout // polling_increment_secs) 653 status_achieved = False 654 655 elb_interfaces = self.ec2_conn.get_all_network_interfaces( 656 filters={'attachment.instance-owner-id': 'amazon-elb', 657 'description': 'ELB {0}'.format(self.name)}) 658 659 for x in range(0, max_retries): 660 for interface in elb_interfaces: 661 try: 662 result = self.ec2_conn.get_all_network_interfaces(interface.id) 663 if result == []: 664 status_achieved = True 665 break 666 else: 667 time.sleep(polling_increment_secs) 668 except (boto.exception.BotoServerError, Exception) as e: 669 if 'InvalidNetworkInterfaceID' in e.code: 670 status_achieved = True 671 break 672 else: 673 self.module.fail_json(msg=to_native(e), exception=traceback.format_exc()) 674 675 return status_achieved 676 677 @_throttleable_operation(_THROTTLING_RETRIES) 678 def _get_elb(self): 679 elbs = self.elb_conn.get_all_load_balancers() 680 for elb in elbs: 681 if self.name == elb.name: 682 self.status = 'ok' 683 return elb 684 685 def _get_elb_connection(self): 686 try: 687 return connect_to_aws(boto.ec2.elb, self.region, 688 **self.aws_connect_params) 689 except (boto.exception.NoAuthHandlerFound, AnsibleAWSError) as e: 690 self.module.fail_json(msg=str(e)) 691 692 def _get_ec2_connection(self): 693 try: 694 return connect_to_aws(boto.ec2, self.region, 695 **self.aws_connect_params) 696 except (boto.exception.NoAuthHandlerFound, Exception) as e: 697 self.module.fail_json(msg=to_native(e), exception=traceback.format_exc()) 698 699 @_throttleable_operation(_THROTTLING_RETRIES) 700 def _delete_elb(self): 701 # True if succeeds, exception raised if not 702 result = self.elb_conn.delete_load_balancer(name=self.name) 703 if result: 704 self.changed = True 705 self.status = 'deleted' 706 707 def _create_elb(self): 708 listeners = [self._listener_as_tuple(l) for l in self.listeners] 709 self.elb = self.elb_conn.create_load_balancer(name=self.name, 710 zones=self.zones, 711 security_groups=self.security_group_ids, 712 complex_listeners=listeners, 713 subnets=self.subnets, 714 scheme=self.scheme) 715 if self.elb: 716 # HACK: Work around a boto bug in which the listeners attribute is 717 # always set to the listeners argument to create_load_balancer, and 718 # not the complex_listeners 719 # We're not doing a self.elb = self._get_elb here because there 720 # might be eventual consistency issues and it doesn't necessarily 721 # make sense to wait until the ELB gets returned from the EC2 API. 722 # This is necessary in the event we hit the throttling errors and 723 # need to retry ensure_ok 724 # See https://github.com/boto/boto/issues/3526 725 self.elb.listeners = self.listeners 726 self.changed = True 727 self.status = 'created' 728 729 def _create_elb_listeners(self, listeners): 730 """Takes a list of listener tuples and creates them""" 731 # True if succeeds, exception raised if not 732 self.changed = self.elb_conn.create_load_balancer_listeners(self.name, 733 complex_listeners=listeners) 734 735 def _delete_elb_listeners(self, listeners): 736 """Takes a list of listener tuples and deletes them from the elb""" 737 ports = [l[0] for l in listeners] 738 739 # True if succeeds, exception raised if not 740 self.changed = self.elb_conn.delete_load_balancer_listeners(self.name, 741 ports) 742 743 def _set_elb_listeners(self): 744 """ 745 Creates listeners specified by self.listeners; overwrites existing 746 listeners on these ports; removes extraneous listeners 747 """ 748 listeners_to_add = [] 749 listeners_to_remove = [] 750 listeners_to_keep = [] 751 752 # Check for any listeners we need to create or overwrite 753 for listener in self.listeners: 754 listener_as_tuple = self._listener_as_tuple(listener) 755 756 # First we loop through existing listeners to see if one is 757 # already specified for this port 758 existing_listener_found = None 759 for existing_listener in self.elb.listeners: 760 # Since ELB allows only one listener on each incoming port, a 761 # single match on the incoming port is all we're looking for 762 if existing_listener[0] == int(listener['load_balancer_port']): 763 existing_listener_found = self._api_listener_as_tuple(existing_listener) 764 break 765 766 if existing_listener_found: 767 # Does it match exactly? 768 if listener_as_tuple != existing_listener_found: 769 # The ports are the same but something else is different, 770 # so we'll remove the existing one and add the new one 771 listeners_to_remove.append(existing_listener_found) 772 listeners_to_add.append(listener_as_tuple) 773 else: 774 # We already have this listener, so we're going to keep it 775 listeners_to_keep.append(existing_listener_found) 776 else: 777 # We didn't find an existing listener, so just add the new one 778 listeners_to_add.append(listener_as_tuple) 779 780 # Check for any extraneous listeners we need to remove, if desired 781 if self.purge_listeners: 782 for existing_listener in self.elb.listeners: 783 existing_listener_tuple = self._api_listener_as_tuple(existing_listener) 784 if existing_listener_tuple in listeners_to_remove: 785 # Already queued for removal 786 continue 787 if existing_listener_tuple in listeners_to_keep: 788 # Keep this one around 789 continue 790 # Since we're not already removing it and we don't need to keep 791 # it, let's get rid of it 792 listeners_to_remove.append(existing_listener_tuple) 793 794 if listeners_to_remove: 795 self._delete_elb_listeners(listeners_to_remove) 796 797 if listeners_to_add: 798 self._create_elb_listeners(listeners_to_add) 799 800 def _api_listener_as_tuple(self, listener): 801 """Adds ssl_certificate_id to ELB API tuple if present""" 802 base_tuple = listener.get_complex_tuple() 803 if listener.ssl_certificate_id and len(base_tuple) < 5: 804 return base_tuple + (listener.ssl_certificate_id,) 805 return base_tuple 806 807 def _listener_as_tuple(self, listener): 808 """Formats listener as a 4- or 5-tuples, in the order specified by the 809 ELB API""" 810 # N.B. string manipulations on protocols below (str(), upper()) is to 811 # ensure format matches output from ELB API 812 listener_list = [ 813 int(listener['load_balancer_port']), 814 int(listener['instance_port']), 815 str(listener['protocol'].upper()), 816 ] 817 818 # Instance protocol is not required by ELB API; it defaults to match 819 # load balancer protocol. We'll mimic that behavior here 820 if 'instance_protocol' in listener: 821 listener_list.append(str(listener['instance_protocol'].upper())) 822 else: 823 listener_list.append(str(listener['protocol'].upper())) 824 825 if 'ssl_certificate_id' in listener: 826 listener_list.append(str(listener['ssl_certificate_id'])) 827 828 return tuple(listener_list) 829 830 def _enable_zones(self, zones): 831 try: 832 self.elb.enable_zones(zones) 833 except boto.exception.BotoServerError as e: 834 self.module.fail_json(msg='unable to enable zones: %s' % e.message, exception=traceback.format_exc()) 835 836 self.changed = True 837 838 def _disable_zones(self, zones): 839 try: 840 self.elb.disable_zones(zones) 841 except boto.exception.BotoServerError as e: 842 self.module.fail_json(msg='unable to disable zones: %s' % e.message, exception=traceback.format_exc()) 843 self.changed = True 844 845 def _attach_subnets(self, subnets): 846 self.elb_conn.attach_lb_to_subnets(self.name, subnets) 847 self.changed = True 848 849 def _detach_subnets(self, subnets): 850 self.elb_conn.detach_lb_from_subnets(self.name, subnets) 851 self.changed = True 852 853 def _set_subnets(self): 854 """Determine which subnets need to be attached or detached on the ELB""" 855 if self.subnets: 856 if self.purge_subnets: 857 subnets_to_detach = list(set(self.elb.subnets) - set(self.subnets)) 858 subnets_to_attach = list(set(self.subnets) - set(self.elb.subnets)) 859 else: 860 subnets_to_detach = None 861 subnets_to_attach = list(set(self.subnets) - set(self.elb.subnets)) 862 863 if subnets_to_attach: 864 self._attach_subnets(subnets_to_attach) 865 if subnets_to_detach: 866 self._detach_subnets(subnets_to_detach) 867 868 def _get_scheme(self): 869 """Determine if the current scheme is different than the scheme of the ELB""" 870 if self.scheme: 871 if self.elb.scheme != self.scheme: 872 if not self.wait: 873 self.module.fail_json(msg="Unable to modify scheme without using the wait option") 874 return True 875 return False 876 877 def _set_zones(self): 878 """Determine which zones need to be enabled or disabled on the ELB""" 879 if self.zones: 880 if self.purge_zones: 881 zones_to_disable = list(set(self.elb.availability_zones) - 882 set(self.zones)) 883 zones_to_enable = list(set(self.zones) - 884 set(self.elb.availability_zones)) 885 else: 886 zones_to_disable = None 887 zones_to_enable = list(set(self.zones) - 888 set(self.elb.availability_zones)) 889 if zones_to_enable: 890 self._enable_zones(zones_to_enable) 891 # N.B. This must come second, in case it would have removed all zones 892 if zones_to_disable: 893 self._disable_zones(zones_to_disable) 894 895 def _set_security_groups(self): 896 if self.security_group_ids is not None and set(self.elb.security_groups) != set(self.security_group_ids): 897 self.elb_conn.apply_security_groups_to_lb(self.name, self.security_group_ids) 898 self.changed = True 899 900 def _set_health_check(self): 901 """Set health check values on ELB as needed""" 902 if self.health_check: 903 # This just makes it easier to compare each of the attributes 904 # and look for changes. Keys are attributes of the current 905 # health_check; values are desired values of new health_check 906 health_check_config = { 907 "target": self._get_health_check_target(), 908 "timeout": self.health_check['response_timeout'], 909 "interval": self.health_check['interval'], 910 "unhealthy_threshold": self.health_check['unhealthy_threshold'], 911 "healthy_threshold": self.health_check['healthy_threshold'], 912 } 913 914 update_health_check = False 915 916 # The health_check attribute is *not* set on newly created 917 # ELBs! So we have to create our own. 918 if not self.elb.health_check: 919 self.elb.health_check = HealthCheck() 920 921 for attr, desired_value in health_check_config.items(): 922 if getattr(self.elb.health_check, attr) != desired_value: 923 setattr(self.elb.health_check, attr, desired_value) 924 update_health_check = True 925 926 if update_health_check: 927 self.elb.configure_health_check(self.elb.health_check) 928 self.changed = True 929 930 def _check_attribute_support(self, attr): 931 return hasattr(boto.ec2.elb.attributes.LbAttributes(), attr) 932 933 def _set_cross_az_load_balancing(self): 934 attributes = self.elb.get_attributes() 935 if self.cross_az_load_balancing: 936 if not attributes.cross_zone_load_balancing.enabled: 937 self.changed = True 938 attributes.cross_zone_load_balancing.enabled = True 939 else: 940 if attributes.cross_zone_load_balancing.enabled: 941 self.changed = True 942 attributes.cross_zone_load_balancing.enabled = False 943 self.elb_conn.modify_lb_attribute(self.name, 'CrossZoneLoadBalancing', 944 attributes.cross_zone_load_balancing.enabled) 945 946 def _set_access_log(self): 947 attributes = self.elb.get_attributes() 948 if self.access_logs: 949 if 's3_location' not in self.access_logs: 950 self.module.fail_json(msg='s3_location information required') 951 952 access_logs_config = { 953 "enabled": True, 954 "s3_bucket_name": self.access_logs['s3_location'], 955 "s3_bucket_prefix": self.access_logs.get('s3_prefix', ''), 956 "emit_interval": self.access_logs.get('interval', 60), 957 } 958 959 update_access_logs_config = False 960 for attr, desired_value in access_logs_config.items(): 961 if getattr(attributes.access_log, attr) != desired_value: 962 setattr(attributes.access_log, attr, desired_value) 963 update_access_logs_config = True 964 if update_access_logs_config: 965 self.elb_conn.modify_lb_attribute(self.name, 'AccessLog', attributes.access_log) 966 self.changed = True 967 elif attributes.access_log.enabled: 968 attributes.access_log.enabled = False 969 self.changed = True 970 self.elb_conn.modify_lb_attribute(self.name, 'AccessLog', attributes.access_log) 971 972 def _set_connection_draining_timeout(self): 973 attributes = self.elb.get_attributes() 974 if self.connection_draining_timeout is not None: 975 if not attributes.connection_draining.enabled or \ 976 attributes.connection_draining.timeout != self.connection_draining_timeout: 977 self.changed = True 978 attributes.connection_draining.enabled = True 979 attributes.connection_draining.timeout = self.connection_draining_timeout 980 self.elb_conn.modify_lb_attribute(self.name, 'ConnectionDraining', attributes.connection_draining) 981 else: 982 if attributes.connection_draining.enabled: 983 self.changed = True 984 attributes.connection_draining.enabled = False 985 self.elb_conn.modify_lb_attribute(self.name, 'ConnectionDraining', attributes.connection_draining) 986 987 def _set_idle_timeout(self): 988 attributes = self.elb.get_attributes() 989 if self.idle_timeout is not None: 990 if attributes.connecting_settings.idle_timeout != self.idle_timeout: 991 self.changed = True 992 attributes.connecting_settings.idle_timeout = self.idle_timeout 993 self.elb_conn.modify_lb_attribute(self.name, 'ConnectingSettings', attributes.connecting_settings) 994 995 def _policy_name(self, policy_type): 996 return 'ec2-elb-lb-{0}'.format(to_native(policy_type, errors='surrogate_or_strict')) 997 998 def _create_policy(self, policy_param, policy_meth, policy): 999 getattr(self.elb_conn, policy_meth)(policy_param, self.elb.name, policy) 1000 1001 def _delete_policy(self, elb_name, policy): 1002 self.elb_conn.delete_lb_policy(elb_name, policy) 1003 1004 def _update_policy(self, policy_param, policy_meth, policy_attr, policy): 1005 self._delete_policy(self.elb.name, policy) 1006 self._create_policy(policy_param, policy_meth, policy) 1007 1008 def _set_listener_policy(self, listeners_dict, policy=None): 1009 policy = [] if policy is None else policy 1010 1011 for listener_port in listeners_dict: 1012 if listeners_dict[listener_port].startswith('HTTP'): 1013 self.elb_conn.set_lb_policies_of_listener(self.elb.name, listener_port, policy) 1014 1015 def _set_stickiness_policy(self, elb_info, listeners_dict, policy, **policy_attrs): 1016 for p in getattr(elb_info.policies, policy_attrs['attr']): 1017 if str(p.__dict__['policy_name']) == str(policy[0]): 1018 if str(p.__dict__[policy_attrs['dict_key']]) != str(policy_attrs['param_value'] or 0): 1019 self._set_listener_policy(listeners_dict) 1020 self._update_policy(policy_attrs['param_value'], policy_attrs['method'], policy_attrs['attr'], policy[0]) 1021 self.changed = True 1022 break 1023 else: 1024 self._create_policy(policy_attrs['param_value'], policy_attrs['method'], policy[0]) 1025 self.changed = True 1026 1027 self._set_listener_policy(listeners_dict, policy) 1028 1029 def select_stickiness_policy(self): 1030 if self.stickiness: 1031 1032 if 'cookie' in self.stickiness and 'expiration' in self.stickiness: 1033 self.module.fail_json(msg='\'cookie\' and \'expiration\' can not be set at the same time') 1034 1035 elb_info = self.elb_conn.get_all_load_balancers(self.elb.name)[0] 1036 d = {} 1037 for listener in elb_info.listeners: 1038 d[listener[0]] = listener[2] 1039 listeners_dict = d 1040 1041 if self.stickiness['type'] == 'loadbalancer': 1042 policy = [] 1043 policy_type = 'LBCookieStickinessPolicyType' 1044 1045 if self.module.boolean(self.stickiness['enabled']): 1046 1047 if 'expiration' not in self.stickiness: 1048 self.module.fail_json(msg='expiration must be set when type is loadbalancer') 1049 1050 try: 1051 expiration = self.stickiness['expiration'] if int(self.stickiness['expiration']) else None 1052 except ValueError: 1053 self.module.fail_json(msg='expiration must be set to an integer') 1054 1055 policy_attrs = { 1056 'type': policy_type, 1057 'attr': 'lb_cookie_stickiness_policies', 1058 'method': 'create_lb_cookie_stickiness_policy', 1059 'dict_key': 'cookie_expiration_period', 1060 'param_value': expiration 1061 } 1062 policy.append(self._policy_name(policy_attrs['type'])) 1063 1064 self._set_stickiness_policy(elb_info, listeners_dict, policy, **policy_attrs) 1065 elif not self.module.boolean(self.stickiness['enabled']): 1066 if len(elb_info.policies.lb_cookie_stickiness_policies): 1067 if elb_info.policies.lb_cookie_stickiness_policies[0].policy_name == self._policy_name(policy_type): 1068 self.changed = True 1069 else: 1070 self.changed = False 1071 self._set_listener_policy(listeners_dict) 1072 self._delete_policy(self.elb.name, self._policy_name(policy_type)) 1073 1074 elif self.stickiness['type'] == 'application': 1075 policy = [] 1076 policy_type = 'AppCookieStickinessPolicyType' 1077 if self.module.boolean(self.stickiness['enabled']): 1078 1079 if 'cookie' not in self.stickiness: 1080 self.module.fail_json(msg='cookie must be set when type is application') 1081 1082 policy_attrs = { 1083 'type': policy_type, 1084 'attr': 'app_cookie_stickiness_policies', 1085 'method': 'create_app_cookie_stickiness_policy', 1086 'dict_key': 'cookie_name', 1087 'param_value': self.stickiness['cookie'] 1088 } 1089 policy.append(self._policy_name(policy_attrs['type'])) 1090 self._set_stickiness_policy(elb_info, listeners_dict, policy, **policy_attrs) 1091 elif not self.module.boolean(self.stickiness['enabled']): 1092 if len(elb_info.policies.app_cookie_stickiness_policies): 1093 if elb_info.policies.app_cookie_stickiness_policies[0].policy_name == self._policy_name(policy_type): 1094 self.changed = True 1095 self._set_listener_policy(listeners_dict) 1096 self._delete_policy(self.elb.name, self._policy_name(policy_type)) 1097 1098 else: 1099 self._set_listener_policy(listeners_dict) 1100 1101 def _get_backend_policies(self): 1102 """Get a list of backend policies""" 1103 policies = [] 1104 if self.elb.backends is not None: 1105 for backend in self.elb.backends: 1106 if backend.policies is not None: 1107 for policy in backend.policies: 1108 policies.append(str(backend.instance_port) + ':' + policy.policy_name) 1109 1110 return policies 1111 1112 def _set_backend_policies(self): 1113 """Sets policies for all backends""" 1114 ensure_proxy_protocol = False 1115 replace = [] 1116 backend_policies = self._get_backend_policies() 1117 1118 # Find out what needs to be changed 1119 for listener in self.listeners: 1120 want = False 1121 1122 if 'proxy_protocol' in listener and listener['proxy_protocol']: 1123 ensure_proxy_protocol = True 1124 want = True 1125 1126 if str(listener['instance_port']) + ':ProxyProtocol-policy' in backend_policies: 1127 if not want: 1128 replace.append({'port': listener['instance_port'], 'policies': []}) 1129 elif want: 1130 replace.append({'port': listener['instance_port'], 'policies': ['ProxyProtocol-policy']}) 1131 1132 # enable or disable proxy protocol 1133 if ensure_proxy_protocol: 1134 self._set_proxy_protocol_policy() 1135 1136 # Make the backend policies so 1137 for item in replace: 1138 self.elb_conn.set_lb_policies_of_backend_server(self.elb.name, item['port'], item['policies']) 1139 self.changed = True 1140 1141 def _get_proxy_protocol_policy(self): 1142 """Find out if the elb has a proxy protocol enabled""" 1143 if self.elb.policies is not None and self.elb.policies.other_policies is not None: 1144 for policy in self.elb.policies.other_policies: 1145 if policy.policy_name == 'ProxyProtocol-policy': 1146 return policy.policy_name 1147 1148 return None 1149 1150 def _set_proxy_protocol_policy(self): 1151 """Install a proxy protocol policy if needed""" 1152 proxy_policy = self._get_proxy_protocol_policy() 1153 1154 if proxy_policy is None: 1155 self.elb_conn.create_lb_policy( 1156 self.elb.name, 'ProxyProtocol-policy', 'ProxyProtocolPolicyType', {'ProxyProtocol': True} 1157 ) 1158 self.changed = True 1159 1160 # TODO: remove proxy protocol policy if not needed anymore? There is no side effect to leaving it there 1161 1162 def _diff_list(self, a, b): 1163 """Find the entries in list a that are not in list b""" 1164 b = set(b) 1165 return [aa for aa in a if aa not in b] 1166 1167 def _get_instance_ids(self): 1168 """Get the current list of instance ids installed in the elb""" 1169 instances = [] 1170 if self.elb.instances is not None: 1171 for instance in self.elb.instances: 1172 instances.append(instance.id) 1173 1174 return instances 1175 1176 def _set_instance_ids(self): 1177 """Register or deregister instances from an lb instance""" 1178 assert_instances = self.instance_ids or [] 1179 1180 has_instances = self._get_instance_ids() 1181 1182 add_instances = self._diff_list(assert_instances, has_instances) 1183 if add_instances: 1184 self.elb_conn.register_instances(self.elb.name, add_instances) 1185 self.changed = True 1186 1187 if self.purge_instance_ids: 1188 remove_instances = self._diff_list(has_instances, assert_instances) 1189 if remove_instances: 1190 self.elb_conn.deregister_instances(self.elb.name, remove_instances) 1191 self.changed = True 1192 1193 def _set_tags(self): 1194 """Add/Delete tags""" 1195 if self.tags is None: 1196 return 1197 1198 params = {'LoadBalancerNames.member.1': self.name} 1199 1200 tagdict = dict() 1201 1202 # get the current list of tags from the ELB, if ELB exists 1203 if self.elb: 1204 current_tags = self.elb_conn.get_list('DescribeTags', params, 1205 [('member', Tag)]) 1206 tagdict = dict((tag.Key, tag.Value) for tag in current_tags 1207 if hasattr(tag, 'Key')) 1208 1209 # Add missing tags 1210 dictact = dict(set(self.tags.items()) - set(tagdict.items())) 1211 if dictact: 1212 for i, key in enumerate(dictact): 1213 params['Tags.member.%d.Key' % (i + 1)] = key 1214 params['Tags.member.%d.Value' % (i + 1)] = dictact[key] 1215 1216 self.elb_conn.make_request('AddTags', params) 1217 self.changed = True 1218 1219 # Remove extra tags 1220 dictact = dict(set(tagdict.items()) - set(self.tags.items())) 1221 if dictact: 1222 for i, key in enumerate(dictact): 1223 params['Tags.member.%d.Key' % (i + 1)] = key 1224 1225 self.elb_conn.make_request('RemoveTags', params) 1226 self.changed = True 1227 1228 def _get_health_check_target(self): 1229 """Compose target string from healthcheck parameters""" 1230 protocol = self.health_check['ping_protocol'].upper() 1231 path = "" 1232 1233 if protocol in ['HTTP', 'HTTPS'] and 'ping_path' in self.health_check: 1234 path = self.health_check['ping_path'] 1235 1236 return "%s:%s%s" % (protocol, self.health_check['ping_port'], path) 1237 1238 1239def main(): 1240 argument_spec = ec2_argument_spec() 1241 argument_spec.update(dict( 1242 state={'required': True, 'choices': ['present', 'absent']}, 1243 name={'required': True}, 1244 listeners={'default': None, 'required': False, 'type': 'list'}, 1245 purge_listeners={'default': True, 'required': False, 'type': 'bool'}, 1246 instance_ids={'default': None, 'required': False, 'type': 'list'}, 1247 purge_instance_ids={'default': False, 'required': False, 'type': 'bool'}, 1248 zones={'default': None, 'required': False, 'type': 'list'}, 1249 purge_zones={'default': False, 'required': False, 'type': 'bool'}, 1250 security_group_ids={'default': None, 'required': False, 'type': 'list'}, 1251 security_group_names={'default': None, 'required': False, 'type': 'list'}, 1252 health_check={'default': None, 'required': False, 'type': 'dict'}, 1253 subnets={'default': None, 'required': False, 'type': 'list'}, 1254 purge_subnets={'default': False, 'required': False, 'type': 'bool'}, 1255 scheme={'default': 'internet-facing', 'required': False, 'choices': ['internal', 'internet-facing']}, 1256 connection_draining_timeout={'default': None, 'required': False, 'type': 'int'}, 1257 idle_timeout={'default': None, 'type': 'int', 'required': False}, 1258 cross_az_load_balancing={'default': None, 'type': 'bool', 'required': False}, 1259 stickiness={'default': None, 'required': False, 'type': 'dict'}, 1260 access_logs={'default': None, 'required': False, 'type': 'dict'}, 1261 wait={'default': False, 'type': 'bool', 'required': False}, 1262 wait_timeout={'default': 60, 'type': 'int', 'required': False}, 1263 tags={'default': None, 'required': False, 'type': 'dict'} 1264 ) 1265 ) 1266 1267 module = AnsibleModule( 1268 argument_spec=argument_spec, 1269 mutually_exclusive=[['security_group_ids', 'security_group_names']] 1270 ) 1271 1272 if not HAS_BOTO: 1273 module.fail_json(msg='boto required for this module') 1274 1275 region, ec2_url, aws_connect_params = get_aws_connection_info(module) 1276 if not region: 1277 module.fail_json(msg="Region must be specified as a parameter, in EC2_REGION or AWS_REGION environment variables or in boto configuration file") 1278 1279 name = module.params['name'] 1280 state = module.params['state'] 1281 listeners = module.params['listeners'] 1282 purge_listeners = module.params['purge_listeners'] 1283 instance_ids = module.params['instance_ids'] 1284 purge_instance_ids = module.params['purge_instance_ids'] 1285 zones = module.params['zones'] 1286 purge_zones = module.params['purge_zones'] 1287 security_group_ids = module.params['security_group_ids'] 1288 security_group_names = module.params['security_group_names'] 1289 health_check = module.params['health_check'] 1290 access_logs = module.params['access_logs'] 1291 subnets = module.params['subnets'] 1292 purge_subnets = module.params['purge_subnets'] 1293 scheme = module.params['scheme'] 1294 connection_draining_timeout = module.params['connection_draining_timeout'] 1295 idle_timeout = module.params['idle_timeout'] 1296 cross_az_load_balancing = module.params['cross_az_load_balancing'] 1297 stickiness = module.params['stickiness'] 1298 wait = module.params['wait'] 1299 wait_timeout = module.params['wait_timeout'] 1300 tags = module.params['tags'] 1301 1302 if state == 'present' and not listeners: 1303 module.fail_json(msg="At least one listener is required for ELB creation") 1304 1305 if state == 'present' and not (zones or subnets): 1306 module.fail_json(msg="At least one availability zone or subnet is required for ELB creation") 1307 1308 if wait_timeout > 600: 1309 module.fail_json(msg='wait_timeout maximum is 600 seconds') 1310 1311 if security_group_names: 1312 security_group_ids = [] 1313 try: 1314 ec2 = connect_to_aws(boto.ec2, region, **aws_connect_params) 1315 if subnets: # We have at least one subnet, ergo this is a VPC 1316 vpc_conn = _get_vpc_connection(module=module, region=region, aws_connect_params=aws_connect_params) 1317 vpc_id = vpc_conn.get_all_subnets([subnets[0]])[0].vpc_id 1318 filters = {'vpc_id': vpc_id} 1319 else: 1320 filters = None 1321 grp_details = ec2.get_all_security_groups(filters=filters) 1322 1323 for group_name in security_group_names: 1324 if isinstance(group_name, string_types): 1325 group_name = [group_name] 1326 1327 group_id = [str(grp.id) for grp in grp_details if str(grp.name) in group_name] 1328 security_group_ids.extend(group_id) 1329 except boto.exception.NoAuthHandlerFound as e: 1330 module.fail_json(msg=str(e)) 1331 1332 elb_man = ElbManager(module, name, listeners, purge_listeners, zones, 1333 purge_zones, security_group_ids, health_check, 1334 subnets, purge_subnets, scheme, 1335 connection_draining_timeout, idle_timeout, 1336 cross_az_load_balancing, 1337 access_logs, stickiness, wait, wait_timeout, tags, 1338 region=region, instance_ids=instance_ids, purge_instance_ids=purge_instance_ids, 1339 **aws_connect_params) 1340 1341 # check for unsupported attributes for this version of boto 1342 if cross_az_load_balancing and not elb_man._check_attribute_support('cross_zone_load_balancing'): 1343 module.fail_json(msg="You must install boto >= 2.18.0 to use the cross_az_load_balancing attribute") 1344 1345 if connection_draining_timeout and not elb_man._check_attribute_support('connection_draining'): 1346 module.fail_json(msg="You must install boto >= 2.28.0 to use the connection_draining_timeout attribute") 1347 1348 if idle_timeout and not elb_man._check_attribute_support('connecting_settings'): 1349 module.fail_json(msg="You must install boto >= 2.33.0 to use the idle_timeout attribute") 1350 1351 if state == 'present': 1352 elb_man.ensure_ok() 1353 elif state == 'absent': 1354 elb_man.ensure_gone() 1355 1356 ansible_facts = {'ec2_elb': 'info'} 1357 ec2_facts_result = dict(changed=elb_man.changed, 1358 elb=elb_man.get_info(), 1359 ansible_facts=ansible_facts) 1360 1361 module.exit_json(**ec2_facts_result) 1362 1363 1364if __name__ == '__main__': 1365 main() 1366