~ubuntu-branches/ubuntu/trusty/heat/trusty-security

« back to all changes in this revision

Viewing changes to contrib/rackspace/heat/engine/plugins/cloud_loadbalancer.py

  • Committer: Package Import Robot
  • Author(s): Chuck Short
  • Date: 2013-10-03 09:43:04 UTC
  • mto: This revision was merged to the branch mainline in revision 15.
  • Revision ID: package-import@ubuntu.com-20131003094304-zhhr2brapzlpvjmm
Tags: upstream-2013.2~rc1
ImportĀ upstreamĀ versionĀ 2013.2~rc1

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# vim: tabstop=4 shiftwidth=4 softtabstop=4
 
2
#
 
3
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
 
4
#    not use this file except in compliance with the License. You may obtain
 
5
#    a copy of the License at
 
6
#
 
7
#         http://www.apache.org/licenses/LICENSE-2.0
 
8
#
 
9
#    Unless required by applicable law or agreed to in writing, software
 
10
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 
11
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 
12
#    License for the specific language governing permissions and limitations
 
13
#    under the License.
 
14
try:
 
15
    from pyrax.exceptions import NotFound
 
16
except ImportError:
 
17
    #Setup fake exception for testing without pyrax
 
18
    class NotFound(Exception):
 
19
        pass
 
20
 
 
21
from heat.openstack.common import log as logging
 
22
from heat.openstack.common.gettextutils import _
 
23
from heat.engine import scheduler
 
24
from heat.engine.properties import Properties
 
25
from heat.common import exception
 
26
 
 
27
from . import rackspace_resource
 
28
 
 
29
logger = logging.getLogger(__name__)
 
30
 
 
31
 
 
32
class LoadbalancerBuildError(exception.HeatException):
 
33
    message = _("There was an error building the loadbalancer:%(lb_name)s.")
 
34
 
 
35
 
 
36
class CloudLoadBalancer(rackspace_resource.RackspaceResource):
 
37
 
 
38
    protocol_values = ["DNS_TCP", "DNS_UDP", "FTP", "HTTP", "HTTPS", "IMAPS",
 
39
                       "IMAPv4", "LDAP", "LDAPS", "MYSQL", "POP3", "POP3S",
 
40
                       "SMTP", "TCP", "TCP_CLIENT_FIRST", "UDP", "UDP_STREAM",
 
41
                       "SFTP"]
 
42
 
 
43
    algorithm_values = ["LEAST_CONNECTIONS", "RANDOM", "ROUND_ROBIN",
 
44
                        "WEIGHTED_LEAST_CONNECTIONS", "WEIGHTED_ROUND_ROBIN"]
 
45
 
 
46
    nodes_schema = {
 
47
        'address': {'Type': 'String', 'Required': False},
 
48
        'ref': {'Type': 'String', 'Required': False},
 
49
        'port': {'Type': 'Number', 'Required': True},
 
50
        'condition': {'Type': 'String', 'Required': True,
 
51
                      'AllowedValues': ['ENABLED', 'DISABLED'],
 
52
                      'Default': 'ENABLED'},
 
53
        'type': {'Type': 'String', 'Required': False,
 
54
                 'AllowedValues': ['PRIMARY', 'SECONDARY']},
 
55
        'weight': {'Type': 'Number', 'MinValue': 1, 'MaxValue': 100}
 
56
    }
 
57
 
 
58
    access_list_schema = {
 
59
        'address': {'Type': 'String', 'Required': True},
 
60
        'type': {'Type': 'String', 'Required': True,
 
61
                 'AllowedValues': ['ALLOW', 'DENY']}
 
62
    }
 
63
 
 
64
    connection_logging_schema = {
 
65
        'enabled': {'Type': 'String', 'Required': True,
 
66
                    'AllowedValues': ["true", "false"]}
 
67
    }
 
68
 
 
69
    connection_throttle_schema = {
 
70
        'maxConnectionRate': {'Type': 'Number', 'Required': False,
 
71
                              'MinValue': 0, 'MaxValue': 100000},
 
72
        'minConnections': {'Type': 'Number', 'Required': False, 'MinValue': 1,
 
73
                           'MaxValue': 1000},
 
74
        'maxConnections': {'Type': 'Number', 'Required': False, 'MinValue': 1,
 
75
                           'MaxValue': 100000},
 
76
        'rateInterval': {'Type': 'Number', 'Required': False, 'MinValue': 1,
 
77
                         'MaxValue': 3600}
 
78
    }
 
