summaryrefslogtreecommitdiff
path: root/contrib
diff options
context:
space:
mode:
authorJenkins <jenkins@review.openstack.org>2016-01-27 22:24:46 +0000
committerGerrit Code Review <review@openstack.org>2016-01-27 22:24:46 +0000
commitc24dd96ade0dc437452626395278187b5fcdbe68 (patch)
treecade3bd44e624e623aa93766ea7d391f69c87a9c /contrib
parent4e7a584e5c978d88b1ecc90e8253b5d85d5d335e (diff)
parente94c70ced32d854dc0a5b8e11957376a1b6b982a (diff)
downloadheat-c24dd96ade0dc437452626395278187b5fcdbe68.tar.gz
Merge "Add Rackspace::Cloud::LBNode"
Diffstat (limited to 'contrib')
-rw-r--r--contrib/rackspace/rackspace/resources/lb_node.py230
-rw-r--r--contrib/rackspace/rackspace/tests/test_lb_node.py305
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)