diff --git a/mc_openapi/__main__.py b/mc_openapi/__main__.py index 4d3ee38f3bff491c7e22204dba811f486db3e3b6..439bf1998070276470a45651a3486f3d113bdfd4 100644 --- a/mc_openapi/__main__.py +++ b/mc_openapi/__main__.py @@ -10,7 +10,7 @@ from doml_synthesis.types import State from mc_openapi.app_config import app from mc_openapi.doml_mc import DOMLVersion -from mc_openapi.doml_mc.csp_compatibility.cspcomp import check_csp_compatibility +from mc_openapi.doml_mc.csp_compatibility._iec_check import check_csp_compatibility from mc_openapi.doml_mc.domlr_parser.exceptions import RequirementException from mc_openapi.doml_mc.domlr_parser.parser import (DOMLRTransformer, Parser, SynthesisDOMLRTransformer) @@ -76,10 +76,9 @@ else: # Check CSP Compatibility if args.csp: - # from mc_openapi.doml_mc.csp_compatibility import CSPCompatibilityValidator - # cspc = CSPCompatibilityValidator - # cspc.check(dmc.intermediate_model, doml_ver) - check_csp_compatibility(dmc.intermediate_model, doml_ver) + from mc_openapi.doml_mc.csp_compatibility import CSPCompatibilityValidator as csp_comp + csp_comp.check(dmc.intermediate_model, doml_ver) + # check_csp_compatibility(dmc.intermediate_model, doml_ver) exit(0) # Store of Requirements and unique string constants diff --git a/mc_openapi/assets/csp/architectures.yml b/mc_openapi/assets/csp/arch.yml similarity index 100% rename from mc_openapi/assets/csp/architectures.yml rename to mc_openapi/assets/csp/arch.yml diff --git a/mc_openapi/assets/csp/keypair.yml b/mc_openapi/assets/csp/keypair.yml new file mode 100644 index 0000000000000000000000000000000000000000..151293ed41ddff6e51f5cee3fa8c9d2b698d868c --- /dev/null +++ b/mc_openapi/assets/csp/keypair.yml @@ -0,0 +1,16 @@ +# CSP Name +aws: + - RSA + - ED25519 + - ECDSA + + +azure: + - RSA + +gcp: + - Elliptic Curve + - RSA + +bad_csp: + - OLD_ALGO diff --git a/mc_openapi/assets/csp/keypairs.yml b/mc_openapi/assets/csp/keypairs.yml deleted file mode 100644 index c73365c06b8b1d4ba6302b83c05cd3e05eb71bad..0000000000000000000000000000000000000000 --- a/mc_openapi/assets/csp/keypairs.yml +++ /dev/null @@ -1,13 +0,0 @@ -# CSP Name -aws: - - algorithm: "RSA" - bits: "2048" - - algorithm: "ED25519" - -azure: - - algorithm: "RSA" - bits: "2048-*" - -bad_csp: - - algorithm: "OLD_ALGO" - bits: "*-1024" \ No newline at end of file diff --git a/mc_openapi/assets/csp/minimum_setup.yml b/mc_openapi/assets/csp/minimum_setup.yml new file mode 100644 index 0000000000000000000000000000000000000000..64e591c690ab151807cb54f5301241363159b632 --- /dev/null +++ b/mc_openapi/assets/csp/minimum_setup.yml @@ -0,0 +1,15 @@ +aws: + - infrastructure_ComputingNode::os # AMI + # - an Instance Type + - [infrastructure_ComputingNode::ifaces, infrastructure_NetworkInterface::associated] # NetworkInterface + - infrastructure_ComputingNode::credentials # KeyPair + +azure: + - infrastructure_ComputingNode::os # OS Image + - [infrastructure_ComputingNode::ifaces, infrastructure_NetworkInterface::belongsTo] # NetworkInterface + - infrastructure_ComputingNode::storage # Storage/VMSize + +gcp: + - infrastructure_ComputingNode::location + - infrastructure_ComputingNode::os # OS Image + - infrastructure_ComputingNode::architecture \ No newline at end of file diff --git a/mc_openapi/assets/csp/os.yml b/mc_openapi/assets/csp/os.yml new file mode 100644 index 0000000000000000000000000000000000000000..9a5060edfbe201a900bab603f44ba74a1f5a52bf --- /dev/null +++ b/mc_openapi/assets/csp/os.yml @@ -0,0 +1,41 @@ +aws: + Linux: + - Amazon Linux + - Amazon Linux 2 + - Bottlerocket + - CentOS + - Debian Server + - Oracle Linux + - Red Hat Enterprise Linux (RHEL) + - Rocky Linux + - SUSE Linux Enterprise Server (SLES) + - Ubuntu Server + macOS: + - macOS + Windows Server: + - Windows Server + +azure: + Linux: + - Red Hat Enterprise Linux + - CentOS + - CoreOS + - Debian + - Oracle Linux + - SUSE Linux Enterprise + - openSUSE + - Ubuntu + Windows Server: + - Windows Server + +gcp: + Linux: + - Amazon Linux + - Amazon Linux 2 + - CentOS + - Debian + - Red Hat Enterprise Linux (RHEL) + - SUSE Linux Enterprise + - Ubuntu + Windows Server: + - Windows Server \ No newline at end of file diff --git a/mc_openapi/assets/csp/regions.yml b/mc_openapi/assets/csp/region.yml similarity index 100% rename from mc_openapi/assets/csp/regions.yml rename to mc_openapi/assets/csp/region.yml diff --git a/mc_openapi/doml_mc/csp_compatibility/__init__.py b/mc_openapi/doml_mc/csp_compatibility/__init__.py index 6040fe698e0a93bc9bbc38dee9ab62483a81b86c..27620127f693445ba8ab6f9846b1f25d7a511d14 100644 --- a/mc_openapi/doml_mc/csp_compatibility/__init__.py +++ b/mc_openapi/doml_mc/csp_compatibility/__init__.py @@ -1,22 +1,22 @@ -from .validator import CSPCompatibilityValidator +from .allowlist_check_v1 import CSPCompatibilityValidator import importlib.resources as ilres from ... import assets import yaml FILE = lambda filename: ilres.files(assets).joinpath(f"csp/{filename}") -with open(FILE('keypairs.yml')) as kp: - KEYPAIRS = yaml.safe_load(kp) +SOURCES = [ + 'keypair', + 'arch', + 'os', + 'minimum_setup' +] -with open(FILE('architectures.yml')) as kp: - ARCHS = yaml.safe_load(kp) +DATA = {} -with open(FILE('regions.yml')) as kp: - REGIONS = yaml.safe_load(kp) +for src in SOURCES: + with open(FILE(f'{src}.yml')) as data: + DATA[src] = yaml.safe_load(data) -CSPCompatibilityValidator = CSPCompatibilityValidator( - keypairs=KEYPAIRS, - architectures=ARCHS, - regions=REGIONS -) +CSPCompatibilityValidator = CSPCompatibilityValidator(DATA) diff --git a/mc_openapi/doml_mc/csp_compatibility/cspcomp.py b/mc_openapi/doml_mc/csp_compatibility/_iec_check.py similarity index 97% rename from mc_openapi/doml_mc/csp_compatibility/cspcomp.py rename to mc_openapi/doml_mc/csp_compatibility/_iec_check.py index 690f7db410f9724c461db6c9c6aec9f4b67fde73..809c26b31ba10e39e0d125c62b817bf8068ae343 100644 --- a/mc_openapi/doml_mc/csp_compatibility/cspcomp.py +++ b/mc_openapi/doml_mc/csp_compatibility/_iec_check.py @@ -4,6 +4,9 @@ import requests from mc_openapi.doml_mc.imc import IntermediateModelChecker from mc_openapi.doml_mc.intermediate_model.metamodel import DOMLVersion +# DEPRECATED +# Doing this would overlap with IOP tool, therefore checks will be performed using the other tool + IEC_API = 'https://iec.ci.piacere.digital.tecnalia.dev/services/iecbackend/api/root-services/catalogue' diff --git a/mc_openapi/doml_mc/csp_compatibility/allowlist_check_v1.py b/mc_openapi/doml_mc/csp_compatibility/allowlist_check_v1.py new file mode 100644 index 0000000000000000000000000000000000000000..7f641c04e64b7518dadda730ec83325935e4c4ae --- /dev/null +++ b/mc_openapi/doml_mc/csp_compatibility/allowlist_check_v1.py @@ -0,0 +1,124 @@ + + +from mc_openapi.doml_mc.intermediate_model.doml_element import DOMLElement, IntermediateModel +from mc_openapi.doml_mc.intermediate_model.metamodel import DOMLVersion +from difflib import SequenceMatcher +from tabulate import tabulate + +import re + +class CSPCompatibilityValidator: + def __init__(self, data: dict) -> None: + self.data = data + + def check(self, model: IntermediateModel, doml_version: DOMLVersion) -> list[str]: + """Returns a list of CSP supported by the model""" + + # Check KeyPair + keypairs = self.check_keypair(model) + if len(keypairs) > 1: + print(tabulate(keypairs, headers='firstrow', tablefmt='fancy_grid')) + + # ComputingNode and inheritors + arch, os, minreq = self.check_computing_nodes(model) + if len(arch) > 1: + print(tabulate(arch, headers='firstrow', tablefmt='fancy_grid')) + if len(os) > 1: + print(tabulate(os, headers='firstrow', tablefmt='fancy_grid')) + if len(minreq) > 1: + print(tabulate(minreq, headers='firstrow', tablefmt='fancy_grid')) + + def check_keypair(self, model: IntermediateModel): + elems = model.values() + keypairs = [kp for kp in elems if kp.class_ == 'commons_KeyPair'] + TABLE = [['KeyPair', 'Algorithm', *self.data['keypair'].keys()]] + for kp in keypairs: + name = kp.user_friendly_name + algorithm = kp.attributes.get('commons_KeyPair::algorithm') + bits = kp.attributes.get('commons_KeyPair::bits') + # For each vendor, check if there's at least one supported configuration + if algorithm is not None and len(algorithm) > 0: + algorithm = algorithm[0].upper() + ROW = [name, algorithm] + for _, valid_algos in self.data['keypair'].items(): + value = '✅' if algorithm in valid_algos else '❌' + ROW.append(value) + TABLE.append(ROW) + return TABLE + + def check_computing_nodes(self, model: IntermediateModel): + elems = model.values() + compnodes = [cn for cn in elems if re.match(r"infrastructure_(ComputingNode|Container|PhysicalComputingNode|VirtualMachine)", cn.class_)] + ARCH_TABLE = [['Node', 'Arch', *self.data['arch'].keys()]] + OS_TABLE = [['Node', 'OS', *self.data['os'].keys()]] + MINREQ_TABLE = [['Node', *self.data['minimum_setup'].keys()]] + + VALID_OS_MATCH_PERCENT = 0.33 + + for el in compnodes: + name = el.user_friendly_name or el.attributes.get('commons_DOMLElement::name') or f'[{el.class_}]' + arch = el.attributes.get('infrastructure_ComputingNode::architecture') + os = el.attributes.get('infrastructure_ComputingNode::os') + # cpu_count = el.attributes.get('infrastructure_ComputingNode::cpu_count') + # memory_mb = el.attributes.get('infrastructure_ComputingNode::memory_mb') + + # CHECK ARCH + if arch and len(arch) > 0: + arch = arch[0].upper() + ROW = [name, arch] + for _, valid_archs in self.data['arch'].items(): + valid_archs = [v.upper() for v in valid_archs] + value = '✅' if arch in valid_archs else '❌' + ROW.append(value) + ARCH_TABLE.append(ROW) + + # CHECK OS + if os and len(os) > 0: + os = os[0].upper() + ROW = [name, os] + for _, valid_os in self.data['os'].items(): + valid_os = [distro.upper() for os_family in valid_os for distro in valid_os[os_family]] + compatible_os = [v for v in valid_os if v in os or os in v] + alt_compatible_os = [(v, SequenceMatcher(None, v, os).ratio()) for v in valid_os if SequenceMatcher(None, v, os).ratio() > VALID_OS_MATCH_PERCENT] + alt_compatible_os = sorted(alt_compatible_os, key=lambda x: x[1]) + value = any(compatible_os) + value = '✅' if value else '❌' if len(alt_compatible_os) == 0 else '❓' + if len(compatible_os) > 0: + value += f' ({compatible_os[0].lower()})' + if value == '❓': + alt_os_name, alt_os_ratio = alt_compatible_os[0] + value += f' ({alt_os_name.lower()}, {alt_os_ratio*100:.{1}f}%)' + ROW.append(value) + OS_TABLE.append(ROW) + + # CHECK MINIMUM REQUIREMENTS FOR CSP + ROW = [name] + for vendor, reqs in self.data['minimum_setup'].items(): + CHECKS = {} + for req in reqs: + if isinstance(req, list): + try: + dep = list(el.associations.get(req[0]))[0] + dep = model[dep] + CHECKS[str(req)] = dep.associations.get(req[1]) is not None or el.attributes.get(req[1]) is not None + except: + CHECKS[str(req)] = False + if isinstance(req, str): + CHECKS[req] = el.associations.get(req) is not None or el.attributes.get(req) is not None + + all_req_valid = all([v for v in CHECKS.values()]) + value = '✅' if all_req_valid else '❌' + if not all_req_valid: + value += " Missing:\n" + "\n".join([ + k.replace("_", ".", 1).replace("::", ".") + for k, v in CHECKS.items() + if v is False + ]) + ROW.append(value) + MINREQ_TABLE.append(ROW) + + return ARCH_TABLE, OS_TABLE, MINREQ_TABLE + + + + diff --git a/mc_openapi/doml_mc/csp_compatibility/validator.py b/mc_openapi/doml_mc/csp_compatibility/validator.py deleted file mode 100644 index 584e610f4b58f053361634e6f5e69d356a25021b..0000000000000000000000000000000000000000 --- a/mc_openapi/doml_mc/csp_compatibility/validator.py +++ /dev/null @@ -1,95 +0,0 @@ - - -from mc_openapi.doml_mc.intermediate_model.doml_element import DOMLElement, IntermediateModel -from mc_openapi.doml_mc.intermediate_model.metamodel import DOMLVersion - -import re - -class CSPCompatibilityValidator: - def __init__(self, keypairs, architectures, regions) -> None: - self.valid_keypairs = keypairs - self.valid_architectures = architectures - self.valid_regions = regions - pass - - def check(self, model: IntermediateModel, doml_version: DOMLVersion) -> list[str]: - """Returns a list of CSP supported by the model""" - if doml_version == DOMLVersion.V2_2: - elems = model.values() - - print('=====================================') - print('========= CSP Compatibility =========') - print('=====================================') - - # Check KeyPair - keypairs = [kp for kp in elems if kp.class_ == 'commons_KeyPair'] - print("\n--- Keypairs ---") - self.check_keypair(keypairs) - - # ComputingNode and inheritors - print("\n--- Architectures ---") - compnodes = [cn for cn in elems if re.match(r"infrastructure_(ComputingNode|Container|PhysicalComputingNode|VirtualMachine)", cn.class_)] - self.check_computing_nodes(compnodes) - - # Locations - print("\n--- Locations ---") - locations = [lc for lc in elems if lc.class_ == 'infrastructure_Location'] - self.check_locations(locations) - else: - raise "Unsupported DOML version (<2.2) for CSP Compatibility check" - - def check_keypair(self, keypairs: list[DOMLElement]): - for kp in keypairs: - algorithm = kp.attributes.get('commons_KeyPair::algorithm') - bits = kp.attributes.get('commons_KeyPair::bits') - # For each vendor, check if there's at least one supported configuration - for vendor, vkps in self.valid_keypairs.items(): - valid_algorithms = [] - valid_bits = [] - - if algorithm: - valid_algorithms = [vkp.get('algorithm').lower() == algorithm[0].lower() for vkp in vkps] - if bits: - def comp_bits(b, bits): - if b is None: - return True - b = b.split('-') - if len(b) > 1: - lb = b[0] - ub = b[1] - return (int(lb) <= bits if lb != '*' else True) and (bits <= int(ub) if ub != '*' else True) - else: - return b[0] == bits - valid_bits = [comp_bits(vkp.get('bits'), bits[0]) for vkp in vkps] - - if not any(valid_algorithms): - print(f"{kp.user_friendly_name} 'algorithm' not compatible with: {vendor}") - if not any(valid_bits): - print(f"{kp.user_friendly_name} 'bits' not compatible with: {vendor}") - - def check_computing_nodes(self, elems: list[DOMLElement]): - for el in elems: - arch = el.attributes.get('infrastructure_ComputingNode::architecture') - # os = el.attributes.get('infrastructure_ComputingNode::os') - # cpu_count = el.attributes.get('infrastructure_ComputingNode::cpu_count') - # memory_mb = el.attributes.get('infrastructure_ComputingNode::memory_mb') - - if arch: - for vendor, varchs in self.valid_architectures.items(): - valid_archs = [arch[0] == varch for varch in varchs] - if not any(valid_archs): - print(f"{el.user_friendly_name} 'architecture' not compatible with: {vendor}") - - # OS requires probably a fine-handling of <os>, <flavor/version> - - def check_locations(self, locations: list[DOMLElement]): - for loc in locations: - reg = loc.attributes.get('infrastructure_Location::region') - - if reg: - for vendor, vregs in self.valid_regions.items(): - valid_regs = [reg[0] == vreg for vreg in vregs] - if not any(valid_regs): - print(f"{loc.user_friendly_name or 'A'} 'region' (value: {reg[0]}) not compatible with: {vendor}") - - diff --git a/mc_openapi/doml_mc/domlr_parser/exceptions.py b/mc_openapi/doml_mc/domlr_parser/exceptions.py index a37ed1f8d1f9c41ebe72a6d86359e16aa18eef0d..630ce72e3c0ec7285c42d5bbc255b1d65fd42d26 100644 --- a/mc_openapi/doml_mc/domlr_parser/exceptions.py +++ b/mc_openapi/doml_mc/domlr_parser/exceptions.py @@ -6,7 +6,7 @@ class RequirementException(Exception): class RequirementMissingKeyException(RequirementException): def __init__(self, key_type: str, key: str, close_matches: list[str], *args: object) -> None: super().__init__(*args) - fix_syntax = lambda x: x.replace("_", ".", 1).replace("::", "->") + fix_syntax = lambda x: x.replace("_", ".", 1).replace("::", ".") key = fix_syntax(key) close_matches = list(map(fix_syntax, close_matches)) self.message = f"Error: no {key_type} found named '{key}'.\n"