From 0f98af53b320c8244b58d0d8751e28e157949e8e Mon Sep 17 00:00:00 2001 From: tierno Date: Mon, 19 Mar 2018 10:28:22 +0100 Subject: [PATCH] Methods for managing VIMs, SDNs Change-Id: I34c3ac84c811dcfe1d71fe7e6fec0b820993d541 Signed-off-by: tierno --- Dockerfile | 2 +- osm_nbi/engine.py | 60 +++++++++---- osm_nbi/html_out.py | 7 +- osm_nbi/msgbase.py | 12 ++- osm_nbi/msgkafka.py | 47 ++++++++--- osm_nbi/msglocal.py | 84 +++++++++++-------- osm_nbi/nbi.cfg | 2 +- osm_nbi/nbi.py | 45 +++++++--- osm_nbi/test/test.py | 119 +++++++++++++++++++------- osm_nbi/validation.py | 190 ++++++++++++++++++++++++++++++++++++++++++ setup.py | 2 +- stdeb.cfg | 2 +- 12 files changed, 454 insertions(+), 118 deletions(-) create mode 100644 osm_nbi/validation.py diff --git a/Dockerfile b/Dockerfile index 29d8376..68c7850 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,7 @@ WORKDIR /app/osm_nbi # Copy the current directory contents into the container at /app ADD . /app -RUN apt-get update && apt-get -y install git python3 \ +RUN apt-get update && apt-get -y install git python3 python3-jsonschema \ python3-cherrypy3 python3-pymongo python3-yaml python3-pip \ && pip3 install aiokafka \ && mkdir -p /app/storage/kafka && mkdir -p /app/log diff --git a/osm_nbi/engine.py b/osm_nbi/engine.py index a15420f..e55c7ec 100644 --- a/osm_nbi/engine.py +++ b/osm_nbi/engine.py @@ -18,6 +18,7 @@ from msgbase import MsgException from http import HTTPStatus from time import time from copy import deepcopy +from validation import validate_input, ValidationError __author__ = "Alfonso Tierno " @@ -276,6 +277,11 @@ class Engine(object): elif item == "nsrs": pass + elif item == "vims" or item == "sdns": + if self.db.get_one(item, {"name": indata.get("name")}, fail_on_empty=False, fail_on_more=False): + raise EngineException("name '{}' already exist for {}".format(indata["name"], item), + HTTPStatus.CONFLICT) + def _format_new_data(self, session, item, indata, admin=None): now = time() @@ -306,6 +312,9 @@ class Engine(object): indata["_admin"]["onboardingState"] = "CREATED" indata["_admin"]["operationalState"] = "DISABLED" indata["_admin"]["usageSate"] = "NOT_IN_USE" + elif item in ("vims", "sdns"): + indata["_admin"]["operationalState"] = "PROCESSING" + if storage: indata["_admin"]["storage"] = storage indata["_id"] = _id @@ -500,25 +509,14 @@ class Engine(object): Creates a new entry into database. For nsds and vnfds it creates an almost empty DISABLED entry, that must be completed with a call to method upload_content :param session: contains the used login username and working project - :param item: it can be: users, projects, nsrs, nsds, vnfds + :param item: it can be: users, projects, vims, sdns, nsrs, nsds, vnfds :param indata: data to be inserted :param kwargs: used to override the indata descriptor :param headers: http request headers :return: _id, transaction_id: identity of the inserted data. or transaction_id if Content-Range is used """ - # TODO validate input. Check not exist at database - # TODO add admin and status transaction = None - # if headers.get("Content-Range") or "application/gzip" in headers.get("Content-Type") or \ - # "application/x-gzip" in headers.get("Content-Type") or "application/zip" in headers.get("Content-Type") or \ - # "text/plain" in headers.get("Content-Type"): - # if not indata: - # raise EngineException("Empty payload") - # transaction = self._new_item_partial(session, item, indata, headers) - # if "desc" not in transaction: - # return transaction["_id"], False - # indata = transaction["desc"] item_envelop = item if item in ("nsds", "vnfds"): @@ -552,6 +550,11 @@ class Engine(object): except IndexError: raise EngineException( "Invalid query string '{}'. Index '{}' out of range".format(k, kitem_old)) + try: + validate_input(content, item, new=True) + except ValidationError as e: + raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY) + if not indata and item not in ("nsds", "vnfds"): raise EngineException("Empty payload") @@ -568,6 +571,14 @@ class Engine(object): _id = self.db.create(item, content) if item == "nsrs": self.msg.write("ns", "create", _id) + elif item == "vims": + msg_data = self.db.get_one(item, {"_id": _id}) + msg_data.pop("_admin", None) + self.msg.write("vim_account", "create", msg_data) + elif item == "sdns": + msg_data = self.db.get_one(item, {"_id": _id}) + msg_data.pop("_admin", None) + self.msg.write("sdn", "create", msg_data) return _id def _add_read_filter(self, session, item, filter): @@ -645,7 +656,6 @@ class Engine(object): "", http_code=HTTPStatus.NOT_ACCEPTABLE) return self.fs.file_open((storage['folder'], storage['zipfile']), "rb"), "application/zip" - def get_item_list(self, session, item, filter={}): """ Get a list of items @@ -701,17 +711,20 @@ class Engine(object): filter = {"_id": _id} self._add_delete_filter(session, item, filter) - if item == "nsrs": + if item in ("nsrs", "vims", "sdns"): desc = self.db.get_one(item, filter) desc["_admin"]["to_delete"] = True self.db.replace(item, _id, desc) # TODO change to set_one - self.msg.write("ns", "delete", _id) - return {"deleted": 1} + if item == "nsrs": + self.msg.write("ns", "delete", _id) + elif item == "vims": + self.msg.write("vim_account", "delete", {"_id": _id}) + elif item == "sdns": + self.msg.write("sdn", "delete", {"_id": _id}) + return {"deleted": 1} # TODO indicate an offline operation to return 202 ACCEPTED v = self.db.del_one(item, filter) self.fs.file_delete(_id, ignore_non_exist=True) - if item == "nsrs": - self.msg.write("ns", "delete", _id) return v def prune(self): @@ -766,11 +779,22 @@ class Engine(object): except IndexError: raise EngineException( "Invalid query string '{}'. Index '{}' out of range".format(k, kitem_old)) + try: + validate_input(content, item, new=False) + except ValidationError as e: + raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY) _deep_update(content, indata) self._validate_new_data(session, item, content, id) # self._format_new_data(session, item, content) self.db.replace(item, id, content) + if item in ("vims", "sdns"): + indata.pop("_admin", None) + indata["_id"] = id + if item == "vims": + self.msg.write("vim_account", "edit", indata) + elif item == "sdns": + self.msg.write("sdn", "edit", indata) return id def edit_item(self, session, item, _id, indata={}, kwargs=None): diff --git a/osm_nbi/html_out.py b/osm_nbi/html_out.py index f37b5dd..23ee97b 100644 --- a/osm_nbi/html_out.py +++ b/osm_nbi/html_out.py @@ -109,9 +109,9 @@ def format(data, request, response, session): body += html_body_error.format(yaml.safe_dump(data, explicit_start=True, indent=4, default_flow_style=False)) elif isinstance(data, (list, tuple)): if request.path_info == "/vnfpkgm/v1/vnf_packages_content": - body += html_upload_body.format("VNFD", request.path_info) + body += html_upload_body.format(request.path_info, "VNFD") elif request.path_info == "/nsd/v1/ns_descriptors_content": - body += html_upload_body.format("NSD", request.path_info) + body += html_upload_body.format(request.path_info, "NSD") for k in data: if isinstance(k, dict): data_id = k.pop("_id", None) @@ -124,6 +124,9 @@ def format(data, request, response, session): else: body += ' '.format(request.path_info) body += "
" + yaml.safe_dump(data, explicit_start=True, indent=4, default_flow_style=False) + "
" + elif data is None: + if request.method == "DELETE" or "METHOD=DELETE" in request.query_string: + body += "
 deleted 
