#!/usr/bin/python3
# -*- coding: utf-8 -*-
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
import cherrypy
import time
import json
from authconn import AuthException
from auth import Authenticator
from engine import Engine, EngineException
+from subscriptions import SubscriptionThread
+from validation import ValidationError
from osm_common.dbbase import DbException
from osm_common.fsbase import FsException
from osm_common.msgbase import MsgException
__author__ = "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
-# TODO consider to remove and provide version using the static version file
__version__ = "0.1.3"
-version_date = "Apr 2018"
+version_date = "Jan 2019"
database_version = '1.0'
auth_database_version = '1.0'
+nbi_server = None # instance of Server class
+subscription_thread = None # instance of SubscriptionThread class
+
"""
North Bound Interface (O: OSM specific; 5,X: SOL005 not implemented yet; O5: SOL005 implemented)
URL: /osm GET POST PUT DELETE PATCH
- /nsd/v1 O O
+ /nsd/v1
/ns_descriptors_content O O
/<nsdInfoId> O O O O
/ns_descriptors O5 O5
/<vnfInstanceId> O
/subscriptions 5 5
/<subscriptionId> 5 X
+
/pdu/v1
/pdu_descriptor O O
/<id> O O O O
+
/admin/v1
/tokens O O
/<id> O O
/<id> O O O O
/projects O O
/<id> O O
- /vims_accounts (also vims for compatibility) O O
+ /vim_accounts (also vims for compatibility) O O
+ /<id> O O O
+ /wim_accounts O O
/<id> O O O
/sdns O O
/<id> O O O
+ /nst/v1 O O
+ /netslice_templates_content O O
+ /<nstInfoId> O O O O
+ /netslice_templates O O
+ /<nstInfoId> O O O
+ /nst_content O O
+ /nst O
+ /artifacts[/<artifactPath>] O
+ /subscriptions X X
+ /<subscriptionId> X X
+
+ /nsilcm/v1
+ /netslice_instances_content O O
+ /<SliceInstanceId> O O
+ /netslice_instances O O
+ /<SliceInstanceId> O O
+ instantiate O
+ terminate O
+ action O
+ /nsi_lcm_op_occs O O
+ /<nsiLcmOpOccId> O O O
+ /subscriptions X X
+ /<subscriptionId> X X
+
query string:
Follows SOL005 section 4.3.2 It contains extra METHOD to override http method, FORCE to force.
+ simpleFilterExpr := <attrName>["."<attrName>]*["."<op>]"="<value>[","<value>]*
+ filterExpr := <simpleFilterExpr>["&"<simpleFilterExpr>]*
+ op := "eq" | "neq" (or "ne") | "gt" | "lt" | "gte" | "lte" | "cont" | "ncont"
+ attrName := string
For filtering inside array, it must select the element of the array, or add ANYINDEX to apply the filtering over any
item of the array, that is, pass if any item of the array pass the filter.
It allows both ne and neq for not equal
"vim_accounts": {"METHODS": ("GET", "POST"),
"<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
},
+ "wim_accounts": {"METHODS": ("GET", "POST"),
+ "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
+ },
"sdns": {"METHODS": ("GET", "POST"),
"<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
},
"<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
},
"ns_descriptors": {"METHODS": ("GET", "POST"),
- "<ID>": {"METHODS": ("GET", "DELETE"), "TODO": "PATCH",
+ "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"),
"nsd_content": {"METHODS": ("GET", "PUT")},
"nsd": {"METHODS": "GET"}, # descriptor inside package
"artifacts": {"*": {"METHODS": "GET"}}
},
}
},
+ "nst": {
+ "v1": {
+ "netslice_templates_content": {"METHODS": ("GET", "POST"),
+ "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
+ },
+ "netslice_templates": {"METHODS": ("GET", "POST"),
+ "<ID>": {"METHODS": ("GET", "DELETE"), "TODO": "PATCH",
+ "nst_content": {"METHODS": ("GET", "PUT")},
+ "nst": {"METHODS": "GET"}, # descriptor inside package
+ "artifacts": {"*": {"METHODS": "GET"}}
+ }
+ },
+ "subscriptions": {"TODO": ("GET", "POST"),
+ "<ID>": {"TODO": ("GET", "DELETE")}
+ },
+ }
+ },
+ "nsilcm": {
+ "v1": {
+ "netslice_instances_content": {"METHODS": ("GET", "POST"),
+ "<ID>": {"METHODS": ("GET", "DELETE")}
+ },
+ "netslice_instances": {"METHODS": ("GET", "POST"),
+ "<ID>": {"METHODS": ("GET", "DELETE"),
+ "terminate": {"METHODS": "POST"},
+ "instantiate": {"METHODS": "POST"},
+ "action": {"METHODS": "POST"},
+ }
+ },
+ "nsi_lcm_op_occs": {"METHODS": "GET",
+ "<ID>": {"METHODS": "GET"},
+ },
+ }
+ },
}
def _format_in(self, kwargs):
if "application/json" in cherrypy.request.headers["Content-Type"]:
error_text = "Invalid json format "
indata = json.load(self.reader(cherrypy.request.body))
+ cherrypy.request.headers.pop("Content-File-MD5", None)
elif "application/yaml" in cherrypy.request.headers["Content-Type"]:
error_text = "Invalid yaml format "
indata = yaml.load(cherrypy.request.body)
+ cherrypy.request.headers.pop("Content-File-MD5", None)
elif "application/binary" in cherrypy.request.headers["Content-Type"] or \
"application/gzip" in cherrypy.request.headers["Content-Type"] or \
"application/zip" in cherrypy.request.headers["Content-Type"] or \
# 'application/yaml' for input format are available")
error_text = "Invalid yaml format "
indata = yaml.load(cherrypy.request.body)
+ cherrypy.request.headers.pop("Content-File-MD5", None)
else:
error_text = "Invalid yaml format "
indata = yaml.load(cherrypy.request.body)
+ cherrypy.request.headers.pop("Content-File-MD5", None)
if not indata:
indata = {}
elif "application/yaml" in accept or "*/*" in accept or "text/plain" in accept:
pass
- else:
+ # if there is not any valid accept, raise an error. But if response is already an error, format in yaml
+ elif cherrypy.response.status >= 400:
raise cherrypy.HTTPError(HTTPStatus.NOT_ACCEPTABLE.value,
"Only 'Accept' of type 'application/json' or 'application/yaml' "
"for output format are available")
def test(self, *args, **kwargs):
thread_info = None
if args and args[0] == "help":
- return "<html><pre>\ninit\nfile/<name> download file\ndb-clear/table\nprune\nlogin\nlogin2\n"\
+ return "<html><pre>\ninit\nfile/<name> download file\ndb-clear/table\nfs-clear[/folder]\nlogin\nlogin2\n"\
"sleep/<time>\nmessage/topic\n</pre></html>"
elif args and args[0] == "init":
return f
elif len(args) == 2 and args[0] == "db-clear":
- return self.engine.db.del_list(args[1], kwargs)
- elif args and args[0] == "prune":
- return self.engine.prune()
+ deleted_info = self.engine.db.del_list(args[1], kwargs)
+ return "{} {} deleted\n".format(deleted_info["deleted"], args[1])
+ elif len(args) and args[0] == "fs-clear":
+ if len(args) >= 2:
+ folders = (args[1],)
+ else:
+ folders = self.engine.fs.dir_ls(".")
+ for folder in folders:
+ self.engine.fs.file_delete(folder)
+ return ",".join(folders) + " folders deleted\n"
elif args and args[0] == "login":
if not cherrypy.request.headers.get("Authorization"):
cherrypy.response.headers["WWW-Authenticate"] = 'Basic realm="Access to OSM site", charset="UTF-8"'
" session: {}\n".format(cherrypy.session) +
" cookie: {}\n".format(cherrypy.request.cookie) +
" method: {}\n".format(cherrypy.request.method) +
- " session: {}\n".format(cherrypy.session.get('fieldname')) +
+ " session: {}\n".format(cherrypy.session.get('fieldname')) +
" body:\n")
return_text += " length: {}\n".format(cherrypy.request.body.length)
if cherrypy.request.body.length:
if not main_topic or not version or not topic:
raise NbiException("URL must contain at least 'main_topic/version/topic'",
HTTPStatus.METHOD_NOT_ALLOWED)
- if main_topic not in ("admin", "vnfpkgm", "nsd", "nslcm"):
+ if main_topic not in ("admin", "vnfpkgm", "nsd", "nslcm", "pdu", "nst", "nsilcm"):
raise NbiException("URL main_topic '{}' not supported".format(main_topic),
HTTPStatus.METHOD_NOT_ALLOWED)
if version != 'v1':
force = kwargs.pop("FORCE")
else:
force = False
-
self._check_valid_url_method(method, main_topic, version, topic, _id, item, *args)
-
if main_topic == "admin" and topic == "tokens":
return self.token(method, _id, kwargs)
engine_topic = "nslcmops"
if topic == "vnfrs" or topic == "vnf_instances":
engine_topic = "vnfrs"
+ elif main_topic == "nst":
+ engine_topic = "nsts"
+ elif main_topic == "nsilcm":
+ engine_topic = "nsis"
+ if topic == "nsi_lcm_op_occs":
+ engine_topic = "nsilcmops"
elif main_topic == "pdu":
engine_topic = "pdus"
if engine_topic == "vims": # TODO this is for backward compatibility, it will remove in the future
engine_topic = "vim_accounts"
if method == "GET":
- if item in ("nsd_content", "package_content", "artifacts", "vnfd", "nsd"):
- if item in ("vnfd", "nsd"):
+ if item in ("nsd_content", "package_content", "artifacts", "vnfd", "nsd", "nst", "nst_content"):
+ if item in ("vnfd", "nsd", "nst"):
path = "$DESCRIPTOR"
elif args:
path = args
else:
outdata = self.engine.get_item(session, engine_topic, _id)
elif method == "POST":
- if topic in ("ns_descriptors_content", "vnf_packages_content"):
+ if topic in ("ns_descriptors_content", "vnf_packages_content", "netslice_templates_content"):
_id = cherrypy.request.headers.get("Transaction-Id")
if not _id:
_id = self.engine.new_item(rollback, session, engine_topic, {}, None, cherrypy.request.headers,
self._set_location_header(main_topic, version, "ns_lcm_op_occs", _id)
outdata = {"id": _id}
cherrypy.response.status = HTTPStatus.ACCEPTED.value
+ elif topic == "netslice_instances_content":
+ # creates NetSlice_Instance_record (NSIR)
+ _id = self.engine.new_item(rollback, session, engine_topic, indata, kwargs, force=force)
+ self._set_location_header(main_topic, version, topic, _id)
+ indata["lcmOperationType"] = "instantiate"
+ indata["nsiInstanceId"] = _id
+ self.engine.new_item(rollback, session, "nsilcmops", indata, kwargs)
+ outdata = {"id": _id}
+
+ elif topic == "netslice_instances" and item:
+ indata["lcmOperationType"] = item
+ indata["nsiInstanceId"] = _id
+ _id = self.engine.new_item(rollback, session, "nsilcmops", indata, kwargs)
+ self._set_location_header(main_topic, version, "nsi_lcm_op_occs", _id)
+ outdata = {"id": _id}
+ cherrypy.response.status = HTTPStatus.ACCEPTED.value
else:
_id = self.engine.new_item(rollback, session, engine_topic, indata, kwargs,
cherrypy.request.headers, force=force)
outdata = self.engine.del_item_list(session, engine_topic, kwargs)
cherrypy.response.status = HTTPStatus.OK.value
else: # len(args) > 1
+ delete_in_process = False
if topic == "ns_instances_content" and not force:
nslcmop_desc = {
"lcmOperationType": "terminate",
"autoremove": True
}
opp_id = self.engine.new_item(rollback, session, "nslcmops", nslcmop_desc, None)
- outdata = {"_id": opp_id}
- cherrypy.response.status = HTTPStatus.ACCEPTED.value
- else:
+ if opp_id:
+ delete_in_process = True
+ outdata = {"_id": opp_id}
+ cherrypy.response.status = HTTPStatus.ACCEPTED.value
+ elif topic == "netslice_instances_content" and not force:
+ nsilcmop_desc = {
+ "lcmOperationType": "terminate",
+ "nsiInstanceId": _id,
+ "autoremove": True
+ }
+ opp_id = self.engine.new_item(rollback, session, "nsilcmops", nsilcmop_desc, None)
+ if opp_id:
+ delete_in_process = True
+ outdata = {"_id": opp_id}
+ cherrypy.response.status = HTTPStatus.ACCEPTED.value
+ if not delete_in_process:
self.engine.del_item(session, engine_topic, _id, force)
cherrypy.response.status = HTTPStatus.NO_CONTENT.value
- if engine_topic in ("vim_accounts", "sdns"):
+ if engine_topic in ("vim_accounts", "wim_accounts", "sdns"):
cherrypy.response.status = HTTPStatus.ACCEPTED.value
elif method in ("PUT", "PATCH"):
if not indata and not kwargs:
raise NbiException("Nothing to update. Provide payload and/or query string",
HTTPStatus.BAD_REQUEST)
- if item in ("nsd_content", "package_content") and method == "PUT":
+ if item in ("nsd_content", "package_content", "nst_content") and method == "PUT":
completed = self.engine.upload_content(session, engine_topic, _id, indata, kwargs,
cherrypy.request.headers, force=force)
if not completed:
raise NbiException("Method {} not allowed".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
return self._format_out(outdata, session, _format)
except Exception as e:
- if isinstance(e, (NbiException, EngineException, DbException, FsException, MsgException, AuthException)):
+ if isinstance(e, (NbiException, EngineException, DbException, FsException, MsgException, AuthException,
+ ValidationError)):
http_code_value = cherrypy.response.status = e.http_code.value
http_code_name = e.http_code.name
cherrypy.log("Exception {}".format(e))
else:
http_code_value = cherrypy.response.status = HTTPStatus.BAD_REQUEST.value # INTERNAL_SERVER_ERROR
- cherrypy.log("CRITICAL: Exception {}".format(e))
+ cherrypy.log("CRITICAL: Exception {}".format(e), traceback=True)
http_code_name = HTTPStatus.BAD_REQUEST.name
if hasattr(outdata, "close"): # is an open file
outdata.close()
rollback.reverse()
for rollback_item in rollback:
try:
- self.engine.del_item(**rollback_item, session=session, force=True)
+ if rollback_item.get("operation") == "set":
+ self.engine.db.set_one(rollback_item["topic"], {"_id": rollback_item["_id"]},
+ rollback_item["content"], fail_on_empty=False)
+ else:
+ self.engine.db.del_one(rollback_item["topic"], {"_id": rollback_item["_id"]},
+ fail_on_empty=False)
except Exception as e2:
rollback_error_text = "Rollback Exception {}: {}".format(rollback_item, e2)
cherrypy.log(rollback_error_text)
# raise cherrypy.HTTPError(e.http_code.value, str(e))
-# def validate_password(realm, username, password):
-# cherrypy.log("realm "+ str(realm))
-# if username == "admin" and password == "admin":
-# return True
-# return False
-
-
def _start_service():
"""
Callback function called when cherrypy.engine starts
Set database, storage, message configuration
Init database with admin/admin user password
"""
+ global nbi_server
+ global subscription_thread
cherrypy.log.error("Starting osm_nbi")
# update general cherrypy configuration
update_dict = {}
# TODO add more entries, e.g.: storage
cherrypy.tree.apps['/osm'].root.engine.start(engine_config)
cherrypy.tree.apps['/osm'].root.authenticator.start(engine_config)
+ cherrypy.tree.apps['/osm'].root.engine.init_db(target_version=database_version)
+ cherrypy.tree.apps['/osm'].root.authenticator.init_db(target_version=auth_database_version)
+
+ # start subscriptions thread:
+ subscription_thread = SubscriptionThread(config=engine_config, engine=nbi_server.engine)
+ subscription_thread.start()
+ # Do not capture except SubscriptionException
+
+ # load and print version. Ignore possible errors, e.g. file not found
try:
- cherrypy.tree.apps['/osm'].root.engine.init_db(target_version=database_version)
- cherrypy.tree.apps['/osm'].root.authenticator.init_db(target_version=auth_database_version)
- except (EngineException, AuthException):
+ with open("{}/version".format(engine_config["/static"]['tools.staticdir.dir'])) as version_file:
+ version_data = version_file.read()
+ cherrypy.log.error("Starting OSM NBI Version: {}".format(version_data.replace("\n", " ")))
+ except Exception:
pass
- # getenv('OSMOPENMANO_TENANT', None)
def _stop_service():
Callback function called when cherrypy.engine stops
TODO: Ending database connections.
"""
+ global subscription_thread
+ subscription_thread.terminate()
+ subscription_thread = None
cherrypy.tree.apps['/osm'].root.engine.stop()
cherrypy.log.error("Stopping osm_nbi")
def nbi(config_file):
+ global nbi_server
# conf = {
# '/': {
# #'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
# cherrypy.config.update({'tools.auth_basic.on': True,
# 'tools.auth_basic.realm': 'localhost',
# 'tools.auth_basic.checkpassword': validate_password})
+ nbi_server = Server()
cherrypy.engine.subscribe('start', _start_service)
cherrypy.engine.subscribe('stop', _stop_service)
- cherrypy.quickstart(Server(), '/osm', config_file)
+ cherrypy.quickstart(nbi_server, '/osm', config_file)
def usage():