From 3b15527580a4fa268738f8c6a5034df6f07112c7 Mon Sep 17 00:00:00 2001 From: dparalen Date: Fri, 14 Oct 2016 16:44:53 +0200 Subject: [PATCH] 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 --- doc/source/http-api.rst | 4 ++ ironic_inspector/main.py | 22 ++++++- ironic_inspector/test/functional.py | 47 ++++++++++++--- ironic_inspector/test/unit/test_main.py | 57 +++++++++++++++---- ironic_inspector/test/unit/test_utils.py | 9 +++ ironic_inspector/utils.py | 15 +++++ ...at-in-the-status-API-7860312102923938.yaml | 10 ++++ requirements.txt | 1 + 8 files changed, 143 insertions(+), 22 deletions(-) create mode 100644 releasenotes/notes/UUID-started_at-finished_at-in-the-status-API-7860312102923938.yaml diff --git a/doc/source/http-api.rst b/doc/source/http-api.rst index 095f52ef0..b5c386478 100644 --- a/doc/source/http-api.rst +++ b/doc/source/http-api.rst @@ -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. diff --git a/ironic_inspector/main.py b/ironic_inspector/main.py index 130c810f3..0ff3f6e93 100644 --- a/ironic_inspector/main.py +++ b/ironic_inspector/main.py @@ -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//abort', methods=['POST']) diff --git a/ironic_inspector/test/functional.py b/ironic_inspector/test/functional.py index fe5dd5bf5..da2a7e24f 100644 --- a/ironic_inspector/test/functional.py +++ b/ironic_inspector/test/functional.py @@ -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) diff --git a/ironic_inspector/test/unit/test_main.py b/ironic_inspector/test/unit/test_main.py index 111de374b..e496fa45a 100644 --- a/ironic_inspector/test/unit/test_main.py +++ b/ironic_inspector/test/unit/test_main.py @@ -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'))) diff --git a/ironic_inspector/test/unit/test_utils.py b/ironic_inspector/test/unit/test_utils.py index b4b7aec75..7270dc871 100644 --- a/ironic_inspector/test/unit/test_utils.py +++ b/ironic_inspector/test/unit/test_utils.py @@ -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)) diff --git a/ironic_inspector/utils.py b/ironic_inspector/utils.py index ebdba7644..2dad8d681 100644 --- a/ironic_inspector/utils.py +++ b/ironic_inspector/utils.py @@ -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() diff --git a/releasenotes/notes/UUID-started_at-finished_at-in-the-status-API-7860312102923938.yaml b/releasenotes/notes/UUID-started_at-finished_at-in-the-status-API-7860312102923938.yaml new file mode 100644 index 000000000..2e55919a6 --- /dev/null +++ b/releasenotes/notes/UUID-started_at-finished_at-in-the-status-API-7860312102923938.yaml @@ -0,0 +1,10 @@ +--- +features: + - | + enhance the introspection status returned from + ``GET@/v1/introspection/`` to contain the ``uuid``, ``started_at`` + and ``finished_at`` fields + +upgrade: + - | + new dependencies: pytz diff --git a/requirements.txt b/requirements.txt index b43d3e7ec..af4d88c41 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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