b7a6990bf9f4a919bdd2c7af128156deba415d71
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 validation
import ValidationError
18 from osm_common
.dbbase
import DbException
19 from osm_common
.fsbase
import FsException
20 from osm_common
.msgbase
import MsgException
21 from http
import HTTPStatus
22 from codecs
import getreader
23 from os
import environ
, path
25 __author__
= "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
27 # TODO consider to remove and provide version using the static version file
29 version_date
= "Apr 2018"
30 database_version
= '1.0'
31 auth_database_version
= '1.0'
34 North Bound Interface (O: OSM specific; 5,X: SOL005 not implemented yet; O5: SOL005 implemented)
35 URL: /osm GET POST PUT DELETE PATCH
37 /ns_descriptors_content O O
43 /artifacts[/<artifactPath>] O
51 /vnf_packages_content O O
55 /package_content O5 O5
58 /artifacts[/<artifactPath>] O5
63 /ns_instances_content O O
75 /vnf_instances (also vnfrs for compatibility) O
91 /vims_accounts (also vims for compatibility) O O
97 /netslice_templates_content O O
99 /netslice_templates O O
103 /artifacts[/<artifactPath>] O
105 /<subscriptionId> X X
108 /netslice_instances_content O O
109 /<SliceInstanceId> O O
110 /netslice_instances O O
111 /<SliceInstanceId> O O
116 /<nsiLcmOpOccId> O O O
118 /<subscriptionId> X X
121 Follows SOL005 section 4.3.2 It contains extra METHOD to override http method, FORCE to force.
122 simpleFilterExpr := <attrName>["."<attrName>]*["."<op>]"="<value>[","<value>]*
123 filterExpr := <simpleFilterExpr>["&"<simpleFilterExpr>]*
124 op := "eq" | "neq" (or "ne") | "gt" | "lt" | "gte" | "lte" | "cont" | "ncont"
126 For filtering inside array, it must select the element of the array, or add ANYINDEX to apply the filtering over any
127 item of the array, that is, pass if any item of the array pass the filter.
128 It allows both ne and neq for not equal
129 TODO: 4.3.3 Attribute selectors
130 all_fields, fields=x,y,.., exclude_default, exclude_fields=x,y,...
131 (none) … same as “exclude_default”
132 all_fields … all attributes.
133 fields=<list> … all attributes except all complex attributes with minimum cardinality of zero that are not
134 conditionally mandatory, and that are not provided in <list>.
135 exclude_fields=<list> … all attributes except those complex attributes with a minimum cardinality of zero that
136 are not conditionally mandatory, and that are provided in <list>.
137 exclude_default … all attributes except those complex attributes with a minimum cardinality of zero that are not
138 conditionally mandatory, and that are part of the "default exclude set" defined in the present specification for
139 the particular resource
140 exclude_default and include=<list> … all attributes except those complex attributes with a minimum cardinality
141 of zero that are not conditionally mandatory and that are part of the "default exclude set" defined in the
142 present specification for the particular resource, but that are not part of <list>
143 Header field name Reference Example Descriptions
144 Accept IETF RFC 7231 [19] application/json Content-Types that are acceptable for the response.
145 This header field shall be present if the response is expected to have a non-empty message body.
146 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the request.
147 This header field shall be present if the request has a non-empty message body.
148 Authorization IETF RFC 7235 [22] Bearer mF_9.B5f-4.1JqM The authorization token for the request.
149 Details are specified in clause 4.5.3.
150 Range IETF RFC 7233 [21] 1000-2000 Requested range of bytes from a file
151 Header field name Reference Example Descriptions
152 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the response.
153 This header field shall be present if the response has a non-empty message body.
154 Location IETF RFC 7231 [19] http://www.example.com/vnflcm/v1/vnf_instances/123 Used in redirection, or when a
155 new resource has been created.
156 This header field shall be present if the response status code is 201 or 3xx.
157 In the present document this header field is also used if the response status code is 202 and a new resource was
159 WWW-Authenticate IETF RFC 7235 [22] Bearer realm="example" Challenge if the corresponding HTTP request has not
160 provided authorization, or error details if the corresponding HTTP request has provided an invalid authorization
162 Accept-Ranges IETF RFC 7233 [21] bytes Used by the Server to signal whether or not it supports ranges for
164 Content-Range IETF RFC 7233 [21] bytes 21010-47021/ 47022 Signals the byte range that is contained in the
165 response, and the total length of the file.
166 Retry-After IETF RFC 7231 [19] Fri, 31 Dec 1999 23:59:59 GMT
170 class NbiException(Exception):
172 def __init__(self
, message
, http_code
=HTTPStatus
.METHOD_NOT_ALLOWED
):
173 Exception.__init
__(self
, message
)
174 self
.http_code
= http_code
177 class Server(object):
179 # to decode bytes to str
180 reader
= getreader("utf-8")
184 self
.engine
= Engine()
185 self
.authenticator
= Authenticator()
186 self
.valid_methods
= { # contains allowed URL and methods
189 "tokens": {"METHODS": ("GET", "POST", "DELETE"),
190 "<ID>": {"METHODS": ("GET", "DELETE")}
192 "users": {"METHODS": ("GET", "POST"),
193 "<ID>": {"METHODS": ("GET", "POST", "DELETE", "PATCH", "PUT")}
195 "projects": {"METHODS": ("GET", "POST"),
196 "<ID>": {"METHODS": ("GET", "DELETE")}
198 "vims": {"METHODS": ("GET", "POST"),
199 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
201 "vim_accounts": {"METHODS": ("GET", "POST"),
202 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
204 "sdns": {"METHODS": ("GET", "POST"),
205 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
211 "pdu_descriptors": {"METHODS": ("GET", "POST"),
212 "<ID>": {"METHODS": ("GET", "POST", "DELETE", "PATCH", "PUT")}
218 "ns_descriptors_content": {"METHODS": ("GET", "POST"),
219 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
221 "ns_descriptors": {"METHODS": ("GET", "POST"),
222 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"),
223 "nsd_content": {"METHODS": ("GET", "PUT")},
224 "nsd": {"METHODS": "GET"}, # descriptor inside package
225 "artifacts": {"*": {"METHODS": "GET"}}
228 "pnf_descriptors": {"TODO": ("GET", "POST"),
229 "<ID>": {"TODO": ("GET", "DELETE", "PATCH"),
230 "pnfd_content": {"TODO": ("GET", "PUT")}
233 "subscriptions": {"TODO": ("GET", "POST"),
234 "<ID>": {"TODO": ("GET", "DELETE")}
240 "vnf_packages_content": {"METHODS": ("GET", "POST"),
241 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
243 "vnf_packages": {"METHODS": ("GET", "POST"),
244 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"), # GET: vnfPkgInfo
245 "package_content": {"METHODS": ("GET", "PUT"), # package
246 "upload_from_uri": {"TODO": "POST"}
248 "vnfd": {"METHODS": "GET"}, # descriptor inside package
249 "artifacts": {"*": {"METHODS": "GET"}}
252 "subscriptions": {"TODO": ("GET", "POST"),
253 "<ID>": {"TODO": ("GET", "DELETE")}
259 "ns_instances_content": {"METHODS": ("GET", "POST"),
260 "<ID>": {"METHODS": ("GET", "DELETE")}
262 "ns_instances": {"METHODS": ("GET", "POST"),
263 "<ID>": {"METHODS": ("GET", "DELETE"),
264 "scale": {"METHODS": "POST"},
265 "terminate": {"METHODS": "POST"},
266 "instantiate": {"METHODS": "POST"},
267 "action": {"METHODS": "POST"},
270 "ns_lcm_op_occs": {"METHODS": "GET",
271 "<ID>": {"METHODS": "GET"},
273 "vnfrs": {"METHODS": ("GET"),
274 "<ID>": {"METHODS": ("GET")}
276 "vnf_instances": {"METHODS": ("GET"),
277 "<ID>": {"METHODS": ("GET")}
283 "netslice_templates_content": {"METHODS": ("GET", "POST"),
284 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
286 "netslice_templates": {"METHODS": ("GET", "POST"),
287 "<ID>": {"METHODS": ("GET", "DELETE"), "TODO": "PATCH",
288 "nst_content": {"METHODS": ("GET", "PUT")},
289 "nst": {"METHODS": "GET"}, # descriptor inside package
290 "artifacts": {"*": {"METHODS": "GET"}}
293 "subscriptions": {"TODO": ("GET", "POST"),
294 "<ID>": {"TODO": ("GET", "DELETE")}
300 "netslice_instances_content": {"METHODS": ("GET", "POST"),
301 "<ID>": {"METHODS": ("GET", "DELETE")}
303 "netslice_instances": {"METHODS": ("GET", "POST"),
304 "<ID>": {"METHODS": ("GET", "DELETE"),
305 "terminate": {"METHODS": "POST"},
306 "instantiate": {"METHODS": "POST"},
307 "action": {"METHODS": "POST"},
310 "nsi_lcm_op_occs": {"METHODS": "GET",
311 "<ID>": {"METHODS": "GET"},
317 def _format_in(self
, kwargs
):
320 if cherrypy
.request
.body
.length
:
321 error_text
= "Invalid input format "
323 if "Content-Type" in cherrypy
.request
.headers
:
324 if "application/json" in cherrypy
.request
.headers
["Content-Type"]:
325 error_text
= "Invalid json format "
326 indata
= json
.load(self
.reader(cherrypy
.request
.body
))
327 cherrypy
.request
.headers
.pop("Content-File-MD5", None)
328 elif "application/yaml" in cherrypy
.request
.headers
["Content-Type"]:
329 error_text
= "Invalid yaml format "
330 indata
= yaml
.load(cherrypy
.request
.body
)
331 cherrypy
.request
.headers
.pop("Content-File-MD5", None)
332 elif "application/binary" in cherrypy
.request
.headers
["Content-Type"] or \
333 "application/gzip" in cherrypy
.request
.headers
["Content-Type"] or \
334 "application/zip" in cherrypy
.request
.headers
["Content-Type"] or \
335 "text/plain" in cherrypy
.request
.headers
["Content-Type"]:
336 indata
= cherrypy
.request
.body
# .read()
337 elif "multipart/form-data" in cherrypy
.request
.headers
["Content-Type"]:
338 if "descriptor_file" in kwargs
:
339 filecontent
= kwargs
.pop("descriptor_file")
340 if not filecontent
.file:
341 raise NbiException("empty file or content", HTTPStatus
.BAD_REQUEST
)
342 indata
= filecontent
.file # .read()
343 if filecontent
.content_type
.value
:
344 cherrypy
.request
.headers
["Content-Type"] = filecontent
.content_type
.value
346 # raise cherrypy.HTTPError(HTTPStatus.Not_Acceptable,
347 # "Only 'Content-Type' of type 'application/json' or
348 # 'application/yaml' for input format are available")
349 error_text
= "Invalid yaml format "
350 indata
= yaml
.load(cherrypy
.request
.body
)
351 cherrypy
.request
.headers
.pop("Content-File-MD5", None)
353 error_text
= "Invalid yaml format "
354 indata
= yaml
.load(cherrypy
.request
.body
)
355 cherrypy
.request
.headers
.pop("Content-File-MD5", None)
360 if cherrypy
.request
.headers
.get("Query-String-Format") == "yaml":
363 for k
, v
in kwargs
.items():
364 if isinstance(v
, str):
369 kwargs
[k
] = yaml
.load(v
)
372 elif k
.endswith(".gt") or k
.endswith(".lt") or k
.endswith(".gte") or k
.endswith(".lte"):
380 elif v
.find(",") > 0:
381 kwargs
[k
] = v
.split(",")
382 elif isinstance(v
, (list, tuple)):
383 for index
in range(0, len(v
)):
388 v
[index
] = yaml
.load(v
[index
])
393 except (ValueError, yaml
.YAMLError
) as exc
:
394 raise NbiException(error_text
+ str(exc
), HTTPStatus
.BAD_REQUEST
)
395 except KeyError as exc
:
396 raise NbiException("Query string error: " + str(exc
), HTTPStatus
.BAD_REQUEST
)
397 except Exception as exc
:
398 raise NbiException(error_text
+ str(exc
), HTTPStatus
.BAD_REQUEST
)
401 def _format_out(data
, session
=None, _format
=None):
403 return string of dictionary data according to requested json, yaml, xml. By default json
404 :param data: response to be sent. Can be a dict, text or file
406 :param _format: The format to be set as Content-Type ir data is a file
409 accept
= cherrypy
.request
.headers
.get("Accept")
411 if accept
and "text/html" in accept
:
412 return html
.format(data
, cherrypy
.request
, cherrypy
.response
, session
)
413 # cherrypy.response.status = HTTPStatus.NO_CONTENT.value
415 elif hasattr(data
, "read"): # file object
417 cherrypy
.response
.headers
["Content-Type"] = _format
418 elif "b" in data
.mode
: # binariy asssumig zip
419 cherrypy
.response
.headers
["Content-Type"] = 'application/zip'
421 cherrypy
.response
.headers
["Content-Type"] = 'text/plain'
422 # TODO check that cherrypy close file. If not implement pending things to close per thread next
425 if "application/json" in accept
:
426 cherrypy
.response
.headers
["Content-Type"] = 'application/json; charset=utf-8'
427 a
= json
.dumps(data
, indent
=4) + "\n"
428 return a
.encode("utf8")
429 elif "text/html" in accept
:
430 return html
.format(data
, cherrypy
.request
, cherrypy
.response
, session
)
432 elif "application/yaml" in accept
or "*/*" in accept
or "text/plain" in accept
:
435 raise cherrypy
.HTTPError(HTTPStatus
.NOT_ACCEPTABLE
.value
,
436 "Only 'Accept' of type 'application/json' or 'application/yaml' "
437 "for output format are available")
438 cherrypy
.response
.headers
["Content-Type"] = 'application/yaml'
439 return yaml
.safe_dump(data
, explicit_start
=True, indent
=4, default_flow_style
=False, tags
=False,
440 encoding
='utf-8', allow_unicode
=True) # , canonical=True, default_style='"'
443 def index(self
, *args
, **kwargs
):
446 if cherrypy
.request
.method
== "GET":
447 session
= self
.authenticator
.authorize()
448 outdata
= "Index page"
450 raise cherrypy
.HTTPError(HTTPStatus
.METHOD_NOT_ALLOWED
.value
,
451 "Method {} not allowed for tokens".format(cherrypy
.request
.method
))
453 return self
._format
_out
(outdata
, session
)
455 except (EngineException
, AuthException
) as e
:
456 cherrypy
.log("index Exception {}".format(e
))
457 cherrypy
.response
.status
= e
.http_code
.value
458 return self
._format
_out
("Welcome to OSM!", session
)
461 def version(self
, *args
, **kwargs
):
462 # TODO consider to remove and provide version using the static version file
463 global __version__
, version_date
465 if cherrypy
.request
.method
!= "GET":
466 raise NbiException("Only method GET is allowed", HTTPStatus
.METHOD_NOT_ALLOWED
)
468 raise NbiException("Invalid URL or query string for version", HTTPStatus
.METHOD_NOT_ALLOWED
)
469 return __version__
+ " " + version_date
470 except NbiException
as 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
, None)
480 def token(self
, method
, token_id
=None, kwargs
=None):
482 # self.engine.load_dbase(cherrypy.request.app.config)
483 indata
= self
._format
_in
(kwargs
)
484 if not isinstance(indata
, dict):
485 raise NbiException("Expected application/yaml or application/json Content-Type", HTTPStatus
.BAD_REQUEST
)
488 session
= self
.authenticator
.authorize()
490 outdata
= self
.authenticator
.get_token(session
, token_id
)
492 outdata
= self
.authenticator
.get_token_list(session
)
493 elif method
== "POST":
495 session
= self
.authenticator
.authorize()
499 indata
.update(kwargs
)
500 outdata
= self
.authenticator
.new_token(session
, indata
, cherrypy
.request
.remote
)
502 cherrypy
.session
['Authorization'] = outdata
["_id"]
503 self
._set
_location
_header
("admin", "v1", "tokens", outdata
["_id"])
504 # cherrypy.response.cookie["Authorization"] = outdata["id"]
505 # cherrypy.response.cookie["Authorization"]['expires'] = 3600
506 elif method
== "DELETE":
507 if not token_id
and "id" in kwargs
:
508 token_id
= kwargs
["id"]
510 session
= self
.authenticator
.authorize()
511 token_id
= session
["_id"]
512 outdata
= self
.authenticator
.del_token(token_id
)
514 cherrypy
.session
['Authorization'] = "logout"
515 # cherrypy.response.cookie["Authorization"] = token_id
516 # cherrypy.response.cookie["Authorization"]['expires'] = 0
518 raise NbiException("Method {} not allowed for token".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
519 return self
._format
_out
(outdata
, session
)
520 except (NbiException
, EngineException
, DbException
, AuthException
) as e
:
521 cherrypy
.log("tokens Exception {}".format(e
))
522 cherrypy
.response
.status
= e
.http_code
.value
524 "code": e
.http_code
.name
,
525 "status": e
.http_code
.value
,
528 return self
._format
_out
(problem_details
, session
)
531 def test(self
, *args
, **kwargs
):
533 if args
and args
[0] == "help":
534 return "<html><pre>\ninit\nfile/<name> download file\ndb-clear/table\nprune\nlogin\nlogin2\n"\
535 "sleep/<time>\nmessage/topic\n</pre></html>"
537 elif args
and args
[0] == "init":
539 # self.engine.load_dbase(cherrypy.request.app.config)
540 self
.engine
.create_admin()
541 return "Done. User 'admin', password 'admin' created"
543 cherrypy
.response
.status
= HTTPStatus
.FORBIDDEN
.value
544 return self
._format
_out
("Database already initialized")
545 elif args
and args
[0] == "file":
546 return cherrypy
.lib
.static
.serve_file(cherrypy
.tree
.apps
['/osm'].config
["storage"]["path"] + "/" + args
[1],
547 "text/plain", "attachment")
548 elif args
and args
[0] == "file2":
549 f_path
= cherrypy
.tree
.apps
['/osm'].config
["storage"]["path"] + "/" + args
[1]
550 f
= open(f_path
, "r")
551 cherrypy
.response
.headers
["Content-type"] = "text/plain"
554 elif len(args
) == 2 and args
[0] == "db-clear":
555 return self
.engine
.db
.del_list(args
[1], kwargs
)
556 elif args
and args
[0] == "prune":
557 return self
.engine
.prune()
558 elif args
and args
[0] == "login":
559 if not cherrypy
.request
.headers
.get("Authorization"):
560 cherrypy
.response
.headers
["WWW-Authenticate"] = 'Basic realm="Access to OSM site", charset="UTF-8"'
561 cherrypy
.response
.status
= HTTPStatus
.UNAUTHORIZED
.value
562 elif args
and args
[0] == "login2":
563 if not cherrypy
.request
.headers
.get("Authorization"):
564 cherrypy
.response
.headers
["WWW-Authenticate"] = 'Bearer realm="Access to OSM site"'
565 cherrypy
.response
.status
= HTTPStatus
.UNAUTHORIZED
.value
566 elif args
and args
[0] == "sleep":
569 sleep_time
= int(args
[1])
571 cherrypy
.response
.status
= HTTPStatus
.FORBIDDEN
.value
572 return self
._format
_out
("Database already initialized")
573 thread_info
= cherrypy
.thread_data
575 time
.sleep(sleep_time
)
577 elif len(args
) >= 2 and args
[0] == "message":
579 return_text
= "<html><pre>{} ->\n".format(main_topic
)
581 if cherrypy
.request
.method
== 'POST':
582 to_send
= yaml
.load(cherrypy
.request
.body
)
583 for k
, v
in to_send
.items():
584 self
.engine
.msg
.write(main_topic
, k
, v
)
585 return_text
+= " {}: {}\n".format(k
, v
)
586 elif cherrypy
.request
.method
== 'GET':
587 for k
, v
in kwargs
.items():
588 self
.engine
.msg
.write(main_topic
, k
, yaml
.load(v
))
589 return_text
+= " {}: {}\n".format(k
, yaml
.load(v
))
590 except Exception as e
:
591 return_text
+= "Error: " + str(e
)
592 return_text
+= "</pre></html>\n"
596 "<html><pre>\nheaders:\n args: {}\n".format(args
) +
597 " kwargs: {}\n".format(kwargs
) +
598 " headers: {}\n".format(cherrypy
.request
.headers
) +
599 " path_info: {}\n".format(cherrypy
.request
.path_info
) +
600 " query_string: {}\n".format(cherrypy
.request
.query_string
) +
601 " session: {}\n".format(cherrypy
.session
) +
602 " cookie: {}\n".format(cherrypy
.request
.cookie
) +
603 " method: {}\n".format(cherrypy
.request
.method
) +
604 " session: {}\n".format(cherrypy
.session
.get('fieldname')) +
606 return_text
+= " length: {}\n".format(cherrypy
.request
.body
.length
)
607 if cherrypy
.request
.body
.length
:
608 return_text
+= " content: {}\n".format(
609 str(cherrypy
.request
.body
.read(int(cherrypy
.request
.headers
.get('Content-Length', 0)))))
611 return_text
+= "thread: {}\n".format(thread_info
)
612 return_text
+= "</pre></html>"
615 def _check_valid_url_method(self
, method
, *args
):
617 raise NbiException("URL must contain at least 'main_topic/version/topic'", HTTPStatus
.METHOD_NOT_ALLOWED
)
619 reference
= self
.valid_methods
623 if not isinstance(reference
, dict):
624 raise NbiException("URL contains unexpected extra items '{}'".format(arg
),
625 HTTPStatus
.METHOD_NOT_ALLOWED
)
628 reference
= reference
[arg
]
629 elif "<ID>" in reference
:
630 reference
= reference
["<ID>"]
631 elif "*" in reference
:
632 reference
= reference
["*"]
635 raise NbiException("Unexpected URL item {}".format(arg
), HTTPStatus
.METHOD_NOT_ALLOWED
)
636 if "TODO" in reference
and method
in reference
["TODO"]:
637 raise NbiException("Method {} not supported yet for this URL".format(method
), HTTPStatus
.NOT_IMPLEMENTED
)
638 elif "METHODS" in reference
and method
not in reference
["METHODS"]:
639 raise NbiException("Method {} not supported for this URL".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
643 def _set_location_header(main_topic
, version
, topic
, id):
645 Insert response header Location with the URL of created item base on URL params
652 # Use cherrypy.request.base for absoluted path and make use of request.header HOST just in case behind aNAT
653 cherrypy
.response
.headers
["Location"] = "/osm/{}/{}/{}/{}".format(main_topic
, version
, topic
, id)
657 def default(self
, main_topic
=None, version
=None, topic
=None, _id
=None, item
=None, *args
, **kwargs
):
666 if not main_topic
or not version
or not topic
:
667 raise NbiException("URL must contain at least 'main_topic/version/topic'",
668 HTTPStatus
.METHOD_NOT_ALLOWED
)
669 if main_topic
not in ("admin", "vnfpkgm", "nsd", "nslcm", "pdu", "nst", "nsilcm"):
670 raise NbiException("URL main_topic '{}' not supported".format(main_topic
),
671 HTTPStatus
.METHOD_NOT_ALLOWED
)
673 raise NbiException("URL version '{}' not supported".format(version
), HTTPStatus
.METHOD_NOT_ALLOWED
)
675 if kwargs
and "METHOD" in kwargs
and kwargs
["METHOD"] in ("PUT", "POST", "DELETE", "GET", "PATCH"):
676 method
= kwargs
.pop("METHOD")
678 method
= cherrypy
.request
.method
679 if kwargs
and "FORCE" in kwargs
:
680 force
= kwargs
.pop("FORCE")
683 self
._check
_valid
_url
_method
(method
, main_topic
, version
, topic
, _id
, item
, *args
)
684 if main_topic
== "admin" and topic
== "tokens":
685 return self
.token(method
, _id
, kwargs
)
687 # self.engine.load_dbase(cherrypy.request.app.config)
688 session
= self
.authenticator
.authorize()
689 indata
= self
._format
_in
(kwargs
)
691 if topic
== "subscriptions":
692 engine_topic
= main_topic
+ "_" + topic
696 if main_topic
== "nsd":
697 engine_topic
= "nsds"
698 elif main_topic
== "vnfpkgm":
699 engine_topic
= "vnfds"
700 elif main_topic
== "nslcm":
701 engine_topic
= "nsrs"
702 if topic
== "ns_lcm_op_occs":
703 engine_topic
= "nslcmops"
704 if topic
== "vnfrs" or topic
== "vnf_instances":
705 engine_topic
= "vnfrs"
706 elif main_topic
== "nst":
707 engine_topic
= "nsts"
708 elif main_topic
== "nsilcm":
709 engine_topic
= "nsis"
710 if topic
== "nsi_lcm_op_occs":
711 engine_topic
= "nsilcmops"
712 elif main_topic
== "pdu":
713 engine_topic
= "pdus"
714 if engine_topic
== "vims": # TODO this is for backward compatibility, it will remove in the future
715 engine_topic
= "vim_accounts"
718 if item
in ("nsd_content", "package_content", "artifacts", "vnfd", "nsd", "nst", "nst_content"):
719 if item
in ("vnfd", "nsd", "nst"):
723 elif item
== "artifacts":
727 file, _format
= self
.engine
.get_file(session
, engine_topic
, _id
, path
,
728 cherrypy
.request
.headers
.get("Accept"))
731 outdata
= self
.engine
.get_item_list(session
, engine_topic
, kwargs
)
733 outdata
= self
.engine
.get_item(session
, engine_topic
, _id
)
734 elif method
== "POST":
735 if topic
in ("ns_descriptors_content", "vnf_packages_content", "netslice_templates_content"):
736 _id
= cherrypy
.request
.headers
.get("Transaction-Id")
738 _id
= self
.engine
.new_item(rollback
, session
, engine_topic
, {}, None, cherrypy
.request
.headers
,
740 completed
= self
.engine
.upload_content(session
, engine_topic
, _id
, indata
, kwargs
,
741 cherrypy
.request
.headers
, force
=force
)
743 self
._set
_location
_header
(main_topic
, version
, topic
, _id
)
745 cherrypy
.response
.headers
["Transaction-Id"] = _id
746 outdata
= {"id": _id
}
747 elif topic
== "ns_instances_content":
749 _id
= self
.engine
.new_item(rollback
, session
, engine_topic
, indata
, kwargs
, force
=force
)
751 indata
["lcmOperationType"] = "instantiate"
752 indata
["nsInstanceId"] = _id
753 self
.engine
.new_item(rollback
, session
, "nslcmops", indata
, None)
754 self
._set
_location
_header
(main_topic
, version
, topic
, _id
)
755 outdata
= {"id": _id
}
756 elif topic
== "ns_instances" and item
:
757 indata
["lcmOperationType"] = item
758 indata
["nsInstanceId"] = _id
759 _id
= self
.engine
.new_item(rollback
, session
, "nslcmops", indata
, kwargs
)
760 self
._set
_location
_header
(main_topic
, version
, "ns_lcm_op_occs", _id
)
761 outdata
= {"id": _id
}
762 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
763 elif topic
== "netslice_instances_content":
764 # creates NetSlice_Instance_record (NSIR)
765 _id
= self
.engine
.new_item(rollback
, session
, engine_topic
, indata
, kwargs
, force
=force
)
766 self
._set
_location
_header
(main_topic
, version
, topic
, _id
)
767 indata
["lcmOperationType"] = "instantiate"
768 indata
["nsiInstanceId"] = _id
769 self
.engine
.new_item(rollback
, session
, "nsilcmops", indata
, kwargs
)
770 outdata
= {"id": _id
}
772 elif topic
== "netslice_instances" and item
:
773 indata
["lcmOperationType"] = item
774 indata
["nsiInstanceId"] = _id
775 _id
= self
.engine
.new_item(rollback
, session
, "nsilcmops", indata
, kwargs
)
776 self
._set
_location
_header
(main_topic
, version
, "nsi_lcm_op_occs", _id
)
777 outdata
= {"id": _id
}
778 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
780 _id
= self
.engine
.new_item(rollback
, session
, engine_topic
, indata
, kwargs
,
781 cherrypy
.request
.headers
, force
=force
)
782 self
._set
_location
_header
(main_topic
, version
, topic
, _id
)
783 outdata
= {"id": _id
}
784 # TODO form NsdInfo when topic in ("ns_descriptors", "vnf_packages")
785 cherrypy
.response
.status
= HTTPStatus
.CREATED
.value
787 elif method
== "DELETE":
789 outdata
= self
.engine
.del_item_list(session
, engine_topic
, kwargs
)
790 cherrypy
.response
.status
= HTTPStatus
.OK
.value
791 else: # len(args) > 1
792 if topic
== "ns_instances_content" and not force
:
794 "lcmOperationType": "terminate",
798 opp_id
= self
.engine
.new_item(rollback
, session
, "nslcmops", nslcmop_desc
, None)
799 outdata
= {"_id": opp_id
}
800 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
801 elif topic
== "netslice_instances_content" and not force
:
803 "lcmOperationType": "terminate",
804 "nsiInstanceId": _id
,
807 opp_id
= self
.engine
.new_item(rollback
, session
, "nsilcmops", nsilcmop_desc
, None)
808 outdata
= {"_id": opp_id
}
809 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
811 self
.engine
.del_item(session
, engine_topic
, _id
, force
)
812 cherrypy
.response
.status
= HTTPStatus
.NO_CONTENT
.value
813 if engine_topic
in ("vim_accounts", "sdns"):
814 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
816 elif method
in ("PUT", "PATCH"):
818 if not indata
and not kwargs
:
819 raise NbiException("Nothing to update. Provide payload and/or query string",
820 HTTPStatus
.BAD_REQUEST
)
821 if item
in ("nsd_content", "package_content", "nst_content") and method
== "PUT":
822 completed
= self
.engine
.upload_content(session
, engine_topic
, _id
, indata
, kwargs
,
823 cherrypy
.request
.headers
, force
=force
)
825 cherrypy
.response
.headers
["Transaction-Id"] = id
827 self
.engine
.edit_item(session
, engine_topic
, _id
, indata
, kwargs
, force
=force
)
828 cherrypy
.response
.status
= HTTPStatus
.NO_CONTENT
.value
830 raise NbiException("Method {} not allowed".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
831 return self
._format
_out
(outdata
, session
, _format
)
832 except Exception as e
:
833 if isinstance(e
, (NbiException
, EngineException
, DbException
, FsException
, MsgException
, AuthException
,
835 http_code_value
= cherrypy
.response
.status
= e
.http_code
.value
836 http_code_name
= e
.http_code
.name
837 cherrypy
.log("Exception {}".format(e
))
839 http_code_value
= cherrypy
.response
.status
= HTTPStatus
.BAD_REQUEST
.value
# INTERNAL_SERVER_ERROR
840 cherrypy
.log("CRITICAL: Exception {}".format(e
))
841 http_code_name
= HTTPStatus
.BAD_REQUEST
.name
842 if hasattr(outdata
, "close"): # is an open file
846 for rollback_item
in rollback
:
848 if rollback_item
.get("operation") == "set":
849 self
.engine
.db
.set_one(rollback_item
["topic"], {"_id": rollback_item
["_id"]},
850 rollback_item
["content"], fail_on_empty
=False)
852 self
.engine
.del_item(**rollback_item
, session
=session
, force
=True)
853 except Exception as e2
:
854 rollback_error_text
= "Rollback Exception {}: {}".format(rollback_item
, e2
)
855 cherrypy
.log(rollback_error_text
)
856 error_text
+= ". " + rollback_error_text
857 # if isinstance(e, MsgException):
858 # error_text = "{} has been '{}' but other modules cannot be informed because an error on bus".format(
859 # engine_topic[:-1], method, error_text)
861 "code": http_code_name
,
862 "status": http_code_value
,
863 "detail": error_text
,
865 return self
._format
_out
(problem_details
, session
)
866 # raise cherrypy.HTTPError(e.http_code.value, str(e))
869 # def validate_password(realm, username, password):
870 # cherrypy.log("realm "+ str(realm))
871 # if username == "admin" and password == "admin":
876 def _start_service():
878 Callback function called when cherrypy.engine starts
879 Override configuration with env variables
880 Set database, storage, message configuration
881 Init database with admin/admin user password
883 cherrypy
.log
.error("Starting osm_nbi")
884 # update general cherrypy configuration
887 engine_config
= cherrypy
.tree
.apps
['/osm'].config
888 for k
, v
in environ
.items():
889 if not k
.startswith("OSMNBI_"):
891 k1
, _
, k2
= k
[7:].lower().partition("_")
895 # update static configuration
896 if k
== 'OSMNBI_STATIC_DIR':
897 engine_config
["/static"]['tools.staticdir.dir'] = v
898 engine_config
["/static"]['tools.staticdir.on'] = True
899 elif k
== 'OSMNBI_SOCKET_PORT' or k
== 'OSMNBI_SERVER_PORT':
900 update_dict
['server.socket_port'] = int(v
)
901 elif k
== 'OSMNBI_SOCKET_HOST' or k
== 'OSMNBI_SERVER_HOST':
902 update_dict
['server.socket_host'] = v
903 elif k1
in ("server", "test", "auth", "log"):
904 update_dict
[k1
+ '.' + k2
] = v
905 elif k1
in ("message", "database", "storage", "authentication"):
906 # k2 = k2.replace('_', '.')
907 if k2
in ("port", "db_port"):
908 engine_config
[k1
][k2
] = int(v
)
910 engine_config
[k1
][k2
] = v
912 except ValueError as e
:
913 cherrypy
.log
.error("Ignoring environ '{}': " + str(e
))
914 except Exception as e
:
915 cherrypy
.log
.warn("skipping environ '{}' on exception '{}'".format(k
, e
))
918 cherrypy
.config
.update(update_dict
)
919 engine_config
["global"].update(update_dict
)
922 log_format_simple
= "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(message)s"
923 log_formatter_simple
= logging
.Formatter(log_format_simple
, datefmt
='%Y-%m-%dT%H:%M:%S')
924 logger_server
= logging
.getLogger("cherrypy.error")
925 logger_access
= logging
.getLogger("cherrypy.access")
926 logger_cherry
= logging
.getLogger("cherrypy")
927 logger_nbi
= logging
.getLogger("nbi")
929 if "log.file" in engine_config
["global"]:
930 file_handler
= logging
.handlers
.RotatingFileHandler(engine_config
["global"]["log.file"],
931 maxBytes
=100e6
, backupCount
=9, delay
=0)
932 file_handler
.setFormatter(log_formatter_simple
)
933 logger_cherry
.addHandler(file_handler
)
934 logger_nbi
.addHandler(file_handler
)
935 # log always to standard output
936 for format_
, logger
in {"nbi.server %(filename)s:%(lineno)s": logger_server
,
937 "nbi.access %(filename)s:%(lineno)s": logger_access
,
938 "%(name)s %(filename)s:%(lineno)s": logger_nbi
940 log_format_cherry
= "%(asctime)s %(levelname)s {} %(message)s".format(format_
)
941 log_formatter_cherry
= logging
.Formatter(log_format_cherry
, datefmt
='%Y-%m-%dT%H:%M:%S')
942 str_handler
= logging
.StreamHandler()
943 str_handler
.setFormatter(log_formatter_cherry
)
944 logger
.addHandler(str_handler
)
946 if engine_config
["global"].get("log.level"):
947 logger_cherry
.setLevel(engine_config
["global"]["log.level"])
948 logger_nbi
.setLevel(engine_config
["global"]["log.level"])
950 # logging other modules
951 for k1
, logname
in {"message": "nbi.msg", "database": "nbi.db", "storage": "nbi.fs"}.items():
952 engine_config
[k1
]["logger_name"] = logname
953 logger_module
= logging
.getLogger(logname
)
954 if "logfile" in engine_config
[k1
]:
955 file_handler
= logging
.handlers
.RotatingFileHandler(engine_config
[k1
]["logfile"],
956 maxBytes
=100e6
, backupCount
=9, delay
=0)
957 file_handler
.setFormatter(log_formatter_simple
)
958 logger_module
.addHandler(file_handler
)
959 if "loglevel" in engine_config
[k1
]:
960 logger_module
.setLevel(engine_config
[k1
]["loglevel"])
961 # TODO add more entries, e.g.: storage
962 cherrypy
.tree
.apps
['/osm'].root
.engine
.start(engine_config
)
963 cherrypy
.tree
.apps
['/osm'].root
.authenticator
.start(engine_config
)
964 cherrypy
.tree
.apps
['/osm'].root
.engine
.init_db(target_version
=database_version
)
965 cherrypy
.tree
.apps
['/osm'].root
.authenticator
.init_db(target_version
=auth_database_version
)
966 # getenv('OSMOPENMANO_TENANT', None)
971 Callback function called when cherrypy.engine stops
972 TODO: Ending database connections.
974 cherrypy
.tree
.apps
['/osm'].root
.engine
.stop()
975 cherrypy
.log
.error("Stopping osm_nbi")
978 def nbi(config_file
):
981 # #'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
982 # 'tools.sessions.on': True,
983 # 'tools.response_headers.on': True,
984 # # 'tools.response_headers.headers': [('Content-Type', 'text/plain')],
987 # cherrypy.Server.ssl_module = 'builtin'
988 # cherrypy.Server.ssl_certificate = "http/cert.pem"
989 # cherrypy.Server.ssl_private_key = "http/privkey.pem"
990 # cherrypy.Server.thread_pool = 10
991 # cherrypy.config.update({'Server.socket_port': config["port"], 'Server.socket_host': config["host"]})
993 # cherrypy.config.update({'tools.auth_basic.on': True,
994 # 'tools.auth_basic.realm': 'localhost',
995 # 'tools.auth_basic.checkpassword': validate_password})
996 cherrypy
.engine
.subscribe('start', _start_service
)
997 cherrypy
.engine
.subscribe('stop', _stop_service
)
998 cherrypy
.quickstart(Server(), '/osm', config_file
)
1002 print("""Usage: {} [options]
1003 -c|--config [configuration_file]: loads the configuration file (default: ./nbi.cfg)
1004 -h|--help: shows this help
1005 """.format(sys
.argv
[0]))
1006 # --log-socket-host HOST: send logs to this host")
1007 # --log-socket-port PORT: send logs using this port (default: 9022)")
1010 if __name__
== '__main__':
1012 # load parameters and configuration
1013 opts
, args
= getopt
.getopt(sys
.argv
[1:], "hvc:", ["config=", "help"])
1014 # TODO add "log-socket-host=", "log-socket-port=", "log-file="
1017 if o
in ("-h", "--help"):
1020 elif o
in ("-c", "--config"):
1022 # elif o == "--log-socket-port":
1023 # log_socket_port = a
1024 # elif o == "--log-socket-host":
1025 # log_socket_host = a
1026 # elif o == "--log-file":
1029 assert False, "Unhandled option"
1031 if not path
.isfile(config_file
):
1032 print("configuration file '{}' that not exist".format(config_file
), file=sys
.stderr
)
1035 for config_file
in (__file__
[:__file__
.rfind(".")] + ".cfg", "./nbi.cfg", "/etc/osm/nbi.cfg"):
1036 if path
.isfile(config_file
):
1039 print("No configuration file 'nbi.cfg' found neither at local folder nor at /etc/osm/", file=sys
.stderr
)
1042 except getopt
.GetoptError
as e
:
1043 print(str(e
), file=sys
.stderr
)