79
 
 
80
    virtualip_schema = {
 
81
        'type': {'Type': 'String', 'Required': True,
 
82
                 'AllowedValues': ['SERVICENET', 'PUBLIC']},
 
83
        'ipVersion': {'Type': 'String', 'Required': False,
 
84
                      'AllowedValues': ['IPV6', 'IPV4'],
 
85
                      'Default': 'IPV6'}
 
86
    }
 
87
 
 
88
    health_monitor_base_schema = {
 
89
        'attemptsBeforeDeactivation': {'Type': 'Number', 'MinValue': 1,
 
90
                                       'MaxValue': 10, 'Required': True},
 
91
        'delay': {'Type': 'Number', 'MinValue': 1, 'MaxValue': 3600,
 
92
                  'Required': True},
 
93
        'timeout': {'Type': 'Number', 'MinValue': 1, 'MaxValue': 300,
 
94
                    'Required': True},
 
95
        'type': {'Type': 'String',
 
96
                 'AllowedValues': ['CONNECT', 'HTTP', 'HTTPS'],
 
97
                 'Required': True},
 
98
        'bodyRegex': {'Type': 'String', 'Required': False},
 
99
        'hostHeader': {'Type': 'String', 'Required': False},
 
100
        'path': {'Type': 'String', 'Required': False},
 
101
        'statusRegex': {'Type': 'String', 'Required': False},
 
102
    }
 
103
 
 
104
    health_monitor_connect_schema = {
 
105
        'attemptsBeforeDeactivation': {'Type': 'Number', 'MinValue': 1,
 
106
                                       'MaxValue': 10, 'Required': True},
 
107
        'delay': {'Type': 'Number', 'MinValue': 1, 'MaxValue': 3600,
 
108
                  'Required': True},
 
109
        'timeout': {'Type': 'Number', 'MinValue': 1, 'MaxValue': 300,
 
110
                    'Required': True},
 
111
        'type': {'Type': 'String', 'AllowedValues': ['CONNECT'],
 
112
                 'Required': True}
 
113
    }
 
114
 
 
115
    health_monitor_http_schema = {
 
116
        'attemptsBeforeDeactivation': {'Type': 'Number', 'Required': True,
 
117
                                       'MaxValue': 10, 'MinValue': 1},
 
118
        'bodyRegex': {'Type': 'String', 'Required': True},
 
119
        'delay': {'Type': 'Number', 'Required': True,
 
120
                  'MaxValue': 3600, 'MinValue': 1},
 
121
        'hostHeader': {'Type': 'String', 'Required': False},
 
122
        'path': {'Type': 'String', 'Required': True},
 
123
        'statusRegex': {'Type': 'String', 'Required': True},
 
124
        'timeout': {'Type': 'Number', 'Required': True,
 
125
                    'MaxValue': 300, 'MinValue': 1},
 
126
        'type': {'Type': 'String', 'Required': True,
 
127
                 'AllowedValues': ['HTTP', 'HTTPS']}
 
128
    }
 
129
 
 
130
    ssl_termination_base_schema = {
 
131
        "enabled": {'Type': 'Boolean', 'Required': True},
 
132
        "securePort": {'Type': 'Number', 'Required': False},
 
133
        "privatekey": {'Type': 'String', 'Required': False},
 
134
        "certificate": {'Type': 'String', 'Required': False},
 
135
        #only required if configuring intermediate ssl termination
 
136
        #add to custom validation
 
137
        "intermediateCertificate": {'Type': 'String', 'Required': False},
 
138
        #pyrax will default to false
 
139
        "secureTrafficOnly": {'Type': 'Boolean', 'Required': False}
 
140
    }
 
141
 
 
142
    ssl_termination_enabled_schema = {
 
143
        "securePort": {'Type': 'Number', 'Required': True},
 
144
        "privatekey": {'Type': 'String', 'Required': True},
 
145
        "certificate": {'Type': 'String', 'Required': True},
 
146
        "intermediateCertificate": {'Type': 'String', 'Required': False},
 
147
        "enabled": {'Type': 'Boolean', 'Required': True,
 
148
                    'AllowedValues': [True]},
 
149
        "secureTrafficOnly": {'Type': 'Boolean', 'Required': False}
 
150
    }
 
