Add support for state selector in the list introspection

This commit add support for state selector to the list introspection.

* ``GET /v1/introspection?state=[starting, waiting, processing,
                                 finished, error, reapplying,
				 enrolling]``

Story: 1625183
Task: 11350
Change-Id: I2c5222110487a08a4e7b1efbcbc5dc3d552fae3e
This commit is contained in:
Tadeas Kot 2021-09-06 14:52:43 +02:00
parent 00c2e99b5a
commit ff93c7799f
9 changed files with 100 additions and 8 deletions

View File

@ -57,6 +57,7 @@ Status list may be paginated with these query string fields:
- marker: marker
- limit: limit
- state: state
Response

View File

@ -107,3 +107,4 @@ Version History
* **1.15** allows reapply with provided introspection data from request.
* **1.16** adds ``scope`` field to introspection rule.
* **1.17** adds ``GET /v1/introspection/<node>/data/unprocessed``.
* **1.18** adds state selector ``GET /v1/introspection?state=starting,...``.

View File

@ -19,6 +19,7 @@ from oslo_config import cfg
from oslo_utils import uuidutils
from ironic_inspector.common.i18n import _
from ironic_inspector import introspection_state as istate
from ironic_inspector import utils
CONF = cfg.CONF
@ -82,3 +83,18 @@ def limit_field(value):
assert value >= 0, _('Limit cannot be negative')
assert value <= CONF.api_max_limit, _('Limit over %s') % CONF.api_max_limit
return value
@request_field('state')
@raises_coercion_exceptions
def state_field(value):
"""Fetch the pagination state field from flask.request.args.
:returns: list of the state(s)
"""
states = istate.States.all()
value = value.split(',')
invalid_states = [state for state in value if state not in states]
assert not invalid_states, \
_('State(s) "%s" are not valid') % ', '.join(invalid_states)
return value

View File

@ -44,7 +44,7 @@ _wsgi_app = _app.wsgi_app
LOG = utils.getProcessingLogger(__name__)
MINIMUM_API_VERSION = (1, 0)
CURRENT_API_VERSION = (1, 17)
CURRENT_API_VERSION = (1, 18)
DEFAULT_API_VERSION = CURRENT_API_VERSION
_LOGGING_EXCLUDED_KEYS = ('logs',)
@ -378,7 +378,8 @@ def api_introspection(node_id):
def api_introspection_statuses():
nodes = node_cache.get_node_list(
marker=api_tools.marker_field(),
limit=api_tools.limit_field(default=CONF.api_max_limit)
limit=api_tools.limit_field(default=CONF.api_max_limit),
state=api_tools.state_field()
)
data = {
'introspection': [generate_introspection_status(node)

View File

@ -976,7 +976,7 @@ def record_node(ironic=None, bmc_addresses=None, macs=None):
manage_boot=False, mac=macs, bmc_address=bmc_addresses)
def get_node_list(ironic=None, marker=None, limit=None):
def get_node_list(ironic=None, marker=None, limit=None, state=None):
"""Get node list from the cache.
The list of the nodes is ordered based on the (started_at, uuid)
@ -985,6 +985,7 @@ def get_node_list(ironic=None, marker=None, limit=None):
:param ironic: optional ironic client instance
:param marker: pagination marker (an UUID or None)
:param limit: pagination limit; None for default CONF.api_max_limit
:param state: list of states for the filter; None for no state filter
:returns: a list of NodeInfo instances.
"""
if marker is not None:
@ -995,6 +996,8 @@ def get_node_list(ironic=None, marker=None, limit=None):
code=404)
rows = db.model_query(db.Node)
if state:
rows = rows.filter(db.Node.state.in_(state))
# ordered based on (started_at, uuid); newer first
rows = db_utils.paginate_query(rows, db.Node, limit,
('started_at', 'uuid'),

View File

@ -140,3 +140,22 @@ class LimitFieldTestCase(test_base.BaseTest):
def test_limit_invalid_value(self, get_mock):
self.assertRaisesRegex(utils.Error, 'Bad request',
api_tools.limit_field)
class StateFieldTestCase(test_base.BaseTest):
@mock_test_field(return_value='error')
def test_single_state(self, get_mock):
value = api_tools.state_field()
self.assertEqual(get_mock.return_value.split(','), value)
@mock_test_field(return_value='error,finished')
def test_multiple_state(self, get_mock):
value = api_tools.state_field()
self.assertEqual(get_mock.return_value.split(','), value)
@mock_test_field(return_value='error,invalid')
def test_invalid_state(self, get_mock):
self.assertRaisesRegex(utils.Error,
r'.*(State\(s\) "%s" are not valid)'
% 'invalid',
api_tools.state_field)

View File

@ -343,19 +343,50 @@ class TestApiListStatus(GetStatusAPIBaseTest):
self.assertEqual([self.finished_node.status,
self.unfinished_node.status], statuses)
list_mock.assert_called_once_with(marker=None,
limit=CONF.api_max_limit)
limit=CONF.api_max_limit, state=None)
def test_list_introspection_limit(self, list_mock):
res = self.app.get('/v1/introspection?limit=1000')
self.assertEqual(200, res.status_code)
list_mock.assert_called_once_with(marker=None, limit=1000)
list_mock.assert_called_once_with(marker=None, limit=1000, state=None)
def test_list_introspection_makrer(self, list_mock):
res = self.app.get('/v1/introspection?marker=%s' %
self.finished_node.uuid)
self.assertEqual(200, res.status_code)
list_mock.assert_called_once_with(marker=self.finished_node.uuid,
limit=CONF.api_max_limit)
limit=CONF.api_max_limit, state=None)
def test_list_introspection_state(self, list_mock):
state = 'error'
list_mock.return_value = [self.finished_node]
res = self.app.get('/v1/introspection?state=%s' % state)
self.assertEqual(200, res.status_code)
statuses = json.loads(res.data.decode('utf-8')).get('introspection')
self.assertEqual([self.finished_node.status], statuses)
list_mock.assert_called_once_with(marker=None,
limit=CONF.api_max_limit,
state=state.split(','))
def test_list_introspection_multiple_state(self, list_mock):
state = 'error,processing'
list_mock.return_value = [self.finished_node,
self.unfinished_node]
res = self.app.get('/v1/introspection?state=%s' % state)
self.assertEqual(200, res.status_code)
statuses = json.loads(res.data.decode('utf-8')).get('introspection')
self.assertEqual([self.finished_node.status,
self.unfinished_node.status], statuses)
list_mock.assert_called_once_with(marker=None,
limit=CONF.api_max_limit,
state=state.split(','))
def test_list_introspection_invalid_state(self, list_mock):
state = 'error,invalid'
res = self.app.get('/v1/introspection?state=%s' % state)
self.assertEqual(400, res.status_code)
class TestApiGetData(BaseAPITest):

