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)
|
||||
* ``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.
|
||||
|
@ -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'])
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
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 = node_cache.NodeInfo(uuid=self.uuid,
|
||||
started_at=42.0)
|
||||
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': False, 'error': None},
|
||||
self.assertEqual(self.unfinished_node.status,
|
||||
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,
|
||||
started_at=42.0,
|
||||
finished_at=100.1,
|
||||
error='boom')
|
||||
get_mock.return_value = self.finished_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.finished_node.status,
|
||||
json.loads(res.data.decode('utf-8')))
|
||||
|
||||
|
||||
|
@ -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))
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
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
|
||||
|
Loading…
Reference in New Issue
Block a user