From 876db5a61314740dc949c2641131f230f9be6c98 Mon Sep 17 00:00:00 2001 From: Yuriy Zveryanskyy Date: Thu, 10 Nov 2016 16:29:27 +0200 Subject: [PATCH] Add node maintenance notifications This patch adds node maintenance notifications, event types are: "baremetal.node.maintenance_set.{start, end, error}". Developer documentation updated. Change-Id: I9105821ed0a4db614ea3e1c73ad563b82b1c6082 Partial-Bug: #1606520 --- doc/source/deploy/notifications.rst | 53 +++++++++++++++++++ ironic/api/controllers/v1/node.py | 19 ++++--- .../api/controllers/v1/notification_utils.py | 9 +++- ironic/objects/node.py | 11 ++++ ironic/tests/unit/api/v1/test_nodes.py | 30 ++++++++++- .../unit/api/v1/test_notification_utils.py | 33 +++++++++++- ironic/tests/unit/objects/test_objects.py | 3 +- ...enance-notifications-43c150efe41b8517.yaml | 5 ++ 8 files changed, 150 insertions(+), 13 deletions(-) create mode 100644 releasenotes/notes/node-maintenance-notifications-43c150efe41b8517.yaml diff --git a/doc/source/deploy/notifications.rst b/doc/source/deploy/notifications.rst index cf7510f88e..fc534b0538 100644 --- a/doc/source/deploy/notifications.rst +++ b/doc/source/deploy/notifications.rst @@ -205,6 +205,59 @@ Example of port CRUD notification:: "publisher_id":"ironic-api.hostname02" } +Node maintenance notifications +------------------------------ + +These notifications are emitted from API service when maintenance mode is +changed via API service. List of maintenance notifications for a node: + +* ``baremetal.node.maintenance_set.start`` +* ``baremetal.node.maintenance_set.end`` +* ``baremetal.node.maintenance_set.error`` + +"start" and "end" notifications have INFO level, "error" has ERROR. Example of +node maintenance notification:: + + { + "priority": "info", + "payload":{ + "ironic_object.namespace":"ironic", + "ironic_object.name":"NodePayload", + "ironic_object.version":"1.0", + "ironic_object.data":{ + "clean_step": None, + "console_enabled": False, + "created_at": "2016-01-26T20:41:03+00:00", + "driver": "fake", + "extra": {}, + "inspection_finished_at": None, + "inspection_started_at": None, + "instance_info": {}, + "instance_uuid": None, + "last_error": None, + "maintenance": True, + "maintenance_reason": "hw upgrade", + "network_interface": "flat", + "name": None, + "power_state": "power off", + "properties": { + "memory_mb": 4096, + "cpu_arch": "x86_64", + "local_gb": 10, + "cpus": 8}, + "provision_state": "available", + "provision_updated_at": "2016-01-27T20:41:03+00:00", + "resource_class": None, + "target_power_state": None, + "target_provision_state": None, + "updated_at": "2016-01-27T20:41:03+00:00", + "uuid": "1be26c0b-03f2-4d2e-ae87-c02d7f33c123", + } + }, + "event_type":"baremetal.node.maintenance_set.start", + "publisher_id":"ironic-api.hostname02" + } + ------------------------------ ironic-conductor notifications ------------------------------ diff --git a/ironic/api/controllers/v1/node.py b/ironic/api/controllers/v1/node.py index 7ea56a9bb5..f7a134cbbf 100644 --- a/ironic/api/controllers/v1/node.py +++ b/ironic/api/controllers/v1/node.py @@ -1005,17 +1005,22 @@ class NodeVendorPassthruController(rest.RestController): class NodeMaintenanceController(rest.RestController): def _set_maintenance(self, node_ident, maintenance_mode, reason=None): + context = pecan.request.context rpc_node = api_utils.get_rpc_node(node_ident) rpc_node.maintenance = maintenance_mode rpc_node.maintenance_reason = reason + notify.emit_start_notification(context, rpc_node, 'maintenance_set') + with notify.handle_error_notification(context, rpc_node, + 'maintenance_set'): + 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.update_node(pecan.request.context, - rpc_node, topic=topic) + new_node = pecan.request.rpcapi.update_node(context, rpc_node, + topic=topic) + notify.emit_end_notification(context, new_node, 'maintenance_set') @METRICS.timer('NodeMaintenanceController.put') @expose.expose(None, types.uuid_or_name, wtypes.text, diff --git a/ironic/api/controllers/v1/notification_utils.py b/ironic/api/controllers/v1/notification_utils.py index cc4d2e25e2..b5db6545f7 100644 --- a/ironic/api/controllers/v1/notification_utils.py +++ b/ironic/api/controllers/v1/notification_utils.py @@ -59,10 +59,15 @@ def _emit_api_notification(context, obj, action, level, status, **kwargs): for k, v in kwargs.items()} try: try: - if resource not in CRUD_NOTIFY_OBJ: + if action == 'maintenance_set': + notification_method = node_objects.NodeMaintenanceNotification + payload_method = node_objects.NodePayload + elif 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] + else: + notification_method, payload_method = CRUD_NOTIFY_OBJ[resource] + notification_name = notification_method.__name__ payload_name = payload_method.__name__ finally: diff --git a/ironic/objects/node.py b/ironic/objects/node.py index d66f99239d..0a3e740382 100644 --- a/ironic/objects/node.py +++ b/ironic/objects/node.py @@ -607,3 +607,14 @@ class NodeCRUDPayload(NodePayload): def __init__(self, node, chassis_uuid): super(NodeCRUDPayload, self).__init__(node, chassis_uuid=chassis_uuid) + + +@base.IronicObjectRegistry.register +class NodeMaintenanceNotification(notification.NotificationBase): + """Notification emitted when maintenance state changed via API.""" + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'payload': object_fields.ObjectField('NodePayload') + } diff --git a/ironic/tests/unit/api/v1/test_nodes.py b/ironic/tests/unit/api/v1/test_nodes.py index 8d8cca97aa..50aa3ab6a1 100644 --- a/ironic/tests/unit/api/v1/test_nodes.py +++ b/ironic/tests/unit/api/v1/test_nodes.py @@ -2985,11 +2985,21 @@ class TestPut(test_api_base.BaseApiTest): mock_update.assert_called_once_with(mock.ANY, mock.ANY, topic='test-topic') + @mock.patch.object(notification_utils, '_emit_api_notification') @mock.patch.object(objects.Node, 'get_by_uuid') @mock.patch.object(rpcapi.ConductorAPI, 'update_node') - def test_set_node_maintenance_mode(self, mock_update, mock_get): + def test_set_node_maintenance_mode(self, mock_update, mock_get, + mock_notify): self._test_set_node_maintenance_mode(mock_update, mock_get, 'fake_reason', self.node.uuid) + mock_notify.assert_has_calls([mock.call(mock.ANY, mock.ANY, + 'maintenance_set', + obj_fields.NotificationLevel.INFO, + obj_fields.NotificationStatus.START), + mock.call(mock.ANY, mock.ANY, + 'maintenance_set', + obj_fields.NotificationLevel.INFO, + obj_fields.NotificationStatus.END)]) @mock.patch.object(objects.Node, 'get_by_uuid') @mock.patch.object(rpcapi.ConductorAPI, 'update_node') @@ -3011,6 +3021,24 @@ class TestPut(test_api_base.BaseApiTest): self._test_set_node_maintenance_mode(mock_update, mock_get, None, self.node.name, is_by_name=True) + @mock.patch.object(notification_utils, '_emit_api_notification') + @mock.patch.object(objects.Node, 'get_by_uuid') + @mock.patch.object(rpcapi.ConductorAPI, 'update_node') + def test_set_node_maintenance_mode_error(self, mock_update, mock_get, + mock_notify): + mock_get.return_value = self.node + mock_update.side_effect = Exception() + self.put_json('/nodes/%s/maintenance' % self.node.uuid, + {'reason': 'fake'}, expect_errors=True) + mock_notify.assert_has_calls([mock.call(mock.ANY, mock.ANY, + 'maintenance_set', + obj_fields.NotificationLevel.INFO, + obj_fields.NotificationStatus.START), + mock.call(mock.ANY, mock.ANY, + 'maintenance_set', + obj_fields.NotificationLevel.ERROR, + obj_fields.NotificationStatus.ERROR)]) + class TestCheckCleanSteps(base.TestCase): def test__check_clean_steps_not_list(self): diff --git a/ironic/tests/unit/api/v1/test_notification_utils.py b/ironic/tests/unit/api/v1/test_notification_utils.py index 9ac428a75e..0300e66389 100644 --- a/ironic/tests/unit/api/v1/test_notification_utils.py +++ b/ironic/tests/unit/api/v1/test_notification_utils.py @@ -23,10 +23,10 @@ from ironic.tests import base as tests_base from ironic.tests.unit.objects import utils as obj_utils -class CRUDNotifyTestCase(tests_base.TestCase): +class APINotifyTestCase(tests_base.TestCase): def setUp(self): - super(CRUDNotifyTestCase, self).setUp() + super(APINotifyTestCase, self).setUp() self.node_notify_mock = mock.Mock() self.port_notify_mock = mock.Mock() self.chassis_notify_mock = mock.Mock() @@ -141,3 +141,32 @@ class CRUDNotifyTestCase(tests_base.TestCase): self.assertEqual({'a': 25}, payload.local_link_connection) self.assertEqual({'as': 34}, payload.extra) self.assertEqual(False, payload.pxe_enabled) + + @mock.patch('ironic.objects.node.NodeMaintenanceNotification') + def test_node_maintenance_notification(self, maintenance_mock): + maintenance_mock.__name__ = 'NodeMaintenanceNotification' + node = obj_utils.get_test_node(self.context, + maintenance=True, + maintenance_reason='test reason') + test_level = fields.NotificationLevel.INFO + test_status = fields.NotificationStatus.START + notif_utils._emit_api_notification(self.context, node, + 'maintenance_set', + test_level, test_status) + init_kwargs = maintenance_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(True, payload.maintenance) + self.assertEqual('test reason', payload.maintenance_reason) + + @mock.patch.object(notification.NotificationBase, 'emit') + def test_emit_maintenance_notification(self, emit_mock): + node = obj_utils.get_test_node(self.context) + test_level = fields.NotificationLevel.INFO + test_status = fields.NotificationStatus.START + notif_utils._emit_api_notification(self.context, node, + 'maintenance_set', + test_level, test_status) + emit_mock.assert_called_once_with(self.context) diff --git a/ironic/tests/unit/objects/test_objects.py b/ironic/tests/unit/objects/test_objects.py index 65dc949287..a1df6c8f78 100644 --- a/ironic/tests/unit/objects/test_objects.py +++ b/ironic/tests/unit/objects/test_objects.py @@ -428,7 +428,8 @@ expected_object_fingerprints = { 'NodeCRUDNotification': '1.0-59acc533c11d306f149846f922739c15', 'NodeCRUDPayload': '1.0-37bb4cdd2c84b59fd6ad0547dbf713a0', 'PortCRUDNotification': '1.0-59acc533c11d306f149846f922739c15', - 'PortCRUDPayload': '1.0-88acd98c9b08b4c8810e77793152057b' + 'PortCRUDPayload': '1.0-88acd98c9b08b4c8810e77793152057b', + 'NodeMaintenanceNotification': '1.0-59acc533c11d306f149846f922739c15' } diff --git a/releasenotes/notes/node-maintenance-notifications-43c150efe41b8517.yaml b/releasenotes/notes/node-maintenance-notifications-43c150efe41b8517.yaml new file mode 100644 index 0000000000..6546570a5f --- /dev/null +++ b/releasenotes/notes/node-maintenance-notifications-43c150efe41b8517.yaml @@ -0,0 +1,5 @@ +--- +features: + - Add notifications for node maintenance. Event types are + "baremetal.node.maintenance_set.{start, end, error}" + For more details, see the developer documentation.