Merge "Add Rackspace::Cloud::LBNode"

This commit is contained in:
Jenkins 2016-01-27 22:24:46 +00:00 committed by Gerrit Code Review
commit c24dd96ade
2 changed files with 535 additions and 0 deletions

View File

@ -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 {}

View File

@ -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)