From 2b0e2d72595a5e25bd8f785138416d12829fbd64 Mon Sep 17 00:00:00 2001 From: magnussonl Date: Tue, 4 Feb 2020 10:52:46 +0100 Subject: [PATCH] Initial commit to gerrit repo Made non-functional change to trigger Jenkins Add/update license headers Dockerfile modification Corrected Dockerfile faults Dockerfile update Yet another Dockerfile update Support for placement without vld:s Change-Id: I63c1733656f682233c96f6bcadeac1a2765ed085 Signed-off-by: magnussonl --- .gitignore | 4 +- Dockerfile | 24 +- Jenkinsfile | 14 + MANIFEST.in | 20 + Makefile | 28 + README.md | 50 +- debian/python3-osm-pla.postinst | 29 + devops-stages/stage-archive.sh | 10 +- devops-stages/stage-build.sh | 2 +- devops-stages/stage-test.sh | 2 +- docker/Dockerfile | 72 ++ docker/integrationtest.Dockerfile | 74 ++ docker/scripts/start.sh | 69 ++ docs/img/PLA_SW_Arch.png | Bin 0 -> 20482 bytes docs/img/gui_instantiate.png | Bin 0 -> 44651 bytes docs/img/nsd_w_constraints.png | Bin 0 -> 72884 bytes docs/img/osm_gui_ns_create.png | Bin 0 -> 37425 bytes docs/pla_design_spec.md | 93 +++ docs/pla_users_guide.md | 160 ++++ osm_pla/__init__.py | 17 + osm_pla/cmd/__init__.py | 16 + osm_pla/cmd/pla_server.py | 49 ++ osm_pla/config/config.py | 65 ++ osm_pla/config/pla.yaml | 30 + osm_pla/placement/__init__.py | 14 + osm_pla/placement/macros.j2 | 105 +++ osm_pla/placement/mznplacement.py | 254 ++++++ osm_pla/placement/osm_pla_dynamic_template.j2 | 40 + osm_pla/server/server.py | 176 ++++ osm_pla/test/__init__.py | 17 + ...orrupt_pil_endpoints_config_unittest1.yaml | 94 +++ osm_pla/test/not_yaml_conformant.yaml | 94 +++ osm_pla/test/nsd_unittest1.yaml | 66 ++ osm_pla/test/nsd_unittest2.yaml | 62 ++ osm_pla/test/nsd_unittest3.yaml | 66 ++ osm_pla/test/nsd_unittest4.yaml | 35 + .../test/nsd_unittest_no_vld_constraints.yaml | 56 ++ osm_pla/test/pil_price_list.yaml | 60 ++ osm_pla/test/pil_price_list_rel7_webinar.yaml | 47 ++ osm_pla/test/pil_unittest1.yaml | 59 ++ osm_pla/test/pil_unittest2.yaml | 55 ++ osm_pla/test/test_five_nsd.yaml | 104 +++ osm_pla/test/test_mznModelGenerator.py | 701 ++++++++++++++++ osm_pla/test/test_mznPlacementConductor.py | 218 +++++ osm_pla/test/test_mznmodels.py | 631 ++++++++++++++ osm_pla/test/test_nsPlacementDataFactory.py | 777 ++++++++++++++++++ osm_pla/test/test_server.py | 517 ++++++++++++ osm_pla/test/vnf_price_list.yaml | 84 ++ osm_pla/test/vnf_price_list_more_vims.yaml | 46 ++ osm_pla/test/vnf_price_list_rel7_webinar.yaml | 98 +++ requirements.txt | 21 + setup.cfg | 38 + setup.py | 65 ++ stdeb.cfg | 17 + test-requirements.txt | 18 + tox.ini | 50 ++ 56 files changed, 5473 insertions(+), 40 deletions(-) create mode 100755 MANIFEST.in create mode 100755 Makefile create mode 100755 debian/python3-osm-pla.postinst create mode 100755 docker/Dockerfile create mode 100755 docker/integrationtest.Dockerfile create mode 100755 docker/scripts/start.sh create mode 100644 docs/img/PLA_SW_Arch.png create mode 100644 docs/img/gui_instantiate.png create mode 100644 docs/img/nsd_w_constraints.png create mode 100644 docs/img/osm_gui_ns_create.png create mode 100644 docs/pla_design_spec.md create mode 100644 docs/pla_users_guide.md create mode 100755 osm_pla/__init__.py create mode 100755 osm_pla/cmd/__init__.py create mode 100755 osm_pla/cmd/pla_server.py create mode 100644 osm_pla/config/config.py create mode 100644 osm_pla/config/pla.yaml create mode 100755 osm_pla/placement/__init__.py create mode 100644 osm_pla/placement/macros.j2 create mode 100755 osm_pla/placement/mznplacement.py create mode 100644 osm_pla/placement/osm_pla_dynamic_template.j2 create mode 100644 osm_pla/server/server.py create mode 100644 osm_pla/test/__init__.py create mode 100644 osm_pla/test/corrupt_pil_endpoints_config_unittest1.yaml create mode 100644 osm_pla/test/not_yaml_conformant.yaml create mode 100644 osm_pla/test/nsd_unittest1.yaml create mode 100644 osm_pla/test/nsd_unittest2.yaml create mode 100644 osm_pla/test/nsd_unittest3.yaml create mode 100644 osm_pla/test/nsd_unittest4.yaml create mode 100644 osm_pla/test/nsd_unittest_no_vld_constraints.yaml create mode 100644 osm_pla/test/pil_price_list.yaml create mode 100644 osm_pla/test/pil_price_list_rel7_webinar.yaml create mode 100644 osm_pla/test/pil_unittest1.yaml create mode 100644 osm_pla/test/pil_unittest2.yaml create mode 100644 osm_pla/test/test_five_nsd.yaml create mode 100644 osm_pla/test/test_mznModelGenerator.py create mode 100644 osm_pla/test/test_mznPlacementConductor.py create mode 100644 osm_pla/test/test_mznmodels.py create mode 100644 osm_pla/test/test_nsPlacementDataFactory.py create mode 100644 osm_pla/test/test_server.py create mode 100644 osm_pla/test/vnf_price_list.yaml create mode 100644 osm_pla/test/vnf_price_list_more_vims.yaml create mode 100644 osm_pla/test/vnf_price_list_rel7_webinar.yaml create mode 100755 requirements.txt create mode 100755 setup.cfg create mode 100755 setup.py create mode 100644 stdeb.cfg create mode 100755 test-requirements.txt create mode 100644 tox.ini diff --git a/.gitignore b/.gitignore index 302f4e1..9007d45 100644 --- a/.gitignore +++ b/.gitignore @@ -31,7 +31,7 @@ deb_dist pool dist -#local stuff files that end in ".local" or folders called "local" +#local stuff, e.g. files that end in ".local" or folders called "local" *.local local - +venv diff --git a/Dockerfile b/Dockerfile index 54f0fd5..234679c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,25 @@ # See the License for the specific language governing permissions and # limitations under the License. -FROM ubuntu:18.04 +FROM ubuntu:16.04 -RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get -y install git \ - make python3 debhelper python3-setuptools apt-utils +RUN apt-get update && \ + DEBIAN_FRONTEND=noninteractive apt-get -y install git tox make python3 python3-pip python-all && \ + DEBIAN_FRONTEND=noninteractive apt-get -y install python3-all debhelper python3-setuptools apt-utils libgl1-mesa-glx && \ + DEBIAN_FRONTEND=noninteractive pip3 install -U setuptools setuptools-version-command stdeb + +ADD https://github.com/MiniZinc/MiniZincIDE/releases/download/2.4.2/MiniZincIDE-2.4.2-bundle-linux-x86_64.tgz /minizinc.tgz + +RUN tar -zxf /minizinc.tgz && \ + mv /MiniZincIDE-2.4.2-bundle-linux /minizinc + +RUN mkdir /entry_data \ + && mkdir /entry_data/mzn-lib \ + && ln -s /entry_data/mzn-lib /minizinc/share/minizinc/exec + +ENV FZNEXEC "/entry_data/fzn-exec" +ENV PATH "/minizinc/bin:${PATH}" + +RUN mkdir /placement +COPY ./osm_pla/test/pil_price_list.yaml /placement/. +COPY ./osm_pla/test/vnf_price_list.yaml /placement/. diff --git a/Jenkinsfile b/Jenkinsfile index 5915574..40e35aa 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,3 +1,17 @@ +/* + 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. +*/ properties([ parameters([ string(defaultValue: env.BRANCH_NAME, description: '', name: 'GERRIT_BRANCH'), diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100755 index 0000000..22675f6 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,20 @@ +# Copyright 2020 ArctosLabs Scandinavia AB +# **************************************** +# This file is part of OSM Placement module +# All Rights Reserved to Intel Corporation + +# 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. +include requirements.txt +include README.md +recursive-include osm_pla *.py *.sh *.yaml +recursive-include devops-stages * diff --git a/Makefile b/Makefile new file mode 100755 index 0000000..7e82f44 --- /dev/null +++ b/Makefile @@ -0,0 +1,28 @@ +# Copyright 2020 ArctosLabs Scandinava AB +# ************************************************************* + +# This file is part of OSM Placement module +# All Rights Reserved to ArctosLabs Scandinavia AB + +# 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. +## + +all: clean package + +clean: + rm -rf dist deb_dist osm_pla-*.tar.gz osm_pla.egg-info .eggs + +package: + python3 setup.py --command-packages=stdeb.command sdist_dsc + cp debian/python3-osm-pla.postinst deb_dist/osm-pla*/debian + cd deb_dist/osm-pla*/ && dpkg-buildpackage -rfakeroot -uc -us diff --git a/README.md b/README.md index e0f6898..5fa8e3d 100644 --- a/README.md +++ b/README.md @@ -14,55 +14,40 @@ implied. See the License for the specific language governing permissions and limitations under the License --> -# Project Title +# OSM PLA -One Paragraph of project description goes here +The PLA module provides computation of optimal placement of xNFs over VIMs by matching NS specific requirements to infrastructure availability and run-time metrics, while considering cost of compute/network. ## Getting Started -These instructions will get you a copy of the project up and running on your local machine for development and testing purposes. See deployment for notes on how to deploy the project on a live system. +Please refer to the [PLA User's Guide](docs/pla_users_guide.md) for a description on how to enable and configure the placement functionality. -### Prerequisites -What things you need to install the software and how to install them - -``` -Give examples -``` - -### Installing - -A step by step series of examples that tell you how to get a development env running - -Say what the step will be - -``` -Give the example -``` - -And repeat - -``` -until finished -``` +## Running the tests -End with an example of getting some data out of the system or using it for a little demo +The preferred method to run the PLA unit test is to use tox. -## Running the tests +`$ tox` -Explain how to run the automated tests for this system +Please note that some of the unit test modules have dependencies to Minizinc, e.g. test_mznmodels.py and test_mznPlacementConductor.py. +If these tests are to be performed outside a PLA container context, like .e.g. from CLI or from within an IDE, setup the environment as follows (linux example): ``` -Give an example +$ sudo snap install minizinc --classic +$ sudo mkdir -p /minizinc/bin +$ sudo ln -s /snap/bin/minizinc /minizinc/bin/minizinc ``` ## Deployment -Add additional notes about how to deploy this on a live system +PLA is an optional module in OSM. It is installed together with OSM by adding ``--pla`` to the install script. + +`$ ./install_osm.sh --pla` ## Built With -* [Python](www.python.org/) - The language used +* [Python](www.python.org/) - the primary programming language for OSM +* [Minizinc](www.minizinc.org) - a free and open source constraint modelling language ## Contributing @@ -78,5 +63,6 @@ This project is licensed under the Apache2 License - see the [LICENSE.md](LICENS ## Acknowledgments -* **Billie Thompson** - *Initial work* - [PurpleBooth](https://github.com/PurpleBooth) +* **Paolo Dragone** - *PyMzn, a python library that wraps and enhance Minizinc* - [pymzn](https://github.com/paolodragone/pymzn) +* **Billie Thompson** - *Initial work on README.md* - [PurpleBooth](https://github.com/PurpleBooth) diff --git a/debian/python3-osm-pla.postinst b/debian/python3-osm-pla.postinst new file mode 100755 index 0000000..bed12db --- /dev/null +++ b/debian/python3-osm-pla.postinst @@ -0,0 +1,29 @@ +#!/bin/bash + +## +# 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. +# +# For those usages not covered by the Apache License, Version 2.0 please +# contact with: OSM_TECH@list.etsi.org +## + +echo "POST INSTALL OSM-PLA" +# Currently it is not needed pip3 installation +# echo "Installing python dependencies via pip..." +# pip3 install pip==9.0.3 +# pip3 install --user aiokafka + +#Creation of log folder +mkdir -p /var/log/osm + +# systemctl enable osm-pla.service diff --git a/devops-stages/stage-archive.sh b/devops-stages/stage-archive.sh index 013953f..831c8c8 100755 --- a/devops-stages/stage-archive.sh +++ b/devops-stages/stage-archive.sh @@ -12,4 +12,12 @@ # implied. # See the License for the specific language governing permissions and # limitations under the License. -echo "ARCHIVE" +MDG=PLA +rm -rf pool +rm -rf dists +mkdir -p pool/$MDG +mv deb_dist/*.deb pool/$MDG/ +mkdir -p dists/unstable/$MDG/binary-amd64/ +apt-ftparchive packages pool/$MDG > dists/unstable/$MDG/binary-amd64/Packages +gzip -9fk dists/unstable/$MDG/binary-amd64/Packages +echo "dists/**,pool/$MDG/*.deb" diff --git a/devops-stages/stage-build.sh b/devops-stages/stage-build.sh index b58d1f3..9c2b16b 100755 --- a/devops-stages/stage-build.sh +++ b/devops-stages/stage-build.sh @@ -12,4 +12,4 @@ # implied. # See the License for the specific language governing permissions and # limitations under the License. -echo "BUILD" +make diff --git a/devops-stages/stage-test.sh b/devops-stages/stage-test.sh index 337cf59..69c6fcd 100755 --- a/devops-stages/stage-test.sh +++ b/devops-stages/stage-test.sh @@ -11,4 +11,4 @@ # implied. # See the License for the specific language governing permissions and # limitations under the License. -echo "TEST" +tox diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100755 index 0000000..1b4a01c --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,72 @@ +# Copyright 2020 ArctosLabs Scandinavia AB +# ************************************************************* + +# This file is part of OSM Placement module +# All Rights Reserved to ArctosLabs Scandinavia AB + +# 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 ubuntu:18.04 + +LABEL authors="Martin Björklund, Lars-Göran Magnusson" + +RUN apt-get --yes update \ + && apt-get --yes install git python3 python3-pip libgl1-mesa-glx \ + && pip3 install pip==9.0.3 + +COPY requirements.txt /pla/requirements.txt + +RUN pip3 install -r /pla/requirements.txt + +ADD https://github.com/MiniZinc/MiniZincIDE/releases/download/2.3.1/MiniZincIDE-2.3.1-bundle-linux-x86_64.tgz /minizinc.tgz +#COPY MiniZincIDE-2.3.1-bundle-linux-x86_64.tgz /minizinc.tgz + +RUN tar -zxf /minizinc.tgz && \ + mv /MiniZincIDE-2.3.1-bundle-linux /minizinc + +RUN mkdir /entry_data \ + && mkdir /entry_data/mzn-lib \ + && ln -s /entry_data/mzn-lib /minizinc/share/minizinc/exec + +COPY . /pla + +RUN pip3 install /pla + +RUN mkdir /placement + +ENV OSMPLA_MESSAGE_DRIVER kafka +ENV OSMPLA_MESSAGE_HOST kafka +ENV OSMPLA_MESSAGE_PORT 9092 + +ENV OSMPLA_DATABASE_DRIVER mongo +ENV OSMPLA_DATABASE_URI mongodb://mongo:27017 + +ENV OSMPLA_SQL_DATABASE_URI sqlite:///pla_sqlite.db +ENV OSMPLA_GLOBAL_REQUEST_TIMEOUT 10 +ENV OSMPLA_GLOBAL_LOGLEVEL INFO +ENV OSMPLA_VCA_HOST localhost +ENV OSMPLA_VCA_SECRET secret +ENV OSMPLA_VCA_USER admin +ENV OSMPLA_DATABASE_COMMONKEY changeme + +ENV FZNEXEC "/entry_data/fzn-exec" +ENV PATH "/minizinc/bin:${PATH}" +ENV LD_LIBRARY_PATH "/minizinc/lib:${LD_LIBRARY_PATH}" + +EXPOSE 1234 + +#HEALTHCHECK --interval=5s --timeout=2s --retries=12 \ +# CMD osm-pla-healthcheck || exit 1 + +CMD /bin/bash pla/docker/scripts/start.sh + +#WORKDIR /minizinc diff --git a/docker/integrationtest.Dockerfile b/docker/integrationtest.Dockerfile new file mode 100755 index 0000000..a4cedf6 --- /dev/null +++ b/docker/integrationtest.Dockerfile @@ -0,0 +1,74 @@ +# Copyright 2020 ArctosLabs Scandinavia AB +# ************************************************************* + +# This file is part of OSM Placement module +# All Rights Reserved to ArctosLabs Scandinavia AB + +# 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 ubuntu:18.04 + +LABEL authors="Martin Björklund, Lars-Göran Magnusson" + +RUN apt-get --yes update \ + && apt-get --yes install git python3 python3-pip libgl1-mesa-glx \ + && pip3 install pip==9.0.3 + +COPY requirements.txt /pla/requirements.txt + +RUN pip3 install -r /pla/requirements.txt + +ADD https://github.com/MiniZinc/MiniZincIDE/releases/download/2.3.1/MiniZincIDE-2.3.1-bundle-linux-x86_64.tgz /minizinc.tgz +#COPY MiniZincIDE-2.3.1-bundle-linux-x86_64.tgz /minizinc.tgz + +RUN tar -zxf /minizinc.tgz && \ + mv /MiniZincIDE-2.3.1-bundle-linux /minizinc + +RUN mkdir /entry_data \ + && mkdir /entry_data/mzn-lib \ + && ln -s /entry_data/mzn-lib /minizinc/share/minizinc/exec + +COPY . /pla + +RUN pip3 install /pla + +RUN mkdir /placement +COPY ./osm_pla/test/pil_price_list.yaml /placement/. +COPY ./osm_pla/test/vnf_price_list.yaml /placement/. + +ENV OSMPLA_MESSAGE_DRIVER kafka +ENV OSMPLA_MESSAGE_HOST kafka +ENV OSMPLA_MESSAGE_PORT 9092 + +ENV OSMPLA_DATABASE_DRIVER mongo +ENV OSMPLA_DATABASE_URI mongodb://mongo:27017 + +ENV OSMPLA_SQL_DATABASE_URI sqlite:///pla_sqlite.db +ENV OSMPLA_GLOBAL_REQUEST_TIMEOUT 10 +ENV OSMPLA_GLOBAL_LOGLEVEL INFO +ENV OSMPLA_VCA_HOST localhost +ENV OSMPLA_VCA_SECRET secret +ENV OSMPLA_VCA_USER admin +ENV OSMPLA_DATABASE_COMMONKEY changeme + +ENV FZNEXEC "/entry_data/fzn-exec" +ENV PATH "/minizinc/bin:${PATH}" +ENV LD_LIBRARY_PATH "/minizinc/lib:${LD_LIBRARY_PATH}" + +EXPOSE 1234 + +#HEALTHCHECK --interval=5s --timeout=2s --retries=12 \ +# CMD osm-pla-healthcheck || exit 1 + +CMD /bin/bash pla/docker/scripts/start.sh + +#WORKDIR /minizinc diff --git a/docker/scripts/start.sh b/docker/scripts/start.sh new file mode 100755 index 0000000..66b96c8 --- /dev/null +++ b/docker/scripts/start.sh @@ -0,0 +1,69 @@ +# Copyright 2020 ArctosLabs Scandinavia AB +# +# 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. +DB_EXISTS="" + +max_attempts=120 +function wait_db(){ + db_host=$1 + db_port=$2 + attempt=0 + echo "Wait until $max_attempts seconds for MySQL mano Server ${db_host}:${db_port} " + while ! mysqladmin ping -h"$db_host" -P"$db_port" --silent; do + #wait 120 sec + if [ $attempt -ge $max_attempts ]; then + echo + echo "Can not connect to database ${db_host}:${db_port} during $max_attempts sec" + return 1 + fi + attempt=$[$attempt+1] + echo -n "." + sleep 1 + done + return 0 +} + +function is_db_created() { + db_host=$1 + db_port=$2 + db_user=$3 + db_pswd=$4 + db_name=$5 + + if mysqlshow -h"$db_host" -P"$db_port" -u"$db_user" -p"$db_pswd" | grep -v Wildcard | grep -q $db_name; then + echo "DB $db_name exists" + return 0 + else + echo "DB $db_name does not exist" + return 1 + fi +} + +if [[ $OSMPLA_SQL_DATABASE_URI == *'mysql'* ]]; then + DB_HOST=$(echo $OSMPLA_SQL_DATABASE_URI | sed -r 's|^.+://.+:.+@(.+):.*$|\1|') + DB_PORT=$(echo $OSMPLA_SQL_DATABASE_URI | sed -r 's|^.+://.*:([0-9]+).*$|\1|') + DB_USER=$(echo $OSMPLA_SQL_DATABASE_URI | sed -r 's|^.+://(.+):.+@.+$|\1|') + DB_PASSWORD=$(echo $OSMPLA_SQL_DATABASE_URI | sed -r 's|^.+://.+:(.+)@.*$|\1|') + DB_NAME=$(echo $OSMPLA_SQL_DATABASE_URI | sed -r 's|^.+://.+:.+@.+:.*/(\w+)(\?.*)?$|\1|') + + wait_db "$DB_HOST" "$DB_PORT" || exit 1 + + is_db_created "$DB_HOST" "$DB_PORT" "$DB_USER" "$DB_PASSWORD" "$DB_NAME" && DB_EXISTS="Y" + + if [ -z $DB_EXISTS ]; then + mysql -h"$DB_HOST" -P"$DB_PORT" -u"$DB_USER" -p"$DB_PASSWORD" --default_character_set utf8 -e "CREATE DATABASE $DB_NAME" + fi +fi + +osm-pla-server diff --git a/docs/img/PLA_SW_Arch.png b/docs/img/PLA_SW_Arch.png new file mode 100644 index 0000000000000000000000000000000000000000..99afc4b832977444e3a600b880c77405914778bc GIT binary patch literal 20482 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 F1OT&eto;B0 literal 0 HcmV?d00001 diff --git a/docs/img/gui_instantiate.png b/docs/img/gui_instantiate.png new file mode 100644 index 0000000000000000000000000000000000000000..4a76b285155cc5cfed17c4ab31513c705e4645e3 GIT binary patch literal 44651 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 NgQu&X%Q~loCIG;SXo&y- literal 0 HcmV?d00001 diff --git a/docs/img/nsd_w_constraints.png b/docs/img/nsd_w_constraints.png new file mode 100644 index 0000000000000000000000000000000000000000..395eb53ef770cb874f3a10a67333ce994428a97a GIT binary patch literal 72884 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