" else: body = str(data) user_text = " " diff --git a/osm_nbi/msgbase.py b/osm_nbi/msgbase.py index a105414..25e8c80 100644 --- a/osm_nbi/msgbase.py +++ b/osm_nbi/msgbase.py @@ -1,4 +1,5 @@ +import asyncio from http import HTTPStatus __author__ = "Alfonso Tierno " @@ -30,12 +31,17 @@ class MsgBase(object): def connect(self, config): pass - def write(self, msg): + def disconnect(self): pass - def read(self): + def write(self, topic, key, msg): pass - def disconnect(self): + def read(self, topic): pass + async def aiowrite(self, topic, key, msg, loop): + pass + + async def aioread(self, topic, loop): + pass diff --git a/osm_nbi/msgkafka.py b/osm_nbi/msgkafka.py index 90c9c7f..96456af 100644 --- a/osm_nbi/msgkafka.py +++ b/osm_nbi/msgkafka.py @@ -31,24 +31,36 @@ class MsgKafka(MsgBase): except Exception as e: # TODO refine raise MsgException(str(e)) + def disconnect(self): + try: + self.loop.close() + except Exception as e: # TODO refine + raise MsgException(str(e)) + def write(self, topic, key, msg): try: - self.loop.run_until_complete(self.aiowrite(topic=topic, key=key, msg=yaml.safe_dump(msg, default_flow_style=True))) + self.loop.run_until_complete(self.aiowrite(topic=topic, key=key, + msg=yaml.safe_dump(msg, default_flow_style=True), + loop=self.loop)) except Exception as e: raise MsgException("Error writing {} topic: {}".format(topic, str(e))) def read(self, topic): - #self.topic_lst.append(topic) + """ + Read from one or several topics. it is non blocking returning None if nothing is available + :param topic: can be str: single topic; or str list: several topics + :return: topic, key, message; or None + """ try: - return self.loop.run_until_complete(self.aioread(topic)) + return self.loop.run_until_complete(self.aioread(topic, self.loop)) + except MsgException: + raise except Exception as e: raise MsgException("Error reading {} topic: {}".format(topic, str(e))) - async def aiowrite(self, topic, key, msg, loop=None): + async def aiowrite(self, topic, key, msg, loop): try: - if not loop: - loop = self.loop self.producer = AIOKafkaProducer(loop=loop, key_serializer=str.encode, value_serializer=str.encode, bootstrap_servers=self.broker) await self.producer.start() @@ -58,15 +70,24 @@ class MsgKafka(MsgBase): finally: await self.producer.stop() - async def aioread(self, topic, loop=None): - if not loop: - loop = self.loop - self.consumer = AIOKafkaConsumer(loop=loop, bootstrap_servers=self.broker) - await self.consumer.start() - self.consumer.subscribe([topic]) + async def aioread(self, topic, loop): + """ + Asyncio read from one or several topics. It blocks + :param topic: can be str: single topic; or str list: several topics + :param loop: asyncio loop + :return: topic, key, message + """ try: + if isinstance(topic, (list, tuple)): + topic_list = topic + else: + topic_list = (topic,) + + self.consumer = AIOKafkaConsumer(loop=loop, bootstrap_servers=self.broker) + await self.consumer.start() + self.consumer.subscribe(topic_list) async for message in self.consumer: - return yaml.load(message.key), yaml.load(message.value) + return message.topic, yaml.load(message.key), yaml.load(message.value) except KafkaError as e: raise MsgException(str(e)) finally: diff --git a/osm_nbi/msglocal.py b/osm_nbi/msglocal.py index a19c5c3..337321f 100644 --- a/osm_nbi/msglocal.py +++ b/osm_nbi/msglocal.py @@ -3,9 +3,16 @@ import os import yaml import asyncio from msgbase import MsgBase, MsgException +from time import sleep __author__ = "Alfonso Tierno " +""" +This emulated kafka bus by just using a shared file system. Usefull for testing or devops. +One file is used per topic. Only one producer and one consumer is allowed per topic. Both consumer and producer +access to the same file. e.g. same volume if running with docker. +One text line per message is used in yaml format +""" class MsgLocal(MsgBase): @@ -14,6 +21,7 @@ class MsgLocal(MsgBase): self.path = None # create a different file for each topic self.files = {} + self.buffer = {} def connect(self, config): try: @@ -41,55 +49,63 @@ class MsgLocal(MsgBase): Insert a message into topic :param topic: topic :param key: key text to be inserted - :param msg: value object to be inserted + :param msg: value object to be inserted, can be str, object ... :return: None or raises and exception """ try: if topic not in self.files: self.files[topic] = open(self.path + topic, "a+") - yaml.safe_dump({key: msg}, self.files[topic], default_flow_style=True) + yaml.safe_dump({key: msg}, self.files[topic], default_flow_style=True, width=20000) self.files[topic].flush() except Exception as e: # TODO refine raise MsgException(str(e)) - def read(self, topic): + def read(self, topic, blocks=True): + """ + Read from one or several topics. it is non blocking returning None if nothing is available + :param topic: can be str: single topic; or str list: several topics + :param blocks: indicates if it should wait and block until a message is present or returns None + :return: topic, key, message; or None if blocks==True + """ try: - msg = "" - if topic not in self.files: - self.files[topic] = open(self.path + topic, "a+") - # ignore previous content - for line in self.files[topic]: - if not line.endswith("\n"): - msg = line - msg += self.files[topic].readline() - if not msg.endswith("\n"): - return None - msg_dict = yaml.load(msg) - assert len(msg_dict) == 1 - for k, v in msg_dict.items(): - return k, v + if isinstance(topic, (list, tuple)): + topic_list = topic + else: + topic_list = (topic, ) + while True: + for single_topic in topic_list: + if single_topic not in self.files: + self.files[single_topic] = open(self.path + single_topic, "a+") + self.buffer[single_topic] = "" + self.buffer[single_topic] += self.files[single_topic].readline() + if not self.buffer[single_topic].endswith("\n"): + continue + msg_dict = yaml.load(self.buffer[single_topic]) + self.buffer[single_topic] = "" + assert len(msg_dict) == 1 + for k, v in msg_dict.items(): + return single_topic, k, v + if not blocks: + return None + sleep(2) except Exception as e: # TODO refine raise MsgException(str(e)) - async def aioread(self, topic, loop=None): + async def aioread(self, topic, loop): + """ + Asyncio read from one or several topics. It blocks + :param topic: can be str: single topic; or str list: several topics + :param loop: asyncio loop + :return: topic, key, message + """ try: - msg = "" - if not loop: - loop = asyncio.get_event_loop() - if topic not in self.files: - self.files[topic] = open(self.path + topic, "a+") - # ignore previous content - for line in self.files[topic]: - if not line.endswith("\n"): - msg = line while True: - msg += self.files[topic].readline() - if msg.endswith("\n"): - break + msg = self.read(topic, blocks=False) + if msg: + return msg await asyncio.sleep(2, loop=loop) - msg_dict = yaml.load(msg) - assert len(msg_dict) == 1 - for k, v in msg_dict.items(): - return k, v + except MsgException: + raise except Exception as e: # TODO refine raise MsgException(str(e)) + diff --git a/osm_nbi/nbi.cfg b/osm_nbi/nbi.cfg index d208367..e75643c 100644 --- a/osm_nbi/nbi.cfg +++ b/osm_nbi/nbi.cfg @@ -26,7 +26,7 @@ server.ssl_module: "builtin" server.ssl_certificate: "./http/cert.pem" server.ssl_private_key: "./http/privkey.pem" server.ssl_pass_phrase: "osm4u" -server.thread_pool: 10 +server.thread_pool: 1 # Only for test. It works without authorization using the provided user and project: # test.user_not_authorized: "admin" diff --git a/osm_nbi/nbi.py b/osm_nbi/nbi.py index 99f90aa..1cd61d5 100644 --- a/osm_nbi/nbi.py +++ b/osm_nbi/nbi.py @@ -10,6 +10,7 @@ import logging from engine import Engine, EngineException from dbbase import DbException from fsbase import FsException +from msgbase import MsgException from base64 import standard_b64decode #from os import getenv from http import HTTPStatus @@ -18,7 +19,7 @@ from codecs import getreader from os import environ __author__ = "Alfonso Tierno " -__version__ = "0.2" +__version__ = "0.3" version_date = "Mar 2018" """ @@ -66,6 +67,10 @@ URL: /osm GET POST / O O /projects O O / O O + /vims O O + / O O O + /sdns O O + / O O O query string. [....]*[.]=[,...]&... @@ -123,14 +128,20 @@ class Server(object): self.valid_methods = { # contains allowed URL and methods "admin": { "v1": { - "tokens": { "METHODS": ("GET", "POST", "DELETE"), + "tokens": {"METHODS": ("GET", "POST", "DELETE"), "": { "METHODS": ("GET", "DELETE")} }, - "users": { "METHODS": ("GET", "POST"), + "users": {"METHODS": ("GET", "POST"), "": {"METHODS": ("GET", "POST", "DELETE")} }, - "projects": { "METHODS": ("GET", "POST"), - "": {"METHODS": ("GET", "POST", "DELETE")} + "projects": {"METHODS": ("GET", "POST"), + "": {"METHODS": ("GET", "DELETE")} + }, + "vims": {"METHODS": ("GET", "POST"), + "": {"METHODS": ("GET", "DELETE")} + }, + "sdns": {"METHODS": ("GET", "POST"), + "": {"METHODS": ("GET", "DELETE")} }, } }, @@ -140,7 +151,7 @@ class Server(object): "": {"METHODS": ("GET", "PUT", "DELETE")} }, "ns_descriptors": { "METHODS": ("GET", "POST"), - "": { "METHODS": ("GET", "DELETE"), "TODO": "PATCH", + "": {"METHODS": ("GET", "DELETE"), "TODO": "PATCH", "nsd_content": { "METHODS": ("GET", "PUT")}, "nsd": {"METHODS": "GET"}, # descriptor inside package "artifacts": {"*": {"METHODS": "GET"}} @@ -317,7 +328,10 @@ class Server(object): :param _format: The format to be set as Content-Type ir data is a file :return: None """ + accept = cherrypy.request.headers.get("Accept") if data is None: + if accept and "text/html" in accept: + return html.format(data, cherrypy.request, cherrypy.response, session) cherrypy.response.status = HTTPStatus.NO_CONTENT.value return elif hasattr(data, "read"): # file object @@ -329,8 +343,7 @@ class Server(object): cherrypy.response.headers["Content-Type"] = 'text/plain' # TODO check that cherrypy close file. If not implement pending things to close per thread next return data - if "Accept" in cherrypy.request.headers: - accept = cherrypy.request.headers["Accept"] + if accept: if "application/json" in accept: cherrypy.response.headers["Content-Type"] = 'application/json; charset=utf-8' a = json.dumps(data, indent=4) + "\n" @@ -390,6 +403,7 @@ class Server(object): outdata = self.engine.new_token(session, indata, cherrypy.request.remote) session = outdata cherrypy.session['Authorization'] = outdata["_id"] + self._set_location_header("admin", "v1", "tokens", outdata["_id"]) # cherrypy.response.cookie["Authorization"] = outdata["id"] # cherrypy.response.cookie["Authorization"]['expires'] = 3600 elif method == "DELETE": @@ -399,6 +413,7 @@ class Server(object): session = self._authorization() token_id = session["_id"] outdata = self.engine.del_token(token_id) + oudata = None session = None cherrypy.session['Authorization'] = "logout" # cherrypy.response.cookie["Authorization"] = token_id @@ -477,7 +492,7 @@ class Server(object): self.engine.msg.write(topic, k, yaml.load(v)) return "ok" except Exception as e: - return "Error: " + format(e) + return "Error: " + str(e) return_text = ( "
\nheaders:\n  args: {}\n".format(args) +
@@ -545,6 +560,8 @@ class Server(object):
         session = None
         outdata = None
         _format = None
