From 1c896a152fbc046f85f29c29024ac7a20dfb4efb Mon Sep 17 00:00:00 2001 From: anjastrunk <119566837+anjastrunk@users.noreply.github.com> Date: Thu, 29 Aug 2024 08:48:17 +0200 Subject: [PATCH] Create Gaia-X compliant credential for CSP's as `LegalPerson` and OpenStack Cloud as "ServiceOffering" (#96) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add GXDCH services Signed-off-by: Anja Strunk * Add legal person vc creation Signed-off-by: Anja Strunk * Create mandatory vc Signed-off-by: Anja Strunk * Add current status Signed-off-by: Anja Strunk * Commit current working state Signed-off-by: Anja Strunk * Add generation of valid GX Credentials for CSPs Signed-off-by: Anja Strunk * Generate Gaia-X Credentials for Service OfferingsW Signed-off-by: Anja Strunk * Update requirements Signed-off-by: Anja Strunk * Remove outdated files Signed-off-by: Anja Strunk * Fix test cli tests Signed-off-by: Anja Strunk * Fix tests Signed-off-by: Anja Strunk * Fix tests and refactor source code Signed-off-by: Anja Strunk * Update config Signed-off-by: Anja Strunk * Fix flake errors Signed-off-by: Anja Strunk * Bump certifi from 2024.2.2 to 2024.7.4 (#100) Bumps [certifi](https://github.com/certifi/python-certifi) from 2024.2.2 to 2024.7.4. - [Commits](https://github.com/certifi/python-certifi/compare/2024.02.02...2024.07.04) --- updated-dependencies: - dependency-name: certifi dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Signed-off-by: Anja Strunk * Bump urllib3 from 2.2.1 to 2.2.2 (#99) Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.2.1 to 2.2.2. - [Release notes](https://github.com/urllib3/urllib3/releases) - [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst) - [Commits](https://github.com/urllib3/urllib3/compare/2.2.1...2.2.2) --- updated-dependencies: - dependency-name: urllib3 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: anjastrunk <119566837+anjastrunk@users.noreply.github.com> Signed-off-by: Anja Strunk * Bump braces from 3.0.2 to 3.0.3 (#101) Bumps [braces](https://github.com/micromatch/braces) from 3.0.2 to 3.0.3. - [Changelog](https://github.com/micromatch/braces/blob/master/CHANGELOG.md) - [Commits](https://github.com/micromatch/braces/compare/3.0.2...3.0.3) --- updated-dependencies: - dependency-name: braces dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: anjastrunk <119566837+anjastrunk@users.noreply.github.com> Signed-off-by: Anja Strunk * Print CPS credentials Signed-off-by: Anja Strunk * Improve output Signed-off-by: Anja Strunk * Fix unit tests Signed-off-by: Anja Strunk * Manage optional properties Signed-off-by: Anja Strunk * Increase code coverage Signed-off-by: Anja Strunk * Increase code coverage Signed-off-by: Anja Strunk * Fix python lint errors Signed-off-by: Anja Strunk * Increase code covrage Signed-off-by: Anja Strunk * Fix python lint errors§ Signed-off-by: Anja Strunk * Update generator/cli.py Co-authored-by: Matthias Büchse Signed-off-by: anjastrunk <119566837+anjastrunk@users.noreply.github.com> Signed-off-by: Anja Strunk * Update generator/cli.py Co-authored-by: Matthias Büchse Signed-off-by: anjastrunk <119566837+anjastrunk@users.noreply.github.com> Signed-off-by: Anja Strunk * Update generator/cli.py Co-authored-by: Matthias Büchse Signed-off-by: anjastrunk <119566837+anjastrunk@users.noreply.github.com> Signed-off-by: Anja Strunk * Update generator/cli.py Co-authored-by: Matthias Büchse Signed-off-by: anjastrunk <119566837+anjastrunk@users.noreply.github.com> Signed-off-by: Anja Strunk * Update generator/discovery/openstack/openstack_discovery.py Co-authored-by: Matthias Büchse Signed-off-by: anjastrunk <119566837+anjastrunk@users.noreply.github.com> Signed-off-by: Anja Strunk * Update generator/discovery/openstack/openstack_discovery.py Co-authored-by: Matthias Büchse Signed-off-by: anjastrunk <119566837+anjastrunk@users.noreply.github.com> Signed-off-by: Anja Strunk * Integrate review comments Signed-off-by: Anja Strunk * Fix typo Signed-off-by: Anja Strunk * Fix python lint errors Signed-off-by: Anja Strunk * Resolve review comments Signed-off-by: Anja Strunk * Update generator/cli.py Co-authored-by: Markus Hentsch <129268441+markus-hentsch@users.noreply.github.com> Signed-off-by: anjastrunk <119566837+anjastrunk@users.noreply.github.com> * Update generator/discovery/csp_generator.py Co-authored-by: Markus Hentsch <129268441+markus-hentsch@users.noreply.github.com> Signed-off-by: anjastrunk <119566837+anjastrunk@users.noreply.github.com> * Update generator/discovery/csp_generator.py Co-authored-by: Markus Hentsch <129268441+markus-hentsch@users.noreply.github.com> Signed-off-by: anjastrunk <119566837+anjastrunk@users.noreply.github.com> * Update generator/discovery/csp_generator.py Co-authored-by: Markus Hentsch <129268441+markus-hentsch@users.noreply.github.com> Signed-off-by: anjastrunk <119566837+anjastrunk@users.noreply.github.com> * Update tests/test_csp_generator.py Co-authored-by: Markus Hentsch <129268441+markus-hentsch@users.noreply.github.com> Signed-off-by: anjastrunk <119566837+anjastrunk@users.noreply.github.com> * Update tests/test_csp_generator.py Co-authored-by: Markus Hentsch <129268441+markus-hentsch@users.noreply.github.com> Signed-off-by: anjastrunk <119566837+anjastrunk@users.noreply.github.com> * Update tests/test_csp_generator.py Co-authored-by: Markus Hentsch <129268441+markus-hentsch@users.noreply.github.com> Signed-off-by: anjastrunk <119566837+anjastrunk@users.noreply.github.com> * Output compliance credential for both CSP and Service Offering Signed-off-by: Anja Strunk * Update generator/common/credentials.py Co-authored-by: Markus Hentsch <129268441+markus-hentsch@users.noreply.github.com> Signed-off-by: anjastrunk <119566837+anjastrunk@users.noreply.github.com> * Update generator/common/const.py Co-authored-by: Markus Hentsch <129268441+markus-hentsch@users.noreply.github.com> Signed-off-by: anjastrunk <119566837+anjastrunk@users.noreply.github.com> * Update config/config.yaml Co-authored-by: Markus Hentsch <129268441+markus-hentsch@users.noreply.github.com> Signed-off-by: anjastrunk <119566837+anjastrunk@users.noreply.github.com> * Update generator/common/const.py Co-authored-by: Markus Hentsch <129268441+markus-hentsch@users.noreply.github.com> Signed-off-by: anjastrunk <119566837+anjastrunk@users.noreply.github.com> * Update generator/cli.py Co-authored-by: Markus Hentsch <129268441+markus-hentsch@users.noreply.github.com> Signed-off-by: anjastrunk <119566837+anjastrunk@users.noreply.github.com> * Update generator/cli.py Co-authored-by: Markus Hentsch <129268441+markus-hentsch@users.noreply.github.com> Signed-off-by: anjastrunk <119566837+anjastrunk@users.noreply.github.com> * Fix os_image.os_distro is None Signed-off-by: Anja Strunk --------- Signed-off-by: Anja Strunk Signed-off-by: dependabot[bot] Signed-off-by: anjastrunk <119566837+anjastrunk@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Matthias Büchse Co-authored-by: Markus Hentsch <129268441+markus-hentsch@users.noreply.github.com> --- config/config.yaml | 41 +- generator/cli.py | 257 +++- generator/common/config.py | 13 +- generator/common/const.py | 35 +- generator/common/credentials.py | 11 + generator/common/crypto.py | 80 ++ generator/common/json_ld.py | 14 +- generator/discovery/csp_generator.py | 120 ++ generator/discovery/gxdch_services.py | 55 + .../openstack/openstack_discovery.py | 16 +- .../openstack/server_flavor_discovery.py | 11 +- .../openstack/vm_images_discovery.py | 31 +- gx-cred-generator.py | 288 ---- gx_context.py | 18 - openstack-discovery.py | 653 --------- requirements.in | 1 + requirements.txt | 10 +- results/osdescriptor_1683188130.json | 1178 ----------------- results/osdescriptor_1683189352.yaml | 775 ----------- tests/data/credential.json | 262 ---- tests/data/empty_credential.json | 12 - tests/test_cli.py | 208 ++- tests/test_csp_generator.py | 84 ++ tests/test_gxdch_services.py | 111 ++ tests/test_json_ld.py | 32 +- tests/test_openstack_discovery.py | 2 +- tests/test_vm_image_discovery.py | 33 +- 27 files changed, 1032 insertions(+), 3319 deletions(-) create mode 100644 generator/common/credentials.py create mode 100644 generator/common/crypto.py create mode 100644 generator/discovery/csp_generator.py create mode 100644 generator/discovery/gxdch_services.py delete mode 100755 gx-cred-generator.py delete mode 100755 gx_context.py delete mode 100755 openstack-discovery.py delete mode 100644 results/osdescriptor_1683188130.json delete mode 100644 results/osdescriptor_1683189352.yaml delete mode 100644 tests/data/credential.json delete mode 100644 tests/data/empty_credential.json create mode 100644 tests/test_csp_generator.py create mode 100644 tests/test_gxdch_services.py diff --git a/config/config.yaml b/config/config.yaml index fd176ac..50d2a4a 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -1,12 +1,38 @@ -################################################################ +######################################################################################## # Mandatory attributes required by Gaia-X -################################################################ -CPS: +######################################################################################## +Credentials: + # absolute path to private key to sign verifiable credentials + key: path-to-private-key + # verification method used to check proof of verifiable credential. Method must be published within DID document of CSP + verification-method: did:web:example.com#JWK2020-X509-0 + # base url for generated credentials + base_url: https://example.com + # URL did resolver web service + did_resolver: https://uniresolver.io/1.0/identifiers + +CSP: did: did:web:example.com + legal-name: Example Corp + # allowed values are country codes according to ISO 3166-2 alpha2, alpha-3 or numeric format. + legal-address-country-code: DE-SN + # allowed values are country codes according to ISO 3166-2 alpha2, alpha-3 or numeric format. + headquarter-address-country-code: DE-SN + # list of registration numbers. At least one registration number must be given. Each key MUST can only be set once. + registration_numbers: + # CSP VAT number + vat-id: DE123456789 + # CSP LEI code + lei-code: 123456789 + # CSP local registration number + local-req-number: 123456789 + # CSP EORI code + eori: 123456789 + # CSP EUID code + euid: 123456789 IaaS: did: did:web:example.com:iaas - # URL to a document containing terms and conditions of service offering terms-and-conditions: - www.example.com/tan1 - www.example.com/tan2 @@ -20,6 +46,13 @@ IaaS: # Allowed values are MIME types format-type: "plain" +# Endpoints of Gaia-X Digital Clearing House +gxdch: + notary-service: https://registrationnumber.notary.lab.gaia-x.eu/v1-staging + compliance-service: https://compliance.lab.gaia-x.eu/v1-staging + registry-service: https://registry.lab.gaia-x.eu/v1-staging + + ################################################################ # CAUTION: Do not change these values, unless you want to overwrite default behaviour ################################################################ diff --git a/generator/cli.py b/generator/cli.py index 1f65ccf..f1dbe4f 100755 --- a/generator/cli.py +++ b/generator/cli.py @@ -11,20 +11,39 @@ """ import json +import os import sys +from datetime import datetime, timezone +from typing import List import click -import openstack as os +import openstack as o_stack import yaml +from openstack.connection import Connection +import generator.common.const as const import generator.common.json_ld as json_ld +from generator.common import credentials, crypto from generator.common.config import Config +from generator.discovery.csp_generator import CspGenerator +from generator.discovery.gxdch_services import (ComplianceService, + RegistryService) from generator.discovery.openstack.openstack_discovery import \ OpenstackDiscovery SHAPES_FILE_FORMAT = "turtle" DATA_FILE_FORMAT = "json-ld" +VC_NAME_LOOKUP = { + "lp": "Legal Person", + "lrn": "Legal Registration Number", + "tandc": "Gaia-X Terms and Conditions", + "cs_csp": "GXDCH Compliance Service", + "cs_so": "GXDCH Compliance Service", + "so": "Service Offering", + "vmso": "Virtual Machine Service Offering", +} + @click.group() def cli_commands(): @@ -32,24 +51,103 @@ def cli_commands(): @click.command() +@click.option( + "--auto-sign/--no-auto-sign", + default=False, + help="Sign Gaia-X Terms and Conditions, automatically, without asking for permission on screen.", +) +@click.option( + "--out-dir", + default=".", + help="Path to output directory.", +) @click.option( "--config", default="config/config.yaml", help="Path to Configuration file for SCS GX Credential Generator.", ) -@click.option("--timeout", default=12, help="Timeout for API calls in seconds") +@click.option("--timeout", default=24, help="Timeout for API calls in seconds") @click.argument("cloud") -def openstack(cloud, timeout, config): - """Generates Gaia-X Credentials for openstack cloud CLOUD. +def openstack(cloud, timeout, config, out_dir, auto_sign): + """Generates Gaia-X Credentials for CSP And OpenStack cloud CLOUD. CLOUD MUST refer to a name defined in Openstack's configuration file clouds.yaml.""" + with open(config, "r") as config_file: + conf = Config(yaml.safe_load(config_file)) + + if not auto_sign and not _are_gaiax_tandc_signed(conf): + # user did not agree Gaia-X terms and conditions, we have to abort here + print("Gaia-X terms and conditions were not signed - process aborted!") + return + + # create Gaia-X Credentials for CSP + csp_gen = CspGenerator(conf=conf) + csp_vcs = csp_gen.generate() + + # create Gaia-X Credentials for OpenStack + so_vcs = create_vmso_vcs( + conf=conf, + cloud=cloud, + csp_vcs=csp_vcs, + timeout=timeout, + ) + + vcs = {**csp_vcs, **so_vcs} + _print_vcs(vcs, out_dir) + + +@click.command() +def kubernetes(): + """Generates Gaia-X Credentials for CSP and Kubernetes.""" + pass + + +# def load_file(filepath, file_format=DATA_FILE_FORMAT): +# """Load file in a given format""" +# graph = rdflib.Graph() +# graph.parse(filepath, format=file_format) +# return graph - # init Openstack Connections - conn = os.connect(cloud=cloud, timeout=timeout, api_timeout=timeout * 1.5 + 4) +@click.command() +@click.option( + "--auto-sign/--no-auto-sign", + default=False, + help="Sign Gaia-X Terms and Conditions, automatically, without asking for permission on screen.", +) +@click.option( + "--out-dir", + help="Path to output directory.", +) +@click.option( + "--config", + default="config/config.yaml", + help="Path to Configuration file for SCS GX Credential Generator.") +def csp(config, out_dir, auto_sign): + """Generate Gaia-X Credential for CSP.""" + # load config file + with open(config, "r") as config_file: + conf = Config(yaml.safe_load(config_file)) + + if not auto_sign and not _are_gaiax_tandc_signed(conf): + # user did not agree Gaia-X terms and conditions, we have to abort here + print("Gaia-X terms and conditions were not signed - process aborted!") + return + vcs = CspGenerator(conf).generate() + _print_vcs(vcs, out_dir) + + +def init_openstack_connection(cloud: str, timeout: int = 12) -> Connection: + """ + Init connection to OpenStack cloud. + @param cloud: name of OpenStack cloud to be connected. + @param timeout: time, after connection is initiated a second time. + @return: OpenStacl connection. + """ try: + conn = o_stack.connect(cloud=cloud, timeout=timeout, api_timeout=timeout * 1.5 + 4) conn.authorize() except Exception: print("INFO: Retry connection with 'default' domain", file=sys.stderr) - conn = os.connect( + conn = o_stack.connect( cloud=cloud, timeout=timeout, api_timeout=timeout * 1.5 + 4, @@ -57,35 +155,142 @@ def openstack(cloud, timeout, config): project_domain_id="default", ) conn.authorize() + return conn - # generate Gaia-X Credentials - with open(config, "r") as config_file: - # init everything - config_dict = yaml.safe_load(config_file) - os_cloud = OpenstackDiscovery(conn, Config(config_dict)) - # run discovery - creds = os_cloud.discover() +def create_vmso_vcs(conf: Config, cloud: str, csp_vcs: List[dict], timeout: int = 12) -> dict[dict]: + """ + Create Gaia-X Credentials for Virtual Machine Service Offering. This means + - Gaia-X Credential of OpenStack Cloud as ServiceOffering with mandatory attributes + - Gaia-X Credential of OpenStack Cloud as VirtualMachineServiceOffering + - Gaia-X Credential of GXDCH Compliance Service, attesting complaince of OpenStack cloud description with Gaia-X rules. + @param conf: configuration settings for creation process. + @param cloud: OpenStack ncloud name. + @param csp_vcs: Gaia-X Credentials of Cloud Service Provider. + @param timeout: timeout for connection to OpenStack cloud. If timeout expires, connection is initialed a second time. + @return: A list of Gaia-X Credentials describing given OpenStack cloud. + """ + csp = conf.get_value([const.CONFIG_CSP]) + # iaas = conf.get_value([const.CONFIG_IAAS]) not yet used, as Gaia-X "abuses" id attribute of Verifiable Credentials + cred_settings = conf.get_value([const.CONFIG_CRED]) - props = json_ld.get_json_ld_context() - props["@graph"] = creds - print(json.dumps(props, indent=4, default=json_ld.to_json_ld)) + # init services + conn = init_openstack_connection(cloud=cloud, timeout=timeout) + compliance = ComplianceService(conf.get_value([const.CONST_GXDCH, const.CONST_GXDCH_COMP])) + discovery = OpenstackDiscovery(conn=conn, config=conf) + # run openstack discovery and build Gaia-X Credential for Virtual Machine Service Offering + print('Create VC of type "gx:VirtualMachineServiceOffering"...', end='') + vm_offering = discovery.discover() + vmso_vc = { + '@context': [const.VC_CONTEXT, const.JWS_CONTEXT, const.REG_CONTEXT], + 'type': "VerifiableCredential", + 'id': cred_settings[const.CONFIG_CRED_BASE_CRED_URL] + "/vmo.json", + 'issuer': csp['did'], + 'issuanceDate': str(datetime.now(tz=timezone.utc).isoformat()), + 'credentialSubject': json.loads(json.dumps(vm_offering, default=json_ld.to_json_ld)), + } + vmso_vc_signed = crypto.sign_cred(cred=vmso_vc, + key=crypto.load_jwk_from_file(cred_settings[const.CONFIG_CRED_KEY]), + verification_method=cred_settings[const.CONFIG_CRED_VER_METH]) + print('ok') -@click.command() -def kubernetes(): - """Generates Gaia-X Credentials for kubernetes.""" - pass + # build Gaia-X Credential for Service Offering + print('Create VC of type "gx:ServiceOffering"...', end='') + so_vc = { + '@context': [const.VC_CONTEXT, const.JWS_CONTEXT, const.REG_CONTEXT], + 'type': "VerifiableCredential", + 'id': cred_settings[const.CONFIG_CRED_BASE_CRED_URL] + "/so.json", + 'issuer': csp['did'], + 'issuanceDate': str(datetime.now(tz=timezone.utc).isoformat()), + 'credentialSubject': { + "type": "gx:ServiceOffering", + "id": cred_settings[const.CONFIG_CRED_BASE_CRED_URL] + "/so.json#subject", # iaas['did'], + "gx:providedBy": { + 'id': csp_vcs['lp']['credentialSubject']['id'] + }, + "gx:termsAndConditions": [ + {'gx:URL': s_tac.url, 'gx:hash': s_tac.hash} + for s_tac in vm_offering.serviceOfferingTermsAndConditions], + "gx:policy": vm_offering.servicePolicy, + "gx:dataAccountExport": { + "gx:requestType": vm_offering.dataAccountExport.requestType.code.text, + "gx:accessType": vm_offering.dataAccountExport.accessType.code.text, + "gx:formatType": "application/" + vm_offering.dataAccountExport.formatType.code.text + } + } + } + # sign service offering credential + so_vc_signed = crypto.sign_cred(cred=so_vc, + key=crypto.load_jwk_from_file(cred_settings[const.CONFIG_CRED_KEY]), + verification_method=cred_settings[const.CONFIG_CRED_VER_METH]) + print('ok') + + # Request Gaia-X Compliance Credential for Service Offering + print('Request VC of type "gx:compliance" for Service Offering at GXDCH Compliance Service...', end='') + vp = credentials.convert_to_vp(creds=[csp_vcs['tandc'], csp_vcs['lrn'], csp_vcs['lp'], so_vc_signed]) + comp_vc = compliance.request_compliance_vc(vp, + cred_settings[const.CONFIG_CRED_BASE_CRED_URL] + "/so_compliance.json") + + print('ok') + return {'so': so_vc, 'cs_so': json.loads(comp_vc), 'vmso': vmso_vc_signed, 'vp_so': vp} + + +def _get_timestamp(): + dt = datetime.now() # for date and time + # ts_1 = datetime.timestamp(dt) # for timestamp + return dt.strftime('%Y-%m-%d_%H-%M-%S') + + +def _print_vcs(vcs: dict, out_dir: str = "."): + if not os.path.isdir(out_dir): + raise NotADirectoryError(out_dir + " is not a directory or does not exit!") + + ts = _get_timestamp() + for key in vcs: + vc_path = os.path.join(out_dir, key + "_" + ts + ".json") + with open(vc_path, "w") as vc_file: + if key == 'vp_csp': + print( + "Write Verifiable Presentation of Cloud Service Provider to be verified at GXDCH Compliance Service to " + str( + vc_path)) + vc_file.write(json.dumps(vcs[key], indent=2)) + elif key == 'vp_so': + print( + "Write Verifiable Presentation of Service Offering to be verified at GXDCH Compliance Service to " + str( + vc_path)) + vc_file.write(json.dumps(vcs[key], indent=2)) + else: + print("Write Gaia-X Credential for " + VC_NAME_LOOKUP[key] + " to " + str(vc_path)) + vc_file.write(json.dumps(vcs[key], indent=2)) + + +def _are_gaiax_tandc_signed(conf: Config) -> bool: + reg = RegistryService(conf.get_value([const.CONST_GXDCH, const.CONST_GXDCH_REG])) + tand = reg.get_gx_tandc() + + print("Do you agree Gaia-X Terms and conditions version " + tand['version'] + ".") + print() + print("-------------------------- Gaia-X Terms and Conditions --------------------------------------------") + print(tand['text']) + print("-------------------------- ------------------------------------------------------------------------") + print() + print("Please type 'y' for 'I do agree' and 'n' for 'I do not agree': ") + + resp = input() + while resp.lower() not in ['y', 'n']: + print("Please type 'y' for 'I do agree' and 'n' for 'I do not agree: '") + resp = input() + + if resp.lower() == 'y': + return True + return False -# def load_file(filepath, file_format=DATA_FILE_FORMAT): -# """Load file in a given format""" -# graph = rdflib.Graph() -# graph.parse(filepath, format=file_format) -# return graph cli_commands.add_command(openstack) cli_commands.add_command(kubernetes) +cli_commands.add_command(csp) if __name__ == "__main__": cli_commands() diff --git a/generator/common/config.py b/generator/common/config.py index 9f1be93..fb1145c 100644 --- a/generator/common/config.py +++ b/generator/common/config.py @@ -16,10 +16,21 @@ def _get_value(config, keys: List[str]): class Config: - def __init__(self, config): + """Wrapper class for all configuration settings. Configuration settings are stored in yaml file on drive and + imported as a nested dictionary.""" + def __init__(self, config: dict): self.config = config def get_value(self, keys: List[str]): + """ + Return configuration value. Config settings are stored as yaml and imported as nested dict. + E.g. { 'key1': {'key2': {'key3': 'foo'}}} + + The list of keys are required to step down throught nested dicts to requested value. + E.g. ['key1', 'key2', 'key3'] returns 'foo' + @param keys: list of keys + @return: value + """ try: return _get_value(self.config, keys) except KeyError: diff --git a/generator/common/const.py b/generator/common/const.py index 035de27..58f3af4 100644 --- a/generator/common/const.py +++ b/generator/common/const.py @@ -5,9 +5,14 @@ # General keys CONFIG_DID = "did" +CONFIG_DID = "did" +CONFIG_T_AND_C = "terms-and-conditions" -# CPS configuration keys -CONFIG_CSP = "CPS" +# Credentials keys +CONFIG_CRED = "Credentials" +CONFIG_CRED_KEY = "key" +CONFIG_CRED_VER_METH = "verification-method" +CONFIG_CRED_BASE_CRED_URL = "base_url" # IaaS configuration keys CONFIG_IAAS = "IaaS" @@ -16,8 +21,24 @@ CONFIG_IAAS_DATA_EXPORT_ACCESS_TYPE = "access-type" CONFIG_IAAS_DATA_EXPORT_FORMAT_TYPE = "format-type" CONFIG_IAAS_SERVICE_POLICY = "service-policy" -CONFIG_IAAS_T_AND_C = "terms-and-conditions" +# CSP configuration keys +CONFIG_CSP = "CSP" +CONFIG_CSP_NAME = "legal-name" +CONFIG_CSP_LEG_AD = "legal-address-country-code" +CONFIG_CSP_HQ_ADR = "headquarter-address-country-code" +CONFIG_CSP_REG_NUMBER = "registration_numbers" +CONFIG_CSP_VAT_ID = "vat-id" +CONFIG_CSP_LEI_CODE = "lei-code" +CONFIG_CSP_LOCAL_CODE = "local-req-number" +CONFIG_CSP_EORI = "eori" +CONFIG_CSP_EUID = "euid" + +# GXDCH +CONST_GXDCH = "gxdch" +CONST_GXDCH_NOT = "notary-service" +CONST_GXDCH_COMP = "compliance-service" +CONST_GXDCH_REG = "registry-service" # Default values DEFAULT_RESOURCE_POLICY = "default: allow intent" @@ -29,6 +50,7 @@ UNIT_GB = "https://qudt.org/vocab/unit/GigaBYTE" UNIT_GHZ = "https://qudt.org/vocab/unit/GigaHZ" +# Dicovery keys CONFIG_VM_IMAGE = "vm image" CONFIG_RESOURCE_POLICY = "resource-policy" CONFIG_LICENSE = "license" @@ -75,3 +97,10 @@ CONFIG_HV_CH = "Cloud Hypervisor" CONFIG_HV_VMW = "vmware" CONFIG_HV_HYV = "hyper-v" + +# JSON-LD Context +VC_CONTEXT = "https://www.w3.org/2018/credentials/v1" +VP_CONTEXT = VC_CONTEXT +JWS_CONTEXT = "https://w3id.org/security/suites/jws-2020/v1" +REG_CONTEXT = "https://registry.lab.gaia-x.eu/development/api/trusted-shape-registry/v1/shapes/jsonld/trustframework#" +LRN_CONTEXT = "https://registry.lab.gaia-x.eu/development/api/trusted-shape-registry/v1/shapes/jsonld/participant" diff --git a/generator/common/credentials.py b/generator/common/credentials.py new file mode 100644 index 0000000..1768746 --- /dev/null +++ b/generator/common/credentials.py @@ -0,0 +1,11 @@ +from typing import List + +from generator.common import const + + +def convert_to_vp(creds: List[dict]) -> dict: + return { + '@context': const.VP_CONTEXT, + 'type': "VerifiablePresentation", + 'verifiableCredential': creds, + } diff --git a/generator/common/crypto.py b/generator/common/crypto.py new file mode 100644 index 0000000..936694d --- /dev/null +++ b/generator/common/crypto.py @@ -0,0 +1,80 @@ +from datetime import datetime, timezone +from hashlib import sha256 + +from jwcrypto.common import json_encode +from jwcrypto.jws import JWK, JWS +from pyld import jsonld + +""" +Methods to handle cryptography. Most source code is taken from Gaia-X Example +in https://gitlab.com/gaia-x/lab/workshops/gaia-x-101/-/tree/master +""" + + +def hash_str(text: str) -> str: + """ + Hash given sting. + @param text: string to hash + @return: hash in hexadecimal format + """ + return sha256(text.encode("utf-8")).hexdigest() + + +def compact_sig(sig) -> str: + """ + Compact Json Web Signature according to JWS Compact Serialization in RFC 7519. + @param sig: JSON Web Signature value to be compacted + @return: compacted signature as string + """ + parts = sig.split(".") + return parts[0] + ".." + parts[2] + + +def sign_cred(cred: dict, key: JWK, verification_method: str) -> dict: + """ + Sign given credential with the given key and the given verification method. + @param cred: credential to be signed + @param key: private kes to sign + @param verification_method: verification method to verifiy signature + @return: Verificable Credential as dict + """ + # canonicalize credential to produce a unique representation + # According to JSON Web Signature 2020 Spec, use https://w3id.org/security#URDNA2015 for canonicalization + cannon_cred = jsonld.normalize(cred, {'algorithm': 'URDNA2015', 'format': 'application/n-quads'}) + + # hash document to insure integrity, see https://w3c.github.io/vc-data-integrity/#how-it-works + # use SHA-256 for JSON Web Signature 2020, see https://w3c-ccg.github.io/lds-jws2020/#suite-definition + # hash acts as JSON Web Signature's payload + payload = hash_str(cannon_cred) + + # create JSON Web Signature for credential's hash + jwstoken = JWS(payload) + jwstoken.add_signature(key, + None, + json_encode({"alg": "PS256", "b64": False, "crit": ["b64"]}), + json_encode({"kid": key.thumbprint()})) + sig = jwstoken.serialize(compact=True) + + # add proof to credential as JsonWebSiganture2020, see https://w3c-ccg.github.io/lds-jws2020/ + cred['proof'] = { + "type": "JsonWebSignature2020", + # ISO_8601 formated date string. + "created": datetime.now(tz=timezone.utc).isoformat(), + # SHOULD match assertion method expressed in DID document. + "proofPurpose": "assertionMethod", + # resolvable link to verification method. Dereferencing SHOULD result in an object of type JsonWebKey2020. + "verificationMethod": verification_method, + "jws": compact_sig(sig) + } + return cred + + +def load_jwk_from_file(path: str) -> JWK: + """ + Load Json Web Key from file + @param path: path to key file + @return: loaded Json Web Key + """ + with open(path, "rb") as key_file: + bytes = key_file.read() + return JWK.from_pem(bytes, password=None) diff --git a/generator/common/json_ld.py b/generator/common/json_ld.py index ce31835..920cb29 100644 --- a/generator/common/json_ld.py +++ b/generator/common/json_ld.py @@ -72,7 +72,7 @@ def to_json_ld(obj) -> dict: if isinstance(obj, JsonLdObject): # if JsonLdObject add id gx_object = obj.gx_object - json_ld["@id"] = "ex:" + obj.gx_id.replace(" ", "") + json_ld["id"] = "ex:" + obj.gx_id.replace(" ", "") # call to_json_ld for gx_object json_ld.update(to_json_ld(gx_object)) return json_ld @@ -82,7 +82,7 @@ def to_json_ld(obj) -> dict: # json_ld['@type'] = get_types(obj.__class__) try: # set type of Gaia-X object if possible - json_ld["@type"] = obj.class_class_curie + json_ld["type"] = obj.class_class_curie except AttributeError: pass for key, value in obj.__dict__.items(): @@ -108,19 +108,19 @@ def to_json_ld(obj) -> dict: return json_ld elif isinstance(obj, datetime): # Add type for datetime - return {"@type": "xsd:dateTime", "@value": obj.strftime("%Y-%m-%dT%H:%M:%S")} + return {"type": "xsd:dateTime", "@value": obj.strftime("%Y-%m-%dT%H:%M:%S")} elif isinstance(obj, date): # add type for date - return {"@type": "xsd:date", "@value": obj.strftime("%Y-%m-%d")} + return {"type": "xsd:date", "@value": obj.strftime("%Y-%m-%d")} elif isinstance(obj, float): # add type for float - return {"@type": "xsd:float", "@value": obj} + return {"type": "xsd:float", "@value": obj} elif isinstance(obj, URI): # add type for URI - return {"@type": "xsd:anyURI", "@value": obj} + return {"type": "xsd:anyURI", "@value": obj} elif isinstance(obj, bool): # add type for boolean - return {"@type": "xsd:boolean", "@value": obj} + return {"type": "xsd:boolean", "@value": obj} else: return obj diff --git a/generator/discovery/csp_generator.py b/generator/discovery/csp_generator.py new file mode 100644 index 0000000..a98b620 --- /dev/null +++ b/generator/discovery/csp_generator.py @@ -0,0 +1,120 @@ +"""Generator of Gaia-X Credentials for CSPs. +""" +import json +from datetime import datetime, timezone + +import generator.common.const as const +from generator.common import credentials, crypto +from generator.common.config import Config +from generator.discovery.gxdch_services import (ComplianceService, + NotaryService, RegistryService) + + +class CspGenerator: + + def __init__(self, conf: Config) -> None: + self.notary = NotaryService(conf.get_value([const.CONST_GXDCH, const.CONST_GXDCH_NOT])) + self.compliance = ComplianceService(conf.get_value([const.CONST_GXDCH, const.CONST_GXDCH_COMP])) + self.registry = RegistryService(conf.get_value([const.CONST_GXDCH, const.CONST_GXDCH_REG])) + + self.csp = conf.get_value([const.CONFIG_CSP]) + self.cred_settings = conf.get_value([const.CONFIG_CRED]) + self.cred_base_url = self.cred_settings[const.CONFIG_CRED_BASE_CRED_URL] + + def generate(self) -> dict: + """ + Generate Gaia-X compliant Gaia-X Credential for CSP. This includes the following Verifiable Credentials (VC). + - VC on signed Gaia-X terms and conditions + - VC on CSP's legal registration number, such as LEI or VAT id + - VC on CSP's properties as Gaia-X Legal Person + - VC on CSP's Gaia-X compliance + + @param auto_sign True, if Gaia-X terms and conditions should be automatically signed. Else user has to confirm + terms and conditions manually on user prompt. + @return: dictionary of VCs + """ + # sign Gaia-X terms and conditions + print('Create and sign VC of type "gx:GaiaXTermsAndConditions" for CSP...', end='') + tandc_vc = self._sign_gaia_x_terms_and_conditions() + if tandc_vc is None: + return + print('ok') + + # retrieve legal registration number from GXDCH Notary + print('Request VC of type for "gx:LegalRegistrationNumber" for CSP at GXDCH Notary Service...', end='') + lrn_vc = self.notary.request_reg_number_vc( + csp=self.csp, + cred_id=self.cred_base_url + "/lrn.json", + cred_subject_id=self.cred_base_url + "/lrn.json#subject") + print('ok') + + # create Gaia-X Credential for CSP as Legal Person + print('Create and sign VC of type "gx:LegalPerson for CSP"...', end='') + lp_vc = self._sign_legal_person(lrn_vc['credentialSubject']['id']) + print('ok') + + # request Gaia-X compliance credential for CSP as Legal Person + print('Request VC of type "gx:compliance" for CSP at GXDCH Compliance Service...', end='') + vp = credentials.convert_to_vp(creds=[tandc_vc, lrn_vc, lp_vc]) + cs_vc = self.compliance.request_compliance_vc(vp, self.cred_base_url + "/csp_compliance.json") + print('ok') + return {'tandc': tandc_vc, 'lrn': lrn_vc, 'lp': lp_vc, 'cs_csp': json.loads(cs_vc), 'vp_csp': vp} + + def _sign_gaia_x_terms_and_conditions(self) -> dict: + """ + Create a Gaia-X Credential on signed Gaia-X terms and conditions. + + @param auto_sign: If true, Gaia-X terms and conditions are signed automatically, + otherwise user is requested of confirm terms and conditions + @return: Gaia-X Credential on signed Gaia-X terms and conditions as dictionary. + """ + + tandc_vc = { + '@context': [const.VC_CONTEXT, const.JWS_CONTEXT, const.REG_CONTEXT], + 'type': "VerifiableCredential", + 'id': self.cred_base_url + "/tandc.json", + 'issuer': self.csp['did'], + 'issuanceDate': str(datetime.now(tz=timezone.utc).isoformat()), + 'credentialSubject': { + "type": "gx:GaiaXTermsAndConditions", + "gx:termsAndConditions": self.registry.get_gx_tandc()['text'], + "id": self.cred_base_url + "/tandc.json#subject" + } + } + return crypto.sign_cred(cred=tandc_vc, + key=crypto.load_jwk_from_file(self.cred_settings[const.CONFIG_CRED_KEY]), + verification_method=self.cred_settings[const.CONFIG_CRED_VER_METH]) + + def _sign_legal_person(self, lrn_cred_id: str): + """ + Create Gaia-X Credential for CSP as Legal Person. + + @param lrn_cred_id: Id of Verifiable Credential attesting CSP's legal registration number. + @return: Gaia-X Credential on CSP as Legal Person as dictionary. + """ + lp_vc = { + '@context': [const.VC_CONTEXT, const.JWS_CONTEXT, const.REG_CONTEXT], + 'type': "VerifiableCredential", + 'id': self.cred_base_url + "/legal_person.json", + 'issuer': self.csp['did'], + 'issuanceDate': str(datetime.now(tz=timezone.utc).isoformat()), + 'credentialSubject': { + "id": self.cred_base_url + "/legal_person.json#subject", + # I think "self.csp['did']" is correct, but Gaia-X expects link, + # "id": self.csp['did'], + "type": "gx:LegalParticipant", + "gx:legalName": self.csp['legal-name'], + "gx:legalRegistrationNumber": { + "id": lrn_cred_id + }, + "gx:headquarterAddress": { + "gx:countrySubdivisionCode": self.csp['legal-address-country-code'] + }, + "gx:legalAddress": { + "gx:countrySubdivisionCode": self.csp['headquarter-address-country-code'] + } + } + } + return crypto.sign_cred(cred=lp_vc, + key=crypto.load_jwk_from_file(self.cred_settings[const.CONFIG_CRED_KEY]), + verification_method=self.cred_settings[const.CONFIG_CRED_VER_METH]) diff --git a/generator/discovery/gxdch_services.py b/generator/discovery/gxdch_services.py new file mode 100644 index 0000000..125a119 --- /dev/null +++ b/generator/discovery/gxdch_services.py @@ -0,0 +1,55 @@ +import json + +import requests + +import generator.common.const as const + + +class ComplianceService: + """ Wrapper class to connect GXDCH Compliance Service. """ + + def __init__(self, api_url: str): + if not api_url: + raise AttributeError("Parameters MUST not be None") + self.api = api_url + + def request_compliance_vc(self, vp: dict, vp_id) -> str: + resp = requests.post(self.api + "/api/credential-offers?vcid=" + vp_id, json.dumps(vp)) + resp.raise_for_status() + return resp.text + + +class NotaryService: + """ Wrapper class to connect GXDCH Notary Service. """ + + def __init__(self, api_url: str): + if not api_url: + raise AttributeError("Parameters MUST not be None") + self.api = api_url + + # TODO: Support all kind of registration numbers + def request_reg_number_vc(self, csp: dict, cred_id: str, cred_subject_id: str) -> dict: + body = { + '@context': const.LRN_CONTEXT, + 'type': "gx:legalRegistrationNumber", + 'id': cred_subject_id, # csp['did'] TODO: I think DID is correct here, but Gaia-X requires credential id, instead of credential subject id + 'gx:vatID': csp[const.CONFIG_CSP_REG_NUMBER][const.CONFIG_CSP_VAT_ID], + } + resp = requests.post(self.api + "/registrationNumberVC?vcid=" + str(cred_id), json=body) + + resp.raise_for_status() + return resp.json() + + +class RegistryService: + """ Wrapper class to connect GXDCH Registry Service. """ + + def __init__(self, api_url: str): + if not api_url: + raise AttributeError("Parameters MUST not be None") + self.api = api_url + + def get_gx_tandc(self) -> dict: + resp = requests.get(self.api + "/api/termsAndConditions") + resp.raise_for_status() + return resp.json() diff --git a/generator/discovery/openstack/openstack_discovery.py b/generator/discovery/openstack/openstack_discovery.py index ab328b5..7f3aa08 100644 --- a/generator/discovery/openstack/openstack_discovery.py +++ b/generator/discovery/openstack/openstack_discovery.py @@ -1,7 +1,4 @@ """"General openstack discovery class. - -(c) Anja Strunk , 2/2024 -SPDX-License-Identifier: EPL-2.0 """ from hashlib import sha256 @@ -29,12 +26,11 @@ def __init__(self, conn: Connection, config: Config) -> None: def discover(self) -> VirtualMachineServiceOffering: """ - Discover all attributes of OS Cloud. + Discover all attributes of OS Cloud as Gaia-X VirtualMachineServiceOffering. - @return: all attributes as list - @rtype List[JsonLdObject] + @return: all attributes as Gaia-X VirtualMachineServiceOffering + @rtype List[dict] """ - images = VmImageDiscovery(self.conn, self.config).discover() flavors = ServerFlavorDiscovery(self.conn, self.config).discover() @@ -64,7 +60,7 @@ def discover(self) -> VirtualMachineServiceOffering: ) service_tac = [] for url in self.config.get_value( - [const.CONFIG_IAAS, const.CONFIG_IAAS_T_AND_C] + [const.CONFIG_IAAS, const.CONFIG_T_AND_C] ): httpResponse = requests.get(url) if httpResponse.status_code == 200: @@ -80,9 +76,9 @@ def discover(self) -> VirtualMachineServiceOffering: + url + "'. HTTP Status code: " + str(httpResponse.status_code) ) - if len(service_tac) == 0: + if not service_tac: raise ValueError( - "Service offerings terms and conditions MUST not be empty. Please check config.yaml. There MUST be at least on entry in " + "Service offerings terms and conditions MUST not be empty. Please check config.yaml. There MUST be at least one entry." + const.CONFIG_IAAS + "." + const.CONFIG_IAAS_T_AND_C ) diff --git a/generator/discovery/openstack/server_flavor_discovery.py b/generator/discovery/openstack/server_flavor_discovery.py index abb6bb3..4b58946 100644 --- a/generator/discovery/openstack/server_flavor_discovery.py +++ b/generator/discovery/openstack/server_flavor_discovery.py @@ -1,7 +1,4 @@ -"""Script to discovery server flavor properties. - -(c) Anja Strunk , 2/2024 -SPDX-License-Identifier: EPL-2.0 +"""Discovery of properties if openstack flavors. """ from typing import List, Optional @@ -60,6 +57,9 @@ class ServerFlavorDiscovery: + """ + Discovery for openstack server flavor properties. + """ def __init__(self, conn: Connection, conf: Config) -> None: self.conn = conn self.conf = conf @@ -229,4 +229,5 @@ def _add_description(self, os_flavor: OS_Flavor, gx_flavor: GX_Flavor) -> None: @param gx_flavor Gaia-X flavor specification @type gx_flavor: ServerFlavor """ - gx_flavor.description = os_flavor.description + if gx_flavor is not None: + gx_flavor.description = os_flavor.description diff --git a/generator/discovery/openstack/vm_images_discovery.py b/generator/discovery/openstack/vm_images_discovery.py index f5250f0..c24e4ce 100644 --- a/generator/discovery/openstack/vm_images_discovery.py +++ b/generator/discovery/openstack/vm_images_discovery.py @@ -1,11 +1,4 @@ -#!/usr/bin/env python3 -# vim: set ts=4 sw=4 et: -# -# vm_images_discovery.py -"""Script to discovery VM images properties. - -(c) Anja Strunk , 1/2024 -SPDX-License-Identifier: EPL-2.0 +"""Discovery for properties of virtual machine images. """ from datetime import datetime from typing import List, Union @@ -22,7 +15,7 @@ DiskBusType, FirmType, HypervisorType, LatestN, MaintenanceSubscription, Memory, MemorySize, OperatingSystem, - RNGTypes, Signature, + OSDistribution, RNGTypes, Signature, SignatureAlgorithm, UpdateFrequency, UpdateStrategy, Validity1, Validity2, VMDiskType) @@ -149,9 +142,9 @@ def _convert_to_gx_image(self, os_image: OS_Image) -> GX_Image: # Discover optional attributes gx_image.vmImageDiskFormat = self._get_disk_format(os_image) gx_image.secureBoot = self._get_secure_boot(os_image) - gx_image.firmwareType = self._get_firme_ware_type(os_image) + gx_image.firmwareType = self._get_firmware_type(os_image) gx_image.watchDogAction = self._get_watchdog_action(os_image) - gx_image.vPMU = self._get_vmpu(os_image) + gx_image.vPMU = self._get_hw_pmu(os_image) gx_image.cpuReq = self._get_cpu_req(os_image) gx_image.multiQueues = self._get_multiqueue_enabled(os_image) gx_image.checksum = self._get_checksum(os_image) @@ -163,7 +156,9 @@ def _convert_to_gx_image(self, os_image: OS_Image) -> GX_Image: gx_image.name = self._get_name(os_image) gx_image.ramReq = self._get_min_ram_req(os_image) gx_image.rootDiskReq = self._get_min_disk_req(os_image) - gx_image.operatingSystem = self._get_operation_system(os_image) + os = self._get_operation_system(os_image) + if os is not None: + gx_image.operatingSystem = os gx_image.buildDate = self._get_build_date(os_image) gx_image.licenseIncluded = self._get_license_included(os_image) gx_image.patchLevel = self._get_patch_level(os_image) @@ -192,7 +187,7 @@ def _get_secure_boot(os_image: OS_Image) -> bool: return bool(os_image.needs_secure_boot) @staticmethod - def _get_firme_ware_type(os_image: OS_Image) -> FirmType: + def _get_firmware_type(os_image: OS_Image) -> FirmType: if ( os_image.properties is not None and "hw_firmware_type" in os_image.properties ): @@ -215,7 +210,7 @@ def _get_watchdog_action(os_image: OS_Image) -> WatchDogActions: return WatchDogActions(WatchDogActions.disabled) @staticmethod - def _get_vmpu(os_image: OS_Image) -> bool: + def _get_hw_pmu(os_image: OS_Image) -> bool: if os_image.properties and "hw_pmu" in os_image.properties: return bool(os_image.properties["hw_pmu"]) else: @@ -294,6 +289,14 @@ def _get_min_disk_req(os_image: OS_Image) -> Disk: def _get_operation_system(self, os_image: OS_Image) -> OperatingSystem: # Copyright owner and license not supported as Image properties, currently --> Default values from config are used + if os_image.os_distro is None: + return OperatingSystem( + version=os_image.os_version, + osDistribution=OSDistribution.others, + resourcePolicy=const.DEFAULT_RESOURCE_POLICY, + copyrightOwnedBy="TBA", + license="https://www.example.com/tba") + if os_image.os_distro.lower() == "arch": return OperatingSystem( version=os_image.os_version, diff --git a/gx-cred-generator.py b/gx-cred-generator.py deleted file mode 100755 index 03b5c79..0000000 --- a/gx-cred-generator.py +++ /dev/null @@ -1,288 +0,0 @@ -#!/usr/bin/env python3 -# vim: set ts=4 sw=4 et: -# -# gx-sd-generator.py -"""Script to generate Gaia-X JSON-LD for credentials. - Uses openstack-discovery.py and k8s-discovery.py to - collect the data. - -(c) Kurt Garloff , 5/2023 -SPDX-License-Identifier: EPL-2.0 -""" - -import getopt -import importlib -import json -import sys -from time import time - -import yaml - -ostack = importlib.import_module("openstack-discovery") -k8s = importlib.import_module("k8s-discovery") - -# Global variables -debug = False -outjson = False -wizard = False -ofile = "/dev/stdout" -indent = " " -uriprefix = "https://scs.community/sd/" -gxid = "test" -svcname = "SCS Test" -gxsvo = "gx:" -policy = "IaaS Test" - - -def valtype(val, typ="xsd:string"): - "Wrapper to return dict pairs with @value and @type as needed in JSON-LD" - return {"@value": val, "@type": typ} - - -def getdocshash(url, hashmeth): - "Calculate hash from terms document at URL" - import requests - - req = requests.get(url) - req.raise_for_status() - # req.raw.decode_content = True - hval = hashmeth(req.content) # .text would be better for HTML - return hval.hexdigest() - - -def getdocsha512(url): - "Calculate SHA512 from terms document at URL" - import hashlib - - return getdocshash(url, hashlib.sha512) - - -def getdocsha256(url): - "Calculate SHA256 from terms document at URL" - import hashlib - - return getdocshash(url, hashlib.sha256) - - -def gettandc(): - "Generate Terms and Conditions structure" - return { - "@type": gxsvo + "SOTermsAndConditions", - gxsvo + "URL": valtype(uriprefix + "terms.pdf"), - gxsvo + "hash": getdocsha256(uriprefix + "terms.pdf"), - } - - -def getdae(): - "Generate dataAccountExport structure" - return { - "@type": gxsvo + "dataAccountExport", - gxsvo + "requestType": "API", - gxsvo + "accessType": "digital", - gxsvo + "formatType": "application/json", - } - - -def getprovby(): - "Generate providedBy structure" - # import requests - # req = requests.get(uriprefix + "participant.json") - # req.raise_for_status() - # return req.json()["selfDescriptionCredential"]["credentialSubject"] - return { - "@id": "https://scs.community/LegalPerson-khnps661nnlgghoq4is18", - "@type": gxsvo + "LegalParticipant", - gxsvo + "legalAddress": { - "@type": gxsvo + "legalAddress", - gxsvo + "countrySubdivisionCode": valtype("DE-BE"), - }, - gxsvo + "headquarterAddress": { - "@type": gxsvo + "legalAddress", - gxsvo + "countrySubdivisionCode": valtype("DE-BE"), - }, - gxsvo + "legalRegistrationNumber": { - "@type": gxsvo + "legalRegistrationNumber", - # At least one of taxID, vatID, EUID, EORI or leiCode must be defined. - gxsvo + "leiCode": valtype("123"), - gxsvo + "vatID": valtype("123"), - gxsvo + "EORI": valtype("123"), - gxsvo + "EUID": valtype("123"), - gxsvo + "taxID": valtype("123"), - }, - } - - -def gxjsonldheader(): - "Dict to generate JSON-LD header for Gaia-X SDs" - import gx_context - - jout = gx_context.gxcontext - jout.update(gx_context.gxtype) - myid = uriprefix + "gxserviceIaaSOfferingOpenStack-" + gxid + f"-{int(time())}.json" - jout["@id"] = myid - if wizard: - del jout["@context"] - jout["id"] = jout.pop("@id") - jout["type"] = jout.pop("@type") - - name = valtype("OpenStack IaaS Service " + svcname) - # svcmodel = valtype("pay per use") - webadr = valtype(uriprefix, "xsd:anyURI") - - # TODO: dependsOn - # TODO: aggregationOf - jout[gxsvo + "providedBy"] = getprovby() - jout[gxsvo + "policy"] = policy - jout[gxsvo + "name"] = name - jout[gxsvo + "webAddress"] = webadr - jout[gxsvo + "termsAndConditions"] = gettandc() - jout[gxsvo + "dataAccountExport"] = getdae() - jout[gxsvo + "SDAutoGeneratedBy"] = ( - "https://github.com/SovereignCloudStack/gx-credential-generator/" - ) - return jout - - -def usage(err=1): - "output help" - print("Usage: gx-sd-generator.py [options] [command]", file=sys.stderr) - print( - "Options: -g/--gaia-x: output Gaia-X JSON-LD instead of YAML (YAML is default)" - ) - print( - " -w/--wizard: remove @context and '@' from @id and @type in generated SD to use it in Gaia-X Wizard" - ) - print(" -j/--json: output compact Gaia-X JSON-LD instead of YAML") - print( - " -f FILE/--file=FILE: write output to FILE_time.yaml/json (default: stdout)" - ) - print( - " -c CLOUD/--os-cloud=CLOUD: use OpenStack cloud CLOUD (default: $OS_CLOUD)" - ) - print( - " -k KCFG/--kubeconfig=KCFG: use kubeconfig file KCFG (default: $KUBECONFIG)" - ) - print(" -K KCTX/--context=KCTX: use kubeconfig context KCTX") - print(" -f FILE/--file=FILE: write output to FILE (default: stdout)") - print( - " -u URI/--uri=URI: use URI prefix. URI/particpant.json and URI/terms.pdf needed" - ) - print(" -n NAME/--name=NAME: use name in self description") - print(" -i ID/--gxid=ID: use ID in self description") - print(" -t TMO/--timeout=TMO: Timeout for the API connections/requests") - print("Command: openstack: collect OpenStack data (default)") - print(" k8s: collect Kubernetes data") - sys.exit(err) - - -def output(mycloud=None, myk8s=None): - "Compose string to be output" - if outjson: - dct = gxjsonldheader() - if mycloud: - prefix = "ex:" - dct[prefix + "OpenStackService"] = mycloud.values(prefix=prefix) - if myk8s: - dct[gxsvo + "K8sClusterService"] = myk8s.values() - return json.dumps(dct, indent=indent) - dct = {} - if mycloud: - dct["openstack"] = mycloud.values() - if myk8s: - dct["k8sCluster"] = myk8s.values() - yaml.emitter.Emitter.process_tag = noop - return yaml.dump(dct, default_flow_style=False) - - -def noop(self, *args, **kw): - pass - - -def main(argv): - "Entry point for main program" - global debug, ofile, outjson, indent, wizard - global uriprefix, gxid, svcname - timeout = 12 - mycloud = None - myk8s = None - # Defaults for OpenStack and K8s are set by imports - # Arg parser - try: - args: list[str] - opts, args = getopt.gnu_getopt( - argv[1:], - "c:f:hgwjdu:n:i:t:k:K:", - ( - "os-cloud=", - "file=", - "help", - "gaia-x", - "wizard", - "json", - "debug", - "uri=", - "name=", - "id=", - "timeout=", - "kubeconfig=", - "context=", - ), - ) - except getopt.GetoptError: # as exc: - usage(1) - for opt in opts: - if opt[0] == "-h" or opt[0] == "--help": - usage(0) - elif opt[0] == "-c" or opt[0] == "--os-cloud": - ostack.cloud = opt[1] - elif opt[0] == "-f" or opt[0] == "--file": - ofile = opt[1] - elif opt[0] == "-u" or opt[0] == "--uri": - uriprefix = opt[1] - elif opt[0] == "-n" or opt[0] == "--name": - svcname = opt[1] - elif opt[0] == "-i" or opt[0] == "--id": - gxid = opt[1] - elif opt[0] == "-d" or opt[0] == "--debug": - debug = True - elif opt[0] == "-g" or opt[0] == "--gaia-x": - outjson = True - ostack.outjson = True - elif opt[0] == "-w" or opt[0] == "--wizard": - wizard = True - elif opt[0] == "-t" or opt[0] == "--timeout": - timeout = int(opt[1]) - elif opt[0] == "-j" or opt[0] == "--json": - outjson = True - ostack.outjson = True - indent = None - elif opt[0] == "-k" or opt[0] == "--kubeconfig": - k8s.config = opt[1] - elif opt[0] == "-K" or opt[0] == "--context": - k8s.context = opt[1] - - if not args or args[0] == "openstack": # default - if ostack.cloud: - conn = ostack.ostackconn(ostack.cloud, timeout) - mycloud = ostack.osCloud(conn) - elif args[0] == "k8s": - if k8s.config: - conn = k8s.kubeconn(k8s.config, k8s.context, timeout) - # conn = k8s.kubeconn(k8s.config, ctx) - myk8s = k8s.KubeCluster(conn) - else: - usage(1) - if not mycloud and not myk8s: - usage(1) - if ofile == "/dev/stdout": - print(output(mycloud, myk8s), file=sys.stdout) - else: - if ostack.outjson: - file_name = f"{ofile}_{int(time())}.json" - else: - file_name = f"{ofile}_{int(time())}.yaml" - print(output(mycloud, myk8s), file=open(file_name, mode="a", encoding="UTF-8")) - - -if __name__ == "__main__": - main(sys.argv) diff --git a/gx_context.py b/gx_context.py deleted file mode 100755 index 98687ff..0000000 --- a/gx_context.py +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env python3 -# vim: set ts=4 sw=4 et: -# -# gx-context.py -""" -Some settings for the JSON-LD SDs needed in Gaia-X - -(c) Kurt Garloff , 6/2022 -SPDX-License-Identifier: EPL-2.0 -""" -gxcontext = { - "@context": { - "gx": "https://https://registry.lab.gaia-x.eu//v1$/gx#", - "xsd": "http://www.w3.org/2001/XMLSchema#", - } - -} -gxtype = {"@type": "gx:ServiceOffering"} diff --git a/openstack-discovery.py b/openstack-discovery.py deleted file mode 100755 index f6f2eb0..0000000 --- a/openstack-discovery.py +++ /dev/null @@ -1,653 +0,0 @@ -#!/usr/bin/env python3 -# vim: set ts=4 sw=4 et: -# -# openstack-discovery.py -""" -Talk to OpenStack APIs to discover environment -Save discovered information in classes that reflect G-X attributes -These can then be dumped as YAML or as Gaia-X JSON-LD Self-Description. - -(c) Kurt Garloff , 3/2022 - 6/2022 -(c) Mathias Fechner , 3/2022 -SPDX-License-Identifier: EPL-2.0 -""" - -import getopt -import json -import os -import sys - -import openstack -import yaml - -# Global variables -if "OS_CLOUD" in os.environ: - cloud = os.environ["OS_CLOUD"] -else: - cloud = "" -debug = False -outjson = False -ofile = "/dev/stdout" -indent = " " -errors = 0 - - -def valtype(val, typ="xsd:string"): - "Wrapper to return dict pairs with @value and @type as needed in JSON-LD" - return {"@value": val, "@type": typ} - - -def appenddicts(dct1, *kwd): - "Return dict d1 with items from kwd added" - dct = dct1 - for k in kwd: - assert k is not None - dct.update(k) - return dct - - -def add_prefix_to_dict_keys(d, prefix): - res = dict() - for key in d.keys(): - pre_key = key - if not key.startswith("@"): - pre_key = prefix + key - - if isinstance(d[key], dict): - res[pre_key] = add_prefix_to_dict_keys(d[key], prefix) - elif isinstance(d[key], list): - res_list = list() - for item in d[key]: - if isinstance(item, dict): - res_list.append(add_prefix_to_dict_keys(item, prefix)) - else: - res_list.append(item) - res[pre_key] = res_list - else: - res[pre_key] = d[key] - return res - - -def versinfo(connprox, stype, region): - "Get list of supported versions and microversions from OS service" - vrdata = connprox.get_all_version_data(region_name=region) - for reg in vrdata: - try: - vdata = vrdata[reg]["public"] - break - except AttributeError: - pass - for keys in vdata: - versdata = vdata[keys] - if keys == stype: - break - vinfo = list( - map( - lambda x: { - "version": x.version, - "url": x.url, - "status": x.status, - "min_microversion": x.min_microversion, - "max_microversion": x.max_microversion, - }, - filter(lambda x: x.max_microversion, versdata), - ) - ) - vinfo.extend( - list( - map( - lambda x: {"version": x.version, "url": x.url, "status": x.status}, - filter(lambda x: not x.max_microversion, versdata), - ) - ) - ) - return vinfo - - -class osServiceCat: - "OpenStack service catalog" - - def __init__(self, dct, regfilter): - self.id = dct["id"] - self.region = regfilter - self.ep = None - # interface: we only look at public - for ept in dct["endpoints"]: - if ept["interface"] == "public" and ( - not regfilter or ept["region_id"] == regfilter - ): - self.ep = ept["url"] - self.type = dct["type"] - self.name = dct["name"] - self.consumed = False - - def __str__(self): - "textual representation" - return f"{self.type}|{self.name}|{self.ep}" - - # def __repr__(self): - # return str(self) - - -class osService: - "A generic openStack service, with a proxy connection object from SDK" - - def __init__(self, conn, stype, name, region, prj_id, ept, quiet=True): - "c'tor for the OpenStack service to be caled by subclasses" - global errors - self.fulltype = stype - if stype[-2] == "v" and stype[-1].isnumeric(): - stype = stype[:-2] - stype = stype.replace("-", "_") - self.stype = stype - self.region = region - self.conn = None - self.apiver = None - self.ep = None - self.versdata = None - self.extensions = None - self.azs = None - self.name = name - try: - if debug: - print(f"#INFO: Creating Conn for {stype}:{name}", file=sys.stderr) - self.conn = conn.__getattribute__(stype) - self.conn.service_name = name - self.conn.region_name = region - self.apiver = self.conn.get_api_major_version() - except Exception as exc: - if not quiet: - print( - f"#ERROR: No service proxy of type {stype} in region {region} in SDK.\n{exc}", - file=sys.stderr, - ) - errors += 1 - # return - if self.conn is None or (self.conn.version is None and self.apiver is None): - print( - f"#WARNING: Skipping over {stype} in region {region} b/c it advertizes no version", - file=sys.stderr, - ) - self.conn = None - return - try: - self.ep = self.conn.get_endpoint().replace(prj_id, "${OS_PROJECT_ID}") - except Exception: # as exc: - if stype == "identity": - self.ep = conn.auth["auth_url"] - else: - if self.conn: - print( - f"#ERROR: No endpoint found for service {self.stype} in region {region}", - file=sys.stderr, - ) - errors += 1 - # raise exc - self.conn = None - return - self.versdata = versinfo(self.conn, self.fulltype, region) - try: - # self.extensions = list(map(lambda x: x.name, self.conn.extensions())) - self.extensions = list(map(lambda x: x.alias, self.conn.extensions())) - except Exception as exc: - if not quiet: - print( - f"#WARNING: Service {self.fulltype} in region {region} does not support getting extensions.\n{exc}", - file=sys.stderr, - ) - - try: - self.azs = list( - filter( - lambda x: x.state["available"] is True, - self.conn.availability_zones(), - ) - ) - except Exception: - try: - self.azs = list( - filter( - lambda x: x.state == "available", self.conn.availability_zones() - ) - ) - except Exception as exc: - if not quiet: - print( - f"#WARNING: Service {self.fulltype} in region {region} does not support getting AZs.\n{exc}", - file=sys.stderr, - ) - # FIXME: We are not using ept, is that right? - try: - self.conn.get("/") - # We could check for a 200, but even if a 'GET /' did not succeed, we could establish a conn - except Exception: # as exc: - print( - f"#ERROR: Service {self.name} in region {region}: Could not connect to {self.ep}", - file=sys.stderr, - ) - self.conn = None - # Will not normally be seen: - self.ep = "ERROR: " + self.ep - errors += 1 - - def values(self): - "return dict representing stored values" - dct = { - self.stype: { - "name": self.name, - "endpoint": self.ep, - # "region": self.region, - "versions": self.versdata, - } - } - if self.extensions: - dct[self.stype]["extensions"] = self.extensions - if self.azs: - # dct[self.stype]["availability_zones"] = list(map(lambda x: x.name, self.azs)) - dct[self.stype]["availability_zones"] = [x.name for x in self.azs] - return dct - - def __str__(self): - "textual representation" - return str(self.values()) - - -class osFlavor: - "Abstraction for flavors" - - def __init__(self, flv): - self.name = flv["name"] - # Note: cpuType, cpuGeneration, diskType are MR34 ideas, - # slightly different from and less comprehensive than - # the abstraction in SCS flavor spec. Convert later. - self.cpuType = "" - self.cpuGeneration = "" - self.numberOfvCPUs = flv["vcpus"] - self.ramSize = flv["ram"] # MiB - self.diskSize = flv["disk"] # GB - self.diskType = "" - - def values(self): - ydct = dict( - name=self.name, - numberOfvCPUs=self.numberOfvCPUs, - ramSize=dict(Value=self.ramSize / 1024, Unit="GiB"), - ) - - if self.diskSize: - ydct["diskSize"] = dict(Value=self.diskSize, Unit="GB") - # TODO: cpuType, cpuGen, diskType output - return ydct - - -class osCompute(osService): - "Abstraction for compute service (nova)" - - svcID = ("compute", "computev2", "compute_legacy") - - # def __init__(self, conn, region="RegionOne"): - def __init__(self, conn, stype, name, region, prj_id, ept): - super().__init__(conn, stype, name, region, prj_id, ept, False) - if not self.conn: - self.flavors = None - return - self.flavors = self.get_openstack_flavors() - - def values(self): - dct = super().values() - if self.flavors: - dct[self.stype]["flavors"] = self.flavors - return dct - - def get_openstack_flavors(self): - """Use OpenStack conn (global var conn) to get flavor list from - compute service. Populate flavor list.""" - flvs = [] - for flv_id in self.conn.flavors(id): - flvs.append(osFlavor(flv_id).values()) - # TODO: - # (a) parse extra specs if any - # (b) parse SCS flavor names - self.flavors = flvs - return flvs - - -class azInfo: - "Convert zoneXxx dict into class with Xxx attributes for availability zones" - - def __init__(self, dct): - for key in dct: - if key[:4] == "zone": - newkey = key[4].lower() + key[5:] - else: - newkey = key - self.__setattr__(newkey, dct[key]) - - -class osVolume(osService): - "osService specialization for volumes (cinderv3)" - - svcID = ("volumev3", "volumev2", "volume") - - def __init__(self, conn, stype, name, region, prj_id, ept): - super().__init__(conn, stype, name, region, prj_id, ept, True) - if not self.conn: - self.voltypes = None - return - self.voltypes = list(self.conn.types()) - # TODO: Fixup azs and exts - req = self.conn.request("/extensions", "GET") - ext = json.loads(req.text)["extensions"] - self.extensions = list(map(lambda x: x["alias"], ext)) - req = self.conn.request("/os-availability-zone", "GET") - azs = json.loads(req.text)["availabilityZoneInfo"] - # self.azs = list(map(lambda x: azInfo(x), filter(lambda x: x["zoneState"]["available"] is True, azs))) - self.azs = [azInfo(x) for x in azs if x["zoneState"]["available"]] - - def values(self): - dct = super().values() - # dct[self.stype]["volume_types"] = list(map( - # lambda x: {"name": x.name, "public": x.is_public}, self.voltypes)) - if self.voltypes: - dct[self.stype]["volume_types"] = [ - {"name": x.name, "public": x.is_public} for x in self.voltypes - ] - return dct - - -class osLoadBalancer(osService): - "osService specialization for load balancers" - - svcID = ("load_balancer", "load-balancer") - - def __init__(self, conn, stype, name, region, prj_id, ept): - super().__init__(conn, stype, name, region, prj_id, ept, True) - try: - self.flavors = self.conn.flavors() - self.flavors = list( - map( - lambda x: {"name": x.name, "description": x.description}, - filter(lambda x: x.is_enabled, self.flavors), - ) - ) - except Exception as exc: - print(exc, file=sys.stderr) - self.flavors = None - try: - self.providers = self.conn.providers() - self.providers = list( - map( - lambda x: {"name": x.name, "description": x.description}, - self.providers, - ) - ) - except Exception as exc: - print(exc, file=sys.stderr) - self.providers = None - - def values(self): - dct = super().values() - if self.providers: - dct[self.stype]["providers"] = self.providers - if self.flavors: - dct[self.stype]["flavors"] = self.flavors - return dct - - -# TODO: List of public images with properties - - -def ext_net_dict(net): - "Return dict with extNet properties" - dct = {"name": net.name, "mtu": net.mtu, "shared": net.is_shared} - if net.description: - dct["description"] = net.descrption - return dct - - -# TODO: Network class needs more detection; have AZs for routers and nets, endpoint not per region -class osNetwork(osService): - "osService specialization for network" - - svcID = "network" - - def __init__(self, conn, stype, name, region, prj_id, ept): - super().__init__(conn, stype, name, region, prj_id, ept, True) - self.ep = ept - # TODO: Fixup AZs - # external network list - self.extnet = list( - filter( - lambda x: x.is_router_external and x.is_admin_state_up, - self.conn.networks(), - ) - ) - self.extnet = list(map(ext_net_dict, self.extnet)) - - def values(self): - dct = super().values() - if self.extnet: - dct[self.stype]["externalNetworks"] = self.extnet - return dct - - -# Known-classes -OSClasses = [osCompute, osVolume, osLoadBalancer, osNetwork] - - -class nonOSService: - "Non-OpenStack services listed in the service catalogue" - - def __init__(self, stype, name, url): - self.stype = stype - self.name = name - self.ep = url - - def values(self): - "return dict with stored values" - return {self.stype: {"name": self.name, "endpoint": self.ep}} - - def __str__(self): - "textual representation" - return str(self.values()) - - -class osCloud: - "Abstraction for openStack cloud with all its services" - - def __init__(self, conn): - # import copy - self.conn = conn - self.auth = conn.auth - self.regions = list(conn.identity.regions()) - # Create per region service catalogs - self.regcat = {} - for region in self.regions: - reg = region.id - self.regcat[reg] = [] - for svc in conn.service_catalog: - svccat = osServiceCat(svc, reg) - if svccat.ep: - self.regcat[reg].append(svccat) - if debug: - print(f"#DEBUG: Svc Cat region {reg}: {self.regcat}", file=sys.stderr) - # Well-known OpenStack services - self.ostacksvc = {} - if "project_id" in conn.auth: - prj_id = conn.auth["project_id"] - else: - prj_id = conn.identity.get_project_id() - # Iterate over regions - for region in self.regions: - # Keep list of already handled services to avoid duplicates/aliases - handled = [] - # Dictionary to collect OpenStack services - ostacksvc = {} - reg = region.id - if debug: - print( - f"#INFO: Creation service catalog for region {reg}", file=sys.stderr - ) - # Iterate over service catalog - for svc in self.regcat[reg]: - assert svc.ep - assert reg == svc.region - # Treating those two legacy services as non-OpenStack (just list EPs) - if svc.type in [*handled, "compute_legacy", "cloudformation"]: - continue - newsvc = None - for osClass in OSClasses: - if svc.type in osClass.svcID: - newsvc = osClass(conn, svc.type, svc.name, reg, prj_id, svc.ep) - handled.extend(osClass.svcID) - break - if not newsvc: - newsvc = osService(conn, svc.type, svc.name, reg, prj_id, svc.ep) - handled.extend( - ( - svc.type, - newsvc.stype, - ) - ) - # Only attach if conn is non-empty - if newsvc.conn: - ostacksvc[newsvc.stype] = newsvc - if debug: - print( - f"#DEBUG: Region {reg} added OS Svc {newsvc}", - file=sys.stderr, - ) - svc.consumed = True - elif debug: - print( - f"#DEBUG: Region {reg} with service {newsvc} without connection", - file=sys.stderr, - ) - # Handle remaining services that are listed - for svc in self.regcat[reg]: - if not svc.consumed and svc.type not in ostacksvc: - ostacksvc[svc.type] = nonOSService( - svc.type, svc.name, svc.ep.replace(prj_id, "${OS_PROJECT_ID}") - ) - if debug: - print( - f"#DEBUG: Region {reg} added Non-OS {ostacksvc[svc.type]}", - file=sys.stderr, - ) - svc.consumed = True - self.ostacksvc[reg] = ostacksvc - # TODO: Iterate over non-consumed services (global, non-region specifc) - - def values(self, prefix=""): - "dict representing stored data" - inner = {"regions": list(map(lambda x: x.id, self.regions))} - if outjson: - inner["auth_url"] = valtype(self.auth["auth_url"], "xsd:anyURI") - else: - inner["auth_url"] = self.auth["auth_url"] - for reg, ostacksvc in self.ostacksvc.items(): - inner[reg] = {} - for svckey in ostacksvc: - svc = ostacksvc[svckey] - inner[reg] = appenddicts(inner[reg], svc.values()) - if outjson: - inner[reg][svc.stype]["endpoint"] = valtype(svc.ep, "xsd:anyURI") - if prefix: - return add_prefix_to_dict_keys(inner, prefix) - return inner - - def __str__(self): - # print(self.values()) - if outjson: - return json.dumps({"OpenStackService": self.values()}, indent=indent) - return yaml.dump({"openstack": self.values()}, default_flow_style=False) - - -def usage(err=1): - "output help" - print("Usage: openstack-discovery.py [options]", file=sys.stderr) - print("Options: -j/--json: output GX JSON-LD instead of YAML") - print(" -J/--json-compact: output compact GX JSON-LD instead of YAML") - print(" -f FILE/--file=FILE: write output to FILE (default: stdout)") - print( - " -c CLOUD/--os-cloud=CLOUD: use OpenStack cloud CLOUD (default: $OS_CLOUD)" - ) - print(" -f FILE/--file=FILE: write output to FILE (default: stdout)") - print(" -t TMO/--timeout=TMO: Timeout for the API connections/requests") - if not cloud: - print( - "You need to have OS_CLOUD set or pass --os-cloud=CLOUD.", file=sys.stderr - ) - sys.exit(err) - - -def ostackconn(cloud, timeout): - "Establish connection to OpenStack cloud cloud (timeout timeout)" - conn = openstack.connect( - cloud=cloud, timeout=timeout, api_timeout=timeout * 1.5 + 4 - ) - try: - conn.authorize() - except Exception: - print("INFO: Retry connection with 'default' domain", file=sys.stderr) - conn = openstack.connect( - cloud=cloud, - timeout=timeout, - api_timeout=timeout * 1.5 + 4, - default_domain="default", - project_domain_id="default", - ) - conn = openstack.connect(cloud=cloud, timeout=timeout, api_timeout=timeout * 1.5 + 4, - default_domain='default', project_domain_id='default') - conn.authorize() - return conn - - -def main(argv): - "Entry point for main program" - global cloud, outjson, indent - global debug, ofile, errors - timeout = 12 - try: - opts, args = getopt.gnu_getopt( - argv[1:], - "c:f:hjJdt:", - ("os-cloud=", "file=", "help", "json", "json-compact", "debug", "timeout="), - ) - except getopt.GetoptError: # as exc: - usage(1) - for opt in opts: - if opt[0] == "-h" or opt[0] == "--help": - usage(0) - elif opt[0] == "-c" or opt[0] == "--os-cloud": - cloud = opt[1] - elif opt[0] == "-f" or opt[0] == "--file": - ofile = opt[1] - elif opt[0] == "-d" or opt[0] == "--debug": - debug = True - elif opt[0] == "-t" or opt[0] == "--timeout": - timeout = int(opt[1]) - elif opt[0] == "-j" or opt[0] == "--json": - outjson = True - elif opt[0] == "-J" or opt[0] == "--json-compact": - outjson = True - indent = None - if args: - usage(1) - if not cloud: - print( - "ERROR: You need to have OS_CLOUD set or pass --os-cloud=CLOUD.", - file=sys.stderr, - ) - errors += 1 - conn = ostackconn(cloud, timeout) - mycloud = osCloud(conn) - if ofile == "/dev/stdout": - print(mycloud, file=sys.stdout) - else: - print(mycloud, file=open(ofile, "a", encoding="UTF-8")) - return errors - - -if __name__ == "__main__": - sys.exit(main(sys.argv)) diff --git a/requirements.in b/requirements.in index 572b70b..1c10057 100644 --- a/requirements.in +++ b/requirements.in @@ -4,3 +4,4 @@ click linkml requests jwcrypto +pyld \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 38c3bf4..df32e44 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,7 +20,9 @@ attrs==23.2.0 # jsonschema # referencing cachetools==5.3.3 - # via google-auth + # via + # google-auth + # pyld certifi==2024.7.4 # via # kubernetes @@ -64,6 +66,8 @@ exceptiongroup==1.2.0 # via pytest fqdn==1.5.1 # via jsonschema +frozendict==2.4.4 + # via pyld google-auth==2.29.0 # via kubernetes graphviz==0.20.3 @@ -137,6 +141,8 @@ linkml-runtime==1.7.5 # via # linkml # linkml-dataops +lxml==5.3.0 + # via pyld markupsafe==2.1.5 # via jinja2 netifaces==0.11.0 @@ -193,6 +199,8 @@ pyjsg==0.11.10 # linkml # pyshexc # shexjsg +pyld==2.0.4 + # via -r requirements.in pyparsing==3.1.2 # via rdflib pyshex==0.8.1 diff --git a/results/osdescriptor_1683188130.json b/results/osdescriptor_1683188130.json deleted file mode 100644 index 6f2d229..0000000 --- a/results/osdescriptor_1683188130.json +++ /dev/null @@ -1,1178 +0,0 @@ -{ - "@context": [ - "http://www.w3.org/ns/shacl#", - "http://www.w3.org/2001/XMLSchema#", - "http://w3id.org/gaia-x/resource#", - "http://w3id.org/gaia-x/participant#", - "http://w3id.org/gaia-x/service-offering#" - ], - "@type": [ - "VerifiableCredential", - "ServiceOfferingExperimental" - ], - "@id": "https://scs.community/sd/gxserviceIaaSOfferingOpenStack-test-1683188130.json", - "credentialSubject": { - "id": "https://scs.community/sd/gxserviceIaaSOfferingOpenStack-test-1683188130.json", - "gx-service-offering:providedBy": { - "@value": "https://scs.community/sd/participant.json", - "@type": "xsd:string" - }, - "gx-service-offering:name": { - "@value": "OpenStack IaaS Service SCS Test", - "@type": "xsd:string" - }, - "gx-service-offering:webAddress": { - "@value": "https://scs.community/sd/", - "@type": "xsd:anyURI" - }, - "gx-service-offering:TermsAndConditions": { - "gx-service-offering:url": { - "@value": "https://scs.community/sd/terms.pdf", - "@type": "xsd:string" - }, - "gx-service-offering:hash": { - "@value": "0e8c5aa80b7d2bf06b1ec42399c56d30424872333001708c958e0b863c34a758", - "@type": "xsd:string" - } - }, - "gx-service-offering:SDAutoGeneratedBy": "https://github.com/SovereignCloudStack/gx-self-description-generator/", - "gxsvo+OpenStackService": { - "regions": [ - "RegionOne" - ], - "auth_url": { - "@value": "https://api.gx-scs.sovereignit.cloud:5000", - "@type": "xsd:anyURI" - }, - "RegionOne": { - "identity": { - "name": "keystone", - "endpoint": { - "@value": "https://api.gx-scs.sovereignit.cloud:5000", - "@type": "xsd:anyURI" - }, - "versions": [ - { - "version": "3.14", - "url": "https://api.gx-scs.sovereignit.cloud:5000/v3/", - "status": "CURRENT" - } - ] - }, - "dns": { - "name": "designate", - "endpoint": { - "@value": "https://api.gx-scs.sovereignit.cloud:9001/v2", - "@type": "xsd:anyURI" - }, - "versions": [ - { - "version": "2.0", - "url": "https://api.gx-scs.sovereignit.cloud:9001/v2", - "status": "SUPPORTED" - }, - { - "version": "2.0", - "url": "https://api.gx-scs.sovereignit.cloud:9001/v2", - "status": "CURRENT" - } - ] - }, - "key_manager": { - "name": "barbican", - "endpoint": { - "@value": "https://api.gx-scs.sovereignit.cloud:9311/v1/", - "@type": "xsd:anyURI" - }, - "versions": [ - { - "version": "1.0", - "url": "https://api.gx-scs.sovereignit.cloud:9311/v1/", - "status": "CURRENT" - } - ] - }, - "object_store": { - "name": "swift", - "endpoint": { - "@value": "https://api.gx-scs.sovereignit.cloud:8080/swift/v1/AUTH_${OS_PROJECT_ID}", - "@type": "xsd:anyURI" - }, - "versions": [ - { - "version": "1.0", - "url": "https://api.gx-scs.sovereignit.cloud:8080/swift/v1/", - "status": "CURRENT" - } - ] - }, - "network": { - "name": "neutron", - "endpoint": { - "@value": "https://api.gx-scs.sovereignit.cloud:9696", - "@type": "xsd:anyURI" - }, - "versions": [ - { - "version": "2.0", - "url": "https://api.gx-scs.sovereignit.cloud:9696/v2.0/", - "status": "CURRENT" - } - ], - "extensions": [ - "address-group", - "address-scope", - "agent", - "allowed-address-pairs", - "auto-allocated-topology", - "availability_zone", - "default-subnetpools", - "dns-integration", - "dns-domain-ports", - "dns-integration-domain-keywords", - "expose-port-forwarding-in-fip", - "external-net", - "extra_dhcp_opt", - "extraroute", - "filter-validation", - "floating-ip-port-forwarding-description", - "floating-ip-port-forwarding-detail", - "floating-ip-port-forwarding-port-ranges", - "fip-port-details", - "flavors", - "floating-ip-port-forwarding", - "floatingip-pools", - "router", - "ext-gw-mode", - "multi-provider", - "net-mtu", - "net-mtu-writable", - "network_availability_zone", - "network-ip-availability", - "pagination", - "port-device-profile", - "port-mac-address-regenerate", - "port-numa-affinity-policy", - "port-resource-request", - "port-resource-request-groups", - "binding", - "binding-extended", - "port-security", - "project-id", - "provider", - "qos", - "qos-bw-limit-direction", - "qos-bw-minimum-ingress", - "qos-default", - "qos-fip", - "qos-gateway-ip", - "qos-port-network-policy", - "qos-pps-minimum", - "qos-pps-minimum-rule-alias", - "qos-pps", - "qos-rule-type-details", - "qos-rule-type-filter", - "qos-rules-alias", - "quota-check-limit", - "quotas", - "quota_details", - "rbac-policies", - "rbac-address-scope", - "rbac-security-groups", - "revision-if-match", - "standard-attr-revisions", - "router_availability_zone", - "security-groups-normalized-cidr", - "security-groups-remote-address-group", - "security-groups-shared-filtering", - "security-group", - "service-type", - "sorting", - "standard-attr-description", - "stateful-security-group", - "subnet-dns-publish-fixed-ip", - "subnet-service-types", - "subnet_allocation", - "standard-attr-tag", - "standard-attr-timestamp" - ], - "availability_zones": [ - "nova" - ] - }, - "volume": { - "name": "cinderv3", - "endpoint": { - "@value": "https://api.gx-scs.sovereignit.cloud:8776/v3/${OS_PROJECT_ID}", - "@type": "xsd:anyURI" - }, - "versions": [ - { - "version": "3.0", - "url": "https://api.gx-scs.sovereignit.cloud:8776/v3/", - "status": "CURRENT", - "min_microversion": "3.0", - "max_microversion": "3.70" - } - ], - "extensions": [ - "os-volume-transfer", - "backups", - "encryption", - "scheduler-stats", - "OS-SCH-HNT", - "cgsnapshots", - "qos-specs", - "os-vol-tenant-attr", - "os-quota-sets", - "os-snapshot-unmanage", - "os-extended-snapshot-attributes", - "os-admin-actions", - "os-snapshot-actions", - "os-extended-services", - "os-vol-host-attr", - "os-types-extra-specs", - "os-volume-unmanage", - "os-quota-class-sets", - "os-volume-encryption-metadata", - "os-vol-mig-status-attr", - "capabilities", - "os-volume-actions", - "os-services", - "os-types-manage", - "os-volume-type-access", - "os-used-limits", - "os-hosts", - "os-snapshot-manage", - "os-vol-image-meta", - "os-availability-zone", - "os-volume-manage", - "consistencygroups" - ], - "availability_zones": [ - "nova" - ], - "volume_types": [ - { - "name": "LUKS", - "public": true - }, - { - "name": "__DEFAULT__", - "public": true - } - ] - }, - "compute": { - "name": "nova", - "endpoint": { - "@value": "https://api.gx-scs.sovereignit.cloud:8774/v2.1", - "@type": "xsd:anyURI" - }, - "versions": [ - { - "version": "2.1", - "url": "https://api.gx-scs.sovereignit.cloud:8774/v2.1/", - "status": "CURRENT", - "min_microversion": "2.1", - "max_microversion": "2.93" - }, - { - "version": "2.0", - "url": "https://api.gx-scs.sovereignit.cloud:8774/v2/", - "status": "SUPPORTED" - } - ], - "extensions": [ - "NMN", - "OS-DCF", - "OS-EXT-AZ", - "OS-EXT-IMG-SIZE", - "OS-EXT-IPS", - "OS-EXT-IPS-MAC", - "OS-EXT-SRV-ATTR", - "OS-EXT-STS", - "OS-FLV-DISABLED", - "OS-FLV-EXT-DATA", - "OS-SCH-HNT", - "OS-SRV-USG", - "os-access-ips", - "os-admin-actions", - "os-admin-password", - "os-agents", - "os-aggregates", - "os-assisted-volume-snapshots", - "os-attach-interfaces", - "os-availability-zone", - "os-baremetal-ext-status", - "os-baremetal-nodes", - "os-block-device-mapping", - "os-block-device-mapping-v2-boot", - "os-cell-capacities", - "os-cells", - "os-certificates", - "os-cloudpipe", - "os-cloudpipe-update", - "os-config-drive", - "os-console-auth-tokens", - "os-console-output", - "os-consoles", - "os-create-backup", - "os-create-server-ext", - "os-deferred-delete", - "os-evacuate", - "os-extended-evacuate-find-host", - "os-extended-floating-ips", - "os-extended-hypervisors", - "os-extended-networks", - "os-extended-quotas", - "os-extended-rescue-with-image", - "os-extended-services", - "os-extended-services-delete", - "os-extended-status", - "os-extended-volumes", - "os-fixed-ips", - "os-flavor-access", - "os-flavor-extra-specs", - "os-flavor-manage", - "os-flavor-rxtx", - "os-flavor-swap", - "os-floating-ip-dns", - "os-floating-ip-pools", - "os-floating-ips", - "os-floating-ips-bulk", - "os-fping", - "os-hide-server-addresses", - "os-hosts", - "os-hypervisor-status", - "os-hypervisors", - "os-instance-actions", - "os-instance_usage_audit_log", - "os-keypairs", - "os-lock-server", - "os-migrate-server", - "os-migrations", - "os-multiple-create", - "os-networks", - "os-networks-associate", - "os-pause-server", - "os-personality", - "os-preserve-ephemeral-rebuild", - "os-quota-class-sets", - "os-quota-sets", - "os-rescue", - "os-security-group-default-rules", - "os-security-groups", - "os-server-diagnostics", - "os-server-external-events", - "os-server-group-quotas", - "os-server-groups", - "os-server-list-multi-status", - "os-server-password", - "os-server-sort-keys", - "os-server-start-stop", - "os-services", - "os-shelve", - "os-simple-tenant-usage", - "os-suspend-server", - "os-tenant-networks", - "os-used-limits", - "os-used-limits-for-admin", - "os-user-data", - "os-user-quotas", - "os-virtual-interfaces", - "os-volume-attachment-update", - "os-volumes" - ], - "availability_zones": [ - "nova" - ], - "flavors": [ - { - "name": "SCS-1V:8", - "numberOfvCPUs": 1, - "ramSize": { - "Value": 8.0, - "Unit": "GiB" - } - }, - { - "name": "SCS-4V:16:50", - "numberOfvCPUs": 4, - "ramSize": { - "Value": 16.0, - "Unit": "GiB" - }, - "diskSize": { - "Value": 50, - "Unit": "GB" - } - }, - { - "name": "SCS-1V:4:10", - "numberOfvCPUs": 1, - "ramSize": { - "Value": 4.0, - "Unit": "GiB" - }, - "diskSize": { - "Value": 10, - "Unit": "GB" - } - }, - { - "name": "SCS-16V:32", - "numberOfvCPUs": 16, - "ramSize": { - "Value": 32.0, - "Unit": "GiB" - } - }, - { - "name": "SCS-4V:8:100", - "numberOfvCPUs": 4, - "ramSize": { - "Value": 8.0, - "Unit": "GiB" - }, - "diskSize": { - "Value": 100, - "Unit": "GB" - } - }, - { - "name": "SCS-4V:32:100", - "numberOfvCPUs": 4, - "ramSize": { - "Value": 32.0, - "Unit": "GiB" - }, - "diskSize": { - "Value": 100, - "Unit": "GB" - } - }, - { - "name": "SCS-1V:8:20", - "numberOfvCPUs": 1, - "ramSize": { - "Value": 8.0, - "Unit": "GiB" - }, - "diskSize": { - "Value": 20, - "Unit": "GB" - } - }, - { - "name": "SCS-8V:32:100", - "numberOfvCPUs": 8, - "ramSize": { - "Value": 32.0, - "Unit": "GiB" - }, - "diskSize": { - "Value": 100, - "Unit": "GB" - } - }, - { - "name": "SCS-1V:1", - "numberOfvCPUs": 1, - "ramSize": { - "Value": 1.0, - "Unit": "GiB" - } - }, - { - "name": "SCS-1V:2:5", - "numberOfvCPUs": 1, - "ramSize": { - "Value": 2.0, - "Unit": "GiB" - }, - "diskSize": { - "Value": 5, - "Unit": "GB" - } - }, - { - "name": "SCS-4V:32", - "numberOfvCPUs": 4, - "ramSize": { - "Value": 32.0, - "Unit": "GiB" - } - }, - { - "name": "SCS-1V:4", - "numberOfvCPUs": 1, - "ramSize": { - "Value": 4.0, - "Unit": "GiB" - } - }, - { - "name": "SCS-1V:0.5:20", - "numberOfvCPUs": 1, - "ramSize": { - "Value": 0.5, - "Unit": "GiB" - }, - "diskSize": { - "Value": 20, - "Unit": "GB" - } - }, - { - "name": "SCS-2V:4:100", - "numberOfvCPUs": 2, - "ramSize": { - "Value": 4.0, - "Unit": "GiB" - }, - "diskSize": { - "Value": 100, - "Unit": "GB" - } - }, - { - "name": "SCS-8V:8:100", - "numberOfvCPUs": 8, - "ramSize": { - "Value": 8.0, - "Unit": "GiB" - }, - "diskSize": { - "Value": 100, - "Unit": "GB" - } - }, - { - "name": "SCS-2V:16", - "numberOfvCPUs": 2, - "ramSize": { - "Value": 16.0, - "Unit": "GiB" - } - }, - { - "name": "SCS-8V:32:50", - "numberOfvCPUs": 8, - "ramSize": { - "Value": 32.0, - "Unit": "GiB" - }, - "diskSize": { - "Value": 50, - "Unit": "GB" - } - }, - { - "name": "SCS-8V:16", - "numberOfvCPUs": 8, - "ramSize": { - "Value": 16.0, - "Unit": "GiB" - } - }, - { - "name": "SCS-4V:16", - "numberOfvCPUs": 4, - "ramSize": { - "Value": 16.0, - "Unit": "GiB" - } - }, - { - "name": "SCS-1V:2", - "numberOfvCPUs": 1, - "ramSize": { - "Value": 2.0, - "Unit": "GiB" - } - }, - { - "name": "SCS-8V:16:50", - "numberOfvCPUs": 8, - "ramSize": { - "Value": 16.0, - "Unit": "GiB" - }, - "diskSize": { - "Value": 50, - "Unit": "GB" - } - }, - { - "name": "SCS-1V:1:20", - "numberOfvCPUs": 1, - "ramSize": { - "Value": 1.0, - "Unit": "GiB" - }, - "diskSize": { - "Value": 20, - "Unit": "GB" - } - }, - { - "name": "SCS-4V:8:50", - "numberOfvCPUs": 4, - "ramSize": { - "Value": 8.0, - "Unit": "GiB" - }, - "diskSize": { - "Value": 50, - "Unit": "GB" - } - }, - { - "name": "SCS-4V:32:50", - "numberOfvCPUs": 4, - "ramSize": { - "Value": 32.0, - "Unit": "GiB" - }, - "diskSize": { - "Value": 50, - "Unit": "GB" - } - }, - { - "name": "SCS-2V:4:50", - "numberOfvCPUs": 2, - "ramSize": { - "Value": 4.0, - "Unit": "GiB" - }, - "diskSize": { - "Value": 50, - "Unit": "GB" - } - }, - { - "name": "SCS-2V:2:20", - "numberOfvCPUs": 2, - "ramSize": { - "Value": 2.0, - "Unit": "GiB" - }, - "diskSize": { - "Value": 20, - "Unit": "GB" - } - }, - { - "name": "SCS-2V:2", - "numberOfvCPUs": 2, - "ramSize": { - "Value": 2.0, - "Unit": "GiB" - } - }, - { - "name": "SCS-1V:0.5", - "numberOfvCPUs": 1, - "ramSize": { - "Value": 0.5, - "Unit": "GiB" - } - }, - { - "name": "SCS-1L:1:5", - "numberOfvCPUs": 1, - "ramSize": { - "Value": 1.0, - "Unit": "GiB" - }, - "diskSize": { - "Value": 5, - "Unit": "GB" - } - }, - { - "name": "SCS-8V:32", - "numberOfvCPUs": 8, - "ramSize": { - "Value": 32.0, - "Unit": "GiB" - } - }, - { - "name": "SCS-2V:8", - "numberOfvCPUs": 2, - "ramSize": { - "Value": 8.0, - "Unit": "GiB" - } - }, - { - "name": "SCS-16V:64:100", - "numberOfvCPUs": 16, - "ramSize": { - "Value": 64.0, - "Unit": "GiB" - }, - "diskSize": { - "Value": 100, - "Unit": "GB" - } - }, - { - "name": "SCS-2V:8:20", - "numberOfvCPUs": 2, - "ramSize": { - "Value": 8.0, - "Unit": "GiB" - }, - "diskSize": { - "Value": 20, - "Unit": "GB" - } - }, - { - "name": "SCS-2V:4", - "numberOfvCPUs": 2, - "ramSize": { - "Value": 4.0, - "Unit": "GiB" - } - }, - { - "name": "SCS-16V:32:100", - "numberOfvCPUs": 16, - "ramSize": { - "Value": 32.0, - "Unit": "GiB" - }, - "diskSize": { - "Value": 100, - "Unit": "GB" - } - }, - { - "name": "SCS-2V:16:50", - "numberOfvCPUs": 2, - "ramSize": { - "Value": 16.0, - "Unit": "GiB" - }, - "diskSize": { - "Value": 50, - "Unit": "GB" - } - }, - { - "name": "SCS-4V:8", - "numberOfvCPUs": 4, - "ramSize": { - "Value": 8.0, - "Unit": "GiB" - } - }, - { - "name": "SCS-2V:4:20", - "numberOfvCPUs": 2, - "ramSize": { - "Value": 4.0, - "Unit": "GiB" - }, - "diskSize": { - "Value": 20, - "Unit": "GB" - } - }, - { - "name": "SCS-1V:1:10", - "numberOfvCPUs": 1, - "ramSize": { - "Value": 1.0, - "Unit": "GiB" - }, - "diskSize": { - "Value": 10, - "Unit": "GB" - } - }, - { - "name": "SCS-1L:1", - "numberOfvCPUs": 1, - "ramSize": { - "Value": 1.0, - "Unit": "GiB" - } - }, - { - "name": "SCS-4V:8:20", - "numberOfvCPUs": 4, - "ramSize": { - "Value": 8.0, - "Unit": "GiB" - }, - "diskSize": { - "Value": 20, - "Unit": "GB" - } - }, - { - "name": "SCS-8V:16:100", - "numberOfvCPUs": 8, - "ramSize": { - "Value": 16.0, - "Unit": "GiB" - }, - "diskSize": { - "Value": 100, - "Unit": "GB" - } - }, - { - "name": "SCS-4V:16:100", - "numberOfvCPUs": 4, - "ramSize": { - "Value": 16.0, - "Unit": "GiB" - }, - "diskSize": { - "Value": 100, - "Unit": "GB" - } - }, - { - "name": "SCS-2V:8:100", - "numberOfvCPUs": 2, - "ramSize": { - "Value": 8.0, - "Unit": "GiB" - }, - "diskSize": { - "Value": 100, - "Unit": "GB" - } - }, - { - "name": "SCS-2V:4:10", - "numberOfvCPUs": 2, - "ramSize": { - "Value": 4.0, - "Unit": "GiB" - }, - "diskSize": { - "Value": 10, - "Unit": "GB" - } - }, - { - "name": "SCS-16V:64", - "numberOfvCPUs": 16, - "ramSize": { - "Value": 64.0, - "Unit": "GiB" - } - }, - { - "name": "SCS-8V:8", - "numberOfvCPUs": 8, - "ramSize": { - "Value": 8.0, - "Unit": "GiB" - } - }, - { - "name": "octavia", - "numberOfvCPUs": 1, - "ramSize": { - "Value": 1.0, - "Unit": "GiB" - }, - "diskSize": { - "Value": 20, - "Unit": "GB" - } - } - ] - }, - "placement": { - "name": "placement", - "endpoint": { - "@value": "https://api.gx-scs.sovereignit.cloud:8780/", - "@type": "xsd:anyURI" - }, - "versions": [ - { - "version": "1.0", - "url": "https://api.gx-scs.sovereignit.cloud:8780/", - "status": "CURRENT", - "min_microversion": "1.0", - "max_microversion": "1.39" - } - ] - }, - "image": { - "name": "glance", - "endpoint": { - "@value": "https://api.gx-scs.sovereignit.cloud:9292/v2/", - "@type": "xsd:anyURI" - }, - "versions": [ - { - "version": "2.0", - "url": "https://api.gx-scs.sovereignit.cloud:9292/v2/", - "status": "SUPPORTED" - }, - { - "version": "2.1", - "url": "https://api.gx-scs.sovereignit.cloud:9292/v2/", - "status": "SUPPORTED" - }, - { - "version": "2.2", - "url": "https://api.gx-scs.sovereignit.cloud:9292/v2/", - "status": "SUPPORTED" - }, - { - "version": "2.3", - "url": "https://api.gx-scs.sovereignit.cloud:9292/v2/", - "status": "SUPPORTED" - }, - { - "version": "2.4", - "url": "https://api.gx-scs.sovereignit.cloud:9292/v2/", - "status": "SUPPORTED" - }, - { - "version": "2.5", - "url": "https://api.gx-scs.sovereignit.cloud:9292/v2/", - "status": "SUPPORTED" - }, - { - "version": "2.6", - "url": "https://api.gx-scs.sovereignit.cloud:9292/v2/", - "status": "SUPPORTED" - }, - { - "version": "2.7", - "url": "https://api.gx-scs.sovereignit.cloud:9292/v2/", - "status": "SUPPORTED" - }, - { - "version": "2.8", - "url": "https://api.gx-scs.sovereignit.cloud:9292/v2/", - "status": "SUPPORTED" - }, - { - "version": "2.9", - "url": "https://api.gx-scs.sovereignit.cloud:9292/v2/", - "status": "SUPPORTED" - }, - { - "version": "2.10", - "url": "https://api.gx-scs.sovereignit.cloud:9292/v2/", - "status": "SUPPORTED" - }, - { - "version": "2.11", - "url": "https://api.gx-scs.sovereignit.cloud:9292/v2/", - "status": "SUPPORTED" - }, - { - "version": "2.12", - "url": "https://api.gx-scs.sovereignit.cloud:9292/v2/", - "status": "SUPPORTED" - }, - { - "version": "2.13", - "url": "https://api.gx-scs.sovereignit.cloud:9292/v2/", - "status": "SUPPORTED" - }, - { - "version": "2.15", - "url": "https://api.gx-scs.sovereignit.cloud:9292/v2/", - "status": "CURRENT" - } - ] - }, - "load_balancer": { - "name": "octavia", - "endpoint": { - "@value": "https://api.gx-scs.sovereignit.cloud:9876/v2.0", - "@type": "xsd:anyURI" - }, - "versions": [ - { - "version": "2.0", - "url": "https://api.gx-scs.sovereignit.cloud:9876/v2", - "status": "SUPPORTED" - }, - { - "version": "2.1", - "url": "https://api.gx-scs.sovereignit.cloud:9876/v2", - "status": "SUPPORTED" - }, - { - "version": "2.2", - "url": "https://api.gx-scs.sovereignit.cloud:9876/v2", - "status": "SUPPORTED" - }, - { - "version": "2.3", - "url": "https://api.gx-scs.sovereignit.cloud:9876/v2", - "status": "SUPPORTED" - }, - { - "version": "2.4", - "url": "https://api.gx-scs.sovereignit.cloud:9876/v2", - "status": "SUPPORTED" - }, - { - "version": "2.5", - "url": "https://api.gx-scs.sovereignit.cloud:9876/v2", - "status": "SUPPORTED" - }, - { - "version": "2.6", - "url": "https://api.gx-scs.sovereignit.cloud:9876/v2", - "status": "SUPPORTED" - }, - { - "version": "2.7", - "url": "https://api.gx-scs.sovereignit.cloud:9876/v2", - "status": "SUPPORTED" - }, - { - "version": "2.8", - "url": "https://api.gx-scs.sovereignit.cloud:9876/v2", - "status": "SUPPORTED" - }, - { - "version": "2.9", - "url": "https://api.gx-scs.sovereignit.cloud:9876/v2", - "status": "SUPPORTED" - }, - { - "version": "2.10", - "url": "https://api.gx-scs.sovereignit.cloud:9876/v2", - "status": "SUPPORTED" - }, - { - "version": "2.11", - "url": "https://api.gx-scs.sovereignit.cloud:9876/v2", - "status": "SUPPORTED" - }, - { - "version": "2.12", - "url": "https://api.gx-scs.sovereignit.cloud:9876/v2", - "status": "SUPPORTED" - }, - { - "version": "2.13", - "url": "https://api.gx-scs.sovereignit.cloud:9876/v2", - "status": "SUPPORTED" - }, - { - "version": "2.14", - "url": "https://api.gx-scs.sovereignit.cloud:9876/v2", - "status": "SUPPORTED" - }, - { - "version": "2.15", - "url": "https://api.gx-scs.sovereignit.cloud:9876/v2", - "status": "SUPPORTED" - }, - { - "version": "2.16", - "url": "https://api.gx-scs.sovereignit.cloud:9876/v2", - "status": "SUPPORTED" - }, - { - "version": "2.17", - "url": "https://api.gx-scs.sovereignit.cloud:9876/v2", - "status": "SUPPORTED" - }, - { - "version": "2.18", - "url": "https://api.gx-scs.sovereignit.cloud:9876/v2", - "status": "SUPPORTED" - }, - { - "version": "2.19", - "url": "https://api.gx-scs.sovereignit.cloud:9876/v2", - "status": "SUPPORTED" - }, - { - "version": "2.20", - "url": "https://api.gx-scs.sovereignit.cloud:9876/v2", - "status": "SUPPORTED" - }, - { - "version": "2.21", - "url": "https://api.gx-scs.sovereignit.cloud:9876/v2", - "status": "SUPPORTED" - }, - { - "version": "2.22", - "url": "https://api.gx-scs.sovereignit.cloud:9876/v2", - "status": "SUPPORTED" - }, - { - "version": "2.23", - "url": "https://api.gx-scs.sovereignit.cloud:9876/v2", - "status": "SUPPORTED" - }, - { - "version": "2.24", - "url": "https://api.gx-scs.sovereignit.cloud:9876/v2", - "status": "SUPPORTED" - }, - { - "version": "2.25", - "url": "https://api.gx-scs.sovereignit.cloud:9876/v2", - "status": "SUPPORTED" - }, - { - "version": "2.26", - "url": "https://api.gx-scs.sovereignit.cloud:9876/v2", - "status": "CURRENT" - } - ], - "flavors": [ - { - "name": "amphora", - "description": "Active Passive" - } - ] - }, - "orchestration": { - "name": "heat", - "endpoint": { - "@value": "https://api.gx-scs.sovereignit.cloud:8004/v1/${OS_PROJECT_ID}", - "@type": "xsd:anyURI" - }, - "versions": [ - { - "version": "1.0", - "url": "https://api.gx-scs.sovereignit.cloud:8004/v1/", - "status": "CURRENT" - } - ] - }, - "compute_legacy": { - "name": "nova_legacy", - "endpoint": { - "@value": "https://api.gx-scs.sovereignit.cloud:8774/v2/${OS_PROJECT_ID}", - "@type": "xsd:anyURI" - } - }, - "cloudformation": { - "name": "heat-cfn", - "endpoint": { - "@value": "https://api.gx-scs.sovereignit.cloud:8000/v1", - "@type": "xsd:anyURI" - } - } - } - } - } -} diff --git a/results/osdescriptor_1683189352.yaml b/results/osdescriptor_1683189352.yaml deleted file mode 100644 index 93efbec..0000000 --- a/results/osdescriptor_1683189352.yaml +++ /dev/null @@ -1,775 +0,0 @@ -openstack: - RegionOne: - cloudformation: - endpoint: https://api.gx-scs.sovereignit.cloud:8000/v1 - name: heat-cfn - compute: - availability_zones: - - nova - endpoint: https://api.gx-scs.sovereignit.cloud:8774/v2.1 - extensions: - - NMN - - OS-DCF - - OS-EXT-AZ - - OS-EXT-IMG-SIZE - - OS-EXT-IPS - - OS-EXT-IPS-MAC - - OS-EXT-SRV-ATTR - - OS-EXT-STS - - OS-FLV-DISABLED - - OS-FLV-EXT-DATA - - OS-SCH-HNT - - OS-SRV-USG - - os-access-ips - - os-admin-actions - - os-admin-password - - os-agents - - os-aggregates - - os-assisted-volume-snapshots - - os-attach-interfaces - - os-availability-zone - - os-baremetal-ext-status - - os-baremetal-nodes - - os-block-device-mapping - - os-block-device-mapping-v2-boot - - os-cell-capacities - - os-cells - - os-certificates - - os-cloudpipe - - os-cloudpipe-update - - os-config-drive - - os-console-auth-tokens - - os-console-output - - os-consoles - - os-create-backup - - os-create-server-ext - - os-deferred-delete - - os-evacuate - - os-extended-evacuate-find-host - - os-extended-floating-ips - - os-extended-hypervisors - - os-extended-networks - - os-extended-quotas - - os-extended-rescue-with-image - - os-extended-services - - os-extended-services-delete - - os-extended-status - - os-extended-volumes - - os-fixed-ips - - os-flavor-access - - os-flavor-extra-specs - - os-flavor-manage - - os-flavor-rxtx - - os-flavor-swap - - os-floating-ip-dns - - os-floating-ip-pools - - os-floating-ips - - os-floating-ips-bulk - - os-fping - - os-hide-server-addresses - - os-hosts - - os-hypervisor-status - - os-hypervisors - - os-instance-actions - - os-instance_usage_audit_log - - os-keypairs - - os-lock-server - - os-migrate-server - - os-migrations - - os-multiple-create - - os-networks - - os-networks-associate - - os-pause-server - - os-personality - - os-preserve-ephemeral-rebuild - - os-quota-class-sets - - os-quota-sets - - os-rescue - - os-security-group-default-rules - - os-security-groups - - os-server-diagnostics - - os-server-external-events - - os-server-group-quotas - - os-server-groups - - os-server-list-multi-status - - os-server-password - - os-server-sort-keys - - os-server-start-stop - - os-services - - os-shelve - - os-simple-tenant-usage - - os-suspend-server - - os-tenant-networks - - os-used-limits - - os-used-limits-for-admin - - os-user-data - - os-user-quotas - - os-virtual-interfaces - - os-volume-attachment-update - - os-volumes - flavors: - - name: SCS-1V:8 - numberOfvCPUs: 1 - ramSize: - Unit: GiB - Value: 8.0 - - diskSize: - Unit: GB - Value: 50 - name: SCS-4V:16:50 - numberOfvCPUs: 4 - ramSize: - Unit: GiB - Value: 16.0 - - diskSize: - Unit: GB - Value: 10 - name: SCS-1V:4:10 - numberOfvCPUs: 1 - ramSize: - Unit: GiB - Value: 4.0 - - name: SCS-16V:32 - numberOfvCPUs: 16 - ramSize: - Unit: GiB - Value: 32.0 - - diskSize: - Unit: GB - Value: 100 - name: SCS-4V:8:100 - numberOfvCPUs: 4 - ramSize: - Unit: GiB - Value: 8.0 - - diskSize: - Unit: GB - Value: 100 - name: SCS-4V:32:100 - numberOfvCPUs: 4 - ramSize: - Unit: GiB - Value: 32.0 - - diskSize: - Unit: GB - Value: 20 - name: SCS-1V:8:20 - numberOfvCPUs: 1 - ramSize: - Unit: GiB - Value: 8.0 - - diskSize: - Unit: GB - Value: 100 - name: SCS-8V:32:100 - numberOfvCPUs: 8 - ramSize: - Unit: GiB - Value: 32.0 - - name: SCS-1V:1 - numberOfvCPUs: 1 - ramSize: - Unit: GiB - Value: 1.0 - - diskSize: - Unit: GB - Value: 5 - name: SCS-1V:2:5 - numberOfvCPUs: 1 - ramSize: - Unit: GiB - Value: 2.0 - - name: SCS-4V:32 - numberOfvCPUs: 4 - ramSize: - Unit: GiB - Value: 32.0 - - name: SCS-1V:4 - numberOfvCPUs: 1 - ramSize: - Unit: GiB - Value: 4.0 - - diskSize: - Unit: GB - Value: 20 - name: SCS-1V:0.5:20 - numberOfvCPUs: 1 - ramSize: - Unit: GiB - Value: 0.5 - - diskSize: - Unit: GB - Value: 100 - name: SCS-2V:4:100 - numberOfvCPUs: 2 - ramSize: - Unit: GiB - Value: 4.0 - - diskSize: - Unit: GB - Value: 100 - name: SCS-8V:8:100 - numberOfvCPUs: 8 - ramSize: - Unit: GiB - Value: 8.0 - - name: SCS-2V:16 - numberOfvCPUs: 2 - ramSize: - Unit: GiB - Value: 16.0 - - diskSize: - Unit: GB - Value: 50 - name: SCS-8V:32:50 - numberOfvCPUs: 8 - ramSize: - Unit: GiB - Value: 32.0 - - name: SCS-8V:16 - numberOfvCPUs: 8 - ramSize: - Unit: GiB - Value: 16.0 - - name: SCS-4V:16 - numberOfvCPUs: 4 - ramSize: - Unit: GiB - Value: 16.0 - - name: SCS-1V:2 - numberOfvCPUs: 1 - ramSize: - Unit: GiB - Value: 2.0 - - diskSize: - Unit: GB - Value: 50 - name: SCS-8V:16:50 - numberOfvCPUs: 8 - ramSize: - Unit: GiB - Value: 16.0 - - diskSize: - Unit: GB - Value: 20 - name: SCS-1V:1:20 - numberOfvCPUs: 1 - ramSize: - Unit: GiB - Value: 1.0 - - diskSize: - Unit: GB - Value: 50 - name: SCS-4V:8:50 - numberOfvCPUs: 4 - ramSize: - Unit: GiB - Value: 8.0 - - diskSize: - Unit: GB - Value: 50 - name: SCS-4V:32:50 - numberOfvCPUs: 4 - ramSize: - Unit: GiB - Value: 32.0 - - diskSize: - Unit: GB - Value: 50 - name: SCS-2V:4:50 - numberOfvCPUs: 2 - ramSize: - Unit: GiB - Value: 4.0 - - diskSize: - Unit: GB - Value: 20 - name: SCS-2V:2:20 - numberOfvCPUs: 2 - ramSize: - Unit: GiB - Value: 2.0 - - name: SCS-2V:2 - numberOfvCPUs: 2 - ramSize: - Unit: GiB - Value: 2.0 - - name: SCS-1V:0.5 - numberOfvCPUs: 1 - ramSize: - Unit: GiB - Value: 0.5 - - diskSize: - Unit: GB - Value: 5 - name: SCS-1L:1:5 - numberOfvCPUs: 1 - ramSize: - Unit: GiB - Value: 1.0 - - name: SCS-8V:32 - numberOfvCPUs: 8 - ramSize: - Unit: GiB - Value: 32.0 - - name: SCS-2V:8 - numberOfvCPUs: 2 - ramSize: - Unit: GiB - Value: 8.0 - - diskSize: - Unit: GB - Value: 100 - name: SCS-16V:64:100 - numberOfvCPUs: 16 - ramSize: - Unit: GiB - Value: 64.0 - - diskSize: - Unit: GB - Value: 20 - name: SCS-2V:8:20 - numberOfvCPUs: 2 - ramSize: - Unit: GiB - Value: 8.0 - - name: SCS-2V:4 - numberOfvCPUs: 2 - ramSize: - Unit: GiB - Value: 4.0 - - diskSize: - Unit: GB - Value: 100 - name: SCS-16V:32:100 - numberOfvCPUs: 16 - ramSize: - Unit: GiB - Value: 32.0 - - diskSize: - Unit: GB - Value: 50 - name: SCS-2V:16:50 - numberOfvCPUs: 2 - ramSize: - Unit: GiB - Value: 16.0 - - name: SCS-4V:8 - numberOfvCPUs: 4 - ramSize: - Unit: GiB - Value: 8.0 - - diskSize: - Unit: GB - Value: 20 - name: SCS-2V:4:20 - numberOfvCPUs: 2 - ramSize: - Unit: GiB - Value: 4.0 - - diskSize: - Unit: GB - Value: 10 - name: SCS-1V:1:10 - numberOfvCPUs: 1 - ramSize: - Unit: GiB - Value: 1.0 - - name: SCS-1L:1 - numberOfvCPUs: 1 - ramSize: - Unit: GiB - Value: 1.0 - - diskSize: - Unit: GB - Value: 20 - name: SCS-4V:8:20 - numberOfvCPUs: 4 - ramSize: - Unit: GiB - Value: 8.0 - - diskSize: - Unit: GB - Value: 100 - name: SCS-8V:16:100 - numberOfvCPUs: 8 - ramSize: - Unit: GiB - Value: 16.0 - - diskSize: - Unit: GB - Value: 100 - name: SCS-4V:16:100 - numberOfvCPUs: 4 - ramSize: - Unit: GiB - Value: 16.0 - - diskSize: - Unit: GB - Value: 100 - name: SCS-2V:8:100 - numberOfvCPUs: 2 - ramSize: - Unit: GiB - Value: 8.0 - - diskSize: - Unit: GB - Value: 10 - name: SCS-2V:4:10 - numberOfvCPUs: 2 - ramSize: - Unit: GiB - Value: 4.0 - - name: SCS-16V:64 - numberOfvCPUs: 16 - ramSize: - Unit: GiB - Value: 64.0 - - name: SCS-8V:8 - numberOfvCPUs: 8 - ramSize: - Unit: GiB - Value: 8.0 - - diskSize: - Unit: GB - Value: 20 - name: octavia - numberOfvCPUs: 1 - ramSize: - Unit: GiB - Value: 1.0 - name: nova - versions: - - max_microversion: '2.93' - min_microversion: '2.1' - status: CURRENT - url: https://api.gx-scs.sovereignit.cloud:8774/v2.1/ - version: '2.1' - - status: SUPPORTED - url: https://api.gx-scs.sovereignit.cloud:8774/v2/ - version: '2.0' - compute_legacy: - endpoint: https://api.gx-scs.sovereignit.cloud:8774/v2/${OS_PROJECT_ID} - name: nova_legacy - dns: - endpoint: https://api.gx-scs.sovereignit.cloud:9001/v2 - name: designate - versions: - - status: SUPPORTED - url: https://api.gx-scs.sovereignit.cloud:9001/v2 - version: '2.0' - - status: CURRENT - url: https://api.gx-scs.sovereignit.cloud:9001/v2 - version: '2.0' - identity: - endpoint: https://api.gx-scs.sovereignit.cloud:5000 - name: keystone - versions: - - status: CURRENT - url: https://api.gx-scs.sovereignit.cloud:5000/v3/ - version: '3.14' - image: - endpoint: https://api.gx-scs.sovereignit.cloud:9292/v2/ - name: glance - versions: - - status: SUPPORTED - url: https://api.gx-scs.sovereignit.cloud:9292/v2/ - version: '2.0' - - status: SUPPORTED - url: https://api.gx-scs.sovereignit.cloud:9292/v2/ - version: '2.1' - - status: SUPPORTED - url: https://api.gx-scs.sovereignit.cloud:9292/v2/ - version: '2.2' - - status: SUPPORTED - url: https://api.gx-scs.sovereignit.cloud:9292/v2/ - version: '2.3' - - status: SUPPORTED - url: https://api.gx-scs.sovereignit.cloud:9292/v2/ - version: '2.4' - - status: SUPPORTED - url: https://api.gx-scs.sovereignit.cloud:9292/v2/ - version: '2.5' - - status: SUPPORTED - url: https://api.gx-scs.sovereignit.cloud:9292/v2/ - version: '2.6' - - status: SUPPORTED - url: https://api.gx-scs.sovereignit.cloud:9292/v2/ - version: '2.7' - - status: SUPPORTED - url: https://api.gx-scs.sovereignit.cloud:9292/v2/ - version: '2.8' - - status: SUPPORTED - url: https://api.gx-scs.sovereignit.cloud:9292/v2/ - version: '2.9' - - status: SUPPORTED - url: https://api.gx-scs.sovereignit.cloud:9292/v2/ - version: '2.10' - - status: SUPPORTED - url: https://api.gx-scs.sovereignit.cloud:9292/v2/ - version: '2.11' - - status: SUPPORTED - url: https://api.gx-scs.sovereignit.cloud:9292/v2/ - version: '2.12' - - status: SUPPORTED - url: https://api.gx-scs.sovereignit.cloud:9292/v2/ - version: '2.13' - - status: CURRENT - url: https://api.gx-scs.sovereignit.cloud:9292/v2/ - version: '2.15' - key_manager: - endpoint: https://api.gx-scs.sovereignit.cloud:9311/v1/ - name: barbican - versions: - - status: CURRENT - url: https://api.gx-scs.sovereignit.cloud:9311/v1/ - version: '1.0' - load_balancer: - endpoint: https://api.gx-scs.sovereignit.cloud:9876/v2.0 - flavors: - - description: Active Passive - name: amphora - name: octavia - versions: - - status: SUPPORTED - url: https://api.gx-scs.sovereignit.cloud:9876/v2 - version: '2.0' - - status: SUPPORTED - url: https://api.gx-scs.sovereignit.cloud:9876/v2 - version: '2.1' - - status: SUPPORTED - url: https://api.gx-scs.sovereignit.cloud:9876/v2 - version: '2.2' - - status: SUPPORTED - url: https://api.gx-scs.sovereignit.cloud:9876/v2 - version: '2.3' - - status: SUPPORTED - url: https://api.gx-scs.sovereignit.cloud:9876/v2 - version: '2.4' - - status: SUPPORTED - url: https://api.gx-scs.sovereignit.cloud:9876/v2 - version: '2.5' - - status: SUPPORTED - url: https://api.gx-scs.sovereignit.cloud:9876/v2 - version: '2.6' - - status: SUPPORTED - url: https://api.gx-scs.sovereignit.cloud:9876/v2 - version: '2.7' - - status: SUPPORTED - url: https://api.gx-scs.sovereignit.cloud:9876/v2 - version: '2.8' - - status: SUPPORTED - url: https://api.gx-scs.sovereignit.cloud:9876/v2 - version: '2.9' - - status: SUPPORTED - url: https://api.gx-scs.sovereignit.cloud:9876/v2 - version: '2.10' - - status: SUPPORTED - url: https://api.gx-scs.sovereignit.cloud:9876/v2 - version: '2.11' - - status: SUPPORTED - url: https://api.gx-scs.sovereignit.cloud:9876/v2 - version: '2.12' - - status: SUPPORTED - url: https://api.gx-scs.sovereignit.cloud:9876/v2 - version: '2.13' - - status: SUPPORTED - url: https://api.gx-scs.sovereignit.cloud:9876/v2 - version: '2.14' - - status: SUPPORTED - url: https://api.gx-scs.sovereignit.cloud:9876/v2 - version: '2.15' - - status: SUPPORTED - url: https://api.gx-scs.sovereignit.cloud:9876/v2 - version: '2.16' - - status: SUPPORTED - url: https://api.gx-scs.sovereignit.cloud:9876/v2 - version: '2.17' - - status: SUPPORTED - url: https://api.gx-scs.sovereignit.cloud:9876/v2 - version: '2.18' - - status: SUPPORTED - url: https://api.gx-scs.sovereignit.cloud:9876/v2 - version: '2.19' - - status: SUPPORTED - url: https://api.gx-scs.sovereignit.cloud:9876/v2 - version: '2.20' - - status: SUPPORTED - url: https://api.gx-scs.sovereignit.cloud:9876/v2 - version: '2.21' - - status: SUPPORTED - url: https://api.gx-scs.sovereignit.cloud:9876/v2 - version: '2.22' - - status: SUPPORTED - url: https://api.gx-scs.sovereignit.cloud:9876/v2 - version: '2.23' - - status: SUPPORTED - url: https://api.gx-scs.sovereignit.cloud:9876/v2 - version: '2.24' - - status: SUPPORTED - url: https://api.gx-scs.sovereignit.cloud:9876/v2 - version: '2.25' - - status: CURRENT - url: https://api.gx-scs.sovereignit.cloud:9876/v2 - version: '2.26' - network: - availability_zones: - - nova - endpoint: https://api.gx-scs.sovereignit.cloud:9696 - extensions: - - address-group - - address-scope - - agent - - allowed-address-pairs - - auto-allocated-topology - - availability_zone - - default-subnetpools - - dns-integration - - dns-domain-ports - - dns-integration-domain-keywords - - expose-port-forwarding-in-fip - - external-net - - extra_dhcp_opt - - extraroute - - filter-validation - - floating-ip-port-forwarding-description - - floating-ip-port-forwarding-detail - - floating-ip-port-forwarding-port-ranges - - fip-port-details - - flavors - - floating-ip-port-forwarding - - floatingip-pools - - router - - ext-gw-mode - - multi-provider - - net-mtu - - net-mtu-writable - - network_availability_zone - - network-ip-availability - - pagination - - port-device-profile - - port-mac-address-regenerate - - port-numa-affinity-policy - - port-resource-request - - port-resource-request-groups - - binding - - binding-extended - - port-security - - project-id - - provider - - qos - - qos-bw-limit-direction - - qos-bw-minimum-ingress - - qos-default - - qos-fip - - qos-gateway-ip - - qos-port-network-policy - - qos-pps-minimum - - qos-pps-minimum-rule-alias - - qos-pps - - qos-rule-type-details - - qos-rule-type-filter - - qos-rules-alias - - quota-check-limit - - quotas - - quota_details - - rbac-policies - - rbac-address-scope - - rbac-security-groups - - revision-if-match - - standard-attr-revisions - - router_availability_zone - - security-groups-normalized-cidr - - security-groups-remote-address-group - - security-groups-shared-filtering - - security-group - - service-type - - sorting - - standard-attr-description - - stateful-security-group - - subnet-dns-publish-fixed-ip - - subnet-service-types - - subnet_allocation - - standard-attr-tag - - standard-attr-timestamp - name: neutron - versions: - - status: CURRENT - url: https://api.gx-scs.sovereignit.cloud:9696/v2.0/ - version: '2.0' - object_store: - endpoint: https://api.gx-scs.sovereignit.cloud:8080/swift/v1/AUTH_${OS_PROJECT_ID} - name: swift - versions: - - status: CURRENT - url: https://api.gx-scs.sovereignit.cloud:8080/swift/v1/ - version: '1.0' - orchestration: - endpoint: https://api.gx-scs.sovereignit.cloud:8004/v1/${OS_PROJECT_ID} - name: heat - versions: - - status: CURRENT - url: https://api.gx-scs.sovereignit.cloud:8004/v1/ - version: '1.0' - placement: - endpoint: https://api.gx-scs.sovereignit.cloud:8780/ - name: placement - versions: - - max_microversion: '1.39' - min_microversion: '1.0' - status: CURRENT - url: https://api.gx-scs.sovereignit.cloud:8780/ - version: '1.0' - volume: - availability_zones: - - nova - endpoint: https://api.gx-scs.sovereignit.cloud:8776/v3/${OS_PROJECT_ID} - extensions: - - os-volume-transfer - - backups - - encryption - - scheduler-stats - - OS-SCH-HNT - - cgsnapshots - - qos-specs - - os-vol-tenant-attr - - os-quota-sets - - os-snapshot-unmanage - - os-extended-snapshot-attributes - - os-admin-actions - - os-snapshot-actions - - os-extended-services - - os-vol-host-attr - - os-types-extra-specs - - os-volume-unmanage - - os-quota-class-sets - - os-volume-encryption-metadata - - os-vol-mig-status-attr - - capabilities - - os-volume-actions - - os-services - - os-types-manage - - os-volume-type-access - - os-used-limits - - os-hosts - - os-snapshot-manage - - os-vol-image-meta - - os-availability-zone - - os-volume-manage - - consistencygroups - name: cinderv3 - versions: - - max_microversion: '3.70' - min_microversion: '3.0' - status: CURRENT - url: https://api.gx-scs.sovereignit.cloud:8776/v3/ - version: '3.0' - volume_types: - - name: LUKS - public: true - - name: __DEFAULT__ - public: true - auth_url: https://api.gx-scs.sovereignit.cloud:5000 - regions: - - RegionOne - diff --git a/tests/data/credential.json b/tests/data/credential.json deleted file mode 100644 index 9eba799..0000000 --- a/tests/data/credential.json +++ /dev/null @@ -1,262 +0,0 @@ -{ - "@context": { - "gx": "https://w3id.org/gaia-x/ONTOLOGY_VERSION/", - "qudt": "http://qudt.org/vocab/", - "schema": "http://schema.org/", - "vcard": "http://www.w3.org/2006/vcard/ns#", - "xsd": "http://www.w3.org/2001/XMLSchema#", - "ex": "https://example.com/" - }, - "@graph": [ - { - "@id": "ex:image_2", - "@type": "gx:VMImage", - "gx:name": "Image2", - "gx:description": "Image 2", - "gx:copyrightOwnedBy": [ - "Fedora-Project" - ], - "gx:license": [ - { - "@type": "xsd:anyURI", - "@value": "https://docs.fedoraproject.org/en-US/legal/fedora-linux-license/" - } - ], - "gx:resourcePolicy": "default: allow intent", - "gx:checksum": { - "@type": "gx:CheckSum", - "gx:checkSumCalculation": "sha-512", - "gx:checkSumValue": "7f8bababc2c2a94880747383750470aee68c7e8840bb8811eaeda1b0ce71d59f40ebb182" - }, - "gx:patchLevel": "1.5.2", - "gx:buildDate": { - "@type": "xsd:dateTime", - "@value": "2023-11-01T00:00:00" - }, - "gx:operatingSystem": { - "@type": "gx:OperatingSystem", - "gx:copyrightOwnedBy": [ - "Fedora-Project" - ], - "gx:license": [ - { - "@type": "xsd:anyURI", - "@value": "https://docs.fedoraproject.org/en-US/legal/fedora-linux-license/" - } - ], - "gx:resourcePolicy": [ - "default: allow intent" - ], - "gx:version": "Stable", - "gx:osDistribution": "Fedora" - }, - "gx:cpuReq": { - "@type": "gx:CPU", - "gx:cpuArchitecture": "x86-64", - "gx:smtEnabled": { - "@type": "xsd:boolean", - "@value": false - }, - "gx:numberOfCores": 2, - "gx:numberOfThreads": 4 - }, - "gx:ramReq": { - "@type": "gx:Memory", - "gx:memorySize": { - "@type": "gx:MemorySize", - "qudt:value": { - "@type": "xsd:float", - "@value": 1.048576 - }, - "qudt:unit": "https://qudt.org/vocab/unit/MegaBYTE" - }, - "gx:memoryClass": "other", - "gx:memoryRank": "other", - "gx:eccEnabled": { - "@type": "xsd:boolean", - "@value": false - }, - "gx:hardwareEncryption": { - "@type": "xsd:boolean", - "@value": false - } - }, - "gx:videoRamSize": { - "@type": "gx:MemorySize", - "qudt:value": { - "@type": "xsd:float", - "@value": 20.0 - }, - "qudt:unit": "https://qudt.org/vocab/unit/MegaBYTE" - }, - "gx:rootDiskReq": { - "@type": "gx:Disk", - "gx:diskSize": { - "@type": "gx:MemorySize", - "qudt:value": { - "@type": "xsd:float", - "@value": 21.47483648 - }, - "qudt:unit": "https://qudt.org/vocab/unit/GigaBYTE" - }, - "gx:diskType": "other", - "gx:diskBusType": "SCSI" - }, - "gx:secureBoot": { - "@type": "xsd:boolean", - "@value": true - }, - "gx:vPMU": { - "@type": "xsd:boolean", - "@value": false - }, - "gx:multiQueues": { - "@type": "xsd:boolean", - "@value": true - }, - "gx:updateStrategy": { - "@type": "gx:UpdateStrategy", - "gx:replaceFrequency": "weekly", - "gx:providedUntil": "none" - }, - "gx:licenseIncluded": { - "@type": "xsd:boolean", - "@value": false - }, - "gx:maintenance": { - "@type": "gx:MaintenanceSubscription", - "gx:subscriptionIncluded": { - "@type": "xsd:boolean", - "@value": false - }, - "gx:subscriptionRequired": { - "@type": "xsd:boolean", - "@value": true - }, - "gx:maintainedUntil": { - "@type": "xsd:date", - "@value": "2024-05-31" - } - }, - "gx:vmImageDiskFormat": "RAW", - "gx:hypervisorType": "other", - "gx:firmwareType": "other", - "gx:hwRngTypeOfImage": "None", - "gx:watchDogAction": "reset", - "null": { - "@type": "gx:MemorySize", - "qudt:value": { - "@type": "xsd:float", - "@value": 9788573790.089773 - }, - "qudt:unit": "https://qudt.org/vocab/unit/GigaBYTE" - } - }, - { - "@id": "ex:flavor_2", - "@type": "gx:ServerFlavor", - "gx:cpu": { - "@type": "gx:CPU", - "gx:vendor": "AMD", - "gx:generation": "Zen-3 (Milan)", - "gx:defaultOversubscriptionRatio": 16, - "gx:cpuArchitecture": "x86-64", - "gx:smtEnabled": { - "@type": "xsd:boolean", - "@value": true - }, - "gx:numberOfCores": 2, - "gx:baseFrequency": { - "@type": "qudt:quantitykind/FrequencyDefinition", - "qudt:value": { - "@type": "xsd:float", - "@value": 3.25 - }, - "qudt:unit": "https://qudt.org/vocab/unit/GigaHZ" - } - }, - "gx:ram": { - "@type": "gx:Memory", - "gx:defaultOversubscriptionRatio": 2, - "gx:memorySize": { - "@type": "gx:MemorySize", - "qudt:value": { - "@type": "xsd:float", - "@value": 16.0 - }, - "qudt:unit": "https://qudt.org/vocab/unit/MegaBYTE" - }, - "gx:memoryClass": "other", - "gx:memoryRank": "other", - "gx:eccEnabled": { - "@type": "xsd:boolean", - "@value": false - }, - "gx:hardwareEncryption": { - "@type": "xsd:boolean", - "@value": false - } - }, - "gx:bootVolume": { - "@type": "gx:Disk", - "gx:diskSize": { - "@type": "gx:MemorySize", - "qudt:value": { - "@type": "xsd:float", - "@value": 50.0 - }, - "qudt:unit": "https://qudt.org/vocab/unit/GigaBYTE" - }, - "gx:diskType": "local SSD", - "gx:diskBusType": "other" - }, - "gx:additionalVolume": [ - { - "@type": "gx:Disk", - "gx:diskSize": { - "@type": "gx:MemorySize", - "qudt:value": { - "@type": "xsd:float", - "@value": 50.0 - }, - "qudt:unit": "https://qudt.org/vocab/unit/GigaBYTE" - }, - "gx:diskType": "local SSD", - "gx:diskBusType": "other" - }, - { - "@type": "gx:Disk", - "gx:diskSize": { - "@type": "gx:MemorySize", - "qudt:value": { - "@type": "xsd:float", - "@value": 50.0 - }, - "qudt:unit": "https://qudt.org/vocab/unit/GigaBYTE" - }, - "gx:diskType": "local SSD", - "gx:diskBusType": "other" - } - ], - "gx:hypervisor": { - "@type": "gx:Hypervisor", - "gx:copyrightOwnedBy": [ - "Qumranet" - ], - "gx:license": [ - "GPL-2.0-or-later", - "LGPL-3.0-or-later" - ], - "gx:resourcePolicy": [ - "default: allow intent" - ], - "gx:hypervisorType": "KVM" - }, - "gx:hardwareAssistedVirtualization": { - "@type": "xsd:boolean", - "@value": false - }, - "gx:hwRngTypeOfFlavor": "None" - } - ] -} \ No newline at end of file diff --git a/tests/data/empty_credential.json b/tests/data/empty_credential.json deleted file mode 100644 index 9f29f3b..0000000 --- a/tests/data/empty_credential.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "@context": { - "gx": "https://w3id.org/gaia-x/ONTOLOGY_VERSION/", - "qudt": "http://qudt.org/vocab/", - "schema": "http://schema.org/", - "vcard": "http://www.w3.org/2006/vcard/ns#", - "xsd": "http://www.w3.org/2001/XMLSchema#", - "ex": "https://example.com/" - }, - "@graph": [ - ] -} \ No newline at end of file diff --git a/tests/test_cli.py b/tests/test_cli.py index 6a281f3..716db64 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,76 +1,201 @@ -import json +import os import unittest from unittest.mock import MagicMock, patch +import yaml from click.testing import CliRunner +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import rsa +from jwcrypto.jwt import JWK from generator import cli -from generator.common import const +from generator.common import config, const +from generator.common.gx_schema import (DataAccountExport, TermsAndConditions, + VirtualMachineServiceOffering) from tests.common import MockConnection, get_absolute_path -EXPECTED_RESULT = { - "@context": { - "gx": "https://w3id.org/gaia-x/deployment#", - "qudt": "http://qudt.org/vocab/", - "schema": "http://schema.org/", - "vcard": "http://www.w3.org/2006/vcard/ns#", - "xsd": "http://www.w3.org/2001/XMLSchema#", - "ex": "https://example.com/" - }, - "@graph": "foo" -} - class CliTestCase(unittest.TestCase): + @patch("generator.discovery.gxdch_services.ComplianceService.request_compliance_vc") + @patch("generator.common.crypto.load_jwk_from_file") @patch("generator.discovery.openstack.openstack_discovery.OpenstackDiscovery.discover") @patch("openstack.connect") - def test_openstack(self, os_connect, os_discover): + @patch("generator.cli._print_vcs") + def test_generate_vsmo(self, cli_print_vs, os_connect, os_discover, load_jwk, gxdch_req_compl): # Mock openstack calls + cli_print_vs.return_value = None os_connect.return_value = MockConnection(images=[], flavors=[]) - os_discover.return_value = "foo" + os_discover.return_value = VirtualMachineServiceOffering( + providedBy="foo", + serviceOfferingTermsAndConditions=[TermsAndConditions( + url="https://example.com/tandc", + hash="123")], + dataAccountExport=DataAccountExport( + requestType="API", + accessType="digital", + formatType="plain"), + codeArtifact=["foo"], + instantiationReq=["bar"]) + load_jwk.return_value = JWK.from_pem(rsa.generate_private_key( + public_exponent=65537, + key_size=2048).private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption())) + csp_vcs = {'tandc': {"credentialSubject": {"type": "gx:GaiaXTermsAndConditions"}}, + "lrn": {"credentialSubject": {"type": "gx:LegalRegistrationNumber", "id": "foo"}}, + "lp": {"credentialSubject": {"type": "gx:LegalParticipant", "id": "foo"}}} + gxdch_req_compl.return_value = "{\"credentialSubject\": {\"type\": \"gx:ComplianceCredential\", \"id\": \"foo\"}}" + + with open(get_absolute_path(const.CONFIG_FILE), "r") as config_file: + conf = config.Config(yaml.safe_load(config_file)) + + vcs = cli.create_vmso_vcs(conf, "myCloud", csp_vcs) + os_connect.assert_called_once() + os_discover.assert_called_once() + gxdch_req_compl.assert_called_once() + + self.assertEqual(4, len(vcs)) + self.assertEqual("gx:ServiceOffering", vcs['so']['credentialSubject']["type"]) + self.assertEqual("gx:ComplianceCredential", vcs['cs_so']['credentialSubject']["type"]) + self.assertEqual("gx:VirtualMachineServiceOffering", vcs['vmso']['credentialSubject']["type"]) + self.assertIsNotNone(vcs['vp_so']) + + @patch("generator.discovery.csp_generator.CspGenerator.generate") + @patch("generator.cli.create_vmso_vcs") + @patch("generator.cli._print_vcs") + def test_openstack(self, cli_print_vs, gen_vmso, gen_csp): + cli_print_vs.return_value = None + gen_vmso.return_value = {"foo": "foo"} + gen_csp.return_value = {"bar": "bar"} runner = CliRunner() result = runner.invoke( - cli.openstack, "myCloud --config=" + get_absolute_path(const.CONFIG_FILE) + cli.openstack, "myCloud --config=" + get_absolute_path(const.CONFIG_FILE) + " --auto-sign" ) + + gen_csp.assert_called_once() + gen_vmso.assert_called_once() + self.assertIsNone(result.exception) self.assertEqual(0, result.exit_code) - os_connect.assert_called_once() - os_discover.assert_called_once() - self.assertEqual(json.dumps(EXPECTED_RESULT, indent=4) + "\n", result.output) - @patch("openstack.connect") - def _test_openstack_empty(self, os_connect): - # Mock openstack calls - os_connect.return_value = MockConnection() + @patch("generator.discovery.csp_generator.CspGenerator.generate") + @patch("generator.cli.create_vmso_vcs") + @patch("generator.cli._print_vcs") + def test_openstack_auto_sign(self, cli_print_vs, gen_vmso, gen_csp): + cli_print_vs.return_value = None + gen_vmso.return_value = {"foo": "foo"} + gen_csp.return_value = {"bar": "bar"} + runner = CliRunner() result = runner.invoke( - cli.openstack, "myCloud --config=" + get_absolute_path(const.CONFIG_FILE) + cli.openstack, "myCloud --config=" + get_absolute_path(const.CONFIG_FILE) + " --auto-sign" ) + gen_csp.assert_called_once() + gen_vmso.assert_called_once() + + self.assertIsNone(result.exception) + self.assertEqual(0, result.exit_code) + + @patch("generator.discovery.csp_generator.CspGenerator.generate") + @patch("generator.cli.create_vmso_vcs") + @patch("generator.cli._print_vcs") + def test_openstack_no_auto_sign(self, cli_print_vs, gen_vmso, gen_csp): + cli_print_vs.return_value = None + gen_vmso.return_value = {"foo": "foo"} + gen_csp.return_value = {"bar": "bar"} + + with patch('builtins.input', return_value="n"): + runner = CliRunner() + result = runner.invoke( + cli.openstack, "myCloud --config=" + get_absolute_path(const.CONFIG_FILE) + " --no-auto-sign" + ) + + self.assertEqual(0, gen_csp.call_count) + self.assertEqual(0, gen_vmso.call_count) + + self.assertIsNone(result.exception) + self.assertEqual(0, result.exit_code) + + @patch("generator.discovery.csp_generator.CspGenerator.generate") + @patch("generator.cli.create_vmso_vcs") + @patch("generator.cli._print_vcs") + def test_openstack_sign(self, cli_print_vs, gen_vmso, gen_csp): + cli_print_vs.return_value = None + gen_vmso.return_value = {"foo": "foo"} + gen_csp.return_value = {"bar": "bar"} + + with patch('builtins.input', return_value="y"): + runner = CliRunner() + result = runner.invoke( + cli.openstack, "myCloud --config=" + get_absolute_path(const.CONFIG_FILE) + ) + + gen_csp.assert_called_once() + gen_vmso.assert_called_once() + self.assertIsNone(result.exception) self.assertEqual(0, result.exit_code) - with open( - get_absolute_path("tests/data/empty_credential.json"), mode="r" - ) as json_file: - expected_output = json.load(json_file) - received_output = json.loads(result.output) - self.assertEqual(expected_output, received_output) @patch("openstack.connect") - def _test_openstack_exception(self, os_connect): + def test_init_connection(self, os_connect): # Mock openstack calls mock_con = MockConnection(images=[], flavors=[]) mock_con.authorize = MagicMock(name='method') mock_con.authorize.side_effect = [Exception(), None] os_connect.return_value = mock_con + + con = cli.init_openstack_connection("myCloud") + self.assertIsNotNone(con) + + @patch("generator.discovery.csp_generator.CspGenerator.generate") + @patch("generator.cli._print_vcs") + def test_csp_auto_sign(self, cli_print_vs, gen_csp): + cli_print_vs.return_value = None + gen_csp.return_value = {"vc": "bar"} + runner = CliRunner() result = runner.invoke( - cli.openstack, "myCloud --config=" + get_absolute_path(const.CONFIG_FILE) + cli.csp, "--config=" + get_absolute_path(const.CONFIG_FILE) + " --auto-sign" ) + + gen_csp.assert_called_once() + self.assertIsNone(result.exception) + self.assertEqual(0, result.exit_code) + + @patch("generator.discovery.csp_generator.CspGenerator.generate") + @patch("generator.cli._print_vcs") + def test_csp_sign(self, cli_print_vs, gen_csp): + cli_print_vs.return_value = None + gen_csp.return_value = {"vc": "bar"} + + with patch('builtins.input', return_value="y"): + runner = CliRunner() + result = runner.invoke( + cli.csp, "--config=" + get_absolute_path(const.CONFIG_FILE) + ) + gen_csp.assert_called_once() self.assertIsNone(result.exception) - self.assertEqual(1, result.exit_code) + self.assertEqual(0, result.exit_code) + + @patch("generator.discovery.csp_generator.CspGenerator.generate") + @patch("generator.cli._print_vcs") + def test_csp_no_auto_sign(self, cli_print_vs, gen_csp): + cli_print_vs.return_value = None + gen_csp.return_value = {"vc": "bar"} + + with patch('builtins.input', return_value="n"): + runner = CliRunner() + result = runner.invoke( + cli.csp, "--config=" + get_absolute_path(const.CONFIG_FILE) + " --no-auto-sign" + ) + + self.assertEqual(0, gen_csp.call_count) + self.assertIsNone(result.exception) + self.assertEqual(0, result.exit_code) def _test_kubernetes(self): # TODO: Implement test case @@ -78,7 +203,20 @@ def _test_kubernetes(self): result = runner.invoke(cli.kubernetes) self.assertIsNone(result.exception) self.assertEqual(0, result.exit_code) - pass + + def test_print_vcs(self): + out_dir = os.getcwd() + vcs = {'so': {'foo': 'bar'}, 'lrn': {'bar': 'foo'}} + with patch('builtins.open', unittest.mock.mock_open()) as m1: + cli._print_vcs(vcs=vcs, out_dir=out_dir) + self.assertEqual(2, m1.call_count) + + with patch('builtins.open', unittest.mock.mock_open()) as m2: + cli._print_vcs(vcs=vcs) + self.assertEqual(2, m2.call_count) + + with patch('builtins.open', unittest.mock.mock_open()): + self.assertRaises(NotADirectoryError, cli._print_vcs, vcs, "/foo") if __name__ == "__main__": diff --git a/tests/test_csp_generator.py b/tests/test_csp_generator.py new file mode 100644 index 0000000..536aa31 --- /dev/null +++ b/tests/test_csp_generator.py @@ -0,0 +1,84 @@ +import unittest +from unittest.mock import patch + +import yaml +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import rsa +from jwcrypto.jwt import JWK + +from generator.common import config, const +from generator.discovery.csp_generator import CspGenerator +from tests.common import get_absolute_path + + +class CspPGeneratorTestCase(unittest.TestCase): + + @classmethod + def setUpClass(cls): + with open(get_absolute_path(const.CONFIG_FILE), "r") as config_file: + cls.conf = config.Config(yaml.safe_load(config_file)) + + cls.private_key = JWK.from_pem(rsa.generate_private_key( + public_exponent=65537, + key_size=2048).private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption())) + cls.tanbc = {"version": "22.10", "text": "foo"} + cls.lrn = {"id": "cred_id", "credentialSubject": {"type": "gx:legalRegistrationNumber", + "id": "cred_sub_id"}} + cls.cs = "{\"credentialSubject\": {\"type\": \"gx:ComplianceCredential\"}}" + + def setUp(self): + self.csp_gen = CspGenerator(self.conf) + + @patch("generator.discovery.gxdch_services.ComplianceService.request_compliance_vc") + @patch("generator.discovery.gxdch_services.NotaryService.request_reg_number_vc") + @patch("generator.discovery.gxdch_services.RegistryService.get_gx_tandc") + @patch("generator.common.crypto.load_jwk_from_file") + def test_generate(self, load_jwk, get_tandc, request_req_number, request_comp_vc): + self._prepare_test(load_jwk, get_tandc, request_req_number, request_comp_vc) + csp_vcs = self.csp_gen.generate() + self._verify_csp_gen(csp_vcs, get_tandc, request_req_number, request_comp_vc) + + def _prepare_test(self, load_jwk, get_tandc, request_req_number, request_comp_vc): + load_jwk.return_value = self.private_key + get_tandc.return_value = self.tanbc + request_req_number.return_value = self.lrn + request_comp_vc.return_value = self.cs + + def _verify_csp_gen(self, csp_vcs, get_tandc, request_req_number, request_comp_vc): + get_tandc.assert_called_once() + request_req_number.assert_called_once() + request_req_number.assert_called_with( + csp=self.conf.get_value([const.CONFIG_CSP]), + cred_id=self.conf.get_value([const.CONFIG_CRED, const.CONFIG_CRED_BASE_CRED_URL]) + "/lrn.json", + cred_subject_id=self.conf.get_value([const.CONFIG_CRED, const.CONFIG_CRED_BASE_CRED_URL]) + "/lrn.json#subject" + ) + request_comp_vc.assert_called_once() + + self.assertEqual(self.conf.get_value([const.CONFIG_CRED, const.CONFIG_CRED_BASE_CRED_URL]) + "/tandc.json", + csp_vcs['tandc']['id']) + self.assertEqual(self.conf.get_value([const.CONFIG_CRED, const.CONFIG_CRED_BASE_CRED_URL]) + "/tandc.json#subject", + csp_vcs['tandc']['credentialSubject']['id']) + self.assertEqual("gx:GaiaXTermsAndConditions", csp_vcs['tandc']['credentialSubject']['type']) + self.assertEqual("foo", csp_vcs['tandc']['credentialSubject']['gx:termsAndConditions']) + self.assertEqual(self.conf.get_value([const.CONFIG_CSP, const.CONFIG_DID]), csp_vcs['tandc']['issuer']) + self.assertIsNotNone(csp_vcs['tandc']['proof']) + self.assertEqual("cred_id", csp_vcs['lrn']['id']) + self.assertEqual("cred_sub_id", csp_vcs['lrn']['credentialSubject']['id']) + self.assertEqual("gx:legalRegistrationNumber", csp_vcs['lrn']['credentialSubject']['type']) + self.assertEqual( + self.conf.get_value([const.CONFIG_CRED, const.CONFIG_CRED_BASE_CRED_URL]) + "/legal_person.json", + csp_vcs['lp']['id']) + self.assertEqual( + self.conf.get_value([const.CONFIG_CRED, const.CONFIG_CRED_BASE_CRED_URL]) + "/legal_person.json#subject", + csp_vcs['lp']['credentialSubject']['id']) + self.assertEqual(self.conf.get_value([const.CONFIG_CSP, const.CONFIG_DID]), csp_vcs['lp']['issuer']) + self.assertIsNotNone(csp_vcs['lp']['proof']) + self.assertEqual("gx:LegalParticipant", csp_vcs['lp']['credentialSubject']['type']) + self.assertEqual("gx:ComplianceCredential", csp_vcs['cs_csp']['credentialSubject']['type']) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_gxdch_services.py b/tests/test_gxdch_services.py new file mode 100644 index 0000000..876c2d6 --- /dev/null +++ b/tests/test_gxdch_services.py @@ -0,0 +1,111 @@ +import unittest +import uuid +from unittest.mock import patch + +from requests.exceptions import HTTPError + +from generator.discovery.gxdch_services import (ComplianceService, + NotaryService, RegistryService) + + +class GxdchTestCase(unittest.TestCase): + + def test_init_notary_service(self): + self.assertRaises(AttributeError, NotaryService, None) + + def test_init_compliance_service(self): + self.assertRaises(AttributeError, ComplianceService, None) + + def test_init_registry_service(self): + self.assertRaises(AttributeError, RegistryService, None) + + @patch("requests.post") + def test_request_registration_number_vc(self, post_mock): + # init test + post_mock.return_value.ok = True + post_mock.return_value.json.return_value = "foo" + cred_id = uuid.uuid4() + cred_subject_id = uuid.uuid4() + + # run test + not_serv = NotaryService("https://exampple.com/gxdch/notary") + resp = not_serv.request_reg_number_vc( + csp={"did": "did:web:example.com", "registration_numbers": {"vat-id": "DE123456789"}}, + cred_subject_id=cred_subject_id, cred_id=cred_id) + + # check results + self.assertEqual( + 'https://exampple.com/gxdch/notary/registrationNumberVC?vcid=' + str(cred_id), + post_mock.call_args.args[0]) + self.assertEqual( + {'json': { + '@context': 'https://registry.lab.gaia-x.eu/development/api/trusted-shape-registry/v1/shapes/jsonld/participant', + 'type': 'gx:legalRegistrationNumber', + 'gx:vatID': 'DE123456789', + 'id': cred_subject_id}}, + post_mock.call_args.kwargs) + self.assertEqual("foo", resp) + + @patch("requests.post") + def test_request_registration_number_vc_exception(self, post_mock): + # init test + post_mock.side_effect = HTTPError(409) + + # run test + not_serv = NotaryService("https://exampple.com/gxdch/notary") + + # check results + self.assertRaises(HTTPError, not_serv.request_reg_number_vc, + csp={"did": "did:web:example.com", "registration_numbers": {"vat-id": "DE123456789"}}, + cred_subject_id="foo", cred_id="bar") + + @patch("requests.post") + def test_request_compliance_vc(self, post_mock): + # init test + post_mock.return_value.ok = True + post_mock.return_value.text = "foo" + + # run test + comp_serv = ComplianceService("https://example.com/gxdch/compliance-service") + resp = comp_serv.request_compliance_vc(vp={"foo": "bar"}, vp_id="example.json") + + # Check results + self.assertEqual(1, post_mock.call_count) + self.assertEqual( + 'https://example.com/gxdch/compliance-service/api/credential-offers?vcid=example.json', + post_mock.call_args.args[0]) + self.assertEqual("{\"foo\": \"bar\"}", post_mock.call_args.args[1]) + self.assertEqual("foo", resp) + + @patch("requests.post") + def test_request_compliance_vc_exception(self, post_mock): + # init test + post_mock.side_effect = HTTPError(409) + + # run test + comp_serv = ComplianceService("https://example.com/gxdch/compliance-service") + + # Check results + self.assertRaises(HTTPError, comp_serv.request_compliance_vc, vp="{\\\"foo\\\": \\\"bar\\\"}", + vp_id="example.json") + + @patch("requests.get") + def test_get_gx_tandc(self, get_mock): + get_mock.return_value.ok = True + get_mock.return_value.json.return_value = {'version': "22.10", "text": "foo"} + + reg = RegistryService("https://example/gxdch/registry") + tandc = reg.get_gx_tandc() + + get_mock.called_with("https://example/gxdch/registry" + "/api/termsAndConditions") + self.assertEqual({'version': "22.10", "text": "foo"}, tandc) + + @patch("requests.get") + def test_get_gx_tandc_exception(self, get_mock): + get_mock.side_effect = HTTPError(409) + reg = RegistryService("https://example/gxdch/registry") + self.assertRaises(HTTPError, reg.get_gx_tandc) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_json_ld.py b/tests/test_json_ld.py index f8da721..0562224 100644 --- a/tests/test_json_ld.py +++ b/tests/test_json_ld.py @@ -36,51 +36,51 @@ def test_get_json_ld_context(self): def test_to_json_ld(self): addr = Address(countryCode="DE") self.assertEqual( - {"@type": "vcard:Address", "gx:countryCode": "DE"}, to_json_ld(addr) + {"type": "vcard:Address", "gx:countryCode": "DE"}, to_json_ld(addr) ) self.assertEqual( - {"@id": "ex:foo", "@type": "vcard:Address", "gx:countryCode": "DE"}, + {"id": "ex:foo", "type": "vcard:Address", "gx:countryCode": "DE"}, to_json_ld(JsonLdObject(gx_object=addr, gx_id="foo")), ) # test attribute is emtpy for value in [None, []]: addr.countryCode = value - self.assertEqual({"@type": "vcard:Address"}, to_json_ld(addr)) + self.assertEqual({"type": "vcard:Address"}, to_json_ld(addr)) # test attribute is an empty list addr.countryCode = ["DE", "FR"] self.assertEqual( - {"@type": "vcard:Address", "gx:countryCode": ["DE", "FR"]}, to_json_ld(addr) + {"type": "vcard:Address", "gx:countryCode": ["DE", "FR"]}, to_json_ld(addr) ) # test attribute is a PermissibleValue addr.countryCode = CountryNameAlpha2.DE self.assertEqual( - {"@type": "vcard:Address", "gx:countryCode": "DE"}, to_json_ld(addr) + {"type": "vcard:Address", "gx:countryCode": "DE"}, to_json_ld(addr) ) # test attribute is EnumDefinitionImpl addr.countryCode = CountryNameAlpha2("DE") self.assertEqual( - {"@type": "vcard:Address", "gx:countryCode": "DE"}, to_json_ld(addr) + {"type": "vcard:Address", "gx:countryCode": "DE"}, to_json_ld(addr) ) # test attribute is date and datetime addr.countryCode = datetime.date(2014, 5, 12) self.assertEqual( { - "@type": "vcard:Address", - "gx:countryCode": {"@type": "xsd:date", "@value": "2014-05-12"}, + "type": "vcard:Address", + "gx:countryCode": {"type": "xsd:date", "@value": "2014-05-12"}, }, to_json_ld(addr), ) addr.countryCode = datetime.datetime(2014, 5, 12, 18, 50, 32) self.assertEqual( { - "@type": "vcard:Address", + "type": "vcard:Address", "gx:countryCode": { - "@type": "xsd:dateTime", + "type": "xsd:dateTime", "@value": "2014-05-12T18:50:32", }, }, @@ -91,17 +91,17 @@ def test_to_json_ld(self): addr.countryCode = 1.5 self.assertEqual( { - "@type": "vcard:Address", - "gx:countryCode": {"@type": "xsd:float", "@value": 1.5}, + "type": "vcard:Address", + "gx:countryCode": {"type": "xsd:float", "@value": 1.5}, }, to_json_ld(addr), ) addr.countryCode = URI("https::example.com") self.assertEqual( { - "@type": "vcard:Address", + "type": "vcard:Address", "gx:countryCode": { - "@type": "xsd:anyURI", + "type": "xsd:anyURI", "@value": "https::example.com", }, }, @@ -110,8 +110,8 @@ def test_to_json_ld(self): addr.countryCode = False self.assertEqual( { - "@type": "vcard:Address", - "gx:countryCode": {"@type": "xsd:boolean", "@value": False}, + "type": "vcard:Address", + "gx:countryCode": {"type": "xsd:boolean", "@value": False}, }, to_json_ld(addr), ) diff --git a/tests/test_openstack_discovery.py b/tests/test_openstack_discovery.py index f058d85..7bc403d 100644 --- a/tests/test_openstack_discovery.py +++ b/tests/test_openstack_discovery.py @@ -37,7 +37,7 @@ def setUp(self): @patch("generator.discovery.openstack.vm_images_discovery.VmImageDiscovery.discover") @patch("generator.discovery.openstack.server_flavor_discovery.ServerFlavorDiscovery.discover") @patch("requests.get") - def test_discovery(self, request_get, flavor_discovery, image_discovery): + def test_generate_gx_credentials(self, request_get, flavor_discovery, image_discovery): # Mock openstack calls request_get.side_effect = [MagicMock(status_code=200, text="foo"), MagicMock(status_code=200, text="foo")] flavor_discovery.return_value = [GX_FLAVOR_1] diff --git a/tests/test_vm_image_discovery.py b/tests/test_vm_image_discovery.py index 91d6a4f..f8e252d 100644 --- a/tests/test_vm_image_discovery.py +++ b/tests/test_vm_image_discovery.py @@ -7,8 +7,9 @@ from generator.common.gx_schema import (CPU, CheckSum, ChecksumAlgorithm, Disk, FirmType, HypervisorType, LatestN, MaintenanceSubscription, Memory, - MemorySize, OperatingSystem, RNGTypes, - Signature, UpdateStrategy, VMDiskType) + MemorySize, OperatingSystem, + OSDistribution, RNGTypes, Signature, + UpdateStrategy, VMDiskType) from generator.common.gx_schema import VMImage as GX_Image from generator.common.gx_schema import WatchDogActions from generator.common.json_ld import JsonLdObject @@ -225,18 +226,18 @@ def test_get_secure_boot(self): def test_get_firmeware_type(self): self.assertEqual( FirmType(FirmType.BIOS), - self.discovery._get_firme_ware_type(OS_Image(hw_firmware_type="BIOS")), + self.discovery._get_firmware_type(OS_Image(hw_firmware_type="BIOS")), ) self.assertEqual( FirmType(FirmType.BIOS), - self.discovery._get_firme_ware_type(OS_Image(hw_firmware_type="bioS")), + self.discovery._get_firmware_type(OS_Image(hw_firmware_type="bioS")), ) self.assertEqual( FirmType(FirmType.other), - self.discovery._get_firme_ware_type(OS_Image(hw_firmware_type="foo")), + self.discovery._get_firmware_type(OS_Image(hw_firmware_type="foo")), ) self.assertEqual( - FirmType(FirmType.other), self.discovery._get_firme_ware_type(OS_Image()) + FirmType(FirmType.other), self.discovery._get_firmware_type(OS_Image()) ) def test_get_watchdog_action(self): @@ -256,10 +257,10 @@ def test_get_watchdog_action(self): WatchDogActions("disabled"), self.discovery._get_watchdog_action(OS_Image()) ) - def test_get_vmpu(self): - self.assertTrue(self.discovery._get_vmpu(OS_Image(hw_pmu=True))) - self.assertFalse(self.discovery._get_vmpu(OS_Image(hw_pmu=False))) - self.assertFalse(self.discovery._get_vmpu(OS_Image())) + def test_get_hw_pmu(self): + self.assertTrue(self.discovery._get_hw_pmu(OS_Image(hw_pmu=True))) + self.assertFalse(self.discovery._get_hw_pmu(OS_Image(hw_pmu=False))) + self.assertFalse(self.discovery._get_hw_pmu(OS_Image())) def test_get_cpu_req(self): self.assertEqual( @@ -871,6 +872,18 @@ def test_get_operating_system(self): OS_Image(os_version="1", os_distro="windows") ), ) + self.assertEqual( + OperatingSystem( + version="1", + osDistribution=OSDistribution.others, + resourcePolicy=const.DEFAULT_RESOURCE_POLICY, + copyrightOwnedBy="TBA", + license=["https://www.example.com/tba"] + ), + self.discovery._get_operation_system( + OS_Image(os_version="1") + ), + ) if __name__ == "__main__":