Generic management I/F for Inject NMI

This patch updates the generic management interface and adds a new
REST API to support the injection of Non-Masking Interrupts (NMI) for
a node. This feature can be used for hardware diagnostics, and actual
support depends on a driver.

Partial-Bug: #1526226
Change-Id: I08d74f5ccbc386baca1fb29e428fe01924499d45
This commit is contained in:
Naohiro Tamura 2016-07-28 11:49:45 +09:00
parent 1e49c7b07b
commit 58d59db30f
15 changed files with 241 additions and 3 deletions

View File

@ -2,6 +2,11 @@
REST API Version History
========================
**1.29** (Ocata)
Add a new management API to support inject NMI,
'PUT /v1/nodes/(node_ident)/management/inject_nmi'.
**1.28** (Ocata)
Add '/v1/nodes/<node identifier>/vifs' endpoint for attach, detach and list of VIFs.

View File

@ -48,6 +48,8 @@
"baremetal:node:vif:attach": "rule:is_admin"
# Detach a VIF from a node
"baremetal:node:vif:detach": "rule:is_admin"
# Inject NMI for a node
"baremetal:node:inject_nmi": "rule:is_admin"
# Retrieve Port records
"baremetal:port:get": "rule:is_admin or rule:is_observer"
# Create Port records

View File

@ -250,11 +250,49 @@ class BootDeviceController(rest.RestController):
return {'supported_boot_devices': boot_devices}
class InjectNmiController(rest.RestController):
@METRICS.timer('InjectNmiController.put')
@expose.expose(None, types.uuid_or_name,
status_code=http_client.NO_CONTENT)
def put(self, node_ident):
"""Inject NMI for a node.
Inject NMI (Non Maskable Interrupt) for a node immediately.
:param node_ident: the UUID or logical name of a node.
:raises: NotFound if requested version of the API doesn't support
inject nmi.
:raises: HTTPForbidden if the policy is not authorized.
:raises: NodeNotFound if the node is not found.
:raises: NodeLocked if the node is locked by another conductor.
:raises: UnsupportedDriverExtension if the node's driver doesn't
support management or management.inject_nmi.
:raises: InvalidParameterValue when the wrong driver info is
specified or an invalid boot device is specified.
:raises: MissingParameterValue if missing supplied info.
"""
if not api_utils.allow_inject_nmi():
raise exception.NotFound()
cdict = pecan.request.context.to_policy_values()
policy.authorize('baremetal:node:inject_nmi', cdict, cdict)
rpc_node = api_utils.get_rpc_node(node_ident)
topic = pecan.request.rpcapi.get_topic_for(rpc_node)
pecan.request.rpcapi.inject_nmi(pecan.request.context,
rpc_node.uuid,
topic=topic)
class NodeManagementController(rest.RestController):
boot_device = BootDeviceController()
"""Expose boot_device as a sub-element of management"""
inject_nmi = InjectNmiController()
"""Expose inject_nmi as a sub-element of management"""
class ConsoleInfo(base.APIBase):
"""API representation of the console information for a node."""

View File

