diff --git a/README.rst b/README.rst index 7ecc00f..bb3f63c 100644 --- a/README.rst +++ b/README.rst @@ -140,6 +140,21 @@ CLI:: $ openstack baremetal introspection abort UUID +Reprocess stored introspection data +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``client.reprocess(uuid)`` + +* ``uuid`` - Ironic node UUID. + +CLI:: + + $ openstack baremetal introspection reprocess UUID + +.. note:: + This feature requires Swift store to be enabled for **Ironic Inspector** + by setting ``[processing]store_data`` configuration option to ``swift``. + Introspection Rules API ~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/ironic_inspector_client/shell.py b/ironic_inspector_client/shell.py index 65a8537..b76c98e 100644 --- a/ironic_inspector_client/shell.py +++ b/ironic_inspector_client/shell.py @@ -104,6 +104,19 @@ class StartCommand(lister.Lister): return self.COLUMNS, result +class ReprocessCommand(command.Command): + """Reprocess stored introspection data""" + + def get_parser(self, prog_name): + parser = super(ReprocessCommand, self).get_parser(prog_name) + parser.add_argument('uuid', help='baremetal node UUID') + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.baremetal_introspection + client.reprocess(parsed_args.uuid) + + class StatusCommand(show.ShowOne): """Get introspection status.""" diff --git a/ironic_inspector_client/test/functional.py b/ironic_inspector_client/test/functional.py index 6c35a4d..2b8ce70 100644 --- a/ironic_inspector_client/test/functional.py +++ b/ironic_inspector_client/test/functional.py @@ -14,9 +14,12 @@ import eventlet eventlet.monkey_patch() +import json +import mock import requests import unittest +from ironic_inspector.common import swift from ironic_inspector.test import functional import ironic_inspector_client as client @@ -26,6 +29,7 @@ class TestV1PythonAPI(functional.Base): def setUp(self): super(TestV1PythonAPI, self).setUp() self.client = client.ClientV1() + functional.cfg.CONF.set_override('store_data', '', 'processing') def test_introspect_get_status(self): self.client.introspect(self.uuid) @@ -47,6 +51,53 @@ class TestV1PythonAPI(functional.Base): status = self.client.get_status(self.uuid) self.assertEqual({'finished': True, 'error': None}, status) + @mock.patch.object(swift, 'store_introspection_data', autospec=True) + @mock.patch.object(swift, 'get_introspection_data', autospec=True) + def test_reprocess_stored_introspection_data(self, get_mock, + store_mock): + functional.cfg.CONF.set_override('store_data', 'swift', 'processing') + port_create_call = mock.call(node_uuid=self.uuid, + address='11:22:33:44:55:66') + get_mock.return_value = json.dumps(self.data) + + # assert reprocessing doesn't work before introspection + self.assertRaises(client.ClientError, self.client.reprocess, + self.uuid) + + self.client.introspect(self.uuid) + eventlet.greenthread.sleep(functional.DEFAULT_SLEEP) + self.cli.node.set_power_state.assert_called_once_with(self.uuid, + 'reboot') + status = self.client.get_status(self.uuid) + self.assertEqual({'finished': False, 'error': None}, + status) + + res = self.call_continue(self.data) + self.assertEqual({'uuid': self.uuid}, res) + eventlet.greenthread.sleep(functional.DEFAULT_SLEEP) + + status = self.client.get_status(self.uuid) + self.assertEqual({'finished': True, 'error': None}, + status) + self.cli.port.create.assert_has_calls([port_create_call], + any_order=True) + self.assertFalse(get_mock.called) + self.assertTrue(store_mock.called) + + res = self.client.reprocess(self.uuid) + self.assertEqual(202, res.status_code) + self.assertEqual('', res.text) + eventlet.greenthread.sleep(functional.DEFAULT_SLEEP) + self.assertEqual({'finished': True, 'error': None}, + status) + + self.cli.port.create.assert_has_calls([port_create_call, + port_create_call], + any_order=True) + self.assertTrue(get_mock.called) + # incoming, processing, reapplying data + self.assertEqual(3, store_mock.call_count) + def test_abort_introspection(self): # assert abort doesn't work before introspect request self.assertRaises(client.ClientError, self.client.abort, diff --git a/ironic_inspector_client/test/test_shell.py b/ironic_inspector_client/test/test_shell.py index 7943060..83ef46d 100644 --- a/ironic_inspector_client/test/test_shell.py +++ b/ironic_inspector_client/test/test_shell.py @@ -104,6 +104,21 @@ class TestIntrospect(BaseTest): for uuid in uuids] self.assertEqual(calls, self.client.introspect.call_args_list) + def test_reprocess(self): + node = 'uuid1' + arglist = [node] + verifylist = [('uuid', node)] + response_mock = mock.Mock(status_code=202, content=b'') + self.client.reprocess.return_value = response_mock + + cmd = shell.ReprocessCommand(self.app, None) + + parsed_args = self.check_parser(cmd, arglist, verifylist) + result = cmd.take_action(parsed_args) + + self.client.reprocess.assert_called_once_with(node) + self.assertIs(None, result) + def test_wait(self): nodes = ['uuid1', 'uuid2', 'uuid3'] arglist = ['--wait'] + nodes diff --git a/ironic_inspector_client/test/test_v1.py b/ironic_inspector_client/test/test_v1.py index 38fece7..5824712 100644 --- a/ironic_inspector_client/test/test_v1.py +++ b/ironic_inspector_client/test/test_v1.py @@ -101,6 +101,20 @@ class TestIntrospect(BaseTest): params={'new_ipmi_username': 'u', 'new_ipmi_password': 'p'}) +@mock.patch.object(http.BaseClient, 'request') +class TestReprocess(BaseTest): + def test(self, mock_req): + self.get_client().reprocess(self.uuid) + mock_req.assert_called_once_with( + 'post', + '/introspection/%s/data/unprocessed' % self.uuid + ) + + def test_invalid_input(self, mock_req): + self.assertRaises(TypeError, self.get_client().reprocess, 42) + self.assertFalse(mock_req.called) + + @mock.patch.object(http.BaseClient, 'request') class TestGetStatus(BaseTest): def test(self, mock_req): diff --git a/ironic_inspector_client/v1.py b/ironic_inspector_client/v1.py index 8d20c32..9bfe97a 100644 --- a/ironic_inspector_client/v1.py +++ b/ironic_inspector_client/v1.py @@ -23,7 +23,7 @@ from ironic_inspector_client.common.i18n import _ DEFAULT_API_VERSION = (1, 0) -MAX_API_VERSION = (1, 3) +MAX_API_VERSION = (1, 4) # using huge timeout by default, as precise timeout should be set in # ironic-inspector settings @@ -74,6 +74,23 @@ class ClientV1(http.BaseClient): 'new_ipmi_password': new_ipmi_password} self.request('post', '/introspection/%s' % uuid, params=params) + def reprocess(self, uuid): + """Reprocess stored introspection data. + + :param uuid: node UUID. + :raises: ClientError on error reported from the server. + :raises: VersionNotSupported if requested api_version is not supported. + :raises: *requests* library exception on connection problems. + :raises: TypeError if uuid is not a string. + """ + if not isinstance(uuid, six.string_types): + raise TypeError(_("Expected string for uuid argument, got" + " %r instead") % uuid) + + return self.request('post', + '/introspection/%s/data/unprocessed' % + uuid) + def get_status(self, uuid): """Get introspection status for a node. diff --git a/releasenotes/notes/reprocess-stored-introspection-data-c4910325254426c5.yaml b/releasenotes/notes/reprocess-stored-introspection-data-c4910325254426c5.yaml new file mode 100644 index 0000000..5b3b64a --- /dev/null +++ b/releasenotes/notes/reprocess-stored-introspection-data-c4910325254426c5.yaml @@ -0,0 +1,4 @@ +--- +features: + - Introduced command "openstack baremetal introspection reprocess " + to reprocess stored introspection data diff --git a/setup.cfg b/setup.cfg index 4dbc686..5cf273b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -23,6 +23,7 @@ openstack.cli.extension = openstack.baremetal_introspection.v1 = baremetal_introspection_start = ironic_inspector_client.shell:StartCommand baremetal_introspection_status = ironic_inspector_client.shell:StatusCommand + baremetal_introspection_reprocess = ironic_inspector_client.shell:ReprocessCommand baremetal_introspection_abort = ironic_inspector_client.shell:AbortCommand baremetal_introspection_data_save = ironic_inspector_client.shell:DataSaveCommand baremetal_introspection_rule_import = ironic_inspector_client.shell:RuleImportCommand