151
 
 
152
    properties_schema = {
 
153
        'name': {'Type': 'String', 'Required': False},
 
154
        'nodes': {'Type': 'List', 'Required': True,
 
155
                  'Schema': {'Type': 'Map', 'Schema': nodes_schema}},
 
156
        'protocol': {'Type': 'String', 'Required': True,
 
157
                     'AllowedValues': protocol_values},
 
158
        'accessList': {'Type': 'List', 'Required': False,
 
159
                       'Schema': {'Type': 'Map',
 
160
                                  'Schema': access_list_schema}},
 
161
        'halfClosed': {'Type': 'Boolean', 'Required': False},
 
162
        'algorithm': {'Type': 'String', 'Required': False},
 
163
        'connectionLogging': {'Type': 'Boolean', 'Required': False},
 
164
        'metadata': {'Type': 'Map', 'Required': False},
 
165
        'port': {'Type': 'Number', 'Required': True},
 
166
        'timeout': {'Type': 'Number', 'Required': False, 'MinValue': 1,
 
167
                    'MaxValue': 120},
 
168
        'connectionThrottle': {'Type': 'Map', 'Required': False,
 
169
                               'Schema': connection_throttle_schema},
 
170
        'sessionPersistence': {'Type': 'String', 'Required': False,
 
171
                               'AllowedValues': ['HTTP_COOKIE', 'SOURCE_IP']},
 
172
        'virtualIps': {'Type': 'List', 'Required': True,
 
173
                       'Schema': {'Type': 'Map', 'Schema': virtualip_schema}},
 
174
        'contentCaching': {'Type': 'String', 'Required': False,
 
175
                           'AllowedValues': ['ENABLED', 'DISABLED']},
 
176
        'healthMonitor': {'Type': 'Map', 'Required': False,
 
177
                          'Schema': health_monitor_base_schema},
 
178
        'sslTermination': {'Type': 'Map', 'Required': False,
 
179
                           'Schema': ssl_termination_base_schema},
 
180
        'errorPage': {'Type': 'String', 'Required': False}
 
181
    }
 
182
 
 
183
    attributes_schema = {
 
184
        'PublicIp': ('Public IP address of the specified '
 
185
                     'instance.')}
 
186
 
 
187
    update_allowed_keys = ('Properties',)
 
188
    update_allowed_properties = ('nodes',)
 
189
 
 
190
    def __init__(self, name, json_snippet, stack):
 
191
        super(CloudLoadBalancer, self).__init__(name, json_snippet, stack)
 
192
        self.clb = self.cloud_lb()
 
193
 
 
194
    def _setup_properties(self, properties, function):
 
195
        """Use defined schema properties as kwargs for loadbalancer objects."""
 
196
        if properties and function:
 
197
            return [function(**item_dict) for item_dict in properties]
 
198
        elif function:
 
199
            return [function()]
 
200
 
 
201
    def _alter_properties_for_api(self):
 
202
        """The following properties have usless key/value pairs which must
 
203
        be passed into the api. Set them up to make template definition easier.
 
204
        """
 
205
        session_persistence = None
 
206
        if'sessionPersistence' in self.properties.data:
 
207
            session_persistence = {'persistenceType':
 
208
                                   self.properties['sessionPersistence']}
 
209
        connection_logging = None
 
210
        if 'connectionLogging' in self.properties.data:
 
211
            connection_logging = {'enabled':
 
212
                                  self.properties['connectionLogging']}
 
213
        metadata = None
 
214
        if 'metadata' in self.properties.data:
 
215
            metadata = [{'key': k, 'value': v}
 
216
                        for k, v in self.properties['metadata'].iteritems()]
 
217
 
 
218
        return (session_persistence, connection_logging, metadata)
 
219
 
 
220
    def _check_status(self, loadbalancer, status_list):
 
221
        """Update the loadbalancer state, check the status."""
 
222
        loadbalancer.get()
 
223
        if loadbalancer.status in status_list:
 
224
            return True
 
225
        else:
 
226
            return False
 
227
 
 
228
    def _configure_post_creation(self, loadbalancer):
 
229
        """Configure all load balancer properties that must be done post
 
230
        creation.
 
231
        """
 
232
        if self.properties['accessList']:
 
233
            while not self._check_status(loadbalancer, ['ACTIVE']):
 
234
                yield
 
235
            loadbalancer.add_access_list(self.properties['accessList'])
 
236
 
 
237
        if self.properties['errorPage']:
 
238
            while not self._check_status(loadbalancer, ['ACTIVE']):
 
239
                yield
 
240
            loadbalancer.set_error_page(self.properties['errorPage'])
 
241
 
 
242
        if self.properties['sslTermination']:
 
243
            while not self._check_status(loadbalancer, ['ACTIVE']):
 
244
                yield
 
245
            loadbalancer.add_ssl_termination(
 
246
                self.properties['sslTermination']['securePort'],
 
247
                self.properties['sslTermination']['privatekey'],
 
248
                self.properties['sslTermination']['certificate'],
 
249
                intermediateCertificate=
 
250
                self.properties['sslTermination']
 
251
                ['intermediateCertificate'],
 
252
                enabled=self.properties['sslTermination']['enabled'],
 
253
                secureTrafficOnly=self.properties['sslTermination']
 
254
                ['secureTrafficOnly'])
 