@ -378,6 +378,14 @@ def allow_soft_power_off():
return pecan.request.version.minor >= versions.MINOR_27_SOFT_POWER_OFF
def allow_inject_nmi():
"""Check if Inject NMI is allowed for the node.
Version 1.29 of the API allows Inject NMI for the node.
"""
return pecan.request.version.minor >= versions.MINOR_29_INJECT_NMI
def allow_links_node_states_and_driver_properties():
"""Check if links are displayable.

View File

@ -59,6 +59,7 @@ BASE_VERSION = 1
# v1.26: Add portgroup.mode and portgroup.properties.
# v1.27: Add soft reboot, soft power off and timeout.
# v1.28: Add vifs subcontroller to node
# v1.29: Add inject nmi.
MINOR_0_JUNO = 0
MINOR_1_INITIAL_VERSION = 1
@ -89,11 +90,12 @@ MINOR_25_UNSET_CHASSIS_UUID = 25
MINOR_26_PORTGROUP_MODE_PROPERTIES = 26
MINOR_27_SOFT_POWER_OFF = 27
MINOR_28_VIFS_SUBCONTROLLER = 28
MINOR_29_INJECT_NMI = 29
# When adding another version, update MINOR_MAX_VERSION and also update
# doc/source/dev/webapi-version-history.rst with a detailed explanation of
# what the version has changed.
MINOR_MAX_VERSION = MINOR_28_VIFS_SUBCONTROLLER
MINOR_MAX_VERSION = MINOR_29_INJECT_NMI
# String representations of the minor and maximum versions
MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION)

View File

@ -127,6 +127,9 @@ node_policies = [
policy.RuleDefault('baremetal:node:vif:detach',
'rule:is_admin',
description='Detach a VIF from a node'),
policy.RuleDefault('baremetal:node:inject_nmi',
'rule:is_admin',
description='Inject NMI for a node'),
]
port_policies = [

View File

@ -83,7 +83,7 @@ class ConductorManager(base_manager.BaseConductorManager):
"""Ironic Conductor manager main class."""
# NOTE(rloo): This must be in sync with rpcapi.ConductorAPI's.
RPC_API_VERSION = '1.39'
RPC_API_VERSION = '1.40'
target = messaging.Target(version=RPC_API_VERSION)
@ -2171,6 +2171,36 @@ class ConductorManager(base_manager.BaseConductorManager):
task.driver.management.validate(task)
return task.driver.management.get_boot_device(task)
@METRICS.timer('ConductorManager.inject_nmi')
@messaging.expected_exceptions(exception.NodeLocked,
exception.UnsupportedDriverExtension,
exception.InvalidParameterValue)
def inject_nmi(self, context, node_id):
"""Inject NMI for a node.
Inject NMI (Non Maskable Interrupt) for a node immediately.
:param context: request context.
:param node_id: node id or uuid.
:raises: NodeLocked if node is locked by another conductor.
:raises: UnsupportedDriverExtension if the node's driver doesn't
support management or management.inject_nmi.
:raises: InvalidParameterValue when the wrong driver info is
specified or an invalid boot device is specified.
:raises: MissingParameterValue if missing supplied info.
"""
LOG.debug('RPC inject_nmi called for node %s', node_id)
with task_manager.acquire(context, node_id,
purpose='inject nmi') as task:
node = task.node
if not getattr(task.driver, 'management', None):
raise exception.UnsupportedDriverExtension(
driver=node.driver, extension='management')
task.driver.management.validate(task)
task.driver.management.inject_nmi(task)
@METRICS.timer('ConductorManager.get_supported_boot_devices')
@messaging.expected_exceptions(exception.NodeLocked,
exception.UnsupportedDriverExtension,

View File

@ -86,11 +86,12 @@ class ConductorAPI(object):
| 1.37 - Added destroy_volume_target and update_volume_target
| 1.38 - Added vif_attach, vif_detach, vif_list
| 1.39 - Added timeout optional parameter to change_node_power_state
| 1.40 - Added inject_nmi
"""
# NOTE(rloo): This must be in sync with manager.ConductorManager's.
RPC_API_VERSION = '1.39'
RPC_API_VERSION = '1.40'
def __init__(self, topic=None):
super(ConductorAPI, self).__init__()
@ -555,6 +556,25 @@ class ConductorAPI(object):
cctxt = self.client.prepare(topic=topic or self.topic, version='1.17')
return cctxt.call(context, 'get_boot_device', node_id=node_id)
def inject_nmi(self, context, node_id, topic=None):
"""Inject NMI for a node.
Inject NMI (Non Maskable Interrupt) for a node immediately.
Be aware that not all drivers support this.
:param context: request context.
:param node_id: node id or uuid.
:raises: NodeLocked if node is locked by another conductor.
:raises: UnsupportedDriverExtension if the node's driver doesn't
support management or management.inject_nmi.
:raises: InvalidParameterValue when the wrong driver info is
specified or an invalid boot device is specified.
:raises: MissingParameterValue if missing supplied info.
"""
cctxt = self.client.prepare(topic=topic or self.topic, version='1.40')
return cctxt.call(context, 'inject_nmi', node_id=node_id)
def get_supported_boot_devices(self, context, node_id, topic=None):
"""Get the list of supported devices.

View File

@ -822,6 +822,17 @@ class ManagementInterface(BaseInterface):
}
"""
def inject_nmi(self, task):
"""Inject NMI, Non Maskable Interrupt.
Inject NMI (Non Maskable Interrupt) for a node immediately.
:param task: A TaskManager instance containing the node to act on.
:raises: UnsupportedDriverExtension
"""
raise exception.UnsupportedDriverExtension(
driver=task.node.driver, extension='inject_nmi')
class InspectInterface(BaseInterface):
"""Interface for inspection-related actions."""

