Merge "Add --wait flag to 'introspection start' to wait for results"

This commit is contained in:
Jenkins 2016-02-03 13:22:50 +00:00 committed by Gerrit Code Review
commit 1b9bb4e3cf
6 changed files with 161 additions and 4 deletions

View File

@ -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
~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -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):

View File

@ -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):

View File

@ -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):

View File

@ -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.

View File

@ -0,0 +1,3 @@
---
features:
- Introspection command got --wait flag to wait for introspection finish.