diff --git a/mc_openapi/__main__.py b/mc_openapi/__main__.py index 65143702bc94856cc0817a2aa5789d879cc1af34..1bfaad2db8f2e7509f1d72f49a70cdf0b2a09e45 100644 --- a/mc_openapi/__main__.py +++ b/mc_openapi/__main__.py @@ -29,7 +29,9 @@ parser.add_argument("-v", "--verbose", dest="verbose", action='store_true', help # Model Checker parser.add_argument("-c", "--check-consistency", dest="consistency", action='store_true', help="check on additional built-in consistency requirements") parser.add_argument("-S", "--skip-common-checks", dest="skip_common", action='store_true', help="skip check on common built-in requirements") +parser.add_argument("-C", "--csp", dest="csp", action='store_true', help="check compatibility with supported CSPs") parser.add_argument("-t", "--threads", dest="threads", type=int, default=2, help="number of threads used by the model checker") + # Synthesis parser.add_argument("-s", "--synth", dest="synth", action='store_true', help="synthetize a new DOMLX file from requirements") parser.add_argument("-m", "--max-tries", dest="tries", type=int, default=8, help="max number of iteration while trying to solve the model with unbounded variables") @@ -71,6 +73,13 @@ else: dmc = ModelChecker(doml_xmi, doml_ver) doml_ver = dmc.doml_version + # Check CSP Compatibility + if args.csp: + from mc_openapi.doml_mc.csp_compatibility import CSPCompatibilityValidator + cspc = CSPCompatibilityValidator + cspc.check(dmc.intermediate_model, doml_ver) + exit(0) + # Store of Requirements and unique string constants user_req_store = RequirementStore() user_req_str_consts = [] diff --git a/mc_openapi/assets/csp/architectures.yml b/mc_openapi/assets/csp/architectures.yml new file mode 100644 index 0000000000000000000000000000000000000000..41ae12f44a1c13755477aa1dddc50acf36f64c57 --- /dev/null +++ b/mc_openapi/assets/csp/architectures.yml @@ -0,0 +1,16 @@ +# CSP Name +aws: + - x86 + - x86_64 + - ARM64 + +gcp: + - x86 + - ARM + +azure: + - x86 + - ARM + +bad_csp: + - PowerPC diff --git a/mc_openapi/assets/csp/keypairs.yml b/mc_openapi/assets/csp/keypairs.yml new file mode 100644 index 0000000000000000000000000000000000000000..c73365c06b8b1d4ba6302b83c05cd3e05eb71bad --- /dev/null +++ b/mc_openapi/assets/csp/keypairs.yml @@ -0,0 +1,13 @@ +# 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/regions.yml b/mc_openapi/assets/csp/regions.yml new file mode 100644 index 0000000000000000000000000000000000000000..8b9e1d62578265a61dd0eb2130c04bd6d5548b54 --- /dev/null +++ b/mc_openapi/assets/csp/regions.yml @@ -0,0 +1,174 @@ +aws: + - us-east-2 + - us-east-1 + - us-west-1 + - us-west-2 + - af-south-1 + - ap-east-1 + - ap-south-2 + - ap-southeast-3 + - ap-southeast-4 + - ap-south-1 + - ap-northeast-3 + - ap-northeast-2 + - ap-southeast-1 + - ap-southeast-2 + - ap-northeast-1 + - ca-central-1 + - eu-central-1 + - eu-west-1 + - eu-west-2 + - eu-south-1 + - eu-west-3 + - eu-south-2 + - eu-north-1 + - eu-central-2 + - me-south-1 + - me-central-1 + - sa-east-1 + - us-gov-east-1 + - us-gov-west-1 + +gcp: + - asia-east1-a + - asia-east1-b + - asia-east1-c + - asia-east2-a + - asia-east2-b + - asia-east2-c + - asia-northeast1-a + - asia-northeast1-b + - asia-northeast1-c + - asia-northeast2-a + - asia-northeast2-b + - asia-northeast2-c + - asia-northeast3-a + - asia-northeast3-b + - asia-northeast3-c + - asia-south1-a + - asia-south1-b + - asia-south1-c + - asia-south2-a + - asia-south2-b + - asia-south2-c + - asia-southeast1-a + - asia-southeast1-b + - asia-southeast1-c + - asia-southeast2-a + - asia-southeast2-b + - asia-southeast2-c + - australia-southeast1-a + - australia-southeast1-b + - australia-southeast1-c + - australia-southeast2-a + - australia-southeast2-b + - australia-southeast2-c + - europe-central2-a + - europe-central2-b + - europe-central2-c + - europe-north1-a + - europe-north1-b + - europe-north1-c + - europe-southwest1-a + - europe-southwest1-b + - europe-southwest1-c + - europe-west1-b + - europe-west1-c + - europe-west1-d + - europe-west2-a + - europe-west2-b + - europe-west2-c + - europe-west3-a + - europe-west3-b + - europe-west3-c + - europe-west4-a + - europe-west4-b + - europe-west4-c + - europe-west6-a + - europe-west6-b + - europe-west6-c + - europe-west8-a + - europe-west8-b + - europe-west8-c + - europe-west9-a + - europe-west9-b + - europe-west9-c + - me-west1-a + - me-west1-b + - me-west1-c + - northamerica-northeast1-a + - northamerica-northeast1-b + - northamerica-northeast1-c + - northamerica-northeast2-a + - northamerica-northeast2-b + - northamerica-northeast2-c + - southamerica-east1-a + - southamerica-east1-b + - southamerica-east1-c + - southamerica-west1-a + - southamerica-west1-b + - southamerica-west1-c + - us-central1-a + - us-central1-b + - us-central1-c + - us-central1-f + - us-east1-b + - us-east1-c + - us-east1-d + - us-east4-a + - us-east4-b + - us-east4-c + - us-east5-a + - us-east5-b + - us-east5-c + - us-south1-a + - us-south1-b + - us-south1-c + - us-west1-a + - us-west1-b + - us-west1-c + - us-west2-a + - us-west2-b + - us-west2-c + - us-west3-a + - us-west3-b + - us-west3-c + - us-west4-a + - us-west4-b + - us-west4-c + +azure: + - eastasia + - southeastasia + - centralus + - eastus + - eastus2 + - westus + - northcentralus + - southcentralus + - northeurope + - westeurope + - japanwest + - japaneast + - brazilsouth + - australiaeast + - australiasoutheast + - southindia + - centralindia + - westindia + - canadacentral + - canadaeast + - uksouth + - ukwest + - westcentralus + - westus2 + - koreacentral + - koreasouth + - francecentral + - francesouth + - australiacentral + - australiacentral2 + - uaecentral + - uaenorth + - southafricanorth + - southafricawest \ No newline at end of file diff --git a/mc_openapi/doml_mc/csp_compatibility/__init__.py b/mc_openapi/doml_mc/csp_compatibility/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..6040fe698e0a93bc9bbc38dee9ab62483a81b86c --- /dev/null +++ b/mc_openapi/doml_mc/csp_compatibility/__init__.py @@ -0,0 +1,22 @@ +from .validator 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) + +with open(FILE('architectures.yml')) as kp: + ARCHS = yaml.safe_load(kp) + +with open(FILE('regions.yml')) as kp: + REGIONS = yaml.safe_load(kp) + +CSPCompatibilityValidator = CSPCompatibilityValidator( + keypairs=KEYPAIRS, + architectures=ARCHS, + regions=REGIONS +) + diff --git a/mc_openapi/doml_mc/csp_compatibility/validator.py b/mc_openapi/doml_mc/csp_compatibility/validator.py new file mode 100644 index 0000000000000000000000000000000000000000..584e610f4b58f053361634e6f5e69d356a25021b --- /dev/null +++ b/mc_openapi/doml_mc/csp_compatibility/validator.py @@ -0,0 +1,95 @@ + + +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/tests/doml/v2.2/nginx-csp-compatibility-test.doml b/tests/doml/v2.2/nginx-csp-compatibility-test.doml new file mode 100644 index 0000000000000000000000000000000000000000..5b6d95639a02c7114a1f245150105b38d21579a2 --- /dev/null +++ b/tests/doml/v2.2/nginx-csp-compatibility-test.doml @@ -0,0 +1,132 @@ +doml nginx_aws_ec2 + +application app { + + software_component nginx { + properties { + source_code="/usr/share/nginx/html/index.html"; + } + } +} + +infrastructure infra { + + vm_image vm_img { + generates vm1 + image "ami-xxxxxxxxxxxxxxxxx" + } + + net vpc { + cidr "/24" + protocol "tcp/ip" + subnet vpc_subnet { + cidr "/24" + protocol "tcp/ip" + } + } + + security_group sg { + egress icmp { + from_port -1 + to_port -1 + protocol "icmp" + cidr ["0.0.0.0/0"] + } + ingress http { + from_port 80 + to_port 80 + protocol "tcp" + cidr ["0.0.0.0/0"] + } + ingress https { + from_port 443 + to_port 443 + protocol "tcp" + cidr ["0.0.0.0/0"] + } + ingress ssh { + from_port 22 + to_port 22 + protocol "tcp" + cidr ["0.0.0.0/0"] + } + ifaces i1 + } + + key_pair ssh_key { + user "ec2-user" + keyfile "/tmp/ssh_key_file" + algorithm "RSA" + bits 4096 + } + + key_pair ed_key { + user "ec2-user" + keyfile "/tmp/ED25519_key_file" + algorithm "ED25519" + } + + autoscale_group ag { + vm vm1 { + arch "x86" + os "RHEL" + cpu_count 128 + mem_mb 512000.0 + + iface i1 { + address "10.0.0.1" + belongs_to vpc + security sg + } + credentials ssh_key + loc { + region "eu-central-1" + } + } + } +} + +deployment conf { + nginx -> vm1 +} + +active deployment conf + +concretizations { + concrete_infrastructure con_infra { + provider aws { + vm ec2_vm { + properties { + vm_name = "nginx-host"; + instance_type = "t2.micro"; + ssh_key_name = "demo-key"; + ec2_role_name = "demo-ec2-role"; + } + maps vm1 + } + + vm_image concrete_vm_image { + maps vm_img + } + + net concrete_net { + properties { + vm_name = "nginx-host"; + } + maps vpc + } + } + } + active con_infra +} + +optimization opt { + objectives { + "cost" => min + "availability" => max + } + nonfunctional_requirements { + req1 "Cost <= 70.0" max 70.0 => "cost"; + req2 "Availability >= 66.5%" min 66.5 => "availability"; + } +} diff --git a/tests/doml/v2.2/nginx-csp-compatibility-test.domlx b/tests/doml/v2.2/nginx-csp-compatibility-test.domlx new file mode 100644 index 0000000000000000000000000000000000000000..a53d27a95dba6e5220fe26fe71d8f72fbe4f5374 --- /dev/null +++ b/tests/doml/v2.2/nginx-csp-compatibility-test.domlx @@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="ASCII"?> +<commons:DOMLModel xmi:version="2.0" xmlns:xmi="http://www.omg.org/XMI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:app="http://www.piacere-project.eu/doml/application" xmlns:commons="http://www.piacere-project.eu/doml/commons" xmlns:infra="http://www.piacere-project.eu/doml/infrastructure" xmlns:optimization="http://www.piacere-project.eu/doml/optimization" name="nginx_aws_ec2" activeConfiguration="//@configurations.0" activeInfrastructure="//@concretizations.0"> + <application name="app"> + <components xsi:type="app:SoftwareComponent" name="nginx"> + <annotations xsi:type="commons:SProperty" key="source_code" value="/usr/share/nginx/html/index.html"/> + </components> + </application> + <infrastructure name="infra"> + <generators xsi:type="infra:VMImage" name="vm_img" uri="ami-xxxxxxxxxxxxxxxxx" kind="IMAGE" generatedVMs="//@infrastructure/@groups.0/@machineDefinition"/> + <credentials xsi:type="commons:KeyPair" name="ssh_key" user="ec2-user" keyfile="/tmp/ssh_key_file" algorithm="RSA" bits="4096"/> + <credentials xsi:type="commons:KeyPair" name="ed_key" user="ec2-user" keyfile="/tmp/ED25519_key_file" algorithm="ED25519"/> + <groups xsi:type="infra:AutoScalingGroup" name="ag"> + <machineDefinition name="vm1" architecture="x86" os="RHEL" memory_mb="512000.0" cpu_count="128" credentials="//@infrastructure/@credentials.0" generatedFrom="//@infrastructure/@generators.0"> + <ifaces name="i1" endPoint="10.0.0.1" belongsTo="//@infrastructure/@networks.0" associated="//@infrastructure/@securityGroups.0"/> + <location region="eu-central-1"/> + </machineDefinition> + </groups> + <securityGroups name="sg" ifaces="//@infrastructure/@groups.0/@machineDefinition/@ifaces.0"> + <rules name="icmp" protocol="icmp" fromPort="-1" toPort="-1"> + <cidr>0.0.0.0/0</cidr> + </rules> + <rules name="http" kind="INGRESS" protocol="tcp" fromPort="80" toPort="80"> + <cidr>0.0.0.0/0</cidr> + </rules> + <rules name="https" kind="INGRESS" protocol="tcp" fromPort="443" toPort="443"> + <cidr>0.0.0.0/0</cidr> + </rules> + <rules name="ssh" kind="INGRESS" protocol="tcp" fromPort="22" toPort="22"> + <cidr>0.0.0.0/0</cidr> + </rules> + </securityGroups> + <networks name="vpc" protocol="tcp/ip" addressRange="/24" connectedIfaces="//@infrastructure/@groups.0/@machineDefinition/@ifaces.0"> + <subnets name="vpc_subnet" protocol="tcp/ip" addressRange="/24"/> + </networks> + </infrastructure> + <concretizations name="con_infra"> + <providers name="aws"> + <vms name="ec2_vm" maps="//@infrastructure/@groups.0/@machineDefinition"> + <annotations xsi:type="commons:SProperty" key="vm_name" value="nginx-host"/> + <annotations xsi:type="commons:SProperty" key="instance_type" value="t2.micro"/> + <annotations xsi:type="commons:SProperty" key="ssh_key_name" value="demo-key"/> + <annotations xsi:type="commons:SProperty" key="ec2_role_name" value="demo-ec2-role"/> + </vms> + <vmImages name="concrete_vm_image" maps="//@infrastructure/@generators.0"/> + <networks name="concrete_net" maps="//@infrastructure/@networks.0"> + <annotations xsi:type="commons:SProperty" key="vm_name" value="nginx-host"/> + </networks> + </providers> + </concretizations> + <optimization name="opt"> + <objectives xsi:type="optimization:MeasurableObjective" kind="min" property="cost"/> + <objectives xsi:type="optimization:MeasurableObjective" kind="max" property="availability"/> + <nonfunctionalRequirements xsi:type="commons:RangedRequirement" name="req1" description="Cost <= 70.0" property="cost" max="70.0"/> + <nonfunctionalRequirements xsi:type="commons:RangedRequirement" name="req2" description="Availability >= 66.5%" property="availability" min="66.5"/> + </optimization> + <configurations name="conf"> + <deployments component="//@application/@components.0" node="//@infrastructure/@groups.0/@machineDefinition"/> + </configurations> +</commons:DOMLModel>