From fb37bca4b7e8178c65df2f1496afe372c1d55e78 Mon Sep 17 00:00:00 2001 From: lombardof Date: Thu, 3 May 2018 16:20:04 +0200 Subject: [PATCH] first commit Change-Id: I8a65ee5527dd16d81e87c8ac5d4bdb471e5e759d Signed-off-by: lombardof --- .bowerrc | 3 + .gitignore | 47 + Dockerfile | 24 + README.md | 50 + bower.json | 27 + instancehandler/__init__.py | 0 instancehandler/admin.py | 19 + instancehandler/apps.py | 23 + instancehandler/models.py | 21 + instancehandler/template/instance_list.html | 152 +++ .../template/modal/instance_create.html | 55 + .../template/modal/instance_new_action.html | 57 + .../template/modal/instance_show.html | 23 + instancehandler/tests.py | 19 + instancehandler/urls.py | 27 + instancehandler/views.py | 97 ++ lib/TopologyModels/example/example.yaml | 53 + lib/TopologyModels/osm/osm.yaml | 187 +++ lib/__init__.py | 1 + lib/osm/__init__.py | 1 + lib/osm/osm_parser.py | 89 ++ lib/osm/osm_rdcl_graph.py | 51 + lib/osm/osmclient/__init__.py | 0 lib/osm/osmclient/client.py | 430 +++++++ lib/parser.py | 46 + lib/rdcl_graph.py | 82 ++ lib/util.py | 198 +++ manage.py | 10 + projecthandler/__init__.py | 0 projecthandler/admin.py | 19 + projecthandler/apps.py | 21 + projecthandler/management/__init__.py | 0 .../management/commands/__init__.py | 0 .../commands/delete_project_type.py | 14 + .../management/commands/new_project_type.py | 14 + projecthandler/models.py | 333 +++++ projecthandler/osm_model.py | 328 +++++ .../descriptor/descriptor_view_base.html | 131 ++ .../descriptor/modal/choose_node_id.html | 27 + .../modal/modal_keyboard_info_base.html | 37 + .../template/project/new_project.html | 129 ++ .../osm/descriptor/descriptor_new.html | 155 +++ .../osm/descriptor/descriptor_view.html | 126 ++ .../osm/descriptor/descriptorlist.html | 57 + .../osm/descriptor/modal/choose_node_id.html | 1 + .../descriptor/modal/create_link_chooser.html | 21 + .../project/osm/modal/files_list.html | 33 + .../osm/modal/modal_keyboard_info.html | 1 + .../template/project/osm/osm_new_project.html | 25 + .../project/osm/osm_project_delete.html | 36 + .../project/osm/osm_project_descriptors.html | 121 ++ .../project/osm/osm_project_details.html | 79 ++ .../project/osm/osm_project_left_sidebar.html | 56 + .../template/project/osm/project_graph.html | 61 + .../project/osm/topology_toolbar.html | 6 + .../template/project/project_delete.html | 73 ++ .../template/project/project_descriptors.html | 54 + .../template/project/project_details.html | 78 ++ .../template/project/project_graph_base.html | 58 + .../template/project/projectlist.html | 84 ++ .../repository/repo_base_left_sidebar.html | 12 + .../project/repository/repo_create_modal.html | 43 + .../project/repository/repo_list.html | 120 ++ projecthandler/tests.py | 19 + projecthandler/urls/__init__.py | 0 projecthandler/urls/project.py | 54 + projecthandler/views.py | 537 ++++++++ requirements.txt | 16 + sf_t3d/__init__.py | 0 sf_t3d/context_processor.py | 10 + sf_t3d/settings.py | 168 +++ sf_t3d/templatetags/__init__.py | 0 sf_t3d/templatetags/get.py | 37 + sf_t3d/urls.py | 35 + sf_t3d/views.py | 22 + sf_t3d/wsgi.py | 16 + sf_user/__init__.py | 0 sf_user/admin.py | 19 + sf_user/apps.py | 21 + sf_user/management/__init__.py | 0 sf_user/management/commands/__init__.py | 0 .../management/commands/clean_guest_data.py | 19 + sf_user/management/commands/clearsessions.py | 17 + sf_user/models.py | 195 +++ sf_user/sessions.py | 32 + sf_user/tests.py | 19 + sf_user/views.py | 89 ++ static/assets/img/OSM-logo.png | Bin 0 -> 22753 bytes static/assets/img/account_circle.png | Bin 0 -> 1206 bytes static/assets/img/ce.png | Bin 0 -> 5951 bytes static/assets/img/cloudnode.png | Bin 0 -> 12709 bytes static/assets/img/controller-256.png | Bin 0 -> 16424 bytes static/assets/img/cp-80.png | Bin 0 -> 2743 bytes static/assets/img/docker.png | Bin 0 -> 7506 bytes static/assets/img/employer.jpg | Bin 0 -> 6019 bytes static/assets/img/employer.png | Bin 0 -> 4280 bytes static/assets/img/euh.png | Bin 0 -> 4177 bytes static/assets/img/favicon.ico | Bin 0 -> 10462 bytes static/assets/img/host-256.png | Bin 0 -> 14861 bytes static/assets/img/host.png | Bin 0 -> 14861 bytes static/assets/img/k8s.png | Bin 0 -> 363564 bytes static/assets/img/l2sw.png | Bin 0 -> 3188 bytes static/assets/img/ofcontroller.png | Bin 0 -> 16424 bytes static/assets/img/ofl2sw.png | Bin 0 -> 6774 bytes static/assets/img/osm_logo.png | Bin 0 -> 5534 bytes static/assets/img/osm_logo.svg | 1 + static/assets/img/osm_small_logo.png | Bin 0 -> 3006 bytes static/assets/img/punto.png | Bin 0 -> 12709 bytes static/assets/img/rdcl3dlogo0.png | Bin 0 -> 186811 bytes static/assets/img/rdcl3dlogo0.svg | 253 ++++ static/assets/img/router.png | Bin 0 -> 9836 bytes static/assets/img/server.png | Bin 0 -> 28817 bytes static/assets/img/sf_logo.png | Bin 0 -> 4641 bytes static/assets/img/sf_logo_big.png | Bin 0 -> 113131 bytes static/assets/img/sf_small_logo.png | Bin 0 -> 2306 bytes static/assets/img/switch.png | Bin 0 -> 3188 bytes static/assets/img/vl-80.png | Bin 0 -> 2264 bytes static/assets/img/vnf-100.png | Bin 0 -> 3085 bytes static/assets/img/vs.png | Bin 0 -> 3188 bytes static/css/rdcl.css | 44 + static/src/adminlte_session_storage.js | 55 + static/src/instancehandler/instance_list.js | 138 +++ static/src/projecthandler/descriptorslist.js | 69 ++ static/src/projecthandler/new_project.js | 84 ++ static/src/projecthandler/osm/controller.js | 107 ++ .../src/projecthandler/osm/gui_properties.js | 128 ++ .../src/projecthandler/osm/project_graph.js | 354 ++++++ static/src/utils.js | 153 +++ static/topology3D/css/d3-context-menu.css | 78 ++ static/topology3D/css/graph_editor_d3js.css | 415 +++++++ static/topology3D/js/d3-context-menu.js | 131 ++ static/topology3D/js/event.js | 73 ++ static/topology3D/js/graph_editor.js | 1084 +++++++++++++++++ static/topology3D/js/graph_request.js | 318 +++++ static/topology3D/js/model_graph_editor.js | 512 ++++++++ template/base.html | 159 +++ template/error.html | 28 + template/footer.html | 12 + template/forbidden.html | 28 + template/head.html | 26 + template/home.html | 73 ++ template/left_sidebar_base.html | 23 + template/login.html | 51 + template/logo_sidebar.html | 8 + template/modals/error_alert.html | 20 + template/topology_toolbar.html | 38 + vimhandler/__init__.py | 0 vimhandler/admin.py | 19 + vimhandler/apps.py | 23 + vimhandler/models.py | 21 + vimhandler/template/config/aws.html | 79 ++ vimhandler/template/config/aws_show.html | 58 + vimhandler/template/config/openstack.html | 198 +++ .../template/config/openstack_show.html | 130 ++ vimhandler/template/config/openvim.html | 27 + vimhandler/template/config/openvim_show.html | 23 + vimhandler/template/config/vmware.html | 148 +++ vimhandler/template/config/vmware_show.html | 114 ++ vimhandler/template/modal/vim_details.html | 20 + vimhandler/template/vim_create.html | 189 +++ vimhandler/template/vim_list.html | 105 ++ vimhandler/template/vim_show.html | 148 +++ vimhandler/tests.py | 19 + vimhandler/urls.py | 26 + vimhandler/views.py | 91 ++ 165 files changed, 11371 insertions(+) create mode 100644 .bowerrc create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 bower.json create mode 100644 instancehandler/__init__.py create mode 100644 instancehandler/admin.py create mode 100644 instancehandler/apps.py create mode 100644 instancehandler/models.py create mode 100644 instancehandler/template/instance_list.html create mode 100644 instancehandler/template/modal/instance_create.html create mode 100644 instancehandler/template/modal/instance_new_action.html create mode 100644 instancehandler/template/modal/instance_show.html create mode 100644 instancehandler/tests.py create mode 100644 instancehandler/urls.py create mode 100644 instancehandler/views.py create mode 100644 lib/TopologyModels/example/example.yaml create mode 100644 lib/TopologyModels/osm/osm.yaml create mode 100644 lib/__init__.py create mode 100644 lib/osm/__init__.py create mode 100644 lib/osm/osm_parser.py create mode 100644 lib/osm/osm_rdcl_graph.py create mode 100644 lib/osm/osmclient/__init__.py create mode 100644 lib/osm/osmclient/client.py create mode 100644 lib/parser.py create mode 100644 lib/rdcl_graph.py create mode 100644 lib/util.py create mode 100755 manage.py create mode 100644 projecthandler/__init__.py create mode 100644 projecthandler/admin.py create mode 100644 projecthandler/apps.py create mode 100644 projecthandler/management/__init__.py create mode 100644 projecthandler/management/commands/__init__.py create mode 100644 projecthandler/management/commands/delete_project_type.py create mode 100644 projecthandler/management/commands/new_project_type.py create mode 100644 projecthandler/models.py create mode 100644 projecthandler/osm_model.py create mode 100644 projecthandler/template/project/descriptor/descriptor_view_base.html create mode 100644 projecthandler/template/project/descriptor/modal/choose_node_id.html create mode 100644 projecthandler/template/project/modal/modal_keyboard_info_base.html create mode 100644 projecthandler/template/project/new_project.html create mode 100644 projecthandler/template/project/osm/descriptor/descriptor_new.html create mode 100644 projecthandler/template/project/osm/descriptor/descriptor_view.html create mode 100644 projecthandler/template/project/osm/descriptor/descriptorlist.html create mode 100644 projecthandler/template/project/osm/descriptor/modal/choose_node_id.html create mode 100644 projecthandler/template/project/osm/descriptor/modal/create_link_chooser.html create mode 100644 projecthandler/template/project/osm/modal/files_list.html create mode 100644 projecthandler/template/project/osm/modal/modal_keyboard_info.html create mode 100644 projecthandler/template/project/osm/osm_new_project.html create mode 100644 projecthandler/template/project/osm/osm_project_delete.html create mode 100644 projecthandler/template/project/osm/osm_project_descriptors.html create mode 100644 projecthandler/template/project/osm/osm_project_details.html create mode 100644 projecthandler/template/project/osm/osm_project_left_sidebar.html create mode 100644 projecthandler/template/project/osm/project_graph.html create mode 100644 projecthandler/template/project/osm/topology_toolbar.html create mode 100644 projecthandler/template/project/project_delete.html create mode 100644 projecthandler/template/project/project_descriptors.html create mode 100644 projecthandler/template/project/project_details.html create mode 100644 projecthandler/template/project/project_graph_base.html create mode 100644 projecthandler/template/project/projectlist.html create mode 100644 projecthandler/template/project/repository/repo_base_left_sidebar.html create mode 100644 projecthandler/template/project/repository/repo_create_modal.html create mode 100644 projecthandler/template/project/repository/repo_list.html create mode 100644 projecthandler/tests.py create mode 100644 projecthandler/urls/__init__.py create mode 100644 projecthandler/urls/project.py create mode 100644 projecthandler/views.py create mode 100644 requirements.txt create mode 100644 sf_t3d/__init__.py create mode 100644 sf_t3d/context_processor.py create mode 100644 sf_t3d/settings.py create mode 100644 sf_t3d/templatetags/__init__.py create mode 100644 sf_t3d/templatetags/get.py create mode 100644 sf_t3d/urls.py create mode 100644 sf_t3d/views.py create mode 100644 sf_t3d/wsgi.py create mode 100644 sf_user/__init__.py create mode 100644 sf_user/admin.py create mode 100644 sf_user/apps.py create mode 100644 sf_user/management/__init__.py create mode 100644 sf_user/management/commands/__init__.py create mode 100644 sf_user/management/commands/clean_guest_data.py create mode 100644 sf_user/management/commands/clearsessions.py create mode 100644 sf_user/models.py create mode 100644 sf_user/sessions.py create mode 100644 sf_user/tests.py create mode 100644 sf_user/views.py create mode 100644 static/assets/img/OSM-logo.png create mode 100644 static/assets/img/account_circle.png create mode 100755 static/assets/img/ce.png create mode 100755 static/assets/img/cloudnode.png create mode 100755 static/assets/img/controller-256.png create mode 100755 static/assets/img/cp-80.png create mode 100644 static/assets/img/docker.png create mode 100755 static/assets/img/employer.jpg create mode 100644 static/assets/img/employer.png create mode 100755 static/assets/img/euh.png create mode 100644 static/assets/img/favicon.ico create mode 100755 static/assets/img/host-256.png create mode 100755 static/assets/img/host.png create mode 100644 static/assets/img/k8s.png create mode 100755 static/assets/img/l2sw.png create mode 100755 static/assets/img/ofcontroller.png create mode 100755 static/assets/img/ofl2sw.png create mode 100644 static/assets/img/osm_logo.png create mode 100644 static/assets/img/osm_logo.svg create mode 100644 static/assets/img/osm_small_logo.png create mode 100755 static/assets/img/punto.png create mode 100644 static/assets/img/rdcl3dlogo0.png create mode 100644 static/assets/img/rdcl3dlogo0.svg create mode 100755 static/assets/img/router.png create mode 100644 static/assets/img/server.png create mode 100644 static/assets/img/sf_logo.png create mode 100644 static/assets/img/sf_logo_big.png create mode 100644 static/assets/img/sf_small_logo.png create mode 100755 static/assets/img/switch.png create mode 100755 static/assets/img/vl-80.png create mode 100755 static/assets/img/vnf-100.png create mode 100755 static/assets/img/vs.png create mode 100644 static/css/rdcl.css create mode 100644 static/src/adminlte_session_storage.js create mode 100644 static/src/instancehandler/instance_list.js create mode 100644 static/src/projecthandler/descriptorslist.js create mode 100644 static/src/projecthandler/new_project.js create mode 100644 static/src/projecthandler/osm/controller.js create mode 100644 static/src/projecthandler/osm/gui_properties.js create mode 100644 static/src/projecthandler/osm/project_graph.js create mode 100644 static/src/utils.js create mode 100644 static/topology3D/css/d3-context-menu.css create mode 100755 static/topology3D/css/graph_editor_d3js.css create mode 100644 static/topology3D/js/d3-context-menu.js create mode 100755 static/topology3D/js/event.js create mode 100755 static/topology3D/js/graph_editor.js create mode 100644 static/topology3D/js/graph_request.js create mode 100644 static/topology3D/js/model_graph_editor.js create mode 100644 template/base.html create mode 100644 template/error.html create mode 100644 template/footer.html create mode 100644 template/forbidden.html create mode 100644 template/head.html create mode 100644 template/home.html create mode 100644 template/left_sidebar_base.html create mode 100644 template/login.html create mode 100644 template/logo_sidebar.html create mode 100644 template/modals/error_alert.html create mode 100644 template/topology_toolbar.html create mode 100644 vimhandler/__init__.py create mode 100644 vimhandler/admin.py create mode 100644 vimhandler/apps.py create mode 100644 vimhandler/models.py create mode 100644 vimhandler/template/config/aws.html create mode 100644 vimhandler/template/config/aws_show.html create mode 100644 vimhandler/template/config/openstack.html create mode 100644 vimhandler/template/config/openstack_show.html create mode 100644 vimhandler/template/config/openvim.html create mode 100644 vimhandler/template/config/openvim_show.html create mode 100644 vimhandler/template/config/vmware.html create mode 100644 vimhandler/template/config/vmware_show.html create mode 100644 vimhandler/template/modal/vim_details.html create mode 100644 vimhandler/template/vim_create.html create mode 100644 vimhandler/template/vim_list.html create mode 100644 vimhandler/template/vim_show.html create mode 100644 vimhandler/tests.py create mode 100644 vimhandler/urls.py create mode 100644 vimhandler/views.py diff --git a/.bowerrc b/.bowerrc new file mode 100644 index 0000000..c7fde75 --- /dev/null +++ b/.bowerrc @@ -0,0 +1,3 @@ +{ +"directory": "static/bower_components" +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..06fb943 --- /dev/null +++ b/.gitignore @@ -0,0 +1,47 @@ +### Python template +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# Distribution / packaging +.Python +env/ +.idea +db.sqlite3 +.idea +wheelhouse/ + +.installed.cfg + + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +#MacOS files +.DS_Store + +#Bower +static/bower_components + +#migrations +projecthandler/migrations/ +sf_user/migrations/ +instancehandler/migrations diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..56b0497 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,24 @@ +FROM python:2.7 + +WORKDIR /usr/src/app +COPY . /usr/src/app + +RUN apt-get update +RUN apt-get -y install npm +RUN npm install -g bower +RUN ln -s /usr/bin/nodejs /usr/bin/node +RUN bower install --allow-root +RUN pip install -r requirements.txt + + +# delete the copy of the database inside the container (if exists) +RUN rm -f db.sqlite3 + +RUN python manage.py makemigrations sf_user projecthandler instancehandler vimhandler +RUN python manage.py migrate + +RUN python manage.py shell -c "from projecthandler.osm_model import OsmProject; from sf_user.models import CustomUser; CustomUser.objects.create_superuser('admin', 'admin'); admin = CustomUser.objects.get(username='admin'); OsmProject.create_project('admin',admin,True, 'project admin','')" + + +EXPOSE 80 +CMD ["python", "manage.py", "runserver", "0.0.0.0:80"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..c865b36 --- /dev/null +++ b/README.md @@ -0,0 +1,50 @@ + + +# Lightweight GUI for OSM lightweight build + +This project focuses on the implementation of a web GUI to interact with the Northbound API of OSM lightweigth build. + +The project is based on ([RDCL 3D](https://github.com/superfluidity/RDCL3D)), a web framework for the design of NFV services and components. The framework allows editing, +validating, visualizing the descriptors of services and components both textually and graphically. + + +## Documentation & publications + +A publication on the background work (RDCL 3D) is available: + +S. Salsano, F. Lombardo, C. Pisa, P. Greto, N. Blefari-Melazzi, +“RDCL 3D, a Model Agnostic Web Framework for the Design and Composition of NFV Services”, +3rd IEEE International Workshop on Orchestration for Software Defined Infrastructures, O4SDI at IEEE NFV-SDN conference, Berlin, 6-8 November 2017 ([pdf on Arxiv](https://arxiv.org/pdf/1702.08242)) + +## Community + +Follow + +The mailing list [rdcl3d@googlegroups.com](mailto:rdcl3d@googlegroups.com) is available for architecture and design discussions, +requests for help, features request, bug reports on RDCL 3D... To join the list, just send an email to [rdcl3d+subscribe@googlegroups.com](mailto:rdcl3d+subscribe@googlegroups.com) or [join with a gmail account](https://groups.google.com/forum/#!forum/rdcl3d) + +## Aknowledgements + +This work has been performed in the context of the project Superfluidity, which received funding from the European Union's Horizon 2020 research and innovation programme under grant agreement No. 671566. + +## Installation + +This project is included in the installer of OSM lightweight build + + +## License + + Copyright 2017 CNIT - Consorzio Nazionale Interuniversitario per le Telecomunicazioni + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT 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/bower.json b/bower.json new file mode 100644 index 0000000..136f061 --- /dev/null +++ b/bower.json @@ -0,0 +1,27 @@ +{ + "name": "osm-lw-ui", + "authors": [ + "Francesco Lombardo " + ], + "description": "OSM Light UI", + "main": "", + "license": "Apache-2.0", + "homepage": "https://osm.etsi.org", + "private": true, + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "test", + "tests" + ], + "resolutions": { + "jquery": "2 - 3" + }, + "dependencies": { + "admin-lte": "^2.4.3", + "codemirror": "^5.36.0", + "d3": "^5.1.0", + "bootbox.js": "bootbox#^4.4.0" + } +} diff --git a/instancehandler/__init__.py b/instancehandler/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/instancehandler/admin.py b/instancehandler/admin.py new file mode 100644 index 0000000..2e9690a --- /dev/null +++ b/instancehandler/admin.py @@ -0,0 +1,19 @@ +# +# Copyright 2018 CNIT - Consorzio Nazionale Interuniversitario per le Telecomunicazioni +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an BASIS, +# WITHOUT 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 django.contrib import admin + +# Register your models here. diff --git a/instancehandler/apps.py b/instancehandler/apps.py new file mode 100644 index 0000000..2f49d31 --- /dev/null +++ b/instancehandler/apps.py @@ -0,0 +1,23 @@ +# +# Copyright 2018 CNIT - Consorzio Nazionale Interuniversitario per le Telecomunicazioni +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from __future__ import unicode_literals + +from django.apps import AppConfig + + +class InstancehandlerConfig(AppConfig): + name = 'instancehandler' diff --git a/instancehandler/models.py b/instancehandler/models.py new file mode 100644 index 0000000..21d5735 --- /dev/null +++ b/instancehandler/models.py @@ -0,0 +1,21 @@ +# +# Copyright 2018 CNIT - Consorzio Nazionale Interuniversitario per le Telecomunicazioni +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from __future__ import unicode_literals + +from django.db import models + +# Create your models here. diff --git a/instancehandler/template/instance_list.html b/instancehandler/template/instance_list.html new file mode 100644 index 0000000..1b0868d --- /dev/null +++ b/instancehandler/template/instance_list.html @@ -0,0 +1,152 @@ +{% extends "base.html" %} +{% load get %} +{% load staticfiles %} + + + +{% block head_block %} + {{ block.super }} + + + + + + +{% endblock %} +{% block title_header_big %} + {{ block.super }} +{% endblock %} +{% block left_sidebar %} + {% include 'osm/osm_project_left_sidebar.html' %} +{% endblock %} + + +{% block breadcrumb_body %} + {{ block.super }} +
  • Instances
  • +{% endblock %} + +{% block content_body %} + {{ block.super }} + {% include 'modal/instance_create.html' %} + {% csrf_token %} +
    +
    + +
    +
    +

    Instances

    + +
    +
    + + + + + + + + + + + + + + {% for i in instances %} + + + + + + + {% if i|get:"operational-status" == 'failed' %} + + {% elif i|get:"operational-status" == 'init' %} + + {% elif i|get:"operational-status" == 'running' %} + + {% else %} + + {% endif %} + {% if i|get:"config-status" == 'failed' %} + + {% elif i|get:"config-status" == 'init' %} + + {% elif i|get:"config-status" == 'running' %} + + {% elif i|get:"config-status" == 'configured' %} + + {% else %} + + {% endif %} + + + + + + {% endfor %} + +
    IdNameNsd nameOperational StatusConfig StatusDetailed StatusActions
    {{ i|get:"_id" }}{{ i|get:"short-name" }}{{ i|get:"nsd-name-ref" }}{{ i|get:"operational-status" }} {{ i|get:"operational-status" }} {{ i|get:"operational-status" }} {{ i|get:"operational-status" }}{{ i|get:"config-status" }} {{ i|get:"config-status" }} {{ i|get:"config-status" }} {{ i|get:"config-status" }} {{ i|get:"config-status" }}{{ i|get:"detailed-status" }} +
    + + + + + + + + +
    + +
    +
    +
    +
    + +
    +{% include 'modal/instance_show.html' %} +{% include 'modal/instance_new_action.html' %} +{% endblock %} + +{% block resource_block %} + {{ block.super }} + + + + + + + + + + + + + + + + + +{% endblock %} + +{% block footer %} + {% include "footer.html" %} +{% endblock %} diff --git a/instancehandler/template/modal/instance_create.html b/instancehandler/template/modal/instance_create.html new file mode 100644 index 0000000..c40ecb2 --- /dev/null +++ b/instancehandler/template/modal/instance_create.html @@ -0,0 +1,55 @@ + diff --git a/instancehandler/template/modal/instance_new_action.html b/instancehandler/template/modal/instance_new_action.html new file mode 100644 index 0000000..6cf3059 --- /dev/null +++ b/instancehandler/template/modal/instance_new_action.html @@ -0,0 +1,57 @@ + diff --git a/instancehandler/template/modal/instance_show.html b/instancehandler/template/modal/instance_show.html new file mode 100644 index 0000000..6df8bc1 --- /dev/null +++ b/instancehandler/template/modal/instance_show.html @@ -0,0 +1,23 @@ + diff --git a/instancehandler/tests.py b/instancehandler/tests.py new file mode 100644 index 0000000..79947e6 --- /dev/null +++ b/instancehandler/tests.py @@ -0,0 +1,19 @@ +# +# Copyright 2018 CNIT - Consorzio Nazionale Interuniversitario per le Telecomunicazioni +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an BASIS, +# WITHOUT 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 django.test import TestCase + +# Create your tests here. diff --git a/instancehandler/urls.py b/instancehandler/urls.py new file mode 100644 index 0000000..b6a9e33 --- /dev/null +++ b/instancehandler/urls.py @@ -0,0 +1,27 @@ +# +# Copyright 2018 CNIT - Consorzio Nazionale Interuniversitario per le Telecomunicazioni +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an BASIS, +# WITHOUT 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 django.conf.urls import url +from instancehandler import views + +urlpatterns = [ + url(r'^(?P[ns|vnf]+)/list/', views.list, name='list'), + url(r'^create/', views.create, name='create'), + url(r'^(?P[ns|vnf]+)/(?P[0-9a-z-]+)/delete$', views.delete, name='delete'), + url(r'^(?P[ns|vnf]+)/(?P[0-9a-z-]+)/action', views.action, name='action'), + url(r'^(?P[ns|vnf]+)/(?P[0-9a-z-]+)', views.show, name='show'), + +] diff --git a/instancehandler/views.py b/instancehandler/views.py new file mode 100644 index 0000000..0c9a8a4 --- /dev/null +++ b/instancehandler/views.py @@ -0,0 +1,97 @@ +# +# Copyright 2018 CNIT - Consorzio Nazionale Interuniversitario per le Telecomunicazioni +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an BASIS, +# WITHOUT 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 django.shortcuts import render, redirect +from django.contrib.auth.decorators import login_required, permission_required +from django.http import HttpResponse, JsonResponse +import json +import logging +from lib.osm.osmclient.client import Client + + +@login_required +def list(request, project_id=None, type=None): + client = Client() + if type == 'ns': + result = client.ns_list() + + return __response_handler(request, {'instances': result, 'type': 'ns', 'project_id': project_id}, 'instance_list.html') + + +@login_required +def create(request, project_id=None): + result = {} + ns_data = { + "nsName": request.POST.get('nsName', 'WithoutName'), + "nsDescription": request.POST.get('nsDescription', ''), + "nsdId": request.POST.get('nsdId', ''), + "vimAccountId": request.POST.get('vimAccountId', ''), + # "ssh-authorized-key": [ + # { + # request.POST.get('key-pair-ref', ''): request.POST.get('keyValue', '') + # } + # ] + } + print ns_data + client = Client() + result = client.ns_create(ns_data) + return __response_handler(request, result, 'projects:instances:list', to_redirect=True, type='ns', project_id=project_id) + + +@login_required +def action(request, project_id=None, instance_id=None, type=None): + result = {} + client = Client() + + # result = client.ns_action(instance_id, action_payload) + primitive_param_keys = request.POST.getlist('primitive_params_name') + primitive_param_value = request.POST.getlist('primitive_params_value') + action_payload = { + "vnf_member_index": request.POST.get('vnf_member_index'), + "primitive": request.POST.get('primitive'), + "primitive_params": {k: v for k, v in zip(primitive_param_keys, primitive_param_value) if len(k) > 0} + } + + result = client.ns_action(instance_id, action_payload) + return __response_handler(request, result, None, to_redirect=False, status=result['status'] ) + + +@login_required +def delete(request, project_id=None, instance_id=None, type=None): + result = {} + client = Client() + result = client.ns_delete(instance_id) + print result + return __response_handler(request, result, 'projects:instances:list', to_redirect=True, type='ns', project_id=project_id) + + +@login_required +def show(request, project_id=None, instance_id=None, type=None): + # result = {} + client = Client() + result = client.ns_get(instance_id) + print result + return __response_handler(request, result) + + +def __response_handler(request, data_res, url=None, to_redirect=None, *args, **kwargs): + raw_content_types = request.META.get('HTTP_ACCEPT', '*/*').split(',') + if 'application/json' in raw_content_types or url is None: + return JsonResponse(data_res, *args, **kwargs) + elif to_redirect: + return redirect(url, *args, **kwargs) + else: + return render(request, url, data_res) diff --git a/lib/TopologyModels/example/example.yaml b/lib/TopologyModels/example/example.yaml new file mode 100644 index 0000000..04633f5 --- /dev/null +++ b/lib/TopologyModels/example/example.yaml @@ -0,0 +1,53 @@ +name: Example #Model name +description: example #Model description +version: 1.0 #Model version +designer: Pierluigi Greto #Model designer +nodes: #List of nodes, with id and label used in the gui (We can add more fields) + examplenode1: + label: EXN1 + examplenode2: + label: EXN2 +layer: #List of Layers + exampleLayer: + nodes: #List of node to be visualized in the current layer + examplenode1: + addable: + callback: addNode + removable: true + examplenode1: + addable: + callback: addNode + removable: true + allowed_edges: #List of allowed edges between the layer's nodes + examplenode1: #Edge's source + destination: #List of edge's destination with the list of controls callback id to call when there is a connections + examplenode1: + direct_edge: false + removable: true + examplenode2: + direct_edge: false + removable: true +action: #Action to show on rightclick on a node/link + rightclick: + node: + delete: + title: Delete + callback: deleteNode + link: + delete: + title: Delete + callback: deleteLink + +callback: #List of callbacks used + addNode: + file: example_controller.js + class: ExampleController + removeNode: + file: example_controller.js + class: ExampleController + addLink: + file: example_controller.js + class: ExampleController + removeLink: + file: example_controller.js + class: ExampleController diff --git a/lib/TopologyModels/osm/osm.yaml b/lib/TopologyModels/osm/osm.yaml new file mode 100644 index 0000000..59ac883 --- /dev/null +++ b/lib/TopologyModels/osm/osm.yaml @@ -0,0 +1,187 @@ +name: Osm #Model name +description: Osm #Model description +version: 1.0 #Model version +designer: Francesco Lombardo #Model designer +nodes: #List of nodes, with id and label used in the gui (We can add more fields) + vnf: + label: VNF + ns_vl: + label: VL + ns_cp: + label: SAP + vnf_vl: + label: intVL + vnf_vdu: + label: VDU + vnf_ext_cp: + label: ExtCP + vnf_vdu_cp: + label: VduCP +layer: #List of Layers + ns: + nodes: #List of node to be visualized in the current layer + vnf: + addable: + callback: addVnf + removable: + callback: removeNode + expands : vnf + ns_vl: + addable: + callback: addNode + removable: + callback: removeNode + ns_cp: + addable: + callback: addNode + removable: + callback: removeNode + allowed_edges: #List of allowed edges between the layer's nodes + vnf: #Edge's source + destination: #List of edge's destination with the list of controls callback id to call when there is a connections + ns_vl: + callback: linkVnftoNsVl + direct_edge: false + removable: + callback: removeLink + ns_cp: + callback: linkVnftoNsCp + direct_edge: false + removable: + callback: removeLink + ns_vl: + destination: + vnf: + callback: linkVnftoNsVl + direct_edge: false + removable: + callback: removeLink + ns_cp: + callback: nsCpExclusiveConnection + direct_edge: false + removable: + callback: removeLink + + ns_cp: + destination: + vnf: + callback: linkVnftoNsCp + direct_edge: false + removable: + callback: removeLink + ns_vl: + callback: nsCpExclusiveConnection + direct_edge: false + removable: + callback: removeLink + action: #Action to show on rightclick all types of node/link + node: + addToCurrentVNFFG: + title: Add to current VNFFG + callback: addToCurrentVNFFG + link: + + vnf: + nodes: #List of node to be visualized in the current layer + vnf_vl: + addable: + callback: addNode + removable: + callback: removeNode + vnf_vdu: + addable: + callback: addVnfVdu + removable: + callback: removeVnfVdu + vnf_ext_cp: + addable: + callback: addNode + removable: + callback: removeNode + vnf_vdu_cp: + addable: + callback: addVnfVduCp + removable: + callback: removeVnfVduCp + allowed_edges: #List of allowed edges between the layer's nodes + vnf_vl: #Edge's source + destination: #List of edge's destination with the list of controls callback id to call when there is a connections + vnf_vdu_cp: + callback: linkVltoVduCp + direct_edge: false + removable: + callback: removeLink + vnf_ext_cp: + callback: linkVnfVltoExpCp + direct_edge: false + removable: + callback: removeLink + vnf_ext_cp: + destination: + vnf_vl: + callback: linkVnfVltoExpCp + direct_edge: false + removable: + callback: removeLink + vnf_vdu_cp: + destination: + vnf_vl: + callback: linkVltoVduCp + direct_edge: false + removable: + callback: removeLink + + + +callback: #List of callbacks used + chooseVnfExp: + file: osm_controller.js + class: OSMController + nsCpExclusiveConnection: + file: osm_controller.js + class: OSMController + getVduConnectedToVduCp: + file: osm_controller.js + class: OSMController + addVnf: + file: osm_controller.js + class: OSMController + addNode: + file: osm_controller.js + class: OSMController + addVnfVdu: + file: osm_controller.js + class: OSMController + addVnfVduCp: + file: osm_controller.js + class: OSMController + addLink: + file: osm_controller.js + class: OSMController + linkVnftoNsCp: + file: osm_controller.js + class: OSMController + linkVnftoNsVl: + file: osm_controller.js + class: OSMController + linkVltoVduCp: + file: osm_controller.js + class: OSMController + linkVnfVltoExpCp: + file: osm_controller.js + class: OSMController + removeNode: + file: osm_controller.js + class: OSMController + removeVnfVdu: + file: osm_controller.js + class: OSMController + removeVnfVduCp: + file: osm_controller.js + class: OSMController + removeLink: + file: osm_controller.js + class: OSMController + addToCurrentVNFFG: + file: osm_controller.js + class: OSMController diff --git a/lib/__init__.py b/lib/__init__.py new file mode 100644 index 0000000..bfcc6bf --- /dev/null +++ b/lib/__init__.py @@ -0,0 +1 @@ +__all__ = ["Util", 'etsi'] diff --git a/lib/osm/__init__.py b/lib/osm/__init__.py new file mode 100644 index 0000000..26d8656 --- /dev/null +++ b/lib/osm/__init__.py @@ -0,0 +1 @@ +__all__ = ["osm_parser", "osm_rdcl_graph"] \ No newline at end of file diff --git a/lib/osm/osm_parser.py b/lib/osm/osm_parser.py new file mode 100644 index 0000000..a596405 --- /dev/null +++ b/lib/osm/osm_parser.py @@ -0,0 +1,89 @@ +# +# Copyright 2017 CNIT - Consorzio Nazionale Interuniversitario per le Telecomunicazioni +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import json +import pyaml +import yaml +from lib.util import Util +from lib.parser import Parser +import logging +import traceback +import glob +import os + +logging.basicConfig(level=logging.DEBUG) +log = logging.getLogger('OsmParser') + +class OsmParser(Parser): + """Parser methods for osm project type + + """ + + def __init__(self): + super(OsmParser, self).__init__() + + @classmethod + def importprojectdir(cls,dir_project, file_type): + """Imports all descriptor files under a given folder + + this method is specific for Osm project type + """ + + project = { + 'nsd':{}, + + 'vnfd':{}, + + 'positions': {} + } + + + for desc_type in project: + cur_type_path = os.path.join(dir_project, desc_type.upper()) + log.debug(cur_type_path) + if os.path.isdir(cur_type_path): + for file in glob.glob(os.path.join(cur_type_path, '*.'+file_type)): + if file_type == 'json': + project[desc_type][os.path.basename(file).split('.')[0]] = Util.loadjsonfile(file) + elif file_type == 'yaml': + project[desc_type][os.path.basename(file).split('.')[0]] = Util.loadyamlfile(file) + + + for vertices_file in glob.glob(os.path.join(dir_project, '*.json')): + if os.path.basename(vertices_file) == 'vertices.json': + project['positions']['vertices'] = Util.loadjsonfile(vertices_file) + + return project + + @classmethod + def importprojectfiles(cls, file_dict): + """Imports descriptors (extracted from the new project POST) + + The keys in the dictionary are the file types + """ + project = { + 'nsd':{}, + + 'vnfd':{}, + + } + for desc_type in project: + if desc_type in file_dict: + files_desc_type = file_dict[desc_type] + for file in files_desc_type: + project[desc_type][os.path.splitext(file.name)[0]] = json.loads(file.read()) + + return project \ No newline at end of file diff --git a/lib/osm/osm_rdcl_graph.py b/lib/osm/osm_rdcl_graph.py new file mode 100644 index 0000000..91b798f --- /dev/null +++ b/lib/osm/osm_rdcl_graph.py @@ -0,0 +1,51 @@ +# +# Copyright 2017 CNIT - Consorzio Nazionale Interuniversitario per le Telecomunicazioni +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import json +import logging +import copy +from lib.rdcl_graph import RdclGraph + +logging.basicConfig(level=logging.DEBUG) +log = logging.getLogger('OsmRdclGraph') + +class OsmRdclGraph(RdclGraph): + """Operates on the graph representation used for the GUI graph views""" + + def __init__(self): + pass + + + def build_graph_from_project(self, json_project, model={}): + """Creates a single graph for a whole project""" + + #print "json_project ",json_project + graph_object = { + 'vertices': [], + 'edges': [], + 'graph_parameters': {}, + 'model': model + } + try: + positions = json_project['positions'] if 'positions' in json_project else False + log.debug('build graph from project json') + + + except Exception as e: + log.exception('Exception in build_graph_from_project') + raise + + return graph_object diff --git a/lib/osm/osmclient/__init__.py b/lib/osm/osmclient/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/osm/osmclient/client.py b/lib/osm/osmclient/client.py new file mode 100644 index 0000000..48c9815 --- /dev/null +++ b/lib/osm/osmclient/client.py @@ -0,0 +1,430 @@ +import requests +import logging +import json +import tarfile +import yaml +import pyaml +import StringIO +from lib.util import Util +import hashlib +import os + +logging.basicConfig(level=logging.DEBUG) +log = logging.getLogger('helper.py') + + +class Client(object): + def __init__(self, host=os.getenv('OSM_SERVER', "localhost"), so_port=9999, so_project='admin', ro_host=None, ro_port=9090, **kwargs): + + self._user = 'admin' + self._password = 'admin' + # self._project = so_project + self._project = so_project + self._token_endpoint = 'admin/v1/tokens' + self._user_endpoint = 'admin/v1/users' + + self._headers = {} + self._host = host + + self._base_path = "https://{0}:{1}/osm".format(self._host, so_port) + + def get_token(self): + postfields_dict = {'username': self._user, + 'password': self._password, + 'project-id': self._project} + token_url = "{0}/{1}".format(self._base_path, self._token_endpoint) + token = self._send_post(token_url, None, postfields_dict, headers={"Content-Type": "application/yaml", "accept": "application/json"}) + if token is not None: + return token['id'] + return None + + def vim_list(self): + token = self.get_token() + if token: + self._headers['Authorization'] = 'Bearer {}'.format(token) + self._headers['accept'] = 'application/json' + _url = "{0}/admin/v1/vims".format(self._base_path) + return self._send_get(_url, headers=self._headers) + + def vim_delete(self, id): + token = self.get_token() + if token: + self._headers['Authorization'] = 'Bearer {}'.format(token) + self._headers['accept'] = 'application/json' + _url = "{0}/admin/v1/vims/{1}".format(self._base_path, id) + return self._send_delete(_url, headers=self._headers) + return None + + def vim_get(self, id): + token = self.get_token() + if token: + self._headers['Authorization'] = 'Bearer {}'.format(token) + self._headers['accept'] = 'application/json' + _url = "{0}/admin/v1/vims/{1}".format(self._base_path, id) + return self._send_get(_url, headers=self._headers) + return None + + def vim_create(self, vim_data): + token = self.get_token() + headers = {} + if token: + headers['Authorization'] = 'Bearer {}'.format(token) + headers['Content-Type'] = 'application/json' + headers['accept'] = 'application/json' + + _url = "{0}/admin/v1/vims".format(self._base_path) + return self._send_post(_url, headers=headers, + json=vim_data) + return None + + def nsd_list(self): + token = self.get_token() + if token: + self._headers['Authorization'] = 'Bearer {}'.format(token) + self._headers['Content-Type'] = 'application/yaml' + self._headers['accept'] = 'application/json' + _url = "{0}/nsd/v1/ns_descriptors_content".format(self._base_path) + return self._send_get(_url, headers=self._headers) + return None + + def nsd_get(self, id): + token = self.get_token() + if token: + self._headers['Authorization'] = 'Bearer {}'.format(token) + self._headers['Content-Type'] = 'application/yaml' + #self._headers['accept'] = 'application/json' + _url = "{0}/nsd/v1/ns_descriptors/{1}/nsd".format(self._base_path,id) + return yaml.load(self._send_get(_url, headers=self._headers)) + return None + + def nsd_delete(self, id): + token = self.get_token() + if token: + self._headers['Authorization'] = 'Bearer {}'.format(token) + self._headers['Content-Type'] = 'application/yaml' + self._headers['accept'] = 'application/json' + _url = "{0}/nsd/v1/ns_descriptors_content/{1}".format(self._base_path, id) + return self._send_delete(_url, headers=self._headers) + return None + + def _descriptor_update(self, tarf, data): + print tarf.getnames() + # extract the package on a tmp directory + tarf.extractall('/tmp') + + for name in tarf.getnames(): + if name.endswith(".yaml") or name.endswith(".yml"): + with open('/tmp/' + name, 'w') as outfile: + yaml.safe_dump(data, outfile, default_flow_style=False) + break + + tarf_temp = tarfile.open('/tmp/' + tarf.getnames()[0] + ".tar.gz", "w:gz") + # tarf_temp = tarfile.open("pippo.tar.gz", "w:gz") + print tarf_temp.getnames() + # tarf_temp.add('/tmp/'+tarf.getnames()[0]) + for tarinfo in tarf: + # if tarinfo.name.startswith(tarf.getnames()[0]): + # new_name = tarinfo.name[len(tarf.getnames()[0]):] + tarf_temp.add('/tmp/' + tarinfo.name, tarinfo.name, recursive=False) + print tarf_temp.getnames() + tarf_temp.close() + return tarf + + def nsd_update(self, id, data): + token = self.get_token() + headers = {} + if token: + # get the package onboarded + tar_pkg = self.get_nsd_pkg(id) + tarf = tarfile.open(fileobj=tar_pkg) + + tarf = self._descriptor_update(tarf, data) + headers['Authorization'] = 'Bearer {}'.format(token) + headers['Content-Type'] = 'application/gzip' + headers['accept'] = 'application/json' + headers['Content-File-MD5'] = self.md5(open('/tmp/' + tarf.getnames()[0] + ".tar.gz", 'rb')) + #headers['Content-File-MD5'] = self.md5(open("pippo.tar.gz", 'rb')) + + _url = "{0}/nsd/v1/ns_descriptors/{1}/nsd_content".format(self._base_path, id) + return self._send_put(_url, headers=headers, data=open('/tmp/'+tarf.getnames()[0] + ".tar.gz", 'rb')) + #return self._send_put(_url, headers=headers, data=open("pippo.tar.gz", 'rb')) + + return None + + def nsd_onboard(self, package): + token = self.get_token() + headers = {} + if token: + + headers['Authorization'] = 'Bearer {}'.format(token) + headers['Content-Type'] = 'application/gzip' + headers['accept'] = 'application/json' + with open('/tmp/'+package.name, 'wb+') as destination: + for chunk in package.chunks(): + destination.write(chunk) + headers['Content-File-MD5'] = self.md5(open('/tmp/'+package.name, 'rb')) + + _url = "{0}/nsd/v1/ns_descriptors_content/".format(self._base_path) + return self._send_post(_url, headers=headers, + data=open('/tmp/'+package.name, 'rb')) + return None + + def nsd_artifacts(self, id): + token = self.get_token() + if token: + self._headers['Authorization'] = 'Bearer {}'.format(token) + self._headers['Content-Type'] = 'application/yaml' + self._headers['accept'] = 'text/plain' + _url = "{0}/nsd/v1/ns_descriptors/{1}/artifacts".format(self._base_path, id) + return self._send_get(_url, headers=self._headers) + return None + + def ns_list(self): + token = self.get_token() + if token: + self._headers['Authorization'] = 'Bearer {}'.format(token) + self._headers['Content-Type'] = 'application/yaml' + self._headers['accept'] = 'application/json' + _url = "{0}/nslcm/v1/ns_instances_content".format(self._base_path) + return self._send_get(_url, headers=self._headers) + return None + + def ns_create(self, ns_data): + token = self.get_token() + headers = {} + if token: + headers['Authorization'] = 'Bearer {}'.format(token) + headers['Content-Type'] = 'application/yaml' + headers['accept'] = 'application/json' + + _url = "{0}/nslcm/v1/ns_instances_content".format(self._base_path) + return self._send_post(_url, headers=headers, + json=ns_data) + return None + + def ns_get(self, id): + token = self.get_token() + if token: + self._headers['Authorization'] = 'Bearer {}'.format(token) + self._headers['Content-Type'] = 'application/json' + self._headers['accept'] = 'application/json' + _url = "{0}/nslcm/v1/ns_instances_content/{1}".format(self._base_path, id) + return self._send_get(_url, headers=self._headers) + return None + + def ns_delete(self, id): + token = self.get_token() + if token: + self._headers['Authorization'] = 'Bearer {}'.format(token) + #self._headers['Content-Type'] = 'application/yaml' + self._headers['accept'] = 'application/json' + _url = "{0}/nslcm/v1/ns_instances_content/{1}".format(self._base_path, id) + return self._send_delete(_url, headers=self._headers) + return None + + def ns_action(self, id, action_payload): + token = self.get_token() + headers = {} + if token: + headers['Authorization'] = 'Bearer {}'.format(token) + headers['Content-Type'] = 'application/json' + headers['accept'] = 'application/json' + + _url = "{0}/nslcm/v1/ns_instances/{1}/action".format(self._base_path, id) + return self._send_post(_url, headers=headers, + json=action_payload) + return None + + def vnfd_list(self): + token = self.get_token() + if token: + self._headers['Authorization'] = 'Bearer {}'.format(token) + self._headers['Content-Type'] = 'application/yaml' + self._headers['accept'] = 'application/json' + _url = "{0}/vnfpkgm/v1/vnf_packages_content".format(self._base_path) + return self._send_get(_url, headers=self._headers) + return None + + def vnfd_get(self, id): + token = self.get_token() + if token: + self._headers['Authorization'] = 'Bearer {}'.format(token) + self._headers['Content-Type'] = 'application/yaml' + #self._headers['accept'] = 'application/yaml' + _url = "{0}/vnfpkgm/v1/vnf_packages/{1}/vnfd".format(self._base_path, id) + return yaml.load(self._send_get(_url, headers=self._headers)) + return None + + def vnfd_delete(self, id): + token = self.get_token() + if token: + self._headers['Authorization'] = 'Bearer {}'.format(token) + self._headers['Content-Type'] = 'application/yaml' + self._headers['accept'] = 'application/json' + _url = "{0}/vnfpkgm/v1/vnf_packages_content/{1}".format(self._base_path, id) + return self._send_delete(_url, headers=self._headers) + return None + + def vnfd_update(self, id, data): + token = self.get_token() + headers = {} + if token: + # get the package onboarded + tar_pkg = self.get_vnfd_pkg(id) + tarf = tarfile.open(fileobj=tar_pkg) + + tarf = self._descriptor_update(tarf, data) + headers['Authorization'] = 'Bearer {}'.format(token) + headers['Content-Type'] = 'application/gzip' + headers['accept'] = 'application/json' + headers['Content-File-MD5'] = self.md5(open('/tmp/' + tarf.getnames()[0] + ".tar.gz", 'rb')) + # headers['Content-File-MD5'] = self.md5(open("pippo.tar.gz", 'rb')) + + _url = "{0}/vnfpkgm/v1/vnf_packages/{1}/package_content".format(self._base_path, id) + return self._send_put(_url, headers=headers, data=open('/tmp/' + tarf.getnames()[0] + ".tar.gz", 'rb')) + # return self._send_put(_url, headers=headers, data=open("pippo.tar.gz", 'rb')) + + return None + + def vnfd_onboard(self, package): + token = self.get_token() + headers = {} + if token: + headers['Authorization'] = 'Bearer {}'.format(token) + headers['Content-Type'] = 'application/gzip' + headers['accept'] = 'application/json' + with open('/tmp/'+package.name, 'wb+') as destination: + for chunk in package.chunks(): + destination.write(chunk) + headers['Content-File-MD5'] = self.md5(open('/tmp/'+package.name, 'rb')) + _url = "{0}/vnfpkgm/v1/vnf_packages_content".format(self._base_path) + return self._send_post(_url, headers=headers, + data=open('/tmp/' + package.name, 'rb')) + return None + def vnf_packages_artifacts(self, id): + token = self.get_token() + if token: + self._headers['Authorization'] = 'Bearer {}'.format(token) + self._headers['Content-Type'] = 'application/yaml' + _url = "{0}/vnfpkgm/v1/vnf_packages/{1}/artifacts".format(self._base_path, id) + return self._send_get(_url, headers=self._headers) + return None + + def _upload_package(self, filename, package): + token = self.get_token() + headers = {} + if token: + headers['Authorization'] = 'Bearer {}'.format(token) + headers['Content-Type'] = 'application/gzip' + headers['Content-File-MD5'] = self.md5(package) + headers['accept'] = 'application/json' + return None + + def _send_post(self, url, data=None, json=None, **kwargs): + try: + r = requests.post(url, data=data, json=json, verify=False, **kwargs) + #print r.text + except Exception as e: + log.exception(e) + #print "Exception during send POST" + return {'error': 'error during connection to agent'} + return Util.json_loads_byteified(r.text) + + def _send_put(self, url, data=None, json=None, **kwargs): + try: + r = requests.put(url, data=data, json=json, verify=False, **kwargs) + print r.text + except Exception as e: + log.exception(e) + #print "Exception during send PUT" + return {'error': 'error during connection to agent'} + + return r.json + + def _send_get(self, url, params=None, **kwargs): + try: + r = requests.get(url, params=None, verify=False, stream=True, **kwargs) + #print r.headers + except Exception as e: + log.exception(e) + #print "Exception during send GET" + return {'error': 'error during connection to agent'} + if 'accept' in kwargs['headers']: + accept = kwargs['headers']['accept'] + if accept == 'application/json': + #print "json" + return Util.json_loads_byteified(r.text) + elif accept == 'application/zip': + tarf =StringIO.StringIO(r.content) + #tarf = tarfile.open(fileobj=StringIO.StringIO(r.content)) + # for tarinfo in tarf: + # #print(tarinfo.name, "is", tarinfo.size, "bytes in size and is") + # if tarinfo.isreg(): + # #print("a regular file.") + # elif tarinfo.isdir(): + # #print("a directory.") + # else: + # #print("something else.") + return tarf + else: + return r.text + else: + return r.text + + def _send_delete(self, url, params=None, **kwargs): + try: + r = requests.delete(url, params=None, verify=False, **kwargs) + len(r.content) + print r.text + except Exception as e: + log.exception(e) + print "Exception during send DELETE" + return {'error': 'error during connection to agent'} + return r.json + + def md5(self, f): + hash_md5 = hashlib.md5() + for chunk in iter(lambda: f.read(1024), b""): + hash_md5.update(chunk) + return hash_md5.hexdigest() + + def get_nsd_pkg(self, id): + token = self.get_token() + if token: + self._headers['Authorization'] = 'Bearer {}'.format(token) + #self._headers['Content-Type'] = 'application/yaml' + self._headers['accept'] = 'application/zip' + _url = "{0}/nsd/v1/ns_descriptors/{1}/nsd_content".format(self._base_path, id) + return self._send_get(_url, headers=self._headers) + return None + + def get_vnfd_pkg(self, id): + token = self.get_token() + if token: + self._headers['Authorization'] = 'Bearer {}'.format(token) + #self._headers['Content-Type'] = 'application/yaml' + self._headers['accept'] = 'application/zip' + _url = "{0}/vnfpkgm/v1/vnf_packages/{1}/package_content".format(self._base_path, id) + return self._send_get(_url, headers=self._headers) + return None + + + +if __name__ == '__main__': + + + client = Client() + package = client.get_nsd_pkg('be489dfb-5f15-48c1-b693-67d830c591e5') + tarf = tarfile.open(fileobj=package) + tarf.extractall('/tmp') + yaml_object = yaml.safe_dump({}, default_flow_style=False) + yaml_file = open('/tmp/cirros_2vnf_ns/cirros_2vnf_nsd.yaml', 'w') + yaml_object = pyaml.dump(yaml_object, yaml_file, safe=True) + tarf_temp = tarfile.open(tarf.getnames()[0]+".tar.gz", "w:gz") + + for tarinfo in tarf: + tarf_temp.add('/tmp/'+tarinfo.name, tarinfo.name) + tarf_temp.close() + + + diff --git a/lib/parser.py b/lib/parser.py new file mode 100644 index 0000000..1b764d2 --- /dev/null +++ b/lib/parser.py @@ -0,0 +1,46 @@ +# +# Copyright 2017 CNIT - Consorzio Nazionale Interuniversitario per le Telecomunicazioni +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import json +import pyaml +import yaml +from lib.util import Util +import logging +import traceback +import glob +import os + + +class Parser(object): + """Parser methods base class + + """ + + def __init__(self): + pass + + @classmethod + def importprojectdir(cls,dir_project, type): + """Imports all files under a given folder + + Returns an empty project + """ + + project = {} + return project + + def get_all_ns_descriptors(self, nsd_id, project_data): + raise NotImplementedError \ No newline at end of file diff --git a/lib/rdcl_graph.py b/lib/rdcl_graph.py new file mode 100644 index 0000000..9d96251 --- /dev/null +++ b/lib/rdcl_graph.py @@ -0,0 +1,82 @@ +# +# Copyright 2017 CNIT - Consorzio Nazionale Interuniversitario per le Telecomunicazioni +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import json +import logging +import copy + +logging.basicConfig(level=logging.DEBUG) +log = logging.getLogger('RdclGraph') + + +class RdclGraph(object): + """ Operates on the graph representation used for the GUI graph views """ + + node_t3d_base = { + 'info': { + 'property': { + 'custom_label': '', + }, + 'type': '', + 'group': [] + } + } + + def __init__(self): + pass + + def add_link(self, source, target, view, group, graph_object, optional={}): + if (source is None) or (target is None): + return + edge_obj = { + 'source': source, + 'target': target, + 'view': view, + 'group': [group], + + } + + edge_obj.update(optional) + if edge_obj not in graph_object['edges']: + graph_object['edges'].append(edge_obj) + + def add_node(self, id, type, group, positions, graph_object, optional={}): + if id is None: + return + node = next((x for x in graph_object['vertices'] if x['id'] == id), None) + if node is not None: + node['info']['group'].append(group) + else: + node = copy.deepcopy(self.node_t3d_base) + node['id'] = id + node['info']['type'] = type + if group is not None: + node['info']['group'].append(group) + if positions and id in positions['vertices'] and 'x' in positions['vertices'][id] and 'y' in positions['vertices'][id]: + node['fx'] = positions['vertices'][id]['x'] + node['fy'] = positions['vertices'][id]['y'] + node['info'].update(optional) + graph_object['vertices'].append(node) + + def is_directed_edge(self, source_type=None, target_type=None, layer=None, model={}): + if source_type is None or target_type is None or layer is None: + return None + if layer in model['layer'] and 'allowed_edges' in model['layer'][layer]: + if source_type in model['layer'][layer]['allowed_edges'] and target_type in model['layer'][layer]['allowed_edges'][source_type]['destination']: + edge_pro = model['layer'][layer]['allowed_edges'][source_type]['destination'][target_type] + return edge_pro['direct_edge'] if 'direct_edge' in edge_pro else False + + return None diff --git a/lib/util.py b/lib/util.py new file mode 100644 index 0000000..71fc36b --- /dev/null +++ b/lib/util.py @@ -0,0 +1,198 @@ +# +# Copyright 2017 CNIT - Consorzio Nazionale Interuniversitario per le Telecomunicazioni +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import json +import yaml +import pyaml +import logging +import jsonschema +import uuid + +_lib_name = 'Util' + + +logging.basicConfig(level=logging.DEBUG) +log = logging.getLogger('lib/util.py') + + +class Util(object): + + def __init__(self): + # logging.basicConfig(level=logging.DEBUG) + # self.log = logging.getLogger('UtilLogger') + pass + + + @classmethod + def json_load_byteified(cls, file_handle): + return cls._byteify( + json.load(file_handle, object_hook=cls._byteify), + ignore_dicts=True + ) + + @classmethod + def json_loads_byteified(cls, json_text): + return cls._byteify( + json.loads(json_text, object_hook=cls._byteify), + ignore_dicts=True + ) + + @classmethod + def _byteify(cls, data, ignore_dicts = False): + # if this is a unicode string, return its string representation + if isinstance(data, unicode): + return data.encode('utf-8') + # if this is a list of values, return list of byteified values + if isinstance(data, list): + return [ cls._byteify(item, ignore_dicts=True) for item in data ] + # if this is a dictionary, return dictionary of byteified keys and values + # but only if we haven't already byteified it + if isinstance(data, dict) and not ignore_dicts: + return { + cls._byteify(key, ignore_dicts=True): cls._byteify(value, ignore_dicts=True) + for key, value in data.iteritems() + } + # if it's anything else, return it in its original form + return data + + @classmethod + def yaml2json(cls, object_yaml): + """Converts a yaml object into a json representation""" + log.debug('yaml2json') + return json.dumps(object_yaml, sort_keys=True, indent=2) if not object_yaml is None else None + + @classmethod + def json2yaml(cls, object_json): + """Converts a json object into a yaml representation""" + log.debug('json2yaml') + return yaml.safe_dump(object_json, default_flow_style=False) if not object_json is None else None + + @classmethod + def openfile(cls, filepath, mode='r', buffering=1): + """Returns an open file given a filepath + + If the filepath is already an open file, returns into + Raises Exception + """ + + log.debug('reading file ' + filepath) + try: + if isinstance(filepath, file): + return filepath + else: + return open(filepath, mode, buffering) + + except IOError as e: + log.exception('openfile', e) + raise + + @classmethod + def loadyamlfile(cls, name): + """Returns a yaml object from a filename or an open file + + Raises Exception + """ + + yaml_object = None + try: + if isinstance(name, file): + yaml_object = yaml.load(name) + else: + yaml_file = cls.openfile(name) + yaml_object = yaml.load(yaml_file) + + return yaml_object + except Exception as e: + log.exception('Exception loadYamlFile', e) + raise + + @classmethod + def loadjsonfile(cls, name): + """Returns a json object from a filename or an open file + + Raises Exception + """ + + json_object = None + try: + #raise IOError('error from throws') + if isinstance(name, file): + json_object = json.load(name) + else: + # json_file = self.openfile(name) + json_file = cls.openfile(name) + json_object = json.load(json_file) + + return json_object + except Exception as e: + log.exception('Exception loadJsonFile', e) + raise + + @classmethod + def writejsonfile(cls, name, json_object): + """Writes the dump of a json obj to a filename or an open file + + Raises Exception + """ + + try: + log.debug('writejsonfile ' + name) + if isinstance(name, file): + json_object = json.dump(json_object, name) + else: + json_file = cls.openfile(name, 'w') + json_object = json.dump(json_object, json_file,separators=(',',': '), indent=4) + except Exception as e: + log.exception('Exception writejsonfile', e) + raise + + @classmethod + def writeyamlfile(cls, name, yaml_object): + """Writes the dump of a yaml obj to a filename or an open file + + Raises Exception + """ + + try: + log.debug('writeyamlfile ' + name) + if isinstance(name, file): + yaml_object = pyaml.dump(yaml_object, name, safe=True) + else: + yaml_file = cls.openfile(name, 'w') + yaml_object = pyaml.dump(yaml_object, yaml_file, safe=True) + except Exception as e: + log.exception('Exception writeyamlfile') + raise + + @classmethod + def validate_json_schema(cls, reference_schema, data): + """Validates a json data against a json schema + + Raises Exception + """ + + try: + # schema = cls.loadjsonfile("lib/etsi/schemas/"+type_descriptor+".json") + #print 'type_descriptor : '+type_descriptor + jsonschema.validate(data, reference_schema) + return True + except Exception as e: + log.exception('Exception validate json schema', e) + return False + + @classmethod + def get_unique_id(cls): + return uuid.uuid4().hex[:6].upper() diff --git a/manage.py b/manage.py new file mode 100755 index 0000000..3f3c655 --- /dev/null +++ b/manage.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "sf_t3d.settings") + + from django.core.management import execute_from_command_line + + execute_from_command_line(sys.argv) diff --git a/projecthandler/__init__.py b/projecthandler/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/projecthandler/admin.py b/projecthandler/admin.py new file mode 100644 index 0000000..d85d57a --- /dev/null +++ b/projecthandler/admin.py @@ -0,0 +1,19 @@ +# +# Copyright 2017 CNIT - Consorzio Nazionale Interuniversitario per le Telecomunicazioni +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an BASIS, +# WITHOUT 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 django.contrib import admin + +# Register your models here. diff --git a/projecthandler/apps.py b/projecthandler/apps.py new file mode 100644 index 0000000..b71d1e7 --- /dev/null +++ b/projecthandler/apps.py @@ -0,0 +1,21 @@ +# +# Copyright 2017 CNIT - Consorzio Nazionale Interuniversitario per le Telecomunicazioni +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an BASIS, +# WITHOUT 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 django.apps import AppConfig + + +class ProjecthandlerConfig(AppConfig): + name = 'projecthandler' diff --git a/projecthandler/management/__init__.py b/projecthandler/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/projecthandler/management/commands/__init__.py b/projecthandler/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/projecthandler/management/commands/delete_project_type.py b/projecthandler/management/commands/delete_project_type.py new file mode 100644 index 0000000..d750f9f --- /dev/null +++ b/projecthandler/management/commands/delete_project_type.py @@ -0,0 +1,14 @@ +from django.core.management.base import BaseCommand, CommandError + + +class Command(BaseCommand): + help = 'Delete a project type' + + def handle(self, *args, **options): + + try: + print 'delete project type' + except Exception: + raise CommandError('Error unable to delete a new project type') + + self.stdout.write(self.style.SUCCESS('Project type successfully deleted')) diff --git a/projecthandler/management/commands/new_project_type.py b/projecthandler/management/commands/new_project_type.py new file mode 100644 index 0000000..7c9dce9 --- /dev/null +++ b/projecthandler/management/commands/new_project_type.py @@ -0,0 +1,14 @@ +from django.core.management.base import BaseCommand, CommandError + + +class Command(BaseCommand): + help = 'Create a new project type' + + def handle(self, *args, **options): + + try: + print 'new project type' + except Exception: + raise CommandError('Error unable to create a new project type') + + self.stdout.write(self.style.SUCCESS('New project type successfully created')) diff --git a/projecthandler/models.py b/projecthandler/models.py new file mode 100644 index 0000000..66fb7a5 --- /dev/null +++ b/projecthandler/models.py @@ -0,0 +1,333 @@ +# +# Copyright 2017 CNIT - Consorzio Nazionale Interuniversitario per le Telecomunicazioni +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from __future__ import unicode_literals + +from django.db import models +from django.utils import timezone +import jsonfield +from StringIO import StringIO +import zipfile +import json +import yaml +from lib.util import Util +from model_utils.managers import InheritanceManager +import logging +import os, shutil + +logging.basicConfig(level=logging.DEBUG) +log = logging.getLogger('models.py') + +project_types = {} + + +class Project(models.Model): + """ Base class for project types + + data_project stores a validated JSON representation of the project + get_dataproject() method returns the python dict representation of the project + + + """ + owner = models.ForeignKey('sf_user.CustomUser', db_column='owner') + name = models.CharField(max_length=20, default='') + created_date = models.DateTimeField(default=timezone.now) + updated_date = models.DateTimeField(default=timezone.now, blank=True, null=True) + info = models.TextField(default='No info') + data_project = jsonfield.JSONField(default={}) + """Stores a validated JSON representation of the project""" + + validated = models.BooleanField(default=False) + + #InheritanceManager + objects = InheritanceManager() + + + @classmethod + def get_project_types(cls): + global project_types + return project_types + + @classmethod + def add_project_type(cls, type, my_class): + global project_types + project_types [type]= my_class + + + @classmethod + def create_project(cls, name, user, validated, info, data_project): + project = cls.objects.create(name=name, owner=user, validated=False, info=info, + data_project=data_project) + return project + + @classmethod + def get_graph_model(cls, file_path): + """Returns the model of the graph of the project type as a yaml object + + Returns an empty dict if there is no file with the model + """ + # file_path = GRAPH_MODEL_FULL_NAME + graph_model = {} + try: + graph_model = Util.loadyamlfile(file_path) + except Exception as e: + log.exception(e) + pass + return graph_model + + def get_type(self): + return "Base" + + #@classmethod + def get_dataproject(self): + """ Return the python dict representation of the project data + + """ + #current_data = json.loads(self.data_project) + current_data = Util.json_loads_byteified(self.data_project) + + return current_data + + #@classmethod + def get_overview_data(self): + result = { + 'owner': self.owner, + 'name': self.name, + 'updated_date': self.updated_date, + 'info': self.info, + 'validated': self.validated + } + + return result + + def set_data_project(self, new_data, validated): + self.data_project = new_data + self.set_validated(validated) + self.update() + + def update(self): + self.updated_date = timezone.now() + self.save() + + def __str__(self): + return self.name + + def edit_graph_positions(self, positions): + # print positions + try: + current_data = json.loads(self.data_project) + if 'positions' not in current_data: + current_data['positions'] = {} + if 'vertices' not in current_data['positions']: + current_data['positions']['vertices'] = {} + if 'vertices' in positions: + current_data['positions']['vertices'].update(positions['vertices']) + self.data_project = current_data + self.update() + result = True + except Exception as e: + log.debug(e) + result = False + return result + + def get_descriptors(self, type_descriptor): + """Returns all descriptors of a given type""" + + try: + current_data = json.loads(self.data_project) + result = current_data[type_descriptor] + except Exception as e: + log.debug(e) + result = {} + return result + + def get_descriptor(self, descriptor_id, type_descriptor): + """Returns a specific descriptor""" + + try: + current_data = json.loads(self.data_project) + result = current_data[type_descriptor][descriptor_id] + print descriptor_id, type_descriptor, result + except Exception as e: + log.debug(e) + result = {} + + return result + + def delete_descriptor(self, type_descriptor, descriptor_id): + try: + log.debug('delete descriptor'+ descriptor_id + ' ' + type_descriptor) + current_data = json.loads(self.data_project) + del (current_data[type_descriptor][descriptor_id]) + self.data_project = current_data + self.update() + result = True + except Exception as e: + log.debug(e) + result = False + return result + + def clone_descriptor(self, type_descriptor, descriptor_id, new_id): + try: + current_data = json.loads(self.data_project) + descriptor = current_data[type_descriptor][descriptor_id] + new_descriptor = self.get_clone_descriptor(descriptor, type_descriptor, new_id) + current_data[type_descriptor][new_id] = new_descriptor + self.data_project = current_data + self.update() + result = True + except Exception as e: + log.debug(e) + result = False + return result + + def edit_descriptor(self, type_descriptor, descriptor_id, new_data, data_type): + try: + + ##FIXME questa parte va completamente rivista cosi' ha varie lacune + #log.info('editing ',+ descriptor_id + ' ' + type_descriptor + ' ' + data_type) + current_data = json.loads(self.data_project) + new_descriptor = new_data + if data_type == 'json': + new_descriptor = json.loads(new_data) + elif data_type == 'yaml': + yaml_object = yaml.load(new_data) + new_descriptor = json.loads(Util.yaml2json(yaml_object)) + if type_descriptor != 'click' and type_descriptor != 'oshi' and type_descriptor !='cran': + reference_schema = self.get_json_schema_by_type(type_descriptor) + Util.validate_json_schema(reference_schema, new_descriptor) + current_data[type_descriptor][descriptor_id] = new_descriptor + self.data_project = current_data + self.update() + result = True + except Exception as e: + log.debug(e) + result = False + return result + + def get_zip_archive(self): + in_memory = StringIO() + try: + current_data = json.loads(self.data_project) + zip = zipfile.ZipFile(in_memory, "w", zipfile.ZIP_DEFLATED) + for desc_type in current_data: + for current_desc in current_data[desc_type]: + zip.writestr(current_desc + '.json', json.dumps(current_data[desc_type][current_desc])) + + zip.close() + except Exception as e: + log.debug(e) + + in_memory.flush() + return in_memory + + def get_positions(self): + """Returns the positions of nodes""" + try: + current_data = json.loads(self.data_project) + positions = {} + if 'positions' in current_data: + positions = current_data['positions'] + except Exception as e: + log.debug(e) + + return positions + + def get_deployment_descriptor(self, **kwargs): + """Returns the deployment descriptor""" + raise NotImplementedError + + def get_node_overview(self, **kwargs): + """Returns the node overview""" + raise NotImplementedError + + def get_all_ns_descriptors(self, nsd_id): + raise NotImplementedError + + def translate_push_ns_on_repository(self, translator, nsd_id, repository, **kwargs): + raise NotImplementedError + + +class ProjectStateless(Project): + + def get_descriptors(self, type_descriptor): + """Returns all descriptors of a given type""" + raise NotImplementedError + + def delete_descriptor(self, type_descriptor, descriptor_id): + raise NotImplementedError + + def get_all_ns_descriptors(self, nsd_id): + pass + + def translate_push_ns_on_repository(self, translator, nsd_id, repository, **kwargs): + pass + + def get_deployment_descriptor(self, **kwargs): + pass + + def get_node_overview(self, **kwargs): + pass + + def get_dataproject(self): + raise NotImplementedError + + def get_overview_data(self): + raise NotImplementedError + +class Repository(models.Model): + """ Repository + """ + name = models.CharField(max_length=20, default='') + base_url = models.TextField(default='') + last_update = models.DateTimeField(default=timezone.now) + DIR_NAME = "/tmp/git_repo/" + + def fetch_repository(self): + """ + :return: git.remote.FetchInfo object + """ + if os.path.isdir(self.DIR_NAME): + shutil.rmtree(self.DIR_NAME) + + os.mkdir(self.DIR_NAME) + repo = git.Repo.init(self.DIR_NAME) + origin = repo.create_remote('origin', self.base_url) + origin.fetch() + fetch_info = origin.pull('master')[0] + return fetch_info + + def push_repository(self, msg=None): + """ + :param msg: Commit message + :return: git.remote.PushInfo object + """ + repo = git.Repo.init(self.DIR_NAME) + origin = repo.remote('origin') + repo.git.add('--all') + repo.git.commit('-m \'[RDCL3D commit] ' + msg + '\'') + push_info = origin.push('master')[0] + return push_info + + def to_json(self): + """ + :return: JSON data of object + """ + return { + 'name': self.name, + 'base_url': self.base_url.rstrip('\/'), + 'last_update': self.last_update + } diff --git a/projecthandler/osm_model.py b/projecthandler/osm_model.py new file mode 100644 index 0000000..4776010 --- /dev/null +++ b/projecthandler/osm_model.py @@ -0,0 +1,328 @@ +# +# Copyright 2017 CNIT - Consorzio Nazionale Interuniversitario per le Telecomunicazioni +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from __future__ import unicode_literals + +import copy +import json +import os.path +import yaml +from lib.util import Util +import logging +from projecthandler.models import ProjectStateless + +from lib.osm.osm_parser import OsmParser +from lib.osm.osm_rdcl_graph import OsmRdclGraph +from lib.osm.osmclient.client import Client + +logging.basicConfig(level=logging.DEBUG) +log = logging.getLogger('OsmModel.py') + + +PATH_TO_SCHEMAS = 'lib/osm/schemas/' +PATH_TO_DESCRIPTORS_TEMPLATES = 'lib/osm/descriptor_template' +DESCRIPTOR_TEMPLATE_SUFFIX = '.json' +GRAPH_MODEL_FULL_NAME = 'lib/TopologyModels/osm/osm.yaml' +EXAMPLES_FOLDER = 'usecases/OSM/' + + +class OsmProject(ProjectStateless): + """Osm Project class + The data model has the following descriptors: + # descrtiptor list in comment # + + """ + + def get_descriptors(self, type_descriptor): + """Returns all descriptors of a given type""" + log.debug("Get %s descriptors", type_descriptor) + try: + client = Client() + if type_descriptor == 'nsd': + result = client.nsd_list() + + elif type_descriptor == 'vnfd': + result = client.vnfd_list() + + except Exception as e: + log.exception(e) + result = {} + return result + + def get_descriptor(self, descriptor_id, type_descriptor): + """Returns a specific descriptor""" + try: + client = Client() + if type_descriptor == 'nsd': + result = client.nsd_get(descriptor_id) + print result + elif type_descriptor == 'vnfd': + result = client.vnfd_get(descriptor_id) + + print result + except Exception as e: + log.exception(e) + result = {} + + return result + + @classmethod + def data_project_from_files(cls, request): + + file_dict = {} + for my_key in request.FILES.keys(): + file_dict[my_key] = request.FILES.getlist(my_key) + + log.debug(file_dict) + + data_project = OsmParser.importprojectfiles(file_dict) + + return data_project + + @classmethod + def data_project_from_example(cls, request): + osm_id = request.POST.get('example-osm-id', '') + data_project = OsmParser.importprojectdir(EXAMPLES_FOLDER + osm_id + '/JSON', 'yaml') + return data_project + + @classmethod + def get_example_list(cls): + """Returns a list of directories, in each directory there is a project osm""" + + path = EXAMPLES_FOLDER + dirs = [d for d in os.listdir(path) if os.path.isdir(os.path.join(path, d))] + return {'osm': dirs} + + @classmethod + def get_new_descriptor(cls, descriptor_type, request_id): + + json_template = cls.get_descriptor_template(descriptor_type) + + return json_template + + @classmethod + def get_descriptor_template(cls, type_descriptor): + """Returns a descriptor template for a given descriptor type""" + + try: + schema = Util.loadjsonfile(os.path.join(PATH_TO_DESCRIPTORS_TEMPLATES, type_descriptor + DESCRIPTOR_TEMPLATE_SUFFIX)) + return schema + except Exception as e: + log.exception(e) + return False + + @classmethod + def get_clone_descriptor(cls, descriptor, type_descriptor, new_descriptor_id): + new_descriptor = copy.deepcopy(descriptor) + + return new_descriptor + + def get_type(self): + return "osm" + + def __str__(self): + return self.name + + def get_overview_data(self): + current_data = json.loads(self.data_project) + client = Client() + nsd = client.nsd_list() + vnfd = client.vnfd_list() + ns = client.ns_list() + result = { + 'owner': self.owner.__str__(), + 'name': self.name, + 'updated_date': self.updated_date.strftime('%Y-%m-%d %H:%M'), + 'info': self.info, + 'type': 'osm', + 'nsd': len(nsd) if nsd else 0, + 'vnfd': len(vnfd) if vnfd else 0, + 'ns': len(ns) if ns else 0, + 'validated': self.validated + } + + return result + + def get_graph_data_json_topology(self, descriptor_id): + rdcl_graph = OsmRdclGraph() + project = self.get_dataproject() + topology = rdcl_graph.build_graph_from_project(project, + model=self.get_graph_model(GRAPH_MODEL_FULL_NAME)) + return json.dumps(topology) + + def create_descriptor(self, descriptor_name, type_descriptor, new_data, data_type, file_uploaded): + """Creates a descriptor of a given type from a json or yaml representation + + Returns the descriptor id or False + """ + log.debug('Create descriptor') + + try: + client = Client() + if type_descriptor == 'nsd': + result = client.nsd_onboard(file_uploaded) + elif type_descriptor == 'vnfd': + result = client.vnfd_onboard(file_uploaded) + + else: + log.debug('Create descriptor: Unknown data type') + return False + + except Exception as e: + log.exception(e) + result = False + return result + + def delete_descriptor(self, type_descriptor, descriptor_id): + log.debug('Delete descriptor') + try: + client = Client() + if type_descriptor == 'nsd': + result = client.nsd_delete(descriptor_id) + elif type_descriptor == 'vnfd': + result = client.vnfd_delete(descriptor_id) + + else: + log.debug('Create descriptor: Unknown data type') + return False + + except Exception as e: + log.exception(e) + result = False + return result + + def edit_descriptor(self, type_descriptor, descriptor_id, new_data, data_type): + log.debug("Edit descriptor") + try: + client = Client() + if type_descriptor == 'nsd': + if data_type == 'yaml': + new_data = yaml.load(new_data) + elif data_type == 'json': + new_data = json.loads(new_data) + result = client.nsd_update(descriptor_id, new_data) + elif type_descriptor == 'vnfd': + if data_type == 'yaml': + new_data = yaml.load(new_data) + elif data_type == 'json': + new_data = json.loads(new_data) + result = client.vnfd_update(descriptor_id, new_data) + + else: + log.debug('Create descriptor: Unknown data type') + return False + except Exception as e: + log.exception(e) + result = False + return result + + def get_package_files_list(self, type_descriptor, descriptor_id): + try: + client = Client() + if type_descriptor == 'nsd': + result = client.nsd_artifacts(descriptor_id) + elif type_descriptor == 'vnfd': + result = client.vnf_packages_artifacts(descriptor_id) + else: + return False + result = yaml.load(result) + print result + except Exception as e: + log.exception(e) + result = False + print result + return result + + def set_validated(self, value): + self.validated = True if value is not None and value == True else False + + def get_add_element(self, request): + result = False + + return result + + def get_remove_element(self, request): + result = False + + return result + + def get_add_link(self, request): + + result = False + + return result + + def get_remove_link(self, request): + result = False + + return result + + def create_ns(self, descriptor_type, descriptor_id, data_ns): + try: + client = Client() + if descriptor_type == 'nsd': + result = client.ns_create( data_ns) + else: + return False + + except Exception as e: + log.exception(e) + result = False + print result + return result + + def download_pkg(self, project, descriptor_id, descriptor_type): + try: + client = Client() + if descriptor_type == 'nsd': + result = client.get_nsd_pkg(descriptor_id) + elif descriptor_type == 'vnfd': + result = client.get_vnfd_pkg(descriptor_id) + else: + return False + + except Exception as e: + log.exception(e) + result = False + print result + return result + + def get_available_nodes(self, args): + """Returns all available node """ + log.debug('get_available_nodes') + try: + result = [] + #current_data = json.loads(self.data_project) + model_graph = self.get_graph_model(GRAPH_MODEL_FULL_NAME) + for node in model_graph['layer'][args['layer']]['nodes']: + + current_data = { + "id": node, + "category_name": model_graph['nodes'][node]['label'], + "types": [ + { + "name": "generic", + "id": node + } + ] + } + result.append(current_data) + + #result = current_data[type_descriptor][descriptor_id] + except Exception as e: + log.debug(e) + result = [] + return result diff --git a/projecthandler/template/project/descriptor/descriptor_view_base.html b/projecthandler/template/project/descriptor/descriptor_view_base.html new file mode 100644 index 0000000..3635d66 --- /dev/null +++ b/projecthandler/template/project/descriptor/descriptor_view_base.html @@ -0,0 +1,131 @@ +{% extends "base.html" %} + +{% load staticfiles %} + +{% block head_block %} + {{ block.super }} + + + + + + + + + +{% endblock %} + + + + +{% block content_body %} + {{ block.super }} +
    +
    + + +
    +
    +{% endblock %} + +{% block resource_block %} + {{ block.super }} + + + + + + + + + + + + + + + + + + + + +{% endblock %} diff --git a/projecthandler/template/project/descriptor/modal/choose_node_id.html b/projecthandler/template/project/descriptor/modal/choose_node_id.html new file mode 100644 index 0000000..3bfea9e --- /dev/null +++ b/projecthandler/template/project/descriptor/modal/choose_node_id.html @@ -0,0 +1,27 @@ + \ No newline at end of file diff --git a/projecthandler/template/project/modal/modal_keyboard_info_base.html b/projecthandler/template/project/modal/modal_keyboard_info_base.html new file mode 100644 index 0000000..1986d5d --- /dev/null +++ b/projecthandler/template/project/modal/modal_keyboard_info_base.html @@ -0,0 +1,37 @@ + + \ No newline at end of file diff --git a/projecthandler/template/project/new_project.html b/projecthandler/template/project/new_project.html new file mode 100644 index 0000000..44fb435 --- /dev/null +++ b/projecthandler/template/project/new_project.html @@ -0,0 +1,129 @@ +{% extends "base.html" %} + +{% load staticfiles %} + +{% block head_block %} +{{ block.super }} + + + +{% endblock %} + +{% block left_sidebar %} + {% include 'left_sidebar_base.html' %} +{% endblock %} + +{% block breadcrumb_body %} +{{ block.super }} +
  • New Project
  • +{% endblock %} + +{% block content_body %} +{{ block.super }} +
    +
    + +
    +
    +

    New Project

    +
    + + +
    + {% csrf_token %} +
    +
    +
    +

    Project Type

    + + + +
    +
    +

    Project Name

    + +
    +
    +

    Info

    + +
    + + +
    +
    + +
    +
    +
    + {% autoescape off %}{{ type_container_template }}{% endautoescape %} +
    +
    + + +
    + +
    +
    + + +
    +
    +{% endblock %} + +{% block resource_block %} +{{ block.super }} + + + + + +{% endblock %} + diff --git a/projecthandler/template/project/osm/descriptor/descriptor_new.html b/projecthandler/template/project/osm/descriptor/descriptor_new.html new file mode 100644 index 0000000..d257d7f --- /dev/null +++ b/projecthandler/template/project/osm/descriptor/descriptor_new.html @@ -0,0 +1,155 @@ +{% extends "descriptor/descriptor_view_base.html" %} + +{% load staticfiles %} +{% block head_base %} + {% with skin_css="AdminLTE/dist/css/skins/skin-purple.min.css"%} + {{ block.super }} + {% endwith %} +{% endblock %} + +{% block body %} + {% with skin="purple"%} + {{ block.super }} + {% endwith %} +{% endblock %} +{% block logo_sidebar %} + {% with logo_mini="assets/img/osm_small_logo.png" logo="assets/img/OSM-logo.png"%} + {{ block.super }} + {% endwith %} +{% endblock %} + +{% block title_header_big %} +{{ block.super }} +Onboard new {{ descriptor_type }} package +{% endblock %} + +{% block left_sidebar %} +{% include 'osm/osm_project_left_sidebar.html' %} +{% endblock %} + +{% block breadcrumb_body %} +{{ block.super }} +
  • {{project_overview_data.name}}
  • +
  • {{ descriptor_type }} Descriptors
  • +
  • Create Descriptor
  • +{% endblock %} + +{% block tab_pane_button_list %} +{{ block.super }} +
  • Onboard Package
  • +{% endblock %} + +{% block nav_buttons_list %} +{{ block.super }} +
  • + +
  • +{% endblock %} + +{% block tab_pane_list %} +{{ block.super }} + +
    + +

    Drag and drop file below

    +
    + Just drag and drop files here +
    +
    +{% endblock %} + + +{% block resource_block %} +{{ block.super }} + + +{% endblock %} + +{% block footer %} + {% include "footer.html" %} +{% endblock %} diff --git a/projecthandler/template/project/osm/descriptor/descriptor_view.html b/projecthandler/template/project/osm/descriptor/descriptor_view.html new file mode 100644 index 0000000..5a082de --- /dev/null +++ b/projecthandler/template/project/osm/descriptor/descriptor_view.html @@ -0,0 +1,126 @@ +{% extends "descriptor/descriptor_view_base.html" %} + +{% load staticfiles %} + + +{% block title_header_big %} +{{ block.super }} +Edit {{ descriptor_type|upper }} Descriptor +{% endblock %} + + +{% block left_sidebar %} +{% include 'osm/osm_project_left_sidebar.html' %} +{% endblock %} + +{% block breadcrumb_body %} +{{ block.super }} +
  • {{project_overview_data.name}}
  • +
  • {{ descriptor_type }} Descriptors
  • +
  • {{descriptor_id}}
  • +{% endblock %} + +{% block tab_pane_button_list %} +{{ block.super }} +
  • YAML
  • +
  • JSON
  • +{% endblock %} + +{% block nav_buttons_list %} + {{ block.super }} +
  • +
  • +
  • +{% endblock %} + +{% block tab_pane_list %} +{{ block.super }} + + +
    + +
    + + +
    + +
    + +{% endblock %} + +{% block resource_block %} +{{ block.super }} + + + +{% endblock %} + + +{% block footer %} + {% include "footer.html" %} +{% endblock %} diff --git a/projecthandler/template/project/osm/descriptor/descriptorlist.html b/projecthandler/template/project/osm/descriptor/descriptorlist.html new file mode 100644 index 0000000..56ee44d --- /dev/null +++ b/projecthandler/template/project/osm/descriptor/descriptorlist.html @@ -0,0 +1,57 @@ +{% load staticfiles %} +{% load get %} +
    + +
    + +
    + + + + + + + + + + + + + {% for k in descriptors %} + + + + + + + + + {% endfor %} + + +
    IdNameDescriptionVendorVersionActions
    {{ k|get:"_id" }}{{ k|get:"short-name" }} {{ k|get:"description" }} {{ k|get:"vendor" }} {{ k|get:"version" }} +
    + {% if descriptor_type == "nsd" %} + + {% endif %} + + + + + +
    + +
    +
    +
    +
    diff --git a/projecthandler/template/project/osm/descriptor/modal/choose_node_id.html b/projecthandler/template/project/osm/descriptor/modal/choose_node_id.html new file mode 100644 index 0000000..91c4d20 --- /dev/null +++ b/projecthandler/template/project/osm/descriptor/modal/choose_node_id.html @@ -0,0 +1 @@ +{% extends "descriptor/modal/choose_node_id.html" %} \ No newline at end of file diff --git a/projecthandler/template/project/osm/descriptor/modal/create_link_chooser.html b/projecthandler/template/project/osm/descriptor/modal/create_link_chooser.html new file mode 100644 index 0000000..ed9a4a5 --- /dev/null +++ b/projecthandler/template/project/osm/descriptor/modal/create_link_chooser.html @@ -0,0 +1,21 @@ + \ No newline at end of file diff --git a/projecthandler/template/project/osm/modal/files_list.html b/projecthandler/template/project/osm/modal/files_list.html new file mode 100644 index 0000000..13105fe --- /dev/null +++ b/projecthandler/template/project/osm/modal/files_list.html @@ -0,0 +1,33 @@ + diff --git a/projecthandler/template/project/osm/modal/modal_keyboard_info.html b/projecthandler/template/project/osm/modal/modal_keyboard_info.html new file mode 100644 index 0000000..bf39e69 --- /dev/null +++ b/projecthandler/template/project/osm/modal/modal_keyboard_info.html @@ -0,0 +1 @@ +{% extends "modal/modal_keyboard_info_base.html" %} \ No newline at end of file diff --git a/projecthandler/template/project/osm/osm_new_project.html b/projecthandler/template/project/osm/osm_new_project.html new file mode 100644 index 0000000..cfdec29 --- /dev/null +++ b/projecthandler/template/project/osm/osm_new_project.html @@ -0,0 +1,25 @@ + + + \ No newline at end of file diff --git a/projecthandler/template/project/osm/osm_project_delete.html b/projecthandler/template/project/osm/osm_project_delete.html new file mode 100644 index 0000000..50bf685 --- /dev/null +++ b/projecthandler/template/project/osm/osm_project_delete.html @@ -0,0 +1,36 @@ +{% extends "project_delete.html" %} + +{% block head_base %} + {% with skin_css="AdminLTE/dist/css/skins/skin-purple.min.css"%} + {{ block.super }} + {% endwith %} +{% endblock %} + +{% block body %} + {% with skin="purple"%} + {{ block.super }} + {% endwith %} +{% endblock %} +{% block logo_sidebar %} + {% with logo_mini="assets/img/osm_small_logo.png" logo="assets/img/OSM-logo.png"%} + {{ block.super }} + {% endwith %} +{% endblock %} + +{% block head_block %} + {{ block.super }} + +{% endblock %} + +{% block left_sidebar %} + {% include 'osm/osm_project_left_sidebar.html' %} +{% endblock %} + +{% block breadcrumb_body %} + {{ block.super }} + +{% endblock %} + +{% block footer %} + {% include "footer.html" %} +{% endblock %} \ No newline at end of file diff --git a/projecthandler/template/project/osm/osm_project_descriptors.html b/projecthandler/template/project/osm/osm_project_descriptors.html new file mode 100644 index 0000000..30ea76a --- /dev/null +++ b/projecthandler/template/project/osm/osm_project_descriptors.html @@ -0,0 +1,121 @@ +{% extends "project_descriptors.html" %} +{% load staticfiles %} + + +{% block head_block %} + {{ block.super }} + +{% endblock %} + +{% block title_header_big %} + {{ block.super }} + {% if descriptor_type %} + {{ descriptor_type|upper }} + {% else%} + Undefined + {% endif %} Packages +{% endblock %} + +{% block left_sidebar %} + {% include 'osm/osm_project_left_sidebar.html' %} +{% endblock %} + +{% block breadcrumb_body %} + {{ block.super }} +
  • {{project_overview_data.name}}
  • +
  • {% if descriptor_type %} + {{ descriptor_type }} + {% else%} + Undefined + {% endif %} Packages
  • + +{% endblock %} + +{% block content_body %} + {{ block.super }} +{% include 'osm/modal/files_list.html' %} +{% include 'modal/instance_create.html' %} +
    + {% include "osm/descriptor/descriptorlist.html" %} +
    +{% endblock %} + + + + +{% block resource_block %} + {{ block.super }} + + +{% endblock %} + +{% block footer %} + {% include "footer.html" %} +{% endblock %} diff --git a/projecthandler/template/project/osm/osm_project_details.html b/projecthandler/template/project/osm/osm_project_details.html new file mode 100644 index 0000000..ab90385 --- /dev/null +++ b/projecthandler/template/project/osm/osm_project_details.html @@ -0,0 +1,79 @@ +{% extends "project_details.html" %} +{% block head_base %} + {{ block.super }} +{% endblock %} + +{% block body %} + {{ block.super }} +{% endblock %} +{% block logo_sidebar %} + {{ block.super }} +{% endblock %} + +{% block left_sidebar %} + {% include 'osm/osm_project_left_sidebar.html' %} +{% endblock %} + +{% block content_body %} + +
    + +
    +
    +
    +
    +

    {{ project_overview.nsd }}

    +

    NS packages

    +
    + Open list
    +
    +
    +
    +

    {{ project_overview.vnfd }}

    +

    VNF packages

    +
    + Open list
    +
    +
    +
    +
    +
    +

    {{ project_overview.ns }}

    +

    NS Instances

    +
    + Open list
    +
    + +
    + +
    +
    + +{% endblock %} + + +{% block footer %} + {% include "footer.html" %} +{% endblock %} diff --git a/projecthandler/template/project/osm/osm_project_left_sidebar.html b/projecthandler/template/project/osm/osm_project_left_sidebar.html new file mode 100644 index 0000000..029ae11 --- /dev/null +++ b/projecthandler/template/project/osm/osm_project_left_sidebar.html @@ -0,0 +1,56 @@ + \ No newline at end of file diff --git a/projecthandler/template/project/osm/project_graph.html b/projecthandler/template/project/osm/project_graph.html new file mode 100644 index 0000000..15d2eeb --- /dev/null +++ b/projecthandler/template/project/osm/project_graph.html @@ -0,0 +1,61 @@ +{% extends "project_graph_base.html" %} + +{% load staticfiles %} +{% block head_base %} + {% with skin_css="AdminLTE/dist/css/skins/skin-purple.min.css"%} + {{ block.super }} + {% endwith %} +{% endblock %} + +{% block body %} + {% with skin="purple"%} + {{ block.super }} + {% endwith %} +{% endblock %} +{% block logo_sidebar %} + {% with logo_mini="assets/img/osm_small_logo.png" logo="assets/img/OSM-logo.png"%} + {{ block.super }} + {% endwith %} +{% endblock %} + +{% block title_header_big %} + {{ block.super }} +{% endblock %} + +{% block left_sidebar %} + {% include 'osm/osm_project_left_sidebar.html' %} +{% endblock %} + +{% block topology_toolbar %} + {{ block.super }} + {% include 'osm/topology_toolbar.html' %} +{% endblock %} + +{% block content_body %} + {{ block.super }} + {% csrf_token %} + + +{% include 'osm/descriptor/modal/create_link_chooser.html' %} +{% include 'osm/descriptor/modal/choose_node_id.html' %} +{% include 'osm/modal/modal_keyboard_info.html' %} +{% endblock %} + +{% block resource_block %} + {{ block.super }} + + + + + + + + +{% endblock %} + +{% block footer %} + {% include "footer.html" %} +{% endblock %} \ No newline at end of file diff --git a/projecthandler/template/project/osm/topology_toolbar.html b/projecthandler/template/project/osm/topology_toolbar.html new file mode 100644 index 0000000..9a1f528 --- /dev/null +++ b/projecthandler/template/project/osm/topology_toolbar.html @@ -0,0 +1,6 @@ +{% extends 'topology_toolbar.html' %} + +{% block topology_toolbar_buttons %} + {{ block.super }} + +{% endblock %} diff --git a/projecthandler/template/project/project_delete.html b/projecthandler/template/project/project_delete.html new file mode 100644 index 0000000..e6afc5e --- /dev/null +++ b/projecthandler/template/project/project_delete.html @@ -0,0 +1,73 @@ +{% extends "base.html" %} + +{% load staticfiles %} + +{% block head_block %} + {{ block.super }} + +{% endblock %} + +{% block left_sidebar %} + +{% endblock %} + +{% block breadcrumb_body %} + {{ block.super }} + {% if project_id %} +
  • {{project_overview_data.name}}
  • +
  • Delete Project
  • + {% endif %} +{% endblock %} + +{% block content_body %} + {{ block.super }} +
    + {% if project_id %} +
    +
    +
    + + +

    Warning

    +
    +
    +
    + {% csrf_token %} +
    +

    The Project {{project_name}} will be deleted!

    + +
    + +
    + +
    +
    +
    + {% endif %} +
    + +{% endblock %} + +{% block resource_block %} + {{ block.super }} + + + +{% endblock %} + diff --git a/projecthandler/template/project/project_descriptors.html b/projecthandler/template/project/project_descriptors.html new file mode 100644 index 0000000..6f7345e --- /dev/null +++ b/projecthandler/template/project/project_descriptors.html @@ -0,0 +1,54 @@ +{% extends "base.html" %} + +{% load staticfiles %} + +{% block head_block %} + {{ block.super }} + +{% endblock %} +{% block title_header_big %} + +{% endblock %} + +{% block left_sidebar %} + +{% endblock %} + +{% block breadcrumb_body %} + {{ block.super }} +
  • Projects
  • +{% endblock %} + +{% block content_body %} + {{ block.super }} +{% if alert_message %} + {% if alert_message.success %} +
    + +

    Success

    + {{alert_message.message}} +
    + {% endif %} + {% if not alert_message.success %} +
    + +

    Alert!

    + {{alert_message.message}} +
    + {% endif %} +{% endif %} + +{% endblock %} + + + + +{% block resource_block %} + {{ block.super }} + + + + + +{% endblock %} + diff --git a/projecthandler/template/project/project_details.html b/projecthandler/template/project/project_details.html new file mode 100644 index 0000000..e944e0d --- /dev/null +++ b/projecthandler/template/project/project_details.html @@ -0,0 +1,78 @@ +{% extends "base.html" %} + +{% load staticfiles %} + +{% block head_block %} + {{ block.super }} + +{% endblock %} + +{% block left_sidebar %} + {{ block.super }} + +{% endblock %} + +{% block title_header_big %} + {{ block.super }} + {{project_overview.name}} +{% endblock %} + +{% block title_header_small %} + {{ block.super }} + Overview +{% endblock %} + +{% block breadcrumb_body %} + {{ block.super }} +
  • Projects
  • +
  • {{project_overview.name}}
  • +{% endblock %} + +{% block content_body %} + {{ block.super }} + + +{% endblock %} + +{% block resource_block %} + {{ block.super }} + + +{% endblock %} + + diff --git a/projecthandler/template/project/project_graph_base.html b/projecthandler/template/project/project_graph_base.html new file mode 100644 index 0000000..7d344da --- /dev/null +++ b/projecthandler/template/project/project_graph_base.html @@ -0,0 +1,58 @@ +{% extends "base.html" %} + +{% load staticfiles %} + +{% block head_block %} + {{ block.super }} + + +{% endblock %} +{% block title_header_big %} + {{ block.super }} +{% endblock %} +{% block left_sidebar %} + +{% endblock %} + + +{% block breadcrumb_body %} + {{ block.super }} +
  • {{project_overview_data.name}}
  • +{% endblock %} + +{% block content_body %} + {{ block.super }} + {% csrf_token %} +
    +
    + {% block topology_toolbar %} + + + {% endblock %} +
    + +
    +
    +
    + + +{% endblock %} + +{% block resource_block %} + {{ block.super }} + + + + + + + + + + +{% endblock %} + + +{% block header %} +{% endblock %} + diff --git a/projecthandler/template/project/projectlist.html b/projecthandler/template/project/projectlist.html new file mode 100644 index 0000000..5fd3b9d --- /dev/null +++ b/projecthandler/template/project/projectlist.html @@ -0,0 +1,84 @@ +{% extends "base.html" %} + +{% load staticfiles %} + +{% block head_block %} + {{ block.super }} + +{% endblock %} +{% block title_header_big %} + {{ block.super }} +{% endblock %} +{% block left_sidebar %} + +{% include 'left_sidebar_base.html' %} + +{% endblock %} + + +{% block breadcrumb_body %} + {{ block.super }} +
  • Projects
  • +{% endblock %} + +{% block content_body %} + {{ block.super }} + {% csrf_token %} +
    +
    + +
    +
    +

    Projects

    + +
    +
    + + + + + + + + + + + + + + + + + {% for p in projects %} + + + + + + + + + + + + + {% endfor %} + +
    NameInfoTypeModification DateCreation DateOwner
    + {{ p.name }} + {{ p.info }}{{ p.get_type }}{{ p.updated_date }}{{ p.created_date }}{{ p.owner.username }} Delete
    +
    +
    +
    + +
    +{% endblock %} + +{% block resource_block %} + {{ block.super }} + + + +{% endblock %} \ No newline at end of file diff --git a/projecthandler/template/project/repository/repo_base_left_sidebar.html b/projecthandler/template/project/repository/repo_base_left_sidebar.html new file mode 100644 index 0000000..3367eeb --- /dev/null +++ b/projecthandler/template/project/repository/repo_base_left_sidebar.html @@ -0,0 +1,12 @@ +{% extends "left_sidebar_base.html" %} + +{% block left_sidebar_base %} +
  • REPOSITORY
  • + {% url "repos:repos_list" as repo_list_url %} +
  • Ropository Registered
  • + + {% block li_list %} + + + {% endblock %} +{% endblock %} diff --git a/projecthandler/template/project/repository/repo_create_modal.html b/projecthandler/template/project/repository/repo_create_modal.html new file mode 100644 index 0000000..6df53b5 --- /dev/null +++ b/projecthandler/template/project/repository/repo_create_modal.html @@ -0,0 +1,43 @@ + \ No newline at end of file diff --git a/projecthandler/template/project/repository/repo_list.html b/projecthandler/template/project/repository/repo_list.html new file mode 100644 index 0000000..1f0a8f3 --- /dev/null +++ b/projecthandler/template/project/repository/repo_list.html @@ -0,0 +1,120 @@ +{% extends "base.html" %} + +{% load staticfiles %} + +{% block head_block %} +{{ block.super }} + + +{% endblock %} + +{% block title_header_big %} +{{ block.super }} +{% endblock %} + +{% block left_sidebar %} + +{% include "repository/repo_base_left_sidebar.html" %} +{% endblock %} + + +{% block breadcrumb_body %} +{{ block.super }} + +{% endblock %} + +{% block content_body %} +{{ block.super }} +{% csrf_token %} +
    + +
    + +
    +
    +

    Registered Repository

    + +
    +
    + + + + + + + + + + + + {% for repo in repos %} + + + + + + + + + {% endfor %} + +
    NameUrlLast UpdateActions
    + {{ repo.name }} + + {{ repo.base_url }} + {{ repo.last_update }} +
    + + + +
    +
    +
    +
    +
    + +
    +{% include 'repository/repo_create_modal.html' %} + +{% endblock %} + +{% block resource_block %} +{{ block.super }} + + + +{% endblock %} \ No newline at end of file diff --git a/projecthandler/tests.py b/projecthandler/tests.py new file mode 100644 index 0000000..d334413 --- /dev/null +++ b/projecthandler/tests.py @@ -0,0 +1,19 @@ +# +# Copyright 2017 CNIT - Consorzio Nazionale Interuniversitario per le Telecomunicazioni +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an BASIS, +# WITHOUT 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 django.test import TestCase + +# Create your tests here. diff --git a/projecthandler/urls/__init__.py b/projecthandler/urls/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/projecthandler/urls/project.py b/projecthandler/urls/project.py new file mode 100644 index 0000000..61e4d4e --- /dev/null +++ b/projecthandler/urls/project.py @@ -0,0 +1,54 @@ +# +# Copyright 2017 CNIT - Consorzio Nazionale Interuniversitario per le Telecomunicazioni +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an BASIS, +# WITHOUT 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 django.conf.urls import url, include +from projecthandler import views + +urlpatterns = [ + url(r'^list/', views.user_projects, name='projects_list'), + url(r'^new/', views.create_new_project, name='new_project'), + url(r'^(?P\d+)/$', views.open_project, name='open_project'), + url(r'^(?P\d+)/instances/', include('instancehandler.urls', namespace='instances'), name='instances_base'), + url(r'^(?P\d+)/delete$', views.delete_project, name='delete_project'), + url(r'^(?P\d+)/graph(/$)', views.graph, name='graph_view'), + url(r'^(?P\d+)/graph/graph_data(/$)', views.graph_data, name='graph_data'), + url(r'^(?P\d+)/graph/graph_data/(?P[-\w]+)(/$)', views.graph_data, name='graph_data'), + url(r'^(?P\d+)/graph/positions$', views.graph_positions, name='graph_positions'), + url(r'^(?P\d+)/graph/unusedvnf/(?P\w+)(/$)', views.unused_vnf, name='unused_vnf'), + url(r'^(?P\d+)/graph/addelement$', views.add_element, name='addelement'), + url(r'^(?P\d+)/graph/overviewelement$', views.overviewelement, name='overviewelement'), + url(r'^(?P\d+)/graph/addnodetovnffg', views.add_node_to_vnffg, name='addnodetovnffg'), + url(r'^(?P\d+)/graph/removeelement$', views.remove_element, name='removeelement'), + url(r'^(?P\d+)/graph/addlink$', views.add_link, name='addlink'), + url(r'^(?P\d+)/graph/removelink$', views.remove_link, name='removelink'), + url(r'^(?P\d+)/graph/availablenodes', views.get_available_nodes, name='get_available_nodes'), + url(r'^(?P\d+)/descriptors/(?P\w+)(/$)', views.show_descriptors, name='list_descriptors'), + url(r'^(?P\d+)/descriptors/(?P\w+)/(?P[-\w]+)(/$)', + views.edit_descriptor, name='edit_descriptor'), + url(r'^(?P\d+)/descriptors/(?P\w+)/(?P[-\w]+)/delete$', + views.delete_descriptor, + name='delete_descriptor'), + url(r'^(?P\d+)/descriptors/(?P\w+)/(?P[-\w]+)/clone$', + views.clone_descriptor, + name='clone_descriptor'), + url(r'^(?P\d+)/descriptors/(?P\w+)/(?P[-\w]+)/action/(?P[-\w]+)', + views.custom_action, + name='custom_action'), + url(r'^(?P\d+)/descriptors/(?P\w+)/new$', views.new_descriptor, + name='new_descriptor'), + + +] \ No newline at end of file diff --git a/projecthandler/views.py b/projecthandler/views.py new file mode 100644 index 0000000..8c553a4 --- /dev/null +++ b/projecthandler/views.py @@ -0,0 +1,537 @@ +# +# Copyright 2017 CNIT - Consorzio Nazionale Interuniversitario per le Telecomunicazioni +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import json + +from django.contrib.auth.decorators import login_required +from django.http import HttpResponse, JsonResponse +from django.middleware.csrf import get_token +from django.shortcuts import render, redirect +from django.template.loader import render_to_string +from lib.util import Util +from sf_user.models import CustomUser +import tarfile + + +from projecthandler.osm_model import OsmProject +from projecthandler.models import Project + + +Project.add_project_type('osm', OsmProject) + + + +from projecthandler.models import Repository + +@login_required +def home(request): + return render(request, 'home.html', {}) + + +@login_required +def create_new_project(request): + if request.method == 'POST': + error_msgs = [] + user = CustomUser.objects.get(id=request.user.id) + name = request.POST.get('name', 'WithoutName') + info = request.POST.get('info', ' ') + type = request.POST.get('type', '') + start_from = request.POST.get('startfrom', 'scratch') + + project_types = Project.get_project_types() + if type in project_types: + project_class = project_types[type] + + else: + # FIXME this error is not handled + error_msgs.append('Project type undefined.') + return render(request, 'error.html', + {'error_msg': 'Error creating new project, project type undefined. Please retry.'}) + + try: + + if start_from == 'scratch': + print 'from scratch' + data_project = {} + + elif start_from == 'files': + print 'from files' + data_project = project_class.data_project_from_files(request) + + elif start_from == 'example': + print 'from example' + data_project = project_class.data_project_from_example(request) + + project = project_class.create_project(name, user, False, info, data_project) + # print project.get_dataproject() + + + except Exception as e: + print 'Error creating ' + type + ' project! Please retry.' + print e + return render(request, 'error.html', {'error_msg': 'Error creating ' + type + ' project! Please retry.'}) + return redirect('projects:open_project', project_id=project.id) + + elif request.method == 'GET': + csrf_token_value = get_token(request) + result = {} + data_type_selector = [{ + 'id': '-1', + 'text': 'Select an option' + }] + type_example_files = {} + type_container_template = '' + project_types = Project.get_project_types() + print "project_types", project_types.keys() + for type in project_types: + project_class = project_types[type] + type_example_files.update(project_class.get_example_list()) + data_type_selector.append({ + 'id': type, + 'text': type, + 'value': type + }) + type_container_template += render_to_string(type + '/' + type + '_new_project.html') + + result.update({'type_example_files': json.dumps(type_example_files)}) + result.update({'data_type_selector': json.dumps(data_type_selector)}) + result.update({'type_container_template': type_container_template}) + result.update({'csrf_token': csrf_token_value}) + return render(request, 'new_project.html', result) + + +@login_required +def user_projects(request): + csrf_token_value = get_token(request) + user = CustomUser.objects.get(id=request.user.id) + projects = Project.objects.filter(owner=user).select_subclasses() + + return render(request, 'projectlist.html', { + 'projects': list(projects), + 'csrf_token': csrf_token_value + }) + + +@login_required +def open_project(request, project_id=None): + try: + projects = Project.objects.filter(id=project_id).select_subclasses() + project_overview = projects[0].get_overview_data() + prj_token = project_overview['type'] + print request.COOKIES.keys() + return render(request, prj_token + '/' + prj_token + '_project_details.html', + {'project_overview': project_overview, 'project_id': project_id}) + + except Exception as e: + print e + return render(request, 'error.html', {'error_msg': 'Error open project! Please retry.'}) + + +@login_required +def delete_project(request, project_id=None): + if request.method == 'POST': + + try: + Project.objects.filter(id=project_id).delete() + return redirect('projects:projects_list') + except Exception as e: + print e + return render(request, 'error.html', {'error_msg': 'Error deleting Project.'}) + + elif request.method == 'GET': + try: + projects = Project.objects.filter(id=project_id).select_subclasses() + project_overview = projects[0].get_overview_data() + prj_token = project_overview['type'] + # example: 'etsi/etsi_project_delete.html' + print prj_token + '/' + prj_token + '_project_delete.html', project_overview['name'] + return render(request, prj_token + '/' + prj_token + '_project_delete.html', + {'project_id': project_id, 'project_name': project_overview['name']}) + + except Exception as e: + print e + return render(request, 'error.html', {'error_msg': 'Project not found.'}) + + + +@login_required +def show_descriptors(request, project_id=None, descriptor_type=None): + csrf_token_value = get_token(request) + projects = Project.objects.filter(id=project_id).select_subclasses() + project_overview = projects[0].get_overview_data() + prj_token = project_overview['type'] + + page = prj_token + '/' + prj_token + '_project_descriptors.html' + + return render(request, page, { + 'descriptors': projects[0].get_descriptors(descriptor_type), + 'project_id': project_id, + 'project_type': prj_token, + 'project_overview_data': project_overview, + "csrf_token_value": csrf_token_value, + 'descriptor_type': descriptor_type + }) + + +@login_required +def graph(request, project_id=None): + if request.method == 'GET': + csrf_token_value = get_token(request) + projects = Project.objects.filter(id=project_id).select_subclasses() + project_overview = projects[0].get_overview_data() + prj_token = project_overview['type'] + # example : 'etsi/project_graph.html' + return render(request, prj_token + '/project_graph.html', { + 'project_id': project_id, + 'project_overview_data': projects[0].get_overview_data(), + 'collapsed_sidebar': False + }) + + +@login_required +def graph_data(request, project_id=None, descriptor_id=None): + print 'graph_data', project_id, descriptor_id + projects = Project.objects.filter(id=project_id).select_subclasses() + project_overview = projects[0].get_overview_data() + # data = projects[0].get_overview_data() + prj_token = project_overview['type'] + + topology = projects[0].get_graph_data_json_topology(descriptor_id) + response = HttpResponse(topology, content_type="application/json") + response["Access-Control-Allow-Origin"] = "*" + + return response + + +@login_required +def delete_descriptor(request, project_id=None, descriptor_type=None, descriptor_id=None): + csrf_token_value = get_token(request) + projects = Project.objects.filter(id=project_id).select_subclasses() + result = projects[0].delete_descriptor(descriptor_type, descriptor_id) + project_overview = projects[0].get_overview_data() + prj_token = project_overview['type'] + page = prj_token + '/' + prj_token + '_project_descriptors.html' + + return render(request, page, { + 'descriptors': projects[0].get_descriptors(descriptor_type), + 'project_id': project_id, + 'project_overview_data': project_overview, + "csrf_token_value": csrf_token_value, + 'descriptor_type': descriptor_type, + #'alert_message': { + # 'success': result, + # 'message': "Delete succeeded!" if result else 'Error in delete'} + }) + + +@login_required +def clone_descriptor(request, project_id=None, descriptor_type=None, descriptor_id=None): + csrf_token_value = get_token(request) + projects = Project.objects.filter(id=project_id).select_subclasses() + new_id = request.GET.get('newid', '') + result = projects[0].clone_descriptor(descriptor_type, descriptor_id, new_id) + project_overview = projects[0].get_overview_data() + prj_token = project_overview['type'] + page = prj_token + '/' + prj_token + '_project_descriptors.html' + + return render(request, page, { + 'descriptors': projects[0].get_descriptors(descriptor_type), + 'project_id': project_id, + 'project_overview_data': project_overview, + "csrf_token_value": csrf_token_value, + 'descriptor_type': descriptor_type, + 'alert_message': { + 'success': result, + 'message': "Cloned!" if result else 'Error in cloning'} + }) + + +@login_required +def new_descriptor(request, project_id=None, descriptor_type=None): + projects = Project.objects.filter(id=project_id).select_subclasses() + project_overview = projects[0].get_overview_data() + prj_token = project_overview['type'] + page = prj_token + '/descriptor/descriptor_new.html' + if request.method == 'GET': + request_id = request.GET.get('id', '') + + json_template = projects[0].get_new_descriptor(descriptor_type, request_id) + print 'new descriptor GET', json_template + + descriptor_string_yaml = Util.json2yaml(json_template) + descriptor_string_json = json.dumps(json_template) + + return render(request, page, { + 'project_id': project_id, + 'descriptor_type': descriptor_type, + 'descriptor_id': request_id, + 'project_overview_data': project_overview, + 'descriptor_strings': {'descriptor_string_yaml': descriptor_string_yaml, + 'descriptor_string_json': descriptor_string_json} + }) + elif request.method == 'POST': + csrf_token_value = get_token(request) + data_type = request.POST.get('type') + print "TYPE", data_type + if data_type == "file": + file_uploaded = request.FILES['file'] + text = file_uploaded.read() + data_type = file_uploaded.name.split(".")[-1] + desc_name = file_uploaded.name.split(".")[0] + result = projects[0].create_descriptor(desc_name, descriptor_type, text, data_type, file_uploaded) + else: + text = request.POST.get('text') + desc_name = request.POST.get('id') + result = projects[0].create_descriptor(desc_name, descriptor_type, text, data_type) + + + response_data = { + 'project_id': project_id, + 'descriptor_type': descriptor_type, + 'project_overview_data': projects[0].get_overview_data(), + 'descriptor_id': result, + 'alert_message': { + 'success': True if result != False else False, + 'message': "Descriptor created" if result else 'Error in creation'} + } + status_code = 200 if result != False else 500 + response = HttpResponse(json.dumps(response_data), content_type="application/json", status=status_code) + response["Access-Control-Allow-Origin"] = "*" + return response + + +@login_required +def edit_descriptor(request, project_id=None, descriptor_id=None, descriptor_type=None): + if request.method == 'POST': + print "edit_descriptor" + projects = Project.objects.filter(id=project_id).select_subclasses() + result = projects[0].edit_descriptor(descriptor_type, descriptor_id, request.POST.get('text'), + request.POST.get('type')) + response_data = { + 'project_id': project_id, + 'descriptor_type': descriptor_type, + #'project_overview_data': projects[0].get_overview_data(), + 'alert_message': { + 'success': True if result else False, + 'message': "Descriptor modified." if result else 'Error during descriptor editing.'} + } + status_code = 200 if result else 500 + response = HttpResponse(json.dumps(response_data), content_type="application/json", status=status_code) + response["Access-Control-Allow-Origin"] = "*" + return response + + elif request.method == 'GET': + csrf_token_value = get_token(request) + projects = Project.objects.filter(id=project_id).select_subclasses() + project_overview = projects[0].get_overview_data() + print project_overview + prj_token = project_overview['type'] + page = prj_token + '/descriptor/descriptor_view.html' + + descriptor = projects[0].get_descriptor(descriptor_id, descriptor_type) + + descriptor_string_json = json.dumps(descriptor) + descriptor_string_yaml = Util.json2yaml(descriptor) + # print descriptor + return render(request, page, { + 'project_id': project_id, + 'descriptor_id': descriptor_id, + 'project_overview_data': projects[0].get_overview_data(), + 'descriptor_type': descriptor_type, + 'descriptor_strings': {'descriptor_string_yaml': descriptor_string_yaml, + 'descriptor_string_json': descriptor_string_json}}) + + +@login_required +def graph_positions(request, project_id=None): + if request.method == 'POST': + projects = Project.objects.filter(id=project_id).select_subclasses() + result = projects[0].edit_graph_positions(json.loads(request.POST.get('positions'))) + status_code = 200 if result else 500 + response = HttpResponse(json.dumps({}), content_type="application/json", status=status_code) + response["Access-Control-Allow-Origin"] = "*" + return response + + +@login_required +def add_element(request, project_id=None): + if request.method == 'POST': + projects = Project.objects.filter(id=project_id).select_subclasses() + result = projects[0].get_add_element(request) + + status_code = 200 if result else 500 + response = HttpResponse(json.dumps({}), content_type="application/json", status=status_code) + response["Access-Control-Allow-Origin"] = "*" + return response + + +@login_required +def remove_element(request, project_id=None): + if request.method == 'POST': + projects = Project.objects.filter(id=project_id).select_subclasses() + result = projects[0].get_remove_element(request) + + status_code = 200 if result else 500 + response = HttpResponse(json.dumps({}), content_type="application/json", status=status_code) + response["Access-Control-Allow-Origin"] = "*" + return response + + +@login_required +def add_link(request, project_id=None): + if request.method == 'POST': + projects = Project.objects.filter(id=project_id).select_subclasses() + result = projects[0].get_add_link(request) + + status_code = 200 if result else 500 + response = HttpResponse(json.dumps({}), content_type="application/json", status=status_code) + response["Access-Control-Allow-Origin"] = "*" + return response + + +@login_required +def remove_link(request, project_id=None): + if request.method == 'POST': + projects = Project.objects.filter(id=project_id).select_subclasses() + result = projects[0].get_remove_link(request) + + status_code = 200 if result else 500 + response = HttpResponse(json.dumps({}), content_type="application/json", status=status_code) + response["Access-Control-Allow-Origin"] = "*" + return response + +@login_required +def get_available_nodes(request, project_id=None): + if request.method == 'GET': + csrf_token_value = get_token(request) + projects = Project.objects.filter(id=project_id).select_subclasses() + print "get_available_nodes", request.GET.dict() + result = projects[0].get_available_nodes(request.GET.dict()) + status_code = 500 if result == None else 200 + print json.dumps(result) + response = HttpResponse(json.dumps(result), content_type="application/json", status=status_code) + response["Access-Control-Allow-Origin"] = "*" + return response + +@login_required +def overviewelement(request, project_id=None): + if request.method == 'GET': + result = {} + error_msg = None + try: + projects = Project.objects.filter(id=project_id).select_subclasses() + project = projects[0] + parameters = request.GET.dict() + print "parameters", parameters + result = project.get_node_overview(**parameters) + except Exception as e: + error_msg = str(e) + + if error_msg is not None: + return JsonResponse({'error': {'error_msg': str(error_msg)}}) + + return JsonResponse({'node_overview': result}) + +# ETSI specific method # +@login_required +def add_node_to_vnffg(request, project_id=None): + print "add_node_to_vnffg" # TODO log + if request.method == 'POST': + projects = Project.objects.filter(id=project_id).select_subclasses() + result = projects[0].add_node_to_vnffg(request) + + status_code = 200 if result else 500 + response = HttpResponse(json.dumps({}), content_type="application/json", status=status_code) + response["Access-Control-Allow-Origin"] = "*" + return response + + +@login_required +def unused_vnf(request, project_id=None, nsd_id=None): + if request.method == 'GET': + print 'in method unused_vnf : ', project_id, nsd_id # TODO log + projects = Project.objects.filter(id=project_id).select_subclasses() + result = projects[0].get_unused_vnf(nsd_id) + status_code = 500 if result == None else 200 + response = HttpResponse(json.dumps(result), content_type="application/json", status=status_code) + response["Access-Control-Allow-Origin"] = "*" + return response + +# end ETSI specific method # + + +# OSM specific method # +def get_package_files_list(request, project_id, project, descriptor_id, descriptor_type): + files_list = [] + try: + files_list = project.get_package_files_list(descriptor_type, descriptor_id) + result = {'files': files_list} + except Exception as e: + print e + url = 'error.html' + result = {'error_msg': 'Unknown error.'} + return __response_handler(request, result) + + +def download_pkg(request, project_id, project, descriptor_id, descriptor_type): + tar_pkg = project.download_pkg(project, descriptor_id, descriptor_type) + + response = HttpResponse(content_type="application/tgz") + response["Content-Disposition"] = "attachment; filename=osm_export.tar.gz" + response.write(tar_pkg.getvalue()) + return response + + +def create_ns(request, project_id, project, descriptor_id, descriptor_type): + files_list = [] + try: + ns_data={ + "nsName": request.POST.get('nsName', 'WithoutName'), + "nsDescription": request.POST.get('nsDescription', ''), + "nsdId": request.POST.get('nsdId', ''), + "vimAccountId": request.POST.get('vimAccountId', ''), + "ssh-authorized-key": [ + { + request.POST.get('key-pair-ref', ''): request.POST.get('keyValue', '') + } + ] + } + #result = project.create_ns(descriptor_type, descriptor_id, ns_data) + + except Exception as e: + print e + url = 'error.html' + result = {'error_msg': 'Unknown error.'} + return __response_handler(request, result) + +# end OSM specific method # + +@login_required +def custom_action(request, project_id=None, descriptor_id=None, descriptor_type=None, action_name=None): + if request.method == 'GET': + projects = Project.objects.filter(id=project_id).select_subclasses() + print "Custom action: " + action_name + return globals()[action_name](request, project_id, projects[0], descriptor_id, descriptor_type) + + +def __response_handler(request, data_res, url=None, to_redirect=None, *args, **kwargs): + raw_content_types = request.META.get('HTTP_ACCEPT', '*/*').split(',') + if 'application/json' in raw_content_types: + return JsonResponse(data_res) + elif to_redirect: + return redirect(url, *args, **kwargs) + else: + return render(request, url, data_res) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..99d35b5 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,16 @@ +decorator==4.0.10 +Django==1.10.1 +django-model-utils==2.6 +functools32==3.2.3.post2 +jsonfield==1.0.3 +jsonschema==2.5.1 +pbr==1.10.0 +pyaml==15.8.2 +pymongo==3.4.0 +python-dateutil==2.6.0 +PyYAML==3.12 +requests==2.12.4 +six==1.10.0 +smmap2==2.0.3 +url==0.2.0 +wheel==0.24.0 diff --git a/sf_t3d/__init__.py b/sf_t3d/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sf_t3d/context_processor.py b/sf_t3d/context_processor.py new file mode 100644 index 0000000..d554515 --- /dev/null +++ b/sf_t3d/context_processor.py @@ -0,0 +1,10 @@ +from django.conf import settings # import the settings file + + +def conf_constants(request): + # return the value you want as a dictionnary. you may add multiple values in there. + return { + 'SITE_NAME': settings.SITE_NAME, + 'SHORT_SITE_NAME': settings.SHORT_SITE_NAME, + 'VERSION': settings.VERSION, + } \ No newline at end of file diff --git a/sf_t3d/settings.py b/sf_t3d/settings.py new file mode 100644 index 0000000..1020acd --- /dev/null +++ b/sf_t3d/settings.py @@ -0,0 +1,168 @@ +""" +Django settings for sf_t3d project. + +Generated by 'django-admin startproject' using Django 1.9. + +For more information on this file, see +https://docs.djangoproject.com/en/1.9/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/1.9/ref/settings/ +""" + +import os + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'o5+o2jv(3-dqr(&ia#-@79cgr%xi*s+6xjws^8cxp211ge#buf' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + +AUTH_USER_MODEL = "sf_user.CustomUser" + +SITE_NAME = "OSM Light Weight" +SHORT_SITE_NAME = "OSM-LW-UI" + +LOGIN_URL = '/auth/' +LOGOUT_URL = '/auth/' + +VERSION = "0.0.1" + + +# Application definition +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'sf_user', + 'projecthandler', + 'vimhandler', + 'instancehandler' +] + +MIDDLEWARE_CLASSES = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + +] + +SESSION_ENGINE='sf_user.sessions' +SESSION_COOKIE_AGE = 3500 #25 min +SESSION_EXPIRE_AT_BROWSER_CLOSE = True +SESSION_SAVE_EVERY_REQUEST = True + +ROOT_URLCONF = 'sf_t3d.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [ + os.path.join(BASE_DIR, 'template'), + os.path.join(BASE_DIR, 'projecthandler', 'template'), + os.path.join(BASE_DIR, 'projecthandler', 'template', 'download'), + os.path.join(BASE_DIR, 'projecthandler', 'template', 'project'), + os.path.join(BASE_DIR, 'vimhandler', 'template'), + os.path.join(BASE_DIR, 'instancehandler', 'template'), + ], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + 'sf_t3d.context_processor.conf_constants', + ], + 'libraries':{ + 'get': 'sf_t3d.templatetags.get', + + } + }, + }, +] + +WSGI_APPLICATION = 'sf_t3d.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/1.9/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + } +} + + +# Password validation +# https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/1.9/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/1.9/howto/static-files/ + +STATIC_URL = '/static/' + +STATICFILES_FINDERS = ( + 'django.contrib.staticfiles.finders.FileSystemFinder', + 'django.contrib.staticfiles.finders.AppDirectoriesFinder', + 'django.contrib.staticfiles.finders.DefaultStorageFinder' +) + +STATICFILES_DIRS = ( + # Put strings here, like "/home/html/static" or "C:/www/django/static". + # Always use forward slashes, even on Windows. + # Don't forget to use absolute paths, not relative paths. + os.path.join(BASE_DIR, "static"), +) + +LOCALE_PATHS = ( + os.path.join(BASE_DIR, 'locale'), +) \ No newline at end of file diff --git a/sf_t3d/templatetags/__init__.py b/sf_t3d/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sf_t3d/templatetags/get.py b/sf_t3d/templatetags/get.py new file mode 100644 index 0000000..80d3695 --- /dev/null +++ b/sf_t3d/templatetags/get.py @@ -0,0 +1,37 @@ +# +# Copyright 2018 CNIT - Consorzio Nazionale Interuniversitario per le Telecomunicazioni +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an BASIS, +# WITHOUT 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 django import template +register = template.Library() + +''' + Custom template filter +''' +@register.filter +def get(mapping, key): + result = mapping.get(key, '') + return result + +''' + Custom template filter +''' +@register.filter +def get_sub(mapping, args): + splitted = args.split(',') + sub_dict = mapping.get(splitted[0], '') + if isinstance(sub_dict, dict): + return sub_dict.get(splitted[1], '') + return '' diff --git a/sf_t3d/urls.py b/sf_t3d/urls.py new file mode 100644 index 0000000..0784ed1 --- /dev/null +++ b/sf_t3d/urls.py @@ -0,0 +1,35 @@ +"""sf_t3d URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/1.9/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') +Including another URLconf + 1. Add an import: from blog import urls as blog_urls + 2. Import the include() function: from django.conf.urls import url, include + 3. Add a URL to urlpatterns: url(r'^blog/', include(blog_urls)) +""" +from django.conf.urls import url, include +from django.contrib import admin +from sf_user import views as user_views +from sf_t3d import views + +app_name = 'base' +urlpatterns = [ + url(r'^admin/', admin.site.urls), + url(r'^auth/$', user_views.login_view, name='auth_user'), + url(r'^auth_guest/$', user_views.guest_login, name='auth_user_guest'), + url(r'^register', user_views.register_view, name='register_user'), + url(r'^projects/', include('projecthandler.urls.project', namespace='projects'), name='projects_base'), + url(r'^vims/', include('vimhandler.urls', namespace='vim'), name='vims_base'), + + url(r'^$', views.home, name='home'), + url(r'^home', views.home, name='home'), + url(r'^forbidden', views.forbidden, name='forbidden'), + +] diff --git a/sf_t3d/views.py b/sf_t3d/views.py new file mode 100644 index 0000000..91590f0 --- /dev/null +++ b/sf_t3d/views.py @@ -0,0 +1,22 @@ +from django.shortcuts import render +from django.contrib.auth.decorators import login_required + +from projecthandler.models import Project +from sf_user.models import CustomUser + + +@login_required +def home(request): + user = CustomUser.objects.get(id=request.user.id) + projects = Project.objects.filter(owner=user).select_subclasses() + result = { + 'projects': len(projects) if projects else 0, + } + return render(request, 'home.html', result) + + +def forbidden(request): + return render(request, 'forbidden.html') + + + diff --git a/sf_t3d/wsgi.py b/sf_t3d/wsgi.py new file mode 100644 index 0000000..c52ee6f --- /dev/null +++ b/sf_t3d/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for sf_t3d project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/1.9/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "sf_t3d.settings") + +application = get_wsgi_application() diff --git a/sf_user/__init__.py b/sf_user/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sf_user/admin.py b/sf_user/admin.py new file mode 100644 index 0000000..2e9690a --- /dev/null +++ b/sf_user/admin.py @@ -0,0 +1,19 @@ +# +# Copyright 2018 CNIT - Consorzio Nazionale Interuniversitario per le Telecomunicazioni +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an BASIS, +# WITHOUT 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 django.contrib import admin + +# Register your models here. diff --git a/sf_user/apps.py b/sf_user/apps.py new file mode 100644 index 0000000..902aa7d --- /dev/null +++ b/sf_user/apps.py @@ -0,0 +1,21 @@ +# +# Copyright 2018 CNIT - Consorzio Nazionale Interuniversitario per le Telecomunicazioni +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an BASIS, +# WITHOUT 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 django.apps import AppConfig + + +class SfUserConfig(AppConfig): + name = 'sf_user' diff --git a/sf_user/management/__init__.py b/sf_user/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sf_user/management/commands/__init__.py b/sf_user/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sf_user/management/commands/clean_guest_data.py b/sf_user/management/commands/clean_guest_data.py new file mode 100644 index 0000000..0ff087b --- /dev/null +++ b/sf_user/management/commands/clean_guest_data.py @@ -0,0 +1,19 @@ +from django.core.management.base import BaseCommand, CommandError +from sf_user.models import CustomSession, CustomUser + + +class Command(BaseCommand): + help = 'Clean Guest Users Sessions' + + #def add_arguments(self, parser): + # parser.add_argument('poll_id', nargs='+', type=int) + + def handle(self, *args, **options): + #for guest in CustomUser.objects.filter(is_guest_user="True"): + # self.stdout.write(self.style.SUCCESS(guest.username)) + try: + CustomUser.objects.filter(is_guest_user="True").delete() + except Exception: + raise CommandError('Error unable to clean guest users sessions') + + self.stdout.write(self.style.SUCCESS('Successfully cleaned guest users sessions')) diff --git a/sf_user/management/commands/clearsessions.py b/sf_user/management/commands/clearsessions.py new file mode 100644 index 0000000..8717521 --- /dev/null +++ b/sf_user/management/commands/clearsessions.py @@ -0,0 +1,17 @@ +from django.core.management.base import BaseCommand, CommandError +from sf_user.models import CustomSession + + +class Command(BaseCommand): + help = 'Clean Users Sessions' + + #def add_arguments(self, parser): + # parser.add_argument('poll_id', nargs='+', type=int) + + def handle(self, *args, **options): + try: + CustomSession.objects.all().delete() + except Exception: + raise CommandError('Error unable to clean users sessions') + + self.stdout.write(self.style.SUCCESS('Successfully cleaned users sessions')) \ No newline at end of file diff --git a/sf_user/models.py b/sf_user/models.py new file mode 100644 index 0000000..6485e82 --- /dev/null +++ b/sf_user/models.py @@ -0,0 +1,195 @@ +# +# Copyright 2018 CNIT - Consorzio Nazionale Interuniversitario per le Telecomunicazioni +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from __future__ import unicode_literals + +from django.contrib.auth.models import ( + AbstractBaseUser, BaseUserManager, PermissionsMixin) +from django.utils import timezone +from django.utils.translation import ugettext_lazy as _ +from django.contrib.sessions.base_session import AbstractBaseSession +from django.db import models + +class CustomUserManager(BaseUserManager): + """Custom manager for CustomUser.""" + + def _create_user(self, username, password, is_staff, is_superuser, **extra_fields): + """Create and save a CustomUser with the given username and password. """ + now = timezone.now() + + if not username: + raise ValueError('The given username must be set') + + is_active = extra_fields.pop("is_active", True) + user = self.model(username=username, is_staff=is_staff, is_active=is_active, + is_superuser=is_superuser, last_login=now, + date_joined=now, **extra_fields) + user.set_password(password) + user.save(using=self._db) + return user + + """Create and save an CustomUser with the given username and password.""" + def create_superuser(self, username, password, **extra_fields): + return self._create_user(username, password, True, True, is_admin=True, + **extra_fields) + + """Create and save an FullOperator with the given email and password. """ + def create_full_operator(self, username, password=None, **extra_fields): + return self._create_user(username, password, False, False, is_full_operator=True, + **extra_fields) + + """Create and save an BasicUser with the given email and password. """ + def create_basic_user(self, username, password=None, **extra_fields): + return self._create_user(username, password, False, False, is_basic_user=True, + **extra_fields) + + """Create and save an GuestUser with the given email and password. """ + def create_guest_user(self, username, password="guest", **extra_fields): + return self._create_user(username, password, False, False, is_guest_user=True, + **extra_fields) + + +class AbstractCustomUser(AbstractBaseUser, PermissionsMixin): + """Abstract User with the same behaviour as Django's default User. + + AbstractCustomUser does not have username field. Uses email as the + USERNAME_FIELD for authentication. + + Use this if you need to extend EmailUser. + + Inherits from both the AbstractBaseUser and PermissionMixin. + + The following attributes are inherited from the superclasses: + * password + * last_login + * is_superuser + + """ + username = models.CharField(_('username'), max_length=255, unique=True, db_index=True) + + email = models.EmailField(_('email address'), max_length=255, unique=True) + + first_name = models.CharField(_('first name'), max_length=255, blank=True) + last_name = models.CharField(_('last name'), max_length=255, blank=True) + phone = models.CharField(_('phone'), max_length=100, blank=True) + # user_groups = models.ManyToManyField(UserGroup, verbose_name=_('user groups'), blank=True) + + is_admin = models.BooleanField(_('admin status'), default=False) + is_full_operator = models.BooleanField(_('full_operator status'), default=False) + is_basic_user = models.BooleanField(_('basic_user status'), default=False) + is_guest_user = models.BooleanField(_('guest_user status'), default=False) + + is_staff = models.BooleanField( + _('staff status'), default=False, help_text=_( + 'Designates whether the user can log into this admin site.')) + is_active = models.BooleanField(_('active'), default=True, help_text=_( + 'Designates whether this user should be treated as ' + 'active. Unselect this instead of deleting accounts.')) + + date_joined = models.DateTimeField(_('date joined'), default=timezone.now) + + objects = CustomUserManager() + + USERNAME_FIELD = 'username' + REQUIRED_FIELDS = ['email', 'first_name', 'last_name', 'phone', ] + + class Meta: + verbose_name = _('custom user') + verbose_name_plural = _('custom users') + abstract = True + + def get_full_name(self): + """Return the fullname.""" + return "%s %s" % (self.last_name, self.first_name) + + def get_short_name(self): + """Return the firstname.""" + return self.first_name + + +class CustomUser(AbstractCustomUser): + """ + Concrete class of AbstractCustomUser. + + Use this if you don't need to extend CustomUser. + + """ + + class Meta(AbstractCustomUser.Meta): + swappable = 'AUTH_USER_MODEL' + + def count_admins(self): + return CustomUser.objects.filter(is_admin=True).count() + + def count_employee(self): + return CustomUser.objects.filter(is_full_operator=True).count() + + def count_basic_users(self): + return CustomUser.objects.filter(is_basic_user=True).count() + + def count_inactives(self): + return CustomUser.objects.filter(is_active=False).count() + + def is_guest(self): + return self.is_guest_user + + def get_avatar(self): + if self.is_admin: + return "assets/img/employer.png" + elif self.is_full_operator: + return "assets/img/employer.png" + elif self.is_basic_user: + return "assets/img/employer.png" + elif self.is_guest_user: + return "assets/img/account_circle.png" + + def get_user_role(self): + if self.is_admin: + return 0, "Admin" + elif self.is_full_operator: + return 1, "Full operator" + elif self.is_basic_user: + return 2, "Basic user" + elif self.is_guest_user: + return 3, "Guest user" + + def get_user_role_name(self): + if self.is_admin: + return "Admin" + elif self.is_full_operator: + return "Full operator" + elif self.is_basic_user: + return "Basic user" + elif self.is_guest_user: + return "Guest user" + + def has_perm(self, perm, obj=None): + if perm == 'deploymenthandler': + if self.is_guest_user: + return False + else: + return True + else: + super.has_perm(perm, obj) + + +class CustomSession(AbstractBaseSession): + account_id = models.IntegerField(null=True, db_index=True) + + @classmethod + def get_session_store_class(cls): + return SessionStore + diff --git a/sf_user/sessions.py b/sf_user/sessions.py new file mode 100644 index 0000000..df4ed71 --- /dev/null +++ b/sf_user/sessions.py @@ -0,0 +1,32 @@ +# +# Copyright 2018 CNIT - Consorzio Nazionale Interuniversitario per le Telecomunicazioni +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an BASIS, +# WITHOUT 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 django.contrib.sessions.backends.db import SessionStore as DBStore +import sf_user + +class SessionStore(DBStore): + @classmethod + def get_model_class(cls): + return sf_user.models.CustomSession + + def create_model_instance(self, data): + obj = super(SessionStore, self).create_model_instance(data) + try: + account_id = int(data.get('_auth_user_id')) + except (ValueError, TypeError): + account_id = None + obj.account_id = account_id + return obj \ No newline at end of file diff --git a/sf_user/tests.py b/sf_user/tests.py new file mode 100644 index 0000000..79947e6 --- /dev/null +++ b/sf_user/tests.py @@ -0,0 +1,19 @@ +# +# Copyright 2018 CNIT - Consorzio Nazionale Interuniversitario per le Telecomunicazioni +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an BASIS, +# WITHOUT 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 django.test import TestCase + +# Create your tests here. diff --git a/sf_user/views.py b/sf_user/views.py new file mode 100644 index 0000000..fb33347 --- /dev/null +++ b/sf_user/views.py @@ -0,0 +1,89 @@ +# +# Copyright 2018 CNIT - Consorzio Nazionale Interuniversitario per le Telecomunicazioni +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an BASIS, +# WITHOUT 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 django.shortcuts import render +from django.contrib.auth import login, logout, authenticate +from django.http import HttpResponseRedirect +from sf_user.models import CustomUser +import urllib +import uuid + + +# Create your views here. +def login_view(request): + if hasattr(request.user, "is_guest_user") and request.user.is_guest_user == True: + print "is_guest", request.user.is_guest_user + CustomUser.objects.get(id=request.user.id).delete() + logout(request) + extra_data = {} + next_page = "" + if request.GET: + next_page = request.GET['next'] + error_message = '' + if request.POST: + print request.POST.get('username') + print request.POST.get('password') + next_page = request.POST.get('next') + next_page = urllib.unquote(next_page).decode('iso-8859-2') + user = authenticate(username=request.POST.get('username'), password=request.POST.get('password')) + print "Auth Result: " + str(user) + " -> " + str(user) + if user and user.is_active: + if user.is_authenticated(): + login(request, user) + print next_page + if next_page == "" or next_page is None: + return HttpResponseRedirect('/home') + else: + return HttpResponseRedirect(next_page) + else: + error_message = 'Login failed!' + return render(request, 'login.html', {'error_message':error_message, 'collapsed_sidebar': False}) + + +def guest_login(request): + #user = CustomUser.objects.get(id=request.user.id) + if hasattr(request.user, "is_guest_user") and request.user.is_guest_user == True: + CustomUser.objects.get(id=request.user.id).delete() + logout(request) + next = "" + + guest_user_name = "Guest_"+str(uuid.uuid4()) + guest_user_email = guest_user_name+"@guest.it" + guest_user = CustomUser.objects.create(username=guest_user_name, is_guest_user="True", email=guest_user_email, first_name='User', last_name='Guest') + print guest_user.username + + if guest_user and guest_user.is_active: + if guest_user.is_authenticated(): + login(request, guest_user) + if next == "": + return HttpResponseRedirect('/home') + else: + return HttpResponseRedirect(next) + + return render(request, 'login.html', {'error_message': 'New Guest session failed.'}) + + +def register_view(request): + + logout(request) + extra_data = {} + next = "" + if request.GET: + next = request.GET['next'] + error_message = '' + if request.POST: + print "new user" + return render(request, 'register_user.html', {'error_message': error_message, 'collapsed_sidebar': False}) \ No newline at end of file diff --git a/static/assets/img/OSM-logo.png b/static/assets/img/OSM-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..7de447ca5e4fe683f6964fb4a880d7880d02f5d1 GIT binary patch literal 22753 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