From d0e89705033807ae77490038dabaf1a00f9996e1 Mon Sep 17 00:00:00 2001
From: Stephen Finucane <stephenfin@redhat.com>
Date: Mon, 5 Dec 2022 10:17:26 +0000
Subject: [PATCH] baremetal: Add Node.inject_nmi method

Noted while attempting to migrate nova from ironicclient to
openstacksdk.

Change-Id: I3fc92219f55bb723d7675c1c0c078b9c9b8da304
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
---
 doc/source/user/proxies/baremetal.rst         | 16 +++++++---
 openstack/baremetal/v1/_common.py             |  3 ++
 openstack/baremetal/v1/_proxy.py              | 15 ++++++++++
 openstack/baremetal/v1/node.py                | 28 ++++++++++++++++++
 .../tests/unit/baremetal/v1/test_node.py      | 29 +++++++++++++++++++
 .../node-inject-nmi-53d12681026e0b6c.yaml     |  6 ++++
 6 files changed, 93 insertions(+), 4 deletions(-)
 create mode 100644 releasenotes/notes/node-inject-nmi-53d12681026e0b6c.yaml

diff --git a/doc/source/user/proxies/baremetal.rst b/doc/source/user/proxies/baremetal.rst
index d8978c0ff..da35cdcab 100644
--- a/doc/source/user/proxies/baremetal.rst
+++ b/doc/source/user/proxies/baremetal.rst
@@ -16,10 +16,18 @@ Node Operations
 ^^^^^^^^^^^^^^^
 .. autoclass:: openstack.baremetal.v1._proxy.Proxy
   :noindex:
-  :members: nodes, find_node, get_node, create_node, update_node, patch_node, delete_node,
-            validate_node, set_node_power_state, set_node_provision_state,
-            wait_for_nodes_provision_state, wait_for_node_power_state,
-            wait_for_node_reservation, set_node_maintenance, unset_node_maintenance
+  :members: nodes, create_node, find_node, get_node, update_node, patch_node, delete_node,
+            set_node_provision_state, set_node_boot_device, set_node_boot_mode,
+            set_node_secure_boot, inject_nmi_to_node, wait_for_nodes_provision_state,
+            set_node_power_state, wait_for_node_power_state,
+            wait_for_node_reservation, validate_node, set_node_maintenance,
+            unset_node_maintenance, delete_node, list_node_vendor_passthru
+
+Node Trait Operations
+^^^^^^^^^^^^^^^^^^^^^
+.. autoclass:: openstack.baremetal.v1._proxy.Proxy
+  :noindex:
+  :members: add_node_trait, remove_node_trait, set_node_traits
 
 Port Operations
 ^^^^^^^^^^^^^^^
