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:
parent
85322e5a4f
commit
1512448f4f
|
@ -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
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue