licomp-toolkit 0.6.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- licomp_toolkit/__init__.py +0 -0
- licomp_toolkit/__main__.py +322 -0
- licomp_toolkit/config.py +40 -0
- licomp_toolkit/data/reply_schema.json +305 -0
- licomp_toolkit/display_compatibility.py +21 -0
- licomp_toolkit/exception.py +11 -0
- licomp_toolkit/expr_parser.py +139 -0
- licomp_toolkit/format.py +362 -0
- licomp_toolkit/lic_expr.py +325 -0
- licomp_toolkit/license_policy.py +564 -0
- licomp_toolkit/return_codes.py +12 -0
- licomp_toolkit/schema_checker.py +67 -0
- licomp_toolkit/suggester.py +162 -0
- licomp_toolkit/toolkit.py +504 -0
- licomp_toolkit/utils.py +81 -0
- licomp_toolkit-0.6.3.dist-info/METADATA +164 -0
- licomp_toolkit-0.6.3.dist-info/RECORD +21 -0
- licomp_toolkit-0.6.3.dist-info/WHEEL +5 -0
- licomp_toolkit-0.6.3.dist-info/entry_points.txt +2 -0
- licomp_toolkit-0.6.3.dist-info/licenses/LICENSES/GPL-3.0-or-later.txt +232 -0
- licomp_toolkit-0.6.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2024 Henrik Sandklef
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
from collections import OrderedDict
|
|
6
|
+
|
|
7
|
+
from functools import cmp_to_key
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
|
|
11
|
+
from licomp_toolkit.toolkit import LicompToolkit
|
|
12
|
+
from licomp.interface import UseCase
|
|
13
|
+
from licomp.interface import Provisioning
|
|
14
|
+
from licomp.interface import Modification
|
|
15
|
+
|
|
16
|
+
class LicensePolicyException(Exception):
|
|
17
|
+
|
|
18
|
+
def __init__(self, message):
|
|
19
|
+
self.message = message
|
|
20
|
+
super().__init__(self.message)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class LicensePolicy:
|
|
24
|
+
|
|
25
|
+
def __init__(self, policy_file):
|
|
26
|
+
data = self._read_json_file(policy_file)
|
|
27
|
+
self.policy_meta = data['meta']
|
|
28
|
+
self.policy = data['policy']
|
|
29
|
+
|
|
30
|
+
def _read_json_data(self, data):
|
|
31
|
+
return json.load(data)
|
|
32
|
+
|
|
33
|
+
def _read_json_file(self, file_name):
|
|
34
|
+
with open(file_name) as fp:
|
|
35
|
+
return self._read_json_data(fp)
|
|
36
|
+
|
|
37
|
+
def allowed(self):
|
|
38
|
+
return self.policy['allowed']
|
|
39
|
+
|
|
40
|
+
def avoided(self):
|
|
41
|
+
return self.policy['avoided']
|
|
42
|
+
|
|
43
|
+
def denied(self):
|
|
44
|
+
return self.policy['denied']
|
|
45
|
+
|
|
46
|
+
def meta(self):
|
|
47
|
+
return self.policy_meta
|
|
48
|
+
|
|
49
|
+
def list_presence(self, lic, ignore_missing=False):
|
|
50
|
+
|
|
51
|
+
if lic in self.allowed():
|
|
52
|
+
return 1, self.allowed().index(lic)
|
|
53
|
+
if lic in self.avoided():
|
|
54
|
+
return 2, self.avoided().index(lic)
|
|
55
|
+
if lic in self.denied():
|
|
56
|
+
return 3, self.denied().index(lic)
|
|
57
|
+
|
|
58
|
+
if ignore_missing:
|
|
59
|
+
return None, None
|
|
60
|
+
raise LicensePolicyException(f'License "{lic}" is not present in any of the policy\'s lists. Licenses: {self.allowed()}, {self.avoided()} ')
|
|
61
|
+
|
|
62
|
+
def list_nr_to_name(self, nr):
|
|
63
|
+
return {
|
|
64
|
+
1: 'allowed',
|
|
65
|
+
2: 'avoided',
|
|
66
|
+
3: 'denied'}.get(nr, '')
|
|
67
|
+
|
|
68
|
+
def list_name_to_nr(self, nr):
|
|
69
|
+
return {
|
|
70
|
+
'allowed': 1,
|
|
71
|
+
'avoided': 2,
|
|
72
|
+
'denied': 3}.get(nr, None)
|
|
73
|
+
|
|
74
|
+
def compare_preferences(self, lic1, lic2, ignore_missing=False):
|
|
75
|
+
return self.compare_preferences_general(lic1, lic2, ignore_missing=ignore_missing)
|
|
76
|
+
|
|
77
|
+
def compare_preferences_expressions(self, lic1, lic2, key, ignore_missing=False):
|
|
78
|
+
return self.compare_preferences_general(lic1, lic2, key, ignore_missing=ignore_missing)
|
|
79
|
+
|
|
80
|
+
def compare_preferences_general(self, lic1, lic2, key=None, ignore_missing=False):
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
returns
|
|
84
|
+
* negative if lic1 is more preferred than lic2
|
|
85
|
+
* None if both licenses are denied
|
|
86
|
+
raises
|
|
87
|
+
* LicensePolicyException if at least one license is not listed (if ignore_missing, then both licenses need to be not listed to raise exception)
|
|
88
|
+
"""
|
|
89
|
+
logging.debug(f'compare_preferences {lic1}, {lic2}, {key}, {ignore_missing}')
|
|
90
|
+
if key:
|
|
91
|
+
list_key = key.replace('_license', '')
|
|
92
|
+
lic1_name = lic1[list_key]['license']
|
|
93
|
+
lic2_name = lic2[list_key]['license']
|
|
94
|
+
if lic1[list_key]['type'] == 'license':
|
|
95
|
+
lic1_list, lic1_index = self.list_presence(lic1[list_key]['license'], ignore_missing=False)
|
|
96
|
+
else:
|
|
97
|
+
lic1_list_name = lic1[list_key]['preferences']['license_list']
|
|
98
|
+
lic1_list = self.list_name_to_nr(lic1_list_name)
|
|
99
|
+
lic1_index = lic1[list_key]['preferences']['license_index']
|
|
100
|
+
|
|
101
|
+
if lic2[list_key]['type'] == 'license':
|
|
102
|
+
lic2_list, lic2_index = self.list_presence(lic2[list_key]['license'], ignore_missing=False)
|
|
103
|
+
else: # license-expression
|
|
104
|
+
lic2_list_name = lic2[list_key]['preferences']['license_list']
|
|
105
|
+
lic2_list = self.list_name_to_nr(lic2_list_name)
|
|
106
|
+
lic2_index = lic2[list_key]['preferences']['license_index']
|
|
107
|
+
else:
|
|
108
|
+
lic1_name = lic1
|
|
109
|
+
lic2_name = lic2
|
|
110
|
+
lic1_list, lic1_index = self.list_presence(lic1, ignore_missing=ignore_missing)
|
|
111
|
+
lic2_list, lic2_index = self.list_presence(lic2, ignore_missing=ignore_missing)
|
|
112
|
+
|
|
113
|
+
logging.debug(f'compare_preferences: "{lic1_list}", "{lic1_index}" "{lic2_list}", "{lic2_index}"')
|
|
114
|
+
|
|
115
|
+
if (not lic1_list) and (not lic2_list):
|
|
116
|
+
if ignore_missing:
|
|
117
|
+
logging.debug(f'compare_preferences({lic1_name}, {lic2_name}, {ignore_missing}): ignore since both None')
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
raise LicensePolicyException(f'License "{lic1_name}" and/or "{lic2_name}" is not present in any of the policy\'s lists.')
|
|
121
|
+
|
|
122
|
+
if lic1_list == lic2_list:
|
|
123
|
+
# if both are denied, return None to indicate "error"
|
|
124
|
+
if lic1_list == 3:
|
|
125
|
+
return None
|
|
126
|
+
return lic1_index - lic2_index
|
|
127
|
+
|
|
128
|
+
if not lic1_list:
|
|
129
|
+
return 1
|
|
130
|
+
if not lic2_list:
|
|
131
|
+
return -1
|
|
132
|
+
|
|
133
|
+
return lic1_list - lic2_list
|
|
134
|
+
|
|
135
|
+
def most_preferred(self, lic1, lic2, ignore_missing=False):
|
|
136
|
+
pref = self.compare_preferences(lic1, lic2, ignore_missing)
|
|
137
|
+
if pref is None:
|
|
138
|
+
return None
|
|
139
|
+
if pref < 0:
|
|
140
|
+
return lic1
|
|
141
|
+
return lic2
|
|
142
|
+
|
|
143
|
+
def OBSOLETE_preferred_score_ignore_missing(self, lic1, lic2):
|
|
144
|
+
pref = self.compare_preferences(lic1, lic2, ignore_missing=True)
|
|
145
|
+
if pref is None:
|
|
146
|
+
return 10000
|
|
147
|
+
if pref < 0:
|
|
148
|
+
return -1
|
|
149
|
+
if lic1 == lic2:
|
|
150
|
+
return 0
|
|
151
|
+
return 1
|
|
152
|
+
|
|
153
|
+
def preferred_score_licenses(self, lic1, lic2, key):
|
|
154
|
+
logging.debug(f'preferred_score_licenses {lic1}, {lic2}, {key}')
|
|
155
|
+
pref = self.compare_preferences_expressions(lic1, lic2, key)
|
|
156
|
+
if pref < 0:
|
|
157
|
+
return -1
|
|
158
|
+
if lic1 == lic2:
|
|
159
|
+
return 0
|
|
160
|
+
return 1
|
|
161
|
+
|
|
162
|
+
def preferred_score_inbounds(self, lic1, lic2):
|
|
163
|
+
logging.debug(f'preferred_score_inbounds {lic1}, {lic2}')
|
|
164
|
+
return self.preferred_score_licenses(lic1, lic2, 'inbound_license')
|
|
165
|
+
|
|
166
|
+
def preferred_score_outbounds(self, lic1, lic2):
|
|
167
|
+
logging.debug(f'preferred_score_outbounds {lic1}, {lic2}')
|
|
168
|
+
return self.preferred_score_licenses(lic1, lic2, 'outbound_license')
|
|
169
|
+
|
|
170
|
+
def least_preferred(self, lic1, lic2, ignore_missing=False):
|
|
171
|
+
most_preferred = self.most_preferred(lic1, lic2, ignore_missing)
|
|
172
|
+
if most_preferred == lic1:
|
|
173
|
+
return lic2
|
|
174
|
+
return lic1
|
|
175
|
+
|
|
176
|
+
class DefaultLicensePolicy(LicensePolicy):
|
|
177
|
+
|
|
178
|
+
def __init__(self, resources, usecase, provisioning):
|
|
179
|
+
self.lt = LicompToolkit()
|
|
180
|
+
self.__licenses(resources, usecase, provisioning)
|
|
181
|
+
license_order = self.__order(resources, usecase, provisioning)
|
|
182
|
+
self.policy_meta = {}
|
|
183
|
+
self.policy = {
|
|
184
|
+
'allowed': license_order,
|
|
185
|
+
'avoided': [],
|
|
186
|
+
'denied': [],
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
def __order(self, resources, usecase, provisioning):
|
|
190
|
+
scores = {}
|
|
191
|
+
for lic in self.licenses:
|
|
192
|
+
scores[lic] = 0
|
|
193
|
+
for out_license in self.licenses:
|
|
194
|
+
for in_license in self.licenses:
|
|
195
|
+
for resource in self.resources:
|
|
196
|
+
compat = resource.outbound_inbound_compatibility(out_license,
|
|
197
|
+
in_license,
|
|
198
|
+
UseCase.string_to_usecase(usecase),
|
|
199
|
+
Provisioning.string_to_provisioning(provisioning),
|
|
200
|
+
Modification.UNMODIFIED)
|
|
201
|
+
if compat['compatibility_status'] == 'yes':
|
|
202
|
+
scores[in_license] += 1
|
|
203
|
+
|
|
204
|
+
scores_dict = OrderedDict(sorted(scores.items(), key=lambda x: x[1], reverse=True))
|
|
205
|
+
return [x for (x, y) in scores_dict.items()]
|
|
206
|
+
|
|
207
|
+
def __licenses(self, resources, usecase, provisioning):
|
|
208
|
+
self.resources = []
|
|
209
|
+
self.licenses = []
|
|
210
|
+
|
|
211
|
+
logging.debug(f'__licenses {resources}, {usecase}, {provisioning}')
|
|
212
|
+
for resource in resources:
|
|
213
|
+
for licomp_resource in self.lt.licomp_resources():
|
|
214
|
+
if resource == licomp_resource:
|
|
215
|
+
self.licenses += self.lt.licomp_resources()[licomp_resource].supported_licenses()
|
|
216
|
+
self.resources.append(self.lt.licomp_resources()[licomp_resource])
|
|
217
|
+
logging.debug(f'__licenses {resources}, {usecase}, {provisioning} ==> {self.licenses}')
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class LicensePolicyHandler:
|
|
221
|
+
|
|
222
|
+
def __init__(self, policy_file=None, resources=None, usecase=None, provisioning=None):
|
|
223
|
+
logging.debug("LicensePolicyHandler()")
|
|
224
|
+
if policy_file:
|
|
225
|
+
self.policy = LicensePolicy(policy_file)
|
|
226
|
+
self.policy_type = 'policy_file'
|
|
227
|
+
self.policy_file = policy_file
|
|
228
|
+
else:
|
|
229
|
+
self.policy = DefaultLicensePolicy(resources, usecase, provisioning)
|
|
230
|
+
self.policy_type = 'default'
|
|
231
|
+
self.policy_file = None
|
|
232
|
+
|
|
233
|
+
def __is_license_expression(self, lic):
|
|
234
|
+
CONTAINS_AND = 'AND' in lic
|
|
235
|
+
CONTAINS_OR = 'OR' in lic
|
|
236
|
+
return CONTAINS_AND or CONTAINS_OR
|
|
237
|
+
|
|
238
|
+
def usable_license(self, lic, key):
|
|
239
|
+
if key in lic:
|
|
240
|
+
license_name = lic[key]['license']
|
|
241
|
+
else:
|
|
242
|
+
license_name = lic
|
|
243
|
+
if self.__is_license_expression(license_name):
|
|
244
|
+
# should have been checked already, so skip
|
|
245
|
+
policy_ok = True
|
|
246
|
+
else:
|
|
247
|
+
policy_ok = license_name in (self.policy.allowed() + self.policy.avoided())
|
|
248
|
+
compat_ok = lic['compatibility'] == 'yes'
|
|
249
|
+
|
|
250
|
+
ret = policy_ok and compat_ok
|
|
251
|
+
logging.debug(f'usable_license {lic} | policy_ok: {policy_ok}, compat_ok: {compat_ok} ===> {ret}')
|
|
252
|
+
|
|
253
|
+
return ret
|
|
254
|
+
|
|
255
|
+
def unusable_licenses(self, licenses, key):
|
|
256
|
+
problematic_licenses = []
|
|
257
|
+
for lic in licenses:
|
|
258
|
+
license_name = lic[key]['license']
|
|
259
|
+
if self.__is_license_expression(license_name):
|
|
260
|
+
# should have been checked already, so skip
|
|
261
|
+
policy_ok = True
|
|
262
|
+
else:
|
|
263
|
+
policy_ok = license_name in (self.policy.allowed() + self.policy.avoided())
|
|
264
|
+
compat_ok = lic['compatibility'] == 'yes'
|
|
265
|
+
|
|
266
|
+
ret = policy_ok and compat_ok
|
|
267
|
+
logging.debug(f'unusable_license {lic} | policy_ok: {policy_ok}, compat_ok: {compat_ok} ===> {ret}')
|
|
268
|
+
if not ret:
|
|
269
|
+
problematic_licenses.append(lic)
|
|
270
|
+
return len(problematic_licenses) > 0
|
|
271
|
+
|
|
272
|
+
def scored_general(self, licenses, operator, key):
|
|
273
|
+
compare_function = {
|
|
274
|
+
'inbound': self.policy.preferred_score_inbounds,
|
|
275
|
+
'outbound': self.policy.preferred_score_outbounds,
|
|
276
|
+
}[key]
|
|
277
|
+
if operator == "OR":
|
|
278
|
+
logging.debug(f'preferred_licenses {licenses}, {operator}')
|
|
279
|
+
filtered_licenses = []
|
|
280
|
+
unusable_licenses = []
|
|
281
|
+
for lic in licenses:
|
|
282
|
+
if self.usable_license(lic, key):
|
|
283
|
+
filtered_licenses.append(lic)
|
|
284
|
+
else:
|
|
285
|
+
print("UNUSABLE: " + str(key))
|
|
286
|
+
print("UNUSABLE: " + str(lic))
|
|
287
|
+
unusable_licenses.append(lic[key])
|
|
288
|
+
sorted_licenses = sorted(filtered_licenses, key=cmp_to_key(compare_function))
|
|
289
|
+
return sorted_licenses, unusable_licenses
|
|
290
|
+
elif operator == "AND":
|
|
291
|
+
if self.unusable_licenses(licenses, key):
|
|
292
|
+
sorted_licenses = []
|
|
293
|
+
unusable_licenses = [x[key] for x in licenses]
|
|
294
|
+
else:
|
|
295
|
+
sorted_licenses = sorted(licenses, key=cmp_to_key(compare_function))
|
|
296
|
+
unusable_licenses = []
|
|
297
|
+
|
|
298
|
+
return sorted_licenses, unusable_licenses
|
|
299
|
+
else:
|
|
300
|
+
raise Exception(f'scored_general got a bad operator "{operator}"')
|
|
301
|
+
|
|
302
|
+
def scored_inbounds(self, inbounds, operator):
|
|
303
|
+
return self.scored_general(inbounds, operator, 'inbound')
|
|
304
|
+
|
|
305
|
+
def scored_outbounds(self, outbounds, operator):
|
|
306
|
+
return self.scored_general(outbounds, operator, 'outbound')
|
|
307
|
+
|
|
308
|
+
def __check_policy_check(self, reply):
|
|
309
|
+
keys = [
|
|
310
|
+
'license',
|
|
311
|
+
'type',
|
|
312
|
+
'preferences:license',
|
|
313
|
+
'preferences:license_list',
|
|
314
|
+
'preferences:license_index',
|
|
315
|
+
]
|
|
316
|
+
for key in keys:
|
|
317
|
+
in_value = reply['inbound']
|
|
318
|
+
out_value = reply['outbound']
|
|
319
|
+
for sub_key in key.split(':'):
|
|
320
|
+
assert sub_key in in_value # noqa: S101
|
|
321
|
+
assert sub_key in out_value # noqa: S101
|
|
322
|
+
in_value = in_value[sub_key]
|
|
323
|
+
out_value = out_value[sub_key]
|
|
324
|
+
|
|
325
|
+
assert 'compatibility' in reply # noqa: S101
|
|
326
|
+
assert 'unusable' in reply # noqa: S101
|
|
327
|
+
|
|
328
|
+
def __pack_policy_check(self, outbound_pref, inbound_pref, compat, unusable):
|
|
329
|
+
return {
|
|
330
|
+
'outbound': outbound_pref,
|
|
331
|
+
'inbound': inbound_pref,
|
|
332
|
+
'compatibility': compat,
|
|
333
|
+
'unusable': unusable,
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
def __pack_policy_unusable(self, unusables):
|
|
337
|
+
return unusables
|
|
338
|
+
|
|
339
|
+
def __pack_policy_preferences(self, license_string, license_list, license_index):
|
|
340
|
+
return {
|
|
341
|
+
'license': license_string,
|
|
342
|
+
'license_list': license_list,
|
|
343
|
+
'license_index': license_index,
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
def __pack_policy_check_license(self, license_name, license_type, pref_lic, pref_lic_list, pref_lic_idx):
|
|
347
|
+
return {
|
|
348
|
+
'license': license_name,
|
|
349
|
+
'type': license_type,
|
|
350
|
+
'preferences': {
|
|
351
|
+
'license': pref_lic,
|
|
352
|
+
'license_list': pref_lic_list,
|
|
353
|
+
'license_index': pref_lic_idx,
|
|
354
|
+
},
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
def __apply_to_compat_object(self, compat_object, ignore_missing=False, indent=0):
|
|
358
|
+
"""
|
|
359
|
+
Specification of the policy_check format
|
|
360
|
+
|
|
361
|
+
define license_data:
|
|
362
|
+
|
|
363
|
+
license (str)
|
|
364
|
+
license_type (str: license | license-expression)
|
|
365
|
+
preferrences {
|
|
366
|
+
license
|
|
367
|
+
license_list
|
|
368
|
+
license_index
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
unusable [
|
|
372
|
+
{ license (str),
|
|
373
|
+
reason (str)
|
|
374
|
+
}
|
|
375
|
+
]
|
|
376
|
+
|
|
377
|
+
inbound (license_data)
|
|
378
|
+
outbound (license_data)
|
|
379
|
+
compatibility (str)
|
|
380
|
+
|
|
381
|
+
"""
|
|
382
|
+
unusable = []
|
|
383
|
+
if 'outbound-expression' in compat_object['compatibility_check']:
|
|
384
|
+
operator = compat_object['operator']
|
|
385
|
+
outbounds = []
|
|
386
|
+
for operand in compat_object['operands']:
|
|
387
|
+
operand['compatibility_object']['TEST'] = "Liverpool"
|
|
388
|
+
self.__apply_to_compat_object(operand['compatibility_object'], indent + 4)
|
|
389
|
+
self.__check_policy_check(operand['compatibility_object']['policy_check'])
|
|
390
|
+
|
|
391
|
+
outbounds.append(operand['compatibility_object']['policy_check'])
|
|
392
|
+
|
|
393
|
+
scored_outbounds, unusable = self.scored_outbounds(outbounds, operator)
|
|
394
|
+
|
|
395
|
+
outbound_list = []
|
|
396
|
+
outbound_list_index = -1
|
|
397
|
+
preferred_inbound_license = None
|
|
398
|
+
compatibility = 'no'
|
|
399
|
+
|
|
400
|
+
if len(scored_outbounds) > 0:
|
|
401
|
+
if operator == 'AND':
|
|
402
|
+
outbound_name = ' AND '.join([x['outbound']['preferences']['license'] for x in scored_outbounds])
|
|
403
|
+
outbound_pref = scored_outbounds[-1]
|
|
404
|
+
else:
|
|
405
|
+
outbound_name = scored_outbounds[0]['outbound']['preferences']['license']
|
|
406
|
+
outbound_pref = scored_outbounds[0]
|
|
407
|
+
|
|
408
|
+
outbound_list = outbound_pref['outbound']['preferences']['license_list']
|
|
409
|
+
outbound_list_index = outbound_pref['outbound']['preferences']['license_index']
|
|
410
|
+
inbound_prefs = outbound_pref['inbound']['preferences']
|
|
411
|
+
preferred_inbound_license = inbound_prefs['license']
|
|
412
|
+
preferred_inbound_license_list = inbound_prefs['license_list']
|
|
413
|
+
preferred_inbound_license_index = inbound_prefs['license_index']
|
|
414
|
+
|
|
415
|
+
compatibility = 'yes'
|
|
416
|
+
else:
|
|
417
|
+
outbound_name = None
|
|
418
|
+
outbound_list = None
|
|
419
|
+
outbound_list_index = None
|
|
420
|
+
|
|
421
|
+
preferred_inbound_license = None
|
|
422
|
+
preferred_inbound_license_list = None
|
|
423
|
+
preferred_inbound_license_index = None
|
|
424
|
+
|
|
425
|
+
compatibility = 'no'
|
|
426
|
+
if 'inbound-expression' in compat_object['compatibility_check']:
|
|
427
|
+
inbound_license_type = 'license-expression'
|
|
428
|
+
else:
|
|
429
|
+
inbound_license_type = 'license'
|
|
430
|
+
|
|
431
|
+
policy_check_inbound_license = self.__pack_policy_check_license(
|
|
432
|
+
compat_object['inbound_license'],
|
|
433
|
+
inbound_license_type,
|
|
434
|
+
preferred_inbound_license,
|
|
435
|
+
preferred_inbound_license_list,
|
|
436
|
+
preferred_inbound_license_index,
|
|
437
|
+
)
|
|
438
|
+
policy_check_outbound_license = self.__pack_policy_check_license(
|
|
439
|
+
compat_object['outbound_license'],
|
|
440
|
+
'license-expression',
|
|
441
|
+
outbound_name,
|
|
442
|
+
outbound_list,
|
|
443
|
+
outbound_list_index,
|
|
444
|
+
)
|
|
445
|
+
unusable = self.__pack_policy_unusable(unusable)
|
|
446
|
+
policy_license_object = self.__pack_policy_check(
|
|
447
|
+
policy_check_outbound_license,
|
|
448
|
+
policy_check_inbound_license,
|
|
449
|
+
compatibility,
|
|
450
|
+
unusable,
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
compat_object['policy_check'] = policy_license_object
|
|
454
|
+
|
|
455
|
+
elif 'outbound-license' in compat_object['compatibility_check']:
|
|
456
|
+
|
|
457
|
+
if 'inbound-expression' in compat_object['compatibility_check']:
|
|
458
|
+
inner_compat_object = compat_object
|
|
459
|
+
operator = inner_compat_object['operator']
|
|
460
|
+
inbounds = []
|
|
461
|
+
for operand in inner_compat_object['operands']:
|
|
462
|
+
self.__apply_to_compat_object(operand['compatibility_object'], indent + 4)
|
|
463
|
+
self.__check_policy_check(operand['compatibility_object']['policy_check'])
|
|
464
|
+
inbounds.append(operand['compatibility_object']['policy_check'])
|
|
465
|
+
|
|
466
|
+
scored_inbounds, unusable = self.scored_inbounds(inbounds, operator)
|
|
467
|
+
inbound_list = ''
|
|
468
|
+
inbound_list_index = -1
|
|
469
|
+
preferred_inbound_license = None
|
|
470
|
+
|
|
471
|
+
if len(scored_inbounds) > 0:
|
|
472
|
+
if operator == 'AND':
|
|
473
|
+
# use last (least preferred) license
|
|
474
|
+
inbound_list = scored_inbounds[-1]['inbound']['preferences']['license_list']
|
|
475
|
+
inbound_list_index = scored_inbounds[-1]['inbound']['preferences']['license_index']
|
|
476
|
+
preferred_inbound_license = ' AND '.join([x['inbound']['preferences']['license'] for x in scored_inbounds])
|
|
477
|
+
else:
|
|
478
|
+
# use first (most preferred) license
|
|
479
|
+
inbound_list = scored_inbounds[0]['inbound']['preferences']['license_list']
|
|
480
|
+
inbound_list_index = scored_inbounds[0]['inbound']['preferences']['license_index']
|
|
481
|
+
preferred_inbound_license = scored_inbounds[0]['inbound']['preferences']['license']
|
|
482
|
+
|
|
483
|
+
out_lic = inner_compat_object["outbound_license"]
|
|
484
|
+
out_list_nr, out_index = self.policy.list_presence(out_lic, ignore_missing=True)
|
|
485
|
+
out_list_name = self.policy.list_nr_to_name(out_list_nr)
|
|
486
|
+
|
|
487
|
+
policy_check_inbound_license = self.__pack_policy_check_license(
|
|
488
|
+
inner_compat_object['inbound_license'],
|
|
489
|
+
'license-expression',
|
|
490
|
+
preferred_inbound_license,
|
|
491
|
+
inbound_list,
|
|
492
|
+
inbound_list_index,
|
|
493
|
+
)
|
|
494
|
+
policy_check_outbound_license = self.__pack_policy_check_license(
|
|
495
|
+
out_lic,
|
|
496
|
+
'license',
|
|
497
|
+
out_lic,
|
|
498
|
+
out_list_name,
|
|
499
|
+
out_index,
|
|
500
|
+
)
|
|
501
|
+
packed_unusable = self.__pack_policy_unusable(unusable)
|
|
502
|
+
policy_license_object = self.__pack_policy_check(
|
|
503
|
+
policy_check_outbound_license,
|
|
504
|
+
policy_check_inbound_license,
|
|
505
|
+
compat_object['compatibility'],
|
|
506
|
+
packed_unusable)
|
|
507
|
+
inner_compat_object['policy_check'] = policy_license_object
|
|
508
|
+
|
|
509
|
+
if 'inbound-license' in compat_object['compatibility_check']:
|
|
510
|
+
|
|
511
|
+
in_lic = compat_object['inbound_license']
|
|
512
|
+
in_list_nr, in_list_index = self.policy.list_presence(in_lic, ignore_missing=True)
|
|
513
|
+
in_list_name = self.policy.list_nr_to_name(in_list_nr)
|
|
514
|
+
|
|
515
|
+
out_lic = compat_object["outbound_license"]
|
|
516
|
+
out_list_nr, out_index = self.policy.list_presence(out_lic, ignore_missing=True)
|
|
517
|
+
out_list_name = self.policy.list_nr_to_name(out_list_nr)
|
|
518
|
+
|
|
519
|
+
policy_check_inbound_license = self.__pack_policy_check_license(
|
|
520
|
+
in_lic,
|
|
521
|
+
'license',
|
|
522
|
+
in_lic,
|
|
523
|
+
in_list_name,
|
|
524
|
+
in_list_index,
|
|
525
|
+
)
|
|
526
|
+
policy_check_outbound_license = self.__pack_policy_check_license(
|
|
527
|
+
out_lic,
|
|
528
|
+
'license',
|
|
529
|
+
out_lic,
|
|
530
|
+
out_list_name,
|
|
531
|
+
out_index,
|
|
532
|
+
)
|
|
533
|
+
packed_unusable = self.__pack_policy_unusable(unusable)
|
|
534
|
+
policy_license_object = self.__pack_policy_check(
|
|
535
|
+
policy_check_outbound_license,
|
|
536
|
+
policy_check_inbound_license,
|
|
537
|
+
compat_object['compatibility'],
|
|
538
|
+
packed_unusable,
|
|
539
|
+
)
|
|
540
|
+
compat_object['policy_check'] = policy_license_object
|
|
541
|
+
|
|
542
|
+
else:
|
|
543
|
+
raise Exception("We should not be here")
|
|
544
|
+
return None
|
|
545
|
+
|
|
546
|
+
def apply_policy(self, compat_report, ignore_missing=False):
|
|
547
|
+
if not self.policy:
|
|
548
|
+
logging.debug('apply_policy, no policy. Creating default one')
|
|
549
|
+
resources = compat_report['resources']
|
|
550
|
+
usecase = compat_report['usecase']
|
|
551
|
+
provisioning = compat_report['provisioning']
|
|
552
|
+
self.policy = DefaultLicensePolicy(resources, usecase, provisioning)
|
|
553
|
+
self.policy_type = 'default'
|
|
554
|
+
self.policy_file = None
|
|
555
|
+
|
|
556
|
+
top_object = compat_report['compatibility_report']
|
|
557
|
+
logging.debug("apply_policy")
|
|
558
|
+
self.__apply_to_compat_object(top_object,
|
|
559
|
+
ignore_missing=ignore_missing)
|
|
560
|
+
self.__check_policy_check(top_object['policy_check'])
|
|
561
|
+
meta = compat_report['meta']
|
|
562
|
+
meta['policy_type'] = self.policy_type
|
|
563
|
+
meta['policy_file'] = self.policy_file
|
|
564
|
+
return compat_report
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Henrik Sandklef
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
from enum import Enum
|
|
6
|
+
|
|
7
|
+
from licomp.return_codes import ReturnCodes
|
|
8
|
+
|
|
9
|
+
class LicompToolkitReturnCodes(Enum):
|
|
10
|
+
LICOMP_TOOLKIT_INVALID_FILE = ReturnCodes.LICOMP_LAST_ERROR_CODE.value + 1
|
|
11
|
+
|
|
12
|
+
LICOMP_TOOLKIT_LAST_ERROR_CODE = ReturnCodes.LICOMP_LAST_ERROR_CODE.value + 100
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Henrik Sandklef
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import jsonschema
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
|
|
10
|
+
from licomp_toolkit.toolkit import LicompToolkit
|
|
11
|
+
from licomp.interface import LicompException
|
|
12
|
+
|
|
13
|
+
SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
|
|
14
|
+
DATA_DIR = os.path.realpath(os.path.join(SCRIPT_DIR, "data"))
|
|
15
|
+
SCHEMA_FILE = os.path.realpath(os.path.join(DATA_DIR, "reply_schema.json"))
|
|
16
|
+
|
|
17
|
+
class LicompToolkitSchemaChecker:
|
|
18
|
+
|
|
19
|
+
def __init__(self):
|
|
20
|
+
with open(SCHEMA_FILE) as fp:
|
|
21
|
+
self.expr_expr_schema = json.load(fp)
|
|
22
|
+
|
|
23
|
+
def __validate_deeply(self, compat):
|
|
24
|
+
validations = 0
|
|
25
|
+
lt = LicompToolkit()
|
|
26
|
+
|
|
27
|
+
compat_check = compat['compatibility_check']
|
|
28
|
+
if compat_check == 'outbound-license -> inbound-license':
|
|
29
|
+
compat_object = compat['compatibility_object']
|
|
30
|
+
if not compat_object:
|
|
31
|
+
details = compat['compatibility_details']
|
|
32
|
+
else:
|
|
33
|
+
details = compat_object['compatibility_details']
|
|
34
|
+
compatibilities = details['compatibilities']
|
|
35
|
+
for compatibility_object in compatibilities:
|
|
36
|
+
logging.debug(f' {compatibility_object["resource_name"]}')
|
|
37
|
+
inner_validations = lt.validate(compatibility_object)
|
|
38
|
+
validations += 1
|
|
39
|
+
logging.debug('Validation OK')
|
|
40
|
+
return validations
|
|
41
|
+
else:
|
|
42
|
+
|
|
43
|
+
if compat_check == 'outbound-expression -> inbound-license' or compat_check == 'outbound-expression -> inbound-expression':
|
|
44
|
+
compat_object = compat
|
|
45
|
+
elif compat['compatibility_check'] == 'outbound-license -> inbound-expression':
|
|
46
|
+
compat_object = compat
|
|
47
|
+
else:
|
|
48
|
+
raise LicompException("Validation failed. Invalid state: " + compat_check)
|
|
49
|
+
|
|
50
|
+
for operand in compat_object['operands']:
|
|
51
|
+
operand_compat_object = operand['compatibility_object']
|
|
52
|
+
inner_validations = self.__validate_deeply(operand_compat_object)
|
|
53
|
+
validations += inner_validations
|
|
54
|
+
return validations
|
|
55
|
+
|
|
56
|
+
def validate(self, content, deep=False):
|
|
57
|
+
jsonschema.validate(instance=content,
|
|
58
|
+
schema=self.expr_expr_schema)
|
|
59
|
+
validations = 1
|
|
60
|
+
if deep:
|
|
61
|
+
report = content['compatibility_report']
|
|
62
|
+
validations = self.__validate_deeply(report)
|
|
63
|
+
return validations
|
|
64
|
+
|
|
65
|
+
def validate_file(self, filename, deep=False):
|
|
66
|
+
with open(filename) as fp:
|
|
67
|
+
return self.validate(json.load(fp), deep)
|