Merge "Add Rackspace::Cloud::LBNode"
This commit is contained in:
commit
c24dd96ade
|
@ -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 {}
|
|
@ -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)
|
Loading…
Reference in New Issue