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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 -&VPvETwy7|oS9iN76J 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%0AW)|M!6O>y=aEfVeln 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 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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 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 -- 2.25.1