From 8b31d0bb1d8789e2f3b136f844a451cdf87ef651 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Fri, 22 Jan 2016 18:24:34 +0100 Subject: [PATCH] Add --wait flag to 'introspection start' to wait for results Change-Id: I8b338600f809bb232deca40c99f5649ea3bace4f Closes-Bug: #1480649 --- README.rst | 5 +- ironic_inspector_client/shell.py | 20 ++++++- ironic_inspector_client/test/test_shell.py | 24 +++++++- ironic_inspector_client/test/test_v1.py | 54 +++++++++++++++++ ironic_inspector_client/v1.py | 59 +++++++++++++++++++ .../introspection-wait-a7e8fe832c3aaff9.yaml | 3 + 6 files changed, 161 insertions(+), 4 deletions(-) create mode 100644 releasenotes/notes/introspection-wait-a7e8fe832c3aaff9.yaml diff --git a/README.rst b/README.rst index f5b19cb..100501c 100644 --- a/README.rst +++ b/README.rst @@ -79,10 +79,13 @@ Start introspection on a node CLI:: - $ openstack baremetal introspection start [--new-ipmi-password=PWD [--new-ipmi-username=USER]] UUID [UUID ...] + $ openstack baremetal introspection start [--wait] [--new-ipmi-password=PWD [--new-ipmi-username=USER]] UUID [UUID ...] Note that the CLI call accepts several UUID's and will stop on the first error. +With ``--wait`` flag it waits until introspection ends for all given nodes, +then displays the results as a table. + Query introspection status ~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/ironic_inspector_client/shell.py b/ironic_inspector_client/shell.py index 00fb4c7..ea6c001 100644 --- a/ironic_inspector_client/shell.py +++ b/ironic_inspector_client/shell.py @@ -59,9 +59,11 @@ def build_option_parser(parser): return parser -class StartCommand(command.Command): +class StartCommand(lister.Lister): """Start the introspection.""" + COLUMNS = ('UUID', 'Error') + def get_parser(self, prog_name): parser = super(StartCommand, self).get_parser(prog_name) parser.add_argument('uuid', help='baremetal node UUID(s)', nargs='+') @@ -73,6 +75,10 @@ class StartCommand(command.Command): default=None, help='if set, *Ironic Inspector* will update IPMI ' 'password to this value') + parser.add_argument('--wait', + action='store_true', + help='wait for introspection to finish; the result' + ' will be displayed in the end') return parser def take_action(self, parsed_args): @@ -83,7 +89,17 @@ class StartCommand(command.Command): new_ipmi_password=parsed_args.new_ipmi_password) if parsed_args.new_ipmi_password: print('Setting IPMI credentials requested, please power on ' - 'the machine manually') + 'the machine manually', file=sys.stderr) + + if parsed_args.wait: + print('Waiting for introspection to finish...', file=sys.stderr) + result = client.wait_for_finish(parsed_args.uuid) + result = [(uuid, s.get('error')) + for uuid, s in result.items()] + else: + result = [] + + return self.COLUMNS, result class StatusCommand(show.ShowOne): diff --git a/ironic_inspector_client/test/test_shell.py b/ironic_inspector_client/test/test_shell.py index ef9e7fa..0d443a9 100644 --- a/ironic_inspector_client/test/test_shell.py +++ b/ironic_inspector_client/test/test_shell.py @@ -38,8 +38,9 @@ class TestIntrospect(BaseTest): cmd = shell.StartCommand(self.app, None) parsed_args = self.check_parser(cmd, arglist, verifylist) - cmd.take_action(parsed_args) + result = cmd.take_action(parsed_args) + self.assertEqual((shell.StartCommand.COLUMNS, []), result) self.client.introspect.assert_called_once_with('uuid1', new_ipmi_password=None, new_ipmi_username=None) @@ -103,6 +104,27 @@ class TestIntrospect(BaseTest): for uuid in uuids] self.assertEqual(calls, self.client.introspect.call_args_list) + def test_wait(self): + nodes = ['uuid1', 'uuid2', 'uuid3'] + arglist = ['--wait'] + nodes + verifylist = [('uuid', nodes), ('wait', True)] + self.client.wait_for_finish.return_value = { + 'uuid1': {'finished': True, 'error': None}, + 'uuid2': {'finished': True, 'error': 'boom'}, + 'uuid3': {'finished': True, 'error': None}, + } + + cmd = shell.StartCommand(self.app, None) + parsed_args = self.check_parser(cmd, arglist, verifylist) + _c, values = cmd.take_action(parsed_args) + + calls = [mock.call(uuid, new_ipmi_password=None, + new_ipmi_username=None) + for uuid in nodes] + self.assertEqual(calls, self.client.introspect.call_args_list) + self.assertEqual([('uuid1', None), ('uuid2', 'boom'), ('uuid3', None)], + sorted(values)) + class TestRules(BaseTest): def test_import_single(self): diff --git a/ironic_inspector_client/test/test_v1.py b/ironic_inspector_client/test/test_v1.py index 5fe52fc..7d8bb39 100644 --- a/ironic_inspector_client/test/test_v1.py +++ b/ironic_inspector_client/test/test_v1.py @@ -19,6 +19,7 @@ from oslo_utils import uuidutils import ironic_inspector_client from ironic_inspector_client.common import http +from ironic_inspector_client import v1 FAKE_HEADERS = { @@ -110,6 +111,59 @@ class TestGetStatus(BaseTest): self.assertRaises(TypeError, self.get_client().get_status, 42) +@mock.patch.object(ironic_inspector_client.ClientV1, 'get_status', + autospec=True) +class TestWaitForFinish(BaseTest): + def setUp(self): + super(TestWaitForFinish, self).setUp() + self.sleep = mock.Mock(spec=[]) + + def test_ok(self, mock_get_st): + mock_get_st.side_effect = ( + [{'finished': False, 'error': None}] * 5 + + [{'finished': True, 'error': None}] + ) + + res = self.get_client().wait_for_finish(['uuid1'], + sleep_function=self.sleep) + self.assertEqual({'uuid1': {'finished': True, 'error': None}}, + res) + self.sleep.assert_called_with(v1.DEFAULT_RETRY_INTERVAL) + self.assertEqual(5, self.sleep.call_count) + + def test_timeout(self, mock_get_st): + mock_get_st.return_value = {'finished': False, 'error': None} + + self.assertRaises(v1.WaitTimeoutError, + self.get_client().wait_for_finish, + ['uuid1'], sleep_function=self.sleep) + self.sleep.assert_called_with(v1.DEFAULT_RETRY_INTERVAL) + self.assertEqual(v1.DEFAULT_MAX_RETRIES, self.sleep.call_count) + + def test_multiple(self, mock_get_st): + mock_get_st.side_effect = [ + # attempt 1 + {'finished': False, 'error': None}, + {'finished': False, 'error': None}, + {'finished': False, 'error': None}, + # attempt 2 + {'finished': True, 'error': None}, + {'finished': False, 'error': None}, + {'finished': True, 'error': 'boom'}, + # attempt 3 (only uuid2) + {'finished': True, 'error': None}, + ] + + res = self.get_client().wait_for_finish(['uuid1', 'uuid2', 'uuid3'], + sleep_function=self.sleep) + self.assertEqual({'uuid1': {'finished': True, 'error': None}, + 'uuid2': {'finished': True, 'error': None}, + 'uuid3': {'finished': True, 'error': 'boom'}}, + res) + self.sleep.assert_called_with(v1.DEFAULT_RETRY_INTERVAL) + self.assertEqual(2, self.sleep.call_count) + + @mock.patch.object(http.BaseClient, 'request') class TestGetData(BaseTest): def test_json(self, mock_req): diff --git a/ironic_inspector_client/v1.py b/ironic_inspector_client/v1.py index fa1de8e..09a9ee8 100644 --- a/ironic_inspector_client/v1.py +++ b/ironic_inspector_client/v1.py @@ -13,6 +13,9 @@ """Client for V1 API.""" +import logging +import time + import six from ironic_inspector_client.common import http @@ -22,6 +25,17 @@ from ironic_inspector_client.common.i18n import _ DEFAULT_API_VERSION = (1, 0) MAX_API_VERSION = (1, 0) +# using huge timeout by default, as precise timeout should be set in +# ironic-inspector settings +DEFAULT_RETRY_INTERVAL = 10 +DEFAULT_MAX_RETRIES = 3600 + +LOG = logging.getLogger(__name__) + + +class WaitTimeoutError(Exception): + """Timeout while waiting for nodes to finish introspection.""" + class ClientV1(http.BaseClient): """Client for API v1.""" @@ -67,6 +81,8 @@ class ClientV1(http.BaseClient): :raises: ClientError on error reported from a server :raises: VersionNotSupported if requested api_version is not supported :raises: *requests* library exception on connection problems. + :return: dictionary with keys "finished" (True/False) and "error" + (error string or None). """ if not isinstance(uuid, six.string_types): raise TypeError( @@ -74,6 +90,49 @@ class ClientV1(http.BaseClient): return self.request('get', '/introspection/%s' % uuid).json() + def wait_for_finish(self, uuids, retry_interval=DEFAULT_RETRY_INTERVAL, + max_retries=DEFAULT_MAX_RETRIES, + sleep_function=time.sleep): + """Wait for introspection finishing for given nodes. + + :param uuids: collection of node uuid's. + :param retry_interval: sleep interval between retries. + :param max_retries: maximum number of retries. + :param sleep_function: function used for sleeping between retries. + :raises: WaitTimeoutError on timeout + :raises: ClientError on error reported from a server + :raises: VersionNotSupported if requested api_version is not supported + :raises: *requests* library exception on connection problems. + :return: dictionary UUID -> status (the same as in get_status). + """ + result = {} + + # Number of attempts = number of retries + first attempt + for attempt in range(max_retries + 1): + new_active_uuids = [] + for uuid in uuids: + status = self.get_status(uuid) + if status.get('finished'): + result[uuid] = status + else: + new_active_uuids.append(uuid) + + if new_active_uuids: + if attempt != max_retries: + uuids = new_active_uuids + LOG.debug('Still waiting for introspection results for ' + '%(count)d nodes, attempt %(attempt)d of ' + '%(total)d', + {'count': len(new_active_uuids), + 'attempt': attempt + 1, + 'total': max_retries + 1}) + sleep_function(retry_interval) + else: + return result + + raise WaitTimeoutError(_("Timeout while waiting for introspection " + "of nodes %s") % new_active_uuids) + def get_data(self, uuid, raw=False): """Get introspection data from the last introspection of a node. diff --git a/releasenotes/notes/introspection-wait-a7e8fe832c3aaff9.yaml b/releasenotes/notes/introspection-wait-a7e8fe832c3aaff9.yaml new file mode 100644 index 0000000..b6b35a3 --- /dev/null +++ b/releasenotes/notes/introspection-wait-a7e8fe832c3aaff9.yaml @@ -0,0 +1,3 @@ +--- +features: + - Introspection command got --wait flag to wait for introspection finish.