diff options
331 files changed, 44777 insertions, 0 deletions
diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..42c57d4 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,26 @@ +[report] +# Regexes for lines to exclude from consideration +exclude_lines = + # Have to re-enable the standard pragma + pragma: no cover + + # Don't complain about missing debug-only code: + def __repr__ + if self\.debug + + # Don't complain if tests don't hit defensive assertion code: + raise AssertionError + raise NotImplementedError + + # Don't complain if non-runnable code isn't run: + if 0: + if __name__ == .__main__.: + +show_missing = True + +[html] +directory = coverage + +[xml] +output = pytest-coverage.xml + diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..9a2ed9a --- /dev/null +++ b/.dockerignore @@ -0,0 +1,134 @@ +## VSCode +.vscode/ +## Python +# tests as they're not needed +tests/ +# 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/ +pip-wheel-metadata/ +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 +coverage/ +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ +pytest-coverage.xml + +# tests +pytest-unit.xml +pytest.xml +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.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 + +# celery beat schedule file +celerybeat-schedule + +# 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/ + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8afe550 --- /dev/null +++ b/.gitignore @@ -0,0 +1,139 @@ +## VSCode +.vscode/ + +## IntelliJ +.idea/ + +## Python + +# 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/ +pip-wheel-metadata/ +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 +coverage/ +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ +pytest-coverage.xml + +# tests +pytest-unit.xml +pytest-integration.xml +pytest.xml + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.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 + +# celery beat schedule file +celerybeat-schedule + +# 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/ + diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..5ed14e3 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,145 @@ +--- + stages: + - linting + - unit_test + - build + - test + - deploy + + image: docker:git + services: + - docker:dind + variables: + DOCKER_DRIVER: overlay + # Variables for pytest.gitlab-ci.yml + PYTHON_VERSIONS: "v3.7 v3.8 v3.9 v3.10" + COVERAGE_FILE: sdk-tests-cov.xml + # Variables for Container-Scanning.gitlab-ci.yml + CI_APPLICATION_REPOSITORY: $CI_REGISTRY_IMAGE #/$CI_COMMIT_REF_SLUG + CI_APPLICATION_TAG: $CI_COMMIT_REF_SLUG #$CI_COMMIT_SHA + # Variable for pylint/pydocstyle/SAST/Code-Quality.gitlab-ci.yml + SRC_PATH: '/src' + DOC_PATH: '/docs' + # Variable for SAST + SAST_EXCLUDED_PATHS: "docs,integration_tests,scripts,tests" + SAST_BANDIT_EXCLUDED_PATHS: "docs,integration_tests,scripts,tests" + + .before_script_docker: &before_script_docker + before_script: + - docker login -u gitlab-ci-token -p "$CI_BUILD_TOKEN" "$CI_REGISTRY" + + build_master: + stage: build + <<: *before_script_docker + script: + - docker build -t "$CI_REGISTRY_IMAGE:latest" . + - docker push "$CI_REGISTRY_IMAGE:latest" + rules: + - if: '$CI_COMMIT_BRANCH == "master"' + + build_testing: + stage: build + <<: *before_script_docker + script: + - docker build -t "$CI_REGISTRY_IMAGE:${CI_COMMIT_REF_SLUG}" . + - docker push "$CI_REGISTRY_IMAGE:${CI_COMMIT_REF_SLUG}" + rules: + - if: '$CI_COMMIT_BRANCH && $CI_COMMIT_BRANCH != "master"' + + build_stable: + stage: build + <<: *before_script_docker + script: + - docker build -t "$CI_REGISTRY_IMAGE:${CI_COMMIT_TAG}" . + - docker push "$CI_REGISTRY_IMAGE:${CI_COMMIT_TAG}" + rules: + - if: '$CI_COMMIT_TAG' + + .integration_tests: &integration_tests + stage: test + variables: + FF_NETWORK_PER_BUILD: 1 # Enable https://docs.gitlab.com/runner/executors/docker.html#network-per-build feature + services: + - name: registry.gitlab.com/orange-opensource/lfn/onap/mock_servers/mock-sdc:develop + alias: sdc.api.fe.simpledemo.onap.org + - name: registry.gitlab.com/orange-opensource/lfn/onap/mock_servers/mock-sdnc:latest + alias: sdnc.api.simpledemo.onap.org + - name: registry.gitlab.com/orange-opensource/lfn/onap/mock_servers/mock-so:latest + alias: so.api.simpledemo.onap.org + - name: registry.gitlab.com/orange-opensource/lfn/onap/mock_servers/mock-aai:latest + alias: aai.api.sparky.simpledemo.onap.org + - name: registry.gitlab.com/orange-opensource/lfn/onap/mock_servers/mock-clamp:develop + alias: clamp.api.simpledemo.onap.org + - name: registry.gitlab.com/orange-opensource/lfn/onap/mock_servers/mock-cds:latest + alias: cds.api.simpledemo.onap.org + - name: registry.gitlab.com/orange-opensource/lfn/onap/mock_servers/mock-msb-k8s:latest + alias: msb.k8s.api.simpledemo.onap.org + - name: registry.gitlab.com/orange-opensource/lfn/onap/mock_servers/mock-dcae/mock-ves:latest + alias: ves.api.simpledemo.onap.org + - name: registry.gitlab.com/orange-opensource/lfn/onap/mock_servers/mock-dmaap:latest + alias: dmaap.api.simpledemo.onap.org + script: + - pip install . + - pip install pytest mock # mock is needed as pytest parse all files before selection + - PYTHONPATH=$PYTHONPATH:integration_tests/ ONAP_PYTHON_SDK_SETTINGS="urls" pytest --verbose -c /dev/null --junitxml=pytest-integration.xml integration_tests + artifacts: + reports: + junit: pytest-*.xml + + integration_tests:3.7: + image: python:3.7 + <<: *integration_tests + + integration_tests:3.8: + image: python:3.8 + <<: *integration_tests + + integration_tests:3.9: + image: python:3.9 + <<: *integration_tests + + integration_tests:3.10: + image: python:3.10 + <<: *integration_tests + + pages: + stage: deploy + image: + name: python:3.7 + script: + - chmod +x scripts/build_all_branches_in.sh + - scripts/build_all_branches_in.sh + artifacts: + paths: + - public + except: + variables: + - $JOBS_DISABLED + + upload: + stage: deploy + image: + name: python:3.8 + script: + - pip install -r upload-requirements.txt + - python setup.py sdist bdist_wheel + - twine upload --non-interactive dist/* + rules: + - if: '$CI_COMMIT_TAG' + + # https://docs.gitlab.com/ee/update/deprecations.html#dependency-scanning-python-39-and-36-image-deprecation + gemnasium-python-dependency_scanning: + image: + name: registry.gitlab.com/gitlab-org/security-products/analyzers/gemnasium-python:2-python-3.9 + + include: + - remote: 'https://gitlab.com/Orange-OpenSource/lfn/ci_cd/gitlab-ci-templates/raw/master/pylint.gitlab-ci.yml' + - remote: 'https://gitlab.com/Orange-OpenSource/lfn/ci_cd/gitlab-ci-templates/-/raw/master/pytest.gitlab-ci.yml' + - remote: 'https://gitlab.com/Orange-OpenSource/lfn/ci_cd/gitlab-ci-templates/raw/master/pydocstyle.gitlab-ci.yml' + - remote: 'https://gitlab.com/Orange-OpenSource/lfn/ci_cd/gitlab-ci-templates/raw/master/doc8.gitlab-ci.yml' + - remote: 'https://gitlab.com/Orange-OpenSource/lfn/ci_cd/gitlab-ci-templates/raw/master/pyup.gitlab-ci.yml' + - template: License-Scanning.gitlab-ci.yml + - template: Dependency-Scanning.gitlab-ci.yml + - template: Jobs/Code-Quality.gitlab-ci.yml + - template: SAST.gitlab-ci.yml + - template: Container-Scanning.gitlab-ci.yml diff --git a/.pyup.yml b/.pyup.yml new file mode 100644 index 0000000..b4adf18 --- /dev/null +++ b/.pyup.yml @@ -0,0 +1,37 @@ +# configure updates globally +# default: all +# allowed: all, insecure, False +update: all + +# configure dependency pinning globally +# default: True +# allowed: True, False +pin: True + +# set the default branch +# default: empty, the default branch on GitHub +branch: develop + +# update schedule +# default: empty +# allowed: "every day", "every week", .. +schedule: "every day" + +# search for requirement files +# default: True +# allowed: True, False +search: True + +# add a label to pull requests, default is not set +# requires private repo permissions, even on public repos +# default: empty +label_prs: update + +# assign users to pull requests, default is not set +# requires private repo permissions, even on public repos +# default: empty +assignees: + - sylvainOL + +gitlab: + should_remove_source_branch: True diff --git a/.pyup.yml.old b/.pyup.yml.old new file mode 100644 index 0000000..b4adf18 --- /dev/null +++ b/.pyup.yml.old @@ -0,0 +1,37 @@ +# configure updates globally +# default: all +# allowed: all, insecure, False +update: all + +# configure dependency pinning globally +# default: True +# allowed: True, False +pin: True + +# set the default branch +# default: empty, the default branch on GitHub +branch: develop + +# update schedule +# default: empty +# allowed: "every day", "every week", .. +schedule: "every day" + +# search for requirement files +# default: True +# allowed: True, False +search: True + +# add a label to pull requests, default is not set +# requires private repo permissions, even on public repos +# default: empty +label_prs: update + +# assign users to pull requests, default is not set +# requires private repo permissions, even on public repos +# default: empty +assignees: + - sylvainOL + +gitlab: + should_remove_source_branch: True diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..bc392e5 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,12 @@ +# Python ONAP SDK Changelog + +## v1.0 + +[Documentation](https://readthedocs.org/dashboard/python-onapsdk/version/v1.0) + +Main new features: + +- Onboard a simple service via SDC +- Instantiate a simple service via SO using GR API +- Instantiate a simple service via NBI +- create business objects in AAI diff --git a/CODEOWNER b/CODEOWNER new file mode 100644 index 0000000..82ab28a --- /dev/null +++ b/CODEOWNER @@ -0,0 +1,10 @@ +# Rules defined later in the file take precedence over the rules +# defined before. + +# For everything except doc +* @davidblaisonneau-orange @Rene_ROBERT @morganrOL @sylvainOL @jardellos + +# For doc +# This will match all files for which the file name ends in `.rst` +*.rst @eric_debeau +/docs/ @eric_debeau diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..43a897b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +FROM python:3.9-alpine3.12 + +ARG PIP_TAG=21.2.4 + +WORKDIR /opt/chained-ci-mqtt-trigger-master + +COPY . . + +ENV CRYPTOGRAPHY_DONT_BUILD_RUST=1 + +RUN apk add --no-cache --virtual .build-deps gcc \ + musl-dev \ + libffi-dev \ + openssl-dev && \ + pip install --no-cache-dir --upgrade pip==$PIP_TAG && \ + pip install --no-cache-dir . && \ + apk del .build-deps @@ -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 2019 Orange-OpenSource / lfn / onap + + 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/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..117c0c7 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +recursive-include src *.j2
\ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..0eb8fd8 --- /dev/null +++ b/README.md @@ -0,0 +1,119 @@ +# Python ONAP SDK + +an SDK to use ONAP programmatically with Python code + +[![Maintainability](https://api.codeclimate.com/v1/badges/858bb5b1aed4b42da2d2/maintainability)](https://codeclimate.com/github/Orange-OpenSource/python-onapsdk/maintainability) +[![Code Coverage](https://gitlab.com/Orange-OpenSource/lfn/onap/python-onapsdk/badges/master/coverage.svg)](https://gitlab.com/Orange-OpenSource/lfn/onap/python-onapsdk/) +[![Documentation Status](https://readthedocs.org/projects/python-onapsdk/badge/?version=latest)](https://python-onapsdk.readthedocs.io/en/latest/?badge=latest) + +## Description + +ONAP SDK is a client library written in Python for building applications to work with ONAP. The project aims to provide a consistent and complete set of interactions with ONAP’s many services, along with complete documentation, examples, and tools. + +Using few python commands, you should be able to onboard, distribute models, instantiate xNFs and many others. Check [doc](https://python-onapsdk.readthedocs.io/en/latest/index.html) site to find out all the features. + +## Installation + +You can install it using `pip` tool + +``` +$ pip install onapsdk +``` + +## Development + +Before you start, ensure you have Python installation in version 3.7 or higher. +Please see [the official Python documentation](https://docs.python.org/3/using/index.html) +in case you have to upgrade or install certain Python version. + +### Setting up development environment + +Clone the project. Inside the project folder create a new virtual environment and activate +it: + +``` +$ python -m venv env +$ source env/bin/activate +``` +On Windows, activate by executing the following: + +``` +$ .\env\Scripts\activate +``` + +When your virtual environment is ready, install required dependencies: + +``` +$ pip install -r requirements.txt +``` + +### Developing + +To use library functions directly from the source code, execute the following +to point to the source folder in *PYTHONPATH* variable and run the interpreter: + + +``` +$ PYTHONPATH=$PYTHONPATH:src/ python +``` + +On Windows: + +``` +$ $env:PYTHONPATH='src\';python +``` + +Verify that packages are accessible: + +``` +>>> import onapsdk +``` +You can then start working with library functions as needed. + +### Testing + +Install [tox](https://tox.readthedocs.io/en/latest/index.html): + +``` +$ pip install tox +``` + +To run all unit test, lint and docstyle checks, inside the project folder simply +execute *tox*: + +``` +$ tox +``` + +Please note that the above runs unit tests on all major versions of Python available on your +OS (3.7, 3.8, 3.9). To limit execution to only specific version of Python interpreter, +use the following example: + +``` +$ tox -e py37 +``` + +### Integration testing + +It is possible to run integration tests using [mock-servers](https://gitlab.com/Orange-OpenSource/lfn/onap/mock_servers) +project. +Make sure Docker Compose is available on your system. Install required dependencies: +``` +$ pip install pytest mock +``` + +Go to *integration_tests/* directory and execute: +``` +$ docker-compose up +``` +Please note that *docker-compose* attempts to create subnet 172.20.0.0/24, so it can not be run if the scope is already allocated. +Also, containers are not reachable by their IP addresses on Windows host since +Docker for Windows [does not support](https://docs.docker.com/docker-for-windows/networking/#known-limitations-use-cases-and-workarounds) +bridged network interface for Linux containers. + +Once containers are running, execute the following in the project's directory: +``` +$ PYTHONPATH=$PYTHONPATH:integration_tests/:src/ ONAP_PYTHON_SDK_SETTINGS="local_urls" pytest -c /dev/null --verbose --junitxml=pytest-integration.xml integration_tests +``` + +Please make sure all the test are passing before creating merge request.
\ No newline at end of file diff --git a/dev-requirements.txt b/dev-requirements.txt new file mode 100644 index 0000000..8e6ca20 --- /dev/null +++ b/dev-requirements.txt @@ -0,0 +1,11 @@ +requests==2.24.0 +jinja2==2.11.3 +simplejson==3.17.2 +mock==4.0.2 +pytest==6.1.1 +pytest-cov==2.10.1 +pytest-mock==3.3.1 +yapf==0.30.0 +coverage==5.3 +pyOpenSSL==19.1.0 +jsonschema==3.2.0 diff --git a/doc-requirements.txt b/doc-requirements.txt new file mode 100644 index 0000000..c33ef9c --- /dev/null +++ b/doc-requirements.txt @@ -0,0 +1,4 @@ +sphinx==3.2.1 +sphinx_rtd_theme==0.5.0 +sphinx-autodoc-typehints==1.11.1 +Pygments==2.7.4 diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..733775b --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# SPDX-License-Identifier: Apache-2.0 +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css new file mode 100644 index 0000000..b07bdb1 --- /dev/null +++ b/docs/_static/css/custom.css @@ -0,0 +1,3 @@ +.wy-nav-content { + max-width: none; +} diff --git a/docs/architecture.rst b/docs/architecture.rst new file mode 100644 index 0000000..ece5cb2 --- /dev/null +++ b/docs/architecture.rst @@ -0,0 +1,2 @@ +Architecture +############ diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..b6dd084 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,75 @@ +# SPDX-License-Identifier: Apache-2.0 +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# http://www.sphinx-doc.org/en/master/config + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +import sys +sys.path.insert(0, os.path.abspath('../src/onapsdk')) + + +# -- Project information ----------------------------------------------------- + +project = 'ONAP SDK' +copyright = '2019, Sylvain Desbureaux' +author = 'Sylvain Desbureaux' + +# The full version, including alpha/beta/rc tags +package_version = {} +with open("../src/onapsdk/version.py") as fp: + exec(fp.read(), package_version) +release = package_version['__version__'] + + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.intersphinx', + 'sphinx.ext.napoleon', + 'sphinx_autodoc_typehints' +] + +intersphinx_mapping = {'python': ('https://docs.python.org/3', None)} + +# These folders are copied to the documentation's HTML output +html_static_path = ['_static'] + +# These paths are either relative to html_static_path +# or fully qualified paths (eg. https://...) +html_css_files = [ + 'css/custom.css', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'sphinx_rtd_theme' + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] +master_doc = 'index' diff --git a/docs/description.rst b/docs/description.rst new file mode 100644 index 0000000..39cf761 --- /dev/null +++ b/docs/description.rst @@ -0,0 +1,13 @@ +Description +########### + +ONAP SDK is a client library written in Python for building applications to +work with ONAP. The project aims to provide a consistent and complete set of +interactions with ONAP’s many services, along with complete documentation, +examples, and tools. + +Using few python commands, you should be able to onboard, distribute models and +instantiate xNFs. + +First beta release deals with ONAP "Legacy" APIs but new +APIs, CDS and policy integration is planned for next releases. diff --git a/docs/development.rst b/docs/development.rst new file mode 100644 index 0000000..d1029b3 --- /dev/null +++ b/docs/development.rst @@ -0,0 +1,122 @@ +Development +############ + + + +Setting up development environment +---------------------------------- +Before you start, ensure you have Python installation in version 3.7 or higher. +Please see the official Python documentation_ in case you have to upgrade or install +certain Python version. + +.. _documentation: https://docs.python.org/3/using/index.html + +Clone the project. Inside the project folder create a new virtual environment and activate +it: + +.. code:: shell + + $ python -m venv env + $ source env/bin/activate + +On Windows, activate by executing the following: + +.. code:: powershell + + $ .\env\Scripts\activate + +When your virtual environment is ready, install required dependencies: + +.. code:: shell + + $ pip install -r requirements.txt + +Developing +---------- + +To use library functions directly from the source code, execute the following +to point to the source folder in *PYTHONPATH* variable and run the interpreter: + + +.. code:: shell + + $ PYTHONPATH=$PYTHONPATH:src/ python + + +On Windows: + +.. code:: powershell + + $ $env:PYTHONPATH='src\';python + +Verify that packages are accessible: + +.. code:: python + + >>> import onapsdk + +You can then start working with library functions as needed. + +New ONAP component package +-------------------------- + +When you create a new ONAP component package and wants to use Jinja templates you need to create `templates` directory +to store them in a newly created package. Furthermore you need to add a `PackageLoader` in `utils.jinja` module. + +Testing +------- + +Install tox: + +.. code:: shell + + $ pip install tox + +To run all unit test, lint and docstyle checks, inside the project folder simply +execute *tox*: + +.. code:: shell + + $ tox + +Please note that the above runs unit tests on all major versions of Python available on your +OS (3.7, 3.8, 3.9). To limit execution to only specific version of Python Interpreter, +use the following example: + +.. code:: shell + + $ tox -e py37 + +Integration testing +------------------- + +It is possible to run integration tests using mock-servers_ project. + +.. _mock-servers: https://gitlab.com/Orange-OpenSource/lfn/onap/mock_servers + +Make sure Docker Compose is available on your system. Install required dependencies: + +.. code:: shell + + $ pip install pytest mock + +Go to *integration_tests/* directory and execute: + +.. code:: shell + + $ docker-compose up + +Please note that *docker-compose* attempts to create subnet 172.20.0.0/24, so it can not be run if the scope is already allocated. +Also, containers are not reachable by their IP addresses on Windows host since +Docker for Windows does not support bridged network interface for Linux containers. +For reference, please see Docker docs_. + +.. _docs: https://docs.docker.com/docker-for-windows/networking/#known-limitations-use-cases-and-workarounds + +Once containers are running, execute the following in the project's directory: + +.. code:: shell + + $ PYTHONPATH=$PYTHONPATH:integration_tests/:src/ ONAP_PYTHON_SDK_SETTINGS="local_urls" pytest -c /dev/null --verbose --junitxml=pytest-integration.xml integration_tests + +Please make sure all the test are passing before creating merge request.
\ No newline at end of file diff --git a/docs/examples/e2e_artifact_upload.rst b/docs/examples/e2e_artifact_upload.rst new file mode 100644 index 0000000..aea4d21 --- /dev/null +++ b/docs/examples/e2e_artifact_upload.rst @@ -0,0 +1,58 @@ +E2E Upload of an artifact +##################################### + + +.. code:: Python + + import os + import logging + + from onapsdk.sdc.vsp import Vsp + from onapsdk.sdc.vf import Vf + from onapsdk.sdc.service import Service + + logger = logging.getLogger("") + logger.setLevel(logging.INFO) + fh = logging.StreamHandler() + fh_formatter = logging.Formatter('%(asctime)s %(levelname)s %(lineno)d:%(filename)s(%(process)d) - %(message)s') + fh.setFormatter(fh_formatter) + logger.addHandler(fh) + + + + # Create required A&AI resources + VF_NAME = "my_VF" + SERVICENAME = "artifact_SERVICE" + + ARTIFACT_NAME = "clampnode" + ARTIFACT_TYPE = "DCAE_INVENTORY_BLUEPRINT" + ARTIFACT_FILE_PATH = "{os.path.dirname(os.path.abspath(__file__))}/my_ArtifactFile.yaml" + + + logger.info("*******************************") + logger.info("******** SERVICE DESIGN *******") + logger.info("*******************************") + + logger.info("******** Get VF *******") + vf = Vf(VF_NAME) + vf.onboard() + + logger.info("******** Create Service *******") + svc = Service(name=SERVICENAME) + svc.create() + svc.add_resource(vf) + + logger.info("******** Extract Artifact Data *******") + data = open(ARTIFACT_FILE_PATH,'rb').read() + + logger.info("******** Upload Artifact *******") + svc.add_artifact_to_vf(vnf_name=VF_NAME, + artifact_type=ARTIFACT_TYPE, + artifact_name=ARTIFACT_NAME, + artifact=data) + + logger.info("******** Distribute Service *******") + svc.checkin() + svc.certify() + svc.distribute() + diff --git a/docs/examples/e2e_basicvm_nomulticloud_instantiation.rst b/docs/examples/e2e_basicvm_nomulticloud_instantiation.rst new file mode 100644 index 0000000..37f3c31 --- /dev/null +++ b/docs/examples/e2e_basicvm_nomulticloud_instantiation.rst @@ -0,0 +1,414 @@ +E2E Instantiation of a simple VM without muticloud +################################################## + + +.. code:: Python + + import logging + import time + from uuid import uuid4 + from onapsdk.aai.aai_element import AaiElement + from onapsdk.aai.cloud_infrastructure import ( + CloudRegion, + Complex, + Tenant + ) + from onapsdk.aai.service_design_and_creation import ( + Service as AaiService + ) + from onapsdk.aai.business import ( + ServiceInstance, + VnfInstance, + VfModuleInstance, + ServiceSubscription, + Customer, + OwningEntity as AaiOwningEntity + ) + from onapsdk.so.instantiation import ( + ServiceInstantiation, + VnfInstantiation, + VnfParameter + ) + from onapsdk.sdc import SDC + from onapsdk.sdc.vendor import Vendor + from onapsdk.sdc.vsp import Vsp + from onapsdk.sdc.vf import Vf + from onapsdk.sdc.service import Service + import onapsdk.constants as const + import os + from onapsdk.vid import LineOfBusiness, OwningEntity, Platform, Project + + logger = logging.getLogger("") + logger.setLevel(logging.DEBUG) + fh = logging.StreamHandler() + fh_formatter = logging.Formatter('%(asctime)s %(levelname)s %(lineno)d:%(filename)s(%(process)d) - %(message)s') + fh.setFormatter(fh_formatter) + logger.addHandler(fh) + + # Required A&AI resources + VSPNAME = "ubuntu16_VSP" + VFNAME = "ubuntu16_VF" + SERVICENAME = "ubuntu16_SERVICE" + + # FULLY CUSTOMIZABLE VALUES + # ************************* + VENDOR = "" # FILL ME + GLOBAL_CUSTOMER_ID = "" # FILL ME + SERVICE_DELETION = True #Â True|False + + COMPLEX_PHYSICAL_LOCATION_ID = "" # FILL ME + COMPLEX_DATA_CENTER_CODE = "" # FILL ME + + CLOUD_OWNER = "" # FILL ME + + OWNING_ENTITY = "" # FILL ME + PROJECT = "" # FILL ME + PLATFORM = "" # FILL ME + LINE_OF_BUSINESS = "" # FILL ME + + SERVICE_INSTANCE_NAME = "" # FILL ME + + AVAILABILITY_ZONE_NAME = "" # FILL ME + AVAILABILITY_ZONE_HYPERVISOR_TYPE = "" # FILL ME + + + # FILL ME with your INFRA values + # ****************************** + # ubuntu16.zip file path including the heat and env files + VSPFILE_PATH = "" # FILL ME + + VIM_USERNAME = "" # FILL ME + VIM_PASSWORD = "" # FILL ME + VIM_SERVICE_URL = "" # FILL ME + + TENANT_NAME = "" # FILL ME + TENANT_ID = "" # FILL ME + + CLOUD_REGION = "" # Shall be defined in Openstack + + + # ************************************************************************************************* + logger.info("*******************************") + logger.info("******** SERVICE DESIGN *******") + logger.info("*******************************") + + logger.info("******** Onboard Vendor *******") + vendor = Vendor(name=VENDOR) + vendor.onboard() + + logger.info("******** Onboard VSP *******") + vsp = Vsp(name=VSPNAME, vendor=vendor, package=open(VSPFILE_PATH, 'rb')) + vsp.onboard() + + logger.info("******** Onboard VF *******") + vf = Vf(name=VFNAME) + vf.vsp = vsp + vf.onboard() + + logger.info("******** Onboard Service *******") + svc = Service(name=SERVICENAME, resources=[vf]) + svc.onboard() + + logger.info("******** Check Service Distribution *******") + distribution_completed = False + nb_try = 0 + nb_try_max = 10 + while distribution_completed is False and nb_try < nb_try_max: + distribution_completed = svc.distributed + if distribution_completed is True: + logger.info("Service Distribution for %s is sucessfully finished",svc.name) + break + logger.info("Service Distribution for %s ongoing, Wait for 60 s",svc.name) + time.sleep(60) + nb_try += 1 + + if distribution_completed is False: + logger.error("Service Distribution for %s failed !!",svc.name) + exit(1) + + logger.info("*******************************") + logger.info("***** RUNTIME PREPARATION *****") + logger.info("*******************************") + + logger.info("******** Create Complex *******") + cmplx = Complex.create( + physical_location_id=COMPLEX_PHYSICAL_LOCATION_ID, + data_center_code=COMPLEX_DATA_CENTER_CODE, + name=COMPLEX_PHYSICAL_LOCATION_ID + ) + + logger.info("******** Create CloudRegion *******") + # Note for non multicloud instanciation, cloud_region_version shall be set to openstack + # versus + cloud_region = CloudRegion.create( + cloud_owner=CLOUD_OWNER, + cloud_region_id=CLOUD_REGION, + orchestration_disabled=False, + in_maint=False, + cloud_type="openstack", + cloud_region_version="openstack", + cloud_zone="z1", + complex_name=COMPLEX_PHYSICAL_LOCATION_ID + ) + + logger.info("******** Link Complex to CloudRegion *******") + cloud_region.link_to_complex(cmplx) + + logger.info("******** Add ESR Info to CloudRegion *******") + cloud_region.add_esr_system_info( + esr_system_info_id=str(uuid4()), + user_name=VIM_USERNAME, + password=VIM_PASSWORD, + system_type="VIM", + service_url=VIM_SERVICE_URL, + cloud_domain="Default", + ssl_insecure=False, + system_status="active", + default_tenant=TENANT_NAME + ) + + logger.info("*******************************") + logger.info("**** SERVICE INSTANTIATION ****") + logger.info("*******************************") + + logger.info("******** Create Customer *******") + customer = None + for found_customer in list(Customer.get_all()): + logger.debug("Customer %s found", found_customer.subscriber_name) + if found_customer.subscriber_name == GLOBAL_CUSTOMER_ID: + logger.info("Customer %s found", found_customer.subscriber_name) + customer = found_customer + break + if not customer: + customer = Customer.create(GLOBAL_CUSTOMER_ID,GLOBAL_CUSTOMER_ID, "INFRA") + + logger.info("******** Find Service in SDC *******") + service = None + services = Service.get_all() + for found_service in services: + logger.debug("Service %s is found, distribution %s",found_service.name, found_service.distribution_status) + if found_service.name == SERVICENAME: + logger.info("Found Service %s in SDC",found_service.name) + service = found_service + break + + if not service: + logger.error("Service %s not found in SDC",SERVICENAME) + exit(1) + + logger.info("******** Check Service Subscription *******") + service_subscription = None + for service_sub in customer.service_subscriptions: + logger.debug("Service subscription %s is found",service_sub.service_type) + if service_sub.service_type == SERVICENAME: + logger.info("Service %s subscribed",SERVICENAME) + service_subscription = service_sub + break + + if not service_subscription: + logger.info("******** Subscribe Service *******") + customer.subscribe_service(SERVICENAME) + + logger.info("******** Get Tenant *******") + cloud_region = CloudRegion(cloud_owner=CLOUD_OWNER, cloud_region_id=CLOUD_REGION, + orchestration_disabled=True, in_maint=False) + try: + tenant: Tenant = cloud_region.get_tenant(settings.TENANT_ID) + except ValueError: + logger.warning("Impossible to retrieve the Specificed Tenant") + logger.debug("If no multicloud selected, add the tenant") + cloud_region.add_tenant( + tenant_id=settings.TENANT_ID, + tenant_name=settings.TENANT_NAME) + + # be sure that an availability zone has been created + # if not, create it + try: + cloud_region.get_availability_zone_by_name( + settings.AVAILABILITY_ZONE_NAME) + except ValueError: + cloud_region.add_availability_zone( + settings.AVAILABILITY_ZONE_NAME, + settings.AVAILABILITY_ZONE_HYPERVISOR_TYPE) + + logger.info("******** Connect Service to Tenant *******") + service_subscription = None + for service_sub in customer.service_subscriptions: + logger.debug("Service subscription %s is found",service_sub.service_type) + if service_sub.service_type == SERVICENAME: + logger.info("Service %s subscribed",SERVICENAME) + service_subscription = service_sub + break + + if not service_subscription: + logger.error("Service subscription %s is not found",SERVICENAME) + exit(1) + + service_subscription.link_to_cloud_region_and_tenant(cloud_region, tenant) + + logger.info("******** Add Business Objects (OE, P, Pl, LoB) in VID *******") + vid_owning_entity = OwningEntity.create(OWNING_ENTITY) + vid_project = Project.create(PROJECT) + vid_platform = Platform.create(PLATFORM) + vid_line_of_business = LineOfBusiness.create(LINE_OF_BUSINESS) + + logger.info("******** Add Owning Entity in AAI *******") + owning_entity = None + for oe in AaiOwningEntity.get_all(): + if oe.name == vid_owning_entity.name: + owning_entity = oe + break + if not owning_entity: + logger.info("******** Owning Entity not existing: create *******") + owning_entity = AaiOwningEntity.create(vid_owning_entity.name, str(uuid4())) + + logger.info("******** Instantiate Service *******") + service_instance = None + service_instantiation = None + for se in service_subscription.service_instances: + if se.instance_name == SERVICE_INSTANCE_NAME: + service_instance = se + break + if not service_instance: + logger.info("******** Service Instance not existing: Instantiate *******") + # Instantiate service + service_instantiation = ServiceInstantiation.instantiate_so_ala_carte( + service, + cloud_region, + tenant, + customer, + owning_entity, + vid_project, + service_instance_name=SERVICE_INSTANCE_NAME + ) + time.sleep(60) + else: + logger.info("******** Service Instance already existing *******") + + service_instance = None + for se in service_subscription.service_instances: + if se.instance_name == SERVICE_INSTANCE_NAME: + service_instance = se + break + if not service_instance: + logger.error("******** Service %s instantiation failed",SERVICE_INSTANCE_NAME) + exit(1) + + nb_try = 0 + nb_try_max = 10 + service_active = False + while service_active is False and nb_try < nb_try_max: + if service_instance.orchestration_status == "Active": + logger.info("******** Service Instance %s is active *******",service_instance.name) + service_active = True + break + logger.info("Service %s instantiation not complete,Status:%s, wait 10s",service_instance.name,service_instance.orchestration_status) + time.sleep(10) + nb_try += 1 + + if service_active is False: + logger.error("Service %s instantiation failed",service_instance.name) + exit(1) + + + logger.info("******** Get VNFs in Service Model *******") + vnfs = service_instance.service_subscription.sdc_service.vnfs + + logger.info("******** Create VNFs *******") + for vnf in vnfs: + logger.debug("Check if VNF instance of class %s exist", vnf.name) + vnf_found = False + for vnf_instance in service_instance.vnf_instances: + logger.debug("VNF instance %s found in Service Instance ",vnf_instance.name) + vnf_found = True + if vnf_found is False: + vnf_instantiation = service_instance.add_vnf(vnf, vid_line_of_business, vid_platform) + while not vnf_instantiation.finished: + print("Wait for VNF %s instantiation",vnf.name) + time.sleep(10) + + + for vnf_instance in service_instance.vnf_instances: + logger.debug("VNF instance %s found in Service Instance ",vnf_instance.name) + logger.info("******** Get VfModules in VNF Model *******") + logger.info("******** Check VF Modules *******") + vf_module = vnf_instance.vnf.vf_module + + logger.info("******** Create VF Module %s *******",vf_module.name) + + for vf_module in vnf_instance.vnf.vf_modules: + vf_module_instantiation = vnf_instance.add_vf_module( + vf_module, + cloud_region,tenant, + SERVICE_INSTANCE_NAME, + vnf_parameters=[]) + nb_try = 0 + nb_try_max = 30 + while not vf_module_instantiation.finished and nb_try < nb_try_max: + logger.info("Wait for vf module instantiation") + nb_try += 1 + time.sleep(10) + if vf_module_instantiation.finished: + logger.info("VfModule %s instantiated",vf_module.name) + else: + logger.error("VfModule instantiation %s failed",vf_module.name) + + if SERVICE_DELETION is False: + logger.info("*****************************************") + logger.info("**** No Deletion requested, finished ****") + logger.info("*****************************************") + exit(0) + + logger.info("*******************************") + logger.info("**** SERVICE DELETION *********") + logger.info("*******************************") + time.sleep(30) + + for vnf_instance in service_instance.vnf_instances: + logger.debug("VNF instance %s found in Service Instance ",vnf_instance.name) + logger.info("******** Get VF Modules *******") + for vf_module in vnf_instance.vf_modules: + logger.info("******** Delete VF Module %s *******",vf_module.name) + vf_module_deletion = vf_module.delete() + + nb_try = 0 + nb_try_max = 30 + while not vf_module_deletion.finished and nb_try < nb_try_max: + logger.info("Wait for vf module deletion") + nb_try += 1 + time.sleep(10) + if vf_module_deletion.finished: + logger.info("VfModule %s deleted",vf_module.name) + else: + logger.error("VfModule deletion %s failed",vf_module.name) + exit(1) + + logger.info("******** Delete VNF %s *******",vnf_instance.name) + vnf_deletion = vnf_instance.delete() + + nb_try = 0 + nb_try_max = 30 + while not vnf_deletion.finished and nb_try < nb_try_max: + logger.info("Wait for vnf deletion") + nb_try += 1 + time.sleep(10) + if vnf_deletion.finished: + logger.info("VNF %s deleted",vnf_instance.name) + else: + logger.error("VNF deletion %s failed",vnf_instance.name) + exit(1) + + logger.info("******** Delete Service %s *******",service_instance.name) + service_deletion = service_instance.delete() + + nb_try = 0 + nb_try_max = 30 + while not service_deletion.finished and nb_try < nb_try_max: + logger.info("Wait for Service deletion") + nb_try += 1 + time.sleep(10) + if service_deletion.finished: + logger.info("Service %s deleted",service_instance.name) + else: + logger.error("Service deletion %s failed",service_instance.name) + exit(1) diff --git a/docs/examples/e2e_closed_loop_instantiation.rst b/docs/examples/e2e_closed_loop_instantiation.rst new file mode 100644 index 0000000..a263a66 --- /dev/null +++ b/docs/examples/e2e_closed_loop_instantiation.rst @@ -0,0 +1,108 @@ +E2E Instantiation of a Closed Loop +########################################## + + +.. code:: Python + + #Service already created in this case + + logger = logging.getLogger("") + logger.setLevel(logging.INFO) + fh = logging.StreamHandler() + fh_formatter = logging.Formatter('%(asctime)s %(levelname)s %(lineno)d:%(filename)s(%(process)d) - %(message)s') + fh.setFormatter(fh_formatter) + logger.addHandler(fh) + + + #Constants + SERVICE_NAME = "Test_SDK" + POLICY_NAME = ["MinMax", "FrequencyLimiter"] + LOOP_INSTANCE_NAME = "instance01" + CERT = (PEM, KEY) # you must add clamp cert for AUTHENTIFICATION + + Clamp.set_proxy({ 'http': 'socks5h://127.0.0.1:8080', 'https': 'socks5h://127.0.0.1:8080'}) + Service.set_proxy({ 'http': 'socks5h://127.0.0.1:8080', 'https': 'socks5h://127.0.0.1:8080'}) + + logger.info("*******************************") + logger.info("******** SERVICE FETCH *******") + logger.info("*******************************") + + svc = Service(name=SERVICE_NAME) + + logger.info("***************************************") + logger.info("******** CLAMP AUTHENTIFICATION *******") + logger.info("***************************************") + + Clamp(cert=CERT) + + logger.info("*************************************") + logger.info("******** LOOP TEMPLATES CHECK *******") + logger.info("*************************************") + + loop_template = Clamp.check_loop_template(service=svc) + if not loop_template: + logger.error("Loop template for the service %s not found", svc.name) + exit(1) + + logger.info("*******************************") + logger.info("******** POLICIES CHECK *******") + logger.info("*******************************") + + minmax_exists = Clamp.check_policies(policy_name=POLICY_NAME[0], + req_policies=30) + frequency_exists = Clamp.check_policies(policy_name=POLICY_NAME[1], + req_policies=30) + policy_exists = (minmax_exists and frequency_exists) + if not policy_exists: + logger.error("Couldn't load the policy %s", POLICY_NAME) + exit(1) + + logger.info("***********************************") + logger.info("******** LOOP INSTANTIATION *******") + logger.info("***********************************") + + loop = LoopInstance(template=loop_template, name=LOOP_INSTANCE_NAME, details={}, cert=CERT) + loop.create() + if loop.details: + logger.info("Loop instance %s successfully created !!", LOOP_INSTANCE_NAME) + else: + logger.error("An error occured while creating the loop instance") + + logger.info("******** UPDATE MICROSERVICE POLICY *******") + loop._update_loop_details() + loop.update_microservice_policy() + + logger.info("******** ADD OPERATIONAL POLICY MINMAX *******") + added = loop.add_operational_policy(policy_type="onap.policies.controlloop.guard.common.MinMax", + policy_version="1.0.0") + + logger.info("******** CONFIGURE OPERATIONAL POLICY MINMAX *******") + loop.add_op_policy_config(loop.add_minmax_config) + + logger.info("******** ADD FREQUENCY POLICY *******") + added = loop.add_operational_policy(policy_type="onap.policies.controlloop.guard.common.FrequencyLimiter", + policy_version="1.0.0") + + logger.info("******** CONFIGURE FREQUENCY POLICY *******") + loop.add_op_policy_config(loop.add_frequency_limiter) + + logger.info("******** SUBMIT POLICIES TO PE *******") + submit = loop.act_on_loop_policy(loop.submit) + + logger.info("******** CHECK POLICIES SUBMITION *******") + if submit : + logger.info("Policies successfully submited to PE") + else: + logger.error("An error occured while submitting the loop instance") + exit(1) + + logger.info("******** DEPLOY LOOP INSTANCE *******") + deploy = loop.deploy_microservice_to_dcae() + if deploy: + logger.info("Loop instance %s successfully deployed on DCAE !!", LOOP_INSTANCE_NAME) + else: + logger.error("An error occured while deploying the loop instance") + exit(2) + + logger.info("******** DELETE LOOP INSTANCE *******") + loop.delete() diff --git a/docs/examples/e2e_net_instantiation.rst b/docs/examples/e2e_net_instantiation.rst new file mode 100644 index 0000000..bfa4346 --- /dev/null +++ b/docs/examples/e2e_net_instantiation.rst @@ -0,0 +1,332 @@ +E2E Instantiation of a simple Network +##################################### + + +.. code:: Python + + import logging + import time + from uuid import uuid4 + from onapsdk.aai.aai_element import AaiElement + from onapsdk.aai.cloud_infrastructure import ( + CloudRegion, + Complex, + Tenant + ) + from onapsdk.aai.service_design_and_creation import ( + Service as AaiService + ) + from onapsdk.aai.business import ( + ServiceInstance, + ServiceSubscription, + Customer, + OwningEntity as AaiOwningEntity + ) + from onapsdk.so.instantiation import ( + ServiceInstantiation, + Subnet + ) + from onapsdk.sdc.service import Service + from onapsdk.sdc.vl import Vl + import onapsdk.constants as const + import os + from onapsdk.vid import LineOfBusiness, OwningEntity, Platform, Project + + logger = logging.getLogger("") + logger.setLevel(logging.INFO) + fh = logging.StreamHandler() + fh_formatter = logging.Formatter('%(asctime)s %(levelname)s %(lineno)d:%(filename)s(%(process)d) - %(message)s') + fh.setFormatter(fh_formatter) + logger.addHandler(fh) + + + + # Create required A&AI resources + VL_NAME = "Generic NeutronNet" + SERVICENAME = "net_SERVICE" + + GLOBAL_CUSTOMER_ID = "" # FILL ME + COMPLEX_PHYSICAL_LOCATION_ID = "" # FILL ME + COMPLEX_DATA_CENTER_CODE = "" # FILL ME + + CLOUD_OWNER = "" # FILL ME + CLOUD_REGION = "" # FILL ME + + VIM_USERNAME = "" # FILL ME + VIM_PASSWORD = "" # FILL ME + VIM_SERVICE_URL = "" # FILL ME + + TENANT_NAME = "" # FILL ME + OWNING_ENTITY = "" # FILL ME + PROJECT = "" # FILL ME + PLATFORM = "" # FILL ME + LINE_OF_BUSINESS = "" # FILL ME + + SERVICE_INSTANCE_NAME = "net-Instance" + SERVICE_DELETION = True + + logger.info("*******************************") + logger.info("******** SERVICE DESIGN *******") + logger.info("*******************************") + + logger.info("******** Get VL *******") + vl = Vl(VL_NAME) + + logger.info("******** Onboard Service *******") + svc = Service(name=SERVICENAME, resources=[vl]) + svc.onboard() + + logger.info("******** Check Service Distribution *******") + distribution_completed = False + nb_try = 0 + nb_try_max = 10 + while distribution_completed is False and nb_try < nb_try_max: + distribution_completed = svc.distributed + if distribution_completed is True: + logger.info("Service Distribution for %s is sucessfully finished",svc.name) + break + logger.info("Service Distribution for %s ongoing, Wait for 60 s",svc.name) + time.sleep(60) + nb_try += 1 + + if distribution_completed is False: + logger.error("Service Distribution for %s failed !!",svc.name) + exit(1) + + logger.info("*******************************") + logger.info("***** RUNTIME PREPARATION *****") + logger.info("*******************************") + + logger.info("******** Create Complex *******") + cmplx = Complex.create( + physical_location_id=COMPLEX_PHYSICAL_LOCATION_ID, + data_center_code=COMPLEX_DATA_CENTER_CODE, + name=COMPLEX_PHYSICAL_LOCATION_ID + ) + + logger.info("******** Create CloudRegion *******") + cloud_region = CloudRegion.create( + cloud_owner=CLOUD_OWNER, + cloud_region_id=CLOUD_REGION, + orchestration_disabled=False, + in_maint=False, + cloud_type="openstack", + cloud_region_version="titanium_cloud", + cloud_zone="z1", + complex_name=COMPLEX_PHYSICAL_LOCATION_ID + ) + + logger.info("******** Link Complex to CloudRegion *******") + cloud_region.link_to_complex(cmplx) + + logger.info("******** Add ESR Info to CloudRegion *******") + cloud_region.add_esr_system_info( + esr_system_info_id=str(uuid4()), + user_name=VIM_USERNAME, + password=VIM_PASSWORD, + system_type="VIM", + service_url=VIM_SERVICE_URL, + cloud_domain="Default", + ssl_insecure=False, + system_status="active", + default_tenant=TENANT_NAME + ) + + logger.info("******** Register CloudRegion to MultiCloud *******") + cloud_region.register_to_multicloud() + + logger.info("******** Check MultiCloud Registration *******") + time.sleep(60) + registration_completed = False + nb_try = 0 + nb_try_max = 10 + while registration_completed is False and nb_try < nb_try_max: + for tenant in cloud_region.tenants: + logger.debug("Tenant %s found in %s_%s",tenant.name,cloud_region.cloud_owner,cloud_region.cloud_region_id) + registration_completed = True + if registration_completed is False: + time.sleep(60) + nb_try += 1 + + if registration_completed is False: + logger.error("Registration of Cloud %s_%s failed !!",cloud_region.cloud_owner,cloud_region.cloud_region_id) + exit(1) + else: + logger.info("Registration of Cloud %s_%s successful !!",cloud_region.cloud_owner,cloud_region.cloud_region_id) + + logger.info("*******************************") + logger.info("**** SERVICE INSTANTIATION ****") + logger.info("*******************************") + + logger.info("******** Create Customer *******") + customer = None + for found_customer in list(Customer.get_all()): + logger.debug("Customer %s found", found_customer.subscriber_name) + if found_customer.subscriber_name == GLOBAL_CUSTOMER_ID: + logger.info("Customer %s found", found_customer.subscriber_name) + customer = found_customer + break + if not customer: + customer = Customer.create(GLOBAL_CUSTOMER_ID,GLOBAL_CUSTOMER_ID, "INFRA") + + logger.info("******** Find Service in SDC *******") + service = None + services = Service.get_all() + for found_service in services: + logger.debug("Service %s is found, distribution %s",found_service.name, found_service.distribution_status) + if found_service.name == SERVICENAME: + logger.info("Found Service %s in SDC",found_service.name) + service = found_service + break + + if not service: + logger.error("Service %s not found in SDC",SERVICENAME) + exit(1) + + logger.info("******** Check Service Subscription *******") + service_subscription = None + for service_sub in customer.service_subscriptions: + logger.debug("Service subscription %s is found",service_sub.service_type) + if service_sub.service_type == SERVICENAME: + logger.info("Service %s subscribed",SERVICENAME) + service_subscription = service_sub + break + + if not service_subscription: + logger.info("******** Subscribe Service *******") + customer.subscribe_service(SERVICENAME) + + logger.info("******** Get Tenant *******") + cloud_region = CloudRegion(cloud_owner=CLOUD_OWNER, cloud_region_id=CLOUD_REGION, + orchestration_disabled=True, in_maint=False) + tenant = None + for found_tenant in cloud_region.tenants: + logger.debug("Tenant %s found in %s_%s",found_tenant.name,cloud_region.cloud_owner,cloud_region.cloud_region_id) + if found_tenant.name == TENANT_NAME: + logger.info("Found my Tenant %s",found_tenant.name) + tenant = found_tenant + break + + if not tenant: + logger.error("tenant %s not found",TENANT_NAME) + exit(1) + + logger.info("******** Connect Service to Tenant *******") + service_subscription = None + for service_sub in customer.service_subscriptions: + logger.debug("Service subscription %s is found",service_sub.service_type) + if service_sub.service_type == SERVICENAME: + logger.info("Service %s subscribed",SERVICENAME) + service_subscription = service_sub + break + + if not service_subscription: + logger.error("Service subscription %s is not found",SERVICENAME) + exit(1) + + service_subscription.link_to_cloud_region_and_tenant(cloud_region, tenant) + + logger.info("******** Add Business Objects (OE, P, Pl, LoB) in VID *******") + vid_owning_entity = OwningEntity.create(OWNING_ENTITY) + vid_project = Project.create(PROJECT) + vid_platform = Platform.create(PLATFORM) + vid_line_of_business = LineOfBusiness.create(LINE_OF_BUSINESS) + + logger.info("******** Add Owning Entity in AAI *******") + owning_entity = None + for oe in AaiOwningEntity.get_all(): + if oe.name == vid_owning_entity.name: + owning_entity = oe + break + if not owning_entity: + logger.info("******** Owning Entity not existing: create *******") + owning_entity = AaiOwningEntity.create(vid_owning_entity.name, str(uuid4())) + + logger.info("******** Instantiate Service *******") + service_instance = None + service_instantiation = None + for se in service_subscription.service_instances: + if se.instance_name == SERVICE_INSTANCE_NAME: + service_instance = se + break + if not service_instance: + logger.info("******** Service Instance not existing: Instantiate *******") + # Instantiate service + service_instantiation = ServiceInstantiation.instantiate_so_ala_carte( + service, + cloud_region, + tenant, + customer, + owning_entity, + vid_project, + service_instance_name=SERVICE_INSTANCE_NAME + ) + time.sleep(60) + else: + logger.info("******** Service Instance already existing *******") + + service_instance = None + for se in service_subscription.service_instances: + if se.instance_name == SERVICE_INSTANCE_NAME: + service_instance = se + break + if not service_instance: + logger.error("******** Service %s instantiation failed",SERVICE_INSTANCE_NAME) + exit(1) + + nb_try = 0 + nb_try_max = 10 + service_active = False + while service_active is False and nb_try < nb_try_max: + if service_instance.orchestration_status == "Active": + logger.info("******** Service Instance %s is active *******",service_instance.name) + service_active = True + break + logger.info("Service %s instantiation not complete,Status:%s, wait 10s",service_instance.name,service_instance.orchestration_status) + time.sleep(10) + nb_try += 1 + + if service_active is False: + logger.error("Service %s instantiation failed",service_instance.name) + exit(1) + + + logger.info("******** Get Networks in Service Model *******") + networks = service_instance.service_subscription.sdc_service.networks + + logger.info("******** Create Network *******") + sn=Subnet(name="test", start_address="127.0.0.0", gateway_address="127.0.0.1") + for network in networks: + logger.debug("Check if Network instance of class %s exist", network.name) + network_found = False + for network_instance in service_instance.network_instances: + logger.debug("Network instance %s found in Service Instance ",network_intance.name) + network_found = True + if network_found is False: + network_instantiation = service_instance.add_network(network, vid_line_of_business, vid_platform, subnets=[sn]) + network_instantiation.wait_for_finish() + + + if SERVICE_DELETION is False: + logger.info("*****************************************") + logger.info("**** No Deletion requested, finished ****") + logger.info("*****************************************") + exit(0) + + logger.info("*******************************") + logger.info("**** SERVICE DELETION *********") + logger.info("*******************************") + time.sleep(30) + + for network_instance in service_instance.network_instances: + logger.debug("Network instance %s found in Service Instance ",network_instance.name) + + logger.info("******** Delete Network %s *******",network_instance.name) + network_deletion = network_instance.delete() + network_deletion.wait_for_finish() + + logger.info("******** Delete Service %s *******",service_instance.name) + service_deletion = service_instance.delete() + service_deletion.wait_for_finish() + + diff --git a/docs/examples/e2e_vfw_instantiation.rst b/docs/examples/e2e_vfw_instantiation.rst new file mode 100644 index 0000000..5455d7d --- /dev/null +++ b/docs/examples/e2e_vfw_instantiation.rst @@ -0,0 +1,429 @@ +E2E Instantiation of vFW (a'la carte) +##################################### + + +.. code:: Python + + import logging + import time + from uuid import uuid4 + from onapsdk.aai.aai_element import AaiElement + from onapsdk.aai.cloud_infrastructure import ( + CloudRegion, + Complex, + Tenant + ) + from onapsdk.aai.service_design_and_creation import ( + Service as AaiService + ) + from onapsdk.aai.business import ( + ServiceInstance, + VnfInstance, + VfModuleInstance, + ServiceSubscription, + Customer, + OwningEntity as AaiOwningEntity + ) + from onapsdk.so.instantiation import ( + ServiceInstantiation, + VnfInstantiation, + VnfParameter + ) + from onapsdk.sdc import SDC + from onapsdk.sdc.vendor import Vendor + from onapsdk.sdc.vsp import Vsp + from onapsdk.sdc.vf import Vf + from onapsdk.sdc.service import Service + import onapsdk.constants as const + import os + from onapsdk.vid import LineOfBusiness, OwningEntity, Platform, Project + + logger = logging.getLogger("") + logger.setLevel(logging.INFO) + fh = logging.StreamHandler() + fh_formatter = logging.Formatter('%(asctime)s %(levelname)s %(lineno)d:%(filename)s(%(process)d) - %(message)s') + fh.setFormatter(fh_formatter) + logger.addHandler(fh) + + + + # Create required A&AI resources + VENDOR = "VNFVendor" + VSPFILE = "vsp/vfw.zip" + VSPNAME = "vfw_VSP" + VFNAME = "vfw_VF" + SERVICENAME = "vfw_SERVICE" + + GLOBAL_CUSTOMER_ID = "" # FILL ME + COMPLEX_PHYSICAL_LOCATION_ID = "" # FILL ME + COMPLEX_DATA_CENTER_CODE = "" # FILL ME + + CLOUD_OWNER = "" # FILL ME + CLOUD_REGION = "" # FILL ME + + VIM_USERNAME = "" # FILL ME + VIM_PASSWORD = "" # FILL ME + VIM_SERVICE_URL = "" # FILL ME + + TENANT_NAME = "" # FILL ME + OWNING_ENTITY = "" # FILL ME + PROJECT = "" # FILL ME + PLATFORM = "" # FILL ME + LINE_OF_BUSINESS = "" # FILL ME + + SERVICE_INSTANCE_NAME = "vFW-Instance" + SERVICE_DELETION = True + + logger.info("*******************************") + logger.info("******** SERVICE DESIGN *******") + logger.info("*******************************") + + logger.info("******** Onboard Vendor *******") + vendor = Vendor(name=VENDOR) + vendor.onboard() + + logger.info("******** Onboard VSP *******") + mypath = os.path.dirname(os.path.realpath(__file__)) + myvspfile = os.path.join(mypath, VSPFILE) + vsp = Vsp(name=VSPNAME, vendor=vendor, package=open(myvspfile, 'rb')) + vsp.onboard() + + logger.info("******** Onboard VF *******") + vf = Vf(name=VFNAME) + vf.vsp = vsp + vf.onboard() + + logger.info("******** Onboard Service *******") + svc = Service(name=SERVICENAME, resources=[vf]) + svc.onboard() + + logger.info("******** Check Service Distribution *******") + distribution_completed = False + nb_try = 0 + nb_try_max = 10 + while distribution_completed is False and nb_try < nb_try_max: + distribution_completed = svc.distributed + if distribution_completed is True: + logger.info("Service Distribution for %s is sucessfully finished",svc.name) + break + logger.info("Service Distribution for %s ongoing, Wait for 60 s",svc.name) + time.sleep(60) + nb_try += 1 + + if distribution_completed is False: + logger.error("Service Distribution for %s failed !!",svc.name) + exit(1) + + logger.info("*******************************") + logger.info("***** RUNTIME PREPARATION *****") + logger.info("*******************************") + + logger.info("******** Create Complex *******") + cmplx = Complex.create( + physical_location_id=COMPLEX_PHYSICAL_LOCATION_ID, + data_center_code=COMPLEX_DATA_CENTER_CODE, + name=COMPLEX_PHYSICAL_LOCATION_ID + ) + + logger.info("******** Create CloudRegion *******") + cloud_region = CloudRegion.create( + cloud_owner=CLOUD_OWNER, + cloud_region_id=CLOUD_REGION, + orchestration_disabled=False, + in_maint=False, + cloud_type="openstack", + cloud_region_version="titanium_cloud", + cloud_zone="z1", + complex_name=COMPLEX_PHYSICAL_LOCATION_ID + ) + + logger.info("******** Link Complex to CloudRegion *******") + cloud_region.link_to_complex(cmplx) + + logger.info("******** Add ESR Info to CloudRegion *******") + cloud_region.add_esr_system_info( + esr_system_info_id=str(uuid4()), + user_name=VIM_USERNAME, + password=VIM_PASSWORD, + system_type="VIM", + service_url=VIM_SERVICE_URL, + cloud_domain="Default", + ssl_insecure=False, + system_status="active", + default_tenant=TENANT_NAME + ) + + logger.info("******** Register CloudRegion to MultiCloud *******") + cloud_region.register_to_multicloud() + + logger.info("******** Check MultiCloud Registration *******") + time.sleep(60) + registration_completed = False + nb_try = 0 + nb_try_max = 10 + while registration_completed is False and nb_try < nb_try_max: + for tenant in cloud_region.tenants: + logger.debug("Tenant %s found in %s_%s",tenant.name,cloud_region.cloud_owner,cloud_region.cloud_region_id) + registration_completed = True + if registration_completed is False: + time.sleep(60) + nb_try += 1 + + if registration_completed is False: + logger.error("Registration of Cloud %s_%s failed !!",cloud_region.cloud_owner,cloud_region.cloud_region_id) + exit(1) + else: + logger.info("Registration of Cloud %s_%s successful !!",cloud_region.cloud_owner,cloud_region.cloud_region_id) + + logger.info("*******************************") + logger.info("**** SERVICE INSTANTIATION ****") + logger.info("*******************************") + + logger.info("******** Create Customer *******") + customer = None + for found_customer in list(Customer.get_all()): + logger.debug("Customer %s found", found_customer.subscriber_name) + if found_customer.subscriber_name == GLOBAL_CUSTOMER_ID: + logger.info("Customer %s found", found_customer.subscriber_name) + customer = found_customer + break + if not customer: + customer = Customer.create(GLOBAL_CUSTOMER_ID,GLOBAL_CUSTOMER_ID, "INFRA") + + logger.info("******** Find Service in SDC *******") + service = None + services = Service.get_all() + for found_service in services: + logger.debug("Service %s is found, distribution %s",found_service.name, found_service.distribution_status) + if found_service.name == SERVICENAME: + logger.info("Found Service %s in SDC",found_service.name) + service = found_service + break + + if not service: + logger.error("Service %s not found in SDC",SERVICENAME) + exit(1) + + logger.info("******** Check Service Subscription *******") + service_subscription = None + for service_sub in customer.service_subscriptions: + logger.debug("Service subscription %s is found",service_sub.service_type) + if service_sub.service_type == SERVICENAME: + logger.info("Service %s subscribed",SERVICENAME) + service_subscription = service_sub + break + + if not service_subscription: + logger.info("******** Subscribe Service *******") + customer.subscribe_service(SERVICENAME) + + logger.info("******** Get Tenant *******") + cloud_region = CloudRegion(cloud_owner=CLOUD_OWNER, cloud_region_id=CLOUD_REGION, + orchestration_disabled=True, in_maint=False) + tenant = None + for found_tenant in cloud_region.tenants: + logger.debug("Tenant %s found in %s_%s",found_tenant.name,cloud_region.cloud_owner,cloud_region.cloud_region_id) + if found_tenant.name == TENANT_NAME: + logger.info("Found my Tenant %s",found_tenant.name) + tenant = found_tenant + break + + if not tenant: + logger.error("tenant %s not found",TENANT_NAME) + exit(1) + + logger.info("******** Connect Service to Tenant *******") + service_subscription = None + for service_sub in customer.service_subscriptions: + logger.debug("Service subscription %s is found",service_sub.service_type) + if service_sub.service_type == SERVICENAME: + logger.info("Service %s subscribed",SERVICENAME) + service_subscription = service_sub + break + + if not service_subscription: + logger.error("Service subscription %s is not found",SERVICENAME) + exit(1) + + service_subscription.link_to_cloud_region_and_tenant(cloud_region, tenant) + + logger.info("******** Add Business Objects (OE, P, Pl, LoB) in VID *******") + vid_owning_entity = OwningEntity.create(OWNING_ENTITY) + vid_project = Project.create(PROJECT) + vid_platform = Platform.create(PLATFORM) + vid_line_of_business = LineOfBusiness.create(LINE_OF_BUSINESS) + + logger.info("******** Add Owning Entity in AAI *******") + owning_entity = None + for oe in AaiOwningEntity.get_all(): + if oe.name == vid_owning_entity.name: + owning_entity = oe + break + if not owning_entity: + logger.info("******** Owning Entity not existing: create *******") + owning_entity = AaiOwningEntity.create(vid_owning_entity.name, str(uuid4())) + + logger.info("******** Instantiate Service *******") + service_instance = None + service_instantiation = None + for se in service_subscription.service_instances: + if se.instance_name == SERVICE_INSTANCE_NAME: + service_instance = se + break + if not service_instance: + logger.info("******** Service Instance not existing: Instantiate *******") + # Instantiate service + service_instantiation = ServiceInstantiation.instantiate_so_ala_carte( + service, + cloud_region, + tenant, + customer, + owning_entity, + vid_project, + service_instance_name=SERVICE_INSTANCE_NAME + ) + time.sleep(60) + else: + logger.info("******** Service Instance already existing *******") + + service_instance = None + for se in service_subscription.service_instances: + if se.instance_name == SERVICE_INSTANCE_NAME: + service_instance = se + break + if not service_instance: + logger.error("******** Service %s instantiation failed",SERVICE_INSTANCE_NAME) + exit(1) + + nb_try = 0 + nb_try_max = 10 + service_active = False + while service_active is False and nb_try < nb_try_max: + if service_instance.orchestration_status == "Active": + logger.info("******** Service Instance %s is active *******",service_instance.name) + service_active = True + break + logger.info("Service %s instantiation not complete,Status:%s, wait 10s",service_instance.name,service_instance.orchestration_status) + time.sleep(10) + nb_try += 1 + + if service_active is False: + logger.error("Service %s instantiation failed",service_instance.name) + exit(1) + + + logger.info("******** Get VNFs in Service Model *******") + vnfs = service_instance.service_subscription.sdc_service.vnfs + + logger.info("******** Create VNFs *******") + for vnf in vnfs: + logger.debug("Check if VNF instance of class %s exist", vnf.name) + vnf_found = False + for vnf_instance in service_instance.vnf_instances: + logger.debug("VNF instance %s found in Service Instance ",vnf_instance.name) + vnf_found = True + if vnf_found is False: + vnf_instantiation = service_instance.add_vnf(vnf, vid_line_of_business, vid_platform) + while not vnf_instantiation.finished: + print("Wait for VNF %s instantiation",vnf.name) + time.sleep(10) + + + for vnf_instance in service_instance.vnf_instances: + logger.debug("VNF instance %s found in Service Instance ",vnf_instance.name) + logger.info("******** Get VfModules in VNF Model *******") + logger.info("******** Check VF Modules *******") + vf_module = vnf_instance.vnf.vf_module + + logger.info("******** Create VF Module %s *******",vf_module.name) + + vf_module_instantiation = vnf_instance.add_vf_module( + vf_module, + vnf_parameters=[ + VnfParameter(name="vfw_image_name", value="Ubuntu_1404"), + VnfParameter(name="vpg_image_name", value="Ubuntu_1404"), + VnfParameter(name="vsn_image_name", value="Ubuntu_1404"), + VnfParameter(name="vfw_flavor_name", value="m1.small"), + VnfParameter(name="vpg_flavor_name", value="m1.small"), + VnfParameter(name="vsn_flavor_name", value="m1.small"), + VnfParameter(name="public_net_id", value="admin"), + VnfParameter(name="onap_private_net_id", value="admin"), + VnfParameter(name="onap_private_subnet_id", value="admin-subnet"), + VnfParameter(name="onap_private_net_cidr", value="10.41.1.0/24"), + VnfParameter(name="vfw_onap_private_ip_0", value="10.41.1.10"), + VnfParameter(name="vpg_onap_private_ip_0", value="10.41.1.11"), + VnfParameter(name="vsn_onap_private_ip_0", value="10.41.1.12"), + VnfParameter(name="sec_group", value="ci-onap-master-vnfs-onap") + ] + ) + nb_try = 0 + nb_try_max = 30 + while not vf_module_instantiation.finished and nb_try < nb_try_max: + logger.info("Wait for vf module instantiation") + nb_try += 1 + time.sleep(10) + if vf_module_instantiation.finished: + logger.info("VfModule %s instantiated",vf_module.name) + else: + logger.error("VfModule instantiation %s failed",vf_module.name) + + if SERVICE_DELETION is False: + logger.info("*****************************************") + logger.info("**** No Deletion requested, finished ****") + logger.info("*****************************************") + exit(0) + + logger.info("*******************************") + logger.info("**** SERVICE DELETION *********") + logger.info("*******************************") + time.sleep(30) + + for vnf_instance in service_instance.vnf_instances: + logger.debug("VNF instance %s found in Service Instance ",vnf_instance.name) + logger.info("******** Get VF Modules *******") + for vf_module in vnf_instance.vf_modules: + logger.info("******** Delete VF Module %s *******",vf_module.name) + vf_module_deletion = vf_module.delete() + + nb_try = 0 + nb_try_max = 30 + while not vf_module_deletion.finished and nb_try < nb_try_max: + logger.info("Wait for vf module deletion") + nb_try += 1 + time.sleep(10) + if vf_module_deletion.finished: + logger.info("VfModule %s deleted",vf_module.name) + else: + logger.error("VfModule deletion %s failed",vf_module.name) + exit(1) + + logger.info("******** Delete VNF %s *******",vnf_instance.name) + vnf_deletion = vnf_instance.delete() + + nb_try = 0 + nb_try_max = 30 + while not vnf_deletion.finished and nb_try < nb_try_max: + logger.info("Wait for vnf deletion") + nb_try += 1 + time.sleep(10) + if vnf_deletion.finished: + logger.info("VNF %s deleted",vnf_instance.name) + else: + logger.error("VNF deletion %s failed",vnf_instance.name) + exit(1) + + logger.info("******** Delete Service %s *******",service_instance.name) + service_deletion = service_instance.delete() + + nb_try = 0 + nb_try_max = 30 + while not service_deletion.finished and nb_try < nb_try_max: + logger.info("Wait for Service deletion") + nb_try += 1 + time.sleep(10) + if service_deletion.finished: + logger.info("Service %s deleted",service_instance.name) + else: + logger.error("Service deletion %s failed",service_instance.name) + exit(1) + diff --git a/docs/examples/e2e_vfw_macro_instantiation.rst b/docs/examples/e2e_vfw_macro_instantiation.rst new file mode 100644 index 0000000..72f8167 --- /dev/null +++ b/docs/examples/e2e_vfw_macro_instantiation.rst @@ -0,0 +1,493 @@ +E2E Instantiation of vFW (macro) +################################ + + +.. code:: Python + + import logging + import time + import json + from uuid import uuid4 + from onapsdk.aai.aai_element import AaiElement + from onapsdk.aai.cloud_infrastructure import ( + CloudRegion, + Complex, + Tenant + ) + from onapsdk.aai.service_design_and_creation import ( + Service as AaiService + ) + from onapsdk.aai.business import ( + ServiceInstance, + VnfInstance, + VfModuleInstance, + ServiceSubscription, + Customer, + OwningEntity as AaiOwningEntity + ) + from onapsdk.so.instantiation import ( + ServiceInstantiation, + VnfInstantiation, + InstantiationParameter, + VnfParameters, + VfmoduleParameters + ) + from onapsdk.sdc.properties import Property + from onapsdk.sdc import SDC + from onapsdk.sdc.vendor import Vendor + from onapsdk.sdc.vsp import Vsp + from onapsdk.sdc.vf import Vf + from onapsdk.sdc.service import Service, ServiceInstantiationType + import onapsdk.constants as const + import os + from onapsdk.vid import LineOfBusiness, OwningEntity, Platform, Project + + from onapsdk.cds.blueprint import Blueprint + from onapsdk.cds.data_dictionary import DataDictionary, DataDictionarySet + + logger = logging.getLogger("") + logger.setLevel(logging.INFO) + logname = "./vfwcds.debug.log" + fh = logging.FileHandler(logname) + fh_formatter = logging.Formatter('%(asctime)s %(levelname)s %(lineno)d:%(filename)s(%(process)d) - %(message)s') + fh.setFormatter(fh_formatter) + logger.addHandler(fh) + + ########################################################################### + ######## CDS Design settings ############################################## + ######## vFW CDS Example ############################################## + ########################################################################### + # DDF Settings (dd files located in following location) + DDDIR = "resources/starter-dictionary" + DDFILE = "resources/my_dd.json" + + # CBA resources (location of base CBA file) + CBAFILE = "resources/vFWCDS/CBA/CBA.zip" + ARTIFACT_LABEL = "vnfcds" + ARTIFACT_NAME = "CBA_enriched.zip" + ARTIFACT_TYPE = "CONTROLLER_BLUEPRINT_ARCHIVE" + ARTIFACT_FILE_PATH = "resources/vFWCDS/CBA/CBA_enriched.zip" + SDNC_TEMPLATE_NAME = "vFW-CDS" + SDNC_TEMPLATE_VERSION = "1.0.0" + SDNC_ARTIFACT_NAME = "vnf" + + ########################################################################### + ######## Service Design settings ########################################## + ########################################################################### + VENDOR = "VNFVendor" + + # HEAT resources (location of zipped HEAT file) + VSPFILE = "resources/vFWCDS/HEAT/vFW/vFW.zip" + VSPNAME = "vfwcds_VS" + VFNAME = "vfwcds_VF" + SERVICENAME = "vfwcds_SERVICE" + + ########################################################################### + ######## Runtime preparation settings ##################################### + ########################################################################### + # Default Cloud + CLOUD_OWNER = "CloudOwner" + CLOUD_REGION = "RegionOne" + + GLOBAL_CUSTOMER_ID = "generic" + CLOUD_TYPE = "openstack" + CLOUD_VERSION = "pike" + VIM_USERNAME = <user> # FILL ME + VIM_PASSWORD = <password> # FILL ME + VIM_SERVICE_URL = "http://<vim-url>/v3" # FILL ME + TENANT_NAME = <tenant> # FILL ME + TENANT_SEC_GROUP = <sec-group> # FILL ME + COMPLEX_PHYSICAL_LOCATION_ID = "location" + COMPLEX_DATA_CENTER_CODE = "1234" + + + # common + OWNING_ENTITY = "Test-OE" + PROJECT = "Test-Project" + PLATFORM = "Test-Platform" + LINE_OF_BUSINESS = "Test-BusinessLine" + + SERVICE_DELETION = False + + ########################################################################### + ######## Service Instance attributes ###################################### + ########################################################################### + SERVICE_INSTANCE_NAME = "vFWCDS-Instance-1" + ONAP_PRIVATE_NET = "onap-oam" # FILL ME + ONAP_PRIVATE_SUBNET = "onap-oam-subnet" # FILL ME + PUBLIC_NET = "admin" # FILL ME + IMAGE_NAME = "Ubuntu_1604" # FILL ME + FLAVOR_NAME = "m1.small" # FILL ME + + logger.info("*******************************") + logger.info("********* CBA Creation ********") + logger.info("*******************************") + + logger.info("******** Load Data Dictionary *******") + mypath = os.path.dirname(os.path.realpath(__file__)) + myddpath = os.path.join(mypath, DDDIR) + myddfile = os.path.join(mypath, DDFILE) + + logger.info("path: %s", myddpath) + dd_set = DataDictionarySet() + for file in os.listdir(myddpath): + logger.info("file: %s", file) + if file.endswith(".json"): + with open(os.path.join(myddpath, file), "r") as dd_file: # type file + dd_json: dict = json.loads(dd_file.read()) + logger.info("DD: %s", dd_json) + dd_set.add(DataDictionary(dd_json)) + logger.info("DD Length: %d", dd_set.length) + dd_set.upload() + + logger.info("******** Open Blueprint *******") + cbafile = os.path.join(mypath, CBAFILE) + artifactfile = os.path.join(mypath, ARTIFACT_FILE_PATH) + + blueprint = Blueprint.load_from_file(cbafile) + enriched_blueprint = blueprint.enrich() # returns enriched blueprint object + enriched_blueprint.save(artifactfile) + + + logger.info("*******************************") + logger.info("******** SERVICE DESIGN *******") + logger.info("*******************************") + + logger.info("******** Onboard Vendor *******") + vendor = Vendor(name=VENDOR) + vendor.onboard() + + logger.info("******** Onboard VSP *******") + vspfile = os.path.join(mypath, VSPFILE) + vsp = Vsp(name=VSPNAME, vendor=vendor, package=open(vspfile, 'rb')) + vsp.onboard() + + + logger.info("******** Onboard VF *******") + vf = Vf(name=VFNAME) + vf.vsp = vsp + vf.create() + + if vf.status == const.DRAFT: + + logger.info("******** Extract Artifact Data *******") + data = open(artifactfile, 'rb').read() + + logger.info("******** Upload Artifact *******") + vf.add_deployment_artifact(artifact_type=ARTIFACT_TYPE, + artifact_name=ARTIFACT_NAME, + artifact_label=ARTIFACT_LABEL, + artifact=artifactfile) + + vf.onboard() + + svc = Service(name=SERVICENAME,instantiation_type=ServiceInstantiationType.MACRO) + svc.create() + + if svc.status == const.DRAFT: + svc.add_resource(vf) + + logger.info("******** Set SDNC properties for VF ********") + component = svc.get_component(vf) + prop = component.get_property("sdnc_model_version") + prop.value = SDNC_TEMPLATE_VERSION + prop = component.get_property("sdnc_artifact_name") + prop.value = SDNC_ARTIFACT_NAME + prop = component.get_property("sdnc_model_name") + prop.value = SDNC_TEMPLATE_NAME + prop = component.get_property("controller_actor") + prop.value = "CDS" + prop = component.get_property("skip_post_instantiation_configuration") + prop.value = False + + logger.info("******** Onboard Service *******") + svc.checkin() + svc.onboard() + + logger.info("******** Check Service Distribution *******") + distribution_completed = False + nb_try = 0 + nb_try_max = 10 + while distribution_completed is False and nb_try < nb_try_max: + distribution_completed = svc.distributed + if distribution_completed is True: + logger.info("Service Distribution for %s is sucessfully finished",svc.name) + break + logger.info("Service Distribution for %s ongoing, Wait for 60 s",svc.name) + time.sleep(60) + nb_try += 1 + + if distribution_completed is False: + logger.error("Service Distribution for %s failed !!",svc.name) + exit(1) + + logger.info("*******************************") + logger.info("***** RUNTIME PREPARATION *****") + logger.info("*******************************") + + logger.info("******** Create Complex *******") + cmplx = Complex.create( + physical_location_id=COMPLEX_PHYSICAL_LOCATION_ID, + data_center_code=COMPLEX_DATA_CENTER_CODE, + name=COMPLEX_PHYSICAL_LOCATION_ID + ) + + logger.info("******** Create CloudRegion *******") + cloud_region = CloudRegion.create( + cloud_owner=CLOUD_OWNER, + cloud_region_id=CLOUD_REGION, + orchestration_disabled=False, + in_maint=False, + cloud_type=CLOUD_TYPE, + cloud_zone="z1", + complex_name=COMPLEX_PHYSICAL_LOCATION_ID, + sriov_automation=False, + owner_defined_type="t1", + cloud_region_version=CLOUD_VERSION + ) + + logger.info("******** Link Complex to CloudRegion *******") + cloud_region.link_to_complex(cmplx) + + logger.info("******** Add ESR Info to CloudRegion *******") + cloud_region.add_esr_system_info( + esr_system_info_id=str(uuid4()), + user_name=VIM_USERNAME, + password=VIM_PASSWORD, + system_type="VIM", + service_url=VIM_SERVICE_URL, + cloud_domain="Default", + ssl_insecure=False, + system_status="active", + default_tenant=TENANT_NAME + ) + + logger.info("******** Register CloudRegion to MultiCloud *******") + cloud_region.register_to_multicloud() + + logger.info("******** Check MultiCloud Registration *******") + time.sleep(60) + tenant_found = False + availability_zone_found = False + registration_completed = False + nb_try = 0 + nb_try_max = 10 + while registration_completed is False and nb_try < nb_try_max: + for tenant in cloud_region.tenants: + logger.debug("Tenant %s found in %s_%s",tenant.name,cloud_region.cloud_owner,cloud_region.cloud_region_id) + tenant_found = True + for az in cloud_region.availability_zones: + logger.debug("A-Zone %s found",az.name) + availability_zone_found = True + if availability_zone_found and tenant_found: + registration_completed = True + if registration_completed is False: + time.sleep(60) + nb_try += 1 + + if registration_completed is False: + logger.error("Registration of Cloud %s_%s failed !!",cloud_region.cloud_owner,cloud_region.cloud_region_id) + exit(1) + else: + logger.info("Registration of Cloud %s_%s successful !!",cloud_region.cloud_owner,cloud_region.cloud_region_id) + + logger.info("*******************************") + logger.info("**** SERVICE INSTANTIATION ****") + logger.info("*******************************") + + logger.info("******** Create Customer *******") + customer = None + for found_customer in list(Customer.get_all()): + logger.debug("Customer %s found", found_customer.subscriber_name) + if found_customer.subscriber_name == GLOBAL_CUSTOMER_ID: + logger.info("Customer %s found", found_customer.subscriber_name) + customer = found_customer + break + if not customer: + customer = Customer.create(GLOBAL_CUSTOMER_ID,GLOBAL_CUSTOMER_ID, "INFRA") + + logger.info("******** Find Service in SDC *******") + service = None + services = Service.get_all() + for found_service in services: + logger.debug("Service %s is found, distribution %s",found_service.name, found_service.distribution_status) + if found_service.name == SERVICENAME: + logger.info("Found Service %s in SDC",found_service.name) + service = found_service + break + + if not service: + logger.error("Service %s not found in SDC",SERVICENAME) + exit(1) + + logger.info("******** Check Service Subscription *******") + service_subscription = None + for service_sub in customer.service_subscriptions: + logger.debug("Service subscription %s is found",service_sub.service_type) + if service_sub.service_type == SERVICENAME: + logger.info("Service %s subscribed",SERVICENAME) + service_subscription = service_sub + break + + if not service_subscription: + logger.info("******** Subscribe Service *******") + customer.subscribe_service(SERVICENAME) + + logger.info("******** Get Tenant *******") + cloud_region = CloudRegion(cloud_owner=CLOUD_OWNER, cloud_region_id=CLOUD_REGION, + orchestration_disabled=True, in_maint=False) + tenant = None + for found_tenant in cloud_region.tenants: + logger.debug("Tenant %s found in %s_%s",found_tenant.name,cloud_region.cloud_owner,cloud_region.cloud_region_id) + if found_tenant.name == TENANT_NAME: + logger.info("Found my Tenant %s",found_tenant.name) + tenant = found_tenant + break + + if not tenant: + logger.error("tenant %s not found",TENANT_NAME) + exit(1) + + logger.info("******** Connect Service to Tenant *******") + service_subscription = None + for service_sub in customer.service_subscriptions: + logger.debug("Service subscription %s is found",service_sub.service_type) + if service_sub.service_type == SERVICENAME: + logger.info("Service %s subscribed",SERVICENAME) + service_subscription = service_sub + break + + if not service_subscription: + logger.error("Service subscription %s is not found",SERVICENAME) + exit(1) + + service_subscription.link_to_cloud_region_and_tenant(cloud_region, tenant) + + logger.info("******** Add Business Objects (OE, P, Pl, LoB) in VID *******") + vid_owning_entity = OwningEntity.create(OWNING_ENTITY) + vid_project = Project.create(PROJECT) + vid_platform = Platform.create(PLATFORM) + vid_line_of_business = LineOfBusiness.create(LINE_OF_BUSINESS) + + logger.info("******** Add Owning Entity in AAI *******") + owning_entity = None + for oe in AaiOwningEntity.get_all(): + if oe.name == vid_owning_entity.name: + owning_entity = oe + break + if not owning_entity: + logger.info("******** Owning Entity not existing: create *******") + owning_entity = AaiOwningEntity.create(vid_owning_entity.name, str(uuid4())) + + ########################################################################### + ######## VFModule parameters ############################################## + ########################################################################### + vfm_base=[ + InstantiationParameter(name="sec_group", value=TENANT_SEC_GROUP), + InstantiationParameter(name="public_net_id", value=PUBLIC_NET) + ] + + vfm_vsn=[ + InstantiationParameter(name="sec_group", value=TENANT_SEC_GROUP), + InstantiationParameter(name="public_net_id", value=PUBLIC_NET) + ] + + vfm_vfw=[ + InstantiationParameter(name="sec_group", value=TENANT_SEC_GROUP), + InstantiationParameter(name="public_net_id", value=PUBLIC_NET) + ] + + vfm_vpkg=[ + InstantiationParameter(name="sec_group", value=TENANT_SEC_GROUP), + InstantiationParameter(name="public_net_id", value=PUBLIC_NET) + ] + + base_paras=VfmoduleParameters("base_template",vfm_base) + vpkg_paras=VfmoduleParameters("vpkg",vfm_vpkg) + vsn_paras=VfmoduleParameters("vsn",vfm_vsn) + vfw_paras=VfmoduleParameters("vfw",vfm_vfw) + + ########################################################################### + ######## VNF parameters ################################################### + ########################################################################### + + vnf_vfw=[ + InstantiationParameter(name="onap_private_net_id", value=ONAP_PRIVATE_NET), + InstantiationParameter(name="onap_private_subnet_id", value=ONAP_PRIVATE_SUBNET), + InstantiationParameter(name="pub_key", value="ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDFBOB1Ea2yej68aqIQw10kEsVf+rNoxT39qrV8JvvTK2yhkniQka1t2oD9h6DlXOLM3HJ6nBegWjOasJmIbminKZ6wvmxZrDVFJXp9Sn1gni0vtEnlDgH14shRUrFDYO0PYjXRHoj7QXZMYxtAdFSbzGuCsaTLcV/xchLBQmqZ4AGhMIiYMfJJF+Ygy0lbgcVmT+8DH7kUUt8SAdh2rRsYFwpKANnQJyPV1dBNuTcD0OW1hEOhXnwqH28tjfb7uHJzTyGZlTmwTs544teTNz5B9L4yT3XiCAlMcaLOBMfBTKRIse+NkiTb+tc60JNnEYR6MqZoqTea/w+YBQaIMcil"), + InstantiationParameter(name="image_name", value=IMAGE_NAME), + InstantiationParameter(name="flavor_name", value=FLAVOR_NAME), + InstantiationParameter(name="sec_group", value=TENANT_SEC_GROUP), + InstantiationParameter(name="install_script_version", value="1.4.0-SNAPSHOT"), + InstantiationParameter(name="demo_artifacts_version", value="1.4.0-SNAPSHOT"), + InstantiationParameter(name="cloud_env", value=CLOUD_TYPE), + InstantiationParameter(name="public_net_id", value=PUBLIC_NET), + InstantiationParameter(name="aic-cloud-region", value=CLOUD_REGION) + ] + + vnf_paras=VnfParameters("vfwcds_VF", vnf_vfw, + [base_paras, vpkg_paras, vsn_paras, vfw_paras]) + + # You must define for each VNF and its vFModule the parameters, + # otherwise they stay empty. + # The matching critera are: + # - VnfParameters.name must match VNF ModelInstanceName + # (see above "vfwcds_VF") + # - VfmoduleParameters.name must match substring in vfModule "instanceName" + # (e.g. "vfwcds_vf0..VfwcdsVf..vsn..module-1") + logger.info("******** Instantiate Service *******") + + service_instantiation = ServiceInstantiation.instantiate_macro( + service, + cloud_region, + tenant, + customer, + owning_entity, + vid_project, + vid_line_of_business, + vid_platform, + service_instance_name=SERVICE_INSTANCE_NAME, + vnf_parameters=[vnf_paras] + ) + + if service_instantiation.wait_for_finish(): + logger.info("Success") + else: + logger.error("Instantiation failed, check logs") + exit(1) + + service_instance = None + for se in service_subscription.service_instances: + if se.instance_name == SERVICE_INSTANCE_NAME: + service_instance = se + break + if not service_instance: + logger.error("******** Service %s instantiation failed",SERVICE_INSTANCE_NAME) + exit(1) + + if SERVICE_DELETION is False: + logger.info("*****************************************") + logger.info("**** No Deletion requested, finished ****") + logger.info("*****************************************") + exit(0) + + logger.info("*******************************") + logger.info("**** SERVICE DELETION *********") + logger.info("*******************************") + time.sleep(30) + + logger.info("******** Delete Service %s *******",service_instance.name) + service_deletion = service_instance.delete() + + nb_try = 0 + nb_try_max = 30 + while not service_deletion.finished and nb_try < nb_try_max: + logger.info("Wait for Service deletion") + nb_try += 1 + time.sleep(10) + if service_deletion.finished: + logger.info("Service %s deleted",service_instance.name) + else: + logger.error("Service deletion %s failed",service_instance.name) + exit(1) + diff --git a/docs/examples/examples.rst b/docs/examples/examples.rst new file mode 100644 index 0000000..f6a43e3 --- /dev/null +++ b/docs/examples/examples.rst @@ -0,0 +1,13 @@ +Real life script examples +######################### + +.. toctree:: + :maxdepth: 4 + + e2e_vfw_instantiation + e2e_closed_loop_instantiation + e2e_vfw_macro_instantiation + e2e_net_instantiation + e2e_artifact_upload + e2e_basicvm_nomulticloud_instantiation + k8s_plugin_usage diff --git a/docs/examples/k8s_plugin_usage.rst b/docs/examples/k8s_plugin_usage.rst new file mode 100644 index 0000000..562dd92 --- /dev/null +++ b/docs/examples/k8s_plugin_usage.rst @@ -0,0 +1,94 @@ +E2E msb k8s plugin usage +######################## + + +.. code:: Python + + import logging + import os + + from onapsdk.msb.k8s import ( + Definition, + Instance, + ConnectivityInfo) + + logger = logging.getLogger("") + logger.setLevel(logging.DEBUG) + fh = logging.StreamHandler() + fh_formatter = logging.Formatter('%(asctime)s %(levelname)s %(lineno)d:%(filename)s(%(process)d) - %(message)s') + fh.setFormatter(fh_formatter) + logger.addHandler(fh) + + RB_NAME = "test_definition" + RB_VERSION = "ver_1" + DEFINITION_ARTIFACT_PATH = "artifacts\\vault-consul-dev.tar.gz" # FILL ME + PROFILE_NAME = "test-profile" + PROFILE_NAMESPACE = "test" + PROFILE_K8S_VERSION = "1.0" + PROFILE_ARTIFACT_PATH = "artifacts\\profile.tar.gz" # FILL ME + CLOUD_REGION_ID = "k8s_region_test" # FILL ME + CLOUD_OWNER = "CloudOwner" + KUBECONFIG_PATH = "artifacts\\kubeconfig" # FILL ME + MYPATH = os.path.dirname(os.path.realpath(__file__)) + + ######## Create new Definition ############################################ + definition = Definition.create(RB_NAME, RB_VERSION) + + ######## Upload artifact for created definition ########################### + definition_artifact_file = os.path.join(MYPATH, DEFINITION_ARTIFACT_PATH) + definition.upload_artifact(open(definition_artifact_file, 'rb').read()) + + ######## Get one Definition ############################################### + check_definition = Definition.get_definition_by_name_version(RB_NAME, + RB_VERSION) + + ######## Get all Definitions ############################################## + definitions = list(Definition.get_all()) + + ######## Create profile for Definition #################################### + profile = definition.create_profile(PROFILE_NAME, + PROFILE_NAMESPACE, + PROFILE_K8S_VERSION) + + ######## Upload artifact for created profile ############################## + profile_artifact_file = os.path.join(MYPATH, PROFILE_ARTIFACT_PATH) + profile.upload_artifact(open(profile_artifact_file, 'rb').read()) + + ######## Get one Profile ################################################## + check_profile = definition.get_profile_by_name(PROFILE_NAME) + + ######## Get all Profiles ################################################# + profiles = list(definition.get_all_profiles()) + + ######## Create Connectivity Info ######################################### + kubeconfig_file = os.path.join(MYPATH, KUBECONFIG_PATH) + conninfo = ConnectivityInfo.create(CLOUD_REGION_ID, + CLOUD_OWNER, + open(kubeconfig_file, 'rb').read()) + + ######## Instantiate Profile ############################################## + instance = Instance.create(CLOUD_REGION_ID, + profile.profile_name, + definition.rb_name, + definition.rb_version) + + ######## Get Instance by ID ############################################### + check_instance = Instance.get_by_id(instance.instance_id) + + ######## Get all Instances ################################################ + instances = list(Instance.get_all()) + + ######## Delete Instance ################################################## + instance.delete() + + ######## Check instance deletion ########################################## + instances = list(Instance.get_all()) + + ######## Delete Connectivity Info ######################################### + conninfo.delete() + + ######## Delete Profile ################################################### + profile.delete() + + ######## Delete Definition ################################################ + definition.delete()
\ No newline at end of file diff --git a/docs/glossary.rst b/docs/glossary.rst new file mode 100644 index 0000000..2c95539 --- /dev/null +++ b/docs/glossary.rst @@ -0,0 +1,20 @@ +.. _glossary: + +Glossary +======== + +.. glossary:: + + Vendor + a vendor is the entity that provides the network function. + + VSP (Vendor Software Product) + a VSP is the package of the network function seen from ONAP Designer + + VF (Virtual Function) + a VF is the virtual function using the VSP. The VF will be used by + services. + + Service + a service is the model that can be instantiated. it's composed of one (or + several) Functions (VF or PNF). diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..a878cb8 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,34 @@ +.. _contents: + +ONAP SDK Documentation contents +=============================== + +.. toctree:: + :maxdepth: 2 + + description + usage/intro + usage/installation + usage/usage + examples/examples + development + + architecture + modules/modules + +.. include:: description.rst + +Indices and tables +================== + +.. only:: builder_html + + * :ref:`genindex` + * :ref:`modindex` + * :ref:`search` + * :ref:`glossary` + +.. only:: not builder_html + + * :ref:`modindex` + * :ref:`glossary` diff --git a/docs/modules/modules.rst b/docs/modules/modules.rst new file mode 100644 index 0000000..e66ce77 --- /dev/null +++ b/docs/modules/modules.rst @@ -0,0 +1,7 @@ +onapsdk +======= + +.. toctree:: + :maxdepth: 4 + + onapsdk diff --git a/docs/modules/onapsdk.aai.business.rst b/docs/modules/onapsdk.aai.business.rst new file mode 100644 index 0000000..1bec9ac --- /dev/null +++ b/docs/modules/onapsdk.aai.business.rst @@ -0,0 +1,109 @@ +onapsdk.aai.business package +============================ + +Submodules +---------- + +onapsdk.aai.business.customer module +------------------------------------ + +.. automodule:: onapsdk.aai.business.customer + :members: + :undoc-members: + :show-inheritance: + +onapsdk.aai.business.instance module +------------------------------------ + +.. automodule:: onapsdk.aai.business.instance + :members: + :undoc-members: + :show-inheritance: + +onapsdk.aai.business.line\_of\_business module +---------------------------------------------- + +.. automodule:: onapsdk.aai.business.line_of_business + :members: + :undoc-members: + :show-inheritance: + +onapsdk.aai.business.network module +----------------------------------- + +.. automodule:: onapsdk.aai.business.network + :members: + :undoc-members: + :show-inheritance: + +onapsdk.aai.business.owning\_entity module +------------------------------------------ + +.. automodule:: onapsdk.aai.business.owning_entity + :members: + :undoc-members: + :show-inheritance: + +onapsdk.aai.business.platform module +------------------------------------ + +.. automodule:: onapsdk.aai.business.platform + :members: + :undoc-members: + :show-inheritance: + +onapsdk.aai.business.pnf module +------------------------------- + +.. automodule:: onapsdk.aai.business.pnf + :members: + :undoc-members: + :show-inheritance: + +onapsdk.aai.business.project module +----------------------------------- + +.. automodule:: onapsdk.aai.business.project + :members: + :undoc-members: + :show-inheritance: + +onapsdk.aai.business.service module +----------------------------------- + +.. automodule:: onapsdk.aai.business.service + :members: + :undoc-members: + :show-inheritance: + +onapsdk.aai.business.sp\_partner module +--------------------------------------- + +.. automodule:: onapsdk.aai.business.sp_partner + :members: + :undoc-members: + :show-inheritance: + +onapsdk.aai.business.vf\_module module +-------------------------------------- + +.. automodule:: onapsdk.aai.business.vf_module + :members: + :undoc-members: + :show-inheritance: + +onapsdk.aai.business.vnf module +------------------------------- + +.. automodule:: onapsdk.aai.business.vnf + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: onapsdk.aai.business + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/modules/onapsdk.aai.cloud_infrastructure.rst b/docs/modules/onapsdk.aai.cloud_infrastructure.rst new file mode 100644 index 0000000..307676b --- /dev/null +++ b/docs/modules/onapsdk.aai.cloud_infrastructure.rst @@ -0,0 +1,37 @@ +onapsdk.aai.cloud\_infrastructure package +========================================= + +Submodules +---------- + +onapsdk.aai.cloud\_infrastructure.cloud\_region module +------------------------------------------------------ + +.. automodule:: onapsdk.aai.cloud_infrastructure.cloud_region + :members: + :undoc-members: + :show-inheritance: + +onapsdk.aai.cloud\_infrastructure.complex module +------------------------------------------------ + +.. automodule:: onapsdk.aai.cloud_infrastructure.complex + :members: + :undoc-members: + :show-inheritance: + +onapsdk.aai.cloud\_infrastructure.tenant module +----------------------------------------------- + +.. automodule:: onapsdk.aai.cloud_infrastructure.tenant + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: onapsdk.aai.cloud_infrastructure + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/modules/onapsdk.aai.rst b/docs/modules/onapsdk.aai.rst new file mode 100644 index 0000000..161e291 --- /dev/null +++ b/docs/modules/onapsdk.aai.rst @@ -0,0 +1,46 @@ +onapsdk.aai package +=================== + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + onapsdk.aai.business + onapsdk.aai.cloud_infrastructure + +Submodules +---------- + +onapsdk.aai.aai\_element module +------------------------------- + +.. automodule:: onapsdk.aai.aai_element + :members: + :undoc-members: + :show-inheritance: + +onapsdk.aai.bulk module +----------------------- + +.. automodule:: onapsdk.aai.bulk + :members: + :undoc-members: + :show-inheritance: + +onapsdk.aai.service\_design\_and\_creation module +------------------------------------------------- + +.. automodule:: onapsdk.aai.service_design_and_creation + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: onapsdk.aai + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/modules/onapsdk.cds.rst b/docs/modules/onapsdk.cds.rst new file mode 100644 index 0000000..c0b8d51 --- /dev/null +++ b/docs/modules/onapsdk.cds.rst @@ -0,0 +1,53 @@ +onapsdk.cds package +=================== + +Submodules +---------- + +onapsdk.cds.blueprint module +---------------------------- + +.. automodule:: onapsdk.cds.blueprint + :members: + :undoc-members: + :show-inheritance: + +onapsdk.cds.blueprint\_model module +----------------------------------- + +.. automodule:: onapsdk.cds.blueprint_model + :members: + :undoc-members: + :show-inheritance: + +onapsdk.cds.blueprint\_processor module +--------------------------------------- + +.. automodule:: onapsdk.cds.blueprint_processor + :members: + :undoc-members: + :show-inheritance: + +onapsdk.cds.cds\_element module +------------------------------- + +.. automodule:: onapsdk.cds.cds_element + :members: + :undoc-members: + :show-inheritance: + +onapsdk.cds.data\_dictionary module +----------------------------------- + +.. automodule:: onapsdk.cds.data_dictionary + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: onapsdk.cds + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/modules/onapsdk.clamp.rst b/docs/modules/onapsdk.clamp.rst new file mode 100644 index 0000000..65d9888 --- /dev/null +++ b/docs/modules/onapsdk.clamp.rst @@ -0,0 +1,29 @@ +onapsdk.clamp package +===================== + +Submodules +---------- + +onapsdk.clamp.clamp\_element module +----------------------------------- + +.. automodule:: onapsdk.clamp.clamp_element + :members: + :undoc-members: + :show-inheritance: + +onapsdk.clamp.loop\_instance module +----------------------------------- + +.. automodule:: onapsdk.clamp.loop_instance + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: onapsdk.clamp + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/modules/onapsdk.configuration.rst b/docs/modules/onapsdk.configuration.rst new file mode 100644 index 0000000..4833ad3 --- /dev/null +++ b/docs/modules/onapsdk.configuration.rst @@ -0,0 +1,29 @@ +onapsdk.configuration package +============================= + +Submodules +---------- + +onapsdk.configuration.global\_settings module +--------------------------------------------- + +.. automodule:: onapsdk.configuration.global_settings + :members: + :undoc-members: + :show-inheritance: + +onapsdk.configuration.loader module +----------------------------------- + +.. automodule:: onapsdk.configuration.loader + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: onapsdk.configuration + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/modules/onapsdk.cps.rst b/docs/modules/onapsdk.cps.rst new file mode 100644 index 0000000..a65d0d4 --- /dev/null +++ b/docs/modules/onapsdk.cps.rst @@ -0,0 +1,45 @@ +onapsdk.cps package +=================== + +Submodules +---------- + +onapsdk.cps.anchor module +------------------------- + +.. automodule:: onapsdk.cps.anchor + :members: + :undoc-members: + :show-inheritance: + +onapsdk.cps.cps\_element module +------------------------------- + +.. automodule:: onapsdk.cps.cps_element + :members: + :undoc-members: + :show-inheritance: + +onapsdk.cps.dataspace module +---------------------------- + +.. automodule:: onapsdk.cps.dataspace + :members: + :undoc-members: + :show-inheritance: + +onapsdk.cps.schemaset module +---------------------------- + +.. automodule:: onapsdk.cps.schemaset + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: onapsdk.cps + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/modules/onapsdk.dmaap.rst b/docs/modules/onapsdk.dmaap.rst new file mode 100644 index 0000000..dc17d2e --- /dev/null +++ b/docs/modules/onapsdk.dmaap.rst @@ -0,0 +1,29 @@ +onapsdk.dmaap package +===================== + +Submodules +---------- + +onapsdk.dmaap.dmaap module +-------------------------- + +.. automodule:: onapsdk.dmaap.dmaap + :members: + :undoc-members: + :show-inheritance: + +onapsdk.dmaap.dmaap\_service module +----------------------------------- + +.. automodule:: onapsdk.dmaap.dmaap_service + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: onapsdk.dmaap + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/modules/onapsdk.msb.k8s.rst b/docs/modules/onapsdk.msb.k8s.rst new file mode 100644 index 0000000..aaba76a --- /dev/null +++ b/docs/modules/onapsdk.msb.k8s.rst @@ -0,0 +1,37 @@ +onapsdk.msb.k8s package +======================= + +Submodules +---------- + +onapsdk.msb.k8s.connectivity\_info module +----------------------------------------- + +.. automodule:: onapsdk.msb.k8s.connectivity_info + :members: + :undoc-members: + :show-inheritance: + +onapsdk.msb.k8s.definition module +--------------------------------- + +.. automodule:: onapsdk.msb.k8s.definition + :members: + :undoc-members: + :show-inheritance: + +onapsdk.msb.k8s.instance module +------------------------------- + +.. automodule:: onapsdk.msb.k8s.instance + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: onapsdk.msb.k8s + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/modules/onapsdk.msb.rst b/docs/modules/onapsdk.msb.rst new file mode 100644 index 0000000..eeea5b7 --- /dev/null +++ b/docs/modules/onapsdk.msb.rst @@ -0,0 +1,45 @@ +onapsdk.msb package +=================== + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + onapsdk.msb.k8s + +Submodules +---------- + +onapsdk.msb.esr module +---------------------- + +.. automodule:: onapsdk.msb.esr + :members: + :undoc-members: + :show-inheritance: + +onapsdk.msb.msb\_service module +------------------------------- + +.. automodule:: onapsdk.msb.msb_service + :members: + :undoc-members: + :show-inheritance: + +onapsdk.msb.multicloud module +----------------------------- + +.. automodule:: onapsdk.msb.multicloud + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: onapsdk.msb + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/modules/onapsdk.nbi.rst b/docs/modules/onapsdk.nbi.rst new file mode 100644 index 0000000..b0a68b5 --- /dev/null +++ b/docs/modules/onapsdk.nbi.rst @@ -0,0 +1,21 @@ +onapsdk.nbi package +=================== + +Submodules +---------- + +onapsdk.nbi.nbi module +---------------------- + +.. automodule:: onapsdk.nbi.nbi + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: onapsdk.nbi + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/modules/onapsdk.rst b/docs/modules/onapsdk.rst new file mode 100644 index 0000000..77e5575 --- /dev/null +++ b/docs/modules/onapsdk.rst @@ -0,0 +1,65 @@ +onapsdk package +=============== + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + onapsdk.aai + onapsdk.cds + onapsdk.clamp + onapsdk.configuration + onapsdk.dmaap + onapsdk.msb + onapsdk.nbi + onapsdk.sdc + onapsdk.sdnc + onapsdk.so + onapsdk.utils + onapsdk.ves + onapsdk.vid + +Submodules +---------- + +onapsdk.constants module +------------------------ + +.. automodule:: onapsdk.constants + :members: + :undoc-members: + :show-inheritance: + +onapsdk.exceptions module +------------------------- + +.. automodule:: onapsdk.exceptions + :members: + :undoc-members: + :show-inheritance: + +onapsdk.onap\_service module +---------------------------- + +.. automodule:: onapsdk.onap_service + :members: + :undoc-members: + :show-inheritance: + +onapsdk.version module +---------------------- + +.. automodule:: onapsdk.version + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: onapsdk + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/modules/onapsdk.sdc.rst b/docs/modules/onapsdk.sdc.rst new file mode 100644 index 0000000..5ba5ec3 --- /dev/null +++ b/docs/modules/onapsdk.sdc.rst @@ -0,0 +1,101 @@ +onapsdk.sdc package +=================== + +Submodules +---------- + +onapsdk.sdc.category\_management module +--------------------------------------- + +.. automodule:: onapsdk.sdc.category_management + :members: + :undoc-members: + :show-inheritance: + +onapsdk.sdc.component module +---------------------------- + +.. automodule:: onapsdk.sdc.component + :members: + :undoc-members: + :show-inheritance: + +onapsdk.sdc.pnf module +---------------------- + +.. automodule:: onapsdk.sdc.pnf + :members: + :undoc-members: + :show-inheritance: + +onapsdk.sdc.properties module +----------------------------- + +.. automodule:: onapsdk.sdc.properties + :members: + :undoc-members: + :show-inheritance: + +onapsdk.sdc.sdc\_element module +------------------------------- + +.. automodule:: onapsdk.sdc.sdc_element + :members: + :undoc-members: + :show-inheritance: + +onapsdk.sdc.sdc\_resource module +-------------------------------- + +.. automodule:: onapsdk.sdc.sdc_resource + :members: + :undoc-members: + :show-inheritance: + +onapsdk.sdc.service module +-------------------------- + +.. automodule:: onapsdk.sdc.service + :members: + :undoc-members: + :show-inheritance: + +onapsdk.sdc.vendor module +------------------------- + +.. automodule:: onapsdk.sdc.vendor + :members: + :undoc-members: + :show-inheritance: + +onapsdk.sdc.vf module +--------------------- + +.. automodule:: onapsdk.sdc.vf + :members: + :undoc-members: + :show-inheritance: + +onapsdk.sdc.vl module +--------------------- + +.. automodule:: onapsdk.sdc.vl + :members: + :undoc-members: + :show-inheritance: + +onapsdk.sdc.vsp module +---------------------- + +.. automodule:: onapsdk.sdc.vsp + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: onapsdk.sdc + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/modules/onapsdk.sdnc.rst b/docs/modules/onapsdk.sdnc.rst new file mode 100644 index 0000000..2a87163 --- /dev/null +++ b/docs/modules/onapsdk.sdnc.rst @@ -0,0 +1,29 @@ +onapsdk.sdnc package +==================== + +Submodules +---------- + +onapsdk.sdnc.preload module +--------------------------- + +.. automodule:: onapsdk.sdnc.preload + :members: + :undoc-members: + :show-inheritance: + +onapsdk.sdnc.sdnc\_element module +--------------------------------- + +.. automodule:: onapsdk.sdnc.sdnc_element + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: onapsdk.sdnc + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/modules/onapsdk.so.rst b/docs/modules/onapsdk.so.rst new file mode 100644 index 0000000..eae1eda --- /dev/null +++ b/docs/modules/onapsdk.so.rst @@ -0,0 +1,45 @@ +onapsdk.so package +================== + +Submodules +---------- + +onapsdk.so.deletion module +-------------------------- + +.. automodule:: onapsdk.so.deletion + :members: + :undoc-members: + :show-inheritance: + +onapsdk.so.instantiation module +------------------------------- + +.. automodule:: onapsdk.so.instantiation + :members: + :undoc-members: + :show-inheritance: + +onapsdk.so.so\_db\_adapter module +--------------------------------- + +.. automodule:: onapsdk.so.so_db_adapter + :members: + :undoc-members: + :show-inheritance: + +onapsdk.so.so\_element module +----------------------------- + +.. automodule:: onapsdk.so.so_element + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: onapsdk.so + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/modules/onapsdk.utils.rst b/docs/modules/onapsdk.utils.rst new file mode 100644 index 0000000..cbb4c73 --- /dev/null +++ b/docs/modules/onapsdk.utils.rst @@ -0,0 +1,53 @@ +onapsdk.utils package +===================== + +Submodules +---------- + +onapsdk.utils.configuration module +---------------------------------- + +.. automodule:: onapsdk.utils.configuration + :members: + :undoc-members: + :show-inheritance: + +onapsdk.utils.headers\_creator module +------------------------------------- + +.. automodule:: onapsdk.utils.headers_creator + :members: + :undoc-members: + :show-inheritance: + +onapsdk.utils.jinja module +-------------------------- + +.. automodule:: onapsdk.utils.jinja + :members: + :undoc-members: + :show-inheritance: + +onapsdk.utils.mixins module +--------------------------- + +.. automodule:: onapsdk.utils.mixins + :members: + :undoc-members: + :show-inheritance: + +onapsdk.utils.tosca\_file\_handler module +----------------------------------------- + +.. automodule:: onapsdk.utils.tosca_file_handler + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: onapsdk.utils + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/modules/onapsdk.ves.rst b/docs/modules/onapsdk.ves.rst new file mode 100644 index 0000000..51a4c6b --- /dev/null +++ b/docs/modules/onapsdk.ves.rst @@ -0,0 +1,29 @@ +onapsdk.ves package +=================== + +Submodules +---------- + +onapsdk.ves.ves module +---------------------- + +.. automodule:: onapsdk.ves.ves + :members: + :undoc-members: + :show-inheritance: + +onapsdk.ves.ves\_service module +------------------------------- + +.. automodule:: onapsdk.ves.ves_service + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: onapsdk.ves + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/modules/onapsdk.vid.rst b/docs/modules/onapsdk.vid.rst new file mode 100644 index 0000000..36a6705 --- /dev/null +++ b/docs/modules/onapsdk.vid.rst @@ -0,0 +1,21 @@ +onapsdk.vid package +=================== + +Submodules +---------- + +onapsdk.vid.vid module +---------------------- + +.. automodule:: onapsdk.vid.vid + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: onapsdk.vid + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/usage/installation.rst b/docs/usage/installation.rst new file mode 100644 index 0000000..c324c96 --- /dev/null +++ b/docs/usage/installation.rst @@ -0,0 +1,32 @@ +Installation +############ + + + +Installing with pip +-------------------- + +.. code:: shell + + $ pip install onapsdk + + +Customize the configuration +--------------------------- + +You can customize the global settings of onapsdk by creating an environment +variable ONAP_PYTHON_SDK_SETTINGS and a file my_settings.py. + +By default the global settings are retrieved from the file located in +src/onapsdk/configuration/global_settings.py. You can create your own customized +configuration file and reference it through the environement variable. +You can thus copy/paste the existing global_settings.py file, rename it as +my_settings.py, adapt it with your favorite editor and export the environnement +variable accordingly. + +It can be useful to move from a nodeport to an an ingress based configuration +or test different API versions. + + .. code:: shell + + $ export ONAP_PYTHON_SDK_SETTINGS="onapsdk.configuration.my_settings" diff --git a/docs/usage/intro.rst b/docs/usage/intro.rst new file mode 100644 index 0000000..7031c13 --- /dev/null +++ b/docs/usage/intro.rst @@ -0,0 +1,16 @@ +Introduction +############ + +It *should* be simple to use. +Once you have installed the Python module, few lines of code are needed to +onboard a Service: + +.. code:: Python + + from onapsdk.vf import Vf + from onapsdk.service import Service + + # We assume here that the VF has been already onboarded + vf = VF(name="myVF") + service = Service(name="myService", resources=[vf]) + service.onboard() diff --git a/docs/usage/usage.rst b/docs/usage/usage.rst new file mode 100644 index 0000000..4a1dc9d --- /dev/null +++ b/docs/usage/usage.rst @@ -0,0 +1,14 @@ +Usage +##### + +A minimum knowledge of ONAP is needed, especially on the onboarding part. + +.. toctree:: + :maxdepth: 2 + + usage/cloud_configuration + usage/design_time + usage/instantiation + usage/deletion + usage/cds + usage/cps diff --git a/docs/usage/usage/cds.rst b/docs/usage/usage/cds.rst new file mode 100644 index 0000000..b516c73 --- /dev/null +++ b/docs/usage/usage/cds.rst @@ -0,0 +1,173 @@ +CDS +### + +Preparation for CDS tests +------------------------- + +To enable CDS Enrichment in an ONAP Frankfurt environment the NodePort 30449 +for the CDS Blueprint Processor API service needs to be opened + +#. Check existing CDS Services: + + .. code-block:: sh + + ubuntu@control01:~$ kubectl get service -n onap|grep cds-blueprints-processor-http + cds-blueprints-processor-http ClusterIP 10.43.101.198 <none> 8080/TCP + +#. Change NodePort to CDS cds-blueprints-processor-http + + Add the "nodePort" under "ports" section + and change "type" from "ClusterIP" to "NodePort" + + .. code-block:: sh + + ubuntu@control01:~$ kubectl edit service cds-blueprints-processor-http -n onap + + apiVersion: v1 + kind: Service + metadata: + creationTimestamp: "2020-07-23T02:57:36Z" + labels: + app: cds-blueprints-processor + chart: cds-blueprints-processor-6.0.0 + heritage: Tiller + release: onap + name: cds-blueprints-processor-http + namespace: onap + resourceVersion: "10256" + selfLink: /api/v1/namespaces/onap/services/cds-blueprints-processor-http + uid: 6f065c03-4563-4d64-b6f5-a8892226c909 + spec: + clusterIP: 10.43.101.198 + ports: + - name: blueprints-processor-http + nodePort: 30449 -> add line + port: 8080 + protocol: TCP + targetPort: 8080 + selector: + app: cds-blueprints-processor + release: onap + sessionAffinity: None + type: ClusterIP -> change to NodePort + status: + loadBalancer: {} + +#. Verify NodePort to CDS cds-blueprints-processor-http + + .. code-block:: sh + + ubuntu@control01:~$ kubectl get service -n onap|grep cds-blueprints-processor-http + cds-blueprints-processor-http NodePort 10.43.101.198 <none> 8080:30449/TCP + +#. Load ModelType via Bootstrap + + .. code-block:: sh + + curl --location --request POST 'http://<k8s-host>:30449/api/v1/blueprint-model/bootstrap' \ + --header 'Content-Type: application/json' \ + --header 'Authorization: Basic Y2NzZGthcHBzOmNjc2RrYXBwcw==' \ + --data-raw '{ + "loadModelType" : true, + "loadResourceDictionary" : false, + "loadCBA" : false + }' + + +Load blueprint from file +------------------------ + +.. code:: Python + + from onapsdk.cds import Blueprint + blueprint = Blueprint.load_from_file("<< path to CBA file >>") + +Enrich blueprint and save +------------------------- + +.. code:: Python + + enriched_blueprint = blueprint.enrich() + enriched_blueprint.save("<< path to dest file >>") + +Publish blueprint +----------------- + +.. code:: Python + + enriched_blueprint.publish() + +Generate data dictionary from blueprint +--------------------------------------- + +The method to generate data dictionaries based on the blueprint mappings. As the result it returns a data dictionaries set +with valid structure, but some additional actions may be needed. Data dictionary input has to be filled by the user +if the type is neither "source-input" nor "source-default". Things, which are needed to be filled are marked by `<< FILL >>` mark. +If the blueprint you are using has only "source-input" or "source-default" input types, the generated data dictionary set is +ready to upload to CDS. + +.. code:: Python + + generated_dd: DataDictionarySet = blueprint.get_data_dictionaries() + generated_dd.save_to_file("<< path to dest file >>") + +Load data dictionary set from file +---------------------------------- + +.. code:: Python + + from onapsdk.cds import DataDictionarySet + dd_set = DataDictionarySet.load_from_file("<< path to dd file >>") + +Upload data dictionary set +-------------------------- + +.. code:: Python + + dd_set.upload() + +Retrieve Blueprint Models from CDS +-------------------------- + +#. All + +.. code:: Python + + from onapsdk.cds import BlueprintModel + all_blueprint_models = BlueprintModel.get_all() + +#. Selected by id of Blueprint Model + +.. code:: Python + + blueprint_model = BlueprintModel.get_by_id(blueprint_model_id='11111111-1111-1111-1111-111111111111') + +#. Selected by name and version of Blueprint Model + +.. code:: Python + + blueprint_model = BlueprintModel.get_by_name_and_version(blueprint_name='test_name', blueprint_version='1.0.0') + +Delete Blueprint Model +-------------------------- + +.. code:: Python + + blueprint_model.delete() + +Download Blueprint Model +-------------------------- + +.. code:: Python + + blueprint_model.save(dst_file_path='/tmp/blueprint.zip') + + +Get Blueprint object for Blueprint Model +-------------------------- + +After that, all operation for blueprint object, like execute blueprint workflow etc. can be executed. + +.. code:: Python + + blueprint = blueprint_model.get_blueprint() diff --git a/docs/usage/usage/cloud_configuration.rst b/docs/usage/usage/cloud_configuration.rst new file mode 100644 index 0000000..b048c0a --- /dev/null +++ b/docs/usage/usage/cloud_configuration.rst @@ -0,0 +1,210 @@ +Cloud configuration +################### + +Create a complex +---------------- + +.. code:: Python + + from onapsdk.aai.cloud_infrastructure import Complex + cmplx = Complex.create( + physical_location_id=COMPLEX_PHYSICAL_LOCATION_ID, + data_center_code=COMPLEX_DATA_CENTER_CODE, + name=COMPLEX_PHYSICAL_LOCATION_ID + ) + +Create cloud region +------------------- + +.. code:: Python + + from onapsdk.aai.cloud_infrastructure import CloudRegion + cloud_region = CloudRegion.create( + cloud_owner=CLOUD_OWNER, + cloud_region_id=CLOUD_REGION, + orchestration_disabled=False, + in_maint=False, + cloud_type=CLOUD_TYPE, + cloud_region_version=CLOUD_REGION_VERSION + ) + +Link cloud region to complex +---------------------------- + +.. code:: Python + + from onapsdk.aai.cloud_infrastructure import CloudRegion, Complex + # We assume that complex has been already created + cmplx = Complex( + physical_location_id=COMPLEX_PHYSICAL_LOCATION_ID, + name=COMPLEX_PHYSICAL_LOCATION_ID + ) + cloud_region = CloudRegion.create( + cloud_owner=CLOUD_OWNER, + cloud_region_id=CLOUD_REGION, + orchestration_disabled=False, + in_maint=False, + cloud_type=CLOUD_TYPE, + cloud_region_version=CLOUD_REGION_VERSION + ) + cloud_region.link_to_complex(cmplx) + +Add ESR Info to cloud region +---------------------------- + +.. code:: Python + + from uuid import uuid4 + from onapsdk.aai.cloud_infrastructure import CloudRegion + # We assume that cloud region has been already created + cloud_region = CloudRegion.get_by_id( + cloud_owner=CLOUD_OWNER, + cloud_region_id=CLOUD_REGION + ) + cloud_region.add_esr_system_info( + esr_system_info_id=str(uuid4()), + user_name=VIM_USERNAME, + password=VIM_PASSWORD, + system_type=CLOUD_TYPE, + service_url=VIM_SERVICE_URL, + cloud_domain=CLOUD_DOMAIN + ) + +Register cloud to MultiCloud +---------------------------- + +.. code:: Python + + from uuid import uuid4 + from onapsdk.aai.cloud_infrastructure import CloudRegion + # We assume that cloud region has been already created + cloud_region = CloudRegion.get_by_id( + cloud_owner=CLOUD_OWNER, + cloud_region_id=CLOUD_REGION + ) + cloud_region.add_esr_system_info( + esr_system_info_id=str(uuid4()), + user_name=VIM_USERNAME, + password=VIM_PASSWORD, + system_type=CLOUD_TYPE, + service_url=VIM_SERVICE_URL, + cloud_domain=CLOUD_DOMAIN + ) + cloud_region.register_to_multicloud() + +Get cloud region tenant +----------------------- + +.. code:: Python + + # We assume that cloud region has been already created + # and connected to multicloud + from onapsdk.aai.cloud_infrastructure import CloudRegion + cloud_region = CloudRegion.get_by_id( + cloud_owner=CLOUD_OWNER, + cloud_region_id=CLOUD_REGION + ) + try: + tenant = next(cloud_region.tenant) + except StopIteration + # No Tenant found in cloud region + +Create customer +--------------- + +.. code:: Python + + from onapsdk.aai.business import Customer + customer = Customer.create(GLOBAL_CUSTOMER_ID, GLOBAL_CUSTOMER_ID, "INFRA") + +Create customer service subscription +------------------------------------ + +.. code:: Python + + # We assume here that the service has been already onboarded + # and customer created + from onapsdk.aai.business import Customer + + customer = Customer.get_by_global_customer_id(GLOBAL_CUSTOMER_ID) + customer.subscribe_service("service_type") + + # Service subscriptions can be also created during Customer + # creation + from onapsdk.aai.business import Customer + + customer = Customer.create(GLOBAL_CUSTOMER_ID, GLOBAL_CUSTOMER_ID, "INFRA", service_subscriptions=["service_type"]) + +Connect service subscription to cloud region and tenant +------------------------------------------------------- + +.. code:: Python + + # We assume here that the service subscription has been already created + # and cloud region has a tenant + from onapsdk.aai.business import Customer + from onapsdk.aai.cloud_infrastructure import CloudRegion, Tenant + + customer = Customer.get_by_global_customer_id(GLOBAL_CUSTOMER_ID) + service_subscription = next(customer.service_subscriptions) + cloud_region = CloudRegion.get_by_id( + cloud_owner=CLOUD_OWNER, + cloud_region_id=CLOUD_REGION + ) + tenant = next(cloud_region.tenants) + service_subscription.link_to_cloud_region_and_tenant(cloud_region, tenant) + +Add Cloud SIte entry to SO Catalog DB +------------------------------------------------------- + +.. code:: Python + + from onapsdk.so.so_db_adapter import IdentityService, SoDbAdapter + + identity_service = IdentityService(identity_id="mc_test_identity_1_KEYSTONE", + url="http://test:5000/v3", + mso_id="test_user", + mso_pass="test_password_encrypted", + roject_domain_name="Default", + user_domain_name="Default", + identity_server_type="KEYSTONE_V3") + response = SoDbAdapter.add_cloud_site(cloud_region_id="test_region_1", + complex_id="test_clli_1", + identity_service=identity_service, + orchestrator="NULL") + +Use A&AI bulk API (experimental) +-------------------------------- + +.. code:: Python + + from onapsdk.aai.bulk import AaiBulk, AaiBulkRequest + from onapsdk.aai.cloud_infrastructure.cloud_region import CloudRegion + from onapsdk.utils.jinja import jinja_env + + + for resp in AaiBulk.single_transaction( + [ + AaiBulkRequest( + action="put", + uri=f"/cloud-infrastructure/cloud-regions/cloud-region/aai_bulk_test_cloud_owner_1/aai_bulk_test_cloud_region_id_1", + body=jinja_env().get_template("cloud_region_create.json.j2").render(cloud_region=CloudRegion( + cloud_owner="aai_bulk_test_cloud_owner_1", + cloud_region_id="aai_bulk_test_cloud_region_id_1", + orchestration_disabled=False, + in_maint=False + )) + ), + AaiBulkRequest( + action="put", + uri=f"/cloud-infrastructure/cloud-regions/cloud-region/aai_bulk_test_cloud_owner_2/aai_bulk_test_cloud_region_id_2", + body=jinja_env().get_template("cloud_region_create.json.j2").render(cloud_region=CloudRegion( + cloud_owner="aai_bulk_test_cloud_owner_2", + cloud_region_id="aai_bulk_test_cloud_region_id_2", + orchestration_disabled=False, + in_maint=False + )) + ) + ] + ): + print(resp) diff --git a/docs/usage/usage/cps.rst b/docs/usage/usage/cps.rst new file mode 100644 index 0000000..0a19b29 --- /dev/null +++ b/docs/usage/usage/cps.rst @@ -0,0 +1,32 @@ +CPS +### + +Create dataspace +---------------- + +.. code:: Python + + from onapsdk.cps import Dataspace + dataspace: Dataspace = Dataspace.create(dataspace_name="test_dataspace") + + +Create schema set +---------------- + +.. code:: Python + + from onapsdk.cps import Dataspace, SchemaSet + dataspace: Dataspace = Dataspace(name="test_dataspace") + with Path("schema_set_zip_file.zip").open("rb") as zip_file: + schema_set: SchemaSet = dataspace.create_schema_set("test_schemaset", zip_file) + + +Create anchor +------------- + +.. code:: Python + + from onapsdk.cps import Anchor, Dataspace, SchemaSet + dataspace: Dataspace = Dataspace(name="test_dataspace") + schema_set: SchemaSet = dataspace.get_schema_set("test_schemaset") + anchor: Anchor = dataspace.create_anchor(schema_set, "test_anchor") diff --git a/docs/usage/usage/deletion.rst b/docs/usage/usage/deletion.rst new file mode 100644 index 0000000..c5e5025 --- /dev/null +++ b/docs/usage/usage/deletion.rst @@ -0,0 +1,28 @@ +Instantiated resources deletion +############################### + +Service, vnf and vf module deletion +----------------------------------- + +.. code:: Python + + from onapsdk.aai.business import Customer + + customer = Customer.get_by_global_customer_id(GLOBAL_CUSTOMER_ID) + service_subscription = next(customer.service_subscriptions) + service_instance = service_subscription.get_service_instance_by_name(SERVICE_INSTANCE_NAME) + for vnf_instance in service_instance.vnf_instances: + for vf_module_instance in vnf_instance.vf_modules: + vf_module_deletion_request = vf_module_instance.delete() + while not vf_module_deletion.finished: + time.sleep(10) + + vnf_instance_deletion_request = vnf_instance.delete() + while not vnf_instance_deletion_request.finished: + time.sleep(10) + + service_instance_deletion_request = service_instance.delete() + if service_instance_deletion_request.wait_for_finish(): + print("Service instance deleted") + else: + print("Service deletion failed, check logs" diff --git a/docs/usage/usage/design_time.rst b/docs/usage/usage/design_time.rst new file mode 100644 index 0000000..5990258 --- /dev/null +++ b/docs/usage/usage/design_time.rst @@ -0,0 +1,351 @@ +Design time +########### + +Onboard a Vendor +---------------- + +.. code:: Python + + from onapsdk.vendor import Vendor + vendor = Vendor(name="myVendor") + vendor.onboard() + +Onboard a VSP +------------- + +You will need the package of the VSP to onboard. + +.. code:: Python + + from onapsdk.sdc.vendor import Vendor + from onapsdk.sdc.vsp import Vsp + + # We assume here that the Vendor has been already onboarded + vendor = Vendor(name="myVendor") + vendor.onboard() + vsp = Vsp(name="myVSP", vendor=vendor, package=open(PATH_TO_PACKAGE, 'rb')) + vsp.onboard() + +Create new VSP version +---------------------- + +You will need the package of the VSP to update. + +.. code:: Python + + from onapsdk.sdc.vendor import Vendor + from onapsdk.sdc.vsp import Vsp + + # We assume here that the Vendor has been already onboarded + vsp = Vsp(name="myVSP") + vsp.create_new_version() + vsp.update_package(open(PATH_TO_PACKAGE, 'rb')) + vsp.onboard() + +Onboard a VF +------------ + +.. code:: Python + + from onapsdk.sdc.vsp import Vsp + from onapsdk.sdc.vf import Vf + + # We assume here that the VSP has been already onboarded + vsp = Vsp(name="myVSP") + vf = Vf(name="myVF", vsp=vsp) + vf.onboard() + +Onboard a VF with properties assignement +---------------------------------------- + +.. code:: Python + + from onapsdk.sdc.properties import Property + from onapsdk.sdc.vsp import Vsp + from onapsdk.sdc.vf import Vf + + # We assume here that the VSP has been already onboarded + vsp = Vsp(name="myVSP") + property_1 = Property( + name="prop1", + property_type="string", + value="test" + ) + property_2 = Property( + name="prop2", + property_type="integer" + ) + vf = Vf(name="myVF", + vsp=vsp, + properties=[ + property_1, + property_2 + ], + inputs=[property_1]) + vf.onboard() + +Onboard a VF with Deployment Artifact +------------------------------------- + +.. code:: Python + + from onapsdk.sdc.properties import Property + from onapsdk.sdc.vsp import Vsp + from onapsdk.sdc.vf import Vf + + logger.info("******** Onboard Vendor *******") + vendor = Vendor(name="my_Vendor") + vendor.onboard() + + # We assume here that the VSP has been already onboarded + vsp = Vsp(name="myVSP") + + logger.info("******** Onboard VF *******") + vf = Vf(name="myVF") + vf.vsp = vsp + vf.create() + + logger.info("******** Upload Artifact *******") + vf.add_deployment_artifact(artifact_type="CONTROLLER_BLUEPRINT_ARCHIVE", + artifact_name="CBA.zip", + artifact_label="vfwcds", + artifact="dir/CBA_enriched.zip") + + vf.onboard() + +Onboard a VF with it's component's property input +------------------------------------------------- + +.. code:: Python + + from onapsdk.sdc.properties import ComponentProperty + from onapsdk.sdc.vsp import Vsp + from onapsdk.sdc.vf import Vf + from onapsdk.sdc.vfc import Vfc + + # We assume here that the VSP has been already onboarded + vsp = Vsp(name="myVSP") + + vfc = Vfc(name="AllottedResource") + + logger.info("******** Onboard VF *******") + vf = Vf(name="myVF") + vf.vsp = vsp + vf.create() + vf.add_resource(vfc) + vfc_comp = vf.get_component(vfc) + comp_prop = vfc_comp.get_property("min_instances") + comp_prop.value = 11 + vf.declare_input(comp_prop) + + vf.onboard() + +Onboard a PNF with VSP +---------------------- +.. code:: Python + + from onapsdk.sdc.pnf import Pnf + from onapsdk.sdc.vendor import Vendor + + logger.info("******** Onboard Vendor *******") + vendor = Vendor(name="my_Vendor") + vendor.onboard() + + # We assume here that the VSP has been already onboarded + vsp = Vsp(name="myVSP") + + logger.info("******** Onboard PNF *******") + pnf = PNF(name="myPNF") + pnf.vsp = vsp + pnf.onboard() + +Onboard a PNF with Deployment Artifact (without VSP) +---------------------------------------------------- +.. code:: Python + + from onapsdk.sdc.vendor import Vendor + from onapsdk.sdc.pnf import Pnf + + logger.info("******** Onboard Vendor *******") + vendor = Vendor(name="my_Vendor") + vendor.onboard() + + logger.info("******** Onboard PNF *******") + pnf = Pnf(name=PNF, vendor=vendor) + pnf.create() + + logger.info("******** Upload Artifact *******") + pnf.add_deployment_artifact(artifact_type=ARTIFACT_TYPE, + artifact_name=ARTIFACT_NAME, + artifact_label=ARTIFACT_LABEL, + artifact=ARTIFACT_FILE_PATH) + pnf.onboard() + +Onboard a Service +----------------- + +.. code:: Python + + from onapsdk.sdc.vf import Vf + from onapsdk.sdc.service import Service + + # We assume here that the VF has been already onboarded + vf = Vf(name="myVF") + service = Service(name="myService", resources=[vf]) + service.onboard() + +Onboard a Service with properties assignement +--------------------------------------------- + +.. code:: Python + + from onapsdk.sdc.properties import Property + from onapsdk.sdc.vf import Vf + from onapsdk.sdc.service import Service + + # We assume here that the VF has been already onboarded + vf = Vf(name="myVF") + property_1 = Property( + name="prop1", + property_type="string", + value="test" + ) + property_2 = Property( + name="prop2", + property_type="integer", + declare_input=True + ) + service = Service(name="myService", + resources=[vf], + properties=[ + property_1, + property_2 + ], + inputs=[property_1]) + service.onboard() + +Onboard a Service with Nested inputs +------------------------------------ + +.. code:: Python + + from onapsdk.sdc.properties import NestedInput + from onapsdk.sdc.vf import Vf + from onapsdk.sdc.service import Service + + # We assume here that the VF has been already onboarded + vf = Vf(name="myVF") + inp = vf.get_input("input_name_we_want_to_declare_in_service") + service = Service(name="myService", + resources=[vf], + inputs=[NestedInput(vf, inp)]) + service.onboard() + +Onboard a Service with VL +------------------------- + +.. code:: Python + + from onapsdk.sdc.vl import VL + from onapsdk.sdc.service import Service + + # No VF needed, but you need to be sure that Vl with given + # name exists in SDC + vl = Vl(name="Generic NeutronNet") + service = Service(name="myServiceWithVl", resources=[vl]) + service.onboard() + +Onboard a Service with custom category +-------------------------------------- + +.. code:: Python + + from onapsdk.sdc.category_management import ServiceCategory + from onapsdk.sdc.vf import Vf + from onapsdk.sdc.service import Service + + # Let's create a custom category + CATEGORY_NAME = "Python ONAP SDK category" + ServiceCategory.create(name=CATEGORY_NAME) + + # We assume here that the VF has been already onboarded + # Create a service with category we created few lines above + vf = Vf(name="myVF") + service = Service(name="myService", resources=[vf], category=CATEGORY_NAME) + service.onboard() + +Onboard an Artifact for an embedded VF +-------------------------------------- + +All SDC artifact types are supported + +.. code:: Python + + from onapsdk.service import Service + + # We assume here that the Service has been already onboarded + # with a Vnf + service = Service(name="myService") + # We load artifact data + data = open("{}/myArtifact.yaml".format(os.path.dirname(os.path.abspath(__file__))), 'rb').read() + # We add the artifact to the service Vnf + # + svc.add_artifact_to_vf(vnf_name="myVnf", + artifact_type="DCAE_INVENTORY_BLUEPRINT", + artifact_name="myArtifact.yaml", + artifact=data) + +Onboard a Service with Deployment Artifact +------------------------------------------ + +.. code:: Python + + from onapsdk.sdc.service import Service + + svc = Service(name="myService") + + logger.info("******** Upload Artifact *******") + svc.add_deployment_artifact(artifact_type="OTHER", + artifact_name="eMBB.zip", + artifact_label="embbcn", + artifact="dir/eMBB.zip") + + svc.onboard() + +Onboard a Service with a CBA blueprint for Macro Instantiation +-------------------------------------------------------------- + +.. code:: Python + + from onapsdk.sdc.service import Service, ServiceInstantiationType + + # Set CBA variables and artifact level + # Must match to values in the CBA TOSCA.meta file + SDNC_TEMPLATE_NAME = "vFW-CDS" + SDNC_TEMPLATE_VERSION = "1.0.0" + SDNC_ARTIFACT_NAME = "vnf" + + svc = Service(name="myService", + instantiation_type=ServiceInstantiationType.MACRO) + + svc.create() + + logger.info("*** add a VF, which includes a CBA blueprint ***") + svc.add_resource(vf) + + logger.info("******** Set SDNC properties for VF ********") + component = svc.get_component(vf) + prop = component.get_property("sdnc_model_version") + prop.value = SDNC_TEMPLATE_NAME + prop = component.get_property("sdnc_artifact_name") + prop.value = SDNC_ARTIFACT_NAME + prop = component.get_property("sdnc_model_name") + prop.value = SDNC_TEMPLATE_NAME + prop = component.get_property("controller_actor") + prop.value = "CDS" + prop = component.get_property("skip_post_instantiation_configuration") + prop.value = False + + logger.info("******** Onboard Service *******") + svc.checkin() + svc.onboard() diff --git a/docs/usage/usage/dmaap.rst b/docs/usage/usage/dmaap.rst new file mode 100644 index 0000000..ee62d04 --- /dev/null +++ b/docs/usage/usage/dmaap.rst @@ -0,0 +1,47 @@ +VES +### + +Preparation for DMAAP tests +------------------------- + +#. Check existing DMaap Services: + + .. code-block:: sh + + kubectl get service -n onap| grep mess + message-router NodePort 10.43.30.205 <none> 3905:31163/TCP,3904:32404/TCP + +#. If the port of Ves Service is different than 3904 you can change it corresponding to the installation instruction. + + +Remove all events from DMaap +--------------------------- + +.. code:: Python + + from onapsdk.dmaap.dmaap import Dmaap + response = Dmaap.reset_events() + +Get all events from DMaap +------------------------- + +.. code:: Python + + from onapsdk.dmaap.dmaap import Dmaap + response = Dmaap.get_all_events() + +Get events from specific topic from DMaap +------------------------- + +.. code:: Python + + from onapsdk.dmaap.dmaap import Dmaap + response = Dmaap.get_events_for_topic("fault") + +Get all topics from DMaap +------------------------- + +.. code:: Python + + from onapsdk.dmaap.dmaap import Dmaap + response = Dmaap.get_all_topics()
\ No newline at end of file diff --git a/docs/usage/usage/instantiation.rst b/docs/usage/usage/instantiation.rst new file mode 100644 index 0000000..642439e --- /dev/null +++ b/docs/usage/usage/instantiation.rst @@ -0,0 +1,485 @@ +Instantiation +############# + +Create business objects +----------------------- + +.. code:: Python + + from onapsdk.vid import LineOfBusiness, OwningEntity, Platform, Project + + vid_owning_entity = OwningEntity.create(OWNING_ENTITY) + vid_project = Project.create(PROJECT) + vid_platform = Platform.create(PLATFORM) + vid_line_of_business = LineOfBusiness.create(LINE_OF_BUSINESS) + +Instantiate a service (ALaCarte) +-------------------------------- + +.. code:: Python + + import time + from onapsdk.aai.cloud_infrastructure import CloudRegion, Tenant + from onapsdk.aai.business import Customer + from onapsdk.service import Service + from onapsdk.vid import LineOfBusiness, OwningEntity, Platform, Project + from onapsdk.so.instantiation import ServiceInstantiation + + # We assume that: + # - service is onboarded, + # - cloud region, customer, owning_entity and project have been already created, + # - cloud region has at least one tenant + # - customer has service subscription + # - service subscription is connected with cloud region and tenant + SERVICE_INSTANCE_NAME = "vFW-AlaCarte-1" + + service = Service(name="myService") + customer = Customer.get_by_global_customer_id(GLOBAL_CUSTOMER_ID) + cloud_region = CloudRegion.get_by_id( + cloud_owner=CLOUD_OWNER, + cloud_region_id=CLOUD_REGION + ) + tenant = next(cloud_region.tenants) + vid_owning_entity = OwningEntity(OWNING_ENTITY) + owning_entity = AaiOwningEntity.get_by_owning_entity_name(OWNING_ENTITY) + vid_project = Project(PROJECT) + + service_instantiation = ServiceInstantiation.instantiate_so_ala_carte( + service, + cloud_region, + tenant, + customer, + owning_entity, + vid_project, + service_instance_name=SERVICE_INSTANCE_NAME + ) + service_instantiation.wait_for_finish(): + print("Success") + else: + print("Instantiation failed, check logs") + +Instantiate a service (Macro) +----------------------------- + +.. code:: Python + + import time + from onapsdk.aai.cloud_infrastructure import CloudRegion, Tenant + from onapsdk.aai.business import Customer + from onapsdk.service import Service + from onapsdk.vid import LineOfBusiness, OwningEntity, Platform, Project + from onapsdk.so.instantiation import ( + ServiceInstantiation, + VnfInstantiation, + InstantiationParameter, + VnfParameters, + VfmoduleParameters + ) + + ... + VSPNAME = "vfwcds_VS" + VFNAME = "vfwcds_VF" + ... + vf = Vf(name=VFNAME) + ... + + # We assume that: + # - service is onboarded, + # - cloud region, customer, owning_entity and project have been already created, + # - cloud region has at least one tenant + # - customer has service subscription + # - service subscription is connected with cloud region and tenant + SERVICE_INSTANCE_NAME = "vFW-Macro-1" + + service = Service(name="myMacroService") + customer = Customer.get_by_global_customer_id(GLOBAL_CUSTOMER_ID) + cloud_region = CloudRegion.get_by_id( + cloud_owner=CLOUD_OWNER, + cloud_region_id=CLOUD_REGION + ) + tenant = next(cloud_region.tenants) + vid_owning_entity = OwningEntity(OWNING_ENTITY) + owning_entity = AaiOwningEntity.get_by_owning_entity_name(OWNING_ENTITY) + vid_project = Project(PROJECT) + + ########################################################################### + ######## VFModule parameters ############################################## + ########################################################################### + vfm_base=[ + InstantiationParameter(name="sec_group", value=TENANT_SEC_GROUP), + InstantiationParameter(name="public_net_id", value=PUBLIC_NET) + ] + + vfm_vsn=[ + InstantiationParameter(name="sec_group", value=TENANT_SEC_GROUP), + InstantiationParameter(name="public_net_id", value=PUBLIC_NET) + ] + + vfm_vfw=[ + InstantiationParameter(name="sec_group", value=TENANT_SEC_GROUP), + InstantiationParameter(name="public_net_id", value=PUBLIC_NET) + ] + + vfm_vpkg=[ + InstantiationParameter(name="sec_group", value=TENANT_SEC_GROUP), + InstantiationParameter(name="public_net_id", value=PUBLIC_NET) + ] + + base_paras=VfmoduleParameters("base_template",vfm_base) + vpkg_paras=VfmoduleParameters("vpkg",vfm_vpkg) + vsn_paras=VfmoduleParameters("vsn",vfm_vsn) + vfw_paras=VfmoduleParameters("vfw",vfm_vfw) + + ########################################################################### + ######## VNF parameters ################################################### + ########################################################################### + + vnf_vfw=[ + InstantiationParameter(name="onap_private_net_id", value=ONAP_PRIVATE_NET), + InstantiationParameter(name="onap_private_subnet_id", value=ONAP_PRIVATE_SUBNET), + InstantiationParameter(name="pub_key", value="ssh-rsa AAAAB3NzaC1yc2EAA\ + AADAQABAAABAQDFBOB1Ea2yej68aqIQw10kEsVf+rNoxT39qrV8JvvTK2yhkniQka1t2oD9h6DlXOL\ + M3HJ6nBegWjOasJmIbminKZ6wvmxZrDVFJXp9Sn1gni0vtEnlDgH14shRUrFDYO0PYjXRHoj7QXZMY\ + xtAdFSbzGuCsaTLcV/xchLBQmqZ4AGhMIiYMfJJF+Ygy0lbgcVmT+8DH7kUUt8SAdh2rRsYFwpKANn\ + QJyPV1dBNuTcD0OW1hEOhXnwqH28tjfb7uHJzTyGZlTmwTs544teTNz5B9L4yT3XiCAlMcaLOBMfBT\ + KRIse+NkiTb+tc60JNnEYR6MqZoqTea/w+YBQaIMcil"), + InstantiationParameter(name="image_name", value=IMAGE_NAME), + InstantiationParameter(name="flavor_name", value=FLAVOR_NAME), + InstantiationParameter(name="sec_group", value=TENANT_SEC_GROUP), + InstantiationParameter(name="install_script_version", value="1.4.0-SNAPSHOT"), + InstantiationParameter(name="demo_artifacts_version", value="1.4.0-SNAPSHOT"), + InstantiationParameter(name="cloud_env", value=CLOUD_TYPE), + InstantiationParameter(name="public_net_id", value=PUBLIC_NET), + InstantiationParameter(name="aic-cloud-region", value=CLOUD_REGION) + ] + + vnf_paras=VnfParameters("vfwcds_VF", vnf_vfw, + [base_paras, vpkg_paras, vsn_paras, vfw_paras]) + + # You must define for each VNF and its vFModule the parameters, + # otherwise they stay empty. + # The matching critera are: + # - VnfParameters.name must match VNF ModelInstanceName + # (see above "vfwcds_VF") + # - VfmoduleParameters.name must match substring in vfModule "instanceName" + # (e.g. "vfwcds_vf0..VfwcdsVf..vsn..module-1") + + service_instantiation = ServiceInstantiation.instantiate_macro( + service, + cloud_region, + tenant, + customer, + owning_entity, + vid_project, + vid_line_of_business, + vid_platform, + service_instance_name=SERVICE_INSTANCE_NAME, + vnf_parameters=[vnf_paras] + ) + + service_instantiation.wait_for_finish(): + print("Success") + else: + print("Instantiation failed, check logs") + +Instantiate a service using SO service template (Macro) +------------------------------------------------------- + +To provide more control on the SO macro instantiation, you can define your service as follows: + +.. code:: Yaml + + myservice: + subscription_service_type: myservice + vnfs: + - model_name: myvfmodel + instance_name: myfirstvnf + parameters: + param1: value1 + processing_priority: 1 + vf_modules: + - instance_name: mysecondvfm + model_name: base + processing_priority: 2 + parameters: + param-vfm1: value-vfm1 + - instance_name: myfirstvfm + model_name: base + processing_priority: 1 + parameters: + param-vfm1: value-vfm1 + - model_name: myvfmodel + instance_name: mysecondvnf + parameters: + param1: value1 + processing_priority: 2 + vf_modules: + - instance_name: myfirstvfm + model_name: base + processing_priority: 1 + parameters: + param-vfm1: value-vfm1 + - instance_name: mysecondvfm + model_name: base + processing_priority: 2 + parameters: + param-vfm1: value-vfm1 + +.. code:: Python + + from onapsdk.aai.business import Customer, OwningEntity, Project, LineOfBusiness, Platform + from onapsdk.aai.cloud_infrastructure import CloudRegion + from onapsdk.sdc.service import Service + from onapsdk.so.instantiation import ServiceInstantiation + from yaml import load + + so_yaml_service = "/path/to/yaml/service" + with open(so_yaml_service, "r") as yaml_template: + so_service_data = load(yaml_template) + + # We assume that: + # - service is onboarded, + # - cloud region, customer, owning_entity and project have been already created, + # - cloud region has at least one tenant + # - customer has service subscription + # - service subscription is connected with cloud region and tenant + + service = Service(next(iter(so_service_data.keys()))) + so_service = SoService.load(so_service_data[service.name]) + SERVICE_INSTANCE_NAME = "my_svc_instance_name" + + customer = Customer.get_by_global_customer_id(GLOBAL_CUSTOMER_ID) + cloud_region = CloudRegion.get_by_id( + cloud_owner=CLOUD_OWNER, + cloud_region_id=CLOUD_REGION + ) + tenant = next(cloud_region.tenants) + owning_entity = OwningEntity(OWNING_ENTITY) + project = Project(PROJECT) + line_of_business = LineOfBusiness(LINE_OF_BUSINESS) + platform = Platform(PLATFORM) + + service_instantiation = ServiceInstantiation.instantiate_macro( + sdc_service=service, + customer=customer, + owning_entity=owning_entity, + project=project, + line_of_business=line_of_business, + platform=platform, + cloud_region=cloud_region, + tenant=tenant, + service_instance_name=SERVICE_INSTANCE_NAME, + so_service=so_service + ) + + +Instantiate VNF (Macro) +--------------- + +Since ONAP Istanbul the creation or deletion of VNFs in macro mode is supported. Examples below: + +.. code:: Python + + import time + from onapsdk.aai.business import Customer + from onapsdk.vid import LineOfBusiness, Platform + + # We assume that + # - service has been already instantiated, + # - line of business and platform are created + + SERVICE_INSTANCE_NAME = "service_instance_demo" + VNF_INSTANCE_NAME = "new_vnf_instance" + + customer = Customer.get_by_global_customer_id(GLOBAL_CUSTOMER_ID) + service_subscription = next(customer.service_subscriptions) + service_instance = service_subscription.get_service_instance_by_name(SERVICE_INSTANCE_NAME) + vnf = service_subscription.sdc_service.vnfs[0] + vid_line_of_business = LineOfBusiness.create(LINE_OF_BUSINESS) + vid_platform = Platform.create(PLATFORM) + + ########################################################################### + ######## VFModule parameters ############################################## + ########################################################################### + + myfirstvfm_params = [ + InstantiationParameter(name="param-vfm1", value="value-vfm1") + ] + + vf1_params = VfmoduleParameters("myfirstvfm", myfirstvfm_params) + + ########################################################################### + ######## VNF parameters ################################################### + ########################################################################### + + vnf_param_list = [ + InstantiationParameter(name="param1", value="value1") + ] + + vnf_paras = VnfParameters("myvfmodel", vnf_param_list, [vf1_params]) + + vnf_instantiation = service_instance.add_vnf( + vnf=vnf, + line_of_business=vid_line_of_business, + platform=vid_platform, + vnf_instance_name=VNF_INSTANCE_NAME, + vnf_parameters=[vnf_paras], + a_la_carte=False + ) + + vnf_instantiation.wait_for_finish(): + print("Success") + else: + print("Instantiation failed, check logs") + + +Instantiate VNF using SO service template (Macro) +--------------- + +To provide more control on the SO macro instantiation for new vnf, you can define your vnf as follows: + +.. code:: Yaml + + model_name: myvfmodel + instance_name: mynewvnf + parameters: + param1: value1 + vf_modules: + - instance_name: mysecondvfm + model_name: base + processing_priority: 2 + parameters: + param-vfm1: value-vfm1 + - instance_name: myfirstvfm + model_name: base + processing_priority: 1 + parameters: + param-vfm1: value-vfm1 + +.. code:: Python + + import time + from onapsdk.aai.business import Customer + from onapsdk.vid import LineOfBusiness, Platform + + SERVICE_INSTANCE_NAME = "service_instance_demo" + VNF_INSTANCE_NAME = "new_vnf_instance" + + # We assume that + # - service has been already instantiated, + # - line of business and platform are created + + customer = Customer.get_by_global_customer_id(GLOBAL_CUSTOMER_ID) + service_subscription = next(customer.service_subscriptions) + service_instance = service_subscription.get_service_instance_by_name(SERVICE_INSTANCE_NAME) + vnf = service_subscription.sdc_service.vnfs[0] + vid_line_of_business = LineOfBusiness.create(LINE_OF_BUSINESS) + vid_platform = Platform.create(PLATFORM) + + so_yaml_vnf = "/path/to/yaml/vnf" + with open(so_yaml_vnf, "r") as yaml_template: + so_vnf_data = load(yaml_template) + + so_vnf = SoServiceVnf.load(so_vnf_data) + + vnf_instantiation = service_instance.add_vnf( + vnf=vnf, + line_of_business=vid_line_of_business, + platform=vid_platform, + vnf_instance_name=VNF_INSTANCE_NAME, + so_vnfs=so_vnfs, + a_la_carte=False + ) + + vnf_instantiation.wait_for_finish(): + print("Success") + else: + print("Instantiation failed, check logs") + + +Instantiate VNF (ALaCarte) +--------------- + +.. code:: Python + + import time + from onapsdk.aai.business import Customer + from onapsdk.vid import LineOfBusiness, Platform + + # We assume that + # - service has been already instantiated, + # - line of business and platform are created + + customer = Customer.get_by_global_customer_id(GLOBAL_CUSTOMER_ID) + service_subscription = next(customer.service_subscriptions) + service_instance = service_subscription.get_service_instance_by_name(SERVICE_INSTANCE_NAME) + vnf = service_subscription.sdc_service.vnfs[0] + vid_line_of_business = LineOfBusiness.create(LINE_OF_BUSINESS) + vid_platform = Platform.create(PLATFORM) + vnf_instantiation = service_instance.add_vnf(vnf, vid_line_of_business, vid_platform) + vnf_instantiation.wait_for_finish(): + print("Success") + else: + print("Instantiation failed, check logs") + +Instantiate Vf module (ALaCarte) +--------------------- + +.. code:: Python + + import time + from onapsdk.aai.business import Customer + + customer = Customer.get_by_global_customer_id(GLOBAL_CUSTOMER_ID) + service_subscription = next(customer.service_subscriptions) + service_instance = service_subscription.get_service_instance_by_name(SERVICE_INSTANCE_NAME) + vnf_instance = next(service_instance.vnf_instances) + vf_module = vnf_instance.vnf.vf_module + vf_module_instantiation = vnf_instance.add_vf_module( + vf_module, + vnf_parameters=[ + VnfParameter(name="parameter1", value="parameter1_value"), + VnfParameter(name="parameter2", value="parameter2_value + ] + ) + vf_module_instantiation.wait_for_finish(): + print("Success") + else: + print("Instantiation failed, check logs") + +Instantiate Vl module (ALaCarte) +--------------------- + +.. code:: Python + + import time + from onapsdk.aai.business import Customer + from onapsdk.vid import LineOfBusiness, Platform + + # We assume that + # - service has been already instantiated, + # - line of business and platform are created + + customer = Customer.get_by_global_customer_id(GLOBAL_CUSTOMER_ID) + service_subscription = next(customer.service_subscriptions) + service_instance = service_subscription.get_service_instance_by_name(SERVICE_INSTANCE_NAME) + + logger.info("******** Get 1st Network in Service Model *******") + network = service_subscription.sdc_service.networks[0] + + logger.info("******** Create Network *******") + sn=Subnet(name="my_subnet", + start_address="10.0.0.1", + cidr_mask="24", + gateway_address="10.0.0.1) + + vid_line_of_business = LineOfBusiness.create(LINE_OF_BUSINESS) + vid_platform = Platform.create(PLATFORM) + + network_instantiation = service_instance.add_network(network, vid_line_of_business, + vid_platform, network_instance_name="my_net", subnets=[sn]) + + if network_instantiation.wait_for_finish(): + print("Success") + else: + print("Instantiation failed, check logs") diff --git a/docs/usage/usage/ves.rst b/docs/usage/usage/ves.rst new file mode 100644 index 0000000..4712d8b --- /dev/null +++ b/docs/usage/usage/ves.rst @@ -0,0 +1,42 @@ +VES +### + +Preparation for VES tests +------------------------- + +To enable CDS Enrichment in an ONAP Frankfurt environment the NodePort 30449 +for the CDS Blueprint Processor API service needs to be opened + +#. Check existing VES Services: + + .. code-block:: sh + + kubectl get service -n onap|grep ves + xdcae-ves-collector NodePort 10.43.48.246 <none> 8443:30417/TCP + +#. If the port of Ves Service is different than 30417 you can change it corresponding to the installation instruction. + + +Send event to Ves Collector +--------------------------- + +.. code:: Python + + from onapsdk.ves.ves import Ves + response = Ves.send_event( + basic_auth={'username': 'sample1', 'password': 'sample1'}, + json_event=event, + version="v7" + ) + +Send event batch to Ves Collector +------------------------- + +.. code:: Python + + from onapsdk.ves.ves import Ves + response = Ves.send_batch_event( + basic_auth={'username': 'sample1', 'password': 'sample1'}, + json_event=event, + version="v7" + ) diff --git a/integration_tests/__init__.py b/integration_tests/__init__.py new file mode 100644 index 0000000..d985dca --- /dev/null +++ b/integration_tests/__init__.py @@ -0,0 +1,21 @@ +"""Main test module.""" +# Copyright 2022 Orange, 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 + +logging.basicConfig( + level=logging.DEBUG, + format='%(asctime)s - %(name)-23s - %(levelname)-8s - %(message)s') +logging.captureWarnings(True) diff --git a/integration_tests/docker-compose.yml b/integration_tests/docker-compose.yml new file mode 100644 index 0000000..6e92625 --- /dev/null +++ b/integration_tests/docker-compose.yml @@ -0,0 +1,53 @@ +version: "3.4" +services: + sdc.api.fe.simpledemo.onap.org: + image: registry.gitlab.com/orange-opensource/lfn/onap/mock_servers/mock-sdc:develop + networks: + sdk_integration: + ipv4_address: 172.20.0.2 + aai.api.sparky.simpledemo.onap.org: + image: registry.gitlab.com/orange-opensource/lfn/onap/mock_servers/mock-aai + networks: + sdk_integration: + ipv4_address: 172.20.0.3 + so.api.simpledemo.onap.org: + image: registry.gitlab.com/orange-opensource/lfn/onap/mock_servers/mock-so + networks: + sdk_integration: + ipv4_address: 172.20.0.4 + sdnc.api.simpledemo.onap.org: + image: registry.gitlab.com/orange-opensource/lfn/onap/mock_servers/mock-sdnc + networks: + sdk_integration: + ipv4_address: 172.20.0.5 + clamp.api.simpledemo.onap.org: + image: registry.gitlab.com/orange-opensource/lfn/onap/mock_servers/mock-clamp:develop + networks: + sdk_integration: + ipv4_address: 172.20.0.6 + cds.api.simpledemo.onap.org: + image: registry.gitlab.com/orange-opensource/lfn/onap/mock_servers/mock-cds + networks: + sdk_integration: + ipv4_address: 172.20.0.7 + msb.k8s.api.simpledemo.onap.org: + image: registry.gitlab.com/orange-opensource/lfn/onap/mock_servers/mock-msb-k8s + networks: + sdk_integration: + ipv4_address: 172.20.0.8 + ves.api.simpledemo.onap.org: + image: registry.gitlab.com/orange-opensource/lfn/onap/mock_servers/mock-dcae/mock-ves + networks: + sdk_integration: + ipv4_address: 172.20.0.9 + dmaap.api.simpledemo.onap.org: + image: registry.gitlab.com/orange-opensource/lfn/onap/mock_servers/mock-dmaap + networks: + sdk_integration: + ipv4_address: 172.20.0.10 + +networks: + sdk_integration: + ipam: + config: + - subnet: 172.20.0.0/24
\ No newline at end of file diff --git a/integration_tests/local_urls.py b/integration_tests/local_urls.py new file mode 100644 index 0000000..9b73a67 --- /dev/null +++ b/integration_tests/local_urls.py @@ -0,0 +1,23 @@ +# Copyright 2022 Orange, 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. +SDC_BE_URL = "http://172.20.0.2:30206" +SDC_FE_URL = "http://172.20.0.2:30206" +AAI_URL = "http://172.20.0.3:5000" +SO_URL = "http://172.20.0.4:5001" +SDNC_URL = "http://172.20.0.5:5002" +CLAMP_URL = "http://172.20.0.6:30258" +CDS_URL = "http://172.20.0.7:8080" +MSB_URL = "http://172.20.0.8:5003" +VES_URL = "http://172.20.0.9:30417" +DMAAP_URL = "http://172.20.0.10:3904" diff --git a/integration_tests/tca_clampnode.yaml b/integration_tests/tca_clampnode.yaml new file mode 100644 index 0000000..bd682b0 --- /dev/null +++ b/integration_tests/tca_clampnode.yaml @@ -0,0 +1,171 @@ +# +# ============LICENSE_START==================================================== +# ============================================================================= +# Copyright (c) 2019 AT&T Intellectual Property. All rights reserved. +# ============================================================================= +# 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. +# ============LICENSE_END====================================================== + +tosca_definitions_version: cloudify_dsl_1_3 + +description: > + This blueprint deploys/manages the TCA module as a Docker container + +imports: + - https://www.getcloudify.org/spec/cloudify/4.5.5/types.yaml + - "https://nexus.onap.org/service/local/repositories/raw/content/org.onap.dcaegen2.platform.plugins/R6/k8splugin/1.7.2/k8splugin_types.yaml" + - "https://nexus.onap.org/service/local/repositories/raw/content/org.onap.dcaegen2.platform.plugins/R6/clamppolicyplugin/1.1.0/clamppolicyplugin_types.yaml" +inputs: + aaiEnrichmentHost: + type: string + default: "aai.onap.svc.cluster.local" + aaiEnrichmentPort: + type: string + default: "8443" + enableAAIEnrichment: + type: string + default: "true" + dmaap_host: + type: string + default: "message-router.onap.svc.cluster.local" + dmaap_port: + type: string + default: "3904" + enableRedisCaching: + type: string + default: "false" + redisHosts: + type: string + default: "dcae-redis.onap.svc.cluster.local:6379" + tag_version: + type: string + default: "nexus3.onap.org:10001/onap/org.onap.dcaegen2.deployments.tca-cdap-container:1.2.2" + consul_host: + type: string + default: "consul-server.onap" + consul_port: + type: string + default: "8500" + cbs_host: + type: string + default: "config-binding-service" + cbs_port: + type: string + default: "10000" + policy_id: + type: string + default: "onap.restart.tca" + external_port: + type: string + description: Kubernetes node port on which CDAPgui is exposed + default: "32012" + policy_model_id: + type: string + default: "onap.policies.monitoring.cdap.tca.hi.lo.app" +node_templates: + tca_k8s: + type: dcae.nodes.ContainerizedServiceComponent + relationships: + - target: tca.policy + type: cloudify.relationships.depends_on + properties: + service_component_type: 'dcaegen2-analytics-tca' + application_config: {} + docker_config: {} + image: + get_input: tag_version + log_info: + log_directory: "/opt/app/TCAnalytics/logs" + application_config: + app_config: + appDescription: DCAE Analytics Threshold Crossing Alert Application + appName: dcae-tca + tcaAlertsAbatementTableName: TCAAlertsAbatementTable + tcaAlertsAbatementTableTTLSeconds: '1728000' + tcaSubscriberOutputStreamName: TCASubscriberOutputStream + tcaVESAlertsTableName: TCAVESAlertsTable + tcaVESAlertsTableTTLSeconds: '1728000' + tcaVESMessageStatusTableName: TCAVESMessageStatusTable + tcaVESMessageStatusTableTTLSeconds: '86400' + thresholdCalculatorFlowletInstances: '2' + app_preferences: + aaiEnrichmentHost: + get_input: aaiEnrichmentHost + aaiEnrichmentIgnoreSSLCertificateErrors: 'true' + aaiEnrichmentPortNumber: '8443' + aaiEnrichmentProtocol: https + aaiEnrichmentUserName: dcae@dcae.onap.org + aaiEnrichmentUserPassword: demo123456! + aaiVMEnrichmentAPIPath: /aai/v11/search/nodes-query + aaiVNFEnrichmentAPIPath: /aai/v11/network/generic-vnfs/generic-vnf + enableAAIEnrichment: + get_input: enableAAIEnrichment + enableRedisCaching: + get_input: enableRedisCaching + redisHosts: + get_input: redisHosts + enableAlertCEFFormat: 'false' + publisherContentType: application/json + publisherHostName: + get_input: dmaap_host + publisherHostPort: + get_input: dmaap_port + publisherMaxBatchSize: '1' + publisherMaxRecoveryQueueSize: '100000' + publisherPollingInterval: '20000' + publisherProtocol: http + publisherTopicName: unauthenticated.DCAE_CL_OUTPUT + subscriberConsumerGroup: OpenDCAE-clamp + subscriberConsumerId: c12 + subscriberContentType: application/json + subscriberHostName: + get_input: dmaap_host + subscriberHostPort: + get_input: dmaap_port + subscriberMessageLimit: '-1' + subscriberPollingInterval: '30000' + subscriberProtocol: http + subscriberTimeoutMS: '-1' + subscriberTopicName: unauthenticated.VES_MEASUREMENT_OUTPUT + #tca.policy: '{"domain":"measurementsForVfScaling","metricsPerEventName":[{"eventName":"vFirewallBroadcastPackets","controlLoopSchemaType":"VM","policyScope":"DCAE","policyName":"DCAE.Config_tca-hi-lo","policyVersion":"v0.0.1","thresholds":[{"closedLoopControlName":"ControlLoop-vFirewall-d0a1dfc6-94f5-4fd4-a5b5-4630b438850a","version":"1.0.2","fieldPath":"$.event.measurementsForVfScalingFields.vNicPerformanceArray[*].receivedTotalPacketsDelta","thresholdValue":300,"direction":"LESS_OR_EQUAL","severity":"MAJOR","closedLoopEventStatus":"ONSET"},{"closedLoopControlName":"ControlLoop-vFirewall-d0a1dfc6-94f5-4fd4-a5b5-4630b438850a","version":"1.0.2","fieldPath":"$.event.measurementsForVfScalingFields.vNicPerformanceArray[*].receivedTotalPacketsDelta","thresholdValue":700,"direction":"GREATER_OR_EQUAL","severity":"CRITICAL","closedLoopEventStatus":"ONSET"}]},{"eventName":"vLoadBalancer","controlLoopSchemaType":"VM","policyScope":"DCAE","policyName":"DCAE.Config_tca-hi-lo","policyVersion":"v0.0.1","thresholds":[{"closedLoopControlName":"ControlLoop-vDNS-6f37f56d-a87d-4b85-b6a9-cc953cf779b3","version":"1.0.2","fieldPath":"$.event.measurementsForVfScalingFields.vNicPerformanceArray[*].receivedTotalPacketsDelta","thresholdValue":300,"direction":"GREATER_OR_EQUAL","severity":"CRITICAL","closedLoopEventStatus":"ONSET"}]},{"eventName":"Measurement_vGMUX","controlLoopSchemaType":"VNF","policyScope":"DCAE","policyName":"DCAE.Config_tca-hi-lo","policyVersion":"v0.0.1","thresholds":[{"closedLoopControlName":"ControlLoop-vCPE-48f0c2c3-a172-4192-9ae3-052274181b6e","version":"1.0.2","fieldPath":"$.event.measurementsForVfScalingFields.additionalMeasurements[*].arrayOfFields[0].value","thresholdValue":0,"direction":"EQUAL","severity":"MAJOR","closedLoopEventStatus":"ABATED"},{"closedLoopControlName":"ControlLoop-vCPE-48f0c2c3-a172-4192-9ae3-052274181b6e","version":"1.0.2","fieldPath":"$.event.measurementsForVfScalingFields.additionalMeasurements[*].arrayOfFields[0].value","thresholdValue":0,"direction":"GREATER","severity":"CRITICAL","closedLoopEventStatus":"ONSET"}]}]}' + tca.policy: '' + service_component_type: dcaegen2-analytics_tca + interfaces: + cloudify.interfaces.lifecycle: + start: + inputs: + envs: + DMAAPHOST: + { get_input: dmaap_host } + DMAAPPORT: "3904" + DMAAPPUBTOPIC: "unauthenticated.DCAE_CL_OUTPUT" + DMAAPSUBTOPIC: "unauthenticated.VES_MEASUREMENT_OUTPUT" + AAIHOST: + { get_input: aaiEnrichmentHost } + AAIPORT: "8443" + CONSUL_HOST: + { get_input: consul_host } + CONSUL_PORT: "8500" + CBS_HOST: + { get_input: cbs_host } + CBS_PORT: "10000" + CONFIG_BINDING_SERVICE: "config_binding_service" + ports: + - concat: ["11011:", { get_input: external_port }] + tca.policy: + type: clamp.nodes.policy + properties: + policy_id: + get_input: policy_id + policy_model_id: + get_input: policy_model_id diff --git a/integration_tests/test_01_vendor.py b/integration_tests/test_01_vendor.py new file mode 100644 index 0000000..26ede03 --- /dev/null +++ b/integration_tests/test_01_vendor.py @@ -0,0 +1,42 @@ +"""Integration test Vendor module.""" +# Copyright 2022 Orange, 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 pytest + +import requests + +from onapsdk.sdc import SDC +from onapsdk.sdc.vendor import Vendor +import onapsdk.constants as const + + +@pytest.mark.integration +def test_vendor_unknown(): + """Integration tests for Vendor.""" + response = requests.post("{}/reset".format(SDC.base_front_url)) + response.raise_for_status() + vendor = Vendor(name="test") + vendor.create() + assert vendor.created() + vendor.submit() + assert vendor.status == const.CERTIFIED + +@pytest.mark.integration +def test_vendor_onboard_unknown(): + """Integration tests for Vendor.""" + response = requests.post("{}/reset".format(SDC.base_front_url)) + response.raise_for_status() + vendor = Vendor(name="test") + vendor.onboard() + assert vendor.status == const.CERTIFIED diff --git a/integration_tests/test_02_vsp.py b/integration_tests/test_02_vsp.py new file mode 100644 index 0000000..972e78e --- /dev/null +++ b/integration_tests/test_02_vsp.py @@ -0,0 +1,63 @@ +"""Integration test Vendor module.""" +# Copyright 2022 Orange, 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 os + +import pytest + +import requests + +from onapsdk.sdc import SDC +from onapsdk.sdc.vendor import Vendor +from onapsdk.sdc.vsp import Vsp +import onapsdk.constants as const + + +@pytest.mark.integration +def test_vsp_unknown(): + """Integration tests for Vsp.""" + response = requests.post("{}/reset".format(Vendor.base_front_url)) + response.raise_for_status() + vendor = Vendor(name="test") + vendor.onboard() + vsp = Vsp(name="test") + vsp.vendor = vendor + vsp.create() + assert vsp.identifier is not None + assert vsp.status == const.DRAFT + vsp.upload_package(open("{}/ubuntu16.zip".format( + os.path.dirname(os.path.abspath(__file__))), 'rb')) + assert vsp.status == const.UPLOADED + vsp.validate() + assert vsp.status == const.VALIDATED + vsp.commit() + assert vsp.status == const.COMMITED + vsp.submit() + assert vsp.status == const.CERTIFIED + vsp.create_csar() + assert vsp.csar_uuid is not None + +@pytest.mark.integration +def test_vsp_onboard_unknown(): + """Integration tests for Vsp.""" + response = requests.post("{}/reset".format(Vendor.base_front_url)) + response.raise_for_status() + vendor = Vendor(name="test") + vendor.onboard() + vsp = Vsp(name="test", package=open("{}/ubuntu16.zip".format( + os.path.dirname(os.path.abspath(__file__))), 'rb')) + vsp.vendor = vendor + vsp.onboard() + assert vsp.status == const.CERTIFIED + assert vsp.csar_uuid is not None diff --git a/integration_tests/test_03_vf.py b/integration_tests/test_03_vf.py new file mode 100644 index 0000000..7cad69b --- /dev/null +++ b/integration_tests/test_03_vf.py @@ -0,0 +1,90 @@ +"""Integration test Vendor module.""" +# Copyright 2022 Orange, 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 os + +import pytest + +import requests + +from onapsdk.sdc import SDC +from onapsdk.sdc.properties import Property +from onapsdk.sdc.vendor import Vendor +from onapsdk.sdc.vsp import Vsp +from onapsdk.sdc.vf import Vf +import onapsdk.constants as const + + +@pytest.mark.integration +def test_vf_unknown(): + """Integration tests for Vf.""" + response = requests.post("{}/reset".format(Vendor.base_front_url)) + response.raise_for_status() + vendor = Vendor(name="test") + vendor.onboard() + vsp = Vsp(name="test", package=open("{}/ubuntu16.zip".format( + os.path.dirname(os.path.abspath(__file__))), 'rb')) + vsp.vendor = vendor + vsp.onboard() + vf = Vf(name='test') + vf.vsp = vsp + vf.create() + assert vf.identifier is not None + assert vf.status == const.DRAFT + assert vf.version == "0.1" + vf.submit() + assert vsp.status == const.CERTIFIED + assert vf.version == "1.0" + vf.load() + assert vsp.status == const.CERTIFIED + assert vf.version == "1.0" + +@pytest.mark.integration +def test_vf_onboard_unknown(): + """Integration tests for Vf.""" + response = requests.post("{}/reset".format(Vendor.base_front_url)) + response.raise_for_status() + vendor = Vendor(name="test") + vendor.onboard() + vsp = Vsp(name="test", package=open("{}/ubuntu16.zip".format( + os.path.dirname(os.path.abspath(__file__))), 'rb')) + vsp.vendor = vendor + vsp.onboard() + vf = Vf(name='test') + vf.vsp = vsp + vf.onboard() + assert vsp.status == const.CERTIFIED + assert vf.version == "1.0" + +@pytest.mark.integration +def test_vf_properties(): + """Integration test to check properties assignment for Vf.""" + response = requests.post("{}/reset".format(Vendor.base_front_url)) + response.raise_for_status() + vendor = Vendor(name="test") + vendor.onboard() + vsp = Vsp(name="test", package=open("{}/ubuntu16.zip".format( + os.path.dirname(os.path.abspath(__file__))), 'rb')) + vsp.vendor = vendor + vsp.onboard() + prop = Property(name="test1", property_type="string", value="123") + vf = Vf(name="test", vsp=vsp, properties=[ + prop, + Property(name="test2", property_type="integer")], + inputs=[prop]) + vf.onboard() + vf_properties = list(vf.properties) + vf_inputs = list(vf.inputs) + assert len(vf_properties) == 2 + assert len(vf_inputs) == 1 diff --git a/integration_tests/test_04_service.py b/integration_tests/test_04_service.py new file mode 100644 index 0000000..80370b0 --- /dev/null +++ b/integration_tests/test_04_service.py @@ -0,0 +1,122 @@ +"""Integration test Vendor module.""" +# Copyright 2022 Orange, 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 os + +import pytest + +import requests + +from onapsdk.sdc import SDC +from onapsdk.sdc.properties import Property +from onapsdk.sdc.vendor import Vendor +from onapsdk.sdc.vsp import Vsp +from onapsdk.sdc.vf import Vf +from onapsdk.sdc.service import Service +import onapsdk.constants as const + + +@pytest.mark.integration +def test_service_unknown(): + """Integration tests for Service.""" + response = requests.post("{}/reset".format(SDC.base_front_url)) + response.raise_for_status() + vendor = Vendor(name="test") + vendor.onboard() + vsp = Vsp(name="test", package=open("{}/ubuntu16.zip".format( + os.path.dirname(os.path.abspath(__file__))), 'rb')) + vsp.vendor = vendor + vsp.onboard() + vf = Vf(name='test', vsp=vsp) + vf.onboard() + svc = Service(name='test') + assert svc.identifier is None + assert svc.status is None + svc.create() + assert svc.identifier is not None + assert svc.status == const.DRAFT + svc.add_resource(vf) + svc.checkin() + assert svc.status == const.CHECKED_IN + svc.certify() + assert svc.status == const.CERTIFIED + svc.distribute() + assert svc.status == const.DISTRIBUTED + assert svc.distributed + +@pytest.mark.integration +def test_service_onboard_unknown(): + """Integration tests for Service.""" + response = requests.post("{}/reset".format(SDC.base_front_url)) + response.raise_for_status() + vendor = Vendor(name="test") + vendor.onboard() + vsp = Vsp(name="test", package=open("{}/ubuntu16.zip".format( + os.path.dirname(os.path.abspath(__file__))), 'rb')) + vsp.vendor = vendor + vsp.onboard() + vf = Vf(name='test', vsp=vsp) + vf.onboard() + svc = Service(name='test', resources=[vf]) + svc.onboard() + assert svc.distributed + +@pytest.mark.integration +def test_service_upload_tca_artifact(): + """Integration tests for Service.""" + response = requests.post("{}/reset".format(SDC.base_front_url)) + response.raise_for_status() + vendor = Vendor(name="test") + vendor.onboard() + vsp = Vsp(name="test", package=open("{}/ubuntu16.zip".format( + os.path.dirname(os.path.abspath(__file__))), 'rb')) + vsp.vendor = vendor + vsp.onboard() + vf = Vf(name='test', vsp=vsp) + vf.onboard() + svc = Service(name='test') + svc.create() + svc.add_resource(vf) + assert svc.status == const.DRAFT + payload_file = open("{}/tca_clampnode.yaml".format(os.path.dirname(os.path.abspath(__file__))), 'rb') + data = payload_file.read() + svc.add_artifact_to_vf(vnf_name="test", + artifact_type="DCAE_INVENTORY_BLUEPRINT", + artifact_name="tca_clampnode.yaml", + artifact=data) + payload_file.close() + +@pytest.mark.integration +def test_service_properties(): + """Integration test to check properties assignment for Service.""" + response = requests.post("{}/reset".format(SDC.base_front_url)) + response.raise_for_status() + vendor = Vendor(name="test") + vendor.onboard() + vsp = Vsp(name="test", package=open("{}/ubuntu16.zip".format( + os.path.dirname(os.path.abspath(__file__))), 'rb')) + vsp.vendor = vendor + vsp.onboard() + vf = Vf(name='test', vsp=vsp) + vf.onboard() + properties = [ + Property(name="test1", property_type="string", value="123"), + Property(name="test2", property_type="integer") + ] + svc = Service(name='test', resources=[vf], properties=properties, inputs=[properties[1]]) + svc.onboard() + service_properties = list(svc.properties) + service_inputs = list(svc.inputs) + assert len(service_properties) == 2 + assert len(service_inputs) == 1 diff --git a/integration_tests/test_05_cloud_infrastructure.py b/integration_tests/test_05_cloud_infrastructure.py new file mode 100644 index 0000000..27e3762 --- /dev/null +++ b/integration_tests/test_05_cloud_infrastructure.py @@ -0,0 +1,88 @@ +# Copyright 2022 Orange, 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 pytest +import requests + +from onapsdk.aai.cloud_infrastructure import CloudRegion, Complex +from onapsdk.exceptions import ResourceNotFound + + +@pytest.mark.integration +def test_cloud_region_get_all(): + requests.get(f"{CloudRegion.base_url}/reset") + cloud_regions = list(CloudRegion.get_all()) + assert len(cloud_regions) == 0 + + with pytest.raises(ResourceNotFound): + CloudRegion.get_by_id("test_owner", "test_cloud_region") + + cloud_region: CloudRegion = CloudRegion.create( + "test_owner", "test_cloud_region", orchestration_disabled=True, in_maint=False + ) + cloud_regions = list(CloudRegion.get_all()) + assert len(cloud_regions) == 1 + cloud_region = cloud_regions[0] + assert cloud_region.cloud_owner == "test_owner" + assert cloud_region.cloud_region_id == "test_cloud_region" + + +@pytest.mark.integration +def test_complex_get_all(): + requests.get(f"{Complex.base_url}/reset") + complexes = list(Complex.get_all()) + assert len(complexes) == 0 + + cmplx: Complex = Complex.create( + name="test_complex", + physical_location_id="test_physical_location_id" + ) + assert cmplx.name == "test_complex" + assert cmplx.physical_location_id == "test_physical_location_id" + + complexes = list(Complex.get_all()) + assert len(complexes) == 1 + + cmplx = complexes[0] + assert cmplx.name == "test_complex" + assert cmplx.physical_location_id == "test_physical_location_id" + + +@pytest.mark.integration +def test_link_cloud_region_to_complex(): + + requests.get(f"{Complex.base_url}/reset") + + cmplx: Complex = Complex.create( + name="test_complex", + physical_location_id="test_physical_location_id" + ) + cloud_region: CloudRegion = CloudRegion.create( + "test_owner", "test_cloud_region", orchestration_disabled=True, in_maint=False + ) + + assert len(list(cloud_region.relationships)) == 0 + cloud_region.link_to_complex(cmplx) + assert len(list(cloud_region.relationships)) == 1 + + +@pytest.mark.integration +def test_cloud_region_tenants(): + + cloud_region: CloudRegion = CloudRegion.create( + "test_owner", "test_cloud_region", orchestration_disabled=True, in_maint=False + ) + assert len(list(cloud_region.tenants)) == 0 + cloud_region.add_tenant(tenant_id="test_tenant_id", tenant_name="test_tenant_name", tenant_context="test_tenant_context") + assert len(list(cloud_region.tenants)) == 1 + tenant = cloud_region.get_tenant(tenant_id="test_tenant_id") diff --git a/integration_tests/test_06_customer.py b/integration_tests/test_06_customer.py new file mode 100644 index 0000000..62ac3f0 --- /dev/null +++ b/integration_tests/test_06_customer.py @@ -0,0 +1,85 @@ +# Copyright 2022 Orange, 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 uuid import uuid4 + +import pytest + +import requests +from onapsdk.aai.cloud_infrastructure import CloudRegion, Tenant +from onapsdk.aai.business import Customer, ServiceSubscription +from onapsdk.exceptions import ParameterError +from onapsdk.sdc.service import Service + + +@pytest.mark.integration +def test_create_customer(): + + requests.get(f"{Customer.base_url}/reset") + + customers = list(Customer.get_all()) + assert len(customers) == 0 + + customer = Customer.create(global_customer_id="test_global_customer_id", + subscriber_name="test_subscriber_name", + subscriber_type="test_subscriber_type") + assert customer.global_customer_id == "test_global_customer_id" + assert customer.subscriber_name == "test_subscriber_name" + assert customer.subscriber_type == "test_subscriber_type" + + customers = list(Customer.get_all()) + assert len(customers) == 1 + + +@pytest.mark.integration +def test_subscribe_service(): + + requests.get(f"{Customer.base_url}/reset") + + customer = Customer.create(global_customer_id="test_global_customer_id", + subscriber_name="test_subscriber_name", + subscriber_type="test_subscriber_type") + assert len(list(customer.service_subscriptions)) == 0 + + customer.subscribe_service("service_type") + assert len(list(customer.service_subscriptions)) == 1 + assert customer.get_service_subscription_by_service_type("service_type") + + +@pytest.mark.integration +def test_link_service_subscription_to_cloud_region_and_tenant(): + + requests.get(f"{Customer.base_url}/reset") + + customer = Customer.create(global_customer_id="test_global_customer_id", + subscriber_name="test_subscriber_name", + subscriber_type="test_subscriber_type") + customer.subscribe_service("service_type") + service_subscription = customer.get_service_subscription_by_service_type("service_type") + + assert len(list(service_subscription.relationships)) == 0 + with pytest.raises(ParameterError): + service_subscription.cloud_region + with pytest.raises(ParameterError): + service_subscription.tenant + + cloud_region = CloudRegion.create( + "test_owner", "test_cloud_region", orchestration_disabled=True, in_maint=False + ) + cloud_region.add_tenant( + tenant_id="test_tenant_name", tenant_name="test_tenant_name", tenant_context="test_tenant_context" + ) + tenant = cloud_region.get_tenant(tenant_id="test_tenant_name") + service_subscription.link_to_cloud_region_and_tenant(cloud_region=cloud_region, tenant=tenant) + assert service_subscription.cloud_region + assert service_subscription.tenant diff --git a/integration_tests/test_07_instantiation.py b/integration_tests/test_07_instantiation.py new file mode 100644 index 0000000..c6cbc85 --- /dev/null +++ b/integration_tests/test_07_instantiation.py @@ -0,0 +1,427 @@ +# Copyright 2022 Orange, 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 time +from unittest.mock import MagicMock, PropertyMock, patch +from uuid import uuid4 + +import pytest +import requests +from onapsdk.exceptions import StatusError +from onapsdk.aai.business import Customer, ServiceInstance +from onapsdk.aai.cloud_infrastructure import CloudRegion +from onapsdk.configuration import settings +from onapsdk.sdc.service import Service, Vnf, VfModule +from onapsdk.so.deletion import ServiceDeletionRequest, VfModuleDeletionRequest, VnfDeletionRequest +from onapsdk.so.instantiation import (ServiceInstantiation, SoService, + VfModuleInstantiation, VnfInstantiation, InstantiationParameter, + VfmoduleParameters, VnfParameters) + + +@pytest.mark.integration +def test_a_la_carte_instantiation(): + requests.get(f"{ServiceInstantiation.base_url}/reset") + requests.get(f"{Customer.base_url}/reset") + requests.post(f"{ServiceInstantiation.base_url}/set_aai_mock", json={"AAI_MOCK": settings.AAI_URL}) + + customer = Customer.create(global_customer_id="test_global_customer_id", + subscriber_name="test_subscriber_name", + subscriber_type="test_subscriber_type") + service = Service("test_service") + service.unique_uuid = str(uuid4()) + service.identifier = str(uuid4()) + service.name = str(uuid4()) + customer.subscribe_service("service_type") + service_subscription = customer.get_service_subscription_by_service_type("service_type") + cloud_region = CloudRegion.create( + "test_owner", "test_cloud_region", orchestration_disabled=True, in_maint=False + ) + cloud_region.add_tenant( + tenant_id="test_tenant_name", tenant_name="test_tenant_name", tenant_context="test_tenant_context" + ) + tenant = cloud_region.get_tenant(tenant_id="test_tenant_name") + service_subscription.link_to_cloud_region_and_tenant(cloud_region=cloud_region, tenant=tenant) + owning_entity = "test_owning_entity" + project = "test_project" + + # Service instantiation + service._distributed = True + assert len(list(service_subscription.service_instances)) == 0 + service_instantiation_request = ServiceInstantiation.instantiate_ala_carte( + service, + cloud_region, + tenant, + customer, + owning_entity, + project, + service_subscription + ) + assert service_instantiation_request.status == ServiceInstantiation.StatusEnum.IN_PROGRESS + time.sleep(2) # After 1 second mocked server changed request status to complete + assert service_instantiation_request.status == ServiceInstantiation.StatusEnum.COMPLETED + assert len(list(service_subscription.service_instances)) == 1 + + # Vnf instantiation + service_instance = next(service_subscription.service_instances) + assert len(list(service_instance.vnf_instances)) == 0 + owning_entity = "test_owning_entity" + project = "test_project" + vnf = MagicMock() + line_of_business = "test_line_of_business" + platform = "test_platform" + with pytest.raises(StatusError): + service_instance.add_vnf( + vnf, + line_of_business, + platform + ) + service_instance.orchestration_status = "Active" + service_instance._sdc_service = service + with patch.object(ServiceInstance, "sdc_service", return_value=service): + vnf_instantiation_request = service_instance.add_vnf( + vnf, + line_of_business, + platform + ) + assert vnf_instantiation_request.status == VnfInstantiation.StatusEnum.IN_PROGRESS + time.sleep(2) # After 1 second mocked server changed request status to complete + assert vnf_instantiation_request.status == VnfInstantiation.StatusEnum.COMPLETED + assert len(list(service_instance.vnf_instances)) == 1 + # VfModule instantiation + vnf_instance = next(service_instance.vnf_instances) + assert len(list(vnf_instance.vf_modules)) == 0 + vnf.model_version_id = vnf_instance.model_version_id + vf_module = MagicMock() + + with patch.object(ServiceInstance, "sdc_service", return_value=service) as service_mock: + service_mock.vnfs = [vnf] + vf_module_instantiation_request = vnf_instance.add_vf_module( + vf_module + ) + assert vf_module_instantiation_request.status == VfModuleInstantiation.StatusEnum.IN_PROGRESS + time.sleep(2) # After 1 second mocked server changed request status to complete + assert vf_module_instantiation_request.status == VfModuleInstantiation.StatusEnum.COMPLETED + assert len(list(vnf_instance.vf_modules)) == 1 + + # Cleanup + vf_module_instance = next(vnf_instance.vf_modules) + vf_module_deletion_request = vf_module_instance.delete() + assert vf_module_deletion_request.status == VfModuleDeletionRequest.StatusEnum.IN_PROGRESS + time.sleep(2) # After 1 second mocked server changed request status to complete + assert vf_module_deletion_request.status == VfModuleDeletionRequest.StatusEnum.COMPLETED + assert len(list(vnf_instance.vf_modules)) == 0 + + vnf_deletion_request = vnf_instance.delete() + assert vnf_deletion_request.status == VnfDeletionRequest.StatusEnum.IN_PROGRESS + time.sleep(2) # After 1 second mocked server changed request status to complete + assert vnf_deletion_request.status == VnfDeletionRequest.StatusEnum.COMPLETED + assert len(list(service_instance.vnf_instances)) == 0 + + with patch.object(ServiceInstance, "sdc_service", return_value=service) as service_mock: + service_deletion_request = service_instance.delete() + assert service_deletion_request.status == ServiceDeletionRequest.StatusEnum.IN_PROGRESS + time.sleep(2) # After 1 second mocked server changed request status to complete + assert service_deletion_request.status == ServiceDeletionRequest.StatusEnum.COMPLETED + assert len(list(service_subscription.service_instances)) == 0 + + +@pytest.mark.integration +def test_a_la_carte_vl_instantiation(): + requests.get(f"{ServiceInstantiation.base_url}/reset") + requests.get(f"{Customer.base_url}/reset") + requests.post(f"{ServiceInstantiation.base_url}/set_aai_mock", json={"AAI_MOCK": settings.AAI_URL}) + + customer = Customer.create(global_customer_id="test_global_customer_id", + subscriber_name="test_subscriber_name", + subscriber_type="test_subscriber_type") + service = Service("test_service") + service.unique_uuid = str(uuid4()) + service.identifier = str(uuid4()) + service.name = str(uuid4()) + customer.subscribe_service("service_type") + service_subscription = customer.get_service_subscription_by_service_type("service_type") + cloud_region = CloudRegion.create( + "test_owner", "test_cloud_region", orchestration_disabled=True, in_maint=False + ) + cloud_region.add_tenant( + tenant_id="test_tenant_name", tenant_name="test_tenant_name", tenant_context="test_tenant_context" + ) + tenant = cloud_region.get_tenant(tenant_id="test_tenant_name") + service_subscription.link_to_cloud_region_and_tenant(cloud_region=cloud_region, tenant=tenant) + owning_entity = "test_owning_entity" + project = "test_project" + + # Service instantiation + service._distributed = True + assert len(list(service_subscription.service_instances)) == 0 + service_instantiation_request = ServiceInstantiation.instantiate_ala_carte( + service, + cloud_region, + tenant, + customer, + owning_entity, + project, + service_subscription + ) + assert service_instantiation_request.status == ServiceInstantiation.StatusEnum.IN_PROGRESS + service_instantiation_request.wait_for_finish() + assert service_instantiation_request.status == ServiceInstantiation.StatusEnum.COMPLETED + assert len(list(service_subscription.service_instances)) == 1 + + # Network instantiation + service_instance = next(service_subscription.service_instances) + assert len(list(service_instance.network_instances)) == 0 + owning_entity = "test_owning_entity" + project = "test_project" + network = MagicMock() + line_of_business = "test_line_of_business" + platform = "test_platform" + with pytest.raises(AttributeError): + service_instance.network( + network, + line_of_business, + platform + ) + service_instance.orchestration_status = "Active" + with patch.object(ServiceInstance, "sdc_service", return_value=service): + network_instantiation_request = service_instance.add_network( + network, + line_of_business, + platform + ) + assert network_instantiation_request.status == VnfInstantiation.StatusEnum.IN_PROGRESS + network_instantiation_request.wait_for_finish() + assert network_instantiation_request.status == VnfInstantiation.StatusEnum.COMPLETED + assert len(list(service_instance.network_instances)) == 1 + + +@patch.object(Service, "vnfs", new_callable=PropertyMock) +@patch.object(Service, "components", new_callable=PropertyMock) +@pytest.mark.integration +def test_instantiate_macro(mock_service_components, mock_service_vnfs): + requests.get(f"{ServiceInstantiation.base_url}/reset") + requests.get(f"{Customer.base_url}/reset") + requests.post(f"{ServiceInstantiation.base_url}/set_aai_mock", json={"AAI_MOCK": settings.AAI_URL}) + + customer = Customer.create(global_customer_id="test_global_customer_id", + subscriber_name="test_subscriber_name", + subscriber_type="test_subscriber_type") + service = Service("test_service") + service._tosca_template = "n/a" + + mock_service_vnfs.return_value = [ + Vnf( + name="test_vnf", + node_template_type="vf", + model_name= "test_vnf_model", + model_version_id = str(uuid4()), + model_invariant_id=str(uuid4()), + model_version="1.0", + model_customization_id=str(uuid4()), + model_instance_name=str(uuid4()), + component=MagicMock(), + vf_modules=[ + VfModule( + name="TestVnfModel..base..module-0", + group_type="vf-module", + model_name="TestVnfModel..base..module-0", + model_version_id=str(uuid4()), + model_invariant_uuid=str(uuid4()), + model_version="1", + model_customization_id=str(uuid4()), + properties=None + ) + ] + ) + ] + service.unique_uuid = str(uuid4()) + service.identifier = str(uuid4()) + service.name = str(uuid4()) + customer.subscribe_service("service_type") + service_subscription = customer.get_service_subscription_by_service_type("service_type") + cloud_region = CloudRegion.create( + "test_owner", "test_cloud_region", orchestration_disabled=True, in_maint=False + ) + cloud_region.add_tenant( + tenant_id="test_tenant_name", tenant_name="test_tenant_name", tenant_context="test_tenant_context" + ) + tenant = cloud_region.get_tenant(tenant_id="test_tenant_name") + service_subscription.link_to_cloud_region_and_tenant(cloud_region=cloud_region, tenant=tenant) + owning_entity = "test_owning_entity" + project = "test_project" + line_of_business = "test_line_of_business" + platform = "test_platform" + + vfm_instance_params = [ + InstantiationParameter(name="vfm_param", value="vfm_param_value"), + + ] + vfm_params = VfmoduleParameters("base", vfm_instance_params) + + vnf_instance_params = [ + InstantiationParameter(name="vnf_param", value="vnf_param_value") + ] + + vnf_params = VnfParameters("test_vnf_model", vnf_instance_params, [vfm_params]) + + # Service instantiation + service._distributed = True + assert len(list(service_subscription.service_instances)) == 0 + service_instantiation_request = ServiceInstantiation.instantiate_macro( + sdc_service=service, + customer=customer, + owning_entity=owning_entity, + project=project, + line_of_business=line_of_business, + platform=platform, + cloud_region=cloud_region, + tenant=tenant, + vnf_parameters=[vnf_params], + service_subscription=service_subscription + ) + assert service_instantiation_request.status == ServiceInstantiation.StatusEnum.IN_PROGRESS + time.sleep(2) # After 1 second mocked server changed request status to complete + assert service_instantiation_request.status == ServiceInstantiation.StatusEnum.COMPLETED + assert len(list(service_subscription.service_instances)) == 1 + service_instance = next(service_subscription.service_instances) + + # Cleanup + with patch.object(ServiceInstance, "sdc_service", return_value=service) as service_mock: + service_deletion_request = service_instance.delete() + assert service_deletion_request.status == ServiceDeletionRequest.StatusEnum.IN_PROGRESS + time.sleep(2) # After 1 second mocked server changed request status to complete + assert service_deletion_request.status == ServiceDeletionRequest.StatusEnum.COMPLETED + assert len(list(service_subscription.service_instances)) == 0 + +@patch.object(Service, "vnfs", new_callable=PropertyMock) +@patch.object(Service, "components", new_callable=PropertyMock) +@pytest.mark.integration +def test_instantiate_macro_multiple_vnf(mock_service_components, mock_service_vnfs): + requests.get(f"{ServiceInstantiation.base_url}/reset") + requests.get(f"{Customer.base_url}/reset") + requests.post(f"{ServiceInstantiation.base_url}/set_aai_mock", json={"AAI_MOCK": settings.AAI_URL}) + + customer = Customer.create(global_customer_id="test_global_customer_id", + subscriber_name="test_subscriber_name", + subscriber_type="test_subscriber_type") + service = Service("test_service") + service._tosca_template = "n/a" + + mock_service_vnfs.return_value = [ + Vnf( + name="test_vnf", + node_template_type="vf", + model_name= "test_vnf_model", + model_version_id = str(uuid4()), + model_invariant_id=str(uuid4()), + model_version="1.0", + model_customization_id=str(uuid4()), + model_instance_name=str(uuid4()), + component=MagicMock(), + vf_modules=[ + VfModule( + name="TestVnfModel..base..module-0", + group_type="vf-module", + model_name="TestVnfModel..base..module-0", + model_version_id=str(uuid4()), + model_invariant_uuid=str(uuid4()), + model_version="1", + model_customization_id=str(uuid4()), + properties=None + ) + ] + ) + ] + service.unique_uuid = str(uuid4()) + service.identifier = str(uuid4()) + service.name = str(uuid4()) + customer.subscribe_service("service_type") + service_subscription = customer.get_service_subscription_by_service_type("service_type") + cloud_region = CloudRegion.create( + "test_owner", "test_cloud_region", orchestration_disabled=True, in_maint=False + ) + cloud_region.add_tenant( + tenant_id="test_tenant_name", tenant_name="test_tenant_name", tenant_context="test_tenant_context" + ) + tenant = cloud_region.get_tenant(tenant_id="test_tenant_name") + service_subscription.link_to_cloud_region_and_tenant(cloud_region=cloud_region, tenant=tenant) + owning_entity = "test_owning_entity" + project = "test_project" + line_of_business = "test_line_of_business" + platform = "test_platform" + + so_service = SoService.load({ + "subscription_service_type": "service_type", + "vnfs": [ + { + "model_name": "test_vnf_model", + "instance_name": "vnf0", + "parameters": { + "param1": "value1" + }, + "vf_modules": [ + { + "instance_name": "vnf0_vfm0", + "model_name": "base", + "parameters": { + "vfm_param1": "vfm_value1" + } + } + ] + }, + { + "model_name": "test_vnf_model", + "instance_name": "vnf1", + "parameters": { + "param2": "value2" + }, + "vf_modules": [ + { + "instance_name": "vnf1_vfm0", + "model_name": "base", + "parameters": { + "vfm_param2": "vfm_value2" + } + } + ] + } + ] + }) + + # Service instantiation + service._distributed = True + assert len(list(service_subscription.service_instances)) == 0 + service_instantiation_request = ServiceInstantiation.instantiate_macro( + sdc_service=service, + customer=customer, + owning_entity=owning_entity, + project=project, + line_of_business=line_of_business, + platform=platform, + cloud_region=cloud_region, + tenant=tenant, + so_service=so_service + ) + assert service_instantiation_request.status == ServiceInstantiation.StatusEnum.IN_PROGRESS + time.sleep(2) # After 1 second mocked server changed request status to complete + assert service_instantiation_request.status == ServiceInstantiation.StatusEnum.COMPLETED + assert len(list(service_subscription.service_instances)) == 1 + service_instance = next(service_subscription.service_instances) + + # Cleanup + with patch.object(ServiceInstance, "sdc_service", return_value=service) as service_mock: + service_deletion_request = service_instance.delete() + assert service_deletion_request.status == ServiceDeletionRequest.StatusEnum.IN_PROGRESS + time.sleep(2) # After 1 second mocked server changed request status to complete + assert service_deletion_request.status == ServiceDeletionRequest.StatusEnum.COMPLETED + assert len(list(service_subscription.service_instances)) == 0 diff --git a/integration_tests/test_08_cds.py b/integration_tests/test_08_cds.py new file mode 100644 index 0000000..3d75f37 --- /dev/null +++ b/integration_tests/test_08_cds.py @@ -0,0 +1,65 @@ +# Copyright 2022 Orange, 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 os +import pytest +import requests + +from tempfile import TemporaryDirectory + +from onapsdk.configuration import settings +from onapsdk.cds.blueprint import Blueprint +from onapsdk.cds.data_dictionary import DataDictionary, DataDictionarySet + + +@pytest.mark.integration +def test_cds_connection(): + + TEST_DD_PATH = os.path.join(os.getcwd(), "integration_tests/test_files/test_dd.json") + TEST_CBA_PATH = os.path.join(os.getcwd(), "integration_tests/test_files/test_vLB_CBA_Python.zip") + + + # Endpoint availability + response = requests.post("{}/api/v1/dictionary".format(settings.CDS_URL)) + assert response.status_code == 200 + + response = requests.post("{}/api/v1/blueprint-model/enrich".format(settings.CDS_URL)) + assert response.status_code == 200 + + response = requests.post("{}/api/v1/blueprint-model/publish".format(settings.CDS_URL)) + assert response.status_code == 200 + + + # Reads from the file system + dd_set = DataDictionarySet.load_from_file(TEST_DD_PATH, True) + blueprint = Blueprint.load_from_file(TEST_CBA_PATH) + + + # Connection availability between CDS API and Blueprint/DataDictionarySet + dd_set.upload() + assert type(blueprint.cba_file_bytes) == bytes + + for dd in dd_set.dd_set: + dd_obj = DataDictionary.get_by_name(dd.name) + assert dd_obj == dd + + blueprint = blueprint.enrich() + assert type(blueprint.cba_file_bytes) == bytes + + blueprint.publish() + + + # Writes to the file system + with TemporaryDirectory() as tmpdirname: + path = os.path.join(tmpdirname, "test-CBA-enriched.zip") + blueprint.save(path) diff --git a/integration_tests/test_09_clamp.py b/integration_tests/test_09_clamp.py new file mode 100644 index 0000000..f745e2f --- /dev/null +++ b/integration_tests/test_09_clamp.py @@ -0,0 +1,78 @@ +"""Integration test Clamp module.""" +# Copyright 2022 Orange, 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 os + +import pytest + +import requests + +from onapsdk.sdc.service import Service +from onapsdk.clamp.clamp_element import Clamp +from onapsdk.clamp.loop_instance import LoopInstance + +@pytest.mark.integration +def test_Clamp_requirements(): + """Integration tests for Clamp.""" + requests.get("{}/reset".format(Clamp._base_url)) + # no add resource in clamp + # svc already exists in mock clamp + Clamp() + svc = Service(name="service01") + template_exists = Clamp.check_loop_template(service=svc) + assert template_exists + policy_exists = Clamp.check_policies(policy_name="MinMax", + req_policies=2) + assert policy_exists + +@pytest.mark.integration +def test_Loop_creation(): + """Integration tests for Loop Instance.""" + requests.get("{}/reset".format(Clamp._base_url)) + Clamp() + svc = Service(name="service01") + loop_template = Clamp.check_loop_template(service=svc) + response = requests.post("{}/reset".format(Clamp._base_url)) + response.raise_for_status() + loop = LoopInstance(template=loop_template, name="instance01", details={}) + loop.create() + +@pytest.mark.integration +def test_Loop_customization(): + """Integration tests for Loop Instance.""" + requests.get("{}/reset".format(Clamp._base_url)) + Clamp() + svc = Service(name="service01") + loop_template = Clamp.check_loop_template(service=svc) + response = requests.post("{}/reset".format(Clamp._base_url)) + response.raise_for_status() + loop = LoopInstance(template=loop_template, name="instance01", details={}) + loop.create() + loop.update_microservice_policy() + #add op policy FrequencyLimiter that already exists in clamp + loop.add_operational_policy(policy_type="onap.policies.controlloop.guard.common.FrequencyLimiter", + policy_version="1.0.0") + #only frequency configuration is available in mock clamp + loop.add_op_policy_config(loop.add_frequency_limiter, limit=1) + submit = loop.act_on_loop_policy(loop.submit) + assert submit + stop = loop.act_on_loop_policy(loop.stop) + assert stop + restart = loop.act_on_loop_policy(loop.restart) + assert restart + deploy = loop.deploy_microservice_to_dcae() + assert deploy + loop.undeploy_microservice_from_dcae() + new_details = loop._update_loop_details() + assert new_details["components"]["DCAE"]["componentState"]["stateName"] == "MICROSERVICE_UNINSTALLED_SUCCESSFULLY" diff --git a/integration_tests/test_10_msb_k8s.py b/integration_tests/test_10_msb_k8s.py new file mode 100644 index 0000000..5b2c0ee --- /dev/null +++ b/integration_tests/test_10_msb_k8s.py @@ -0,0 +1,125 @@ +# Copyright 2022 Orange, 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 +import os + +import pytest + +from onapsdk.msb.k8s import ( + Definition, + Instance, + ConnectivityInfo) + +logger = logging.getLogger("") +logger.setLevel(logging.DEBUG) +fh = logging.StreamHandler() +fh_formatter = logging.Formatter('%(asctime)s %(levelname)s %(lineno)d:%(filename)s(%(process)d) - %(message)s') +fh.setFormatter(fh_formatter) +logger.addHandler(fh) + +RB_NAME = "test_definition" +RB_VERSION = "ver_1" +PROFILE_NAME = "test-profile" +PROFILE_NAMESPACE = "test" +PROFILE_K8S_VERSION = "1.0" +PROFILE_ARTIFACT_PATH = "artifacts\\profile.tar.gz" # FILL ME +TEMPLATE_NAME = "test_template" +TEMPLATE_DESCRIPTION = "test description" +CLOUD_REGION_ID = "k8s_region_test" # FILL ME +CLOUD_OWNER = "CloudOwner" +KUBECONFIG_PATH = "artifacts\\kubeconfig" # FILL ME +MYPATH = os.path.dirname(os.path.realpath(__file__)) + +pytest.INSTANCE_ID = "" + + +@pytest.mark.integration +def test_definition_create_upload_artifact(): + definition = Definition.create(RB_NAME, RB_VERSION) + definition.upload_artifact(b'definition_artifact_file') + + +@pytest.mark.integration +def test_definition_get_all(): + definitions = list(Definition.get_all()) + + +@pytest.mark.integration +def test_configuration_template(): + definition = Definition.get_definition_by_name_version(RB_NAME, + RB_VERSION) + definition.create_configuration_template(TEMPLATE_NAME, TEMPLATE_DESCRIPTION) + definition.get_all_configuration_templates() + definition.get_configuration_template_by_name(TEMPLATE_NAME) + + +@pytest.mark.integration +def test_profile_create_upload_artifact(): + definition = Definition.get_definition_by_name_version(RB_NAME, + RB_VERSION) + profile = definition.create_profile(PROFILE_NAME, + PROFILE_NAMESPACE, + PROFILE_K8S_VERSION) + profile.upload_artifact(b'profile_artifact_file') + + +@pytest.mark.integration +def test_profile_get_all(): + definition = Definition.get_definition_by_name_version(RB_NAME, + RB_VERSION) + profiles = list(definition.get_all_profiles()) + + +@pytest.mark.integration +def test_connectivity_info_create(): + conninfo = ConnectivityInfo.create(CLOUD_REGION_ID, + CLOUD_OWNER, + b'kubeconfig_content_test') + + +@pytest.mark.integration +def test_instance_create(): + definition = Definition.get_definition_by_name_version(RB_NAME, + RB_VERSION) + profile = definition.get_profile_by_name(PROFILE_NAME) + instance = Instance.create(CLOUD_REGION_ID, + profile.profile_name, + definition.rb_name, + definition.rb_version) + pytest.INSTANCE_ID = instance.instance_id + + +@pytest.mark.integration +def test_instance_get_all(): + instances = list(Instance.get_all()) + + +@pytest.mark.integration +def test_instance_delete(): + instance = Instance.get_by_id(pytest.INSTANCE_ID) + instance.delete() + + +@pytest.mark.integration +def test_connectivity_info_delete(): + conninfo = ConnectivityInfo.get_connectivity_info_by_region_id(CLOUD_REGION_ID) + conninfo.delete() + + +@pytest.mark.integration +def test_definition_profile_get_delete(): + definition = Definition.get_definition_by_name_version(RB_NAME, RB_VERSION) + profile = definition.get_profile_by_name(PROFILE_NAME) + profile.delete() + definition.delete() diff --git a/integration_tests/test_11_ves.py b/integration_tests/test_11_ves.py new file mode 100644 index 0000000..5553176 --- /dev/null +++ b/integration_tests/test_11_ves.py @@ -0,0 +1,109 @@ +# Copyright 2022 Nokia +# +# 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 pytest +import logging +import os +import requests + +from onapsdk.configuration import settings +from onapsdk.utils.jinja import jinja_env +from onapsdk.ves.ves import Ves +from onapsdk.dmaap.dmaap import Dmaap + +logging.basicConfig(level=os.environ.get("LOGLEVEL", "DEBUG")) + + +def reset_dmaap_mock(): + requests.get("{}/reset".format(settings.DMAAP_URL)) + + +@pytest.mark.integration +def test_should_send_event_to_ves(): + # given + + requests.post("{}/set_dmaap_address".format(settings.VES_URL), json={"DMAAP_MOCK": settings.DMAAP_URL}) + event: str = jinja_env().get_template("ves_stnd_event.json.j2").render() + + # when + response = Ves.send_event( + basic_auth={'username': 'sample1', 'password': 'sample1'}, + json_event=event, + version="v7" + ) + + # then + assert response.status_code == 202 + + +@pytest.mark.integration +def test_should_send_batch_event_to_ves(): + # given + + requests.post("{}/set_dmaap_address".format(settings.VES_URL), json={"DMAAP_MOCK": settings.DMAAP_URL}) + event: str = jinja_env().get_template("ves7_batch_with_stndDefined_valid.json.j2").render() + + # when + response = Ves.send_batch_event( + basic_auth={'username': 'sample1', 'password': 'sample1'}, + json_event=event, + version="v7" + ) + + # then + assert response.status_code == 202 + + +@pytest.mark.integration +def test_should_send_event_to_ves_and_dmaap(): + # given + + requests.post("{}/set_dmaap_address".format(settings.VES_URL), json={"DMAAP_MOCK": settings.DMAAP_URL}) + event: str = jinja_env().get_template("ves_stnd_event.json.j2").render() + + # when + reset_dmaap_mock() + response = Ves.send_event( + basic_auth={'username': 'sample1', 'password': 'sample1'}, + json_event=event, + + version="v7" + ) + + # then + assert response.status_code == 202 + events = Dmaap.get_events_for_topic("fault", + basic_auth={'username': 'dcae@dcae.onap.org', 'password': 'demo123456!'}) + assert len(events) == 1 + + +@pytest.mark.integration +def test_should_send_batch_event_to_ves_and_dmaap(): + # given + + requests.post("{}/set_dmaap_address".format(settings.VES_URL), json={"DMAAP_MOCK": settings.DMAAP_URL}) + event: str = jinja_env().get_template("ves7_batch_with_stndDefined_valid.json.j2").render() + + # when + reset_dmaap_mock() + response = Ves.send_batch_event( + basic_auth={'username': 'sample1', 'password': 'sample1'}, + json_event=event, + version="v7" + ) + + # then + assert response.status_code == 202 + events = Dmaap.get_events_for_topic("fault", + basic_auth={'username': 'dcae@dcae.onap.org', 'password': 'demo123456!'}) + assert len(events) == 2 diff --git a/integration_tests/test_12_dmaap.py b/integration_tests/test_12_dmaap.py new file mode 100644 index 0000000..a3e9247 --- /dev/null +++ b/integration_tests/test_12_dmaap.py @@ -0,0 +1,31 @@ +# Copyright 2022 Nokia +# +# 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 pytest +import logging +import os + +from onapsdk.dmaap.dmaap import Dmaap + +logging.basicConfig(level=os.environ.get("LOGLEVEL", "DEBUG")) + + +@pytest.mark.integration +def test_should_get_all_topics_from_dmaap(): + # given + + # when + response = Dmaap.get_all_topics(basic_auth={'username': 'demo', 'password': 'demo123456!'}) + + # then + assert len(response) == 9 diff --git a/integration_tests/test_files/test_dd.json b/integration_tests/test_files/test_dd.json new file mode 100644 index 0000000..26f0b90 --- /dev/null +++ b/integration_tests/test_files/test_dd.json @@ -0,0 +1,48 @@ +[ + { + "name": "vpg_int_pktgen_private_ip_0", + "tags": "vpg_int_pktgen_private_ip_0", + "data_type": "string", + "description": "vpg_int_pktgen_private_ip_0", + "entry_schema": "string", + "updatedBy": "Singal, Kapil <ks220y@att.com>", + "definition": { + "tags": "vpg_int_pktgen_private_ip_0", + "name": "vpg_int_pktgen_private_ip_0", + "property": { + "description": "vpg_int_pktgen_private_ip_0", + "type": "string" + }, + "updated-by": "Singal, Kapil <ks220y@att.com>", + "sources": { + "input": { + "type": "source-input" + }, + "default": { + "type": "source-default", + "properties": {} + }, + "sdnc": { + "type": "source-rest", + "properties": { + "verb": "GET", + "type": "JSON", + "url-path": "/restconf/config/GENERIC-RESOURCE-API:services/service/$service-instance-id/service-data/vnfs/vnf/$vnf-id/vnf-data/vnf-topology/vnf-parameters-data/param/vpg_int_pktgen_private_ip_0", + "path": "/param/0/value", + "input-key-mapping": { + "service-instance-id": "service-instance-id", + "vnf-id": "vnf-id" + }, + "output-key-mapping": { + "vpg_int_pktgen_private_ip_0": "value" + }, + "key-dependencies": [ + "service-instance-id", + "vnf-id" + ] + } + } + } + } + } +] diff --git a/integration_tests/test_files/test_vLB_CBA_Python.zip b/integration_tests/test_files/test_vLB_CBA_Python.zip Binary files differnew file mode 100644 index 0000000..ddd41ac --- /dev/null +++ b/integration_tests/test_files/test_vLB_CBA_Python.zip diff --git a/integration_tests/ubuntu16.zip b/integration_tests/ubuntu16.zip Binary files differnew file mode 100644 index 0000000..9a98baa --- /dev/null +++ b/integration_tests/ubuntu16.zip diff --git a/integration_tests/urls.py b/integration_tests/urls.py new file mode 100644 index 0000000..2d6b11e --- /dev/null +++ b/integration_tests/urls.py @@ -0,0 +1,23 @@ +# Copyright 2022 Orange, 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. +SDC_BE_URL = "http://sdc.api.fe.simpledemo.onap.org:30206" +SDC_FE_URL = "http://sdc.api.fe.simpledemo.onap.org:30206" +AAI_URL = "http://aai.api.sparky.simpledemo.onap.org:5000" +SO_URL = "http://so.api.simpledemo.onap.org:5001" +SDNC_URL = "http://sdnc.api.simpledemo.onap.org:5002" +CLAMP_URL = "http://clamp.api.simpledemo.onap.org:30258" +CDS_URL = "http://cds.api.simpledemo.onap.org:8080" +MSB_URL = "http://msb.k8s.api.simpledemo.onap.org:5003" +VES_URL = "http://ves.api.simpledemo.onap.org:30417" +DMAAP_URL = "http://dmaap.api.simpledemo.onap.org:3904" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..a486f46 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,8 @@ +requests[socks]==2.27.1 +jinja2==3.0.3 +simplejson==3.17.6 +oyaml==1.0 +pyOpenSSL==22.0.0 +jsonschema==4.4.0 +dacite==1.6.0 +more-itertools>=8.12.0 diff --git a/scripts/build_all_branches_in.sh b/scripts/build_all_branches_in.sh new file mode 100755 index 0000000..3e56ddd --- /dev/null +++ b/scripts/build_all_branches_in.sh @@ -0,0 +1,36 @@ +#!/bin/sh + +INITIAL_FOLDER=${PWD} +INITIAL_BRANCH=$(git rev-parse --abbrev-ref HEAD) + +if [ -e requirements.txt ] +then + pip install -r requirements.txt +fi +if [ -e doc-requirements.txt ] +then + pip install -r doc-requirements.txt +fi +if [ -e requirements.txt ] +then + pip install . +fi + +set -x +# Generating documentation for each other branch in a subdirectory +for BRANCH in $(git branch --remotes --format '%(refname:lstrip=3)' | grep -Ev '^(HEAD)$'); do + echo "*** Building doc for branch ${BRANCH} ***" + git checkout $BRANCH + cd ${INITIAL_FOLDER}${DOC_PATH} + make html + mkdir -p ${INITIAL_FOLDER}/public/$BRANCH + mv _build/html/ ${INITIAL_FOLDER}/public/$BRANCH + rm -rf _build/html/ + cd ${INITIAL_FOLDER} +done + +# "Develop" is the default branch so we point it as "latest" +# May/Will change to point master +ln public/develop public/latest + +git checkout $INITIAL_BRANCH diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..9fbcc6b --- /dev/null +++ b/setup.cfg @@ -0,0 +1,53 @@ +# SPDX-License-Identifier: Apache-2.0 +[metadata] +name = onapsdk +version = attr: src.onapsdk.version.__version__ +description = SDK to use ONAP Programatically +long_description = file: README.md, CHANGELOG.md +long_description_content_type = text/markdown +url = https://gitlab.com/Orange-OpenSource/lfn/onap/python-onapsdk +author = Orange OpenSource +license = Apache 2.0 +classifiers = + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + +[options] +zip_safe = False +include_package_data = True +python_requires = >=3.7,<4 +package_dir= + =src +packages=find_namespace: +install_requires = + requests[socks]==2.24.0 + jinja2==3.0.3 + simplejson==3.17.6 + oyaml==1.0 + pyOpenSSL==22.0.0 + jsonschema==4.4.0 + dacite==1.6.0 +setup_requires = + pytest-runner==5.2 +tests_require = + pytest==7.0.1 + pytest-cov==3.0.0 + requests-mock==1.9.3 + +[options.packages.find] +where=src + +[options.package_data] +onapsdk = **/templates/* + +[aliases] +test=pytest + +[tool:pytest] +addopts = + --verbose --doctest-modules --junitxml=pytest-unit.xml + --cov-report term-missing --cov-report xml --cov-report html + --cov=src/onapsdk --maxfail=1 --cov-fail-under=98 + +testpaths = tests src diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..acfdb5d --- /dev/null +++ b/setup.py @@ -0,0 +1,7 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 +# -*- coding: utf-8 -*- + +from setuptools import setup + +setup() diff --git a/src/onapsdk/__init__.py b/src/onapsdk/__init__.py new file mode 100644 index 0000000..ce228b1 --- /dev/null +++ b/src/onapsdk/__init__.py @@ -0,0 +1,14 @@ +"""ONAP SDK master package.""" +# Copyright 2022 Orange, 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/src/onapsdk/aai/__init__.py b/src/onapsdk/aai/__init__.py new file mode 100644 index 0000000..e340efb --- /dev/null +++ b/src/onapsdk/aai/__init__.py @@ -0,0 +1,14 @@ +"""ONAP SDK AAI package.""" +# Copyright 2022 Orange, 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/src/onapsdk/aai/aai_element.py b/src/onapsdk/aai/aai_element.py new file mode 100644 index 0000000..9472165 --- /dev/null +++ b/src/onapsdk/aai/aai_element.py @@ -0,0 +1,192 @@ +"""AAI Element module.""" +# Copyright 2022 Orange, 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 dataclasses import dataclass, field +from typing import Dict, Iterator, List, Optional + +from onapsdk.configuration import settings +from onapsdk.onap_service import OnapService +from onapsdk.utils.headers_creator import headers_aai_creator +from onapsdk.utils.jinja import jinja_env +from onapsdk.utils.gui import GuiItem, GuiList + +from onapsdk.exceptions import RelationshipNotFound, ResourceNotFound + + +@dataclass +class Relationship: + """Relationship class. + + A&AI elements could have relationship with other A&AI elements. + Relationships are represented by this class objects. + """ + + related_to: str + related_link: str + relationship_data: List[Dict[str, str]] + relationship_label: str = "" + related_to_property: List[Dict[str, str]] = field(default_factory=list) + + def get_relationship_data(self, relationship_key: str) -> Optional[str]: + """Get relationship data for given relationship key. + + From list of relationship data get the value for + given key + + Args: + relationship_key (str): Key to get relationship data value + + Returns: + Optional[str]: Relationship value or None if relationship data + with provided ket doesn't exist + + """ + for data in self.relationship_data: + if data["relationship-key"] == relationship_key: + return data["relationship-value"] + return None + + +class AaiElement(OnapService): + """Mother Class of all A&AI elements.""" + + name: str = "AAI" + server: str = "AAI" + base_url = settings.AAI_URL + api_version = "/aai/" + settings.AAI_API_VERSION + headers = headers_aai_creator(OnapService.headers) + + @classmethod + def get_guis(cls) -> GuiItem: + """Retrieve the status of the AAI GUIs. + + Only one GUI is referenced for AAI + the AAI sparky GUI + + Return the list of GUIs + """ + gui_url = settings.AAI_GUI_SERVICE + aai_gui_response = cls.send_message( + "GET", "Get AAI GUI Status", gui_url) + guilist = GuiList([]) + guilist.add(GuiItem( + gui_url, + aai_gui_response.status_code)) + return guilist + + +class AaiResource(AaiElement): + """A&AI resource class.""" + + @classmethod + def filter_none_key_values(cls, dict_to_filter: Dict[str, Optional[str]]) -> Dict[str, str]: + """Filter out None key values from dictionary. + + Iterate through given dictionary and filter None values. + + Args: + dict_to_filter (Dict): Dictionary to filter out None + + Returns:dataclasse init a field + Dict[str, str]: Filtered dictionary + + """ + return dict( + filter(lambda key_value_tuple: key_value_tuple[1] is not None, dict_to_filter.items(),) + ) + + @property + def url(self) -> str: + """Resource's url. + + Returns: + str: Resource's url + + """ + raise NotImplementedError + + @property + def relationships(self) -> Iterator[Relationship]: + """Resource relationships iterator. + + Yields: + Relationship: resource relationship + + Raises: + RelationshipNotFound: if request for relationships returned 404 + + """ + try: + generator = self.send_message_json("GET", + "Get object relationships", + f"{self.url}/relationship-list")\ + .get("relationship", []) + for relationship in generator: + yield Relationship( + related_to=relationship.get("related-to"), + relationship_label=relationship.get("relationship-label"), + related_link=relationship.get("related-link"), + relationship_data=relationship.get("relationship-data"), + ) + except ResourceNotFound as exc: + self._logger.error("Getting object relationships failed: %s", exc) + + msg = (f'{self.name} relationships not found.' + f'Server: {self.server}. Url: {self.url}') + raise RelationshipNotFound(msg) from exc + + @classmethod + def get_all_url(cls, *args, **kwargs) -> str: + """Return an url for all objects of given class. + + Returns: + str: URL to get all objects of given class + + """ + raise NotImplementedError + + @classmethod + def count(cls, *args, **kwargs) -> int: + """Get the count number of all objects of given class. + + Get the response, iterate through response (each class has different response) + -- the first key value is the count. + + Returns: + int: Count of the objects + + """ + return next(iter(cls.send_message_json( + "GET", + f"Get count of {cls.__name__} class instances", + f"{cls.get_all_url(*args, **kwargs)}?format=count" + )["results"][0].values())) + + def add_relationship(self, relationship: Relationship) -> None: + """Add relationship to aai resource. + + Add relationship to resource using A&AI API + + Args: + relationship (Relationship): Relationship to add + + """ + self.send_message( + "PUT", + f"add relationship to {self.__class__.__name__}", + f"{self.url}/relationship-list/relationship", + data=jinja_env() + .get_template("aai_add_relationship.json.j2") + .render(relationship=relationship), + ) diff --git a/src/onapsdk/aai/bulk.py b/src/onapsdk/aai/bulk.py new file mode 100644 index 0000000..435a0b4 --- /dev/null +++ b/src/onapsdk/aai/bulk.py @@ -0,0 +1,90 @@ +"""A&AI bulk module.""" +# Copyright 2022 Orange, 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 dataclasses import dataclass +from typing import Any, Dict, Iterable + +from more_itertools import chunked + +from onapsdk.configuration import settings +from onapsdk.utils.jinja import jinja_env + +from .aai_element import AaiElement + + +@dataclass +class AaiBulkRequest: + """Class to store information about a request to be sent in A&AI bulk request.""" + + action: str + uri: str + body: Dict[Any, Any] + + +@dataclass +class AaiBulkResponse: + """Class to store A&AI bulk response.""" + + action: str + uri: str + status_code: int + body: str + + +class AaiBulk(AaiElement): + """A&AI bulk class. + + Use it to send bulk request to A&AI. With bulk request you can send + multiple requests at once. + """ + + @property + def url(self) -> str: + """Bulk url. + + Returns: + str: A&AI bulk API url. + + """ + return f"{self.base_url}{self.api_version}/bulk" + + @classmethod + def single_transaction(cls, aai_requests: Iterable[AaiBulkRequest])\ + -> Iterable[AaiBulkResponse]: + """Singe transaction bulk request. + + Args: + aai_requests (Iterable[AaiBulkRequest]): Iterable object of requests to be sent + as a bulk request. + + Yields: + Iterator[Iterable[AaiBulkResponse]]: Bulk request responses. Each object + correspond to the sent request. + + """ + for requests_chunk in chunked(aai_requests, settings.AAI_BULK_CHUNK): + for response in cls.send_message_json(\ + "POST",\ + "Send bulk A&AI request",\ + f"{cls.base_url}{cls.api_version}/bulk/single-transaction",\ + data=jinja_env().get_template(\ + "aai_bulk.json.j2").render(operations=requests_chunk)\ + )["operation-responses"]: + yield AaiBulkResponse( + action=response["action"], + uri=response["uri"], + status_code=response["response-status-code"], + body=response["response-body"] + ) diff --git a/src/onapsdk/aai/business/__init__.py b/src/onapsdk/aai/business/__init__.py new file mode 100644 index 0000000..41f9671 --- /dev/null +++ b/src/onapsdk/aai/business/__init__.py @@ -0,0 +1,27 @@ +"""A&AI business package.""" +# Copyright 2022 Orange, 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 .customer import Customer, ServiceSubscription +from .instance import Instance +from .line_of_business import LineOfBusiness +from .network import NetworkInstance +from .owning_entity import OwningEntity +from .platform import Platform +from .pnf import PnfInstance +from .project import Project +from .service import ServiceInstance +from .sp_partner import SpPartner +from .vf_module import VfModuleInstance +from .vnf import VnfInstance diff --git a/src/onapsdk/aai/business/customer.py b/src/onapsdk/aai/business/customer.py new file mode 100644 index 0000000..cdefd6f --- /dev/null +++ b/src/onapsdk/aai/business/customer.py @@ -0,0 +1,603 @@ +"""AAI business module.""" +# Copyright 2022 Orange, 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 dataclasses import dataclass +from typing import Iterable, Iterator, Optional +from urllib.parse import urlencode + +from onapsdk.utils.jinja import jinja_env +from onapsdk.exceptions import APIError, ParameterError, ResourceNotFound + +from ..aai_element import AaiResource, Relationship +from ..cloud_infrastructure.cloud_region import CloudRegion +from .service import ServiceInstance + + +@dataclass +class ServiceSubscriptionCloudRegionTenantData: + """Dataclass to store cloud regions and tenants data for service subscription.""" + + cloud_owner: str = None + cloud_region_id: str = None + tenant_id: str = None + + +@dataclass +class ServiceSubscription(AaiResource): + """Service subscription class.""" + + service_type: str + resource_version: str + customer: "Customer" + + def __init__(self, customer: "Customer", service_type: str, resource_version: str) -> None: + """Service subscription object initialization. + + Args: + customer (Customer): Customer object + service_type (str): Service type + resource_version (str): Service subscription resource version + """ + super().__init__() + self.customer: "Customer" = customer + self.service_type: str = service_type + self.resource_version: str = resource_version + + def _get_service_instance_by_filter_parameter(self, + filter_parameter_name: str, + filter_parameter_value: str) -> ServiceInstance: + """Call a request to get service instance with given filter parameter and value. + + Args: + filter_parameter_name (str): Name of parameter to filter + filter_parameter_value (str): Value of filter parameter + + Returns: + ServiceInstance: ServiceInstance object + + """ + service_instance: dict = self.send_message_json( + "GET", + f"Get service instance with {filter_parameter_value} {filter_parameter_name}", + f"{self.url}/service-instances?{filter_parameter_name}={filter_parameter_value}" + )["service-instance"][0] + return ServiceInstance( + service_subscription=self, + instance_id=service_instance.get("service-instance-id"), + instance_name=service_instance.get("service-instance-name"), + service_type=service_instance.get("service-type"), + service_role=service_instance.get("service-role"), + environment_context=service_instance.get("environment-context"), + workload_context=service_instance.get("workload-context"), + created_at=service_instance.get("created-at"), + updated_at=service_instance.get("updated-at"), + description=service_instance.get("description"), + model_invariant_id=service_instance.get("model-invariant-id"), + model_version_id=service_instance.get("model-version-id"), + persona_model_version=service_instance.get("persona-model-version"), + widget_model_id=service_instance.get("widget-model-id"), + widget_model_version=service_instance.get("widget-model-version"), + bandwith_total=service_instance.get("bandwidth-total"), + vhn_portal_url=service_instance.get("vhn-portal-url"), + service_instance_location_id=service_instance.get("service-instance-location-id"), + resource_version=service_instance.get("resource-version"), + selflink=service_instance.get("selflink"), + orchestration_status=service_instance.get("orchestration-status"), + input_parameters=service_instance.get("input-parameters") + ) + + @classmethod + def get_all_url(cls, customer: "Customer") -> str: # pylint: disable=arguments-differ + """Return url to get all customers. + + Returns: + str: Url to get all customers + + """ + return (f"{cls.base_url}{cls.api_version}/business/customers/" + f"customer/{customer.global_customer_id}/service-subscriptions/") + + @classmethod + def create_from_api_response(cls, + api_response: dict, + customer: "Customer") -> "ServiceSubscription": + """Create service subscription using API response dict. + + Returns: + ServiceSubscription: ServiceSubscription object. + + """ + return cls( + service_type=api_response.get("service-type"), + resource_version=api_response.get("resource-version"), + customer=customer + ) + + @property + def url(self) -> str: + """Cloud region object url. + + URL used to call CloudRegion A&AI API + + Returns: + str: CloudRegion object url + + """ + return ( + f"{self.base_url}{self.api_version}/business/customers/" + f"customer/{self.customer.global_customer_id}/service-subscriptions/" + f"service-subscription/{self.service_type}" + ) + + @property + def service_instances(self) -> Iterator[ServiceInstance]: + """Service instances. + + Yields: + Iterator[ServiceInstance]: Service instance + + """ + for service_instance in \ + self.send_message_json("GET", + (f"Get all service instances for {self.service_type} service " + f"subscription"), + f"{self.url}/service-instances").get("service-instance", []): + yield ServiceInstance( + service_subscription=self, + instance_id=service_instance.get("service-instance-id"), + instance_name=service_instance.get("service-instance-name"), + service_type=service_instance.get("service-type"), + service_role=service_instance.get("service-role"), + environment_context=service_instance.get("environment-context"), + workload_context=service_instance.get("workload-context"), + created_at=service_instance.get("created-at"), + updated_at=service_instance.get("updated-at"), + description=service_instance.get("description"), + model_invariant_id=service_instance.get("model-invariant-id"), + model_version_id=service_instance.get("model-version-id"), + persona_model_version=service_instance.get("persona-model-version"), + widget_model_id=service_instance.get("widget-model-id"), + widget_model_version=service_instance.get("widget-model-version"), + bandwith_total=service_instance.get("bandwidth-total"), + vhn_portal_url=service_instance.get("vhn-portal-url"), + service_instance_location_id=service_instance.get("service-instance-location-id"), + resource_version=service_instance.get("resource-version"), + selflink=service_instance.get("selflink"), + orchestration_status=service_instance.get("orchestration-status"), + input_parameters=service_instance.get("input-parameters") + ) + + @property + def tenant_relationships(self) -> Iterator["Relationship"]: + """Tenant related relationships. + + Iterate through relationships and get related to tenant. + + Yields: + Relationship: Relationship related to tenant. + + """ + for relationship in self.relationships: + if relationship.related_to == "tenant": + yield relationship + + @property + def cloud_region(self) -> "CloudRegion": + """Cloud region associated with service subscription. + + IT'S DEPRECATED! `cloud_regions` parameter SHOULD BE USED + + Raises: + ParameterError: Service subscription has no associated cloud region. + + Returns: + CloudRegion: CloudRegion object + + """ + try: + return next(self.cloud_regions) + except StopIteration: + msg = f"No cloud region for service subscription '{self.name}'" + raise ParameterError(msg) + + @property + def tenant(self) -> "Tenant": + """Tenant associated with service subscription. + + IT'S DEPRECATED! `tenants` parameter SHOULD BE USED + + Raises: + ParameterError: Service subscription has no associated tenants + + Returns: + Tenant: Tenant object + + """ + try: + return next(self.tenants) + except StopIteration: + msg = f"No tenants for service subscription '{self.name}'" + raise ParameterError(msg) + + @property + def _cloud_regions_tenants_data(self) -> Iterator["ServiceSubscriptionCloudRegionTenantData"]: + for relationship in self.tenant_relationships: + cr_tenant_data: ServiceSubscriptionCloudRegionTenantData = \ + ServiceSubscriptionCloudRegionTenantData() + for data in relationship.relationship_data: + if data["relationship-key"] == "cloud-region.cloud-owner": + cr_tenant_data.cloud_owner = data["relationship-value"] + if data["relationship-key"] == "cloud-region.cloud-region-id": + cr_tenant_data.cloud_region_id = data["relationship-value"] + if data["relationship-key"] == "tenant.tenant-id": + cr_tenant_data.tenant_id = data["relationship-value"] + if all([cr_tenant_data.cloud_owner, + cr_tenant_data.cloud_region_id, + cr_tenant_data.tenant_id]): + yield cr_tenant_data + else: + self._logger.error("Invalid tenant relationship: %s", relationship) + + @property + def cloud_regions(self) -> Iterator["CloudRegion"]: + """Cloud regions associated with service subscription. + + Yields: + CloudRegion: CloudRegion object + + """ + cloud_region_set: set = set() + for cr_data in self._cloud_regions_tenants_data: + cloud_region_set.add((cr_data.cloud_owner, cr_data.cloud_region_id)) + for cloud_region_data in cloud_region_set: + try: + yield CloudRegion.get_by_id(cloud_owner=cloud_region_data[0], + cloud_region_id=cloud_region_data[1]) + except ResourceNotFound: + self._logger.error("Can't get cloud region %s %s", cloud_region_data[0], \ + cloud_region_data[1]) + + @property + def tenants(self) -> Iterator["Tenant"]: + """Tenants associated with service subscription. + + Yields: + Tenant: Tenant object + + """ + for cr_data in self._cloud_regions_tenants_data: + try: + cloud_region: CloudRegion = CloudRegion.get_by_id(cr_data.cloud_owner, + cr_data.cloud_region_id) + yield cloud_region.get_tenant(cr_data.tenant_id) + except ResourceNotFound: + self._logger.error("Can't get %s tenant", cr_data.tenant_id) + + def get_service_instance_by_id(self, service_instance_id) -> ServiceInstance: + """Get service instance using it's ID. + + Args: + service_instance_id (str): ID of the service instance + + Returns: + ServiceInstance: ServiceInstance object + + """ + return self._get_service_instance_by_filter_parameter( + "service-instance-id", + service_instance_id + ) + + def get_service_instance_by_name(self, service_instance_name: str) -> ServiceInstance: + """Get service instance using it's name. + + Args: + service_instance_name (str): Name of the service instance + + Returns: + ServiceInstance: ServiceInstance object + + """ + return self._get_service_instance_by_filter_parameter( + "service-instance-name", + service_instance_name + ) + + def link_to_cloud_region_and_tenant(self, + cloud_region: "CloudRegion", + tenant: "Tenant") -> None: + """Create relationship between object and cloud region with tenant. + + Args: + cloud_region (CloudRegion): Cloud region to link to + tenant (Tenant): Cloud region tenant to link to + """ + relationship: Relationship = Relationship( + related_to="tenant", + related_link=tenant.url, + relationship_data=[ + { + "relationship-key": "cloud-region.cloud-owner", + "relationship-value": cloud_region.cloud_owner, + }, + { + "relationship-key": "cloud-region.cloud-region-id", + "relationship-value": cloud_region.cloud_region_id, + }, + { + "relationship-key": "tenant.tenant-id", + "relationship-value": tenant.tenant_id, + }, + ], + related_to_property=[ + {"property-key": "tenant.tenant-name", "property-value": tenant.name} + ], + ) + self.add_relationship(relationship) + + +class Customer(AaiResource): + """Customer class.""" + + def __init__(self, + global_customer_id: str, + subscriber_name: str, + subscriber_type: str, + resource_version: str = None) -> None: + """Initialize Customer class object. + + Args: + global_customer_id (str): Global customer id used across ONAP to + uniquely identify customer. + subscriber_name (str): Subscriber name, an alternate way to retrieve a customer. + subscriber_type (str): Subscriber type, a way to provide VID with + only the INFRA customers. + resource_version (str, optional): Used for optimistic concurrency. + Must be empty on create, valid on update + and delete. Defaults to None. + + """ + super().__init__() + self.global_customer_id: str = global_customer_id + self.subscriber_name: str = subscriber_name + self.subscriber_type: str = subscriber_type + self.resource_version: str = resource_version + + def __repr__(self) -> str: # noqa + """Customer description. + + Returns: + str: Customer object description + + """ + return (f"Customer(global_customer_id={self.global_customer_id}, " + f"subscriber_name={self.subscriber_name}, " + f"subscriber_type={self.subscriber_type}, " + f"resource_version={self.resource_version})") + + def get_service_subscription_by_service_type(self, service_type: str) -> ServiceSubscription: + """Get subscribed service by service type. + + Call a request to get service subscriptions filtered by service-type parameter. + + Args: + service_type (str): Service type + + Returns: + ServiceSubscription: Service subscription + + """ + response: dict = self.send_message_json( + "GET", + f"Get service subscription with {service_type} service type", + (f"{self.base_url}{self.api_version}/business/customers/" + f"customer/{self.global_customer_id}/service-subscriptions" + f"?service-type={service_type}") + ) + return ServiceSubscription.create_from_api_response(response["service-subscription"][0], + self) + + @classmethod + def get_all_url(cls) -> str: # pylint: disable=arguments-differ + """Return an url to get all customers. + + Returns: + str: URL to get all customers + + """ + return f"{cls.base_url}{cls.api_version}/business/customers" + + @classmethod + def get_all(cls, + global_customer_id: str = None, + subscriber_name: str = None, + subscriber_type: str = None) -> Iterator["Customer"]: + """Get all customers. + + Call an API to retrieve all customers. It can be filtered + by global-customer-id, subscriber-name and/or subsriber-type. + + Args: + global_customer_id (str): global-customer-id to filer customers by. Defaults to None. + subscriber_name (str): subscriber-name to filter customers by. Defaults to None. + subscriber_type (str): subscriber-type to filter customers by. Defaults to None. + + """ + filter_parameters: dict = cls.filter_none_key_values( + { + "global-customer-id": global_customer_id, + "subscriber-name": subscriber_name, + "subscriber-type": subscriber_type, + } + ) + url: str = (f"{cls.get_all_url()}?{urlencode(filter_parameters)}") + for customer in cls.send_message_json("GET", "get customers", url).get("customer", []): + yield Customer( + global_customer_id=customer["global-customer-id"], + subscriber_name=customer["subscriber-name"], + subscriber_type=customer["subscriber-type"], + resource_version=customer["resource-version"], + ) + + @classmethod + def get_by_global_customer_id(cls, global_customer_id: str) -> "Customer": + """Get customer by it's global customer id. + + Args: + global_customer_id (str): global customer ID + + Returns: + Customer: Customer with given global_customer_id + + """ + response: dict = cls.send_message_json( + "GET", + f"Get {global_customer_id} customer", + f"{cls.base_url}{cls.api_version}/business/customers/customer/{global_customer_id}" + ) + return Customer( + global_customer_id=response["global-customer-id"], + subscriber_name=response["subscriber-name"], + subscriber_type=response["subscriber-type"], + resource_version=response["resource-version"], + ) + + @classmethod + def create(cls, + global_customer_id: str, + subscriber_name: str, + subscriber_type: str, + service_subscriptions: Optional[Iterable[str]] = None) -> "Customer": + """Create customer. + + Args: + global_customer_id (str): Global customer id used across ONAP + to uniquely identify customer. + subscriber_name (str): Subscriber name, an alternate way + to retrieve a customer. + subscriber_type (str): Subscriber type, a way to provide + VID with only the INFRA customers. + service_subscriptions (Optional[Iterable[str]], optional): Iterable + of service subscription names should be created for newly + created customer. Defaults to None. + + Returns: + Customer: Customer object. + + """ + url: str = ( + f"{cls.base_url}{cls.api_version}/business/customers/" + f"customer/{global_customer_id}" + ) + cls.send_message( + "PUT", + "declare customer", + url, + data=jinja_env() + .get_template("customer_create.json.j2") + .render( + global_customer_id=global_customer_id, + subscriber_name=subscriber_name, + subscriber_type=subscriber_type, + service_subscriptions=service_subscriptions + ), + ) + response: dict = cls.send_message_json( + "GET", "get created customer", url + ) # Call API one more time to get Customer's resource version + return Customer( + global_customer_id=response["global-customer-id"], + subscriber_name=response["subscriber-name"], + subscriber_type=response["subscriber-type"], + resource_version=response["resource-version"], + ) + + @property + def url(self) -> str: + """Return customer object url. + + Unique url address to get customer's data. + + Returns: + str: Customer object url + + """ + return ( + f"{self.base_url}{self.api_version}/business/customers/customer/" + f"{self.global_customer_id}?resource-version={self.resource_version}" + ) + + @property + def service_subscriptions(self) -> Iterator[ServiceSubscription]: + """Service subscriptions of customer resource. + + Yields: + ServiceSubscription: ServiceSubscription object + + """ + try: + response: dict = self.send_message_json( + "GET", + "get customer service subscriptions", + f"{self.base_url}{self.api_version}/business/customers/" + f"customer/{self.global_customer_id}/service-subscriptions" + ) + for service_subscription in response.get("service-subscription", []): + yield ServiceSubscription.create_from_api_response( + service_subscription, + self + ) + except ResourceNotFound as exc: + self._logger.info( + "Subscriptions are not " \ + "found for a customer: %s", exc) + except APIError as exc: + self._logger.error( + "API returned an error: %s", exc) + + def subscribe_service(self, service_type: str) -> "ServiceSubscription": + """Create SDC Service subscription. + + If service subscription with given service_type already exists it won't create + a new resource but use the existing one. + + Args: + service_type (str): Value defined by orchestration to identify this service + across ONAP. + """ + try: + return self.get_service_subscription_by_service_type(service_type) + except ResourceNotFound: + self._logger.info("Create service subscription for %s customer", + self.global_customer_id) + self.send_message( + "PUT", + "Create service subscription", + (f"{self.base_url}{self.api_version}/business/customers/" + f"customer/{self.global_customer_id}/service-subscriptions/" + f"service-subscription/{service_type}") + ) + return self.get_service_subscription_by_service_type(service_type) + + def delete(self) -> None: + """Delete customer. + + Sends request to A&AI to delete customer object. + + """ + self.send_message( + "DELETE", + "Delete customer", + self.url + ) diff --git a/src/onapsdk/aai/business/instance.py b/src/onapsdk/aai/business/instance.py new file mode 100644 index 0000000..146aee9 --- /dev/null +++ b/src/onapsdk/aai/business/instance.py @@ -0,0 +1,55 @@ +"""Base instance module.""" +# Copyright 2022 Orange, 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 ..aai_element import AaiResource + + +class Instance(AaiResource, ABC): + """Abstract instance class.""" + + def __init__(self, # pylint: disable=too-many-arguments + resource_version: str = None, + model_invariant_id: str = None, + model_version_id: str = None) -> None: + """Instance initialization. + + Args: + resource_version (str, optional): Used for optimistic concurrency. + Must be empty on create, valid on update and delete. Defaults to None. + model_invariant_id (str, optional): The ASDC model id for this resource or + service model. Defaults to None. + model_version_id (str, optional): The ASDC model version for this resource or + service model. Defaults to None. + """ + super().__init__() + self.resource_version: str = resource_version + self.model_invariant_id: str = model_invariant_id + self.model_version_id: str = model_version_id + + @abstractmethod + def delete(self, a_la_carte: bool = True) -> "DeletionRequest": + """Create instance deletion request. + + Send request to delete instance + + Args: + a_la_carte (boolean): deletion mode + + Returns: + DeletionRequest: Deletion request + + """ diff --git a/src/onapsdk/aai/business/line_of_business.py b/src/onapsdk/aai/business/line_of_business.py new file mode 100644 index 0000000..61fc0f8 --- /dev/null +++ b/src/onapsdk/aai/business/line_of_business.py @@ -0,0 +1,123 @@ +"""A&AI line of business module.""" +# Copyright 2022 Orange, 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, Iterator + +from onapsdk.utils.jinja import jinja_env + +from ..aai_element import AaiResource + + +class LineOfBusiness(AaiResource): + """Line of business class.""" + + def __init__(self, name: str, resource_version: str) -> None: + """Line of business object initialization. + + Args: + name (str): Line of business name + resource_version (str): resource version + """ + super().__init__() + self.name: str = name + self.resource_version: str = resource_version + + @property + def url(self) -> str: + """Line of business's url. + + Returns: + str: Resource's url + + """ + return (f"{self.base_url}{self.api_version}/business/lines-of-business/" + f"line-of-business/{self.name}") + + @classmethod + def get_all_url(cls) -> str: # pylint: disable=arguments-differ + """Return url to get all lines of business. + + Returns: + str: Url to get all lines of business + + """ + return f"{cls.base_url}{cls.api_version}/business/lines-of-business/" + + def __repr__(self) -> str: + """Line of business object representation. + + Returns: + str: Line of business object representation + + """ + return f"LineOfBusiness(name={self.name})" + + @classmethod + def get_all(cls) -> Iterator["LineOfBusiness"]: + """Get all line of business. + + Yields: + LineOfBusiness: LineOfBusiness object + + """ + url: str = f"{cls.base_url}{cls.api_version}/business/lines-of-business" + for line_of_business in cls.send_message_json("GET", + "Get A&AI lines of business", + url).get("line-of-business", []): + yield cls( + line_of_business.get("line-of-business-name"), + line_of_business.get("resource-version") + ) + + @classmethod + def create(cls, name: str) -> "LineOfBusiness": + """Create line of business A&AI resource. + + Args: + name (str): line of business name + + Returns: + LineOfBusiness: Created LineOfBusiness object + + """ + cls.send_message( + "PUT", + "Declare A&AI line of business", + (f"{cls.base_url}{cls.api_version}/business/lines-of-business/" + f"line-of-business/{name}"), + data=jinja_env().get_template("aai_line_of_business_create.json.j2").render( + line_of_business_name=name + ) + ) + return cls.get_by_name(name) + + @classmethod + def get_by_name(cls, name: str) -> "LineOfBusiness": + """Get line of business resource by it's name. + + Raises: + ResourceNotFound: Line of business requested by a name does not exist. + + Returns: + LineOfBusiness: Line of business requested by a name. + + """ + url = (f"{cls.base_url}{cls.api_version}/business/lines-of-business/" + f"line-of-business/{name}") + response: Dict[str, Any] = \ + cls.send_message_json("GET", + f"Get {name} line of business", + url) + return cls(response["line-of-business-name"], response["resource-version"]) diff --git a/src/onapsdk/aai/business/network.py b/src/onapsdk/aai/business/network.py new file mode 100644 index 0000000..e36cf62 --- /dev/null +++ b/src/onapsdk/aai/business/network.py @@ -0,0 +1,223 @@ +"""Network instance module.""" +# Copyright 2022 Orange, 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 onapsdk.so.deletion import NetworkDeletionRequest + +from .instance import Instance + + +class NetworkInstance(Instance): # pylint: disable=too-many-instance-attributes + """Network instance class.""" + + def __init__(self, # pylint: disable=too-many-arguments, too-many-locals + service_instance: "ServiceInstance", + network_id: str, + is_bound_to_vpn: bool, + is_provider_network: bool, + is_shared_network: bool, + is_external_network: bool, + network_name: str = None, + network_type: str = None, + network_role: str = None, + network_technology: str = None, + neutron_network_id: str = None, + service_id: str = None, + network_role_instance: str = None, + resource_version: str = None, + orchestration_status: str = None, + heat_stack_id: str = None, + mso_catalog_key: str = None, + model_invariant_id: str = None, + contrail_network_fqdn: str = None, + persona_model_version: str = None, + model_version_id: str = None, + model_customization_id: str = None, + widget_model_id: str = None, + physical_network_name: str = None, + widget_model_version: str = None, + selflink: str = None, + operational_status: str = None, + is_trunked: bool = None): + """Network instance object initialization. + + Args: + service_instance (ServiceInstance): Service instance object + network_id (str): Network ID, should be uuid. Unique across A&AI. + is_bound_to_vpn (bool): Set to true if bound to VPN + is_provider_network (bool): boolean indicatating whether or not network + is a provider network. + is_shared_network (bool): boolean indicatating whether + or not network is a shared network. + is_external_network (bool): boolean indicatating whether + or not network is an external network. + network_name (str, optional): Name of the network, governed by some naming convention. + Defaults to None. + network_type (str, optional): Type of the network. Defaults to None. + network_role (str, optional): Role the network. Defaults to None. + network_technology (str, optional): Network technology. Defaults to None. + neutron_network_id (str, optional): Neutron network id of this Interface. + Defaults to None. + service_id (str, optional): Unique identifier of service from ASDC. + Does not strictly map to ASDC services. Defaults to None. + network_role_instance (str, optional): network role instance. Defaults to None. + resource_version (str, optional): Used for optimistic concurrency. + Must be empty on create, valid on update and delete. Defaults to None. + orchestration_status (str, optional): Orchestration status of this VNF, + mastered by MSO. Defaults to None. + heat_stack_id (str, optional): Heat stack id corresponding to this instance, + managed by MSO. Defaults to None. + mso_catalog_key (str, optional): Corresponds to the SDN-C catalog id used to + configure this VCE. Defaults to None. + contrail_network_fqdn (str, optional): Contrail FQDN for the network. Defaults to None. + model_invariant_id (str, optional): the ASDC model id for this resource + or service model. Defaults to None. + model_version_id (str, optional): the ASDC model version for this resource + or service model. Defaults to None. + persona_model_version (str, optional): the ASDC model version for this resource + or service model. Defaults to None. + model_customization_id (str, optional): captures the id of all the configuration + used to customize the resource for the service. Defaults to None. + widget_model_id (str, optional): the ASDC data dictionary widget model. + This maps directly to the A&AI widget. Defaults to None. + widget_model_version (str, optional): the ASDC data dictionary version of + the widget model. This maps directly to the A&AI version of the widget. + Defaults to None. + physical_network_name (str, optional): Name associated with the physical network. + Defaults to None. + selflink (str, optional): Path to the controller object. Defaults to None. + operational_status (str, optional): Indicator for whether the resource is considered + operational. Defaults to None. + is_trunked (bool, optional): Trunked network indication. Defaults to None. + """ + super().__init__(resource_version=resource_version, + model_version_id=model_version_id, + model_invariant_id=model_invariant_id) + self.service_instance: "ServiceInstance" = service_instance + self.network_id: str = network_id + self.is_bound_to_vpn: bool = is_bound_to_vpn + self.is_provider_network: bool = is_provider_network + self.is_shared_network: bool = is_shared_network + self.is_external_network: bool = is_external_network + self.network_name: str = network_name + self.network_type: str = network_type + self.network_role: str = network_role + self.network_technology: str = network_technology + self.neutron_network_id: str = neutron_network_id + self.service_id: str = service_id + self.network_role_instance: str = network_role_instance + self.orchestration_status: str = orchestration_status + self.heat_stack_id: str = heat_stack_id + self.mso_catalog_key: str = mso_catalog_key + self.contrail_network_fqdn: str = contrail_network_fqdn + self.model_customization_id: str = model_customization_id + self.physical_network_name: str = physical_network_name + self.selflink: str = selflink + self.operational_status: str = operational_status + self.is_trunked: bool = is_trunked + self.persona_model_version: str = persona_model_version + self.widget_model_id: str = widget_model_id + self.widget_model_version: str = widget_model_version + + def __repr__(self) -> str: + """Network instance object representation. + + Returns: + str: Human readable network instance representation + + """ + return (f"NetworkInstance(network_id={self.network_id}, " + f"network_name={self.network_name}, " + f"is_bound_to_vpn={self.is_bound_to_vpn}, " + f"is_provider_network={self.is_provider_network}, " + f"is_shared_network={self.is_shared_network}, " + f"is_external_network={self.is_external_network}, " + f"orchestration_status={self.orchestration_status})") + + @classmethod + def get_all_url(cls) -> str: # pylint: disable=arguments-differ + """Return url to get all networks. + + Returns: + str: Url to get all networks + + """ + return f"{cls.base_url}{cls.api_version}/network/l3-networks/" + + @property + def url(self) -> str: + """Network instance url. + + Returns: + str: NetworkInstance url + + """ + return f"{self.base_url}{self.api_version}/network/l3-networks/l3-network/{self.network_id}" + + @classmethod + def create_from_api_response(cls, api_response: dict, + service_instance: "ServiceInstance") -> "NetworkInstance": + """Create network instance object using HTTP API response dictionary. + + Args: + api_response (dict): A&AI API response dictionary + service_instance (ServiceInstance): Service instance with which network is related + + Returns: + VnfInstance: VnfInstance object + + """ + return cls(service_instance=service_instance, + network_id=api_response["network-id"], + is_bound_to_vpn=api_response["is-bound-to-vpn"], + is_provider_network=api_response["is-provider-network"], + is_shared_network=api_response["is-shared-network"], + is_external_network=api_response["is-external-network"], + network_name=api_response.get("network-name"), + network_type=api_response.get("network-type"), + network_role=api_response.get("network-role"), + network_technology=api_response.get("network-technology"), + neutron_network_id=api_response.get("neutron-network-id"), + service_id=api_response.get("service-id"), + network_role_instance=api_response.get("network-role-instance"), + resource_version=api_response.get("resource-version"), + orchestration_status=api_response.get("orchestration-status"), + heat_stack_id=api_response.get("heat-stack-id"), + mso_catalog_key=api_response.get("mso-catalog-key"), + model_invariant_id=api_response.get("model-invariant-id"), + contrail_network_fqdn=api_response.get("contrail-network-fqdn"), + model_version_id=api_response.get("model-version-id"), + model_customization_id=api_response.get("model-customization-id"), + widget_model_id=api_response.get("widget-model-id"), + persona_model_version=api_response.get("persona-model-version"), + physical_network_name=api_response.get("physical-network-name"), + selflink=api_response.get("selflink"), + widget_model_version=api_response.get("widget-model-version"), + operational_status=api_response.get("operational-status"), + is_trunked=api_response.get("is-trunked")) + + def delete(self, a_la_carte: bool = True) -> "NetworkDeletionRequest": + """Create network deletion request. + + Send request to delete network instance + + Args: + a_la_carte (boolean): deletion mode + + Returns: + NetworkDeletionRequest: Deletion request + + """ + self._logger.debug("Delete %s network", self.network_id) + return NetworkDeletionRequest.send_request(self, a_la_carte) diff --git a/src/onapsdk/aai/business/owning_entity.py b/src/onapsdk/aai/business/owning_entity.py new file mode 100644 index 0000000..bf1e7c1 --- /dev/null +++ b/src/onapsdk/aai/business/owning_entity.py @@ -0,0 +1,154 @@ +"""A&AI owning entity module.""" +# Copyright 2022 Orange, 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 uuid import uuid4 +from typing import Iterator + +from onapsdk.utils.jinja import jinja_env +from onapsdk.exceptions import ResourceNotFound + +from ..aai_element import AaiResource + + +class OwningEntity(AaiResource): + """Owning entity class.""" + + def __init__(self, name: str, owning_entity_id: str, resource_version: str) -> None: + """Owning entity object initialization. + + Args: + name (str): Owning entity name + owning_entity_id (str): owning entity ID + resource_version (str): resource version + """ + super().__init__() + self.name: str = name + self.owning_entity_id: str = owning_entity_id + self.resource_version: str = resource_version + + def __repr__(self) -> str: + """Owning entity object representation. + + Returns: + str: Owning entity object representation + + """ + return f"OwningEntity(name={self.name}, owning_entity_id={self.owning_entity_id})" + + @property + def url(self) -> str: + """Owning entity object url. + + Returns: + str: Url + + """ + return (f"{self.base_url}{self.api_version}/business/owning-entities/owning-entity/" + f"{self.owning_entity_id}") + + @classmethod + def get_all_url(cls) -> str: # pylint: disable=arguments-differ + """Return url to get all owning entities. + + Returns: + str: Url to get all owning entities + + """ + return f"{cls.base_url}{cls.api_version}/business/owning-entities" + + @classmethod + def get_all(cls) -> Iterator["OwningEntity"]: + """Get all owning entities. + + Yields: + OwningEntity: OwningEntity object + + """ + url: str = cls.get_all_url() + for owning_entity in cls.send_message_json("GET", + "Get A&AI owning entities", + url).get("owning-entity", []): + yield cls( + owning_entity.get("owning-entity-name"), + owning_entity.get("owning-entity-id"), + owning_entity.get("resource-version") + ) + + @classmethod + def get_by_owning_entity_id(cls, owning_entity_id: str) -> "OwningEntity": + """Get owning entity by it's ID. + + Args: + owning_entity_id (str): owning entity object id + + Returns: + OwningEntity: OwningEntity object + + """ + response: dict = cls.send_message_json( + "GET", + "Get A&AI owning entity", + (f"{cls.base_url}{cls.api_version}/business/owning-entities/" + f"owning-entity/{owning_entity_id}") + ) + return cls( + response.get("owning-entity-name"), + response.get("owning-entity-id"), + response.get("resource-version") + ) + + @classmethod + def get_by_owning_entity_name(cls, owning_entity_name: str) -> "OwningEntity": + """Get owning entity resource by it's name. + + Raises: + ResourceNotFound: Owning entity requested by a name does not exist. + + Returns: + OwningEntity: Owning entity requested by a name. + + """ + for owning_entity in cls.get_all(): + if owning_entity.name == owning_entity_name: + return owning_entity + + msg = f'Owning entity "{owning_entity_name}" does not exist.' + raise ResourceNotFound(msg) + + @classmethod + def create(cls, name: str, owning_entity_id: str = None) -> "OwningEntity": + """Create owning entity A&AI resource. + + Args: + name (str): owning entity name + owning_entity_id (str): owning entity ID. Defaults to None. + + Returns: + OwningEntity: Created OwningEntity object + + """ + if not owning_entity_id: + owning_entity_id = str(uuid4()) + cls.send_message( + "PUT", + "Declare A&AI owning entity", + (f"{cls.base_url}{cls.api_version}/business/owning-entities/" + f"owning-entity/{owning_entity_id}"), + data=jinja_env().get_template("aai_owning_entity_create.json.j2").render( + owning_entity_name=name, + owning_entity_id=owning_entity_id + ) + ) + return cls.get_by_owning_entity_id(owning_entity_id) diff --git a/src/onapsdk/aai/business/platform.py b/src/onapsdk/aai/business/platform.py new file mode 100644 index 0000000..5c12ba8 --- /dev/null +++ b/src/onapsdk/aai/business/platform.py @@ -0,0 +1,123 @@ +"""A&AI platform module.""" +# Copyright 2022 Orange, 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, Iterator + +from onapsdk.utils.jinja import jinja_env + +from ..aai_element import AaiResource + + +class Platform(AaiResource): + """Platform class.""" + + def __init__(self, name: str, resource_version: str) -> None: + """Platform object initialization. + + Args: + name (str): Platform name + resource_version (str): resource version + """ + super().__init__() + self.name: str = name + self.resource_version: str = resource_version + + def __repr__(self) -> str: + """Platform object representation. + + Returns: + str: Platform object representation + + """ + return f"Platform(name={self.name})" + + @property + def url(self) -> str: + """Platform's url. + + Returns: + str: Resource's url + + """ + return (f"{self.base_url}{self.api_version}/business/platforms/" + f"platform/{self.name}") + + @classmethod + def get_all_url(cls) -> str: # pylint: disable=arguments-differ + """Return url to get all platforms. + + Returns: + str: Url to get all platforms + + """ + return f"{cls.base_url}{cls.api_version}/business/platforms" + + @classmethod + def get_all(cls) -> Iterator["Platform"]: + """Get all platform. + + Yields: + Platform: Platform object + + """ + url: str = cls.get_all_url() + for platform in cls.send_message_json("GET", + "Get A&AI platforms", + url).get("platform", []): + yield cls( + platform.get("platform-name"), + platform.get("resource-version") + ) + + @classmethod + def create(cls, name: str) -> "Platform": + """Create platform A&AI resource. + + Args: + name (str): platform name + + Returns: + Platform: Created Platform object + + """ + cls.send_message( + "PUT", + "Declare A&AI platform", + (f"{cls.base_url}{cls.api_version}/business/platforms/" + f"platform/{name}"), + data=jinja_env().get_template("aai_platform_create.json.j2").render( + platform_name=name + ) + ) + return cls.get_by_name(name) + + @classmethod + def get_by_name(cls, name: str) -> "Platform": + """Get platform resource by it's name. + + Raises: + ResourceNotFound: Platform requested by a name does not exist. + + Returns: + Platform: Platform requested by a name. + + """ + url = (f"{cls.base_url}{cls.api_version}/business/platforms/" + f"platform/{name}") + response: Dict[str, Any] = \ + cls.send_message_json("GET", + f"Get {name} platform", + url) + return cls(response["platform-name"], response["resource-version"]) diff --git a/src/onapsdk/aai/business/pnf.py b/src/onapsdk/aai/business/pnf.py new file mode 100644 index 0000000..9061ebf --- /dev/null +++ b/src/onapsdk/aai/business/pnf.py @@ -0,0 +1,267 @@ +"""Pnf instance module.""" +# Copyright 2022 Orange, 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 Iterator, Optional, TYPE_CHECKING + +from onapsdk.exceptions import ResourceNotFound +from .instance import Instance + +if TYPE_CHECKING: + from .service import ServiceInstance # pylint: disable=cyclic-import + +class PnfInstance(Instance): # pylint: disable=too-many-instance-attributes + """Pnf instance class.""" + + def __init__(self, # pylint: disable=too-many-arguments, too-many-locals + service_instance: "ServiceInstance", + pnf_name: str, + in_maint: bool, + selflink: str = None, + pnf_id: str = None, + equip_type: str = None, + equip_vendor: str = None, + equip_model: str = None, + management_option: str = None, + orchestration_status: str = None, + ipaddress_v4_oam: str = None, + sw_version: str = None, + frame_id: str = None, + serial_number: str = None, + ipaddress_v4_loopback_0: str = None, + ipaddress_v6_loopback_0: str = None, + ipaddress_v4_aim: str = None, + ipaddress_v6_aim: str = None, + ipaddress_v6_oam: str = None, + inv_status: str = None, + resource_version: str = None, + prov_status: str = None, + nf_role: str = None, + admin_status: str = None, + operational_status: str = None, + model_customization_id: str = None, + model_invariant_id: str = None, + model_version_id: str = None, + pnf_ipv4_address: str = None, + pnf_ipv6_address: str = None) -> None: + """Pnf instance object initialization. + + Args: + service_instance (ServiceInstance): Service instance object + pnf_name (str): unique name of Physical Network Function + in_maint (bool): Used to indicate whether or not this object is in maintenance mode + (maintenance mode = True). This field (in conjunction with prov_status) + is used to suppress alarms and vSCL on VNFs/VMs. + selflink (str, optional): URL to endpoint where AAI can get more details. + Defaults to None. + pnf_id (str, optional): id of pnf. Defaults to None. + equip_type (str, optional): Equipment type. Source of truth should define valid values. + Defaults to None. + equip_vendor (str, optional): Equipment vendor. Source of truth should define + valid values. Defaults to None. + equip_model (str, optional): Equipment model. Source of truth should define + valid values. Defaults to None. + management_option (str, optional): identifier of managed customer. Defaults to None. + orchestration_status (str, optional): Orchestration status of this pnf. + Defaults to None. + ipaddress_v4_oam (str, optional): ipv4-oam-address with new naming + convention for IP addresses. Defaults to None. + sw_version (str, optional): sw-version is the version of SW for the hosted + application on the PNF. Defaults to None. + frame_id (str, optional): ID of the physical frame (relay rack) where pnf is installed. + Defaults to None. + serial_number (str, optional): Serial number of the device. Defaults to None. + ipaddress_v4_loopback_0 (str, optional): IPV4 Loopback 0 address. Defaults to None. + ipaddress_v6_loopback_0 (str, optional): IPV6 Loopback 0 address. Defaults to None. + ipaddress_v4_aim (str, optional): IPV4 AIM address. Defaults to None. + ipaddress_v6_aim (str, optional): IPV6 AIM address. Defaults to None. + ipaddress_v6_oam (str, optional): IPV6 OAM address. Defaults to None. + inv_status (str, optional): CANOPI's inventory status. Only set with values exactly + as defined by CANOPI. Defaults to None. + resource_version (str, optional): Used for optimistic concurrency. + Must be empty on create, valid on update and delete. Defaults to None. + prov_status (str, optional): Prov Status of this device (not under canopi control) + Valid values [PREPROV/NVTPROV/PROV]. Defaults to None. + nf_role (str, optional): Nf Role is the role performed by this instance in the network. + Defaults to None. + admin_status (str, optional): admin Status of this PNF. Defaults to None. + operational_status (str, optional): Store the operational-status for this object. + Defaults to None. + model_customization_id (str, optional): Store the model-customization-id + for this object. Defaults to None. + model_invariant_id (str, optional): The ASDC model id for this resource model. + Defaults to None. + model_version_id (str, optional): The ASDC model version for this resource model. + Defaults to None. + pnf_ipv4_address (str, optional): This is the IP address (IPv4) for the PNF itself. + This is the IPv4 address that the PNF iself can be accessed at. Defaults to None. + pnf_ipv6_address (str, optional): This is the IP address (IPv6) for the PNF itself. + This is the IPv6 address that the PNF iself can be accessed at. Defaults to None. + """ + super().__init__(resource_version=resource_version, + model_invariant_id=model_invariant_id, + model_version_id=model_version_id) + self.service_instance: "ServiceInstance" = service_instance + self.pnf_name: str = pnf_name + self.in_maint: bool = in_maint + self.selflink: Optional[str] = selflink + self.pnf_id: Optional[str] = pnf_id + self.equip_type: Optional[str] = equip_type + self.equip_vendor: Optional[str] = equip_vendor + self.equip_model: Optional[str] = equip_model + self.management_option: Optional[str] = management_option + self.orchestration_status: Optional[str] = orchestration_status + self.ipaddress_v4_oam: Optional[str] = ipaddress_v4_oam + self.sw_version: Optional[str] = sw_version + self.frame_id: Optional[str] = frame_id + self.serial_number: Optional[str] = serial_number + self.ipaddress_v4_loopback_0: Optional[str] = ipaddress_v4_loopback_0 + self.ipaddress_v6_loopback_0: Optional[str] = ipaddress_v6_loopback_0 + self.ipaddress_v4_aim: Optional[str] = ipaddress_v4_aim + self.ipaddress_v6_aim: Optional[str] = ipaddress_v6_aim + self.ipaddress_v6_oam: Optional[str] = ipaddress_v6_oam + self.inv_status: Optional[str] = inv_status + self.prov_status: Optional[str] = prov_status + self.nf_role: Optional[str] = nf_role + self.admin_status: Optional[str] = admin_status + self.operational_status: Optional[str] = operational_status + self.model_customization_id: Optional[str] = model_customization_id + self.pnf_ipv4_address: Optional[str] = pnf_ipv4_address + self.pnf_ipv6_address: Optional[str] = pnf_ipv6_address + + self._pnf: "Pnf" = None + + def __repr__(self) -> str: + """Pnf instance object representation. + + Returns: + str: Human readable pnf instance representation + + """ + return f"PnfInstance(pnf_name={self.pnf_name})" + + @classmethod + def get_all_url(cls) -> str: # pylint: disable=arguments-differ + """Return an url to get all pnfs. + + Returns: + str: Url to get all pnfs + + """ + return f"{cls.base_url}{cls.api_version}/network/pnfs/" + + @classmethod + def get_all(cls) -> Iterator["PnfInstance"]: + """Get all PNF instances. + + Yields: + PnfInstance: Pnf instance + + """ + for pnf_data in cls.send_message_json( \ + "GET", \ + "Get all pnf instances", \ + cls.get_all_url() \ + ).get("pnf", []): + yield cls.create_from_api_response(pnf_data, None) + + @property + def url(self) -> str: + """Network instance url. + + Returns: + str: NetworkInstance url + + """ + return f"{self.base_url}{self.api_version}/network/pnfs/pnf/{self.pnf_name}" + + @property + def pnf(self) -> "Pnf": + """Pnf associated with that pnf instance. + + Raises: + ResourceNotFound: Could not find PNF for that PNF instance + + Returns: + Pnf: Pnf object associated with Pnf instance + + """ + if not self._pnf: + for pnf in self.service_instance.sdc_service.pnfs: + if pnf.model_version_id == self.model_version_id: + self._pnf = pnf + return self._pnf + + msg = ( + f'Could not find PNF for the PNF instance' + f' with model version ID "{self.model_version_id}"' + ) + raise ResourceNotFound(msg) + return self._pnf + + @classmethod + def create_from_api_response(cls, api_response: dict, + service_instance: "ServiceInstance") -> "PnfInstance": + """Create pnf instance object using HTTP API response dictionary. + + Args: + api_response (dict): A&AI API response dictionary + service_instance (ServiceInstance): Service instance with which network is related + + Returns: + PnfInstance: PnfInstance object + + """ + return cls(service_instance=service_instance, + pnf_name=api_response["pnf-name"], + in_maint=api_response["in-maint"], + selflink=api_response.get("selflink"), + pnf_id=api_response.get("pnf-id"), + equip_type=api_response.get("equip-type"), + equip_vendor=api_response.get("equip-vendor"), + equip_model=api_response.get("equip-model"), + management_option=api_response.get("management-option"), + orchestration_status=api_response.get("orchestration-status"), + ipaddress_v4_oam=api_response.get("ipaddress-v4-oam"), + sw_version=api_response.get("sw-version"), + frame_id=api_response.get("frame-id"), + serial_number=api_response.get("serial-number"), + ipaddress_v4_loopback_0=api_response.get("ipaddress-v4-loopback-0"), + ipaddress_v6_loopback_0=api_response.get("ipaddress-v6-loopback-0"), + ipaddress_v4_aim=api_response.get("ipaddress-v4-aim"), + ipaddress_v6_aim=api_response.get("ipaddress-v6-aim"), + ipaddress_v6_oam=api_response.get("ipaddress-v6-oam"), + inv_status=api_response.get("inv-status"), + resource_version=api_response.get("resource-version"), + prov_status=api_response.get("prov-status"), + nf_role=api_response.get("nf-role"), + admin_status=api_response.get("admin-status"), + operational_status=api_response.get("operational-status"), + model_customization_id=api_response.get("model-customization-id"), + model_invariant_id=api_response.get("model-invariant-id"), + model_version_id=api_response.get("model-version-id"), + pnf_ipv4_address=api_response.get("pnf-ipv4-address"), + pnf_ipv6_address=api_response.get("pnf-ipv6-address")) + + def delete(self, a_la_carte: bool = True) -> None: + """Delete Pnf instance. + + PNF deletion it's just A&AI resource deletion. That's difference between another instances. + You don't have to wait for that task finish, because it's not async task. + + """ + self._logger.debug("Delete %s pnf", self.pnf_name) + self.send_message("DELETE", + f"Delete {self.pnf_name} PNF", + f"{self.url}?resource-version={self.resource_version}") diff --git a/src/onapsdk/aai/business/project.py b/src/onapsdk/aai/business/project.py new file mode 100644 index 0000000..989444a --- /dev/null +++ b/src/onapsdk/aai/business/project.py @@ -0,0 +1,123 @@ +"""A&AI project module.""" +# Copyright 2022 Orange, 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, Iterator + +from onapsdk.utils.jinja import jinja_env + +from ..aai_element import AaiResource + + +class Project(AaiResource): + """Project class.""" + + def __init__(self, name: str, resource_version: str) -> None: + """Project object initialization. + + Args: + name (str): Project name + resource_version (str): resource version + """ + super().__init__() + self.name: str = name + self.resource_version: str = resource_version + + @classmethod + def get_all(cls) -> Iterator["Project"]: + """Get all project. + + Yields: + Project: Project object + + """ + url: str = cls.get_all_url() + for project in cls.send_message_json("GET", + "Get A&AI projects", + url).get("project", []): + yield cls( + project.get("project-name"), + project.get("resource-version") + ) + + @classmethod + def get_all_url(cls) -> str: # pylint: disable=arguments-differ + """Return url to get all projects. + + Returns: + str: Url to get all projects + + """ + return f"{cls.base_url}{cls.api_version}/business/projects" + + def __repr__(self) -> str: + """Project object representation. + + Returns: + str: Project object representation + + """ + return f"Project(name={self.name})" + + @property + def url(self) -> str: + """Project's url. + + Returns: + str: Resource's url + + """ + return (f"{self.base_url}{self.api_version}/business/projects/" + f"project/{self.name}") + + @classmethod + def create(cls, name: str) -> "Project": + """Create project A&AI resource. + + Args: + name (str): project name + + Returns: + Project: Created Project object + + """ + cls.send_message( + "PUT", + "Declare A&AI project", + (f"{cls.base_url}{cls.api_version}/business/projects/" + f"project/{name}"), + data=jinja_env().get_template("aai_project_create.json.j2").render( + project_name=name + ) + ) + return cls.get_by_name(name) + + @classmethod + def get_by_name(cls, name: str) -> "Project": + """Get project resource by it's name. + + Raises: + ResourceNotFound: Project requested by a name does not exist. + + Returns: + Project: Project requested by a name. + + """ + url = (f"{cls.base_url}{cls.api_version}/business/projects/" + f"project/{name}") + response: Dict[str, Any] = \ + cls.send_message_json("GET", + f"Get {name} project", + url) + return cls(response["project-name"], response["resource-version"]) diff --git a/src/onapsdk/aai/business/service.py b/src/onapsdk/aai/business/service.py new file mode 100644 index 0000000..fe3b34d --- /dev/null +++ b/src/onapsdk/aai/business/service.py @@ -0,0 +1,484 @@ +"""Service instance module.""" +# Copyright 2022 Orange, 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 Iterator, Type, Union, Iterable, Optional + +from onapsdk.exceptions import StatusError, ParameterError +from onapsdk.sdc.service import Service +from onapsdk.so.deletion import ServiceDeletionRequest +from onapsdk.so.instantiation import NetworkInstantiation, VnfInstantiation +from onapsdk.utils.jinja import jinja_env + +from .instance import Instance +from .network import NetworkInstance +from .pnf import PnfInstance +from .vnf import VnfInstance + + +class ServiceInstance(Instance): # pylint: disable=too-many-instance-attributes + """Service instanve class.""" + + def __init__(self, # pylint: disable=too-many-arguments, too-many-locals + service_subscription: "ServiceSubscription", + instance_id: str, + instance_name: str = None, + service_type: str = None, + service_role: str = None, + environment_context: str = None, + workload_context: str = None, + created_at: str = None, + updated_at: str = None, + resource_version: str = None, + description: str = None, + model_invariant_id: str = None, + model_version_id: str = None, + persona_model_version: str = None, + widget_model_id: str = None, + widget_model_version: str = None, + bandwith_total: str = None, + vhn_portal_url: str = None, + service_instance_location_id: str = None, + selflink: str = None, + orchestration_status: str = None, + input_parameters: str = None) -> None: + """Service instance object initialization. + + Args: + service_subscription (ServiceSubscription): service subscription which is belongs to + instance_id (str): Uniquely identifies this instance of a service + instance_name (str, optional): This field will store a name assigned to + the service-instance. Defaults to None. + service_type (str, optional): String capturing type of service. Defaults to None. + service_role (str, optional): String capturing the service role. Defaults to None. + environment_context (str, optional): This field will store the environment context + assigned to the service-instance. Defaults to None. + workload_context (str, optional): This field will store the workload context assigned to + the service-instance. Defaults to None. + created_at (str, optional): Create time of Network Service. Defaults to None. + updated_at (str, optional): Last update of Network Service. Defaults to None. + description (str, optional): Short description for service-instance. Defaults to None. + model_invariant_id (str, optional): The ASDC model id for this resource or + service model. Defaults to None. + model_version_id (str, optional): The ASDC model version for this resource or + service model. Defaults to None. + persona_model_version (str, optional): The ASDC model version for this resource or + service model. Defaults to None. + widget_model_id (str, optional): The ASDC data dictionary widget model. This maps + directly to the A&AI widget. Defaults to None. + widget_model_version (str, optional): The ASDC data dictionary version of the widget + model. This maps directly to the A&AI version of the widget. Defaults to None. + bandwith_total (str, optional): Indicates the total bandwidth to be used for this + service. Defaults to None. + vhn_portal_url (str, optional): URL customers will use to access the vHN Portal. + Defaults to None. + service_instance_location_id (str, optional): An identifier that customers assign to + the location where this service is being used. Defaults to None. + resource_version (str, optional): Used for optimistic concurrency. Must be empty on + create, valid on update and delete. Defaults to None. + selflink (str, optional): Path to the controller object. Defaults to None. + orchestration_status (str, optional): Orchestration status of this service. + Defaults to None. + input_parameters (str, optional): String capturing request parameters from SO to + pass to Closed Loop. Defaults to None. + """ + super().__init__(resource_version=resource_version, + model_invariant_id=model_invariant_id, + model_version_id=model_version_id) + self.service_subscription: "ServiceSubscription" = service_subscription + self.instance_id: str = instance_id + self.instance_name: str = instance_name + self.service_type: str = service_type + self.service_role: str = service_role + self.environment_context: str = environment_context + self.workload_context: str = workload_context + self.created_at: str = created_at + self.updated_at: str = updated_at + self.description: str = description + self.bandwith_total: str = bandwith_total + self.vhn_portal_url: str = vhn_portal_url + self.service_instance_location_id: str = service_instance_location_id + self.selflink: str = selflink + self.orchestration_status: str = orchestration_status + self.input_parameters: str = input_parameters + self.persona_model_version: str = persona_model_version + self.widget_model_id: str = widget_model_id + self.widget_model_version: str = widget_model_version + self._sdc_service: Optional[Service] = None + + def __repr__(self) -> str: + """Service instance object representation. + + Returns: + str: Human readable service instance representation + + """ + return (f"ServiceInstance(instance_id={self.instance_id}, " + f"instance_name={self.instance_name})") + + def _get_related_instance(self, + related_instance_class: Union[Type[NetworkInstance], + Type[VnfInstance]], + relationship_related_to_type: str) -> Iterator[\ + Union[NetworkInstance, + VnfInstance]]: + """Iterate through related service instances. + + This is method which for given `relationship_related_to_type` creates iterator + it iterate through objects which are related with service. + + Args: + related_instance_class (Union[Type[NetworkInstance], Type[VnfInstance]]): Class object + to create required object instances + relationship_related_to_type (str): Has to be "generic-vnf" or "l3-network" + + Raises: + ParameterError: relationship_related_to_type does not satisfy the requirements + + Yields: + Iterator[ Union[NetworkInstance, VnfInstance]]: [description] + + """ + if not relationship_related_to_type in ["l3-network", "generic-vnf", "pnf"]: + msg = ( + f'Invalid "relationship_related_to_type" value. ' + f'Provided "{relationship_related_to_type}". ' + f'Has to be "l3-network" or "generic-vnf".' + ) + raise ParameterError(msg) + for relationship in self.relationships: + if relationship.related_to == relationship_related_to_type: + yield related_instance_class.create_from_api_response(\ + self.send_message_json("GET", + (f"Get {self.instance_id} " + f"{related_instance_class.__class__.__name__}"), + f"{self.base_url}{relationship.related_link}"), + self) + + @classmethod + def create(cls, service_subscription: "ServiceSubscription", # pylint: disable=too-many-arguments, too-many-locals + instance_id: str, + instance_name: str = None, + service_type: str = None, + service_role: str = None, + environment_context: str = None, + workload_context: str = None, + created_at: str = None, + updated_at: str = None, + resource_version: str = None, + description: str = None, + model_invariant_id: str = None, + model_version_id: str = None, + persona_model_version: str = None, + widget_model_id: str = None, + widget_model_version: str = None, + bandwith_total: str = None, + vhn_portal_url: str = None, + service_instance_location_id: str = None, + selflink: str = None, + orchestration_status: str = None, + input_parameters: str = None): + """Service instance creation. + + Args: + service_subscription (ServiceSubscription): service subscription which is belongs to + instance_id (str): Uniquely identifies this instance of a service + instance_name (str, optional): This field will store a name assigned to + the service-instance. Defaults to None. + service_type (str, optional): String capturing type of service. Defaults to None. + service_role (str, optional): String capturing the service role. Defaults to None. + environment_context (str, optional): This field will store the environment context + assigned to the service-instance. Defaults to None. + workload_context (str, optional): This field will store the workload context assigned to + the service-instance. Defaults to None. + created_at (str, optional): Create time of Network Service. Defaults to None. + updated_at (str, optional): Last update of Network Service. Defaults to None. + description (str, optional): Short description for service-instance. Defaults to None. + model_invariant_id (str, optional): The ASDC model id for this resource or + service model. Defaults to None. + model_version_id (str, optional): The ASDC model version for this resource or + service model. Defaults to None. + persona_model_version (str, optional): The ASDC model version for this resource or + service model. Defaults to None. + widget_model_id (str, optional): The ASDC data dictionary widget model. This maps + directly to the A&AI widget. Defaults to None. + widget_model_version (str, optional): The ASDC data dictionary version of the widget + model. This maps directly to the A&AI version of the widget. Defaults to None. + bandwith_total (str, optional): Indicates the total bandwidth to be used for this + service. Defaults to None. + vhn_portal_url (str, optional): URL customers will use to access the vHN Portal. + Defaults to None. + service_instance_location_id (str, optional): An identifier that customers assign to + the location where this service is being used. Defaults to None. + resource_version (str, optional): Used for optimistic concurrency. Must be empty on + create, valid on update and delete. Defaults to None. + selflink (str, optional): Path to the controller object. Defaults to None. + orchestration_status (str, optional): Orchestration status of this service. + Defaults to None. + input_parameters (str, optional): String capturing request parameters from SO to + pass to Closed Loop. Defaults to None. + """ + service_instance: "ServiceInstance" = cls( + service_subscription, + instance_id, + instance_name, + service_type, + service_role, + environment_context, + workload_context, + created_at, + updated_at, + resource_version, + description, + model_invariant_id, + model_version_id, + persona_model_version, + widget_model_id, + widget_model_version, + bandwith_total, + vhn_portal_url, + service_instance_location_id, + selflink, + orchestration_status, + input_parameters + ) + cls.send_message("PUT", + f"Create service instance {instance_id} for "\ + f"{service_subscription.service_type} service subscription", + f"{service_subscription.url}/service-instances/service-instance/"\ + f"{instance_id}", + data=jinja_env() + .get_template("aai_service_instance_create.json.j2") + .render( + service_instance=service_instance + )) + return service_instance + + @classmethod + def get_all_url(cls, service_subscription: "ServiceSubscription") -> str: # pylint: disable=arguments-differ + """Return an url to get all service instances for service subscription. + + Args: + service_subscription (ServiceSubscription): Service subscription object + + Returns: + str: Url to get all service instances for service subscription + + """ + return f"{service_subscription.url}/service-instances/" + + @property + def url(self) -> str: + """Service instance resource URL. + + Returns: + str: Service instance url + + """ + return ( + f"{self.service_subscription.url}/service-instances/service-instance/{self.instance_id}" + ) + + @property + def vnf_instances(self) -> Iterator[VnfInstance]: + """Vnf instances associated with service instance. + + Returns iterator of VnfInstances representing VNF instantiated for that service + + Yields: + VnfInstance: VnfInstance object + + """ + return self._get_related_instance(VnfInstance, "generic-vnf") + + @property + def network_instances(self) -> Iterator[NetworkInstance]: + """Network instances associated with service instance. + + Returns iterator of NetworkInstance representing network instantiated for that service + + Yields: + NetworkInstance: NetworkInstance object + + """ + return self._get_related_instance(NetworkInstance, "l3-network") + + @property + def pnfs(self) -> Iterator[PnfInstance]: + """Pnfs associated with service instance. + + Returns iterator of PnfInstance representing pnfs instantiated for that service + + Yields: + PnfInstance: PnfInstance object + + """ + return self._get_related_instance(PnfInstance, "pnf") + + @property + def sdc_service(self) -> Service: + """Sdc service related with that instance. + + Sdc service model which was used to create that instance. + + Raises: + ResourceNotFound: Service model not found + + """ + if not self._sdc_service: + self._sdc_service = Service.get_by_unique_uuid(self.model_invariant_id) + return self._sdc_service + + @property + def active(self) -> bool: + """Information if service instance's orchestration status is active.""" + return self.orchestration_status == "Active" + + def add_vnf(self, # pylint: disable=too-many-arguments + vnf: "Vnf", + line_of_business: "LineOfBusiness", + platform: "Platform", + cloud_region: "CloudRegion" = None, + tenant: "Tenant" = None, + vnf_instance_name: str = None, + vnf_parameters: Iterable["InstantiationParameter"] = None, + so_vnf: "SoServiceVnf" = None, + a_la_carte: bool = True + ) -> "VnfInstantiation": + """Add vnf into service instance. + + Instantiate VNF. + + Args: + vnf (Vnf): Vnf from service configuration to instantiate + line_of_business (LineOfBusiness): LineOfBusiness to use in instantiation request + platform (Platform): Platform to use in instantiation request + cloud_region (CloudRegion, optional): Cloud region to use in instantiation request. + Defaults to None. + THAT PROPERTY WILL BE REQUIRED IN ONE OF THE FUTURE RELEASE. REFACTOR YOUR CODE + TO USE IT!. + tenant (Tenant, optional): Tenant to use in instnatiation request. + Defaults to None. + THAT PROPERTY WILL BE REQUIRED IN ONE OF THE FUTURE RELEASE. REFACTOR YOUR CODE + TO USE IT!. + vnf_instance_name (str, optional): VNF instantion name. + If no value is provided it's going to be + "Python_ONAP_SDK_vnf_instance_{str(uuid4())}". + Defaults to None. + vnf_parameters (Iterable[InstantiationParameter], optional): InstantiationParameter to + be passed as "userParams". Defaults to None. + so_vnf: (SoServiceVnf, optional): object with vnf instance parameters. Defaults to None. + a_la_carte (bool): instantiation type for vnf. Defaults to True. + + Raises: + StatusError: Service orchestration status is not "Active". + + Returns: + VnfInstantiation: VnfInstantiation request object + + """ + if not self.active: + raise StatusError('Service orchestration status must be "Active"') + + if a_la_carte: + return VnfInstantiation.instantiate_ala_carte( + self, + vnf, + line_of_business, + platform, + cloud_region=cloud_region, + tenant=tenant, + vnf_instance_name=vnf_instance_name, + vnf_parameters=vnf_parameters, + sdc_service=self.sdc_service + ) + + return VnfInstantiation.instantiate_macro( + self, + vnf, + line_of_business, + platform, + cloud_region=cloud_region, + tenant=tenant, + vnf_instance_name=vnf_instance_name, + vnf_parameters=vnf_parameters, + so_vnf=so_vnf, + sdc_service=self.sdc_service + ) + + def add_network(self, # pylint: disable=too-many-arguments + network: "Network", + line_of_business: "LineOfBusiness", + platform: "Platform", + cloud_region: "CloudRegion" = None, + tenant: "Tenant" = None, + network_instance_name: str = None, + subnets: Iterator["Subnet"] = None) -> "NetworkInstantiation": + """Add network into service instance. + + Instantiate vl. + + Args: + network (Network): Network from service configuration to instantiate + line_of_business (LineOfBusiness): LineOfBusiness to use in instantiation request + platform (Platform): Platform to use in instantiation request + cloud_region (CloudRegion, optional): Cloud region to use in instantiation request. + Defaults to None. + THAT PROPERTY WILL BE REQUIRED IN ONE OF THE FUTURE RELEASE. REFACTOR YOUR CODE + TO USE IT!. + tenant (Tenant, optional): Tenant to use in instnatiation request. + Defaults to None. + THAT PROPERTY WILL BE REQUIRED IN ONE OF THE FUTURE RELEASE. REFACTOR YOUR CODE + TO USE IT!. + network_instance_name (str, optional): Network instantion name. + If no value is provided it's going to be + "Python_ONAP_SDK_network_instance_{str(uuid4())}". + Defaults to None. + + Raises: + StatusError: Service orchestration status is not "Active" + + Returns: + NetworkInstantiation: NetworkInstantiation request object + + """ + if not self.active: + msg = f'Service orchestration status must be "Active"' + raise StatusError(msg) + + return NetworkInstantiation.instantiate_ala_carte( + self, + network, + line_of_business, + platform, + cloud_region=cloud_region, + tenant=tenant, + network_instance_name=network_instance_name, + subnets=subnets + ) + + def delete(self, a_la_carte: bool = True) -> "ServiceDeletionRequest": + """Create service deletion request. + + Send a request to delete service instance + + Args: + a_la_carte (boolean): deletion mode + + Returns: + ServiceDeletionRequest: Deletion request object + + """ + self._logger.debug("Delete %s service instance", self.instance_id) + return ServiceDeletionRequest.send_request(self, a_la_carte) diff --git a/src/onapsdk/aai/business/sp_partner.py b/src/onapsdk/aai/business/sp_partner.py new file mode 100644 index 0000000..05d6a05 --- /dev/null +++ b/src/onapsdk/aai/business/sp_partner.py @@ -0,0 +1,176 @@ +"""A&AI sp-partner module.""" +# Copyright 2022 Orange, 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 Iterator, Optional + +from onapsdk.utils.jinja import jinja_env + +from ..aai_element import AaiResource + + +class SpPartner(AaiResource): # pylint: disable=too-many-instance-attributes + """Sp partner class.""" + + def __init__(self, sp_partner_id: str, resource_version: str, url: str = None, # pylint: disable=too-many-arguments, too-many-locals + callsource: str = None, operational_status: str = None, + model_customization_id: str = None, model_invariant_id: str = None, + model_version_id: str = None) -> None: + """Sp partner object initialization. + + Args: + sp_partner_id (str): Uniquely identifies this sp-partner by id + resource_version (str): resource version + url (str, optional): Store the URL of this sp-partner. Defaults to None + callsource (str, optional): Store the callsource of this sp-partner. Defaults to None + operational_status (str, optional): Store the operational-status of this sp-partner. + Defaults to None + model_customization_id (str, optional): Store the model-customization-id + of this sp-partner. Defaults to None + model_invariant_id (str, optional): The ASDC model id for this sp-partner model. + Defaults to None + model_version_id (str, optional): The ASDC model version for this sp-partner model. + Defaults to None + + """ + super().__init__() + self.sp_partner_id: str = sp_partner_id + self.resource_version: str = resource_version + self.sp_partner_url: Optional[str] = url + self.callsource: Optional[str] = callsource + self.operational_status: Optional[str] = operational_status + self.model_customization_id: Optional[str] = model_customization_id + self.model_invariant_id: Optional[str] = model_invariant_id + self.model_version_id: Optional[str] = model_version_id + + def __repr__(self) -> str: + """Sp partner object representation. + + Returns: + str: SpPartner object representation + + """ + return f"SpPartner(sp_partner_id={self.sp_partner_id})" + + @property + def url(self) -> str: + """Sp partner's url. + + Returns: + str: Resource's url + + """ + return (f"{self.base_url}{self.api_version}/business/sp-partners/" + f"sp-partner/{self.sp_partner_id}") + + @classmethod + def get_all_url(cls) -> str: # pylint: disable=arguments-differ + """Return url to get all sp partners. + + Returns: + str: Url to get all sp partners + + """ + return f"{cls.base_url}{cls.api_version}/business/sp-partners" + + @classmethod + def get_all(cls) -> Iterator["SpPartner"]: + """Get all sp partners. + + Yields: + SpPartner: SpPartner object + + """ + url: str = cls.get_all_url() + for sp_partner in cls.send_message_json("GET", + "Get A&AI sp-partners", + url).get("sp-partner", []): + yield cls( + sp_partner["sp-partner-id"], + sp_partner["resource-version"], + sp_partner.get("url"), + sp_partner.get("callsource"), + sp_partner.get("operational-status"), + sp_partner.get("model-customization-id"), + sp_partner.get("model-invariant-id"), + sp_partner.get("model-version-id"), + ) + + @classmethod + def create(cls, sp_partner_id: str, url: str = "", callsource: str = "", # pylint: disable=too-many-arguments + operational_status: str = "", model_customization_id: str = "", + model_invariant_id: str = "", model_version_id: str = "") -> "SpPartner": + """Create sp partner A&AI resource. + + Args: + sp_partner_id (str): sp partner unique ID + url (str, optional): Store the URL of this sp-partner. Defaults to None + callsource (str, optional): Store the callsource of this sp-partner. Defaults to None + operational_status (str, optional): Store the operational-status of this sp-partner. + Defaults to None + model_customization_id (str, optional): Store the model-customization-id + of this sp-partner. Defaults to None + model_invariant_id (str, optional): The ASDC model id for this sp-partner model. + Defaults to None + model_version_id (str, optional): The ASDC model version for this sp-partner model. + Defaults to None + + Returns: + SpPartner: Created SpPartner object + + """ + cls.send_message( + "PUT", + "Declare A&AI sp partner", + (f"{cls.base_url}{cls.api_version}/business/sp-partners/" + f"sp-partner/{sp_partner_id}"), + data=jinja_env().get_template("aai_sp_partner_create.json.j2").render( + sp_partner_id=sp_partner_id, + url=url, + callsource=callsource, + operational_status=operational_status, + model_customization_id=model_customization_id, + model_invariant_id=model_invariant_id, + model_version_id=model_version_id + ) + ) + return cls.get_by_sp_partner_id(sp_partner_id) + + @classmethod + def get_by_sp_partner_id(cls, sp_partner_id: str) -> "SpPartner": + """Get sp partner by it's ID. + + Args: + sp_partner_id (str): sp partner object id + + Returns: + SpPartner: SpPartner object + + """ + response: dict = cls.send_message_json( + "GET", + "Get A&AI sp partner", + (f"{cls.base_url}{cls.api_version}/business/sp-partners/" + f"sp-partner/{sp_partner_id}") + ) + return cls( + response["sp-partner-id"], + response["resource-version"], + response.get("url"), + response.get("callsource"), + response.get("operational-status"), + response.get("model-customization-id"), + response.get("model-invariant-id"), + response.get("model-version-id") + ) diff --git a/src/onapsdk/aai/business/vf_module.py b/src/onapsdk/aai/business/vf_module.py new file mode 100644 index 0000000..ac91560 --- /dev/null +++ b/src/onapsdk/aai/business/vf_module.py @@ -0,0 +1,199 @@ +"""VF module instance.""" +# Copyright 2022 Orange, 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 onapsdk.so.deletion import VfModuleDeletionRequest +from onapsdk.exceptions import ResourceNotFound + +from .instance import Instance + + +class VfModuleInstance(Instance): # pylint: disable=too-many-instance-attributes + """Vf module instance class.""" + + def __init__(self, # pylint: disable=too-many-arguments, too-many-locals + vnf_instance: "VnfInstance", + vf_module_id: str, + is_base_vf_module: bool, + automated_assignment: bool, + vf_module_name: str = None, + heat_stack_id: str = None, + resource_version: str = None, + model_invariant_id: str = None, + orchestration_status: str = None, + persona_model_version: str = None, + model_version_id: str = None, + model_customization_id: str = None, + widget_model_id: str = None, + widget_model_version: str = None, + contrail_service_instance_fqdn: str = None, + module_index: int = None, + selflink: str = None) -> None: + """Vf module initialization. + + Args: + vnf_instance (VnfInstance): VnfInstance + vf_module_id (str): Unique ID of vf-module + is_base_vf_module (bool): used to indicate whether or not this object is base vf module + automated_assignment (bool): ndicates whether vf-module assignment was done via + automation or manually + vf_module_name (str, optional): Name of vf-module. Defaults to None. + heat_stack_id (str, optional): Heat stack id corresponding to this instance. + Defaults to None. + orchestration_status (str, optional): orchestration status of this vf-module, + mastered by MSO. Defaults to None. + resource_version (str, optional): Used for optimistic concurrency. + Must be empty on create, valid on update and delete. Defaults to None. + model_invariant_id (str, optional): the ASDC model id for this resource or + service model. Defaults to None. + model_version_id (str, optional): the ASDC model version for this resource or + service model. Defaults to None. + persona_model_version (str, optional): the ASDC model version for this resource or + service model. Defaults to None. + model_customization_id (str, optional): captures the id of all the configuration + used to customize the resource for the service. Defaults to None. + widget_model_id (str, optional): the ASDC data dictionary widget model. + This maps directly to the A&AI widget. Defaults to None. + widget_model_version (str, optional): the ASDC data dictionary version of + the widget model. This maps directly to the A&AI version of the widget. + Defaults to None. + contrail_service_instance_fqdn (str, optional): the Contrail unique ID + for a service-instance. Defaults to None. + module_index (int, optional): the index will track the number of modules + of a given type that have been deployed in a VNF, starting with 0, + and always choosing the lowest available digit. Defaults to None. + selflink (str, optional): Path to the controller object. Defaults to None. + """ + super().__init__(resource_version=resource_version, model_version_id=model_version_id, + model_invariant_id=model_invariant_id) + self.vnf_instance: "VnfInstance" = vnf_instance + self.vf_module_id: str = vf_module_id + self.is_base_vf_module: bool = is_base_vf_module + self.automated_assignment: bool = automated_assignment + self.vf_module_name: str = vf_module_name + self.heat_stack_id: str = heat_stack_id + self.orchestration_status: str = orchestration_status + self.model_customization_id: str = model_customization_id + self.contrail_service_instance_fqdn: str = contrail_service_instance_fqdn + self.module_index: int = module_index + self.selflink: str = selflink + self.persona_model_version: str = persona_model_version + self.widget_model_id: str = widget_model_id + self.widget_model_version: str = widget_model_version + + self._vf_module: "VfModule" = None + + def __repr__(self) -> str: + """Object represetation. + + Returns: + str: Human readble VfModuleInstance representation + + """ + return (f"VfModuleInstance(vf_module_id={self.vf_module_id}, " + f"is_base_vf_module={self.is_base_vf_module}, " + f"automated_assignment={self.automated_assignment})") + + @classmethod + def get_all_url(cls, vnf_instance: "VnfInstance") -> str: # pylint: disable=arguments-differ + """Return url to get all vf modules for vnf instance. + + Args: + vnf_instance (VnfInstance): VNF instance object + + Returns: + str: Url to get all vf modules for vnf instance + + """ + return f"{vnf_instance.url}/vf-modules/" + + @property + def url(self) -> str: + """Resource url. + + Returns: + str: VfModuleInstance url + + """ + return f"{self.vnf_instance.url}/vf-modules/vf-module/{self.vf_module_id}" + + @property + def vf_module(self) -> "VfModule": + """Vf module associated with that vf module instance. + + Returns: + VfModule: VfModule object associated with vf module instance + + """ + if not self._vf_module: + for vf_module in self.vnf_instance.vnf.vf_modules: + if vf_module.model_version_id == self.model_version_id: + self._vf_module = vf_module + return self._vf_module + + msg = ( + f'Could not find VF modules for the VF Module instance' + f' with model version ID "{self.model_version_id}"' + ) + raise ResourceNotFound(msg) + return self._vf_module + + @classmethod + def create_from_api_response(cls, + api_response: dict, + vnf_instance: "VnfInstance") -> "VfModuleInstance": + """Create vf module instance object using HTTP API response dictionary. + + Args: + api_response (dict): HTTP API response content + vnf_instance (VnfInstance): VnfInstance associated with VfModuleInstance + + Returns: + VfModuleInstance: VfModuleInstance object + + """ + return cls( + vnf_instance=vnf_instance, + vf_module_id=api_response.get("vf-module-id"), + is_base_vf_module=api_response.get("is-base-vf-module"), + automated_assignment=api_response.get("automated-assignment"), + vf_module_name=api_response.get("vf-module-name"), + heat_stack_id=api_response.get("heat-stack-id"), + orchestration_status=api_response.get("orchestration-status"), + resource_version=api_response.get("resource-version"), + model_invariant_id=api_response.get("model-invariant-id"), + model_version_id=api_response.get("model-version-id"), + persona_model_version=api_response.get("persona-model-version"), + model_customization_id=api_response.get("model-customization-id"), + widget_model_id=api_response.get("widget-model-id"), + widget_model_version=api_response.get("widget-model-version"), + contrail_service_instance_fqdn=api_response.get("contrail-service-instance-fqdn"), + module_index=api_response.get("module-index"), + selflink=api_response.get("selflink") + ) + + def delete(self, a_la_carte: bool = True) -> "VfModuleDeletionRequest": + """Create deletion request. + + Send request to delete VF module instance + + Args: + a_la_carte (boolean): deletion mode + + Returns: + VfModuleDeletionRequest: Deletion request object + + """ + self._logger.debug("Delete %s VF module", self.vf_module_id) + return VfModuleDeletionRequest.send_request(self, a_la_carte) diff --git a/src/onapsdk/aai/business/vnf.py b/src/onapsdk/aai/business/vnf.py new file mode 100644 index 0000000..2045291 --- /dev/null +++ b/src/onapsdk/aai/business/vnf.py @@ -0,0 +1,536 @@ +"""Vnf instance module.""" +# Copyright 2022 Orange, 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 Iterable, Iterator + +from onapsdk.exceptions import ResourceNotFound, StatusError +from onapsdk.so.deletion import VnfDeletionRequest +from onapsdk.so.instantiation import VfModuleInstantiation, VnfInstantiation, SoService, \ + InstantiationParameter, VnfOperation +from onapsdk.configuration import settings + +from .instance import Instance +from .vf_module import VfModuleInstance + + +class VnfInstance(Instance): # pylint: disable=too-many-instance-attributes + """VNF Instance class.""" + + def __init__(self, # pylint: disable=too-many-arguments, too-many-locals + service_instance: "ServiceInstance", + vnf_id: str, + vnf_type: str, + in_maint: bool, + is_closed_loop_disabled: bool, + vnf_name: str = None, + service_id: str = None, + regional_resource_zone: str = None, + prov_status: str = None, + operational_status: str = None, + equipment_role: str = None, + orchestration_status: str = None, + vnf_package_name: str = None, + vnf_discriptor_name: str = None, + job_id: str = None, + heat_stack_id: str = None, + mso_catalog_key: str = None, + management_option: str = None, + ipv4_oam_address: str = None, + ipv4_loopback0_address: str = None, + nm_lan_v6_address: str = None, + management_v6_address: str = None, + vcpu: int = None, + vcpu_units: str = None, + vmemory: int = None, + vmemory_units: str = None, + vdisk: int = None, + vdisk_units: str = None, + nshd: int = None, + nvm: int = None, + nnet: int = None, + resource_version: str = None, + encrypted_access_flag: bool = None, + model_invariant_id: str = None, + model_version_id: str = None, + persona_model_version: str = None, + model_customization_id: str = None, + widget_model_id: str = None, + widget_model_version: str = None, + as_number: str = None, + regional_resource_subzone: str = None, + nf_type: str = None, + nf_function: str = None, + nf_role: str = None, + nf_naming_code: str = None, + selflink: str = None, + ipv4_oam_gateway_address: str = None, + ipv4_oam_gateway_address_prefix_length: int = None, + vlan_id_outer: int = None, + nm_profile_name: str = None) -> None: + """Vnf instance object initialization. + + Args: + vnf_id (str): Unique id of VNF. This is unique across the graph. + vnf_type (str): String capturing type of vnf, that was intended to identify + the ASDC resource. This field has been overloaded in service-specific ways and + clients should expect changes to occur in the future to this field as ECOMP + matures. + in_maint (bool): used to indicate whether or not this object is in maintenance mode + (maintenance mode = true). This field (in conjunction with prov-status) + is used to suppress alarms and vSCL on VNFs/VMs. + is_closed_loop_disabled (bool): used to indicate whether closed loop function is + enabled on this node + vnf_name (str, optional): Name of VNF. Defaults to None. + service_id (str, optional): Unique identifier of service, does not necessarily map to + ASDC service models. Defaults to None. + regional_resource_zone (str, optional): Regional way of organizing pservers, source of + truth should define values. Defaults to None. + prov_status (str, optional): Trigger for operational monitoring of this resource by + Service Assurance systems. Defaults to None. + operational_status (str, optional): Indicator for whether the resource is considered + operational. Valid values are in-service-path and out-of-service-path. + Defaults to None. + equipment_role (str, optional): Client should send valid enumerated value. + Defaults to None. + orchestration_status (str, optional): Orchestration status of this VNF, used by MSO. + Defaults to None. + vnf_package_name (str, optional): vnf package name. Defaults to None. + vnf_discriptor_name (str, optional): vnf discriptor name. Defaults to None. + job_id (str, optional): job id corresponding to vnf. Defaults to None. + heat_stack_id (str, optional): Heat stack id corresponding to this instance, + managed by MSO. Defaults to None. + mso_catalog_key (str, optional): Corresponds to the SDN-C catalog id used to + configure this VCE. Defaults to None. + management_option (str, optional): identifier of managed customer. Defaults to None. + ipv4_oam_address (str, optional): Address tail-f uses to configure generic-vnf, + also used for troubleshooting and is IP used for traps generated by generic-vnf. + Defaults to None. + ipv4_loopback0_address (str, optional): v4 Loopback0 address. Defaults to None. + nm_lan_v6_address (str, optional): v6 Loopback address. Defaults to None. + management_v6_address (str, optional): v6 management address. Defaults to None. + vcpu (int, optional): number of vcpus ordered for this instance of VNF, + used for VNFs with no vservers/flavors, to be used only by uCPE. Defaults to None. + vcpu_units (str, optional): units associated with vcpu, used for VNFs with no + vservers/flavors, to be used only by uCPE. Defaults to None. + vmemory (int, optional): number of GB of memory ordered for this instance of VNF, + used for VNFs with no vservers/flavors, to be used only by uCPE. Defaults to None. + vmemory_units (str, optional): units associated with vmemory, used for VNFs with + no vservers/flavors, to be used only by uCPE. Defaults to None. + vdisk (int, optional): number of vdisks ordered for this instance of VNF, + used for VNFs with no vservers/flavors, to be used only uCPE. Defaults to None. + vdisk_units (str, optional): units associated with vdisk, used for VNFs with + no vservers/flavors, to be used only by uCPE. Defaults to None. + nshd (int, optional): number of associated SHD in vnf. Defaults to None. + nvm (int, optional): number of vms in vnf. Defaults to None. + nnet (int, optional): number of network in vnf. Defaults to None. + resource_version (str, optional): Used for optimistic concurrency. + Must be empty on create, valid on update and delete. Defaults to None. + encrypted_access_flag (bool, optional): indicates whether generic-vnf access uses SSH. + Defaults to None. + model_invariant_id (str, optional): the ASDC model id for this resource or + service model. Defaults to None. + model_version_id (str, optional): the ASDC model version for this resource or + service model. Defaults to None. + persona_model_version (str, optional): the ASDC model version for this resource or + service model. Defaults to None. + model_customization_id (str, optional): captures the id of all the configuration used + to customize the resource for the service. Defaults to None. + widget_model_id (str, optional): the ASDC data dictionary widget model. This maps + directly to the A&AI widget. Defaults to None. + widget_model_version (str, optional): the ASDC data dictionary version of + the widget model.This maps directly to the A&AI version of the widget. + Defaults to None. + as_number (str, optional): as-number of the VNF. Defaults to None. + regional_resource_subzone (str, optional): represents sub zone of the rr plane. + Defaults to None. + nf_type (str, optional): Generic description of the type of NF. Defaults to None. + nf_function (str, optional): English description of Network function that + the specific VNF deployment is providing. Defaults to None. + nf_role (str, optional): role in the network that this model will be providing. + Defaults to None. + nf_naming_code (str, optional): string assigned to this model used for naming purposes. + Defaults to None. + selflink (str, optional): Path to the controller object. Defaults to None. + ipv4_oam_gateway_address (str, optional): Gateway address. Defaults to None. + ipv4_oam_gateway_address_prefix_length (int, optional): Prefix length for oam-address. + Defaults to None. + vlan_id_outer (int, optional): Temporary location for S-TAG to get to VCE. + Defaults to None. + nm_profile_name (str, optional): Network Management profile of this VNF. + Defaults to None. + + """ + super().__init__(resource_version=resource_version, + model_invariant_id=model_invariant_id, + model_version_id=model_version_id) + self.service_instance: "ServiceInstance" = service_instance + self.vnf_id: str = vnf_id + self.vnf_type: str = vnf_type + self.in_maint: bool = in_maint + self.is_closed_loop_disabled: bool = is_closed_loop_disabled + self.vnf_name: str = vnf_name + self.service_id: str = service_id + self.regional_resource_zone: str = regional_resource_zone + self.prov_status: str = prov_status + self.operational_status: str = operational_status + self.equipment_role: str = equipment_role + self.orchestration_status: str = orchestration_status + self.vnf_package_name: str = vnf_package_name + self.vnf_discriptor_name: str = vnf_discriptor_name + self.job_id: str = job_id + self.heat_stack_id: str = heat_stack_id + self.mso_catalog_key: str = mso_catalog_key + self.management_option: str = management_option + self.ipv4_oam_address: str = ipv4_oam_address + self.ipv4_loopback0_address: str = ipv4_loopback0_address + self.nm_lan_v6_address: str = nm_lan_v6_address + self.management_v6_address: str = management_v6_address + self.vcpu: int = vcpu + self.vcpu_units: str = vcpu_units + self.vmemory: int = vmemory + self.vmemory_units: str = vmemory_units + self.vdisk: int = vdisk + self.vdisk_units: str = vdisk_units + self.nshd: int = nshd + self.nvm: int = nvm + self.nnet: int = nnet + self.encrypted_access_flag: bool = encrypted_access_flag + self.model_customization_id: str = model_customization_id + self.as_number: str = as_number + self.regional_resource_subzone: str = regional_resource_subzone + self.nf_type: str = nf_type + self.nf_function: str = nf_function + self.nf_role: str = nf_role + self.nf_naming_code: str = nf_naming_code + self.selflink: str = selflink + self.ipv4_oam_gateway_address: str = ipv4_oam_gateway_address + self.ipv4_oam_gateway_address_prefix_length: int = ipv4_oam_gateway_address_prefix_length + self.vlan_id_outer: int = vlan_id_outer + self.nm_profile_name: str = nm_profile_name + self.persona_model_version: str = persona_model_version + self.widget_model_id: str = widget_model_id + self.widget_model_version: str = widget_model_version + + self._vnf: "Vnf" = None + + def __repr__(self) -> str: + """Vnf instance object representation. + + Returns: + str: Human readable vnf instance representation + + """ + return (f"VnfInstance(vnf_id={self.vnf_id}, vnf_type={self.vnf_type}, " + f"in_maint={self.in_maint}, " + f"is_closed_loop_disabled={self.is_closed_loop_disabled})") + + @classmethod + def get_all_url(cls) -> str: # pylint: disable=arguments-differ + """Return url to get all vnfs. + + Returns: + str: Url to get all vnfs + + """ + return f"{cls.base_url}{cls.api_version}/network/generic-vnfs/" + + @property + def url(self) -> str: + """Vnf instance url. + + Returns: + str: VnfInstance url + + """ + return f"{self.base_url}{self.api_version}/network/generic-vnfs/generic-vnf/{self.vnf_id}" + + @property + def vf_modules(self) -> Iterator[VfModuleInstance]: + """Vf modules associated with vnf instance. + + Yields: + VfModuleInstance: VfModuleInstance associated with VnfInstance + + """ + for vf_module in self.send_message_json("GET", + f"GET VNF {self.vnf_name} VF modules", + f"{self.url}/vf-modules").get("vf-module", []): + yield VfModuleInstance.create_from_api_response(vf_module, self) + + @property + def vnf(self) -> "Vnf": + """Vnf associated with that vnf instance. + + Raises: + ResourceNotFound: Could not find VNF for that VNF instance + + Returns: + Vnf: Vnf object associated with vnf instance + + """ + if not self._vnf: + for vnf in self.service_instance.sdc_service.vnfs: + if vnf.model_version_id == self.model_version_id: + self._vnf = vnf + return self._vnf + + msg = ( + f'Could not find VNF for the VNF instance' + f' with model version ID "{self.model_version_id}"' + ) + raise ResourceNotFound(msg) + return self._vnf + + @classmethod + def create_from_api_response(cls, api_response: dict, + service_instance: "ServiceInstance") -> "VnfInstance": + """Create vnf instance object using HTTP API response dictionary. + + Returns: + VnfInstance: VnfInstance object + + """ + return cls(service_instance=service_instance, + vnf_id=api_response.get("vnf-id"), + vnf_type=api_response.get("vnf-type"), + in_maint=api_response.get("in-maint"), + is_closed_loop_disabled=api_response.get("is-closed-loop-disabled"), + vnf_name=api_response.get("vnf-name"), + service_id=api_response.get("service-id"), + regional_resource_zone=api_response.get("regional-resource-zone"), + prov_status=api_response.get("prov-status"), + operational_status=api_response.get("operational-status"), + equipment_role=api_response.get("equipment-role"), + orchestration_status=api_response.get("orchestration-status"), + vnf_package_name=api_response.get("vnf-package-name"), + vnf_discriptor_name=api_response.get("vnf-discriptor-name"), + job_id=api_response.get("job-id"), + heat_stack_id=api_response.get("heat-stack-id"), + mso_catalog_key=api_response.get("mso-catalog-key"), + management_option=api_response.get("management-option"), + ipv4_oam_address=api_response.get("ipv4-oam-address"), + ipv4_loopback0_address=api_response.get("ipv4-loopback0-address"), + nm_lan_v6_address=api_response.get("nm-lan-v6-address"), + management_v6_address=api_response.get("management-v6-address"), + vcpu=api_response.get("vcpu"), + vcpu_units=api_response.get("vcpu-units"), + vmemory=api_response.get("vmemory"), + vmemory_units=api_response.get("vmemory-units"), + vdisk=api_response.get("vdisk"), + vdisk_units=api_response.get("vdisk-units"), + nshd=api_response.get("nshd"), + nvm=api_response.get("nvm"), + nnet=api_response.get("nnet"), + resource_version=api_response.get("resource-version"), + model_invariant_id=api_response.get("model-invariant-id"), + model_version_id=api_response.get("model-version-id"), + encrypted_access_flag=api_response.get("encrypted-access-flag"), + persona_model_version=api_response.get("persona-model-version"), + model_customization_id=api_response.get("model-customization-id"), + widget_model_id=api_response.get("widget-model-id"), + widget_model_version=api_response.get("widget-model-version"), + as_number=api_response.get("as-number"), + regional_resource_subzone=api_response.get("regional-resource-subzone"), + nf_type=api_response.get("nf-type"), + nf_function=api_response.get("nf-function"), + nf_role=api_response.get("nf-role"), + nf_naming_code=api_response.get("nf-naming-code"), + selflink=api_response.get("selflink"), + ipv4_oam_gateway_address=api_response.get("ipv4-oam-gateway-address"), + ipv4_oam_gateway_address_prefix_length=\ + api_response.get("ipv4-oam-gateway-address-prefix-length"), + vlan_id_outer=api_response.get("vlan-id-outer"), + nm_profile_name=api_response.get("nm-profile-name")) + + def add_vf_module(self, # pylint: disable=too-many-arguments + vf_module: "VfModule", + cloud_region: "CloudRegion" = None, + tenant: "Tenant" = None, + vf_module_instance_name: str = None, + vnf_parameters: Iterable["InstantiationParameter"] = None, + use_preload: bool = True + ) -> "VfModuleInstantiation": + """Instantiate vf module for that VNF instance. + + Args: + vf_module (VfModule): VfModule to instantiate + cloud_region (CloudRegion, optional): Cloud region to use in instantiation request. + Defaults to None. + THAT PROPERTY WILL BE REQUIRED IN ONE OF THE FUTURE RELEASE. REFACTOR YOUR CODE + TO USE IT!. + tenant (Tenant, optional): Tenant to use in instnatiation request. + Defaults to None. + THAT PROPERTY WILL BE REQUIRED IN ONE OF THE FUTURE RELEASE. REFACTOR YOUR CODE + TO USE IT!. + vf_module_instance_name (str, optional): VfModule instance name. Defaults to None. + vnf_parameters (Iterable[InstantiationParameter], optional): InstantiationParameter + to use for preloading or to be passed as "userParams". Defaults to None. + use_preload (bool, optional): Based on this flag InstantiationParameters are passed + in preload or as "userParam" in the request. Defaults to True + + Returns: + VfModuleInstantiation: VfModuleInstantiation request object + + """ + return VfModuleInstantiation.instantiate_ala_carte( + vf_module, + self, + cloud_region=cloud_region, + tenant=tenant, + vf_module_instance_name=vf_module_instance_name, + vnf_parameters=vnf_parameters, + use_preload=use_preload + ) + + def update(self, + vnf_parameters: Iterable["InstantiationParameter"] = None + ) -> VnfInstantiation: + """Update vnf instance. + + Args: + vnf_parameters (Iterable["InstantiationParameter"], Optional): list of instantiation + parameters for update operation. + Raises: + StatusError: Skip post instantiation configuration flag for VF to True. + It might cause problems with SO component. + + Returns: + VnfInstantiation: VnfInstantiation object. + + """ + skip_flag = next(p for p in self.vnf.properties + if p.name == 'skip_post_instantiation_configuration') + if not skip_flag.value or skip_flag.value != "false": + raise StatusError("Operation for the vnf is not supported! " + "Skip_post_instantiation_configuration flag for VF should be False") + + return self._execute_so_action(operation_type=VnfOperation.UPDATE, + vnf_parameters=vnf_parameters) + + def healthcheck(self) -> VnfInstantiation: + """Execute healthcheck operation for vnf instance. + + Returns: + VnfInstantiation: VnfInstantiation object. + + """ + return self._execute_so_action(operation_type=VnfOperation.HEALTHCHECK) + + def _execute_so_action(self, + operation_type: VnfOperation, + vnf_parameters: Iterable["InstantiationParameter"] = None + ) -> VnfInstantiation: + """Execute SO workflow for selected operation. + + Args: + operation_type (str): Name of the operation to execute. + vnf_parameters (Iterable["InstantiationParameter"], Optional): list of instantiation + parameters for update operation. + + Returns: + VnfInstantiation: VnfInstantiation object. + + """ + if not self.service_instance.active: + msg = f'Service orchestration status must be "Active"' + raise StatusError(msg) + + lob = settings.LOB + platform = settings.PLATFORM + + for relationship in self.relationships: + if relationship.related_to == "line-of-business": + lob = relationship.relationship_data.pop().get("relationship-value") + if relationship.related_to == "platform": + platform = relationship.relationship_data.pop().get("relationship-value") + + so_input = self._build_so_input(vnf_params=vnf_parameters) + + return VnfInstantiation.so_action( + vnf_instance=self, + operation_type=operation_type, + aai_service_instance=self.service_instance, + line_of_business=lob, + platform=platform, + sdc_service=self.service_instance.sdc_service, + so_service=so_input + ) + + def _build_so_input(self, vnf_params: Iterable[InstantiationParameter] = None) -> SoService: + """Prepare so_input with params retrieved from existing service instance. + + Args: + vnf_params (Iterable[InstantiationParameter], Optional): list of instantiation + parameters for update operation. + + Returns: + SoService: SoService object to store SO Service parameters used for macro instantiation. + + """ + so_vnfs = [] + so_pnfs = [] + + if not vnf_params: + vnf_params = [] + + for pnf in self.service_instance.pnfs: + _pnf = { + "model_name": pnf.pnf.model_name, + "instance_name": pnf.pnf_name + } + + so_pnfs.append(_pnf) + + for vnf in self.service_instance.vnf_instances: + _vnf = {"model_name": vnf.vnf.model_name, + "instance_name": vnf.vnf_name, + "parameters": {}} + if vnf.vnf_name == self.vnf_name: + for _param in vnf_params: + _vnf["parameters"][_param.name] = _param.value + + _vf_modules = [] + for vf_module in vnf.vf_modules: + _vf_module = { + "model_name": vf_module.vf_module.model_name.split('..')[1], + "instance_name": vf_module.vf_module_name, + "parameters": {} + } + + _vf_modules.append(_vf_module) + + _vnf["vf_modules"] = _vf_modules + so_vnfs.append(_vnf) + + return SoService.load(data={ + 'subscription_service_type': self.service_instance.service_subscription.service_type, + 'vnfs': so_vnfs, + 'pnfs': so_pnfs + }) + + def delete(self, a_la_carte: bool = True) -> "VnfDeletionRequest": + """Create VNF deletion request. + + Send request to delete VNF instance + + Args: + a_la_carte (boolean): deletion mode + + Returns: + VnfDeletionRequest: Deletion request + + """ + self._logger.debug("Delete %s VNF", self.vnf_id) + return VnfDeletionRequest.send_request(self, a_la_carte) diff --git a/src/onapsdk/aai/cloud_infrastructure/__init__.py b/src/onapsdk/aai/cloud_infrastructure/__init__.py new file mode 100644 index 0000000..a380ce3 --- /dev/null +++ b/src/onapsdk/aai/cloud_infrastructure/__init__.py @@ -0,0 +1,18 @@ +"""A&AI cloud infrastructure package.""" +# Copyright 2022 Orange, 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 .cloud_region import AvailabilityZone, CloudRegion, EsrSystemInfo +from .complex import Complex +from .geo_region import GeoRegion +from .tenant import Tenant diff --git a/src/onapsdk/aai/cloud_infrastructure/cloud_region.py b/src/onapsdk/aai/cloud_infrastructure/cloud_region.py new file mode 100644 index 0000000..d57a025 --- /dev/null +++ b/src/onapsdk/aai/cloud_infrastructure/cloud_region.py @@ -0,0 +1,621 @@ +"""Cloud region module.""" +# Copyright 2022 Orange, 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 dataclasses import dataclass +from typing import Any, Dict, Iterator, List, Optional +from urllib.parse import urlencode + +from onapsdk.msb.multicloud import Multicloud +from onapsdk.utils.jinja import jinja_env +from onapsdk.exceptions import ResourceNotFound + +from ..aai_element import AaiResource, Relationship +from .complex import Complex +from .tenant import Tenant + + +@dataclass +class AvailabilityZone: + """Availability zone. + + A collection of compute hosts/pservers + """ + + name: str + hypervisor_type: str + operational_status: str = None + resource_version: str = None + + +@dataclass +class EsrSystemInfo: # pylint: disable=too-many-instance-attributes + """Persist common address information of external systems.""" + + esr_system_info_id: str + user_name: str + password: str + system_type: str + resource_version: str + system_name: str = None + esr_type: str = None + vendor: str = None + version: str = None + service_url: str = None + protocol: str = None + ssl_cacert: str = None + ssl_insecure: Optional[bool] = None + ip_address: str = None + port: str = None + cloud_domain: str = None + default_tenant: str = None + passive: Optional[bool] = None + remote_path: str = None + system_status: str = None + openstack_region_id: str = None + + +class CloudRegion(AaiResource): # pylint: disable=too-many-instance-attributes + """Cloud region class. + + Represents A&AI cloud region object. + """ + + def __init__(self, + cloud_owner: str, + cloud_region_id: str, + orchestration_disabled: bool, + in_maint: bool, + *, # rest of parameters are keyword + cloud_type: str = "", + owner_defined_type: str = "", + cloud_region_version: str = "", + identity_url: str = "", + cloud_zone: str = "", + complex_name: str = "", + sriov_automation: str = "", + cloud_extra_info: str = "", + upgrade_cycle: str = "", + resource_version: str = "") -> None: + """Cloud region object initialization. + + Args: + cloud_owner (str): Identifies the vendor and cloud name. + cloud_region_id (str): Identifier used by the vendor for the region. + orchestration_disabled (bool): Used to indicate whether orchestration is + enabled for this cloud-region. + in_maint (bool): Used to indicate whether or not cloud-region object + is in maintenance mode. + owner_defined_type (str, optional): Cloud-owner defined type + indicator (e.g., dcp, lcp). Defaults to "". + cloud_region_version (str, optional): Software version employed at the site. + Defaults to "". + identity_url (str, optional): URL of the keystone identity service. Defaults to "". + cloud_zone (str, optional): Zone where the cloud is homed. Defaults to "". + complex_name (str, optional): Complex name for cloud-region instance. Defaults to "". + sriov_automation (str, optional): Whether the cloud region supports (true) or does + not support (false) SR-IOV automation. Defaults to "". + cloud_extra_info (str, optional): ESR inputs extra information about the VIM or Cloud + which will be decoded by MultiVIM. Defaults to "". + upgrade_cycle (str, optional): Upgrade cycle for the cloud region. + For AIC regions upgrade cycle is designated by A,B,C etc. Defaults to "". + resource_version (str, optional): Used for optimistic concurrency. + Must be empty on create, valid on update and delete. Defaults to "". + + """ + super().__init__() + self.cloud_owner = cloud_owner + self.cloud_region_id = cloud_region_id + self.orchestration_disabled = orchestration_disabled + self.in_maint = in_maint + self.cloud_type = cloud_type + self.owner_defined_type = owner_defined_type + self.cloud_region_version = cloud_region_version + self.identity_url = identity_url + self.cloud_zone = cloud_zone + self.complex_name = complex_name + self.sriov_automation = sriov_automation + self.cloud_extra_info = cloud_extra_info + self.upgrade_cycle = upgrade_cycle + self.resource_version = resource_version + + def __repr__(self) -> str: + """Cloud region object representation. + + Returns: + str: Human readable string contains most important information about cloud region. + + """ + return ( + f"CloudRegion(cloud_owner={self.cloud_owner}, cloud_region_id={self.cloud_region_id})" + ) + + @classmethod + def get_all_url(cls) -> str: # pylint: disable=arguments-differ + """Return url to get all cloud regions. + + Returns: + str: Url to get all cloud regions + + """ + return f"{cls.base_url}{cls.api_version}/cloud-infrastructure/cloud-regions" + + @classmethod + def get_all(cls, + cloud_owner: str = None, + cloud_region_id: str = None, + cloud_type: str = None, + owner_defined_type: str = None) -> Iterator["CloudRegion"]: + """Get all A&AI cloud regions. + + Cloud regions can be filtered by 4 parameters: cloud-owner, + cloud-region-id, cloud-type and owner-defined-type. + + Yields: + CloudRegion -- CloudRegion object. Can not yield anything + if cloud region with given filter parameters doesn't exist + + """ + # Filter request parameters - use only these which are not None + filter_parameters: dict = cls.filter_none_key_values( + { + "cloud-owner": cloud_owner, + "cloud-region-id": cloud_region_id, + "cloud-type": cloud_type, + "owner-defined-type": owner_defined_type, + } + ) + url: str = (f"{cls.get_all_url()}?{urlencode(filter_parameters)}") + response_json: Dict[str, List[Dict[str, Any]]] = cls.send_message_json( + "GET", "get cloud regions", url + ) + for cloud_region in response_json.get("cloud-region", []): # typing: dict + yield CloudRegion( + cloud_owner=cloud_region["cloud-owner"], # required + cloud_region_id=cloud_region["cloud-region-id"], # required + cloud_type=cloud_region.get("cloud-type"), + owner_defined_type=cloud_region.get("owner-defined-type"), + cloud_region_version=cloud_region.get("cloud-region-version"), + identity_url=cloud_region.get("identity_url"), + cloud_zone=cloud_region.get("cloud-zone"), + complex_name=cloud_region.get("complex-name"), + sriov_automation=cloud_region.get("sriov-automation"), + cloud_extra_info=cloud_region.get("cloud-extra-info"), + upgrade_cycle=cloud_region.get("upgrade-cycle"), + orchestration_disabled=cloud_region["orchestration-disabled"], # required + in_maint=cloud_region["in-maint"], # required + resource_version=cloud_region.get("resource-version"), + ) + + @classmethod + def get_by_id(cls, cloud_owner: str, cloud_region_id: str) -> "CloudRegion": + """Get CloudRegion object by cloud_owner and cloud-region-id field value. + + This method calls A&AI cloud region API filtering them by cloud_owner and + cloud-region-id field value. + + Raises: + ResourceNotFound: Cloud region with given id does not exist. + + Returns: + CloudRegion: CloudRegion object with given cloud-region-id. + + """ + try: + return next(cls.get_all(cloud_owner=cloud_owner, cloud_region_id=cloud_region_id)) + except StopIteration: + msg = ( + f'CloudRegion with {cloud_owner}, ' + f'{cloud_region_id} cloud-id not found. ' + ) + raise ResourceNotFound(msg) + + @classmethod + def create(cls, # pylint: disable=too-many-locals + cloud_owner: str, + cloud_region_id: str, + orchestration_disabled: bool, + in_maint: bool, + *, # rest of parameters are keyword + cloud_type: str = "", + owner_defined_type: str = "", + cloud_region_version: str = "", + identity_url: str = "", + cloud_zone: str = "", + complex_name: str = "", + sriov_automation: str = "", + cloud_extra_info: str = "", + upgrade_cycle: str = "") -> "CloudRegion": + """Create CloudRegion object. + + Create cloud region with given values. + + Returns: + CloudRegion: Created cloud region. + + """ + cloud_region: "CloudRegion" = CloudRegion( + cloud_owner=cloud_owner, + cloud_region_id=cloud_region_id, + orchestration_disabled=orchestration_disabled, + in_maint=in_maint, + cloud_type=cloud_type, + owner_defined_type=owner_defined_type, + cloud_region_version=cloud_region_version, + identity_url=identity_url, + cloud_zone=cloud_zone, + complex_name=complex_name, + sriov_automation=sriov_automation, + cloud_extra_info=cloud_extra_info, + upgrade_cycle=upgrade_cycle, + ) + url: str = ( + f"{cls.base_url}{cls.api_version}/cloud-infrastructure/cloud-regions/cloud-region/" + f"{cloud_region.cloud_owner}/{cloud_region.cloud_region_id}" + ) + cls.send_message( + "PUT", + "Create cloud region", + url, + data=jinja_env() + .get_template("cloud_region_create.json.j2") + .render(cloud_region=cloud_region), + ) + return cloud_region + + @property + def url(self) -> str: + """Cloud region object url. + + URL used to call CloudRegion A&AI API + + Returns: + str: CloudRegion object url + + """ + return ( + f"{self.base_url}{self.api_version}/cloud-infrastructure/cloud-regions/cloud-region/" + f"{self.cloud_owner}/{self.cloud_region_id}" + ) + + @property + def tenants(self) -> Iterator["Tenant"]: + """Tenants iterator. + + Cloud region tenants iterator. + + Returns: + Iterator[Tenant]: Iterate through cloud region tenants + + """ + response: dict = self.send_message_json("GET", "get tenants", f"{self.url}/tenants") + return ( + Tenant( + cloud_region=self, + tenant_id=tenant["tenant-id"], + tenant_name=tenant["tenant-name"], + tenant_context=tenant.get("tenant-context"), + resource_version=tenant.get("resource-version"), + ) + for tenant in response.get("tenant", []) + ) + + @property + def availability_zones(self) -> Iterator[AvailabilityZone]: + """Cloud region availability zones. + + Iterate over CloudRegion availability zones. Relationship list is given using A&AI API call. + + Returns: + Iterator[AvailabilityZone]: CloudRegion availability zone + + """ + response: dict = self.send_message_json( + "GET", "get cloud region availability zones", f"{self.url}/availability-zones" + ) + return ( + AvailabilityZone( + name=availability_zone["availability-zone-name"], + hypervisor_type=availability_zone["hypervisor-type"], + operational_status=availability_zone.get("operational-status"), + resource_version=availability_zone.get("resource-version") + ) + for availability_zone in response.get("availability-zone", []) + ) + + @property + def esr_system_infos(self) -> Iterator[EsrSystemInfo]: + """Cloud region collection of persistent block-level external system auth info. + + Returns: + Iterator[EsrSystemInfo]: Cloud region external system address information. + + """ + response: dict = self.send_message_json( + "GET", "get cloud region external systems info list", f"{self.url}/esr-system-info-list" + ) + return ( + EsrSystemInfo( + esr_system_info_id=esr_system_info.get("esr-system-info-id"), + user_name=esr_system_info.get("user-name"), + password=esr_system_info.get("password"), + system_type=esr_system_info.get("system-type"), + system_name=esr_system_info.get("system-name"), + esr_type=esr_system_info.get("type"), + vendor=esr_system_info.get("vendor"), + version=esr_system_info.get("version"), + service_url=esr_system_info.get("service-url"), + protocol=esr_system_info.get("protocol"), + ssl_cacert=esr_system_info.get("ssl-cacert"), + ssl_insecure=esr_system_info.get("ssl-insecure"), + ip_address=esr_system_info.get("ip-address"), + port=esr_system_info.get("port"), + cloud_domain=esr_system_info.get("cloud-domain"), + default_tenant=esr_system_info.get("default-tenant"), + passive=esr_system_info.get("passive"), + remote_path=esr_system_info.get("remote-path"), + system_status=esr_system_info.get("system-status"), + openstack_region_id=esr_system_info.get("openstack-region-id"), + resource_version=esr_system_info.get("resource-version"), + ) + for esr_system_info in response.get("esr-system-info", []) + ) + + @property + def complex(self) -> Optional[Complex]: + """Complex related with cloud region. + + Returns: + Optional[Complex]: Complex object related with CloudRegion or None if + CloudRegion has no relationship with any Complex + + """ + try: + for relationship in self.relationships: + if relationship.related_to == "complex": + physical_location_id: Optional[str] = relationship.get_relationship_data( + "complex.physical-location-id" + ) + if physical_location_id is not None: + try: + return Complex.get_by_physical_location_id( + physical_location_id + ) + except ResourceNotFound: + self._logger.error("Complex with %s physical location id does " + "not exist", physical_location_id) + self._logger.error("Invalid Complex relationship!") + return None + except ResourceNotFound: + self._logger.debug("Cloud region %s has no relationships", self.cloud_region_id) + self._logger.debug("Cloud region %s has no related complex", self.cloud_region_id) + return None + + def add_tenant(self, tenant_id: str, tenant_name: str, tenant_context: str = None) -> None: + """Add tenant to cloud region. + + Args: + tenant_id (str): Unique id relative to the cloud-region. + tenant_name (str): Readable name of tenant + tenant_context (str, optional): This field will store + the tenant context.. Defaults to None. + + """ + self.send_message( + "PUT", + "add tenant to cloud region", + f"{self.url}/tenants/tenant/{tenant_id}", + data=jinja_env() + .get_template("cloud_region_add_tenant.json.j2") + .render(tenant_id=tenant_id, tenant_name=tenant_name, + tenant_context=tenant_context) + ) + + def get_tenant(self, tenant_id: str) -> "Tenant": + """Get tenant with provided ID. + + Args: + tenant_id (str): Tenant ID + + Returns: + Tenant: Tenant object + + """ + response: dict = self.send_message_json( + "GET", + "get tenants", + f"{self.url}/tenants/tenant/{tenant_id}" + ) + return Tenant( + cloud_region=self, + tenant_id=response["tenant-id"], + tenant_name=response["tenant-name"], + tenant_context=response.get("tenant-context"), + resource_version=response.get("resource-version"), + ) + + def get_tenants_by_name(self, tenant_name: str) -> Iterator["Tenant"]: + """Get tenants with given name. + + Args: + tenant_name (str): Tenant name + + Returns: + Iterator[Tenant]: Iterate through cloud region tenants with given name + + """ + return (tenant for tenant in self.tenants if tenant.name == tenant_name) + + + def get_availability_zone_by_name(self, + zone_name: str) -> "AvailabilityZone": + """Get availability zone with provided Name. + + Args: + availability_zone name (str): The name of the availibilty zone + + Returns: + AvailabilityZone: AvailabilityZone object + + """ + response: dict = self.send_message_json( + "GET", + "get availability_zones", + f"{self.url}/availability-zones/availability-zone/{zone_name}" + ) + return AvailabilityZone( + name=response["availability-zone-name"], + hypervisor_type=response["hypervisor-type"], + resource_version=response["resource-version"] + ) + + def add_availability_zone(self, + availability_zone_name: str, + availability_zone_hypervisor_type: str, + availability_zone_operational_status: str = None) -> None: + """Add avaiability zone to cloud region. + + Args: + availability_zone_name (str): Name of the availability zone. + Unique across a cloud region + availability_zone_hypervisor_type (str): Type of hypervisor + availability_zone_operational_status (str, optional): State that indicates whether + the availability zone should be used. Defaults to None. + """ + self.send_message( + "PUT", + "Add availability zone to cloud region", + f"{self.url}/availability-zones/availability-zone/{availability_zone_name}", + data=jinja_env() + .get_template("cloud_region_add_availability_zone.json.j2") + .render(availability_zone_name=availability_zone_name, + availability_zone_hypervisor_type=availability_zone_hypervisor_type, + availability_zone_operational_status=availability_zone_operational_status) + ) + + def add_esr_system_info(self, # pylint: disable=too-many-arguments, too-many-locals + esr_system_info_id: str, + user_name: str, + password: str, + system_type: str, + system_name: str = None, + esr_type: str = None, + vendor: str = None, + version: str = None, + service_url: str = None, + protocol: str = None, + ssl_cacert: str = None, + ssl_insecure: Optional[bool] = None, + ip_address: str = None, + port: str = None, + cloud_domain: str = None, + default_tenant: str = None, + passive: Optional[bool] = None, + remote_path: str = None, + system_status: str = None, + openstack_region_id: str = None, + resource_version: str = None) -> None: + """Add external system info to cloud region. + + Args: + esr_system_info_id (str): Unique ID of esr system info + user_name (str): username used to access external system + password (str): password used to access external system + system_type (str): it could be vim/vnfm/thirdparty-sdnc/ + ems-resource/ems-performance/ems-alarm + system_name (str, optional): name of external system. Defaults to None. + esr_type (str, optional): type of external system. Defaults to None. + vendor (str, optional): vendor of external system. Defaults to None. + version (str, optional): version of external system. Defaults to None. + service_url (str, optional): url used to access external system. Defaults to None. + protocol (str, optional): protocol of third party SDNC, + for example netconf/snmp. Defaults to None. + ssl_cacert (str, optional): ca file content if enabled ssl on auth-url. + Defaults to None. + ssl_insecure (bool, optional): Whether to verify VIM's certificate. Defaults to True. + ip_address (str, optional): service IP of ftp server. Defaults to None. + port (str, optional): service port of ftp server. Defaults to None. + cloud_domain (str, optional): domain info for authentication. Defaults to None. + default_tenant (str, optional): default tenant of VIM. Defaults to None. + passive (bool, optional): ftp passive mode or not. Defaults to False. + remote_path (str, optional): resource or performance data file path. Defaults to None. + system_status (str, optional): he status of external system. Defaults to None. + openstack_region_id (str, optional): OpenStack region ID used by MultiCloud plugin to + interact with an OpenStack instance. Defaults to None. + """ + self.send_message( + "PUT", + "Add external system info to cloud region", + f"{self.url}/esr-system-info-list/esr-system-info/{esr_system_info_id}", + data=jinja_env() + .get_template("cloud_region_add_esr_system_info.json.j2") + .render(esr_system_info_id=esr_system_info_id, + user_name=user_name, + password=password, + system_type=system_type, + system_name=system_name, + esr_type=esr_type, + vendor=vendor, + version=version, + service_url=service_url, + protocol=protocol, + ssl_cacert=ssl_cacert, + ssl_insecure=ssl_insecure, + ip_address=ip_address, + port=port, + cloud_domain=cloud_domain, + default_tenant=default_tenant, + passive=passive, + remote_path=remote_path, + system_status=system_status, + openstack_region_id=openstack_region_id, + resource_version=resource_version) + ) + + def register_to_multicloud(self, default_tenant: str = None) -> None: + """Register cloud to multicloud using MSB API. + + Args: + default_tenant (str, optional): Default tenant. Defaults to None. + """ + Multicloud.register_vim(self.cloud_owner, self.cloud_region_id, default_tenant) + + def unregister_from_multicloud(self) -> None: + """Unregister cloud from mutlicloud.""" + Multicloud.unregister_vim(self.cloud_owner, self.cloud_region_id) + + def delete(self) -> None: + """Delete cloud region.""" + self.send_message( + "DELETE", + f"Delete cloud region {self.cloud_region_id}", + self.url, + params={"resource-version": self.resource_version} + ) + + def link_to_complex(self, complex_object: Complex) -> None: + """Link cloud region to comples. + + It creates relationhip object and add it into cloud region. + """ + relationship = Relationship( + related_to="complex", + related_link=(f"aai/v13/cloud-infrastructure/complexes/" + f"complex/{complex_object.physical_location_id}"), + relationship_data={ + "relationship-key": "complex.physical-location-id", + "relationship-value": f"{complex_object.physical_location_id}", + }, + relationship_label="org.onap.relationships.inventory.LocatedIn", + ) + self.add_relationship(relationship) diff --git a/src/onapsdk/aai/cloud_infrastructure/complex.py b/src/onapsdk/aai/cloud_infrastructure/complex.py new file mode 100644 index 0000000..a854f02 --- /dev/null +++ b/src/onapsdk/aai/cloud_infrastructure/complex.py @@ -0,0 +1,300 @@ +"""A&AI Complex module.""" +# Copyright 2022 Orange, 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, Iterator +from urllib.parse import urlencode + +from onapsdk.utils.jinja import jinja_env + +from ..aai_element import AaiResource + + +class Complex(AaiResource): # pylint: disable=too-many-instance-attributes + """Complex class. + + Collection of physical locations that can house cloud-regions. + """ + + def __init__(self, # pylint: disable=too-many-locals + physical_location_id: str, + *, + name: str = "", + data_center_code: str = "", + identity_url: str = "", + resource_version: str = "", + physical_location_type: str = "", + street1: str = "", + street2: str = "", + city: str = "", + state: str = "", + postal_code: str = "", + country: str = "", + region: str = "", + latitude: str = "", + longitude: str = "", + elevation: str = "", + lata: str = "", + timezone: str = "", + data_owner: str = "", + data_source: str = "", + data_source_version: str = "") -> None: + """Complex object initialization. + + Args: + name (str): complex name + physical_location_id (str): complex ID + data_center_code (str, optional): complex data center code. Defaults to "". + identity_url (str, optional): complex identity url. Defaults to "". + resource_version (str, optional): complex resource version. Defaults to "". + physical_location_type (str, optional): complex physical location type. Defaults to "". + street1 (str, optional): complex address street part one. Defaults to "". + street2 (str, optional): complex address street part two. Defaults to "". + city (str, optional): complex address city. Defaults to "". + state (str, optional): complex address state. Defaults to "". + postal_code (str, optional): complex address postal code. Defaults to "". + country (str, optional): complex address country. Defaults to "". + region (str, optional): complex address region. Defaults to "". + latitude (str, optional): complex geographical location latitude. Defaults to "". + longitude (str, optional): complex geographical location longitude. Defaults to "". + elevation (str, optional): complex elevation. Defaults to "". + lata (str, optional): complex lata. Defaults to "". + timezone (str, optional): the time zone where the complex is located. Defaults to "". + data_owner (str, optional): Identifies the entity that is responsible managing this + inventory object. Defaults to "". + data_source (str, optional): Identifies the upstream source of the data. Defaults to "". + data_source_version (str, optional): Identifies the version of the upstream source. + Defaults to "". + + """ + super().__init__() + self.name: str = name + self.physical_location_id: str = physical_location_id + self.data_center_code: str = data_center_code + self.identity_url: str = identity_url + self.resource_version: str = resource_version + self.physical_location_type: str = physical_location_type + self.street1: str = street1 + self.street2: str = street2 + self.city: str = city + self.state: str = state + self.postal_code: str = postal_code + self.country: str = country + self.region: str = region + self.latitude: str = latitude + self.longitude: str = longitude + self.elevation: str = elevation + self.lata: str = lata + self.timezone: str = timezone + self.data_owner: str = data_owner + self.data_source: str = data_source + self.data_source_version: str = data_source_version + + def __repr__(self) -> str: + """Complex object description. + + Returns: + str: Complex object description + + """ + return (f"Complex(name={self.name}, " + f"physical_location_id={self.physical_location_id}, " + f"resource_version={self.resource_version})") + + @property + def url(self) -> str: + """Complex url. + + Returns: + str: Complex url + + """ + return (f"{self.base_url}{self.api_version}/cloud-infrastructure/complexes/complex/" + f"{self.physical_location_id}") + + @classmethod + def create(cls, # pylint: disable=too-many-locals + physical_location_id: str, + *, + name: str = "", + data_center_code: str = "", + identity_url: str = "", + resource_version: str = "", + physical_location_type: str = "", + street1: str = "", + street2: str = "", + city: str = "", + state: str = "", + postal_code: str = "", + country: str = "", + region: str = "", + latitude: str = "", + longitude: str = "", + elevation: str = "", + lata: str = "", + timezone: str = "", + data_owner: str = "", + data_source: str = "", + data_source_version: str = "") -> "Complex": + """Create complex. + + Create complex object by calling A&AI API. + If API request doesn't fail it returns Complex object. + + Returns: + Complex: Created complex object + + """ + complex_object: Complex = Complex( + name=name, + physical_location_id=physical_location_id, + data_center_code=data_center_code, + identity_url=identity_url, + resource_version=resource_version, + physical_location_type=physical_location_type, + street1=street1, + street2=street2, + city=city, + state=state, + postal_code=postal_code, + country=country, + region=region, + latitude=latitude, + longitude=longitude, + elevation=elevation, + lata=lata, + timezone=timezone, + data_owner=data_owner, + data_source=data_source, + data_source_version=data_source_version + ) + payload: str = jinja_env().get_template("complex_create.json.j2").render( + complex=complex_object) + url: str = ( + f"{cls.base_url}{cls.api_version}/cloud-infrastructure/complexes/complex/" + f"{complex_object.physical_location_id}" + ) + cls.send_message("PUT", "create complex", url, data=payload) + return complex_object + + @classmethod + def get_all_url(cls) -> str: # pylint: disable=arguments-differ + """Return an url to get all complexes. + + Returns: + str: URL to get all complexes + + """ + return f"{cls.base_url}{cls.api_version}/cloud-infrastructure/complexes" + + @classmethod + def get_all(cls, + physical_location_id: str = None, + data_center_code: str = None, + complex_name: str = None, + identity_url: str = None) -> Iterator["Complex"]: + """Get all complexes from A&AI. + + Call A&AI API to get all complex objects. + + Args: + physical_location_id (str, optional): Unique identifier for physical location, + e.g., CLLI. Defaults to None. + data_center_code (str, optional): Data center code which can be an alternate way + to identify a complex. Defaults to None. + complex_name (str, optional): Gamma complex name for LCP instance. Defaults to None. + identity_url (str, optional): URL of the keystone identity service. Defaults to None. + + Yields: + Complex -- Complex object. Can not yield anything if any complex with given filter + parameters doesn't exist + + """ + filter_parameters: dict = cls.filter_none_key_values( + { + "physical-location-id": physical_location_id, + "data-center-code": data_center_code, + "complex-name": complex_name, + "identity-url": identity_url, + } + ) + url: str = (f"{cls.get_all_url()}?{urlencode(filter_parameters)}") + for complex_json in cls.send_message_json("GET", + "get cloud regions", + url).get("complex", []): + yield cls.create_from_api_response(complex_json) + + @classmethod + def get_by_physical_location_id(cls, physical_location_id: str) -> "Complex": + """Get complex by physical location id. + + Args: + physical_location_id (str): Physical location id of Complex + + Returns: + Complex: Complex object + + Raises: + ResourceNotFound: Complex with given physical location id not found + + """ + response = cls.send_message_json("GET", + "Get complex with physical location id: " + f"{physical_location_id}", + f"{cls.base_url}{cls.api_version}/cloud-infrastructure/" + f"complexes/complex/{physical_location_id}") + return cls.create_from_api_response(response) + + @classmethod + def create_from_api_response(cls, + api_response: Dict[str, Any]) -> "Complex": + """Create complex object using given A&AI API response JSON. + + Args: + api_response (Dict[str, Any]): Complex A&AI API response + + Returns: + Complex: Complex object created from given response + + """ + return cls( + name=api_response.get("complex-name"), + physical_location_id=api_response["physical-location-id"], + data_center_code=api_response.get("data-center-code"), + identity_url=api_response.get("identity-url"), + resource_version=api_response.get("resource-version"), + physical_location_type=api_response.get("physical-location-type"), + street1=api_response.get("street1"), + street2=api_response.get("street2"), + city=api_response.get("city"), + state=api_response.get("state"), + postal_code=api_response.get("postal-code"), + country=api_response.get("country"), + region=api_response.get("region"), + latitude=api_response.get("latitude"), + longitude=api_response.get("longitude"), + elevation=api_response.get("elevation"), + lata=api_response.get("lata"), + timezone=api_response.get("time-zone"), + data_owner=api_response.get("data-owner"), + data_source=api_response.get("data-source"), + data_source_version=api_response.get("data-source-version") + ) + + def delete(self) -> None: + """Delete complex.""" + self.send_message( + "DELETE", + f"Delete {self.physical_location_id} complex", + f"{self.url}?resource-version={self.resource_version}" + ) diff --git a/src/onapsdk/aai/cloud_infrastructure/geo_region.py b/src/onapsdk/aai/cloud_infrastructure/geo_region.py new file mode 100644 index 0000000..32ff820 --- /dev/null +++ b/src/onapsdk/aai/cloud_infrastructure/geo_region.py @@ -0,0 +1,191 @@ +"""Geo region module.""" +# Copyright 2022 Orange, 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 Iterator, Optional + +from onapsdk.utils.jinja import jinja_env + +from ..aai_element import AaiResource + + +class GeoRegion(AaiResource): # pylint: disable=too-many-instance-attributes + """Geo region class.""" + + def __init__(self, + geo_region_id: str, + *, + geo_region_name: str = "", + geo_region_type: str = "", + geo_region_role: str = "", + geo_region_function: str = "", + data_owner: str = "", + data_source: str = "", + data_source_version: str = "", + resource_version: str = "", + ) -> None: + """Geo region init. + + Args: + geo_region_id (str): UUID, key for geo-region object. + geo_region_name (str, optional): Name of geo-region. Defaults to "". + geo_region_type (str, optional): Type of geo-region. Defaults to "". + geo_region_role (str, optional): Role of geo-region. Defaults to "". + geo_region_function (str, optional): Function of geo-region. Defaults to "". + data_owner (str, optional): Identifies the entity that is responsible managing + this inventory object. Defaults to "". + data_source (str, optional): Identifies the upstream source of the data. + Defaults to "". + data_source_version (str, optional): Identifies the version of + the upstream source. Defaults to "". + resource_version (str, optional): Resource version. Defaults to "". + + """ + super().__init__() + self.geo_region_id: str = geo_region_id + self.geo_region_name: str = geo_region_name + self.geo_region_type: str = geo_region_type + self.geo_region_role: str = geo_region_role + self.geo_region_function: str = geo_region_function + self.data_owner: str = data_owner + self.data_source: str = data_source + self.data_source_version: str = data_source_version + self.resource_version: str = resource_version + + def __repr__(self) -> str: + """Geo region object representation. + + Returns: + str: Human readable string contains most important information about geo region. + + """ + return ( + f"GeoRegion(geo_region_id={self.geo_region_id})" + ) + + @property + def url(self) -> str: + """Geo region's url. + + Returns: + str: Geo Region's url + + """ + return (f"{self.base_url}{self.api_version}/cloud-infrastructure/" + f"geo-regions/geo-region/{self.geo_region_id}") + + @classmethod + def get_all_url(cls, *args, **kwargs) -> str: # pylint: disable=arguments-differ + """Return url to get all geo regions. + + Returns: + str: Url to get all geo regions + + Raises: + ResourceNotFound: No geo regions found + + """ + return f"{cls.base_url}{cls.api_version}/cloud-infrastructure/geo-regions" + + @classmethod + def get_all(cls) -> Iterator["GeoRegion"]: + """Get all geo regions. + + Yields: + GeoRegion: Geo region + + """ + for geo_region_data in cls.send_message_json("GET", + "Get all geo regions", + cls.get_all_url()).get("geo-region", []): + yield cls(geo_region_id=geo_region_data["geo-region-id"], + geo_region_name=geo_region_data.get("geo-region-name", ""), + geo_region_type=geo_region_data.get("geo-region-type", ""), + geo_region_role=geo_region_data.get("geo-region-role", ""), + geo_region_function=geo_region_data.get("geo-region-function", ""), + data_owner=geo_region_data.get("data-owner", ""), + data_source=geo_region_data.get("data-source", ""), + data_source_version=geo_region_data.get("data-source-version", ""), + resource_version=geo_region_data.get("resource-version", "")) + + @classmethod + def get_by_geo_region_id(cls, geo_region_id: str) -> "GeoRegion": + """Get geo region by it's id. + + Args: + geo_region_id (str): Geo region id + + Returns: + GeoRegion: Geo region + + """ + resp = cls.send_message_json("GET", + f"Get geo region with {geo_region_id} id", + f"{cls.get_all_url()}/geo-region/{geo_region_id}") + return GeoRegion(resp["geo-region-id"], + geo_region_name=resp.get("geo-region-name", ""), + geo_region_type=resp.get("geo-region-type", ""), + geo_region_role=resp.get("geo-region-role", ""), + geo_region_function=resp.get("geo-region-function", ""), + data_owner=resp.get("data-owner", ""), + data_source=resp.get("data-source", ""), + data_source_version=resp.get("data-source-version", ""), + resource_version=resp["resource-version"]) + + @classmethod + def create(cls, # pylint: disable=too-many-arguments + geo_region_id: str, + geo_region_name: Optional[str] = None, + geo_region_type: Optional[str] = None, + geo_region_role: Optional[str] = None, + geo_region_function: Optional[str] = None, + data_owner: Optional[str] = None, + data_source: Optional[str] = None, + data_source_version: Optional[str] = None) -> "GeoRegion": + """Create geo region. + + Args: + geo_region_id (str): UUID, key for geo-region object. + geo_region_name (Optional[str], optional): Name of geo-region. Defaults to None. + geo_region_type (Optional[str], optional): Type of geo-region. Defaults to None. + geo_region_role (Optional[str], optional): Role of geo-region. Defaults to None. + geo_region_function (Optional[str], optional): Function of geo-region. + Defaults to None. + data_owner (Optional[str], optional): Identifies the entity that is + responsible managing this inventory object.. Defaults to None. + data_source (Optional[str], optional): Identifies the upstream source of the data. + Defaults to None. + data_source_version (Optional[str], optional): Identifies the version of + the upstream source. Defaults to None. + + Returns: + GeoRegion: Geo region object + + """ + cls.send_message( + "PUT", + "Create geo region", + f"{cls.get_all_url()}/geo-region/{geo_region_id}", + data=jinja_env() + .get_template("geo_region_create.json.j2") + .render(geo_region_id=geo_region_id, + geo_region_name=geo_region_name, + geo_region_type=geo_region_type, + geo_region_role=geo_region_role, + geo_region_function=geo_region_function, + data_owner=data_owner, + data_source=data_source, + data_source_version=data_source_version), + ) + return cls.get_by_geo_region_id(geo_region_id) diff --git a/src/onapsdk/aai/cloud_infrastructure/tenant.py b/src/onapsdk/aai/cloud_infrastructure/tenant.py new file mode 100644 index 0000000..13d9aec --- /dev/null +++ b/src/onapsdk/aai/cloud_infrastructure/tenant.py @@ -0,0 +1,101 @@ +"""A&AI Tenant module.""" +# Copyright 2022 Orange, 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 onapsdk.aai.cloud_infrastructure.cloud_region import CloudRegion +from ..aai_element import AaiResource + + +class Tenant(AaiResource): + """Tenant class.""" + + def __init__(self, # pylint: disable=too-many-arguments + cloud_region: "CloudRegion", + tenant_id: str, + tenant_name: str, + tenant_context: str = None, + resource_version: str = None): + """Tenant object initialization. + + Tenant object represents A&AI Tenant resource. + + Args: + cloud_region (str): Cloud region object + tenant_id (str): Unique Tenant ID + tenant_name (str): Tenant name + tenant_context (str, optional): Tenant context. Defaults to None. + resource_version (str, optional): Tenant resource version. Defaults to None. + + """ + super().__init__() + self.cloud_region: "CloudRegion" = cloud_region + self.tenant_id: str = tenant_id + self.name: str = tenant_name + self.context: str = tenant_context + self.resource_version: str = resource_version + + def __repr__(self) -> str: + """Tenant repr. + + Returns: + str: Human readable Tenant object description + + """ + return ( + f"Tenant(tenant_id={self.tenant_id}, tenant_name={self.name}, " + f"tenant_context={self.context}, " + f"resource_version={self.resource_version}, " + f"cloud_region={self.cloud_region.cloud_region_id})" + ) + + @classmethod + def get_all_url(cls, cloud_region: "CloudRegion") -> str: # pylint: disable=arguments-differ + """Return an url to get all tenants for given cloud region. + + Args: + cloud_region (CloudRegion): Cloud region object + + Returns: + str: Url to get all tenants + + """ + return (f"{cls.base_url}{cls.api_version}/cloud-infrastructure/cloud-regions/cloud-region/" + f"{cloud_region.cloud_owner}/{cloud_region.cloud_region_id}" + f"/tenants/") + + @property + def url(self) -> str: + """Tenant url. + + Returns: + str: Url which can be used to update or delete tenant. + + """ + return ( + f"{self.base_url}{self.api_version}/cloud-infrastructure/cloud-regions/cloud-region/" + f"{self.cloud_region.cloud_owner}/{self.cloud_region.cloud_region_id}" + f"/tenants/tenant/{self.tenant_id}?" + f"resource-version={self.resource_version}" + ) + + def delete(self) -> None: + """Delete tenant. + + Remove tenant from cloud region. + + """ + return self.send_message( + "DELETE", + f"Remove tenant {self.name} from {self.cloud_region.cloud_region_id} cloud region", + url=self.url, + ) diff --git a/src/onapsdk/aai/network/__init__.py b/src/onapsdk/aai/network/__init__.py new file mode 100644 index 0000000..c3795c1 --- /dev/null +++ b/src/onapsdk/aai/network/__init__.py @@ -0,0 +1,16 @@ +"""A&AI network package.""" +# Copyright 2022 Orange, 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 .site_resource import SiteResource diff --git a/src/onapsdk/aai/network/site_resource.py b/src/onapsdk/aai/network/site_resource.py new file mode 100644 index 0000000..3ac3c20 --- /dev/null +++ b/src/onapsdk/aai/network/site_resource.py @@ -0,0 +1,244 @@ +"""A&AI site resource module.""" +# Copyright 2022 Orange, 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 Iterable, Optional + +from onapsdk.utils.jinja import jinja_env +from ..aai_element import AaiResource + + +class SiteResource(AaiResource): # pylint: disable=too-many-instance-attributes + """Site resource class.""" + + def __init__(self, # pylint: disable=too-many-locals + site_resource_id: str, + *, + site_resource_name: str = "", + description: str = "", + site_resource_type: str = "", + role: str = "", + generated_site_id: str = "", + selflink: str = "", + operational_status: str = "", + model_customization_id: str = "", + model_invariant_id: str = "", + model_version_id: str = "", + data_owner: str = "", + data_source: str = "", + data_source_version: str = "", + resource_version: str = "") -> None: + """Site resource object init. + + Args: + site_resource_id (str): Uniquely identifies this site-resource by id. + site_resource_name (str, optional): Store the name of this site-resource. + Defaults to "". + description (str, optional): Store the description of this site-resource. + Defaults to "". + site_resource_type (str, optional): Store the type of this site-resource. + Defaults to "". + role (str, optional): Store the role of this site-resource. Defaults to "". + generated_site_id (str, optional): Store the generated-site-id of this site-resource. + Defaults to "". + selflink (str, optional): Store the link to get more information for this object. + Defaults to "". + operational_status (str, optional): Store the operational-status for this object. + Defaults to "". + model_customization_id (str, optional): Store the model-customization-id + for this object. Defaults to "". + model_invariant_id (str, optional): The ASDC model id for this resource or + service model. Defaults to "". + model_version_id (str, optional): The ASDC model version for this resource or service + model. Defaults to "". + data_owner (str, optional): Identifies the entity that is responsible managing + this inventory object. Defaults to "". + data_source (str, optional): Identifies the upstream source of the data. + Defaults to "". + data_source_version (str, optional): Identifies the version of the upstream source. + Defaults to "". + resource_version (str, optional): Used for optimistic concurrency. Must be empty on + create, valid on update and delete. Defaults to "". + + """ + super().__init__() + self.site_resource_id: str = site_resource_id + self.site_resource_name: str = site_resource_name + self.description: str = description + self.site_resource_type: str = site_resource_type + self.role: str = role + self.generated_site_id: str = generated_site_id + self.selflink: str = selflink + self.operational_status: str = operational_status + self.model_customization_id: str = model_customization_id + self.model_invariant_id: str = model_invariant_id + self.model_version_id: str = model_version_id + self.data_owner: str = data_owner + self.data_source: str = data_source + self.data_source_version: str = data_source_version + self.resource_version: str = resource_version + + @property + def url(self) -> str: + """Site resource's url. + + Returns: + str: Site resources's url + + """ + return (f"{self.base_url}{self.api_version}/network/site-resources" + f"/site-resource/{self.site_resource_id}") + + @classmethod + def get_all_url(cls, *args, **kwargs) -> str: + """Get all site resources request url. + + Returns: + str: Url used on get all site resources request + + """ + return f"{cls.base_url}{cls.api_version}/network/site-resources" + + @classmethod + def get_all(cls) -> Iterable["SiteResource"]: + """Get all site resources. + + Yields: + SiteResource: Site resource object + + """ + for site_resource_data in cls.send_message_json("GET", + "Get all site resources", + cls.get_all_url()).get("site-resource", []): + yield SiteResource(site_resource_id=site_resource_data["site-resource-id"], + site_resource_name=site_resource_data.get("site-resource-name", ""), + description=site_resource_data.get("description", ""), + site_resource_type=site_resource_data.get("type", ""), + role=site_resource_data.get("role", ""), + generated_site_id=site_resource_data.get("generated-site-id", ""), + selflink=site_resource_data.get("selflink", ""), + operational_status=site_resource_data.get("operational-status", ""), + model_customization_id=site_resource_data.\ + get("model-customization-id", ""), + model_invariant_id=site_resource_data.get("model-invariant-id", ""), + model_version_id=site_resource_data.get("model-version-id", ""), + data_owner=site_resource_data.get("data-owner", ""), + data_source=site_resource_data.get("data-source", ""), + data_source_version=site_resource_data.get("data-source-version", + ""), + resource_version=site_resource_data.get("resource-version", "")) + + @classmethod + def get_by_site_resource_id(cls, site_resource_id: str) -> "SiteResource": + """Get site resource by it's id. + + Args: + site_resource_id (str): Site resource id. + + Returns: + SiteResource: Site resource object. + + """ + site_resource_data = cls.send_message_json("GET", + f"Get site resource with {site_resource_id} id", + f"{cls.get_all_url()}" + f"/site-resource/{site_resource_id}") + return SiteResource(site_resource_id=site_resource_data["site-resource-id"], + site_resource_name=site_resource_data.get("site-resource-name", ""), + description=site_resource_data.get("description", ""), + site_resource_type=site_resource_data.get("type", ""), + role=site_resource_data.get("role", ""), + generated_site_id=site_resource_data.get("generated-site-id", ""), + selflink=site_resource_data.get("selflink", ""), + operational_status=site_resource_data.get("operational-status", ""), + model_customization_id=site_resource_data.get("model-customization-id", + ""), + model_invariant_id=site_resource_data.get("model-invariant-id", ""), + model_version_id=site_resource_data.get("model-version-id", ""), + data_owner=site_resource_data.get("data-owner", ""), + data_source=site_resource_data.get("data-source", ""), + data_source_version=site_resource_data.get("data-source-version", ""), + resource_version=site_resource_data.get("resource-version", "")) + + @classmethod + def create(cls, # pylint: disable=too-many-arguments + site_resource_id: str, + site_resource_name: Optional[str] = None, + description: Optional[str] = None, + site_resource_type: Optional[str] = None, + role: Optional[str] = None, + generated_site_id: Optional[str] = None, + selflink: Optional[str] = None, + operational_status: Optional[str] = None, + model_customization_id: Optional[str] = None, + model_invariant_id: Optional[str] = None, + model_version_id: Optional[str] = None, + data_owner: Optional[str] = None, + data_source: Optional[str] = None, + data_source_version: Optional[str] = None) -> "SiteResource": + """Create site resource. + + Args: + site_resource_id (str): Uniquely identifies this site-resource by id + site_resource_name (Optional[str], optional): Store the name of this site-resource. + Defaults to None. + description (Optional[str], optional): Store the description of this site-resource. + Defaults to None. + site_resource_type (Optional[str], optional): Store the type of this site-resource. + Defaults to None. + role (Optional[str], optional): Store the role of this site-resource. + Defaults to None. + generated_site_id (Optional[str], optional): Store the generated-site-id of + this site-resource. Defaults to None. + selflink (Optional[str], optional): Store the link to get more information + for this object. Defaults to None. + operational_status (Optional[str], optional): Store the operational-status + for this object. Defaults to None. + model_customization_id (Optional[str], optional): Store the model-customization-id + for this object. Defaults to None. + model_invariant_id (Optional[str], optional): The ASDC model id for + this resource or service model. Defaults to None. + model_version_id (Optional[str], optional): The ASDC model version for this + resource or service model. Defaults to None. + data_owner (Optional[str], optional): Identifies the entity that is responsible + managing this inventory object. Defaults to None. + data_source (Optional[str], optional): Identifies the upstream source of the data. + Defaults to None. + data_source_version (Optional[str], optional): Identifies the version of the upstream + source. Defaults to None. + + Returns: + SiteResource: Site resource object + + """ + cls.send_message("PUT", + f"Create site resource {site_resource_id}", + f"{cls.get_all_url()}/site-resource/{site_resource_id}", + data=jinja_env() + .get_template("site_resource_create.json.j2") + .render(site_resource_id=site_resource_id, + site_resource_name=site_resource_name, + description=description, + site_resource_type=site_resource_type, + role=role, + generated_site_id=generated_site_id, + selflink=selflink, + operational_status=operational_status, + model_customization_id=model_customization_id, + model_invariant_id=model_invariant_id, + model_version_id=model_version_id, + data_owner=data_owner, + data_source=data_source, + data_source_version=data_source_version)) + return cls.get_by_site_resource_id(site_resource_id) diff --git a/src/onapsdk/aai/service_design_and_creation.py b/src/onapsdk/aai/service_design_and_creation.py new file mode 100644 index 0000000..5bf2c6f --- /dev/null +++ b/src/onapsdk/aai/service_design_and_creation.py @@ -0,0 +1,186 @@ +"""AAI service-design-and-creation module.""" +# Copyright 2022 Orange, 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 Iterator +from urllib.parse import urlencode + +from onapsdk.utils.jinja import jinja_env + +from .aai_element import AaiResource + + +class Service(AaiResource): + """SDC service class.""" + + def __init__(self, service_id: str, service_description: str, resource_version: str) -> None: + """Service model initialization. + + Args: + service_id (str): This gets defined by others to provide a unique ID for the service. + service_description (str): Description of the service. + resource_version (str): Used for optimistic concurrency. + + """ + super().__init__() + self.service_id = service_id + self.service_description = service_description + self.resource_version = resource_version + + def __repr__(self) -> str: + """Service object description. + + Returns: + str: Service object description + + """ + return ( + f"Service(service_id={self.service_id}, " + f"service_description={self.service_description}, " + f"resource_version={self.resource_version})" + ) + + @property + def url(self) -> str: + """Service object url. + + Returns: + str: Service object url address + + """ + return (f"{self.base_url}{self.api_version}/service-design-and-creation/services/service/" + f"{self.service_id}?resource-version={self.resource_version}") + + @classmethod + def get_all_url(cls) -> str: # pylint: disable=arguments-differ + """Return url to get all services. + + Returns: + str: Url to get all services + + """ + return f"{cls.base_url}{cls.api_version}/service-design-and-creation/services" + + @classmethod + def get_all(cls, + service_id: str = None, + service_description: str = None) -> Iterator["Service"]: + """Services iterator. + + Stand-in for service model definitions. + + Returns: + Iterator[Service]: Service + + """ + filter_parameters: dict = cls.filter_none_key_values( + {"service-id": service_id, "service-description": service_description} + ) + url: str = (f"{cls.get_all_url()}?{urlencode(filter_parameters)}") + for service in cls.send_message_json("GET", "get subscriptions", url).get("service", []): + yield Service( + service_id=service["service-id"], + service_description=service["service-description"], + resource_version=service["resource-version"], + ) + + @classmethod + def create(cls, + service_id: str, + service_description: str) -> None: + """Create service. + + Args: + service_id (str): service ID + service_description (str): service description + + """ + cls.send_message( + "PUT", + "Create A&AI service", + f"{cls.base_url}{cls.api_version}/service-design-and-creation/" + f"services/service/{service_id}", + data=jinja_env() + .get_template("aai_service_create.json.j2") + .render( + service_id=service_id, + service_description=service_description + ) + ) + + +class Model(AaiResource): + """Model resource class.""" + + def __init__(self, invariant_id: str, model_type: str, resource_version: str) -> None: + """Model object initialization. + + Args: + invariant_id (str): invariant id + model_type (str): model type + resource_version (str): resource version + + """ + super().__init__() + self.invariant_id: str = invariant_id + self.model_type: str = model_type + self.resource_version: str = resource_version + + def __repr__(self) -> str: + """Model object representation. + + Returns: + str: model object representation + + """ + return (f"Model(invatiant_id={self.invariant_id}, " + f"model_type={self.model_type}, " + f"resource_version={self.resource_version}") + + @property + def url(self) -> str: + """Model instance url. + + Returns: + str: Model's url + + """ + return (f"{self.base_url}{self.api_version}/service-design-and-creation/models/" + f"model/{self.invariant_id}?resource-version={self.resource_version}") + + @classmethod + def get_all_url(cls) -> str: # pylint: disable=arguments-differ + """Return url to get all models. + + Returns: + str: Url to get all models + + """ + return f"{cls.base_url}{cls.api_version}/service-design-and-creation/models" + + @classmethod + def get_all(cls) -> Iterator["Model"]: + """Get all models. + + Yields: + Model: Model object + + """ + for model in cls.send_message_json("GET", + "Get A&AI sdc models", + cls.get_all_url()).get("model", []): + yield Model( + invariant_id=model.get("model-invariant-id"), + model_type=model.get("model-type"), + resource_version=model.get("resource-version") + ) diff --git a/src/onapsdk/aai/templates/aai_add_relationship.json.j2 b/src/onapsdk/aai/templates/aai_add_relationship.json.j2 new file mode 100644 index 0000000..5d7acb8 --- /dev/null +++ b/src/onapsdk/aai/templates/aai_add_relationship.json.j2 @@ -0,0 +1,11 @@ +{ + "related-to": "{{ relationship.related_to }}", + "related-link": "{{ relationship.related_link }}", + {% if relationship.relationship_label %} + "relationship-label": "{{ relationship.relationship_label }}", + {% endif %} + {% if relationship.related_to_property %} + "related-to-property": {{ relationship.related_to_property | tojson }}, + {% endif %} + "relationship-data": {{ relationship.relationship_data | tojson }} +}
\ No newline at end of file diff --git a/src/onapsdk/aai/templates/aai_bulk.json.j2 b/src/onapsdk/aai/templates/aai_bulk.json.j2 new file mode 100644 index 0000000..40a97bd --- /dev/null +++ b/src/onapsdk/aai/templates/aai_bulk.json.j2 @@ -0,0 +1,11 @@ +{ + "operations": [ + {% for operation in operations %} + { + "action": "{{ operation.action }}", + "uri": "{{ operation.uri }}", + "body": {{ operation.body }} + }{% if not loop.last %},{% endif %} + {% endfor %} + ] +}
\ No newline at end of file diff --git a/src/onapsdk/aai/templates/aai_line_of_business_create.json.j2 b/src/onapsdk/aai/templates/aai_line_of_business_create.json.j2 new file mode 100644 index 0000000..adab1fa --- /dev/null +++ b/src/onapsdk/aai/templates/aai_line_of_business_create.json.j2 @@ -0,0 +1,3 @@ +{ + "line-of-business-name": "{{ line_of_business_name }}" +}
\ No newline at end of file diff --git a/src/onapsdk/aai/templates/aai_owning_entity_create.json.j2 b/src/onapsdk/aai/templates/aai_owning_entity_create.json.j2 new file mode 100644 index 0000000..2877a3d --- /dev/null +++ b/src/onapsdk/aai/templates/aai_owning_entity_create.json.j2 @@ -0,0 +1,4 @@ +{ + "owning-entity-name": "{{ owning_entity_name }}", + "owning-entity-id": "{{ owning_entity_id }}" +}
\ No newline at end of file diff --git a/src/onapsdk/aai/templates/aai_platform_create.json.j2 b/src/onapsdk/aai/templates/aai_platform_create.json.j2 new file mode 100644 index 0000000..afe339a --- /dev/null +++ b/src/onapsdk/aai/templates/aai_platform_create.json.j2 @@ -0,0 +1,3 @@ +{ + "platform-name": "{{ platform_name }}" +}
\ No newline at end of file diff --git a/src/onapsdk/aai/templates/aai_project_create.json.j2 b/src/onapsdk/aai/templates/aai_project_create.json.j2 new file mode 100644 index 0000000..3c7a426 --- /dev/null +++ b/src/onapsdk/aai/templates/aai_project_create.json.j2 @@ -0,0 +1,3 @@ +{ + "project-name": "{{ project_name }}" +}
\ No newline at end of file diff --git a/src/onapsdk/aai/templates/aai_service_create.json.j2 b/src/onapsdk/aai/templates/aai_service_create.json.j2 new file mode 100644 index 0000000..ee360cc --- /dev/null +++ b/src/onapsdk/aai/templates/aai_service_create.json.j2 @@ -0,0 +1,4 @@ +{ + "service-id": "{{ service_id }}", + "service-description": "{{ service_description }}" +}
\ No newline at end of file diff --git a/src/onapsdk/aai/templates/aai_service_instance_create.json.j2 b/src/onapsdk/aai/templates/aai_service_instance_create.json.j2 new file mode 100644 index 0000000..91f0046 --- /dev/null +++ b/src/onapsdk/aai/templates/aai_service_instance_create.json.j2 @@ -0,0 +1,22 @@ +{ + "service-instance-id": "{{ service_instance.instance_id }}" + {% if service_instance.instance_name %}, "service-instance-name": "{{ service_instance.instance_name }}"{% endif %} + {% if service_instance.service_type %}, "service-type": "{{ service_instance.service_type }}"{% endif %} + {% if service_instance.service_role %}, "service-role": "{{ service_instance.service_role }}"{% endif %} + {% if service_instance.environment_context %}, "environment-context": "{{ service_instance.environment_context }}"{% endif %} + {% if service_instance.workload_context %}, "workload-context": "{{ service_instance.workload_context }}"{% endif %} + {% if service_instance.created_at %}, "created-at": "{{ service_instance.created_at }}"{% endif %} + {% if service_instance.updated_at %}, "updated-at": "{{ service_instance.updated_at }}"{% endif %} + {% if service_instance.description %}, "description": "{{ service_instance.description }}"{% endif %} + {% if service_instance.model_invariant_id %}, "model-invariant-id": "{{ service_instance.model_invariant_id }}"{% endif %} + {% if service_instance.model_version_id %}, "model-version-id": "{{ service_instance.model_version_id }}"{% endif %} + {% if service_instance.persona_model_version %}, "persona-model-version": "{{ service_instance.persona_model_version }}"{% endif %} + {% if service_instance.widget_model_id %}, "widget-model-id": "{{ service_instance.widget_model_id }}"{% endif %} + {% if service_instance.widget_model_version %}, "widget-model-version": "{{ service_instance.widget_model_version }}"{% endif %} + {% if service_instance.bandwith_total %}, "bandwidth-total": "{{ service_instance.bandwith_total }}"{% endif %} + {% if service_instance.vhn_portal_url %}, "vhn-portal-url": "{{ service_instance.vhn_portal_url }}"{% endif %} + {% if service_instance.service_instance_location_id %}, "service-instance-location-id": "{{ service_instance.service_instance_location_id }}"{% endif %} + {% if service_instance.selflink %}, "selflink": "{{ service_instance.selflink }}"{% endif %} + {% if service_instance.orchestration_status %}, "orchestration-status": "{{ service_instance.orchestration_status }}"{% endif %} + {% if service_instance.input_parameters %}, "input-parameters": "{{ service_instance.input_parameters }}"{% endif %} +}
\ No newline at end of file diff --git a/src/onapsdk/aai/templates/aai_sp_partner_create.json.j2 b/src/onapsdk/aai/templates/aai_sp_partner_create.json.j2 new file mode 100644 index 0000000..40ba1d7 --- /dev/null +++ b/src/onapsdk/aai/templates/aai_sp_partner_create.json.j2 @@ -0,0 +1,21 @@ +{ + "sp-partner-id": "{{ sp_partner_id }}" + {% if url %} + , "url": "{{ url }}" + {% endif %} + {% if callsource %} + , "callsource": "{{ callsource }}" + {% endif %} + {% if operational_status %} + , "operational-status": "{{ operational_status }}" + {% endif %} + {% if model_customization_id %} + , "model-customization-id": "{{ model_customization_id }}" + {% endif %} + {% if model_invariant_id %} + , "model-invariant-id": "{{ model_invariant_id }}" + {% endif %} + {% if model_version_id %} + , "model-version-id": "{{ model_version_id }}" + {% endif %} +}
\ No newline at end of file diff --git a/src/onapsdk/aai/templates/cloud_region_add_availability_zone.json.j2 b/src/onapsdk/aai/templates/cloud_region_add_availability_zone.json.j2 new file mode 100644 index 0000000..be6ebc5 --- /dev/null +++ b/src/onapsdk/aai/templates/cloud_region_add_availability_zone.json.j2 @@ -0,0 +1,7 @@ +{ + "availability-zone-name": "{{ availability_zone_name }}", + "hypervisor-type": "{{ availability_zone_hypervisor_type }}" + {% if availability_zone_operational_status %} + , "operational-status": "{{ availability_zone_operational_status }}" + {% endif %} +}
\ No newline at end of file diff --git a/src/onapsdk/aai/templates/cloud_region_add_esr_system_info.json.j2 b/src/onapsdk/aai/templates/cloud_region_add_esr_system_info.json.j2 new file mode 100644 index 0000000..ab03de3 --- /dev/null +++ b/src/onapsdk/aai/templates/cloud_region_add_esr_system_info.json.j2 @@ -0,0 +1,54 @@ +{ + "esr-system-info-id": "{{ esr_system_info_id }}", + "user-name": "{{ user_name }}", + "password": "{{ password }}", + "system-type": "{{ system_type }}" + {% if system_name %} + , "system-name": "{{ system_name }}" + {% endif %} + {% if esr_type %} + , "type": "{{ esr_type }}" + {% endif %} + {% if vendor %} + , "vendor": "{{ vendor }}" + {% endif %} + {% if version %} + , "version": "{{ version }}" + {% endif %} + {% if service_url %} + , "service-url": "{{ service_url }}" + {% endif %} + {% if protocol %} + , "protocol": "{{ protocol }}" + {% endif %} + {% if ssl_cacert %} + , "ssl-cacert": "{{ ssl_cacert }}" + {% endif %} + {% if ssl_insecure is not none %} + , "ssl-insecure": {{ ssl_insecure | tojson }} + {% endif %} + {% if ip_address %} + , "ip-address": "{{ ip_address }}" + {% endif %} + {% if port %} + , "port": "{{ port }}" + {% endif %} + {% if cloud_domain %} + , "cloud-domain": "{{ cloud_domain }}" + {% endif %} + {% if default_tenant %} + , "default-tenant": "{{ default_tenant }}" + {% endif %} + {% if passive is not none %} + , "passive": {{ passive | tojson }} + {% endif %} + {% if remote_path %} + , "remote-path": "{{ remote_path }}" + {% endif %} + {% if system_status %} + , "system-status": "{{ system_status }}" + {% endif %} + {% if openstack_region_id %} + , "openstack-region-id": "{{ openstack_region_id }}" + {% endif %} +}
\ No newline at end of file diff --git a/src/onapsdk/aai/templates/cloud_region_add_tenant.json.j2 b/src/onapsdk/aai/templates/cloud_region_add_tenant.json.j2 new file mode 100644 index 0000000..fd7bcb7 --- /dev/null +++ b/src/onapsdk/aai/templates/cloud_region_add_tenant.json.j2 @@ -0,0 +1,5 @@ +{ + "tenant-id": "{{ tenant_id }}", + "tenant-name": "{{ tenant_name }}", + "tenant-context": "{{ tenant_context }}" +}
\ No newline at end of file diff --git a/src/onapsdk/aai/templates/cloud_region_create.json.j2 b/src/onapsdk/aai/templates/cloud_region_create.json.j2 new file mode 100644 index 0000000..65a7057 --- /dev/null +++ b/src/onapsdk/aai/templates/cloud_region_create.json.j2 @@ -0,0 +1,16 @@ +{ + "cloud-owner": "{{ cloud_region.cloud_owner }}", + "cloud-region-id": "{{ cloud_region.cloud_region_id }}", + "orchestration-disabled": "{{ cloud_region.orchestration_disabled }}", + "in-maint": "{{ cloud_region.in_maint }}", + "cloud-type": "{{ cloud_region.cloud_type }}", + "owner-defined-type": "{{ cloud_region.owner_defined_type }}", + "cloud-region-version": "{{ cloud_region.cloud_region_version }}", + "identity-url": "{{ cloud_region.identity_url }}", + "cloud-zone": "{{ cloud_region.cloud_zone }}", + "complex-name": "{{ cloud_region.complex_name }}", + "sriov-automation": "{{ cloud_region.sriov_automation }}", + "cloud-extra-info": "{{ cloud_region.cloud_extra_info }}", + "upgrade-cycle": "{{ cloud_region.upgrade_cycle }}", + "resource-version": "{{ cloud_region.resource_version }}" +}
\ No newline at end of file diff --git a/src/onapsdk/aai/templates/complex_create.json.j2 b/src/onapsdk/aai/templates/complex_create.json.j2 new file mode 100644 index 0000000..681fdad --- /dev/null +++ b/src/onapsdk/aai/templates/complex_create.json.j2 @@ -0,0 +1,23 @@ +{ + "physical-location-id": "{{ complex.physical_location_id }}", + "data-center-code": "{{ complex.data_center_code }}", + "complex-name": "{{ complex.name }}", + "identity-url": "{{ complex.identity_url }}", + "resource-version": "{{ complex.resource_version }}", + "physical-location-type": "{{ complex.physical_location_type }}", + "street1": "{{ complex.street1 }}", + "street2": "{{ complex.street2 }}", + "city": "{{ complex.city }}", + "state": "{{ complex.state }}", + "postal-code": "{{ complex.postal_code }}", + "country": "{{ complex.country }}", + "region": "{{ complex.region }}", + "latitude": "{{ complex.latitude }}", + "longitude": "{{ complex.longitude }}", + "elevation": "{{ complex.elevation }}", + "lata": "{{ complex.lata }}", + "time-zone": "{{ complex.timezone }}", + "data-owner": "{{ complex.data_owner }}", + "data-source": "{{ complex.data_source }}", + "data-source-version": "{{ complex.data_source_version }}" +}
\ No newline at end of file diff --git a/src/onapsdk/aai/templates/customer_create.json.j2 b/src/onapsdk/aai/templates/customer_create.json.j2 new file mode 100644 index 0000000..0eea2ed --- /dev/null +++ b/src/onapsdk/aai/templates/customer_create.json.j2 @@ -0,0 +1,15 @@ +{ + "global-customer-id": "{{ global_customer_id }}", + "subscriber-name": "{{ subscriber_name }}", + "subscriber-type": "{{ subscriber_type }}"{% if service_subscriptions %}, + "service-subscriptions": { + "service-subscription": [ + {% for service_subscription in service_subscriptions %} + { + "service-type": "{{ service_subscription }}" + }{% if not loop.last %},{% endif %} + {% endfor %} + ] + } + {% endif %} +}
\ No newline at end of file diff --git a/src/onapsdk/aai/templates/customer_service_subscription_create.json.j2 b/src/onapsdk/aai/templates/customer_service_subscription_create.json.j2 new file mode 100644 index 0000000..c1ee61e --- /dev/null +++ b/src/onapsdk/aai/templates/customer_service_subscription_create.json.j2 @@ -0,0 +1,3 @@ +{ + "service-id": "{{ service_id }}" +}
\ No newline at end of file diff --git a/src/onapsdk/aai/templates/geo_region_create.json.j2 b/src/onapsdk/aai/templates/geo_region_create.json.j2 new file mode 100644 index 0000000..d84b427 --- /dev/null +++ b/src/onapsdk/aai/templates/geo_region_create.json.j2 @@ -0,0 +1,10 @@ +{ + {% if geo_region_name is not none %}"geo-region-name": "{{ geo_region_name }}",{% endif %} + {% if geo_region_type is not none %}"geo-region-type": "{{ geo_region_type }}",{% endif %} + {% if geo_region_role is not none %}"geo-region-role": "{{ geo_region_role }}",{% endif %} + {% if geo_region_function is not none %}"geo-region-function": "{{ geo_region_function }}",{% endif %} + {% if data_owner is not none %}"data-owner": "{{ data_owner }}",{% endif %} + {% if data_source is not none %}"data-source": "{{ data_source }}",{% endif %} + {% if data_source_version is not none %}"data-source-version": "{{ data_source_version }}",{% endif %} + "geo-region-id": "{{ geo_region_id }}" +}
\ No newline at end of file diff --git a/src/onapsdk/aai/templates/site_resource_create.json.j2 b/src/onapsdk/aai/templates/site_resource_create.json.j2 new file mode 100644 index 0000000..caaf291 --- /dev/null +++ b/src/onapsdk/aai/templates/site_resource_create.json.j2 @@ -0,0 +1,16 @@ +{ + {% if site_resource_name is not none %}"site-resource-name": "{{ site_resource_name }}",{% endif %} + {% if description is not none %}"description": "{{ description }}",{% endif %} + {% if site_resource_type is not none %}"type": "{{ site_resource_type }}",{% endif %} + {% if role is not none %}"role": "{{ role }}",{% endif %} + {% if generated_site_id is not none %}"generated-site-id": "{{ generated_site_id }}",{% endif %} + {% if selflink is not none %}"selflink": "{{ selflink }}",{% endif %} + {% if operational_status is not none %}"operational-status": "{{ operational_status }}",{% endif %} + {% if model_customization_id is not none %}"model-customization-id": "{{ model_customization_id }}",{% endif %} + {% if model_invariant_id is not none %}"model-invariant-id": "{{ model_invariant_id }}",{% endif %} + {% if model_version_id is not none %}"model-version-id": "{{ model_version_id }}",{% endif %} + {% if data_owner is not none %}"data-owner": "{{ data_owner }}",{% endif %} + {% if data_source is not none %}"data-source": "{{ data_source }}",{% endif %} + {% if data_source_version is not none %}"data-source-version": "{{ data_source_version }}",{% endif %} + "site-resource-id": "{{ site_resource_id }}" +}
\ No newline at end of file diff --git a/src/onapsdk/cds/README.md b/src/onapsdk/cds/README.md new file mode 100644 index 0000000..5875e43 --- /dev/null +++ b/src/onapsdk/cds/README.md @@ -0,0 +1,71 @@ +# CDS module # + +## Load blueprint ## + +``` +>>> from onapsdk.cds import Blueprint +>>> blueprint = Blueprint.load_from_file("<< path to CBA file >>") # load a blueprint from ZIP file +``` + +## Enrich, publish blueprint + +``` +>>> enriched_blueprint = blueprint.enrich() # returns enriched blueprint object +>>> enriched_blueprint.publish() +``` + +## Execute blueprint workflow + +``` +>>> blueprint.workflows +[Workflow(name='resource-assignment', blueprint_name='vDNS-CDS-test1)', Workflow(name='config-assign', blueprint_name='vDNS-CDS-test1)', Workflow(name='config-deploy', blueprint_name='vDNS-CDS-test1)'] +>>> workflow = blueprint.workflows[0] # get the first workflow named 'resource-assignment` +>>> workflow.inputs # display what workflow needs as an input +[Workflow.WorkflowInput(name='template-prefix', required=True, type='list', description=None), Workflow.WorkflowInput(name='resource-assignment-properties', required=True, type='dt-resource-assignment-properties', description='Dynamic PropertyDefinition for workflow(resource-assignment).')] +>>> response = workflow.execute({"template-prefix": ["vpkg"], "resource-assignment-properties": {}}) # execute workflow with required inputs +``` + +## Generate data dictionary for blueprint + +Generated data dictionaries have to be manually filled for "source-rest" and "source-db" input types. + +``` +>>> blueprint.get_data_dictionaries().save_to_file("/tmp/dd.json") # generate data dictionaries for blueprint and save it to "/tmp/dd.json" file +``` + +## Manage Blueprint Models in CDS + +### Retrieve Blueprint Models from CDS + - All +``` +>>> from onapsdk.cds import BlueprintModel +>>> all_blueprint_models = BlueprintModel.get_all() +``` + - Selected by **id** of Blueprint Model +``` +>>> blueprint_model = BlueprintModel.get_by_id(blueprint_model_id='11111111-1111-1111-1111-111111111111') +>>> blueprint_model +BlueprintModel(artifact_name='test_name', blueprint_model_id='11111111-1111-1111-1111-111111111111') +``` +- Selected by **name and version** of Blueprint Model +``` +>>> blueprint_model = BlueprintModel.get_by_name_and_version(blueprint_name='test_name', blueprint_version='1.0.0') +>>> blueprint_model +BlueprintModel(artifact_name='test_name', blueprint_model_id='11111111-1111-1111-1111-111111111111') +``` + +### Delete Blueprint Model +``` +>>> blueprint_model.delete() +``` + +### Download Blueprint Model +``` +>>> blueprint_model.save(dst_file_path='/tmp/blueprint.zip') +``` + +### Get Blueprint object for Blueprint Model +``` +>>> blueprint = blueprint_model.get_blueprint() +``` +After that, all operation for blueprint object, like execute blueprint workflow etc. can be executed. diff --git a/src/onapsdk/cds/__init__.py b/src/onapsdk/cds/__init__.py new file mode 100644 index 0000000..f58e7e1 --- /dev/null +++ b/src/onapsdk/cds/__init__.py @@ -0,0 +1,18 @@ +"""ONAP SDK CDS package.""" +# Copyright 2022 Orange, 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 .blueprint import Blueprint +from .blueprint_model import BlueprintModel +from .data_dictionary import DataDictionarySet diff --git a/src/onapsdk/cds/blueprint.py b/src/onapsdk/cds/blueprint.py new file mode 100644 index 0000000..1286375 --- /dev/null +++ b/src/onapsdk/cds/blueprint.py @@ -0,0 +1,830 @@ +"""CDS Blueprint module.""" +# Copyright 2022 Orange, 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 json +import re +from dataclasses import dataclass, field +from datetime import datetime +from io import BytesIO +from typing import Any, Dict, Generator, Iterator, List, Optional +from urllib.parse import urlencode +from uuid import uuid4 +from zipfile import ZipFile + +import oyaml as yaml + +from onapsdk.utils.jinja import jinja_env +from onapsdk.exceptions import FileError, ParameterError, ValidationError + +from .cds_element import CdsElement +from .data_dictionary import DataDictionary, DataDictionarySet + + +@dataclass +class CbaMetadata: + """Class to hold CBA metadata values.""" + + tosca_meta_file_version: str + csar_version: str + created_by: str + entry_definitions: str + template_name: str + template_version: str + template_tags: str + + +@dataclass +class Mapping: + """Blueprint's template mapping. + + Stores mapping data: + - name, + - type, + - name of dictionary from which value should be get, + - dictionary source of value. + """ + + name: str + mapping_type: str + dictionary_name: str + dictionary_sources: List[str] = field(default_factory=list) + + def __hash__(self) -> int: # noqa: D401 + """Mapping object hash. + + Based on mapping name. + + Returns: + int: Mapping hash + + """ + return hash(self.name) + + def __eq__(self, mapping: "Mapping") -> bool: + """Compare two mapping objects. + + Mappings are equal if have the same name. + + Args: + mapping (Mapping): Mapping object to compare with. + + Returns: + bool: True if objects have the same name, False otherwise. + + """ + return self.name == mapping.name + + def merge(self, mapping: "Mapping") -> None: + """Merge mapping objects. + + Merge objects dictionary sources. + + Args: + mapping (Mapping): Mapping object to merge. + + """ + self.dictionary_sources = list( + set(self.dictionary_sources) | set(mapping.dictionary_sources) + ) + + def generate_data_dictionary(self) -> dict: + """Generate data dictionary for mapping. + + Data dictionary with required data sources, type and name for mapping will be created from + Jinja2 template. + + Returns: + dict: Data dictionary + + """ + return json.loads( + jinja_env().get_template("data_dictionary_base.json.j2").render(mapping=self) + ) + + +class MappingSet: + """Set of mapping objects. + + Mapping objects will be stored in dictionary where mapping name is a key. + No two mappings with the same name can be stored in this collection. + """ + + def __init__(self) -> None: + """Initialize mappings collection. + + Create dictionary to store mappings. + """ + self.mappings = {} + + def __len__(self) -> int: # noqa: D401 + """Mapping set length. + + Returns: + int: Number of stored mapping objects. + + """ + return len(self.mappings) + + def __iter__(self) -> Iterator[Mapping]: + """Iterate through mapping stored in set. + + Returns: + Iterator[Mapping]: Stored mappings iterator. + + """ + return iter(list(self.mappings.values())) + + def __getitem__(self, index: int) -> Mapping: + """Get item stored on given index. + + Args: + index (int): Index number. + + Returns: + Mapping: Mapping stored on given index. + + """ + return list(self.mappings.values())[index] + + def add(self, mapping: Mapping) -> None: + """Add mapping to set. + + If there is already mapping object with the same name in collection + they will be merged. + + Args: + mapping (Mapping): Mapping to add to collection. + + """ + if mapping.name not in self.mappings: + self.mappings.update({mapping.name: mapping}) + else: + self.mappings[mapping.name].merge(mapping) + + def extend(self, iterable: Iterator[Mapping]) -> None: + """Extend set with an iterator of mappings. + + Args: + iterable (Iterator[Mapping]): Mappings iterator. + + """ + for mapping in iterable: + self.add(mapping) + + +class Workflow(CdsElement): + """Blueprint's workflow. + + Stores workflow steps, inputs, outputs. + Executes workflow using CDS HTTP API. + """ + + @dataclass + class WorkflowStep: + """Workflow step class. + + Stores step name, description, target and optional activities. + """ + + name: str + description: str + target: str + activities: List[Dict[str, str]] = field(default_factory=list) + + @dataclass + class WorkflowInput: + """Workflow input class. + + Stores input name, information if it's required, type, and optional description. + """ + + name: str + required: bool + type: str + description: str = "" + + @dataclass + class WorkflowOutput: + """Workflow output class. + + Stores output name, type na value. + """ + + name: str + type: str + value: Dict[str, Any] + + def __init__(self, + cba_workflow_name: str, + cba_workflow_data: dict, + blueprint: "Blueprint") -> None: + """Workflow initialization. + + Args: + cba_workflow_name (str): Workflow name. + cba_workflow_data (dict): Workflow data. + blueprint (Blueprint): Blueprint object which contains workflow. + + """ + super().__init__() + self.name: str = cba_workflow_name + self.workflow_data: dict = cba_workflow_data + self.blueprint: "Blueprint" = blueprint + self._steps: List[self.WorkflowStep] = None + self._inputs: List[self.WorkflowInput] = None + self._outputs: List[self.WorkflowOutput] = None + + def __repr__(self) -> str: + """Representation of object. + + Returns: + str: Object's string representation + + """ + return (f"Workflow(name='{self.name}', " + f"blueprint_name='{self.blueprint.metadata.template_name})'") + + @property + def steps(self) -> List["Workflow.WorkflowStep"]: + """Workflow's steps property. + + Returns: + List[Workflow.WorkflowStep]: List of workflow's steps. + + """ + if self._steps is None: + self._steps = [] + for step_name, step_data in self.workflow_data.get("steps", {}).items(): + self._steps.append( + self.WorkflowStep( + name=step_name, + description=step_data.get("description"), + target=step_data.get("target"), + activities=step_data.get("activities", []), + ) + ) + return self._steps + + @property + def inputs(self) -> List["Workflow.WorkflowInput"]: + """Workflow's inputs property. + + Returns: + List[Workflow.WorkflowInput]: List of workflows's inputs. + + """ + if self._inputs is None: + self._inputs = [] + for input_name, input_data in self.workflow_data.get("inputs", {}).items(): + self._inputs.append( + self.WorkflowInput( + name=input_name, + required=input_data.get("required"), + type=input_data.get("type"), + description=input_data.get("description"), + ) + ) + return self._inputs + + @property + def outputs(self) -> List["Workflow.WorkflowOutput"]: + """Workflow's outputs property. + + Returns: + List[Workflow.WorkflowOutput]: List of workflows's outputs. + + """ + if self._outputs is None: + self._outputs = [] + for output_name, output_data in self.workflow_data.get("outputs", {}).items(): + self._outputs.append( + self.WorkflowOutput( + name=output_name, + type=output_data.get("type"), + value=output_data.get("value"), + ) + ) + return self._outputs + + @property + def url(self) -> str: + """Workflow execution url. + + Returns: + str: Url to call warkflow execution. + + """ + return f"{self._url}/api/v1/execution-service/process" + + def execute(self, inputs: dict) -> dict: + """Execute workflow. + + Call CDS HTTP API to execute workflow. + + Args: + inputs (dict): Inputs dictionary. + + Returns: + dict: Response's payload. + + """ + # There should be some flague to check if CDS UI API is used or blueprintprocessor. + # For CDS UI API there is no endporint to execute workflow, so it has to be turned off. + execution_service_input: dict = { + "commonHeader": { + "originatorId": "onapsdk", + "requestId": str(uuid4()), + "subRequestId": str(uuid4()), + "timestamp": datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S.%fZ"), + }, + "actionIdentifiers": { + "blueprintName": self.blueprint.metadata.template_name, + "blueprintVersion": self.blueprint.metadata.template_version, + "actionName": self.name, + "mode": "SYNC", # Has to be SYNC for REST call + }, + "payload": {f"{self.name}-request": inputs}, + } + response: "requests.Response" = self.send_message_json( + "POST", + f"Execute {self.blueprint.metadata.template_name} blueprint {self.name} workflow", + self.url, + auth=self.auth, + data=json.dumps(execution_service_input) + ) + return response["payload"] + + +class ResolvedTemplate(CdsElement): + """Resolved template class. + + Store and retrieve rendered template results. + """ + + def __init__(self, blueprint: "Blueprint", # pylint: disable=too-many-arguments + artifact_name: Optional[str] = None, + resolution_key: Optional[str] = None, + resource_id: Optional[str] = None, + resource_type: Optional[str] = None, + occurrence: Optional[str] = None, + response_format: str = "application/json") -> None: + """Init resolved template class instance. + + Args: + blueprint (Blueprint): Blueprint object. + artifact_name (Optional[str], optional): Artifact name for which to retrieve + a resolved resource. Defaults to None. + resolution_key (Optional[str], optional): Resolution Key associated with + the resolution. Defaults to None. + resource_id (Optional[str], optional): Resource Id associated with + the resolution. Defaults to None. + resource_type (Optional[str], optional): Resource Type associated + with the resolution. Defaults to None. + occurrence (Optional[str], optional): Occurrence of the template resolution (1-n). + Defaults to None. + response_format (str): Expected format of the template being retrieved. + + """ + super().__init__() + self.blueprint: "Blueprint" = blueprint + self.artifact_name: Optional[str] = artifact_name + self.resolution_key: Optional[str] = resolution_key + self.resource_id: Optional[str] = resource_id + self.resource_type: Optional[str] = resource_type + self.occurrence: Optional[str] = occurrence + self.response_format: str = response_format + + @property + def url(self) -> str: + """Url property. + + Returns: + str: Url + + """ + return f"{self._url}/api/v1/template" + + @property + def resolved_template_url(self) -> str: + """Url to retrieve resolved template. + + Filter None parameters. + + Returns: + str: Retrieve resolved template url + + """ + params_dict: Dict[str, str] = urlencode(dict(filter(lambda item: item[1] is not None, { + "bpName": self.blueprint.metadata.template_name, + "bpVersion": self.blueprint.metadata.template_version, + "artifactName": self.artifact_name, + "resolutionKey": self.resolution_key, + "resourceType": self.resource_type, + "resourceId": self.resource_id, + "occurrence": self.occurrence, + "format": self.response_format + }.items()))) + return f"{self.url}?{params_dict}" + + def get_resolved_template(self) -> Dict[str, str]: + """Get resolved template. + + Returns: + Dict[str, str]: Resolved template + + """ + return self.send_message_json( + "GET", + f"Get resolved template {self.artifact_name} for " + f"{self.blueprint.metadata.template_name} version " + f"{self.blueprint.metadata.template_version}", + self.resolved_template_url, + auth=self.auth + ) + + def store_resolved_template(self, resolved_template: str) -> None: + """Store resolved template. + + Args: + resolved_template (str): Template to store + + Raises: + ParameterError: To store template it's needed to pass artifact name and: + - resolution key, or + - resource type and resource id. + If not all needed parameters are given that exception will be raised. + + """ + if self.artifact_name and self.resolution_key: + return self.store_resolved_template_with_resolution_key(resolved_template) + if self.artifact_name and self.resource_type and self.resource_id: + return self.store_resolved_template_with_resource_type_and_id(resolved_template) + raise ParameterError("To store template artifact name with resolution key or both " + "resource type and id is needed") + + def store_resolved_template_with_resolution_key(self, resolved_template: str) -> None: + """Store template using resolution key. + + Args: + resolved_template (str): Template to store + + """ + return self.send_message( + "POST", + f"Store resolved template {self.artifact_name} for " + f"{self.blueprint.metadata.template_name} version " + f"{self.blueprint.metadata.template_version}", + f"{self.url}/{self.blueprint.metadata.template_name}/" + f"{self.blueprint.metadata.template_version}/{self.artifact_name}/" + f"{self.resolution_key}", + auth=self.auth, + data=resolved_template + ) + + def store_resolved_template_with_resource_type_and_id(self, resolved_template: str) -> None: + """Store template using resource type and resource ID. + + Args: + resolved_template (str): Template to store + + """ + return self.send_message( + "POST", + f"Store resolved template {self.artifact_name} for " + f"{self.blueprint.metadata.template_name} version " + f"{self.blueprint.metadata.template_version}", + f"{self.url}/{self.blueprint.metadata.template_name}/" + f"{self.blueprint.metadata.template_version}/{self.artifact_name}/" + f"{self.resource_type}/{self.resource_id}", + auth=self.auth, + data=resolved_template + ) + +class Blueprint(CdsElement): + """CDS blueprint representation.""" + + TEMPLATES_RE = r"Templates\/.*json$" + TOSCA_META = "TOSCA-Metadata/TOSCA.meta" + + def __init__(self, cba_file_bytes: bytes) -> None: + """Blueprint initialization. + + Save blueprint zip file bytes. + You can create that object using opened file or bytes: + blueprint = Blueprint(open("path/to/CBA.zip", "rb")) + or + with open("path/to/CBA.zip", "rb") as cba: + blueprint = Blueprint(cba.read()) + It is even better to use second example due to CBA file will be correctly closed for sure. + + Args: + cba_file_bytes (bytes): CBA ZIP file bytes + + """ + super().__init__() + self.cba_file_bytes: bytes = cba_file_bytes + self._cba_metadata: CbaMetadata = None + self._cba_mappings: MappingSet = None + self._cba_workflows: List[Workflow] = None + + @property + def url(self) -> str: + """URL address to use for CDS API call. + + Returns: + str: URL to CDS blueprintprocessor. + + """ + return f"{self._url}/api/v1/blueprint-model" + + @property + def metadata(self) -> CbaMetadata: + """Blueprint metadata. + + Data from TOSCA.meta file. + + Returns: + CbaMetadata: Blueprint metadata object. + + """ + if not self._cba_metadata: + with ZipFile(BytesIO(self.cba_file_bytes)) as cba_zip_file: + self._cba_metadata = self.get_cba_metadata(cba_zip_file.read(self.TOSCA_META)) + return self._cba_metadata + + @property + def mappings(self) -> MappingSet: + """Blueprint mappings collection. + + Returns: + MappingSet: Mappings collection. + + """ + if not self._cba_mappings: + with ZipFile(BytesIO(self.cba_file_bytes)) as cba_zip_file: + self._cba_mappings = self.get_mappings(cba_zip_file) + return self._cba_mappings + + @property + def workflows(self) -> List["Workflow"]: + """Blueprint's workflows property. + + Returns: + List[Workflow]: Blueprint's workflow list. + + """ + if not self._cba_workflows: + with ZipFile(BytesIO(self.cba_file_bytes)) as cba_zip_file: + self._cba_workflows = list( + self.get_workflows(cba_zip_file.read(self.metadata.entry_definitions)) + ) + return self._cba_workflows + + @classmethod + def load_from_file(cls, cba_file_path: str) -> "Blueprint": + """Load blueprint from file. + + Raises: + FileError: File to load blueprint from doesn't exist. + + Returns: + Blueprint: Blueprint object + + """ + try: + with open(cba_file_path, "rb") as cba_file: + return Blueprint(cba_file.read()) + except FileNotFoundError as exc: + msg = "The requested file with a blueprint doesn't exist." + raise FileError(msg) from exc + + def enrich(self) -> "Blueprint": + """Call CDS API to get enriched blueprint file. + + Returns: + Blueprint: Enriched blueprint object + + """ + response: "requests.Response" = self.send_message( + "POST", + "Enrich CDS blueprint", + f"{self.url}/enrich", + files={"file": self.cba_file_bytes}, + headers={}, # Leave headers empty to fill it correctly by `requests` library + auth=self.auth + ) + return Blueprint(response.content) + + def publish(self) -> None: + """Publish blueprint.""" + self.send_message( + "POST", + "Publish CDS blueprint", + f"{self.url}/publish", + files={"file": self.cba_file_bytes}, + headers={}, # Leave headers empty to fill it correctly by `requests` library + auth=self.auth + ) + + def deploy(self) -> None: + """Deploy blueprint.""" + self.send_message( + "POST", + "Deploy CDS blueprint", + f"{self.url}", + files={"file": self.cba_file_bytes}, + headers={}, # Leave headers empty to fill it correctly by `requests` library + auth=self.auth + ) + + def save(self, dest_file_path: str) -> None: + """Save blueprint to file. + + Args: + dest_file_path (str): Path of file where blueprint is going to be saved + + """ + with open(dest_file_path, "wb") as cba_file: + cba_file.write(self.cba_file_bytes) + + def get_data_dictionaries(self) -> DataDictionarySet: + """Get the generated data dictionaries required by blueprint. + + If mapping reqires other source than input it should be updated before upload to CDS. + + Returns: + Generator[DataDictionary, None, None]: DataDictionary objects. + + """ + dd_set: DataDictionarySet = DataDictionarySet() + for mapping in self.mappings: + dd_set.add(DataDictionary(mapping.generate_data_dictionary())) + return dd_set + + @staticmethod + def get_cba_metadata(cba_tosca_meta_bytes: bytes) -> CbaMetadata: + """Parse CBA TOSCA.meta file and get values from it. + + Args: + cba_tosca_meta_bytes (bytes): TOSCA.meta file bytes. + + Raises: + ValidationError: TOSCA Meta file has invalid format. + + Returns: + CbaMetadata: Dataclass with CBA metadata + + """ + meta_dict: dict = yaml.safe_load(cba_tosca_meta_bytes) + if not isinstance(meta_dict, dict): + raise ValidationError("Invalid TOSCA Meta file") + return CbaMetadata( + tosca_meta_file_version=meta_dict.get("TOSCA-Meta-File-Version"), + csar_version=meta_dict.get("CSAR-Version"), + created_by=meta_dict.get("Created-By"), + entry_definitions=meta_dict.get("Entry-Definitions"), + template_name=meta_dict.get("Template-Name"), + template_version=meta_dict.get("Template-Version"), + template_tags=meta_dict.get("Template-Tags"), + ) + + @staticmethod + def get_mappings_from_mapping_file(cba_mapping_file_bytes: bytes + ) -> Generator[Mapping, None, None]: + """Read mapping file and create Mappping object for it. + + Args: + cba_mapping_file_bytes (bytes): CBA mapping file bytes. + + Yields: + Generator[Mapping, None, None]: Mapping object. + + """ + mapping_file_json = json.loads(cba_mapping_file_bytes) + for mapping in mapping_file_json: + yield Mapping( + name=mapping["name"], + mapping_type=mapping["property"]["type"], + dictionary_name=mapping["dictionary-name"], + dictionary_sources=[mapping["dictionary-source"]], + ) + + def get_mappings(self, cba_zip_file: ZipFile) -> MappingSet: + """Read mappings from CBA file. + + Search mappings in CBA file and create Mapping object for each of them. + + Args: + cba_zip_file (ZipFile): CBA file to get mappings from. + + Returns: + MappingSet: Mappings set object. + + """ + mapping_set = MappingSet() + for info in cba_zip_file.infolist(): + if re.match(self.TEMPLATES_RE, info.filename): + mapping_set.extend( + self.get_mappings_from_mapping_file(cba_zip_file.read(info.filename)) + ) + return mapping_set + + def get_workflows(self, + cba_entry_definitions_file_bytes: bytes) -> Generator[Workflow, None, None]: + """Get worfklows from entry_definitions file. + + Parse entry_definitions file and create Workflow objects for workflows stored in. + + Args: + cba_entry_definitions_file_bytes (bytes): entry_definition file. + + Yields: + Generator[Workflow, None, None]: Workflow object. + + """ + entry_definitions_json: dict = json.loads(cba_entry_definitions_file_bytes) + workflows: dict = entry_definitions_json.get("topology_template", {}).get("workflows", {}) + for workflow_name, workflow_data in workflows.items(): + yield Workflow(workflow_name, workflow_data, self) + + def get_workflow_by_name(self, workflow_name: str) -> Workflow: + """Get workflow by name. + + If there is no workflow with given name `ParameterError` is going to be raised. + + Args: + workflow_name (str): Name of the workflow + + Returns: + Workflow: Workflow with given name + + """ + try: + return next(filter(lambda workflow: workflow.name == workflow_name, self.workflows)) + except StopIteration: + raise ParameterError("Workflow with given name does not exist") + + def get_resolved_template(self, # pylint: disable=too-many-arguments + artifact_name: str, + resolution_key: Optional[str] = None, + resource_type: Optional[str] = None, + resource_id: Optional[str] = None, + occurrence: Optional[str] = None) -> Dict[str, str]: + """Get resolved template for Blueprint. + + Args: + artifact_name (str): Resolved template's artifact name + resolution_key (Optional[str], optional): Resolved template's resolution key. + Defaults to None. + resource_type (Optional[str], optional): Resolved template's resource type. + Defaults to None. + resource_id (Optional[str], optional): Resolved template's resource ID. + Defaults to None. + occurrence: (Optional[str], optional): Resolved template's occurrence value. + Defaults to None. + + Returns: + Dict[str, str]: Resolved template + + """ + return ResolvedTemplate(blueprint=self, + artifact_name=artifact_name, + resolution_key=resolution_key, + resource_type=resource_type, + resource_id=resource_id, + occurrence=occurrence).get_resolved_template() + + def store_resolved_template(self, # pylint: disable=too-many-arguments + artifact_name: str, + data: str, + resolution_key: Optional[str] = None, + resource_type: Optional[str] = None, + resource_id: Optional[str] = None) -> None: + """Store resolved template for Blueprint. + + Args: + artifact_name (str): Resolved template's artifact name + data (str): Resolved template + resolution_key (Optional[str], optional): Resolved template's resolution key. + Defaults to None. + resource_type (Optional[str], optional): Resolved template's resource type. + Defaults to None. + resource_id (Optional[str], optional): Resolved template's resource ID. + Defaults to None. + """ + ResolvedTemplate(blueprint=self, + artifact_name=artifact_name, + resolution_key=resolution_key, + resource_type=resource_type, + resource_id=resource_id).store_resolved_template(data) diff --git a/src/onapsdk/cds/blueprint_model.py b/src/onapsdk/cds/blueprint_model.py new file mode 100644 index 0000000..7976001 --- /dev/null +++ b/src/onapsdk/cds/blueprint_model.py @@ -0,0 +1,222 @@ +"""CDS Blueprint Models module.""" +# Copyright 2022 Orange, 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 Iterator +from onapsdk.exceptions import ResourceNotFound # for custom exceptions + +from .blueprint import Blueprint +from .cds_element import CdsElement + + +class BlueprintModel(CdsElement): # pylint: disable=too-many-instance-attributes + """Blueprint Model class. + + Represents blueprint models in CDS + """ + + def __init__(self, # pylint: disable=too-many-arguments + blueprint_model_id: str, + artifact_uuid: str = None, + artifact_type: str = None, + artifact_version: str = None, + artifact_description: str = None, + internal_version: str = None, + created_date: str = None, + artifact_name: str = None, + published: str = 'N', + updated_by: str = None, + tags: str = None): + """Blueprint Model initialization. + + Args: + blueprint_model_id (str): Blueprint model identifier + artifact_uuid (str): Blueprint model uuid + artifact_type (str): Blueprint artifact type + artifact_version (str): Blueprint model version + artifact_description (str): Blueprint model description + internal_version (str): Blueprint model internal version + created_date (str): Blueprint model created date + artifact_name (str): Blueprint model name + published (str): Blueprint model publish status - 'N' or 'Y' + updated_by (str): Blueprint model author + tags (str): Blueprint model tags + + """ + super().__init__() + self.blueprint_model_id = blueprint_model_id + self.artifact_uuid = artifact_uuid + self.artifact_type = artifact_type + self.artifact_version = artifact_version + self.artifact_description = artifact_description + self.internal_version = internal_version + self.created_date = created_date + self.artifact_name = artifact_name + self.published = published + self.updated_by = updated_by + self.tags = tags + + def __repr__(self) -> str: + """Representation of object. + + Returns: + str: Object's string representation + + """ + return (f"BlueprintModel(artifact_name='{self.artifact_name}', " + f"blueprint_model_id='{self.blueprint_model_id}')") + + @classmethod + def get_by_id(cls, blueprint_model_id: str) -> "BlueprintModel": + """Retrieve blueprint model with provided ID. + + Args: blueprint_model_id (str): + + Returns: + BlueprintModel: Blueprint model object + + Raises: + ResourceNotFound: Blueprint model with provided ID doesn't exist + + """ + try: + blueprint_model = cls.send_message_json( + "GET", + "Retrieve blueprint", + f"{cls._url}/api/v1/blueprint-model/{blueprint_model_id}", + auth=cls.auth) + + return cls( + blueprint_model_id=blueprint_model["blueprintModel"]['id'], + artifact_uuid=blueprint_model["blueprintModel"]['artifactUUId'], + artifact_type=blueprint_model["blueprintModel"]['artifactType'], + artifact_version=blueprint_model["blueprintModel"]['artifactVersion'], + internal_version=blueprint_model["blueprintModel"]['internalVersion'], + created_date=blueprint_model["blueprintModel"]['createdDate'], + artifact_name=blueprint_model["blueprintModel"]['artifactName'], + published=blueprint_model["blueprintModel"]['published'], + updated_by=blueprint_model["blueprintModel"]['updatedBy'], + tags=blueprint_model["blueprintModel"]['tags'] + ) + + except ResourceNotFound: + raise ResourceNotFound(f"BlueprintModel blueprint_model_id='{blueprint_model_id}" + f" not found") + + @classmethod + def get_by_name_and_version(cls, blueprint_name: str, + blueprint_version: str) -> "BlueprintModel": + """Retrieve blueprint model with provided name and version. + + Args: + blueprint_name (str): Blueprint model name + blueprint_version (str): Blueprint model version + + Returns: + BlueprintModel: Blueprint model object + + Raises: + ResourceNotFound: Blueprint model with provided name and version doesn't exist + + """ + try: + blueprint_model = cls.send_message_json( + "GET", + "Retrieve blueprint", + f"{cls._url}/api/v1/blueprint-model/by-name/{blueprint_name}" + f"/version/{blueprint_version}", + auth=cls.auth) + + return cls( + blueprint_model_id=blueprint_model["blueprintModel"]['id'], + artifact_uuid=blueprint_model["blueprintModel"]['artifactUUId'], + artifact_type=blueprint_model["blueprintModel"]['artifactType'], + artifact_version=blueprint_model["blueprintModel"]['artifactVersion'], + internal_version=blueprint_model["blueprintModel"]['internalVersion'], + created_date=blueprint_model["blueprintModel"]['createdDate'], + artifact_name=blueprint_model["blueprintModel"]['artifactName'], + published=blueprint_model["blueprintModel"]['published'], + updated_by=blueprint_model["blueprintModel"]['updatedBy'], + tags=blueprint_model["blueprintModel"]['tags'] + ) + + except ResourceNotFound: + raise ResourceNotFound(f"BlueprintModel blueprint_name='{blueprint_name}" + f" and blueprint_version='{blueprint_version}' not found") + + @classmethod + def get_all(cls) -> Iterator["BlueprintModel"]: + """Get all blueprint models. + + Yields: + BlueprintModel: BlueprintModel object. + + """ + for blueprint_model in cls.send_message_json( + "GET", + "Retrieve all blueprints", + f"{cls._url}/api/v1/blueprint-model", + auth=cls.auth): + + yield cls( + blueprint_model_id=blueprint_model["blueprintModel"]['id'], + artifact_uuid=blueprint_model["blueprintModel"]['artifactUUId'], + artifact_type=blueprint_model["blueprintModel"]['artifactType'], + artifact_version=blueprint_model["blueprintModel"]['artifactVersion'], + internal_version=blueprint_model["blueprintModel"]['internalVersion'], + created_date=blueprint_model["blueprintModel"]['createdDate'], + artifact_name=blueprint_model["blueprintModel"]['artifactName'], + published=blueprint_model["blueprintModel"]['published'], + updated_by=blueprint_model["blueprintModel"]['updatedBy'], + tags=blueprint_model["blueprintModel"]['tags'] + ) + + def get_blueprint(self) -> Blueprint: + """Get Blueprint object for selected blueprint model. + + Returns: + Blueprint: Blueprint object + + """ + cba_package = self.send_message( + "GET", + "Retrieve selected blueprint object", + f"{self._url}/api/v1/blueprint-model/download/{self.blueprint_model_id}", + auth=self.auth) + + return Blueprint(cba_file_bytes=cba_package.content) + + def save(self, dst_file_path: str): + """Save blueprint model to file. + + Args: + dst_file_path (str): Path of file where blueprint is going to be saved + """ + cba_package = self.send_message( + "GET", + "Retrieve and save selected blueprint", + f"{self._url}/api/v1/blueprint-model/download/{self.blueprint_model_id}", + auth=self.auth) + + with open(dst_file_path, "wb") as content: + for chunk in cba_package.iter_content(chunk_size=128): + content.write(chunk) + + def delete(self): + """Delete blueprint model.""" + self.send_message( + "DELETE", + "Delete blueprint", + f"{self._url}/api/v1/blueprint-model/{self.blueprint_model_id}", + auth=self.auth) diff --git a/src/onapsdk/cds/blueprint_processor.py b/src/onapsdk/cds/blueprint_processor.py new file mode 100644 index 0000000..3763e1b --- /dev/null +++ b/src/onapsdk/cds/blueprint_processor.py @@ -0,0 +1,53 @@ +"""CDS Blueprintprocessor module.""" +# Copyright 2022 Orange, 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 onapsdk.utils.jinja import jinja_env + +from .cds_element import CdsElement + + +class Blueprintprocessor(CdsElement): + """Blueprintprocessor class.""" + + @classmethod + def bootstrap(cls, + load_model_type: bool = True, + load_resource_dictionary: bool = True, + load_cba: bool = True) -> None: + """Bootstrap CDS blueprintprocessor. + + That action in needed to work with CDS. Can be done only once. + + Args: + load_model_type (bool, optional): Datermines if model types should be loaded + on bootstrap. Defaults to True. + load_resource_dictionary (bool, optional): Determines if resource dictionaries + should be loaded on bootstrap. Defaults to True. + load_cba (bool, optional): Determines if cba files should be loaded on + bootstrap. Defaults to True. + + """ + cls.send_message( + "POST", + "Bootstrap CDS blueprintprocessor", + f"{cls._url}/api/v1/blueprint-model/bootstrap", + data=jinja_env().get_template("cds_blueprintprocessor_bootstrap.json.j2").render( + load_model_type=load_model_type, + load_resource_dictionary=load_resource_dictionary, + load_cba=load_cba + ), + auth=cls.auth, + headers=cls.headers + ) diff --git a/src/onapsdk/cds/cds_element.py b/src/onapsdk/cds/cds_element.py new file mode 100644 index 0000000..7a4b9c0 --- /dev/null +++ b/src/onapsdk/cds/cds_element.py @@ -0,0 +1,47 @@ +"""Base CDS module.""" +# Copyright 2022 Orange, 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 onapsdk.configuration import settings +from onapsdk.onap_service import OnapService +from onapsdk.utils.gui import GuiItem, GuiList + +class CdsElement(OnapService, ABC): + """Base CDS class. + + Stores url to CDS API (edit if you want to use other) and authentication tuple + (username, password). + """ + + # These should be stored in configuration. There is even a task in Orange repo. + _url: str = settings.CDS_URL + auth: tuple = settings.CDS_AUTH + + @classmethod + def get_guis(cls) -> GuiItem: + """Retrieve the status of the CDS GUIs. + + Only one GUI is referenced for CDS: CDS UI + + Return the list of GUIs + """ + gui_url = settings.CDS_GUI_SERVICE + cds_gui_response = cls.send_message( + "GET", "Get CDS GUI Status", gui_url) + guilist = GuiList([]) + guilist.add(GuiItem( + gui_url, + cds_gui_response.status_code)) + return guilist diff --git a/src/onapsdk/cds/data_dictionary.py b/src/onapsdk/cds/data_dictionary.py new file mode 100644 index 0000000..b4d8d0e --- /dev/null +++ b/src/onapsdk/cds/data_dictionary.py @@ -0,0 +1,266 @@ +"""CDS data dictionary module.""" +# Copyright 2022 Orange, 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 json +from logging import getLogger, Logger + +from onapsdk.exceptions import FileError, ValidationError + +from .cds_element import CdsElement + + +class DataDictionary(CdsElement): + """Data dictionary class.""" + + logger: Logger = getLogger(__name__) + + def __init__(self, data_dictionary_json: dict, fix_schema: bool = True) -> None: + """Initialize data dictionary. + + Args: + data_dictionary_json (dict): data dictionary json + fix_schema (bool, optional): determines if data dictionary should be fixed if + the invalid schema is detected. Fixing can raise ValidationError if + dictionary is invalid. Defaults to True. + + """ + super().__init__() + self.data_dictionary_json: dict = data_dictionary_json + if not self.has_valid_schema() and fix_schema: + self.fix_schema() + + def __hash__(self) -> int: # noqa: D401 + """Data dictionary object hash. + + Based on data dictionary name + + Returns: + int: Data dictionary hash + + """ + return hash(self.name) + + def __eq__(self, dd: "DataDictionary") -> bool: + """Compare two data dictionaries. + + Data dictionaries are equal if have the same name. + + Args: + dd (DataDictionary): Object to compare with. + + Returns: + bool: True if objects have the same name, False otherwise. + + """ + return self.name == dd.name + + def __repr__(self) -> str: + """Representation of object. + + Returns: + str: Object's string representation + + """ + return f'DataDictionary[name: "{self.name}"]' + + @property + def name(self) -> str: # noqa: D401 + """Data dictionary name. + + Returns: + str: Data dictionary name + + """ + return self.data_dictionary_json["name"] + + @property + def url(self) -> str: + """URL to call. + + Returns: + str: CDS dictionary API url + + """ + return f"{self._url}/api/v1/dictionary" + + @classmethod + def get_by_name(cls, name: str) -> "DataDictionary": + """Get data dictionary by the provided name. + + Returns: + DataDictionary: Data dicionary object with the given name + + """ + cls.logger.debug("Get CDS data dictionary with %s name", name) + return DataDictionary( + data_dictionary_json=cls.send_message_json( + "GET", + f"Get {name} CDS data dictionary", + f"{cls._url}/api/v1/dictionary/{name}", + auth=cls.auth), + fix_schema=False + ) + + def upload(self) -> None: + """Upload data dictionary using CDS API.""" + self.logger.debug("Upload %s data dictionary", self.name) + self.send_message( + "POST", + "Publish CDS data dictionary", + f"{self.url}", + auth=self.auth, + data=json.dumps(self.data_dictionary_json) + ) + + def has_valid_schema(self) -> bool: + """Check data dictionary json schema. + + Check data dictionary JSON and return bool if schema is valid or not. + Valid schema means that data dictionary has given keys: + - "name" + - "tags" + - "data_type" + - "description" + - "entry_schema" + - "updatedBy" + - "definition" + "definition" key value should contains the "raw" data dictionary. + + Returns: + bool: True if data dictionary has valid schema, False otherwise + + """ + return all(key_to_check in self.data_dictionary_json for + key_to_check in ["name", "tags", "data_type", "description", "entry_schema", + "updatedBy", "definition"]) + + def fix_schema(self) -> None: + """Fix data dictionary schema. + + "Raw" data dictionary can be passed during initialization, but + this kind of data dictionary can't be uploaded to blueprintprocessor. + That method tries to fix it. It can be done only if "raw" data dictionary + has a given schema: + { + "name": "string", + "tags": "string", + "updated-by": "string", + "property": { + "description": "string", + "type": "string" + } + } + + Raises: + ValidationError: Data dictionary doesn't have all required keys + + """ + try: + self.data_dictionary_json = { + "name": self.data_dictionary_json["name"], + "tags": self.data_dictionary_json["tags"], + "data_type": self.data_dictionary_json["property"]["type"], + "description": self.data_dictionary_json["property"]["description"], + "entry_schema": self.data_dictionary_json["property"]["type"], + "updatedBy": self.data_dictionary_json["updated-by"], + "definition": self.data_dictionary_json + } + except KeyError: + raise ValidationError("Raw data dictionary JSON has invalid schema") + + +class DataDictionarySet: + """Data dictionary set. + + Stores data dictionary and upload to server. + """ + + logger: Logger = getLogger(__name__) + + def __init__(self) -> None: + """Initialize data dictionary set.""" + self.dd_set = set() + + @property + def length(self) -> int: + """Get the length of data dicitonary set. + + Returns: + int: Number of data dictionaries in set + + """ + return len(self.dd_set) + + def add(self, data_dictionary: DataDictionary) -> None: + """Add data dictionary object to set. + + Based on name it won't add duplications. + + Args: + data_dictionary (DataDictionary): object to add to set. + + """ + self.dd_set.add(data_dictionary) + + def upload(self) -> None: + """Upload all data dictionaries using CDS API. + + Raises: + RuntimeError: Raises if any data dictionary won't be uploaded to server. + Data dictionaries uploaded before the one which raises excepion won't be + deleted from server. + + """ + self.logger.debug("Upload data dictionary") + for data_dictionary in self.dd_set: # type DataDictionary + data_dictionary.upload() # raise a relevant exception + + def save_to_file(self, dd_file_path: str) -> None: + """Save data dictionaries to file. + + Args: + dd_file_path (str): Data dictinary file path. + """ + with open(dd_file_path, "w") as dd_file: + dd_file.write(json.dumps([dd.data_dictionary_json for dd in self.dd_set], indent=4)) + + @classmethod + def load_from_file(cls, dd_file_path: str, fix_schema: bool = True) -> "DataDictionarySet": + """Create data dictionary set from file. + + File has to have valid JSON with data dictionaries list. + + Args: + dd_file_path (str): Data dictionaries file path. + fix_schema (bool): Determines if schema should be fixed or not. + + Raises: + FileError: File to load data dictionaries from doesn't exist. + + Returns: + DataDictionarySet: Data dictionary set with data dictionaries from given file. + + """ + dd_set: DataDictionarySet = DataDictionarySet() + + try: + with open(dd_file_path, "r") as dd_file: # type file + dd_json: dict = json.loads(dd_file.read()) + for data_dictionary in dd_json: # type DataDictionary + dd_set.add(DataDictionary(data_dictionary, fix_schema=fix_schema)) + return dd_set + except FileNotFoundError as exc: + msg = "File with a set of data dictionaries does not exist." + raise FileError(msg) from exc diff --git a/src/onapsdk/cds/templates/cds_blueprintprocessor_bootstrap.json.j2 b/src/onapsdk/cds/templates/cds_blueprintprocessor_bootstrap.json.j2 new file mode 100644 index 0000000..41f43cd --- /dev/null +++ b/src/onapsdk/cds/templates/cds_blueprintprocessor_bootstrap.json.j2 @@ -0,0 +1,5 @@ +{ + "loadModelType" : {{ load_model_type | tojson }}, + "loadResourceDictionary" : {{ load_resource_dictionary | tojson }}, + "loadCBA" : {{ load_cba | tojson }} +}
\ No newline at end of file diff --git a/src/onapsdk/cds/templates/data_dictionary_base.json.j2 b/src/onapsdk/cds/templates/data_dictionary_base.json.j2 new file mode 100644 index 0000000..0ea6752 --- /dev/null +++ b/src/onapsdk/cds/templates/data_dictionary_base.json.j2 @@ -0,0 +1,52 @@ +{ + "name": "{{ mapping.dictionary_name }}", + "tags": "{{ mapping.dictionary_name }}", + "data_type": "{{ mapping.mapping_type }}", + "description": "{{ mapping.dictionary_name }}", + "entry_schema": "{{ mapping.mapping_type }}", + "updatedBy": "Python ONAP SDK", + "definition": { + "tags": "{{ mapping.dictionary_name }}", + "name": "{{ mapping.dictionary_name }}", + "property": { + "description": "{{ mapping.dictionary_name }}", + "type": "{{ mapping.mapping_type }}" + }, + "updated-by": "Python ONAP SDK", + "sources": { + {% for source in mapping.dictionary_sources %} + {% if source == "input" %} + "input": { + "type": "source-input" + }, + {% elif source == "sdnc" %} + "sdnc": {% include "data_dictionary_source_rest.json.j2" %}, + {% elif source == "processor-db" %} + "processor-db": { + "type": "source-db", + "properties": { + "type": "<< FILL >>", + "query": "<< FILL >>", + "input-key-mapping": {}, + "output-key-mapping": {}, + "key-dependencies": [] + } + }, + {% elif source == "aai-data" %} + "aai-data": {% include "data_dictionary_source_rest.json.j2" %}, + {% elif source == "default" %} + {# Do not do anything, default will be always added #} + {% else %} + "{{ source }}": { + "type": "unknown", + "properties": {} + }, + {% endif %} + {% endfor %} + "default": { + "type": "source-default", + "properties": {} + } + } + } +}
\ No newline at end of file diff --git a/src/onapsdk/cds/templates/data_dictionary_source_rest.json.j2 b/src/onapsdk/cds/templates/data_dictionary_source_rest.json.j2 new file mode 100644 index 0000000..088a044 --- /dev/null +++ b/src/onapsdk/cds/templates/data_dictionary_source_rest.json.j2 @@ -0,0 +1,13 @@ +{ + "type": "source-rest", + "properties": { + "verb": "<< FILL >>", + "type": "<< FILL >>", + "url-path": "<< FILL >>", + "path": "<< FILL >>", + "payload": "<< FILL >>", + "input-key-mapping": {}, + "output-key-mapping": {}, + "key-dependencies": [] + } +}
\ No newline at end of file diff --git a/src/onapsdk/clamp/__init__.py b/src/onapsdk/clamp/__init__.py new file mode 100644 index 0000000..621adc9 --- /dev/null +++ b/src/onapsdk/clamp/__init__.py @@ -0,0 +1,14 @@ +"""ONAP SDK CLAMP package.""" +# Copyright 2022 Orange, 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/src/onapsdk/clamp/clamp_element.py b/src/onapsdk/clamp/clamp_element.py new file mode 100644 index 0000000..843db42 --- /dev/null +++ b/src/onapsdk/clamp/clamp_element.py @@ -0,0 +1,79 @@ +"""Clamp module.""" +# Copyright 2022 Orange, 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 onapsdk.configuration import settings +from onapsdk.onap_service import OnapService as Onap +from onapsdk.sdc.service import Service +from onapsdk.exceptions import ResourceNotFound +from onapsdk.utils.headers_creator import headers_clamp_creator + + +class Clamp(Onap): + """Mother Class of all CLAMP elements.""" + + #class variable + _base_url = settings.CLAMP_URL + name: str = "CLAMP" + headers = headers_clamp_creator(Onap.headers) + + @classmethod + def base_url(cls) -> str: + """Give back the base url of Clamp.""" + return f"{cls._base_url}/restservices/clds/v2" + + @classmethod + def check_loop_template(cls, service: Service) -> str: + """ + Return loop template name if exists. + + Args: + service (Service): the distributed sdc service with tca blueprint artifact + + Raises: + ResourceNotFound: Template not found. + + Returns: + if required template exists in CLAMP or not + + """ + url = f"{cls.base_url()}/templates/" + for template in cls.send_message_json('GET', + 'Get Loop Templates', + url): + if template["modelService"]["serviceDetails"]["name"] == service.name: + return template["name"] + raise ResourceNotFound("Template not found.") + + @classmethod + def check_policies(cls, policy_name: str, req_policies: int = 30) -> bool: + """ + Ensure that a policy is stored in CLAMP. + + Args: + policy_name (str): policy acronym + req_policies (int): number of required policies in CLAMP + + Returns: + if required policy exists in CLAMP or not + + """ + url = f"{cls.base_url()}/policyToscaModels/" + policies = cls.send_message_json('GET', + 'Get stocked policies', + url) + exist_policy = False + for policy in policies: + if policy["policyAcronym"] == policy_name: + exist_policy = True + return (len(policies) >= req_policies) and exist_policy diff --git a/src/onapsdk/clamp/loop_instance.py b/src/onapsdk/clamp/loop_instance.py new file mode 100644 index 0000000..a72f9d1 --- /dev/null +++ b/src/onapsdk/clamp/loop_instance.py @@ -0,0 +1,349 @@ +"""Control Loop module.""" +# Copyright 2022 Orange, 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 json +from pathlib import Path +from jsonschema import validate, ValidationError + +from onapsdk.clamp.clamp_element import Clamp +from onapsdk.utils.jinja import jinja_env +from onapsdk.exceptions import ParameterError + +CLAMP_UPDDATE_REFRESH_TIMER = 60 + +class LoopInstance(Clamp): + """Control Loop instantiation class.""" + + # class variable + _loop_schema = None + operational_policies = "" + + def __init__(self, template: str, name: str, details: dict) -> None: + """ + Initialize loop instance object. + + Args: + template (str): template from which we build the loop + name (str) : loop creation name + details (dict) : dictionnary containing all loop details + + """ + super().__init__() + self.template = template + self.name = "LOOP_" + name + self._details = details + + @property + def details(self) -> dict: + """Return and lazy load the details.""" + if not self._details: + self._update_loop_details() + return self._details + + @details.setter + def details(self, details: dict) -> None: + """Set value for details.""" + self._details = details + + def _update_loop_details(self) -> dict: + """ + Update loop details. + + Returns: + the dictionnary of loop details + + """ + url = f"{self.base_url()}/loop/{self.name}" + loop_details = self.send_message_json('GET', + 'Get loop details', + url) + return loop_details + + def refresh_status(self) -> None: + """Reshresh loop status.""" + url = f"{self.base_url()}/loop/getstatus/{self.name}" + loop_details = self.send_message_json('GET', + 'Get loop status', + url) + + self.details = loop_details + + @property + def loop_schema(self) -> dict: + """ + Return and lazy load the details schema. + + Returns: + schema to be respected to accede to loop details + + """ + if not self._loop_schema: + schema_file = Path.cwd() / 'src' / 'onapsdk' / 'clamp' / 'schema_details.json' + with open(schema_file, "rb") as plan: + self._loop_schema = json.load(plan) + return self._loop_schema + + def validate_details(self) -> bool: + """ + Validate Loop Instance details. + + Returns: + schema validation status (True, False) + + """ + try: + validate(self.details, self.loop_schema) + except ValidationError as error: + self._logger.error(error) + self._logger.error("---------") + self._logger.error(error.absolute_path) + self._logger.error("---------") + self._logger.error(error.absolute_schema_path) + return False + return True + + def create(self) -> None: + """Create instance and load loop details.""" + url = f"{self.base_url()}/loop/create/{self.name}?templateName={self.template}" + instance_details = self.send_message_json('POST', + 'Create Loop Instance', + url) + self.details = instance_details + + def add_operational_policy(self, policy_type: str, policy_version: str) -> None: + """ + Add operational policy to the loop instance. + + Args: + policy_type (str): the full policy model type + policy_version (str): policy version + + Raises: + ParameterError : Corrupt response or a key in a dictionary not found. + It will also be raised when the response contains more operational + policies than there are currently. + + """ + url = (f"{self.base_url()}/loop/addOperationaPolicy/{self.name}/" + f"policyModel/{policy_type}/{policy_version}") + add_response = self.send_message_json('PUT', + 'Create Operational Policy', + url) + + key = "operationalPolicies" + + try: + if self.details[key] is None: + self.details[key] = [] + + response_policies = add_response[key] + current_policies = self.details[key] + except KeyError as exc: + msg = 'Corrupt response, current loop details. Key not found.' + raise ParameterError(msg) from exc + + if len(response_policies) > len(current_policies): + self.details = add_response + else: + raise ParameterError("Couldn't add the operational policy.") + + def remove_operational_policy(self, policy_type: str, policy_version: str) -> None: + """ + Remove operational policy from the loop instance. + + Args: + policy_type (str): the full policy model type + policy_version (str): policy version + + """ + url = (f"{self.base_url()}/loop/removeOperationaPolicy/" + f"{self.name}/policyModel/{policy_type}/{policy_version}") + self.details = self.send_message_json('PUT', + 'Remove Operational Policy', + url) + + def update_microservice_policy(self) -> None: + """ + Update microservice policy configuration. + + Update microservice policy configuration using payload data. + + """ + url = f"{self.base_url()}/loop/updateMicroservicePolicy/{self.name}" + template = jinja_env().get_template("clamp_add_tca_config.json.j2") + microservice_name = self.details["globalPropertiesJson"]["dcaeDeployParameters"]\ + ["uniqueBlueprintParameters"]["policy_id"] + data = template.render(name=microservice_name, + LOOP_name=self.name) + + self.send_message('POST', + 'ADD TCA config', + url, + data=data) + + def extract_operational_policy_name(self, policy_type: str) -> str: + """ + Return operational policy name for a closed loop and a given policy. + + Args: + policy_type (str): the policy acronym. + + Raises: + ParameterError : Couldn't load the operational policy name. + + Returns: + Operational policy name in the loop details after adding a policy. + + """ + for policy in filter(lambda x: x["policyModel"]["policyAcronym"] == policy_type, + self.details["operationalPolicies"]): + return policy["name"] + + raise ParameterError("Couldn't load the operational policy name.") + + def add_drools_conf(self) -> dict: + """Add drools configuration.""" + self.validate_details() + vfmodule_dicts = self.details["modelService"]["resourceDetails"]["VFModule"] + entity_ids = {} + #Get the vf module details + for vfmodule in vfmodule_dicts.values(): + entity_ids["resourceID"] = vfmodule["vfModuleModelName"] + entity_ids["modelInvariantId"] = vfmodule["vfModuleModelInvariantUUID"] + entity_ids["modelVersionId"] = vfmodule["vfModuleModelUUID"] + entity_ids["modelName"] = vfmodule["vfModuleModelName"] + entity_ids["modelVersion"] = vfmodule["vfModuleModelVersion"] + entity_ids["modelCustomizationId"] = vfmodule["vfModuleModelCustomizationUUID"] + template = jinja_env().get_template("clamp_add_drools_policy.json.j2") + data = template.render(name=self.extract_operational_policy_name("Drools"), + resourceID=entity_ids["resourceID"], + modelInvariantId=entity_ids["modelInvariantId"], + modelVersionId=entity_ids["modelVersionId"], + modelName=entity_ids["modelName"], + modelVersion=entity_ids["modelVersion"], + modelCustomizationId=entity_ids["modelCustomizationId"], + LOOP_name=self.name) + return data + + def add_minmax_config(self) -> None: + """Add MinMax operational policy config.""" + #must configure start/end time and min/max instances in json file + template = jinja_env().get_template("clamp_MinMax_config.json.j2") + return template.render(name=self.extract_operational_policy_name("MinMax")) + + def add_frequency_limiter(self, limit: int = 1) -> None: + """Add frequency limiter config.""" + template = jinja_env().get_template("clamp_add_frequency.json.j2") + return template.render(name=self.extract_operational_policy_name("FrequencyLimiter"), + LOOP_name=self.name, + limit=limit) + + def add_op_policy_config(self, func, **kwargs) -> None: + """ + Add operational policy configuration. + + Add operation policy configuration using payload data. + + Args: + func (function): policy configuration function in (add_drools_conf, + add_minmax_config, + add_frequency_limiter) + + """ + data = func(**kwargs) + if not data: + raise ParameterError("Payload data from configuration is None.") + if self.operational_policies: + self.operational_policies = self.operational_policies[:-1] + "," + data = data[1:] + self.operational_policies += data + url = f"{self.base_url()}/loop/updateOperationalPolicies/{self.name}" + self.send_message('POST', + 'ADD operational policy config', + url, + data=self.operational_policies) + + self._logger.info(("Files for op policy config %s have been uploaded to loop's" + "Op policy"), self.name) + + def submit(self): + """Submit policies to policy engine.""" + state = self.details["components"]["POLICY"]["componentState"]["stateName"] + return state == "SENT_AND_DEPLOYED" + + def stop(self): + """Undeploy Policies from policy engine.""" + state = self.details["components"]["POLICY"]["componentState"]["stateName"] + return state == "SENT" + + def restart(self): + """Redeploy policies to policy engine.""" + state = self.details["components"]["POLICY"]["componentState"]["stateName"] + return state == "SENT_AND_DEPLOYED" + + def act_on_loop_policy(self, func) -> bool: + """ + Act on loop's policy. + + Args: + func (function): function of action to be done (submit, stop, restart) + + Returns: + action state : failed or done + + """ + url = f"{self.base_url()}/loop/{func.__name__}/{self.name}" + self.send_message('PUT', + f'{func.__name__} policy', + url) + self.refresh_status() + self.validate_details() + return func() + + def deploy_microservice_to_dcae(self) -> bool: + """ + Execute the deploy operation on the loop instance. + + Returns: + loop deploy on DCAE status (True, False) + + """ + url = f"{self.base_url()}/loop/deploy/{self.name}" + self.send_message('PUT', + 'Deploy microservice to DCAE', + url) + self.validate_details() + state = self.details["components"]["DCAE"]["componentState"]["stateName"] + failure = "MICROSERVICE_INSTALLATION_FAILED" + success = "MICROSERVICE_INSTALLED_SUCCESSFULLY" + while state not in (success, failure): + self.refresh_status() + self.validate_details() + state = self.details["components"]["DCAE"]["componentState"]["stateName"] + return state == success + + def undeploy_microservice_from_dcae(self) -> None: + """Stop the deploy operation.""" + url = f"{self.base_url()}/loop/undeploy/{self.name}" + self.send_message('PUT', + 'Undeploy microservice from DCAE', + url) + + def delete(self) -> None: + """Delete the loop instance.""" + self._logger.debug("Delete %s loop instance", self.name) + url = "{}/loop/delete/{}".format(self.base_url(), self.name) + self.send_message('PUT', + 'Delete loop instance', + url) diff --git a/src/onapsdk/clamp/schema_details.json b/src/onapsdk/clamp/schema_details.json new file mode 100644 index 0000000..3caea6c --- /dev/null +++ b/src/onapsdk/clamp/schema_details.json @@ -0,0 +1,138 @@ +{ + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "components": { + "type": "object", + "properties": { + "POLICY": { + "type": "object", + "properties": { + "componentState": { + "type": "object", + "properties": { + "stateName": { + "type": "string" + } + }, + "required": [ + "stateName" + ] + } + }, + "required": [ + "componentState" + ] + }, + "DCAE": { + "type": "object", + "properties": { + "componentState": { + "type": "object", + "properties": { + "stateName": { + "type": "string" + } + }, + "required": [ + "stateName" + ] + } + }, + "required": [ + "componentState" + ] + } + }, + "required": [ + "POLICY", + "DCAE" + ] + }, + "modelService": { + "type": "object", + "properties": { + "resourceDetails": { + "type": "object", + "properties": { + "VFModule": { + "type": "object", + "properties": { + "resourceID": { + "type": "object", + "properties": { + "vfModuleModelName": { + "type": "string" + }, + "vfModuleModelInvariantUUID": { + "type": "string" + }, + "vfModuleModelUUID": { + "type": "string" + }, + "vfModuleModelVersion": { + "type": "string" + }, + "vfModuleModelCustomizationUUID": { + "type": "string" + } + }, + "required": [ + "vfModuleModelName", + "vfModuleModelInvariantUUID", + "vfModuleModelUUID", + "vfModuleModelVersion", + "vfModuleModelCustomizationUUID" + ] + } + } + } + }, + "required": [ + "VFModule" + ] + } + }, + "required": [ + "resourceDetails" + ] + }, + "operationalPolicies": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + }, + "microServicePolicies": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + }, + "required": [ + "name", + "components", + "modelService", + "operationalPolicies", + "microServicePolicies" + ] + }
\ No newline at end of file diff --git a/src/onapsdk/clamp/templates/clamp_MinMax_config.json.j2 b/src/onapsdk/clamp/templates/clamp_MinMax_config.json.j2 new file mode 100644 index 0000000..2402ace --- /dev/null +++ b/src/onapsdk/clamp/templates/clamp_MinMax_config.json.j2 @@ -0,0 +1,94 @@ +[ + { + "name": "{{ name }}", + "jsonRepresentation": { + "title": "onap.policies.controlloop.guard.common.MinMax", + "type": "object", + "description": "Supports Min/Max number of entity for scaling operations. Although min and max fields are marked as not\nrequired, you need to have at least one or the other.\n", + "required": [ + "actor", + "operation", + "target" + ], + "properties": { + "id": { + "type": "string", + "description": "The Control Loop id this applies to." + }, + "actor": { + "type": "string", + "description": "Specifies the Actor the guard applies to." + }, + "operation": { + "type": "string", + "description": "Specified the operation that the actor is performing the guard applies to." + }, + "timeRange": { + "title": "tosca.datatypes.TimeInterval", + "type": "object", + "required": [ + "start_time", + "end_time" + ], + "properties": { + "start_time": { + "type": "string", + "format": "date-time" + }, + "end_time": { + "type": "string", + "format": "date-time" + } + } + }, + "min": { + "type": "integer", + "description": "The minimum instances of this entity" + }, + "max": { + "type": "integer", + "description": "The maximum instances of this entity" + }, + "target": { + "type": "string", + "description": "The target entity that has scaling restricted" + } + } + }, + "configurationsJson": { + "actor": "test", + "operation": "test", + "target": "test", + "timeRange": { + "start_time": "00:00:00", + "end_time": "01:00:00" + }, + "min": 1, + "max": 10 + }, + "policyModel": { + "policyModelType": "onap.policies.controlloop.guard.common.MinMax", + "version": "1.0.0", + "policyAcronym": "MinMax", + "policyPdpGroup": { + "supportedPdpGroups": [ + { + "defaultGroup": [ + "xacml" + ] + } + ] + }, + "createdDate": "2020-07-22T01:37:35.861060Z", + "updatedDate": "2020-07-22T01:37:51.719018Z", + "updatedBy": "Not found", + "createdBy": "Not found" + }, + "createdDate": "2020-07-22T09:01:14.168344Z", + "updatedDate": "2020-07-22T09:01:14.168344Z", + "updatedBy": "clamp@clamp.onap.org", + "createdBy": "clamp@clamp.onap.org", + "pdpGroup": "defaultGroup", + "pdpSubgroup": "xacml" + } +]
\ No newline at end of file diff --git a/src/onapsdk/clamp/templates/clamp_add_drools_policy.json.j2 b/src/onapsdk/clamp/templates/clamp_add_drools_policy.json.j2 new file mode 100644 index 0000000..40ca7cd --- /dev/null +++ b/src/onapsdk/clamp/templates/clamp_add_drools_policy.json.j2 @@ -0,0 +1,325 @@ +[ + { + "name": "{{ name }}", + "jsonRepresentation": { + "title": "onap.policies.controlloop.operational.common.Drools", + "type": "object", + "description": "Operational policies for Drools PDP", + "required": [ + "abatement", + "operations", + "trigger", + "timeout", + "id" + ], + "properties": { + "abatement": { + "type": "boolean", + "description": "Whether an abatement event message will be expected for the control loop from DCAE.", + "default": "false" + }, + "operations": { + "type": "array", + "description": "List of operations to be performed when Control Loop is triggered.", + "items": { + "title": "onap.datatype.controlloop.Operation", + "type": "object", + "description": "An operation supported by an actor", + "required": [ + "id", + "operation", + "retries", + "timeout" + ], + "properties": { + "failure_retries": { + "type": "string", + "description": "Points to the operation to invoke when the current operation has exceeded its max retries.", + "default": "final_failure_retries" + }, + "id": { + "type": "string", + "description": "Unique identifier for the operation" + }, + "failure_timeout": { + "type": "string", + "description": "Points to the operation to invoke when the time out for the operation occurs.", + "default": "final_failure_timeout" + }, + "failure": { + "type": "string", + "description": "Points to the operation to invoke on Actor operation failure.", + "default": "final_failure" + }, + "operation": { + "title": "onap.datatype.controlloop.Actor", + "type": "object", + "description": "An actor/operation/target definition", + "required": [ + "target", + "actor", + "operation" + ], + "properties": { + "payload": { + "type": "object", + "description": "Name/value pairs of payload information passed by Policy to the actor", + "anyOf": [ + { + "title": "User defined", + "properties": { + + } + } + ] + }, + "target": { + "title": "onap.datatype.controlloop.Target", + "type": "object", + "description": "Definition for a entity in A&AI to perform a control loop operation on", + "required": [ + "targetType" + ], + "properties": { + "entityIds": { + "type": "object", + "description": "Map of values that identify the resource. If none are provided, it is assumed that the\nentity that generated the ONSET event will be the target.\n", + "anyOf": [ + { + "title": "User defined", + "properties": { + + } + }, + { + "title": "VNF-ubuntu18agent_VF 0", + "properties": { + "resourceID": { + "title": "Resource ID", + "type": "string", + "default": "6daf6e05-fc26-4aa3-9f0b-d47cf3f37ece", + "readOnly": "True" + } + } + }, + { + "title": "VFMODULE-Ubuntu18agentVf..base_ubuntu18..module-0", + "properties": { + "resourceID": { + "title": "Resource ID", + "type": "string", + "default": "Ubuntu18agentVf..base_ubuntu18..module-0", + "readOnly": "True" + }, + "modelInvariantId": { + "title": "Model Invariant Id (ModelInvariantUUID)", + "type": "string", + "default": "2556faee-75dd-448f-8d2f-d4201a957e7c", + "readOnly": "True" + }, + "modelVersionId": { + "title": "Model Version Id (ModelUUID)", + "type": "string", + "default": "98df9741-530a-486c-b156-b2cb62e6fc6c", + "readOnly": "True" + }, + "modelName": { + "title": "Model Name", + "type": "string", + "default": "Ubuntu18agentVf..base_ubuntu18..module-0", + "readOnly": "True" + }, + "modelVersion": { + "title": "Model Version", + "type": "string", + "default": "1", + "readOnly": "True" + }, + "modelCustomizationId": { + "title": "Customization ID", + "type": "string", + "default": "ba567b66-e46b-4521-8fdd-54185cb21a7f", + "readOnly": "True" + } + } + } + ] + }, + "targetType": { + "type": "string", + "description": "Category for the target type", + "enum": [ + "VNF", + "VM", + "VFMODULE", + "PNF" + ] + } + } + }, + "actor": { + "type": "string", + "description": "The actor performing the operation.", + "enum": [ + "SDNR", + "SDNC", + "VFC", + "SO", + "APPC", + "CDS" + ], + "options": { + "enum_titles": [ + "SDNR", + "SDNC", + "VFC", + "SO", + "APPC" + ] + } + }, + "operation": { + "type": "string", + "description": "The operation the actor is performing.", + "enum": [ + "BandwidthOnDemand", + "VF Module Delete", + "Reroute", + "VF Module Create", + "ModifyConfig", + "Rebuild", + "Restart", + "Migrate", + "Health-Check" + ], + "options": { + "enum_titles": [ + "BandwidthOnDemand (SDNC operation)", + "VF Module Delete (SO operation)", + "Reroute (SDNC operation)", + "VF Module Create (SO operation)", + "ModifyConfig (APPC/VFC operation)", + "Rebuild (APPC operation)", + "Restart (APPC operation)", + "Migrate (APPC operation)", + "Health-Check (APPC operation)" + ] + } + } + } + }, + "failure_guard": { + "type": "string", + "description": "Points to the operation to invoke when the current operation is blocked due to guard policy enforcement.", + "default": "final_failure_guard" + }, + "retries": { + "type": "integer", + "description": "The number of retries the actor should attempt to perform the operation.", + "default": "0" + }, + "timeout": { + "type": "integer", + "description": "The amount of time for the actor to perform the operation." + }, + "failure_exception": { + "type": "string", + "description": "Points to the operation to invoke when the current operation causes an exception.", + "default": "final_failure_exception" + }, + "description": { + "type": "string", + "description": "A user-friendly description of the intent for the operation" + }, + "success": { + "type": "string", + "description": "Points to the operation to invoke on success. A value of \"final_success\" indicates and end to the operation.", + "default": "final_success" + } + } + }, + "format": "tabs-top" + }, + "trigger": { + "type": "string", + "description": "Initial operation to execute upon receiving an Onset event message for the Control Loop." + }, + "timeout": { + "type": "integer", + "description": "Overall timeout for executing all the operations. This timeout should equal or exceed the total\ntimeout for each operation listed.\n" + }, + "id": { + "type": "string", + "description": "The unique control loop id." + }, + "controllerName": { + "type": "string", + "description": "Drools controller properties" + } + } + }, + "configurationsJson": { + "abatement": false, + "operations": [ + { + "failure_retries": "final_failure_retries", + "id": "policy-1-vfmodule-create", + "failure_timeout": "final_failure_timeout", + "failure": "final_failure", + "operation": { + "payload": { + "requestParameters": "{\"usePreload\":false,\"userParams\":[]}", + "configurationParameters": "[{\"ip-addr\":\"$.vf-module-topology.vf-module-parameters.param[16].value\",\"oam-ip-addr\":\"$.vf-module-topology.vf-module-parameters.param[30].value\"}]" + }, + "target": { + "entityIds": { + "resourceID": "{{ resourceID }}", + "modelInvariantId": "{{ modelInvariantId }}", + "modelVersionId": "{{ modelVersionId }}", + "modelName": "{{ modelName }}", + "modelVersion": "{{ modelVersion }}", + "modelCustomizationId": "{{ modelCustomizationId }}" + }, + "targetType": "VFMODULE" + }, + "actor": "SO", + "operation": "VF Module Create" + }, + "failure_guard": "final_failure_guard", + "retries": 1, + "timeout": 300, + "failure_exception": "final_failure_exception", + "description": "test", + "success": "final_success" + } + ], + "trigger": "policy-1-vfmodule-create", + "timeout": 650, + "id": "{{ LOOP_name }}" + }, + "policyModel": { + "policyModelType": "onap.policies.controlloop.operational.common.Drools", + "version": "1.0.0", + "policyAcronym": "Drools", + "policyPdpGroup": { + "supportedPdpGroups": [ + { + "defaultGroup": [ + "drools" + ] + } + ] + }, + "createdDate": "2020-07-22T01:37:38.528901Z", + "updatedDate": "2020-07-22T01:37:51.752302Z", + "updatedBy": "Not found", + "createdBy": "Not found" + }, + "createdDate": "2020-07-22T07:50:00.076714Z", + "updatedDate": "2020-07-22T07:50:00.076714Z", + "updatedBy": "clamp@clamp.onap.org", + "createdBy": "clamp@clamp.onap.org", + "pdpGroup": "defaultGroup", + "pdpSubgroup": "drools" + } +]
\ No newline at end of file diff --git a/src/onapsdk/clamp/templates/clamp_add_frequency.json.j2 b/src/onapsdk/clamp/templates/clamp_add_frequency.json.j2 new file mode 100644 index 0000000..fabf9e6 --- /dev/null +++ b/src/onapsdk/clamp/templates/clamp_add_frequency.json.j2 @@ -0,0 +1,102 @@ +[ + { + "name": "{{ name }}", + "jsonRepresentation": { + "title": "onap.policies.controlloop.guard.common.FrequencyLimiter", + "type": "object", + "description": "Supports limiting the frequency of actions being taken by a Actor.", + "required": [ + "actor", + "operation", + "limit", + "timeWindow", + "timeUnits" + ], + "properties": { + "id": { + "type": "string", + "description": "The Control Loop id this applies to." + }, + "actor": { + "type": "string", + "description": "Specifies the Actor the guard applies to." + }, + "operation": { + "type": "string", + "description": "Specified the operation that the actor is performing the guard applies to." + }, + "timeRange": { + "title": "tosca.datatypes.TimeInterval", + "type": "object", + "required": [ + "start_time", + "end_time" + ], + "properties": { + "start_time": { + "type": "string", + "format": "date-time" + }, + "end_time": { + "type": "string", + "format": "date-time" + } + } + }, + "limit": { + "type": "integer", + "description": "The limit", + "exclusiveMinimum": "0" + }, + "timeWindow": { + "type": "integer", + "description": "The time window to count the actions against." + }, + "timeUnits": { + "type": "string", + "description": "The units of time the window is counting.", + "enum": [ + "second", + "minute", + "hour", + "day", + "week", + "month", + "year" + ] + } + } + }, + "configurationsJson": { + "actor": "SO", + "operation": "VF Module Create", + "limit": {{ limit }}, + "timeWindow": 10, + "timeUnits": "minute" + }, + "policyModel": { + "policyModelType": "onap.policies.controlloop.guard.common.FrequencyLimiter", + "version": "1.0.0", + "policyAcronym": "FrequencyLimiter", + "policyPdpGroup": { + "supportedPdpGroups": [ + { + "defaultGroup": [ + "xacml" + ] + } + ] + }, + "createdDate": "2020-07-22T01:37:35.106757Z", + "updatedDate": "2020-07-22T01:37:51.709386Z", + "updatedBy": "Not found", + "createdBy": "Not found" + }, + "createdDate": "2020-07-22T08:27:34.576868Z", + "updatedDate": "2020-07-22T08:27:34.576868Z", + "updatedBy": "clamp@clamp.onap.org", + "createdBy": "clamp@clamp.onap.org", + "pdpGroup": "defaultGroup", + "pdpSubgroup": "xacml" + } +]
\ No newline at end of file diff --git a/src/onapsdk/clamp/templates/clamp_add_tca_config.json.j2 b/src/onapsdk/clamp/templates/clamp_add_tca_config.json.j2 new file mode 100644 index 0000000..0919a6b --- /dev/null +++ b/src/onapsdk/clamp/templates/clamp_add_tca_config.json.j2 @@ -0,0 +1,30 @@ +{ + "name": "{{ name }}", + "configurationsJson": { + "tca.policy": { + "domain": "measurementsForVfScaling", + "metricsPerEventName": [ + { + "policyScope": "DCAE", + "thresholds": [ + { + "version": "1.0.2", + "severity": "MAJOR", + "thresholdValue": 200, + "closedLoopEventStatus": "ONSET", + "closedLoopControlName": "{{ LOOP_name }}", + "direction": "LESS_OR_EQUAL", + "fieldPath": "$.event.measurementsForVfScalingFields.vNicPerformanceArray[*].receivedTotalPacketsDelta" + } + ], + "eventName": "vLoadBalancer", + "policyVersion": "v0.0.1", + "controlLoopSchemaType": "VM", + "policyName": "DCAE.Config_tca-hi-lo" + } + ] + } + }, + "pdpGroup": "defaultGroup", + "pdpSubgroup": "xacml" +} diff --git a/src/onapsdk/configuration/__init__.py b/src/onapsdk/configuration/__init__.py new file mode 100644 index 0000000..027e7ef --- /dev/null +++ b/src/onapsdk/configuration/__init__.py @@ -0,0 +1,18 @@ +"""Configuration module.""" +# Copyright 2022 Orange, 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 .loader import SettingsLoader, SETTINGS_ENV + + +settings = SettingsLoader() # pylint: disable=invalid-name diff --git a/src/onapsdk/configuration/global_settings.py b/src/onapsdk/configuration/global_settings.py new file mode 100644 index 0000000..6f7e4d7 --- /dev/null +++ b/src/onapsdk/configuration/global_settings.py @@ -0,0 +1,71 @@ +"""Global settings module.""" # pylint: disable=bad-whitespace +# Copyright 2022 Orange, 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. + + +###################### +# # +# ONAP SERVICES URLS # +# # +###################### + +## API +AAI_URL = "https://aai.api.sparky.simpledemo.onap.org:30233" +AAI_API_VERSION = "v23" +AAI_AUTH = "Basic QUFJOkFBSQ==" +AAI_BULK_CHUNK = 30 +CDS_URL = "http://portal.api.simpledemo.onap.org:30449" +CDS_AUTH = ("ccsdkapps", "ccsdkapps") +CPS_URL = "http://portal.api.simpledemo.onap.org:8080" +CPS_AUTH = ("cpsuser", "cpsr0cks!") +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" +SDC_AUTH = "Basic YWFpOktwOGJKNFNYc3pNMFdYbGhhazNlSGxjc2UyZ0F3ODR2YW9HR21KdlV5MlU=" +SDNC_URL = "https://sdnc.api.simpledemo.onap.org:30267" +SDNC_AUTH = "Basic YWRtaW46S3A4Yko0U1hzek0wV1hsaGFrM2VIbGNzZTJnQXc4NHZhb0dHbUp2VXkyVQ==" +SO_URL = "http://so.api.simpledemo.onap.org:30277" +SO_API_VERSION = "v7" +SO_AUTH = "Basic SW5mcmFQb3J0YWxDbGllbnQ6cGFzc3dvcmQxJA==" +SO_CAT_DB_AUTH = "Basic YnBlbDpwYXNzd29yZDEk" +VID_URL = "https://vid.api.simpledemo.onap.org:30200" +VID_API_VERSION = "/vid" +CLAMP_URL = "https://clamp.api.simpledemo.onap.org:30258" +CLAMP_AUTH = "Basic ZGVtb0BwZW9wbGUub3NhYWYub3JnOmRlbW8xMjM0NTYh" +VES_URL = "http://ves.api.simpledemo.onap.org:30417" +DMAAP_URL = "http://dmaap.api.simpledemo.onap.org:3904" +NBI_URL = "https://nbi.api.simpledemo.onap.org:30274" +NBI_API_VERSION = "/nbi/api/v4" +DCAEMOD_URL = "" +HOLMES_URL = "https://aai.api.sparky.simpledemo.onap.org:30293" +POLICY_URL = "" + +## GUI +AAI_GUI_URL = "https://aai.api.sparky.simpledemo.onap.org:30220" +AAI_GUI_SERVICE = f"{AAI_GUI_URL}/services/aai/webapp/index.html#/browse" +CDS_GUI_SERVICE = f"{CDS_URL}/" +SO_MONITOR_GUI_SERVICE = f"{SO_URL}/" +SDC_GUI_SERVICE = f"{SDC_FE_URL}/sdc1/portal" +SDNC_DG_GUI_SERVICE = f"{SDNC_URL}/nifi/" +SDNC_ODL_GUI_SERVICE = f"{SDNC_URL}/odlux/index.html" + +DCAEMOD_GUI_SERVICE = f"{DCAEMOD_URL}/" +HOLMES_GUI_SERVICE = f"{HOLMES_URL}/iui/holmes/default.html" +POLICY_GUI_SERVICE = f"{POLICY_URL}/onap/login.html" +POLICY_CLAMP_GUI_SERVICE = f"{CLAMP_URL}/" + +# VID OBJECTS DEFAULT VALUES +PROJECT = "Onapsdk_project" +LOB = "Onapsdk_lob" +PLATFORM = "Onapsdk_platform" diff --git a/src/onapsdk/configuration/loader.py b/src/onapsdk/configuration/loader.py new file mode 100644 index 0000000..a9aae6f --- /dev/null +++ b/src/onapsdk/configuration/loader.py @@ -0,0 +1,115 @@ +"""Settings loader module.""" +# Copyright 2022 Orange, 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 importlib +import os +from typing import Any + +from onapsdk.exceptions import ModuleError, SettingsError + +from . import global_settings + + +SETTINGS_ENV = "ONAP_PYTHON_SDK_SETTINGS" + + +class SettingsLoader: + """Settings loader class. + + Load global settings and optionally load + custom settings by importing the module + stored in ONAP_PYTHON_SDK_SETTINGS environment + variable. + The module has to be uder PYTHONPATH. + """ + + def __init__(self) -> None: + """Load settings. + + Load global settings and optionally load custom one. + + Raises: + ModuleError: If ONAP_PYTHON_SDK_SETTINGS environment variable + is set and module can't be imported. + + """ + self._settings = {} + + # Load values from global_settings (only uppercase) + self.filter_and_set(global_settings) + + settings_env_value: str = os.environ.get(SETTINGS_ENV) + if settings_env_value: + # Load values from custom settings + try: + module = importlib.import_module(settings_env_value) + except ModuleNotFoundError: + msg = "Can't import custom settings. Is it under PYTHONPATH?" + raise ModuleError(msg) + self.filter_and_set(module) + + def __getattribute__(self, name: str) -> Any: + """Return stored attributes. + + If attribute name is uppercase return it from + _settings dictionary. + Look for attribute in __dict__ otherwise. + + Args: + name (str): Attribute's name + + Raises: + SettingsError: a setting is not found by the key. + + Returns: + Any: Attribute's value + + """ + if name.isupper(): + try: + return self._settings[name] + except KeyError as exc: + msg = f"Requested setting {exc.args[0]} does not exist." + raise SettingsError(msg) from exc + return super().__getattribute__(name) + + def __setattr__(self, name: str, value: Any) -> None: + """Save attribute. + + If attribute name is uppercase save the value + in _settings dictionary. + Use Object class __setattr__ implementation + otherwise. + + Args: + name (str): Attribute's name + value (Any): Attribute's value + + """ + if name.isupper(): + self._settings[name] = value + super().__setattr__(name, value) + + def filter_and_set(self, module: "module") -> None: + """Filter module attributes and save the uppercased. + + Iterate through module's attribures and save the value + of them which name is uppercase. + + Args: + module (module): Module to get attributes from + + """ + for key in filter(lambda x: x.isupper(), dir(module)): + self._settings[key] = getattr(module, key) diff --git a/src/onapsdk/constants.py b/src/onapsdk/constants.py new file mode 100644 index 0000000..191e7aa --- /dev/null +++ b/src/onapsdk/constants.py @@ -0,0 +1,61 @@ +"""Constant package.""" +# Copyright 2022 Orange, 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. + +## +# State Machines +# Vendor: DRAFT --> CERTIFIED +# VSP: DRAFT --> UPLOADED --> VALIDATED --> COMMITED --> CERTIFIED +## + +## +# States +## +DRAFT = "Draft" +CERTIFIED = "Certified" +COMMITED = "Commited" +UPLOADED = "Uploaded" +VALIDATED = "Validated" +APPROVED = "Approved" +UNDER_CERTIFICATION = "Certification in progress" +CHECKED_IN = "Checked In" +SUBMITTED = "Submitted" +DISTRIBUTED = "Distributed" +## +# Actions +## +CERTIFY = "Certify" +COMMIT = "Commit" +CREATE_PACKAGE = "Create_Package" +SUBMIT = "Submit" +SUBMIT_FOR_TESTING = "certificationRequest" +CHECKOUT = "checkout" +UNDOCHECKOUT = "UNDOCHECKOUT" +CHECKIN = "checkin" +APPROVE = "approve" +DISTRIBUTE = "PROD/activate" +TOSCA = "toscaModel" +DISTRIBUTION = "distribution" +START_CERTIFICATION = "startCertification" +NOT_CERTIFIED_CHECKOUT = "NOT_CERTIFIED_CHECKOUT" +NOT_CERTIFIED_CHECKIN = "NOT_CERTIFIED_CHECKIN" +READY_FOR_CERTIFICATION = "READY_FOR_CERTIFICATION" +CERTIFICATION_IN_PROGRESS = "CERTIFICATION_IN_PROGRESS" +DISTRIBUTION_APPROVED = "DISTRIBUTION_APPROVED" +DISTRIBUTION_NOT_APPROVED = "DISTRIBUTION_NOT_APPROVED" +SDC_DISTRIBUTED = "DISTRIBUTED" +## +# Distribution States +## +DOWNLOAD_OK = "DOWNLOAD_OK" diff --git a/src/onapsdk/cps/__init__.py b/src/onapsdk/cps/__init__.py new file mode 100644 index 0000000..63d5dd5 --- /dev/null +++ b/src/onapsdk/cps/__init__.py @@ -0,0 +1,18 @@ +"""ONAP SDK CPS package.""" +# Copyright 2022 Orange, 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 .anchor import Anchor # pylint: disable=unused-import +from .dataspace import Dataspace # pylint: disable=unused-import +from .schemaset import SchemaSet, SchemaSetModuleReference # pylint: disable=unused-import diff --git a/src/onapsdk/cps/anchor.py b/src/onapsdk/cps/anchor.py new file mode 100644 index 0000000..f02687d --- /dev/null +++ b/src/onapsdk/cps/anchor.py @@ -0,0 +1,193 @@ +"""ONAP SDK CPS anchor module.""" +# Copyright 2022 Orange, 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, TYPE_CHECKING + +from .cps_element import CpsElement + +if TYPE_CHECKING: + from .schemaset import SchemaSet # pylint: disable=cyclic-import + + +class Anchor(CpsElement): + """CPS anchor class.""" + + def __init__(self, name: str, schema_set: "SchemaSet") -> None: + """Initialise CPS anchor object. + + Args: + name (str): Anchor name + schema_set (SchemaSet): Schema set + + """ + super().__init__() + self.name: str = name + self.schema_set: "SchemaSet" = schema_set + + def __repr__(self) -> str: + """Human readable representation of the object. + + Returns: + str: Human readable string + + """ + return f"Anchor(name={self.name}, "\ + f"schema set={self.schema_set.name}, "\ + f"dataspace={self.schema_set.dataspace.name})" + + @property + def url(self) -> str: + """Anchor url. + + Returns: + str: Anchor url + + """ + return f"{self._url}/cps/api/v1/dataspaces/"\ + f"{self.schema_set.dataspace.name}/anchors/{self.name}" + + def delete(self) -> None: + """Delete anchor.""" + self.send_message( + "DELETE", + f"Delete {self.name} anchor", + self.url, + auth=self.auth + ) + + def create_node(self, node_data: str) -> None: + """Create anchor node. + + Fill CPS anchor with a data. + + Args: + node_data (str): Node data. Should be JSON formatted. + + """ + self.send_message( + "POST", + f"Create {self.name} anchor node", + f"{self.url}/nodes", + data=node_data, + auth=self.auth + ) + + def get_node(self, xpath: str, include_descendants: bool = False) -> Dict[Any, Any]: + """Get anchor node data. + + Using XPATH get anchor's node data. + + Args: + xpath (str): Anchor node xpath. + include_descendants (bool, optional): Determies if descendants should be included in + response. Defaults to False. + + Returns: + Dict[Any, Any]: Anchor node data. + + """ + return self.send_message_json( + "GET", + f"Get {self.name} anchor node with {xpath} xpath", + f"{self.url}/node?xpath={xpath}&include-descendants={include_descendants}", + auth=self.auth + ) + + def update_node(self, xpath: str, node_data: str) -> None: + """Update anchor node data. + + Using XPATH update anchor's node data. + + Args: + xpath (str): Anchor node xpath. + node_data (str): Node data. + + """ + self.send_message( + "PATCH", + f"Update {self.name} anchor node with {xpath} xpath", + f"{self.url}/nodes?xpath={xpath}", + data=node_data, + auth=self.auth + ) + + def replace_node(self, xpath: str, node_data: str) -> None: + """Replace anchor node data. + + Using XPATH replace anchor's node data. + + Args: + xpath (str): Anchor node xpath. + node_data (str): Node data. + + """ + self.send_message( + "PUT", + f"Replace {self.name} anchor node with {xpath} xpath", + f"{self.url}/nodes?xpath={xpath}", + data=node_data, + auth=self.auth + ) + + def add_list_node(self, xpath: str, node_data: str) -> None: + """Add an element to the list node of an anchor. + + Args: + xpath (str): Xpath to the list node. + node_data (str): Data to be added. + + """ + self.send_message( + "POST", + f"Add element to {self.name} anchor node with {xpath} xpath", + f"{self.url}/list-nodes?xpath={xpath}", + data=node_data, + auth=self.auth + ) + + def query_node(self, query: str, include_descendants: bool = False) -> Dict[Any, Any]: + """Query CPS anchor data. + + Args: + query (str): Query + include_descendants (bool, optional): Determies if descendants should be included in + response. Defaults to False. + + Returns: + Dict[Any, Any]: Query return values. + + """ + return self.send_message_json( + "GET", + f"Get {self.name} anchor node with {query} query", + f"{self.url}/nodes/query?cps-path={query}&include-descendants={include_descendants}", + auth=self.auth + ) + + def delete_nodes(self, xpath: str) -> None: + """Delete nodes. + + Use XPATH to delete Anchor nodes. + + Args: + xpath (str): Nodes to delete + + """ + self.send_message( + "DELETE", + f"Delete {self.name} anchor nodes with {xpath} xpath", + f"{self.url}/nodes?xpath={xpath}", + auth=self.auth + ) diff --git a/src/onapsdk/cps/cps_element.py b/src/onapsdk/cps/cps_element.py new file mode 100644 index 0000000..27f1faa --- /dev/null +++ b/src/onapsdk/cps/cps_element.py @@ -0,0 +1,24 @@ +"""ONAP SDK CPS element module.""" +# Copyright 2022 Orange, 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 onapsdk.configuration import settings +from onapsdk.onap_service import OnapService + + +class CpsElement(OnapService): + """Mother Class of all CPS elements.""" + + _url: str = settings.CPS_URL + auth: tuple = settings.CPS_AUTH diff --git a/src/onapsdk/cps/dataspace.py b/src/onapsdk/cps/dataspace.py new file mode 100644 index 0000000..e6340d7 --- /dev/null +++ b/src/onapsdk/cps/dataspace.py @@ -0,0 +1,193 @@ +"""ONAP SDK CPS dataspace module.""" +# Copyright 2022 Orange, 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, Iterable + +from .anchor import Anchor +from .cps_element import CpsElement +from .schemaset import SchemaSet, SchemaSetModuleReference + + +class Dataspace(CpsElement): + """CPS dataspace class.""" + + def __init__(self, name: str) -> None: + """Initialize dataspace object. + + Args: + name (str): Dataspace name + + """ + super().__init__() + self.name: str = name + + def __repr__(self) -> str: + """Human readable representation of the object. + + Returns: + str: Human readable string + + """ + return f"Dataspace(name={self.name})" + + @property + def url(self) -> str: + """Dataspace url. + + Returns: + str: Dataspace url + + """ + return f"{self._url}/cps/api/v1/dataspaces/{self.name}" + + @classmethod + def create(cls, dataspace_name: str) -> "Dataspace": + """Create dataspace with given name. + + Args: + dataspace_name (str): Dataspace name + + Returns: + Dataspace: Newly created dataspace + + """ + cls.send_message( + "POST", + f"Create {dataspace_name} dataspace", + f"{cls._url}/cps/api/v1/dataspaces?dataspace-name={dataspace_name}", + auth=cls.auth + ) + return Dataspace(dataspace_name) + + def create_anchor(self, schema_set: SchemaSet, anchor_name: str) -> Anchor: + """Create anchor. + + Args: + schema_set (SchemaSet): Schema set object which is going to be used to create anchor. + anchor_name (str): Anchor name + + Returns: + Anchor: Created anchor + + """ + self.send_message( + "POST", + "Get all CPS dataspace schemasets", + f"{self.url}/anchors/?schema-set-name={schema_set.name}&anchor-name={anchor_name}", + auth=self.auth + ) + return Anchor(name=anchor_name, schema_set=schema_set) + + def get_anchors(self) -> Iterable[Anchor]: + """Get all dataspace's anchors. + + Iterable of related with dataspace anchors. + + Yields: + Iterator[Anchor]: Anchor object + + """ + for anchor_data in self.send_message_json(\ + "GET",\ + "Get all CPS dataspace anchors",\ + f"{self.url}/anchors",\ + auth=self.auth\ + ): + yield Anchor(name=anchor_data["name"], + schema_set=SchemaSet(name=anchor_data["schemaSetName"], + dataspace=self)) + + def get_anchor(self, anchor_name: str) -> Anchor: + """Get dataspace anchor by name. + + To get anchor there is no need to use `SchemaSet` object, but to create anchor it it. + + Args: + anchor_name (str): Anchor name. + + Returns: + Anchor: Anchor object + + """ + anchor_data: Dict[str, Any] = self.send_message_json( + "GET", + f"Get {anchor_name} anchor", + f"{self.url}/anchors/{anchor_name}", + auth=self.auth + ) + return Anchor(name=anchor_data["name"], + schema_set=SchemaSet(name=anchor_data["schemaSetName"], + dataspace=self)) + + def get_schema_set(self, schema_set_name: str) -> SchemaSet: + """Get schema set by name. + + Args: + schema_set_name (str): Schema set name + + Returns: + SchemaSet: Schema set object + + """ + schema_set_data: Dict[str, Any] = self.send_message_json( + "GET", + "Get all CPS dataspace schemasets", + f"{self._url}/cps/api/v1/dataspaces/{self.name}/schema-sets/{schema_set_name}", + auth=self.auth + ) + return SchemaSet( + name=schema_set_data["name"], + dataspace=self, + module_references=[ + SchemaSetModuleReference( + name=module_reference_data["name"], + namespace=module_reference_data["namespace"], + revision=module_reference_data["revision"] + ) for module_reference_data in schema_set_data["moduleReferences"] + ] + ) + + def create_schema_set(self, schema_set_name: str, schema_set: bytes) -> SchemaSet: + """Create schema set. + + Create CPS schema set in dataspace + + Args: + schema_set_name (str): Schema set name + schema_set (bytes): Schema set YANG + + Returns: + SchemaSet: Created schema set object + + """ + self.send_message( + "POST", + "Create schema set", + f"{self._url}/cps/api/v1/dataspaces/{self.name}/schema-sets/", + files={"file": schema_set}, + data={"schema-set-name": schema_set_name}, + headers={}, # Leave headers empty to fill it correctly by `requests` library + auth=self.auth + ) + return self.get_schema_set(schema_set_name) + + def delete(self) -> None: + """Delete dataspace.""" + self.send_message( + "DELETE", + f"Delete {self.name} dataspace", + f"{self._url}/cps/api/v1/dataspaces/{self.name}", + auth=self.auth + ) diff --git a/src/onapsdk/cps/schemaset.py b/src/onapsdk/cps/schemaset.py new file mode 100644 index 0000000..650d12d --- /dev/null +++ b/src/onapsdk/cps/schemaset.py @@ -0,0 +1,73 @@ +"""ONAP SDK CPS schemaset module.""" +# Copyright 2022 Orange, 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 dataclasses import dataclass +from typing import List, Optional, TYPE_CHECKING + +from .cps_element import CpsElement + +if TYPE_CHECKING: + from .dataspace import Dataspace # pylint: disable=cyclic-import + + +@dataclass +class SchemaSetModuleReference: + """Schema set module reference dataclass. + + Stores all information about module reference. + """ + + name: str + namespace: str + revision: str + + +class SchemaSet(CpsElement): + """Schema set class.""" + + def __init__(self, + name: str, + dataspace: "Dataspace", + module_references: Optional[List[SchemaSetModuleReference]] = None) -> None: + """Initialize schema set class object. + + Args: + name (str): Schema set name + dataspace (Dataspace): Dataspace on which schema set was created. + module_references (Optional[List[SchemaSetModuleReference]], optional): + List of module references. Defaults to None. + """ + super().__init__() + self.name: str = name + self.dataspace: "Dataspace" = dataspace + self.module_refences: List[SchemaSetModuleReference] = module_references \ + if module_references else [] + + def __repr__(self) -> str: + """Human readable representation of the object. + + Returns: + str: Human readable string + + """ + return f"SchemaSet(name={self.name}, dataspace={self.dataspace.name})" + + def delete(self) -> None: + """Delete schema set.""" + self.send_message( + "DELETE", + f"Delete {self.name} schema set", + f"{self._url}/cps/api/v1/dataspaces/{self.dataspace.name}/schema-sets/{self.name}" + ) diff --git a/src/onapsdk/dmaap/__init__.py b/src/onapsdk/dmaap/__init__.py new file mode 100644 index 0000000..e0e448d --- /dev/null +++ b/src/onapsdk/dmaap/__init__.py @@ -0,0 +1,14 @@ +"""ONAP SDK Dmaap package.""" +# Copyright 2022 Orange, 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/src/onapsdk/dmaap/dmaap.py b/src/onapsdk/dmaap/dmaap.py new file mode 100644 index 0000000..27c72c5 --- /dev/null +++ b/src/onapsdk/dmaap/dmaap.py @@ -0,0 +1,87 @@ +"""Base Dmaap event store.""" +# Copyright 2022 Orange, Deutsche Telekom AG, Nokia +# +# 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 Dict + +from onapsdk.dmaap.dmaap_service import DmaapService + +ACTION = "Get events from Dmaap" +GET_HTTP_METHOD = "GET" + + +class Dmaap(DmaapService): + """Dmaap library provides functions for getting events from Dmaap.""" + + dmaap_url = DmaapService._url + get_all_events_url = f"{dmaap_url}/events" + get_all_topics_url = f"{dmaap_url}/topics" + get_events_from_topic_url = "{}/events/{}/CG1/C1" + + @classmethod + def get_all_events(cls, + basic_auth: Dict[str, str]) -> dict: + """ + Get all events stored in Dmaap. + + Args: + basic_auth: (Dict[str, str]) for example:{ 'username': 'bob', 'password': 'secret' } + Returns: + (dict) Events from Dmaap + + """ + return Dmaap.__get_events(cls.get_all_events_url, basic_auth) + + @classmethod + def get_events_for_topic(cls, + topic: str, + basic_auth: Dict[str, str]) -> dict: + """ + Get all events stored specific topic in Dmaap. + + Args: + topic: (str) topic of events stored in Dmaap + basic_auth: (Dict[str, str]) for example:{ 'username': 'bob', 'password': 'secret' } + + Returns: + (dict) Events from Dmaap + + """ + url = cls.get_events_from_topic_url.format(cls.dmaap_url, topic) + return Dmaap.__get_events(url, basic_auth) + + @classmethod + def get_all_topics(cls, + basic_auth: Dict[str, str]) -> dict: + """ + Get all topics stored in Dmaap. + + Args: + basic_auth: (Dict[str, str]) for example:{ 'username': 'bob', 'password': 'secret' } + + Returns: + (dict) Topics from Dmaap + + """ + return Dmaap.__get_events(cls.get_all_topics_url, basic_auth)['topics'] + + @classmethod + def __get_events(cls, + url: str, + basic_auth: Dict[str, str]) -> dict: + return cls.send_message_json( + GET_HTTP_METHOD, + ACTION, + url, + basic_auth=basic_auth + ) diff --git a/src/onapsdk/dmaap/dmaap_service.py b/src/onapsdk/dmaap/dmaap_service.py new file mode 100644 index 0000000..5fe393b --- /dev/null +++ b/src/onapsdk/dmaap/dmaap_service.py @@ -0,0 +1,26 @@ +"""Base VES module.""" +# Copyright 2022 Orange, Deutsche Telekom AG, Nokia +# +# 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 onapsdk.configuration import settings +from onapsdk.onap_service import OnapService + + +class DmaapService(OnapService): + """Base DMAAP class. + + Stores url to DMAAP API (edit if you want to use other). + """ + + _url: str = settings.DMAAP_URL diff --git a/src/onapsdk/exceptions.py b/src/onapsdk/exceptions.py new file mode 100644 index 0000000..76af60e --- /dev/null +++ b/src/onapsdk/exceptions.py @@ -0,0 +1,109 @@ +"""ONAP Exception module.""" +# Copyright 2022 Orange, 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 Optional + + +class SDKException(Exception): + """Generic exception for ONAP SDK.""" + + +class RequestError(SDKException): + """Request error occured.""" + + +class ConnectionFailed(RequestError): + """Unable to connect.""" + + +class APIError(RequestError): + """API error occured.""" + + def __init__(self, + message: Optional[str] = None, + response_status_code: Optional[int] = None) -> None: + """Init api error exception. + + Save message and optional response status code. + + Args: + message (Optional[str]): Response error message. Defaults to None. + response_status_code (Optional[int], optional): Response status code. Defaults to None. + + """ + if message: + super().__init__(message) + else: + super().__init__() + self._response_status_code: int = response_status_code if response_status_code else 0 + + @property + def response_status_code(self) -> int: + """Response status code property. + + Returns: + int: Response status code. If not set, returns 0 + + """ + return self._response_status_code + + @response_status_code.setter + def response_status_code(self, status_code: int) -> None: + """Response status code property setter. + + Args: + status_code (int): Response status code + + """ + self._response_status_code = status_code + + +class InvalidResponse(RequestError): + """Unable to decode response.""" + + +class ResourceNotFound(APIError): + """Requested resource does not exist.""" + + +class RelationshipNotFound(ResourceNotFound): + """Required relationship is missing.""" + + +class StatusError(SDKException): + """Invalid status.""" + + +class ParameterError(SDKException): + """Parameter does not satisfy requirements.""" + +class ModuleError(SDKException): + """Unable to import module.""" + + +class ValidationError(SDKException): + """Data validation failed.""" + + +class FileError(ValidationError): + """Reading in a file failed.""" + + +class SettingsError(SDKException): + """Some settings are wrong.""" + + +class NoGuiError(SDKException): + """No GUI available for this component.""" diff --git a/src/onapsdk/msb/__init__.py b/src/onapsdk/msb/__init__.py new file mode 100644 index 0000000..6b23278 --- /dev/null +++ b/src/onapsdk/msb/__init__.py @@ -0,0 +1,17 @@ +"""Microsevice bus package.""" +# Copyright 2022 Orange, 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 .msb_service import MSB +from .esr import ESR +from .multicloud import Multicloud diff --git a/src/onapsdk/msb/esr.py b/src/onapsdk/msb/esr.py new file mode 100644 index 0000000..b29cfa4 --- /dev/null +++ b/src/onapsdk/msb/esr.py @@ -0,0 +1,85 @@ +"""ESR module.""" +# Copyright 2022 Orange, 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 onapsdk.utils.jinja import jinja_env +from .msb_service import MSB + + +class ESR(MSB): + """External system EST module.""" + + base_url = f"{MSB.base_url}/api/aai-esr-server/v1/vims" + + @classmethod + def register_vim(cls, # pylint: disable=too-many-arguments + cloud_owner: str, + cloud_region_id: str, + cloud_type: str, + cloud_region_version: str, + auth_info_cloud_domain: str, + auth_info_username: str, + auth_info_password: str, + auth_info_url: str, + owner_defined_type: str = None, + cloud_zone: str = None, + physical_location_id: str = None, + cloud_extra_info: str = None, + auth_info_ssl_cacert: str = None, + auth_info_ssl_insecure: bool = None) -> None: + """Register VIM. + + Args: + cloud_owner (str): cloud owner name, can be customized, e.g. att-aic + cloud_region_id (str): cloud region info based on deployment, e.g. RegionOne + cloud_type (str): type of the cloud, decides which multicloud plugin to use, + openstack or vio + cloud_region_version (str): cloud version, ocata, mitaka or other + auth_info_cloud_domain (str): domain info for keystone v3 + auth_info_username (str): user name + auth_info_password (str): password + auth_info_url (str): authentication url of the cloud, e.g. keystone url + owner_defined_type (str, optional): cloud-owner defined type indicator (e.g., dcp, lcp). + Defaults to None. + cloud_zone (str, optional): zone where the cloud is homed.. Defaults to None. + physical_location_id (str, optional): complex physical location id for + cloud-region instance. Defaults to None. + cloud_extra_info (str, optional): extra info for Cloud. Defaults to None. + auth_info_ssl_cacert (str, optional): ca file content if enabled ssl on auth-url. + Defaults to None. + auth_info_ssl_insecure (bool, optional): whether to verify VIM's certificate. + Defaults to None. + """ + cls.send_message( + "POST", + "Register VIM instance to ONAP", + cls.base_url, + data=jinja_env() + .get_template("msb_esr_vim_registration.json.j2") + .render( + cloud_owner=cloud_owner, + cloud_region_id=cloud_region_id, + cloud_type=cloud_type, + cloud_region_version=cloud_region_version, + auth_info_cloud_domain=auth_info_cloud_domain, + auth_info_username=auth_info_username, + auth_info_password=auth_info_password, + auth_info_url=auth_info_url, + owner_defined_type=owner_defined_type, + cloud_zone=cloud_zone, + physical_location_id=physical_location_id, + cloud_extra_info=cloud_extra_info, + auth_info_ssl_cacert=auth_info_ssl_cacert, + auth_info_ssl_insecure=auth_info_ssl_insecure, + ), + ) diff --git a/src/onapsdk/msb/k8s/__init__.py b/src/onapsdk/msb/k8s/__init__.py new file mode 100644 index 0000000..655502d --- /dev/null +++ b/src/onapsdk/msb/k8s/__init__.py @@ -0,0 +1,17 @@ +"""K8s package.""" +# Copyright 2022 Orange, 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 .definition import Definition, Profile, ConfigurationTemplate +from .connectivity_info import ConnectivityInfo +from .instance import InstantiationParameter, InstantiationRequest, Instance diff --git a/src/onapsdk/msb/k8s/connectivity_info.py b/src/onapsdk/msb/k8s/connectivity_info.py new file mode 100644 index 0000000..71a43c1 --- /dev/null +++ b/src/onapsdk/msb/k8s/connectivity_info.py @@ -0,0 +1,105 @@ +"""Connectivity-Info module.""" +# Copyright 2022 Orange, 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 onapsdk.utils.jinja import jinja_env +from ..msb_service import MSB + + +class ConnectivityInfo(MSB): + """Connectivity-Info class.""" + + api_version = "/api/multicloud-k8s/v1/v1" + url = f"{MSB.base_url}{api_version}/connectivity-info" + + def __init__(self, cloud_region_id: str, + cloud_owner: str, + other_connectivity_list: dict, + kubeconfig: str) -> None: + """Connectivity-info object initialization. + + Args: + cloud_region_id (str): Cloud region ID + cloud_owner (str): Cloud owner name + other_connectivity_list (dict): Optional other connectivity list + kubeconfig (str): kubernetes cluster kubeconfig + """ + super().__init__() + self.cloud_region_id: str = cloud_region_id + self.cloud_owner: str = cloud_owner + self.other_connectivity_list: dict = other_connectivity_list + self.kubeconfig: str = kubeconfig + + @classmethod + def get_connectivity_info_by_region_id(cls, cloud_region_id: str) -> "ConnectivityInfo": + """Get connectivity-info by its name (cloud region id). + + Args: + cloud_region_id (str): Cloud region ID + + Returns: + ConnectivityInfo: Connectivity-Info object + + """ + url: str = f"{cls.url}/{cloud_region_id}" + connectivity_info: dict = cls.send_message_json( + "GET", + "Get Connectivity Info", + url + ) + return cls( + connectivity_info["cloud-region"], + connectivity_info["cloud-owner"], + connectivity_info.get("other-connectivity-list"), + connectivity_info["kubeconfig"] + ) + + def delete(self) -> None: + """Delete connectivity info.""" + url: str = f"{self.url}/{self.cloud_region_id}" + self.send_message( + "DELETE", + "Delete Connectivity Info", + url + ) + + @classmethod + def create(cls, + cloud_region_id: str, + cloud_owner: str, + kubeconfig: bytes = None) -> "ConnectivityInfo": + """Create Connectivity Info. + + Args: + cloud_region_id (str): Cloud region ID + cloud_owner (str): Cloud owner name + kubeconfig (bytes): kubernetes cluster kubeconfig file + + Returns: + ConnectivityInfo: Created object + + """ + json_file = jinja_env().get_template("multicloud_k8s_add_connectivity_info.json.j2").render( + cloud_region_id=cloud_region_id, + cloud_owner=cloud_owner + ) + url: str = f"{cls.url}" + cls.send_message( + "POST", + "Create Connectivity Info", + url, + files={"file": kubeconfig, + "metadata": (None, json_file)}, + headers={} + ) + return cls.get_connectivity_info_by_region_id(cloud_region_id) diff --git a/src/onapsdk/msb/k8s/definition.py b/src/onapsdk/msb/k8s/definition.py new file mode 100644 index 0000000..6c0def2 --- /dev/null +++ b/src/onapsdk/msb/k8s/definition.py @@ -0,0 +1,424 @@ +"""Definition module.""" +# Copyright 2022 Orange, 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 Iterator +from dataclasses import dataclass + +from onapsdk.utils.jinja import jinja_env +from ..msb_service import MSB + + +# pylint: disable=too-many-arguments, too-few-public-methods +class DefinitionBase(MSB): + """DefinitionBase class.""" + + base_url = f"{MSB.base_url}/api/multicloud-k8s/v1/v1/rb/definition" + + def __init__(self, rb_name: str, + rb_version: str) -> None: + """Definition-Base object initialization. + + Args: + rb_name (str): Definition name + rb_version (str): Definition version + """ + super().__init__() + self.rb_name: str = rb_name + self.rb_version: str = rb_version + + @property + def url(self) -> str: + """URL address for Definition Based calls. + + Returns: + str: URL to RB Definition + + """ + return f"{self.base_url}/{self.rb_name}/{self.rb_version}" + + def delete(self) -> None: + """Delete Definition Based object.""" + self.send_message( + "DELETE", + f"Delete {self.__class__.__name__}", + self.url + ) + + def upload_artifact(self, package: bytes = None): + """Upload artifact. + + Args: + package (bytes): Artifact to be uploaded to multicloud-k8s plugin + + """ + url: str = f"{self.url}/content" + self.send_message( + "POST", + "Upload Artifact content", + url, + data=package, + headers={} + ) + + +class Definition(DefinitionBase): + """Definition class.""" + + def __init__(self, rb_name: str, + rb_version: str, + chart_name: str, + description: str, + labels: dict) -> None: + """Definition object initialization. + + Args: + rb_name (str): Definition name + rb_version (str): Definition version + chart_name (str): Chart name, optional field, will be detected if it is not provided + description (str): Definition description + labels (str): Labels + """ + super().__init__(rb_name, rb_version) + self.rb_name: str = rb_name + self.rb_version: str = rb_version + self.chart_name: str = chart_name + self.description: str = description + self.labels: dict = labels + + @classmethod + def get_all(cls): + """Get all definitions. + + Yields: + Definition: Definition object + + """ + for definition in cls.send_message_json("GET", + "Get definitions", + cls.base_url): + yield cls( + definition["rb-name"], + definition["rb-version"], + definition.get("chart-name"), + definition.get("description"), + definition.get("labels") + ) + + @classmethod + def get_definition_by_name_version(cls, rb_name: str, rb_version: str) -> "Definition": + """Get definition by it's name and version. + + Args: + rb_name (str): definition name + rb_version (str): definition version + + Returns: + Definition: Definition object + + """ + url: str = f"{cls.base_url}/{rb_name}/{rb_version}" + definition: dict = cls.send_message_json( + "GET", + "Get definition", + url + ) + return cls( + definition["rb-name"], + definition["rb-version"], + definition.get("chart-name"), + definition.get("description"), + definition.get("labels") + ) + + @classmethod + def create(cls, rb_name: str, + rb_version: str, + chart_name: str = "", + description: str = "", + labels=None) -> "Definition": + """Create Definition. + + Args: + rb_name (str): Definition name + rb_version (str): Definition version + chart_name (str): Chart name, optional field, will be detected if it is not provided + description (str): Definition description + labels (str): Labels + + Returns: + Definition: Created object + + """ + if labels is None: + labels = {} + url: str = f"{cls.base_url}" + cls.send_message( + "POST", + "Create definition", + url, + data=jinja_env().get_template("multicloud_k8s_add_definition.json.j2").render( + rb_name=rb_name, + rb_version=rb_version, + chart_name=chart_name, + description=description, + labels=labels + ) + ) + return cls.get_definition_by_name_version(rb_name, rb_version) + + def create_profile(self, profile_name: str, + namespace: str, + kubernetes_version: str, + release_name=None) -> "Profile": + """Create Profile for Definition. + + Args: + profile_name (str): Name of profile + namespace (str): Namespace that service is created in + kubernetes_version (str): Required Kubernetes version + release_name (str): Release name + + Returns: + Profile: Created object + + """ + url: str = f"{self.url}/profile" + if release_name is None: + release_name = profile_name + self.send_message( + "POST", + "Create profile for definition", + url, + data=jinja_env().get_template("multicloud_k8s_create_profile_" + "for_definition.json.j2").render( + rb_name=self.rb_name, + rb_version=self.rb_version, + profile_name=profile_name, + release_name=release_name, + namespace=namespace, + kubernetes_version=kubernetes_version + ) + ) + return self.get_profile_by_name(profile_name) + + def get_all_profiles(self) -> Iterator["Profile"]: + """Get all profiles. + + Yields: + Profile: Profile object + + """ + url: str = f"{self.url}/profile" + + for profile in self.send_message_json("GET", + "Get profiles", + url): + yield Profile( + profile["rb-name"], + profile["rb-version"], + profile["profile-name"], + profile["namespace"], + profile.get("kubernetes-version"), + profile.get("labels"), + profile.get("release-name") + ) + + def get_profile_by_name(self, profile_name: str) -> "Profile": + """Get profile by it's name. + + Args: + profile_name (str): profile name + + Returns: + Profile: Profile object + + """ + url: str = f"{self.url}/profile/{profile_name}" + + profile: dict = self.send_message_json( + "GET", + "Get profile", + url + ) + return Profile( + profile["rb-name"], + profile["rb-version"], + profile["profile-name"], + profile["namespace"], + profile.get("kubernetes-version"), + profile.get("labels"), + profile.get("release-name") + ) + + def get_all_configuration_templates(self): + """Get all configuration templates. + + Yields: + ConfigurationTemplate: ConfigurationTemplate object + + """ + url: str = f"{self.url}/config-template" + + for template in self.send_message_json("GET", + "Get configuration templates", + url): + yield ConfigurationTemplate( + self.rb_name, + self.rb_version, + template["template-name"], + template.get("description") + ) + + def create_configuration_template(self, template_name: str, + description="") -> "ConfigurationTemplate": + """Create configuration template. + + Args: + template_name (str): Name of the template + description (str): Description + + Returns: + ConfigurationTemplate: Created object + + """ + url: str = f"{self.url}/config-template" + + self.send_message( + "POST", + "Create configuration template", + url, + data=jinja_env().get_template("multicloud_k8s_create_configuration_" + "template.json.j2").render( + template_name=template_name, + description=description + ) + ) + + return self.get_configuration_template_by_name(template_name) + + def get_configuration_template_by_name(self, template_name: str) -> "ConfigurationTemplate": + """Get configuration template. + + Args: + template_name (str): Name of the template + + Returns: + ConfigurationTemplate: object + + """ + url: str = f"{self.url}/config-template/{template_name}" + + template: dict = self.send_message_json( + "GET", + "Get Configuration template", + url + ) + return ConfigurationTemplate( + self.rb_name, + self.rb_version, + template["template-name"], + template.get("description") + ) + + +class ProfileBase(DefinitionBase): + """ProfileBase class.""" + + def __init__(self, rb_name: str, + rb_version: str, + profile_name: str) -> None: + """Profile-Base object initialization. + + Args: + rb_name (str): Definition name + rb_version (str): Definition version + profile_name (str): Name of profile + """ + super().__init__(rb_name, rb_version) + self.rb_name: str = rb_name + self.rb_version: str = rb_version + self.profile_name: str = profile_name + + @property + def url(self) -> str: + """URL address for Profile calls. + + Returns: + str: URL to RB Profile + + """ + return f"{super().url}/profile/{self.profile_name}" + + +@dataclass +class Profile(ProfileBase): + """Profile class.""" + + def __init__(self, rb_name: str, + rb_version: str, + profile_name: str, + namespace: str, + kubernetes_version: str, + labels=None, + release_name=None) -> None: + """Profile object initialization. + + Args: + rb_name (str): Definition name + rb_version (str): Definition version + profile_name (str): Name of profile + release_name (str): Release name, if release_name is not provided, + namespace (str): Namespace that service is created in + kubernetes_version (str): Required Kubernetes version + labels (dict): Labels + """ + super().__init__(rb_name, rb_version, profile_name) + if release_name is None: + release_name = profile_name + self.release_name: str = release_name + self.namespace: str = namespace + self.kubernetes_version: str = kubernetes_version + self.labels: dict = labels + if self.labels is None: + self.labels = dict() + + +class ConfigurationTemplate(DefinitionBase): + """ConfigurationTemplate class.""" + + @property + def url(self) -> str: + """URL address for ConfigurationTemplate calls. + + Returns: + str: URL to Configuration template in Multicloud-k8s API. + + """ + return f"{super().url}/config-template/{self.template_name}" + + def __init__(self, rb_name: str, + rb_version: str, + template_name: str, + description="") -> None: + """Configuration-Template object initialization. + + Args: + rb_name (str): Definition name + rb_version (str): Definition version + template_name (str): Configuration template name + description (str): Namespace that service is created in + """ + super().__init__(rb_name, rb_version) + self.template_name: str = template_name + self.description: str = description diff --git a/src/onapsdk/msb/k8s/instance.py b/src/onapsdk/msb/k8s/instance.py new file mode 100644 index 0000000..196b9d2 --- /dev/null +++ b/src/onapsdk/msb/k8s/instance.py @@ -0,0 +1,190 @@ +"""Instantiation module.""" +# Copyright 2022 Orange, 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 Iterator +from dataclasses import dataclass + +from onapsdk.msb import MSB +from onapsdk.utils.jinja import jinja_env + + +# pylint: disable=too-many-arguments +@dataclass +class InstantiationRequest: + """Instantiation Request class.""" + + def __init__(self, request: dict) -> None: + """Request object initialization. + + Args: + cloud_region_id (str): Cloud region ID + profile_name (str): Name of profile + rb_name (str): Definition name + rb_version (str): Definition version + override_values (dict): Optional parameters + labels (dict): Optional labels + """ + super().__init__() + self.cloud_region_id: str = request["cloud-region"] + self.profile_name: str = request["profile-name"] + self.rb_name: str = request["rb-name"] + self.rb_version: str = request["rb-version"] + self.override_values: dict = request["override-values"] + self.labels: dict = request["labels"] + + +@dataclass +class InstantiationParameter: + """Class to store instantiation parameters used to pass override_values and labels. + + Contains two values: name of parameter and it's value + """ + + name: str + value: str + + +class Instance(MSB): + """Instance class.""" + + base_url = f"{MSB.base_url}/api/multicloud-k8s/v1/v1/instance" + + def __init__(self, instance_id: str, + namespace: str, + request: InstantiationRequest, + resources: dict = None, + override_values: dict = None) -> None: + """Instance object initialization. + + Args: + instance_id (str): instance ID + namespace (str): namespace that instance is created in + request (InstantiationRequest): datails of the instantiation request + resources (dict): Created resources + override_values (dict): Optional values + """ + super().__init__() + self.instance_id: str = instance_id + self.namespace: str = namespace + self.request: InstantiationRequest = request + self.resources: dict = resources + self.override_values: dict = override_values + + @property + def url(self) -> str: + """URL address. + + Returns: + str: URL to Instance + + """ + return f"{self.base_url}/{self.instance_id}" + + @classmethod + def get_all(cls) -> Iterator["Instance"]: + """Get all instantiated Kubernetes resources. + + Yields: + Instantiation: Instantiation object + + """ + for resource in cls.send_message_json("GET", + "Get Kubernetes resources", + cls.base_url): + yield cls( + instance_id=resource["id"], + namespace=resource["namespace"], + request=InstantiationRequest(resource["request"]) + ) + + @classmethod + def get_by_id(cls, instance_id: str) -> "Instance": + """Get Kubernetes resource by id. + + Args: + instance_id (str): instance ID + + Returns: + Instantiation: Instantiation object + + """ + url: str = f"{cls.base_url}/{instance_id}" + resource: dict = cls.send_message_json( + "GET", + "Get Kubernetes resource by id", + url + ) + return cls( + instance_id=resource["id"], + namespace=resource["namespace"], + request=InstantiationRequest(resource["request"]), + resources=resource["resources"], + override_values=resource.get("override-values") + ) + + @classmethod + def create(cls, + cloud_region_id: str, + profile_name: str, + rb_name: str, + rb_version: str, + override_values: dict = None, + labels: dict = None) -> "Instance": + """Create Instance. + + Args: + cloud_region_id (str): Cloud region ID + profile_name (str): Name of profile to be instantiated + rb_name: (bytes): Definition name + rb_version (str): Definition version + override_values (dict): List of optional override values + labels (dict): List of optional labels + + Returns: + Instance: Created object + + """ + if labels is None: + labels = {} + if override_values is None: + override_values = {} + url: str = f"{cls.base_url}" + response: dict = cls.send_message_json( + "POST", + "Create Instance", + url, + data=jinja_env().get_template("multicloud_k8s_instantiate.json.j2").render( + cloud_region_id=cloud_region_id, + profile_name=profile_name, + rb_name=rb_name, + rb_version=rb_version, + override_values=override_values, + labels=labels), + headers={} + ) + return cls( + instance_id=response["id"], + namespace=response["namespace"], + request=InstantiationRequest(response["request"]), + resources=response["resources"], + override_values=response.get("override-values") + ) + + def delete(self) -> None: + """Delete Instance object.""" + self.send_message( + "DELETE", + f"Delete {self.instance_id} instance", + self.url + ) diff --git a/src/onapsdk/msb/msb_service.py b/src/onapsdk/msb/msb_service.py new file mode 100644 index 0000000..017fed7 --- /dev/null +++ b/src/onapsdk/msb/msb_service.py @@ -0,0 +1,24 @@ +"""Microsevice bus module.""" +# Copyright 2022 Orange, 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 onapsdk.configuration import settings +from onapsdk.onap_service import OnapService +from onapsdk.utils.headers_creator import headers_msb_creator + + +class MSB(OnapService): + """Microservice Bus base class.""" + + base_url = settings.MSB_URL + headers = headers_msb_creator(OnapService.headers) diff --git a/src/onapsdk/msb/multicloud.py b/src/onapsdk/msb/multicloud.py new file mode 100644 index 0000000..bc8b468 --- /dev/null +++ b/src/onapsdk/msb/multicloud.py @@ -0,0 +1,55 @@ +"""Multicloud module.""" +# Copyright 2022 Orange, 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 .msb_service import MSB + + +class Multicloud(MSB): + """MSB subclass to register/unregister instance to ONAP.""" + + base_url = f"{MSB.base_url}/api/multicloud/v1" + + @classmethod + def register_vim(cls, + cloud_owner: str, + cloud_region_id: str, + default_tenant: str = None) -> None: + """Register a VIM instance to ONAP. + + Args: + cloud_owner (str): Cloud owner name + cloud_region_id (str): Cloud region ID + default_tenant (str, optional): Default tenant name. Defaults to None. + """ + cls.send_message( + "POST", + "Register VIM instance to ONAP", + f"{cls.base_url}/{cloud_owner}/{cloud_region_id}/registry", + data={"defaultTenant": default_tenant} if default_tenant else None + ) + + @classmethod + def unregister_vim(cls, cloud_owner: str, cloud_region_id: str) -> None: + """Unregister a VIM instance from ONAP. + + Args: + cloud_owner (str): Cloud owner name + cloud_region_id (str): Cloud region ID + """ + cls.send_message( + "DELETE", + "Unregister VIM instance from ONAP", + f"{cls.base_url}/{cloud_owner}/{cloud_region_id}" + ) diff --git a/src/onapsdk/msb/templates/msb_esr_vim_registration.json.j2 b/src/onapsdk/msb/templates/msb_esr_vim_registration.json.j2 new file mode 100644 index 0000000..ba19258 --- /dev/null +++ b/src/onapsdk/msb/templates/msb_esr_vim_registration.json.j2 @@ -0,0 +1,31 @@ +{ + "cloudOwner": "{{ cloud_owner }}", + "cloudRegionId": "{{ cloud_region_id }}", + "cloudType": "{{ cloud_type }}", + "cloudRegionVersion": "{{ cloud_region_version }}" + {% if owner_defined_type %} + , "ownerDefinedType": "{{ owner_defined_type }}" + {% endif %} + {% if cloud_zone %} + , "cloudZone": "{{ cloud_zone }}" + {% endif %} + {% if complex_name %} + , "physicalLocationId": "{{ physical_location_id }}" + {% endif %} + {% if cloud_extra_info %} + , "cloudExtraInfo": "{{ cloud_extra_info }}" + {% endif %} + , "vimAuthInfos": + [{ + "userName": "{{ auth_info_username }}", + "password": "{{ auth_info_password }}", + "authUrl": "{{ auth_info_url }}", + "cloudDomain": "{{ auth_info_cloud_domain }}" + {% if auth_info_ssl_cacert %} + , "sslCacert": "{{ auth_info_ssl_cacert }}" + {% endif %} + {% if auth_info_ssl_insecure is not none %} + , "sslInsecure": {{ auth_info_ssl_insecure | tojson }} + {% endif %} + }] +} diff --git a/src/onapsdk/msb/templates/multicloud_k8s_add_connectivity_info.json.j2 b/src/onapsdk/msb/templates/multicloud_k8s_add_connectivity_info.json.j2 new file mode 100644 index 0000000..4a3dc2d --- /dev/null +++ b/src/onapsdk/msb/templates/multicloud_k8s_add_connectivity_info.json.j2 @@ -0,0 +1,8 @@ +{ + "cloud-region" : "{{ cloud_region_id }}", + "cloud-owner" : "{{ cloud_owner }}", + "other-connectivity-list" : { + "connectivity-records" : [ + ] + } +}
\ No newline at end of file diff --git a/src/onapsdk/msb/templates/multicloud_k8s_add_definition.json.j2 b/src/onapsdk/msb/templates/multicloud_k8s_add_definition.json.j2 new file mode 100644 index 0000000..866d577 --- /dev/null +++ b/src/onapsdk/msb/templates/multicloud_k8s_add_definition.json.j2 @@ -0,0 +1,7 @@ +{ + "rb-name": "{{ rb_name }}", + "rb-version": "{{ rb_version }}", + "chart-name": "{{ chart_name }}", + "description": "{{ description }}", + "labels": {{ labels }} +} diff --git a/src/onapsdk/msb/templates/multicloud_k8s_create_configuration_template.json.j2 b/src/onapsdk/msb/templates/multicloud_k8s_create_configuration_template.json.j2 new file mode 100644 index 0000000..61e6d2b --- /dev/null +++ b/src/onapsdk/msb/templates/multicloud_k8s_create_configuration_template.json.j2 @@ -0,0 +1,4 @@ +{ + "template-name": "{{ template_name }}", + "description": "{{ description }}" +}
\ No newline at end of file diff --git a/src/onapsdk/msb/templates/multicloud_k8s_create_profile_for_definition.json.j2 b/src/onapsdk/msb/templates/multicloud_k8s_create_profile_for_definition.json.j2 new file mode 100644 index 0000000..5ea2de1 --- /dev/null +++ b/src/onapsdk/msb/templates/multicloud_k8s_create_profile_for_definition.json.j2 @@ -0,0 +1,8 @@ +{ + "rb-name": "{{ rb_name }}", + "rb-version": "{{ rb_version }}", + "profile-name": "{{ profile_name }}", + "release-name": "{{ release_name }}", + "namespace": "{{ namespace }}", + "kubernetes-version": "{{ kubernetes_version }}" +}
\ No newline at end of file diff --git a/src/onapsdk/msb/templates/multicloud_k8s_instantiate.json.j2 b/src/onapsdk/msb/templates/multicloud_k8s_instantiate.json.j2 new file mode 100644 index 0000000..fa5ef66 --- /dev/null +++ b/src/onapsdk/msb/templates/multicloud_k8s_instantiate.json.j2 @@ -0,0 +1,18 @@ +{ + "cloud-region": "{{ cloud_region_id }}", + "profile-name": "{{ profile_name }}", + "rb-name": "{{ rb_name }}", + "rb-version": "{{ rb_version }}", + "override-values": + { + {% for override_value in override_values %} + "{{ override_value.name }}": "{{ override_value.value }}"{% if not loop.last %},{% endif %} + {% endfor %} + }, + "labels": + { + {% for label in labels %} + "{{ label.name }}": "{{ label.value }}"{% if not loop.last %},{% endif %} + {% endfor %} + } +}
\ No newline at end of file diff --git a/src/onapsdk/nbi/__init__.py b/src/onapsdk/nbi/__init__.py new file mode 100644 index 0000000..d3ceaf6 --- /dev/null +++ b/src/onapsdk/nbi/__init__.py @@ -0,0 +1,16 @@ +"""NBI package.""" +# Copyright 2022 Orange, 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 .nbi import Nbi, Service, ServiceOrder, ServiceSpecification diff --git a/src/onapsdk/nbi/nbi.py b/src/onapsdk/nbi/nbi.py new file mode 100644 index 0000000..17356cb --- /dev/null +++ b/src/onapsdk/nbi/nbi.py @@ -0,0 +1,490 @@ +"""NBI module.""" +# Copyright 2022 Orange, 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 enum import Enum +from typing import Iterator +from uuid import uuid4 + +from onapsdk.aai.business.customer import Customer +from onapsdk.exceptions import RequestError +from onapsdk.onap_service import OnapService +from onapsdk.utils import get_zulu_time_isoformat +from onapsdk.utils.jinja import jinja_env +from onapsdk.utils.mixins import WaitForFinishMixin +from onapsdk.configuration import settings + + +class Nbi(OnapService, ABC): + """NBI base class.""" + + base_url = settings.NBI_URL + api_version = settings.NBI_API_VERSION + + @classmethod + def is_status_ok(cls) -> bool: + """Check NBI service status. + + Returns: + bool: True if NBI works fine, False otherwise + + """ + try: + cls.send_message( + "GET", + "Check NBI status", + f"{cls.base_url}{cls.api_version}/status" + ) + except RequestError as exc: + msg = f"An error occured during NBI status check: {exc}" + cls._logger.error(msg) + return False + return True + + +class ServiceSpecification(Nbi): + """NBI service specification class.""" + + def __init__(self, # pylint: disable=too-many-arguments + unique_id: str, + name: str, + invariant_uuid: str, + category: str, + distribution_status: str, + version: str, + lifecycle_status: str) -> None: + """Service specification object initialization. + + Args: + unique_id (str): Unique ID + name (str): Service specification name + invariant_uuid (str): Invariant UUID + category (str): Category + distribution_status (str): Service distribution status + version (str): Service version + lifecycle_status (str): Service lifecycle status + """ + super().__init__() + self.unique_id: str = unique_id + self.name: str = name + self.invariant_uuid: str = invariant_uuid + self.category: str = category + self.distribution_status: str = distribution_status + self.version: str = version + self.lifecycle_status: str = lifecycle_status + + def __repr__(self) -> str: + """Service specification representation. + + Returns: + str: Service specification object human readable representation + + """ + return (f"ServiceSpecification(unique_id={self.unique_id}, name={self.name}, " + f"invariant_uuid={self.invariant_uuid}, category={self.category}, " + f"distribution_status={self.distribution_status}, version={self.version}, " + f"lifecycle_status={self.lifecycle_status})") + + @classmethod + def get_all(cls) -> Iterator["ServiceSpecification"]: + """Get all service specifications. + + Yields: + ServiceSpecification: Service specification object + + """ + for service_specification in cls.send_message_json("GET", + "Get service specifications from NBI", + (f"{cls.base_url}{cls.api_version}/" + "serviceSpecification")): + yield ServiceSpecification( + service_specification.get("id"), + service_specification.get("name"), + service_specification.get("invariantUUID"), + service_specification.get("category"), + service_specification.get("distributionStatus"), + service_specification.get("version"), + service_specification.get("lifecycleStatus"), + ) + + @classmethod + def get_by_id(cls, service_specification_id: str) -> "ServiceSpecification": + """Get service specification by ID. + + Args: + service_specification_id (str): Service specification ID + + Returns: + ServiceSpecification: Service specification object + + """ + service_specification: dict = cls.send_message_json( + "GET", + f"Get service specification with {service_specification_id} ID from NBI", + f"{cls.base_url}{cls.api_version}/serviceSpecification/{service_specification_id}" + ) + return ServiceSpecification( + service_specification.get("id"), + service_specification.get("name"), + service_specification.get("invariantUUID"), + service_specification.get("category"), + service_specification.get("distributionStatus"), + service_specification.get("version"), + service_specification.get("lifecycleStatus"), + ) + + +class Service(Nbi): + """NBI service.""" + + def __init__(self, # pylint: disable=too-many-arguments + name: str, + service_id: str, + service_specification_name: str, + service_specification_id: str, + customer_id: str, + customer_role: str, + href: str) -> None: + """Service object initialization. + + Args: + name (str): Service name + service_id (str): Service ID + service_specification_name (str): Service specification name + service_specification_id (str): Service specification ID + customer_id (str): Global customer ID + customer_role (str): Customer role + href (str): Service object href + """ + super().__init__() + self.name: str = name + self.service_id: str = service_id + self._service_specification_name: str = service_specification_name + self._service_specification_id: str = service_specification_id + self._customer_id: str = customer_id + self.customer_role: str = customer_role + self.href: str = href + + def __repr__(self) -> str: + """Service object representation. + + Returns: + str: Human readable service object representation + + """ + return (f"Service(name={self.name}, service_id={self.service_id}, " + f"service_specification={self.service_specification}, customer={self.customer}, " + f"customer_role={self.customer_role})") + + @classmethod + def get_all(cls, customer_id: str = 'generic') -> Iterator["Service"]: + """Get all services for selected customer. + + Args: + customer_id (str): Global customer ID + + Yields: + Service: Service object + + """ + for service in cls.send_message_json("GET", + "Get service instances from NBI", + f"{cls.base_url}{cls.api_version}/service?" + f"relatedParty.id={customer_id}"): + yield cls(service.get("name"), + service.get("id"), + service.get("serviceSpecification", {}).get("name"), + service.get("serviceSpecification", {}).get("id"), + service.get("relatedParty", {}).get("id"), + service.get("relatedParty", {}).get("role"), + service.get("href")) + + @property + def customer(self) -> Customer: + """Service order Customer object. + + Returns: + Customer: Customer object + + """ + if not self._customer_id: + return None + return Customer.get_by_global_customer_id(self._customer_id) + + @property + def service_specification(self) -> ServiceSpecification: + """Service specification. + + Returns: + ServiceSpecification: Service specification object + + """ + if not self._service_specification_id: + return None + return ServiceSpecification.get_by_id(self._service_specification_id) + + +class ServiceOrder(Nbi, WaitForFinishMixin): # pylint: disable=too-many-instance-attributes + """Service order class.""" + + WAIT_FOR_SLEEP_TIME = 10 + + def __init__(self, # pylint: disable=too-many-arguments + unique_id: str, + href: str, + priority: str, + description: str, + category: str, + external_id: str, + service_instance_name: str, + state: str = None, + customer: Customer = None, + customer_id: str = None, + service_specification: ServiceSpecification = None, + service_specification_id: str = None) -> None: + """Service order object initialization. + + Args: + unique_id (str): unique ID + href (str): object's href + priority (str): order priority + description (str): order description + category (str): category description + external_id (str): external ID + service_instance_name (str): name of service instance + state (str, optional): instantiation state. Defaults to None. + customer (Customer, optional): Customer object. Defaults to None. + customer_id (str, optional): global customer ID. Defaults to None. + service_specification (ServiceSpecification, optional): service specification object. + Defaults to None. + service_specification_id (str, optional): service specification ID. Defaults to None. + """ + super().__init__() + self.unique_id: str = unique_id + self.href: str = href + self.priority: str = priority + self.category: str = category + self.description: str = description + self.external_id: str = external_id + self._customer: Customer = customer + self._customer_id: str = customer_id + self._service_specification: ServiceSpecification = service_specification + self._service_specification_id: str = service_specification_id + self.service_instance_name: str = service_instance_name + self.state: str = state + + class StatusEnum(Enum): + """Status enum. + + Store possible statuses for service order: + - completed, + - failed, + - inProgress. + If instantiation has status which is not covered by these values + `unknown` value is used. + + """ + + ACKNOWLEDGED = "acknowledged" + IN_PROGRESS = "inProgress" + FAILED = "failed" + COMPLETED = "completed" + REJECTED = "rejected" + UNKNOWN = "unknown" + + def __repr__(self) -> str: + """Service order object representation. + + Returns: + str: Service order object representation. + + """ + return (f"ServiceOrder(unique_id={self.unique_id}, href={self.href}, " + f"priority={self.priority}, category={self.category}, " + f"description={self.description}, external_id={self.external_id}, " + f"customer={self.customer}, service_specification={self.service_specification}" + f"service_instance_name={self.service_instance_name}, state={self.state})") + + @property + def customer(self) -> Customer: + """Get customer object used in service order. + + Returns: + Customer: Customer object + + """ + if not self._customer: + if not self._customer_id: + self._logger.error("No customer ID") + return None + self._customer = Customer.get_by_global_customer_id(self._customer_id) + return self._customer + + @property + def service_specification(self) -> ServiceSpecification: + """Service order service specification used in order item. + + Returns: + ServiceSpecification: Service specification + + """ + if not self._service_specification: + if not self._service_specification_id: + self._logger.error("No service specification") + return None + self._service_specification = ServiceSpecification.\ + get_by_id(self._service_specification_id) + return self._service_specification + + @classmethod + def get_all(cls) -> Iterator["ServiceOrder"]: + """Get all service orders. + + Returns: + Iterator[ServiceOrder]: ServiceOrder object + + """ + for service_order in cls.send_message_json("GET", + "Get all service orders", + f"{cls.base_url}{cls.api_version}/serviceOrder"): + service_order_related_party = None + if service_order.get("relatedParty") is not None: + service_order_related_party = service_order.get( + "relatedParty", [{}])[0].get("id") + + yield ServiceOrder( + unique_id=service_order.get("id"), + href=service_order.get("href"), + priority=service_order.get("priority"), + category=service_order.get("category"), + description=service_order.get("description"), + external_id=service_order.get("externalId"), + customer_id=service_order_related_party, + service_specification_id=service_order.get("orderItem", [{}])[0].get("service")\ + .get("serviceSpecification").get("id"), + service_instance_name=service_order.get("orderItem", [{}])[0].\ + get("service", {}).get("name"), + state=service_order.get("state") + ) + + @classmethod + def create(cls, + customer: Customer, + service_specification: ServiceSpecification, + name: str = None, + external_id: str = None) -> "ServiceOrder": + """Create service order. + + Returns: + ServiceOrder: ServiceOrder object + + """ + if external_id is None: + external_id = str(uuid4()) + if name is None: + name = f"Python_ONAP_SDK_service_instance_{str(uuid4())}" + response: dict = cls.send_message_json( + "POST", + "Add service instance via ServiceOrder API", + f"{cls.base_url}{cls.api_version}/serviceOrder", + data=jinja_env() + .get_template("nbi_service_order_create.json.j2") + .render( + customer=customer, + service_specification=service_specification, + service_instance_name=name, + external_id=external_id, + request_time=get_zulu_time_isoformat() + ) + ) + return cls( + unique_id=response.get("id"), + href=response.get("href"), + priority=response.get("priority"), + description=response.get("description"), + category=response.get("category"), + external_id=response.get("externalId"), + customer=customer, + service_specification=service_specification, + service_instance_name=name + ) + + @property + def status(self) -> "StatusEnum": + """Service order instantiation status. + + It's populated by call Service order endpoint. + + Returns: + StatusEnum: Service order status. + + """ + response: dict = self.send_message_json("GET", + "Get service order status", + (f"{self.base_url}{self.api_version}/" + f"serviceOrder/{self.unique_id}")) + try: + return self.StatusEnum(response.get("state")) + except (KeyError, ValueError): + self._logger.exception("Invalid status") + return self.StatusEnum.UNKNOWN + + @property + def completed(self) -> bool: + """Store an information if service order is completed or not. + + Service orded is completed if it's status is COMPLETED. + + Returns: + bool: True if service orded is completed, False otherwise. + + """ + return self.status == self.StatusEnum.COMPLETED + + @property + def rejected(self) -> bool: + """Store an information if service order is rejected or not. + + Service orded is completed if it's status is REJECTED. + + Returns: + bool: True if service orded is rejected, False otherwise. + + """ + return self.status == self.StatusEnum.REJECTED + + @property + def failed(self) -> bool: + """Store an information if service order is failed or not. + + Service orded is completed if it's status is FAILED. + + Returns: + bool: True if service orded is failed, False otherwise. + + """ + return self.status == self.StatusEnum.FAILED + + @property + def finished(self) -> bool: + """Store an information if service order is finished or not. + + Service orded is finished if it's status is not ACKNOWLEDGED or IN_PROGRESS. + + Returns: + bool: True if service orded is finished, False otherwise. + + """ + return self.status not in [self.StatusEnum.ACKNOWLEDGED, + self.StatusEnum.IN_PROGRESS] diff --git a/src/onapsdk/nbi/templates/nbi_service_order_create.json.j2 b/src/onapsdk/nbi/templates/nbi_service_order_create.json.j2 new file mode 100644 index 0000000..aee124d --- /dev/null +++ b/src/onapsdk/nbi/templates/nbi_service_order_create.json.j2 @@ -0,0 +1,28 @@ +{ + "externalId": "{{ external_id }}", + "priority": "1", + "description": "{{ service_specification.name }} order for {{ customer.global_customer_id }} customer via Python ONAP SDK", + "category": "Consumer", + "requestedStartDate": "{{ request_time }}", + "requestedCompletionDate": "{{ request_time }}", + "relatedParty": [ + { + "id": "{{ customer.global_customer_id }}", + "role": "ONAPcustomer", + "name": "{{ customer.global_customer_id }}" + } + ], + "orderItem": [ + { + "id": "1", + "action": "add", + "service": { + "name": "{{ service_instance_name }}", + "serviceState": "active", + "serviceSpecification": { + "id": "{{ service_specification.unique_id }}" + } + } + } + ] +}
\ No newline at end of file diff --git a/src/onapsdk/onap_service.py b/src/onapsdk/onap_service.py new file mode 100644 index 0000000..9298715 --- /dev/null +++ b/src/onapsdk/onap_service.py @@ -0,0 +1,327 @@ +"""ONAP Service module.""" +# Copyright 2022 Orange, 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 dataclasses import dataclass, field +from typing import Any, Callable, Dict, Iterator, List, Optional, Union + +import logging +import requests +import urllib3 +from urllib3.util.retry import Retry +import simplejson.errors + +from requests.adapters import HTTPAdapter +from requests import ( # pylint: disable=redefined-builtin + HTTPError, RequestException, ConnectionError +) + +from onapsdk.exceptions import ( + RequestError, APIError, ResourceNotFound, InvalidResponse, + ConnectionFailed, NoGuiError +) + +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + + +class OnapService(ABC): + """ + Mother Class of all ONAP services. + + An important attribute when inheriting from this class is `_jinja_env`. + it allows to fetch simply jinja templates where they are. + by default jinja engine will look for templates in `templates` directory of + the package. + See in Examples to see how to use. + + Attributes: + server (str): nickname of the server we send the request. Used in logs + strings. For example, 'SDC' is the nickame for SDC server. + headers (Dict[str, str]): the headers dictionnary to use. + proxy (Dict[str, str]): the proxy configuration if needed. + permanent_headers (Optional[Dict[str, str]]): optional dictionary of + headers which could be set by the user and which are **always** + added into sended request. Unlike the `headers`, which could be + overrided on `send_message` call these headers are constant. + + """ + + @dataclass + class PermanentHeadersCollection: + """Collection to store permanent headers.""" + + ph_dict: Dict[str, Any] = field(default_factory=dict) + ph_call: List[Callable] = field(default_factory=list) + + def __iter__(self) -> Iterator[Dict[str, any]]: + """Iterate through the headers. + + For dictionary based headers just return the dict and + for the callables iterate through the list of them, + call them and yield the result. + """ + yield self.ph_dict + for ph_call in self.ph_call: + yield ph_call() + + _logger: logging.Logger = logging.getLogger(__qualname__) + server: str = None + headers: Dict[str, str] = { + "Content-Type": "application/json", + "Accept": "application/json", + } + proxy: Dict[str, str] = None + permanent_headers: PermanentHeadersCollection = PermanentHeadersCollection() + + def __init_subclass__(cls): + """Subclass initialization. + + Add _logger property for any OnapService with it's class name as a logger name + """ + super().__init_subclass__() + cls._logger: logging.Logger = logging.getLogger(cls.__qualname__) + + def __init__(self) -> None: + """Initialize the service.""" + + @classmethod + def send_message(cls, method: str, action: str, url: str, # pylint: disable=too-many-locals + **kwargs) -> Union[requests.Response, None]: + """ + Send a message to an ONAP service. + + Args: + method (str): which method to use (GET, POST, PUT, PATCH, ...) + action (str): what action are we doing, used in logs strings. + url (str): the url to use + exception (Exception, optional): if an error occurs, raise the + exception given instead of RequestError + **kwargs: Arbitrary keyword arguments. any arguments used by + requests can be used here. + + Raises: + RequestError: if other exceptions weren't caught or didn't raise, + or if there was an ambiguous exception by a request + ResourceNotFound: 404 returned + APIError: returned an error code within 400 and 599, except 404 + ConnectionFailed: connection can't be established + + Returns: + the request response if OK + + """ + cert = kwargs.pop('cert', None) + basic_auth: Dict[str, str] = kwargs.pop('basic_auth', None) + exception = kwargs.pop('exception', None) + headers = kwargs.pop('headers', cls.headers).copy() + if OnapService.permanent_headers: + for header in OnapService.permanent_headers: + headers.update(header) + data = kwargs.get('data', None) + try: + # build the request with the requested method + session = cls.__requests_retry_session() + if cert: + session.cert = cert + OnapService._set_basic_auth_if_needed(basic_auth, session) + + cls._logger.debug("[%s][%s] sent header: %s", cls.server, action, + headers) + cls._logger.debug("[%s][%s] url used: %s", cls.server, action, url) + cls._logger.debug("[%s][%s] data sent: %s", cls.server, action, + data) + + response = session.request(method, + url, + headers=headers, + verify=False, + proxies=cls.proxy, + **kwargs) + + cls._logger.info( + "[%s][%s] response code: %s", + cls.server, action, + response.status_code if response is not None else "n/a") + cls._logger.debug( + "[%s][%s] response: %s", + cls.server, action, + response.text if (response is not None and + response.headers.get("Content-Type", "") in \ + ["application/json", "text/plain"]) else "n/a") + + response.raise_for_status() + return response + + except HTTPError as cause: + cls._logger.error("[%s][%s] API returned and error: %s", + cls.server, action, headers) + + msg = f'Code: {cause.response.status_code}. Info: {cause.response.text}.' + + if cause.response.status_code == 404: + exc = ResourceNotFound(msg) + else: + exc = APIError(msg) + + exc.response_status_code = cause.response.status_code + + raise exc from cause + + except ConnectionError as cause: + cls._logger.error("[%s][%s] Failed to connect: %s", cls.server, + action, cause) + + msg = f"Can't connect to {url}." + raise ConnectionFailed(msg) from cause + + except RequestException as cause: + cls._logger.error("[%s][%s] Request failed: %s", + cls.server, action, cause) + + if not exception: + msg = f"Ambiguous error while requesting {url}." + raise RequestError(msg) + + raise exception + + @classmethod + def _set_basic_auth_if_needed(cls, basic_auth, session): + if basic_auth: + session.auth = (basic_auth.get('username'), + basic_auth.get('password')) + + @classmethod + def send_message_json(cls, method: str, action: str, url: str, + **kwargs) -> Dict[Any, Any]: + """ + Send a message to an ONAP service and parse the response as JSON. + + Args: + method (str): which method to use (GET, POST, PUT, PATCH, ...) + action (str): what action are we doing, used in logs strings. + url (str): the url to use + exception (Exception, optional): if an error occurs, raise the + exception given + **kwargs: Arbitrary keyword arguments. any arguments used by + requests can be used here. + + Raises: + InvalidResponse: if JSON coudn't be decoded + RequestError: if other exceptions weren't caught or didn't raise + APIError/ResourceNotFound: send_message() got an HTTP error code + ConnectionFailed: connection can't be established + RequestError: send_message() raised an ambiguous exception + + + Returns: + the response body in dict format if OK + + """ + exception = kwargs.get('exception', None) + try: + + response = cls.send_message(method, action, url, **kwargs) + + if response: + return response.json() + + except simplejson.errors.JSONDecodeError as cause: + cls._logger.error("[%s][%s]Failed to decode JSON: %s", cls.server, + action, cause) + raise InvalidResponse from cause + + except RequestError as exc: + cls._logger.error("[%s][%s] request failed: %s", + cls.server, action, exc) + if not exception: + exception = exc + + raise exception + + @staticmethod + def __requests_retry_session(retries: int = 10, + backoff_factor: float = 0.3, + session: requests.Session = None + ) -> requests.Session: + """ + Create a request Session with retries. + + Args: + retries (int, optional): number of retries. Defaults to 10. + backoff_factor (float, optional): backoff_factor. Defaults to 0.3. + session (requests.Session, optional): an existing session to + enhance. Defaults to None. + + Returns: + requests.Session: the session with retries set + + """ + session = session or requests.Session() + retry = Retry( + total=retries, + read=retries, + connect=retries, + backoff_factor=backoff_factor, + ) + adapter = HTTPAdapter(max_retries=retry) + session.mount('http://', adapter) + session.mount('https://', adapter) + return session + + @staticmethod + def set_proxy(proxy: Dict[str, str]) -> None: + """ + Set the proxy for Onap Services rest calls. + + Args: + proxy (Dict[str, str]): the proxy configuration + + Examples: + >>> OnapService.set_proxy({ + ... 'http': 'socks5h://127.0.0.1:8082', + ... 'https': 'socks5h://127.0.0.1:8082'}) + + """ + OnapService.proxy = proxy + + @staticmethod + def set_header(header: Optional[Union[Dict[str, Any], Callable]] = None) -> None: + """Set the header which will be always send on request. + + The header can be: + * dictionary - will be used same dictionary for each request + * callable - a method which is going to be called every time on request + creation. Could be useful if you need to connect with ONAP through some API + gateway and you need to take care about authentication. The callable shouldn't + require any parameters + * None - reset headers + + Args: + header (Optional[Union[Dict[str, Any], Callable]]): header to set. Defaults to None + + """ + if not header: + OnapService._logger.debug("Reset headers") + OnapService.permanent_headers = OnapService.PermanentHeadersCollection() + return + if callable(header): + OnapService.permanent_headers.ph_call.append(header) + else: + OnapService.permanent_headers.ph_dict.update(header) + OnapService._logger.debug("Set permanent header %s", header) + + @classmethod + def get_guis(cls): + """Return the list of GUI and its status.""" + raise NoGuiError diff --git a/src/onapsdk/sdc/__init__.py b/src/onapsdk/sdc/__init__.py new file mode 100644 index 0000000..15280d9 --- /dev/null +++ b/src/onapsdk/sdc/__init__.py @@ -0,0 +1,486 @@ +"""SDC Element module.""" +# Copyright 2022 Orange, 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, List, Optional, Union +from operator import attrgetter +from abc import ABC, abstractmethod + +from requests import Response + +from onapsdk.configuration import settings +from onapsdk.exceptions import APIError, RequestError +from onapsdk.onap_service import OnapService +import onapsdk.constants as const +from onapsdk.utils.jinja import jinja_env +from onapsdk.utils.gui import GuiItem, GuiList + +class SDC(OnapService, ABC): + """Mother Class of all SDC elements.""" + + server: str = "SDC" + base_front_url = settings.SDC_FE_URL + base_back_url = settings.SDC_BE_URL + + def __init__(self, name: str = None) -> None: + """Initialize SDC.""" + super().__init__() + self.name: str = name + + def __eq__(self, other: Any) -> bool: + """ + Check equality for SDC and children. + + Args: + other: another object + + Returns: + bool: True if same object, False if not + + """ + if isinstance(other, type(self)): + return self.name == other.name + return False + + @classmethod + @abstractmethod + def _get_all_url(cls) -> str: + """ + Get URL for all elements in SDC. + + Raises: + NotImplementedError: this is an abstract method. + + """ + + @classmethod + @abstractmethod + def _get_objects_list(cls, + result: List[Dict[str, Any]]) -> List['SdcResource']: + """ + Import objects created in SDC. + + Args: + result (Dict[str, Any]): the result returned by SDC in a Dict + + Raises: + NotImplementedError: this is an abstract method. + + """ + + @classmethod + @abstractmethod + def _base_url(cls) -> str: + """ + Give back the base url of Sdc. + + Raises: + NotImplementedError: this is an abstract method. + + """ + + @classmethod + @abstractmethod + def _base_create_url(cls) -> str: + """ + Give back the base url of Sdc. + + Raises: + NotImplementedError: this is an abstract method. + + """ + + @abstractmethod + def _copy_object(self, obj: 'SDC') -> None: + """ + Copy relevant properties from object. + + Args: + obj (Sdc): the object to "copy" + + Raises: + NotImplementedError: this is an abstract method. + + """ + + @classmethod + @abstractmethod + def import_from_sdc(cls, values: Dict[str, Any]) -> 'SDC': + """ + Import Sdc object from SDC. + + Args: + values (Dict[str, Any]): dict to parse returned from SDC. + + Raises: + NotImplementedError: this is an abstract method. + + """ + + @staticmethod + def _get_mapped_version(item: "SDC") -> Optional[Union[float, str]]: + """Map Sdc objects version to float. + + Mostly we need to get the newest version of the requested objects. To do + so we use the version property of them. In most cases it's string + formatted float value, but in some cases (like VSP objects) it isn't. + That method checks if given object has "version" attribute and if it's not + a None it tries to map it's value to float. If it's not possible it + returns the alrady existing value. + + Args: + item (SDC): SDC item to map version to float + + Returns: + Optional[Union[float, str]]: Float format version if possible, + string otherwise. If object doesn't have "version" + attribut returns None. + + """ + if hasattr(item, "version") and item.version is not None: + try: + return float(item.version) + except ValueError: + return item.version + else: + return None + + @classmethod + def get_all(cls, **kwargs) -> List['SDC']: + """ + Get the objects list created in SDC. + + Returns: + the list of the objects + + """ + cls._logger.info("retrieving all objects of type %s from SDC", + cls.__name__) + url = cls._get_all_url() + objects = [] + + try: + result = \ + cls.send_message_json('GET', "get {}s".format(cls.__name__), + url, **kwargs) + + for obj_info in cls._get_objects_list(result): + objects.append(cls.import_from_sdc(obj_info)) + + except APIError as exc: + cls._logger.debug("Couldn't get %s: %s", cls.__name__, exc) + except KeyError as exc: + cls._logger.debug("Invalid result dictionary: %s", exc) + + cls._logger.debug("number of %s returned: %s", cls.__name__, + len(objects)) + return objects + + def exists(self) -> bool: + """ + Check if object already exists in SDC and update infos. + + Returns: + True if exists, False either + + """ + self._logger.debug("check if %s %s exists in SDC", + type(self).__name__, self.name) + objects = self.get_all() + + self._logger.debug("filtering objects of all versions to be %s", + self.name) + relevant_objects = list(filter(lambda obj: obj == self, objects)) + + if not relevant_objects: + + self._logger.info("%s %s doesn't exist in SDC", + type(self).__name__, self.name) + return False + + if hasattr(self, 'version_filter') and self.version_filter is not None: # pylint: disable=no-member + + self._logger.debug("filtering %s objects by version %s", + self.name, self.version_filter) # pylint: disable=no-member + + all_versioned = filter( + lambda obj: obj.version == self.version_filter, relevant_objects) # pylint: disable=no-member + + try: + versioned_object = next(all_versioned) + except StopIteration: + self._logger.info("Version %s of %s %s, doesn't exist in SDC", + self.version_filter, type(self).__name__, # pylint: disable=no-member + self.name) + return False + + else: + versioned_object = max(relevant_objects, key=self._get_mapped_version) + + self._logger.info("%s found, updating information", type(self).__name__) + self._copy_object(versioned_object) + return True + + @classmethod + def get_guis(cls) -> GuiItem: + """Retrieve the status of the SDC GUIs. + + Only one GUI is referenced for SDC + the SDC Front End + + Return the list of GUIs + """ + gui_url = settings.SDC_GUI_SERVICE + sdc_gui_response = cls.send_message( + "GET", "Get SDC GUI Status", gui_url) + guilist = GuiList([]) + guilist.add(GuiItem( + gui_url, + sdc_gui_response.status_code)) + return guilist + +class SdcOnboardable(SDC, ABC): + """Base class for onboardable SDC resources (Vendors, Services, ...).""" + + ACTION_TEMPLATE: str + ACTION_METHOD: str + + def __init__(self, name: str = None) -> None: + """Initialize the object.""" + super().__init__(name) + self._identifier: str = None + self._status: str = None + self._version: str = None + + @property + def identifier(self) -> str: + """Return and lazy load the identifier.""" + if not self._identifier: + self.load() + return self._identifier + + @property + def status(self) -> str: + """Return and lazy load the status.""" + if self.created() and not self._status: + self.load() + return self._status + + @property + def version(self) -> str: + """Return and lazy load the version.""" + if self.created() and not self._version: + self.load() + return self._version + + @identifier.setter + def identifier(self, value: str) -> None: + """Set value for identifier.""" + self._identifier = value + + @status.setter + def status(self, status: str) -> None: + """Return and lazy load the status.""" + self._status = status + + @version.setter + def version(self, version: str) -> None: + """Return and lazy load the status.""" + self._version = version + + def created(self) -> bool: + """Determine if SDC is created.""" + if self.name and not self._identifier: + return self.exists() + return bool(self._identifier) + + def submit(self) -> None: + """Submit the SDC object in order to enable it.""" + self._logger.info("attempting to certify/sumbit %s %s in SDC", + type(self).__name__, self.name) + if self.status != const.CERTIFIED and self.created(): + self._really_submit() + elif self.status == const.CERTIFIED: + self._logger.warning("%s %s in SDC is already submitted/certified", + type(self).__name__, self.name) + elif not self.created(): + self._logger.warning("%s %s in SDC is not created", + type(self).__name__, self.name) + + def _create(self, template_name: str, **kwargs) -> None: + """Create the object in SDC if not already existing.""" + self._logger.info("attempting to create %s %s in SDC", + type(self).__name__, self.name) + if not self.exists(): + url = "{}/{}".format(self._base_create_url(), self._sdc_path()) + template = jinja_env().get_template(template_name) + data = template.render(**kwargs) + try: + create_result = self.send_message_json('POST', + "create {}".format( + type(self).__name__), + url, + data=data) + except RequestError as exc: + self._logger.error( + "an error occured during creation of %s %s in SDC", + type(self).__name__, self.name) + raise exc + else: + self._logger.info("%s %s is created in SDC", + type(self).__name__, self.name) + self._status = const.DRAFT + self.identifier = self._get_identifier_from_sdc(create_result) + self._version = self._get_version_from_sdc(create_result) + self.update_informations_from_sdc_creation(create_result) + + else: + self._logger.warning("%s %s is already created in SDC", + type(self).__name__, self.name) + + def _action_to_sdc(self, action: str, action_type: str = None, + **kwargs) -> Response: + """ + Really do an action in the SDC. + + Args: + action (str): the action to perform + action_type (str, optional): the type of action + headers (Dict[str, str], optional): headers to use if any + + Returns: + Response: the response + + """ + subpath = self._generate_action_subpath(action) + url = self._action_url(self._base_create_url(), + subpath, + self._version_path(), + action_type=action_type) + template = jinja_env().get_template(self.ACTION_TEMPLATE) + data = template.render(action=action, const=const) + + return self.send_message(self.ACTION_METHOD, + "{} {}".format(action, + type(self).__name__), + url, + data=data, + **kwargs) + + @abstractmethod + def update_informations_from_sdc(self, details: Dict[str, Any]) -> None: + """ + + Update instance with details from SDC. + + Args: + details ([type]): [description] + + """ + @abstractmethod + def update_informations_from_sdc_creation(self, + details: Dict[str, Any]) -> None: + """ + + Update instance with details from SDC after creation. + + Args: + details ([type]): the details from SDC + + """ + + @abstractmethod + def load(self) -> None: + """ + Load Object information from SDC. + + Raises: + NotImplementedError: this is an abstract method. + + """ + + @abstractmethod + def _get_version_from_sdc(self, sdc_infos: Dict[str, Any]) -> str: + """ + Get version from SDC results. + + Args: + sdc_infos (Dict[str, Any]): the result dict from SDC + + Raises: + NotImplementedError: this is an abstract method. + + """ + @abstractmethod + def _get_identifier_from_sdc(self, sdc_infos: Dict[str, Any]) -> str: + """ + Get identifier from SDC results. + + Args: + sdc_infos (Dict[str, Any]): the result dict from SDC + + Raises: + NotImplementedError: this is an abstract method. + + """ + @abstractmethod + def _generate_action_subpath(self, action: str) -> str: + """ + + Generate subpath part of SDC action url. + + Args: + action (str): the action that will be done + + Raises: + NotImplementedError: this is an abstract method. + + """ + @abstractmethod + def _version_path(self) -> str: + """ + Give the end of the path for a version. + + Raises: + NotImplementedError: this is an abstract method. + + """ + @abstractmethod + def _really_submit(self) -> None: + """Really submit the SDC Vf in order to enable it.""" + @staticmethod + @abstractmethod + def _action_url(base: str, + subpath: str, + version_path: str, + action_type: str = None) -> str: + """ + Generate action URL for SDC. + + Raises: + NotImplementedError: this is an abstract method. + + """ + @classmethod + @abstractmethod + def _sdc_path(cls) -> None: + """Give back the end of SDC path.""" + + @abstractmethod + def onboard(self) -> None: + """Onboard resource. + + Onboarding is a full stack of actions which needs to be done to + make SDC resource ready to use. It depends on the type of object + but most of them needs to be created and submitted. + """ diff --git a/src/onapsdk/sdc/category_management.py b/src/onapsdk/sdc/category_management.py new file mode 100644 index 0000000..c2d63b4 --- /dev/null +++ b/src/onapsdk/sdc/category_management.py @@ -0,0 +1,285 @@ +"""SDC category management module.""" +# Copyright 2022 Orange, 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 json +from abc import ABC, abstractmethod +from typing import Any, Dict, List + +from onapsdk.configuration import settings +from onapsdk.exceptions import ResourceNotFound +from onapsdk.sdc import SDC +from onapsdk.utils.headers_creator import headers_sdc_generic + + +class BaseCategory(SDC, ABC): # pylint: disable=too-many-instance-attributes + """Base SDC category class. + + It's SDC admin resource, has no common properties with + SDC resourcer or elements, so SDC class can't be it's + base class. + + """ + + SDC_ADMIN_USER = "demo" + + def __init__(self, name: str) -> None: + """Service category initialization. + + Args: + name (str): Service category name. + + """ + super().__init__(name) + self.normalized_name: str = None + self.unique_id: str = None + self.icons: List[str] = None + self.subcategories: List[Dict[str, str]] = None + self.version: str = None + self.owner_id: str = None + self.empty: bool = None + self.type: str = None + + @classmethod + def _get_all_url(cls) -> str: + """Get URL for all categories in SDC.""" + return f"{cls.base_front_url}/sdc1/feProxy/rest/v1/setup/ui" + + @classmethod + def _base_url(cls) -> str: + """Give back the base url of Sdc.""" + return f"{settings.SDC_FE_URL}/sdc1/feProxy/rest/v1/category" + + @classmethod + def headers(cls) -> Dict[str, str]: + """Headers used for category management. + + It uses SDC admin user. + + Returns: + Dict[str, str]: Headers + + """ + return headers_sdc_generic(super().headers, user=cls.SDC_ADMIN_USER) + + @classmethod + def get_all(cls, **kwargs) -> List['SDC']: + """ + Get the categories list created in SDC. + + Returns: + the list of the categories + + """ + return super().get_all(headers=cls.headers()) + + @classmethod + def import_from_sdc(cls, values: Dict[str, Any]) -> 'BaseCategory': + """ + Import category object from SDC. + + Args: + values (Dict[str, Any]): dict to parse returned from SDC. + + """ + category_obj = cls(name=values["name"]) + category_obj.normalized_name = values["normalizedName"] + category_obj.unique_id = values["uniqueId"] + category_obj.icons = values["icons"] + category_obj.subcategories = values["subcategories"] + category_obj.version = values["version"] + category_obj.owner_id = values["ownerId"] + category_obj.empty = values["empty"] + return category_obj + + @classmethod + @abstractmethod + def category_name(cls) -> str: + """Class category name. + + Used for logs. + + Returns: + str: Category name + + """ + + @classmethod + def get(cls, name: str) -> "BaseCategory": + """Get category with given name. + + Raises: + ResourceNotFound: Category with given name does not exist + + Returns: + BaseCategory: BaseCategory instance + + """ + category_obj: "BaseCategory" = cls(name) + if category_obj.exists(): + return category_obj + msg = f"{cls.category_name()} with \"{name}\" name does not exist." + raise ResourceNotFound(msg) + + @classmethod + def create(cls, name: str) -> "BaseCategory": + """Create category instance. + + Checks if category with given name exists and if it already + exists just returns category with given name. + + Returns: + BaseCategory: Created category instance + + """ + category_obj: "BaseCategory" = cls(name) + if category_obj.exists(): + return category_obj + cls.send_message_json("POST", + f"Create {name} {cls.category_name()}", + cls._base_create_url(), + data=json.dumps({"name": name}), + headers=cls.headers()) + category_obj.exists() + return category_obj + + def _copy_object(self, obj: 'BaseCategory') -> None: + """ + Copy relevant properties from object. + + Args: + obj (BaseCategory): the object to "copy" + + """ + self.name = obj.name + self.normalized_name = obj.normalized_name + self.unique_id = obj.unique_id + self.icons = obj.icons + self.subcategories = obj.subcategories + self.version = obj.version + self.owner_id = obj.owner_id + self.empty = obj.empty + + +class ResourceCategory(BaseCategory): + """Resource category class.""" + + @classmethod + def _get_objects_list(cls, + result: Dict[str, Any]) -> List[Dict[str, Any]]: + """ + Get list of resource categories. + + Args: + result (List[Dict[str, Any]]): the result returned by SDC + in a list of dicts + + Raises: + KeyError: Invalid result dictionary + + """ + return result["categories"]["resourceCategories"] + + @classmethod + def _base_create_url(cls) -> str: + """Url to create resource category. + + Returns: + str: Creation url + + """ + return f"{cls._base_url()}/resources" + + @classmethod + def category_name(cls) -> str: + """Resource category name. + + Used for logging. + + Returns: + str: Resource category name + + """ + return "Resource Category" + + @classmethod + def get(cls, name: str, subcategory: str = None) -> "ResourceCategory": # pylint: disable=arguments-differ + """Get resource category with given name. + + It returns resource category with all subcategories by default. You can + get resource category with only one subcategory if you provide it's + name as `subcategory` parameter. + + Args: + name (str): Resource category name. + subcategory (str, optional): Name of subcategory. Defaults to None. + + Raises: + ResourceNotFound: Subcategory with given name does not exist + + Returns: + BaseCategory: BaseCategory instance + + """ + category_obj: "ResourceCategory" = super().get(name=name) + if not subcategory: + return category_obj + filtered_subcategories: Dict[str, str] = list(filter(lambda x: x["name"] == subcategory, + category_obj.subcategories)) + if not filtered_subcategories: + raise ResourceNotFound(f"Subcategory {subcategory} does not exist.") + category_obj.subcategories = filtered_subcategories + return category_obj + + +class ServiceCategory(BaseCategory): + """Service category class.""" + + @classmethod + def _get_objects_list(cls, + result: Dict[str, Any]) -> List[Dict[str, Any]]: + """ + Get list of service categories. + + Args: + result (List[Dict[str, Any]]): the result returned by SDC + in a list of dicts + + Raises: + KeyError: Invalid result dictionary + + """ + return result["categories"]["serviceCategories"] + + @classmethod + def _base_create_url(cls) -> str: + """Url to create service category. + + Returns: + str: Creation url + + """ + return f"{cls._base_url()}/services" + + @classmethod + def category_name(cls) -> str: + """Service category name. + + Used for logging. + + Returns: + str: Service category name + + """ + return "Service Category" diff --git a/src/onapsdk/sdc/component.py b/src/onapsdk/sdc/component.py new file mode 100644 index 0000000..9b26bef --- /dev/null +++ b/src/onapsdk/sdc/component.py @@ -0,0 +1,162 @@ +"""SDC Component module.""" +# Copyright 2022 Orange, 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 dataclasses import dataclass +from typing import Any, Dict, Iterator, List, Optional +from onapsdk.exceptions import ParameterError + +from onapsdk.sdc.properties import ComponentProperty +from onapsdk.utils.jinja import jinja_env + + +@dataclass +class Component: # pylint: disable=too-many-instance-attributes + """Component dataclass.""" + + created_from_csar: bool + actual_component_uid: str + unique_id: str + normalized_name: str + name: str + origin_type: str + customization_uuid: str + component_uid: str + component_version: str + tosca_component_name: str + component_name: str + group_instances: Optional[List[Dict[str, Any]]] + sdc_resource: "SdcResource" + parent_sdc_resource: "SdcResource" + + @classmethod + def create_from_api_response(cls, + api_response: Dict[str, Any], + sdc_resource: "SdcResource", + parent_sdc_resource: "SdcResource") -> "Component": + """Create component from api response. + + Args: + api_response (Dict[str, Any]): component API response + sdc_resource (SdcResource): component's SDC resource + parent_sdc_resource (SdcResource): component's parent SDC resource + + Returns: + Component: Component created using api_response and SDC resource + + """ + return cls(created_from_csar=api_response["createdFromCsar"], + actual_component_uid=api_response["actualComponentUid"], + unique_id=api_response["uniqueId"], + normalized_name=api_response["normalizedName"], + name=api_response["name"], + origin_type=api_response["originType"], + customization_uuid=api_response["customizationUUID"], + component_uid=api_response["componentUid"], + component_version=api_response["componentVersion"], + tosca_component_name=api_response["toscaComponentName"], + component_name=api_response["componentName"], + group_instances=api_response["groupInstances"], + sdc_resource=sdc_resource, + parent_sdc_resource=parent_sdc_resource) + + @property + def properties_url(self) -> str: + """Url to get component's properties. + + Returns: + str: Compoent's properties url + + """ + return self.parent_sdc_resource.get_component_properties_url(self) + + @property + def properties_value_url(self) -> str: + """Url to set component property value. + + Returns: + str: Url to set component property value + + """ + return self.parent_sdc_resource.get_component_properties_value_set_url(self) + + @property + def properties(self) -> Iterator["ComponentProperty"]: + """Component properties. + + In SDC it's named as properties, but we uses "inputs" endpoint to fetch them. + Structure is also input's like, but it's a property. + + Yields: + ComponentProperty: Component property object + + """ + for component_property in self.sdc_resource.send_message_json(\ + "GET", + f"Get {self.name} component properties", + self.properties_url): + yield ComponentProperty(unique_id=component_property["uniqueId"], + name=component_property["name"], + property_type=component_property["type"], + _value=component_property.get("value"), + component=self) + + def get_property(self, property_name: str) -> "ComponentProperty": + """Get component property by it's name. + + Args: + property_name (str): property name + + Raises: + ParameterError: Component has no property with given name + + Returns: + ComponentProperty: Component's property object + + """ + for property_obj in self.properties: + if property_obj.name == property_name: + return property_obj + msg = f"Component has no property with {property_name} name" + raise ParameterError(msg) + + def set_property_value(self, property_obj: "ComponentProperty", value: Any) -> None: + """Set property value. + + Set given value to component property + + Args: + property_obj (ComponentProperty): Component property object + value (Any): Property value to set + + """ + self.sdc_resource.send_message_json( + "POST", + f"Set {self.name} component property {property_obj.name} value", + self.properties_value_url, + data=jinja_env().get_template(\ + "sdc_resource_component_set_property_value.json.j2").\ + render( + component=self, + value=value, + property=property_obj + ) + ) + + def delete(self) -> None: + """Delete component.""" + self.sdc_resource.send_message_json( + "DELETE", + f"Delete {self.name} component", + f"{self.parent_sdc_resource.resource_inputs_url}/resourceInstance/{self.unique_id}" + ) diff --git a/src/onapsdk/sdc/pnf.py b/src/onapsdk/sdc/pnf.py new file mode 100644 index 0000000..3fe4657 --- /dev/null +++ b/src/onapsdk/sdc/pnf.py @@ -0,0 +1,74 @@ +"""Pnf module.""" +# Copyright 2022 Orange, 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 Dict, List, Union +from onapsdk.exceptions import ParameterError + +from onapsdk.sdc.sdc_resource import SdcResource +from onapsdk.sdc.properties import NestedInput, Property +import onapsdk.constants as const +from onapsdk.sdc.vendor import Vendor +from onapsdk.sdc.vsp import Vsp + + +class Pnf(SdcResource): + """ + ONAP PNF Object used for SDC operations. + + Attributes: + name (str): the name of the pnf. Defaults to "ONAP-test-PNF". + identifier (str): the unique ID of the pnf from SDC. + status (str): the status of the pnf from SDC. + version (str): the version ID of the vendor from SDC. + uuid (str): the UUID of the PNF (which is different from identifier, + don't ask why...) + unique_identifier (str): Yet Another ID, just to puzzle us... + vendor (optional): the vendor of the PNF + vsp (optional): the vsp related to the PNF + + """ + + def __init__(self, name: str = None, version: str = None, vendor: Vendor = None, # pylint: disable=too-many-arguments + sdc_values: Dict[str, str] = None, vsp: Vsp = None, + properties: List[Property] = None, inputs: Union[Property, NestedInput] = None, + category: str = None, subcategory: str = None): + """ + Initialize pnf object. + + Args: + name (optional): the name of the pnf + version (str, optional): the version of a PNF object + + """ + super().__init__(sdc_values=sdc_values, version=version, properties=properties, + inputs=inputs, category=category, subcategory=subcategory) + self.name: str = name or "ONAP-test-PNF" + self.vendor: Vendor = vendor + self.vsp: Vsp = vsp + + def create(self) -> None: + """Create the PNF in SDC if not already existing.""" + if not self.vsp and not self.vendor: + raise ParameterError("Neither Vsp nor Vendor provided.") + self._create("pnf_create.json.j2", + name=self.name, + vsp=self.vsp, + vendor=self.vendor, + category=self.category) + + def _really_submit(self) -> None: + """Really submit the SDC PNF in order to enable it.""" + result = self._action_to_sdc(const.CERTIFY, "lifecycleState") + if result: + self.load() diff --git a/src/onapsdk/sdc/properties.py b/src/onapsdk/sdc/properties.py new file mode 100644 index 0000000..f7e07a0 --- /dev/null +++ b/src/onapsdk/sdc/properties.py @@ -0,0 +1,202 @@ +"""Service properties module.""" +# Copyright 2022 Orange, 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 dataclasses import dataclass, field +from typing import Any, Dict, List, Optional + +from onapsdk.exceptions import ParameterError + + +@dataclass +class Input: + """Property input dataclass.""" + + unique_id: str + input_type: str + name: str + sdc_resource: "SdcResource" + _default_value: Optional[Any] = field(repr=False, default=None) + + @property + def default_value(self) -> Any: + """Input default value. + + Returns: + Any: Input default value + + """ + return self._default_value + + @default_value.setter + def default_value(self, value: Any) -> None: + """Set input default value. + + Use related sdc_resource "set_input_default_value" + method to set default value in SDC. We use that + because different types of SDC resources has different + urls to call SDC API. + + Args: + value (Any): Default value to set + + """ + self.sdc_resource.set_input_default_value(self, value) + self._default_value = value + +@dataclass +class NestedInput: + """Dataclass used for nested input declaration.""" + + sdc_resource: "SdcResource" + input_obj: Input + + +class Property: # pylint: disable=too-many-instance-attributes, too-few-public-methods + """Service property class.""" + + def __init__(self, # pylint: disable=too-many-arguments + name: str, + property_type: str, + description: Optional[str] = None, + unique_id: Optional[str] = None, + parent_unique_id: Optional[str] = None, + sdc_resource: Optional["SdcResource"] = None, + value: Optional[Any] = None, + get_input_values: Optional[List[Dict[str, str]]] = None) -> None: + """Property class initialization. + + Args: + property_type (str): [description] + description (Optional[str], optional): [description]. Defaults to None. + unique_id (Optional[str], optional): [description]. Defaults to None. + parent_unique_id (Optional[str], optional): [description]. Defaults to None. + sdc_resource (Optional[, optional): [description]. Defaults to None. + value (Optional[Any], optional): [description]. Defaults to None. + get_input_values (Optional[List[Dict[str, str]]], optional): [description]. + Defaults to None. + """ + self.name: str = name + self.property_type: str = property_type + self.description: str = description + self.unique_id: str = unique_id + self.parent_unique_id: str = parent_unique_id + self.sdc_resource: "SdcResource" = sdc_resource + self._value: Any = value + self.get_input_values: List[Dict[str, str]] = get_input_values + + def __repr__(self) -> str: + """Property object human readable representation. + + Returns: + str: Property human readable representation + + """ + return f"Property(name={self.name}, property_type={self.property_type})" + + def __eq__(self, obj: "Property") -> bool: + """Check if two Property object are equal. + + Args: + obj (Property): Object to compare + + Returns: + bool: True if objects are equal, False otherwise + + """ + return self.name == obj.name and self.property_type == obj.property_type + + @property + def input(self) -> Input: + """Property input. + + Returns property Input object. + Returns None if property has no associated input. + + Raises: + ParameterError: Input has no associated SdcResource + + ParameterError: Input for given property does not exits. + It shouldn't ever happen, but it's possible if after you + get property object someone delete input. + + Returns: + Input: Property input object. + + """ + if not self.sdc_resource: + raise ParameterError("Property has no associated SdcResource") + if not self.get_input_values: + return None + try: + return next(filter(lambda x: x.unique_id == self.get_input_values[0].get("inputId"), + self.sdc_resource.inputs)) + except StopIteration: + raise ParameterError("Property input does not exist") + + @property + def value(self) -> Any: + """Value property. + + Get property value. + + Returns: + Any: Property value + + """ + return self._value + + @value.setter + def value(self, val: Any) -> Any: + if self.sdc_resource: + self.sdc_resource.set_property_value(self, val) + self._value = val + + +@dataclass +class ComponentProperty: + """Component property dataclass. + + Component properties are inputs objects in SDC, but in logic + it's a property. + + """ + + unique_id: str + property_type: str + name: str + component: "Component" + _value: Optional[Any] = field(repr=False, default=None) + + @property + def value(self) -> Any: + """Property value getter. + + Returns: + Any: Property value + + """ + return self._value + + @value.setter + def value(self, val: Any) -> None: + """Property value setter. + + Set value both in an object and in SDC using it's HTTP API. + + Args: + val (Any): Property value to set + + """ + self.component.set_property_value(self, val) + self._value = val diff --git a/src/onapsdk/sdc/sdc_element.py b/src/onapsdk/sdc/sdc_element.py new file mode 100644 index 0000000..df513b7 --- /dev/null +++ b/src/onapsdk/sdc/sdc_element.py @@ -0,0 +1,227 @@ +"""SDC Element module.""" +# Copyright 2022 Orange, 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 operator import itemgetter +from typing import Any, Dict, List, Optional + +from onapsdk.sdc import SdcOnboardable +import onapsdk.constants as const + + +class SdcElement(SdcOnboardable, ABC): + """Mother Class of all SDC elements.""" + + ACTION_TEMPLATE = 'sdc_element_action.json.j2' + ACTION_METHOD = 'PUT' + + def __init__(self, name: str = None) -> None: + """Initialize the object.""" + super().__init__(name=name) + self.human_readable_version: Optional[str] = None + + def _get_item_details(self) -> Dict[str, Any]: + """ + Get item details. + + Returns: + Dict[str, Any]: the description of the item + + """ + if self.created(): + url = "{}/items/{}/versions".format(self._base_url(), + self.identifier) + results: Dict[str, Any] = self.send_message_json('GET', 'get item', url) + if results["listCount"] > 1: + items: List[Dict[str, Any]] = results["results"] + return sorted(items, key=itemgetter("creationTime"), reverse=True)[0] + return results["results"][0] + return {} + + def load(self) -> None: + """Load Object information from SDC.""" + vsp_details = self._get_item_details() + if vsp_details: + self._logger.debug("details found, updating") + self.version = vsp_details['id'] + self.human_readable_version = vsp_details["name"] + self.update_informations_from_sdc(vsp_details) + else: + # exists() method check if exists AND update identifier + self.exists() + + def update_informations_from_sdc(self, details: Dict[str, Any]) -> None: + """ + + Update instance with details from SDC. + + Args: + details ([type]): [description] + + """ + def update_informations_from_sdc_creation(self, + details: Dict[str, Any]) -> None: + """ + + Update instance with details from SDC after creation. + + Args: + details ([type]): the details from SDC + + """ + @classmethod + def _base_url(cls) -> str: + """ + Give back the base url of Sdc. + + Returns: + str: the base url + + """ + return "{}/sdc1/feProxy/onboarding-api/v1.0".format(cls.base_front_url) + + @classmethod + def _base_create_url(cls) -> str: + """ + Give back the base url of Sdc. + + Returns: + str: the base url + + """ + return "{}/sdc1/feProxy/onboarding-api/v1.0".format(cls.base_front_url) + + def _generate_action_subpath(self, action: str) -> str: + """ + + Generate subpath part of SDC action url. + + Args: + action (str): the action that will be done + + Returns: + str: the subpath part + + """ + subpath = self._sdc_path() + if action == const.COMMIT: + subpath = "items" + return subpath + + def _version_path(self) -> str: + """ + Give the end of the path for a version. + + Returns: + str: the end of the path + + """ + return "{}/versions/{}".format(self.identifier, self.version) + + @staticmethod + def _action_url(base: str, + subpath: str, + version_path: str, + action_type: str = None) -> str: + """ + Generate action URL for SDC. + + Args: + base (str): base part of url + subpath (str): subpath of url + version_path (str): version path of the url + action_type (str, optional): the type of action. UNUSED here + + Returns: + str: the URL to use + + """ + return "{}/{}/{}/actions".format(base, subpath, version_path) + + @classmethod + def _get_objects_list(cls, result: List[Dict[str, Any]] + ) -> List[Dict[str, Any]]: + """ + Import objects created in SDC. + + Args: + result (Dict[str, Any]): the result returned by SDC in a Dict + + Return: + List[Dict[str, Any]]: the list of objects + + """ + return result['results'] + + @classmethod + def _get_all_url(cls) -> str: + """ + Get URL for all elements in SDC. + + Returns: + str: the url + + """ + return "{}/{}".format(cls._base_url(), cls._sdc_path()) + + def _copy_object(self, obj: 'SdcElement') -> None: + """ + Copy relevant properties from object. + + Args: + obj (SdcElement): the object to "copy" + + """ + self.identifier = obj.identifier + + def _get_version_from_sdc(self, sdc_infos: Dict[str, Any]) -> str: + """ + Get version from SDC results. + + Args: + sdc_infos (Dict[str, Any]): the result dict from SDC + + Returns: + str: the version + + """ + return sdc_infos['version']['id'] + + def _get_identifier_from_sdc(self, sdc_infos: Dict[str, Any]) -> str: + """ + Get identifier from SDC results. + + Args: + sdc_infos (Dict[str, Any]): the result dict from SDC + + Returns: + str: the identifier + + """ + return sdc_infos['itemId'] + + @classmethod + @abstractmethod + def import_from_sdc(cls, values: Dict[str, Any]) -> 'SdcElement': + """ + Import SdcElement from SDC. + + Args: + values (Dict[str, Any]): dict to parse returned from SDC. + + Raises: + NotImplementedError: this is an abstract method. + + """ + raise NotImplementedError("SdcElement is an abstract class") diff --git a/src/onapsdk/sdc/sdc_resource.py b/src/onapsdk/sdc/sdc_resource.py new file mode 100644 index 0000000..7e7dbb9 --- /dev/null +++ b/src/onapsdk/sdc/sdc_resource.py @@ -0,0 +1,960 @@ +"""SDC Element module.""" +# Copyright 2022 Orange, 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 abc import ABC +from typing import Any, Dict, Iterator, List, Union +import base64 +import time + +import onapsdk.constants as const +from onapsdk.exceptions import ParameterError, ResourceNotFound, StatusError +from onapsdk.sdc import SdcOnboardable +from onapsdk.sdc.category_management import ResourceCategory, ServiceCategory +from onapsdk.sdc.component import Component +from onapsdk.sdc.properties import Input, NestedInput, Property +from onapsdk.utils.headers_creator import (headers_sdc_creator, + headers_sdc_tester, + headers_sdc_artifact_upload) +from onapsdk.utils.jinja import jinja_env + + +# For an unknown reason, pylint keeps seeing _unique_uuid and +# _unique_identifier as attributes along with unique_uuid and unique_identifier +class SdcResource(SdcOnboardable, ABC): # pylint: disable=too-many-instance-attributes, too-many-public-methods + """Mother Class of all SDC resources.""" + + RESOURCE_PATH = 'resources' + ACTION_TEMPLATE = 'sdc_resource_action.json.j2' + ACTION_METHOD = 'POST' + headers = headers_sdc_creator(SdcOnboardable.headers) + + def __init__(self, name: str = None, version: str = None, # pylint: disable=too-many-arguments + sdc_values: Dict[str, str] = None, properties: List[Property] = None, + inputs: Union[Property, NestedInput] = None, + category: str = None, subcategory: str = None): + """Initialize the object.""" + super().__init__(name) + self.version_filter: str = version + self._unique_uuid: str = None + self._unique_identifier: str = None + self._resource_type: str = "resources" + self._properties_to_add: List[Property] = properties or [] + self._inputs_to_add: Union[Property, NestedInput] = inputs or [] + self._time_wait: int = 10 + self._category_name: str = category + self._subcategory_name: str = subcategory + if sdc_values: + self._logger.debug("SDC values given, using them") + self.identifier = sdc_values['uuid'] + self.version = sdc_values['version'] + self.unique_uuid = sdc_values['invariantUUID'] + distribitution_state = None + if 'distributionStatus' in sdc_values: + distribitution_state = sdc_values['distributionStatus'] + self.status = self._parse_sdc_status(sdc_values['lifecycleState'], + distribitution_state, + self._logger) + self._logger.debug("SDC resource %s status: %s", self.name, + self.status) + + def __repr__(self) -> str: + """SDC resource description. + + Returns: + str: SDC resource object description + + """ + return f"{self.__class__.__name__.upper()}(name={self.name})" + + @property + def unique_uuid(self) -> str: + """Return and lazy load the unique_uuid.""" + if not self._unique_uuid: + self.load() + return self._unique_uuid + + @property + def unique_identifier(self) -> str: + """Return and lazy load the unique_identifier.""" + if not self._unique_identifier: + self.deep_load() + return self._unique_identifier + + @unique_uuid.setter + def unique_uuid(self, value: str) -> None: + """Set value for unique_uuid.""" + self._unique_uuid = value + + @unique_identifier.setter + def unique_identifier(self, value: str) -> None: + """Set value for unique_identifier.""" + self._unique_identifier = value + + def load(self) -> None: + """Load Object information from SDC.""" + self.exists() + + def deep_load(self) -> None: + """Deep load Object informations from SDC.""" + url = ( + f"{self.base_front_url}/sdc1/feProxy/rest/v1/" + "screen?excludeTypes=VFCMT&excludeTypes=Configuration" + ) + headers = headers_sdc_creator(SdcResource.headers) + if self.status == const.UNDER_CERTIFICATION: + headers = headers_sdc_tester(SdcResource.headers) + + response = self.send_message_json("GET", + "Deep Load {}".format( + type(self).__name__), + url, + headers=headers) + + for resource in response[self._sdc_path()]: + if resource["invariantUUID"] == self.unique_uuid: + if resource["uuid"] == self.identifier: + self._logger.debug("Resource %s found in %s list", + resource["name"], self._sdc_path()) + self.unique_identifier = resource["uniqueId"] + self._category_name = resource["categories"][0]["name"] + subcategories = resource["categories"][0].get("subcategories", [{}]) + self._subcategory_name = None if subcategories is None else \ + subcategories[0].get("name") + return + if self._sdc_path() == "services": + for dependency in self.send_message_json("GET", + "Get service dependecies", + f"{self._base_create_url()}/services/" + f"{resource['uniqueId']}/" + "dependencies"): + if dependency["version"] == self.version: + self.unique_identifier = dependency["uniqueId"] + return + + def _generate_action_subpath(self, action: str) -> str: + """ + + Generate subpath part of SDC action url. + + Args: + action (str): the action that will be done + + Returns: + str: the subpath part + + """ + return action + + def _version_path(self) -> str: + """ + Give the end of the path for a version. + + Returns: + str: the end of the path + + """ + return self.unique_identifier + + def _action_url(self, + base: str, + subpath: str, + version_path: str, + action_type: str = None) -> str: + """ + Generate action URL for SDC. + + Args: + base (str): base part of url + subpath (str): subpath of url + version_path (str): version path of the url + action_type (str, optional): the type of action + ('distribution', 'distribution-state' + or 'lifecycleState'). Default to + 'lifecycleState'). + + Returns: + str: the URL to use + + """ + if not action_type: + action_type = "lifecycleState" + return "{}/{}/{}/{}/{}".format(base, self._resource_type, version_path, + action_type, subpath) + + @classmethod + def _base_create_url(cls) -> str: + """ + Give back the base url of Sdc. + + Returns: + str: the base url + + """ + return "{}/sdc1/feProxy/rest/v1/catalog".format(cls.base_front_url) + + @classmethod + def _base_url(cls) -> str: + """ + Give back the base url of Sdc. + + Returns: + str: the base url + + """ + return "{}/sdc/v1/catalog".format(cls.base_back_url) + + @classmethod + def _get_all_url(cls) -> str: + """ + Get URL for all elements in SDC. + + Returns: + str: the url + + """ + return "{}/{}?resourceType={}".format(cls._base_url(), cls._sdc_path(), + cls.__name__.upper()) + + @classmethod + def _get_objects_list(cls, result: List[Dict[str, Any]] + ) -> List[Dict[str, Any]]: + """ + Import objects created in SDC. + + Args: + result (Dict[str, Any]): the result returned by SDC in a Dict + + Return: + List[Dict[str, Any]]: the list of objects + + """ + return result + + def _get_version_from_sdc(self, sdc_infos: Dict[str, Any]) -> str: + """ + Get version from SDC results. + + Args: + sdc_infos (Dict[str, Any]): the result dict from SDC + + Returns: + str: the version + + """ + return sdc_infos['version'] + + def _get_identifier_from_sdc(self, sdc_infos: Dict[str, Any]) -> str: + """ + Get identifier from SDC results. + + Args: + sdc_infos (Dict[str, Any]): the result dict from SDC + + Returns: + str: the identifier + + """ + return sdc_infos['uuid'] + + @classmethod + def import_from_sdc(cls, values: Dict[str, Any]) -> 'SdcResource': + """ + Import SdcResource from SDC. + + Args: + values (Dict[str, Any]): dict to parse returned from SDC. + + Return: + SdcResource: the created resource + + """ + cls._logger.debug("importing SDC Resource %s from SDC", values['name']) + return cls(name=values['name'], sdc_values=values) + + def _copy_object(self, obj: 'SdcResource') -> None: + """ + Copy relevant properties from object. + + Args: + obj (SdcResource): the object to "copy" + + """ + self.identifier = obj.identifier + self.unique_uuid = obj.unique_uuid + self.status = obj.status + self.version = obj.version + self.unique_identifier = obj.unique_identifier + self._specific_copy(obj) + + def _specific_copy(self, obj: 'SdcResource') -> None: + """ + Copy specific properties from object. + + Args: + obj (SdcResource): the object to "copy" + + """ + + def update_informations_from_sdc(self, details: Dict[str, Any]) -> None: + """ + + Update instance with details from SDC. + + Args: + details ([type]): [description] + + """ + def update_informations_from_sdc_creation(self, + details: Dict[str, Any]) -> None: + """ + + Update instance with details from SDC after creation. + + Args: + details ([type]): the details from SDC + + """ + self.unique_uuid = details['invariantUUID'] + distribution_state = None + + if 'distributionStatus' in details: + distribution_state = details['distributionStatus'] + self.status = self._parse_sdc_status(details['lifecycleState'], + distribution_state, self._logger) + self.version = details['version'] + self.unique_identifier = details['uniqueId'] + + # Not my fault if SDC has so many states... + # pylint: disable=too-many-return-statements + @staticmethod + def _parse_sdc_status(sdc_status: str, distribution_state: str, + logger: logging.Logger) -> str: + """ + Parse SDC status in order to normalize it. + + Args: + sdc_status (str): the status found in SDC + distribution_state (str): the distribution status found in SDC. + Can be None. + + Returns: + str: the normalized status + + """ + logger.debug("Parse status for SDC Resource") + if sdc_status.capitalize() == const.CERTIFIED: + if distribution_state and distribution_state == const.SDC_DISTRIBUTED: + return const.DISTRIBUTED + return const.CERTIFIED + if sdc_status == const.NOT_CERTIFIED_CHECKOUT: + return const.DRAFT + if sdc_status == const.NOT_CERTIFIED_CHECKIN: + return const.CHECKED_IN + if sdc_status == const.READY_FOR_CERTIFICATION: + return const.SUBMITTED + if sdc_status == const.CERTIFICATION_IN_PROGRESS: + return const.UNDER_CERTIFICATION + if sdc_status != "": + return sdc_status + return None + + def _really_submit(self) -> None: + """Really submit the SDC Vf in order to enable it.""" + raise NotImplementedError("SDC is an abstract class") + + def onboard(self) -> None: + """Onboard resource in SDC.""" + if not self.status: + self.create() + time.sleep(self._time_wait) + self.onboard() + elif self.status == const.DRAFT: + for property_to_add in self._properties_to_add: + self.add_property(property_to_add) + for input_to_add in self._inputs_to_add: + self.declare_input(input_to_add) + self.submit() + time.sleep(self._time_wait) + self.onboard() + elif self.status == const.CHECKED_IN: + # Checked in status check added + self.certify() + time.sleep(self._time_wait) + self.onboard() + elif self.status == const.CERTIFIED: + self.load() + + @classmethod + def _sdc_path(cls) -> None: + """Give back the end of SDC path.""" + return cls.RESOURCE_PATH + + @property + def deployment_artifacts_url(self) -> str: + """Deployment artifacts url. + + Returns: + str: SdcResource Deployment artifacts url + + """ + return (f"{self._base_create_url()}/resources/" + f"{self.unique_identifier}/filteredDataByParams?include=deploymentArtifacts") + + @property + def add_deployment_artifacts_url(self) -> str: + """Add deployment artifacts url. + + Returns: + str: Url used to add deployment artifacts + + """ + return (f"{self._base_create_url()}/resources/" + f"{self.unique_identifier}/artifacts") + + @property + def properties_url(self) -> str: + """Properties url. + + Returns: + str: SdcResource properties url + + """ + return (f"{self._base_create_url()}/resources/" + f"{self.unique_identifier}/filteredDataByParams?include=properties") + + @property + def add_property_url(self) -> str: + """Add property url. + + Returns: + str: Url used to add property + + """ + return (f"{self._base_create_url()}/services/" + f"{self.unique_identifier}/properties") + + @property + def set_input_default_value_url(self) -> str: + """Url to set input default value. + + Returns: + str: SDC API url used to set input default value + + """ + return (f"{self._base_create_url()}/resources/" + f"{self.unique_identifier}/update/inputs") + + @property + def origin_type(self) -> str: + """Resource origin type. + + Value needed for composition. It's used for adding SDC resource + as an another SDC resource component. + + Returns: + str: SDC resource origin type + + """ + return type(self).__name__.upper() + + @property + def properties(self) -> Iterator[Property]: + """SDC resource properties. + + Iterate resource properties. + + Yields: + Property: Resource property + + """ + for property_data in self.send_message_json(\ + "GET", + f"Get {self.name} resource properties", + self.properties_url).get("properties", []): + yield Property( + sdc_resource=self, + unique_id=property_data["uniqueId"], + name=property_data["name"], + property_type=property_data["type"], + parent_unique_id=property_data["parentUniqueId"], + value=property_data.get("value"), + description=property_data.get("description"), + get_input_values=property_data.get("getInputValues"), + ) + + def get_property(self, property_name: str) -> Property: + """Get resource property by it's name. + + Args: + property_name (str): property name + + Raises: + ResourceNotFound: Resource has no property with given name + + Returns: + Property: Resource's property object + + """ + for property_obj in self.properties: + if property_obj.name == property_name: + return property_obj + + msg = f"Resource has no property with {property_name} name" + raise ResourceNotFound(msg) + + @property + def resource_inputs_url(self) -> str: + """Resource inputs url. + + Method which returns url which point to resource inputs. + + Returns: + str: Resource inputs url + + """ + return (f"{self._base_create_url()}/resources/" + f"{self.unique_identifier}") + + + def create(self) -> None: + """Create resource. + + Abstract method which should be implemented by subclasses and creates resource in SDC. + + Raises: + NotImplementedError: Method not implemented by subclasses. + + """ + raise NotImplementedError + + @property + def inputs(self) -> Iterator[Input]: + """SDC resource inputs. + + Iterate resource inputs. + + Yields: + Iterator[Input]: Resource input + + """ + url = f"{self.resource_inputs_url}/filteredDataByParams?include=inputs" + for input_data in self.send_message_json(\ + "GET", f"Get {self.name} resource inputs", + url).get("inputs", []): + + yield Input( + unique_id=input_data["uniqueId"], + input_type=input_data["type"], + name=input_data["name"], + sdc_resource=self, + _default_value=input_data.get("defaultValue") + ) + + def get_input(self, input_name: str) -> Input: + """Get input by it's name. + + Args: + input_name (str): Input name + + Raises: + ResourceNotFound: Resource doesn't have input with given name + + Returns: + Input: Found input object + + """ + for input_obj in self.inputs: + if input_obj.name == input_name: + return input_obj + raise ResourceNotFound(f"SDC resource has no {input_name} input") + + def add_deployment_artifact(self, artifact_type: str, artifact_label: str, + artifact_name: str, artifact: str): + """ + Add deployment artifact to resource. + + Add deployment artifact to resource using payload data. + + Args: + artifact_type (str): all SDC artifact types are supported (DCAE_*, HEAT_*, ...) + artifact_name (str): the artifact file name including its extension + artifact (str): artifact file to upload + artifact_label (str): Unique Identifier of the artifact within the VF / Service. + + Raises: + StatusError: Resource has not DRAFT status + + """ + data = open(artifact, 'rb').read() + artifact_string = base64.b64encode(data).decode('utf-8') + if self.status != const.DRAFT: + msg = "Can't add artifact to resource which is not in DRAFT status" + raise StatusError(msg) + self._logger.debug("Add deployment artifact to sdc resource") + my_data = jinja_env().get_template( + "sdc_resource_add_deployment_artifact.json.j2").\ + render(artifact_name=artifact_name, + artifact_label=artifact_label, + artifact_type=artifact_type, + b64_artifact=artifact_string) + my_header = headers_sdc_artifact_upload(base_header=self.headers, data=my_data) + + self.send_message_json("POST", + f"Add deployment artifact for {self.name} sdc resource", + self.add_deployment_artifacts_url, + data=my_data, + headers=my_header) + + @property + def components(self) -> Iterator[Component]: + """Resource components. + + Iterate resource components. + + Yields: + Component: Resource component object + + """ + for component_instance in self.send_message_json(\ + "GET", + f"Get {self.name} resource inputs", + f"{self.resource_inputs_url}/filteredDataByParams?include=componentInstances" + ).get("componentInstances", []): + sdc_resource: "SdcResource" = SdcResource.import_from_sdc(self.send_message_json(\ + "GET", + f"Get {self.name} component's SDC resource metadata", + (f"{self.base_front_url}/sdc1/feProxy/rest/v1/catalog/resources/" + f"{component_instance['actualComponentUid']}/" + "filteredDataByParams?include=metadata"))["metadata"]) + yield Component.create_from_api_response(api_response=component_instance, + sdc_resource=sdc_resource, + parent_sdc_resource=self) + + @property + def category(self) -> Union[ResourceCategory, ServiceCategory]: + """Sdc resource category. + + Depends on the resource type returns ResourceCategory or ServiceCategory. + + Returns: + Uniton[ResourceCategory, ServiceCategory]: resource category + + """ + if self.created(): + if not any([self._category_name, self._subcategory_name]): + self.deep_load() + if all([self._category_name, self._subcategory_name]): + return ResourceCategory.get(name=self._category_name, + subcategory=self._subcategory_name) + return ServiceCategory.get(name=self._category_name) + return self.get_category_for_new_resource() + + def get_category_for_new_resource(self) -> ResourceCategory: + """Get category for resource not created in SDC yet. + + If no category values are provided default category is going to be used. + + Returns: + ResourceCategory: Category of the new resource + + """ + if not all([self._category_name, self._subcategory_name]): + return ResourceCategory.get(name="Generic", subcategory="Abstract") + return ResourceCategory.get(name=self._category_name, subcategory=self._subcategory_name) + + def get_component_properties_url(self, component: "Component") -> str: + """Url to get component's properties. + + This method is here because component can have different url when + it's a component of another SDC resource type, eg. for service and + for VF components have different urls. + + Args: + component (Component): Component object to prepare url for + + Returns: + str: Component's properties url + + """ + return (f"{self.resource_inputs_url}/" + f"componentInstances/{component.unique_id}/properties") + + def get_component_properties_value_set_url(self, component: "Component") -> str: + """Url to set component property value. + + This method is here because component can have different url when + it's a component of another SDC resource type, eg. for service and + for VF components have different urls. + + Args: + component (Component): Component object to prepare url for + + Returns: + str: Component's properties url + + """ + return (f"{self.resource_inputs_url}/" + f"resourceInstance/{component.unique_id}/properties") + + def is_own_property(self, property_to_check: Property) -> bool: + """Check if given property is one of the resource's properties. + + Args: + property_to_check (Property): Property to check + + Returns: + bool: True if resource has given property, False otherwise + + """ + return any(( + prop == property_to_check for prop in self.properties + )) + + def get_component(self, sdc_resource: "SdcResource") -> Component: + """Get resource's component. + + Get component by SdcResource object. + + Args: + sdc_resource (SdcResource): Component's SdcResource + + Raises: + ResourceNotFound: Component with given SdcResource does not exist + + Returns: + Component: Component object + + """ + for component in self.components: + if component.sdc_resource.name == sdc_resource.name: + return component + msg = f"SDC resource {sdc_resource.name} is not a component" + raise ResourceNotFound(msg) + + def get_component_by_name(self, component_name: str) -> Component: + """Get resource's component by it's name. + + Get component by name. + + Args: + component_name (str): Component's name + + Raises: + ResourceNotFound: Component with given name does not exist + + Returns: + Component: Component object + + """ + for component in self.components: + if component.sdc_resource.name == component_name: + return component + msg = f"SDC resource {component_name} is not a component" + raise ResourceNotFound(msg) + + def declare_input_for_own_property(self, property_obj: Property) -> None: + """Declare input for resource's property. + + For each property input can be declared. + + Args: + property_obj (Property): Property to declare input + + """ + self._logger.debug("Declare input for SDC resource property") + self.send_message_json("POST", + f"Declare new input for {property_obj.name} property", + f"{self.resource_inputs_url}/create/inputs", + data=jinja_env().get_template(\ + "sdc_resource_add_input.json.j2").\ + render(\ + sdc_resource=self, + property=property_obj)) + + def declare_nested_input(self, + nested_input: NestedInput) -> None: + """Declare nested input for SDC resource. + + Nested input is an input of one of the components. + + Args: + nested_input (NestedInput): Nested input object + + """ + self._logger.debug("Declare input for SDC resource's component property") + component: Component = self.get_component(nested_input.sdc_resource) + self.send_message_json("POST", + f"Declare new input for {nested_input.input_obj.name} input", + f"{self.resource_inputs_url}/create/inputs", + data=jinja_env().get_template(\ + "sdc_resource_add_nested_input.json.j2").\ + render(\ + sdc_resource=self, + component=component, + input=nested_input.input_obj)) + + def declare_input(self, input_to_declare: Union[Property, NestedInput]) -> None: + """Declare input for given property or nested input object. + + Call SDC FE API to declare input for given property. + + Args: + input_declaration (Union[Property, NestedInput]): Property to declare input + or NestedInput object + + Raises: + ParameterError: if the given property is not SDC resource property + + """ + self._logger.debug("Declare input") + if isinstance(input_to_declare, Property): + if self.is_own_property(input_to_declare): + self.declare_input_for_own_property(input_to_declare) + else: + msg = "Given property is not SDC resource property" + raise ParameterError(msg) + else: + self.declare_nested_input(input_to_declare) + + def add_property(self, property_to_add: Property) -> None: + """Add property to resource. + + Call SDC FE API to add property to resource. + + Args: + property_to_add (Property): Property object to add to resource. + + Raises: + StatusError: Resource has not DRAFT status + + """ + if self.status != const.DRAFT: + msg = "Can't add property to resource which is not in DRAFT status" + raise StatusError(msg) + self._logger.debug("Add property to sdc resource") + self.send_message_json("POST", + f"Declare new property for {self.name} sdc resource", + self.add_property_url, + data=jinja_env().get_template( + "sdc_resource_add_property.json.j2").\ + render( + property=property_to_add + )) + + def set_property_value(self, property_obj: Property, value: Any) -> None: + """Set property value. + + Set given value to resource property + + Args: + property_obj (Property): Property object + value (Any): Property value to set + + Raises: + ParameterError: if the given property is not the resource's property + + """ + if not self.is_own_property(property_obj): + raise ParameterError("Given property is not a resource's property") + self._logger.debug("Set %s property value", property_obj.name) + self.send_message_json("PUT", + f"Set {property_obj.name} value to {value}", + self.add_property_url, + data=jinja_env().get_template( + "sdc_resource_set_property_value.json.j2").\ + render( + sdc_resource=self, + property=property_obj, + value=value + ) + ) + + def set_input_default_value(self, input_obj: Input, default_value: Any) -> None: + """Set input default value. + + Set given value as input default value + + Args: + input_obj (Input): Input object + value (Any): Default value to set + + """ + self._logger.debug("Set %s input default value", input_obj.name) + self.send_message_json("POST", + f"Set {input_obj.name} default value to {default_value}", + self.set_input_default_value_url, + data=jinja_env().get_template( + "sdc_resource_set_input_default_value.json.j2").\ + render( + sdc_resource=self, + input=input_obj, + default_value=default_value + ) + ) + + def checkout(self) -> None: + """Checkout SDC resource.""" + self._logger.debug("Checkout %s SDC resource", self.name) + result = self._action_to_sdc(const.CHECKOUT, "lifecycleState") + if result: + self.load() + + def undo_checkout(self) -> None: + """Undo Checkout SDC resource.""" + self._logger.debug("Undo Checkout %s SDC resource", self.name) + result = self._action_to_sdc(const.UNDOCHECKOUT, "lifecycleState") + if result: + self.load() + + def certify(self) -> None: + """Certify SDC resource.""" + self._logger.debug("Certify %s SDC resource", self.name) + result = self._action_to_sdc(const.CERTIFY, "lifecycleState") + if result: + self.load() + + def add_resource(self, resource: 'SdcResource') -> None: + """ + Add a Resource. + + Args: + resource (SdcResource): the resource to add + + """ + if self.status == const.DRAFT: + url = "{}/{}/{}/resourceInstance".format(self._base_create_url(), + self._sdc_path(), + self.unique_identifier) + + template = jinja_env().get_template( + "add_resource_to_service.json.j2") + data = template.render(resource=resource, + resource_type=resource.origin_type) + result = self.send_message("POST", + f"Add {resource.origin_type} to {self.origin_type}", + url, + data=data) + if result: + self._logger.info("Resource %s %s has been added on %s %s", + resource.origin_type, resource.name, + self.origin_type, self.name) + return result + self._logger.error(("an error occured during adding resource %s %s" + " on %s %s in SDC"), + resource.origin_type, resource.name, + self.origin_type, self.name) + return None + msg = f"Can't add resource to {self.origin_type} which is not in DRAFT status" + raise StatusError(msg) diff --git a/src/onapsdk/sdc/service.py b/src/onapsdk/sdc/service.py new file mode 100644 index 0000000..c866fa4 --- /dev/null +++ b/src/onapsdk/sdc/service.py @@ -0,0 +1,932 @@ +"""Service module.""" +# Copyright 2022 Orange, 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 base64 +import pathlib as Path +import time +from dataclasses import dataclass, field +from enum import Enum +from io import BytesIO, TextIOWrapper +from os import makedirs +from typing import Dict, List, Callable, Iterator, Optional, Type, Union, Any, BinaryIO +from zipfile import ZipFile, BadZipFile + +import oyaml as yaml +from requests import Response + +import onapsdk.constants as const +from onapsdk.exceptions import (ParameterError, RequestError, ResourceNotFound, + StatusError, ValidationError) +from onapsdk.sdc.category_management import ServiceCategory +from onapsdk.sdc.properties import NestedInput, Property +from onapsdk.sdc.sdc_resource import SdcResource +from onapsdk.utils.configuration import (components_needing_distribution, + tosca_path) +from onapsdk.utils.headers_creator import headers_sdc_creator, headers_sdc_artifact_upload +from onapsdk.utils.jinja import jinja_env + + +@dataclass +class VfModule: # pylint: disable=too-many-instance-attributes + """VfModule dataclass.""" + + name: str + group_type: str + model_name: str + model_version_id: str + model_invariant_uuid: str + model_version: str + model_customization_id: str + properties: Iterator[Property] + + +@dataclass +class NodeTemplate: # pylint: disable=too-many-instance-attributes + """Node template dataclass. + + Base class for Vnf, Pnf and Network classes. + """ + + name: str + node_template_type: str + model_name: str + model_version_id: str + model_invariant_id: str + model_version: str + model_customization_id: str + model_instance_name: str + component: "Component" + + @property + def properties(self) -> Iterator["Property"]: + """Node template properties. + + Returns: + Iterator[Property]: Node template properties iterator + + """ + return self.component.properties + + +@dataclass +class Vnf(NodeTemplate): + """Vnf dataclass.""" + + vf_modules: List[VfModule] = field(default_factory=list) + + +@dataclass +class Pnf(NodeTemplate): + """Pnf dataclass.""" + + +class Network(NodeTemplate): # pylint: disable=too-few-public-methods + """Network dataclass.""" + + +class ServiceInstantiationType(Enum): + """Service instantiation type enum class. + + Service can be instantiated using `A-la-carte` or `Macro` flow. + It has to be determined during design time. That class stores these + two values to set during initialization. + + """ + + A_LA_CARTE = "A-la-carte" + MACRO = "Macro" + + +class Service(SdcResource): # pylint: disable=too-many-instance-attributes, too-many-public-methods + """ + ONAP Service Object used for SDC operations. + + Attributes: + name (str): the name of the service. Defaults to "ONAP-test-Service". + identifier (str): the unique ID of the service from SDC. + status (str): the status of the service from SDC. + version (str): the version ID of the service from SDC. + uuid (str): the UUID of the Service (which is different from + identifier, don't ask why...) + distribution_status (str): the status of distribution in the different + ONAP parts. + distribution_id (str): the ID of the distribution when service is + distributed. + distributed (bool): True if the service is distributed + unique_identifier (str): Yet Another ID, just to puzzle us... + + """ + + SERVICE_PATH = "services" + + def __init__(self, name: str = None, version: str = None, sdc_values: Dict[str, str] = None, # pylint: disable=too-many-arguments + resources: List[SdcResource] = None, properties: List[Property] = None, + inputs: List[Union[Property, NestedInput]] = None, + instantiation_type: Optional[ServiceInstantiationType] = \ + None, + category: str = None, role: str = "", function: str = "", service_type: str = ""): + """ + Initialize service object. + + Args: + name (str, optional): the name of the service + version (str, optional): the version of the service + sdc_values (Dict[str, str], optional): dictionary of values + returned by SDC + resources (List[SdcResource], optional): list of SDC resources + properties (List[Property], optional): list of properties to add to service. + None by default. + inputs (List[Union[Property, NestedInput]], optional): list of inputs + to declare for service. It can be both Property or NestedInput object. + None by default. + instantiation_type (ServiceInstantiationType, optional): service instantiation + type. ServiceInstantiationType.A_LA_CARTE by default + category (str, optional): service category name + role (str, optional): service role + function (str, optional): service function. Empty by default + service_type (str, optional): service type. Empty by default + + """ + super().__init__(sdc_values=sdc_values, version=version, properties=properties, + inputs=inputs, category=category) + self.name: str = name or "ONAP-test-Service" + self.distribution_status = None + self.category_name: str = category + self.role: str = role + self.function: str = function + self.service_type: str = service_type + if sdc_values: + self.distribution_status = sdc_values['distributionStatus'] + self.category_name = sdc_values["category"] + self.resources = resources or [] + self._instantiation_type: Optional[ServiceInstantiationType] = instantiation_type + self._distribution_id: str = None + self._distributed: bool = False + self._resource_type: str = "services" + self._tosca_model: bytes = None + self._tosca_template: str = None + self._vnfs: list = None + self._pnfs: list = None + self._networks: list = None + self._vf_modules: list = None + + @classmethod + def get_by_unique_uuid(cls, unique_uuid: str) -> "Service": + """Get the service model using unique uuid. + + Returns: + Service: object with provided unique_uuid + + Raises: + ResourceNotFound: No service with given unique_uuid exists + + """ + services: List["Service"] = cls.get_all() + for service in services: + if service.unique_uuid == unique_uuid: + return service + raise ResourceNotFound("Service with given unique uuid doesn't exist") + + def onboard(self) -> None: + """Onboard the Service in SDC. + + Raises: + StatusError: service has an invalid status + ParameterError: no resources, no properties for service + in DRAFT status + + """ + # first Lines are equivalent for all onboard functions but it's more + # readable + if not self.status: + # equivalent step as in onboard-function in sdc_resource + self.create() + time.sleep(self._time_wait) + self.onboard() + elif self.status == const.DRAFT: + if not any([self.resources, self._properties_to_add]): + raise ParameterError("No resources nor properties were given") + self.declare_resources_and_properties() + self.checkin() + time.sleep(self._time_wait) + self.onboard() + elif self.status == const.CHECKED_IN: + self.certify() + time.sleep(self._time_wait) + self.onboard() + elif self.status == const.CERTIFIED: + self.distribute() + self.onboard() + elif self.status == const.DISTRIBUTED: + self._logger.info("Service %s onboarded", self.name) + else: + self._logger.error("Service has invalid status: %s", self.status) + raise StatusError(self.status) + + @property + def distribution_id(self) -> str: + """Return and lazy load the distribution_id.""" + if not self._distribution_id: + self.load_metadata() + return self._distribution_id + + @distribution_id.setter + def distribution_id(self, value: str) -> None: + """Set value for distribution_id.""" + self._distribution_id = value + + @property + def distributed(self) -> bool: + """Return and lazy load the distributed state.""" + if not self._distributed: + self._check_distributed() + return self._distributed + + @property + def tosca_template(self) -> str: + """Service tosca template file. + + Get tosca template from service tosca model bytes. + + Returns: + str: Tosca template file + + """ + if not self._tosca_template and self.tosca_model: + self._unzip_csar_file(BytesIO(self.tosca_model), + self._load_tosca_template) + return self._tosca_template + + @property + def tosca_model(self) -> bytes: + """Service's tosca model file. + + Send request to get service TOSCA model, + + Returns: + bytes: TOSCA model file bytes + + """ + if not self._tosca_model: + url = "{}/services/{}/toscaModel".format(self._base_url(), + self.identifier) + headers = self.headers.copy() + headers["Accept"] = "application/octet-stream" + self._tosca_model = self.send_message( + "GET", + "Download Tosca Model for {}".format(self.name), + url, + headers=headers).content + return self._tosca_model + + def create_node_template(self, + node_template_type: Type[NodeTemplate], + component: "Component") -> NodeTemplate: + """Create a node template type object. + + The base of the all node template types objects (Vnf, Pnf, Network) is the + same. The difference is only for the Vnf which can have vf modules associated with. + Vf modules could have "vf_module_label" property with"base_template_dummy_ignore" + value. These vf modules should be ignored/ + + Args: + node_template_type (Type[NodeTemplate]): Node template class type + component (Component): Component on which base node template object should be created + + Returns: + NodeTemplate: Node template object created from component + + """ + node_template: NodeTemplate = node_template_type( + name=component.name, + node_template_type=component.tosca_component_name, + model_name=component.component_name, + model_version_id=component.sdc_resource.identifier, + model_invariant_id=component.sdc_resource.unique_uuid, + model_version=component.sdc_resource.version, + model_customization_id=component.customization_uuid, + model_instance_name=self.name, + component=component + ) + if node_template_type is Vnf: + if component.group_instances: + for vf_module in component.group_instances: + if not any([property_def["name"] == "vf_module_label"] and \ + property_def["value"] == "base_template_dummy_ignore" for \ + property_def in vf_module["properties"]): + node_template.vf_modules.append(VfModule( + name=vf_module["name"], + group_type=vf_module["type"], + model_name=vf_module["groupName"], + model_version_id=vf_module["groupUUID"], + model_invariant_uuid=vf_module["invariantUUID"], + model_version=vf_module["version"], + model_customization_id=vf_module["customizationUUID"], + properties=( + Property( + name=property_def["name"], + property_type=property_def["type"], + description=property_def["description"], + value=property_def["value"] + ) for property_def in vf_module["properties"] \ + if property_def["value"] and not ( + property_def["name"] == "vf_module_label" and \ + property_def["value"] == "base_template_dummy_ignore" + ) + ) + )) + return node_template + + def __has_component_type(self, origin_type: str) -> bool: + """Check if any of Service's component type is provided origin type. + + In template generation is checked if Service has some types of components, + based on that blocks are added to the request template. It's not + the best option to get all components to check if at least one with + given type exists for conditional statement. + + Args: + origin_type (str): Type to check if any component exists. + + Returns: + bool: True if service has at least one component with given origin type, + False otherwise + + """ + return any((component.origin_type == origin_type for component in self.components)) + + @property + def has_vnfs(self) -> bool: + """Check if service has at least one VF component.""" + return self.__has_component_type("VF") + + @property + def has_pnfs(self) -> bool: + """Check if service has at least one PNF component.""" + return self.__has_component_type("PNF") + + @property + def has_vls(self) -> bool: + """Check if service has at least one VL component.""" + return self.__has_component_type("VL") + + @property + def vnfs(self) -> Iterator[Vnf]: + """Service Vnfs. + + Load VNFs from components generator. + It creates a generator of the vf modules as well, but without + vf modules which has "vf_module_label" property value equal + to "base_template_dummy_ignore". + + Returns: + Iterator[Vnf]: Vnf objects iterator + + """ + for component in self.components: + if component.origin_type == "VF": + yield self.create_node_template(Vnf, component) + + @property + def pnfs(self) -> Iterator[Pnf]: + """Service Pnfs. + + Load PNFS from components generator. + + Returns: + Iterator[Pnf]: Pnf objects generator + + """ + for component in self.components: + if component.origin_type == "PNF": + yield self.create_node_template(Pnf, component) + + @property + def networks(self) -> Iterator[Network]: + """Service networks. + + Load networks from service's components generator. + + Returns: + Iterator[Network]: Network objects generator + + """ + for component in self.components: + if component.origin_type == "VL": + yield self.create_node_template(Network, component) + + @property + def deployment_artifacts_url(self) -> str: + """Deployment artifacts url. + + Returns: + str: SdcResource Deployment artifacts url + + """ + return (f"{self._base_create_url()}/services/" + f"{self.unique_identifier}/filteredDataByParams?include=deploymentArtifacts") + + @property + def add_deployment_artifacts_url(self) -> str: + """Add deployment artifacts url. + + Returns: + str: Url used to add deployment artifacts + + """ + return (f"{self._base_create_url()}/services/" + f"{self.unique_identifier}/artifacts") + + @property + def properties_url(self) -> str: + """Properties url. + + Returns: + str: SdcResource properties url + + """ + return (f"{self._base_create_url()}/services/" + f"{self.unique_identifier}/filteredDataByParams?include=properties") + + @property + def metadata_url(self) -> str: + """Metadata url. + + Returns: + str: Service metadata url + + """ + return (f"{self._base_create_url()}/services/" + f"{self.unique_identifier}/filteredDataByParams?include=metadata") + + @property + def resource_inputs_url(self) -> str: + """Service inputs url. + + Returns: + str: Service inputs url + + """ + return (f"{self._base_create_url()}/services/" + f"{self.unique_identifier}") + + @property + def set_input_default_value_url(self) -> str: + """Url to set input default value. + + Returns: + str: SDC API url used to set input default value + + """ + return (f"{self._base_create_url()}/services/" + f"{self.unique_identifier}/update/inputs") + + @property + def origin_type(self) -> str: + """Service origin type. + + Value needed for composition. It's used for adding SDC resource + as an another SDC resource component. + For Service that value has to be set to "ServiceProxy". + + Returns: + str: Service resource origin type + + """ + return "ServiceProxy" + + @property + def instantiation_type(self) -> ServiceInstantiationType: + """Service instantiation type. + + One of `ServiceInstantiationType` enum value. + + Returns: + ServiceInstantiationType: Service instantiation type + + """ + if not self._instantiation_type: + if not self.created(): + self._instantiation_type = ServiceInstantiationType.A_LA_CARTE + else: + response: str = self.send_message_json("GET", + f"Get service {self.name} metadata", + self.metadata_url)["metadata"]\ + ["instantiationType"] + self._instantiation_type = ServiceInstantiationType(response) + return self._instantiation_type + + def create(self) -> None: + """Create the Service in SDC if not already existing.""" + self._create("service_create.json.j2", + name=self.name, + instantiation_type=self.instantiation_type.value, + category=self.category, + role=self.role, service_type=self.service_type, function=self.function) + + def declare_resources_and_properties(self) -> None: + """Delcare resources and properties. + + It declares also inputs. + + """ + for resource in self.resources: + self.add_resource(resource) + for property_to_add in self._properties_to_add: + self.add_property(property_to_add) + for input_to_add in self._inputs_to_add: + self.declare_input(input_to_add) + + def checkin(self) -> None: + """Checkin Service.""" + self._verify_lcm_to_sdc(const.DRAFT, const.CHECKIN) + + def submit(self) -> None: + """Really submit the SDC Service.""" + self._verify_lcm_to_sdc(const.CHECKED_IN, const.SUBMIT_FOR_TESTING) + + def start_certification(self) -> None: + """Start Certification on Service.""" + headers = headers_sdc_creator(SdcResource.headers) + self._verify_lcm_to_sdc(const.CHECKED_IN, + const.START_CERTIFICATION, + headers=headers) + + def certify(self) -> None: + """Certify Service in SDC.""" + headers = headers_sdc_creator(SdcResource.headers) + self._verify_lcm_to_sdc(const.CHECKED_IN, + const.CERTIFY, + headers=headers) + + def approve(self) -> None: + """Approve Service in SDC.""" + headers = headers_sdc_creator(SdcResource.headers) + self._verify_approve_to_sdc(const.CERTIFIED, + const.APPROVE, + headers=headers) + + def distribute(self) -> None: + """Apptove Service in SDC.""" + headers = headers_sdc_creator(SdcResource.headers) + self._verify_distribute_to_sdc(const.CERTIFIED, + const.DISTRIBUTE, + headers=headers) + + def redistribute(self) -> None: + """Apptove Service in SDC.""" + headers = headers_sdc_creator(SdcResource.headers) + self._verify_distribute_to_sdc(const.DISTRIBUTED, + const.DISTRIBUTE, + headers=headers) + + def get_tosca(self) -> None: + """Get Service tosca files and save it.""" + url = "{}/services/{}/toscaModel".format(self._base_url(), + self.identifier) + headers = self.headers.copy() + headers["Accept"] = "application/octet-stream" + result = self.send_message("GET", + "Download Tosca Model for {}".format( + self.name), + url, + headers=headers) + if result: + self._create_tosca_file(result) + + def _create_tosca_file(self, result: Response) -> None: + """Create Service Tosca files from HTTP response.""" + csar_filename = "service-{}-csar.csar".format(self.name) + makedirs(tosca_path(), exist_ok=True) + with open((tosca_path() + csar_filename), 'wb') as csar_file: + for chunk in result.iter_content(chunk_size=128): + csar_file.write(chunk) + try: + self._unzip_csar_file(tosca_path() + csar_filename, + self._write_csar_file) + except BadZipFile as exc: + self._logger.exception(exc) + + def _check_distributed(self) -> bool: + """Check if service is distributed and update status accordingly.""" + url = "{}/services/distribution/{}".format(self._base_create_url(), + self.distribution_id) + headers = headers_sdc_creator(SdcResource.headers) + + status = {} + for component in components_needing_distribution(): + status[component] = False + + try: + result = self.send_message_json("GET", + "Check distribution for {}".format( + self.name), + url, + headers=headers) + except ResourceNotFound: + msg = f"No distributions found for {self.name} of {self.__class__.__name__}." + self._logger.debug(msg) + else: + status = self._update_components_status(status, result) + + for state in status.values(): + if not state: + self._distributed = False + return + self._distributed = True + + def _update_components_status(self, status: Dict[str, bool], + result: Response) -> Dict[str, bool]: + """Update components distribution status.""" + distrib_list = result['distributionStatusList'] + self._logger.debug("[SDC][Get Distribution] distrib_list = %s", + distrib_list) + for elt in distrib_list: + status = self._parse_components_status(status, elt) + return status + + def _parse_components_status(self, status: Dict[str, bool], + element: Dict[str, Any]) -> Dict[str, bool]: + """Parse components distribution status.""" + for key in status: + if ((key in element['omfComponentID']) + and (const.DOWNLOAD_OK in element['status'])): + status[key] = True + self._logger.info(("[SDC][Get Distribution] Service " + "distributed in %s"), key) + return status + + def load_metadata(self) -> None: + """Load Metada of Service and retrieve informations.""" + url = "{}/services/{}/distribution".format(self._base_create_url(), + self.identifier) + headers = headers_sdc_creator(SdcResource.headers) + result = self.send_message_json("GET", + "Get Metadata for {}".format( + self.name), + url, + headers=headers) + if ('distributionStatusOfServiceList' in result + and len(result['distributionStatusOfServiceList']) > 0): + # API changed and the latest distribution is not added to the end + # of distributions list but inserted as the first one. + dist_status = result['distributionStatusOfServiceList'][0] + self._distribution_id = dist_status['distributionID'] + + @classmethod + def _get_all_url(cls) -> str: + """ + Get URL for all elements in SDC. + + Returns: + str: the url + + """ + return "{}/{}".format(cls._base_url(), cls._sdc_path()) + + def _really_submit(self) -> None: + """Really submit the SDC Service in order to enable it.""" + result = self._action_to_sdc(const.CERTIFY, + action_type="lifecycleState") + if result: + self.load() + + def _specific_copy(self, obj: 'Service') -> None: + """ + Copy specific properties from object. + + Args: + obj (Service): the object to "copy" + + """ + super()._specific_copy(obj) + self.category_name = obj.category_name + self.role = obj.role + + def _verify_distribute_to_sdc(self, desired_status: str, + desired_action: str, **kwargs) -> None: + self._verify_action_to_sdc(desired_status, desired_action, + "distribution", **kwargs) + + def _verify_approve_to_sdc(self, desired_status: str, desired_action: str, + **kwargs) -> None: + self._verify_action_to_sdc(desired_status, desired_action, + "distribution-state", **kwargs) + + def _verify_lcm_to_sdc(self, desired_status: str, desired_action: str, + **kwargs) -> None: + self._verify_action_to_sdc(desired_status, desired_action, + "lifecycleState", **kwargs) + + def _verify_action_to_sdc(self, desired_status: str, desired_action: str, + action_type: str, **kwargs) -> None: + """ + Verify action to SDC. + + Verify that object is in right state before launching the action on + SDC. + + Raises: + StatusError: if current status is not the desired status. + + Args: + desired_status (str): the status the object should be + desired_action (str): the action we want to perform + action_type (str): the type of action ('distribution-state' or + 'lifecycleState') + **kwargs: any specific stuff to give to requests + + """ + self._logger.info("attempting to %s Service %s in SDC", desired_action, + self.name) + if self.status == desired_status and self.created(): + self._action_to_sdc(desired_action, + action_type=action_type, + **kwargs) + self.load() + elif not self.created(): + self._logger.warning("Service %s in SDC is not created", self.name) + elif self.status != desired_status: + msg = (f"Service {self.name} in SDC is in status {self.status} " + f"and it should be in status {desired_status}") + raise StatusError(msg) + + @staticmethod + def _unzip_csar_file(zip_file: Union[str, BytesIO], + function: Callable[[str, + TextIOWrapper], None]) -> None: + """ + Unzip Csar File and perform an action on the file. + + Raises: + ValidationError: CSAR file has no service template + + """ + folder = "Definitions" + prefix = "service-" + suffix = "-template.yml" + with ZipFile(zip_file) as myzip: + service_template = None + for name in myzip.namelist(): + if (name[-13:] == suffix + and name[:20] == f"{folder}/{prefix}"): + service_template = name + + if not service_template: + msg = (f"CSAR file has no service template. " + f"Valid path: {folder}/{prefix}*{suffix}") + raise ValidationError(msg) + + with myzip.open(service_template) as template_file: + function(service_template, template_file) + + @staticmethod + def _write_csar_file(service_template: str, + template_file: TextIOWrapper) -> None: + """Write service temple into a file.""" + with open(tosca_path() + service_template[12:], 'wb') as file: + file.write(template_file.read()) + + # _service_template is not used but function generation is generic + # pylint: disable-unused-argument + def _load_tosca_template(self, _service_template: str, + template_file: TextIOWrapper) -> None: + """Load Tosca template.""" + self._tosca_template = yaml.safe_load(template_file.read()) + + @classmethod + def _sdc_path(cls) -> None: + """Give back the end of SDC path.""" + return cls.SERVICE_PATH + + def get_nf_unique_id(self, nf_name: str) -> str: + """ + Get nf (network function) uniqueID. + + Get nf uniqueID from service nf in sdc. + + Args: + nf_name (str): the nf from which we extract the unique ID + + Returns: + the nf unique ID + + Raises: + ResourceNotFound: Couldn't find NF by name. + + """ + url = f"{self._base_create_url()}/services/{self.unique_identifier}" + request_return = self.send_message_json('GET', + 'Get nf unique ID', + url) + + for instance in filter(lambda x: x["componentName"] == nf_name, + request_return["componentInstances"]): + return instance["uniqueId"] + + raise ResourceNotFound(f"NF '{nf_name}'") + + + def add_artifact_to_vf(self, vnf_name: str, artifact_type: str, + artifact_name: str, artifact: BinaryIO = None): + """ + Add artifact to vf. + + Add artifact to vf using payload data. + + Raises: + RequestError: file upload (POST request) for an artifact fails. + + Args: + vnf_name (str): the vnf which we want to add the artifact + artifact_type (str): all SDC artifact types are supported (DCAE_*, HEAT_*, ...) + artifact_name (str): the artifact file name including its extension + artifact (str): binary data to upload + + """ + missing_identifier = self.get_nf_unique_id(vnf_name) + url = (f"{self._base_create_url()}/services/{self.unique_identifier}/" + f"resourceInstance/{missing_identifier}/artifacts") + template = jinja_env().get_template("add_artifact_to_vf.json.j2") + data = template.render(artifact_name=artifact_name, + artifact_label=f"sdk{Path.PurePosixPath(artifact_name).stem}", + artifact_type=artifact_type, + b64_artifact=base64.b64encode(artifact).decode('utf-8')) + headers = headers_sdc_artifact_upload(base_header=self.headers, + data=data) + try: + self.send_message('POST', + 'Add artifact to vf', + url, + headers=headers, + data=data) + except RequestError as exc: + self._logger.error(("an error occured during file upload for an Artifact" + "to VNF %s"), vnf_name) + raise exc + + def get_component_properties_url(self, component: "Component") -> str: + """Url to get component's properties. + + This method is here because component can have different url when + it's a component of another SDC resource type, eg. for service and + for VF components have different urls. + Also for VL origin type components properties url is different than + for the other types. + + Args: + component (Component): Component object to prepare url for + + Returns: + str: Component's properties url + + """ + if component.origin_type == "VL": + return super().get_component_properties_url(component) + return (f"{self.resource_inputs_url}/" + f"componentInstances/{component.unique_id}/{component.actual_component_uid}/inputs") + + def get_component_properties_value_set_url(self, component: "Component") -> str: + """Url to set component property value. + + This method is here because component can have different url when + it's a component of another SDC resource type, eg. for service and + for VF components have different urls. + Also for VL origin type components properties url is different than + for the other types. + + Args: + component (Component): Component object to prepare url for + + Returns: + str: Component's properties url + + """ + if component.origin_type == "VL": + return super().get_component_properties_value_set_url(component) + return (f"{self.resource_inputs_url}/" + f"resourceInstance/{component.unique_id}/inputs") + + def get_category_for_new_resource(self) -> ServiceCategory: + """Get category for service not created in SDC yet. + + If no category values are provided default category is going to be used. + + Returns: + ServiceCategory: Category of the new service + + """ + if not self._category_name: + return ServiceCategory.get(name="Network Service") + return ServiceCategory.get(name=self._category_name) diff --git a/src/onapsdk/sdc/templates/add_artifact_to_vf.json.j2 b/src/onapsdk/sdc/templates/add_artifact_to_vf.json.j2 new file mode 100644 index 0000000..2b0446c --- /dev/null +++ b/src/onapsdk/sdc/templates/add_artifact_to_vf.json.j2 @@ -0,0 +1,9 @@ +{ + "artifactGroupType": "DEPLOYMENT", + "artifactName": "{{artifact_name}}", + "artifactLabel": "{{artifact_label}}", + "artifactType": "{{artifact_type}}", + "description": "test", + "payloadData": "{{b64_artifact}}", + "heatParameters": [] +} diff --git a/src/onapsdk/sdc/templates/add_resource_to_service.json.j2 b/src/onapsdk/sdc/templates/add_resource_to_service.json.j2 new file mode 100644 index 0000000..d6676e9 --- /dev/null +++ b/src/onapsdk/sdc/templates/add_resource_to_service.json.j2 @@ -0,0 +1,10 @@ +{ + "name": "{{ resource.name }}", + "componentVersion": "{{ resource.version }}", + "posY": {{ posY| default(100) }}, + "posX": {{ posX| default(200) }}, + "uniqueId": "{{ resource.unique_identifier }}", + "originType": "{{ resource_type }}", + "componentUid": "{{ resource.unique_identifier }}", + "icon": "defaulticon" +} diff --git a/src/onapsdk/sdc/templates/component_declare_input.json.j2 b/src/onapsdk/sdc/templates/component_declare_input.json.j2 new file mode 100644 index 0000000..fd0ee03 --- /dev/null +++ b/src/onapsdk/sdc/templates/component_declare_input.json.j2 @@ -0,0 +1,37 @@ +{ + "componentInstanceInputsMap": {}, + "componentInstanceProperties": { + "{{ component.unique_id }}": [ + { + "constraints": null, + "defaultValue": null, + "description": "", + "name": "{{ property.name }}", + "origName": "{{ property.name }}", + "parentUniqueId": null, + "password": false, + "required": true, + "schema": { + "property": {} + }, + "schemaType": null, + "type": "{{ property.property_type }}", + "uniqueId": "{{ property.unique_id }}", + {% if property.value is not none %} + "value":"{{ property.value }}", + {% else %} + "value":null, + {% endif %} + "definition": false, + "getInputValues": null, + "parentPropertyType": null, + "subPropertyInputPath": null, + "getPolicyValues": null, + "inputPath": null, + "metadata": null + } + ] + }, + "groupProperties": {}, + "policyProperties": {} +}
\ No newline at end of file diff --git a/src/onapsdk/sdc/templates/pnf_create.json.j2 b/src/onapsdk/sdc/templates/pnf_create.json.j2 new file mode 100644 index 0000000..fe7e60c --- /dev/null +++ b/src/onapsdk/sdc/templates/pnf_create.json.j2 @@ -0,0 +1,29 @@ +{ + "artifacts": {}, + "attributes": [], + "capabilities": {}, + {% include "sdc_resource_category.json.j2" %}, + "componentInstances": [], + "componentInstancesAttributes": {}, + "componentInstancesProperties": {}, + "componentType": "RESOURCE", + "contactId": "cs0008", + {% if vsp is not none %} + "csarUUID": "{{ vsp.csar_uuid }}", + "csarVersion": "1.0", + "vendorName": "{{ vsp.vendor.name }}", + {% else %} + "vendorName": "{{ vendor.name }}", + {% endif %} + "deploymentArtifacts": {}, + "description": "PNF", + "icon": "defaulticon", + "name": "{{ name }}", + "properties": [], + "groups": [], + "requirements": {}, + "resourceType": "PNF", + "tags": ["{{ name }}"], + "toscaArtifacts": {}, + "vendorRelease": "1.0" +} diff --git a/src/onapsdk/sdc/templates/sdc_element_action.json.j2 b/src/onapsdk/sdc/templates/sdc_element_action.json.j2 new file mode 100644 index 0000000..04fd946 --- /dev/null +++ b/src/onapsdk/sdc/templates/sdc_element_action.json.j2 @@ -0,0 +1,6 @@ +{ +{% if action == const.COMMIT %} + "commitRequest":{"message":"ok"}, +{% endif %} + "action": "{{ action }}" +} diff --git a/src/onapsdk/sdc/templates/sdc_resource_action.json.j2 b/src/onapsdk/sdc/templates/sdc_resource_action.json.j2 new file mode 100644 index 0000000..742d076 --- /dev/null +++ b/src/onapsdk/sdc/templates/sdc_resource_action.json.j2 @@ -0,0 +1,3 @@ +{ + "userRemarks": "{{ action | lower }}" +} diff --git a/src/onapsdk/sdc/templates/sdc_resource_add_deployment_artifact.json.j2 b/src/onapsdk/sdc/templates/sdc_resource_add_deployment_artifact.json.j2 new file mode 100644 index 0000000..290c6d2 --- /dev/null +++ b/src/onapsdk/sdc/templates/sdc_resource_add_deployment_artifact.json.j2 @@ -0,0 +1,8 @@ +{ + "artifactGroupType": "DEPLOYMENT", + "artifactName": "{{artifact_name}}", + "artifactLabel": "{{artifact_label}}", + "artifactType": "{{artifact_type}}", + "description": "test", + "payloadData": "{{b64_artifact}}" +} diff --git a/src/onapsdk/sdc/templates/sdc_resource_add_input.json.j2 b/src/onapsdk/sdc/templates/sdc_resource_add_input.json.j2 new file mode 100644 index 0000000..1964f36 --- /dev/null +++ b/src/onapsdk/sdc/templates/sdc_resource_add_input.json.j2 @@ -0,0 +1,39 @@ +{ + "serviceProperties":{ + "{{ sdc_resource.unique_identifier }}":[ + { + "constraints":null, + "defaultValue":null, + "description":null, + "name":"{{ property.name }}", + "origName":"{{ property.name }}", + "parentUniqueId":"{{ sdc_resource.unique_identifier }}", + "password":false, + "required":false, + "schema":{ + "property":{ + "type":"", + "required":false, + "definition":false, + "description":null, + "password":false + } + }, + "schemaType":"", + "type":"{{ property.property_type }}", + "uniqueId":"{{ sdc_resource.unique_identifier }}.{{ property.name }}", + {% if property.value is not none %} + "value":"{{ property.value }}", + {% else %} + "value":null, + {% endif %} + "definition":false, + "getInputValues":null, + "parentPropertyType":null, + "subPropertyInputPath":null, + "getPolicyValues":null, + "inputPath":null + } + ] + } +}
\ No newline at end of file diff --git a/src/onapsdk/sdc/templates/sdc_resource_add_nested_input.json.j2 b/src/onapsdk/sdc/templates/sdc_resource_add_nested_input.json.j2 new file mode 100644 index 0000000..9dc8261 --- /dev/null +++ b/src/onapsdk/sdc/templates/sdc_resource_add_nested_input.json.j2 @@ -0,0 +1,35 @@ +{ + "componentInstanceInputsMap":{ + "{{ component.unique_id }}":[ + { + {# "defaultValue":null, #} + "name":"{{ input.name }}", + "origName":"{{ input.name }}", + {# "parentUniqueId":"cs0008", #} + "password":false, + "required":false, + "schema":{ + "property":{ + {# "type":"", + "required":false, + "definition":false, + "password":false #} + } + }, + {# "schemaType":"", #} + "type":"{{ input.input_type }}", + "uniqueId":"{{ sdc_resource.unique_identifier }}.{{ input.name }}", + {% if input.default_value is not none %} + "value":"{{ input.default_value }}", + {% endif %} + "definition":false + {# "type":"{{ input.input_type }}", #} + } + ] + }, + "componentInstanceProperties":{ + "{{ component.unique_id }}":[] + }, + "groupProperties":{}, + "policyProperties":{} +}
\ No newline at end of file diff --git a/src/onapsdk/sdc/templates/sdc_resource_add_property.json.j2 b/src/onapsdk/sdc/templates/sdc_resource_add_property.json.j2 new file mode 100644 index 0000000..bed49ca --- /dev/null +++ b/src/onapsdk/sdc/templates/sdc_resource_add_property.json.j2 @@ -0,0 +1,17 @@ +{ + "{{ property.name }}":{ + "schema":{ + "property":{ + "type":"" + } + }, + "name": "{{ property.name }}", + {% if property.description %} + "description": "{{ property.description }}", + {% endif %} + {% if property.value %} + "value": "{{ property.value }}", + {% endif %} + "type": "{{ property.property_type }}" + } +}
\ No newline at end of file diff --git a/src/onapsdk/sdc/templates/sdc_resource_category.json.j2 b/src/onapsdk/sdc/templates/sdc_resource_category.json.j2 new file mode 100644 index 0000000..633aacd --- /dev/null +++ b/src/onapsdk/sdc/templates/sdc_resource_category.json.j2 @@ -0,0 +1,13 @@ + "categories": [ + { + "normalizedName": "{{ category.normalized_name }}", + "name": "{{ category.name }}", + "uniqueId": "{{ category.unique_id }}", + "subcategories": {% if category.subcategories %}{{ category.subcategories|tojson }}{% else %}null{% endif %}, + "version": {% if category.version %}"{{ category.version }}"{% else %}null{% endif %}, + "ownerId": {% if category.owner_id %}"{{ category.owner_id }}"{% else %}null{% endif %}, + "empty": {{ category.empty|tojson }}, + "type": {% if category.type %}"{{ category.type }}"{% else %}null{% endif %}, + "icons": {% if category.icons %}{{ category.icons|tojson }}{% else %}null{% endif %} + } + ]
\ No newline at end of file diff --git a/src/onapsdk/sdc/templates/sdc_resource_component_set_property_value.json.j2 b/src/onapsdk/sdc/templates/sdc_resource_component_set_property_value.json.j2 new file mode 100644 index 0000000..46bd527 --- /dev/null +++ b/src/onapsdk/sdc/templates/sdc_resource_component_set_property_value.json.j2 @@ -0,0 +1,13 @@ +[ + { + "name":"{{ property.name }}", + "parentUniqueId":"{{ component.actual_component_uid }}", + "type":"{{ property.property_type }}", + "uniqueId":"{{ component.actual_component_uid }}.{{ property.name }}", + "value":"{{ value }}", + "definition":false, + "toscaPresentation":{ + "ownerId":"{{ component.actual_component_uid }}" + } + } +]
\ No newline at end of file diff --git a/src/onapsdk/sdc/templates/sdc_resource_set_input_default_value.json.j2 b/src/onapsdk/sdc/templates/sdc_resource_set_input_default_value.json.j2 new file mode 100644 index 0000000..97c2cfd --- /dev/null +++ b/src/onapsdk/sdc/templates/sdc_resource_set_input_default_value.json.j2 @@ -0,0 +1,8 @@ +[ + { + "defaultValue":"{{ default_value }}", + "name":"{{ input.name }}", + "type":"{{ input.input_type }}", + "uniqueId":"{{ input.unique_id }}" + } +]
\ No newline at end of file diff --git a/src/onapsdk/sdc/templates/sdc_resource_set_property_value.json.j2 b/src/onapsdk/sdc/templates/sdc_resource_set_property_value.json.j2 new file mode 100644 index 0000000..d0e73f7 --- /dev/null +++ b/src/onapsdk/sdc/templates/sdc_resource_set_property_value.json.j2 @@ -0,0 +1,13 @@ +[ + { + "name":"{{ property.name }}", + "parentUniqueId":"{{ sdc_resource.unique_identifier }}", + "type":"{{ property.property_type }}", + "uniqueId":"{{ sdc_resource.unique_identifier }}.{{ property.name }}", + "value":"{{ value }}", + "definition":false, + "toscaPresentation":{ + "ownerId":"{{ sdc_resource.unique_identifier }}" + } + } +]
\ No newline at end of file diff --git a/src/onapsdk/sdc/templates/service_create.json.j2 b/src/onapsdk/sdc/templates/service_create.json.j2 new file mode 100644 index 0000000..f247059 --- /dev/null +++ b/src/onapsdk/sdc/templates/service_create.json.j2 @@ -0,0 +1,29 @@ + +{ + "componentType": "SERVICE", + "properties": [], + "requirements": {}, + "toscaArtifacts": {}, + "tags": ["{{ name }}"], + "artifacts": {}, + "description": "service", + "serviceApiArtifacts": {}, + "capabilities": {}, + "name": "{{ name }}", + "componentInstancesProperties": {}, + "componentInstancesAttributes": {}, + "contactId": "cs0008", + "groups": [], + "projectCode": "123456", + "deploymentArtifacts": {}, + "attributes": [], + "componentInstances": [], + "ecompGeneratedNaming": true, + "instantiationType": "{{ instantiation_type }}", + "environmentContext": "General_Revenue-Bearing", + {% include "sdc_resource_category.json.j2" %}, + "icon": "network_l_1-3", + "serviceFunction": "{{ function }}", + "serviceRole": "{{ role }}", + "serviceType": "{{ service_type }}" +} diff --git a/src/onapsdk/sdc/templates/vendor_create.json.j2 b/src/onapsdk/sdc/templates/vendor_create.json.j2 new file mode 100644 index 0000000..858f736 --- /dev/null +++ b/src/onapsdk/sdc/templates/vendor_create.json.j2 @@ -0,0 +1,5 @@ +{ + "iconRef": "icon", + "vendorName": "{{ name }}", + "description": "vendor" +} diff --git a/src/onapsdk/sdc/templates/vf_create.json.j2 b/src/onapsdk/sdc/templates/vf_create.json.j2 new file mode 100644 index 0000000..6f165e5 --- /dev/null +++ b/src/onapsdk/sdc/templates/vf_create.json.j2 @@ -0,0 +1,27 @@ +{ + "artifacts": {}, + "attributes": [], + "capabilities": {}, + {% include "sdc_resource_category.json.j2" %}, + "componentInstances": [], + "componentInstancesAttributes": {}, + "componentInstancesProperties": {}, + "componentType": "RESOURCE", + "contactId": "cs0008", + {% if category.name != "Allotted Resource" %} + "csarUUID": "{{ vsp.csar_uuid }}", + "csarVersion": "1.0", + {% endif %} + "deploymentArtifacts": {}, + "description": "VF", + "icon": "defaulticon", + "name": "{{ name }}", + "properties": [], + "groups": [], + "requirements": {}, + "resourceType": "VF", + "tags": ["{{ name }}"], + "toscaArtifacts": {}, + "vendorName": "{{ vendor.name }}", + "vendorRelease": "1.0" +} diff --git a/src/onapsdk/sdc/templates/vf_vsp_update.json.j2 b/src/onapsdk/sdc/templates/vf_vsp_update.json.j2 new file mode 100644 index 0000000..f862676 --- /dev/null +++ b/src/onapsdk/sdc/templates/vf_vsp_update.json.j2 @@ -0,0 +1,61 @@ +{ + "resourceType": "{{ resource_data['resourceType'] }}", + "componentType": "{{ resource_data['componentType'] }}", + "tags": {{ resource_data['tags'] | tojson }}, + "icon": "{{ resource_data['icon'] }}", + "uniqueId": "{{ resource_data['uniqueId'] }}", + "uuid": "{{ resource_data['uuid'] }}", + "invariantUUID": "{{ resource_data['invariantUUID'] }}", + "contactId": "{{ resource_data['contactId'] }}", + "categories": {{ resource_data['categories'] | tojson }}, + "creatorUserId": "{{ resource_data['creatorUserId'] }}", + "creationDate": {{ resource_data['creationDate'] }}, + "creatorFullName": "{{ resource_data['creatorFullName'] }}", + "description": "{{ resource_data['description'] }}", + "lastUpdateDate": {{ resource_data['lastUpdateDate'] }}, + "lastUpdaterUserId": "{{ resource_data['lastUpdaterUserId'] }}", + "lastUpdaterFullName": "{{ resource_data['lastUpdaterFullName'] }}", + "lifecycleState": "{{ resource_data['lifecycleState'] }}", + "name": "{{ resource_data['name'] }}", + "version": "{{ resource_data['version'] }}", + "allVersions": {{ resource_data['allVersions'] | tojson }}, + "vendorName": "{{ resource_data['vendorName'] }}", + "vendorRelease": "{{ resource_data['vendorRelease'] }}", + "normalizedName": "{{ resource_data['normalizedName'] }}", + "systemName": "{{ resource_data['systemName'] }}", + "archived": {{ resource_data['archived'] | tojson }}, + "componentMetadata": { + "resourceType": "{{ resource_data['resourceType'] }}", + "componentType": "{{ resource_data['componentType'] }}", + "tags": {{ resource_data['tags'] | tojson }}, + "icon": "{{ resource_data['icon'] }}", + "uniqueId": "{{ resource_data['uniqueId'] }}", + "uuid": "{{ resource_data['uuid'] }}", + "invariantUUID": "{{ resource_data['invariantUUID'] }}", + "contactId": "{{ resource_data['contactId'] }}", + "categories": {{ resource_data['categories'] | tojson }}, + "creatorUserId": "{{ resource_data['creatorUserId'] }}", + "creationDate": {{ resource_data['creationDate'] }}, + "creatorFullName": "{{ resource_data['creatorFullName'] }}", + "description": "{{ resource_data['description'] }}", + "lastUpdateDate": {{ resource_data['lastUpdateDate'] }}, + "lastUpdaterUserId": "{{ resource_data['lastUpdaterUserId'] }}", + "lastUpdaterFullName": "{{ resource_data['lastUpdaterFullName'] }}", + "lifecycleState": "{{ resource_data['lifecycleState'] }}", + "name": "{{ resource_data['name'] }}", + "version": "{{ resource_data['version'] }}", + "allVersions": {{ resource_data['allVersions'] | tojson }}, + "vendorName": "{{ resource_data['vendorName'] }}", + "vendorRelease": "{{ resource_data['vendorRelease'] }}", + "normalizedName": "{{ resource_data['normalizedName'] }}", + "systemName": "{{ resource_data['systemName'] }}", + "csarUUID": "{{ resource_data['csarUUID'] }}", + "csarVersion": "{{ resource_data['csarVersion'] }}", + "derivedFrom": null, + "resourceVendorModelNumber": "{{ resource_data['resourceVendorModelNumber'] }}" + }, + "csarUUID": "{{ csarUUID }}", + "csarVersion": "{{ csarVersion }}", + "derivedFrom": null, + "resourceVendorModelNumber": "{{ resource_data['resourceVendorModelNumber'] }}" +}
\ No newline at end of file diff --git a/src/onapsdk/sdc/templates/vsp_create.json.j2 b/src/onapsdk/sdc/templates/vsp_create.json.j2 new file mode 100644 index 0000000..30fa6b9 --- /dev/null +++ b/src/onapsdk/sdc/templates/vsp_create.json.j2 @@ -0,0 +1,11 @@ +{ + "name": "{{ name }}", + "description": "vendor software product", + "icon": "icon", + "category": "resourceNewCategory.generic", + "subCategory": "resourceNewCategory.generic.abstract", + "vendorName": "{{ vendor.name }}", + "vendorId": "{{ vendor.identifier }}", + "licensingData": {}, + "onboardingMethod": "NetworkPackage" +} diff --git a/src/onapsdk/sdc/vendor.py b/src/onapsdk/sdc/vendor.py new file mode 100644 index 0000000..a82e3b9 --- /dev/null +++ b/src/onapsdk/sdc/vendor.py @@ -0,0 +1,108 @@ +"""Vendor module.""" +# Copyright 2022 Orange, 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 +from typing import Dict + +from onapsdk.sdc.sdc_element import SdcElement +import onapsdk.constants as const +from onapsdk.utils.headers_creator import headers_sdc_creator + + +class Vendor(SdcElement): + """ + ONAP Vendor Object used for SDC operations. + + Attributes: + name (str): the name of the vendor. Defaults to "Generic-Vendor". + identifier (str): the unique ID of the vendor from SDC. + status (str): the status of the vendor from SDC. + version (str): the version ID of the vendor from SDC. + + """ + + VENDOR_PATH = "vendor-license-models" + headers = headers_sdc_creator(SdcElement.headers) + + def __init__(self, name: str = None): + """ + Initialize vendor object. + + Args: + name (optional): the name of the vendor + + """ + super().__init__() + self.name: str = name or "Generic-Vendor" + + def onboard(self) -> None: + """Onboard the vendor in SDC.""" + if not self.status: + self.create() + self.onboard() + elif self.status == const.DRAFT: + self.submit() + + def create(self) -> None: + """Create the vendor in SDC if not already existing.""" + self._create("vendor_create.json.j2", name=self.name) + + def submit(self) -> None: + """Submit the SDC vendor in order to enable it.""" + self._logger.info("attempting to certify/sumbit vendor %s in SDC", + self.name) + if self.status != const.CERTIFIED and self.created(): + self._really_submit() + elif self.status == const.CERTIFIED: + self._logger.warning( + "vendor %s in SDC is already submitted/certified", self.name) + elif not self.created(): + self._logger.warning("vendor %s in SDC is not created", self.name) + + def update_informations_from_sdc(self, details: Dict[str, Any]) -> None: + """ + + Update instance with details from SDC. + + Args: + details (Dict[str, Any]): dict from SDC + + """ + self._status = details['status'] + + @classmethod + def import_from_sdc(cls, values: Dict[str, Any]) -> 'Vendor': + """ + Import Vendor from SDC. + + Args: + values (Dict[str, Any]): dict to parse returned from SDC. + + Returns: + a Vsp instance with right values + + """ + vendor = Vendor(values['name']) + vendor.identifier = values['id'] + return vendor + + def _really_submit(self) -> None: + """Really submit the SDC Vf in order to enable it.""" + self._action_to_sdc(const.SUBMIT) + self._status = const.CERTIFIED + + @classmethod + def _sdc_path(cls) -> None: + """Give back the end of SDC path.""" + return cls.VENDOR_PATH diff --git a/src/onapsdk/sdc/vf.py b/src/onapsdk/sdc/vf.py new file mode 100644 index 0000000..7e4e4a7 --- /dev/null +++ b/src/onapsdk/sdc/vf.py @@ -0,0 +1,164 @@ +"""Vf module.""" +# Copyright 2022 Orange, 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 __future__ import annotations +from typing import Any, Dict, List, Optional, TYPE_CHECKING, Union + +from onapsdk.exceptions import ParameterError +from onapsdk.sdc.properties import ComponentProperty, NestedInput, Property +from onapsdk.sdc.sdc_resource import SdcResource +from onapsdk.sdc.vendor import Vendor +from onapsdk.utils.jinja import jinja_env +import onapsdk.constants as const + +if TYPE_CHECKING: + from onapsdk.sdc.vsp import Vsp + + +class Vf(SdcResource): + """ + ONAP Vf Object used for SDC operations. + + Attributes: + name (str): the name of the vendor. Defaults to "Generic-Vendor". + identifier (str): the unique ID of the vendor from SDC. + status (str): the status of the vendor from SDC. + version (str): the version ID of the vendor from SDC. + vsp (Vsp): the Vsp used for VF creation + uuid (str): the UUID of the VF (which is different from identifier, + don't ask why...) + unique_identifier (str): Yet Another ID, just to puzzle us... + + """ + + def __init__(self, name: str = None, version: str = None, sdc_values: Dict[str, str] = None, # pylint: disable=too-many-arguments + vsp: Vsp = None, properties: List[Property] = None, + inputs: Union[Property, NestedInput] = None, + category: str = None, subcategory: str = None, + vendor: Vendor = None): + """ + Initialize vf object. + + Args: + name (optional): the name of the vf + version (str, optional): the version of the vf + vsp (Vsp, optional): VSP object related with the Vf object. + Defaults to None. + vendor (Vendor, optional): Vendor object used if VSP was not provided. + Defaults to None. + + """ + super().__init__(sdc_values=sdc_values, version=version, properties=properties, + inputs=inputs, category=category, subcategory=subcategory) + self.name: str = name or "ONAP-test-VF" + self.vsp: Vsp = vsp + self._vendor: Vendor = vendor + + @property + def vendor(self) -> Optional[Vendor]: + """Vendor related wth Vf. + + If Vf is created vendor is get from it's resource metadata. + Otherwise it's vendor provided by the user or the vendor from vsp. + It's possible that method returns None, but it won't be possible then + to create that Vf resource. + + Returns: + Optional[Vendor]: Vendor object related with Vf + + """ + if self._vendor: + return self._vendor + if self.created(): + resource_data: Dict[str, Any] = self.send_message_json( + "GET", + "Get VF data to update VSP", + self.resource_inputs_url + ) + self._vendor = Vendor(name=resource_data.get("vendorName")) + elif self.vsp: + self._vendor = self.vsp.vendor + return self._vendor + + + def create(self) -> None: + """Create the Vf in SDC if not already existing. + + Raises: + ParameterError: VSP is not provided during VF object initalization + + """ + if not any([self.vsp, self.vendor]): + raise ParameterError("At least vsp or vendor needs to be given") + self._create("vf_create.json.j2", name=self.name, vsp=self.vsp, + category=self.category, vendor=self.vendor) + + def _really_submit(self) -> None: + """Really submit the SDC Vf in order to enable it.""" + self._action_to_sdc(const.CERTIFY, "lifecycleState") + self.load() + + def update_vsp(self, vsp: Vsp) -> None: + """Update Vsp. + + Update VSP UUID and version for Vf object. + + Args: + vsp (Vsp): Object to be used in Vf + + Raises: + ValidationError: Vf object request has invalid structure. + + """ + resource_data: Dict[str, Any] = self.send_message_json( + "GET", + "Get VF data to update VSP", + self.resource_inputs_url + ) + self.send_message_json( + "PUT", + "Update vsp data", + self.resource_inputs_url, + data=jinja_env() + .get_template("vf_vsp_update.json.j2") + .render(resource_data=resource_data, + csarUUID=vsp.csar_uuid, + csarVersion=vsp.human_readable_version) + ) + + def declare_input(self, + input_to_declare: Union[Property, NestedInput, ComponentProperty]) -> None: + """Declare input for given property, nested input or component property object. + + Call SDC FE API to declare input for given property. + + Args: + input_declaration (Union[Property, NestedInput]): Property or ComponentProperty + to declare input or NestedInput object + + Raises: + ParameterError: if the given property is not SDC resource property + + """ + if not isinstance(input_to_declare, ComponentProperty): + super().declare_input(input_to_declare) + else: + self.send_message("POST", + f"Declare new input for {input_to_declare.name} property", + f"{self.resource_inputs_url}/create/inputs", + data=jinja_env().get_template(\ + "component_declare_input.json.j2").\ + render(\ + component=input_to_declare.component, + property=input_to_declare)) diff --git a/src/onapsdk/sdc/vfc.py b/src/onapsdk/sdc/vfc.py new file mode 100644 index 0000000..e2a6f57 --- /dev/null +++ b/src/onapsdk/sdc/vfc.py @@ -0,0 +1,46 @@ +"""VFC module.""" +# Copyright 2022 Orange, 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 Dict + +from onapsdk.exceptions import ResourceNotFound + +from .sdc_resource import SdcResource + + +class Vfc(SdcResource): + """ONAP VFC Object used for SDC operations.""" + + def __init__(self, name: str, version: str = None, sdc_values: Dict[str, str] = None): + """Initialize VFC object. + + Vfc has to exist in SDC. + + Args: + name (str): Vfc name + version (str): Vfc version + sdc_values (Dict[str, str], optional): Sdc values of existing Vfc. Defaults to None. + + Raises: + ResourceNotFound: Vfc doesn't exist in SDC + + """ + super().__init__(name=name, version=version, sdc_values=sdc_values) + if not sdc_values and not self.exists(): + raise ResourceNotFound( + "This Vfc has to exist prior to object initialization.") + + def _really_submit(self) -> None: + """Really submit the SDC in order to enable it.""" + raise NotImplementedError("Vfc doesn't need _really_submit") diff --git a/src/onapsdk/sdc/vl.py b/src/onapsdk/sdc/vl.py new file mode 100644 index 0000000..22872f0 --- /dev/null +++ b/src/onapsdk/sdc/vl.py @@ -0,0 +1,46 @@ +"""Vl module.""" +# Copyright 2022 Orange, 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 Dict + +from onapsdk.exceptions import ResourceNotFound + +from .sdc_resource import SdcResource + + +class Vl(SdcResource): + """ONAP Vl Object used for SDC operations.""" + + def __init__(self, name: str, version: str = None, sdc_values: Dict[str, str] = None): + """Initialize Vl object. + + Vl has to exists in SDC. + + Args: + name (str): Vl name + version (str): Vl version + sdc_values (Dict[str, str], optional): Sdc values of existing Vl. Defaults to None. + + Raises: + ResourceNotFound: Vl doesn't exist in SDC + + """ + super().__init__(name=name, version=version, sdc_values=sdc_values) + if not sdc_values and not self.exists(): + raise ResourceNotFound( + "This Vl has to exist prior to object initialization.") + + def _really_submit(self) -> None: + """Really submit the SDC in order to enable it.""" + raise NotImplementedError("Vl don't need _really_submit") diff --git a/src/onapsdk/sdc/vsp.py b/src/onapsdk/sdc/vsp.py new file mode 100644 index 0000000..a6c4c24 --- /dev/null +++ b/src/onapsdk/sdc/vsp.py @@ -0,0 +1,370 @@ +"""VSP module.""" +# Copyright 2022 Orange, 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 json +from typing import Any, Optional +from typing import BinaryIO +from typing import Callable +from typing import Dict + +from onapsdk.exceptions import APIError, ParameterError +from onapsdk.sdc.sdc_element import SdcElement +from onapsdk.sdc.vendor import Vendor +import onapsdk.constants as const +from onapsdk.utils.headers_creator import headers_sdc_creator + +# Hard to do fewer attributes and still mapping SDC VSP object. +class Vsp(SdcElement): # pylint: disable=too-many-instance-attributes + """ + ONAP VSP Object used for SDC operations. + + Attributes: + name (str): the name of the vsp. Defaults to "ONAP-test-VSP". + identifier (str): the unique ID of the VSP from SDC. + status (str): the status of the VSP from SDC. + version (str): the version ID of the VSP from SDC. + csar_uuid (str): the CSAR ID of the VSP from SDC. + vendor (Vendor): The VSP Vendor + + """ + + VSP_PATH = "vendor-software-products" + headers = headers_sdc_creator(SdcElement.headers) + + def __init__(self, name: str = None, package: BinaryIO = None, + vendor: Vendor = None): + """ + Initialize vsp object. + + Args: + name (optional): the name of the vsp + + """ + super().__init__() + self._csar_uuid: str = None + self._vendor: Vendor = vendor or None + self.name: str = name or "ONAP-test-VSP" + self.package = package or None + + @property + def status(self): + """Return and load the status.""" + self.load_status() + return self._status + + def onboard(self) -> None: + """Onboard the VSP in SDC.""" + status: Optional[str] = self.status + if not status: + if not self._vendor: + raise ParameterError("No Vendor provided.") + self.create() + self.onboard() + elif status == const.DRAFT: + if not self.package: + raise ParameterError("No file/package provided.") + self.upload_package(self.package) + self.onboard() + elif status == const.UPLOADED: + self.validate() + self.onboard() + elif status == const.VALIDATED: + self.commit() + self.onboard() + elif status == const.COMMITED: + self.submit() + self.onboard() + elif status == const.CERTIFIED: + self.create_csar() + + def create(self) -> None: + """Create the Vsp in SDC if not already existing.""" + if self.vendor: + self._create("vsp_create.json.j2", + name=self.name, + vendor=self.vendor) + + def upload_package(self, package_to_upload: BinaryIO) -> None: + """ + Upload given zip file into SDC as artifacts for this Vsp. + + Args: + package_to_upload (file): the zip file to upload + + """ + self._action("upload package", + const.DRAFT, + self._upload_action, + package_to_upload=package_to_upload) + + def update_package(self, package_to_upload: BinaryIO) -> None: + """ + Upload given zip file into SDC as artifacts for this Vsp. + + Args: + package_to_upload (file): the zip file to upload + + """ + self._action("update package", + const.COMMITED, + self._upload_action, + package_to_upload=package_to_upload) + + def validate(self) -> None: + """Validate the artifacts uploaded.""" + self._action("validate", const.UPLOADED, self._validate_action) + + def commit(self) -> None: + """Commit the SDC Vsp.""" + self._action("commit", + const.VALIDATED, + self._generic_action, + action=const.COMMIT) + + def submit(self) -> None: + """Submit the SDC Vsp in order to enable it.""" + self._action("certify/sumbit", + const.COMMITED, + self._generic_action, + action=const.SUBMIT) + + def create_csar(self) -> None: + """Create the CSAR package in the SDC Vsp.""" + self._action("create CSAR package", const.CERTIFIED, + self._create_csar_action) + + @property + def vendor(self) -> Vendor: + """Return and lazy load the vendor.""" + if not self._vendor and self.created(): + details = self._get_vsp_details() + if details: + self._vendor = Vendor(name=details['vendorName']) + return self._vendor + + @vendor.setter + def vendor(self, vendor: Vendor) -> None: + """Set value for Vendor.""" + self._vendor = vendor + + @property + def csar_uuid(self) -> str: + """Return and lazy load the CSAR UUID.""" + if self.created() and not self._csar_uuid: + self.create_csar() + return self._csar_uuid + + @csar_uuid.setter + def csar_uuid(self, csar_uuid: str) -> None: + """Set value for csar uuid.""" + self._csar_uuid = csar_uuid + + def _get_item_version_details(self) -> Dict[Any, Any]: + """Get vsp item details.""" + if self.created() and self.version: + url = "{}/items/{}/versions/{}".format(self._base_url(), + self.identifier, + self.version) + return self.send_message_json('GET', 'get item version', url) + return {} + + def _upload_action(self, package_to_upload: BinaryIO): + """Do upload for real.""" + url = "{}/{}/{}/orchestration-template-candidate".format( + self._base_url(), Vsp._sdc_path(), self._version_path()) + headers = self.headers.copy() + headers.pop("Content-Type") + headers["Accept-Encoding"] = "gzip, deflate" + data = {'upload': package_to_upload} + upload_result = self.send_message('POST', + 'upload ZIP for Vsp', + url, + headers=headers, + files=data) + if upload_result: + # TODO https://jira.onap.org/browse/SDC-3505 pylint: disable=W0511 + response_json = json.loads(upload_result.text) + if response_json["status"] != "Success": + self._logger.error( + "an error occured during file upload for Vsp %s", + self.name) + raise APIError(response_json) + # End TODO https://jira.onap.org/browse/SDC-3505 + self._logger.info("Files for Vsp %s have been uploaded", + self.name) + else: + self._logger.error( + "an error occured during file upload for Vsp %s", + self.name) + + def _validate_action(self): + """Do validate for real.""" + url = "{}/{}/{}/orchestration-template-candidate/process".format( + self._base_url(), Vsp._sdc_path(), self._version_path()) + validate_result = self.send_message_json('PUT', + 'Validate artifacts for Vsp', + url) + if validate_result and validate_result['status'] == 'Success': + self._logger.info("Artifacts for Vsp %s have been validated", + self.name) + else: + self._logger.error( + "an error occured during artifacts validation for Vsp %s", + self.name) + + def _generic_action(self, action=None): + """Do a generic action for real.""" + if action: + self._action_to_sdc(action, action_type="lifecycleState") + + def _create_csar_action(self): + """Create CSAR package for real.""" + result = self._action_to_sdc(const.CREATE_PACKAGE, + action_type="lifecycleState") + if result: + self._logger.info("result: %s", result.text) + data = result.json() + self.csar_uuid = data['packageId'] + + def _action(self, action_name: str, right_status: str, + action_function: Callable[['Vsp'], None], **kwargs) -> None: + """ + Generate an action on the instance in order to send it to SDC. + + Args: + action_name (str): The name of the action (for the logs) + right_status (str): The status that the object must be + action_function (function): the function to perform if OK + + """ + self._logger.info("attempting to %s for %s in SDC", action_name, + self.name) + if self.status == right_status: + action_function(**kwargs) + else: + self._logger.warning( + "vsp %s in SDC is not created or not in %s status", self.name, + right_status) + + # VSP: DRAFT --> UPLOADED --> VALIDATED --> COMMITED --> CERTIFIED + def load_status(self) -> None: + """ + Load Vsp status from SDC. + + rules are following: + + * DRAFT: status == DRAFT and networkPackageName not present + + * UPLOADED: status == DRAFT and networkPackageName present and + validationData not present + + * VALIDATED: status == DRAFT and networkPackageName present and + validationData present and state.dirty = true + + * COMMITED: status == DRAFT and networkPackageName present and + validationData present and state.dirty = false + + * CERTIFIED: status == CERTIFIED + + status is found in sdc items + state is found in sdc version from items + networkPackageName and validationData is found in SDC vsp show + + """ + item_details = self._get_item_details() + if (item_details and item_details['status'] == const.CERTIFIED): + self._status = const.CERTIFIED + else: + self._check_status_not_certified() + + def _check_status_not_certified(self) -> None: + """Check a status when it's not certified.""" + vsp_version_details = self._get_item_version_details() + vsp_details = self._get_vsp_details() + if (vsp_version_details and 'state' in vsp_version_details + and not vsp_version_details['state']['dirty'] and vsp_details + and 'validationData' in vsp_details): + self._status = const.COMMITED + else: + self._check_status_not_commited() + + def _check_status_not_commited(self) -> None: + """Check a status when it's not certified or commited.""" + vsp_details = self._get_vsp_details() + if (vsp_details and 'validationData' in vsp_details): + self._status = const.VALIDATED + elif (vsp_details and 'validationData' not in vsp_details + and 'networkPackageName' in vsp_details): + self._status = const.UPLOADED + elif vsp_details: + self._status = const.DRAFT + + def _get_vsp_details(self) -> Dict[Any, Any]: + """Get vsp details.""" + if self.created() and self.version: + url = "{}/vendor-software-products/{}/versions/{}".format( + self._base_url(), self.identifier, self.version) + + return self.send_message_json('GET', 'get vsp version', url) + return {} + + @classmethod + def import_from_sdc(cls, values: Dict[str, Any]) -> 'Vsp': + """ + Import Vsp from SDC. + + Args: + values (Dict[str, Any]): dict to parse returned from SDC. + + Returns: + a Vsp instance with right values + + """ + cls._logger.debug("importing VSP %s from SDC", values['name']) + vsp = Vsp(values['name']) + vsp.identifier = values['id'] + vsp.vendor = Vendor(name=values['vendorName']) + return vsp + + def _really_submit(self) -> None: + """Really submit the SDC Vf in order to enable it. + + Raises: + NotImplementedError + + """ + raise NotImplementedError("VSP don't need _really_submit") + + @classmethod + def _sdc_path(cls) -> None: + """Give back the end of SDC path.""" + return cls.VSP_PATH + + def create_new_version(self) -> None: + """Create new version of VSP. + + Create a new major version of VSP so it would be possible to + update a package or do some changes in VSP. + + """ + self._logger.debug("Create new version of %s VSP", self.name) + self.send_message_json("POST", + "Create new VSP version", + (f"{self._base_url()}/items/{self.identifier}/" + f"versions/{self.version}"), + data=json.dumps({ + "creationMethod": "major", + "description": "New VSP version" + })) + self.load() diff --git a/src/onapsdk/sdnc/__init__.py b/src/onapsdk/sdnc/__init__.py new file mode 100644 index 0000000..8bc40e8 --- /dev/null +++ b/src/onapsdk/sdnc/__init__.py @@ -0,0 +1,15 @@ +"""SDNC package.""" +# Copyright 2022 Orange, 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 .preload import NetworkPreload, VfModulePreload diff --git a/src/onapsdk/sdnc/preload.py b/src/onapsdk/sdnc/preload.py new file mode 100644 index 0000000..8b00e5c --- /dev/null +++ b/src/onapsdk/sdnc/preload.py @@ -0,0 +1,148 @@ +"""SDNC preload module.""" +# Copyright 2022 Orange, 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, Iterable + +from onapsdk.utils.headers_creator import headers_sdnc_creator +from onapsdk.utils.jinja import jinja_env + +from .sdnc_element import SdncElement + + +class Preload(SdncElement): + """Preload base class.""" + + headers: Dict[str, str] = headers_sdnc_creator(SdncElement.headers) + + +class PreloadInformation(Preload): + """Preload information.""" + + def __init__(self, preload_id: str, preload_type: str, preload_data: Dict[str, Any]) -> None: + """Preload information initialization. + + Args: + preload_id (str): Preload id + preload_type (str): Preload type + preload_data (Dict[str, Any]): Preload data + """ + super().__init__() + self.preload_id: str = preload_id + self.preload_type: str = preload_type + self.preload_data: Dict[str, Any] = preload_data + + def __repr__(self) -> str: # noqa + """Preload information human readble string. + + Returns: + str: Preload information description + + """ + return (f"PreloadInformation(preload_id={self.preload_id}, " + f"preload_type={self.preload_type}, " + f"preload_data={self.preload_data})") + + @classmethod + def get_all(cls) -> Iterable["PreloadInformation"]: + """Get all preload informations. + + Get all uploaded preloads. + + Yields: + PreloadInformation: Preload information object + + """ + for preload_information in \ + cls.send_message_json(\ + "GET",\ + "Get SDNC preload information",\ + f"{cls.base_url}/restconf/operational/GENERIC-RESOURCE-API:preload-information" + ).get('preload-information', {}).get('preload-list', []): + yield PreloadInformation(preload_id=preload_information["preload-id"], + preload_type=preload_information["preload-type"], + preload_data=preload_information["preload-data"]) + + +class NetworkPreload(Preload): + """Class to upload network module preload.""" + + @classmethod + def upload_network_preload(cls, + network: "Network", + network_instance_name: str, + subnets: Iterable["Subnet"] = None) -> None: + """Upload network preload. + + Args: + network: Network object + network_instance_name (str): network instance name + subnets (Iterable[Subnet], optional): Iterable object of Subnet. + Defaults to None. + + """ + cls.send_message_json( + "POST", + "Upload Network preload using GENERIC-RESOURCE-API", + (f"{cls.base_url}/restconf/operations/" + "GENERIC-RESOURCE-API:preload-network-topology-operation"), + data=jinja_env().get_template( + "instantiate_network_ala_carte_upload_preload_gr_api.json.j2"). + render( + network=network, + network_instance_name=network_instance_name, + subnets=subnets if subnets else [] + ) + ) + + +class VfModulePreload(Preload): + """Class to upload vf module preload.""" + + @classmethod + def upload_vf_module_preload(cls, # pylint: disable=too-many-arguments + vnf_instance: "VnfInstance", + vf_module_instance_name: str, + vf_module: "VfModule", + vnf_parameters: Iterable["InstantiationParameter"] = None) -> None: + """Upload vf module preload. + + Args: + vnf_instance: VnfInstance object + vf_module_instance_name (str): VF module instance name + vf_module (VfModule): VF module + vnf_parameters (Iterable[InstantiationParameter], optional): Iterable object + of InstantiationParameter. Defaults to None. + + """ + vnf_para = [] + if vnf_parameters: + for vnf_parameter in vnf_parameters: + vnf_para.append({ + "name": vnf_parameter.name, + "value": vnf_parameter.value + }) + cls.send_message_json( + "POST", + "Upload VF module preload using GENERIC-RESOURCE-API", + (f"{cls.base_url}/restconf/operations/" + "GENERIC-RESOURCE-API:preload-vf-module-topology-operation"), + data=jinja_env().get_template( + "instantiate_vf_module_ala_carte_upload_preload_gr_api.json.j2"). + render( + vnf_instance=vnf_instance, + vf_module_instance_name=vf_module_instance_name, + vf_module=vf_module, + vnf_parameters=vnf_para + ) + ) diff --git a/src/onapsdk/sdnc/sdnc_element.py b/src/onapsdk/sdnc/sdnc_element.py new file mode 100644 index 0000000..84f56d9 --- /dev/null +++ b/src/onapsdk/sdnc/sdnc_element.py @@ -0,0 +1,48 @@ +"""SDNC base module.""" +# Copyright 2022 Orange, 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 onapsdk.configuration import settings +from onapsdk.onap_service import OnapService +from onapsdk.utils.gui import GuiItem, GuiList + + +class SdncElement(OnapService): + """SDNC base class.""" + + base_url = settings.SDNC_URL + + @classmethod + def get_guis(cls) -> GuiItem: + """Retrieve the status of the SDNC GUIs. + + There are 2 GUIS + - SDNC DG Builder + - SDNC ODL + + Return the list of GUIs + """ + guilist = GuiList([]) + url = settings.SDNC_DG_GUI_SERVICE + response = cls.send_message( + "GET", "Get SDNC GUI DG Status", url) + guilist.add(GuiItem( + url, + response.status_code)) + url = settings.SDNC_ODL_GUI_SERVICE + response = cls.send_message( + "GET", "Get SDNC ODL GUI Status", url) + guilist.add(GuiItem( + url, + response.status_code)) + return guilist diff --git a/src/onapsdk/sdnc/templates/instantiate_network_ala_carte_upload_preload_gr_api.json.j2 b/src/onapsdk/sdnc/templates/instantiate_network_ala_carte_upload_preload_gr_api.json.j2 new file mode 100644 index 0000000..9eead38 --- /dev/null +++ b/src/onapsdk/sdnc/templates/instantiate_network_ala_carte_upload_preload_gr_api.json.j2 @@ -0,0 +1,42 @@ +{ + "input": { + "preload-network-topology-information": { + "network-policy": [], + "route-table-reference": [], + "vpn-bindings": [], + "network-topology-identifier-structure": { + "network-role": "integration_test_net", + "network-technology": "neutron", + "network-name": "{{ network_instance_name }}", + "network-type": "Generic NeutronNet" + }, + "is-external-network": false, + "is-shared-network": false, + "is-provider-network": false, + "physical-network-name": "Not Aplicable", + "subnets": [ + {% for subnet in subnets %} + { + "subnet-name": "{{ subnet.name }}", + "start-address": "{{ subnet.start_address }}", + "cidr-mask": "{{ subnet.cidr_mask }}", + "ip-version": "{{ subnet.ip_version }}", + {% if subnet.dhcp_enabled %} + "dhcp-enabled": "Y", + "dhcp-start-address": "{{ subnet.dhcp_start_address }}", + "dhcp-end-address": "{{ subnet.dhcp_end_address }}", + {% else %} + "dhcp-enabled": "N", + {% endif %} + "gateway-address": "{{ subnet.gateway_address }}" + }{% if not loop.last %},{% endif %} + {% endfor %} + ] + }, + "sdnc-request-header": { + "svc-request-id": "test", + "svc-notification-url": "http:\/\/onap.org:8080\/adapters\/rest\/SDNCNotify", + "svc-action": "reserve" + } + } +} diff --git a/src/onapsdk/sdnc/templates/instantiate_vf_module_ala_carte_upload_preload_gr_api.json.j2 b/src/onapsdk/sdnc/templates/instantiate_vf_module_ala_carte_upload_preload_gr_api.json.j2 new file mode 100644 index 0000000..eaa5936 --- /dev/null +++ b/src/onapsdk/sdnc/templates/instantiate_vf_module_ala_carte_upload_preload_gr_api.json.j2 @@ -0,0 +1,41 @@ +{ + "input":{ + "preload-vf-module-topology-information":{ + "vf-module-topology":{ + "vf-module-topology-identifier":{ + "vf-module-name":"{{vf_module_instance_name}}" + }, + "vf-module-parameters": { + "param": {{ vnf_parameters }} + } + }, + "vnf-topology-identifier-structure":{ + "vnf-name":"{{vnf_instance.vnf_name}}", + "vnf-type":"{{vnf_instance.vnf_type}}" + }, + "vnf-resource-assignments":{ + "availability-zones":{ + "availability-zone":[ + "nova" + ], + "max-count":1 + }, + "vnf-networks":{ + "vnf-network":[] + } + } + }, + "request-information":{ + "request-id":"test", + "order-version":"1", + "notification-url":"onap.org", + "order-number":"1", + "request-action":"PreloadVfModuleRequest" + }, + "sdnc-request-header":{ + "svc-request-id":"test", + "svc-notification-url":"http:\/\/onap.org:8080\/adapters\/rest\/SDNCNotify", + "svc-action":"reserve" + } + } +} diff --git a/src/onapsdk/so/__init__.py b/src/onapsdk/so/__init__.py new file mode 100644 index 0000000..15a483f --- /dev/null +++ b/src/onapsdk/so/__init__.py @@ -0,0 +1,14 @@ +"""ONAP SDK SO package.""" +# Copyright 2022 Orange, 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/src/onapsdk/so/deletion.py b/src/onapsdk/so/deletion.py new file mode 100644 index 0000000..35ff0ee --- /dev/null +++ b/src/onapsdk/so/deletion.py @@ -0,0 +1,167 @@ +"""Deletion module.""" +# Copyright 2022 Orange, 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 onapsdk.onap_service import OnapService +from onapsdk.utils.headers_creator import headers_so_creator +from onapsdk.utils.jinja import jinja_env + +from onapsdk.so.so_element import OrchestrationRequest + + +class DeletionRequest(OrchestrationRequest, ABC): + """Deletion request base class.""" + + @classmethod + def send_request(cls, instance: "AaiResource", a_la_carte: bool = True) -> "Deletion": + """Abstract method to send instance deletion request. + + Raises: + NotImplementedError: Needs to be implemented in inheriting classes + + """ + raise NotImplementedError + + +class VfModuleDeletionRequest(DeletionRequest): # pytest: disable=too-many-ancestors + """VF module deletion class.""" + + @classmethod + def send_request(cls, + instance: "VfModuleInstance", + a_la_carte: bool = True) -> "VfModuleDeletion": + """Send request to SO to delete VNF instance. + + Args: + instance (VfModuleInstance): Vf Module instance to delete + a_la_carte (boolean): deletion mode + + Returns: + VnfDeletionRequest: Deletion request object + + """ + cls._logger.debug("VF module %s deletion request", instance.vf_module_id) + response = cls.send_message_json("DELETE", + (f"Create {instance.vf_module_id} VF module" + "deletion request"), + (f"{cls.base_url}/onap/so/infra/" + f"serviceInstantiation/{cls.api_version}/" + "serviceInstances/" + f"{instance.vnf_instance.service_instance.instance_id}/" + f"vnfs/{instance.vnf_instance.vnf_id}/" + f"vfModules/{instance.vf_module_id}"), + data=jinja_env(). + get_template("deletion_vf_module.json.j2"). + render(vf_module_instance=instance, + a_la_carte=a_la_carte), + headers=headers_so_creator(OnapService.headers)) + return cls(request_id=response["requestReferences"]["requestId"]) + + +class VnfDeletionRequest(DeletionRequest): # pytest: disable=too-many-ancestors + """VNF deletion class.""" + + @classmethod + def send_request(cls, + instance: "VnfInstance", + a_la_carte: bool = True) -> "VnfDeletionRequest": + """Send request to SO to delete VNF instance. + + Args: + instance (VnfInstance): VNF instance to delete + a_la_carte (boolean): deletion mode + + Returns: + VnfDeletionRequest: Deletion request object + + """ + cls._logger.debug("VNF %s deletion request", instance.vnf_id) + response = cls.send_message_json("DELETE", + f"Create {instance.vnf_id} VNF deletion request", + (f"{cls.base_url}/onap/so/infra/" + f"serviceInstantiation/{cls.api_version}/" + "serviceInstances/" + f"{instance.service_instance.instance_id}/" + f"vnfs/{instance.vnf_id}"), + data=jinja_env(). + get_template("deletion_vnf.json.j2"). + render(vnf_instance=instance, + a_la_carte=a_la_carte), + headers=headers_so_creator(OnapService.headers)) + return cls(request_id=response["requestReferences"]["requestId"]) + + +class ServiceDeletionRequest(DeletionRequest): # pytest: disable=too-many-ancestors + """Service deletion request class.""" + + @classmethod + def send_request(cls, + instance: "ServiceInstance", + a_la_carte: bool = True) -> "ServiceDeletionRequest": + """Send request to SO to delete service instance. + + Args: + instance (ServiceInstance): service instance to delete + a_la_carte (boolean): deletion mode + + Returns: + ServiceDeletionRequest: Deletion request object + + """ + cls._logger.debug("Service %s deletion request", instance.instance_id) + response = cls.send_message_json("DELETE", + f"Create {instance.instance_id} Service deletion request", + (f"{cls.base_url}/onap/so/infra/" + f"serviceInstantiation/{cls.api_version}/" + f"serviceInstances/{instance.instance_id}"), + data=jinja_env(). + get_template("deletion_service.json.j2"). + render(service_instance=instance, + a_la_carte=a_la_carte), + headers=headers_so_creator(OnapService.headers)) + return cls(request_id=response["requestReferences"]["requestId"]) + + +class NetworkDeletionRequest(DeletionRequest): # pylint: disable=too-many-ancestors + """Network deletion request class.""" + + @classmethod + def send_request(cls, + instance: "NetworkInstance", + a_la_carte: bool = True) -> "VnfDeletionRequest": + """Send request to SO to delete Network instance. + + Args: + instance (NetworkInstance): Network instance to delete + a_la_carte (boolean): deletion mode + + Returns: + NetworkDeletionRequest: Deletion request object + + """ + cls._logger.debug("Network %s deletion request", instance.network_id) + response = cls.send_message_json("DELETE", + f"Create {instance.network_id} Network deletion request", + (f"{cls.base_url}/onap/so/infra/" + f"serviceInstantiation/{cls.api_version}/" + "serviceInstances/" + f"{instance.service_instance.instance_id}/" + f"networks/{instance.network_id}"), + data=jinja_env(). + get_template("deletion_network.json.j2"). + render(network_instance=instance, + a_la_carte=a_la_carte), + headers=headers_so_creator(OnapService.headers)) + return cls(request_id=response["requestReferences"]["requestId"]) diff --git a/src/onapsdk/so/instantiation.py b/src/onapsdk/so/instantiation.py new file mode 100644 index 0000000..bb0bde2 --- /dev/null +++ b/src/onapsdk/so/instantiation.py @@ -0,0 +1,957 @@ +"""Instantion module.""" +# Copyright 2022 Orange, 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 dataclasses import dataclass, field +from typing import Any, Dict, Iterable, List, Optional +from uuid import uuid4 +from dacite import from_dict + +from onapsdk.aai.business.owning_entity import OwningEntity +from onapsdk.exceptions import ( + APIError, InvalidResponse, ParameterError, ResourceNotFound, StatusError +) +from onapsdk.onap_service import OnapService +from onapsdk.sdnc import NetworkPreload, VfModulePreload +from onapsdk.sdc.service import Network, Service as SdcService, Vnf, VfModule +from onapsdk.utils.jinja import jinja_env +from onapsdk.utils.headers_creator import headers_so_creator +from onapsdk.configuration import settings + +from .so_element import OrchestrationRequest + + +@dataclass +class Operation: + """Operation class with data about method and suffix for VnfOperation.""" + + request_method: str + request_suffix: str + + +class VnfOperation(Operation): # pylint: disable=too-few-public-methods + """Class to store possible operations' data for vnfs (request method and suffix).""" + + UPDATE = Operation("PUT", "") + HEALTHCHECK = Operation("POST", "/healthcheck") + + +@dataclass +class SoServiceVfModule: + """Class to store a VfModule instance parameters.""" + + model_name: str + instance_name: str + parameters: Dict[str, Any] = field(default_factory=dict) + processing_priority: Optional[int] = None + + +@dataclass +class SoServiceXnf: + """Class to store a Xnf instance parameters.""" + + model_name: str + instance_name: str + parameters: Dict[str, Any] = field(default_factory=dict) + processing_priority: Optional[int] = None + + @classmethod + def load(cls, data: Dict[str, Any]) -> "SoServiceVnf": + """Create a vnf instance description object from the dict. + + Useful if you keep your instance data in file. + + Returns: + SoServiceVnf: SoServiceVnf object created from the dictionary + + """ + return from_dict(data_class=cls, data=data) + + +@dataclass +class SoServiceVnf(SoServiceXnf): + """Class to store a Vnf instance parameters.""" + + vf_modules: List[SoServiceVfModule] = field(default_factory=list) + + +@dataclass +class SoServicePnf(SoServiceXnf): + """Class to store a Pnf instance parameters.""" + + +@dataclass +class SoService: + """Class to store SO Service parameters used for macro instantiation. + + Contains value list: List of vnfs to instantiate + Contains value: subscription service type + """ + + subscription_service_type: str + vnfs: List[SoServiceVnf] = field(default_factory=list) + pnfs: List[SoServicePnf] = field(default_factory=list) + instance_name: Optional[str] = None + + @classmethod + def load(cls, data: Dict[str, Any]) -> "SoService": + """Create a service instance description object from the dict. + + Useful if you keep your instance data in file. + + Returns: + SoService: SoService object created from the dictionary + + """ + return from_dict(data_class=cls, data=data) + + + +@dataclass +class VnfParameters: + """Class to store vnf parameters used for macro instantiation. + + Contains value lists: List vnf Instantiation parameters and list of + vfModule parameters + """ + + name: str + vnf_parameters: Iterable["InstantiationParameter"] = None + vfmodule_parameters: Iterable["VfmoduleParameters"] = None + +@dataclass +class VfmoduleParameters: + """Class to store vfmodule parameters used for macro instantiation. + + Contains value lists: List of vfModule parameters + """ + + name: str + vfmodule_parameters: Iterable["InstantiationParameter"] = None + + +@dataclass +class InstantiationParameter: + """Class to store instantiation parameters used for preload or macro instantiation. + + Contains two values: name of parameter and it's value + """ + + name: str + value: str + + +@dataclass +class Subnet: # pylint: disable=too-many-instance-attributes + """Class to store subnet parameters used for preload.""" + + name: str + start_address: str + gateway_address: str + role: str = None + cidr_mask: str = "24" + ip_version: str = "4" + dhcp_enabled: bool = False + dhcp_start_address: Optional[str] = None + dhcp_end_address: Optional[str] = None + + def __post_init__(self) -> None: + """Post init subnet method. + + Checks if both dhcp_start_address and dhcp_end_address values are + provided if dhcp is enabled. + + Raises: + ParameterError: Neither dhcp_start_addres + nor dhcp_end_address are provided + + """ + if self.dhcp_enabled and \ + not all([self.dhcp_start_address, + self.dhcp_end_address]): + msg = "DHCP is enabled but neither DHCP " \ + "start nor end adresses are provided." + raise ParameterError(msg) + + +class Instantiation(OrchestrationRequest, ABC): + """Abstract class used for instantiation.""" + + def __init__(self, + name: str, + request_id: str, + instance_id: str) -> None: + """Instantiate object initialization. + + Initializator used by classes inherited from this abstract class. + + Args: + name (str): instantiated object name + request_id (str): request ID + instance_id (str): instance ID + """ + super().__init__(request_id) + self.name: str = name + self.instance_id: str = instance_id + + +class VfModuleInstantiation(Instantiation): # pytest: disable=too-many-ancestors + """VF module instantiation class.""" + + def __init__(self, + name: str, + request_id: str, + instance_id: str, + vf_module: VfModule) -> None: + """Initialize class object. + + Args: + name (str): vf module name + request_id (str): request ID + instance_id (str): instance ID + vnf_instantiation (VnfInstantiation): VNF instantiation class object + vf_module (VfModule): VF module used for instantiation + """ + super().__init__(name, request_id, instance_id) + self.vf_module: VfModule = vf_module + + @classmethod + def instantiate_ala_carte(cls, # pylint: disable=too-many-arguments + vf_module: "VfModule", + vnf_instance: "VnfInstance", + cloud_region: "CloudRegion", + tenant: "Tenant", + vf_module_instance_name: str = None, + vnf_parameters: Iterable["InstantiationParameter"] = None, + use_preload: bool = True) -> "VfModuleInstantiation": + """Instantiate VF module. + + Iterate throught vf modules from service Tosca file and instantiate vf modules. + + Args: + vf_module (VfModule): VfModule to instantiate + vnf_instance (VnfInstance): VnfInstance object + cloud_region (CloudRegion, optional): Cloud region to use in instantiation request. + Defaults to None. + tenant (Tenant, optional): Tenant to use in instnatiation request. + Defaults to None. + vf_module_instance_name_factory (str, optional): Factory to create VF module names. + It's going to be a prefix of name. Index of vf module in Tosca file will be + added to it. + If no value is provided it's going to be + "Python_ONAP_SDK_vf_module_service_instance_{str(uuid4())}". + Defaults to None. + vnf_parameters (Iterable[InstantiationParameter], optional): Parameters which are + going to be used in preload upload for vf modules or passed in "userParams". + Defaults to None. + use_preload (bool, optional): This flag determines whether instantiation parameters + are used as preload or "userParams" content. Defaults to True + + Yields: + Iterator[VfModuleInstantiation]: VfModuleInstantiation class object. + + """ + if vf_module_instance_name is None: + vf_module_instance_name = \ + f"Python_ONAP_SDK_vf_module_instance_{str(uuid4())}" + if use_preload: + VfModulePreload.upload_vf_module_preload( + vnf_instance, + vf_module_instance_name, + vf_module, + vnf_parameters + ) + vnf_parameters = None + sdc_service: SdcService = vnf_instance.service_instance.sdc_service + response: dict = cls.send_message_json( + "POST", + (f"Instantiate {sdc_service.name} " + f"service vf module {vf_module.name}"), + (f"{cls.base_url}/onap/so/infra/serviceInstantiation/{cls.api_version}/" + f"serviceInstances/{vnf_instance.service_instance.instance_id}/vnfs/" + f"{vnf_instance.vnf_id}/vfModules"), + data=jinja_env().get_template("instantiate_vf_module_ala_carte.json.j2"). + render( + vf_module_instance_name=vf_module_instance_name, + vf_module=vf_module, + service=sdc_service, + cloud_region=cloud_region, + tenant=tenant, + vnf_instance=vnf_instance, + vf_module_parameters=vnf_parameters or [] + ), + headers=headers_so_creator(OnapService.headers) + ) + return VfModuleInstantiation( + name=vf_module_instance_name, + request_id=response["requestReferences"].get("requestId"), + instance_id=response["requestReferences"].get("instanceId"), + vf_module=vf_module + ) + + +class NodeTemplateInstantiation(Instantiation, ABC): # pytest: disable=too-many-ancestors + """Base class for service's node_template object instantiation.""" + + def __init__(self, # pylint: disable=too-many-arguments + name: str, + request_id: str, + instance_id: str, + line_of_business: str, + platform: str) -> None: + """Node template object initialization. + + Args: + name (str): Node template name + request_id (str): Node template instantiation request ID + instance_id (str): Node template instance ID + line_of_business (str): LineOfBusiness name + platform (str): Platform name + """ + super().__init__(name, request_id, instance_id) + self.line_of_business = line_of_business + self.platform = platform + + +class VnfInstantiation(NodeTemplateInstantiation): # pylint: disable=too-many-ancestors + """VNF instantiation class.""" + + def __init__(self, # pylint: disable=too-many-arguments + name: str, + request_id: str, + instance_id: str, + line_of_business: str, + platform: str, + vnf: Vnf) -> None: + """Class VnfInstantion object initialization. + + Args: + name (str): VNF name + request_id (str): request ID + instance_id (str): instance ID + service_instantiation ([type]): ServiceInstantiation class object + line_of_business (str): LineOfBusiness name + platform (str): Platform name + vnf (Vnf): Vnf class object + """ + super().__init__(name, request_id, instance_id, line_of_business, platform) + self.vnf = vnf + + @classmethod + def create_from_request_response(cls, request_response: dict) -> "VnfInstantiation": + """Create VNF instantiation object based on request details. + + Raises: + ResourceNotFound: Service related with given object doesn't exist + ResourceNotFound: No ServiceInstantiation related with given VNF instantiation + ResourceNotFound: VNF related with given object doesn't exist + InvalidResponse: Invalid dictionary - couldn't create VnfInstantiation object + + Returns: + VnfInstantiation: VnfInstantiation object + + """ + if request_response.get("request", {}).get("requestScope") == "vnf" and \ + request_response.get("request", {}).get("requestType") == "createInstance": + service: SdcService = None + for related_instance in request_response.get("request", {}).get("requestDetails", {})\ + .get("relatedInstanceList", []): + if related_instance.get("relatedInstance", {}).get("modelInfo", {})\ + .get("modelType") == "service": + service = SdcService(related_instance.get("relatedInstance", {})\ + .get("modelInfo", {}).get("modelName")) + if not service: + raise ResourceNotFound("No related service in Vnf instance details response") + vnf: Vnf = None + for service_vnf in service.vnfs: + if service_vnf.name == request_response.get("request", {})\ + .get("requestDetails", {}).get("modelInfo", {}).get("modelCustomizationName"): + vnf = service_vnf + if not vnf: + raise ResourceNotFound("No vnf in service vnfs list") + return cls( + name=request_response.get("request", {})\ + .get("instanceReferences", {}).get("vnfInstanceName"), + request_id=request_response.get("request", {}).get("requestId"), + instance_id=request_response.get("request", {})\ + .get("instanceReferences", {}).get("vnfInstanceId"), + line_of_business=request_response.get("request", {})\ + .get("requestDetails", {}).get("lineOfBusiness", {}).get("lineOfBusinessName"), + platform=request_response.get("request", {})\ + .get("requestDetails", {}).get("platform", {}).get("platformName"), + vnf=vnf + ) + raise InvalidResponse("Invalid vnf instantions in response dictionary's requestList") + + @classmethod + def get_by_vnf_instance_name(cls, vnf_instance_name: str) -> "VnfInstantiation": + """Get VNF instantiation request by instance name. + + Raises: + InvalidResponse: Vnf instance with given name does not contain + requestList or the requestList does not contain any details. + + Returns: + VnfInstantiation: Vnf instantiation request object + + """ + response: dict = cls.send_message_json( + "GET", + f"Check {vnf_instance_name} service instantiation status", + (f"{cls.base_url}/onap/so/infra/orchestrationRequests/{cls.api_version}?" + f"filter=vnfInstanceName:EQUALS:{vnf_instance_name}"), + headers=headers_so_creator(OnapService.headers) + ) + key = "requestList" + if not response.get(key, []): + raise InvalidResponse(f"{key} of a Vnf instance is missing.") + for details in response[key]: + return cls.create_from_request_response(details) + msg = f"No details available in response dictionary's {key}." + raise InvalidResponse(msg) + + @classmethod + def instantiate_ala_carte(cls, # pylint: disable=too-many-arguments + aai_service_instance: "ServiceInstance", + vnf_object: "Vnf", + line_of_business: str, + platform: str, + cloud_region: "CloudRegion", + tenant: "Tenant", + sdc_service: "SdcService", + vnf_instance_name: str = None, + vnf_parameters: Iterable["InstantiationParameter"] = None + ) -> "VnfInstantiation": + """Instantiate Vnf using a'la carte method. + + Args: + vnf_object (Vnf): Vnf to instantiate + line_of_business_object (LineOfBusiness): LineOfBusiness to use in instantiation request + platform_object (Platform): Platform to use in instantiation request + cloud_region (CloudRegion): Cloud region to use in instantiation request. + tenant (Tenant): Tenant to use in instnatiation request. + vnf_instance_name (str, optional): Vnf instance name. Defaults to None. + vnf_parameters (Iterable[InstantiationParameter], optional): Instantiation parameters + that are sent in the request. Defaults to None + + Returns: + VnfInstantiation: VnfInstantiation object + + """ + if vnf_instance_name is None: + vnf_instance_name = \ + f"Python_ONAP_SDK_vnf_instance_{str(uuid4())}" + response: dict = cls.send_message_json( + "POST", + (f"Instantiate {sdc_service.name} " + f"service vnf {vnf_object.name}"), + (f"{cls.base_url}/onap/so/infra/serviceInstantiation/{cls.api_version}/" + f"serviceInstances/{aai_service_instance.instance_id}/vnfs"), + data=jinja_env().get_template("instantiate_vnf_ala_carte.json.j2"). + render( + instance_name=vnf_instance_name, + vnf=vnf_object, + service=sdc_service, + cloud_region=cloud_region or \ + next(aai_service_instance.service_subscription.cloud_regions), + tenant=tenant or next(aai_service_instance.service_subscription.tenants), + line_of_business=line_of_business, + platform=platform, + service_instance=aai_service_instance, + vnf_parameters=vnf_parameters or [] + ), + headers=headers_so_creator(OnapService.headers) + ) + return VnfInstantiation( + name=vnf_instance_name, + request_id=response["requestReferences"]["requestId"], + instance_id=response["requestReferences"]["instanceId"], + line_of_business=line_of_business, + platform=platform, + vnf=vnf_object + ) + + @classmethod + def instantiate_macro(cls, # pylint: disable=too-many-arguments, too-many-locals + aai_service_instance: "ServiceInstance", + vnf_object: "Vnf", + line_of_business: str, + platform: str, + cloud_region: "CloudRegion", + tenant: "Tenant", + sdc_service: "SdcService", + vnf_instance_name: str = None, + vnf_parameters: Iterable["InstantiationParameter"] = None, + so_vnf: "SoServiceVnf" = None + ) -> "VnfInstantiation": + """Instantiate Vnf using macro method. + + Args: + aai_service_instance (ServiceInstance): Service instance associated with request + vnf_object (Vnf): Vnf to instantiate + line_of_business (LineOfBusiness): LineOfBusiness to use in instantiation request + platform (Platform): Platform to use in instantiation request + cloud_region (CloudRegion): Cloud region to use in instantiation request. + tenant (Tenant): Tenant to use in instantiation request. + vnf_instance_name (str, optional): Vnf instance name. Defaults to None. + vnf_parameters (Iterable[InstantiationParameter], optional): Instantiation parameters + that are sent in the request. Defaults to None + so_vnf (SoServiceVnf): object with vnf instance parameters + + Returns: + VnfInstantiation: VnfInstantiation object + + """ + owning_entity_id = None + project = settings.PROJECT + + for relationship in aai_service_instance.relationships: + if relationship.related_to == "owning-entity": + owning_entity_id = relationship.relationship_data.pop().get("relationship-value") + if relationship.related_to == "project": + project = relationship.relationship_data.pop().get("relationship-value") + + owning_entity = OwningEntity.get_by_owning_entity_id( + owning_entity_id=owning_entity_id) + + if so_vnf: + template_file = "instantiate_vnf_macro_so_vnf.json.j2" + if so_vnf.instance_name: + vnf_instance_name = so_vnf.instance_name + else: + template_file = "instantiate_vnf_macro.json.j2" + if vnf_instance_name is None: + vnf_instance_name = \ + f"Python_ONAP_SDK_vnf_instance_{str(uuid4())}" + + response: dict = cls.send_message_json( + "POST", + (f"Instantiate {sdc_service.name} " + f"service vnf {vnf_object.name}"), + (f"{cls.base_url}/onap/so/infra/serviceInstantiation/{cls.api_version}/" + f"serviceInstances/{aai_service_instance.instance_id}/vnfs"), + data=jinja_env().get_template(template_file).render( + instance_name=vnf_instance_name, + vnf=vnf_object, + service=sdc_service, + cloud_region=cloud_region or \ + next(aai_service_instance.service_subscription.cloud_regions), + tenant=tenant or next(aai_service_instance.service_subscription.tenants), + project=project, + owning_entity=owning_entity, + line_of_business=line_of_business, + platform=platform, + service_instance=aai_service_instance, + vnf_parameters=vnf_parameters or [], + so_vnf=so_vnf + ), + headers=headers_so_creator(OnapService.headers) + ) + + return VnfInstantiation( + name=vnf_instance_name, + request_id=response["requestReferences"]["requestId"], + instance_id=response["requestReferences"]["instanceId"], + line_of_business=line_of_business, + platform=platform, + vnf=vnf_object + ) + + @classmethod + def so_action(cls, # pylint: disable=too-many-arguments, too-many-locals + vnf_instance: "VnfInstance", + operation_type: VnfOperation, + aai_service_instance: "ServiceInstance", + line_of_business: str, + platform: str, + sdc_service: "SdcService", + so_service: "SoService" = None + ) -> "VnfInstantiation": + """Execute SO action (update or healthcheck) for selected vnf with SO macro request. + + Args: + vnf_instance (VnfInstance): vnf instance object + operation_type (VnfOperation): name of the operation to trigger + aai_service_instance (AaiService): Service Instance object from aai + line_of_business (LineOfBusiness): LineOfBusiness name to use + in instantiation request + platform (Platform): Platform name to use in instantiation request + sdc_service (SdcService): Service model information + so_service (SoService, optional): SO values to use in SO request + + Raises: + StatusError: if the provided operation is not supported + + Returns: + VnfInstantiation: VnfInstantiation object + + """ + if operation_type not in (VnfOperation.HEALTHCHECK, VnfOperation.UPDATE): + raise StatusError("Operation not supported!") + + owning_entity_id = None + project = settings.PROJECT + + for relationship in aai_service_instance.relationships: + if relationship.related_to == "owning-entity": + owning_entity_id = relationship.relationship_data.pop().get("relationship-value") + if relationship.related_to == "project": + project = relationship.relationship_data.pop().get("relationship-value") + + owning_entity = OwningEntity.get_by_owning_entity_id( + owning_entity_id=owning_entity_id) + + response: dict = cls.send_message_json( + operation_type.request_method, + (f"So Action {sdc_service.name} " + f" vnf instance {vnf_instance.vnf_id}"), + (f"{cls.base_url}/onap/so/infra/serviceInstantiation/{cls.api_version}/" + f"serviceInstances/{aai_service_instance.instance_id}/vnfs/{vnf_instance.vnf_id}" + f"{operation_type.request_suffix}"), + data=jinja_env().get_template("instantiate_multi_vnf_service_macro.json.j2").render( + sdc_service=sdc_service, + cloud_region=next(aai_service_instance.service_subscription.cloud_regions), + tenant=next(aai_service_instance.service_subscription.tenants), + customer=aai_service_instance.service_subscription.customer, + project=project, + owning_entity=owning_entity, + line_of_business=line_of_business, + platform=platform, + service_instance_name=aai_service_instance.instance_name, + so_service=so_service + ), + headers=headers_so_creator(OnapService.headers) + ) + + return VnfInstantiation( + name=vnf_instance.vnf_name, + request_id=response["requestReferences"]["requestId"], + instance_id=response["requestReferences"]["instanceId"], + line_of_business=line_of_business, + platform=platform, + vnf=vnf_instance + ) + + +class ServiceInstantiation(Instantiation): # pylint: disable=too-many-ancestors + """Service instantiation class.""" + + def __init__(self, # pylint: disable=too-many-arguments + name: str, + request_id: str, + instance_id: str, + sdc_service: "SdcService", + cloud_region: "CloudRegion", + tenant: "Tenant", + customer: "Customer", + owning_entity: OwningEntity, + project: str) -> None: + """Class ServiceInstantiation object initialization. + + Args: + name (str): service instance name + request_id (str): service instantiation request ID + instance_id (str): service instantiation ID + sdc_service (SdcService): SdcService class object + cloud_region (CloudRegion): CloudRegion class object + tenant (Tenant): Tenant class object + customer (Customer): Customer class object + owning_entity (OwningEntity): OwningEntity class object + project (str): Project name + + """ + super().__init__(name, request_id, instance_id) + self.sdc_service = sdc_service + self.cloud_region = cloud_region + self.tenant = tenant + self.customer = customer + self.owning_entity = owning_entity + self.project = project + + @classmethod + def instantiate_ala_carte(cls, # pylint: disable=too-many-arguments + sdc_service: "SdcService", + cloud_region: "CloudRegion", + tenant: "Tenant", + customer: "Customer", + owning_entity: OwningEntity, + project: str, + service_subscription: "ServiceSubscription", + service_instance_name: str = None, + enable_multicloud: bool = False) -> "ServiceInstantiation": + """Instantiate service using SO a'la carte request. + + Args: + sdc_service (SdcService): Service to instantiate + cloud_region (CloudRegion): Cloud region to use in instantiation request + tenant (Tenant): Tenant to use in instantiation request + customer (Customer): Customer to use in instantiation request + owning_entity (OwningEntity): Owning entity to use in instantiation request + project (str): Project name to use in instantiation request + service_subscription (ServiceSubscription): Customer's service subsription. + service_instance_name (str, optional): Service instance name. Defaults to None. + enable_multicloud (bool, optional): Determines if Multicloud should be enabled + for instantiation request. Defaults to False. + + Raises: + StatusError: if a service is not distributed. + + Returns: + ServiceInstantiation: instantiation request object + + """ + if not sdc_service.distributed: + msg = f"Service {sdc_service.name} is not distributed." + raise StatusError(msg) + if service_instance_name is None: + service_instance_name = f"Python_ONAP_SDK_service_instance_{str(uuid4())}" + response: dict = cls.send_message_json( + "POST", + f"Instantiate {sdc_service.name} service a'la carte", + (f"{cls.base_url}/onap/so/infra/" + f"serviceInstantiation/{cls.api_version}/serviceInstances"), + data=jinja_env().get_template("instantiate_service_ala_carte.json.j2"). + render( + sdc_service=sdc_service, + cloud_region=cloud_region, + tenant=tenant, + customer=customer, + owning_entity=owning_entity, + service_instance_name=service_instance_name, + project=project, + enable_multicloud=enable_multicloud, + service_subscription=service_subscription + ), + headers=headers_so_creator(OnapService.headers) + ) + return cls( + name=service_instance_name, + request_id=response["requestReferences"].get("requestId"), + instance_id=response["requestReferences"].get("instanceId"), + sdc_service=sdc_service, + cloud_region=cloud_region, + tenant=tenant, + customer=customer, + owning_entity=owning_entity, + project=project + ) + + # pylint: disable=too-many-arguments, too-many-locals + @classmethod + def instantiate_macro(cls, + sdc_service: "SdcService", + customer: "Customer", + owning_entity: OwningEntity, + project: str, + line_of_business: str, + platform: str, + aai_service: "AaiService" = None, + cloud_region: "CloudRegion" = None, + tenant: "Tenant" = None, + service_instance_name: str = None, + vnf_parameters: Iterable["VnfParameters"] = None, + enable_multicloud: bool = False, + so_service: "SoService" = None, + service_subscription: "ServiceSubscription" = None + ) -> "ServiceInstantiation": + """Instantiate service using SO macro request. + + Args: + sdc_service (SdcService): Service to instantiate + customer (Customer): Customer to use in instantiation request + owning_entity (OwningEntity): Owning entity to use in instantiation request + project (Project): Project name to use in instantiation request + line_of_business_object (LineOfBusiness): LineOfBusiness name to use + in instantiation request + platform_object (Platform): Platform name to use in instantiation request + aai_service (AaiService): Service object from aai sdc + cloud_region (CloudRegion): Cloud region to use in instantiation request + tenant (Tenant): Tenant to use in instantiation request + service_instance_name (str, optional): Service instance name. Defaults to None. + vnf_parameters: (Iterable[VnfParameters]): Parameters which are + going to be used for vnfs instantiation. Defaults to None. + enable_multicloud (bool, optional): Determines if Multicloud should be enabled + for instantiation request. Defaults to False. + so_service (SoService, optional): SO values to use in instantiation request + service_subscription(ServiceSubscription, optional): Customer service subscription + for the instantiated service. Required if so_service is not provided. + + Raises: + StatusError: if a service is not distributed. + + Returns: + ServiceInstantiation: instantiation request object + + """ + template_file = "instantiate_service_macro.json.j2" + if so_service: + template_file = "instantiate_multi_vnf_service_macro.json.j2" + if so_service.instance_name: + service_instance_name = so_service.instance_name + else: + if not service_subscription: + raise ParameterError("If no so_service is provided, " + "service_subscription parameter is required!") + if service_instance_name is None: + service_instance_name = f"Python_ONAP_SDK_service_instance_{str(uuid4())}" + if not sdc_service.distributed: + msg = f"Service {sdc_service.name} is not distributed." + raise StatusError(msg) + + response: dict = cls.send_message_json( + "POST", + f"Instantiate {sdc_service.name} service macro", + (f"{cls.base_url}/onap/so/infra/" + f"serviceInstantiation/{cls.api_version}/serviceInstances"), + data=jinja_env().get_template(template_file). \ + render( + so_service=so_service, + sdc_service=sdc_service, + cloud_region=cloud_region, + tenant=tenant, + customer=customer, + owning_entity=owning_entity, + project=project, + aai_service=aai_service, + line_of_business=line_of_business, + platform=platform, + service_instance_name=service_instance_name, + vnf_parameters=vnf_parameters, + enable_multicloud=enable_multicloud, + service_subscription=service_subscription + ), + headers=headers_so_creator(OnapService.headers) + ) + return cls( + name=service_instance_name, + request_id=response["requestReferences"].get("requestId"), + instance_id=response["requestReferences"].get("instanceId"), + sdc_service=sdc_service, + cloud_region=cloud_region, + tenant=tenant, + customer=customer, + owning_entity=owning_entity, + project=project + ) + + @property + def aai_service_instance(self) -> "ServiceInstance": + """Service instance associated with service instantiation request. + + Raises: + StatusError: if a service is not instantiated - + not in COMPLETE status. + APIError: A&AI resource is not created + + Returns: + ServiceInstance: ServiceInstance + + """ + required_status = self.StatusEnum.COMPLETED + if self.status != required_status: + msg = (f"Service {self.name} is not instantiated - " + f"not in {required_status} status.") + raise StatusError(msg) + try: + service_subscription: "ServiceSubscription" = \ + self.customer.get_service_subscription_by_service_type(self.sdc_service.name) + return service_subscription.get_service_instance_by_name(self.name) + except APIError as exc: + self._logger.error("A&AI resources not created properly") + raise exc + + +class NetworkInstantiation(NodeTemplateInstantiation): # pylint: disable=too-many-ancestors + """Network instantiation class.""" + + def __init__(self, # pylint: disable=too-many-arguments + name: str, + request_id: str, + instance_id: str, + line_of_business: str, + platform: str, + network: Network) -> None: + """Class NetworkInstantiation object initialization. + + Args: + name (str): VNF name + request_id (str): request ID + instance_id (str): instance ID + service_instantiation ([type]): ServiceInstantiation class object + line_of_business (str): LineOfBusiness name + platform (str): Platform name + vnf (Network): Network class object + """ + super().__init__(name, request_id, instance_id, line_of_business, platform) + self.network = network + + @classmethod + def instantiate_ala_carte(cls, # pylint: disable=too-many-arguments + aai_service_instance: "ServiceInstance", + network_object: "Network", + line_of_business: str, + platform: str, + cloud_region: "CloudRegion", + tenant: "Tenant", + network_instance_name: str = None, + subnets: Iterable[Subnet] = None) -> "NetworkInstantiation": + """Instantiate Network using a'la carte method. + + Args: + network_object (Network): Network to instantiate + line_of_business (str): LineOfBusiness name to use in instantiation request + platform (str): Platform name to use in instantiation request + cloud_region (CloudRegion): Cloud region to use in instantiation request. + tenant (Tenant): Tenant to use in instnatiation request. + network_instance_name (str, optional): Network instance name. Defaults to None. + + Returns: + NetworkInstantiation: NetworkInstantiation object + + """ + if network_instance_name is None: + network_instance_name = \ + f"Python_ONAP_SDK_network_instance_{str(uuid4())}" + NetworkPreload.upload_network_preload(network=network_object, + network_instance_name=network_instance_name, + subnets=subnets) + response: dict = cls.send_message_json( + "POST", + (f"Instantiate {aai_service_instance.sdc_service.name} " + f"service network {network_object.name}"), + (f"{cls.base_url}/onap/so/infra/serviceInstantiation/{cls.api_version}/" + f"serviceInstances/{aai_service_instance.instance_id}/networks"), + data=jinja_env().get_template("instantiate_network_ala_carte.json.j2"). + render( + instance_name=network_instance_name, + network=network_object, + service=aai_service_instance.sdc_service, + cloud_region=cloud_region or \ + next(aai_service_instance.service_subscription.cloud_regions), + tenant=tenant or next(aai_service_instance.service_subscription.tenants), + line_of_business=line_of_business, + platform=platform, + service_instance=aai_service_instance, + subnets=subnets + ), + headers=headers_so_creator(OnapService.headers) + ) + return cls( + name=network_instance_name, + request_id=response["requestReferences"]["requestId"], + instance_id=response["requestReferences"]["instanceId"], + line_of_business=line_of_business, + platform=platform, + network=network_object + ) diff --git a/src/onapsdk/so/so_db_adapter.py b/src/onapsdk/so/so_db_adapter.py new file mode 100644 index 0000000..b3694d1 --- /dev/null +++ b/src/onapsdk/so/so_db_adapter.py @@ -0,0 +1,94 @@ +"""Database Adapter module.""" +# Copyright 2022 Orange, 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 dataclasses import dataclass +from typing import Dict, Any + +from onapsdk.so.so_element import SoElement +from onapsdk.onap_service import OnapService +from onapsdk.utils.headers_creator import headers_so_creator, headers_so_catelog_db_creator +from onapsdk.utils.jinja import jinja_env + + +@dataclass +class IdentityService: # pylint: disable=too-many-instance-attributes + """Class to store identity service details.""" + + identity_id: str + url: str = "http://1.2.3.4:5000/v2.0" + mso_id: str = "onapsdk_user" + mso_pass: str = "mso_pass_onapsdk" + project_domain_name: str = "NULL" + user_domain_name: str = "NULL" + admin_tenant: str = "service" + member_role: str = "admin" + identity_server_type: str = "KEYSTONE" + identity_authentication_type: str = "USERNAME_PASSWORD" + hibernate_lazy_initializer = {} + server_type_as_string: str = "KEYSTONE" + tenant_metadata: bool = True + + +class SoDbAdapter(SoElement, ABC): + """DB Adapter class.""" + + @classmethod + def add_cloud_site(cls, + cloud_region_id: str, + complex_id: str, + identity_service: IdentityService, + orchestrator: str = "multicloud" + ): + """Add cloud_site data with identity_service to SO db. + + Args: + cloud_region_id (str): The id of cloud region + complex_id (str): The id of complex + identity_service (IdentityService): Identity service related to the cloud region + orchestrator (str, optional): Orchestrator type. Defaults to multicloud. + + Important: + identity_services data will be overwrite, but in the same time + cloud_sites data will not (shouldn't) be overwrite! + SOCatalogDB REST API has some limitations reported: https://jira.onap.org/browse/SO-2727 + + Return: + response object + """ + response = cls.send_message_json( + "POST", + "Create a region in SO db", + f"{cls.base_url}/cloudSite", + data=jinja_env().get_template("add_cloud_site_with_identity_service.json.j2"). + render( + cloud_region_id=cloud_region_id, + complex_id=complex_id, + orchestrator=orchestrator, + identity_service=identity_service + ), + headers=headers_so_creator(OnapService.headers) + ) + return response + @classmethod + def get_service_vnf_info(cls, identifier: str) -> Dict[Any, Any]: + """Get Service VNF and VF details. + + Returns: + The response in a dict format + + """ + url = f"{cls.base_url}/ecomp/mso/catalog/v2/serviceVnfs?serviceModelUuid={identifier}" + headers = headers_so_catelog_db_creator(OnapService.headers) + return cls.send_message_json("GET", "Get Service Details", url, headers=headers) diff --git a/src/onapsdk/so/so_element.py b/src/onapsdk/so/so_element.py new file mode 100644 index 0000000..fca6ba7 --- /dev/null +++ b/src/onapsdk/so/so_element.py @@ -0,0 +1,223 @@ +"""SO Element module.""" +# Copyright 2022 Orange, 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 json +from abc import ABC +from dataclasses import dataclass +from enum import Enum +from typing import Dict + +from onapsdk.configuration import settings +from onapsdk.sdc.service import Service +from onapsdk.sdc.vf import Vf +from onapsdk.onap_service import OnapService +from onapsdk.utils.headers_creator import headers_so_creator +from onapsdk.utils.jinja import jinja_env +from onapsdk.utils.mixins import WaitForFinishMixin +from onapsdk.utils.tosca_file_handler import get_modules_list_from_tosca_file +from onapsdk.utils.gui import GuiItem, GuiList + +@dataclass +class SoElement(OnapService): + """Mother Class of all SO elements.""" + + name: str = None + _server: str = "SO" + base_url = settings.SO_URL + api_version = settings.SO_API_VERSION + _status: str = None + + @property + def headers(self): + """Create headers for SO request. + + It is used as a property because x-transactionid header should be unique for each request. + """ + return headers_so_creator(OnapService.headers) + + @classmethod + def get_subscription_service_type(cls, vf_name): + """Retrieve the model info of the VFs.""" + vf_object = Vf(name=vf_name) + return vf_object.name + + @classmethod + def get_service_model_info(cls, service_name): + """Retrieve Service Model info.""" + service = Service(name=service_name) + template_service = jinja_env().get_template("service_instance_model_info.json.j2") + # Get service instance model + parsed = json.loads( + template_service.render( + model_invariant_id=service.unique_uuid, + model_name_version_id=service.identifier, + model_name=service.name, + model_version=service.version, + ) + ) + return json.dumps(parsed, indent=4) + + @classmethod + def get_vnf_model_info(cls, vf_name): + """Retrieve the model info of the VFs.""" + vf_object = Vf(name=vf_name) + template_service = jinja_env().get_template("vnf_model_info.json.j2") + parsed = json.loads( + template_service.render( + vnf_model_invariant_uuid=vf_object.unique_uuid, + vnf_model_customization_id="????", + vnf_model_version_id=vf_object.identifier, + vnf_model_name=vf_object.name, + vnf_model_version=vf_object.version, + vnf_model_instance_name=(vf_object.name + " 0"), + ) + ) + # we need also a vnf instance Name + # Usually it is found like that + # name: toto + # instance name: toto 0 + # it can be retrieved from the toscafrom onapsdk.configuration import settings + return json.dumps(parsed, indent=4) + + @classmethod + def get_vf_model_info(cls, vf_model: str) -> str: + """Retrieve the VF model info From Tosca?.""" + modules: Dict = get_modules_list_from_tosca_file(vf_model) + template_service = jinja_env().get_template("vf_model_info.json.j2") + parsed = json.loads(template_service.render(modules=modules)) + return json.dumps(parsed, indent=4) + + @classmethod + def _base_create_url(cls) -> str: + """ + Give back the base url of SO. + + Returns: + str: the base url + + """ + return "{}/onap/so/infra/serviceInstantiation/{}/serviceInstances".format( + cls.base_url, cls.api_version + ) + + @classmethod + def get_guis(cls) -> GuiItem: + """Retrieve the status of the SO GUIs. + + Only one GUI is referenced for SO: SO monitor + + Return the list of GUIs + """ + gui_url = settings.SO_MONITOR_GUI_SERVICE + so_gui_response = cls.send_message( + "GET", "Get SO GUI Status", gui_url) + guilist = GuiList([]) + guilist.add(GuiItem( + gui_url, + so_gui_response.status_code)) + return guilist + + +class OrchestrationRequest(SoElement, WaitForFinishMixin, ABC): + """Base SO orchestration request class.""" + + WAIT_FOR_SLEEP_TIME = 10 + + def __init__(self, + request_id: str) -> None: + """Instantiate object initialization. + + Initializator used by classes inherited from this abstract class. + + Args: + request_id (str): request ID + """ + super().__init__() + self.request_id: str = request_id + + class StatusEnum(Enum): + """Status enum. + + Store possible statuses for instantiation: + - IN_PROGRESS, + - FAILED, + - COMPLETE. + If instantiation has status which is not covered by these values + UNKNOWN value is used. + + """ + + IN_PROGRESS = "IN_PROGRESS" + FAILED = "FAILED" + COMPLETED = "COMPLETE" + UNKNOWN = "UNKNOWN" + + @property + def status(self) -> "StatusEnum": + """Object instantiation status. + + It's populated by call SO orchestation request endpoint. + + Returns: + StatusEnum: Instantiation status. + + """ + response: dict = self.send_message_json( + "GET", + f"Check {self.request_id} orchestration request status", + (f"{self.base_url}/onap/so/infra/" + f"orchestrationRequests/{self.api_version}/{self.request_id}"), + headers=headers_so_creator(OnapService.headers) + ) + try: + return self.StatusEnum(response["request"]["requestStatus"]["requestState"]) + except (KeyError, ValueError): + self._logger.exception("Invalid status") + return self.StatusEnum.UNKNOWN + + @property + def finished(self) -> bool: + """Store an information if instantion is finished or not. + + Instantiation is finished if it's status is COMPLETED or FAILED. + + Returns: + bool: True if instantiation is finished, False otherwise. + + """ + return self.status in [self.StatusEnum.COMPLETED, self.StatusEnum.FAILED] + + @property + def completed(self) -> bool: + """Store an information if instantion is completed or not. + + Instantiation is completed if it's status is COMPLETED. + + Returns: + bool: True if instantiation is completed, False otherwise. + + """ + return self.finished and self.status == self.StatusEnum.COMPLETED + + @property + def failed(self) -> bool: + """Store an information if instantion is failed or not. + + Instantiation is failed if it's status is FAILED. + + Returns: + bool: True if instantiation is failed, False otherwise. + + """ + return self.finished and self.status == self.StatusEnum.FAILED diff --git a/src/onapsdk/so/templates/add_cloud_site_with_identity_service.json.j2 b/src/onapsdk/so/templates/add_cloud_site_with_identity_service.json.j2 new file mode 100644 index 0000000..31599f5 --- /dev/null +++ b/src/onapsdk/so/templates/add_cloud_site_with_identity_service.json.j2 @@ -0,0 +1,22 @@ +{ + "id": "{{ cloud_region_id }}", + "region_id": "{{ cloud_region_id }}", + "aic_version": "2.5", + "clli": "{{ complex_id }}", + "orchestrator": "{{ orchestrator }}", + "identityService": { + "id": "{{ identity_service.identity_id }}", + "identityServerTypeAsString": "{{ identity_service.server_type_as_string }}", + "hibernateLazyInitializer": {{ identity_service.hibernate_lazy_initializer }}, + "identity_url": "{{ identity_service.url }}", + "mso_id": "{{ identity_service.mso_id }}", + "mso_pass": "{{ identity_service.mso_pass }}", + "project_domain_name": "{{ identity_service.project_domain_name }}", + "user_domain_name": "{{ identity_service.user_domain_name }}", + "admin_tenant": "{{ identity_service.admin_tenant }}", + "member_role": "{{ identity_service.member_role }}", + "tenant_metadata": "{{ identity_service.tenant_metadata }}", + "identity_server_type": "{{ identity_service.identity_server_type }}", + "identity_authentication_type": "{{ identity_service.identity_authentication_type }}" + } +}
\ No newline at end of file diff --git a/src/onapsdk/so/templates/deletion_network.json.j2 b/src/onapsdk/so/templates/deletion_network.json.j2 new file mode 100644 index 0000000..93f0990 --- /dev/null +++ b/src/onapsdk/so/templates/deletion_network.json.j2 @@ -0,0 +1,22 @@ +{ + "requestDetails": { + "requestInfo": { + "source": "VID", + "requestorId": "demo" + }, + "modelInfo": { + "modelType": "network" + }, + "requestParameters": { + "testApi": "GR_API", + "aLaCarte": {{ a_la_carte | tojson }} + }, + {# the code below is needed to be refactored #} + {# https://gitlab.com/Orange-OpenSource/lfn/onap/python-onapsdk/-/issues/133 #} + "cloudConfiguration": { + "cloudOwner": "{{ network_instance.service_instance.service_subscription.cloud_region.cloud_owner }}", + "lcpCloudRegionId": "{{ network_instance.service_instance.service_subscription.cloud_region.cloud_region_id }}", + "tenantId": "{{ network_instance.service_instance.service_subscription.tenant.tenant_id }}" + } + } +}
\ No newline at end of file diff --git a/src/onapsdk/so/templates/deletion_service.json.j2 b/src/onapsdk/so/templates/deletion_service.json.j2 new file mode 100644 index 0000000..1244e97 --- /dev/null +++ b/src/onapsdk/so/templates/deletion_service.json.j2 @@ -0,0 +1,26 @@ +{ + "requestDetails": { + "requestInfo": { + "source": "VID", + "requestorId": "demo" + }, + "modelInfo": { + "modelType": "service", + "modelName": "{{ service_instance.sdc_service.name }}", + "modelInvariantId": "{{ service_instance.sdc_service.unique_uuid }}", + "modelVersion": "1.0", + "modelVersionId": "{{ service_instance.sdc_service.identifier }}" + }, + "requestParameters": { + "testApi": "GR_API", + "aLaCarte": {{ a_la_carte | tojson }} + }{% if service_instance.sdc_service.has_vnfs %}, + {# the code below is needed to be refactored #} + {# https://gitlab.com/Orange-OpenSource/lfn/onap/python-onapsdk/-/issues/133 #} + "cloudConfiguration": { + "cloudOwner": "{{ service_instance.service_subscription.cloud_region.cloud_owner }}", + "lcpCloudRegionId": "{{ service_instance.service_subscription.cloud_region.cloud_region_id }}", + "tenantId": "{{ service_instance.service_subscription.tenant.tenant_id }}" + }{% endif %} + } +}
\ No newline at end of file diff --git a/src/onapsdk/so/templates/deletion_vf_module.json.j2 b/src/onapsdk/so/templates/deletion_vf_module.json.j2 new file mode 100644 index 0000000..8f83717 --- /dev/null +++ b/src/onapsdk/so/templates/deletion_vf_module.json.j2 @@ -0,0 +1,27 @@ +{ + "requestDetails": { + "requestInfo": { + "source": "VID", + "requestorId": "demo" + }, + "modelInfo": { + "modelType": "vfModule", + "modelInvariantId": "{{ vf_module_instance.model_invariant_id }}", + "modelVersionId": "{{ vf_module_instance.model_version_id }}", + "modelName": "{{ vf_module_instance.vf_module_name }}", + "modelVersion": "{{ vf_module_instance.resource_version }}", + "modelCustomizationId": "{{ vf_module_instance.model_customization_id }}" + }, + "requestParameters": { + "testApi": "GR_API", + "aLaCarte": {{ a_la_carte | tojson }} + }, + {# the code below is needed to be refactored #} + {# https://gitlab.com/Orange-OpenSource/lfn/onap/python-onapsdk/-/issues/133 #} + "cloudConfiguration": { + "cloudOwner": "{{ vf_module_instance.vnf_instance.service_instance.service_subscription.cloud_region.cloud_owner }}", + "lcpCloudRegionId": "{{ vf_module_instance.vnf_instance.service_instance.service_subscription.cloud_region.cloud_region_id }}", + "tenantId": "{{ vf_module_instance.vnf_instance.service_instance.service_subscription.tenant.tenant_id }}" + } + } +}
\ No newline at end of file diff --git a/src/onapsdk/so/templates/deletion_vnf.json.j2 b/src/onapsdk/so/templates/deletion_vnf.json.j2 new file mode 100644 index 0000000..fb640ec --- /dev/null +++ b/src/onapsdk/so/templates/deletion_vnf.json.j2 @@ -0,0 +1,28 @@ +{ + "requestDetails": { + "requestInfo": { + "source": "VID", + "requestorId": "demo" + }, + "modelInfo": { + "modelType": "vnf", + "modelName": "{{ vnf_instance.vnf.model_name }}", + "modelInvariantId": "{{ vnf_instance.vnf.model_invariant_id }}", + "modelVersion": "{{ vnf_instance.vnf.model_version }}", + "modelVersionId": "{{ vnf_instance.vnf.model_version_id }}", + "modelCustomizationId": "{{ vnf_instance.vnf.model_customization_id }}", + "modelCustomizationName": "{{ vnf_instance.vnf.name }}" + }, + "requestParameters": { + "testApi": "GR_API", + "aLaCarte": {{ a_la_carte | tojson }} + }, + {# the code below is needed to be refactored #} + {# https://gitlab.com/Orange-OpenSource/lfn/onap/python-onapsdk/-/issues/133 #} + "cloudConfiguration": { + "cloudOwner": "{{ vnf_instance.service_instance.service_subscription.cloud_region.cloud_owner }}", + "lcpCloudRegionId": "{{ vnf_instance.service_instance.service_subscription.cloud_region.cloud_region_id }}", + "tenantId": "{{ vnf_instance.service_instance.service_subscription.tenant.tenant_id }}" + } + } +}
\ No newline at end of file diff --git a/src/onapsdk/so/templates/instantiate_multi_vnf_service_macro.json.j2 b/src/onapsdk/so/templates/instantiate_multi_vnf_service_macro.json.j2 new file mode 100644 index 0000000..32e1b68 --- /dev/null +++ b/src/onapsdk/so/templates/instantiate_multi_vnf_service_macro.json.j2 @@ -0,0 +1,121 @@ +{% extends "instantiate_service_macro.json.j2" %} + +{% block subscriptionServiceType %} + "subscriptionServiceType": "{{ so_service.subscription_service_type }}", +{% endblock %} + +{% block pnfs %} + {% if so_service.pnfs %} + "pnfs": [ + {% for pnf in so_service.pnfs %} + { + "modelInfo":{ + {% for sdc_pnf in sdc_service.pnfs %} + {% if sdc_pnf.model_name == pnf.model_name %} + "modelCustomizationName":"{{ sdc_pnf.name }}", + "modelCustomizationId":"{{ sdc_pnf.model_customization_id }}", + "modelInvariantId":"{{ sdc_service.unique_uuid }}", + "modelVersionId":"{{ sdc_service.identifier }}", + "modelName":"{{ sdc_service.name }}", + "modelType":"pnf", + "modelVersion":"{{ sdc_pnf.model_version }}" + {% endif %} + {% endfor %} + }, + "platform":{ + "platformName":"{{ platform }}" + }, + "lineOfBusiness":{ + "lineOfBusinessName":"{{ line_of_business }}" + }, + "productFamilyId":"{{ aai_service.service_id }}", + "instanceParams":[], + {% if pnf.processing_priority %} + "processingPriority": "{{ pnf.processing_priority }}", + {% endif %} + "instanceName": "{{ pnf.instance_name }}" + }{% if not loop.last %},{% endif %} + {% endfor %} + ]{% if so_service.vnfs %},{% endif %} + {% endif %} +{% endblock %} + +{% block vnfs %} + {% if so_service.vnfs %} + "vnfs": [ + {% for vnf in so_service.vnfs %} + { + "modelInfo": { + {% for sdc_vnf in sdc_service.vnfs %} + {% if sdc_vnf.model_name == vnf.model_name %} + "modelName": "{{ sdc_vnf.model_name }}", + "modelVersionId": "{{ sdc_vnf.model_version_id }}", + "modelInvariantUuid": "{{ sdc_vnf.model_invariant_uuid }}", + "modelVersion": "{{ sdc_vnf.model_version }}", + "modelCustomizationId": "{{ sdc_vnf.model_customization_id }}", + "modelInstanceName": "{{ sdc_vnf.model_name }}" + {% endif %} + {% endfor %} + }, + "cloudConfiguration": { + "tenantId": "{{ tenant.tenant_id }}", + "cloudOwner": "{{ cloud_region.cloud_owner }}", + "lcpCloudRegionId": "{{ cloud_region.cloud_region_id }}" + }, + "platform": { + "platformName": "{{ platform }}" + }, + "lineOfBusiness": { + "lineOfBusinessName": "{{ line_of_business }}" + }, + "productFamilyId": "1234", + "instanceName": "{{ vnf.instance_name }}", + "instanceParams": [ + { + {% for key, value in vnf.parameters.items() %} + "{{ key }}": "{{ value }}"{% if not loop.last %},{% endif %} + {% endfor %} + } + ], + {% if vnf.processing_priority %} + "processingPriority": "{{ vnf.processing_priority }}", + {% endif %} + "vfModules": [ + {% for vf_module in vnf.vf_modules %} + { + "modelInfo": { + {% for sdc_vnf in sdc_service.vnfs %} + {% if sdc_vnf.model_name == vnf.model_name %} + {% for sdc_vf_module in sdc_vnf.vf_modules %} + {% set mylist = sdc_vf_module.name.split('..') %} + {% set item = mylist|length-2 %} + {% if vf_module.model_name == mylist[item] %} + "modelName": "{{ sdc_vf_module.model_name }}", + "modelVersionId": "{{ sdc_vf_module.model_version_id }}", + "modelInvariantUuid": "{{ sdc_vf_module.model_invariant_uuid }}", + "modelVersion": "{{ sdc_vf_module.model_version }}", + "modelCustomizationId": "{{ sdc_vf_module.model_customization_id }}" + {% endif %} + {% endfor %} + {% endif %} + {% endfor %} + }, + "instanceName": "{{ vf_module.instance_name }}", + {% if vf_module.processing_priority %} + "processingPriority": "{{ vf_module.processing_priority }}", + {% endif %} + "instanceParams": [ + { + {% for key, value in vf_module.parameters.items() %} + "{{ key }}": "{{ value }}"{% if not loop.last %},{% endif %} + {% endfor %} + } + ] + }{% if not loop.last %},{% endif %} + {% endfor %} + ] + }{% if not loop.last %},{% endif %} + {% endfor %} + ] + {% endif %} +{% endblock %} diff --git a/src/onapsdk/so/templates/instantiate_network_ala_carte.json.j2 b/src/onapsdk/so/templates/instantiate_network_ala_carte.json.j2 new file mode 100644 index 0000000..90b3c16 --- /dev/null +++ b/src/onapsdk/so/templates/instantiate_network_ala_carte.json.j2 @@ -0,0 +1,10 @@ +{% extends "instantiate_network_vnf_ala_carte_base.json.j2" %} +{% block model_info %} + "modelType": "network", + "modelInvariantId": "{{ network.model_invariant_id }}", + "modelVersionId": "{{ network.model_version_id }}", + "modelName": "{{ network.model_name }}", + "modelVersion": "{{ network.model_version }}", + "modelCustomizationId": "{{ network.model_customization_id }}", + "modelCustomizationName": "{{ network.name }}" +{% endblock %}
\ No newline at end of file diff --git a/src/onapsdk/so/templates/instantiate_network_vnf_ala_carte_base.json.j2 b/src/onapsdk/so/templates/instantiate_network_vnf_ala_carte_base.json.j2 new file mode 100644 index 0000000..757cdd8 --- /dev/null +++ b/src/onapsdk/so/templates/instantiate_network_vnf_ala_carte_base.json.j2 @@ -0,0 +1,44 @@ +{ + "requestDetails": { + "requestInfo": { + "instanceName": "{{ instance_name }}", + "source": "VID", + "suppressRollback": false, + "requestorId": "test", + "productFamilyId": "{{ service_instance.model_invariant_id }}" + }, + "modelInfo": { + {% block model_info %}{% endblock %} + }, + "requestParameters": { + "userParams": [ + {% block user_params %}{% endblock %} + ], + "aLaCarte": true, + "testApi": "GR_API" + }, + "cloudConfiguration": { + "tenantId": "{{ tenant.tenant_id }}", + "cloudOwner": "{{ cloud_region.cloud_owner }}", + "lcpCloudRegionId": "{{ cloud_region.cloud_region_id }}" + }, + "lineOfBusiness": { + "lineOfBusinessName": "{{ line_of_business }}" + }, + "platform": { + "platformName": "{{ platform }}" + }, + "relatedInstanceList": [{ + "relatedInstance": { + "instanceId": "{{ service_instance.instance_id }}", + "modelInfo": { + "modelType": "service", + "modelName": "{{ service.name }}", + "modelInvariantId": "{{ service.unique_uuid }}", + "modelVersion": "1.0", + "modelVersionId": "{{ service.identifier }}" + } + } + }] + } +}
\ No newline at end of file diff --git a/src/onapsdk/so/templates/instantiate_service_ala_carte.json.j2 b/src/onapsdk/so/templates/instantiate_service_ala_carte.json.j2 new file mode 100644 index 0000000..4954cde --- /dev/null +++ b/src/onapsdk/so/templates/instantiate_service_ala_carte.json.j2 @@ -0,0 +1,45 @@ +{ + "requestDetails": { + "requestInfo": { + "instanceName": "{{ service_instance_name }}", + "source": "VID", + "suppressRollback": false, + "requestorId": "demo" + }, + "modelInfo": { + "modelType": "service", + "modelInvariantId": "{{ sdc_service.unique_uuid }}", + "modelVersionId": "{{ sdc_service.identifier }}", + "modelName": "{{ sdc_service.name }}", + "modelVersion": "1.0" + }, + "cloudConfiguration": { + "tenantId": "{{ tenant.tenant_id }}", + "cloudOwner": "{{ cloud_region.cloud_owner }}", + "lcpCloudRegionId": "{{ cloud_region.cloud_region_id }}" + }, + "requestParameters": { + "userParams": [ + {% if enable_multicloud %} + { + "name":"orchestrator", + "value":"multicloud" + } + {% endif %} + ], + "testApi": "GR_API", + "subscriptionServiceType": "{{ service_subscription.service_type }}", + "aLaCarte": true + }, + "subscriberInfo": { + "globalSubscriberId": "{{ customer.global_customer_id }}" + }, + "project": { + "projectName": "{{ project }}" + }, + "owningEntity": { + "owningEntityId": "{{ owning_entity.owning_entity_id }}", + "owningEntityName": "{{ owning_entity.name }}" + } + } +}
\ No newline at end of file diff --git a/src/onapsdk/so/templates/instantiate_service_macro.json.j2 b/src/onapsdk/so/templates/instantiate_service_macro.json.j2 new file mode 100644 index 0000000..43b92ee --- /dev/null +++ b/src/onapsdk/so/templates/instantiate_service_macro.json.j2 @@ -0,0 +1,173 @@ +{ + "requestDetails": { + "requestInfo": { + "suppressRollback": false, + {% if aai_service %} + "productFamilyId":"{{ aai_service.service_id }}", + {% else %} + "productFamilyId": "1234", + {% endif %} + "requestorId": "demo", + "instanceName": "{{ service_instance_name }}", + "source": "VID" + }, + "modelInfo": { + "modelType": "service", + "modelInvariantId": "{{ sdc_service.unique_uuid }}", + "modelVersionId": "{{ sdc_service.identifier }}", + "modelName": "{{ sdc_service.name }}", + "modelVersion": "1.0" + }, + {% if sdc_service.has_vnfs %} + "cloudConfiguration": { + "tenantId": "{{ tenant.tenant_id }}", + "cloudOwner": "{{ cloud_region.cloud_owner }}", + "lcpCloudRegionId": "{{ cloud_region.cloud_region_id }}" + }, + {% endif %} + "subscriberInfo": { + "globalSubscriberId": "{{ customer.global_customer_id }}" + }, + "requestParameters": { + {% block subscriptionServiceType %} + "subscriptionServiceType": "{{ service_subscription.service_type }}", + {% endblock %} + "userParams": [ + { + "Homing_Solution": "none" + }, + {% if enable_multicloud %} + { + "name":"orchestrator", + "value":"multicloud" + }, + {% endif %} + { + "service": { + "instanceParams": [], + "instanceName": "{{ service_instance_name }}", + "resources": { + {% block pnfs %} + {% if sdc_service.pnfs %} + "pnfs":[ + {% for pnf in sdc_service.pnfs %} + { + "modelInfo":{ + "modelCustomizationName":"{{ pnf.name }}", + "modelCustomizationId":"{{ pnf.model_customization_id }}", + "modelInvariantId":"{{ sdc_service.unique_uuid }}", + "modelVersionId":"{{ sdc_service.identifier }}", + "modelName":"{{ sdc_service.name }}", + "modelType":"pnf", + "modelVersion":"1.0" + }, + "platform":{ + "platformName":"{{ platform }}" + }, + "lineOfBusiness":{ + "lineOfBusinessName":"{{ line_of_business }}" + }, + "productFamilyId":"{{ aai_service.service_id }}", + "instanceParams":[], + "instanceName":"{{ service_instance_name }}" + }{% if not loop.last %},{% endif %} + {% endfor %} + ] + {% if sdc_service.vnfs %},{% endif %} + {% endif %} + {% endblock %} + {% block vnfs %} + {% if sdc_service.vnfs %} + "vnfs": [ + {% for vnf in sdc_service.vnfs %} + { + "modelInfo": { + "modelName": "{{ vnf.model_name }}", + "modelVersionId": "{{ vnf.model_version_id }}", + "modelInvariantUuid": "{{ vnf.model_invariant_id }}", + "modelVersion": "{{ vnf.model_version }}", + "modelCustomizationId": "{{ vnf.model_customization_id }}", + "modelInstanceName": "{{ vnf.model_name }}" + }, + "cloudConfiguration": { + "tenantId": "{{ tenant.tenant_id }}", + "cloudOwner": "{{ cloud_region.cloud_owner }}", + "lcpCloudRegionId": "{{ cloud_region.cloud_region_id }}" + }, + "platform": { + "platformName": "{{ platform }}" + }, + "lineOfBusiness": { + "lineOfBusinessName": "{{ line_of_business }}" + }, + "productFamilyId": "1234", + "instanceName": "{{ vnf.model_name }}", + "instanceParams": [ + { + {% for vnf_parameter in vnf_parameters %} + {% if vnf_parameter.name == vnf.model_name %} + {% for parameter in vnf_parameter.vnf_parameters %} + "{{ parameter.name }}": "{{ parameter.value }}"{% if not loop.last %},{% endif %} + {% endfor %} + {% endif %} + {% endfor %} + } + ], + "vfModules": [ + {% for vf_module in vnf.vf_modules %} + { + "modelInfo": { + "modelName": "{{ vf_module.model_name }}", + "modelVersionId": "{{ vf_module.model_version_id }}", + "modelInvariantUuid": "{{ vf_module.model_invariant_uuid }}", + "modelVersion": "{{ vf_module.model_version }}", + "modelCustomizationId": "{{ vf_module.model_customization_id }}" + }, + "instanceName": "{{ service_instance_name }}_{{ vf_module.name }}", + "instanceParams": [ + { + {% for vnf_parameter in vnf_parameters %} + {% if vnf_parameter.name == vnf.model_name %} + {% set mylist = vf_module.name.split('..') %} + {% set item = mylist|length-2 %} + {% for vf_module_parameter in vnf_parameter.vfmodule_parameters %} + {% if vf_module_parameter.name == mylist[item] %} + {% for parameter in vf_module_parameter.vfmodule_parameters %} + "{{ parameter.name }}": "{{ parameter.value }}"{% if not loop.last %},{% endif %} + {% endfor %} + {% endif %} + {% endfor %} + {% endif %} + {% endfor %} + } + ] + }{% if not loop.last %},{% endif %} + {% endfor %} + ] + }{% if not loop.last %},{% endif %} + {% endfor %} + ] + {% endif %} + {% endblock %} + }, + "modelInfo": { + "modelVersion": "1.0", + "modelVersionId": "{{ sdc_service.identifier }}", + "modelInvariantId": "{{ sdc_service.unique_uuid }}", + "modelName": "{{ sdc_service.name }}", + "modelType": "service" + } + } + } + ], + "aLaCarte": false + }, + "project": { + "projectName": "{{ project }}" + }, + "owningEntity": { + "owningEntityId": "{{ owning_entity.owning_entity_id }}", + "owningEntityName": "{{ owning_entity.name }}" + } + } +} diff --git a/src/onapsdk/so/templates/instantiate_vf_module_ala_carte.json.j2 b/src/onapsdk/so/templates/instantiate_vf_module_ala_carte.json.j2 new file mode 100644 index 0000000..0738379 --- /dev/null +++ b/src/onapsdk/so/templates/instantiate_vf_module_ala_carte.json.j2 @@ -0,0 +1,66 @@ +{ + "requestDetails": { + "requestInfo": + { + "instanceName": "{{ vf_module_instance_name }}", + "source": "VID", + "suppressRollback": false, + "requestorId": "test" + }, + "modelInfo": { + "modelType": "vfModule", + "modelInvariantId": "{{ vf_module.model_invariant_uuid }}", + "modelVersionId": "{{ vf_module.model_version_id }}", + "modelName": "{{ vf_module.model_name }}", + "modelVersion": "{{ vf_module.model_version }}", + "modelCustomizationId": "{{ vf_module.model_customization_id }}", + "modelCustomizationName": "{{ vf_module.model_name }}" + }, + "requestParameters": { + "userParams": [ + {% for parameter in vf_module_parameters %} + { + "name": "{{ parameter.name }}", + "value": "{{ parameter.value }}" + }{% if not loop.last %},{% endif %} + {% endfor %} + ], + "testApi": "GR_API", + "usePreload": true, + "aLaCarte": true + }, + "cloudConfiguration": { + "tenantId": "{{ tenant.tenant_id }}", + "cloudOwner": "{{ cloud_region.cloud_owner }}", + "lcpCloudRegionId": "{{ cloud_region.cloud_region_id }}" + }, + "relatedInstanceList": [ + { + "relatedInstance": { + "instanceId": "{{ vnf_instance.service_instance.instance_id }}", + "modelInfo": { + "modelType": "service", + "modelName": "{{ service.name }}", + "modelInvariantId": "{{ service.unique_uuid }}", + "modelVersion": "1.0", + "modelVersionId": "{{ service.identifier }}" + } + } + }, + { + "relatedInstance": { + "instanceId": "{{ vnf_instance.vnf_id }}", + "modelInfo": { + "modelType": "vnf", + "modelName": "{{ vnf_instance.vnf.model_name }}", + "modelInvariantId": "{{ vnf_instance.vnf.model_invariant_id }}", + "modelVersion": "{{ vnf_instance.vnf.model_version }}", + "modelVersionId": "{{ vnf_instance.vnf.model_version_id }}", + "modelCustomizationId": "{{ vnf_instance.vnf.model_customization_id }}", + "modelCustomizationName": "{{ vnf_instance.vnf.name }}" + } + } + } + ] + } +} diff --git a/src/onapsdk/so/templates/instantiate_vnf_ala_carte.json.j2 b/src/onapsdk/so/templates/instantiate_vnf_ala_carte.json.j2 new file mode 100644 index 0000000..9fbf989 --- /dev/null +++ b/src/onapsdk/so/templates/instantiate_vnf_ala_carte.json.j2 @@ -0,0 +1,18 @@ +{% extends "instantiate_network_vnf_ala_carte_base.json.j2" %} +{% block model_info %} + "modelType": "vnf", + "modelInvariantId": "{{ vnf.model_invariant_id }}", + "modelVersionId": "{{ vnf.model_version_id }}", + "modelName": "{{ vnf.model_name }}", + "modelVersion": "{{ vnf.model_version }}", + "modelCustomizationId": "{{ vnf.model_customization_id }}", + "modelCustomizationName": "{{ vnf.name }}" +{% endblock %} +{% block user_params %} +{% for parameter in vnf_parameters %} +{ + "name": "{{ parameter.name }}", + "value": "{{ parameter.value }}" +}{% if not loop.last %},{% endif %} +{% endfor %} +{% endblock %}
\ No newline at end of file diff --git a/src/onapsdk/so/templates/instantiate_vnf_macro.json.j2 b/src/onapsdk/so/templates/instantiate_vnf_macro.json.j2 new file mode 100644 index 0000000..2d7aeee --- /dev/null +++ b/src/onapsdk/so/templates/instantiate_vnf_macro.json.j2 @@ -0,0 +1,153 @@ +{ + "requestDetails": { + "requestInfo": { + "instanceName": "{{ service_instance.instance_name }}", + "source": "VID", + "suppressRollback": false, + "requestorId": "demo", + "productFamilyId": "1234" + }, + "modelInfo": { + "modelType": "vnf", + "modelInvariantId": "{{ vnf.model_invariant_id }}", + "modelVersionId": "{{ vnf.model_version_id }}", + "modelName": "{{ vnf.model_name }}", + "modelVersion": "{{ vnf.model_version }}", + "modelCustomizationId": "{{ vnf.model_customization_id }}", + "modelInstanceName": "{{ vnf.name }}" + }, + "cloudConfiguration": { + "tenantId": "{{ tenant.tenant_id }}", + "cloudOwner": "{{ cloud_region.cloud_owner }}", + "lcpCloudRegionId": "{{ cloud_region.cloud_region_id }}" + }, + "platform": { + "platformName": "{{ platform }}" + }, + "lineOfBusiness": { + "lineOfBusinessName": "{{ line_of_business }}" + }, + "subscriberInfo": { + "globalSubscriberId": "{{ service_instance.service_subscription.customer.global_customer_id }}" + }, + "requestParameters": { + {% block subscriptionServiceType %} + "subscriptionServiceType": "{{ service.name }}", + {% endblock %} + "userParams": [ + { + "Homing_Solution": "none" + }, + { + "service": { + "instanceParams": [], + "resources": { + {% block vnfs %} + "vnfs": [ + { + "modelInfo": { + "modelName": "{{ vnf.model_name }}", + "modelVersionId": "{{ vnf.model_version_id }}", + "modelInvariantUuid": "{{ vnf.model_invariant_id }}", + "modelVersion": "{{ vnf.model_version }}", + "modelCustomizationId": "{{ vnf.model_customization_id }}", + "modelInstanceName": "{{ vnf.name }}" + }, + "cloudConfiguration": { + "tenantId": "{{ tenant.tenant_id }}", + "cloudOwner": "{{ cloud_region.cloud_owner }}", + "lcpCloudRegionId": "{{ cloud_region.cloud_region_id }}" + }, + "platform": { + "platformName": "{{ platform }}" + }, + "lineOfBusiness": { + "lineOfBusinessName": "{{ line_of_business }}" + }, + "productFamilyId": "1234", + "instanceName": "{{ instance_name }}", + "instanceParams": [ + { + {% for vnf_parameter in vnf_parameters %} + {% if vnf_parameter.name == vnf.model_name %} + {% for parameter in vnf_parameter.vnf_parameters %} + "{{ parameter.name }}": "{{ parameter.value }}"{% if not loop.last %},{% endif %} + {% endfor %} + {% endif %} + {% endfor %} + } + ], + "vfModules": [ + {% for vf_module in vnf.vf_modules %} + + { + "modelInfo": { + "modelName": "{{ vf_module.model_name }}", + "modelVersionId": "{{ vf_module.model_version_id }}", + "modelInvariantUuid": "{{ vf_module.model_invariant_uuid }}", + "modelVersion": "{{ vf_module.model_version }}", + "modelCustomizationId": "{{ vf_module.model_customization_id }}" + }, + "instanceName": "{{instance_name}}_{{ vf_module.name }}", + "instanceParams": [ + { + {% for vnf_parameter in vnf_parameters %} + {% if vnf_parameter.name == vnf.model_name %} + {% set mylist = vf_module.name.split('..') %} + {% set item = mylist|length-2 %} + {% for vf_module_parameter in vnf_parameter.vfmodule_parameters %} + {% if vf_module_parameter.name == mylist[item] %} + {% for parameter in vf_module_parameter.vfmodule_parameters %} + "{{ parameter.name }}": "{{ parameter.value }}"{% if not loop.last %},{% endif %} + {% endfor %} + {% endif %} + {% endfor %} + {% endif %} + {% endfor %} + } + ] + }{% if not loop.last %},{% endif %} + {% endfor %} + ] + } + ] + {% endblock %} + }, + "modelInfo": { + "modelType": "vnf", + "modelInvariantId": "{{ vnf.model_invariant_id }}", + "modelVersionId": "{{ vnf.model_version_id }}", + "modelName": "{{ vnf.model_name }}", + "modelVersion": "{{ vnf.model_version }}", + "modelCustomizationId": "{{ vnf.model_customization_id }}", + "modelCustomizationName": "{{ vnf.name }}" + } + } + } + ], + "aLaCarte": false + }, + "project": { + "projectName": "{{ project }}" + }, + "owningEntity": { + "owningEntityId": "{{ owning_entity.owning_entity_id }}", + "owningEntityName": "{{ owning_entity.name }}" + }, + "relatedInstanceList": [ + { + "relatedInstance": { + "instanceId": "{{ service_instance.instance_id }}", + "modelInfo": { + "modelType": "service", + "modelInvariantId": "{{ service.unique_uuid }}", + "modelVersionId": "{{ service.identifier }}", + "modelName": "{{ service.name }}", + "modelVersion": "1.0" + } + } + } + ] + }, + "serviceInstanceId": "{{ service_instance.instance_id }}" +} diff --git a/src/onapsdk/so/templates/instantiate_vnf_macro_so_vnf.json.j2 b/src/onapsdk/so/templates/instantiate_vnf_macro_so_vnf.json.j2 new file mode 100644 index 0000000..c7f4356 --- /dev/null +++ b/src/onapsdk/so/templates/instantiate_vnf_macro_so_vnf.json.j2 @@ -0,0 +1,151 @@ +{ + "requestDetails": { + "requestInfo": { + "instanceName": "{{ service_instance.instance_name }}", + "source": "VID", + "suppressRollback": false, + "requestorId": "demo", + "productFamilyId": "1234" + }, + "modelInfo": { + "modelType": "vnf", + "modelInvariantId": "{{ vnf.model_invariant_id }}", + "modelVersionId": "{{ vnf.model_version_id }}", + "modelName": "{{ vnf.model_name }}", + "modelVersion": "{{ vnf.model_version }}", + "modelCustomizationId": "{{ vnf.model_customization_id }}", + "modelInstanceName": "{{ vnf.name }}" + }, + "cloudConfiguration": { + "tenantId": "{{ tenant.tenant_id }}", + "cloudOwner": "{{ cloud_region.cloud_owner }}", + "lcpCloudRegionId": "{{ cloud_region.cloud_region_id }}" + }, + "platform": { + "platformName": "{{ platform }}" + }, + "lineOfBusiness": { + "lineOfBusinessName": "{{ line_of_business }}" + }, + "subscriberInfo": { + "globalSubscriberId": "{{ service_instance.service_subscription.customer.global_customer_id }}" + }, + "requestParameters": { + {% block subscriptionServiceType %} + "subscriptionServiceType": "{{ service.name }}", + {% endblock %} + "userParams": [ + { + "Homing_Solution": "none" + }, + { + "service": { + "instanceParams": [], + "resources": { + {% block vnfs %} + "vnfs": [ + { + "modelInfo": { + "modelName": "{{ vnf.model_name }}", + "modelVersionId": "{{ vnf.model_version_id }}", + "modelInvariantUuid": "{{ vnf.model_invariant_id }}", + "modelVersion": "{{ vnf.model_version }}", + "modelCustomizationId": "{{ vnf.model_customization_id }}", + "modelInstanceName": "{{ vnf.name }}" + }, + "cloudConfiguration": { + "tenantId": "{{ tenant.tenant_id }}", + "cloudOwner": "{{ cloud_region.cloud_owner }}", + "lcpCloudRegionId": "{{ cloud_region.cloud_region_id }}" + }, + "platform": { + "platformName": "{{ platform }}" + }, + "lineOfBusiness": { + "lineOfBusinessName": "{{ line_of_business }}" + }, + "productFamilyId": "1234", + "instanceName": "{{ instance_name }}", + "instanceParams": [ + { + {% for key, value in so_vnf.parameters.items() %} + "{{ key }}": "{{ value }}"{% if not loop.last %},{% endif %} + {% endfor %} + } + ], + "vfModules": [ + {% for vf_module in so_vnf.vf_modules %} + { + "modelInfo": { + + {% if vnf.model_name == so_vnf.model_name %} + {% for sdc_vf_module in vnf.vf_modules %} + {% set mylist = sdc_vf_module.name.split('..') %} + {% set item = mylist|length-2 %} + {% if vf_module.model_name == mylist[item] %} + "modelName": "{{ sdc_vf_module.model_name }}", + "modelVersionId": "{{ sdc_vf_module.model_version_id}}", + "modelInvariantUuid": "{{ sdc_vf_module.model_invariant_uuid }}", + "modelVersion": "{{ sdc_vf_module.model_version }}", + "modelCustomizationId": "{{ sdc_vf_module.model_customization_id }}" + {% endif %} + {% endfor %} + {% endif %} + + }, + "instanceName": "{{ vf_module.instance_name }}", + {% if vf_module.processing_priority %} + "processingPriority": "{{ vf_module.processing_priority }}", + {% endif %} + "instanceParams": [ + { + {% for key, value in vf_module.parameters.items() %} + "{{ key }}": "{{ value }}"{% if not loop.last %},{% endif %} + {% endfor %} + } + ] + }{% if not loop.last %},{% endif %} + {% endfor %} + ] + } + ] + {% endblock %} + }, + "modelInfo": { + "modelType": "vnf", + "modelInvariantId": "{{ vnf.model_invariant_id }}", + "modelVersionId": "{{ vnf.model_version_id }}", + "modelName": "{{ vnf.model_name }}", + "modelVersion": "{{ vnf.model_version }}", + "modelCustomizationId": "{{ vnf.model_customization_id }}", + "modelCustomizationName": "{{ vnf.name }}" + } + } + } + ], + "aLaCarte": false + }, + "project": { + "projectName": "{{ project }}" + }, + "owningEntity": { + "owningEntityId": "{{ owning_entity.owning_entity_id }}", + "owningEntityName": "{{ owning_entity.name }}" + }, + "relatedInstanceList": [ + { + "relatedInstance": { + "instanceId": "{{ service_instance.instance_id }}", + "modelInfo": { + "modelType": "service", + "modelInvariantId": "{{ service.unique_uuid }}", + "modelVersionId": "{{ service.identifier }}", + "modelName": "{{ service.name }}", + "modelVersion": "1.0" + } + } + } + ] + }, + "serviceInstanceId": "{{ service_instance.instance_id }}" +} diff --git a/src/onapsdk/so/templates/service_instance_model_info.json.j2 b/src/onapsdk/so/templates/service_instance_model_info.json.j2 new file mode 100644 index 0000000..fc66de4 --- /dev/null +++ b/src/onapsdk/so/templates/service_instance_model_info.json.j2 @@ -0,0 +1,7 @@ +{ + "modelType": "service", + "modelInvariantId": "{{ model_invariant_id }}", + "modelName": "{{ model_name }}", + "modelVersion": "{{ model_version }}", + "modelVersionId": "{{ model_name_version_id }}" +} diff --git a/src/onapsdk/so/templates/vf_model_info.json.j2 b/src/onapsdk/so/templates/vf_model_info.json.j2 new file mode 100644 index 0000000..4b40898 --- /dev/null +++ b/src/onapsdk/so/templates/vf_model_info.json.j2 @@ -0,0 +1,15 @@ +[ +{% for _, module in modules.items() %} + { + "modelInfo": { + "modelName": "{{ module["metadata"]["vfModuleModelName"] }}", + "modelVersion": "{{ module.metadata.vfModuleModelVersion }}", + "modelVersionId": "{{ module.metadata.vfModuleModelUUID }}", + "modelInvariantUuid": "{{ module.metadata.vfModuleInvariantUUID }}", + "modelCustomizationId": "{{ module.metadata.vfModuleModelCustomizationUUID }}" + }, + "instanceName": "{{ module.metadata.vfModuleModelName }}", + "instanceParams": [] + }{% if not loop.last %},{% endif %} +{% endfor %} +]
\ No newline at end of file diff --git a/src/onapsdk/so/templates/vnf_model_info.json.j2 b/src/onapsdk/so/templates/vnf_model_info.json.j2 new file mode 100644 index 0000000..ecc788b --- /dev/null +++ b/src/onapsdk/so/templates/vnf_model_info.json.j2 @@ -0,0 +1,9 @@ +{ + "modelType": "vnf", + "modelName": "{{ vnf_model_name }}", + "modelVersion": "{{ vnf_model_version }}", + "modelVersionId": "{{ vnf_model_version_id }}", + "modelInvariantUuid": "{{ vnf_model_invariant_uuid }}", + "modelCustomizationId": "{{ vnf_model_customization_id }}", + "modelInstanceName": "{{ vnf_model_instance_name }}" +} diff --git a/src/onapsdk/utils/__init__.py b/src/onapsdk/utils/__init__.py new file mode 100644 index 0000000..bd7f9f5 --- /dev/null +++ b/src/onapsdk/utils/__init__.py @@ -0,0 +1,40 @@ +"""ONAP SDK utils package.""" +# Copyright 2022 Orange, 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 json +from datetime import datetime + + +def get_zulu_time_isoformat() -> str: + """Get zulu time in accepted by ONAP modules format. + + Returns: + str: Actual Zulu time. + + """ + return datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%S.%fZ') + + +def load_json_file(path_to_json_file: str) -> str: + """ + Return json as string from selected file. + + Args: + path_to_json_file: (str) path to file with json + Returns: + File content as string (str) + """ + with open(path_to_json_file) as json_file: + data = json.load(json_file) + return json.dumps(data) diff --git a/src/onapsdk/utils/configuration.py b/src/onapsdk/utils/configuration.py new file mode 100644 index 0000000..89bee5a --- /dev/null +++ b/src/onapsdk/utils/configuration.py @@ -0,0 +1,25 @@ +"""Configuration package.""" +# Copyright 2022 Orange, 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 List + + +def tosca_path() -> str: + """Return tosca file paths.""" + return '/tmp/tosca_files/' + + +def components_needing_distribution() -> List[str]: + """Return the list of components needing distribution.""" + return ["SO", "sdnc", "aai"] diff --git a/src/onapsdk/utils/gui.py b/src/onapsdk/utils/gui.py new file mode 100644 index 0000000..421e966 --- /dev/null +++ b/src/onapsdk/utils/gui.py @@ -0,0 +1,35 @@ +"""Definition of GUI objects.""" +# Copyright 2022 Orange, 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 dataclasses import dataclass +from typing import List + +@dataclass +class GuiItem: + """Class for keeping track of a GUI.""" + + url: str + status: int + +@dataclass +class GuiList: + """Class to list all the GUIs.""" + + guilist: List[GuiItem] + + def add(self, element): + """Add a GUi to GUI list.""" + if not isinstance(element, GuiItem): + raise AttributeError + self.guilist.append(element) diff --git a/src/onapsdk/utils/headers_creator.py b/src/onapsdk/utils/headers_creator.py new file mode 100644 index 0000000..adb0609 --- /dev/null +++ b/src/onapsdk/utils/headers_creator.py @@ -0,0 +1,245 @@ +"""Header creator package.""" +# Copyright 2022 Orange, 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 Dict +from uuid import uuid4 +import base64 +import hashlib + +from onapsdk.configuration import settings + + +def headers_sdc_creator(base_header: Dict[str, str], + user: str = "cs0008", + authorization: str = None): + """ + Create the right headers for SDC creator type. + + Args: + base_header (Dict[str, str]): the base header to use + user (str, optional): the user to use. Default to cs0008 + authorization (str, optional): the basic auth to use. + Default to "classic" one + + Returns: + Dict[str, str]: the needed headers + + """ + return headers_sdc_generic(base_header, user, authorization=authorization) + + +def headers_sdc_tester(base_header: Dict[str, str], + user: str = "jm0007", + authorization: str = None): + """ + Create the right headers for SDC tester type. + + Args: + base_header (Dict[str, str]): the base header to use + user (str, optional): the user to use. Default to jm0007 + authorization (str, optional): the basic auth to use. + Default to "classic" one + + Returns: + Dict[str, str]: the needed headers + + """ + return headers_sdc_generic(base_header, user, authorization=authorization) + + +def headers_sdc_governor(base_header: Dict[str, str], + user: str = "gv0001", + authorization: str = None): + """ + Create the right headers for SDC governor type. + + Args: + base_header (Dict[str, str]): the base header to use + user (str, optional): the user to use. Default to gv0001 + authorization (str, optional): the basic auth to use. + Default to "classic" one + + Returns: + Dict[str, str]: the needed headers + + """ + return headers_sdc_generic(base_header, user, authorization=authorization) + + +def headers_sdc_operator(base_header: Dict[str, str], + user: str = "op0001", + authorization: str = None): + """ + Create the right headers for SDC operator type. + + Args: + base_header (Dict[str, str]): the base header to use + user (str, optional): the user to use. Default to op0001 + authorization (str, optional): the basic auth to use. + Default to "classic" one + + Returns: + Dict[str, str]: the needed headers + + """ + return headers_sdc_generic(base_header, user, authorization=authorization) + + +def headers_sdc_generic(base_header: Dict[str, str], + user: str, + authorization: str = None): + """ + Create the right headers for SDC generic type. + + Args: + base_header (Dict[str, str]): the base header to use + user (str): the user to use. + authorization (str, optional): the basic auth to use. + Default to "classic" one + + Returns: + Dict[str, str]: the needed headers + + """ + headers = base_header.copy() + headers["USER_ID"] = user + headers["Authorization"] = authorization or settings.SDC_AUTH + headers["X-ECOMP-InstanceID"] = "onapsdk" + return headers + + +def headers_aai_creator(base_header: Dict[str, str]): + """ + Create the right headers for AAI creator type. + + Args: + base_header (Dict[str, str]): the base header to use + + Returns: + Dict[str, str]: the needed headers + + """ + headers = base_header.copy() + headers["x-fromappid"] = "AAI" + headers["x-transactionid"] = "0a3f6713-ba96-4971-a6f8-c2da85a3176e" + headers["authorization"] = settings.AAI_AUTH + return headers + + +def headers_so_creator(base_header: Dict[str, str]): + """ + Create the right headers for SO creator type. + + Args: + base_header (Dict[str, str]): the base header to use + + Returns: + Dict[str, str]: the needed headers + + """ + headers = base_header.copy() + headers["x-fromappid"] = "AAI" + headers["x-transactionid"] = str(uuid4()) + headers["authorization"] = settings.SO_AUTH + headers["cache-control"] = "no-cache" + return headers + +def headers_so_catelog_db_creator(base_header: Dict[str, str]): + """ + Create the right headers for SO creator type. + + Args: + base_header (Dict[str, str]): the base header to use + + Returns: + Dict[str, str]: the needed headers + + """ + headers = base_header.copy() + headers["x-fromappid"] = "AAI" + headers["x-transactionid"] = str(uuid4()) + headers["authorization"] = settings.SO_CAT_DB_AUTH + headers["cache-control"] = "no-cache" + return headers + +def headers_msb_creator(base_header: Dict[str, str]): + """ + Create the right headers for MSB. + + Args: + base_header (Dict[str, str]): the base header to use + + Returns: + Dict[str, str]: the needed headers + + """ + headers = base_header.copy() + headers["cache-control"] = "no-cache" + return headers + + +def headers_sdnc_creator(base_header: Dict[str, str]): + """ + Create the right headers for SDNC. + + Args: + base_header (Dict[str, str]): the base header to use + + Returns: + Dict[str, str]: the needed headers + + """ + headers = base_header.copy() + headers["authorization"] = settings.SDNC_AUTH + headers["x-transactionid"] = str(uuid4()) + headers["x-fromappid"] = "API client" + return headers + + +def headers_sdc_artifact_upload(base_header: Dict[str, str], data: str): + """ + Create the right headers for sdc artifact upload. + + Args: + base_header (Dict[str, str]): the base header to use + data (str): payload data used to create an md5 content header + + Returns: + Dict[str, str]: the needed headers + + """ + headers = base_header.copy() + headers["Accept"] = "application/json, text/plain, */*" + headers["Accept-Encoding"] = "gzip, deflate, br" + headers["Content-Type"] = "application/json; charset=UTF-8" + md5_content = hashlib.md5(data.encode('UTF-8')).hexdigest() + content = base64.b64encode(md5_content.encode('ascii')).decode('UTF-8') + headers["Content-MD5"] = content + return headers + +def headers_clamp_creator(base_header: Dict[str, str]): + """ + Create the right headers for CLAMP generic type. + + base_header (Dict[str, str]): the base header to use + data (str): payload data used to create an md5 content header + + Returns: + Dict[str, str]: the needed headers + + """ + headers = base_header.copy() + headers["Authorization"] = settings.CLAMP_AUTH + headers["X-ECOMP-InstanceID"] = "onapsdk" + return headers diff --git a/src/onapsdk/utils/jinja.py b/src/onapsdk/utils/jinja.py new file mode 100644 index 0000000..fe59eae --- /dev/null +++ b/src/onapsdk/utils/jinja.py @@ -0,0 +1,50 @@ +"""Jinja module.""" +# Copyright 2022 Orange, 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 jinja2 import Environment, PackageLoader, select_autoescape, ChoiceLoader + + +def jinja_env() -> Environment: + """Create Jinja environment. + + jinja_env allow to fetch simply jinja templates where they are. + by default jinja engine will look for templates in `templates` directory of + the package. So to load a template, you just have to do: + + Example: + >>> template = jinja_env().get_template('vendor_create.json.j2') + >>> data = template.render(name="vendor") + + See also: + SdcElement.create() for real use + + Returns: + Environment: the Jinja environment to use + + """ + return Environment(autoescape=select_autoescape(['html', 'htm', 'xml']), + loader=ChoiceLoader([ + PackageLoader("onapsdk.aai"), + PackageLoader("onapsdk.cds"), + PackageLoader("onapsdk.clamp"), + PackageLoader("onapsdk.msb"), + PackageLoader("onapsdk.nbi"), + PackageLoader("onapsdk.sdc"), + PackageLoader("onapsdk.sdnc"), + PackageLoader("onapsdk.sdnc"), + PackageLoader("onapsdk.so"), + PackageLoader("onapsdk.ves"), + PackageLoader("onapsdk.vid") + ])) diff --git a/src/onapsdk/utils/mixins.py b/src/onapsdk/utils/mixins.py new file mode 100644 index 0000000..7a64a15 --- /dev/null +++ b/src/onapsdk/utils/mixins.py @@ -0,0 +1,99 @@ +"""Mixins module.""" +# Copyright 2022 Orange, 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 ctypes import c_bool +from multiprocessing import Process, Value +from time import sleep + + +class WaitForFinishMixin(ABC): + """Wait for finish mixin. + + Mixin with wait_for_finish method and two properties: + - completed, + - finished. + + Can be used to wait for result of asynchronous tasks. + + """ + + WAIT_FOR_SLEEP_TIME = 10 + + @property + @abstractmethod + def completed(self) -> bool: + """Store an information if object task is completed or not. + + Returns: + bool: True if object task is completed, False otherwise. + + """ + + @property + @abstractmethod + def finished(self) -> bool: + """Store an information if object task is finished or not. + + Returns: + bool: True if object task is finished, False otherwise. + + """ + + def _wait_for_finish(self, return_value: Value) -> bool: + """Wait until object task is finished. + + Method called in another process. + + Args: + return_value(Value): value shared with main process to pass there + if object task was completed or not + + """ + while not self.finished: + sleep(self.WAIT_FOR_SLEEP_TIME) + self._logger.info(f"{self.__class__.__name__} task finished") + return_value.value = self.completed + + def wait_for_finish(self, timeout: float = None) -> bool: + """Wait until object task is finished. + + It uses time.sleep with WAIT_FOR_SLEEP_TIME value as a parameter to + wait unitl request is finished (object's finished property is + equal to True). + + It runs another process to control time of the function. If process timed out + TimeoutError is going to be raised. + + Args: + timeout(float, optional): positive number, wait at most timeout seconds + + Raises: + TimeoutError: Raised when function timed out + + Returns: + bool: True if object's task is successfully completed, False otherwise + + """ + self._logger.debug(f"Wait until {self.__class__.__name__} task is not finished") + return_value: Value = Value(c_bool) + wait_for_process: Process = Process(target=self._wait_for_finish, args=(return_value,)) + try: + wait_for_process.start() + wait_for_process.join(timeout) + return return_value.value + finally: + if wait_for_process.is_alive(): + wait_for_process.terminate() + raise TimeoutError diff --git a/src/onapsdk/utils/tosca_file_handler.py b/src/onapsdk/utils/tosca_file_handler.py new file mode 100644 index 0000000..921b868 --- /dev/null +++ b/src/onapsdk/utils/tosca_file_handler.py @@ -0,0 +1,106 @@ +"""Utils class.""" +# Copyright 2022 Orange, 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 json +import string +import random +from typing import Dict, List + +from onapsdk.exceptions import ValidationError + +def get_parameter_from_yaml(parameter: str, config_file: str): + """Get the value of a given parameter in file.yaml. + + Parameter must be given in string format with dots + Example: general.openstack.image_name + + Args: + parameter (str): + config_file (str): configuration yaml file formtatted as string + + Raises: + ParameterError: parameter not defined + + Returns: + the value of the parameter + + """ + value = json.loads(config_file) + + # Workaround for the .. within the params in the yaml file + ugly_param = parameter.replace("..", "##") + for element in ugly_param.split("."): + value = value.get(element.replace("##", "..")) + if value is None: + msg = f"{element} in the {parameter} is not in YAML config file." + raise ValidationError(msg) + + return value + +def get_vf_list_from_tosca_file(model: str) -> List: + """Get the list of Vfs of a VNF based on the tosca file. + + Args: + model (str): the model retrieved from the tosca file at Vnf + instantiation + + Returns: + list: a list of Vfs + + """ + newlist = [] + node_list = get_parameter_from_yaml( + "topology_template.node_templates", model) + + for node in node_list: + value = get_parameter_from_yaml( + "topology_template.node_templates." + node + ".type", + model) + if "org.openecomp.resource.vf" in value: + print(node, value) + if node not in newlist: + search_value = str(node).split(" ")[0] + newlist.append(search_value) + return newlist + +def get_modules_list_from_tosca_file(model: str) -> Dict: + """Get the list of modules from tosca file. + + Modules are stored on topology_template.groups TOSCA file section. + + Args: + model (str): the model retrieved from the tosca file at Vnf + instantiation. + + Returns: + dict: a list of modules + + """ + return get_parameter_from_yaml( + "topology_template.groups", model + ) + +def random_string_generator(size=6, + chars=string.ascii_uppercase + string.digits) -> str: + """Get a random String for VNF. + + Args: + size (int): the number of alphanumerical chars for CI + chars (str): alphanumerical characters (ASCII uppercase and digits) + + Returns: + str: a sequence of random characters + + """ + return ''.join(random.choice(chars) for _ in range(size)) diff --git a/src/onapsdk/version.py b/src/onapsdk/version.py new file mode 100644 index 0000000..9cf4f97 --- /dev/null +++ b/src/onapsdk/version.py @@ -0,0 +1,16 @@ +"""Version module.""" +# Copyright 2022 Orange, 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. + +__version__ = "10.2.0" diff --git a/src/onapsdk/ves/__init__.py b/src/onapsdk/ves/__init__.py new file mode 100644 index 0000000..186f0e7 --- /dev/null +++ b/src/onapsdk/ves/__init__.py @@ -0,0 +1,14 @@ +"""ONAP SDK VES package.""" +# Copyright 2022 Orange, 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/src/onapsdk/ves/templates/ves7_batch_with_stndDefined_valid.json.j2 b/src/onapsdk/ves/templates/ves7_batch_with_stndDefined_valid.json.j2 new file mode 100644 index 0000000..662e55e --- /dev/null +++ b/src/onapsdk/ves/templates/ves7_batch_with_stndDefined_valid.json.j2 @@ -0,0 +1,109 @@ +{ + "eventList": [ + { + "commonEventHeader": { + "version": "4.1", + "vesEventListenerVersion": "7.2", + "domain": "stndDefined", + "eventId": "stndDefined-gNB_Nokia000001", + "eventName": "stndDefined-gNB-Nokia-PowerLost", + "stndDefinedNamespace": "3GPP-FaultSupervision", + "startEpochMicrosec": 1413378172000000, + "lastEpochMicrosec": 1413378172000000, + "reportingEntityName": "ibcx0001vm002oam001", + "sourceName": "scfx0001vm002cap001", + "sequence": 1, + "priority": "High" + }, + "stndDefinedFields": { + "schemaReference": "https://forge.3gpp.org/rep/sa5/MnS/blob/SA88-Rel16/OpenAPI/faultMnS.yaml#components/schemas/NotifyNewAlarm", + "data": { + "href": 1, + "uri": "1", + "notificationId": 1, + "notificationType": "notifyNewAlarm", + "eventTime": "xyz", + "systemDN": "xyz", + "probableCause": 1, + "perceivedSeverity": "INDETERMINATE", + "rootCauseIndicator": false, + "specificProblem": "xyz", + "correlatedNotifications": [], + "backedUpStatus": true, + "backUpObject": "xyz", + "trendIndication": "MORE_SEVERE", + "thresholdInfo": { + "observedMeasurement": "new", + "observedValue": 123 + }, + "stateChangeDefinition": { + }, + "monitoredAttributes": { + "newAtt": "new" + }, + "proposedRepairActions": "xyz", + "additionalText": "xyz", + "additionalInformation": { + "addInfo": "new" + }, + "alarmId": "1", + "alarmType": "COMMUNICATIONS_ALARM" + }, + "stndDefinedFieldsVersion": "1.0" + } + }, + { + "commonEventHeader": { + "version": "4.1", + "vesEventListenerVersion": "7.2", + "domain": "stndDefined", + "eventId": "stndDefined-gNB_Nokia000001", + "eventName": "stndDefined-gNB-Nokia-PowerLost", + "stndDefinedNamespace": "3GPP-FaultSupervision", + "startEpochMicrosec": 1413378172000000, + "lastEpochMicrosec": 1413378172000000, + "reportingEntityName": "ibcx0001vm002oam001", + "sourceName": "scfx0001vm002cap001", + "sequence": 1, + "priority": "High" + }, + "stndDefinedFields": { + "schemaReference": "https://forge.3gpp.org/rep/sa5/MnS/blob/SA88-Rel16/OpenAPI/faultMnS.yaml#components/schemas/NotifyNewAlarm", + "data": { + "href": 1, + "uri": "1", + "notificationId": 1, + "notificationType": "notifyNewAlarm", + "eventTime": "xyz", + "systemDN": "xyz", + "probableCause": 1, + "perceivedSeverity": "INDETERMINATE", + "rootCauseIndicator": false, + "specificProblem": "xyz", + "correlatedNotifications": [], + "backedUpStatus": true, + "backUpObject": "xyz", + "trendIndication": "MORE_SEVERE", + "thresholdInfo": { + "observedMeasurement": "new", + "observedValue": 123 + }, + "stateChangeDefinition": { + }, + "monitoredAttributes": { + "newAtt": "new" + }, + "proposedRepairActions": "xyz", + "additionalText": "xyz", + "additionalInformation": { + "addInfo": "new" + }, + "alarmId": "1", + "alarmType": "COMMUNICATIONS_ALARM" + }, + "stndDefinedFieldsVersion": "1.0" + } + } + ] +} + diff --git a/src/onapsdk/ves/templates/ves_stnd_event.json.j2 b/src/onapsdk/ves/templates/ves_stnd_event.json.j2 new file mode 100644 index 0000000..fd1ce98 --- /dev/null +++ b/src/onapsdk/ves/templates/ves_stnd_event.json.j2 @@ -0,0 +1,54 @@ +{ + "event": { + "commonEventHeader": { + "version": "4.1", + "vesEventListenerVersion": "7.2", + "domain": "stndDefined", + "eventId": "stndDefined-gNB_Nokia000001", + "eventName": "stndDefined-gNB-Nokia-PowerLost", + "stndDefinedNamespace": "3GPP-FaultSupervision", + "startEpochMicrosec": 1413378172000000, + "lastEpochMicrosec": 1413378172000000, + "reportingEntityName": "ibcx0001vm002oam001", + "sourceName": "scfx0001vm002cap001", + "sequence": 1, + "priority": "High" + }, + "stndDefinedFields": { + "schemaReference": "https://forge.3gpp.org/rep/sa5/MnS/blob/SA88-Rel16/OpenAPI/faultMnS.yaml#components/schemas/NotifyNewAlarm", + "data": { + "href": 1, + "uri": "1", + "notificationId": 1, + "notificationType": "notifyNewAlarm", + "eventTime": "xyz", + "systemDN": "xyz", + "probableCause": 1, + "perceivedSeverity": "INDETERMINATE", + "rootCauseIndicator": false, + "specificProblem": "xyz", + "correlatedNotifications": [], + "backedUpStatus": true, + "backUpObject": "xyz", + "trendIndication": "MORE_SEVERE", + "thresholdInfo": { + "observedMeasurement": "new", + "observedValue": 123 + }, + "stateChangeDefinition": { + }, + "monitoredAttributes": { + "newAtt": "new" + }, + "proposedRepairActions": "xyz", + "additionalText": "xyz", + "additionalInformation": { + "addInfo": "new" + }, + "alarmId": "1", + "alarmType": "COMMUNICATIONS_ALARM" + }, + "stndDefinedFieldsVersion": "1.0" + } + } +} diff --git a/src/onapsdk/ves/templates/ves_stnd_valid_event.json.j2 b/src/onapsdk/ves/templates/ves_stnd_valid_event.json.j2 new file mode 100644 index 0000000..08d335b --- /dev/null +++ b/src/onapsdk/ves/templates/ves_stnd_valid_event.json.j2 @@ -0,0 +1,54 @@ +{ + "event": { + "commonEventHeader": { + "version": "4.1", + "vesEventListenerVersion": "7.2", + "domain": "stndDefined", + "eventId": "12", + "eventName": "someEventName", + "stndDefinedNamespace": "{{ header.namespace }}", + "startEpochMicrosec": 1413378172000000, + "lastEpochMicrosec": 1413378172000000, + "reportingEntityName": "ibcx0001vm002oam001", + "sourceName": "scfx0001vm002cap001", + "sequence": 1, + "priority": "High" + }, + "stndDefinedFields": { + "schemaReference": "{{ schema_reference }}", + "data": { + "href": 1, + "uri": "1", + "notificationId": 1, + "notificationType": "notifyNewAlarm", + "eventTime": "xyz", + "systemDN": "xyz", + "probableCause": 1, + "perceivedSeverity": "INDETERMINATE", + "rootCauseIndicator": false, + "specificProblem": "xyz", + "correlatedNotifications": [], + "backedUpStatus": true, + "backUpObject": "xyz", + "trendIndication": "MORE_SEVERE", + "thresholdInfo": { + "observedMeasurement": "new", + "observedValue": 123 + }, + "stateChangeDefinition": { + }, + "monitoredAttributes": { + "newAtt": "new" + }, + "proposedRepairActions": "xyz", + "additionalText": "xyz", + "additionalInformation": { + "addInfo": "new" + }, + "alarmId": "1", + "alarmType": "COMMUNICATIONS_ALARM" + }, + "stndDefinedFieldsVersion": "1.0" + } + } +} diff --git a/src/onapsdk/ves/ves.py b/src/onapsdk/ves/ves.py new file mode 100644 index 0000000..1f3f592 --- /dev/null +++ b/src/onapsdk/ves/ves.py @@ -0,0 +1,84 @@ +"""Base VES event sender.""" +# Copyright 2022 Orange, Deutsche Telekom AG, Nokia +# +# 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 Dict, Union + +import json +import requests + +from onapsdk.ves.ves_service import VesService + +ACTION = "Send event to Ves" +POST_HTTP_METHOD = "POST" + + +class Ves(VesService): + """Ves library provides functions for sending events to VES.""" + + event_endpoint_url: str = "{}/eventListener/{}" + event_batch_endpoint_url: str = "{}/eventListener/{}/eventBatch" + + @classmethod + def send_event(cls, + version: str, + json_event: str, + basic_auth: Dict[str, str]) -> Union[requests.Response, None]: + """ + Send an event stored in a file to VES. + + Args: + version: (str) version of VES data format + json_event: (str) event to send + basic_auth: Dict[str, str], for example:{ 'username': 'bob', 'password': 'secret' } + Returns: + (requests.Response) HTTP response status + + """ + return Ves.__send_event_message(cls.event_endpoint_url.format(VesService._url, version), + json_event, basic_auth) + + @classmethod + def send_batch_event(cls, + version: str, + json_event: str, + basic_auth: Dict[str, str]) -> Union[requests.Response, None]: + """ + Send a batch event stored in a file to VES. + + Args: + version: (str) version of VES data format + json_event: (str) event to send + basic_auth: Dict[str, str], for example:{ 'username': 'bob', 'password': 'secret' } + Returns: + (requests.Response) HTTP response status + + """ + return Ves.__send_event_message(cls.event_batch_endpoint_url. + format(VesService._url, version), + json_event, basic_auth) + + @classmethod + def __send_event_message(cls, + base_url: str, + json_event: str, + basic_auth: Dict[str, str] + ) -> Union[requests.Response, None]: + cls._logger.debug("Event to send %s", json_event) + return cls.send_message( + POST_HTTP_METHOD, + ACTION, + f"{base_url}", + basic_auth=basic_auth, + json=json.loads(json_event) + ) diff --git a/src/onapsdk/ves/ves_service.py b/src/onapsdk/ves/ves_service.py new file mode 100644 index 0000000..c4bc8ed --- /dev/null +++ b/src/onapsdk/ves/ves_service.py @@ -0,0 +1,27 @@ +"""Base VES module.""" +# Copyright 2022 Orange, Deutsche Telekom AG, Nokia +# +# 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 onapsdk.configuration import settings +from onapsdk.onap_service import OnapService + + +class VesService(OnapService): + """Base VES class. + + Stores url to VES API (edit if you want to use other) and authentication tuple + (username, password). + """ + + _url: str = settings.VES_URL diff --git a/src/onapsdk/vid/__init__.py b/src/onapsdk/vid/__init__.py new file mode 100644 index 0000000..fd58581 --- /dev/null +++ b/src/onapsdk/vid/__init__.py @@ -0,0 +1,16 @@ +"""VID package.""" +# Copyright 2022 Orange, 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 .vid import LineOfBusiness, OwningEntity, Platform, Project, Vid diff --git a/src/onapsdk/vid/templates/vid_declare_resource.json.j2 b/src/onapsdk/vid/templates/vid_declare_resource.json.j2 new file mode 100644 index 0000000..b14e6f4 --- /dev/null +++ b/src/onapsdk/vid/templates/vid_declare_resource.json.j2 @@ -0,0 +1,3 @@ +{ + "options": ["{{ name }}"] +}
\ No newline at end of file diff --git a/src/onapsdk/vid/vid.py b/src/onapsdk/vid/vid.py new file mode 100644 index 0000000..31cb92e --- /dev/null +++ b/src/onapsdk/vid/vid.py @@ -0,0 +1,134 @@ +"""VID module.""" +# Copyright 2022 Orange, 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 warnings import warn + +from onapsdk.configuration import settings +from onapsdk.onap_service import OnapService +from onapsdk.utils.jinja import jinja_env + + +class Vid(OnapService, ABC): + """VID base class.""" + + base_url = settings.VID_URL + api_version = settings.VID_API_VERSION + + def __init__(self, name: str) -> None: + """VID resource object initialization. + + Args: + name (str): Resource name + """ + warn("VID is deprecated and shouldn't be used! " + "It's not a part of the ONAP release since Istanbul.") + super().__init__() + self.name: str = name + + @classmethod + def get_create_url(cls) -> str: + """Resource url. + + Used to create resources + + Returns: + str: Url used for resource creation + + """ + raise NotImplementedError + + @classmethod + def create(cls, name: str) -> "Vid": + """Create VID resource. + + Returns: + Vid: Created VID resource + + """ + warn("VID is deprecated and shouldn't be used! " + "It's not a part of the ONAP release since Istanbul.") + cls.send_message( + "POST", + f"Declare VID resource with {name} name", + cls.get_create_url(), + data=jinja_env().get_template("vid_declare_resource.json.j2").render( + name=name + ) + ) + return cls(name) + + +class OwningEntity(Vid): + """VID owning entity class.""" + + @classmethod + def get_create_url(cls) -> str: + """Owning entity creation url. + + Returns: + str: Url used for ownint entity creation + + """ + warn("VID is deprecated and shouldn't be used! " + "It's not a part of the ONAP release since Istanbul.") + return f"{cls.base_url}{cls.api_version}/maintenance/category_parameter/owningEntity" + + +class Project(Vid): + """VID project class.""" + + @classmethod + def get_create_url(cls) -> str: + """Project creation url. + + Returns: + str: Url used for project creation + + """ + warn("VID is deprecated and shouldn't be used! " + "It's not a part of the ONAP release since Istanbul.") + return f"{cls.base_url}{cls.api_version}/maintenance/category_parameter/project" + + +class LineOfBusiness(Vid): + """VID line of business class.""" + + @classmethod + def get_create_url(cls) -> str: + """Line of business creation url. + + Returns: + str: Url used for line of business creation + + """ + warn("VID is deprecated and shouldn't be used! " + "It's not a part of the ONAP release since Istanbul.") + return f"{cls.base_url}{cls.api_version}/maintenance/category_parameter/lineOfBusiness" + + +class Platform(Vid): + """VID platform class.""" + + @classmethod + def get_create_url(cls) -> str: + """Platform creation url. + + Returns: + str: Url used for platform creation + + """ + warn("VID is deprecated and shouldn't be used! " + "It's not a part of the ONAP release since Istanbul.") + return f"{cls.base_url}{cls.api_version}/maintenance/category_parameter/platform" diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000..b1ef494 --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,7 @@ +-r requirements.txt +bandit +pylint==2.4.4 +pytest +pytest-cov +pydocstyle +requests-mock diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..d985dca --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,21 @@ +"""Main test module.""" +# Copyright 2022 Orange, 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 + +logging.basicConfig( + level=logging.DEBUG, + format='%(asctime)s - %(name)-23s - %(levelname)-8s - %(message)s') +logging.captureWarnings(True) diff --git a/tests/data/__init__.py b/tests/data/__init__.py new file mode 100644 index 0000000..5b3f765 --- /dev/null +++ b/tests/data/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2022 Orange, 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.
\ No newline at end of file diff --git a/tests/data/bad.csar b/tests/data/bad.csar new file mode 100644 index 0000000..6a445b6 --- /dev/null +++ b/tests/data/bad.csar @@ -0,0 +1 @@ +bad is bad diff --git a/tests/data/bad_no_service.csar b/tests/data/bad_no_service.csar Binary files differnew file mode 100644 index 0000000..f0a0627 --- /dev/null +++ b/tests/data/bad_no_service.csar diff --git a/tests/data/csar.meta b/tests/data/csar.meta new file mode 100644 index 0000000..bc3359c --- /dev/null +++ b/tests/data/csar.meta @@ -0,0 +1,2 @@ +SDC-TOSCA-Meta-File-Version: 1.0 +SDC-TOSCA-Definitions-Version: 9.0 diff --git a/tests/data/service-Foo-template.yml b/tests/data/service-Foo-template.yml new file mode 100644 index 0000000..12ba4f4 --- /dev/null +++ b/tests/data/service-Foo-template.yml @@ -0,0 +1,1228 @@ +tosca_definitions_version: tosca_simple_yaml_1_1 +metadata: + invariantUUID: 6a081157-7d17-4369-a1f2-099322bfa9bc + UUID: 862d4c8a-bb0e-40dc-ad4c-7768fc03530a + name: vFW-service + description: vFW-service + type: Service + category: Network L4+ + serviceType: '' + serviceRole: '' + serviceEcompNaming: true + ecompGeneratedNaming: true + namingPolicy: '' +imports: + - nodes: + file: nodes.yml + - datatypes: + file: data.yml + - capabilities: + file: capabilities.yml + - relationships: + file: relationships.yml + - groups: + file: groups.yml + - policies: + file: policies.yml + - service-vFW-service-interface: + file: service-VfwService-template-interface.yml + - resource-vFWCL_vPKG-vf: + file: resource-VfwclVpkgVf-template.yml + - resource-vFWCL_vPKG-vf-interface: + file: resource-VfwclVpkgVf-template-interface.yml + - resource-vFWCL_vFWSNK-vf: + file: resource-VfwclVfwsnkVf-template.yml + - resource-vFWCL_vFWSNK-vf-interface: + file: resource-VfwclVfwsnkVf-template-interface.yml +topology_template: + node_templates: + vFWCL_vPKG-vf 0: + type: org.openecomp.resource.vf.VfwclVpkgVf + metadata: + invariantUUID: 696c4c49-b5d2-4d6f-ac2d-be2cd09fc356 + UUID: 9530fa98-94ca-41c3-bb67-bae3f3dae4a2 + customizationUUID: a2e4b36f-bcea-4b22-a383-0d4dc0a7dd65 + version: '1.0' + name: vFWCL_vPKG-vf + description: vPacketGenerator function + type: VF + category: Application L4+ + subcategory: Firewall + resourceVendor: Generic-Vendor + resourceVendorRelease: '1.0' + resourceVendorModelNumber: '' + properties: + vf_module_id: vTrafficPNG + repo_url_blob: https://nexus.onap.org/content/sites/raw + unprotected_private_subnet_id: zdfw1fwl01_unprotected_sub + public_net_id: 715a1ca1-cbc6-4d00-84bb-0f8667a748ce + vfw_private_ip_0: 192.168.10.100 + onap_private_subnet_id: 51a5a838-7318-464d-858a-974bef8d49e3 + onap_private_net_cidr: 10.4.2.0/24 + image_name: ubuntu-14.04-daily + flavor_name: onap.medium + vnf_id: vPNG_Firewall_demo_app + vpg_name_0: zdfw1fwl01pgn01 + vpg_private_ip_1: 10.4.2.200 + vsn_private_ip_0: 192.168.20.250 + vpg_private_ip_0: 192.168.10.200 + protected_private_net_cidr: 192.168.20.0/24 + unprotected_private_net_cidr: 192.168.10.0/24 + nf_naming: + ecomp_generated_naming: true + onap_private_net_id: 715a1ca1-cbc6-4d00-84bb-0f8667a748ce + unprotected_private_net_id: zdfw1fwl01_unprotected + availability_zone_max_count: 1 + demo_artifacts_version: 1.2.0 + key_name: onap_key_LnHa + repo_url_artifacts: https://nexus.onap.org/content/groups/staging + install_script_version: 1.2.0-SNAPSHOT + cloud_env: openstack + vFWCL_vFWSNK-vf 0: + type: org.openecomp.resource.vf.VfwclVfwsnkVf + metadata: + invariantUUID: f37bfc8b-5d9f-4471-a799-1508f0d8fab5 + UUID: 1fb229a7-0bd9-4358-9072-60501374bcc2 + customizationUUID: 2b98b1f4-498c-4bc9-ab6c-b76af29cb981 + version: '1.0' + name: vFWCL_vFWSNK-vf + description: vFirewall et vSink functions + type: VF + category: Application L4+ + subcategory: Firewall + resourceVendor: Generic-Vendor + resourceVendorRelease: '1.0' + resourceVendorModelNumber: '' + properties: + vf_module_id: vFirewallCL + repo_url_blob: https://nexus.onap.org/content/sites/raw + vfw_private_ip_1: 192.168.20.100 + unprotected_private_subnet_id: zdfw1fwl01_unprotected_sub + public_net_id: 715a1ca1-cbc6-4d00-84bb-0f8667a748ce + vfw_private_ip_0: 192.168.10.100 + onap_private_subnet_id: 51a5a838-7318-464d-858a-974bef8d49e3 + vfw_private_ip_2: 10.4.2.201 + vfw_name_0: zdfw1fwl01fwl01 + onap_private_net_cidr: 10.4.2.0/24 + image_name: ubuntu-14.04-daily + flavor_name: onap.medium + dcae_collector_ip: 10.4.2.38 + vnf_id: vFirewall_demo_app + dcae_collector_port: '8080' + protected_private_subnet_id: zdfw1fwl01_protected_sub + vsn_private_ip_0: 192.168.20.250 + vsn_private_ip_1: 10.4.2.202 + vpg_private_ip_0: 192.168.10.200 + protected_private_net_cidr: 192.168.20.0/24 + unprotected_private_net_cidr: 192.168.10.0/24 + nf_naming: + ecomp_generated_naming: true + vsn_name_0: zdfw1fwl01snk01 + onap_private_net_id: 715a1ca1-cbc6-4d00-84bb-0f8667a748ce + unprotected_private_net_id: zdfw1fwl01_unprotected + availability_zone_max_count: 1 + demo_artifacts_version: 1.2.0 + key_name: onap_key_LnHa + repo_url_artifacts: https://nexus.onap.org/content/groups/staging + install_script_version: 1.2.0-SNAPSHOT + protected_private_net_id: zdfw1fwl01_protected + cloud_env: openstack + capabilities: + network.outgoing.bytes_vfw_vfw_private_0_port: + properties: + unit: request + description: Number of write requests + type: Cumulative + category: compute + end_point: + properties: + protocol: tcp + initiator: source + network_name: PRIVATE + secure: false + network.outgoing.bytes.rate_vfw_vfw_private_2_port: + properties: + unit: request + description: Number of write requests + type: Cumulative + category: compute + memory.usage_vfw: + properties: + unit: request + description: Number of write requests + type: Cumulative + category: compute + network.incoming.packets_vfw_vfw_private_0_port: + properties: + unit: request + description: Number of write requests + type: Cumulative + category: compute + disk.device.read.requests_vfw: + properties: + unit: request + description: Number of write requests + type: Cumulative + category: compute + instance_vfw: + properties: + unit: request + description: Number of write requests + type: Cumulative + category: compute + disk.latency_vfw: + properties: + unit: request + description: Number of write requests + type: Cumulative + category: compute + memory.resident_vfw: + properties: + unit: request + description: Number of write requests + type: Cumulative + category: compute + network.outpoing.packets_vfw_vfw_private_0_port: + properties: + unit: request + description: Number of write requests + type: Cumulative + category: compute + network.outgoing.bytes.rate_vfw_vfw_private_1_port: + properties: + unit: request + description: Number of write requests + type: Cumulative + category: compute + disk.allocation_vfw: + properties: + unit: request + description: Number of write requests + type: Cumulative + category: compute + disk.write.requests.rate_vfw: + properties: + unit: request + description: Number of write requests + type: Cumulative + category: compute + network.incoming.bytes.rate_vfw_vfw_private_1_port: + properties: + unit: request + description: Number of write requests + type: Cumulative + category: compute + network.incoming.bytes_vfw_vfw_private_2_port: + properties: + unit: request + description: Number of write requests + type: Cumulative + category: compute + disk.device.iops_vfw: + properties: + unit: request + description: Number of write requests + type: Cumulative + category: compute + network.incoming.packets.rate_vfw_vfw_private_0_port: + properties: + unit: request + description: Number of write requests + type: Cumulative + category: compute + scalable_vfw: + properties: + min_instances: 1 + max_instances: 1 + network.incoming.packets_vfw_vfw_private_1_port: + properties: + unit: request + description: Number of write requests + type: Cumulative + category: compute + disk.device.usage_vfw: + properties: + unit: request + description: Number of write requests + type: Cumulative + category: compute + network.outgoing.bytes_vfw_vfw_private_1_port: + properties: + unit: request + description: Number of write requests + type: Cumulative + category: compute + disk.usage_vfw: + properties: + unit: request + description: Number of write requests + type: Cumulative + category: compute + disk.ephemeral.size_vfw: + properties: + unit: request + description: Number of write requests + type: Cumulative + category: compute + disk.write.bytes.rate_vfw: + properties: + unit: request + description: Number of write requests + type: Cumulative + category: compute + network.outpoing.packets_vfw_vfw_private_2_port: + properties: + unit: request + description: Number of write requests + type: Cumulative + category: compute + cpu.delta_vfw: + properties: + unit: request + description: Number of write requests + type: Cumulative + category: compute + disk.device.write.bytes.rate_vfw: + properties: + unit: request + description: Number of write requests + type: Cumulative + category: compute + disk.read.bytes_vfw: + properties: + unit: request + description: Number of write requests + type: Cumulative + category: compute + disk.capacity_vfw: + properties: + unit: request + description: Number of write requests + type: Cumulative + category: compute + disk.write.bytes_vfw: + properties: + unit: request + description: Number of write requests + type: Cumulative + category: compute + disk.device.allocation_vfw: + properties: + unit: request + description: Number of write requests + type: Cumulative + category: compute + disk.device.read.requests.rate_vfw: + properties: + unit: request + description: Number of write requests + type: Cumulative + category: compute + disk.read.requests_vfw: + properties: + unit: request + description: Number of write requests + type: Cumulative + category: compute + vcpus_vfw: + properties: + unit: request + description: Number of write requests + type: Cumulative + category: compute + network.incoming.bytes_vfw_vfw_private_0_port: + properties: + unit: request + description: Number of write requests + type: Cumulative + category: compute + disk.device.read.bytes_vfw: + properties: + unit: request + description: Number of write requests + type: Cumulative + category: compute + disk.device.read.bytes.rate_vfw: + properties: + unit: request + description: Number of write requests + type: Cumulative + category: compute + endpoint_vfw: + properties: + secure: true + network.outgoing.packets.rate_vfw_vfw_private_1_port: + properties: + unit: request + description: Number of write requests + type: Cumulative + category: compute + network.outgoing.packets.rate_vfw_vfw_private_0_port: + properties: + unit: request + description: Number of write requests + type: Cumulative + category: compute + disk.read.bytes.rate_vfw: + properties: + unit: request + description: Number of write requests + type: Cumulative + category: compute + network.incoming.packets.rate_vfw_vfw_private_1_port: + properties: + unit: request + description: Number of write requests + type: Cumulative + category: compute + network.incoming.bytes.rate_vfw_vfw_private_0_port: + properties: + unit: request + description: Number of write requests + type: Cumulative + category: compute + disk.write.requests_vfw: + properties: + unit: request + description: Number of write requests + type: Cumulative + category: compute + memory_vfw: + properties: + unit: request + description: Number of write requests + type: Cumulative + category: compute + disk.device.write.requests_vfw: + properties: + unit: request + description: Number of write requests + type: Cumulative + category: compute + disk.root.size_vfw: + properties: + unit: request + description: Number of write requests + type: Cumulative + category: compute + network.outgoing.packets.rate_vfw_vfw_private_2_port: + properties: + unit: request + description: Number of write requests + type: Cumulative + category: compute + network.incoming.packets.rate_vfw_vfw_private_2_port: + properties: + unit: request + description: Number of write requests + type: Cumulative + category: compute + disk.device.capacity_vfw: + properties: + unit: request + description: Number of write requests + type: Cumulative + category: compute + network.outgoing.bytes_vfw_vfw_private_2_port: + properties: + unit: request + description: Number of write requests + type: Cumulative + category: compute + network.outpoing.packets_vfw_vfw_private_1_port: + properties: + unit: request + description: Number of write requests + type: Cumulative + category: compute + network.incoming.packets_vfw_vfw_private_2_port: + properties: + unit: request + description: Number of write requests + type: Cumulative + category: compute + disk.device.write.requests.rate_vfw: + properties: + unit: request + description: Number of write requests + type: Cumulative + category: compute + network.incoming.bytes_vfw_vfw_private_1_port: + properties: + unit: request + description: Number of write requests + type: Cumulative + category: compute + disk.device.latency_vfw: + properties: + unit: request + description: Number of write requests + type: Cumulative + category: compute + cpu_vfw: + properties: + unit: request + description: Number of write requests + type: Cumulative + category: compute + disk.iops_vfw: + properties: + unit: request + description: Number of write requests + type: Cumulative + category: compute + network.incoming.bytes.rate_vfw_vfw_private_2_port: + properties: + unit: request + description: Number of write requests + type: Cumulative + category: compute + disk.device.write.bytes_vfw: + properties: + unit: request + description: Number of write requests + type: Cumulative + category: compute + network.outgoing.bytes.rate_vfw_vfw_private_0_port: + properties: + unit: request + description: Number of write requests + type: Cumulative + category: compute + cpu_util_vfw: + properties: + unit: request + description: Number of write requests + type: Cumulative + category: compute + groups: + vfwcl_vfwsnkvf0..VfwclVfwsnkVf..base_vfw..module-0: + type: org.openecomp.groups.VfModule + metadata: + vfModuleModelName: VfwclVfwsnkVf..base_vfw..module-0 + vfModuleModelInvariantUUID: 0cf42138-9514-4667-af27-19e04eae09ea + vfModuleModelUUID: fd867a01-04a0-4733-a0dd-38293bcb5b1e + vfModuleModelVersion: '1' + vfModuleModelCustomizationUUID: 14f54459-417e-4608-adf3-e173b25bf26c + properties: + min_vf_module_instances: 1 + vf_module_label: base_vfw + max_vf_module_instances: 1 + vfc_list: + vf_module_type: Base + vf_module_description: + initial_count: 1 + volume_group: false + availability_zone_count: + vfwcl_vpkgvf0..VfwclVpkgVf..base_vpkg..module-0: + type: org.openecomp.groups.VfModule + metadata: + vfModuleModelName: VfwclVpkgVf..base_vpkg..module-0 + vfModuleModelInvariantUUID: 8154de96-1f48-42ec-b8a0-9d2eb60cd6b3 + vfModuleModelUUID: d44761cf-c1e6-429f-8cbd-bd9923827965 + vfModuleModelVersion: '1' + vfModuleModelCustomizationUUID: a567f92e-d8ac-4547-bd7e-958419df6b86 + properties: + min_vf_module_instances: 1 + vf_module_label: base_vpkg + max_vf_module_instances: 1 + vfc_list: + vf_module_type: Base + vf_module_description: + initial_count: 1 + volume_group: false + availability_zone_count: + substitution_mappings: + node_type: org.openecomp.service.VfwService + capabilities: + vfwcl_vpkgvf0.vpg.abstract_vpg.disk.device.write.bytes_vpg: + - vfwcl_vpkgvf0 + - vpg.abstract_vpg.disk.device.write.bytes_vpg + vfwcl_vfwsnkvf0.vfw.abstract_vfw.disk.device.write.requests.rate_vfw: + - vfwcl_vfwsnkvf0 + - vfw.abstract_vfw.disk.device.write.requests.rate_vfw + vfwcl_vfwsnkvf0.vfw.abstract_vfw.disk.latency_vfw: + - vfwcl_vfwsnkvf0 + - vfw.abstract_vfw.disk.latency_vfw + vfwcl_vpkgvf0.vpg_vpg_private_0_port.abstract_vpg.network.incoming.bytes_vpg_vpg_private_0_port: + - vfwcl_vpkgvf0 + - vpg_vpg_private_0_port.abstract_vpg.network.incoming.bytes_vpg_vpg_private_0_port + vfwcl_vpkgvf0.vpg.abstract_vpg.cpu_util_vpg: + - vfwcl_vpkgvf0 + - vpg.abstract_vpg.cpu_util_vpg + vfwcl_vfwsnkvf0.protected_private_network.attachment: + - vfwcl_vfwsnkvf0 + - protected_private_network.attachment + vfwcl_vfwsnkvf0.vfw.abstract_vfw.disk.device.iops_vfw: + - vfwcl_vfwsnkvf0 + - vfw.abstract_vfw.disk.device.iops_vfw + vfwcl_vpkgvf0.vpg_vpg_private_0_port.abstract_vpg.binding_vpg_vpg_private_0_port: + - vfwcl_vpkgvf0 + - vpg_vpg_private_0_port.abstract_vpg.binding_vpg_vpg_private_0_port + vfwcl_vfwsnkvf0.vsn.abstract_vsn.os_vsn: + - vfwcl_vfwsnkvf0 + - vsn.abstract_vsn.os_vsn + vfwcl_vpkgvf0.vpg.abstract_vpg.scalable_vpg: + - vfwcl_vpkgvf0 + - vpg.abstract_vpg.scalable_vpg + vfwcl_vfwsnkvf0.vfw.abstract_vfw.disk.device.write.requests_vfw: + - vfwcl_vfwsnkvf0 + - vfw.abstract_vfw.disk.device.write.requests_vfw + vfwcl_vfwsnkvf0.vfw_vfw_private_2_port.abstract_vfw.network.incoming.bytes.rate_vfw_vfw_private_2_port: + - vfwcl_vfwsnkvf0 + - vfw_vfw_private_2_port.abstract_vfw.network.incoming.bytes.rate_vfw_vfw_private_2_port + vfwcl_vfwsnkvf0.vfw_vfw_private_2_port.abstract_vfw.network.incoming.bytes_vfw_vfw_private_2_port: + - vfwcl_vfwsnkvf0 + - vfw_vfw_private_2_port.abstract_vfw.network.incoming.bytes_vfw_vfw_private_2_port + vfwcl_vpkgvf0.vpg.abstract_vpg.disk.device.read.requests.rate_vpg: + - vfwcl_vpkgvf0 + - vpg.abstract_vpg.disk.device.read.requests.rate_vpg + vfwcl_vfwsnkvf0.vfw.abstract_vfw.disk.capacity_vfw: + - vfwcl_vfwsnkvf0 + - vfw.abstract_vfw.disk.capacity_vfw + vfwcl_vpkgvf0.vpg_vpg_private_0_port.abstract_vpg.network.incoming.packets_vpg_vpg_private_0_port: + - vfwcl_vpkgvf0 + - vpg_vpg_private_0_port.abstract_vpg.network.incoming.packets_vpg_vpg_private_0_port + vfwcl_vfwsnkvf0.vfw.abstract_vfw.disk.root.size_vfw: + - vfwcl_vfwsnkvf0 + - vfw.abstract_vfw.disk.root.size_vfw + vfwcl_vfwsnkvf0.vsn.abstract_vsn.disk.device.write.bytes_vsn: + - vfwcl_vfwsnkvf0 + - vsn.abstract_vsn.disk.device.write.bytes_vsn + vfwcl_vfwsnkvf0.vsn.abstract_vsn.cpu_vsn: + - vfwcl_vfwsnkvf0 + - vsn.abstract_vsn.cpu_vsn + vfwcl_vfwsnkvf0.vfw.abstract_vfw.vcpus_vfw: + - vfwcl_vfwsnkvf0 + - vfw.abstract_vfw.vcpus_vfw + vfwcl_vpkgvf0.vpg.abstract_vpg.disk.device.iops_vpg: + - vfwcl_vpkgvf0 + - vpg.abstract_vpg.disk.device.iops_vpg + vfwcl_vfwsnkvf0.vfw.abstract_vfw.disk.device.usage_vfw: + - vfwcl_vfwsnkvf0 + - vfw.abstract_vfw.disk.device.usage_vfw + vfwcl_vpkgvf0.vpg.abstract_vpg.disk.device.capacity_vpg: + - vfwcl_vpkgvf0 + - vpg.abstract_vpg.disk.device.capacity_vpg + vfwcl_vfwsnkvf0.vsn.abstract_vsn.disk.read.requests_vsn: + - vfwcl_vfwsnkvf0 + - vsn.abstract_vsn.disk.read.requests_vsn + vfwcl_vpkgvf0.vpg.abstract_vpg.disk.ephemeral.size_vpg: + - vfwcl_vpkgvf0 + - vpg.abstract_vpg.disk.ephemeral.size_vpg + vfwcl_vfwsnkvf0.vfw_vfw_private_1_port.abstract_vfw.binding_vfw_vfw_private_1_port: + - vfwcl_vfwsnkvf0 + - vfw_vfw_private_1_port.abstract_vfw.binding_vfw_vfw_private_1_port + vfwcl_vfwsnkvf0.vsn.abstract_vsn.disk.device.write.requests_vsn: + - vfwcl_vfwsnkvf0 + - vsn.abstract_vsn.disk.device.write.requests_vsn + vfwcl_vpkgvf0.vpg.abstract_vpg.disk.device.write.requests_vpg: + - vfwcl_vpkgvf0 + - vpg.abstract_vpg.disk.device.write.requests_vpg + vfwcl_vfwsnkvf0.vfw.abstract_vfw.disk.device.read.bytes_vfw: + - vfwcl_vfwsnkvf0 + - vfw.abstract_vfw.disk.device.read.bytes_vfw + vfwcl_vpkgvf0.vpg_vpg_private_0_port.abstract_vpg.network.outgoing.bytes_vpg_vpg_private_0_port: + - vfwcl_vpkgvf0 + - vpg_vpg_private_0_port.abstract_vpg.network.outgoing.bytes_vpg_vpg_private_0_port + vfwcl_vfwsnkvf0.vfw.abstract_vfw.disk.read.bytes.rate_vfw: + - vfwcl_vfwsnkvf0 + - vfw.abstract_vfw.disk.read.bytes.rate_vfw + vfwcl_vfwsnkvf0.vsn.abstract_vsn.disk.device.usage_vsn: + - vfwcl_vfwsnkvf0 + - vsn.abstract_vsn.disk.device.usage_vsn + vfwcl_vfwsnkvf0.vsn.abstract_vsn.feature_vsn: + - vfwcl_vfwsnkvf0 + - vsn.abstract_vsn.feature_vsn + vfwcl_vfwsnkvf0.vsn_vsn_private_0_port.abstract_vsn.binding_vsn_vsn_private_0_port: + - vfwcl_vfwsnkvf0 + - vsn_vsn_private_0_port.abstract_vsn.binding_vsn_vsn_private_0_port + vfwcl_vfwsnkvf0.vsn.abstract_vsn.disk.write.requests.rate_vsn: + - vfwcl_vfwsnkvf0 + - vsn.abstract_vsn.disk.write.requests.rate_vsn + vfwcl_vfwsnkvf0.vsn_vsn_private_1_port.abstract_vsn.network.outgoing.packets.rate_vsn_vsn_private_1_port: + - vfwcl_vfwsnkvf0 + - vsn_vsn_private_1_port.abstract_vsn.network.outgoing.packets.rate_vsn_vsn_private_1_port + vfwcl_vfwsnkvf0.vfw.abstract_vfw.host_vfw: + - vfwcl_vfwsnkvf0 + - vfw.abstract_vfw.host_vfw + vfwcl_vfwsnkvf0.unprotected_private_network.feature: + - vfwcl_vfwsnkvf0 + - unprotected_private_network.feature + vfwcl_vfwsnkvf0.vsn.abstract_vsn.disk.device.iops_vsn: + - vfwcl_vfwsnkvf0 + - vsn.abstract_vsn.disk.device.iops_vsn + vfwcl_vpkgvf0.vpg.abstract_vpg.host_vpg: + - vfwcl_vpkgvf0 + - vpg.abstract_vpg.host_vpg + vfwcl_vfwsnkvf0.vfw_vfw_private_2_port.abstract_vfw.network.outgoing.bytes_vfw_vfw_private_2_port: + - vfwcl_vfwsnkvf0 + - vfw_vfw_private_2_port.abstract_vfw.network.outgoing.bytes_vfw_vfw_private_2_port + vfwcl_vpkgvf0.vpg.abstract_vpg.disk.device.latency_vpg: + - vfwcl_vpkgvf0 + - vpg.abstract_vpg.disk.device.latency_vpg + vfwcl_vpkgvf0.vpg.abstract_vpg.disk.device.read.bytes_vpg: + - vfwcl_vpkgvf0 + - vpg.abstract_vpg.disk.device.read.bytes_vpg + vfwcl_vfwsnkvf0.vsn.abstract_vsn.memory_vsn: + - vfwcl_vfwsnkvf0 + - vsn.abstract_vsn.memory_vsn + vfwcl_vpkgvf0.vpg_vpg_private_1_port.abstract_vpg.network.outgoing.packets.rate_vpg_vpg_private_1_port: + - vfwcl_vpkgvf0 + - vpg_vpg_private_1_port.abstract_vpg.network.outgoing.packets.rate_vpg_vpg_private_1_port + vfwcl_vfwsnkvf0.vfw.abstract_vfw.instance_vfw: + - vfwcl_vfwsnkvf0 + - vfw.abstract_vfw.instance_vfw + vfwcl_vfwsnkvf0.unprotected_private_network.end_point: + - vfwcl_vfwsnkvf0 + - unprotected_private_network.end_point + vfwcl_vfwsnkvf0.vfw.abstract_vfw.disk.iops_vfw: + - vfwcl_vfwsnkvf0 + - vfw.abstract_vfw.disk.iops_vfw + vfwcl_vfwsnkvf0.vfw.abstract_vfw.memory_vfw: + - vfwcl_vfwsnkvf0 + - vfw.abstract_vfw.memory_vfw + vfwcl_vfwsnkvf0.vsn_vsn_private_1_port.abstract_vsn.network.incoming.bytes.rate_vsn_vsn_private_1_port: + - vfwcl_vfwsnkvf0 + - vsn_vsn_private_1_port.abstract_vsn.network.incoming.bytes.rate_vsn_vsn_private_1_port + vfwcl_vfwsnkvf0.vsn_vsn_private_1_port.abstract_vsn.network.outpoing.packets_vsn_vsn_private_1_port: + - vfwcl_vfwsnkvf0 + - vsn_vsn_private_1_port.abstract_vsn.network.outpoing.packets_vsn_vsn_private_1_port + vfwcl_vpkgvf0.vpg_vpg_private_0_port.abstract_vpg.network.outpoing.packets_vpg_vpg_private_0_port: + - vfwcl_vpkgvf0 + - vpg_vpg_private_0_port.abstract_vpg.network.outpoing.packets_vpg_vpg_private_0_port + vfwcl_vfwsnkvf0.vfw_vfw_private_2_port.abstract_vfw.network.outgoing.packets.rate_vfw_vfw_private_2_port: + - vfwcl_vfwsnkvf0 + - vfw_vfw_private_2_port.abstract_vfw.network.outgoing.packets.rate_vfw_vfw_private_2_port + vfwcl_vfwsnkvf0.vfw.abstract_vfw.disk.device.allocation_vfw: + - vfwcl_vfwsnkvf0 + - vfw.abstract_vfw.disk.device.allocation_vfw + vfwcl_vfwsnkvf0.vfw_vfw_private_0_port.abstract_vfw.network.outgoing.bytes_vfw_vfw_private_0_port: + - vfwcl_vfwsnkvf0 + - vfw_vfw_private_0_port.abstract_vfw.network.outgoing.bytes_vfw_vfw_private_0_port + vfwcl_vfwsnkvf0.vsn.abstract_vsn.disk.write.requests_vsn: + - vfwcl_vfwsnkvf0 + - vsn.abstract_vsn.disk.write.requests_vsn + vfwcl_vpkgvf0.vpg.abstract_vpg.vcpus_vpg: + - vfwcl_vpkgvf0 + - vpg.abstract_vpg.vcpus_vpg + vfwcl_vpkgvf0.vpg_vpg_private_1_port.abstract_vpg.network.outgoing.bytes.rate_vpg_vpg_private_1_port: + - vfwcl_vpkgvf0 + - vpg_vpg_private_1_port.abstract_vpg.network.outgoing.bytes.rate_vpg_vpg_private_1_port + vfwcl_vfwsnkvf0.vsn.abstract_vsn.disk.write.bytes_vsn: + - vfwcl_vfwsnkvf0 + - vsn.abstract_vsn.disk.write.bytes_vsn + vfwcl_vpkgvf0.vpg.abstract_vpg.disk.device.write.bytes.rate_vpg: + - vfwcl_vpkgvf0 + - vpg.abstract_vpg.disk.device.write.bytes.rate_vpg + vfwcl_vfwsnkvf0.vfw_vfw_private_2_port.abstract_vfw.network.outpoing.packets_vfw_vfw_private_2_port: + - vfwcl_vfwsnkvf0 + - vfw_vfw_private_2_port.abstract_vfw.network.outpoing.packets_vfw_vfw_private_2_port + vfwcl_vpkgvf0.vpg.abstract_vpg.disk.write.requests.rate_vpg: + - vfwcl_vpkgvf0 + - vpg.abstract_vpg.disk.write.requests.rate_vpg + vfwcl_vfwsnkvf0.vfw_vfw_private_0_port.abstract_vfw.attachment_vfw_vfw_private_0_port: + - vfwcl_vfwsnkvf0 + - vfw_vfw_private_0_port.abstract_vfw.attachment_vfw_vfw_private_0_port + vfwcl_vfwsnkvf0.vsn.abstract_vsn.disk.latency_vsn: + - vfwcl_vfwsnkvf0 + - vsn.abstract_vsn.disk.latency_vsn + vfwcl_vpkgvf0.vpg.abstract_vpg.cpu_vpg: + - vfwcl_vpkgvf0 + - vpg.abstract_vpg.cpu_vpg + vfwcl_vpkgvf0.vpg.abstract_vpg.disk.root.size_vpg: + - vfwcl_vpkgvf0 + - vpg.abstract_vpg.disk.root.size_vpg + vfwcl_vfwsnkvf0.vsn.abstract_vsn.host_vsn: + - vfwcl_vfwsnkvf0 + - vsn.abstract_vsn.host_vsn + vfwcl_vpkgvf0.vpg.abstract_vpg.disk.capacity_vpg: + - vfwcl_vpkgvf0 + - vpg.abstract_vpg.disk.capacity_vpg + vfwcl_vfwsnkvf0.vfw.abstract_vfw.memory.resident_vfw: + - vfwcl_vfwsnkvf0 + - vfw.abstract_vfw.memory.resident_vfw + vfwcl_vpkgvf0.vpg.abstract_vpg.memory.resident_vpg: + - vfwcl_vpkgvf0 + - vpg.abstract_vpg.memory.resident_vpg + vfwcl_vpkgvf0.vpg.abstract_vpg.binding_vpg: + - vfwcl_vpkgvf0 + - vpg.abstract_vpg.binding_vpg + vfwcl_vfwsnkvf0.vfw.abstract_vfw.cpu_vfw: + - vfwcl_vfwsnkvf0 + - vfw.abstract_vfw.cpu_vfw + vfwcl_vfwsnkvf0.vsn_vsn_private_1_port.abstract_vsn.attachment_vsn_vsn_private_1_port: + - vfwcl_vfwsnkvf0 + - vsn_vsn_private_1_port.abstract_vsn.attachment_vsn_vsn_private_1_port + vfwcl_vpkgvf0.vpg_vpg_private_0_port.abstract_vpg.attachment_vpg_vpg_private_0_port: + - vfwcl_vpkgvf0 + - vpg_vpg_private_0_port.abstract_vpg.attachment_vpg_vpg_private_0_port + vfwcl_vfwsnkvf0.vfw_vfw_private_1_port.abstract_vfw.network.outgoing.bytes_vfw_vfw_private_1_port: + - vfwcl_vfwsnkvf0 + - vfw_vfw_private_1_port.abstract_vfw.network.outgoing.bytes_vfw_vfw_private_1_port + vfwcl_vpkgvf0.vpg.abstract_vpg.disk.read.requests_vpg: + - vfwcl_vpkgvf0 + - vpg.abstract_vpg.disk.read.requests_vpg + vfwcl_vfwsnkvf0.vsn.abstract_vsn.disk.root.size_vsn: + - vfwcl_vfwsnkvf0 + - vsn.abstract_vsn.disk.root.size_vsn + vfwcl_vfwsnkvf0.vfw.abstract_vfw.disk.read.requests_vfw: + - vfwcl_vfwsnkvf0 + - vfw.abstract_vfw.disk.read.requests_vfw + vfwcl_vfwsnkvf0.vfw.abstract_vfw.disk.device.read.bytes.rate_vfw: + - vfwcl_vfwsnkvf0 + - vfw.abstract_vfw.disk.device.read.bytes.rate_vfw + vfwcl_vfwsnkvf0.vfw_vfw_private_2_port.abstract_vfw.network.incoming.packets_vfw_vfw_private_2_port: + - vfwcl_vfwsnkvf0 + - vfw_vfw_private_2_port.abstract_vfw.network.incoming.packets_vfw_vfw_private_2_port + vfwcl_vpkgvf0.vpg.abstract_vpg.disk.device.read.bytes.rate_vpg: + - vfwcl_vpkgvf0 + - vpg.abstract_vpg.disk.device.read.bytes.rate_vpg + vfwcl_vfwsnkvf0.vsn_vsn_private_0_port.abstract_vsn.network.incoming.bytes_vsn_vsn_private_0_port: + - vfwcl_vfwsnkvf0 + - vsn_vsn_private_0_port.abstract_vsn.network.incoming.bytes_vsn_vsn_private_0_port + vfwcl_vfwsnkvf0.vsn_vsn_private_0_port.abstract_vsn.network.outgoing.packets.rate_vsn_vsn_private_0_port: + - vfwcl_vfwsnkvf0 + - vsn_vsn_private_0_port.abstract_vsn.network.outgoing.packets.rate_vsn_vsn_private_0_port + vfwcl_vfwsnkvf0.vsn.abstract_vsn.disk.device.read.bytes.rate_vsn: + - vfwcl_vfwsnkvf0 + - vsn.abstract_vsn.disk.device.read.bytes.rate_vsn + vfwcl_vfwsnkvf0.vsn.abstract_vsn.disk.read.bytes_vsn: + - vfwcl_vfwsnkvf0 + - vsn.abstract_vsn.disk.read.bytes_vsn + vfwcl_vpkgvf0.vpg.abstract_vpg.disk.read.bytes_vpg: + - vfwcl_vpkgvf0 + - vpg.abstract_vpg.disk.read.bytes_vpg + vfwcl_vpkgvf0.vpg_vpg_private_0_port.abstract_vpg.feature_vpg_vpg_private_0_port: + - vfwcl_vpkgvf0 + - vpg_vpg_private_0_port.abstract_vpg.feature_vpg_vpg_private_0_port + vfwcl_vfwsnkvf0.vfw.abstract_vfw.disk.read.bytes_vfw: + - vfwcl_vfwsnkvf0 + - vfw.abstract_vfw.disk.read.bytes_vfw + vfwcl_vfwsnkvf0.vfw_vfw_private_0_port.abstract_vfw.network.incoming.packets_vfw_vfw_private_0_port: + - vfwcl_vfwsnkvf0 + - vfw_vfw_private_0_port.abstract_vfw.network.incoming.packets_vfw_vfw_private_0_port + vfwcl_vfwsnkvf0.vfw.abstract_vfw.disk.device.latency_vfw: + - vfwcl_vfwsnkvf0 + - vfw.abstract_vfw.disk.device.latency_vfw + vfwcl_vfwsnkvf0.vfw.abstract_vfw.endpoint_vfw: + - vfwcl_vfwsnkvf0 + - vfw.abstract_vfw.endpoint_vfw + vfwcl_vfwsnkvf0.vfw_vfw_private_0_port.abstract_vfw.feature_vfw_vfw_private_0_port: + - vfwcl_vfwsnkvf0 + - vfw_vfw_private_0_port.abstract_vfw.feature_vfw_vfw_private_0_port + vfwcl_vpkgvf0.vpg_vpg_private_0_port.abstract_vpg.network.outgoing.bytes.rate_vpg_vpg_private_0_port: + - vfwcl_vpkgvf0 + - vpg_vpg_private_0_port.abstract_vpg.network.outgoing.bytes.rate_vpg_vpg_private_0_port + vfwcl_vfwsnkvf0.vsn_vsn_private_1_port.abstract_vsn.network.incoming.packets.rate_vsn_vsn_private_1_port: + - vfwcl_vfwsnkvf0 + - vsn_vsn_private_1_port.abstract_vsn.network.incoming.packets.rate_vsn_vsn_private_1_port + vfwcl_vfwsnkvf0.vfw.abstract_vfw.disk.device.capacity_vfw: + - vfwcl_vfwsnkvf0 + - vfw.abstract_vfw.disk.device.capacity_vfw + vfwcl_vfwsnkvf0.vfw.abstract_vfw.disk.write.requests.rate_vfw: + - vfwcl_vfwsnkvf0 + - vfw.abstract_vfw.disk.write.requests.rate_vfw + vfwcl_vfwsnkvf0.vsn.abstract_vsn.disk.device.allocation_vsn: + - vfwcl_vfwsnkvf0 + - vsn.abstract_vsn.disk.device.allocation_vsn + vfwcl_vfwsnkvf0.vfw_vfw_private_1_port.abstract_vfw.network.incoming.packets_vfw_vfw_private_1_port: + - vfwcl_vfwsnkvf0 + - vfw_vfw_private_1_port.abstract_vfw.network.incoming.packets_vfw_vfw_private_1_port + vfwcl_vfwsnkvf0.vfw.abstract_vfw.disk.usage_vfw: + - vfwcl_vfwsnkvf0 + - vfw.abstract_vfw.disk.usage_vfw + vfwcl_vpkgvf0.vpg_vpg_private_0_port.abstract_vpg.network.outgoing.packets.rate_vpg_vpg_private_0_port: + - vfwcl_vpkgvf0 + - vpg_vpg_private_0_port.abstract_vpg.network.outgoing.packets.rate_vpg_vpg_private_0_port + vfwcl_vfwsnkvf0.vfw_vfw_private_2_port.abstract_vfw.network.incoming.packets.rate_vfw_vfw_private_2_port: + - vfwcl_vfwsnkvf0 + - vfw_vfw_private_2_port.abstract_vfw.network.incoming.packets.rate_vfw_vfw_private_2_port + vfwcl_vpkgvf0.vpg.abstract_vpg.memory.usage_vpg: + - vfwcl_vpkgvf0 + - vpg.abstract_vpg.memory.usage_vpg + vfwcl_vfwsnkvf0.vfw_vfw_private_1_port.abstract_vfw.feature_vfw_vfw_private_1_port: + - vfwcl_vfwsnkvf0 + - vfw_vfw_private_1_port.abstract_vfw.feature_vfw_vfw_private_1_port + vfwcl_vfwsnkvf0.protected_private_network.feature: + - vfwcl_vfwsnkvf0 + - protected_private_network.feature + vfwcl_vfwsnkvf0.vfw_vfw_private_2_port.abstract_vfw.attachment_vfw_vfw_private_2_port: + - vfwcl_vfwsnkvf0 + - vfw_vfw_private_2_port.abstract_vfw.attachment_vfw_vfw_private_2_port + vfwcl_vpkgvf0.vpg_vpg_private_0_port.abstract_vpg.network.incoming.bytes.rate_vpg_vpg_private_0_port: + - vfwcl_vpkgvf0 + - vpg_vpg_private_0_port.abstract_vpg.network.incoming.bytes.rate_vpg_vpg_private_0_port + vfwcl_vfwsnkvf0.vsn.abstract_vsn.memory.usage_vsn: + - vfwcl_vfwsnkvf0 + - vsn.abstract_vsn.memory.usage_vsn + vfwcl_vpkgvf0.vpg_vpg_private_1_port.abstract_vpg.network.incoming.packets.rate_vpg_vpg_private_1_port: + - vfwcl_vpkgvf0 + - vpg_vpg_private_1_port.abstract_vpg.network.incoming.packets.rate_vpg_vpg_private_1_port + vfwcl_vfwsnkvf0.vfw_vfw_private_1_port.abstract_vfw.network.outgoing.packets.rate_vfw_vfw_private_1_port: + - vfwcl_vfwsnkvf0 + - vfw_vfw_private_1_port.abstract_vfw.network.outgoing.packets.rate_vfw_vfw_private_1_port + vfwcl_vfwsnkvf0.vfw.abstract_vfw.disk.device.write.bytes_vfw: + - vfwcl_vfwsnkvf0 + - vfw.abstract_vfw.disk.device.write.bytes_vfw + vfwcl_vfwsnkvf0.vfw.abstract_vfw.scalable_vfw: + - vfwcl_vfwsnkvf0 + - vfw.abstract_vfw.scalable_vfw + vfwcl_vfwsnkvf0.vfw.abstract_vfw.memory.usage_vfw: + - vfwcl_vfwsnkvf0 + - vfw.abstract_vfw.memory.usage_vfw + vfwcl_vpkgvf0.vpg.abstract_vpg.disk.usage_vpg: + - vfwcl_vpkgvf0 + - vpg.abstract_vpg.disk.usage_vpg + vfwcl_vfwsnkvf0.vfw_vfw_private_1_port.abstract_vfw.network.incoming.bytes_vfw_vfw_private_1_port: + - vfwcl_vfwsnkvf0 + - vfw_vfw_private_1_port.abstract_vfw.network.incoming.bytes_vfw_vfw_private_1_port + vfwcl_vpkgvf0.vpg_vpg_private_1_port.abstract_vpg.network.incoming.bytes.rate_vpg_vpg_private_1_port: + - vfwcl_vpkgvf0 + - vpg_vpg_private_1_port.abstract_vpg.network.incoming.bytes.rate_vpg_vpg_private_1_port + vfwcl_vfwsnkvf0.vsn_vsn_private_1_port.abstract_vsn.network.incoming.bytes_vsn_vsn_private_1_port: + - vfwcl_vfwsnkvf0 + - vsn_vsn_private_1_port.abstract_vsn.network.incoming.bytes_vsn_vsn_private_1_port + vfwcl_vpkgvf0.vpg.abstract_vpg.disk.device.read.requests_vpg: + - vfwcl_vpkgvf0 + - vpg.abstract_vpg.disk.device.read.requests_vpg + vfwcl_vpkgvf0.vpg.abstract_vpg.feature_vpg: + - vfwcl_vpkgvf0 + - vpg.abstract_vpg.feature_vpg + vfwcl_vfwsnkvf0.vfw.abstract_vfw.cpu_util_vfw: + - vfwcl_vfwsnkvf0 + - vfw.abstract_vfw.cpu_util_vfw + vfwcl_vfwsnkvf0.vfw_vfw_private_2_port.abstract_vfw.feature_vfw_vfw_private_2_port: + - vfwcl_vfwsnkvf0 + - vfw_vfw_private_2_port.abstract_vfw.feature_vfw_vfw_private_2_port + vfwcl_vfwsnkvf0.vsn.abstract_vsn.disk.device.read.requests.rate_vsn: + - vfwcl_vfwsnkvf0 + - vsn.abstract_vsn.disk.device.read.requests.rate_vsn + vfwcl_vfwsnkvf0.vsn.abstract_vsn.disk.ephemeral.size_vsn: + - vfwcl_vfwsnkvf0 + - vsn.abstract_vsn.disk.ephemeral.size_vsn + vfwcl_vfwsnkvf0.vfw.abstract_vfw.binding_vfw: + - vfwcl_vfwsnkvf0 + - vfw.abstract_vfw.binding_vfw + vfwcl_vfwsnkvf0.vsn_vsn_private_0_port.abstract_vsn.attachment_vsn_vsn_private_0_port: + - vfwcl_vfwsnkvf0 + - vsn_vsn_private_0_port.abstract_vsn.attachment_vsn_vsn_private_0_port + vfwcl_vfwsnkvf0.vfw.abstract_vfw.disk.allocation_vfw: + - vfwcl_vfwsnkvf0 + - vfw.abstract_vfw.disk.allocation_vfw + vfwcl_vfwsnkvf0.vsn_vsn_private_1_port.abstract_vsn.binding_vsn_vsn_private_1_port: + - vfwcl_vfwsnkvf0 + - vsn_vsn_private_1_port.abstract_vsn.binding_vsn_vsn_private_1_port + vfwcl_vpkgvf0.vpg.abstract_vpg.disk.read.bytes.rate_vpg: + - vfwcl_vpkgvf0 + - vpg.abstract_vpg.disk.read.bytes.rate_vpg + vfwcl_vfwsnkvf0.vsn.abstract_vsn.memory.resident_vsn: + - vfwcl_vfwsnkvf0 + - vsn.abstract_vsn.memory.resident_vsn + vfwcl_vpkgvf0.vpg_vpg_private_1_port.abstract_vpg.feature_vpg_vpg_private_1_port: + - vfwcl_vpkgvf0 + - vpg_vpg_private_1_port.abstract_vpg.feature_vpg_vpg_private_1_port + vfwcl_vpkgvf0.vpg_vpg_private_1_port.abstract_vpg.network.incoming.bytes_vpg_vpg_private_1_port: + - vfwcl_vpkgvf0 + - vpg_vpg_private_1_port.abstract_vpg.network.incoming.bytes_vpg_vpg_private_1_port + vfwcl_vfwsnkvf0.vsn.abstract_vsn.binding_vsn: + - vfwcl_vfwsnkvf0 + - vsn.abstract_vsn.binding_vsn + vfwcl_vfwsnkvf0.vfw.abstract_vfw.cpu.delta_vfw: + - vfwcl_vfwsnkvf0 + - vfw.abstract_vfw.cpu.delta_vfw + vfwcl_vpkgvf0.vpg.abstract_vpg.os_vpg: + - vfwcl_vpkgvf0 + - vpg.abstract_vpg.os_vpg + vfwcl_vfwsnkvf0.vsn_vsn_private_0_port.abstract_vsn.network.outpoing.packets_vsn_vsn_private_0_port: + - vfwcl_vfwsnkvf0 + - vsn_vsn_private_0_port.abstract_vsn.network.outpoing.packets_vsn_vsn_private_0_port + vfwcl_vpkgvf0.vpg_vpg_private_1_port.abstract_vpg.network.incoming.packets_vpg_vpg_private_1_port: + - vfwcl_vpkgvf0 + - vpg_vpg_private_1_port.abstract_vpg.network.incoming.packets_vpg_vpg_private_1_port + vfwcl_vfwsnkvf0.vsn_vsn_private_0_port.abstract_vsn.network.incoming.packets.rate_vsn_vsn_private_0_port: + - vfwcl_vfwsnkvf0 + - vsn_vsn_private_0_port.abstract_vsn.network.incoming.packets.rate_vsn_vsn_private_0_port + vfwcl_vfwsnkvf0.vsn_vsn_private_0_port.abstract_vsn.network.incoming.bytes.rate_vsn_vsn_private_0_port: + - vfwcl_vfwsnkvf0 + - vsn_vsn_private_0_port.abstract_vsn.network.incoming.bytes.rate_vsn_vsn_private_0_port + vfwcl_vfwsnkvf0.vsn_vsn_private_1_port.abstract_vsn.network.outgoing.bytes_vsn_vsn_private_1_port: + - vfwcl_vfwsnkvf0 + - vsn_vsn_private_1_port.abstract_vsn.network.outgoing.bytes_vsn_vsn_private_1_port + vfwcl_vpkgvf0.vpg_vpg_private_1_port.abstract_vpg.network.outpoing.packets_vpg_vpg_private_1_port: + - vfwcl_vpkgvf0 + - vpg_vpg_private_1_port.abstract_vpg.network.outpoing.packets_vpg_vpg_private_1_port + vfwcl_vpkgvf0.vpg.abstract_vpg.cpu.delta_vpg: + - vfwcl_vpkgvf0 + - vpg.abstract_vpg.cpu.delta_vpg + vfwcl_vfwsnkvf0.vfw_vfw_private_0_port.abstract_vfw.binding_vfw_vfw_private_0_port: + - vfwcl_vfwsnkvf0 + - vfw_vfw_private_0_port.abstract_vfw.binding_vfw_vfw_private_0_port + vfwcl_vfwsnkvf0.vfw.abstract_vfw.os_vfw: + - vfwcl_vfwsnkvf0 + - vfw.abstract_vfw.os_vfw + vfwcl_vpkgvf0.vpg.abstract_vpg.disk.latency_vpg: + - vfwcl_vpkgvf0 + - vpg.abstract_vpg.disk.latency_vpg + vfwcl_vfwsnkvf0.vsn.abstract_vsn.disk.read.bytes.rate_vsn: + - vfwcl_vfwsnkvf0 + - vsn.abstract_vsn.disk.read.bytes.rate_vsn + vfwcl_vfwsnkvf0.vsn_vsn_private_1_port.abstract_vsn.network.outgoing.bytes.rate_vsn_vsn_private_1_port: + - vfwcl_vfwsnkvf0 + - vsn_vsn_private_1_port.abstract_vsn.network.outgoing.bytes.rate_vsn_vsn_private_1_port + vfwcl_vfwsnkvf0.vfw.abstract_vfw.disk.device.write.bytes.rate_vfw: + - vfwcl_vfwsnkvf0 + - vfw.abstract_vfw.disk.device.write.bytes.rate_vfw + vfwcl_vfwsnkvf0.vfw_vfw_private_1_port.abstract_vfw.network.incoming.packets.rate_vfw_vfw_private_1_port: + - vfwcl_vfwsnkvf0 + - vfw_vfw_private_1_port.abstract_vfw.network.incoming.packets.rate_vfw_vfw_private_1_port + vfwcl_vpkgvf0.vpg.abstract_vpg.disk.device.allocation_vpg: + - vfwcl_vpkgvf0 + - vpg.abstract_vpg.disk.device.allocation_vpg + vfwcl_vpkgvf0.vpg_vpg_private_1_port.abstract_vpg.network.outgoing.bytes_vpg_vpg_private_1_port: + - vfwcl_vpkgvf0 + - vpg_vpg_private_1_port.abstract_vpg.network.outgoing.bytes_vpg_vpg_private_1_port + vfwcl_vpkgvf0.vpg_vpg_private_0_port.abstract_vpg.network.incoming.packets.rate_vpg_vpg_private_0_port: + - vfwcl_vpkgvf0 + - vpg_vpg_private_0_port.abstract_vpg.network.incoming.packets.rate_vpg_vpg_private_0_port + vfwcl_vfwsnkvf0.vfw_vfw_private_0_port.abstract_vfw.network.outgoing.packets.rate_vfw_vfw_private_0_port: + - vfwcl_vfwsnkvf0 + - vfw_vfw_private_0_port.abstract_vfw.network.outgoing.packets.rate_vfw_vfw_private_0_port + vfwcl_vpkgvf0.vpg.abstract_vpg.instance_vpg: + - vfwcl_vpkgvf0 + - vpg.abstract_vpg.instance_vpg + vfwcl_vfwsnkvf0.vsn.abstract_vsn.vcpus_vsn: + - vfwcl_vfwsnkvf0 + - vsn.abstract_vsn.vcpus_vsn + vfwcl_vfwsnkvf0.vsn_vsn_private_0_port.abstract_vsn.network.outgoing.bytes_vsn_vsn_private_0_port: + - vfwcl_vfwsnkvf0 + - vsn_vsn_private_0_port.abstract_vsn.network.outgoing.bytes_vsn_vsn_private_0_port + vfwcl_vfwsnkvf0.vsn.abstract_vsn.endpoint_vsn: + - vfwcl_vfwsnkvf0 + - vsn.abstract_vsn.endpoint_vsn + vfwcl_vfwsnkvf0.vsn.abstract_vsn.disk.usage_vsn: + - vfwcl_vfwsnkvf0 + - vsn.abstract_vsn.disk.usage_vsn + vfwcl_vfwsnkvf0.vfw.abstract_vfw.disk.write.bytes.rate_vfw: + - vfwcl_vfwsnkvf0 + - vfw.abstract_vfw.disk.write.bytes.rate_vfw + vfwcl_vfwsnkvf0.vsn.abstract_vsn.disk.device.latency_vsn: + - vfwcl_vfwsnkvf0 + - vsn.abstract_vsn.disk.device.latency_vsn + vfwcl_vfwsnkvf0.vfw_vfw_private_2_port.abstract_vfw.network.outgoing.bytes.rate_vfw_vfw_private_2_port: + - vfwcl_vfwsnkvf0 + - vfw_vfw_private_2_port.abstract_vfw.network.outgoing.bytes.rate_vfw_vfw_private_2_port + vfwcl_vpkgvf0.vpg.abstract_vpg.memory_vpg: + - vfwcl_vpkgvf0 + - vpg.abstract_vpg.memory_vpg + vfwcl_vfwsnkvf0.vsn.abstract_vsn.cpu.delta_vsn: + - vfwcl_vfwsnkvf0 + - vsn.abstract_vsn.cpu.delta_vsn + vfwcl_vfwsnkvf0.unprotected_private_network.attachment: + - vfwcl_vfwsnkvf0 + - unprotected_private_network.attachment + vfwcl_vfwsnkvf0.vfw_vfw_private_1_port.abstract_vfw.network.outpoing.packets_vfw_vfw_private_1_port: + - vfwcl_vfwsnkvf0 + - vfw_vfw_private_1_port.abstract_vfw.network.outpoing.packets_vfw_vfw_private_1_port + vfwcl_vfwsnkvf0.vfw.abstract_vfw.disk.ephemeral.size_vfw: + - vfwcl_vfwsnkvf0 + - vfw.abstract_vfw.disk.ephemeral.size_vfw + vfwcl_vfwsnkvf0.vfw_vfw_private_1_port.abstract_vfw.network.incoming.bytes.rate_vfw_vfw_private_1_port: + - vfwcl_vfwsnkvf0 + - vfw_vfw_private_1_port.abstract_vfw.network.incoming.bytes.rate_vfw_vfw_private_1_port + vfwcl_vfwsnkvf0.vfw_vfw_private_2_port.abstract_vfw.binding_vfw_vfw_private_2_port: + - vfwcl_vfwsnkvf0 + - vfw_vfw_private_2_port.abstract_vfw.binding_vfw_vfw_private_2_port + vfwcl_vfwsnkvf0.vsn.abstract_vsn.disk.iops_vsn: + - vfwcl_vfwsnkvf0 + - vsn.abstract_vsn.disk.iops_vsn + vfwcl_vfwsnkvf0.vsn.abstract_vsn.disk.device.capacity_vsn: + - vfwcl_vfwsnkvf0 + - vsn.abstract_vsn.disk.device.capacity_vsn + vfwcl_vpkgvf0.vpg.abstract_vpg.disk.device.write.requests.rate_vpg: + - vfwcl_vpkgvf0 + - vpg.abstract_vpg.disk.device.write.requests.rate_vpg + vfwcl_vpkgvf0.vpg.abstract_vpg.disk.write.bytes.rate_vpg: + - vfwcl_vpkgvf0 + - vpg.abstract_vpg.disk.write.bytes.rate_vpg + vfwcl_vfwsnkvf0.vfw_vfw_private_0_port.abstract_vfw.network.outpoing.packets_vfw_vfw_private_0_port: + - vfwcl_vfwsnkvf0 + - vfw_vfw_private_0_port.abstract_vfw.network.outpoing.packets_vfw_vfw_private_0_port + vfwcl_vpkgvf0.vpg_vpg_private_1_port.abstract_vpg.attachment_vpg_vpg_private_1_port: + - vfwcl_vpkgvf0 + - vpg_vpg_private_1_port.abstract_vpg.attachment_vpg_vpg_private_1_port + vfwcl_vfwsnkvf0.vsn.abstract_vsn.disk.device.write.requests.rate_vsn: + - vfwcl_vfwsnkvf0 + - vsn.abstract_vsn.disk.device.write.requests.rate_vsn + vfwcl_vpkgvf0.vpg.abstract_vpg.disk.write.requests_vpg: + - vfwcl_vpkgvf0 + - vpg.abstract_vpg.disk.write.requests_vpg + vfwcl_vfwsnkvf0.protected_private_network.link: + - vfwcl_vfwsnkvf0 + - protected_private_network.link + vfwcl_vpkgvf0.vpg.abstract_vpg.disk.device.usage_vpg: + - vfwcl_vpkgvf0 + - vpg.abstract_vpg.disk.device.usage_vpg + vfwcl_vfwsnkvf0.vsn.abstract_vsn.disk.device.write.bytes.rate_vsn: + - vfwcl_vfwsnkvf0 + - vsn.abstract_vsn.disk.device.write.bytes.rate_vsn + vfwcl_vfwsnkvf0.vsn.abstract_vsn.disk.write.bytes.rate_vsn: + - vfwcl_vfwsnkvf0 + - vsn.abstract_vsn.disk.write.bytes.rate_vsn + vfwcl_vfwsnkvf0.vsn_vsn_private_0_port.abstract_vsn.network.outgoing.bytes.rate_vsn_vsn_private_0_port: + - vfwcl_vfwsnkvf0 + - vsn_vsn_private_0_port.abstract_vsn.network.outgoing.bytes.rate_vsn_vsn_private_0_port + vfwcl_vfwsnkvf0.vfw.abstract_vfw.disk.device.read.requests.rate_vfw: + - vfwcl_vfwsnkvf0 + - vfw.abstract_vfw.disk.device.read.requests.rate_vfw + vfwcl_vfwsnkvf0.vfw.abstract_vfw.feature_vfw: + - vfwcl_vfwsnkvf0 + - vfw.abstract_vfw.feature_vfw + vfwcl_vfwsnkvf0.vsn.abstract_vsn.disk.allocation_vsn: + - vfwcl_vfwsnkvf0 + - vsn.abstract_vsn.disk.allocation_vsn + vfwcl_vfwsnkvf0.vsn.abstract_vsn.instance_vsn: + - vfwcl_vfwsnkvf0 + - vsn.abstract_vsn.instance_vsn + vfwcl_vfwsnkvf0.vfw.abstract_vfw.disk.device.read.requests_vfw: + - vfwcl_vfwsnkvf0 + - vfw.abstract_vfw.disk.device.read.requests_vfw + vfwcl_vfwsnkvf0.vsn.abstract_vsn.cpu_util_vsn: + - vfwcl_vfwsnkvf0 + - vsn.abstract_vsn.cpu_util_vsn + vfwcl_vfwsnkvf0.vsn_vsn_private_1_port.abstract_vsn.feature_vsn_vsn_private_1_port: + - vfwcl_vfwsnkvf0 + - vsn_vsn_private_1_port.abstract_vsn.feature_vsn_vsn_private_1_port + vfwcl_vpkgvf0.vpg.abstract_vpg.disk.allocation_vpg: + - vfwcl_vpkgvf0 + - vpg.abstract_vpg.disk.allocation_vpg + vfwcl_vfwsnkvf0.vsn.abstract_vsn.disk.capacity_vsn: + - vfwcl_vfwsnkvf0 + - vsn.abstract_vsn.disk.capacity_vsn + vfwcl_vpkgvf0.vpg.abstract_vpg.disk.write.bytes_vpg: + - vfwcl_vpkgvf0 + - vpg.abstract_vpg.disk.write.bytes_vpg + vfwcl_vfwsnkvf0.vfw_vfw_private_0_port.abstract_vfw.network.outgoing.bytes.rate_vfw_vfw_private_0_port: + - vfwcl_vfwsnkvf0 + - vfw_vfw_private_0_port.abstract_vfw.network.outgoing.bytes.rate_vfw_vfw_private_0_port + vfwcl_vfwsnkvf0.vsn_vsn_private_1_port.abstract_vsn.network.incoming.packets_vsn_vsn_private_1_port: + - vfwcl_vfwsnkvf0 + - vsn_vsn_private_1_port.abstract_vsn.network.incoming.packets_vsn_vsn_private_1_port + vfwcl_vpkgvf0.vpg.abstract_vpg.endpoint_vpg: + - vfwcl_vpkgvf0 + - vpg.abstract_vpg.endpoint_vpg + vfwcl_vfwsnkvf0.unprotected_private_network.link: + - vfwcl_vfwsnkvf0 + - unprotected_private_network.link + vfwcl_vpkgvf0.vpg.abstract_vpg.disk.iops_vpg: + - vfwcl_vpkgvf0 + - vpg.abstract_vpg.disk.iops_vpg + vfwcl_vfwsnkvf0.vfw_vfw_private_0_port.abstract_vfw.network.incoming.packets.rate_vfw_vfw_private_0_port: + - vfwcl_vfwsnkvf0 + - vfw_vfw_private_0_port.abstract_vfw.network.incoming.packets.rate_vfw_vfw_private_0_port + vfwcl_vfwsnkvf0.vsn.abstract_vsn.scalable_vsn: + - vfwcl_vfwsnkvf0 + - vsn.abstract_vsn.scalable_vsn + vfwcl_vfwsnkvf0.vfw_vfw_private_1_port.abstract_vfw.network.outgoing.bytes.rate_vfw_vfw_private_1_port: + - vfwcl_vfwsnkvf0 + - vfw_vfw_private_1_port.abstract_vfw.network.outgoing.bytes.rate_vfw_vfw_private_1_port + vfwcl_vfwsnkvf0.vfw_vfw_private_1_port.abstract_vfw.attachment_vfw_vfw_private_1_port: + - vfwcl_vfwsnkvf0 + - vfw_vfw_private_1_port.abstract_vfw.attachment_vfw_vfw_private_1_port + vfwcl_vfwsnkvf0.vsn_vsn_private_0_port.abstract_vsn.feature_vsn_vsn_private_0_port: + - vfwcl_vfwsnkvf0 + - vsn_vsn_private_0_port.abstract_vsn.feature_vsn_vsn_private_0_port + vfwcl_vfwsnkvf0.vsn_vsn_private_0_port.abstract_vsn.network.incoming.packets_vsn_vsn_private_0_port: + - vfwcl_vfwsnkvf0 + - vsn_vsn_private_0_port.abstract_vsn.network.incoming.packets_vsn_vsn_private_0_port + vfwcl_vfwsnkvf0.vfw_vfw_private_0_port.abstract_vfw.network.incoming.bytes.rate_vfw_vfw_private_0_port: + - vfwcl_vfwsnkvf0 + - vfw_vfw_private_0_port.abstract_vfw.network.incoming.bytes.rate_vfw_vfw_private_0_port + vfwcl_vfwsnkvf0.vsn.abstract_vsn.disk.device.read.bytes_vsn: + - vfwcl_vfwsnkvf0 + - vsn.abstract_vsn.disk.device.read.bytes_vsn + vfwcl_vfwsnkvf0.vfw.abstract_vfw.disk.write.requests_vfw: + - vfwcl_vfwsnkvf0 + - vfw.abstract_vfw.disk.write.requests_vfw + vfwcl_vfwsnkvf0.protected_private_network.end_point: + - vfwcl_vfwsnkvf0 + - protected_private_network.end_point + vfwcl_vfwsnkvf0.vfw_vfw_private_0_port.abstract_vfw.network.incoming.bytes_vfw_vfw_private_0_port: + - vfwcl_vfwsnkvf0 + - vfw_vfw_private_0_port.abstract_vfw.network.incoming.bytes_vfw_vfw_private_0_port + vfwcl_vpkgvf0.vpg_vpg_private_1_port.abstract_vpg.binding_vpg_vpg_private_1_port: + - vfwcl_vpkgvf0 + - vpg_vpg_private_1_port.abstract_vpg.binding_vpg_vpg_private_1_port + vfwcl_vfwsnkvf0.vfw.abstract_vfw.disk.write.bytes_vfw: + - vfwcl_vfwsnkvf0 + - vfw.abstract_vfw.disk.write.bytes_vfw + vfwcl_vfwsnkvf0.vsn.abstract_vsn.disk.device.read.requests_vsn: + - vfwcl_vfwsnkvf0 + - vsn.abstract_vsn.disk.device.read.requests_vsn + requirements: + vfwcl_vfwsnkvf0.vfw.abstract_vfw.local_storage_vfw: + - vfwcl_vfwsnkvf0 + - vfw.abstract_vfw.local_storage_vfw + vfwcl_vfwsnkvf0.protected_private_network.dependency: + - vfwcl_vfwsnkvf0 + - protected_private_network.dependency + vfwcl_vfwsnkvf0.unprotected_private_network.dependency: + - vfwcl_vfwsnkvf0 + - unprotected_private_network.dependency + vfwcl_vfwsnkvf0.vsn.abstract_vsn.dependency_vsn: + - vfwcl_vfwsnkvf0 + - vsn.abstract_vsn.dependency_vsn + vfwcl_vpkgvf0.vpg.abstract_vpg.local_storage_vpg: + - vfwcl_vpkgvf0 + - vpg.abstract_vpg.local_storage_vpg + vfwcl_vpkgvf0.vpg_vpg_private_1_port.abstract_vpg.link_vpg_vpg_private_1_port: + - vfwcl_vpkgvf0 + - vpg_vpg_private_1_port.abstract_vpg.link_vpg_vpg_private_1_port + vfwcl_vpkgvf0.vpg_vpg_private_1_port.abstract_vpg.dependency_vpg_vpg_private_1_port: + - vfwcl_vpkgvf0 + - vpg_vpg_private_1_port.abstract_vpg.dependency_vpg_vpg_private_1_port + vfwcl_vfwsnkvf0.vfw_vfw_private_2_port.abstract_vfw.link_vfw_vfw_private_2_port: + - vfwcl_vfwsnkvf0 + - vfw_vfw_private_2_port.abstract_vfw.link_vfw_vfw_private_2_port + vfwcl_vfwsnkvf0.vfw.abstract_vfw.dependency_vfw: + - vfwcl_vfwsnkvf0 + - vfw.abstract_vfw.dependency_vfw + vfwcl_vfwsnkvf0.vsn_vsn_private_0_port.abstract_vsn.dependency_vsn_vsn_private_0_port: + - vfwcl_vfwsnkvf0 + - vsn_vsn_private_0_port.abstract_vsn.dependency_vsn_vsn_private_0_port + vfwcl_vfwsnkvf0.vfw_vfw_private_0_port.abstract_vfw.link_vfw_vfw_private_0_port: + - vfwcl_vfwsnkvf0 + - vfw_vfw_private_0_port.abstract_vfw.link_vfw_vfw_private_0_port + vfwcl_vfwsnkvf0.vsn_vsn_private_1_port.abstract_vsn.link_vsn_vsn_private_1_port: + - vfwcl_vfwsnkvf0 + - vsn_vsn_private_1_port.abstract_vsn.link_vsn_vsn_private_1_port + vfwcl_vfwsnkvf0.vfw_vfw_private_2_port.abstract_vfw.dependency_vfw_vfw_private_2_port: + - vfwcl_vfwsnkvf0 + - vfw_vfw_private_2_port.abstract_vfw.dependency_vfw_vfw_private_2_port + vfwcl_vfwsnkvf0.vfw_vfw_private_0_port.abstract_vfw.dependency_vfw_vfw_private_0_port: + - vfwcl_vfwsnkvf0 + - vfw_vfw_private_0_port.abstract_vfw.dependency_vfw_vfw_private_0_port + vfwcl_vpkgvf0.vpg_vpg_private_0_port.abstract_vpg.dependency_vpg_vpg_private_0_port: + - vfwcl_vpkgvf0 + - vpg_vpg_private_0_port.abstract_vpg.dependency_vpg_vpg_private_0_port + vfwcl_vfwsnkvf0.vsn.abstract_vsn.local_storage_vsn: + - vfwcl_vfwsnkvf0 + - vsn.abstract_vsn.local_storage_vsn + vfwcl_vfwsnkvf0.vfw_vfw_private_1_port.abstract_vfw.link_vfw_vfw_private_1_port: + - vfwcl_vfwsnkvf0 + - vfw_vfw_private_1_port.abstract_vfw.link_vfw_vfw_private_1_port + vfwcl_vfwsnkvf0.vsn_vsn_private_0_port.abstract_vsn.link_vsn_vsn_private_0_port: + - vfwcl_vfwsnkvf0 + - vsn_vsn_private_0_port.abstract_vsn.link_vsn_vsn_private_0_port + vfwcl_vpkgvf0.vpg_vpg_private_0_port.abstract_vpg.link_vpg_vpg_private_0_port: + - vfwcl_vpkgvf0 + - vpg_vpg_private_0_port.abstract_vpg.link_vpg_vpg_private_0_port + vfwcl_vfwsnkvf0.vsn_vsn_private_1_port.abstract_vsn.dependency_vsn_vsn_private_1_port: + - vfwcl_vfwsnkvf0 + - vsn_vsn_private_1_port.abstract_vsn.dependency_vsn_vsn_private_1_port + vfwcl_vfwsnkvf0.vfw_vfw_private_1_port.abstract_vfw.dependency_vfw_vfw_private_1_port: + - vfwcl_vfwsnkvf0 + - vfw_vfw_private_1_port.abstract_vfw.dependency_vfw_vfw_private_1_port + vfwcl_vpkgvf0.vpg.abstract_vpg.dependency_vpg: + - vfwcl_vpkgvf0 + - vpg.abstract_vpg.dependency_vpg diff --git a/tests/data/service-TestPnfVsp-template.yml b/tests/data/service-TestPnfVsp-template.yml new file mode 100644 index 0000000..b1ffa37 --- /dev/null +++ b/tests/data/service-TestPnfVsp-template.yml @@ -0,0 +1,146 @@ +tosca_definitions_version: tosca_simple_yaml_1_1 +metadata: + invariantUUID: 25871897-1960-445b-8c4f-78111fab1e32 + UUID: fda7e33f-775d-496c-a2a9-76bb8814fed7 + name: test_service_pnf_vsp + description: service + type: Service + category: Network Service + serviceType: '' + serviceRole: '' + instantiationType: A-la-carte + serviceEcompNaming: true + ecompGeneratedNaming: true + namingPolicy: '' + environmentContext: General_Revenue-Bearing + serviceFunction: '' +imports: +- nodes: + file: nodes.yml +- datatypes: + file: data.yml +- capabilities: + file: capabilities.yml +- relationships: + file: relationships.yml +- groups: + file: groups.yml +- policies: + file: policies.yml +- annotations: + file: annotations.yml +- service-test_service_pnf_vsp-interface: + file: service-TestServicePnfVsp-template-interface.yml +- resource-test_pnf_vsp: + file: resource-TestPnfVsp-template.yml +- resource-test_pnf_vsp-interface: + file: resource-TestPnfVsp-template-interface.yml +topology_template: + inputs: + skip_post_instantiation_configuration: + default: true + type: boolean + required: false + controller_actor: + default: SO-REF-DATA + type: string + required: false + cds_model_version: + type: string + required: false + cds_model_name: + type: string + required: false + node_templates: + test_pnf_vsp 0: + type: org.openecomp.resource.pnf.TestPnfVsp + metadata: + invariantUUID: d52250cb-f4e4-4e16-880b-de0c3a9a3273 + UUID: 081bc1b2-0283-44f6-80a2-f0f3672fec05 + customizationUUID: dc0627b2-cdb6-40a7-8510-3ee70d98ce66 + version: '1.0' + name: test_pnf_vsp + description: PNF + type: PNF + category: Generic + subcategory: Abstract + resourceVendor: test-vendor + resourceVendorRelease: '1.0' + resourceVendorModelNumber: '' + properties: + skip_post_instantiation_configuration: false + controller_actor: CDS + sdnc_model_version: 2.0.0 + sdnc_artifact_name: vnf + sdnc_model_name: vSIM_CNF_CDS + capabilities: + pnfextcp_1.network.incoming.packets.rate: + properties: + name: network.incoming.packets.rate + pnfextcp_1.network.outgoing.packets.rate: + properties: + name: network.outgoing.packets.rate + pnfextcp_1.network.incoming.bytes: + properties: + name: network.incoming.bytes + pnfextcp_1.network.outgoing.bytes.rate: + properties: + name: network.outgoing.bytes.rate + pnfextcp_1.network.outpoing.packets: + properties: + name: network.outpoing.packets + pnfextcp_1.network.outgoing.bytes: + properties: + name: network.outgoing.bytes + pnfextcp_1.network.incoming.bytes.rate: + properties: + name: network.incoming.bytes.rate + pnfextcp_1.network.incoming.packets: + properties: + name: network.incoming.packets + substitution_mappings: + node_type: org.openecomp.service.TestServicePnfVsp8 + capabilities: + test_pnf_vsp0.pnfextcp_1.network.outpoing.packets: + - test_pnf_vsp 0 + - pnfextcp_1.network.outpoing.packets + test_pnf_vsp0.pnfextcp_1.feature: + - test_pnf_vsp 0 + - pnfextcp_1.feature + test_pnf_vsp0.pnfextcp_1.network.incoming.bytes: + - test_pnf_vsp 0 + - pnfextcp_1.network.incoming.bytes + test_pnf_vsp0.pnfextcp_1.network.incoming.packets: + - test_pnf_vsp 0 + - pnfextcp_1.network.incoming.packets + test_pnf_vsp0.pnfextcp_1.port_mirroring: + - test_pnf_vsp 0 + - pnfextcp_1.port_mirroring + test_pnf_vsp0.pnfextcp_1.network.incoming.packets.rate: + - test_pnf_vsp 0 + - pnfextcp_1.network.incoming.packets.rate + test_pnf_vsp0.pnfextcp_1.network.outgoing.bytes.rate: + - test_pnf_vsp 0 + - pnfextcp_1.network.outgoing.bytes.rate + test_pnf_vsp0.pnfextcp_1.network.incoming.bytes.rate: + - test_pnf_vsp 0 + - pnfextcp_1.network.incoming.bytes.rate + test_pnf_vsp0.pnfextcp_1.forwarder: + - test_pnf_vsp 0 + - pnfextcp_1.forwarder + test_pnf_vsp0.pnfextcp_1.network.outgoing.packets.rate: + - test_pnf_vsp 0 + - pnfextcp_1.network.outgoing.packets.rate + test_pnf_vsp0.pnfextcp_1.network.outgoing.bytes: + - test_pnf_vsp 0 + - pnfextcp_1.network.outgoing.bytes + requirements: + test_pnf_vsp0.pnfextcp_1.dependency: + - test_pnf_vsp 0 + - pnfextcp_1.dependency + test_pnf_vsp0.pnfextcp_1.binding: + - test_pnf_vsp 0 + - pnfextcp_1.binding + test_pnf_vsp0.pnfextcp_1.link: + - test_pnf_vsp 0 + - pnfextcp_1.link diff --git a/tests/data/service-TestServiceFyx-template.yml b/tests/data/service-TestServiceFyx-template.yml new file mode 100644 index 0000000..179ff43 --- /dev/null +++ b/tests/data/service-TestServiceFyx-template.yml @@ -0,0 +1,646 @@ +tosca_definitions_version: tosca_simple_yaml_1_1 +metadata: + invariantUUID: 0ca2fd47-4f5b-4d0f-ad83-f458a0e6c4cd + UUID: 2ad116d1-5219-41de-b2ee-d00f100d7f00 + name: freeRadiusInnova_SERVICE123 + description: service + type: Service + category: Network Service + serviceType: '' + serviceRole: '' + instantiationType: A-la-carte + serviceEcompNaming: true + ecompGeneratedNaming: true + namingPolicy: '' + environmentContext: General_Revenue-Bearing + serviceFunction: '' +imports: +- nodes: + file: nodes.yml +- datatypes: + file: data.yml +- capabilities: + file: capabilities.yml +- relationships: + file: relationships.yml +- groups: + file: groups.yml +- policies: + file: policies.yml +- annotations: + file: annotations.yml +- service-freeRadiusInnova_SERVICE123-interface: + file: service-FreeradiusinnovaService123-template-interface.yml +- resource-NeutronNet: + file: resource-Neutronnet-template.yml +- resource-freeRadiusInnova_VF: + file: resource-FreeradiusinnovaVf-template.yml +- resource-freeRadiusInnova_VF-interface: + file: resource-FreeradiusinnovaVf-template-interface.yml +topology_template: + inputs: + skip_post_instantiation_configuration: + default: true + type: boolean + required: false + controller_actor: + default: SO-REF-DATA + type: string + required: false + cds_model_version: + type: string + required: false + cds_model_name: + type: string + required: false + node_templates: + NeutronNet 0: + type: org.openecomp.resource.vl.nodes.heat.network.neutron.Net + metadata: + invariantUUID: 4084c513-5149-456d-9be0-efc503058799 + UUID: e12cedf4-fd3f-4d76-ae2a-0368eaee40dc + customizationUUID: ae1df985-3313-4a8f-93e6-efbc71fd3938 + version: '1.0' + name: NeutronNet + description: Represents a network service with optional subnets and advanced configurations. + type: VL + category: Generic + subcategory: Network Elements + resourceVendor: ONAP (Tosca) + resourceVendorRelease: 1.0.0.wd03 + resourceVendorModelNumber: '' + properties: + dhcp_enabled: true + shared: false + ip_version: 4 + admin_state_up: true + capabilities: + end_point: + properties: + protocol: tcp + initiator: source + network_name: PRIVATE + secure: false + freeRadiusInnova_VF 0: + type: org.openecomp.resource.vf.FreeradiusinnovaVf + metadata: + invariantUUID: e7ba6a8b-ece2-4f4e-bd1a-0f5ca461eedc + UUID: e65f6295-82d1-4e34-8ce9-4df5a81f98a0 + customizationUUID: 674140ec-1aba-4c59-b775-9cb239fc5c70 + version: '1.0' + name: freeRadiusInnova_VF + description: VF + type: VF + category: Generic + subcategory: Abstract + resourceVendor: VNFVendor + resourceVendorRelease: '1.0' + resourceVendorModelNumber: '' + properties: + vf_module_id: FreeRadiusInnova-VF-module + freeRadius_image_name: ubuntu-16.04-daily + skip_post_instantiation_configuration: true + nf_naming: + ecomp_generated_naming: true + user_plane_net_name: user-plane-net, + multi_stage_design: 'false' + controller_actor: SO-REF-DATA + availability_zone_max_count: 1 + control_plane_net_name: control-plane-net + freeRadius_key_name: innova + freeRadius_flavor_name: onap.small + vnf_name: FreeRadiusInnova + freeRadius_pub_key: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCifYw9iPtGH/enR6rigILYu3djnyT+6g+bOnzSSNLVffDsU6QAv7i3Girc+JgXGjxCtWAzZTHx3qjniKixhIpyzt65J0RDeyZCS9WnMfOqkgIlj+85yxlQDLZdkS0xaQYzeqDEPsT6N0i+gxGWu52NxKYazVqyRXrqwiXZvOMyY4hcgtbNiWRy9Vqdkeippo/+3Jg2gawOz54fIF5zbWMqiJAALBCBl5w1LGTJIaJ+f6vQPq05kLKZOOs3zlfcUY5u1bEzulm/ZNyfhh3rrnJD20jCp8KETgg9sGHQdWdRkbsqT5AGeJQhwK47BEk7ckwZH3lmKwgRw8S/J604tpUh Generated-by-Nova + oam_plane_net_name: admin + freeRadius_name_0: FreeRadiusInnova-VM + vnf_id: FreeRadiusInnova-Id + capabilities: + abstract_freeradius.network.outgoing.bytes.rate_freeRadius_freeRadius_control_plane_port_0: + properties: + name: network.outgoing.bytes.rate + abstract_freeradius.cpu_util_freeRadius: + properties: + name: cpu_util + abstract_freeradius.memory.usage_freeRadius: + properties: + name: memory.usage + abstract_freeradius.network.incoming.bytes_freeRadius_freeRadius_control_plane_port_0: + properties: + name: network.incoming.bytes + abstract_freeradius.memory_freeRadius: + properties: + name: memory + abstract_freeradius.network.outgoing.packets.rate_freeRadius_freeRadius_oam_port_0: + properties: + name: network.outgoing.packets.rate + abstract_freeradius.network.incoming.packets.rate_freeRadius_freeRadius_control_plane_port_0: + properties: + name: network.incoming.packets.rate + abstract_freeradius.disk.device.read.bytes.rate_freeRadius: + properties: + name: disk.device.read.bytes.rate + abstract_freeradius.disk.read.requests_freeRadius: + properties: + name: disk.read.requests + abstract_freeradius.disk.device.usage_freeRadius: + properties: + name: disk.device.usage + abstract_freeradius.network.incoming.packets_freeRadius_freeRadius_user_plane_port_0: + properties: + name: network.incoming.packets + abstract_freeradius.scalable_freeRadius: + properties: + min_instances: 1 + max_instances: 1 + abstract_freeradius.disk.latency_freeRadius: + properties: + name: disk.latency + abstract_freeradius.disk.write.requests.rate_freeRadius: + properties: + name: disk.write.requests.rate + abstract_freeradius.disk.write.requests_freeRadius: + properties: + name: disk.write.requests + abstract_freeradius.network.incoming.bytes_freeRadius_freeRadius_user_plane_port_0: + properties: + name: network.incoming.bytes + abstract_freeradius.disk.device.write.bytes.rate_freeRadius: + properties: + name: disk.device.write.bytes.rate + abstract_freeradius.disk.device.iops_freeRadius: + properties: + name: disk.device.iops + abstract_freeradius.network.outpoing.packets_freeRadius_freeRadius_user_plane_port_0: + properties: + name: network.outpoing.packets + abstract_freeradius.network.incoming.packets_freeRadius_freeRadius_control_plane_port_0: + properties: + name: network.incoming.packets + abstract_freeradius.disk.iops_freeRadius: + properties: + name: disk.iops + abstract_freeradius.disk.device.write.requests_freeRadius: + properties: + name: disk.device.write.requests + abstract_freeradius.disk.device.read.requests.rate_freeRadius: + properties: + name: disk.device.read.requests.rate + abstract_freeradius.network.incoming.bytes.rate_freeRadius_freeRadius_user_plane_port_0: + properties: + name: network.incoming.bytes.rate + abstract_freeradius.disk.device.write.requests.rate_freeRadius: + properties: + name: disk.device.write.requests.rate + abstract_freeradius.network.outgoing.bytes.rate_freeRadius_freeRadius_user_plane_port_0: + properties: + name: network.outgoing.bytes.rate + abstract_freeradius.network.outpoing.packets_freeRadius_freeRadius_oam_port_0: + properties: + name: network.outpoing.packets + abstract_freeradius.disk.device.write.bytes_freeRadius: + properties: + name: disk.device.write.bytes + abstract_freeradius.instance_freeRadius: + properties: + name: instance + abstract_freeradius.disk.device.capacity_freeRadius: + properties: + name: disk.device.capacity + abstract_freeradius.port_mirroring_freeRadius_freeRadius_oam_port_0: + properties: + connection_point: + network_role: + get_input: port_freeRadius_oam_port_0_network_role + nfc_naming_code: freeRadius + abstract_freeradius.disk.usage_freeRadius: + properties: + name: disk.usage + abstract_freeradius.network.outgoing.bytes_freeRadius_freeRadius_oam_port_0: + properties: + name: network.outgoing.bytes + abstract_freeradius.disk.device.latency_freeRadius: + properties: + name: disk.device.latency + abstract_freeradius.disk.root.size_freeRadius: + properties: + name: disk.root.size + abstract_freeradius.disk.capacity_freeRadius: + properties: + name: disk.capacity + abstract_freeradius.network.incoming.bytes.rate_freeRadius_freeRadius_oam_port_0: + properties: + name: network.incoming.bytes.rate + abstract_freeradius.network.incoming.bytes.rate_freeRadius_freeRadius_control_plane_port_0: + properties: + name: network.incoming.bytes.rate + abstract_freeradius.memory.resident_freeRadius: + properties: + name: memory.resident + abstract_freeradius.network.incoming.packets.rate_freeRadius_freeRadius_oam_port_0: + properties: + name: network.incoming.packets.rate + abstract_freeradius.disk.write.bytes.rate_freeRadius: + properties: + name: disk.write.bytes.rate + abstract_freeradius.network.outgoing.bytes_freeRadius_freeRadius_control_plane_port_0: + properties: + name: network.outgoing.bytes + abstract_freeradius.disk.device.read.bytes_freeRadius: + properties: + name: disk.device.read.bytes + abstract_freeradius.network.outgoing.bytes.rate_freeRadius_freeRadius_oam_port_0: + properties: + name: network.outgoing.bytes.rate + abstract_freeradius.disk.allocation_freeRadius: + properties: + name: disk.allocation + abstract_freeradius.network.outgoing.packets.rate_freeRadius_freeRadius_user_plane_port_0: + properties: + name: network.outgoing.packets.rate + abstract_freeradius.cpu.delta_freeRadius: + properties: + name: cpu.delta + abstract_freeradius.network.outgoing.packets.rate_freeRadius_freeRadius_control_plane_port_0: + properties: + name: network.outgoing.packets.rate + abstract_freeradius.disk.read.bytes.rate_freeRadius: + properties: + name: disk.read.bytes.rate + abstract_freeradius.vcpus_freeRadius: + properties: + name: vcpus + abstract_freeradius.network.incoming.packets_freeRadius_freeRadius_oam_port_0: + properties: + name: network.incoming.packets + abstract_freeradius.disk.device.allocation_freeRadius: + properties: + name: disk.device.allocation + abstract_freeradius.disk.write.bytes_freeRadius: + properties: + name: disk.write.bytes + abstract_freeradius.endpoint_freeRadius: + properties: + secure: true + abstract_freeradius.network.incoming.packets.rate_freeRadius_freeRadius_user_plane_port_0: + properties: + name: network.incoming.packets.rate + abstract_freeradius.disk.read.bytes_freeRadius: + properties: + name: disk.read.bytes + abstract_freeradius.cpu_freeRadius: + properties: + name: cpu + abstract_freeradius.port_mirroring_freeRadius_freeRadius_control_plane_port_0: + properties: + connection_point: + network_role: + get_input: port_freeRadius_control_plane_port_0_network_role + nfc_naming_code: freeRadius + abstract_freeradius.network.outpoing.packets_freeRadius_freeRadius_control_plane_port_0: + properties: + name: network.outpoing.packets + abstract_freeradius.disk.device.read.requests_freeRadius: + properties: + name: disk.device.read.requests + abstract_freeradius.port_mirroring_freeRadius_freeRadius_user_plane_port_0: + properties: + connection_point: + network_role: + get_input: port_freeRadius_user_plane_port_0_network_role + nfc_naming_code: freeRadius + abstract_freeradius.disk.ephemeral.size_freeRadius: + properties: + name: disk.ephemeral.size + abstract_freeradius.network.incoming.bytes_freeRadius_freeRadius_oam_port_0: + properties: + name: network.incoming.bytes + abstract_freeradius.network.outgoing.bytes_freeRadius_freeRadius_user_plane_port_0: + properties: + name: network.outgoing.bytes + groups: + freeradiusinnova_vf0..FreeradiusinnovaVf..base_freeRadiusInnova..module-0: + type: org.openecomp.groups.VfModule + metadata: + vfModuleModelName: FreeradiusinnovaVf..base_freeRadiusInnova..module-0 + vfModuleModelInvariantUUID: bd638287-e1a0-47d0-a0b1-825a4ad23fef + vfModuleModelUUID: 9bc52324-3e35-4795-9181-804a4eee0806 + vfModuleModelVersion: '1' + vfModuleModelCustomizationUUID: 0a633c1a-c122-4e0a-8ed6-233ac56c132f + properties: + min_vf_module_instances: 1 + vf_module_label: base_freeRadiusInnova + max_vf_module_instances: 1 + vf_module_type: Base + isBase: true + initial_count: 1 + volume_group: false + substitution_mappings: + node_type: org.openecomp.service.FreeradiusinnovaService123 + capabilities: + freeradiusinnova_vf0.abstract_freeradius.disk.latency_freeRadius: + - freeRadiusInnova_VF 0 + - abstract_freeradius.disk.latency_freeRadius + freeradiusinnova_vf0.abstract_freeradius.disk.usage_freeRadius: + - freeRadiusInnova_VF 0 + - abstract_freeradius.disk.usage_freeRadius + freeradiusinnova_vf0.abstract_freeradius.disk.read.bytes_freeRadius: + - freeRadiusInnova_VF 0 + - abstract_freeradius.disk.read.bytes_freeRadius + freeradiusinnova_vf0.abstract_freeradius.os_freeRadius: + - freeRadiusInnova_VF 0 + - abstract_freeradius.os_freeRadius + freeradiusinnova_vf0.abstract_freeradius.attachment_freeRadius_freeRadius_control_plane_port_0: + - freeRadiusInnova_VF 0 + - abstract_freeradius.attachment_freeRadius_freeRadius_control_plane_port_0 + freeradiusinnova_vf0.abstract_freeradius.feature_freeRadius: + - freeRadiusInnova_VF 0 + - abstract_freeradius.feature_freeRadius + freeradiusinnova_vf0.abstract_freeradius.disk.device.read.bytes_freeRadius: + - freeRadiusInnova_VF 0 + - abstract_freeradius.disk.device.read.bytes_freeRadius + freeradiusinnova_vf0.abstract_freeradius.disk.device.latency_freeRadius: + - freeRadiusInnova_VF 0 + - abstract_freeradius.disk.device.latency_freeRadius + freeradiusinnova_vf0.abstract_freeradius.binding_freeRadius: + - freeRadiusInnova_VF 0 + - abstract_freeradius.binding_freeRadius + freeradiusinnova_vf0.abstract_freeradius.binding_freeRadius_freeRadius_user_plane_port_0: + - freeRadiusInnova_VF 0 + - abstract_freeradius.binding_freeRadius_freeRadius_user_plane_port_0 + freeradiusinnova_vf0.abstract_freeradius.forwarder_freeRadius_freeRadius_user_plane_port_0: + - freeRadiusInnova_VF 0 + - abstract_freeradius.forwarder_freeRadius_freeRadius_user_plane_port_0 + freeradiusinnova_vf0.abstract_freeradius.disk.device.usage_freeRadius: + - freeRadiusInnova_VF 0 + - abstract_freeradius.disk.device.usage_freeRadius + freeradiusinnova_vf0.abstract_freeradius.endpoint_freeRadius: + - freeRadiusInnova_VF 0 + - abstract_freeradius.endpoint_freeRadius + freeradiusinnova_vf0.abstract_freeradius.port_mirroring_freeRadius_freeRadius_oam_port_0: + - freeRadiusInnova_VF 0 + - abstract_freeradius.port_mirroring_freeRadius_freeRadius_oam_port_0 + freeradiusinnova_vf0.abstract_freeradius.cpu.delta_freeRadius: + - freeRadiusInnova_VF 0 + - abstract_freeradius.cpu.delta_freeRadius + freeradiusinnova_vf0.abstract_freeradius.feature_freeRadius_freeRadius_user_plane_port_0: + - freeRadiusInnova_VF 0 + - abstract_freeradius.feature_freeRadius_freeRadius_user_plane_port_0 + freeradiusinnova_vf0.abstract_freeradius.disk.write.bytes_freeRadius: + - freeRadiusInnova_VF 0 + - abstract_freeradius.disk.write.bytes_freeRadius + freeradiusinnova_vf0.abstract_freeradius.forwarder_freeRadius_freeRadius_oam_port_0: + - freeRadiusInnova_VF 0 + - abstract_freeradius.forwarder_freeRadius_freeRadius_oam_port_0 + freeradiusinnova_vf0.abstract_freeradius.network.incoming.packets_freeRadius_freeRadius_control_plane_port_0: + - freeRadiusInnova_VF 0 + - abstract_freeradius.network.incoming.packets_freeRadius_freeRadius_control_plane_port_0 + freeradiusinnova_vf0.abstract_freeradius.disk.device.write.bytes_freeRadius: + - freeRadiusInnova_VF 0 + - abstract_freeradius.disk.device.write.bytes_freeRadius + freeradiusinnova_vf0.abstract_freeradius.disk.device.write.requests.rate_freeRadius: + - freeRadiusInnova_VF 0 + - abstract_freeradius.disk.device.write.requests.rate_freeRadius + freeradiusinnova_vf0.abstract_freeradius.host_freeRadius: + - freeRadiusInnova_VF 0 + - abstract_freeradius.host_freeRadius + freeradiusinnova_vf0.abstract_freeradius.attachment_freeRadius_freeRadius_user_plane_port_0: + - freeRadiusInnova_VF 0 + - abstract_freeradius.attachment_freeRadius_freeRadius_user_plane_port_0 + freeradiusinnova_vf0.abstract_freeradius.network.incoming.bytes.rate_freeRadius_freeRadius_user_plane_port_0: + - freeRadiusInnova_VF 0 + - abstract_freeradius.network.incoming.bytes.rate_freeRadius_freeRadius_user_plane_port_0 + freeradiusinnova_vf0.abstract_freeradius.network.outgoing.bytes_freeRadius_freeRadius_user_plane_port_0: + - freeRadiusInnova_VF 0 + - abstract_freeradius.network.outgoing.bytes_freeRadius_freeRadius_user_plane_port_0 + freeradiusinnova_vf0.abstract_freeradius.network.incoming.bytes_freeRadius_freeRadius_control_plane_port_0: + - freeRadiusInnova_VF 0 + - abstract_freeradius.network.incoming.bytes_freeRadius_freeRadius_control_plane_port_0 + freeradiusinnova_vf0.abstract_freeradius.disk.device.write.bytes.rate_freeRadius: + - freeRadiusInnova_VF 0 + - abstract_freeradius.disk.device.write.bytes.rate_freeRadius + neutronnet0.end_point: + - NeutronNet 0 + - end_point + freeradiusinnova_vf0.abstract_freeradius.network.outgoing.bytes_freeRadius_freeRadius_oam_port_0: + - freeRadiusInnova_VF 0 + - abstract_freeradius.network.outgoing.bytes_freeRadius_freeRadius_oam_port_0 + freeradiusinnova_vf0.abstract_freeradius.cpu_util_freeRadius: + - freeRadiusInnova_VF 0 + - abstract_freeradius.cpu_util_freeRadius + freeradiusinnova_vf0.abstract_freeradius.network.incoming.packets_freeRadius_freeRadius_oam_port_0: + - freeRadiusInnova_VF 0 + - abstract_freeradius.network.incoming.packets_freeRadius_freeRadius_oam_port_0 + neutronnet0.attachment: + - NeutronNet 0 + - attachment + freeradiusinnova_vf0.abstract_freeradius.disk.device.iops_freeRadius: + - freeRadiusInnova_VF 0 + - abstract_freeradius.disk.device.iops_freeRadius + freeradiusinnova_vf0.freeradius_control_plane_security_group.feature: + - freeRadiusInnova_VF 0 + - freeradius_control_plane_security_group.feature + freeradiusinnova_vf0.freeradius_user_plane_security_group.feature: + - freeRadiusInnova_VF 0 + - freeradius_user_plane_security_group.feature + freeradiusinnova_vf0.abstract_freeradius.disk.capacity_freeRadius: + - freeRadiusInnova_VF 0 + - abstract_freeradius.disk.capacity_freeRadius + freeradiusinnova_vf0.abstract_freeradius.network.incoming.bytes.rate_freeRadius_freeRadius_control_plane_port_0: + - freeRadiusInnova_VF 0 + - abstract_freeradius.network.incoming.bytes.rate_freeRadius_freeRadius_control_plane_port_0 + freeradiusinnova_vf0.abstract_freeradius.network.outgoing.bytes.rate_freeRadius_freeRadius_control_plane_port_0: + - freeRadiusInnova_VF 0 + - abstract_freeradius.network.outgoing.bytes.rate_freeRadius_freeRadius_control_plane_port_0 + freeradiusinnova_vf0.abstract_freeradius.network.outpoing.packets_freeRadius_freeRadius_oam_port_0: + - freeRadiusInnova_VF 0 + - abstract_freeradius.network.outpoing.packets_freeRadius_freeRadius_oam_port_0 + neutronnet0.link: + - NeutronNet 0 + - link + freeradiusinnova_vf0.abstract_freeradius.disk.write.bytes.rate_freeRadius: + - freeRadiusInnova_VF 0 + - abstract_freeradius.disk.write.bytes.rate_freeRadius + freeradiusinnova_vf0.abstract_freeradius.disk.device.read.requests_freeRadius: + - freeRadiusInnova_VF 0 + - abstract_freeradius.disk.device.read.requests_freeRadius + freeradiusinnova_vf0.abstract_freeradius.network.outgoing.bytes_freeRadius_freeRadius_control_plane_port_0: + - freeRadiusInnova_VF 0 + - abstract_freeradius.network.outgoing.bytes_freeRadius_freeRadius_control_plane_port_0 + freeradiusinnova_vf0.abstract_freeradius.vcpus_freeRadius: + - freeRadiusInnova_VF 0 + - abstract_freeradius.vcpus_freeRadius + freeradiusinnova_vf0.abstract_freeradius.network.incoming.bytes.rate_freeRadius_freeRadius_oam_port_0: + - freeRadiusInnova_VF 0 + - abstract_freeradius.network.incoming.bytes.rate_freeRadius_freeRadius_oam_port_0 + freeradiusinnova_vf0.abstract_freeradius.disk.device.allocation_freeRadius: + - freeRadiusInnova_VF 0 + - abstract_freeradius.disk.device.allocation_freeRadius + freeradiusinnova_vf0.abstract_freeradius.disk.device.read.requests.rate_freeRadius: + - freeRadiusInnova_VF 0 + - abstract_freeradius.disk.device.read.requests.rate_freeRadius + freeradiusinnova_vf0.abstract_freeradius.attachment_freeRadius_freeRadius_oam_port_0: + - freeRadiusInnova_VF 0 + - abstract_freeradius.attachment_freeRadius_freeRadius_oam_port_0 + freeradiusinnova_vf0.abstract_freeradius.memory_freeRadius: + - freeRadiusInnova_VF 0 + - abstract_freeradius.memory_freeRadius + freeradiusinnova_vf0.abstract_freeradius.disk.device.write.requests_freeRadius: + - freeRadiusInnova_VF 0 + - abstract_freeradius.disk.device.write.requests_freeRadius + freeradiusinnova_vf0.abstract_freeradius.disk.read.bytes.rate_freeRadius: + - freeRadiusInnova_VF 0 + - abstract_freeradius.disk.read.bytes.rate_freeRadius + freeradiusinnova_vf0.abstract_freeradius.disk.root.size_freeRadius: + - freeRadiusInnova_VF 0 + - abstract_freeradius.disk.root.size_freeRadius + freeradiusinnova_vf0.abstract_freeradius.disk.allocation_freeRadius: + - freeRadiusInnova_VF 0 + - abstract_freeradius.disk.allocation_freeRadius + freeradiusinnova_vf0.abstract_freeradius.network.outgoing.packets.rate_freeRadius_freeRadius_control_plane_port_0: + - freeRadiusInnova_VF 0 + - abstract_freeradius.network.outgoing.packets.rate_freeRadius_freeRadius_control_plane_port_0 + freeradiusinnova_vf0.abstract_freeradius.network.incoming.packets_freeRadius_freeRadius_user_plane_port_0: + - freeRadiusInnova_VF 0 + - abstract_freeradius.network.incoming.packets_freeRadius_freeRadius_user_plane_port_0 + freeradiusinnova_vf0.abstract_freeradius.disk.device.capacity_freeRadius: + - freeRadiusInnova_VF 0 + - abstract_freeradius.disk.device.capacity_freeRadius + freeradiusinnova_vf0.abstract_freeradius.network.outgoing.bytes.rate_freeRadius_freeRadius_user_plane_port_0: + - freeRadiusInnova_VF 0 + - abstract_freeradius.network.outgoing.bytes.rate_freeRadius_freeRadius_user_plane_port_0 + neutronnet0.feature: + - NeutronNet 0 + - feature + freeradiusinnova_vf0.abstract_freeradius.network.outpoing.packets_freeRadius_freeRadius_control_plane_port_0: + - freeRadiusInnova_VF 0 + - abstract_freeradius.network.outpoing.packets_freeRadius_freeRadius_control_plane_port_0 + freeradiusinnova_vf0.abstract_freeradius.port_mirroring_freeRadius_freeRadius_control_plane_port_0: + - freeRadiusInnova_VF 0 + - abstract_freeradius.port_mirroring_freeRadius_freeRadius_control_plane_port_0 + freeradiusinnova_vf0.abstract_freeradius.instance_freeRadius: + - freeRadiusInnova_VF 0 + - abstract_freeradius.instance_freeRadius + freeradiusinnova_vf0.abstract_freeradius.disk.write.requests.rate_freeRadius: + - freeRadiusInnova_VF 0 + - abstract_freeradius.disk.write.requests.rate_freeRadius + freeradiusinnova_vf0.abstract_freeradius.memory.resident_freeRadius: + - freeRadiusInnova_VF 0 + - abstract_freeradius.memory.resident_freeRadius + freeradiusinnova_vf0.abstract_freeradius.disk.write.requests_freeRadius: + - freeRadiusInnova_VF 0 + - abstract_freeradius.disk.write.requests_freeRadius + freeradiusinnova_vf0.abstract_freeradius.network.incoming.packets.rate_freeRadius_freeRadius_user_plane_port_0: + - freeRadiusInnova_VF 0 + - abstract_freeradius.network.incoming.packets.rate_freeRadius_freeRadius_user_plane_port_0 + freeradiusinnova_vf0.abstract_freeradius.network.incoming.packets.rate_freeRadius_freeRadius_control_plane_port_0: + - freeRadiusInnova_VF 0 + - abstract_freeradius.network.incoming.packets.rate_freeRadius_freeRadius_control_plane_port_0 + freeradiusinnova_vf0.abstract_freeradius.forwarder_freeRadius_freeRadius_control_plane_port_0: + - freeRadiusInnova_VF 0 + - abstract_freeradius.forwarder_freeRadius_freeRadius_control_plane_port_0 + freeradiusinnova_vf0.abstract_freeradius.network.incoming.packets.rate_freeRadius_freeRadius_oam_port_0: + - freeRadiusInnova_VF 0 + - abstract_freeradius.network.incoming.packets.rate_freeRadius_freeRadius_oam_port_0 + freeradiusinnova_vf0.abstract_freeradius.network.incoming.bytes_freeRadius_freeRadius_oam_port_0: + - freeRadiusInnova_VF 0 + - abstract_freeradius.network.incoming.bytes_freeRadius_freeRadius_oam_port_0 + freeradiusinnova_vf0.abstract_freeradius.network.outgoing.packets.rate_freeRadius_freeRadius_user_plane_port_0: + - freeRadiusInnova_VF 0 + - abstract_freeradius.network.outgoing.packets.rate_freeRadius_freeRadius_user_plane_port_0 + freeradiusinnova_vf0.abstract_freeradius.binding_freeRadius_freeRadius_oam_port_0: + - freeRadiusInnova_VF 0 + - abstract_freeradius.binding_freeRadius_freeRadius_oam_port_0 + freeradiusinnova_vf0.abstract_freeradius.binding_freeRadius_freeRadius_control_plane_port_0: + - freeRadiusInnova_VF 0 + - abstract_freeradius.binding_freeRadius_freeRadius_control_plane_port_0 + freeradiusinnova_vf0.abstract_freeradius.network.outgoing.packets.rate_freeRadius_freeRadius_oam_port_0: + - freeRadiusInnova_VF 0 + - abstract_freeradius.network.outgoing.packets.rate_freeRadius_freeRadius_oam_port_0 + freeradiusinnova_vf0.abstract_freeradius.network.outpoing.packets_freeRadius_freeRadius_user_plane_port_0: + - freeRadiusInnova_VF 0 + - abstract_freeradius.network.outpoing.packets_freeRadius_freeRadius_user_plane_port_0 + freeradiusinnova_vf0.abstract_freeradius.network.outgoing.bytes.rate_freeRadius_freeRadius_oam_port_0: + - freeRadiusInnova_VF 0 + - abstract_freeradius.network.outgoing.bytes.rate_freeRadius_freeRadius_oam_port_0 + freeradiusinnova_vf0.abstract_freeradius.scalable_freeRadius: + - freeRadiusInnova_VF 0 + - abstract_freeradius.scalable_freeRadius + freeradiusinnova_vf0.abstract_freeradius.feature_freeRadius_freeRadius_oam_port_0: + - freeRadiusInnova_VF 0 + - abstract_freeradius.feature_freeRadius_freeRadius_oam_port_0 + freeradiusinnova_vf0.freeradius_oam_security_group.feature: + - freeRadiusInnova_VF 0 + - freeradius_oam_security_group.feature + freeradiusinnova_vf0.abstract_freeradius.feature_freeRadius_freeRadius_control_plane_port_0: + - freeRadiusInnova_VF 0 + - abstract_freeradius.feature_freeRadius_freeRadius_control_plane_port_0 + freeradiusinnova_vf0.abstract_freeradius.port_mirroring_freeRadius_freeRadius_user_plane_port_0: + - freeRadiusInnova_VF 0 + - abstract_freeradius.port_mirroring_freeRadius_freeRadius_user_plane_port_0 + freeradiusinnova_vf0.abstract_freeradius.disk.device.read.bytes.rate_freeRadius: + - freeRadiusInnova_VF 0 + - abstract_freeradius.disk.device.read.bytes.rate_freeRadius + freeradiusinnova_vf0.abstract_freeradius.disk.read.requests_freeRadius: + - freeRadiusInnova_VF 0 + - abstract_freeradius.disk.read.requests_freeRadius + freeradiusinnova_vf0.abstract_freeradius.disk.iops_freeRadius: + - freeRadiusInnova_VF 0 + - abstract_freeradius.disk.iops_freeRadius + freeradiusinnova_vf0.abstract_freeradius.memory.usage_freeRadius: + - freeRadiusInnova_VF 0 + - abstract_freeradius.memory.usage_freeRadius + freeradiusinnova_vf0.abstract_freeradius.disk.ephemeral.size_freeRadius: + - freeRadiusInnova_VF 0 + - abstract_freeradius.disk.ephemeral.size_freeRadius + freeradiusinnova_vf0.abstract_freeradius.network.incoming.bytes_freeRadius_freeRadius_user_plane_port_0: + - freeRadiusInnova_VF 0 + - abstract_freeradius.network.incoming.bytes_freeRadius_freeRadius_user_plane_port_0 + freeradiusinnova_vf0.abstract_freeradius.cpu_freeRadius: + - freeRadiusInnova_VF 0 + - abstract_freeradius.cpu_freeRadius + requirements: + freeradiusinnova_vf0.abstract_freeradius.dependency_freeRadius_freeRadius_user_plane_port_0: + - freeRadiusInnova_VF 0 + - abstract_freeradius.dependency_freeRadius_freeRadius_user_plane_port_0 + freeradiusinnova_vf0.abstract_freeradius.dependency_freeRadius_freeRadius_oam_port_0: + - freeRadiusInnova_VF 0 + - abstract_freeradius.dependency_freeRadius_freeRadius_oam_port_0 + freeradiusinnova_vf0.abstract_freeradius.link_freeRadius_freeRadius_oam_port_0: + - freeRadiusInnova_VF 0 + - abstract_freeradius.link_freeRadius_freeRadius_oam_port_0 + freeradiusinnova_vf0.freeradius_control_plane_security_group.port: + - freeRadiusInnova_VF 0 + - freeradius_control_plane_security_group.port + freeradiusinnova_vf0.freeradius_oam_security_group.dependency: + - freeRadiusInnova_VF 0 + - freeradius_oam_security_group.dependency + freeradiusinnova_vf0.freeradius_control_plane_security_group.dependency: + - freeRadiusInnova_VF 0 + - freeradius_control_plane_security_group.dependency + neutronnet0.dependency: + - NeutronNet 0 + - dependency + freeradiusinnova_vf0.freeradius_user_plane_security_group.port: + - freeRadiusInnova_VF 0 + - freeradius_user_plane_security_group.port + freeradiusinnova_vf0.freeradius_user_plane_security_group.dependency: + - freeRadiusInnova_VF 0 + - freeradius_user_plane_security_group.dependency + freeradiusinnova_vf0.abstract_freeradius.dependency_freeRadius: + - freeRadiusInnova_VF 0 + - abstract_freeradius.dependency_freeRadius + freeradiusinnova_vf0.abstract_freeradius.dependency_freeRadius_freeRadius_control_plane_port_0: + - freeRadiusInnova_VF 0 + - abstract_freeradius.dependency_freeRadius_freeRadius_control_plane_port_0 + freeradiusinnova_vf0.abstract_freeradius.link_freeRadius_freeRadius_control_plane_port_0: + - freeRadiusInnova_VF 0 + - abstract_freeradius.link_freeRadius_freeRadius_control_plane_port_0 + freeradiusinnova_vf0.abstract_freeradius.local_storage_freeRadius: + - freeRadiusInnova_VF 0 + - abstract_freeradius.local_storage_freeRadius + freeradiusinnova_vf0.freeradius_oam_security_group.port: + - freeRadiusInnova_VF 0 + - freeradius_oam_security_group.port + freeradiusinnova_vf0.abstract_freeradius.link_freeRadius_freeRadius_user_plane_port_0: + - freeRadiusInnova_VF 0 + - abstract_freeradius.link_freeRadius_freeRadius_user_plane_port_0
\ No newline at end of file diff --git a/tests/data/service-Ubuntu16-template.yml b/tests/data/service-Ubuntu16-template.yml new file mode 100644 index 0000000..63cee88 --- /dev/null +++ b/tests/data/service-Ubuntu16-template.yml @@ -0,0 +1,543 @@ +tosca_definitions_version: tosca_simple_yaml_1_1 +metadata: + invariantUUID: d65b4e82-06e7-424a-8944-284558b94ff4 + UUID: b54a8c68-d520-42c3-8f8e-08f3866f4fc1 + name: ubuntu16 + description: ubuntu16 + type: Service + category: Network Service + serviceType: '' + serviceRole: '' + instantiationType: A-la-carte + serviceEcompNaming: true + ecompGeneratedNaming: true + namingPolicy: '' + environmentContext: General_Revenue-Bearing +imports: +- nodes: + file: nodes.yml +- datatypes: + file: data.yml +- capabilities: + file: capabilities.yml +- relationships: + file: relationships.yml +- groups: + file: groups.yml +- policies: + file: policies.yml +- annotations: + file: annotations.yml +- service-ubuntu16-interface: + file: service-Ubuntu16-template-interface.yml +- resource-ubuntu16_VF: + file: resource-Ubuntu16Vf-template.yml +- resource-ubuntu16_VF-interface: + file: resource-Ubuntu16Vf-template-interface.yml +topology_template: + node_templates: + ubuntu16_VF 0: + type: org.openecomp.resource.vf.Ubuntu16Vf + metadata: + invariantUUID: 712e6e88-404e-49cb-99db-19460b29c2ac + UUID: dc4f02bb-f318-49c4-bd70-2a1ee95b439a + customizationUUID: 1066c03b-0aab-43b3-a661-7543de231e7c + version: '1.0' + name: ubuntu16_VF + description: VF + type: VF + category: Generic + subcategory: Abstract + resourceVendor: Generic-Vendor + resourceVendorRelease: '1.0' + resourceVendorModelNumber: '' + properties: + vf_module_id: Ubuntu16-VF-module + ubuntu16_name_0: ubuntu16 + nf_naming: + ecomp_generated_naming: true + ubuntu16_flavor_name: onap.small + multi_stage_design: false + ubuntu16_pub_key: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDY15cdBmIs2XOpe4EiFCsaY6bmUmK/GysMoLl4UG51JCfJwvwoWCoA+6mDIbymZxhxq9IGxilp/yTA6WQ9s/5pBag1cUMJmFuda9PjOkXl04jgqh5tR6I+GZ97AvCg93KAECis5ubSqw1xOCj4utfEUtPoF1OuzqM/lE5mY4N6VKXn+fT7pCD6cifBEs6JHhVNvs5OLLp/tO8Pa3kKYQOdyS0xc3rh+t2lrzvKUSWGZbX+dLiFiEpjsUL3tDqzkEMNUn4pdv69OJuzWHCxRWPfdrY9Wg0j3mJesP29EBht+w+EC9/kBKq+1VKdmsXUXAcjEvjovVL8l1BrX3BY0R8D imported-openssh-key + availability_zone_max_count: 1 + vnf_id: Ubuntu16-VNF + vnf_name: Ubuntu16-VNF-name + ubuntu16_image_name: ubuntu-16.04-daily + admin_plane_net_name: admin + ubuntu16_key_name: cleouverte + capabilities: + abstract_ubuntu16.port_mirroring_ubuntu16_ubuntu16_admin_plane_port: + properties: + connection_point: + network_role: + get_input: port_ubuntu16_admin_plane_port_network_role + nfc_naming_code: ubuntu16 + abstract_ubuntu16.cpu_util_ubuntu16: + properties: + unit: '%' + description: Average CPU utilization + type: Gauge + category: compute + abstract_ubuntu16.network.outpoing.packets_ubuntu16_ubuntu16_admin_plane_port: + properties: + unit: packet + description: Number of outgoing packets + type: Cumulative + category: network + abstract_ubuntu16.disk.device.read.bytes.rate_ubuntu16: + properties: + unit: B/s + description: Average rate of reads + type: Gauge + category: disk + abstract_ubuntu16.endpoint_ubuntu16: + properties: + secure: true + abstract_ubuntu16.disk.ephemeral.size_ubuntu16: + properties: + unit: GB + description: Size of ephemeral disk + type: Gauge + category: compute + abstract_ubuntu16.disk.write.bytes_ubuntu16: + properties: + unit: B + description: Volume of writes + type: Cumulative + category: compute + abstract_ubuntu16.cpu.delta_ubuntu16: + properties: + unit: ns + description: CPU time used since previous datapoint + type: Delta + category: compute + abstract_ubuntu16.network.incoming.bytes.rate_ubuntu16_ubuntu16_admin_plane_port: + properties: + unit: B/s + description: Average rate of incoming bytes + type: Gauge + category: network + abstract_ubuntu16.network.incoming.bytes_ubuntu16_ubuntu16_admin_plane_port: + properties: + unit: B + description: Number of incoming bytes + type: Cumulative + category: network + abstract_ubuntu16.disk.write.requests.rate_ubuntu16: + properties: + unit: request/s + description: Average rate of write requests + type: Gauge + category: compute + abstract_ubuntu16.memory_ubuntu16: + properties: + unit: MB + description: Volume of RAM allocated to the instance + type: Gauge + category: compute + abstract_ubuntu16.disk.root.size_ubuntu16: + properties: + unit: GB + description: Size of root disk + type: Gauge + category: compute + abstract_ubuntu16.disk.device.usage_ubuntu16: + properties: + unit: B + description: The physical size in bytes of the image container on the host per device + type: Gauge + category: disk + abstract_ubuntu16.disk.write.requests_ubuntu16: + properties: + unit: request + description: Number of write requests + type: Cumulative + category: compute + abstract_ubuntu16.disk.device.read.bytes_ubuntu16: + properties: + unit: B + description: Volume of reads + type: Cumulative + category: disk + abstract_ubuntu16.vcpus_ubuntu16: + properties: + unit: vcpu + description: Number of virtual CPUs allocated to the instance + type: Gauge + category: compute + abstract_ubuntu16.network.incoming.packets.rate_ubuntu16_ubuntu16_admin_plane_port: + properties: + unit: packet/s + description: Average rate of incoming packets + type: Gauge + category: network + abstract_ubuntu16.network.incoming.packets_ubuntu16_ubuntu16_admin_plane_port: + properties: + unit: packet + description: Number of incoming packets + type: Cumulative + category: network + abstract_ubuntu16.disk.read.bytes_ubuntu16: + properties: + unit: B + description: Volume of reads + type: Cumulative + category: compute + abstract_ubuntu16.disk.latency_ubuntu16: + properties: + unit: ms + description: Average disk latency + type: Gauge + category: disk + abstract_ubuntu16.disk.device.read.requests.rate_ubuntu16: + properties: + unit: request/s + description: Average rate of read requests + type: Gauge + category: disk + abstract_ubuntu16.network.outgoing.bytes_ubuntu16_ubuntu16_admin_plane_port: + properties: + unit: B + description: Number of outgoing bytes + type: Cumulative + category: network + abstract_ubuntu16.scalable_ubuntu16: + properties: + max_instances: 1 + min_instances: 1 + abstract_ubuntu16.disk.device.write.requests.rate_ubuntu16: + properties: + unit: request/s + description: Average rate of write requests + type: Gauge + category: disk + abstract_ubuntu16.disk.device.allocation_ubuntu16: + properties: + unit: B + description: The amount of disk per device occupied by the instance on the host machine + type: Gauge + category: disk + abstract_ubuntu16.disk.device.write.bytes_ubuntu16: + properties: + unit: B + description: Volume of writes + type: Cumulative + category: disk + abstract_ubuntu16.disk.device.capacity_ubuntu16: + properties: + unit: B + description: The amount of disk per device that the instance can see + type: Gauge + category: disk + abstract_ubuntu16.disk.device.latency_ubuntu16: + properties: + unit: ms + description: Average disk latency per device + type: Gauge + category: disk + abstract_ubuntu16.disk.write.bytes.rate_ubuntu16: + properties: + unit: B/s + description: Average rate of writes + type: Gauge + category: compute + abstract_ubuntu16.instance_ubuntu16: + properties: + unit: instance + description: Existence of instance + type: Gauge + category: compute + abstract_ubuntu16.disk.iops_ubuntu16: + properties: + unit: count/s + description: Average disk iops + type: Gauge + category: disk + abstract_ubuntu16.disk.capacity_ubuntu16: + properties: + unit: B + description: The amount of disk that the instance can see + type: Gauge + category: disk + abstract_ubuntu16.memory.resident_ubuntu16: + properties: + unit: MB + description: Volume of RAM used by the instance on the physical machine + type: Gauge + category: compute + abstract_ubuntu16.disk.allocation_ubuntu16: + properties: + unit: B + description: The amount of disk occupied by the instance on the host machine + type: Gauge + category: disk + abstract_ubuntu16.network.outgoing.packets.rate_ubuntu16_ubuntu16_admin_plane_port: + properties: + unit: packet/s + description: Average rate of outgoing packets + type: Gauge + category: network + abstract_ubuntu16.disk.read.requests_ubuntu16: + properties: + unit: request + description: Number of read requests + type: Cumulative + category: compute + abstract_ubuntu16.network.outgoing.bytes.rate_ubuntu16_ubuntu16_admin_plane_port: + properties: + unit: B/s + description: Average rate of outgoing bytes + type: Gauge + category: network + abstract_ubuntu16.cpu_ubuntu16: + properties: + unit: ns + description: CPU time used + type: Cumulative + category: compute + abstract_ubuntu16.disk.device.iops_ubuntu16: + properties: + unit: count/s + description: Average disk iops per device + type: Gauge + category: disk + abstract_ubuntu16.disk.device.read.requests_ubuntu16: + properties: + unit: request + description: Number of read requests + type: Cumulative + category: disk + abstract_ubuntu16.memory.usage_ubuntu16: + properties: + unit: MB + description: Volume of RAM used by the instance from the amount of its allocated memory + type: Gauge + category: compute + abstract_ubuntu16.disk.usage_ubuntu16: + properties: + unit: B + description: The physical size in bytes of the image container on the host + type: Gauge + category: disk + abstract_ubuntu16.disk.device.write.bytes.rate_ubuntu16: + properties: + unit: B/s + description: Average rate of writes + type: Gauge + category: disk + abstract_ubuntu16.disk.read.bytes.rate_ubuntu16: + properties: + unit: B/s + description: Average rate of reads + type: Gauge + category: compute + abstract_ubuntu16.disk.device.write.requests_ubuntu16: + properties: + unit: request + description: Number of write requests + type: Cumulative + category: disk + groups: + ubuntu16_vf0..Ubuntu16Vf..base_ubuntu16..module-0: + type: org.openecomp.groups.VfModule + metadata: + vfModuleModelName: Ubuntu16Vf..base_ubuntu16..module-0 + vfModuleModelInvariantUUID: f47c3a9b-6a5f-4d1a-8a0b-b7f56ebb9a90 + vfModuleModelUUID: ed041b38-63fc-486d-9d4d-4e2531bc7e54 + vfModuleModelVersion: '1' + vfModuleModelCustomizationUUID: d946ea06-ec4b-4ed2-921a-117e1379b913 + properties: + min_vf_module_instances: 1 + vf_module_label: base_ubuntu16 + max_vf_module_instances: 1 + vf_module_type: Base + isBase: true + initial_count: 1 + volume_group: false + substitution_mappings: + node_type: org.openecomp.service.Ubuntu16 + capabilities: + ubuntu16_vf0.abstract_ubuntu16.disk.device.write.bytes_ubuntu16: + - ubuntu16_vf0 + - abstract_ubuntu16.disk.device.write.bytes_ubuntu16 + ubuntu16_vf0.abstract_ubuntu16.memory.resident_ubuntu16: + - ubuntu16_vf0 + - abstract_ubuntu16.memory.resident_ubuntu16 + ubuntu16_vf0.abstract_ubuntu16.forwarder_ubuntu16_ubuntu16_admin_plane_port: + - ubuntu16_vf0 + - abstract_ubuntu16.forwarder_ubuntu16_ubuntu16_admin_plane_port + ubuntu16_vf0.abstract_ubuntu16.binding_ubuntu16_ubuntu16_admin_plane_port: + - ubuntu16_vf0 + - abstract_ubuntu16.binding_ubuntu16_ubuntu16_admin_plane_port + ubuntu16_vf0.abstract_ubuntu16.vcpus_ubuntu16: + - ubuntu16_vf0 + - abstract_ubuntu16.vcpus_ubuntu16 + ubuntu16_vf0.abstract_ubuntu16.disk.latency_ubuntu16: + - ubuntu16_vf0 + - abstract_ubuntu16.disk.latency_ubuntu16 + ubuntu16_vf0.abstract_ubuntu16.disk.device.latency_ubuntu16: + - ubuntu16_vf0 + - abstract_ubuntu16.disk.device.latency_ubuntu16 + ubuntu16_vf0.abstract_ubuntu16.disk.read.bytes_ubuntu16: + - ubuntu16_vf0 + - abstract_ubuntu16.disk.read.bytes_ubuntu16 + ubuntu16_vf0.abstract_ubuntu16.network.outgoing.bytes.rate_ubuntu16_ubuntu16_admin_plane_port: + - ubuntu16_vf0 + - abstract_ubuntu16.network.outgoing.bytes.rate_ubuntu16_ubuntu16_admin_plane_port + ubuntu16_vf0.abstract_ubuntu16.disk.device.usage_ubuntu16: + - ubuntu16_vf0 + - abstract_ubuntu16.disk.device.usage_ubuntu16 + ubuntu16_vf0.abstract_ubuntu16.cpu.delta_ubuntu16: + - ubuntu16_vf0 + - abstract_ubuntu16.cpu.delta_ubuntu16 + ubuntu16_vf0.abstract_ubuntu16.endpoint_ubuntu16: + - ubuntu16_vf0 + - abstract_ubuntu16.endpoint_ubuntu16 + ubuntu16_vf0.abstract_ubuntu16.disk.device.read.bytes_ubuntu16: + - ubuntu16_vf0 + - abstract_ubuntu16.disk.device.read.bytes_ubuntu16 + ubuntu16_vf0.abstract_ubuntu16.disk.device.write.requests_ubuntu16: + - ubuntu16_vf0 + - abstract_ubuntu16.disk.device.write.requests_ubuntu16 + ubuntu16_vf0.abstract_ubuntu16.network.outgoing.bytes_ubuntu16_ubuntu16_admin_plane_port: + - ubuntu16_vf0 + - abstract_ubuntu16.network.outgoing.bytes_ubuntu16_ubuntu16_admin_plane_port + ubuntu16_vf0.abstract_ubuntu16.disk.capacity_ubuntu16: + - ubuntu16_vf0 + - abstract_ubuntu16.disk.capacity_ubuntu16 + ubuntu16_vf0.abstract_ubuntu16.attachment_ubuntu16_ubuntu16_admin_plane_port: + - ubuntu16_vf0 + - abstract_ubuntu16.attachment_ubuntu16_ubuntu16_admin_plane_port + ubuntu16_vf0.abstract_ubuntu16.network.incoming.bytes.rate_ubuntu16_ubuntu16_admin_plane_port: + - ubuntu16_vf0 + - abstract_ubuntu16.network.incoming.bytes.rate_ubuntu16_ubuntu16_admin_plane_port + ubuntu16_vf0.abstract_ubuntu16.disk.device.iops_ubuntu16: + - ubuntu16_vf0 + - abstract_ubuntu16.disk.device.iops_ubuntu16 + ubuntu16_vf0.abstract_ubuntu16.disk.device.capacity_ubuntu16: + - ubuntu16_vf0 + - abstract_ubuntu16.disk.device.capacity_ubuntu16 + ubuntu16_vf0.abstract_ubuntu16.disk.write.bytes.rate_ubuntu16: + - ubuntu16_vf0 + - abstract_ubuntu16.disk.write.bytes.rate_ubuntu16 + ubuntu16_vf0.abstract_ubuntu16.disk.device.write.requests.rate_ubuntu16: + - ubuntu16_vf0 + - abstract_ubuntu16.disk.device.write.requests.rate_ubuntu16 + ubuntu16_vf0.abstract_ubuntu16.feature_ubuntu16_ubuntu16_admin_plane_port: + - ubuntu16_vf0 + - abstract_ubuntu16.feature_ubuntu16_ubuntu16_admin_plane_port + ubuntu16_vf0.abstract_ubuntu16.port_mirroring_ubuntu16_ubuntu16_admin_plane_port: + - ubuntu16_vf0 + - abstract_ubuntu16.port_mirroring_ubuntu16_ubuntu16_admin_plane_port + ubuntu16_vf0.abstract_ubuntu16.disk.write.requests.rate_ubuntu16: + - ubuntu16_vf0 + - abstract_ubuntu16.disk.write.requests.rate_ubuntu16 + ubuntu16_vf0.abstract_ubuntu16.disk.root.size_ubuntu16: + - ubuntu16_vf0 + - abstract_ubuntu16.disk.root.size_ubuntu16 + ubuntu16_vf0.abstract_ubuntu16.disk.allocation_ubuntu16: + - ubuntu16_vf0 + - abstract_ubuntu16.disk.allocation_ubuntu16 + ubuntu16_vf0.abstract_ubuntu16.feature_ubuntu16: + - ubuntu16_vf0 + - abstract_ubuntu16.feature_ubuntu16 + ubuntu16_vf0.abstract_ubuntu16.disk.device.read.bytes.rate_ubuntu16: + - ubuntu16_vf0 + - abstract_ubuntu16.disk.device.read.bytes.rate_ubuntu16 + ubuntu16_vf0.abstract_ubuntu16.disk.device.read.requests_ubuntu16: + - ubuntu16_vf0 + - abstract_ubuntu16.disk.device.read.requests_ubuntu16 + ubuntu16_vf0.abstract_ubuntu16.network.incoming.packets.rate_ubuntu16_ubuntu16_admin_plane_port: + - ubuntu16_vf0 + - abstract_ubuntu16.network.incoming.packets.rate_ubuntu16_ubuntu16_admin_plane_port + ubuntu16_vf0.ubuntu16_admin_security_group.feature: + - ubuntu16_vf0 + - ubuntu16_admin_security_group.feature + ubuntu16_vf0.abstract_ubuntu16.host_ubuntu16: + - ubuntu16_vf0 + - abstract_ubuntu16.host_ubuntu16 + ubuntu16_vf0.abstract_ubuntu16.disk.write.bytes_ubuntu16: + - ubuntu16_vf0 + - abstract_ubuntu16.disk.write.bytes_ubuntu16 + ubuntu16_vf0.abstract_ubuntu16.memory_ubuntu16: + - ubuntu16_vf0 + - abstract_ubuntu16.memory_ubuntu16 + ubuntu16_vf0.abstract_ubuntu16.cpu_ubuntu16: + - ubuntu16_vf0 + - abstract_ubuntu16.cpu_ubuntu16 + ubuntu16_vf0.abstract_ubuntu16.disk.usage_ubuntu16: + - ubuntu16_vf0 + - abstract_ubuntu16.disk.usage_ubuntu16 + ubuntu16_vf0.abstract_ubuntu16.cpu_util_ubuntu16: + - ubuntu16_vf0 + - abstract_ubuntu16.cpu_util_ubuntu16 + ubuntu16_vf0.abstract_ubuntu16.disk.write.requests_ubuntu16: + - ubuntu16_vf0 + - abstract_ubuntu16.disk.write.requests_ubuntu16 + ubuntu16_vf0.abstract_ubuntu16.os_ubuntu16: + - ubuntu16_vf0 + - abstract_ubuntu16.os_ubuntu16 + ubuntu16_vf0.abstract_ubuntu16.network.outgoing.packets.rate_ubuntu16_ubuntu16_admin_plane_port: + - ubuntu16_vf0 + - abstract_ubuntu16.network.outgoing.packets.rate_ubuntu16_ubuntu16_admin_plane_port + ubuntu16_vf0.abstract_ubuntu16.disk.ephemeral.size_ubuntu16: + - ubuntu16_vf0 + - abstract_ubuntu16.disk.ephemeral.size_ubuntu16 + ubuntu16_vf0.abstract_ubuntu16.network.outpoing.packets_ubuntu16_ubuntu16_admin_plane_port: + - ubuntu16_vf0 + - abstract_ubuntu16.network.outpoing.packets_ubuntu16_ubuntu16_admin_plane_port + ubuntu16_vf0.abstract_ubuntu16.disk.device.write.bytes.rate_ubuntu16: + - ubuntu16_vf0 + - abstract_ubuntu16.disk.device.write.bytes.rate_ubuntu16 + ubuntu16_vf0.abstract_ubuntu16.binding_ubuntu16: + - ubuntu16_vf0 + - abstract_ubuntu16.binding_ubuntu16 + ubuntu16_vf0.abstract_ubuntu16.disk.read.requests_ubuntu16: + - ubuntu16_vf0 + - abstract_ubuntu16.disk.read.requests_ubuntu16 + ubuntu16_vf0.abstract_ubuntu16.memory.usage_ubuntu16: + - ubuntu16_vf0 + - abstract_ubuntu16.memory.usage_ubuntu16 + ubuntu16_vf0.abstract_ubuntu16.disk.read.bytes.rate_ubuntu16: + - ubuntu16_vf0 + - abstract_ubuntu16.disk.read.bytes.rate_ubuntu16 + ubuntu16_vf0.abstract_ubuntu16.disk.device.allocation_ubuntu16: + - ubuntu16_vf0 + - abstract_ubuntu16.disk.device.allocation_ubuntu16 + ubuntu16_vf0.abstract_ubuntu16.scalable_ubuntu16: + - ubuntu16_vf0 + - abstract_ubuntu16.scalable_ubuntu16 + ubuntu16_vf0.abstract_ubuntu16.network.incoming.bytes_ubuntu16_ubuntu16_admin_plane_port: + - ubuntu16_vf0 + - abstract_ubuntu16.network.incoming.bytes_ubuntu16_ubuntu16_admin_plane_port + ubuntu16_vf0.abstract_ubuntu16.disk.device.read.requests.rate_ubuntu16: + - ubuntu16_vf0 + - abstract_ubuntu16.disk.device.read.requests.rate_ubuntu16 + ubuntu16_vf0.abstract_ubuntu16.instance_ubuntu16: + - ubuntu16_vf0 + - abstract_ubuntu16.instance_ubuntu16 + ubuntu16_vf0.abstract_ubuntu16.network.incoming.packets_ubuntu16_ubuntu16_admin_plane_port: + - ubuntu16_vf0 + - abstract_ubuntu16.network.incoming.packets_ubuntu16_ubuntu16_admin_plane_port + ubuntu16_vf0.abstract_ubuntu16.disk.iops_ubuntu16: + - ubuntu16_vf0 + - abstract_ubuntu16.disk.iops_ubuntu16 + requirements: + ubuntu16_vf0.ubuntu16_admin_security_group.port: + - ubuntu16_vf0 + - ubuntu16_admin_security_group.port + ubuntu16_vf0.abstract_ubuntu16.local_storage_ubuntu16: + - ubuntu16_vf0 + - abstract_ubuntu16.local_storage_ubuntu16 + ubuntu16_vf0.abstract_ubuntu16.dependency_ubuntu16_ubuntu16_admin_plane_port: + - ubuntu16_vf0 + - abstract_ubuntu16.dependency_ubuntu16_ubuntu16_admin_plane_port + ubuntu16_vf0.abstract_ubuntu16.dependency_ubuntu16: + - ubuntu16_vf0 + - abstract_ubuntu16.dependency_ubuntu16 + ubuntu16_vf0.abstract_ubuntu16.link_ubuntu16_ubuntu16_admin_plane_port: + - ubuntu16_vf0 + - abstract_ubuntu16.link_ubuntu16_ubuntu16_admin_plane_port + ubuntu16_vf0.ubuntu16_admin_security_group.dependency: + - ubuntu16_vf0 + - ubuntu16_admin_security_group.dependency diff --git a/tests/data/service-VfwcdsService-template.yml b/tests/data/service-VfwcdsService-template.yml new file mode 100644 index 0000000..15e0beb --- /dev/null +++ b/tests/data/service-VfwcdsService-template.yml @@ -0,0 +1,1439 @@ +tosca_definitions_version: tosca_simple_yaml_1_1 +metadata: + invariantUUID: 40f5261c-38dd-4e7b-9ef4-4af383982efe + UUID: 7227d3dc-fa6c-470c-b752-9879395e3560 + name: vfwcds_SERVICE + description: service + type: Service + category: Network Service + serviceType: '' + serviceRole: '' + instantiationType: Macro + serviceEcompNaming: true + ecompGeneratedNaming: true + namingPolicy: '' + environmentContext: General_Revenue-Bearing + serviceFunction: '' +imports: +- nodes: + file: nodes.yml +- datatypes: + file: data.yml +- capabilities: + file: capabilities.yml +- relationships: + file: relationships.yml +- groups: + file: groups.yml +- policies: + file: policies.yml +- annotations: + file: annotations.yml +- service-vfwcds_SERVICE-interface: + file: service-VfwcdsService-template-interface.yml +- resource-vfwcds_VF: + file: resource-VfwcdsVf-template.yml +- resource-vfwcds_VF-interface: + file: resource-VfwcdsVf-template-interface.yml +topology_template: + inputs: + skip_post_instantiation_configuration: + default: true + type: boolean + required: false + controller_actor: + default: SO-REF-DATA + type: string + required: false + cds_model_version: + type: string + required: false + cds_model_name: + type: string + required: false + node_templates: + vfwcds_VF 0: + type: org.openecomp.resource.vf.VfwcdsVf + metadata: + invariantUUID: 1d96a236-7c64-4bfe-9eeb-5026e060e6cd + UUID: 8726a1fd-e84e-4477-bffb-319defb84b76 + customizationUUID: eabe78af-3a24-45ed-a50a-24ad778d0bac + version: '1.0' + name: vfwcds_VF + description: VF + type: VF + category: Generic + subcategory: Abstract + resourceVendor: VNFVendor + resourceVendorRelease: '1.0' + resourceVendorModelNumber: '' + properties: + vf_module_id: vFirewallCL + skip_post_instantiation_configuration: false + controller_actor: CDS + vsn_flavor_name: PUT THE VM FLAVOR NAME HERE (m1.medium suggested) + vfw_int_private2_ip_0: 192.168.20.100 + int_private1_subnet_id: zdfw1fwl01_unprotected_sub + public_net_id: PUT THE PUBLIC NETWORK ID HERE + vnf_name: vFW_NextGen + onap_private_subnet_id: PUT THE ONAP PRIVATE NETWORK NAME HERE + vsn_int_private2_ip_0: 192.168.20.250 + sec_group: PUT THE ONAP SECURITY GROUP HERE + vfw_name_0: zdfw1fwl01fwl01 + nexus_artifact_repo: https://nexus.onap.org + onap_private_net_cidr: 10.0.0.0/16 + vpg_onap_private_ip_0: 10.0.100.2 + dcae_collector_ip: 10.0.4.1 + vsn_image_name: PUT THE VM IMAGE NAME HERE (UBUNTU 1404) + vnf_id: vSink_demo_app + vpg_flavor_name: PUT THE VM FLAVOR NAME HERE (m1.medium suggested) + dcae_collector_port: '30417' + vfw_int_private2_floating_ip: 192.168.10.200 + vpg_name_0: zdfw1fwl01pgn01 + int_private2_subnet_id: zdfw1fwl01_protected_sub + int_private2_net_cidr: 192.168.20.0/24 + nf_naming: + ecomp_generated_naming: true + vsn_name_0: zdfw1fwl01snk01 + multi_stage_design: 'false' + vpg_image_name: PUT THE VM IMAGE NAME HERE (UBUNTU 1404) + onap_private_net_id: PUT THE ONAP PRIVATE NETWORK NAME HERE + availability_zone_max_count: 1 + sdnc_artifact_name: vfwcds + vsn_onap_private_ip_0: 10.0.100.3 + vfw_flavor_name: PUT THE VM FLAVOR NAME HERE (m1.medium suggested) + demo_artifacts_version: 1.6.0-SNAPSHOT + pub_key: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDQXYJYYi3/OUZXUiCYWdtc7K0m5C0dJKVxPG0eI8EWZrEHYdfYe6WoTSDJCww+1qlBSpA5ac/Ba4Wn9vh+lR1vtUKkyIC/nrYb90ReUd385Glkgzrfh5HdR5y5S2cL/Frh86lAn9r6b3iWTJD8wBwXFyoe1S2nMTOIuG4RPNvfmyCTYVh8XTCCE8HPvh3xv2r4egawG1P4Q4UDwk+hDBXThY2KS8M5/8EMyxHV0ImpLbpYCTBA6KYDIRtqmgS6iKyy8v2D1aSY5mc9J0T5t9S2Gv+VZQNWQDDKNFnxqYaAo1uEoq/i1q63XC5AD3ckXb2VT6dp23BQMdDfbHyUWfJN + key_name: vfw_key + vfw_int_private1_ip_0: 192.168.10.100 + sdnc_model_version: 1.0.0 + int_private1_net_cidr: 192.168.10.0/24 + install_script_version: 1.6.0-SNAPSHOT + vfw_image_name: PUT THE VM IMAGE NAME HERE (UBUNTU 1404) + vfw_onap_private_ip_0: 10.0.100.1 + vpg_int_private1_ip_0: 192.168.10.200 + int_private2_net_id: zdfw1fwl01_protected + cloud_env: PUT openstack OR rackspace HERE + sdnc_model_name: vFW-CDS + int_private1_net_id: zdfw1fwl01_unprotected + capabilities: + abstract_vfw.cpu_vfw: + properties: + name: cpu + abstract_vpg.memory.resident_vpg: + properties: + name: memory.resident + abstract_vfw.disk.device.read.requests_vfw: + properties: + name: disk.device.read.requests + abstract_vpg.disk.write.bytes_vpg: + properties: + name: disk.write.bytes + abstract_vsn.disk.device.write.requests.rate_vsn: + properties: + name: disk.device.write.requests.rate + abstract_vpg.disk.usage_vpg: + properties: + name: disk.usage + abstract_vfw.network.outpoing.packets_vfw_vfw_int_private1_port_0: + properties: + name: network.outpoing.packets + abstract_vfw.disk.device.latency_vfw: + properties: + name: disk.device.latency + abstract_vfw.network.outpoing.packets_vfw_vfw_int_private2_port_0: + properties: + name: network.outpoing.packets + abstract_vfw.network.outpoing.packets_vfw_vfw_onap_private_port_0: + properties: + name: network.outpoing.packets + abstract_vsn.disk.device.write.bytes_vsn: + properties: + name: disk.device.write.bytes + abstract_vfw.disk.read.requests_vfw: + properties: + name: disk.read.requests + abstract_vsn.memory.resident_vsn: + properties: + name: memory.resident + abstract_vfw.network.incoming.bytes_vfw_vfw_int_private1_port_0: + properties: + name: network.incoming.bytes + abstract_vfw.network.incoming.bytes_vfw_vfw_int_private2_port_0: + properties: + name: network.incoming.bytes + abstract_vfw.memory_vfw: + properties: + name: memory + abstract_vsn.disk.device.read.requests.rate_vsn: + properties: + name: disk.device.read.requests.rate + abstract_vfw.disk.device.allocation_vfw: + properties: + name: disk.device.allocation + abstract_vsn.network.outgoing.packets.rate_vsn_vsn_int_private2_port_0: + properties: + name: network.outgoing.packets.rate + abstract_vfw.disk.root.size_vfw: + properties: + name: disk.root.size + abstract_vpg.network.incoming.packets_vpg_vpg_int_private1_port_0: + properties: + name: network.incoming.packets + abstract_vpg.network.outpoing.packets_vpg_vpg_onap_private_port_0: + properties: + name: network.outpoing.packets + abstract_vpg.memory.usage_vpg: + properties: + name: memory.usage + abstract_vfw.disk.capacity_vfw: + properties: + name: disk.capacity + abstract_vfw.disk.allocation_vfw: + properties: + name: disk.allocation + abstract_vpg.disk.iops_vpg: + properties: + name: disk.iops + abstract_vpg.disk.device.capacity_vpg: + properties: + name: disk.device.capacity + abstract_vsn.scalable_vsn: + properties: + max_instances: 1 + min_instances: 1 + abstract_vsn.network.incoming.packets.rate_vsn_vsn_int_private2_port_0: + properties: + name: network.incoming.packets.rate + abstract_vpg.disk.write.requests.rate_vpg: + properties: + name: disk.write.requests.rate + abstract_vsn.disk.read.bytes_vsn: + properties: + name: disk.read.bytes + abstract_vsn.network.incoming.bytes.rate_vsn_vsn_onap_private_port_0: + properties: + name: network.incoming.bytes.rate + abstract_vfw.disk.device.iops_vfw: + properties: + name: disk.device.iops + abstract_vsn.disk.device.write.requests_vsn: + properties: + name: disk.device.write.requests + abstract_vfw.disk.write.requests_vfw: + properties: + name: disk.write.requests + abstract_vpg.network.outgoing.bytes_vpg_vpg_int_private1_port_0: + properties: + name: network.outgoing.bytes + abstract_vsn.disk.device.allocation_vsn: + properties: + name: disk.device.allocation + abstract_vpg.disk.read.bytes_vpg: + properties: + name: disk.read.bytes + abstract_vsn.disk.ephemeral.size_vsn: + properties: + name: disk.ephemeral.size + abstract_vsn.cpu_vsn: + properties: + name: cpu + abstract_vpg.network.outgoing.packets.rate_vpg_vpg_onap_private_port_0: + properties: + name: network.outgoing.packets.rate + abstract_vpg.network.incoming.packets.rate_vpg_vpg_int_private1_port_0: + properties: + name: network.incoming.packets.rate + abstract_vsn.disk.write.bytes_vsn: + properties: + name: disk.write.bytes + abstract_vfw.network.incoming.packets.rate_vfw_vfw_int_private2_port_0: + properties: + name: network.incoming.packets.rate + abstract_vpg.disk.device.read.bytes_vpg: + properties: + name: disk.device.read.bytes + int_private1_network.end_point: + properties: + protocol: tcp + initiator: source + network_name: PRIVATE + secure: false + abstract_vfw.network.incoming.bytes.rate_vfw_vfw_int_private2_port_0: + properties: + name: network.incoming.bytes.rate + abstract_vfw.network.outgoing.bytes_vfw_vfw_onap_private_port_0: + properties: + name: network.outgoing.bytes + abstract_vpg.disk.capacity_vpg: + properties: + name: disk.capacity + abstract_vfw.network.outgoing.packets.rate_vfw_vfw_onap_private_port_0: + properties: + name: network.outgoing.packets.rate + abstract_vpg.disk.device.read.bytes.rate_vpg: + properties: + name: disk.device.read.bytes.rate + abstract_vfw.network.outgoing.bytes.rate_vfw_vfw_onap_private_port_0: + properties: + name: network.outgoing.bytes.rate + abstract_vfw.disk.usage_vfw: + properties: + name: disk.usage + abstract_vsn.disk.device.latency_vsn: + properties: + name: disk.device.latency + abstract_vpg.scalable_vpg: + properties: + max_instances: 1 + min_instances: 1 + abstract_vsn.instance_vsn: + properties: + name: instance + abstract_vpg.disk.device.allocation_vpg: + properties: + name: disk.device.allocation + abstract_vsn.disk.write.requests_vsn: + properties: + name: disk.write.requests + abstract_vfw.disk.write.bytes_vfw: + properties: + name: disk.write.bytes + abstract_vsn.disk.device.read.bytes.rate_vsn: + properties: + name: disk.device.read.bytes.rate + abstract_vsn.vcpus_vsn: + properties: + name: vcpus + abstract_vfw.disk.device.write.bytes_vfw: + properties: + name: disk.device.write.bytes + abstract_vsn.network.outgoing.bytes.rate_vsn_vsn_onap_private_port_0: + properties: + name: network.outgoing.bytes.rate + abstract_vfw.network.incoming.packets.rate_vfw_vfw_onap_private_port_0: + properties: + name: network.incoming.packets.rate + abstract_vsn.cpu.delta_vsn: + properties: + name: cpu.delta + abstract_vfw.memory.resident_vfw: + properties: + name: memory.resident + abstract_vpg.disk.device.iops_vpg: + properties: + name: disk.device.iops + abstract_vsn.cpu_util_vsn: + properties: + name: cpu_util + abstract_vpg.network.incoming.packets.rate_vpg_vpg_onap_private_port_0: + properties: + name: network.incoming.packets.rate + abstract_vpg.network.outgoing.packets.rate_vpg_vpg_int_private1_port_0: + properties: + name: network.outgoing.packets.rate + abstract_vpg.disk.latency_vpg: + properties: + name: disk.latency + abstract_vsn.port_mirroring_vsn_vsn_onap_private_port_0: + properties: + connection_point: + network_role: + get_input: port_vsn_onap_private_port_0_network_role + nfc_naming_code: vsn + abstract_vpg.instance_vpg: + properties: + name: instance + abstract_vfw.network.incoming.bytes_vfw_vfw_onap_private_port_0: + properties: + name: network.incoming.bytes + abstract_vpg.port_mirroring_vpg_vpg_onap_private_port_0: + properties: + connection_point: + network_role: + get_input: port_vpg_onap_private_port_0_network_role + nfc_naming_code: vpg + abstract_vfw.network.outgoing.bytes.rate_vfw_vfw_int_private2_port_0: + properties: + name: network.outgoing.bytes.rate + abstract_vfw.network.outgoing.packets.rate_vfw_vfw_int_private2_port_0: + properties: + name: network.outgoing.packets.rate + abstract_vfw.port_mirroring_vfw_vfw_onap_private_port_0: + properties: + connection_point: + network_role: + get_input: port_vfw_onap_private_port_0_network_role + nfc_naming_code: vfw + abstract_vfw.network.incoming.bytes.rate_vfw_vfw_onap_private_port_0: + properties: + name: network.incoming.bytes.rate + abstract_vpg.network.incoming.packets_vpg_vpg_onap_private_port_0: + properties: + name: network.incoming.packets + abstract_vsn.disk.allocation_vsn: + properties: + name: disk.allocation + abstract_vsn.disk.capacity_vsn: + properties: + name: disk.capacity + abstract_vpg.network.incoming.bytes_vpg_vpg_int_private1_port_0: + properties: + name: network.incoming.bytes + abstract_vpg.disk.allocation_vpg: + properties: + name: disk.allocation + abstract_vsn.disk.device.capacity_vsn: + properties: + name: disk.device.capacity + abstract_vfw.disk.device.read.requests.rate_vfw: + properties: + name: disk.device.read.requests.rate + abstract_vsn.disk.root.size_vsn: + properties: + name: disk.root.size + abstract_vsn.disk.usage_vsn: + properties: + name: disk.usage + abstract_vsn.disk.write.bytes.rate_vsn: + properties: + name: disk.write.bytes.rate + abstract_vsn.network.outgoing.bytes_vsn_vsn_int_private2_port_0: + properties: + name: network.outgoing.bytes + abstract_vsn.endpoint_vsn: + properties: + secure: true + abstract_vpg.disk.read.bytes.rate_vpg: + properties: + name: disk.read.bytes.rate + abstract_vpg.disk.device.read.requests_vpg: + properties: + name: disk.device.read.requests + abstract_vsn.network.outgoing.bytes_vsn_vsn_onap_private_port_0: + properties: + name: network.outgoing.bytes + abstract_vfw.disk.ephemeral.size_vfw: + properties: + name: disk.ephemeral.size + abstract_vpg.network.incoming.bytes.rate_vpg_vpg_onap_private_port_0: + properties: + name: network.incoming.bytes.rate + abstract_vpg.network.incoming.bytes.rate_vpg_vpg_int_private1_port_0: + properties: + name: network.incoming.bytes.rate + abstract_vpg.disk.read.requests_vpg: + properties: + name: disk.read.requests + abstract_vfw.network.incoming.packets_vfw_vfw_onap_private_port_0: + properties: + name: network.incoming.packets + abstract_vsn.disk.device.write.bytes.rate_vsn: + properties: + name: disk.device.write.bytes.rate + abstract_vpg.network.incoming.bytes_vpg_vpg_onap_private_port_0: + properties: + name: network.incoming.bytes + abstract_vpg.disk.device.write.bytes_vpg: + properties: + name: disk.device.write.bytes + abstract_vpg.network.outgoing.bytes.rate_vpg_vpg_int_private1_port_0: + properties: + name: network.outgoing.bytes.rate + abstract_vsn.memory.usage_vsn: + properties: + name: memory.usage + abstract_vsn.network.outgoing.bytes.rate_vsn_vsn_int_private2_port_0: + properties: + name: network.outgoing.bytes.rate + abstract_vsn.network.incoming.packets_vsn_vsn_int_private2_port_0: + properties: + name: network.incoming.packets + abstract_vsn.disk.device.iops_vsn: + properties: + name: disk.device.iops + abstract_vfw.network.incoming.packets_vfw_vfw_int_private1_port_0: + properties: + name: network.incoming.packets + abstract_vpg.disk.device.write.bytes.rate_vpg: + properties: + name: disk.device.write.bytes.rate + abstract_vsn.disk.device.read.bytes_vsn: + properties: + name: disk.device.read.bytes + abstract_vfw.disk.latency_vfw: + properties: + name: disk.latency + abstract_vsn.disk.read.requests_vsn: + properties: + name: disk.read.requests + abstract_vpg.endpoint_vpg: + properties: + secure: true + abstract_vsn.network.incoming.bytes.rate_vsn_vsn_int_private2_port_0: + properties: + name: network.incoming.bytes.rate + abstract_vsn.disk.device.read.requests_vsn: + properties: + name: disk.device.read.requests + abstract_vsn.memory_vsn: + properties: + name: memory + abstract_vfw.disk.device.read.bytes.rate_vfw: + properties: + name: disk.device.read.bytes.rate + abstract_vfw.disk.device.read.bytes_vfw: + properties: + name: disk.device.read.bytes + abstract_vfw.network.outgoing.bytes.rate_vfw_vfw_int_private1_port_0: + properties: + name: network.outgoing.bytes.rate + abstract_vsn.network.incoming.packets.rate_vsn_vsn_onap_private_port_0: + properties: + name: network.incoming.packets.rate + abstract_vfw.network.outgoing.packets.rate_vfw_vfw_int_private1_port_0: + properties: + name: network.outgoing.packets.rate + abstract_vfw.disk.read.bytes_vfw: + properties: + name: disk.read.bytes + abstract_vfw.disk.read.bytes.rate_vfw: + properties: + name: disk.read.bytes.rate + abstract_vfw.endpoint_vfw: + properties: + secure: true + abstract_vfw.disk.device.write.requests_vfw: + properties: + name: disk.device.write.requests + int_private2_network.end_point: + properties: + protocol: tcp + initiator: source + network_name: PRIVATE + secure: false + abstract_vpg.cpu_util_vpg: + properties: + name: cpu_util + abstract_vfw.network.outgoing.bytes_vfw_vfw_int_private2_port_0: + properties: + name: network.outgoing.bytes + abstract_vpg.vcpus_vpg: + properties: + name: vcpus + abstract_vpg.cpu_vpg: + properties: + name: cpu + abstract_vsn.network.incoming.packets_vsn_vsn_onap_private_port_0: + properties: + name: network.incoming.packets + abstract_vpg.network.outpoing.packets_vpg_vpg_int_private1_port_0: + properties: + name: network.outpoing.packets + abstract_vfw.disk.write.requests.rate_vfw: + properties: + name: disk.write.requests.rate + abstract_vpg.disk.root.size_vpg: + properties: + name: disk.root.size + abstract_vsn.disk.write.requests.rate_vsn: + properties: + name: disk.write.requests.rate + abstract_vpg.disk.device.usage_vpg: + properties: + name: disk.device.usage + abstract_vpg.disk.device.write.requests.rate_vpg: + properties: + name: disk.device.write.requests.rate + abstract_vfw.disk.device.capacity_vfw: + properties: + name: disk.device.capacity + abstract_vsn.network.outgoing.packets.rate_vsn_vsn_onap_private_port_0: + properties: + name: network.outgoing.packets.rate + abstract_vfw.network.incoming.packets.rate_vfw_vfw_int_private1_port_0: + properties: + name: network.incoming.packets.rate + abstract_vfw.network.incoming.bytes.rate_vfw_vfw_int_private1_port_0: + properties: + name: network.incoming.bytes.rate + abstract_vfw.cpu_util_vfw: + properties: + name: cpu_util + abstract_vfw.cpu.delta_vfw: + properties: + name: cpu.delta + abstract_vfw.vcpus_vfw: + properties: + name: vcpus + abstract_vsn.disk.iops_vsn: + properties: + name: disk.iops + abstract_vfw.disk.iops_vfw: + properties: + name: disk.iops + abstract_vfw.disk.write.bytes.rate_vfw: + properties: + name: disk.write.bytes.rate + abstract_vfw.scalable_vfw: + properties: + max_instances: 1 + min_instances: 1 + abstract_vpg.memory_vpg: + properties: + name: memory + abstract_vpg.disk.ephemeral.size_vpg: + properties: + name: disk.ephemeral.size + abstract_vfw.memory.usage_vfw: + properties: + name: memory.usage + abstract_vpg.disk.device.read.requests.rate_vpg: + properties: + name: disk.device.read.requests.rate + abstract_vpg.disk.device.write.requests_vpg: + properties: + name: disk.device.write.requests + abstract_vpg.disk.device.latency_vpg: + properties: + name: disk.device.latency + abstract_vfw.disk.device.usage_vfw: + properties: + name: disk.device.usage + abstract_vfw.network.incoming.packets_vfw_vfw_int_private2_port_0: + properties: + name: network.incoming.packets + abstract_vfw.disk.device.write.bytes.rate_vfw: + properties: + name: disk.device.write.bytes.rate + abstract_vpg.cpu.delta_vpg: + properties: + name: cpu.delta + abstract_vsn.network.outpoing.packets_vsn_vsn_onap_private_port_0: + properties: + name: network.outpoing.packets + abstract_vfw.instance_vfw: + properties: + name: instance + abstract_vsn.disk.latency_vsn: + properties: + name: disk.latency + abstract_vsn.network.incoming.bytes_vsn_vsn_onap_private_port_0: + properties: + name: network.incoming.bytes + abstract_vpg.disk.write.bytes.rate_vpg: + properties: + name: disk.write.bytes.rate + abstract_vfw.network.outgoing.bytes_vfw_vfw_int_private1_port_0: + properties: + name: network.outgoing.bytes + abstract_vpg.network.outgoing.bytes_vpg_vpg_onap_private_port_0: + properties: + name: network.outgoing.bytes + abstract_vsn.network.incoming.bytes_vsn_vsn_int_private2_port_0: + properties: + name: network.incoming.bytes + abstract_vfw.disk.device.write.requests.rate_vfw: + properties: + name: disk.device.write.requests.rate + abstract_vpg.network.outgoing.bytes.rate_vpg_vpg_onap_private_port_0: + properties: + name: network.outgoing.bytes.rate + abstract_vpg.disk.write.requests_vpg: + properties: + name: disk.write.requests + abstract_vsn.disk.read.bytes.rate_vsn: + properties: + name: disk.read.bytes.rate + abstract_vsn.disk.device.usage_vsn: + properties: + name: disk.device.usage + abstract_vsn.network.outpoing.packets_vsn_vsn_int_private2_port_0: + properties: + name: network.outpoing.packets + groups: + vfwcds_vf0..VfwcdsVf..vsn..module-1: + type: org.openecomp.groups.VfModule + metadata: + vfModuleModelName: VfwcdsVf..vsn..module-1 + vfModuleModelInvariantUUID: f4b9d91b-75b7-4d14-98e5-907d8034fc4c + vfModuleModelUUID: fda666d1-d0d0-47c4-82cf-d02ee041a021 + vfModuleModelVersion: '1' + vfModuleModelCustomizationUUID: 34040775-31da-4034-9387-e2bb3fab236c + properties: + min_vf_module_instances: 0 + vf_module_label: vsn + vf_module_type: Expansion + isBase: false + initial_count: 0 + volume_group: false + vfwcds_vf0..VfwcdsVf..vpg..module-2: + type: org.openecomp.groups.VfModule + metadata: + vfModuleModelName: VfwcdsVf..vpg..module-2 + vfModuleModelInvariantUUID: 048e1738-4cc7-49d4-99a4-fb2d75e7a8db + vfModuleModelUUID: cfd79d7d-e63a-46ab-a765-b6753c781f32 + vfModuleModelVersion: '1' + vfModuleModelCustomizationUUID: d134bf7f-af50-4ea2-aa9c-1220497a6ec7 + properties: + min_vf_module_instances: 0 + vf_module_label: vpg + vf_module_type: Expansion + isBase: false + initial_count: 0 + volume_group: false + vfwcds_vf0..VfwcdsVf..vfw..module-3: + type: org.openecomp.groups.VfModule + metadata: + vfModuleModelName: VfwcdsVf..vfw..module-3 + vfModuleModelInvariantUUID: 09d24ca5-b3ef-4f82-a3ca-532e565929ed + vfModuleModelUUID: adf1eae4-0bb1-456e-8162-3cbc0554349f + vfModuleModelVersion: '1' + vfModuleModelCustomizationUUID: 2d9a63ad-d364-471f-a780-29140f3a2d62 + properties: + min_vf_module_instances: 0 + vf_module_label: vfw + vf_module_type: Expansion + isBase: false + initial_count: 0 + volume_group: false + vfwcds_vf0..VfwcdsVf..base_template..module-0: + type: org.openecomp.groups.VfModule + metadata: + vfModuleModelName: VfwcdsVf..base_template..module-0 + vfModuleModelInvariantUUID: 1b1c6c52-b506-4a37-9b83-8fddab20a0e5 + vfModuleModelUUID: 49dc98c5-2e56-4d37-a8bb-5f814b772a42 + vfModuleModelVersion: '1' + vfModuleModelCustomizationUUID: 4533702f-7d3f-4205-a5d4-d1b213ab8f2e + properties: + min_vf_module_instances: 1 + vf_module_label: base_template + max_vf_module_instances: 1 + vf_module_type: Base + isBase: true + initial_count: 1 + volume_group: false + substitution_mappings: + node_type: org.openecomp.service.VfwcdsService + capabilities: + vfwcds_vf0.abstract_vsn.instance_vsn: + - vfwcds_VF 0 + - abstract_vsn.instance_vsn + vfwcds_vf0.abstract_vsn.disk.device.read.bytes_vsn: + - vfwcds_VF 0 + - abstract_vsn.disk.device.read.bytes_vsn + vfwcds_vf0.abstract_vpg.memory.usage_vpg: + - vfwcds_VF 0 + - abstract_vpg.memory.usage_vpg + vfwcds_vf0.abstract_vsn.network.outpoing.packets_vsn_vsn_onap_private_port_0: + - vfwcds_VF 0 + - abstract_vsn.network.outpoing.packets_vsn_vsn_onap_private_port_0 + vfwcds_vf0.abstract_vfw.attachment_vfw_vfw_int_private2_port_0: + - vfwcds_VF 0 + - abstract_vfw.attachment_vfw_vfw_int_private2_port_0 + vfwcds_vf0.abstract_vsn.host_vsn: + - vfwcds_VF 0 + - abstract_vsn.host_vsn + vfwcds_vf0.abstract_vsn.network.outgoing.packets.rate_vsn_vsn_int_private2_port_0: + - vfwcds_VF 0 + - abstract_vsn.network.outgoing.packets.rate_vsn_vsn_int_private2_port_0 + vfwcds_vf0.abstract_vfw.cpu.delta_vfw: + - vfwcds_VF 0 + - abstract_vfw.cpu.delta_vfw + vfwcds_vf0.abstract_vfw.network.incoming.packets_vfw_vfw_int_private2_port_0: + - vfwcds_VF 0 + - abstract_vfw.network.incoming.packets_vfw_vfw_int_private2_port_0 + vfwcds_vf0.abstract_vfw.forwarder_vfw_vfw_onap_private_port_0: + - vfwcds_VF 0 + - abstract_vfw.forwarder_vfw_vfw_onap_private_port_0 + vfwcds_vf0.abstract_vsn.disk.device.usage_vsn: + - vfwcds_VF 0 + - abstract_vsn.disk.device.usage_vsn + vfwcds_vf0.abstract_vfw.feature_vfw_vfw_onap_private_port_0: + - vfwcds_VF 0 + - abstract_vfw.feature_vfw_vfw_onap_private_port_0 + vfwcds_vf0.abstract_vfw.feature_vfw_vfw_int_private1_port_0: + - vfwcds_VF 0 + - abstract_vfw.feature_vfw_vfw_int_private1_port_0 + vfwcds_vf0.abstract_vpg.port_mirroring_vpg_vpg_onap_private_port_0: + - vfwcds_VF 0 + - abstract_vpg.port_mirroring_vpg_vpg_onap_private_port_0 + vfwcds_vf0.abstract_vfw.disk.device.read.bytes_vfw: + - vfwcds_VF 0 + - abstract_vfw.disk.device.read.bytes_vfw + vfwcds_vf0.abstract_vpg.network.incoming.packets_vpg_vpg_onap_private_port_0: + - vfwcds_VF 0 + - abstract_vpg.network.incoming.packets_vpg_vpg_onap_private_port_0 + vfwcds_vf0.abstract_vfw.forwarder_vfw_vfw_int_private1_port_0: + - vfwcds_VF 0 + - abstract_vfw.forwarder_vfw_vfw_int_private1_port_0 + vfwcds_vf0.abstract_vfw.disk.latency_vfw: + - vfwcds_VF 0 + - abstract_vfw.disk.latency_vfw + vfwcds_vf0.abstract_vpg.memory.resident_vpg: + - vfwcds_VF 0 + - abstract_vpg.memory.resident_vpg + vfwcds_vf0.abstract_vfw.os_vfw: + - vfwcds_VF 0 + - abstract_vfw.os_vfw + vfwcds_vf0.abstract_vpg.instance_vpg: + - vfwcds_VF 0 + - abstract_vpg.instance_vpg + vfwcds_vf0.abstract_vfw.disk.read.bytes_vfw: + - vfwcds_VF 0 + - abstract_vfw.disk.read.bytes_vfw + vfwcds_vf0.abstract_vfw.feature_vfw: + - vfwcds_VF 0 + - abstract_vfw.feature_vfw + vfwcds_vf0.abstract_vfw.disk.device.read.requests_vfw: + - vfwcds_VF 0 + - abstract_vfw.disk.device.read.requests_vfw + vfwcds_vf0.int_private2_network.link: + - vfwcds_VF 0 + - int_private2_network.link + vfwcds_vf0.abstract_vpg.disk.device.latency_vpg: + - vfwcds_VF 0 + - abstract_vpg.disk.device.latency_vpg + vfwcds_vf0.abstract_vpg.network.outgoing.bytes.rate_vpg_vpg_int_private1_port_0: + - vfwcds_VF 0 + - abstract_vpg.network.outgoing.bytes.rate_vpg_vpg_int_private1_port_0 + vfwcds_vf0.abstract_vsn.forwarder_vsn_vsn_int_private2_port_0: + - vfwcds_VF 0 + - abstract_vsn.forwarder_vsn_vsn_int_private2_port_0 + vfwcds_vf0.abstract_vfw.binding_vfw_vfw_int_private1_port_0: + - vfwcds_VF 0 + - abstract_vfw.binding_vfw_vfw_int_private1_port_0 + vfwcds_vf0.abstract_vfw.network.incoming.packets.rate_vfw_vfw_onap_private_port_0: + - vfwcds_VF 0 + - abstract_vfw.network.incoming.packets.rate_vfw_vfw_onap_private_port_0 + vfwcds_vf0.abstract_vpg.disk.root.size_vpg: + - vfwcds_VF 0 + - abstract_vpg.disk.root.size_vpg + vfwcds_vf0.abstract_vfw.memory.resident_vfw: + - vfwcds_VF 0 + - abstract_vfw.memory.resident_vfw + vfwcds_vf0.abstract_vpg.forwarder_vpg_vpg_int_private1_port_0: + - vfwcds_VF 0 + - abstract_vpg.forwarder_vpg_vpg_int_private1_port_0 + vfwcds_vf0.abstract_vsn.vcpus_vsn: + - vfwcds_VF 0 + - abstract_vsn.vcpus_vsn + vfwcds_vf0.abstract_vpg.disk.device.read.bytes.rate_vpg: + - vfwcds_VF 0 + - abstract_vpg.disk.device.read.bytes.rate_vpg + vfwcds_vf0.abstract_vfw.disk.device.write.bytes.rate_vfw: + - vfwcds_VF 0 + - abstract_vfw.disk.device.write.bytes.rate_vfw + vfwcds_vf0.abstract_vpg.vcpus_vpg: + - vfwcds_VF 0 + - abstract_vpg.vcpus_vpg + vfwcds_vf0.abstract_vpg.disk.device.write.requests.rate_vpg: + - vfwcds_VF 0 + - abstract_vpg.disk.device.write.requests.rate_vpg + vfwcds_vf0.abstract_vsn.disk.device.latency_vsn: + - vfwcds_VF 0 + - abstract_vsn.disk.device.latency_vsn + vfwcds_vf0.abstract_vfw.disk.root.size_vfw: + - vfwcds_VF 0 + - abstract_vfw.disk.root.size_vfw + vfwcds_vf0.abstract_vfw.disk.device.capacity_vfw: + - vfwcds_VF 0 + - abstract_vfw.disk.device.capacity_vfw + vfwcds_vf0.abstract_vsn.disk.device.read.bytes.rate_vsn: + - vfwcds_VF 0 + - abstract_vsn.disk.device.read.bytes.rate_vsn + vfwcds_vf0.abstract_vsn.disk.write.requests.rate_vsn: + - vfwcds_VF 0 + - abstract_vsn.disk.write.requests.rate_vsn + vfwcds_vf0.abstract_vsn.cpu.delta_vsn: + - vfwcds_VF 0 + - abstract_vsn.cpu.delta_vsn + vfwcds_vf0.abstract_vpg.disk.capacity_vpg: + - vfwcds_VF 0 + - abstract_vpg.disk.capacity_vpg + vfwcds_vf0.abstract_vpg.network.outgoing.bytes.rate_vpg_vpg_onap_private_port_0: + - vfwcds_VF 0 + - abstract_vpg.network.outgoing.bytes.rate_vpg_vpg_onap_private_port_0 + vfwcds_vf0.abstract_vsn.os_vsn: + - vfwcds_VF 0 + - abstract_vsn.os_vsn + vfwcds_vf0.abstract_vfw.binding_vfw_vfw_onap_private_port_0: + - vfwcds_VF 0 + - abstract_vfw.binding_vfw_vfw_onap_private_port_0 + vfwcds_vf0.abstract_vsn.disk.root.size_vsn: + - vfwcds_VF 0 + - abstract_vsn.disk.root.size_vsn + vfwcds_vf0.abstract_vsn.network.outgoing.bytes_vsn_vsn_onap_private_port_0: + - vfwcds_VF 0 + - abstract_vsn.network.outgoing.bytes_vsn_vsn_onap_private_port_0 + vfwcds_vf0.abstract_vfw.network.incoming.packets.rate_vfw_vfw_int_private1_port_0: + - vfwcds_VF 0 + - abstract_vfw.network.incoming.packets.rate_vfw_vfw_int_private1_port_0 + vfwcds_vf0.abstract_vfw.network.outpoing.packets_vfw_vfw_int_private2_port_0: + - vfwcds_VF 0 + - abstract_vfw.network.outpoing.packets_vfw_vfw_int_private2_port_0 + vfwcds_vf0.abstract_vfw.scalable_vfw: + - vfwcds_VF 0 + - abstract_vfw.scalable_vfw + vfwcds_vf0.abstract_vpg.network.incoming.bytes_vpg_vpg_int_private1_port_0: + - vfwcds_VF 0 + - abstract_vpg.network.incoming.bytes_vpg_vpg_int_private1_port_0 + vfwcds_vf0.abstract_vsn.feature_vsn_vsn_onap_private_port_0: + - vfwcds_VF 0 + - abstract_vsn.feature_vsn_vsn_onap_private_port_0 + vfwcds_vf0.abstract_vsn.disk.device.capacity_vsn: + - vfwcds_VF 0 + - abstract_vsn.disk.device.capacity_vsn + vfwcds_vf0.abstract_vsn.binding_vsn_vsn_onap_private_port_0: + - vfwcds_VF 0 + - abstract_vsn.binding_vsn_vsn_onap_private_port_0 + vfwcds_vf0.abstract_vfw.binding_vfw: + - vfwcds_VF 0 + - abstract_vfw.binding_vfw + vfwcds_vf0.abstract_vpg.os_vpg: + - vfwcds_VF 0 + - abstract_vpg.os_vpg + vfwcds_vf0.abstract_vpg.disk.device.write.bytes.rate_vpg: + - vfwcds_VF 0 + - abstract_vpg.disk.device.write.bytes.rate_vpg + vfwcds_vf0.abstract_vsn.cpu_util_vsn: + - vfwcds_VF 0 + - abstract_vsn.cpu_util_vsn + vfwcds_vf0.abstract_vpg.disk.write.bytes.rate_vpg: + - vfwcds_VF 0 + - abstract_vpg.disk.write.bytes.rate_vpg + vfwcds_vf0.abstract_vsn.disk.device.allocation_vsn: + - vfwcds_VF 0 + - abstract_vsn.disk.device.allocation_vsn + vfwcds_vf0.abstract_vfw.network.outgoing.packets.rate_vfw_vfw_int_private1_port_0: + - vfwcds_VF 0 + - abstract_vfw.network.outgoing.packets.rate_vfw_vfw_int_private1_port_0 + vfwcds_vf0.abstract_vsn.network.outgoing.bytes.rate_vsn_vsn_onap_private_port_0: + - vfwcds_VF 0 + - abstract_vsn.network.outgoing.bytes.rate_vsn_vsn_onap_private_port_0 + vfwcds_vf0.abstract_vpg.network.outpoing.packets_vpg_vpg_int_private1_port_0: + - vfwcds_VF 0 + - abstract_vpg.network.outpoing.packets_vpg_vpg_int_private1_port_0 + vfwcds_vf0.abstract_vpg.disk.read.bytes.rate_vpg: + - vfwcds_VF 0 + - abstract_vpg.disk.read.bytes.rate_vpg + vfwcds_vf0.int_private1_network.link: + - vfwcds_VF 0 + - int_private1_network.link + vfwcds_vf0.abstract_vfw.disk.device.iops_vfw: + - vfwcds_VF 0 + - abstract_vfw.disk.device.iops_vfw + vfwcds_vf0.abstract_vsn.network.outgoing.bytes.rate_vsn_vsn_int_private2_port_0: + - vfwcds_VF 0 + - abstract_vsn.network.outgoing.bytes.rate_vsn_vsn_int_private2_port_0 + vfwcds_vf0.abstract_vsn.disk.device.write.requests.rate_vsn: + - vfwcds_VF 0 + - abstract_vsn.disk.device.write.requests.rate_vsn + vfwcds_vf0.abstract_vpg.disk.allocation_vpg: + - vfwcds_VF 0 + - abstract_vpg.disk.allocation_vpg + vfwcds_vf0.abstract_vsn.binding_vsn_vsn_int_private2_port_0: + - vfwcds_VF 0 + - abstract_vsn.binding_vsn_vsn_int_private2_port_0 + vfwcds_vf0.abstract_vfw.network.outgoing.bytes.rate_vfw_vfw_int_private1_port_0: + - vfwcds_VF 0 + - abstract_vfw.network.outgoing.bytes.rate_vfw_vfw_int_private1_port_0 + vfwcds_vf0.abstract_vfw.cpu_util_vfw: + - vfwcds_VF 0 + - abstract_vfw.cpu_util_vfw + vfwcds_vf0.abstract_vfw.network.outpoing.packets_vfw_vfw_int_private1_port_0: + - vfwcds_VF 0 + - abstract_vfw.network.outpoing.packets_vfw_vfw_int_private1_port_0 + vfwcds_vf0.abstract_vsn.endpoint_vsn: + - vfwcds_VF 0 + - abstract_vsn.endpoint_vsn + vfwcds_vf0.abstract_vfw.network.outgoing.bytes_vfw_vfw_int_private2_port_0: + - vfwcds_VF 0 + - abstract_vfw.network.outgoing.bytes_vfw_vfw_int_private2_port_0 + vfwcds_vf0.abstract_vsn.disk.device.write.requests_vsn: + - vfwcds_VF 0 + - abstract_vsn.disk.device.write.requests_vsn + vfwcds_vf0.abstract_vpg.network.incoming.packets.rate_vpg_vpg_onap_private_port_0: + - vfwcds_VF 0 + - abstract_vpg.network.incoming.packets.rate_vpg_vpg_onap_private_port_0 + vfwcds_vf0.abstract_vsn.network.incoming.packets.rate_vsn_vsn_onap_private_port_0: + - vfwcds_VF 0 + - abstract_vsn.network.incoming.packets.rate_vsn_vsn_onap_private_port_0 + vfwcds_vf0.abstract_vpg.network.incoming.bytes_vpg_vpg_onap_private_port_0: + - vfwcds_VF 0 + - abstract_vpg.network.incoming.bytes_vpg_vpg_onap_private_port_0 + vfwcds_vf0.abstract_vfw.network.incoming.bytes_vfw_vfw_onap_private_port_0: + - vfwcds_VF 0 + - abstract_vfw.network.incoming.bytes_vfw_vfw_onap_private_port_0 + vfwcds_vf0.abstract_vsn.forwarder_vsn_vsn_onap_private_port_0: + - vfwcds_VF 0 + - abstract_vsn.forwarder_vsn_vsn_onap_private_port_0 + vfwcds_vf0.abstract_vsn.disk.device.iops_vsn: + - vfwcds_VF 0 + - abstract_vsn.disk.device.iops_vsn + vfwcds_vf0.abstract_vfw.disk.read.requests_vfw: + - vfwcds_VF 0 + - abstract_vfw.disk.read.requests_vfw + vfwcds_vf0.abstract_vpg.network.incoming.packets_vpg_vpg_int_private1_port_0: + - vfwcds_VF 0 + - abstract_vpg.network.incoming.packets_vpg_vpg_int_private1_port_0 + vfwcds_vf0.abstract_vpg.feature_vpg: + - vfwcds_VF 0 + - abstract_vpg.feature_vpg + vfwcds_vf0.abstract_vsn.disk.capacity_vsn: + - vfwcds_VF 0 + - abstract_vsn.disk.capacity_vsn + vfwcds_vf0.abstract_vfw.disk.read.bytes.rate_vfw: + - vfwcds_VF 0 + - abstract_vfw.disk.read.bytes.rate_vfw + vfwcds_vf0.abstract_vsn.network.incoming.packets.rate_vsn_vsn_int_private2_port_0: + - vfwcds_VF 0 + - abstract_vsn.network.incoming.packets.rate_vsn_vsn_int_private2_port_0 + vfwcds_vf0.abstract_vsn.scalable_vsn: + - vfwcds_VF 0 + - abstract_vsn.scalable_vsn + vfwcds_vf0.abstract_vpg.binding_vpg_vpg_int_private1_port_0: + - vfwcds_VF 0 + - abstract_vpg.binding_vpg_vpg_int_private1_port_0 + vfwcds_vf0.abstract_vpg.disk.device.read.bytes_vpg: + - vfwcds_VF 0 + - abstract_vpg.disk.device.read.bytes_vpg + vfwcds_vf0.abstract_vpg.feature_vpg_vpg_int_private1_port_0: + - vfwcds_VF 0 + - abstract_vpg.feature_vpg_vpg_int_private1_port_0 + vfwcds_vf0.abstract_vpg.disk.device.write.bytes_vpg: + - vfwcds_VF 0 + - abstract_vpg.disk.device.write.bytes_vpg + vfwcds_vf0.abstract_vpg.attachment_vpg_vpg_int_private1_port_0: + - vfwcds_VF 0 + - abstract_vpg.attachment_vpg_vpg_int_private1_port_0 + vfwcds_vf0.abstract_vsn.disk.device.write.bytes_vsn: + - vfwcds_VF 0 + - abstract_vsn.disk.device.write.bytes_vsn + vfwcds_vf0.abstract_vpg.disk.device.read.requests_vpg: + - vfwcds_VF 0 + - abstract_vpg.disk.device.read.requests_vpg + vfwcds_vf0.abstract_vsn.feature_vsn_vsn_int_private2_port_0: + - vfwcds_VF 0 + - abstract_vsn.feature_vsn_vsn_int_private2_port_0 + vfwcds_vf0.abstract_vsn.feature_vsn: + - vfwcds_VF 0 + - abstract_vsn.feature_vsn + vfwcds_vf0.abstract_vsn.disk.usage_vsn: + - vfwcds_VF 0 + - abstract_vsn.disk.usage_vsn + vfwcds_vf0.abstract_vfw.network.outgoing.bytes_vfw_vfw_onap_private_port_0: + - vfwcds_VF 0 + - abstract_vfw.network.outgoing.bytes_vfw_vfw_onap_private_port_0 + vfwcds_vf0.abstract_vpg.network.outgoing.packets.rate_vpg_vpg_onap_private_port_0: + - vfwcds_VF 0 + - abstract_vpg.network.outgoing.packets.rate_vpg_vpg_onap_private_port_0 + vfwcds_vf0.abstract_vfw.disk.device.usage_vfw: + - vfwcds_VF 0 + - abstract_vfw.disk.device.usage_vfw + vfwcds_vf0.int_private2_network.feature: + - vfwcds_VF 0 + - int_private2_network.feature + vfwcds_vf0.abstract_vfw.network.incoming.bytes.rate_vfw_vfw_int_private2_port_0: + - vfwcds_VF 0 + - abstract_vfw.network.incoming.bytes.rate_vfw_vfw_int_private2_port_0 + vfwcds_vf0.abstract_vfw.network.incoming.bytes_vfw_vfw_int_private2_port_0: + - vfwcds_VF 0 + - abstract_vfw.network.incoming.bytes_vfw_vfw_int_private2_port_0 + vfwcds_vf0.abstract_vpg.host_vpg: + - vfwcds_VF 0 + - abstract_vpg.host_vpg + vfwcds_vf0.abstract_vfw.disk.device.read.requests.rate_vfw: + - vfwcds_VF 0 + - abstract_vfw.disk.device.read.requests.rate_vfw + vfwcds_vf0.abstract_vfw.network.incoming.bytes.rate_vfw_vfw_onap_private_port_0: + - vfwcds_VF 0 + - abstract_vfw.network.incoming.bytes.rate_vfw_vfw_onap_private_port_0 + vfwcds_vf0.abstract_vsn.disk.iops_vsn: + - vfwcds_VF 0 + - abstract_vsn.disk.iops_vsn + vfwcds_vf0.abstract_vfw.disk.write.bytes_vfw: + - vfwcds_VF 0 + - abstract_vfw.disk.write.bytes_vfw + vfwcds_vf0.abstract_vfw.attachment_vfw_vfw_int_private1_port_0: + - vfwcds_VF 0 + - abstract_vfw.attachment_vfw_vfw_int_private1_port_0 + vfwcds_vf0.abstract_vfw.network.incoming.packets_vfw_vfw_int_private1_port_0: + - vfwcds_VF 0 + - abstract_vfw.network.incoming.packets_vfw_vfw_int_private1_port_0 + vfwcds_vf0.abstract_vfw.cpu_vfw: + - vfwcds_VF 0 + - abstract_vfw.cpu_vfw + vfwcds_vf0.abstract_vpg.disk.device.read.requests.rate_vpg: + - vfwcds_VF 0 + - abstract_vpg.disk.device.read.requests.rate_vpg + vfwcds_vf0.int_private1_network.feature: + - vfwcds_VF 0 + - int_private1_network.feature + vfwcds_vf0.abstract_vpg.disk.iops_vpg: + - vfwcds_VF 0 + - abstract_vpg.disk.iops_vpg + vfwcds_vf0.abstract_vfw.disk.device.latency_vfw: + - vfwcds_VF 0 + - abstract_vfw.disk.device.latency_vfw + vfwcds_vf0.abstract_vpg.disk.latency_vpg: + - vfwcds_VF 0 + - abstract_vpg.disk.latency_vpg + vfwcds_vf0.abstract_vfw.feature_vfw_vfw_int_private2_port_0: + - vfwcds_VF 0 + - abstract_vfw.feature_vfw_vfw_int_private2_port_0 + vfwcds_vf0.abstract_vfw.forwarder_vfw_vfw_int_private2_port_0: + - vfwcds_VF 0 + - abstract_vfw.forwarder_vfw_vfw_int_private2_port_0 + vfwcds_vf0.abstract_vfw.instance_vfw: + - vfwcds_VF 0 + - abstract_vfw.instance_vfw + vfwcds_vf0.abstract_vfw.disk.capacity_vfw: + - vfwcds_VF 0 + - abstract_vfw.disk.capacity_vfw + vfwcds_vf0.abstract_vpg.network.incoming.bytes.rate_vpg_vpg_int_private1_port_0: + - vfwcds_VF 0 + - abstract_vpg.network.incoming.bytes.rate_vpg_vpg_int_private1_port_0 + vfwcds_vf0.abstract_vpg.disk.device.write.requests_vpg: + - vfwcds_VF 0 + - abstract_vpg.disk.device.write.requests_vpg + vfwcds_vf0.abstract_vsn.network.outgoing.packets.rate_vsn_vsn_onap_private_port_0: + - vfwcds_VF 0 + - abstract_vsn.network.outgoing.packets.rate_vsn_vsn_onap_private_port_0 + vfwcds_vf0.abstract_vfw.disk.write.bytes.rate_vfw: + - vfwcds_VF 0 + - abstract_vfw.disk.write.bytes.rate_vfw + vfwcds_vf0.abstract_vfw.network.incoming.packets_vfw_vfw_onap_private_port_0: + - vfwcds_VF 0 + - abstract_vfw.network.incoming.packets_vfw_vfw_onap_private_port_0 + vfwcds_vf0.abstract_vsn.disk.read.bytes.rate_vsn: + - vfwcds_VF 0 + - abstract_vsn.disk.read.bytes.rate_vsn + vfwcds_vf0.abstract_vsn.memory.resident_vsn: + - vfwcds_VF 0 + - abstract_vsn.memory.resident_vsn + vfwcds_vf0.abstract_vpg.disk.write.requests_vpg: + - vfwcds_VF 0 + - abstract_vpg.disk.write.requests_vpg + vfwcds_vf0.int_private1_network.end_point: + - vfwcds_VF 0 + - int_private1_network.end_point + vfwcds_vf0.abstract_vfw.memory_vfw: + - vfwcds_VF 0 + - abstract_vfw.memory_vfw + vfwcds_vf0.abstract_vfw.attachment_vfw_vfw_onap_private_port_0: + - vfwcds_VF 0 + - abstract_vfw.attachment_vfw_vfw_onap_private_port_0 + vfwcds_vf0.abstract_vpg.disk.write.bytes_vpg: + - vfwcds_VF 0 + - abstract_vpg.disk.write.bytes_vpg + vfwcds_vf0.abstract_vpg.cpu_vpg: + - vfwcds_VF 0 + - abstract_vpg.cpu_vpg + vfwcds_vf0.abstract_vpg.network.incoming.bytes.rate_vpg_vpg_onap_private_port_0: + - vfwcds_VF 0 + - abstract_vpg.network.incoming.bytes.rate_vpg_vpg_onap_private_port_0 + vfwcds_vf0.abstract_vfw.network.outgoing.bytes_vfw_vfw_int_private1_port_0: + - vfwcds_VF 0 + - abstract_vfw.network.outgoing.bytes_vfw_vfw_int_private1_port_0 + vfwcds_vf0.abstract_vfw.memory.usage_vfw: + - vfwcds_VF 0 + - abstract_vfw.memory.usage_vfw + vfwcds_vf0.abstract_vpg.attachment_vpg_vpg_onap_private_port_0: + - vfwcds_VF 0 + - abstract_vpg.attachment_vpg_vpg_onap_private_port_0 + vfwcds_vf0.abstract_vsn.disk.read.bytes_vsn: + - vfwcds_VF 0 + - abstract_vsn.disk.read.bytes_vsn + vfwcds_vf0.abstract_vsn.disk.write.requests_vsn: + - vfwcds_VF 0 + - abstract_vsn.disk.write.requests_vsn + vfwcds_vf0.int_private2_network.end_point: + - vfwcds_VF 0 + - int_private2_network.end_point + vfwcds_vf0.abstract_vsn.disk.device.read.requests_vsn: + - vfwcds_VF 0 + - abstract_vsn.disk.device.read.requests_vsn + vfwcds_vf0.abstract_vfw.endpoint_vfw: + - vfwcds_VF 0 + - abstract_vfw.endpoint_vfw + vfwcds_vf0.abstract_vpg.network.outgoing.bytes_vpg_vpg_onap_private_port_0: + - vfwcds_VF 0 + - abstract_vpg.network.outgoing.bytes_vpg_vpg_onap_private_port_0 + vfwcds_vf0.abstract_vpg.network.incoming.packets.rate_vpg_vpg_int_private1_port_0: + - vfwcds_VF 0 + - abstract_vpg.network.incoming.packets.rate_vpg_vpg_int_private1_port_0 + vfwcds_vf0.abstract_vpg.network.outgoing.bytes_vpg_vpg_int_private1_port_0: + - vfwcds_VF 0 + - abstract_vpg.network.outgoing.bytes_vpg_vpg_int_private1_port_0 + vfwcds_vf0.abstract_vsn.network.outpoing.packets_vsn_vsn_int_private2_port_0: + - vfwcds_VF 0 + - abstract_vsn.network.outpoing.packets_vsn_vsn_int_private2_port_0 + vfwcds_vf0.abstract_vpg.binding_vpg: + - vfwcds_VF 0 + - abstract_vpg.binding_vpg + vfwcds_vf0.abstract_vpg.disk.usage_vpg: + - vfwcds_VF 0 + - abstract_vpg.disk.usage_vpg + vfwcds_vf0.abstract_vfw.disk.write.requests.rate_vfw: + - vfwcds_VF 0 + - abstract_vfw.disk.write.requests.rate_vfw + vfwcds_vf0.abstract_vsn.binding_vsn: + - vfwcds_VF 0 + - abstract_vsn.binding_vsn + vfwcds_vf0.abstract_vpg.disk.write.requests.rate_vpg: + - vfwcds_VF 0 + - abstract_vpg.disk.write.requests.rate_vpg + vfwcds_vf0.abstract_vsn.disk.latency_vsn: + - vfwcds_VF 0 + - abstract_vsn.disk.latency_vsn + vfwcds_vf0.abstract_vpg.memory_vpg: + - vfwcds_VF 0 + - abstract_vpg.memory_vpg + vfwcds_vf0.abstract_vsn.network.outgoing.bytes_vsn_vsn_int_private2_port_0: + - vfwcds_VF 0 + - abstract_vsn.network.outgoing.bytes_vsn_vsn_int_private2_port_0 + vfwcds_vf0.abstract_vfw.network.outpoing.packets_vfw_vfw_onap_private_port_0: + - vfwcds_VF 0 + - abstract_vfw.network.outpoing.packets_vfw_vfw_onap_private_port_0 + vfwcds_vf0.abstract_vfw.disk.device.read.bytes.rate_vfw: + - vfwcds_VF 0 + - abstract_vfw.disk.device.read.bytes.rate_vfw + vfwcds_vf0.abstract_vsn.disk.ephemeral.size_vsn: + - vfwcds_VF 0 + - abstract_vsn.disk.ephemeral.size_vsn + vfwcds_vf0.abstract_vfw.binding_vfw_vfw_int_private2_port_0: + - vfwcds_VF 0 + - abstract_vfw.binding_vfw_vfw_int_private2_port_0 + vfwcds_vf0.abstract_vpg.feature_vpg_vpg_onap_private_port_0: + - vfwcds_VF 0 + - abstract_vpg.feature_vpg_vpg_onap_private_port_0 + vfwcds_vf0.abstract_vfw.network.outgoing.bytes.rate_vfw_vfw_onap_private_port_0: + - vfwcds_VF 0 + - abstract_vfw.network.outgoing.bytes.rate_vfw_vfw_onap_private_port_0 + vfwcds_vf0.abstract_vpg.binding_vpg_vpg_onap_private_port_0: + - vfwcds_VF 0 + - abstract_vpg.binding_vpg_vpg_onap_private_port_0 + vfwcds_vf0.abstract_vfw.disk.device.allocation_vfw: + - vfwcds_VF 0 + - abstract_vfw.disk.device.allocation_vfw + vfwcds_vf0.abstract_vpg.scalable_vpg: + - vfwcds_VF 0 + - abstract_vpg.scalable_vpg + vfwcds_vf0.abstract_vsn.attachment_vsn_vsn_int_private2_port_0: + - vfwcds_VF 0 + - abstract_vsn.attachment_vsn_vsn_int_private2_port_0 + vfwcds_vf0.abstract_vpg.network.outpoing.packets_vpg_vpg_onap_private_port_0: + - vfwcds_VF 0 + - abstract_vpg.network.outpoing.packets_vpg_vpg_onap_private_port_0 + vfwcds_vf0.abstract_vsn.memory.usage_vsn: + - vfwcds_VF 0 + - abstract_vsn.memory.usage_vsn + vfwcds_vf0.abstract_vsn.network.incoming.bytes.rate_vsn_vsn_onap_private_port_0: + - vfwcds_VF 0 + - abstract_vsn.network.incoming.bytes.rate_vsn_vsn_onap_private_port_0 + vfwcds_vf0.abstract_vsn.memory_vsn: + - vfwcds_VF 0 + - abstract_vsn.memory_vsn + vfwcds_vf0.abstract_vpg.disk.device.iops_vpg: + - vfwcds_VF 0 + - abstract_vpg.disk.device.iops_vpg + vfwcds_vf0.int_private1_network.attachment: + - vfwcds_VF 0 + - int_private1_network.attachment + vfwcds_vf0.abstract_vfw.disk.ephemeral.size_vfw: + - vfwcds_VF 0 + - abstract_vfw.disk.ephemeral.size_vfw + vfwcds_vf0.abstract_vsn.network.incoming.bytes_vsn_vsn_int_private2_port_0: + - vfwcds_VF 0 + - abstract_vsn.network.incoming.bytes_vsn_vsn_int_private2_port_0 + vfwcds_vf0.abstract_vsn.attachment_vsn_vsn_onap_private_port_0: + - vfwcds_VF 0 + - abstract_vsn.attachment_vsn_vsn_onap_private_port_0 + vfwcds_vf0.abstract_vfw.disk.device.write.requests.rate_vfw: + - vfwcds_VF 0 + - abstract_vfw.disk.device.write.requests.rate_vfw + vfwcds_vf0.abstract_vsn.disk.allocation_vsn: + - vfwcds_VF 0 + - abstract_vsn.disk.allocation_vsn + vfwcds_vf0.abstract_vfw.network.incoming.bytes_vfw_vfw_int_private1_port_0: + - vfwcds_VF 0 + - abstract_vfw.network.incoming.bytes_vfw_vfw_int_private1_port_0 + vfwcds_vf0.abstract_vfw.network.outgoing.packets.rate_vfw_vfw_int_private2_port_0: + - vfwcds_VF 0 + - abstract_vfw.network.outgoing.packets.rate_vfw_vfw_int_private2_port_0 + vfwcds_vf0.abstract_vsn.network.incoming.bytes_vsn_vsn_onap_private_port_0: + - vfwcds_VF 0 + - abstract_vsn.network.incoming.bytes_vsn_vsn_onap_private_port_0 + vfwcds_vf0.abstract_vpg.cpu_util_vpg: + - vfwcds_VF 0 + - abstract_vpg.cpu_util_vpg + vfwcds_vf0.abstract_vpg.disk.device.usage_vpg: + - vfwcds_VF 0 + - abstract_vpg.disk.device.usage_vpg + vfwcds_vf0.abstract_vfw.disk.usage_vfw: + - vfwcds_VF 0 + - abstract_vfw.disk.usage_vfw + vfwcds_vf0.abstract_vsn.disk.device.write.bytes.rate_vsn: + - vfwcds_VF 0 + - abstract_vsn.disk.device.write.bytes.rate_vsn + vfwcds_vf0.abstract_vpg.network.outgoing.packets.rate_vpg_vpg_int_private1_port_0: + - vfwcds_VF 0 + - abstract_vpg.network.outgoing.packets.rate_vpg_vpg_int_private1_port_0 + vfwcds_vf0.abstract_vfw.disk.allocation_vfw: + - vfwcds_VF 0 + - abstract_vfw.disk.allocation_vfw + vfwcds_vf0.abstract_vfw.network.incoming.packets.rate_vfw_vfw_int_private2_port_0: + - vfwcds_VF 0 + - abstract_vfw.network.incoming.packets.rate_vfw_vfw_int_private2_port_0 + vfwcds_vf0.abstract_vfw.network.incoming.bytes.rate_vfw_vfw_int_private1_port_0: + - vfwcds_VF 0 + - abstract_vfw.network.incoming.bytes.rate_vfw_vfw_int_private1_port_0 + vfwcds_vf0.abstract_vsn.disk.read.requests_vsn: + - vfwcds_VF 0 + - abstract_vsn.disk.read.requests_vsn + vfwcds_vf0.abstract_vsn.network.incoming.bytes.rate_vsn_vsn_int_private2_port_0: + - vfwcds_VF 0 + - abstract_vsn.network.incoming.bytes.rate_vsn_vsn_int_private2_port_0 + vfwcds_vf0.abstract_vfw.host_vfw: + - vfwcds_VF 0 + - abstract_vfw.host_vfw + vfwcds_vf0.abstract_vsn.network.incoming.packets_vsn_vsn_int_private2_port_0: + - vfwcds_VF 0 + - abstract_vsn.network.incoming.packets_vsn_vsn_int_private2_port_0 + vfwcds_vf0.abstract_vpg.disk.read.requests_vpg: + - vfwcds_VF 0 + - abstract_vpg.disk.read.requests_vpg + vfwcds_vf0.abstract_vsn.disk.device.read.requests.rate_vsn: + - vfwcds_VF 0 + - abstract_vsn.disk.device.read.requests.rate_vsn + vfwcds_vf0.abstract_vfw.port_mirroring_vfw_vfw_onap_private_port_0: + - vfwcds_VF 0 + - abstract_vfw.port_mirroring_vfw_vfw_onap_private_port_0 + vfwcds_vf0.abstract_vfw.disk.device.write.bytes_vfw: + - vfwcds_VF 0 + - abstract_vfw.disk.device.write.bytes_vfw + vfwcds_vf0.int_private2_network.attachment: + - vfwcds_VF 0 + - int_private2_network.attachment + vfwcds_vf0.abstract_vfw.network.outgoing.packets.rate_vfw_vfw_onap_private_port_0: + - vfwcds_VF 0 + - abstract_vfw.network.outgoing.packets.rate_vfw_vfw_onap_private_port_0 + vfwcds_vf0.abstract_vfw.vcpus_vfw: + - vfwcds_VF 0 + - abstract_vfw.vcpus_vfw + vfwcds_vf0.abstract_vpg.disk.read.bytes_vpg: + - vfwcds_VF 0 + - abstract_vpg.disk.read.bytes_vpg + vfwcds_vf0.abstract_vpg.disk.ephemeral.size_vpg: + - vfwcds_VF 0 + - abstract_vpg.disk.ephemeral.size_vpg + vfwcds_vf0.abstract_vsn.network.incoming.packets_vsn_vsn_onap_private_port_0: + - vfwcds_VF 0 + - abstract_vsn.network.incoming.packets_vsn_vsn_onap_private_port_0 + vfwcds_vf0.abstract_vpg.endpoint_vpg: + - vfwcds_VF 0 + - abstract_vpg.endpoint_vpg + vfwcds_vf0.abstract_vpg.forwarder_vpg_vpg_onap_private_port_0: + - vfwcds_VF 0 + - abstract_vpg.forwarder_vpg_vpg_onap_private_port_0 + vfwcds_vf0.abstract_vfw.disk.iops_vfw: + - vfwcds_VF 0 + - abstract_vfw.disk.iops_vfw + vfwcds_vf0.abstract_vpg.cpu.delta_vpg: + - vfwcds_VF 0 + - abstract_vpg.cpu.delta_vpg + vfwcds_vf0.abstract_vpg.disk.device.capacity_vpg: + - vfwcds_VF 0 + - abstract_vpg.disk.device.capacity_vpg + vfwcds_vf0.abstract_vfw.network.outgoing.bytes.rate_vfw_vfw_int_private2_port_0: + - vfwcds_VF 0 + - abstract_vfw.network.outgoing.bytes.rate_vfw_vfw_int_private2_port_0 + vfwcds_vf0.abstract_vsn.cpu_vsn: + - vfwcds_VF 0 + - abstract_vsn.cpu_vsn + vfwcds_vf0.abstract_vpg.disk.device.allocation_vpg: + - vfwcds_VF 0 + - abstract_vpg.disk.device.allocation_vpg + vfwcds_vf0.abstract_vsn.disk.write.bytes_vsn: + - vfwcds_VF 0 + - abstract_vsn.disk.write.bytes_vsn + vfwcds_vf0.abstract_vsn.disk.write.bytes.rate_vsn: + - vfwcds_VF 0 + - abstract_vsn.disk.write.bytes.rate_vsn + vfwcds_vf0.abstract_vfw.disk.device.write.requests_vfw: + - vfwcds_VF 0 + - abstract_vfw.disk.device.write.requests_vfw + vfwcds_vf0.abstract_vsn.port_mirroring_vsn_vsn_onap_private_port_0: + - vfwcds_VF 0 + - abstract_vsn.port_mirroring_vsn_vsn_onap_private_port_0 + vfwcds_vf0.abstract_vfw.disk.write.requests_vfw: + - vfwcds_VF 0 + - abstract_vfw.disk.write.requests_vfw + requirements: + vfwcds_vf0.abstract_vpg.dependency_vpg: + - vfwcds_VF 0 + - abstract_vpg.dependency_vpg + vfwcds_vf0.abstract_vpg.link_vpg_vpg_onap_private_port_0: + - vfwcds_VF 0 + - abstract_vpg.link_vpg_vpg_onap_private_port_0 + vfwcds_vf0.abstract_vsn.local_storage_vsn: + - vfwcds_VF 0 + - abstract_vsn.local_storage_vsn + vfwcds_vf0.abstract_vfw.dependency_vfw: + - vfwcds_VF 0 + - abstract_vfw.dependency_vfw + vfwcds_vf0.abstract_vfw.local_storage_vfw: + - vfwcds_VF 0 + - abstract_vfw.local_storage_vfw + vfwcds_vf0.int_private2_network.dependency: + - vfwcds_VF 0 + - int_private2_network.dependency + vfwcds_vf0.int_private1_network.dependency: + - vfwcds_VF 0 + - int_private1_network.dependency + vfwcds_vf0.abstract_vfw.dependency_vfw_vfw_int_private1_port_0: + - vfwcds_VF 0 + - abstract_vfw.dependency_vfw_vfw_int_private1_port_0 + vfwcds_vf0.abstract_vpg.dependency_vpg_vpg_int_private1_port_0: + - vfwcds_VF 0 + - abstract_vpg.dependency_vpg_vpg_int_private1_port_0 + vfwcds_vf0.abstract_vfw.dependency_vfw_vfw_int_private2_port_0: + - vfwcds_VF 0 + - abstract_vfw.dependency_vfw_vfw_int_private2_port_0 + vfwcds_vf0.abstract_vpg.dependency_vpg_vpg_onap_private_port_0: + - vfwcds_VF 0 + - abstract_vpg.dependency_vpg_vpg_onap_private_port_0 + vfwcds_vf0.abstract_vsn.link_vsn_vsn_onap_private_port_0: + - vfwcds_VF 0 + - abstract_vsn.link_vsn_vsn_onap_private_port_0 + vfwcds_vf0.abstract_vpg.link_vpg_vpg_int_private1_port_0: + - vfwcds_VF 0 + - abstract_vpg.link_vpg_vpg_int_private1_port_0 + vfwcds_vf0.abstract_vfw.link_vfw_vfw_int_private1_port_0: + - vfwcds_VF 0 + - abstract_vfw.link_vfw_vfw_int_private1_port_0 + vfwcds_vf0.abstract_vpg.local_storage_vpg: + - vfwcds_VF 0 + - abstract_vpg.local_storage_vpg + vfwcds_vf0.abstract_vfw.link_vfw_vfw_int_private2_port_0: + - vfwcds_VF 0 + - abstract_vfw.link_vfw_vfw_int_private2_port_0 + vfwcds_vf0.abstract_vsn.dependency_vsn_vsn_int_private2_port_0: + - vfwcds_VF 0 + - abstract_vsn.dependency_vsn_vsn_int_private2_port_0 + vfwcds_vf0.abstract_vsn.link_vsn_vsn_int_private2_port_0: + - vfwcds_VF 0 + - abstract_vsn.link_vsn_vsn_int_private2_port_0 + vfwcds_vf0.abstract_vsn.dependency_vsn: + - vfwcds_VF 0 + - abstract_vsn.dependency_vsn + vfwcds_vf0.abstract_vfw.link_vfw_vfw_onap_private_port_0: + - vfwcds_VF 0 + - abstract_vfw.link_vfw_vfw_onap_private_port_0 + vfwcds_vf0.abstract_vfw.dependency_vfw_vfw_onap_private_port_0: + - vfwcds_VF 0 + - abstract_vfw.dependency_vfw_vfw_onap_private_port_0 + vfwcds_vf0.abstract_vsn.dependency_vsn_vsn_onap_private_port_0: + - vfwcds_VF 0 + - abstract_vsn.dependency_vsn_vsn_onap_private_port_0 diff --git a/tests/data/test.csar b/tests/data/test.csar Binary files differnew file mode 100644 index 0000000..adedc40 --- /dev/null +++ b/tests/data/test.csar diff --git a/tests/data/test_so_service_data.yaml b/tests/data/test_so_service_data.yaml new file mode 100644 index 0000000..594ff17 --- /dev/null +++ b/tests/data/test_so_service_data.yaml @@ -0,0 +1,35 @@ +myservice: + subscription_service_type: myservice + vnfs: + - model_name: myvfmodel + instance_name: myfirstvnf + parameters: + param1: value1 + processing_priority: 1 + vf_modules: + - instance_name: mysecondvfm + model_name: base + processing_priority: 2 + parameters: + param-vfm1: value-vfm1 + - instance_name: myfirstvfm + model_name: base + processing_priority: 1 + parameters: + param-vfm1: value-vfm1 + - model_name: myvfmodel + instance_name: mysecondvnf + parameters: + param1: value1 + processing_priority: 2 + vf_modules: + - instance_name: myfirstvfm + model_name: base + processing_priority: 1 + parameters: + param-vfm1: value-vfm1 + - instance_name: mysecondvfm + model_name: base + processing_priority: 2 + parameters: + param-vfm1: value-vfm1
\ No newline at end of file diff --git a/tests/data/tests_settings.py b/tests/data/tests_settings.py new file mode 100644 index 0000000..09437b6 --- /dev/null +++ b/tests/data/tests_settings.py @@ -0,0 +1,15 @@ +# Copyright 2022 Orange, 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. +AAI_URL = "http://tests.settings.py:1234" +TEST_VALUE = "test" diff --git a/tests/data/utils_load_json_file_test.json b/tests/data/utils_load_json_file_test.json new file mode 100644 index 0000000..3b4000d --- /dev/null +++ b/tests/data/utils_load_json_file_test.json @@ -0,0 +1 @@ +{"event":{"test1":"val1"}} diff --git a/tests/data/vLB_CBA_Python.zip b/tests/data/vLB_CBA_Python.zip Binary files differnew file mode 100755 index 0000000..ddd41ac --- /dev/null +++ b/tests/data/vLB_CBA_Python.zip diff --git a/tests/test_aai_bulk.py b/tests/test_aai_bulk.py new file mode 100644 index 0000000..0567055 --- /dev/null +++ b/tests/test_aai_bulk.py @@ -0,0 +1,90 @@ +# Copyright 2022 Orange, 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 unittest import mock + +from onapsdk.aai.bulk import AaiBulk, AaiBulkRequest, AaiBulkResponse + + +BULK_RESPONSES = { + "operation-responses": [ + { + "action": "put", + "uri": "test-uri", + "response-status-code": 400, + "response-body": None + }, + { + "action": "post", + "uri": "test-uri", + "response-status-code": 201, + "response-body": "blabla" + } + ] +} + + +@mock.patch("onapsdk.aai.bulk.AaiBulk.send_message_json") +def test_aai_bulk(mock_send_message_json): + assert AaiBulk().url.endswith("bulk") + mock_send_message_json.return_value = BULK_RESPONSES + responses = list(AaiBulk.single_transaction( + [ + AaiBulkRequest( + action="post", + uri="test-uri", + body={"blabla: blabla"} + ), + AaiBulkRequest( + action="get", + uri="test-uri", + body={} + ) + ] + )) + assert len(responses) == 2 + resp_1, resp_2 = responses + assert resp_1.action == "put" + assert resp_1.uri == "test-uri" + assert resp_1.status_code == 400 + assert resp_1.body is None + assert resp_2.action == "post" + assert resp_2.uri == "test-uri" + assert resp_2.status_code == 201 + assert resp_2.body == "blabla" + + # Check if requests was splitted into chunks for generator + mock_send_message_json.reset_mock() + responses = list(AaiBulk.single_transaction( + ( + AaiBulkRequest( + action="post", + uri=f"test-uri-{i}", + body={"blabla: blabla"} + ) for i in range(31) + ) + )) + assert mock_send_message_json.call_count == 2 + + # Check if requests was splitted into chunks for list + mock_send_message_json.reset_mock() + responses = list(AaiBulk.single_transaction( + [ + AaiBulkRequest( + action="post", + uri=f"test-uri-{i}", + body={"blabla: blabla"} + ) for i in range(31) + ] + )) + assert mock_send_message_json.call_count == 2 diff --git a/tests/test_aai_cloud_region.py b/tests/test_aai_cloud_region.py new file mode 100644 index 0000000..a821090 --- /dev/null +++ b/tests/test_aai_cloud_region.py @@ -0,0 +1,71 @@ +# Copyright 2022 Orange, 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 unittest import mock + +from onapsdk.aai.cloud_infrastructure.cloud_region import CloudRegion, Tenant +from onapsdk.exceptions import ResourceNotFound + + +COUNT = { + "results":[ + { + "cloud-region":2 + } + ] +} + + +@mock.patch("onapsdk.aai.cloud_infrastructure.cloud_region.AaiResource.relationships", new_callable=mock.PropertyMock) +@mock.patch("onapsdk.aai.cloud_infrastructure.cloud_region.Complex.get_by_physical_location_id") +def test_cloud_region_complex_property(mock_complex_get, mock_relationships): + cr = CloudRegion("test_cloud_owner", "test_cloud_region_id", False, False) + + mock_relationships.return_value = [] + assert cr.complex is None + + mock_relationships.return_value = [mock.MagicMock()] + assert cr.complex is None + + relationship_mock = mock.MagicMock() + relationship_mock.related_to = "complex" + relationship_mock.get_relationship_data.return_value = None + mock_relationships.return_value = [relationship_mock] + assert cr.complex is None + + relationship_mock.get_relationship_data.return_value = "123" + mock_complex_get.side_effect = ResourceNotFound + assert cr.complex is None + + mock_complex_get.side_effect = None + mock_complex_get.return_value = mock.MagicMock() + assert cr.complex is not None + + mock_relationships.side_effect = ResourceNotFound + assert cr.complex is None + +@mock.patch("onapsdk.aai.cloud_infrastructure.cloud_region.CloudRegion.tenants", new_callable=mock.PropertyMock) +def test_cloud_region_get_tenants_by_name(mock_tenants): + cr = CloudRegion("test_cloud_owner", "test_cloud_region_id", False, False) + mock_tenants.return_value = iter([ + Tenant(cloud_region="test_cloud_region_id",tenant_id="test-tenant",tenant_name="test-tenant") + ]) + tenants = list(cr.get_tenants_by_name("test-tenant")) + assert len(tenants) == 1 + assert isinstance(tenants[0], Tenant) + assert tenants[0].name == "test-tenant" + +@mock.patch("onapsdk.aai.cloud_infrastructure.cloud_region.CloudRegion.send_message_json") +def test_cloud_region_count(mock_send_message_json): + mock_send_message_json.return_value = COUNT + assert CloudRegion.count() == 2 diff --git a/tests/test_aai_complex.py b/tests/test_aai_complex.py new file mode 100644 index 0000000..13d2cf5 --- /dev/null +++ b/tests/test_aai_complex.py @@ -0,0 +1,138 @@ +# Copyright 2022 Orange, 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 unittest import mock + +from onapsdk.aai.cloud_infrastructure import Complex +from onapsdk.aai.cloud_infrastructure import CloudRegion + + +COMPLEXES = { + "complex":[ + { + "physical-location-id":"integration_test_complex", + "data-center-code":"1234", + "complex-name":"integration_test_complex", + "identity-url":"", + "resource-version":"1588244056133", + "physical-location-type":"", + "street1":"", + "street2":"", + "city":"", + "state":"", + "postal-code":"", + "country":"", + "region":"", + "latitude":"", + "longitude":"", + "elevation":"", + "lata":"", + "time-zone":"", + "data-owner":"", + "data-source":"", + "data-source-version":"" + } + ] +} + + +COMPLEXES_COUNT = { + "results":[ + { + "complex":12 + } + ] +} + + +@mock.patch.object(Complex, "send_message") +def test_complex(mock_send_message): + cmplx = Complex(name="test_complex_name", + physical_location_id="test_location_id", + resource_version="1234") + assert cmplx.name == "test_complex_name" + assert cmplx.physical_location_id == "test_location_id" + assert cmplx.url == (f"{Complex.base_url}{Complex.api_version}/cloud-infrastructure/" + "complexes/complex/test_location_id") + + cmplx2 = Complex.create(name="test_complex_name", + physical_location_id="test_location_id") + mock_send_message.assert_called_once() + assert cmplx2.name == "test_complex_name" + assert cmplx2.physical_location_id == "test_location_id" + assert cmplx2.url == (f"{Complex.base_url}{Complex.api_version}/cloud-infrastructure/" + "complexes/complex/test_location_id") + method, _, url = mock_send_message.call_args[0] + assert method == "PUT" + assert url == (f"{Complex.base_url}{Complex.api_version}/cloud-infrastructure/" + "complexes/complex/test_location_id") + + +@mock.patch.object(Complex, "send_message_json") +def test_complex_get_all(mock_send_message_json): + mock_send_message_json.return_value = COMPLEXES + complexes = list(Complex.get_all()) + assert len(complexes) == 1 + cmplx = complexes[0] + assert cmplx.name == "integration_test_complex" + assert cmplx.physical_location_id == "integration_test_complex" + + +@mock.patch.object(CloudRegion, "add_relationship") +def test_cloud_region_link_to_complex(mock_add_rel): + """Test Cloud Region linking with Complex. + + Test Relationship object creation + """ + cloud_region = CloudRegion(cloud_owner="test_cloud_owner", + cloud_region_id="test_cloud_region", + orchestration_disabled=True, + in_maint=False) + cmplx = Complex(name="test_complex_name", + physical_location_id="test_location_id", + resource_version="1234") + cloud_region.link_to_complex(cmplx) + mock_add_rel.assert_called_once() + relationship = mock_add_rel.call_args[0][0] + assert relationship.related_to == "complex" + assert relationship.related_link == (f"aai/v13/cloud-infrastructure/complexes/" + f"complex/test_location_id") + assert len(relationship.relationship_data) == 2 + + +@mock.patch.object(Complex, "send_message_json") +def test_complex_get_by_physical_location_id(mock_send_message_json): + """Test complex get_by_physical_location_id url creation.""" + Complex.get_by_physical_location_id("test") + assert mock_send_message_json.called_once_with( + "GET", + "Get complex with physical location id: test", + f"{Complex.base_url}{Complex.api_version}/cloud-infrastructure/" + f"complexes/complex/test" + ) + +@mock.patch.object(Complex, "send_message") +def test_complex_delete(mock_send_message): + cmplx = Complex(physical_location_id="test_location_id", + resource_version="1234") + cmplx.delete() + mock_send_message.assert_called_once_with( + "DELETE", + "Delete test_location_id complex", + f"{cmplx.url}?resource-version={cmplx.resource_version}" + ) + +@mock.patch.object(Complex, "send_message_json") +def test_complex_count(mock_send_message_json): + mock_send_message_json.return_value = COMPLEXES_COUNT + assert Complex.count() == 12 diff --git a/tests/test_aai_customer.py b/tests/test_aai_customer.py new file mode 100644 index 0000000..7e427bd --- /dev/null +++ b/tests/test_aai_customer.py @@ -0,0 +1,580 @@ +"""Test A&AI Customer module.""" +# Copyright 2022 Orange, 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 unittest import mock + +import pytest + +from onapsdk.aai.business import Customer, ServiceSubscription +from onapsdk.aai.cloud_infrastructure import CloudRegion, Tenant +from onapsdk.msb.multicloud import Multicloud +from onapsdk.sdc.service import Service as SdcService +from onapsdk.exceptions import ParameterError, ResourceNotFound + + +SIMPLE_CUSTOMER = { + "customer": [ + { + "global-customer-id": "generic", + "subscriber-name": "generic", + "subscriber-type": "INFRA", + "resource-version": "1561218640404", + } + ] +} + + +SERVICE_SUBSCRIPTION = { + "service-subscription": [ + { + "service-type": "freeradius", + "resource-version": "1562591478146", + "relationship-list": { + "relationship": [ + { + "related-to": "tenant", + "relationship-label": "org.onap.relationships.inventory.Uses", + "related-link": "/aai/v16/cloud-infrastructure/cloud-regions/cloud-region/OPNFV/RegionOne/tenants/tenant/4bdc6f0f2539430f9428c852ba606808", + "relationship-data": [ + { + "relationship-key": "cloud-region.cloud-owner", + "relationship-value": "OPNFV", + }, + { + "relationship-key": "cloud-region.cloud-region-id", + "relationship-value": "RegionOne", + }, + { + "relationship-key": "tenant.tenant-id", + "relationship-value": "4bdc6f0f2539430f9428c852ba606808", + }, + ], + "related-to-property": [ + { + "property-key": "tenant.tenant-name", + "property-value": "onap-dublin-daily-vnfs", + } + ], + } + ] + }, + }, + {"service-type": "ims"}, + ] +} + + +CUSTOMERS = { + "customer": [ + { + "subscriber-name": "generic", + "subscriber-type": "INFRA", + "global-customer-id": "generic", + "resource-version": "1581510772967", + } + ] +} + + +SIMPLE_CUSTOMER_2 = { + "global-customer-id": "generic", + "subscriber-name": "generic", + "subscriber-type": "INFRA", + "resource-version": "1561218640404", +} + + +SERVICE_INSTANCES = { + "service-instance":[ + { + "service-instance-id":"5410bf79-2aa3-450e-a324-ec5630dc18cf", + "service-instance-name":"test", + "environment-context":"General_Revenue-Bearing", + "workload-context":"Production", + "model-invariant-id":"2a51a89b-6f94-4417-8831-c468fb30ed02", + "model-version-id":"92a82807-b483-4579-86b1-c79b1286aab4", + "resource-version":"1589457727708", + "orchestration-status":"Active", + "relationship-list":{ + "relationship":[ + { + "related-to":"owning-entity", + "relationship-label":"org.onap.relationships.inventory.BelongsTo", + "related-link":"/aai/v16/business/owning-entities/owning-entity/ff6c945f-89ab-4f14-bafd-0cdd6eac791a", + "relationship-data":[ + { + "relationship-key":"owning-entity.owning-entity-id", + "relationship-value":"ff6c945f-89ab-4f14-bafd-0cdd6eac791a" + } + ] + }, + { + "related-to":"project", + "relationship-label":"org.onap.relationships.inventory.Uses", + "related-link":"/aai/v16/business/projects/project/python_onap_sdk_project", + "relationship-data":[ + { + "relationship-key":"project.project-name", + "relationship-value":"python_onap_sdk_project" + } + ] + } + ] + } + } + ] +} + + +SERVICE_SUBSCRIPTION_RELATIONSHIPS = { + "relationship": [ + { + "related-to": "tenant", + "relationship-label": "org.onap.relationships.inventory.Uses", + "related-link": "/aai/v16/cloud-infrastructure/cloud-regions/cloud-region/OPNFV/RegionOne/tenants/tenant/4bdc6f0f2539430f9428c852ba606808", + "relationship-data": [ + { + "relationship-key": "cloud-region.cloud-owner", + "relationship-value": "OPNFV", + }, + { + "relationship-key": "cloud-region.cloud-region-id", + "relationship-value": "RegionOne", + }, + { + "relationship-key": "tenant.tenant-id", + "relationship-value": "4bdc6f0f2539430f9428c852ba606808", + }, + ], + "related-to-property": [ + { + "property-key": "tenant.tenant-name", + "property-value": "onap-dublin-daily-vnfs", + } + ], + } + ] +} + + +CLOUD_REGION = { + "cloud-region": [ + { + "cloud-owner": "OPNFV", + "cloud-region-id": "RegionOne", + "cloud-type": "openstack", + "owner-defined-type": "N/A", + "cloud-region-version": "pike", + "identity-url": "http://msb-iag.onap:80/api/multicloud-pike/v0/OPNFV_RegionOne/identity/v2.0", + "cloud-zone": "OPNFV LaaS", + "complex-name": "Cruguil", + "resource-version": "1561217827955", + "orchestration-disabled": True, + "in-maint": False, + "relationship-list": { + "relationship": [ + { + "related-to": "complex", + "relationship-label": "org.onap.relationships.inventory.LocatedIn", + "related-link": "/aai/v13/cloud-infrastructure/complexes/complex/cruguil", + "relationship-data": [ + { + "relationship-key": "complex.physical-location-id", + "relationship-value": "cruguil", + } + ], + } + ] + }, + } + ] +} + + +TENANT = { + "tenant-id": "4bdc6f0f2539430f9428c852ba606808", + "tenant-name": "onap-dublin-daily-vnfs", + "resource-version": "1562591004273", + "relationship-list": { + "relationship": [ + { + "related-to": "service-subscription", + "relationship-label": "org.onap.relationships.inventory.Uses", + "related-link": "/aai/v16/business/customers/customer/generic/service-subscriptions/service-subscription/freeradius", + "relationship-data": [ + { + "relationship-key": "customer.global-customer-id", + "relationship-value": "generic", + }, + { + "relationship-key": "service-subscription.service-type", + "relationship-value": "freeradius", + }, + ], + }, + { + "related-to": "service-subscription", + "relationship-label": "org.onap.relationships.inventory.Uses", + "related-link": "/aai/v16/business/customers/customer/generic/service-subscriptions/service-subscription/ims", + "relationship-data": [ + { + "relationship-key": "customer.global-customer-id", + "relationship-value": "generic", + }, + { + "relationship-key": "service-subscription.service-type", + "relationship-value": "ims", + }, + ], + }, + { + "related-to": "service-subscription", + "relationship-label": "org.onap.relationships.inventory.Uses", + "related-link": "/aai/v16/business/customers/customer/generic/service-subscriptions/service-subscription/ubuntu16", + "relationship-data": [ + { + "relationship-key": "customer.global-customer-id", + "relationship-value": "generic", + }, + { + "relationship-key": "service-subscription.service-type", + "relationship-value": "ubuntu16", + }, + ], + }, + ] + }, +} + + +CUSTOMERS_COUNT = { + "results":[ + { + "customer":12 + } + ] +} + + +@mock.patch.object(Customer, 'send_message_json') +def test_customer_service_tenant_relations(mock_send): + """Test the retrieval of service/tenant relations in A&AI.""" + mock_send.return_value = SIMPLE_CUSTOMER + customer = next(Customer.get_all()) + mock_send.return_value = SERVICE_SUBSCRIPTION + res = list(customer.service_subscriptions) + assert len(res) == 2 + assert res[0].service_type == "freeradius" + + +@mock.patch.object(Customer, "send_message_json") +def test_customers_get_all(mock_send): + """Test get_all Customer class method.""" + mock_send.return_value = {} + customers = list(Customer.get_all()) + assert len(customers) == 0 + + mock_send.return_value = CUSTOMERS + customers = list(Customer.get_all()) + assert len(customers) == 1 + + +@mock.patch.object(Customer, "send_message_json") +def test_customer_get_service_subscription_by_service_type(mock_send): + """Test Customer's get_service_subscription_by_service_type method.""" + mock_send.return_value = CUSTOMERS + customer = next(Customer.get_all()) + + mock_send.return_value = SERVICE_SUBSCRIPTION + service_subscription = customer.get_service_subscription_by_service_type("freeradius") + assert service_subscription.service_type == "freeradius" + + +@mock.patch.object(Customer, "send_message_json") +@mock.patch.object(ServiceSubscription, "send_message_json") +def test_customer_service_subscription_service_instance(mock_send_serv_sub, mock_send): + """Test Customer's service subscription service instances.""" + mock_send.return_value = CUSTOMERS + customer = next(Customer.get_all()) + mock_send.return_value = SERVICE_SUBSCRIPTION + service_subscription = customer.get_service_subscription_by_service_type("freeradius") + + mock_send_serv_sub.return_value = SERVICE_INSTANCES + service_instances = list(service_subscription.service_instances) + assert len(service_instances) == 1 + service_instance = service_instances[0] + assert service_instance.instance_name == "test" + assert service_instance.instance_id == "5410bf79-2aa3-450e-a324-ec5630dc18cf" + assert service_instance.service_subscription == service_subscription + assert service_instance.url == (f"{service_subscription.url}/service-instances/" + f"service-instance/{service_instance.instance_id}") + + +@mock.patch.object(Customer, "send_message_json") +@mock.patch.object(ServiceSubscription, "send_message_json") +@mock.patch.object(CloudRegion, "send_message_json") +def test_customer_service_subscription_cloud_region(mock_cloud_region, mock_send_serv_sub, mock_send): + """Test Customer's service subscription cloud region object.""" + mock_send.return_value = CUSTOMERS + customer = next(Customer.get_all()) + mock_send.return_value = SERVICE_SUBSCRIPTION + service_subscription = customer.get_service_subscription_by_service_type("freeradius") + + mock_send_serv_sub.return_value = {} + relationships = list(service_subscription.relationships) + assert len(relationships) == 0 + with pytest.raises(ParameterError): + service_subscription.cloud_region + with pytest.raises(ParameterError): + service_subscription.tenant + with pytest.raises(StopIteration): + next(service_subscription.cloud_regions) + with pytest.raises(StopIteration): + next(service_subscription.tenants) + + mock_cloud_region.return_value = CLOUD_REGION + mock_send_serv_sub.return_value = SERVICE_SUBSCRIPTION_RELATIONSHIPS + relationships = list(service_subscription.relationships) + assert len(relationships) == 1 + cloud_region = next(service_subscription.cloud_regions) + assert cloud_region.cloud_owner == "OPNFV" + assert cloud_region.cloud_region_id == "RegionOne" + assert cloud_region.cloud_type == "openstack" + + mock_cloud_region.side_effect = ResourceNotFound + with pytest.raises(StopIteration): + next(service_subscription.tenants) + mock_cloud_region.side_effect = [CLOUD_REGION, TENANT] + tenant = next(service_subscription.tenants) + assert tenant.tenant_id == "4bdc6f0f2539430f9428c852ba606808" + assert tenant.name == "onap-dublin-daily-vnfs" + + +@mock.patch.object(Customer, "send_message_json") +def test_customer_get_by_global_customer_id(mock_send): + """Test Customer's get_by_global_customer_id method.""" + mock_send.return_value = SIMPLE_CUSTOMER_2 + customer = Customer.get_by_global_customer_id("generic") + assert customer.global_customer_id == "generic" + assert customer.subscriber_name == "generic" + assert customer.subscriber_type == "INFRA" + assert customer.resource_version is not None + + +@mock.patch.object(Customer, "send_message") +@mock.patch.object(Customer, "send_message_json") +def test_customer_create(mock_send_json, mock_send): + """Test Customer's create method.""" + mock_send_json.return_value = SIMPLE_CUSTOMER_2 + customer = Customer.create("generic", "generic", "INFRA") + assert customer.global_customer_id == "generic" + assert customer.subscriber_name == "generic" + assert customer.subscriber_type == "INFRA" + assert customer.resource_version is not None + + customer = Customer.create("generic", "generic", "INFRA", service_subscriptions=["test-service-type"]) + assert customer.global_customer_id == "generic" + assert customer.subscriber_name == "generic" + assert customer.subscriber_type == "INFRA" + assert customer.resource_version is not None + + +@mock.patch.object(Customer, "send_message") +def test_customer_delete(mock_send): + """Test Customer's delete method.""" + customer = Customer("test", "test", "test", "test") + customer.delete() + mock_send.assert_called_once_with( + "DELETE", + "Delete customer", + customer.url + ) + + +def test_customer_url(): + """Test Customer's url property.""" + customer = Customer("generic", "generic", "INFRA") + assert customer.url == (f"{customer.base_url}{customer.api_version}/business/customers/" + f"customer/{customer.global_customer_id}?" + f"resource-version={customer.resource_version}") + + +@mock.patch.object(ServiceSubscription, "add_relationship") +def test_service_subscription_link_cloud_region_and_tenant(mock_add_rel): + """Test service subscription linking with cloud region and tenant. + + Test Relationship object creation + """ + service_subscription = ServiceSubscription(customer=None, + service_type="test_service_type", + resource_version="test_resource_version") + cloud_region = CloudRegion(cloud_owner="test_cloud_owner", + cloud_region_id="test_cloud_region", + orchestration_disabled=True, + in_maint=False) + tenant = Tenant(cloud_region=cloud_region, + tenant_id="test_tenant_id", + tenant_name="test_tenant_name") + service_subscription.link_to_cloud_region_and_tenant(cloud_region, tenant) + mock_add_rel.assert_called_once() + relationship = mock_add_rel.call_args[0][0] + assert relationship.related_to == "tenant" + assert relationship.related_link == tenant.url + assert len(relationship.relationship_data) == 3 + + +@mock.patch.object(Customer, "send_message_json") +@mock.patch.object(Customer, "send_message") +def test_customer_subscribe_service(mock_send_message, mock_send_message_json): + customer = Customer(global_customer_id="test_customer_id", + subscriber_name="test_subscriber_name", + subscriber_type="test_subscriber_type") + mock_send_message_json.side_effect = (ResourceNotFound, SERVICE_SUBSCRIPTION) + customer.subscribe_service("test_service") + + +#test the Cloud Region Class +AVAILABILITY_ZONE = { + "availability-zone-name":"OPNFV LaaS", + "hypervisor-type":"1234", + "operational-status":"working", + "resource-version":"version1.0" +} + +AVAILABILITY_ZONES = { + "availability-zone":[ + { + "availability-zone-name":"OPNFV LaaS", + "hypervisor-type":"1234", + "operational-status":"working", + "resource-version":"version1.0" + } + ] +} + + +@mock.patch.object(CloudRegion, "send_message_json") +def test_availability_zones(mock_send_message_json): + """Test Cloud Region property""" + cloud_region = CloudRegion(cloud_owner="test_cloud_owner", + cloud_region_id="test_cloud_region", + orchestration_disabled=True, + in_maint=False) + mock_send_message_json.return_value = AVAILABILITY_ZONES + cloud_zones = cloud_region.availability_zones + zone1 = next(cloud_zones) + assert zone1.name == "OPNFV LaaS" + assert zone1.hypervisor_type == "1234" + +@mock.patch.object(CloudRegion, "send_message_json") +def test_get_availability_zone_from_name(mock_send_message_json): + """Test get Availability Zone by name""" + cloud_region = CloudRegion(cloud_owner="test_cloud_owner", + cloud_region_id="test_cloud_region", + orchestration_disabled=True, + in_maint=False) + mock_send_message_json.return_value = AVAILABILITY_ZONE + availability_zone = cloud_region.get_availability_zone_by_name("OPNFV LaaS") + assert availability_zone.name == "OPNFV LaaS" + assert availability_zone.hypervisor_type == "1234" + assert availability_zone.resource_version == "version1.0" + +@mock.patch.object(CloudRegion, "send_message") +def test_add_availability_zone(mock_send_message): + """Test Cloud Region class method""" + cloud_region = CloudRegion(cloud_owner="test_cloud_owner", + cloud_region_id="test_cloud_region", + orchestration_disabled=True, + in_maint=False) + cloud_region.add_availability_zone(availability_zone_name="test_zone", + availability_zone_hypervisor_type="1234") + mock_send_message.assert_called_once() + method, description, url = mock_send_message.call_args[0] + assert method == "PUT" + assert description == "Add availability zone to cloud region" + assert url == f"{cloud_region.url}/availability-zones/availability-zone/test_zone" + +@mock.patch.object(CloudRegion, "send_message") +def test_add_tenant_to_cloud(mock_send_message): + """Test Cloud Region class method""" + cloud_region = CloudRegion(cloud_owner="test_cloud_owner", + cloud_region_id="test_cloud_region", + orchestration_disabled=True, + in_maint=False) + cloud_region.add_tenant(tenant_id="123456", tenant_name="test_tenant") + mock_send_message.assert_called_once() + method, description, url = mock_send_message.call_args[0] + assert method == "PUT" + assert description == "add tenant to cloud region" + assert url == f"{cloud_region.url}/tenants/tenant/123456" + + +@mock.patch.object(CloudRegion, "send_message") +def test_add_esr_system_info(mock_send_message): + """Test Cloud Region class method""" + cloud_region = CloudRegion(cloud_owner="test_cloud_owner", + cloud_region_id="test_cloud_region", + orchestration_disabled=True, + in_maint=False) + cloud_region.add_esr_system_info(esr_system_info_id="123456", + user_name="test_user", + password="password", + system_type="test_type") + mock_send_message.assert_called_once() + method, description, url = mock_send_message.call_args[0] + assert method == "PUT" + assert description == "Add external system info to cloud region" + assert url == f"{cloud_region.url}/esr-system-info-list/esr-system-info/123456" + + +@mock.patch.object(Multicloud, "register_vim") +def test_register_to_multicloud(mock_register): + """Test register to multicloud""" + cloud_region = CloudRegion(cloud_owner="test_cloud_owner", + cloud_region_id="test_cloud_region", + orchestration_disabled=True, + in_maint=False) + cloud_region.register_to_multicloud() + mock_register.assert_called_once() + + +@mock.patch.object(Multicloud, "unregister_vim") +def test_unregister_from_multicloud(mock_unregister): + """Test register to multicloud""" + cloud_region = CloudRegion(cloud_owner="test_cloud_owner", + cloud_region_id="test_cloud_region", + orchestration_disabled=True, + in_maint=False) + cloud_region.unregister_from_multicloud() + mock_unregister.assert_called_once() + + +@mock.patch.object(CloudRegion, "send_message") +def test_delete_cloud_region(mock_send_message): + cloud_region = CloudRegion(cloud_owner="test_cloud_owner", + cloud_region_id="test_cloud_region", + orchestration_disabled=True, + in_maint=False) + cloud_region.delete() + mock_send_message.assert_called_once() + method, descritption, url = mock_send_message.call_args[0] + assert method == "DELETE" + assert descritption == f"Delete cloud region test_cloud_region" + assert url == cloud_region.url + +@mock.patch.object(Customer, "send_message_json") +def test_customer_count(mock_send_message_json): + mock_send_message_json.return_value = CUSTOMERS_COUNT + assert Customer.count() == 12 + diff --git a/tests/test_aai_geo_region.py b/tests/test_aai_geo_region.py new file mode 100644 index 0000000..b33f77a --- /dev/null +++ b/tests/test_aai_geo_region.py @@ -0,0 +1,54 @@ +# Copyright 2022 Orange, 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 unittest.mock import patch + +from onapsdk.aai.cloud_infrastructure.geo_region import GeoRegion + +GEO_REGIONS = { + "geo-region": [ + { + "geo-region-id": "123" + }, + { + "geo-region-id": "321" + } + ] +} + +GEO_REGION = { + "geo-region-id": "123", + "resource-version": "123" +} + +@patch("onapsdk.aai.cloud_infrastructure.geo_region.GeoRegion.send_message_json") +def test_geo_region_get_all(mock_send_message_json): + mock_send_message_json.return_value = {} + assert len(list(GeoRegion.get_all())) == 0 + + mock_send_message_json.return_value = GEO_REGIONS + assert len(list(GeoRegion.get_all())) == 2 + +@patch("onapsdk.aai.cloud_infrastructure.geo_region.GeoRegion.send_message_json") +def test_geo_region_get_by_region_id(mock_send_message_json): + mock_send_message_json.return_value = GEO_REGION + geo_region = GeoRegion.get_by_geo_region_id("123") + assert geo_region.geo_region_id == "123" + assert geo_region.resource_version == "123" + +@patch("onapsdk.aai.cloud_infrastructure.geo_region.GeoRegion.send_message") +@patch("onapsdk.aai.cloud_infrastructure.geo_region.GeoRegion.get_by_geo_region_id") +def test_geo_region_create(mock_get_geo_region_by_id, mock_send_message): + GeoRegion.create("123") + mock_send_message.assert_called_once() + assert mock_get_geo_region_by_id.called_once_with("123") diff --git a/tests/test_aai_line_of_business.py b/tests/test_aai_line_of_business.py new file mode 100644 index 0000000..1bf4672 --- /dev/null +++ b/tests/test_aai_line_of_business.py @@ -0,0 +1,82 @@ +# Copyright 2022 Orange, 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 unittest import mock + +from onapsdk.aai.business.line_of_business import LineOfBusiness + + +LINES_OF_BUSINESS = { + "line-of-business": [ + { + "line-of-business-name": "test-name", + "resource-version": "1234" + }, + { + "line-of-business-name": "test-name2", + "resource-version": "4321" + } + ] +} + + +COUNT = { + "results":[ + { + "line-of-business":1 + } + ] +} + + +@mock.patch("onapsdk.aai.business.line_of_business.LineOfBusiness.send_message_json") +def test_line_of_business_get_all(mock_send_message_json): + mock_send_message_json.return_value = {} + assert len(list(LineOfBusiness.get_all())) == 0 + + mock_send_message_json.return_value = LINES_OF_BUSINESS + lines_of_business = list(LineOfBusiness.get_all()) + assert len(lines_of_business) == 2 + lob1, lob2 = lines_of_business + assert lob1.name == "test-name" + assert lob1.resource_version == "1234" + assert lob2.name == "test-name2" + assert lob2.resource_version == "4321" + + +@mock.patch("onapsdk.aai.business.line_of_business.LineOfBusiness.send_message_json") +def test_line_of_business_get_by_name(mock_send): + LineOfBusiness.get_by_name(name="test-name") + mock_send.assert_called_once_with("GET", + "Get test-name line of business", + "https://aai.api.sparky.simpledemo.onap.org:30233/aai/v23/business/lines-of-business/line-of-business/test-name") + + +@mock.patch("onapsdk.aai.business.line_of_business.LineOfBusiness.send_message") +@mock.patch("onapsdk.aai.business.line_of_business.LineOfBusiness.get_by_name") +def test_line_of_business_create(_, mock_send): + LineOfBusiness.create(name="test-name") + mock_send.assert_called_once_with("PUT", + "Declare A&AI line of business", + "https://aai.api.sparky.simpledemo.onap.org:30233/aai/v23/business/lines-of-business/line-of-business/test-name", + data='{\n "line-of-business-name": "test-name"\n}') + + +@mock.patch("onapsdk.aai.business.line_of_business.LineOfBusiness.send_message_json") +def test_line_of_business_count(mock_send_message_json): + mock_send_message_json.return_value = COUNT + assert LineOfBusiness.count() == 1 + +def test_line_of_business_url(): + line_of_business = LineOfBusiness(name="test-lob", resource_version="123") + assert line_of_business.name in line_of_business.url diff --git a/tests/test_aai_network.py b/tests/test_aai_network.py new file mode 100644 index 0000000..7196d5a --- /dev/null +++ b/tests/test_aai_network.py @@ -0,0 +1,159 @@ +# Copyright 2022 Orange, 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 unittest import mock + +from onapsdk.aai.business import NetworkInstance +from onapsdk.so.deletion import NetworkDeletionRequest + + +NETWORK_INSTANCE = { + 'network-id': '49dab38b-3a5b-47e5-9cd6-b8d069d6109d', + 'network-name': 'Python_ONAP_SDK_network_instance_0b4308ca-3fe0-4af1-9c4e-ed2c816b9496', + 'is-bound-to-vpn': False, + 'resource-version': '1593162237842', + 'orchestration-status': 'Inventoried', + 'model-invariant-id': 'cdbb2169-e638-4aab-a4e9-b9d2d6d62b04', + 'model-version-id': '51789f7b-5ffc-4c12-ac87-02363fdb40b1', + 'model-customization-id': 'db9c9a6c-2a1c-4cdd-8fbc-e10448d0e4cc', + 'is-provider-network': False, + 'is-shared-network': False, + 'is-external-network': False, + 'relationship-list': { + 'relationship': [ + { + 'related-to': 'service-instance', + 'relationship-label': 'org.onap.relationships.inventory.ComposedOf', + 'related-link': '/aai/v19/business/customers/customer/TestCustomer/service-subscriptions/service-subscription/vFW_with_net/service-instances/service-instance/72fd9ee9-077f-4d3d-8e86-08ed24514802', + 'relationship-data': [ + { + 'relationship-key': 'customer.global-customer-id', + 'relationship-value': 'TestCustomer' + }, + { + 'relationship-key': 'service-subscription.service-type', + 'relationship-value': 'vFW_with_net' + }, + { + 'relationship-key': 'service-instance.service-instance-id', + 'relationship-value': '72fd9ee9-077f-4d3d-8e86-08ed24514802' + } + ], + 'related-to-property': [ + { + 'property-key': 'service-instance.service-instance-name', + 'property-value': 'Python_ONAP_SDK_service_instance_7be66d06-c466-46cf-b84a-cd7af2d633ed' + } + ] + }, + { + 'related-to': 'cloud-region', + 'relationship-label': 'org.onap.relationships.inventory.Uses', + 'related-link': '/aai/v19/cloud-infrastructure/cloud-regions/cloud-region/TestCloudOwner/RegionOne', + 'relationship-data': [ + { + 'relationship-key': 'cloud-region.cloud-owner', + 'relationship-value': 'TestCloudOwner' + }, + { + 'relationship-key': 'cloud-region.cloud-region-id', + 'relationship-value': 'RegionOne' + } + ], + 'related-to-property': [ + { + 'property-key': 'cloud-region.owner-defined-type', + 'property-value': '' + } + ] + }, + { + 'related-to': 'line-of-business', + 'relationship-label': 'org.onap.relationships.inventory.Uses', + 'related-link': '/aai/v19/business/lines-of-business/line-of-business/Test-BusinessLine', + 'relationship-data': [ + { + 'relationship-key': 'line-of-business.line-of-business-name', + 'relationship-value': 'Test-BusinessLine' + } + ] + }, + { + 'related-to': 'tenant', + 'relationship-label': 'org.onap.relationships.inventory.Uses', + 'related-link': '/aai/v19/cloud-infrastructure/cloud-regions/cloud-region/TestCloudOwner/RegionOne/tenants/tenant/89788fdf49514f94963b12a6c0cfdc71', + 'relationship-data': [ + { + 'relationship-key': 'cloud-region.cloud-owner', + 'relationship-value': 'TestCloudOwner' + }, + { + 'relationship-key': 'cloud-region.cloud-region-id', + 'relationship-value': 'RegionOne' + }, + { + 'relationship-key': 'tenant.tenant-id', + 'relationship-value': '89788fdf49514f94963b12a6c0cfdc71' + } + ], + 'related-to-property': [ + { + 'property-key': 'tenant.tenant-name', + 'property-value': 'test-tenant' + } + ] + }, + { + 'related-to': 'platform', + 'relationship-label': 'org.onap.relationships.inventory.Uses', + 'related-link': '/aai/v19/business/platforms/platform/Test-Platform', + 'relationship-data': [ + { + 'relationship-key': 'platform.platform-name', + 'relationship-value': 'Test-Platform' + } + ] + } + ] + } +} + + +def test_create_network_instance_from_api_response(): + service_instance = mock.MagicMock() + network_instance = NetworkInstance.create_from_api_response( + NETWORK_INSTANCE, + service_instance + ) + assert network_instance.network_name == "Python_ONAP_SDK_network_instance_0b4308ca-3fe0-4af1-9c4e-ed2c816b9496" + assert network_instance.network_id == "49dab38b-3a5b-47e5-9cd6-b8d069d6109d" + assert network_instance.is_bound_to_vpn is False + assert network_instance.is_provider_network is False + assert network_instance.is_shared_network is False + assert network_instance.is_external_network is False + assert network_instance.resource_version == "1593162237842" + assert network_instance.model_invariant_id == "cdbb2169-e638-4aab-a4e9-b9d2d6d62b04" + assert network_instance.model_version_id == "51789f7b-5ffc-4c12-ac87-02363fdb40b1" + assert network_instance.model_customization_id == "db9c9a6c-2a1c-4cdd-8fbc-e10448d0e4cc" + + +@mock.patch.object(NetworkDeletionRequest, "send_message_json") +def test_network_instance_delete(mock_send_message_json): + network_instance = NetworkInstance(mock.MagicMock(), + network_id="test_network_id", + is_bound_to_vpn=True, + is_provider_network=False, + is_shared_network=True, + is_external_network=False) + network_instance.delete() + mock_send_message_json.assert_called_once() diff --git a/tests/test_aai_owning_entity.py b/tests/test_aai_owning_entity.py new file mode 100644 index 0000000..0f16044 --- /dev/null +++ b/tests/test_aai_owning_entity.py @@ -0,0 +1,87 @@ +# Copyright 2022 Orange, 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 unittest import mock + +import pytest + +from onapsdk.aai.business import OwningEntity +from onapsdk.exceptions import ResourceNotFound + + +OWNING_ENTITIES = { + "owning-entity":[ + { + "owning-entity-id":"ff6c945f-89ab-4f14-bafd-0cdd6eac791a", + "owning-entity-name":"OE-Generic", + "resource-version":"1588244348931", + }, + { + "owning-entity-id":"OE-generic", + "owning-entity-name":"OE-generic", + "resource-version":"1587388597761" + }, + { + "owning-entity-id":"b3dcdbb0-edae-4384-b91e-2f114472520c" + ,"owning-entity-name":"test", + "resource-version":"1588145971158" + } + ] +} + + +OWNING_ENTITY = { + "owning-entity-id":"OE-generic", + "owning-entity-name":"OE-generic", + "resource-version":"1587388597761" +} + + +@mock.patch.object(OwningEntity, "send_message_json") +def test_owning_entity_get_all(mock_send): + mock_send.return_value = OWNING_ENTITIES + owning_entities = list(OwningEntity.get_all()) + assert len(owning_entities) == 3 + owning_entity = owning_entities[0] + assert owning_entity.owning_entity_id == "ff6c945f-89ab-4f14-bafd-0cdd6eac791a" + assert owning_entity.name == "OE-Generic" + assert owning_entity.url == (f"{owning_entity.base_url}{owning_entity.api_version}/" + "business/owning-entities/owning-entity/" + f"{owning_entity.owning_entity_id}") + + +@mock.patch.object(OwningEntity, "send_message_json") +def test_owning_entity_get_by_name(mock_send): + mock_send.return_value = OWNING_ENTITIES + with pytest.raises(ResourceNotFound) as exc: + OwningEntity.get_by_owning_entity_name("invalid name") + assert exc.type == ResourceNotFound + owning_entity = OwningEntity.get_by_owning_entity_name("OE-Generic") + assert owning_entity.owning_entity_id == "ff6c945f-89ab-4f14-bafd-0cdd6eac791a" + assert owning_entity.name == "OE-Generic" + + +@mock.patch.object(OwningEntity, "send_message") +@mock.patch.object(OwningEntity, "send_message_json") +def test_owning_entity_create(mock_send_json, mock_send): + mock_send_json.return_value = OWNING_ENTITY + OwningEntity.create( + name="OE-generic", + ) + + owning_entity = OwningEntity.create( + name="OE-generic", + owning_entity_id="OE-generic" + ) + assert owning_entity.owning_entity_id == "OE-generic" + assert owning_entity.name == "OE-generic" diff --git a/tests/test_aai_platform.py b/tests/test_aai_platform.py new file mode 100644 index 0000000..ed20e50 --- /dev/null +++ b/tests/test_aai_platform.py @@ -0,0 +1,82 @@ +# Copyright 2022 Orange, 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 unittest import mock + +from onapsdk.aai.business.platform import Platform + + +PLATFORMS = { + "platform": [ + { + "platform-name": "test-name", + "resource-version": "1234" + }, + { + "platform-name": "test-name2", + "resource-version": "4321" + } + ] +} + + +COUNT = { + "results":[ + { + "platform":1 + } + ] +} + + +@mock.patch("onapsdk.aai.business.platform.Platform.send_message_json") +def test_platform_get_all(mock_send_message_json): + mock_send_message_json.return_value = {} + assert len(list(Platform.get_all())) == 0 + + mock_send_message_json.return_value = PLATFORMS + platforms = list(Platform.get_all()) + assert len(platforms) == 2 + lob1, lob2 = platforms + assert lob1.name == "test-name" + assert lob1.resource_version == "1234" + assert lob2.name == "test-name2" + assert lob2.resource_version == "4321" + + +@mock.patch("onapsdk.aai.business.platform.Platform.send_message_json") +def test_platform_get_by_name(mock_send): + Platform.get_by_name(name="test-name") + mock_send.assert_called_once_with("GET", + "Get test-name platform", + "https://aai.api.sparky.simpledemo.onap.org:30233/aai/v23/business/platforms/platform/test-name") + + +@mock.patch("onapsdk.aai.business.platform.Platform.send_message") +@mock.patch("onapsdk.aai.business.platform.Platform.get_by_name") +def test_platform_create(_, mock_send): + Platform.create(name="test-name") + mock_send.assert_called_once_with("PUT", + "Declare A&AI platform", + "https://aai.api.sparky.simpledemo.onap.org:30233/aai/v23/business/platforms/platform/test-name", + data='{\n "platform-name": "test-name"\n}') + + +@mock.patch("onapsdk.aai.business.platform.Platform.send_message_json") +def test_line_of_business_count(mock_send_message_json): + mock_send_message_json.return_value = COUNT + assert Platform.count() == 1 + +def test_platform_url(): + platform = Platform(name="test-platform", resource_version="123") + assert platform.name in platform.url diff --git a/tests/test_aai_pnf.py b/tests/test_aai_pnf.py new file mode 100644 index 0000000..32e9ea7 --- /dev/null +++ b/tests/test_aai_pnf.py @@ -0,0 +1,140 @@ +# Copyright 2022 Orange, 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 unittest import mock + +import pytest + +from onapsdk.aai.business import PnfInstance, pnf +from onapsdk.exceptions import ResourceNotFound +# from onapsdk.so.deletion import NetworkDeletionRequest + + +PNF_INSTANCE = { + "pnf-name": "blablabla", + "pnf-id": "546b282b-2ff7-41a4-9329-55c9a2888477", + "equip-type": "pnf", + "equip-vendor": "PNF", + "equip-model": "Simulated Device", + "orchestration-status": "Active", + "ipaddress-v4-oam": "172.30.1.6", + "sw-version": "2.3.5", + "in-maint":False, + "serial-number": "123", + "ipaddress-v6-oam": "0:0:0:0:0:ffff:a0a:011", + "resource-version": "1610142659380", + "nf-role": "sdn controller", + "model-customization-id": "137ce8e8-bee9-465f-b7e1-0c006f10b443", + "model-invariant-id": "2ca7ea68-cf61-449c-a733-8122bcac1f9a", + "model-version-id": "da467f24-a26d-4620-b185-e1afa1d365ac", + "relationship-list": { + "relationship":[ + { + "related-to":"service-instance", + "relationship-label":"org.onap.relationships.inventory.ComposedOf", + "related-link":"/aai/v21/business/customers/customer/test/service-subscriptions/service-subscription/test/service-instances/service-instance/4c3ab996-afdb-4956-9c4d-038b4eed3db1", + "relationship-data":[ + { + "relationship-key":"customer.global-customer-id", + "relationship-value":"test" + }, + { + "relationship-key":"service-subscription.service-type", + "relationship-value":"test" + }, + { + "relationship-key":"service-instance.service-instance-id", + "relationship-value":"4c3ab996-afdb-4956-9c4d-038b4eed3db1" + } + ], + "related-to-property":[ + { + "property-key":"service-instance.service-instance-name", + "property-value":"blablabla" + } + ] + } + ] + } +} + + +COUNT = { + "results":[ + { + "pnf":12 + } + ] +} + + +def test_create_pnf_instance_from_api_response(): + service_instance = mock.MagicMock() + pnf_instance = PnfInstance.create_from_api_response( + PNF_INSTANCE, + service_instance + ) + assert pnf_instance.pnf_name == "blablabla" + assert pnf_instance.pnf_id == "546b282b-2ff7-41a4-9329-55c9a2888477" + assert pnf_instance.equip_type == "pnf" + assert pnf_instance.equip_vendor == "PNF" + assert pnf_instance.equip_model == "Simulated Device" + assert pnf_instance.orchestration_status == "Active" + assert pnf_instance.ipaddress_v4_oam == "172.30.1.6" + assert pnf_instance.sw_version == "2.3.5" + assert pnf_instance.in_maint == False + assert pnf_instance.serial_number == "123" + assert pnf_instance.ipaddress_v6_oam == "0:0:0:0:0:ffff:a0a:011" + assert pnf_instance.resource_version == "1610142659380" + assert pnf_instance.nf_role == "sdn controller" + assert pnf_instance.model_customization_id == "137ce8e8-bee9-465f-b7e1-0c006f10b443" + assert pnf_instance.model_invariant_id == "2ca7ea68-cf61-449c-a733-8122bcac1f9a" + assert pnf_instance.model_version_id == "da467f24-a26d-4620-b185-e1afa1d365ac" + + assert pnf_instance.url.endswith(pnf_instance.pnf_name) + + +@mock.patch.object(PnfInstance, "send_message") +def test_delete_pnf_instance(mock_send_message): + pnf = PnfInstance(mock.MagicMock, "test_pnf", False) + pnf.delete() + method, _, address = mock_send_message.call_args[0] + assert method == "DELETE" + assert address == f"{pnf.url}?resource-version={pnf.resource_version}" + + +def test_pnf_instance_pnf(): + service_instance = mock.MagicMock() + pnf_instance = PnfInstance.create_from_api_response( + PNF_INSTANCE, + service_instance + ) + + assert pnf_instance._pnf is None + service_instance.sdc_service.pnfs = [] + with pytest.raises(ResourceNotFound) as exc: + pnf_instance.pnf + assert exc.type == ResourceNotFound + assert pnf_instance._pnf is None + + pnf = mock.MagicMock() + pnf.model_version_id = "da467f24-a26d-4620-b185-e1afa1d365ac" + service_instance.sdc_service.pnfs = [pnf] + assert pnf == pnf_instance.pnf + assert pnf_instance._pnf is not None + assert pnf_instance.pnf == pnf_instance._pnf + +@mock.patch.object(PnfInstance, "send_message_json") +def test_pnf_count(mock_send_message_json): + mock_send_message_json.return_value = COUNT + assert PnfInstance.count() == 12 diff --git a/tests/test_aai_project.py b/tests/test_aai_project.py new file mode 100644 index 0000000..4186772 --- /dev/null +++ b/tests/test_aai_project.py @@ -0,0 +1,82 @@ +# Copyright 2022 Orange, 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 unittest import mock + +from onapsdk.aai.business.project import Project + + +PROJECTS = { + "project": [ + { + "project-name": "test-name", + "resource-version": "1234" + }, + { + "project-name": "test-name2", + "resource-version": "4321" + } + ] +} + + +COUNT = { + "results":[ + { + "project":1 + } + ] +} + + +@mock.patch("onapsdk.aai.business.project.Project.send_message_json") +def test_project_get_all(mock_send_message_json): + mock_send_message_json.return_value = {} + assert len(list(Project.get_all())) == 0 + + mock_send_message_json.return_value = PROJECTS + projects = list(Project.get_all()) + assert len(projects) == 2 + lob1, lob2 = projects + assert lob1.name == "test-name" + assert lob1.resource_version == "1234" + assert lob2.name == "test-name2" + assert lob2.resource_version == "4321" + + +@mock.patch("onapsdk.aai.business.project.Project.send_message_json") +def test_project_get_by_name(mock_send): + Project.get_by_name(name="test-name") + mock_send.assert_called_once_with("GET", + "Get test-name project", + "https://aai.api.sparky.simpledemo.onap.org:30233/aai/v23/business/projects/project/test-name") + + +@mock.patch("onapsdk.aai.business.project.Project.send_message") +@mock.patch("onapsdk.aai.business.project.Project.get_by_name") +def test_project_create(_, mock_send): + Project.create(name="test-name") + mock_send.assert_called_once_with("PUT", + "Declare A&AI project", + "https://aai.api.sparky.simpledemo.onap.org:30233/aai/v23/business/projects/project/test-name", + data='{\n "project-name": "test-name"\n}') + + +@mock.patch("onapsdk.aai.business.project.Project.send_message_json") +def test_project_count(mock_send_message_json): + mock_send_message_json.return_value = COUNT + assert Project.count() == 1 + +def test_project_url(): + project = Project(name="test-project", resource_version="123") + assert project.name in project.url diff --git a/tests/test_aai_resource.py b/tests/test_aai_resource.py new file mode 100644 index 0000000..c1da0b7 --- /dev/null +++ b/tests/test_aai_resource.py @@ -0,0 +1,60 @@ +"""Test A&AI Element.""" +# Copyright 2022 Orange, 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 pytest +from unittest import mock + +from onapsdk.aai.aai_element import AaiResource, Relationship +from onapsdk.exceptions import RequestError, ResourceNotFound, RelationshipNotFound +from onapsdk.utils.gui import GuiList + +@mock.patch.object(AaiResource, "send_message_json") +@mock.patch.object(AaiResource, "url") +def test_relationship_not_found(mock_send, mock_url): + + aai_element = AaiResource() + mock_url.return_value = "http://my.url/" + + mock_send.side_effect = ResourceNotFound + + aai_element.send_message_json = mock_send + + with pytest.raises(ResourceNotFound) as exc: + list(aai_element.relationships) + assert exc.type == RelationshipNotFound + + mock_send.assert_called_once() + + +def test_relationship_get_relationship_data(): + r = Relationship( + related_to="test", + related_link="test", + relationship_data=[{ + "relationship-key": "test", + "relationship-value": "test" + }] + ) + assert r.get_relationship_data("invalid key") is None + assert r.get_relationship_data("test") == "test" + +@mock.patch.object(AaiResource, "send_message") +def test_get_guis(send_message_mock): + component = AaiResource() + send_message_mock.return_value.status_code = 200 + send_message_mock.return_value.url = "https://aai.api.sparky.simpledemo.onap.org:30220/services/aai/webapp/index.html#/browse" + gui_results = component.get_guis() + assert type(gui_results) == GuiList + assert gui_results.guilist[0].url == send_message_mock.return_value.url + assert gui_results.guilist[0].status == send_message_mock.return_value.status_code diff --git a/tests/test_aai_service.py b/tests/test_aai_service.py new file mode 100644 index 0000000..aac4c59 --- /dev/null +++ b/tests/test_aai_service.py @@ -0,0 +1,770 @@ +"""Test AaiElement module.""" +# Copyright 2022 Orange, 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 unittest import mock + +import pytest + +from onapsdk.aai.aai_element import AaiElement, AaiResource, Relationship +from onapsdk.aai.cloud_infrastructure import ( + CloudRegion, + Complex, + EsrSystemInfo, + Tenant +) +from onapsdk.aai.business import Customer +from onapsdk.aai.service_design_and_creation import Service, Model +from onapsdk.onap_service import OnapService + + +# pylint: disable=C0301 +TENANT = { + "tenant": [ + { + "tenant-id": "4bdc6f0f2539430f9428c852ba606808", + "tenant-name": "onap-dublin-daily-vnfs", + "resource-version": "1562591004273", + "relationship-list": { + "relationship": [ + { + "related-to": "service-subscription", + "relationship-label": "org.onap.relationships.inventory.Uses", + "related-link": "/aai/v16/business/customers/customer/generic/service-subscriptions/service-subscription/freeradius", + "relationship-data": [ + { + "relationship-key": "customer.global-customer-id", + "relationship-value": "generic", + }, + { + "relationship-key": "service-subscription.service-type", + "relationship-value": "freeradius", + }, + ], + }, + { + "related-to": "service-subscription", + "relationship-label": "org.onap.relationships.inventory.Uses", + "related-link": "/aai/v16/business/customers/customer/generic/service-subscriptions/service-subscription/ims", + "relationship-data": [ + { + "relationship-key": "customer.global-customer-id", + "relationship-value": "generic", + }, + { + "relationship-key": "service-subscription.service-type", + "relationship-value": "ims", + }, + ], + }, + { + "related-to": "service-subscription", + "relationship-label": "org.onap.relationships.inventory.Uses", + "related-link": "/aai/v16/business/customers/customer/generic/service-subscriptions/service-subscription/ubuntu16", + "relationship-data": [ + { + "relationship-key": "customer.global-customer-id", + "relationship-value": "generic", + }, + { + "relationship-key": "service-subscription.service-type", + "relationship-value": "ubuntu16", + }, + ], + }, + ] + }, + } + ] +} + + +CLOUD_REGIONS = { + "cloud-region": [ + { + "cloud-owner": "OPNFV", + "cloud-region-id": "RegionOne", + "cloud-type": "openstack", + "owner-defined-type": "N/A", + "cloud-region-version": "pike", + "identity-url": "http://msb-iag.onap:80/api/multicloud-pike/v0/OPNFV_RegionOne/identity/v2.0", + "cloud-zone": "OPNFV LaaS", + "complex-name": "Cruguil", + "resource-version": "1561217827955", + "orchestration-disabled": False, + "in-maint": False, + "relationship-list": { + "relationship": [ + { + "related-to": "complex", + "relationship-label": "org.onap.relationships.inventory.LocatedIn", + "related-link": "/aai/v13/cloud-infrastructure/complexes/complex/cruguil", + "relationship-data": [ + { + "relationship-key": "complex.physical-location-id", + "relationship-value": "cruguil", + } + ], + } + ] + }, + } + ] +} + + +CLOUD_REGION = { + "cloud-region": [ + { + "cloud-owner": "OPNFV", + "cloud-region-id": "RegionOne", + "cloud-type": "openstack", + "owner-defined-type": "N/A", + "cloud-region-version": "pike", + "identity-url": "http://msb-iag.onap:80/api/multicloud-pike/v0/OPNFV_RegionOne/identity/v2.0", + "cloud-zone": "OPNFV LaaS", + "complex-name": "Cruguil", + "resource-version": "1561217827955", + "orchestration-disabled": True, + "in-maint": False, + "relationship-list": { + "relationship": [ + { + "related-to": "complex", + "relationship-label": "org.onap.relationships.inventory.LocatedIn", + "related-link": "/aai/v13/cloud-infrastructure/complexes/complex/cruguil", + "relationship-data": [ + { + "relationship-key": "complex.physical-location-id", + "relationship-value": "cruguil", + } + ], + } + ] + }, + } + ] +} + + +COMPLEXES = { + "complex": [ + { + "city": "", + "data-center-code": "1234", + "street1": "", + "street2": "", + "physical-location-id": "integration_test_complex", + "identity-url": "", + "lata": "", + "elevation": "", + "state": "", + "physical-location-type": "", + "longitude": "", + "relationship-list": { + "relationship": [ + { + "related-to-property": [ + { + "property-value": "OwnerType", + "property-key": "cloud-region.owner-defined-type", + } + ], + "relationship-label": "org.onap.relationships.inventory.LocatedIn", + "related-link": "/aai/v16/cloud-infrastructure/cloud-regions/cloud-region/CloudOwner/RegionOne", + "relationship-data": [ + { + "relationship-key": "cloud-region.cloud-owner", + "relationship-value": "CloudOwner", + }, + { + "relationship-key": "cloud-region.cloud-region-id", + "relationship-value": "RegionOne", + }, + ], + "related-to": "cloud-region", + } + ] + }, + "resource-version": "1581510773583", + "latitude": "", + "complex-name": "integration_test_complex", + "postal-code": "", + "country": "", + "region": "", + }, + { + "city": "Beijing", + "data-center-code": "example-data-center-code-val-5556", + "street1": "example-street1-val-34205", + "street2": "example-street2-val-99210", + "physical-location-id": "My_Complex", + "identity-url": "example-identity-url-val-56898", + "lata": "example-lata-val-46073", + "elevation": "example-elevation-val-30253", + "state": "example-state-val-59487", + "physical-location-type": "example-physical-location-type-val-7608", + "longitude": "106.4074", + "resource-version": "1581504768889", + "latitude": "39.9042", + "complex-name": "My_Complex", + "postal-code": "100000", + "country": "example-country-val-94173", + "region": "example-region-val-13893", + }, + ] +} + + +CLOUD_REGION_RELATIONSHIP = { + "relationship": [ + { + "relationship-label": "org.onap.relationships.inventory.LocatedIn", + "related-link": "/aai/v16/cloud-infrastructure/complexes/complex/integration_test_complex", + "relationship-data": [ + { + "relationship-key": "complex.physical-location-id", + "relationship-value": "integration_test_complex", + } + ], + "related-to": "complex", + } + ] +} + + +SERVICE_SUBSCRIPTION = { + "service-subscription": [ + { + "service-type": "freeradius", + "resource-version": "1562591478146", + "relationship-list": { + "relationship": [ + { + "related-to": "tenant", + "relationship-label": "org.onap.relationships.inventory.Uses", + "related-link": "/aai/v16/cloud-infrastructure/cloud-regions/cloud-region/OPNFV/RegionOne/tenants/tenant/4bdc6f0f2539430f9428c852ba606808", + "relationship-data": [ + { + "relationship-key": "cloud-region.cloud-owner", + "relationship-value": "OPNFV", + }, + { + "relationship-key": "cloud-region.cloud-region-id", + "relationship-value": "RegionOne", + }, + { + "relationship-key": "tenant.tenant-id", + "relationship-value": "4bdc6f0f2539430f9428c852ba606808", + }, + ], + "related-to-property": [ + { + "property-key": "tenant.tenant-name", + "property-value": "onap-dublin-daily-vnfs", + } + ], + } + ] + }, + }, + {"service-type": "ims"}, + ] +} + + +SUBSCRIPTION_TYPES_NO_RESOURCES = { + "requestError": { + "serviceException": { + "messageId": "SVC3001", + "text": ("Resource not found for %1 using id " + "%2 (msg=%3) +(ec=%4)"), + "variables": [ + "GET", + "service-design-and-creation/services", + ( + "Node Not Found:No Node of type service found at: " + + "/service-design-and-creation/services" + ), + "ERR.5.4.6114", + ], + } + } +} + + +SUBSCRIPTION_TYPES_LIST = { + "service": [ + { + "service-id": "f4bcf0b0-b44e-423a-8357-5758afc14e88", + "service-description": "ubuntu16", + "resource-version": "1561218639393", + }, + { + "service-id": "2e812e77-e437-46c4-8e8e-908fbc7e176c", + "service-description": "freeradius", + "resource-version": "1561219163076", + }, + { + "service-id": "f208de57-0e02-4505-a0fa-375b13ad24ac", + "service-description": "ims", + "resource-version": "1561219799684", + }, + ] +} + + +CUSTOMERS_NO_RESOURCES = { + "requestError": { + "serviceException": { + "messageId": "SVC3001", + "text": ("Resource not found for %1 using id " + "%2 (msg=%3) +(ec=%4)"), + "variables": [ + "GET", + "business/customers", + ( + "Node Not Found:No Node of type customer found at: " + + "business/customers" + ), + "ERR.5.4.6114", + ], + } + } +} + + +SIMPLE_CUSTOMER = { + "customer": [ + { + "global-customer-id": "generic", + "subscriber-name": "generic", + "subscriber-type": "INFRA", + "resource-version": "1561218640404", + } + ] +} + + +ESR_SYSTEM_INFO = { + 'esr-system-info': [ + { + 'esr-system-info-id': 'c2d5e75d-56fd-47bc-af31-95607b26fa93', + 'service-url': 'http://keystone:5000/v3', + 'user-name': 'test-devel', + 'password': 'test-devel', + 'system-type': 'openstack', + 'cloud-domain': 'Default', + 'resource-version': '1586436352654' + } + ] +} + + +CLOUD_REGIONS_ITERATOR = ( + cloud_region + for cloud_region in [ + CloudRegion( + cloud_owner="OPNFV", + cloud_region_id="RegionOne", + cloud_type="openstack", + owner_defined_type="N/A", + cloud_region_version="pike", + identity_url=None, + cloud_zone="OPNFV LaaS", + complex_name="Cruguil", + sriov_automation=None, + cloud_extra_info=None, + upgrade_cycle=None, + orchestration_disabled=False, + in_maint=False, + resource_version=None, + ) + ] +) +# pylint: enable=C0301 + + +def test_init(): + """Test the initialization.""" + element = AaiElement() + assert isinstance(element, OnapService) + + +def test_class_variables(): + """Test the class variables.""" + assert AaiElement.server == "AAI" + assert AaiElement.base_url == "https://aai.api.sparky.simpledemo.onap.org:30233" + assert AaiElement.headers == { + "Content-Type": "application/json", + "Accept": "application/json", + "x-fromappid": "AAI", + "x-transactionid": "0a3f6713-ba96-4971-a6f8-c2da85a3176e", + "authorization": "Basic QUFJOkFBSQ=="} + +@mock.patch.object(AaiElement, 'send_message_json') +def test_customers(mock_send): + """Test get_customer function of A&AI.""" + mock_send.return_value = SIMPLE_CUSTOMER + assert len(list(Customer.get_all())) == 1 + aai_customer_1 = next(Customer.get_all()) + assert aai_customer_1.global_customer_id == "generic" + assert aai_customer_1.subscriber_name == "generic" + assert aai_customer_1.subscriber_type == "INFRA" + assert aai_customer_1.resource_version == "1561218640404" + mock_send.assert_called_with("GET", 'get customers', mock.ANY) + +@mock.patch.object(AaiElement, 'send_message_json') +def test_customers_no_resources(mock_send): + """Test get_customer function with no customer declared in A&AI.""" + mock_send.return_value = CUSTOMERS_NO_RESOURCES + assert len(list(Customer.get_all())) == 0 + mock_send.assert_called_with("GET", 'get customers', mock.ANY) + +@mock.patch.object(AaiElement, 'send_message_json') +def test_subscription_type_list(mock_send): + """Test the getter of subscription types in A&AI.""" + mock_send.return_value = {} + assert len(list(Service.get_all())) == 0 + assert len(list(Service.get_all())) == 0 + + mock_send.return_value = SUBSCRIPTION_TYPES_LIST + assert len(list(Service.get_all())) == 3 + assert len(list(Service.get_all())) == 3 + subscriptions = Service.get_all() + aai_service_1 = next(subscriptions) + aai_service_2 = next(subscriptions) + aai_service_3 = next(subscriptions) + assert aai_service_1.service_id == "f4bcf0b0-b44e-423a-8357-5758afc14e88" + assert aai_service_1.service_description == "ubuntu16" + assert aai_service_1.resource_version == "1561218639393" + assert aai_service_2.service_id == "2e812e77-e437-46c4-8e8e-908fbc7e176c" + assert aai_service_2.service_description == "freeradius" + assert aai_service_2.resource_version == "1561219163076" + assert aai_service_3.service_id == "f208de57-0e02-4505-a0fa-375b13ad24ac" + assert aai_service_3.service_description == "ims" + assert aai_service_3.resource_version == "1561219799684" + mock_send.assert_called_with("GET", 'get subscriptions', mock.ANY) + +@mock.patch.object(AaiElement, 'send_message_json') +def test_subscription_types_no_resources(mock_send): + """Test get_customer function with no customer declared in A&AI.""" + mock_send.return_value = SUBSCRIPTION_TYPES_NO_RESOURCES + assert len(list(Service.get_all())) == 0 + mock_send.assert_called_with("GET", 'get subscriptions', mock.ANY) + +@mock.patch.object(AaiElement, 'send_message_json') +def test_cloud_regions(mock_send): + """Test get cloud regions from A&AI.""" + mock_send.return_value = CLOUD_REGION + assert len(list(CloudRegion.get_all())) == 1 + cloud_region = next(CloudRegion.get_all()) + assert cloud_region.cloud_owner == "OPNFV" + assert cloud_region.cloud_type == "openstack" + assert cloud_region.complex_name == "Cruguil" + + cloud_region = next(CloudRegion.get_all()) + assert cloud_region.cloud_owner == "OPNFV" + assert cloud_region.cloud_type == "openstack" + assert cloud_region.complex_name == "Cruguil" + + mock_send.return_value = {} + cloud_regions = list(CloudRegion.get_all()) + assert len(cloud_regions) == 0 + + with pytest.raises(StopIteration): + cloud_region = next(CloudRegion.get_all()) + + mock_send.return_value = CLOUD_REGIONS + cloud_regions = list(CloudRegion.get_all()) + assert len(cloud_regions) == 1 + +@mock.patch.object(CloudRegion, "send_message") +def test_cloud_region_creation(mock_send): + """Test cloud region creation""" + cloud_region = CloudRegion.create( + cloud_owner="test_owner", + cloud_region_id="test_cloud_region", + orchestration_disabled=False, + in_maint=True, + owner_defined_type="Test", + cloud_zone="Test zone", + sriov_automation="Test", + upgrade_cycle="Test" + ) + assert cloud_region.cloud_owner == "test_owner" + assert cloud_region.cloud_region_id == "test_cloud_region" + assert cloud_region.orchestration_disabled == False + assert cloud_region.in_maint == True + assert cloud_region.cloud_type == "" + assert cloud_region.owner_defined_type == "Test" + assert cloud_region.cloud_region_version == "" + assert cloud_region.identity_url == "" + assert cloud_region.cloud_zone == "Test zone" + assert cloud_region.complex_name == "" + assert cloud_region.sriov_automation == "Test" + assert cloud_region.cloud_extra_info == "" + assert cloud_region.upgrade_cycle == "Test" + +@mock.patch.object(CloudRegion, 'get_all') +@mock.patch.object(AaiElement, 'send_message_json') +def test_tenants_info(mock_send, mock_cloud_regions): + """Test get Tenant from A&AI.""" + mock_cloud_regions.return_value = CLOUD_REGIONS_ITERATOR + mock_send.return_value = TENANT + cloud_name = "RegionOne" + cloud_region = CloudRegion.get_by_id("DT", cloud_name) + res = list(cloud_region.tenants) + assert len(res) == 1 + assert isinstance(res[0], Tenant) + tenant = res[0] + assert tenant.tenant_id == "4bdc6f0f2539430f9428c852ba606808" + assert tenant.name == "onap-dublin-daily-vnfs" + assert tenant.context is None + assert tenant.resource_version == "1562591004273" + assert tenant.url == ( + f"{tenant.base_url}{tenant.api_version}/cloud-infrastructure/cloud-regions/cloud-region/" + f"OPNFV/RegionOne/tenants/tenant/4bdc6f0f2539430f9428c852ba606808?" + f"resource-version=1562591004273" + ) + +@mock.patch.object(CloudRegion, 'get_all') +@mock.patch.object(AaiElement, 'send_message_json') +def test_tenants_info_wrong_cloud_name(mock_send, mock_cloud_regions): + """Test get Tenant from A&AI.""" + mock_cloud_regions.return_value = CLOUD_REGIONS_ITERATOR + mock_send.return_value = TENANT + cloud_name = "Wrong_cloud_name" + with pytest.raises(Exception) as excinfo: + CloudRegion.get_by_id("DT", cloud_name) + assert "not found" in str(excinfo.value) + + +@mock.patch.object(CloudRegion, "send_message_json") +def test_cloud_regions_relationship(mock_send): + """Test cloud region relationship property.""" + mock_send.return_value = CLOUD_REGION_RELATIONSHIP + cloud_region = CloudRegion(cloud_owner="tester", cloud_region_id="test", + orchestration_disabled=True, in_maint=False) + relationship = next(cloud_region.relationships) + assert isinstance(relationship, Relationship) + assert relationship.relationship_label == "org.onap.relationships.inventory.LocatedIn" + assert relationship.related_link == \ + "/aai/v16/cloud-infrastructure/complexes/complex/integration_test_complex" + assert relationship.related_to == "complex" + assert relationship.relationship_data[0]["relationship-key"] == "complex.physical-location-id" + assert relationship.relationship_data[0]["relationship-value"] == "integration_test_complex" + + +@mock.patch.object(CloudRegion, "send_message_json") +def test_cloud_regions_esr_system_infos(mock_send): + """Test cloud region esr system info""" + mock_send.return_value = ESR_SYSTEM_INFO + cloud_region = CloudRegion(cloud_owner="tester", cloud_region_id="test", + orchestration_disabled=True, in_maint=False) + esr_system_info = next(cloud_region.esr_system_infos) + assert isinstance(esr_system_info, EsrSystemInfo) + assert esr_system_info.esr_system_info_id == "c2d5e75d-56fd-47bc-af31-95607b26fa93" + assert esr_system_info.user_name == "test-devel" + assert esr_system_info.password == "test-devel" + assert esr_system_info.system_type == "openstack" + assert esr_system_info.resource_version == "1586436352654" + assert esr_system_info.system_name is None + assert esr_system_info.esr_type is None + assert esr_system_info.vendor is None + assert esr_system_info.version is None + assert esr_system_info.service_url == "http://keystone:5000/v3" + assert esr_system_info.protocol is None + assert esr_system_info.ssl_cacert is None + assert esr_system_info.ssl_insecure is None + assert esr_system_info.ip_address is None + assert esr_system_info.port is None + assert esr_system_info.cloud_domain == "Default" + assert esr_system_info.default_tenant is None + assert esr_system_info.passive is None + assert esr_system_info.remote_path is None + assert esr_system_info.system_status is None + assert esr_system_info.openstack_region_id is None + +@mock.patch.object(Complex, "send_message") +def test_create_complex(mock_send): + """Test complex creation""" + cmplx = Complex.create( + name="test complex", + physical_location_id="somewhere", + data_center_code="5555", + physical_location_type="test", + city="Test City", + postal_code="55555", + region="Test region", + elevation="TestElevation", + ) + + assert cmplx.name == "test complex" + assert cmplx.physical_location_id == "somewhere" + assert cmplx.identity_url == "" + assert cmplx.physical_location_type == "test" + assert cmplx.street1 == "" + assert cmplx.street2 == "" + assert cmplx.city == "Test City" + assert cmplx.state == "" + assert cmplx.postal_code == "55555" + assert cmplx.country == "" + assert cmplx.region == "Test region" + assert cmplx.latitude == "" + assert cmplx.longitude == "" + assert cmplx.elevation == "TestElevation" + assert cmplx.lata == "" + + +@mock.patch.object(Complex, "send_message_json") +def text_get_all_complexes(mock_send): + """Test get_all Complex class method.""" + mock_send.return_value = {} + assert len(list(Complex.get_all())) == 0 + + mock_send.return_value = COMPLEXES + assert len(list(Complex.get_all())) == 2 + + +def test_filter_none_value(): + """Test method to filter out None value keys from dictionary.""" + ret: dict = AaiResource.filter_none_key_values({"a": None}) + assert not ret + + ret: dict = AaiResource.filter_none_key_values({"a": "b", "c": None}) + assert ret == {"a": "b"} + + ret: dict = AaiResource.filter_none_key_values({"a": "b", "c": "d"}) + assert ret == {"a": "b", "c": "d"} + + +@mock.patch.object(AaiResource, "send_message") +def test_add_relationship(mock_send): + """Test add_relationship method.""" + cloud_region = CloudRegion(cloud_owner="tester", cloud_region_id="test", + orchestration_disabled=True, in_maint=False) + cloud_region.add_relationship(Relationship(related_to="test", + related_link="test", + relationship_data={})) + + +# # ----------------------------------------------------------------------------- +# def test_check_aai_resource_service(): +# """Test that a given service instance is in A&AI.""" +# pass + +# def test_check_aai_resource_service_not_found(): +# """Test that a given service instance is not in A&AI (cleaned).""" +# pass + +# def test_check_aai_resource_vnf(): +# """Test that a given vnf is in A&AI.""" +# pass + +# def test_check_aai_resource_vnf_not_found(): +# """Test that a given vnf is not in A&AI (cleaned).""" +# pass + +# def test_check_aai_resource_module(): +# """Test that a given module is in A&AI.""" +# pass + +# def test_check_aai_resource_module_not_found(): +# """Test that a given module is not in A&AI (cleaned).""" +# pass + +# def test_check_aai_net_module(): +# """Test that a given net is in A&AI.""" +# pass + +# def test_check_aai_resource_net_not_found(): +# """Test that a given net is not in A&AI (cleaned).""" +# pass + + +# pylint: disable=C0301 +SIMPLE_MODEL = { + "model": [ + { + "model-invariant-id": "1234567890", + "model-type": "generic", + "resource-version": "1561218640404", + } + ] +} +# pylint: enable=C0301 + + +def test_service_url(): + """Test service property""" + service = Service("12345", "description", "version1.0") + assert service.url == (f"{service.base_url}{service.api_version}/service-design-and-creation/services/service/" + f"{service.service_id}?resource-version={service.resource_version}") + + +@mock.patch.object(Service, 'send_message') +def test_service_create(mock_send): + """Test service creation""" + Service.create("1234", "description") + mock_send.assert_called_once() + method, description, url = mock_send.call_args[0] + assert method == "PUT" + assert description == "Create A&AI service" + assert url == (f"{Service.base_url}{Service.api_version}/service-design-and-creation/" + f"services/service/1234") + + +def test_model_init(): + """Test model initailization""" + model = Model("12345", "ubuntu", "version16") + assert isinstance(model, Model) + + +def test_model_url(): + """Test Model's url property""" + model = Model("12345", "ubuntu", "version16") + assert model.url == (f"{model.base_url}{model.api_version}/service-design-and-creation/models/" + f"model/{model.invariant_id}?resource-version={model.resource_version}") + + +@mock.patch.object(Model, 'send_message_json') +def test_zero_model_get_all(mock_send_message_json): + """Test get_all Model class method""" + mock_send_message_json.return_value = {} + Model.get_all() + assert len(list(Model.get_all())) == 0 + + +@mock.patch.object(Model, 'send_message_json') +def test_model_get_all(mock_send_message_json): + """Test get_all Model class method""" + mock_send_message_json.return_value = SIMPLE_MODEL + Model.get_all() + assert len(list(Model.get_all())) == 1 + model_1 = next(Model.get_all()) + assert model_1.invariant_id == "1234567890" + assert model_1.model_type == "generic" + assert model_1.resource_version == "1561218640404" + mock_send_message_json.assert_called_with("GET", 'Get A&AI sdc models', mock.ANY) + + +@mock.patch.object(CloudRegion, "send_message_json") +@mock.patch.object(Complex, "get_by_physical_location_id") +def test_cloud_region_complex_property(mock_complex_get_by_physical_location_id, mock_send): + """Test cloud region complex property.""" + mock_send.return_value = {} + cloud_region = CloudRegion(cloud_owner="tester", cloud_region_id="test", + orchestration_disabled=True, in_maint=False) + assert cloud_region.complex is None + mock_send.return_value = CLOUD_REGION_RELATIONSHIP + assert cloud_region.complex is not None + assert mock_complex_get_by_physical_location_id.called_once_with("integration_test_complex") diff --git a/tests/test_aai_service_instance.py b/tests/test_aai_service_instance.py new file mode 100644 index 0000000..9f84dfd --- /dev/null +++ b/tests/test_aai_service_instance.py @@ -0,0 +1,275 @@ +# Copyright 2022 Orange, 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 unittest import mock + +import pytest + +from onapsdk.aai.aai_element import AaiElement +from onapsdk.aai.business import ServiceInstance +from onapsdk.so.deletion import ServiceDeletionRequest +from onapsdk.so.instantiation import NetworkInstantiation, VnfInstantiation +from onapsdk.exceptions import StatusError + + +RELATIONSHIPS_VNF = { + "relationship": [ + { + "related-to": "generic-vnf", + "relationship_label": "anything", + "related_link": "test_relationship_related_link", + "relationship_data": [] + } + ] +} + + +RELATIONSHIPS_NETWORK = { + "relationship": [ + { + "related-to": "l3-network", + "relationship-label": "anything", + "related-link": "related_link", + "relationship-data": [] + } + ] +} + + +NETWORK_INSTANCE = { + 'network-id': '49dab38b-3a5b-47e5-9cd6-b8d069d6109d', + 'network-name': 'Python_ONAP_SDK_network_instance_0b4308ca-3fe0-4af1-9c4e-ed2c816b9496', + 'is-bound-to-vpn': False, + 'resource-version': '1593162237842', + 'orchestration-status': 'Inventoried', + 'model-invariant-id': 'cdbb2169-e638-4aab-a4e9-b9d2d6d62b04', + 'model-version-id': '51789f7b-5ffc-4c12-ac87-02363fdb40b1', + 'model-customization-id': 'db9c9a6c-2a1c-4cdd-8fbc-e10448d0e4cc', + 'is-provider-network': False, + 'is-shared-network': False, + 'is-external-network': False, + 'relationship-list': { + 'relationship': [ + { + 'related-to': 'service-instance', + 'relationship-label': 'org.onap.relationships.inventory.ComposedOf', + 'related-link': '/aai/v19/business/customers/customer/TestCustomer/service-subscriptions/service-subscription/vFW_with_net/service-instances/service-instance/72fd9ee9-077f-4d3d-8e86-08ed24514802', + 'relationship-data': [ + { + 'relationship-key': 'customer.global-customer-id', + 'relationship-value': 'TestCustomer' + }, + { + 'relationship-key': 'service-subscription.service-type', + 'relationship-value': 'vFW_with_net' + }, + { + 'relationship-key': 'service-instance.service-instance-id', + 'relationship-value': '72fd9ee9-077f-4d3d-8e86-08ed24514802' + } + ], + 'related-to-property': [ + { + 'property-key': 'service-instance.service-instance-name', + 'property-value': 'Python_ONAP_SDK_service_instance_7be66d06-c466-46cf-b84a-cd7af2d633ed' + } + ] + }, + { + 'related-to': 'cloud-region', + 'relationship-label': 'org.onap.relationships.inventory.Uses', + 'related-link': '/aai/v19/cloud-infrastructure/cloud-regions/cloud-region/TestCloudOwner/RegionOne', + 'relationship-data': [ + { + 'relationship-key': 'cloud-region.cloud-owner', + 'relationship-value': 'TestCloudOwner' + }, + { + 'relationship-key': 'cloud-region.cloud-region-id', + 'relationship-value': 'RegionOne' + } + ], + 'related-to-property': [ + { + 'property-key': 'cloud-region.owner-defined-type', + 'property-value': '' + } + ] + }, + { + 'related-to': 'line-of-business', + 'relationship-label': 'org.onap.relationships.inventory.Uses', + 'related-link': '/aai/v19/business/lines-of-business/line-of-business/Test-BusinessLine', + 'relationship-data': [ + { + 'relationship-key': 'line-of-business.line-of-business-name', + 'relationship-value': 'Test-BusinessLine' + } + ] + }, + { + 'related-to': 'tenant', + 'relationship-label': 'org.onap.relationships.inventory.Uses', + 'related-link': '/aai/v19/cloud-infrastructure/cloud-regions/cloud-region/TestCloudOwner/RegionOne/tenants/tenant/89788fdf49514f94963b12a6c0cfdc71', + 'relationship-data': [ + { + 'relationship-key': 'cloud-region.cloud-owner', + 'relationship-value': 'TestCloudOwner' + }, + { + 'relationship-key': 'cloud-region.cloud-region-id', + 'relationship-value': 'RegionOne' + }, + { + 'relationship-key': 'tenant.tenant-id', + 'relationship-value': '89788fdf49514f94963b12a6c0cfdc71' + } + ], + 'related-to-property': [ + { + 'property-key': 'tenant.tenant-name', + 'property-value': 'test-tenant' + } + ] + }, + { + 'related-to': 'platform', + 'relationship-label': 'org.onap.relationships.inventory.Uses', + 'related-link': '/aai/v19/business/platforms/platform/Test-Platform', + 'relationship-data': [ + { + 'relationship-key': 'platform.platform-name', + 'relationship-value': 'Test-Platform' + } + ] + } + ] + } +} + + +COUNT = { + "results":[ + { + "service-instance":29 + } + ] +} + + +def test_service_instance(): + service_subscription = mock.MagicMock() + service_subscription.url = "test_url" + service_instance = ServiceInstance(service_subscription=service_subscription, + instance_id="test_service_instance_id") + assert service_instance.url == (f"{service_instance.service_subscription.url}/service-instances/" + f"service-instance/{service_instance.instance_id}") + + +@mock.patch.object(ServiceInstance, "send_message_json") +def test_service_instance_vnf_instances(mock_relationships_send_message_json): + service_instance = ServiceInstance(service_subscription=mock.MagicMock(), + instance_id="test_service_instance_id") + mock_relationships_send_message_json.return_value = {"relationship": []} + assert len(list(service_instance.vnf_instances)) == 0 + mock_relationships_send_message_json.return_value = RELATIONSHIPS_VNF + assert len(list(service_instance.vnf_instances)) == 1 + + +@mock.patch.object(AaiElement, "send_message_json") +def test_service_instance_network_instances(mock_aai_element_send_message_json): + service_instance = ServiceInstance(service_subscription=mock.MagicMock(), + instance_id="test_service_instance_id") + mock_aai_element_send_message_json.side_effect = [RELATIONSHIPS_NETWORK, NETWORK_INSTANCE] + assert len(list(service_instance.network_instances)) == 1 + + +@mock.patch.object(VnfInstantiation, "instantiate_ala_carte") +@mock.patch.object(ServiceInstance, "sdc_service", new_callable=mock.PropertyMock) +def test_service_instance_add_vnf(mock_sdc_service, mock_vnf_instantiation): + service_instance = ServiceInstance(service_subscription=mock.MagicMock(), + instance_id="test_service_instance_id") + service_instance.orchestration_status = "Inactive" + with pytest.raises(StatusError) as exc: + service_instance.add_vnf(mock.MagicMock(), + mock.MagicMock(), + mock.MagicMock()) + assert exc.type == StatusError + service_instance.orchestration_status = "Active" + service_instance.add_vnf(mock.MagicMock(), + mock.MagicMock(), + mock.MagicMock()) + mock_vnf_instantiation.assert_called_once() + + +@mock.patch.object(VnfInstantiation, "instantiate_macro") +@mock.patch.object(ServiceInstance, "sdc_service", new_callable=mock.PropertyMock) +def test_service_instance_add_vnf_macro(mock_sdc_service, mock_vnf_instantiation): + service_instance = ServiceInstance(service_subscription=mock.MagicMock(), + instance_id="test_service_instance_id") + service_instance.orchestration_status = "Inactive" + with pytest.raises(StatusError) as exc: + service_instance.add_vnf(mock.MagicMock(), + mock.MagicMock(), + mock.MagicMock(), + a_la_carte=False) + assert exc.type == StatusError + service_instance.orchestration_status = "Active" + service_instance.add_vnf(mock.MagicMock(), + mock.MagicMock(), + mock.MagicMock(), + a_la_carte=False) + mock_vnf_instantiation.assert_called_once() + + +@mock.patch.object(NetworkInstantiation, "instantiate_ala_carte") +@mock.patch.object(ServiceInstance, "sdc_service", new_callable=mock.PropertyMock) +def test_service_instance_add_network(mock_sdc_service, mock_network_instantiation): + service_instance = ServiceInstance(service_subscription=mock.MagicMock(), + instance_id="test_service_instance_id") + service_instance.orchestration_status = "Inactive" + with pytest.raises(StatusError) as exc: + service_instance.add_network(mock.MagicMock(), + mock.MagicMock(), + mock.MagicMock()) + assert exc.type == StatusError + service_instance.orchestration_status = "Active" + service_instance.add_network(mock.MagicMock(), + mock.MagicMock(), + mock.MagicMock()) + mock_network_instantiation.assert_called_once() + + +@mock.patch.object(ServiceDeletionRequest, "send_request") +@mock.patch.object(ServiceInstance, "sdc_service", new_callable=mock.PropertyMock) +def test_service_instance_deletion(mock_sdc_service, mock_service_deletion_request): + service_instance = ServiceInstance(service_subscription=mock.MagicMock(), + instance_id="test_service_instance_id") + service_instance.delete() + mock_service_deletion_request.assert_called_once_with(service_instance, True) + + +@mock.patch("onapsdk.aai.business.service.Service.get_by_unique_uuid") +def test_service_instance_sdc_service(mock_service_get_by_unique_uuid): + si = ServiceInstance(service_subscription=mock.MagicMock(), + instance_id="test_service_instance_id", + model_invariant_id="1234") + si.sdc_service + mock_service_get_by_unique_uuid.assert_called_once_with("1234") + si.sdc_service + mock_service_get_by_unique_uuid.assert_called_once_with("1234") + +@mock.patch.object(ServiceInstance, "send_message_json") +def test_service_instance_count(mock_send_message_json): + mock_send_message_json.return_value = COUNT + assert ServiceInstance.count(service_subscription=mock.MagicMock()) diff --git a/tests/test_aai_service_subscription.py b/tests/test_aai_service_subscription.py new file mode 100644 index 0000000..3c7eb91 --- /dev/null +++ b/tests/test_aai_service_subscription.py @@ -0,0 +1,191 @@ +# Copyright 2022 Orange, 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 unittest import mock + +from onapsdk.aai.business import Customer, ServiceSubscription, ServiceInstance +from onapsdk.aai.cloud_infrastructure import CloudRegion + + +SERVICE_INSTANCES = { + "service-instance":[ + { + "service-instance-id":"5410bf79-2aa3-450e-a324-ec5630dc18cf", + "service-instance-name":"test", + "environment-context":"General_Revenue-Bearing", + "workload-context":"Production", + "model-invariant-id":"2a51a89b-6f94-4417-8831-c468fb30ed02", + "model-version-id":"92a82807-b483-4579-86b1-c79b1286aab4", + "resource-version":"1589457727708", + "orchestration-status":"Active", + "relationship-list":{ + "relationship":[ + { + "related-to":"owning-entity", + "relationship-label":"org.onap.relationships.inventory.BelongsTo", + "related-link":"/aai/v16/business/owning-entities/owning-entity/ff6c945f-89ab-4f14-bafd-0cdd6eac791a", + "relationship-data":[ + { + "relationship-key":"owning-entity.owning-entity-id", + "relationship-value":"ff6c945f-89ab-4f14-bafd-0cdd6eac791a" + } + ] + }, + { + "related-to":"project", + "relationship-label":"org.onap.relationships.inventory.Uses", + "related-link":"/aai/v16/business/projects/project/python_onap_sdk_project", + "relationship-data":[ + { + "relationship-key":"project.project-name", + "relationship-value":"python_onap_sdk_project" + } + ] + } + ] + } + } + ] +} + + +MULTIPLE_CLOUD_REGIONS_AND_TENATS_RELATIONSHIP = { + "relationship":[ + { + "related-to":"tenant", + "relationship-label":"org.onap.relationships.inventory.Uses", + "related-link":"/aai/v19/cloud-infrastructure/cloud-regions/cloud-region/DT/RegionOne/tenants/tenant/8fa33ca96caa4172aeeeefd1dbf5c715", + "relationship-data":[ + { + "relationship-key":"cloud-region.cloud-owner", + "relationship-value":"DT" + }, + { + "relationship-key":"cloud-region.cloud-region-id", + "relationship-value":"RegionOne" + }, + { + "relationship-key":"tenant.tenant-id", + "relationship-value":"8fa33ca96caa4172aeeeefd1dbf5c715" + } + ], + "related-to-property":[ + { + "property-key":"tenant.tenant-name", + "property-value":"ci-onap-master-vnfs" + } + ] + }, + { + "related-to":"tenant", + "relationship-label":"org.onap.relationships.inventory.Uses", + "related-link":"/aai/v19/cloud-infrastructure/cloud-regions/cloud-region/test_cloud_owner/test_cloud_region_id/tenants/tenant/1234", + "relationship-data":[ + { + "relationship-key":"cloud-region.cloud-owner", + "relationship-value":"test_cloud_owner" + }, + { + "relationship-key":"cloud-region.cloud-region-id", + "relationship-value":"test_cloud_region_id" + }, + { + "relationship-key":"tenant.tenant-id", + "relationship-value":"1234" + } + ], + "related-to-property":[ + { + "property-key":"tenant.tenant-name", + "property-value":"test_tenant" + } + ] + } + ] +} + + +COUNT = { + "results":[ + { + "service-subscription":1 + } + ] +} + + +@mock.patch.object(ServiceSubscription, "send_message_json") +def test_get_service_instance_by_filter_parameter(mock_send_message_json): + """Test Service Subscription get_service_instance_by_filter_parameter method""" + customer = Customer("generic", "generic", "INFRA") + service_subscription = ServiceSubscription(customer=customer, + service_type="test_service_type", + resource_version="test_resource_version") + mock_send_message_json.return_value = SERVICE_INSTANCES + service_instance = service_subscription._get_service_instance_by_filter_parameter(filter_parameter_name="service-instance-id", filter_parameter_value="5410bf79-2aa3-450e-a324-ec5630dc18cf") + assert service_instance.instance_name == "test" + assert service_instance.instance_id == "5410bf79-2aa3-450e-a324-ec5630dc18cf" + + +@mock.patch.object(ServiceSubscription, "_get_service_instance_by_filter_parameter") +def test_get_service_instance_by_id(mock_get): + """Test Service Subscription get_service_instance_by_id method""" + service_subscription = ServiceSubscription(customer=None, + service_type="test_service_type", + resource_version="test_resource_version") + mock_get.return_value = ServiceInstance(service_subscription="ServiceSubscription", + instance_id="5410bf79-2aa3-450e-a324-ec5630dc18cf") + service_instance = service_subscription.get_service_instance_by_id(service_instance_id="5410bf79-2aa3-450e-a324-ec5630dc18cf") + assert service_instance.instance_id == "5410bf79-2aa3-450e-a324-ec5630dc18cf" + + +@mock.patch.object(ServiceSubscription, "_get_service_instance_by_filter_parameter") +def test_get_service_instance_by_name(mock_get): + """Test Service Subscription get_service_instance_by_name method""" + service_subscription = ServiceSubscription(customer=None, + service_type="test_service_type", + resource_version="test_resource_version") + mock_get.return_value = ServiceInstance(service_subscription="ServiceSubscription", + instance_id="5410bf79-2aa3-450e-a324-ec5630dc18cf", + instance_name="test") + service_instance = service_subscription.get_service_instance_by_name(service_instance_name="test") + assert service_instance.instance_name == "test" + + +@mock.patch.object(ServiceSubscription, "send_message_json") +@mock.patch.object(CloudRegion, "get_by_id") +def test_cloud_regions(mock_cloud_region_get_by_id, mock_send_message_json): + """Test service subscription `cloud_regions` property""" + service_subscription = ServiceSubscription(customer=mock.MagicMock(), + service_type="test_service_type", + resource_version="test_resource_version") + mock_send_message_json.return_value = MULTIPLE_CLOUD_REGIONS_AND_TENATS_RELATIONSHIP + assert len(list(service_subscription.cloud_regions)) == 2 + assert len(mock_cloud_region_get_by_id.mock_calls) == 2 + + +@mock.patch.object(ServiceSubscription, "send_message_json") +@mock.patch.object(CloudRegion, "get_by_id") +@mock.patch.object(CloudRegion, "get_tenant") +def test_tenants(mock_cloud_region_get_tenant, mock_cloud_region_get_by_id, mock_send_message_json): + """Test service subscription `tenants` property""" + service_subscription = ServiceSubscription(customer=mock.MagicMock(), + service_type="test_service_type", + resource_version="test_resource_version") + mock_send_message_json.return_value = MULTIPLE_CLOUD_REGIONS_AND_TENATS_RELATIONSHIP + assert len(list(service_subscription.tenants)) == 2 + +@mock.patch.object(ServiceSubscription, "send_message_json") +def test_service_subscription_count(mock_send_message_json): + mock_send_message_json.return_value = COUNT + assert ServiceSubscription.count(customer=mock.MagicMock()) == 1 diff --git a/tests/test_aai_site_resource.py b/tests/test_aai_site_resource.py new file mode 100644 index 0000000..68dd556 --- /dev/null +++ b/tests/test_aai_site_resource.py @@ -0,0 +1,57 @@ +# Copyright 2022 Orange, 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 unittest.mock import patch + +from onapsdk.aai.network.site_resource import SiteResource + +SITE_RESOURCE = { + "site-resource-id":"123", + "resource-version":"213" +} + +SITE_RESOURCES = { + "site-resource":[ + SITE_RESOURCE, + { + "site-resource-id":"321", + "resource-version":"312" + } + ] +} + +@patch("onapsdk.aai.network.site_resource.SiteResource.send_message_json") +def test_site_resource_get_all(mock_send_message_json): + assert len(list(SiteResource.get_all())) == 0 + mock_send_message_json.return_value = SITE_RESOURCES + site_resources = list(SiteResource.get_all()) + assert len(site_resources) == 2 + sr1, sr2 = site_resources + assert sr1.site_resource_id == "123" + assert sr1.resource_version == "213" + assert sr2.site_resource_id == "321" + assert sr2.resource_version == "312" + +@patch("onapsdk.aai.network.site_resource.SiteResource.send_message_json") +def test_site_resource_get_by_id(mock_send_message_json): + mock_send_message_json.return_value = SITE_RESOURCE + sr = SiteResource.get_by_site_resource_id("123") + assert sr.site_resource_id == "123" + assert sr.resource_version == "213" + +@patch("onapsdk.aai.network.site_resource.SiteResource.send_message") +@patch("onapsdk.aai.network.site_resource.SiteResource.get_by_site_resource_id") +def test_site_resource_create(mock_get_by_site_resource_id, mock_send_message): + SiteResource.create("123") + mock_send_message.assert_called_once() + assert mock_get_by_site_resource_id.called_once_with("123") diff --git a/tests/test_aai_vf_module.py b/tests/test_aai_vf_module.py new file mode 100644 index 0000000..7f3b8a6 --- /dev/null +++ b/tests/test_aai_vf_module.py @@ -0,0 +1,83 @@ +# Copyright 2022 Orange, 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 unittest import mock + +import pytest + +from onapsdk.aai.business import VfModuleInstance +from onapsdk.so.deletion import VfModuleDeletionRequest +from onapsdk.exceptions import ResourceNotFound + + +COUNT = { + "results":[ + { + "vf-module":1 + } + ] +} + + +def test_vf_module(): + vnf_instance = mock.MagicMock() + vnf_instance.url = "test_url" + vf_module_instance = VfModuleInstance(vnf_instance=vnf_instance, + vf_module_id="test_vf_module_id", + is_base_vf_module=True, + automated_assignment=False) + + assert vf_module_instance.url == (f"{vf_module_instance.vnf_instance.url}/vf-modules/" + f"vf-module/{vf_module_instance.vf_module_id}") + + +@mock.patch.object(VfModuleDeletionRequest, "send_request") +def test_vf_module_deletion(mock_deletion_request): + vf_module_instance = VfModuleInstance(vnf_instance=mock.MagicMock(), + vf_module_id="test_vf_module_id", + is_base_vf_module=True, + automated_assignment=False) + vf_module_instance.delete() + mock_deletion_request.assert_called_once_with(vf_module_instance, True) + + +def test_vnf_vf_module(): + """Test VfModudleInstance's vf_module property""" + vnf_instance = mock.MagicMock() + vnf_instance.vnf = mock.MagicMock() + + vf_module = mock.MagicMock() + vf_module.model_version_id = "test_model_version_id" + + vf_module_instance = VfModuleInstance(vnf_instance=vnf_instance, + model_version_id="test_model_version_id", + vf_module_id="test_vf_module_id", + is_base_vf_module=True, + automated_assignment=False) + + vnf_instance.vnf.vf_modules = [] + with pytest.raises(ResourceNotFound) as exc: + vf_module_instance.vf_module + assert exc.type == ResourceNotFound + assert vf_module_instance._vf_module is None + + vnf_instance.vnf.vf_modules = [vf_module] + + assert vf_module == vf_module_instance.vf_module + assert vf_module_instance._vf_module is not None + assert vf_module_instance.vf_module == vf_module_instance._vf_module + +@mock.patch.object(VfModuleInstance, "send_message_json") +def test_vf_module_instance_count(mock_send_message_json): + mock_send_message_json.return_value = COUNT + assert VfModuleInstance.count(vnf_instance=mock.MagicMock()) == 1 diff --git a/tests/test_aai_vnf.py b/tests/test_aai_vnf.py new file mode 100644 index 0000000..4dea6a8 --- /dev/null +++ b/tests/test_aai_vnf.py @@ -0,0 +1,449 @@ +"""Test A&AI VNF module.""" +# Copyright 2022 Orange, 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 unittest import mock + +import pytest + +from onapsdk.aai.business import ServiceInstance, VnfInstance, PnfInstance, VfModuleInstance +from onapsdk.so.deletion import VnfDeletionRequest +from onapsdk.so.instantiation import VfModuleInstantiation, VnfInstantiation, SoService +from onapsdk.exceptions import ResourceNotFound, StatusError + + +VNF_INSTANCE = { + "vnf-id":"6d644ab5-254d-4a49-98fe-0f481c099f1a", + "vnf-name":"Python_ONAP_SDK_vnf_instance_14856120-e946-46ce-bf5f-384b20209f9c", + "vnf-type":"testService11/testVF11 0", + "service-id":"1234", + "prov-status":"PREPROV", + "orchestration-status":"Inventoried", + "in-maint":True, + "is-closed-loop-disabled":False, + "resource-version":"1590395148980", + "model-invariant-id":"a3285832-77d5-4ab2-95c5-217070de77c9", + "model-version-id":"0da841b9-f787-4ce0-9227-a23092a4a035", + "model-customization-id":"9426293e-bc5d-4fd3-8236-85190f1142aa", + "selflink":"restconf/config/GENERIC-RESOURCE-API:services/service/5410bf79-2aa3-450e-a324-ec5630dc18cf/service-data/vnfs/vnf/6d644ab5-254d-4a49-98fe-0f481c099f1a/vnf-data/vnf-topology/", + "relationship-list":{ + "relationship":[ + { + "related-to":"tenant", + "relationship-label":"org.onap.relationships.inventory.BelongsTo", + "related-link":"/aai/v19/cloud-infrastructure/cloud-regions/cloud-region/DT/RegionOne/tenants/tenant/89788fdf49514f94963b12a6c0cfdc71", + "relationship-data":[ + { + "relationship-key":"cloud-region.cloud-owner", + "relationship-value":"DT" + }, + { + "relationship-key":"cloud-region.cloud-region-id", + "relationship-value":"RegionOne" + }, + { + "relationship-key":"tenant.tenant-id", + "relationship-value":"89788fdf49514f94963b12a6c0cfdc71" + } + ], + "related-to-property":[ + { + "property-key":"tenant.tenant-name", + "property-value":"onap-devel" + } + ] + }, + { + "related-to":"cloud-region", + "relationship-label":"org.onap.relationships.inventory.LocatedIn", + "related-link":"/aai/v19/cloud-infrastructure/cloud-regions/cloud-region/DT/RegionOne", + "relationship-data":[ + { + "relationship-key":"cloud-region.cloud-owner", + "relationship-value":"DT" + },{ + "relationship-key":"cloud-region.cloud-region-id", + "relationship-value":"RegionOne" + } + ], + "related-to-property":[ + { + "property-key":"cloud-region.owner-defined-type", + "property-value":"" + } + ] + }, + { + "related-to":"service-instance", + "relationship-label":"org.onap.relationships.inventory.ComposedOf", + "related-link":"/aai/v19/business/customers/customer/generic/service-subscriptions/service-subscription/testService11/service-instances/service-instance/5410bf79-2aa3-450e-a324-ec5630dc18cf", + "relationship-data":[ + { + "relationship-key":"customer.global-customer-id", + "relationship-value":"generic" + }, + { + "relationship-key":"service-subscription.service-type", + "relationship-value":"testService11" + }, + { + "relationship-key":"service-instance.service-instance-id", + "relationship-value":"5410bf79-2aa3-450e-a324-ec5630dc18cf" + } + ], + "related-to-property":[ + { + "property-key":"service-instance.service-instance-name", + "property-value":"test22" + } + ] + }, + { + "related-to":"availability-zone", + "relationship-label":"org.onap.relationships.inventory.Uses", + "related-link":"/aai/v19/cloud-infrastructure/cloud-regions/cloud-region/DT/RegionOne/availability-zones/availability-zone/nova", + "relationship-data":[ + { + "relationship-key":"cloud-region.cloud-owner", + "relationship-value":"DT" + }, + { + "relationship-key":"cloud-region.cloud-region-id", + "relationship-value":"RegionOne" + }, + { + "relationship-key":"availability-zone.availability-zone-name", + "relationship-value":"nova" + } + ] + }, + { + "related-to":"availability-zone", + "relationship-label":"org.onap.relationships.inventory.Uses", + "related-link":"/aai/v19/cloud-infrastructure/cloud-regions/cloud-region/DT/RegionOne/availability-zones/availability-zone/brittany", + "relationship-data":[ + { + "relationship-key":"cloud-region.cloud-owner", + "relationship-value":"DT" + }, + { + "relationship-key":"cloud-region.cloud-region-id", + "relationship-value":"RegionOne" + }, + { + "relationship-key":"availability-zone.availability-zone-name", + "relationship-value":"brittany" + } + ] + }, + { + "related-to":"platform", + "relationship-label":"org.onap.relationships.inventory.Uses", + "related-link":"/aai/v19/business/platforms/platform/Python_ONAPSDK_Platform", + "relationship-data":[ + { + "relationship-key":"platform.platform-name", + "relationship-value":"Python_ONAPSDK_Platform" + } + ] + }, + { + "related-to":"line-of-business", + "relationship-label":"org.onap.relationships.inventory.Uses", + "related-link":"/aai/v19/business/lines-of-business/line-of-business/Python_ONAPSDK_LineOfBusiness", + "relationship-data":[ + { + "relationship-key":"line-of-business.line-of-business-name", + "relationship-value":"Python_ONAPSDK_LineOfBusiness" + } + ] + } + ] + } +} + + +VF_MODULE = { + "vf-module": [ + { + "vf-module-id": "test-module-id", + "is-base-vf-module": True, + "automated-assignment": False, + "vf-module-name": "test_vf_module", + "heat-stack-id": "test_heat_stack_id", + "orchestration-status": "test_orchestration_status", + "resource-version": "1590395148980", + "model-invariant-id": "test_model_invariant_id", + "model-version-id": "test_model_version_id" + } + ] +} + + +COUNT = { + "results":[ + { + "generic-vnf":17 + } + ] +} + + +@mock.patch.object(VnfDeletionRequest, "send_request") +def test_vnf_instance(mock_vnf_deletion_request): + service_instance = ServiceInstance(None, + instance_id="test_service_instance_id") + vnf_instance = VnfInstance(service_instance, + vnf_id="test_vnf_id", + vnf_type="test_vnf_type", + in_maint=False, + is_closed_loop_disabled=True) + assert vnf_instance.service_instance == service_instance + assert vnf_instance.vnf_id == "test_vnf_id" + assert vnf_instance.vnf_type == "test_vnf_type" + assert vnf_instance.in_maint is False + assert vnf_instance.is_closed_loop_disabled is True + assert vnf_instance._vnf is None + assert vnf_instance.url == (f"{vnf_instance.base_url}{vnf_instance.api_version}/network/" + f"generic-vnfs/generic-vnf/{vnf_instance.vnf_id}") + vnf_instance.delete() + mock_vnf_deletion_request.assert_called_once_with(vnf_instance, True) + + +@mock.patch.object(VnfInstance, "send_message_json") +def test_vnf_instance_vf_modules(mock_vnf_instance_send_message_json): + service_instance = mock.MagicMock() + vnf_instance = VnfInstance(service_instance, + vnf_id="test_vnf_id", + vnf_type="test_vnf_type", + in_maint=False, + is_closed_loop_disabled=True) + mock_vnf_instance_send_message_json.return_value = {"vf-module": []} + vf_modules = list(vnf_instance.vf_modules) + assert len(vf_modules) == 0 + + mock_vnf_instance_send_message_json.return_value = VF_MODULE + vf_modules = list(vnf_instance.vf_modules) + assert len(vf_modules) == 1 + + +def test_vnf_instance_vnf(): + service_instance = mock.MagicMock() + vnf_instance = VnfInstance(service_instance, + vnf_id="test_vnf_id", + vnf_type="test_vnf_type", + in_maint=False, + is_closed_loop_disabled=True, + model_version_id="test_model_version_id") + assert vnf_instance._vnf is None + service_instance.sdc_service.vnfs = [] + with pytest.raises(ResourceNotFound) as exc: + vnf_instance.vnf + assert exc.type == ResourceNotFound + assert vnf_instance._vnf is None + + vnf = mock.MagicMock() + vnf.model_version_id = "test_model_version_id" + service_instance.sdc_service.vnfs = [vnf] + assert vnf == vnf_instance.vnf + assert vnf_instance._vnf is not None + assert vnf_instance.vnf == vnf_instance._vnf + + +@mock.patch.object(VfModuleInstantiation, "instantiate_ala_carte") +def test_vnf_add_vf_module(mock_vf_module_instantiation): + vnf_instance = VnfInstance(mock.MagicMock(), + vnf_id="test_vnf_id", + vnf_type="test_vnf_type", + in_maint=False, + is_closed_loop_disabled=True, + model_version_id="test_model_version_id") + vnf_instance.add_vf_module(mock.MagicMock()) + mock_vf_module_instantiation.assert_called_once() + + +@mock.patch.object(VnfInstance, "_execute_so_action") +@mock.patch.object(VnfInstance, "vnf") +def test_vnf_update(mock_vnf, mock_vnf_instantiation): + + property_skip_true = mock.MagicMock() + property_skip_true.name = "skip_post_instantiation_configuration" + property_skip_true.value = "false" + + vnf_instance = mock.MagicMock() + vnf_instance.vnf = mock_vnf + vnf_instance.vnf.properties = (item for item in [property_skip_true]) + + vnf_instance = VnfInstance(vnf_instance, + vnf_id="test_vnf_id", + vnf_type="test_vnf_type", + in_maint=False, + is_closed_loop_disabled=True) + + vnf_instance.update([mock.MagicMock()]) + mock_vnf_instantiation.assert_called_once() + + property_skip_false = mock.MagicMock() + property_skip_false.name = "skip_post_instantiation_configuration" + property_skip_false.value = "true" + + vnf_instance2 = mock.MagicMock() + vnf_instance2.vnf = mock_vnf + vnf_instance2.vnf.properties = (item for item in [property_skip_false]) + + vnf_instance2 = VnfInstance(vnf_instance2, + vnf_id="test_vnf_id", + vnf_type="test_vnf_type", + in_maint=False, + is_closed_loop_disabled=True) + + with pytest.raises(StatusError): + vnf_instance2.update([mock.MagicMock()]) + + +@mock.patch.object(VnfInstance, "_execute_so_action") +def test_vnf_healthcheck(mock_vnf_instantiation): + + instance = mock.MagicMock() + vnf_instance = VnfInstance(instance, + vnf_id="test_vnf_id", + vnf_type="test_vnf_type", + in_maint=False, + is_closed_loop_disabled=True) + + vnf_instance.healthcheck() + mock_vnf_instantiation.assert_called_once() + + +@mock.patch.object(VnfInstance, "_build_so_input") +@mock.patch.object(VnfInstantiation, "so_action") +def test_vnf_execute_so_action(mock_build_so_input, mock_so_action): + + instance = mock.MagicMock() + + relation_1 = mock.MagicMock() + relation_1.related_to = "line-of-business" + relation_1.relationship_data = [{"relationship-value": "test"}] + relation_2 = mock.MagicMock() + relation_2.related_to = "platform" + relation_2.relationship_data = [{"relationship-value": "test"}] + + vnf_instance = VnfInstance(instance, + vnf_id="test_vnf_id", + vnf_type="test_vnf_type", + in_maint=False, + is_closed_loop_disabled=True) + + vnf_instance.service_instance = mock.MagicMock() + vnf_instance.service_instance.active = True + + type(vnf_instance).relationships = mock.PropertyMock(return_value=[relation_1, relation_2]) + + vnf_instance._execute_so_action(operation_type="test", + vnf_parameters=[mock.MagicMock()]) + mock_so_action.assert_called_once() + + vnf_instance.service_instance.active = False + with pytest.raises(StatusError): + vnf_instance._execute_so_action(operation_type="test", + vnf_parameters=[mock.MagicMock()]) + + +@mock.patch.object(VnfInstance, "send_message") +def test_build_so_input(mock_send_message): + + pnf = mock.MagicMock() + pnf.model_version_id = "test_pnf_model_version_id" + pnf.model_name = "test_model" + + vnf = mock.MagicMock() + vnf.model_version_id = "test_vnf_model_version_id" + vnf.model_name = "vnf_test_model" + + vf_module = mock.MagicMock() + vf_module.model_version_id = "test_vfm_model_version_id" + vf_module.model_name = "test..vfm_model..name" + + vnf.vf_modules = [vf_module] + + instance = mock.MagicMock() + instance.service_subscription = mock.MagicMock() + instance.service_subscription.service_type = "1234" + + instance.sdc_service.pnfs = [pnf] + instance.sdc_service.vnfs = [vnf] + + pnf_instance = PnfInstance(instance, + pnf_name="test_pnf", + in_maint=False, + model_version_id="test_pnf_model_version_id") + + vnf_instance = VnfInstance(instance, + vnf_name="test_name", + vnf_id="test_vnf_id", + vnf_type="test_vnf_type", + in_maint=False, + is_closed_loop_disabled=True, + model_version_id="test_vnf_model_version_id") + + vf_module_instance = VfModuleInstance(vnf_instance=vnf_instance, + vf_module_name="test_vfm_name", + model_version_id="test_vfm_model_version_id", + vf_module_id="test_vf_module_id", + is_base_vf_module=True, + automated_assignment=False) + + vnf_instance.vnf.vf_modules = [vf_module] + type(vnf_instance).vf_modules = mock.PropertyMock(return_value=[vf_module_instance]) + instance.pnfs = [pnf_instance] + instance.vnf_instances = [vnf_instance] + + test_so_input_no_params = vnf_instance._build_so_input() + + assert isinstance(test_so_input_no_params, SoService) + assert len(test_so_input_no_params.vnfs[0].parameters) == 0 + + vnf_param1 = mock.MagicMock() + vnf_param1.name = "test_name" + vnf_param1.value = "test_value" + + test_so_input = vnf_instance._build_so_input([vnf_param1]) + + assert isinstance(test_so_input, SoService) + assert test_so_input.subscription_service_type == "1234" + assert not test_so_input.instance_name + assert len(test_so_input.vnfs) == 1 + + test_so_input_vnf = test_so_input.vnfs[0] + + assert test_so_input_vnf.model_name == "vnf_test_model" + assert test_so_input_vnf.instance_name == "test_name" + assert len(test_so_input_vnf.parameters) == 1 + assert test_so_input_vnf.parameters["test_name"] == "test_value" + assert len(test_so_input_vnf.vf_modules) == 1 + + test_so_input_vnf_vf_module = test_so_input_vnf.vf_modules[0] + + assert test_so_input_vnf_vf_module.model_name == "vfm_model" + assert test_so_input_vnf_vf_module.instance_name == "test_vfm_name" + assert len(test_so_input_vnf_vf_module.parameters) == 0 + + assert len(test_so_input.pnfs) == 1 + assert test_so_input.pnfs[0].model_name == "test_model" + assert test_so_input.pnfs[0].instance_name == "test_pnf" + +@mock.patch.object(VnfInstance, "send_message_json") +def test_vnf_instance_mock(mock_send_message_json): + mock_send_message_json.return_value = COUNT + assert VnfInstance.count() == 17 diff --git a/tests/test_cds.py b/tests/test_cds.py new file mode 100644 index 0000000..b7859f3 --- /dev/null +++ b/tests/test_cds.py @@ -0,0 +1,435 @@ +# Copyright 2022 Orange, 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 json +import os.path +from pathlib import Path +from tempfile import TemporaryDirectory +from unittest.mock import MagicMock, patch, PropertyMock, mock_open + +from pytest import raises + +from onapsdk.cds.blueprint import Blueprint, Mapping, MappingSet, ResolvedTemplate, Workflow +from onapsdk.cds.blueprint_processor import Blueprintprocessor +from onapsdk.cds.cds_element import CdsElement +from onapsdk.cds.data_dictionary import DataDictionary, DataDictionarySet +from onapsdk.exceptions import FileError, ParameterError, RequestError, ValidationError +from onapsdk.utils.gui import GuiItem, GuiList + +DD_1 = { + "name": "vf-module-name", + "tags": "vf-module-name", + "data_type": "string", + "description": "vf-module-name", + "entry_schema": "string", + "updatedBy": "Singal, Kapil <ks220y@att.com>", + "definition": { + "tags": "vf-module-name", + "name": "vf-module-name", + "property": { + "description": "vf-module-name", + "type": "string" + }, + "updated-by": "Singal, Kapil <ks220y@att.com>", + "sources": { + "input": { + "type": "source-input" + }, + "default": { + "type": "source-default", + "properties": {} + } + } + } +} + + +RAW_DD = { + "tags": "vf-module-name", + "name": "vf-module-name", + "property": { + "description": "vf-module-name", + "type": "string" + }, + "updated-by": "Singal, Kapil <ks220y@att.com>", + "sources": { + "input": { + "type": "source-input" + }, + "default": { + "type": "source-default", + "properties": {} + } + } +} + + +vLB_CBA_Python_meta_bytes = b'TOSCA-Meta-File-Version: 1.0.0\nCSAR-Version: 1.0\nCreated-By: PLATANIA, MARCO <platania@research.att.com>\nEntry-Definitions: Definitions/vLB_CDS.json\nTemplate-Tags: vDNS-CDS-test1\nContent-Type: application/vnd.oasis.bpmn\nTemplate-Name: vDNS-CDS-test1\nTemplate-Version: 1.0' + +vLB_CBA_Python_base_template_mapping_bytes = b'[\n {\n "name": "service-instance-id",\n "property": {\n "description": "",\n "required": false,\n "type": "string",\n "status": "",\n "constraints": [\n {}\n ],\n "entry_schema": {\n "type": ""\n }\n },\n "input-param": false,\n "dictionary-name": "service-instance-id",\n "dictionary-source": "input",\n "dependencies": [],\n "version": 0\n },\n {\n "name": "vnf-id",\n "property": {\n "description": "",\n "required": false,\n "type": "string",\n "status": "",\n "constraints": [\n {}\n ],\n "entry_schema": {\n "type": ""\n }\n },\n "input-param": false,\n "dictionary-name": "vnf-id",\n "dictionary-source": "input",\n "dependencies": [],\n "version": 0\n },\n {\n "name": "vdns_vf_module_id",\n "property": {\n "description": "",\n "required": false,\n "type": "string",\n "status": "",\n "constraints": [\n {}\n ],\n "entry_schema": {\n "type": ""\n }\n },\n "input-param": false,\n "dictionary-name": "vdns_vf_module_id",\n "dictionary-source": "sdnc",\n "dependencies": [\n\t "service-instance-id",\n "vnf-id"\n ],\n "version": 0\n },\n {\n "name": "vdns_int_private_ip_0",\n "property": {\n "description": "",\n "required": false,\n "type": "string",\n "status": "",\n "constraints": [\n {}\n ],\n "entry_schema": {\n "type": ""\n }\n },\n "input-param": false,\n "dictionary-name": "vdns_int_private_ip_0",\n "dictionary-source": "sdnc",\n "dependencies": [\n "service-instance-id",\n "vnf-id",\n "vdns_vf_module_id"\n ],\n "version": 0\n },\n {\n "name": "vdns_onap_private_ip_0",\n "property": {\n "description": "",\n "required": false,\n "type": "string",\n "status": "",\n "constraints": [\n {}\n ],\n "entry_schema": {\n "type": ""\n }\n },\n "input-param": false,\n "dictionary-name": "vdns_onap_private_ip_0",\n "dictionary-source": "sdnc",\n "dependencies": [\n "service-instance-id",\n "vnf-id",\n "vdns_vf_module_id"\n ],\n "version": 0\n }\n]' + + +@patch.object(Blueprint, "send_message") +def test_blueprint_enrichment(send_message_mock): + blueprint = Blueprint(b"test cba - it will never work") + blueprint.enrich() + send_message_mock.assert_called_once() + send_message_mock.reset_mock() + send_message_mock.side_effect = RequestError + with raises(RequestError): + blueprint.enrich() + + +@patch.object(Blueprint, "send_message") +def test_blueprint_publish(send_message_mock): + blueprint = Blueprint(b"test cba - it will never work") + blueprint.publish() + send_message_mock.assert_called_once() + + +@patch.object(Blueprint, "send_message") +def test_blueprint_deploy(send_message_mock): + blueprint = Blueprint(b"test cba - it will never work") + blueprint.deploy() + send_message_mock.assert_called_once() + + +def test_blueprint_load_from_file(): + with TemporaryDirectory() as tmpdirname: + path = os.path.join(tmpdirname, "test.zip") + with open(path, "wb") as f: + f.write(b"test cba - it will never work") + blueprint = Blueprint.load_from_file(path) + assert blueprint.cba_file_bytes == b"test cba - it will never work" + +def test_blueprint_load_from_file_file_error(): + + with TemporaryDirectory() as tmpdirname, \ + patch("__main__.open", new_callable=mock_open) as mo, \ + raises(FileError) as exc: + + path = os.path.join(tmpdirname, "nonexistent_file.zip") + mo.side_effect = FileNotFoundError + + Blueprint.load_from_file(path) + + assert exc.type == FileError + + +def test_blueprint_save(): + blueprint = Blueprint(b"test cba - it will never work") + with TemporaryDirectory() as tmpdirname: + path = os.path.join(tmpdirname, "test.zip") + blueprint.save(path) + with open(path, "rb") as f: + assert f.read() == b"test cba - it will never work" + + +def test_blueprint_read_cba_metadata(): + b = Blueprint(b"test cba - it will never work") + with raises(ValidationError) as exc: + b.get_cba_metadata(b"Invalid") + b.get_cba_metadata(b"123: 456") + assert exc.type is ValidationError + + cba_metadata = b.get_cba_metadata(vLB_CBA_Python_meta_bytes) + assert cba_metadata.tosca_meta_file_version == "1.0.0" + assert cba_metadata.csar_version == 1.0 + assert cba_metadata.created_by == "PLATANIA, MARCO <platania@research.att.com>" + assert cba_metadata.entry_definitions == "Definitions/vLB_CDS.json" + assert cba_metadata.template_name == "vDNS-CDS-test1" + assert cba_metadata.template_version == 1.0 + assert cba_metadata.template_tags == "vDNS-CDS-test1" + + with open(Path(Path(__file__).resolve().parent, "data/vLB_CBA_Python.zip"), "rb") as cba_file: + b = Blueprint(cba_file.read()) + assert b.metadata.tosca_meta_file_version == "1.0.0" + assert b.metadata.csar_version == 1.0 + assert b.metadata.created_by == "PLATANIA, MARCO <platania@research.att.com>" + assert b.metadata.entry_definitions == "Definitions/vLB_CDS.json" + assert b.metadata.template_name == "vDNS-CDS-test1" + assert b.metadata.template_version == 1.0 + assert b.metadata.template_tags == "vDNS-CDS-test1" + + +def test_blueprint_get_mappings_from_mapping_file(): + b = Blueprint(b"test cba - it will never work") + mappings = list(b.get_mappings_from_mapping_file(vLB_CBA_Python_base_template_mapping_bytes)) + assert len(mappings) == 5 + mapping = mappings[0] + assert mapping.name == "service-instance-id" + assert mapping.mapping_type == "string" + assert mapping.dictionary_name == "service-instance-id" + assert mapping.dictionary_sources == ["input"] + + +def test_blueprint_generate_data_dictionary_set(): + with open(Path(Path(__file__).resolve().parent, "data/vLB_CBA_Python.zip"), "rb") as cba_file: + b = Blueprint(cba_file.read()) + dd_set = b.get_data_dictionaries() + print(dd_set) + + +@patch.object(CdsElement, "_url", new_callable=PropertyMock) +def test_data_dictionary(cds_element_url_property_mock): + cds_element_url_property_mock.return_value = "http://127.0.0.1" + + with raises(ValidationError) as exc: + DataDictionary({}) + assert exc.type is ValidationError + + dd = DataDictionary({}, fix_schema=False) + assert dd.url == "http://127.0.0.1/api/v1/dictionary" + assert dd.data_dictionary_json == {} + + dd = DataDictionary(DD_1) + dd.name == DD_1["name"] + + +@patch.object(DataDictionary, "send_message") +def test_data_dictionary_upload(send_message_mock): + dd = DataDictionary(DD_1) + dd.upload() + send_message_mock.assert_called_once() + + +@patch.object(DataDictionary, "send_message") +def test_data_dictionary_set(send_message_mock): + dd_set = DataDictionarySet() + + dd_set.add(DataDictionary(DD_1)) + assert dd_set.length == 1 + + dd_set.add(DataDictionary(DD_1)) + assert dd_set.length == 1 + + dd_set.add(DataDictionary({"name": "test"}, fix_schema=False)) + assert dd_set.length == 2 + + dd_set.upload() + assert send_message_mock.call_count == 2 + + +def test_data_dictionary_set_save_to_file_load_from_file(): + dd = DataDictionarySet() + dd.add(DataDictionary(DD_1)) + with TemporaryDirectory() as tmpdirname: + path = os.path.join(tmpdirname, "dd.json") + dd.save_to_file(path) + with open(path, "r") as f: + assert f.read() == json.dumps([dd.data_dictionary_json for dd in dd.dd_set], indent=4) + dd_2 = DataDictionarySet.load_from_file(path) + assert dd.dd_set == dd_2.dd_set + +def test_data_dictionary_load_from_file_file_error(): + + with TemporaryDirectory() as tmpdirname, \ + patch("__main__.open", new_callable=mock_open) as mo, \ + raises(FileError) as exc: + + path = os.path.join(tmpdirname, "nonexistent_file.zip") + mo.side_effect = FileNotFoundError + + DataDictionarySet.load_from_file(path) + + assert exc.type == FileError + + +def test_mapping(): + m1 = Mapping(name="test", + mapping_type="string", + dictionary_name="test_dictionary_name", + dictionary_sources=["dictionary_source_1"]) + + m2 = Mapping(name="test", mapping_type="string", dictionary_name="test_dictionary_name", dictionary_sources=["dictionary_source_2"]) + + assert m1 == m2 + m1.merge(m2) + assert sorted(m1.dictionary_sources) == ["dictionary_source_1", "dictionary_source_2"] + m1.merge(m2) + assert sorted(m1.dictionary_sources) == ["dictionary_source_1", "dictionary_source_2"] + + +def test_mapping_set(): + ms = MappingSet() + assert len(ms) == 0 + m1 = Mapping(name="test", + mapping_type="string", + dictionary_name="test_dictionary_name", + dictionary_sources=["dictionary_source_1"]) + + m2 = Mapping(name="test", mapping_type="string", dictionary_name="test_dictionary_name", dictionary_sources=["dictionary_source_2"]) + + ms.add(m1) + assert len(ms) == 1 + ms.add(m2) + assert len(ms) == 1 + assert sorted(ms[0].dictionary_sources) == ["dictionary_source_1", "dictionary_source_2"] + + +def test_blueprint_get_workflows_from_entry_definitions_file(): + with open(Path(Path(__file__).resolve().parent, "data/vLB_CBA_Python.zip"), "rb") as cba_file: + b = Blueprint(cba_file.read()) + assert len(b.workflows) == 3 + workflow = b.workflows[0] + assert len(workflow.steps) == 1 + assert workflow.steps[0].name == "resource-assignment" + assert workflow.steps[0].description == "Resource Assign Workflow" + assert workflow.steps[0].target == "resource-assignment" + assert len(workflow.inputs) == 2 + assert len(workflow.outputs) == 1 + + +def test_blueprint_get_workflow_by_name(): + with open(Path(Path(__file__).resolve().parent, "data/vLB_CBA_Python.zip"), "rb") as cba_file: + b = Blueprint(cba_file.read()) + workflow = b.get_workflow_by_name("resource-assignment") + assert workflow.name == "resource-assignment" + workflow = b.get_workflow_by_name("config-assign") + assert workflow.name == "config-assign" + workflow = b.get_workflow_by_name("config-deploy") + assert workflow.name == "config-deploy" + with raises(ParameterError): + b.get_workflow_by_name("non-existing-workflow") + + +@patch.object(Workflow, "send_message") +def test_workflow_execute(send_message_mock): + metadata = MagicMock(template_name="test", template_version="test") + blueprint = MagicMock(metadata=metadata) + workflow = Workflow("test_workflow", {}, blueprint) + assert len(workflow.steps) == 0 + assert len(workflow.inputs) == 0 + assert len(workflow.outputs) == 0 + workflow.execute({}) + send_message_mock.assert_called_once() + + +def test_data_dictionary_validation(): + assert DataDictionary(DD_1).has_valid_schema() + raw_dd = DataDictionary(RAW_DD, fix_schema=False) + assert not raw_dd.has_valid_schema() + raw_dd = DataDictionary(RAW_DD, fix_schema=True) + assert raw_dd.has_valid_schema() + + +@patch.object(Blueprintprocessor, "send_message") +def test_blueprintprocessor_bootstrap(mock_send_message): + + Blueprintprocessor.bootstrap() + assert mock_send_message.called_once() + assert mock_send_message.call_args[1]["data"] == '{\n "loadModelType" : true,\n "loadResourceDictionary" : true,\n "loadCBA" : true\n}' + mock_send_message.reset_mock() + + Blueprintprocessor.bootstrap(load_cba=False, load_model_type=False, load_resource_dictionary=False) + assert mock_send_message.called_once() + assert mock_send_message.call_args[1]["data"] == '{\n "loadModelType" : false,\n "loadResourceDictionary" : false,\n "loadCBA" : false\n}' + + +@patch.object(DataDictionary, "send_message_json") +def test_data_dictionary_get_by_name(mock_send_message_json): + + DataDictionary.get_by_name("test_name") + mock_send_message_json.assert_called_once() + assert "test_name" in mock_send_message_json.call_args[0][2] + + +@patch.object(CdsElement, "send_message") +def test_get_guis(send_message_mock): + component = CdsElement() + send_message_mock.return_value.status_code = 200 + send_message_mock.return_value.url = "http://portal.api.simpledemo.onap.org:30449/" + gui_results = component.get_guis() + assert type(gui_results) == GuiList + assert gui_results.guilist[0].url == send_message_mock.return_value.url + assert gui_results.guilist[0].status == send_message_mock.return_value.status_code + + +@patch.object(ResolvedTemplate, "send_message_json") +@patch.object(CdsElement, "_url", new_callable=PropertyMock) +def test_blueprint_get_resolved_template(cds_element_url_property_mock, mock_send_message_json): + cds_element_url_property_mock.return_value = "http://127.0.0.1" + + with open(Path(Path(__file__).resolve().parent, "data/vLB_CBA_Python.zip"), "rb") as cba_file: + b = Blueprint(cba_file.read()) + b.get_resolved_template("test_artifact") + assert mock_send_message_json.called_once() + assert mock_send_message_json.call_args[0][2] == 'http://127.0.0.1/api/v1/template?bpName=vDNS-CDS-test1&bpVersion=1.0&artifactName=test_artifact&format=application%2Fjson' + + +@patch.object(ResolvedTemplate, "send_message") +@patch.object(CdsElement, "_url", new_callable=PropertyMock) +def test_blueprint_store_resolved_template(cds_element_url_property_mock, mock_send_message): + cds_element_url_property_mock.return_value = "http://127.0.0.1" + + with open(Path(Path(__file__).resolve().parent, "data/vLB_CBA_Python.zip"), "rb") as cba_file: + b = Blueprint(cba_file.read()) + b.store_resolved_template("test_artifact", resolution_key="resolution_key", data={"a": "b"}) + assert mock_send_message.called_once() + assert mock_send_message.call_args[0][2] == 'http://127.0.0.1/api/v1/template/vDNS-CDS-test1/1.0/test_artifact/resolution_key' + + +@patch.object(ResolvedTemplate, "send_message_json") +@patch.object(CdsElement, "_url", new_callable=PropertyMock) +def test_resolved_template_get_template_url(cds_element_url_property_mock, mock_send_message_json): + cds_element_url_property_mock.return_value = "http://127.0.0.1" + blueprint = MagicMock() + blueprint.metadata.template_name = "test_blueprint" + blueprint.metadata.template_version = "v1.0.0" + rt = ResolvedTemplate(blueprint, "test_artifact") + rt.get_resolved_template() + assert mock_send_message_json.called_once() + assert mock_send_message_json.call_args[0][2] == 'http://127.0.0.1/api/v1/template?bpName=test_blueprint&bpVersion=v1.0.0&artifactName=test_artifact&format=application%2Fjson' + + mock_send_message_json.reset_mock() + blueprint = MagicMock() + blueprint.metadata.template_name = "test_blueprint" + blueprint.metadata.template_version = "v1.0.0" + rt = ResolvedTemplate(blueprint, resolution_key="test_rk") + rt.get_resolved_template() + assert mock_send_message_json.called_once() + assert mock_send_message_json.call_args[0][2] == 'http://127.0.0.1/api/v1/template?bpName=test_blueprint&bpVersion=v1.0.0&resolutionKey=test_rk&format=application%2Fjson' + + mock_send_message_json.reset_mock() + blueprint = MagicMock() + blueprint.metadata.template_name = "test_blueprint" + blueprint.metadata.template_version = "v1.0.0" + rt = ResolvedTemplate(blueprint, resource_id="r_id", resource_type="r_type") + rt.get_resolved_template() + assert mock_send_message_json.called_once() + assert mock_send_message_json.call_args[0][2] == 'http://127.0.0.1/api/v1/template?bpName=test_blueprint&bpVersion=v1.0.0&resourceType=r_type&resourceId=r_id&format=application%2Fjson' + + +@patch.object(ResolvedTemplate, "send_message") +@patch.object(CdsElement, "_url", new_callable=PropertyMock) +def test_resolved_template_store_template_url(cds_element_url_property_mock, mock_send_message): + cds_element_url_property_mock.return_value = "http://127.0.0.1" + + blueprint = MagicMock() + blueprint.metadata.template_name = "test_blueprint" + blueprint.metadata.template_version = "v1.0.0" + rt = ResolvedTemplate(blueprint, "test_artifact", resolution_key="resolution_key") + rt.store_resolved_template({"a": "b"}) + assert mock_send_message.called_once() + assert mock_send_message.call_args[0][2] == 'http://127.0.0.1/api/v1/template/test_blueprint/v1.0.0/test_artifact/resolution_key' + + mock_send_message.reset_mock() + rt = ResolvedTemplate(blueprint, "test_artifact", resource_id="resource_id", resource_type="resource_type") + rt.store_resolved_template({"a": "b"}) + assert mock_send_message.called_once() + assert mock_send_message.call_args[0][2] == 'http://127.0.0.1/api/v1/template/test_blueprint/v1.0.0/test_artifact/resource_type/resource_id' + + mock_send_message.reset_mock() + rt = ResolvedTemplate(blueprint, "test_artifact") + with raises(ParameterError): + rt.store_resolved_template({"a": "b"}) diff --git a/tests/test_cds_blueprint_models.py b/tests/test_cds_blueprint_models.py new file mode 100644 index 0000000..4b1744d --- /dev/null +++ b/tests/test_cds_blueprint_models.py @@ -0,0 +1,201 @@ +"""Test CdsBlueprintModel module.""" +# Copyright 2022 Orange, 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 os.path + +from unittest import mock +from tempfile import TemporaryDirectory + +import pytest +import requests +import io + +from onapsdk.exceptions import ResourceNotFound +from onapsdk.cds.blueprint_model import BlueprintModel +from onapsdk.cds.cds_element import CdsElement +from onapsdk.cds.blueprint import Blueprint +from onapsdk.onap_service import OnapService + + +# pylint: disable=C0301 +BLUEPRINT_MODEL = { + "blueprintModel": { + "id": "11111111-2222-3333-4444-555555555555", + "artifactUUId": None, + "artifactType": "SDNC_MODEL", + "artifactVersion": "1.0.0", + "artifactDescription": "", + "internalVersion": None, + "createdDate": "2020-12-14T19:33:57.000Z", + "artifactName": "test_blueprint", + "published": "Y", + "updatedBy": "Carlos Santana <carlos.santana@onap.com>", + "tags": "Carlos Santana, test, blueprint" + } +} + +BLUEPRINT_MODEL_LIST = [ + BLUEPRINT_MODEL +] +# pylint: enable=C0301 + + +def test_init(): + """Test the initialization.""" + element = CdsElement() + assert isinstance(element, OnapService) + + +def test_class_variables(): + """Test the class variables.""" + assert CdsElement._url == "http://portal.api.simpledemo.onap.org:30449" + assert CdsElement.auth == ("ccsdkapps", "ccsdkapps") + assert CdsElement.headers == { + "Content-Type": "application/json", + "Accept": "application/json"} + + +@mock.patch.object(CdsElement, 'send_message_json') +def test_blueprint_model_all(mock_send): + """Test get_all function of BlueprintModel.""" + mock_send.return_value = BLUEPRINT_MODEL_LIST + assert len(list(BlueprintModel.get_all())) == 1 + + blueprint_model_1 = next(BlueprintModel.get_all()) + assert blueprint_model_1.blueprint_model_id == "11111111-2222-3333-4444-555555555555" + assert blueprint_model_1.artifact_uuid is None + assert blueprint_model_1.artifact_type == "SDNC_MODEL" + assert blueprint_model_1.artifact_version == "1.0.0" + assert blueprint_model_1.internal_version is None + assert blueprint_model_1.created_date == "2020-12-14T19:33:57.000Z" + assert blueprint_model_1.artifact_name == "test_blueprint" + assert blueprint_model_1.published == "Y" + assert blueprint_model_1.updated_by == "Carlos Santana <carlos.santana@onap.com>" + assert blueprint_model_1.tags == "Carlos Santana, test, blueprint" + + +@mock.patch.object(CdsElement, 'send_message_json') +def test_blueprint_model_all_empty(mock_send): + """Test get_all function of BlueprintModel with no BlueprintModels.""" + mock_send.return_value = "" + assert len(list(BlueprintModel.get_all())) == 0 + + +@mock.patch.object(CdsElement, 'send_message_json') +def test_blueprint_model_by_id(mock_send): + """Test get_by_id function of BlueprintModel.""" + mock_send.return_value = BLUEPRINT_MODEL + + blueprint_model_2 = BlueprintModel.get_by_id( + blueprint_model_id="11111111-2222-3333-4444-555555555555") + assert blueprint_model_2.blueprint_model_id == "11111111-2222-3333-4444-555555555555" + assert blueprint_model_2.artifact_uuid is None + assert blueprint_model_2.artifact_type == "SDNC_MODEL" + assert blueprint_model_2.artifact_version == "1.0.0" + assert blueprint_model_2.internal_version is None + assert blueprint_model_2.created_date == "2020-12-14T19:33:57.000Z" + assert blueprint_model_2.artifact_name == "test_blueprint" + assert blueprint_model_2.published == "Y" + assert blueprint_model_2.updated_by == "Carlos Santana <carlos.santana@onap.com>" + assert blueprint_model_2.tags == "Carlos Santana, test, blueprint" + + +@mock.patch.object(CdsElement, 'send_message_json') +def test_blueprint_model_by_id_non_existing(mock_send): + """Test get_by_id exception for non existing BlueprintModel.""" + + mock_send.side_effect = ResourceNotFound + with pytest.raises(ResourceNotFound) as exc: + BlueprintModel.get_by_id( + blueprint_model_id="11111111-2222-3333-4444-555555555555") + + assert exc.type == ResourceNotFound + + +@mock.patch.object(CdsElement, 'send_message_json') +def test_blueprint_model_by_name_and_version(mock_send): + """Test get_by_name_and_version function of BlueprintModel.""" + mock_send.return_value = BLUEPRINT_MODEL + + blueprint_model_3 = BlueprintModel.get_by_name_and_version( + blueprint_name="test_blueprint", + blueprint_version="1.0.0") + assert blueprint_model_3.blueprint_model_id == "11111111-2222-3333-4444-555555555555" + assert blueprint_model_3.artifact_uuid is None + assert blueprint_model_3.artifact_type == "SDNC_MODEL" + assert blueprint_model_3.artifact_version == "1.0.0" + assert blueprint_model_3.internal_version is None + assert blueprint_model_3.created_date == "2020-12-14T19:33:57.000Z" + assert blueprint_model_3.artifact_name == "test_blueprint" + assert blueprint_model_3.published == "Y" + assert blueprint_model_3.updated_by == "Carlos Santana <carlos.santana@onap.com>" + assert blueprint_model_3.tags == "Carlos Santana, test, blueprint" + + +@mock.patch.object(CdsElement, 'send_message_json') +def test_blueprint_model_by_name_and_version_non_existing(mock_send): + """Test get_by_name_and_version exception for non existing BlueprintModel.""" + + mock_send.side_effect = ResourceNotFound + with pytest.raises(ResourceNotFound) as exc: + BlueprintModel.get_by_name_and_version( + blueprint_name="test_blueprint_wrong", + blueprint_version="1.0.0") + + assert exc.type == ResourceNotFound + + +@mock.patch.object(CdsElement, 'send_message') +def test_get_blueprint_object(mock_send): + """Test retrieve Blueprint object for selected BlueprintModel.""" + mock_send.return_value.content = b"test cba - it will never work" + + blueprint_model_4 = BlueprintModel( + blueprint_model_id="11111111-2222-3333-4444-555555555555") + + blueprint4_object = blueprint_model_4.get_blueprint() + assert isinstance(blueprint4_object, Blueprint) + + +@mock.patch.object(CdsElement, 'send_message') +def test_save_blueprint(mock_send): + """Test download BlueprintModel from onap cds.""" + r = requests.Response() + r.raw = io.BytesIO(b'test cba - it will never work') + mock_send.return_value = r + + blueprint_model_5 = BlueprintModel( + blueprint_model_id="11111111-2222-3333-4444-555555555555") + + with TemporaryDirectory() as tmpdirname: + path = os.path.join(tmpdirname, "test.zip") + blueprint_model_5.save(dst_file_path=path) + + with open(path, "rb") as f: + assert f.read() == b"test cba - it will never work" + + +@mock.patch.object(CdsElement, 'send_message') +def test_delete_blueprint(mock_send): + """Test delete BlueprintModel in onap cds. """ + + blueprint_model_6 = BlueprintModel( + blueprint_model_id="11111111-2222-3333-4444-555555555555") + blueprint_model_6.delete() + mock_send.assert_called_once() + + method, description, url = mock_send.call_args[0] + assert method == "DELETE" + assert description == f"Delete blueprint" + assert url == f"{CdsElement._url}/api/v1/blueprint-model/{blueprint_model_6.blueprint_model_id}" diff --git a/tests/test_clamp.py b/tests/test_clamp.py new file mode 100644 index 0000000..5768341 --- /dev/null +++ b/tests/test_clamp.py @@ -0,0 +1,532 @@ +"""Test clamp module.""" +# Copyright 2022 Orange, 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 unittest import mock +import pytest + +from onapsdk.clamp.clamp_element import Clamp +from onapsdk.clamp.loop_instance import LoopInstance +from onapsdk.exceptions import ParameterError, ResourceNotFound +from onapsdk.sdc.service import Service + +#examples +TEMPLATES = [ + { + "name" : "test_template", + "modelService" : { + "serviceDetails" : { + "name" : "test" + } + } + } +] + +POLICIES = [ + { + "policyModelType" : "onap.policies.controlloop.Test", + "version" : "1.0.0", + "policyAcronym" : "Test", + "createdDate" : "2020-04-30T09:03:30.362897Z", + "updatedDate" : "2020-04-30T09:03:30.362897Z", + "updatedBy" : "Not found", + "createdBy" : "Not found" + } +] + +LOOP_DETAILS = { + "name" : "LOOP_test", + "globalPropertiesJson": { + "dcaeDeployParameters" : { + "uniqueBlueprintParameters" : { + "policy_id" : "Microservice12345" + } + } + }, + "components" : { + "POLICY" : { + "componentState" : { + "stateName" : "UNKNOWN" + } + }, + "DCAE" : { + "componentState" : { + "stateName" : "BLUEPRINT_DEPLOYED" + } + } + }, + "modelService" : { + "resourceDetails": { + "VFModule" : { + "resourceID" : { + "vfModuleModelName" : "resourceID", + "vfModuleModelInvariantUUID" : "InvariantUUID", + "vfModuleModelUUID" : "UUID", + "vfModuleModelVersion" : "1.0", + "vfModuleModelCustomizationUUID" : "CustomizationUUID" + } + } + } + }, + "operationalPolicies" : [ + { + "name" : "MICROSERVICE_test" + } + ], + "microServicePolicies" : [ + { + "name" : "MICROSERVICE_test" + } + ] +} + +#for policy deploy to policy engine +SUBMITED_POLICY = { + "components" : { + "POLICY" : { + "componentState" : { + "stateName" : "SENT_AND_DEPLOYED" + } + } + } +} + +NOT_SUBMITED_POLICY = { + "components" : { + "POLICY" : { + "componentState" : { + "stateName" : "SENT" + } + } + } +} + +#for the deploy to DCAE +SUBMITED = { + "components" : { + "DCAE" : { + "componentState" : { + "stateName" : "MICROSERVICE_INSTALLED_SUCCESSFULLY" + } + } + } +} + +NOT_SUBMITED = { + "components" : { + "DCAE" : { + "componentState" : { + "stateName" : "MICROSERVICE_INSTALLATION_FAILED" + } + } + } +} +#end of examples + + +def test_initialization(): + """Class initialization test.""" + clamp = Clamp() + assert isinstance(clamp, Clamp) + + +@mock.patch.object(Clamp, 'send_message_json') +def test_check_loop_template(mock_send_message_json): + """Test Clamp's class method.""" + svc = Service(name='test') + mock_send_message_json.return_value = TEMPLATES + template = Clamp.check_loop_template(service=svc) + mock_send_message_json.assert_called_once_with('GET', + 'Get Loop Templates', + (f"{Clamp.base_url()}/templates/")) + assert template == "test_template" + + +@mock.patch.object(Clamp, 'send_message_json') +def test_check_loop_template_none(mock_send_message_json): + """Test Clamp's class method.""" + svc = Service(name='test') + mock_send_message_json.return_value = {} + with pytest.raises(ResourceNotFound) as exc: + template = Clamp.check_loop_template(service=svc) + assert template is None + assert exc.type is ResourceNotFound + + +@mock.patch.object(Clamp, 'send_message_json') +def test_check_policies(mock_send_message_json): + mock_send_message_json.return_value = POLICIES + exists = Clamp.check_policies(policy_name="Test", req_policies=1) + mock_send_message_json.\ + assert_called_once_with('GET', + 'Get stocked policies', + (f"{Clamp.base_url()}/policyToscaModels/")) + assert exists + + +@mock.patch.object(Clamp, 'send_message_json') +def test_check_policies_none(mock_send_message_json): + mock_send_message_json.return_value = POLICIES + exists = Clamp.check_policies(policy_name="Test") + mock_send_message_json.\ + assert_called_once_with('GET', + 'Get stocked policies', + (f"{Clamp.base_url()}/policyToscaModels/")) + assert not exists + + +def test_cl_initialization(): + """Class initialization test.""" + loop = LoopInstance(template="template", name="LOOP_name", details={}) + assert isinstance(loop, LoopInstance) + + +@mock.patch.object(LoopInstance, '_update_loop_details') +def test_details(mock_update): + """Test loop instace details gette.""" + loop = LoopInstance(template="template", name="LOOP_name", details={}) + mock_update.return_value = {"name" : "test"} + details = loop.details + assert details == {} + + +@mock.patch.object(LoopInstance, 'send_message_json') +def test_update_loop_details(mock_send_message_json): + """Test Loop instance methode.""" + loop = LoopInstance(template="template", name="test", details={}) + mock_send_message_json.return_value = LOOP_DETAILS + loop.details = loop._update_loop_details() + mock_send_message_json.assert_called_once_with('GET', 'Get loop details', + (f"{loop.base_url()}/loop/LOOP_test")) + assert loop.details == LOOP_DETAILS + + +@mock.patch('time.sleep', return_value=False) +@mock.patch.object(LoopInstance, 'send_message_json') +def test_refresh_status(mock_send_message_json,mock_timer): + """Test Loop instance methode.""" + loop = LoopInstance(template="template", name="test", details={}) + mock_send_message_json.return_value = LOOP_DETAILS + loop.refresh_status() + mock_send_message_json.assert_called_once_with('GET', 'Get loop status', + (f"{loop.base_url()}/loop/getstatus/LOOP_test")) + assert loop.details == LOOP_DETAILS + + +def test_validate(): + """Test Loop instance details validation.""" + loop = LoopInstance(template="template", name="test", details=LOOP_DETAILS) + valid = loop.validate_details() + assert valid + + +def test_validate_details(): + """Test Loop instance details validation.""" + loop = LoopInstance(template="template", name="test", details=LOOP_DETAILS) + loop.details = {"test":"test"} + valid = loop.validate_details() + assert not valid + + +@mock.patch.object(LoopInstance, 'send_message_json') +def test_create(mock_send_message_json): + """Test Loop instance creation.""" + instance = LoopInstance(template="template", name="test", details={}) + mock_send_message_json.return_value = LOOP_DETAILS + instance.create() + mock_send_message_json.assert_called_once_with('POST', 'Create Loop Instance', + (f"{instance.base_url()}/loop/create/LOOP_test?templateName=template")) + assert instance.name == "LOOP_test" + assert len(instance.details["microServicePolicies"]) > 0 + + +@mock.patch.object(LoopInstance, 'send_message_json') +def test_add_operational_policy(mock_send_message_json): + """Test adding an op policy.""" + loop = LoopInstance(template="template", name="test", details={}) + loop.details = { + "name" : "LOOP_test", + "operationalPolicies" : None, + "microServicePolicies" : [ + { + "name" : "MICROSERVICE_test" + } + ] + } + mock_send_message_json.return_value = LOOP_DETAILS + loop.add_operational_policy(policy_type="FrequencyLimiter", policy_version="1.0.0") + mock_send_message_json.assert_called_once_with('PUT', 'Create Operational Policy', + (f"{loop.base_url()}/loop/addOperationaPolicy/{loop.name}/policyModel/FrequencyLimiter/1.0.0")) + assert loop.name == "LOOP_test" + assert len(loop.details["operationalPolicies"]) > 0 + + +@mock.patch.object(LoopInstance, 'send_message_json') +def test_not_add_operational_policy_parameter_error(mock_send_message_json): + """Test adding an op policy - mistaken policy version.""" + loop = LoopInstance(template="template", name="test", details={}) + loop.details = { + "name" : "LOOP_test", + "operationalPolicies" : [], + "microServicePolicies" : [ + { + "name" : "MICROSERVICE_test" + } + ] + } + with pytest.raises(ParameterError) as exc: + mock_send_message_json.return_value = loop.details + loop.add_operational_policy(policy_type="FrequencyLimiter", policy_version="not_correct") + mock_send_message_json.assert_called_once_with('PUT', 'Create Operational Policy', + (f"{loop.base_url()}/loop/addOperationaPolicy/{loop.name}/policyModel/FrequencyLimiter/not_correct")) + assert len(loop.details["operationalPolicies"]) == 0 + assert exc.type is ParameterError + +@mock.patch.object(LoopInstance, 'send_message_json') +def test_add_operational_policy_key_parameter_error(mock_send_message_json): + """Test adding an op policy - key doesn't exist.""" + loop = LoopInstance(template="template", name="test", details={}) + loop.details = {} + with pytest.raises(ParameterError) as exc: + mock_send_message_json.return_value = loop.details + loop.add_operational_policy(policy_type="FrequencyLimiter", policy_version="not_correct") + mock_send_message_json.assert_called_once_with('PUT', 'Create Operational Policy', + (f"{loop.base_url()}/loop/addOperationaPolicy/{loop.name}/policyModel/FrequencyLimiter/not_correct")) + assert exc.type is ParameterError + +@mock.patch.object(LoopInstance, 'send_message_json') +def test_add_operational_policy_condition_parameter_error(mock_send_message_json): + """Test adding an op policy - response cintains more policies.""" + + key = "operationalPolicies" + + response_policies = ["one"] # N policies + current_policies = ["one", "two"] # N+1 policies + + details = {key: current_policies} + response = {key: response_policies} + + loop = LoopInstance(template="template", name="test", details=details) + + assert len(response_policies) < len(current_policies) # raising condition + with pytest.raises(ParameterError) as exc: + mock_send_message_json.return_value = response + loop.add_operational_policy(policy_type="FrequencyLimiter", policy_version="not_correct") + assert exc.type is ParameterError + + +@mock.patch.object(LoopInstance, 'send_message_json') +def test_remove_operational_policy(mock_send_message_json): + """Test remove an op policy.""" + loop = LoopInstance(template="template", name="test", details=LOOP_DETAILS) + mock_send_message_json.return_value = { + "name" : "LOOP_test", + "operationalPolicies" : [], + "microServicePolicies" : [ + { + "name" : "MICROSERVICE_test" + } + ] + } + loop.remove_operational_policy(policy_type="FrequencyLimiter", policy_version="1.0.0") + mock_send_message_json.assert_called_once_with('PUT', 'Remove Operational Policy', + (f"{loop.base_url()}/loop/removeOperationaPolicy/{loop.name}/policyModel/FrequencyLimiter/1.0.0")) + assert len(loop.details["operationalPolicies"]) == 0 + + +@mock.patch.object(LoopInstance, 'send_message') +def test_update_microservice_policy(mock_send_message): + """Test Loop Instance add TCA configuration.""" + loop = LoopInstance(template="template", name="test", details=LOOP_DETAILS) + mock_send_message.return_value = True + loop.update_microservice_policy() + mock_send_message.assert_called_once() + method, description, url = mock_send_message.call_args[0] + assert method == "POST" + assert description == "ADD TCA config" + assert url == (f"{loop.base_url()}/loop/updateMicroservicePolicy/{loop.name}") + + +@mock.patch.object(LoopInstance, 'send_message') +def test_update_microservice_policy_none(mock_send_message): + """Test Loop Instance add TCA configuration.""" + loop = LoopInstance(template="template", name="test", details=LOOP_DETAILS) + mock_send_message.return_value = False + loop.update_microservice_policy() + mock_send_message.assert_called_once() + + +def test_extract_operational_policy_name(): + """Test Loop Instance extract operational policy name.""" + loop = LoopInstance(template="template", name="test", details={}) + loop.details = {"operationalPolicies":[{"name":"test","policyModel":{"policyAcronym":"Drools"}}]} + policy_name = loop.extract_operational_policy_name(policy_type="Drools") + assert policy_name=='test' + + +def test_extract_none(): + """Test Loop Instance extract operational policy name.""" + loop = LoopInstance(template="template", name="test", details={}) + loop.details = {"operationalPolicies":[]} + with pytest.raises(ParameterError) as exc: + policy_name = loop.extract_operational_policy_name(policy_type="Drools") + assert policy_name == None + assert exc.type is ParameterError + + +@mock.patch.object(LoopInstance, 'extract_operational_policy_name') +@mock.patch.object(LoopInstance, 'send_message') +def test_add_drools_policy_config(mock_send_message, mock_extract): + """Test Loop Instance add op policy configuration.""" + loop = LoopInstance(template="template", name="test", details=LOOP_DETAILS) + mock_send_message.return_value = True + loop.add_op_policy_config(loop.add_drools_conf) + mock_send_message.assert_called_once() + method, description, url = mock_send_message.call_args[0] + assert method == "POST" + assert description == "ADD operational policy config" + assert url == (f"{loop.base_url()}/loop/updateOperationalPolicies/{loop.name}") + + +@mock.patch.object(LoopInstance, 'extract_operational_policy_name') +@mock.patch.object(LoopInstance, 'send_message') +def test_add_minmax_config(mock_send_message, mock_extract): + """Test Loop Instance add op policy configuration.""" + loop = LoopInstance(template="template", name="test", details=LOOP_DETAILS) + mock_send_message.return_value = True + loop.add_op_policy_config(loop.add_minmax_config) + mock_send_message.assert_called_once() + method, description, url = mock_send_message.call_args[0] + assert method == "POST" + assert description == "ADD operational policy config" + assert url == (f"{loop.base_url()}/loop/updateOperationalPolicies/{loop.name}") + + +@mock.patch.object(LoopInstance, 'extract_operational_policy_name') +@mock.patch.object(LoopInstance, 'send_message') +def test_add_frequency_policy_config(mock_send_message, mock_extract): + """Test Loop Instance add op policy configuration.""" + loop = LoopInstance(template="template", name="test", details=LOOP_DETAILS) + mock_send_message.return_value = True + loop.add_op_policy_config(loop.add_frequency_limiter) + mock_send_message.assert_called_once() + method, description, url = mock_send_message.call_args[0] + assert method == "POST" + assert description == "ADD operational policy config" + assert url == (f"{loop.base_url()}/loop/updateOperationalPolicies/{loop.name}") + +@mock.patch.object(LoopInstance, 'send_message') +@mock.patch.object(LoopInstance, 'add_minmax_config') +@mock.patch.object(LoopInstance, 'add_frequency_limiter') +def test_add_two_policies_config(mock_freq, mock_min, mock_send_message): + """Test Loop Instance add op policy configuration.""" + loop = LoopInstance(template="template", name="test", details=LOOP_DETAILS) + mock_min.return_value = '[{"test1":"test1"}]' + mock_freq.return_value = '[{"test2":"test2"}]' + loop.add_op_policy_config(loop.add_minmax_config) + mock_min.assert_called_once() + mock_send_message.assert_called_once() + loop.add_op_policy_config(loop.add_frequency_limiter) + mock_freq.assert_called_once() + assert loop.operational_policies == '[{"test1":"test1"},{"test2":"test2"}]' + + +@mock.patch.object(LoopInstance, 'refresh_status') +@mock.patch.object(LoopInstance, 'send_message') +def test_submit_policy(mock_send_message, mock_refresh): + """Test submit policies to policy engine.""" + loop = LoopInstance(template="template", name="test", details=LOOP_DETAILS) + action = loop.act_on_loop_policy(loop.submit) + mock_send_message.assert_called_once_with('PUT', + 'submit policy', + (f"{loop.base_url()}/loop/submit/LOOP_test")) + mock_refresh.assert_called_once() + loop.details = SUBMITED_POLICY + assert loop.details["components"]["POLICY"]["componentState"]["stateName"] == "SENT_AND_DEPLOYED" + + +@mock.patch.object(LoopInstance, 'refresh_status') +@mock.patch.object(LoopInstance, 'send_message') +def test_stop_policy(mock_send_message, mock_refresh): + """Test submit policies to policy engine.""" + loop = LoopInstance(template="template", name="test", details=LOOP_DETAILS) + action = loop.act_on_loop_policy(loop.stop) + mock_send_message.assert_called_once_with('PUT', + 'stop policy', + (f"{loop.base_url()}/loop/stop/LOOP_test")) + mock_refresh.assert_called_once() + loop.details = {"components":{"POLICY":{"componentState":{"stateName":"SENT"}}}} + assert loop.details["components"]["POLICY"]["componentState"]["stateName"] == "SENT" + + +@mock.patch.object(LoopInstance, 'refresh_status') +@mock.patch.object(LoopInstance, 'send_message') +def test_restart_policy(mock_send_message, mock_refresh): + """Test submit policies to policy engine.""" + loop = LoopInstance(template="template", name="test", details=LOOP_DETAILS) + action = loop.act_on_loop_policy(loop.restart) + mock_send_message.assert_called_once_with('PUT', + 'restart policy', + (f"{loop.base_url()}/loop/restart/LOOP_test")) + mock_refresh.assert_called_once() + loop.details = SUBMITED_POLICY + assert loop.details["components"]["POLICY"]["componentState"]["stateName"] == "SENT_AND_DEPLOYED" + + +@mock.patch.object(LoopInstance, 'refresh_status') +@mock.patch.object(LoopInstance, 'send_message') +def test_not_submited_policy(mock_send_message, mock_refresh): + """Test submit policies to policy engine.""" + loop = LoopInstance(template="template", name="test", details=LOOP_DETAILS) + mock_refresh.return_value = NOT_SUBMITED_POLICY + action = loop.act_on_loop_policy(loop.submit) + mock_send_message.assert_called_once_with('PUT', + 'submit policy', + (f"{loop.base_url()}/loop/submit/LOOP_test")) + mock_refresh.assert_called_once() + loop.details = NOT_SUBMITED_POLICY + assert loop.details["components"]["POLICY"]["componentState"]["stateName"] == "SENT" + + +@mock.patch('time.sleep', return_value=False) +@mock.patch.object(LoopInstance, 'send_message_json') +@mock.patch.object(LoopInstance, 'send_message') +def test_deploy_microservice_to_dcae(mock_send_message, mock_send_message_json, mock_timer): + """Test stop microservice.""" + loop = LoopInstance(template="template", name="test", details=LOOP_DETAILS) + mock_send_message_json.return_value = SUBMITED + state = loop.deploy_microservice_to_dcae() + mock_send_message.assert_called_once_with('PUT', + 'Deploy microservice to DCAE', + (f"{loop.base_url()}/loop/deploy/LOOP_test")) + assert state + + +@mock.patch.object(LoopInstance, 'send_message') +def test_undeploy_microservice_from_dcae(mock_send_message): + """Test stop microservice.""" + loop = LoopInstance(template="template", name="test", details=LOOP_DETAILS) + request = loop.undeploy_microservice_from_dcae() + mock_send_message.assert_called_once_with('PUT', + 'Undeploy microservice from DCAE', + (f"{loop.base_url()}/loop/undeploy/LOOP_test")) + + +@mock.patch.object(LoopInstance, 'send_message') +def test_delete(mock_send_message): + loop = LoopInstance(template="template", name="test", details=LOOP_DETAILS) + request = loop.delete() + mock_send_message.assert_called_once_with('PUT', + 'Delete loop instance', + (f"{loop.base_url()}/loop/delete/{loop.name}")) diff --git a/tests/test_configuration.py b/tests/test_configuration.py new file mode 100644 index 0000000..70ae14a --- /dev/null +++ b/tests/test_configuration.py @@ -0,0 +1,24 @@ +# Copyright 2022 Orange, 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 onapsdk.utils.configuration import tosca_path +from onapsdk.utils.configuration import components_needing_distribution + +def test_tosca_path(): + assert tosca_path() == "/tmp/tosca_files/" + +def test_components_needing_distribution(): + assert "SO" in components_needing_distribution() + assert "sdnc" in components_needing_distribution() + assert "aai" in components_needing_distribution() diff --git a/tests/test_cps.py b/tests/test_cps.py new file mode 100644 index 0000000..8f7bdc4 --- /dev/null +++ b/tests/test_cps.py @@ -0,0 +1,240 @@ +# Copyright 2022 Orange, 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 unittest import mock +from typing import List + +from onapsdk.cps import Anchor, Dataspace, SchemaSet, SchemaSetModuleReference, anchor + +DATASPACE_ANCHOR = { + "name": "anchor1", + "schemaSetName": "schemaSet1" +} + +DATASPACE_ANCHORS = [ + DATASPACE_ANCHOR, + { + "name": "anchor2", + "schemaSetName": "schemaSet2" + } +] + +DATASPACE_SCHEMA_SET = { + "name": "schemaSet1", + "moduleReferences": [ + { + "name": "mr1", + "namespace": "mr1_namespace", + "revision": "mr1_revision", + }, + { + "name": "mr2", + "namespace": "mr2_namespace", + "revision": "mr2_revision", + } + ] +} + +# Dataspace tests +def test_dataspace(): + ds = Dataspace(name="test_ds") + assert ds.name == "test_ds" + assert f"cps/api/v1/dataspaces/{ds.name}" in ds.url + +@mock.patch("onapsdk.cps.Dataspace.send_message") +def test_dataspace_create_anchor(mock_send_message): + ds = Dataspace(name="test_ds") + anchor = ds.create_anchor(mock.MagicMock(), "test_anchor") + mock_send_message.assert_called_once() + assert anchor.name == "test_anchor" + +@mock.patch("onapsdk.cps.Dataspace.send_message_json") +def test_dataspace_get_anchors(mock_send_message_json): + mock_send_message_json.return_value = DATASPACE_ANCHORS + ds = Dataspace(name="test_ds") + anchors = list(ds.get_anchors()) + assert len(anchors) == 2 + anchor_1, anchor_2 = anchors + assert isinstance(anchor_1, Anchor) + assert isinstance(anchor_2, Anchor) + assert anchor_1.name == "anchor1" + assert isinstance(anchor_1.schema_set, SchemaSet) + assert anchor_1.schema_set.name == "schemaSet1" + assert anchor_1.schema_set.dataspace == ds + assert anchor_2.name == "anchor2" + assert isinstance(anchor_2.schema_set, SchemaSet) + assert anchor_2.schema_set.name == "schemaSet2" + assert anchor_2.schema_set.dataspace == ds + +@mock.patch("onapsdk.cps.Dataspace.send_message_json") +def test_dataspace_get_anchor(mock_send_message_json): + mock_send_message_json.return_value = DATASPACE_ANCHOR + ds = Dataspace(name="test_ds") + anchor = ds.get_anchor("anything") + assert anchor.name == "anchor1" + assert anchor.schema_set.name == "schemaSet1" + assert anchor.schema_set.dataspace == ds + +@mock.patch("onapsdk.cps.Dataspace.send_message_json") +def test_dataspace_get_schema_set(mock_send_message_json): + mock_send_message_json.return_value = DATASPACE_SCHEMA_SET + ds = Dataspace(name="test_ds") + schema_set = ds.get_schema_set("anything") + assert isinstance(schema_set, SchemaSet) + assert schema_set.dataspace == ds + assert schema_set.name == "schemaSet1" + assert len(schema_set.module_refences) == 2 + mr_1, mr_2 = schema_set.module_refences + assert mr_1.name == "mr1" + assert mr_1.namespace == "mr1_namespace" + assert mr_1.revision == "mr1_revision" + assert mr_2.name == "mr2" + assert mr_2.namespace == "mr2_namespace" + assert mr_2.revision == "mr2_revision" + +@mock.patch("onapsdk.cps.Dataspace.send_message") +@mock.patch("onapsdk.cps.Dataspace.get_schema_set") +def test_dataspace_create_schema_set(mock_get_chema_set, mock_send_message): + ds = Dataspace(name="test_ds") + _ = ds.create_schema_set("test_schema_set_name", b"fake_file") + mock_send_message.assert_called_once() + mock_get_chema_set.assert_called_once_with("test_schema_set_name") + +@mock.patch("onapsdk.cps.Dataspace.send_message") +def test_dataspace_delete(mock_send_message): + ds = Dataspace(name="test_ds") + ds.delete() + mock_send_message.assert_called_once() + +# Schemaset tests +def test_schema_set(): + schema_set = SchemaSet(name="test", dataspace=mock.MagicMock()) + assert schema_set.name == "test" + assert isinstance(schema_set.module_refences, List) + assert not len(schema_set.module_refences) + + schema_set = SchemaSet(name="test_with_mr", dataspace=mock.MagicMock(), + module_references=[SchemaSetModuleReference(name="mr1", namespace="mr1_n", revision="mr1_rev"), + SchemaSetModuleReference(name="mr2", namespace="mr2_n", revision="mr2_rev")]) + assert schema_set.name == "test_with_mr" + assert isinstance(schema_set.module_refences, List) + assert len(schema_set.module_refences) == 2 + mr_1, mr_2 = schema_set.module_refences + assert isinstance(mr_1, SchemaSetModuleReference) + assert isinstance(mr_2, SchemaSetModuleReference) + assert mr_1.name == "mr1" + assert mr_1.namespace == "mr1_n" + assert mr_1.revision == "mr1_rev" + assert mr_2.name == "mr2" + assert mr_2.namespace == "mr2_n" + assert mr_2.revision == "mr2_rev" + +@mock.patch("onapsdk.cps.SchemaSet.send_message") +def test_schemaset_delete(mock_send_message): + schema_set = SchemaSet(name="test", dataspace=mock.MagicMock()) + schema_set.delete() + mock_send_message.assert_called_once() + +# Anchor tests +def test_anchor(): + anchor = Anchor(name="test_anchor", schema_set=mock.MagicMock()) + assert anchor.name == "test_anchor" + assert "test_anchor" in anchor.url + +@mock.patch("onapsdk.cps.Anchor.send_message") +def test_anchor_delete(mock_send_message): + anchor = Anchor(name="test_anchor", schema_set=mock.MagicMock()) + anchor.delete() + mock_send_message.assert_called_once() + url = mock_send_message.call_args[0][2] + assert anchor.url in url + +@mock.patch("onapsdk.cps.Anchor.send_message") +def test_anchor_create_node(mock_send_message): + anchor = Anchor(name="test_anchor", schema_set=mock.MagicMock()) + anchor.create_node('{"test": "data"}') + mock_send_message.assert_called_once() + data = mock_send_message.call_args[1]["data"] + assert data == '{"test": "data"}' + +@mock.patch("onapsdk.cps.Anchor.send_message_json") +def test_anchor_get_node(mock_send_message_json): + anchor = Anchor(name="test_anchor", schema_set=mock.MagicMock()) + anchor.get_node("test-xpath") + mock_send_message_json.assert_called_once() + url = mock_send_message_json.call_args[0][2] + assert "xpath=test-xpath" in url + assert "include-descendants=False" in url + + mock_send_message_json.reset_mock() + anchor.get_node("test-xpath-2", include_descendants=True) + mock_send_message_json.assert_called_once() + url = mock_send_message_json.call_args[0][2] + assert "xpath=test-xpath-2" in url + assert "include-descendants=True" in url + + mock_send_message_json.reset_mock() + anchor.get_node("test-xpath-3", include_descendants=False) + mock_send_message_json.assert_called_once() + url = mock_send_message_json.call_args[0][2] + assert "xpath=test-xpath-3" in url + assert "include-descendants=False" in url + +@mock.patch("onapsdk.cps.Anchor.send_message") +def test_anchor_update_node(mock_send_message): + anchor = Anchor(name="test_anchor", schema_set=mock.MagicMock()) + anchor.update_node("test-xpath", '{"test": "data"}') + mock_send_message.assert_called_once() + url = mock_send_message.call_args[0][2] + assert "xpath=test-xpath" in url + +@mock.patch("onapsdk.cps.Anchor.send_message") +def test_anchor_replace_node(mock_send_message): + anchor = Anchor(name="test_anchor", schema_set=mock.MagicMock()) + anchor.replace_node("test-xpath", '{"test": "data"}') + mock_send_message.assert_called_once() + url = mock_send_message.call_args[0][2] + assert "xpath=test-xpath" in url + +@mock.patch("onapsdk.cps.Anchor.send_message") +def test_anchor_add_list_node(mock_send_message): + anchor = Anchor(name="test_anchor", schema_set=mock.MagicMock()) + anchor.add_list_node("test-xpath", '{"test": "data"}') + mock_send_message.assert_called_once() + url = mock_send_message.call_args[0][2] + assert "xpath=test-xpath" in url + +@mock.patch("onapsdk.cps.Anchor.send_message_json") +def test_anchor_query_node(mock_send_message_json): + anchor = Anchor(name="test_anchor", schema_set=mock.MagicMock()) + anchor.query_node("/test-query") + mock_send_message_json.assert_called_once() + url = mock_send_message_json.call_args[0][2] + assert "cps-path=/test-query" in url + assert "include-descendants=False" in url + + mock_send_message_json.reset_mock() + anchor.query_node("/test-query1", include_descendants=True) + mock_send_message_json.assert_called_once() + url = mock_send_message_json.call_args[0][2] + assert "cps-path=/test-query1" in url + assert "include-descendants=True" in url + +@mock.patch("onapsdk.cps.Anchor.send_message") +def test_anchor_delete_nodes(mock_send_message): + anchor = Anchor(name="test_anchor", schema_set=mock.MagicMock()) + anchor.delete_nodes("test-xpath") + mock_send_message.assert_called_once() + url = mock_send_message.call_args[0][2] + assert "xpath=test-xpath" in url diff --git a/tests/test_dmaap.py b/tests/test_dmaap.py new file mode 100644 index 0000000..28165bc --- /dev/null +++ b/tests/test_dmaap.py @@ -0,0 +1,47 @@ +# Copyright 2022 Orange, 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 unittest.mock import patch + +from onapsdk.dmaap.dmaap import Dmaap, ACTION, GET_HTTP_METHOD + +TOPIC = "fault" + +DMAAP_EVENTS_URL = "http://dmaap.api.simpledemo.onap.org:3904/events" +DMAAP_EVENTS_FROM_TOPIC_URL = f"http://dmaap.api.simpledemo.onap.org:3904/events/{TOPIC}/CG1/C1" +DMAAP_RESET_EVENTS = "http://dmaap.api.simpledemo.onap.org:3904/reset" +DMAAP_GET_ALL_TOPICS = "http://dmaap.api.simpledemo.onap.org:3904/topics" +BASIC_AUTH = {'username': 'dcae@dcae.onap.org', 'password': 'demo123456!'} + + +@patch.object(Dmaap, "send_message_json") +def test_should_get_all_events(send_message_mock): + Dmaap.get_all_events(BASIC_AUTH) + verify_send_event_to_ves_called(send_message_mock, DMAAP_EVENTS_URL) + +@patch.object(Dmaap, "send_message_json") +def test_should_get_events_from_topic(send_message_mock): + Dmaap.get_events_for_topic(TOPIC, BASIC_AUTH) + verify_send_event_to_ves_called(send_message_mock, DMAAP_EVENTS_FROM_TOPIC_URL) + +@patch.object(Dmaap, "send_message_json") +def test_should_get_all_topics(send_message_mock): + Dmaap.get_all_topics(BASIC_AUTH) + verify_send_event_to_ves_called(send_message_mock, DMAAP_GET_ALL_TOPICS) + +def verify_send_event_to_ves_called(send_message_mock, dmaap_url): + send_message_mock.assert_called_once_with( + GET_HTTP_METHOD, ACTION, dmaap_url, + basic_auth=BASIC_AUTH + ) + diff --git a/tests/test_esr.py b/tests/test_esr.py new file mode 100644 index 0000000..9f3c234 --- /dev/null +++ b/tests/test_esr.py @@ -0,0 +1,39 @@ +# Copyright 2022 Orange, 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 unittest import mock + +from onapsdk.msb.esr import ESR, MSB + + +def test_esr(): + esr = ESR() + assert esr.base_url == f"{MSB.base_url}/api/aai-esr-server/v1/vims" + + +@mock.patch.object(ESR, "send_message") +def test_est_register_vim(mock_esr_send_message): + ESR.register_vim( + "test_cloud_owner", + "test_cloud_region_id", + "test_cloud_type", + "test_cloud_region_version", + "test_auth_info_cloud_domain", + "test_auth_info_username", + "test_auth_info_password", + "test_auth_info_url" + ) + mock_esr_send_message.assert_called_once() + method, _, url = mock_esr_send_message.call_args[0] + assert method == "POST" + assert url == ESR.base_url diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py new file mode 100644 index 0000000..6c564b6 --- /dev/null +++ b/tests/test_exceptions.py @@ -0,0 +1,23 @@ +# Copyright 2022 Orange, 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 onapsdk.exceptions import APIError + + +def test_api_error_response_status_code(): + err = APIError() + assert err.response_status_code == 0 + err.response_status_code = 404 + assert err.response_status_code == 404 + err = APIError(response_status_code=404) + assert err.response_status_code == 404 diff --git a/tests/test_generic_instance.txt b/tests/test_generic_instance.txt new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/test_generic_instance.txt diff --git a/tests/test_gui.py b/tests/test_gui.py new file mode 100644 index 0000000..06b2a28 --- /dev/null +++ b/tests/test_gui.py @@ -0,0 +1,55 @@ +"""Test A&AI Element.""" +# Copyright 2022 Orange, 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 +import unittest + +from onapsdk.nbi.nbi import Nbi +from onapsdk.utils.gui import GuiItem, GuiList +from onapsdk.exceptions import NoGuiError + +class GuiTestingBase(unittest.TestCase): + + """The super class which testing classes could inherit.""" + logging.disable(logging.CRITICAL) + + def test_get_guis_request_error(self): + nbi_element = Nbi() + with self.assertRaises(NoGuiError): + nbi_element.get_guis() + + def test_create_bad_gui_item(self): + with self.assertRaises(TypeError): + gui1 = GuiItem(184) + + def test_create_bad_gui_list(self): + with self.assertRaises(TypeError): + list = GuiList(1, 2, 3) + + def test_add_gui_item(self): + gui1 = GuiItem('url1', 184) + gui2 = GuiItem('url2', 200) + test = GuiList([]) + test.add(gui1) + test.add(gui2) + assert len(test.guilist) == 2 + assert test.guilist[0].status == 184 + assert test.guilist[1].url == 'url2' + + def test_add_bad_gui_item(self): + with self.assertRaises(AttributeError): + test = GuiList([]) + test.add('not a gui item object') + + diff --git a/tests/test_headers_creator.py b/tests/test_headers_creator.py new file mode 100644 index 0000000..98efd8d --- /dev/null +++ b/tests/test_headers_creator.py @@ -0,0 +1,84 @@ +# Copyright 2022 Orange, 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 onapsdk.utils.headers_creator import ( + headers_aai_creator, + headers_sdc_creator, + headers_sdc_tester, + headers_sdc_governor, + headers_sdc_operator, + headers_sdnc_creator, + headers_so_creator, + headers_so_catelog_db_creator, +) + +def test_headers_sdc_creator(): + base_header = {} + sdc_headers_creator = headers_sdc_creator(base_header) + assert base_header != sdc_headers_creator + assert sdc_headers_creator["USER_ID"] == "cs0008" + assert sdc_headers_creator["Authorization"] + +def test_headers_sdc_tester(): + base_header = {} + sdc_headers_tester = headers_sdc_tester(base_header) + assert base_header != sdc_headers_tester + assert sdc_headers_tester["USER_ID"] == "jm0007" + assert sdc_headers_tester["Authorization"] + +def test_headers_sdc_governor(): + base_header = {} + sdc_headers_governor = headers_sdc_governor(base_header) + assert base_header != sdc_headers_governor + assert sdc_headers_governor["USER_ID"] == "gv0001" + assert sdc_headers_governor["Authorization"] + +def test_headers_sdc_operator(): + base_header = {} + sdc_headers_operator = headers_sdc_operator(base_header) + assert base_header != sdc_headers_operator + assert sdc_headers_operator["USER_ID"] == "op0001" + assert sdc_headers_operator["Authorization"] + +def test_headers_aai_creator(): + base_header = {} + aai_headers_creator = headers_aai_creator(base_header) + assert base_header != aai_headers_creator + assert aai_headers_creator["x-fromappid"] == "AAI" + assert aai_headers_creator["authorization"] + assert aai_headers_creator["x-transactionid"] + +def test_headers_so_creator(): + base_header = {} + so_headers_creator = headers_so_creator(base_header) + assert base_header != so_headers_creator + assert so_headers_creator["x-fromappid"] == "AAI" + assert so_headers_creator["authorization"] + assert so_headers_creator["x-transactionid"] + +def test_headers_so_catelog_db_creator(): + base_header = {} + so_headers_creator = headers_so_catelog_db_creator(base_header) + assert base_header != so_headers_creator + assert so_headers_creator["x-fromappid"] == "AAI" + assert so_headers_creator["authorization"] + assert so_headers_creator["x-transactionid"] + +def test_headers_sdnc_creator(): + base_header = {} + so_headers_creator = headers_sdnc_creator(base_header) + assert base_header != so_headers_creator + assert so_headers_creator["x-fromappid"] == "API client" + assert so_headers_creator["authorization"] + assert so_headers_creator["x-transactionid"] diff --git a/tests/test_jinja.py b/tests/test_jinja.py new file mode 100644 index 0000000..8ba6d34 --- /dev/null +++ b/tests/test_jinja.py @@ -0,0 +1,26 @@ +"""Test Jinja module.""" +# Copyright 2022 Orange, 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 jinja2 import Environment + +from onapsdk.utils.jinja import jinja_env + +def test_jinja_env(): + """test jinja_env function.""" + test_jinja_env = jinja_env() + assert isinstance(test_jinja_env, Environment) + assert 'sdc_element_action.json.j2' in test_jinja_env.list_templates() + assert 'vendor_create.json.j2' in test_jinja_env.list_templates() + assert 'vsp_create.json.j2' in test_jinja_env.list_templates() + assert test_jinja_env.autoescape != None diff --git a/tests/test_msb_k8s.py b/tests/test_msb_k8s.py new file mode 100644 index 0000000..37be189 --- /dev/null +++ b/tests/test_msb_k8s.py @@ -0,0 +1,378 @@ +# Copyright 2022 Orange, 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 unittest import mock + +from onapsdk.msb.k8s import Definition, ConnectivityInfo, Instance + + +CONNECTIVITY_INFO = { + "cloud-region": "test_cloud_region", + "cloud-owner": "test_cloud_owner", + "other-connectivity-list": {}, + "kubeconfig": "test_kubeconfig" +} + + +DEFINITION = { + "rb-name": "test_rb_name_0", + "rb-version": "test_rb_version_0" +} + + +DEFINITIONS = [ + DEFINITION, + { + "rb-name": "test_rb_name_1", + "rb-version": "test_rb_version_1", + "chart-name": "test_chart_name_1", + "description": "test_description_1", + "labels": {} + } +] + + +PROFILE = { + "rb-name": "test_rb_name", + "rb-version": "test_rb_version", + "profile-name": "test_profile_name", + "namespace": "test_namespace" +} + + +PROFILES = [ + PROFILE, + { + "rb-name": "test_rb_name_1", + "rb-version": "test_rb_version_1", + "profile-name": "test_profile_name_1", + "namespace": "test_namespace_1" + } +] + + +CONFIGURATION_TEMPLATE = { + "template-name": "test_configuration_template_name", + "description": "test_configuration_template_description" +} + + +CONFIGURATION_TEMPLATES = [ + CONFIGURATION_TEMPLATE, + { + "template-name": "test_configuration_template_name_0" + } +] + + +INSTANCE = { + "id": "ID_GENERATED_BY_K8SPLUGIN", + "namespace": "NAMESPACE_WHERE_INSTANCE_HAS_BEEN_DEPLOYED_AS_DERIVED_FROM_PROFILE", + "release-name": "RELEASE_NAME_AS_COMPUTED_BASED_ON_INSTANTIATION_REQUEST_AND_PROFILE_DEFAULT", + "request": { + "rb-name": "test-rbdef", + "rb-version": "v1", + "profile-name": "p1", + "release-name": "release-x", + "cloud-region": "krd", + "override-values": { + "optionalDictOfParameters": "andTheirValues, like", + "global.name": "dummy-name" + }, + "labels": { + "optionalLabelForInternalK8spluginInstancesMetadata": "dummy-value" + }, + }, + "resources": [ + { + "GVK": { + "Group": "", + "Kind": "ConfigMap", + "Version": "v1" + }, + "Name": "test-cm" + }, + { + "GVK": { + "Group": "", + "Kind": "Service", + "Version": "v1" + }, + "Name": "test-svc" + }, + { + "GVK": { + "Group": "apps", + "Kind": "Deployment", + "Version": "v1" + }, + "Name": "test-dep" + } + ] +} + + +INSTANCES = [ + INSTANCE +] + + +@mock.patch.object(ConnectivityInfo, "send_message_json") +def test_get_connectivity_info_by_region_id(mock_send_message_json): + mock_send_message_json.return_value = CONNECTIVITY_INFO + conn_info: ConnectivityInfo = ConnectivityInfo.get_connectivity_info_by_region_id("test_cloud_region_id") + assert conn_info.cloud_region_id == "test_cloud_region" + assert conn_info.cloud_owner == "test_cloud_owner" + assert conn_info.other_connectivity_list == {} + assert conn_info.kubeconfig == "test_kubeconfig" + + +@mock.patch.object(ConnectivityInfo, "send_message") +@mock.patch.object(ConnectivityInfo, "send_message_json") +def test_connectivity_info_create_delete(mock_send_message_json, mock_send_message): + mock_send_message_json.return_value = CONNECTIVITY_INFO + conn_info: ConnectivityInfo = ConnectivityInfo.create("test_cloud_region", "test_cloud_owner", b"kubeconfig") + assert conn_info.cloud_region_id == "test_cloud_region" + assert conn_info.cloud_owner == "test_cloud_owner" + assert conn_info.other_connectivity_list == {} + assert conn_info.kubeconfig == "test_kubeconfig" + conn_info.delete() + + +@mock.patch.object(Definition, "send_message_json") +def test_definition_get_all(mock_send_message_json): + mock_send_message_json.return_value = [] + assert len(list(Definition.get_all())) == 0 + + mock_send_message_json.return_value = DEFINITIONS + definitions = list(Definition.get_all()) + assert len(definitions) == 2 + + def_0, def_1 = definitions + assert def_0.rb_name == "test_rb_name_0" + assert def_0.rb_version == "test_rb_version_0" + assert def_0.chart_name is None + assert def_0.description is None + assert def_0.labels is None + + assert def_1.rb_name == "test_rb_name_1" + assert def_1.rb_version == "test_rb_version_1" + assert def_1.chart_name == "test_chart_name_1" + assert def_1.description == "test_description_1" + assert def_1.labels == {} + + +@mock.patch.object(Definition, "send_message_json") +def test_get_definition_by_name_version(mock_send_message_json): + mock_send_message_json.return_value = DEFINITION + def_0 = Definition.get_definition_by_name_version("rb_name", "rb_version") + assert def_0.rb_name == "test_rb_name_0" + assert def_0.rb_version == "test_rb_version_0" + assert def_0.chart_name is None + assert def_0.description is None + assert def_0.labels is None + + +@mock.patch.object(Definition, "send_message_json") +@mock.patch.object(Definition, "send_message") +def test_create_definition(mock_send_message, mock_send_message_json): + mock_send_message_json.return_value = DEFINITION + def_0 = Definition.create( + rb_name="test_rb_name_0", + rb_version="test_rb_version_0" + ) + assert def_0.rb_name == "test_rb_name_0" + assert def_0.rb_version == "test_rb_version_0" + assert def_0.chart_name is None + assert def_0.description is None + assert def_0.labels is None + + +@mock.patch.object(Definition, "send_message_json") +@mock.patch.object(Definition, "send_message") +def test_definition_create_profile(mock_send_message, mock_send_message_json): + mock_send_message_json.return_value = PROFILE + deff = Definition( + rb_name="test_rb_name", + rb_version="test_rb_version", + chart_name="test_chart_name", + description="test_description", + labels={} + ) + profile = deff.create_profile( + profile_name="test_profile_name", + namespace="test_namespace", + kubernetes_version="test_k8s_version" + ) + assert profile.rb_name == "test_rb_name" + assert profile.rb_version == "test_rb_version" + assert profile.profile_name == "test_profile_name" + assert profile.namespace == "test_namespace" + assert profile.kubernetes_version is None + assert profile.labels == {} + assert profile.release_name == "test_profile_name" + + +@mock.patch.object(Definition, "send_message_json") +def test_definition_get_profile_by_name(mock_send_message_json): + mock_send_message_json.return_value = PROFILE + deff = Definition( + rb_name="test_rb_name", + rb_version="test_rb_version", + chart_name="test_chart_name", + description="test_description", + labels={} + ) + profile = deff.get_profile_by_name("test_profile_name") + assert profile.rb_name == "test_rb_name" + assert profile.rb_version == "test_rb_version" + assert profile.profile_name == "test_profile_name" + assert profile.namespace == "test_namespace" + assert profile.kubernetes_version is None + assert profile.labels == {} + assert profile.release_name == "test_profile_name" + + +@mock.patch.object(Definition, "send_message_json") +def test_definition_get_all_profiles(mock_send_message_json): + mock_send_message_json.return_value = [] + deff = Definition( + rb_name="test_rb_name", + rb_version="test_rb_version", + chart_name="test_chart_name", + description="test_description", + labels={} + ) + assert len(list(deff.get_all_profiles())) == 0 + + mock_send_message_json.return_value = PROFILES + profiles = list(deff.get_all_profiles()) + assert len(profiles) == 2 + prof_0, prof_1 = profiles + + assert prof_0.rb_name == "test_rb_name" + assert prof_0.rb_version == "test_rb_version" + assert prof_0.profile_name == "test_profile_name" + assert prof_0.namespace == "test_namespace" + assert prof_0.kubernetes_version is None + assert prof_0.labels == {} + assert prof_0.release_name == "test_profile_name" + + assert prof_1.rb_name == "test_rb_name_1" + assert prof_1.rb_version == "test_rb_version_1" + assert prof_1.profile_name == "test_profile_name_1" + assert prof_1.namespace == "test_namespace_1" + assert prof_1.kubernetes_version is None + assert prof_1.labels == {} + assert prof_1.release_name == "test_profile_name_1" + + +@mock.patch.object(Definition, "send_message_json") +def test_definition_get_configuration_template_by_name(mock_send_message_json): + mock_send_message_json.return_value = CONFIGURATION_TEMPLATE + deff = Definition( + rb_name="test_rb_name", + rb_version="test_rb_version", + chart_name="test_chart_name", + description="test_description", + labels={} + ) + configuration_tmpl = deff.get_configuration_template_by_name( + template_name="test_configuration_template_name" + ) + assert configuration_tmpl.rb_name == deff.rb_name + assert configuration_tmpl.rb_version == deff.rb_version + assert configuration_tmpl.template_name == "test_configuration_template_name" + assert configuration_tmpl.description == "test_configuration_template_description" + + +@mock.patch.object(Definition, "send_message_json") +@mock.patch.object(Definition, "send_message") +def test_definition_create_configuration_template(mock_send_message, mock_send_message_json): + mock_send_message_json.return_value = CONFIGURATION_TEMPLATE + deff = Definition( + rb_name="test_rb_name", + rb_version="test_rb_version", + chart_name="test_chart_name", + description="test_description", + labels={} + ) + configuration_tmpl = deff.create_configuration_template( + template_name="test_configuration_template_name", + description="test_configuration_template_description" + ) + assert configuration_tmpl.rb_name == deff.rb_name + assert configuration_tmpl.rb_version == deff.rb_version + assert configuration_tmpl.template_name == "test_configuration_template_name" + assert configuration_tmpl.description == "test_configuration_template_description" + assert configuration_tmpl.url == f"{deff.base_url}/{deff.rb_name}/{deff.rb_version}/config-template/test_configuration_template_name" + + +@mock.patch.object(Definition, "send_message_json") +def test_definition_get_all_configuration_templates(mock_send_message_json): + mock_send_message_json.return_value = [] + deff = Definition( + rb_name="test_rb_name", + rb_version="test_rb_version", + chart_name="test_chart_name", + description="test_description", + labels={} + ) + assert len(list(deff.get_all_configuration_templates())) == 0 + + mock_send_message_json.return_value = CONFIGURATION_TEMPLATES + configuration_tmplts = list(deff.get_all_configuration_templates()) + assert len(configuration_tmplts) == 2 + + tmpl_0, tmpl_1 = configuration_tmplts + assert tmpl_0.rb_name == deff.rb_name + assert tmpl_0.rb_version == deff.rb_version + assert tmpl_0.template_name == "test_configuration_template_name" + assert tmpl_0.description == "test_configuration_template_description" + + assert tmpl_1.rb_name == deff.rb_name + assert tmpl_1.rb_version == deff.rb_version + assert tmpl_1.template_name == "test_configuration_template_name_0" + assert tmpl_1.description is None + + +@mock.patch.object(Instance, "send_message_json") +def test_instance_get_all(mock_send_message_json): + mock_send_message_json.return_value = [] + assert len(list(Instance.get_all())) == 0 + + mock_send_message_json.return_value = INSTANCES + assert len(list(Instance.get_all())) == 1 + + +@mock.patch.object(Instance, "send_message_json") +def test_instance_create(mock_send_message_json): + mock_send_message_json.return_value = INSTANCE + instance = Instance.create( + "test_cloud_region_id", + "test_profile_name", + "test_rb_name", + "test_rb_version" + ) + assert instance.instance_id == "ID_GENERATED_BY_K8SPLUGIN" + assert instance.namespace == "NAMESPACE_WHERE_INSTANCE_HAS_BEEN_DEPLOYED_AS_DERIVED_FROM_PROFILE" + + +@mock.patch.object(Instance, "send_message_json") +@mock.patch.object(Instance, "send_message") +def test_instance_get_by_id(mock_send_message, mock_send_message_json): + mock_send_message_json.return_value = INSTANCE + instance = Instance.get_by_id("ID_GENERATED_BY_K8SPLUGIN") + assert instance.instance_id == "ID_GENERATED_BY_K8SPLUGIN" + assert instance.namespace == "NAMESPACE_WHERE_INSTANCE_HAS_BEEN_DEPLOYED_AS_DERIVED_FROM_PROFILE" + instance.delete() diff --git a/tests/test_multicloud.py b/tests/test_multicloud.py new file mode 100644 index 0000000..8c05b93 --- /dev/null +++ b/tests/test_multicloud.py @@ -0,0 +1,38 @@ +# Copyright 2022 Orange, 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 unittest import mock + +from onapsdk.msb.multicloud import Multicloud + + +@mock.patch.object(Multicloud, "send_message") +def test_multicloud_register(mock_send_message): + Multicloud.register_vim(cloud_owner="test_cloud_owner", + cloud_region_id="test_cloud_region") + mock_send_message.assert_called_once() + method, description, url = mock_send_message.call_args[0] + assert method == "POST" + assert description == "Register VIM instance to ONAP" + assert url == f"{Multicloud.base_url}/test_cloud_owner/test_cloud_region/registry" + + +@mock.patch.object(Multicloud, "send_message") +def test_multicloud_unregister(mock_send_message): + Multicloud.unregister_vim(cloud_owner="test_cloud_owner", + cloud_region_id="test_cloud_region") + mock_send_message.assert_called_once() + method, description, url = mock_send_message.call_args[0] + assert method == "DELETE" + assert description == "Unregister VIM instance from ONAP" + assert url == f"{Multicloud.base_url}/test_cloud_owner/test_cloud_region" diff --git a/tests/test_nbi.py b/tests/test_nbi.py new file mode 100644 index 0000000..725baf2 --- /dev/null +++ b/tests/test_nbi.py @@ -0,0 +1,563 @@ +# Copyright 2022 Orange, 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 namedtuple +from unittest import mock + +from onapsdk.aai.business import Customer +from onapsdk.exceptions import RequestError +from onapsdk.nbi import Nbi, Service, ServiceOrder, ServiceSpecification + + +SERVICE_SPECIFICATION = { + "id":"a80c901c-6593-491f-9465-877e5acffb46", + "name":"testService1", + "invariantUUID":"217deaa7-dfc3-41d8-aa53-bb009029c09f", + "category":"Network Service", + "distributionStatus":"DISTRIBUTED", + "version":"1.0", + "lifecycleStatus":"CERTIFIED", + "relatedParty":{ + "id":"cs0008", + "role":"lastUpdater" + } +} + + +SERVICE_SPECIFICATIONS = [ + { + "id":"a80c901c-6593-491f-9465-877e5acffb46", + "name":"testService1", + "invariantUUID":"217deaa7-dfc3-41d8-aa53-bb009029c09f", + "category":"Network Service", + "distributionStatus":"DISTRIBUTED", + "version":"1.0", + "lifecycleStatus":"CERTIFIED", + "relatedParty":{ + "id":"cs0008", + "role":"lastUpdater" + } + }, + { + "id":"b1cda0ab-d968-41ef-9051-d26b33b120be", + "name":"testService2", + "invariantUUID":"906c3185-9656-4639-8f4d-d51d9ee0695d", + "category":"Network Service", + "distributionStatus":"DISTRIBUTED", + "version":"1.0", + "lifecycleStatus":"CERTIFIED", + "relatedParty":{ + "id":"cs0008" + ,"role":"lastUpdater" + } + } +] + + +SERVICES = [ + { + "id":"5c855390-7c39-4fe4-b164-2029b09de57c", + "name":"test6", + "serviceSpecification":{ + "name":"testService9", + "id":"125727ad-8660-423e-b4a1-99cd4a749f45" + }, + "relatedParty":{ + "role":"ONAPcustomer", + "id":"generic" + }, + "href":"service/5c855390-7c39-4fe4-b164-2029b09de57c" + }, + { + "id":"f948be83-c3e8-4515-a27d-2983eba63911", + "name":"test4", + "serviceSpecification":{ + "name":"testService8", + "id":"0960aedb-3ad8-49e1-ade5-a59414f6fda4" + }, + "relatedParty":{ + "role":"ONAPcustomer", + "id":"generic" + }, + "href":"service/f948be83-c3e8-4515-a27d-2983eba63911" + }, + { + "id":"5066eabd-846c-4ed9-886b-69892a12968d", + "name":"test5", + "serviceSpecification":{ + "name":"testService8", + "id":"0960aedb-3ad8-49e1-ade5-a59414f6fda4" + }, + "relatedParty":{ + "role":"ONAPcustomer", + "id":"generic" + }, + "href":"service/5066eabd-846c-4ed9-886b-69892a12968d" + } +] + + +SERVICES_CUSTOMER = [ + { + "id":"6a855390-7c39-4fe4-b164-2029b09de57d", + "name":"test7", + "serviceSpecification":{ + "name":"testService9", + "id":"125727ad-8660-423e-b4a1-99cd4a749f45" + }, + "relatedParty":{ + "role":"ONAPcustomer", + "id":"test_customer" + }, + "href":"service/6a855390-7c39-4fe4-b164-2029b09de57d" + } +] + + + +SERVICE_ORDERS = [ + { + "id":"5e9d6d98ae76af6b04e4df9a", + "href":"serviceOrder/5e9d6d98ae76af6b04e4df9a", + "externalId":"", + "priority":"1", + "description":"testService order for generic customer via Python ONAP SDK", + "category":"Consumer", + "state":"rejected", + "orderDate":"2020-04-20T09:38:32.286Z", + "completionDateTime":"2020-04-20T09:38:47.866Z", + "expectedCompletionDate":None, + "requestedStartDate":"2020-04-20T09:47:49.919Z", + "requestedCompletionDate":"2020-04-20T09:47:49.919Z", + "startDate":None, + "@baseType":None, + "@type":None, + "@schemaLocation":None, + "relatedParty":[ + { + "id":"generic", + "href":None, + "role":"ONAPcustomer", + "name":"generic", + "@referredType":None + } + ], + "orderRelationship":None, + "orderItem":[ + { + "orderMessage":[], + "id":"1", + "action":"add", + "state":"rejected", + "percentProgress":"0", + "@type":None, + "@schemaLocation":None, + "@baseType":None, + "orderItemRelationship":[], + "service":{ + "id":None, + "serviceType":None, + "href":None, + "name":"08d960ae-c2e1-4d5c-baf0-6420659ea68a", + "serviceState":"active", + "@type":None, + "@schemaLocation":None, + "serviceCharacteristic":None, + "serviceRelationship":None, + "relatedParty":None, + "serviceSpecification":{ + "id":"a80c901c-6593-491f-9465-877e5acffb46", + "href":None, + "name":None, + "version":None, + "targetServiceSchema":None, + "@type":None, + "@schemaLocation":None, + "@baseType":None + } + }, + "orderItemMessage":[] + } + ], + "orderMessage":[ + { + "code":"501", + "field":None, + "messageInformation":"Problem with AAI API", + "severity":"error", + "correctionRequired":True + }, + { + "code":"503", + "field":None, + "messageInformation":"tenantId not found in AAI", + "severity":"error", + "correctionRequired":True + } + ] + } +] + +SERVICE_ORDERS_NO_RELATED_PARTY = [ + { + "id":"5e9d6d98ae76af6b04e4df9a", + "href":"serviceOrder/5e9d6d98ae76af6b04e4df9a", + "externalId":"", + "priority":"1", + "description":"testService order for generic customer via Python ONAP SDK", + "category":"Consumer", + "state":"rejected", + "orderDate":"2020-04-20T09:38:32.286Z", + "completionDateTime":"2020-04-20T09:38:47.866Z", + "expectedCompletionDate":None, + "requestedStartDate":"2020-04-20T09:47:49.919Z", + "requestedCompletionDate":"2020-04-20T09:47:49.919Z", + "startDate":None, + "@baseType":None, + "@type":None, + "@schemaLocation":None, + "relatedParty": None, + "orderRelationship":None, + "orderItem":[ + { + "orderMessage":[], + "id":"1", + "action":"add", + "state":"rejected", + "percentProgress":"0", + "@type":None, + "@schemaLocation":None, + "@baseType":None, + "orderItemRelationship":[], + "service":{ + "id":None, + "serviceType":None, + "href":None, + "name":"08d960ae-c2e1-4d5c-baf0-6420659ea68a", + "serviceState":"active", + "@type":None, + "@schemaLocation":None, + "serviceCharacteristic":None, + "serviceRelationship":None, + "relatedParty":None, + "serviceSpecification":{ + "id":"a80c901c-6593-491f-9465-877e5acffb46", + "href":None, + "name":None, + "version":None, + "targetServiceSchema":None, + "@type":None, + "@schemaLocation":None, + "@baseType":None + } + }, + "orderItemMessage":[] + } + ], + "orderMessage":[ + { + "code":"501", + "field":None, + "messageInformation":"Problem with AAI API", + "severity":"error", + "correctionRequired":True + }, + { + "code":"503", + "field":None, + "messageInformation":"tenantId not found in AAI", + "severity":"error", + "correctionRequired":True + } + ] + } +] + +SERVICE_ORDER_STATE_COMPLETED = { + 'state': 'completed' +} + +SERVICE_ORDER_STATE_FAILED = { + 'state': 'failed' +} + +SERVICE_ORDER_STATE_IN_PROGRESS = { + 'state': 'inProgress' +} + +SERVICE_ORDER_STATE_REJECTED = { + 'state': 'rejected' +} + +SERVICE_ORDER_STATE_UNKNOWN = { + 'state': 'lalala' +} + +@mock.patch.object(Nbi, "send_message") +def test_nbi(mock_send_message): + + assert Nbi.base_url == "https://nbi.api.simpledemo.onap.org:30274" + assert Nbi.api_version == "/nbi/api/v4" + + mock_send_message.side_effect = RequestError + assert Nbi.is_status_ok() == False + mock_send_message.side_effect = None + assert Nbi.is_status_ok() == True + + +@mock.patch.object(ServiceSpecification, "send_message_json") +def test_service_specification_get_all(mock_service_specification_send_message): + mock_service_specification_send_message.return_value = [] + assert len(list(ServiceSpecification.get_all())) == 0 + + mock_service_specification_send_message.return_value = SERVICE_SPECIFICATIONS + service_specifications = list(ServiceSpecification.get_all()) + assert len(service_specifications) == 2 + + assert service_specifications[0].unique_id == "a80c901c-6593-491f-9465-877e5acffb46" + assert service_specifications[0].name == "testService1" + assert service_specifications[0].invariant_uuid == "217deaa7-dfc3-41d8-aa53-bb009029c09f" + assert service_specifications[0].category == "Network Service" + assert service_specifications[0].distribution_status == "DISTRIBUTED" + assert service_specifications[0].version == "1.0" + assert service_specifications[0].lifecycle_status == "CERTIFIED" + + assert service_specifications[1].unique_id == "b1cda0ab-d968-41ef-9051-d26b33b120be" + assert service_specifications[1].name == "testService2" + assert service_specifications[1].invariant_uuid == "906c3185-9656-4639-8f4d-d51d9ee0695d" + assert service_specifications[1].category == "Network Service" + assert service_specifications[1].distribution_status == "DISTRIBUTED" + assert service_specifications[1].version == "1.0" + assert service_specifications[1].lifecycle_status == "CERTIFIED" + + +@mock.patch.object(ServiceSpecification, "send_message_json") +def test_service_specification_get_by_id(mock_service_specification_send_message): + + mock_service_specification_send_message.return_value = SERVICE_SPECIFICATION + service_specification = ServiceSpecification.get_by_id("test") + assert service_specification.unique_id == "a80c901c-6593-491f-9465-877e5acffb46" + assert service_specification.name == "testService1" + assert service_specification.invariant_uuid == "217deaa7-dfc3-41d8-aa53-bb009029c09f" + assert service_specification.category == "Network Service" + assert service_specification.distribution_status == "DISTRIBUTED" + assert service_specification.version == "1.0" + assert service_specification.lifecycle_status == "CERTIFIED" + + +@mock.patch.object(Service, "send_message_json") +@mock.patch.object(Customer, "get_by_global_customer_id") +@mock.patch.object(ServiceSpecification, "get_by_id") +def test_service_get_all(mock_service_specification_get_by_id, + mock_customer_get_by_id, + mock_service_send_message): + mock_service_send_message.return_value = [] + assert len(list(Service.get_all())) == 0 + mock_service_send_message.return_value = SERVICES + services_list = list(Service.get_all()) + assert len(services_list) == 3 + + service = services_list[0] + + assert service.name == "test6" + assert service.service_id == "5c855390-7c39-4fe4-b164-2029b09de57c" + assert service._service_specification_name == "testService9" + assert service._service_specification_id == "125727ad-8660-423e-b4a1-99cd4a749f45" + assert service._customer_id == "generic" + assert service.customer_role == "ONAPcustomer" + assert service.href == "service/5c855390-7c39-4fe4-b164-2029b09de57c" + + assert service.customer is not None + mock_customer_get_by_id.assert_called_once_with(service._customer_id) + + service._customer_id = None + assert service.customer is None + + assert service.service_specification is not None + mock_service_specification_get_by_id.assert_called_once_with(service._service_specification_id) + + service._service_specification_id = None + assert service.service_specification is None + + mock_service_send_message.return_value = SERVICES_CUSTOMER + services_list = list(Service.get_all(customer_id="test_customer")) + assert len(services_list) == 1 + + service = services_list[0] + + assert service.name == "test7" + assert service.service_id == "6a855390-7c39-4fe4-b164-2029b09de57d" + assert service._service_specification_name == "testService9" + assert service._service_specification_id == "125727ad-8660-423e-b4a1-99cd4a749f45" + assert service._customer_id == "test_customer" + assert service.customer_role == "ONAPcustomer" + assert service.href == "service/6a855390-7c39-4fe4-b164-2029b09de57d" + + + +@mock.patch.object(ServiceOrder, "send_message_json") +def test_service_order(mock_service_order_send_message): + mock_service_order_send_message.return_value = [] + assert len(list(ServiceOrder.get_all())) == 0 + + mock_service_order_send_message.return_value = SERVICE_ORDERS + service_orders = list(ServiceOrder.get_all()) + assert len(service_orders) == 1 + service_order = service_orders[0] + assert service_order.unique_id == "5e9d6d98ae76af6b04e4df9a" + assert service_order.href =="serviceOrder/5e9d6d98ae76af6b04e4df9a" + assert service_order.priority == "1" + assert service_order.category == "Consumer" + assert service_order.description == "testService order for generic customer via Python ONAP SDK" + assert service_order.external_id == "" + assert service_order._customer == None + assert service_order._customer_id == "generic" + assert service_order._service_specification == None + assert service_order._service_specification_id == "a80c901c-6593-491f-9465-877e5acffb46" + assert service_order.service_instance_name == "08d960ae-c2e1-4d5c-baf0-6420659ea68a" + assert service_order.state == "rejected" + + +@mock.patch.object(ServiceOrder, "send_message_json") +def test_service_order_status(mock_service_order_send_message): + mock_service_order_send_message.return_value = SERVICE_ORDERS + service_order = next(ServiceOrder.get_all()) + + mock_service_order_send_message.return_value = SERVICE_ORDER_STATE_COMPLETED + assert service_order.status == service_order.StatusEnum.COMPLETED + assert service_order.completed + assert service_order.finished + assert not service_order.rejected + assert not service_order.failed + + mock_service_order_send_message.return_value = SERVICE_ORDER_STATE_FAILED + assert service_order.status == service_order.StatusEnum.FAILED + assert not service_order.completed + assert service_order.finished + assert not service_order.rejected + assert service_order.failed + + mock_service_order_send_message.return_value = SERVICE_ORDER_STATE_IN_PROGRESS + assert service_order.status == service_order.StatusEnum.IN_PROGRESS + assert not service_order.completed + assert not service_order.finished + assert not service_order.rejected + assert not service_order.failed + + mock_service_order_send_message.return_value = SERVICE_ORDER_STATE_REJECTED + assert service_order.status == service_order.StatusEnum.REJECTED + assert not service_order.completed + assert service_order.finished + assert service_order.rejected + assert not service_order.failed + + mock_service_order_send_message.return_value = SERVICE_ORDER_STATE_UNKNOWN + assert service_order.status == service_order.StatusEnum.UNKNOWN + assert not service_order.completed + assert service_order.finished + assert not service_order.rejected + assert not service_order.failed + + mock_service_order_send_message.return_value = {} + assert service_order.status == service_order.StatusEnum.UNKNOWN + assert not service_order.completed + assert service_order.finished + assert not service_order.rejected + assert not service_order.failed + + +@mock.patch.object(ServiceOrder, "send_message_json") +def test_service_order_no_related_party(mock_service_order_send_message): + mock_service_order_send_message.return_value = [] + assert len(list(ServiceOrder.get_all())) == 0 + + mock_service_order_send_message.return_value = SERVICE_ORDERS_NO_RELATED_PARTY + service_orders = list(ServiceOrder.get_all()) + assert len(service_orders) == 1 + service_order = service_orders[0] + assert service_order.unique_id == "5e9d6d98ae76af6b04e4df9a" + assert service_order.href =="serviceOrder/5e9d6d98ae76af6b04e4df9a" + assert service_order.priority == "1" + assert service_order.category == "Consumer" + assert service_order.description == "testService order for generic customer via Python ONAP SDK" + assert service_order.external_id == "" + assert service_order._customer == None + assert service_order._customer_id == None + assert service_order._service_specification == None + assert service_order._service_specification_id == "a80c901c-6593-491f-9465-877e5acffb46" + assert service_order.service_instance_name == "08d960ae-c2e1-4d5c-baf0-6420659ea68a" + assert service_order.state == "rejected" + + +@mock.patch.object(Customer, "get_by_global_customer_id") +def test_service_order_customer(mock_customer_get_by_id): + service_order = ServiceOrder("test_unique_id", + "test_href", + "test_priority", + "test_description", + "test_category", + "test_external_id", + "test_service_instance_name") + assert service_order.customer is None + assert service_order._customer is None + service_order._customer_id = "test_customer_id" + assert service_order.customer is not None + mock_customer_get_by_id.assert_called_once_with("test_customer_id") + assert service_order._customer is not None + + +@mock.patch.object(ServiceSpecification, "get_by_id") +def test_service_order_service_specification(mock_service_spec_get_by_id): + service_order = ServiceOrder("test_unique_id", + "test_href", + "test_priority", + "test_description", + "test_category", + "test_external_id", + "test_service_instance_name") + assert service_order.service_specification is None + assert service_order._service_specification_id is None + service_order._service_specification_id = "test_service_spec_id" + assert service_order.service_specification is not None + mock_service_spec_get_by_id.assert_called_once_with("test_service_spec_id") + assert service_order._service_specification is not None + + +@mock.patch.object(ServiceOrder, "send_message_json") +def test_service_order_create(mock_service_order_send_message): + ServiceOrder.create(customer=mock.MagicMock(), + service_specification=mock.MagicMock()) + mock_service_order_send_message.assert_called_once() + method, _, url = mock_service_order_send_message.call_args[0] + assert method == "POST" + assert url == f"{ServiceOrder.base_url}{ServiceOrder.api_version}/serviceOrder" + + +def test_service_order_wait_for_finish(): + with mock.patch.object(ServiceOrder, "finished", new_callable=mock.PropertyMock) as mock_finished: + with mock.patch.object(ServiceOrder, "completed", new_callable=mock.PropertyMock) as mock_completed: + service_order = ServiceOrder( + unique_id="test", + href="test", + priority="test", + description="test", + category="test", + external_id="test", + service_instance_name="test", + ) + service_order.WAIT_FOR_SLEEP_TIME = 0 + mock_finished.side_effect = [False, False, True] + mock_completed.return_value = True + rv = namedtuple("Value", ["return_value"]) + service_order._wait_for_finish(rv) + assert rv.return_value diff --git a/tests/test_onap_service.py b/tests/test_onap_service.py new file mode 100644 index 0000000..62a9880 --- /dev/null +++ b/tests/test_onap_service.py @@ -0,0 +1,380 @@ +"""Test OnapService module.""" +# Copyright 2022 Orange, 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 unittest import mock +from unittest.mock import ANY + +import pytest +from requests import Response, Session + +from requests import ConnectionError, RequestException + +from onapsdk.exceptions import ( + RequestError, APIError, ResourceNotFound, InvalidResponse, ConnectionFailed +) + +from onapsdk.onap_service import OnapService +from onapsdk.sdc.vendor import Vendor + +def http_codes(): + return [ + 400, # Bad Request + 401, # Unauthorized + 403, # Forbidden + 405, # Method Not Allowed + 408, # Request Timeout + 415, # Unsupported Media Type + 429, # Too Many Requests + 500, # Internal Server Error + 501, # Not Implemented + 502, # Bad Gateway + 503, # Service Unavailable + 504 # Gateway Timeout + ] + +class TestException(Exception): + """Test exception.""" + +def test_init(): + """Test initialization.""" + svc = OnapService() + +def test_class_variables(): + """Test class variables.""" + assert OnapService.server == None + assert OnapService.headers == { + "Content-Type": "application/json", + "Accept": "application/json", + } + assert OnapService.proxy == None + +def test_set_proxy(): + """Test set_proxy().""" + assert OnapService.proxy == None + Vendor.set_proxy({'the', 'proxy'}) + assert OnapService.proxy == {'the', 'proxy'} + Vendor.set_proxy(None) + assert OnapService.proxy == None + +# ------------------ + +@mock.patch.object(Session, 'request') +def test_send_message_OK(mock_request): + """Returns response if OK.""" + svc = OnapService() + mocked_response = Response() + mocked_response.status_code = 200 + mock_request.return_value = mocked_response + expect_headers = { + "Content-Type": "application/json", + "Accept": "application/json", + } + response = svc.send_message("GET", 'test get', 'http://my.url/') + mock_request.assert_called_once_with('GET', 'http://my.url/', + headers=expect_headers, verify=False, + proxies=None) + assert response == mocked_response + +@mock.patch.object(Session, 'request') +def test_send_message_custom_header_OK(mock_request): + """Returns response if returns OK with a custom header.""" + svc = OnapService() + mocked_response = Response() + mocked_response.status_code = 200 + mock_request.return_value = mocked_response + expect_headers = { + "Content-Type": "application/json", + "Accept": "application/json", + "Custom": "Header" + } + response = svc.send_message("GET", 'test get', 'http://my.url/', + headers=expect_headers) + mock_request.assert_called_once_with('GET', 'http://my.url/', + headers=expect_headers, verify=False, + proxies=None) + assert response == mocked_response + +@mock.patch.object(OnapService, '_set_basic_auth_if_needed') +@mock.patch.object(Session, 'request') +def test_send_message_with_basic_auth(mock_request, mock_set_basic_auth_if_needed): + """Should give response of request if OK.""" + svc = OnapService() + mocked_response = Response() + mocked_response.status_code = 200 + basic_auth = {'username': 'user1', "password": "password1"} + mock_request.return_value = mocked_response + expect_headers = { + "Content-Type": "application/json", + "Accept": "application/json", + "Once": "Upon a time" + } + response = svc.send_message("GET", 'test get', 'http://my.url/', + headers=expect_headers, basic_auth=basic_auth) + mock_set_basic_auth_if_needed.assert_called_once_with(basic_auth, ANY) + mock_request.assert_called_once_with('GET', 'http://my.url/', + headers=expect_headers, verify=False, + proxies=None) + assert response == mocked_response + +@mock.patch.object(Session, 'request') +def test_send_message_resource_not_found(mock_request): + """Should raise ResourceNotFound if status code 404.""" + svc = OnapService() + + mocked_response = Response() + mocked_response.status_code = 404 + + mock_request.return_value = mocked_response + + with pytest.raises(ResourceNotFound) as exc: + svc.send_message("GET", 'test get', 'http://my.url/') + assert exc.type is ResourceNotFound + + mock_request.assert_called_once() + +@mock.patch.object(Session, 'request') +@pytest.mark.parametrize("code", http_codes()) +def test_send_message_api_error(mock_request, code): + """Raise APIError if status code is between 400 and 599, and not 404.""" + svc = OnapService() + mocked_response = Response() + mocked_response.status_code = code + mock_request.return_value = mocked_response + + with pytest.raises(APIError) as exc: + svc.send_message("GET", 'test get', 'http://my.url/') + assert exc.type is APIError + + mock_request.assert_called_once() + +@mock.patch.object(Session, 'request') +def test_send_message_connection_failed(mock_request): + """Should raise ResourceNotFound if status code 404.""" + svc = OnapService() + + mock_request.side_effect = ConnectionError + + with pytest.raises(ConnectionFailed) as exc: + svc.send_message("GET", 'test get', 'http://my.url/') + assert exc.type is ConnectionFailed + + mock_request.assert_called_once() + +@mock.patch.object(Session, 'request') +def test_send_message_request_error(mock_request): + """Should raise RequestError for an amiguous request exception.""" + svc = OnapService() + + mock_request.side_effect = RequestException + + with pytest.raises(RequestError) as exc: + svc.send_message("GET", 'test get', 'http://my.url/') + assert exc.type is RequestError + + mock_request.assert_called_once() + + +@mock.patch.object(Session, 'request') +def test_send_message_custom_error(mock_request): + """Should raise RequestError for an amiguous request exception.""" + svc = OnapService() + + mock_request.side_effect = RequestException + + with pytest.raises(TestException) as exc: + svc.send_message("GET", 'test get', 'http://my.url/', + exception=TestException) + assert exc.type is TestException + + mock_request.assert_called_once() + +@mock.patch.object(OnapService, 'send_message') +def test_send_message_json_OK(mock_send): + """JSON is received and successfully decoded.""" + svc = OnapService() + + mocked_response = Response() + mocked_response._content = b'{"yolo": "yala"}' + mocked_response.encoding = "UTF-8" + mocked_response.status_code = 200 + + mock_send.return_value = mocked_response + + response = svc.send_message_json("GET", 'test get', 'http://my.url/') + + mock_send.assert_called_once_with("GET", 'test get', 'http://my.url/') + assert response['yolo'] == 'yala' + +@mock.patch.object(OnapService, 'send_message') +def test_send_message_json_invalid_response(mock_send): + """Raises InvalidResponse if response is not JSON.""" + svc = OnapService() + + mocked_response = Response() + mocked_response._content = b'{yolo}' + mocked_response.encoding = "UTF-8" + mocked_response.status_code = 200 + + mock_send.return_value = mocked_response + + with pytest.raises(InvalidResponse) as exc: + svc.send_message_json("GET", 'test get', 'http://my.url/') + assert exc.type is InvalidResponse + + mock_send.assert_called_once() + +@mock.patch.object(OnapService, 'send_message') +def test_send_message_json_connection_failed(mock_send): + """ConnectionFailed from send_message is handled.""" + svc = OnapService() + + mock_send.side_effect = ConnectionFailed + + with pytest.raises(ConnectionFailed) as exc: + svc.send_message_json("GET", 'test get', 'http://my.url/') + assert exc.type is ConnectionFailed + + mock_send.assert_called_once() + +@mock.patch.object(OnapService, 'send_message') +def test_send_message_json_api_error(mock_send): + """APIError (error codes) from send_message is handled.""" + svc = OnapService() + + mock_send.side_effect = APIError + + with pytest.raises(APIError) as exc: + svc.send_message_json("GET", 'test get', 'http://my.url/') + assert exc.type is APIError + + mock_send.assert_called_once() + +@mock.patch.object(OnapService, 'send_message') +def test_send_message_json_resource_not_found(mock_send): + """ResourceNotFound exception from send_message is handled.""" + svc = OnapService() + + mock_send.side_effect = ResourceNotFound + + with pytest.raises(ResourceNotFound) as exc: + svc.send_message_json("GET", 'test get', 'http://my.url/') + assert exc.type is ResourceNotFound + + mock_send.assert_called_once() + +@mock.patch.object(OnapService, 'send_message') +def test_send_message_json_request_error(mock_send): + """RequestError exception from send_message is handled.""" + svc = OnapService() + + mock_send.side_effect = RequestError + + with pytest.raises(RequestError) as exc: + svc.send_message_json("GET", 'test get', 'http://my.url/') + assert exc.type is RequestError + + mock_send.assert_called_once() + + +@mock.patch.object(OnapService, 'send_message') +def test_send_message_json_custom_error(mock_send): + """RequestError exception from send_message is handled.""" + svc = OnapService() + + mock_send.side_effect = RequestError + + with pytest.raises(TestException) as exc: + svc.send_message_json("GET", 'test get', 'http://my.url/', + exception=TestException) + assert exc.type is TestException + + mock_send.assert_called_once() + +@mock.patch("onapsdk.onap_service.requests.Session") +def test_set_header(mock_session): + + OnapService.send_message("GET", 'test get', 'http://my.url/') + _, _, kwargs = mock_session.return_value.request.mock_calls[0] + headers = kwargs["headers"] + assert "test-header-key" not in headers + + mock_session.reset_mock() + OnapService.set_header({"test-header-key": "test-header-value"}) + OnapService.send_message("GET", 'test get', 'http://my.url/') + _, _, kwargs = mock_session.return_value.request.mock_calls[0] + headers = kwargs["headers"] + assert "test-header-key" in headers + assert headers["test-header-key"] == "test-header-value" + + mock_session.reset_mock() + OnapService.send_message("GET", 'test get', 'http://my.url/', headers={}) + _, _, kwargs = mock_session.return_value.request.mock_calls[0] + headers = kwargs["headers"] + assert "test-header-key" in headers + assert headers["test-header-key"] == "test-header-value" + + mock_session.reset_mock() + OnapService.send_message("GET", 'test get', 'http://my.url/', headers={"test-header-key": "test-header-another-value"}) + _, _, kwargs = mock_session.return_value.request.mock_calls[0] + headers = kwargs["headers"] + assert "test-header-key" in headers + assert headers["test-header-key"] == "test-header-value" + + mock_session.reset_mock() + OnapService.set_header(None) + OnapService.send_message("GET", 'test get', 'http://my.url/') + _, _, kwargs = mock_session.return_value.request.mock_calls[0] + headers = kwargs["headers"] + assert "test-header-key" not in headers + + def test_header_method(): + return {"test-header-callable-key": "test-header-callable-value"} + + mock_session.reset_mock() + OnapService.set_header(test_header_method) + OnapService.send_message("GET", 'test get', 'http://my.url/', headers={}) + _, _, kwargs = mock_session.return_value.request.mock_calls[0] + headers = kwargs["headers"] + assert "test-header-callable-key" in headers + assert headers["test-header-callable-key"] == "test-header-callable-value" + + mock_session.reset_mock() + OnapService.send_message("GET", 'test get', 'http://my.url/', headers={"test-header-key": "test-header-value"}) + _, _, kwargs = mock_session.return_value.request.mock_calls[0] + headers = kwargs["headers"] + assert "test-header-callable-key" in headers + assert headers["test-header-callable-key"] == "test-header-callable-value" + assert "test-header-key" in headers + assert headers["test-header-key"] == "test-header-value" + + mock_session.reset_mock() + OnapService.set_header({"test-header-dict-key": "test-header-dict-value"}) + OnapService.send_message("GET", 'test get', 'http://my.url/', headers={}) + _, _, kwargs = mock_session.return_value.request.mock_calls[0] + headers = kwargs["headers"] + assert "test-header-callable-key" in headers + assert headers["test-header-callable-key"] == "test-header-callable-value" + assert "test-header-dict-key" in headers + assert headers["test-header-dict-key"] == "test-header-dict-value" + + mock_session.reset_mock() + OnapService.send_message("GET", 'test get', 'http://my.url/', headers={"test-header-common-key": "test-header-common-value"}) + _, _, kwargs = mock_session.return_value.request.mock_calls[0] + headers = kwargs["headers"] + assert "test-header-callable-key" in headers + assert headers["test-header-callable-key"] == "test-header-callable-value" + assert "test-header-dict-key" in headers + assert headers["test-header-dict-key"] == "test-header-dict-value" + assert "test-header-common-key" in headers + assert headers["test-header-common-key"] == "test-header-common-value" diff --git a/tests/test_pnf.py b/tests/test_pnf.py new file mode 100644 index 0000000..0f606a0 --- /dev/null +++ b/tests/test_pnf.py @@ -0,0 +1,485 @@ +"""Test pnf module.""" +# Copyright 2022 Orange, 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 unittest import mock +from unittest.mock import MagicMock +from pathlib import Path + +import pytest + +import onapsdk.constants as const +from onapsdk.exceptions import ParameterError, RequestError, StatusError +from onapsdk.sdc.category_management import ResourceCategory +from onapsdk.sdc.properties import Property +from onapsdk.sdc.sdc_resource import SdcResource +from onapsdk.sdc.pnf import Pnf +from onapsdk.sdc.vsp import Vsp +from onapsdk.sdc.vsp import Vendor + + +@mock.patch.object(Pnf, 'send_message_json') +def test_get_all_no_pnf(mock_send): + """Returns empty array if no pnfs.""" + mock_send.return_value = {} + assert Pnf.get_all() == [] + mock_send.assert_called_once_with("GET", 'get Pnfs', 'https://sdc.api.be.simpledemo.onap.org:30204/sdc/v1/catalog/resources?resourceType=PNF') + + +@mock.patch.object(Pnf, 'send_message_json') +def test_get_all_some_pnfs(mock_send): + """Returns a list of pnfs.""" + mock_send.return_value = [ + {'resourceType': 'PNF', 'name': 'one', 'uuid': '1234', 'invariantUUID': '5678', 'version': '1.0', 'lifecycleState': 'CERTIFIED', 'category': 'Generic', "subCategory": "Abstract"}, + {'resourceType': 'PNF', 'name': 'two', 'uuid': '1235', 'invariantUUID': '5679', 'version': '1.0', 'lifecycleState': 'NOT_CERTIFIED_CHECKOUT', 'category': 'Generic', "subCategory": "Abstract"}] + all_pnfs = Pnf.get_all() + assert len(all_pnfs) == 2 + pnf_1 = all_pnfs[0] + assert pnf_1.name == "one" + assert pnf_1.identifier == "1234" + assert pnf_1.unique_uuid == "5678" + assert pnf_1.version == "1.0" + assert pnf_1.status == const.CERTIFIED + assert pnf_1.created() + pnf_2 = all_pnfs[1] + assert pnf_2.name == "two" + assert pnf_2.identifier == "1235" + assert pnf_2.unique_uuid == "5679" + assert pnf_2.status == const.DRAFT + assert pnf_2.version == "1.0" + assert pnf_2.created() + mock_send.assert_called_once_with("GET", 'get Pnfs', 'https://sdc.api.be.simpledemo.onap.org:30204/sdc/v1/catalog/resources?resourceType=PNF') + + +def test_init_no_name(): + """Check init with no names.""" + pnf = Pnf() + assert isinstance(pnf, SdcResource) + assert pnf._identifier is None + assert pnf._version is None + assert pnf.name == "ONAP-test-PNF" + assert pnf.headers["USER_ID"] == "cs0008" + assert pnf.vsp is None + assert pnf.vendor is None + assert isinstance(pnf._base_url(), str) + +@mock.patch.object(Pnf, 'exists') +def test_init_with_name(mock_exists): + """Check init with names.""" + mock_exists.return_value = False + pnf = Pnf(name="YOLO") + assert pnf._identifier == None + assert pnf._version == None + assert pnf.name == "YOLO" + assert pnf.created() == False + assert pnf.headers["USER_ID"] == "cs0008" + assert pnf.vsp == None + assert isinstance(pnf._base_url(), str) + + +def test_equality_really_equals(): + """Check two pnfs are equals if name is the same.""" + pnf_1 = Pnf(name="equal") + pnf_1.identifier = "1234" + pnf_2 = Pnf(name="equal") + pnf_2.identifier = "1235" + assert pnf_1 == pnf_2 + + +def test_equality_not_equals(): + """Check two pnfs are not equals if name is not the same.""" + pnf_1 = Pnf(name="equal") + pnf_1.identifier = "1234" + pnf_2 = Pnf(name="not_equal") + pnf_2.identifier = "1234" + assert pnf_1 != pnf_2 + + +def test_equality_not_equals_not_same_object(): + """Check a pnf and something different are not equals.""" + pnf_1 = Pnf(name="equal") + pnf_1.identifier = "1234" + pnf_2 = SdcResource() + pnf_2.name = "equal" + assert pnf_1 != pnf_2 + + +@mock.patch.object(Pnf, 'get_all') +def test_exists_not_exists(mock_get_all): + """Return False if pnf doesn't exist in SDC.""" + pnf_1 = Pnf(name="one") + pnf_1.identifier = "1234" + mock_get_all.return_value = [pnf_1] + pnf = Pnf(name="two") + assert not pnf.exists() + + +@mock.patch.object(Pnf, 'get_all') +def test_exists(mock_get_all): + """Return True if pnf exists in SDC.""" + pnf_1 = Pnf(name="one") + pnf_1.identifier = "1234" + pnf_1.unique_uuid = "5689" + pnf_1.unique_identifier = "71011" + pnf_1.status = const.DRAFT + pnf_1.version = "1.1" + mock_get_all.return_value = [pnf_1] + pnf = Pnf(name="one") + assert pnf.exists() + assert pnf.identifier == "1234" + assert pnf.unique_uuid == "5689" + assert pnf.unique_identifier == "71011" + assert pnf.status == const.DRAFT + assert pnf.version == "1.1" + + +@mock.patch.object(Pnf, 'exists') +def test_load_created(mock_exists): + """Load is a wrapper around exists().""" + pnf = Pnf(name="one") + pnf.load() + mock_exists.assert_called_once() + + +@mock.patch.object(Pnf, 'exists') +def test_create_no_vsp_no_vendor(mock_exists): + """Do nothing if no vsp and no vendor""" + pnf = Pnf() + mock_exists.return_value = False + with pytest.raises(ParameterError) as err: + pnf.create() + assert err.type == ParameterError + assert str(err.value) == "Neither Vsp nor Vendor provided." + + +@mock.patch.object(Pnf, 'exists') +@mock.patch.object(Pnf, 'send_message_json') +@mock.patch.object(Pnf, "category") +def test_create_already_exists(mock_category, mock_send, mock_exists): + """Do nothing if already created in SDC.""" + pnf = Pnf() + vsp = Vsp() + vsp._identifier = "1232" + pnf.vsp = vsp + mock_exists.return_value = True + pnf.create() + mock_send.assert_not_called() + + +@mock.patch.object(Pnf, 'exists') +@mock.patch.object(Pnf, 'send_message_json') +@mock.patch.object(Pnf, "category", new_callable=mock.PropertyMock) +def test_create_issue_in_creation(mock_category, mock_send, mock_exists): +# def test_create_issue_in_creation(mock_send, mock_exists): + """Do nothing if not created but issue during creation.""" + pnf = Pnf() + vsp = Vsp() + vendor = Vendor() + vsp._identifier = "1232" + vsp.create_csar = MagicMock(return_value=True) + vsp.vendor = vendor + pnf.vsp = vsp + expected_data = '{\n "artifacts": {},\n "attributes": [],\n "capabilities": {},\n "categories": [\n {\n "normalizedName": "generic",\n "name": "Generic",\n "uniqueId": "resourceNewCategory.generic",\n "subcategories": [{"empty": false, "groupings": null, "icons": ["objectStorage", "compute"], "name": "Abstract", "normalizedName": "abstract", "ownerId": null, "type": null, "uniqueId": "resourceNewCategory.generic.abstract", "version": null}],\n "version": null,\n "ownerId": null,\n "empty": false,\n "type": null,\n "icons": null\n }\n ],\n "componentInstances": [],\n "componentInstancesAttributes": {},\n "componentInstancesProperties": {},\n "componentType": "RESOURCE",\n "contactId": "cs0008",\n \n "csarUUID": "None",\n "csarVersion": "1.0",\n "vendorName": "Generic-Vendor",\n \n "deploymentArtifacts": {},\n "description": "PNF",\n "icon": "defaulticon",\n "name": "ONAP-test-PNF",\n "properties": [],\n "groups": [],\n "requirements": {},\n "resourceType": "PNF",\n "tags": ["ONAP-test-PNF"],\n "toscaArtifacts": {},\n "vendorRelease": "1.0"\n}' + mock_exists.return_value = False + mock_send.side_effect = RequestError + rc = ResourceCategory( + name="Generic" + ) + rc.normalized_name="generic" + rc.unique_id="resourceNewCategory.generic" + rc.subcategories=[{"empty": False, "groupings": None, "icons": ["objectStorage", "compute"], "name": "Abstract", "normalizedName": "abstract", "ownerId": None, "type": None, "uniqueId": "resourceNewCategory.generic.abstract", "version": None}] + rc.version=None + rc.owner_id=None + rc.empty=False + rc.type=None + rc.icons=None + mock_category.return_value = rc + with pytest.raises(RequestError) as exc: + pnf.create() + mock_send.assert_called_once_with("POST", "create Pnf", 'https://sdc.api.fe.simpledemo.onap.org:30207/sdc1/feProxy/rest/v1/catalog/resources', data=expected_data) + assert not pnf.created() + + +@mock.patch.object(Pnf, 'exists') +@mock.patch.object(Pnf, 'send_message_json') +@mock.patch.object(Pnf, "category", new_callable=mock.PropertyMock) +def test_create_OK(mock_category, mock_send, mock_exists): + """Create and update object.""" + pnf = Pnf() + vsp = Vsp() + vendor = Vendor() + vsp._identifier = "1232" + pnf.vsp = vsp + vsp.vendor = vendor + vsp._csar_uuid = "1234" + expected_data = '{\n "artifacts": {},\n "attributes": [],\n "capabilities": {},\n "categories": [\n {\n "normalizedName": "generic",\n "name": "Generic",\n "uniqueId": "resourceNewCategory.generic",\n "subcategories": [{"empty": false, "groupings": null, "icons": ["objectStorage", "compute"], "name": "Abstract", "normalizedName": "abstract", "ownerId": null, "type": null, "uniqueId": "resourceNewCategory.generic.abstract", "version": null}],\n "version": null,\n "ownerId": null,\n "empty": false,\n "type": null,\n "icons": null\n }\n ],\n "componentInstances": [],\n "componentInstancesAttributes": {},\n "componentInstancesProperties": {},\n "componentType": "RESOURCE",\n "contactId": "cs0008",\n \n "csarUUID": "1234",\n "csarVersion": "1.0",\n "vendorName": "Generic-Vendor",\n \n "deploymentArtifacts": {},\n "description": "PNF",\n "icon": "defaulticon",\n "name": "ONAP-test-PNF",\n "properties": [],\n "groups": [],\n "requirements": {},\n "resourceType": "PNF",\n "tags": ["ONAP-test-PNF"],\n "toscaArtifacts": {},\n "vendorRelease": "1.0"\n}' + mock_exists.return_value = False + mock_send.return_value = {'resourceType': 'PNF', 'name': 'one', 'uuid': '1234', 'invariantUUID': '5678', 'version': '1.0', 'uniqueId': '91011', 'lifecycleState': 'NOT_CERTIFIED_CHECKOUT'} + rc = ResourceCategory( + name="Generic" + ) + rc.normalized_name="generic" + rc.unique_id="resourceNewCategory.generic" + rc.subcategories=[{"empty": False, "groupings": None, "icons": ["objectStorage", "compute"], "name": "Abstract", "normalizedName": "abstract", "ownerId": None, "type": None, "uniqueId": "resourceNewCategory.generic.abstract", "version": None}] + rc.version=None + rc.owner_id=None + rc.empty=False + rc.type=None + rc.icons=None + mock_category.return_value = rc + pnf.create() + mock_send.assert_called_once_with("POST", "create Pnf", 'https://sdc.api.fe.simpledemo.onap.org:30207/sdc1/feProxy/rest/v1/catalog/resources', data=expected_data) + assert pnf.created() + assert pnf._status == const.DRAFT + assert pnf.identifier == "1234" + assert pnf.unique_uuid == "5678" + assert pnf.version == "1.0" + +@mock.patch.object(Pnf, 'exists') +@mock.patch.object(Pnf, 'load') +def test_version_no_load_no_created(mock_load, mock_exists): + """Test versions when not created.""" + mock_exists.return_value = False + pnf = Pnf() + assert pnf.version is None + mock_load.assert_not_called() + +@mock.patch.object(Pnf, 'load') +def test_version_no_load_created(mock_load): + """Test versions when created.""" + pnf = Pnf() + pnf.identifier = "1234" + pnf._version = "64" + assert pnf.version == "64" + mock_load.assert_not_called() + + +@mock.patch.object(Pnf, 'load') +def test_version_with_load(mock_load): + """Test versions when not created but with identifier.""" + pnf = Pnf() + pnf.identifier = "1234" + assert pnf.version is None + mock_load.assert_called_once() + +@mock.patch.object(Pnf, 'exists') +@mock.patch.object(Pnf, 'load') +def test_status_no_load_no_created(mock_load, mock_exists): + """Test status when not created.""" + mock_exists.return_value = False + pnf = Pnf() + assert pnf.status is None + + +@pytest.mark.parametrize("status", [const.COMMITED, const.CERTIFIED, const.UPLOADED, const.VALIDATED]) +@mock.patch.object(Pnf, 'exists') +@mock.patch.object(Pnf, 'load') +@mock.patch.object(Pnf, 'send_message') +def test_submit_not_Commited(mock_send, mock_load, mock_exists, status): + """Do nothing if not created.""" + mock_exists.return_value = False + pnf = Pnf() + pnf._status = status + pnf.submit() + mock_send.assert_not_called() + +@mock.patch.object(Pnf, 'exists') +@mock.patch.object(Pnf, 'load') +@mock.patch.object(Pnf, 'send_message') +def test_submit_OK(mock_send, mock_load, mock_exists): + """Don't update status if submission NOK.""" + mock_exists.return_value = True + pnf = Pnf() + pnf._status = const.COMMITED + expected_data = '{\n "userRemarks": "certify"\n}' + pnf._version = "1234" + pnf._unique_identifier = "12345" + pnf.submit() + mock_send.assert_called_once_with( + "POST", "Certify Pnf", + 'https://sdc.api.fe.simpledemo.onap.org:30207/sdc1/feProxy/rest/v1/catalog/resources/12345/lifecycleState/Certify', + data=expected_data) + + +@mock.patch.object(Pnf, 'load') +@mock.patch.object(Pnf, 'certify') +@mock.patch.object(Pnf, 'submit') +@mock.patch.object(Pnf, 'create') +def test_onboard_new_pnf(mock_create, mock_submit, mock_certify, mock_load): + getter_mock = mock.Mock(wraps=Pnf.status.fget) + mock_status = Pnf.status.getter(getter_mock) + with mock.patch.object(Pnf, 'status', mock_status): + getter_mock.side_effect = [None, const.APPROVED, const.APPROVED, + const.APPROVED, const.APPROVED] + vsp = Vsp() + pnf = Pnf(vsp=vsp) + pnf._time_wait = 0 + pnf.onboard() + mock_create.assert_called_once() + mock_submit.assert_not_called() + mock_certify.assert_not_called() + mock_load.assert_not_called() + +@mock.patch.object(Pnf, 'load') +@mock.patch.object(Pnf, 'certify') +@mock.patch.object(Pnf, 'submit') +@mock.patch.object(Pnf, 'create') +def test_onboard_pnf_submit(mock_create, mock_submit, mock_certify, mock_load): + getter_mock = mock.Mock(wraps=Pnf.status.fget) + mock_status = Pnf.status.getter(getter_mock) + with mock.patch.object(Pnf, 'status', mock_status): + getter_mock.side_effect = [const.DRAFT, const.DRAFT, const.APPROVED, + const.APPROVED, const.APPROVED, const.APPROVED] + pnf = Pnf() + pnf._time_wait = 0 + pnf.onboard() + mock_create.assert_not_called() + mock_submit.assert_called_once() + mock_certify.assert_not_called() + mock_load.assert_not_called() + +@mock.patch.object(Pnf, 'load') +@mock.patch.object(Pnf, 'certify') +@mock.patch.object(Pnf, 'submit') +@mock.patch.object(Pnf, 'create') +def test_onboard_pnf_certify(mock_create, mock_submit, mock_certify, mock_load): + getter_mock = mock.Mock(wraps=Pnf.status.fget) + mock_status = Pnf.status.getter(getter_mock) + with mock.patch.object(Pnf, 'status', mock_status): + getter_mock.side_effect = [const.CHECKED_IN, const.CHECKED_IN, const.CHECKED_IN, + const.APPROVED, const.APPROVED, const.APPROVED, + const.APPROVED] + pnf = Pnf() + pnf._time_wait = 0 + pnf.onboard() + mock_create.assert_not_called() + mock_submit.assert_not_called() + mock_certify.assert_called_once() + mock_load.assert_not_called() + +@mock.patch.object(Pnf, 'load') +@mock.patch.object(Pnf, 'certify') +@mock.patch.object(Pnf, 'submit') +@mock.patch.object(Pnf, 'create') +def test_onboard_pnf_load(mock_create, mock_submit, mock_certify, mock_load): + getter_mock = mock.Mock(wraps=Pnf.status.fget) + mock_status = Pnf.status.getter(getter_mock) + with mock.patch.object(Pnf, 'status', mock_status): + getter_mock.side_effect = [const.CERTIFIED, const.CERTIFIED, + const.CERTIFIED, const.CERTIFIED, + const.APPROVED, const.APPROVED, + const.APPROVED] + pnf = Pnf() + pnf._time_wait = 0 + pnf.onboard() + mock_create.assert_not_called() + mock_submit.assert_not_called() + mock_certify.assert_not_called() + mock_load.assert_called_once() + +@mock.patch.object(Pnf, 'load') +@mock.patch.object(Pnf, 'submit') +@mock.patch.object(Pnf, 'create') +@mock.patch.object(Pnf, 'certify') +def test_onboard_whole_pnf_vsp(mock_certify, mock_create, mock_submit, mock_load): + """Test onboarding with vsp""" + getter_mock = mock.Mock(wraps=Pnf.status.fget) + mock_status = Pnf.status.getter(getter_mock) + with mock.patch.object(Pnf, 'status', mock_status): + getter_mock.side_effect = [None, const.DRAFT, const.DRAFT, + const.CHECKED_IN, const.CHECKED_IN, const.CHECKED_IN, + const.CERTIFIED, const.CERTIFIED, const.CERTIFIED, + const.CERTIFIED, const.APPROVED, const.APPROVED, + const.APPROVED, const.APPROVED] + vsp = Vsp() + pnf = Pnf(vsp=vsp) + pnf._time_wait = 0 + pnf.onboard() + mock_create.assert_called_once() + mock_submit.assert_called_once() + mock_load.assert_called_once() + mock_certify.assert_called_once() + +@mock.patch.object(Pnf, 'load') +@mock.patch.object(Pnf, 'submit') +@mock.patch.object(Pnf, 'create') +@mock.patch.object(Pnf, 'certify') +def test_onboard_whole_pnf_vendor(mock_certify, mock_create, mock_submit, mock_load): + """Test onboarding with vendor""" + getter_mock = mock.Mock(wraps=Pnf.status.fget) + mock_status = Pnf.status.getter(getter_mock) + with mock.patch.object(Pnf, 'status', mock_status): + getter_mock.side_effect = [None, const.DRAFT, const.DRAFT, + const.CHECKED_IN, const.CHECKED_IN, const.CHECKED_IN, + const.CERTIFIED, const.CERTIFIED, const.CERTIFIED, + const.CERTIFIED, const.APPROVED, const.APPROVED, + const.APPROVED, const.APPROVED] + vendor = Vendor() + pnf = Pnf(vendor=vendor) + pnf._time_wait = 0 + pnf.onboard() + mock_create.assert_called_once() + mock_submit.assert_called_once() + mock_load.assert_called_once() + mock_certify.assert_called_once() + +@mock.patch.object(Pnf, "send_message_json") +def test_add_properties(mock_send_message_json): + pnf = Pnf(name="test") + pnf._identifier = "toto" + pnf._unique_identifier = "toto" + pnf._status = const.CERTIFIED + with pytest.raises(StatusError) as err: + pnf.add_property(Property(name="test", property_type="string")) + pnf._status = const.DRAFT + pnf.add_property(Property(name="test", property_type="string")) + mock_send_message_json.assert_called_once() + +@mock.patch.object(Pnf, 'load') +@mock.patch.object(Pnf, 'send_message') +def test_add_artifact_to_pnf(mock_send_message, mock_load): + """Test Pnf add artifact""" + pnf = Pnf(name="test") + pnf.status = const.DRAFT + mycbapath = Path(Path(__file__).resolve().parent, "data/vLB_CBA_Python.zip") + + result = pnf.add_deployment_artifact(artifact_label="cba", + artifact_type="CONTROLLER_BLUEPRINT_ARCHIVE", + artifact_name="vLB_CBA_Python.zip", + artifact=mycbapath) + mock_send_message.assert_called() + method, description, url = mock_send_message.call_args[0] + assert method == "POST" + assert description == "Add deployment artifact for test sdc resource" + assert url == ("https://sdc.api.fe.simpledemo.onap.org:30207/sdc1/feProxy/rest/v1/catalog/resources/" + f"{pnf.unique_identifier}/artifacts") + + +@mock.patch.object(Pnf, "created") +@mock.patch.object(ResourceCategory, "get") +def test_pnf_category(mock_resource_category, mock_created): + mock_created.return_value = False + pnf = Pnf(name="test") + _ = pnf.category + mock_resource_category.assert_called_once_with(name="Generic", subcategory="Abstract") + mock_resource_category.reset_mock() + + pnf = Pnf(name="test", category="test", subcategory="test") + _ = pnf.category + mock_resource_category.assert_called_once_with(name="test", subcategory="test") + mock_resource_category.reset_mock() + + mock_created.return_value = True + _ = pnf.category + mock_resource_category.assert_called_once_with(name="test", subcategory="test") diff --git a/tests/test_preload.py b/tests/test_preload.py new file mode 100644 index 0000000..0a97043 --- /dev/null +++ b/tests/test_preload.py @@ -0,0 +1,150 @@ +# Copyright 2022 Orange, 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 json +from collections.abc import Iterable +from unittest import mock + +from onapsdk.sdnc.preload import NetworkPreload, PreloadInformation, VfModulePreload +from onapsdk.so.instantiation import Subnet + + +PRELOAD_INFORMATIONS = { + 'preload-information': { + 'preload-list': [ + { + 'preload-id': 'Python_ONAP_SDK_network_instance_338d5238-22fe-44d1-857a-223e2f6edd9b', + 'preload-type': 'network', + 'preload-data': { + 'preload-network-topology-information': { + 'physical-network-name': 'Not Aplicable', + 'is-provider-network': False, + 'is-external-network': False, + 'network-topology-identifier-structure': { + 'network-technology': 'neutron', + 'network-type': 'Generic NeutronNet', + 'network-name': 'Python_ONAP_SDK_network_instance_338d5238-22fe-44d1-857a-223e2f6edd9b', + 'network-role': 'integration_test_net' + }, + 'is-shared-network': False + }, + 'preload-oper-status': { + 'create-timestamp': '2020-06-26T09:12:03.708Z', + 'order-status': 'PendingAssignment' + } + } + }, + { + 'preload-id': 'Python_ONAP_SDK_network_instance_5d61bcf6-ec37-4cea-9d1b-744d0c2b75b9', + 'preload-type': 'network', + 'preload-data': { + 'preload-network-topology-information': { + 'is-provider-network': False, + 'is-external-network': False, + 'network-topology-identifier-structure': { + 'network-technology': 'neutron', + 'network-type': 'Generic NeutronNet', + 'network-name': 'Python_ONAP_SDK_network_instance_5d61bcf6-ec37-4cea-9d1b-744d0c2b75b9', + 'network-id': '1234', + 'network-role': 'integration_test_net' + }, + 'is-shared-network': False + }, + 'preload-oper-status': { + 'create-timestamp': '2020-06-25T12:22:35.939Z', + 'order-status': 'PendingAssignment' + } + } + } + ] + } +} + + +@mock.patch.object(VfModulePreload, "send_message_json") +def test_vf_module_preload_gr_api(mock_send_message_json): + VfModulePreload.upload_vf_module_preload(vnf_instance=mock.MagicMock(), + vf_module_instance_name="test", + vf_module=mock.MagicMock()) + mock_send_message_json.assert_called_once() + method, description, url = mock_send_message_json.call_args[0] + assert method == "POST" + assert description == "Upload VF module preload using GENERIC-RESOURCE-API" + assert url == (f"{VfModulePreload.base_url}/restconf/operations/" + "GENERIC-RESOURCE-API:preload-vf-module-topology-operation") + + +@mock.patch.object(PreloadInformation, "send_message_json") +def test_preload_information(mock_send_message_json): + mock_send_message_json.return_value = PRELOAD_INFORMATIONS + preload_informations = PreloadInformation.get_all() + assert isinstance(preload_informations, Iterable) + preload_informations_list = list(preload_informations) + assert len(preload_informations_list) == 2 + preload_information = preload_informations_list[0] + assert isinstance(preload_information, PreloadInformation) + assert preload_information.preload_id == "Python_ONAP_SDK_network_instance_338d5238-22fe-44d1-857a-223e2f6edd9b" + assert preload_information.preload_type == "network" + + +@mock.patch.object(NetworkPreload, "send_message_json") +def test_network_preload(mock_send_message_json): + NetworkPreload.upload_network_preload( + mock.MagicMock(), + network_instance_name="test_instance", + ) + mock_send_message_json.assert_called_once() + _, _, kwargs = mock_send_message_json.mock_calls[0] + assert "data" in kwargs + data = json.loads(kwargs["data"]) + assert not len(data["input"]["preload-network-topology-information"]["subnets"]) + + mock_send_message_json.reset_mock() + NetworkPreload.upload_network_preload( + mock.MagicMock(), + network_instance_name="test_instance", + subnets=[Subnet( + name="test_subnet", + start_address="127.0.0.0", + gateway_address="127.0.0.1" + )] + ) + mock_send_message_json.assert_called_once() + _, _, kwargs = mock_send_message_json.mock_calls[0] + assert "data" in kwargs + data = json.loads(kwargs["data"]) + assert len(data["input"]["preload-network-topology-information"]["subnets"]) + assert data["input"]["preload-network-topology-information"]["subnets"][0]["subnet-name"] == "test_subnet" + assert data["input"]["preload-network-topology-information"]["subnets"][0]["dhcp-enabled"] == "N" + + mock_send_message_json.reset_mock() + NetworkPreload.upload_network_preload( + mock.MagicMock(), + network_instance_name="test_instance", + subnets=[Subnet( + name="test_subnet", + start_address="127.0.0.0", + gateway_address="127.0.0.1", + dhcp_enabled=True, + dhcp_start_address="192.168.0.0", + dhcp_end_address="192.168.0.1" + )] + ) + mock_send_message_json.assert_called_once() + _, _, kwargs = mock_send_message_json.mock_calls[0] + assert "data" in kwargs + data = json.loads(kwargs["data"]) + assert len(data["input"]["preload-network-topology-information"]["subnets"]) + assert data["input"]["preload-network-topology-information"]["subnets"][0]["subnet-name"] == "test_subnet" + assert data["input"]["preload-network-topology-information"]["subnets"][0]["dhcp-start-address"] == "192.168.0.0" + assert data["input"]["preload-network-topology-information"]["subnets"][0]["dhcp-end-address"] == "192.168.0.1" diff --git a/tests/test_sdc_category_management.py b/tests/test_sdc_category_management.py new file mode 100644 index 0000000..59ca5f5 --- /dev/null +++ b/tests/test_sdc_category_management.py @@ -0,0 +1,349 @@ +"""Test SdcElement module.""" +# Copyright 2022 Orange, 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 unittest import mock + +import pytest +from onapsdk.exceptions import APIError, ResourceNotFound + +from onapsdk.sdc.category_management import ResourceCategory, ServiceCategory + + +CATEGORIES = { + "categories": { + 'resourceCategories': [ + { + 'name': 'Network L4+', + 'normalizedName': 'network l4+', + 'uniqueId': 'resourceNewCategory.network l4+', + 'icons': None, + 'subcategories': [ + { + 'name': 'Common Network Resources', + 'normalizedName': 'common network resources', + 'uniqueId': 'resourceNewCategory.network l4+.common network resources', + 'icons': ['network'], + 'groupings': None, + 'version': None, + 'ownerId': None, + 'empty': False, + 'type': None + } + ], + 'version': None, + 'ownerId': None, + 'empty': False, + 'type': None + }, + { + 'name': 'Network L2-3', + 'normalizedName': 'network l2-3', + 'uniqueId': 'resourceNewCategory.network l2-3', + 'icons': None, + 'subcategories': [ + { + 'name': 'Router', + 'normalizedName': 'router', + 'uniqueId': 'resourceNewCategory.network l2-3.router', + 'icons': ['router', 'vRouter'], + 'groupings': None, + 'version': None, + 'ownerId': None, + 'empty': False, + 'type': None + }, + { + 'name': 'LAN Connectors', + 'normalizedName': 'lan connectors', + 'uniqueId': 'resourceNewCategory.network l2-3.lan connectors', + 'icons': ['network', 'connector', 'port'], + 'groupings': None, + 'version': None, + 'ownerId': None, + 'empty': False, + 'type': None + }, + { + 'name': 'Infrastructure', + 'normalizedName': 'infrastructure', + 'uniqueId': 'resourceNewCategory.network l2-3.infrastructure', + 'icons': ['ucpe'], + 'groupings': None, + 'version': None, + 'ownerId': None, + 'empty': False, + 'type': None + }, + { + 'name': 'Gateway', + 'normalizedName': 'gateway', + 'uniqueId': 'resourceNewCategory.network l2-3.gateway', + 'icons': ['gateway'], + 'groupings': None, + 'version': None, + 'ownerId': None, + 'empty': False, + 'type': None + }, + { + 'name': 'WAN Connectors', + 'normalizedName': 'wan connectors', + 'uniqueId': 'resourceNewCategory.network l2-3.wan connectors', + 'icons': ['network', 'connector', 'port'], + 'groupings': None, + 'version': None, + 'ownerId': None, + 'empty': False, + 'type': None + } + ], + 'version': None, + 'ownerId': None, + 'empty': False, + 'type': None + }, + { + 'name': 'Network Connectivity', + 'normalizedName': 'network connectivity', + 'uniqueId': 'resourceNewCategory.network connectivity', + 'icons': None, + 'subcategories': [ + { + 'name': 'Connection Points', + 'normalizedName': 'connection points', + 'uniqueId': 'resourceNewCategory.network connectivity.connection points', + 'icons': ['cp'], + 'groupings': None, + 'version': None, + 'ownerId': None, + 'empty': False, + 'type': None + }, + { + 'name': 'Virtual Links', + 'normalizedName': 'virtual links', + 'uniqueId': 'resourceNewCategory.network connectivity.virtual links', + 'icons': ['vl'], + 'groupings': None, + 'version': None, + 'ownerId': None, + 'empty': False, + 'type': None + } + ], + 'version': None, + 'ownerId': None, + 'empty': False, + 'type': None + }, + { + 'name': 'Configuration', + 'normalizedName': 'configuration', + 'uniqueId': 'resourceNewCategory.configuration', + 'icons': None, + 'subcategories': None, + 'version': None, + 'ownerId': None, + 'empty': False, + 'type': None + }, + ], + 'serviceCategories': [ + { + 'name': 'Partner Domain Service', + 'normalizedName': 'partner domain service', + 'uniqueId': 'serviceNewCategory.partner domain service', + 'icons': ['partner_domain_service'], + 'subcategories': None, + 'version': None, + 'ownerId': None, + 'empty': False, + 'type': None + }, + { + 'name': 'Mobility', + 'normalizedName': 'mobility', + 'uniqueId': 'serviceNewCategory.mobility', + 'icons': ['mobility'], + 'subcategories': None, + 'version': None, + 'ownerId': None, + 'empty': False, + 'type': None + }, + { + 'name': 'VoIP Call Control', + 'normalizedName': 'voip call control', + 'uniqueId': 'serviceNewCategory.voip call control', + 'icons': ['call_controll'], + 'subcategories': None, + 'version': None, + 'ownerId': None, + 'empty': False, + 'type': None + }, + { + 'name': 'E2E Service', + 'normalizedName': 'e2e service', + 'uniqueId': 'serviceNewCategory.e2e service', + 'icons': ['network_l_1-3'], + 'subcategories': None, + 'version': None, + 'ownerId': None, + 'empty': False, + 'type': None + }, + { + 'name': 'Network L4+', + 'normalizedName': 'network l4+', + 'uniqueId': 'serviceNewCategory.network l4+', + 'icons': ['network_l_4'], + 'subcategories': None, + 'version': None, + 'ownerId': None, + 'empty': False, + 'type': None + }, + { + 'name': 'Network L1-3', + 'normalizedName': 'network l1-3', + 'uniqueId': 'serviceNewCategory.network l1-3', + 'icons': ['network_l_1-3'], + 'subcategories': None, + 'version': None, + 'ownerId': None, + 'empty': False, + 'type': None + }, + { + 'name': 'Network Service', + 'normalizedName': 'network service', + 'uniqueId': 'serviceNewCategory.network service', + 'icons': ['network_l_1-3'], + 'subcategories': None, + 'version': None, + 'ownerId': None, + 'empty': False, + 'type': None + }, + ], + 'productCategories': [] + } +} + + +@mock.patch.object(ResourceCategory, "send_message_json") +def test_resource_category_exists(mock_send_message_json): + + rc = ResourceCategory(name="test_name") + mock_send_message_json.return_value = {} + assert not rc.exists() + mock_send_message_json.return_value = CATEGORIES + assert not rc.exists() + rc = ResourceCategory(name="Network Connectivity") + assert rc.exists() + +@mock.patch.object(ResourceCategory, "send_message_json") +def test_resource_category_get(mock_send_message_json): + + mock_send_message_json.return_value = CATEGORIES + rc = ResourceCategory.get(name="Network Connectivity") + assert rc.name == "Network Connectivity" + assert rc.normalized_name == "network connectivity" + assert rc.unique_id == "resourceNewCategory.network connectivity" + assert not rc.icons + assert not rc.version + assert not rc.owner_id + assert not rc.empty + assert not rc.type + assert len(rc.subcategories) == 2 + + with pytest.raises(ResourceNotFound): + ResourceCategory.get(name="Network Connectivity", subcategory="Toto") + rc = ResourceCategory.get(name="Network Connectivity", subcategory="Connection Points") + assert rc.name == "Network Connectivity" + assert rc.normalized_name == "network connectivity" + assert rc.unique_id == "resourceNewCategory.network connectivity" + assert not rc.icons + assert not rc.version + assert not rc.owner_id + assert not rc.empty + assert not rc.type + assert len(rc.subcategories) == 1 + + mock_send_message_json.side_effect = APIError + with pytest.raises(ResourceNotFound): + ResourceCategory.get(name="Network Connectivity") + + mock_send_message_json.side_effect = KeyError + with pytest.raises(ResourceNotFound): + ResourceCategory.get(name="Network Connectivity") + +@mock.patch.object(ResourceCategory, "send_message_json") +def test_resource_category_create(mock_send_message_json): + + mock_send_message_json.return_value = CATEGORIES + rc = ResourceCategory.create(name="Network Connectivity") + assert rc.name == "Network Connectivity" + assert rc.normalized_name == "network connectivity" + assert rc.unique_id == "resourceNewCategory.network connectivity" + assert not rc.icons + assert not rc.version + assert not rc.owner_id + assert not rc.empty + assert not rc.type + ResourceCategory.create(name="New category") + +@mock.patch.object(ServiceCategory, "send_message_json") +def test_service_category_exists(mock_send_message_json): + + sc = ServiceCategory(name="test_name") + mock_send_message_json.return_value = CATEGORIES + assert not sc.exists() + sc = ServiceCategory(name="Partner Domain Service") + assert sc.exists() + mock_send_message_json.side_effect = APIError + assert not sc.exists() + mock_send_message_json.side_effect = KeyError + assert not sc.exists() + +@mock.patch.object(ServiceCategory, "send_message_json") +def test_service_category_get(mock_send_message_json): + + mock_send_message_json.return_value = {} + with pytest.raises(ResourceNotFound): + ServiceCategory.get(name="Partner Domain Service") + mock_send_message_json.return_value = CATEGORIES + sc = ServiceCategory.get(name="Partner Domain Service") + assert sc.name == "Partner Domain Service" + assert sc.normalized_name == "partner domain service" + assert sc.unique_id == "serviceNewCategory.partner domain service" + assert not sc.version + assert not sc.owner_id + assert not sc.empty + assert not sc.type + +@mock.patch.object(ServiceCategory, "send_message_json") +def test_service_category_create(mock_send_message_json): + + mock_send_message_json.return_value = CATEGORIES + sc = ServiceCategory.create(name="Partner Domain Service") + assert sc.name == "Partner Domain Service" + assert sc.normalized_name == "partner domain service" + assert sc.unique_id == "serviceNewCategory.partner domain service" + assert not sc.version + assert not sc.owner_id + assert not sc.empty + assert not sc.type + ServiceCategory.create(name="New category")
\ No newline at end of file diff --git a/tests/test_sdc_component.py b/tests/test_sdc_component.py new file mode 100644 index 0000000..ee77de7 --- /dev/null +++ b/tests/test_sdc_component.py @@ -0,0 +1,44 @@ +# Copyright 2022 Orange, 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 unittest import mock + +from onapsdk.sdc.component import Component + + +def test_sdc_component_delete(): + mock_sdc_resource = mock.MagicMock() + mock_parent_sdc_resource = mock.MagicMock() + mock_parent_sdc_resource.resource_inputs_url = "http://test.onap.org" + component = Component( + created_from_csar=False, + actual_component_uid="123", + unique_id="456", + normalized_name="789", + name="test_component", + origin_type="test-origin-type", + customization_uuid="098", + component_uid="765", + component_version="432", + tosca_component_name="test-tosca-component-name", + component_name="test-component-name", + sdc_resource=mock_sdc_resource, + parent_sdc_resource=mock_parent_sdc_resource, + group_instances=None + ) + component.delete() + mock_sdc_resource.send_message_json.assert_called_once_with( + "DELETE", + "Delete test_component component", + f"http://test.onap.org/resourceInstance/{component.unique_id}" + ) diff --git a/tests/test_sdc_element.py b/tests/test_sdc_element.py new file mode 100644 index 0000000..88bcd8f --- /dev/null +++ b/tests/test_sdc_element.py @@ -0,0 +1,109 @@ +"""Test SdcElement module.""" +# Copyright 2022 Orange, 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 unittest import mock + +from onapsdk.onap_service import OnapService +from onapsdk.sdc.sdc_element import SdcElement +from onapsdk.sdc.vendor import Vendor +from onapsdk.sdc.vsp import Vsp +from onapsdk.sdc import SDC +from onapsdk.utils.gui import GuiList + +def test_init(): + """Test the initialization.""" + element = Vendor() + assert isinstance(element, OnapService) + +def test_class_variables(): + """Test the class variables.""" + assert SdcElement.server == "SDC" + assert SdcElement.base_front_url == "https://sdc.api.fe.simpledemo.onap.org:30207" + assert SdcElement.base_back_url == "https://sdc.api.be.simpledemo.onap.org:30204" + assert SdcElement.headers == { + "Content-Type": "application/json", + "Accept": "application/json" + } + +@mock.patch.object(Vendor, 'created') +@mock.patch.object(Vendor, 'send_message_json') +def test__get_item_details_not_created(mock_send, mock_created): + vendor = Vendor() + mock_created.return_value = False + assert vendor._get_item_details() == {} + mock_send.assert_not_called() + +@mock.patch.object(Vsp, 'send_message_json') +def test__get_item_details_created(mock_send): + vsp = Vsp() + vsp.identifier = "1234" + mock_send.return_value = {'results': [{"creationTime": "2"}, {"creationTime": "3"}], "listCount": 2} + assert vsp._get_item_details() == {"creationTime": "3"} + mock_send.assert_called_once_with('GET', 'get item', "{}/items/1234/versions".format(vsp._base_url())) + +@mock.patch.object(Vsp, 'created') +@mock.patch.object(Vsp, 'send_message_json') +def test__get_items_version_details_not_created(mock_send, mock_created): + vsp = Vsp() + mock_created.return_value = False + assert vsp._get_item_version_details() == {} + mock_send.assert_not_called() + +@mock.patch.object(Vsp, 'load') +@mock.patch.object(Vsp, 'send_message_json') +def test__get_items_version_details_no_version(mock_send, mock_load): + vsp = Vsp() + vsp.identifier = "1234" + assert vsp._get_item_version_details() == {} + mock_send.assert_not_called() + +@mock.patch.object(Vsp, 'send_message_json') +def test__get_items_version_details(mock_send): + vsp = Vsp() + vsp.identifier = "1234" + vsp._version = "4567" + mock_send.return_value = {'return': 'value'} + assert vsp._get_item_version_details() == {'return': 'value'} + mock_send.assert_called_once_with('GET', 'get item version', "{}/items/1234/versions/4567".format(vsp._base_url())) + +@mock.patch.object(SDC, "send_message") +def test_get_guis(send_message_mock): + send_message_mock.return_value.status_code = 200 + send_message_mock.return_value.url = "https://sdc.api.fe.simpledemo.onap.org:30207/sdc1/portal" + gui_results = SDC.get_guis() + assert type(gui_results) == GuiList + assert gui_results.guilist[0].url == send_message_mock.return_value.url + assert gui_results.guilist[0].status == send_message_mock.return_value.status_code + +@mock.patch.object(SDC, "get_all") +@mock.patch.object(Vsp, "created") +def test_exists_versions(mock_vsp_created, mock_get_all): + mock_vsp_created.return_value = True + sdc_el1 = Vsp(name="test1") + sdc_el1._version = "1.0" + sdc_el1._identifier = "123" + sdc_el2 = Vsp(name="test2") + sdc_el2._version = "2.0" + sdc_el2._identifier = "123" + mock_get_all.return_value = [sdc_el1, sdc_el2] + assert sdc_el1.exists() + + sdc_el1 = Vsp(name="test1") + sdc_el1._version = "anything" + sdc_el1._identifier = "123" + sdc_el2 = Vsp(name="test2") + sdc_el2._version = "what_is_not_a_float" + sdc_el2._identifier = "123" + mock_get_all.return_value = [sdc_el1, sdc_el2] + assert sdc_el1.exists() diff --git a/tests/test_sdc_resource.py b/tests/test_sdc_resource.py new file mode 100644 index 0000000..1bf562a --- /dev/null +++ b/tests/test_sdc_resource.py @@ -0,0 +1,487 @@ +"""Test SdcResource module.""" +# Copyright 2022 Orange, 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 unittest import mock +import logging + +import pytest + +import onapsdk.constants as const +from onapsdk.exceptions import ParameterError, RequestError, ResourceNotFound +from onapsdk.onap_service import OnapService +from onapsdk.sdc.component import Component +from onapsdk.sdc.properties import ComponentProperty, Input, NestedInput, Property +from onapsdk.sdc.sdc_resource import SdcResource +from onapsdk.sdc.service import Service +from onapsdk.sdc.vf import Vf +from onapsdk.utils.headers_creator import headers_sdc_tester +from onapsdk.utils.headers_creator import headers_sdc_creator + + +COMPONENT_PROPERTIES = [ + { + "uniqueId":"3d9a184f-4268-4a0e-9ddd-252e49670013.vf_module_id", + "type":"string", + "required":False, + "definition":False, + "description":"The vFirewall Module ID is provided by ECOMP", + "password":False, + "name":"vf_module_id", + "label":"vFirewall module ID", + "hidden":False, + "immutable":False, + "isDeclaredListInput":False, + "getInputProperty":False, + "empty":False + },{ + "uniqueId":"74f79006-ae56-4d58-947e-6a5089000774.skip_post_instantiation_configuration", + "type":"boolean", + "required":False, + "definition":False, + "password":False, + "name":"skip_post_instantiation_configuration", + "value":"true", + "hidden":False, + "immutable":False, + "parentUniqueId":"74f79006-ae56-4d58-947e-6a5089000774", + "isDeclaredListInput":False, + "getInputProperty":False, + "ownerId":"74f79006-ae56-4d58-947e-6a5089000774", + "empty":False + } +] + + + +def test_init(): + """Test the initialization.""" + element = SdcResource() + assert isinstance(element, OnapService) + +def test_class_variables(): + """Test the class variables.""" + assert SdcResource.server == "SDC" + assert SdcResource.base_front_url == "https://sdc.api.fe.simpledemo.onap.org:30207" + assert SdcResource.base_back_url == "https://sdc.api.be.simpledemo.onap.org:30204" + assert SdcResource.headers == { + "Content-Type": "application/json", + "Accept": "application/json", + "Authorization": "Basic YWFpOktwOGJKNFNYc3pNMFdYbGhhazNlSGxjc2UyZ0F3ODR2YW9HR21KdlV5MlU=", + "USER_ID": "cs0008", + "X-ECOMP-InstanceID": "onapsdk" + } + +@mock.patch.object(Vf, 'load') +def test__unique_uuid_no_load(mock_load): + vf = Vf() + vf.identifier = "1234" + vf._unique_uuid = "4567" + assert vf.unique_uuid == "4567" + mock_load.assert_not_called() + +@mock.patch.object(Vf, 'load') +def test__unique_uuid_load(mock_load): + vf = Vf() + vf.identifier = "1234" + assert vf.unique_uuid == None + mock_load.assert_called_once() + +def test__unique_uuid_setter(): + vf = Vf() + vf.identifier = "1234" + vf.unique_uuid = "4567" + assert vf._unique_uuid == "4567" + +@mock.patch.object(Vf, 'deep_load') +def test__unique_identifier_load(mock_load): + vf = Vf() + vf.identifier = "1234" + assert vf.unique_identifier == None + mock_load.assert_called_once() + +@mock.patch.object(Vf, 'deep_load') +def test__unique_identifier_no_load(mock_load): + vf = Vf() + vf.identifier = "1234" + vf._unique_identifier= "toto" + assert vf.unique_identifier == "toto" + mock_load.assert_not_called() + +def test__status_setter(): + vf = Vf() + vf.identifier = "1234" + vf.status = "4567" + assert vf._status == "4567" + +@mock.patch.object(Vf, 'created') +@mock.patch.object(Vf, 'send_message_json') +def test__deep_load_request_error(mock_send, mock_created): + mock_created.return_value = True + vf = Vf() + vf.identifier = "1234" + vf._version = "4567" + vf._status = const.CHECKED_IN + mock_send.side_effect = RequestError + with pytest.raises(RequestError) as err: + vf.deep_load() + assert err.type == RequestError + assert vf._unique_identifier is None + mock_send.assert_called_once_with('GET', 'Deep Load Vf', + "{}/sdc1/feProxy/rest/v1/screen?excludeTypes=VFCMT&excludeTypes=Configuration".format(vf.base_front_url), + headers=headers_sdc_creator(vf.headers)) + +@mock.patch.object(Vf, 'created') +@mock.patch.object(Vf, 'send_message_json') +def test__deep_load_response_OK(mock_send, mock_created): + mock_created.return_value = True + vf = Vf() + vf.identifier = "5689" + vf.unique_uuid = "1234" + vf._version = "4567" + vf._status = const.CHECKED_IN + mock_send.return_value = {'resources': [{'uuid': '5689', 'name': 'test', 'uniqueId': '71011', 'invariantUUID': '1234', 'categories': [{'name': 'test', 'subcategories': [{'name': 'test_subcategory'}]}]}]} + vf.deep_load() + assert vf.unique_identifier == "71011" + assert vf._category_name == "test" + assert vf._subcategory_name == "test_subcategory" + mock_send.assert_called_once_with('GET', 'Deep Load Vf', + "{}/sdc1/feProxy/rest/v1/screen?excludeTypes=VFCMT&excludeTypes=Configuration".format(vf.base_front_url), + headers=headers_sdc_creator(vf.headers)) + +@mock.patch.object(Service, 'created') +@mock.patch.object(Service, 'send_message_json') +def test__deep_load_response_OK_dependency(mock_send, mock_created): + mock_created.return_value = True + vf = Service() + vf.identifier = "4321" + vf.unique_uuid = "1234" + vf._version = "4567" + vf._status = const.CHECKED_IN + mock_send.side_effect = [{'services': [{'uuid': '5689', 'name': 'test', 'uniqueId': '71011', 'invariantUUID': '1234', 'categories': [{'name': 'test', 'subcategories': [{'name': 'test_subcategory'}]}]}]}, [{'version': '4567', 'uniqueId': '71011'}]] + vf.deep_load() + assert vf.unique_identifier == "71011" + +@mock.patch.object(Vf, 'created') +@mock.patch.object(Vf, 'send_message_json') +def test__deep_load_response_NOK(mock_send, mock_created): + mock_created.return_value = True + vf = Vf() + vf.identifier = "5678" + vf.unique_uuid = "1234" + vf._version = "4567" + vf._status = const.CHECKED_IN + mock_send.return_value = {'resources': [{'uuid': '5689', 'name': 'test', 'uniqueId': '71011', 'invariantUUID': '1234', }]} + vf.deep_load() + assert vf._unique_identifier is None + mock_send.assert_called_once_with('GET', 'Deep Load Vf', + "{}/sdc1/feProxy/rest/v1/screen?excludeTypes=VFCMT&excludeTypes=Configuration".format(vf.base_front_url), + headers=headers_sdc_creator(vf.headers)) + +@mock.patch.object(Vf, 'created') +@mock.patch.object(Vf, 'send_message_json') +def test__deep_load_response_OK_under_cert(mock_send, mock_created): + mock_created.return_value = True + vf = Vf() + vf.identifier = "5689" + vf.unique_uuid = "1234" + vf._version = "4567" + vf._status = const.UNDER_CERTIFICATION + mock_send.return_value = {'resources': [{'uuid': '5689', 'name': 'test', 'uniqueId': '71011', 'invariantUUID': '1234', 'categories': [{'name': 'test', 'subcategories': [{'name': 'test_subcategory'}]}]}]} + vf.deep_load() + assert vf.unique_identifier == "71011" + assert vf._category_name == "test" + assert vf._subcategory_name == "test_subcategory" + mock_send.assert_called_once_with('GET', 'Deep Load Vf', + "{}/sdc1/feProxy/rest/v1/screen?excludeTypes=VFCMT&excludeTypes=Configuration".format(vf.base_front_url), + headers=headers_sdc_tester(vf.headers)) + +@mock.patch.object(Vf, 'created') +@mock.patch.object(Vf, 'send_message_json') +def test__deep_load_response_NOK_under_cert(mock_send, mock_created): + mock_created.return_value = True + vf = Vf() + vf.identifier = "5678" + vf.unique_uuid = "1234" + vf._version = "4567" + vf._status = const.UNDER_CERTIFICATION + mock_send.return_value = {'resources': [{'uuid': '5689', 'name': 'test', 'invariantUUID': '1234', 'uniqueId': '71011'}]} + vf.deep_load() + assert vf._unique_identifier is None + mock_send.assert_called_once_with('GET', 'Deep Load Vf', + "{}/sdc1/feProxy/rest/v1/screen?excludeTypes=VFCMT&excludeTypes=Configuration".format(vf.base_front_url), + headers=headers_sdc_tester(vf.headers)) + +def test__parse_sdc_status_certified(): + assert SdcResource._parse_sdc_status("CERTIFIED", None, logging.getLogger()) == const.CERTIFIED + +def test__parse_sdc_status_certified_not_approved(): + assert SdcResource._parse_sdc_status("CERTIFIED", + const.DISTRIBUTION_NOT_APPROVED, + logging.getLogger()) == const.CERTIFIED + +def test__parse_sdc_status_certified_approved(): + assert SdcResource._parse_sdc_status("CERTIFIED", + const.DISTRIBUTION_APPROVED, + logging.getLogger()) == const.CERTIFIED + +def test__parse_sdc_status_distributed(): + assert SdcResource._parse_sdc_status("CERTIFIED", const.SDC_DISTRIBUTED, + logging.getLogger()) == const.DISTRIBUTED + +def test__parse_sdc_status_draft(): + assert SdcResource._parse_sdc_status(const.NOT_CERTIFIED_CHECKOUT, None, + logging.getLogger() ) == const.DRAFT + +def test__parse_sdc_status_draft(): + assert SdcResource._parse_sdc_status(const.NOT_CERTIFIED_CHECKIN, None, + logging.getLogger() ) == const.CHECKED_IN + +def test__parse_sdc_status_submitted(): + assert SdcResource._parse_sdc_status(const.READY_FOR_CERTIFICATION, None, + logging.getLogger() ) == const.SUBMITTED + +def test__parse_sdc_status_under_certification(): + assert SdcResource._parse_sdc_status(const.CERTIFICATION_IN_PROGRESS, None, + logging.getLogger() ) == const.UNDER_CERTIFICATION + +def test__parse_sdc_status_unknown(): + assert SdcResource._parse_sdc_status("UNKNOWN", None, logging.getLogger() ) == 'UNKNOWN' + +def test__parse_sdc_status_empty(): + assert SdcResource._parse_sdc_status("", None, logging.getLogger() ) is None + +def test__really_submit(): + sdcResource = SdcResource() + with pytest.raises(NotImplementedError): + sdcResource._really_submit() + +def test__action_url_no_action_type(): + sdcResource = SdcResource() + url = sdcResource._action_url("base", "subpath", "version_path") + assert url == "base/resources/version_path/lifecycleState/subpath" + +def test__action_url_action_type(): + sdcResource = SdcResource() + url = sdcResource._action_url("base", "subpath", "version_path", + action_type="distribution") + assert url == "base/resources/version_path/distribution/subpath" + +@mock.patch.object(SdcResource, '_parse_sdc_status') +def test_update_informations_from_sdc_creation_no_distribitution_state(mock_parse): + mock_parse.return_value = "12" + sdcResource = SdcResource() + details = {'invariantUUID': '1234', + 'lifecycleState': 'state', + 'version': 'v12', + 'uniqueId': '5678'} + + sdcResource.update_informations_from_sdc_creation(details) + assert sdcResource.unique_uuid == "1234" + assert sdcResource.status == "12" + assert sdcResource.version == "v12" + assert sdcResource.unique_identifier == "5678" + mock_parse.assert_called_once_with("state", None, mock.ANY) + +@mock.patch.object(SdcResource, '_parse_sdc_status') +def test_update_informations_from_sdc_creation_distribitution_state(mock_parse): + mock_parse.return_value = "bgt" + sdcResource = SdcResource() + details = {'invariantUUID': '1234', + 'lifecycleState': 'state', + 'distributionStatus': 'trez', + 'version': 'v12', + 'uniqueId': '5678'} + + sdcResource.update_informations_from_sdc_creation(details) + assert sdcResource.unique_uuid == "1234" + assert sdcResource.status == "bgt" + assert sdcResource.version == "v12" + assert sdcResource.unique_identifier == "5678" + mock_parse.assert_called_once_with("state", 'trez', mock.ANY) + +@mock.patch.object(SdcResource, "is_own_property") +@mock.patch.object(SdcResource, "declare_input_for_own_property") +@mock.patch.object(SdcResource, "declare_nested_input") +def test_declare_input(mock_nested, mock_own, mock_is_own): + sdc_resource = SdcResource() + prop = Property(name="test", property_type="test") + mock_is_own.return_value = False + with pytest.raises(ParameterError): + sdc_resource.declare_input(prop) + mock_is_own.return_value = True + sdc_resource.declare_input(prop) + mock_own.assert_called_once() + mock_nested.assert_not_called() + + mock_nested.reset_mock() + mock_own.reset_mock() + sdc_resource.declare_input(NestedInput(sdc_resource=mock.MagicMock(), input_obj=mock.MagicMock())) + mock_own.assert_not_called() + mock_nested.assert_called_once() + +@mock.patch.object(SdcResource, "send_message_json") +@mock.patch.object(SdcResource, "get_component") +@mock.patch.object(SdcResource, "resource_inputs_url", new_callable=mock.PropertyMock) +def test_declare_nested_input(mock_resource_inputs, mock_get_component, mock_send_json): + sdc_resource = SdcResource() + sdc_resource.unique_identifier = "toto" + mock_resource_inputs.return_value = "test" + sdc_resource.declare_input(NestedInput(sdc_resource=mock.MagicMock(), input_obj=mock.MagicMock())) + mock_get_component.assert_called_once() + mock_send_json.assert_called_once() + +@mock.patch.object(SdcResource, "inputs", new_callable=mock.PropertyMock) +def test_get_input(mock_inputs): + sdc_resource = SdcResource() + + mock_inputs.return_value = [ + Input(unique_id="123", + input_type="integer", + name="test", + sdc_resource=sdc_resource), + Input(unique_id="321", + input_type="string", + name="test2", + sdc_resource=sdc_resource) + ] + assert sdc_resource.get_input("test") + assert sdc_resource.get_input("test2") + with pytest.raises(ResourceNotFound): + sdc_resource.get_input("test3") + +@mock.patch.object(SdcResource, "components", new_callable=mock.PropertyMock) +def test_get_component(mock_components): + sdc_resource = SdcResource() + + mock_components.return_value = [ + Component( + created_from_csar=False, + actual_component_uid="123", + unique_id="123", + normalized_name="123", + name="123", + origin_type="123", + customization_uuid="123", + tosca_component_name="123", + component_name="123", + component_uid="123", + component_version="123", + group_instances=None, + sdc_resource=SdcResource(name="test"), + parent_sdc_resource=sdc_resource + ) + ] + assert sdc_resource.get_component(SdcResource(name="test")) + with pytest.raises(ResourceNotFound): + sdc_resource.get_component(SdcResource(name="test2")) + assert sdc_resource.get_component_by_name("test") + with pytest.raises(ResourceNotFound): + sdc_resource.get_component_by_name("invalid_name") + +def test_component_properties(): + sdc_resource = mock.MagicMock() + parent_sdc_resource = SdcResource() + + component = Component( + created_from_csar=False, + actual_component_uid="123", + unique_id="123", + normalized_name="123", + name="123", + origin_type="123", + customization_uuid="123", + tosca_component_name="123", + component_name="123", + component_uid="123", + component_version="123", + group_instances=None, + sdc_resource=sdc_resource, + parent_sdc_resource=mock.MagicMock() + ) + sdc_resource.send_message_json.return_value = {} + assert not len(list(component.properties)) + + sdc_resource.send_message_json.return_value = COMPONENT_PROPERTIES + properties = list(component.properties) + assert len(properties) == 2 + prop1, prop2 = properties + + assert prop1.unique_id == "3d9a184f-4268-4a0e-9ddd-252e49670013.vf_module_id" + assert prop1.property_type == "string" + assert prop1.name == "vf_module_id" + assert prop1.value is None + + assert prop2.unique_id == "74f79006-ae56-4d58-947e-6a5089000774.skip_post_instantiation_configuration" + assert prop2.property_type == "boolean" + assert prop2.name == "skip_post_instantiation_configuration" + assert prop2.value == "true" + +@mock.patch.object(Component, "properties", new_callable=mock.PropertyMock) +def test_component_property_set_value(mock_component_properties): + mock_sdc_resource = mock.MagicMock() + component = Component( + created_from_csar=False, + actual_component_uid="123", + unique_id="123", + normalized_name="123", + name="123", + origin_type="123", + customization_uuid="123", + tosca_component_name="123", + component_name="123", + component_uid="123", + component_version="123", + group_instances=None, + sdc_resource=mock_sdc_resource, + parent_sdc_resource=mock.MagicMock() + ) + mock_component_properties.return_value = [ + ComponentProperty( + unique_id="123", + property_type="string", + name="test_property", + component=component + ) + ] + with pytest.raises(ParameterError): + component.get_property(property_name="non_exists") + prop1 = component.get_property(property_name="test_property") + assert prop1.name == "test_property" + assert prop1.unique_id == "123" + assert prop1.property_type == "string" + assert not prop1.value + + prop1.value = "123" + mock_sdc_resource.send_message_json.assert_called_once() + +@mock.patch.object(SdcResource, "_action_to_sdc") +def test_sdc_resource_checkout(mock_action_to_sdc): + mock_action_to_sdc.return_value = None + sdc_resource = SdcResource() + sdc_resource.checkout() + mock_action_to_sdc.assert_called_once_with(const.CHECKOUT, "lifecycleState") + +@mock.patch.object(SdcResource, "_action_to_sdc") +def test_sdc_resource_undo_checkout(mock_action_to_sdc): + mock_action_to_sdc.return_value = None + sdc_resource = SdcResource() + sdc_resource.undo_checkout() + mock_action_to_sdc.assert_called_once_with(const.UNDOCHECKOUT, "lifecycleState") + +@mock.patch.object(SdcResource, "_action_to_sdc") +def test_sdc_resource_certify(mock_action_to_sdc): + mock_action_to_sdc.return_value = None + sdc_resource = SdcResource() + sdc_resource.certify() + mock_action_to_sdc.assert_called_once_with(const.CERTIFY, "lifecycleState") diff --git a/tests/test_sdc_resource_properties.py b/tests/test_sdc_resource_properties.py new file mode 100644 index 0000000..4502868 --- /dev/null +++ b/tests/test_sdc_resource_properties.py @@ -0,0 +1,1080 @@ +# Copyright 2022 Orange, 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 unittest import mock + +import pytest + +from onapsdk.exceptions import ParameterError +from onapsdk.sdc.properties import Input, Property +from onapsdk.sdc.sdc_resource import SdcResource +from onapsdk.sdc.service import Service +from onapsdk.sdc.vf import Vf +from onapsdk.sdc.vl import Vl + +INPUTS = { + 'inputs': [ + { + 'uniqueId': '9ee5fb23-4c4a-46bd-8682-68698559ee9c.skip_post_instantiation_configuration', + 'type': 'boolean', + 'required': False, + 'definition': False, + 'defaultValue': 'true', + 'description': None, + 'schema': None, + 'password': False, + 'name': 'skip_post_instantiation_configuration', + 'value': None, + 'label': None, + 'hidden': False, + 'immutable': False, + 'inputPath': None, + 'status': None, + 'inputId': None, + 'instanceUniqueId': None, + 'propertyId': None, + 'parentPropertyType': None, + 'subPropertyInputPath': None, + 'annotations': None, + 'parentUniqueId': '9ee5fb23-4c4a-46bd-8682-68698559ee9c', + 'getInputValues': None, + 'isDeclaredListInput': False, + 'getPolicyValues': None, + 'propertyConstraints': None, + 'constraints': None, + 'inputs': None, + 'properties': None, + 'schemaType': None, + 'schemaProperty': None, + 'getInputProperty': False, + 'version': None, + 'ownerId': '9ee5fb23-4c4a-46bd-8682-68698559ee9c', + 'empty': False + }, + { + 'uniqueId': '4a84415b-4580-4a78-aa33-501f0cd3d079.test', + 'type': 'string', + 'required': False, + 'definition': False, + 'defaultValue': None, + 'description': None, + 'schema': { + 'derivedFrom': None, + 'constraints': None, + 'properties': None, + 'property': { + 'uniqueId': None, + 'type': '', + 'required': False, + 'definition': False, + 'defaultValue': None, + 'description': None, + 'schema': None, + 'password': False, + 'name': None, + 'value': None, + 'label': None, + 'hidden': False, + 'immutable': False, + 'inputPath': None, + 'status': None, + 'inputId': None, + 'instanceUniqueId': None, + 'propertyId': None, + 'parentPropertyType': None, + 'subPropertyInputPath': None, + 'annotations': None, + 'parentUniqueId': None, + 'getInputValues': None, + 'isDeclaredListInput': False, + 'getPolicyValues': None, + 'propertyConstraints': None, + 'schemaType': None, + 'schemaProperty': None, + 'getInputProperty': False, + 'version': None, + 'ownerId': None, + 'empty': False + }, + 'version': None, + 'ownerId': None, + 'empty': False, + 'type': None + }, + 'password': False, + 'name': 'test', + 'value': None, + 'label': None, + 'hidden': False, + 'immutable': False, + 'inputPath': None, + 'status': None, + 'inputId': None, + 'instanceUniqueId': '4a84415b-4580-4a78-aa33-501f0cd3d079', + 'propertyId': '4a84415b-4580-4a78-aa33-501f0cd3d079.sraka', + 'parentPropertyType': 'string', + 'subPropertyInputPath': None, + 'annotations': None, + 'parentUniqueId': 'cs0008', + 'getInputValues': None, + 'isDeclaredListInput': False, + 'getPolicyValues': None, + 'propertyConstraints': None, + 'constraints': None, + 'inputs': None, + 'properties': None, + 'schemaType': '', + 'schemaProperty': { + 'uniqueId': None, + 'type': '', + 'required': False, + 'definition': False, + 'defaultValue': None, + 'description': None, + 'schema': None, + 'password': False, + 'name': None, + 'value': None, + 'label': None, + 'hidden': False, + 'immutable': False, + 'inputPath': None, + 'status': None, + 'inputId': None, + 'instanceUniqueId': None, + 'propertyId': None, + 'parentPropertyType': None, + 'subPropertyInputPath': None, + 'annotations': None, + 'parentUniqueId': None, + 'getInputValues': None, + 'isDeclaredListInput': False, + 'getPolicyValues': None, + 'propertyConstraints': None, + 'schemaType': None, + 'schemaProperty': None, + 'getInputProperty': False, + 'version': None, + 'ownerId': None, + 'empty': False + }, + 'getInputProperty': False, + 'version': None, + 'ownerId': 'cs0008', + 'empty': False + }, + { + 'uniqueId': '9ee5fb23-4c4a-46bd-8682-68698559ee9c.controller_actor', + 'type': 'string', + 'required': False, + 'definition': False, + 'defaultValue': 'SO-REF-DATA', + 'description': None, + 'schema': None, + 'password': False, + 'name': 'controller_actor', + 'value': None, + 'label': None, + 'hidden': False, + 'immutable': False, + 'inputPath': None, + 'status': None, + 'inputId': None, + 'instanceUniqueId': None, + 'propertyId': None, + 'parentPropertyType': None, + 'subPropertyInputPath': None, + 'annotations': None, + 'parentUniqueId': '9ee5fb23-4c4a-46bd-8682-68698559ee9c', + 'getInputValues': None, + 'isDeclaredListInput': False, + 'getPolicyValues': None, + 'propertyConstraints': None, + 'constraints': None, + 'inputs': None, + 'properties': None, + 'schemaType': None, + 'schemaProperty': None, + 'getInputProperty': False, + 'version': None, + 'ownerId': '9ee5fb23-4c4a-46bd-8682-68698559ee9c', + 'empty': False + }, + { + 'uniqueId': '4a84415b-4580-4a78-aa33-501f0cd3d079.lililili', + 'type': 'list', + 'required': False, + 'definition': False, + 'defaultValue': None, + 'description': None, + 'schema': { + 'derivedFrom': None, + 'constraints': None, + 'properties': None, + 'property': { + 'uniqueId': None, + 'type': 'abc', + 'required': False, + 'definition': False, + 'defaultValue': None, + 'description': None, + 'schema': None, + 'password': False, + 'name': None, + 'value': None, + 'label': None, + 'hidden': False, + 'immutable': False, + 'inputPath': None, + 'status': None, + 'inputId': None, + 'instanceUniqueId': None, + 'propertyId': None, + 'parentPropertyType': None, + 'subPropertyInputPath': None, + 'annotations': None, + 'parentUniqueId': None, + 'getInputValues': None, + 'isDeclaredListInput': False, + 'getPolicyValues': None, + 'propertyConstraints': None, + 'schemaType': None, + 'schemaProperty': None, + 'getInputProperty': False, + 'version': None, + 'ownerId': None, + 'empty': False + }, + 'version': None, + 'ownerId': None, + 'empty': False, + 'type': None + }, + 'password': False, + 'name': 'lililili', + 'value': None, + 'label': None, + 'hidden': False, + 'immutable': False, + 'inputPath': None, + 'status': None, + 'inputId': None, + 'instanceUniqueId': '4a84415b-4580-4a78-aa33-501f0cd3d079', + 'propertyId': None, + 'parentPropertyType': None, + 'subPropertyInputPath': None, + 'annotations': None, + 'parentUniqueId': None, + 'getInputValues': None, + 'isDeclaredListInput': True, + 'getPolicyValues': None, + 'propertyConstraints': None, + 'constraints': None, + 'inputs': None, + 'properties': None, + 'schemaType': 'abc', + 'schemaProperty': { + 'uniqueId': None, + 'type': 'abc', + 'required': False, + 'definition': False, + 'defaultValue': None, + 'description': None, + 'schema': None, + 'password': False, + 'name': None, + 'value': None, + 'label': None, + 'hidden': False, + 'immutable': False, + 'inputPath': None, + 'status': None, + 'inputId': None, + 'instanceUniqueId': None, + 'propertyId': None, + 'parentPropertyType': None, + 'subPropertyInputPath': None, + 'annotations': None, + 'parentUniqueId': None, + 'getInputValues': None, + 'isDeclaredListInput': False, + 'getPolicyValues': None, + 'propertyConstraints': None, + 'schemaType': None, + 'schemaProperty': None, + 'getInputProperty': False, + 'version': None, + 'ownerId': None, + 'empty': False + }, + 'getInputProperty': False, + 'version': None, + 'ownerId': None, + 'empty': False + } + ] +} + + +PROPERTIES = { + "properties": [{ + 'uniqueId': '4a84415b-4580-4a78-aa33-501f0cd3d079.llllll', + 'type': 'integer', + 'required': False, + 'definition': False, + 'defaultValue': None, + 'description': None, + 'schema': { + 'derivedFrom': None, + 'constraints': None, + 'properties': None, + 'property': { + 'uniqueId': None, + 'type': '', + 'required': False, + 'definition': False, + 'defaultValue': None, + 'description': None, + 'schema': None, + 'password': False, + 'name': None, + 'value': None, + 'label': None, + 'hidden': False, + 'immutable': False, + 'inputPath': None, + 'status': None, + 'inputId': None, + 'instanceUniqueId': None, + 'propertyId': None, + 'parentPropertyType': None, + 'subPropertyInputPath': None, + 'annotations': None, + 'parentUniqueId': None, + 'getInputValues': None, + 'isDeclaredListInput': False, + 'getPolicyValues': None, + 'propertyConstraints': None, + 'schemaType': None, + 'schemaProperty': None, + 'getInputProperty': False, + 'version': None, + 'ownerId': None, + 'empty': False + }, + 'version': None, + 'ownerId': None, + 'empty': False, + 'type': None + }, + 'password': False, + 'name': 'llllll', + 'value': '{"get_input":["lililili","INDEX","llllll"]}', + 'label': None, + 'hidden': False, + 'immutable': False, + 'inputPath': None, + 'status': None, + 'inputId': None, + 'instanceUniqueId': None, + 'propertyId': None, + 'parentPropertyType': None, + 'subPropertyInputPath': None, + 'annotations': None, + 'parentUniqueId': '4a84415b-4580-4a78-aa33-501f0cd3d079', + 'getInputValues': [ + { + 'propName': None, + 'inputName': 'lililili', + 'inputId': '4a84415b-4580-4a78-aa33-501f0cd3d079.lililili', + 'indexValue': None, + 'getInputIndex': None, + 'list': False, + 'version': None, + 'ownerId': None, + 'empty': False, + 'type': None + } + ], + 'isDeclaredListInput': False, + 'getPolicyValues': None, + 'propertyConstraints': None, + 'constraints': None, + 'schemaType': '', + 'schemaProperty': { + 'uniqueId': None, + 'type': '', + 'required': False, + 'definition': False, + 'defaultValue': None, + 'description': None, + 'schema': None, + 'password': False, + 'name': None, + 'value': None, + 'label': None, + 'hidden': False, + 'immutable': False, + 'inputPath': None, + 'status': None, + 'inputId': None, + 'instanceUniqueId': None, + 'propertyId': None, + 'parentPropertyType': None, + 'subPropertyInputPath': None, + 'annotations': None, + 'parentUniqueId': None, + 'getInputValues': None, + 'isDeclaredListInput': False, + 'getPolicyValues': None, + 'propertyConstraints': None, + 'schemaType': None, + 'schemaProperty': None, + 'getInputProperty': False, + 'version': None, + 'ownerId': None, + 'empty': False + }, + 'getInputProperty': True, + 'version': None, + 'ownerId': '4a84415b-4580-4a78-aa33-501f0cd3d079', + 'empty': False + }, + { + 'uniqueId': '4a84415b-4580-4a78-aa33-501f0cd3d079.test', + 'type': 'string', + 'required': False, + 'definition': False, + 'defaultValue': None, + 'description': None, + 'schema': { + 'derivedFrom': None, + 'constraints': None, + 'properties': None, + 'property': { + 'uniqueId': None, + 'type': '', + 'required': False, + 'definition': False, + 'defaultValue': None, + 'description': None, + 'schema': None, + 'password': False, + 'name': None, + 'value': None, + 'label': None, + 'hidden': False, + 'immutable': False, + 'inputPath': None, + 'status': None, + 'inputId': None, + 'instanceUniqueId': None, + 'propertyId': None, + 'parentPropertyType': None, + 'subPropertyInputPath': None, + 'annotations': None, + 'parentUniqueId': None, + 'getInputValues': None, + 'isDeclaredListInput': False, + 'getPolicyValues': None, + 'propertyConstraints': None, + 'schemaType': None, + 'schemaProperty': None, + 'getInputProperty': False, + 'version': None, + 'ownerId': None, + 'empty': False + }, + 'version': None, + 'ownerId': None, + 'empty': False, + 'type': None + }, + 'password': False, + 'name': 'test', + 'value': None, + 'label': None, + 'hidden': False, + 'immutable': False, + 'inputPath': None, + 'status': None, + 'inputId': None, + 'instanceUniqueId': None, + 'propertyId': None, + 'parentPropertyType': None, + 'subPropertyInputPath': None, + 'annotations': None, + 'parentUniqueId': '4a84415b-4580-4a78-aa33-501f0cd3d079', + 'getInputValues': [], + 'isDeclaredListInput': False, + 'getPolicyValues': None, + 'propertyConstraints': None, + 'constraints': None, + 'schemaType': '', + 'schemaProperty': { + 'uniqueId': None, + 'type': '', + 'required': False, + 'definition': False, + 'defaultValue': None, + 'description': None, + 'schema': None, + 'password': False, + 'name': None, + 'value': None, + 'label': None, + 'hidden': False, + 'immutable': False, + 'inputPath': None, + 'status': None, + 'inputId': None, + 'instanceUniqueId': None, + 'propertyId': None, + 'parentPropertyType': None, + 'subPropertyInputPath': None, + 'annotations': None, + 'parentUniqueId': None, + 'getInputValues': None, + 'isDeclaredListInput': False, + 'getPolicyValues': None, + 'propertyConstraints': None, + 'schemaType': None, + 'schemaProperty': None, + 'getInputProperty': False, + 'version': None, + 'ownerId': None, + 'empty': False + }, + 'getInputProperty': True, + 'version': None, + 'ownerId': '4a84415b-4580-4a78-aa33-501f0cd3d079', + 'empty': False + }, + { + 'uniqueId': '4a84415b-4580-4a78-aa33-501f0cd3d079.yyy', + 'type': 'string', + 'required': False, + 'definition': False, + 'defaultValue': None, + 'description': None, + 'schema': { + 'derivedFrom': None, + 'constraints': None, + 'properties': None, + 'property': { + 'uniqueId': None, + 'type': '', + 'required': False, + 'definition': False, + 'defaultValue': None, + 'description': None, + 'schema': None, + 'password': False, + 'name': None, + 'value': None, + 'label': None, + 'hidden': False, + 'immutable': False, + 'inputPath': None, + 'status': None, + 'inputId': None, + 'instanceUniqueId': None, + 'propertyId': None, + 'parentPropertyType': None, + 'subPropertyInputPath': None, + 'annotations': None, + 'parentUniqueId': None, + 'getInputValues': None, + 'isDeclaredListInput': False, + 'getPolicyValues': None, + 'propertyConstraints': None, + 'schemaType': None, + 'schemaProperty': None, + 'getInputProperty': False, + 'version': None, + 'ownerId': None, + 'empty': False + }, + 'version': None, + 'ownerId': None, + 'empty': False, + 'type': None + }, + 'password': False, + 'name': 'yyy', + 'value': 'lalala', + 'label': None, + 'hidden': False, + 'immutable': False, + 'inputPath': None, + 'status': None, + 'inputId': None, + 'instanceUniqueId': None, + 'propertyId': None, + 'parentPropertyType': None, + 'subPropertyInputPath': None, + 'annotations': None, + 'parentUniqueId': '4a84415b-4580-4a78-aa33-501f0cd3d079', + 'getInputValues': None, + 'isDeclaredListInput': False, + 'getPolicyValues': None, + 'propertyConstraints': None, + 'constraints': None, + 'schemaType': '', + 'schemaProperty': { + 'uniqueId': None, + 'type': '', + 'required': False, + 'definition': False, + 'defaultValue': None, + 'description': None, + 'schema': None, + 'password': False, + 'name': None, + 'value': None, + 'label': None, + 'hidden': False, + 'immutable': False, + 'inputPath': None, + 'status': None, + 'inputId': None, + 'instanceUniqueId': None, + 'propertyId': None, + 'parentPropertyType': None, + 'subPropertyInputPath': None, + 'annotations': None, + 'parentUniqueId': None, + 'getInputValues': None, + 'isDeclaredListInput': False, + 'getPolicyValues': None, + 'propertyConstraints': None, + 'schemaType': None, + 'schemaProperty': None, + 'getInputProperty': False, + 'version': None, + 'ownerId': None, + 'empty': False + }, + 'getInputProperty': False, + 'version': None, + 'ownerId': '4a84415b-4580-4a78-aa33-501f0cd3d079', + 'empty': False + }, + { + 'uniqueId': '4a84415b-4580-4a78-aa33-501f0cd3d079.test2', + 'type': 'boolean', + 'required': False, + 'definition': False, + 'defaultValue': None, + 'description': 'test2', + 'schema': { + 'derivedFrom': None, + 'constraints': None, + 'properties': None, + 'property': { + 'uniqueId': None, + 'type': '', + 'required': False, + 'definition': False, + 'defaultValue': None, + 'description': None, + 'schema': None, + 'password': False, + 'name': None, + 'value': None, + 'label': None, + 'hidden': False, + 'immutable': False, + 'inputPath': None, + 'status': None, + 'inputId': None, + 'instanceUniqueId': None, + 'propertyId': None, + 'parentPropertyType': None, + 'subPropertyInputPath': None, + 'annotations': None, + 'parentUniqueId': None, + 'getInputValues': None, + 'isDeclaredListInput': False, + 'getPolicyValues': None, + 'propertyConstraints': None, + 'schemaType': None, + 'schemaProperty': None, + 'getInputProperty': False, + 'version': None, + 'ownerId': None, + 'empty': False + }, + 'version': None, + 'ownerId': None, + 'empty': False, + 'type': None + }, + 'password': False, + 'name': 'test2', + 'value': '{"get_input":"test2"}', + 'label': None, + 'hidden': False, + 'immutable': False, + 'inputPath': None, + 'status': None, + 'inputId': None, + 'instanceUniqueId': None, + 'propertyId': None, + 'parentPropertyType': None, + 'subPropertyInputPath': None, + 'annotations': None, + 'parentUniqueId': '4a84415b-4580-4a78-aa33-501f0cd3d079', + 'getInputValues': [ + { + 'propName': None, + 'inputName': 'test2', + 'inputId': '4a84415b-4580-4a78-aa33-501f0cd3d079.test2', + 'indexValue': None, + 'getInputIndex': None, + 'list': False, + 'version': None, + 'ownerId': None, + 'empty': False, + 'type': None + } + ], + 'isDeclaredListInput': False, + 'getPolicyValues': None, + 'propertyConstraints': None, + 'constraints': None, + 'schemaType': '', + 'schemaProperty': { + 'uniqueId': None, + 'type': '', + 'required': False, + 'definition': False, + 'defaultValue': None, + 'description': None, + 'schema': None, + 'password': False, + 'name': None, + 'value': None, + 'label': None, + 'hidden': False, + 'immutable': False, + 'inputPath': None, + 'status': None, + 'inputId': None, + 'instanceUniqueId': None, + 'propertyId': None, + 'parentPropertyType': None, + 'subPropertyInputPath': None, + 'annotations': None, + 'parentUniqueId': None, + 'getInputValues': None, + 'isDeclaredListInput': False, + 'getPolicyValues': None, + 'propertyConstraints': None, + 'schemaType': None, + 'schemaProperty': None, + 'getInputProperty': False, + 'version': None, + 'ownerId': None, + 'empty': False + }, + 'getInputProperty': True, + 'version': None, + 'ownerId': '4a84415b-4580-4a78-aa33-501f0cd3d079', + 'empty': False + }] +} + + +VL_PROPERTIES = { + "properties": [{ + 'uniqueId': 'd37cd65e-9842-4490-9343-a1a874e6b52a.network_role', + 'type': 'string', + 'required': False, + 'definition': False, + 'defaultValue': None, + 'description': 'Unique label that defines the role that this network performs. example: vce oam network, vnat sr-iov1 network\n', + 'schema': None, + 'password': False, + 'name': 'network_role', + 'value': None, + 'label': None, + 'hidden': False, + 'immutable': False, + 'inputPath': None, + 'status': None, + 'inputId': None, + 'instanceUniqueId': None, + 'propertyId': None, + 'parentPropertyType': None, + 'subPropertyInputPath': None, + 'annotations': None, + 'parentUniqueId': '1af9771b-0f79-4e98-8747-30fd06da85cb', + 'getInputValues': None, + 'isDeclaredListInput': False, + 'getPolicyValues': None, + 'propertyConstraints': None, + 'constraints': None, + 'schemaType': None, + 'schemaProperty': None, + 'getInputProperty': False, + 'version': None, + 'ownerId': '1af9771b-0f79-4e98-8747-30fd06da85cb', + 'empty': False + }] +} + + +@mock.patch.object(Service, "send_message_json") +@mock.patch.object(Service, "send_message") +def test_service_properties(mock_send, mock_send_json): + + service = Service(name="test") + service.unique_identifier = "toto" + + mock_send_json.return_value = {} + assert len(list(service.properties)) == 0 + + mock_send_json.return_value = PROPERTIES + properties_list = list(service.properties) + assert len(properties_list) == 4 + prop1, prop2, prop3, prop4 = properties_list + + mock_send_json.return_value = INPUTS + + assert prop1.sdc_resource == service + assert prop1.unique_id == "4a84415b-4580-4a78-aa33-501f0cd3d079.llllll" + assert prop1.name == "llllll" + assert prop1.property_type == "integer" + assert prop1.parent_unique_id == "4a84415b-4580-4a78-aa33-501f0cd3d079" + assert prop1.value == '{"get_input":["lililili","INDEX","llllll"]}' + assert prop1.description is None + assert prop1.get_input_values + prop1_input = prop1.input + assert prop1_input.unique_id == "4a84415b-4580-4a78-aa33-501f0cd3d079.lililili" + assert prop1_input.input_type == "list" + assert prop1_input.name == "lililili" + assert prop1_input.default_value is None + + assert prop2.sdc_resource == service + assert prop2.unique_id == "4a84415b-4580-4a78-aa33-501f0cd3d079.test" + assert prop2.name == "test" + assert prop2.property_type == "string" + assert prop2.parent_unique_id == "4a84415b-4580-4a78-aa33-501f0cd3d079" + assert prop2.value is None + assert prop2.description is None + assert prop2.get_input_values == [] + assert prop2.input is None + + assert prop3.sdc_resource == service + assert prop3.unique_id == "4a84415b-4580-4a78-aa33-501f0cd3d079.yyy" + assert prop3.name == "yyy" + assert prop3.property_type == "string" + assert prop3.parent_unique_id == "4a84415b-4580-4a78-aa33-501f0cd3d079" + assert prop3.value == "lalala" + assert prop3.description is None + assert prop3.get_input_values is None + assert prop3.input is None + + assert prop4.sdc_resource == service + assert prop4.unique_id == "4a84415b-4580-4a78-aa33-501f0cd3d079.test2" + assert prop4.name == "test2" + assert prop4.property_type == "boolean" + assert prop4.parent_unique_id == "4a84415b-4580-4a78-aa33-501f0cd3d079" + assert prop4.value == '{"get_input":"test2"}' + assert prop4.description == "test2" + assert prop4.get_input_values + with pytest.raises(ParameterError): + prop4.input + + +@mock.patch.object(Service, "send_message_json") +def test_service_inputs(mock_send_json): + service = Service(name="test") + service.unique_identifier = "toto" + + mock_send_json.return_value = {} + assert len(list(service.inputs)) == 0 + + mock_send_json.return_value = INPUTS + inputs_list = list(service.inputs) + assert len(inputs_list) == 4 + + input1, input2, input3, input4 = inputs_list + assert input1.unique_id == "9ee5fb23-4c4a-46bd-8682-68698559ee9c.skip_post_instantiation_configuration" + assert input1.input_type == "boolean" + assert input1.name == "skip_post_instantiation_configuration" + assert input1.default_value == "true" + + assert input2.unique_id == "4a84415b-4580-4a78-aa33-501f0cd3d079.test" + assert input2.input_type == "string" + assert input2.name == "test" + assert input2.default_value is None + + assert input3.unique_id == "9ee5fb23-4c4a-46bd-8682-68698559ee9c.controller_actor" + assert input3.input_type == "string" + assert input3.name == "controller_actor" + assert input3.default_value == "SO-REF-DATA" + + assert input4.unique_id == "4a84415b-4580-4a78-aa33-501f0cd3d079.lililili" + assert input4.input_type == "list" + assert input4.name == "lililili" + assert input4.default_value is None + + +@mock.patch.object(Vf, "send_message_json") +def test_vf_properties(mock_send_json): + vf = Vf(name="test") + vf.unique_identifier = "toto" + + mock_send_json.return_value = {} + assert len(list(vf.properties)) == 0 + + mock_send_json.return_value = PROPERTIES + properties_list = list(vf.properties) + assert len(properties_list) == 4 + prop1, prop2, prop3, prop4 = properties_list + + mock_send_json.return_value = INPUTS + + assert prop1.sdc_resource == vf + assert prop1.unique_id == "4a84415b-4580-4a78-aa33-501f0cd3d079.llllll" + assert prop1.name == "llllll" + assert prop1.property_type == "integer" + assert prop1.parent_unique_id == "4a84415b-4580-4a78-aa33-501f0cd3d079" + assert prop1.value == '{"get_input":["lililili","INDEX","llllll"]}' + assert prop1.description is None + assert prop1.get_input_values + prop1_input = prop1.input + assert prop1_input.unique_id == "4a84415b-4580-4a78-aa33-501f0cd3d079.lililili" + assert prop1_input.input_type == "list" + assert prop1_input.name == "lililili" + assert prop1_input.default_value is None + + assert prop2.sdc_resource == vf + assert prop2.unique_id == "4a84415b-4580-4a78-aa33-501f0cd3d079.test" + assert prop2.name == "test" + assert prop2.property_type == "string" + assert prop2.parent_unique_id == "4a84415b-4580-4a78-aa33-501f0cd3d079" + assert prop2.value is None + assert prop2.description is None + assert prop2.get_input_values == [] + assert prop2.input is None + + assert prop3.sdc_resource == vf + assert prop3.unique_id == "4a84415b-4580-4a78-aa33-501f0cd3d079.yyy" + assert prop3.name == "yyy" + assert prop3.property_type == "string" + assert prop3.parent_unique_id == "4a84415b-4580-4a78-aa33-501f0cd3d079" + assert prop3.value == "lalala" + assert prop3.description is None + assert prop3.get_input_values is None + assert prop3.input is None + + assert prop4.sdc_resource == vf + assert prop4.unique_id == "4a84415b-4580-4a78-aa33-501f0cd3d079.test2" + assert prop4.name == "test2" + assert prop4.property_type == "boolean" + assert prop4.parent_unique_id == "4a84415b-4580-4a78-aa33-501f0cd3d079" + assert prop4.value == '{"get_input":"test2"}' + assert prop4.description == "test2" + assert prop4.get_input_values + with pytest.raises(ParameterError): + prop4.input + + +@mock.patch.object(Vl, "send_message_json") +@mock.patch.object(Vl, "exists") +def test_vl_properties(mock_exists, mock_send_json): + mock_exists.return_value = True + vl = Vl(name="test") + vl.unique_identifier = "toto" + + mock_send_json.return_value = {} + assert len(list(vl.properties)) == 0 + + mock_send_json.return_value = VL_PROPERTIES + properties_list = list(vl.properties) + assert len(properties_list) == 1 + prop = properties_list[0] + + assert prop.sdc_resource == vl + assert prop.unique_id == "d37cd65e-9842-4490-9343-a1a874e6b52a.network_role" + assert prop.name == "network_role" + assert prop.property_type == "string" + assert prop.parent_unique_id == "1af9771b-0f79-4e98-8747-30fd06da85cb" + assert prop.value is None + assert prop.description == "Unique label that defines the role that this network performs. example: vce oam network, vnat sr-iov1 network\n" + assert prop.get_input_values is None + assert prop.input is None + + +@mock.patch.object(SdcResource, "send_message_json") +def test_sdc_resource_is_own_property(mock_send_json): + sdc_resource = SdcResource(name="test") + sdc_resource.unique_identifier = "toto" + mock_send_json.return_value = PROPERTIES + prop1 = Property( + name="llllll", + property_type="integer" + ) + prop2 = Property( + name="test2", + property_type="string" + ) + assert sdc_resource.is_own_property(prop1) + assert not sdc_resource.is_own_property(prop2) + +@mock.patch.object(SdcResource, "properties", new_callable=mock.PropertyMock) +@mock.patch.object(SdcResource, "send_message_json") +def test_sdc_resource_set_property_value(mock_send_message_json, mock_sdc_resource_properties): + sdc_resource = SdcResource(name="test") + sdc_resource.unique_identifier = "toto" + + mock_sdc_resource_properties.return_value = [ + Property(name="test", + property_type="string", + sdc_resource=sdc_resource) + ] + with pytest.raises(ParameterError): + sdc_resource.set_property_value(Property(name="test2", + property_type="integer", + sdc_resource=sdc_resource), + value="lalala") + prop = sdc_resource.get_property(property_name="test") + assert prop.name == "test" + assert prop.property_type == "string" + assert not prop.value + + prop.value = "test" + mock_send_message_json.assert_called_once() + assert prop.value == "test" + +@mock.patch.object(SdcResource, "inputs", new_callable=mock.PropertyMock) +@mock.patch.object(SdcResource, "send_message_json") +def test_sdc_resource_input_default_value(mock_send_message_json, mock_inputs): + sdc_resource = SdcResource(name="test") + sdc_resource.unique_identifier = "toto" + + mock_inputs.return_value = [ + Input(unique_id="123", + input_type="integer", + name="test", + sdc_resource=sdc_resource) + ] + assert sdc_resource.get_input("test") + input_obj = sdc_resource.get_input("test") + assert not input_obj.default_value + input_obj.default_value = "123" + mock_send_message_json.assert_called_once() + assert input_obj.default_value == "123" diff --git a/tests/test_sdc_vfc.py b/tests/test_sdc_vfc.py new file mode 100644 index 0000000..7322e0a --- /dev/null +++ b/tests/test_sdc_vfc.py @@ -0,0 +1,82 @@ +# Copyright 2022 Orange, 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 unittest import mock + +import pytest + +import onapsdk.constants as const +from onapsdk.exceptions import ResourceNotFound +from onapsdk.sdc.vfc import Vfc + + +VFCS = [ + { + "uuid":"18167a36-5f7d-4e10-809f-b73ce7268b00", + "invariantUUID":"a5852d77-c364-4eec-97c6-8630b8f138ac", + "name":"Allotted Resource", + "version":"1.0", + "toscaModelURL":"/sdc/v1/catalog/resources/18167a36-5f7d-4e10-809f-b73ce7268b00/toscaModel", + "category":"Allotted Resource", + "subCategory":"Allotted Resource", + "resourceType":"VFC", + "lifecycleState":"CERTIFIED", + "lastUpdaterUserId":"jh0003" + }, + { + "uuid":"3b9f3a0d-f9d1-4d95-80ce-7f7812a2b7b5", + "invariantUUID":"c4aa9ad7-1c68-4fde-884e-b9d693b5f725", + "name":"Controller", + "version":"1.0", + "toscaModelURL":"/sdc/v1/catalog/resources/3b9f3a0d-f9d1-4d95-80ce-7f7812a2b7b5/toscaModel", + "category":"Generic", + "subCategory":"Infrastructure", + "resourceType":"VFC", + "lifecycleState":"CERTIFIED", + "lastUpdaterUserId":"jh0003" + } +] + + +@mock.patch.object(Vfc, 'send_message_json') +def test_get_all_no_vfc(mock_send): + """Returns empty array if no vfcs.""" + mock_send.return_value = {} + assert Vfc.get_all() == [] + mock_send.assert_called_once_with("GET", 'get Vfcs', 'https://sdc.api.be.simpledemo.onap.org:30204/sdc/v1/catalog/resources?resourceType=VFC') + + +@mock.patch.object(Vfc, 'send_message_json') +def test_get_all_vfc(mock_send): + mock_send.return_value = VFCS + vfcs = Vfc.get_all() + assert len(vfcs) == 2 + vfc = vfcs[0] + assert vfc.name == "Allotted Resource" + assert vfc.identifier == "18167a36-5f7d-4e10-809f-b73ce7268b00" + assert vfc.unique_uuid == "a5852d77-c364-4eec-97c6-8630b8f138ac" + assert vfc.version == "1.0" + assert vfc.status == const.CERTIFIED + vfc = vfcs[1] + assert vfc.name == "Controller" + assert vfc.identifier == "3b9f3a0d-f9d1-4d95-80ce-7f7812a2b7b5" + assert vfc.unique_uuid == "c4aa9ad7-1c68-4fde-884e-b9d693b5f725" + assert vfc.version == "1.0" + assert vfc.status == const.CERTIFIED + + +@mock.patch.object(Vfc, 'send_message_json') +def test_create_vfc_not_exists(mock_send): + mock_send.return_value = VFCS + with pytest.raises(ResourceNotFound): + Vfc("not_exists") diff --git a/tests/test_sdc_vl.py b/tests/test_sdc_vl.py new file mode 100644 index 0000000..8eb2a20 --- /dev/null +++ b/tests/test_sdc_vl.py @@ -0,0 +1,82 @@ +# Copyright 2022 Orange, 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 unittest import mock + +import pytest + +import onapsdk.constants as const +from onapsdk.exceptions import ResourceNotFound +from onapsdk.sdc.vl import Vl + + +VLS = [ + { + "uuid":"e12cedf4-fd3f-4d76-ae2a-0368eaee40dc", + "invariantUUID":"4084c513-5149-456d-9be0-efc503058799", + "name":"NeutronNet", + "version":"1.0", + "toscaModelURL":"/sdc/v1/catalog/resources/e12cedf4-fd3f-4d76-ae2a-0368eaee40dc/toscaModel", + "category":"Generic", + "subCategory":"Network Elements", + "resourceType":"VL", + "lifecycleState":"CERTIFIED", + "lastUpdaterUserId":"jh0003" + }, + { + "uuid":"3b9f3a0d-f9d1-4d95-80ce-7f7812a2b7b5", + "invariantUUID":"c4aa9ad7-1c68-4fde-884e-b9d693b5f725", + "name":"Network", + "version":"1.0", + "toscaModelURL":"/sdc/v1/catalog/resources/3b9f3a0d-f9d1-4d95-80ce-7f7812a2b7b5/toscaModel", + "category":"Generic", + "subCategory":"Infrastructure", + "resourceType":"VL", + "lifecycleState":"CERTIFIED", + "lastUpdaterUserId":"jh0003" + } +] + + +@mock.patch.object(Vl, 'send_message_json') +def test_get_all_no_vl(mock_send): + """Returns empty array if no vls.""" + mock_send.return_value = {} + assert Vl.get_all() == [] + mock_send.assert_called_once_with("GET", 'get Vls', 'https://sdc.api.be.simpledemo.onap.org:30204/sdc/v1/catalog/resources?resourceType=VL') + + +@mock.patch.object(Vl, 'send_message_json') +def test_get_all_vl(mock_send): + mock_send.return_value = VLS + vls = Vl.get_all() + assert len(vls) == 2 + vl = vls[0] + assert vl.name == "NeutronNet" + assert vl.identifier == "e12cedf4-fd3f-4d76-ae2a-0368eaee40dc" + assert vl.unique_uuid == "4084c513-5149-456d-9be0-efc503058799" + assert vl.version == "1.0" + assert vl.status == const.CERTIFIED + vl = vls[1] + assert vl.name == "Network" + assert vl.identifier == "3b9f3a0d-f9d1-4d95-80ce-7f7812a2b7b5" + assert vl.unique_uuid == "c4aa9ad7-1c68-4fde-884e-b9d693b5f725" + assert vl.version == "1.0" + assert vl.status == const.CERTIFIED + + +@mock.patch.object(Vl, 'send_message_json') +def test_create_vl_not_exists(mock_send): + mock_send.return_value = VLS + with pytest.raises(ResourceNotFound): + Vl("not_exists") diff --git a/tests/test_sdnc_element.py b/tests/test_sdnc_element.py new file mode 100644 index 0000000..6c8fe2d --- /dev/null +++ b/tests/test_sdnc_element.py @@ -0,0 +1,26 @@ +"""Test A&AI Element.""" +# Copyright 2022 Orange, 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 unittest import mock + +from onapsdk.sdnc.sdnc_element import SdncElement +from onapsdk.utils.gui import GuiList + +@mock.patch.object(SdncElement, "send_message") +def test_get_guis(send_message_mock): + component = SdncElement() + gui_results = component.get_guis() + assert type(gui_results) == GuiList + assert len(gui_results.guilist) == 2 + # assert gui_results.guilist[0].status == send_message_mock.return_value.status_code diff --git a/tests/test_service.py b/tests/test_service.py new file mode 100755 index 0000000..e486833 --- /dev/null +++ b/tests/test_service.py @@ -0,0 +1,1498 @@ +"""Test Service module.""" +# Copyright 2022 Orange, 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 os import path +from pathlib import Path +from unittest import mock +from unittest.mock import MagicMock, PropertyMock +import shutil + +import oyaml as yaml +import pytest + +import onapsdk.constants as const +from onapsdk.exceptions import ParameterError, RequestError, ResourceNotFound, StatusError, ValidationError +from onapsdk.sdc.category_management import ServiceCategory +from onapsdk.sdc.component import Component +from onapsdk.sdc.properties import ComponentProperty, Property +from onapsdk.sdc.service import Service, ServiceInstantiationType +from onapsdk.sdc.sdc_resource import SdcResource +from onapsdk.utils.headers_creator import headers_sdc_operator +from onapsdk.utils.headers_creator import headers_sdc_creator + + +ARTIFACTS = { + "componentInstances" : [ + { + "uniqueId" : "test_unique_id", + "componentName" : "ubuntu16test_VF 0" + } + ] +} + + +COMPONENTS = { + "componentInstances":[ + { + "actualComponentUid":"374f0a98-a280-43f1-9e6c-00b436782ce7", + "createdFromCsar":True, + "uniqueId":"bcfa7544-6e3d-4666-93b1-c5973356d069.374f0a98-a280-43f1-9e6c-00b436782ce7.abstract_vsn", + "normalizedName":"abstract_vsn", + "name":"abstract_vsn", + "originType":"CVFC", + "customizationUUID":"971043e1-495b-4b75-901e-3d09baed7521", + "componentUid":"374f0a98-a280-43f1-9e6c-00b436782ce7", + "componentVersion":"1.0", + "toscaComponentName":"org.openecomp.resource.vfc.11111cvfc.abstract.abstract.nodes.vsn", + "componentName":"11111-nodes.vsnCvfc", + "groupInstances":None + } + ] +} + + +COMPONENT = { + "metadata":{ + "uniqueId":"374f0a98-a280-43f1-9e6c-00b436782ce7", + "name":"11111-nodes.vsnCvfc", + "version":"1.0", + "isHighestVersion":True, + "creationDate":1594898496259, + "lastUpdateDate":1594898496325, + "description":"Complex node type that is used as nested type in VF", + "lifecycleState":"CERTIFIED", + "tags":[ + "11111-nodes.vsnCvfc" + ], + "icon":"defaulticon", + "normalizedName":"11111nodesvsncvfc", + "systemName":"11111NodesVsncvfc", + "contactId":"cs0008", + "allVersions":{ + "1.0":"374f0a98-a280-43f1-9e6c-00b436782ce7" + }, + "isDeleted":None, + "projectCode":None, + "csarUUID":None, + "csarVersion":None, + "importedToscaChecksum":None, + "invariantUUID":"3c027ba1-8d3a-4b59-9394-d748fec5e42c", + "componentType":"RESOURCE", + "name":"Generic", + "normalizedName":"generic", + "uniqueId":"resourceNewCategory.generic", + "icons":None, + "creatorUserId":"cs0008", + "creatorFullName":"Carlos Santana", + "lastUpdaterUserId":"cs0008", + "lastUpdaterFullName":"Carlos Santana", + "archiveTime":0, + "vendorName":"mj", + "vendorRelease":"1.0", + "resourceVendorModelNumber":"", + "resourceType":"CVFC", + "isAbstract":None, + "cost":None, + "licenseType":None, + "toscaResourceName":"org.openecomp.resource.vfc.11111cvfc.abstract.abstract.nodes.vsn", + "derivedFrom":None, + "uuid":"59f05bfb-ccea-4857-8799-6acff59e6344", + "archived":False, + "vspArchived":False, + "groupInstances":None + } +} + + +COMPONENT_PROPERTIES = [ + { + "uniqueId":"3d9a184f-4268-4a0e-9ddd-252e49670013.vf_module_id", + "type":"string", + "required":False, + "definition":False, + "description":"The vFirewall Module ID is provided by ECOMP", + "password":False, + "name":"vf_module_id", + "label":"vFirewall module ID", + "hidden":False, + "immutable":False, + "isDeclaredListInput":False, + "getInputProperty":False, + "empty":False + },{ + "uniqueId":"74f79006-ae56-4d58-947e-6a5089000774.skip_post_instantiation_configuration", + "type":"boolean", + "required":False, + "definition":False, + "password":False, + "name":"skip_post_instantiation_configuration", + "value":"true", + "hidden":False, + "immutable":False, + "parentUniqueId":"74f79006-ae56-4d58-947e-6a5089000774", + "isDeclaredListInput":False, + "getInputProperty":False, + "ownerId":"74f79006-ae56-4d58-947e-6a5089000774", + "empty":False + } +] + + +COMPONENTS_WITH_ALL_ORIGIN_TYPES = { + "componentInstances":[ + { + "actualComponentUid":"374f0a98-a280-43f1-9e6c-00b436782ce7", + "createdFromCsar":True, + "uniqueId":"bcfa7544-6e3d-4666-93b1-c5973356d069.374f0a98-a280-43f1-9e6c-00b436782ce7.abstract_vsn", + "normalizedName":"abstract_vsn", + "name":"abstract_vsn", + "originType":"VF", + "customizationUUID":"971043e1-495b-4b75-901e-3d09baed7521", + "componentUid":"374f0a98-a280-43f1-9e6c-00b436782ce7", + "componentVersion":"1.0", + "toscaComponentName":"org.openecomp.resource.vfc.11111cvfc.abstract.abstract.nodes.vsn", + "componentName":"11111-nodes.vsnCvfc", + "groupInstances":None + }, + { + "actualComponentUid":"374f0a98-a280-43f1-9e6c-00b436782ce7", + "createdFromCsar":True, + "uniqueId":"bcfa7544-6e3d-4666-93b1-c5973356d069.374f0a98-a280-43f1-9e6c-00b436782ce7.abstract_vsn", + "normalizedName":"abstract_vsn", + "name":"abstract_vsn", + "originType":"PNF", + "customizationUUID":"971043e1-495b-4b75-901e-3d09baed7521", + "componentUid":"374f0a98-a280-43f1-9e6c-00b436782ce7", + "componentVersion":"1.0", + "toscaComponentName":"org.openecomp.resource.vfc.11111cvfc.abstract.abstract.nodes.vsn", + "componentName":"11111-nodes.vsnCvfc", + "groupInstances":None + }, + { + "actualComponentUid":"374f0a98-a280-43f1-9e6c-00b436782ce7", + "createdFromCsar":True, + "uniqueId":"bcfa7544-6e3d-4666-93b1-c5973356d069.374f0a98-a280-43f1-9e6c-00b436782ce7.abstract_vsn", + "normalizedName":"abstract_vsn", + "name":"abstract_vsn", + "originType":"VL", + "customizationUUID":"971043e1-495b-4b75-901e-3d09baed7521", + "componentUid":"374f0a98-a280-43f1-9e6c-00b436782ce7", + "componentVersion":"1.0", + "toscaComponentName":"org.openecomp.resource.vfc.11111cvfc.abstract.abstract.nodes.vsn", + "componentName":"11111-nodes.vsnCvfc", + "groupInstances":None + } + ] +} + + +def test_init_no_name(): + """Check init with no names.""" + svc = Service() + assert isinstance(svc, SdcResource) + assert svc._identifier is None + assert svc._version is None + assert svc.name == "ONAP-test-Service" + assert svc.headers["USER_ID"] == "cs0008" + assert svc.distribution_status is None + assert svc._distribution_id is None + assert isinstance(svc._base_url(), str) + +@mock.patch.object(Service, 'exists') +def test_init_with_name(mock_exists): + """Check init with no names.""" + mock_exists.return_value = False + svc = Service(name="YOLO") + assert svc._identifier == None + assert svc._version == None + assert svc.name == "YOLO" + assert svc.created() == False + assert svc.headers["USER_ID"] == "cs0008" + assert svc.distribution_status is None + assert svc._distribution_id is None + assert isinstance(svc._base_url(), str) + +@mock.patch.object(Service, 'exists') +def test_init_with_sdc_values(mock_exists): + """Check init with no names.""" + sdc_values = {'uuid': '12', 'version': '14', 'invariantUUID': '56', + 'distributionStatus': 'yes', 'lifecycleState': 'state', + 'category': 'Network Service'} + svc = Service(sdc_values=sdc_values) + mock_exists.return_value = True + assert svc._identifier == "12" + assert svc._version == "14" + assert svc.name == "ONAP-test-Service" + assert svc.created() + assert svc.headers["USER_ID"] == "cs0008" + assert svc.distribution_status == "yes" + assert svc._distribution_id is None + assert svc.category_name == "Network Service" + assert isinstance(svc._base_url(), str) + +@mock.patch.object(Service, 'get_all') +def test_version_filter(mock_get_all): + """Check version filter""" + svc_1 = Service(name="test_version_filter") + svc_1.identifier = "1111" + svc_1.unique_uuid = "2222" + svc_1.unique_identifier = "3333" + svc_1.status = const.CERTIFIED + svc_1.version = "1.0" + + svc_2 = Service(name="test_version_filter") + svc_2.identifier = "1111" + svc_2.unique_uuid = "2222" + svc_2.unique_identifier = "3333" + svc_2.status = const.DRAFT + svc_2.version = "1.1" + + mock_get_all.return_value = [svc_1, svc_2] + + svc = Service(name='test_version_filter') + assert svc.exists() + assert svc.version == "1.1" + + svc = Service(name='test_version_filter', version='1.0') + assert svc.exists() + assert svc.version == "1.0" + + svc = Service(name='test_version_filter', version='-111') + assert not svc.exists() + assert not svc.version + +@mock.patch.object(Service, 'get_all') +def test_get_the_latest_version(mock_get_all): + svc_1 = Service(name="test_get_max_version") + svc_1.identifier = "1111" + svc_1.unique_uuid = "2222" + svc_1.unique_identifier = "3333" + svc_1.status = const.CERTIFIED + svc_1.version = "9.0" + + svc_2 = Service(name="test_get_max_version") + svc_2.identifier = "1111" + svc_2.unique_uuid = "2222" + svc_2.unique_identifier = "3333" + svc_2.status = const.DRAFT + svc_2.version = "10.0" + + mock_get_all.return_value = [svc_1, svc_2] + svc = Service(name='test_get_max_version') + assert svc.version == "10.0" + + svc_3 = Service(name="test_get_max_version") + svc_3.identifier = "1111" + svc_3.unique_uuid = "2222" + svc_3.unique_identifier = "3333" + svc_3.status = const.DRAFT + svc_3.version = "10.1" + mock_get_all.return_value = [svc_1, svc_2, svc_3] + svc = Service(name='test_get_max_version') + assert svc.version == "10.1" + + svc_4 = Service(name="test_get_max_version") + svc_4.identifier = "1111" + svc_4.unique_uuid = "2222" + svc_4.unique_identifier = "3333" + svc_4.status = const.DRAFT + svc_4.version = "20.0" + mock_get_all.return_value = [svc_1, svc_2, svc_3, svc_4] + svc = Service(name='test_get_max_version') + assert svc.version == "20.0" + + svc_5 = Service(name="test_get_max_version") + svc_5.identifier = "1111" + svc_5.unique_uuid = "2222" + svc_5.unique_identifier = "3333" + svc_5.status = const.DRAFT + svc_5.version = "99.0" + + svc_6 = Service(name="test_get_max_version") + svc_6.identifier = "1111" + svc_6.unique_uuid = "2222" + svc_6.unique_identifier = "3333" + svc_6.status = const.DRAFT + svc_6.version = "100.0" + mock_get_all.return_value = [svc_1, svc_2, svc_3, svc_4, svc_5, svc_6] + svc = Service(name='test_get_max_version') + assert svc.version == "100.0" + + +def test_equality_really_equals(): + """Check two vfs are equals if name is the same.""" + svc_1 = Service(name="equal") + svc_1.identifier = "1234" + svc_2 = Service(name="equal") + svc_2.identifier = "1235" + assert svc_1 == svc_2 + + +def test_equality_not_equals(): + """Check two vfs are not equals if name is not the same.""" + svc_1 = Service(name="equal") + svc_1.identifier = "1234" + svc_2 = Service(name="not_equal") + svc_2.identifier = "1234" + assert svc_1 != svc_2 + + +def test_equality_not_equals_not_same_object(): + """Check a vf and something different are not equals.""" + svc_1 = Service(name="equal") + svc_1.identifier = "1234" + svc_2 = SdcResource() + svc_2.name = "equal" + assert svc_1 != svc_2 + +@mock.patch.object(Service, 'load_metadata') +def test_distribution_id_no_load(mock_load): + svc = Service() + svc.identifier = "1234" + svc._distribution_id = "4567" + assert svc.distribution_id == "4567" + mock_load.assert_not_called() + +@mock.patch.object(Service, 'load_metadata') +def test_distribution_id_load(mock_load): + svc = Service() + svc.identifier = "1234" + assert svc.distribution_id is None + mock_load.assert_called_once() + +@mock.patch.object(Service, '_check_distributed') +def test_distributed_no_load(mock_check_distributed): + svc = Service() + svc.identifier = "1234" + svc._distributed = True + assert svc.distributed + mock_check_distributed.assert_not_called() + +@mock.patch.object(Service, '_check_distributed') +def test_distributed_load(mock_check_distributed): + svc = Service() + svc.identifier = "1234" + assert not svc.distributed + mock_check_distributed.assert_called_once() + +def test_distribution_id_setter(): + svc = Service() + svc.identifier = "1234" + svc.distribution_id = "4567" + assert svc._distribution_id == "4567" + +@mock.patch.object(Service, '_create') +@mock.patch.object(Service, "category", new_callable=mock.PropertyMock) +@mock.patch.object(Service, "exists") +def test_create(mock_exists, mock_category, mock_create): + mock_exists.return_value = False + + svc = Service() + svc.create() + mock_create.assert_called_once_with("service_create.json.j2", + name="ONAP-test-Service", + instantiation_type="A-la-carte", + category=svc.category, + role="", + function="", + service_type="" + ) + mock_create.reset_mock() + svc = Service(instantiation_type=ServiceInstantiationType.MACRO) + svc.create() + mock_create.assert_called_once_with("service_create.json.j2", + name="ONAP-test-Service", + instantiation_type="Macro", + category=svc.category, + role="", + function="", + service_type="" + ) + +@mock.patch.object(Service, 'exists') +@mock.patch.object(Service, 'send_message') +def test_add_resource_not_draft(mock_send, mock_exists): + mock_exists.return_value = False + svc = Service() + resource = SdcResource() + with pytest.raises(StatusError): + svc.add_resource(resource) + mock_send.assert_not_called() + +@mock.patch.object(Service, 'load') +@mock.patch.object(Service, 'send_message') +def test_add_resource_bad_result(mock_send, mock_load): + svc = Service() + svc.unique_identifier = "45" + svc.identifier = "93" + svc.status = const.DRAFT + mock_send.return_value = {} + resource = SdcResource() + resource.unique_identifier = "12" + resource.created = MagicMock(return_value=True) + resource.version = "40" + resource.name = "test" + assert svc.add_resource(resource) is None + mock_send.assert_called_once_with( + 'POST', 'Add SDCRESOURCE to ServiceProxy', + 'https://sdc.api.fe.simpledemo.onap.org:30207/sdc1/feProxy/rest/v1/catalog/services/45/resourceInstance', + data='{\n "name": "test",\n "componentVersion": "40",\n "posY": 100,\n "posX": 200,\n "uniqueId": "12",\n "originType": "SDCRESOURCE",\n "componentUid": "12",\n "icon": "defaulticon"\n}') + +@mock.patch.object(Service, 'load') +@mock.patch.object(Service, 'send_message') +def test_add_resource_OK(mock_send, mock_load): + svc = Service() + svc.unique_identifier = "45" + svc.identifier = "93" + svc.status = const.DRAFT + mock_send.return_value = {'yes': 'indeed'} + resource = SdcResource() + resource.unique_identifier = "12" + resource.created = MagicMock(return_value=True) + resource.version = "40" + resource.name = "test" + result = svc.add_resource(resource) + assert result['yes'] == "indeed" + mock_send.assert_called_once_with( + 'POST', 'Add SDCRESOURCE to ServiceProxy', + 'https://sdc.api.fe.simpledemo.onap.org:30207/sdc1/feProxy/rest/v1/catalog/services/45/resourceInstance', + data='{\n "name": "test",\n "componentVersion": "40",\n "posY": 100,\n "posX": 200,\n "uniqueId": "12",\n "originType": "SDCRESOURCE",\n "componentUid": "12",\n "icon": "defaulticon"\n}') + +@mock.patch.object(Service, '_verify_action_to_sdc') +def test_checkin(mock_verify): + svc = Service() + svc.checkin() + mock_verify.assert_called_once_with(const.DRAFT, const.CHECKIN, 'lifecycleState') + +@mock.patch.object(Service, '_verify_action_to_sdc') +def test_submit(mock_verify): + svc = Service() + svc.submit() + mock_verify.assert_called_once_with(const.CHECKED_IN, const.SUBMIT_FOR_TESTING, 'lifecycleState') + +@mock.patch.object(Service, '_verify_action_to_sdc') +def test_certify(mock_verify): + svc = Service() + svc.certify() + mock_verify.assert_called_once_with( + const.CHECKED_IN, const.CERTIFY, 'lifecycleState', + headers=headers_sdc_creator(svc.headers)) + +@mock.patch.object(Service, '_verify_action_to_sdc') +def test_distribute(mock_verify): + svc = Service() + svc.distribute() + mock_verify.assert_called_once_with( + const.CERTIFIED, const.DISTRIBUTE, 'distribution', + headers=headers_sdc_creator(svc.headers)) + +@mock.patch.object(Service, '_verify_action_to_sdc') +def test_redistribute(mock_verify): + svc = Service() + svc.redistribute() + mock_verify.assert_called_once_with( + const.DISTRIBUTED, const.DISTRIBUTE, 'distribution', + headers=headers_sdc_creator(svc.headers)) + +@mock.patch.object(Service, 'send_message') +def test_get_tosca_no_result(mock_send): + if path.exists('/tmp/tosca_files'): + shutil.rmtree('/tmp/tosca_files') + mock_send.return_value = {} + svc = Service() + svc.identifier = "12" + svc.get_tosca() + headers = headers_sdc_creator(svc.headers) + headers['Accept'] = 'application/octet-stream' + mock_send.assert_called_once_with( + 'GET', 'Download Tosca Model for ONAP-test-Service', + 'https://sdc.api.be.simpledemo.onap.org:30204/sdc/v1/catalog/services/12/toscaModel', + headers=headers) + assert not path.exists('/tmp/tosca_files') + + +def test_get_tosca_bad_csart(requests_mock): + if path.exists('/tmp/tosca_files'): + shutil.rmtree('/tmp/tosca_files') + svc = Service() + svc.identifier = "12" + with open('tests/data/bad.csar', mode='rb') as file: + file_content = file.read() + requests_mock.get( + 'https://sdc.api.be.simpledemo.onap.org:30204/sdc/v1/catalog/services/12/toscaModel', + content=file_content) + svc.get_tosca() + + +def test_get_tosca_result(requests_mock): + if path.exists('/tmp/tosca_files'): + shutil.rmtree('/tmp/tosca_files') + with open('tests/data/test.csar', mode='rb') as file: + file_content = file.read() + requests_mock.get( + 'https://sdc.api.be.simpledemo.onap.org:30204/sdc/v1/catalog/services/12/toscaModel', + content=file_content) + svc = Service() + svc.identifier = "12" + svc.get_tosca() + +def test_get_tosca_result_no_service_in_csar(requests_mock): + if path.exists('/tmp/tosca_files'): + shutil.rmtree('/tmp/tosca_files') + with open('tests/data/bad_no_service.csar', mode='rb') as file: + file_content = file.read() + requests_mock.get( + 'https://sdc.api.be.simpledemo.onap.org:30204/sdc/v1/catalog/services/12/toscaModel', + content=file_content) + svc = Service() + svc.identifier = "12" + with pytest.raises(ValidationError): + svc.get_tosca() + +@mock.patch.object(Service, 'send_message_json') +def test_distributed_api_error(mock_send): + mock_send.side_effect = ResourceNotFound + svc = Service() + svc.distribution_id = "12" + assert not svc.distributed + +@mock.patch.object(Service, 'send_message_json') +def test_distributed_not_distributed(mock_send): + mock_send.return_value = { + 'distributionStatusList':[ + {'omfComponentID': "SO", 'status': "DOWNLOAD_OK"}, + {'omfComponentID': "sdnc", 'status': "DOWNLOAD_NOK"}, + {'omfComponentID': "aai", 'status': "DOWNLOAD_OK"}]} + svc = Service() + svc.distribution_id = "12" + assert not svc.distributed + mock_send.assert_called_once_with( + 'GET', 'Check distribution for ONAP-test-Service', + 'https://sdc.api.fe.simpledemo.onap.org:30207/sdc1/feProxy/rest/v1/catalog/services/distribution/12', + headers=headers_sdc_operator(svc.headers)) + +@mock.patch.object(Service, 'send_message_json') +def test_distributed_not_distributed(mock_send): + mock_send.return_value = { + 'distributionStatusList':[ + {'omfComponentID': "SO", 'status': "DOWNLOAD_OK"}, + {'omfComponentID': "aai", 'status': "DOWNLOAD_OK"}]} + svc = Service() + svc.distribution_id = "12" + assert not svc.distributed + mock_send.assert_called_once_with( + 'GET', 'Check distribution for ONAP-test-Service', + 'https://sdc.api.fe.simpledemo.onap.org:30207/sdc1/feProxy/rest/v1/catalog/services/distribution/12', + headers=headers_sdc_creator(svc.headers)) + +@mock.patch.object(Service, 'send_message_json') +def test_distributed_distributed(mock_send): + mock_send.return_value = { + 'distributionStatusList':[ + {'omfComponentID': "SO", 'status': "DOWNLOAD_OK"}, + {'omfComponentID': "sdnc", 'status': "DOWNLOAD_OK"}, + {'omfComponentID': "aai", 'status': "DOWNLOAD_OK"}]} + svc = Service() + svc.distribution_id = "12" + assert svc.distributed + mock_send.assert_called_once_with( + 'GET', 'Check distribution for ONAP-test-Service', + 'https://sdc.api.fe.simpledemo.onap.org:30207/sdc1/feProxy/rest/v1/catalog/services/distribution/12', + headers=headers_sdc_creator(svc.headers)) + +@mock.patch.object(Service, 'send_message_json') +def test_load_metadata_no_result(mock_send): + mock_send.return_value = {} + svc = Service() + svc.identifier = "1" + svc.load_metadata() + assert svc._distribution_id is None + mock_send.assert_called_once_with( + 'GET', 'Get Metadata for ONAP-test-Service', + 'https://sdc.api.fe.simpledemo.onap.org:30207/sdc1/feProxy/rest/v1/catalog/services/1/distribution', + headers=headers_sdc_creator(svc.headers)) + +@mock.patch.object(Service, 'send_message_json') +def test_load_metadata_bad_json(mock_send): + mock_send.return_value = {'yolo': 'in the wood'} + svc = Service() + svc.identifier = "1" + svc.load_metadata() + assert svc._distribution_id is None + mock_send.assert_called_once_with( + 'GET', 'Get Metadata for ONAP-test-Service', + 'https://sdc.api.fe.simpledemo.onap.org:30207/sdc1/feProxy/rest/v1/catalog/services/1/distribution', + headers=headers_sdc_creator(svc.headers)) + +@mock.patch.object(Service, 'send_message_json') +def test_load_metadata_OK(mock_send): + mock_send.return_value = {'distributionStatusOfServiceList': [ + {'distributionID': "11"}, {'distributionID': "12"}]} + svc = Service() + svc.identifier = "1" + svc.load_metadata() + assert svc._distribution_id == "11" + mock_send.assert_called_once_with( + 'GET', 'Get Metadata for ONAP-test-Service', + 'https://sdc.api.fe.simpledemo.onap.org:30207/sdc1/feProxy/rest/v1/catalog/services/1/distribution', + headers=headers_sdc_creator(svc.headers)) + +def test_get_all_url(): + assert Service._get_all_url() == "https://sdc.api.be.simpledemo.onap.org:30204/sdc/v1/catalog/services" + +@mock.patch.object(Service, '_action_to_sdc') +@mock.patch.object(Service, 'load') +def test_really_submit_request_failed(mock_load, mock_action): + mock_action.side_effect = RequestError + svc = Service() + with pytest.raises(RequestError) as err: + svc._really_submit() + assert err.type == RequestError + mock_load.assert_not_called() + mock_action.assert_called_once_with('Certify', action_type='lifecycleState') + +@mock.patch.object(Service, '_action_to_sdc') +@mock.patch.object(Service, 'load') +def test_really_submit_OK(mock_load, mock_action): + mock_action.return_value = "yes" + svc = Service() + svc._really_submit() + mock_load.assert_called_once() + mock_action.assert_called_once_with('Certify', action_type='lifecycleState') + +@mock.patch.object(Service, 'load') +@mock.patch.object(Service, '_action_to_sdc') +@mock.patch.object(Service, 'created') +def test_verify_action_to_sdc_not_created(mock_created, mock_action, mock_load): + mock_created.return_value = False + svc = Service() + svc._status = "no_yes" + svc._verify_action_to_sdc("yes", "action", action_type='lifecycleState') + mock_created.assert_called() + mock_action.assert_not_called() + mock_load.assert_not_called() + +@mock.patch.object(Service, 'load') +@mock.patch.object(Service, '_action_to_sdc') +@mock.patch.object(Service, 'created') +def test_verify_action_to_sdc_bad_status(mock_created, mock_action, mock_load): + mock_created.return_value = True + svc = Service() + svc._status = "no_yes" + with pytest.raises(StatusError) as err: + svc._verify_action_to_sdc("yes", "action", action_type='lifecycleState') + assert err.type == StatusError + mock_created.assert_called() + mock_action.assert_not_called() + mock_load.assert_not_called() + +@mock.patch.object(Service, 'load') +@mock.patch.object(Service, '_action_to_sdc') +@mock.patch.object(Service, 'created') +def test_verify_action_to_sdc_OK(mock_created, mock_action, mock_load): + mock_created.return_value = True + mock_action.return_value = "good" + svc = Service() + svc._status = "yes" + svc._verify_action_to_sdc("yes", "action", action_type='lifecycleState') + mock_created.assert_called() + mock_action.assert_called_once() + mock_load.assert_called_once() + +@mock.patch.object(Service, 'distribute') +@mock.patch.object(Service, 'approve') +@mock.patch.object(Service, 'certify') +@mock.patch.object(Service, 'start_certification') +@mock.patch.object(Service, 'submit') +@mock.patch.object(Service, 'checkin') +@mock.patch.object(Service, 'add_resource') +@mock.patch.object(Service, 'create') +def test_onboard_new_service(mock_create, mock_add_resource, + mock_checkin, mock_submit, + mock_start_certification, mock_certify, + mock_approve, mock_distribute): + getter_mock = mock.Mock(wraps=Service.status.fget) + mock_status = Service.status.getter(getter_mock) + with mock.patch.object(Service, 'status', mock_status): + getter_mock.side_effect = [None, const.DISTRIBUTED, const.DISTRIBUTED, + const.DISTRIBUTED, const.DISTRIBUTED, + const.DISTRIBUTED, const.DISTRIBUTED, + const.DISTRIBUTED, None] + service = Service() + service._time_wait = 0 + service.onboard() + mock_create.assert_called_once() + mock_add_resource.assert_not_called() + mock_checkin.assert_not_called() + mock_submit.assert_not_called() + mock_start_certification.assert_not_called() + mock_certify.assert_not_called() + mock_approve.assert_not_called() + mock_distribute.assert_not_called() + +@mock.patch.object(Service, 'status') +def test_onboard_invalid_status(mock_status): + mock_status.return_value = False + service = Service() + service._time_wait = 0 + with pytest.raises(StatusError) as err: + service.onboard() + assert err.type == StatusError + +@mock.patch.object(Service, 'distribute') +@mock.patch.object(Service, 'approve') +@mock.patch.object(Service, 'certify') +@mock.patch.object(Service, 'start_certification') +@mock.patch.object(Service, 'submit') +@mock.patch.object(Service, 'checkin') +@mock.patch.object(Service, 'add_resource') +@mock.patch.object(Service, 'create') +def test_onboard_service_no_resources(mock_create, + mock_add_resource, mock_checkin, + mock_submit, mock_start_certification, + mock_certify, mock_approve, + mock_distribute): + getter_mock = mock.Mock(wraps=Service.status.fget) + mock_status = Service.status.getter(getter_mock) + with mock.patch.object(Service, 'status', mock_status): + getter_mock.side_effect = [const.DRAFT, const.DRAFT, const.DISTRIBUTED, + const.DISTRIBUTED, const.DISTRIBUTED, + const.DISTRIBUTED, const.DISTRIBUTED, + const.DISTRIBUTED, const.DISTRIBUTED, None] + service = Service() + service._time_wait = 0 + with pytest.raises(ParameterError): + service.onboard() + mock_create.assert_not_called() + mock_add_resource.assert_not_called() + mock_checkin.assert_not_called() + mock_submit.assert_not_called() + mock_start_certification.assert_not_called() + mock_certify.assert_not_called() + mock_approve.assert_not_called() + mock_distribute.assert_not_called() + +@mock.patch.object(Service, 'distribute') +@mock.patch.object(Service, 'approve') +@mock.patch.object(Service, 'certify') +@mock.patch.object(Service, 'start_certification') +@mock.patch.object(Service, 'submit') +@mock.patch.object(Service, 'checkin') +@mock.patch.object(Service, 'add_resource') +@mock.patch.object(Service, 'create') +def test_onboard_service_resources(mock_create, mock_add_resource, + mock_checkin, mock_submit, + mock_start_certification, mock_certify, + mock_approve, mock_distribute): + getter_mock = mock.Mock(wraps=Service.status.fget) + mock_status = Service.status.getter(getter_mock) + with mock.patch.object(Service, 'status', mock_status): + getter_mock.side_effect = [const.DRAFT, const.DRAFT, const.DISTRIBUTED, + const.DISTRIBUTED, const.DISTRIBUTED, + const.DISTRIBUTED, const.DISTRIBUTED, + const.DISTRIBUTED, const.DISTRIBUTED, None] + resource = SdcResource() + service = Service(resources=[resource]) + service._time_wait = 0 + service.onboard() + mock_create.assert_not_called() + mock_add_resource.assert_called_once_with(resource) + mock_checkin.assert_called_once() + mock_submit.assert_not_called() + mock_start_certification.assert_not_called() + mock_certify.assert_not_called() + mock_approve.assert_not_called() + mock_distribute.assert_not_called() + +@mock.patch.object(Service, 'distribute') +@mock.patch.object(Service, 'approve') +@mock.patch.object(Service, 'certify') +@mock.patch.object(Service, 'start_certification') +@mock.patch.object(Service, 'submit') +@mock.patch.object(Service, 'checkin') +@mock.patch.object(Service, 'add_resource') +@mock.patch.object(Service, 'create') +def test_onboard_service_several_resources(mock_create, + mock_add_resource, mock_checkin, + mock_submit, + mock_start_certification, + mock_certify, mock_approve, + mock_distribute): + getter_mock = mock.Mock(wraps=Service.status.fget) + mock_status = Service.status.getter(getter_mock) + with mock.patch.object(Service, 'status', mock_status): + getter_mock.side_effect = [const.DRAFT, const.DRAFT, const.DISTRIBUTED, + const.DISTRIBUTED, const.DISTRIBUTED, + const.DISTRIBUTED, const.DISTRIBUTED, + const.DISTRIBUTED, const.DISTRIBUTED, None] + resource1 = SdcResource() + resource2 = SdcResource() + service = Service(resources=[resource1, resource2]) + service._time_wait = 0 + service.onboard() + mock_create.assert_not_called() + calls = [mock.call(resource1), mock.call(resource2)] + mock_add_resource.assert_has_calls(calls, any_order=True) + assert mock_add_resource.call_count == 2 + mock_checkin.assert_called_once() + mock_submit.assert_not_called() + mock_start_certification.assert_not_called() + mock_certify.assert_not_called() + mock_approve.assert_not_called() + mock_distribute.assert_not_called() + +@mock.patch.object(Service, 'distribute') +@mock.patch.object(Service, 'approve') +@mock.patch.object(Service, 'certify') +@mock.patch.object(Service, 'start_certification') +@mock.patch.object(Service, 'submit') +@mock.patch.object(Service, 'checkin') +@mock.patch.object(Service, 'add_resource') +@mock.patch.object(Service, 'create') +def test_onboard_service_certifi(mock_create, + mock_add_resource, mock_checkin, + mock_submit, mock_start_certification, + mock_certify, mock_approve, + mock_distribute): + getter_mock = mock.Mock(wraps=Service.status.fget) + mock_status = Service.status.getter(getter_mock) + with mock.patch.object(Service, 'status', mock_status): + getter_mock.side_effect = [const.CHECKED_IN, + const.CHECKED_IN, + const.CHECKED_IN, + const.CHECKED_IN, + const.CHECKED_IN, + const.DISTRIBUTED, const.DISTRIBUTED, + const.DISTRIBUTED, const.DISTRIBUTED, + const.DISTRIBUTED, const.DISTRIBUTED, + const.DISTRIBUTED, None] + service = Service() + service._time_wait = 0 + service.onboard() + mock_create.assert_not_called() + mock_add_resource.assert_not_called() + mock_checkin.assert_not_called() + mock_submit.assert_not_called() + mock_start_certification.assert_not_called() + mock_certify.assert_called_once() + mock_approve.assert_not_called() + mock_distribute.assert_not_called() + +@mock.patch.object(Service, 'distribute') +@mock.patch.object(Service, 'certify') +@mock.patch.object(Service, 'checkin') +@mock.patch.object(Service, 'add_resource') +@mock.patch.object(Service, 'create') +def test_onboard_service_distribute(mock_create, + mock_add_resource, + mock_checkin, + mock_certify, + mock_distribute): + getter_mock = mock.Mock(wraps=Service.status.fget) + mock_status = Service.status.getter(getter_mock) + with mock.patch.object(Service, 'status', mock_status): + getter_mock.side_effect = [const.CERTIFIED, const.CERTIFIED, const.CERTIFIED, + const.CERTIFIED, const.CERTIFIED, const.CERTIFIED, + const.CERTIFIED, const.DISTRIBUTED, + const.DISTRIBUTED, const.DISTRIBUTED, + const.DISTRIBUTED, const.DISTRIBUTED, + const.DISTRIBUTED, const.DISTRIBUTED, None] + service = Service() + service._time_wait = 0 + service.onboard() + mock_create.assert_not_called() + mock_add_resource.assert_not_called() + mock_checkin.assert_not_called() + mock_certify.assert_not_called() + mock_distribute.assert_called_once() + +@mock.patch.object(Service, 'distribute') +@mock.patch.object(Service, 'certify') +@mock.patch.object(Service, 'checkin') +@mock.patch.object(Service, 'add_resource') +@mock.patch.object(Service, 'create') +def test_onboard_whole_service(mock_create, + mock_add_resource, + mock_checkin, + mock_certify, + mock_distribute): + getter_mock = mock.Mock(wraps=Service.status.fget) + mock_status = Service.status.getter(getter_mock) + with mock.patch.object(Service, 'status', mock_status): + getter_mock.side_effect = [None, const.DRAFT, const.DRAFT,const.CHECKED_IN, + const.CHECKED_IN, const.CHECKED_IN, + const.CERTIFIED, const.CERTIFIED, + const.CERTIFIED, const.CERTIFIED, + const.CERTIFIED, const.CERTIFIED, + const.DISTRIBUTED, const.DISTRIBUTED, + const.DISTRIBUTED, const.DISTRIBUTED, + const.DISTRIBUTED, const.DISTRIBUTED, + const.DISTRIBUTED, None] + resource = SdcResource() + service = Service(resources=[resource]) + service._time_wait = 0 + service.onboard() + mock_create.assert_called_once() + mock_add_resource.assert_called_once_with(resource) + mock_checkin.assert_called_once() + mock_certify.assert_called_once() + mock_distribute.assert_called_once() + +@mock.patch("onapsdk.sdc.service.Service.send_message_json") +@mock.patch("onapsdk.sdc.service.SdcResource.import_from_sdc") +@mock.patch("onapsdk.sdc.service.Service.resource_inputs_url", new_callable=mock.PropertyMock) +def test_vnf_vf_modules_one(mock_service_resource_inputs_url, mock_import_from_sdc, mock_send_message_json): + """Test parsing TOSCA file with one VNF which has associated one VFmodule""" + service = Service(name="test") + mock_send_message_json.side_effect = [{ + "componentInstances": [{ + "actualComponentUid": "123", + "originType": "VF", + "name": "ubuntu16_VF 0", + "toscaComponentName": "org.openecomp.resource.vf.Ubuntu16Vf", + "createdFromCsar": False, + "uniqueId": "123", + "normalizedName": "123", + "customizationUUID": "123", + "componentUid": "123", + "componentVersion": "123", + "componentName": "123", + "groupInstances": [ + { + "name": "ubuntu16_vf0..Ubuntu16Vf..base_ubuntu16..module-0", + "type": "org.openecomp.groups.VfModule", + "groupName": "Ubuntu16Vf..base_ubuntu16..module-0", + "groupUUID": "ed041b38-63fc-486d-9d4d-4e2531bc7e54", + "invariantUUID": "f47c3a9b-6a5f-4d1a-8a0b-b7f56ebb9a90", + "version": "1", + "customizationUUID": "d946ea06-ec4b-4ed2-921a-117e1379b913", + "properties": [ + { + "name": "123", + "type": "test type", + "value": "val", + "description": "12234", + }, + { + "name": "123", + "type": "test type", + "value": None, + "description": "12234", + } + ] + } + ] + }] + }, MagicMock() + ] + vnfs = list(service.vnfs) + assert len(vnfs) == 1 + vnf = vnfs[0] + assert vnf.name == "ubuntu16_VF 0" + assert vnf.node_template_type == "org.openecomp.resource.vf.Ubuntu16Vf" + assert vnf.vf_modules + assert vnf.vf_modules[0].name == "ubuntu16_vf0..Ubuntu16Vf..base_ubuntu16..module-0" + assert len(list(vnf.vf_modules[0].properties)) == 1 + +@mock.patch("onapsdk.sdc.service.Service.send_message_json") +@mock.patch("onapsdk.sdc.service.SdcResource.import_from_sdc") +@mock.patch("onapsdk.sdc.service.Service.resource_inputs_url", new_callable=mock.PropertyMock) +def test_pnf_modules_one(mock_service_resource_inputs_url, mock_import_from_sdc, mock_send_message_json): + """Test parsing TOSCA file with one PNF which has associated one PNFmodule""" + service = Service(name="test") + mock_send_message_json.side_effect = [{ + "componentInstances": [{ + "actualComponentUid": "123", + "originType": "PNF", + "name": "test_pnf_vsp 0", + "toscaComponentName": "org.openecomp.resource.pnf.TestPnfVsp", + "createdFromCsar": False, + "uniqueId": "123", + "normalizedName": "123", + "customizationUUID": "123", + "componentUid": "123", + "componentVersion": "123", + "componentName": "123", + "groupInstances": None + }] + }, MagicMock() + ] + pnfs = list(service.pnfs) + assert len(pnfs) == 1 + pnf = pnfs[0] + assert pnf.name == "test_pnf_vsp 0" + assert pnf.node_template_type == "org.openecomp.resource.pnf.TestPnfVsp" + +@mock.patch("onapsdk.sdc.service.Service.send_message_json") +@mock.patch("onapsdk.sdc.service.SdcResource.import_from_sdc") +@mock.patch("onapsdk.sdc.service.Service.resource_inputs_url", new_callable=mock.PropertyMock) +def test_vnf_vf_modules_two(mock_service_resource_inputs_url, mock_import_from_sdc, mock_send_message_json): + """Test parsing TOSCA file with two VNF which has associated one VFmodule""" + service = Service(name="test") + mock_send_message_json.side_effect = [{ + "componentInstances": [{ + "actualComponentUid": "123", + "originType": "VF", + "name": "vFWCL_vPKG-vf 0", + "toscaComponentName": "org.openecomp.resource.vf.VfwclVpkgVf", + "createdFromCsar": False, + "uniqueId": "123", + "normalizedName": "123", + "customizationUUID": "123", + "componentUid": "123", + "componentVersion": "123", + "componentName": "123", + "groupInstances": [ + { + "name": "vfwcl_vpkgvf0..VfwclVpkgVf..base_vpkg..module-0", + "type": "org.openecomp.groups.VfModule", + "groupName": "Ubuntu16Vf..base_ubuntu16..module-0", + "groupUUID": "ed041b38-63fc-486d-9d4d-4e2531bc7e54", + "invariantUUID": "f47c3a9b-6a5f-4d1a-8a0b-b7f56ebb9a90", + "version": "1", + "customizationUUID": "d946ea06-ec4b-4ed2-921a-117e1379b913", + "properties": [ + { + "name": "123", + "type": "test type", + "value": "val", + "description": "12234", + }, + { + "name": "333", + "type": "test type", + "value": "val", + "description": "12234", + }, + { + "name": "123", + "type": "test type", + "value": None, + "description": "12234", + } + ] + }, + { + "name": "vfwcl_vpkgvf0..base_template_dummy_ignore..base_vpkg..module-0", + "type": "org.openecomp.groups.VfModule", + "groupName": "Ubuntu16Vf..base_ubuntu16..module-0", + "groupUUID": "ed041b38-63fc-486d-9d4d-4e2531bc7e54", + "invariantUUID": "f47c3a9b-6a5f-4d1a-8a0b-b7f56ebb9a90", + "version": "1", + "customizationUUID": "d946ea06-ec4b-4ed2-921a-117e1379b913", + "properties": [ + { + "name": "123", + "type": "test type", + "value": "val", + "description": "12234", + }, + { + "name": "333", + "type": "test type", + "value": "val", + "description": "12234", + }, + { + "name": "vf_module_label", + "type": "test type", + "value": "base_template_dummy_ignore", + "description": "12234", + } + ] + } + ] + }, + { + "actualComponentUid": "123", + "originType": "VF", + "name": "vFWCL_vFWSNK-vf 0", + "toscaComponentName": "org.openecomp.resource.vf.VfwclVfwsnkVf", + "createdFromCsar": False, + "uniqueId": "123", + "normalizedName": "123", + "customizationUUID": "123", + "componentUid": "123", + "componentVersion": "123", + "componentName": "123", + "groupInstances": [ + { + "name": "vfwcl_vfwsnkvf0..VfwclVfwsnkVf..base_vfw..module-0", + "type": "org.openecomp.groups.VfModule", + "groupName": "Ubuntu16Vf..base_ubuntu16..module-0", + "groupUUID": "ed041b38-63fc-486d-9d4d-4e2531bc7e54", + "invariantUUID": "f47c3a9b-6a5f-4d1a-8a0b-b7f56ebb9a90", + "version": "1", + "customizationUUID": "d946ea06-ec4b-4ed2-921a-117e1379b913", + "properties": [ + { + "name": "123", + "type": "test type", + "value": "val", + "description": "12234", + }, + { + "name": "123", + "type": "test type", + "value": None, + "description": "12234", + } + ] + } + ] + }] + }, MagicMock(), MagicMock() + ] + vnfs = list(service.vnfs) + assert len(vnfs) == 2 + vnf = vnfs[0] + assert vnf.name == "vFWCL_vPKG-vf 0" + assert vnf.node_template_type == "org.openecomp.resource.vf.VfwclVpkgVf" + assert vnf.vf_modules + assert len(vnf.vf_modules) == 1 + assert vnf.vf_modules[0].name == "vfwcl_vpkgvf0..VfwclVpkgVf..base_vpkg..module-0" + assert len(list(vnf.vf_modules[0].properties)) == 2 + + vnf = vnfs[1] + assert vnf.name == "vFWCL_vFWSNK-vf 0" + assert vnf.node_template_type == "org.openecomp.resource.vf.VfwclVfwsnkVf" + assert vnf.vf_modules + assert vnf.vf_modules[0].name == "vfwcl_vfwsnkvf0..VfwclVfwsnkVf..base_vfw..module-0" + assert len(list(vnf.vf_modules[0].properties)) == 1 + + +@mock.patch.object(Service, 'send_message_json') +def test_get_vnf_unique_id(mock_send): + """Test Service get nf uid with One Vf""" + svc = Service() + svc.unique_identifier = "service_unique_identifier" + mock_send.return_value = ARTIFACTS + unique_id = svc.get_nf_unique_id(nf_name="ubuntu16test_VF 0") + mock_send.assert_called_once_with( + 'GET', 'Get nf unique ID', + f"https://sdc.api.fe.simpledemo.onap.org:30207/sdc1/feProxy/rest/v1/catalog/services/{svc.unique_identifier}") + assert unique_id == 'test_unique_id' + +@mock.patch.object(Service, 'send_message_json') +def test_get_vnf_unique_id_not_found(mock_send): + """Test Service get nf uid with One Vf""" + svc = Service() + svc.unique_identifier = "service_unique_identifier" + artifacts = {"componentInstances": []} + mock_send.return_value = artifacts + with pytest.raises(ResourceNotFound) as err: + svc.get_nf_unique_id(nf_name="ubuntu16test_VF 0") + assert err.type == ResourceNotFound + mock_send.assert_called_once_with( + 'GET', 'Get nf unique ID', + f"https://sdc.api.fe.simpledemo.onap.org:30207/sdc1/feProxy/rest/v1/catalog/services/{svc.unique_identifier}") + +@mock.patch.object(Service, 'get_nf_unique_id') +@mock.patch.object(Service, 'load') +@mock.patch.object(Service, 'send_message') +def test_add_artifact_to_vf(mock_send_message, mock_load, mock_add): + """Test Service add artifact""" + svc = Service() + mock_add.return_value = "54321" + result = svc.add_artifact_to_vf(vnf_name="ubuntu16test_VF 0", + artifact_type="DCAE_INVENTORY_BLUEPRINT", + artifact_name="clampnode.yaml", + artifact="data".encode('utf-8')) + mock_send_message.assert_called() + method, description, url = mock_send_message.call_args[0] + assert method == "POST" + assert description == "Add artifact to vf" + assert url == ("https://sdc.api.fe.simpledemo.onap.org:30207/sdc1/feProxy/rest/v1/catalog/services/" + f"{svc.unique_identifier}/resourceInstance/54321/artifacts") + +@mock.patch.object(Service, 'load') +@mock.patch.object(Service, 'send_message') +def test_add_artifact_to_service(mock_send_message, mock_load): + """Test Service add artifact""" + svc = Service() + svc.status = const.DRAFT + mycbapath = Path(Path(__file__).resolve().parent, "data/vLB_CBA_Python.zip") + + result = svc.add_deployment_artifact(artifact_label="cba", + artifact_type="CONTROLLER_BLUEPRINT_ARCHIVE", + artifact_name="vLB_CBA_Python.zip", + artifact=mycbapath) + mock_send_message.assert_called() + method, description, url = mock_send_message.call_args[0] + assert method == "POST" + assert description == "Add deployment artifact for ONAP-test-Service sdc resource" + assert url == ("https://sdc.api.fe.simpledemo.onap.org:30207/sdc1/feProxy/rest/v1/catalog/services/" + f"{svc.unique_identifier}/artifacts") + +@mock.patch("onapsdk.sdc.service.Service.send_message_json") +@mock.patch("onapsdk.sdc.service.SdcResource.import_from_sdc") +@mock.patch("onapsdk.sdc.service.Service.resource_inputs_url", new_callable=mock.PropertyMock) +def test_service_networks(mock_service_resource_inputs_url, mock_import_from_sdc, mock_send_message_json): + mock_send_message_json.side_effect = [{ + "componentInstances": [{ + "actualComponentUid": "123", + "originType": "VL", + "name": "NeutronNet 0", + "toscaComponentName": "org.openecomp.resource.vl.nodes.heat.network.neutron.Net", + "createdFromCsar": False, + "uniqueId": "123", + "normalizedName": "123", + "customizationUUID": "123", + "componentUid": "123", + "componentVersion": "123", + "componentName": "123", + "groupInstances": None + }] + }, MagicMock() + ] + + service = Service(name="test") + networks = list(service.networks) + assert len(networks) == 1 + network = networks[0] + assert network.name == "NeutronNet 0" + assert network.node_template_type == "org.openecomp.resource.vl.nodes.heat.network.neutron.Net" + +@mock.patch.object(Service, '_unzip_csar_file') +def test_tosca_template_no_tosca_model(mock_unzip): + service = Service(name="test") + getter_mock = mock.Mock(wraps=Service.tosca_model.fget) + getter_mock.return_value = False + mock_tosca_model = Service.tosca_model.getter(getter_mock) + with mock.patch.object(Service, 'tosca_model', mock_tosca_model): + service.tosca_template + mock_unzip.assert_not_called() + +@mock.patch.object(Service, '_unzip_csar_file') +def test_tosca_template_tosca_model(mock_unzip): + service = Service(name="test") + service._tosca_model = str.encode("test") + service.tosca_template + mock_unzip.assert_called_once_with(mock.ANY, mock.ANY) + +@mock.patch.object(Service, '_unzip_csar_file') +def test_tosca_template_present(mock_unzip): + service = Service(name="test") + service._tosca_template = "test" + assert service.tosca_template == "test" + mock_unzip.assert_not_called() + +@mock.patch.object(Service, 'send_message') +def test_tosca_model(mock_send): + service = Service(name="test") + service.identifier = "toto" + service.tosca_model + mock_send.assert_called_once_with("GET", "Download Tosca Model for test", + "https://sdc.api.be.simpledemo.onap.org:30204/sdc/v1/catalog/services/toto/toscaModel", + headers={'Content-Type': 'application/json', 'Accept': 'application/octet-stream', 'USER_ID': 'cs0008', 'Authorization': 'Basic YWFpOktwOGJKNFNYc3pNMFdYbGhhazNlSGxjc2UyZ0F3ODR2YW9HR21KdlV5MlU=', 'X-ECOMP-InstanceID': 'onapsdk'}) + +@mock.patch.object(Service, "send_message_json") +def test_add_properties(mock_send_message_json): + service = Service(name="test") + service._identifier = "toto" + service._unique_identifier = "toto" + service._status = const.CERTIFIED + with pytest.raises(StatusError): + service.add_property(Property(name="test", property_type="string")) + service._status = const.DRAFT + service.add_property(Property(name="test", property_type="string")) + mock_send_message_json.assert_called_once() + +@mock.patch.object(Service, "send_message_json") +def test_service_components(mock_send_message_json): + service = Service(name="test") + service.unique_identifier = "toto" + + mock_send_message_json.return_value = {} + assert len(list(service.components)) == 0 + + mock_send_message_json.reset_mock() + mock_send_message_json.side_effect = [COMPONENTS, COMPONENT] + components = list(service.components) + assert len(components) == 1 + assert mock_send_message_json.call_count == 2 + component = components[0] + assert component.actual_component_uid == "374f0a98-a280-43f1-9e6c-00b436782ce7" + assert component.sdc_resource.unique_uuid == "3c027ba1-8d3a-4b59-9394-d748fec5e42c" + +def test_component_properties(): + sdc_resource = mock.MagicMock() + service = Service(name="test") + service.unique_identifier = "toto" + + component = Component( + created_from_csar=False, + actual_component_uid="123", + unique_id="123", + normalized_name="123", + name="123", + origin_type="123", + customization_uuid="123", + tosca_component_name="123", + component_name="123", + component_uid="123", + component_version="123", + sdc_resource=sdc_resource, + parent_sdc_resource=service, + group_instances=None + ) + sdc_resource.send_message_json.return_value = {} + assert not len(list(component.properties)) + + sdc_resource.send_message_json.return_value = COMPONENT_PROPERTIES + properties = list(component.properties) + assert len(properties) == 2 + prop1, prop2 = properties + + assert prop1.unique_id == "3d9a184f-4268-4a0e-9ddd-252e49670013.vf_module_id" + assert prop1.property_type == "string" + assert prop1.name == "vf_module_id" + assert prop1.value is None + + assert prop2.unique_id == "74f79006-ae56-4d58-947e-6a5089000774.skip_post_instantiation_configuration" + assert prop2.property_type == "boolean" + assert prop2.name == "skip_post_instantiation_configuration" + assert prop2.value == "true" + +@mock.patch.object(Component, "properties", new_callable=mock.PropertyMock) +def test_component_property_set_value(mock_component_properties): + mock_sdc_resource = mock.MagicMock() + service = Service(name="test") + service.unique_identifier = "toto" + component = Component( + created_from_csar=False, + actual_component_uid="123", + unique_id="123", + normalized_name="123", + name="123", + origin_type="123", + customization_uuid="123", + tosca_component_name="123", + component_name="123", + component_uid="123", + component_version="123", + sdc_resource=mock_sdc_resource, + parent_sdc_resource=service, + group_instances=None + ) + mock_component_properties.return_value = [ + ComponentProperty( + unique_id="123", + property_type="string", + name="test_property", + component=component + ) + ] + with pytest.raises(ParameterError): + component.get_property(property_name="non_exists") + prop1 = component.get_property(property_name="test_property") + assert prop1.name == "test_property" + assert prop1.unique_id == "123" + assert prop1.property_type == "string" + assert not prop1.value + + prop1.value = "123" + mock_sdc_resource.send_message_json.assert_called_once() + +@mock.patch.object(Service, "add_resource") +@mock.patch.object(Service, "add_property") +@mock.patch.object(Service, "declare_input") +def test_declare_resources_and_properties(mock_declare_input, mock_add_property, mock_add_resource): + + service = Service(name="test", + resources=[SdcResource()], + properties=[Property(name="test", property_type="string")], + inputs=[Property(name="test", property_type="string")]) + service.declare_resources_and_properties() + mock_add_resource.assert_called_once() + mock_add_property.assert_called_once() + mock_declare_input.assert_called_once() + +@mock.patch.object(Service, "created") +@mock.patch.object(ServiceCategory, "get") +def test_service_category(mock_resource_category, mock_created): + mock_created.return_value = False + service = Service(name="test") + _ = service.category + mock_resource_category.assert_called_once_with(name="Network Service") + mock_resource_category.reset_mock() + + service = Service(name="test", category="test") + _ = service.category + mock_resource_category.assert_called_once_with(name="test") + mock_resource_category.reset_mock() + + mock_created.return_value = True + _ = service.category + mock_resource_category.assert_called_once_with(name="test") + +def test_service_origin_type(): + service = Service(name="test") + assert service.origin_type == "ServiceProxy" + +@mock.patch.object(Service, "unique_identifier", new_callable=PropertyMock) +def test_service_metadata_url(mock_uniquie_identifier): + mock_uniquie_identifier.return_value = "1233" + service = Service(name="test") + assert service.metadata_url == f"{service._base_create_url()}/services/1233/filteredDataByParams?include=metadata" + + +@mock.patch.object(Service, "created") +@mock.patch.object(Service, "send_message_json") +@mock.patch.object(Service, "metadata_url", new_callable=PropertyMock) +def test_service_instantiation_type(mock_metadata_url, mock_send_message_json, mock_created): + mock_created.return_value = False + service = Service(name="test") + assert service.instantiation_type == ServiceInstantiationType.A_LA_CARTE + + service = Service(name="test", instantiation_type=ServiceInstantiationType.MACRO) + assert service.instantiation_type == ServiceInstantiationType.MACRO + + mock_created.return_value = True + mock_send_message_json.return_value = {"metadata": {"instantiationType": "A-la-carte"}} + service = Service(name="test") + assert service.instantiation_type == ServiceInstantiationType.A_LA_CARTE + + mock_send_message_json.return_value = {"metadata": {"instantiationType": "Macro"}} + service = Service(name="test") + assert service.instantiation_type == ServiceInstantiationType.MACRO + + +@mock.patch.object(Service, "get_all") +def test_service_get_by_unique_uuid(mock_get_all): + mock_get_all.return_value = [] + with pytest.raises(ResourceNotFound): + Service.get_by_unique_uuid("test") + mock_service = MagicMock() + mock_service.unique_uuid = "test" + mock_get_all.return_value = [mock_service] + Service.get_by_unique_uuid("test") + +@mock.patch.object(Service, "send_message_json") +def test_service_components(mock_send_message_json): + service = Service(name="test") + service.unique_identifier = "toto" + + mock_send_message_json.side_effect = [COMPONENTS, COMPONENT, COMPONENTS, COMPONENT, COMPONENTS, COMPONENT] + assert not service.has_vnfs + assert not service.has_pnfs + assert not service.has_vls + + mock_send_message_json.side_effect = [COMPONENTS_WITH_ALL_ORIGIN_TYPES, COMPONENT, + COMPONENTS_WITH_ALL_ORIGIN_TYPES, COMPONENT, COMPONENT, + COMPONENTS_WITH_ALL_ORIGIN_TYPES, COMPONENT, COMPONENT, COMPONENT] + assert service.has_vnfs + assert service.has_pnfs + assert service.has_vls diff --git a/tests/test_settings.py b/tests/test_settings.py new file mode 100644 index 0000000..7e62da6 --- /dev/null +++ b/tests/test_settings.py @@ -0,0 +1,79 @@ +# Copyright 2022 Orange, 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 os +import sys +from pathlib import PurePath + +import pytest + +from onapsdk.configuration import settings, SETTINGS_ENV +from onapsdk.configuration.loader import SettingsLoader +from onapsdk.exceptions import ModuleError + + +def test_global_settings(): + """Test global settings.""" + assert len(settings._settings) == 43 + assert settings.AAI_URL == "https://aai.api.sparky.simpledemo.onap.org:30233" + assert settings.CDS_URL == "http://portal.api.simpledemo.onap.org:30449" + assert settings.SDNC_URL == "https://sdnc.api.simpledemo.onap.org:30267" + assert settings.SO_URL == "http://so.api.simpledemo.onap.org:30277" + assert settings.MSB_URL == "https://msb.api.simpledemo.onap.org:30283" + assert settings.SDC_FE_URL == "https://sdc.api.fe.simpledemo.onap.org:30207" + assert settings.SDC_BE_URL == "https://sdc.api.be.simpledemo.onap.org:30204" + assert settings.VID_URL == "https://vid.api.simpledemo.onap.org:30200" + assert settings.CLAMP_URL == "https://clamp.api.simpledemo.onap.org:30258" + assert settings.VES_URL == "http://ves.api.simpledemo.onap.org:30417" + assert settings.DMAAP_URL == "http://dmaap.api.simpledemo.onap.org:3904" + assert settings.NBI_URL == "https://nbi.api.simpledemo.onap.org:30274" + assert settings.DCAEMOD_URL == "" + assert settings.HOLMES_URL == "https://aai.api.sparky.simpledemo.onap.org:30293" + assert settings.POLICY_URL == "" + assert settings.AAI_GUI_URL == "https://aai.api.sparky.simpledemo.onap.org:30220" + assert settings.AAI_GUI_SERVICE == "https://aai.api.sparky.simpledemo.onap.org:30220/services/aai/webapp/index.html#/browse" + assert settings.CDS_GUI_SERVICE == "http://portal.api.simpledemo.onap.org:30449/" + assert settings.SO_MONITOR_GUI_SERVICE == "http://so.api.simpledemo.onap.org:30277/" + assert settings.SDC_GUI_SERVICE == "https://sdc.api.fe.simpledemo.onap.org:30207/sdc1/portal" + assert settings.SDNC_DG_GUI_SERVICE == "https://sdnc.api.simpledemo.onap.org:30267/nifi/" + assert settings.SDNC_ODL_GUI_SERVICE == "https://sdnc.api.simpledemo.onap.org:30267/odlux/index.html" + assert settings.DCAEMOD_GUI_SERVICE == "/" + assert settings.HOLMES_GUI_SERVICE == "https://aai.api.sparky.simpledemo.onap.org:30293/iui/holmes/default.html" + assert settings.POLICY_GUI_SERVICE == "/onap/login.html" + assert settings.POLICY_CLAMP_GUI_SERVICE == "https://clamp.api.simpledemo.onap.org:30258/" + assert settings.PROJECT == "Onapsdk_project" + assert settings.LOB == "Onapsdk_lob" + assert settings.PLATFORM == "Onapsdk_platform" + assert hasattr(settings, "AAI_AUTH") + assert hasattr(settings, "CDS_AUTH") + assert hasattr(settings, "SDC_AUTH") + assert hasattr(settings, "SDNC_AUTH") + assert hasattr(settings, "CLAMP_AUTH") + assert hasattr(settings, "SO_AUTH") + assert hasattr(settings, "SO_CAT_DB_AUTH") + + +def test_settings_load_custom(): + """Test if custom settings is loaded correctly.""" + sys.path.append(str(PurePath(__file__).parent)) + os.environ[SETTINGS_ENV] = "data.tests_settings" + custom_settings = SettingsLoader() + assert custom_settings.AAI_URL == "http://tests.settings.py:1234" + assert custom_settings.TEST_VALUE == "test" + + +def test_invalid_custom_settings(): + """Test if loading invalid custom settings raises ModuleError.""" + os.environ[SETTINGS_ENV] = "non.existings.package" + with pytest.raises(ModuleError): + SettingsLoader() diff --git a/tests/test_so_db_adapter.py b/tests/test_so_db_adapter.py new file mode 100644 index 0000000..50f26d6 --- /dev/null +++ b/tests/test_so_db_adapter.py @@ -0,0 +1,136 @@ +# Copyright 2022 Orange, 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 unittest import mock + +from onapsdk.so.so_db_adapter import SoDbAdapter, IdentityService + +ADD_CLOUD_SITE_RESPONSE = { + '_links': { + 'cloudSite': { + 'href': 'http://so.api.simpledemo.onap.org:30277/cloudSite/mc_test_cloud_site_3' + }, + 'self': { + 'href': 'http://so.api.simpledemo.onap.org:30277/cloudSite/mc_test_cloud_site_3' + } + }, + 'aic_version': '2.5', + 'clli': 'test_clli_0', + 'cloud_owner': None, + 'cloudify_id': None, + 'creation_timestamp': '2021-05-12T08:52:48.134+0000', + 'identityService': { + 'admin_tenant': 'service', + 'creation_timestamp': '2021-05-12T08:52:48.134+0000', + 'identityServerTypeAsString': 'KEYSTONE', + 'identity_authentication_type': 'USERNAME_PASSWORD', + 'identity_server_type': 'KEYSTONE', + 'identity_url': 'http://1.2.3.4:5000/v2.0', + 'last_updated_by': None, + 'member_role': 'admin', + 'mso_id': 'onapsdk_user', + 'mso_pass': 'mso_pass_onapsdk', + 'project_domain_name': 'NULL', + 'tenant_metadata': True, + 'update_timestamp': '2021-05-12T08:52:48.134+0000', + 'user_domain_name': 'NULL' + }, + 'identity_service_id': 'test_identity_0', + 'last_updated_by': None, + 'orchestrator': 'multicloud', + 'platform': None, + 'region_id': 'test_region_0', + 'support_fabric': True, + 'update_timestamp': '2021-05-12T08:52:48.134+0000', + 'uri': None +} + +SERVICE_VNF_RESPONSE = { + 'serviceVnfs': [ + { + 'modelInfo': { + 'modelName': 'test_vnf_01', + 'modelUuid': 'd2779cc5-fb01-449f-a355-7e5d911dca93', + 'modelInvariantUuid': '027cb696-f68f-47db-9b0e-585ea3eaa512', + 'modelVersion': '1.0', + 'modelCustomizationUuid': 'b8740912-e0fc-426f-af97-7657caf57847', + 'modelInstanceName': 'test_vnf_01 0' + }, + 'toscaNodeType': 'org.openecomp.resource.vf.Mvnr5gCucpVfT003', + 'nfFunction': None, + 'nfType': None, + 'nfRole': None, + 'nfNamingCode': None, + 'multiStageDesign': 'false', + 'vnfcInstGroupOrder': None, + 'resourceInput': None, + 'vfModules': [{'modelInfo': + { + 'modelName': 'test_vf_01', + 'modelUuid': '153464b8-4f47-4140-8b92-9614c4578d91', + 'modelInvariantUuid': '753deff5-99a2-4154-8c1d-3e956cb96f32', + 'modelVersion': '1', + 'modelCustomizationUuid': '7ca564f3-b908-499c-b086-ae77ad270d8c' + }, + 'isBase': False, + 'vfModuleLabel': 'vf_mod_label', + 'initialCount': 0, + 'hasVolumeGroup': False + } + ], + 'groups': [] + } + ] +} + + +def test_identity_service(): + identity_service = IdentityService(identity_id="identity_123") + assert identity_service.identity_id == "identity_123" + assert identity_service.url == "http://1.2.3.4:5000/v2.0" + assert identity_service.mso_id == "onapsdk_user" + assert identity_service.mso_pass == "mso_pass_onapsdk" + assert identity_service.project_domain_name == "NULL" + assert identity_service.user_domain_name == "NULL" + assert identity_service.admin_tenant == "service" + assert identity_service.member_role == "admin" + assert identity_service.identity_server_type == "KEYSTONE" + assert identity_service.identity_authentication_type == "USERNAME_PASSWORD" + assert identity_service.hibernate_lazy_initializer == {} + assert identity_service.server_type_as_string == "KEYSTONE" + assert identity_service.tenant_metadata is True + +@mock.patch.object(SoDbAdapter, "send_message_json") +def test_add_cloud_site(mock_send_message_json): + identity_service = IdentityService(identity_id="test_identity_0") + mock_send_message_json.return_value = ADD_CLOUD_SITE_RESPONSE + + response = SoDbAdapter.add_cloud_site(cloud_region_id="test_region_0", + complex_id="test_clli_0", + identity_service=identity_service) + assert response['region_id'] == "test_region_0" + assert response['aic_version'] == "2.5" + assert response['clli'] == "test_clli_0" + assert response['orchestrator'] == "multicloud" + assert response['identity_service_id'] == "test_identity_0" + +@mock.patch.object(SoDbAdapter, "send_message_json") +def test_get_service_vnf_info(mock_send_message_json): + mock_send_message_json.return_value = ADD_CLOUD_SITE_RESPONSE + + response = SoDbAdapter.get_service_vnf_info(identifier="test_id_0") + assert response['region_id'] == "test_region_0" + assert response['aic_version'] == "2.5" + assert response['clli'] == "test_clli_0" + assert response['orchestrator'] == "multicloud" + assert response['identity_service_id'] == "test_identity_0" diff --git a/tests/test_so_deletion.py b/tests/test_so_deletion.py new file mode 100644 index 0000000..ff10474 --- /dev/null +++ b/tests/test_so_deletion.py @@ -0,0 +1,74 @@ +# Copyright 2022 Orange, 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 unittest import mock + +from onapsdk.so.deletion import ( + ServiceDeletionRequest, + VfModuleDeletionRequest, + VnfDeletionRequest +) + + +@mock.patch.object(ServiceDeletionRequest, "send_message") +def test_service_deletion_request(mock_send_message): + mock_instance = mock.MagicMock() + mock_instance.instance_id = "test_instance_id" + ServiceDeletionRequest.send_request(instance=mock_instance) + mock_send_message.assert_called_once() + method, _, url = mock_send_message.call_args[0] + assert method == "DELETE" + assert url == (f"{ServiceDeletionRequest.base_url}/onap/so/infra/" + f"serviceInstantiation/{ServiceDeletionRequest.api_version}/" + "serviceInstances/test_instance_id") + + +@mock.patch.object(VfModuleDeletionRequest, "send_message") +def test_vf_module_deletion_request(mock_send_message): + mock_vf_module_instance = mock.MagicMock() + mock_vf_module_instance.vf_module_id = "test_vf_module_id" + + mock_vnf_instance = mock.MagicMock() + mock_vnf_instance.vnf_id = "test_vnf_id" + mock_vf_module_instance.vnf_instance = mock_vnf_instance + + mock_service_instance = mock.MagicMock() + mock_service_instance.instance_id = "test_service_instance_id" + mock_vnf_instance.service_instance = mock_service_instance + + VfModuleDeletionRequest.send_request(instance=mock_vf_module_instance) + mock_send_message.assert_called_once() + method, _, url = mock_send_message.call_args[0] + assert method == "DELETE" + assert url == (f"{VfModuleDeletionRequest.base_url}/onap/so/infra/" + f"serviceInstantiation/{VfModuleDeletionRequest.api_version}/" + "serviceInstances/test_service_instance_id/" + "vnfs/test_vnf_id/vfModules/test_vf_module_id") + + +@mock.patch.object(VnfDeletionRequest, "send_message") +def test_vnf_deletion_request(mock_send_message): + mock_vnf_instance = mock.MagicMock() + mock_vnf_instance.vnf_id = "test_vnf_id" + + mock_service_instance = mock.MagicMock() + mock_service_instance.instance_id = "test_service_instance" + mock_vnf_instance.service_instance = mock_service_instance + VnfDeletionRequest.send_request(instance=mock_vnf_instance) + mock_send_message.assert_called_once() + method, _, url = mock_send_message.call_args[0] + assert method == "DELETE" + assert url == (f"{VnfDeletionRequest.base_url}/onap/so/infra/" + f"serviceInstantiation/{VnfDeletionRequest.api_version}/" + "serviceInstances/test_service_instance/" + "vnfs/test_vnf_id") diff --git a/tests/test_so_element.py b/tests/test_so_element.py new file mode 100644 index 0000000..cc866d9 --- /dev/null +++ b/tests/test_so_element.py @@ -0,0 +1,28 @@ +"""Test A&AI Element.""" +# Copyright 2022 Orange, 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 unittest import mock + +from onapsdk.so.so_element import SoElement +from onapsdk.utils.gui import GuiList + +@mock.patch.object(SoElement, "send_message") +def test_get_guis(send_message_mock): + component = SoElement() + send_message_mock.return_value.status_code = 200 + send_message_mock.return_value.url = "http://so.api.simpledemo.onap.org:30277/" + gui_results = component.get_guis() + assert type(gui_results) == GuiList + assert gui_results.guilist[0].url == send_message_mock.return_value.url + assert gui_results.guilist[0].status == send_message_mock.return_value.status_code diff --git a/tests/test_so_instantiation.py b/tests/test_so_instantiation.py new file mode 100644 index 0000000..5c8f41a --- /dev/null +++ b/tests/test_so_instantiation.py @@ -0,0 +1,1002 @@ +# Copyright 2022 Orange, 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 json +import oyaml as yaml +from collections import namedtuple +from pathlib import Path +from unittest import mock + +import pytest + +from onapsdk.exceptions import APIError, InvalidResponse, ResourceNotFound, StatusError +from onapsdk.sdc.service import Service +from onapsdk.sdnc import NetworkPreload, VfModulePreload +from onapsdk.so.instantiation import ( + NetworkInstantiation, + ServiceInstantiation, + SoService, + SoServicePnf, + SoServiceVfModule, + SoServiceVnf, + VfModuleInstantiation, + VnfInstantiation, + VnfOperation +) +from onapsdk.vid import Vid +from onapsdk.aai.business.owning_entity import OwningEntity + + +@mock.patch.object(ServiceInstantiation, "send_message_json") +def test_service_ala_carte_instantiation(mock_service_instantiation_send_message): + mock_sdc_service = mock.MagicMock() + mock_sdc_service.distributed = False + with pytest.raises(StatusError): + ServiceInstantiation.\ + instantiate_ala_carte(sdc_service=mock_sdc_service, + cloud_region=mock.MagicMock(), + tenant=mock.MagicMock(), + customer=mock.MagicMock(), + owning_entity=mock.MagicMock(), + project=mock.MagicMock(), + service_instance_name="test", + service_subscription=mock.MagicMock()) + mock_sdc_service.distributed = True + service_instance = ServiceInstantiation.\ + instantiate_ala_carte(sdc_service=mock_sdc_service, + cloud_region=mock.MagicMock(), + tenant=mock.MagicMock(), + customer=mock.MagicMock(), + owning_entity=mock.MagicMock(), + project=mock.MagicMock(), + service_instance_name="test", + service_subscription=mock.MagicMock()) + assert service_instance.name == "test" + + service_instance = ServiceInstantiation.\ + instantiate_ala_carte(sdc_service=mock_sdc_service, + cloud_region=mock.MagicMock(), + tenant=mock.MagicMock(), + customer=mock.MagicMock(), + owning_entity=mock.MagicMock(), + project=mock.MagicMock(), + service_subscription=mock.MagicMock()) + assert service_instance.name.startswith("Python_ONAP_SDK_service_instance_") + mock_service_instantiation_send_message.assert_called() + method, _, url = mock_service_instantiation_send_message.call_args[0] + assert method == "POST" + assert url == (f"{ServiceInstantiation.base_url}/onap/so/infra/" + f"serviceInstantiation/{ServiceInstantiation.api_version}/serviceInstances") + + +@mock.patch.object(ServiceInstantiation, "send_message_json") +def test_service_macro_instantiation(mock_service_instantiation_send_message): + mock_sdc_service = mock.MagicMock() + mock_sdc_service.distributed = False + with pytest.raises(StatusError): + ServiceInstantiation.\ + instantiate_macro(sdc_service=mock_sdc_service, + cloud_region=mock.MagicMock(), + tenant=mock.MagicMock(), + customer=mock.MagicMock(), + owning_entity=mock.MagicMock(), + project=mock.MagicMock(), + line_of_business=mock.MagicMock(), + platform=mock.MagicMock(), + service_instance_name="test", + service_subscription=mock.MagicMock()) + mock_sdc_service.distributed = True + service_instance = ServiceInstantiation.\ + instantiate_macro(sdc_service=mock_sdc_service, + cloud_region=mock.MagicMock(), + tenant=mock.MagicMock(), + customer=mock.MagicMock(), + owning_entity=mock.MagicMock(), + project=mock.MagicMock(), + line_of_business=mock.MagicMock(), + platform=mock.MagicMock(), + service_instance_name="test", + service_subscription=mock.MagicMock()) + assert service_instance.name == "test" + + service_instance = ServiceInstantiation.\ + instantiate_macro(sdc_service=mock_sdc_service, + cloud_region=mock.MagicMock(), + tenant=mock.MagicMock(), + customer=mock.MagicMock(), + owning_entity=mock.MagicMock(), + line_of_business=mock.MagicMock(), + platform=mock.MagicMock(), + project=mock.MagicMock(), + so_service=mock.MagicMock()) + assert service_instance.name.startswith("Python_ONAP_SDK_service_instance_") + mock_service_instantiation_send_message.assert_called() + method, _, url = mock_service_instantiation_send_message.call_args[0] + assert method == "POST" + assert url == (f"{ServiceInstantiation.base_url}/onap/so/infra/" + f"serviceInstantiation/{ServiceInstantiation.api_version}/serviceInstances") + + so_service_mock = mock.MagicMock() + so_service_mock.instance_name = "SoServiceInstanceName" + service_instance = ServiceInstantiation.\ + instantiate_macro(sdc_service=mock_sdc_service, + cloud_region=mock.MagicMock(), + tenant=mock.MagicMock(), + customer=mock.MagicMock(), + owning_entity=mock.MagicMock(), + line_of_business=mock.MagicMock(), + platform=mock.MagicMock(), + project=mock.MagicMock(), + so_service=so_service_mock) + assert service_instance.name == "SoServiceInstanceName" + mock_service_instantiation_send_message.assert_called() + method, _, url = mock_service_instantiation_send_message.call_args[0] + assert method == "POST" + assert url == (f"{ServiceInstantiation.base_url}/onap/so/infra/" + f"serviceInstantiation/{ServiceInstantiation.api_version}/serviceInstances") + + +def test_service_instance_aai_service_instance(): + customer_mock = mock.MagicMock() + service_instantiation = ServiceInstantiation(name="test", + request_id="test_request_id", + instance_id="test_instance_id", + sdc_service=mock.MagicMock(), + cloud_region=mock.MagicMock(), + tenant=mock.MagicMock(), + customer=customer_mock, + owning_entity=mock.MagicMock(), + project=mock.MagicMock()) + status_mock = mock.PropertyMock(return_value=ServiceInstantiation.StatusEnum.IN_PROGRESS) + type(service_instantiation).status = status_mock + with pytest.raises(StatusError): + service_instantiation.aai_service_instance + + status_mock.return_value = return_value=ServiceInstantiation.StatusEnum.COMPLETED + assert service_instantiation.aai_service_instance is not None + + customer_mock.get_service_subscription_by_service_type.side_effect = APIError + with pytest.raises(APIError) as err: + service_instantiation.aai_service_instance + assert err.type == APIError + + +@mock.patch.object(VnfInstantiation, "send_message_json") +def test_vnf_instantiation(mock_vnf_instantiation_send_message): + aai_service_instance_mock = mock.MagicMock() + aai_service_instance_mock.instance_id = "test_instance_id" + vnf_instantiation = VnfInstantiation.\ + instantiate_ala_carte(aai_service_instance=aai_service_instance_mock, + vnf_object=mock.MagicMock(), + line_of_business="test_lob", + platform="test_platform", + cloud_region=mock.MagicMock(), + tenant=mock.MagicMock(), + sdc_service=mock.MagicMock()) + assert vnf_instantiation.name.startswith("Python_ONAP_SDK_vnf_instance_") + mock_vnf_instantiation_send_message.assert_called_once() + method, _, url = mock_vnf_instantiation_send_message.call_args[0] + assert method == "POST" + assert url == (f"{VnfInstantiation.base_url}/onap/so/infra/serviceInstantiation/" + f"{VnfInstantiation.api_version}/serviceInstances/" + f"{aai_service_instance_mock.instance_id}/vnfs") + + vnf_instantiation = VnfInstantiation.\ + instantiate_ala_carte(aai_service_instance=aai_service_instance_mock, + vnf_object=mock.MagicMock(), + line_of_business="test_lob", + platform="test_platform", + vnf_instance_name="test", + cloud_region=mock.MagicMock(), + tenant=mock.MagicMock(), + sdc_service=mock.MagicMock()) + assert vnf_instantiation.name == "test" + + +@mock.patch.object(VnfInstantiation, "send_message_json") +def test_vnf_instantiation_with_cr_and_tenant(mock_vnf_instantiation_send_message): + aai_service_instance_mock = mock.MagicMock() + aai_service_instance_mock.instance_id = "test_instance_id" + vnf_instantiation = VnfInstantiation.\ + instantiate_ala_carte(aai_service_instance=aai_service_instance_mock, + vnf_object=mock.MagicMock(), + line_of_business="test_lob", + platform="test_platform", + cloud_region=mock.MagicMock(), + tenant=mock.MagicMock(), + sdc_service=mock.MagicMock()) + assert vnf_instantiation.name.startswith("Python_ONAP_SDK_vnf_instance_") + mock_vnf_instantiation_send_message.assert_called_once() + method, _, url = mock_vnf_instantiation_send_message.call_args[0] + assert method == "POST" + assert url == (f"{VnfInstantiation.base_url}/onap/so/infra/serviceInstantiation/" + f"{VnfInstantiation.api_version}/serviceInstances/" + f"{aai_service_instance_mock.instance_id}/vnfs") + + vnf_instantiation = VnfInstantiation.\ + instantiate_ala_carte(aai_service_instance=aai_service_instance_mock, + vnf_object=mock.MagicMock(), + line_of_business="test_lob", + platform="test_platform", + cloud_region=mock.MagicMock(), + tenant=mock.MagicMock(), + vnf_instance_name="test", + sdc_service=mock.MagicMock()) + assert vnf_instantiation.name == "test" + + +@mock.patch.object(VnfInstantiation, "send_message_json") +@mock.patch.object(OwningEntity, "get_by_owning_entity_id") +def test_vnf_instantiation_macro(mock_owning_entity_get, mock_vnf_instantiation_send_message): + aai_service_instance_mock = mock.MagicMock() + aai_service_instance_mock.instance_id = "test_instance_id" + + relation_1 = mock.MagicMock() + relation_1.related_to = "owning-entity" + relation_1.relationship_data = [{"relationship-value": "test"}] + relation_2 = mock.MagicMock() + relation_2.related_to = "project" + relation_2.relationship_data = [{"relationship-value": "test"}] + + aai_service_instance_mock.relationships = (item for item in [relation_1, relation_2]) + + vnf_instantiation = VnfInstantiation.\ + instantiate_macro(aai_service_instance=aai_service_instance_mock, + vnf_object=mock.MagicMock(), + line_of_business="test_lob", + platform="test_platform", + cloud_region=mock.MagicMock(), + tenant=mock.MagicMock(), + sdc_service=mock.MagicMock()) + assert vnf_instantiation.name.startswith("Python_ONAP_SDK_vnf_instance_") + mock_vnf_instantiation_send_message.assert_called_once() + method, _, url = mock_vnf_instantiation_send_message.call_args[0] + assert method == "POST" + assert url == (f"{VnfInstantiation.base_url}/onap/so/infra/serviceInstantiation/" + f"{VnfInstantiation.api_version}/serviceInstances/" + f"{aai_service_instance_mock.instance_id}/vnfs") + + vnf_instantiation = VnfInstantiation. \ + instantiate_macro(aai_service_instance=aai_service_instance_mock, + vnf_object=mock.MagicMock(), + line_of_business="test_lob", + platform="test_platform", + vnf_instance_name="test", + cloud_region=mock.MagicMock(), + tenant=mock.MagicMock(), + sdc_service=mock.MagicMock()) + assert vnf_instantiation.name == "test" + + vnf_instantiation = VnfInstantiation. \ + instantiate_macro(aai_service_instance=aai_service_instance_mock, + vnf_object=mock.MagicMock(), + line_of_business="test_lob", + platform="test_platform", + cloud_region=mock.MagicMock(), + tenant=mock.MagicMock(), + sdc_service=mock.MagicMock(), + so_vnf=mock.MagicMock()) + assert vnf_instantiation.name.startswith("Python_ONAP_SDK_service_instance_") + + so_vnf_mock = mock.MagicMock() + so_vnf_mock.instance_name = "SoVnfInstanceName" + vnf_instantiation = VnfInstantiation. \ + instantiate_macro(aai_service_instance=aai_service_instance_mock, + vnf_object=mock.MagicMock(), + line_of_business="test_lob", + platform="test_platform", + cloud_region=mock.MagicMock(), + tenant=mock.MagicMock(), + sdc_service=mock.MagicMock(), + so_vnf=so_vnf_mock) + assert vnf_instantiation.name == "SoVnfInstanceName" + + +@mock.patch.object(VnfInstantiation, "send_message_json") +@mock.patch.object(OwningEntity, "get_by_owning_entity_id") +def test_vnf_macro_so_action(mock_owning_entity_get, mock_vnf_instantiation_send_message): + + mock_sdc_service = mock.MagicMock() + with pytest.raises(StatusError): + VnfInstantiation.\ + so_action(vnf_instance=mock.MagicMock(), + operation_type=mock.MagicMock(), + aai_service_instance=mock.MagicMock(), + line_of_business=mock.MagicMock(), + platform=mock.MagicMock(), + sdc_service=mock_sdc_service, + so_service=mock.MagicMock()) + + relation_1 = mock.MagicMock() + relation_1.related_to = "owning-entity" + relation_1.relationship_data = [{"relationship-value": "test"}] + relation_2 = mock.MagicMock() + relation_2.related_to = "project" + relation_2.relationship_data = [{"relationship-value": "test"}] + + mock_aai_service_instance = mock.MagicMock() + mock_aai_service_instance.instance_id = mock.MagicMock() + mock_aai_service_instance.relationships = (item for item in [relation_1, relation_2]) + mock_aai_service_instance.service_subscription = mock.MagicMock() + mock_vnf_instance = mock.MagicMock() + mock_vnf_instance.vnf_name = "test_name_update" + mock_vnf_instance.vnf_id = "1234" + + vnf_instance_update = VnfInstantiation.\ + so_action(vnf_instance=mock_vnf_instance, + operation_type=VnfOperation.UPDATE, + aai_service_instance=mock_aai_service_instance, + line_of_business=mock.MagicMock(), + platform=mock.MagicMock(), + sdc_service=mock_sdc_service, + so_service=mock.MagicMock()) + assert vnf_instance_update.name == "test_name_update" + mock_vnf_instantiation_send_message.assert_called() + method, _, url = mock_vnf_instantiation_send_message.call_args[0] + assert method == "PUT" + assert url == (f"{ServiceInstantiation.base_url}/onap/so/infra/" + f"serviceInstantiation/{ServiceInstantiation.api_version}/serviceInstances/" + f"{mock_aai_service_instance.instance_id}/vnfs/{mock_vnf_instance.vnf_id}") + + mock_vnf_instance = mock.MagicMock() + mock_vnf_instance.vnf_name = "test_name_healthcheck" + + vnf_instance_healthcheck = VnfInstantiation. \ + so_action(vnf_instance=mock_vnf_instance, + operation_type=VnfOperation.HEALTHCHECK, + aai_service_instance=mock_aai_service_instance, + line_of_business=mock.MagicMock(), + platform=mock.MagicMock(), + sdc_service=mock_sdc_service, + so_service=mock.MagicMock()) + assert vnf_instance_healthcheck.name == "test_name_healthcheck" + mock_vnf_instantiation_send_message.assert_called() + method, _, url = mock_vnf_instantiation_send_message.call_args[0] + assert method == "POST" + assert url == (f"{ServiceInstantiation.base_url}/onap/so/infra/" + f"serviceInstantiation/{ServiceInstantiation.api_version}/serviceInstances/" + f"{mock_aai_service_instance.instance_id}/vnfs/{mock_vnf_instance.vnf_id}/healthcheck") + + +@mock.patch.object(NetworkInstantiation, "send_message_json") +@mock.patch.object(NetworkPreload, "send_message_json") +def test_network_instantiation(mock_network_preload, mock_network_instantiation_send_message): + aai_service_instance_mock = mock.MagicMock() + aai_service_instance_mock.instance_id = "test_instance_id" + vnf_instantiation = NetworkInstantiation.\ + instantiate_ala_carte(aai_service_instance=aai_service_instance_mock, + network_object=mock.MagicMock(), + line_of_business="test_lob", + platform="test_platform", + cloud_region=mock.MagicMock(), + tenant=mock.MagicMock()) + mock_network_preload.assert_called_once() + assert vnf_instantiation.name.startswith("Python_ONAP_SDK_network_instance_") + mock_network_instantiation_send_message.assert_called_once() + method, _, url = mock_network_instantiation_send_message.call_args[0] + assert method == "POST" + assert url == (f"{NetworkInstantiation.base_url}/onap/so/infra/serviceInstantiation/" + f"{NetworkInstantiation.api_version}/serviceInstances/" + f"{aai_service_instance_mock.instance_id}/networks") + + network_instantiation = NetworkInstantiation.\ + instantiate_ala_carte(aai_service_instance=aai_service_instance_mock, + network_object=mock.MagicMock(), + line_of_business="test_lob", + platform="test_platform", + network_instance_name="test", + cloud_region=mock.MagicMock(), + tenant=mock.MagicMock()) + assert mock_network_preload.call_count == 2 + assert network_instantiation.name == "test" + + +@mock.patch.object(NetworkInstantiation, "send_message_json") +@mock.patch.object(NetworkPreload, "send_message_json") +def test_network_instantiation_with_cr_and_tenant(mock_network_preload, mock_network_instantiation_send_message): + aai_service_instance_mock = mock.MagicMock() + aai_service_instance_mock.instance_id = "test_instance_id" + vnf_instantiation = NetworkInstantiation.\ + instantiate_ala_carte(aai_service_instance=aai_service_instance_mock, + network_object=mock.MagicMock(), + line_of_business=mock.MagicMock(), + platform=mock.MagicMock(), + cloud_region=mock.MagicMock(), + tenant=mock.MagicMock()) + mock_network_preload.assert_called_once() + assert vnf_instantiation.name.startswith("Python_ONAP_SDK_network_instance_") + mock_network_instantiation_send_message.assert_called_once() + method, _, url = mock_network_instantiation_send_message.call_args[0] + assert method == "POST" + assert url == (f"{NetworkInstantiation.base_url}/onap/so/infra/serviceInstantiation/" + f"{NetworkInstantiation.api_version}/serviceInstances/" + f"{aai_service_instance_mock.instance_id}/networks") + + network_instantiation = NetworkInstantiation.\ + instantiate_ala_carte(aai_service_instance=aai_service_instance_mock, + network_object=mock.MagicMock(), + line_of_business="test_lob", + platform="test_platform", + cloud_region=mock.MagicMock(), + tenant=mock.MagicMock(), + network_instance_name="test") + assert mock_network_preload.call_count == 2 + assert network_instantiation.name == "test" + +@mock.patch.object(Vid, "send_message") +@mock.patch.object(VnfInstantiation, "send_message_json") +@mock.patch("onapsdk.so.instantiation.SdcService") +def test_vnf_instantiation_get_by_vnf_instance_name(mock_sdc_service, mock_send_message_json, mock_send): + mock_sdc_service.return_value.vnfs = [] + mock_send_message_json.return_value = {} + with pytest.raises(InvalidResponse): + VnfInstantiation.get_by_vnf_instance_name("test_vnf_instance_name") + mock_send_message_json.return_value = { + "requestList": [ + { + "request": { + "requestScope": "not_vnf" + } + } + ] + } + with pytest.raises(InvalidResponse): + VnfInstantiation.get_by_vnf_instance_name("test_vnf_instance_name") + mock_send_message_json.return_value = { + "requestList": [ + { + "request": { + "requestScope": "vnf", + "requestType": "updateInstance" + } + } + ] + } + with pytest.raises(InvalidResponse): + VnfInstantiation.get_by_vnf_instance_name("test_vnf_instance_name") + mock_send_message_json.return_value = { + "requestList": [ + { + "request": { + "requestScope": "vnf", + "requestType": "createInstance" + } + } + ] + } + with pytest.raises(ResourceNotFound): + VnfInstantiation.get_by_vnf_instance_name("test_vnf_instance_name") + mock_send_message_json.return_value = { + "requestList": [ + { + "request": { + "requestScope": "vnf", + "requestType": "createInstance", + "requestDetails": { + "relatedInstanceList": [ + { + "relatedInstance": { + "modelInfo": { + "modelType": "service", + "modelName": "test_service" + } + } + } + ] + } + } + } + ] + } + with pytest.raises(ResourceNotFound): + VnfInstantiation.get_by_vnf_instance_name("test_vnf_instance_name") + mock_vnf = mock.MagicMock() + mock_vnf.name = "test_vnf_name" + mock_sdc_service.return_value.vnfs = [mock_vnf] + mock_send_message_json.return_value = { + "requestList": [ + { + "request": { + "requestScope": "vnf", + "requestType": "createInstance", + "requestDetails": { + "modelInfo": { + "modelCustomizationName": "test_fail_vnf_name" + }, + "relatedInstanceList": [ + { + "relatedInstance": { + "modelInfo": { + "modelType": "service", + "modelName": "test_service", + } + } + } + ] + } + } + } + ] + } + with pytest.raises(ResourceNotFound): + VnfInstantiation.get_by_vnf_instance_name("test_vnf_instance_name") + mock_sdc_service.return_value.vnfs = [mock_vnf] + mock_send_message_json.return_value = { + "requestList": [ + { + "request": { + "requestScope": "vnf", + "requestType": "createInstance", + "requestDetails": { + "modelInfo": { + "modelCustomizationName": "test_vnf_name" + }, + "relatedInstanceList": [ + { + "relatedInstance": { + "modelInfo": { + "modelType": "service", + "modelName": "test_service" + } + } + } + ] + } + } + } + ] + } + assert VnfInstantiation.get_by_vnf_instance_name("test_vnf_instance_name") is not None + + +@mock.patch.object(VfModuleInstantiation, "send_message_json") +@mock.patch.object(VfModulePreload, "upload_vf_module_preload") +def test_vf_module_instantiation(mock_vf_module_preload, mock_send_message_json): + mock_service_instance = mock.MagicMock() + mock_service_instance.instance_id = "1234" + mock_vnf_instance = mock.MagicMock() + mock_vnf_instance.service_instance = mock_service_instance + mock_vnf_instance.vnf_id = "4321" + instantiation = VfModuleInstantiation.\ + instantiate_ala_carte(vf_module=mock.MagicMock(), + vnf_instance=mock_vnf_instance, + cloud_region=mock.MagicMock(), + tenant=mock.MagicMock()) + assert instantiation.name.startswith("Python_ONAP_SDK_vf_module_instance_") + mock_send_message_json.assert_called_once() + method, _, url = mock_send_message_json.call_args[0] + assert method == "POST" + assert url == (f"{VfModuleInstantiation.base_url}/onap/so/infra/serviceInstantiation/" + f"{VfModuleInstantiation.api_version}/serviceInstances/1234/vnfs/" + f"4321/vfModules") + + instantiation = VfModuleInstantiation.\ + instantiate_ala_carte(vf_module=mock.MagicMock(), + vnf_instance=mock_vnf_instance, + vf_module_instance_name="test", + cloud_region=mock.MagicMock(), + tenant=mock.MagicMock()) + assert instantiation.name == "test" + + +@mock.patch.object(VfModuleInstantiation, "send_message_json") +@mock.patch.object(VfModulePreload, "upload_vf_module_preload") +def test_vf_module_instantiation_with_cr_and_tenant(mock_vf_module_preload, mock_send_message_json): + mock_service_instance = mock.MagicMock() + mock_service_instance.instance_id = "1234" + mock_vnf_instance = mock.MagicMock() + mock_vnf_instance.service_instance = mock_service_instance + mock_vnf_instance.vnf_id = "4321" + instantiation = VfModuleInstantiation.\ + instantiate_ala_carte(vf_module=mock.MagicMock(), + vnf_instance=mock_vnf_instance, + cloud_region=mock.MagicMock(), + tenant=mock.MagicMock()) + assert instantiation.name.startswith("Python_ONAP_SDK_vf_module_instance_") + mock_send_message_json.assert_called_once() + method, _, url = mock_send_message_json.call_args[0] + assert method == "POST" + assert url == (f"{VfModuleInstantiation.base_url}/onap/so/infra/serviceInstantiation/" + f"{VfModuleInstantiation.api_version}/serviceInstances/1234/vnfs/" + f"4321/vfModules") + + instantiation = VfModuleInstantiation.\ + instantiate_ala_carte(vf_module=mock.MagicMock(), + vnf_instance=mock_vnf_instance, + cloud_region=mock.MagicMock(), + tenant=mock.MagicMock(), + vf_module_instance_name="test") + assert instantiation.name == "test" + + +def test_instantiation_wait_for_finish(): + with mock.patch.object(ServiceInstantiation, "finished", new_callable=mock.PropertyMock) as mock_finished: + with mock.patch.object(ServiceInstantiation, "completed", new_callable=mock.PropertyMock) as mock_completed: + instantiation = ServiceInstantiation( + name="test", + request_id="test", + instance_id="test", + sdc_service=mock.MagicMock(), + cloud_region=mock.MagicMock(), + tenant=mock.MagicMock(), + customer=mock.MagicMock(), + owning_entity=mock.MagicMock(), + project=mock.MagicMock() + ) + instantiation.WAIT_FOR_SLEEP_TIME = 0 + mock_finished.side_effect = [False, False, True] + mock_completed.return_value = True + rv = namedtuple("Value", ["return_value"]) + instantiation._wait_for_finish(rv) + assert rv.return_value + +@mock.patch.object(ServiceInstantiation, "send_message_json") +def test_service_instantiation_multicloud(mock_send_message_json): + + mock_sdc_service = mock.MagicMock() + mock_sdc_service.distributed = True + _ = ServiceInstantiation.\ + instantiate_ala_carte(sdc_service=mock_sdc_service, + cloud_region=mock.MagicMock(), + tenant=mock.MagicMock(), + customer=mock.MagicMock(), + owning_entity=mock.MagicMock(), + project=mock.MagicMock(), + service_subscription=mock.MagicMock()) + _, kwargs = mock_send_message_json.call_args + data = json.loads(kwargs["data"]) + assert data["requestDetails"]["requestParameters"]["userParams"] == [] + mock_send_message_json.reset_mock() + + _ = ServiceInstantiation.\ + instantiate_ala_carte(sdc_service=mock_sdc_service, + cloud_region=mock.MagicMock(), + tenant=mock.MagicMock(), + customer=mock.MagicMock(), + owning_entity=mock.MagicMock(), + project=mock.MagicMock(), + enable_multicloud=True, + service_subscription=mock.MagicMock()) + _, kwargs = mock_send_message_json.call_args + data = json.loads(kwargs["data"]) + assert data["requestDetails"]["requestParameters"]["userParams"] == [{"name": "orchestrator", "value": "multicloud"}] + mock_send_message_json.reset_mock() + + _ = ServiceInstantiation.\ + instantiate_macro(sdc_service=mock_sdc_service, + cloud_region=mock.MagicMock(), + tenant=mock.MagicMock(), + customer=mock.MagicMock(), + owning_entity=mock.MagicMock(), + project=mock.MagicMock(), + line_of_business=mock.MagicMock(), + platform=mock.MagicMock(), + service_instance_name="test", + service_subscription=mock.MagicMock()) + _, kwargs = mock_send_message_json.call_args + data = json.loads(kwargs["data"]) + assert not any(filter(lambda x: x == {"name": "orchestrator", "value": "multicloud"}, data["requestDetails"]["requestParameters"]["userParams"])) + mock_send_message_json.reset_mock() + + _ = ServiceInstantiation.\ + instantiate_macro(sdc_service=mock_sdc_service, + cloud_region=mock.MagicMock(), + tenant=mock.MagicMock(), + customer=mock.MagicMock(), + owning_entity=mock.MagicMock(), + project=mock.MagicMock(), + line_of_business=mock.MagicMock(), + platform=mock.MagicMock(), + service_instance_name="test", + enable_multicloud=True, + service_subscription=mock.MagicMock()) + _, kwargs = mock_send_message_json.call_args + data = json.loads(kwargs["data"]) + assert any(filter(lambda x: x == {"name": "orchestrator", "value": "multicloud"}, data["requestDetails"]["requestParameters"]["userParams"])) + + +@mock.patch.object(ServiceInstantiation, "send_message_json") +def test_service_instantiation_so_service(mock_send_message_json): + mock_sdc_service = mock.MagicMock() + mock_sdc_service.distributed = True + + so_service = SoService( + subscription_service_type="test_so_service", + vnfs=[ + SoServiceVnf( + model_name="test_so_service_vnf_model_name_1", + instance_name="test_so_service_vnf_instance_name_1", + parameters={ + "param_1": "param_1_value", + "param_2": "param_2_value" + } + ), + SoServiceVnf( + model_name="test_so_service_vnf_model_name_2", + instance_name="test_so_service_vnf_instance_name_2", + vf_modules=[ + SoServiceVfModule( + model_name="test_so_service_vf_module_model_name_1", + instance_name="test_so_service_vf_module_instance_name_1", + parameters={ + "vf_module_param_1": "vf_module_param_1_value", + "vf_module_param_2": "vf_module_param_2_value" + } + ), + SoServiceVfModule( + model_name="test_so_service_vf_module_model_name_2", + instance_name="test_so_service_vf_module_instance_name_2", + parameters={ + "vf_module_param_1": "vf_module_param_1_value", + "vf_module_param_2": "vf_module_param_2_value" + } + ), + ] + ) + ], + pnfs=[ + SoServicePnf( + model_name="test_so_service_pnf_model_name_1", + instance_name="test_so_service_pnf_instance_name_1" + ), + SoServicePnf( + model_name="test_so_service_pnf_model_name_2", + instance_name="test_so_service_pnf_instance_name_2" + ) + ] + ) + + _ = ServiceInstantiation.\ + instantiate_macro(sdc_service=mock_sdc_service, + cloud_region=mock.MagicMock(), + tenant=mock.MagicMock(), + customer=mock.MagicMock(), + owning_entity=mock.MagicMock(), + project=mock.MagicMock(), + line_of_business=mock.MagicMock(), + platform=mock.MagicMock(), + service_instance_name="test", + so_service=so_service) + _, kwargs = mock_send_message_json.call_args + data = json.loads(kwargs["data"]) + assert data["requestDetails"]["requestParameters"]["subscriptionServiceType"] == "test_so_service" + assert len(data["requestDetails"]["requestParameters"]["userParams"][1]["service"]["resources"]["vnfs"]) == 2 + assert len(data["requestDetails"]["requestParameters"]["userParams"][1]["service"]["resources"]["pnfs"]) == 2 + vnf_1_data = data["requestDetails"]["requestParameters"]["userParams"][1]["service"]["resources"]["vnfs"][0] + vnf_2_data = data["requestDetails"]["requestParameters"]["userParams"][1]["service"]["resources"]["vnfs"][1] + pnf_1_data = data["requestDetails"]["requestParameters"]["userParams"][1]["service"]["resources"]["pnfs"][0] + pnf_2_data = data["requestDetails"]["requestParameters"]["userParams"][1]["service"]["resources"]["pnfs"][1] + + assert vnf_1_data["instanceName"] == "test_so_service_vnf_instance_name_1" + assert len(vnf_1_data["instanceParams"][0]) == 2 + assert vnf_1_data["instanceParams"][0]["param_1"] == "param_1_value" + assert vnf_1_data["instanceParams"][0]["param_2"] == "param_2_value" + assert len(vnf_1_data["vfModules"]) == 0 + + assert vnf_2_data["instanceName"] == "test_so_service_vnf_instance_name_2" + assert len(vnf_2_data["instanceParams"][0]) == 0 + assert len(vnf_2_data["vfModules"]) == 2 + vf_module_1_data = vnf_2_data["vfModules"][0] + vf_module_2_data = vnf_2_data["vfModules"][1] + + assert vf_module_1_data["instanceName"] == "test_so_service_vf_module_instance_name_1" + assert len(vf_module_1_data["instanceParams"][0]) == 2 + assert vf_module_1_data["instanceParams"][0]["vf_module_param_1"] == "vf_module_param_1_value" + assert vf_module_1_data["instanceParams"][0]["vf_module_param_2"] == "vf_module_param_2_value" + + assert vf_module_2_data["instanceName"] == "test_so_service_vf_module_instance_name_2" + assert len(vf_module_2_data["instanceParams"][0]) == 2 + assert vf_module_2_data["instanceParams"][0]["vf_module_param_1"] == "vf_module_param_1_value" + assert vf_module_2_data["instanceParams"][0]["vf_module_param_2"] == "vf_module_param_2_value" + + assert pnf_1_data["instanceName"] == "test_so_service_pnf_instance_name_1" + + assert pnf_2_data["instanceName"] == "test_so_service_pnf_instance_name_2" + + +def test_so_service_load_from_yaml(): + + so_service_yaml = """ + subscription_service_type: myservice + vnfs: + - model_name: myvfmodel + instance_name: myfirstvnf + parameters: + param1: value1 + processing_priority: 1 + vf_modules: + - instance_name: mysecondvfm + model_name: base + processing_priority: 2 + parameters: + param-vfm1: value-vfm1 + - instance_name: myfirstvfm + model_name: base + processing_priority: 1 + parameters: + param-vfm1: value-vfm1 + - model_name: myvfmodel + instance_name: mysecondvnf + parameters: + param1: value1 + processing_priority: 2 + vf_modules: + - instance_name: myfirstvfm + model_name: base + processing_priority: 1 + parameters: + param-vfm1: value-vfm1 + - instance_name: mysecondvfm + model_name: base + processing_priority: 2 + parameters: + param-vfm1: value-vfm1 + pnfs: + - model_name: mypnfmodel + instance_name: myfirstpnf + """ + so_service = SoService.load(yaml.safe_load(so_service_yaml)) + assert so_service.subscription_service_type == "myservice" + assert not so_service.instance_name + assert len(so_service.vnfs) == 2 + assert len(so_service.pnfs) == 1 + + so_service_vnf_1 = so_service.vnfs[0] + so_service_vnf_2 = so_service.vnfs[1] + so_service_pnf = so_service.pnfs[0] + + assert so_service_vnf_1.model_name == "myvfmodel" + assert so_service_vnf_1.instance_name == "myfirstvnf" + assert so_service_vnf_1.processing_priority == 1 + assert len(so_service_vnf_1.parameters) == 1 + assert so_service_vnf_1.parameters["param1"] == "value1" + assert len(so_service_vnf_1.vf_modules) == 2 + + so_service_vnf_1_vf_module_1 = so_service_vnf_1.vf_modules[0] + so_service_vnf_1_vf_module_2 = so_service_vnf_1.vf_modules[1] + + assert so_service_vnf_1_vf_module_1.model_name == "base" + assert so_service_vnf_1_vf_module_1.instance_name == "mysecondvfm" + assert so_service_vnf_1_vf_module_1.processing_priority == 2 + assert len(so_service_vnf_1_vf_module_1.parameters) == 1 + assert so_service_vnf_1_vf_module_1.parameters["param-vfm1"] == "value-vfm1" + assert so_service_vnf_1_vf_module_2.model_name == "base" + assert so_service_vnf_1_vf_module_2.instance_name == "myfirstvfm" + assert so_service_vnf_1_vf_module_2.processing_priority == 1 + assert len(so_service_vnf_1_vf_module_2.parameters) == 1 + assert so_service_vnf_1_vf_module_2.parameters["param-vfm1"] == "value-vfm1" + + assert so_service_vnf_2.model_name == "myvfmodel" + assert so_service_vnf_2.instance_name == "mysecondvnf" + assert so_service_vnf_2.processing_priority == 2 + assert len(so_service_vnf_2.parameters) == 1 + assert so_service_vnf_2.parameters["param1"] == "value1" + assert len(so_service_vnf_2.vf_modules) == 2 + + so_service_vnf_1_vf_module_1 = so_service_vnf_2.vf_modules[0] + so_service_vnf_1_vf_module_2 = so_service_vnf_2.vf_modules[1] + + assert so_service_vnf_1_vf_module_1.model_name == "base" + assert so_service_vnf_1_vf_module_1.instance_name == "myfirstvfm" + assert so_service_vnf_1_vf_module_1.processing_priority == 1 + assert len(so_service_vnf_1_vf_module_1.parameters) == 1 + assert so_service_vnf_1_vf_module_1.parameters["param-vfm1"] == "value-vfm1" + assert so_service_vnf_1_vf_module_2.model_name == "base" + assert so_service_vnf_1_vf_module_2.instance_name == "mysecondvfm" + assert so_service_vnf_1_vf_module_2.processing_priority == 2 + assert len(so_service_vnf_1_vf_module_2.parameters) == 1 + assert so_service_vnf_1_vf_module_2.parameters["param-vfm1"] == "value-vfm1" + + assert so_service_pnf.model_name == "mypnfmodel" + assert so_service_pnf.instance_name == "myfirstpnf" + + +def test_so_service_load_from_file(): + with Path(Path(__file__).parent, "data/test_so_service_data.yaml").open() as yaml_template: + so_service_data = yaml.safe_load(yaml_template) + service = Service(next(iter(so_service_data.keys()))) + so_service = SoService.load(so_service_data[service.name]) + assert so_service.subscription_service_type == "myservice" + assert not so_service.instance_name + assert len(so_service.vnfs) == 2 + + so_service_vnf_1 = so_service.vnfs[0] + so_service_vnf_2 = so_service.vnfs[1] + + assert so_service_vnf_1.model_name == "myvfmodel" + assert so_service_vnf_1.instance_name == "myfirstvnf" + assert so_service_vnf_1.processing_priority == 1 + assert len(so_service_vnf_1.parameters) == 1 + assert so_service_vnf_1.parameters["param1"] == "value1" + assert len(so_service_vnf_1.vf_modules) == 2 + + so_service_vnf_1_vf_module_1 = so_service_vnf_1.vf_modules[0] + so_service_vnf_1_vf_module_2 = so_service_vnf_1.vf_modules[1] + + assert so_service_vnf_1_vf_module_1.model_name == "base" + assert so_service_vnf_1_vf_module_1.instance_name == "mysecondvfm" + assert so_service_vnf_1_vf_module_1.processing_priority == 2 + assert len(so_service_vnf_1_vf_module_1.parameters) == 1 + assert so_service_vnf_1_vf_module_1.parameters["param-vfm1"] == "value-vfm1" + assert so_service_vnf_1_vf_module_2.model_name == "base" + assert so_service_vnf_1_vf_module_2.instance_name == "myfirstvfm" + assert so_service_vnf_1_vf_module_2.processing_priority == 1 + assert len(so_service_vnf_1_vf_module_2.parameters) == 1 + assert so_service_vnf_1_vf_module_2.parameters["param-vfm1"] == "value-vfm1" + + assert so_service_vnf_2.model_name == "myvfmodel" + assert so_service_vnf_2.instance_name == "mysecondvnf" + assert so_service_vnf_2.processing_priority == 2 + assert len(so_service_vnf_2.parameters) == 1 + assert so_service_vnf_2.parameters["param1"] == "value1" + assert len(so_service_vnf_2.vf_modules) == 2 + + so_service_vnf_1_vf_module_1 = so_service_vnf_2.vf_modules[0] + so_service_vnf_1_vf_module_2 = so_service_vnf_2.vf_modules[1] + + assert so_service_vnf_1_vf_module_1.model_name == "base" + assert so_service_vnf_1_vf_module_1.instance_name == "myfirstvfm" + assert so_service_vnf_1_vf_module_1.processing_priority == 1 + assert len(so_service_vnf_1_vf_module_1.parameters) == 1 + assert so_service_vnf_1_vf_module_1.parameters["param-vfm1"] == "value-vfm1" + assert so_service_vnf_1_vf_module_2.model_name == "base" + assert so_service_vnf_1_vf_module_2.instance_name == "mysecondvfm" + assert so_service_vnf_1_vf_module_2.processing_priority == 2 + assert len(so_service_vnf_1_vf_module_2.parameters) == 1 + assert so_service_vnf_1_vf_module_2.parameters["param-vfm1"] == "value-vfm1" + + +def test_so_service_vnf_load_from_yaml(): + + so_vnf_yaml = """ + model_name: myvnfmodel + instance_name: mynewvnf + parameters: + param1: value1 + vf_modules: + - instance_name: myfirstvfm + model_name: base + processing_priority: 1 + parameters: + param-vfm1: value-vfm1 + - instance_name: mysecondvfm + model_name: second_base + processing_priority: 2 + parameters: + param-vfm2: value-vfm2 + param-vfm3: value-vfm3 + """ + + so_vnf = SoServiceVnf.load(yaml.safe_load(so_vnf_yaml)) + assert so_vnf.model_name == "myvnfmodel" + assert so_vnf.instance_name == "mynewvnf" + + assert len(so_vnf.parameters) == 1 + assert so_vnf.parameters["param1"] == "value1" + + assert len(so_vnf.vf_modules) == 2 + so_vnf_vf_module_1 = so_vnf.vf_modules[0] + so_vnf_vf_module_2 = so_vnf.vf_modules[1] + + assert so_vnf_vf_module_1.model_name == "base" + assert so_vnf_vf_module_1.instance_name == "myfirstvfm" + assert so_vnf_vf_module_1.processing_priority == 1 + assert len(so_vnf_vf_module_1.parameters) == 1 + assert so_vnf_vf_module_1.parameters["param-vfm1"] == "value-vfm1" + + assert so_vnf_vf_module_2.model_name == "second_base" + assert so_vnf_vf_module_2.instance_name == "mysecondvfm" + assert so_vnf_vf_module_2.processing_priority == 2 + assert len(so_vnf_vf_module_2.parameters) == 2 + assert so_vnf_vf_module_2.parameters["param-vfm2"] == "value-vfm2" + assert so_vnf_vf_module_2.parameters["param-vfm3"] == "value-vfm3" diff --git a/tests/test_so_orchestration_request.py b/tests/test_so_orchestration_request.py new file mode 100644 index 0000000..c93189b --- /dev/null +++ b/tests/test_so_orchestration_request.py @@ -0,0 +1,103 @@ +# Copyright 2022 Orange, 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 unittest import mock + +from onapsdk.so.so_element import OrchestrationRequest, SoElement +from onapsdk.utils.headers_creator import headers_so_creator +from onapsdk.onap_service import OnapService + + +IN_PROGRESS = { + "request": { + "requestStatus": { + "requestState": "IN_PROGRESS" + } + } +} +FAILED = { + "request": { + "requestStatus": { + "requestState": "FAILED" + } + } +} +COMPLETE = { + "request": { + "requestStatus": { + "requestState": "COMPLETE" + } + } +} +UNKNOWN = { + "request": { + "requestStatus": { + "requestState": "INVALID" + } + } +} +BAD_RESPONSE = {} + + +@mock.patch.object(OrchestrationRequest, "send_message_json") +def test_orchestration_request_status(mock_send_message): + orchestration_req = OrchestrationRequest(request_id="test") + + mock_send_message.return_value = BAD_RESPONSE + assert orchestration_req.status == OrchestrationRequest.StatusEnum.UNKNOWN + + mock_send_message.return_value = UNKNOWN + assert orchestration_req.status == OrchestrationRequest.StatusEnum.UNKNOWN + + mock_send_message.return_value = FAILED + assert orchestration_req.status == OrchestrationRequest.StatusEnum.FAILED + + mock_send_message.return_value = COMPLETE + assert orchestration_req.status == OrchestrationRequest.StatusEnum.COMPLETED + + mock_send_message.return_value = IN_PROGRESS + assert orchestration_req.status == OrchestrationRequest.StatusEnum.IN_PROGRESS + assert not orchestration_req.finished + assert not orchestration_req.completed + assert not orchestration_req.failed + + mock_send_message.return_value = COMPLETE + assert orchestration_req.finished + assert orchestration_req.completed + assert not orchestration_req.failed + + mock_send_message.return_value = FAILED + assert orchestration_req.finished + assert not orchestration_req.completed + assert orchestration_req.failed + + +#Test the Class SoElement +def test_SoElement_headers(): + """Test the header property""" + element = SoElement() + assert element.headers != headers_so_creator(OnapService.headers) + #check x-transactionid for headers + + +def test_get_subscription_service_type(): + """Test SO Element class method""" + vf_object_name = SoElement.get_subscription_service_type("vf_name") + assert vf_object_name == "vf_name" + + +def test_base_create_url(): + """Test base create url class method""" + assert SoElement._base_create_url() == "{}/onap/so/infra/serviceInstantiation/{}/serviceInstances".\ + format(SoElement.base_url, SoElement.api_version) + diff --git a/tests/test_sp_partner.py b/tests/test_sp_partner.py new file mode 100644 index 0000000..73a4839 --- /dev/null +++ b/tests/test_sp_partner.py @@ -0,0 +1,84 @@ +# Copyright 2022 Orange, 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 unittest import mock + +from onapsdk.aai.business import SpPartner +from onapsdk.exceptions import ResourceNotFound + + +SP_PARTNERS = { + "sp-partner":[ + { + "sp-partner-id":"ff6c945f-89ab-4f14-bafd-0cdd6eac791a", + "url":"http://127.0.0.1", + "resource-version":"1588244348931", + }, + { + "sp-partner-id":"OE-generic", + "callsource":"test-callsource", + "resource-version":"1587388597761" + }, + { + "sp-partner-id":"b3dcdbb0-edae-4384-b91e-2f114472520c", + "url":"http://127.0.0.1", + "callsource":"test-callsource", + "operational-status":"test-operational-status", + "model-customization-id":"test-model-customization-id", + "model-invariant-id":"test-model-invariant-id", + "model-version-id":"test-model-version-id", + "resource-version":"1588145971158" + } + ] +} + + +SP_PARTNER = { + "sp-partner-id":"blablabla", + "url":"http://127.0.0.1", + "callsource":"test-callsource", + "resource-version":"1587388597761" +} + + +@mock.patch.object(SpPartner, "send_message_json") +def test_sp_partner_get_all(mock_send): + mock_send.return_value = SP_PARTNERS + owning_entities = list(SpPartner.get_all()) + assert len(owning_entities) == 3 + sp_partner = owning_entities[0] + assert sp_partner.sp_partner_id == "ff6c945f-89ab-4f14-bafd-0cdd6eac791a" + assert sp_partner.sp_partner_url == "http://127.0.0.1" + assert sp_partner.url == (f"{sp_partner.base_url}{sp_partner.api_version}/" + "business/sp-partners/sp-partner/" + f"{sp_partner.sp_partner_id}") + + +@mock.patch.object(SpPartner, "send_message_json") +def test_sp_partner_get_by_sp_partner_id(mock_send): + mock_send.return_value = SP_PARTNER + sp_partner = SpPartner.get_by_sp_partner_id("blablabla") + assert sp_partner.sp_partner_id == "blablabla" + + +@mock.patch.object(SpPartner, "send_message") +@mock.patch.object(SpPartner, "get_by_sp_partner_id") +def test_sp_partner_create(_, mock_send): + + SpPartner.create( + sp_partner_id="123" + ) + mock_send.assert_called_once_with("PUT", + "Declare A&AI sp partner", + "https://aai.api.sparky.simpledemo.onap.org:30233/aai/v23/business/sp-partners/sp-partner/123", + data='{\n "sp-partner-id": "123"\n \n \n \n \n \n \n}') diff --git a/tests/test_subnet.py b/tests/test_subnet.py new file mode 100644 index 0000000..cc7eced --- /dev/null +++ b/tests/test_subnet.py @@ -0,0 +1,49 @@ +# Copyright 2022 Orange, 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 pytest +from onapsdk.exceptions import ParameterError + +from onapsdk.so.instantiation import Subnet + + +def test_dhcp_subnet(): + with pytest.raises(ParameterError): + Subnet(name="test", + role="test", + start_address="192.168.8.0", + gateway_address="192.168.8.1", + dhcp_enabled="sss" + ) + with pytest.raises(ParameterError): + Subnet(name="test", + role="test", + start_address="192.168.8.0", + gateway_address="192.168.8.1", + dhcp_enabled="Y" + ) + subnet = Subnet(name="test", + role="test", + start_address="192.168.8.0", + gateway_address="192.168.8.1", + dhcp_enabled="Y", + dhcp_start_address="10.8.1.0", + dhcp_end_address="10.8.1.1" + ) + assert subnet.name == "test" + assert subnet.role == "test" + assert subnet.start_address == "192.168.8.0" + assert subnet.gateway_address == "192.168.8.1" + assert subnet.dhcp_enabled == "Y" + assert subnet.dhcp_start_address == "10.8.1.0" + assert subnet.dhcp_end_address == "10.8.1.1" diff --git a/tests/test_tosca_file_handler.py b/tests/test_tosca_file_handler.py new file mode 100644 index 0000000..e288693 --- /dev/null +++ b/tests/test_tosca_file_handler.py @@ -0,0 +1,88 @@ +# Copyright 2022 Orange, 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 +import json +import oyaml as yaml +import os +import os.path +import unittest + +from onapsdk.exceptions import ValidationError +import onapsdk.utils.tosca_file_handler as tosca_file_handler + + +__author__ = "Morgan Richomme <morgan.richomme@orange.com>" + + +class ToscaFileHandlerTestingBase(unittest.TestCase): + + """The super class which testing classes could inherit.""" + + logging.disable(logging.CRITICAL) + + _root_path = os.getcwd().rsplit('/onapsdk')[0] + _foo_path = _root_path +"/tests/data/service-Ubuntu16-template.yml" + + + def setUp(self): + pass + + def test_get_parameter_from_yaml(self): + with open(self._foo_path) as f: + model = json.dumps(yaml.safe_load(f)) + param = tosca_file_handler.get_parameter_from_yaml( + "metadata", model) + self.assertEqual(param['name'], "ubuntu16") + + def test_get_wrong_parameter_from_yaml(self): + with open(self._foo_path) as f: + model = json.dumps(yaml.safe_load(f)) + with self.assertRaises(ValidationError): + tosca_file_handler.get_parameter_from_yaml( + "wrong_parameter", model) + + def test_get_parameter_from_wrong_yaml(self): + with self.assertRaises(FileNotFoundError): + with open("wrong_path") as f: + model = json.dumps(yaml.safe_load(f)) + tosca_file_handler.get_parameter_from_yaml( + "metadata", model) + + def test_get_random_string_generator(self): + self.assertEqual( + len(tosca_file_handler.random_string_generator()), 6) + + def test_get_vf_list_from_tosca_file(self): + with open(self._foo_path) as f: + model = json.dumps(yaml.safe_load(f)) + vf_list = tosca_file_handler.get_vf_list_from_tosca_file(model) + self.assertEqual(vf_list[0], 'ubuntu16_VF') + + def test_get_modules_list_from_tosca_file(self): + with open(self._foo_path) as f: + model = json.dumps(yaml.safe_load(f)) + vf_modules = tosca_file_handler.get_modules_list_from_tosca_file(model) + self.assertEqual(len(vf_modules), 1) + + # def get_vf_list_from_tosca_file_wrong_model(self): + # with self.assertRaises(FileNotFoundError): + # tosca_file_handler.get_vf_list_from_tosca_file( + # self._root_path + "wrong_path") + +if __name__ == "__main__": + # logging must be disabled else it calls time.time() + # what will break these unit tests. + logging.disable(logging.CRITICAL) + unittest.main(verbosity=2) diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..62bffde --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,46 @@ +# Copyright 2022 Orange, 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 os + +import pytest +import time + +from onapsdk.onap_service import OnapService +from onapsdk.utils.mixins import WaitForFinishMixin +from onapsdk.utils import load_json_file + + +class TestWaitForFinish(WaitForFinishMixin, OnapService): + + @property + def completed(self): + return True + + @property + def finished(self): + time.sleep(0.1) + return True + + +def test_wait_for_finish_timeout(): + t = TestWaitForFinish() + with pytest.raises(TimeoutError): + t.wait_for_finish(timeout=0.01) + t.wait_for_finish() + + +def test_load_json_file(): + path_to_event: str = os.path.join(os.getcwd(), "tests/data/utils_load_json_file_test.json") + test_json: str = load_json_file(path_to_event) + assert test_json == '{"event": {"test1": "val1"}}' diff --git a/tests/test_vendor.py b/tests/test_vendor.py new file mode 100644 index 0000000..78c4bad --- /dev/null +++ b/tests/test_vendor.py @@ -0,0 +1,316 @@ +"""Test vendor module.""" +# Copyright 2022 Orange, 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 unittest import mock + +import pytest +from onapsdk.exceptions import RequestError + +from onapsdk.sdc.vendor import Vendor +import onapsdk.constants as const +from onapsdk.sdc.sdc_element import SdcElement + +@mock.patch.object(Vendor, 'send_message_json') +def test_get_all_no_vendors(mock_send): + """Returns empty array if no vendors.""" + mock_send.return_value = {} + assert Vendor.get_all() == [] + mock_send.assert_called_once_with("GET", 'get Vendors', 'https://sdc.api.fe.simpledemo.onap.org:30207/sdc1/feProxy/onboarding-api/v1.0/vendor-license-models') + +@mock.patch.object(Vendor, 'send_message_json') +def test_get_all_some_vendors(mock_send): + """Returns a list of vendors.""" + mock_send.return_value = {'results':[ + {'name': 'one', 'id': '1234'}, + {'name': 'two', 'id': '1235'}]} + assert len(Vendor.get_all()) == 2 + vendor_1 = Vendor.get_all()[0] + assert vendor_1.name == "one" + assert vendor_1.identifier == "1234" + assert vendor_1.created() == True + vendor_2 = Vendor.get_all()[1] + assert vendor_2.name == "two" + assert vendor_2.identifier == "1235" + assert vendor_2.created() + mock_send.assert_called_with("GET", 'get Vendors', 'https://sdc.api.fe.simpledemo.onap.org:30207/sdc1/feProxy/onboarding-api/v1.0/vendor-license-models') + +@mock.patch.object(Vendor, 'exists') +def test_init_no_name(mock_exists): + """Check init with no names.""" + mock_exists.return_value = False + vendor = Vendor() + assert isinstance(vendor, SdcElement) + assert vendor._identifier == None + assert vendor._version == None + assert vendor.name == "Generic-Vendor" + assert vendor.created() == False + assert vendor.headers["USER_ID"] == "cs0008" + assert isinstance(vendor._base_url(), str) + assert "sdc1/feProxy/onboarding-api/v1.0" in vendor._base_url() + +def test_init_with_name(): + """Check init with no names.""" + vendor = Vendor(name="YOLO") + assert vendor._identifier == None + assert vendor._version == None + assert vendor.name == "YOLO" + assert vendor.headers["USER_ID"] == "cs0008" + assert isinstance(vendor._base_url(), str) + assert "sdc1/feProxy/onboarding-api/v1.0" in vendor._base_url() + +def test_equality_really_equals(): + """Check two Vendors are equals if name is the same.""" + vendor_1 = Vendor(name="equal") + vendor_1.identifier = "1234" + vendor_2 = Vendor(name="equal") + vendor_2.identifier = "1235" + assert vendor_1 == vendor_2 + +def test_equality_not_equals(): + """Check two Vendors are not equals if name is not the same.""" + vendor_1 = Vendor(name="equal") + vendor_1.identifier = "1234" + vendor_2 = Vendor(name="not_equal") + vendor_2.identifier = "1234" + assert vendor_1 != vendor_2 + +def test_equality_not_equals_not_same_object(): + """Check a Vendor and something different are not equals.""" + vendor_1 = Vendor(name="equal") + vendor_1.identifier = "1234" + vendor_2 = "equal" + assert vendor_1 != vendor_2 + +@mock.patch.object(Vendor, 'get_all') +def test_exists_not_exists(mock_get_all): + """Return False if vendor doesn't exist in SDC.""" + vendor_1 = Vendor(name="one") + vendor_1.identifier = "1234" + mock_get_all.return_value = [vendor_1] + vendor = Vendor(name="two") + assert not vendor.exists() + +@mock.patch.object(Vendor, 'get_all') +def test_exists_exists(mock_get_all): + """Return True if vendor exists in SDC.""" + vendor_1 = Vendor(name="one") + vendor_1.identifier = "1234" + vendor_1.version = "1.1" + mock_get_all.return_value = [vendor_1] + vendor = Vendor(name="one") + assert vendor.exists() + +@mock.patch.object(Vendor, 'get_all') +@mock.patch.object(Vendor, 'send_message_json') +def test_load_created(mock_send, mock_get_all): + mock_send.return_value = {'results': + [{'status': 'state_one', 'id': "5678", "name": "1.0"}], "listCount": 1} + vendor = Vendor(name="one") + vendor.identifier = "1234" + vendor.load() + mock_get_all.assert_not_called() + mock_send.assert_called_once_with('GET', 'get item', 'https://sdc.api.fe.simpledemo.onap.org:30207/sdc1/feProxy/onboarding-api/v1.0/items/1234/versions') + assert vendor.status == "state_one" + assert vendor.version == "5678" + +@mock.patch.object(Vendor, 'get_all') +@mock.patch.object(Vendor, 'send_message_json') +def test_load_not_created(mock_send, mock_get_all): + mock_send.return_value = {'results': + [{'status': 'state_one', 'id': "5678", "name": "1.0"}], "listCount": 1} + vendor = Vendor(name="one") + vendor.load() + mock_get_all.return_value = [] + mock_send.assert_not_called() + assert vendor._status == None + assert vendor.version == None + assert vendor._identifier == None + +@mock.patch.object(Vendor, 'exists') +@mock.patch.object(Vendor, 'send_message_json') +def test_create_already_exists(mock_send, mock_exists): + """Do nothing if already created in SDC.""" + vendor = Vendor() + mock_exists.return_value = True + vendor.create() + mock_send.assert_not_called() + +@mock.patch.object(Vendor, 'exists') +@mock.patch.object(Vendor, 'send_message_json') +def test_create_issue_in_creation(mock_send, mock_exists): + """Do nothing if not created but issue during creation.""" + vendor = Vendor() + expected_data = '{\n "iconRef": "icon",\n "vendorName": "Generic-Vendor",\n "description": "vendor"\n}' + mock_exists.return_value = False + mock_send.side_effect = RequestError + with pytest.raises(RequestError) as exc: + vendor.create() + mock_send.assert_called_once_with("POST", "create Vendor", mock.ANY, data=expected_data) + assert vendor.created() == False + +@mock.patch.object(Vendor, 'exists') +@mock.patch.object(Vendor, 'send_message_json') +def test_create_OK(mock_send, mock_exists): + """Create and update object.""" + vendor = Vendor() + expected_data = '{\n "iconRef": "icon",\n "vendorName": "Generic-Vendor",\n "description": "vendor"\n}' + mock_exists.return_value = False + mock_send.return_value = { + 'itemId': "1234", + 'version': {'id': "5678", 'status': 'state_created'}} + vendor.create() + mock_send.assert_called_once_with("POST", "create Vendor", mock.ANY, data=expected_data) + assert vendor.status == const.DRAFT + assert vendor.identifier == "1234" + assert vendor.version == "5678" + +@mock.patch.object(Vendor, 'exists') +@mock.patch.object(Vendor, 'load') +@mock.patch.object(Vendor, 'send_message') +def test_submit_already_certified(mock_send, mock_load, mock_exists): + """Do nothing if already certified.""" + mock_exists.return_value = True + vendor = Vendor() + vendor._status = const.CERTIFIED + vendor.submit() + mock_send.assert_not_called() + +@mock.patch.object(Vendor, 'exists') +@mock.patch.object(Vendor, 'load') +@mock.patch.object(Vendor, 'send_message') +def test_submit_not_created(mock_send, mock_load, mock_exists): + """Do nothing if not created.""" + mock_exists.return_value = False + vendor = Vendor() + vendor.submit() + mock_send.assert_not_called() + +@mock.patch.object(Vendor, 'exists') +@mock.patch.object(Vendor, 'load') +@mock.patch.object(Vendor, 'send_message') +def test_submit_certified_NOK(mock_send, mock_load, mock_exists): + """Don't update status if submission NOK.""" + mock_exists.return_value = True + vendor = Vendor() + vendor._identifier = "12345" + mock_send.side_effect = RequestError + expected_data = '{\n\n "action": "Submit"\n}' + vendor._status = "Draft" + vendor._version = "1234" + with pytest.raises(RequestError) as err: + vendor.submit() + assert err.type == RequestError + mock_send.assert_called_once_with("PUT", "Submit Vendor", 'https://sdc.api.fe.simpledemo.onap.org:30207/sdc1/feProxy/onboarding-api/v1.0/vendor-license-models/12345/versions/1234/actions', data=expected_data) + assert vendor._status != const.CERTIFIED + +@mock.patch.object(Vendor, 'exists') +@mock.patch.object(Vendor, 'load') +@mock.patch.object(Vendor, 'send_message') +def test_submit_certified_OK(mock_send, mock_load, mock_exists): + """Set status to CERTIFIED if submission OK.""" + mock_exists.return_value = True + vendor = Vendor() + vendor._status = "Draft" + vendor._version = "1234" + vendor.identifier = "12345" + mock_send.return_value = mock.Mock() + expected_data = '{\n\n "action": "Submit"\n}' + vendor.submit() + mock_send.assert_called_once_with("PUT", "Submit Vendor", 'https://sdc.api.fe.simpledemo.onap.org:30207/sdc1/feProxy/onboarding-api/v1.0/vendor-license-models/12345/versions/1234/actions', data=expected_data) + assert vendor.status == const.CERTIFIED + +@mock.patch.object(Vendor, 'created') +@mock.patch.object(Vendor, 'load') +def test_version_no_load_no_created(mock_load, mock_created): + mock_created.return_value = False + vendor = Vendor() + assert vendor.version == None + mock_load.assert_not_called() + +@mock.patch.object(Vendor, 'created') +@mock.patch.object(Vendor, 'load') +def test_version_no_load_created(mock_load, mock_created): + mock_created.return_value = True + vendor = Vendor() + vendor._version = "64" + assert vendor.version == "64" + mock_load.assert_not_called() + +@mock.patch.object(Vendor, 'load') +def test_version_with_load(mock_load): + vendor = Vendor() + vendor.identifier = "12345" + assert vendor.version == None + mock_load.assert_called_once() + +@mock.patch.object(Vendor, 'created') +@mock.patch.object(Vendor, 'load') +def test_status_no_load_no_created(mock_load, mock_created): + mock_created.return_value = False + vendor = Vendor() + assert vendor.status == None + mock_load.assert_not_called() + +@mock.patch.object(Vendor, 'created') +@mock.patch.object(Vendor, 'load') +def test_status_no_load_created(mock_load, mock_created): + mock_created.return_value = True + vendor = Vendor() + vendor.identifier = "12345" + vendor._status = "Draft" + assert vendor.status == "Draft" + mock_load.assert_not_called() + +@mock.patch.object(Vendor, 'load') +def test_status_with_load(mock_load): + vendor = Vendor() + vendor.identifier = "12345" + assert vendor.status == None + mock_load.assert_called_once() + +@mock.patch.object(Vendor, 'submit') +@mock.patch.object(Vendor, 'create') +def test_onboard_new_vendor(mock_create, mock_submit): + getter_mock = mock.Mock(wraps=Vendor.status.fget) + mock_status = Vendor.status.getter(getter_mock) + with mock.patch.object(Vendor, 'status', mock_status): + getter_mock.side_effect = [None, const.CERTIFIED, const.CERTIFIED] + vendor = Vendor() + vendor.onboard() + mock_create.assert_called_once() + mock_submit.assert_not_called() + +@mock.patch.object(Vendor, 'submit') +@mock.patch.object(Vendor, 'create') +def test_onboard_created_vendor(mock_create, mock_submit): + getter_mock = mock.Mock(wraps=Vendor.status.fget) + mock_status = Vendor.status.getter(getter_mock) + with mock.patch.object(Vendor, 'status', mock_status): + getter_mock.side_effect = [const.DRAFT, const.DRAFT, None] + vendor = Vendor() + vendor.onboard() + mock_submit.assert_called_once() + mock_create.assert_not_called() + +@mock.patch.object(Vendor, 'submit') +@mock.patch.object(Vendor, 'create') +def test_onboard_whole_vendor(mock_create, mock_submit): + getter_mock = mock.Mock(wraps=Vendor.status.fget) + mock_status = Vendor.status.getter(getter_mock) + with mock.patch.object(Vendor, 'status', mock_status): + getter_mock.side_effect = [None, const.DRAFT, const.DRAFT, None] + vendor = Vendor() + vendor.onboard() + mock_submit.assert_called_once() + mock_create.assert_called_once() diff --git a/tests/test_version.py b/tests/test_version.py new file mode 100644 index 0000000..589f949 --- /dev/null +++ b/tests/test_version.py @@ -0,0 +1,20 @@ +"""Test version module.""" +# Copyright 2022 Orange, 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 onapsdk.version as version + +def test_version(): + """Check version is the right one.""" + assert version.__version__ == '10.2.0' diff --git a/tests/test_ves.py b/tests/test_ves.py new file mode 100644 index 0000000..17d71d5 --- /dev/null +++ b/tests/test_ves.py @@ -0,0 +1,56 @@ +# Copyright 2022 Orange, Deutsche Telekom AG, Nokia +# +# 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 unittest.mock import patch +import json + +from onapsdk.ves.ves import Ves, ACTION, POST_HTTP_METHOD + +VERSION = "v7" + +VES_URL = f"http://ves.api.simpledemo.onap.org:30417/eventListener/{VERSION}" +VES_BATCH_URL = f"http://ves.api.simpledemo.onap.org:30417/eventListener/{VERSION}/eventBatch" + +TEST_EVENT = '{"event": {"test": "val"}}' +BASIC_AUTH = {'username': 'dcae@dcae.onap.org', 'password': 'demo123456!'} + + +@patch.object(Ves, "send_message") +def test_should_send_event_to_ves_service(send_message_mock): + # given + + # when + Ves.send_event(VERSION, TEST_EVENT, BASIC_AUTH) + + # then + verify_that_event_was_send_to_ves(TEST_EVENT, send_message_mock, VES_URL) + + +@patch.object(Ves, "send_message") +def test_should_send_event_batch_to_ves_service(send_message_mock): + # given + + # when + Ves.send_batch_event(VERSION, TEST_EVENT, BASIC_AUTH) + + # then + verify_that_event_was_send_to_ves(TEST_EVENT, send_message_mock, VES_BATCH_URL) + + +def verify_that_event_was_send_to_ves(expected_event, send_message_mock, ves_url): + send_message_mock.assert_called_once_with( + POST_HTTP_METHOD, ACTION, ves_url, + basic_auth=BASIC_AUTH, + json=json.loads(expected_event) + ) + send_message_mock.return_value = None diff --git a/tests/test_vf.py b/tests/test_vf.py new file mode 100644 index 0000000..540a377 --- /dev/null +++ b/tests/test_vf.py @@ -0,0 +1,564 @@ +"""Test vf module.""" +# Copyright 2022 Orange, 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 json +import time +from unittest import mock +from unittest.mock import MagicMock +from pathlib import Path + +import pytest + +import onapsdk.constants as const +from onapsdk.exceptions import ParameterError, StatusError, RequestError, ValidationError +from onapsdk.sdc.category_management import ResourceCategory +from onapsdk.sdc.properties import ComponentProperty, NestedInput, Property +from onapsdk.sdc.sdc_resource import SdcResource +from onapsdk.sdc.vf import Vf +from onapsdk.sdc.vsp import Vsp +from onapsdk.sdc.vsp import Vendor + + +@mock.patch.object(Vf, 'send_message_json') +def test_get_all_no_vf(mock_send): + """Returns empty array if no vfs.""" + mock_send.return_value = {} + assert Vf.get_all() == [] + mock_send.assert_called_once_with("GET", 'get Vfs', 'https://sdc.api.be.simpledemo.onap.org:30204/sdc/v1/catalog/resources?resourceType=VF') + + +@mock.patch.object(Vf, 'send_message_json') +def test_get_all_some_vfs(mock_send): + """Returns a list of vf.""" + mock_send.return_value = [ + {'resourceType': 'VF', 'name': 'one', 'uuid': '1234', 'invariantUUID': '5678', 'version': '1.0', 'lifecycleState': 'CERTIFIED', 'category': 'Generic', "subCategory": "Abstract"}, + {'resourceType': 'VF', 'name': 'two', 'uuid': '1235', 'invariantUUID': '5679', 'version': '1.0', 'lifecycleState': 'NOT_CERTIFIED_CHECKOUT', 'category': 'Generic', "subCategory": "Abstract"}] + all_vfs = Vf.get_all() + assert len(all_vfs) == 2 + vf_1 = all_vfs[0] + assert vf_1.name == "one" + assert vf_1.identifier == "1234" + assert vf_1.unique_uuid == "5678" + assert vf_1.version == "1.0" + assert vf_1.status == const.CERTIFIED + assert vf_1.created() + vf_2 = all_vfs[1] + assert vf_2.name == "two" + assert vf_2.identifier == "1235" + assert vf_2.unique_uuid == "5679" + assert vf_2.status == const.DRAFT + assert vf_2.version == "1.0" + assert vf_2.created() + mock_send.assert_called_once_with("GET", 'get Vfs', 'https://sdc.api.be.simpledemo.onap.org:30204/sdc/v1/catalog/resources?resourceType=VF') + + +def test_init_no_name(): + """Check init with no names.""" + vf = Vf() + assert isinstance(vf, SdcResource) + assert vf._identifier is None + assert vf._version is None + assert vf.name == "ONAP-test-VF" + assert vf.headers["USER_ID"] == "cs0008" + assert vf.vsp is None + assert isinstance(vf._base_url(), str) + +@mock.patch.object(Vf, 'exists') +def test_init_with_name(mock_exists): + """Check init with no names.""" + mock_exists.return_value = False + vf = Vf(name="YOLO") + assert vf._identifier == None + assert vf._version == None + assert vf.name == "YOLO" + assert vf.created() == False + assert vf.headers["USER_ID"] == "cs0008" + assert vf.vsp == None + assert isinstance(vf._base_url(), str) + + +def test_equality_really_equals(): + """Check two vfs are equals if name is the same.""" + vf_1 = Vf(name="equal") + vf_1.identifier = "1234" + vf_2 = Vf(name="equal") + vf_2.identifier = "1235" + assert vf_1 == vf_2 + + +def test_equality_not_equals(): + """Check two vfs are not equals if name is not the same.""" + vf_1 = Vf(name="equal") + vf_1.identifier = "1234" + vf_2 = Vf(name="not_equal") + vf_2.identifier = "1234" + assert vf_1 != vf_2 + + +def test_equality_not_equals_not_same_object(): + """Check a vf and something different are not equals.""" + vf_1 = Vf(name="equal") + vf_1.identifier = "1234" + vf_2 = SdcResource() + vf_2.name = "equal" + assert vf_1 != vf_2 + + +@mock.patch.object(Vf, 'get_all') +def test_exists_not_exists(mock_get_all): + """Return False if vf doesn't exist in SDC.""" + vf_1 = Vf(name="one") + vf_1.identifier = "1234" + mock_get_all.return_value = [vf_1] + vf = Vf(name="two") + assert not vf.exists() + + +@mock.patch.object(Vf, 'get_all') +def test_exists_exists(mock_get_all): + """Return True if vf exists in SDC.""" + vf_1 = Vf(name="one") + vf_1.identifier = "1234" + vf_1.unique_uuid = "5689" + vf_1.unique_identifier = "71011" + vf_1.status = const.DRAFT + vf_1.version = "1.1" + mock_get_all.return_value = [vf_1] + vf = Vf(name="one") + assert vf.exists() + assert vf.identifier == "1234" + assert vf.unique_uuid == "5689" + assert vf.unique_identifier == "71011" + assert vf.status == const.DRAFT + assert vf.version == "1.1" + + +@mock.patch.object(Vf, 'exists') +def test_load_created(mock_exists): + """Load is a wrapper around exists().""" + vf = Vf(name="one") + vf.load() + mock_exists.assert_called_once() + + +@mock.patch.object(Vf, 'exists') +@mock.patch.object(Vf, 'send_message_json') +def test_create_no_vsp(mock_send, mock_exists): + """Do nothing if no vsp.""" + vf = Vf() + mock_exists.return_value = False + with pytest.raises(ParameterError) as err: + vf.create() + assert err.type == ParameterError + assert str(err.value) == "At least vsp or vendor needs to be given" + + +@mock.patch.object(Vf, 'exists') +@mock.patch.object(Vf, 'send_message_json') +@mock.patch.object(Vf, "category", new_callable=mock.PropertyMock) +def test_create_already_exists(mock_category, mock_send, mock_exists): + """Do nothing if already created in SDC.""" + vf = Vf(vendor=MagicMock()) + vsp = Vsp() + vsp._identifier = "1232" + vf.vsp = vsp + mock_exists.return_value = True + vf.create() + mock_send.assert_not_called() + +@mock.patch.object(Vf, 'exists') +@mock.patch.object(Vf, 'send_message_json') +@mock.patch.object(Vf, "category", new_callable=mock.PropertyMock) +def test_create_issue_in_creation(mock_category, mock_send, mock_exists): + """Do nothing if not created but issue during creation.""" + vf = Vf() + vsp = Vsp() + vendor = Vendor() + vsp._identifier = "1232" + vsp.create_csar = MagicMock(return_value=True) + vsp.vendor = vendor + vf.vsp = vsp + expected_data = '{\n "artifacts": {},\n "attributes": [],\n "capabilities": {},\n "categories": [\n {\n "normalizedName": "generic",\n "name": "Generic",\n "uniqueId": "resourceNewCategory.generic",\n "subcategories": [{"empty": false, "groupings": null, "icons": ["objectStorage", "compute"], "name": "Abstract", "normalizedName": "abstract", "ownerId": null, "type": null, "uniqueId": "resourceNewCategory.generic.abstract", "version": null}],\n "version": null,\n "ownerId": null,\n "empty": false,\n "type": null,\n "icons": null\n }\n ],\n "componentInstances": [],\n "componentInstancesAttributes": {},\n "componentInstancesProperties": {},\n "componentType": "RESOURCE",\n "contactId": "cs0008",\n \n "csarUUID": "None",\n "csarVersion": "1.0",\n \n "deploymentArtifacts": {},\n "description": "VF",\n "icon": "defaulticon",\n "name": "ONAP-test-VF",\n "properties": [],\n "groups": [],\n "requirements": {},\n "resourceType": "VF",\n "tags": ["ONAP-test-VF"],\n "toscaArtifacts": {},\n "vendorName": "Generic-Vendor",\n "vendorRelease": "1.0"\n}' + mock_exists.return_value = False + mock_send.side_effect = RequestError + rc = ResourceCategory( + name="Generic" + ) + rc.normalized_name="generic" + rc.unique_id="resourceNewCategory.generic" + rc.subcategories=[{"empty": False, "groupings": None, "icons": ["objectStorage", "compute"], "name": "Abstract", "normalizedName": "abstract", "ownerId": None, "type": None, "uniqueId": "resourceNewCategory.generic.abstract", "version": None}] + rc.version=None + rc.owner_id=None + rc.empty=False + rc.type=None + rc.icons=None + mock_category.return_value = rc + with pytest.raises(RequestError) as exc: + vf.create() + mock_send.assert_called_once_with("POST", "create Vf", 'https://sdc.api.fe.simpledemo.onap.org:30207/sdc1/feProxy/rest/v1/catalog/resources', data=expected_data) + assert not vf.created() + + +@mock.patch.object(Vf, 'exists') +@mock.patch.object(Vf, 'send_message_json') +@mock.patch.object(Vf, "category", new_callable=mock.PropertyMock) +def test_create_OK(mock_category, mock_send, mock_exists): + """Create and update object.""" + vf = Vf() + vsp = Vsp() + vendor = Vendor() + vsp._identifier = "1232" + vf.vsp = vsp + vsp.vendor = vendor + vsp._csar_uuid = "1234" + expected_data = '{\n "artifacts": {},\n "attributes": [],\n "capabilities": {},\n "categories": [\n {\n "normalizedName": "generic",\n "name": "Generic",\n "uniqueId": "resourceNewCategory.generic",\n "subcategories": [{"empty": false, "groupings": null, "icons": ["objectStorage", "compute"], "name": "Abstract", "normalizedName": "abstract", "ownerId": null, "type": null, "uniqueId": "resourceNewCategory.generic.abstract", "version": null}],\n "version": null,\n "ownerId": null,\n "empty": false,\n "type": null,\n "icons": null\n }\n ],\n "componentInstances": [],\n "componentInstancesAttributes": {},\n "componentInstancesProperties": {},\n "componentType": "RESOURCE",\n "contactId": "cs0008",\n \n "csarUUID": "1234",\n "csarVersion": "1.0",\n \n "deploymentArtifacts": {},\n "description": "VF",\n "icon": "defaulticon",\n "name": "ONAP-test-VF",\n "properties": [],\n "groups": [],\n "requirements": {},\n "resourceType": "VF",\n "tags": ["ONAP-test-VF"],\n "toscaArtifacts": {},\n "vendorName": "Generic-Vendor",\n "vendorRelease": "1.0"\n}' + mock_exists.return_value = False + mock_send.return_value = {'resourceType': 'VF', 'name': 'one', 'uuid': '1234', 'invariantUUID': '5678', 'version': '1.0', 'uniqueId': '91011', 'lifecycleState': 'NOT_CERTIFIED_CHECKOUT'} + rc = ResourceCategory( + name="Generic" + ) + rc.normalized_name="generic" + rc.unique_id="resourceNewCategory.generic" + rc.subcategories=[{"empty": False, "groupings": None, "icons": ["objectStorage", "compute"], "name": "Abstract", "normalizedName": "abstract", "ownerId": None, "type": None, "uniqueId": "resourceNewCategory.generic.abstract", "version": None}] + rc.version=None + rc.owner_id=None + rc.empty=False + rc.type=None + rc.icons=None + mock_category.return_value = rc + vf.create() + mock_send.assert_called_once_with("POST", "create Vf", 'https://sdc.api.fe.simpledemo.onap.org:30207/sdc1/feProxy/rest/v1/catalog/resources', data=expected_data) + assert vf.created() + assert vf._status == const.DRAFT + assert vf.identifier == "1234" + assert vf.unique_uuid == "5678" + assert vf.version == "1.0" + +@mock.patch.object(Vf, 'exists') +@mock.patch.object(Vf, 'load') +def test_version_no_load_no_created(mock_load, mock_exists): + """Test versions when not created.""" + mock_exists.return_value = False + vf = Vf() + assert vf.version is None + mock_load.assert_not_called() + +@mock.patch.object(Vf, 'load') +def test_version_no_load_created(mock_load): + """Test versions when created.""" + vf = Vf() + vf.identifier = "1234" + vf._version = "64" + assert vf.version == "64" + mock_load.assert_not_called() + + +@mock.patch.object(Vf, 'load') +def test_version_with_load(mock_load): + """Test versions when not created but with identifier.""" + vf = Vf() + vf.identifier = "1234" + assert vf.version is None + mock_load.assert_called_once() + +@mock.patch.object(Vf, 'exists') +@mock.patch.object(Vf, 'load') +def test_status_no_load_no_created(mock_load, mock_exists): + """Test status when not created.""" + mock_exists.return_value = False + vf = Vf() + assert vf.status is None + + +@pytest.mark.parametrize("status", [const.COMMITED, const.CERTIFIED, const.UPLOADED, const.VALIDATED]) +@mock.patch.object(Vf, 'exists') +@mock.patch.object(Vf, 'load') +@mock.patch.object(Vf, 'send_message') +def test_submit_not_Commited(mock_send, mock_load, mock_exists, status): + """Do nothing if not created.""" + mock_exists.return_value = False + vf = Vf() + vf._status = status + vf.submit() + mock_send.assert_not_called() + +@mock.patch.object(Vf, 'exists') +@mock.patch.object(Vf, 'load') +@mock.patch.object(Vf, 'send_message') +def test_submit_OK(mock_send, mock_load, mock_exists): + """Don't update status if submission NOK.""" + mock_exists.return_value = True + vf = Vf() + vf._status = const.COMMITED + expected_data = '{\n "userRemarks": "certify"\n}' + vf._version = "1234" + vf._unique_identifier = "12345" + vf.submit() + mock_send.assert_called_once_with( + "POST", "Certify Vf", + 'https://sdc.api.fe.simpledemo.onap.org:30207/sdc1/feProxy/rest/v1/catalog/resources/12345/lifecycleState/Certify', + data=expected_data) + +@mock.patch.object(Vf, 'load') +@mock.patch.object(Vf, 'certify') +@mock.patch.object(Vf, 'submit') +@mock.patch.object(Vf, 'create') +@mock.patch.object(Vf, 'add_resource') +def test_onboard_new_vf(mock_add_resource, mock_create, mock_submit, mock_certify, mock_load): + getter_mock = mock.Mock(wraps=Vf.status.fget) + mock_status = Vf.status.getter(getter_mock) + with mock.patch.object(Vf, 'status', mock_status): + getter_mock.side_effect = [None, const.APPROVED, const.APPROVED, + const.APPROVED, const.APPROVED] + vsp = Vsp() + vf = Vf(vsp=vsp) + vf._time_wait = 0 + vf.onboard() + mock_create.assert_called_once() + mock_add_resource.assert_not_called() + mock_submit.assert_not_called() + mock_certify.assert_not_called() + mock_load.assert_not_called() + +@mock.patch.object(Vf, 'load') +@mock.patch.object(Vf, 'submit') +@mock.patch.object(Vf, 'create') +@mock.patch.object(Vf, 'add_resource') +@mock.patch.object(Vf, "certify") +def test_onboard_vf_submit(mock_certify, mock_add_resource, mock_create, mock_submit, mock_load): + getter_mock = mock.Mock(wraps=Vf.status.fget) + mock_status = Vf.status.getter(getter_mock) + with mock.patch.object(Vf, 'status', mock_status): + getter_mock.side_effect = [const.DRAFT, const.DRAFT, + const.CHECKED_IN, const.CHECKED_IN, const.CHECKED_IN, + const.APPROVED, const.APPROVED, const.APPROVED, const.APPROVED] + vf = Vf() + vf._time_wait = 0 + vf.onboard() + mock_create.assert_not_called() + mock_add_resource.assert_not_called() + mock_submit.assert_called_once() + mock_load.assert_not_called() + mock_certify.assert_called_once() + +@mock.patch.object(Vf, 'load') +@mock.patch.object(Vf, 'submit') +@mock.patch.object(Vf, 'create') +@mock.patch.object(Vf, 'add_resource') +@mock.patch.object(Vf, "certify") +def test_onboard_vf_load(mock_certify, mock_add_resource, mock_create, mock_submit, mock_load): + getter_mock = mock.Mock(wraps=Vf.status.fget) + mock_status = Vf.status.getter(getter_mock) + with mock.patch.object(Vf, 'status', mock_status): + getter_mock.side_effect = [const.CERTIFIED, const.CERTIFIED, + const.CERTIFIED, const.CERTIFIED, + const.APPROVED, const.APPROVED, + const.APPROVED, const.APPROVED] + vf = Vf() + vf._time_wait = 0 + vf.onboard() + mock_create.assert_not_called() + mock_add_resource.assert_not_called() + mock_submit.assert_not_called() + mock_load.assert_called_once() + mock_certify.assert_not_called() + +@mock.patch.object(Vf, 'load') +@mock.patch.object(Vf, 'submit') +@mock.patch.object(Vf, 'create') +@mock.patch.object(Vf, 'add_resource') +@mock.patch.object(Vf, "certify") +def test_onboard_whole_vf(mock_certify, mock_add_resource, mock_create, mock_submit, mock_load): + getter_mock = mock.Mock(wraps=Vf.status.fget) + mock_status = Vf.status.getter(getter_mock) + with mock.patch.object(Vf, 'status', mock_status): + getter_mock.side_effect = [None, const.DRAFT, const.DRAFT, + const.CHECKED_IN, const.CHECKED_IN, const.CHECKED_IN, + const.CERTIFIED, const.CERTIFIED, const.CERTIFIED, const.CERTIFIED, + const.APPROVED, const.APPROVED, const.APPROVED, const.APPROVED] + vsp = Vsp() + vf = Vf(vsp=vsp) + vf._time_wait = 0 + vf.onboard() + mock_create.assert_called_once() + mock_add_resource.assert_not_called() + mock_submit.assert_called_once() + mock_load.assert_called_once() + mock_certify.assert_called_once() + + +@mock.patch.object(Vf, "send_message_json") +def test_add_properties(mock_send_message_json): + vf = Vf(name="test") + vf._identifier = "toto" + vf._unique_identifier = "toto" + vf._status = const.CERTIFIED + with pytest.raises(StatusError): + vf.add_property(Property(name="test", property_type="string")) + vf._status = const.DRAFT + vf.add_property(Property(name="test", property_type="string")) + mock_send_message_json.assert_called_once() + +@mock.patch.object(Vf, 'load') +@mock.patch.object(Vf, 'send_message') +def test_add_artifact_to_vf(mock_send_message, mock_load): + """Test VF add artifact""" + vf = Vf(name="test") + vf.status = const.DRAFT + mycbapath = Path(Path(__file__).resolve().parent, "data/vLB_CBA_Python.zip") + + result = vf.add_deployment_artifact(artifact_label="cba", + artifact_type="CONTROLLER_BLUEPRINT_ARCHIVE", + artifact_name="vLB_CBA_Python.zip", + artifact=mycbapath) + mock_send_message.assert_called() + method, description, url = mock_send_message.call_args[0] + assert method == "POST" + assert description == "Add deployment artifact for test sdc resource" + assert url == ("https://sdc.api.fe.simpledemo.onap.org:30207/sdc1/feProxy/rest/v1/catalog/resources/" + f"{vf.unique_identifier}/artifacts") + + +@mock.patch.object(Vf, "created") +@mock.patch.object(ResourceCategory, "get") +def test_vf_category(mock_resource_category, mock_created): + mock_created.return_value = False + vf = Vf(name="test") + _ = vf.category + mock_resource_category.assert_called_once_with(name="Generic", subcategory="Abstract") + mock_resource_category.reset_mock() + + vf = Vf(name="test", category="Allotted Resource", subcategory="Allotted Resource") + _ = vf.category + mock_resource_category.assert_called_once_with(name="Allotted Resource", subcategory="Allotted Resource") + mock_resource_category.reset_mock() + + vf = Vf(name="test", category="test", subcategory="test") + _ = vf.category + mock_resource_category.assert_called_once_with(name="test", subcategory="test") + mock_resource_category.reset_mock() + + mock_created.return_value = True + _ = vf.category + mock_resource_category.assert_called_once_with(name="test", subcategory="test") + +@mock.patch.object(Vf, "send_message_json") +def test_update_vsp(mock_send): + + vf = Vf(name="test") + vf._unique_identifier = "123" + vsp = MagicMock() + vsp.csar_uuid = "122333" + vsp.human_readable_version = "1.0" + mock_send.return_value = { + "csarUUID": "322111", + "csarVersion": "0.1", + "tags": [], + "categories": [], + "allVersions": [], + "archived": False, + "creationDate": int(time.time()), + "lastUpdateDate": int(time.time()), + } + vf.update_vsp(vsp) + assert mock_send.call_count == 2 + mock_call_kwargs_data = json.loads(mock_send.mock_calls[-1][2]["data"]) # Get kward from `unittest.mock.call` tuple + assert mock_call_kwargs_data["csarUUID"] == "122333" + assert mock_call_kwargs_data["csarVersion"] == "1.0" + +@mock.patch.object(Vf, 'exists') +@mock.patch.object(Vf, 'send_message') +def test_add_resource_not_draft(mock_send, mock_exists): + mock_exists.return_value = False + vf = Vf() + resource = SdcResource() + with pytest.raises(StatusError): + vf.add_resource(resource) + mock_send.assert_not_called() + +@mock.patch.object(Vf, 'load') +@mock.patch.object(Vf, 'send_message') +def test_add_resource_bad_result(mock_send, mock_load): + vf = Vf() + vf.unique_identifier = "45" + vf.identifier = "93" + vf.status = const.DRAFT + mock_send.return_value = {} + resource = SdcResource() + resource.unique_identifier = "12" + resource.created = MagicMock(return_value=True) + resource.version = "40" + resource.name = "test" + assert vf.add_resource(resource) is None + mock_send.assert_called_once_with( + 'POST', 'Add SDCRESOURCE to VF', + 'https://sdc.api.fe.simpledemo.onap.org:30207/sdc1/feProxy/rest/v1/catalog/resources/45/resourceInstance', + data='{\n "name": "test",\n "componentVersion": "40",\n "posY": 100,\n "posX": 200,\n "uniqueId": "12",\n "originType": "SDCRESOURCE",\n "componentUid": "12",\n "icon": "defaulticon"\n}') + +@mock.patch.object(Vf, 'load') +@mock.patch.object(Vf, 'send_message') +def test_add_resource_OK(mock_send, mock_load): + vf = Vf() + vf.unique_identifier = "45" + vf.identifier = "93" + vf.status = const.DRAFT + mock_send.return_value = {'yes': 'indeed'} + resource = SdcResource() + resource.unique_identifier = "12" + resource.created = MagicMock(return_value=True) + resource.version = "40" + resource.name = "test" + result = vf.add_resource(resource) + assert result['yes'] == "indeed" + mock_send.assert_called_once_with( + 'POST', 'Add SDCRESOURCE to VF', + 'https://sdc.api.fe.simpledemo.onap.org:30207/sdc1/feProxy/rest/v1/catalog/resources/45/resourceInstance', + data='{\n "name": "test",\n "componentVersion": "40",\n "posY": 100,\n "posX": 200,\n "uniqueId": "12",\n "originType": "SDCRESOURCE",\n "componentUid": "12",\n "icon": "defaulticon"\n}') + +@mock.patch.object(Vf, 'created') +@mock.patch.object(Vf, "send_message_json") +@mock.patch.object(Vf, "resource_inputs_url") +def test_vf_vendor_property(mock_resource_inputs_url, mock_send_message_json, mock_created): + mock_created.return_value = False + vf = Vf() + assert vf.vendor is None + + vsp_mock = MagicMock() + vsp_mock.vendor = MagicMock() + vf.vsp = vsp_mock + assert vf.vendor == vsp_mock.vendor + + vf._vendor = None + mock_created.return_value = True + mock_send_message_json.return_value = {"vendorName": "123"} + assert vf.vendor.name == "123" + +@mock.patch.object(SdcResource, "declare_input") +@mock.patch.object(Vf, "send_message") +def test_vf_declare_input(mock_send_message, mock_sdc_resource_declare_input): + vf = Vf() + prop = Property(name="test_prop", property_type="string") + nested_input = NestedInput(MagicMock(), MagicMock()) + vf.declare_input(prop) + mock_sdc_resource_declare_input.assert_called_once() + mock_send_message.assert_not_called() + mock_sdc_resource_declare_input.reset_mock() + vf.declare_input(nested_input) + mock_sdc_resource_declare_input.assert_called_once() + mock_send_message.assert_not_called() + mock_sdc_resource_declare_input.reset_mock() + vf.declare_input(ComponentProperty("test_unique_id", "test_property_type", "test_name", MagicMock())) + mock_send_message.assert_called() + mock_sdc_resource_declare_input.assert_not_called() diff --git a/tests/test_vid.py b/tests/test_vid.py new file mode 100644 index 0000000..47281ba --- /dev/null +++ b/tests/test_vid.py @@ -0,0 +1,54 @@ +# Copyright 2022 Orange, 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 unittest.mock import patch + +from onapsdk.vid import ( + OwningEntity, + Project, + LineOfBusiness, + Platform +) + + +@patch.object(LineOfBusiness, "send_message") +def test_line_of_business(send_message_mock): + assert LineOfBusiness.get_create_url() == "https://vid.api.simpledemo.onap.org:30200/vid/maintenance/category_parameter/lineOfBusiness" + + line_of_businnes = LineOfBusiness.create("test") + assert line_of_businnes.name == "test" + + +@patch.object(OwningEntity, "send_message") +def test_owning_entity(send_message_mock): + assert OwningEntity.get_create_url() == "https://vid.api.simpledemo.onap.org:30200/vid/maintenance/category_parameter/owningEntity" + + owning_entity = OwningEntity.create("test") + assert owning_entity.name == "test" + + +@patch.object(Project, "send_message") +def test_project(send_message_mock): + assert Project.get_create_url() == "https://vid.api.simpledemo.onap.org:30200/vid/maintenance/category_parameter/project" + + project = Project.create("test") + assert project.name == "test" + + +@patch.object(Platform, "send_message") +def test_platform(send_message_mock): + assert Platform.get_create_url() == "https://vid.api.simpledemo.onap.org:30200/vid/maintenance/category_parameter/platform" + + platform = Platform.create("test") + assert platform.name == "test" diff --git a/tests/test_vsp.py b/tests/test_vsp.py new file mode 100644 index 0000000..a1aa8a7 --- /dev/null +++ b/tests/test_vsp.py @@ -0,0 +1,799 @@ +"""Test vsp module.""" +# Copyright 2022 Orange, 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 unittest import mock +import json + +import pytest +import requests +from onapsdk.exceptions import APIError, ParameterError, RequestError + +from onapsdk.sdc.vsp import Vsp +from onapsdk.sdc.vendor import Vendor +import onapsdk.constants as const +from onapsdk.sdc.sdc_element import SdcElement + +@mock.patch.object(Vsp, 'send_message_json') +def test_get_all_no_vsp(mock_send): + """Returns empty array if no vsps.""" + mock_send.return_value = {} + assert Vsp.get_all() == [] + mock_send.assert_called_once_with( + "GET", 'get Vsps', + 'https://sdc.api.fe.simpledemo.onap.org:30207/sdc1/feProxy/onboarding-api/v1.0/vendor-software-products') + +@mock.patch.object(Vsp, 'load_status') +@mock.patch.object(Vsp, 'send_message_json') +def test_get_all_some_vsps(mock_send, mock_load_status): + """Returns a list of vsp.""" + mock_send.return_value = {'results':[ + {'name': 'one', 'id': '1234', 'vendorName': 'vspOne'}, + {'name': 'two', 'id': '1235', 'vendorName': 'vspOne'}]} + assert len(Vsp.get_all()) == 2 + vsp_1 = Vsp.get_all()[0] + assert vsp_1.name == "one" + assert vsp_1.identifier == "1234" + assert vsp_1.created() + vsp_2 = Vsp.get_all()[1] + assert vsp_2.name == "two" + assert vsp_2.identifier == "1235" + assert vsp_2.vendor == vsp_1.vendor + assert vsp_2.created() + mock_send.assert_called_with( + "GET", 'get Vsps', + 'https://sdc.api.fe.simpledemo.onap.org:30207/sdc1/feProxy/onboarding-api/v1.0/vendor-software-products') + +@mock.patch.object(Vsp, 'created') +def test_init_no_name(mock_created): + """Check init with no names.""" + mock_created.return_value = False + vsp = Vsp() + assert isinstance(vsp, SdcElement) + assert vsp._identifier == None + assert vsp._version == None + assert vsp.name == "ONAP-test-VSP" + assert vsp.headers["USER_ID"] == "cs0008" + assert vsp.vendor == None + assert isinstance(vsp._base_url(), str) + assert "sdc1/feProxy/onboarding-api/v1.0" in vsp._base_url() + +@mock.patch.object(Vsp, 'exists') +def test_init_with_name(mock_exists): + """Check init with no names.""" + vsp = Vsp(name="YOLO") + mock_exists.return_value = False + assert vsp._identifier == None + assert vsp._version == None + assert vsp.name == "YOLO" + assert vsp.created() == False + assert vsp.headers["USER_ID"] == "cs0008" + assert vsp.vendor == None + assert isinstance(vsp._base_url(), str) + assert "sdc1/feProxy/onboarding-api/v1.0" in vsp._base_url() + +def test_equality_really_equals(): + """Check two vsps are equals if name is the same.""" + vsp_1 = Vsp(name="equal") + vsp_1.identifier = "1234" + vsp_2 = Vsp(name="equal") + vsp_2.identifier = "1235" + assert vsp_1 == vsp_2 + +def test_equality_not_equals(): + """Check two vsps are not equals if name is not the same.""" + vsp_1 = Vsp(name="equal") + vsp_1.identifier = "1234" + vsp_2 = Vsp(name="not_equal") + vsp_2.identifier = "1234" + assert vsp_1 != vsp_2 + +def test_equality_not_equals_not_same_object(): + """Check a vsp and something different are not equals.""" + vsp_1 = Vsp(name="equal") + vsp_1.identifier = "1234" + vsp_2 = Vendor(name="equal") + assert vsp_1 != vsp_2 + +@mock.patch.object(Vsp, 'get_all') +def test_exists_not_exists(mock_get_all): + """Return False if vsp doesn't exist in SDC.""" + vsp_1 = Vsp(name="one") + vsp_1.identifier = "1234" + mock_get_all.return_value = [vsp_1] + vsp = Vsp(name="two") + assert not vsp.exists() + +@mock.patch.object(Vsp, 'get_all') +def test_exists_exists(mock_get_all): + """Return True if vsp exists in SDC.""" + vsp_1 = Vsp(name="one") + vsp_1.identifier = "1234" + vsp_1.version = "1.1" + mock_get_all.return_value = [vsp_1] + vsp = Vsp(name="one") + assert vsp.exists() + +@mock.patch.object(Vsp, 'get_all') +@mock.patch.object(Vsp, 'send_message_json') +def test_load_created(mock_send, mock_get_all): + mock_send.return_value = {'results': + [{'status': 'state_one', 'id': "5678", 'vendorName': 'vspOne', 'name': "1.0"}], "listCount": 1} + vsp = Vsp(name="one") + vsp.identifier = "1234" + vsp.load() + mock_get_all.assert_not_called() + mock_send.assert_called_once_with('GET', 'get item', 'https://sdc.api.fe.simpledemo.onap.org:30207/sdc1/feProxy/onboarding-api/v1.0/items/1234/versions') + assert vsp._status == None + assert vsp.version == "5678" + +@mock.patch.object(Vsp, 'get_all') +@mock.patch.object(Vsp, 'send_message_json') +def test_load_not_created(mock_send, mock_get_all): + mock_send.return_value = {'results': + [{'status': 'state_one', 'id': "5678", 'vendorName': 'vspOne', 'name': "1.0"}], "listCount": 1} + vsp = Vsp(name="one") + vsp.load() + mock_get_all.return_value = [] + mock_send.assert_not_called() + assert vsp._status == None + assert vsp.version == None + assert vsp.identifier == None + +@mock.patch.object(Vsp, 'exists') +@mock.patch.object(Vsp, 'send_message_json') +def test_create_no_vendor(mock_send, mock_exists): + """Do nothing if no vendor.""" + vsp = Vsp() + mock_exists.return_value = False + vsp.create() + mock_send.assert_not_called() + +@mock.patch.object(Vsp, 'exists') +@mock.patch.object(Vsp, 'send_message_json') +def test_create_already_exists(mock_send, mock_exists): + """Do nothing if already created in SDC.""" + vsp = Vsp() + vendor = Vendor() + vendor._identifier = "1232" + vsp.vendor = vendor + mock_exists.return_value = True + vsp.create() + mock_send.assert_not_called() + +@mock.patch.object(Vsp, 'exists') +@mock.patch.object(Vsp, 'send_message_json') +def test_create_issue_in_creation(mock_send, mock_exists): + """Do nothing if not created but issue during creation.""" + vsp = Vsp() + vendor = Vendor() + vendor._identifier = "1232" + vsp.vendor = vendor + expected_data = '{\n "name": "ONAP-test-VSP",\n "description": "vendor software product",\n "icon": "icon",\n "category": "resourceNewCategory.generic",\n "subCategory": "resourceNewCategory.generic.abstract",\n "vendorName": "Generic-Vendor",\n "vendorId": "1232",\n "licensingData": {},\n "onboardingMethod": "NetworkPackage"\n}' + mock_exists.return_value = False + mock_send.side_effect = RequestError + with pytest.raises(RequestError) as exc: + vsp.create() + mock_send.assert_called_once_with("POST", "create Vsp", 'https://sdc.api.fe.simpledemo.onap.org:30207/sdc1/feProxy/onboarding-api/v1.0/vendor-software-products', data=expected_data) + assert vsp.created() == False + +@mock.patch.object(Vsp, 'exists') +@mock.patch.object(Vsp, 'send_message_json') +def test_create_OK(mock_send, mock_exists): + """Create and update object.""" + vsp = Vsp() + vendor = Vendor() + vendor._identifier = "1232" + vsp.vendor = vendor + expected_data = '{\n "name": "ONAP-test-VSP",\n "description": "vendor software product",\n "icon": "icon",\n "category": "resourceNewCategory.generic",\n "subCategory": "resourceNewCategory.generic.abstract",\n "vendorName": "Generic-Vendor",\n "vendorId": "1232",\n "licensingData": {},\n "onboardingMethod": "NetworkPackage"\n}' + mock_exists.return_value = False + mock_send.return_value = { + 'itemId': "1234", + 'version': {'id': "5678", 'status': 'state_created'}} + vsp.create() + mock_send.assert_called_once_with("POST", "create Vsp", 'https://sdc.api.fe.simpledemo.onap.org:30207/sdc1/feProxy/onboarding-api/v1.0/vendor-software-products', data=expected_data) + assert vsp.created() == True + assert vsp._status == const.DRAFT + assert vsp.identifier == "1234" + assert vsp.version == "5678" + +@mock.patch.object(Vsp, 'exists') +@mock.patch.object(Vsp, 'load') +def test_version_no_load_no_created(mock_load, mock_exists): + mock_exists.return_value = False + vsp = Vsp() + assert vsp.version == None + mock_load.assert_not_called() + +@mock.patch.object(Vsp, 'load') +def test_version_no_load_created(mock_load): + vsp = Vsp() + vsp.identifier = "1234" + vsp._version = "64" + assert vsp.version == "64" + mock_load.assert_not_called() + +@mock.patch.object(Vsp, 'load') +def test_version_with_load(mock_load): + vsp = Vsp() + vsp.identifier = "1234" + assert vsp.version == None + mock_load.assert_called_once() + +@mock.patch.object(Vsp, '_get_vsp_details') +@mock.patch.object(Vsp, 'created') +def test_vendor_not_created_not_vendor(mock_created, mock_details): + mock_created.return_value = False + vsp = Vsp() + assert vsp.vendor == None + mock_details.assert_not_called() + +@mock.patch.object(Vsp, '_get_vsp_details') +@mock.patch.object(Vsp, 'created') +def test_vendor_not_created_vendor(mock_created, mock_details): + mock_created.return_value = False + vsp = Vsp() + vendor = Vendor() + vsp.vendor = vendor + assert vsp.vendor == vendor + mock_details.assert_not_called() + +@mock.patch.object(Vsp, '_get_vsp_details') +@mock.patch.object(Vsp, 'created') +def test_vendor_created_not_details(mock_created, mock_details): + mock_created.return_value = True + mock_details.return_value = {} + vsp = Vsp() + assert vsp.vendor == None + mock_details.assert_called_once() + +@mock.patch.object(Vsp, '_get_vsp_details') +@mock.patch.object(Vsp, 'created') +def test_vendor_created_details(mock_created, mock_details): + mock_created.return_value = True + mock_details.return_value = {'vendorName': 'test'} + vsp = Vsp() + assert vsp.vendor.name == 'test' + mock_details.assert_called_once() + +@mock.patch.object(Vsp, '_get_vsp_details') +@mock.patch.object(Vsp, 'created') +def test_vendor_created_but_already_vendor(mock_created, mock_details): + mock_created.return_value = True + vsp = Vsp() + vendor = Vendor() + vsp.vendor = vendor + assert vsp.vendor == vendor + mock_details.assert_not_called() + +@mock.patch.object(Vsp, 'exists') +def test_status_no_load_no_created(mock_exists): + mock_exists.return_value = False + vsp = Vsp() + assert vsp.status == None + +@mock.patch.object(Vsp, '_get_item_details') +def test_status_status_is_certified_in_SDC(mock_vsp_items): + vsp = Vsp() + vsp.identifier = "1234" + mock_vsp_items.return_value={'status': const.CERTIFIED} + vsp._status = "Draft" + assert vsp.status == const.CERTIFIED + +@mock.patch.object(Vsp, '_get_vsp_details') +@mock.patch.object(Vsp, '_get_item_version_details') +@mock.patch.object(Vsp, '_get_item_details') +def test_status_version_is_not_dirty(mock_vsp_items, mock_vsp_items_version, mock_vsp_details): + vsp = Vsp() + vsp.identifier = "1234" + mock_vsp_items.return_value={'status': const.DRAFT} + mock_vsp_items_version.return_value={"state": {'dirty': False}} + mock_vsp_details.return_value={'validationData': "true"} + assert vsp.status == const.COMMITED + +@mock.patch.object(Vsp, '_get_vsp_details') +@mock.patch.object(Vsp, '_get_item_version_details') +@mock.patch.object(Vsp, '_get_item_details') +def test_status_version_is_dirty_has_validation_data(mock_vsp_items, mock_vsp_items_version, + mock_vsp_details): + vsp = Vsp() + vsp.identifier = "1234" + mock_vsp_items.return_value={'status': const.DRAFT} + mock_vsp_items_version.return_value={"state": {'dirty': True}} + mock_vsp_details.return_value={'validationData': {'some': 'thing'}} + assert vsp.status == const.VALIDATED + +@mock.patch.object(Vsp, '_get_vsp_details') +@mock.patch.object(Vsp, '_get_item_version_details') +@mock.patch.object(Vsp, '_get_item_details') +def test_status_version_is_dirty_no_validation_data_no_state(mock_vsp_items, mock_vsp_items_version, + mock_vsp_details): + vsp = Vsp() + vsp.identifier = "1234" + mock_vsp_items.return_value={'status': const.DRAFT} + mock_vsp_items_version.return_value={"status": {'dirty': False}} + mock_vsp_details.return_value={'no_validationData': {'some': 'thing'}} + assert vsp.status == const.DRAFT + +@mock.patch.object(Vsp, '_get_vsp_details') +@mock.patch.object(Vsp, '_get_item_version_details') +@mock.patch.object(Vsp, '_get_item_details') +def test_status_version_is_dirty_no_validation_data_but_state(mock_vsp_items, mock_vsp_items_version, + mock_vsp_details): + vsp = Vsp() + vsp.identifier = "1234" + mock_vsp_items.return_value={'status': const.DRAFT} + mock_vsp_items_version.return_value={"state": {'dirty': True}} + mock_vsp_details.return_value={'no_validationData': {'some': 'thing'}} + assert vsp.status == const.DRAFT + +@mock.patch.object(Vsp, '_get_vsp_details') +@mock.patch.object(Vsp, '_get_item_version_details') +@mock.patch.object(Vsp, '_get_item_details') +def test_status_version_is_dirty_no_validation_data_but_networkPackageName(mock_vsp_items, mock_vsp_items_version, + mock_vsp_details): + vsp = Vsp() + vsp.identifier = "1234" + mock_vsp_items.return_value={'status': const.DRAFT} + mock_vsp_items_version.return_value={"state": {'dirty': True}} + mock_vsp_details.return_value={'no_validationData': {'some': 'thing'}, 'networkPackageName': 'ubuntu16'} + assert vsp.status == const.UPLOADED + + +@mock.patch.object(Vsp, 'exists') +@mock.patch.object(Vsp, 'send_message_json') +def test__get_vsp_details_not_created(mock_send, mock_exists): + mock_exists.return_value = False + vsp = Vsp() + assert vsp._get_vsp_details() == {} + mock_send.assert_not_called() + +@mock.patch.object(Vsp, 'load') +@mock.patch.object(Vsp, 'send_message_json') +def test__get_vsp_details_no_version(mock_send, mock_load): + vsp = Vsp() + vsp.identifier = "1234" + mock_send.assert_not_called() + assert vsp._get_vsp_details() == {} + +@mock.patch.object(Vsp, 'send_message_json') +def test__get_vsp_details(mock_send): + vsp = Vsp() + vsp.identifier = "1234" + vsp._version = "4567" + mock_send.return_value = {'return': 'value'} + assert vsp._get_vsp_details() == {'return': 'value'} + mock_send.assert_called_once_with('GET', 'get vsp version', "{}/vendor-software-products/1234/versions/4567".format(vsp._base_url())) + +@pytest.mark.parametrize("status", [const.DRAFT, const.CERTIFIED, const.UPLOADED, const.VALIDATED]) +@mock.patch.object(Vsp, 'load_status') +@mock.patch.object(Vsp, 'send_message') +def test_submit_not_Commited(mock_send, mock_status, status): + """Do nothing if not created.""" + vsp = Vsp() + vsp._status = status + vsp.submit() + mock_send.assert_not_called() + +@mock.patch.object(Vsp, 'load_status') +@mock.patch.object(Vsp, 'send_message') +def test_submit_OK(mock_send, mock_status): + """Don't update status if submission NOK.""" + vsp = Vsp() + vsp._status = const.COMMITED + expected_data = '{\n\n "action": "Submit"\n}' + vsp._version = "1234" + vsp._identifier = "12345" + vsp.submit() + mock_send.assert_called_once_with("PUT", "Submit Vsp", 'https://sdc.api.fe.simpledemo.onap.org:30207/sdc1/feProxy/onboarding-api/v1.0/vendor-software-products/12345/versions/1234/actions', data=expected_data) + +@pytest.mark.parametrize("status", [const.DRAFT, const.COMMITED, const.UPLOADED, const.VALIDATED]) +@mock.patch.object(Vsp, 'load_status') +@mock.patch.object(Vsp, 'send_message') +def test_create_csar_not_Certified(mock_send, mock_status, status): + """Do nothing if not created.""" + vsp = Vsp() + vsp._status = status + vsp.create_csar() + mock_send.assert_not_called() + assert vsp.csar_uuid == None + +@mock.patch.object(Vsp, 'load_status') +@mock.patch.object(Vsp, 'send_message') +def test_create_csar_not_OK(mock_send, mock_status): + """Don't update status if submission NOK.""" + vsp = Vsp() + vsp._status = const.CERTIFIED + mock_send.return_value = {} + expected_data = '{\n\n "action": "Create_Package"\n}' + vsp._version = "1234" + vsp._identifier = "12345" + vsp.create_csar() + mock_send.assert_called_once_with("PUT", "Create_Package Vsp", 'https://sdc.api.fe.simpledemo.onap.org:30207/sdc1/feProxy/onboarding-api/v1.0/vendor-software-products/12345/versions/1234/actions', data=expected_data) + assert vsp.csar_uuid == None + +@mock.patch.object(Vsp, 'load_status') +@mock.patch.object(Vsp, 'send_message') +def test_create_csar_OK(mock_send, mock_status): + """Don't update status if submission NOK.""" + vsp = Vsp() + vsp._status = const.CERTIFIED + result = requests.Response() + result.status_code = 201 + result._content = json.dumps({'packageId': "64"}).encode('UTF-8') + mock_send.return_value = result + expected_data = '{\n\n "action": "Create_Package"\n}' + vsp._version = "1234" + vsp._identifier = "12345" + vsp.create_csar() + mock_send.assert_called_once_with("PUT", "Create_Package Vsp", 'https://sdc.api.fe.simpledemo.onap.org:30207/sdc1/feProxy/onboarding-api/v1.0/vendor-software-products/12345/versions/1234/actions', data=expected_data) + assert vsp.csar_uuid == "64" + +@pytest.mark.parametrize("status", [const.DRAFT, const.CERTIFIED, const.UPLOADED, const.COMMITED]) +@mock.patch.object(Vsp, 'load_status') +@mock.patch.object(Vsp, 'send_message') +def test_commit_not_Validated(mock_send, mock_status, status): + """Do nothing if not created.""" + vsp = Vsp() + vsp._status = status + vsp.commit() + mock_send.assert_not_called() + +@mock.patch.object(Vsp, 'load_status') +@mock.patch.object(Vsp, 'send_message') +def test_commit_OK(mock_send, mock_status): + """Don't update status if submission NOK.""" + vsp = Vsp() + vsp._status = const.VALIDATED + expected_data = '{\n\n "commitRequest":{"message":"ok"},\n\n "action": "Commit"\n}' + vsp._version = "1234" + vsp._identifier = "12345" + vsp.commit() + mock_send.assert_called_once_with("PUT", "Commit Vsp", 'https://sdc.api.fe.simpledemo.onap.org:30207/sdc1/feProxy/onboarding-api/v1.0/items/12345/versions/1234/actions', data=expected_data) + +@pytest.mark.parametrize("status", [const.CERTIFIED, const.COMMITED, const.UPLOADED, const.VALIDATED]) +@mock.patch.object(Vsp, 'load_status') +@mock.patch.object(Vsp, 'send_message') +def test_upload_not_Draft(mock_send, mock_status, status): + """Do nothing if not created.""" + vsp = Vsp() + vsp._status = status + vsp.upload_package('data') + mock_send.assert_not_called() + +@mock.patch.object(Vsp, 'load_status') +@mock.patch.object(Vsp, 'send_message') +def test_upload_not_OK(mock_send, mock_status): + """Don't update status if submission NOK.""" + vsp = Vsp() + vsp._status = const.DRAFT + mock_send.return_value = None + vsp._version = "1234" + vsp._identifier = "12345" + vsp.upload_package('data') + mock_send.assert_called_once_with('POST', 'upload ZIP for Vsp', "https://sdc.api.fe.simpledemo.onap.org:30207/sdc1/feProxy/onboarding-api/v1.0/vendor-software-products/12345/versions/1234/orchestration-template-candidate", files={'upload': 'data'}, headers={'Accept': 'application/json', 'USER_ID': 'cs0008', 'Authorization': 'Basic YWFpOktwOGJKNFNYc3pNMFdYbGhhazNlSGxjc2UyZ0F3ODR2YW9HR21KdlV5MlU=', 'X-ECOMP-InstanceID': 'onapsdk', 'Accept-Encoding': 'gzip, deflate'}) + +@mock.patch.object(Vsp, 'load_status') +@mock.patch.object(Vsp, 'send_message') +def test_upload_error_in_response(mock_send, mock_status): + """Don't update status if submission NOK.""" + vsp = Vsp() + vsp._status = const.DRAFT + mock_send.return_value = mock.MagicMock(text='{"status": "Failure"}') + vsp._version = "1234" + vsp._identifier = "12345" + with pytest.raises(APIError): + vsp.upload_package('data') + mock_send.assert_called_once_with('POST', 'upload ZIP for Vsp', "https://sdc.api.fe.simpledemo.onap.org:30207/sdc1/feProxy/onboarding-api/v1.0/vendor-software-products/12345/versions/1234/orchestration-template-candidate", files={'upload': 'data'}, headers={'Accept': 'application/json', 'USER_ID': 'cs0008', 'Authorization': 'Basic YWFpOktwOGJKNFNYc3pNMFdYbGhhazNlSGxjc2UyZ0F3ODR2YW9HR21KdlV5MlU=', 'X-ECOMP-InstanceID': 'onapsdk', 'Accept-Encoding': 'gzip, deflate'}) + +@mock.patch.object(Vsp, 'load_status') +@mock.patch.object(Vsp, 'send_message') +def test_upload_OK(mock_send, mock_status): + """Don't update status if submission NOK.""" + vsp = Vsp() + vsp._status = const.DRAFT + mock_send.return_value = mock.MagicMock(text='{"status": "Success"}') + vsp._version = "1234" + vsp._identifier = "12345" + vsp.upload_package('data') + mock_send.assert_called_once_with('POST', 'upload ZIP for Vsp', "https://sdc.api.fe.simpledemo.onap.org:30207/sdc1/feProxy/onboarding-api/v1.0/vendor-software-products/12345/versions/1234/orchestration-template-candidate", files={'upload': 'data'}, headers={'Accept': 'application/json', 'USER_ID': 'cs0008', 'Authorization': 'Basic YWFpOktwOGJKNFNYc3pNMFdYbGhhazNlSGxjc2UyZ0F3ODR2YW9HR21KdlV5MlU=', 'X-ECOMP-InstanceID': 'onapsdk', 'Accept-Encoding': 'gzip, deflate'}) + +@pytest.mark.parametrize("status", [const.CERTIFIED, const.COMMITED, const.DRAFT, const.VALIDATED]) +@mock.patch.object(Vsp, 'load_status') +@mock.patch.object(Vsp, 'send_message_json') +def test_validate_not_Draft(mock_send, mock_status, status): + """Do nothing if not created.""" + vsp = Vsp() + vsp._status = status + vsp.validate() + mock_send.assert_not_called() + +@mock.patch.object(Vsp, 'load_status') +@mock.patch.object(Vsp, 'send_message_json') +def test_validate_not_OK(mock_send, mock_status): + """Don't update status if submission NOK.""" + vsp = Vsp() + vsp._status = const.UPLOADED + mock_send.return_value = {} + vsp._version = "1234" + vsp._identifier = "12345" + vsp.validate() + mock_send.assert_called_once_with('PUT', 'Validate artifacts for Vsp', 'https://sdc.api.fe.simpledemo.onap.org:30207/sdc1/feProxy/onboarding-api/v1.0/vendor-software-products/12345/versions/1234/orchestration-template-candidate/process') + +@mock.patch.object(Vsp, 'load_status') +@mock.patch.object(Vsp, 'send_message_json') +def test_validate_not_success(mock_send, mock_status): + """Don't update status if submission NOK.""" + vsp = Vsp() + vsp._status = const.UPLOADED + mock_send.return_value = {'status': 'not_success'} + vsp._version = "1234" + vsp._identifier = "12345" + vsp.validate() + mock_send.assert_called_once_with('PUT', 'Validate artifacts for Vsp', 'https://sdc.api.fe.simpledemo.onap.org:30207/sdc1/feProxy/onboarding-api/v1.0/vendor-software-products/12345/versions/1234/orchestration-template-candidate/process') + +@mock.patch.object(Vsp, 'load_status') +@mock.patch.object(Vsp, 'send_message_json') +def test_validate_OK(mock_send, mock_status): + """Don't update status if submission NOK.""" + vsp = Vsp() + vsp._status = const.UPLOADED + mock_send.return_value = {'status': 'Success'} + vsp._version = "1234" + vsp._identifier = "12345" + vsp.validate() + mock_send.assert_called_once_with('PUT', 'Validate artifacts for Vsp', 'https://sdc.api.fe.simpledemo.onap.org:30207/sdc1/feProxy/onboarding-api/v1.0/vendor-software-products/12345/versions/1234/orchestration-template-candidate/process') + +@mock.patch.object(Vsp, 'create_csar') +@mock.patch.object(Vsp, 'submit') +@mock.patch.object(Vsp, 'commit') +@mock.patch.object(Vsp, 'validate') +@mock.patch.object(Vsp, 'upload_package') +@mock.patch.object(Vsp, 'create') +@mock.patch.object(Vsp, 'created') +def test_onboard_new_vsp_no_vendor(mock_created, mock_create, mock_upload_package, + mock_validate, mock_commit, mock_submit, + mock_create_csar): + mock_created.return_value = False + getter_mock = mock.Mock(wraps=Vsp.status.fget) + mock_status = Vsp.status.getter(getter_mock) + with mock.patch.object(Vsp, 'status', mock_status): + getter_mock.side_effect = [None, const.APPROVED, const.APPROVED, + const.APPROVED, const.APPROVED, + const.APPROVED, None] + vsp = Vsp() + with pytest.raises(ParameterError): + vsp.onboard() + mock_create.assert_not_called() + mock_upload_package.assert_not_called() + mock_validate.assert_not_called() + mock_commit.assert_not_called() + mock_submit.assert_not_called() + mock_create_csar.assert_not_called() + + +@mock.patch.object(Vsp, 'create_csar') +@mock.patch.object(Vsp, 'submit') +@mock.patch.object(Vsp, 'commit') +@mock.patch.object(Vsp, 'validate') +@mock.patch.object(Vsp, 'upload_package') +@mock.patch.object(Vsp, 'create') +@mock.patch.object(Vsp, 'created') +def test_onboard_new_vsp(mock_created, mock_create, mock_upload_package, + mock_validate, mock_commit, mock_submit, + mock_create_csar): + mock_created.return_value = False + getter_mock = mock.Mock(wraps=Vsp.status.fget) + mock_status = Vsp.status.getter(getter_mock) + with mock.patch.object(Vsp, 'status', mock_status): + getter_mock.side_effect = [None, const.APPROVED, const.APPROVED, + const.APPROVED, const.APPROVED, + const.APPROVED, None] + vendor = Vendor() + vsp = Vsp(vendor=vendor) + vsp.onboard() + mock_create.assert_called_once() + mock_upload_package.assert_not_called() + mock_validate.assert_not_called() + mock_commit.assert_not_called() + mock_submit.assert_not_called() + mock_create_csar.assert_not_called() + +@mock.patch.object(Vsp, 'create_csar') +@mock.patch.object(Vsp, 'submit') +@mock.patch.object(Vsp, 'commit') +@mock.patch.object(Vsp, 'validate') +@mock.patch.object(Vsp, 'upload_package') +@mock.patch.object(Vsp, 'create') +def test_onboard_vsp_upload_no_files(mock_create, mock_upload_package, + mock_validate, mock_commit, mock_submit, + mock_create_csar): + getter_mock = mock.Mock(wraps=Vsp.status.fget) + mock_status = Vsp.status.getter(getter_mock) + with mock.patch.object(Vsp, 'status', mock_status): + getter_mock.side_effect = [const.DRAFT, const.DRAFT, const.APPROVED, + const.APPROVED, const.APPROVED, const.APPROVED, + const.APPROVED, None] + vsp = Vsp() + with pytest.raises(ParameterError): + vsp.onboard() + mock_create.assert_not_called() + mock_upload_package.assert_not_called() + mock_validate.assert_not_called() + mock_commit.assert_not_called() + mock_submit.assert_not_called() + mock_create_csar.assert_not_called() + +@mock.patch.object(Vsp, 'create_csar') +@mock.patch.object(Vsp, 'submit') +@mock.patch.object(Vsp, 'commit') +@mock.patch.object(Vsp, 'validate') +@mock.patch.object(Vsp, 'upload_package') +@mock.patch.object(Vsp, 'create') +def test_onboard_vsp_upload_package(mock_create, mock_upload_package, + mock_validate, mock_commit, mock_submit, + mock_create_csar): + getter_mock = mock.Mock(wraps=Vsp.status.fget) + mock_status = Vsp.status.getter(getter_mock) + with mock.patch.object(Vsp, 'status', mock_status): + getter_mock.side_effect = [const.DRAFT, const.APPROVED, None] + vsp = Vsp(package="yes") + vsp.onboard() + mock_create.assert_not_called() + mock_upload_package.assert_called_once_with("yes") + mock_validate.assert_not_called() + mock_commit.assert_not_called() + mock_submit.assert_not_called() + mock_create_csar.assert_not_called() + +@mock.patch.object(Vsp, 'create_csar') +@mock.patch.object(Vsp, 'submit') +@mock.patch.object(Vsp, 'commit') +@mock.patch.object(Vsp, 'validate') +@mock.patch.object(Vsp, 'upload_package') +@mock.patch.object(Vsp, 'create') +def test_onboard_new_vsp_validate(mock_create, mock_upload_package, + mock_validate, mock_commit, mock_submit, + mock_create_csar): + getter_mock = mock.Mock(wraps=Vsp.status.fget) + mock_status = Vsp.status.getter(getter_mock) + with mock.patch.object(Vsp, 'status', mock_status): + getter_mock.side_effect = [const.UPLOADED, const.APPROVED, None] + vsp = Vsp() + vsp.onboard() + mock_create.assert_not_called() + mock_upload_package.assert_not_called() + mock_validate.assert_called_once() + mock_commit.assert_not_called() + mock_submit.assert_not_called() + mock_create_csar.assert_not_called() + +@mock.patch.object(Vsp, 'create_csar') +@mock.patch.object(Vsp, 'submit') +@mock.patch.object(Vsp, 'commit') +@mock.patch.object(Vsp, 'validate') +@mock.patch.object(Vsp, 'upload_package') +@mock.patch.object(Vsp, 'create') +def test_onboard_new_vsp_commit(mock_create, mock_upload_package, + mock_validate, mock_commit, mock_submit, + mock_create_csar): + getter_mock = mock.Mock(wraps=Vsp.status.fget) + mock_status = Vsp.status.getter(getter_mock) + with mock.patch.object(Vsp, 'status', mock_status): + getter_mock.side_effect = [const.VALIDATED, const.APPROVED, None] + vsp = Vsp() + vsp.onboard() + mock_create.assert_not_called() + mock_upload_package.assert_not_called() + mock_validate.assert_not_called() + mock_commit.assert_called_once() + mock_submit.assert_not_called() + mock_create_csar.assert_not_called() + +@mock.patch.object(Vsp, 'create_csar') +@mock.patch.object(Vsp, 'submit') +@mock.patch.object(Vsp, 'commit') +@mock.patch.object(Vsp, 'validate') +@mock.patch.object(Vsp, 'upload_package') +@mock.patch.object(Vsp, 'create') +def test_onboard_new_vsp_submit(mock_create, mock_upload_package, + mock_validate, mock_commit, mock_submit, + mock_create_csar): + getter_mock = mock.Mock(wraps=Vsp.status.fget) + mock_status = Vsp.status.getter(getter_mock) + with mock.patch.object(Vsp, 'status', mock_status): + getter_mock.side_effect = [const.COMMITED, const.APPROVED, None] + vsp = Vsp() + vsp.onboard() + mock_create.assert_not_called() + mock_upload_package.assert_not_called() + mock_validate.assert_not_called() + mock_commit.assert_not_called() + mock_submit.assert_called_once() + mock_create_csar.assert_not_called() + +@mock.patch.object(Vsp, 'create_csar') +@mock.patch.object(Vsp, 'submit') +@mock.patch.object(Vsp, 'commit') +@mock.patch.object(Vsp, 'validate') +@mock.patch.object(Vsp, 'upload_package') +@mock.patch.object(Vsp, 'create') +def test_onboard_new_vsp_create_csar(mock_create, + mock_upload_package, mock_validate, + mock_commit, mock_submit, + mock_create_csar): + getter_mock = mock.Mock(wraps=Vsp.status.fget) + mock_status = Vsp.status.getter(getter_mock) + with mock.patch.object(Vsp, 'status', mock_status): + getter_mock.side_effect = [const.CERTIFIED, const.APPROVED, None] + vsp = Vsp() + vsp.onboard() + mock_create.assert_not_called() + mock_upload_package.assert_not_called() + mock_validate.assert_not_called() + mock_commit.assert_not_called() + mock_submit.assert_not_called() + mock_create_csar.assert_called_once() + +@mock.patch.object(Vsp, 'create_csar') +@mock.patch.object(Vsp, 'submit') +@mock.patch.object(Vsp, 'commit') +@mock.patch.object(Vsp, 'validate') +@mock.patch.object(Vsp, 'upload_package') +@mock.patch.object(Vsp, 'create') +@mock.patch.object(Vsp, 'created') +@mock.patch.object(Vsp, 'load') +def test_onboard_whole_vsp(mock_load, mock_created, mock_create, + mock_upload_package, mock_validate, + mock_commit, mock_submit, + mock_create_csar): + getter_mock = mock.Mock(wraps=Vsp.status.fget) + mock_status = Vsp.status.getter(getter_mock) + with mock.patch.object(Vsp, 'status', mock_status): + getter_mock.side_effect = [None, const.DRAFT, const.UPLOADED, const.VALIDATED, + const.COMMITED, const.CERTIFIED, None] + vendor = Vendor() + vsp = Vsp(vendor=vendor, package="yes") + vsp.onboard() + mock_create.assert_called_once() + mock_upload_package.assert_called_once() + mock_validate.assert_called_once() + mock_commit.assert_called_once() + mock_submit.assert_called_once() + mock_create_csar.assert_called_once() + + +@mock.patch.object(Vsp, "status", new_callable=mock.PropertyMock) +@mock.patch.object(Vsp, '_upload_action') +def test_update_package(mock_upload, mock_status): + mock_status.return_value = const.DRAFT + vsp = Vsp(vendor=mock.MagicMock()) + vsp.update_package("new") + mock_upload.assert_not_called() + mock_status.return_value = const.COMMITED + vsp.update_package("new") + mock_upload.assert_called_once() + + +@mock.patch.object(Vsp, "send_message_json") +@mock.patch.object(Vsp, 'load') +def test_create_new_version(mock_load, mock_send): + vsp = Vsp(vendor=mock.MagicMock()) + vsp._identifier = "1232" + vsp._version = "4321" + vsp.create_new_version() + mock_load.assert_called_once() + mock_send.assert_called_once_with("POST", + "Create new VSP version", + "https://sdc.api.fe.simpledemo.onap.org:30207/sdc1/feProxy/onboarding-api/v1.0/items/1232/versions/4321", + data='{"creationMethod": "major", "description": "New VSP version"}') @@ -0,0 +1,14 @@ +[tox] +envlist = py37,py38,py39,py310,pylint,pydocstyle + +[testenv] +commands = pytest tests/ +deps = -rtest-requirements.txt +setenv = PYTHONPATH = {toxinidir}/src + +[testenv:pylint] +commands = pylint src/ +basepython = python3.8 + +[testenv:pydocstyle] +commands = pydocstyle src/ diff --git a/upload-requirements.txt b/upload-requirements.txt new file mode 100644 index 0000000..9ab57ec --- /dev/null +++ b/upload-requirements.txt @@ -0,0 +1,3 @@ +twine==3.2.0 +wheel==0.35.1 +setuptools==50.3.0 |