View File

@ -3063,6 +3063,39 @@ class TestPut(test_api_base.BaseApiTest):
self.assertEqual('application/json', ret.content_type)
self.assertEqual(http_client.BAD_REQUEST, ret.status_code)
@mock.patch.object(rpcapi.ConductorAPI, 'inject_nmi')
def test_inject_nmi(self, mock_inject_nmi):
ret = self.put_json('/nodes/%s/management/inject_nmi'
% self.node.uuid, {},
headers={api_base.Version.string: "1.29"})
self.assertEqual(http_client.NO_CONTENT, ret.status_code)
self.assertEqual(b'', ret.body)
mock_inject_nmi.assert_called_once_with(mock.ANY, self.node.uuid,
topic='test-topic')
@mock.patch.object(rpcapi.ConductorAPI, 'inject_nmi')
def test_inject_nmi_not_allowed(self, mock_inject_nmi):
ret = self.put_json('/nodes/%s/management/inject_nmi'
% self.node.uuid, {},
headers={api_base.Version.string: "1.28"},
expect_errors=True)
self.assertEqual(http_client.NOT_FOUND, ret.status_code)
self.assertTrue(ret.json['error_message'])
self.assertFalse(mock_inject_nmi.called)
@mock.patch.object(rpcapi.ConductorAPI, 'inject_nmi')
def test_inject_nmi_not_supported(self, mock_inject_nmi):
mock_inject_nmi.side_effect = exception.UnsupportedDriverExtension(
extension='management', driver='test-driver')
ret = self.put_json('/nodes/%s/management/inject_nmi'
% self.node.uuid, {},
headers={api_base.Version.string: "1.29"},
expect_errors=True)
self.assertEqual(http_client.BAD_REQUEST, ret.status_code)
self.assertTrue(ret.json['error_message'])
mock_inject_nmi.assert_called_once_with(mock.ANY, self.node.uuid,
topic='test-topic')
def _test_set_node_maintenance_mode(self, mock_update, mock_get, reason,
node_ident, is_by_name=False):
request_body = {}

View File

@ -280,6 +280,13 @@ class TestApiUtils(base.TestCase):
def test_check_allow_unknown_verbs(self, mock_request):
utils.check_allow_management_verbs('rebuild')
@mock.patch.object(pecan, 'request', spec_set=['version'])
def test_allow_inject_nmi(self, mock_request):
mock_request.version.minor = 29
self.assertTrue(utils.allow_inject_nmi())
mock_request.version.minor = 28
self.assertFalse(utils.allow_inject_nmi())
@mock.patch.object(pecan, 'request', spec_set=['version'])
def test_allow_links_node_states_and_driver_properties(self, mock_request):
mock_request.version.minor = 14

View File

