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)
* ``error`` error string or ``null``; ``Canceled by operator`` in
case introspection was aborted
* ``uuid`` node UUID
* ``started_at`` a UTC ISO8601 timestamp
* ``finished_at`` a UTC ISO8601 timestamp or ``null``
Abort Running Introspection
@ -334,3 +337,4 @@ Version History
* **1.4** endpoint for reapplying the introspection over stored data.
* **1.5** support for Ironic node names.
* **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__)
MINIMUM_API_VERSION = (1, 0)
CURRENT_API_VERSION = (1, 6)
CURRENT_API_VERSION = (1, 7)
_LOGGING_EXCLUDED_KEYS = ('logs',)
@ -133,6 +133,23 @@ def generate_resource_data(resources):
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'])
@convert_exceptions
def api_root():
@ -206,8 +223,7 @@ def api_introspection(node_id):
return '', 202
else:
node_info = node_cache.get_node(node_id)
return flask.json.jsonify(finished=bool(node_info.finished_at),
error=node_info.error or None)
return flask.json.jsonify(generate_introspection_status(node_info))
@app.route('/v1/introspection/<node_id>/abort', methods=['POST'])

View File

@ -14,10 +14,14 @@
import eventlet
eventlet.monkey_patch()
import datetime
import time
import contextlib
import copy
import json
import os
import pytz
import shutil
import tempfile
import unittest
@ -25,6 +29,7 @@ import unittest
import mock
from oslo_config import cfg
from oslo_config import fixture as config_fixture
from oslo_utils import timeutils
import requests
from ironic_inspector.common import ironic as ir_utils
@ -165,6 +170,32 @@ class Base(base.NodeTest):
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):
self.call_introspect(self.uuid)
eventlet.greenthread.sleep(DEFAULT_SLEEP)
@ -172,7 +203,7 @@ class Test(Base):
'reboot')
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)
self.assertEqual({'uuid': self.uuid}, res)
@ -184,7 +215,7 @@ class Test(Base):
node_uuid=self.uuid, address='11:22:33:44:55:66')
status = self.call_get_status(self.uuid)
self.assertEqual({'finished': True, 'error': None}, status)
self.assertStatus(status, finished=True)
def test_setup_ipmi(self):
patch_credentials = [
@ -200,7 +231,7 @@ class Test(Base):
self.assertFalse(self.cli.node.set_power_state.called)
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)
self.assertEqual('admin', res['ipmi_username'])
@ -214,7 +245,7 @@ class Test(Base):
node_uuid=self.uuid, address='11:22:33:44:55:66')
status = self.call_get_status(self.uuid)
self.assertEqual({'finished': True, 'error': None}, status)
self.assertStatus(status, finished=True)
def test_rules_api(self):
res = self.call_list_rules()
@ -356,7 +387,7 @@ class Test(Base):
'reboot')
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)
self.assertEqual({'uuid': self.uuid}, res)
@ -367,7 +398,7 @@ class Test(Base):
node_uuid=self.uuid, address='11:22:33:44:55:66')
status = self.call_get_status(self.uuid)
self.assertEqual({'finished': True, 'error': None}, status)
self.assertStatus(status, finished=True)
def test_abort_introspection(self):
self.call_introspect(self.uuid)
@ -375,7 +406,7 @@ class Test(Base):
self.cli.node.set_power_state.assert_called_once_with(self.uuid,
'reboot')
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)
eventlet.greenthread.sleep(DEFAULT_SLEEP)
@ -413,7 +444,7 @@ class Test(Base):
eventlet.greenthread.sleep(DEFAULT_SLEEP)
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)
self.assertEqual(202, res.status_code)

View File

@ -175,25 +175,60 @@ class TestApiAbort(BaseAPITest):
self.assertEqual(str(exc), data['error']['message'])
class TestApiGetStatus(BaseAPITest):
@mock.patch.object(node_cache, 'get_node', autospec=True)
def test_get_introspection_in_progress(self, get_mock):
get_mock.return_value = node_cache.NodeInfo(uuid=self.uuid,
started_at=42.0)
res = self.app.get('/v1/introspection/%s' % self.uuid)
self.assertEqual(200, res.status_code)
self.assertEqual({'finished': False, 'error': None},
json.loads(res.data.decode('utf-8')))
@mock.patch.object(node_cache, 'get_node', autospec=True)
def test_get_introspection_finished(self, get_mock):
get_mock.return_value = node_cache.NodeInfo(uuid=self.uuid,
class GetStatusAPIBaseTest(BaseAPITest):
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):
get_mock.return_value = self.unfinished_node
res = self.app.get('/v1/introspection/%s' % self.uuid)
self.assertEqual(200, res.status_code)
self.assertEqual({'finished': True, 'error': 'boom'},
self.assertEqual(self.unfinished_node.status,
json.loads(res.data.decode('utf-8')))
def test_get_introspection_finished(self, get_mock):
get_mock.return_value = self.finished_node
res = self.app.get('/v1/introspection/%s' % self.uuid)
self.assertEqual(200, res.status_code)
self.assertEqual(self.finished_node.status,
json.loads(res.data.decode('utf-8')))

View File

@ -170,3 +170,12 @@ class TestProcessingLogger(base.BaseTest):
logger = utils.getProcessingLogger(__name__)
msg, _kwargs = logger.process('foo', {})
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
# limitations under the License.
import datetime
import logging as pylog
import re
@ -19,6 +20,7 @@ from keystonemiddleware import auth_token
from oslo_config import cfg
from oslo_log import log
from oslo_middleware import cors as cors_middleware
import pytz
import six
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)
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
python-ironicclient>=1.6.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.config>=3.14.0 # Apache-2.0
oslo.db!=4.13.1,!=4.13.2,>=4.10.0 # Apache-2.0