From 5dc8e37ade2d4d732526d243d576df105632bc46 Mon Sep 17 00:00:00 2001 From: Andrew Plunk Date: Wed, 3 Jul 2013 16:41:26 -0500 Subject: [PATCH] Add Rackspace cloud loadbalancer resource. Add a resource implementation of the Rackspace Cloud Loadbalancer. Change-Id: Ie62471424af3170c71e84bed69c603d243572fdb --- .../resources/rackspace/cloud_loadbalancer.py | 462 +++++++++++++++ heat/tests/test_cloud_loadbalancer.py | 538 ++++++++++++++++++ 2 files changed, 1000 insertions(+) create mode 100644 heat/engine/resources/rackspace/cloud_loadbalancer.py create mode 100644 heat/tests/test_cloud_loadbalancer.py diff --git a/heat/engine/resources/rackspace/cloud_loadbalancer.py b/heat/engine/resources/rackspace/cloud_loadbalancer.py new file mode 100644 index 0000000000..c6419197c3 --- /dev/null +++ b/heat/engine/resources/rackspace/cloud_loadbalancer.py @@ -0,0 +1,462 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +try: + from pyrax.exceptions import NotFound +except ImportError: + #Setup fake exception for testing without pyrax + class NotFound(Exception): + pass + +from heat.openstack.common import log as logging +from heat.openstack.common.exception import OpenstackException +from heat.openstack.common.gettextutils import _ +from heat.engine import scheduler +from heat.engine.properties import Properties +from heat.engine.resources.rackspace import rackspace_resource +from heat.common import exception + +logger = logging.getLogger(__name__) + + +class LoadbalancerBuildError(OpenstackException): + message = _("There was an error building the loadbalancer:%(lb_name)s.") + + +class CloudLoadBalancer(rackspace_resource.RackspaceResource): + + protocol_values = ["DNS_TCP", "DNS_UDP", "FTP", "HTTP", "HTTPS", "IMAPS", + "IMAPv4", "LDAP", "LDAPS", "MYSQL", "POP3", "POP3S", + "SMTP", "TCP", "TCP_CLIENT_FIRST", "UDP", "UDP_STREAM", + "SFTP"] + + algorithm_values = ["LEAST_CONNECTIONS", "RANDOM", "ROUND_ROBIN", + "WEIGHTED_LEAST_CONNECTIONS", "WEIGHTED_ROUND_ROBIN"] + + nodes_schema = { + 'address': {'Type': 'String', 'Required': False}, + 'ref': {'Type': 'String', 'Required': False}, + 'port': {'Type': 'Number', 'Required': True}, + 'condition': {'Type': 'String', 'Required': True, + 'AllowedValues': ['ENABLED', 'DISABLED'], + 'Default': 'ENABLED'}, + 'type': {'Type': 'String', 'Required': False, + 'AllowedValues': ['PRIMARY', 'SECONDARY']}, + 'weight': {'Type': 'Number', 'MinValue': 1, 'MaxValue': 100} + } + + access_list_schema = { + 'address': {'Type': 'String', 'Required': True}, + 'type': {'Type': 'String', 'Required': True, + 'AllowedValues': ['ALLOW', 'DENY']} + } + + connection_logging_schema = { + 'enabled': {'Type': 'String', 'Required': True, + 'AllowedValues': ["true", "false"]} + } + + connection_throttle_schema = { + 'maxConnectionRate': {'Type': 'Number', 'Required': False, + 'MinValue': 0, 'MaxValue': 100000}, + 'minConnections': {'Type': 'Number', 'Required': False, 'MinValue': 1, + 'MaxValue': 1000}, + 'maxConnections': {'Type': 'Number', 'Required': False, 'MinValue': 1, + 'MaxValue': 100000}, + 'rateInterval': {'Type': 'Number', 'Required': False, 'MinValue': 1, + 'MaxValue': 3600} + } + + virtualip_schema = { + 'type': {'Type': 'String', 'Required': True, + 'AllowedValues': ['SERVICENET', 'PUBLIC']}, + 'ipVersion': {'Type': 'String', 'Required': False, + 'AllowedValues': ['IPV6', 'IPV4'], + 'Default': 'IPV6'} + } + + health_monitor_base_schema = { + 'attemptsBeforeDeactivation': {'Type': 'Number', 'MinValue': 1, + 'MaxValue': 10, 'Required': True}, + 'delay': {'Type': 'Number', 'MinValue': 1, 'MaxValue': 3600, + 'Required': True}, + 'timeout': {'Type': 'Number', 'MinValue': 1, 'MaxValue': 300, + 'Required': True}, + 'type': {'Type': 'String', + 'AllowedValues': ['CONNECT', 'HTTP', 'HTTPS'], + 'Required': True}, + 'bodyRegex': {'Type': 'String', 'Required': False}, + 'hostHeader': {'Type': 'String', 'Required': False}, + 'path': {'Type': 'String', 'Required': False}, + 'statusRegex': {'Type': 'String', 'Required': False}, + } + + health_monitor_connect_schema = { + 'attemptsBeforeDeactivation': {'Type': 'Number', 'MinValue': 1, + 'MaxValue': 10, 'Required': True}, + 'delay': {'Type': 'Number', 'MinValue': 1, 'MaxValue': 3600, + 'Required': True}, + 'timeout': {'Type': 'Number', 'MinValue': 1, 'MaxValue': 300, + 'Required': True}, + 'type': {'Type': 'String', 'AllowedValues': ['CONNECT'], + 'Required': True} + } + + health_monitor_http_schema = { + 'attemptsBeforeDeactivation': {'Type': 'Number', 'Required': True, + 'MaxValue': 10, 'MinValue': 1}, + 'bodyRegex': {'Type': 'String', 'Required': True}, + 'delay': {'Type': 'Number', 'Required': True, + 'MaxValue': 3600, 'MinValue': 1}, + 'hostHeader': {'Type': 'String', 'Required': False}, + 'path': {'Type': 'String', 'Required': True}, + 'statusRegex': {'Type': 'String', 'Required': True}, + 'timeout': {'Type': 'Number', 'Required': True, + 'MaxValue': 300, 'MinValue': 1}, + 'type': {'Type': 'String', 'Required': True, + 'AllowedValues': ['HTTP', 'HTTPS']} + } + + ssl_termination_base_schema = { + "enabled": {'Type': 'Boolean', 'Required': True}, + "securePort": {'Type': 'Number', 'Required': False}, + "privatekey": {'Type': 'String', 'Required': False}, + "certificate": {'Type': 'String', 'Required': False}, + #only required if configuring intermediate ssl termination + #add to custom validation + "intermediateCertificate": {'Type': 'String', 'Required': False}, + #pyrax will default to false + "secureTrafficOnly": {'Type': 'Boolean', 'Required': False} + } + + ssl_termination_enabled_schema = { + "securePort": {'Type': 'Number', 'Required': True}, + "privatekey": {'Type': 'String', 'Required': True}, + "certificate": {'Type': 'String', 'Required': True}, + "intermediateCertificate": {'Type': 'String', 'Required': False}, + "enabled": {'Type': 'Boolean', 'Required': True, + 'AllowedValues': [True]}, + "secureTrafficOnly": {'Type': 'Boolean', 'Required': False} + } + + properties_schema = { + 'name': {'Type': 'String', 'Required': False}, + 'nodes': {'Type': 'List', 'Required': True, + 'Schema': {'Type': 'Map', 'Schema': nodes_schema}}, + 'protocol': {'Type': 'String', 'Required': True, + 'AllowedValues': protocol_values}, + 'accessList': {'Type': 'List', 'Required': False, + 'Schema': {'Type': 'Map', + 'Schema': access_list_schema}}, + 'halfClosed': {'Type': 'Boolean', 'Required': False}, + 'algorithm': {'Type': 'String', 'Required': False}, + 'connectionLogging': {'Type': 'Boolean', 'Required': False}, + 'metadata': {'Type': 'Map', 'Required': False}, + 'port': {'Type': 'Number', 'Required': True}, + 'timeout': {'Type': 'Number', 'Required': False, 'MinValue': 1, + 'MaxValue': 120}, + 'connectionThrottle': {'Type': 'Map', 'Required': False, + 'Schema': connection_throttle_schema}, + 'sessionPersistence': {'Type': 'String', 'Required': False, + 'AllowedValues': ['HTTP_COOKIE', 'SOURCE_IP']}, + 'virtualIps': {'Type': 'List', 'Required': True, + 'Schema': {'Type': 'Map', 'Schema': virtualip_schema}}, + 'contentCaching': {'Type': 'String', 'Required': False, + 'AllowedValues': ['ENABLED', 'DISABLED']}, + 'healthMonitor': {'Type': 'Map', 'Required': False, + 'Schema': health_monitor_base_schema}, + 'sslTermination': {'Type': 'Map', 'Required': False, + 'Schema': ssl_termination_base_schema}, + 'errorPage': {'Type': 'String', 'Required': False} + } + + attributes_schema = { + 'PublicIp': ('Public IP address of the specified ' + 'instance.')} + + update_allowed_keys = ('Properties',) + update_allowed_properties = ('nodes',) + + def __init__(self, name, json_snippet, stack): + super(CloudLoadBalancer, self).__init__(name, json_snippet, stack) + self.clb = self.cloud_lb() + + def _setup_properties(self, properties, function): + """Use defined schema properties as kwargs for loadbalancer objects.""" + if properties and function: + return [function(**item_dict) for item_dict in properties] + elif function: + return [function()] + + def _alter_properties_for_api(self): + """The following properties have usless key/value pairs which must + be passed into the api. Set them up to make template definition easier. + """ + session_persistence = None + if'sessionPersistence' in self.properties.data: + session_persistence = {'persistenceType': + self.properties['sessionPersistence']} + connection_logging = None + if 'connectionLogging' in self.properties.data: + connection_logging = {'enabled': + self.properties['connectionLogging']} + metadata = None + if 'metadata' in self.properties.data: + metadata = [{'key': k, 'value': v} + for k, v in self.properties['metadata'].iteritems()] + + return (session_persistence, connection_logging, metadata) + + def _check_status(self, loadbalancer, status_list): + """Update the loadbalancer state, check the status.""" + loadbalancer.get() + if loadbalancer.status in status_list: + return True + else: + return False + + def _configure_post_creation(self, loadbalancer): + """Configure all load balancer properties that must be done post + creation. + """ + if self.properties['accessList']: + while not self._check_status(loadbalancer, ['ACTIVE']): + yield + loadbalancer.add_access_list(self.properties['accessList']) + + if self.properties['errorPage']: + while not self._check_status(loadbalancer, ['ACTIVE']): + yield + loadbalancer.set_error_page(self.properties['errorPage']) + + if self.properties['sslTermination']: + while not self._check_status(loadbalancer, ['ACTIVE']): + yield + loadbalancer.add_ssl_termination( + self.properties['sslTermination']['securePort'], + self.properties['sslTermination']['privatekey'], + self.properties['sslTermination']['certificate'], + intermediateCertificate= + self.properties['sslTermination'] + ['intermediateCertificate'], + enabled=self.properties['sslTermination']['enabled'], + secureTrafficOnly=self.properties['sslTermination'] + ['secureTrafficOnly']) + + if 'contentCaching' in self.properties: + enabled = True if self.properties['contentCaching'] == 'ENABLED'\ + else False + while not self._check_status(loadbalancer, ['ACTIVE']): + yield + loadbalancer.content_caching = enabled + + def handle_create(self): + node_list = [] + for node in self.properties['nodes']: + # resolve references to stack resource IP's + if node.get('ref'): + node['address'] = (self.stack + .resource_by_refid(node['ref']) + .FnGetAtt('PublicIp')) + del node['ref'] + node_list.append(node) + + nodes = self._setup_properties(node_list, self.clb.Node) + virtual_ips = self._setup_properties(self.properties.get('virtualIps'), + self.clb.VirtualIP) + + (session_persistence, connection_logging, metadata) = \ + self._alter_properties_for_api() + + lb_body = { + 'port': self.properties['port'], + 'protocol': self.properties['protocol'], + 'nodes': nodes, + 'virtual_ips': virtual_ips, + 'algorithm': self.properties.get('algorithm'), + 'halfClosed': self.properties.get('halfClosed'), + 'connectionThrottle': self.properties.get('connectionThrottle'), + 'metadata': metadata, + 'healthMonitor': self.properties.get('healthMonitor'), + 'sessionPersistence': session_persistence, + 'timeout': self.properties.get('timeout'), + 'connectionLogging': connection_logging, + } + + lb_name = self.properties.get('name') or self.physical_resource_name() + logger.debug('Creating loadbalancer: %s' % {lb_name: lb_body}) + loadbalancer = self.clb.create(lb_name, **lb_body) + self.resource_id_set(str(loadbalancer.id)) + + post_create = scheduler.TaskRunner(self._configure_post_creation, + loadbalancer) + post_create(timeout=600) + return loadbalancer + + def check_create_complete(self, loadbalancer): + return self._check_status(loadbalancer, ['ACTIVE']) + + def handle_update(self, json_snippet, tmpl_diff, prop_diff): + """ + Add and remove nodes specified in the prop_diff. + """ + loadbalancer = self.clb.get(self.resource_id) + if 'nodes' in prop_diff: + current_nodes = loadbalancer.nodes + #Loadbalancers can be uniquely identified by address and port. + #Old is a dict of all nodes the loadbalancer currently knows about. + for node in prop_diff['nodes']: + # resolve references to stack resource IP's + if node.get('ref'): + node['address'] = (self.stack + .resource_by_refid(node['ref']) + .FnGetAtt('PublicIp')) + del node['ref'] + old = dict(("{0.address}{0.port}".format(node), node) + for node in current_nodes) + #New is a dict of the nodes the loadbalancer will know about after + #this update. + new = dict(("%s%s" % (node['address'], node['port']), node) + for node in prop_diff['nodes']) + + old_set = set(old.keys()) + new_set = set(new.keys()) + + deleted = old_set.difference(new_set) + added = new_set.difference(old_set) + updated = new_set.intersection(old_set) + + if len(current_nodes) + len(added) - len(deleted) < 1: + raise ValueError("The loadbalancer:%s requires at least one " + "node." % self.name) + """ + Add loadbalancers in the new map that are not in the old map. + Add before delete to avoid deleting the last node and getting in + an invalid state. + """ + new_nodes = [self.clb.Node(**new[lb_node]) + for lb_node in added] + if new_nodes: + loadbalancer.add_nodes(new_nodes) + + #Delete loadbalancers in the old dict that are not in the new dict. + for node in deleted: + old[node].delete() + + #Update nodes that have been changed + for node in updated: + node_changed = False + for attribute in new[node].keys(): + if new[node][attribute] != getattr(old[node], attribute): + node_changed = True + setattr(old[node], attribute, new[node][attribute]) + if node_changed: + old[node].update() + + def handle_delete(self): + if self.resource_id is None: + return + try: + loadbalancer = self.clb.get(self.resource_id) + except NotFound: + pass + else: + if loadbalancer.status != 'DELETED': + loadbalancer.delete() + self.resource_id_set(None) + + def _remove_none(self, property_dict): + ''' + Remove values that may be initialized to None and would cause problems + during schema validation. + ''' + return dict((key, value) + for (key, value) in property_dict.iteritems() + if value) + + def validate(self): + """ + Validate any of the provided params + """ + res = super(CloudLoadBalancer, self).validate() + if res: + return res + + if self.properties.get('halfClosed'): + if not (self.properties['protocol'] == 'TCP' or + self.properties['protocol'] == 'TCP_CLIENT_FIRST'): + return {'Error': + 'The halfClosed property is only available for the ' + 'TCP or TCP_CLIENT_FIRST protocols'} + + #health_monitor connect and http types require completely different + #schema + if self.properties.get('healthMonitor'): + health_monitor = \ + self._remove_none(self.properties['healthMonitor']) + + if health_monitor['type'] == 'CONNECT': + schema = CloudLoadBalancer.health_monitor_connect_schema + else: + schema = CloudLoadBalancer.health_monitor_http_schema + try: + Properties(schema, + health_monitor, + self.stack.resolve_runtime_data, + self.name).validate() + except exception.StackValidationFailed as svf: + return {'Error': str(svf)} + + if self.properties.get('sslTermination'): + ssl_termination = self._remove_none( + self.properties['sslTermination']) + + if ssl_termination['enabled']: + try: + Properties(CloudLoadBalancer. + ssl_termination_enabled_schema, + ssl_termination, + self.stack.resolve_runtime_data, + self.name).validate() + except exception.StackValidationFailed as svf: + return {'Error': str(svf)} + + def FnGetRefId(self): + return unicode(self.name) + + def _public_ip(self): + #TODO(andrew-plunk) return list here and let caller choose ip + for ip in self.clb.get(self.resource_id).virtual_ips: + if ip.type == 'PUBLIC': + return ip.address + + def _resolve_attribute(self, key): + attribute_function = { + 'PublicIp': self._public_ip() + } + if key not in attribute_function: + raise exception.InvalidTemplateAttribute(resource=self.name, + key=key) + function = attribute_function[key] + logger.info('%s.GetAtt(%s) == %s' % (self.name, key, function)) + return unicode(function) + + +def resource_mapping(): + if rackspace_resource.PYRAX_INSTALLED: + return { + 'Rackspace::Cloud::LoadBalancer': CloudLoadBalancer + } + else: + return {} diff --git a/heat/tests/test_cloud_loadbalancer.py b/heat/tests/test_cloud_loadbalancer.py new file mode 100644 index 0000000000..583221d303 --- /dev/null +++ b/heat/tests/test_cloud_loadbalancer.py @@ -0,0 +1,538 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +import uuid +import json +import copy +import random +import string + +from heat.common import template_format +from heat.engine import scheduler +from heat.engine import resource +from heat.engine.resources.rackspace import cloud_loadbalancer as lb +from heat.tests.common import HeatTestCase +from heat.tests.utils import setup_dummy_db +from heat.tests.utils import parse_stack + +# The following fakes are for pyrax + + +class FakeClient(object): + user_agent = "Fake" + USER_AGENT = "Fake" + + +class FakeManager(object): + api = FakeClient() + + def list(self): + pass + + def get(self, item): + pass + + def delete(self, item): + pass + + def create(self, *args, **kwargs): + pass + + def find(self, *args, **kwargs): + pass + + def action(self, item, action_type, body={}): + pass + + +class FakeLoadBalancerManager(object): + def __init__(self, api=None, *args, **kwargs): + pass + + def set_content_caching(self, *args, **kwargs): + pass + + +class FakeNode(object): + def __init__(self, address="0.0.0.0", port=80, condition=None, weight=None, + status=None, parent=None, type=None, id=None): + self.address = address + self.port = port + self.condition = condition + self.weight = weight + self.status = status + self.parent = parent + self.type = type + self.id = id + + def __eq__(self, other): + return self.__dict__ == other.__dict__ + + def __ne__(self, other): + return not self.__eq__(other) + + +class FakeVirtualIP(object): + def __init__(self, address=None, port=None, condition=None, + ipVersion=None, type=None): + self.address = address + self.port = port + self.condition = condition + self.ipVersion = ipVersion + self.type = type + + def __eq__(self, other): + return self.__dict__ == other.__dict__ + + def __ne__(self, other): + return not self.__eq__(other) + + +class FakeLoadBalancerClient(object): + def __init__(self, *args, **kwargs): + self.Node = FakeNode + self.VirtualIP = FakeVirtualIP + pass + + def get(*args, **kwargs): + pass + + def create(*args, **kwargs): + pass + + +class FakeLoadBalancer(object): + def __init__(self, name=None, info=None, *args, **kwargs): + name = name or uuid.uuid4() + info = info or {"fake": "fake"} + self.id = uuid.uuid4() + self.manager = FakeLoadBalancerManager() + self.Node = FakeNode + self.VirtualIP = FakeVirtualIP + self.nodes = [] + + def get(*args, **kwargs): + pass + + def add_nodes(*args, **kwargs): + pass + + def add_ssl_termination(*args, **kwargs): + pass + + def set_error_page(*args, **kwargs): + pass + + def add_access_list(*args, **kwargs): + pass + + +class LoadBalancerWithFakeClient(lb.CloudLoadBalancer): + def cloud_lb(self): + return FakeLoadBalancerClient() + + +def override_resource(): + return { + 'Rackspace::Cloud::LoadBalancer': LoadBalancerWithFakeClient + } + + +class LoadBalancerTest(HeatTestCase): + + def setUp(self): + super(LoadBalancerTest, self).setUp() + + self.lb_template = { + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "fawef", + "Resources": { + self._get_lb_resource_name(): { + "Type": "Rackspace::Cloud::LoadBalancer", + "Properties": { + "name": "test-clb", + "nodes": [{"address": "166.78.103.141", "port": 80, + "condition": "ENABLED"}], + "protocol": "HTTP", + "port": 80, + "virtualIps": [ + {"type": "PUBLIC", "ipVersion": "IPV6"}], + "algorithm": 'LEAST_CONNECTIONS', + "connectionThrottle": {'maxConnectionRate': 1000}, + 'timeout': 110, + 'contentCaching': 'DISABLED' + } + } + } + } + + self.lb_name = 'test-clb' + self.expected_body = { + "nodes": [FakeNode(address=u"166.78.103.141", port=80, + condition=u"ENABLED")], + "protocol": u'HTTP', + "port": 80, + "virtual_ips": [FakeVirtualIP(type=u"PUBLIC", ipVersion=u"IPV6")], + "halfClosed": None, + "algorithm": u'LEAST_CONNECTIONS', + "connectionThrottle": {'maxConnectionRate': 1000, + 'maxConnections': None, + 'rateInterval': None, + 'minConnections': None}, + "connectionLogging": None, + "halfClosed": None, + "healthMonitor": None, + "metadata": None, + "sessionPersistence": None, + "timeout": 110 + } + + lb.resource_mapping = override_resource + setup_dummy_db() + resource._register_class("Rackspace::Cloud::LoadBalancer", + LoadBalancerWithFakeClient) + + def _get_lb_resource_name(self): + return "lb-" + str(uuid.uuid4()) + + def __getattribute__(self, name): + if name == 'expected_body' or name == 'lb_template': + return copy.deepcopy(super(LoadBalancerTest, self) + .__getattribute__(name)) + return super(LoadBalancerTest, self).__getattribute__(name) + + def _mock_create(self, t, stack, resource_name, lb_name, lb_body): + rsrc = LoadBalancerWithFakeClient(resource_name, + t['Resources'][resource_name], + stack) + self.m.StubOutWithMock(rsrc.clb, 'create') + fake_loadbalancer = FakeLoadBalancer(name=lb_name) + rsrc.clb.create(lb_name, **lb_body).AndReturn(fake_loadbalancer) + return (rsrc, fake_loadbalancer) + + def _get_first_resource_name(self, templ): + return next(k for k in templ['Resources']) + + def _random_name(self): + return ''.join(random.choice(string.ascii_uppercase) + for x in range(10)) + + def _mock_loadbalancer(self, lb_template, expected_name, expected_body): + t = template_format.parse(json.dumps(lb_template)) + s = parse_stack(t, stack_name=self._random_name()) + + rsrc, fake_loadbalancer = self._mock_create(t, s, + self. + _get_first_resource_name( + lb_template), + expected_name, + expected_body) + self.m.StubOutWithMock(fake_loadbalancer, 'get') + fake_loadbalancer.get().MultipleTimes().AndReturn(None) + + fake_loadbalancer.status = 'ACTIVE' + + return (rsrc, fake_loadbalancer) + + def _set_template(self, templ, **kwargs): + for k, v in kwargs.iteritems(): + templ['Resources'][self._get_first_resource_name(templ)][ + 'Properties'][k] = v + return templ + + def _set_expected(self, expected, **kwargs): + for k, v in kwargs.iteritems(): + expected[k] = v + return expected + + def test_alter_properties(self): + #test alter properties functions + template = self._set_template(self.lb_template, + sessionPersistence='HTTP_COOKIE', + connectionLogging=True, + metadata={'yolo': 'heeyyy_gurl'}) + + expected = self._set_expected(self.expected_body, + sessionPersistence= + {'persistenceType': 'HTTP_COOKIE'}, + connectionLogging={'enabled': True}, + metadata=[ + {'key': 'yolo', + 'value': 'heeyyy_gurl'}]) + + rsrc, fake_loadbalancer = self._mock_loadbalancer(template, + self.lb_name, + expected) + + self.m.ReplayAll() + scheduler.TaskRunner(rsrc.create)() + self.m.VerifyAll() + + def test_validate_half_closed(self): + #test failure (invalid protocol) + template = self._set_template(self.lb_template, halfClosed=True) + expected = self._set_expected(self.expected_body, halfClosed=True) + rsrc, fake_loadbalancer = self._mock_loadbalancer(template, + self.lb_name, + expected) + self.assertEquals(rsrc.validate(), { + 'Error': + 'The halfClosed property is only available for the ' + 'TCP or TCP_CLIENT_FIRST protocols'}) + + #test TCP protocol + template = self._set_template(template, protocol='TCP') + expected = self._set_expected(expected, protocol='TCP') + rsrc, fake_loadbalancer = self._mock_loadbalancer(template, + self.lb_name, + expected) + self.assertEquals(rsrc.validate(), None) + + #test TCP_CLIENT_FIRST protocol + template = self._set_template(template, + protocol='TCP_CLIENT_FIRST') + expected = self._set_expected(expected, + protocol='TCP_CLIENT_FIRST') + rsrc, fake_loadbalancer = self._mock_loadbalancer(template, + self.lb_name, + expected) + self.assertEquals(rsrc.validate(), None) + + def test_validate_health_monitor(self): + #test connect success + health_monitor = { + 'type': 'CONNECT', + 'attemptsBeforeDeactivation': 1, + 'delay': 1, + 'timeout': 1 + } + template = self._set_template(self.lb_template, + healthMonitor=health_monitor) + expected = self._set_expected(self.expected_body, + healthMonitor=health_monitor) + rsrc, fake_loadbalancer = self._mock_loadbalancer(template, + self.lb_name, + expected) + + self.assertEquals(rsrc.validate(), None) + + #test connect failure + #bodyRegex is only valid for type 'HTTP(S)' + health_monitor['bodyRegex'] = 'dfawefawe' + template = self._set_template(template, + healthMonitor=health_monitor) + expected = self._set_expected(expected, + healthMonitor=health_monitor) + rsrc, fake_loadbalancer = self._mock_loadbalancer(template, + self.lb_name, + expected) + self.assertEquals(rsrc.validate(), + {'Error': 'Unknown Property bodyRegex'}) + + #test http fields + health_monitor['type'] = 'HTTP' + health_monitor['bodyRegex'] = 'bodyRegex' + health_monitor['statusRegex'] = 'statusRegex' + health_monitor['hostHeader'] = 'hostHeader' + health_monitor['path'] = 'path' + + template = self._set_template(template, + healthMonitor=health_monitor) + expected = self._set_expected(expected, + healthMonitor=health_monitor) + rsrc, fake_loadbalancer = self._mock_loadbalancer(template, + self.lb_name, + expected) + self.assertEquals(rsrc.validate(), None) + + def test_validate_ssl_termination(self): + ssl_termination = { + 'enabled': True, + 'privatekey': 'ewfawe', + 'certificate': 'dfaewfwef', + 'intermediateCertificate': 'fwaefawe', + 'secureTrafficOnly': True + } + + #test ssl termination enabled without required fields failure + template = self._set_template(self.lb_template, + sslTermination=ssl_termination) + expected = self._set_expected(self.expected_body, + sslTermination=ssl_termination) + rsrc, fake_loadbalancer = self._mock_loadbalancer(template, + self.lb_name, + expected) + self.assertEquals(rsrc.validate(), + {'Error': + 'Property error : %s: Property securePort not ' + 'assigned' % rsrc.name}) + + ssl_termination['securePort'] = 443 + template = self._set_template(template, + sslTermination=ssl_termination) + expected = self._set_expected(expected, + sslTermination=ssl_termination) + rsrc, fake_loadbalancer = self._mock_loadbalancer(template, + self.lb_name, + expected) + self.assertEquals(rsrc.validate(), None) + + def test_post_creation_access_list(self): + access_list = [{"address": '192.168.1.1/0', + 'type': 'ALLOW'}, + {'address': '172.165.3.43', + 'type': 'DENY'}] + + template = self._set_template(self.lb_template, + accessList=access_list) + rsrc, fake_loadbalancer = self._mock_loadbalancer(template, + self.lb_name, + self.expected_body) + self.m.StubOutWithMock(fake_loadbalancer, 'add_access_list') + fake_loadbalancer.add_access_list(access_list) + + self.m.ReplayAll() + scheduler.TaskRunner(rsrc.create)() + self.m.VerifyAll() + + def test_post_creation_error_page(self): + error_page = "REALLY BIG ERROR" + + template = self._set_template(self.lb_template, + errorPage=error_page) + rsrc, fake_loadbalancer = self._mock_loadbalancer(template, + self.lb_name, + self.expected_body) + self.m.StubOutWithMock(fake_loadbalancer, 'set_error_page') + fake_loadbalancer.set_error_page(error_page) + + self.m.ReplayAll() + scheduler.TaskRunner(rsrc.create)() + self.m.VerifyAll() + + def test_post_creation_ssl_termination(self): + ssl_termination = { + 'securePort': 443, + 'privatekey': 'afwefawe', + 'certificate': 'fawefwea', + 'intermediateCertificate': "intermediate_certificate", + 'enabled': True, + 'secureTrafficOnly': False + } + + template = self._set_template(self.lb_template, + sslTermination=ssl_termination) + rsrc, fake_loadbalancer = self._mock_loadbalancer(template, + self.lb_name, + self.expected_body) + self.m.StubOutWithMock(fake_loadbalancer, 'add_ssl_termination') + fake_loadbalancer.add_ssl_termination( + ssl_termination['securePort'], + ssl_termination['privatekey'], + ssl_termination['certificate'], + intermediateCertificate=ssl_termination['intermediateCertificate'], + enabled=ssl_termination['enabled'], + secureTrafficOnly=ssl_termination['secureTrafficOnly']) + + self.m.ReplayAll() + scheduler.TaskRunner(rsrc.create)() + self.m.VerifyAll() + + def test_post_creation_content_caching(self): + template = self._set_template(self.lb_template, + contentCaching='ENABLED') + rsrc, fake_loadbalancer = self._mock_loadbalancer(template, + self.lb_name, + self.expected_body) + self.m.ReplayAll() + scheduler.TaskRunner(rsrc.create)() + self.m.VerifyAll() + + def test_update_add_node_by_ref(self): + added_node = {'nodes': [ + {"address": "166.78.103.141", "port": 80, "condition": "ENABLED"}, + {"ref": "TEST_NODE_REF", "port": 80, "condition": "ENABLED"}]} + expected_ip = '172.168.1.4' + rsrc, fake_loadbalancer = self._mock_loadbalancer(self.lb_template, + self.lb_name, + self.expected_body) + fake_loadbalancer.nodes = self.expected_body['nodes'] + self.m.ReplayAll() + scheduler.TaskRunner(rsrc.create)() + self.m.VerifyAll() + + self.m.StubOutWithMock(rsrc.clb, 'get') + rsrc.clb.get(rsrc.resource_id).AndReturn(fake_loadbalancer) + + self.m.StubOutWithMock(rsrc.stack, 'resource_by_refid') + + class FakeFn(object): + def FnGetAtt(self, attr): + return expected_ip + + rsrc.stack.resource_by_refid('TEST_NODE_REF').AndReturn(FakeFn()) + + self.m.StubOutWithMock(fake_loadbalancer, 'add_nodes') + fake_loadbalancer.add_nodes([ + fake_loadbalancer.Node(address=expected_ip, + port=80, + condition='ENABLED')]) + + self.m.ReplayAll() + rsrc.handle_update({}, {}, added_node) + self.m.VerifyAll() + + def test_update_add_node_by_address(self): + expected_ip = '172.168.1.4' + added_node = {'nodes': [ + {"address": "166.78.103.141", "port": 80, "condition": "ENABLED"}, + {"address": expected_ip, "port": 80, "condition": "ENABLED"}]} + rsrc, fake_loadbalancer = self._mock_loadbalancer(self.lb_template, + self.lb_name, + self.expected_body) + fake_loadbalancer.nodes = self.expected_body['nodes'] + self.m.ReplayAll() + scheduler.TaskRunner(rsrc.create)() + self.m.VerifyAll() + + self.m.StubOutWithMock(rsrc.clb, 'get') + rsrc.clb.get(rsrc.resource_id).AndReturn(fake_loadbalancer) + + self.m.StubOutWithMock(fake_loadbalancer, 'add_nodes') + fake_loadbalancer.add_nodes([ + fake_loadbalancer.Node(address=expected_ip, + port=80, + condition='ENABLED')]) + + self.m.ReplayAll() + rsrc.handle_update({}, {}, added_node) + self.m.VerifyAll() + + def test_update_delete_node_failed(self): + deleted_node = {'nodes': []} + rsrc, fake_loadbalancer = self._mock_loadbalancer(self.lb_template, + self.lb_name, + self.expected_body) + fake_loadbalancer.nodes = self.expected_body['nodes'] + self.m.ReplayAll() + scheduler.TaskRunner(rsrc.create)() + self.m.VerifyAll() + + self.m.StubOutWithMock(rsrc.clb, 'get') + rsrc.clb.get(rsrc.resource_id).AndReturn(fake_loadbalancer) + + self.m.ReplayAll() + self.assertRaises(ValueError, rsrc.handle_update, {}, {}, deleted_node) + self.m.VerifyAll()