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:
dparalen 2016-10-14 16:44:53 +02:00
parent 0334c7e390
commit 3b15527580
8 changed files with 143 additions and 22 deletions

View File

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

View File

@ -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'])

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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