Add ironic resources CRUD notifications
This patch adds notifications for create, update or delete ironic resources (node, port and chassis). Event types general form are: baremetal.<resource>.{create, update, delete}.{start,end,error}. Developer documentation updated. Partial-Bug: #1606520 Change-Id: I95c64d9aa806ff2d7e7dae54ced169c98282c67d
This commit is contained in:
parent
9cd777fe2a
commit
499ef55dd1
doc/source/deploy
ironic
api/controllers/v1
conductor
objects
tests/unit
releasenotes/notes
@ -69,6 +69,142 @@ The notifications that ironic emits are described here. They are listed
|
||||
(alphabetically) by service first, then by event_type. All examples below
|
||||
show payloads before serialization to JSON.
|
||||
|
||||
------------------------
|
||||
ironic-api notifications
|
||||
------------------------
|
||||
|
||||
Resources CRUD notifications
|
||||
----------------------------
|
||||
|
||||
These notifications are emitted from API service when ironic resources are
|
||||
modified as part of create, update, or delete (CRUD) [3]_ procedures. All
|
||||
CRUD notifications are emitted at INFO level, except for "error" status that
|
||||
is emitted at ERROR level.
|
||||
|
||||
List of CRUD notifications for chassis:
|
||||
|
||||
* ``baremetal.chassis.create.start``
|
||||
* ``baremetal.chassis.create.end``
|
||||
* ``baremetal.chassis.create.error``
|
||||
* ``baremetal.chassis.update.start``
|
||||
* ``baremetal.chassis.update.end``
|
||||
* ``baremetal.chassis.update.error``
|
||||
* ``baremetal.chassis.delete.start``
|
||||
* ``baremetal.chassis.delete.end``
|
||||
* ``baremetal.chassis.delete.error``
|
||||
|
||||
Example of chassis CRUD notification::
|
||||
|
||||
{
|
||||
"priority": "info",
|
||||
"payload":{
|
||||
"ironic_object.namespace":"ironic",
|
||||
"ironic_object.name":"ChassisCRUDPayload",
|
||||
"ironic_object.version":"1.0",
|
||||
"ironic_object.data":{
|
||||
"created_at": "2016-04-10T10:13:03+00:00",
|
||||
"description": "bare 28",
|
||||
"extra": {},
|
||||
"updated_at": "2016-04-27T21:11:03+00:00",
|
||||
"uuid": "1910f669-ce8b-43c2-b1d8-cf3d65be815e",
|
||||
}
|
||||
},
|
||||
"event_type":"baremetal.chassis.update.end",
|
||||
"publisher_id":"ironic-api.hostname02"
|
||||
}
|
||||
|
||||
List of CRUD notifications for node:
|
||||
|
||||
* ``baremetal.node.create.start``
|
||||
* ``baremetal.node.create.end``
|
||||
* ``baremetal.node.create.error``
|
||||
* ``baremetal.node.update.start``
|
||||
* ``baremetal.node.update.end``
|
||||
* ``baremetal.node.update.error``
|
||||
* ``baremetal.node.delete.start``
|
||||
* ``baremetal.node.delete.end``
|
||||
* ``baremetal.node.delete.error``
|
||||
|
||||
Example of node CRUD notification::
|
||||
|
||||
{
|
||||
"priority": "info",
|
||||
"payload":{
|
||||
"ironic_object.namespace":"ironic",
|
||||
"ironic_object.name":"NodeCRUDPayload",
|
||||
"ironic_object.version":"1.0",
|
||||
"ironic_object.data":{
|
||||
"chassis_uuid": "db0eef9d-45b2-4dc0-94a8-fc283c01171f",
|
||||
"clean_step": None,
|
||||
"console_enabled": False,
|
||||
"created_at": "2016-01-26T20:41:03+00:00",
|
||||
"driver": "fake",
|
||||
"driver_info": {
|
||||
"host": "192.168.0.111"},
|
||||
"extra": {},
|
||||
"inspection_finished_at": None,
|
||||
"inspection_started_at": None,
|
||||
"instance_info": {},
|
||||
"instance_uuid": None,
|
||||
"last_error": None,
|
||||
"maintenance": False,
|
||||
"maintenance_reason": None,
|
||||
"network_interface": "flat",
|
||||
"name": None,
|
||||
"power_state": "power off",
|
||||
"properties": {
|
||||
"memory_mb": 4096,
|
||||
"cpu_arch": "x86_64",
|
||||
"local_gb": 10,
|
||||
"cpus": 8},
|
||||
"provision_state": "deploying",
|
||||
"provision_updated_at": "2016-01-27T20:41:03+00:00",
|
||||
"resource_class": None,
|
||||
"target_power_state": None,
|
||||
"target_provision_state": "active",
|
||||
"updated_at": "2016-01-27T20:41:03+00:00",
|
||||
"uuid": "1be26c0b-03f2-4d2e-ae87-c02d7f33c123",
|
||||
}
|
||||
},
|
||||
"event_type":"baremetal.node.update.end",
|
||||
"publisher_id":"ironic-api.hostname02"
|
||||
}
|
||||
|
||||
List of CRUD notifications for port:
|
||||
|
||||
* ``baremetal.port.create.start``
|
||||
* ``baremetal.port.create.end``
|
||||
* ``baremetal.port.create.error``
|
||||
* ``baremetal.port.update.start``
|
||||
* ``baremetal.port.update.end``
|
||||
* ``baremetal.port.update.error``
|
||||
* ``baremetal.port.delete.start``
|
||||
* ``baremetal.port.delete.end``
|
||||
* ``baremetal.port.delete.error``
|
||||
|
||||
Example of port CRUD notification::
|
||||
|
||||
{
|
||||
"priority": "info",
|
||||
"payload":{
|
||||
"ironic_object.namespace":"ironic",
|
||||
"ironic_object.name":"PortCRUDPayload",
|
||||
"ironic_object.version":"1.0",
|
||||
"ironic_object.data":{
|
||||
"address": "77:66:23:34:11:b7",
|
||||
"created_at": "2016-02-11T15:23:03+00:00",
|
||||
"node_uuid": "5b236cab-ad4e-4220-b57c-e827e858745a",
|
||||
"extra": {},
|
||||
"local_link_connection": {},
|
||||
"pxe_enabled": True,
|
||||
"updated_at": "2016-03-27T20:41:03+00:00",
|
||||
"uuid": "1be26c0b-03f2-4d2e-ae87-c02d7f33c123",
|
||||
}
|
||||
},
|
||||
"event_type":"baremetal.port.update.end",
|
||||
"publisher_id":"ironic-api.hostname02"
|
||||
}
|
||||
|
||||
------------------------------
|
||||
ironic-conductor notifications
|
||||
------------------------------
|
||||
@ -257,3 +393,4 @@ indicate a node's provision states before state change, "event" is the FSM
|
||||
|
||||
.. [1] https://wiki.openstack.org/wiki/LoggingStandards#Log_level_definitions
|
||||
.. [2] https://www.rabbitmq.com/documentation.html
|
||||
.. [3] https://en.wikipedia.org/wiki/Create,_read,_update_and_delete
|
||||
|
@ -16,6 +16,7 @@
|
||||
import datetime
|
||||
|
||||
from ironic_lib import metrics_utils
|
||||
from oslo_utils import uuidutils
|
||||
import pecan
|
||||
from pecan import rest
|
||||
from six.moves import http_client
|
||||
@ -26,6 +27,7 @@ from ironic.api.controllers import base
|
||||
from ironic.api.controllers import link
|
||||
from ironic.api.controllers.v1 import collection
|
||||
from ironic.api.controllers.v1 import node
|
||||
from ironic.api.controllers.v1 import notification_utils as notify
|
||||
from ironic.api.controllers.v1 import types
|
||||
from ironic.api.controllers.v1 import utils as api_utils
|
||||
from ironic.api import expose
|
||||
@ -270,12 +272,19 @@ class ChassisController(rest.RestController):
|
||||
|
||||
:param chassis: a chassis within the request body.
|
||||
"""
|
||||
cdict = pecan.request.context.to_policy_values()
|
||||
context = pecan.request.context
|
||||
cdict = context.to_policy_values()
|
||||
policy.authorize('baremetal:chassis:create', cdict, cdict)
|
||||
|
||||
new_chassis = objects.Chassis(pecan.request.context,
|
||||
**chassis.as_dict())
|
||||
new_chassis.create()
|
||||
# NOTE(yuriyz): UUID is mandatory for notifications payload
|
||||
if not chassis.uuid:
|
||||
chassis.uuid = uuidutils.generate_uuid()
|
||||
|
||||
new_chassis = objects.Chassis(context, **chassis.as_dict())
|
||||
notify.emit_start_notification(context, new_chassis, 'create')
|
||||
with notify.handle_error_notification(context, new_chassis, 'create'):
|
||||
new_chassis.create()
|
||||
notify.emit_end_notification(context, new_chassis, 'create')
|
||||
# Set the HTTP Location Header
|
||||
pecan.response.location = link.build_url('chassis', new_chassis.uuid)
|
||||
return Chassis.convert_with_links(new_chassis)
|
||||
@ -289,11 +298,11 @@ class ChassisController(rest.RestController):
|
||||
:param chassis_uuid: UUID of a chassis.
|
||||
:param patch: a json PATCH document to apply to this chassis.
|
||||
"""
|
||||
cdict = pecan.request.context.to_policy_values()
|
||||
context = pecan.request.context
|
||||
cdict = context.to_policy_values()
|
||||
policy.authorize('baremetal:chassis:update', cdict, cdict)
|
||||
|
||||
rpc_chassis = objects.Chassis.get_by_uuid(pecan.request.context,
|
||||
chassis_uuid)
|
||||
rpc_chassis = objects.Chassis.get_by_uuid(context, chassis_uuid)
|
||||
try:
|
||||
chassis = Chassis(
|
||||
**api_utils.apply_jsonpatch(rpc_chassis.as_dict(), patch))
|
||||
@ -313,7 +322,10 @@ class ChassisController(rest.RestController):
|
||||
if rpc_chassis[field] != patch_val:
|
||||
rpc_chassis[field] = patch_val
|
||||
|
||||
rpc_chassis.save()
|
||||
notify.emit_start_notification(context, rpc_chassis, 'update')
|
||||
with notify.handle_error_notification(context, rpc_chassis, 'update'):
|
||||
rpc_chassis.save()
|
||||
notify.emit_end_notification(context, rpc_chassis, 'update')
|
||||
return Chassis.convert_with_links(rpc_chassis)
|
||||
|
||||
@METRICS.timer('ChassisController.delete')
|
||||
@ -323,9 +335,12 @@ class ChassisController(rest.RestController):
|
||||
|
||||
:param chassis_uuid: UUID of a chassis.
|
||||
"""
|
||||
cdict = pecan.request.context.to_policy_values()
|
||||
context = pecan.request.context
|
||||
cdict = context.to_policy_values()
|
||||
policy.authorize('baremetal:chassis:delete', cdict, cdict)
|
||||
|
||||
rpc_chassis = objects.Chassis.get_by_uuid(pecan.request.context,
|
||||
chassis_uuid)
|
||||
rpc_chassis.destroy()
|
||||
rpc_chassis = objects.Chassis.get_by_uuid(context, chassis_uuid)
|
||||
notify.emit_start_notification(context, rpc_chassis, 'delete')
|
||||
with notify.handle_error_notification(context, rpc_chassis, 'delete'):
|
||||
rpc_chassis.destroy()
|
||||
notify.emit_end_notification(context, rpc_chassis, 'delete')
|
||||
|
@ -29,6 +29,7 @@ from wsme import types as wtypes
|
||||
from ironic.api.controllers import base
|
||||
from ironic.api.controllers import link
|
||||
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 portgroup
|
||||
from ironic.api.controllers.v1 import types
|
||||
@ -776,7 +777,9 @@ class Node(base.APIBase):
|
||||
# that as_dict() will contain chassis_id field when converting it
|
||||
# before saving it in the database.
|
||||
self.fields.append('chassis_id')
|
||||
setattr(self, 'chassis_uuid', kwargs.get('chassis_id', wtypes.Unset))
|
||||
if 'chassis_uuid' not in kwargs:
|
||||
setattr(self, 'chassis_uuid', kwargs.get('chassis_id',
|
||||
wtypes.Unset))
|
||||
|
||||
@staticmethod
|
||||
def _convert_with_links(node, url, fields=None, show_states_links=True,
|
||||
@ -1395,7 +1398,8 @@ class NodesController(rest.RestController):
|
||||
|
||||
:param node: a node within the request body.
|
||||
"""
|
||||
cdict = pecan.request.context.to_policy_values()
|
||||
context = pecan.request.context
|
||||
cdict = context.to_policy_values()
|
||||
policy.authorize('baremetal:node:create', cdict, cdict)
|
||||
|
||||
if self.from_chassis:
|
||||
@ -1431,13 +1435,19 @@ class NodesController(rest.RestController):
|
||||
self._check_names_acceptable([node.name], error_msg)
|
||||
node.provision_state = api_utils.initial_node_provision_state()
|
||||
|
||||
new_node = objects.Node(pecan.request.context,
|
||||
**node.as_dict())
|
||||
new_node = pecan.request.rpcapi.create_node(
|
||||
pecan.request.context, new_node, topic)
|
||||
new_node = objects.Node(context, **node.as_dict())
|
||||
notify.emit_start_notification(context, new_node, 'create',
|
||||
chassis_uuid=node.chassis_uuid)
|
||||
with notify.handle_error_notification(context, new_node, 'create',
|
||||
chassis_uuid=node.chassis_uuid):
|
||||
new_node = pecan.request.rpcapi.create_node(context,
|
||||
new_node, topic)
|
||||
# Set the HTTP Location Header
|
||||
pecan.response.location = link.build_url('nodes', new_node.uuid)
|
||||
return Node.convert_with_links(new_node)
|
||||
api_node = Node.convert_with_links(new_node)
|
||||
notify.emit_end_notification(context, new_node, 'create',
|
||||
chassis_uuid=api_node.chassis_uuid)
|
||||
return api_node
|
||||
|
||||
@METRICS.timer('NodesController.patch')
|
||||
@wsme.validate(types.uuid, [NodePatchType])
|
||||
@ -1448,7 +1458,8 @@ class NodesController(rest.RestController):
|
||||
:param node_ident: UUID or logical name of a node.
|
||||
:param patch: a json PATCH document to apply to this node.
|
||||
"""
|
||||
cdict = pecan.request.context.to_policy_values()
|
||||
context = pecan.request.context
|
||||
cdict = context.to_policy_values()
|
||||
policy.authorize('baremetal:node:update', cdict, cdict)
|
||||
|
||||
if self.from_chassis:
|
||||
@ -1508,10 +1519,19 @@ class NodesController(rest.RestController):
|
||||
e.code = http_client.BAD_REQUEST
|
||||
raise
|
||||
self._check_driver_changed_and_console_enabled(rpc_node, node_ident)
|
||||
new_node = pecan.request.rpcapi.update_node(
|
||||
pecan.request.context, rpc_node, topic)
|
||||
|
||||
return Node.convert_with_links(new_node)
|
||||
notify.emit_start_notification(context, rpc_node, 'update',
|
||||
chassis_uuid=node.chassis_uuid)
|
||||
with notify.handle_error_notification(context, rpc_node, 'update',
|
||||
chassis_uuid=node.chassis_uuid):
|
||||
new_node = pecan.request.rpcapi.update_node(context,
|
||||
rpc_node, topic)
|
||||
|
||||
api_node = Node.convert_with_links(new_node)
|
||||
notify.emit_end_notification(context, new_node, 'update',
|
||||
chassis_uuid=api_node.chassis_uuid)
|
||||
|
||||
return api_node
|
||||
|
||||
@METRICS.timer('NodesController.delete')
|
||||
@expose.expose(None, types.uuid_or_name,
|
||||
@ -1521,19 +1541,28 @@ class NodesController(rest.RestController):
|
||||
|
||||
:param node_ident: UUID or logical name of a node.
|
||||
"""
|
||||
cdict = pecan.request.context.to_policy_values()
|
||||
context = pecan.request.context
|
||||
cdict = context.to_policy_values()
|
||||
policy.authorize('baremetal:node:delete', cdict, cdict)
|
||||
|
||||
if self.from_chassis:
|
||||
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
|
||||
notify.emit_start_notification(context, rpc_node, 'delete',
|
||||
chassis_uuid=chassis_uuid)
|
||||
with notify.handle_error_notification(context, rpc_node, 'delete',
|
||||
chassis_uuid=chassis_uuid):
|
||||
try:
|
||||
topic = pecan.request.rpcapi.get_topic_for(rpc_node)
|
||||
except exception.NoValidHost as e:
|
||||
e.code = http_client.BAD_REQUEST
|
||||
raise
|
||||
|
||||
try:
|
||||
topic = pecan.request.rpcapi.get_topic_for(rpc_node)
|
||||
except exception.NoValidHost as e:
|
||||
e.code = http_client.BAD_REQUEST
|
||||
raise
|
||||
|
||||
pecan.request.rpcapi.destroy_node(pecan.request.context,
|
||||
rpc_node.uuid, topic)
|
||||
pecan.request.rpcapi.destroy_node(context, rpc_node.uuid, topic)
|
||||
notify.emit_end_notification(context, rpc_node, 'delete',
|
||||
chassis_uuid=chassis_uuid)
|
||||
|
150
ironic/api/controllers/v1/notification_utils.py
Normal file
150
ironic/api/controllers/v1/notification_utils.py
Normal file
@ -0,0 +1,150 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import contextlib
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log
|
||||
from oslo_messaging import exceptions as oslo_msg_exc
|
||||
from oslo_utils import excutils
|
||||
from oslo_versionedobjects import exception as oslo_vo_exc
|
||||
from wsme import types as wtypes
|
||||
|
||||
from ironic.common import exception
|
||||
from ironic.common.i18n import _
|
||||
from ironic.objects import chassis as chassis_objects
|
||||
from ironic.objects import fields
|
||||
from ironic.objects import node as node_objects
|
||||
from ironic.objects import notification
|
||||
from ironic.objects import port as port_objects
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
CRUD_NOTIFY_OBJ = {
|
||||
'chassis': (chassis_objects.ChassisCRUDNotification,
|
||||
chassis_objects.ChassisCRUDPayload),
|
||||
'node': (node_objects.NodeCRUDNotification,
|
||||
node_objects.NodeCRUDPayload),
|
||||
'port': (port_objects.PortCRUDNotification,
|
||||
port_objects.PortCRUDPayload)
|
||||
}
|
||||
|
||||
|
||||
def _emit_api_notification(context, obj, action, level, status, **kwargs):
|
||||
"""Helper for emitting API notifications.
|
||||
|
||||
:param context: request context.
|
||||
:param obj: resource rpc object.
|
||||
:param action: Action string to go in the EventType.
|
||||
:param level: Notification level. One of
|
||||
`ironic.objects.fields.NotificationLevel.ALL`
|
||||
:param status: Status to go in the EventType. One of
|
||||
`ironic.objects.fields.NotificationStatus.ALL`
|
||||
:param **kwargs: kwargs to use when creating the notification payload.
|
||||
"""
|
||||
resource = obj.__class__.__name__.lower()
|
||||
# value wsme.Unset can be passed from API representation of resource
|
||||
extra_args = {k: (v if v != wtypes.Unset else None)
|
||||
for k, v in kwargs.items()}
|
||||
try:
|
||||
try:
|
||||
if resource not in CRUD_NOTIFY_OBJ:
|
||||
notification_name = payload_name = _("is not defined")
|
||||
raise KeyError(_("Unsupported resource: %s") % resource)
|
||||
notification_method, payload_method = CRUD_NOTIFY_OBJ[resource]
|
||||
notification_name = notification_method.__name__
|
||||
payload_name = payload_method.__name__
|
||||
finally:
|
||||
# Prepare our exception message just in case
|
||||
exception_values = {"resource": resource,
|
||||
"uuid": obj.uuid,
|
||||
"action": action,
|
||||
"status": status,
|
||||
"level": level,
|
||||
"notification_method": notification_name,
|
||||
"payload_method": payload_name}
|
||||
exception_message = (_("Failed to send baremetal.%(resource)s."
|
||||
"%(action)s.%(status)s notification for "
|
||||
"%(resource)s %(uuid)s with level "
|
||||
"%(level)s, notification method "
|
||||
"%(notification_method)s, payload method "
|
||||
"%(payload_method)s, error %(error)s"))
|
||||
|
||||
payload = payload_method(obj, **extra_args)
|
||||
if resource == 'node':
|
||||
notification.mask_secrets(payload)
|
||||
notification_method(
|
||||
publisher=notification.NotificationPublisher(
|
||||
service='ironic-api', host=CONF.host),
|
||||
event_type=notification.EventType(
|
||||
object=resource, action=action, status=status),
|
||||
level=level,
|
||||
payload=payload).emit(context)
|
||||
except (exception.NotificationSchemaObjectError,
|
||||
exception.NotificationSchemaKeyError,
|
||||
exception.NotificationPayloadError,
|
||||
oslo_msg_exc.MessageDeliveryFailure,
|
||||
oslo_vo_exc.VersionedObjectsException) as e:
|
||||
exception_values['error'] = e
|
||||
LOG.warning(exception_message, exception_values)
|
||||
except Exception as e:
|
||||
exception_values['error'] = e
|
||||
LOG.exception(exception_message, exception_values)
|
||||
|
||||
|
||||
def emit_start_notification(context, obj, action, **kwargs):
|
||||
"""Helper for emitting API 'start' notifications.
|
||||
|
||||
:param context: request context.
|
||||
:param obj: resource rpc object.
|
||||
:param action: Action string to go in the EventType.
|
||||
:param **kwargs: kwargs to use when creating the notification payload.
|
||||
"""
|
||||
_emit_api_notification(context, obj, action,
|
||||
fields.NotificationLevel.INFO,
|
||||
fields.NotificationStatus.START,
|
||||
**kwargs)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def handle_error_notification(context, obj, action, **kwargs):
|
||||
"""Context manager to handle any error notifications.
|
||||
|
||||
:param context: request context.
|
||||
:param obj: resource rpc object.
|
||||
:param action: Action string to go in the EventType.
|
||||
:param **kwargs: kwargs to use when creating the notification payload.
|
||||
"""
|
||||
try:
|
||||
yield
|
||||
except Exception:
|
||||
with excutils.save_and_reraise_exception():
|
||||
_emit_api_notification(context, obj, action,
|
||||
fields.NotificationLevel.ERROR,
|
||||
fields.NotificationStatus.ERROR,
|
||||
**kwargs)
|
||||
|
||||
|
||||
def emit_end_notification(context, obj, action, **kwargs):
|
||||
"""Helper for emitting API 'end' notifications.
|
||||
|
||||
:param context: request context.
|
||||
:param obj: resource rpc object.
|
||||
:param action: Action string to go in the EventType.
|
||||
:param **kwargs: kwargs to use when creating the notification payload.
|
||||
"""
|
||||
_emit_api_notification(context, obj, action,
|
||||
fields.NotificationLevel.INFO,
|
||||
fields.NotificationStatus.END,
|
||||
**kwargs)
|
@ -26,6 +26,7 @@ from wsme import types as wtypes
|
||||
from ironic.api.controllers import base
|
||||
from ironic.api.controllers import link
|
||||
from ironic.api.controllers.v1 import collection
|
||||
from ironic.api.controllers.v1 import notification_utils as notify
|
||||
from ironic.api.controllers.v1 import types
|
||||
from ironic.api.controllers.v1 import utils as api_utils
|
||||
from ironic.api import expose
|
||||
@ -494,7 +495,8 @@ class PortsController(rest.RestController):
|
||||
:param port: a port within the request body.
|
||||
:raises: NotAcceptable, HTTPNotFound, Conflict
|
||||
"""
|
||||
cdict = pecan.request.context.to_policy_values()
|
||||
context = pecan.request.context
|
||||
cdict = context.to_policy_values()
|
||||
policy.authorize('baremetal:port:create', cdict, cdict)
|
||||
|
||||
if self.parent_node_ident or self.parent_portgroup_ident:
|
||||
@ -512,7 +514,7 @@ class PortsController(rest.RestController):
|
||||
vif = extra.get('vif_port_id') if extra else None
|
||||
if (pdict.get('portgroup_uuid') and
|
||||
(pdict.get('pxe_enabled') or vif)):
|
||||
rpc_pg = objects.Portgroup.get_by_uuid(pecan.request.context,
|
||||
rpc_pg = objects.Portgroup.get_by_uuid(context,
|
||||
pdict['portgroup_uuid'])
|
||||
if not rpc_pg.standalone_ports_supported:
|
||||
msg = _("Port group %s doesn't support standalone ports. "
|
||||
@ -522,10 +524,19 @@ class PortsController(rest.RestController):
|
||||
raise exception.Conflict(
|
||||
msg % pdict['portgroup_uuid'])
|
||||
|
||||
new_port = objects.Port(pecan.request.context,
|
||||
**pdict)
|
||||
# NOTE(yuriyz): UUID is mandatory for notifications payload
|
||||
if not pdict.get('uuid'):
|
||||
pdict['uuid'] = uuidutils.generate_uuid()
|
||||
|
||||
new_port.create()
|
||||
new_port = objects.Port(context, **pdict)
|
||||
|
||||
notify.emit_start_notification(context, new_port, 'create',
|
||||
node_uuid=port.node_uuid)
|
||||
with notify.handle_error_notification(context, new_port, 'create',
|
||||
node_uuid=port.node_uuid):
|
||||
new_port.create()
|
||||
notify.emit_end_notification(context, new_port, 'create',
|
||||
node_uuid=port.node_uuid)
|
||||
# Set the HTTP Location Header
|
||||
pecan.response.location = link.build_url('ports', new_port.uuid)
|
||||
return Port.convert_with_links(new_port)
|
||||
@ -540,7 +551,8 @@ class PortsController(rest.RestController):
|
||||
:param patch: a json PATCH document to apply to this port.
|
||||
:raises: NotAcceptable, HTTPNotFound
|
||||
"""
|
||||
cdict = pecan.request.context.to_policy_values()
|
||||
context = pecan.request.context
|
||||
cdict = context.to_policy_values()
|
||||
policy.authorize('baremetal:port:update', cdict, cdict)
|
||||
|
||||
if self.parent_node_ident or self.parent_portgroup_ident:
|
||||
@ -559,7 +571,7 @@ class PortsController(rest.RestController):
|
||||
not api_utils.allow_portgroups_subcontrollers()):
|
||||
raise exception.NotAcceptable()
|
||||
|
||||
rpc_port = objects.Port.get_by_uuid(pecan.request.context, port_uuid)
|
||||
rpc_port = objects.Port.get_by_uuid(context, port_uuid)
|
||||
try:
|
||||
port_dict = rpc_port.as_dict()
|
||||
# NOTE(lucasagomes):
|
||||
@ -591,14 +603,20 @@ class PortsController(rest.RestController):
|
||||
if rpc_port[field] != patch_val:
|
||||
rpc_port[field] = patch_val
|
||||
|
||||
rpc_node = objects.Node.get_by_id(pecan.request.context,
|
||||
rpc_port.node_id)
|
||||
topic = pecan.request.rpcapi.get_topic_for(rpc_node)
|
||||
rpc_node = objects.Node.get_by_id(context, rpc_port.node_id)
|
||||
notify.emit_start_notification(context, rpc_port, 'update',
|
||||
node_uuid=rpc_node.uuid)
|
||||
with notify.handle_error_notification(context, rpc_port, 'update',
|
||||
node_uuid=rpc_node.uuid):
|
||||
topic = pecan.request.rpcapi.get_topic_for(rpc_node)
|
||||
new_port = pecan.request.rpcapi.update_port(context, rpc_port,
|
||||
topic)
|
||||
|
||||
new_port = pecan.request.rpcapi.update_port(
|
||||
pecan.request.context, rpc_port, topic)
|
||||
api_port = Port.convert_with_links(new_port)
|
||||
notify.emit_end_notification(context, new_port, 'update',
|
||||
node_uuid=api_port.node_uuid)
|
||||
|
||||
return Port.convert_with_links(new_port)
|
||||
return api_port
|
||||
|
||||
@METRICS.timer('PortsController.delete')
|
||||
@expose.expose(None, types.uuid, status_code=http_client.NO_CONTENT)
|
||||
@ -608,16 +626,20 @@ class PortsController(rest.RestController):
|
||||
:param port_uuid: UUID of a port.
|
||||
:raises OperationNotPermitted, HTTPNotFound
|
||||
"""
|
||||
cdict = pecan.request.context.to_policy_values()
|
||||
context = pecan.request.context
|
||||
cdict = context.to_policy_values()
|
||||
policy.authorize('baremetal:port:delete', cdict, cdict)
|
||||
|
||||
if self.parent_node_ident or self.parent_portgroup_ident:
|
||||
raise exception.OperationNotPermitted()
|
||||
|
||||
rpc_port = objects.Port.get_by_uuid(pecan.request.context,
|
||||
port_uuid)
|
||||
rpc_node = objects.Node.get_by_id(pecan.request.context,
|
||||
rpc_port.node_id)
|
||||
topic = pecan.request.rpcapi.get_topic_for(rpc_node)
|
||||
pecan.request.rpcapi.destroy_port(pecan.request.context,
|
||||
rpc_port, topic)
|
||||
rpc_port = objects.Port.get_by_uuid(context, port_uuid)
|
||||
rpc_node = objects.Node.get_by_id(context, rpc_port.node_id)
|
||||
notify.emit_start_notification(context, rpc_port, 'delete',
|
||||
node_uuid=rpc_node.uuid)
|
||||
with notify.handle_error_notification(context, rpc_port, 'delete',
|
||||
node_uuid=rpc_node.uuid):
|
||||
topic = pecan.request.rpcapi.get_topic_for(rpc_node)
|
||||
pecan.request.rpcapi.destroy_port(context, rpc_port, topic)
|
||||
notify.emit_end_notification(context, rpc_port, 'delete',
|
||||
node_uuid=rpc_node.uuid)
|
||||
|
@ -13,7 +13,6 @@
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log
|
||||
from oslo_messaging import exceptions as oslo_msg_exc
|
||||
from oslo_utils import strutils
|
||||
from oslo_versionedobjects import exception as oslo_vo_exc
|
||||
|
||||
from ironic.common import exception
|
||||
@ -26,17 +25,6 @@ LOG = log.getLogger(__name__)
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
def mask_secrets(payload):
|
||||
"""Remove secrets from payload object."""
|
||||
mask = '******'
|
||||
if hasattr(payload, 'instance_info'):
|
||||
payload.instance_info = strutils.mask_dict_password(
|
||||
payload.instance_info, mask)
|
||||
if 'image_url' in payload.instance_info:
|
||||
payload.instance_info['image_url'] = mask
|
||||
# TODO(yuriyz): add "driver_info" support
|
||||
|
||||
|
||||
def _emit_conductor_node_notification(task, notification_method,
|
||||
payload_method, action,
|
||||
level, status, **kwargs):
|
||||
@ -70,7 +58,7 @@ def _emit_conductor_node_notification(task, notification_method,
|
||||
"payload_method %(payload_method)s, error "
|
||||
"%(error)s"))
|
||||
payload = payload_method(task.node, **kwargs)
|
||||
mask_secrets(payload)
|
||||
notification.mask_secrets(payload)
|
||||
notification_method(
|
||||
publisher=notification.NotificationPublisher(
|
||||
service='ironic-conductor', host=CONF.host),
|
||||
|
@ -21,6 +21,7 @@ from ironic.common import exception
|
||||
from ironic.db import api as dbapi
|
||||
from ironic.objects import base
|
||||
from ironic.objects import fields as object_fields
|
||||
from ironic.objects import notification
|
||||
|
||||
|
||||
@base.IronicObjectRegistry.register
|
||||
@ -195,3 +196,40 @@ class Chassis(base.IronicObject, object_base.VersionedObjectDictCompat):
|
||||
"""
|
||||
current = self.__class__.get_by_uuid(self._context, uuid=self.uuid)
|
||||
self.obj_refresh(current)
|
||||
|
||||
|
||||
@base.IronicObjectRegistry.register
|
||||
class ChassisCRUDNotification(notification.NotificationBase):
|
||||
"""Notification emitted when ironic creates, updates, deletes a chassis."""
|
||||
# Version 1.0: Initial version
|
||||
VERSION = '1.0'
|
||||
|
||||
fields = {
|
||||
'payload': object_fields.ObjectField('ChassisCRUDPayload')
|
||||
}
|
||||
|
||||
|
||||
@base.IronicObjectRegistry.register
|
||||
class ChassisCRUDPayload(notification.NotificationPayloadBase):
|
||||
# Version 1.0: Initial version
|
||||
VERSION = '1.0'
|
||||
|
||||
SCHEMA = {
|
||||
'description': ('chassis', 'description'),
|
||||
'extra': ('chassis', 'extra'),
|
||||
'created_at': ('chassis', 'created_at'),
|
||||
'updated_at': ('chassis', 'updated_at'),
|
||||
'uuid': ('chassis', 'uuid')
|
||||
}
|
||||
|
||||
fields = {
|
||||
'description': object_fields.StringField(nullable=True),
|
||||
'extra': object_fields.FlexibleDictField(nullable=True),
|
||||
'created_at': object_fields.DateTimeField(nullable=True),
|
||||
'updated_at': object_fields.DateTimeField(nullable=True),
|
||||
'uuid': object_fields.UUIDField()
|
||||
}
|
||||
|
||||
def __init__(self, chassis, **kwargs):
|
||||
super(ChassisCRUDPayload, self).__init__(**kwargs)
|
||||
self.populate_schema(chassis=chassis)
|
||||
|
@ -439,10 +439,11 @@ class NodePayload(notification.NotificationPayloadBase):
|
||||
# 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.
|
||||
VERSION = '1.1'
|
||||
# Version 1.2: Add nullable to console_enabled and maintenance.
|
||||
VERSION = '1.2'
|
||||
fields = {
|
||||
'clean_step': object_fields.FlexibleDictField(nullable=True),
|
||||
'console_enabled': object_fields.BooleanField(),
|
||||
'console_enabled': object_fields.BooleanField(nullable=True),
|
||||
'created_at': object_fields.DateTimeField(nullable=True),
|
||||
'driver': object_fields.StringField(nullable=True),
|
||||
'extra': object_fields.FlexibleDictField(nullable=True),
|
||||
@ -450,7 +451,7 @@ class NodePayload(notification.NotificationPayloadBase):
|
||||
'inspection_started_at': object_fields.DateTimeField(nullable=True),
|
||||
'instance_uuid': object_fields.UUIDField(nullable=True),
|
||||
'last_error': object_fields.StringField(nullable=True),
|
||||
'maintenance': object_fields.BooleanField(),
|
||||
'maintenance': object_fields.BooleanField(nullable=True),
|
||||
'maintenance_reason': object_fields.StringField(nullable=True),
|
||||
'network_interface': object_fields.StringField(nullable=True),
|
||||
'name': object_fields.StringField(nullable=True),
|
||||
@ -486,7 +487,8 @@ class NodeSetPowerStatePayload(NodePayload):
|
||||
"""Payload schema for when ironic changes a node's power state."""
|
||||
# Version 1.0: Initial version
|
||||
# Version 1.1: Parent NodePayload version 1.1
|
||||
VERSION = '1.1'
|
||||
# Version 1.2: Parent NodePayload version 1.2
|
||||
VERSION = '1.2'
|
||||
|
||||
fields = {
|
||||
# "to_power" indicates the future target_power_state of the node. A
|
||||
@ -528,7 +530,8 @@ class NodeCorrectedPowerStatePayload(NodePayload):
|
||||
"""
|
||||
# Version 1.0: Initial version
|
||||
# Version 1.1: Parent NodePayload version 1.1
|
||||
VERSION = '1.1'
|
||||
# Version 1.2: Parent NodePayload version 1.2
|
||||
VERSION = '1.2'
|
||||
|
||||
fields = {
|
||||
'from_power': object_fields.StringField(nullable=True)
|
||||
@ -555,7 +558,8 @@ class NodeSetProvisionStatePayload(NodePayload):
|
||||
"""Payload schema for when ironic changes a node provision state."""
|
||||
# Version 1.0: Initial version
|
||||
# Version 1.1: Parent NodePayload version 1.1
|
||||
VERSION = '1.1'
|
||||
# Version 1.2: Parent NodePayload version 1.2
|
||||
VERSION = '1.2'
|
||||
|
||||
SCHEMA = dict(NodePayload.SCHEMA,
|
||||
**{'instance_info': ('node', 'instance_info')})
|
||||
@ -572,3 +576,34 @@ class NodeSetProvisionStatePayload(NodePayload):
|
||||
super(NodeSetProvisionStatePayload, self).__init__(
|
||||
node, event=event, previous_provision_state=prev_state,
|
||||
previous_target_provision_state=prev_target)
|
||||
|
||||
|
||||
@base.IronicObjectRegistry.register
|
||||
class NodeCRUDNotification(notification.NotificationBase):
|
||||
"""Notification emitted when ironic creates, updates or deletes a node."""
|
||||
# Version 1.0: Initial version
|
||||
VERSION = '1.0'
|
||||
|
||||
fields = {
|
||||
'payload': object_fields.ObjectField('NodeCRUDPayload')
|
||||
}
|
||||
|
||||
|
||||
@base.IronicObjectRegistry.register
|
||||
class NodeCRUDPayload(NodePayload):
|
||||
"""Payload schema for when ironic creates, updates or deletes a node."""
|
||||
# Version 1.0: Initial version
|
||||
VERSION = '1.0'
|
||||
|
||||
SCHEMA = dict(NodePayload.SCHEMA,
|
||||
**{'instance_info': ('node', 'instance_info'),
|
||||
'driver_info': ('node', 'driver_info')})
|
||||
|
||||
fields = {
|
||||
'chassis_uuid': object_fields.UUIDField(nullable=True),
|
||||
'instance_info': object_fields.FlexibleDictField(nullable=True),
|
||||
'driver_info': object_fields.FlexibleDictField(nullable=True)
|
||||
}
|
||||
|
||||
def __init__(self, node, chassis_uuid):
|
||||
super(NodeCRUDPayload, self).__init__(node, chassis_uuid=chassis_uuid)
|
||||
|
@ -10,6 +10,7 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
from oslo_config import cfg
|
||||
from oslo_utils import strutils
|
||||
|
||||
from ironic.common import exception
|
||||
from ironic.common import rpc
|
||||
@ -182,3 +183,16 @@ class NotificationPublisher(base.IronicObject):
|
||||
'service': fields.StringField(nullable=False),
|
||||
'host': fields.StringField(nullable=False)
|
||||
}
|
||||
|
||||
|
||||
def mask_secrets(payload):
|
||||
"""Remove secrets from payload object."""
|
||||
mask = '******'
|
||||
if hasattr(payload, 'instance_info'):
|
||||
payload.instance_info = strutils.mask_dict_password(
|
||||
payload.instance_info, mask)
|
||||
if 'image_url' in payload.instance_info:
|
||||
payload.instance_info['image_url'] = mask
|
||||
if hasattr(payload, 'driver_info'):
|
||||
payload.driver_info = strutils.mask_dict_password(
|
||||
payload.driver_info, mask)
|
||||
|
@ -22,6 +22,7 @@ from ironic.common import exception
|
||||
from ironic.db import api as dbapi
|
||||
from ironic.objects import base
|
||||
from ironic.objects import fields as object_fields
|
||||
from ironic.objects import notification
|
||||
|
||||
|
||||
@base.IronicObjectRegistry.register
|
||||
@ -289,3 +290,47 @@ class Port(base.IronicObject, object_base.VersionedObjectDictCompat):
|
||||
"""
|
||||
current = self.__class__.get_by_uuid(self._context, uuid=self.uuid)
|
||||
self.obj_refresh(current)
|
||||
|
||||
|
||||
@base.IronicObjectRegistry.register
|
||||
class PortCRUDNotification(notification.NotificationBase):
|
||||
"""Notification emitted when ironic creates, updates or deletes a port."""
|
||||
# Version 1.0: Initial version
|
||||
VERSION = '1.0'
|
||||
|
||||
fields = {
|
||||
'payload': object_fields.ObjectField('PortCRUDPayload')
|
||||
}
|
||||
|
||||
|
||||
@base.IronicObjectRegistry.register
|
||||
class PortCRUDPayload(notification.NotificationPayloadBase):
|
||||
# Version 1.0: Initial version
|
||||
VERSION = '1.0'
|
||||
|
||||
SCHEMA = {
|
||||
'address': ('port', 'address'),
|
||||
'extra': ('port', 'extra'),
|
||||
'local_link_connection': ('port', 'local_link_connection'),
|
||||
'pxe_enabled': ('port', 'pxe_enabled'),
|
||||
'created_at': ('port', 'created_at'),
|
||||
'updated_at': ('port', 'updated_at'),
|
||||
'uuid': ('port', 'uuid')
|
||||
}
|
||||
|
||||
fields = {
|
||||
'address': object_fields.MACAddressField(nullable=True),
|
||||
'extra': object_fields.FlexibleDictField(nullable=True),
|
||||
'local_link_connection': object_fields.FlexibleDictField(
|
||||
nullable=True),
|
||||
'pxe_enabled': object_fields.BooleanField(nullable=True),
|
||||
'node_uuid': object_fields.UUIDField(),
|
||||
'created_at': object_fields.DateTimeField(nullable=True),
|
||||
'updated_at': object_fields.DateTimeField(nullable=True),
|
||||
'uuid': object_fields.UUIDField()
|
||||
# TODO(yuriyz): add "portgroup_uuid" field with portgroup notifications
|
||||
}
|
||||
|
||||
def __init__(self, port, node_uuid):
|
||||
super(PortCRUDPayload, self).__init__(node_uuid=node_uuid)
|
||||
self.populate_schema(port=port)
|
||||
|
@ -29,6 +29,9 @@ from wsme import types as wtypes
|
||||
from ironic.api.controllers import base as api_base
|
||||
from ironic.api.controllers import v1 as api_v1
|
||||
from ironic.api.controllers.v1 import chassis as api_chassis
|
||||
from ironic.api.controllers.v1 import notification_utils
|
||||
from ironic import objects
|
||||
from ironic.objects import fields as obj_fields
|
||||
from ironic.tests import base
|
||||
from ironic.tests.unit.api import base as test_api_base
|
||||
from ironic.tests.unit.api import utils as apiutils
|
||||
@ -252,8 +255,9 @@ class TestPatch(test_api_base.BaseApiTest):
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
self.assertTrue(response.json['error_message'])
|
||||
|
||||
@mock.patch.object(notification_utils, '_emit_api_notification')
|
||||
@mock.patch.object(timeutils, 'utcnow')
|
||||
def test_replace_singular(self, mock_utcnow):
|
||||
def test_replace_singular(self, mock_utcnow, mock_notify):
|
||||
chassis = obj_utils.get_test_chassis(self.context)
|
||||
description = 'chassis-new-description'
|
||||
test_time = datetime.datetime(2000, 1, 1, 0, 0)
|
||||
@ -269,6 +273,27 @@ class TestPatch(test_api_base.BaseApiTest):
|
||||
return_updated_at = timeutils.parse_isotime(
|
||||
result['updated_at']).replace(tzinfo=None)
|
||||
self.assertEqual(test_time, return_updated_at)
|
||||
mock_notify.assert_has_calls([mock.call(mock.ANY, mock.ANY, 'update',
|
||||
obj_fields.NotificationLevel.INFO,
|
||||
obj_fields.NotificationStatus.START),
|
||||
mock.call(mock.ANY, mock.ANY, 'update',
|
||||
obj_fields.NotificationLevel.INFO,
|
||||
obj_fields.NotificationStatus.END)])
|
||||
|
||||
@mock.patch.object(notification_utils, '_emit_api_notification')
|
||||
@mock.patch.object(objects.Chassis, 'save')
|
||||
def test_update_error(self, mock_save, mock_notify):
|
||||
mock_save.side_effect = Exception()
|
||||
chassis = obj_utils.get_test_chassis(self.context)
|
||||
self.patch_json('/chassis/%s' % chassis.uuid, [{'path': '/description',
|
||||
'value': 'new', 'op': 'replace'}],
|
||||
expect_errors=True)
|
||||
mock_notify.assert_has_calls([mock.call(mock.ANY, mock.ANY, 'update',
|
||||
obj_fields.NotificationLevel.INFO,
|
||||
obj_fields.NotificationStatus.START),
|
||||
mock.call(mock.ANY, mock.ANY, 'update',
|
||||
obj_fields.NotificationLevel.ERROR,
|
||||
obj_fields.NotificationStatus.ERROR)])
|
||||
|
||||
def test_replace_multi(self):
|
||||
extra = {"foo1": "bar1", "foo2": "bar2", "foo3": "bar3"}
|
||||
@ -386,8 +411,9 @@ class TestPatch(test_api_base.BaseApiTest):
|
||||
|
||||
class TestPost(test_api_base.BaseApiTest):
|
||||
|
||||
@mock.patch.object(notification_utils, '_emit_api_notification')
|
||||
@mock.patch.object(timeutils, 'utcnow')
|
||||
def test_create_chassis(self, mock_utcnow):
|
||||
def test_create_chassis(self, mock_utcnow, mock_notify):
|
||||
cdict = apiutils.chassis_post_data()
|
||||
test_time = datetime.datetime(2000, 1, 1, 0, 0)
|
||||
mock_utcnow.return_value = test_time
|
||||
@ -405,6 +431,25 @@ class TestPost(test_api_base.BaseApiTest):
|
||||
expected_location = '/v1/chassis/%s' % cdict['uuid']
|
||||
self.assertEqual(urlparse.urlparse(response.location).path,
|
||||
expected_location)
|
||||
mock_notify.assert_has_calls([mock.call(mock.ANY, mock.ANY, 'create',
|
||||
obj_fields.NotificationLevel.INFO,
|
||||
obj_fields.NotificationStatus.START),
|
||||
mock.call(mock.ANY, mock.ANY, 'create',
|
||||
obj_fields.NotificationLevel.INFO,
|
||||
obj_fields.NotificationStatus.END)])
|
||||
|
||||
@mock.patch.object(notification_utils, '_emit_api_notification')
|
||||
@mock.patch.object(objects.Chassis, 'create')
|
||||
def test_create_chassis_error(self, mock_save, mock_notify):
|
||||
mock_save.side_effect = Exception()
|
||||
cdict = apiutils.chassis_post_data()
|
||||
self.post_json('/chassis', cdict, expect_errors=True)
|
||||
mock_notify.assert_has_calls([mock.call(mock.ANY, mock.ANY, 'create',
|
||||
obj_fields.NotificationLevel.INFO,
|
||||
obj_fields.NotificationStatus.START),
|
||||
mock.call(mock.ANY, mock.ANY, 'create',
|
||||
obj_fields.NotificationLevel.ERROR,
|
||||
obj_fields.NotificationStatus.ERROR)])
|
||||
|
||||
def test_create_chassis_doesnt_contain_id(self):
|
||||
with mock.patch.object(self.dbapi, 'create_chassis',
|
||||
@ -417,7 +462,9 @@ class TestPost(test_api_base.BaseApiTest):
|
||||
# Check that 'id' is not in first arg of positional args
|
||||
self.assertNotIn('id', cc_mock.call_args[0][0])
|
||||
|
||||
def test_create_chassis_generate_uuid(self):
|
||||
@mock.patch.object(notification_utils.LOG, 'exception', autospec=True)
|
||||
@mock.patch.object(notification_utils.LOG, 'warning', autospec=True)
|
||||
def test_create_chassis_generate_uuid(self, mock_warning, mock_exception):
|
||||
cdict = apiutils.chassis_post_data()
|
||||
del cdict['uuid']
|
||||
self.post_json('/chassis', cdict)
|
||||
@ -425,6 +472,8 @@ class TestPost(test_api_base.BaseApiTest):
|
||||
self.assertEqual(cdict['description'],
|
||||
result['chassis'][0]['description'])
|
||||
self.assertTrue(uuidutils.is_uuid_like(result['chassis'][0]['uuid']))
|
||||
self.assertFalse(mock_warning.called)
|
||||
self.assertFalse(mock_exception.called)
|
||||
|
||||
def test_post_nodes_subresource(self):
|
||||
chassis = obj_utils.create_test_chassis(self.context)
|
||||
@ -472,7 +521,8 @@ class TestPost(test_api_base.BaseApiTest):
|
||||
|
||||
class TestDelete(test_api_base.BaseApiTest):
|
||||
|
||||
def test_delete_chassis(self):
|
||||
@mock.patch.object(notification_utils, '_emit_api_notification')
|
||||
def test_delete_chassis(self, mock_notify):
|
||||
chassis = obj_utils.create_test_chassis(self.context)
|
||||
self.delete('/chassis/%s' % chassis.uuid)
|
||||
response = self.get_json('/chassis/%s' % chassis.uuid,
|
||||
@ -480,8 +530,15 @@ class TestDelete(test_api_base.BaseApiTest):
|
||||
self.assertEqual(http_client.NOT_FOUND, response.status_int)
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
self.assertTrue(response.json['error_message'])
|
||||
mock_notify.assert_has_calls([mock.call(mock.ANY, mock.ANY, 'delete',
|
||||
obj_fields.NotificationLevel.INFO,
|
||||
obj_fields.NotificationStatus.START),
|
||||
mock.call(mock.ANY, mock.ANY, 'delete',
|
||||
obj_fields.NotificationLevel.INFO,
|
||||
obj_fields.NotificationStatus.END)])
|
||||
|
||||
def test_delete_chassis_with_node(self):
|
||||
@mock.patch.object(notification_utils, '_emit_api_notification')
|
||||
def test_delete_chassis_with_node(self, mock_notify):
|
||||
chassis = obj_utils.create_test_chassis(self.context)
|
||||
obj_utils.create_test_node(self.context, chassis_id=chassis.id)
|
||||
response = self.delete('/chassis/%s' % chassis.uuid,
|
||||
@ -490,6 +547,12 @@ class TestDelete(test_api_base.BaseApiTest):
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
self.assertTrue(response.json['error_message'])
|
||||
self.assertIn(chassis.uuid, response.json['error_message'])
|
||||
mock_notify.assert_has_calls([mock.call(mock.ANY, mock.ANY, 'delete',
|
||||
obj_fields.NotificationLevel.INFO,
|
||||
obj_fields.NotificationStatus.START),
|
||||
mock.call(mock.ANY, mock.ANY, 'delete',
|
||||
obj_fields.NotificationLevel.ERROR,
|
||||
obj_fields.NotificationStatus.ERROR)])
|
||||
|
||||
def test_delete_chassis_not_found(self):
|
||||
uuid = uuidutils.generate_uuid()
|
||||
|
@ -31,6 +31,7 @@ from wsme import types as wtypes
|
||||
from ironic.api.controllers import base as api_base
|
||||
from ironic.api.controllers import v1 as api_v1
|
||||
from ironic.api.controllers.v1 import node as api_node
|
||||
from ironic.api.controllers.v1 import notification_utils
|
||||
from ironic.api.controllers.v1 import utils as api_utils
|
||||
from ironic.api.controllers.v1 import versions
|
||||
from ironic.common import boot_devices
|
||||
@ -39,6 +40,7 @@ from ironic.common import exception
|
||||
from ironic.common import states
|
||||
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.unit.api import base as test_api_base
|
||||
from ironic.tests.unit.api import utils as test_api_utils
|
||||
@ -1077,7 +1079,8 @@ class TestPatch(test_api_base.BaseApiTest):
|
||||
self.mock_cnps = p.start()
|
||||
self.addCleanup(p.stop)
|
||||
|
||||
def test_update_ok(self):
|
||||
@mock.patch.object(notification_utils, '_emit_api_notification')
|
||||
def test_update_ok(self, mock_notify):
|
||||
self.mock_update_node.return_value = self.node
|
||||
(self
|
||||
.mock_update_node
|
||||
@ -1094,6 +1097,14 @@ class TestPatch(test_api_base.BaseApiTest):
|
||||
timeutils.parse_isotime(response.json['updated_at']))
|
||||
self.mock_update_node.assert_called_once_with(
|
||||
mock.ANY, mock.ANY, 'test-topic')
|
||||
mock_notify.assert_has_calls([mock.call(mock.ANY, mock.ANY, 'update',
|
||||
obj_fields.NotificationLevel.INFO,
|
||||
obj_fields.NotificationStatus.START,
|
||||
chassis_uuid=self.chassis.uuid),
|
||||
mock.call(mock.ANY, mock.ANY, 'update',
|
||||
obj_fields.NotificationLevel.INFO,
|
||||
obj_fields.NotificationStatus.END,
|
||||
chassis_uuid=self.chassis.uuid)])
|
||||
|
||||
def test_update_by_name_unsupported(self):
|
||||
self.mock_update_node.return_value = self.node
|
||||
@ -1137,7 +1148,8 @@ class TestPatch(test_api_base.BaseApiTest):
|
||||
self.assertEqual(http_client.BAD_REQUEST, response.status_code)
|
||||
self.assertTrue(response.json['error_message'])
|
||||
|
||||
def test_update_fails_bad_driver_info(self):
|
||||
@mock.patch.object(notification_utils, '_emit_api_notification')
|
||||
def test_update_fails_bad_driver_info(self, mock_notify):
|
||||
fake_err = 'Fake Error Message'
|
||||
self.mock_update_node.side_effect = (
|
||||
exception.InvalidParameterValue(fake_err))
|
||||
@ -1155,6 +1167,14 @@ class TestPatch(test_api_base.BaseApiTest):
|
||||
|
||||
self.mock_update_node.assert_called_once_with(
|
||||
mock.ANY, mock.ANY, 'test-topic')
|
||||
mock_notify.assert_has_calls([mock.call(mock.ANY, mock.ANY, 'update',
|
||||
obj_fields.NotificationLevel.INFO,
|
||||
obj_fields.NotificationStatus.START,
|
||||
chassis_uuid=self.chassis.uuid),
|
||||
mock.call(mock.ANY, mock.ANY, 'update',
|
||||
obj_fields.NotificationLevel.ERROR,
|
||||
obj_fields.NotificationStatus.ERROR,
|
||||
chassis_uuid=self.chassis.uuid)])
|
||||
|
||||
def test_update_fails_bad_driver(self):
|
||||
self.mock_gtf.side_effect = exception.NoValidHost('Fake Error')
|
||||
@ -1765,8 +1785,12 @@ class TestPost(test_api_base.BaseApiTest):
|
||||
expected_location)
|
||||
return result
|
||||
|
||||
def test_create_node(self):
|
||||
@mock.patch.object(notification_utils.LOG, 'exception', autospec=True)
|
||||
@mock.patch.object(notification_utils.LOG, 'warning', autospec=True)
|
||||
def test_create_node(self, mock_warning, mock_exception):
|
||||
self._test_create_node()
|
||||
self.assertFalse(mock_warning.called)
|
||||
self.assertFalse(mock_exception.called)
|
||||
|
||||
def test_create_node_chassis_uuid_always_in_response(self):
|
||||
result = self._test_create_node(chassis_uuid=None)
|
||||
@ -2037,7 +2061,8 @@ class TestPost(test_api_base.BaseApiTest):
|
||||
self.assertEqual(urlparse.urlparse(response.location).path,
|
||||
expected_location)
|
||||
|
||||
def test_create_node_with_chassis_uuid(self):
|
||||
@mock.patch.object(notification_utils, '_emit_api_notification')
|
||||
def test_create_node_with_chassis_uuid(self, mock_notify):
|
||||
ndict = test_api_utils.post_get_test_node(
|
||||
chassis_uuid=self.chassis.uuid)
|
||||
response = self.post_json('/nodes', ndict)
|
||||
@ -2050,6 +2075,14 @@ class TestPost(test_api_base.BaseApiTest):
|
||||
expected_location = '/v1/nodes/%s' % ndict['uuid']
|
||||
self.assertEqual(urlparse.urlparse(response.location).path,
|
||||
expected_location)
|
||||
mock_notify.assert_has_calls([mock.call(mock.ANY, mock.ANY, 'create',
|
||||
obj_fields.NotificationLevel.INFO,
|
||||
obj_fields.NotificationStatus.START,
|
||||
chassis_uuid=self.chassis.uuid),
|
||||
mock.call(mock.ANY, mock.ANY, 'create',
|
||||
obj_fields.NotificationLevel.INFO,
|
||||
obj_fields.NotificationStatus.END,
|
||||
chassis_uuid=self.chassis.uuid)])
|
||||
|
||||
def test_create_node_chassis_uuid_not_found(self):
|
||||
ndict = test_api_utils.post_get_test_node(
|
||||
@ -2145,11 +2178,20 @@ class TestDelete(test_api_base.BaseApiTest):
|
||||
self.mock_gtf.return_value = 'test-topic'
|
||||
self.addCleanup(p.stop)
|
||||
|
||||
@mock.patch.object(notification_utils, '_emit_api_notification')
|
||||
@mock.patch.object(rpcapi.ConductorAPI, 'destroy_node')
|
||||
def test_delete_node(self, mock_dn):
|
||||
def test_delete_node(self, mock_dn, mock_notify):
|
||||
node = obj_utils.create_test_node(self.context)
|
||||
self.delete('/nodes/%s' % node.uuid)
|
||||
mock_dn.assert_called_once_with(mock.ANY, node.uuid, 'test-topic')
|
||||
mock_notify.assert_has_calls([mock.call(mock.ANY, mock.ANY, 'delete',
|
||||
obj_fields.NotificationLevel.INFO,
|
||||
obj_fields.NotificationStatus.START,
|
||||
chassis_uuid=None),
|
||||
mock.call(mock.ANY, mock.ANY, 'delete',
|
||||
obj_fields.NotificationLevel.INFO,
|
||||
obj_fields.NotificationStatus.END,
|
||||
chassis_uuid=None)])
|
||||
|
||||
@mock.patch.object(rpcapi.ConductorAPI, 'destroy_node')
|
||||
def test_delete_node_by_name_unsupported(self, mock_dn):
|
||||
@ -2224,8 +2266,9 @@ class TestDelete(test_api_base.BaseApiTest):
|
||||
headers={'X-OpenStack-Ironic-API-Version': '1.24'})
|
||||
self.assertEqual(http_client.FORBIDDEN, response.status_int)
|
||||
|
||||
@mock.patch.object(notification_utils, '_emit_api_notification')
|
||||
@mock.patch.object(rpcapi.ConductorAPI, 'destroy_node')
|
||||
def test_delete_associated(self, mock_dn):
|
||||
def test_delete_associated(self, mock_dn, mock_notify):
|
||||
node = obj_utils.create_test_node(
|
||||
self.context,
|
||||
instance_uuid='aaaaaaaa-1111-bbbb-2222-cccccccccccc')
|
||||
@ -2235,6 +2278,14 @@ class TestDelete(test_api_base.BaseApiTest):
|
||||
response = self.delete('/nodes/%s' % node.uuid, expect_errors=True)
|
||||
self.assertEqual(http_client.CONFLICT, response.status_int)
|
||||
mock_dn.assert_called_once_with(mock.ANY, node.uuid, 'test-topic')
|
||||
mock_notify.assert_has_calls([mock.call(mock.ANY, mock.ANY, 'delete',
|
||||
obj_fields.NotificationLevel.INFO,
|
||||
obj_fields.NotificationStatus.START,
|
||||
chassis_uuid=None),
|
||||
mock.call(mock.ANY, mock.ANY, 'delete',
|
||||
obj_fields.NotificationLevel.ERROR,
|
||||
obj_fields.NotificationStatus.ERROR,
|
||||
chassis_uuid=None)])
|
||||
|
||||
@mock.patch.object(objects.Node, 'get_by_uuid')
|
||||
@mock.patch.object(rpcapi.ConductorAPI, 'update_node')
|
||||
|
143
ironic/tests/unit/api/v1/test_notification_utils.py
Normal file
143
ironic/tests/unit/api/v1/test_notification_utils.py
Normal file
@ -0,0 +1,143 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""Test class for ironic-api notification utilities."""
|
||||
|
||||
import mock
|
||||
from oslo_utils import uuidutils
|
||||
from wsme import types as wtypes
|
||||
|
||||
from ironic.api.controllers.v1 import notification_utils as notif_utils
|
||||
from ironic.objects import fields
|
||||
from ironic.objects import notification
|
||||
from ironic.tests import base as tests_base
|
||||
from ironic.tests.unit.objects import utils as obj_utils
|
||||
|
||||
|
||||
class CRUDNotifyTestCase(tests_base.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(CRUDNotifyTestCase, self).setUp()
|
||||
self.node_notify_mock = mock.Mock()
|
||||
self.port_notify_mock = mock.Mock()
|
||||
self.chassis_notify_mock = mock.Mock()
|
||||
self.node_notify_mock.__name__ = 'NodeCRUDNotification'
|
||||
self.port_notify_mock.__name__ = 'PortCRUDNotification'
|
||||
self.chassis_notify_mock.__name__ = 'ChassisCRUDNotification'
|
||||
_notification_mocks = {
|
||||
'chassis': (self.chassis_notify_mock,
|
||||
notif_utils.CRUD_NOTIFY_OBJ['chassis'][1]),
|
||||
'node': (self.node_notify_mock,
|
||||
notif_utils.CRUD_NOTIFY_OBJ['node'][1]),
|
||||
'port': (self.port_notify_mock,
|
||||
notif_utils.CRUD_NOTIFY_OBJ['port'][1])
|
||||
}
|
||||
self.addCleanup(self._restore, notif_utils.CRUD_NOTIFY_OBJ.copy())
|
||||
notif_utils.CRUD_NOTIFY_OBJ = _notification_mocks
|
||||
|
||||
def _restore(self, value):
|
||||
notif_utils.CRUD_NOTIFY_OBJ = value
|
||||
|
||||
def test_common_params(self):
|
||||
self.config(host='fake-host')
|
||||
node = obj_utils.get_test_node(self.context)
|
||||
test_level = fields.NotificationLevel.INFO
|
||||
test_status = fields.NotificationStatus.SUCCESS
|
||||
notif_utils._emit_api_notification(self.context, node, 'create',
|
||||
test_level, test_status,
|
||||
chassis_uuid=None)
|
||||
init_kwargs = self.node_notify_mock.call_args[1]
|
||||
publisher = init_kwargs['publisher']
|
||||
event_type = init_kwargs['event_type']
|
||||
level = init_kwargs['level']
|
||||
self.assertEqual('fake-host', publisher.host)
|
||||
self.assertEqual('ironic-api', publisher.service)
|
||||
self.assertEqual('create', event_type.action)
|
||||
self.assertEqual(test_status, event_type.status)
|
||||
self.assertEqual(test_level, level)
|
||||
|
||||
def test_node_notification(self):
|
||||
chassis_uuid = uuidutils.generate_uuid()
|
||||
node = obj_utils.get_test_node(self.context,
|
||||
instance_info={'foo': 'baz'},
|
||||
driver_info={'param': 104})
|
||||
test_level = fields.NotificationLevel.INFO
|
||||
test_status = fields.NotificationStatus.SUCCESS
|
||||
notif_utils._emit_api_notification(self.context, node, 'create',
|
||||
test_level, test_status,
|
||||
chassis_uuid=chassis_uuid)
|
||||
init_kwargs = self.node_notify_mock.call_args[1]
|
||||
payload = init_kwargs['payload']
|
||||
event_type = init_kwargs['event_type']
|
||||
self.assertEqual('node', event_type.object)
|
||||
self.assertEqual(node.uuid, payload.uuid)
|
||||
self.assertEqual({'foo': 'baz'}, payload.instance_info)
|
||||
self.assertEqual({'param': 104}, payload.driver_info)
|
||||
self.assertEqual(chassis_uuid, payload.chassis_uuid)
|
||||
|
||||
def test_node_notification_mask_secrets(self):
|
||||
test_info = {'password': 'secret123', 'some_value': 'fake-value'}
|
||||
node = obj_utils.get_test_node(self.context,
|
||||
driver_info=test_info)
|
||||
notification.mask_secrets(node)
|
||||
self.assertEqual('******', node.driver_info['password'])
|
||||
self.assertEqual('fake-value', node.driver_info['some_value'])
|
||||
|
||||
def test_notification_uuid_unset(self):
|
||||
node = obj_utils.get_test_node(self.context)
|
||||
test_level = fields.NotificationLevel.INFO
|
||||
test_status = fields.NotificationStatus.SUCCESS
|
||||
notif_utils._emit_api_notification(self.context, node, 'create',
|
||||
test_level, test_status,
|
||||
chassis_uuid=wtypes.Unset)
|
||||
init_kwargs = self.node_notify_mock.call_args[1]
|
||||
payload = init_kwargs['payload']
|
||||
self.assertIsNone(payload.chassis_uuid)
|
||||
|
||||
def test_chassis_notification(self):
|
||||
chassis = obj_utils.get_test_chassis(self.context,
|
||||
extra={'foo': 'boo'},
|
||||
description='bare01')
|
||||
test_level = fields.NotificationLevel.INFO
|
||||
test_status = fields.NotificationStatus.SUCCESS
|
||||
notif_utils._emit_api_notification(self.context, chassis, 'create',
|
||||
test_level, test_status)
|
||||
init_kwargs = self.chassis_notify_mock.call_args[1]
|
||||
payload = init_kwargs['payload']
|
||||
event_type = init_kwargs['event_type']
|
||||
self.assertEqual('chassis', event_type.object)
|
||||
self.assertEqual(chassis.uuid, payload.uuid)
|
||||
self.assertEqual({'foo': 'boo'}, payload.extra)
|
||||
self.assertEqual('bare01', payload.description)
|
||||
|
||||
def test_port_notification(self):
|
||||
node_uuid = uuidutils.generate_uuid()
|
||||
port = obj_utils.get_test_port(self.context,
|
||||
address='11:22:33:77:88:99',
|
||||
local_link_connection={'a': 25},
|
||||
extra={'as': 34},
|
||||
pxe_enabled=False)
|
||||
test_level = fields.NotificationLevel.INFO
|
||||
test_status = fields.NotificationStatus.SUCCESS
|
||||
notif_utils._emit_api_notification(self.context, port, 'create',
|
||||
test_level, test_status,
|
||||
node_uuid=node_uuid)
|
||||
init_kwargs = self.port_notify_mock.call_args[1]
|
||||
payload = init_kwargs['payload']
|
||||
event_type = init_kwargs['event_type']
|
||||
self.assertEqual('port', event_type.object)
|
||||
self.assertEqual(port.uuid, payload.uuid)
|
||||
self.assertEqual(node_uuid, payload.node_uuid)
|
||||
self.assertEqual('11:22:33:77:88:99', payload.address)
|
||||
self.assertEqual({'a': 25}, payload.local_link_connection)
|
||||
self.assertEqual({'as': 34}, payload.extra)
|
||||
self.assertEqual(False, payload.pxe_enabled)
|
@ -29,11 +29,14 @@ from wsme import types as wtypes
|
||||
|
||||
from ironic.api.controllers import base as api_base
|
||||
from ironic.api.controllers import v1 as api_v1
|
||||
from ironic.api.controllers.v1 import notification_utils
|
||||
from ironic.api.controllers.v1 import port as api_port
|
||||
from ironic.api.controllers.v1 import utils as api_utils
|
||||
from ironic.api.controllers.v1 import versions
|
||||
from ironic.common import exception
|
||||
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.unit.api import base as test_api_base
|
||||
from ironic.tests.unit.api import utils as apiutils
|
||||
@ -467,7 +470,8 @@ class TestPatch(test_api_base.BaseApiTest):
|
||||
self.mock_gtf.return_value = 'test-topic'
|
||||
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'}
|
||||
mock_upd.return_value = self.port
|
||||
mock_upd.return_value.extra = extra
|
||||
@ -481,6 +485,14 @@ class TestPatch(test_api_base.BaseApiTest):
|
||||
|
||||
kargs = mock_upd.call_args[0][1]
|
||||
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_byaddress_not_allowed(self, mock_upd):
|
||||
extra = {'foo': 'bar'}
|
||||
@ -524,7 +536,8 @@ class TestPatch(test_api_base.BaseApiTest):
|
||||
kargs = mock_upd.call_args[0][1]
|
||||
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'
|
||||
mock_upd.side_effect = exception.MACAlreadyExists(mac=address)
|
||||
response = self.patch_json('/ports/%s' % self.port.uuid,
|
||||
@ -539,6 +552,14 @@ class TestPatch(test_api_base.BaseApiTest):
|
||||
|
||||
kargs = mock_upd.call_args[0][1]
|
||||
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):
|
||||
mock_upd.return_value = self.port
|
||||
@ -935,8 +956,9 @@ class TestPost(test_api_base.BaseApiTest):
|
||||
self.headers = {api_base.Version.string: str(
|
||||
versions.MAX_VERSION_STRING)}
|
||||
|
||||
@mock.patch.object(notification_utils, '_emit_api_notification')
|
||||
@mock.patch.object(timeutils, 'utcnow')
|
||||
def test_create_port(self, mock_utcnow):
|
||||
def test_create_port(self, mock_utcnow, mock_notify):
|
||||
pdict = post_get_test_port()
|
||||
test_time = datetime.datetime(2000, 1, 1, 0, 0)
|
||||
mock_utcnow.return_value = test_time
|
||||
@ -954,6 +976,14 @@ class TestPost(test_api_base.BaseApiTest):
|
||||
expected_location = '/v1/ports/%s' % pdict['uuid']
|
||||
self.assertEqual(urlparse.urlparse(response.location).path,
|
||||
expected_location)
|
||||
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)])
|
||||
|
||||
def test_create_port_min_api_version(self):
|
||||
pdict = post_get_test_port(
|
||||
@ -979,7 +1009,9 @@ class TestPost(test_api_base.BaseApiTest):
|
||||
# Check that 'id' is not in first arg of positional args
|
||||
self.assertNotIn('id', cp_mock.call_args[0][0])
|
||||
|
||||
def test_create_port_generate_uuid(self):
|
||||
@mock.patch.object(notification_utils.LOG, 'exception', autospec=True)
|
||||
@mock.patch.object(notification_utils.LOG, 'warning', autospec=True)
|
||||
def test_create_port_generate_uuid(self, mock_warning, mock_exception):
|
||||
pdict = post_get_test_port()
|
||||
del pdict['uuid']
|
||||
response = self.post_json('/ports', pdict, headers=self.headers)
|
||||
@ -987,6 +1019,24 @@ class TestPost(test_api_base.BaseApiTest):
|
||||
headers=self.headers)
|
||||
self.assertEqual(pdict['address'], result['address'])
|
||||
self.assertTrue(uuidutils.is_uuid_like(result['uuid']))
|
||||
self.assertFalse(mock_warning.called)
|
||||
self.assertFalse(mock_exception.called)
|
||||
|
||||
@mock.patch.object(notification_utils, '_emit_api_notification')
|
||||
@mock.patch.object(objects.Port, 'create')
|
||||
def test_create_port_error(self, mock_create, mock_notify):
|
||||
mock_create.side_effect = Exception()
|
||||
pdict = post_get_test_port()
|
||||
self.post_json('/ports', 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_port_valid_extra(self):
|
||||
pdict = post_get_test_port(extra={'str': 'foo', 'int': 123,
|
||||
@ -1325,11 +1375,21 @@ class TestDelete(test_api_base.BaseApiTest):
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
self.assertIn(self.port.address, response.json['error_message'])
|
||||
|
||||
def test_delete_port_byid(self, mock_dpt):
|
||||
@mock.patch.object(notification_utils, '_emit_api_notification')
|
||||
def test_delete_port_byid(self, mock_notify, mock_dpt):
|
||||
self.delete('/ports/%s' % self.port.uuid, expect_errors=True)
|
||||
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_port_node_locked(self, mock_dpt):
|
||||
@mock.patch.object(notification_utils, '_emit_api_notification')
|
||||
def test_delete_port_node_locked(self, mock_notify, mock_dpt):
|
||||
self.node.reserve(self.context, 'fake', self.node.uuid)
|
||||
mock_dpt.side_effect = exception.NodeLocked(node='fake-node',
|
||||
host='fake-host')
|
||||
@ -1337,6 +1397,14 @@ class TestDelete(test_api_base.BaseApiTest):
|
||||
self.assertEqual(http_client.CONFLICT, ret.status_code)
|
||||
self.assertTrue(ret.json['error_message'])
|
||||
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_portgroups_subresource_delete(self, mock_dpt):
|
||||
portgroup = obj_utils.create_test_portgroup(self.context,
|
||||
|
@ -24,6 +24,7 @@ from ironic.conductor import notification_utils as notif_utils
|
||||
from ironic.conductor import task_manager
|
||||
from ironic.objects import fields
|
||||
from ironic.objects import node as node_objects
|
||||
from ironic.objects import notification
|
||||
from ironic.tests import base as tests_base
|
||||
from ironic.tests.unit.db import base
|
||||
from ironic.tests.unit.objects import utils as obj_utils
|
||||
@ -69,7 +70,7 @@ class TestNotificationUtils(base.DbTestCase):
|
||||
to_power=states.POWER_ON
|
||||
)
|
||||
|
||||
@mock.patch.object(notif_utils, 'mask_secrets')
|
||||
@mock.patch.object(notification, 'mask_secrets')
|
||||
def test__emit_conductor_node_notification(self, mock_secrets):
|
||||
mock_notify_method = mock.Mock()
|
||||
# Required for exception handling
|
||||
@ -124,7 +125,7 @@ class TestNotificationUtils(base.DbTestCase):
|
||||
|
||||
self.assertFalse(mock_notify_method.called)
|
||||
|
||||
@mock.patch.object(notif_utils, 'mask_secrets')
|
||||
@mock.patch.object(notification, 'mask_secrets')
|
||||
def test__emit_conductor_node_notification_known_notify_exc(self,
|
||||
mock_secrets):
|
||||
"""Test exception caught for a known notification exception."""
|
||||
@ -190,7 +191,7 @@ class ProvisionNotifyTestCase(tests_base.TestCase):
|
||||
'some_value': 'fake-value'}
|
||||
node = obj_utils.get_test_node(self.context,
|
||||
instance_info=test_info)
|
||||
notif_utils.mask_secrets(node)
|
||||
notification.mask_secrets(node)
|
||||
self.assertEqual('******', node.instance_info['configdrive'])
|
||||
self.assertEqual('******', node.instance_info['image_url'])
|
||||
self.assertEqual('fake-value', node.instance_info['some_value'])
|
||||
|
@ -412,17 +412,23 @@ expected_object_fingerprints = {
|
||||
'Conductor': '1.1-5091f249719d4a465062a1b3dc7f860d',
|
||||
'EventType': '1.1-aa2ba1afd38553e3880c267404e8d370',
|
||||
'NotificationPublisher': '1.0-51a09397d6c0687771fb5be9a999605d',
|
||||
'NodePayload': '1.1-d895cf6411ac666f9e982f85ea0a9499',
|
||||
'NodePayload': '1.2-f4e7a1def3b2a5784863eeed46e3a25f',
|
||||
'NodeSetPowerStateNotification': '1.0-59acc533c11d306f149846f922739c15',
|
||||
'NodeSetPowerStatePayload': '1.1-b8fab1bea5a2da5900445ab515e41715',
|
||||
'NodeSetPowerStatePayload': '1.2-06b6daec792fdef69c672ab5899c6a07',
|
||||
'NodeCorrectedPowerStateNotification': '1.0-59acc533c11d306f149846f922739'
|
||||
'c15',
|
||||
'NodeCorrectedPowerStatePayload': '1.1-5d1544defc858ae8a722f4cadd511bac',
|
||||
'NodeCorrectedPowerStatePayload': '1.2-ef6515d2f20944f4ed3d3e06a6476396',
|
||||
'NodeSetProvisionStateNotification':
|
||||
'1.0-59acc533c11d306f149846f922739c15',
|
||||
'NodeSetProvisionStatePayload': '1.1-743be1f5748f346e3da33390983172b1',
|
||||
'NodeSetProvisionStatePayload': '1.2-2695d18d1eccbb0f5d3bbcb0575630dc',
|
||||
'VolumeConnector': '1.0-3e0252c0ab6e6b9d158d09238a577d97',
|
||||
'VolumeTarget': '1.0-0b10d663d8dae675900b2c7548f76f5e',
|
||||
'ChassisCRUDNotification': '1.0-59acc533c11d306f149846f922739c15',
|
||||
'ChassisCRUDPayload': '1.0-dce63895d8186279a7dd577cffccb202',
|
||||
'NodeCRUDNotification': '1.0-59acc533c11d306f149846f922739c15',
|
||||
'NodeCRUDPayload': '1.0-37bb4cdd2c84b59fd6ad0547dbf713a0',
|
||||
'PortCRUDNotification': '1.0-59acc533c11d306f149846f922739c15',
|
||||
'PortCRUDPayload': '1.0-88acd98c9b08b4c8810e77793152057b'
|
||||
}
|
||||
|
||||
|
||||
|
@ -0,0 +1,7 @@
|
||||
---
|
||||
features:
|
||||
- Adds notifications for creation, updates, or deletions of ironic resources
|
||||
(node, port and chassis). Event types are formatted as follows
|
||||
"baremetal.<resource>.{create,update,delete}.{start,end,error}".
|
||||
For more details, see the developer documentation
|
||||
/http://docs.openstack.org/developer/ironic/deploy/notifications.html.
|
Loading…
x
Reference in New Issue
Block a user