From 2475bf1ba27683b3ce5c679b5ea3517921b57926 Mon Sep 17 00:00:00 2001 From: openstack Date: Mon, 4 Mar 2019 07:11:50 +0000 Subject: [PATCH] Display progress_details of the notification Added 1.1 microversion support which will display `progress_details` of the notification. Note: Referred APIVersion class from python-novaclient [1]. [1]: https://github.com/openstack/python-novaclient/blob/master/novaclient/api_versions.py#L42 Depends-On: I93c1b7d88823e02d9a02855cabb8b22c9e40a7d5 Implements: bp progress-details-recovery-workflows Change-Id: I9ba787bc8ef9528a7cff5b4c1411dffa454b66d2 --- masakariclient/api_versions.py | 150 ++++++++++++++++++ masakariclient/common/exception.py | 5 + masakariclient/osc/v1/notification.py | 10 +- masakariclient/plugin.py | 5 +- .../tests/unit/osc/v1/test_notification.py | 105 ++++++++++++ ...s-recovery-workflows-06614c76d44e64ff.yaml | 7 + 6 files changed, 279 insertions(+), 3 deletions(-) create mode 100644 masakariclient/api_versions.py create mode 100644 masakariclient/tests/unit/osc/v1/test_notification.py create mode 100644 releasenotes/notes/add-version-support-for-progress-details-recovery-workflows-06614c76d44e64ff.yaml diff --git a/masakariclient/api_versions.py b/masakariclient/api_versions.py new file mode 100644 index 0000000..ae10002 --- /dev/null +++ b/masakariclient/api_versions.py @@ -0,0 +1,150 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import re + +from masakariclient.common import exception +from masakariclient.common.i18n import _ + + +_type_error_msg = _("'%(other)s' should be an instance of '%(cls)s'") + + +class APIVersion(object): + """This class represents an API Version Request. + + This class provides convenience methods for manipulation + and comparison of version numbers that we need to do to + implement microversions. + """ + + def __init__(self, version_str=None): + """Create an API version object. + + :param version_str: String representation of APIVersionRequest. + Correct format is 'X.Y', where 'X' and 'Y' + are int values. None value should be used + to create Null APIVersionRequest, which is + equal to 0.0 + """ + self.ver_major = 0 + self.ver_minor = 0 + + if version_str is not None: + match = re.match(r"^([1-9]\d*)\.([1-9]\d*|0|latest)$", version_str) + if match: + self.ver_major = int(match.group(1)) + if match.group(2) == "latest": + # Infinity allows to easily determine latest version and + # doesn't require any additional checks in comparison + # methods. + self.ver_minor = float("inf") + else: + self.ver_minor = int(match.group(2)) + else: + msg = _("Invalid format of client version '%s'. " + "Expected format 'X.Y', where X is a major part and Y " + "is a minor part of version.") % version_str + raise exception.UnsupportedVersion(msg) + + def __str__(self): + """Debug/Logging representation of object.""" + if self.is_latest(): + return "Latest API Version Major: %s" % self.ver_major + return ("API Version Major: %s, Minor: %s" + % (self.ver_major, self.ver_minor)) + + def __repr__(self): + if self.is_null(): + return "" + else: + return "" % self.get_string() + + def is_null(self): + return self.ver_major == 0 and self.ver_minor == 0 + + def is_latest(self): + return self.ver_minor == float("inf") + + def __lt__(self, other): + if not isinstance(other, APIVersion): + raise TypeError(_type_error_msg % {"other": other, + "cls": self.__class__}) + + return ((self.ver_major, self.ver_minor) < + (other.ver_major, other.ver_minor)) + + def __eq__(self, other): + if not isinstance(other, APIVersion): + raise TypeError(_type_error_msg % {"other": other, + "cls": self.__class__}) + + return ((self.ver_major, self.ver_minor) == + (other.ver_major, other.ver_minor)) + + def __gt__(self, other): + if not isinstance(other, APIVersion): + raise TypeError(_type_error_msg % {"other": other, + "cls": self.__class__}) + + return ((self.ver_major, self.ver_minor) > + (other.ver_major, other.ver_minor)) + + def __le__(self, other): + return self < other or self == other + + def __ne__(self, other): + return not self.__eq__(other) + + def __ge__(self, other): + return self > other or self == other + + def matches(self, min_version, max_version): + """Matches the version object. + + Returns whether the version object represents a version + greater than or equal to the minimum version and less than + or equal to the maximum version. + + :param min_version: Minimum acceptable version. + :param max_version: Maximum acceptable version. + :returns: boolean + + If min_version is null then there is no minimum limit. + If max_version is null then there is no maximum limit. + If self is null then raise ValueError + """ + + if self.is_null(): + raise ValueError(_("Null APIVersion doesn't support 'matches'.")) + if max_version.is_null() and min_version.is_null(): + return True + elif max_version.is_null(): + return min_version <= self + elif min_version.is_null(): + return self <= max_version + else: + return min_version <= self <= max_version + + def get_string(self): + """Version string representation. + + Converts object to string representation which if used to create + an APIVersion object results in the same version. + """ + if self.is_null(): + raise ValueError( + _("Null APIVersion cannot be converted to string.")) + elif self.is_latest(): + return "%s.%s" % (self.ver_major, "latest") + return "%s.%s" % (self.ver_major, self.ver_minor) diff --git a/masakariclient/common/exception.py b/masakariclient/common/exception.py index 317bd76..b47b2e7 100644 --- a/masakariclient/common/exception.py +++ b/masakariclient/common/exception.py @@ -24,3 +24,8 @@ class BaseException(Exception): class CommandError(BaseException): """Invalid usage of CLI.""" + + +class UnsupportedVersion(Exception): + """User is trying to use an unsupported version of the API.""" + pass diff --git a/masakariclient/osc/v1/notification.py b/masakariclient/osc/v1/notification.py index 32c0e47..4f01bc6 100644 --- a/masakariclient/osc/v1/notification.py +++ b/masakariclient/osc/v1/notification.py @@ -18,6 +18,7 @@ from osc_lib import exceptions from osc_lib import utils from oslo_serialization import jsonutils +from masakariclient import api_versions from masakariclient.common.i18n import _ import masakariclient.common.utils as masakariclient_utils @@ -151,7 +152,14 @@ def _show_notification(masakari_client, notification_uuid): 'status', 'source_host_uuid', 'generated_time', - 'payload', + 'payload' ] + + if masakari_client.default_microversion: + api_version = api_versions.APIVersion( + masakari_client.default_microversion) + if api_version >= api_versions.APIVersion("1.1"): + columns.append('recovery_workflow_details') + return columns, utils.get_dict_properties(notification.to_dict(), columns, formatters=formatters) diff --git a/masakariclient/plugin.py b/masakariclient/plugin.py index f64c13b..2e1d509 100644 --- a/masakariclient/plugin.py +++ b/masakariclient/plugin.py @@ -18,13 +18,14 @@ from osc_lib import utils LOG = logging.getLogger(__name__) -DEFAULT_HA_API_VERSION = '1' +DEFAULT_HA_API_VERSION = '1.1' API_VERSION_OPTION = 'os_ha_api_version' API_NAME = 'ha' SUPPORTED_VERSIONS = [ '1', - '1.0' + '1.0', + '1.1' ] API_VERSIONS = {v: 'masakariclient.v1.client.Client' diff --git a/masakariclient/tests/unit/osc/v1/test_notification.py b/masakariclient/tests/unit/osc/v1/test_notification.py new file mode 100644 index 0000000..8a699c2 --- /dev/null +++ b/masakariclient/tests/unit/osc/v1/test_notification.py @@ -0,0 +1,105 @@ +# Copyright(c) 2019 NTT DATA +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +test_masakariclient +---------------------------------- + +Tests for `masakariclient` module. +""" +import mock +import uuid + +from osc_lib.tests import utils as osc_lib_utils +from osc_lib import utils + +from masakariclient.osc.v1.notification import ShowNotification +from masakariclient.tests import base + +NOTIFICATION_NAME = 'notification_name' +NOTIFICATION_ID = uuid.uuid4() +RECOVERY_WORKFLOW_DETAILS = [{ + "progress": 1.0, "state": "SUCCESS", + "name": "DisableComputeNodeTask", + "progress_details": [ + {"timestamp:": "2019-02-28 07:21:33.170190", + "progress": 0.5, + "message": "Disabling compute host: host"}, + {"timestamp:": "2019-02-28 07:21:33.291810", + "progress": 1.0, + "message": "Skipping recovery for process nova-compute " + "as it is already disabled"}]}] + + +class FakeNotification(object): + """Fake notification show detail.""" + def __init__(self,): + super(FakeNotification, self).__init__() + + def to_dict(self): + return { + 'created_at': '2019-02-18T05:47:46.000000', + 'updated_at': '2019-02-18T06:05:16.000000', + 'notification_uuid': NOTIFICATION_ID, + 'source_host_uuid': '9ab67dc7-110a-4a4c-af64-abc6e5798433', + 'name': NOTIFICATION_NAME, + 'id': 1, + 'type': 'VM', + 'payload': { + "instance_uuid": "99ffc832-2252-4a9e-9b98-28bc70f7ff09", + "vir_domain_event": "STOPPED_FAILED", "event": "LIFECYCLE"}, + 'status': 'finished', + 'recovery_workflow_details': RECOVERY_WORKFLOW_DETAILS, + 'generated_time': '2019-02-13T15:34:55.000000' + } + + +class BaseV1Notification(base.TestCase, osc_lib_utils.TestCommand): + def setUp(self): + super(BaseV1Notification, self).setUp() + self.app = mock.Mock() + self.app_args = mock.Mock() + self.client_manager = mock.Mock() + self.client_manager.default_microversion = '1.0' + self.app.client_manager.ha = self.client_manager + self.dummy_notification = FakeNotification() + self.show_notification = ShowNotification( + self.app, self.app_args, cmd_name='notification show') + self.columns = ['created_at', 'updated_at', 'notification_uuid', + 'type', 'status', 'source_host_uuid', + 'generated_time', 'payload'] + + +class TestShowNotificationV1(BaseV1Notification): + + def test_take_action_by_uuid(self): + arglist = ['8c35987c-f416-46ca-be37-52f58fd8d294'] + parsed_args = self.check_parser(self.show_notification, arglist, []) + self._test_take_action(parsed_args) + + @mock.patch.object(utils, 'get_dict_properties') + def _test_take_action(self, parsed_args, mock_get_dict_properties): + self.app.client_manager.ha.get_notification.return_value = ( + self.dummy_notification) + + self.show_notification.take_action(parsed_args) + mock_get_dict_properties.assert_called_once_with( + self.dummy_notification.to_dict(), self.columns, formatters={}) + + +class TestShowNotificationV1_1(TestShowNotificationV1): + + def setUp(self): + super(TestShowNotificationV1_1, self).setUp() + self.client_manager.default_microversion = '1.1' + self.columns.append('recovery_workflow_details') diff --git a/releasenotes/notes/add-version-support-for-progress-details-recovery-workflows-06614c76d44e64ff.yaml b/releasenotes/notes/add-version-support-for-progress-details-recovery-workflows-06614c76d44e64ff.yaml new file mode 100644 index 0000000..0795436 --- /dev/null +++ b/releasenotes/notes/add-version-support-for-progress-details-recovery-workflows-06614c76d44e64ff.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + The 1.1 microversion is now supported. This introduces the following changes: + + * User can get the `progress_details` of the notification in microversion 1.1. The default version + is set to 1.1 if not provided.