import yaml
import html_out as html
import logging
+import logging.handlers
+import getopt
+import sys
from engine import Engine, EngineException
-from dbbase import DbException
-from fsbase import FsException
-from msgbase import MsgException
+from osm_common.dbbase import DbException
+from osm_common.fsbase import FsException
+from osm_common.msgbase import MsgException
from base64 import standard_b64decode
-#from os import getenv
from http import HTTPStatus
-#from http.client import responses as http_responses
from codecs import getreader
-from os import environ
+from os import environ, path
__author__ = "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
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
- /ns_descriptors_content O O
- /<nsdInfoId> O O O O
+ /ns_descriptors_content O O
+ /<nsdInfoId> O O O O
/ns_descriptors O5 O5
/<nsdInfoId> O5 O5 5
/nsd_content O5 O5
/vnfpkgm/v1
/vnf_packages_content O O
- /<vnfPkgId> O O
+ /<vnfPkgId> O O
/vnf_packages O5 O5
/<vnfPkgId> O5 O5 5
/package_content O5 O5
/nslcm/v1
/ns_instances_content O O
- /<nsInstanceId> O O
+ /<nsInstanceId> O O
/ns_instances 5 5
- /<nsInstanceId> 5 5
+ /<nsInstanceId> 5 5
instantiate O5
terminate O5
action O
/ns_lcm_op_occs 5 5
/<nsLcmOpOccId> 5 5 5
TO BE COMPLETED 5 5
+ /vnfrs O
+ /<vnfrId> O
/subscriptions 5 5
/<subscriptionId> 5 X
/admin/v1
/tokens O O
- /<id> O O
+ /<id> O O
/users O O
- /<id> O O
+ /<id> O O
/projects O O
- /<id> O O
+ /<id> O O
/vims_accounts (also vims for compatibility) O O
- /<id> O O O
+ /<id> O O O
/sdns O O
- /<id> O O O
-
-query string.
- <attrName>[.<attrName>...]*[.<op>]=<value>[,<value>...]&...
- op: "eq"(or empty to one or the values) | "neq" (to any of the values) | "gt" | "lt" | "gte" | "lte" | "cont" | "ncont"
- all_fields, fields=x,y,.., exclude_default, exclude_fields=x,y,...
+ /<id> O O O
+
+query string:
+ Follows SOL005 section 4.3.2 It contains extra METHOD to override http method, FORCE to force.
+ 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
+ TODO: 4.3.3 Attribute selectors
+ all_fields, fields=x,y,.., exclude_default, exclude_fields=x,y,...
(none) … same as “exclude_default”
all_fields … all attributes.
- fields=<list> … all attributes except all complex attributes with minimum cardinality of zero that are not conditionally mandatory, and that are not provided in <list>.
- exclude_fields=<list> … all attributes except those complex attributes with a minimum cardinality of zero that are not conditionally mandatory, and that are provided in <list>.
- exclude_default … all attributes except those complex attributes with a minimum cardinality of zero that are not conditionally mandatory, and that are part of the "default exclude set" defined in the present specification for the particular resource
- exclude_default and include=<list> … all attributes except those complex attributes with a minimum cardinality of zero that are not conditionally mandatory and that are part of the "default exclude set" defined in the present specification for the particular resource, but that are not part of <list>
+ fields=<list> … all attributes except all complex attributes with minimum cardinality of zero that are not
+ conditionally mandatory, and that are not provided in <list>.
+ exclude_fields=<list> … all attributes except those complex attributes with a minimum cardinality of zero that
+ are not conditionally mandatory, and that are provided in <list>.
+ exclude_default … all attributes except those complex attributes with a minimum cardinality of zero that are not
+ conditionally mandatory, and that are part of the "default exclude set" defined in the present specification for
+ the particular resource
+ exclude_default and include=<list> … all attributes except those complex attributes with a minimum cardinality
+ of zero that are not conditionally mandatory and that are part of the "default exclude set" defined in the
+ present specification for the particular resource, but that are not part of <list>
Header field name Reference Example Descriptions
Accept IETF RFC 7231 [19] application/json Content-Types that are acceptable for the response.
This header field shall be present if the response is expected to have a non-empty message body.
Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the request.
This header field shall be present if the request has a non-empty message body.
- Authorization IETF RFC 7235 [22] Bearer mF_9.B5f-4.1JqM The authorization token for the request. Details are specified in clause 4.5.3.
+ Authorization IETF RFC 7235 [22] Bearer mF_9.B5f-4.1JqM The authorization token for the request.
+ Details are specified in clause 4.5.3.
Range IETF RFC 7233 [21] 1000-2000 Requested range of bytes from a file
Header field name Reference Example Descriptions
Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the response.
This header field shall be present if the response has a non-empty message body.
- Location IETF RFC 7231 [19] http://www.example.com/vnflcm/v1/vnf_instances/123 Used in redirection, or when a new resource has been created.
+ Location IETF RFC 7231 [19] http://www.example.com/vnflcm/v1/vnf_instances/123 Used in redirection, or when a
+ new resource has been created.
This header field shall be present if the response status code is 201 or 3xx.
- In the present document this header field is also used if the response status code is 202 and a new resource was created.
- WWW-Authenticate IETF RFC 7235 [22] Bearer realm="example" Challenge if the corresponding HTTP request has not provided authorization, or error details if the corresponding HTTP request has provided an invalid authorization token.
- Accept-Ranges IETF RFC 7233 [21] bytes Used by the Server to signal whether or not it supports ranges for certain resources.
- Content-Range IETF RFC 7233 [21] bytes 21010-47021/ 47022 Signals the byte range that is contained in the response, and the total length of the file.
+ In the present document this header field is also used if the response status code is 202 and a new resource was
+ created.
+ WWW-Authenticate IETF RFC 7235 [22] Bearer realm="example" Challenge if the corresponding HTTP request has not
+ provided authorization, or error details if the corresponding HTTP request has provided an invalid authorization
+ token.
+ Accept-Ranges IETF RFC 7233 [21] bytes Used by the Server to signal whether or not it supports ranges for
+ certain resources.
+ Content-Range IETF RFC 7233 [21] bytes 21010-47021/ 47022 Signals the byte range that is contained in the
+ response, and the total length of the file.
Retry-After IETF RFC 7231 [19] Fri, 31 Dec 1999 23:59:59 GMT
-
- or
-
- 120 Used to indicate how long the user agent ought to wait before making a follow-up request.
- It can be used with 503 responses.
- The value of this field can be an HTTP-date or a number of seconds to delay after the response is received.
-
- #TODO http header for partial uploads: Content-Range: "bytes 0-1199/15000". Id is returned first time and send in following chunks
"""
"admin": {
"v1": {
"tokens": {"METHODS": ("GET", "POST", "DELETE"),
- "<ID>": { "METHODS": ("GET", "DELETE")}
- },
+ "<ID>": {"METHODS": ("GET", "DELETE")}
+ },
"users": {"METHODS": ("GET", "POST"),
- "<ID>": {"METHODS": ("GET", "POST", "DELETE")}
- },
+ "<ID>": {"METHODS": ("GET", "POST", "DELETE")}
+ },
"projects": {"METHODS": ("GET", "POST"),
- "<ID>": {"METHODS": ("GET", "DELETE")}
- },
+ "<ID>": {"METHODS": ("GET", "DELETE")}
+ },
"vims": {"METHODS": ("GET", "POST"),
- "<ID>": {"METHODS": ("GET", "DELETE")}
- },
+ "<ID>": {"METHODS": ("GET", "DELETE")}
+ },
"vim_accounts": {"METHODS": ("GET", "POST"),
- "<ID>": {"METHODS": ("GET", "DELETE")}
- },
+ "<ID>": {"METHODS": ("GET", "DELETE", "PATCH")}
+ },
"sdns": {"METHODS": ("GET", "POST"),
- "<ID>": {"METHODS": ("GET", "DELETE")}
- },
+ "<ID>": {"METHODS": ("GET", "DELETE", "PATCH")}
+ },
}
},
"nsd": {
"v1": {
- "ns_descriptors_content": { "METHODS": ("GET", "POST"),
- "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
- },
- "ns_descriptors": { "METHODS": ("GET", "POST"),
- "<ID>": {"METHODS": ("GET", "DELETE"), "TODO": "PATCH",
- "nsd_content": { "METHODS": ("GET", "PUT")},
- "nsd": {"METHODS": "GET"}, # descriptor inside package
- "artifacts": {"*": {"METHODS": "GET"}}
- }
-
- },
+ "ns_descriptors_content": {"METHODS": ("GET", "POST"),
+ "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
+ },
+ "ns_descriptors": {"METHODS": ("GET", "POST"),
+ "<ID>": {"METHODS": ("GET", "DELETE"), "TODO": "PATCH",
+ "nsd_content": {"METHODS": ("GET", "PUT")},
+ "nsd": {"METHODS": "GET"}, # descriptor inside package
+ "artifacts": {"*": {"METHODS": "GET"}}
+ }
+ },
"pnf_descriptors": {"TODO": ("GET", "POST"),
- "<ID>": {"TODO": ("GET", "DELETE", "PATCH"),
- "pnfd_content": {"TODO": ("GET", "PUT")}
- }
- },
+ "<ID>": {"TODO": ("GET", "DELETE", "PATCH"),
+ "pnfd_content": {"TODO": ("GET", "PUT")}
+ }
+ },
"subscriptions": {"TODO": ("GET", "POST"),
- "<ID>": {"TODO": ("GET", "DELETE"),}
- },
+ "<ID>": {"TODO": ("GET", "DELETE")}
+ },
}
},
"vnfpkgm": {
"v1": {
- "vnf_packages_content": { "METHODS": ("GET", "POST"),
- "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
- },
- "vnf_packages": { "METHODS": ("GET", "POST"),
- "<ID>": { "METHODS": ("GET", "DELETE"), "TODO": "PATCH", # GET: vnfPkgInfo
- "package_content": { "METHODS": ("GET", "PUT"), # package
- "upload_from_uri": {"TODO": "POST"}
- },
- "vnfd": {"METHODS": "GET"}, # descriptor inside package
- "artifacts": {"*": {"METHODS": "GET"}}
- }
-
- },
+ "vnf_packages_content": {"METHODS": ("GET", "POST"),
+ "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
+ },
+ "vnf_packages": {"METHODS": ("GET", "POST"),
+ "<ID>": {"METHODS": ("GET", "DELETE"), "TODO": "PATCH", # GET: vnfPkgInfo
+ "package_content": {"METHODS": ("GET", "PUT"), # package
+ "upload_from_uri": {"TODO": "POST"}
+ },
+ "vnfd": {"METHODS": "GET"}, # descriptor inside package
+ "artifacts": {"*": {"METHODS": "GET"}}
+ }
+ },
"subscriptions": {"TODO": ("GET", "POST"),
- "<ID>": {"TODO": ("GET", "DELETE"),}
- },
+ "<ID>": {"TODO": ("GET", "DELETE")}
+ },
}
},
"nslcm": {
"v1": {
"ns_instances_content": {"METHODS": ("GET", "POST"),
- "<ID>": {"METHODS": ("GET", "DELETE")}
- },
+ "<ID>": {"METHODS": ("GET", "DELETE")}
+ },
"ns_instances": {"METHODS": ("GET", "POST"),
- "<ID>": {"TODO": ("GET", "DELETE"),
- "scale": {"TODO": "POST"},
- "terminate": {"METHODS": "POST"},
- "instantiate": {"METHODS": "POST"},
- "action": {"METHODS": "POST"},
- }
- },
+ "<ID>": {"TODO": ("GET", "DELETE"),
+ "scale": {"TODO": "POST"},
+ "terminate": {"METHODS": "POST"},
+ "instantiate": {"METHODS": "POST"},
+ "action": {"METHODS": "POST"},
+ }
+ },
"ns_lcm_op_occs": {"METHODS": "GET",
- "<ID>": {"METHODS": "GET"},
- }
+ "<ID>": {"METHODS": "GET"},
+ },
+ "vnfrs": {"METHODS": ("GET"),
+ "<ID>": {"METHODS": ("GET")}
+ },
}
},
}
raise NbiException(error_text + str(exc), HTTPStatus.BAD_REQUEST)
except KeyError as exc:
raise NbiException("Query string error: " + str(exc), HTTPStatus.BAD_REQUEST)
+ except Exception as exc:
+ raise NbiException(error_text + str(exc), HTTPStatus.BAD_REQUEST)
@staticmethod
def _format_out(data, session=None, _format=None):
outdata = "Index page"
else:
raise cherrypy.HTTPError(HTTPStatus.METHOD_NOT_ALLOWED.value,
- "Method {} not allowed for tokens".format(cherrypy.request.method))
+ "Method {} not allowed for tokens".format(cherrypy.request.method))
return self._format_out(outdata, session)
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
raise NbiException("Unexpected URL item {}".format(arg), HTTPStatus.METHOD_NOT_ALLOWED)
if "TODO" in reference and method in reference["TODO"]:
raise NbiException("Method {} not supported yet for this URL".format(method), HTTPStatus.NOT_IMPLEMENTED)
- elif "METHODS" in reference and not method in reference["METHODS"]:
+ elif "METHODS" in reference and method not in reference["METHODS"]:
raise NbiException("Method {} not supported for this URL".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
return
_format = None
method = "DONE"
engine_item = None
+ rollback = 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)
method = kwargs.pop("METHOD")
else:
method = cherrypy.request.method
+ if kwargs and "FORCE" in kwargs:
+ force = kwargs.pop("FORCE")
+ else:
+ force = False
self._check_valid_url_method(method, topic, version, item, _id, item2, *args)
engine_item = "nsrs"
if item == "ns_lcm_op_occs":
engine_item = "nslcmops"
+ if item == "vnfrs":
+ engine_item = "vnfrs"
if engine_item == "vims": # TODO this is for backward compatibility, it will remove in the future
engine_item = "vim_accounts"
else:
path = None
file, _format = self.engine.get_file(session, engine_item, _id, path,
- cherrypy.request.headers.get("Accept"))
+ cherrypy.request.headers.get("Accept"))
outdata = file
elif not _id:
outdata = self.engine.get_item_list(session, engine_item, kwargs)
if item in ("ns_descriptors_content", "vnf_packages_content"):
_id = cherrypy.request.headers.get("Transaction-Id")
if not _id:
- _id = self.engine.new_item(session, engine_item, {}, None, cherrypy.request.headers)
- completed = self.engine.upload_content(session, engine_item, _id, indata, kwargs, cherrypy.request.headers)
+ _id = self.engine.new_item(session, engine_item, {}, None, cherrypy.request.headers,
+ force=force)
+ rollback = {"session": session, "item": engine_item, "_id": _id, "force": True}
+ completed = self.engine.upload_content(session, engine_item, _id, indata, kwargs,
+ cherrypy.request.headers)
if completed:
self._set_location_header(topic, version, item, _id)
else:
cherrypy.response.headers["Transaction-Id"] = _id
outdata = {"id": _id}
elif item == "ns_instances_content":
- _id = self.engine.new_item(session, engine_item, indata, kwargs)
- self.engine.ns_action(session, _id, "instantiate", {}, None)
+ _id = self.engine.new_item(session, engine_item, indata, kwargs, force=force)
+ rollback = {"session": session, "item": engine_item, "_id": _id, "force": True}
+ self.engine.ns_operation(session, _id, "instantiate", {}, None)
self._set_location_header(topic, version, item, _id)
outdata = {"id": _id}
elif item == "ns_instances" and item2:
- _id = self.engine.ns_action(session, _id, item2, indata, kwargs)
+ _id = self.engine.ns_operation(session, _id, item2, indata, kwargs)
self._set_location_header(topic, version, "ns_lcm_op_occs", _id)
outdata = {"id": _id}
cherrypy.response.status = HTTPStatus.ACCEPTED.value
else:
- _id = self.engine.new_item(session, engine_item, indata, kwargs, cherrypy.request.headers)
+ _id = self.engine.new_item(session, engine_item, indata, kwargs, cherrypy.request.headers,
+ force=force)
self._set_location_header(topic, version, item, _id)
outdata = {"id": _id}
# TODO form NsdInfo when item in ("ns_descriptors", "vnf_packages")
cherrypy.response.status = HTTPStatus.OK.value
else: # len(args) > 1
if item == "ns_instances_content":
- self.engine.ns_action(session, _id, "terminate", {"autoremove": True}, None)
+ opp_id = self.engine.ns_operation(session, _id, "terminate", {"autoremove": True}, None)
+ outdata = {"_id": opp_id}
cherrypy.response.status = HTTPStatus.ACCEPTED.value
else:
- force = kwargs.get("FORCE")
self.engine.del_item(session, engine_item, _id, force)
cherrypy.response.status = HTTPStatus.NO_CONTENT.value
if engine_item in ("vim_accounts", "sdns"):
raise NbiException("Nothing to update. Provide payload and/or query string",
HTTPStatus.BAD_REQUEST)
if item2 in ("nsd_content", "package_content"):
- completed = self.engine.upload_content(session, engine_item, _id, indata, kwargs, cherrypy.request.headers)
+ completed = self.engine.upload_content(session, engine_item, _id, indata, kwargs,
+ cherrypy.request.headers)
if not completed:
cherrypy.response.headers["Transaction-Id"] = id
cherrypy.response.status = HTTPStatus.NO_CONTENT.value
outdata = None
else:
- outdata = {"id": self.engine.edit_item(session, engine_item, args[1], indata, kwargs)}
+ outdata = {"id": self.engine.edit_item(session, engine_item, _id, indata, kwargs, force=force)}
+ elif method == "PATCH":
+ if not indata and not kwargs:
+ raise NbiException("Nothing to update. Provide payload and/or query string",
+ HTTPStatus.BAD_REQUEST)
+ outdata = {"id": self.engine.edit_item(session, engine_item, _id, indata, kwargs, force=force)}
else:
raise NbiException("Method {} not allowed".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
return self._format_out(outdata, session, _format)
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
+ if hasattr(outdata, "close"): # is an open file
+ outdata.close()
+ if rollback:
+ try:
+ self.engine.del_item(**rollback)
+ except Exception as e2:
+ cherrypy.log("Rollback Exception {}: {}".format(rollback, e2))
error_text = str(e)
if isinstance(e, MsgException):
error_text = "{} has been '{}' but other modules cannot be informed because an error on bus".format(
update_dict['server.socket_port'] = int(v)
elif k == 'OSMNBI_SOCKET_HOST' or k == 'OSMNBI_SERVER_HOST':
update_dict['server.socket_host'] = v
- elif k1 == "server":
- update_dict['server' + k2] = v
- # TODO add more entries
+ elif k1 in ("server", "test", "auth", "log"):
+ update_dict[k1 + '.' + k2] = v
elif k1 in ("message", "database", "storage"):
+ # k2 = k2.replace('_', '.')
if k2 == "port":
engine_config[k1][k2] = int(v)
else:
if update_dict:
cherrypy.config.update(update_dict)
+ engine_config["global"].update(update_dict)
# logging cherrypy
log_format_simple = "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(message)s"
logger_cherry = logging.getLogger("cherrypy")
logger_nbi = logging.getLogger("nbi")
- if "logfile" in engine_config["global"]:
- file_handler = logging.handlers.RotatingFileHandler(engine_config["global"]["logfile"],
+ if "log.file" in engine_config["global"]:
+ file_handler = logging.handlers.RotatingFileHandler(engine_config["global"]["log.file"],
maxBytes=100e6, backupCount=9, delay=0)
file_handler.setFormatter(log_formatter_simple)
logger_cherry.addHandler(file_handler)
str_handler.setFormatter(log_formatter_cherry)
logger.addHandler(str_handler)
- if engine_config["global"].get("loglevel"):
- logger_cherry.setLevel(engine_config["global"]["loglevel"])
- logger_nbi.setLevel(engine_config["global"]["loglevel"])
+ if engine_config["global"].get("log.level"):
+ logger_cherry.setLevel(engine_config["global"]["log.level"])
+ logger_nbi.setLevel(engine_config["global"]["log.level"])
# logging other modules
for k1, logname in {"message": "nbi.msg", "database": "nbi.db", "storage": "nbi.fs"}.items():
logger_module = logging.getLogger(logname)
if "logfile" in engine_config[k1]:
file_handler = logging.handlers.RotatingFileHandler(engine_config[k1]["logfile"],
- maxBytes=100e6, backupCount=9, delay=0)
+ maxBytes=100e6, backupCount=9, delay=0)
file_handler.setFormatter(log_formatter_simple)
logger_module.addHandler(file_handler)
if "loglevel" in engine_config[k1]:
cherrypy.tree.apps['/osm'].root.engine.stop()
cherrypy.log.error("Stopping osm_nbi")
-def nbi():
+
+def nbi(config_file):
# conf = {
# '/': {
# #'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
# 'tools.auth_basic.checkpassword': validate_password})
cherrypy.engine.subscribe('start', _start_service)
cherrypy.engine.subscribe('stop', _stop_service)
- cherrypy.quickstart(Server(), '/osm', "nbi.cfg")
+ cherrypy.quickstart(Server(), '/osm', config_file)
+
+
+def usage():
+ print("""Usage: {} [options]
+ -c|--config [configuration_file]: loads the configuration file (default: ./nbi.cfg)
+ -h|--help: shows this help
+ """.format(sys.argv[0]))
+ # --log-socket-host HOST: send logs to this host")
+ # --log-socket-port PORT: send logs using this port (default: 9022)")
if __name__ == '__main__':
- nbi()
+ try:
+ # load parameters and configuration
+ opts, args = getopt.getopt(sys.argv[1:], "hvc:", ["config=", "help"])
+ # TODO add "log-socket-host=", "log-socket-port=", "log-file="
+ config_file = None
+ for o, a in opts:
+ if o in ("-h", "--help"):
+ usage()
+ sys.exit()
+ elif o in ("-c", "--config"):
+ config_file = a
+ # elif o == "--log-socket-port":
+ # log_socket_port = a
+ # elif o == "--log-socket-host":
+ # log_socket_host = a
+ # elif o == "--log-file":
+ # log_file = a
+ else:
+ assert False, "Unhandled option"
+ if config_file:
+ if not path.isfile(config_file):
+ print("configuration file '{}' that not exist".format(config_file), file=sys.stderr)
+ exit(1)
+ else:
+ for config_file in (__file__[:__file__.rfind(".")] + ".cfg", "./nbi.cfg", "/etc/osm/nbi.cfg"):
+ if path.isfile(config_file):
+ break
+ else:
+ print("No configuration file 'nbi.cfg' found neither at local folder nor at /etc/osm/", file=sys.stderr)
+ exit(1)
+ nbi(config_file)
+ except getopt.GetoptError as e:
+ print(str(e), file=sys.stderr)
+ # usage()
+ exit(1)