#!/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
__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>]*
"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")}
},
},
}
},
+ "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):
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", "pdu"):
+ 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:
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()
self.engine.db.set_one(rollback_item["topic"], {"_id": rollback_item["_id"]},
rollback_item["content"], fail_on_empty=False)
else:
- self.engine.del_item(**rollback_item, session=session, force=True)
+ 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 = {}
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)
- # getenv('OSMOPENMANO_TENANT', None)
+
+ # 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:
+ 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
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():