HvbW0 zlS0Sq&l`%=H@%BnzJI};y!|WZ?(0|l^UU}hWEWbTR?LN(T?-69ep3B9RqNaLnyM9s zzZ3WkB(wL2<@6_SonL(-mcQ`wDo>w`Jg2uREc~x8Cnj>mZ4Thv`FiTz^86z8caz;Z zPtWB@zV;;VO3C`q9apr=ecqKVQTv$NUM13!o-Gut$uYZFD}|PDRj((OvnhtMI3Mnj&^w!Ve|ZS^`5x6l=Ga)g+EP$=AYG$_;TOlU7Xfl zR^CuM74^fj3$hQl&U>Pf=2tOwwf)ZBMmpKQg(oHOuP^TCMMmlD5g zls~m_`#E8qnT|C_dDH(lM_)`_!BsZdnoWKgU+&Kbwc*X4UwO!+5$Qnpa~Q|-AaHJU}&rCxrz zwDRMNDQl;?ncZW)^uGFYqNbu%POC}&sf~gniwf8366MPjWo#@hV!r!{^z(I=#nV9>`AV zubk4qeZ!6&t&6pvzqse|E>6#8v(GJ~vk$JMb^3L!5}F@r1=^)8e`b13*fNzv4bofY zYpy&WHg$^ViZ?N!^ddjL);i-KKTpS=|f!N=a* zIc*a%Z|C*b-&J<}j@%Qfl4>oJ+?%=c)f(f|#+%&rsyII%0l&Mxzw&q=4x}t7< zsaFTj)``~N_DO8Ik!V$F%xSN~%-5sw>!erC?ROH+ZTkb#c*?v#zkaeTcEgUeulHU1 z>pnh@nyc)wq9&j5)QriUi*~3rRVdm#+OVeUN|DI>EuvaHJG0jW1gh>-UU$~=_l0R+ zeoQ)Dt7aAV%~7{3Q|XGe*^;;HFI0bj=byLbGT7dNQw(V2Q>*EjD1<(<2)n!fz-Lh^6iHV>2O z*88T+dU?mj+&^9_Pu_TSOy1YyrWJAzMSs3gD!2W0HT2!CJ?s{L>b!H6qN@JR$)xIjNV%l`qurubg`wyGxB)Dv%aajXU7HeyF5K- z@%`s_OSMUiXE+5fw0t$2x;yT#mXFcTr2e}Pe&_AU&;9(p>hC<3yHDSVonB-(v*?DZ zN8hxC=Wm~oT`Z6(Iqj@whg$2o%&FGWZOS?FzvJ`jILv-`oG{YKcy7XfnlJP4rbpB7 zERK&@^aiiElaUGMag-p^&?kDVJ(iPAB6%_t&2k^{9t@I)q48QB6oo!u}&be zpZVn7&kaxd6yAwKn=;7TXdgX%^6m}Csn;6Q4%x7&_AI@@c5n65WB(p8PBAys(f{-p zbpA`p8&fGzF>vl`p~p!}nQuRe_J4c&txi3JBTJPvJN_zPe22Nk@mWc6DgXc7JY8|i z&uWM6)fWb9Ce3(QaU66)jM}E8*NP{T4rMmmUwr-gr{@VX@nxBJCMtBeKbWXho;s)Q z-hoR_$=h=FKE2%0=J~GUbx=x>isA(0_K-KpX?K;2?thuN^x$2~mLQ{=)p9?|u1P%# zoN-a^^AUFGjUuln2k@NS4I0?t0}t#hFYob~`-!V*jr_cp-Gb~%P0wewPLkVk^YyPO zl0Kj6zUh|#D(wZ$&YWV`f4zN++@WPZ{G!%wX*)S3{Pp%NTFF7VC!Ri3kF2-bomTGD z|KLO3_A@<5o6CPIns+O<%a+Y%WfoiAvA7^%iJkCfE>pu}^OBpVzAyi2IN$Vi5Z^^X zPQKZqVXJ%JP8Ss1zryJVKYRbS4SWX|ni{v4@HSuQy$sW;{r zskyZ&?v#0RM@Y?uPx^ADOWV(6vj{ew4PDuj(xksFb8}a?Svg-;LMr)s`rG(-UZ+gj zdPC)8oM(8<(p!a&&MOIlb~9{U{$9hSLsL~hxZKUD5#?3`_x0$Sr~hQed0*}Fo1E%7MM3uYiicQyoNSLadrwJb`nsh54a=Muwxi4V z)}OFk?-8wNwB+fAtMX~jr|E>xzuuQMDgJ%L;5YR}PC zD*v2#Q)XFgO@wLtPep0r71y|CX0oN{{fa6&-ny!8t<90^RqaLBSH{P@TeIvzVv_>L zvF4MpuC@K!H>T=H-QU^0@2K^+#-Mt?kIZr1sV86Q?rB`?^|=zCa`i zPslz-IZ^HJ>Da1pdA+BHHyxX>emeWxuD>-eOtw62&i8Ix8Z%kBFU+zBtOZUZ`iVFaM!Tv**U|%d4NiM4St3Q}9%; zzhsM{blJ=yAk8h8Nx_3zMDP)0{W6W&! z$-6&&WZ(0D&7&`Cdc1y$xWA7}HoOy2x8_GB^J(w=j~}-*dAV+xR{Z5MsKF0f3U=~B z3-p?=bkMSS(BSzItXoZY7=!i@f%<-O@>t4O#4%eJgWvtD{C5AN{QtMo|32I6A5{dc z+;J2*61;TzosVHUp(ZCEhuUOaSrNF{tyju4%YXY<)p9uRN?ngzkvm8nB=oIE>ZGhG zRP8O}EER;p^D8Faftqk-h3#UBHRas;-~3;Gd-<2SA9igzz`(%3;OXk;vd$@?2>_eA B#q|IH literal 0 HcmV?d00001 diff --git a/docs/img/osm_gui_ns_create.png b/docs/img/osm_gui_ns_create.png new file mode 100644 index 0000000000000000000000000000000000000000..ce7950daa6daccba4a28e3339ca2d11d499cc7da GIT binary patch literal 37425 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 BbVL9E literal 0 HcmV?d00001 diff --git a/docs/pla_design_spec.md b/docs/pla_design_spec.md new file mode 100644 index 0000000..a55e73c --- /dev/null +++ b/docs/pla_design_spec.md @@ -0,0 +1,93 @@ + + +# Placement module (PLA) design description +## Overview +The PLA module provides computation of optimal placement of VNFs over VIMs by matching NS specific requirements to infrastructure availability and run-time metrics, while considering cost of compute/network. + +This document supplement the Placement Module (PLA) Users's Guide by providing details on the SW design. + +PLA is a separate module in the OSM architecture. As other OSM modules it runs in a Docker container. +PLA interacts with LCM over the Kafka message bus and handles the following message: +* topic: pla - command: get_placement + +When LCM receives a ns instantiation command with config parameter `placement-engine: PLA` it shall request placement computation from PLA. +See PLA User's Guide for details and capabilities for different types of instantiation commands and necessary configuration of PLA. + +The placement computation is done by Minizinc (www.minizinc.org) on a constraints model that is created according to the content of the ns instantiation command. + +The minizinc models are created on demand within PLA using the Jinja2 templating language. + +## SW components overview +The diagram below illustrates the important classes, data structures and libraries within PLA. + +![PLA SW Architecture](img/PLA_SW_Arch.png) + +**PLA Server** + +This is the PLA server (server.py). +Capability: Produce placement suggestions based on a placement request from LCM OSM module. The get_placement() method extract information based on the referred instantiation operation id and calculates possible deployments for the NS by matching the NS specific requirements to infrastructure, - availability and, -run-time metrics, while considering cost of compute/network. +Collaborates with: MznPlacementConductor, NsPlacementDataFactory + +**MznPlacementConductor** + +Capability: Manages minizinc model creation, execution and processing of minizinc output. Use the NsPlacementData provided from the PLA Server and collaborates with MznModelGenerator for model creation and uses PyMzn for execution of the created minizinc model. +Collaborates with: PLA Server, MznModelGenerator + +**MznModelGenerator** + +Capability: Create instance of MznModel using the information kept in NsPlacementData combined with Jinja2 templating language/templating engine. +Collaborates with: MznPlacementConductor + +**NsPlacementDataFactory** + +Capability: Knows how to create a NsPlacementData instance. Receives placement request data and collects additional information on; NSD/VNFD, Infrastructure topology, link delays and possibly more things (e.g. resource utilization, after rel.7). +Collaborates with: PLA Server + +**NsPlacementData** + +Dictionary keeping relevant data for a placement request so that a corresponding minizinc model can be created. Content includes e.g. vim account information, price lists, network topology, link characteristics and network service characteristics. + +**PlacementResult** + +Carries processed result of the optimal placement solution as computed by minizinc + +**MznModel** + +String representation of a mzn model + +**PyMzn** + +PyMzn is a Python library providing bindings to minizinc. + +**Jinja2** + +Jinja2 is a template engine used in PLA when creating the minizinc model for a placement request. + + +## Unit tests +### Unit testing dependencies +Some of the unit test modules have dependencies to Minizinc, e.g. test_mznmodels.py and test_mznPlacementConductor.py. +If these tests are to be performed outside a PLA container context, like .e.g. from CLI or from within an IDE, setup the environment as follows (linux example): +1. install minizinc as a snap from snapcraft.io/minizinc +2. create a softlink from /minizinc/bin/minizinc to /snap/bin/minizinc to mimic the container structure in the development host + +``` +$ sudo snap install minizinc --classic +$ sudo mkdir -p /minizinc/bin +$ sudo ln -s /snap/bin/minizinc /minizinc/bin/minizinc +``` diff --git a/docs/pla_users_guide.md b/docs/pla_users_guide.md new file mode 100644 index 0000000..d7ec810 --- /dev/null +++ b/docs/pla_users_guide.md @@ -0,0 +1,160 @@ + + +# Placement module (PLA) User's Guide + +## Introduction +To use the placement functionality of OSM several steps needs to be taken to configure and enable the function. +1. OSM needs to be installed with the PLA module included +2. Create the price lists for compute and transport links +3. Create the inventory of PoP interconnecting links +4. Update the PLA container with price list and inventory file +5. Usage - Instantiate the service using the placement engine + +## Install OSM including PLA +PLA is an optional module in OSM. It is installed together with OSM by adding ``--pla`` to the install script. + +`$ ./install_osm.sh --pla` + +## Create the price lists +The price list for compute determines the price for each VNF at each VIM (or Point of Presence - PoP). The file (vnf_price_list.yaml) is written in Yaml and is exemplified below. + +``` +- vnfd: testVnfOne + prices: + - vim_url: http://10.234.12.47:5000/v3 + vim_name: OpenStack1 + price: 10 + - vim_url: http://10.234.12.44:5000/v3 + vim_name: OpenStack2 + price: 9 + - vim_url: http://10.234.12.46:5000/v3 + vim_name: OpenStack3 + price: 8 + - vim_url: http://10.234.12.43:5000/v3 + vim_name: OpenStack4 + price: 7 +- vnfd: hackfest_multivdu-vnf + prices: + - vim_url: http://10.234.12.47:5000/v3 + vim_name: OpenStack1 + price: 17 + - vim_url: http://10.234.12.44:5000/v3 + vim_name: OpenStack2 + price: 18 + - vim_url: http://10.234.12.46:5000/v3 + vim_name: OpenStack3 + price: 19 + - vim_url: http://10.234.12.43:5000/v3 + vim_name: OpenStack4 + price: 20 +``` + +The price list for transport links between VIMs (PoP Interconnecting Link – PiL). In current release the price is given per link without any consideration to BW or other QoS parameter. The file (pil_price_list.yaml) is written in Yaml and is exemplified below. Note: In current OSM release the link characteristics are hard coded into this file, in future releases this data should be retrieved from the infrastructure by monitoring mechanisms. + +``` +pil: + - pil_description: Link between OpenStack1 and OpenStack2 + pil_price: 12 + pil_latency: 120 + pil_jitter: 12 + pil_endpoints: + - http://10.234.12.47:5000/v3 + - http://10.234.12.44:5000/v3 + - pil_description: Link between OpenStack1 and OpenStack3 + pil_price: 13 + pil_latency: 130 + pil_jitter: 13 + pil_endpoints: + - http://10.234.12.47:5000/v3 + - http://10.234.12.46:5000/v3 + - pil_description: Link between OpenStack1 and OpenStack4 + pil_price: 14 + pil_latency: 140 + pil_jitter: 14 + pil_endpoints: + - http://10.234.12.47:5000/v3 + - http://10.234.12.43:5000/v3 + - pil_description: Link between OpenStack2 and OpenStack3 + pil_price: 23 + pil_latency: 230 + pil_jitter: 23 + pil_endpoints: + - http://10.234.12.44:5000/v3 + - http://10.234.12.46:5000/v3 + - pil_description: Link between OpenStack2 and OpenStack4 + pil_price: 24 + pil_latency: 240 + pil_jitter: 24 + pil_endpoints: + - http://10.234.12.44:5000/v3 + - http://10.234.12.43:5000/v3 + - pil_description: Link between OpenStack3 and OpenStack4 + pil_price: 34 + pil_latency: 340 + pil_jitter: 34 + pil_endpoints: + - http://10.234.12.46:5000/v3 + - http://10.234.12.43:5000/v3 + +``` + +## Create the inventory file +The VIMs configured in OSM are assumed to be connected to each other with transport links - PiLs. In current release the inventory file describes the available PiLs with latency and jitter. If there is no PiL in the inventory file for any pair of PoPs the placement engine will not be able to use that relation for a VL. + +## Update the PLA container +Copy the price lists and inventory files to the PLA container using the following commands: + +`$ docker cp vnf_price_list.yaml $(docker ps -qf name=osm_pla):/placement/.` + +`$ docker cp pil_price_list.yaml $(docker ps -qf name=osm_pla):/placement/.` + +## Instantiate the service +When creating a NS instance, it is possible to pass instantiation parameters to OSM using the `--config` option of the client or the `config` parameter of the UI. To invoke the placement engine following directives are used. + +` +--config '{placement-engine: PLA}' +` + +### Usage examples + +#### Basic usage +`$ osm ns-create --ns_name ThreeVNFTest --nsd_name three_vnf_constrained_nsd --vim_account OpenStack1 --config '{placement-engine: PLA}' +` + +Using PLA support from the GUI network service create form: + +![Instantiate with PLA support](img/osm_gui_ns_create.png) + +### With pinning of member-vnf-index: "3" to vim_account: OpenStack3 +To enable automatic placement with one (or multiple) VNFs at a pre-determined place (e.g. near CPE), PLA has the ability do placement with one or multiple VNFs pinned to datacenter(s). The pinning is done in the same way as explained in section [Multi-site deployments](https://osm.etsi.org/wikipub/index.php/OSM_instantiation_parameters#Multi-site_deployments_.28specifying_different_VIM_accounts_for_different_VNFs.29) in the OSM wiki. + +Example NS instantiation using CLI command (pinning one of three VNFs in the used NSD): +`$ osm ns-create --ns_name ThreeVnfTest2 --nsd_name three_vnf_constrained_nsd --vim_account OpenStack1 --config '{placement-engine: PLA, vnf: [{member-vnf-index: “3", vim_account: OpenStack3}]}'` + + +### With constraints as instantiation parameters +It is also possible to provide constraints as instantiation parameters. Such constraints are included in a dictionary belonging to the `constraints` key as follows: + +`config: {placement-engine: PLA, constraints: {}}` + +#### vld constraints as instantiation parameters +Currently the supported type of constraints is placed on the vlds, and is identified with the `vld-constraints` key in the constraints specifications. Each vld that is given constraints have the form `{id: , link-constraints: {latency: , jitter: }`. It is not necessary to place constraints on all links, it is also possible to freely mix the supported constraint types `latency` and `jitter` as desired. + +Example NS instantiation using CLI command with constraints put on `vld_1` and `vld_2` + +`$ osm ns-create --ns_name ThreeVnfTest2 --nsd_name three_vnf_constrained_nsd --vim_account OpenStack1 --config '{placement-engine: PLA, vnf: [{member-vnf-index: “3", vim_account: OpenStack3}], constraints: {vld-constraints: [{id:vld_1, link-constraints: {latency: 120, jitter: 20}}, {id:vld_2, link-constraints: {latency: 120, jitter: 20 }}]}}'` diff --git a/osm_pla/__init__.py b/osm_pla/__init__.py new file mode 100755 index 0000000..0847222 --- /dev/null +++ b/osm_pla/__init__.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- + +# Copyright 2020 ArctosLabs Scandinavia AB +# +# 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/osm_pla/cmd/__init__.py b/osm_pla/cmd/__init__.py new file mode 100755 index 0000000..468be48 --- /dev/null +++ b/osm_pla/cmd/__init__.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- + +# Copyright 2020 ArctosLabs Scandinavia AB +# +# 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/osm_pla/cmd/pla_server.py b/osm_pla/cmd/pla_server.py new file mode 100755 index 0000000..767e61d --- /dev/null +++ b/osm_pla/cmd/pla_server.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- + +# Copyright 2020 ArctosLabs Scandinavia AB +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +import asyncio +import logging +import sys + +from osm_pla.config.config import Config +from osm_pla.server.server import Server + + +def main(): + parser = argparse.ArgumentParser(prog='osm-policy-agent') + parser.add_argument('--config-file', nargs='?', help='PLA configuration file') + args = parser.parse_args() + cfg = Config(args.config_file) + + root = logging.getLogger() + root.setLevel(logging.getLevelName(cfg.get('global', 'loglevel'))) + ch = logging.StreamHandler(sys.stdout) + ch.setLevel(logging.getLevelName(cfg.get('global', 'loglevel'))) + formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s', '%m/%d/%Y %I:%M:%S %p') + ch.setFormatter(formatter) + root.addHandler(ch) + + log = logging.getLogger(__name__) + log.info("Starting PLA Server...") + + loop = asyncio.get_event_loop() + server = Server(cfg, loop) + server.run() + + +if __name__ == '__main__': + main() diff --git a/osm_pla/config/config.py b/osm_pla/config/config.py new file mode 100644 index 0000000..114fc5f --- /dev/null +++ b/osm_pla/config/config.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 ArctosLabs Scandinavia AB +# +# 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. +"""Global configuration managed by environment variables.""" + +import logging +import os + +import pkg_resources +import yaml + +logger = logging.getLogger(__name__) + + +class Config: + def __init__(self, config_file: str = ''): + self.conf = {} + self._read_config_file(config_file) + self._read_env() + + def _read_config_file(self, config_file): + if not config_file: + path = 'pla.yaml' + config_file = pkg_resources.resource_filename(__name__, path) + with open(config_file) as f: + self.conf = yaml.load(f) + + def _read_env(self): + for env in os.environ: + if not env.startswith("OSMPLA_"): + continue + elements = env.lower().split("_") + if len(elements) < 3: + logger.warning( + "Environment variable %s=%s does not comply with required format. Section and/or field missing.", + env, os.getenv(env)) + continue + section = elements[1] + field = '_'.join(elements[2:]) + value = os.getenv(env) + if section not in self.conf: + self.conf[section] = {} + self.conf[section][field] = value + + def get(self, section, field=None): + if not field: + return self.conf[section] + return self.conf[section][field] + + def set(self, section, field, value): + if section not in self.conf: + self.conf[section] = {} + self.conf[section][field] = value diff --git a/osm_pla/config/pla.yaml b/osm_pla/config/pla.yaml new file mode 100644 index 0000000..108f2b4 --- /dev/null +++ b/osm_pla/config/pla.yaml @@ -0,0 +1,30 @@ +## Copyright 2020 ArctosLabs Scandinavia AB +## +## 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. + +global: + loglevel: INFO + +message: + driver: kafka + host: kafka + port: 9092 + group_id: pla + +database: + driver: mongo + host: mongo + port: 27017 + name: osm + \ No newline at end of file diff --git a/osm_pla/placement/__init__.py b/osm_pla/placement/__init__.py new file mode 100755 index 0000000..2669d22 --- /dev/null +++ b/osm_pla/placement/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2020 ArctosLabs Scandinavia AB +# +# 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/osm_pla/placement/macros.j2 b/osm_pla/placement/macros.j2 new file mode 100644 index 0000000..7b2dcf5 --- /dev/null +++ b/osm_pla/placement/macros.j2 @@ -0,0 +1,105 @@ +% Copyright 2020 ArctosLabs Scandinavia AB +% +% 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. +{%- macro vim_accounts(vim_accounts) -%} +enum Vims = { +{%- for vim in vim_accounts %} +{{vim}}{% if loop.nextitem is defined%},{% endif %} +{%- endfor -%} +}; % The vim-accounts +{%- endmacro -%} + +{%- macro variables_vnf(ns_desc) -%} +{%- for vnf in ns_desc -%} +{%- if vnf.vim_account %} +Vims: VNF{{vnf.vnf_id}} = {{vnf.vim_account}}; +{%- else %} +var Vims: VNF{{vnf.vnf_id}}; +{%- endif -%} +{% endfor -%} +{%- endmacro -%} + +{%- macro trp_link_latency(trp_link_latency) -%} +array[Vims, Vims] of int: trp_link_latency = [ +{%- for row in trp_link_latency -%} +| +{%- for col in row -%} +{{col}}, +{%- endfor %} +{% endfor -%} +|]; % Transport link latency between data centers +{%- endmacro -%} + +{%- macro trp_link_jitter(trp_link_jitter) -%} +array[Vims, Vims] of int: trp_link_jitter = [ +{%- for row in trp_link_jitter -%} +| +{%- for col in row -%} +{{col}}, +{%- endfor %} +{% endfor -%} +|]; % Transport link jitter between data centers +{%- endmacro -%} + +{%- macro trp_link_price_list(trp_link_price_list) -%} +array[Vims, Vims] of int: trp_link_price_list = [ +{%- for row in trp_link_price_list -%} +| +{%- for col in row -%} +{{col}}, +{%- endfor %} +{% endfor -%} +|]; % Transport link price list +{%- endmacro -%} + +{%- macro vnf_price_list_per_vim(ns_desc) -%} +{%- for vnf in ns_desc -%} +array[Vims] of int: vim_price_list_{{vnf.vnf_id}} = [ +{%- for price in vnf.vnf_price_per_vim -%} +{{price}}{% if loop.nextitem is defined%},{% endif %} +{%- endfor -%} +]; +{% endfor %} +{%- endmacro -%} + +{%- macro vld_constraints(vld_desc) -%} +{%- for cp in vld_desc -%} +{%- if 'latency' in cp.keys()%} +constraint trp_link_latency[VNF{{cp.cp_refs[0]}}, VNF{{cp.cp_refs[1]}}] <= {{cp.latency}}; +{% endif -%} +{% endfor -%} +{%- for cp in vld_desc -%} +{%- if 'jitter' in cp.keys()%} +constraint trp_link_jitter[VNF{{cp.cp_refs[0]}}, VNF{{cp.cp_refs[1]}}] <= {{cp.jitter}}; +{% endif -%} +{% endfor -%} +{%- endmacro -%} + +{% macro transport_cost(vld_desc) -%} +var int: used_transport_cost = +{%- if not vld_desc -%} +0; +{% else %} +{%- for cp in vld_desc -%} +trp_link_price_list[VNF{{cp.cp_refs[0]}}, VNF{{cp.cp_refs[1]}}]{% if loop.nextitem is defined %}+{% else %};{% endif %} +{% endfor -%} +{% endif -%} +{%- endmacro -%} + +{%- macro used_vim_cost(ns_desc) -%} +var int: used_vim_cost = +{%- for vnf in ns_desc -%} +vim_price_list_{{vnf.vnf_id}}[VNF{{vnf.vnf_id}}]{% if loop.nextitem is defined %}+{% else %};{% endif %} +{% endfor -%} +{%- endmacro -%} diff --git a/osm_pla/placement/mznplacement.py b/osm_pla/placement/mznplacement.py new file mode 100755 index 0000000..cf6236a --- /dev/null +++ b/osm_pla/placement/mznplacement.py @@ -0,0 +1,254 @@ +# Copyright 2020 ArctosLabs Scandinavia AB +# +# 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 datetime +import platform +import itertools + +import pymzn +from jinja2 import Environment +from jinja2.loaders import FileSystemLoader + + +class MznPlacementConductor(object): + """ + Knows how to process placement req using minizinc + """ + if platform.system() == 'Windows': + default_mzn_path = 'C:\\Program Files\\MiniZinc IDE (bundled)\\minizinc.exe' + else: + default_mzn_path = '/minizinc/bin/minizinc' + + def __init__(self, log, mzn_path=default_mzn_path): + pymzn.config['minizinc'] = mzn_path + self.log = log # FIXME what to log (besides forwarding it to MznModelGenerator) here? + + def _run_placement_model(self, mzn_model, ns_desc, mzn_model_data={}): + """ + Runs the minizinc placement model and post process the result + Note: in this revision we use the 'item' output mode from pymzn.minizinc since it ease + post processing of the solutions when we use enumerations in mzn_model + Note: minizinc does not support '-' in identifiers and therefore we convert back from use of '_' when we + process the result + Note: minizinc does not support identifiers starting with numbers and therefore we skip the leading 'vim_' + when we process the result + + :param mzn_model: a minizinc model as str (note: may also be path to .mzn file) + :param ns_desc: network service descriptor, carries information about pinned VNFs so those can be included in + the result + :param mzn_model_data: minizinc model data dictionary (typically not used with our models) + :return: list of dicts formatted as {'vimAccountId': '', 'member-vnf-index': <'index'>} + or formatted as [{}] if unsatisfiable model + """ + solns = pymzn.minizinc(mzn_model, data=mzn_model_data, output_mode='item') + + if 'UNSATISFIABLE' in str(solns): + return [{}] + + solns_as_str = str(solns[0]) + + # make it easier to extract the desired information by cleaning from newline, whitespace etc. + solns_as_str = solns_as_str.replace('\n', '').replace(' ', '').rstrip(';') + + vnf_vim_mapping = (e.split('=') for e in solns_as_str.split(';')) + + res = [{'vimAccountId': e[1][3:].replace('_', '-'), 'member-vnf-index': e[0][3:]} for e in + vnf_vim_mapping] + # add any pinned VNFs + pinned = [{'vimAccountId': e['vim_account'][3:].replace('_', '-'), 'member-vnf-index': e['vnf_id']} for e in + ns_desc if 'vim_account' in e.keys()] + + return res + pinned + + def do_placement_computation(self, nspd): + """ + Orchestrates the placement computation + + :param nspd: placement data + :return: see _run_placement_model + """ + mzn_model = MznModelGenerator(self.log).create_model(nspd) + return self._run_placement_model(mzn_model, nspd['ns_desc']) + + +class MznModelGenerator(object): + ''' + Has the capability to generate minizinc models from information contained in + NsPlacementData objects. Uses jinja2 as templating language for the model + ''' + default_j2_template = "osm_pla_dynamic_template.j2" + template_search_path = ['osm_pla/placement', '../placement', '/pla/osm_pla/placement'] + + def __init__(self, log): + ''' + Constructor + ''' + self.log = log # FIXME we do not log anything so far + + def create_model(self, ns_placement_data): + ''' + Creates a minizinc model according to the content of nspd + nspd - NSPlacementData + return MZNModel + ''' + self.log.info('ns_desc: {}'.format(ns_placement_data['ns_desc'])) + self.log.info('vld_desc: {}'.format(ns_placement_data['vld_desc'])) + mzn_model_template = self._load_jinja_template() + mzn_model = mzn_model_template.render(ns_placement_data) + self.log.info('Minizinc model: {}'.format(mzn_model)) + return mzn_model + + def _load_jinja_template(self, template_name=default_j2_template): + """loads the jinja template used for model generation""" + env = Environment(loader=FileSystemLoader(MznModelGenerator.template_search_path)) + return env.get_template(template_name) + + +class NsPlacementDataFactory(object): + """ + process information an network service and applicable network infrastructure resources in order to produce + information tailored for the minizinc model code generator + """ + + def __init__(self, vim_accounts_info, vnf_prices, nsd, pil_info, pinning=None, order_constraints=None): + """ + :param vim_accounts_info: a dictionary with vim url as key and id as value, we add a unique index to it for use + in the mzn array constructs and adjust the value of the id to minizinc acceptable identifier syntax + :param vnf_prices: a dictionary with 'vnfd-id-ref' as key and a dictionary with vim_urls: cost as value + :param nsd: the network service descriptor + :param pil_info: price list and metrics for PoP interconnection links + :param pinning: list of {'member-vnf-index': '', 'vim_account': ''} + :param order_constraints: any constraints provided at instantiation time + """ + next_idx = itertools.count() + self._vim_accounts_info = {k: {'id': 'vim' + v.replace('-', '_'), 'idx': next(next_idx)} for k, v in + vim_accounts_info.items()} + self._vnf_prices = vnf_prices + self._nsd = nsd + self._pil_info = pil_info + self._pinning = pinning + self._order_constraints = order_constraints + + def _produce_trp_link_characteristics_data(self, characteristics): + """ + :param characteristics: one of {pil_latency, pil_price, pil_jitter} + :return: 2d array of requested trp_link characteristics data + """ + if characteristics not in {'pil_latency', 'pil_price', 'pil_jitter'}: + raise Exception('characteristic \'{}\' not supported'.format(characteristics)) + num_vims = len(self._vim_accounts_info) + trp_link_characteristics = [[0 if col == row else 0x7fff for col in range(num_vims)] for row in range(num_vims)] + for pil in self._pil_info['pil']: + if characteristics in pil.keys(): + url1 = pil['pil_endpoints'][0] + url2 = pil['pil_endpoints'][1] + # only consider links between applicable vims + if url1 in self._vim_accounts_info and url2 in self._vim_accounts_info: + idx1 = self._vim_accounts_info[url1]['idx'] + idx2 = self._vim_accounts_info[url2]['idx'] + trp_link_characteristics[idx1][idx2] = pil[characteristics] + trp_link_characteristics[idx2][idx1] = pil[characteristics] + + return trp_link_characteristics + + def _produce_vld_desc(self): + """ + Creates the expected vlds from the nsd. Includes constraints if part of nsd. + Overrides constraints with any syntactically correct instantiation parameters + :return: + """ + vld_desc = [] + for vld in self._nsd['vld']: + if vld['mgmt-network'] is False: + vld_desc_entry = {} + cp_refs = [ep_ref['member-vnf-index-ref'] for ep_ref in vld['vnfd-connection-point-ref']] + vld_desc_entry['cp_refs'] = cp_refs + if 'link-constraint' in vld.keys(): + for constraint in vld['link-constraint']: + if constraint['constraint-type'] == 'LATENCY': + vld_desc_entry['latency'] = constraint['value'] + elif constraint['constraint-type'] == 'JITTER': + vld_desc_entry['jitter'] = constraint['value'] + vld_desc.append(vld_desc_entry) + + # create candidates from instantiate params + if self._order_constraints is not None: + candidate_vld_desc = [] + # use id to find the endpoints in the nsd + for entry in self._order_constraints.get('vld-constraints'): + for vld in self._nsd['vld']: + if entry['id'] == vld['id']: + vld_desc_instantiate_entry = {} + cp_refs = [ep_ref['member-vnf-index-ref'] for ep_ref in vld['vnfd-connection-point-ref']] + vld_desc_instantiate_entry['cp_refs'] = cp_refs + # add whatever constraints that are provided to the vld_desc_entry + # misspelled 'link-constraints' => empty dict + # lack (or misspelling) of one or both supported constraints => entry not appended + for constraint, value in entry.get('link-constraints', {}).items(): + if constraint == 'latency': + vld_desc_instantiate_entry['latency'] = value + elif constraint == 'jitter': + vld_desc_instantiate_entry['jitter'] = value + if set(['latency', 'jitter']).intersection(vld_desc_instantiate_entry.keys()): + candidate_vld_desc.append(vld_desc_instantiate_entry) + # merge with nsd originated, FIXME log any deviations? + for vld_d in vld_desc: + for vld_d_i in candidate_vld_desc: + if set(vld_d['cp_refs']) == set(vld_d_i['cp_refs']): + if vld_d_i.get('jitter'): + vld_d['jitter'] = vld_d_i['jitter'] + if vld_d_i.get('latency'): + vld_d['latency'] = vld_d_i['latency'] + + return vld_desc + + def _produce_ns_desc(self): + """ + collect information for the ns_desc part of the placement data + for the vim_accounts that are applicable, collect the vnf_price + """ + ns_desc = [] + for vnfd in self._nsd['constituent-vnfd']: + vnf_info = {'vnf_id': vnfd['member-vnf-index']} + # prices + prices_for_vnfd = self._vnf_prices[vnfd['vnfd-id-ref']] + # the list of prices must be ordered according to the indexing of the vim_accounts + price_list = [_ for _ in range(len(self._vim_accounts_info))] + for k in prices_for_vnfd.keys(): + if k in self._vim_accounts_info.keys(): + price_list[self._vim_accounts_info[k]['idx']] = prices_for_vnfd[k] + vnf_info['vnf_price_per_vim'] = price_list + + # pinning to dc + if self._pinning is not None: + for pinned_vnf in self._pinning: + if vnfd['member-vnf-index'] == pinned_vnf['member-vnf-index']: + vnf_info['vim_account'] = 'vim' + pinned_vnf['vimAccountId'].replace('-', '_') + + ns_desc.append(vnf_info) + return ns_desc + + def create_ns_placement_data(self): + """populate NsPlacmentData object + """ + ns_placement_data = {'vim_accounts': [vim_data['id'] for + vim_data in self._vim_accounts_info.values()], + 'trp_link_latency': self._produce_trp_link_characteristics_data('pil_latency'), + 'trp_link_jitter': self._produce_trp_link_characteristics_data('pil_jitter'), + 'trp_link_price_list': self._produce_trp_link_characteristics_data('pil_price'), + 'ns_desc': self._produce_ns_desc(), + 'vld_desc': self._produce_vld_desc(), + 'generator_data': {'file': __file__, 'time': datetime.datetime.now()}} + + return ns_placement_data diff --git a/osm_pla/placement/osm_pla_dynamic_template.j2 b/osm_pla/placement/osm_pla_dynamic_template.j2 new file mode 100644 index 0000000..e95be0c --- /dev/null +++ b/osm_pla/placement/osm_pla_dynamic_template.j2 @@ -0,0 +1,40 @@ +% Copyright 2020 ArctosLabs Scandinavia AB +% +% 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. +% This minizinc model is generated using {{generator_data.file}} +% at {{generator_data.time}}. + +{% import 'macros.j2' as macros -%} +%This is the NETWORK RESOURCE MODEL +{{ macros.vim_accounts(vim_accounts) }} +{{ macros.trp_link_latency(trp_link_latency) }} +{{ macros.trp_link_jitter(trp_link_jitter) }} +{{ macros.trp_link_price_list(trp_link_price_list) }} +{{ macros.vnf_price_list_per_vim(ns_desc) }} + +% This is the NETWORK BASIC LOAD MODEL (CONSUMED) +% NOTE. This is not applicable in OSM Release 7 + +% This is the SERVICE CONSUMPTION MODEL +% These are the variables, i.e. which DC to select for each VNF +{{ macros.variables_vnf(ns_desc)}} + +% These are the set of rules for selecting DCs to VNFs +{{ macros.vld_constraints(vld_desc) }} +% Calculate the cost for VNFs and cost for transport link and total cost +{{ macros.transport_cost(vld_desc) }} +{{ macros.used_vim_cost(ns_desc) }} +var int: total_cost = used_transport_cost + used_vim_cost; + +solve minimize total_cost; \ No newline at end of file diff --git a/osm_pla/server/server.py b/osm_pla/server/server.py new file mode 100644 index 0000000..8d25879 --- /dev/null +++ b/osm_pla/server/server.py @@ -0,0 +1,176 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- + +# Copyright 2020 ArctosLabs Scandinavia AB +# +# 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 asyncio +import logging +# import platform +from pathlib import Path + +# import pkg_resources +import yaml +from osm_common import dbmemory, dbmongo, msglocal, msgkafka + +from osm_pla.config.config import Config +from osm_pla.placement.mznplacement import MznPlacementConductor +from osm_pla.placement.mznplacement import NsPlacementDataFactory + + +class Server: + pil_price_list_file = Path('/placement/pil_price_list.yaml') + vnf_price_list_file = Path('/placement/vnf_price_list.yaml') + + def __init__(self, config: Config, loop=None): + self.log = logging.getLogger("pla.server") + self.db = None + self.msgBus = None + self.config = config + self.loop = loop or asyncio.get_event_loop() + + try: + if config.get('database', 'driver') == "mongo": + self.db = dbmongo.DbMongo() + self.db.db_connect(config.get('database')) + elif config.get('database', 'driver') == "memory": + self.db = dbmemory.DbMemory() + self.db.db_connect(config.get('database')) + else: + raise Exception("Invalid configuration param '{}' at '[database]':'driver'".format( + config.get('database', 'driver'))) + + if config.get('message', 'driver') == "local": + self.msgBus = msglocal.MsgLocal() + elif config.get('message', 'driver') == "kafka": + self.msgBus = msgkafka.MsgKafka() + else: + raise Exception("Invalid message bus driver {}".format( + config.get('message', 'driver'))) + self.msgBus.loop = loop + self.msgBus.connect(config.get('message')) + + except Exception as e: + self.log.exception("kafka setup error. Exception: {}".format(e)) + + def _get_nslcmop(self, nsdlcmop_id): + """ + :param nsdlcmop_id: + :return: nslcmop from database corresponding to nslcmop_id + """ + db_filter = {"_id": nsdlcmop_id} + nslcmop = self.db.get_one("nslcmops", db_filter) + return nslcmop + + def _get_nsd(self, nsd_id): + """ + :param nsd_id: + :return: nsd from database corresponding to nsd_id + """ + db_filter = {"_id": nsd_id} + return self.db.get_one("nsds", db_filter) + + def _get_vim_accounts(self, vim_account_ids): + """ + :param vim_account_ids: list of VIM account ids + :return: list of vim account entries from database corresponding to list in vim_accounts_id + """ + db_filter = {"_id": vim_account_ids} + return self.db.get_list("vim_accounts", db_filter) + + def _get_vnf_price_list(self, price_list_file_path): + """ + read vnf price list configuration file and reformat its content + + :param: price_list_file: Path to price list file + :return: dictionary formatted as {'': {'':''}} + """ + with open(str(price_list_file_path)) as pl_fd: + price_list_data = yaml.safe_load_all(pl_fd) + return {i['vnfd']: {i1['vim_url']: i1['price'] for i1 in i['prices']} for i in next(price_list_data)} + + def _get_pil_info(self, pil_info_file_path): + """ + read and return pil information from file + :param pil_info_file_path: Path to pil_info file + :return pil configuration file content as Python object + """ + with open(str(pil_info_file_path)) as pil_fd: + data = yaml.safe_load_all(pil_fd) + return next(data) + + async def get_placement(self, nslcmop_id): + """ + - Collects and prepares placement information. + - Request placement computation. + - Formats and distribute placement result + + Note: exceptions result in empty response message + + :param nslcmop_id: + :return: + """ + try: + nslcmop = self._get_nslcmop(nslcmop_id) + nsd = self._get_nsd(nslcmop['operationParams']['nsdId']) + self.log.info("nsd: {}".format(nsd)) + valid_vim_accounts = nslcmop['operationParams']['validVimAccounts'] + vim_accounts_data = self._get_vim_accounts(valid_vim_accounts) + vims_information = {_['vim_url']: _['_id'] for _ in vim_accounts_data} + price_list = self._get_vnf_price_list(Server.vnf_price_list_file) + pil_info = self._get_pil_info(Server.pil_price_list_file) + pinning = nslcmop['operationParams'].get('vnf') + self.log.info("pinning: {}".format(pinning)) + order_constraints = nslcmop['operationParams'].get('placement-constraints') + self.log.info("order constraints: {}".format(order_constraints)) + + nspd = NsPlacementDataFactory(vims_information, + price_list, + nsd, + pil_info, + pinning, order_constraints).create_ns_placement_data() + + vnf_placement = MznPlacementConductor(self.log).do_placement_computation(nspd) + + except Exception as e: + # Note: there is no cure for failure so we have a catch-all clause here + self.log.exception("PLA fault. Exception: {}".format(e)) + vnf_placement = [] + finally: + await self.msgBus.aiowrite("pla", "placement", + {'placement': {'vnf': vnf_placement, 'nslcmopId': nslcmop_id}}) + + def handle_kafka_command(self, topic, command, params): + self.log.info("Kafka msg arrived: {} {} {}".format(topic, command, params)) + if topic == "pla" and command == "get_placement": + nslcmop_id = params.get('nslcmopId') + self.loop.create_task(self.get_placement(nslcmop_id)) + + async def kafka_read(self): + self.log.info("Task kafka_read start") + while True: + try: + topics = "pla" + await self.msgBus.aioread(topics, self.loop, self.handle_kafka_command) + except Exception as e: + self.log.error("kafka read error. Exception: {}".format(e)) + await asyncio.sleep(5, loop=self.loop) + + def run(self): + self.loop.run_until_complete(self.kafka_read()) + self.loop.close() + self.loop = None + if self.msgBus: + self.msgBus.disconnect() diff --git a/osm_pla/test/__init__.py b/osm_pla/test/__init__.py new file mode 100644 index 0000000..968ffb4 --- /dev/null +++ b/osm_pla/test/__init__.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- + +# Copyright 2019 ArctosLabs Scandinavia AB +# +# 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/osm_pla/test/corrupt_pil_endpoints_config_unittest1.yaml b/osm_pla/test/corrupt_pil_endpoints_config_unittest1.yaml new file mode 100644 index 0000000..1e53b37 --- /dev/null +++ b/osm_pla/test/corrupt_pil_endpoints_config_unittest1.yaml @@ -0,0 +1,94 @@ +# Copyright 2020 ArctosLabs Scandinavia AB +# +# 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. + +# Point Of Precence (POP), price list +pop: + - vim_url: http://10.234.12.47:5000/v3 + vim_name: OpenStack1 + num_vm: 10 + vm_price: + - x_large: 10 + - large: 5 + - medium: 4 + - small: 3 + - tiny: 2 + - vim_url: http://10.234.12.44:5000/v3 + vim_name: OpenStack2 + num_vm: 10 + vm_price: + - large: 10 + - medium: 8 + - small: 6 + - tiny: 4 + - vim_url: http://10.234.12.46:5000/v3 + vim_name: OpenStack3 + num_vm: 10 + vm_price: + - large: 8 + - medium: 6 + - small: 3 + - tiny: 2 + - vim_url: http://10.234.12.43:5000/v3 + vim_name: OpenStack4 + num_vm: 10 + vm_price: + - large: 9 + - medium: 7 + - small: 4 + - tiny: 3 +# POP Interconnecting Link (PIL), price list and latency +pil: + - pil_description: Link between OpenStack1 and OpenStack2 + pil_price: 12 + pil_latency: 120 + pil_jitter: 1200 + pil_endpoints: + - http://10.234.12.47:5000/v3 + - http://10.234.12.44:5000/v3 + - pil_description: Link between OpenStack1 and OpenStack3 + pil_price: 13 + pil_latency: 130 + pil_jitter: 1300 + pil_endpoints: + - http://10.234.12.47:5000/v3 + - http://10.234.12.46:5000/v3 + - pil_description: Link between OpenStack1 and OpenStack4 + pil_price: 14 + pil_latency: 140 + pil_jitter: 1400 + pil_endpoints: + - http://10.234.12.47:5000/v3 + - http://10.234.12.43:5000/v3 + - pil_description: Link between OpenStack2 and OpenStack3 + pil_price: 23 + pil_latency: 230 + pil_jitter: 2300 + pil_endpoints: + - http://10.234.12.44:5000/v3 + - http://10.234.12.46:5000/v3 + - pil_description: Link between OpenStack2 and OpenStack4 + pil_price: 24 + pil_latency: 240 + pil_jitter: 2400 + pil_endpoints: + - http://10.234.12.44:5000/v3 + - http://10.234.12.43:5000/v3 + - pil_description: Link between OpenStack3 and OpenStack4 + pil_price: 34 + pil_latency: 340 + pil_jitter: 3400 + pil_endpoints: +# - http://10.234.12.46:5000/v3 + - http://10.234.12.43:5000/v3 \ No newline at end of file diff --git a/osm_pla/test/not_yaml_conformant.yaml b/osm_pla/test/not_yaml_conformant.yaml new file mode 100644 index 0000000..90e3679 --- /dev/null +++ b/osm_pla/test/not_yaml_conformant.yaml @@ -0,0 +1,94 @@ +# Copyright 2020 ArctosLabs Scandinavia AB +# +# 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. + +# Point Of Precence (POP), price list +pop + - vim_url: http://10.234.12.47:5000/v3 + vim_name: OpenStack1 + num_vm: 10 + vm_price: + - x_large: 10 + - large: 5 + - medium: 4 + - small: 3 + - tiny: 2 + - vim_url: http://10.234.12.44:5000/v3 + vim_name: OpenStack2 + num_vm: 10 + vm_price: + - large: 10 + - medium: 8 + - small: 6 + - tiny: 4 + - vim_url: http://10.234.12.46:5000/v3 + vim_name: OpenStack3 + num_vm: 10 + vm_price: + - large: 8 + - medium: 6 + - small: 3 + - tiny: 2 + - vim_url: http://10.234.12.43:5000/v3 + vim_name: OpenStack4 + num_vm: 10 + vm_price: + - large: 9 + - medium: 7 + - small: 4 + - tiny: 3 +# POP Interconnecting Link (PIL), price list and latency +pil: + - pil_description: Link between OpenStack1 and OpenStack2 + pil_price: 12 + pil_latency: 120 + pil_jitter: 1200 + pil_endpoints: + - http://10.234.12.47:5000/v3 + - http://10.234.12.44:5000/v3 + - pil_description: Link between OpenStack1 and OpenStack3 + pil_price: 13 + pil_latency: 130 + pil_jitter: 1300 + pil_endpoints: + - http://10.234.12.47:5000/v3 + - http://10.234.12.46:5000/v3 + - pil_description: Link between OpenStack1 and OpenStack4 + pil_price: 14 + pil_latency: 140 + pil_jitter: 1400 + pil_endpoints: + - http://10.234.12.47:5000/v3 + - http://10.234.12.43:5000/v3 + - pil_description: Link between OpenStack2 and OpenStack3 + pil_price: 23 + pil_latency: 230 + pil_jitter: 2300 + pil_endpoints: + - http://10.234.12.44:5000/v3 + - http://10.234.12.46:5000/v3 + - pil_description: Link between OpenStack2 and OpenStack4 + pil_price: 24 + pil_latency: 240 + pil_jitter: 2400 + pil_endpoints: + - http://10.234.12.44:5000/v3 + - http://10.234.12.43:5000/v3 + - pil_description: Link between OpenStack3 and OpenStack4 + pil_price: 34 + pil_latency: 340 + pil_jitter: 3400 + pil_endpoints: + - http://10.234.12.46:5000/v3 + - http://10.234.12.43:5000/v3 diff --git a/osm_pla/test/nsd_unittest1.yaml b/osm_pla/test/nsd_unittest1.yaml new file mode 100644 index 0000000..b4832a7 --- /dev/null +++ b/osm_pla/test/nsd_unittest1.yaml @@ -0,0 +1,66 @@ +# Copyright 2020 ArctosLabs Scandinavia AB +# +# 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. +nsd:nsd-catalog: + nsd: + - constituent-vnfd: + - member-vnf-index: 1 + vnfd-id-ref: cirros_vnfd_v2 + - member-vnf-index: 2 + vnfd-id-ref: cirros_vnfd_v2 + - member-vnf-index: 3 + vnfd-id-ref: cirros_vnfd_v2 + description: Placement constraints NSD + id: three_vnf_constrained_nsd + name: three_vnf_constrained_nsd + short-name: three_vnf_constrained_nsd + vendor: ArctosLabs + version: '1.0' + vld: + - id: three_vnf_constrained_nsd_vld1 + link-constraint: + - constraint-type: LATENCY + value: 150 + - constraint-type: JITTER + value: 30 + mgmt-network: !!bool False + name: ns_constrained_nsd_vld1 + short-name: ns_constrained_nsd_vld1 + type: ELAN + vim-network-name: private + vnfd-connection-point-ref: + - member-vnf-index-ref: 1 + vnfd-connection-point-ref: vnf-cp0 + vnfd-id-ref: cirros_vnfd_v2 + - member-vnf-index-ref: 2 + vnfd-connection-point-ref: vnf-cp0 + vnfd-id-ref: cirros_vnfd_v2 + - id: three_vnf_constrained_nsd_vld2 + link-constraint: + - constraint-type: LATENCY + value: 90 + - constraint-type: JITTER + value: 30 + mgmt-network: !!bool False + name: ns_constrained_nsd_vld2 + short-name: ns_constrained_nsd_vld2 + type: ELAN + vim-network-name: private + vnfd-connection-point-ref: + - member-vnf-index-ref: 2 + vnfd-connection-point-ref: vnf-cp0 + vnfd-id-ref: cirros_vnfd_v2 + - member-vnf-index-ref: 3 + vnfd-connection-point-ref: vnf-cp0 + vnfd-id-ref: cirros_vnfd_v2 \ No newline at end of file diff --git a/osm_pla/test/nsd_unittest2.yaml b/osm_pla/test/nsd_unittest2.yaml new file mode 100644 index 0000000..be54166 --- /dev/null +++ b/osm_pla/test/nsd_unittest2.yaml @@ -0,0 +1,62 @@ +# Copyright 2020 ArctosLabs Scandinavia AB +# +# 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. +nsd:nsd-catalog: + nsd: + - constituent-vnfd: + - member-vnf-index: one + vnfd-id-ref: cirros_vnfd_v2 + - member-vnf-index: two + vnfd-id-ref: cirros_vnfd_v2 + - member-vnf-index: three + vnfd-id-ref: cirros_vnfd_v2 + description: Placement no constraints NSD + id: three_vnf_no_constrained_nsd + name: three_vnf_no_constrained_nsd + short-name: three_vnf_no_constrained_nsd + vendor: ArctosLabs + version: '1.0' + vld: + - id: three_vnf_no_constrained_nsd_vld1 + link-constraint: + - constraint-type: JITTER + value: 30 + mgmt-network: !!bool False + name: ns_no_constrained_nsd_vld1 + short-name: ns_no_constrained_nsd_vld1 + type: ELAN + vim-network-name: private + vnfd-connection-point-ref: + - member-vnf-index-ref: one + vnfd-connection-point-ref: vnf-cp0 + vnfd-id-ref: cirros_vnfd_v2 + - member-vnf-index-ref: two + vnfd-connection-point-ref: vnf-cp0 + vnfd-id-ref: cirros_vnfd_v2 + - id: three_vnf_no_constrained_nsd_vld2 + link-constraint: + - constraint-type: LATENCY + value: 120 + mgmt-network: !!bool False + name: ns_no_constrained_nsd_vld2 + short-name: ns_no_constrained_nsd_vld2 + type: ELAN + vim-network-name: private + vnfd-connection-point-ref: + - member-vnf-index-ref: two + vnfd-connection-point-ref: vnf-cp0 + vnfd-id-ref: cirros_vnfd_v2 + - member-vnf-index-ref: three + vnfd-connection-point-ref: vnf-cp0 + vnfd-id-ref: cirros_vnfd_v2 \ No newline at end of file diff --git a/osm_pla/test/nsd_unittest3.yaml b/osm_pla/test/nsd_unittest3.yaml new file mode 100644 index 0000000..c66df82 --- /dev/null +++ b/osm_pla/test/nsd_unittest3.yaml @@ -0,0 +1,66 @@ +# Copyright 2020 ArctosLabs Scandinavia AB +# +# 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. +nsd:nsd-catalog: + nsd: + - constituent-vnfd: + - member-vnf-index: one + vnfd-id-ref: cirros_vnfd_v2 + - member-vnf-index: two + vnfd-id-ref: cirros_vnfd_v2 + - member-vnf-index: three + vnfd-id-ref: cirros_vnfd_v2 + description: Placement constraints NSD + id: three_vnf_constrained_nsd + name: three_vnf_constrained_nsd + short-name: three_vnf_constrained_nsd + vendor: ArctosLabs + version: '1.0' + vld: + - id: three_vnf_constrained_nsd_vld1 + link-constraint: + - constraint-type: LATENCY + value: 150 + - constraint-type: JITTER + value: 30 + mgmt-network: !!bool False + name: ns_constrained_nsd_vld1 + short-name: ns_constrained_nsd_vld1 + type: ELAN + vim-network-name: private + vnfd-connection-point-ref: + - member-vnf-index-ref: one + vnfd-connection-point-ref: vnf-cp0 + vnfd-id-ref: cirros_vnfd_v2 + - member-vnf-index-ref: two + vnfd-connection-point-ref: vnf-cp0 + vnfd-id-ref: cirros_vnfd_v2 + - id: three_vnf_constrained_nsd_vld2 + link-constraint: + - constraint-type: LATENCY + value: 90 + - constraint-type: JITTER + value: 30 + mgmt-network: !!bool False + name: ns_constrained_nsd_vld2 + short-name: ns_constrained_nsd_vld2 + type: ELAN + vim-network-name: private + vnfd-connection-point-ref: + - member-vnf-index-ref: two + vnfd-connection-point-ref: vnf-cp0 + vnfd-id-ref: cirros_vnfd_v2 + - member-vnf-index-ref: three + vnfd-connection-point-ref: vnf-cp0 + vnfd-id-ref: cirros_vnfd_v2 \ No newline at end of file diff --git a/osm_pla/test/nsd_unittest4.yaml b/osm_pla/test/nsd_unittest4.yaml new file mode 100644 index 0000000..68d8f7e --- /dev/null +++ b/osm_pla/test/nsd_unittest4.yaml @@ -0,0 +1,35 @@ +# Copyright 2020 ArctosLabs Scandinavia AB +# +# 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. +nsd:nsd-catalog: + nsd: + - constituent-vnfd: + - member-vnf-index: 1 + vnfd-id-ref: hackfest-basic_vnfd + description: Generated by OSM package generator + id: hackfest-basic_nsd + name: hackfest-basic_nsd + short-name: hackfest-basic_nsd + vendor: Abubakr Magzoub, Lancaster University + version: '1.0' + vld: + - id: hackfest-basic_nsd_vld0 + mgmt-network: !!bool True + name: management + short-name: management + type: ELAN + vnfd-connection-point-ref: + - member-vnf-index-ref: 1 + vnfd-connection-point-ref: vnf-cp0 + vnfd-id-ref: hackfest-basic_vnfd diff --git a/osm_pla/test/nsd_unittest_no_vld_constraints.yaml b/osm_pla/test/nsd_unittest_no_vld_constraints.yaml new file mode 100644 index 0000000..7a440f4 --- /dev/null +++ b/osm_pla/test/nsd_unittest_no_vld_constraints.yaml @@ -0,0 +1,56 @@ +# Copyright 2020 ArctosLabs Scandinavia AB +# +# 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. +nsd:nsd-catalog: + nsd: + - constituent-vnfd: + - member-vnf-index: one + vnfd-id-ref: cirros_vnfd_v2 + - member-vnf-index: two + vnfd-id-ref: cirros_vnfd_v2 + - member-vnf-index: three + vnfd-id-ref: cirros_vnfd_v2 + description: Placement constraints NSD + id: three_vnf_constrained_nsd + name: three_vnf_constrained_nsd + short-name: three_vnf_constrained_nsd + vendor: ArctosLabs + version: '1.0' + vld: + - id: three_vnf_constrained_nsd_vld1 + mgmt-network: !!bool False + name: ns_constrained_nsd_vld1 + short-name: ns_constrained_nsd_vld1 + type: ELAN + vim-network-name: private + vnfd-connection-point-ref: + - member-vnf-index-ref: one + vnfd-connection-point-ref: vnf-cp0 + vnfd-id-ref: cirros_vnfd_v2 + - member-vnf-index-ref: two + vnfd-connection-point-ref: vnf-cp0 + vnfd-id-ref: cirros_vnfd_v2 + - id: three_vnf_constrained_nsd_vld2 + mgmt-network: !!bool False + name: ns_constrained_nsd_vld2 + short-name: ns_constrained_nsd_vld2 + type: ELAN + vim-network-name: private + vnfd-connection-point-ref: + - member-vnf-index-ref: two + vnfd-connection-point-ref: vnf-cp0 + vnfd-id-ref: cirros_vnfd_v2 + - member-vnf-index-ref: three + vnfd-connection-point-ref: vnf-cp0 + vnfd-id-ref: cirros_vnfd_v2 \ No newline at end of file diff --git a/osm_pla/test/pil_price_list.yaml b/osm_pla/test/pil_price_list.yaml new file mode 100644 index 0000000..45f7577 --- /dev/null +++ b/osm_pla/test/pil_price_list.yaml @@ -0,0 +1,60 @@ +# Copyright 2020 ArctosLabs Scandinavia AB +# +# 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. + +# POP Interconnecting Link (PIL), price list and latency +pil: + - pil_description: Link between OpenStack1 and OpenStack2 + pil_price: 5 + pil_latency: 30 + pil_jitter: 5 + pil_endpoints: + - http://10.234.12.47:5000/v3 + - http://10.234.12.44:5000/v3 + - pil_description: Link between OpenStack1 and OpenStack3 + pil_price: 5 + pil_latency: 70 + pil_jitter: 5 + pil_endpoints: + - http://10.234.12.47:5000/v3 + - http://10.234.12.46:5000/v3 + - pil_description: Link between OpenStack1 and OpenStack4 + pil_price: 10 + pil_latency: 80 + pil_jitter: 10 + pil_endpoints: + - http://10.234.12.47:5000/v3 + - http://10.234.12.43:5000/v3 + - pil_description: Link between OpenStack2 and OpenStack3 + pil_price: 5 + pil_latency: 75 + pil_jitter: 5 + pil_endpoints: + - http://10.234.12.44:5000/v3 + - http://10.234.12.46:5000/v3 + - pil_description: Link between OpenStack2 and OpenStack4 + pil_price: 10 + pil_latency: 60 + pil_jitter: 10 + pil_endpoints: + - http://10.234.12.44:5000/v3 + - http://10.234.12.43:5000/v3 + - pil_description: Link between OpenStack3 and OpenStack4 + pil_price: 10 + pil_latency: 40 + pil_jitter: 10 + pil_endpoints: + - http://10.234.12.46:5000/v3 + - http://10.234.12.43:5000/v3 + diff --git a/osm_pla/test/pil_price_list_rel7_webinar.yaml b/osm_pla/test/pil_price_list_rel7_webinar.yaml new file mode 100644 index 0000000..24d492f --- /dev/null +++ b/osm_pla/test/pil_price_list_rel7_webinar.yaml @@ -0,0 +1,47 @@ +# Copyright 2020 ArctosLabs Scandinavia AB +# +# 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. + +# POP Interconnecting Link (PIL), price list and latency +pil: + - pil_description: Link between OpenStack1 and OpenStack2 + pil_price: 5 + pil_latency: 20 + pil_endpoints: + - http://10.234.12.47:5000/v3 + - http://10.234.12.44:5000/v3 + - pil_description: Link between OpenStack1 and OpenStack3 + pil_price: 30 + pil_latency: 30 + pil_endpoints: + - http://10.234.12.47:5000/v3 + - http://10.234.12.46:5000/v3 + - pil_description: Link between OpenStack1 and OpenStack4 + pil_price: 30 + pil_latency: 30 + pil_endpoints: + - http://10.234.12.47:5000/v3 + - http://10.234.12.43:5000/v3 + - pil_description: Link between OpenStack2 and OpenStack3 + pil_price: 10 + pil_latency: 10 + pil_endpoints: + - http://10.234.12.44:5000/v3 + - http://10.234.12.46:5000/v3 + - pil_description: Link between OpenStack2 and OpenStack4 + pil_price: 10 + pil_latency: 10 + pil_endpoints: + - http://10.234.12.44:5000/v3 + - http://10.234.12.43:5000/v3 diff --git a/osm_pla/test/pil_unittest1.yaml b/osm_pla/test/pil_unittest1.yaml new file mode 100644 index 0000000..c1e228c --- /dev/null +++ b/osm_pla/test/pil_unittest1.yaml @@ -0,0 +1,59 @@ +# Copyright 2020 ArctosLabs Scandinavia AB +# +# 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. + +# POP Interconnecting Link (PIL), price list and latency +pil: + - pil_description: Link between OpenStack1 and OpenStack2 + pil_price: 12 + pil_latency: 120 + pil_jitter: 1200 + pil_endpoints: + - http://10.234.12.47:5000/v3 + - http://10.234.12.44:5000/v3 + - pil_description: Link between OpenStack1 and OpenStack3 + pil_price: 13 + pil_latency: 130 + pil_jitter: 1300 + pil_endpoints: + - http://10.234.12.47:5000/v3 + - http://10.234.12.46:5000/v3 + - pil_description: Link between OpenStack1 and OpenStack4 + pil_price: 14 + pil_latency: 140 + pil_jitter: 1400 + pil_endpoints: + - http://10.234.12.47:5000/v3 + - http://10.234.12.43:5000/v3 + - pil_description: Link between OpenStack2 and OpenStack3 + pil_price: 23 + pil_latency: 230 + pil_jitter: 2300 + pil_endpoints: + - http://10.234.12.44:5000/v3 + - http://10.234.12.46:5000/v3 + - pil_description: Link between OpenStack2 and OpenStack4 + pil_price: 24 + pil_latency: 240 + pil_jitter: 2400 + pil_endpoints: + - http://10.234.12.44:5000/v3 + - http://10.234.12.43:5000/v3 + - pil_description: Link between OpenStack3 and OpenStack4 + pil_price: 34 + pil_latency: 340 + pil_jitter: 3400 + pil_endpoints: + - http://10.234.12.46:5000/v3 + - http://10.234.12.43:5000/v3 diff --git a/osm_pla/test/pil_unittest2.yaml b/osm_pla/test/pil_unittest2.yaml new file mode 100644 index 0000000..a3165a8 --- /dev/null +++ b/osm_pla/test/pil_unittest2.yaml @@ -0,0 +1,55 @@ +# Copyright 2020 ArctosLabs Scandinavia AB +# +# 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. + +# POP Interconnecting Link (PIL), price list and latency +pil: + - pil_description: Link between OpenStack1 and OpenStack2 + pil_price: 12 + pil_latency: 120 + pil_jitter: 1200 + pil_endpoints: + - http://10.234.12.47:5000/v3 + - http://10.234.12.44:5000/v3 + - pil_description: Link between OpenStack1 and OpenStack3 + pil_price: 13 + pil_latency: 130 + pil_endpoints: + - http://10.234.12.47:5000/v3 + - http://10.234.12.46:5000/v3 + - pil_description: Link between OpenStack1 and OpenStack4 + pil_price: 14 + pil_jitter: 1400 + pil_endpoints: + - http://10.234.12.47:5000/v3 + - http://10.234.12.43:5000/v3 + - pil_description: Link between OpenStack2 and OpenStack3 + pil_price: 23 + pil_endpoints: + - http://10.234.12.44:5000/v3 + - http://10.234.12.46:5000/v3 + - pil_description: Link between OpenStack2 and OpenStack4 + pil_price: 24 + pil_latency: 240 + pil_jitter: 2400 + pil_endpoints: + - http://10.234.12.44:5000/v3 + - http://10.234.12.43:5000/v3 + - pil_description: Link between OpenStack3 and OpenStack4 + pil_price: 34 + pil_latency: 340 + pil_jitter: 3400 + pil_endpoints: + - http://10.234.12.46:5000/v3 + - http://10.234.12.43:5000/v3 diff --git a/osm_pla/test/test_five_nsd.yaml b/osm_pla/test/test_five_nsd.yaml new file mode 100644 index 0000000..0a209d1 --- /dev/null +++ b/osm_pla/test/test_five_nsd.yaml @@ -0,0 +1,104 @@ +# Copyright 2020 ArctosLabs Scandinavia AB +# +# 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. +nsd:nsd-catalog: + nsd: + - description: Four cirros VNF latency and jitter constrained + id: test_five_nsd + name: test_five_nsd + short-name: test_five_nsd + vendor: ArctosLabs + version: '1.0' + constituent-vnfd: + - member-vnf-index: 1 + vnfd-id-ref: test_one_a_vnfd + - member-vnf-index: 2 + vnfd-id-ref: test_two_vnfd + - member-vnf-index: 3 + vnfd-id-ref: test_one_a_vnfd + - member-vnf-index: 4 + vnfd-id-ref: test_one_a_vnfd + vld: + - name: vl_two_vld + id: vl_two_vld + mgmt-network: !!bool False + type: ELAN + link-constraint: + - constraint-type: LATENCY + value: 120 + - constraint-type: JITTER + value: 20 + vnfd-connection-point-ref: + - member-vnf-index-ref: '1' + vnfd-connection-point-ref: vnf_cp_one_cp + vnfd-id-ref: test_one_a_vnfd + - member-vnf-index-ref: '2' + vnfd-connection-point-ref: vnf_cp_one_cp + vnfd-id-ref: test_two_vnfd + - name: vl_four_vld + id: vl_four_vld + mgmt-network: !!bool False + type: ELAN + link-constraint: + - constraint-type: LATENCY + value: 50 + - constraint-type: JITTER + value: 10 + vnfd-connection-point-ref: + - member-vnf-index-ref: '2' + vnfd-connection-point-ref: vnf_cp_three_cp + vnfd-id-ref: test_two_vnfd + - member-vnf-index-ref: '4' + vnfd-connection-point-ref: vnf_cp_one_cp + vnfd-id-ref: test_one_a_vnfd + - name: vl_five_vld + id: vl_five_vld + mgmt-network: !!bool False + type: ELAN + link-constraint: + - constraint-type: LATENCY + value: 20 + - constraint-type: JITTER + value: 10 + vnfd-connection-point-ref: + - member-vnf-index-ref: '2' + vnfd-connection-point-ref: vnf_cp_two_cp + vnfd-id-ref: test_two_vnfd + - member-vnf-index-ref: '3' + vnfd-connection-point-ref: vnf_cp_one_cp + vnfd-id-ref: test_one_a_vnfd + - name: vld_vnf_mgmt + id: vld_vnf_mgmt1 + mgmt-network: !!bool True + type: ELAN + vnfd-connection-point-ref: + - member-vnf-index-ref: '1' + vnfd-connection-point-ref: vnf_cp_two_cp + vnfd-id-ref: test_one_a_vnfd + - member-vnf-index-ref: '3' + vnfd-connection-point-ref: vnf_cp_two_cp + vnfd-id-ref: test_one_a_vnfd + - name: vld_vnf_mgmt + id: vld_vnf_mgmt2 + mgmt-network: !!bool True + type: ELAN + vnfd-connection-point-ref: + - member-vnf-index-ref: '1' + vnfd-connection-point-ref: vnf_cp_two_cp + vnfd-id-ref: test_one_a_vnfd + - member-vnf-index-ref: '4' + vnfd-connection-point-ref: vnf_cp_two_cp + vnfd-id-ref: test_one_a_vnfd + + diff --git a/osm_pla/test/test_mznModelGenerator.py b/osm_pla/test/test_mznModelGenerator.py new file mode 100644 index 0000000..ed571f4 --- /dev/null +++ b/osm_pla/test/test_mznModelGenerator.py @@ -0,0 +1,701 @@ +# Copyright 2020 ArctosLabs Scandinavia AB +# +# 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 datetime +import logging +# import unittest +from unittest import TestCase +# import random +# from operator import itemgetter +import re + +from jinja2 import Template + +from osm_pla.placement.mznplacement import MznModelGenerator + +test_ns_placement_data_str = { + 'vim_accounts': ['vim' + vim_account.replace('-', '_') for vim_account in ['aaaaaaaa-38f5-438d-b8ee-3f93b3531f87', + 'bbbbbbbb-38f5-438d-b8ee-3f93b3531f87', + 'cccccccc-ed84-4e49-b5df-a9d117bd731f', + 'dddddddd-ed84-4e49-b5df-a9d117bd731f', + 'eeeeeeee-38f5-438d-b8ee-3f93b3531f87']], + 'trp_link_latency': [[0, 50, 100, 150, 200], [0, 0, 100, 150, 200], [0, 0, 0, 150, 200], [0, 0, 0, 0, 200], + [0, 0, 0, 0, 0]], + 'trp_link_jitter': [[0, 5, 10, 15, 20], [0, 0, 10, 15, 20], [0, 0, 0, 15, 20], [0, 0, 0, 0, 20], + [0, 0, 0, 0, 0]], + 'trp_link_price_list': [[0, 5, 6, 6, 7], [0, 0, 6, 6, 7], [0, 0, 0, 6, 7], [0, 0, 0, 0, 7], [0, 0, 0, 0, 0]], + 'ns_desc': [ + {'vnf_id': 'one', 'vnf_price_per_vim': [50, 51, 52, 53, 54]}, + {'vnf_id': 'two', 'vnf_price_per_vim': [20, 21, 22, 23, 24]}, + {'vnf_id': 'three', 'vnf_price_per_vim': [70, 71, 72, 73, 74]}, + {'vnf_id': 'four', 'vnf_price_per_vim': [40, 41, 42, 43, 44]}], + 'vld_desc': [{'cp_refs': ['one', 'two'], 'latency': 150, 'jitter': 30}, + {'cp_refs': ['two', 'three'], 'latency': 140, 'jitter': 30}, + {'cp_refs': ['three', 'four'], 'latency': 130, 'jitter': 30}], + 'generator_data': {'file': __file__, 'time': datetime.datetime.now()} +} + +test_ns_placement_data_str_no_vld_constraints = { + 'vim_accounts': ['vim' + vim_account.replace('-', '_') for vim_account in ['aaaaaaaa-38f5-438d-b8ee-3f93b3531f87', + 'bbbbbbbb-38f5-438d-b8ee-3f93b3531f87', + 'cccccccc-ed84-4e49-b5df-a9d117bd731f', + 'dddddddd-ed84-4e49-b5df-a9d117bd731f', + 'eeeeeeee-38f5-438d-b8ee-3f93b3531f87']], + 'trp_link_latency': [[0, 50, 100, 150, 200], [0, 0, 100, 150, 200], [0, 0, 0, 150, 200], [0, 0, 0, 0, 200], + [0, 0, 0, 0, 0]], + 'trp_link_jitter': [[0, 5, 10, 15, 20], [0, 0, 10, 15, 20], [0, 0, 0, 15, 20], [0, 0, 0, 0, 20], + [0, 0, 0, 0, 0]], + 'trp_link_price_list': [[0, 5, 6, 6, 7], [0, 0, 6, 6, 7], [0, 0, 0, 6, 7], [0, 0, 0, 0, 7], [0, 0, 0, 0, 0]], + 'ns_desc': [ + {'vnf_id': 'one', 'vnf_price_per_vim': [50, 51, 52, 53, 54]}, + {'vnf_id': 'two', 'vnf_price_per_vim': [20, 21, 22, 23, 24]}, + {'vnf_id': 'three', 'vnf_price_per_vim': [70, 71, 72, 73, 74]}, + {'vnf_id': 'four', 'vnf_price_per_vim': [40, 41, 42, 43, 44]}], + 'vld_desc': [{'cp_refs': ['one', 'two']}, + {'cp_refs': ['two', 'three']}, + {'cp_refs': ['three', 'four']}], + 'generator_data': {'file': __file__, 'time': datetime.datetime.now()} +} + +test_ns_placement_data = { + 'vim_accounts': ['vim' + vim_account.replace('-', '_') for vim_account in ['aaaaaaaa-38f5-438d-b8ee-3f93b3531f87', + 'bbbbbbbb-38f5-438d-b8ee-3f93b3531f87', + 'cccccccc-ed84-4e49-b5df-a9d117bd731f', + 'dddddddd-ed84-4e49-b5df-a9d117bd731f', + 'eeeeeeee-38f5-438d-b8ee-3f93b3531f87']], + 'trp_link_latency': [[0, 50, 100, 150, 200], [0, 0, 100, 150, 200], [0, 0, 0, 150, 200], [0, 0, 0, 0, 200], + [0, 0, 0, 0, 0]], + 'trp_link_jitter': [[0, 5, 10, 15, 20], [0, 0, 10, 15, 20], [0, 0, 0, 15, 20], [0, 0, 0, 0, 20], + [0, 0, 0, 0, 0]], + 'trp_link_price_list': [[0, 5, 6, 6, 7], [0, 0, 6, 6, 7], [0, 0, 0, 6, 7], [0, 0, 0, 0, 7], [0, 0, 0, 0, 0]], + 'ns_desc': [ + {'vnf_id': '1', 'vnf_price_per_vim': [50, 51, 52, 53, 54]}, + {'vnf_id': '2', 'vnf_price_per_vim': [20, 21, 22, 23, 24]}, + {'vnf_id': '3', 'vnf_price_per_vim': [70, 71, 72, 73, 74]}, + {'vnf_id': '4', 'vnf_price_per_vim': [40, 41, 42, 43, 44]}], + 'vld_desc': [{'cp_refs': ['1', '2'], 'latency': 150, 'jitter': 30}, + {'cp_refs': ['2', '3'], 'latency': 140, 'jitter': 30}, + {'cp_refs': ['3', '4'], 'latency': 130, 'jitter': 30}], + 'generator_data': {'file': __file__, 'time': datetime.datetime.now()} +} + +test_ns_placement_data_w_pinning = { + 'vim_accounts': ['vim' + vim_account.replace('-', '_') for vim_account in ['aaaaaaaa-38f5-438d-b8ee-3f93b3531f87', + 'bbbbbbbb-38f5-438d-b8ee-3f93b3531f87', + 'cccccccc-ed84-4e49-b5df-a9d117bd731f', + 'dddddddd-ed84-4e49-b5df-a9d117bd731f', + 'eeeeeeee-38f5-438d-b8ee-3f93b3531f87']], + 'trp_link_latency': [[0, 50, 100, 150, 200], [0, 0, 100, 150, 200], [0, 0, 0, 150, 200], [0, 0, 0, 0, 200], + [0, 0, 0, 0, 0]], + 'trp_link_jitter': [[0, 5, 10, 15, 20], [0, 0, 10, 15, 20], [0, 0, 0, 15, 20], [0, 0, 0, 0, 20], + [0, 0, 0, 0, 0]], + 'trp_link_price_list': [[0, 5, 6, 6, 7], [0, 0, 6, 6, 7], [0, 0, 0, 6, 7], [0, 0, 0, 0, 7], [0, 0, 0, 0, 0]], + 'ns_desc': [ + {'vnf_id': '1', 'vnf_price_per_vim': [50, 51, 52, 53, 54]}, + {'vnf_id': '2', 'vnf_price_per_vim': [20, 21, 22, 23, 24], + 'vim_account': 'vimeeeeeeee_38f5_438d_b8ee_3f93b3531f87'}, + {'vnf_id': '3', 'vnf_price_per_vim': [70, 71, 72, 73, 74]}, + {'vnf_id': '4', 'vnf_price_per_vim': [40, 41, 42, 43, 44], + 'vim_account': 'vimcccccccc_ed84_4e49_b5df_a9d117bd731f'}], + 'vld_desc': [{'cp_refs': ['1', '2'], 'latency': 150, 'jitter': 30}, + {'cp_refs': ['2', '3'], 'latency': 140, 'jitter': 30}, + {'cp_refs': ['3', '4'], 'latency': 130, 'jitter': 30}], + 'generator_data': {'file': __file__, 'time': datetime.datetime.now()} +} + +test_ns_placement_data_w_pinning_str = { + 'vim_accounts': ['vim' + vim_account.replace('-', '_') for vim_account in ['aaaaaaaa-38f5-438d-b8ee-3f93b3531f87', + 'bbbbbbbb-38f5-438d-b8ee-3f93b3531f87', + 'cccccccc-ed84-4e49-b5df-a9d117bd731f', + 'dddddddd-ed84-4e49-b5df-a9d117bd731f', + 'eeeeeeee-38f5-438d-b8ee-3f93b3531f87']], + 'trp_link_latency': [[0, 50, 100, 150, 200], [0, 0, 100, 150, 200], [0, 0, 0, 150, 200], [0, 0, 0, 0, 200], + [0, 0, 0, 0, 0]], + 'trp_link_jitter': [[0, 5, 10, 15, 20], [0, 0, 10, 15, 20], [0, 0, 0, 15, 20], [0, 0, 0, 0, 20], + [0, 0, 0, 0, 0]], + 'trp_link_price_list': [[0, 5, 6, 6, 7], [0, 0, 6, 6, 7], [0, 0, 0, 6, 7], [0, 0, 0, 0, 7], [0, 0, 0, 0, 0]], + 'ns_desc': [ + {'vnf_id': 'one', 'vnf_price_per_vim': [50, 51, 52, 53, 54]}, + {'vnf_id': 'two', 'vnf_price_per_vim': [20, 21, 22, 23, 24], + 'vim_account': 'vimeeeeeeee_38f5_438d_b8ee_3f93b3531f87'}, + {'vnf_id': 'three', 'vnf_price_per_vim': [70, 71, 72, 73, 74]}, + {'vnf_id': 'four', 'vnf_price_per_vim': [40, 41, 42, 43, 44], + 'vim_account': 'vimcccccccc_ed84_4e49_b5df_a9d117bd731f'}], + 'vld_desc': [{'cp_refs': ['one', 'two'], 'latency': 150, 'jitter': 30}, + {'cp_refs': ['two', 'three'], 'latency': 140, 'jitter': 30}, + {'cp_refs': ['three', 'four'], 'latency': 130, 'jitter': 30}], + 'generator_data': {'file': __file__, 'time': datetime.datetime.now()} +} + +test_ns_placement_data_str_no_vld = { + 'vim_accounts': ['vim' + vim_account.replace('-', '_') for vim_account in ['aaaaaaaa-38f5-438d-b8ee-3f93b3531f87', + 'bbbbbbbb-38f5-438d-b8ee-3f93b3531f87', + 'cccccccc-ed84-4e49-b5df-a9d117bd731f', + 'dddddddd-ed84-4e49-b5df-a9d117bd731f', + 'eeeeeeee-38f5-438d-b8ee-3f93b3531f87']], + 'trp_link_latency': [[0, 50, 100, 150, 200], [0, 0, 100, 150, 200], [0, 0, 0, 150, 200], [0, 0, 0, 0, 200], + [0, 0, 0, 0, 0]], + 'trp_link_jitter': [[0, 5, 10, 15, 20], [0, 0, 10, 15, 20], [0, 0, 0, 15, 20], [0, 0, 0, 0, 20], + [0, 0, 0, 0, 0]], + 'trp_link_price_list': [[0, 5, 6, 6, 7], [0, 0, 6, 6, 7], [0, 0, 0, 6, 7], [0, 0, 0, 0, 7], [0, 0, 0, 0, 0]], + 'ns_desc': [ + {'vnf_id': 'one', 'vnf_price_per_vim': [50, 51, 52, 53, 54]}], + 'vld_desc': [], + 'generator_data': {'file': __file__, 'time': datetime.datetime.now()} +} + +expected_model_fragment = """ +%This is the NETWORK RESOURCE MODEL +enum Vims = { +vimaaaaaaaa_38f5_438d_b8ee_3f93b3531f87, +vimbbbbbbbb_38f5_438d_b8ee_3f93b3531f87, +vimcccccccc_ed84_4e49_b5df_a9d117bd731f, +vimdddddddd_ed84_4e49_b5df_a9d117bd731f, +vimeeeeeeee_38f5_438d_b8ee_3f93b3531f87}; % The vim-accounts +array[Vims, Vims] of int: trp_link_latency = [|0,50,100,150,200, +|0,0,100,150,200, +|0,0,0,150,200, +|0,0,0,0,200, +|0,0,0,0,0, +|]; % Transport link latency between data centers +array[Vims, Vims] of int: trp_link_jitter = [|0,5,10,15,20, +|0,0,10,15,20, +|0,0,0,15,20, +|0,0,0,0,20, +|0,0,0,0,0, +|]; % Transport link jitter between data centers +array[Vims, Vims] of int: trp_link_price_list = [|0,5,6,6,7, +|0,0,6,6,7, +|0,0,0,6,7, +|0,0,0,0,7, +|0,0,0,0,0, +|]; % Transport link price list +array[Vims] of int: vim_price_list_1 = [50,51,52,53,54]; +array[Vims] of int: vim_price_list_2 = [20,21,22,23,24]; +array[Vims] of int: vim_price_list_3 = [70,71,72,73,74]; +array[Vims] of int: vim_price_list_4 = [40,41,42,43,44]; + + +% This is the NETWORK BASIC LOAD MODEL (CONSUMED) +% NOTE. This is not applicable in OSM Release 7 + +% This is the SERVICE CONSUMPTION MODEL +% These are the variables, i.e. which DC to select for each VNF +var Vims: VNF1; +var Vims: VNF2; +var Vims: VNF3; +var Vims: VNF4; + + +% These are the set of rules for selecting DCs to VNFs +constraint trp_link_latency[VNF1, VNF2] <= 150; +constraint trp_link_latency[VNF2, VNF3] <= 140; +constraint trp_link_latency[VNF3, VNF4] <= 130; +constraint trp_link_jitter[VNF1, VNF2] <= 30; +constraint trp_link_jitter[VNF2, VNF3] <= 30; +constraint trp_link_jitter[VNF3, VNF4] <= 30; + +% Calculate the cost for VNFs and cost for transport link and total cost +var int: used_transport_cost =trp_link_price_list[VNF1, VNF2]+ +trp_link_price_list[VNF2, VNF3]+ +trp_link_price_list[VNF3, VNF4]; + +var int: used_vim_cost =vim_price_list_1[VNF1]+ +vim_price_list_2[VNF2]+ +vim_price_list_3[VNF3]+ +vim_price_list_4[VNF4]; + +var int: total_cost = used_transport_cost + used_vim_cost; + +solve minimize total_cost; +""" +expected_model_fragment_str = """ +%This is the NETWORK RESOURCE MODEL +enum Vims = { +vimaaaaaaaa_38f5_438d_b8ee_3f93b3531f87, +vimbbbbbbbb_38f5_438d_b8ee_3f93b3531f87, +vimcccccccc_ed84_4e49_b5df_a9d117bd731f, +vimdddddddd_ed84_4e49_b5df_a9d117bd731f, +vimeeeeeeee_38f5_438d_b8ee_3f93b3531f87}; % The vim-accounts +array[Vims, Vims] of int: trp_link_latency = [|0,50,100,150,200, +|0,0,100,150,200, +|0,0,0,150,200, +|0,0,0,0,200, +|0,0,0,0,0, +|]; % Transport link latency between data centers +array[Vims, Vims] of int: trp_link_jitter = [|0,5,10,15,20, +|0,0,10,15,20, +|0,0,0,15,20, +|0,0,0,0,20, +|0,0,0,0,0, +|]; % Transport link jitter between data centers +array[Vims, Vims] of int: trp_link_price_list = [|0,5,6,6,7, +|0,0,6,6,7, +|0,0,0,6,7, +|0,0,0,0,7, +|0,0,0,0,0, +|]; % Transport link price list +array[Vims] of int: vim_price_list_one = [50,51,52,53,54]; +array[Vims] of int: vim_price_list_two = [20,21,22,23,24]; +array[Vims] of int: vim_price_list_three = [70,71,72,73,74]; +array[Vims] of int: vim_price_list_four = [40,41,42,43,44]; + + +% This is the NETWORK BASIC LOAD MODEL (CONSUMED) +% NOTE. This is not applicable in OSM Release 7 + +% This is the SERVICE CONSUMPTION MODEL +% These are the variables, i.e. which DC to select for each VNF +var Vims: VNFone; +var Vims: VNFtwo; +var Vims: VNFthree; +var Vims: VNFfour; + + +% These are the set of rules for selecting DCs to VNFs +constraint trp_link_latency[VNFone, VNFtwo] <= 150; +constraint trp_link_latency[VNFtwo, VNFthree] <= 140; +constraint trp_link_latency[VNFthree, VNFfour] <= 130; +constraint trp_link_jitter[VNFone, VNFtwo] <= 30; +constraint trp_link_jitter[VNFtwo, VNFthree] <= 30; +constraint trp_link_jitter[VNFthree, VNFfour] <= 30; + +% Calculate the cost for VNFs and cost for transport link and total cost +var int: used_transport_cost =trp_link_price_list[VNFone, VNFtwo]+ +trp_link_price_list[VNFtwo, VNFthree]+ +trp_link_price_list[VNFthree, VNFfour]; + +var int: used_vim_cost =vim_price_list_one[VNFone]+ +vim_price_list_two[VNFtwo]+ +vim_price_list_three[VNFthree]+ +vim_price_list_four[VNFfour]; + +var int: total_cost = used_transport_cost + used_vim_cost; + +solve minimize total_cost; +""" + +expected_model_fragment_str_no_vld_constraints = """ +%This is the NETWORK RESOURCE MODEL +enum Vims = { +vimaaaaaaaa_38f5_438d_b8ee_3f93b3531f87, +vimbbbbbbbb_38f5_438d_b8ee_3f93b3531f87, +vimcccccccc_ed84_4e49_b5df_a9d117bd731f, +vimdddddddd_ed84_4e49_b5df_a9d117bd731f, +vimeeeeeeee_38f5_438d_b8ee_3f93b3531f87}; % The vim-accounts +array[Vims, Vims] of int: trp_link_latency = [|0,50,100,150,200, +|0,0,100,150,200, +|0,0,0,150,200, +|0,0,0,0,200, +|0,0,0,0,0, +|]; % Transport link latency between data centers +array[Vims, Vims] of int: trp_link_jitter = [|0,5,10,15,20, +|0,0,10,15,20, +|0,0,0,15,20, +|0,0,0,0,20, +|0,0,0,0,0, +|]; % Transport link jitter between data centers +array[Vims, Vims] of int: trp_link_price_list = [|0,5,6,6,7, +|0,0,6,6,7, +|0,0,0,6,7, +|0,0,0,0,7, +|0,0,0,0,0, +|]; % Transport link price list +array[Vims] of int: vim_price_list_one = [50,51,52,53,54]; +array[Vims] of int: vim_price_list_two = [20,21,22,23,24]; +array[Vims] of int: vim_price_list_three = [70,71,72,73,74]; +array[Vims] of int: vim_price_list_four = [40,41,42,43,44]; + + +% This is the NETWORK BASIC LOAD MODEL (CONSUMED) +% NOTE. This is not applicable in OSM Release 7 + +% This is the SERVICE CONSUMPTION MODEL +% These are the variables, i.e. which DC to select for each VNF +var Vims: VNFone; +var Vims: VNFtwo; +var Vims: VNFthree; +var Vims: VNFfour; + + +% These are the set of rules for selecting DCs to VNFs + +% Calculate the cost for VNFs and cost for transport link and total cost +var int: used_transport_cost =trp_link_price_list[VNFone, VNFtwo]+ +trp_link_price_list[VNFtwo, VNFthree]+ +trp_link_price_list[VNFthree, VNFfour]; + +var int: used_vim_cost =vim_price_list_one[VNFone]+ +vim_price_list_two[VNFtwo]+ +vim_price_list_three[VNFthree]+ +vim_price_list_four[VNFfour]; + +var int: total_cost = used_transport_cost + used_vim_cost; + +solve minimize total_cost; +""" + +expected_model_fragment_w_pinning = """ +%This is the NETWORK RESOURCE MODEL +enum Vims = { +vimaaaaaaaa_38f5_438d_b8ee_3f93b3531f87, +vimbbbbbbbb_38f5_438d_b8ee_3f93b3531f87, +vimcccccccc_ed84_4e49_b5df_a9d117bd731f, +vimdddddddd_ed84_4e49_b5df_a9d117bd731f, +vimeeeeeeee_38f5_438d_b8ee_3f93b3531f87}; % The vim-accounts +array[Vims, Vims] of int: trp_link_latency = [|0,50,100,150,200, +|0,0,100,150,200, +|0,0,0,150,200, +|0,0,0,0,200, +|0,0,0,0,0, +|]; % Transport link latency between data centers +array[Vims, Vims] of int: trp_link_jitter = [|0,5,10,15,20, +|0,0,10,15,20, +|0,0,0,15,20, +|0,0,0,0,20, +|0,0,0,0,0, +|]; % Transport link jitter between data centers +array[Vims, Vims] of int: trp_link_price_list = [|0,5,6,6,7, +|0,0,6,6,7, +|0,0,0,6,7, +|0,0,0,0,7, +|0,0,0,0,0, +|]; % Transport link price list +array[Vims] of int: vim_price_list_1 = [50,51,52,53,54]; +array[Vims] of int: vim_price_list_2 = [20,21,22,23,24]; +array[Vims] of int: vim_price_list_3 = [70,71,72,73,74]; +array[Vims] of int: vim_price_list_4 = [40,41,42,43,44]; + + +% This is the NETWORK BASIC LOAD MODEL (CONSUMED) +% NOTE. This is not applicable in OSM Release 7 + +% This is the SERVICE CONSUMPTION MODEL +% These are the variables, i.e. which DC to select for each VNF +var Vims: VNF1; +Vims: VNF2 = vimeeeeeeee_38f5_438d_b8ee_3f93b3531f87; +var Vims: VNF3; +Vims: VNF4 = vimcccccccc_ed84_4e49_b5df_a9d117bd731f; + + +% These are the set of rules for selecting DCs to VNFs +constraint trp_link_latency[VNF1, VNF2] <= 150; +constraint trp_link_latency[VNF2, VNF3] <= 140; +constraint trp_link_latency[VNF3, VNF4] <= 130; +constraint trp_link_jitter[VNF1, VNF2] <= 30; +constraint trp_link_jitter[VNF2, VNF3] <= 30; +constraint trp_link_jitter[VNF3, VNF4] <= 30; + +% Calculate the cost for VNFs and cost for transport link and total cost +var int: used_transport_cost =trp_link_price_list[VNF1, VNF2]+ +trp_link_price_list[VNF2, VNF3]+ +trp_link_price_list[VNF3, VNF4]; + +var int: used_vim_cost =vim_price_list_1[VNF1]+ +vim_price_list_2[VNF2]+ +vim_price_list_3[VNF3]+ +vim_price_list_4[VNF4]; + +var int: total_cost = used_transport_cost + used_vim_cost; + +solve minimize total_cost; +""" + +expected_model_fragment_w_pinning_str = """ +%This is the NETWORK RESOURCE MODEL +enum Vims = { +vimaaaaaaaa_38f5_438d_b8ee_3f93b3531f87, +vimbbbbbbbb_38f5_438d_b8ee_3f93b3531f87, +vimcccccccc_ed84_4e49_b5df_a9d117bd731f, +vimdddddddd_ed84_4e49_b5df_a9d117bd731f, +vimeeeeeeee_38f5_438d_b8ee_3f93b3531f87}; % The vim-accounts +array[Vims, Vims] of int: trp_link_latency = [|0,50,100,150,200, +|0,0,100,150,200, +|0,0,0,150,200, +|0,0,0,0,200, +|0,0,0,0,0, +|]; % Transport link latency between data centers +array[Vims, Vims] of int: trp_link_jitter = [|0,5,10,15,20, +|0,0,10,15,20, +|0,0,0,15,20, +|0,0,0,0,20, +|0,0,0,0,0, +|]; % Transport link jitter between data centers +array[Vims, Vims] of int: trp_link_price_list = [|0,5,6,6,7, +|0,0,6,6,7, +|0,0,0,6,7, +|0,0,0,0,7, +|0,0,0,0,0, +|]; % Transport link price list +array[Vims] of int: vim_price_list_one = [50,51,52,53,54]; +array[Vims] of int: vim_price_list_two = [20,21,22,23,24]; +array[Vims] of int: vim_price_list_three = [70,71,72,73,74]; +array[Vims] of int: vim_price_list_four = [40,41,42,43,44]; + + +% This is the NETWORK BASIC LOAD MODEL (CONSUMED) +% NOTE. This is not applicable in OSM Release 7 + +% This is the SERVICE CONSUMPTION MODEL +% These are the variables, i.e. which DC to select for each VNF +var Vims: VNFone; +Vims: VNFtwo = vimeeeeeeee_38f5_438d_b8ee_3f93b3531f87; +var Vims: VNFthree; +Vims: VNFfour = vimcccccccc_ed84_4e49_b5df_a9d117bd731f; + + +% These are the set of rules for selecting DCs to VNFs +constraint trp_link_latency[VNFone, VNFtwo] <= 150; +constraint trp_link_latency[VNFtwo, VNFthree] <= 140; +constraint trp_link_latency[VNFthree, VNFfour] <= 130; +constraint trp_link_jitter[VNFone, VNFtwo] <= 30; +constraint trp_link_jitter[VNFtwo, VNFthree] <= 30; +constraint trp_link_jitter[VNFthree, VNFfour] <= 30; + +% Calculate the cost for VNFs and cost for transport link and total cost +var int: used_transport_cost =trp_link_price_list[VNFone, VNFtwo]+ +trp_link_price_list[VNFtwo, VNFthree]+ +trp_link_price_list[VNFthree, VNFfour]; + +var int: used_vim_cost =vim_price_list_one[VNFone]+ +vim_price_list_two[VNFtwo]+ +vim_price_list_three[VNFthree]+ +vim_price_list_four[VNFfour]; + +var int: total_cost = used_transport_cost + used_vim_cost; + +solve minimize total_cost; +""" + +expected_model_fragment_str_no_vld = """ +%This is the NETWORK RESOURCE MODEL +enum Vims = { +vimaaaaaaaa_38f5_438d_b8ee_3f93b3531f87, +vimbbbbbbbb_38f5_438d_b8ee_3f93b3531f87, +vimcccccccc_ed84_4e49_b5df_a9d117bd731f, +vimdddddddd_ed84_4e49_b5df_a9d117bd731f, +vimeeeeeeee_38f5_438d_b8ee_3f93b3531f87}; % The vim-accounts +array[Vims, Vims] of int: trp_link_latency = [|0,50,100,150,200, +|0,0,100,150,200, +|0,0,0,150,200, +|0,0,0,0,200, +|0,0,0,0,0, +|]; % Transport link latency between data centers +array[Vims, Vims] of int: trp_link_jitter = [|0,5,10,15,20, +|0,0,10,15,20, +|0,0,0,15,20, +|0,0,0,0,20, +|0,0,0,0,0, +|]; % Transport link jitter between data centers +array[Vims, Vims] of int: trp_link_price_list = [|0,5,6,6,7, +|0,0,6,6,7, +|0,0,0,6,7, +|0,0,0,0,7, +|0,0,0,0,0, +|]; % Transport link price list +array[Vims] of int: vim_price_list_one = [50,51,52,53,54]; + + +% This is the NETWORK BASIC LOAD MODEL (CONSUMED) +% NOTE. This is not applicable in OSM Release 7 + +% This is the SERVICE CONSUMPTION MODEL +% These are the variables, i.e. which DC to select for each VNF + +var Vims: VNFone; + +% These are the set of rules for selecting DCs to VNFs + +% Calculate the cost for VNFs and cost for transport link and total cost +var int: used_transport_cost =0; + +var int: used_vim_cost =vim_price_list_one[VNFone]; + +var int: total_cost = used_transport_cost + used_vim_cost; + +solve minimize total_cost; +""" + + +class TestMznModelGenerator(TestCase): + def test_create_model(self): + mg = MznModelGenerator(logging.getLogger(__name__)) + mzn_model = mg.create_model(test_ns_placement_data_str) + + # so asserting exact content is difficult due to the datetime.now(), therefore we ignore the first lines + self.assertTrue(expected_model_fragment_str.replace('\n', '') in + mzn_model.replace('\n', ''), "faulty model generated") + + def test_create_model_no_vld_constraints(self): + """ + instantiate w/o constraints in nsd or order params has a valid model + :return: + """ + mg = MznModelGenerator(logging.getLogger(__name__)) + mzn_model = mg.create_model(test_ns_placement_data_str_no_vld_constraints) + + # so asserting exact content is difficult due to the datetime.now(), therefore we ignore the first lines + self.assertTrue(expected_model_fragment_str_no_vld_constraints.replace('\n', '') in + mzn_model.replace('\n', ''), "faulty model generated") + + def test_create_model_w_pinning(self): + mg = MznModelGenerator(logging.getLogger(__name__)) + mzn_model = mg.create_model(test_ns_placement_data_w_pinning_str) + + # so asserting exact content is difficult due to the datetime.now(), therefore we ignore the first lines + self.assertTrue(expected_model_fragment_w_pinning_str.replace('\n', '') in + mzn_model.replace('\n', ''), "faulty model generated") + + def test_create_model_no_vld(self): + mg = MznModelGenerator(logging.getLogger(__name__)) + mzn_model = mg.create_model(test_ns_placement_data_str_no_vld) + + # so asserting exact content is difficult due to the datetime.now(), therefore we ignore the first lines + self.assertTrue(expected_model_fragment_str_no_vld.replace('\n', '') in + mzn_model.replace('\n', ''), "faulty model generated") + + def test__load_jinja_template(self): + """ + + add other test to check exception if template not loaded (e.g. invalid template name, + perhaps also valid name but invalid content (in case jinja2 detects such things)) + """ + mg = MznModelGenerator(logging.getLogger(__name__)) + template = mg._load_jinja_template() # Note we use the default template + self.assertTrue(isinstance(template, Template), "failed to load jinja2 template") + + def test_vim_account_replace(self): + mg = MznModelGenerator(logging.getLogger(__name__)) + nspd = test_ns_placement_data_str + mzn_model = mg.create_model(nspd) + + expected = '%This is the NETWORK RESOURCE MODEL' + '\n' + 'enum Vims = {' + '\n' + for val in test_ns_placement_data_str['vim_accounts']: + expected = expected + val.replace('-', '_') + ',\n' + expected = expected[:-2] + '}; % The vim-accounts' + res = re.findall(expected, mzn_model) + self.assertEqual(1, len(res), "vim accounts didnt replace from - to _") + + def test_trp_link_price_list(self): + mg = MznModelGenerator(logging.getLogger(__name__)) + mzn_model = mg.create_model(test_ns_placement_data_str) + + expected = 'array\\[Vims, Vims\\] of int: trp_link_price_list = \\[' + for price_list in test_ns_placement_data_str['trp_link_price_list']: + expected = expected + '\\|' + (str(price_list)[1:-1]).replace(" ", "") + ',\n' + expected = expected + '\\|\\]; % Transport link price list' + res = re.findall(expected, mzn_model) + self.assertEqual(1, len(res), "price list is not correct") + + def test_link_latency(self): + mg = MznModelGenerator(logging.getLogger(__name__)) + mzn_model = mg.create_model(test_ns_placement_data_str) + + expected = 'array\\[Vims, Vims\\] of int: trp_link_latency = \\[' + for link_latency in test_ns_placement_data_str['trp_link_latency']: + expected = expected + '\\|' + (str(link_latency)[1:-1]).replace(" ", "") + ',\n' + expected = expected + '\\|\\]; % Transport link latency between data centers' + res = re.findall(expected, mzn_model) + self.assertEqual(1, len(res), "trp_link_latency values is not correct") + + def test_link_jitter(self): + mg = MznModelGenerator(logging.getLogger(__name__)) + mzn_model = mg.create_model(test_ns_placement_data_str) + + expected = 'array\\[Vims, Vims\\] of int: trp_link_jitter = \\[' + for link_jitter in test_ns_placement_data_str['trp_link_jitter']: + expected = expected + '\\|' + (str(link_jitter)[1:-1]).replace(" ", "") + ',\n' + expected = expected + '\\|\\]; % Transport link jitter between data centers' + + res = re.findall(expected, mzn_model) + + self.assertEqual(1, len(res), "trp_link_jitter values is not correct") + + def test_price_per_vim(self): + mg = MznModelGenerator(logging.getLogger(__name__)) + mzn_model = mg.create_model(test_ns_placement_data_w_pinning_str) + + expected = "" + for price_list in test_ns_placement_data_w_pinning_str['ns_desc']: + expected += 'array\\[Vims\\] of int: vim_price_list_' + price_list.get('vnf_id') + " = " + temp = str(price_list.get('vnf_price_per_vim'))[1:-1].replace(" ", "") + expected += "\\[" + temp + "\\];\n" + + res = re.findall(expected, mzn_model) + self.assertEqual(1, len(res), "mzn_model contains pinning") + + def test_pinning(self): + mg = MznModelGenerator(logging.getLogger(__name__)) + mzn_model = mg.create_model(test_ns_placement_data_str) + + expected = "" + for pin_list in test_ns_placement_data_str['ns_desc']: + if pin_list.get('vim_account'): + expected += 'Vims: VNF' + pin_list.get('vnf_id') + ' = ' + pin_list.get('vim_account') + ';\n' + else: + expected += 'var Vims: VNF' + pin_list.get('vnf_id') + ';\n' + + res = re.findall(expected, mzn_model) + self.assertEqual(1, len(res), "mzn_model has no pinning") + + def test__without_pinning(self): + mg = MznModelGenerator(logging.getLogger(__name__)) + mzn_model = mg.create_model(test_ns_placement_data_w_pinning_str) + + expected = "" + for pin_list in test_ns_placement_data_w_pinning_str['ns_desc']: + if pin_list.get('vim_account'): + expected += 'Vims: VNF' + pin_list.get('vnf_id') + ' = ' + pin_list.get('vim_account') + ';\n' + else: + expected += 'var Vims: VNF' + pin_list.get('vnf_id') + ';\n' + + res = re.findall(expected, mzn_model) + self.assertEqual(1, len(res), "mzn_model contains pinning") + + def test__without_constraints_for_jitter_and_latency(self): + mg = MznModelGenerator(logging.getLogger(__name__)) + mzn_model = mg.create_model(test_ns_placement_data_str_no_vld_constraints) + + expected_latency = "constraint trp_link_latency" + expected_jitter = "constraint trp_link_jitter" + latency_or_jitter_was_found = 0 + for l_o_j in test_ns_placement_data_str_no_vld_constraints['vld_desc']: + if l_o_j.get('latency') or l_o_j.get('jitter'): + latency_or_jitter_was_found = 1 + + res_latency = re.findall(expected_latency, mzn_model) + res_jitter = re.findall(expected_jitter, mzn_model) + self.assertEqual(0, latency_or_jitter_was_found, "Jitter or latency was found in the test input") + self.assertEqual(0, len(res_latency), "constraint trp_link_latency was found in mzn_model") + self.assertEqual(0, len(res_jitter), "constraint trp_link_latency was found in mzn_model") + + def test__constraints_for_jitter_and_latency(self): + mg = MznModelGenerator(logging.getLogger(__name__)) + mzn_model = mg.create_model(test_ns_placement_data_str) + + expected_latency = "" + expected_jitter = "" + latency_or_jitter_was_found = 0 + for l_o_j in test_ns_placement_data_str['vld_desc']: + if not (l_o_j.get('latency') or l_o_j.get('jitter')): + latency_or_jitter_was_found = 1 + expected_latency += "constraint trp_link_latency" + "\\[VNF" + l_o_j.get('cp_refs')[0] + ", VNF" + \ + l_o_j.get('cp_refs')[1] + "\\] \\<= " + str(l_o_j.get('latency')) + ";\n\n" + + expected_jitter += "constraint trp_link_jitter" + "\\[VNF" + l_o_j.get('cp_refs')[0] + ", VNF" + \ + l_o_j.get('cp_refs')[1] + "\\] \\<= " + str(l_o_j.get('jitter')) + ";\n\n" + + res = re.findall(expected_latency + expected_jitter, mzn_model) + self.assertEqual(0, latency_or_jitter_was_found, "Jitter or latency was not found in the test input") + self.assertEqual(1, len(res), "faulty model generated") diff --git a/osm_pla/test/test_mznPlacementConductor.py b/osm_pla/test/test_mznPlacementConductor.py new file mode 100644 index 0000000..6bc42b6 --- /dev/null +++ b/osm_pla/test/test_mznPlacementConductor.py @@ -0,0 +1,218 @@ +# Copyright 2020 ArctosLabs Scandinavia AB +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging +# from collections import Counter +from unittest import TestCase, mock + +# import osm_pla +from osm_pla.placement.mznplacement import MznPlacementConductor, MznModelGenerator + +test_mzn_model = """ +% This minizinc model is generated using +% C:/Users/LG/PycharmProjects/dynamic_jijna2_mzn/osm_pla/placement/mznplacement.py +% at 2019-10-24 11:12:02.058905. + +%This is the NETWORK RESOURCE MODEL +enum Vims = { +vimaaaaaaaa_38f5_438d_b8ee_3f93b3531f87, +vimbbbbbbbb_38f5_438d_b8ee_3f93b3531f87, +vimcccccccc_ed84_4e49_b5df_a9d117bd731f, +vimdddddddd_ed84_4e49_b5df_a9d117bd731f, +vimeeeeeeee_38f5_438d_b8ee_3f93b3531f87}; % The vim-accounts +array[Vims, Vims] of int: trp_link_latency = [|0,50,100,150,200, +|0,0,100,150,200, +|0,0,0,150,200, +|0,0,0,0,200, +|0,0,0,0,0, +|]; % Transport link latency between data centers +array[Vims, Vims] of int: trp_link_jitter = [|0,50,100,150,200, +|0,0,100,150,200, +|0,0,0,150,200, +|0,0,0,0,200, +|0,0,0,0,0, +|]; % Transport link jitter between data centers +array[Vims, Vims] of int: trp_link_price_list = [|0,5,6,6,7, +|0,0,6,6,7, +|0,0,0,6,7, +|0,0,0,0,7, +|0,0,0,0,0, +|]; % Transport link price list +array[Vims] of int: vim_price_list_1 = [500,51,52,53,54]; +array[Vims] of int: vim_price_list_2 = [20,21,22,23,24]; +array[Vims] of int: vim_price_list_3 = [70,71,72,73,74]; +array[Vims] of int: vim_price_list_4 = [40,41,42,43,44]; + + +% This is the NETWORK BASIC LOAD MODEL (CONSUMED) +% NOTE. This is not applicable in OSM Release 7 + +% This is the SERVICE CONSUMPTION MODEL +% These are the variables, i.e. which DC to select for each VNF +var Vims: VNF1; +var Vims: VNF2; +var Vims: VNF3; +var Vims: VNF4; + + +% These are the set of rules for selecting DCs to VNFs +constraint trp_link_latency[VNF1, VNF2] <= 150; +constraint trp_link_latency[VNF2, VNF3] <= 140; +constraint trp_link_latency[VNF3, VNF4] <= 130; +constraint trp_link_jitter[VNF1, VNF2] <= 30; +constraint trp_link_jitter[VNF2, VNF3] <= 30; +constraint trp_link_jitter[VNF3, VNF4] <= 30; + +% Calculate the cost for VNFs and cost for transport link and total cost +var int: used_transport_cost =trp_link_price_list[VNF1, VNF2]+ +trp_link_price_list[VNF2, VNF3]+ +trp_link_price_list[VNF3, VNF4]; + +var int: used_vim_cost =vim_price_list_1[VNF1]+ +vim_price_list_2[VNF2]+ +vim_price_list_3[VNF3]+ +vim_price_list_4[VNF4]; + +var int: total_cost = used_transport_cost + used_vim_cost; + +solve minimize total_cost; + +""" + +test_mzn_model_w_pinning = """ +% This minizinc model is generated using +% C:/Users/LG/PycharmProjects/dynamic_jijna2_mzn/osm_pla/placement/mznplacement.py +% at 2019-10-24 11:12:02.058905. + +%This is the NETWORK RESOURCE MODEL +enum Vims = { +vimaaaaaaaa_38f5_438d_b8ee_3f93b3531f87, +vimbbbbbbbb_38f5_438d_b8ee_3f93b3531f87, +vimcccccccc_ed84_4e49_b5df_a9d117bd731f, +vimdddddddd_ed84_4e49_b5df_a9d117bd731f, +vimeeeeeeee_38f5_438d_b8ee_3f93b3531f87}; % The vim-accounts +array[Vims, Vims] of int: trp_link_latency = [|0,50,100,150,200, +|0,0,100,150,200, +|0,0,0,150,200, +|0,0,0,0,200, +|0,0,0,0,0, +|]; % Transport link latency between data centers +array[Vims, Vims] of int: trp_link_jitter = [|0,50,100,150,200, +|0,0,100,150,200, +|0,0,0,150,200, +|0,0,0,0,200, +|0,0,0,0,0, +|]; % Transport link jitter between data centers +array[Vims, Vims] of int: trp_link_price_list = [|0,5,6,6,7, +|0,0,6,6,7, +|0,0,0,6,7, +|0,0,0,0,7, +|0,0,0,0,0, +|]; % Transport link price list +array[Vims] of int: vim_price_list_1 = [500,51,52,53,54]; +array[Vims] of int: vim_price_list_2 = [20,21,22,23,24]; +array[Vims] of int: vim_price_list_3 = [70,71,72,73,74]; +array[Vims] of int: vim_price_list_4 = [40,41,42,43,44]; + + +% This is the NETWORK BASIC LOAD MODEL (CONSUMED) +% NOTE. This is not applicable in OSM Release 7 + +% This is the SERVICE CONSUMPTION MODEL +% These are the variables, i.e. which DC to select for each VNF +Vims: VNF1 = vimbbbbbbbb_38f5_438d_b8ee_3f93b3531f87; +var Vims: VNF2; +Vims: VNF3 = vimbbbbbbbb_38f5_438d_b8ee_3f93b3531f87; +var Vims: VNF4; + + +% These are the set of rules for selecting DCs to VNFs +constraint trp_link_latency[VNF1, VNF2] <= 150; +constraint trp_link_latency[VNF2, VNF3] <= 140; +constraint trp_link_latency[VNF3, VNF4] <= 130; +constraint trp_link_jitter[VNF1, VNF2] <= 30; +constraint trp_link_jitter[VNF2, VNF3] <= 30; +constraint trp_link_jitter[VNF3, VNF4] <= 30; + +% Calculate the cost for VNFs and cost for transport link and total cost +var int: used_transport_cost =trp_link_price_list[VNF1, VNF2]+ +trp_link_price_list[VNF2, VNF3]+ +trp_link_price_list[VNF3, VNF4]; + +var int: used_vim_cost =vim_price_list_1[VNF1]+ +vim_price_list_2[VNF2]+ +vim_price_list_3[VNF3]+ +vim_price_list_4[VNF4]; + +var int: total_cost = used_transport_cost + used_vim_cost; + +solve minimize total_cost; + +""" + +test_mzn_unsatisfiable_model = """ +var 1..2: item1; +var 1..2: item2; +constraint item1 + item2 == 5; + +solve satisfy; +""" + + +class TestMznPlacementConductor(TestCase): + def test__run_placement_model(self): + expected_result = [{'vimAccountId': 'bbbbbbbb-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '1'}, + {'vimAccountId': 'aaaaaaaa-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '2'}, + {'vimAccountId': 'aaaaaaaa-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '3'}, + {'vimAccountId': 'aaaaaaaa-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '4'}] + + mpc = MznPlacementConductor(logging.getLogger(__name__)) + placement = mpc._run_placement_model(mzn_model=test_mzn_model, ns_desc={}) + # sort the result to ease assert with expected result + sorted_placement = sorted(placement, key=lambda k: k['member-vnf-index']) + self.assertEqual(expected_result, sorted_placement, 'Faulty syntax or content') + + def test__run_placement_model_w_pinning(self): + expected_result = [{'vimAccountId': 'bbbbbbbb-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '1'}, + {'vimAccountId': 'bbbbbbbb-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '2'}, + {'vimAccountId': 'bbbbbbbb-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '3'}, + {'vimAccountId': 'aaaaaaaa-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '4'}] + + ns_desc = [{'vnf_price_per_vim': [10, 9, 7, 8], 'vnf_id': '2'}, + {'vim_account': 'vimbbbbbbbb_38f5_438d_b8ee_3f93b3531f87', 'vnf_price_per_vim': [10, 9, 7, 8], + 'vnf_id': '1'}, + {'vnf_price_per_vim': [10, 9, 7, 8], 'vnf_id': '4'}, + {'vim_account': 'vimbbbbbbbb_38f5_438d_b8ee_3f93b3531f87', 'vnf_price_per_vim': [10, 9, 7, 8], + 'vnf_id': '3'} + ] + + mpc = MznPlacementConductor(logging.getLogger(__name__)) + placement = mpc._run_placement_model(mzn_model=test_mzn_model_w_pinning, ns_desc=ns_desc) + # sort the result to ease assert with expected result + sorted_placement = sorted(placement, key=lambda k: k['member-vnf-index']) + self.assertEqual(expected_result, sorted_placement, 'Faulty syntax or content') + + def test__run_placement_model_unsatisfiable(self): + mpc = MznPlacementConductor(logging.getLogger(__name__)) + self.assertEqual([{}], mpc._run_placement_model(mzn_model=test_mzn_unsatisfiable_model, ns_desc={}), + "Faulty syntax or content for unsatisfiable model") + + @mock.patch.object(MznModelGenerator, 'create_model', side_effect=['%model']) + @mock.patch.object(MznPlacementConductor, '_run_placement_model') + def test_do_placement_computation(self, mock_run, mock_create): + mpc = MznPlacementConductor(logging.getLogger(__name__)) + dummy_nspd = {'ns_desc': {}} + _ = mpc.do_placement_computation(dummy_nspd) + mock_create.assert_called_with(dummy_nspd) + mock_run.assert_called_with('%model', {}) diff --git a/osm_pla/test/test_mznmodels.py b/osm_pla/test/test_mznmodels.py new file mode 100644 index 0000000..b32ec94 --- /dev/null +++ b/osm_pla/test/test_mznmodels.py @@ -0,0 +1,631 @@ + +# Copyright 2019 ArctosLabs Scandinavia AB +# +# 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. + +# Note: +# This unit test file is generated - +# from: c:\Users\LG\Desktop\plarepo\documents\Test\pla_algo_test (pr_update_4).xlsx +# by code generator: mzntestcasegenerator.py +# at: 2019-12-04 09:26:38.412430 +############# + +import datetime +import logging +from unittest import TestCase + +from osm_pla.placement.mznplacement import MznPlacementConductor, MznModelGenerator + + +class TestMznModels(TestCase): + def test_mznmodel_scenario1_subtestcase1(self): + # generate the model + ns_placement_data = {'vim_accounts': ['vim00000000_38f5_438d_b8ee_3f93b3531f87', 'vim11111111_38f5_438d_b8ee_3f93b3531f87', 'vim22222222_38f5_438d_b8ee_3f93b3531f87', 'vim33333333_38f5_438d_b8ee_3f93b3531f87', 'vim44444444_38f5_438d_b8ee_3f93b3531f87', 'vim55555555_38f5_438d_b8ee_3f93b3531f87', 'vim66666666_38f5_438d_b8ee_3f93b3531f87', 'vim77777777_38f5_438d_b8ee_3f93b3531f87', 'vim88888888_38f5_438d_b8ee_3f93b3531f87', 'vim99999999_38f5_438d_b8ee_3f93b3531f87'], + 'trp_link_latency' : [[0, 30, 70, 80, 32767, 32767, 32767, 32767, 32767, 32767], [30, 0, 75, 60, 32767, 32767, 32767, 32767, 32767, 32767], [70, 75, 0, 40, 100, 32767, 32767, 32767, 32767, 32767], [80, 60, 40, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 100, 32767, 0, 5, 5, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 5, 0, 5, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 5, 5, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 30, 20], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 30, 0, 20], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 20, 20, 0]], + 'trp_link_jitter' : [[0, 5, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 0, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 5, 0, 10, 5, 32767, 32767, 32767, 32767, 32767], [10, 10, 10, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 5, 32767, 0, 4, 4, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 4, 0, 10, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 4, 10, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 1, 1], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 1, 0, 1], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 1, 1, 0]], + 'trp_link_price_list' : [[0, 5, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 0, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 5, 0, 10, 10, 32767, 32767, 32767, 32767, 32767], [10, 10, 10, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 10, 32767, 0, 20, 20, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 20, 0, 20, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 20, 20, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 15, 15], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 15, 0, 15], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 15, 15, 0]], + 'ns_desc' : [{'vnf_id': '1', 'vnf_price_per_vim': ['5,10,30,30,100,70,40,100,150,200']}, {'vnf_id': '2', 'vnf_price_per_vim': ['10,15,30,30,100,70,40,150,150,150']}, {'vnf_id': '3', 'vnf_price_per_vim': ['30,30,60,40,100,90,40,150,200,170']}], + 'vld_desc' : [{'cp_refs': ['1', '2'], 'latency': 120, 'jitter': 20}, {'cp_refs': ['2', '3'], 'latency': 120, 'jitter': 20}], + 'generator_data': {'file': __file__ ,'time': datetime.datetime.now()} + } + + mg = MznModelGenerator(logging.getLogger(__name__)) + test_mzn_model = mg.create_model(ns_placement_data) + + # run the model + expected_result = [{'vimAccountId': '00000000-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '1'}, {'vimAccountId': '00000000-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '2'}, {'vimAccountId': '00000000-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '3'}] + + mpc = MznPlacementConductor(logging.getLogger(__name__)) + placement = mpc._run_placement_model(mzn_model=test_mzn_model, ns_desc=[{'vnf_id': '1', 'vnf_price_per_vim': ['5,10,30,30,100,70,40,100,150,200']}, {'vnf_id': '2', 'vnf_price_per_vim': ['10,15,30,30,100,70,40,150,150,150']}, {'vnf_id': '3', 'vnf_price_per_vim': ['30,30,60,40,100,90,40,150,200,170']}]) + # sort the result to ease assert with expected result + if not placement[0]: + sorted_placement = placement + else: + sorted_placement = sorted(placement, key=lambda k: k['member-vnf-index']) + self.assertEqual(expected_result, sorted_placement, 'Faulty syntax or content') + def test_mznmodel_scenario1_subtestcase2(self): + # generate the model + ns_placement_data = {'vim_accounts': ['vim00000000_38f5_438d_b8ee_3f93b3531f87', 'vim11111111_38f5_438d_b8ee_3f93b3531f87', 'vim22222222_38f5_438d_b8ee_3f93b3531f87', 'vim33333333_38f5_438d_b8ee_3f93b3531f87', 'vim44444444_38f5_438d_b8ee_3f93b3531f87', 'vim55555555_38f5_438d_b8ee_3f93b3531f87', 'vim66666666_38f5_438d_b8ee_3f93b3531f87', 'vim77777777_38f5_438d_b8ee_3f93b3531f87', 'vim88888888_38f5_438d_b8ee_3f93b3531f87', 'vim99999999_38f5_438d_b8ee_3f93b3531f87'], + 'trp_link_latency' : [[0, 30, 70, 80, 32767, 32767, 32767, 32767, 32767, 32767], [30, 0, 75, 60, 32767, 32767, 32767, 32767, 32767, 32767], [70, 75, 0, 40, 100, 32767, 32767, 32767, 32767, 32767], [80, 60, 40, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 100, 32767, 0, 5, 5, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 5, 0, 5, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 5, 5, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 30, 20], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 30, 0, 20], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 20, 20, 0]], + 'trp_link_jitter' : [[0, 5, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 0, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 5, 0, 10, 5, 32767, 32767, 32767, 32767, 32767], [10, 10, 10, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 5, 32767, 0, 4, 4, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 4, 0, 10, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 4, 10, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 1, 1], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 1, 0, 1], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 1, 1, 0]], + 'trp_link_price_list' : [[0, 5, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 0, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 5, 0, 10, 10, 32767, 32767, 32767, 32767, 32767], [10, 10, 10, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 10, 32767, 0, 20, 20, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 20, 0, 20, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 20, 20, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 15, 15], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 15, 0, 15], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 15, 15, 0]], + 'ns_desc' : [{'vnf_id': '1', 'vnf_price_per_vim': ['5,10,30,30,100,70,40,100,150,200'], 'vim_account': 'vim33333333_38f5_438d_b8ee_3f93b3531f87'}, {'vnf_id': '2', 'vnf_price_per_vim': ['10,15,30,30,100,70,40,150,150,150']}, {'vnf_id': '3', 'vnf_price_per_vim': ['30,30,60,40,100,90,40,150,200,170']}], + 'vld_desc' : [{'cp_refs': ['1', '2'], 'latency': 120, 'jitter': 20}, {'cp_refs': ['2', '3'], 'latency': 120, 'jitter': 20}], + 'generator_data': {'file': __file__ ,'time': datetime.datetime.now()} + } + + mg = MznModelGenerator(logging.getLogger(__name__)) + test_mzn_model = mg.create_model(ns_placement_data) + + # run the model + expected_result = [{'vimAccountId': '33333333-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '1'}, {'vimAccountId': '00000000-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '2'}, {'vimAccountId': '00000000-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '3'}] + + mpc = MznPlacementConductor(logging.getLogger(__name__)) + placement = mpc._run_placement_model(mzn_model=test_mzn_model, ns_desc=[{'vnf_id': '1', 'vnf_price_per_vim': ['5,10,30,30,100,70,40,100,150,200'], 'vim_account': 'vim33333333_38f5_438d_b8ee_3f93b3531f87'}, {'vnf_id': '2', 'vnf_price_per_vim': ['10,15,30,30,100,70,40,150,150,150']}, {'vnf_id': '3', 'vnf_price_per_vim': ['30,30,60,40,100,90,40,150,200,170']}]) + # sort the result to ease assert with expected result + if not placement[0]: + sorted_placement = placement + else: + sorted_placement = sorted(placement, key=lambda k: k['member-vnf-index']) + self.assertEqual(expected_result, sorted_placement, 'Faulty syntax or content') + def test_mznmodel_scenario1_subtestcase3(self): + # generate the model + ns_placement_data = {'vim_accounts': ['vim00000000_38f5_438d_b8ee_3f93b3531f87', 'vim11111111_38f5_438d_b8ee_3f93b3531f87', 'vim22222222_38f5_438d_b8ee_3f93b3531f87', 'vim33333333_38f5_438d_b8ee_3f93b3531f87', 'vim44444444_38f5_438d_b8ee_3f93b3531f87', 'vim55555555_38f5_438d_b8ee_3f93b3531f87', 'vim66666666_38f5_438d_b8ee_3f93b3531f87', 'vim77777777_38f5_438d_b8ee_3f93b3531f87', 'vim88888888_38f5_438d_b8ee_3f93b3531f87', 'vim99999999_38f5_438d_b8ee_3f93b3531f87'], + 'trp_link_latency' : [[0, 30, 70, 80, 32767, 32767, 32767, 32767, 32767, 32767], [30, 0, 75, 60, 32767, 32767, 32767, 32767, 32767, 32767], [70, 75, 0, 40, 100, 32767, 32767, 32767, 32767, 32767], [80, 60, 40, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 100, 32767, 0, 5, 5, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 5, 0, 5, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 5, 5, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 30, 20], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 30, 0, 20], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 20, 20, 0]], + 'trp_link_jitter' : [[0, 5, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 0, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 5, 0, 10, 5, 32767, 32767, 32767, 32767, 32767], [10, 10, 10, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 5, 32767, 0, 4, 4, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 4, 0, 10, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 4, 10, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 1, 1], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 1, 0, 1], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 1, 1, 0]], + 'trp_link_price_list' : [[0, 5, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 0, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 5, 0, 10, 10, 32767, 32767, 32767, 32767, 32767], [10, 10, 10, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 10, 32767, 0, 20, 20, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 20, 0, 20, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 20, 20, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 15, 15], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 15, 0, 15], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 15, 15, 0]], + 'ns_desc' : [{'vnf_id': '1', 'vnf_price_per_vim': ['5,10,30,30,100,70,40,100,150,200'], 'vim_account': 'vim22222222_38f5_438d_b8ee_3f93b3531f87'}, {'vnf_id': '2', 'vnf_price_per_vim': ['10,15,30,30,100,70,40,150,150,150']}, {'vnf_id': '3', 'vnf_price_per_vim': ['30,30,60,40,100,90,40,150,200,170']}], + 'vld_desc' : [{'cp_refs': ['1', '2'], 'latency': 120, 'jitter': 20}, {'cp_refs': ['2', '3'], 'latency': 120, 'jitter': 20}], + 'generator_data': {'file': __file__ ,'time': datetime.datetime.now()} + } + + mg = MznModelGenerator(logging.getLogger(__name__)) + test_mzn_model = mg.create_model(ns_placement_data) + + # run the model + expected_result = [{'vimAccountId': '22222222-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '1'}, {'vimAccountId': '00000000-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '2'}, {'vimAccountId': '00000000-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '3'}] + + mpc = MznPlacementConductor(logging.getLogger(__name__)) + placement = mpc._run_placement_model(mzn_model=test_mzn_model, ns_desc=[{'vnf_id': '1', 'vnf_price_per_vim': ['5,10,30,30,100,70,40,100,150,200'], 'vim_account': 'vim22222222_38f5_438d_b8ee_3f93b3531f87'}, {'vnf_id': '2', 'vnf_price_per_vim': ['10,15,30,30,100,70,40,150,150,150']}, {'vnf_id': '3', 'vnf_price_per_vim': ['30,30,60,40,100,90,40,150,200,170']}]) + # sort the result to ease assert with expected result + if not placement[0]: + sorted_placement = placement + else: + sorted_placement = sorted(placement, key=lambda k: k['member-vnf-index']) + self.assertEqual(expected_result, sorted_placement, 'Faulty syntax or content') + def test_mznmodel_scenario1_subtestcase4(self): + # generate the model + ns_placement_data = {'vim_accounts': ['vim00000000_38f5_438d_b8ee_3f93b3531f87', 'vim11111111_38f5_438d_b8ee_3f93b3531f87', 'vim22222222_38f5_438d_b8ee_3f93b3531f87', 'vim33333333_38f5_438d_b8ee_3f93b3531f87', 'vim44444444_38f5_438d_b8ee_3f93b3531f87', 'vim55555555_38f5_438d_b8ee_3f93b3531f87', 'vim66666666_38f5_438d_b8ee_3f93b3531f87', 'vim77777777_38f5_438d_b8ee_3f93b3531f87', 'vim88888888_38f5_438d_b8ee_3f93b3531f87', 'vim99999999_38f5_438d_b8ee_3f93b3531f87'], + 'trp_link_latency' : [[0, 30, 70, 80, 32767, 32767, 32767, 32767, 32767, 32767], [30, 0, 75, 60, 32767, 32767, 32767, 32767, 32767, 32767], [70, 75, 0, 40, 100, 32767, 32767, 32767, 32767, 32767], [80, 60, 40, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 100, 32767, 0, 5, 5, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 5, 0, 5, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 5, 5, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 30, 20], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 30, 0, 20], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 20, 20, 0]], + 'trp_link_jitter' : [[0, 5, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 0, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 5, 0, 10, 5, 32767, 32767, 32767, 32767, 32767], [10, 10, 10, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 5, 32767, 0, 4, 4, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 4, 0, 10, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 4, 10, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 1, 1], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 1, 0, 1], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 1, 1, 0]], + 'trp_link_price_list' : [[0, 5, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 0, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 5, 0, 10, 10, 32767, 32767, 32767, 32767, 32767], [10, 10, 10, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 10, 32767, 0, 20, 20, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 20, 0, 20, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 20, 20, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 15, 15], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 15, 0, 15], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 15, 15, 0]], + 'ns_desc' : [{'vnf_id': '1', 'vnf_price_per_vim': ['5,10,30,30,100,70,40,100,150,200'], 'vim_account': 'vim44444444_38f5_438d_b8ee_3f93b3531f87'}, {'vnf_id': '2', 'vnf_price_per_vim': ['10,15,30,30,100,70,40,150,150,150']}, {'vnf_id': '3', 'vnf_price_per_vim': ['30,30,60,40,100,90,40,150,200,170']}], + 'vld_desc' : [{'cp_refs': ['1', '2'], 'latency': 120, 'jitter': 20}, {'cp_refs': ['2', '3'], 'latency': 120, 'jitter': 20}], + 'generator_data': {'file': __file__ ,'time': datetime.datetime.now()} + } + + mg = MznModelGenerator(logging.getLogger(__name__)) + test_mzn_model = mg.create_model(ns_placement_data) + + # run the model + expected_result = [{'vimAccountId': '44444444-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '1'}, {'vimAccountId': '22222222-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '2'}, {'vimAccountId': '00000000-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '3'}] + + mpc = MznPlacementConductor(logging.getLogger(__name__)) + placement = mpc._run_placement_model(mzn_model=test_mzn_model, ns_desc=[{'vnf_id': '1', 'vnf_price_per_vim': ['5,10,30,30,100,70,40,100,150,200'], 'vim_account': 'vim44444444_38f5_438d_b8ee_3f93b3531f87'}, {'vnf_id': '2', 'vnf_price_per_vim': ['10,15,30,30,100,70,40,150,150,150']}, {'vnf_id': '3', 'vnf_price_per_vim': ['30,30,60,40,100,90,40,150,200,170']}]) + # sort the result to ease assert with expected result + if not placement[0]: + sorted_placement = placement + else: + sorted_placement = sorted(placement, key=lambda k: k['member-vnf-index']) + self.assertEqual(expected_result, sorted_placement, 'Faulty syntax or content') + def test_mznmodel_scenario1_subtestcase5(self): + # generate the model + ns_placement_data = {'vim_accounts': ['vim00000000_38f5_438d_b8ee_3f93b3531f87', 'vim11111111_38f5_438d_b8ee_3f93b3531f87', 'vim22222222_38f5_438d_b8ee_3f93b3531f87', 'vim33333333_38f5_438d_b8ee_3f93b3531f87', 'vim44444444_38f5_438d_b8ee_3f93b3531f87', 'vim55555555_38f5_438d_b8ee_3f93b3531f87', 'vim66666666_38f5_438d_b8ee_3f93b3531f87', 'vim77777777_38f5_438d_b8ee_3f93b3531f87', 'vim88888888_38f5_438d_b8ee_3f93b3531f87', 'vim99999999_38f5_438d_b8ee_3f93b3531f87'], + 'trp_link_latency' : [[0, 30, 70, 80, 32767, 32767, 32767, 32767, 32767, 32767], [30, 0, 75, 60, 32767, 32767, 32767, 32767, 32767, 32767], [70, 75, 0, 40, 100, 32767, 32767, 32767, 32767, 32767], [80, 60, 40, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 100, 32767, 0, 5, 5, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 5, 0, 5, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 5, 5, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 30, 20], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 30, 0, 20], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 20, 20, 0]], + 'trp_link_jitter' : [[0, 5, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 0, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 5, 0, 10, 5, 32767, 32767, 32767, 32767, 32767], [10, 10, 10, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 5, 32767, 0, 4, 4, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 4, 0, 10, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 4, 10, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 1, 1], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 1, 0, 1], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 1, 1, 0]], + 'trp_link_price_list' : [[0, 5, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 0, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 5, 0, 10, 10, 32767, 32767, 32767, 32767, 32767], [10, 10, 10, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 10, 32767, 0, 20, 20, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 20, 0, 20, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 20, 20, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 15, 15], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 15, 0, 15], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 15, 15, 0]], + 'ns_desc' : [{'vnf_id': '1', 'vnf_price_per_vim': ['5,10,30,30,100,70,40,100,150,200'], 'vim_account': 'vim88888888_38f5_438d_b8ee_3f93b3531f87'}, {'vnf_id': '2', 'vnf_price_per_vim': ['10,15,30,30,100,70,40,150,150,150']}, {'vnf_id': '3', 'vnf_price_per_vim': ['30,30,60,40,100,90,40,150,200,170']}], + 'vld_desc' : [{'cp_refs': ['1', '2'], 'latency': 120, 'jitter': 20}, {'cp_refs': ['2', '3'], 'latency': 120, 'jitter': 20}], + 'generator_data': {'file': __file__ ,'time': datetime.datetime.now()} + } + + mg = MznModelGenerator(logging.getLogger(__name__)) + test_mzn_model = mg.create_model(ns_placement_data) + + # run the model + expected_result = [{'vimAccountId': '88888888-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '1'}, {'vimAccountId': '77777777-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '2'}, {'vimAccountId': '77777777-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '3'}] + + mpc = MznPlacementConductor(logging.getLogger(__name__)) + placement = mpc._run_placement_model(mzn_model=test_mzn_model, ns_desc=[{'vnf_id': '1', 'vnf_price_per_vim': ['5,10,30,30,100,70,40,100,150,200'], 'vim_account': 'vim88888888_38f5_438d_b8ee_3f93b3531f87'}, {'vnf_id': '2', 'vnf_price_per_vim': ['10,15,30,30,100,70,40,150,150,150']}, {'vnf_id': '3', 'vnf_price_per_vim': ['30,30,60,40,100,90,40,150,200,170']}]) + # sort the result to ease assert with expected result + if not placement[0]: + sorted_placement = placement + else: + sorted_placement = sorted(placement, key=lambda k: k['member-vnf-index']) + self.assertEqual(expected_result, sorted_placement, 'Faulty syntax or content') + def test_mznmodel_scenario1_subtestcase6(self): + # generate the model + ns_placement_data = {'vim_accounts': ['vim00000000_38f5_438d_b8ee_3f93b3531f87', 'vim11111111_38f5_438d_b8ee_3f93b3531f87', 'vim22222222_38f5_438d_b8ee_3f93b3531f87', 'vim33333333_38f5_438d_b8ee_3f93b3531f87', 'vim44444444_38f5_438d_b8ee_3f93b3531f87', 'vim55555555_38f5_438d_b8ee_3f93b3531f87', 'vim66666666_38f5_438d_b8ee_3f93b3531f87', 'vim77777777_38f5_438d_b8ee_3f93b3531f87', 'vim88888888_38f5_438d_b8ee_3f93b3531f87', 'vim99999999_38f5_438d_b8ee_3f93b3531f87'], + 'trp_link_latency' : [[0, 30, 70, 80, 32767, 32767, 32767, 32767, 32767, 32767], [30, 0, 75, 60, 32767, 32767, 32767, 32767, 32767, 32767], [70, 75, 0, 40, 100, 32767, 32767, 32767, 32767, 32767], [80, 60, 40, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 100, 32767, 0, 5, 5, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 5, 0, 5, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 5, 5, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 30, 20], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 30, 0, 20], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 20, 20, 0]], + 'trp_link_jitter' : [[0, 5, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 0, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 5, 0, 10, 5, 32767, 32767, 32767, 32767, 32767], [10, 10, 10, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 5, 32767, 0, 4, 4, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 4, 0, 10, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 4, 10, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 1, 1], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 1, 0, 1], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 1, 1, 0]], + 'trp_link_price_list' : [[0, 5, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 0, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 5, 0, 10, 10, 32767, 32767, 32767, 32767, 32767], [10, 10, 10, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 10, 32767, 0, 20, 20, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 20, 0, 20, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 20, 20, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 15, 15], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 15, 0, 15], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 15, 15, 0]], + 'ns_desc' : [{'vnf_id': '1', 'vnf_price_per_vim': ['5,10,30,30,100,70,40,100,150,200'], 'vim_account': 'vim99999999_38f5_438d_b8ee_3f93b3531f87'}, {'vnf_id': '2', 'vnf_price_per_vim': ['10,15,30,30,100,70,40,150,150,150']}, {'vnf_id': '3', 'vnf_price_per_vim': ['30,30,60,40,100,90,40,150,200,170']}], + 'vld_desc' : [{'cp_refs': ['1', '2'], 'latency': 120, 'jitter': 20}, {'cp_refs': ['2', '3'], 'latency': 120, 'jitter': 20}], + 'generator_data': {'file': __file__ ,'time': datetime.datetime.now()} + } + + mg = MznModelGenerator(logging.getLogger(__name__)) + test_mzn_model = mg.create_model(ns_placement_data) + + # run the model + expected_result = [{'vimAccountId': '99999999-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '1'}, {'vimAccountId': '77777777-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '2'}, {'vimAccountId': '77777777-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '3'}] + + mpc = MznPlacementConductor(logging.getLogger(__name__)) + placement = mpc._run_placement_model(mzn_model=test_mzn_model, ns_desc=[{'vnf_id': '1', 'vnf_price_per_vim': ['5,10,30,30,100,70,40,100,150,200'], 'vim_account': 'vim99999999_38f5_438d_b8ee_3f93b3531f87'}, {'vnf_id': '2', 'vnf_price_per_vim': ['10,15,30,30,100,70,40,150,150,150']}, {'vnf_id': '3', 'vnf_price_per_vim': ['30,30,60,40,100,90,40,150,200,170']}]) + # sort the result to ease assert with expected result + if not placement[0]: + sorted_placement = placement + else: + sorted_placement = sorted(placement, key=lambda k: k['member-vnf-index']) + self.assertEqual(expected_result, sorted_placement, 'Faulty syntax or content') + def test_mznmodel_scenario1_subtestcase7(self): + # generate the model + ns_placement_data = {'vim_accounts': ['vim00000000_38f5_438d_b8ee_3f93b3531f87', 'vim11111111_38f5_438d_b8ee_3f93b3531f87', 'vim22222222_38f5_438d_b8ee_3f93b3531f87', 'vim33333333_38f5_438d_b8ee_3f93b3531f87', 'vim44444444_38f5_438d_b8ee_3f93b3531f87', 'vim55555555_38f5_438d_b8ee_3f93b3531f87', 'vim66666666_38f5_438d_b8ee_3f93b3531f87', 'vim77777777_38f5_438d_b8ee_3f93b3531f87', 'vim88888888_38f5_438d_b8ee_3f93b3531f87', 'vim99999999_38f5_438d_b8ee_3f93b3531f87'], + 'trp_link_latency' : [[0, 30, 70, 80, 32767, 32767, 32767, 32767, 32767, 32767], [30, 0, 75, 60, 32767, 32767, 32767, 32767, 32767, 32767], [70, 75, 0, 40, 100, 32767, 32767, 32767, 32767, 32767], [80, 60, 40, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 100, 32767, 0, 5, 5, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 5, 0, 5, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 5, 5, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 30, 20], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 30, 0, 20], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 20, 20, 0]], + 'trp_link_jitter' : [[0, 5, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 0, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 5, 0, 10, 5, 32767, 32767, 32767, 32767, 32767], [10, 10, 10, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 5, 32767, 0, 4, 4, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 4, 0, 10, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 4, 10, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 1, 1], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 1, 0, 1], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 1, 1, 0]], + 'trp_link_price_list' : [[0, 5, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 0, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 5, 0, 10, 10, 32767, 32767, 32767, 32767, 32767], [10, 10, 10, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 10, 32767, 0, 20, 20, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 20, 0, 20, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 20, 20, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 15, 15], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 15, 0, 15], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 15, 15, 0]], + 'ns_desc' : [{'vnf_id': '1', 'vnf_price_per_vim': ['5,10,30,30,100,70,40,100,150,200'], 'vim_account': 'vim11111111_38f5_438d_b8ee_3f93b3531f87'}, {'vnf_id': '2', 'vnf_price_per_vim': ['10,15,30,30,100,70,40,150,150,150'], 'vim_account': 'vim33333333_38f5_438d_b8ee_3f93b3531f87'}, {'vnf_id': '3', 'vnf_price_per_vim': ['30,30,60,40,100,90,40,150,200,170'], 'vim_account': 'vim77777777_38f5_438d_b8ee_3f93b3531f87'}], + 'vld_desc' : [{'cp_refs': ['1', '2'], 'latency': 120, 'jitter': 20}, {'cp_refs': ['2', '3'], 'latency': 120, 'jitter': 20}], + 'generator_data': {'file': __file__ ,'time': datetime.datetime.now()} + } + + mg = MznModelGenerator(logging.getLogger(__name__)) + test_mzn_model = mg.create_model(ns_placement_data) + + # run the model + expected_result = [{}] + + mpc = MznPlacementConductor(logging.getLogger(__name__)) + placement = mpc._run_placement_model(mzn_model=test_mzn_model, ns_desc=[{'vnf_id': '1', 'vnf_price_per_vim': ['5,10,30,30,100,70,40,100,150,200'], 'vim_account': 'vim11111111_38f5_438d_b8ee_3f93b3531f87'}, {'vnf_id': '2', 'vnf_price_per_vim': ['10,15,30,30,100,70,40,150,150,150'], 'vim_account': 'vim33333333_38f5_438d_b8ee_3f93b3531f87'}, {'vnf_id': '3', 'vnf_price_per_vim': ['30,30,60,40,100,90,40,150,200,170'], 'vim_account': 'vim77777777_38f5_438d_b8ee_3f93b3531f87'}]) + # sort the result to ease assert with expected result + if not placement[0]: + sorted_placement = placement + else: + sorted_placement = sorted(placement, key=lambda k: k['member-vnf-index']) + self.assertEqual(expected_result, sorted_placement, 'Faulty syntax or content') + def test_mznmodel_scenario1_subtestcase8(self): + # generate the model + ns_placement_data = {'vim_accounts': ['vim00000000_38f5_438d_b8ee_3f93b3531f87', 'vim11111111_38f5_438d_b8ee_3f93b3531f87', 'vim22222222_38f5_438d_b8ee_3f93b3531f87', 'vim33333333_38f5_438d_b8ee_3f93b3531f87', 'vim44444444_38f5_438d_b8ee_3f93b3531f87', 'vim55555555_38f5_438d_b8ee_3f93b3531f87', 'vim66666666_38f5_438d_b8ee_3f93b3531f87', 'vim77777777_38f5_438d_b8ee_3f93b3531f87', 'vim88888888_38f5_438d_b8ee_3f93b3531f87', 'vim99999999_38f5_438d_b8ee_3f93b3531f87'], + 'trp_link_latency' : [[0, 30, 70, 80, 32767, 32767, 32767, 32767, 32767, 32767], [30, 0, 75, 60, 32767, 32767, 32767, 32767, 32767, 32767], [70, 75, 0, 40, 100, 32767, 32767, 32767, 32767, 32767], [80, 60, 40, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 100, 32767, 0, 5, 5, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 5, 0, 5, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 5, 5, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 30, 20], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 30, 0, 20], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 20, 20, 0]], + 'trp_link_jitter' : [[0, 5, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 0, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 5, 0, 10, 5, 32767, 32767, 32767, 32767, 32767], [10, 10, 10, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 5, 32767, 0, 4, 4, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 4, 0, 10, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 4, 10, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 1, 1], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 1, 0, 1], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 1, 1, 0]], + 'trp_link_price_list' : [[0, 5, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 0, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 5, 0, 10, 10, 32767, 32767, 32767, 32767, 32767], [10, 10, 10, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 10, 32767, 0, 20, 20, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 20, 0, 20, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 20, 20, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 15, 15], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 15, 0, 15], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 15, 15, 0]], + 'ns_desc' : [{'vnf_id': '1', 'vnf_price_per_vim': ['5,10,30,30,100,70,40,100,150,200'], 'vim_account': 'vim77777777_38f5_438d_b8ee_3f93b3531f87'}, {'vnf_id': '2', 'vnf_price_per_vim': ['10,15,30,30,100,70,40,150,150,150']}, {'vnf_id': '3', 'vnf_price_per_vim': ['30,30,60,40,100,90,40,150,200,170']}], + 'vld_desc' : [{'cp_refs': ['1', '2'], 'latency': 120, 'jitter': 20}, {'cp_refs': ['2', '3'], 'latency': 120, 'jitter': 20}], + 'generator_data': {'file': __file__ ,'time': datetime.datetime.now()} + } + + mg = MznModelGenerator(logging.getLogger(__name__)) + test_mzn_model = mg.create_model(ns_placement_data) + + # run the model + expected_result = [{'vimAccountId': '77777777-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '1'}, {'vimAccountId': '77777777-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '2'}, {'vimAccountId': '77777777-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '3'}] + + mpc = MznPlacementConductor(logging.getLogger(__name__)) + placement = mpc._run_placement_model(mzn_model=test_mzn_model, ns_desc=[{'vnf_id': '1', 'vnf_price_per_vim': ['5,10,30,30,100,70,40,100,150,200'], 'vim_account': 'vim77777777_38f5_438d_b8ee_3f93b3531f87'}, {'vnf_id': '2', 'vnf_price_per_vim': ['10,15,30,30,100,70,40,150,150,150']}, {'vnf_id': '3', 'vnf_price_per_vim': ['30,30,60,40,100,90,40,150,200,170']}]) + # sort the result to ease assert with expected result + if not placement[0]: + sorted_placement = placement + else: + sorted_placement = sorted(placement, key=lambda k: k['member-vnf-index']) + self.assertEqual(expected_result, sorted_placement, 'Faulty syntax or content') + def test_mznmodel_scenario2_subtestcase1(self): + # generate the model + ns_placement_data = {'vim_accounts': ['vim00000000_38f5_438d_b8ee_3f93b3531f87', 'vim11111111_38f5_438d_b8ee_3f93b3531f87', 'vim22222222_38f5_438d_b8ee_3f93b3531f87', 'vim33333333_38f5_438d_b8ee_3f93b3531f87', 'vim44444444_38f5_438d_b8ee_3f93b3531f87', 'vim55555555_38f5_438d_b8ee_3f93b3531f87', 'vim66666666_38f5_438d_b8ee_3f93b3531f87', 'vim77777777_38f5_438d_b8ee_3f93b3531f87', 'vim88888888_38f5_438d_b8ee_3f93b3531f87', 'vim99999999_38f5_438d_b8ee_3f93b3531f87'], + 'trp_link_latency' : [[0, 30, 70, 80, 32767, 32767, 32767, 32767, 32767, 32767], [30, 0, 75, 60, 32767, 32767, 32767, 32767, 32767, 32767], [70, 75, 0, 40, 100, 32767, 32767, 32767, 32767, 32767], [80, 60, 40, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 100, 32767, 0, 5, 5, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 5, 0, 5, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 5, 5, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 30, 20], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 30, 0, 20], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 20, 20, 0]], + 'trp_link_jitter' : [[0, 5, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 0, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 5, 0, 10, 5, 32767, 32767, 32767, 32767, 32767], [10, 10, 10, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 5, 32767, 0, 4, 4, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 4, 0, 10, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 4, 10, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 1, 1], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 1, 0, 1], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 1, 1, 0]], + 'trp_link_price_list' : [[0, 5, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 0, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 5, 0, 10, 10, 32767, 32767, 32767, 32767, 32767], [10, 10, 10, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 10, 32767, 0, 20, 20, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 20, 0, 20, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 20, 20, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 15, 15], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 15, 0, 15], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 15, 15, 0]], + 'ns_desc' : [{'vnf_id': '1', 'vnf_price_per_vim': ['5,10,30,30,100,70,40,100,150,200']}, {'vnf_id': '2', 'vnf_price_per_vim': ['10,15,30,30,100,70,40,150,150,150']}, {'vnf_id': '3', 'vnf_price_per_vim': ['30,30,60,40,100,90,40,150,200,170']}], + 'vld_desc' : [{'cp_refs': ['1', '2'], 'latency': 120, 'jitter': 20}, {'cp_refs': ['2', '3'], 'latency': 20, 'jitter': 6}], + 'generator_data': {'file': __file__ ,'time': datetime.datetime.now()} + } + + mg = MznModelGenerator(logging.getLogger(__name__)) + test_mzn_model = mg.create_model(ns_placement_data) + + # run the model + expected_result = [{'vimAccountId': '00000000-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '1'}, {'vimAccountId': '00000000-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '2'}, {'vimAccountId': '00000000-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '3'}] + + mpc = MznPlacementConductor(logging.getLogger(__name__)) + placement = mpc._run_placement_model(mzn_model=test_mzn_model, ns_desc=[{'vnf_id': '1', 'vnf_price_per_vim': ['5,10,30,30,100,70,40,100,150,200']}, {'vnf_id': '2', 'vnf_price_per_vim': ['10,15,30,30,100,70,40,150,150,150']}, {'vnf_id': '3', 'vnf_price_per_vim': ['30,30,60,40,100,90,40,150,200,170']}]) + # sort the result to ease assert with expected result + if not placement[0]: + sorted_placement = placement + else: + sorted_placement = sorted(placement, key=lambda k: k['member-vnf-index']) + self.assertEqual(expected_result, sorted_placement, 'Faulty syntax or content') + def test_mznmodel_scenario2_subtestcase2(self): + # generate the model + ns_placement_data = {'vim_accounts': ['vim00000000_38f5_438d_b8ee_3f93b3531f87', 'vim11111111_38f5_438d_b8ee_3f93b3531f87', 'vim22222222_38f5_438d_b8ee_3f93b3531f87', 'vim33333333_38f5_438d_b8ee_3f93b3531f87', 'vim44444444_38f5_438d_b8ee_3f93b3531f87', 'vim55555555_38f5_438d_b8ee_3f93b3531f87', 'vim66666666_38f5_438d_b8ee_3f93b3531f87', 'vim77777777_38f5_438d_b8ee_3f93b3531f87', 'vim88888888_38f5_438d_b8ee_3f93b3531f87', 'vim99999999_38f5_438d_b8ee_3f93b3531f87'], + 'trp_link_latency' : [[0, 30, 70, 80, 32767, 32767, 32767, 32767, 32767, 32767], [30, 0, 75, 60, 32767, 32767, 32767, 32767, 32767, 32767], [70, 75, 0, 40, 100, 32767, 32767, 32767, 32767, 32767], [80, 60, 40, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 100, 32767, 0, 5, 5, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 5, 0, 5, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 5, 5, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 30, 20], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 30, 0, 20], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 20, 20, 0]], + 'trp_link_jitter' : [[0, 5, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 0, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 5, 0, 10, 5, 32767, 32767, 32767, 32767, 32767], [10, 10, 10, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 5, 32767, 0, 4, 4, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 4, 0, 10, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 4, 10, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 1, 1], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 1, 0, 1], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 1, 1, 0]], + 'trp_link_price_list' : [[0, 5, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 0, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 5, 0, 10, 10, 32767, 32767, 32767, 32767, 32767], [10, 10, 10, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 10, 32767, 0, 20, 20, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 20, 0, 20, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 20, 20, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 15, 15], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 15, 0, 15], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 15, 15, 0]], + 'ns_desc' : [{'vnf_id': '1', 'vnf_price_per_vim': ['5,10,30,30,100,70,40,100,150,200'], 'vim_account': 'vim44444444_38f5_438d_b8ee_3f93b3531f87'}, {'vnf_id': '2', 'vnf_price_per_vim': ['10,15,30,30,100,70,40,150,150,150']}, {'vnf_id': '3', 'vnf_price_per_vim': ['30,30,60,40,100,90,40,150,200,170']}], + 'vld_desc' : [{'cp_refs': ['1', '2'], 'latency': 120, 'jitter': 20}, {'cp_refs': ['2', '3'], 'latency': 20, 'jitter': 6}], + 'generator_data': {'file': __file__ ,'time': datetime.datetime.now()} + } + + mg = MznModelGenerator(logging.getLogger(__name__)) + test_mzn_model = mg.create_model(ns_placement_data) + + # run the model + expected_result = [{'vimAccountId': '44444444-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '1'}, {'vimAccountId': '22222222-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '2'}, {'vimAccountId': '22222222-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '3'}] + + mpc = MznPlacementConductor(logging.getLogger(__name__)) + placement = mpc._run_placement_model(mzn_model=test_mzn_model, ns_desc=[{'vnf_id': '1', 'vnf_price_per_vim': ['5,10,30,30,100,70,40,100,150,200'], 'vim_account': 'vim44444444_38f5_438d_b8ee_3f93b3531f87'}, {'vnf_id': '2', 'vnf_price_per_vim': ['10,15,30,30,100,70,40,150,150,150']}, {'vnf_id': '3', 'vnf_price_per_vim': ['30,30,60,40,100,90,40,150,200,170']}]) + # sort the result to ease assert with expected result + if not placement[0]: + sorted_placement = placement + else: + sorted_placement = sorted(placement, key=lambda k: k['member-vnf-index']) + self.assertEqual(expected_result, sorted_placement, 'Faulty syntax or content') + def test_mznmodel_scenario2_subtestcase3(self): + # generate the model + ns_placement_data = {'vim_accounts': ['vim00000000_38f5_438d_b8ee_3f93b3531f87', 'vim11111111_38f5_438d_b8ee_3f93b3531f87', 'vim22222222_38f5_438d_b8ee_3f93b3531f87', 'vim33333333_38f5_438d_b8ee_3f93b3531f87', 'vim44444444_38f5_438d_b8ee_3f93b3531f87', 'vim55555555_38f5_438d_b8ee_3f93b3531f87', 'vim66666666_38f5_438d_b8ee_3f93b3531f87', 'vim77777777_38f5_438d_b8ee_3f93b3531f87', 'vim88888888_38f5_438d_b8ee_3f93b3531f87', 'vim99999999_38f5_438d_b8ee_3f93b3531f87'], + 'trp_link_latency' : [[0, 30, 70, 80, 32767, 32767, 32767, 32767, 32767, 32767], [30, 0, 75, 60, 32767, 32767, 32767, 32767, 32767, 32767], [70, 75, 0, 40, 100, 32767, 32767, 32767, 32767, 32767], [80, 60, 40, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 100, 32767, 0, 5, 5, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 5, 0, 5, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 5, 5, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 30, 20], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 30, 0, 20], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 20, 20, 0]], + 'trp_link_jitter' : [[0, 5, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 0, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 5, 0, 10, 5, 32767, 32767, 32767, 32767, 32767], [10, 10, 10, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 5, 32767, 0, 4, 4, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 4, 0, 10, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 4, 10, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 1, 1], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 1, 0, 1], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 1, 1, 0]], + 'trp_link_price_list' : [[0, 5, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 0, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 5, 0, 10, 10, 32767, 32767, 32767, 32767, 32767], [10, 10, 10, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 10, 32767, 0, 20, 20, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 20, 0, 20, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 20, 20, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 15, 15], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 15, 0, 15], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 15, 15, 0]], + 'ns_desc' : [{'vnf_id': '1', 'vnf_price_per_vim': ['5,10,30,30,100,70,40,100,150,200'], 'vim_account': 'vim55555555_38f5_438d_b8ee_3f93b3531f87'}, {'vnf_id': '2', 'vnf_price_per_vim': ['10,15,30,30,100,70,40,150,150,150']}, {'vnf_id': '3', 'vnf_price_per_vim': ['30,30,60,40,100,90,40,150,200,170']}], + 'vld_desc' : [{'cp_refs': ['1', '2'], 'latency': 120, 'jitter': 20}, {'cp_refs': ['2', '3'], 'latency': 20, 'jitter': 6}], + 'generator_data': {'file': __file__ ,'time': datetime.datetime.now()} + } + + mg = MznModelGenerator(logging.getLogger(__name__)) + test_mzn_model = mg.create_model(ns_placement_data) + + # run the model + expected_result = [{'vimAccountId': '55555555-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '1'}, {'vimAccountId': '66666666-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '2'}, {'vimAccountId': '66666666-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '3'}] + + mpc = MznPlacementConductor(logging.getLogger(__name__)) + placement = mpc._run_placement_model(mzn_model=test_mzn_model, ns_desc=[{'vnf_id': '1', 'vnf_price_per_vim': ['5,10,30,30,100,70,40,100,150,200'], 'vim_account': 'vim55555555_38f5_438d_b8ee_3f93b3531f87'}, {'vnf_id': '2', 'vnf_price_per_vim': ['10,15,30,30,100,70,40,150,150,150']}, {'vnf_id': '3', 'vnf_price_per_vim': ['30,30,60,40,100,90,40,150,200,170']}]) + # sort the result to ease assert with expected result + if not placement[0]: + sorted_placement = placement + else: + sorted_placement = sorted(placement, key=lambda k: k['member-vnf-index']) + self.assertEqual(expected_result, sorted_placement, 'Faulty syntax or content') + def test_mznmodel_scenario2_subtestcase4(self): + # generate the model + ns_placement_data = {'vim_accounts': ['vim00000000_38f5_438d_b8ee_3f93b3531f87', 'vim11111111_38f5_438d_b8ee_3f93b3531f87', 'vim22222222_38f5_438d_b8ee_3f93b3531f87', 'vim33333333_38f5_438d_b8ee_3f93b3531f87', 'vim44444444_38f5_438d_b8ee_3f93b3531f87', 'vim55555555_38f5_438d_b8ee_3f93b3531f87', 'vim66666666_38f5_438d_b8ee_3f93b3531f87', 'vim77777777_38f5_438d_b8ee_3f93b3531f87', 'vim88888888_38f5_438d_b8ee_3f93b3531f87', 'vim99999999_38f5_438d_b8ee_3f93b3531f87'], + 'trp_link_latency' : [[0, 30, 70, 80, 32767, 32767, 32767, 32767, 32767, 32767], [30, 0, 75, 60, 32767, 32767, 32767, 32767, 32767, 32767], [70, 75, 0, 40, 100, 32767, 32767, 32767, 32767, 32767], [80, 60, 40, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 100, 32767, 0, 5, 5, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 5, 0, 5, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 5, 5, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 30, 20], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 30, 0, 20], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 20, 20, 0]], + 'trp_link_jitter' : [[0, 5, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 0, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 5, 0, 10, 5, 32767, 32767, 32767, 32767, 32767], [10, 10, 10, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 5, 32767, 0, 4, 4, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 4, 0, 10, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 4, 10, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 1, 1], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 1, 0, 1], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 1, 1, 0]], + 'trp_link_price_list' : [[0, 5, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 0, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 5, 0, 10, 10, 32767, 32767, 32767, 32767, 32767], [10, 10, 10, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 10, 32767, 0, 20, 20, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 20, 0, 20, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 20, 20, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 15, 15], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 15, 0, 15], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 15, 15, 0]], + 'ns_desc' : [{'vnf_id': '1', 'vnf_price_per_vim': ['5,10,30,30,100,70,40,100,150,200'], 'vim_account': 'vim55555555_38f5_438d_b8ee_3f93b3531f87'}, {'vnf_id': '2', 'vnf_price_per_vim': ['10,15,30,30,100,70,40,150,150,150'], 'vim_account': 'vim55555555_38f5_438d_b8ee_3f93b3531f87'}, {'vnf_id': '3', 'vnf_price_per_vim': ['30,30,60,40,100,90,40,150,200,170']}], + 'vld_desc' : [{'cp_refs': ['1', '2'], 'latency': 120, 'jitter': 20}, {'cp_refs': ['2', '3'], 'latency': 20, 'jitter': 6}], + 'generator_data': {'file': __file__ ,'time': datetime.datetime.now()} + } + + mg = MznModelGenerator(logging.getLogger(__name__)) + test_mzn_model = mg.create_model(ns_placement_data) + + # run the model + expected_result = [{'vimAccountId': '55555555-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '1'}, {'vimAccountId': '55555555-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '2'}, {'vimAccountId': '55555555-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '3'}] + + mpc = MznPlacementConductor(logging.getLogger(__name__)) + placement = mpc._run_placement_model(mzn_model=test_mzn_model, ns_desc=[{'vnf_id': '1', 'vnf_price_per_vim': ['5,10,30,30,100,70,40,100,150,200'], 'vim_account': 'vim55555555_38f5_438d_b8ee_3f93b3531f87'}, {'vnf_id': '2', 'vnf_price_per_vim': ['10,15,30,30,100,70,40,150,150,150'], 'vim_account': 'vim55555555_38f5_438d_b8ee_3f93b3531f87'}, {'vnf_id': '3', 'vnf_price_per_vim': ['30,30,60,40,100,90,40,150,200,170']}]) + # sort the result to ease assert with expected result + if not placement[0]: + sorted_placement = placement + else: + sorted_placement = sorted(placement, key=lambda k: k['member-vnf-index']) + self.assertEqual(expected_result, sorted_placement, 'Faulty syntax or content') + def test_mznmodel_scenario2_subtestcase5(self): + # generate the model + ns_placement_data = {'vim_accounts': ['vim00000000_38f5_438d_b8ee_3f93b3531f87', 'vim11111111_38f5_438d_b8ee_3f93b3531f87', 'vim22222222_38f5_438d_b8ee_3f93b3531f87', 'vim33333333_38f5_438d_b8ee_3f93b3531f87', 'vim44444444_38f5_438d_b8ee_3f93b3531f87', 'vim55555555_38f5_438d_b8ee_3f93b3531f87', 'vim66666666_38f5_438d_b8ee_3f93b3531f87', 'vim77777777_38f5_438d_b8ee_3f93b3531f87', 'vim88888888_38f5_438d_b8ee_3f93b3531f87', 'vim99999999_38f5_438d_b8ee_3f93b3531f87'], + 'trp_link_latency' : [[0, 30, 70, 80, 32767, 32767, 32767, 32767, 32767, 32767], [30, 0, 75, 60, 32767, 32767, 32767, 32767, 32767, 32767], [70, 75, 0, 40, 100, 32767, 32767, 32767, 32767, 32767], [80, 60, 40, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 100, 32767, 0, 5, 5, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 5, 0, 5, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 5, 5, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 30, 20], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 30, 0, 20], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 20, 20, 0]], + 'trp_link_jitter' : [[0, 5, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 0, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 5, 0, 10, 5, 32767, 32767, 32767, 32767, 32767], [10, 10, 10, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 5, 32767, 0, 4, 4, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 4, 0, 10, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 4, 10, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 1, 1], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 1, 0, 1], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 1, 1, 0]], + 'trp_link_price_list' : [[0, 5, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 0, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 5, 0, 10, 10, 32767, 32767, 32767, 32767, 32767], [10, 10, 10, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 10, 32767, 0, 20, 20, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 20, 0, 20, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 20, 20, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 15, 15], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 15, 0, 15], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 15, 15, 0]], + 'ns_desc' : [{'vnf_id': '1', 'vnf_price_per_vim': ['5,10,30,30,100,70,40,100,150,200'], 'vim_account': 'vim33333333_38f5_438d_b8ee_3f93b3531f87'}, {'vnf_id': '2', 'vnf_price_per_vim': ['10,15,30,30,100,70,40,150,150,150'], 'vim_account': 'vim44444444_38f5_438d_b8ee_3f93b3531f87'}, {'vnf_id': '3', 'vnf_price_per_vim': ['30,30,60,40,100,90,40,150,200,170']}], + 'vld_desc' : [{'cp_refs': ['1', '2'], 'latency': 120, 'jitter': 20}, {'cp_refs': ['2', '3'], 'latency': 20, 'jitter': 6}], + 'generator_data': {'file': __file__ ,'time': datetime.datetime.now()} + } + + mg = MznModelGenerator(logging.getLogger(__name__)) + test_mzn_model = mg.create_model(ns_placement_data) + + # run the model + expected_result = [{}] + + mpc = MznPlacementConductor(logging.getLogger(__name__)) + placement = mpc._run_placement_model(mzn_model=test_mzn_model, ns_desc=[{'vnf_id': '1', 'vnf_price_per_vim': ['5,10,30,30,100,70,40,100,150,200'], 'vim_account': 'vim33333333_38f5_438d_b8ee_3f93b3531f87'}, {'vnf_id': '2', 'vnf_price_per_vim': ['10,15,30,30,100,70,40,150,150,150'], 'vim_account': 'vim44444444_38f5_438d_b8ee_3f93b3531f87'}, {'vnf_id': '3', 'vnf_price_per_vim': ['30,30,60,40,100,90,40,150,200,170']}]) + # sort the result to ease assert with expected result + if not placement[0]: + sorted_placement = placement + else: + sorted_placement = sorted(placement, key=lambda k: k['member-vnf-index']) + self.assertEqual(expected_result, sorted_placement, 'Faulty syntax or content') + def test_mznmodel_scenario2_subtestcase6(self): + # generate the model + ns_placement_data = {'vim_accounts': ['vim00000000_38f5_438d_b8ee_3f93b3531f87', 'vim11111111_38f5_438d_b8ee_3f93b3531f87', 'vim22222222_38f5_438d_b8ee_3f93b3531f87', 'vim33333333_38f5_438d_b8ee_3f93b3531f87', 'vim44444444_38f5_438d_b8ee_3f93b3531f87', 'vim55555555_38f5_438d_b8ee_3f93b3531f87', 'vim66666666_38f5_438d_b8ee_3f93b3531f87', 'vim77777777_38f5_438d_b8ee_3f93b3531f87', 'vim88888888_38f5_438d_b8ee_3f93b3531f87', 'vim99999999_38f5_438d_b8ee_3f93b3531f87'], + 'trp_link_latency' : [[0, 30, 70, 80, 32767, 32767, 32767, 32767, 32767, 32767], [30, 0, 75, 60, 32767, 32767, 32767, 32767, 32767, 32767], [70, 75, 0, 40, 100, 32767, 32767, 32767, 32767, 32767], [80, 60, 40, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 100, 32767, 0, 5, 5, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 5, 0, 5, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 5, 5, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 30, 20], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 30, 0, 20], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 20, 20, 0]], + 'trp_link_jitter' : [[0, 5, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 0, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 5, 0, 10, 5, 32767, 32767, 32767, 32767, 32767], [10, 10, 10, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 5, 32767, 0, 4, 4, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 4, 0, 10, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 4, 10, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 1, 1], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 1, 0, 1], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 1, 1, 0]], + 'trp_link_price_list' : [[0, 5, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 0, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 5, 0, 10, 10, 32767, 32767, 32767, 32767, 32767], [10, 10, 10, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 10, 32767, 0, 20, 20, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 20, 0, 20, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 20, 20, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 15, 15], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 15, 0, 15], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 15, 15, 0]], + 'ns_desc' : [{'vnf_id': '1', 'vnf_price_per_vim': ['5,10,30,30,100,70,40,100,150,200'], 'vim_account': 'vim55555555_38f5_438d_b8ee_3f93b3531f87'}, {'vnf_id': '2', 'vnf_price_per_vim': ['10,15,30,30,100,70,40,150,150,150'], 'vim_account': 'vim55555555_38f5_438d_b8ee_3f93b3531f87'}, {'vnf_id': '3', 'vnf_price_per_vim': ['30,30,60,40,100,90,40,150,200,170'], 'vim_account': 'vim66666666_38f5_438d_b8ee_3f93b3531f87'}], + 'vld_desc' : [{'cp_refs': ['1', '2'], 'latency': 120, 'jitter': 20}, {'cp_refs': ['2', '3'], 'latency': 20, 'jitter': 6}], + 'generator_data': {'file': __file__ ,'time': datetime.datetime.now()} + } + + mg = MznModelGenerator(logging.getLogger(__name__)) + test_mzn_model = mg.create_model(ns_placement_data) + + # run the model + expected_result = [{}] + + mpc = MznPlacementConductor(logging.getLogger(__name__)) + placement = mpc._run_placement_model(mzn_model=test_mzn_model, ns_desc=[{'vnf_id': '1', 'vnf_price_per_vim': ['5,10,30,30,100,70,40,100,150,200'], 'vim_account': 'vim55555555_38f5_438d_b8ee_3f93b3531f87'}, {'vnf_id': '2', 'vnf_price_per_vim': ['10,15,30,30,100,70,40,150,150,150'], 'vim_account': 'vim55555555_38f5_438d_b8ee_3f93b3531f87'}, {'vnf_id': '3', 'vnf_price_per_vim': ['30,30,60,40,100,90,40,150,200,170'], 'vim_account': 'vim66666666_38f5_438d_b8ee_3f93b3531f87'}]) + # sort the result to ease assert with expected result + if not placement[0]: + sorted_placement = placement + else: + sorted_placement = sorted(placement, key=lambda k: k['member-vnf-index']) + self.assertEqual(expected_result, sorted_placement, 'Faulty syntax or content') + def test_mznmodel_scenario3_subtestcase1(self): + # generate the model + ns_placement_data = {'vim_accounts': ['vim00000000_38f5_438d_b8ee_3f93b3531f87', 'vim11111111_38f5_438d_b8ee_3f93b3531f87', 'vim22222222_38f5_438d_b8ee_3f93b3531f87', 'vim33333333_38f5_438d_b8ee_3f93b3531f87', 'vim44444444_38f5_438d_b8ee_3f93b3531f87', 'vim55555555_38f5_438d_b8ee_3f93b3531f87', 'vim66666666_38f5_438d_b8ee_3f93b3531f87', 'vim77777777_38f5_438d_b8ee_3f93b3531f87', 'vim88888888_38f5_438d_b8ee_3f93b3531f87', 'vim99999999_38f5_438d_b8ee_3f93b3531f87'], + 'trp_link_latency' : [[0, 30, 70, 80, 32767, 32767, 32767, 32767, 32767, 32767], [30, 0, 75, 60, 32767, 32767, 32767, 32767, 32767, 32767], [70, 75, 0, 40, 100, 32767, 32767, 32767, 32767, 32767], [80, 60, 40, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 100, 32767, 0, 5, 5, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 5, 0, 5, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 5, 5, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 30, 20], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 30, 0, 20], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 20, 20, 0]], + 'trp_link_jitter' : [[0, 5, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 0, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 5, 0, 10, 5, 32767, 32767, 32767, 32767, 32767], [10, 10, 10, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 5, 32767, 0, 4, 4, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 4, 0, 10, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 4, 10, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 1, 1], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 1, 0, 1], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 1, 1, 0]], + 'trp_link_price_list' : [[0, 5, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 0, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 5, 0, 10, 10, 32767, 32767, 32767, 32767, 32767], [10, 10, 10, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 10, 32767, 0, 20, 20, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 20, 0, 20, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 20, 20, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 15, 15], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 15, 0, 15], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 15, 15, 0]], + 'ns_desc' : [{'vnf_id': '1', 'vnf_price_per_vim': ['5,10,30,30,100,70,40,100,150,200']}, {'vnf_id': '2', 'vnf_price_per_vim': ['10,15,30,30,100,70,40,150,150,150']}, {'vnf_id': '3', 'vnf_price_per_vim': ['30,30,60,40,100,90,40,150,200,170']}], + 'vld_desc' : [{'cp_refs': ['1', '2'], 'latency': 120, 'jitter': 3}, {'cp_refs': ['2', '3'], 'latency': 25, 'jitter': 6}], + 'generator_data': {'file': __file__ ,'time': datetime.datetime.now()} + } + + mg = MznModelGenerator(logging.getLogger(__name__)) + test_mzn_model = mg.create_model(ns_placement_data) + + # run the model + expected_result = [{'vimAccountId': '00000000-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '1'}, {'vimAccountId': '00000000-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '2'}, {'vimAccountId': '00000000-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '3'}] + + mpc = MznPlacementConductor(logging.getLogger(__name__)) + placement = mpc._run_placement_model(mzn_model=test_mzn_model, ns_desc=[{'vnf_id': '1', 'vnf_price_per_vim': ['5,10,30,30,100,70,40,100,150,200']}, {'vnf_id': '2', 'vnf_price_per_vim': ['10,15,30,30,100,70,40,150,150,150']}, {'vnf_id': '3', 'vnf_price_per_vim': ['30,30,60,40,100,90,40,150,200,170']}]) + # sort the result to ease assert with expected result + if not placement[0]: + sorted_placement = placement + else: + sorted_placement = sorted(placement, key=lambda k: k['member-vnf-index']) + self.assertEqual(expected_result, sorted_placement, 'Faulty syntax or content') + def test_mznmodel_scenario3_subtestcase2(self): + # generate the model + ns_placement_data = {'vim_accounts': ['vim00000000_38f5_438d_b8ee_3f93b3531f87', 'vim11111111_38f5_438d_b8ee_3f93b3531f87', 'vim22222222_38f5_438d_b8ee_3f93b3531f87', 'vim33333333_38f5_438d_b8ee_3f93b3531f87', 'vim44444444_38f5_438d_b8ee_3f93b3531f87', 'vim55555555_38f5_438d_b8ee_3f93b3531f87', 'vim66666666_38f5_438d_b8ee_3f93b3531f87', 'vim77777777_38f5_438d_b8ee_3f93b3531f87', 'vim88888888_38f5_438d_b8ee_3f93b3531f87', 'vim99999999_38f5_438d_b8ee_3f93b3531f87'], + 'trp_link_latency' : [[0, 30, 70, 80, 32767, 32767, 32767, 32767, 32767, 32767], [30, 0, 75, 60, 32767, 32767, 32767, 32767, 32767, 32767], [70, 75, 0, 40, 100, 32767, 32767, 32767, 32767, 32767], [80, 60, 40, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 100, 32767, 0, 5, 5, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 5, 0, 5, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 5, 5, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 30, 20], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 30, 0, 20], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 20, 20, 0]], + 'trp_link_jitter' : [[0, 5, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 0, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 5, 0, 10, 5, 32767, 32767, 32767, 32767, 32767], [10, 10, 10, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 5, 32767, 0, 4, 4, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 4, 0, 10, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 4, 10, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 1, 1], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 1, 0, 1], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 1, 1, 0]], + 'trp_link_price_list' : [[0, 5, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 0, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 5, 0, 10, 10, 32767, 32767, 32767, 32767, 32767], [10, 10, 10, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 10, 32767, 0, 20, 20, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 20, 0, 20, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 20, 20, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 15, 15], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 15, 0, 15], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 15, 15, 0]], + 'ns_desc' : [{'vnf_id': '1', 'vnf_price_per_vim': ['5,10,30,30,100,70,40,100,150,200'], 'vim_account': 'vim44444444_38f5_438d_b8ee_3f93b3531f87'}, {'vnf_id': '2', 'vnf_price_per_vim': ['10,15,30,30,100,70,40,150,150,150'], 'vim_account': 'vim22222222_38f5_438d_b8ee_3f93b3531f87'}, {'vnf_id': '3', 'vnf_price_per_vim': ['30,30,60,40,100,90,40,150,200,170']}], + 'vld_desc' : [{'cp_refs': ['1', '2'], 'latency': 120, 'jitter': 3}, {'cp_refs': ['2', '3'], 'latency': 25, 'jitter': 6}], + 'generator_data': {'file': __file__ ,'time': datetime.datetime.now()} + } + + mg = MznModelGenerator(logging.getLogger(__name__)) + test_mzn_model = mg.create_model(ns_placement_data) + + # run the model + expected_result = [{}] + + mpc = MznPlacementConductor(logging.getLogger(__name__)) + placement = mpc._run_placement_model(mzn_model=test_mzn_model, ns_desc=[{'vnf_id': '1', 'vnf_price_per_vim': ['5,10,30,30,100,70,40,100,150,200'], 'vim_account': 'vim44444444_38f5_438d_b8ee_3f93b3531f87'}, {'vnf_id': '2', 'vnf_price_per_vim': ['10,15,30,30,100,70,40,150,150,150'], 'vim_account': 'vim22222222_38f5_438d_b8ee_3f93b3531f87'}, {'vnf_id': '3', 'vnf_price_per_vim': ['30,30,60,40,100,90,40,150,200,170']}]) + # sort the result to ease assert with expected result + if not placement[0]: + sorted_placement = placement + else: + sorted_placement = sorted(placement, key=lambda k: k['member-vnf-index']) + self.assertEqual(expected_result, sorted_placement, 'Faulty syntax or content') + def test_mznmodel_scenario3_subtestcase3(self): + # generate the model + ns_placement_data = {'vim_accounts': ['vim00000000_38f5_438d_b8ee_3f93b3531f87', 'vim11111111_38f5_438d_b8ee_3f93b3531f87', 'vim22222222_38f5_438d_b8ee_3f93b3531f87', 'vim33333333_38f5_438d_b8ee_3f93b3531f87', 'vim44444444_38f5_438d_b8ee_3f93b3531f87', 'vim55555555_38f5_438d_b8ee_3f93b3531f87', 'vim66666666_38f5_438d_b8ee_3f93b3531f87', 'vim77777777_38f5_438d_b8ee_3f93b3531f87', 'vim88888888_38f5_438d_b8ee_3f93b3531f87', 'vim99999999_38f5_438d_b8ee_3f93b3531f87'], + 'trp_link_latency' : [[0, 30, 70, 80, 32767, 32767, 32767, 32767, 32767, 32767], [30, 0, 75, 60, 32767, 32767, 32767, 32767, 32767, 32767], [70, 75, 0, 40, 100, 32767, 32767, 32767, 32767, 32767], [80, 60, 40, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 100, 32767, 0, 5, 5, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 5, 0, 5, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 5, 5, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 30, 20], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 30, 0, 20], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 20, 20, 0]], + 'trp_link_jitter' : [[0, 5, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 0, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 5, 0, 10, 5, 32767, 32767, 32767, 32767, 32767], [10, 10, 10, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 5, 32767, 0, 4, 4, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 4, 0, 10, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 4, 10, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 1, 1], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 1, 0, 1], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 1, 1, 0]], + 'trp_link_price_list' : [[0, 5, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 0, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 5, 0, 10, 10, 32767, 32767, 32767, 32767, 32767], [10, 10, 10, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 10, 32767, 0, 20, 20, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 20, 0, 20, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 20, 20, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 15, 15], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 15, 0, 15], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 15, 15, 0]], + 'ns_desc' : [{'vnf_id': '1', 'vnf_price_per_vim': ['5,10,30,30,100,70,40,100,150,200'], 'vim_account': 'vim77777777_38f5_438d_b8ee_3f93b3531f87'}, {'vnf_id': '2', 'vnf_price_per_vim': ['10,15,30,30,100,70,40,150,150,150']}, {'vnf_id': '3', 'vnf_price_per_vim': ['30,30,60,40,100,90,40,150,200,170']}], + 'vld_desc' : [{'cp_refs': ['1', '2'], 'latency': 120, 'jitter': 3}, {'cp_refs': ['2', '3'], 'latency': 25, 'jitter': 6}], + 'generator_data': {'file': __file__ ,'time': datetime.datetime.now()} + } + + mg = MznModelGenerator(logging.getLogger(__name__)) + test_mzn_model = mg.create_model(ns_placement_data) + + # run the model + expected_result = [{'vimAccountId': '77777777-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '1'}, {'vimAccountId': '77777777-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '2'}, {'vimAccountId': '77777777-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '3'}] + + mpc = MznPlacementConductor(logging.getLogger(__name__)) + placement = mpc._run_placement_model(mzn_model=test_mzn_model, ns_desc=[{'vnf_id': '1', 'vnf_price_per_vim': ['5,10,30,30,100,70,40,100,150,200'], 'vim_account': 'vim77777777_38f5_438d_b8ee_3f93b3531f87'}, {'vnf_id': '2', 'vnf_price_per_vim': ['10,15,30,30,100,70,40,150,150,150']}, {'vnf_id': '3', 'vnf_price_per_vim': ['30,30,60,40,100,90,40,150,200,170']}]) + # sort the result to ease assert with expected result + if not placement[0]: + sorted_placement = placement + else: + sorted_placement = sorted(placement, key=lambda k: k['member-vnf-index']) + self.assertEqual(expected_result, sorted_placement, 'Faulty syntax or content') + def test_mznmodel_scenario3_subtestcase4(self): + # generate the model + ns_placement_data = {'vim_accounts': ['vim00000000_38f5_438d_b8ee_3f93b3531f87', 'vim11111111_38f5_438d_b8ee_3f93b3531f87', 'vim22222222_38f5_438d_b8ee_3f93b3531f87', 'vim33333333_38f5_438d_b8ee_3f93b3531f87', 'vim44444444_38f5_438d_b8ee_3f93b3531f87', 'vim55555555_38f5_438d_b8ee_3f93b3531f87', 'vim66666666_38f5_438d_b8ee_3f93b3531f87', 'vim77777777_38f5_438d_b8ee_3f93b3531f87', 'vim88888888_38f5_438d_b8ee_3f93b3531f87', 'vim99999999_38f5_438d_b8ee_3f93b3531f87'], + 'trp_link_latency' : [[0, 30, 70, 80, 32767, 32767, 32767, 32767, 32767, 32767], [30, 0, 75, 60, 32767, 32767, 32767, 32767, 32767, 32767], [70, 75, 0, 40, 100, 32767, 32767, 32767, 32767, 32767], [80, 60, 40, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 100, 32767, 0, 5, 5, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 5, 0, 5, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 5, 5, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 30, 20], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 30, 0, 20], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 20, 20, 0]], + 'trp_link_jitter' : [[0, 5, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 0, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 5, 0, 10, 5, 32767, 32767, 32767, 32767, 32767], [10, 10, 10, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 5, 32767, 0, 4, 4, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 4, 0, 10, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 4, 10, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 1, 1], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 1, 0, 1], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 1, 1, 0]], + 'trp_link_price_list' : [[0, 5, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 0, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 5, 0, 10, 10, 32767, 32767, 32767, 32767, 32767], [10, 10, 10, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 10, 32767, 0, 20, 20, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 20, 0, 20, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 20, 20, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 15, 15], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 15, 0, 15], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 15, 15, 0]], + 'ns_desc' : [{'vnf_id': '1', 'vnf_price_per_vim': ['5,10,30,30,100,70,40,100,150,200'], 'vim_account': 'vim88888888_38f5_438d_b8ee_3f93b3531f87'}, {'vnf_id': '2', 'vnf_price_per_vim': ['10,15,30,30,100,70,40,150,150,150']}, {'vnf_id': '3', 'vnf_price_per_vim': ['30,30,60,40,100,90,40,150,200,170']}], + 'vld_desc' : [{'cp_refs': ['1', '2'], 'latency': 120, 'jitter': 3}, {'cp_refs': ['2', '3'], 'latency': 25, 'jitter': 6}], + 'generator_data': {'file': __file__ ,'time': datetime.datetime.now()} + } + + mg = MznModelGenerator(logging.getLogger(__name__)) + test_mzn_model = mg.create_model(ns_placement_data) + + # run the model + expected_result = [{'vimAccountId': '88888888-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '1'}, {'vimAccountId': '77777777-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '2'}, {'vimAccountId': '77777777-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '3'}] + + mpc = MznPlacementConductor(logging.getLogger(__name__)) + placement = mpc._run_placement_model(mzn_model=test_mzn_model, ns_desc=[{'vnf_id': '1', 'vnf_price_per_vim': ['5,10,30,30,100,70,40,100,150,200'], 'vim_account': 'vim88888888_38f5_438d_b8ee_3f93b3531f87'}, {'vnf_id': '2', 'vnf_price_per_vim': ['10,15,30,30,100,70,40,150,150,150']}, {'vnf_id': '3', 'vnf_price_per_vim': ['30,30,60,40,100,90,40,150,200,170']}]) + # sort the result to ease assert with expected result + if not placement[0]: + sorted_placement = placement + else: + sorted_placement = sorted(placement, key=lambda k: k['member-vnf-index']) + self.assertEqual(expected_result, sorted_placement, 'Faulty syntax or content') + def test_mznmodel_scenario3_subtestcase5(self): + # generate the model + ns_placement_data = {'vim_accounts': ['vim00000000_38f5_438d_b8ee_3f93b3531f87', 'vim11111111_38f5_438d_b8ee_3f93b3531f87', 'vim22222222_38f5_438d_b8ee_3f93b3531f87', 'vim33333333_38f5_438d_b8ee_3f93b3531f87', 'vim44444444_38f5_438d_b8ee_3f93b3531f87', 'vim55555555_38f5_438d_b8ee_3f93b3531f87', 'vim66666666_38f5_438d_b8ee_3f93b3531f87', 'vim77777777_38f5_438d_b8ee_3f93b3531f87', 'vim88888888_38f5_438d_b8ee_3f93b3531f87', 'vim99999999_38f5_438d_b8ee_3f93b3531f87'], + 'trp_link_latency' : [[0, 30, 70, 80, 32767, 32767, 32767, 32767, 32767, 32767], [30, 0, 75, 60, 32767, 32767, 32767, 32767, 32767, 32767], [70, 75, 0, 40, 100, 32767, 32767, 32767, 32767, 32767], [80, 60, 40, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 100, 32767, 0, 5, 5, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 5, 0, 5, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 5, 5, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 30, 20], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 30, 0, 20], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 20, 20, 0]], + 'trp_link_jitter' : [[0, 5, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 0, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 5, 0, 10, 5, 32767, 32767, 32767, 32767, 32767], [10, 10, 10, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 5, 32767, 0, 4, 4, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 4, 0, 10, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 4, 10, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 1, 1], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 1, 0, 1], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 1, 1, 0]], + 'trp_link_price_list' : [[0, 5, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 0, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 5, 0, 10, 10, 32767, 32767, 32767, 32767, 32767], [10, 10, 10, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 10, 32767, 0, 20, 20, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 20, 0, 20, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 20, 20, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 15, 15], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 15, 0, 15], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 15, 15, 0]], + 'ns_desc' : [{'vnf_id': '1', 'vnf_price_per_vim': ['5,10,30,30,100,70,40,100,150,200'], 'vim_account': 'vim99999999_38f5_438d_b8ee_3f93b3531f87'}, {'vnf_id': '2', 'vnf_price_per_vim': ['10,15,30,30,100,70,40,150,150,150'], 'vim_account': 'vim77777777_38f5_438d_b8ee_3f93b3531f87'}, {'vnf_id': '3', 'vnf_price_per_vim': ['30,30,60,40,100,90,40,150,200,170'], 'vim_account': 'vim88888888_38f5_438d_b8ee_3f93b3531f87'}], + 'vld_desc' : [{'cp_refs': ['1', '2'], 'latency': 120, 'jitter': 3}, {'cp_refs': ['2', '3'], 'latency': 25, 'jitter': 6}], + 'generator_data': {'file': __file__ ,'time': datetime.datetime.now()} + } + + mg = MznModelGenerator(logging.getLogger(__name__)) + test_mzn_model = mg.create_model(ns_placement_data) + + # run the model + expected_result = [{}] + + mpc = MznPlacementConductor(logging.getLogger(__name__)) + placement = mpc._run_placement_model(mzn_model=test_mzn_model, ns_desc=[{'vnf_id': '1', 'vnf_price_per_vim': ['5,10,30,30,100,70,40,100,150,200'], 'vim_account': 'vim99999999_38f5_438d_b8ee_3f93b3531f87'}, {'vnf_id': '2', 'vnf_price_per_vim': ['10,15,30,30,100,70,40,150,150,150'], 'vim_account': 'vim77777777_38f5_438d_b8ee_3f93b3531f87'}, {'vnf_id': '3', 'vnf_price_per_vim': ['30,30,60,40,100,90,40,150,200,170'], 'vim_account': 'vim88888888_38f5_438d_b8ee_3f93b3531f87'}]) + # sort the result to ease assert with expected result + if not placement[0]: + sorted_placement = placement + else: + sorted_placement = sorted(placement, key=lambda k: k['member-vnf-index']) + self.assertEqual(expected_result, sorted_placement, 'Faulty syntax or content') + def test_mznmodel_scenario3_subtestcase6(self): + # generate the model + ns_placement_data = {'vim_accounts': ['vim00000000_38f5_438d_b8ee_3f93b3531f87', 'vim11111111_38f5_438d_b8ee_3f93b3531f87', 'vim22222222_38f5_438d_b8ee_3f93b3531f87', 'vim33333333_38f5_438d_b8ee_3f93b3531f87', 'vim44444444_38f5_438d_b8ee_3f93b3531f87', 'vim55555555_38f5_438d_b8ee_3f93b3531f87', 'vim66666666_38f5_438d_b8ee_3f93b3531f87', 'vim77777777_38f5_438d_b8ee_3f93b3531f87', 'vim88888888_38f5_438d_b8ee_3f93b3531f87', 'vim99999999_38f5_438d_b8ee_3f93b3531f87'], + 'trp_link_latency' : [[0, 30, 70, 80, 32767, 32767, 32767, 32767, 32767, 32767], [30, 0, 75, 60, 32767, 32767, 32767, 32767, 32767, 32767], [70, 75, 0, 40, 100, 32767, 32767, 32767, 32767, 32767], [80, 60, 40, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 100, 32767, 0, 5, 5, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 5, 0, 5, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 5, 5, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 30, 20], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 30, 0, 20], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 20, 20, 0]], + 'trp_link_jitter' : [[0, 5, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 0, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 5, 0, 10, 5, 32767, 32767, 32767, 32767, 32767], [10, 10, 10, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 5, 32767, 0, 4, 4, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 4, 0, 10, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 4, 10, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 1, 1], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 1, 0, 1], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 1, 1, 0]], + 'trp_link_price_list' : [[0, 5, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 0, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 5, 0, 10, 10, 32767, 32767, 32767, 32767, 32767], [10, 10, 10, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 10, 32767, 0, 20, 20, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 20, 0, 20, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 20, 20, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 15, 15], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 15, 0, 15], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 15, 15, 0]], + 'ns_desc' : [{'vnf_id': '1', 'vnf_price_per_vim': ['5,10,30,30,100,70,40,100,150,200'], 'vim_account': 'vim77777777_38f5_438d_b8ee_3f93b3531f87'}, {'vnf_id': '2', 'vnf_price_per_vim': ['10,15,30,30,100,70,40,150,150,150'], 'vim_account': 'vim99999999_38f5_438d_b8ee_3f93b3531f87'}, {'vnf_id': '3', 'vnf_price_per_vim': ['30,30,60,40,100,90,40,150,200,170']}], + 'vld_desc' : [{'cp_refs': ['1', '2'], 'latency': 120, 'jitter': 3}, {'cp_refs': ['2', '3'], 'latency': 25, 'jitter': 6}], + 'generator_data': {'file': __file__ ,'time': datetime.datetime.now()} + } + + mg = MznModelGenerator(logging.getLogger(__name__)) + test_mzn_model = mg.create_model(ns_placement_data) + + # run the model + expected_result = [{'vimAccountId': '77777777-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '1'}, {'vimAccountId': '99999999-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '2'}, {'vimAccountId': '77777777-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '3'}] + + mpc = MznPlacementConductor(logging.getLogger(__name__)) + placement = mpc._run_placement_model(mzn_model=test_mzn_model, ns_desc=[{'vnf_id': '1', 'vnf_price_per_vim': ['5,10,30,30,100,70,40,100,150,200'], 'vim_account': 'vim77777777_38f5_438d_b8ee_3f93b3531f87'}, {'vnf_id': '2', 'vnf_price_per_vim': ['10,15,30,30,100,70,40,150,150,150'], 'vim_account': 'vim99999999_38f5_438d_b8ee_3f93b3531f87'}, {'vnf_id': '3', 'vnf_price_per_vim': ['30,30,60,40,100,90,40,150,200,170']}]) + # sort the result to ease assert with expected result + if not placement[0]: + sorted_placement = placement + else: + sorted_placement = sorted(placement, key=lambda k: k['member-vnf-index']) + self.assertEqual(expected_result, sorted_placement, 'Faulty syntax or content') + def test_mznmodel_scenario4_subtestcase1(self): + # generate the model + ns_placement_data = {'vim_accounts': ['vim00000000_38f5_438d_b8ee_3f93b3531f87', 'vim11111111_38f5_438d_b8ee_3f93b3531f87', 'vim22222222_38f5_438d_b8ee_3f93b3531f87', 'vim33333333_38f5_438d_b8ee_3f93b3531f87', 'vim44444444_38f5_438d_b8ee_3f93b3531f87', 'vim55555555_38f5_438d_b8ee_3f93b3531f87', 'vim66666666_38f5_438d_b8ee_3f93b3531f87', 'vim77777777_38f5_438d_b8ee_3f93b3531f87', 'vim88888888_38f5_438d_b8ee_3f93b3531f87', 'vim99999999_38f5_438d_b8ee_3f93b3531f87'], + 'trp_link_latency' : [[0, 30, 70, 80, 32767, 32767, 32767, 32767, 32767, 32767], [30, 0, 75, 60, 32767, 32767, 32767, 32767, 32767, 32767], [70, 75, 0, 40, 100, 32767, 32767, 32767, 32767, 32767], [80, 60, 40, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 100, 32767, 0, 5, 5, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 5, 0, 5, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 5, 5, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 30, 20], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 30, 0, 20], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 20, 20, 0]], + 'trp_link_jitter' : [[0, 5, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 0, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 5, 0, 10, 5, 32767, 32767, 32767, 32767, 32767], [10, 10, 10, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 5, 32767, 0, 4, 4, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 4, 0, 10, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 4, 10, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 1, 1], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 1, 0, 1], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 1, 1, 0]], + 'trp_link_price_list' : [[0, 5, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 0, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 5, 0, 10, 10, 32767, 32767, 32767, 32767, 32767], [10, 10, 10, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 10, 32767, 0, 20, 20, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 20, 0, 20, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 20, 20, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 15, 15], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 15, 0, 15], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 15, 15, 0]], + 'ns_desc' : [{'vnf_id': '1', 'vnf_price_per_vim': ['5,10,30,30,100,70,40,100,150,200']}, {'vnf_id': '2', 'vnf_price_per_vim': ['15,20,40,40,110,80,50,110,160,210']}, {'vnf_id': '3', 'vnf_price_per_vim': ['5,10,30,30,100,70,40,100,150,200']}, {'vnf_id': '4', 'vnf_price_per_vim': ['5,10,30,30,100,70,40,100,150,200']}], + 'vld_desc' : [{'cp_refs': ['1', '2'], 'latency': 120, 'jitter': 20}, {'cp_refs': ['2', '4'], 'latency': 50, 'jitter': 10}, {'cp_refs': ['2', '3'], 'latency': 20, 'jitter': 10}], + 'generator_data': {'file': __file__ ,'time': datetime.datetime.now()} + } + + mg = MznModelGenerator(logging.getLogger(__name__)) + test_mzn_model = mg.create_model(ns_placement_data) + + # run the model + expected_result = [{'vimAccountId': '00000000-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '1'}, {'vimAccountId': '00000000-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '2'}, {'vimAccountId': '00000000-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '3'}, {'vimAccountId': '00000000-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '4'}] + + mpc = MznPlacementConductor(logging.getLogger(__name__)) + placement = mpc._run_placement_model(mzn_model=test_mzn_model, ns_desc=[{'vnf_id': '1', 'vnf_price_per_vim': ['5,10,30,30,100,70,40,100,150,200']}, {'vnf_id': '2', 'vnf_price_per_vim': ['15,20,40,40,110,80,50,110,160,210']}, {'vnf_id': '3', 'vnf_price_per_vim': ['5,10,30,30,100,70,40,100,150,200']}, {'vnf_id': '4', 'vnf_price_per_vim': ['5,10,30,30,100,70,40,100,150,200']}]) + # sort the result to ease assert with expected result + if not placement[0]: + sorted_placement = placement + else: + sorted_placement = sorted(placement, key=lambda k: k['member-vnf-index']) + self.assertEqual(expected_result, sorted_placement, 'Faulty syntax or content') + def test_mznmodel_scenario4_subtestcase2(self): + # generate the model + ns_placement_data = {'vim_accounts': ['vim00000000_38f5_438d_b8ee_3f93b3531f87', 'vim11111111_38f5_438d_b8ee_3f93b3531f87', 'vim22222222_38f5_438d_b8ee_3f93b3531f87', 'vim33333333_38f5_438d_b8ee_3f93b3531f87', 'vim44444444_38f5_438d_b8ee_3f93b3531f87', 'vim55555555_38f5_438d_b8ee_3f93b3531f87', 'vim66666666_38f5_438d_b8ee_3f93b3531f87', 'vim77777777_38f5_438d_b8ee_3f93b3531f87', 'vim88888888_38f5_438d_b8ee_3f93b3531f87', 'vim99999999_38f5_438d_b8ee_3f93b3531f87'], + 'trp_link_latency' : [[0, 30, 70, 80, 32767, 32767, 32767, 32767, 32767, 32767], [30, 0, 75, 60, 32767, 32767, 32767, 32767, 32767, 32767], [70, 75, 0, 40, 100, 32767, 32767, 32767, 32767, 32767], [80, 60, 40, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 100, 32767, 0, 5, 5, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 5, 0, 5, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 5, 5, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 30, 20], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 30, 0, 20], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 20, 20, 0]], + 'trp_link_jitter' : [[0, 5, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 0, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 5, 0, 10, 5, 32767, 32767, 32767, 32767, 32767], [10, 10, 10, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 5, 32767, 0, 4, 4, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 4, 0, 10, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 4, 10, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 1, 1], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 1, 0, 1], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 1, 1, 0]], + 'trp_link_price_list' : [[0, 5, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 0, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 5, 0, 10, 10, 32767, 32767, 32767, 32767, 32767], [10, 10, 10, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 10, 32767, 0, 20, 20, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 20, 0, 20, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 20, 20, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 15, 15], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 15, 0, 15], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 15, 15, 0]], + 'ns_desc' : [{'vnf_id': '1', 'vnf_price_per_vim': ['5,10,30,30,100,70,40,100,150,200'], 'vim_account': 'vim22222222_38f5_438d_b8ee_3f93b3531f87'}, {'vnf_id': '2', 'vnf_price_per_vim': ['15,20,40,40,110,80,50,110,160,210']}, {'vnf_id': '3', 'vnf_price_per_vim': ['5,10,30,30,100,70,40,100,150,200'], 'vim_account': 'vim55555555_38f5_438d_b8ee_3f93b3531f87'}, {'vnf_id': '4', 'vnf_price_per_vim': ['5,10,30,30,100,70,40,100,150,200']}], + 'vld_desc' : [{'cp_refs': ['1', '2'], 'latency': 120, 'jitter': 20}, {'cp_refs': ['2', '4'], 'latency': 50, 'jitter': 10}, {'cp_refs': ['2', '3'], 'latency': 20, 'jitter': 10}], + 'generator_data': {'file': __file__ ,'time': datetime.datetime.now()} + } + + mg = MznModelGenerator(logging.getLogger(__name__)) + test_mzn_model = mg.create_model(ns_placement_data) + + # run the model + expected_result = [{'vimAccountId': '22222222-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '1'}, {'vimAccountId': '44444444-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '2'}, {'vimAccountId': '55555555-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '3'}, {'vimAccountId': '66666666-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '4'}] + + mpc = MznPlacementConductor(logging.getLogger(__name__)) + placement = mpc._run_placement_model(mzn_model=test_mzn_model, ns_desc=[{'vnf_id': '1', 'vnf_price_per_vim': ['5,10,30,30,100,70,40,100,150,200'], 'vim_account': 'vim22222222_38f5_438d_b8ee_3f93b3531f87'}, {'vnf_id': '2', 'vnf_price_per_vim': ['15,20,40,40,110,80,50,110,160,210']}, {'vnf_id': '3', 'vnf_price_per_vim': ['5,10,30,30,100,70,40,100,150,200'], 'vim_account': 'vim55555555_38f5_438d_b8ee_3f93b3531f87'}, {'vnf_id': '4', 'vnf_price_per_vim': ['5,10,30,30,100,70,40,100,150,200']}]) + # sort the result to ease assert with expected result + if not placement[0]: + sorted_placement = placement + else: + sorted_placement = sorted(placement, key=lambda k: k['member-vnf-index']) + self.assertEqual(expected_result, sorted_placement, 'Faulty syntax or content') + def test_mznmodel_scenario4_subtestcase3(self): + # generate the model + ns_placement_data = {'vim_accounts': ['vim00000000_38f5_438d_b8ee_3f93b3531f87', 'vim11111111_38f5_438d_b8ee_3f93b3531f87', 'vim22222222_38f5_438d_b8ee_3f93b3531f87', 'vim33333333_38f5_438d_b8ee_3f93b3531f87', 'vim44444444_38f5_438d_b8ee_3f93b3531f87', 'vim55555555_38f5_438d_b8ee_3f93b3531f87', 'vim66666666_38f5_438d_b8ee_3f93b3531f87', 'vim77777777_38f5_438d_b8ee_3f93b3531f87', 'vim88888888_38f5_438d_b8ee_3f93b3531f87', 'vim99999999_38f5_438d_b8ee_3f93b3531f87'], + 'trp_link_latency' : [[0, 30, 70, 80, 32767, 32767, 32767, 32767, 32767, 32767], [30, 0, 75, 60, 32767, 32767, 32767, 32767, 32767, 32767], [70, 75, 0, 40, 100, 32767, 32767, 32767, 32767, 32767], [80, 60, 40, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 100, 32767, 0, 5, 5, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 5, 0, 5, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 5, 5, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 30, 20], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 30, 0, 20], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 20, 20, 0]], + 'trp_link_jitter' : [[0, 5, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 0, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 5, 0, 10, 5, 32767, 32767, 32767, 32767, 32767], [10, 10, 10, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 5, 32767, 0, 4, 4, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 4, 0, 10, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 4, 10, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 1, 1], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 1, 0, 1], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 1, 1, 0]], + 'trp_link_price_list' : [[0, 5, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 0, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 5, 0, 10, 10, 32767, 32767, 32767, 32767, 32767], [10, 10, 10, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 10, 32767, 0, 20, 20, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 20, 0, 20, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 20, 20, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 15, 15], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 15, 0, 15], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 15, 15, 0]], + 'ns_desc' : [{'vnf_id': '1', 'vnf_price_per_vim': ['5,10,30,30,100,70,40,100,150,200'], 'vim_account': 'vim55555555_38f5_438d_b8ee_3f93b3531f87'}, {'vnf_id': '2', 'vnf_price_per_vim': ['15,20,40,40,110,80,50,110,160,210']}, {'vnf_id': '3', 'vnf_price_per_vim': ['5,10,30,30,100,70,40,100,150,200']}, {'vnf_id': '4', 'vnf_price_per_vim': ['5,10,30,30,100,70,40,100,150,200'], 'vim_account': 'vim55555555_38f5_438d_b8ee_3f93b3531f87'}], + 'vld_desc' : [{'cp_refs': ['1', '2'], 'latency': 120, 'jitter': 20}, {'cp_refs': ['2', '4'], 'latency': 50, 'jitter': 10}, {'cp_refs': ['2', '3'], 'latency': 20, 'jitter': 10}], + 'generator_data': {'file': __file__ ,'time': datetime.datetime.now()} + } + + mg = MznModelGenerator(logging.getLogger(__name__)) + test_mzn_model = mg.create_model(ns_placement_data) + + # run the model + expected_result = [{'vimAccountId': '55555555-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '1'}, {'vimAccountId': '66666666-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '2'}, {'vimAccountId': '66666666-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '3'}, {'vimAccountId': '55555555-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '4'}] + + mpc = MznPlacementConductor(logging.getLogger(__name__)) + placement = mpc._run_placement_model(mzn_model=test_mzn_model, ns_desc=[{'vnf_id': '1', 'vnf_price_per_vim': ['5,10,30,30,100,70,40,100,150,200'], 'vim_account': 'vim55555555_38f5_438d_b8ee_3f93b3531f87'}, {'vnf_id': '2', 'vnf_price_per_vim': ['15,20,40,40,110,80,50,110,160,210']}, {'vnf_id': '3', 'vnf_price_per_vim': ['5,10,30,30,100,70,40,100,150,200']}, {'vnf_id': '4', 'vnf_price_per_vim': ['5,10,30,30,100,70,40,100,150,200'], 'vim_account': 'vim55555555_38f5_438d_b8ee_3f93b3531f87'}]) + # sort the result to ease assert with expected result + if not placement[0]: + sorted_placement = placement + else: + sorted_placement = sorted(placement, key=lambda k: k['member-vnf-index']) + self.assertEqual(expected_result, sorted_placement, 'Faulty syntax or content') + def test_mznmodel_scenario4_subtestcase4(self): + # generate the model + ns_placement_data = {'vim_accounts': ['vim00000000_38f5_438d_b8ee_3f93b3531f87', 'vim11111111_38f5_438d_b8ee_3f93b3531f87', 'vim22222222_38f5_438d_b8ee_3f93b3531f87', 'vim33333333_38f5_438d_b8ee_3f93b3531f87', 'vim44444444_38f5_438d_b8ee_3f93b3531f87', 'vim55555555_38f5_438d_b8ee_3f93b3531f87', 'vim66666666_38f5_438d_b8ee_3f93b3531f87', 'vim77777777_38f5_438d_b8ee_3f93b3531f87', 'vim88888888_38f5_438d_b8ee_3f93b3531f87', 'vim99999999_38f5_438d_b8ee_3f93b3531f87'], + 'trp_link_latency' : [[0, 30, 70, 80, 32767, 32767, 32767, 32767, 32767, 32767], [30, 0, 75, 60, 32767, 32767, 32767, 32767, 32767, 32767], [70, 75, 0, 40, 100, 32767, 32767, 32767, 32767, 32767], [80, 60, 40, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 100, 32767, 0, 5, 5, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 5, 0, 5, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 5, 5, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 30, 20], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 30, 0, 20], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 20, 20, 0]], + 'trp_link_jitter' : [[0, 5, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 0, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 5, 0, 10, 5, 32767, 32767, 32767, 32767, 32767], [10, 10, 10, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 5, 32767, 0, 4, 4, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 4, 0, 10, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 4, 10, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 1, 1], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 1, 0, 1], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 1, 1, 0]], + 'trp_link_price_list' : [[0, 5, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 0, 5, 10, 32767, 32767, 32767, 32767, 32767, 32767], [5, 5, 0, 10, 10, 32767, 32767, 32767, 32767, 32767], [10, 10, 10, 0, 32767, 32767, 32767, 32767, 32767, 32767], [32767, 32767, 10, 32767, 0, 20, 20, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 20, 0, 20, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 20, 20, 0, 32767, 32767, 32767], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 15, 15], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 15, 0, 15], [32767, 32767, 32767, 32767, 32767, 32767, 32767, 15, 15, 0]], + 'ns_desc' : [{'vnf_id': '1', 'vnf_price_per_vim': ['5,10,30,30,100,70,40,100,150,200'], 'vim_account': 'vim00000000_38f5_438d_b8ee_3f93b3531f87'}, {'vnf_id': '2', 'vnf_price_per_vim': ['15,20,40,40,110,80,50,110,160,210']}, {'vnf_id': '3', 'vnf_price_per_vim': ['5,10,30,30,100,70,40,100,150,200'], 'vim_account': 'vim44444444_38f5_438d_b8ee_3f93b3531f87'}, {'vnf_id': '4', 'vnf_price_per_vim': ['5,10,30,30,100,70,40,100,150,200']}], + 'vld_desc' : [{'cp_refs': ['1', '2'], 'latency': 120, 'jitter': 20}, {'cp_refs': ['2', '4'], 'latency': 50, 'jitter': 10}, {'cp_refs': ['2', '3'], 'latency': 20, 'jitter': 10}], + 'generator_data': {'file': __file__ ,'time': datetime.datetime.now()} + } + + mg = MznModelGenerator(logging.getLogger(__name__)) + test_mzn_model = mg.create_model(ns_placement_data) + + # run the model + expected_result = [{}] + + mpc = MznPlacementConductor(logging.getLogger(__name__)) + placement = mpc._run_placement_model(mzn_model=test_mzn_model, ns_desc=[{'vnf_id': '1', 'vnf_price_per_vim': ['5,10,30,30,100,70,40,100,150,200'], 'vim_account': 'vim00000000_38f5_438d_b8ee_3f93b3531f87'}, {'vnf_id': '2', 'vnf_price_per_vim': ['15,20,40,40,110,80,50,110,160,210']}, {'vnf_id': '3', 'vnf_price_per_vim': ['5,10,30,30,100,70,40,100,150,200'], 'vim_account': 'vim44444444_38f5_438d_b8ee_3f93b3531f87'}, {'vnf_id': '4', 'vnf_price_per_vim': ['5,10,30,30,100,70,40,100,150,200']}]) + # sort the result to ease assert with expected result + if not placement[0]: + sorted_placement = placement + else: + sorted_placement = sorted(placement, key=lambda k: k['member-vnf-index']) + self.assertEqual(expected_result, sorted_placement, 'Faulty syntax or content') \ No newline at end of file diff --git a/osm_pla/test/test_nsPlacementDataFactory.py b/osm_pla/test/test_nsPlacementDataFactory.py new file mode 100644 index 0000000..c53ad57 --- /dev/null +++ b/osm_pla/test/test_nsPlacementDataFactory.py @@ -0,0 +1,777 @@ +# Copyright 2020 ArctosLabs Scandinavia AB +# +# 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 unittest +from collections import Counter +from pathlib import Path +from unittest import TestCase, mock +from unittest.mock import call + +import yaml + +from osm_pla.placement.mznplacement import NsPlacementDataFactory + + +class TestNsPlacementDataFactory(TestCase): + vim_accounts = [{"vim_password": "FxtnynxBCnouzAT4Hkerhg==", "config": {}, + "_admin": {"modified": 1564579854.0480285, "created": 1564579854.0480285, + "operationalState": "ENABLED", + "projects_read": ["69915588-e5e2-46d3-96b0-a29bedef6f73"], + "deployed": {"RO-account": "6beb4e2e-b397-11e9-a7a3-02420aff0008", + "RO": "6bcfc3fc-b397-11e9-a7a3-02420aff0008"}, + "projects_write": ["69915588-e5e2-46d3-96b0-a29bedef6f73"], "detailed-status": "Done"}, + "name": "OpenStack1", "vim_type": "openstack", "_id": "92b056a7-38f5-438d-b8ee-3f93b3531f87", + "schema_version": "1.1", "vim_user": "admin", "vim_url": "http://10.234.12.47:5000/v3", + "vim_tenant_name": "admin"}, + {"config": {}, "vim_tenant_name": "osm_demo", "schema_version": "1.1", "name": "OpenStack2", + "vim_password": "gK5v4Gh2Pl41o6Skwp6RCw==", "vim_type": "openstack", + "_admin": {"modified": 1567148372.2490237, "created": 1567148372.2490237, + "operationalState": "ENABLED", + "projects_read": ["69915588-e5e2-46d3-96b0-a29bedef6f73"], + "deployed": {"RO-account": "b7fb0034-caf3-11e9-9388-02420aff000a", + "RO": "b7f129ce-caf3-11e9-9388-02420aff000a"}, + "projects_write": ["69915588-e5e2-46d3-96b0-a29bedef6f73"], "detailed-status": "Done"}, + "vim_user": "admin", "vim_url": "http://10.234.12.44:5000/v3", + "_id": "6618d412-d7fc-4eb0-a6f8-d2c258e0e900"}, + {"config": {}, "schema_version": "1.1", "name": "OpenStack3", + "vim_password": "1R2FoMQnaL6rNSosoRP2hw==", "vim_type": "openstack", "vim_tenant_name": "osm_demo", + "_admin": {"modified": 1567599746.689582, "created": 1567599746.689582, + "operationalState": "ENABLED", + "projects_read": ["69915588-e5e2-46d3-96b0-a29bedef6f73"], + "deployed": {"RO-account": "a8161f54-cf0e-11e9-9388-02420aff000a", + "RO": "a80b6280-cf0e-11e9-9388-02420aff000a"}, + "projects_write": ["69915588-e5e2-46d3-96b0-a29bedef6f73"], "detailed-status": "Done"}, + "vim_user": "admin", "vim_url": "http://10.234.12.46:5000/v3", + "_id": "331ffdec-44a8-4707-94a1-af7a292d9735"}, + {"config": {}, "schema_version": "1.1", "name": "OpenStack4", + "vim_password": "6LScyPeMq3QFh3GRb/xwZw==", "vim_type": "openstack", "vim_tenant_name": "osm_demo", + "_admin": {"modified": 1567599911.5108898, "created": 1567599911.5108898, + "operationalState": "ENABLED", + "projects_read": ["69915588-e5e2-46d3-96b0-a29bedef6f73"], + "deployed": {"RO-account": "0a651200-cf0f-11e9-9388-02420aff000a", + "RO": "0a4defc6-cf0f-11e9-9388-02420aff000a"}, + "projects_write": ["69915588-e5e2-46d3-96b0-a29bedef6f73"], "detailed-status": "Done"}, + "vim_user": "admin", "vim_url": "http://10.234.12.43:5000/v3", + "_id": "eda92f47-29b9-4007-9709-c1833dbfbe31"}] + + vim_accounts_fewer_vims = [{"vim_password": "FxtnynxBCnouzAT4Hkerhg==", "config": {}, + "_admin": {"modified": 1564579854.0480285, "created": 1564579854.0480285, + "operationalState": "ENABLED", + "projects_read": ["69915588-e5e2-46d3-96b0-a29bedef6f73"], + "deployed": {"RO-account": "6beb4e2e-b397-11e9-a7a3-02420aff0008", + "RO": "6bcfc3fc-b397-11e9-a7a3-02420aff0008"}, + "projects_write": ["69915588-e5e2-46d3-96b0-a29bedef6f73"], + "detailed-status": "Done"}, + "name": "OpenStack1", "vim_type": "openstack", + "_id": "92b056a7-38f5-438d-b8ee-3f93b3531f87", + "schema_version": "1.1", "vim_user": "admin", "vim_url": "http://10.234.12.47:5000/v3", + "vim_tenant_name": "admin"}, + {"config": {}, "vim_tenant_name": "osm_demo", "schema_version": "1.1", + "name": "OpenStack2", + "vim_password": "gK5v4Gh2Pl41o6Skwp6RCw==", "vim_type": "openstack", + "_admin": {"modified": 1567148372.2490237, "created": 1567148372.2490237, + "operationalState": "ENABLED", + "projects_read": ["69915588-e5e2-46d3-96b0-a29bedef6f73"], + "deployed": {"RO-account": "b7fb0034-caf3-11e9-9388-02420aff000a", + "RO": "b7f129ce-caf3-11e9-9388-02420aff000a"}, + "projects_write": ["69915588-e5e2-46d3-96b0-a29bedef6f73"], + "detailed-status": "Done"}, + "vim_user": "admin", "vim_url": "http://10.234.12.44:5000/v3", + "_id": "6618d412-d7fc-4eb0-a6f8-d2c258e0e900"}, + {"config": {}, "schema_version": "1.1", "name": "OpenStack4", + "vim_password": "6LScyPeMq3QFh3GRb/xwZw==", "vim_type": "openstack", + "vim_tenant_name": "osm_demo", + "_admin": {"modified": 1567599911.5108898, "created": 1567599911.5108898, + "operationalState": "ENABLED", + "projects_read": ["69915588-e5e2-46d3-96b0-a29bedef6f73"], + "deployed": {"RO-account": "0a651200-cf0f-11e9-9388-02420aff000a", + "RO": "0a4defc6-cf0f-11e9-9388-02420aff000a"}, + "projects_write": ["69915588-e5e2-46d3-96b0-a29bedef6f73"], + "detailed-status": "Done"}, + "vim_user": "admin", "vim_url": "http://10.234.12.43:5000/v3", + "_id": "eda92f47-29b9-4007-9709-c1833dbfbe31"}] + + vim_accounts_more_vims = [{"vim_password": "FxtnynxBCnouzAT4Hkerhg==", "config": {}, + "_admin": {"modified": 1564579854.0480285, "created": 1564579854.0480285, + "operationalState": "ENABLED", + "projects_read": ["69915588-e5e2-46d3-96b0-a29bedef6f73"], + "deployed": {"RO-account": "6beb4e2e-b397-11e9-a7a3-02420aff0008", + "RO": "6bcfc3fc-b397-11e9-a7a3-02420aff0008"}, + "projects_write": ["69915588-e5e2-46d3-96b0-a29bedef6f73"], + "detailed-status": "Done"}, + "name": "OpenStack1", "vim_type": "openstack", + "_id": "92b056a7-38f5-438d-b8ee-3f93b3531f87", + "schema_version": "1.1", "vim_user": "admin", "vim_url": "http://10.234.12.47:5000/v3", + "vim_tenant_name": "admin"}, + {"config": {}, "vim_tenant_name": "osm_demo", "schema_version": "1.1", + "name": "OpenStack2", + "vim_password": "gK5v4Gh2Pl41o6Skwp6RCw==", "vim_type": "openstack", + "_admin": {"modified": 1567148372.2490237, "created": 1567148372.2490237, + "operationalState": "ENABLED", + "projects_read": ["69915588-e5e2-46d3-96b0-a29bedef6f73"], + "deployed": {"RO-account": "b7fb0034-caf3-11e9-9388-02420aff000a", + "RO": "b7f129ce-caf3-11e9-9388-02420aff000a"}, + "projects_write": ["69915588-e5e2-46d3-96b0-a29bedef6f73"], + "detailed-status": "Done"}, + "vim_user": "admin", "vim_url": "http://10.234.12.44:5000/v3", + "_id": "6618d412-d7fc-4eb0-a6f8-d2c258e0e900"}, + {"config": {}, "schema_version": "1.1", "name": "OpenStack4", + "vim_password": "6LScyPeMq3QFh3GRb/xwZw==", "vim_type": "openstack", + "vim_tenant_name": "osm_demo", + "_admin": {"modified": 1567599911.5108898, "created": 1567599911.5108898, + "operationalState": "ENABLED", + "projects_read": ["69915588-e5e2-46d3-96b0-a29bedef6f73"], + "deployed": {"RO-account": "0a651200-cf0f-11e9-9388-02420aff000a", + "RO": "0a4defc6-cf0f-11e9-9388-02420aff000a"}, + "projects_write": ["69915588-e5e2-46d3-96b0-a29bedef6f73"], + "detailed-status": "Done"}, + "vim_user": "admin", "vim_url": "http://10.234.12.43:5000/v3", + "_id": "eda92f47-29b9-4007-9709-c1833dbfbe31"}, + {"config": {}, "schema_version": "1.1", "name": "OpenStack3", + "vim_password": "6LScyPeMq3QFh3GRb/xwZw==", "vim_type": "openstack", + "vim_tenant_name": "osm_demo", + "_admin": {"modified": 1567599911.5108898, "created": 1567599911.5108898, + "operationalState": "ENABLED", + "projects_read": ["69915588-e5e2-46d3-96b0-a29bedef6f73"], + "deployed": {"RO-account": "0a651200-cf0f-11e9-9388-02420aff000a", + "RO": "0a4defc6-cf0f-11e9-9388-02420aff000a"}, + "projects_write": ["69915588-e5e2-46d3-96b0-a29bedef6f73"], + "detailed-status": "Done"}, + "vim_user": "admin", "vim_url": "http://10.234.12.46:5000/v3", + "_id": "eda92f47-29b9-4007-9709-c1833dbfbe31"}, + {"config": {}, "schema_version": "1.1", "name": "OpenStack5", + "vim_password": "6LScyPeMq3QFh3GRb/xwZw==", "vim_type": "openstack", + "vim_tenant_name": "osm_demo", + "_admin": {"modified": 1567599911.5108898, "created": 1567599911.5108898, + "operationalState": "ENABLED", + "projects_read": ["69915588-e5e2-46d3-96b0-a29bedef6f73"], + "deployed": {"RO-account": "0a651200-cf0f-11e9-9388-02420aff000a", + "RO": "0a4defc6-cf0f-11e9-9388-02420aff000a"}, + "projects_write": ["69915588-e5e2-46d3-96b0-a29bedef6f73"], + "detailed-status": "Done"}, + "vim_user": "admin", "vim_url": "http://1.1.1.1:5000/v3", + "_id": "ffffffff-29b9-4007-9709-c1833dbfbe31"}] + + def _produce_ut_vim_accounts_info(self, vim_accounts): + """ + FIXME temporary, we will need more control over vim_urls and _id for test purpose - make a generator + :return: vim_url and _id as dict, i.e. extract these from vim_accounts data + """ + return {_['vim_url']: _['_id'] for _ in vim_accounts} + + def _adjust_path(self, file): + """In case we are not running from test directory, + then assume we are in top level directory (e.g. running from tox) and adjust file path accordingly""" + path_component = '/osm_pla/test/' + real_path = os.path.realpath(file) + if path_component not in real_path: + return os.path.dirname(real_path) + path_component + os.path.basename(real_path) + else: + return real_path + + def _populate_pil_info(self, file): + """ + Note str(Path()) is a 3.5 thing + """ + with open(str(Path(self._adjust_path(file)))) as pp_fd: + test_data = yaml.safe_load_all(pp_fd) + return next(test_data) + + def _get_ut_nsd_from_file(self, nsd_file_name): + """ + creates the structure representing the nsd. + + IMPORTANT NOTE: If using .yaml files from the NS packages for the unit tests (which we do), + then the files must be modified with respect to the way booleans are processed at on-boarding in OSM. + The following construct in the NS package yaml file: + mgmt-network: 'false' + will become a boolean in the MongoDB, and therefore the yaml used in these unit test must use yaml + tag as follows: + mgmt-network: !!bool False + The modification also applies to 'true' => !!bool True + This will ensure that the object returned from this function is as expected by PLA. + """ + with open(str(Path(self._adjust_path(nsd_file_name)))) as nsd_fd: + test_data = yaml.safe_load_all(nsd_fd) + return next(test_data) + + def _produce_ut_vnf_price_list(self): + price_list_file = "vnf_price_list.yaml" + with open(str(Path(self._adjust_path(price_list_file)))) as pl_fd: + price_list_data = yaml.safe_load_all(pl_fd) + return {i['vnfd']: {i1['vim_url']: i1['price'] for i1 in i['prices']} for i in next(price_list_data)} + + def _produce_ut_vnf_test_price_list(self, price_list): + price_list_file = price_list + with open(str(Path(self._adjust_path(price_list_file)))) as pl_fd: + price_list_data = yaml.safe_load_all(pl_fd) + return {i['vnfd']: {i1['vim_url']: i1['price'] for i1 in i['prices']} for i in next(price_list_data)} + + def test__produce_trp_link_characteristics_link_latency_with_more_vims(self): + """ + -test with more(other) vims compared to pil + """ + content_expected = [0, 0, 0, 0, 0, 120, 120, 130, 130, 140, 140, 230, 230, 240, 240, + 340, 340, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767] + nspdf = NsPlacementDataFactory( + self._produce_ut_vim_accounts_info(TestNsPlacementDataFactory.vim_accounts_more_vims), + self._produce_ut_vnf_price_list(), + nsd=None, + pil_info=self._populate_pil_info('pil_unittest1.yaml'), + pinning=None) + pil_latencies = nspdf._produce_trp_link_characteristics_data('pil_latency') + content_produced = [i for row in pil_latencies for i in row] + self.assertEqual(Counter(content_expected), Counter(content_produced), 'trp_link_latency incorrect') + + def test__produce_trp_link_characteristics_link_latency_with_fewer_vims(self): + """ + -test with fewer vims compared to pil + :return: + """ + content_expected = [0, 0, 0, 120, 120, 140, 140, 240, 240] + nspdf = NsPlacementDataFactory( + self._produce_ut_vim_accounts_info(TestNsPlacementDataFactory.vim_accounts_fewer_vims), + self._produce_ut_vnf_price_list(), + nsd=None, + pil_info=self._populate_pil_info('pil_unittest1.yaml'), + pinning=None) + pil_latencies = nspdf._produce_trp_link_characteristics_data('pil_latency') + content_produced = [i for row in pil_latencies for i in row] + self.assertEqual(Counter(content_expected), Counter(content_produced), 'trp_link_latency incorrect') + + def test__produce_trp_link_characteristic_not_supported(self): + """ + - test with non-supported characteristic + """ + nspdf = NsPlacementDataFactory(self._produce_ut_vim_accounts_info(TestNsPlacementDataFactory.vim_accounts), + self._produce_ut_vnf_price_list(), + nsd=None, + pil_info=self._populate_pil_info('pil_unittest1.yaml'), pinning=None) + + with self.assertRaises(Exception) as e: + nspdf._produce_trp_link_characteristics_data('test_no_support') + self.assertRegex(str(e.exception), r'characteristic.*not supported', "invalid exception content") + + def test__produce_trp_link_characteristics_link_latency(self): + """ + -test with full set of vims as in pil + -test with fewer vims compared to pil + -test with more(other) vims compared to pil + -test with invalid/corrupt pil configuration file (e.g. missing endpoint), empty file, not yaml conformant + - test with non-supported characteristic + + :return: + """ + content_expected = [0, 0, 0, 0, 120, 120, 130, 130, 140, 140, 230, 230, 240, 240, 340, 340] + + nspdf = NsPlacementDataFactory(self._produce_ut_vim_accounts_info(TestNsPlacementDataFactory.vim_accounts), + self._produce_ut_vnf_price_list(), + nsd=None, + pil_info=self._populate_pil_info('pil_unittest1.yaml'), pinning=None) + pil_latencies = nspdf._produce_trp_link_characteristics_data('pil_latency') + content_produced = [i for row in pil_latencies for i in row] + self.assertEqual(Counter(content_expected), Counter(content_produced), 'trp_link_latency incorrect') + + def test__produce_trp_link_characteristics_link_jitter(self): + """ + -test with full set of vims as in pil + """ + content_expected = [0, 0, 0, 0, 1200, 1200, 1300, 1300, 1400, 1400, 2300, 2300, 2400, 2400, 3400, 3400] + + nspdf = NsPlacementDataFactory(self._produce_ut_vim_accounts_info(TestNsPlacementDataFactory.vim_accounts), + self._produce_ut_vnf_price_list(), + nsd=None, + pil_info=self._populate_pil_info('pil_unittest1.yaml'), pinning=None) + pil_jitter = nspdf._produce_trp_link_characteristics_data('pil_jitter') + content_produced = [i for row in pil_jitter for i in row] + self.assertEqual(Counter(content_expected), Counter(content_produced), 'trp_link_jitter incorrect') + + def test__produce_trp_link_characteristics_link_jitter_with_fewer_vims(self): + """ + -test with fewer vims compared to pil, link jitter + """ + content_expected = [0, 0, 0, 1200, 1200, 1400, 1400, 2400, 2400] + nspdf = NsPlacementDataFactory(self._produce_ut_vim_accounts_info(self.vim_accounts_fewer_vims), + self._produce_ut_vnf_price_list(), + nsd=None, + pil_info=self._populate_pil_info('pil_unittest1.yaml'), pinning=None) + pil_latencies = nspdf._produce_trp_link_characteristics_data('pil_jitter') + content_produced = [i for row in pil_latencies for i in row] + self.assertEqual(Counter(content_expected), Counter(content_produced), 'trp_link_jitter incorrect') + + def test__produce_trp_link_characteristics_link_jitter_with_more_vims(self): + """ + -test with more vims compared to pil, link jitter + """ + content_expected = [0, 0, 0, 0, 0, 1200, 1200, 1300, 1300, 1400, 1400, 2300, + 2300, 2400, 2400, 3400, 3400, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767] + nspdf = NsPlacementDataFactory(self._produce_ut_vim_accounts_info(self.vim_accounts_more_vims), + self._produce_ut_vnf_price_list(), + nsd=None, + pil_info=self._populate_pil_info('pil_unittest1.yaml'), pinning=None) + pil_latencies = nspdf._produce_trp_link_characteristics_data('pil_jitter') + content_produced = [i for row in pil_latencies for i in row] + self.assertEqual(Counter(content_expected), Counter(content_produced), 'trp_link_jitter incorrect') + + def test__produce_trp_link_characteristics_link_price(self): + """ + -test with full set of vims as in pil + """ + content_expected = [0, 0, 0, 0, 12, 12, 13, 13, 14, 14, 23, 23, 24, 24, 34, 34] + nspdf = NsPlacementDataFactory(self._produce_ut_vim_accounts_info(TestNsPlacementDataFactory.vim_accounts), + self._produce_ut_vnf_price_list(), + nsd=None, + pil_info=self._populate_pil_info('pil_unittest1.yaml'), pinning=None) + pil_prices = nspdf._produce_trp_link_characteristics_data('pil_price') + content_produced = [i for row in pil_prices for i in row] + self.assertEqual(Counter(content_expected), Counter(content_produced), 'invalid trp link prices') + + def test__produce_trp_link_characteristics_link_price_with_fewer_vims(self): + """ + -test with fewer vims compared to pil + """ + content_expected = [0, 0, 0, 12, 12, 14, 14, 24, 24] + nspdf = NsPlacementDataFactory(self._produce_ut_vim_accounts_info(self.vim_accounts_fewer_vims), + self._produce_ut_vnf_price_list(), + nsd=None, + pil_info=self._populate_pil_info('pil_unittest1.yaml'), pinning=None) + pil_prices = nspdf._produce_trp_link_characteristics_data('pil_price') + content_produced = [i for row in pil_prices for i in row] + self.assertEqual(Counter(content_expected), Counter(content_produced), 'invalid trp link prices') + + def test__produce_trp_link_characteristics_partly_constrained(self): + content_expected = [0, 0, 0, 0, 32767, 32767, 32767, 32767, 1200, 1200, 1400, 1400, 2400, 2400, 3400, 3400] + nspdf = NsPlacementDataFactory(self._produce_ut_vim_accounts_info(TestNsPlacementDataFactory.vim_accounts), + self._produce_ut_vnf_price_list(), + nsd=None, + pil_info=self._populate_pil_info('pil_unittest2.yaml'), pinning=None) + pil_jitter = nspdf._produce_trp_link_characteristics_data('pil_jitter') + content_produced = [i for row in pil_jitter for i in row] + self.assertEqual(Counter(content_expected), Counter(content_produced), + 'invalid trp link jitter, partly constrained') + + def test__produce_vld_desc_partly_constrained(self): + vld_desc_expected = [{'cp_refs': ['one', 'two'], 'jitter': 30}, + {'cp_refs': ['two', 'three'], 'latency': 120}] + + nsd = self._get_ut_nsd_from_file('nsd_unittest2.yaml') + nsd = nsd['nsd:nsd-catalog']['nsd'][0] + nspdf = NsPlacementDataFactory(self._produce_ut_vim_accounts_info(TestNsPlacementDataFactory.vim_accounts), + self._produce_ut_vnf_price_list(), + nsd=nsd, + pil_info=None, pinning=None) + self.assertEqual(vld_desc_expected, nspdf._produce_vld_desc(), + "vld_desc incorrect") + + def test__produce_trp_link_characteristics_link_latency_not_yaml_conformant(self): + """ + -test with invalid/corrupt pil configuration file (not yaml conformant) + """ + with self.assertRaises(Exception) as e: + _ = NsPlacementDataFactory(self._produce_ut_vim_accounts_info(TestNsPlacementDataFactory.vim_accounts), + self._produce_ut_vnf_price_list(), + nsd=None, + pil_info=self._populate_pil_info('not_yaml_conformant.yaml'), + pinning=None) + self.assertRegex(str(e.exception), r'mapping values are not allowed here.*', "invalid exception content") + + def test__produce_trp_link_characteristics_with_invalid_pil_config(self): + """ + -test with invalid/corrupt pil configuration file (missing endpoint) + """ + nspdf = NsPlacementDataFactory(self._produce_ut_vim_accounts_info(TestNsPlacementDataFactory.vim_accounts), + self._produce_ut_vnf_price_list(), + nsd=None, + pil_info=self._populate_pil_info('corrupt_pil_endpoints_config_unittest1.yaml'), + pinning=None) + with self.assertRaises(Exception) as e: + _ = nspdf._produce_trp_link_characteristics_data('pil_latency') + self.assertEqual('list index out of range', str(e.exception), "unexpected exception") + + def test__produce_vld_desc_w_instantiate_override(self): + + vld_desc_expected = [{'cp_refs': ['one', 'two'], 'latency': 150, 'jitter': 30}, + {'cp_refs': ['two', 'three'], 'latency': 90, 'jitter': 30}] + + nsd = self._get_ut_nsd_from_file('nsd_unittest_no_vld_constraints.yaml') + nsd = nsd['nsd:nsd-catalog']['nsd'][0] + nspdf = NsPlacementDataFactory(self._produce_ut_vim_accounts_info(TestNsPlacementDataFactory.vim_accounts), + self._produce_ut_vnf_price_list(), + nsd=nsd, + pil_info=None, pinning=None, + order_constraints=None) + + self.assertNotEqual(nspdf._produce_vld_desc(), + vld_desc_expected, "vld_desc incorrect") + + def test__produce_vld_desc_nsd_w_instantiate_wo(self): + """ + nsd w/ constraints, instantiate w/o constraints + :return: + """ + vld_desc_expected = [{'cp_refs': ['one', 'two'], 'latency': 150, 'jitter': 30}, + {'cp_refs': ['two', 'three'], 'latency': 90, 'jitter': 30}] + + nsd = self._get_ut_nsd_from_file('nsd_unittest3.yaml') + nsd = nsd['nsd:nsd-catalog']['nsd'][0] + nspdf = NsPlacementDataFactory(self._produce_ut_vim_accounts_info(TestNsPlacementDataFactory.vim_accounts), + self._produce_ut_vnf_price_list(), + nsd=nsd, + pil_info=None, pinning=None, + order_constraints=None) + + self.assertEqual(vld_desc_expected, nspdf._produce_vld_desc(), + "vld_desc incorrect") + + def test__produce_vld_desc_nsd_w_instantiate_w(self): + """ + nsd w/ constraints, instantiate w/ constraints => override + :return: + """ + vld_desc_expected = [{'cp_refs': ['one', 'two'], 'latency': 120, 'jitter': 21}, + {'cp_refs': ['two', 'three'], 'latency': 121, 'jitter': 22}] + + nsd = self._get_ut_nsd_from_file('nsd_unittest3.yaml') + nsd = nsd['nsd:nsd-catalog']['nsd'][0] + nspdf = NsPlacementDataFactory(self._produce_ut_vim_accounts_info(TestNsPlacementDataFactory.vim_accounts), + self._produce_ut_vnf_price_list(), + nsd=nsd, + pil_info=None, pinning=None, + order_constraints={ + 'vld-constraints': [{'id': 'three_vnf_constrained_nsd_vld1', + 'link-constraints': {'latency': 120, + 'jitter': 21}}, + {'id': 'three_vnf_constrained_nsd_vld2', + 'link-constraints': {'latency': 121, + 'jitter': 22}}]}) + + self.assertEqual(vld_desc_expected, nspdf._produce_vld_desc(), + "vld_desc incorrect") + + def test__produce_vld_desc_nsd_wo_instantiate_wo(self): + """ + nsd w/o constraints, instantiate w/o constraints = no constraints in model + :return: + """ + vld_desc_expected = [{'cp_refs': ['one', 'two']}, + {'cp_refs': ['two', 'three']}] + + nsd = self._get_ut_nsd_from_file('nsd_unittest_no_vld_constraints.yaml') + nsd = nsd['nsd:nsd-catalog']['nsd'][0] + nspdf = NsPlacementDataFactory(self._produce_ut_vim_accounts_info(TestNsPlacementDataFactory.vim_accounts), + self._produce_ut_vnf_price_list(), + nsd=nsd, + pil_info=None, pinning=None, + order_constraints=None) + + self.assertEqual(vld_desc_expected, nspdf._produce_vld_desc(), + "vld_desc incorrect") + + def test__produce_vld_desc_nsd_wo_instantiate_w(self): + """ + nsd w/o constraints, instantiate w/ constraints => add constraints + :return: + """ + vld_desc_expected = [{'cp_refs': ['one', 'two'], 'latency': 140, 'jitter': 41}, + {'cp_refs': ['two', 'three'], 'latency': 141, 'jitter': 42}] + + nsd = self._get_ut_nsd_from_file('nsd_unittest_no_vld_constraints.yaml') + nsd = nsd['nsd:nsd-catalog']['nsd'][0] + nspdf = NsPlacementDataFactory(self._produce_ut_vim_accounts_info(TestNsPlacementDataFactory.vim_accounts), + self._produce_ut_vnf_price_list(), + nsd=nsd, + pil_info=None, pinning=None, + order_constraints={ + 'vld-constraints': [{'id': 'three_vnf_constrained_nsd_vld1', + 'link-constraints': {'latency': 140, + 'jitter': 41}}, + {'id': 'three_vnf_constrained_nsd_vld2', + 'link-constraints': {'latency': 141, + 'jitter': 42}}]}) + + self.assertEqual(vld_desc_expected, nspdf._produce_vld_desc(), + "vld_desc incorrect") + + def test__produce_vld_desc_nsd_wo_instantiate_w_faulty_input(self): + """ + nsd w/o constraints, instantiate w/ constraints => add constraints that can be parsed + :return: + """ + vld_desc_expected = [{'cp_refs': ['one', 'two']}, + {'cp_refs': ['two', 'three'], 'latency': 151}] + + nsd = self._get_ut_nsd_from_file('nsd_unittest_no_vld_constraints.yaml') + nsd = nsd['nsd:nsd-catalog']['nsd'][0] + nspdf = NsPlacementDataFactory(self._produce_ut_vim_accounts_info(TestNsPlacementDataFactory.vim_accounts), + self._produce_ut_vnf_price_list(), + nsd=nsd, + pil_info=None, pinning=None, + order_constraints={'vld-constraints': [{'id': 'not_included_vld', + 'misspelled-constraints': + {'latency': 120, + 'jitter': 20}}, + {'id': 'three_vnf_constrained_nsd_vld2', + 'link-constraints': { + 'latency': 151}}]}) + + self.assertEqual(vld_desc_expected, nspdf._produce_vld_desc(), + "vld_desc incorrect") + + def test__produce_vld_desc_nsd_wo_instantiate_w_faulty_input_again(self): + """ + nsd w/o constraints, instantiate w/ faulty constraints => add constraints that can be parsed + :return: + """ + vld_desc_expected = [{'cp_refs': ['one', 'two'], 'jitter': 21}, + {'cp_refs': ['two', 'three']}] + + nsd = self._get_ut_nsd_from_file('nsd_unittest_no_vld_constraints.yaml') + nsd = nsd['nsd:nsd-catalog']['nsd'][0] + nspdf = NsPlacementDataFactory(self._produce_ut_vim_accounts_info(TestNsPlacementDataFactory.vim_accounts), + self._produce_ut_vnf_price_list(), + nsd=nsd, + pil_info=None, pinning=None, + order_constraints={ + 'vld-constraints': [{'id': 'three_vnf_constrained_nsd_vld1', + 'link-constraints': {'delay': 120, + 'jitter': 21}}, + {'id': 'three_vnf_constrained_nsd_vld2', + 'misspelled-constraints': {'latency': 121, + 'jitter': 22}}]}) + + self.assertEqual(vld_desc_expected, nspdf._produce_vld_desc(), + "vld_desc incorrect") + + def test__produce_vld_desc_mgmt_network(self): + vld_desc_expected = [{'cp_refs': ['1', '2'], 'latency': 120, 'jitter': 20}, + {'cp_refs': ['2', '4'], 'latency': 50, 'jitter': 10}, + {'cp_refs': ['2', '3'], 'latency': 20, 'jitter': 10}, ] + + nsd = self._get_ut_nsd_from_file('test_five_nsd.yaml') + nsd = nsd['nsd:nsd-catalog']['nsd'][0] + nspdf = NsPlacementDataFactory(self._produce_ut_vim_accounts_info(TestNsPlacementDataFactory.vim_accounts), + self._produce_ut_vnf_price_list(), + nsd=nsd, + pil_info=None, pinning=None, + order_constraints=None) + + self.assertEqual(vld_desc_expected, nspdf._produce_vld_desc(), "vld_desc incorrect") + + def test__produce_vld_desc_single_vnf_nsd(self): + vld_desc_expected = [] + + nsd = self._get_ut_nsd_from_file('nsd_unittest4.yaml') + nsd = nsd['nsd:nsd-catalog']['nsd'][0] + nspdf = NsPlacementDataFactory(self._produce_ut_vim_accounts_info(TestNsPlacementDataFactory.vim_accounts), + self._produce_ut_vnf_price_list(), + nsd=nsd, + pil_info=None, pinning=None, + order_constraints=None) + + self.assertEqual(vld_desc_expected, nspdf._produce_vld_desc(), "vld_desc_incorrect") + + def test__produce_vld_desc(self): + """ + + :return: + """ + vld_desc_expected = [{'cp_refs': ['one', 'two'], 'latency': 150, 'jitter': 30}, + {'cp_refs': ['two', 'three'], 'latency': 90, 'jitter': 30}] + + nsd = self._get_ut_nsd_from_file('nsd_unittest3.yaml') + nsd = nsd['nsd:nsd-catalog']['nsd'][0] + nspdf = NsPlacementDataFactory(self._produce_ut_vim_accounts_info(TestNsPlacementDataFactory.vim_accounts), + self._produce_ut_vnf_price_list(), + nsd=nsd, + pil_info=None, pinning=None, + order_constraints=None) + + self.assertEqual(vld_desc_expected, nspdf._produce_vld_desc(), "vld_desc incorrect") + + def test__produce_ns_desc(self): + """ + ToDo + - price list sheet with more vims than associated with session + - price list sheet with fewer vims than associated with session + - nsd with different vndfd-id-refs + - fault case scenarios with non-existing vims, non-existing vnfds + """ + nsd = self._get_ut_nsd_from_file('nsd_unittest3.yaml') + nsd = nsd['nsd:nsd-catalog']['nsd'][0] + nspdf = NsPlacementDataFactory(self._produce_ut_vim_accounts_info(TestNsPlacementDataFactory.vim_accounts), + self._produce_ut_vnf_price_list(), + nsd=nsd, + pil_info=None, + pinning=None) + + ns_desc = nspdf._produce_ns_desc() + # check that all expected member-vnf-index are present + vnfs = [e['vnf_id'] for e in ns_desc] + self.assertEqual(Counter(['one', 'two', 'three']), Counter(vnfs), 'vnf_id invalid') + + expected_keys = ['vnf_id', 'vnf_price_per_vim'] + for e in ns_desc: + # check that vnf_price_per_vim has proper values + self.assertEqual(Counter([5, 10, 30, 30]), Counter(e['vnf_price_per_vim']), 'vnf_price_per_vim invalid') + # check that no pinning directives included + self.assertEqual(Counter(expected_keys), Counter(e.keys()), 'pinning directive misplaced') + + def test__produce_ns_desc_with_more_vims(self): + nsd = self._get_ut_nsd_from_file('nsd_unittest1.yaml') + nsd = nsd['nsd:nsd-catalog']['nsd'][0] + nspdf = NsPlacementDataFactory(self._produce_ut_vim_accounts_info(self.vim_accounts_more_vims), + self._produce_ut_vnf_test_price_list('vnf_price_list_more_vims.yaml'), + nsd=nsd, + pil_info=None, + pinning=None) + + ns_desc = nspdf._produce_ns_desc() + # check that all expected member-vnf-index are present + vnfs = [e['vnf_id'] for e in ns_desc] + self.assertEqual(Counter([1, 3, 2]), Counter(vnfs), 'vnf_id invalid') + + expected_keys = ['vnf_id', 'vnf_price_per_vim'] + for e in ns_desc: + # check that vnf_price_per_vim has proper values + self.assertEqual(Counter([5, 10, 30, 30, 3]), Counter(e['vnf_price_per_vim']), 'vnf_price_per_vim invalid') + # check that no pinning directives included + self.assertEqual(Counter(expected_keys), Counter(e.keys()), 'pinning directive misplaced') + + def test__produce_ns_desc_with_fewer_vims(self): + nsd = self._get_ut_nsd_from_file('nsd_unittest1.yaml') + nsd = nsd['nsd:nsd-catalog']['nsd'][0] + nspdf = NsPlacementDataFactory(self._produce_ut_vim_accounts_info(self.vim_accounts_fewer_vims), + self._produce_ut_vnf_price_list(), + nsd=nsd, + pil_info=None, + pinning=None) + + ns_desc = nspdf._produce_ns_desc() + # check that all expected member-vnf-index are present + vnfs = [e['vnf_id'] for e in ns_desc] + self.assertEqual(Counter([1, 3, 2]), Counter(vnfs), 'vnf_id invalid') + + expected_keys = ['vnf_id', 'vnf_price_per_vim'] + for e in ns_desc: + # check that vnf_price_per_vim has proper values + self.assertEqual(Counter([5, 10, 30]), Counter(e['vnf_price_per_vim']), 'vnf_price_per_vim invalid') + # check that no pinning directives included + self.assertEqual(Counter(expected_keys), Counter(e.keys()), 'pinning directive misplaced') + + def test__produce_ns_desc_w_pinning(self): + nsd = self._get_ut_nsd_from_file('nsd_unittest3.yaml') + nsd = nsd['nsd:nsd-catalog']['nsd'][0] + pinning = [{'member-vnf-index': 'two', 'vimAccountId': '331ffdec-44a8-4707-94a1-af7a292d9735'}] + nspdf = NsPlacementDataFactory(self._produce_ut_vim_accounts_info(TestNsPlacementDataFactory.vim_accounts), + self._produce_ut_vnf_price_list(), + nsd=nsd, + pil_info=None, + pinning=pinning) + ns_desc = nspdf._produce_ns_desc() + # check that all expected member-vnf-index are present + vnfs = [e['vnf_id'] for e in ns_desc] + self.assertEqual(Counter(['one', 'three', 'two']), Counter(vnfs), 'vnf_id invalid') + + for e in ns_desc: + # check that vnf_price_per_vim has proper values + self.assertEqual(Counter([5, 10, 30, 30]), Counter(e['vnf_price_per_vim']), 'vnf_price_per_vim invalid') + # check that member-vnf-index 2 is pinned correctly + if e['vnf_id'] == 'two': + self.assertTrue('vim_account' in e.keys(), 'missing pinning directive') + self.assertTrue(pinning[0]['vimAccountId'] == e['vim_account'][3:].replace('_', '-'), + 'invalid pinning vim-account') + else: + self.assertTrue('vim-account' not in e.keys(), 'pinning directive misplaced') + + @mock.patch.object(NsPlacementDataFactory, '_produce_trp_link_characteristics_data') + @mock.patch.object(NsPlacementDataFactory, '_produce_vld_desc') + @mock.patch.object(NsPlacementDataFactory, '_produce_ns_desc') + def test_create_ns_placement_data_wo_order(self, mock_prd_ns_desc, mock_prd_vld_desc, mock_prd_trp_link_char): + """ + :return: + """ + vim_accounts_expected = [v.replace('-', '_') for v in ['vim92b056a7-38f5-438d-b8ee-3f93b3531f87', + 'vim6618d412-d7fc-4eb0-a6f8-d2c258e0e900', + 'vim331ffdec-44a8-4707-94a1-af7a292d9735', + 'vimeda92f47-29b9-4007-9709-c1833dbfbe31']] + + nsd = self._get_ut_nsd_from_file('nsd_unittest3.yaml') + nsd = nsd['nsd:nsd-catalog']['nsd'][0] + nspdf = NsPlacementDataFactory(self._produce_ut_vim_accounts_info(TestNsPlacementDataFactory.vim_accounts), + self._produce_ut_vnf_price_list(), + nsd=nsd, + pil_info=self._populate_pil_info('pil_unittest1.yaml'), + pinning=None, + order_constraints=None) + nspd = nspdf.create_ns_placement_data() + self.assertEqual(Counter(vim_accounts_expected), Counter(nspd['vim_accounts']), + "vim_accounts incorrect") + # mock1.assert_called_once() Note for python > 3.5 + self.assertTrue(mock_prd_ns_desc.called, '_produce_ns_desc not called') + # mock2.assert_called_once() Note for python > 3.5 + self.assertTrue(mock_prd_vld_desc.called, ' _produce_vld_desc not called') + mock_prd_trp_link_char.assert_has_calls([call('pil_latency'), call('pil_jitter'), call('pil_price')]) + + regexps = [r"\{.*\}", r".*'file':.*mznplacement.py", r".*'time':.*datetime.datetime\(.*\)"] + generator_data = str(nspd['generator_data']) + for regex in regexps: + self.assertRegex(generator_data, regex, "generator data invalid") + + @mock.patch.object(NsPlacementDataFactory, '_produce_trp_link_characteristics_data') + @mock.patch.object(NsPlacementDataFactory, '_produce_vld_desc') + @mock.patch.object(NsPlacementDataFactory, '_produce_ns_desc') + def test_create_ns_placement_data_w_order(self, mock_prd_ns_desc, mock_prd_vld_desc, + mock_prd_trp_link_char): + """ + :return: + """ + vim_accounts_expected = [v.replace('-', '_') for v in ['vim92b056a7-38f5-438d-b8ee-3f93b3531f87', + 'vim6618d412-d7fc-4eb0-a6f8-d2c258e0e900', + 'vim331ffdec-44a8-4707-94a1-af7a292d9735', + 'vimeda92f47-29b9-4007-9709-c1833dbfbe31']] + + nsd = self._get_ut_nsd_from_file('nsd_unittest3.yaml') + nsd = nsd['nsd:nsd-catalog']['nsd'][0] + nspdf = NsPlacementDataFactory(self._produce_ut_vim_accounts_info(TestNsPlacementDataFactory.vim_accounts), + self._produce_ut_vnf_price_list(), + nsd=nsd, + pil_info=self._populate_pil_info('pil_unittest1.yaml'), + pinning=None, + order_constraints={ + 'vld-constraints': [{'id': 'three_vnf_constrained_nsd_vld1', + 'link-constraints': {'latency': 120, + 'jitter': 21}}, + {'id': 'three_vnf_constrained_nsd_vld2', + 'link-constraints': {'latency': 121, + 'jitter': 22}}]} + ) + nspd = nspdf.create_ns_placement_data() + self.assertEqual(Counter(vim_accounts_expected), Counter(nspd['vim_accounts']), + "vim_accounts incorrect") + # mock1.assert_called_once() Note for python > 3.5 + self.assertTrue(mock_prd_ns_desc.called, '_produce_ns_desc not called') + # mock2.assert_called_once() Note for python > 3.5 + self.assertTrue(mock_prd_vld_desc.called, ' _produce_vld_desc not called') + mock_prd_trp_link_char.assert_has_calls([call('pil_latency'), call('pil_jitter'), call('pil_price')]) + + regexps = [r"\{.*\}", r".*'file':.*mznplacement.py", r".*'time':.*datetime.datetime\(.*\)"] + generator_data = str(nspd['generator_data']) + for regex in regexps: + self.assertRegex(generator_data, regex, "generator data invalid") + + +if __name__ == "__main__": + if __name__ == '__main__': + unittest.main() diff --git a/osm_pla/test/test_server.py b/osm_pla/test/test_server.py new file mode 100644 index 0000000..56cf212 --- /dev/null +++ b/osm_pla/test/test_server.py @@ -0,0 +1,517 @@ +# Copyright 2020 ArctosLabs Scandinavia AB +# +# 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 asyncio +# import platform +# import random +import os +import sys +# import unittest +from unittest import TestCase, mock +from unittest.mock import Mock + +# import pkg_resources +import yaml + +from osm_pla.placement.mznplacement import NsPlacementDataFactory, MznPlacementConductor +from pathlib import Path + +# need to Mock the imports from osm_common made in Server and Config beforehand +sys.modules['osm_common'] = Mock() +from osm_pla.server.server import Server # noqa: E402 +from osm_pla.config.config import Config # noqa: E402 + +nslcmop_record_wo_pinning = {'statusEnteredTime': 1574625718.8280587, 'startTime': 1574625718.8280587, + '_admin': {'created': 1574625718.8286533, + 'projects_write': ['61e4bbab-9659-4abc-a01d-ba3a307becf9'], + 'worker': 'e5121e773e8b', 'modified': 1574625718.8286533, + 'projects_read': ['61e4bbab-9659-4abc-a01d-ba3a307becf9']}, + 'operationState': 'PROCESSING', 'nsInstanceId': '45f588bd-5bf4-4181-b13b-f16a55a23be4', + 'lcmOperationType': 'instantiate', 'isCancelPending': False, + 'id': 'a571b1de-19e5-48bd-b252-ba0ad7d540c9', + '_id': 'a571b1de-19e5-48bd-b252-ba0ad7d540c9', + 'isAutomaticInvocation': False, + 'links': {'nsInstance': '/osm/nslcm/v1/ns_instances/45f588bd-5bf4-4181-b13b-f16a55a23be4', + 'self': '/osm/nslcm/v1/ns_lcm_op_occs/a571b1de-19e5-48bd-b252-ba0ad7d540c9'}, + 'operationParams': {'vimAccountId': 'eb553051-5b6c-4ad6-939b-2ad23bd82e57', + 'lcmOperationType': 'instantiate', 'nsDescription': 'just a test', + 'nsdId': '0f4e658f-62a6-4f73-8623-270e8f0a18bc', + 'nsName': 'ThreeNsd plain placement', 'ssh_keys': [], + 'validVimAccounts': ['eb553051-5b6c-4ad6-939b-2ad23bd82e57', + '576bbe0a-b95d-4ced-a63e-f387f8e6e2ce', + '3d1ffc5d-b36d-4f69-8356-7f59c740ca2f', + 'db54dcd4-9fc4-441c-8820-17bce0aef2c3'], + 'nsr_id': '45f588bd-5bf4-4181-b13b-f16a55a23be4', + 'placement-engine': 'PLA', + 'nsInstanceId': '45f588bd-5bf4-4181-b13b-f16a55a23be4'}} + +nslcmop_record_w_pinning = {'statusEnteredTime': 1574627411.420499, 'startTime': 1574627411.420499, + '_admin': {'created': 1574627411.4209971, + 'projects_write': ['61e4bbab-9659-4abc-a01d-ba3a307becf9'], + 'worker': 'e5121e773e8b', 'modified': 1574627411.4209971, + 'projects_read': ['61e4bbab-9659-4abc-a01d-ba3a307becf9']}, + 'operationState': 'PROCESSING', + 'nsInstanceId': '61587478-ea25-44eb-9f13-7005046ddb08', 'lcmOperationType': 'instantiate', + 'isCancelPending': False, 'id': '80f95a17-6fa7-408d-930f-40aa4589d074', + '_id': '80f95a17-6fa7-408d-930f-40aa4589d074', + 'isAutomaticInvocation': False, + 'links': { + 'nsInstance': '/osm/nslcm/v1/ns_instances/61587478-ea25-44eb-9f13-7005046ddb08', + 'self': '/osm/nslcm/v1/ns_lcm_op_occs/80f95a17-6fa7-408d-930f-40aa4589d074'}, + 'operationParams': { + 'vimAccountId': '576bbe0a-b95d-4ced-a63e-f387f8e6e2ce', + 'nsr_id': '61587478-ea25-44eb-9f13-7005046ddb08', + 'nsDescription': 'default description', 'nsdId': '0f4e658f-62a6-4f73-8623-270e8f0a18bc', + 'validVimAccounts': [ + 'eb553051-5b6c-4ad6-939b-2ad23bd82e57', '576bbe0a-b95d-4ced-a63e-f387f8e6e2ce', + '3d1ffc5d-b36d-4f69-8356-7f59c740ca2f', + 'db54dcd4-9fc4-441c-8820-17bce0aef2c3'], 'nsName': 'ThreeVnfTest2', + 'wimAccountId': False, 'vnf': [ + {'vimAccountId': '3d1ffc5d-b36d-4f69-8356-7f59c740ca2f', 'member-vnf-index': '1'}], + 'placementEngine': 'PLA', + 'nsInstanceId': '61587478-ea25-44eb-9f13-7005046ddb08', + 'lcmOperationType': 'instantiate'}} + +nslcmop_record_w_pinning_and_order_constraints = { + 'links': {'nsInstance': '/osm/nslcm/v1/ns_instances/7c4c3d94-ebb2-44e8-b236-d876b118434e', + 'self': '/osm/nslcm/v1/ns_lcm_op_occs/fd7c9e15-38aa-4fc5-913c-417b26859fb0'}, + 'id': 'fd7c9e15-38aa-4fc5-913c-417b26859fb0', 'operationState': 'PROCESSING', 'isAutomaticInvocation': False, + 'nsInstanceId': '7c4c3d94-ebb2-44e8-b236-d876b118434e', '_id': 'fd7c9e15-38aa-4fc5-913c-417b26859fb0', + 'isCancelPending': False, 'startTime': 1574772631.6932728, 'statusEnteredTime': 1574772631.6932728, + 'lcmOperationType': 'instantiate', + 'operationParams': {'placementEngine': 'PLA', + 'placement-constraints': { + 'vld-constraints': [{ + 'id': 'three_vnf_constrained_vld_1', + 'link-constraints': { + 'latency': 120, + 'jitter': 20}}, + { + 'link_constraints': { + 'latency': 120, + 'jitter': 20}, + 'id': 'three_vnf_constrained_nsd_vld_2'}]}, + 'nsName': 'ThreeVnfTest2', + 'nsDescription': 'default description', + 'nsr_id': '7c4c3d94-ebb2-44e8-b236-d876b118434e', + 'nsdId': '0f4e658f-62a6-4f73-8623-270e8f0a18bc', + 'validVimAccounts': ['eb553051-5b6c-4ad6-939b-2ad23bd82e57', + '576bbe0a-b95d-4ced-a63e-f387f8e6e2ce', + '3d1ffc5d-b36d-4f69-8356-7f59c740ca2f', + 'db54dcd4-9fc4-441c-8820-17bce0aef2c3'], + 'wimAccountId': False, + 'vnf': [{'member-vnf-index': '3', 'vimAccountId': '3d1ffc5d-b36d-4f69-8356-7f59c740ca2f'}], + 'nsInstanceId': '7c4c3d94-ebb2-44e8-b236-d876b118434e', + 'lcmOperationType': 'instantiate', + 'vimAccountId': '576bbe0a-b95d-4ced-a63e-f387f8e6e2ce'}, + '_admin': {'projects_read': ['61e4bbab-9659-4abc-a01d-ba3a307becf9'], 'modified': 1574772631.693885, + 'projects_write': ['61e4bbab-9659-4abc-a01d-ba3a307becf9'], 'created': 1574772631.693885, + 'worker': 'e5121e773e8b'}} + +list_of_vims = [{"_id": "73cd1a1b-333e-4e29-8db2-00d23bd9b644", "vim_user": "admin", "name": "OpenStack1", + "vim_url": "http://10.234.12.47:5000/v3", "vim_type": "openstack", "vim_tenant_name": "osm_demo", + "vim_password": "O/mHomfXPmCrTvUbYXVoyg==", "schema_version": "1.1", + "_admin": {"modified": 1565597984.3155663, + "deployed": {"RO": "f0c1b516-bcd9-11e9-bb73-02420aff0030", + "RO-account": "f0d45496-bcd9-11e9-bb73-02420aff0030"}, + "projects_write": ["admin"], "operationalState": "ENABLED", "detailed-status": "Done", + "created": 1565597984.3155663, "projects_read": ["admin"]}, + "config": {}}, + {"_id": "684165ea-2cf9-4fbd-ac22-8464ca07d1d8", "vim_user": "admin", + "name": "OpenStack2", "vim_url": "http://10.234.12.44:5000/v3", + "vim_tenant_name": "osm_demo", "vim_password": "Rw7gln9liP4ClMyHd5OFsw==", + "description": "Openstack on NUC", "vim_type": "openstack", + "admin": {"modified": 1566474766.7288046, + "deployed": {"RO": "5bc59656-c4d3-11e9-b1e5-02420aff0006", + "RO-account": "5bd772e0-c4d3-11e9-b1e5-02420aff0006"}, + "projects_write": ["admin"], "operationalState": "ENABLED", + "detailed-status": "Done", "created": 1566474766.7288046, + "projects_read": ["admin"]}, + "config": {}, "schema_version": "1.1"}, + {"_id": "8460b670-31cf-4fae-9f3e-d0dd6c57b61e", "vim_user": "admin", "name": "OpenStack1", + "vim_url": "http://10.234.12.47:5000/v3", "vim_type": "openstack", + "vim_tenant_name": "osm_demo", "vim_password": "NsgJJDlCdKreX30FQFNz7A==", + "description": "Openstack on Dell", + "_admin": {"modified": 1566992449.5942867, + "deployed": {"RO": "aed94f86-c988-11e9-bb38-02420aff0088", + "RO-account": "aee72fac-c988-11e9-bb38-02420aff0088"}, + "projects_write": ["0a5d0c5b-7e08-48a1-a686-642a038bbd70"], + "operationalState": "ENABLED", "detailed-status": "Done", "created": 1566992449.5942867, + "projects_read": ["0a5d0c5b-7e08-48a1-a686-642a038bbd70"]}, "config": {}, + "schema_version": "1.1"}, + {"_id": "9b8b5268-acb7-4893-b494-a77656b418f2", + "vim_user": "admin", "name": "OpenStack2", + "vim_url": "http://10.234.12.44:5000/v3", + "vim_type": "openstack", "vim_tenant_name": "osm_demo", + "vim_password": "AnAV3xtoiwwdnAfv0KahSw==", + "description": "Openstack on NUC", + "_admin": {"modified": 1566992484.9190753, + "deployed": {"RO": "c3d61158-c988-11e9-bb38-02420aff0088", + "RO-account": "c3ec973e-c988-11e9-bb38-02420aff0088"}, + "projects_write": ["0a5d0c5b-7e08-48a1-a686-642a038bbd70"], + "operationalState": "ENABLED", "detailed-status": "Done", + "created": 1566992484.9190753, + "projects_read": ["0a5d0c5b-7e08-48a1-a686-642a038bbd70"]}, + "config": {}, "schema_version": "1.1"}, + {"_id": "3645f215-f32d-4355-b5ab-df0a2e2233c3", "vim_user": "admin", "name": "OpenStack3", + "vim_url": "http://10.234.12.46:5000/v3", "vim_tenant_name": "osm_demo", + "vim_password": "XkG2w8e8/DiuohCFNp0+lQ==", "description": "Openstack on NUC", + "vim_type": "openstack", + "_admin": {"modified": 1567421247.7016313, + "deployed": {"RO": "0e80f6a2-cd6f-11e9-bb50-02420aff00b6", + "RO-account": "0e974524-cd6f-11e9-bb50-02420aff00b6"}, + "projects_write": ["0a5d0c5b-7e08-48a1-a686-642a038bbd70"], + "operationalState": "ENABLED", "detailed-status": "Done", + "created": 1567421247.7016313, + "projects_read": ["0a5d0c5b-7e08-48a1-a686-642a038bbd70"]}, + "schema_version": "1.1", "config": {}}, + {"_id": "53f8f2bb-88b5-4bf9-babf-556698b5261f", + "vim_user": "admin", "name": "OpenStack4", + "vim_url": "http://10.234.12.43:5000/v3", + "vim_tenant_name": "osm_demo", + "vim_password": "GLrgVn8fMVneXMZq1r4yVA==", + "description": "Openstack on NUC", + "vim_type": "openstack", + "_admin": {"modified": 1567421296.1576457, + "deployed": { + "RO": "2b43c756-cd6f-11e9-bb50-02420aff00b6", + "RO-account": "2b535aea-cd6f-11e9-bb50-02420aff00b6"}, + "projects_write": [ + "0a5d0c5b-7e08-48a1-a686-642a038bbd70"], + "operationalState": "ENABLED", + "detailed-status": "Done", + "created": 1567421296.1576457, + "projects_read": [ + "0a5d0c5b-7e08-48a1-a686-642a038bbd70"]}, + "schema_version": "1.1", "config": {}}] + +# FIXME this is not correct re mgmt-network setting. +nsd_from_db = {"_id": "15fc1941-f095-4cd8-af2d-1000bd6d9eaa", "short-name": "three_vnf_constrained_nsd_low", + "name": "three_vnf_constrained_nsd_low", "version": "1.0", + "description": "Placement constraints NSD", + "_admin": {"modified": 1567672251.7531693, + "storage": {"pkg-dir": "ns_constrained_nsd", "fs": "local", + "descriptor": "ns_constrained_nsd/ns_constrained_nsd.yaml", + "zipfile": "package.tar.gz", + "folder": "15fc1941-f095-4cd8-af2d-1000bd6d9eaa", "path": "/app/storage/"}, + "onboardingState": "ONBOARDED", "usageState": "NOT_IN_USE", + "projects_write": ["0a5d0c5b-7e08-48a1-a686-642a038bbd70"], "operationalState": "ENABLED", + "userDefinedData": {}, "created": 1567672251.7531693, + "projects_read": ["0a5d0c5b-7e08-48a1-a686-642a038bbd70"]}, + "constituent-vnfd": [{"vnfd-id-ref": "cirros_vnfd_v2", "member-vnf-index": "one"}, + {"vnfd-id-ref": "cirros_vnfd_v2", "member-vnf-index": "two"}, + {"vnfd-id-ref": "cirros_vnfd_v2", "member-vnf-index": "three"}], + "id": "three_vnf_constrained_nsd_low", "vendor": "ArctosLabs", + "vld": [{"type": "ELAN", "short-name": "ns_constrained_nsd_low_vld1", + "link-constraint": [{"constraint-type": "LATENCY", "value": "100"}, + {"constraint-type": "JITTER", "value": "30"}], + "vim-network-name": "external", "mgmt-network": True, + "id": "three_vnf_constrained_nsd_low_vld1", + "vnfd-connection-point-ref": [ + {"vnfd-id-ref": "cirros_vnfd_v2", "member-vnf-index-ref": "one", + "vnfd-connection-point-ref": "vnf-cp0"}, + {"vnfd-id-ref": "cirros_vnfd_v2", + "member-vnf-index-ref": "two", + "vnfd-connection-point-ref": "vnf-cp0"}], + "name": "ns_constrained_nsd_vld1"}, + {"type": "ELAN", "short-name": "ns_constrained_nsd_low_vld2", + "link-constraint": [{"constraint-type": "LATENCY", "value": "50"}, + {"constraint-type": "JITTER", "value": "30"}], + "vim-network-name": "lanretxe", "mgmt-network": True, + "id": "three_vnf_constrained_nsd_low_vld2", + "vnfd-connection-point-ref": [ + {"vnfd-id-ref": "cirros_vnfd_v2", "member-vnf-index-ref": "two", + "vnfd-connection-point-ref": "vnf-cp0"}, + {"vnfd-id-ref": "cirros_vnfd_v2", "member-vnf-index-ref": "three", + "vnfd-connection-point-ref": "vnf-cp0"}], + "name": "ns_constrained_nsd_vld2"}]} + + +###################################################### +# These are helper functions to handle unittest of asyncio. +# Inspired by: https://blog.miguelgrinberg.com/post/unit-testing-asyncio-code +def _run(co_routine): + return asyncio.get_event_loop().run_until_complete(co_routine) + + +def _async_mock(*args, **kwargs): + m = mock.MagicMock(*args, **kwargs) + + async def mock_coro(*args, **kwargs): + return m(*args, **kwargs) + + mock_coro.mock = m + return mock_coro + + +###################################################### + +class TestServer(TestCase): + + def _produce_ut_vim_accounts_info(self, list_of_vims): + """ + FIXME temporary, we will need more control over vim_urls and _id for test purpose - make a generator + :return: vim_url and _id as dict, i.e. extract these from vim_accounts data + """ + return {_['vim_url']: _['_id'] for _ in list_of_vims} + + def _produce_ut_vnf_price_list(self): + price_list_file = "vnf_price_list.yaml" + with open(str(Path(price_list_file))) as pl_fd: + price_list_data = yaml.safe_load_all(pl_fd) + return {i['vnfd']: {i1['vim_url']: i1['price'] for i1 in i['prices']} for i in next(price_list_data)} + + def _populate_pil_info(self, file): + """ + FIXME we need more control over content in pil information - more files or generator and data + Note str(Path()) is a 3.5 thing + """ + with open(str(Path(file))) as pp_fd: + test_data = yaml.safe_load_all(pp_fd) + return next(test_data) + + @mock.patch.object(Config, '_read_config_file') + @mock.patch.object(Config, 'get', side_effect=['doesnotmatter', 'memory', 'memory', 'local', 'doesnotmatter']) + def serverSetup(self, mock_get, mock__read_config_file): + """ + Helper that returns a Server object + :return: + """ + cfg = Config(None) + return Server(cfg) + + def _adjust_path(self, file): + """In case we are not running from test directory, + then assume we are in top level directory (e.g. running from tox) and adjust file path accordingly""" + path_component = '/osm_pla/test/' + real_path = os.path.realpath(file) + if path_component not in real_path: + return os.path.dirname(real_path) + path_component + os.path.basename(real_path) + else: + return real_path + + def test__get_nslcmop(self): + server = self.serverSetup() + server.db = Mock() + _ = server._get_nslcmop(nslcmop_record_wo_pinning["id"]) + server.db.get_one.assert_called_with("nslcmops", {'_id': nslcmop_record_wo_pinning["id"]}) + + def test__get_nsd(self): # OK + server = self.serverSetup() + server.db = Mock() + _ = server._get_nsd(nslcmop_record_wo_pinning['operationParams']['nsdId']) + server.db.get_one.assert_called_with("nsds", {'_id': nslcmop_record_wo_pinning['operationParams']['nsdId']}) + + def test__get_vim_accounts(self): # OK + server = self.serverSetup() + server.db = Mock() + _ = server._get_vim_accounts(nslcmop_record_wo_pinning['operationParams']['validVimAccounts']) + server.db.get_list.assert_called_with('vim_accounts', + {'_id': nslcmop_record_wo_pinning['operationParams']['validVimAccounts']}) + + def test__get_vnf_price_list(self): + server = self.serverSetup() + pl = server._get_vnf_price_list(Path(self._adjust_path('./vnf_price_list.yaml'))) + self.assertIs(type(pl), dict, "price list not a dictionary") + for k, v in pl.items(): + self.assertIs(type(v), dict, "price list values not a dict") + + def test__get_pil_info(self): + server = self.serverSetup() + ppi = server._get_pil_info(Path(self._adjust_path('./pil_price_list.yaml'))) + self.assertIs(type(ppi), dict, "pil is not a dict") + self.assertIn('pil', ppi.keys(), "pil has no pil key") + self.assertIs(type(ppi['pil']), list, "pil does not contain a list") + # check for expected keys + expected_keys = {'pil_description', 'pil_price', 'pil_latency', 'pil_jitter', 'pil_endpoints'} + self.assertEqual(expected_keys, ppi['pil'][0].keys(), 'expected keys not found') + + def test_handle_kafka_command(self): # OK + server = self.serverSetup() + server.loop.create_task = Mock() + server.handle_kafka_command('pli', 'get_placement', {}) + server.loop.create_task.assert_not_called() + server.loop.create_task.reset_mock() + server.handle_kafka_command('pla', 'get_placement', {'nslcmopId': nslcmop_record_wo_pinning["id"]}) + self.assertTrue(server.loop.create_task.called, 'create_task not called') + args, kwargs = server.loop.create_task.call_args + self.assertIn('Server.get_placement', str(args[0]), 'get_placement not called') + + @mock.patch.object(NsPlacementDataFactory, '__init__', lambda x0, x1, x2, x3, x4, x5, x6: None) + @mock.patch.object(MznPlacementConductor, 'do_placement_computation') + @mock.patch.object(NsPlacementDataFactory, 'create_ns_placement_data') + @mock.patch.object(Server, '_get_vim_accounts') + @mock.patch.object(Server, '_get_nsd') + @mock.patch.object(Server, '_get_nslcmop') + @mock.patch.object(Server, '_get_vnf_price_list') + @mock.patch.object(Server, '_get_pil_info') + def test_get_placement(self, mock_get_pil_info, mock_get_vnf_price_list, mock__get_nslcmop, mock__get_nsd, + mock__get_vim_accounts, + mock_create_ns_placement_data, + mock_do_placement_computation): + """ + run _get_placement and check that things get called as expected + :return: + """ + placement_ret_val = [{'vimAccountId': 'bbbbbbbb-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': 'one'}, + {'vimAccountId': 'aaaaaaaa-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': 'two'}, + {'vimAccountId': 'aaaaaaaa-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': 'three'}] + server = self.serverSetup() + + server.msgBus.aiowrite = _async_mock() + mock__get_nsd.return_value = nsd_from_db + mock__get_vim_accounts.return_value = list_of_vims + + # FIXME need update to match nslcmop, not for test but for consistency + mock_do_placement_computation.return_value = placement_ret_val + _run(server.get_placement(nslcmop_record_wo_pinning['id'])) + + self.assertTrue(mock_get_vnf_price_list.called, '_get_vnf_price_list not called as expected') + self.assertTrue(mock_get_pil_info.called, '_get_pil_info not called as expected') + self.assertTrue(mock__get_nslcmop.called, '_get_nslcmop not called as expected') + # mock_get_nsd.assert_called_once() assert_called_once() for python > 3.5 + self.assertTrue(mock__get_nsd.called, 'get_nsd not called as expected') + # mock_get_enabled_vims.assert_called_once() assert_called_once() for python > 3.5 + self.assertTrue(mock__get_vim_accounts.called, 'get_vim_accounts not called as expected') + # mock_create_ns_placement_data.assert_called_once() assert_called_once() for python > 3.5 + self.assertTrue(mock_create_ns_placement_data.called, 'create_ns_placement_data not called as expected') + # mock_do_placement_computation.assert_called_once() assert_called_once() for python > 3.5 + self.assertTrue(mock_do_placement_computation.called, 'do_placement_computation not called as expected') + self.assertTrue(server.msgBus.aiowrite.mock.called) + + args, kwargs = server.msgBus.aiowrite.mock.call_args + self.assertTrue(len(args) == 3, 'invalid format') + self.assertEqual('pla', args[0], 'topic invalid') + self.assertEqual('placement', args[1], 'message invalid') + # extract placement result and check content + rsp_payload = args[2] + + expected_rsp_keys = {'placement'} + self.assertEqual(expected_rsp_keys, set(rsp_payload.keys()), "placement response missing keys") + self.assertIs(type(rsp_payload['placement']), dict, 'placement not a dict') + + expected_placement_keys = {'vnf', 'nslcmopId'} + self.assertEqual(expected_placement_keys, set(rsp_payload['placement']), "placement keys invalid") + + vim_account_candidates = [e['vimAccountId'] for e in placement_ret_val] + + self.assertEqual(nslcmop_record_wo_pinning['id'], rsp_payload['placement']['nslcmopId'], "nslcmopId invalid") + + self.assertIs(type(rsp_payload['placement']['vnf']), list, 'vnf not a list') + expected_vnf_keys = {'vimAccountId', 'member-vnf-index'} + self.assertEqual(expected_vnf_keys, set(rsp_payload['placement']['vnf'][0]), "placement['vnf'] missing keys") + self.assertIn(rsp_payload['placement']['vnf'][0]['vimAccountId'], vim_account_candidates, + "vimAccountId invalid") + + @mock.patch.object(NsPlacementDataFactory, '__init__', lambda x0, x1, x2, x3, x4, x5, x6: None) + @mock.patch.object(MznPlacementConductor, 'do_placement_computation') + @mock.patch.object(NsPlacementDataFactory, 'create_ns_placement_data') + @mock.patch.object(Server, '_get_vim_accounts') + @mock.patch.object(Server, '_get_nsd') + @mock.patch.object(Server, '_get_nslcmop') + @mock.patch.object(Server, '_get_vnf_price_list') + @mock.patch.object(Server, '_get_pil_info') + def test_get_placement_with_pinning(self, mock_get_pil_info, mock_get_vnf_price_list, mock__get_nslcmop, + mock__get_nsd, mock__get_vim_accounts, + mock_create_ns_placement_data, + mock_do_placement_computation): + """ + run _get_placement and check that things get called as expected + :return: + """ + placement_ret_val = [{'vimAccountId': 'bbbbbbbb-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': 'one'}, + {'vimAccountId': 'aaaaaaaa-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': 'two'}, + {'vimAccountId': 'aaaaaaaa-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': 'three'}] + server = self.serverSetup() + + server.msgBus.aiowrite = _async_mock() + mock__get_nsd.return_value = nsd_from_db + mock__get_vim_accounts.return_value = list_of_vims + + # FIXME need update to match nslcmop, not for test but for consistency + mock_do_placement_computation.return_value = placement_ret_val + _run(server.get_placement(nslcmop_record_w_pinning['id'])) + + self.assertTrue(mock_get_vnf_price_list.called, '_get_vnf_price_list not called as expected') + self.assertTrue(mock_get_pil_info.called, '_get_pil_info not called as expected') + self.assertTrue(mock__get_nslcmop.called, '_get_nslcmop not called as expected') + # mock_get_nsd.assert_called_once() assert_called_once() for python > 3.5 + self.assertTrue(mock__get_nsd.called, 'get_nsd not called as expected') + # mock_get_enabled_vims.assert_called_once() assert_called_once() for python > 3.5 + self.assertTrue(mock__get_vim_accounts.called, 'get_vim_accounts not called as expected') + # mock_create_ns_placement_data.assert_called_once() assert_called_once() for python > 3.5 + self.assertTrue(mock_create_ns_placement_data.called, 'create_ns_placement_data not called as expected') + # mock_do_placement_computation.assert_called_once() assert_called_once() for python > 3.5 + self.assertTrue(mock_do_placement_computation.called, 'do_placement_computation not called as expected') + self.assertTrue(server.msgBus.aiowrite.mock.called) + + args, kwargs = server.msgBus.aiowrite.mock.call_args + self.assertTrue(len(args) == 3, 'invalid format') + self.assertEqual('pla', args[0], 'topic invalid') + self.assertEqual('placement', args[1], 'message invalid') + # extract placement result and check content + rsp_payload = args[2] + + expected_rsp_keys = {'placement'} + self.assertEqual(expected_rsp_keys, set(rsp_payload.keys()), "placement response missing keys") + self.assertIs(type(rsp_payload['placement']), dict, 'placement not a dict') + + expected_placement_keys = {'vnf', 'nslcmopId'} + self.assertEqual(expected_placement_keys, set(rsp_payload['placement']), "placement keys invalid") + + vim_account_candidates = [e['vimAccountId'] for e in placement_ret_val] + + self.assertEqual(nslcmop_record_w_pinning['id'], rsp_payload['placement']['nslcmopId'], "nslcmopId invalid") + + self.assertIs(type(rsp_payload['placement']['vnf']), list, 'vnf not a list') + expected_vnf_keys = {'vimAccountId', 'member-vnf-index'} + self.assertEqual(expected_vnf_keys, set(rsp_payload['placement']['vnf'][0]), "placement['vnf'] missing keys") + self.assertIn(rsp_payload['placement']['vnf'][0]['vimAccountId'], vim_account_candidates, + "vimAccountId invalid") + + # Note: does not mock reading of price list and pil_info + @mock.patch.object(NsPlacementDataFactory, '__init__', lambda x0, x1, x2, x3, x4, x5: None) + @mock.patch.object(MznPlacementConductor, 'do_placement_computation') + @mock.patch.object(NsPlacementDataFactory, 'create_ns_placement_data') + @mock.patch.object(Server, '_get_vim_accounts') + @mock.patch.object(Server, '_get_nsd') + @mock.patch.object(Server, '_get_nslcmop') + def test_get_placement_w_exception(self, mock__get_nslcmop, + mock__get_nsd, + mock__get_vim_accounts, + mock_create_ns_placement_data, + mock_do_placement_computation): + """ + check that raised exceptions are handled and response provided accordingly + """ + server = self.serverSetup() + + server.msgBus.aiowrite = _async_mock() + mock__get_nsd.return_value = nsd_from_db + mock__get_nsd.side_effect = RuntimeError('kaboom!') + mock__get_vim_accounts.return_value = list_of_vims + mock_do_placement_computation.return_value = \ + [{'vimAccountId': 'bbbbbbbb-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '1'}, + {'vimAccountId': 'aaaaaaaa-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '2'}, + {'vimAccountId': 'aaaaaaaa-38f5-438d-b8ee-3f93b3531f87', 'member-vnf-index': '3'}] + + _run(server.get_placement(nslcmop_record_w_pinning['id'])) + self.assertTrue(server.msgBus.aiowrite.mock.called) + args, kwargs = server.msgBus.aiowrite.mock.call_args + rsp_payload = args[2] + expected_keys = {'placement'} + self.assertEqual(expected_keys, set(rsp_payload.keys()), "placement response missing keys") + self.assertIs(type(rsp_payload['placement']['vnf']), list, 'vnf not a list') + self.assertEqual([], rsp_payload['placement']['vnf'], 'vnf list not empty') + self.assertEqual(nslcmop_record_w_pinning['id'], rsp_payload['placement']['nslcmopId'], "nslcmopId invalid") diff --git a/osm_pla/test/vnf_price_list.yaml b/osm_pla/test/vnf_price_list.yaml new file mode 100644 index 0000000..9f02045 --- /dev/null +++ b/osm_pla/test/vnf_price_list.yaml @@ -0,0 +1,84 @@ +# Copyright 2020 ArctosLabs Scandinavia AB +# +# 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. +- vnfd: cirros_vnfd_v2 + prices: + - vim_url: http://10.234.12.47:5000/v3 + vim_name: OpenStack1 + price: 5 + - vim_url: http://10.234.12.44:5000/v3 + vim_name: OpenStack2 + price: 10 + - vim_url: http://10.234.12.46:5000/v3 + vim_name: OpenStack3 + price: 30 + - vim_url: http://10.234.12.43:5000/v3 + vim_name: OpenStack4 + price: 30 +- vnfd: hackfest_multivdu-vnf + prices: + - vim_url: http://10.234.12.47:5000/v3 + vim_name: OpenStack1 + price: 17 + - vim_url: http://10.234.12.44:5000/v3 + vim_name: OpenStack2 + price: 18 + - vim_url: http://10.234.12.46:5000/v3 + vim_name: OpenStack3 + price: 19 + - vim_url: http://10.234.12.43:5000/v3 + vim_name: OpenStack4 + price: 20 +- vnfd: test_one_a_vnfd + prices: + - vim_url: http://10.234.12.47:5000/v3 + vim_name: OpenStack1 + price: 10 + - vim_url: http://10.234.12.44:5000/v3 + vim_name: OpenStack2 + price: 20 + - vim_url: http://10.234.12.46:5000/v3 + vim_name: OpenStack3 + price: 30 + - vim_url: http://10.234.12.43:5000/v3 + vim_name: OpenStack4 + price: 30 +- vnfd: test_one_b_vnfd + prices: + - vim_url: http://10.234.12.47:5000/v3 + vim_name: OpenStack1 + price: 10 + - vim_url: http://10.234.12.44:5000/v3 + vim_name: OpenStack2 + price: 20 + - vim_url: http://10.234.12.46:5000/v3 + vim_name: OpenStack3 + price: 30 + - vim_url: http://10.234.12.43:5000/v3 + vim_name: OpenStack4 + price: 30 +- vnfd: test_one_c_vnfd + prices: + - vim_url: http://10.234.12.47:5000/v3 + vim_name: OpenStack1 + price: 10 + - vim_url: http://10.234.12.44:5000/v3 + vim_name: OpenStack2 + price: 20 + - vim_url: http://10.234.12.46:5000/v3 + vim_name: OpenStack3 + price: 30 + - vim_url: http://10.234.12.43:5000/v3 + vim_name: OpenStack4 + price: 30 diff --git a/osm_pla/test/vnf_price_list_more_vims.yaml b/osm_pla/test/vnf_price_list_more_vims.yaml new file mode 100644 index 0000000..d4ede2d --- /dev/null +++ b/osm_pla/test/vnf_price_list_more_vims.yaml @@ -0,0 +1,46 @@ +# Copyright 2020 ArctosLabs Scandinavia AB +# +# 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. +- vnfd: cirros_vnfd_v2 + prices: + - vim_url: http://10.234.12.47:5000/v3 + vim_name: OpenStack1 + price: 5 + - vim_url: http://10.234.12.44:5000/v3 + vim_name: OpenStack2 + price: 10 + - vim_url: http://10.234.12.46:5000/v3 + vim_name: OpenStack3 + price: 30 + - vim_url: http://10.234.12.43:5000/v3 + vim_name: OpenStack4 + price: 30 + - vim_url: http://1.1.1.1:5000/v3 + vim_name: OpenStack5 + price: 3 + +- vnfd: hackfest_multivdu-vnf + prices: + - vim_url: http://10.234.12.47:5000/v3 + vim_name: OpenStack1 + price: 17 + - vim_url: http://10.234.12.44:5000/v3 + vim_name: OpenStack2 + price: 18 + - vim_url: http://10.234.12.46:5000/v3 + vim_name: OpenStack3 + price: 19 + - vim_url: http://10.234.12.43:5000/v3 + vim_name: OpenStack4 + price: 20 \ No newline at end of file diff --git a/osm_pla/test/vnf_price_list_rel7_webinar.yaml b/osm_pla/test/vnf_price_list_rel7_webinar.yaml new file mode 100644 index 0000000..e94930f --- /dev/null +++ b/osm_pla/test/vnf_price_list_rel7_webinar.yaml @@ -0,0 +1,98 @@ +# Copyright 2020 ArctosLabs Scandinavia AB +# +# 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. +- vnfd: cirros_vnfd_v2 + prices: + - vim_url: http://10.234.12.47:5000/v3 + vim_name: OpenStack1 + price: 5 + - vim_url: http://10.234.12.44:5000/v3 + vim_name: OpenStack2 + price: 10 + - vim_url: http://10.234.12.46:5000/v3 + vim_name: OpenStack3 + price: 30 + - vim_url: http://10.234.12.43:5000/v3 + vim_name: OpenStack4 + price: 30 +- vnfd: hackfest_multivdu-vnf + prices: + - vim_url: http://10.234.12.47:5000/v3 + vim_name: OpenStack1 + price: 17 + - vim_url: http://10.234.12.44:5000/v3 + vim_name: OpenStack2 + price: 18 + - vim_url: http://10.234.12.46:5000/v3 + vim_name: OpenStack3 + price: 19 + - vim_url: http://10.234.12.43:5000/v3 + vim_name: OpenStack4 + price: 20 +- vnfd: test_one_a_vnfd + prices: + - vim_url: http://10.234.12.47:5000/v3 + vim_name: OpenStack1 + price: 10 + - vim_url: http://10.234.12.44:5000/v3 + vim_name: OpenStack2 + price: 20 + - vim_url: http://10.234.12.46:5000/v3 + vim_name: OpenStack3 + price: 50 + - vim_url: http://10.234.12.43:5000/v3 + vim_name: OpenStack4 + price: 50 +- vnfd: test_one_b_vnfd + prices: + - vim_url: http://10.234.12.47:5000/v3 + vim_name: OpenStack1 + price: 10 + - vim_url: http://10.234.12.44:5000/v3 + vim_name: OpenStack2 + price: 20 + - vim_url: http://10.234.12.46:5000/v3 + vim_name: OpenStack3 + price: 50 + - vim_url: http://10.234.12.43:5000/v3 + vim_name: OpenStack4 + price: 50 +- vnfd: test_one_c_vnfd + prices: + - vim_url: http://10.234.12.47:5000/v3 + vim_name: OpenStack1 + price: 10 + - vim_url: http://10.234.12.44:5000/v3 + vim_name: OpenStack2 + price: 20 + - vim_url: http://10.234.12.46:5000/v3 + vim_name: OpenStack3 + price: 50 + - vim_url: http://10.234.12.43:5000/v3 + vim_name: OpenStack4 + price: 50 +- vnfd: hackfest-basic_vnfd + prices: + - vim_url: http://10.234.12.47:5000/v3 + vim_name: OpenStack1 + price: 10 + - vim_url: http://10.234.12.44:5000/v3 + vim_name: OpenStack2 + price: 20 + - vim_url: http://10.234.12.46:5000/v3 + vim_name: OpenStack3 + price: 50 + - vim_url: http://10.234.12.43:5000/v3 + vim_name: OpenStack4 + price: 50 diff --git a/requirements.txt b/requirements.txt new file mode 100755 index 0000000..960f499 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,21 @@ +# Copyright 2020 ArctosLabs Scandinavia AB +# ************************************************************* + +# This file is part of OSM Placement module +# All Rights Reserved to ArctosLabs Scandinavia AB + +# 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. +pyyaml==5.1.2 +pymzn==0.18.* +jinja2==2.10.3 +git+https://osm.etsi.org/gerrit/osm/common.git#egg=osm-common \ No newline at end of file diff --git a/setup.cfg b/setup.cfg new file mode 100755 index 0000000..75ce737 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,38 @@ +# Copyright 2020 ArctosLabs Scandinavia AB +# +# 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. + +[metadata] +name = PLA +summary = Placement module for OSM. +description-file = + README.rst +author = OSM +home-page = https://osm.etsi.org/ +classifier = + Environment :: OSM + Intended Audience :: Information Technology + Intended Audience :: System Administrators + License :: ETSI Approved :: Apache Software License + Operating System :: POSIX :: Linux + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.5 + +[test] +test_suite=osm_pla.test + +[files] +packages = + pbr diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..3c80986 --- /dev/null +++ b/setup.py @@ -0,0 +1,65 @@ +# Copyright 2020 ArctosLabs Scandinavia AB +# +# 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 setuptools import setup + + +def parse_requirements(requirements): + with open(requirements) as f: + return [l.strip('\n') for l in f if l.strip('\n') and not l.startswith('#') and '://' not in l] + + +_name = 'osm_pla' +# _version_command = ('git describe --match v* --tags --long --dirty', 'pep440-git-full') FIXME we have no tags yet +_version = '0.0.1' # FIXME temporary workaround for _version_command +_description = 'OSM Placement Module' +_author = "Lars Goran Magnusson" +_author_email = 'lars-goran.magnusson@arctoslabs.com' +_maintainer = 'Lars Goran Magnusson' +_maintainer_email = 'lars-goran.magnusson@arctoslabs.com' +_license = 'Apache 2.0' +_url = 'https://osm.etsi.org/gitweb?p=osm/PLA.git;a=tree' + + +setup( + name=_name, + # version_command=_version_command, FIXME temporary fix + version=_version, + description=_description, + long_description=open('README.md', encoding='utf-8').read(), + author=_author, + author_email=_author_email, + maintainer=_maintainer, + maintainer_email=_maintainer_email, + url=_url, + license=_license, + packages=[_name], + package_dir={_name: _name}, + install_requires=[ + 'osm-common', + 'jinja2==2.10.3', + 'pymzn==0.18.*', + 'pyyaml==5.1.2' + ], + dependency_links=[ + 'git+https://osm.etsi.org/gerrit/osm/common.git#egg=osm-common', + ], + include_package_data=True, + entry_points={ + "console_scripts": [ + "osm-pla-server = osm_pla.cmd.pla_server:main", + ] + }, + setup_requires=['setuptools-version-command'] +) diff --git a/stdeb.cfg b/stdeb.cfg new file mode 100644 index 0000000..05381db --- /dev/null +++ b/stdeb.cfg @@ -0,0 +1,17 @@ +# Copyright 2020 ArctosLabs Scandinavia AB +# +# 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. +[DEFAULT] +X-Python3-Version : >= 3.5 +Depends3: python3-osm-common, python3-yaml, python3-jinja2, python3-pip diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100755 index 0000000..429714f --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,18 @@ +# Copyright 2020 ArctosLabs Scandinavia AB +# ************************************************************* + +# This file is part of OSM Placement module +# All Rights Reserved to ArctosLabs Scandinavia AB + +# 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. +coverage \ No newline at end of file diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..68c6211 --- /dev/null +++ b/tox.ini @@ -0,0 +1,50 @@ +## +# Copyright 2020 ArctosLabs Scandinavia AB +# +# 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. +## +[tox] +envlist = py3 + +[testenv] +basepython = python3 +install_command = python3 -m pip install -U {opts} {packages} +deps = -rrequirements.txt + -rtest-requirements.txt +commands = python3 -m unittest discover -v + +[testenv:coverage] +basepython = python3 +deps = -rrequirements.txt + -rtest-requirements.txt +commands = coverage run -m unittest discover + coverage report --omit='*site-packages*','*test*','*__init__*' + coverage html -d ./.tox/coverage/HTMLreport --omit='*site-packages*','*test*','*__init__*' + coverage xml -o ./.tox/coverage/XMLreport/coverage.xml --omit='*site-packages*','*test*','*__init__*' + coverage erase + +[testenv:flake8] +basepython = python3 +deps = flake8 + -rrequirements.txt +commands = flake8 {toxinidir}/osm_pla/ {toxinidir}/setup.py \ + --max-line-length 120 \ + --exclude test_mznmodels.py,.svn,CVS,.gz,.git,__pycache__,.tox,local,temp + +[testenv:build] +basepython = python3 +# changedir ={toxinidir} +deps = stdeb + setuptools-version-command + -rrequirements.txt +commands = python3 setup.py --command-packages=stdeb.command bdist_deb -- GitLab