diff options
author | Michal Jagiello <michal.jagiello@t-mobile.pl> | 2021-11-30 08:25:09 +0000 |
---|---|---|
committer | Michal Jagiello <michal.jagiello@t-mobile.pl> | 2021-12-03 09:58:59 +0000 |
commit | 66e44262b8eb996c06670dcededd899dd1cbd7dc (patch) | |
tree | 3fcea0fe3317f8069281cb93c61add4b1599ab83 | |
parent | 2416a1a546c1d2922c37d513df42e9d26bbaaa42 (diff) |
Data provider release
Change-Id: Ia041a07152e8dabd87de05992d3670cbdc1ddaae
Issue-ID: INT-2010
Signed-off-by: Michal Jagiello <michal.jagiello@t-mobile.pl>
80 files changed, 6007 insertions, 2 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d563def --- /dev/null +++ b/.gitignore @@ -0,0 +1,141 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# VSCode local config directory +.vscode
\ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6f12536 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +FROM python:3.9-slim AS builder + +COPY requirements.txt /opt/app/onap_data_provider/requirements.txt + +WORKDIR /opt/app/onap_data_provider/ + +RUN python -m pip install -r requirements.txt --prefix=/opt/install + +FROM nexus3.onap.org:10001/onap/integration-python:9.1.0 + +COPY --from=builder --chown=onap:onap /opt/install /usr/local + +COPY --chown=onap:onap . /opt/app/onap_data_provider + +WORKDIR /opt/app/onap_data_provider/ + +RUN python setup.py install + +CMD ["onap-data-provider"] @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2021 TNAP / development / system-team + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..578c7fb --- /dev/null +++ b/README.md @@ -0,0 +1,129 @@ +# ONAP data provider + +Data ingestion service for ONAP + +## Description + +Data provider is a project to provide a tool to automate common ONAP resource creation. For many of tasks in ONAP some resources are needed and could be created once, like cloud region, complex or customer in A\&AI. With that tool it can be automated to create them for every ONAP instance. It can be also used to create requested resource on already running instance on demand. + +## Usage + +This project is intended to be included in automation chain, e.g. triggered from the pipeline. +You can also run it locally using Python interpreter or Docker image. + +### Installation + +To run `onap-data-provider` Python >= 3.8 version is required. Install it using + +``` +python setup.py install +``` + +command. You can call then + +``` +onap-data-provider +``` + +command. + +### Run locally + +When installed `onap-data-provider` is ready to work. We need some data to be created. Let's use `samples/vendor.yaml` and create SDC's Vendor resource. Call + +``` +onap-data-provider -f samples/vendor.yaml +``` + +and in your ONAP instance Vendor resource should be created. If that resource already exists no new data will be created. Check `samples` directory to get more examples of files which describes resources to create. + +You can use multiple files as an input: + +``` +onap-data-provider -f samples/vendor.yaml -f samples/vsp.yaml +``` + +Directories could be used as well: + +``` +onap-data-provider -f samples/ +``` + +### Configuration + +Configuration is needed if your environment setup is different that usuall so ONAP components listen on different hosts/ports than default, so are available on other URLs than: + +``` +AAI_URL = "https://aai.api.sparky.simpledemo.onap.org:30233" +CDS_URL = "http://portal.api.simpledemo.onap.org:30449" +MSB_URL = "https://msb.api.simpledemo.onap.org:30283" +SDC_BE_URL = "https://sdc.api.be.simpledemo.onap.org:30204" +SDC_FE_URL = "https://sdc.api.fe.simpledemo.onap.org:30207" +SDNC_URL = "https://sdnc.api.simpledemo.onap.org:30267" +SO_URL = "http://so.api.simpledemo.onap.org:30277" +VID_URL = "https://vid.api.simpledemo.onap.org:30200" +CLAMP_URL = "https://clamp.api.simpledemo.onap.org:30258" +VES_URL = "http://ves.api.simpledemo.onap.org:30417" +DMAAP_URL = "http://dmaap.api.simpledemo.onap.org:3904" +``` + +If you want to use another URLs you need to override default `onap-data-provider` settings by create Python file with values you want to use. Example: I want to test `onap-data-provider` data creation on my "test" ONAP instance which is available on "172.17.0.1" IP address, so I need to create `my_test_onap_instance_settings.py` Python file which looks: + +``` +AAI_URL = "https://172.17.0.1:30233" +CDS_URL = "http://172.17.0.1:30449" +MSB_URL = "https://172.17.0.1:30283" +SDC_BE_URL = "https://172.17.0.1:30204" +SDC_FE_URL = "https://172.17.0.1:30207" +SDNC_URL = "https://172.17.0.1:30267" +SO_URL = "http://172.17.0.1:30277" +VID_URL = "https://172.17.0.1:30200" +CLAMP_URL = "https://172.17.0.1:30258" +VES_URL = "http://172.17.0.1:30417" +DMAAP_URL = "http://172.17.0.1:3904" +``` + +and then if I call + +``` +ONAP_PYTHON_SDK_SETTINGS=my_test_onap_instance_settings onap-data-provider ... +``` + +all data are going to be created on my local instance. + +### Set proxy + +ONAP data provider can be run with proxy configured. You need to pass urls you want to use for proxy connection as `--proxy` arguments. Call `onap-data-provider -f <infra-file> --proxy http://localhost:8080 https://localhost:8080` to setup proxy for `http` and `https` on `localhost:8080` address. + +## Data verification + +You can verify the data provided is correct, before you would try to actually push it +to the ONAP instance. To do so, use the flag `--validate-only`: + +``` +onap-data-provider -f samples/vendor.yml --validate-only +``` + +For reference, please see example data files under `samples/` directory. + +## Development and testing + +The following utilities are used within the project: + +- Black +- mypy +- pydocstyle + +To run all the tests (unit tests, linter and mypy checks), install tox and then run it: + +``` +pip install tox +tox . +``` + +## Licenses + +The software that data-provider is built on uses the following licenses. + +- Apache 2 License: onapsdk +- MIT license: PyYAML, jsonschema diff --git a/onap_data_provider/__init__.py b/onap_data_provider/__init__.py new file mode 100644 index 0000000..710af4e --- /dev/null +++ b/onap_data_provider/__init__.py @@ -0,0 +1,16 @@ +"""ONAP data provider package.""" +""" + Copyright 2021 Deutsche Telekom AG + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" diff --git a/onap_data_provider/config_loader.py b/onap_data_provider/config_loader.py new file mode 100644 index 0000000..5757e1e --- /dev/null +++ b/onap_data_provider/config_loader.py @@ -0,0 +1,70 @@ +"""Data loader module.""" +""" + Copyright 2021 Deutsche Telekom AG + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" +from pathlib import Path +from typing import Any, Iterator, List +import yaml +from onap_data_provider.tag_handlers import join, generate_random_uuid + +# register custom tag handlers in yaml.SafeLoader +yaml.add_constructor("!join", join, yaml.SafeLoader) +yaml.add_constructor("!uuid4", generate_random_uuid, yaml.SafeLoader) + + +class ConfigLoader: + """Configuration loader class. + + Loads data from file resource. + """ + + YAML_EXTENSIONS = {".yml", ".yaml"} + + def __init__(self, config_file_path: List[Path]) -> None: + """Initialize configuration loader class. + + Args: + config_file_path (str): Path to yaml data source file. + + """ + self.config_file_path: List[Path] = config_file_path + + def _yamls_from_dir(self, dir: Path) -> Iterator[Path]: + for child in dir.iterdir(): # type: Path + if child.suffix in self.YAML_EXTENSIONS: + yield child + + @property + def _yamls(self) -> Iterator[Path]: + for config_file_path in self.config_file_path: # type: Path + if config_file_path.is_file(): + yield config_file_path + elif config_file_path.is_dir(): + yield from self._yamls_from_dir(config_file_path) + else: + raise ValueError("Provided path is neither file nor directory") + + def load(self) -> Iterator[Any]: + """Get data from the config file. + + Get data from the config file and return parsed to dictionary resource. + + Returns: + Any: Data from yaml file. + + """ + for yaml_path in self._yamls: # type: Path + with yaml_path.open() as f: + yield yaml.safe_load(f) diff --git a/onap_data_provider/config_parser.py b/onap_data_provider/config_parser.py new file mode 100644 index 0000000..e734b72 --- /dev/null +++ b/onap_data_provider/config_parser.py @@ -0,0 +1,173 @@ +"""Data parser module.""" +""" + Copyright 2021 Deutsche Telekom AG + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" +from collections import OrderedDict +from pathlib import Path +from typing import Any, Dict, Iterator, List, Optional +from .config_loader import ConfigLoader +from .resources.resource import Resource +from .resources.resource_creator import ResourceCreator +from .validator import Validator +from .versions import VersionsEnum + + +class Config: + """Config class.""" + + VERSION_TAG = "odpSchemaVersion" + + def __init__(self, config: Dict[str, Any]) -> None: + """Initialize config object. + + Args: + config (Dict[str, Any]): Entites files content loaded by loader. + + """ + self.config: Dict[str, Any] = config + + @property + def version(self) -> VersionsEnum: + """Config file version. + + Files with entities are versioned to keep backward compatibility. + Each config keep the version number and that value is represented + by that property. + + Returns: + VersionsEnum: VersionsEnum class object + + """ + return VersionsEnum.get_version_by_number( + str(self.config.get(self.VERSION_TAG)) + ) + + @property + def resources(self) -> Dict[str, Any]: + """Resources dictionary. + + Dictionary with definition of objects to be created in ONAP. + + Returns: + Dict[str, Any]: Resources dictionary + + """ + if self.version == VersionsEnum.NONE: + return self.config + resources: Dict[str, Any] = self.config["resources"] + return resources + + +class ConfigParser: + """Configuration parser class. + + Processes data loaded from resource. + """ + + def __init__(self, config_file_path: List[Path]) -> None: + """Initialize configuration parser class. + + Args: + config_file_path (str): Path to yaml data source file. + + """ + self._config_file_path: List[Path] = config_file_path + self._config_loader: ConfigLoader = ConfigLoader(self._config_file_path) + self._configs: Optional[List[Config]] = None + self._validator: Optional[Validator] = None + self._PRIORITY_ORDER = ( + "complexes", + "cloud-regions", + "vendors", + "vsps", + "pnfs", + "vnfs", + "services", + "customers", + "msb-k8s-definitions", + "aai-services", + "service-instances", + ) + + def parse(self) -> Iterator[Resource]: + """Parser method. + + Invokes factory method to create objects from nested data dictionary. + + Returns: + Iterator[Resource]: Iterator of Resource type objects. + + """ + for config in self.configs: + for resource in self._get_ordered_resources(config.resources): + for resource_type, data in resource.items(): + yield ResourceCreator.create(resource_type, data, config.version) + + def _get_ordered_resources( + self, resources_data: Dict[str, Any] + ) -> Iterator[Dict[str, Any]]: + """Resources helper method. + + Generates data in fixed order defined in _PRIORITY_ORDER property. + + Args: + resources_data (Dict[str, Any]): Dictionary generated from YAML infra file. + + Returns: + Dict[str, Any]: Iterator of Dict type objects where key is the name + of resource type, and the value is actual resource data. + + """ + ordered_resources: Dict[str, Any] = OrderedDict.fromkeys( + self._PRIORITY_ORDER, {} + ) + ordered_resources.update(resources_data) + for ordered_resource in ordered_resources.values(): + for resource_data in ordered_resource: + yield resource_data + + @property + def configs(self) -> List[Config]: + """Config loaded using loader. + + Returns: + Dict[str, Any]: Config + + """ + if self._configs is None: + self._configs = [Config(config) for config in self._config_loader.load()] + return self._configs + + @property + def validator(self) -> Validator: + """Property which stores validator object. + + Used to validate provided data. + + Returns: + Validator: Validator object + + """ + if not self._validator: + self._validator = Validator() + return self._validator + + def validate(self) -> None: + """Validate provided resources. + + Checks whether the data provided by the user are correct. + """ + for config in self.configs: + self.validator.validate(config.version, config.resources) diff --git a/onap_data_provider/data_provider.py b/onap_data_provider/data_provider.py new file mode 100644 index 0000000..0cf941e --- /dev/null +++ b/onap_data_provider/data_provider.py @@ -0,0 +1,106 @@ +"""Main project class.""" +""" + Copyright 2021 Deutsche Telekom AG + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" + +import argparse +import logging +import logging.config +import os +import sys +from pathlib import Path + +from onapsdk.onap_service import OnapService # type: ignore + +from onap_data_provider.config_parser import ConfigParser + + +logging.config.dictConfig( + { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "odp": { + "class": "logging.Formatter", + "format": "%(asctime)s [%(levelname)s] %(module)s: %(message)s", + } + }, + "handlers": { + "console": { + "class": "logging.StreamHandler", + "level": os.getenv("LOGGING_LEVEL", "INFO").upper(), + "formatter": "odp", + }, + "file": { + "class": "logging.FileHandler", + "level": "DEBUG", + "filename": "odp.log", + "mode": "w", + "formatter": "odp", + }, + }, + "loggers": { + "": {"level": "DEBUG", "handlers": ["console", "file"]}, + }, + } +) + + +def create_parser() -> argparse.ArgumentParser: + """Create argument parser.""" + parser: argparse.ArgumentParser = argparse.ArgumentParser( + description="ONAP data provider" + ) + parser.add_argument( + "-f", + "--filename", + type=Path, + action="append", + dest="infra_files", + required=True, + help="Path to the infra file which describes resources to create. Can be directory as well", + ) + parser.add_argument( + "--validate-only", + action="store_true", + help="Doesn't create any resources - checks only if data in infra file has valid format", + ) + parser.add_argument( + "--proxy", + nargs="*", + help="Setup proxy connection with given url. Provide full URL with protocol, eg. http://localhost:8080", + ) + return parser + + +def run() -> None: + """Project main function.""" + parser: argparse.ArgumentParser = create_parser() + args: argparse.Namespace = parser.parse_args() + if args.proxy: + OnapService.set_proxy( + {url.split("://")[0]: url.split("://")[1] for url in args.proxy} + ) + conf_parser = ConfigParser(args.infra_files) + conf_parser.validate() + if args.validate_only: + print("Input data is valid!") + sys.exit(0) + for x in conf_parser.parse(): + x.create() + + +if __name__ == "__main__": + run() diff --git a/onap_data_provider/resources/__init__.py b/onap_data_provider/resources/__init__.py new file mode 100644 index 0000000..dbcdf74 --- /dev/null +++ b/onap_data_provider/resources/__init__.py @@ -0,0 +1,16 @@ +"""Resources package.""" +""" + Copyright 2021 Deutsche Telekom AG + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" diff --git a/onap_data_provider/resources/aai_service_resource.py b/onap_data_provider/resources/aai_service_resource.py new file mode 100644 index 0000000..8fbc119 --- /dev/null +++ b/onap_data_provider/resources/aai_service_resource.py @@ -0,0 +1,83 @@ +"""A&AI service model resource module.""" +""" + Copyright 2021 Deutsche Telekom AG + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" +import logging +from typing import Any, Dict + +from onapsdk.aai.service_design_and_creation import Service as AaiService # type: ignore +from onapsdk.exceptions import ResourceNotFound # type: ignore + +from .resource import Resource + + +class AaiServiceResource(Resource): + """A&AI service model resource class.""" + + def __init__(self, data: Dict[str, Any]) -> None: + """Initialize A&AI SDC service resource. + + Args: + data (Dict[str, Any]): Data needed to create resource. + + """ + super().__init__(data) + self._aai_service: AaiService = None + + def create(self) -> None: + """Create aai service resource.""" + if not self.exists: + logging.debug("Create AaiService %s", self.data["service-id"]) + AaiService.create( + service_id=self.data["service-id"], + service_description=self.data["service-description"], + ) + + @property + def exists(self) -> bool: + """Determine if resource already exists or not. + + Returns: + bool: True if object exists, False otherwise + + """ + return self.aai_service is not None + + @property + def aai_service(self) -> AaiService: + """A&AI service property. + + A&AI servic emodel which is represented by the data provided by user. + + Returns: + AaiService: A&AI service model object + + """ + if not self._aai_service: + try: + for aai_service in AaiService.get_all(): + if ( + aai_service.service_id == self.data["service-id"] + and aai_service.service_description + == self.data["service-description"] + ): + self._aai_service = aai_service + return self._aai_service + except ResourceNotFound: + logging.error( + "A&AI service %s does not exist", + self.data["service-id"], + ) + return self._aai_service diff --git a/onap_data_provider/resources/cloud_region_resource.py b/onap_data_provider/resources/cloud_region_resource.py new file mode 100644 index 0000000..7bcc3b4 --- /dev/null +++ b/onap_data_provider/resources/cloud_region_resource.py @@ -0,0 +1,177 @@ +"""Cloud region resource module.""" +""" + Copyright 2021 Deutsche Telekom AG + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" + +from onap_data_provider.resources.esr_system_info_resource import ( + EsrSystemInfoResource, +) +import logging +from typing import Any, Dict + +from onapsdk.aai.cloud_infrastructure import CloudRegion, Complex # type: ignore +from onapsdk.msb.k8s.connectivity_info import ConnectivityInfo # type: ignore +from onapsdk.so.so_db_adapter import SoDbAdapter, IdentityService # type: ignore + +from .resource import Resource +from .tenant_resource import TenantResource +from onapsdk.exceptions import APIError, ResourceNotFound # type: ignore + + +class CloudRegionResource(Resource): + """Cloud region resource class. + + Creates cloud region. + """ + + def __init__(self, data: Dict[str, Any]) -> None: + """Initialize cloud region resource. + + Args: + data (Dict[str, Any]): Data needed to create resource. + + """ + super().__init__(data) + self._cloud_region: CloudRegion = None + + def create(self) -> None: + """Create cloud region resource. + + Create cloud region resource and all related resources. + + """ + logging.debug("Create CloudRegion %s", self.data["cloud-region-id"]) + if not self.exists: + self._cloud_region = CloudRegion.create( + cloud_owner=self.data["cloud-owner"], + cloud_region_id=self.data["cloud-region-id"], + orchestration_disabled=self.data["orchestration-disabled"], + in_maint=self.data["in-maint"], + cloud_type=self.data.get("cloud-region-type", "openstack"), + cloud_region_version="pike", + ) + + # Create tenants + for tenant_data in self.data.get("tenants", []): + tenant_resource = TenantResource( + tenant_data, cloud_region=self._cloud_region + ) + tenant_resource.create() + + # Link with complex + if ( + complex_physical_id := self.data.get("complex", {}).get( + "physical-location-id" + ) + ) is not None: + self._link_to_complex(complex_physical_id) + + # Add availability zones + try: + for az_data in self.data.get("availability-zones", []): + self.cloud_region.add_availability_zone( + availability_zone_name=az_data["availability-zone-name"], + availability_zone_hypervisor_type=az_data["hypervisor-type"], + ) + except APIError: + logging.error("Availability zone update not supported.") + + # Create external system infos + for esr_system_info_data in self.data.get("esr-system-infos", []): + esr_system_info_resource: EsrSystemInfoResource = EsrSystemInfoResource( + esr_system_info_data, cloud_region=self._cloud_region + ) + esr_system_info_resource.create() + + if self.data.get("register-to-multicloud", False): + self.cloud_region.register_to_multicloud() + + # Create connectivity info for Cloud region if it's type is k8s + if self.cloud_region.cloud_type == "k8s": + try: + ConnectivityInfo.get_connectivity_info_by_region_id( + self.cloud_region.cloud_region_id + ) + except APIError: + with open(self.data["kube-config"], "rb") as kube_config: + ConnectivityInfo.create( + cloud_owner=self.cloud_region.cloud_owner, + cloud_region_id=self.cloud_region.cloud_region_id, + kubeconfig=kube_config.read(), + ) + if not self.cloud_region.complex: + logging.error( + "k8s cloud region should have complex linked to create SO cloud site DB entry" + ) + else: + SoDbAdapter.add_cloud_site( + self.cloud_region.cloud_region_id, + self.cloud_region.complex.physical_location_id, + IdentityService("DEFAULT_KEYSTONE"), + ) + + @property + def exists(self) -> bool: + """Determine if resource already exists or not. + + Returns: + bool: True if object exists, False otherwise + + """ + return self.cloud_region is not None + + @property + def cloud_region(self) -> CloudRegion: + """Cloud region property. + + Cloud region which is represented by the data provided by user. + + Returns: + CloudRegion: Cloud region object + + """ + if not self._cloud_region: + try: + self._cloud_region = CloudRegion.get_by_id( + self.data["cloud-owner"], self.data["cloud-region-id"] + ) + except ResourceNotFound: + logging.error( + "Cloud region %s does not exist", + self.data["cloud-region-id"], + ) + return None + return self._cloud_region + + def _link_to_complex(self, complex_physical_id: str) -> None: + try: # TODO: change it when https://gitlab.com/Orange-OpenSource/lfn/onap/python-onapsdk/-/issues/120 is fixed + if self.cloud_region.complex: + logging.info( + "Cloud region has relationship with complex: %s. New relationship can't be created", + self.cloud_region.complex.physical_location_id, + ) + return + except ResourceNotFound: + logging.debug("Cloud region has no complex linked with") + try: + complex: Complex = next( + Complex.get_all(physical_location_id=complex_physical_id) + ) + self.cloud_region.link_to_complex(complex) + except StopIteration: + logging.error( + "Complex %s does not exist, please create it before cloud region creation", + complex_physical_id, + ) diff --git a/onap_data_provider/resources/complex_resource.py b/onap_data_provider/resources/complex_resource.py new file mode 100644 index 0000000..82ab462 --- /dev/null +++ b/onap_data_provider/resources/complex_resource.py @@ -0,0 +1,92 @@ +"""Complex resource module.""" +""" + Copyright 2021 Deutsche Telekom AG + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" + +import logging +from typing import Any, Dict + +from onapsdk.aai.cloud_infrastructure import Complex # type: ignore + +from .resource import Resource +from onapsdk.exceptions import ResourceNotFound # type: ignore + + +class ComplexResource(Resource): + """Complex resource class.""" + + def __init__(self, data: Dict[str, Any]) -> None: + """Complex resource initialization. + + Args: + data (Dict[str, Any]): Data needed to create complex + + """ + super().__init__(data) + self._complex: Complex = None + + def create(self) -> None: + """Create complex resource.""" + if not self.exists: + self._complex = Complex.create( + physical_location_id=self.data["physical-location-id"], + name=self.data.get("complex-name"), + data_center_code=self.data.get("data-center-code"), + identity_url=self.data.get("identity-url"), + physical_location_type=self.data.get("physical-location-type"), + street1=self.data.get("street1"), + street2=self.data.get("street2"), + city=self.data.get("city"), + state=self.data.get("state"), + postal_code=self.data.get("postal-code"), + country=self.data.get("country"), + region=self.data.get("region"), + latitude=self.data.get("latitude"), + longitude=self.data.get("longitude"), + elevation=self.data.get("elevation"), + lata=self.data.get("lata"), + ) + + @property + def exists(self) -> bool: + """Determine if resource already exists or not. + + Returns: + bool: True if object exists, False otherwise + + """ + return self.complex is not None + + @property + def complex(self) -> Complex: + """Complex property. + + Returns: + Complex: Complex object + + """ + if not self._complex: + try: + self._complex = next( + Complex.get_all( + physical_location_id=self.data["physical-location-id"] + ) + ) + except ResourceNotFound: + logging.error( + "Complex %s does not exist", self.data["physical-location-id"] + ) + return None + return self._complex diff --git a/onap_data_provider/resources/customer_resource.py b/onap_data_provider/resources/customer_resource.py new file mode 100644 index 0000000..2bbb1ef --- /dev/null +++ b/onap_data_provider/resources/customer_resource.py @@ -0,0 +1,189 @@ +"""Customer resource module.""" +""" + Copyright 2021 Deutsche Telekom AG + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" + +import logging +from typing import Any, Dict + +from onapsdk.aai.business import Customer, ServiceSubscription # type: ignore +from onapsdk.aai.cloud_infrastructure import CloudRegion, Tenant # type: ignore + +from onapsdk.sdc.service import Service # type: ignore + +from .resource import Resource +from onapsdk.exceptions import ResourceNotFound # type: ignore + + +class CustomerResource(Resource): + """Customer resource class. + + Creates customer. + """ + + def __init__(self, data: Dict[str, Any]) -> None: + """Initialize customer resource. + + Args: + data (Dict[str, Any]): Data needed to create resource. + + """ + super().__init__(data) + self._customer: Customer = None + + def create(self) -> None: + """Create customer resource. + + Create customer resource and all related resources. + + """ + logging.debug("Create Customer %s", self.data["global-customer-id"]) + if not self.exists: + self._customer = Customer.create( + global_customer_id=self.data["global-customer-id"], + subscriber_name=self.data["subscriber-name"], + subscriber_type=self.data["subscriber-type"], + ) + + for service_subscription in self.data.get("service-subscriptions", []): + resource = CustomerResource.ServiceSubscriptionResource( + service_subscription, self._customer + ) + resource.create() + + @property + def exists(self) -> bool: + """Determine if resource already exists or not. + + Returns: + bool: True if object exists, False otherwise + + """ + return self.customer is not None + + @property + def customer(self) -> Customer: + """Access to customer property. + + Customer property containing Customer object. + + Returns: + Customer: Customer object + + """ + if not self._customer: + try: + self._customer = Customer.get_by_global_customer_id( + self.data["global-customer-id"] + ) + except ResourceNotFound: + logging.error( + "Customer %s does not exist", + self.data["global-customer-id"], + ) + return None + return self._customer + + class ServiceSubscriptionResource(Resource): + """Service subscription class. + + Creates service subscription. + """ + + def __init__(self, data: Dict[str, str], customer: Customer) -> None: + """Initialize service subscription resource. + + Args: + data (Dict[str, str]): Data needed to create resource. + customer (Customer): Related Customer object. + + """ + super().__init__(data) + self._service_subscription: ServiceSubscription = None + self._customer: Customer = customer + + def create(self) -> None: + """Create Service subscription resource. + + Create service subscription resource belonging to a customer. + + """ + logging.debug("Create ServiceSubscription %s", self.data["service-type"]) + if not self.exists: + self._service_subscription = self._customer.subscribe_service( + Service(self.data["service-type"]) + ) + + for tenant_cloud_region_data in self.data.get("tenants", []): + try: + cloud_region: CloudRegion = CloudRegion.get_by_id( + tenant_cloud_region_data["cloud-owner"], + tenant_cloud_region_data["cloud-region-id"], + ) + except ResourceNotFound: + logging.error( + f"Cloud region {tenant_cloud_region_data['cloud-owner']} {tenant_cloud_region_data['cloud-region-id']} does not exists" + ) + continue + try: + tenant: Tenant = cloud_region.get_tenant( + tenant_cloud_region_data["tenant-id"] + ) + except ResourceNotFound: + logging.error( + f"Tenant {tenant_cloud_region_data['tenant-id']} does not exist" + ) + continue + + self.service_subscription.link_to_cloud_region_and_tenant( + cloud_region, tenant + ) + logging.debug( + f"Service subscription linked to {tenant.name} tenant and {cloud_region.cloud_region_id} cloud region" + ) + + @property + def exists(self) -> bool: + """Determine if resource already exists or not. + + Returns: + bool: True if object exists, False otherwise + + """ + return self.service_subscription is not None + + @property + def service_subscription(self) -> ServiceSubscription: + """Get ServiceSubscription instance. + + Get ServiceSubscription instance. + + Returns: + ServiceSubscription: Created `ServiceSubscription` subclass instance. + """ + if not self._service_subscription: + try: + self._service_subscription = ( + self._customer.get_service_subscription_by_service_type( + self.data["service-type"] + ) + ) + except ResourceNotFound: + logging.error( + "Service type %s does not exist", + self.data["service-type"], + ) + return None + return self._service_subscription diff --git a/onap_data_provider/resources/esr_system_info_resource.py b/onap_data_provider/resources/esr_system_info_resource.py new file mode 100644 index 0000000..4c26bbb --- /dev/null +++ b/onap_data_provider/resources/esr_system_info_resource.py @@ -0,0 +1,114 @@ +"""External system info resource module.""" +""" + Copyright 2021 Deutsche Telekom AG + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" +import logging +from typing import Any, Dict + +from onapsdk.aai.cloud_infrastructure import CloudRegion, EsrSystemInfo # type: ignore +from onapsdk.exceptions import APIError # type: ignore + +from .resource import Resource + + +class EsrSystemInfoResource(Resource): + """ESR system info resource class.""" + + def __init__(self, data: Dict[str, Any], cloud_region: CloudRegion) -> None: + """ESR system info resource initialization. + + Args: + data (Dict[str, Any]): Data needed to create esr system info + cloud_region (CloudRegion): Cloud region for which esr system info is going to be created + + """ + super().__init__(data) + self.cloud_region: CloudRegion = cloud_region + self._esr_system_info: EsrSystemInfo = None + + @staticmethod + def get_esr_info_by_id( + cloud_region: CloudRegion, esr_syste_info_id: str + ) -> EsrSystemInfo: + """Get esr system info from Cloud region by it's ID. + + Iterate through cloud region's esr system infos and check + if it's already have some with provided ID. + + Args: + cloud_region (CloudRegion): CloudRegion object to check if esr system info already exists + esr_syste_info_id (str): ESR system info ID to check. + + Returns: + EsrSystemInfo: ESR system info object + """ + for esr_system_info in cloud_region.esr_system_infos: + if esr_system_info.esr_system_info_id == esr_syste_info_id: + return esr_system_info + + def create(self) -> None: + """Create ESR system info resource. + + Add ESR system info to provided cloud region + + """ + logging.debug( + "Create ESR system info for %s cloud region", + self.cloud_region.cloud_region_id, + ) + if not self.exists: + self.cloud_region.add_esr_system_info( + esr_system_info_id=self.data["esr-system-info-id"], + user_name=self.data["user-name"], + password=self.data["password"], + system_type=self.data["system-type"], + service_url=self.data["service-url"], + system_status="active", + cloud_domain=self.data["cloud-domain"], + default_tenant=self.data.get("default-tenant"), + ) + self._esr_system_info = self.get_esr_info_by_id( + self.cloud_region, self.data["esr-system-info-id"] + ) + + @property + def exists(self) -> bool: + """Determine if resource already exists or not. + + Returns: + bool: True if object exists, False otherwise + + """ + return self.esr_system_info is not None + + @property + def esr_system_info(self) -> EsrSystemInfo: + """External system info property. + + Returns: + EsrSystemInfo: EsrSystemInfo object + + """ + if self._esr_system_info is None: + try: + if ( + esr_system_info := self.get_esr_info_by_id( + self.cloud_region, self.data["esr-system-info-id"] + ) + ) is not None: + self._esr_system_info = esr_system_info + except APIError: + logging.info("No esr system infos") + return self._esr_system_info diff --git a/onap_data_provider/resources/line_of_business_resource.py b/onap_data_provider/resources/line_of_business_resource.py new file mode 100644 index 0000000..0150746 --- /dev/null +++ b/onap_data_provider/resources/line_of_business_resource.py @@ -0,0 +1,73 @@ +"""Line of business resource module.""" +""" + Copyright 2021 Deutsche Telekom AG + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" +import logging +from typing import Any, Dict, Optional + +from onapsdk.aai.business import LineOfBusiness # type: ignore +from onapsdk.exceptions import ResourceNotFound # type: ignore + +from .resource import Resource + + +class LineOfBusinessResource(Resource): + """Line of business resource class. + + Creates A&AI line of business. + """ + + def __init__(self, data: Dict[str, Any]) -> None: + """Initialize line of business resource. + + Args: + data (Dict[str, Any]): Data needed to create resource. + + """ + super().__init__(data) + self._line_of_business: Optional[LineOfBusiness] = None + + def create(self) -> None: + """Create line of business resource.""" + logging.debug(f"Create Line of business {self.data['name']}") + if not self.exists: + self._line_of_business = LineOfBusiness.create(self.data["name"]) + + @property + def exists(self) -> bool: + """Determine if resource already exists or not. + + Returns: + bool: True if object exists, False otherwise + + """ + return bool(self.line_of_business) + + @property + def line_of_business(self) -> LineOfBusiness: + """Line of business property. + + Line of business which is represented by the data provided by user. + + Returns: + LineOfBusiness: Line of business object + + """ + if not self._line_of_business: + try: + self._line_of_business = LineOfBusiness.get_by_name(self.data["name"]) + except ResourceNotFound: + return None + return self._line_of_business diff --git a/onap_data_provider/resources/msb_k8s_definition.py b/onap_data_provider/resources/msb_k8s_definition.py new file mode 100644 index 0000000..b4b3342 --- /dev/null +++ b/onap_data_provider/resources/msb_k8s_definition.py @@ -0,0 +1,85 @@ +"""MSB K8S definition resource module.""" +""" + Copyright 2021 Deutsche Telekom AG + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" + +import logging +from typing import Any, Dict, Optional + +from onapsdk.exceptions import ResourceNotFound # type: ignore +from onapsdk.msb.k8s.definition import Definition # type: ignore + +from .msb_k8s_profile import MsbK8SProfileResource +from .resource import Resource + + +class MsbK8SDefinitionResource(Resource): + """Definition resource class. + + Creates MSB Kubernetes plugin's definition. + """ + + def __init__(self, data: Dict[str, Any]) -> None: + """Initialize definition resource. + + Args: + data (Dict[str, Any]): Data needed to create resource. + + """ + super().__init__(data) + self._definition: Optional[Definition] = None + + def create(self) -> None: + """Create definition if not already exists.""" + if not self.exists: + self._definition = Definition.create( + self.data["name"], + self.data["version"], + self.data.get("chart-name"), + self.data.get("description"), + ) + with open(self.data["artifact"], "rb") as artifact: + self._definition.upload_artifact(artifact.read()) + for profile_data in self.data.get("profiles", []): + MsbK8SProfileResource(profile_data, self.definition).create() + + @property + def exists(self) -> bool: + """Determine if resource already exists or not. + + Returns: + bool: True if object exists, False otherwise + + """ + return self.definition is not None + + @property + def definition(self) -> Optional[Definition]: + """Definition property. + + Definition which is represented by the data provided by user. + + Returns: + Definition: Definition object + + """ + if not self._definition: + try: + self._definition = Definition.get_definition_by_name_version( + self.data["name"], self.data["version"] + ) + except ResourceNotFound: + logging.error("Definition %s does not exist", self.data["rb-name"]) + return self._definition diff --git a/onap_data_provider/resources/msb_k8s_profile.py b/onap_data_provider/resources/msb_k8s_profile.py new file mode 100644 index 0000000..ae884c2 --- /dev/null +++ b/onap_data_provider/resources/msb_k8s_profile.py @@ -0,0 +1,80 @@ +"""MSB K8S definition profile resource module.""" +""" + Copyright 2021 Deutsche Telekom AG + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" + +import logging +from typing import Any, Dict, Optional + +from onapsdk.exceptions import ResourceNotFound # type: ignore +from onapsdk.msb.k8s.definition import Definition, Profile # type: ignore + +from .resource import Resource + + +class MsbK8SProfileResource(Resource): + """Profile resource class. + + Creates MSB Kubernetes plugin's profile + """ + + def __init__(self, data: Dict[str, Any], definition: Definition) -> None: + """Initialize definition resource. + + Args: + data (Dict[str, Any]): Data needed to create resource. + + """ + super().__init__(data) + self._profile: Optional[Profile] = None + self.definition: Definition = definition + + def create(self) -> None: + """Create profile if not already exists.""" + if not self.exists: + self._profile = self.definition.create_profile( + self.data["name"], self.data["namespace"], self.data["k8s-version"] + ) + with open(self.data["artifact"], "rb") as artifact: + self._profile.upload_artifact(artifact.read()) + + @property + def exists(self) -> bool: + """Determine if resource already exists or not. + + Returns: + bool: True if object exists, False otherwise + + """ + return self.profile is not None + + @property + def profile(self) -> Optional[Profile]: + """Profile property. + + Profile which is represented by the data provided by user. + + Returns: + Profile: Profile object + + """ + if not self._profile: + try: + self._profile = self.definition.get_profile_by_name( + self.data["rb-name"] + ) + except ResourceNotFound: + logging.error("Profile %s not found", self.data["name"]) + return self._profile diff --git a/onap_data_provider/resources/owning_entity_resource.py b/onap_data_provider/resources/owning_entity_resource.py new file mode 100644 index 0000000..496ec22 --- /dev/null +++ b/onap_data_provider/resources/owning_entity_resource.py @@ -0,0 +1,75 @@ +"""Owning entity resource module.""" +""" + Copyright 2021 Deutsche Telekom AG + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" +import logging +from typing import Any, Dict, Optional + +from onapsdk.aai.business import OwningEntity # type: ignore +from onapsdk.exceptions import ResourceNotFound # type: ignore + +from .resource import Resource + + +class OwningEntityResource(Resource): + """Owning entity resource class. + + Creates A&AI line of business. + """ + + def __init__(self, data: Dict[str, Any]) -> None: + """Initialize line of business resource. + + Args: + data (Dict[str, Any]): Data needed to create resource. + + """ + super().__init__(data) + self._owning_entity: Optional[OwningEntity] = None + + def create(self) -> None: + """Create line of business resource.""" + logging.debug(f"Create Owning entity {self.data['name']}") + if not self.exists: + self._owning_entity = OwningEntity.create(self.data["name"]) + + @property + def exists(self) -> bool: + """Determine if resource already exists or not. + + Returns: + bool: True if object exists, False otherwise + + """ + return bool(self.owning_entity) + + @property + def owning_entity(self) -> OwningEntity: + """Owning entity property. + + Owning entity which is represented by the data provided by user. + + Returns: + OwningEntity: Owning entity object + + """ + if not self._owning_entity: + try: + self._owning_entity = OwningEntity.get_by_owning_entity_name( + self.data["name"] + ) + except ResourceNotFound: + return None + return self._owning_entity diff --git a/onap_data_provider/resources/platform_resource.py b/onap_data_provider/resources/platform_resource.py new file mode 100644 index 0000000..5e8893c --- /dev/null +++ b/onap_data_provider/resources/platform_resource.py @@ -0,0 +1,73 @@ +"""Platform resource module.""" +""" + Copyright 2021 Deutsche Telekom AG + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" +import logging +from typing import Any, Dict, Optional + +from onapsdk.aai.business import Platform # type: ignore +from onapsdk.exceptions import ResourceNotFound # type: ignore + +from .resource import Resource + + +class PlatformResource(Resource): + """Platform resource class. + + Creates A&AI platform. + """ + + def __init__(self, data: Dict[str, Any]) -> None: + """Initialize platform resource. + + Args: + data (Dict[str, Any]): Data needed to create resource. + + """ + super().__init__(data) + self._platform: Optional[Platform] = None + + def create(self) -> None: + """Create platform resource.""" + logging.debug(f"Create Platform {self.data['name']}") + if not self.exists: + self._platform = Platform.create(self.data["name"]) + + @property + def exists(self) -> bool: + """Determine if resource already exists or not. + + Returns: + bool: True if object exists, False otherwise + + """ + return bool(self.platform) + + @property + def platform(self) -> Platform: + """Platform property. + + Platform which is represented by the data provided by user. + + Returns: + Platform: Platform object + + """ + if not self._platform: + try: + self._platform = Platform.get_by_name(self.data["name"]) + except ResourceNotFound: + return None + return self._platform diff --git a/onap_data_provider/resources/pnf_resource.py b/onap_data_provider/resources/pnf_resource.py new file mode 100644 index 0000000..553018b --- /dev/null +++ b/onap_data_provider/resources/pnf_resource.py @@ -0,0 +1,78 @@ +"""Pnf resource module.""" +""" + Copyright 2021 Deutsche Telekom AG + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" + +import logging +from typing import Any, Dict + +from onapsdk.sdc.pnf import Pnf # type: ignore +from onapsdk.sdc.vendor import Vendor # type: ignore +from .resource import Resource +from .xnf_resource import XnfResource + + +class PnfResource(Resource, XnfResource): + """Pnf resource class. + + Creates pnf. + """ + + def __init__(self, data: Dict[str, Any]) -> None: + """Initialize pnf resource. + + Args: + data (Dict[str, Any]): Data needed to create resource. + + """ + super().__init__(data) + + def create(self) -> None: + """Create pnf resource. + + Create pnf resource and link to provided resources. + + """ + if not self.exists: + logging.debug("Create Pnf %s", self.data["name"]) + self._xnf = Pnf(self.data["name"]) + if (vendor_name := self.data.get("vendor")) is not None: + self._xnf.vendor = Vendor(vendor_name) + self.onboard_resource_with_properties(self.data) + + @property + def exists(self) -> bool: + """Determine if resource already exists or not. + + Returns: + bool: True if object exists, False otherwise + + """ + return self.pnf is not None + + @property + def pnf(self) -> Pnf: + """Pnf property. + + Pnf which is represented by the data provided by user. + + Returns: + Pnf: Pnf object + + """ + if (pnf := Pnf(name=self.data["name"])).created(): + self._xnf = pnf + return self._xnf + return None diff --git a/onap_data_provider/resources/project_resource.py b/onap_data_provider/resources/project_resource.py new file mode 100644 index 0000000..e4c19c2 --- /dev/null +++ b/onap_data_provider/resources/project_resource.py @@ -0,0 +1,73 @@ +"""Project resource module.""" +""" + Copyright 2021 Deutsche Telekom AG + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" +import logging +from typing import Any, Dict, Optional + +from onapsdk.aai.business import Project # type: ignore +from onapsdk.exceptions import ResourceNotFound # type: ignore + +from .resource import Resource + + +class ProjectResource(Resource): + """Project resource class. + + Creates A&AI project. + """ + + def __init__(self, data: Dict[str, Any]) -> None: + """Initialize project resource. + + Args: + data (Dict[str, Any]): Data needed to create resource. + + """ + super().__init__(data) + self._project: Optional[Project] = None + + def create(self) -> None: + """Create project resource.""" + logging.debug(f"Create Project {self.data['name']}") + if not self.exists: + self._project = Project.create(self.data["name"]) + + @property + def exists(self) -> bool: + """Determine if resource already exists or not. + + Returns: + bool: True if object exists, False otherwise + + """ + return bool(self.project) + + @property + def project(self) -> Project: + """Project property. + + Project which is represented by the data provided by user. + + Returns: + Project: Project object + + """ + if not self._project: + try: + self._project = Project.get_by_name(self.data["name"]) + except ResourceNotFound: + return None + return self._project diff --git a/onap_data_provider/resources/resource.py b/onap_data_provider/resources/resource.py new file mode 100644 index 0000000..10477d1 --- /dev/null +++ b/onap_data_provider/resources/resource.py @@ -0,0 +1,45 @@ +"""Resource base module.""" +""" + Copyright 2021 Deutsche Telekom AG + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" +from abc import ABC, abstractmethod +from typing import Dict, Any + + +class Resource(ABC): + """Base Resource class. + + Abstract class which is a base for all other resource classes. + """ + + def __init__(self, data: Dict[str, Any]) -> None: + """Initialize resource. + + Data contains all needed information to create resource. + It's readed from configuration file. + + Args: + data (Dict[str, Any]): Data needed to create resource. + + """ + self.data = data + + @abstractmethod + def create(self) -> None: + """Create resource. + + Abstract method to create resource + + """ diff --git a/onap_data_provider/resources/resource_creator.py b/onap_data_provider/resources/resource_creator.py new file mode 100644 index 0000000..34cbafd --- /dev/null +++ b/onap_data_provider/resources/resource_creator.py @@ -0,0 +1,177 @@ +"""Resource creator module.""" +from __future__ import annotations + +""" + Copyright 2021 Deutsche Telekom AG + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" + +from onap_data_provider.resources.platform_resource import PlatformResource +import typing +from abc import ABC + +from .aai_service_resource import AaiServiceResource +from .cloud_region_resource import CloudRegionResource +from .complex_resource import ComplexResource +from .customer_resource import CustomerResource +from .line_of_business_resource import LineOfBusinessResource +from .msb_k8s_definition import MsbK8SDefinitionResource +from .owning_entity_resource import OwningEntityResource +from .pnf_resource import PnfResource +from .project_resource import ProjectResource +from .service_resource import ServiceResource +from .service_instance_resource import ( + ServiceInstanceResource, + ServiceInstanceResource_1_1, +) +from .vendor_resource import VendorResource +from .vnf_resource import VnfResource +from .vsp_resource import VspResource +from ..versions import VersionsEnum + +if typing.TYPE_CHECKING: + from .resource import Resource + + +class ResourceCreator(ABC): + """Resource creator. + + Provides a method to create `Resource` instances. + """ + + RESOURCES_TYPES_DICT: typing.Mapping[ + str, typing.Mapping[VersionsEnum, typing.Type[Resource]] + ] = { + "aai-service": { + VersionsEnum.NONE: AaiServiceResource, + VersionsEnum.V1_0: AaiServiceResource, + VersionsEnum.V1_1: AaiServiceResource, + }, + "cloud-region": { + VersionsEnum.NONE: CloudRegionResource, + VersionsEnum.V1_0: CloudRegionResource, + VersionsEnum.V1_1: CloudRegionResource, + }, + "complex": { + VersionsEnum.NONE: ComplexResource, + VersionsEnum.V1_0: ComplexResource, + VersionsEnum.V1_1: ComplexResource, + }, + "customer": { + VersionsEnum.NONE: CustomerResource, + VersionsEnum.V1_0: CustomerResource, + VersionsEnum.V1_1: CustomerResource, + }, + "vsp": { + VersionsEnum.NONE: VspResource, + VersionsEnum.V1_0: VspResource, + VersionsEnum.V1_1: VspResource, + }, + "service": { + VersionsEnum.NONE: ServiceResource, + VersionsEnum.V1_0: ServiceResource, + VersionsEnum.V1_1: ServiceResource, + }, + "vendor": { + VersionsEnum.NONE: VendorResource, + VersionsEnum.V1_0: VendorResource, + VersionsEnum.V1_1: VendorResource, + }, + "pnf": { + VersionsEnum.NONE: PnfResource, + VersionsEnum.V1_0: PnfResource, + VersionsEnum.V1_1: PnfResource, + }, + "vnf": { + VersionsEnum.NONE: VnfResource, + VersionsEnum.V1_0: VnfResource, + VersionsEnum.V1_1: VnfResource, + }, + "service-instance": { + VersionsEnum.NONE: ServiceInstanceResource, + VersionsEnum.V1_0: ServiceInstanceResource, + VersionsEnum.V1_1: ServiceInstanceResource_1_1, + }, + "line-of-business": { + VersionsEnum.NONE: LineOfBusinessResource, + VersionsEnum.V1_0: LineOfBusinessResource, + VersionsEnum.V1_1: LineOfBusinessResource, + }, + "project": { + VersionsEnum.NONE: ProjectResource, + VersionsEnum.V1_0: ProjectResource, + VersionsEnum.V1_1: ProjectResource, + }, + "platform": { + VersionsEnum.NONE: PlatformResource, + VersionsEnum.V1_0: PlatformResource, + VersionsEnum.V1_1: PlatformResource, + }, + "owning-entity": { + VersionsEnum.NONE: OwningEntityResource, + VersionsEnum.V1_0: OwningEntityResource, + VersionsEnum.V1_1: OwningEntityResource, + }, + "msb-k8s-definition": { + VersionsEnum.NONE: MsbK8SDefinitionResource, + VersionsEnum.V1_0: MsbK8SDefinitionResource, + VersionsEnum.V1_1: MsbK8SDefinitionResource, + }, + } + + @classmethod + def create( + cls, + resource_type: str, + data: typing.Dict[str, typing.Any], + version: VersionsEnum, + ) -> Resource: + """Resources factory method. + + Based on provided `resource_type` creates `Resource` subclass. + + Supported `resource_type` values: + - aai-service: AaiServiceResource + - cloud-region: CloudRegionResource + - complex: ComplexResource + - customer: CustomerResource + - vsp: VspResource + - service: ServiceResource + - vendor: VendorResource + - pnf: PnfResource + - vnf: VnfResource + - service-instance: ServiceInstanceResource + - line-of-business: LineOfBusinessResource + - project: ProjectResource + - platform: PlatformResource + - owning-entity: OwningEntityResource + - msb-k8s-definition: MsbK8SDefinitionResource + + Args: + resource_type (str): Resource type to create + data (typing.Dict[str, typing.Any]): Resource data + + Raises: + ValueError: Not support `resource_type` value provided. + + Returns: + Resource: Created `Resource` subclass instance. + + """ + try: + return cls.RESOURCES_TYPES_DICT[resource_type][version](data) + except KeyError as key_error: + raise ValueError( + "Invalid resource type provided: %d", resource_type + ) from key_error diff --git a/onap_data_provider/resources/service_instance_resource.py b/onap_data_provider/resources/service_instance_resource.py new file mode 100644 index 0000000..b89f9df --- /dev/null +++ b/onap_data_provider/resources/service_instance_resource.py @@ -0,0 +1,271 @@ +"""Service instance resource module.""" +""" + Copyright 2021 Deutsche Telekom AG + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" +import logging +from typing import Any, Dict + +from onapsdk.aai.cloud_infrastructure import CloudRegion, Tenant # type: ignore +from onapsdk.aai.business import Customer, OwningEntity # type: ignore +from onapsdk.aai.service_design_and_creation import Service as AaiService # type: ignore +from onapsdk.sdc.service import Service # type: ignore +from onapsdk.vid import LineOfBusiness, Platform, Project # type: ignore +from onapsdk.aai.business import ServiceSubscription +from onapsdk.aai.business import ServiceInstance +from onapsdk.so.instantiation import ( # type: ignore + ServiceInstantiation, + SoService, +) + +from .resource import Resource +from onapsdk.exceptions import APIError, ResourceNotFound # type: ignore + + +class ServiceInstanceResource(Resource): + """Service instance resource class. + + Creates service instance. + """ + + def __init__(self, data: Dict[str, Any]) -> None: + """Service instance resource initialization. + + Args: + data (Dict[str, Any]): Data needed to create service instance + + """ + super().__init__(data) + self._customer: Customer = None + self._service_subscription: ServiceSubscription = None + self._service_instance: ServiceInstance = None + self._aai_service: AaiService = None + + def create(self) -> None: + """Create ServiceInstance resource.""" + if not self.exists: + + service: Service = Service(name=self.data["service_name"]) + if not service.distributed: + raise AttributeError( + "Service not distrbuted - instance can't be created" + ) + if (cloud_region_id := self.data["cloud_region_id"]) is not None: + cloud_region: CloudRegion = CloudRegion.get_by_id( + cloud_owner=self.data["cloud_owner"], + cloud_region_id=cloud_region_id, + ) + tenant: Tenant = cloud_region.get_tenant(self.data["tenant_id"]) + self.service_subscription.link_to_cloud_region_and_tenant( + cloud_region, tenant + ) + else: + cloud_region, tenant = None, None + try: + owning_entity = OwningEntity.get_by_owning_entity_name( + self.data["owning_entity"] + ) + except APIError: + owning_entity = OwningEntity.create(self.data["owning_entity"]) + + try: + aai_service = next( + AaiService.get_all(service_id=self.data["aai_service"]) + ) + except StopIteration: + raise ValueError( + f"A&AI Service {self.data['aai_service']} does not exist" + ) + + service_instantiation: ServiceInstantiation = ( + ServiceInstantiation.instantiate_macro( + sdc_service=service, + customer=self.customer, + owning_entity=owning_entity, + project=Project(self.data["project"]), + line_of_business=LineOfBusiness(self.data["line_of_business"]), + platform=Platform(self.data["platform"]), + cloud_region=cloud_region, + tenant=tenant, + service_instance_name=self.data["service_instance_name"], + so_service=self.so_service, + aai_service=aai_service, + ) + ) + service_instantiation.wait_for_finish( + timeout=self.data.get("timeout") + ) # 20 minutes timeout + + if service_instantiation.failed == True: + logging.error( + "Service instantiation failed for %s", + self.data["service_instance_name"], + ) + return + self._service_instance = ( + self.service_subscription.get_service_instance_by_name( + self.data["service_instance_name"] + ) + ) + + @property + def exists(self) -> bool: + """Determine if resource already exists or not. + + Returns: + bool: True if object exists, False otherwise + + """ + return self.service_instance is not None + + @property + def service_instance(self) -> ServiceInstance: + """Serviceinstance property. + + Returns: + ServiceInstance: ServiceInstance object + + """ + if not self._service_instance: + try: + service_instance: ServiceInstance = ( + self.service_subscription.get_service_instance_by_name( + self.data["service_instance_name"] + ) + ) + if service_instance: + self._service_instance = service_instance + except ResourceNotFound: + logging.error( + "Customer %s does not exist", + self.data["customer_id"], + ) + return self._service_instance + + @property + def customer(self) -> Customer: + """Access to Customer object property. + + Returns: + Customer: Customer object + + """ + if not self._customer: + self._customer = Customer.get_by_global_customer_id( + self.data["customer_id"] + ) + return self._customer + + @property + def service_subscription(self) -> ServiceSubscription: + """Service subscription property. + + Returns: + ServiceSubscription: ServiceSubscription object + + """ + if not self._service_subscription and self.customer: + self._service_subscription = ( + self.customer.get_service_subscription_by_service_type( + service_type=self.data.get( + "service_subscription_type", self.data["service_name"] + ) + ) + ) + return self._service_subscription + + @property + def so_service(self) -> SoService: + """Create an object with parameters for the service instantiation. + + Based on the instance definition data create an object + which is used for instantiation. + + Returns: + SoService: SoService object + + """ + return SoService( + subscription_service_type=self.data.get( + "service_subscription_type", self.data["service_name"] + ), + vnfs=[ + { + "model_name": vnf["vnf_name"], + "vnf_name": vnf.get("instance_name", vnf["vnf_name"]), + "parameters": vnf.get("parameters", {}), + "vf_modules": [ + { + "model_name": vf_module["name"], + "vf_module_name": vf_module.get( + "instance_name", vf_module["name"] + ), + "parameters": vf_module.get("parameters", {}), + } + for vf_module in vnf.get("vf_modules", []) + ], + } + for vnf in self.data.get("instantiation_parameters", []) + ], + ) + + @property + def aai_service(self) -> AaiService: + """A&AI service which is used during the instantiation. + + Raises: + ValueError: AaiService with given service id doesn't exist + + Returns: + AaiService: AaiService object + + """ + if ( + not self._aai_service + and (aai_service_id := self.data.get("aai_service")) is not None + ): + try: + self._aai_service = next(AaiService.get_all(service_id=aai_service_id)) + except StopIteration: + raise ValueError(f"A&AI Service {aai_service_id} does not exist") + return self._aai_service + + +class ServiceInstanceResource_1_1(ServiceInstanceResource): + """Service instance resource class. + + That's the Service instance resource class for 1.1 schema version. + """ + + @property + def aai_service(self) -> AaiService: + """A&AI service which is used during the instantiation. + + Raises: + ValueError: AaiService with given service id doesn't exist + + Returns: + AaiService: AaiService object + + """ + if not self._aai_service: + try: + self._aai_service = next( + AaiService.get_all(service_id=self.data["aai_service"]) + ) + except StopIteration: + raise ValueError( + f"A&AI Service {self.data['aai_service']} does not exist" + ) + return self._aai_service diff --git a/onap_data_provider/resources/service_resource.py b/onap_data_provider/resources/service_resource.py new file mode 100644 index 0000000..8489982 --- /dev/null +++ b/onap_data_provider/resources/service_resource.py @@ -0,0 +1,100 @@ +"""Service resource module.""" +""" + Copyright 2021 Deutsche Telekom AG + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" +from typing import Any, Dict, Mapping, Optional, Type + +from onapsdk.sdc.pnf import Pnf # type: ignore +from onapsdk.sdc.properties import Property # type: ignore +from onapsdk.sdc.sdc_resource import SdcResource # type: ignore +from onapsdk.sdc.service import Service, ServiceInstantiationType # type: ignore +from onapsdk.sdc.vf import Vf # type: ignore +from onapsdk.sdc.vl import Vl # type: ignore + +from .resource import Resource + + +class ServiceResource(Resource): + """Service resource class.""" + + RESOURCES: Mapping[str, Type[SdcResource]] = { + "PNF": Pnf, + "VF": Vf, + "VL": Vl, + } + + def __init__(self, data: Dict[str, Any]) -> None: + """Initialize Service resource. + + Args: + data (Dict[str, Any]): Data needed to create resource. + + """ + super().__init__(data) + self._service: Optional[Service] = None + + def create(self) -> None: + """Create Service resource.""" + if not self.exists: + service = Service( + name=self.data["name"], + instantiation_type=ServiceInstantiationType.MACRO, + ) + service.create() + for resource_data in self.data.get("resources", []): + resource = self.RESOURCES[resource_data["type"].upper()]( + name=resource_data["name"] + ) + service.add_resource(resource) + component = service.get_component(resource) + for prop_key, prop_value in resource_data.get("properties", {}).items(): + prop = component.get_property(prop_key) + prop.value = prop_value + for property_data in self.data.get("properties", []): + service.add_property( + Property( + property_data["name"], + property_data["type"], + value=property_data.get("value"), + ) + ) + service.checkin() + service.onboard() + self._service = service + + @property + def exists(self) -> bool: + """Check if Service exists in SDC. + + Returns: + bool: True if Service exists, False otherwise + + """ + return self.service is not None and self.service.distributed + + @property + def service(self) -> Optional[Service]: + """Service property. + + Returns: + Service: Service object which is describer by provided data. None if does not exist yet. + + """ + if not self._service: + service: Service = Service(name=self.data["name"]) + if not service.created(): + return None + self._service = service + return self._service diff --git a/onap_data_provider/resources/tenant_resource.py b/onap_data_provider/resources/tenant_resource.py new file mode 100644 index 0000000..13d003f --- /dev/null +++ b/onap_data_provider/resources/tenant_resource.py @@ -0,0 +1,85 @@ +"""Tenant resource module.""" +""" + Copyright 2021 Deutsche Telekom AG + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" +import logging +from typing import Any, Dict, Optional + +from onapsdk.aai.cloud_infrastructure import CloudRegion, Tenant # type: ignore + +from .resource import Resource +from onapsdk.exceptions import ResourceNotFound # type: ignore + + +class TenantResource(Resource): + """Tenant resource class. + + Creates tenant. + """ + + def __init__(self, data: Dict[str, Any], cloud_region: CloudRegion) -> None: + """Tenant resource initialization. + + Args: + data (Dict[str, Any]): Data needed to create tenant + cloud_region (CloudRegion): Cloud region for which tenant is going to be created + + """ + super().__init__(data) + self.cloud_region: CloudRegion = cloud_region + self._tenant: Optional[Tenant] = None + + def create(self) -> None: + """Create tenant resource. + + Add tenant to provided cloud region + + """ + if not self.exists: + self.cloud_region.add_tenant( + tenant_id=self.data["tenant-id"], + tenant_name=self.data["tenant-name"], + tenant_context=self.data.get("tenant-context"), + ) + + @property + def exists(self) -> bool: + """Determine if resource already exists or not. + + Returns: + bool: True if object exists, False otherwise + + """ + return self.tenant is not None + + @property + def tenant(self) -> Tenant: + """Tenant property. + + Returns: + Tenant: Tenant object + + """ + if not self._tenant: + try: + self._tenant = self.cloud_region.get_tenant(self.data["tenant-id"]) + except ResourceNotFound: + logging.error( + "Tenant %s does not exist in %s cloud region", + self.data["tenant-id"], + self.cloud_region.cloud_region_id, + ) + return None + return self._tenant diff --git a/onap_data_provider/resources/vendor_resource.py b/onap_data_provider/resources/vendor_resource.py new file mode 100644 index 0000000..14f2b18 --- /dev/null +++ b/onap_data_provider/resources/vendor_resource.py @@ -0,0 +1,75 @@ +"""Vendor resource module.""" +""" + Copyright 2021 Deutsche Telekom AG + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" + +import logging +from typing import Any, Dict + +from onapsdk.sdc.vendor import Vendor # type: ignore +from .resource import Resource + + +class VendorResource(Resource): + """Vendor resource class. + + Creates vendor. + """ + + def __init__(self, data: Dict[str, Any]) -> None: + """Initialize vendor resource. + + Args: + data (Dict[str, Any]): Data needed to create resource. + + """ + super().__init__(data) + self._vendor: Vendor = None + + def create(self) -> None: + """Create vendor resource. + + Create vendor resource. + + """ + if not self.exists: + logging.debug("Create Vendor %s", self.data["name"]) + self._vendor = Vendor(name=self.data["name"]) + self._vendor.onboard() + + @property + def exists(self) -> bool: + """Determine if resource already exists or not. + + Returns: + bool: True if object exists, False otherwise + + """ + return self.vendor is not None + + @property + def vendor(self) -> Vendor: + """Vendor property. + + Vendor which is represented by the data provided by user. + + Returns: + Vendor: Vendor object + + """ + if (vendor := Vendor(name=self.data["name"])).created(): + self._vendor = vendor + return self._vendor + return None diff --git a/onap_data_provider/resources/vnf_resource.py b/onap_data_provider/resources/vnf_resource.py new file mode 100644 index 0000000..1d47413 --- /dev/null +++ b/onap_data_provider/resources/vnf_resource.py @@ -0,0 +1,75 @@ +"""Vnf resource module.""" +""" + Copyright 2021 Deutsche Telekom AG + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" + +import logging +from typing import Any, Dict + +from onapsdk.sdc.vf import Vf # type: ignore +from .resource import Resource +from .xnf_resource import XnfResource + + +class VnfResource(Resource, XnfResource): + """Vnf resource class. + + Creates vnf. + """ + + def __init__(self, data: Dict[str, Any]) -> None: + """Initialize vnf resource. + + Args: + data (Dict[str, Any]): Data needed to create resource. + + """ + super().__init__(data) + + def create(self) -> None: + """Create vnf resource. + + Create vnf resource and link to specified resources. + + """ + if not self.exists: + logging.debug("Create Vnf %s", self.data["name"]) + self._xnf = Vf(name=self.data["name"]) + self.onboard_resource_with_properties(self.data) + + @property + def exists(self) -> bool: + """Determine if resource already exists or not. + + Returns: + bool: True if object exists, False otherwise + + """ + return self.vnf is not None + + @property + def vnf(self) -> Vf: + """Vnf property. + + Vnf which is represented by the data provided by user. + + Returns: + Vf: Vf object + + """ + if (vnf := Vf(name=self.data["name"])).created(): + self._xnf = vnf + return self._xnf + return None diff --git a/onap_data_provider/resources/vsp_resource.py b/onap_data_provider/resources/vsp_resource.py new file mode 100644 index 0000000..17a4d5b --- /dev/null +++ b/onap_data_provider/resources/vsp_resource.py @@ -0,0 +1,71 @@ +"""VSP resource module.""" +""" + Copyright 2021 Deutsche Telekom AG + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" +from typing import Any, Dict, Optional +from onapsdk.sdc.vendor import Vendor # type: ignore +from onapsdk.sdc.vsp import Vsp # type: ignore + +from .resource import Resource + + +class VspResource(Resource): + """VSP resource class.""" + + def __init__(self, data: Dict[str, Any]) -> None: + """Initialize VSP resource. + + Args: + data (Dict[str, Any]): Data needed to create resource. + + """ + super().__init__(data) + self._vsp: Optional[Vsp] = None + + def create(self) -> None: + """Create VSP resource.""" + if not self.exists: + with open(self.data["package"], "rb") as package: + self._vsp = Vsp( + name=self.data["name"], + vendor=Vendor(self.data["vendor"]), + package=package, + ) + self._vsp.onboard() + + @property + def exists(self) -> bool: + """Check if VSP exists. + + Returns: + bool: True if VSP exists, False otherwise + + """ + return self.vsp is not None + + @property + def vsp(self) -> Vsp: + """VSP property. + + Returns: + Vsp: VSP object which is describer by provided data. None if does not exist yet. + + """ + if not self._vsp: + vsp: Vsp = Vsp(name=self.data["name"]) + if not vsp.created(): + return None + self._vsp = vsp + return self._vsp diff --git a/onap_data_provider/resources/xnf_resource.py b/onap_data_provider/resources/xnf_resource.py new file mode 100644 index 0000000..cada088 --- /dev/null +++ b/onap_data_provider/resources/xnf_resource.py @@ -0,0 +1,60 @@ +"""Xnf resource module.""" +""" + Copyright 2021 Deutsche Telekom AG + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" + +from abc import ABC +from typing import Any, Dict +from onapsdk.sdc.vsp import Vsp # type: ignore +from onapsdk.sdc.sdc_resource import SdcResource # type: ignore +from onapsdk.sdc.properties import Property # type: ignore + + +class XnfResource(ABC): + """Xnf resource class. + + Network function base class. + """ + + def __init__(self) -> None: + """Initialize xnf resource.""" + self._xnf: SdcResource = None + + def onboard_resource_with_properties(self, data: Dict[str, Any]) -> None: + """Set properties provided and instantiate SDC resource. + + Args: + data (Dict[str, Any]): Data needed to create resource. + + """ + if (vsp_name := data.get("vsp")) is not None: + self._xnf.vsp = Vsp(vsp_name) + self._xnf.create() + if (artifact_data := data.get("deployment_artifact")) is not None: + self._xnf.add_deployment_artifact( + artifact_type=data["deployment_artifact"]["artifact_type"], + artifact_name=data["deployment_artifact"]["artifact_name"], + artifact_label=data["deployment_artifact"]["artifact_label"], + artifact=data["deployment_artifact"]["artifact_file_name"], + ) + for property_data in data.get("properties", []): + self._xnf.add_property( + Property( + name=property_data["name"], + property_type=property_data["type"], + value=property_data.get("value"), + ) + ) + self._xnf.onboard() diff --git a/onap_data_provider/schemas/infra.schema b/onap_data_provider/schemas/infra.schema new file mode 100644 index 0000000..61e7bf2 --- /dev/null +++ b/onap_data_provider/schemas/infra.schema @@ -0,0 +1,533 @@ +--- +"$schema": http://json-schema.org/draft-04/schema# +type: object +properties: + aai-services: + type: array + items: + - type: object + properties: + aai-service: + type: object + properties: + service-id: + type: string + service-description: + type: string + required: + - service-id + - service-description + required: + - aai-service + complexes: + type: array + items: + - type: object + properties: + complex: + type: object + properties: + physical-location-id: + type: string + complex-name: + type: string + data-center-code: + type: string + identity-url: + type: string + physical-location-type: + type: string + street1: + type: string + street2: + type: string + city: + type: string + state: + type: string + postal-code: + type: string + country: + type: string + region: + type: string + latitude: + type: string + longitude: + type: string + elevation: + type: string + lata: + type: string + required: + - physical-location-id + required: + - complex + cloud-regions: + type: array + items: + - type: object + properties: + cloud-region: + type: object + properties: + cloud-owner: + type: string + cloud-region-id: + type: string + orchestration-disabled: + type: boolean + in-maint: + type: boolean + cloud-type: + type: string + kube-config: + type: string + tenants: + type: array + items: + - type: object + properties: + tenant-id: + type: string + tenant-name: + type: string + tenant-context: + type: string + required: + - tenant-id + - tenant-name + esr-system-infos: + type: array + items: + - type: object + properties: + esr-system-info-id: + type: string + user-name: + type: string + password: + type: string + system-type: + type: string + service-url: + type: string + cloud-domain: + type: string + default-tenant: + type: string + required: + - esr-system-info-id + - user-name + - password + - system-type + - service-url + - cloud-domain + complex: + type: object + properties: + physical-location-id: + type: string + required: + - physical-location-id + availability-zones: + type: array + items: + - type: object + properties: + availability-zone-name: + type: string + hypervisor-type: + type: string + required: + - availability-zone-name + - hypervisor-type + required: + - cloud-owner + - cloud-region-id + - orchestration-disabled + - in-maint + required: + - cloud-region + customers: + type: array + items: + - type: object + properties: + customer: + type: object + properties: + global-customer-id: + type: string + subscriber-name: + type: string + subscriber-type: + type: string + service-subscriptions: + type: array + items: + - type: object + properties: + service-type: + type: string + tenants: + type: array + items: + - type: object + properities: + tenant-id: + type: string + cloud-owner: + type: string + cloud-region-id: + type: string + required: + - tenant-id + - cloud-owner + - cloud-region-id + required: + - service-type + required: + - global-customer-id + - subscriber-name + - subscriber-type + required: + - customer + vendors: + type: array + items: + - type: object + properties: + vendor: + type: object + properties: + name: + type: string + required: + - name + required: + - vendor + vsps: + type: array + items: + - type: object + properties: + vsp: + type: object + properties: + name: + type: string + vendor: + type: string + package: + type: string + required: + - name + - vendor + - package + required: + - vsp + services: + type: array + items: + - type: object + properties: + service: + type: object + properties: + name: + type: string + resources: + type: array + items: + - type: object + properties: + name: + type: string + type: + type: string + required: + - name + - type + properties: + type: array + items: + - type: object + properties: + name: + type: string + type: + type: string + value: + type: string + required: + - name + - type + required: + - name + required: + - service + pnfs: + type: array + items: + - type: object + properties: + pnf: + type: object + properties: + name: + type: string + vendor: + type: string + vsp: + type: string + deployment_artifact: + type: object + properties: + artifact_type: + type: string + artifact_name: + type: string + artifact_label: + type: string + artifact_file_name: + type: string + required: + - artifact_type + - artifact_name + - artifact_label + - artifact_file_name + properties: + type: array + items: + - type: object + properties: + name: + type: string + type: + type: string + value: + type: string + required: + - name + - type + required: + - name + required: + - pnf + vnfs: + type: array + items: + - type: object + properties: + vnf: + type: object + properties: + name: + type: string + vsp: + type: string + deployment_artifact: + type: object + properties: + artifact_type: + type: string + artifact_name: + type: string + artifact_label: + type: string + artifact_file_name: + type: string + required: + - artifact_type + - artifact_name + - artifact_label + - artifact_file_name + properties: + type: array + items: + - type: object + properties: + name: + type: string + type: + type: string + value: + type: string + required: + - name + - type + required: + - name + required: + - vnf + service-instances: + type: array + items: + - type: object + properties: + service-instance: + type: object + properties: + service_instance_name: + type: string + service_name: + type: string + cloud_region: + type: string + customer_id: + type: string + owning_entity: + type: string + project: + type: string + platform: + type: string + line_of_business: + type: string + cloud_region_id: + type: string + cloud_owner: + type: string + timeout: + type: number + minimum: 1 + maximum: 99999 + aai_service: + type: string + service_subscription_type: + type: string + instantiation_parameters: + type: array + items: + - type: object + properties: + vnf_name: + type: string + sec_group: + type: string + public_net_id: + type: string + onap_private_net_id: + type: string + onap_private_subnet_id: + type: string + image_name: + type: string + flavor_name: + type: string + install_script_version: + type: string + demo_artifacts_version: + type: string + cloud_env: + type: string + aic-cloud-region: + type: string + pub_key: + type: string + required: + - service_instance_name + - service_name + - cloud_region + - customer_id + - owning_entity + - project + - platform + - line_of_business + - cloud_region_id + - cloud_owner + - instantiation_parameters + owning-entities: + type: array + items: + - type: object + properities: + owning-entity: + type: object + properties: + name: + type: string + required: + - name + required: + - owning-entity + projects: + type: array + items: + - type: object + properties: + project: + type: object + properities: + name: + type: string + required: + - name + required: + - project + platforms: + type: array + items: + - type: object + properities: + platform: + type: object + properities: + name: + type: string + required: + - name + required: + - platform + lines-of-business: + type: array + items: + - type: object + properties: + line-of-business: + type: object + properities: + name: + type: string + required: + - name + required: + - line-of-business + msb-k8s-definitions: + type: array + items: + type: object + properties: + name: + type: string + version: + type: string + chart-name: + type: string + description: + type: string + artifact: + type: string + profiles: + type: array + items: + - type: object + properties: + name: + type: string + namespace: + type: string + k8s-version: + type: string + artifact: + type: string + required: + - name + - namespace + - k8s-version + - artifact + required: + - name + - version + - artifact diff --git a/onap_data_provider/schemas/infra_1_1.schema b/onap_data_provider/schemas/infra_1_1.schema new file mode 100644 index 0000000..9cb1f09 --- /dev/null +++ b/onap_data_provider/schemas/infra_1_1.schema @@ -0,0 +1,533 @@ +--- +"$schema": http://json-schema.org/draft-04/schema# +type: object +properties: + aai-services: + type: array + items: + - type: object + properties: + aai-service: + type: object + properties: + service-id: + type: string + service-description: + type: string + required: + - service-id + - service-description + required: + - aai-service + complexes: + type: array + items: + - type: object + properties: + complex: + type: object + properties: + physical-location-id: + type: string + complex-name: + type: string + data-center-code: + type: string + identity-url: + type: string + physical-location-type: + type: string + street1: + type: string + street2: + type: string + city: + type: string + state: + type: string + postal-code: + type: string + country: + type: string + region: + type: string + latitude: + type: string + longitude: + type: string + elevation: + type: string + lata: + type: string + required: + - physical-location-id + required: + - complex + cloud-regions: + type: array + items: + - type: object + properties: + cloud-region: + type: object + properties: + cloud-owner: + type: string + cloud-region-id: + type: string + orchestration-disabled: + type: boolean + in-maint: + type: boolean + cloud-type: + type: string + kube-config: + type: string + tenants: + type: array + items: + - type: object + properties: + tenant-id: + type: string + tenant-name: + type: string + tenant-context: + type: string + required: + - tenant-id + - tenant-name + esr-system-infos: + type: array + items: + - type: object + properties: + esr-system-info-id: + type: string + user-name: + type: string + password: + type: string + system-type: + type: string + service-url: + type: string + cloud-domain: + type: string + default-tenant: + type: string + required: + - esr-system-info-id + - user-name + - password + - system-type + - service-url + - cloud-domain + complex: + type: object + properties: + physical-location-id: + type: string + required: + - physical-location-id + availability-zones: + type: array + items: + - type: object + properties: + availability-zone-name: + type: string + hypervisor-type: + type: string + required: + - availability-zone-name + - hypervisor-type + required: + - cloud-owner + - cloud-region-id + - orchestration-disabled + - in-maint + required: + - cloud-region + customers: + type: array + items: + - type: object + properties: + customer: + type: object + properties: + global-customer-id: + type: string + subscriber-name: + type: string + subscriber-type: + type: string + service-subscriptions: + type: array + items: + - type: object + properties: + service-type: + type: string + tenants: + type: array + items: + - type: object + properities: + tenant-id: + type: string + cloud-owner: + type: string + cloud-region-id: + type: string + required: + - tenant-id + - cloud-owner + - cloud-region-id + required: + - service-type + required: + - global-customer-id + - subscriber-name + - subscriber-type + required: + - customer + vendors: + type: array + items: + - type: object + properties: + vendor: + type: object + properties: + name: + type: string + required: + - name + required: + - vendor + vsps: + type: array + items: + - type: object + properties: + vsp: + type: object + properties: + name: + type: string + vendor: + type: string + package: + type: string + required: + - name + - vendor + - package + required: + - vsp + services: + type: array + items: + - type: object + properties: + service: + type: object + properties: + name: + type: string + resources: + type: array + items: + - type: object + properties: + name: + type: string + type: + type: string + required: + - name + - type + properties: + type: array + items: + - type: object + properties: + name: + type: string + type: + type: string + value: + type: string + required: + - name + - type + required: + - name + required: + - service + pnfs: + type: array + items: + - type: object + properties: + pnf: + type: object + properties: + name: + type: string + vendor: + type: string + vsp: + type: string + deployment_artifact: + type: object + properties: + artifact_type: + type: string + artifact_name: + type: string + artifact_label: + type: string + artifact_file_name: + type: string + required: + - artifact_type + - artifact_name + - artifact_label + - artifact_file_name + properties: + type: array + items: + - type: object + properties: + name: + type: string + type: + type: string + value: + type: string + required: + - name + - type + required: + - name + required: + - pnf + vnfs: + type: array + items: + - type: object + properties: + vnf: + type: object + properties: + name: + type: string + vsp: + type: string + deployment_artifact: + type: object + properties: + artifact_type: + type: string + artifact_name: + type: string + artifact_label: + type: string + artifact_file_name: + type: string + required: + - artifact_type + - artifact_name + - artifact_label + - artifact_file_name + properties: + type: array + items: + - type: object + properties: + name: + type: string + type: + type: string + value: + type: string + required: + - name + - type + required: + - name + required: + - vnf + service-instances: + type: array + items: + - type: object + properties: + service-instance: + type: object + properties: + service_instance_name: + type: string + service_name: + type: string + cloud_region: + type: string + customer_id: + type: string + owning_entity: + type: string + project: + type: string + platform: + type: string + line_of_business: + type: string + cloud_region_id: + type: string + cloud_owner: + type: string + timeout: + type: number + minimum: 1 + maximum: 99999 + aai_service: + type: string + service_subscription_type: + type: string + instantiation_parameters: + type: array + items: + - type: object + properties: + vnf_name: + type: string + sec_group: + type: string + public_net_id: + type: string + onap_private_net_id: + type: string + onap_private_subnet_id: + type: string + image_name: + type: string + flavor_name: + type: string + install_script_version: + type: string + demo_artifacts_version: + type: string + cloud_env: + type: string + aic-cloud-region: + type: string + pub_key: + type: string + required: + - service_instance_name + - service_name + - cloud_region + - customer_id + - owning_entity + - project + - platform + - line_of_business + - cloud_region_id + - cloud_owner + - aai_service + owning-entities: + type: array + items: + - type: object + properities: + owning-entity: + type: object + properties: + name: + type: string + required: + - name + required: + - owning-entity + projects: + type: array + items: + - type: object + properties: + project: + type: object + properities: + name: + type: string + required: + - name + required: + - project + platforms: + type: array + items: + - type: object + properities: + platform: + type: object + properities: + name: + type: string + required: + - name + required: + - platform + lines-of-business: + type: array + items: + - type: object + properties: + line-of-business: + type: object + properities: + name: + type: string + required: + - name + required: + - line-of-business + msb-k8s-definitions: + type: array + items: + type: object + properties: + name: + type: string + version: + type: string + chart-name: + type: string + description: + type: string + artifact: + type: string + profiles: + type: array + items: + - type: object + properties: + name: + type: string + namespace: + type: string + k8s-version: + type: string + artifact: + type: string + required: + - name + - namespace + - k8s-version + - artifact + required: + - name + - version + - artifact diff --git a/onap_data_provider/tag_handlers.py b/onap_data_provider/tag_handlers.py new file mode 100644 index 0000000..8f29d0d --- /dev/null +++ b/onap_data_provider/tag_handlers.py @@ -0,0 +1,52 @@ +"""Custom yaml tag handlers module.""" +""" + Copyright 2021 Deutsche Telekom AG + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" +import yaml +import uuid + + +def join(loader: yaml.SafeLoader, node: yaml.Node) -> str: + """Concatinates the nodes fields for !join tag. + + Concatinates multiple strings in yaml value f.e. !join [a, b, c] results in 'abc'. + join supports separator syntax f.e. !join ['_', [a, b, c]] results in 'a_b_c'. + + Args: + node (yaml.Node): the yaml node + + Returns: + str: the joined string of node + + """ + seq = loader.construct_sequence(node, deep=True) # type: ignore + if len(seq) == 2 and isinstance(seq[0], str) and isinstance(seq[1], list): + sep = seq[0] + return sep.join([str(i) for i in seq[1]]) + else: + return "".join([str(i) for i in seq]) + + +def generate_random_uuid(*_) -> str: + """Random UUID generator. + + Args: + loader (yaml.SafeLoader): SafeLoader object + node (yaml.Node): Node object + + Returns: + str: randomly generated UUID + """ + return str(uuid.uuid4()) diff --git a/onap_data_provider/validator.py b/onap_data_provider/validator.py new file mode 100644 index 0000000..3589f85 --- /dev/null +++ b/onap_data_provider/validator.py @@ -0,0 +1,52 @@ +"""Infra file schema validatior module.""" +""" + Copyright 2021 Deutsche Telekom AG + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" + +from typing import Any, Dict + +import yaml +from jsonschema import validate # type: ignore + +from .versions import VersionsEnum + + +class Validator: + """Validate input schema class.""" + + def __init__(self) -> None: + """Validate class initialization. + + Load schema file. + + """ + self.schemas: Dict[str, Any] = {} + + def validate(self, version: VersionsEnum, input_data: Dict[str, Any]) -> None: + """Check if given input is valid from schema perspective. + + Args: + input_data (Dict[str, Any]): Input to check + + Raises: + ValidationError: Raises if input is invalid + + """ + if not version.value.version_number in self.schemas: + with open(version.value.schema_path, "r") as schema_file: + self.schemas[version.value.version_number] = yaml.safe_load( + schema_file.read() + ) + validate(input_data, schema=self.schemas[version.value.version_number]) diff --git a/onap_data_provider/versions.py b/onap_data_provider/versions.py new file mode 100644 index 0000000..7651bec --- /dev/null +++ b/onap_data_provider/versions.py @@ -0,0 +1,68 @@ +"""Versions class.""" +""" + Copyright 2021 Deutsche Telekom AG + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" +import logging +from collections import namedtuple +from enum import Enum +from pathlib import Path + + +Version = namedtuple("Version", ["version_number", "schema_path", "deprecated"]) + + +class VersionsEnum(Enum): + """Class for storing information about supported versions.""" + + V1_1 = Version( + version_number="1.1", + schema_path=Path(Path(__file__).parent, "schemas/infra_1_1.schema"), + deprecated=False, + ) + V1_0 = Version( + version_number="1.0", + schema_path=Path(Path(__file__).parent, "schemas/infra.schema"), + deprecated=False, + ) + NONE = Version( + version_number="None", + schema_path=Path(Path(__file__).parent, "schemas/infra.schema"), + deprecated=True, + ) + + @classmethod + def get_version_by_number(cls, version_number: str) -> "VersionsEnum": + """Get an enum element based on the given string version value. + + Because the version enum elements are not simple objects, + but also have information about the path to the supported schema and + whether this version is deprecated this method allows to retrieve + the version only based on its value stored in the string format. + + Raises: + ValueError: Provided version number is not supported + + Returns: + VersionsEnum: The version enum + + """ + for version in cls: + if version.value.version_number == version_number: + if version.value.deprecated: + logging.warning( + f"This version [{version.value.version_number}] is deprecated, consider using the newer one!" + ) + return version + raise ValueError(f"Version number {version_number} not supported") diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..3bdc671 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +onapsdk==8.2.0 +PyYAML~=5.4.1 +jsonschema==3.2.0 diff --git a/samples/BASIC_VM_enriched.zip b/samples/BASIC_VM_enriched.zip Binary files differnew file mode 100644 index 0000000..28c14bb --- /dev/null +++ b/samples/BASIC_VM_enriched.zip diff --git a/samples/README.md b/samples/README.md new file mode 100644 index 0000000..bf62051 --- /dev/null +++ b/samples/README.md @@ -0,0 +1,29 @@ +# Data provider infra file samples + +## vendor.yaml + +Creates vendor + +## vsp.yaml + +Creates vendor and two vsps. Shows YAML anchor usage example. + +## xnfs.yaml + +Creates PNFs and VNFs + +## service.yaml + +Creates and distribute SDC service + +## complex.yaml + +Creates Complex + +## cloud-region.yaml + +Creates cloud region and register it in multicloud (all data \[like tenants] from OpenStack are going to be created by ONAP) + +## customer.yaml + +Creates customer with subscribed service diff --git a/samples/aai_business.yaml b/samples/aai_business.yaml new file mode 100644 index 0000000..65a70bd --- /dev/null +++ b/samples/aai_business.yaml @@ -0,0 +1,17 @@ +# A&AI business sample +# Creates one owning entity, project, platform and line of business +# +odpSchemaVersion: 1.0 +resources: + owning_entities: + - owning-entity: + name: oran_owner + projects: + - project: + name: oran_town + platforms: + - platform: + name: oran_platform + lines-of-business: + - line-of-business: + name: oran_lob diff --git a/samples/aai_service.yaml b/samples/aai_service.yaml new file mode 100644 index 0000000..1200d38 --- /dev/null +++ b/samples/aai_service.yaml @@ -0,0 +1,9 @@ +# A&AI service sample +# Creates one A&AI service model resource +# +odpSchemaVersion: 1.0 +resources: + aai-services: + - aai-service: + service-id: test_aai_service + service-description: test_aai_service diff --git a/samples/cloud-region.yaml b/samples/cloud-region.yaml new file mode 100644 index 0000000..86c7273 --- /dev/null +++ b/samples/cloud-region.yaml @@ -0,0 +1,27 @@ +# Cloud region sample +# Cloud region resource is one of the biggest to describe (if you want to configure it with OpenStack) +# Please fill the data based on your OpenStack instance RC v3 file (ask OpenStack admin). +# +odpSchemaVersion: 1.0 +resources: + cloud-regions: + - cloud-region: + cloud-owner: sample-cloud-owner + cloud-region-id: RegionOne + orchestration-disabled: false + in-maint: false + complex: + physical-location-id: sample-complex # Make sure it exists! + register-to-multicloud: true + availability-zones: + - cloud-owner: sample-cloud-owner + availability-zone-name: sample-availbility-zone + hypervisor-type: nova + esr-system-infos: # Take these information from openstack config file + - esr-system-info-id: 5433b0ac-594d-41f7-911d-dfe413e1cb2c # Has to be unique + user-name: username + password: password + system-type: VIM + service-url: http://127.0.0.1:5000/v3 + cloud-domain: Default + default-tenant: default-tenant diff --git a/samples/complex.yaml b/samples/complex.yaml new file mode 100644 index 0000000..8b989c7 --- /dev/null +++ b/samples/complex.yaml @@ -0,0 +1,8 @@ +# Complex sample +# Creates one complex with `sample-complex` physical location id +# +odpSchemaVersion: 1.0 +resources: + complexes: + - complex: + physical-location-id: sample-complex diff --git a/samples/customer.yaml b/samples/customer.yaml new file mode 100644 index 0000000..e8fdbec --- /dev/null +++ b/samples/customer.yaml @@ -0,0 +1,14 @@ +# Customer sample +# Creates a customer with one service subscribed - sample-service. It's not required to create customer +# with service subscription, but it's useful if you want to use that customer later for service +# instantiation. +# Service type has to be the name of the SDC service! +odpSchemaVersion: 1.0 +resources: + customers: + - customer: + global-customer-id: sample-customer + subscriber-name: sample-customer + subscriber-type: Customer + service-subscriptions: + - service-type: sample-service # Make sure it exists! diff --git a/samples/msb_k8s.yaml b/samples/msb_k8s.yaml new file mode 100644 index 0000000..a6efaf3 --- /dev/null +++ b/samples/msb_k8s.yaml @@ -0,0 +1,11 @@ +odpSchemaVersion: 1.0 +resources: + msb-k8s-definitions: + - name: test + version: test + artifact: definition.tar.gz + profiles: + - name: test + namespace: test + k8s-version: "1.19" + artifact: profile.tar.gz diff --git a/samples/service.yaml b/samples/service.yaml new file mode 100644 index 0000000..36c92d7 --- /dev/null +++ b/samples/service.yaml @@ -0,0 +1,52 @@ +# Service sample +# Using that file you will create SDC Services both with custom properties and not. +# * sample-service-with-vf is a simple service with VF resource +# * sample-service-with-vf-and-properties is a service with VF resource and +# it's properties - it's ready to create service instance using Macro flow +# * sample-service-with-pnf is a simple service with PNF resource +# * sample-service-with-pnf-and-properties is a service with PNF resource and +# it's properties - it's ready to create service instance using Macro flow +# * sample-service-with-vl is a simple service with VL resource +# Make sure that resources you want to use are already created. If not - use `xnfs.yaml` +# sample file and create needed xNFs. +# +odpSchemaVersion: 1.0 +resources: + services: + - service: + name: sample-service-with-vf + resources: + - name: sample-vnf # Make sure it exists! + type: VF + - service: + name: sample-service-with-vf-and-properties + resources: + - name: sample-vnf # Make sure it exists! + type: VF + properties: + controller_actor: "CDS" + skip_post_instantiation_configuration: False + sdnc_artifact_name: "vnf" + sdnc_model_version: "1.0.0" + sdnc_model_name: "ubuntu20" + - service: + name: sample-service-with-pnf + resources: + - name: sample-pnf # Make sure it exists! + type: PNF + - service: + name: sample-service-with-pnf-and-properties + resources: + - name: sample-pnf # Make sure it exists! + type: PNF + properties: + controller_actor: "CDS" + skip_post_instantiation_configuration: False + sdnc_artifact_name: "vnf" + sdnc_model_version: "1.0.0" + sdnc_model_name: "ubuntu20" + - service: + name: sample-service-with-vl + resources: + - name: sample-vl # Make sure it exists! + type: VL diff --git a/samples/ubuntu.zip b/samples/ubuntu.zip Binary files differnew file mode 100644 index 0000000..2dc60aa --- /dev/null +++ b/samples/ubuntu.zip diff --git a/samples/vendor.yaml b/samples/vendor.yaml new file mode 100644 index 0000000..abffca8 --- /dev/null +++ b/samples/vendor.yaml @@ -0,0 +1,8 @@ +# Vendor sample +# Using that file you will create one SDC Vendor resource with "sample-vendor" name +# +odpSchemaVersion: 1.0 +resources: + vendors: + - vendor: + name: sample-vendor diff --git a/samples/vsp.yaml b/samples/vsp.yaml new file mode 100644 index 0000000..6336f44 --- /dev/null +++ b/samples/vsp.yaml @@ -0,0 +1,25 @@ +# VSP sample +# Using that file you will create: +# - one SDC Vendor resource with "sample-vendor" name +# - two SDC VSPs: +# * one with "sample-vsp-anchor" name, where we use YAML's anchor to share vendor name between two resource, +# * onw with "sample-vsp-no-anchor" name where we just simply copy&paste the name of the vendor we want to use. +# It will also use "ubuntu.zip" package as an VSP artifact. +# Remember: package value is a path, it has to point to the file from the `onap-data-provider` runner perspective. +# I recommend to use absulute path to be sure there will be no errors. +# +odpSchemaVersion: 1.0 +resources: + vendors: + - vendor: + name: &vendor sample-vendor + + vsps: + - vsp: + name: sample-vsp-anchor + vendor: *vendor + package: ubuntu.zip + - vsp: + name: sample-vsp-no-anchor + vendor: sample-vendor + package: ubuntu.zip diff --git a/samples/xnfs.yaml b/samples/xnfs.yaml new file mode 100644 index 0000000..35f3024 --- /dev/null +++ b/samples/xnfs.yaml @@ -0,0 +1,35 @@ +# VNFs and PNFs sample. +# In that sample we don't create any additional resources - just xNFs. Make +# sure that VSP you want to use already exists or use sample from `vsp.yaml` +# file and create it. +# It will also use "BASIC_VM_enriched.zip" package as an xNF artifact. +# Remember: package value is a path, it has to point to the file from the `onap-data-provider` runner perspective. +# I recommend to use absulute path to be sure there will be no errors. + +odpSchemaVersion: 1.0 +resources: + vnfs: + - vnf: + name: sample-vnf-without-artifact + vsp: sample-vsp # Make sure it exists! + - vnf: + name: sample-vnf-with-artifact + vsp: sample-vsp # Make sure it exists! + deployment_artifact: + artifact_type: CONTROLLER_BLUEPRINT_ARCHIVE + artifact_name: BASIC_VM_enriched.zip + artifact_label: vfwcds + artifact_file_name: BASIC_VM_enriched.zip + + pnfs: + - pnf: + name: sample-pnf-without-artifact + vsp: sample-vsp # Make sure it exists! + - pnf: + name: sample-pnf-with-artifact + vsp: sample-vsp # Make sure it exists! + deployment_artifact: + artifact_type: CONTROLLER_BLUEPRINT_ARCHIVE + artifact_name: BASIC_VM_enriched.zip + artifact_label: vfwcds + artifact_file_name: BASIC_VM_enriched.zip diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..3345e64 --- /dev/null +++ b/setup.py @@ -0,0 +1,45 @@ +""" + Copyright 2021 Deutsche Telekom AG + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" + +import setuptools + +with open("README.md", "r", encoding="utf-8") as readme: + long_description = readme.read() + +setuptools.setup( + name="onap_data_provider", + version="0.4.1", + author="Michal Jagiello <michal.jagiello@t-mobile.pl>, Piotr Stanior <piotr.stanior@t-mobile.pl>", + description="Tool to provide data for ONAP instances", + long_description=long_description, + long_description_content_type="text/markdown", + keywords="ONAP", + packages=setuptools.find_packages(), + package_data={"onap_data_provider": ["schemas/*"]}, + python_requires=">=3.8", + entry_points={ + "console_scripts": [ + "onap-data-provider=onap_data_provider.data_provider:run", + ] + }, + install_requires=["onapsdk==8.2.0", "PyYAML~=5.4.1", "jsonschema==3.2.0"], + classifiers=[ + "Development Status :: 5 - Production/Stable", + "Programming Language :: Python", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + ], +) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/__init__.py diff --git a/tests/config_dirs/not_yaml b/tests/config_dirs/not_yaml new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/config_dirs/not_yaml diff --git a/tests/config_dirs/test-data-version.yml b/tests/config_dirs/test-data-version.yml new file mode 100644 index 0000000..51e92c9 --- /dev/null +++ b/tests/config_dirs/test-data-version.yml @@ -0,0 +1,48 @@ +odpSchemaVersion: 1.0 +resources: + complexes: + - complex: + data-center-code: AMICPL1 + complex-name: AMIST-COMPLEX-1 + physical-location-id: &complex_id AMIST-COMPLEX-1 + physical-location-type: Office + street1: '505' + street2: Terry Fox Drive + city: Kanata + state: Ontario + postal-code: A1A1A1 + region: Eastern + country: Canada + + cloud-regions: + - cloud-region: + cloud-owner: &clown AMIST + cloud-region-id: AMCR1 + cloud-region-version: '11.0' + orchestration-disabled: true + in-maint: false + complex: + physical-location-id: *complex_id + tenants: + - tenant-id: !join ['-', [*clown, 'TENANT', 1]] + tenant-name: AMIST-TENANT-1-NAME + - tenant-id: !join [*clown, '-', 'TENANT', '-', 2] + tenant-name: AMIST-TENANT-2-NAME + availability-zones: + - cloud-owner: *clown + availability-zone-name: AMIST-AZ-1 + hypervisor-type: OpenStackAmd + + customers: + - customer: + global-customer-id: AMIST-CUST-11 + subscriber-name: AAIIST-TESTER-11 + subscriber-type: Customer + service-subscriptions: + - service-type: amist-voip + - customer: + global-customer-id: AMIST-CUST-12 + subscriber-name: AAIIST-TESTER-12 + subscriber-type: Customer + service-subscriptions: + - service-type: amist-voip diff --git a/tests/config_dirs/test-data.yml b/tests/config_dirs/test-data.yml new file mode 100644 index 0000000..281e754 --- /dev/null +++ b/tests/config_dirs/test-data.yml @@ -0,0 +1,47 @@ +--- +complexes: +- complex: + data-center-code: AMICPL1 + complex-name: AMIST-COMPLEX-1 + physical-location-id: &complex_id AMIST-COMPLEX-1 + physical-location-type: Office + street1: '505' + street2: Terry Fox Drive + city: Kanata + state: Ontario + postal-code: A1A1A1 + region: Eastern + country: Canada + +cloud-regions: +- cloud-region: + cloud-owner: &clown AMIST + cloud-region-id: AMCR1 + cloud-region-version: '11.0' + orchestration-disabled: true + in-maint: false + complex: + physical-location-id: *complex_id + tenants: + - tenant-id: !join ['-', [*clown, 'TENANT', 1]] + tenant-name: AMIST-TENANT-1-NAME + - tenant-id: !join [*clown, '-', 'TENANT', '-', 2] + tenant-name: AMIST-TENANT-2-NAME + availability-zones: + - cloud-owner: *clown + availability-zone-name: AMIST-AZ-1 + hypervisor-type: OpenStackAmd + +customers: +- customer: + global-customer-id: AMIST-CUST-11 + subscriber-name: AAIIST-TESTER-11 + subscriber-type: Customer + service-subscriptions: + - service-type: amist-voip +- customer: + global-customer-id: AMIST-CUST-12 + subscriber-name: AAIIST-TESTER-12 + subscriber-type: Customer + service-subscriptions: + - service-type: amist-voip diff --git a/tests/test-data-version.yml b/tests/test-data-version.yml new file mode 100644 index 0000000..51e92c9 --- /dev/null +++ b/tests/test-data-version.yml @@ -0,0 +1,48 @@ +odpSchemaVersion: 1.0 +resources: + complexes: + - complex: + data-center-code: AMICPL1 + complex-name: AMIST-COMPLEX-1 + physical-location-id: &complex_id AMIST-COMPLEX-1 + physical-location-type: Office + street1: '505' + street2: Terry Fox Drive + city: Kanata + state: Ontario + postal-code: A1A1A1 + region: Eastern + country: Canada + + cloud-regions: + - cloud-region: + cloud-owner: &clown AMIST + cloud-region-id: AMCR1 + cloud-region-version: '11.0' + orchestration-disabled: true + in-maint: false + complex: + physical-location-id: *complex_id + tenants: + - tenant-id: !join ['-', [*clown, 'TENANT', 1]] + tenant-name: AMIST-TENANT-1-NAME + - tenant-id: !join [*clown, '-', 'TENANT', '-', 2] + tenant-name: AMIST-TENANT-2-NAME + availability-zones: + - cloud-owner: *clown + availability-zone-name: AMIST-AZ-1 + hypervisor-type: OpenStackAmd + + customers: + - customer: + global-customer-id: AMIST-CUST-11 + subscriber-name: AAIIST-TESTER-11 + subscriber-type: Customer + service-subscriptions: + - service-type: amist-voip + - customer: + global-customer-id: AMIST-CUST-12 + subscriber-name: AAIIST-TESTER-12 + subscriber-type: Customer + service-subscriptions: + - service-type: amist-voip diff --git a/tests/test-data.yml b/tests/test-data.yml new file mode 100644 index 0000000..281e754 --- /dev/null +++ b/tests/test-data.yml @@ -0,0 +1,47 @@ +--- +complexes: +- complex: + data-center-code: AMICPL1 + complex-name: AMIST-COMPLEX-1 + physical-location-id: &complex_id AMIST-COMPLEX-1 + physical-location-type: Office + street1: '505' + street2: Terry Fox Drive + city: Kanata + state: Ontario + postal-code: A1A1A1 + region: Eastern + country: Canada + +cloud-regions: +- cloud-region: + cloud-owner: &clown AMIST + cloud-region-id: AMCR1 + cloud-region-version: '11.0' + orchestration-disabled: true + in-maint: false + complex: + physical-location-id: *complex_id + tenants: + - tenant-id: !join ['-', [*clown, 'TENANT', 1]] + tenant-name: AMIST-TENANT-1-NAME + - tenant-id: !join [*clown, '-', 'TENANT', '-', 2] + tenant-name: AMIST-TENANT-2-NAME + availability-zones: + - cloud-owner: *clown + availability-zone-name: AMIST-AZ-1 + hypervisor-type: OpenStackAmd + +customers: +- customer: + global-customer-id: AMIST-CUST-11 + subscriber-name: AAIIST-TESTER-11 + subscriber-type: Customer + service-subscriptions: + - service-type: amist-voip +- customer: + global-customer-id: AMIST-CUST-12 + subscriber-name: AAIIST-TESTER-12 + subscriber-type: Customer + service-subscriptions: + - service-type: amist-voip diff --git a/tests/test-kube-config b/tests/test-kube-config new file mode 100644 index 0000000..3c546eb --- /dev/null +++ b/tests/test-kube-config @@ -0,0 +1 @@ +dummy file
\ No newline at end of file diff --git a/tests/test_aai_service_resource.py b/tests/test_aai_service_resource.py new file mode 100644 index 0000000..9056299 --- /dev/null +++ b/tests/test_aai_service_resource.py @@ -0,0 +1,51 @@ +from unittest.mock import patch, PropertyMock + +from onap_data_provider.resources.aai_service_resource import AaiService, AaiServiceResource, ResourceNotFound + + +AAI_SERVICE_DATA = { + "service-id": "123", + "service-description": "123" +} + + +@patch("onap_data_provider.resources.aai_service_resource.AaiService.get_all") +def test_aai_service_resource_aai_resource(mock_aai_service_get_all): + mock_aai_service_get_all.side_effect = ResourceNotFound + mock_aai_service_get_all.return_value = iter([]) + aai_service_resource = AaiServiceResource(AAI_SERVICE_DATA) + assert aai_service_resource.aai_service is None + mock_aai_service_get_all.side_effect = None + mock_aai_service_get_all.return_value = iter([AaiService(service_id="123", service_description="123", resource_version="123")]) + assert aai_service_resource.aai_service is not None + + +@patch( + "onap_data_provider.resources.aai_service_resource.AaiServiceResource.aai_service", + new_callable=PropertyMock, +) +def test_aai_service_resource_exists(mock_aai_service): + mock_aai_service.return_value = None + aai_service_resource = AaiServiceResource(AAI_SERVICE_DATA) + assert aai_service_resource.exists is False + mock_aai_service.return_value = 1 # Anything but not None + assert aai_service_resource.exists is True + + +@patch( + "onap_data_provider.resources.aai_service_resource.AaiServiceResource.exists", + new_callable=PropertyMock, +) +@patch("onap_data_provider.resources.aai_service_resource.AaiService.create") +def test_aai_service_resource_create(mock_aai_service_create, mock_exists): + mock_exists.return_value = True + aai_service_resource = AaiServiceResource(AAI_SERVICE_DATA) + aai_service_resource.create() + mock_aai_service_create.assert_not_called() + + mock_exists.return_value = False + aai_service_resource.create() + mock_aai_service_create.assert_called_once_with( + service_id="123", + service_description="123" + ) diff --git a/tests/test_cloud_region_resource.py b/tests/test_cloud_region_resource.py new file mode 100644 index 0000000..b704720 --- /dev/null +++ b/tests/test_cloud_region_resource.py @@ -0,0 +1,149 @@ +from pathlib import Path +from unittest.mock import MagicMock, patch, PropertyMock + +from onapsdk.aai.cloud_infrastructure.complex import Complex + +from onap_data_provider.resources.cloud_region_resource import ( + CloudRegion, + CloudRegionResource, +) +from onapsdk.exceptions import ResourceNotFound + +CLOUD_REGION_DATA = { + "cloud-owner": "test", + "cloud-region-id": "test", + "orchestration-disabled": True, + "in-maint": False, +} + +CLOUD_REGION_K8S_TYPE = { + "cloud-region-id": "k8s-test", + "cloud-owner": "k8s-test", + "orchestration-disabled": True, + "in-maint": False, + "cloud-type": "k8s", + "kube-config": Path(Path(__file__).parent, "test-kube-config"), +} + + +@patch("onap_data_provider.resources.cloud_region_resource.CloudRegion.get_by_id") +def test_cloud_region_resource_cloud_region(mock_cloud_region_get_by_id): + mock_cloud_region_get_by_id.side_effect = ResourceNotFound + cloud_region_resource = CloudRegionResource(CLOUD_REGION_DATA) + assert cloud_region_resource.cloud_region is None + + mock_cloud_region_get_by_id.side_effect = None + mock_cloud_region_get_by_id.return_value = 1 + assert cloud_region_resource.cloud_region == 1 + + +@patch( + "onap_data_provider.resources.cloud_region_resource.CloudRegionResource.cloud_region", + new_callable=PropertyMock, +) +def test_cloud_region_resource_exists(mock_cloud_region): + mock_cloud_region.return_value = None + cloud_region_resource = CloudRegionResource(CLOUD_REGION_DATA) + assert cloud_region_resource.exists is False + mock_cloud_region.return_value = 1 # Anything but not None + assert cloud_region_resource.exists is True + + +@patch( + "onap_data_provider.resources.cloud_region_resource.CloudRegionResource.exists", + new_callable=PropertyMock, +) +@patch("onap_data_provider.resources.cloud_region_resource.CloudRegion.create") +def test_cloud_region_create(mock_cloud_region_create, mock_exists): + + cloud_region_resource = CloudRegionResource(CLOUD_REGION_DATA) + assert cloud_region_resource.data == CLOUD_REGION_DATA + + mock_exists.return_value = False + cloud_region_resource.create() + assert mock_cloud_region_create.called_once_with( + cloud_owner="test", + cloud_region_id="test", + orchestration_disabled=True, + in_maint=False, + ) + + mock_exists.reset_mock() + mock_cloud_region_create.reset_mock() + + mock_exists.return_value = True + cloud_region_resource.create() + mock_cloud_region_create.assert_not_called() + + +@patch( + "onap_data_provider.resources.cloud_region_resource.CloudRegionResource.cloud_region", + new_callable=PropertyMock, +) +@patch("onap_data_provider.resources.cloud_region_resource.Complex.get_all") +def test_cloud_region_resource_link_to_complex( + mock_complex_get_all, mock_cloud_region_property +): + mock_cloud_region_property.return_value.complex = MagicMock() + cloud_region_resource = CloudRegionResource(CLOUD_REGION_DATA) + cloud_region_resource._link_to_complex("test") + mock_complex_get_all.assert_not_called() + + mock_cloud_region_property.return_value.complex = None + mock_complex_get_all.return_value = iter(()) + cloud_region_resource._link_to_complex("test") + mock_cloud_region_property.return_value.link_to_complex.assert_not_called() + + mock_complex_get_all.return_value = iter([Complex("test")]) + cloud_region_resource._link_to_complex("test") + mock_cloud_region_property.return_value.link_to_complex.assert_called_once() + + +@patch( + "onap_data_provider.resources.cloud_region_resource.CloudRegionResource.cloud_region", + new_callable=PropertyMock, +) +def test_cloud_region_resource_create_availability_zones(mock_cloud_region_property): + cloud_region_resource = CloudRegionResource(CLOUD_REGION_DATA) + cloud_region_resource.data["availability-zones"] = [ + {"availability-zone-name": "testzone1", "hypervisor-type": "OpenStackTest"} + ] + cloud_region_resource.create() + mock_cloud_region_property.return_value.add_availability_zone.assert_called_once() + + +@patch( + "onap_data_provider.resources.cloud_region_resource.CloudRegionResource.exists", + new_callable=PropertyMock, +) +@patch("onap_data_provider.resources.cloud_region_resource.ConnectivityInfo") +@patch("onap_data_provider.resources.cloud_region_resource.CloudRegion.create") +@patch("onap_data_provider.resources.cloud_region_resource.CloudRegion.complex") +@patch("onap_data_provider.resources.cloud_region_resource.SoDbAdapter.add_cloud_site") +def test_cloud_region_k8s_type( + mock_add_cloud_site, + _, + mock_cloud_region_create, + mock_connectivity_info, + mock_exists, +): + mock_exists.return_value = False + mock_cloud_region_create.return_value = CloudRegion( + cloud_owner=CLOUD_REGION_K8S_TYPE["cloud-owner"], + cloud_region_id=CLOUD_REGION_K8S_TYPE["cloud-region-id"], + orchestration_disabled=CLOUD_REGION_K8S_TYPE["orchestration-disabled"], + in_maint=CLOUD_REGION_K8S_TYPE["in-maint"], + cloud_type=CLOUD_REGION_K8S_TYPE["cloud-type"], + ) + cloud_region_resource = CloudRegionResource(CLOUD_REGION_K8S_TYPE) + cloud_region_resource.create() + mock_connectivity_info.get_connectivity_info_by_region_id.assert_called_once_with( + CLOUD_REGION_K8S_TYPE["cloud-region-id"] + ) + mock_add_cloud_site.assert_called_once() + + mock_connectivity_info.get_connectivity_info_by_region_id.side_effect = ( + ResourceNotFound + ) + cloud_region_resource.create() + mock_connectivity_info.create.assert_called_once() diff --git a/tests/test_complex_resource.py b/tests/test_complex_resource.py new file mode 100644 index 0000000..441b37c --- /dev/null +++ b/tests/test_complex_resource.py @@ -0,0 +1,82 @@ +from unittest.mock import patch, PropertyMock + +from onapsdk.aai.cloud_infrastructure.complex import Complex + +from onap_data_provider.resources.complex_resource import ComplexResource +from onapsdk.exceptions import ResourceNotFound + + +COMPLEX_DATA = { + "physical-location-id": "123", + "complex-name": "NB central office 1", + "data-center-code": "veniam", + "identity-url": "https://estevan.org", + "physical-location-type": "centraloffice", + "street1": "Ravensburgstraße", + "street2": "123", + "city": "Neubrandenburg", + "state": "Mecklenburg-Vorpommern", + "postal-code": "17034", + "country": "DE", + "region": "Mecklenburg Lakeland", + "latitude": "53.5630015", + "longitude": "13.2722710", + "elevation": "100", + "lata": "dolorem", +} + + +@patch("onap_data_provider.resources.complex_resource.Complex.get_all") +def test_complex_resource_complex(mock_complex_get_all): + mock_complex_get_all.side_effect = ResourceNotFound + mock_complex_get_all.return_value = iter([]) + complex_resource = ComplexResource(COMPLEX_DATA) + assert complex_resource.complex is None + mock_complex_get_all.side_effect = None + mock_complex_get_all.return_value = iter([Complex(physical_location_id="123")]) + assert complex_resource.complex is not None + + +@patch( + "onap_data_provider.resources.complex_resource.ComplexResource.complex", + new_callable=PropertyMock, +) +def test_complex_resource_exists(mock_complex): + mock_complex.return_value = None + complex_resource = ComplexResource(COMPLEX_DATA) + assert complex_resource.exists is False + mock_complex.return_value = 1 # Anything but not None + assert complex_resource.exists is True + + +@patch( + "onap_data_provider.resources.complex_resource.ComplexResource.exists", + new_callable=PropertyMock, +) +@patch("onap_data_provider.resources.complex_resource.Complex.create") +def test_complex_resource_create(mock_complex_create, mock_exists): + mock_exists.return_value = True + complex_resource = ComplexResource(COMPLEX_DATA) + complex_resource.create() + mock_complex_create.assert_not_called() + + mock_exists.return_value = False + complex_resource.create() + mock_complex_create.assert_called_once_with( + physical_location_id="123", + name="NB central office 1", + data_center_code="veniam", + identity_url="https://estevan.org", + physical_location_type="centraloffice", + street1="Ravensburgstraße", + street2="123", + city="Neubrandenburg", + state="Mecklenburg-Vorpommern", + postal_code="17034", + country="DE", + region="Mecklenburg Lakeland", + latitude="53.5630015", + longitude="13.2722710", + elevation="100", + lata="dolorem", + ) diff --git a/tests/test_config_loader.py b/tests/test_config_loader.py new file mode 100644 index 0000000..c10e329 --- /dev/null +++ b/tests/test_config_loader.py @@ -0,0 +1,27 @@ + +from pathlib import Path + +from onap_data_provider.config_loader import ConfigLoader + + +def test_config_loader_no_dirs(): + config_loader = ConfigLoader([Path(Path(__file__).parent, "test-data.yml")]) + configs = list(config_loader.load()) + assert len(configs) == 1 + + config_loader = ConfigLoader([Path(Path(__file__).parent, "test-data.yml"), + Path(Path(__file__).parent, "test-data-version.yml")]) + configs = list(config_loader.load()) + assert len(configs) == 2 + +def test_config_loader_dir(): + config_loader = ConfigLoader([Path(Path(__file__).parent, "config_dirs")]) + configs = list(config_loader.load()) + assert len(configs) == 2 + +def test_config_loader_both_dirs_and_files(): + config_loader = ConfigLoader([Path(Path(__file__).parent, "test-data.yml"), + Path(Path(__file__).parent, "test-data-version.yml"), + Path(Path(__file__).parent, "config_dirs")]) + configs = list(config_loader.load()) + assert len(configs) == 4 diff --git a/tests/test_config_parser.py b/tests/test_config_parser.py new file mode 100644 index 0000000..dc118c9 --- /dev/null +++ b/tests/test_config_parser.py @@ -0,0 +1,50 @@ +from pathlib import Path + +from onap_data_provider.resources.cloud_region_resource import CloudRegionResource +from onap_data_provider.config_parser import ConfigParser + + +def test_create_cloud_region_resource(): + parser = ConfigParser([Path("tests/test-data.yml")]) + parsed_objects = list(parser.parse()) + assert isinstance(parsed_objects[1], CloudRegionResource) + assert parsed_objects[1].data['cloud-owner'] == 'AMIST' + assert parsed_objects[1].data['tenants'][0]['tenant-id'] == '-'.join( + [parsed_objects[1].data['cloud-owner'], 'TENANT', '1']) + assert parsed_objects[1].data['tenants'][1]['tenant-id'] == ''.join( + [parsed_objects[1].data['cloud-owner'], '-', 'TENANT', '-', '2']) + + +def test_config_parser_versioning(): + parser = ConfigParser([Path("tests/test-data.yml")]) + config = parser.configs[0] + assert config.version.value.version_number == "None" + parsed_objects = list(parser.parse()) + assert isinstance(parsed_objects[1], CloudRegionResource) + assert parsed_objects[1].data['cloud-owner'] == 'AMIST' + assert parsed_objects[1].data['tenants'][0]['tenant-id'] == '-'.join( + [parsed_objects[1].data['cloud-owner'], 'TENANT', '1']) + assert parsed_objects[1].data['tenants'][1]['tenant-id'] == ''.join( + [parsed_objects[1].data['cloud-owner'], '-', 'TENANT', '-', '2']) + + parser = ConfigParser([Path("tests/test-data-version.yml")]) + config = parser.configs[0] + assert config.version.value.version_number == "1.0" + parsed_objects = list(parser.parse()) + assert isinstance(parsed_objects[1], CloudRegionResource) + assert parsed_objects[1].data['cloud-owner'] == 'AMIST' + assert parsed_objects[1].data['tenants'][0]['tenant-id'] == '-'.join( + [parsed_objects[1].data['cloud-owner'], 'TENANT', '1']) + assert parsed_objects[1].data['tenants'][1]['tenant-id'] == ''.join( + [parsed_objects[1].data['cloud-owner'], '-', 'TENANT', '-', '2']) + + parser = ConfigParser([Path("tests/test-data.yml"), Path("tests/test-data-version.yml")]) + assert parser.configs[0].version.value.version_number == "None" + assert parser.configs[1].version.value.version_number == "1.0" + parsed_objects = list(parser.parse()) + assert isinstance(parsed_objects[1], CloudRegionResource) + assert parsed_objects[1].data['cloud-owner'] == 'AMIST' + assert parsed_objects[1].data['tenants'][0]['tenant-id'] == '-'.join( + [parsed_objects[1].data['cloud-owner'], 'TENANT', '1']) + assert parsed_objects[1].data['tenants'][1]['tenant-id'] == ''.join( + [parsed_objects[1].data['cloud-owner'], '-', 'TENANT', '-', '2']) diff --git a/tests/test_customer_resource.py b/tests/test_customer_resource.py new file mode 100644 index 0000000..23b51ef --- /dev/null +++ b/tests/test_customer_resource.py @@ -0,0 +1,84 @@ +from unittest.mock import MagicMock, patch, PropertyMock + +from onap_data_provider.resources.customer_resource import CustomerResource + +from onapsdk.exceptions import ResourceNotFound + +CUSTOMER_DATA = { + "global-customer-id": "test_id", + "subscriber-name": "test_name", + "subscriber-type": "Customer", + "service-subscriptions": [{"service-type": "test_voip"}], +} + +SERVICE_SUBSCRIPTION_WITH_TENANTS_DATA = { + "service-type": "test-service-subscription", + "tenants": [ + { + "tenant-id": "1234", + "cloud-owner": "test-cloud-owner", + "cloud-region-id": "test-cloud-region", + } + ], +} + + +@patch( + "onap_data_provider.resources.customer_resource.Customer.get_by_global_customer_id" +) +def test_customer_resource_customer(mock_customer_get_by_global_customer_id): + mock_customer_get_by_global_customer_id.side_effect = ResourceNotFound + customer_resource = CustomerResource(CUSTOMER_DATA) + assert customer_resource.customer is None + mock_customer_get_by_global_customer_id.side_effect = None + mock_customer_get_by_global_customer_id.return_value = 1 + assert customer_resource.customer == 1 + + +@patch( + "onap_data_provider.resources.customer_resource.CustomerResource.customer", + new_callable=PropertyMock, +) +def test_customer_exists(mock_customer): + mock_customer.return_value = None + customer_resource = CustomerResource(CUSTOMER_DATA) + assert customer_resource.exists is False + mock_customer.return_value = 1 # Anything but not None + assert customer_resource.exists is True + + +@patch( + "onap_data_provider.resources.customer_resource.CustomerResource.exists", + new_callable=PropertyMock, +) +@patch("onap_data_provider.resources.customer_resource.Customer.create") +def test_customer_create(mock_customer_create, mock_exists): + customer_resource = CustomerResource(CUSTOMER_DATA) + assert customer_resource.data == CUSTOMER_DATA + mock_exists.return_value = False + customer_resource.create() + assert mock_customer_create.called_once_with( + global_customer_id="test_id", + subscriber_name="test_name", + subscriber_type="Customer", + ) + + +@patch( + "onap_data_provider.resources.customer_resource.CustomerResource.ServiceSubscriptionResource.service_subscription", + new_callable=PropertyMock, +) +@patch("onap_data_provider.resources.customer_resource.CloudRegion") +def test_service_subscription_with_tenants( + mock_cloud_region, _ +): + cloud_region_mock = MagicMock() + mock_cloud_region.get_by_id.return_value = cloud_region_mock + service_subscription_resource = CustomerResource.ServiceSubscriptionResource( + SERVICE_SUBSCRIPTION_WITH_TENANTS_DATA, MagicMock() + ) + service_subscription_resource.create() + mock_cloud_region.get_by_id.assert_called_once_with( + "test-cloud-owner", "test-cloud-region" + ) + cloud_region_mock.get_tenant.assert_called_once_with("1234") diff --git a/tests/test_esr_resource.py b/tests/test_esr_resource.py new file mode 100644 index 0000000..d6c8901 --- /dev/null +++ b/tests/test_esr_resource.py @@ -0,0 +1,82 @@ +from collections import namedtuple +from unittest.mock import MagicMock, patch, PropertyMock + +from onap_data_provider.resources.esr_system_info_resource import ( + CloudRegion, + EsrSystemInfoResource, +) + + +ESR_RESOURCE_DATA = { + "esr-system-info-id": "Test ID", + "user-name": "Test name", + "password": "testpass", + "system-type": "test type", + "service-url": "test url", + "cloud-domain": "test cloud domain", +} + + +EsrSystemInfoNamedtuple = namedtuple("EsrSystemInfo", ["esr_system_info_id"]) + + +@patch( + "onap_data_provider.resources.esr_system_info_resource.CloudRegion.esr_system_infos", + new_callable=PropertyMock, +) +def test_esr_system_info_resource_esr_system_info(mock_cloud_region_esr_system_infos): + cloud_region = CloudRegion( + cloud_owner="test", + cloud_region_id="test", + orchestration_disabled=True, + in_maint=True, + ) + esr_resource = EsrSystemInfoResource(ESR_RESOURCE_DATA, cloud_region) + mock_cloud_region_esr_system_infos.return_value = iter([]) + assert esr_resource.esr_system_info is None + + mock_cloud_region_esr_system_infos.return_value = iter( + [EsrSystemInfoNamedtuple("Test ID")] + ) + assert esr_resource.esr_system_info is not None + + +@patch( + "onap_data_provider.resources.esr_system_info_resource.EsrSystemInfoResource.esr_system_info", + new_callable=PropertyMock, +) +def test_esr_system_info_resource_exists(mock_esr_system_info): + mock_esr_system_info.return_value = None + cloud_region_mock = MagicMock() + esr_resource = EsrSystemInfoResource(ESR_RESOURCE_DATA, cloud_region_mock) + assert esr_resource.exists is False + + mock_esr_system_info.return_value = 1 + assert esr_resource.exists is True + + +@patch( + "onap_data_provider.resources.esr_system_info_resource.EsrSystemInfoResource.exists", + new_callable=PropertyMock, +) +def test_esr_system_info_resource_create(mock_exists): + + cloud_region_mock = MagicMock() + esr_resource = EsrSystemInfoResource(ESR_RESOURCE_DATA, cloud_region_mock) + + mock_exists.return_value = True + esr_resource.create() + cloud_region_mock.add_esr_system_info.assert_not_called() + + mock_exists.return_value = False + esr_resource.create() + cloud_region_mock.add_esr_system_info.assert_called_once_with( + esr_system_info_id="Test ID", + user_name="Test name", + password="testpass", + system_type="test type", + system_status="active", + service_url="test url", + cloud_domain="test cloud domain", + default_tenant=None, + ) diff --git a/tests/test_line_of_business_resource.py b/tests/test_line_of_business_resource.py new file mode 100644 index 0000000..1a60e8f --- /dev/null +++ b/tests/test_line_of_business_resource.py @@ -0,0 +1,51 @@ +from unittest import mock + +from onap_data_provider.resources.line_of_business_resource import ( + LineOfBusinessResource, + ResourceNotFound, +) + + +LINE_OF_BUSINESS = {"name": "test-name"} + + +@mock.patch( + "onap_data_provider.resources.line_of_business_resource.LineOfBusiness.get_by_name" +) +def test_line_of_business_resource_line_of_business_property(mock_get_by_name): + + lob = LineOfBusinessResource(LINE_OF_BUSINESS) + mock_get_by_name.side_effect = ResourceNotFound + assert lob.line_of_business is None + + mock_get_by_name.side_effect = None + assert lob.line_of_business is not None + + +@mock.patch( + "onap_data_provider.resources.line_of_business_resource.LineOfBusinessResource.line_of_business", + new_callable=mock.PropertyMock, +) +def test_line_of_business_resource_exists(mock_line_of_business): + + lob = LineOfBusinessResource(LINE_OF_BUSINESS) + assert lob.exists is True + mock_line_of_business.return_value = None + assert lob.exists is False + + +@mock.patch( + "onap_data_provider.resources.line_of_business_resource.LineOfBusinessResource.exists", + new_callable=mock.PropertyMock, +) +@mock.patch( + "onap_data_provider.resources.line_of_business_resource.LineOfBusiness.send_message" +) +def test_line_of_business_create(mock_send_message, mock_exists): + mock_exists.return_value = True + lob = LineOfBusinessResource(LINE_OF_BUSINESS) + lob.create() + mock_send_message.assert_not_called() + mock_exists.return_value = False + lob.create() + mock_send_message.assert_called() diff --git a/tests/test_owning_entity_resource.py b/tests/test_owning_entity_resource.py new file mode 100644 index 0000000..c32351c --- /dev/null +++ b/tests/test_owning_entity_resource.py @@ -0,0 +1,51 @@ +from unittest import mock + +from onap_data_provider.resources.owning_entity_resource import ( + OwningEntityResource, + ResourceNotFound, +) + + +OWNING_ENTITY = {"name": "test-name"} + + +@mock.patch( + "onap_data_provider.resources.owning_entity_resource.OwningEntity.get_by_owning_entity_name" +) +def test_owning_entity_resource_owning_entity_property(mock_get_by_name): + + owning_entity = OwningEntityResource(OWNING_ENTITY) + mock_get_by_name.side_effect = ResourceNotFound + assert owning_entity.owning_entity is None + + mock_get_by_name.side_effect = None + assert owning_entity.owning_entity is not None + + +@mock.patch( + "onap_data_provider.resources.owning_entity_resource.OwningEntityResource.owning_entity", + new_callable=mock.PropertyMock, +) +def test_owning_entity_resource_exists(mock_owning_entity): + + owning_entity = OwningEntityResource(OWNING_ENTITY) + assert owning_entity.exists is True + mock_owning_entity.return_value = None + assert owning_entity.exists is False + + +@mock.patch( + "onap_data_provider.resources.owning_entity_resource.OwningEntityResource.exists", + new_callable=mock.PropertyMock, +) +@mock.patch( + "onap_data_provider.resources.owning_entity_resource.OwningEntity.send_message" +) +def test_owning_entity_create(mock_send_message, mock_exists): + mock_exists.return_value = True + owning_entity = OwningEntityResource(OWNING_ENTITY) + owning_entity.create() + mock_send_message.assert_not_called() + mock_exists.return_value = False + owning_entity.create() + mock_send_message.assert_called() diff --git a/tests/test_platform_resource.py b/tests/test_platform_resource.py new file mode 100644 index 0000000..eafbae4 --- /dev/null +++ b/tests/test_platform_resource.py @@ -0,0 +1,47 @@ +from unittest import mock + +from onap_data_provider.resources.platform_resource import ( + PlatformResource, + ResourceNotFound, +) + + +PLATFORM = {"name": "test-name"} + + +@mock.patch("onap_data_provider.resources.platform_resource.Platform.get_by_name") +def test_platform_resource_platform_property(mock_get_by_name): + + platform = PlatformResource(PLATFORM) + mock_get_by_name.side_effect = ResourceNotFound + assert platform.platform is None + + mock_get_by_name.side_effect = None + assert platform.platform is not None + + +@mock.patch( + "onap_data_provider.resources.platform_resource.PlatformResource.platform", + new_callable=mock.PropertyMock, +) +def test_platform_resource_exists(mock_platform): + + platform = PlatformResource(PLATFORM) + assert platform.exists is True + mock_platform.return_value = None + assert platform.exists is False + + +@mock.patch( + "onap_data_provider.resources.platform_resource.PlatformResource.exists", + new_callable=mock.PropertyMock, +) +@mock.patch("onap_data_provider.resources.platform_resource.Platform.send_message") +def test_platform_create(mock_send_message, mock_exists): + mock_exists.return_value = True + platform = PlatformResource(PLATFORM) + platform.create() + mock_send_message.assert_not_called() + mock_exists.return_value = False + platform.create() + mock_send_message.assert_called() diff --git a/tests/test_pnf_resource.py b/tests/test_pnf_resource.py new file mode 100644 index 0000000..c58717a --- /dev/null +++ b/tests/test_pnf_resource.py @@ -0,0 +1,28 @@ +from unittest.mock import patch, PropertyMock + +from onap_data_provider.resources.pnf_resource import PnfResource + +PNF_RESOURCE_DATA = {"name": "test_pnf"} + + +@patch( + "onap_data_provider.resources.pnf_resource.PnfResource.pnf", + new_callable=PropertyMock, +) +def test_pnf_resource_exists(mock_pnf): + mock_pnf.return_value = None + pnf_resource = PnfResource(PNF_RESOURCE_DATA) + assert pnf_resource.exists is False + mock_pnf.return_value = 1 # Anything but not None + assert pnf_resource.exists is True + + +@patch( + "onap_data_provider.resources.pnf_resource.Pnf.created", +) +def test_pnf_resource_pnf(mock_pnf_created): + mock_pnf_created.return_value = False + pnf_resource = PnfResource(PNF_RESOURCE_DATA) + assert pnf_resource.pnf is None + mock_pnf_created.return_value = True + assert pnf_resource.pnf is not None diff --git a/tests/test_project_resource.py b/tests/test_project_resource.py new file mode 100644 index 0000000..c1ff167 --- /dev/null +++ b/tests/test_project_resource.py @@ -0,0 +1,47 @@ +from unittest import mock + +from onap_data_provider.resources.project_resource import ( + ProjectResource, + ResourceNotFound, +) + + +PROJECT = {"name": "test-name"} + + +@mock.patch("onap_data_provider.resources.project_resource.Project.get_by_name") +def test_project_resource_project_property(mock_get_by_name): + + project = ProjectResource(PROJECT) + mock_get_by_name.side_effect = ResourceNotFound + assert project.project is None + + mock_get_by_name.side_effect = None + assert project.project is not None + + +@mock.patch( + "onap_data_provider.resources.project_resource.ProjectResource.project", + new_callable=mock.PropertyMock, +) +def test_project_resource_exists(mock_project): + + project = ProjectResource(PROJECT) + assert project.exists is True + mock_project.return_value = None + assert project.exists is False + + +@mock.patch( + "onap_data_provider.resources.project_resource.ProjectResource.exists", + new_callable=mock.PropertyMock, +) +@mock.patch("onap_data_provider.resources.project_resource.Project.send_message") +def test_project_create(mock_send_message, mock_exists): + mock_exists.return_value = True + project = ProjectResource(PROJECT) + project.create() + mock_send_message.assert_not_called() + mock_exists.return_value = False + project.create() + mock_send_message.assert_called() diff --git a/tests/test_resource_creator.py b/tests/test_resource_creator.py new file mode 100644 index 0000000..a8e62ed --- /dev/null +++ b/tests/test_resource_creator.py @@ -0,0 +1,31 @@ +import pytest + +from onap_data_provider.resources.cloud_region_resource import CloudRegionResource +from onap_data_provider.resources.complex_resource import ComplexResource +from onap_data_provider.resources.resource_creator import ResourceCreator +from onap_data_provider.versions import VersionsEnum + + +def test_create_cloud_region_resource(): + cloud_region_resource = ResourceCreator.create("cloud-region", {"a": "B"}, VersionsEnum.NONE) + assert isinstance(cloud_region_resource, CloudRegionResource) + assert cloud_region_resource.data == {"a": "B"} + cloud_region_resource = ResourceCreator.create("cloud-region", {"a": "B"}, VersionsEnum.V1_0) + assert isinstance(cloud_region_resource, CloudRegionResource) + assert cloud_region_resource.data == {"a": "B"} + + +def test_create_complex_resource(): + complex_resource = ResourceCreator.create("complex", {"a": "B"}, VersionsEnum.NONE) + assert isinstance(complex_resource, ComplexResource) + assert complex_resource.data == {"a": "B"} + complex_resource = ResourceCreator.create("complex", {"a": "B"}, VersionsEnum.V1_0) + assert isinstance(complex_resource, ComplexResource) + assert complex_resource.data == {"a": "B"} + + +def test_create_invalid_resource(): + with pytest.raises(ValueError): + ResourceCreator.create("invalid", {}, VersionsEnum.NONE) + with pytest.raises(ValueError): + ResourceCreator.create("invalid", {}, VersionsEnum.V1_0) diff --git a/tests/test_service_instance_resource.py b/tests/test_service_instance_resource.py new file mode 100644 index 0000000..69a9e57 --- /dev/null +++ b/tests/test_service_instance_resource.py @@ -0,0 +1,141 @@ +from unittest.mock import MagicMock, patch, PropertyMock + +from onap_data_provider.resources.service_instance_resource import ( + ServiceInstanceResource +) +from onapsdk.exceptions import APIError + +RESOURCE_DATA_1_0 = { + "service_instance_name": "vFW-Macro-1", + "service_name": "service1", + "cloud_region": "test", + "customer_id": "*cust1", + "owning_entity": "test", + "project": "test", + "platform": "test", + "line_of_business": "test", + "cloud_region_id": "*cloudregionid1", + "cloud_owner": "*cloudowner1", + "tenant_id": "test", + "instantiation_parameters": [], +} + + +RESOURCE_DATA_1_1 = { + "service_instance_name": "vFW-Macro-1", + "service_name": "service1", + "cloud_region": "test", + "customer_id": "*cust1", + "owning_entity": "test", + "project": "test", + "platform": "test", + "line_of_business": "test", + "cloud_region_id": "*cloudregionid1", + "cloud_owner": "*cloudowner1", + "tenant_id": "test", + "instantiation_parameters": [], + "aai_service": "test" +} + + +INSTANTIATION_PARAMETERS_DATA = { + "service_name": "service1", + "instantiation_parameters": [ + { + "vnf_name": "test", + "parameters": {"a": "b", "c": "d"}, + "vf_modules": [ + { + "name": "base_ubuntu20", + "parameters": { + "ubuntu20_image_name": "Ubuntu_2004", + "ubuntu20_key_name": "cleouverte", + "ubuntu20_pub_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDY15cdBmIs2XOpe4EiFCsaY6bmUmK/GysMoLl4UG51JCfJwvwoWCoA+6mDIbymZxhxq9IGxilp/yTA6WQ9s/5pBag1cUMJmFuda9PjOkXl04jgqh5tR6I+GZ97AvCg93KAECis5ubSqw1xOCj4utfEUtPoF1OuzqM/lE5mY4N6VKXn+fT7pCD6cifBEs6JHhVNvs5OLLp/tO8Pa3kKYQOdyS0xc3rh+t2lrzvKUSWGZbX+dLiFiEpjsUL3tDqzkEMNUn4pdv69OJuzWHCxRWPfdrY9Wg0j3mJesP29EBht+w+EC9/kBKq+1VKdmsXUXAcjEvjovVL8l1BrX3BY0R8D imported-openssh-key", + "ubuntu20_flavor_name": "m1.smaller", + "VM_name": "ubuntu20agent-VM-01", + "vnf_id": "ubuntu20agent-VNF-instance", + "vf_module_id": "ubuntu20agent-vfmodule-instance", + "vnf_name": "ubuntu20agent-VNF", + "admin_plane_net_name": "admin", + "ubuntu20_name_0": "ubuntu20agent-VNF", + }, + } + ], + } + ] +} + + +@patch( + "onap_data_provider.resources.service_instance_resource.ServiceInstanceResource.service_instance", + new_callable=PropertyMock, +) +def test_si_resource_exists(mock_si): + mock_si.return_value = None + si_resource = ServiceInstanceResource(RESOURCE_DATA_1_1) + assert si_resource.exists is False + mock_si.return_value = 1 # Anything but not None + assert si_resource.exists is True + + +@patch( + "onap_data_provider.resources.service_instance_resource.ServiceInstanceResource.exists", + new_callable=PropertyMock, +) +@patch("onap_data_provider.resources.service_instance_resource.Customer") +@patch("onap_data_provider.resources.service_instance_resource.Project") +@patch("onap_data_provider.resources.service_instance_resource.OwningEntity") +@patch("onap_data_provider.resources.service_instance_resource.CloudRegion") +@patch("onap_data_provider.resources.service_instance_resource.ServiceInstantiation") +@patch("onap_data_provider.resources.service_instance_resource.Service") +@patch("onap_data_provider.resources.service_instance_resource.AaiService") +def test_si_resource_create( + mock_aai_service, + mock_service, + mock_service_instantionation, + mock_cr, + mock_oe, + mock_project, + mock_customer, + mock_si_resource_exists, +): + si_resource = ServiceInstanceResource(RESOURCE_DATA_1_1) + mock_oe.get_by_owning_entity_name.side_effect = APIError + mock_si_resource_exists.return_value = True + si_resource.create() + mock_service.assert_not_called() + + mock_si_resource_exists.return_value = False + si_resource.create() + mock_oe.create.assert_called_once() + mock_oe.get_by_owning_entity_name.assert_called_once() + mock_aai_service.get_all.called_once_with(service_id="test") + mock_service_instantionation.instantiate_macro.assert_called_once() + + +def test_so_service(): + si_resource = ServiceInstanceResource(INSTANTIATION_PARAMETERS_DATA) + so_service = si_resource.so_service + assert so_service.subscription_service_type == "service1" + assert len(so_service.vnfs) == 1 + vnf = so_service.vnfs[0] + assert vnf["model_name"] == "test" + assert vnf["vnf_name"] == "test" + assert len(vnf["parameters"]) == 2 + assert len(vnf["vf_modules"]) == 1 + vf_module = vnf["vf_modules"][0] + assert vf_module["model_name"] == "base_ubuntu20" + assert vf_module["vf_module_name"] == "base_ubuntu20" + assert len(vf_module["parameters"]) == 10 + + +@patch("onap_data_provider.resources.service_instance_resource.AaiService.get_all") +def test_service_instance_resource_version_1_0_and_1_1(mock_aai_service_get_all): + si_resource_1_0 = ServiceInstanceResource(RESOURCE_DATA_1_0) + assert si_resource_1_0.aai_service is None + mock_aai_service_get_all.assert_not_called() + + mock_aai_service_get_all.return_value = iter([MagicMock()]) + si_resource_1_0 = ServiceInstanceResource(RESOURCE_DATA_1_1) + assert si_resource_1_0.aai_service is not None + mock_aai_service_get_all.assert_called_once() diff --git a/tests/test_service_resource.py b/tests/test_service_resource.py new file mode 100644 index 0000000..f664676 --- /dev/null +++ b/tests/test_service_resource.py @@ -0,0 +1,50 @@ +from collections import namedtuple +from unittest.mock import patch, PropertyMock + +from onap_data_provider.resources.service_resource import ServiceResource + + +SERVICE_RESOURCE_DATA = { + "name": "test", +} + + +@patch("onap_data_provider.resources.service_resource.Service.created") +def test_service_resource_service_property(mock_service_created): + service_resource = ServiceResource(SERVICE_RESOURCE_DATA) + mock_service_created.return_value = False + assert service_resource.service is None + + mock_service_created.return_value = True + assert service_resource.service is not None + + +@patch( + "onap_data_provider.resources.service_resource.ServiceResource.service", + new_callable=PropertyMock, +) +def test_service_resource_exists(mock_service_resource_service): + service_resource = ServiceResource(SERVICE_RESOURCE_DATA) + mock_service_resource_service.return_value = None + assert service_resource.exists is False + ServiceNamedtuple = namedtuple( + "ServiceNamedtuple", ["distributed"], defaults=[True] + ) + mock_service_resource_service.return_value = ServiceNamedtuple() + assert service_resource.exists is True + + +@patch( + "onap_data_provider.resources.service_resource.ServiceResource.exists", + new_callable=PropertyMock, +) +@patch("onap_data_provider.resources.service_resource.Service") +def test_service_resource_create(mock_service, mock_service_resource_exists): + service_resource = ServiceResource(SERVICE_RESOURCE_DATA) + mock_service_resource_exists.return_value = True + service_resource.create() + mock_service.assert_not_called() + + mock_service_resource_exists.return_value = False + service_resource.create() + mock_service.assert_called_once() diff --git a/tests/test_tag_handlers.py b/tests/test_tag_handlers.py new file mode 100644 index 0000000..719295f --- /dev/null +++ b/tests/test_tag_handlers.py @@ -0,0 +1,15 @@ +from unittest.mock import patch, PropertyMock +from onap_data_provider.tag_handlers import join, generate_random_uuid + + +def test_generate_random_uuid(): + uuid1 = generate_random_uuid(None, None) + uuid2 = generate_random_uuid(None, None) + assert isinstance(uuid1, str) + assert uuid1 != uuid2 + + +@patch("yaml.SafeLoader", new_callable=PropertyMock) +def test_join(mock_safe_loader): + mock_safe_loader.construct_sequence.return_value = ["-", ["cloud", "owner", "DC1"]] + assert join(mock_safe_loader, None) == "cloud-owner-DC1" diff --git a/tests/test_tenant_resource.py b/tests/test_tenant_resource.py new file mode 100644 index 0000000..2c89651 --- /dev/null +++ b/tests/test_tenant_resource.py @@ -0,0 +1,56 @@ +from unittest.mock import MagicMock, patch, PropertyMock + +from onap_data_provider.resources.tenant_resource import TenantResource +from onapsdk.exceptions import ResourceNotFound + + +TENANT_RESOURCE_DATA = {"tenant-id": "Test ID", "tenant-name": "Test name"} + + +def test_tenant_resource_tenant(): + cloud_region_mock = MagicMock() + tenant_resource = TenantResource(TENANT_RESOURCE_DATA, cloud_region_mock) + cloud_region_mock.get_tenant.side_effect = ResourceNotFound + assert tenant_resource.tenant is None + + cloud_region_mock.get_tenant.side_effect = None + cloud_region_mock.get_tenant.return_value = 1 + assert tenant_resource.tenant == 1 + + cloud_region_mock.reset_mock() + assert tenant_resource.tenant == 1 + cloud_region_mock.assert_not_called() + + +@patch( + "onap_data_provider.resources.tenant_resource.TenantResource.tenant", + new_callable=PropertyMock, +) +def test_tenant_resource_exists(mock_tenant): + mock_tenant.return_value = None + cloud_region_mock = MagicMock() + tenant_resource = TenantResource(TENANT_RESOURCE_DATA, cloud_region_mock) + assert tenant_resource.exists is False + + mock_tenant.return_value = 1 + assert tenant_resource.exists is True + + +@patch( + "onap_data_provider.resources.tenant_resource.TenantResource.exists", + new_callable=PropertyMock, +) +def test_tenant_resource_create(mock_exists): + + cloud_region_mock = MagicMock() + tenant_resource = TenantResource(TENANT_RESOURCE_DATA, cloud_region_mock) + + mock_exists.return_value = True + tenant_resource.create() + cloud_region_mock.add_tenant.assert_not_called() + + mock_exists.return_value = False + tenant_resource.create() + cloud_region_mock.add_tenant.assert_called_once_with( + tenant_id="Test ID", tenant_name="Test name", tenant_context=None + ) diff --git a/tests/test_validator.py b/tests/test_validator.py new file mode 100644 index 0000000..ad291a9 --- /dev/null +++ b/tests/test_validator.py @@ -0,0 +1,123 @@ +from pytest import raises +from jsonschema import ValidationError + +from onap_data_provider.validator import Validator +from onap_data_provider.versions import VersionsEnum + + +def test_validator_customer(): + validator = Validator() + input_data = { + "customers": [ + { + "customer": { + "global-customer-id": "test", + "subscriber-name": "test", + "subscriber-type": "test", + } + } + ] + } + validator.validate(VersionsEnum.NONE, input_data) + input_data = { + "customers": [ + { + "customer": { + "global-customer-id": "test", + "subscriber-name": "test", + "subscriber-type": "test", + } + } + ] + } + validator.validate(VersionsEnum.V1_0, input_data) + + invalid_input_data = { # Missing subscriber-type + "customers": [ + {"customer": {"global-customer-id": "test", "subscriber-name": "test"}} + ] + } + with raises(ValidationError): + validator.validate(VersionsEnum.V1_0, invalid_input_data) + + +def test_validator_vsps(): + validator = Validator() + input_data = { + "vsps": [ + { + "vsp": { + "name": "test", + "vendor": "test", + "package": "test", + } + } + ] + } + validator.validate(VersionsEnum.NONE, input_data) + + input_data = { + "vsps": [ + { + "vsp": { + "name": "test", + "vendor": "test", + "package": "test", + } + } + ] + } + validator.validate(VersionsEnum.V1_0, input_data) + + input_data = { + "vsps": [ + { + "vsp": { + "name": "test", + } + } + ] + } + with raises(ValidationError): + validator.validate(VersionsEnum.V1_0, input_data) + + +def test_validator_service(): + validator = Validator() + input_data = { + "services": [ + { + "service": { + "name": "test", + "resources": [ + {"name": "test", "type": "test"}, + {"name": "test1", "type": "test2"}, + ], + "properties": [ + {"name": "test", "type": "test", "value": "test"}, + {"name": "test1", "type": "test1"}, + ], + } + } + ] + } + validator.validate(VersionsEnum.NONE, input_data) + + input_data = { + "services": [ + { + "service": { + "name": "test", + "resources": [ + {"name": "test", "type": "test"}, + {"name": "test1", "type": "test2"}, + ], + "properties": [ + {"name": "test", "type": "test", "value": "test"}, + {"name": "test1", "type": "test1"}, + ], + } + } + ] + } + validator.validate(VersionsEnum.V1_0, input_data) diff --git a/tests/test_vendor_resource.py b/tests/test_vendor_resource.py new file mode 100644 index 0000000..2a4d0aa --- /dev/null +++ b/tests/test_vendor_resource.py @@ -0,0 +1,28 @@ +from unittest.mock import patch, PropertyMock + +from onap_data_provider.resources.vendor_resource import VendorResource + +VENDOR_RESOURCE_DATA = {"name": "testVendor"} + + +@patch( + "onap_data_provider.resources.vendor_resource.VendorResource.vendor", + new_callable=PropertyMock, +) +def test_vendor_resource_exists(mock_vendor): + mock_vendor.return_value = None + vendor_resource = VendorResource(VENDOR_RESOURCE_DATA) + assert vendor_resource.exists is False + mock_vendor.return_value = 1 # Anything but not None + assert vendor_resource.exists is True + + +@patch( + "onap_data_provider.resources.vendor_resource.Vendor.created", +) +def test_vendor_resource_vendor(mock_vendor_created): + mock_vendor_created.return_value = False + vendor_resource = VendorResource(VENDOR_RESOURCE_DATA) + assert vendor_resource.vendor is None + mock_vendor_created.return_value = True + assert vendor_resource.vendor is not None diff --git a/tests/test_versions.py b/tests/test_versions.py new file mode 100644 index 0000000..7571854 --- /dev/null +++ b/tests/test_versions.py @@ -0,0 +1,15 @@ +import warnings + +from onap_data_provider.versions import VersionsEnum + + +def test_versions_init(): + v_none = VersionsEnum.get_version_by_number("None") + assert v_none == VersionsEnum.NONE + assert v_none.value.version_number == "None" + assert v_none.value.schema_path + + v_1_0 = VersionsEnum.get_version_by_number("1.0") + assert v_1_0 == VersionsEnum.V1_0 + assert v_1_0.value.version_number == "1.0" + assert v_1_0.value.schema_path diff --git a/tests/test_vnf_resource.py b/tests/test_vnf_resource.py new file mode 100644 index 0000000..6398aec --- /dev/null +++ b/tests/test_vnf_resource.py @@ -0,0 +1,28 @@ +from unittest.mock import patch, PropertyMock + +from onap_data_provider.resources.vnf_resource import VnfResource + +VNF_RESOURCE_DATA = {"name": "test_vnf"} + + +@patch( + "onap_data_provider.resources.vnf_resource.VnfResource.vnf", + new_callable=PropertyMock, +) +def test_vnf_resource_exists(mock_vnf): + mock_vnf.return_value = None + vnf_resource = VnfResource(VNF_RESOURCE_DATA) + assert vnf_resource.exists is False + mock_vnf.return_value = 1 # Anything but not None + assert vnf_resource.exists is True + + +@patch( + "onap_data_provider.resources.vnf_resource.Vf.created", +) +def test_vnf_resource_vnf(mock_vnf_created): + mock_vnf_created.return_value = False + vnf_resource = VnfResource(VNF_RESOURCE_DATA) + assert vnf_resource.vnf is None + mock_vnf_created.return_value = True + assert vnf_resource.vnf is not None diff --git a/tests/test_vsp_resource.py b/tests/test_vsp_resource.py new file mode 100644 index 0000000..9ad5bb6 --- /dev/null +++ b/tests/test_vsp_resource.py @@ -0,0 +1,28 @@ +from unittest.mock import patch, PropertyMock + +from onap_data_provider.resources.vsp_resource import VspResource + + +VSP_RESOURCE_DATA = {"name": "test", "vendor": "test", "package": "test"} + + +@patch("onap_data_provider.resources.vsp_resource.Vsp.created") +def test_vsp_resource_vsp_property(mock_vsp_created): + vsp_resource = VspResource(VSP_RESOURCE_DATA) + mock_vsp_created.return_value = False + assert vsp_resource.vsp is None + + mock_vsp_created.return_value = True + assert vsp_resource.vsp is not None + + +@patch( + "onap_data_provider.resources.vsp_resource.VspResource.vsp", + new_callable=PropertyMock, +) +def test_vsp_resource_exists(mock_vsp): + mock_vsp.return_value = None + vsp_resource = VspResource(VSP_RESOURCE_DATA) + assert not vsp_resource.exists + mock_vsp.return_value = 1 + assert vsp_resource.exists @@ -1,11 +1,11 @@ [tox] minversion = 3.2.0 -envlist = json,yaml,py,rst,md +envlist = json,yaml,py,rst,md,mypy skipsdist = true requires = pip >= 8 [testenv] -basepython = python3 +basepython = python3.8 whitelist_externals = git bash @@ -53,3 +53,10 @@ commands_pre = /bin/sh -c "git --no-pager diff HEAD HEAD^ --name-only '*.md' > /tmp/.coalist_md" commands = /bin/bash -c "coala --non-interactive --disable-caching --no-autoapply-warn md --files $(</tmp/.coalist_md) \ " + +[testenv:mypy] +deps = + mypy + types-PyYAML + -rrequirements.txt +commands = mypy --strict onap_data_provider/ |