2 # -*- coding: utf-8 -*-
8 import html_out
as html
10 from engine
import Engine
, EngineException
11 from dbbase
import DbException
12 from base64
import standard_b64decode
14 from http
import HTTPStatus
15 from http
.client
import responses
as http_responses
16 from codecs
import getreader
17 from os
import environ
19 __author__
= "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
21 version_date
= "Feb 2018"
24 North Bound Interface (O: OSM; S: SOL5
25 URL: /osm GET POST PUT DELETE PATCH
40 /package_content O5 O5
42 /artifacts/<artifactPatch X
57 <attrName>[.<attrName>...]*[.<op>]=<value>[,<value>...]&...
58 op: "eq"(or empty to one or the values) | "neq" (to any of the values) | "gt" | "lt" | "gte" | "lte" | "cont" | "ncont"
59 all_fields, fields=x,y,.., exclude_default, exclude_fields=x,y,...
60 (none) … same as “exclude_default”
61 all_fields … all attributes.
62 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>.
63 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>.
64 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
65 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>
66 Header field name Reference Example Descriptions
67 Accept IETF RFC 7231 [19] application/json Content-Types that are acceptable for the response.
68 This header field shall be present if the response is expected to have a non-empty message body.
69 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the request.
70 This header field shall be present if the request has a non-empty message body.
71 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.
72 Range IETF RFC 7233 [21] 1000-2000 Requested range of bytes from a file
73 Header field name Reference Example Descriptions
74 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the response.
75 This header field shall be present if the response has a non-empty message body.
76 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.
77 This header field shall be present if the response status code is 201 or 3xx.
78 In the present document this header field is also used if the response status code is 202 and a new resource was created.
79 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.
80 Accept-Ranges IETF RFC 7233 [21] bytes Used by the Server to signal whether or not it supports ranges for certain resources.
81 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.
82 Retry-After IETF RFC 7231 [19] Fri, 31 Dec 1999 23:59:59 GMT
86 120 Used to indicate how long the user agent ought to wait before making a follow-up request.
87 It can be used with 503 responses.
88 The value of this field can be an HTTP-date or a number of seconds to delay after the response is received.
90 #TODO http header for partial uploads: Content-Range: "bytes 0-1199/15000". Id is returned first time and send in following chunks
94 class NbiException(Exception):
96 def __init__(self
, message
, http_code
=HTTPStatus
.METHOD_NOT_ALLOWED
):
97 Exception.__init
__(self
, message
)
98 self
.http_code
= http_code
101 class Server(object):
103 # to decode bytes to str
104 reader
= getreader("utf-8")
108 self
.engine
= Engine()
110 def _authorization(self
):
114 # 1. Get token Authorization bearer
115 auth
= cherrypy
.request
.headers
.get("Authorization")
117 auth_list
= auth
.split(" ")
118 if auth_list
[0].lower() == "bearer":
119 token
= auth_list
[-1]
120 elif auth_list
[0].lower() == "basic":
121 user_passwd64
= auth_list
[-1]
123 if cherrypy
.session
.get("Authorization"):
124 # 2. Try using session before request a new token. If not, basic authentication will generate
125 token
= cherrypy
.session
.get("Authorization")
126 if token
== "logout":
127 token
= None # force Unauthorized response to insert user pasword again
128 elif user_passwd64
and cherrypy
.request
.config
.get("auth.allow_basic_authentication"):
129 # 3. Get new token from user password
133 user_passwd
= standard_b64decode(user_passwd64
).decode()
134 user
, _
, passwd
= user_passwd
.partition(":")
137 outdata
= self
.engine
.new_token(None, {"username": user
, "password": passwd
})
138 token
= outdata
["id"]
139 cherrypy
.session
['Authorization'] = token
140 # 4. Get token from cookie
142 # auth_cookie = cherrypy.request.cookie.get("Authorization")
144 # token = auth_cookie.value
145 return self
.engine
.authorize(token
)
146 except EngineException
as e
:
147 if cherrypy
.session
.get('Authorization'):
148 del cherrypy
.session
['Authorization']
149 cherrypy
.response
.headers
["WWW-Authenticate"] = 'Bearer realm="{}"'.format(e
)
152 def _format_in(self
, kwargs
):
155 if cherrypy
.request
.body
.length
:
156 error_text
= "Invalid input format "
158 if "Content-Type" in cherrypy
.request
.headers
:
159 if "application/json" in cherrypy
.request
.headers
["Content-Type"]:
160 error_text
= "Invalid json format "
161 indata
= json
.load(self
.reader(cherrypy
.request
.body
))
162 elif "application/yaml" in cherrypy
.request
.headers
["Content-Type"]:
163 error_text
= "Invalid yaml format "
164 indata
= yaml
.load(cherrypy
.request
.body
)
165 elif "application/binary" in cherrypy
.request
.headers
["Content-Type"] or \
166 "application/gzip" in cherrypy
.request
.headers
["Content-Type"] or \
167 "application/zip" in cherrypy
.request
.headers
["Content-Type"]:
168 indata
= cherrypy
.request
.body
.read()
169 elif "multipart/form-data" in cherrypy
.request
.headers
["Content-Type"]:
170 if "descriptor_file" in kwargs
:
171 filecontent
= kwargs
.pop("descriptor_file")
172 if not filecontent
.file:
173 raise NbiException("empty file or content", HTTPStatus
.BAD_REQUEST
)
174 indata
= filecontent
.file.read()
175 if filecontent
.content_type
.value
:
176 cherrypy
.request
.headers
["Content-Type"] = filecontent
.content_type
.value
178 # raise cherrypy.HTTPError(HTTPStatus.Not_Acceptable,
179 # "Only 'Content-Type' of type 'application/json' or
180 # 'application/yaml' for input format are available")
181 error_text
= "Invalid yaml format "
182 indata
= yaml
.load(cherrypy
.request
.body
)
184 error_text
= "Invalid yaml format "
185 indata
= yaml
.load(cherrypy
.request
.body
)
189 if "METHOD" in kwargs
:
190 method
= kwargs
.pop("METHOD")
192 method
= cherrypy
.request
.method
194 if cherrypy
.request
.headers
.get("Query-String-Format") == "yaml":
197 for k
, v
in kwargs
.items():
198 if isinstance(v
, str):
203 kwargs
[k
] = yaml
.load(v
)
206 elif k
.endswith(".gt") or k
.endswith(".lt") or k
.endswith(".gte") or k
.endswith(".lte"):
214 elif v
.find(",") > 0:
215 kwargs
[k
] = v
.split(",")
216 elif isinstance(v
, (list, tuple)):
217 for index
in range(0, len(v
)):
222 v
[index
] = yaml
.load(v
[index
])
226 return indata
, method
227 except (ValueError, yaml
.YAMLError
) as exc
:
228 raise NbiException(error_text
+ str(exc
), HTTPStatus
.BAD_REQUEST
)
229 except KeyError as exc
:
230 raise NbiException("Query string error: " + str(exc
), HTTPStatus
.BAD_REQUEST
)
233 def _format_out(data
, session
=None):
235 return string of dictionary data according to requested json, yaml, xml. By default json
236 :param data: response to be sent. Can be a dict or text
240 if "Accept" in cherrypy
.request
.headers
:
241 accept
= cherrypy
.request
.headers
["Accept"]
242 if "application/json" in accept
:
243 cherrypy
.response
.headers
["Content-Type"] = 'application/json; charset=utf-8'
244 a
= json
.dumps(data
, indent
=4) + "\n"
245 return a
.encode("utf8")
246 elif "text/html" in accept
:
247 return html
.format(data
, cherrypy
.request
, cherrypy
.response
, session
)
249 elif "application/yaml" in accept
or "*/*" in accept
:
252 raise cherrypy
.HTTPError(HTTPStatus
.NOT_ACCEPTABLE
.value
,
253 "Only 'Accept' of type 'application/json' or 'application/yaml' "
254 "for output format are available")
255 cherrypy
.response
.headers
["Content-Type"] = 'application/yaml'
256 return yaml
.safe_dump(data
, explicit_start
=True, indent
=4, default_flow_style
=False, tags
=False,
257 encoding
='utf-8', allow_unicode
=True) # , canonical=True, default_style='"'
260 def index(self
, *args
, **kwargs
):
263 if cherrypy
.request
.method
== "GET":
264 session
= self
._authorization
()
265 outdata
= "Index page"
267 raise cherrypy
.HTTPError(HTTPStatus
.METHOD_NOT_ALLOWED
.value
,
268 "Method {} not allowed for tokens".format(cherrypy
.request
.method
))
270 return self
._format
_out
(outdata
, session
)
272 except EngineException
as e
:
273 cherrypy
.log("index Exception {}".format(e
))
274 cherrypy
.response
.status
= e
.http_code
.value
275 return self
._format
_out
("Welcome to OSM!", session
)
278 def token(self
, *args
, **kwargs
):
280 raise NbiException("URL must contain at least 'item/version'", HTTPStatus
.METHOD_NOT_ALLOWED
)
283 raise NbiException("URL version '{}' not supported".format(version
), HTTPStatus
.METHOD_NOT_ALLOWED
)
285 # self.engine.load_dbase(cherrypy.request.app.config)
287 indata
, method
= self
._format
_in
(kwargs
)
289 session
= self
._authorization
()
291 outdata
= self
.engine
.get_token(session
, args
[1])
293 outdata
= self
.engine
.get_token_list(session
)
294 elif method
== "POST":
296 session
= self
._authorization
()
300 indata
.update(kwargs
)
301 outdata
= self
.engine
.new_token(session
, indata
, cherrypy
.request
.remote
)
303 cherrypy
.session
['Authorization'] = outdata
["_id"]
304 # cherrypy.response.cookie["Authorization"] = outdata["id"]
305 # cherrypy.response.cookie["Authorization"]['expires'] = 3600
306 elif method
== "DELETE":
307 if len(args
) >= 2 and "logout" not in args
:
310 token_id
= kwargs
["id"]
312 session
= self
._authorization
()
313 token_id
= session
["_id"]
314 outdata
= self
.engine
.del_token(token_id
)
316 cherrypy
.session
['Authorization'] = "logout"
317 # cherrypy.response.cookie["Authorization"] = token_id
318 # cherrypy.response.cookie["Authorization"]['expires'] = 0
320 raise NbiException("Method {} not allowed for token".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
321 return self
._format
_out
(outdata
, session
)
322 except (NbiException
, EngineException
, DbException
) as e
:
323 cherrypy
.log("tokens Exception {}".format(e
))
324 cherrypy
.response
.status
= e
.http_code
.value
326 "code": e
.http_code
.name
,
327 "status": e
.http_code
.value
,
330 return self
._format
_out
(problem_details
, session
)
333 def test(self
, *args
, **kwargs
):
335 if args
and args
[0] == "init":
337 # self.engine.load_dbase(cherrypy.request.app.config)
338 self
.engine
.create_admin()
339 return "Done. User 'admin', password 'admin' created"
341 cherrypy
.response
.status
= HTTPStatus
.FORBIDDEN
.value
342 return self
._format
_out
("Database already initialized")
343 elif args
and args
[0] == "prune":
344 return self
.engine
.prune()
345 elif args
and args
[0] == "login":
346 if not cherrypy
.request
.headers
.get("Authorization"):
347 cherrypy
.response
.headers
["WWW-Authenticate"] = 'Basic realm="Access to OSM site", charset="UTF-8"'
348 cherrypy
.response
.status
= HTTPStatus
.UNAUTHORIZED
.value
349 elif args
and args
[0] == "login2":
350 if not cherrypy
.request
.headers
.get("Authorization"):
351 cherrypy
.response
.headers
["WWW-Authenticate"] = 'Bearer realm="Access to OSM site"'
352 cherrypy
.response
.status
= HTTPStatus
.UNAUTHORIZED
.value
353 elif args
and args
[0] == "sleep":
356 sleep_time
= int(args
[1])
358 cherrypy
.response
.status
= HTTPStatus
.FORBIDDEN
.value
359 return self
._format
_out
("Database already initialized")
360 thread_info
= cherrypy
.thread_data
362 time
.sleep(sleep_time
)
364 elif len(args
) >= 2 and args
[0] == "message":
367 for k
, v
in kwargs
.items():
368 self
.engine
.msg
.write(topic
, k
, yaml
.load(v
))
370 except Exception as e
:
371 return "Error: " + format(e
)
374 "<html><pre>\nheaders:\n args: {}\n".format(args
) +
375 " kwargs: {}\n".format(kwargs
) +
376 " headers: {}\n".format(cherrypy
.request
.headers
) +
377 " path_info: {}\n".format(cherrypy
.request
.path_info
) +
378 " query_string: {}\n".format(cherrypy
.request
.query_string
) +
379 " session: {}\n".format(cherrypy
.session
) +
380 " cookie: {}\n".format(cherrypy
.request
.cookie
) +
381 " method: {}\n".format(cherrypy
.request
.method
) +
382 " session: {}\n".format(cherrypy
.session
.get('fieldname')) +
384 return_text
+= " length: {}\n".format(cherrypy
.request
.body
.length
)
385 if cherrypy
.request
.body
.length
:
386 return_text
+= " content: {}\n".format(
387 str(cherrypy
.request
.body
.read(int(cherrypy
.request
.headers
.get('Content-Length', 0)))))
389 return_text
+= "thread: {}\n".format(thread_info
)
390 return_text
+= "</pre></html>"
394 def default(self
, *args
, **kwargs
):
397 if not args
or len(args
) < 2:
398 raise NbiException("URL must contain at least 'item/version'", HTTPStatus
.METHOD_NOT_ALLOWED
)
401 if item
not in ("token", "user", "project", "vnfpkgm", "nsd", "nslcm"):
402 raise NbiException("URL item '{}' not supported".format(item
), HTTPStatus
.METHOD_NOT_ALLOWED
)
404 raise NbiException("URL version '{}' not supported".format(version
), HTTPStatus
.METHOD_NOT_ALLOWED
)
406 # self.engine.load_dbase(cherrypy.request.app.config)
407 session
= self
._authorization
()
408 indata
, method
= self
._format
_in
(kwargs
)
413 if len(args
) < 3 or args
[2] != "ns_descriptors":
414 raise NbiException("only ns_descriptors is allowed", HTTPStatus
.METHOD_NOT_ALLOWED
)
417 if len(args
) > 4 and args
[4] != "nsd_content":
418 raise NbiException("only nsd_content is allowed", HTTPStatus
.METHOD_NOT_ALLOWED
)
419 elif item
== "vnfpkgm":
421 if len(args
) < 3 or args
[2] != "vnf_packages":
422 raise NbiException("only vnf_packages is allowed", HTTPStatus
.METHOD_NOT_ALLOWED
)
425 if len(args
) > 4 and args
[4] not in ("vnfd", "package_content"):
426 raise NbiException("only vnfd or package_content are allowed", HTTPStatus
.METHOD_NOT_ALLOWED
)
427 elif item
== "nslcm":
429 if len(args
) < 3 or args
[2] != "ns_instances":
430 raise NbiException("only ns_instances is allowed", HTTPStatus
.METHOD_NOT_ALLOWED
)
434 raise NbiException("This feature is not implemented", HTTPStatus
.METHOD_NOT_ALLOWED
)
442 outdata
= self
.engine
.get_item_list(session
, item
, kwargs
)
443 else: # len(args) > 1
444 outdata
= self
.engine
.get_item(session
, item
, _id
)
445 elif method
== "POST":
446 id, completed
= self
.engine
.new_item(session
, item
, indata
, kwargs
, cherrypy
.request
.headers
)
448 cherrypy
.response
.headers
["Transaction-Id"] = id
449 cherrypy
.response
.status
= HTTPStatus
.CREATED
.value
451 cherrypy
.response
.headers
["Location"] = cherrypy
.request
.base
+ "/osm/" + "/".join(args
[0:3]) + "/" + id
453 elif method
== "DELETE":
455 outdata
= self
.engine
.del_item_list(session
, item
, kwargs
)
456 else: # len(args) > 1
457 outdata
= self
.engine
.del_item(session
, item
, _id
)
458 elif method
== "PUT":
460 raise NbiException("Missing '/<id>' at the URL to identify item to be updated",
461 HTTPStatus
.METHOD_NOT_ALLOWED
)
462 elif not indata
and not kwargs
:
463 raise NbiException("Nothing to update. Provide payload and/or query string",
464 HTTPStatus
.BAD_REQUEST
)
465 outdata
= {"id": self
.engine
.edit_item(session
, item
, args
[1], indata
, kwargs
)}
467 raise NbiException("Method {} not allowed".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
468 return self
._format
_out
(outdata
, session
)
469 except (NbiException
, EngineException
, DbException
) as e
:
470 cherrypy
.log("Exception {}".format(e
))
471 cherrypy
.response
.status
= e
.http_code
.value
473 "code": e
.http_code
.name
,
474 "status": e
.http_code
.value
,
477 return self
._format
_out
(problem_details
, session
)
478 # raise cherrypy.HTTPError(e.http_code.value, str(e))
481 # def validate_password(realm, username, password):
482 # cherrypy.log("realm "+ str(realm))
483 # if username == "admin" and password == "admin":
488 def _start_service():
490 Callback function called when cherrypy.engine starts
491 Override configuration with env variables
492 Set database, storage, message configuration
493 Init database with admin/admin user password
495 cherrypy
.log
.error("Starting osm_nbi")
496 # update general cherrypy configuration
499 engine_config
= cherrypy
.tree
.apps
['/osm'].config
500 for k
, v
in environ
.items():
501 if not k
.startswith("OSMNBI_"):
503 k1
, _
, k2
= k
[7:].lower().partition("_")
507 # update static configuration
508 if k
== 'OSMNBI_STATIC_DIR':
509 engine_config
["/static"]['tools.staticdir.dir'] = v
510 engine_config
["/static"]['tools.staticdir.on'] = True
511 elif k
== 'OSMNBI_SOCKET_PORT' or k
== 'OSMNBI_SERVER_PORT':
512 update_dict
['server.socket_port'] = int(v
)
513 elif k
== 'OSMNBI_SOCKET_HOST' or k
== 'OSMNBI_SERVER_HOST':
514 update_dict
['server.socket_host'] = v
516 update_dict
['server' + k2
] = v
517 # TODO add more entries
518 elif k1
in ("message", "database", "storage"):
520 engine_config
[k1
][k2
] = int(v
)
522 engine_config
[k1
][k2
] = v
523 except ValueError as e
:
524 cherrypy
.log
.error("Ignoring environ '{}': " + str(e
))
525 except Exception as e
:
526 cherrypy
.log
.warn("skipping environ '{}' on exception '{}'".format(k
, e
))
529 cherrypy
.config
.update(update_dict
)
532 log_format_simple
= "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(message)s"
533 log_formatter_simple
= logging
.Formatter(log_format_simple
, datefmt
='%Y-%m-%dT%H:%M:%S')
534 logger_server
= logging
.getLogger("cherrypy.error")
535 logger_access
= logging
.getLogger("cherrypy.access")
536 logger_cherry
= logging
.getLogger("cherrypy")
537 logger_nbi
= logging
.getLogger("nbi")
539 if "logfile" in engine_config
["global"]:
540 file_handler
= logging
.handlers
.RotatingFileHandler(engine_config
["global"]["logfile"],
541 maxBytes
=100e6
, backupCount
=9, delay
=0)
542 file_handler
.setFormatter(log_formatter_simple
)
543 logger_cherry
.addHandler(file_handler
)
544 logger_nbi
.addHandler(file_handler
)
546 for format_
, logger
in {"nbi.server": logger_server
,
547 "nbi.access": logger_access
,
548 "%(name)s %(filename)s:%(lineno)s": logger_nbi
550 log_format_cherry
= "%(asctime)s %(levelname)s {} %(message)s".format(format_
)
551 log_formatter_cherry
= logging
.Formatter(log_format_cherry
, datefmt
='%Y-%m-%dT%H:%M:%S')
552 str_handler
= logging
.StreamHandler()
553 str_handler
.setFormatter(log_formatter_cherry
)
554 logger
.addHandler(str_handler
)
556 if engine_config
["global"].get("loglevel"):
557 logger_cherry
.setLevel(engine_config
["global"]["loglevel"])
558 logger_nbi
.setLevel(engine_config
["global"]["loglevel"])
560 # logging other modules
561 for k1
, logname
in {"message": "nbi.msg", "database": "nbi.db", "storage": "nbi.fs"}.items():
562 engine_config
[k1
]["logger_name"] = logname
563 logger_module
= logging
.getLogger(logname
)
564 if "logfile" in engine_config
[k1
]:
565 file_handler
= logging
.handlers
.RotatingFileHandler(engine_config
[k1
]["logfile"],
566 maxBytes
=100e6
, backupCount
=9, delay
=0)
567 file_handler
.setFormatter(log_formatter_simple
)
568 logger_module
.addHandler(file_handler
)
569 if "loglevel" in engine_config
[k1
]:
570 logger_module
.setLevel(engine_config
[k1
]["loglevel"])
571 # TODO add more entries, e.g.: storage
572 cherrypy
.tree
.apps
['/osm'].root
.engine
.start(engine_config
)
574 cherrypy
.tree
.apps
['/osm'].root
.engine
.create_admin()
575 except EngineException
:
577 # getenv('OSMOPENMANO_TENANT', None)
582 Callback function called when cherrypy.engine stops
583 TODO: Ending database connections.
585 cherrypy
.tree
.apps
['/osm'].root
.engine
.stop()
586 cherrypy
.log
.error("Stopping osm_nbi")
591 # #'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
592 # 'tools.sessions.on': True,
593 # 'tools.response_headers.on': True,
594 # # 'tools.response_headers.headers': [('Content-Type', 'text/plain')],
597 # cherrypy.Server.ssl_module = 'builtin'
598 # cherrypy.Server.ssl_certificate = "http/cert.pem"
599 # cherrypy.Server.ssl_private_key = "http/privkey.pem"
600 # cherrypy.Server.thread_pool = 10
601 # cherrypy.config.update({'Server.socket_port': config["port"], 'Server.socket_host': config["host"]})
603 # cherrypy.config.update({'tools.auth_basic.on': True,
604 # 'tools.auth_basic.realm': 'localhost',
605 # 'tools.auth_basic.checkpassword': validate_password})
606 cherrypy
.engine
.subscribe('start', _start_service
)
607 cherrypy
.engine
.subscribe('stop', _stop_service
)
608 cherrypy
.quickstart(Server(), '/osm', "nbi.cfg")
611 if __name__
== '__main__':