@ -3380,6 +3380,64 @@ class UpdatePortTestCase(mgr_utils.ServiceSetUpMixin,
# Compare true exception hidden by @messaging.expected_exceptions
self.assertEqual(exception.InvalidParameterValue, exc.exc_info[0])
def test_inject_nmi(self):
node = obj_utils.create_test_node(self.context, driver='fake')
with mock.patch.object(self.driver.management, 'validate') as mock_val:
with mock.patch.object(self.driver.management,
'inject_nmi') as mock_sbd:
self.service.inject_nmi(self.context, node.uuid)
mock_val.assert_called_once_with(mock.ANY)
mock_sbd.assert_called_once_with(mock.ANY)
def test_inject_nmi_node_locked(self):
node = obj_utils.create_test_node(self.context, driver='fake',
reservation='fake-reserv')
exc = self.assertRaises(messaging.rpc.ExpectedException,
self.service.inject_nmi,
self.context, node.uuid)
# Compare true exception hidden by @messaging.expected_exceptions
self.assertEqual(exception.NodeLocked, exc.exc_info[0])
def test_inject_nmi_not_supported(self):
node = obj_utils.create_test_node(self.context, driver='fake')
# null the management interface
self.driver.management = None
exc = self.assertRaises(messaging.rpc.ExpectedException,
self.service.inject_nmi,
self.context, node.uuid)
# Compare true exception hidden by @messaging.expected_exceptions
self.assertEqual(exception.UnsupportedDriverExtension,
exc.exc_info[0])
def test_inject_nmi_validate_invalid_param(self):
node = obj_utils.create_test_node(self.context, driver='fake')
with mock.patch.object(self.driver.management, 'validate') as mock_val:
mock_val.side_effect = exception.InvalidParameterValue('error')
exc = self.assertRaises(messaging.rpc.ExpectedException,
self.service.inject_nmi,
self.context, node.uuid)
# Compare true exception hidden by @messaging.expected_exceptions
self.assertEqual(exception.InvalidParameterValue, exc.exc_info[0])
def test_inject_nmi_validate_missing_param(self):
node = obj_utils.create_test_node(self.context, driver='fake')
with mock.patch.object(self.driver.management, 'validate') as mock_val:
mock_val.side_effect = exception.MissingParameterValue('error')
exc = self.assertRaises(messaging.rpc.ExpectedException,
self.service.inject_nmi,
self.context, node.uuid)
# Compare true exception hidden by @messaging.expected_exceptions
self.assertEqual(exception.MissingParameterValue, exc.exc_info[0])
def test_inject_nmi_not_implemented(self):
node = obj_utils.create_test_node(self.context, driver='fake')
exc = self.assertRaises(messaging.rpc.ExpectedException,
self.service.inject_nmi,
self.context, node.uuid)
# Compare true exception hidden by @messaging.expected_exceptions
self.assertEqual(exception.UnsupportedDriverExtension,
exc.exc_info[0])
def test_get_supported_boot_devices(self):
node = obj_utils.create_test_node(self.context, driver='fake')
bootdevs = self.service.get_supported_boot_devices(self.context,

View File

@ -276,6 +276,12 @@ class RPCAPITestCase(base.DbTestCase):
version='1.17',
node_id=self.fake_node['uuid'])
def test_inject_nmi(self):
self._test_rpcapi('inject_nmi',
'call',
version='1.40',
node_id=self.fake_node['uuid'])
def test_get_supported_boot_devices(self):
self._test_rpcapi('get_supported_boot_devices',
'call',

View File

@ -498,3 +498,13 @@ class NetworkInterfaceTestCase(base.TestCase):
network.get_current_vif(mock_task, port)
mock_gcv.assert_called_once_with(mock_task, port)
self.assertTrue(mock_warn.called)
class TestManagementInterface(base.TestCase):
def test_inject_nmi_default_impl(self):
management = fake.FakeManagement()
task_mock = mock.MagicMock(spec_set=['node'])
self.assertRaises(exception.UnsupportedDriverExtension,
management.inject_nmi, task_mock)

View File

@ -0,0 +1,5 @@
---
features:
- Add support for the injection of Non-Masking Interrupts (NMI) for
a node in Ironic API 1.29. This feature can be used for hardware
diagnostics, and actual support depends on a driver.