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 elif "application/yaml" in cherrypy
.request
.headers
["Content-Type"]:
323 error_text
= "Invalid yaml format "
324 indata
= yaml
.load(cherrypy
.request
.body
)
325 elif "application/binary" in cherrypy
.request
.headers
["Content-Type"] or \
326 "application/gzip" in cherrypy
.request
.headers
["Content-Type"] or \
327 "application/zip" in cherrypy
.request
.headers
["Content-Type"] or \
328 "text/plain" in cherrypy
.request
.headers
["Content-Type"]:
329 indata
= cherrypy
.request
.body
# .read()
330 elif "multipart/form-data" in cherrypy
.request
.headers
["Content-Type"]:
331 if "descriptor_file" in kwargs
:
332 filecontent
= kwargs
.pop("descriptor_file")
333 if not filecontent
.file:
334 raise NbiException("empty file or content", HTTPStatus
.BAD_REQUEST
)
335 indata
= filecontent
.file # .read()
336 if filecontent
.content_type
.value
:
337 cherrypy
.request
.headers
["Content-Type"] = filecontent
.content_type
.value
339 # raise cherrypy.HTTPError(HTTPStatus.Not_Acceptable,
340 # "Only 'Content-Type' of type 'application/json' or
341 # 'application/yaml' for input format are available")
342 error_text
= "Invalid yaml format "
343 indata
= yaml
.load(cherrypy
.request
.body
)
345 error_text
= "Invalid yaml format "
346 indata
= yaml
.load(cherrypy
.request
.body
)
351 if cherrypy
.request
.headers
.get("Query-String-Format") == "yaml":
354 for k
, v
in kwargs
.items():
355 if isinstance(v
, str):
360 kwargs
[k
] = yaml
.load(v
)
363 elif k
.endswith(".gt") or k
.endswith(".lt") or k
.endswith(".gte") or k
.endswith(".lte"):
371 elif v
.find(",") > 0:
372 kwargs
[k
] = v
.split(",")
373 elif isinstance(v
, (list, tuple)):
374 for index
in range(0, len(v
)):
379 v
[index
] = yaml
.load(v
[index
])
384 except (ValueError, yaml
.YAMLError
) as exc
:
385 raise NbiException(error_text
+ str(exc
), HTTPStatus
.BAD_REQUEST
)
386 except KeyError as exc
:
387 raise NbiException("Query string error: " + str(exc
), HTTPStatus
.BAD_REQUEST
)
388 except Exception as exc
:
389 raise NbiException(error_text
+ str(exc
), HTTPStatus
.BAD_REQUEST
)
392 def _format_out(data
, session
=None, _format
=None):
394 return string of dictionary data according to requested json, yaml, xml. By default json
395 :param data: response to be sent. Can be a dict, text or file
397 :param _format: The format to be set as Content-Type ir data is a file
400 accept
= cherrypy
.request
.headers
.get("Accept")
402 if accept
and "text/html" in accept
:
403 return html
.format(data
, cherrypy
.request
, cherrypy
.response
, session
)
404 # cherrypy.response.status = HTTPStatus.NO_CONTENT.value
406 elif hasattr(data
, "read"): # file object
408 cherrypy
.response
.headers
["Content-Type"] = _format
409 elif "b" in data
.mode
: # binariy asssumig zip
410 cherrypy
.response
.headers
["Content-Type"] = 'application/zip'
412 cherrypy
.response
.headers
["Content-Type"] = 'text/plain'
413 # TODO check that cherrypy close file. If not implement pending things to close per thread next
416 if "application/json" in accept
:
417 cherrypy
.response
.headers
["Content-Type"] = 'application/json; charset=utf-8'
418 a
= json
.dumps(data
, indent
=4) + "\n"
419 return a
.encode("utf8")
420 elif "text/html" in accept
:
421 return html
.format(data
, cherrypy
.request
, cherrypy
.response
, session
)
423 elif "application/yaml" in accept
or "*/*" in accept
or "text/plain" in accept
:
426 raise cherrypy
.HTTPError(HTTPStatus
.NOT_ACCEPTABLE
.value
,
427 "Only 'Accept' of type 'application/json' or 'application/yaml' "
428 "for output format are available")
429 cherrypy
.response
.headers
["Content-Type"] = 'application/yaml'
430 return yaml
.safe_dump(data
, explicit_start
=True, indent
=4, default_flow_style
=False, tags
=False,
431 encoding
='utf-8', allow_unicode
=True) # , canonical=True, default_style='"'
434 def index(self
, *args
, **kwargs
):
437 if cherrypy
.request
.method
== "GET":
438 session
= self
.authenticator
.authorize()
439 outdata
= "Index page"
441 raise cherrypy
.HTTPError(HTTPStatus
.METHOD_NOT_ALLOWED
.value
,
442 "Method {} not allowed for tokens".format(cherrypy
.request
.method
))
444 return self
._format
_out
(outdata
, session
)
446 except (EngineException
, AuthException
) as e
:
447 cherrypy
.log("index Exception {}".format(e
))
448 cherrypy
.response
.status
= e
.http_code
.value
449 return self
._format
_out
("Welcome to OSM!", session
)
452 def version(self
, *args
, **kwargs
):
453 # TODO consider to remove and provide version using the static version file
454 global __version__
, version_date
456 if cherrypy
.request
.method
!= "GET":
457 raise NbiException("Only method GET is allowed", HTTPStatus
.METHOD_NOT_ALLOWED
)
459 raise NbiException("Invalid URL or query string for version", HTTPStatus
.METHOD_NOT_ALLOWED
)
460 return __version__
+ " " + version_date
461 except NbiException
as e
:
462 cherrypy
.response
.status
= e
.http_code
.value
464 "code": e
.http_code
.name
,
465 "status": e
.http_code
.value
,
468 return self
._format
_out
(problem_details
, None)
471 def token(self
, method
, token_id
=None, kwargs
=None):
473 # self.engine.load_dbase(cherrypy.request.app.config)
474 indata
= self
._format
_in
(kwargs
)
475 if not isinstance(indata
, dict):
476 raise NbiException("Expected application/yaml or application/json Content-Type", HTTPStatus
.BAD_REQUEST
)
479 session
= self
.authenticator
.authorize()
481 outdata
= self
.authenticator
.get_token(session
, token_id
)
483 outdata
= self
.authenticator
.get_token_list(session
)
484 elif method
== "POST":
486 session
= self
.authenticator
.authorize()
490 indata
.update(kwargs
)
491 outdata
= self
.authenticator
.new_token(session
, indata
, cherrypy
.request
.remote
)
493 cherrypy
.session
['Authorization'] = outdata
["_id"]
494 self
._set
_location
_header
("admin", "v1", "tokens", outdata
["_id"])
495 # cherrypy.response.cookie["Authorization"] = outdata["id"]
496 # cherrypy.response.cookie["Authorization"]['expires'] = 3600
497 elif method
== "DELETE":
498 if not token_id
and "id" in kwargs
:
499 token_id
= kwargs
["id"]
501 session
= self
.authenticator
.authorize()
502 token_id
= session
["_id"]
503 outdata
= self
.authenticator
.del_token(token_id
)
505 cherrypy
.session
['Authorization'] = "logout"
506 # cherrypy.response.cookie["Authorization"] = token_id
507 # cherrypy.response.cookie["Authorization"]['expires'] = 0
509 raise NbiException("Method {} not allowed for token".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
510 return self
._format
_out
(outdata
, session
)
511 except (NbiException
, EngineException
, DbException
, AuthException
) as e
:
512 cherrypy
.log("tokens Exception {}".format(e
))
513 cherrypy
.response
.status
= e
.http_code
.value
515 "code": e
.http_code
.name
,
516 "status": e
.http_code
.value
,
519 return self
._format
_out
(problem_details
, session
)
522 def test(self
, *args
, **kwargs
):
524 if args
and args
[0] == "help":
525 return "<html><pre>\ninit\nfile/<name> download file\ndb-clear/table\nprune\nlogin\nlogin2\n"\
526 "sleep/<time>\nmessage/topic\n</pre></html>"
528 elif args
and args
[0] == "init":
530 # self.engine.load_dbase(cherrypy.request.app.config)
531 self
.engine
.create_admin()
532 return "Done. User 'admin', password 'admin' created"
534 cherrypy
.response
.status
= HTTPStatus
.FORBIDDEN
.value
535 return self
._format
_out
("Database already initialized")
536 elif args
and args
[0] == "file":
537 return cherrypy
.lib
.static
.serve_file(cherrypy
.tree
.apps
['/osm'].config
["storage"]["path"] + "/" + args
[1],
538 "text/plain", "attachment")
539 elif args
and args
[0] == "file2":
540 f_path
= cherrypy
.tree
.apps
['/osm'].config
["storage"]["path"] + "/" + args
[1]
541 f
= open(f_path
, "r")
542 cherrypy
.response
.headers
["Content-type"] = "text/plain"
545 elif len(args
) == 2 and args
[0] == "db-clear":
546 return self
.engine
.db
.del_list(args
[1], kwargs
)
547 elif args
and args
[0] == "prune":
548 return self
.engine
.prune()
549 elif args
and args
[0] == "login":
550 if not cherrypy
.request
.headers
.get("Authorization"):
551 cherrypy
.response
.headers
["WWW-Authenticate"] = 'Basic realm="Access to OSM site", charset="UTF-8"'
552 cherrypy
.response
.status
= HTTPStatus
.UNAUTHORIZED
.value
553 elif args
and args
[0] == "login2":
554 if not cherrypy
.request
.headers
.get("Authorization"):
555 cherrypy
.response
.headers
["WWW-Authenticate"] = 'Bearer realm="Access to OSM site"'
556 cherrypy
.response
.status
= HTTPStatus
.UNAUTHORIZED
.value
557 elif args
and args
[0] == "sleep":
560 sleep_time
= int(args
[1])
562 cherrypy
.response
.status
= HTTPStatus
.FORBIDDEN
.value
563 return self
._format
_out
("Database already initialized")
564 thread_info
= cherrypy
.thread_data
566 time
.sleep(sleep_time
)
568 elif len(args
) >= 2 and args
[0] == "message":
570 return_text
= "<html><pre>{} ->\n".format(main_topic
)
572 if cherrypy
.request
.method
== 'POST':
573 to_send
= yaml
.load(cherrypy
.request
.body
)
574 for k
, v
in to_send
.items():
575 self
.engine
.msg
.write(main_topic
, k
, v
)
576 return_text
+= " {}: {}\n".format(k
, v
)
577 elif cherrypy
.request
.method
== 'GET':
578 for k
, v
in kwargs
.items():
579 self
.engine
.msg
.write(main_topic
, k
, yaml
.load(v
))
580 return_text
+= " {}: {}\n".format(k
, yaml
.load(v
))
581 except Exception as e
:
582 return_text
+= "Error: " + str(e
)
583 return_text
+= "</pre></html>\n"
587 "<html><pre>\nheaders:\n args: {}\n".format(args
) +
588 " kwargs: {}\n".format(kwargs
) +
589 " headers: {}\n".format(cherrypy
.request
.headers
) +
590 " path_info: {}\n".format(cherrypy
.request
.path_info
) +
591 " query_string: {}\n".format(cherrypy
.request
.query_string
) +
592 " session: {}\n".format(cherrypy
.session
) +
593 " cookie: {}\n".format(cherrypy
.request
.cookie
) +
594 " method: {}\n".format(cherrypy
.request
.method
) +
595 " session: {}\n".format(cherrypy
.session
.get('fieldname')) +
597 return_text
+= " length: {}\n".format(cherrypy
.request
.body
.length
)
598 if cherrypy
.request
.body
.length
:
599 return_text
+= " content: {}\n".format(
600 str(cherrypy
.request
.body
.read(int(cherrypy
.request
.headers
.get('Content-Length', 0)))))
602 return_text
+= "thread: {}\n".format(thread_info
)
603 return_text
+= "</pre></html>"
606 def _check_valid_url_method(self
, method
, *args
):
608 raise NbiException("URL must contain at least 'main_topic/version/topic'", HTTPStatus
.METHOD_NOT_ALLOWED
)
610 reference
= self
.valid_methods
614 if not isinstance(reference
, dict):
615 raise NbiException("URL contains unexpected extra items '{}'".format(arg
),
616 HTTPStatus
.METHOD_NOT_ALLOWED
)
619 reference
= reference
[arg
]
620 elif "<ID>" in reference
:
621 reference
= reference
["<ID>"]
622 elif "*" in reference
:
623 reference
= reference
["*"]
626 raise NbiException("Unexpected URL item {}".format(arg
), HTTPStatus
.METHOD_NOT_ALLOWED
)
627 if "TODO" in reference
and method
in reference
["TODO"]:
628 raise NbiException("Method {} not supported yet for this URL".format(method
), HTTPStatus
.NOT_IMPLEMENTED
)
629 elif "METHODS" in reference
and method
not in reference
["METHODS"]:
630 raise NbiException("Method {} not supported for this URL".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
634 def _set_location_header(main_topic
, version
, topic
, id):
636 Insert response header Location with the URL of created item base on URL params
643 # Use cherrypy.request.base for absoluted path and make use of request.header HOST just in case behind aNAT
644 cherrypy
.response
.headers
["Location"] = "/osm/{}/{}/{}/{}".format(main_topic
, version
, topic
, id)
648 def default(self
, main_topic
=None, version
=None, topic
=None, _id
=None, item
=None, *args
, **kwargs
):
657 if not main_topic
or not version
or not topic
:
658 raise NbiException("URL must contain at least 'main_topic/version/topic'",
659 HTTPStatus
.METHOD_NOT_ALLOWED
)
660 if main_topic
not in ("admin", "vnfpkgm", "nsd", "nslcm"):
661 raise NbiException("URL main_topic '{}' not supported".format(main_topic
),
662 HTTPStatus
.METHOD_NOT_ALLOWED
)
664 raise NbiException("URL version '{}' not supported".format(version
), HTTPStatus
.METHOD_NOT_ALLOWED
)
666 if kwargs
and "METHOD" in kwargs
and kwargs
["METHOD"] in ("PUT", "POST", "DELETE", "GET", "PATCH"):
667 method
= kwargs
.pop("METHOD")
669 method
= cherrypy
.request
.method
670 if kwargs
and "FORCE" in kwargs
:
671 force
= kwargs
.pop("FORCE")
675 self
._check
_valid
_url
_method
(method
, main_topic
, version
, topic
, _id
, item
, *args
)
677 if main_topic
== "admin" and topic
== "tokens":
678 return self
.token(method
, _id
, kwargs
)
680 # self.engine.load_dbase(cherrypy.request.app.config)
681 session
= self
.authenticator
.authorize()
682 indata
= self
._format
_in
(kwargs
)
684 if topic
== "subscriptions":
685 engine_topic
= main_topic
+ "_" + topic
689 if main_topic
== "nsd":
690 engine_topic
= "nsds"
691 elif main_topic
== "vnfpkgm":
692 engine_topic
= "vnfds"
693 elif main_topic
== "nslcm":
694 engine_topic
= "nsrs"
695 if topic
== "ns_lcm_op_occs":
696 engine_topic
= "nslcmops"
697 if topic
== "vnfrs" or topic
== "vnf_instances":
698 engine_topic
= "vnfrs"
699 elif main_topic
== "nst":
700 engine_topic
= "nsts"
701 elif main_topic
== "nsilcm":
702 engine_topic
= "nsis"
703 if topic
== "nsi_lcm_op_occs":
704 engine_topic
= "nsilcmops"
705 elif main_topic
== "pdu":
706 engine_topic
= "pdus"
707 if engine_topic
== "vims": # TODO this is for backward compatibility, it will remove in the future
708 engine_topic
= "vim_accounts"
711 if item
in ("nsd_content", "package_content", "artifacts", "vnfd", "nsd", "nst", "nst_content"):
712 if item
in ("vnfd", "nsd", "nst"):
716 elif item
== "artifacts":
720 file, _format
= self
.engine
.get_file(session
, engine_topic
, _id
, path
,
721 cherrypy
.request
.headers
.get("Accept"))
724 outdata
= self
.engine
.get_item_list(session
, engine_topic
, kwargs
)
726 outdata
= self
.engine
.get_item(session
, engine_topic
, _id
)
727 elif method
== "POST":
728 if topic
in ("ns_descriptors_content", "vnf_packages_content", "netslice_templates_content"):
729 _id
= cherrypy
.request
.headers
.get("Transaction-Id")
731 _id
= self
.engine
.new_item(rollback
, session
, engine_topic
, {}, None, cherrypy
.request
.headers
,
733 completed
= self
.engine
.upload_content(session
, engine_topic
, _id
, indata
, kwargs
,
734 cherrypy
.request
.headers
, force
=force
)
736 self
._set
_location
_header
(main_topic
, version
, topic
, _id
)
738 cherrypy
.response
.headers
["Transaction-Id"] = _id
739 outdata
= {"id": _id
}
740 elif topic
== "ns_instances_content":
742 _id
= self
.engine
.new_item(rollback
, session
, engine_topic
, indata
, kwargs
, force
=force
)
744 indata
["lcmOperationType"] = "instantiate"
745 indata
["nsInstanceId"] = _id
746 self
.engine
.new_item(rollback
, session
, "nslcmops", indata
, None)
747 self
._set
_location
_header
(main_topic
, version
, topic
, _id
)
748 outdata
= {"id": _id
}
749 elif topic
== "ns_instances" and item
:
750 indata
["lcmOperationType"] = item
751 indata
["nsInstanceId"] = _id
752 _id
= self
.engine
.new_item(rollback
, session
, "nslcmops", indata
, kwargs
)
753 self
._set
_location
_header
(main_topic
, version
, "ns_lcm_op_occs", _id
)
754 outdata
= {"id": _id
}
755 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
756 elif topic
== "netslice_instances_content":
758 _id
= self
.engine
.new_item(rollback
, session
, engine_topic
, indata
, kwargs
, force
=force
)
760 indata
["lcmOperationType"] = "instantiate"
761 indata
["nsiInstanceId"] = _id
762 self
.engine
.new_item(rollback
, session
, "nsilcmops", indata
, None)
763 self
._set
_location
_header
(main_topic
, version
, topic
, _id
)
764 outdata
= {"id": _id
}
765 elif topic
== "netslice_instances" and item
:
766 indata
["lcmOperationType"] = item
767 indata
["nsiInstanceId"] = _id
768 _id
= self
.engine
.new_item(rollback
, session
, "nsilcmops", indata
, kwargs
)
769 self
._set
_location
_header
(main_topic
, version
, "nsi_lcm_op_occs", _id
)
770 outdata
= {"id": _id
}
771 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
773 _id
= self
.engine
.new_item(rollback
, session
, engine_topic
, indata
, kwargs
,
774 cherrypy
.request
.headers
, force
=force
)
775 self
._set
_location
_header
(main_topic
, version
, topic
, _id
)
776 outdata
= {"id": _id
}
777 # TODO form NsdInfo when topic in ("ns_descriptors", "vnf_packages")
778 cherrypy
.response
.status
= HTTPStatus
.CREATED
.value
780 elif method
== "DELETE":
782 outdata
= self
.engine
.del_item_list(session
, engine_topic
, kwargs
)
783 cherrypy
.response
.status
= HTTPStatus
.OK
.value
784 else: # len(args) > 1
785 if topic
== "ns_instances_content" and not force
:
787 "lcmOperationType": "terminate",
791 opp_id
= self
.engine
.new_item(rollback
, session
, "nslcmops", nslcmop_desc
, None)
792 outdata
= {"_id": opp_id
}
793 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
794 elif topic
== "netslice_instances_content" and not force
:
796 "lcmOperationType": "terminate",
797 "nsiInstanceId": _id
,
800 opp_id
= self
.engine
.new_item(rollback
, session
, "nsilcmops", nsilcmop_desc
, None)
801 outdata
= {"_id": opp_id
}
802 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
804 self
.engine
.del_item(session
, engine_topic
, _id
, force
)
805 cherrypy
.response
.status
= HTTPStatus
.NO_CONTENT
.value
806 if engine_topic
in ("vim_accounts", "sdns"):
807 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
809 elif method
in ("PUT", "PATCH"):
811 if not indata
and not kwargs
:
812 raise NbiException("Nothing to update. Provide payload and/or query string",
813 HTTPStatus
.BAD_REQUEST
)
814 if item
in ("nsd_content", "package_content", "nst_content") and method
== "PUT":
815 completed
= self
.engine
.upload_content(session
, engine_topic
, _id
, indata
, kwargs
,
816 cherrypy
.request
.headers
, force
=force
)
818 cherrypy
.response
.headers
["Transaction-Id"] = id
820 self
.engine
.edit_item(session
, engine_topic
, _id
, indata
, kwargs
, force
=force
)
821 cherrypy
.response
.status
= HTTPStatus
.NO_CONTENT
.value
823 raise NbiException("Method {} not allowed".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
824 return self
._format
_out
(outdata
, session
, _format
)
825 except Exception as e
:
826 if isinstance(e
, (NbiException
, EngineException
, DbException
, FsException
, MsgException
, AuthException
)):
827 http_code_value
= cherrypy
.response
.status
= e
.http_code
.value
828 http_code_name
= e
.http_code
.name
829 cherrypy
.log("Exception {}".format(e
))
831 http_code_value
= cherrypy
.response
.status
= HTTPStatus
.BAD_REQUEST
.value
# INTERNAL_SERVER_ERROR
832 cherrypy
.log("CRITICAL: Exception {}".format(e
))
833 http_code_name
= HTTPStatus
.BAD_REQUEST
.name
834 if hasattr(outdata
, "close"): # is an open file
838 for rollback_item
in rollback
:
840 self
.engine
.del_item(**rollback_item
, session
=session
, force
=True)
841 except Exception as e2
:
842 rollback_error_text
= "Rollback Exception {}: {}".format(rollback_item
, e2
)
843 cherrypy
.log(rollback_error_text
)
844 error_text
+= ". " + rollback_error_text
845 # if isinstance(e, MsgException):
846 # error_text = "{} has been '{}' but other modules cannot be informed because an error on bus".format(
847 # engine_topic[:-1], method, error_text)
849 "code": http_code_name
,
850 "status": http_code_value
,
851 "detail": error_text
,
853 return self
._format
_out
(problem_details
, session
)
854 # raise cherrypy.HTTPError(e.http_code.value, str(e))
857 # def validate_password(realm, username, password):
858 # cherrypy.log("realm "+ str(realm))
859 # if username == "admin" and password == "admin":
864 def _start_service():
866 Callback function called when cherrypy.engine starts
867 Override configuration with env variables
868 Set database, storage, message configuration
869 Init database with admin/admin user password
871 cherrypy
.log
.error("Starting osm_nbi")
872 # update general cherrypy configuration
875 engine_config
= cherrypy
.tree
.apps
['/osm'].config
876 for k
, v
in environ
.items():
877 if not k
.startswith("OSMNBI_"):
879 k1
, _
, k2
= k
[7:].lower().partition("_")
883 # update static configuration
884 if k
== 'OSMNBI_STATIC_DIR':
885 engine_config
["/static"]['tools.staticdir.dir'] = v
886 engine_config
["/static"]['tools.staticdir.on'] = True
887 elif k
== 'OSMNBI_SOCKET_PORT' or k
== 'OSMNBI_SERVER_PORT':
888 update_dict
['server.socket_port'] = int(v
)
889 elif k
== 'OSMNBI_SOCKET_HOST' or k
== 'OSMNBI_SERVER_HOST':
890 update_dict
['server.socket_host'] = v
891 elif k1
in ("server", "test", "auth", "log"):
892 update_dict
[k1
+ '.' + k2
] = v
893 elif k1
in ("message", "database", "storage", "authentication"):
894 # k2 = k2.replace('_', '.')
895 if k2
in ("port", "db_port"):
896 engine_config
[k1
][k2
] = int(v
)
898 engine_config
[k1
][k2
] = v
900 except ValueError as e
:
901 cherrypy
.log
.error("Ignoring environ '{}': " + str(e
))
902 except Exception as e
:
903 cherrypy
.log
.warn("skipping environ '{}' on exception '{}'".format(k
, e
))
906 cherrypy
.config
.update(update_dict
)
907 engine_config
["global"].update(update_dict
)
910 log_format_simple
= "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(message)s"
911 log_formatter_simple
= logging
.Formatter(log_format_simple
, datefmt
='%Y-%m-%dT%H:%M:%S')
912 logger_server
= logging
.getLogger("cherrypy.error")
913 logger_access
= logging
.getLogger("cherrypy.access")
914 logger_cherry
= logging
.getLogger("cherrypy")
915 logger_nbi
= logging
.getLogger("nbi")
917 if "log.file" in engine_config
["global"]:
918 file_handler
= logging
.handlers
.RotatingFileHandler(engine_config
["global"]["log.file"],
919 maxBytes
=100e6
, backupCount
=9, delay
=0)
920 file_handler
.setFormatter(log_formatter_simple
)
921 logger_cherry
.addHandler(file_handler
)
922 logger_nbi
.addHandler(file_handler
)
923 # log always to standard output
924 for format_
, logger
in {"nbi.server %(filename)s:%(lineno)s": logger_server
,
925 "nbi.access %(filename)s:%(lineno)s": logger_access
,
926 "%(name)s %(filename)s:%(lineno)s": logger_nbi
928 log_format_cherry
= "%(asctime)s %(levelname)s {} %(message)s".format(format_
)
929 log_formatter_cherry
= logging
.Formatter(log_format_cherry
, datefmt
='%Y-%m-%dT%H:%M:%S')
930 str_handler
= logging
.StreamHandler()
931 str_handler
.setFormatter(log_formatter_cherry
)
932 logger
.addHandler(str_handler
)
934 if engine_config
["global"].get("log.level"):
935 logger_cherry
.setLevel(engine_config
["global"]["log.level"])
936 logger_nbi
.setLevel(engine_config
["global"]["log.level"])
938 # logging other modules
939 for k1
, logname
in {"message": "nbi.msg", "database": "nbi.db", "storage": "nbi.fs"}.items():
940 engine_config
[k1
]["logger_name"] = logname
941 logger_module
= logging
.getLogger(logname
)
942 if "logfile" in engine_config
[k1
]:
943 file_handler
= logging
.handlers
.RotatingFileHandler(engine_config
[k1
]["logfile"],
944 maxBytes
=100e6
, backupCount
=9, delay
=0)
945 file_handler
.setFormatter(log_formatter_simple
)
946 logger_module
.addHandler(file_handler
)
947 if "loglevel" in engine_config
[k1
]:
948 logger_module
.setLevel(engine_config
[k1
]["loglevel"])
949 # TODO add more entries, e.g.: storage
950 cherrypy
.tree
.apps
['/osm'].root
.engine
.start(engine_config
)
951 cherrypy
.tree
.apps
['/osm'].root
.authenticator
.start(engine_config
)
953 cherrypy
.tree
.apps
['/osm'].root
.engine
.init_db(target_version
=database_version
)
954 cherrypy
.tree
.apps
['/osm'].root
.authenticator
.init_db(target_version
=auth_database_version
)
955 except (EngineException
, AuthException
):
957 # getenv('OSMOPENMANO_TENANT', None)
962 Callback function called when cherrypy.engine stops
963 TODO: Ending database connections.
965 cherrypy
.tree
.apps
['/osm'].root
.engine
.stop()
966 cherrypy
.log
.error("Stopping osm_nbi")
969 def nbi(config_file
):
972 # #'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
973 # 'tools.sessions.on': True,
974 # 'tools.response_headers.on': True,
975 # # 'tools.response_headers.headers': [('Content-Type', 'text/plain')],
978 # cherrypy.Server.ssl_module = 'builtin'
979 # cherrypy.Server.ssl_certificate = "http/cert.pem"
980 # cherrypy.Server.ssl_private_key = "http/privkey.pem"
981 # cherrypy.Server.thread_pool = 10
982 # cherrypy.config.update({'Server.socket_port': config["port"], 'Server.socket_host': config["host"]})
984 # cherrypy.config.update({'tools.auth_basic.on': True,
985 # 'tools.auth_basic.realm': 'localhost',
986 # 'tools.auth_basic.checkpassword': validate_password})
987 cherrypy
.engine
.subscribe('start', _start_service
)
988 cherrypy
.engine
.subscribe('stop', _stop_service
)
989 cherrypy
.quickstart(Server(), '/osm', config_file
)
993 print("""Usage: {} [options]
994 -c|--config [configuration_file]: loads the configuration file (default: ./nbi.cfg)
995 -h|--help: shows this help
996 """.format(sys
.argv
[0]))
997 # --log-socket-host HOST: send logs to this host")
998 # --log-socket-port PORT: send logs using this port (default: 9022)")
1001 if __name__
== '__main__':
1003 # load parameters and configuration
1004 opts
, args
= getopt
.getopt(sys
.argv
[1:], "hvc:", ["config=", "help"])
1005 # TODO add "log-socket-host=", "log-socket-port=", "log-file="
1008 if o
in ("-h", "--help"):
1011 elif o
in ("-c", "--config"):
1013 # elif o == "--log-socket-port":
1014 # log_socket_port = a
1015 # elif o == "--log-socket-host":
1016 # log_socket_host = a
1017 # elif o == "--log-file":
1020 assert False, "Unhandled option"
1022 if not path
.isfile(config_file
):
1023 print("configuration file '{}' that not exist".format(config_file
), file=sys
.stderr
)
1026 for config_file
in (__file__
[:__file__
.rfind(".")] + ".cfg", "./nbi.cfg", "/etc/osm/nbi.cfg"):
1027 if path
.isfile(config_file
):
1030 print("No configuration file 'nbi.cfg' found neither at local folder nor at /etc/osm/", file=sys
.stderr
)
1033 except getopt
.GetoptError
as e
:
1034 print(str(e
), file=sys
.stderr
)