Add ironic port group CRUD notifications

This patch adds notifications for create, update or delete
port groups. Event types are:
baremetal.portgroup.{create, update, delete}.{start,end,error}.
Developer documentation updated. "portgroup_uuid" field added to
port payload.

Closes-Bug: #1660292
Change-Id: I9a8ce6c34e9c704b1aeeb526babcb20a5b1261db
This commit is contained in:
Yuriy Zveryanskyy 2017-01-31 18:05:04 +02:00
parent 57cd68caf8
commit f9d9b334da
16 changed files with 347 additions and 110 deletions

View File

@ -197,13 +197,14 @@ Example of port CRUD notification::
"payload":{ "payload":{
"ironic_object.namespace":"ironic", "ironic_object.namespace":"ironic",
"ironic_object.name":"PortCRUDPayload", "ironic_object.name":"PortCRUDPayload",
"ironic_object.version":"1.0", "ironic_object.version":"1.1",
"ironic_object.data":{ "ironic_object.data":{
"address": "77:66:23:34:11:b7", "address": "77:66:23:34:11:b7",
"created_at": "2016-02-11T15:23:03+00:00", "created_at": "2016-02-11T15:23:03+00:00",
"node_uuid": "5b236cab-ad4e-4220-b57c-e827e858745a", "node_uuid": "5b236cab-ad4e-4220-b57c-e827e858745a",
"extra": {}, "extra": {},
"local_link_connection": {}, "local_link_connection": {},
"portgroup_uuid": "bd2f385e-c51c-4752-82d1-7a9ec2c25f24",
"pxe_enabled": True, "pxe_enabled": True,
"updated_at": "2016-03-27T20:41:03+00:00", "updated_at": "2016-03-27T20:41:03+00:00",
"uuid": "1be26c0b-03f2-4d2e-ae87-c02d7f33c123" "uuid": "1be26c0b-03f2-4d2e-ae87-c02d7f33c123"
@ -213,6 +214,43 @@ Example of port CRUD notification::
"publisher_id":"ironic-api.hostname02" "publisher_id":"ironic-api.hostname02"
} }
List of CRUD notifications for port group:
* ``baremetal.portgroup.create.start``
* ``baremetal.portgroup.create.end``
* ``baremetal.portgroup.create.error``
* ``baremetal.portgroup.update.start``
* ``baremetal.portgroup.update.end``
* ``baremetal.portgroup.update.error``
* ``baremetal.portgroup.delete.start``
* ``baremetal.portgroup.delete.end``
* ``baremetal.portgroup.delete.error``
Example of portgroup CRUD notification::
{
"priority": "info",
"payload":{
"ironic_object.namespace":"ironic",
"ironic_object.name":"PortgroupCRUDPayload",
"ironic_object.version":"1.0",
"ironic_object.data":{
"address": "11:44:32:87:61:e5",
"created_at": "2017-01-11T11:33:03+00:00",
"node_uuid": "5b236cab-ad4e-4220-b57c-e827e858745a",
"extra": {},
"mode": "7",
"name": "portgroup-node-18",
"properties": {},
"standalone_ports_supported": True,
"updated_at": "2017-01-31T11:41:07+00:00",
"uuid": "db033a40-bfed-4c84-815a-3db26bb268bb",
}
},
"event_type":"baremetal.portgroup.update.end",
"publisher_id":"ironic-api.hostname02"
}
Node maintenance notifications Node maintenance notifications
------------------------------ ------------------------------

View File

@ -26,6 +26,7 @@ from ironic.objects import fields
from ironic.objects import node as node_objects from ironic.objects import node as node_objects
from ironic.objects import notification from ironic.objects import notification
from ironic.objects import port as port_objects from ironic.objects import port as port_objects
from ironic.objects import portgroup as portgroup_objects
LOG = log.getLogger(__name__) LOG = log.getLogger(__name__)
CONF = cfg.CONF CONF = cfg.CONF
@ -37,7 +38,9 @@ CRUD_NOTIFY_OBJ = {
'node': (node_objects.NodeCRUDNotification, 'node': (node_objects.NodeCRUDNotification,
node_objects.NodeCRUDPayload), node_objects.NodeCRUDPayload),
'port': (port_objects.PortCRUDNotification, 'port': (port_objects.PortCRUDNotification,
port_objects.PortCRUDPayload) port_objects.PortCRUDPayload),
'portgroup': (portgroup_objects.PortgroupCRUDNotification,
portgroup_objects.PortgroupCRUDPayload)
} }

View File

@ -533,13 +533,15 @@ class PortsController(rest.RestController):
new_port = objects.Port(context, **pdict) new_port = objects.Port(context, **pdict)
notify_extra = {'node_uuid': port.node_uuid,
'portgroup_uuid': port.portgroup_uuid}
notify.emit_start_notification(context, new_port, 'create', notify.emit_start_notification(context, new_port, 'create',
node_uuid=port.node_uuid) **notify_extra)
with notify.handle_error_notification(context, new_port, 'create', with notify.handle_error_notification(context, new_port, 'create',
node_uuid=port.node_uuid): **notify_extra):
new_port.create() new_port.create()
notify.emit_end_notification(context, new_port, 'create', notify.emit_end_notification(context, new_port, 'create',
node_uuid=port.node_uuid) **notify_extra)
# Set the HTTP Location Header # Set the HTTP Location Header
pecan.response.location = link.build_url('ports', new_port.uuid) pecan.response.location = link.build_url('ports', new_port.uuid)
return Port.convert_with_links(new_port) return Port.convert_with_links(new_port)
@ -607,17 +609,19 @@ class PortsController(rest.RestController):
rpc_port[field] = patch_val rpc_port[field] = patch_val
rpc_node = objects.Node.get_by_id(context, rpc_port.node_id) rpc_node = objects.Node.get_by_id(context, rpc_port.node_id)
notify_extra = {'node_uuid': rpc_node.uuid,
'portgroup_uuid': port.portgroup_uuid}
notify.emit_start_notification(context, rpc_port, 'update', notify.emit_start_notification(context, rpc_port, 'update',
node_uuid=rpc_node.uuid) **notify_extra)
with notify.handle_error_notification(context, rpc_port, 'update', with notify.handle_error_notification(context, rpc_port, 'update',
node_uuid=rpc_node.uuid): **notify_extra):
topic = pecan.request.rpcapi.get_topic_for(rpc_node) topic = pecan.request.rpcapi.get_topic_for(rpc_node)
new_port = pecan.request.rpcapi.update_port(context, rpc_port, new_port = pecan.request.rpcapi.update_port(context, rpc_port,
topic) topic)
api_port = Port.convert_with_links(new_port) api_port = Port.convert_with_links(new_port)
notify.emit_end_notification(context, new_port, 'update', notify.emit_end_notification(context, new_port, 'update',
node_uuid=api_port.node_uuid) **notify_extra)
return api_port return api_port
@ -638,11 +642,20 @@ class PortsController(rest.RestController):
rpc_port = objects.Port.get_by_uuid(context, port_uuid) rpc_port = objects.Port.get_by_uuid(context, port_uuid)
rpc_node = objects.Node.get_by_id(context, rpc_port.node_id) rpc_node = objects.Node.get_by_id(context, rpc_port.node_id)
portgroup_uuid = None
if rpc_port.portgroup_id:
portgroup = objects.Portgroup.get_by_id(context,
rpc_port.portgroup_id)
portgroup_uuid = portgroup.uuid
notify_extra = {'node_uuid': rpc_node.uuid,
'portgroup_uuid': portgroup_uuid}
notify.emit_start_notification(context, rpc_port, 'delete', notify.emit_start_notification(context, rpc_port, 'delete',
node_uuid=rpc_node.uuid) **notify_extra)
with notify.handle_error_notification(context, rpc_port, 'delete', with notify.handle_error_notification(context, rpc_port, 'delete',
node_uuid=rpc_node.uuid): **notify_extra):
topic = pecan.request.rpcapi.get_topic_for(rpc_node) topic = pecan.request.rpcapi.get_topic_for(rpc_node)
pecan.request.rpcapi.destroy_port(context, rpc_port, topic) pecan.request.rpcapi.destroy_port(context, rpc_port, topic)
notify.emit_end_notification(context, rpc_port, 'delete', notify.emit_end_notification(context, rpc_port, 'delete',
node_uuid=rpc_node.uuid) **notify_extra)

View File

@ -13,6 +13,7 @@
import datetime import datetime
from ironic_lib import metrics_utils from ironic_lib import metrics_utils
from oslo_utils import uuidutils
import pecan import pecan
from six.moves import http_client from six.moves import http_client
import wsme import wsme
@ -21,6 +22,7 @@ from wsme import types as wtypes
from ironic.api.controllers import base from ironic.api.controllers import base
from ironic.api.controllers import link from ironic.api.controllers import link
from ironic.api.controllers.v1 import collection from ironic.api.controllers.v1 import collection
from ironic.api.controllers.v1 import notification_utils as notify
from ironic.api.controllers.v1 import port from ironic.api.controllers.v1 import port
from ironic.api.controllers.v1 import types from ironic.api.controllers.v1 import types
from ironic.api.controllers.v1 import utils as api_utils from ironic.api.controllers.v1 import utils as api_utils
@ -429,7 +431,8 @@ class PortgroupsController(pecan.rest.RestController):
if not api_utils.allow_portgroups(): if not api_utils.allow_portgroups():
raise exception.NotFound() raise exception.NotFound()
cdict = pecan.request.context.to_policy_values() context = pecan.request.context
cdict = context.to_policy_values()
policy.authorize('baremetal:portgroup:create', cdict, cdict) policy.authorize('baremetal:portgroup:create', cdict, cdict)
if self.parent_node_ident: if self.parent_node_ident:
@ -452,9 +455,20 @@ class PortgroupsController(pecan.rest.RestController):
if vif: if vif:
common_utils.warn_about_deprecated_extra_vif_port_id() common_utils.warn_about_deprecated_extra_vif_port_id()
new_portgroup = objects.Portgroup(pecan.request.context, # NOTE(yuriyz): UUID is mandatory for notifications payload
**pg_dict) if not pg_dict.get('uuid'):
pg_dict['uuid'] = uuidutils.generate_uuid()
new_portgroup = objects.Portgroup(context, **pg_dict)
notify.emit_start_notification(context, new_portgroup, 'create',
node_uuid=portgroup.node_uuid)
with notify.handle_error_notification(context, new_portgroup, 'create',
node_uuid=portgroup.node_uuid):
new_portgroup.create() new_portgroup.create()
notify.emit_end_notification(context, new_portgroup, 'create',
node_uuid=portgroup.node_uuid)
# Set the HTTP Location Header # Set the HTTP Location Header
pecan.response.location = link.build_url('portgroups', pecan.response.location = link.build_url('portgroups',
new_portgroup.uuid) new_portgroup.uuid)
@ -472,7 +486,8 @@ class PortgroupsController(pecan.rest.RestController):
if not api_utils.allow_portgroups(): if not api_utils.allow_portgroups():
raise exception.NotFound() raise exception.NotFound()
cdict = pecan.request.context.to_policy_values() context = pecan.request.context
cdict = context.to_policy_values()
policy.authorize('baremetal:portgroup:update', cdict, cdict) policy.authorize('baremetal:portgroup:update', cdict, cdict)
if self.parent_node_ident: if self.parent_node_ident:
@ -520,14 +535,21 @@ class PortgroupsController(pecan.rest.RestController):
if rpc_portgroup[field] != patch_val: if rpc_portgroup[field] != patch_val:
rpc_portgroup[field] = patch_val rpc_portgroup[field] = patch_val
rpc_node = objects.Node.get_by_id(pecan.request.context, rpc_node = objects.Node.get_by_id(context, rpc_portgroup.node_id)
rpc_portgroup.node_id)
notify.emit_start_notification(context, rpc_portgroup, 'update',
node_uuid=rpc_node.uuid)
with notify.handle_error_notification(context, rpc_portgroup, 'update',
node_uuid=rpc_node.uuid):
topic = pecan.request.rpcapi.get_topic_for(rpc_node) topic = pecan.request.rpcapi.get_topic_for(rpc_node)
new_portgroup = pecan.request.rpcapi.update_portgroup( new_portgroup = pecan.request.rpcapi.update_portgroup(
pecan.request.context, rpc_portgroup, topic) context, rpc_portgroup, topic)
return Portgroup.convert_with_links(new_portgroup) api_portgroup = Portgroup.convert_with_links(new_portgroup)
notify.emit_end_notification(context, new_portgroup, 'update',
node_uuid=api_portgroup.node_uuid)
return api_portgroup
@METRICS.timer('PortgroupsController.delete') @METRICS.timer('PortgroupsController.delete')
@expose.expose(None, types.uuid_or_name, @expose.expose(None, types.uuid_or_name,
@ -540,7 +562,8 @@ class PortgroupsController(pecan.rest.RestController):
if not api_utils.allow_portgroups(): if not api_utils.allow_portgroups():
raise exception.NotFound() raise exception.NotFound()
cdict = pecan.request.context.to_policy_values() context = pecan.request.context
cdict = context.to_policy_values()
policy.authorize('baremetal:portgroup:delete', cdict, cdict) policy.authorize('baremetal:portgroup:delete', cdict, cdict)
if self.parent_node_ident: if self.parent_node_ident:
@ -549,6 +572,13 @@ class PortgroupsController(pecan.rest.RestController):
rpc_portgroup = api_utils.get_rpc_portgroup(portgroup_ident) rpc_portgroup = api_utils.get_rpc_portgroup(portgroup_ident)
rpc_node = objects.Node.get_by_id(pecan.request.context, rpc_node = objects.Node.get_by_id(pecan.request.context,
rpc_portgroup.node_id) rpc_portgroup.node_id)
notify.emit_start_notification(context, rpc_portgroup, 'delete',
node_uuid=rpc_node.uuid)
with notify.handle_error_notification(context, rpc_portgroup, 'delete',
node_uuid=rpc_node.uuid):
topic = pecan.request.rpcapi.get_topic_for(rpc_node) topic = pecan.request.rpcapi.get_topic_for(rpc_node)
pecan.request.rpcapi.destroy_portgroup(pecan.request.context, pecan.request.rpcapi.destroy_portgroup(context, rpc_portgroup,
rpc_portgroup, topic) topic)
notify.emit_end_notification(context, rpc_portgroup, 'delete',
node_uuid=rpc_node.uuid)

View File

@ -307,7 +307,8 @@ class PortCRUDNotification(notification.NotificationBase):
@base.IronicObjectRegistry.register @base.IronicObjectRegistry.register
class PortCRUDPayload(notification.NotificationPayloadBase): class PortCRUDPayload(notification.NotificationPayloadBase):
# Version 1.0: Initial version # Version 1.0: Initial version
VERSION = '1.0' # Version 1.1: Add "portgroup_uuid" field
VERSION = '1.1'
SCHEMA = { SCHEMA = {
'address': ('port', 'address'), 'address': ('port', 'address'),
@ -326,12 +327,13 @@ class PortCRUDPayload(notification.NotificationPayloadBase):
nullable=True), nullable=True),
'pxe_enabled': object_fields.BooleanField(nullable=True), 'pxe_enabled': object_fields.BooleanField(nullable=True),
'node_uuid': object_fields.UUIDField(), 'node_uuid': object_fields.UUIDField(),
'portgroup_uuid': object_fields.UUIDField(nullable=True),
'created_at': object_fields.DateTimeField(nullable=True), 'created_at': object_fields.DateTimeField(nullable=True),
'updated_at': object_fields.DateTimeField(nullable=True), 'updated_at': object_fields.DateTimeField(nullable=True),
'uuid': object_fields.UUIDField() 'uuid': object_fields.UUIDField()
# TODO(yuriyz): add "portgroup_uuid" field with portgroup notifications
} }
def __init__(self, port, node_uuid): def __init__(self, port, node_uuid, portgroup_uuid):
super(PortCRUDPayload, self).__init__(node_uuid=node_uuid) super(PortCRUDPayload, self).__init__(node_uuid=node_uuid,
portgroup_uuid=portgroup_uuid)
self.populate_schema(port=port) self.populate_schema(port=port)

View File

@ -23,6 +23,7 @@ from ironic.common import utils
from ironic.db import api as dbapi from ironic.db import api as dbapi
from ironic.objects import base from ironic.objects import base
from ironic.objects import fields as object_fields from ironic.objects import fields as object_fields
from ironic.objects import notification
@base.IronicObjectRegistry.register @base.IronicObjectRegistry.register
@ -280,3 +281,51 @@ class Portgroup(base.IronicObject, object_base.VersionedObjectDictCompat):
current = self.get_by_uuid(self._context, uuid=self.uuid) current = self.get_by_uuid(self._context, uuid=self.uuid)
self.obj_refresh(current) self.obj_refresh(current)
self.obj_reset_changes() self.obj_reset_changes()
@base.IronicObjectRegistry.register
class PortgroupCRUDNotification(notification.NotificationBase):
"""Notification when ironic creates, updates or deletes a portgroup."""
# Version 1.0: Initial version
VERSION = '1.0'
fields = {
'payload': object_fields.ObjectField('PortgroupCRUDPayload')
}
@base.IronicObjectRegistry.register
class PortgroupCRUDPayload(notification.NotificationPayloadBase):
# Version 1.0: Initial version
VERSION = '1.0'
SCHEMA = {
'address': ('portgroup', 'address'),
'extra': ('portgroup', 'extra'),
'mode': ('portgroup', 'mode'),
'name': ('portgroup', 'name'),
'properties': ('portgroup', 'properties'),
'standalone_ports_supported': ('portgroup',
'standalone_ports_supported'),
'created_at': ('portgroup', 'created_at'),
'updated_at': ('portgroup', 'updated_at'),
'uuid': ('portgroup', 'uuid')
}
fields = {
'address': object_fields.MACAddressField(nullable=True),
'extra': object_fields.FlexibleDictField(nullable=True),
'mode': object_fields.StringField(nullable=True),
'name': object_fields.StringField(nullable=True),
'node_uuid': object_fields.UUIDField(),
'properties': object_fields.FlexibleDictField(nullable=True),
'standalone_ports_supported': object_fields.BooleanField(
nullable=True),
'created_at': object_fields.DateTimeField(nullable=True),
'updated_at': object_fields.DateTimeField(nullable=True),
'uuid': object_fields.UUIDField()
}
def __init__(self, portgroup, node_uuid):
super(PortgroupCRUDPayload, self).__init__(node_uuid=node_uuid)
self.populate_schema(portgroup=portgroup)

View File

@ -30,16 +30,20 @@ class APINotifyTestCase(tests_base.TestCase):
self.node_notify_mock = mock.Mock() self.node_notify_mock = mock.Mock()
self.port_notify_mock = mock.Mock() self.port_notify_mock = mock.Mock()
self.chassis_notify_mock = mock.Mock() self.chassis_notify_mock = mock.Mock()
self.portgroup_notify_mock = mock.Mock()
self.node_notify_mock.__name__ = 'NodeCRUDNotification' self.node_notify_mock.__name__ = 'NodeCRUDNotification'
self.port_notify_mock.__name__ = 'PortCRUDNotification' self.port_notify_mock.__name__ = 'PortCRUDNotification'
self.chassis_notify_mock.__name__ = 'ChassisCRUDNotification' self.chassis_notify_mock.__name__ = 'ChassisCRUDNotification'
self.portgroup_notify_mock.__name__ = 'PortgroupCRUDNotification'
_notification_mocks = { _notification_mocks = {
'chassis': (self.chassis_notify_mock, 'chassis': (self.chassis_notify_mock,
notif_utils.CRUD_NOTIFY_OBJ['chassis'][1]), notif_utils.CRUD_NOTIFY_OBJ['chassis'][1]),
'node': (self.node_notify_mock, 'node': (self.node_notify_mock,
notif_utils.CRUD_NOTIFY_OBJ['node'][1]), notif_utils.CRUD_NOTIFY_OBJ['node'][1]),
'port': (self.port_notify_mock, 'port': (self.port_notify_mock,
notif_utils.CRUD_NOTIFY_OBJ['port'][1]) notif_utils.CRUD_NOTIFY_OBJ['port'][1]),
'portgroup': (self.portgroup_notify_mock,
notif_utils.CRUD_NOTIFY_OBJ['portgroup'][1])
} }
self.addCleanup(self._restore, notif_utils.CRUD_NOTIFY_OBJ.copy()) self.addCleanup(self._restore, notif_utils.CRUD_NOTIFY_OBJ.copy())
notif_utils.CRUD_NOTIFY_OBJ = _notification_mocks notif_utils.CRUD_NOTIFY_OBJ = _notification_mocks
@ -121,6 +125,7 @@ class APINotifyTestCase(tests_base.TestCase):
def test_port_notification(self): def test_port_notification(self):
node_uuid = uuidutils.generate_uuid() node_uuid = uuidutils.generate_uuid()
portgroup_uuid = uuidutils.generate_uuid()
port = obj_utils.get_test_port(self.context, port = obj_utils.get_test_port(self.context,
address='11:22:33:77:88:99', address='11:22:33:77:88:99',
local_link_connection={'a': 25}, local_link_connection={'a': 25},
@ -130,18 +135,45 @@ class APINotifyTestCase(tests_base.TestCase):
test_status = fields.NotificationStatus.SUCCESS test_status = fields.NotificationStatus.SUCCESS
notif_utils._emit_api_notification(self.context, port, 'create', notif_utils._emit_api_notification(self.context, port, 'create',
test_level, test_status, test_level, test_status,
node_uuid=node_uuid) node_uuid=node_uuid,
portgroup_uuid=portgroup_uuid)
init_kwargs = self.port_notify_mock.call_args[1] init_kwargs = self.port_notify_mock.call_args[1]
payload = init_kwargs['payload'] payload = init_kwargs['payload']
event_type = init_kwargs['event_type'] event_type = init_kwargs['event_type']
self.assertEqual('port', event_type.object) self.assertEqual('port', event_type.object)
self.assertEqual(port.uuid, payload.uuid) self.assertEqual(port.uuid, payload.uuid)
self.assertEqual(node_uuid, payload.node_uuid) self.assertEqual(node_uuid, payload.node_uuid)
self.assertEqual(portgroup_uuid, payload.portgroup_uuid)
self.assertEqual('11:22:33:77:88:99', payload.address) self.assertEqual('11:22:33:77:88:99', payload.address)
self.assertEqual({'a': 25}, payload.local_link_connection) self.assertEqual({'a': 25}, payload.local_link_connection)
self.assertEqual({'as': 34}, payload.extra) self.assertEqual({'as': 34}, payload.extra)
self.assertEqual(False, payload.pxe_enabled) self.assertEqual(False, payload.pxe_enabled)
def test_portgroup_notification(self):
node_uuid = uuidutils.generate_uuid()
portgroup = obj_utils.get_test_portgroup(self.context,
address='22:55:88:AA:BB:99',
name='new01',
mode='mode2',
extra={'bs': 11})
test_level = fields.NotificationLevel.INFO
test_status = fields.NotificationStatus.SUCCESS
notif_utils._emit_api_notification(self.context, portgroup, 'create',
test_level, test_status,
node_uuid=node_uuid)
init_kwargs = self.portgroup_notify_mock.call_args[1]
payload = init_kwargs['payload']
event_type = init_kwargs['event_type']
self.assertEqual('portgroup', event_type.object)
self.assertEqual(portgroup.uuid, payload.uuid)
self.assertEqual(node_uuid, payload.node_uuid)
self.assertEqual(portgroup.address, payload.address)
self.assertEqual(portgroup.name, payload.name)
self.assertEqual(portgroup.mode, payload.mode)
self.assertEqual(portgroup.extra, payload.extra)
self.assertEqual(portgroup.standalone_ports_supported,
payload.standalone_ports_supported)
@mock.patch('ironic.objects.node.NodeMaintenanceNotification') @mock.patch('ironic.objects.node.NodeMaintenanceNotification')
def test_node_maintenance_notification(self, maintenance_mock): def test_node_maintenance_notification(self, maintenance_mock):
maintenance_mock.__name__ = 'NodeMaintenanceNotification' maintenance_mock.__name__ = 'NodeMaintenanceNotification'

View File

@ -27,11 +27,14 @@ from wsme import types as wtypes
from ironic.api.controllers import base as api_base from ironic.api.controllers import base as api_base
from ironic.api.controllers import v1 as api_v1 from ironic.api.controllers import v1 as api_v1
from ironic.api.controllers.v1 import notification_utils
from ironic.api.controllers.v1 import portgroup as api_portgroup from ironic.api.controllers.v1 import portgroup as api_portgroup
from ironic.api.controllers.v1 import utils as api_utils from ironic.api.controllers.v1 import utils as api_utils
from ironic.common import exception from ironic.common import exception
from ironic.common import utils as common_utils from ironic.common import utils as common_utils
from ironic.conductor import rpcapi from ironic.conductor import rpcapi
from ironic import objects
from ironic.objects import fields as obj_fields
from ironic.tests import base from ironic.tests import base
from ironic.tests.unit.api import base as test_api_base from ironic.tests.unit.api import base as test_api_base
from ironic.tests.unit.api import utils as apiutils from ironic.tests.unit.api import utils as apiutils
@ -443,7 +446,8 @@ class TestPatch(test_api_base.BaseApiTest):
self.mock_gtf.return_value = 'test-topic' self.mock_gtf.return_value = 'test-topic'
self.addCleanup(p.stop) self.addCleanup(p.stop)
def test_update_byid(self, mock_upd): @mock.patch.object(notification_utils, '_emit_api_notification')
def test_update_byid(self, mock_notify, mock_upd):
extra = {'foo': 'bar'} extra = {'foo': 'bar'}
mock_upd.return_value = self.portgroup mock_upd.return_value = self.portgroup
mock_upd.return_value.extra = extra mock_upd.return_value.extra = extra
@ -458,6 +462,14 @@ class TestPatch(test_api_base.BaseApiTest):
kargs = mock_upd.call_args[0][1] kargs = mock_upd.call_args[0][1]
self.assertEqual(extra, kargs.extra) self.assertEqual(extra, kargs.extra)
mock_notify.assert_has_calls([mock.call(mock.ANY, mock.ANY, 'update',
obj_fields.NotificationLevel.INFO,
obj_fields.NotificationStatus.START,
node_uuid=self.node.uuid),
mock.call(mock.ANY, mock.ANY, 'update',
obj_fields.NotificationLevel.INFO,
obj_fields.NotificationStatus.END,
node_uuid=self.node.uuid)])
def test_update_byname(self, mock_upd): def test_update_byname(self, mock_upd):
extra = {'foo': 'bar'} extra = {'foo': 'bar'}
@ -540,7 +552,8 @@ class TestPatch(test_api_base.BaseApiTest):
kargs = mock_upd.call_args[0][1] kargs = mock_upd.call_args[0][1]
self.assertEqual(address, kargs.address) self.assertEqual(address, kargs.address)
def test_replace_address_already_exist(self, mock_upd): @mock.patch.object(notification_utils, '_emit_api_notification')
def test_replace_address_already_exist(self, mock_notify, mock_upd):
address = 'aa:aa:aa:aa:aa:aa' address = 'aa:aa:aa:aa:aa:aa'
mock_upd.side_effect = exception.MACAlreadyExists(mac=address) mock_upd.side_effect = exception.MACAlreadyExists(mac=address)
response = self.patch_json('/portgroups/%s' % self.portgroup.uuid, response = self.patch_json('/portgroups/%s' % self.portgroup.uuid,
@ -556,6 +569,14 @@ class TestPatch(test_api_base.BaseApiTest):
kargs = mock_upd.call_args[0][1] kargs = mock_upd.call_args[0][1]
self.assertEqual(address, kargs.address) self.assertEqual(address, kargs.address)
mock_notify.assert_has_calls([mock.call(mock.ANY, mock.ANY, 'update',
obj_fields.NotificationLevel.INFO,
obj_fields.NotificationStatus.START,
node_uuid=self.node.uuid),
mock.call(mock.ANY, mock.ANY, 'update',
obj_fields.NotificationLevel.ERROR,
obj_fields.NotificationStatus.ERROR,
node_uuid=self.node.uuid)])
def test_replace_node_uuid(self, mock_upd): def test_replace_node_uuid(self, mock_upd):
mock_upd.return_value = self.portgroup mock_upd.return_value = self.portgroup
@ -876,10 +897,11 @@ class TestPost(test_api_base.BaseApiTest):
super(TestPost, self).setUp() super(TestPost, self).setUp()
self.node = obj_utils.create_test_node(self.context) self.node = obj_utils.create_test_node(self.context)
@mock.patch.object(notification_utils, '_emit_api_notification')
@mock.patch.object(common_utils, 'warn_about_deprecated_extra_vif_port_id', @mock.patch.object(common_utils, 'warn_about_deprecated_extra_vif_port_id',
autospec=True) autospec=True)
@mock.patch.object(timeutils, 'utcnow', autospec=True) @mock.patch.object(timeutils, 'utcnow', autospec=True)
def test_create_portgroup(self, mock_utcnow, mock_warn): def test_create_portgroup(self, mock_utcnow, mock_warn, mock_notify):
pdict = apiutils.post_get_test_portgroup() pdict = apiutils.post_get_test_portgroup()
test_time = datetime.datetime(2000, 1, 1, 0, 0) test_time = datetime.datetime(2000, 1, 1, 0, 0)
mock_utcnow.return_value = test_time mock_utcnow.return_value = test_time
@ -899,6 +921,14 @@ class TestPost(test_api_base.BaseApiTest):
self.assertEqual(urlparse.urlparse(response.location).path, self.assertEqual(urlparse.urlparse(response.location).path,
expected_location) expected_location)
self.assertEqual(0, mock_warn.call_count) self.assertEqual(0, mock_warn.call_count)
mock_notify.assert_has_calls([mock.call(mock.ANY, mock.ANY, 'create',
obj_fields.NotificationLevel.INFO,
obj_fields.NotificationStatus.START,
node_uuid=self.node.uuid),
mock.call(mock.ANY, mock.ANY, 'create',
obj_fields.NotificationLevel.INFO,
obj_fields.NotificationStatus.END,
node_uuid=self.node.uuid)])
@mock.patch.object(timeutils, 'utcnow', autospec=True) @mock.patch.object(timeutils, 'utcnow', autospec=True)
def test_create_portgroup_v123(self, mock_utcnow): def test_create_portgroup_v123(self, mock_utcnow):
@ -942,7 +972,9 @@ class TestPost(test_api_base.BaseApiTest):
# Check that 'id' is not in first arg of positional args # Check that 'id' is not in first arg of positional args
self.assertNotIn('id', cp_mock.call_args[0][0]) self.assertNotIn('id', cp_mock.call_args[0][0])
def test_create_portgroup_generate_uuid(self): @mock.patch.object(notification_utils.LOG, 'exception', autospec=True)
@mock.patch.object(notification_utils.LOG, 'warning', autospec=True)
def test_create_portgroup_generate_uuid(self, mock_warn, mock_except):
pdict = apiutils.post_get_test_portgroup() pdict = apiutils.post_get_test_portgroup()
del pdict['uuid'] del pdict['uuid']
response = self.post_json('/portgroups', pdict, headers=self.headers) response = self.post_json('/portgroups', pdict, headers=self.headers)
@ -950,6 +982,24 @@ class TestPost(test_api_base.BaseApiTest):
headers=self.headers) headers=self.headers)
self.assertEqual(pdict['address'], result['address']) self.assertEqual(pdict['address'], result['address'])
self.assertTrue(uuidutils.is_uuid_like(result['uuid'])) self.assertTrue(uuidutils.is_uuid_like(result['uuid']))
self.assertFalse(mock_warn.called)
self.assertFalse(mock_except.called)
@mock.patch.object(notification_utils, '_emit_api_notification')
@mock.patch.object(objects.Portgroup, 'create')
def test_create_portgroup_error(self, mock_create, mock_notify):
mock_create.side_effect = Exception()
pdict = apiutils.post_get_test_portgroup()
self.post_json('/portgroups', pdict, headers=self.headers,
expect_errors=True)
mock_notify.assert_has_calls([mock.call(mock.ANY, mock.ANY, 'create',
obj_fields.NotificationLevel.INFO,
obj_fields.NotificationStatus.START,
node_uuid=self.node.uuid),
mock.call(mock.ANY, mock.ANY, 'create',
obj_fields.NotificationLevel.ERROR,
obj_fields.NotificationStatus.ERROR,
node_uuid=self.node.uuid)])
def test_create_portgroup_valid_extra(self): def test_create_portgroup_valid_extra(self):
pdict = apiutils.post_get_test_portgroup( pdict = apiutils.post_get_test_portgroup(
@ -1137,12 +1187,22 @@ class TestDelete(test_api_base.BaseApiTest):
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertIn(self.portgroup.address, response.json['error_message']) self.assertIn(self.portgroup.address, response.json['error_message'])
def test_delete_portgroup_byid(self, mock_dpt): @mock.patch.object(notification_utils, '_emit_api_notification')
def test_delete_portgroup_byid(self, mock_notify, mock_dpt):
self.delete('/portgroups/%s' % self.portgroup.uuid, self.delete('/portgroups/%s' % self.portgroup.uuid,
headers=self.headers) headers=self.headers)
self.assertTrue(mock_dpt.called) self.assertTrue(mock_dpt.called)
mock_notify.assert_has_calls([mock.call(mock.ANY, mock.ANY, 'delete',
obj_fields.NotificationLevel.INFO,
obj_fields.NotificationStatus.START,
node_uuid=self.node.uuid),
mock.call(mock.ANY, mock.ANY, 'delete',
obj_fields.NotificationLevel.INFO,
obj_fields.NotificationStatus.END,
node_uuid=self.node.uuid)])
def test_delete_portgroup_node_locked(self, mock_dpt): @mock.patch.object(notification_utils, '_emit_api_notification')
def test_delete_portgroup_node_locked(self, mock_notify, mock_dpt):
self.node.reserve(self.context, 'fake', self.node.uuid) self.node.reserve(self.context, 'fake', self.node.uuid)
mock_dpt.side_effect = exception.NodeLocked(node='fake-node', mock_dpt.side_effect = exception.NodeLocked(node='fake-node',
host='fake-host') host='fake-host')
@ -1151,6 +1211,14 @@ class TestDelete(test_api_base.BaseApiTest):
self.assertEqual(http_client.CONFLICT, ret.status_code) self.assertEqual(http_client.CONFLICT, ret.status_code)
self.assertTrue(ret.json['error_message']) self.assertTrue(ret.json['error_message'])
self.assertTrue(mock_dpt.called) self.assertTrue(mock_dpt.called)
mock_notify.assert_has_calls([mock.call(mock.ANY, mock.ANY, 'delete',
obj_fields.NotificationLevel.INFO,
obj_fields.NotificationStatus.START,
node_uuid=self.node.uuid),
mock.call(mock.ANY, mock.ANY, 'delete',
obj_fields.NotificationLevel.ERROR,
obj_fields.NotificationStatus.ERROR,
node_uuid=self.node.uuid)])
def test_delete_portgroup_invalid_api_version(self, mock_dpt): def test_delete_portgroup_invalid_api_version(self, mock_dpt):
response = self.delete('/portgroups/%s' % self.portgroup.uuid, response = self.delete('/portgroups/%s' % self.portgroup.uuid,

View File

@ -489,11 +489,13 @@ class TestPatch(test_api_base.BaseApiTest):
mock_notify.assert_has_calls([mock.call(mock.ANY, mock.ANY, 'update', mock_notify.assert_has_calls([mock.call(mock.ANY, mock.ANY, 'update',
obj_fields.NotificationLevel.INFO, obj_fields.NotificationLevel.INFO,
obj_fields.NotificationStatus.START, obj_fields.NotificationStatus.START,
node_uuid=self.node.uuid), node_uuid=self.node.uuid,
portgroup_uuid=wtypes.Unset),
mock.call(mock.ANY, mock.ANY, 'update', mock.call(mock.ANY, mock.ANY, 'update',
obj_fields.NotificationLevel.INFO, obj_fields.NotificationLevel.INFO,
obj_fields.NotificationStatus.END, obj_fields.NotificationStatus.END,
node_uuid=self.node.uuid)]) node_uuid=self.node.uuid,
portgroup_uuid=wtypes.Unset)])
def test_update_byaddress_not_allowed(self, mock_upd): def test_update_byaddress_not_allowed(self, mock_upd):
extra = {'foo': 'bar'} extra = {'foo': 'bar'}
@ -556,11 +558,13 @@ class TestPatch(test_api_base.BaseApiTest):
mock_notify.assert_has_calls([mock.call(mock.ANY, mock.ANY, 'update', mock_notify.assert_has_calls([mock.call(mock.ANY, mock.ANY, 'update',
obj_fields.NotificationLevel.INFO, obj_fields.NotificationLevel.INFO,
obj_fields.NotificationStatus.START, obj_fields.NotificationStatus.START,
node_uuid=self.node.uuid), node_uuid=self.node.uuid,
portgroup_uuid=wtypes.Unset),
mock.call(mock.ANY, mock.ANY, 'update', mock.call(mock.ANY, mock.ANY, 'update',
obj_fields.NotificationLevel.ERROR, obj_fields.NotificationLevel.ERROR,
obj_fields.NotificationStatus.ERROR, obj_fields.NotificationStatus.ERROR,
node_uuid=self.node.uuid)]) node_uuid=self.node.uuid,
portgroup_uuid=wtypes.Unset)])
def test_replace_node_uuid(self, mock_upd): def test_replace_node_uuid(self, mock_upd):
mock_upd.return_value = self.port mock_upd.return_value = self.port
@ -982,11 +986,13 @@ class TestPost(test_api_base.BaseApiTest):
mock_notify.assert_has_calls([mock.call(mock.ANY, mock.ANY, 'create', mock_notify.assert_has_calls([mock.call(mock.ANY, mock.ANY, 'create',
obj_fields.NotificationLevel.INFO, obj_fields.NotificationLevel.INFO,
obj_fields.NotificationStatus.START, obj_fields.NotificationStatus.START,
node_uuid=self.node.uuid), node_uuid=self.node.uuid,
portgroup_uuid=self.portgroup.uuid),
mock.call(mock.ANY, mock.ANY, 'create', mock.call(mock.ANY, mock.ANY, 'create',
obj_fields.NotificationLevel.INFO, obj_fields.NotificationLevel.INFO,
obj_fields.NotificationStatus.END, obj_fields.NotificationStatus.END,
node_uuid=self.node.uuid)]) node_uuid=self.node.uuid,
portgroup_uuid=self.portgroup.uuid)])
self.assertEqual(0, mock_warn.call_count) self.assertEqual(0, mock_warn.call_count)
def test_create_port_min_api_version(self): def test_create_port_min_api_version(self):
@ -1036,11 +1042,13 @@ class TestPost(test_api_base.BaseApiTest):
mock_notify.assert_has_calls([mock.call(mock.ANY, mock.ANY, 'create', mock_notify.assert_has_calls([mock.call(mock.ANY, mock.ANY, 'create',
obj_fields.NotificationLevel.INFO, obj_fields.NotificationLevel.INFO,
obj_fields.NotificationStatus.START, obj_fields.NotificationStatus.START,
node_uuid=self.node.uuid), node_uuid=self.node.uuid,
portgroup_uuid=self.portgroup.uuid),
mock.call(mock.ANY, mock.ANY, 'create', mock.call(mock.ANY, mock.ANY, 'create',
obj_fields.NotificationLevel.ERROR, obj_fields.NotificationLevel.ERROR,
obj_fields.NotificationStatus.ERROR, obj_fields.NotificationStatus.ERROR,
node_uuid=self.node.uuid)]) node_uuid=self.node.uuid,
portgroup_uuid=self.portgroup.uuid)])
def test_create_port_valid_extra(self): def test_create_port_valid_extra(self):
pdict = post_get_test_port(extra={'str': 'foo', 'int': 123, pdict = post_get_test_port(extra={'str': 'foo', 'int': 123,
@ -1396,11 +1404,13 @@ class TestDelete(test_api_base.BaseApiTest):
mock_notify.assert_has_calls([mock.call(mock.ANY, mock.ANY, 'delete', mock_notify.assert_has_calls([mock.call(mock.ANY, mock.ANY, 'delete',
obj_fields.NotificationLevel.INFO, obj_fields.NotificationLevel.INFO,
obj_fields.NotificationStatus.START, obj_fields.NotificationStatus.START,
node_uuid=self.node.uuid), node_uuid=self.node.uuid,
portgroup_uuid=None),
mock.call(mock.ANY, mock.ANY, 'delete', mock.call(mock.ANY, mock.ANY, 'delete',
obj_fields.NotificationLevel.INFO, obj_fields.NotificationLevel.INFO,
obj_fields.NotificationStatus.END, obj_fields.NotificationStatus.END,
node_uuid=self.node.uuid)]) node_uuid=self.node.uuid,
portgroup_uuid=None)])
@mock.patch.object(notification_utils, '_emit_api_notification') @mock.patch.object(notification_utils, '_emit_api_notification')
def test_delete_port_node_locked(self, mock_notify, mock_dpt): def test_delete_port_node_locked(self, mock_notify, mock_dpt):
@ -1414,11 +1424,13 @@ class TestDelete(test_api_base.BaseApiTest):
mock_notify.assert_has_calls([mock.call(mock.ANY, mock.ANY, 'delete', mock_notify.assert_has_calls([mock.call(mock.ANY, mock.ANY, 'delete',
obj_fields.NotificationLevel.INFO, obj_fields.NotificationLevel.INFO,
obj_fields.NotificationStatus.START, obj_fields.NotificationStatus.START,
node_uuid=self.node.uuid), node_uuid=self.node.uuid,
portgroup_uuid=None),
mock.call(mock.ANY, mock.ANY, 'delete', mock.call(mock.ANY, mock.ANY, 'delete',
obj_fields.NotificationLevel.ERROR, obj_fields.NotificationLevel.ERROR,
obj_fields.NotificationStatus.ERROR, obj_fields.NotificationStatus.ERROR,
node_uuid=self.node.uuid)]) node_uuid=self.node.uuid,
portgroup_uuid=None)])
def test_portgroups_subresource_delete(self, mock_dpt): def test_portgroups_subresource_delete(self, mock_dpt):
portgroup = obj_utils.create_test_portgroup(self.context, portgroup = obj_utils.create_test_portgroup(self.context,

View File

@ -25,7 +25,7 @@ from ironic.tests.unit.db import utils
from ironic.tests.unit.objects import utils as obj_utils from ironic.tests.unit.objects import utils as obj_utils
class TestChassisObject(base.DbTestCase): class TestChassisObject(base.DbTestCase, obj_utils.SchemasTestMixIn):
def setUp(self): def setUp(self):
super(TestChassisObject, self).setUp() super(TestChassisObject, self).setUp()
@ -121,21 +121,4 @@ class TestChassisObject(base.DbTestCase):
self.assertEqual(self.context, chassis[0]._context) self.assertEqual(self.context, chassis[0]._context)
def test_payload_schemas(self): def test_payload_schemas(self):
"""Assert that the chassis' Payload SCHEMAs have the expected properties. self._check_payload_schemas(objects.chassis, objects.Chassis.fields)
A payload's SCHEMA should:
1. Have each of its keys in the payload's fields
2. Have each member of the schema match with a corresponding field
in the Chassis object
"""
payloads = obj_utils.get_payloads_with_schemas(objects.chassis)
for payload in payloads:
for schema_key in payload.SCHEMA:
self.assertIn(schema_key, payload.fields,
"for %s, schema key %s is not in fields"
% (payload, schema_key))
chassis_key = payload.SCHEMA[schema_key][1]
self.assertIn(chassis_key, objects.Chassis.fields,
"for %s, schema key %s has invalid chassis "
"field %s" % (payload, schema_key, chassis_key))

View File

@ -25,7 +25,7 @@ from ironic.tests.unit.db import utils
from ironic.tests.unit.objects import utils as obj_utils from ironic.tests.unit.objects import utils as obj_utils
class TestNodeObject(base.DbTestCase): class TestNodeObject(base.DbTestCase, obj_utils.SchemasTestMixIn):
def setUp(self): def setUp(self):
super(TestNodeObject, self).setUp() super(TestNodeObject, self).setUp()
@ -244,21 +244,4 @@ class TestNodeObject(base.DbTestCase):
self.assertEqual(expect, values['properties']) self.assertEqual(expect, values['properties'])
def test_payload_schemas(self): def test_payload_schemas(self):
"""Assert that the node's Payload SCHEMAs have the expected properties. self._check_payload_schemas(objects.node, objects.Node.fields)
A payload's SCHEMA should:
1. Have each of its keys in the payload's fields
2. Have each member of the schema match with a corresponding field
in the Node object
"""
payloads = obj_utils.get_payloads_with_schemas(objects.node)
for payload in payloads:
for schema_key in payload.SCHEMA:
self.assertIn(schema_key, payload.fields,
"for %s, schema key %s is not in fields"
% (payload, schema_key))
node_key = payload.SCHEMA[schema_key][1]
self.assertIn(node_key, objects.Node.fields,
"for %s, schema key %s has invalid node field %s"
% (payload, schema_key, node_key))

View File

@ -428,9 +428,11 @@ expected_object_fingerprints = {
'NodeCRUDNotification': '1.0-59acc533c11d306f149846f922739c15', 'NodeCRUDNotification': '1.0-59acc533c11d306f149846f922739c15',
'NodeCRUDPayload': '1.1-35c16dd49d75812763e4e99bfebc3191', 'NodeCRUDPayload': '1.1-35c16dd49d75812763e4e99bfebc3191',
'PortCRUDNotification': '1.0-59acc533c11d306f149846f922739c15', 'PortCRUDNotification': '1.0-59acc533c11d306f149846f922739c15',
'PortCRUDPayload': '1.0-88acd98c9b08b4c8810e77793152057b', 'PortCRUDPayload': '1.1-1ecf2d63b68014c52cb52d0227f8b5b8',
'NodeMaintenanceNotification': '1.0-59acc533c11d306f149846f922739c15', 'NodeMaintenanceNotification': '1.0-59acc533c11d306f149846f922739c15',
'NodeConsoleNotification': '1.0-59acc533c11d306f149846f922739c15' 'NodeConsoleNotification': '1.0-59acc533c11d306f149846f922739c15',
'PortgroupCRUDNotification': '1.0-59acc533c11d306f149846f922739c15',
'PortgroupCRUDPayload': '1.0-b73c1fecf0cef3aa56bbe3c7e2275018',
} }

View File

@ -24,7 +24,7 @@ from ironic.tests.unit.db import utils
from ironic.tests.unit.objects import utils as obj_utils from ironic.tests.unit.objects import utils as obj_utils
class TestPortObject(base.DbTestCase): class TestPortObject(base.DbTestCase, obj_utils.SchemasTestMixIn):
def setUp(self): def setUp(self):
super(TestPortObject, self).setUp() super(TestPortObject, self).setUp()
@ -129,21 +129,4 @@ class TestPortObject(base.DbTestCase):
self.assertEqual(self.context, ports[0]._context) self.assertEqual(self.context, ports[0]._context)
def test_payload_schemas(self): def test_payload_schemas(self):
"""Assert that the port's Payload SCHEMAs have the expected properties. self._check_payload_schemas(objects.port, objects.Port.fields)
A payload's SCHEMA should:
1. Have each of its keys in the payload's fields
2. Have each member of the schema match with a corresponding field
in the Port object
"""
payloads = obj_utils.get_payloads_with_schemas(objects.port)
for payload in payloads:
for schema_key in payload.SCHEMA:
self.assertIn(schema_key, payload.fields,
"for %s, schema key %s is not in fields"
% (payload, schema_key))
port_key = payload.SCHEMA[schema_key][1]
self.assertIn(port_key, objects.Port.fields,
"for %s, schema key %s has invalid port field %s"
% (payload, schema_key, port_key))

View File

@ -18,9 +18,10 @@ from ironic.common import exception
from ironic import objects from ironic import objects
from ironic.tests.unit.db import base from ironic.tests.unit.db import base
from ironic.tests.unit.db import utils from ironic.tests.unit.db import utils
from ironic.tests.unit.objects import utils as obj_utils
class TestPortgroupObject(base.DbTestCase): class TestPortgroupObject(base.DbTestCase, obj_utils.SchemasTestMixIn):
def setUp(self): def setUp(self):
super(TestPortgroupObject, self).setUp() super(TestPortgroupObject, self).setUp()
@ -147,3 +148,7 @@ class TestPortgroupObject(base.DbTestCase):
self.assertThat(portgroups, matchers.HasLength(1)) self.assertThat(portgroups, matchers.HasLength(1))
self.assertIsInstance(portgroups[0], objects.Portgroup) self.assertIsInstance(portgroups[0], objects.Portgroup)
self.assertEqual(self.context, portgroups[0]._context) self.assertEqual(self.context, portgroups[0]._context)
def test_payload_schemas(self):
self._check_payload_schemas(objects.portgroup,
objects.Portgroup.fields)

View File

@ -245,3 +245,27 @@ def get_payloads_with_schemas(from_module):
payloads.append(payload) payloads.append(payload)
return payloads return payloads
class SchemasTestMixIn(object):
def _check_payload_schemas(self, from_module, fields):
"""Assert that the Payload SCHEMAs have the expected properties.
A payload's SCHEMA should:
1. Have each of its keys in the payload's fields
2. Have each member of the schema match with a corresponding field
in the object
"""
resource = from_module.__name__.split('.')[-1]
payloads = get_payloads_with_schemas(from_module)
for payload in payloads:
for schema_key in payload.SCHEMA:
self.assertIn(schema_key, payload.fields,
"for %s, schema key %s is not in fields"
% (payload, schema_key))
key = payload.SCHEMA[schema_key][1]
self.assertIn(key, fields,
"for %s, schema key %s has invalid %s "
"field %s" % (payload, schema_key, resource,
key))

View File

@ -0,0 +1,10 @@
---
features:
- |
Adds notifications for creation, updates, or deletions of port groups.
Event types are formatted as follows:
* baremetal.portgroup.{create, update, delete}.{start,end,error}
Also adds portgroup_uuid field to port notifications, port payload version
bumped to 1.1.