# 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
from http import HTTPStatus
from time import time
from copy import deepcopy
+from validation import validate_input, ValidationError
__author__ = "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
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()
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
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"):
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")
_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):
"", 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
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):
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):
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)
else:
body += '<a href="/osm/{}?METHOD=DELETE"> <img src="/osm/static/delete.png" height="25" width="25"> </a>'.format(request.path_info)
body += "<pre>" + yaml.safe_dump(data, explicit_start=True, indent=4, default_flow_style=False) + "</pre>"
+ elif data is None:
+ if request.method == "DELETE" or "METHOD=DELETE" in request.query_string:
+ body += "<pre> deleted </pre>"
else:
body = str(data)
user_text = " "
+import asyncio
from http import HTTPStatus
__author__ = "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
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
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()
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:
import yaml
import asyncio
from msgbase import MsgBase, MsgException
+from time import sleep
__author__ = "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
+"""
+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):
self.path = None
# create a different file for each topic
self.files = {}
+ self.buffer = {}
def connect(self, config):
try:
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))
+
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"
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
from os import environ
__author__ = "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
-__version__ = "0.2"
+__version__ = "0.3"
version_date = "Mar 2018"
"""
/<id> O O
/projects O O
/<id> O O
+ /vims O O
+ /<id> O O O
+ /sdns O O
+ /<id> O O O
query string.
<attrName>[.<attrName>...]*[.<op>]=<value>[,<value>...]&...
self.valid_methods = { # contains allowed URL and methods
"admin": {
"v1": {
- "tokens": { "METHODS": ("GET", "POST", "DELETE"),
+ "tokens": {"METHODS": ("GET", "POST", "DELETE"),
"<ID>": { "METHODS": ("GET", "DELETE")}
},
- "users": { "METHODS": ("GET", "POST"),
+ "users": {"METHODS": ("GET", "POST"),
"<ID>": {"METHODS": ("GET", "POST", "DELETE")}
},
- "projects": { "METHODS": ("GET", "POST"),
- "<ID>": {"METHODS": ("GET", "POST", "DELETE")}
+ "projects": {"METHODS": ("GET", "POST"),
+ "<ID>": {"METHODS": ("GET", "DELETE")}
+ },
+ "vims": {"METHODS": ("GET", "POST"),
+ "<ID>": {"METHODS": ("GET", "DELETE")}
+ },
+ "sdns": {"METHODS": ("GET", "POST"),
+ "<ID>": {"METHODS": ("GET", "DELETE")}
},
}
},
"<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
},
"ns_descriptors": { "METHODS": ("GET", "POST"),
- "<ID>": { "METHODS": ("GET", "DELETE"), "TODO": "PATCH",
+ "<ID>": {"METHODS": ("GET", "DELETE"), "TODO": "PATCH",
"nsd_content": { "METHODS": ("GET", "PUT")},
"nsd": {"METHODS": "GET"}, # descriptor inside package
"artifacts": {"*": {"METHODS": "GET"}}
: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
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"
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":
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
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 = (
"<html><pre>\nheaders:\n args: {}\n".format(args) +
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)
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:
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,
}
# 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):
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
: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("@"):
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":
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))
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')
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()
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
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
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")
--- /dev/null
+# -*- coding: utf-8 -*-
+
+from jsonschema import validate as js_v, exceptions as js_e
+
+__author__ = "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
+__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))
],
install_requires=[
- 'CherryPy', 'pymongo'
+ 'CherryPy', 'pymongo', 'jsonchema'
],
# setup_requires=['setuptools-version-command'],
# test_suite='nose.collector',
[DEFAULT]
-Depends: python3-cherrypy3, python3-pymongo, python3-yaml
+Depends: python3-cherrypy3, python3-pymongo, python3-yaml, python3-jsonschema