2 # -*- coding: utf-8 -*-
8 import html_out
as html
10 import logging
.handlers
14 from authconn
import AuthException
15 from auth
import Authenticator
16 from engine
import Engine
, EngineException
17 from osm_common
.dbbase
import DbException
18 from osm_common
.fsbase
import FsException
19 from osm_common
.msgbase
import MsgException
20 from http
import HTTPStatus
21 from codecs
import getreader
22 from os
import environ
, path
24 __author__
= "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
26 # TODO consider to remove and provide version using the static version file
28 version_date
= "Apr 2018"
29 database_version
= '1.0'
30 auth_database_version
= '1.0'
33 North Bound Interface (O: OSM specific; 5,X: SOL005 not implemented yet; O5: SOL005 implemented)
34 URL: /osm GET POST PUT DELETE PATCH
36 /ns_descriptors_content O O
42 /artifacts[/<artifactPath>] O
50 /vnf_packages_content O O
54 /package_content O5 O5
57 /artifacts[/<artifactPath>] O5
62 /ns_instances_content O O
74 /vnf_instances (also vnfrs for compatibility) O
90 /vims_accounts (also vims for compatibility) O O
96 /netslice_templates_content O O
98 /netslice_templates O O
102 /artifacts[/<artifactPath>] O
104 /<subscriptionId> X X
107 /netslice_instances_content O O
108 /<SliceInstanceId> O O
109 /netslice_instances O O
110 /<SliceInstanceId> O O
115 /<nsiLcmOpOccId> O O O
117 /<subscriptionId> X X
120 Follows SOL005 section 4.3.2 It contains extra METHOD to override http method, FORCE to force.
121 For filtering inside array, it must select the element of the array, or add ANYINDEX to apply the filtering over any
122 item of the array, that is, pass if any item of the array pass the filter.
123 It allows both ne and neq for not equal
124 TODO: 4.3.3 Attribute selectors
125 all_fields, fields=x,y,.., exclude_default, exclude_fields=x,y,...
126 (none) … same as “exclude_default”
127 all_fields … all attributes.
128 fields=<list> … all attributes except all complex attributes with minimum cardinality of zero that are not
129 conditionally mandatory, and that are not provided in <list>.
130 exclude_fields=<list> … all attributes except those complex attributes with a minimum cardinality of zero that
131 are not conditionally mandatory, and that are provided in <list>.
132 exclude_default … all attributes except those complex attributes with a minimum cardinality of zero that are not
133 conditionally mandatory, and that are part of the "default exclude set" defined in the present specification for
134 the particular resource
135 exclude_default and include=<list> … all attributes except those complex attributes with a minimum cardinality
136 of zero that are not conditionally mandatory and that are part of the "default exclude set" defined in the
137 present specification for the particular resource, but that are not part of <list>
138 Header field name Reference Example Descriptions
139 Accept IETF RFC 7231 [19] application/json Content-Types that are acceptable for the response.
140 This header field shall be present if the response is expected to have a non-empty message body.
141 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the request.
142 This header field shall be present if the request has a non-empty message body.
143 Authorization IETF RFC 7235 [22] Bearer mF_9.B5f-4.1JqM The authorization token for the request.
144 Details are specified in clause 4.5.3.
145 Range IETF RFC 7233 [21] 1000-2000 Requested range of bytes from a file
146 Header field name Reference Example Descriptions
147 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the response.
148 This header field shall be present if the response has a non-empty message body.
149 Location IETF RFC 7231 [19] http://www.example.com/vnflcm/v1/vnf_instances/123 Used in redirection, or when a
150 new resource has been created.
151 This header field shall be present if the response status code is 201 or 3xx.
152 In the present document this header field is also used if the response status code is 202 and a new resource was
154 WWW-Authenticate IETF RFC 7235 [22] Bearer realm="example" Challenge if the corresponding HTTP request has not
155 provided authorization, or error details if the corresponding HTTP request has provided an invalid authorization
157 Accept-Ranges IETF RFC 7233 [21] bytes Used by the Server to signal whether or not it supports ranges for
159 Content-Range IETF RFC 7233 [21] bytes 21010-47021/ 47022 Signals the byte range that is contained in the
160 response, and the total length of the file.
161 Retry-After IETF RFC 7231 [19] Fri, 31 Dec 1999 23:59:59 GMT
165 class NbiException(Exception):
167 def __init__(self
, message
, http_code
=HTTPStatus
.METHOD_NOT_ALLOWED
):
168 Exception.__init
__(self
, message
)
169 self
.http_code
= http_code
172 class Server(object):
174 # to decode bytes to str
175 reader
= getreader("utf-8")
179 self
.engine
= Engine()
180 self
.authenticator
= Authenticator()
181 self
.valid_methods
= { # contains allowed URL and methods
184 "tokens": {"METHODS": ("GET", "POST", "DELETE"),
185 "<ID>": {"METHODS": ("GET", "DELETE")}
187 "users": {"METHODS": ("GET", "POST"),
188 "<ID>": {"METHODS": ("GET", "POST", "DELETE", "PATCH", "PUT")}
190 "projects": {"METHODS": ("GET", "POST"),
191 "<ID>": {"METHODS": ("GET", "DELETE")}
193 "vims": {"METHODS": ("GET", "POST"),
194 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
196 "vim_accounts": {"METHODS": ("GET", "POST"),
197 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
199 "sdns": {"METHODS": ("GET", "POST"),
200 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
206 "pdu_descriptors": {"METHODS": ("GET", "POST"),
207 "<ID>": {"METHODS": ("GET", "POST", "DELETE", "PATCH", "PUT")}
213 "ns_descriptors_content": {"METHODS": ("GET", "POST"),
214 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
216 "ns_descriptors": {"METHODS": ("GET", "POST"),
217 "<ID>": {"METHODS": ("GET", "DELETE"), "TODO": "PATCH",
218 "nsd_content": {"METHODS": ("GET", "PUT")},
219 "nsd": {"METHODS": "GET"}, # descriptor inside package
220 "artifacts": {"*": {"METHODS": "GET"}}
223 "pnf_descriptors": {"TODO": ("GET", "POST"),
224 "<ID>": {"TODO": ("GET", "DELETE", "PATCH"),
225 "pnfd_content": {"TODO": ("GET", "PUT")}
228 "subscriptions": {"TODO": ("GET", "POST"),
229 "<ID>": {"TODO": ("GET", "DELETE")}
235 "vnf_packages_content": {"METHODS": ("GET", "POST"),
236 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
238 "vnf_packages": {"METHODS": ("GET", "POST"),
239 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"), # GET: vnfPkgInfo
240 "package_content": {"METHODS": ("GET", "PUT"), # package
241 "upload_from_uri": {"TODO": "POST"}
243 "vnfd": {"METHODS": "GET"}, # descriptor inside package
244 "artifacts": {"*": {"METHODS": "GET"}}
247 "subscriptions": {"TODO": ("GET", "POST"),
248 "<ID>": {"TODO": ("GET", "DELETE")}
254 "ns_instances_content": {"METHODS": ("GET", "POST"),
255 "<ID>": {"METHODS": ("GET", "DELETE")}
257 "ns_instances": {"METHODS": ("GET", "POST"),
258 "<ID>": {"METHODS": ("GET", "DELETE"),
259 "scale": {"METHODS": "POST"},
260 "terminate": {"METHODS": "POST"},
261 "instantiate": {"METHODS": "POST"},
262 "action": {"METHODS": "POST"},
265 "ns_lcm_op_occs": {"METHODS": "GET",
266 "<ID>": {"METHODS": "GET"},
268 "vnfrs": {"METHODS": ("GET"),
269 "<ID>": {"METHODS": ("GET")}
271 "vnf_instances": {"METHODS": ("GET"),
272 "<ID>": {"METHODS": ("GET")}
278 "netslice_templates_content": {"METHODS": ("GET", "POST"),
279 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
281 "netslice_templates": {"METHODS": ("GET", "POST"),
282 "<ID>": {"METHODS": ("GET", "DELETE"), "TODO": "PATCH",
283 "nst_content": {"METHODS": ("GET", "PUT")},
284 "nst": {"METHODS": "GET"}, # descriptor inside package
285 "artifacts": {"*": {"METHODS": "GET"}}
288 "subscriptions": {"TODO": ("GET", "POST"),
289 "<ID>": {"TODO": ("GET", "DELETE")}
295 "netslice_instances_content": {"METHODS": ("GET", "POST"),
296 "<ID>": {"METHODS": ("GET", "DELETE")}
298 "netslice_instances": {"METHODS": ("GET", "POST"),
299 "<ID>": {"METHODS": ("GET", "DELETE"),
300 "terminate": {"METHODS": "POST"},
301 "instantiate": {"METHODS": "POST"},
302 "action": {"METHODS": "POST"},
305 "nsi_lcm_op_occs": {"METHODS": "GET",
306 "<ID>": {"METHODS": "GET"},
312 def _format_in(self
, kwargs
):
315 if cherrypy
.request
.body
.length
:
316 error_text
= "Invalid input format "
318 if "Content-Type" in cherrypy
.request
.headers
:
319 if "application/json" in cherrypy
.request
.headers
["Content-Type"]:
320 error_text
= "Invalid json format "
321 indata
= json
.load(self
.reader(cherrypy
.request
.body
))
322 cherrypy
.request
.headers
.pop("Content-File-MD5", None)
323 elif "application/yaml" in cherrypy
.request
.headers
["Content-Type"]:
324 error_text
= "Invalid yaml format "
325 indata
= yaml
.load(cherrypy
.request
.body
)
326 cherrypy
.request
.headers
.pop("Content-File-MD5", None)
327 elif "application/binary" in cherrypy
.request
.headers
["Content-Type"] or \
328 "application/gzip" in cherrypy
.request
.headers
["Content-Type"] or \
329 "application/zip" in cherrypy
.request
.headers
["Content-Type"] or \
330 "text/plain" in cherrypy
.request
.headers
["Content-Type"]:
331 indata
= cherrypy
.request
.body
# .read()
332 elif "multipart/form-data" in cherrypy
.request
.headers
["Content-Type"]:
333 if "descriptor_file" in kwargs
:
334 filecontent
= kwargs
.pop("descriptor_file")
335 if not filecontent
.file:
336 raise NbiException("empty file or content", HTTPStatus
.BAD_REQUEST
)
337 indata
= filecontent
.file # .read()
338 if filecontent
.content_type
.value
:
339 cherrypy
.request
.headers
["Content-Type"] = filecontent
.content_type
.value
341 # raise cherrypy.HTTPError(HTTPStatus.Not_Acceptable,
342 # "Only 'Content-Type' of type 'application/json' or
343 # 'application/yaml' for input format are available")
344 error_text
= "Invalid yaml format "
345 indata
= yaml
.load(cherrypy
.request
.body
)
346 cherrypy
.request
.headers
.pop("Content-File-MD5", None)
348 error_text
= "Invalid yaml format "
349 indata
= yaml
.load(cherrypy
.request
.body
)
350 cherrypy
.request
.headers
.pop("Content-File-MD5", None)
355 if cherrypy
.request
.headers
.get("Query-String-Format") == "yaml":
358 for k
, v
in kwargs
.items():
359 if isinstance(v
, str):
364 kwargs
[k
] = yaml
.load(v
)
367 elif k
.endswith(".gt") or k
.endswith(".lt") or k
.endswith(".gte") or k
.endswith(".lte"):
375 elif v
.find(",") > 0:
376 kwargs
[k
] = v
.split(",")
377 elif isinstance(v
, (list, tuple)):
378 for index
in range(0, len(v
)):
383 v
[index
] = yaml
.load(v
[index
])
388 except (ValueError, yaml
.YAMLError
) as exc
:
389 raise NbiException(error_text
+ str(exc
), HTTPStatus
.BAD_REQUEST
)
390 except KeyError as exc
:
391 raise NbiException("Query string error: " + str(exc
), HTTPStatus
.BAD_REQUEST
)
392 except Exception as exc
:
393 raise NbiException(error_text
+ str(exc
), HTTPStatus
.BAD_REQUEST
)
396 def _format_out(data
, session
=None, _format
=None):
398 return string of dictionary data according to requested json, yaml, xml. By default json
399 :param data: response to be sent. Can be a dict, text or file
401 :param _format: The format to be set as Content-Type ir data is a file
404 accept
= cherrypy
.request
.headers
.get("Accept")
406 if accept
and "text/html" in accept
:
407 return html
.format(data
, cherrypy
.request
, cherrypy
.response
, session
)
408 # cherrypy.response.status = HTTPStatus.NO_CONTENT.value
410 elif hasattr(data
, "read"): # file object
412 cherrypy
.response
.headers
["Content-Type"] = _format
413 elif "b" in data
.mode
: # binariy asssumig zip
414 cherrypy
.response
.headers
["Content-Type"] = 'application/zip'
416 cherrypy
.response
.headers
["Content-Type"] = 'text/plain'
417 # TODO check that cherrypy close file. If not implement pending things to close per thread next
420 if "application/json" in accept
:
421 cherrypy
.response
.headers
["Content-Type"] = 'application/json; charset=utf-8'
422 a
= json
.dumps(data
, indent
=4) + "\n"
423 return a
.encode("utf8")
424 elif "text/html" in accept
:
425 return html
.format(data
, cherrypy
.request
, cherrypy
.response
, session
)
427 elif "application/yaml" in accept
or "*/*" in accept
or "text/plain" in accept
:
430 raise cherrypy
.HTTPError(HTTPStatus
.NOT_ACCEPTABLE
.value
,
431 "Only 'Accept' of type 'application/json' or 'application/yaml' "
432 "for output format are available")
433 cherrypy
.response
.headers
["Content-Type"] = 'application/yaml'
434 return yaml
.safe_dump(data
, explicit_start
=True, indent
=4, default_flow_style
=False, tags
=False,
435 encoding
='utf-8', allow_unicode
=True) # , canonical=True, default_style='"'
438 def index(self
, *args
, **kwargs
):
441 if cherrypy
.request
.method
== "GET":
442 session
= self
.authenticator
.authorize()
443 outdata
= "Index page"
445 raise cherrypy
.HTTPError(HTTPStatus
.METHOD_NOT_ALLOWED
.value
,
446 "Method {} not allowed for tokens".format(cherrypy
.request
.method
))
448 return self
._format
_out
(outdata
, session
)
450 except (EngineException
, AuthException
) as e
:
451 cherrypy
.log("index Exception {}".format(e
))
452 cherrypy
.response
.status
= e
.http_code
.value
453 return self
._format
_out
("Welcome to OSM!", session
)
456 def version(self
, *args
, **kwargs
):
457 # TODO consider to remove and provide version using the static version file
458 global __version__
, version_date
460 if cherrypy
.request
.method
!= "GET":
461 raise NbiException("Only method GET is allowed", HTTPStatus
.METHOD_NOT_ALLOWED
)
463 raise NbiException("Invalid URL or query string for version", HTTPStatus
.METHOD_NOT_ALLOWED
)
464 return __version__
+ " " + version_date
465 except NbiException
as e
:
466 cherrypy
.response
.status
= e
.http_code
.value
468 "code": e
.http_code
.name
,
469 "status": e
.http_code
.value
,
472 return self
._format
_out
(problem_details
, None)
475 def token(self
, method
, token_id
=None, kwargs
=None):
477 # self.engine.load_dbase(cherrypy.request.app.config)
478 indata
= self
._format
_in
(kwargs
)
479 if not isinstance(indata
, dict):
480 raise NbiException("Expected application/yaml or application/json Content-Type", HTTPStatus
.BAD_REQUEST
)
483 session
= self
.authenticator
.authorize()
485 outdata
= self
.authenticator
.get_token(session
, token_id
)
487 outdata
= self
.authenticator
.get_token_list(session
)
488 elif method
== "POST":
490 session
= self
.authenticator
.authorize()
494 indata
.update(kwargs
)
495 outdata
= self
.authenticator
.new_token(session
, indata
, cherrypy
.request
.remote
)
497 cherrypy
.session
['Authorization'] = outdata
["_id"]
498 self
._set
_location
_header
("admin", "v1", "tokens", outdata
["_id"])
499 # cherrypy.response.cookie["Authorization"] = outdata["id"]
500 # cherrypy.response.cookie["Authorization"]['expires'] = 3600
501 elif method
== "DELETE":
502 if not token_id
and "id" in kwargs
:
503 token_id
= kwargs
["id"]
505 session
= self
.authenticator
.authorize()
506 token_id
= session
["_id"]
507 outdata
= self
.authenticator
.del_token(token_id
)
509 cherrypy
.session
['Authorization'] = "logout"
510 # cherrypy.response.cookie["Authorization"] = token_id
511 # cherrypy.response.cookie["Authorization"]['expires'] = 0
513 raise NbiException("Method {} not allowed for token".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
514 return self
._format
_out
(outdata
, session
)
515 except (NbiException
, EngineException
, DbException
, AuthException
) as e
:
516 cherrypy
.log("tokens Exception {}".format(e
))
517 cherrypy
.response
.status
= e
.http_code
.value
519 "code": e
.http_code
.name
,
520 "status": e
.http_code
.value
,
523 return self
._format
_out
(problem_details
, session
)
526 def test(self
, *args
, **kwargs
):
528 if args
and args
[0] == "help":
529 return "<html><pre>\ninit\nfile/<name> download file\ndb-clear/table\nprune\nlogin\nlogin2\n"\
530 "sleep/<time>\nmessage/topic\n</pre></html>"
532 elif args
and args
[0] == "init":
534 # self.engine.load_dbase(cherrypy.request.app.config)
535 self
.engine
.create_admin()
536 return "Done. User 'admin', password 'admin' created"
538 cherrypy
.response
.status
= HTTPStatus
.FORBIDDEN
.value
539 return self
._format
_out
("Database already initialized")
540 elif args
and args
[0] == "file":
541 return cherrypy
.lib
.static
.serve_file(cherrypy
.tree
.apps
['/osm'].config
["storage"]["path"] + "/" + args
[1],
542 "text/plain", "attachment")
543 elif args
and args
[0] == "file2":
544 f_path
= cherrypy
.tree
.apps
['/osm'].config
["storage"]["path"] + "/" + args
[1]
545 f
= open(f_path
, "r")
546 cherrypy
.response
.headers
["Content-type"] = "text/plain"
549 elif len(args
) == 2 and args
[0] == "db-clear":
550 return self
.engine
.db
.del_list(args
[1], kwargs
)
551 elif args
and args
[0] == "prune":
552 return self
.engine
.prune()
553 elif args
and args
[0] == "login":
554 if not cherrypy
.request
.headers
.get("Authorization"):
555 cherrypy
.response
.headers
["WWW-Authenticate"] = 'Basic realm="Access to OSM site", charset="UTF-8"'
556 cherrypy
.response
.status
= HTTPStatus
.UNAUTHORIZED
.value
557 elif args
and args
[0] == "login2":
558 if not cherrypy
.request
.headers
.get("Authorization"):
559 cherrypy
.response
.headers
["WWW-Authenticate"] = 'Bearer realm="Access to OSM site"'
560 cherrypy
.response
.status
= HTTPStatus
.UNAUTHORIZED
.value
561 elif args
and args
[0] == "sleep":
564 sleep_time
= int(args
[1])
566 cherrypy
.response
.status
= HTTPStatus
.FORBIDDEN
.value
567 return self
._format
_out
("Database already initialized")
568 thread_info
= cherrypy
.thread_data
570 time
.sleep(sleep_time
)
572 elif len(args
) >= 2 and args
[0] == "message":
574 return_text
= "<html><pre>{} ->\n".format(main_topic
)
576 if cherrypy
.request
.method
== 'POST':
577 to_send
= yaml
.load(cherrypy
.request
.body
)
578 for k
, v
in to_send
.items():
579 self
.engine
.msg
.write(main_topic
, k
, v
)
580 return_text
+= " {}: {}\n".format(k
, v
)
581 elif cherrypy
.request
.method
== 'GET':
582 for k
, v
in kwargs
.items():
583 self
.engine
.msg
.write(main_topic
, k
, yaml
.load(v
))
584 return_text
+= " {}: {}\n".format(k
, yaml
.load(v
))
585 except Exception as e
:
586 return_text
+= "Error: " + str(e
)
587 return_text
+= "</pre></html>\n"
591 "<html><pre>\nheaders:\n args: {}\n".format(args
) +
592 " kwargs: {}\n".format(kwargs
) +
593 " headers: {}\n".format(cherrypy
.request
.headers
) +
594 " path_info: {}\n".format(cherrypy
.request
.path_info
) +
595 " query_string: {}\n".format(cherrypy
.request
.query_string
) +
596 " session: {}\n".format(cherrypy
.session
) +
597 " cookie: {}\n".format(cherrypy
.request
.cookie
) +
598 " method: {}\n".format(cherrypy
.request
.method
) +
599 " session: {}\n".format(cherrypy
.session
.get('fieldname')) +
601 return_text
+= " length: {}\n".format(cherrypy
.request
.body
.length
)
602 if cherrypy
.request
.body
.length
:
603 return_text
+= " content: {}\n".format(
604 str(cherrypy
.request
.body
.read(int(cherrypy
.request
.headers
.get('Content-Length', 0)))))
606 return_text
+= "thread: {}\n".format(thread_info
)
607 return_text
+= "</pre></html>"
610 def _check_valid_url_method(self
, method
, *args
):
612 raise NbiException("URL must contain at least 'main_topic/version/topic'", HTTPStatus
.METHOD_NOT_ALLOWED
)
614 reference
= self
.valid_methods
618 if not isinstance(reference
, dict):
619 raise NbiException("URL contains unexpected extra items '{}'".format(arg
),
620 HTTPStatus
.METHOD_NOT_ALLOWED
)
623 reference
= reference
[arg
]
624 elif "<ID>" in reference
:
625 reference
= reference
["<ID>"]
626 elif "*" in reference
:
627 reference
= reference
["*"]
630 raise NbiException("Unexpected URL item {}".format(arg
), HTTPStatus
.METHOD_NOT_ALLOWED
)
631 if "TODO" in reference
and method
in reference
["TODO"]:
632 raise NbiException("Method {} not supported yet for this URL".format(method
), HTTPStatus
.NOT_IMPLEMENTED
)
633 elif "METHODS" in reference
and method
not in reference
["METHODS"]:
634 raise NbiException("Method {} not supported for this URL".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
638 def _set_location_header(main_topic
, version
, topic
, id):
640 Insert response header Location with the URL of created item base on URL params
647 # Use cherrypy.request.base for absoluted path and make use of request.header HOST just in case behind aNAT
648 cherrypy
.response
.headers
["Location"] = "/osm/{}/{}/{}/{}".format(main_topic
, version
, topic
, id)
652 def default(self
, main_topic
=None, version
=None, topic
=None, _id
=None, item
=None, *args
, **kwargs
):
661 if not main_topic
or not version
or not topic
:
662 raise NbiException("URL must contain at least 'main_topic/version/topic'",
663 HTTPStatus
.METHOD_NOT_ALLOWED
)
664 if main_topic
not in ("admin", "vnfpkgm", "nsd", "nslcm", "nst", "nsilcm"):
665 raise NbiException("URL main_topic '{}' not supported".format(main_topic
),
666 HTTPStatus
.METHOD_NOT_ALLOWED
)
668 raise NbiException("URL version '{}' not supported".format(version
), HTTPStatus
.METHOD_NOT_ALLOWED
)
670 if kwargs
and "METHOD" in kwargs
and kwargs
["METHOD"] in ("PUT", "POST", "DELETE", "GET", "PATCH"):
671 method
= kwargs
.pop("METHOD")
673 method
= cherrypy
.request
.method
674 if kwargs
and "FORCE" in kwargs
:
675 force
= kwargs
.pop("FORCE")
679 self
._check
_valid
_url
_method
(method
, main_topic
, version
, topic
, _id
, item
, *args
)
681 if main_topic
== "admin" and topic
== "tokens":
682 return self
.token(method
, _id
, kwargs
)
684 # self.engine.load_dbase(cherrypy.request.app.config)
685 session
= self
.authenticator
.authorize()
686 indata
= self
._format
_in
(kwargs
)
688 if topic
== "subscriptions":
689 engine_topic
= main_topic
+ "_" + topic
693 if main_topic
== "nsd":
694 engine_topic
= "nsds"
695 elif main_topic
== "vnfpkgm":
696 engine_topic
= "vnfds"
697 elif main_topic
== "nslcm":
698 engine_topic
= "nsrs"
699 if topic
== "ns_lcm_op_occs":
700 engine_topic
= "nslcmops"
701 if topic
== "vnfrs" or topic
== "vnf_instances":
702 engine_topic
= "vnfrs"
703 elif main_topic
== "nst":
704 engine_topic
= "nsts"
705 elif main_topic
== "nsilcm":
706 engine_topic
= "nsis"
707 if topic
== "nsi_lcm_op_occs":
708 engine_topic
= "nsilcmops"
709 elif main_topic
== "pdu":
710 engine_topic
= "pdus"
711 if engine_topic
== "vims": # TODO this is for backward compatibility, it will remove in the future
712 engine_topic
= "vim_accounts"
715 if item
in ("nsd_content", "package_content", "artifacts", "vnfd", "nsd", "nst", "nst_content",
716 "netslice_instances"):
717 if item
in ("vnfd", "nsd", "nst"):
721 elif item
== "artifacts":
725 file, _format
= self
.engine
.get_file(session
, engine_topic
, _id
, path
,
726 cherrypy
.request
.headers
.get("Accept"))
729 outdata
= self
.engine
.get_item_list(session
, engine_topic
, kwargs
)
731 outdata
= self
.engine
.get_item(session
, engine_topic
, _id
)
732 elif method
== "POST":
733 if topic
in ("ns_descriptors_content", "vnf_packages_content", "netslice_templates_content"):
734 _id
= cherrypy
.request
.headers
.get("Transaction-Id")
736 _id
= self
.engine
.new_item(rollback
, session
, engine_topic
, {}, None, cherrypy
.request
.headers
,
738 completed
= self
.engine
.upload_content(session
, engine_topic
, _id
, indata
, kwargs
,
739 cherrypy
.request
.headers
, force
=force
)
741 self
._set
_location
_header
(main_topic
, version
, topic
, _id
)
743 cherrypy
.response
.headers
["Transaction-Id"] = _id
744 outdata
= {"id": _id
}
745 elif topic
== "ns_instances_content":
747 _id
= self
.engine
.new_item(rollback
, session
, engine_topic
, indata
, kwargs
, force
=force
)
749 indata
["lcmOperationType"] = "instantiate"
750 indata
["nsInstanceId"] = _id
751 self
.engine
.new_item(rollback
, session
, "nslcmops", indata
, None)
752 self
._set
_location
_header
(main_topic
, version
, topic
, _id
)
753 outdata
= {"id": _id
}
754 elif topic
== "ns_instances" and item
:
755 indata
["lcmOperationType"] = item
756 indata
["nsInstanceId"] = _id
757 _id
= self
.engine
.new_item(rollback
, session
, "nslcmops", indata
, kwargs
)
758 self
._set
_location
_header
(main_topic
, version
, "ns_lcm_op_occs", _id
)
759 outdata
= {"id": _id
}
760 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
761 elif topic
== "netslice_instances_content":
763 _id
= self
.engine
.new_item(rollback
, session
, engine_topic
, indata
, kwargs
, force
=force
)
765 indata
["lcmOperationType"] = "instantiate"
766 indata
["nsiInstanceId"] = _id
767 self
.engine
.new_item(rollback
, session
, "nsilcmops", indata
, None)
768 self
._set
_location
_header
(main_topic
, version
, topic
, _id
)
769 outdata
= {"id": _id
}
770 elif topic
== "netslice_instances" and item
:
771 indata
["lcmOperationType"] = item
772 indata
["nsiInstanceId"] = _id
773 _id
= self
.engine
.new_item(rollback
, session
, "nsilcmops", indata
, kwargs
)
774 self
._set
_location
_header
(main_topic
, version
, "nsi_lcm_op_occs", _id
)
775 outdata
= {"id": _id
}
776 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
778 _id
= self
.engine
.new_item(rollback
, session
, engine_topic
, indata
, kwargs
,
779 cherrypy
.request
.headers
, force
=force
)
780 self
._set
_location
_header
(main_topic
, version
, topic
, _id
)
781 outdata
= {"id": _id
}
782 # TODO form NsdInfo when topic in ("ns_descriptors", "vnf_packages")
783 cherrypy
.response
.status
= HTTPStatus
.CREATED
.value
785 elif method
== "DELETE":
787 outdata
= self
.engine
.del_item_list(session
, engine_topic
, kwargs
)
788 cherrypy
.response
.status
= HTTPStatus
.OK
.value
789 else: # len(args) > 1
790 if topic
== "ns_instances_content" and not force
:
792 "lcmOperationType": "terminate",
796 opp_id
= self
.engine
.new_item(rollback
, session
, "nslcmops", nslcmop_desc
, None)
797 outdata
= {"_id": opp_id
}
798 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
799 elif topic
== "netslice_instances_content" and not force
:
801 "lcmOperationType": "terminate",
802 "nsiInstanceId": _id
,
805 opp_id
= self
.engine
.new_item(rollback
, session
, "nsilcmops", nsilcmop_desc
, None)
806 outdata
= {"_id": opp_id
}
807 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
809 self
.engine
.del_item(session
, engine_topic
, _id
, force
)
810 cherrypy
.response
.status
= HTTPStatus
.NO_CONTENT
.value
811 if engine_topic
in ("vim_accounts", "sdns"):
812 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
814 elif method
in ("PUT", "PATCH"):
816 if not indata
and not kwargs
:
817 raise NbiException("Nothing to update. Provide payload and/or query string",
818 HTTPStatus
.BAD_REQUEST
)
819 if item
in ("nsd_content", "package_content", "nst_content") and method
== "PUT":
820 completed
= self
.engine
.upload_content(session
, engine_topic
, _id
, indata
, kwargs
,
821 cherrypy
.request
.headers
, force
=force
)
823 cherrypy
.response
.headers
["Transaction-Id"] = id
825 self
.engine
.edit_item(session
, engine_topic
, _id
, indata
, kwargs
, force
=force
)
826 cherrypy
.response
.status
= HTTPStatus
.NO_CONTENT
.value
828 raise NbiException("Method {} not allowed".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
829 return self
._format
_out
(outdata
, session
, _format
)
830 except Exception as e
:
831 if isinstance(e
, (NbiException
, EngineException
, DbException
, FsException
, MsgException
, AuthException
)):
832 http_code_value
= cherrypy
.response
.status
= e
.http_code
.value
833 http_code_name
= e
.http_code
.name
834 cherrypy
.log("Exception {}".format(e
))
836 http_code_value
= cherrypy
.response
.status
= HTTPStatus
.BAD_REQUEST
.value
# INTERNAL_SERVER_ERROR
837 cherrypy
.log("CRITICAL: Exception {}".format(e
))
838 http_code_name
= HTTPStatus
.BAD_REQUEST
.name
839 if hasattr(outdata
, "close"): # is an open file
843 for rollback_item
in rollback
:
845 if rollback_item
.get("operation") == "set":
846 self
.engine
.db
.set_one(rollback_item
["topic"], {"_id": rollback_item
["_id"]},
847 rollback_item
["content"], fail_on_empty
=False)
849 self
.engine
.del_item(**rollback_item
, session
=session
, force
=True)
850 except Exception as e2
:
851 rollback_error_text
= "Rollback Exception {}: {}".format(rollback_item
, e2
)
852 cherrypy
.log(rollback_error_text
)
853 error_text
+= ". " + rollback_error_text
854 # if isinstance(e, MsgException):
855 # error_text = "{} has been '{}' but other modules cannot be informed because an error on bus".format(
856 # engine_topic[:-1], method, error_text)
858 "code": http_code_name
,
859 "status": http_code_value
,
860 "detail": error_text
,
862 return self
._format
_out
(problem_details
, session
)
863 # raise cherrypy.HTTPError(e.http_code.value, str(e))
866 # def validate_password(realm, username, password):
867 # cherrypy.log("realm "+ str(realm))
868 # if username == "admin" and password == "admin":
873 def _start_service():
875 Callback function called when cherrypy.engine starts
876 Override configuration with env variables
877 Set database, storage, message configuration
878 Init database with admin/admin user password
880 cherrypy
.log
.error("Starting osm_nbi")
881 # update general cherrypy configuration
884 engine_config
= cherrypy
.tree
.apps
['/osm'].config
885 for k
, v
in environ
.items():
886 if not k
.startswith("OSMNBI_"):
888 k1
, _
, k2
= k
[7:].lower().partition("_")
892 # update static configuration
893 if k
== 'OSMNBI_STATIC_DIR':
894 engine_config
["/static"]['tools.staticdir.dir'] = v
895 engine_config
["/static"]['tools.staticdir.on'] = True
896 elif k
== 'OSMNBI_SOCKET_PORT' or k
== 'OSMNBI_SERVER_PORT':
897 update_dict
['server.socket_port'] = int(v
)
898 elif k
== 'OSMNBI_SOCKET_HOST' or k
== 'OSMNBI_SERVER_HOST':
899 update_dict
['server.socket_host'] = v
900 elif k1
in ("server", "test", "auth", "log"):
901 update_dict
[k1
+ '.' + k2
] = v
902 elif k1
in ("message", "database", "storage", "authentication"):
903 # k2 = k2.replace('_', '.')
904 if k2
in ("port", "db_port"):
905 engine_config
[k1
][k2
] = int(v
)
907 engine_config
[k1
][k2
] = v
909 except ValueError as e
:
910 cherrypy
.log
.error("Ignoring environ '{}': " + str(e
))
911 except Exception as e
:
912 cherrypy
.log
.warn("skipping environ '{}' on exception '{}'".format(k
, e
))
915 cherrypy
.config
.update(update_dict
)
916 engine_config
["global"].update(update_dict
)
919 log_format_simple
= "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(message)s"
920 log_formatter_simple
= logging
.Formatter(log_format_simple
, datefmt
='%Y-%m-%dT%H:%M:%S')
921 logger_server
= logging
.getLogger("cherrypy.error")
922 logger_access
= logging
.getLogger("cherrypy.access")
923 logger_cherry
= logging
.getLogger("cherrypy")
924 logger_nbi
= logging
.getLogger("nbi")
926 if "log.file" in engine_config
["global"]:
927 file_handler
= logging
.handlers
.RotatingFileHandler(engine_config
["global"]["log.file"],
928 maxBytes
=100e6
, backupCount
=9, delay
=0)
929 file_handler
.setFormatter(log_formatter_simple
)
930 logger_cherry
.addHandler(file_handler
)
931 logger_nbi
.addHandler(file_handler
)
932 # log always to standard output
933 for format_
, logger
in {"nbi.server %(filename)s:%(lineno)s": logger_server
,
934 "nbi.access %(filename)s:%(lineno)s": logger_access
,
935 "%(name)s %(filename)s:%(lineno)s": logger_nbi
937 log_format_cherry
= "%(asctime)s %(levelname)s {} %(message)s".format(format_
)
938 log_formatter_cherry
= logging
.Formatter(log_format_cherry
, datefmt
='%Y-%m-%dT%H:%M:%S')
939 str_handler
= logging
.StreamHandler()
940 str_handler
.setFormatter(log_formatter_cherry
)
941 logger
.addHandler(str_handler
)
943 if engine_config
["global"].get("log.level"):
944 logger_cherry
.setLevel(engine_config
["global"]["log.level"])
945 logger_nbi
.setLevel(engine_config
["global"]["log.level"])
947 # logging other modules
948 for k1
, logname
in {"message": "nbi.msg", "database": "nbi.db", "storage": "nbi.fs"}.items():
949 engine_config
[k1
]["logger_name"] = logname
950 logger_module
= logging
.getLogger(logname
)
951 if "logfile" in engine_config
[k1
]:
952 file_handler
= logging
.handlers
.RotatingFileHandler(engine_config
[k1
]["logfile"],
953 maxBytes
=100e6
, backupCount
=9, delay
=0)
954 file_handler
.setFormatter(log_formatter_simple
)
955 logger_module
.addHandler(file_handler
)
956 if "loglevel" in engine_config
[k1
]:
957 logger_module
.setLevel(engine_config
[k1
]["loglevel"])
958 # TODO add more entries, e.g.: storage
959 cherrypy
.tree
.apps
['/osm'].root
.engine
.start(engine_config
)
960 cherrypy
.tree
.apps
['/osm'].root
.authenticator
.start(engine_config
)
961 cherrypy
.tree
.apps
['/osm'].root
.engine
.init_db(target_version
=database_version
)
962 cherrypy
.tree
.apps
['/osm'].root
.authenticator
.init_db(target_version
=auth_database_version
)
963 # getenv('OSMOPENMANO_TENANT', None)
968 Callback function called when cherrypy.engine stops
969 TODO: Ending database connections.
971 cherrypy
.tree
.apps
['/osm'].root
.engine
.stop()
972 cherrypy
.log
.error("Stopping osm_nbi")
975 def nbi(config_file
):
978 # #'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
979 # 'tools.sessions.on': True,
980 # 'tools.response_headers.on': True,
981 # # 'tools.response_headers.headers': [('Content-Type', 'text/plain')],
984 # cherrypy.Server.ssl_module = 'builtin'
985 # cherrypy.Server.ssl_certificate = "http/cert.pem"
986 # cherrypy.Server.ssl_private_key = "http/privkey.pem"
987 # cherrypy.Server.thread_pool = 10
988 # cherrypy.config.update({'Server.socket_port': config["port"], 'Server.socket_host': config["host"]})
990 # cherrypy.config.update({'tools.auth_basic.on': True,
991 # 'tools.auth_basic.realm': 'localhost',
992 # 'tools.auth_basic.checkpassword': validate_password})
993 cherrypy
.engine
.subscribe('start', _start_service
)
994 cherrypy
.engine
.subscribe('stop', _stop_service
)
995 cherrypy
.quickstart(Server(), '/osm', config_file
)
999 print("""Usage: {} [options]
1000 -c|--config [configuration_file]: loads the configuration file (default: ./nbi.cfg)
1001 -h|--help: shows this help
1002 """.format(sys
.argv
[0]))
1003 # --log-socket-host HOST: send logs to this host")
1004 # --log-socket-port PORT: send logs using this port (default: 9022)")
1007 if __name__
== '__main__':
1009 # load parameters and configuration
1010 opts
, args
= getopt
.getopt(sys
.argv
[1:], "hvc:", ["config=", "help"])
1011 # TODO add "log-socket-host=", "log-socket-port=", "log-file="
1014 if o
in ("-h", "--help"):
1017 elif o
in ("-c", "--config"):
1019 # elif o == "--log-socket-port":
1020 # log_socket_port = a
1021 # elif o == "--log-socket-host":
1022 # log_socket_host = a
1023 # elif o == "--log-file":
1026 assert False, "Unhandled option"
1028 if not path
.isfile(config_file
):
1029 print("configuration file '{}' that not exist".format(config_file
), file=sys
.stderr
)
1032 for config_file
in (__file__
[:__file__
.rfind(".")] + ".cfg", "./nbi.cfg", "/etc/osm/nbi.cfg"):
1033 if path
.isfile(config_file
):
1036 print("No configuration file 'nbi.cfg' found neither at local folder nor at /etc/osm/", file=sys
.stderr
)
1039 except getopt
.GetoptError
as e
:
1040 print(str(e
), file=sys
.stderr
)