List introspection statuses support

This patch adds support to list introspection statuses as provided by
the Ironic Inspector service.

The CLI code in shell.py:StatusCommand.take_action was refactored to
be able to be reused in the new CLI class shell.py:StatusListCommand.

Change-Id: I121306c158bf71eaa83973cdbd404876a6f8b479
This commit is contained in:
dparalen 2016-12-07 12:08:38 +01:00
parent 85322e5a4f
commit 1512448f4f
8 changed files with 230 additions and 3 deletions

View File

@ -52,6 +52,29 @@ Returns following information about a node introspection status:
* ``started_at``: an ISO8601 timestamp
* ``uuid``: node UUID
List introspection statuses
~~~~~~~~~~~~~~~~~~~~~~~~~~~
This command supports pagination.
::
$ openstack baremetal introspection list [--marker] [--limit]
* ``--marker`` the last item on the previous page, a UUID
* ``--limit`` the amount of items to list, an integer, 50 by default
Shows a table with the following columns:
* ``Error``: an error string or ``None``
* ``Finished at``: an ISO8601 timestamp or ``None`` if not finished
* ``Started at``: and ISO8601 timestamp
* ``UUID``: node UUID
.. note::
The server orders the introspection status items according to the
``Started at`` column, newer items first.
Retrieving introspection data
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -117,6 +117,18 @@ class StatusCommand(command.ShowOne):
"""Get introspection status."""
hidden_status_items = {'links'}
@classmethod
def status_attributes(cls, client_item):
"""Get status attributes from an API client dict.
Filters the status fields according to the cls.hidden_status_items
:param client_item: an item returned from either the get_status or the
list_statuses client method
:return: introspection status as a list of name, value pairs
"""
return [item for item in client_item.items()
if item[0] not in cls.hidden_status_items]
def get_parser(self, prog_name):
parser = super(StatusCommand, self).get_parser(prog_name)
parser.add_argument('uuid', help='baremetal node UUID')
@ -125,8 +137,41 @@ class StatusCommand(command.ShowOne):
def take_action(self, parsed_args):
client = self.app.client_manager.baremetal_introspection
status = client.get_status(parsed_args.uuid)
return zip(*sorted(item for item in status.items()
if item[0] not in self.hidden_status_items))
return zip(*sorted(self.status_attributes(status)))
class StatusListCommand(command.Lister):
"""List introspection statuses"""
COLUMNS = ('UUID', 'Started at', 'Finished at', 'Error')
@classmethod
def status_row(cls, client_item):
"""Get a row from a client_item.
The row columns are filtered&sorted according to cls.COLUMNS.
:param client_item: an item returned from either the get_status or the
list_statuses client method.
:return: a list of client_item attributes as the row
"""
status = dict(StatusCommand.status_attributes(client_item))
return utils.get_dict_properties(status, cls.COLUMNS)
def get_parser(self, prog_name):
parser = super(StatusListCommand, self).get_parser(prog_name)
parser.add_argument('--marker', help='UUID of the last item on the '
'previous page', default=None)
parser.add_argument('--limit', help='the amount of items to return',
type=int, default=None)
return parser
def take_action(self, parsed_args):
client = self.app.client_manager.baremetal_introspection
statuses = client.list_statuses(marker=parsed_args.marker,
limit=parsed_args.limit)
rows = [self.status_row(status) for status in statuses]
return self.COLUMNS, rows
class AbortCommand(command.Command):

View File

@ -36,6 +36,10 @@ class TestV1PythonAPI(functional.Base):
self.client = client.ClientV1()
functional.cfg.CONF.set_override('store_data', '', 'processing')
def my_status_index(self, statuses):
my_status = self._fake_status()
return statuses.index(my_status)
def test_introspect_get_status(self):
self.client.introspect(self.uuid)
eventlet.greenthread.sleep(functional.DEFAULT_SLEEP)
@ -56,6 +60,28 @@ class TestV1PythonAPI(functional.Base):
status = self.client.get_status(self.uuid)
self.check_status(status, finished=True)
def test_introspect_list_statuses(self):
self.client.introspect(self.uuid)
eventlet.greenthread.sleep(functional.DEFAULT_SLEEP)
self.cli.node.set_power_state.assert_called_once_with(self.uuid,
'reboot')
statuses = self.client.list_statuses()
my_status = statuses[self.my_status_index(statuses)]
self.check_status(my_status, finished=False)
res = self.call_continue(self.data)
self.assertEqual({'uuid': self.uuid}, res)
eventlet.greenthread.sleep(functional.DEFAULT_SLEEP)
self.assertCalledWithPatch(self.patch, self.cli.node.update)
self.cli.port.create.assert_called_once_with(
node_uuid=self.uuid, address='11:22:33:44:55:66')
statuses = self.client.list_statuses()
my_status = statuses[self.my_status_index(statuses)]
self.check_status(my_status, finished=True)
def test_wait_for_finish(self):
shared = [0] # mutable structure to hold number of retries

View File

