Merge "Add --wait flag to 'introspection start' to wait for results"
This commit is contained in:
commit
1b9bb4e3cf
@ -80,10 +80,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
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -20,6 +20,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 = {
|
||||
@ -114,6 +115,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):
|
||||
|
@ -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.
|
||||
|
||||
|
@ -0,0 +1,3 @@
|
||||
---
|
||||
features:
|
||||
- Introspection command got --wait flag to wait for introspection finish.
|
Loading…
Reference in New Issue
Block a user