Skip to content

Commit d82b88d

Browse files
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 <mbalutoiu@cloudbasesolutions.com>
1 parent 438b877 commit d82b88d

6 files changed

Lines changed: 201 additions & 58 deletions

File tree

‎coriolisclient/cli/licensing.py‎

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,38 +20,45 @@
2020
from cliff import show
2121

2222
from coriolisclient.cli import formatter
23+
from coriolisclient import constants
24+
from coriolisclient.v1 import licensing as licensing_api
2325

2426

2527
class LicensingStatusFormatter(formatter.EntityFormatter):
2628

2729
def __init__(self):
2830
self.columns = [
2931
"appliance_id",
32+
"licence_editions",
3033
"earliest_licence_expiry_time",
3134
"latest_licence_expiry_time",
32-
"current_performed_migrations",
33-
"current_performed_replicas",
34-
"current_available_migrations",
35-
"current_available_replicas",
36-
"lifetime_performed_migrations",
37-
"lifetime_performed_replicas",
38-
"lifetime_available_migrations",
39-
"lifetime_available_replicas",
4035
]
36+
# totals across all the licence editions:
37+
self.columns.extend(constants.LICENCE_STATS_FIELDS)
38+
# per-edition breakdown:
39+
self.columns.extend(
40+
"standard_%s" % field
41+
for field in constants.LICENCE_STATS_FIELDS)
42+
self.columns.extend(
43+
"sap_%s" % field for field in constants.LICENCE_STATS_FIELDS)
4144

4245
def _get_formatted_data(self, obj):
46+
standard_stats = getattr(
47+
obj, constants.LICENCE_STATS_KEY_STANDARD)
48+
sap_stats = getattr(obj, constants.LICENCE_STATS_KEY_SAP)
49+
4350
data = [obj.appliance_id,
51+
", ".join(obj.licence_editions) or None,
4452
obj.earliest_licence_expiry_time,
4553
obj.latest_licence_expiry_time,
46-
obj.current_performed_migrations,
47-
obj.current_performed_replicas,
48-
obj.current_available_migrations,
49-
obj.current_available_replicas,
50-
obj.lifetime_performed_migrations,
51-
obj.lifetime_performed_replicas,
52-
obj.lifetime_available_migrations,
53-
obj.lifetime_available_replicas,
5454
]
55+
data.extend(
56+
getattr(obj, field) for field in constants.LICENCE_STATS_FIELDS)
57+
data.extend(
58+
standard_stats[field]
59+
for field in constants.LICENCE_STATS_FIELDS)
60+
data.extend(
61+
sap_stats[field] for field in constants.LICENCE_STATS_FIELDS)
5562

5663
return data
5764

@@ -65,27 +72,37 @@ class LicenceFormatter(formatter.EntityFormatter):
6572
"Period End",
6673
"Period Duration",
6774
"Licence Version",
75+
"Licence Edition",
6876
)
6977

7078
def _get_sorted_list(self, obj_list):
7179
return sorted(obj_list, key=lambda o: o.period_end)
7280

7381
def _get_formatted_data(self, obj):
82+
# NOTE: licences issued before licence versioning was introduced
83+
# carry no version at all:
84+
licence_version = getattr(obj, 'licence_version', None)
7485
data = (obj.id,
7586
obj.issue_date,
7687
obj.migrations,
7788
obj.replicas,
7889
obj.period_start,
7990
obj.period_end,
8091
obj.period_duration,
81-
obj.licence_version,
92+
licence_version,
93+
licensing_api.get_licence_edition(licence_version),
8294
)
8395

8496
return data
8597

8698

8799
class LicensingApplianceStatus(show.ShowOne):
88-
""" Retrieves Licensing Status for the given appliance. """
100+
""" Retrieves Licensing Status for the given appliance.
101+
102+
The usage counters are reported as a total across all the licence
103+
editions the appliance holds, followed by a per-edition breakdown under
104+
the 'standard_*' and 'sap_*' fields.
105+
"""
89106

90107
def get_parser(self, prog_name):
91108
parser = super().get_parser(prog_name)

‎coriolisclient/cli/licensing_reservations.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,15 @@
1717
from cliff import show
1818

1919
from coriolisclient.cli import formatter
20+
from coriolisclient.v1 import licensing as licensing_api
2021

2122

2223
class ReservationFormatter(formatter.EntityFormatter):
2324
columns = ("ID",
2425
"Appliance ID",
2526
"Licence ID",
2627
"Type",
28+
"Licence Edition",
2729
"Count",
2830
"Created At",
2931
)
@@ -36,6 +38,7 @@ def _get_formatted_data(self, obj):
3638
obj.appliance_id,
3739
obj.licence_id,
3840
obj.type,
41+
licensing_api.get_reservation_edition(obj.type),
3942
obj.count,
4043
obj.created_at,
4144
)

‎coriolisclient/cli/licensing_server.py‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,33 @@
1616
from cliff import show
1717

1818
from coriolisclient.cli import formatter
19+
from coriolisclient.v1 import licensing as licensing_api
1920

