1# Copyright (c) 2006-2012 Mitch Garnaat http://garnaat.org/
2# Copyright (c) 2012 Amazon.com, Inc. or its affiliates.  All Rights Reserved
3#
4# Permission is hereby granted, free of charge, to any person obtaining a
5# copy of this software and associated documentation files (the
6# "Software"), to deal in the Software without restriction, including
7# without limitation the rights to use, copy, modify, merge, publish, dis-
8# tribute, sublicense, and/or sell copies of the Software, and to permit
9# persons to whom the Software is furnished to do so, subject to the fol-
10# lowing conditions:
11#
12# The above copyright notice and this permission notice shall be included
13# in all copies or substantial portions of the Software.
14#
15# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
16# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
17# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
18# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
19# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
21# IN THE SOFTWARE.
22
23from boto.ec2.elb.healthcheck import HealthCheck
24from boto.ec2.elb.listener import Listener
25from boto.ec2.elb.listelement import ListElement
26from boto.ec2.elb.policies import Policies, OtherPolicy
27from boto.ec2.elb.securitygroup import SecurityGroup
28from boto.ec2.instanceinfo import InstanceInfo
29from boto.resultset import ResultSet
30from boto.compat import six
31
32
33class Backend(object):
34    """Backend server description"""
35
36    def __init__(self, connection=None):
37        self.connection = connection
38        self.instance_port = None
39        self.policies = None
40
41    def __repr__(self):
42        return 'Backend(%r:%r)' % (self.instance_port, self.policies)
43
44    def startElement(self, name, attrs, connection):
45        if name == 'PolicyNames':
46            self.policies = ResultSet([('member', OtherPolicy)])
47            return self.policies
48
49    def endElement(self, name, value, connection):
50        if name == 'InstancePort':
51            self.instance_port = int(value)
52        return
53
54
55class LoadBalancerZones(object):
56    """
57    Used to collect the zones for a Load Balancer when enable_zones
58    or disable_zones are called.
59    """
60    def __init__(self, connection=None):
61        self.connection = connection
62        self.zones = ListElement()
63
64    def startElement(self, name, attrs, connection):
65        if name == 'AvailabilityZones':
66            return self.zones
67
68    def endElement(self, name, value, connection):
69        pass
70
71
72class LoadBalancer(object):
73    """
74    Represents an EC2 Load Balancer.
75    """
76
77    def __init__(self, connection=None, name=None, endpoints=None):
78        """
79        :ivar boto.ec2.elb.ELBConnection connection: The connection this load
80            balancer was instance was instantiated from.
81        :ivar list listeners: A list of tuples in the form of
82            ``(<Inbound port>, <Outbound port>, <Protocol>)``
83        :ivar boto.ec2.elb.healthcheck.HealthCheck health_check: The health
84            check policy for this load balancer.
85        :ivar boto.ec2.elb.policies.Policies policies: Cookie stickiness and
86            other policies.
87        :ivar str name: The name of the Load Balancer.
88        :ivar str dns_name: The external DNS name for the balancer.
89        :ivar str created_time: A date+time string showing when the
90            load balancer was created.
91        :ivar list instances: A list of :py:class:`boto.ec2.instanceinfo.InstanceInfo`
92            instances, representing the EC2 instances this load balancer is
93            distributing requests to.
94        :ivar list availability_zones: The availability zones this balancer
95            covers.
96        :ivar str canonical_hosted_zone_name: Current CNAME for the balancer.
97        :ivar str canonical_hosted_zone_name_id: The Route 53 hosted zone
98            ID of this balancer. Needed when creating an Alias record in a
99            Route 53 hosted zone.
100        :ivar boto.ec2.elb.securitygroup.SecurityGroup source_security_group:
101            The security group that you can use as part of your inbound rules
102            for your load balancer back-end instances to disallow traffic
103            from sources other than your load balancer.
104        :ivar list subnets: A list of subnets this balancer is on.
105        :ivar list security_groups: A list of additional security groups that
106            have been applied.
107        :ivar str vpc_id: The ID of the VPC that this ELB resides within.
108        :ivar list backends: A list of :py:class:`boto.ec2.elb.loadbalancer.Backend
109            back-end server descriptions.
110        """
111        self.connection = connection
112        self.name = name
113        self.listeners = None
114        self.health_check = None
115        self.policies = None
116        self.dns_name = None
117        self.created_time = None
118        self.instances = None
119        self.availability_zones = ListElement()
120        self.canonical_hosted_zone_name = None
121        self.canonical_hosted_zone_name_id = None
122        self.source_security_group = None
123        self.subnets = ListElement()
124        self.security_groups = ListElement()
125        self.vpc_id = None
126        self.scheme = None
127        self.backends = None
128        self._attributes = None
129
130    def __repr__(self):
131        return 'LoadBalancer:%s' % self.name
132
133    def startElement(self, name, attrs, connection):
134        if name == 'HealthCheck':
135            self.health_check = HealthCheck(self)
136            return self.health_check
137        elif name == 'ListenerDescriptions':
138            self.listeners = ResultSet([('member', Listener)])
139            return self.listeners
140        elif name == 'AvailabilityZones':
141            return self.availability_zones
142        elif name == 'Instances':
143            self.instances = ResultSet([('member', InstanceInfo)])
144            return self.instances
145        elif name == 'Policies':
146            self.policies = Policies(self)
147            return self.policies
148        elif name == 'SourceSecurityGroup':
149            self.source_security_group = SecurityGroup()
150            return self.source_security_group
151        elif name == 'Subnets':
152            return self.subnets
153        elif name == 'SecurityGroups':
154            return self.security_groups
155        elif name == 'VPCId':
156            pass
157        elif name == "BackendServerDescriptions":
158            self.backends = ResultSet([('member', Backend)])
159            return self.backends
160        else:
161            return None
162
163    def endElement(self, name, value, connection):
164        if name == 'LoadBalancerName':
165            self.name = value
166        elif name == 'DNSName':
167            self.dns_name = value
168        elif name == 'CreatedTime':
169            self.created_time = value
170        elif name == 'InstanceId':
171            self.instances.append(value)
172        elif name == 'CanonicalHostedZoneName':
173            self.canonical_hosted_zone_name = value
174        elif name == 'CanonicalHostedZoneNameID':
175            self.canonical_hosted_zone_name_id = value
176        elif name == 'VPCId':
177            self.vpc_id = value
178        elif name == 'Scheme':
179            self.scheme = value
180        else:
181            setattr(self, name, value)
182
183    def enable_zones(self, zones):
184        """
185        Enable availability zones to this Access Point.
186        All zones must be in the same region as the Access Point.
187
188        :type zones: string or List of strings
189        :param zones: The name of the zone(s) to add.
190
191        """
192        if isinstance(zones, six.string_types):
193            zones = [zones]
194        new_zones = self.connection.enable_availability_zones(self.name, zones)
195        self.availability_zones = new_zones
196
197    def disable_zones(self, zones):
198        """
199        Disable availability zones from this Access Point.
200
201        :type zones: string or List of strings
202        :param zones: The name of the zone(s) to add.
203
204        """
205        if isinstance(zones, six.string_types):
206            zones = [zones]
207        new_zones = self.connection.disable_availability_zones(
208            self.name, zones)
209        self.availability_zones = new_zones
210
211    def get_attributes(self, force=False):
212        """
213        Gets the LbAttributes.  The Attributes will be cached.
214
215        :type force: bool
216        :param force: Ignore cache value and reload.
217
218        :rtype: boto.ec2.elb.attributes.LbAttributes
219        :return: The LbAttribues object
220        """
221        if not self._attributes or force:
222            self._attributes = self.connection.get_all_lb_attributes(self.name)
223        return self._attributes
224
225    def is_cross_zone_load_balancing(self, force=False):
226        """
227        Identifies if the ELB is current configured to do CrossZone Balancing.
228
229        :type force: bool
230        :param force: Ignore cache value and reload.
231
232        :rtype: bool
233        :return: True if balancing is enabled, False if not.
234        """
235        return self.get_attributes(force).cross_zone_load_balancing.enabled
236
237    def enable_cross_zone_load_balancing(self):
238        """
239        Turns on CrossZone Load Balancing for this ELB.
240
241        :rtype: bool
242        :return: True if successful, False if not.
243        """
244        success = self.connection.modify_lb_attribute(
245            self.name, 'crossZoneLoadBalancing', True)
246        if success and self._attributes:
247            self._attributes.cross_zone_load_balancing.enabled = True
248        return success
249
250    def disable_cross_zone_load_balancing(self):
251        """
252        Turns off CrossZone Load Balancing for this ELB.
253
254        :rtype: bool
255        :return: True if successful, False if not.
256        """
257        success = self.connection.modify_lb_attribute(
258            self.name, 'crossZoneLoadBalancing', False)
259        if success and self._attributes:
260            self._attributes.cross_zone_load_balancing.enabled = False
261        return success
262
263    def register_instances(self, instances):
264        """
265        Adds instances to this load balancer. All instances must be in the same
266        region as the load balancer. Adding endpoints that are already
267        registered with the load balancer has no effect.
268
269        :param list instances: List of instance IDs (strings) that you'd like
270            to add to this load balancer.
271
272        """
273        if isinstance(instances, six.string_types):
274            instances = [instances]
275        new_instances = self.connection.register_instances(self.name,
276                                                           instances)
277        self.instances = new_instances
278
279    def deregister_instances(self, instances):
280        """
281        Remove instances from this load balancer. Removing instances that are
282        not registered with the load balancer has no effect.
283
284        :param list instances: List of instance IDs (strings) that you'd like
285            to remove from this load balancer.
286
287        """
288        if isinstance(instances, six.string_types):
289            instances = [instances]
290        new_instances = self.connection.deregister_instances(self.name,
291                                                             instances)
292        self.instances = new_instances
293
294    def delete(self):
295        """
296        Delete this load balancer.
297        """
298        return self.connection.delete_load_balancer(self.name)
299
300    def configure_health_check(self, health_check):
301        """
302        Configures the health check behavior for the instances behind this
303        load balancer. See :ref:`elb-configuring-a-health-check` for a
304        walkthrough.
305
306        :param boto.ec2.elb.healthcheck.HealthCheck health_check: A
307            HealthCheck instance that tells the load balancer how to check
308            its instances for health.
309        """
310        return self.connection.configure_health_check(self.name, health_check)
311
312    def get_instance_health(self, instances=None):
313        """
314        Returns a list of :py:class:`boto.ec2.elb.instancestate.InstanceState`
315        objects, which show the health of the instances attached to this
316        load balancer.
317
318        :rtype: list
319        :returns: A list of
320            :py:class:`InstanceState <boto.ec2.elb.instancestate.InstanceState>`
321            instances, representing the instances
322            attached to this load balancer.
323        """
324        return self.connection.describe_instance_health(self.name, instances)
325
326    def create_listeners(self, listeners):
327        return self.connection.create_load_balancer_listeners(self.name,
328                                                              listeners)
329
330    def create_listener(self, inPort, outPort=None, proto="tcp"):
331        if outPort is None:
332            outPort = inPort
333        return self.create_listeners([(inPort, outPort, proto)])
334
335    def delete_listeners(self, listeners):
336        return self.connection.delete_load_balancer_listeners(self.name,
337                                                              listeners)
338
339    def delete_listener(self, inPort):
340        return self.delete_listeners([inPort])
341
342    def delete_policy(self, policy_name):
343        """
344        Deletes a policy from the LoadBalancer. The specified policy must not
345        be enabled for any listeners.
346        """
347        return self.connection.delete_lb_policy(self.name, policy_name)
348
349    def set_policies_of_listener(self, lb_port, policies):
350        return self.connection.set_lb_policies_of_listener(self.name,
351                                                           lb_port,
352                                                           policies)
353
354    def set_policies_of_backend_server(self, instance_port, policies):
355        return self.connection.set_lb_policies_of_backend_server(
356            self.name, instance_port, policies)
357
358    def create_cookie_stickiness_policy(self, cookie_expiration_period,
359                                        policy_name):
360        return self.connection.create_lb_cookie_stickiness_policy(
361            cookie_expiration_period, self.name, policy_name)
362
363    def create_app_cookie_stickiness_policy(self, name, policy_name):
364        return self.connection.create_app_cookie_stickiness_policy(name,
365                                                                   self.name,
366                                                                   policy_name)
367
368    def set_listener_SSL_certificate(self, lb_port, ssl_certificate_id):
369        return self.connection.set_lb_listener_SSL_certificate(
370            self.name, lb_port, ssl_certificate_id)
371
372    def create_lb_policy(self, policy_name, policy_type, policy_attribute):
373        return self.connection.create_lb_policy(
374            self.name, policy_name, policy_type, policy_attribute)
375
376    def attach_subnets(self, subnets):
377        """
378        Attaches load balancer to one or more subnets.
379        Attaching subnets that are already registered with the
380        Load Balancer has no effect.
381
382        :type subnets: string or List of strings
383        :param subnets: The name of the subnet(s) to add.
384
385        """
386        if isinstance(subnets, six.string_types):
387            subnets = [subnets]
388        new_subnets = self.connection.attach_lb_to_subnets(self.name, subnets)
389        self.subnets = new_subnets
390
391    def detach_subnets(self, subnets):
392        """
393        Detaches load balancer from one or more subnets.
394
395        :type subnets: string or List of strings
396        :param subnets: The name of the subnet(s) to detach.
397
398        """
399        if isinstance(subnets, six.string_types):
400            subnets = [subnets]
401        new_subnets = self.connection.detach_lb_from_subnets(
402            self.name, subnets)
403        self.subnets = new_subnets
404
405    def apply_security_groups(self, security_groups):
406        """
407        Associates one or more security groups with the load balancer.
408        The provided security groups will override any currently applied
409        security groups.
410
411        :type security_groups: string or List of strings
412        :param security_groups: The name of the security group(s) to add.
413
414        """
415        if isinstance(security_groups, six.string_types):
416            security_groups = [security_groups]
417        new_sgs = self.connection.apply_security_groups_to_lb(
418            self.name, security_groups)
419        self.security_groups = new_sgs
420