diff options
author | Jenkins <jenkins@review.openstack.org> | 2016-01-27 22:24:46 +0000 |
---|---|---|
committer | Gerrit Code Review <review@openstack.org> | 2016-01-27 22:24:46 +0000 |
commit | c24dd96ade0dc437452626395278187b5fcdbe68 (patch) | |
tree | cade3bd44e624e623aa93766ea7d391f69c87a9c /contrib | |
parent | 4e7a584e5c978d88b1ecc90e8253b5d85d5d335e (diff) | |
parent | e94c70ced32d854dc0a5b8e11957376a1b6b982a (diff) | |
download | heat-c24dd96ade0dc437452626395278187b5fcdbe68.tar.gz |
Merge "Add Rackspace::Cloud::LBNode"
Diffstat (limited to 'contrib')
-rw-r--r-- | contrib/rackspace/rackspace/resources/lb_node.py | 230 | ||||
-rw-r--r-- | contrib/rackspace/rackspace/tests/test_lb_node.py | 305 |
2 files changed, 535 insertions, 0 deletions
diff --git a/contrib/rackspace/rackspace/resources/lb_node.py b/contrib/rackspace/rackspace/resources/lb_node.py new file mode 100644 index 000000000..d25f1febb --- /dev/null +++ b/contrib/rackspace/rackspace/resources/lb_node.py @@ -0,0 +1,230 @@ +# +# 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 datetime + +from oslo_utils import timeutils +import six + +from heat.common import exception +from heat.common.i18n import _ +from heat.engine import constraints +from heat.engine import properties +from heat.engine import resource + +try: + from pyrax.exceptions import NotFound # noqa + PYRAX_INSTALLED = True +except ImportError: + # Setup fake exception for testing without pyrax + class NotFound(Exception): + pass + PYRAX_INSTALLED = False + + +def lb_immutable(exc): + return 'immutable' in six.text_type(exc) + + +class LoadbalancerDeleted(exception.HeatException): + msg_fmt = _("The Load Balancer (ID %(lb_id)s) has been deleted.") + + +class NodeNotFound(exception.HeatException): + msg_fmt = _("Node (ID %(node_id)s) not found on Load Balancer " + "(ID %(lb_id)s).") + + +class LBNode(resource.Resource): + """Represents a single node of a Rackspace Cloud Load Balancer""" + + default_client_name = 'cloud_lb' + + _CONDITIONS = ( + ENABLED, DISABLED, DRAINING, + ) = ( + 'ENABLED', 'DISABLED', 'DRAINING', + ) + + _NODE_KEYS = ( + ADDRESS, PORT, CONDITION, TYPE, WEIGHT + ) = ( + 'address', 'port', 'condition', 'type', 'weight' + ) + + _OTHER_KEYS = ( + LOAD_BALANCER, DRAINING_TIMEOUT + ) = ( + 'load_balancer', 'draining_timeout' + ) + + PROPERTIES = _NODE_KEYS + _OTHER_KEYS + + properties_schema = { + LOAD_BALANCER: properties.Schema( + properties.Schema.STRING, + _("The ID of the load balancer to associate the node with."), + required=True + ), + DRAINING_TIMEOUT: properties.Schema( + properties.Schema.INTEGER, + _("The time to wait, in seconds, for the node to drain before it " + "is deleted."), + default=0, + constraints=[ + constraints.Range(min=0) + ], + update_allowed=True + ), + ADDRESS: properties.Schema( + properties.Schema.STRING, + _("IP address for the node."), + required=True + ), + PORT: properties.Schema( + properties.Schema.INTEGER, + required=True + ), + CONDITION: properties.Schema( + properties.Schema.STRING, + default=ENABLED, + constraints=[ + constraints.AllowedValues(_CONDITIONS), + ], + update_allowed=True + ), + TYPE: properties.Schema( + properties.Schema.STRING, + constraints=[ + constraints.AllowedValues(['PRIMARY', + 'SECONDARY']), + ], + update_allowed=True + ), + WEIGHT: properties.Schema( + properties.Schema.NUMBER, + constraints=[ + constraints.Range(1, 100), + ], + update_allowed=True + ), + } + + def lb(self): + lb_id = self.properties.get(self.LOAD_BALANCER) + lb = self.client().get(lb_id) + + if lb.status in ('DELETED', 'PENDING_DELETE'): + raise LoadbalancerDeleted(lb_id=lb.id) + + return lb + + def node(self, lb): + for node in getattr(lb, 'nodes', []): + if node.id == self.resource_id: + return node + raise NodeNotFound(node_id=self.resource_id, lb_id=lb.id) + + def handle_create(self): + pass + + def check_create_complete(self, *args): + node_args = {k: self.properties.get(k) for k in self._NODE_KEYS} + node = self.client().Node(**node_args) + + try: + resp, body = self.lb().add_nodes([node]) + except Exception as exc: + if lb_immutable(exc): + return False + raise + + new_node = body['nodes'][0] + node_id = new_node['id'] + + self.resource_id_set(node_id) + return True + + def handle_update(self, json_snippet, tmpl_diff, prop_diff): + return prop_diff + + def check_update_complete(self, prop_diff): + node = self.node(self.lb()) + is_complete = True + + for key in self._NODE_KEYS: + if key in prop_diff and getattr(node, key, None) != prop_diff[key]: + setattr(node, key, prop_diff[key]) + is_complete = False + + if is_complete: + return True + + try: + node.update() + except Exception as exc: + if lb_immutable(exc): + return False + raise + + return False + + def handle_delete(self): + return timeutils.utcnow() + + def check_delete_complete(self, deleted_at): + if self.resource_id is None: + return True + + try: + node = self.node(self.lb()) + except (NotFound, LoadbalancerDeleted, NodeNotFound): + return True + + if isinstance(deleted_at, six.string_types): + deleted_at = timeutils.parse_isotime(deleted_at) + + deleted_at = timeutils.normalize_time(deleted_at) + waited = timeutils.utcnow() - deleted_at + timeout_secs = self.properties[self.DRAINING_TIMEOUT] + timeout_secs = datetime.timedelta(seconds=timeout_secs) + + if waited > timeout_secs: + try: + node.delete() + except NotFound: + return True + except Exception as exc: + if lb_immutable(exc): + return False + raise + elif node.condition != self.DRAINING: + node.condition = self.DRAINING + try: + node.update() + except Exception as exc: + if lb_immutable(exc): + return False + raise + + return False + + +def resource_mapping(): + return {'Rackspace::Cloud::LBNode': LBNode} + + +def available_resource_mapping(): + if PYRAX_INSTALLED: + return resource_mapping() + return {} diff --git a/contrib/rackspace/rackspace/tests/test_lb_node.py b/contrib/rackspace/rackspace/tests/test_lb_node.py new file mode 100644 index 000000000..e42135576 --- /dev/null +++ b/contrib/rackspace/rackspace/tests/test_lb_node.py @@ -0,0 +1,305 @@ +# +# 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 datetime + +import mock + +from heat.engine import rsrc_defn +from heat.tests import common + +from ..resources import lb_node # noqa +from ..resources.lb_node import ( # noqa + LoadbalancerDeleted, + NotFound, + NodeNotFound) + +from .test_cloud_loadbalancer import FakeNode # noqa + + +class LBNode(lb_node.LBNode): + @classmethod + def is_service_available(cls, context): + return True + + +class LBNodeTest(common.HeatTestCase): + def setUp(self): + super(LBNodeTest, self).setUp() + self.mockstack = mock.Mock() + self.mockstack.has_cache_data.return_value = False + self.mockstack.db_resource_get.return_value = None + self.mockclient = mock.Mock() + self.mockstack.clients.client.return_value = self.mockclient + + self.def_props = { + LBNode.LOAD_BALANCER: 'some_lb_id', + LBNode.DRAINING_TIMEOUT: 60, + LBNode.ADDRESS: 'some_ip', + LBNode.PORT: 80, + LBNode.CONDITION: 'ENABLED', + LBNode.TYPE: 'PRIMARY', + LBNode.WEIGHT: None, + } + self.resource_def = rsrc_defn.ResourceDefinition( + "test", LBNode, properties=self.def_props) + + self.resource = LBNode("test", self.resource_def, self.mockstack) + self.resource.resource_id = 12345 + + def test_create(self): + self.resource.resource_id = None + + fake_lb = mock.Mock() + fake_lb.add_nodes.return_value = (None, {'nodes': [{'id': 12345}]}) + self.mockclient.get.return_value = fake_lb + + fake_node = mock.Mock() + self.mockclient.Node.return_value = fake_node + + self.resource.check_create_complete() + + self.mockclient.get.assert_called_once_with('some_lb_id') + self.mockclient.Node.assert_called_once_with( + address='some_ip', port=80, condition='ENABLED', + type='PRIMARY', weight=0) + fake_lb.add_nodes.assert_called_once_with([fake_node]) + self.assertEqual(self.resource.resource_id, 12345) + + def test_create_lb_not_found(self): + self.mockclient.get.side_effect = NotFound() + self.assertRaises(NotFound, self.resource.check_create_complete) + + def test_create_lb_deleted(self): + fake_lb = mock.Mock() + fake_lb.id = 1111 + fake_lb.status = 'DELETED' + self.mockclient.get.return_value = fake_lb + + exc = self.assertRaises(LoadbalancerDeleted, + self.resource.check_create_complete) + self.assertEqual("The Load Balancer (ID 1111) has been deleted.", + str(exc)) + + def test_create_lb_pending_delete(self): + fake_lb = mock.Mock() + fake_lb.id = 1111 + fake_lb.status = 'PENDING_DELETE' + self.mockclient.get.return_value = fake_lb + + exc = self.assertRaises(LoadbalancerDeleted, + self.resource.check_create_complete) + self.assertEqual("The Load Balancer (ID 1111) has been deleted.", + str(exc)) + + def test_handle_update_method(self): + self.assertEqual(self.resource.handle_update(None, None, 'foo'), 'foo') + + def _test_update(self, diff): + fake_lb = mock.Mock() + fake_node = FakeNode(id=12345, address='a', port='b') + fake_node.update = mock.Mock() + expected_node = FakeNode(id=12345, address='a', port='b', **diff) + expected_node.update = fake_node.update + fake_lb.nodes = [fake_node] + self.mockclient.get.return_value = fake_lb + + self.assertFalse(self.resource.check_update_complete(prop_diff=diff)) + + self.mockclient.get.assert_called_once_with('some_lb_id') + fake_node.update.assert_called_once_with() + self.assertEqual(fake_node, expected_node) + + def test_update_condition(self): + self._test_update({'condition': 'DISABLED'}) + + def test_update_weight(self): + self._test_update({'weight': 100}) + + def test_update_type(self): + self._test_update({'type': 'SECONDARY'}) + + def test_update_multiple(self): + self._test_update({'condition': 'DISABLED', + 'weight': 100, + 'type': 'SECONDARY'}) + + def test_update_finished(self): + fake_lb = mock.Mock() + fake_node = FakeNode(id=12345, address='a', port='b', + condition='ENABLED') + fake_node.update = mock.Mock() + expected_node = FakeNode(id=12345, address='a', port='b', + condition='ENABLED') + expected_node.update = fake_node.update + fake_lb.nodes = [fake_node] + self.mockclient.get.return_value = fake_lb + + diff = {'condition': 'ENABLED'} + self.assertTrue(self.resource.check_update_complete(prop_diff=diff)) + + self.mockclient.get.assert_called_once_with('some_lb_id') + self.assertFalse(fake_node.update.called) + self.assertEqual(fake_node, expected_node) + + def test_update_lb_not_found(self): + self.mockclient.get.side_effect = NotFound() + + diff = {'condition': 'ENABLED'} + self.assertRaises(NotFound, self.resource.check_update_complete, + prop_diff=diff) + + def test_update_lb_deleted(self): + fake_lb = mock.Mock() + fake_lb.id = 1111 + fake_lb.status = 'DELETED' + self.mockclient.get.return_value = fake_lb + + diff = {'condition': 'ENABLED'} + exc = self.assertRaises(LoadbalancerDeleted, + self.resource.check_update_complete, + prop_diff=diff) + self.assertEqual("The Load Balancer (ID 1111) has been deleted.", + str(exc)) + + def test_update_lb_pending_delete(self): + fake_lb = mock.Mock() + fake_lb.id = 1111 + fake_lb.status = 'PENDING_DELETE' + self.mockclient.get.return_value = fake_lb + + diff = {'condition': 'ENABLED'} + exc = self.assertRaises(LoadbalancerDeleted, + self.resource.check_update_complete, + prop_diff=diff) + self.assertEqual("The Load Balancer (ID 1111) has been deleted.", + str(exc)) + + def test_update_node_not_found(self): + fake_lb = mock.Mock() + fake_lb.id = 4444 + fake_lb.nodes = [] + self.mockclient.get.return_value = fake_lb + + diff = {'condition': 'ENABLED'} + exc = self.assertRaises(NodeNotFound, + self.resource.check_update_complete, + prop_diff=diff) + self.assertEqual( + "Node (ID 12345) not found on Load Balancer (ID 4444).", str(exc)) + + def test_delete_no_id(self): + self.resource.resource_id = None + self.assertTrue(self.resource.check_delete_complete(None)) + + def test_delete_lb_already_deleted(self): + self.mockclient.get.side_effect = NotFound() + self.assertTrue(self.resource.check_delete_complete(None)) + self.mockclient.get.assert_called_once_with('some_lb_id') + + def test_delete_lb_deleted_status(self): + fake_lb = mock.Mock() + fake_lb.status = 'DELETED' + self.mockclient.get.return_value = fake_lb + + self.assertTrue(self.resource.check_delete_complete(None)) + self.mockclient.get.assert_called_once_with('some_lb_id') + + def test_delete_lb_pending_delete_status(self): + fake_lb = mock.Mock() + fake_lb.status = 'PENDING_DELETE' + self.mockclient.get.return_value = fake_lb + + self.assertTrue(self.resource.check_delete_complete(None)) + self.mockclient.get.assert_called_once_with('some_lb_id') + + def test_delete_node_already_deleted(self): + fake_lb = mock.Mock() + fake_lb.nodes = [] + self.mockclient.get.return_value = fake_lb + + self.assertTrue(self.resource.check_delete_complete(None)) + self.mockclient.get.assert_called_once_with('some_lb_id') + + @mock.patch.object(lb_node.timeutils, 'utcnow') + def test_drain_before_delete(self, mock_utcnow): + fake_lb = mock.Mock() + fake_node = FakeNode(id=12345, address='a', port='b') + expected_node = FakeNode(id=12345, address='a', port='b', + condition='DRAINING') + fake_node.update = mock.Mock() + expected_node.update = fake_node.update + fake_node.delete = mock.Mock() + expected_node.delete = fake_node.delete + fake_lb.nodes = [fake_node] + self.mockclient.get.return_value = fake_lb + + now = datetime.datetime.utcnow() + mock_utcnow.return_value = now + + self.assertFalse(self.resource.check_delete_complete(now)) + + self.mockclient.get.assert_called_once_with('some_lb_id') + fake_node.update.assert_called_once_with() + self.assertFalse(fake_node.delete.called) + self.assertEqual(fake_node, expected_node) + + @mock.patch.object(lb_node.timeutils, 'utcnow') + def test_delete_waiting(self, mock_utcnow): + fake_lb = mock.Mock() + fake_node = FakeNode(id=12345, address='a', port='b', + condition='DRAINING') + expected_node = FakeNode(id=12345, address='a', port='b', + condition='DRAINING') + fake_node.update = mock.Mock() + expected_node.update = fake_node.update + fake_node.delete = mock.Mock() + expected_node.delete = fake_node.delete + fake_lb.nodes = [fake_node] + self.mockclient.get.return_value = fake_lb + + now = datetime.datetime.utcnow() + now_plus_30 = now + datetime.timedelta(seconds=30) + mock_utcnow.return_value = now_plus_30 + + self.assertFalse(self.resource.check_delete_complete(now)) + + self.mockclient.get.assert_called_once_with('some_lb_id') + self.assertFalse(fake_node.update.called) + self.assertFalse(fake_node.delete.called) + self.assertEqual(fake_node, expected_node) + + @mock.patch.object(lb_node.timeutils, 'utcnow') + def test_delete_finishing(self, mock_utcnow): + fake_lb = mock.Mock() + fake_node = FakeNode(id=12345, address='a', port='b', + condition='DRAINING') + expected_node = FakeNode(id=12345, address='a', port='b', + condition='DRAINING') + fake_node.update = mock.Mock() + expected_node.update = fake_node.update + fake_node.delete = mock.Mock() + expected_node.delete = fake_node.delete + fake_lb.nodes = [fake_node] + self.mockclient.get.return_value = fake_lb + + now = datetime.datetime.utcnow() + now_plus_62 = now + datetime.timedelta(seconds=62) + mock_utcnow.return_value = now_plus_62 + + self.assertFalse(self.resource.check_delete_complete(now)) + + self.mockclient.get.assert_called_once_with('some_lb_id') + self.assertFalse(fake_node.update.called) + self.assertTrue(fake_node.delete.called) + self.assertEqual(fake_node, expected_node) |