Merge "Generic management I/F for Inject NMI"
This commit is contained in:
commit
8db68fef4e
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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."""
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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 = [
|
||||
|
@ -84,7 +84,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)
|
||||
|
||||
@ -2172,6 +2172,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,
|
||||
|
@ -87,11 +87,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__()
|
||||
@ -557,6 +558,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.
|
||||
|
||||
|
@ -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."""
|
||||
|
@ -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 = {}
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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',
|
||||
|
@ -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)
|
||||
|
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…
Reference in New Issue
Block a user