@ -171,6 +171,60 @@ class TestGetStatus(BaseTest):
self.client.get_status.assert_called_once_with('uuid1')
class TestStatusList(BaseTest):
def setUp(self):
super(TestStatusList, self).setUp()
self.COLUMNS = ('UUID', 'Started at', 'Finished at', 'Error')
self.status1 = {
'error': None,
'finished': True,
'finished_at': '1970-01-01T00:10',
'links': None,
'started_at': '1970-01-01T00:00',
'uuid': 'uuid1'
}
self.status2 = {
'error': None,
'finished': False,
'finished_at': None,
'links': None,
'started_at': '1970-01-01T00:01',
'uuid': 'uuid2'
}
def status_row(self, status):
status = dict(item for item in status.items()
if item[0] != 'links')
return (status['uuid'], status['started_at'], status['finished_at'],
status['error'])
def test_list_statuses(self):
status_list = [self.status1, self.status2]
self.client.list_statuses.return_value = status_list
arglist = []
verifylist = []
cmd = shell.StatusListCommand(self.app, None)
parsed_args = self.check_parser(cmd, arglist, verifylist)
result = cmd.take_action(parsed_args)
self.assertEqual((self.COLUMNS, [self.status_row(status)
for status in status_list]),
result)
self.client.list_statuses.assert_called_once_with(limit=None,
marker=None)
def test_list_statuses_marker_limit(self):
self.client.list_statuses.return_value = []
arglist = ['--marker', 'uuid1', '--limit', '42']
verifylist = [('marker', 'uuid1'), ('limit', 42)]
cmd = shell.StatusListCommand(self.app, None)
parsed_args = self.check_parser(cmd, arglist, verifylist)
result = cmd.take_action(parsed_args)
self.assertEqual((self.COLUMNS, []), result)
self.client.list_statuses.assert_called_once_with(limit=42,
marker='uuid1')
class TestRules(BaseTest):
def test_import_single(self):
f = tempfile.NamedTemporaryFile()

View File

@ -11,6 +11,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import six
import unittest
from keystoneauth1 import session
@ -129,6 +130,41 @@ class TestGetStatus(BaseTest):
self.assertRaises(TypeError, self.get_client().get_status, 42)
@mock.patch.object(http.BaseClient, 'request')
class TestListStatuses(BaseTest):
def test_default(self, mock_req):
mock_req.return_value.json.return_value = {
'introspection': None
}
params = {
'marker': None,
'limit': None
}
self.get_client().list_statuses()
mock_req.assert_called_once_with('get', '/introspection',
params=params)
def test_nondefault(self, mock_req):
mock_req.return_value.json.return_value = {
'introspection': None
}
params = {
'marker': 'uuid',
'limit': 42
}
self.get_client().list_statuses(**params)
mock_req.assert_called_once_with('get', '/introspection',
params=params)
def test_invalid_marker(self, _):
six.assertRaisesRegex(self, TypeError, 'Expected a string value.*',
self.get_client().list_statuses, marker=42)
def test_invalid_limit(self, _):
six.assertRaisesRegex(self, TypeError, 'Expected an integer.*',
self.get_client().list_statuses, limit='42')
@mock.patch.object(ironic_inspector_client.ClientV1, 'get_status',
autospec=True)
class TestWaitForFinish(BaseTest):

View File

@ -25,7 +25,7 @@ from ironic_inspector_client.common.i18n import _
DEFAULT_API_VERSION = (1, 0)
"""Server API version used by default."""
MAX_API_VERSION = (1, 7)
MAX_API_VERSION = (1, 8)
"""Maximum API version this client was designed to work with.
This does not mean that other versions won't work at all - the server might
@ -130,6 +130,41 @@ class ClientV1(http.BaseClient):
'/introspection/%s/data/unprocessed' %
uuid)
def list_statuses(self, marker=None, limit=None):
"""List introspection statuses.
Supports pagination via the marker and limit params. The items are
sorted by the server according to the `started_at` attribute, newer
items first.
:param marker: pagination maker, UUID or None
:param limit: pagination limit, int or None
:raises: :py:class:`.ClientError` on error reported from a server
:raises: :py:class:`.VersionNotSupported` if requested api_version
is not supported
:raises: *requests* library exception on connection problems.
:return: a list of status dictionaries with the keys:
`error` an error string or None,
`finished` True/False,
`finished_at` an ISO8601 timestamp or None,
`links` with a self-link URL,
`started_at` an ISO8601 timestamp,
`uuid` the node UUID
"""
if not (marker is None or isinstance(marker, six.string_types)):
raise TypeError(_('Expected a string value of the marker, got '
'%s instead') % marker)
if not (limit is None or isinstance(limit, int)):
raise TypeError(_('Expected an integer value of the limit, got '
'%s instead') % limit)
params = {
'marker': marker,
'limit': limit,
}
response = self.request('get', '/introspection', params=params)
return response.json()['introspection']
def get_status(self, uuid):
"""Get introspection status for a node.

View File

@ -0,0 +1,7 @@
---
features:
- |
Add support for listing introspection statuses both for the API and the CLI
upgrade:
- |
Service max API version bumped to 1.8

View File

@ -26,6 +26,7 @@ openstack.cli.extension =
openstack.baremetal_introspection.v1 =
baremetal_introspection_start = ironic_inspector_client.shell:StartCommand
baremetal_introspection_status = ironic_inspector_client.shell:StatusCommand
baremetal_introspection_list = ironic_inspector_client.shell:StatusListCommand
baremetal_introspection_reprocess = ironic_inspector_client.shell:ReprocessCommand
baremetal_introspection_abort = ironic_inspector_client.shell:AbortCommand
baremetal_introspection_data_save = ironic_inspector_client.shell:DataSaveCommand