diff --git a/openstack/baremetal/v1/_common.py b/openstack/baremetal/v1/_common.py
index 54f1862e7..3cbb68ba8 100644
--- a/openstack/baremetal/v1/_common.py
+++ b/openstack/baremetal/v1/_common.py
@@ -68,6 +68,9 @@ STATE_VERSIONS = {
 VIF_VERSION = '1.28'
 """API version in which the VIF operations were introduced."""
 
+INJECT_NMI_VERSION = '1.29'
+"""API vresion in which support for injecting NMI was introduced."""
+
 CONFIG_DRIVE_REBUILD_VERSION = '1.35'
 """API version in which rebuild accepts a configdrive."""
 
diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py
index 1722f5ac8..0285fc773 100644
--- a/openstack/baremetal/v1/_proxy.py
+++ b/openstack/baremetal/v1/_proxy.py
@@ -445,6 +445,21 @@ class Proxy(proxy.Proxy):
         res = self._get_resource(_node.Node, node)
         return res.set_secure_boot(self, target)
 
+    def inject_nmi_to_node(self, node):
+        """Inject NMI to node.
+
+        Injects a non-maskable interrupt (NMI) message to the node. This is
+        used when response time is critical, such as during non-recoverable
+        hardware errors. In addition, virsh inject-nmi is useful for triggering
+        a crashdump in Windows guests.
+
+        :param node: The value can be the name or ID of a node or a
+            :class:`~openstack.baremetal.v1.node.Node` instance.
+        :return: None
+        """
+        res = self._get_resource(_node.Node, node)
+        res.inject_nmi(self)
+
     def wait_for_nodes_provision_state(self, nodes, expected_state,
                                        timeout=None,
                                        abort_on_failed_state=True,
diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py
index 0a3a1385d..276540abc 100644
--- a/openstack/baremetal/v1/node.py
+++ b/openstack/baremetal/v1/node.py
@@ -602,6 +602,34 @@ class Node(_common.ListMixin, resource.Resource):
                 "the last error is %(error)s" %
                 {'node': self.id, 'error': self.last_error})
 
+    def inject_nmi(self, session):
+        """Inject NMI.
+
+        :param session: The session to use for making this request.
+        :return: None
+        """
+        session = self._get_session(session)
+        version = self._assert_microversion_for(
+            session,
+            'commit',
+            _common.INJECT_NMI_VERSION,
+        )
+        request = self._prepare_request(requires_id=True)
+        request.url = utils.urljoin(request.url, 'management', 'inject_nmi')
+
+        body = {}
+
+        response = session.put(
+            request.url,
+            json=body,
+            headers=request.headers,
+            microversion=version,
+            retriable_status_codes=_common.RETRIABLE_STATUS_CODES,
+        )
+
+        msg = ("Failed to inject NMI to node {node}".format(node=self.id))
+        exceptions.raise_from_response(response, error_message=msg)
+
     def set_power_state(self, session, target, wait=False, timeout=None):
         """Run an action modifying this node's power state.
 
diff --git a/openstack/tests/unit/baremetal/v1/test_node.py b/openstack/tests/unit/baremetal/v1/test_node.py
index 3d001497b..73386fd3e 100644
--- a/openstack/tests/unit/baremetal/v1/test_node.py
+++ b/openstack/tests/unit/baremetal/v1/test_node.py
@@ -590,6 +590,35 @@ class TestNodeWaitForReservation(base.TestCase):
         mock_fetch.assert_called_with(self.node, self.session)
 
 
+@mock.patch.object(exceptions, 'raise_from_response', mock.Mock())
+class TestNodeInjectNMI(base.TestCase):
+
+    def setUp(self):
+        super().setUp()
+        self.node = node.Node(**FAKE)
+        self.session = mock.Mock(spec=adapter.Adapter)
+        self.session.default_microversion = '1.29'
+        self.node = node.Node(**FAKE)
+
+    def test_inject_nmi(self):
+        self.node.inject_nmi(self.session)
+        self.session.put.assert_called_once_with(
+            'nodes/%s/management/inject_nmi' % FAKE['uuid'],
+            json={},
+            headers=mock.ANY,
+            microversion='1.29',
+            retriable_status_codes=_common.RETRIABLE_STATUS_CODES,
+        )
+
+    def test_incompatible_microversion(self):
+        self.session.default_microversion = '1.28'
+        self.assertRaises(
+            exceptions.NotSupported,
+            self.node.inject_nmi,
+            self.session,
+        )
+
+
 @mock.patch.object(node.Node, '_assert_microversion_for', _fake_assert)
 @mock.patch.object(exceptions, 'raise_from_response', mock.Mock())
 class TestNodeSetPowerState(base.TestCase):
diff --git a/releasenotes/notes/node-inject-nmi-53d12681026e0b6c.yaml b/releasenotes/notes/node-inject-nmi-53d12681026e0b6c.yaml
new file mode 100644
index 000000000..41f065451
--- /dev/null
+++ b/releasenotes/notes/node-inject-nmi-53d12681026e0b6c.yaml
@@ -0,0 +1,6 @@
+---
+features:
+  - |
+    Adds ``inject_nmi`` ``openstack.baremetal.v1.Node``.
+  - |
+    Adds ``inject_nmi_to_node`` to the baremetal Proxy.