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:
parent
1e49c7b07b
commit
58d59db30f
@ -2,6 +2,11 @@
|
|||||||
REST API Version History
|
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)
|
**1.28** (Ocata)
|
||||||
|
|
||||||
Add '/v1/nodes/<node identifier>/vifs' endpoint for attach, detach and list of VIFs.
|
Add '/v1/nodes/<node identifier>/vifs' endpoint for attach, detach and list of VIFs.
|
||||||
|
@ -48,6 +48,8 @@
|
|||||||
"baremetal:node:vif:attach": "rule:is_admin"
|
"baremetal:node:vif:attach": "rule:is_admin"
|
||||||
# Detach a VIF from a node
|
# Detach a VIF from a node
|
||||||
"baremetal:node:vif:detach": "rule:is_admin"
|
"baremetal:node:vif:detach": "rule:is_admin"
|
||||||
|
# Inject NMI for a node
|
||||||
|
"baremetal:node:inject_nmi": "rule:is_admin"
|
||||||
# Retrieve Port records
|
# Retrieve Port records
|
||||||
"baremetal:port:get": "rule:is_admin or rule:is_observer"
|
"baremetal:port:get": "rule:is_admin or rule:is_observer"
|
||||||
# Create Port records
|
# Create Port records
|
||||||
|
@ -250,11 +250,49 @@ class BootDeviceController(rest.RestController):
|
|||||||
return {'supported_boot_devices': boot_devices}
|
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):
|
class NodeManagementController(rest.RestController):
|
||||||
|
|
||||||
boot_device = BootDeviceController()
|
boot_device = BootDeviceController()
|
||||||
"""Expose boot_device as a sub-element of management"""
|
"""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):
|
class ConsoleInfo(base.APIBase):
|
||||||
"""API representation of the console information for a node."""
|
"""API representation of the console information for a node."""
|
||||||
|
@ -378,6 +378,14 @@ def allow_soft_power_off():
|
|||||||
return pecan.request.version.minor >= versions.MINOR_27_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():
|
def allow_links_node_states_and_driver_properties():
|
||||||
"""Check if links are displayable.
|
"""Check if links are displayable.
|
||||||
|
|
||||||
|
@ -59,6 +59,7 @@ BASE_VERSION = 1
|
|||||||
# v1.26: Add portgroup.mode and portgroup.properties.
|
# v1.26: Add portgroup.mode and portgroup.properties.
|
||||||
# v1.27: Add soft reboot, soft power off and timeout.
|
# v1.27: Add soft reboot, soft power off and timeout.
|
||||||
# v1.28: Add vifs subcontroller to node
|
# v1.28: Add vifs subcontroller to node
|
||||||
|
# v1.29: Add inject nmi.
|
||||||
|
|
||||||
MINOR_0_JUNO = 0
|
MINOR_0_JUNO = 0
|
||||||
MINOR_1_INITIAL_VERSION = 1
|
MINOR_1_INITIAL_VERSION = 1
|
||||||
@ -89,11 +90,12 @@ MINOR_25_UNSET_CHASSIS_UUID = 25
|
|||||||
MINOR_26_PORTGROUP_MODE_PROPERTIES = 26
|
MINOR_26_PORTGROUP_MODE_PROPERTIES = 26
|
||||||
MINOR_27_SOFT_POWER_OFF = 27
|
MINOR_27_SOFT_POWER_OFF = 27
|
||||||
MINOR_28_VIFS_SUBCONTROLLER = 28
|
MINOR_28_VIFS_SUBCONTROLLER = 28
|
||||||
|
MINOR_29_INJECT_NMI = 29
|
||||||
|
|
||||||
# When adding another version, update MINOR_MAX_VERSION and also update
|
# When adding another version, update MINOR_MAX_VERSION and also update
|
||||||
# doc/source/dev/webapi-version-history.rst with a detailed explanation of
|
# doc/source/dev/webapi-version-history.rst with a detailed explanation of
|
||||||
# what the version has changed.
|
# 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
|
# String representations of the minor and maximum versions
|
||||||
MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION)
|
MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION)
|
||||||
|
@ -127,6 +127,9 @@ node_policies = [
|
|||||||
policy.RuleDefault('baremetal:node:vif:detach',
|
policy.RuleDefault('baremetal:node:vif:detach',
|
||||||
'rule:is_admin',
|
'rule:is_admin',
|
||||||
description='Detach a VIF from a node'),
|
description='Detach a VIF from a node'),
|
||||||
|
policy.RuleDefault('baremetal:node:inject_nmi',
|
||||||
|
'rule:is_admin',
|
||||||
|
description='Inject NMI for a node'),
|
||||||
]
|
]
|
||||||
|
|
||||||
port_policies = [
|
port_policies = [
|
||||||
|
@ -83,7 +83,7 @@ class ConductorManager(base_manager.BaseConductorManager):
|
|||||||
"""Ironic Conductor manager main class."""
|
"""Ironic Conductor manager main class."""
|
||||||
|
|
||||||
# NOTE(rloo): This must be in sync with rpcapi.ConductorAPI's.
|
# 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)
|
target = messaging.Target(version=RPC_API_VERSION)
|
||||||
|
|
||||||
@ -2171,6 +2171,36 @@ class ConductorManager(base_manager.BaseConductorManager):
|
|||||||
task.driver.management.validate(task)
|
task.driver.management.validate(task)
|
||||||
return task.driver.management.get_boot_device(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')
|
@METRICS.timer('ConductorManager.get_supported_boot_devices')
|
||||||
@messaging.expected_exceptions(exception.NodeLocked,
|
@messaging.expected_exceptions(exception.NodeLocked,
|
||||||
exception.UnsupportedDriverExtension,
|
exception.UnsupportedDriverExtension,
|
||||||
|
@ -86,11 +86,12 @@ class ConductorAPI(object):
|
|||||||
| 1.37 - Added destroy_volume_target and update_volume_target
|
| 1.37 - Added destroy_volume_target and update_volume_target
|
||||||
| 1.38 - Added vif_attach, vif_detach, vif_list
|
| 1.38 - Added vif_attach, vif_detach, vif_list
|
||||||
| 1.39 - Added timeout optional parameter to change_node_power_state
|
| 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.
|
# 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):
|
def __init__(self, topic=None):
|
||||||
super(ConductorAPI, self).__init__()
|
super(ConductorAPI, self).__init__()
|
||||||
@ -555,6 +556,25 @@ class ConductorAPI(object):
|
|||||||
cctxt = self.client.prepare(topic=topic or self.topic, version='1.17')
|
cctxt = self.client.prepare(topic=topic or self.topic, version='1.17')
|
||||||
return cctxt.call(context, 'get_boot_device', node_id=node_id)
|
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):
|
def get_supported_boot_devices(self, context, node_id, topic=None):
|
||||||
"""Get the list of supported devices.
|
"""Get the list of supported devices.
|
||||||
|
|
||||||
|
@ -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):
|
class InspectInterface(BaseInterface):
|
||||||
"""Interface for inspection-related actions."""
|
"""Interface for inspection-related actions."""
|
||||||
|
@ -3063,6 +3063,39 @@ class TestPut(test_api_base.BaseApiTest):
|
|||||||
self.assertEqual('application/json', ret.content_type)
|
self.assertEqual('application/json', ret.content_type)
|
||||||
self.assertEqual(http_client.BAD_REQUEST, ret.status_code)
|
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,
|
def _test_set_node_maintenance_mode(self, mock_update, mock_get, reason,
|
||||||
node_ident, is_by_name=False):
|
node_ident, is_by_name=False):
|
||||||
request_body = {}
|
request_body = {}
|
||||||
|
@ -280,6 +280,13 @@ class TestApiUtils(base.TestCase):
|
|||||||
def test_check_allow_unknown_verbs(self, mock_request):
|
def test_check_allow_unknown_verbs(self, mock_request):
|
||||||
utils.check_allow_management_verbs('rebuild')
|
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'])
|
@mock.patch.object(pecan, 'request', spec_set=['version'])
|
||||||
def test_allow_links_node_states_and_driver_properties(self, mock_request):
|
def test_allow_links_node_states_and_driver_properties(self, mock_request):
|
||||||
mock_request.version.minor = 14
|
mock_request.version.minor = 14
|
||||||
|
@ -3380,6 +3380,64 @@ class UpdatePortTestCase(mgr_utils.ServiceSetUpMixin,
|
|||||||
# Compare true exception hidden by @messaging.expected_exceptions
|
# Compare true exception hidden by @messaging.expected_exceptions
|
||||||
self.assertEqual(exception.InvalidParameterValue, exc.exc_info[0])
|
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):
|
def test_get_supported_boot_devices(self):
|
||||||
node = obj_utils.create_test_node(self.context, driver='fake')
|
node = obj_utils.create_test_node(self.context, driver='fake')
|
||||||
bootdevs = self.service.get_supported_boot_devices(self.context,
|
bootdevs = self.service.get_supported_boot_devices(self.context,
|
||||||
|
@ -276,6 +276,12 @@ class RPCAPITestCase(base.DbTestCase):
|
|||||||
version='1.17',
|
version='1.17',
|
||||||
node_id=self.fake_node['uuid'])
|
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):
|
def test_get_supported_boot_devices(self):
|
||||||
self._test_rpcapi('get_supported_boot_devices',
|
self._test_rpcapi('get_supported_boot_devices',
|
||||||
'call',
|
'call',
|
||||||
|
@ -498,3 +498,13 @@ class NetworkInterfaceTestCase(base.TestCase):
|
|||||||
network.get_current_vif(mock_task, port)
|
network.get_current_vif(mock_task, port)
|
||||||
mock_gcv.assert_called_once_with(mock_task, port)
|
mock_gcv.assert_called_once_with(mock_task, port)
|
||||||
self.assertTrue(mock_warn.called)
|
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)
|
||||||
|
5
releasenotes/notes/inject-nmi-dacd692b1f259a30.yaml
Normal file
5
releasenotes/notes/inject-nmi-dacd692b1f259a30.yaml
Normal 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.
|
Loading…
x
Reference in New Issue
Block a user