2021

2122
class ServerStatusFormatter(formatter.EntityFormatter):
2223
columns = [
2324
"Hostname", "Multi-appliance",
24-
"Supported Licence Versions", "Server Time"]
25+
"Supported Licence Versions",
26+
"Supported Licence Editions",
27+
"Server Time"]
2528

2629
def _get_sorted_list(self, obj_list):
2730
return sorted(obj_list, key=lambda o: o.hostname)
2831

32+
def _get_editions(self, licence_versions):
33+
editions = []
34+
for version in licence_versions or []:
35+
edition = licensing_api.get_licence_edition(version)
36+
if edition not in editions:
37+
editions.append(edition)
38+
return ", ".join(editions) or None
39+
2940
def _get_formatted_data(self, obj):
3041
data = [
3142
obj.hostname,
3243
obj.multi_appliance,
3344
obj.supported_licence_versions,
45+
self._get_editions(obj.supported_licence_versions),
3446
obj.server_local_time]
3547
return data
3648

‎coriolisclient/tests/cli/test_licensing.py‎

Lines changed: 112 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,12 @@
88
from cliff import show
99

1010
from coriolisclient.cli import licensing
11+
from coriolisclient import constants
1112
from coriolisclient.tests import test_base
1213

14+
# sentinel for "this licence has no 'licence_version' attribute at all":
15+
_UNSET = object()
16+
1317

1418
class LicensingStatusFormatterTestCase(test_base.CoriolisBaseTestCase):
1519
"""Test suite for the Coriolis Client Licensing Status Formatter."""
@@ -18,48 +22,92 @@ def setUp(self):
1822
super(LicensingStatusFormatterTestCase, self).setUp()
1923
self.licence = licensing.LicensingStatusFormatter()
2024

21-
def test_get_formatted_data(self):
25+
@staticmethod
26+
def _make_stats(offset):
27+
"""Builds a stats body whose counters are all distinguishable."""
28+
return {
29+
field: offset + i
30+
for i, field in enumerate(constants.LICENCE_STATS_FIELDS)}
31+
32+
def _make_status(self, standard_stats, sap_stats, licence_editions):
2233
obj = mock.Mock()
2334
obj.appliance_id = mock.sentinel.appliance_id
2435
obj.earliest_licence_expiry_time = \
2536
mock.sentinel.earliest_licence_expiry_time
2637
obj.latest_licence_expiry_time = \
2738
mock.sentinel.latest_licence_expiry_time
28-
obj.current_performed_migrations = \
29-
mock.sentinel.current_performed_migrations
30-
obj.current_performed_replicas = \
31-
mock.sentinel.current_performed_replicas
32-
obj.current_available_migrations = \
33-
mock.sentinel.current_available_migrations
34-
obj.current_available_replicas = \
35-
mock.sentinel.current_available_replicas
36-
obj.lifetime_performed_migrations = \
37-
mock.sentinel.lifetime_performed_migrations
38-
obj.lifetime_performed_replicas = \
39-
mock.sentinel.lifetime_performed_replicas
40-
obj.lifetime_available_migrations = \
41-
mock.sentinel.lifetime_available_migrations
42-
obj.lifetime_available_replicas = \
43-
mock.sentinel.lifetime_available_replicas
39+
obj.licence_editions = licence_editions
40+
setattr(obj, constants.LICENCE_STATS_KEY_STANDARD, standard_stats)
41+
setattr(obj, constants.LICENCE_STATS_KEY_SAP, sap_stats)
42+
for field in constants.LICENCE_STATS_FIELDS:
43+
setattr(obj, field, standard_stats[field] + sap_stats[field])
44+
return obj
45+
46+
def test_columns(self):
47+
self.assertEqual(
48+
[
49+
"appliance_id",
50+
"licence_editions",
51+
"earliest_licence_expiry_time",
52+
"latest_licence_expiry_time",
53+
] + list(constants.LICENCE_STATS_FIELDS) + [
54+
"standard_%s" % f for f in constants.LICENCE_STATS_FIELDS
55+
] + [
56+
"sap_%s" % f for f in constants.LICENCE_STATS_FIELDS
57+
],
58+
self.licence.columns
59+
)
60+
61+
def test_get_formatted_data(self):
62+
standard_stats = self._make_stats(0)
63+
sap_stats = self._make_stats(100)
64+
obj = self._make_status(standard_stats, sap_stats,
65+
["Standard", "SAP"])
4466

4567
result = self.licence._get_formatted_data(obj)
4668

