diff --git a/mc_openapi/__main__.py b/mc_openapi/__main__.py index 4d7e3e897dd7ab7dd99fa3f5025f5a297228af98..3d4bf9315506f0bdf811effcb280862e419da1a3 100644 --- a/mc_openapi/__main__.py +++ b/mc_openapi/__main__.py @@ -2,6 +2,9 @@ import argparse import logging from logging.config import dictConfig +import re + +from tabulate import tabulate from mc_openapi.app_config import app from mc_openapi.doml_mc import DOMLVersion, init_model, verify_csp_compatibility, verify_model, synthesize_model @@ -88,8 +91,16 @@ else: # Check CSP Compatibility if args.csp: - verify_csp_compatibility(dmc) - # TODO: Do something with the results + csp = verify_csp_compatibility(dmc) + for csp_k, csp_v in csp.items(): + # Format items in minreq + if csp_k == 'minreq': + for row in csp_v: + for index, col in enumerate(row): + if index > 0 and isinstance(col, list): + row[index] = "\n".join(col) + + print(tabulate(csp_v, headers='firstrow', tablefmt='fancy_grid')) else: result, msg = verify_model(dmc, domlr_src, args.threads, args.consistency, args.skip_builtin) diff --git a/mc_openapi/app_config.py b/mc_openapi/app_config.py index f6234dd5585799ceb66b993316301db9b8ebc97f..869bdba07f84d0bffa6bb94762306111e6b425da 100644 --- a/mc_openapi/app_config.py +++ b/mc_openapi/app_config.py @@ -1,8 +1,12 @@ import connexion - -from logging.config import dictConfig +from flask import render_template app = connexion.App(__name__, specification_dir='openapi/') app.add_api('model_checker.yaml') +@app.route("/") +def home(): + return render_template('home.html') + + application = app.app diff --git a/mc_openapi/doml_mc/csp_compatibility/allowlist_check_v1.py b/mc_openapi/doml_mc/csp_compatibility/allowlist_check_v1.py index 7f641c04e64b7518dadda730ec83325935e4c4ae..3c8b0c4aadfd7f7ad42b2daba1160d18ab849fe1 100644 --- a/mc_openapi/doml_mc/csp_compatibility/allowlist_check_v1.py +++ b/mc_openapi/doml_mc/csp_compatibility/allowlist_check_v1.py @@ -3,7 +3,6 @@ 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 @@ -11,22 +10,25 @@ class CSPCompatibilityValidator: def __init__(self, data: dict) -> None: self.data = data - def check(self, model: IntermediateModel, doml_version: DOMLVersion) -> list[str]: + def check(self, model: IntermediateModel, doml_version: DOMLVersion) -> dict[str, list]: """Returns a list of CSP supported by the model""" + ret = {} + # Check KeyPair keypairs = self.check_keypair(model) if len(keypairs) > 1: - print(tabulate(keypairs, headers='firstrow', tablefmt='fancy_grid')) - + ret['keypairs'] = keypairs # ComputingNode and inheritors arch, os, minreq = self.check_computing_nodes(model) if len(arch) > 1: - print(tabulate(arch, headers='firstrow', tablefmt='fancy_grid')) + ret['arch'] = arch if len(os) > 1: - print(tabulate(os, headers='firstrow', tablefmt='fancy_grid')) + ret['os'] = os if len(minreq) > 1: - print(tabulate(minreq, headers='firstrow', tablefmt='fancy_grid')) + ret['minreq'] = minreq + + return ret def check_keypair(self, model: IntermediateModel): elems = model.values() @@ -107,13 +109,18 @@ class CSPCompatibilityValidator: 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 '❌' + value = ['✅' if all_req_valid else '❌'] if not all_req_valid: - value += " Missing:\n" + "\n".join([ - k.replace("_", ".", 1).replace("::", ".") + value += [ + re.sub(r"\s*,\s*", " -> ", k) + .replace("_", ".", 1) + .replace("::", ".") + .replace("[", "") + .replace("]", "") + .replace("'", "") for k, v in CHECKS.items() if v is False - ]) + ] ROW.append(value) MINREQ_TABLE.append(ROW) diff --git a/mc_openapi/doml_mc/main.py b/mc_openapi/doml_mc/main.py index 189bb72a7eb7a000736a534b2f934cd6ad818990..ac542d106cde4ed108defe1fa555027c2a15558f 100644 --- a/mc_openapi/doml_mc/main.py +++ b/mc_openapi/doml_mc/main.py @@ -8,7 +8,7 @@ from mc_openapi.doml_mc import ModelChecker from mc_openapi.doml_mc.common_reqs import CommonRequirements from mc_openapi.doml_mc.consistency_reqs import get_association_multiplicity_reqs, get_association_type_reqs, get_attribute_multiplicity_reqs, get_attribute_type_reqs, get_inverse_association_reqs from mc_openapi.doml_mc.csp_compatibility import \ - CSPCompatibilityValidator as csp_comp + CSPCompatibilityValidator as CSPCompatibility from mc_openapi.doml_mc.domlr_parser import (DOMLRTransformer, Parser, SynthesisDOMLRTransformer) from mc_openapi.doml_mc.imc import RequirementStore @@ -155,6 +155,5 @@ def synthesize_model(dmc: ModelChecker, external_domlr: str, max_tries: int): def verify_csp_compatibility(dmc: ModelChecker): - csp_comp.check(dmc.intermediate_model, dmc.doml_version) - # TODO: Refactor CSP to output a datastructure/table to print via CLI or REST - exit(0) \ No newline at end of file + return CSPCompatibility.check(dmc.intermediate_model, dmc.doml_version) + \ No newline at end of file diff --git a/mc_openapi/handlers.py b/mc_openapi/handlers.py index 488b6a87fafbd59977e318003ec170af58702256..5308e80b2eaf2221b51d226647985448918d31af 100644 --- a/mc_openapi/handlers.py +++ b/mc_openapi/handlers.py @@ -1,6 +1,8 @@ import datetime import logging import os + +from flask import render_template from mc_openapi.doml_mc.html_output import html_template from mc_openapi.doml_mc.intermediate_model.metamodel import DOMLVersion @@ -71,7 +73,12 @@ def csp(body, version=None): dmc = init_model(doml_xmi, doml_version) # TODO: Do something with the results - verify_csp_compatibility(dmc) + return verify_csp_compatibility(dmc) + except Exception as e: return make_error("The supplied DOMLX model is malformed or its DOML version is unsupported.", debug_msg=str(e)), 400 + +def csp_html(body, version=None): + ret = csp(body, version) + return render_template('csp.html.jinja', **ret).replace('\n', '') diff --git a/mc_openapi/openapi/model_checker.yaml b/mc_openapi/openapi/model_checker.yaml index 1cb4f6e761567db33bcd8f01ed62d7025427668c..3a20209a933620f3a45da32c76595d088a1f4f4b 100644 --- a/mc_openapi/openapi/model_checker.yaml +++ b/mc_openapi/openapi/model_checker.yaml @@ -113,6 +113,58 @@ paths: schema: type: string required: true + parameters: + - in: query + name: version + schema: + type: string + description: > + The DOML version you're currently using, written as e.g. `2.0` or `V2_0`. + If not specified, it will try to infer it by trying to parse it for each supported + DOML version, starting with the most recent. + required: false + responses: + "200": + content: + application/json: + schema: + type: object + description: OK - CSP compatibility successfully completed. + "400": + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: malformed request + "500": + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: internal error + /csp_html: + post: + summary: Verify compatibility of a DOML model with a set of Cloud Service Providers. + description: Send a DOML model in XMI format and a requirement to check. + The response says whether the model can be deployed on the supported CSPs. + It produces a compatibility matrix and lists required properties for each CSP. + operationId: mc_openapi.handlers.csp_html + requestBody: + content: + application/xml: + schema: + type: string + required: true + parameters: + - in: query + name: version + schema: + type: string + description: > + The DOML version you're currently using, written as e.g. `2.0` or `V2_0`. + If not specified, it will try to infer it by trying to parse it for each supported + DOML version, starting with the most recent. + required: false responses: "200": content: diff --git a/mc_openapi/templates/csp.html.jinja b/mc_openapi/templates/csp.html.jinja new file mode 100644 index 0000000000000000000000000000000000000000..d4bb921294a7e03f5d90a74b2a2e749b9004991f --- /dev/null +++ b/mc_openapi/templates/csp.html.jinja @@ -0,0 +1,93 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>CSP Compatibility Report</title> + <meta charset="utf8"> + <style type="text/css"> + {% include "style.css" %} + </style> +</head> +<body> + <h1>CSP Compatibility Report</h1> + + {% if keypairs %} + <h2>Keypairs</h2> + <table> + {% for row in keypairs %} + {% set outer_loop = loop %} + <tr> + {% for column in row %} + {% if outer_loop.first or loop.first %} + <th>{{ column }}</th> + {% else %} + <td>{{ column }}</td> + {% endif %} + {% endfor %} + </tr> + {% endfor %} + </table> + {% endif %} + + {% if os %} + <h2>OS</h2> + <table> + {% for row in os %} + {% set outer_loop = loop %} + <tr> + {% for column in row %} + {% if outer_loop.first or loop.first %} + <th>{{ column }}</th> + {% else %} + <td>{{ column }}</td> + {% endif %} + {% endfor %} + </tr> + {% endfor %} + </table> + {% endif %} + + {% if arch %} + <h2>Architectures</h2> + <table> + {% for row in arch %} + {% set outer_loop = loop %} + <tr> + {% for column in row %} + {% if outer_loop.first or loop.first %} + <th>{{ column }}</th> + {% else %} + <td>{{ column }}</td> + {% endif %} + {% endfor %} + </tr> + {% endfor %} + </table> + {% endif %} + + {% if minreq %} + <h2>Minimum Requirements</h2> + <table> + {% for row in minreq %} + {% set outer_loop = loop %} + <tr> + {% for column in row %} + {% if outer_loop.first or loop.first %} + <th>{{ column }}</th> + {% else %} + <td> + <ul> + {% for item in column %} + <li> + {{ item }} + </li> + {% endfor %} + </ul> + </td> + {% endif %} + {% endfor %} + </tr> + {% endfor %} + </table> + {% endif %} +</body> +</html> \ No newline at end of file diff --git a/mc_openapi/templates/home.html b/mc_openapi/templates/home.html new file mode 100644 index 0000000000000000000000000000000000000000..c71017d9ae33da28f92e61be9f978b0d2ab538d5 --- /dev/null +++ b/mc_openapi/templates/home.html @@ -0,0 +1,44 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>DOML Model Checker - Dashboard</title> + <meta charset="utf-8"> +</head> +<body> + <h1>DOML Model Checker - Dashboard</h1> + <input type="file" id="domlx" name="domlx" accept="text/xml,.domlx" /> + + <script> + const input = document.querySelector("#domlx") + input.addEventListener('change', readFile, false) + + async function readFile(event) { + const file = event.target.files[0] + if (file) { + const res = await fetch('/csp_html', { + method: 'POST', + headers: { + "Content-Type": "application/xml", + }, + redirect: "follow", + body: await file.text() + }) + if (res.status === 200) { + newHtml = await res.text() + // Fix escaped quotes for tags + newHtml = newHtml.replaceAll("\\\"", "\"") + // Unescape emojis + // See: https://stackoverflow.com/questions/51640509/ + newHtml = newHtml.replace(/\\u[\dA-F]{4}/gi, function(match) { + return String.fromCharCode(parseInt(match.replace(/\\u/g, ''), 16)); + }) + // Remove trailing and heading " + newHtml = newHtml.substr(1, newHtml.length - 3) + document.write(newHtml) + } + } + } + + </script> +</body> +</html> \ No newline at end of file diff --git a/mc_openapi/templates/style.css b/mc_openapi/templates/style.css new file mode 100644 index 0000000000000000000000000000000000000000..9336cc2e4ebf42fe28f07e82dafa1ea864973111 --- /dev/null +++ b/mc_openapi/templates/style.css @@ -0,0 +1,44 @@ +* { + color: black; + font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; +} + +/* spacing */ + +table { + table-layout: fixed; + width: 100%; + border-collapse: collapse; + border: 1px solid black; +} + +th, +td { + padding: 0.5em; + border: 1px solid black; + text-align: left; +} + +th { + background-color: #e7e7e7; +} + +td { + vertical-align: top; +} + +ul { + padding: 0; + margin: 0; + list-style: none; +} + +li { + word-wrap: anywhere; + margin-bottom: 1em; +} + +li:last-child { + margin-bottom: 0; +} + \ No newline at end of file