1
# vim: tabstop=4 shiftwidth=4 softtabstop=4
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
7
# http://www.apache.org/licenses/LICENSE-2.0
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
15
from pyrax.exceptions import NotFound
17
#Setup fake exception for testing without pyrax
18
class NotFound(Exception):
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
27
from . import rackspace_resource
29
logger = logging.getLogger(__name__)
32
class LoadbalancerBuildError(exception.HeatException):
33
message = _("There was an error building the loadbalancer:%(lb_name)s.")
36
class CloudLoadBalancer(rackspace_resource.RackspaceResource):
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",
43
algorithm_values = ["LEAST_CONNECTIONS", "RANDOM", "ROUND_ROBIN",
44
"WEIGHTED_LEAST_CONNECTIONS", "WEIGHTED_ROUND_ROBIN"]
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}
58
access_list_schema = {
59
'address': {'Type': 'String', 'Required': True},
60
'type': {'Type': 'String', 'Required': True,
61
'AllowedValues': ['ALLOW', 'DENY']}
64
connection_logging_schema = {
65
'enabled': {'Type': 'String', 'Required': True,
66
'AllowedValues': ["true", "false"]}
69
connection_throttle_schema = {
70
'maxConnectionRate': {'Type': 'Number', 'Required': False,
71
'MinValue': 0, 'MaxValue': 100000},
72
'minConnections': {'Type': 'Number', 'Required': False, 'MinValue': 1,
74
'maxConnections': {'Type': 'Number', 'Required': False, 'MinValue': 1,
76
'rateInterval': {'Type': 'Number', 'Required': False, 'MinValue': 1,
81
'type': {'Type': 'String', 'Required': True,
82
'AllowedValues': ['SERVICENET', 'PUBLIC']},
83
'ipVersion': {'Type': 'String', 'Required': False,
84
'AllowedValues': ['IPV6', 'IPV4'],
88
health_monitor_base_schema = {
89
'attemptsBeforeDeactivation': {'Type': 'Number', 'MinValue': 1,
90
'MaxValue': 10, 'Required': True},
91
'delay': {'Type': 'Number', 'MinValue': 1, 'MaxValue': 3600,
93
'timeout': {'Type': 'Number', 'MinValue': 1, 'MaxValue': 300,
95
'type': {'Type': 'String',
96
'AllowedValues': ['CONNECT', 'HTTP', 'HTTPS'],
98
'bodyRegex': {'Type': 'String', 'Required': False},
99
'hostHeader': {'Type': 'String', 'Required': False},
100
'path': {'Type': 'String', 'Required': False},
101
'statusRegex': {'Type': 'String', 'Required': False},
104
health_monitor_connect_schema = {
105
'attemptsBeforeDeactivation': {'Type': 'Number', 'MinValue': 1,
106
'MaxValue': 10, 'Required': True},
107
'delay': {'Type': 'Number', 'MinValue': 1, 'MaxValue': 3600,
109
'timeout': {'Type': 'Number', 'MinValue': 1, 'MaxValue': 300,
111
'type': {'Type': 'String', 'AllowedValues': ['CONNECT'],
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']}
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}
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}
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,
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}
183
attributes_schema = {
184
'PublicIp': ('Public IP address of the specified '
187
update_allowed_keys = ('Properties',)
188
update_allowed_properties = ('nodes',)
190
def __init__(self, name, json_snippet, stack):
191
super(CloudLoadBalancer, self).__init__(name, json_snippet, stack)
192
self.clb = self.cloud_lb()
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]
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.
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']}
214
if 'metadata' in self.properties.data:
215
metadata = [{'key': k, 'value': v}
216
for k, v in self.properties['metadata'].iteritems()]
218
return (session_persistence, connection_logging, metadata)
220
def _check_status(self, loadbalancer, status_list):
221
"""Update the loadbalancer state, check the status."""
223
if loadbalancer.status in status_list:
228
def _configure_post_creation(self, loadbalancer):
229
"""Configure all load balancer properties that must be done post
232
if self.properties['accessList']:
233
while not self._check_status(loadbalancer, ['ACTIVE']):
235
loadbalancer.add_access_list(self.properties['accessList'])
237
if self.properties['errorPage']:
238
while not self._check_status(loadbalancer, ['ACTIVE']):
240
loadbalancer.set_error_page(self.properties['errorPage'])
242
if self.properties['sslTermination']:
243
while not self._check_status(loadbalancer, ['ACTIVE']):
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'])
256
if 'contentCaching' in self.properties:
257
enabled = True if self.properties['contentCaching'] == 'ENABLED'\
259
while not self._check_status(loadbalancer, ['ACTIVE']):
261
loadbalancer.content_caching = enabled
263
def handle_create(self):
265
for node in self.properties['nodes']:
266
# resolve references to stack resource IP's
268
node['address'] = (self.stack
269
.resource_by_refid(node['ref'])
270
.FnGetAtt('PublicIp'))
272
node_list.append(node)
274
nodes = self._setup_properties(node_list, self.clb.Node)
275
virtual_ips = self._setup_properties(self.properties.get('virtualIps'),
278
(session_persistence, connection_logging, metadata) = \
279
self._alter_properties_for_api()
282
'port': self.properties['port'],
283
'protocol': self.properties['protocol'],
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,
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))
301
post_create = scheduler.TaskRunner(self._configure_post_creation,
303
post_create(timeout=600)
306
def check_create_complete(self, loadbalancer):
307
return self._check_status(loadbalancer, ['ACTIVE'])
309
def handle_update(self, json_snippet, tmpl_diff, prop_diff):
311
Add and remove nodes specified in the prop_diff.
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
321
node['address'] = (self.stack
322
.resource_by_refid(node['ref'])
323
.FnGetAtt('PublicIp'))
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
329
new = dict(("%s%s" % (node['address'], node['port']), node)
330
for node in prop_diff['nodes'])
332
old_set = set(old.keys())
333
new_set = set(new.keys())
335
deleted = old_set.difference(new_set)
336
added = new_set.difference(old_set)
337
updated = new_set.intersection(old_set)
339
if len(current_nodes) + len(added) - len(deleted) < 1:
340
raise ValueError("The loadbalancer:%s requires at least one "
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
347
new_nodes = [self.clb.Node(**new[lb_node])
348
for lb_node in added]
350
loadbalancer.add_nodes(new_nodes)
352
#Delete loadbalancers in the old dict that are not in the new dict.
356
#Update nodes that have been changed
359
for attribute in new[node].keys():
360
if new[node][attribute] != getattr(old[node], attribute):
362
setattr(old[node], attribute, new[node][attribute])
366
def handle_delete(self):
367
if self.resource_id is None:
370
loadbalancer = self.clb.get(self.resource_id)
374
if loadbalancer.status != 'DELETED':
375
loadbalancer.delete()
376
self.resource_id_set(None)
378
def _remove_none(self, property_dict):
380
Remove values that may be initialized to None and would cause problems
381
during schema validation.
383
return dict((key, value)
384
for (key, value) in property_dict.iteritems()
389
Validate any of the provided params
391
res = super(CloudLoadBalancer, self).validate()
395
if self.properties.get('halfClosed'):
396
if not (self.properties['protocol'] == 'TCP' or
397
self.properties['protocol'] == 'TCP_CLIENT_FIRST'):
399
'The halfClosed property is only available for the '
400
'TCP or TCP_CLIENT_FIRST protocols'}
402
#health_monitor connect and http types require completely different
404
if self.properties.get('healthMonitor'):
406
self._remove_none(self.properties['healthMonitor'])
408
if health_monitor['type'] == 'CONNECT':
409
schema = CloudLoadBalancer.health_monitor_connect_schema
411
schema = CloudLoadBalancer.health_monitor_http_schema
415
self.stack.resolve_runtime_data,
416
self.name).validate()
417
except exception.StackValidationFailed as svf:
418
return {'Error': str(svf)}
420
if self.properties.get('sslTermination'):
421
ssl_termination = self._remove_none(
422
self.properties['sslTermination'])
424
if ssl_termination['enabled']:
426
Properties(CloudLoadBalancer.
427
ssl_termination_enabled_schema,
429
self.stack.resolve_runtime_data,
430
self.name).validate()
431
except exception.StackValidationFailed as svf:
432
return {'Error': str(svf)}
434
def FnGetRefId(self):
435
return unicode(self.name)
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':
443
def _resolve_attribute(self, key):
444
attribute_function = {
445
'PublicIp': self._public_ip()
447
if key not in attribute_function:
448
raise exception.InvalidTemplateAttribute(resource=self.name,
450
function = attribute_function[key]
451
logger.info('%s.GetAtt(%s) == %s' % (self.name, key, function))
452
return unicode(function)
455
def resource_mapping():
456
if rackspace_resource.PYRAX_INSTALLED:
458
'Rackspace::Cloud::LoadBalancer': CloudLoadBalancer