4769
self.assertEqual(
4870
[
4971
mock.sentinel.appliance_id,
72+
"Standard, SAP",
5073
mock.sentinel.earliest_licence_expiry_time,
5174
mock.sentinel.latest_licence_expiry_time,
52-
mock.sentinel.current_performed_migrations,
53-
mock.sentinel.current_performed_replicas,
54-
mock.sentinel.current_available_migrations,
55-
mock.sentinel.current_available_replicas,
56-
mock.sentinel.lifetime_performed_migrations,
57-
mock.sentinel.lifetime_performed_replicas,
58-
mock.sentinel.lifetime_available_migrations,
59-
mock.sentinel.lifetime_available_replicas
75+
] + [
76+
standard_stats[f] + sap_stats[f]
77+
for f in constants.LICENCE_STATS_FIELDS
78+
] + [
79+
standard_stats[f] for f in constants.LICENCE_STATS_FIELDS
80+
] + [
81+
sap_stats[f] for f in constants.LICENCE_STATS_FIELDS
6082
],
6183
result
6284
)
85+
self.assertEqual(len(self.licence.columns), len(result))
86+
87+
def test_get_formatted_data_standard_only(self):
88+
standard_stats = self._make_stats(0)
89+
sap_stats = dict.fromkeys(constants.LICENCE_STATS_FIELDS, 0)
90+
obj = self._make_status(standard_stats, sap_stats, ["Standard"])
91+
92+
result = self.licence._get_formatted_data(obj)
93+
94+
self.assertEqual("Standard", result[1])
95+
self.assertEqual(
96+
[standard_stats[f] for f in constants.LICENCE_STATS_FIELDS],
97+
result[4:4 + len(constants.LICENCE_STATS_FIELDS)]
98+
)
99+
self.assertEqual(
100+
[0] * len(constants.LICENCE_STATS_FIELDS),
101+
result[-len(constants.LICENCE_STATS_FIELDS):]
102+
)
103+
104+
def test_get_formatted_data_no_licences(self):
105+
empty = dict.fromkeys(constants.LICENCE_STATS_FIELDS, 0)
106+
obj = self._make_status(empty, dict(empty), [])
107+
108+
result = self.licence._get_formatted_data(obj)
109+
110+
self.assertIsNone(result[1])
63111

64112

65113
class LicenceFormatterTestCase(test_base.CoriolisBaseTestCase):
@@ -85,19 +133,24 @@ def test_get_sorted_list(self):
85133
result
86134
)
87135

88-
def test_get_formatted_data(self):
89-
obj = mock.Mock()
136+
@staticmethod
137+
def _make_licence(licence_version=_UNSET):
138+
obj = mock.Mock(spec=[
139+
'id', 'issue_date', 'migrations', 'replicas', 'period_start',
140+
'period_end', 'period_duration'] + (
141+
[] if licence_version is _UNSET else ['licence_version']))
90142
obj.id = mock.sentinel.id
91143
obj.issue_date = mock.sentinel.issue_date
92144
obj.migrations = mock.sentinel.migrations
93145
obj.replicas = mock.sentinel.replicas
94146
obj.period_start = mock.sentinel.period_start
95147
obj.period_end = mock.sentinel.period_end
96148
obj.period_duration = mock.sentinel.period_duration
97-
obj.licence_version = mock.sentinel.licence_version
98-
99-
result = self.licence._get_formatted_data(obj)
149+
if licence_version is not _UNSET:
150+
obj.licence_version = licence_version
151+
return obj
100152

153+
def _assert_formatted(self, obj, licence_version, licence_edition):
101154
self.assertEqual(
102155
(
103156
mock.sentinel.id,
@@ -107,11 +160,38 @@ def test_get_formatted_data(self):
107160
mock.sentinel.period_start,
108161
mock.sentinel.period_end,
109162
mock.sentinel.period_duration,
110-
mock.sentinel.licence_version
163+
licence_version,
164+
licence_edition,
111165
),
112-
result
166+
self.licence._get_formatted_data(obj)
113167
)
114168

169+
def test_get_formatted_data(self):
170+
self._assert_formatted(
171+
self._make_licence(constants.LICENCE_VERSION_V2),
172+
constants.LICENCE_VERSION_V2, constants.LICENCE_EDITION_STANDARD)
173+
174+
def test_get_formatted_data_sap(self):
175+
self._assert_formatted(
176+
self._make_licence(constants.LICENCE_VERSION_V2_SAP),
177+
constants.LICENCE_VERSION_V2_SAP, constants.LICENCE_EDITION_SAP)
178+
179+
def test_get_formatted_data_v1(self):
180+
self._assert_formatted(
181+
self._make_licence(constants.LICENCE_VERSION_V1),
182+
constants.LICENCE_VERSION_V1, constants.LICENCE_EDITION_STANDARD)
183+
184+
def test_get_formatted_data_unversioned_licence(self):
185+
# licences issued before licence versioning carry no version at all:
186+
self._assert_formatted(
187+
self._make_licence(), None, constants.LICENCE_EDITION_STANDARD)
188+
189+
def test_get_formatted_data_unknown_version(self):
190+
# an unknown version must not be misreported as a standard licence:
191+
self._assert_formatted(
192+
self._make_licence("v3-something"),
193+
"v3-something", "v3-something")
194+
115195

116196
class LicensingApplianceStatusTestCase(test_base.CoriolisBaseTestCase):
117197
"""Test suite for the Coriolis Client Licensing Appliance Status."""

0 commit comments

Comments
 (0)