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:
Mark Goddard 2018-01-22 09:51:00 +00:00
parent 180234b445
commit c9677cd43b
7 changed files with 435 additions and 54 deletions

View File

@ -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',

View File

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

View File

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

View File

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

View File

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

View File

@ -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',

View File

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