From 78730d8a599ed1e0422b4bdc136c0dcc491f8bca Mon Sep 17 00:00:00 2001 From: Mihaela Balutoiu Date: Tue, 11 Aug 2026 21:25:36 +0300 Subject: [PATCH 1/2] Add SAP licence support to the licensing client The licensing server now issues standard ('v2') and SAP ('v2-sap') licences, and splits the appliance usage counters per edition under `standard_licence_stats` and `sap_licence_stats` instead of one flat set. Signed-off-by: Mihaela Balutoiu --- coriolisclient/constants.py | 40 +++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/coriolisclient/constants.py b/coriolisclient/constants.py index ca33db8..c8df4f5 100644 --- a/coriolisclient/constants.py +++ b/coriolisclient/constants.py @@ -13,6 +13,46 @@ # See the License for the specific language governing permissions and # limitations under the License. +LICENCE_VERSION_V1 = "v1" +LICENCE_VERSION_V2 = "v2" +LICENCE_VERSION_V2_SAP = "v2-sap" + +LICENCE_EDITION_STANDARD = "Standard" +LICENCE_EDITION_SAP = "SAP" + +LICENCE_TYPE_EDITION_MAP = { + LICENCE_VERSION_V1: LICENCE_EDITION_STANDARD, + LICENCE_VERSION_V2: LICENCE_EDITION_STANDARD, + LICENCE_VERSION_V2_SAP: LICENCE_EDITION_SAP, +} + +RESERVATION_TYPE_REPLICA = "replica" +RESERVATION_TYPE_MIGRATION = "migration" +RESERVATION_TYPE_SAP_REPLICA = "sap_replica" +RESERVATION_TYPE_SAP_MIGRATION = "sap_migration" + +RESERVATION_TYPE_EDITION_MAP = { + RESERVATION_TYPE_REPLICA: LICENCE_EDITION_STANDARD, + RESERVATION_TYPE_MIGRATION: LICENCE_EDITION_STANDARD, + RESERVATION_TYPE_SAP_REPLICA: LICENCE_EDITION_SAP, + RESERVATION_TYPE_SAP_MIGRATION: LICENCE_EDITION_SAP, +} + +LICENCE_STATS_KEY_STANDARD = "standard_licence_stats" +LICENCE_STATS_KEY_SAP = "sap_licence_stats" + +LICENCE_STATS_FIELDS = ( + "current_performed_migrations", + "current_performed_replicas", + "current_available_migrations", + "current_available_replicas", + "lifetime_performed_migrations", + "lifetime_performed_replicas", + "lifetime_available_migrations", + "lifetime_available_replicas", +) + + MIGRATION_STATUS_RUNNING = "RUNNING" MIGRATION_STATUS_COMPLETED = "COMPLETED" MIGRATION_STATUS_ERROR = "ERROR" From 35719c2df8e7b4ab4570cd76e1b5c48588592633 Mon Sep 17 00:00:00 2001 From: Mihaela Balutoiu Date: Tue, 11 Aug 2026 21:28:51 +0300 Subject: [PATCH 2/2] Display licence editions in the licensing CLI The appliance licensing status now carries a separate set of usage counters for each licence edition, and licences and reservations identify the edition they belong to. Signed-off-by: Mihaela Balutoiu --- coriolisclient/cli/licensing.py | 33 +++--- coriolisclient/cli/licensing_reservations.py | 3 + coriolisclient/tests/cli/test_licensing.py | 103 ++++++++++++------ .../tests/cli/test_licensing_reservations.py | 23 +++- 4 files changed, 112 insertions(+), 50 deletions(-) diff --git a/coriolisclient/cli/licensing.py b/coriolisclient/cli/licensing.py index 2266e93..fcf5478 100644 --- a/coriolisclient/cli/licensing.py +++ b/coriolisclient/cli/licensing.py @@ -20,6 +20,7 @@ from cliff import show from coriolisclient.cli import formatter +from coriolisclient import constants class LicensingStatusFormatter(formatter.EntityFormatter): @@ -29,29 +30,26 @@ def __init__(self): "appliance_id", "earliest_licence_expiry_time", "latest_licence_expiry_time", - "current_performed_migrations", - "current_performed_replicas", - "current_available_migrations", - "current_available_replicas", - "lifetime_performed_migrations", - "lifetime_performed_replicas", - "lifetime_available_migrations", - "lifetime_available_replicas", ] + self.columns.extend( + "standard_%s" % field + for field in constants.LICENCE_STATS_FIELDS) + self.columns.extend( + "sap_%s" % field for field in constants.LICENCE_STATS_FIELDS) def _get_formatted_data(self, obj): + standard_stats = getattr(obj, constants.LICENCE_STATS_KEY_STANDARD) + sap_stats = getattr(obj, constants.LICENCE_STATS_KEY_SAP) + data = [obj.appliance_id, obj.earliest_licence_expiry_time, obj.latest_licence_expiry_time, - obj.current_performed_migrations, - obj.current_performed_replicas, - obj.current_available_migrations, - obj.current_available_replicas, - obj.lifetime_performed_migrations, - obj.lifetime_performed_replicas, - obj.lifetime_available_migrations, - obj.lifetime_available_replicas, ] + data.extend( + standard_stats[field] + for field in constants.LICENCE_STATS_FIELDS) + data.extend( + sap_stats[field] for field in constants.LICENCE_STATS_FIELDS) return data @@ -65,6 +63,7 @@ class LicenceFormatter(formatter.EntityFormatter): "Period End", "Period Duration", "Licence Version", + "Licence Edition", ) def _get_sorted_list(self, obj_list): @@ -79,6 +78,8 @@ def _get_formatted_data(self, obj): obj.period_end, obj.period_duration, obj.licence_version, + constants.LICENCE_TYPE_EDITION_MAP.get( + obj.licence_version), ) return data diff --git a/coriolisclient/cli/licensing_reservations.py b/coriolisclient/cli/licensing_reservations.py index f90f14a..c43d848 100644 --- a/coriolisclient/cli/licensing_reservations.py +++ b/coriolisclient/cli/licensing_reservations.py @@ -17,6 +17,7 @@ from cliff import show from coriolisclient.cli import formatter +from coriolisclient import constants class ReservationFormatter(formatter.EntityFormatter): @@ -24,6 +25,7 @@ class ReservationFormatter(formatter.EntityFormatter): "Appliance ID", "Licence ID", "Type", + "Licence Edition", "Count", "Created At", ) @@ -36,6 +38,7 @@ def _get_formatted_data(self, obj): obj.appliance_id, obj.licence_id, obj.type, + constants.RESERVATION_TYPE_EDITION_MAP.get(obj.type), obj.count, obj.created_at, ) diff --git a/coriolisclient/tests/cli/test_licensing.py b/coriolisclient/tests/cli/test_licensing.py index 39c4dab..f94a2f9 100644 --- a/coriolisclient/tests/cli/test_licensing.py +++ b/coriolisclient/tests/cli/test_licensing.py @@ -8,6 +8,7 @@ from cliff import show from coriolisclient.cli import licensing +from coriolisclient import constants from coriolisclient.tests import test_base @@ -18,29 +19,42 @@ def setUp(self): super(LicensingStatusFormatterTestCase, self).setUp() self.licence = licensing.LicensingStatusFormatter() - def test_get_formatted_data(self): + @staticmethod + def _make_stats(offset): + """Builds a stats body whose counters are all distinguishable.""" + return { + field: offset + i + for i, field in enumerate(constants.LICENCE_STATS_FIELDS)} + + def _make_status(self, standard_stats, sap_stats): obj = mock.Mock() obj.appliance_id = mock.sentinel.appliance_id obj.earliest_licence_expiry_time = \ mock.sentinel.earliest_licence_expiry_time obj.latest_licence_expiry_time = \ mock.sentinel.latest_licence_expiry_time - obj.current_performed_migrations = \ - mock.sentinel.current_performed_migrations - obj.current_performed_replicas = \ - mock.sentinel.current_performed_replicas - obj.current_available_migrations = \ - mock.sentinel.current_available_migrations - obj.current_available_replicas = \ - mock.sentinel.current_available_replicas - obj.lifetime_performed_migrations = \ - mock.sentinel.lifetime_performed_migrations - obj.lifetime_performed_replicas = \ - mock.sentinel.lifetime_performed_replicas - obj.lifetime_available_migrations = \ - mock.sentinel.lifetime_available_migrations - obj.lifetime_available_replicas = \ - mock.sentinel.lifetime_available_replicas + setattr(obj, constants.LICENCE_STATS_KEY_STANDARD, standard_stats) + setattr(obj, constants.LICENCE_STATS_KEY_SAP, sap_stats) + return obj + + def test_columns(self): + self.assertEqual( + [ + "appliance_id", + "earliest_licence_expiry_time", + "latest_licence_expiry_time", + ] + [ + "standard_%s" % f for f in constants.LICENCE_STATS_FIELDS + ] + [ + "sap_%s" % f for f in constants.LICENCE_STATS_FIELDS + ], + self.licence.columns + ) + + def test_get_formatted_data(self): + standard_stats = self._make_stats(0) + sap_stats = self._make_stats(100) + obj = self._make_status(standard_stats, sap_stats) result = self.licence._get_formatted_data(obj) @@ -49,17 +63,30 @@ def test_get_formatted_data(self): mock.sentinel.appliance_id, mock.sentinel.earliest_licence_expiry_time, mock.sentinel.latest_licence_expiry_time, - mock.sentinel.current_performed_migrations, - mock.sentinel.current_performed_replicas, - mock.sentinel.current_available_migrations, - mock.sentinel.current_available_replicas, - mock.sentinel.lifetime_performed_migrations, - mock.sentinel.lifetime_performed_replicas, - mock.sentinel.lifetime_available_migrations, - mock.sentinel.lifetime_available_replicas + ] + [ + standard_stats[f] for f in constants.LICENCE_STATS_FIELDS + ] + [ + sap_stats[f] for f in constants.LICENCE_STATS_FIELDS ], result ) + self.assertEqual(len(self.licence.columns), len(result)) + + def test_get_formatted_data_standard_only(self): + standard_stats = self._make_stats(0) + sap_stats = dict.fromkeys(constants.LICENCE_STATS_FIELDS, 0) + obj = self._make_status(standard_stats, sap_stats) + + result = self.licence._get_formatted_data(obj) + + self.assertEqual( + [standard_stats[f] for f in constants.LICENCE_STATS_FIELDS], + result[3:3 + len(constants.LICENCE_STATS_FIELDS)] + ) + self.assertEqual( + [0] * len(constants.LICENCE_STATS_FIELDS), + result[-len(constants.LICENCE_STATS_FIELDS):] + ) class LicenceFormatterTestCase(test_base.CoriolisBaseTestCase): @@ -85,7 +112,7 @@ def test_get_sorted_list(self): result ) - def test_get_formatted_data(self): + def _assert_formatted(self, licence_version, licence_edition): obj = mock.Mock() obj.id = mock.sentinel.id obj.issue_date = mock.sentinel.issue_date @@ -94,9 +121,7 @@ def test_get_formatted_data(self): obj.period_start = mock.sentinel.period_start obj.period_end = mock.sentinel.period_end obj.period_duration = mock.sentinel.period_duration - obj.licence_version = mock.sentinel.licence_version - - result = self.licence._get_formatted_data(obj) + obj.licence_version = licence_version self.assertEqual( ( @@ -107,11 +132,27 @@ def test_get_formatted_data(self): mock.sentinel.period_start, mock.sentinel.period_end, mock.sentinel.period_duration, - mock.sentinel.licence_version + licence_version, + licence_edition, ), - result + self.licence._get_formatted_data(obj) ) + def test_get_formatted_data(self): + self._assert_formatted( + constants.LICENCE_VERSION_V2, constants.LICENCE_EDITION_STANDARD) + + def test_get_formatted_data_sap(self): + self._assert_formatted( + constants.LICENCE_VERSION_V2_SAP, constants.LICENCE_EDITION_SAP) + + def test_get_formatted_data_v1(self): + self._assert_formatted( + constants.LICENCE_VERSION_V1, constants.LICENCE_EDITION_STANDARD) + + def test_get_formatted_data_unknown_version(self): + self._assert_formatted("v3-something", None) + class LicensingApplianceStatusTestCase(test_base.CoriolisBaseTestCase): """Test suite for the Coriolis Client Licensing Appliance Status.""" diff --git a/coriolisclient/tests/cli/test_licensing_reservations.py b/coriolisclient/tests/cli/test_licensing_reservations.py index 705c79f..3d9856d 100644 --- a/coriolisclient/tests/cli/test_licensing_reservations.py +++ b/coriolisclient/tests/cli/test_licensing_reservations.py @@ -7,6 +7,7 @@ from cliff import show from coriolisclient.cli import licensing_reservations +from coriolisclient import constants from coriolisclient.tests import test_base @@ -33,12 +34,12 @@ def test_get_sorted_list(self): result ) - def test_get_formatted_data(self): + def _assert_formatted(self, reservation_type, licence_edition): obj = mock.Mock() obj.id = mock.sentinel.id obj.appliance_id = mock.sentinel.appliance_id obj.licence_id = mock.sentinel.licence_id - obj.type = mock.sentinel.type + obj.type = reservation_type obj.count = mock.sentinel.count obj.created_at = mock.sentinel.created_at @@ -49,13 +50,29 @@ def test_get_formatted_data(self): mock.sentinel.id, mock.sentinel.appliance_id, mock.sentinel.licence_id, - mock.sentinel.type, + reservation_type, + licence_edition, mock.sentinel.count, mock.sentinel.created_at ), result ) + def test_get_formatted_data(self): + for reservation_type in [constants.RESERVATION_TYPE_REPLICA, + constants.RESERVATION_TYPE_MIGRATION]: + self._assert_formatted( + reservation_type, constants.LICENCE_EDITION_STANDARD) + + def test_get_formatted_data_sap(self): + for reservation_type in [constants.RESERVATION_TYPE_SAP_REPLICA, + constants.RESERVATION_TYPE_SAP_MIGRATION]: + self._assert_formatted( + reservation_type, constants.LICENCE_EDITION_SAP) + + def test_get_formatted_data_unknown_type(self): + self._assert_formatted("something", None) + class ReservationListTestCase(test_base.CoriolisBaseTestCase): """Test suite for the Coriolis Client Reservation List."""