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

View File

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

View File

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

View File

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

View File

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

View File

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