255
 
 
256
        if 'contentCaching' in self.properties:
 
257
            enabled = True if self.properties['contentCaching'] == 'ENABLED'\
 
258
                else False
 
259
            while not self._check_status(loadbalancer, ['ACTIVE']):
 
260
                yield
 
261
            loadbalancer.content_caching = enabled
 
262
 
 
263
    def handle_create(self):
 
264
        node_list = []
 
265
        for node in self.properties['nodes']:
 
266
            # resolve references to stack resource IP's
 
267
            if node.get('ref'):
 
268
                node['address'] = (self.stack
 
269
                                   .resource_by_refid(node['ref'])
 
270
                                   .FnGetAtt('PublicIp'))
 
271
            del node['ref']
 
272
            node_list.append(node)
 
273
 
 
274
        nodes = self._setup_properties(node_list, self.clb.Node)
 
275
        virtual_ips = self._setup_properties(self.properties.get('virtualIps'),
 
276
                                             self.clb.VirtualIP)
 
277
 
 
278
        (session_persistence, connection_logging, metadata) = \
 
279
            self._alter_properties_for_api()
 
280
 
 
281
        lb_body = {
 
282
            'port': self.properties['port'],
 
283
            'protocol': self.properties['protocol'],
 
284
            'nodes': nodes,
 
285
            'virtual_ips': virtual_ips,
 
286
            'algorithm': self.properties.get('algorithm'),
 
287
            'halfClosed': self.properties.get('halfClosed'),
 
288
            'connectionThrottle': self.properties.get('connectionThrottle'),
 
289
            'metadata': metadata,
 
290
            'healthMonitor': self.properties.get('healthMonitor'),
 
291
            'sessionPersistence': session_persistence,
 
292
            'timeout': self.properties.get('timeout'),
 
293
            'connectionLogging': connection_logging,
 
294
        }
 
295
 
 
296
        lb_name = self.properties.get('name') or self.physical_resource_name()
 
297
        logger.debug('Creating loadbalancer: %s' % {lb_name: lb_body})
 
298
        loadbalancer = self.clb.create(lb_name, **lb_body)
 
299
        self.resource_id_set(str(loadbalancer.id))
 
300
 
 
301
        post_create = scheduler.TaskRunner(self._configure_post_creation,
 
302
                                           loadbalancer)
 
303
        post_create(timeout=600)
 
304
        return loadbalancer
 
305
 
 
306
    def check_create_complete(self, loadbalancer):
 
307
        return self._check_status(loadbalancer, ['ACTIVE'])
 
308
 
 
309
    def handle_update(self, json_snippet, tmpl_diff, prop_diff):
 
310
        """
 
311
        Add and remove nodes specified in the prop_diff.
 
312
        """
 
313
        loadbalancer = self.clb.get(self.resource_id)
 
314
        if 'nodes' in prop_diff:
 
315
            current_nodes = loadbalancer.nodes
 
316
            #Loadbalancers can be uniquely identified by address and port.
 
317
            #Old is a dict of all nodes the loadbalancer currently knows about.
 
318
            for node in prop_diff['nodes']:
 
319
                # resolve references to stack resource IP's
 
320
                if node.get('ref'):
 
321
                    node['address'] = (self.stack
 
322
                                       .resource_by_refid(node['ref'])
 
323
                                       .FnGetAtt('PublicIp'))
 
324
                    del node['ref']
 
325
            old = dict(("{0.address}{0.port}".format(node), node)
 
326
                       for node in current_nodes)
 
327
            #New is a dict of the nodes the loadbalancer will know about after
 
328
            #this update.
 
329
            new = dict(("%s%s" % (node['address'], node['port']), node)
 
330
                       for node in prop_diff['nodes'])
 
331
 
 
332
            old_set = set(old.keys())
 
333
            new_set = set(new.keys())
 
334
 
 
335
            deleted = old_set.difference(new_set)
 
336
            added = new_set.difference(old_set)
 
337
            updated = new_set.intersection(old_set)
 
338
 
 
339
            if len(current_nodes) + len(added) - len(deleted) < 1:
 
340
                raise ValueError("The loadbalancer:%s requires at least one "
 
341
                                 "node." % self.name)
 
342
            """
 
343
            Add loadbalancers in the new map that are not in the old map.
 
344
            Add before delete to avoid deleting the last node and getting in
 
345
            an invalid state.
 
346
            """
 
