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::
|
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.
|
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
|
Query introspection status
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
@ -59,9 +59,11 @@ def build_option_parser(parser):
|
|||||||
return parser
|
return parser
|
||||||
|
|
||||||
|
|
||||||
class StartCommand(command.Command):
|
class StartCommand(lister.Lister):
|
||||||
"""Start the introspection."""
|
"""Start the introspection."""
|
||||||
|
|
||||||
|
COLUMNS = ('UUID', 'Error')
|
||||||
|
|
||||||
def get_parser(self, prog_name):
|
def get_parser(self, prog_name):
|
||||||
parser = super(StartCommand, self).get_parser(prog_name)
|
parser = super(StartCommand, self).get_parser(prog_name)
|
||||||
parser.add_argument('uuid', help='baremetal node UUID(s)', nargs='+')
|
parser.add_argument('uuid', help='baremetal node UUID(s)', nargs='+')
|
||||||
@ -73,6 +75,10 @@ class StartCommand(command.Command):
|
|||||||
default=None,
|
default=None,
|
||||||
help='if set, *Ironic Inspector* will update IPMI '
|
help='if set, *Ironic Inspector* will update IPMI '
|
||||||
'password to this value')
|
'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
|
return parser
|
||||||
|
|
||||||
def take_action(self, parsed_args):
|
def take_action(self, parsed_args):
|
||||||
@ -83,7 +89,17 @@ class StartCommand(command.Command):
|
|||||||
new_ipmi_password=parsed_args.new_ipmi_password)
|
new_ipmi_password=parsed_args.new_ipmi_password)
|
||||||
if parsed_args.new_ipmi_password:
|
if parsed_args.new_ipmi_password:
|
||||||
print('Setting IPMI credentials requested, please power on '
|
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):
|
class StatusCommand(show.ShowOne):
|
||||||
|
@ -38,8 +38,9 @@ class TestIntrospect(BaseTest):
|
|||||||
|
|
||||||
cmd = shell.StartCommand(self.app, None)
|
cmd = shell.StartCommand(self.app, None)
|
||||||
parsed_args = self.check_parser(cmd, arglist, verifylist)
|
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',
|
self.client.introspect.assert_called_once_with('uuid1',
|
||||||
new_ipmi_password=None,
|
new_ipmi_password=None,
|
||||||
new_ipmi_username=None)
|
new_ipmi_username=None)
|
||||||
@ -103,6 +104,27 @@ class TestIntrospect(BaseTest):
|
|||||||
for uuid in uuids]
|
for uuid in uuids]
|
||||||
self.assertEqual(calls, self.client.introspect.call_args_list)
|
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):
|
class TestRules(BaseTest):
|
||||||
def test_import_single(self):
|
def test_import_single(self):
|
||||||
|
@ -20,6 +20,7 @@ from oslo_utils import uuidutils
|
|||||||
|
|
||||||
import ironic_inspector_client
|
import ironic_inspector_client
|
||||||
from ironic_inspector_client.common import http
|
from ironic_inspector_client.common import http
|
||||||
|
from ironic_inspector_client import v1
|
||||||
|
|
||||||
|
|
||||||
FAKE_HEADERS = {
|
FAKE_HEADERS = {
|
||||||
@ -114,6 +115,59 @@ class TestGetStatus(BaseTest):
|
|||||||
self.assertRaises(TypeError, self.get_client().get_status, 42)
|
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')
|
@mock.patch.object(http.BaseClient, 'request')
|
||||||
class TestGetData(BaseTest):
|
class TestGetData(BaseTest):
|
||||||
def test_json(self, mock_req):
|
def test_json(self, mock_req):
|
||||||
|
@ -13,6 +13,9 @@
|
|||||||
|
|
||||||
"""Client for V1 API."""
|
"""Client for V1 API."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
|
||||||
import six
|
import six
|
||||||
|
|
||||||
from ironic_inspector_client.common import http
|
from ironic_inspector_client.common import http
|
||||||
@ -22,6 +25,17 @@ from ironic_inspector_client.common.i18n import _
|
|||||||
DEFAULT_API_VERSION = (1, 0)
|
DEFAULT_API_VERSION = (1, 0)
|
||||||
MAX_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):
|
class ClientV1(http.BaseClient):
|
||||||
"""Client for API v1."""
|
"""Client for API v1."""
|
||||||
@ -67,6 +81,8 @@ class ClientV1(http.BaseClient):
|
|||||||
:raises: ClientError on error reported from a server
|
:raises: ClientError on error reported from a server
|
||||||
:raises: VersionNotSupported if requested api_version is not supported
|
:raises: VersionNotSupported if requested api_version is not supported
|
||||||
:raises: *requests* library exception on connection problems.
|
: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):
|
if not isinstance(uuid, six.string_types):
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
@ -74,6 +90,49 @@ class ClientV1(http.BaseClient):
|
|||||||
|
|
||||||
return self.request('get', '/introspection/%s' % uuid).json()
|
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):
|
def get_data(self, uuid, raw=False):
|
||||||
"""Get introspection data from the last introspection of a node.
|
"""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