+        method = "DONE"
+        engine_item = None
         try:
             if not topic or not version or not item:
                 raise NbiException("URL must contain at least 'topic/version/item'", HTTPStatus.METHOD_NOT_ALLOWED)
@@ -621,8 +638,8 @@ class Server(object):
                 if not _id:
                     outdata = self.engine.del_item_list(session, engine_item, kwargs)
                 else:  # len(args) > 1
-                    outdata = self.engine.del_item(session, engine_item, _id)
-                if item in ("ns_descriptors", "vnf_packages"):  # SOL005
+                    # TODO return 202 ACCEPTED for nsrs vims
+                    self.engine.del_item(session, engine_item, _id)
                     outdata = None
             elif method == "PUT":
                 if not indata and not kwargs:
@@ -638,11 +655,15 @@ class Server(object):
             else:
                 raise NbiException("Method {} not allowed".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
             return self._format_out(outdata, session, _format)
-        except (NbiException, EngineException, DbException, FsException) as e:
+        except (NbiException, EngineException, DbException, FsException, MsgException) as e:
             if hasattr(outdata, "close"):  # is an open file
                 outdata.close()
             cherrypy.log("Exception {}".format(e))
             cherrypy.response.status = e.http_code.value
+            error_text = str(e)
+            if isinstance(e, MsgException):
+                error_text = "{} has been '{}' but other modules cannot be informed because an error on bus".format(
+                    engine_item[:-1], method, error_text)
             problem_details = {
                 "code": e.http_code.name,
                 "status": e.http_code.value,
diff --git a/osm_nbi/test/test.py b/osm_nbi/test/test.py
index 734d667..004da02 100755
--- a/osm_nbi/test/test.py
+++ b/osm_nbi/test/test.py
@@ -54,16 +54,41 @@ headers_zip = {
 }
 # test without authorization
 test_not_authorized_list = (
-    ("Invalid token", "GET", "/admin/v1/users", headers_json, None, 401, r_header_json, "json"),
-    ("Invalid URL", "POST", "/admin/v1/nonexist", headers_yaml, None, 405, r_header_yaml, "yaml"),
-    ("Invalid version", "DELETE", "/admin/v2/users", headers_yaml, None, 405, r_header_yaml, "yaml"),
+    ("NA1", "Invalid token", "GET", "/admin/v1/users", headers_json, None, 401, r_header_json, "json"),
+    ("NA2", "Invalid URL", "POST", "/admin/v1/nonexist", headers_yaml, None, 405, r_header_yaml, "yaml"),
+    ("NA3", "Invalid version", "DELETE", "/admin/v2/users", headers_yaml, None, 405, r_header_yaml, "yaml"),
 )
 
 # test ones authorized
 test_authorized_list = (
-    ("Invalid vnfd id", "GET", "/vnfpkgm/v1/vnf_packages/non-existing-id", headers_json, None, 404, r_header_json, "json"),
-    ("Invalid nsd id", "GET", "/nsd/v1/ns_descriptors/non-existing-id", headers_yaml, None, 404, r_header_yaml, "yaml"),
-    ("Invalid nsd id", "DELETE", "/nsd/v1/ns_descriptors_content/non-existing-id", headers_yaml, None, 404, r_header_yaml, "yaml"),
+    ("AU1", "Invalid vnfd id", "GET", "/vnfpkgm/v1/vnf_packages/non-existing-id", headers_json, None, 404, r_header_json, "json"),
+    ("AU2","Invalid nsd id", "GET", "/nsd/v1/ns_descriptors/non-existing-id", headers_yaml, None, 404, r_header_yaml, "yaml"),
+    ("AU3","Invalid nsd id", "DELETE", "/nsd/v1/ns_descriptors_content/non-existing-id", headers_yaml, None, 404, r_header_yaml, "yaml"),
+)
+
+vim = {
+    "schema_version": "1.0",
+    "schema_type": "No idea",
+    "name": "myVim",
+    "description": "Descriptor name",
+    "vim_type": "openstack",
+    "vim_url": "http://localhost:/vim",
+    "vim_tenant_name": "vimTenant",
+    "vim_user": "user",
+    "vim_password": "password",
+    "config": {"config_param": 1}
+}
+
+vim_bad = vim.copy()
+vim_bad.pop("name")
+
+test_admin_list1 = (
+    ("VIM1", "Create VIM", "POST", "/admin/v1/vims", headers_json, vim, (201, 204), {"Location": "/admin/v1/vims/", "Content-Type": "application/json"}, "json"),
+    ("VIM2", "Create VIM bad schema", "POST", "/admin/v1/vims", headers_json, vim_bad, 422, None, headers_json),
+    ("VIM2", "Create VIM name repeated", "POST", "/admin/v1/vims", headers_json, vim, 409, None, headers_json),
+    ("VIM4", "Show VIMs", "GET", "/admin/v1/vims", headers_yaml, None, 200, r_header_yaml, "yaml"),
+    ("VIM5", "Show VIM", "GET", "/admin/v1/vims/{VIM1}", headers_yaml, None, 200, r_header_yaml, "yaml"),
+    ("VIM6", "Delete VIM", "DELETE", "/admin/v1/vims/{VIM1}", headers_yaml, None, 204, None, 0),
 )
 
 class TestException(Exception):
@@ -77,14 +102,18 @@ class TestRest:
         self.s = requests.session()
         self.s.headers = header_base
         self.verify = verify
+        # contains ID of tests obtained from Location response header. "" key contains last obtained id
+        self.test_ids = {}
 
     def set_header(self, header):
         self.s.headers.update(header)
 
-    def test(self, name, method, url, headers, payload, expected_codes, expected_headers, expected_payload):
+    def test(self, name, description, method, url, headers, payload, expected_codes, expected_headers, expected_payload):
         """
-        Performs an http request and check http code response. Exit if different than allowed
-        :param name:  name of the test
+        Performs an http request and check http code response. Exit if different than allowed. It get the returned id
+        that can be used by following test in the URL with {name} where name is the name of the test
+        :param name:  short name of the test
+        :param description:  description of the test
         :param method: HTTP method: GET,PUT,POST,DELETE,...
         :param url: complete URL or relative URL
         :param headers: request headers to add to the base headers
@@ -92,15 +121,27 @@ class TestRest:
         :param expected_codes: expected response codes, can be int, int tuple or int range
         :param expected_headers: expected response headers, dict with key values
         :param expected_payload: expected payload, 0 if empty, 'yaml', 'json', 'text', 'zip'
-        :return:
+        :return: requests response
         """
         try:
             if not self.s:
                 self.s = requests.session()
+            # URL
             if not url:
                 url = self.url_base
             elif not url.startswith("http"):
                 url = self.url_base + url
+
+            var_start = url.find("{") + 1
+            while var_start:
+                var_end = url.find("}", var_start)
+                if var_end == -1:
+                    break
+                var_name = url[var_start:var_end]
+                if var_name in self.test_ids:
+                    url = url[:var_start-1] + self.test_ids[var_name] + url[var_end+1:]
+                    var_start += len(self.test_ids[var_name])
+                var_start = url.find("{", var_start) + 1
             if payload:
                 if isinstance(payload, str):
                     if payload.startswith("@"):
@@ -114,7 +155,7 @@ class TestRest:
                 elif isinstance(payload, dict):
                     payload = json.dumps(payload)
     
-            test = "Test {} {} {}".format(name, method, url)
+            test = "Test {} {} {} {}".format(name, description, method, url)
             logger.warning(test)
             stream = False
             # if expected_payload == "zip":
@@ -165,6 +206,12 @@ class TestRest:
                     if len(r.content) == 0:
                         raise TestException("Expected some response payload, but got empty")
                     #r.text
+            location = r.headers.get("Location")
+            if location:
+                _id = location[location.rfind("/") + 1:]
+                if _id:
+                    self.test_ids[name] = str(_id)
+                    self.test_ids[""] = str(_id)  # last id
             return r
         except TestException as e:
             logger.error("{} \nRX code{}: {}".format(e, r.status_code, r.text))
@@ -177,6 +224,9 @@ class TestRest:
 if __name__ == "__main__":
     global logger
     test = ""
+
+    # Disable warnings from self-signed certificates.
+    requests.packages.urllib3.disable_warnings()
     try:
         logging.basicConfig(format="%(levelname)s %(message)s", level=logging.ERROR)
         logger = logging.getLogger('NBI')
@@ -225,7 +275,7 @@ if __name__ == "__main__":
             test_rest.test(*t)
 
         # get token
-        r = test_rest.test("Obtain token", "POST", "/admin/v1/tokens", headers_json,
+        r = test_rest.test("token1", "Obtain token", "POST", "/admin/v1/tokens", headers_json,
                            {"username": user, "password": password, "project_id": project},
                            (200, 201), {"Content-Type": "application/json"}, "json")
         response = r.json()
@@ -236,23 +286,28 @@ if __name__ == "__main__":
         for t in test_authorized_list:
             test_rest.test(*t)
 
+        # tests admin
+        for t in test_admin_list1:
+            test_rest.test(*t)
+
+
         # nsd CREATE
-        r = test_rest.test("Onboard NSD step 1", "POST", "/nsd/v1/ns_descriptors", headers_json, None,
+        r = test_rest.test("NSD1", "Onboard NSD step 1", "POST", "/nsd/v1/ns_descriptors", headers_json, None,
                            201, {"Location": "/nsd/v1/ns_descriptors/", "Content-Type": "application/json"}, "json")
         location = r.headers["Location"]
         nsd_id = location[location.rfind("/")+1:]
         # print(location, nsd_id)
 
         # nsd UPLOAD test
-        r = test_rest.test("Onboard NSD step 2 as TEXT", "PUT", "/nsd/v1/ns_descriptors/{}/nsd_content".format(nsd_id),
+        r = test_rest.test("NSD2", "Onboard NSD step 2 as TEXT", "PUT", "/nsd/v1/ns_descriptors/{}/nsd_content".format(nsd_id),
                            r_header_text, "@./cirros_ns/cirros_nsd.yaml", 204, None, 0)
 
         # nsd SHOW OSM format
-        r = test_rest.test("Show NSD OSM format", "GET", "/nsd/v1/ns_descriptors_content/{}".format(nsd_id),
+        r = test_rest.test("NSD3", "Show NSD OSM format", "GET", "/nsd/v1/ns_descriptors_content/{}".format(nsd_id),
                            headers_json, None, 200, r_header_json, "json")
 
         # nsd SHOW text
-        r = test_rest.test("Show NSD SOL005 text", "GET", "/nsd/v1/ns_descriptors/{}/nsd_content".format(nsd_id),
+        r = test_rest.test("NSD4", "Show NSD SOL005 text", "GET", "/nsd/v1/ns_descriptors/{}/nsd_content".format(nsd_id),
                            headers_text, None, 200, r_header_text, "text")
 
         # nsd UPLOAD ZIP
@@ -260,45 +315,45 @@ if __name__ == "__main__":
         tar = tarfile.open("temp/cirros_ns.tar.gz", "w:gz")
         tar.add("cirros_ns")
         tar.close()
-        r = test_rest.test("Onboard NSD step 3 replace with ZIP", "PUT", "/nsd/v1/ns_descriptors/{}/nsd_content".format(nsd_id),
+        r = test_rest.test("NSD5", "Onboard NSD step 3 replace with ZIP", "PUT", "/nsd/v1/ns_descriptors/{}/nsd_content".format(nsd_id),
                            r_header_zip, "@b./temp/cirros_ns.tar.gz", 204, None, 0)
 
         # nsd SHOW OSM format
-        r = test_rest.test("Show NSD OSM format", "GET", "/nsd/v1/ns_descriptors_content/{}".format(nsd_id),
+        r = test_rest.test("NSD6", "Show NSD OSM format", "GET", "/nsd/v1/ns_descriptors_content/{}".format(nsd_id),
                            headers_json, None, 200, r_header_json, "json")
 
         # nsd SHOW zip
-        r = test_rest.test("Show NSD SOL005 zip", "GET", "/nsd/v1/ns_descriptors/{}/nsd_content".format(nsd_id),
+        r = test_rest.test("NSD7", "Show NSD SOL005 zip", "GET", "/nsd/v1/ns_descriptors/{}/nsd_content".format(nsd_id),
                            headers_zip, None, 200, r_header_zip, "zip")
 
         # nsd SHOW descriptor
-        r = test_rest.test("Show NSD descriptor", "GET", "/nsd/v1/ns_descriptors/{}/nsd".format(nsd_id),
+        r = test_rest.test("NSD8", "Show NSD descriptor", "GET", "/nsd/v1/ns_descriptors/{}/nsd".format(nsd_id),
                            headers_text, None, 200, r_header_text, "text")
         # nsd SHOW actifact
-        r = test_rest.test("Show NSD artifact", "GET", "/nsd/v1/ns_descriptors/{}/artifacts/icons/osm_2x.png".format(nsd_id),
+        r = test_rest.test("NSD9", "Show NSD artifact", "GET", "/nsd/v1/ns_descriptors/{}/artifacts/icons/osm_2x.png".format(nsd_id),
                            headers_text, None, 200, r_header_octect, "text")
 
         # nsd DELETE
-        r = test_rest.test("Delete NSD SOL005 text", "DELETE", "/nsd/v1/ns_descriptors/{}".format(nsd_id),
+        r = test_rest.test("NSD10", "Delete NSD SOL005 text", "DELETE", "/nsd/v1/ns_descriptors/{}".format(nsd_id),
                            headers_yaml, None, 204, None, 0)
 
         # vnfd CREATE
-        r = test_rest.test("Onboard VNFD step 1", "POST", "/vnfpkgm/v1/vnf_packages", headers_json, None,
+        r = test_rest.test("VNFD1", "Onboard VNFD step 1", "POST", "/vnfpkgm/v1/vnf_packages", headers_json, None,
                            201, {"Location": "/vnfpkgm/v1/vnf_packages/", "Content-Type": "application/json"}, "json")
         location = r.headers["Location"]
         vnfd_id = location[location.rfind("/")+1:]
         # print(location, vnfd_id)
 
         # vnfd UPLOAD test
-        r = test_rest.test("Onboard VNFD step 2 as TEXT", "PUT", "/vnfpkgm/v1/vnf_packages/{}/package_content".format(vnfd_id),
+        r = test_rest.test("VNFD2", "Onboard VNFD step 2 as TEXT", "PUT", "/vnfpkgm/v1/vnf_packages/{}/package_content".format(vnfd_id),
                            r_header_text, "@./cirros_vnf/cirros_vnfd.yaml", 204, None, 0)
 
         # vnfd SHOW OSM format
-        r = test_rest.test("Show VNFD OSM format", "GET", "/vnfpkgm/v1/vnf_packages_content/{}".format(vnfd_id),
+        r = test_rest.test("VNFD3", "Show VNFD OSM format", "GET", "/vnfpkgm/v1/vnf_packages_content/{}".format(vnfd_id),
                            headers_json, None, 200, r_header_json, "json")
 
         # vnfd SHOW text
-        r = test_rest.test("Show VNFD SOL005 text", "GET", "/vnfpkgm/v1/vnf_packages/{}/package_content".format(vnfd_id),
+        r = test_rest.test("VNFD4", "Show VNFD SOL005 text", "GET", "/vnfpkgm/v1/vnf_packages/{}/package_content".format(vnfd_id),
                            headers_text, None, 200, r_header_text, "text")
 
         # vnfd UPLOAD ZIP
@@ -306,25 +361,25 @@ if __name__ == "__main__":
         tar = tarfile.open("temp/cirros_vnf.tar.gz", "w:gz")
         tar.add("cirros_vnf")
         tar.close()
-        r = test_rest.test("Onboard VNFD step 3 replace with ZIP", "PUT", "/vnfpkgm/v1/vnf_packages/{}/package_content".format(vnfd_id),
+        r = test_rest.test("VNFD5", "Onboard VNFD step 3 replace with ZIP", "PUT", "/vnfpkgm/v1/vnf_packages/{}/package_content".format(vnfd_id),
                            r_header_zip, "@b./temp/cirros_vnf.tar.gz", 204, None, 0)
 
         # vnfd SHOW OSM format
-        r = test_rest.test("Show VNFD OSM format", "GET", "/vnfpkgm/v1/vnf_packages_content/{}".format(vnfd_id),
+        r = test_rest.test("VNFD6", "Show VNFD OSM format", "GET", "/vnfpkgm/v1/vnf_packages_content/{}".format(vnfd_id),
                            headers_json, None, 200, r_header_json, "json")
 
         # vnfd SHOW zip
-        r = test_rest.test("Show VNFD SOL005 zip", "GET", "/vnfpkgm/v1/vnf_packages/{}/package_content".format(vnfd_id),
+        r = test_rest.test("VNFD7", "Show VNFD SOL005 zip", "GET", "/vnfpkgm/v1/vnf_packages/{}/package_content".format(vnfd_id),
                            headers_zip, None, 200, r_header_zip, "zip")
         # vnfd SHOW descriptor
-        r = test_rest.test("Show VNFD descriptor", "GET", "/vnfpkgm/v1/vnf_packages/{}/vnfd".format(vnfd_id),
+        r = test_rest.test("VNFD8", "Show VNFD descriptor", "GET", "/vnfpkgm/v1/vnf_packages/{}/vnfd".format(vnfd_id),
                            headers_text, None, 200, r_header_text, "text")
         # vnfd SHOW actifact
-        r = test_rest.test("Show VNFD artifact", "GET", "/vnfpkgm/v1/vnf_packages/{}/artifacts/icons/cirros-64.png".format(vnfd_id),
+        r = test_rest.test("VNFD9", "Show VNFD artifact", "GET", "/vnfpkgm/v1/vnf_packages/{}/artifacts/icons/cirros-64.png".format(vnfd_id),
                            headers_text, None, 200, r_header_octect, "text")
 
         # vnfd DELETE
-        r = test_rest.test("Delete VNFD SOL005 text", "DELETE", "/vnfpkgm/v1/vnf_packages/{}".format(vnfd_id),
+        r = test_rest.test("VNFD10", "Delete VNFD SOL005 text", "DELETE", "/vnfpkgm/v1/vnf_packages/{}".format(vnfd_id),
                            headers_yaml, None, 204, None, 0)
 
         print("PASS")
diff --git a/osm_nbi/validation.py b/osm_nbi/validation.py
new file mode 100644
index 0000000..2419c1a
--- /dev/null
+++ b/osm_nbi/validation.py
@@ -0,0 +1,190 @@
+# -*- coding: utf-8 -*-
+
+from jsonschema import validate as js_v, exceptions as js_e
+
+__author__ = "Alfonso Tierno "
+__version__ = "0.1"
+version_date = "Mar 2018"
+
+"""
+Validator of input data using JSON schemas for those items that not contains an  OSM yang information model
+"""
+
+# Basis schemas
+patern_name = "^[ -~]+$"
+passwd_schema = {"type": "string", "minLength": 1, "maxLength": 60}
+nameshort_schema = {"type": "string", "minLength": 1, "maxLength": 60, "pattern": "^[^,;()'\"]+$"}
+name_schema = {"type": "string", "minLength": 1, "maxLength": 255, "pattern": "^[^,;()'\"]+$"}
+xml_text_schema = {"type": "string", "minLength": 1, "maxLength": 1000, "pattern": "^[^']+$"}
+description_schema = {"type": ["string", "null"], "maxLength": 255, "pattern": "^[^'\"]+$"}
+id_schema_fake = {"type": "string", "minLength": 2,
+                  "maxLength": 36}  # "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$"
+id_schema = {"type": "string", "pattern": "^[a-fA-F0-9]{8}(-[a-fA-F0-9]{4}){3}-[a-fA-F0-9]{12}$"}
+pci_schema = {"type": "string", "pattern": "^[0-9a-fA-F]{4}(:[0-9a-fA-F]{2}){2}\.[0-9a-fA-F]$"}
+http_schema = {"type": "string", "pattern": "^https?://[^'\"=]+$"}
+bandwidth_schema = {"type": "string", "pattern": "^[0-9]+ *([MG]bps)?$"}
+memory_schema = {"type": "string", "pattern": "^[0-9]+ *([MG]i?[Bb])?$"}
+integer0_schema = {"type": "integer", "minimum": 0}
+integer1_schema = {"type": "integer", "minimum": 1}
+path_schema = {"type": "string", "pattern": "^(\.){0,2}(/[^/\"':{}\(\)]+)+$"}
+vlan_schema = {"type": "integer", "minimum": 1, "maximum": 4095}
+vlan1000_schema = {"type": "integer", "minimum": 1000, "maximum": 4095}
+mac_schema = {"type": "string",
+              "pattern": "^[0-9a-fA-F][02468aceACE](:[0-9a-fA-F]{2}){5}$"}  # must be unicast: LSB bit of MSB byte ==0
+# mac_schema={"type":"string", "pattern":"^([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$"}
+ip_schema = {"type": "string",
+             "pattern": "^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$"}
+ip_prefix_schema = {"type": "string",
+                    "pattern": "^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)/(30|[12]?[0-9])$"}
+port_schema = {"type": "integer", "minimum": 1, "maximum": 65534}
+object_schema = {"type": "object"}
+schema_version_2 = {"type": "integer", "minimum": 2, "maximum": 2}
+# schema_version_string={"type":"string","enum": ["0.1", "2", "0.2", "3", "0.3"]}
+log_level_schema = {"type": "string", "enum": ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]}
+checksum_schema = {"type": "string", "pattern": "^[0-9a-fA-F]{32}$"}
+size_schema = {"type": "integer", "minimum": 1, "maximum": 100}
+
+schema_version = {"type": "string", "enum": ["1.0"]}
+schema_type = {"type": "string"}
+
+vim_new_schema = {
+    "title": "vims new user input schema",
+    "$schema": "http://json-schema.org/draft-04/schema#",
+    "type": "object",
+    "properties": {
+        "schema_version": schema_version,
+        "schema_type": schema_type,
+        "name": name_schema,
+        "description": description_schema,
+        "vim_type": {"enum": ["openstack", "openvim", "vmware", "opennebula", "aws"]},
+        "vim_url": description_schema,
+        # "vim_url_admin": description_schema,
+        # "vim_tenant": name_schema,
+        "vim_tenant_name": name_schema,
+        "vim_user": nameshort_schema,
+        "vim_password": nameshort_schema,
+        "config": {"type": "object"}
+    },
+    "required": ["name", "vim_url", "vim_type", "vim_user", "vim_password", "vim_tenant_name"],
+    "additionalProperties": False
+}
+vim_edit_schema = {
+    "title": "datacenter edit nformation schema",
+    "$schema": "http://json-schema.org/draft-04/schema#",
+    "type": "object",
+    "properties": {
+        "name": name_schema,
+        "description": description_schema,
+        "type": nameshort_schema,  # currently "openvim" or "openstack", can be enlarged with plugins
+        "vim_url": description_schema,
+        "vim_url_admin": description_schema,
+        "vim_tenant": name_schema,
+        "vim_tenant_name": name_schema,
+        "vim_username": nameshort_schema,
+        "vim_password": nameshort_schema,
+        "config": {"type": "object"}
+    },
+    "additionalProperties": False
+}
+
+
+sdn_properties = {
+    "name": name_schema,
+    "dpid": {"type": "string", "pattern": "^[0-9a-fA-F]{2}(:[0-9a-fA-F]{2}){7}$"},
+    "ip": ip_schema,
+    "port": port_schema,
+    "type": {"type": "string", "enum": ["opendaylight", "floodlight", "onos"]},
+    "version": {"type": "string", "minLength": 1, "maxLength": 12},
+    "user": nameshort_schema,
+    "password": passwd_schema
+}
+sdn_new_schema = {
+    "title": "sdn controller information schema",
+    "$schema": "http://json-schema.org/draft-04/schema#",
+    "type": "object",
+    "properties": sdn_properties,
+    "required": ["name", "port", 'ip', 'dpid', 'type'],
+    "additionalProperties": False
+}
+sdn_edit_schema = {
+    "title": "sdn controller update information schema",
+    "$schema": "http://json-schema.org/draft-04/schema#",
+    "type": "object",
+    "properties": sdn_properties,
+    "required": ["name", "port", 'ip', 'dpid', 'type'],
+    "additionalProperties": False
+}
+sdn_port_mapping_schema = {
+    "$schema": "http://json-schema.org/draft-04/schema#",
+    "title": "sdn port mapping information schema",
+    "type": "array",
+    "items": {
+        "type": "object",
+        "properties": {
+            "compute_node": nameshort_schema,
+            "ports": {
+                "type": "array",
+                "items": {
+                    "type": "object",
+                    "properties": {
+                        "pci": pci_schema,
+                        "switch_port": nameshort_schema,
+                        "switch_mac": mac_schema
+                    },
+                    "required": ["pci"]
+                }
+            }
+        },
+        "required": ["compute_node", "ports"]
+    }
+}
+sdn_external_port_schema = {
+    "$schema": "http://json-schema.org/draft-04/schema#",
+    "title": "External port ingformation",
+    "type": "object",
+    "properties": {
+        "port": {"type": "string", "minLength": 1, "maxLength": 60},
+        "vlan": vlan_schema,
+        "mac": mac_schema
+    },
+    "required": ["port"]
+}
+
+
+nbi_new_input_schemas = {
+    "vims": vim_new_schema,
+    "sdns": sdn_new_schema
+}
+
+nbi_edit_input_schemas = {
+    "vims": vim_edit_schema,
+    "sdns": sdn_edit_schema
+}
+
+
+class ValidationError(Exception):
+    pass
+
+
+def validate_input(indata, item, new=True):
+    """
+    Validates input data agains json schema
+    :param indata: user input data. Should be a dictionary
+    :param item: can be users, projects, vims, sdns
+    :param new: True if the validation is for creating or False if it is for editing
+    :return: None if ok, raises ValidationError exception otherwise
+    """
+    try:
+        if new:
+            schema_to_use = nbi_new_input_schemas.get(item)
+        else:
+            schema_to_use = nbi_edit_input_schemas.get(item)
+        if schema_to_use:
+            js_v(indata, schema_to_use)
+        return None
+    except js_e.ValidationError as e:
+        if e.path:
+            error_pos = "at '" + ":".join(e.path) + "'"
+        else:
+            error_pos = ""
+        raise ValidationError("Format error {} '{}' ".format(error_pos, e))
diff --git a/setup.py b/setup.py
index 60d7e28..96c9e49 100644
--- a/setup.py
+++ b/setup.py
@@ -23,7 +23,7 @@ setup(
                 ],
 
     install_requires=[
-        'CherryPy', 'pymongo'
+        'CherryPy', 'pymongo', 'jsonchema'
     ],
 #    setup_requires=['setuptools-version-command'],
     # test_suite='nose.collector',
diff --git a/stdeb.cfg b/stdeb.cfg
index 0fbc1d6..72970ec 100644
--- a/stdeb.cfg
+++ b/stdeb.cfg
@@ -1,2 +1,2 @@
 [DEFAULT]
-Depends: python3-cherrypy3, python3-pymongo, python3-yaml
+Depends: python3-cherrypy3, python3-pymongo, python3-yaml, python3-jsonschema
-- 
2.17.1