UUID, started_at, finished_at in the status API
Enhance the introspection status with the fields: * uuid * started_at * finished_at Change-Id: I36caa7d954a9bfb029d3f849fdf5e73f06f3da74 Partial-Bug: #1525238
This commit is contained in:
parent
0334c7e390
commit
3b15527580
@ -53,6 +53,9 @@ Response body: JSON dictionary with keys:
|
|||||||
(``true`` on introspection completion or if it ends because of an error)
|
(``true`` on introspection completion or if it ends because of an error)
|
||||||
* ``error`` error string or ``null``; ``Canceled by operator`` in
|
* ``error`` error string or ``null``; ``Canceled by operator`` in
|
||||||
case introspection was aborted
|
case introspection was aborted
|
||||||
|
* ``uuid`` node UUID
|
||||||
|
* ``started_at`` a UTC ISO8601 timestamp
|
||||||
|
* ``finished_at`` a UTC ISO8601 timestamp or ``null``
|
||||||
|
|
||||||
|
|
||||||
Abort Running Introspection
|
Abort Running Introspection
|
||||||
@ -334,3 +337,4 @@ Version History
|
|||||||
* **1.4** endpoint for reapplying the introspection over stored data.
|
* **1.4** endpoint for reapplying the introspection over stored data.
|
||||||
* **1.5** support for Ironic node names.
|
* **1.5** support for Ironic node names.
|
||||||
* **1.6** endpoint for rules creating returns 201 instead of 200 on success.
|
* **1.6** endpoint for rules creating returns 201 instead of 200 on success.
|
||||||
|
* **1.7** UUID, started_at, finished_at in the introspection status API.
|
||||||
|
@ -47,7 +47,7 @@ app = flask.Flask(__name__)
|
|||||||
LOG = utils.getProcessingLogger(__name__)
|
LOG = utils.getProcessingLogger(__name__)
|
||||||
|
|
||||||
MINIMUM_API_VERSION = (1, 0)
|
MINIMUM_API_VERSION = (1, 0)
|
||||||
CURRENT_API_VERSION = (1, 6)
|
CURRENT_API_VERSION = (1, 7)
|
||||||
_LOGGING_EXCLUDED_KEYS = ('logs',)
|
_LOGGING_EXCLUDED_KEYS = ('logs',)
|
||||||
|
|
||||||
|
|
||||||
@ -133,6 +133,23 @@ def generate_resource_data(resources):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def generate_introspection_status(node):
|
||||||
|
"""Return a dict representing current node status.
|
||||||
|
|
||||||
|
:param node: a NodeInfo instance
|
||||||
|
:return: dictionary
|
||||||
|
"""
|
||||||
|
status = {}
|
||||||
|
status['uuid'] = node.uuid
|
||||||
|
status['finished'] = bool(node.finished_at)
|
||||||
|
status['started_at'] = utils.iso_timestamp(node.started_at)
|
||||||
|
status['finished_at'] = utils.iso_timestamp(node.finished_at)
|
||||||
|
status['error'] = node.error
|
||||||
|
status['links'] = create_link_object(
|
||||||
|
["v%s/introspection/%s" % (CURRENT_API_VERSION[0], node.uuid)])
|
||||||
|
return status
|
||||||
|
|
||||||
|
|
||||||
@app.route('/', methods=['GET'])
|
@app.route('/', methods=['GET'])
|
||||||
@convert_exceptions
|
@convert_exceptions
|
||||||
def api_root():
|
def api_root():
|
||||||
@ -206,8 +223,7 @@ def api_introspection(node_id):
|
|||||||
return '', 202
|
return '', 202
|
||||||
else:
|
else:
|
||||||
node_info = node_cache.get_node(node_id)
|
node_info = node_cache.get_node(node_id)
|
||||||
return flask.json.jsonify(finished=bool(node_info.finished_at),
|
return flask.json.jsonify(generate_introspection_status(node_info))
|
||||||
error=node_info.error or None)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/v1/introspection/<node_id>/abort', methods=['POST'])
|
@app.route('/v1/introspection/<node_id>/abort', methods=['POST'])
|
||||||
|
@ -14,10 +14,14 @@
|
|||||||
import eventlet
|
import eventlet
|
||||||
eventlet.monkey_patch()
|
eventlet.monkey_patch()
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import time
|
||||||
|
|
||||||
import contextlib
|
import contextlib
|
||||||
import copy
|
import copy
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import pytz
|
||||||
import shutil
|
import shutil
|
||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
@ -25,6 +29,7 @@ import unittest
|
|||||||
import mock
|
import mock
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
from oslo_config import fixture as config_fixture
|
from oslo_config import fixture as config_fixture
|
||||||
|
from oslo_utils import timeutils
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from ironic_inspector.common import ironic as ir_utils
|
from ironic_inspector.common import ironic as ir_utils
|
||||||
@ -165,6 +170,32 @@ class Base(base.NodeTest):
|
|||||||
|
|
||||||
|
|
||||||
class Test(Base):
|
class Test(Base):
|
||||||
|
def mock_status(self, finished=mock.ANY, error=mock.ANY,
|
||||||
|
started_at=mock.ANY, finished_at=mock.ANY, links=mock.ANY):
|
||||||
|
return {'uuid': self.uuid, 'finished': finished, 'error': error,
|
||||||
|
'finished_at': finished_at, 'started_at': started_at,
|
||||||
|
'links': [{u'href': u'%s/v1/introspection/%s' % (self.ROOT_URL,
|
||||||
|
self.uuid),
|
||||||
|
u'rel': u'self'}]}
|
||||||
|
|
||||||
|
def assertStatus(self, status, finished, error=None):
|
||||||
|
self.assertEqual(
|
||||||
|
self.mock_status(finished=finished,
|
||||||
|
finished_at=finished and mock.ANY or None,
|
||||||
|
error=error),
|
||||||
|
status
|
||||||
|
)
|
||||||
|
curr_time = datetime.datetime.fromtimestamp(
|
||||||
|
time.time(), tz=pytz.timezone(time.tzname[0]))
|
||||||
|
started_at = timeutils.parse_isotime(status['started_at'])
|
||||||
|
self.assertLess(started_at, curr_time)
|
||||||
|
if finished:
|
||||||
|
finished_at = timeutils.parse_isotime(status['finished_at'])
|
||||||
|
self.assertLess(started_at, finished_at)
|
||||||
|
self.assertLess(finished_at, curr_time)
|
||||||
|
else:
|
||||||
|
self.assertIsNone(status['finished_at'])
|
||||||
|
|
||||||
def test_bmc(self):
|
def test_bmc(self):
|
||||||
self.call_introspect(self.uuid)
|
self.call_introspect(self.uuid)
|
||||||
eventlet.greenthread.sleep(DEFAULT_SLEEP)
|
eventlet.greenthread.sleep(DEFAULT_SLEEP)
|
||||||
@ -172,7 +203,7 @@ class Test(Base):
|
|||||||
'reboot')
|
'reboot')
|
||||||
|
|
||||||
status = self.call_get_status(self.uuid)
|
status = self.call_get_status(self.uuid)
|
||||||
self.assertEqual({'finished': False, 'error': None}, status)
|
self.assertStatus(status, finished=False)
|
||||||
|
|
||||||
res = self.call_continue(self.data)
|
res = self.call_continue(self.data)
|
||||||
self.assertEqual({'uuid': self.uuid}, res)
|
self.assertEqual({'uuid': self.uuid}, res)
|
||||||
@ -184,7 +215,7 @@ class Test(Base):
|
|||||||
node_uuid=self.uuid, address='11:22:33:44:55:66')
|
node_uuid=self.uuid, address='11:22:33:44:55:66')
|
||||||
|
|
||||||
status = self.call_get_status(self.uuid)
|
status = self.call_get_status(self.uuid)
|
||||||
self.assertEqual({'finished': True, 'error': None}, status)
|
self.assertStatus(status, finished=True)
|
||||||
|
|
||||||
def test_setup_ipmi(self):
|
def test_setup_ipmi(self):
|
||||||
patch_credentials = [
|
patch_credentials = [
|
||||||
@ -200,7 +231,7 @@ class Test(Base):
|
|||||||
self.assertFalse(self.cli.node.set_power_state.called)
|
self.assertFalse(self.cli.node.set_power_state.called)
|
||||||
|
|
||||||
status = self.call_get_status(self.uuid)
|
status = self.call_get_status(self.uuid)
|
||||||
self.assertEqual({'finished': False, 'error': None}, status)
|
self.assertStatus(status, finished=False)
|
||||||
|
|
||||||
res = self.call_continue(self.data)
|
res = self.call_continue(self.data)
|
||||||
self.assertEqual('admin', res['ipmi_username'])
|
self.assertEqual('admin', res['ipmi_username'])
|
||||||
@ -214,7 +245,7 @@ class Test(Base):
|
|||||||
node_uuid=self.uuid, address='11:22:33:44:55:66')
|
node_uuid=self.uuid, address='11:22:33:44:55:66')
|
||||||
|
|
||||||
status = self.call_get_status(self.uuid)
|
status = self.call_get_status(self.uuid)
|
||||||
self.assertEqual({'finished': True, 'error': None}, status)
|
self.assertStatus(status, finished=True)
|
||||||
|
|
||||||
def test_rules_api(self):
|
def test_rules_api(self):
|
||||||
res = self.call_list_rules()
|
res = self.call_list_rules()
|
||||||
@ -356,7 +387,7 @@ class Test(Base):
|
|||||||
'reboot')
|
'reboot')
|
||||||
|
|
||||||
status = self.call_get_status(self.uuid)
|
status = self.call_get_status(self.uuid)
|
||||||
self.assertEqual({'finished': False, 'error': None}, status)
|
self.assertStatus(status, finished=False)
|
||||||
|
|
||||||
res = self.call_continue(self.data)
|
res = self.call_continue(self.data)
|
||||||
self.assertEqual({'uuid': self.uuid}, res)
|
self.assertEqual({'uuid': self.uuid}, res)
|
||||||
@ -367,7 +398,7 @@ class Test(Base):
|
|||||||
node_uuid=self.uuid, address='11:22:33:44:55:66')
|
node_uuid=self.uuid, address='11:22:33:44:55:66')
|
||||||
|
|
||||||
status = self.call_get_status(self.uuid)
|
status = self.call_get_status(self.uuid)
|
||||||
self.assertEqual({'finished': True, 'error': None}, status)
|
self.assertStatus(status, finished=True)
|
||||||
|
|
||||||
def test_abort_introspection(self):
|
def test_abort_introspection(self):
|
||||||
self.call_introspect(self.uuid)
|
self.call_introspect(self.uuid)
|
||||||
@ -375,7 +406,7 @@ class Test(Base):
|
|||||||
self.cli.node.set_power_state.assert_called_once_with(self.uuid,
|
self.cli.node.set_power_state.assert_called_once_with(self.uuid,
|
||||||
'reboot')
|
'reboot')
|
||||||
status = self.call_get_status(self.uuid)
|
status = self.call_get_status(self.uuid)
|
||||||
self.assertEqual({'finished': False, 'error': None}, status)
|
self.assertStatus(status, finished=False)
|
||||||
|
|
||||||
res = self.call_abort_introspect(self.uuid)
|
res = self.call_abort_introspect(self.uuid)
|
||||||
eventlet.greenthread.sleep(DEFAULT_SLEEP)
|
eventlet.greenthread.sleep(DEFAULT_SLEEP)
|
||||||
@ -413,7 +444,7 @@ class Test(Base):
|
|||||||
eventlet.greenthread.sleep(DEFAULT_SLEEP)
|
eventlet.greenthread.sleep(DEFAULT_SLEEP)
|
||||||
|
|
||||||
status = self.call_get_status(self.uuid)
|
status = self.call_get_status(self.uuid)
|
||||||
self.assertEqual({'finished': True, 'error': None}, status)
|
self.assertStatus(status, finished=True)
|
||||||
|
|
||||||
res = self.call_reapply(self.uuid)
|
res = self.call_reapply(self.uuid)
|
||||||
self.assertEqual(202, res.status_code)
|
self.assertEqual(202, res.status_code)
|
||||||
|
@ -175,25 +175,60 @@ class TestApiAbort(BaseAPITest):
|
|||||||
self.assertEqual(str(exc), data['error']['message'])
|
self.assertEqual(str(exc), data['error']['message'])
|
||||||
|
|
||||||
|
|
||||||
class TestApiGetStatus(BaseAPITest):
|
class GetStatusAPIBaseTest(BaseAPITest):
|
||||||
@mock.patch.object(node_cache, 'get_node', autospec=True)
|
def setUp(self):
|
||||||
|
super(GetStatusAPIBaseTest, self).setUp()
|
||||||
|
self.uuid2 = uuidutils.generate_uuid()
|
||||||
|
self.finished_node = node_cache.NodeInfo(uuid=self.uuid,
|
||||||
|
started_at=42.0,
|
||||||
|
finished_at=100.1,
|
||||||
|
error='boom')
|
||||||
|
self.finished_node.links = [
|
||||||
|
{u'href': u'http://localhost/v1/introspection/%s' %
|
||||||
|
self.finished_node.uuid,
|
||||||
|
u'rel': u'self'},
|
||||||
|
]
|
||||||
|
self.finished_node.status = {
|
||||||
|
'finished': True,
|
||||||
|
'started_at': utils.iso_timestamp(self.finished_node.started_at),
|
||||||
|
'finished_at': utils.iso_timestamp(self.finished_node.finished_at),
|
||||||
|
'error': self.finished_node.error,
|
||||||
|
'uuid': self.finished_node.uuid,
|
||||||
|
'links': self.finished_node.links
|
||||||
|
}
|
||||||
|
|
||||||
|
self.unfinished_node = node_cache.NodeInfo(uuid=self.uuid2,
|
||||||
|
started_at=42.0)
|
||||||
|
self.unfinished_node.links = [
|
||||||
|
{u'href': u'http://localhost/v1/introspection/%s' %
|
||||||
|
self.unfinished_node.uuid,
|
||||||
|
u'rel': u'self'}
|
||||||
|
]
|
||||||
|
self.unfinished_node.status = {
|
||||||
|
'finished': False,
|
||||||
|
'started_at': utils.iso_timestamp(self.unfinished_node.started_at),
|
||||||
|
'finished_at': utils.iso_timestamp(
|
||||||
|
self.unfinished_node.finished_at),
|
||||||
|
'error': None,
|
||||||
|
'uuid': self.unfinished_node.uuid,
|
||||||
|
'links': self.unfinished_node.links
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch.object(node_cache, 'get_node', autospec=True)
|
||||||
|
class TestApiGetStatus(GetStatusAPIBaseTest):
|
||||||
def test_get_introspection_in_progress(self, get_mock):
|
def test_get_introspection_in_progress(self, get_mock):
|
||||||
get_mock.return_value = node_cache.NodeInfo(uuid=self.uuid,
|
get_mock.return_value = self.unfinished_node
|
||||||
started_at=42.0)
|
|
||||||
res = self.app.get('/v1/introspection/%s' % self.uuid)
|
res = self.app.get('/v1/introspection/%s' % self.uuid)
|
||||||
self.assertEqual(200, res.status_code)
|
self.assertEqual(200, res.status_code)
|
||||||
self.assertEqual({'finished': False, 'error': None},
|
self.assertEqual(self.unfinished_node.status,
|
||||||
json.loads(res.data.decode('utf-8')))
|
json.loads(res.data.decode('utf-8')))
|
||||||
|
|
||||||
@mock.patch.object(node_cache, 'get_node', autospec=True)
|
|
||||||
def test_get_introspection_finished(self, get_mock):
|
def test_get_introspection_finished(self, get_mock):
|
||||||
get_mock.return_value = node_cache.NodeInfo(uuid=self.uuid,
|
get_mock.return_value = self.finished_node
|
||||||
started_at=42.0,
|
|
||||||
finished_at=100.1,
|
|
||||||
error='boom')
|
|
||||||
res = self.app.get('/v1/introspection/%s' % self.uuid)
|
res = self.app.get('/v1/introspection/%s' % self.uuid)
|
||||||
self.assertEqual(200, res.status_code)
|
self.assertEqual(200, res.status_code)
|
||||||
self.assertEqual({'finished': True, 'error': 'boom'},
|
self.assertEqual(self.finished_node.status,
|
||||||
json.loads(res.data.decode('utf-8')))
|
json.loads(res.data.decode('utf-8')))
|
||||||
|
|
||||||
|
|
||||||
|
@ -170,3 +170,12 @@ class TestProcessingLogger(base.BaseTest):
|
|||||||
logger = utils.getProcessingLogger(__name__)
|
logger = utils.getProcessingLogger(__name__)
|
||||||
msg, _kwargs = logger.process('foo', {})
|
msg, _kwargs = logger.process('foo', {})
|
||||||
self.assertEqual('foo', msg)
|
self.assertEqual('foo', msg)
|
||||||
|
|
||||||
|
|
||||||
|
class TestIsoTimestamp(base.BaseTest):
|
||||||
|
def test_ok(self):
|
||||||
|
iso_date = '1970-01-01T00:00:00+00:00'
|
||||||
|
self.assertEqual(iso_date, utils.iso_timestamp(0.0))
|
||||||
|
|
||||||
|
def test_none(self):
|
||||||
|
self.assertIsNone(utils.iso_timestamp(None))
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
import datetime
|
||||||
import logging as pylog
|
import logging as pylog
|
||||||
import re
|
import re
|
||||||
|
|
||||||
@ -19,6 +20,7 @@ from keystonemiddleware import auth_token
|
|||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
from oslo_log import log
|
from oslo_log import log
|
||||||
from oslo_middleware import cors as cors_middleware
|
from oslo_middleware import cors as cors_middleware
|
||||||
|
import pytz
|
||||||
import six
|
import six
|
||||||
|
|
||||||
from ironic_inspector.common.i18n import _, _LE
|
from ironic_inspector.common.i18n import _, _LE
|
||||||
@ -224,3 +226,16 @@ def get_inventory(data, node_info=None):
|
|||||||
'or empty') % key, data=data, node_info=node_info)
|
'or empty') % key, data=data, node_info=node_info)
|
||||||
|
|
||||||
return inventory
|
return inventory
|
||||||
|
|
||||||
|
|
||||||
|
def iso_timestamp(timestamp=None, tz=pytz.timezone('utc')):
|
||||||
|
"""Return an ISO8601-formatted timestamp (tz: UTC) or None.
|
||||||
|
|
||||||
|
:param timestamp: such as time.time() or None
|
||||||
|
:param tz: timezone
|
||||||
|
:returns: an ISO8601-formatted timestamp, or None
|
||||||
|
"""
|
||||||
|
if timestamp is None:
|
||||||
|
return None
|
||||||
|
date = datetime.datetime.fromtimestamp(timestamp, tz=tz)
|
||||||
|
return date.isoformat()
|
||||||
|
@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
enhance the introspection status returned from
|
||||||
|
``GET@/v1/introspection/<Node Id>`` to contain the ``uuid``, ``started_at``
|
||||||
|
and ``finished_at`` fields
|
||||||
|
|
||||||
|
upgrade:
|
||||||
|
- |
|
||||||
|
new dependencies: pytz
|
@ -14,6 +14,7 @@ netaddr!=0.7.16,>=0.7.13 # BSD
|
|||||||
pbr>=1.6 # Apache-2.0
|
pbr>=1.6 # Apache-2.0
|
||||||
python-ironicclient>=1.6.0 # Apache-2.0
|
python-ironicclient>=1.6.0 # Apache-2.0
|
||||||
python-swiftclient>=2.2.0 # Apache-2.0
|
python-swiftclient>=2.2.0 # Apache-2.0
|
||||||
|
pytz>=2013.6 # MIT
|
||||||
oslo.concurrency>=3.8.0 # Apache-2.0
|
oslo.concurrency>=3.8.0 # Apache-2.0
|
||||||
oslo.config>=3.14.0 # Apache-2.0
|
oslo.config>=3.14.0 # Apache-2.0
|
||||||
oslo.db!=4.13.1,!=4.13.2,>=4.10.0 # Apache-2.0
|
oslo.db!=4.13.1,!=4.13.2,>=4.10.0 # Apache-2.0
|
||||||
|
Loading…
Reference in New Issue
Block a user