347
            new_nodes = [self.clb.Node(**new[lb_node])
 
348
                         for lb_node in added]
 
349
            if new_nodes:
 
350
                loadbalancer.add_nodes(new_nodes)
 
351
 
 
352
            #Delete loadbalancers in the old dict that are not in the new dict.
 
353
            for node in deleted:
 
354
                old[node].delete()
 
355
 
 
356
            #Update nodes that have been changed
 
357
            for node in updated:
 
358
                node_changed = False
 
359
                for attribute in new[node].keys():
 
360
                    if new[node][attribute] != getattr(old[node], attribute):
 
361
                        node_changed = True
 
362
                        setattr(old[node], attribute, new[node][attribute])
 
363
                if node_changed:
 
364
                    old[node].update()
 
365
 
 
366
    def handle_delete(self):
 
367
        if self.resource_id is None:
 
368
            return
 
369
        try:
 
370
            loadbalancer = self.clb.get(self.resource_id)
 
371
        except NotFound:
 
372
            pass
 
373
        else:
 
374
            if loadbalancer.status != 'DELETED':
 
375
                loadbalancer.delete()
 
376
                self.resource_id_set(None)
 
377
 
 
378
    def _remove_none(self, property_dict):
 
379
        '''
 
380
        Remove values that may be initialized to None and would cause problems
 
381
        during schema validation.
 
382
        '''
 
383
        return dict((key, value)
 
384
                    for (key, value) in property_dict.iteritems()
 
385
                    if value)
 
386
 
 
387
    def validate(self):
 
388
        """
 
389
        Validate any of the provided params
 
390
        """
 
391
        res = super(CloudLoadBalancer, self).validate()
 
392
        if res:
 
393
            return res
 
394
 
 
395
        if self.properties.get('halfClosed'):
 
396
            if not (self.properties['protocol'] == 'TCP' or
 
397
                    self.properties['protocol'] == 'TCP_CLIENT_FIRST'):
 
398
                return {'Error':
 
399
                        'The halfClosed property is only available for the '
 
400
                        'TCP or TCP_CLIENT_FIRST protocols'}
 
401
 
 
402
        #health_monitor connect and http types require completely different
 
403
        #schema
 
404
        if self.properties.get('healthMonitor'):
 
405
            health_monitor = \
 
406
                self._remove_none(self.properties['healthMonitor'])
 
407
 
 
408
            if health_monitor['type'] == 'CONNECT':
 
409
                schema = CloudLoadBalancer.health_monitor_connect_schema
 
410
            else:
 
411
                schema = CloudLoadBalancer.health_monitor_http_schema
 
412
            try:
 
413
                Properties(schema,
 
414
                           health_monitor,
 
415
                           self.stack.resolve_runtime_data,
 
416
                           self.name).validate()
 
417
            except exception.StackValidationFailed as svf:
 
418
                return {'Error': str(svf)}
 
419
 
 
420
        if self.properties.get('sslTermination'):
 
421
            ssl_termination = self._remove_none(
 
422
                self.properties['sslTermination'])
 
423
 
 
424
            if ssl_termination['enabled']:
 
425
                try:
 
426
                    Properties(CloudLoadBalancer.
 
427
                               ssl_termination_enabled_schema,
 
428
                               ssl_termination,
 
429
                               self.stack.resolve_runtime_data,
 
430
                               self.name).validate()
 
431
                except exception.StackValidationFailed as svf:
 
432
                    return {'Error': str(svf)}
 
433
 
 
434
    def FnGetRefId(self):
 
435
        return unicode(self.name)
 
436
 
 
437
    def _public_ip(self):
 
438
        #TODO(andrew-plunk) return list here and let caller choose ip
 
439
        for ip in self.clb.get(self.resource_id).virtual_ips:
 
440
            if ip.type == 'PUBLIC':
 
441
                return ip.address
 
442
 
 
443
    def _resolve_attribute(self, key):
 
444
        attribute_function = {
 
445
            'PublicIp': self._public_ip()
 
446
        }
 
447
        if key not in attribute_function:
 
448
            raise exception.InvalidTemplateAttribute(resource=self.name,
 
449
                                                     key=key)
 
450
        function = attribute_function[key]
 
451
        logger.info('%s.GetAtt(%s) == %s' % (self.name, key, function))
 
452
        return unicode(function)
 
453
 
 
454
 
 
455
def resource_mapping():
 
456
    if rackspace_resource.PYRAX_INSTALLED:
 
457
        return {
 
458
            'Rackspace::Cloud::LoadBalancer': CloudLoadBalancer
 
459
        }
 
460
    else:
 
461
        return {}