Add traits field to node notifications
Adds a traits field to node notifications, and triggers notifications when node traits are added or removed. Node traits are emitted in notifications as a list of trait name strings. Bumps the following notification payload versions: NodePayload: 1.6 NodeSetPowerStatePayload: 1.6 NodeCorrectedPowerStatePayload: 1.6 NodeSetProvisionStatePayload: 1.6 NodeCRUDPayload: 1.4 Change-Id: I4e0333173250a641b317d466e52742cf7728ed90 Partial-Bug: #1722194
This commit is contained in:
parent
180234b445
commit
c9677cd43b
@ -726,10 +726,32 @@ class Traits(base.APIBase):
|
||||
return cls(traits=traits)
|
||||
|
||||
|
||||
def _get_trait_names(traits):
|
||||
if not traits:
|
||||
return []
|
||||
return [t.trait for t in traits]
|
||||
def _get_chassis_uuid(node):
|
||||
"""Return the UUID of a node's chassis, or None.
|
||||
|
||||
:param node: a Node object.
|
||||
:returns: the UUID of the node's chassis, or None if the node has no
|
||||
chassis set.
|
||||
"""
|
||||
if not node.chassis_id:
|
||||
return
|
||||
chassis = objects.Chassis.get_by_id(pecan.request.context, node.chassis_id)
|
||||
return chassis.uuid
|
||||
|
||||
|
||||
def _make_trait_list(context, node_id, traits):
|
||||
"""Return a TraitList object for the specified node and traits.
|
||||
|
||||
The Trait objects will not be created in the database.
|
||||
|
||||
:param context: a request context.
|
||||
:param node_id: the ID of a node.
|
||||
:param traits: a list of trait strings to add to the TraitList.
|
||||
:returns: a TraitList object.
|
||||
"""
|
||||
trait_objs = [objects.Trait(context, node_id=node_id, trait=t)
|
||||
for t in traits]
|
||||
return objects.TraitList(context, objects=trait_objs)
|
||||
|
||||
|
||||
class NodeTraitsController(rest.RestController):
|
||||
@ -747,7 +769,7 @@ class NodeTraitsController(rest.RestController):
|
||||
node = api_utils.get_rpc_node(self.node_ident)
|
||||
traits = objects.TraitList.get_by_node_id(pecan.request.context,
|
||||
node.id)
|
||||
return Traits(traits=_get_trait_names(traits))
|
||||
return Traits(traits=traits.get_trait_names())
|
||||
|
||||
@METRICS.timer('NodeTraitsController.put')
|
||||
@expose.expose(None, wtypes.text, wtypes.ArrayType(str),
|
||||
@ -761,7 +783,8 @@ class NodeTraitsController(rest.RestController):
|
||||
Mutually exclusive with 'trait'. If not None, replaces the node's
|
||||
traits with this list.
|
||||
"""
|
||||
cdict = pecan.request.context.to_policy_values()
|
||||
context = pecan.request.context
|
||||
cdict = context.to_policy_values()
|
||||
policy.authorize('baremetal:node:traits:set', cdict, cdict)
|
||||
node = api_utils.get_rpc_node(self.node_ident)
|
||||
|
||||
@ -781,16 +804,27 @@ class NodeTraitsController(rest.RestController):
|
||||
raise exception.Invalid(msg)
|
||||
traits = [trait]
|
||||
replace = False
|
||||
new_traits = {t.trait for t in node.traits} | {trait}
|
||||
else:
|
||||
replace = True
|
||||
new_traits = set(traits)
|
||||
|
||||
for trait in traits:
|
||||
api_utils.validate_trait(trait)
|
||||
|
||||
topic = pecan.request.rpcapi.get_topic_for(node)
|
||||
pecan.request.rpcapi.add_node_traits(
|
||||
pecan.request.context, node.id, traits, replace=replace,
|
||||
topic=topic)
|
||||
# Update the node's traits to reflect the desired state.
|
||||
node.traits = _make_trait_list(context, node.id, sorted(new_traits))
|
||||
node.obj_reset_changes()
|
||||
chassis_uuid = _get_chassis_uuid(node)
|
||||
notify.emit_start_notification(context, node, 'update',
|
||||
chassis_uuid=chassis_uuid)
|
||||
with notify.handle_error_notification(context, node, 'update',
|
||||
chassis_uuid=chassis_uuid):
|
||||
topic = pecan.request.rpcapi.get_topic_for(node)
|
||||
pecan.request.rpcapi.add_node_traits(
|
||||
context, node.id, traits, replace=replace, topic=topic)
|
||||
notify.emit_end_notification(context, node, 'update',
|
||||
chassis_uuid=chassis_uuid)
|
||||
|
||||
@METRICS.timer('NodeTraitsController.delete')
|
||||
@expose.expose(None, wtypes.text,
|
||||
@ -801,18 +835,31 @@ class NodeTraitsController(rest.RestController):
|
||||
:param trait: String value; trait to remove from a node, or None. If
|
||||
None, all traits are removed.
|
||||
"""
|
||||
cdict = pecan.request.context.to_policy_values()
|
||||
context = pecan.request.context
|
||||
cdict = context.to_policy_values()
|
||||
policy.authorize('baremetal:node:traits:delete', cdict, cdict)
|
||||
node = api_utils.get_rpc_node(self.node_ident)
|
||||
|
||||
if trait:
|
||||
traits = [trait]
|
||||
new_traits = {t.trait for t in node.traits} - {trait}
|
||||
else:
|
||||
traits = None
|
||||
new_traits = set()
|
||||
|
||||
topic = pecan.request.rpcapi.get_topic_for(node)
|
||||
pecan.request.rpcapi.remove_node_traits(
|
||||
pecan.request.context, node.id, traits, topic=topic)
|
||||
# Update the node's traits to reflect the desired state.
|
||||
node.traits = _make_trait_list(context, node.id, sorted(new_traits))
|
||||
node.obj_reset_changes()
|
||||
chassis_uuid = _get_chassis_uuid(node)
|
||||
notify.emit_start_notification(context, node, 'update',
|
||||
chassis_uuid=chassis_uuid)
|
||||
with notify.handle_error_notification(context, node, 'update',
|
||||
chassis_uuid=chassis_uuid):
|
||||
topic = pecan.request.rpcapi.get_topic_for(node)
|
||||
pecan.request.rpcapi.remove_node_traits(
|
||||
context, node.id, traits, topic=topic)
|
||||
notify.emit_end_notification(context, node, 'update',
|
||||
chassis_uuid=chassis_uuid)
|
||||
|
||||
|
||||
class Node(base.APIBase):
|
||||
@ -998,8 +1045,8 @@ class Node(base.APIBase):
|
||||
if hasattr(self, k):
|
||||
self.fields.append(k)
|
||||
# TODO(jroll) is there a less hacky way to do this?
|
||||
if k == 'traits' and 'traits' in kwargs:
|
||||
value = _get_trait_names(kwargs['traits'])
|
||||
if k == 'traits' and kwargs.get('traits') is not None:
|
||||
value = kwargs['traits'].get_trait_names()
|
||||
else:
|
||||
value = kwargs.get(k, wtypes.Unset)
|
||||
setattr(self, k, value)
|
||||
@ -1937,10 +1984,7 @@ class NodesController(rest.RestController):
|
||||
raise exception.OperationNotPermitted()
|
||||
|
||||
rpc_node = api_utils.get_rpc_node(node_ident)
|
||||
chassis_uuid = None
|
||||
if rpc_node.chassis_id:
|
||||
chassis_uuid = objects.Chassis.get_by_id(context,
|
||||
rpc_node.chassis_id).uuid
|
||||
chassis_uuid = _get_chassis_uuid(rpc_node)
|
||||
notify.emit_start_notification(context, rpc_node, 'delete',
|
||||
chassis_uuid=chassis_uuid)
|
||||
with notify.handle_error_notification(context, rpc_node, 'delete',
|
||||
|
@ -548,8 +548,6 @@ class NodePayload(notification.NotificationPayloadBase):
|
||||
'uuid': ('node', 'uuid')
|
||||
}
|
||||
|
||||
# TODO(mgoddard): Add a traits field to the NodePayload object.
|
||||
|
||||
# Version 1.0: Initial version, based off of Node version 1.18.
|
||||
# Version 1.1: Type of network_interface changed to just nullable string
|
||||
# similar to version 1.20 of Node.
|
||||
@ -557,7 +555,8 @@ class NodePayload(notification.NotificationPayloadBase):
|
||||
# Version 1.3: Add dynamic interfaces fields exposed via API.
|
||||
# Version 1.4: Add storage interface field exposed via API.
|
||||
# Version 1.5: Add rescue interface field exposed via API.
|
||||
VERSION = '1.5'
|
||||
# Version 1.6: Add traits field exposed via API.
|
||||
VERSION = '1.6'
|
||||
fields = {
|
||||
'clean_step': object_fields.FlexibleDictField(nullable=True),
|
||||
'console_enabled': object_fields.BooleanField(nullable=True),
|
||||
@ -589,6 +588,7 @@ class NodePayload(notification.NotificationPayloadBase):
|
||||
'resource_class': object_fields.StringField(nullable=True),
|
||||
'target_power_state': object_fields.StringField(nullable=True),
|
||||
'target_provision_state': object_fields.StringField(nullable=True),
|
||||
'traits': object_fields.ListOfStringsField(nullable=True),
|
||||
'updated_at': object_fields.DateTimeField(nullable=True),
|
||||
'uuid': object_fields.UUIDField()
|
||||
}
|
||||
@ -596,6 +596,12 @@ class NodePayload(notification.NotificationPayloadBase):
|
||||
def __init__(self, node, **kwargs):
|
||||
super(NodePayload, self).__init__(**kwargs)
|
||||
self.populate_schema(node=node)
|
||||
# NOTE(mgoddard): Populate traits with a list of trait names, rather
|
||||
# than the TraitList object.
|
||||
if node.obj_attr_is_set('traits') and node.traits is not None:
|
||||
self.traits = node.traits.get_trait_names()
|
||||
else:
|
||||
self.traits = []
|
||||
|
||||
|
||||
@base.IronicObjectRegistry.register
|
||||
@ -618,7 +624,8 @@ class NodeSetPowerStatePayload(NodePayload):
|
||||
# Version 1.3: Parent NodePayload version 1.3
|
||||
# Version 1.4: Parent NodePayload version 1.4
|
||||
# Version 1.5: Parent NodePayload version 1.5
|
||||
VERSION = '1.5'
|
||||
# Version 1.6: Parent NodePayload version 1.6
|
||||
VERSION = '1.6'
|
||||
|
||||
fields = {
|
||||
# "to_power" indicates the future target_power_state of the node. A
|
||||
@ -664,7 +671,8 @@ class NodeCorrectedPowerStatePayload(NodePayload):
|
||||
# Version 1.3: Parent NodePayload version 1.3
|
||||
# Version 1.4: Parent NodePayload version 1.4
|
||||
# Version 1.5: Parent NodePayload version 1.5
|
||||
VERSION = '1.5'
|
||||
# Version 1.6: Parent NodePayload version 1.6
|
||||
VERSION = '1.6'
|
||||
|
||||
fields = {
|
||||
'from_power': object_fields.StringField(nullable=True)
|
||||
@ -694,8 +702,8 @@ class NodeSetProvisionStatePayload(NodePayload):
|
||||
# Version 1.2: Parent NodePayload version 1.2
|
||||
# Version 1.3: Parent NodePayload version 1.3
|
||||
# Version 1.4: Parent NodePayload version 1.4
|
||||
# Version 1.5: Parent NodePayload version 1.5
|
||||
VERSION = '1.5'
|
||||
# Version 1.6: Parent NodePayload version 1.6
|
||||
VERSION = '1.6'
|
||||
|
||||
SCHEMA = dict(NodePayload.SCHEMA,
|
||||
**{'instance_info': ('node', 'instance_info')})
|
||||
@ -732,7 +740,8 @@ class NodeCRUDPayload(NodePayload):
|
||||
# Version 1.1: Parent NodePayload version 1.3
|
||||
# Version 1.2: Parent NodePayload version 1.4
|
||||
# Version 1.3: Parent NodePayload version 1.5
|
||||
VERSION = '1.3'
|
||||
# Version 1.4: Parent NodePayload version 1.6
|
||||
VERSION = '1.4'
|
||||
|
||||
SCHEMA = dict(NodePayload.SCHEMA,
|
||||
**{'instance_info': ('node', 'instance_info'),
|
||||
|
@ -173,3 +173,7 @@ class TraitList(object_base.ObjectListBase, base.IronicObject):
|
||||
:raises: NodeNotFound if the node no longer appears in the database.
|
||||
"""
|
||||
cls.dbapi.unset_node_traits(node_id)
|
||||
|
||||
def get_trait_names(self):
|
||||
"""Return a list of names of the traits in this list."""
|
||||
return [t.trait for t in self.objects]
|
||||
|
@ -4312,6 +4312,7 @@ class TestTraits(test_api_base.BaseApiTest):
|
||||
provision_state=states.AVAILABLE, name='node-39')
|
||||
self.traits = ['CUSTOM_1', 'CUSTOM_2']
|
||||
self._add_traits(self.node, self.traits)
|
||||
self.node.obj_reset_changes()
|
||||
p = mock.patch.object(rpcapi.ConductorAPI, 'get_topic_for')
|
||||
self.mock_gtf = p.start()
|
||||
self.mock_gtf.return_value = 'test-topic'
|
||||
@ -4325,7 +4326,7 @@ class TestTraits(test_api_base.BaseApiTest):
|
||||
def test_get_all_traits(self):
|
||||
ret = self.get_json('/nodes/%s/traits' % self.node.uuid,
|
||||
headers={api_base.Version.string: self.version})
|
||||
self.assertEqual({'traits': ['CUSTOM_1', 'CUSTOM_2']}, ret)
|
||||
self.assertEqual({'traits': self.traits}, ret)
|
||||
|
||||
def test_get_all_traits_fails_with_node_not_found(self):
|
||||
ret = self.get_json('/nodes/badname/traits',
|
||||
@ -4340,18 +4341,60 @@ class TestTraits(test_api_base.BaseApiTest):
|
||||
self.assertEqual(http_client.NOT_ACCEPTABLE, ret.status_code)
|
||||
|
||||
@mock.patch.object(rpcapi.ConductorAPI, 'add_node_traits')
|
||||
def test_set_all_traits(self, mock_add):
|
||||
request_body = {'traits': ['CUSTOM_3']}
|
||||
@mock.patch.object(notification_utils, '_emit_api_notification')
|
||||
def test_set_all_traits(self, mock_notify, mock_add):
|
||||
traits = ['CUSTOM_3']
|
||||
request_body = {'traits': traits}
|
||||
ret = self.put_json('/nodes/%s/traits' % self.node.name,
|
||||
request_body,
|
||||
headers={api_base.Version.string: self.version})
|
||||
self.assertEqual(http_client.NO_CONTENT, ret.status_code)
|
||||
mock_add.assert_called_once_with(mock.ANY, self.node.id,
|
||||
['CUSTOM_3'], replace=True,
|
||||
traits, replace=True,
|
||||
topic='test-topic')
|
||||
mock_notify.assert_has_calls(
|
||||
[mock.call(mock.ANY, mock.ANY, 'update',
|
||||
obj_fields.NotificationLevel.INFO,
|
||||
obj_fields.NotificationStatus.START, chassis_uuid=None),
|
||||
mock.call(mock.ANY, mock.ANY, 'update',
|
||||
obj_fields.NotificationLevel.INFO,
|
||||
obj_fields.NotificationStatus.END,
|
||||
chassis_uuid=None)])
|
||||
notify_args = mock_notify.call_args_list
|
||||
self.assertEqual(traits, notify_args[0][0][1].traits.get_trait_names())
|
||||
self.assertEqual(traits, notify_args[1][0][1].traits.get_trait_names())
|
||||
|
||||
@mock.patch.object(rpcapi.ConductorAPI, 'add_node_traits')
|
||||
def test_set_all_traits_empty(self, mock_add):
|
||||
@mock.patch.object(notification_utils, '_emit_api_notification')
|
||||
def test_set_all_traits_with_chassis(self, mock_notify, mock_add):
|
||||
traits = ['CUSTOM_3']
|
||||
chassis = obj_utils.create_test_chassis(self.context)
|
||||
self.node.chassis_id = chassis.id
|
||||
self.node.save()
|
||||
request_body = {'traits': traits}
|
||||
ret = self.put_json('/nodes/%s/traits' % self.node.name,
|
||||
request_body,
|
||||
headers={api_base.Version.string: self.version})
|
||||
self.assertEqual(http_client.NO_CONTENT, ret.status_code)
|
||||
mock_add.assert_called_once_with(mock.ANY, self.node.id,
|
||||
traits, replace=True,
|
||||
topic='test-topic')
|
||||
mock_notify.assert_has_calls(
|
||||
[mock.call(mock.ANY, mock.ANY, 'update',
|
||||
obj_fields.NotificationLevel.INFO,
|
||||
obj_fields.NotificationStatus.START,
|
||||
chassis_uuid=chassis.uuid),
|
||||
mock.call(mock.ANY, mock.ANY, 'update',
|
||||
obj_fields.NotificationLevel.INFO,
|
||||
obj_fields.NotificationStatus.END,
|
||||
chassis_uuid=chassis.uuid)])
|
||||
notify_args = mock_notify.call_args_list
|
||||
self.assertEqual(traits, notify_args[0][0][1].traits.get_trait_names())
|
||||
self.assertEqual(traits, notify_args[1][0][1].traits.get_trait_names())
|
||||
|
||||
@mock.patch.object(rpcapi.ConductorAPI, 'add_node_traits')
|
||||
@mock.patch.object(notification_utils, '_emit_api_notification')
|
||||
def test_set_all_traits_empty(self, mock_notify, mock_add):
|
||||
request_body = {'traits': []}
|
||||
ret = self.put_json('/nodes/%s/traits' % self.node.name,
|
||||
request_body,
|
||||
@ -4360,9 +4403,21 @@ class TestTraits(test_api_base.BaseApiTest):
|
||||
mock_add.assert_called_once_with(mock.ANY, self.node.id,
|
||||
[], replace=True,
|
||||
topic='test-topic')
|
||||
mock_notify.assert_has_calls(
|
||||
[mock.call(mock.ANY, mock.ANY, 'update',
|
||||
obj_fields.NotificationLevel.INFO,
|
||||
obj_fields.NotificationStatus.START, chassis_uuid=None),
|
||||
mock.call(mock.ANY, mock.ANY, 'update',
|
||||
obj_fields.NotificationLevel.INFO,
|
||||
obj_fields.NotificationStatus.END,
|
||||
chassis_uuid=None)])
|
||||
notify_args = mock_notify.call_args_list
|
||||
self.assertEqual([], notify_args[0][0][1].traits.get_trait_names())
|
||||
self.assertEqual([], notify_args[1][0][1].traits.get_trait_names())
|
||||
|
||||
@mock.patch.object(rpcapi.ConductorAPI, 'add_node_traits')
|
||||
def test_set_all_traits_rejects_bad_trait(self, mock_add):
|
||||
@mock.patch.object(notification_utils, '_emit_api_notification')
|
||||
def test_set_all_traits_rejects_bad_trait(self, mock_notify, mock_add):
|
||||
request_body = {'traits': ['CUSTOM_3', 'BAD_TRAIT']}
|
||||
ret = self.put_json('/nodes/%s/traits' % self.node.name,
|
||||
request_body,
|
||||
@ -4370,9 +4425,12 @@ class TestTraits(test_api_base.BaseApiTest):
|
||||
expect_errors=True)
|
||||
self.assertEqual(http_client.BAD_REQUEST, ret.status_code)
|
||||
self.assertFalse(mock_add.called)
|
||||
self.assertFalse(mock_notify.called)
|
||||
|
||||
@mock.patch.object(rpcapi.ConductorAPI, 'add_node_traits')
|
||||
def test_set_all_traits_rejects_too_long_trait(self, mock_add):
|
||||
@mock.patch.object(notification_utils, '_emit_api_notification')
|
||||
def test_set_all_traits_rejects_too_long_trait(self, mock_notify,
|
||||
mock_add):
|
||||
# Maximum length is 255.
|
||||
long_trait = 'CUSTOM_' + 'T' * 249
|
||||
request_body = {'traits': ['CUSTOM_3', long_trait]}
|
||||
@ -4382,14 +4440,17 @@ class TestTraits(test_api_base.BaseApiTest):
|
||||
expect_errors=True)
|
||||
self.assertEqual(http_client.BAD_REQUEST, ret.status_code)
|
||||
self.assertFalse(mock_add.called)
|
||||
self.assertFalse(mock_notify.called)
|
||||
|
||||
@mock.patch.object(rpcapi.ConductorAPI, 'add_node_traits')
|
||||
def test_set_all_traits_rejects_no_body(self, mock_add):
|
||||
@mock.patch.object(notification_utils, '_emit_api_notification')
|
||||
def test_set_all_traits_rejects_no_body(self, mock_notify, mock_add):
|
||||
ret = self.put_json('/nodes/%s/traits' % self.node.name, {},
|
||||
headers={api_base.Version.string: self.version},
|
||||
expect_errors=True)
|
||||
self.assertEqual(http_client.BAD_REQUEST, ret.status_code)
|
||||
self.assertFalse(mock_add.called)
|
||||
self.assertFalse(mock_notify.called)
|
||||
|
||||
def test_set_all_traits_fails_with_bad_version(self):
|
||||
request_body = {'traits': []}
|
||||
@ -4399,16 +4460,30 @@ class TestTraits(test_api_base.BaseApiTest):
|
||||
self.assertEqual(http_client.METHOD_NOT_ALLOWED, ret.status_code)
|
||||
|
||||
@mock.patch.object(rpcapi.ConductorAPI, 'add_node_traits')
|
||||
def test_add_single_trait(self, mock_add):
|
||||
@mock.patch.object(notification_utils, '_emit_api_notification')
|
||||
def test_add_single_trait(self, mock_notify, mock_add):
|
||||
ret = self.put_json('/nodes/%s/traits/CUSTOM_3' % self.node.name, {},
|
||||
headers={api_base.Version.string: self.version})
|
||||
self.assertEqual(http_client.NO_CONTENT, ret.status_code)
|
||||
mock_add.assert_called_once_with(mock.ANY, self.node.id,
|
||||
['CUSTOM_3'], replace=False,
|
||||
topic='test-topic')
|
||||
mock_notify.assert_has_calls(
|
||||
[mock.call(mock.ANY, mock.ANY, 'update',
|
||||
obj_fields.NotificationLevel.INFO,
|
||||
obj_fields.NotificationStatus.START, chassis_uuid=None),
|
||||
mock.call(mock.ANY, mock.ANY, 'update',
|
||||
obj_fields.NotificationLevel.INFO,
|
||||
obj_fields.NotificationStatus.END,
|
||||
chassis_uuid=None)])
|
||||
traits = self.traits + ['CUSTOM_3']
|
||||
notify_args = mock_notify.call_args_list
|
||||
self.assertEqual(traits, notify_args[0][0][1].traits.get_trait_names())
|
||||
self.assertEqual(traits, notify_args[1][0][1].traits.get_trait_names())
|
||||
|
||||
@mock.patch.object(rpcapi.ConductorAPI, 'add_node_traits')
|
||||
def test_no_add_single_trait_via_body(self, mock_add):
|
||||
@mock.patch.object(notification_utils, '_emit_api_notification')
|
||||
def test_no_add_single_trait_via_body(self, mock_notify, mock_add):
|
||||
request_body = {'trait': 'CUSTOM_3'}
|
||||
ret = self.put_json('/nodes/%s/traits' % self.node.name,
|
||||
request_body,
|
||||
@ -4416,9 +4491,11 @@ class TestTraits(test_api_base.BaseApiTest):
|
||||
expect_errors=True)
|
||||
self.assertEqual(http_client.BAD_REQUEST, ret.status_code)
|
||||
self.assertFalse(mock_add.called)
|
||||
self.assertFalse(mock_notify.called)
|
||||
|
||||
@mock.patch.object(rpcapi.ConductorAPI, 'add_node_traits')
|
||||
def test_no_add_single_trait_via_body_2(self, mock_add):
|
||||
@mock.patch.object(notification_utils, '_emit_api_notification')
|
||||
def test_no_add_single_trait_via_body_2(self, mock_notify, mock_add):
|
||||
request_body = {'traits': ['CUSTOM_3']}
|
||||
ret = self.put_json('/nodes/%s/traits/CUSTOM_3' % self.node.name,
|
||||
request_body,
|
||||
@ -4426,17 +4503,22 @@ class TestTraits(test_api_base.BaseApiTest):
|
||||
expect_errors=True)
|
||||
self.assertEqual(http_client.BAD_REQUEST, ret.status_code)
|
||||
self.assertFalse(mock_add.called)
|
||||
self.assertFalse(mock_notify.called)
|
||||
|
||||
@mock.patch.object(rpcapi.ConductorAPI, 'add_node_traits')
|
||||
def test_add_single_trait_rejects_bad_trait(self, mock_add):
|
||||
@mock.patch.object(notification_utils, '_emit_api_notification')
|
||||
def test_add_single_trait_rejects_bad_trait(self, mock_notify, mock_add):
|
||||
ret = self.put_json('/nodes/%s/traits/bad_trait' % self.node.name, {},
|
||||
headers={api_base.Version.string: self.version},
|
||||
expect_errors=True)
|
||||
self.assertEqual(http_client.BAD_REQUEST, ret.status_code)
|
||||
self.assertFalse(mock_add.called)
|
||||
self.assertFalse(mock_notify.called)
|
||||
|
||||
@mock.patch.object(rpcapi.ConductorAPI, 'add_node_traits')
|
||||
def test_add_single_trait_rejects_too_long_trait(self, mock_add):
|
||||
@mock.patch.object(notification_utils, '_emit_api_notification')
|
||||
def test_add_single_trait_rejects_too_long_trait(self, mock_notify,
|
||||
mock_add):
|
||||
# Maximum length is 255.
|
||||
long_trait = 'CUSTOM_' + 'T' * 249
|
||||
ret = self.put_json('/nodes/%s/traits/%s' % (
|
||||
@ -4445,9 +4527,12 @@ class TestTraits(test_api_base.BaseApiTest):
|
||||
expect_errors=True)
|
||||
self.assertEqual(http_client.BAD_REQUEST, ret.status_code)
|
||||
self.assertFalse(mock_add.called)
|
||||
self.assertFalse(mock_notify.called)
|
||||
|
||||
@mock.patch.object(rpcapi.ConductorAPI, 'add_node_traits')
|
||||
def test_add_single_trait_fails_max_trait_limit(self, mock_add):
|
||||
@mock.patch.object(notification_utils, '_emit_api_notification')
|
||||
def test_add_single_trait_fails_max_trait_limit(self, mock_notify,
|
||||
mock_add):
|
||||
mock_add.side_effect = exception.InvalidParameterValue(
|
||||
err='too many traits')
|
||||
ret = self.put_json('/nodes/%s/traits/CUSTOM_3' % self.node.name, {},
|
||||
@ -4457,9 +4542,23 @@ class TestTraits(test_api_base.BaseApiTest):
|
||||
mock_add.assert_called_once_with(mock.ANY, self.node.id,
|
||||
['CUSTOM_3'], replace=False,
|
||||
topic='test-topic')
|
||||
mock_notify.assert_has_calls(
|
||||
[mock.call(mock.ANY, mock.ANY, 'update',
|
||||
obj_fields.NotificationLevel.INFO,
|
||||
obj_fields.NotificationStatus.START, chassis_uuid=None),
|
||||
mock.call(mock.ANY, mock.ANY, 'update',
|
||||
obj_fields.NotificationLevel.ERROR,
|
||||
obj_fields.NotificationStatus.ERROR,
|
||||
chassis_uuid=None)])
|
||||
traits = self.traits + ['CUSTOM_3']
|
||||
notify_args = mock_notify.call_args_list
|
||||
self.assertEqual(traits, notify_args[0][0][1].traits.get_trait_names())
|
||||
self.assertEqual(traits, notify_args[1][0][1].traits.get_trait_names())
|
||||
|
||||
@mock.patch.object(rpcapi.ConductorAPI, 'add_node_traits')
|
||||
def test_add_single_trait_fails_if_node_locked(self, mock_add):
|
||||
@mock.patch.object(notification_utils, '_emit_api_notification')
|
||||
def test_add_single_trait_fails_if_node_locked(self, mock_notify,
|
||||
mock_add):
|
||||
mock_add.side_effect = exception.NodeLocked(
|
||||
node=self.node.uuid, host='host1')
|
||||
ret = self.put_json('/nodes/%s/traits/CUSTOM_3' % self.node.name, {},
|
||||
@ -4469,9 +4568,23 @@ class TestTraits(test_api_base.BaseApiTest):
|
||||
mock_add.assert_called_once_with(mock.ANY, self.node.id,
|
||||
['CUSTOM_3'], replace=False,
|
||||
topic='test-topic')
|
||||
mock_notify.assert_has_calls(
|
||||
[mock.call(mock.ANY, mock.ANY, 'update',
|
||||
obj_fields.NotificationLevel.INFO,
|
||||
obj_fields.NotificationStatus.START, chassis_uuid=None),
|
||||
mock.call(mock.ANY, mock.ANY, 'update',
|
||||
obj_fields.NotificationLevel.ERROR,
|
||||
obj_fields.NotificationStatus.ERROR,
|
||||
chassis_uuid=None)])
|
||||
traits = self.traits + ['CUSTOM_3']
|
||||
notify_args = mock_notify.call_args_list
|
||||
self.assertEqual(traits, notify_args[0][0][1].traits.get_trait_names())
|
||||
self.assertEqual(traits, notify_args[1][0][1].traits.get_trait_names())
|
||||
|
||||
@mock.patch.object(rpcapi.ConductorAPI, 'add_node_traits')
|
||||
def test_add_single_trait_fails_if_node_not_found(self, mock_add):
|
||||
@mock.patch.object(notification_utils, '_emit_api_notification')
|
||||
def test_add_single_trait_fails_if_node_not_found(self, mock_notify,
|
||||
mock_add):
|
||||
mock_add.side_effect = exception.NodeNotFound(node=self.node.uuid)
|
||||
ret = self.put_json('/nodes/%s/traits/CUSTOM_3' % self.node.name, {},
|
||||
headers={api_base.Version.string: self.version},
|
||||
@ -4480,6 +4593,18 @@ class TestTraits(test_api_base.BaseApiTest):
|
||||
mock_add.assert_called_once_with(mock.ANY, self.node.id,
|
||||
['CUSTOM_3'], replace=False,
|
||||
topic='test-topic')
|
||||
mock_notify.assert_has_calls(
|
||||
[mock.call(mock.ANY, mock.ANY, 'update',
|
||||
obj_fields.NotificationLevel.INFO,
|
||||
obj_fields.NotificationStatus.START, chassis_uuid=None),
|
||||
mock.call(mock.ANY, mock.ANY, 'update',
|
||||
obj_fields.NotificationLevel.ERROR,
|
||||
obj_fields.NotificationStatus.ERROR,
|
||||
chassis_uuid=None)])
|
||||
traits = self.traits + ['CUSTOM_3']
|
||||
notify_args = mock_notify.call_args_list
|
||||
self.assertEqual(traits, notify_args[0][0][1].traits.get_trait_names())
|
||||
self.assertEqual(traits, notify_args[1][0][1].traits.get_trait_names())
|
||||
|
||||
def test_add_single_traits_fails_with_bad_version(self):
|
||||
ret = self.put_json('/nodes/%s/traits/CUSTOM_TRAIT1' % self.node.uuid,
|
||||
@ -4488,12 +4613,48 @@ class TestTraits(test_api_base.BaseApiTest):
|
||||
self.assertEqual(http_client.METHOD_NOT_ALLOWED, ret.status_code)
|
||||
|
||||
@mock.patch.object(rpcapi.ConductorAPI, 'remove_node_traits')
|
||||
def test_delete_all_traits(self, mock_remove):
|
||||
@mock.patch.object(notification_utils, '_emit_api_notification')
|
||||
def test_delete_all_traits(self, mock_notify, mock_remove):
|
||||
ret = self.delete('/nodes/%s/traits' % self.node.name,
|
||||
headers={api_base.Version.string: self.version})
|
||||
self.assertEqual(http_client.NO_CONTENT, ret.status_code)
|
||||
mock_remove.assert_called_once_with(mock.ANY, self.node.id,
|
||||
None, topic='test-topic')
|
||||
mock_notify.assert_has_calls(
|
||||
[mock.call(mock.ANY, mock.ANY, 'update',
|
||||
obj_fields.NotificationLevel.INFO,
|
||||
obj_fields.NotificationStatus.START, chassis_uuid=None),
|
||||
mock.call(mock.ANY, mock.ANY, 'update',
|
||||
obj_fields.NotificationLevel.INFO,
|
||||
obj_fields.NotificationStatus.END,
|
||||
chassis_uuid=None)])
|
||||
notify_args = mock_notify.call_args_list
|
||||
self.assertEqual([], notify_args[0][0][1].traits.get_trait_names())
|
||||
self.assertEqual([], notify_args[1][0][1].traits.get_trait_names())
|
||||
|
||||
@mock.patch.object(rpcapi.ConductorAPI, 'remove_node_traits')
|
||||
@mock.patch.object(notification_utils, '_emit_api_notification')
|
||||
def test_delete_all_traits_with_chassis(self, mock_notify, mock_remove):
|
||||
chassis = obj_utils.create_test_chassis(self.context)
|
||||
self.node.chassis_id = chassis.id
|
||||
self.node.save()
|
||||
ret = self.delete('/nodes/%s/traits' % self.node.name,
|
||||
headers={api_base.Version.string: self.version})
|
||||
self.assertEqual(http_client.NO_CONTENT, ret.status_code)
|
||||
mock_remove.assert_called_once_with(mock.ANY, self.node.id,
|
||||
None, topic='test-topic')
|
||||
mock_notify.assert_has_calls(
|
||||
[mock.call(mock.ANY, mock.ANY, 'update',
|
||||
obj_fields.NotificationLevel.INFO,
|
||||
obj_fields.NotificationStatus.START,
|
||||
chassis_uuid=chassis.uuid),
|
||||
mock.call(mock.ANY, mock.ANY, 'update',
|
||||
obj_fields.NotificationLevel.INFO,
|
||||
obj_fields.NotificationStatus.END,
|
||||
chassis_uuid=chassis.uuid)])
|
||||
notify_args = mock_notify.call_args_list
|
||||
self.assertEqual([], notify_args[0][0][1].traits.get_trait_names())
|
||||
self.assertEqual([], notify_args[1][0][1].traits.get_trait_names())
|
||||
|
||||
def test_delete_all_traits_fails_with_bad_version(self):
|
||||
ret = self.delete('/nodes/%s/traits' % self.node.uuid,
|
||||
@ -4502,15 +4663,29 @@ class TestTraits(test_api_base.BaseApiTest):
|
||||
self.assertEqual(http_client.NOT_FOUND, ret.status_code)
|
||||
|
||||
@mock.patch.object(rpcapi.ConductorAPI, 'remove_node_traits')
|
||||
def test_delete_trait(self, mock_remove):
|
||||
@mock.patch.object(notification_utils, '_emit_api_notification')
|
||||
def test_delete_trait(self, mock_notify, mock_remove):
|
||||
ret = self.delete('/nodes/%s/traits/CUSTOM_1' % self.node.name,
|
||||
headers={api_base.Version.string: self.version})
|
||||
self.assertEqual(http_client.NO_CONTENT, ret.status_code)
|
||||
mock_remove.assert_called_once_with(mock.ANY, self.node.id,
|
||||
['CUSTOM_1'], topic='test-topic')
|
||||
mock_notify.assert_has_calls(
|
||||
[mock.call(mock.ANY, mock.ANY, 'update',
|
||||
obj_fields.NotificationLevel.INFO,
|
||||
obj_fields.NotificationStatus.START, chassis_uuid=None),
|
||||
mock.call(mock.ANY, mock.ANY, 'update',
|
||||
obj_fields.NotificationLevel.INFO,
|
||||
obj_fields.NotificationStatus.END,
|
||||
chassis_uuid=None)])
|
||||
traits = ['CUSTOM_2']
|
||||
notify_args = mock_notify.call_args_list
|
||||
self.assertEqual(traits, notify_args[0][0][1].traits.get_trait_names())
|
||||
self.assertEqual(traits, notify_args[1][0][1].traits.get_trait_names())
|
||||
|
||||
@mock.patch.object(rpcapi.ConductorAPI, 'remove_node_traits')
|
||||
def test_delete_trait_fails_if_node_locked(self, mock_remove):
|
||||
@mock.patch.object(notification_utils, '_emit_api_notification')
|
||||
def test_delete_trait_fails_if_node_locked(self, mock_notify, mock_remove):
|
||||
mock_remove.side_effect = exception.NodeLocked(
|
||||
node=self.node.uuid, host='host1')
|
||||
ret = self.delete('/nodes/%s/traits/CUSTOM_1' % self.node.name,
|
||||
@ -4519,9 +4694,23 @@ class TestTraits(test_api_base.BaseApiTest):
|
||||
self.assertEqual(http_client.CONFLICT, ret.status_code)
|
||||
mock_remove.assert_called_once_with(mock.ANY, self.node.id,
|
||||
['CUSTOM_1'], topic='test-topic')
|
||||
mock_notify.assert_has_calls(
|
||||
[mock.call(mock.ANY, mock.ANY, 'update',
|
||||
obj_fields.NotificationLevel.INFO,
|
||||
obj_fields.NotificationStatus.START, chassis_uuid=None),
|
||||
mock.call(mock.ANY, mock.ANY, 'update',
|
||||
obj_fields.NotificationLevel.ERROR,
|
||||
obj_fields.NotificationStatus.ERROR,
|
||||
chassis_uuid=None)])
|
||||
traits = ['CUSTOM_2']
|
||||
notify_args = mock_notify.call_args_list
|
||||
self.assertEqual(traits, notify_args[0][0][1].traits.get_trait_names())
|
||||
self.assertEqual(traits, notify_args[1][0][1].traits.get_trait_names())
|
||||
|
||||
@mock.patch.object(rpcapi.ConductorAPI, 'remove_node_traits')
|
||||
def test_delete_trait_fails_if_node_not_found(self, mock_remove):
|
||||
@mock.patch.object(notification_utils, '_emit_api_notification')
|
||||
def test_delete_trait_fails_if_node_not_found(self, mock_notify,
|
||||
mock_remove):
|
||||
mock_remove.side_effect = exception.NodeNotFound(node=self.node.uuid)
|
||||
ret = self.delete('/nodes/%s/traits/CUSTOM_1' % self.node.name,
|
||||
headers={api_base.Version.string: self.version},
|
||||
@ -4529,9 +4718,23 @@ class TestTraits(test_api_base.BaseApiTest):
|
||||
self.assertEqual(http_client.NOT_FOUND, ret.status_code)
|
||||
mock_remove.assert_called_once_with(mock.ANY, self.node.id,
|
||||
['CUSTOM_1'], topic='test-topic')
|
||||
mock_notify.assert_has_calls(
|
||||
[mock.call(mock.ANY, mock.ANY, 'update',
|
||||
obj_fields.NotificationLevel.INFO,
|
||||
obj_fields.NotificationStatus.START, chassis_uuid=None),
|
||||
mock.call(mock.ANY, mock.ANY, 'update',
|
||||
obj_fields.NotificationLevel.ERROR,
|
||||
obj_fields.NotificationStatus.ERROR,
|
||||
chassis_uuid=None)])
|
||||
traits = ['CUSTOM_2']
|
||||
notify_args = mock_notify.call_args_list
|
||||
self.assertEqual(traits, notify_args[0][0][1].traits.get_trait_names())
|
||||
self.assertEqual(traits, notify_args[1][0][1].traits.get_trait_names())
|
||||
|
||||
@mock.patch.object(rpcapi.ConductorAPI, 'remove_node_traits')
|
||||
def test_delete_trait_fails_if_trait_not_found(self, mock_remove):
|
||||
@mock.patch.object(notification_utils, '_emit_api_notification')
|
||||
def test_delete_trait_fails_if_trait_not_found(self, mock_notify,
|
||||
mock_remove):
|
||||
mock_remove.side_effect = exception.NodeTraitNotFound(
|
||||
node_id=self.node.uuid, trait='CUSTOM_12')
|
||||
ret = self.delete('/nodes/%s/traits/CUSTOM_12' % self.node.name,
|
||||
@ -4540,6 +4743,19 @@ class TestTraits(test_api_base.BaseApiTest):
|
||||
self.assertEqual(http_client.NOT_FOUND, ret.status_code)
|
||||
mock_remove.assert_called_once_with(mock.ANY, self.node.id,
|
||||
['CUSTOM_12'], topic='test-topic')
|
||||
mock_notify.assert_has_calls(
|
||||
[mock.call(mock.ANY, mock.ANY, 'update',
|
||||
obj_fields.NotificationLevel.INFO,
|
||||
obj_fields.NotificationStatus.START, chassis_uuid=None),
|
||||
mock.call(mock.ANY, mock.ANY, 'update',
|
||||
obj_fields.NotificationLevel.ERROR,
|
||||
obj_fields.NotificationStatus.ERROR,
|
||||
chassis_uuid=None)])
|
||||
notify_args = mock_notify.call_args_list
|
||||
self.assertEqual(self.traits,
|
||||
notify_args[0][0][1].traits.get_trait_names())
|
||||
self.assertEqual(self.traits,
|
||||
notify_args[1][0][1].traits.get_trait_names())
|
||||
|
||||
def test_delete_trait_fails_with_bad_version(self):
|
||||
ret = self.delete('/nodes/%s/traits/CUSTOM_TRAIT1' % self.node.uuid,
|
||||
|
@ -16,6 +16,7 @@
|
||||
import datetime
|
||||
|
||||
import mock
|
||||
from oslo_utils import uuidutils
|
||||
from testtools import matchers
|
||||
|
||||
from ironic.common import context
|
||||
@ -425,3 +426,100 @@ class TestConvertToVersion(db_base.DbTestCase):
|
||||
|
||||
self.assertIsNone(node.traits)
|
||||
self.assertEqual({}, node.obj_get_changes())
|
||||
|
||||
|
||||
class TestNodePayloads(db_base.DbTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestNodePayloads, self).setUp()
|
||||
self.ctxt = context.get_admin_context()
|
||||
self.fake_node = db_utils.get_test_node()
|
||||
self.node = obj_utils.get_test_node(self.ctxt, **self.fake_node)
|
||||
|
||||
def _test_node_payload(self, payload):
|
||||
self.assertEqual(self.node.clean_step, payload.clean_step)
|
||||
self.assertEqual(self.node.console_enabled,
|
||||
payload.console_enabled)
|
||||
self.assertEqual(self.node.created_at, payload.created_at)
|
||||
self.assertEqual(self.node.driver, payload.driver)
|
||||
self.assertEqual(self.node.extra, payload.extra)
|
||||
self.assertEqual(self.node.inspection_finished_at,
|
||||
payload.inspection_finished_at)
|
||||
self.assertEqual(self.node.inspection_started_at,
|
||||
payload.inspection_started_at)
|
||||
self.assertEqual(self.node.instance_uuid, payload.instance_uuid)
|
||||
self.assertEqual(self.node.last_error, payload.last_error)
|
||||
self.assertEqual(self.node.maintenance, payload.maintenance)
|
||||
self.assertEqual(self.node.maintenance_reason,
|
||||
payload.maintenance_reason)
|
||||
self.assertEqual(self.node.boot_interface, payload.boot_interface)
|
||||
self.assertEqual(self.node.console_interface,
|
||||
payload.console_interface)
|
||||
self.assertEqual(self.node.deploy_interface, payload.deploy_interface)
|
||||
self.assertEqual(self.node.inspect_interface,
|
||||
payload.inspect_interface)
|
||||
self.assertEqual(self.node.management_interface,
|
||||
payload.management_interface)
|
||||
self.assertEqual(self.node.network_interface,
|
||||
payload.network_interface)
|
||||
self.assertEqual(self.node.power_interface, payload.power_interface)
|
||||
self.assertEqual(self.node.raid_interface, payload.raid_interface)
|
||||
self.assertEqual(self.node.storage_interface,
|
||||
payload.storage_interface)
|
||||
self.assertEqual(self.node.vendor_interface,
|
||||
payload.vendor_interface)
|
||||
self.assertEqual(self.node.name, payload.name)
|
||||
self.assertEqual(self.node.power_state, payload.power_state)
|
||||
self.assertEqual(self.node.properties, payload.properties)
|
||||
self.assertEqual(self.node.provision_state, payload.provision_state)
|
||||
self.assertEqual(self.node.provision_updated_at,
|
||||
payload.provision_updated_at)
|
||||
self.assertEqual(self.node.resource_class, payload.resource_class)
|
||||
self.assertEqual(self.node.target_power_state,
|
||||
payload.target_power_state)
|
||||
self.assertEqual(self.node.target_provision_state,
|
||||
payload.target_provision_state)
|
||||
self.assertEqual(self.node.traits.get_trait_names(), payload.traits)
|
||||
self.assertEqual(self.node.updated_at, payload.updated_at)
|
||||
self.assertEqual(self.node.uuid, payload.uuid)
|
||||
|
||||
def test_node_payload(self):
|
||||
payload = objects.NodePayload(self.node)
|
||||
self._test_node_payload(payload)
|
||||
|
||||
def test_node_payload_no_traits(self):
|
||||
delattr(self.node, 'traits')
|
||||
payload = objects.NodePayload(self.node)
|
||||
self.assertEqual([], payload.traits)
|
||||
|
||||
def test_node_payload_traits_is_none(self):
|
||||
self.node.traits = None
|
||||
payload = objects.NodePayload(self.node)
|
||||
self.assertEqual([], payload.traits)
|
||||
|
||||
def test_node_set_power_state_payload(self):
|
||||
payload = objects.NodeSetPowerStatePayload(self.node, 'POWER_ON')
|
||||
self._test_node_payload(payload)
|
||||
self.assertEqual('POWER_ON', payload.to_power)
|
||||
|
||||
def test_node_corrected_power_state_payload(self):
|
||||
payload = objects.NodeCorrectedPowerStatePayload(self.node, 'POWER_ON')
|
||||
self._test_node_payload(payload)
|
||||
self.assertEqual('POWER_ON', payload.from_power)
|
||||
|
||||
def test_node_set_provision_state_payload(self):
|
||||
payload = objects.NodeSetProvisionStatePayload(self.node, 'AVAILABLE',
|
||||
'DEPLOYING', 'DEPLOY')
|
||||
self._test_node_payload(payload)
|
||||
self.assertEqual(self.node.instance_info, payload.instance_info)
|
||||
self.assertEqual('DEPLOY', payload.event)
|
||||
self.assertEqual('AVAILABLE', payload.previous_provision_state)
|
||||
self.assertEqual('DEPLOYING', payload.previous_target_provision_state)
|
||||
|
||||
def test_node_crud_payload(self):
|
||||
chassis_uuid = uuidutils.generate_uuid()
|
||||
payload = objects.NodeCRUDPayload(self.node, chassis_uuid)
|
||||
self._test_node_payload(payload)
|
||||
self.assertEqual(chassis_uuid, payload.chassis_uuid)
|
||||
self.assertEqual(self.node.instance_info, payload.instance_info)
|
||||
self.assertEqual(self.node.driver_info, payload.driver_info)
|
||||
|
@ -692,21 +692,21 @@ expected_object_fingerprints = {
|
||||
'Conductor': '1.2-5091f249719d4a465062a1b3dc7f860d',
|
||||
'EventType': '1.1-aa2ba1afd38553e3880c267404e8d370',
|
||||
'NotificationPublisher': '1.0-51a09397d6c0687771fb5be9a999605d',
|
||||
'NodePayload': '1.5-a08d9f8a74b1f827ade12efd853cebc4',
|
||||
'NodePayload': '1.6-33b165d11b05f8e1a91da5b1bd344dda',
|
||||
'NodeSetPowerStateNotification': '1.0-59acc533c11d306f149846f922739c15',
|
||||
'NodeSetPowerStatePayload': '1.5-5292dea58d84e1bec9345b2e1a10114b',
|
||||
'NodeSetPowerStatePayload': '1.6-b32af2903cba9a61be83f5b119188e4d',
|
||||
'NodeCorrectedPowerStateNotification':
|
||||
'1.0-59acc533c11d306f149846f922739c15',
|
||||
'NodeCorrectedPowerStatePayload': '1.5-d22164f7f8f36dedee7d685525acb844',
|
||||
'NodeCorrectedPowerStatePayload': '1.6-e44d9d913afba61281190082605dd1fb',
|
||||
'NodeSetProvisionStateNotification':
|
||||
'1.0-59acc533c11d306f149846f922739c15',
|
||||
'NodeSetProvisionStatePayload': '1.5-65f972bc0cd0096632c8d7749c3dca96',
|
||||
'NodeSetProvisionStatePayload': '1.6-5e608497664b122ae53240f35072d731',
|
||||
'VolumeConnector': '1.0-3e0252c0ab6e6b9d158d09238a577d97',
|
||||
'VolumeTarget': '1.0-0b10d663d8dae675900b2c7548f76f5e',
|
||||
'ChassisCRUDNotification': '1.0-59acc533c11d306f149846f922739c15',
|
||||
'ChassisCRUDPayload': '1.0-dce63895d8186279a7dd577cffccb202',
|
||||
'NodeCRUDNotification': '1.0-59acc533c11d306f149846f922739c15',
|
||||
'NodeCRUDPayload': '1.3-3026beced6c8857dc0574e8e0762415b',
|
||||
'NodeCRUDPayload': '1.4-e55b77489d3ff71d561d0ae03668e298',
|
||||
'PortCRUDNotification': '1.0-59acc533c11d306f149846f922739c15',
|
||||
'PortCRUDPayload': '1.2-233d259df442eb15cc584fae1fe81504',
|
||||
'NodeMaintenanceNotification': '1.0-59acc533c11d306f149846f922739c15',
|
||||
|
@ -87,3 +87,13 @@ class TestTraitObject(db_base.DbTestCase, obj_utils.SchemasTestMixIn):
|
||||
|
||||
self.assertTrue(result)
|
||||
mock_trait_exists.assert_called_once_with(self.node_id, "trait")
|
||||
|
||||
def test_get_trait_names(self):
|
||||
trait = objects.Trait(context=self.context,
|
||||
node_id=self.fake_trait['node_id'],
|
||||
trait=self.fake_trait['trait'])
|
||||
trait_list = objects.TraitList(context=self.context, objects=[trait])
|
||||
|
||||
result = trait_list.get_trait_names()
|
||||
|
||||
self.assertEqual([self.fake_trait['trait']], result)
|
||||
|
Loading…
x
Reference in New Issue
Block a user