View File

@ -975,12 +975,15 @@ class TestNodeCacheListNode(test_base.NodeTest):
def setUp(self):
super(TestNodeCacheListNode, self).setUp()
self.uuid2 = uuidutils.generate_uuid()
self.uuid3 = uuidutils.generate_uuid()
session = db.get_writer_session()
with session.begin():
db.Node(uuid=self.uuid,
started_at=datetime.datetime(1, 1, 2)).save(session)
db.Node(uuid=self.uuid2, started_at=datetime.datetime(1, 1, 1),
finished_at=datetime.datetime(1, 1, 3)).save(session)
db.Node(uuid=self.uuid3, started_at=datetime.datetime(1, 1, 3),
state='error').save(session)
# mind please node(self.uuid).started_at > node(self.uuid2).started_at
# and the result ordering is strict in node_cache.get_node_list newer first
@ -988,12 +991,12 @@ class TestNodeCacheListNode(test_base.NodeTest):
def test_list_node(self):
nodes = node_cache.get_node_list()
self.assertEqual([self.uuid, self.uuid2],
self.assertEqual([self.uuid3, self.uuid, self.uuid2],
[node.uuid for node in nodes])
def test_list_node_limit(self):
nodes = node_cache.get_node_list(limit=1)
self.assertEqual([self.uuid], [node.uuid for node in nodes])
self.assertEqual([self.uuid3], [node.uuid for node in nodes])
def test_list_node_marker(self):
# get nodes started_at after node(self.uuid)
@ -1004,6 +1007,15 @@ class TestNodeCacheListNode(test_base.NodeTest):
self.assertRaises(utils.Error, node_cache.get_node_list,
marker='foo-bar')
def test_list_node_state(self):
nodes = node_cache.get_node_list(state=['error'])
self.assertEqual([self.uuid3], [node.uuid for node in nodes])
def test_list_node_state_multiple(self):
nodes = node_cache.get_node_list(state=['error', 'finished'])
self.assertEqual([self.uuid3, self.uuid, self.uuid2],
[node.uuid for node in nodes])
class TestNodeInfoVersionId(test_base.NodeStateTest):
def test_get(self):

View File

@ -0,0 +1,8 @@
---
features:
- |
Adds support for filter by state in the list introspection API.
See `story 1625183
<https://storyboard.openstack.org/#!/story/1625183>`_.
* ``GET /v1/introspection?state=starting,...``