2 # -*- coding: utf-8 -*-
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at
8 # http://www.apache.org/licenses/LICENSE-2.0
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
21 import html_out
as html
23 import logging
.handlers
27 from authconn
import AuthException
28 from auth
import Authenticator
29 from engine
import Engine
, EngineException
30 from subscriptions
import SubscriptionThread
31 from validation
import ValidationError
32 from osm_common
.dbbase
import DbException
33 from osm_common
.fsbase
import FsException
34 from osm_common
.msgbase
import MsgException
35 from http
import HTTPStatus
36 from codecs
import getreader
37 from os
import environ
, path
39 __author__
= "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
42 version_date
= "Jan 2019"
43 database_version
= '1.1'
44 auth_database_version
= '1.0'
45 nbi_server
= None # instance of Server class
46 subscription_thread
= None # instance of SubscriptionThread class
50 North Bound Interface (O: OSM specific; 5,X: SOL005 not implemented yet; O5: SOL005 implemented)
51 URL: /osm GET POST PUT DELETE PATCH
53 /ns_descriptors_content O O
59 /artifacts[/<artifactPath>] O
67 /vnf_packages_content O O
71 /package_content O5 O5
74 /artifacts[/<artifactPath>] O5
79 /ns_instances_content O O
91 /vnf_instances (also vnfrs for compatibility) O
107 /vim_accounts (also vims for compatibility) O O
115 /netslice_templates_content O O
117 /netslice_templates O O
121 /artifacts[/<artifactPath>] O
123 /<subscriptionId> X X
126 /netslice_instances_content O O
127 /<SliceInstanceId> O O
128 /netslice_instances O O
129 /<SliceInstanceId> O O
134 /<nsiLcmOpOccId> O O O
136 /<subscriptionId> X X
139 Follows SOL005 section 4.3.2 It contains extra METHOD to override http method, FORCE to force.
140 simpleFilterExpr := <attrName>["."<attrName>]*["."<op>]"="<value>[","<value>]*
141 filterExpr := <simpleFilterExpr>["&"<simpleFilterExpr>]*
142 op := "eq" | "neq" (or "ne") | "gt" | "lt" | "gte" | "lte" | "cont" | "ncont"
144 For filtering inside array, it must select the element of the array, or add ANYINDEX to apply the filtering over any
145 item of the array, that is, pass if any item of the array pass the filter.
146 It allows both ne and neq for not equal
147 TODO: 4.3.3 Attribute selectors
148 all_fields, fields=x,y,.., exclude_default, exclude_fields=x,y,...
149 (none) … same as “exclude_default”
150 all_fields … all attributes.
151 fields=<list> … all attributes except all complex attributes with minimum cardinality of zero that are not
152 conditionally mandatory, and that are not provided in <list>.
153 exclude_fields=<list> … all attributes except those complex attributes with a minimum cardinality of zero that
154 are not conditionally mandatory, and that are provided in <list>.
155 exclude_default … all attributes except those complex attributes with a minimum cardinality of zero that are not
156 conditionally mandatory, and that are part of the "default exclude set" defined in the present specification for
157 the particular resource
158 exclude_default and include=<list> … all attributes except those complex attributes with a minimum cardinality
159 of zero that are not conditionally mandatory and that are part of the "default exclude set" defined in the
160 present specification for the particular resource, but that are not part of <list>
161 Additionally it admits some administrator values:
162 FORCE: To force operations skipping dependency checkings
163 ADMIN: To act as an administrator or a different project
164 PUBLIC: To get public descriptors or set a descriptor as public
165 SET_PROJECT: To make a descriptor available for other project
167 Header field name Reference Example Descriptions
168 Accept IETF RFC 7231 [19] application/json Content-Types that are acceptable for the response.
169 This header field shall be present if the response is expected to have a non-empty message body.
170 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the request.
171 This header field shall be present if the request has a non-empty message body.
172 Authorization IETF RFC 7235 [22] Bearer mF_9.B5f-4.1JqM The authorization token for the request.
173 Details are specified in clause 4.5.3.
174 Range IETF RFC 7233 [21] 1000-2000 Requested range of bytes from a file
175 Header field name Reference Example Descriptions
176 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the response.
177 This header field shall be present if the response has a non-empty message body.
178 Location IETF RFC 7231 [19] http://www.example.com/vnflcm/v1/vnf_instances/123 Used in redirection, or when a
179 new resource has been created.
180 This header field shall be present if the response status code is 201 or 3xx.
181 In the present document this header field is also used if the response status code is 202 and a new resource was
183 WWW-Authenticate IETF RFC 7235 [22] Bearer realm="example" Challenge if the corresponding HTTP request has not
184 provided authorization, or error details if the corresponding HTTP request has provided an invalid authorization
186 Accept-Ranges IETF RFC 7233 [21] bytes Used by the Server to signal whether or not it supports ranges for
188 Content-Range IETF RFC 7233 [21] bytes 21010-47021/ 47022 Signals the byte range that is contained in the
189 response, and the total length of the file.
190 Retry-After IETF RFC 7231 [19] Fri, 31 Dec 1999 23:59:59 GMT
194 class NbiException(Exception):
196 def __init__(self
, message
, http_code
=HTTPStatus
.METHOD_NOT_ALLOWED
):
197 Exception.__init
__(self
, message
)
198 self
.http_code
= http_code
201 class Server(object):
203 # to decode bytes to str
204 reader
= getreader("utf-8")
208 self
.engine
= Engine()
209 self
.authenticator
= Authenticator()
210 self
.valid_methods
= { # contains allowed URL and methods
213 "tokens": {"METHODS": ("GET", "POST", "DELETE"),
214 "<ID>": {"METHODS": ("GET", "DELETE")}
216 "users": {"METHODS": ("GET", "POST"),
217 "<ID>": {"METHODS": ("GET", "POST", "DELETE", "PATCH", "PUT")}
219 "projects": {"METHODS": ("GET", "POST"),
220 # Added PUT to allow Project Name modification
221 "<ID>": {"METHODS": ("GET", "DELETE", "PUT")}
223 "roles": {"METHODS": ("GET", "POST"),
224 "<ID>": {"METHODS": ("GET", "POST", "DELETE")}
226 "vims": {"METHODS": ("GET", "POST"),
227 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
229 "vim_accounts": {"METHODS": ("GET", "POST"),
230 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
232 "wim_accounts": {"METHODS": ("GET", "POST"),
233 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
235 "sdns": {"METHODS": ("GET", "POST"),
236 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
242 "pdu_descriptors": {"METHODS": ("GET", "POST"),
243 "<ID>": {"METHODS": ("GET", "POST", "DELETE", "PATCH", "PUT")}
249 "ns_descriptors_content": {"METHODS": ("GET", "POST"),
250 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
252 "ns_descriptors": {"METHODS": ("GET", "POST"),
253 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"),
254 "nsd_content": {"METHODS": ("GET", "PUT")},
255 "nsd": {"METHODS": "GET"}, # descriptor inside package
256 "artifacts": {"*": {"METHODS": "GET"}}
259 "pnf_descriptors": {"TODO": ("GET", "POST"),
260 "<ID>": {"TODO": ("GET", "DELETE", "PATCH"),
261 "pnfd_content": {"TODO": ("GET", "PUT")}
264 "subscriptions": {"TODO": ("GET", "POST"),
265 "<ID>": {"TODO": ("GET", "DELETE")}
271 "vnf_packages_content": {"METHODS": ("GET", "POST"),
272 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
274 "vnf_packages": {"METHODS": ("GET", "POST"),
275 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"), # GET: vnfPkgInfo
276 "package_content": {"METHODS": ("GET", "PUT"), # package
277 "upload_from_uri": {"TODO": "POST"}
279 "vnfd": {"METHODS": "GET"}, # descriptor inside package
280 "artifacts": {"*": {"METHODS": "GET"}}
283 "subscriptions": {"TODO": ("GET", "POST"),
284 "<ID>": {"TODO": ("GET", "DELETE")}
290 "ns_instances_content": {"METHODS": ("GET", "POST"),
291 "<ID>": {"METHODS": ("GET", "DELETE")}
293 "ns_instances": {"METHODS": ("GET", "POST"),
294 "<ID>": {"METHODS": ("GET", "DELETE"),
295 "scale": {"METHODS": "POST"},
296 "terminate": {"METHODS": "POST"},
297 "instantiate": {"METHODS": "POST"},
298 "action": {"METHODS": "POST"},
301 "ns_lcm_op_occs": {"METHODS": "GET",
302 "<ID>": {"METHODS": "GET"},
304 "vnfrs": {"METHODS": ("GET"),
305 "<ID>": {"METHODS": ("GET")}
307 "vnf_instances": {"METHODS": ("GET"),
308 "<ID>": {"METHODS": ("GET")}
314 "netslice_templates_content": {"METHODS": ("GET", "POST"),
315 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
317 "netslice_templates": {"METHODS": ("GET", "POST"),
318 "<ID>": {"METHODS": ("GET", "DELETE"), "TODO": "PATCH",
319 "nst_content": {"METHODS": ("GET", "PUT")},
320 "nst": {"METHODS": "GET"}, # descriptor inside package
321 "artifacts": {"*": {"METHODS": "GET"}}
324 "subscriptions": {"TODO": ("GET", "POST"),
325 "<ID>": {"TODO": ("GET", "DELETE")}
331 "netslice_instances_content": {"METHODS": ("GET", "POST"),
332 "<ID>": {"METHODS": ("GET", "DELETE")}
334 "netslice_instances": {"METHODS": ("GET", "POST"),
335 "<ID>": {"METHODS": ("GET", "DELETE"),
336 "terminate": {"METHODS": "POST"},
337 "instantiate": {"METHODS": "POST"},
338 "action": {"METHODS": "POST"},
341 "nsi_lcm_op_occs": {"METHODS": "GET",
342 "<ID>": {"METHODS": "GET"},
351 "<ID>": {"METHODS": ("GET")}
359 def _format_in(self
, kwargs
):
362 if cherrypy
.request
.body
.length
:
363 error_text
= "Invalid input format "
365 if "Content-Type" in cherrypy
.request
.headers
:
366 if "application/json" in cherrypy
.request
.headers
["Content-Type"]:
367 error_text
= "Invalid json format "
368 indata
= json
.load(self
.reader(cherrypy
.request
.body
))
369 cherrypy
.request
.headers
.pop("Content-File-MD5", None)
370 elif "application/yaml" in cherrypy
.request
.headers
["Content-Type"]:
371 error_text
= "Invalid yaml format "
372 indata
= yaml
.load(cherrypy
.request
.body
)
373 cherrypy
.request
.headers
.pop("Content-File-MD5", None)
374 elif "application/binary" in cherrypy
.request
.headers
["Content-Type"] or \
375 "application/gzip" in cherrypy
.request
.headers
["Content-Type"] or \
376 "application/zip" in cherrypy
.request
.headers
["Content-Type"] or \
377 "text/plain" in cherrypy
.request
.headers
["Content-Type"]:
378 indata
= cherrypy
.request
.body
# .read()
379 elif "multipart/form-data" in cherrypy
.request
.headers
["Content-Type"]:
380 if "descriptor_file" in kwargs
:
381 filecontent
= kwargs
.pop("descriptor_file")
382 if not filecontent
.file:
383 raise NbiException("empty file or content", HTTPStatus
.BAD_REQUEST
)
384 indata
= filecontent
.file # .read()
385 if filecontent
.content_type
.value
:
386 cherrypy
.request
.headers
["Content-Type"] = filecontent
.content_type
.value
388 # raise cherrypy.HTTPError(HTTPStatus.Not_Acceptable,
389 # "Only 'Content-Type' of type 'application/json' or
390 # 'application/yaml' for input format are available")
391 error_text
= "Invalid yaml format "
392 indata
= yaml
.load(cherrypy
.request
.body
)
393 cherrypy
.request
.headers
.pop("Content-File-MD5", None)
395 error_text
= "Invalid yaml format "
396 indata
= yaml
.load(cherrypy
.request
.body
)
397 cherrypy
.request
.headers
.pop("Content-File-MD5", None)
402 if cherrypy
.request
.headers
.get("Query-String-Format") == "yaml":
405 for k
, v
in kwargs
.items():
406 if isinstance(v
, str):
411 kwargs
[k
] = yaml
.load(v
)
414 elif k
.endswith(".gt") or k
.endswith(".lt") or k
.endswith(".gte") or k
.endswith(".lte"):
422 elif v
.find(",") > 0:
423 kwargs
[k
] = v
.split(",")
424 elif isinstance(v
, (list, tuple)):
425 for index
in range(0, len(v
)):
430 v
[index
] = yaml
.load(v
[index
])
435 except (ValueError, yaml
.YAMLError
) as exc
:
436 raise NbiException(error_text
+ str(exc
), HTTPStatus
.BAD_REQUEST
)
437 except KeyError as exc
:
438 raise NbiException("Query string error: " + str(exc
), HTTPStatus
.BAD_REQUEST
)
439 except Exception as exc
:
440 raise NbiException(error_text
+ str(exc
), HTTPStatus
.BAD_REQUEST
)
443 def _format_out(data
, session
=None, _format
=None):
445 return string of dictionary data according to requested json, yaml, xml. By default json
446 :param data: response to be sent. Can be a dict, text or file
448 :param _format: The format to be set as Content-Type ir data is a file
451 accept
= cherrypy
.request
.headers
.get("Accept")
453 if accept
and "text/html" in accept
:
454 return html
.format(data
, cherrypy
.request
, cherrypy
.response
, session
)
455 # cherrypy.response.status = HTTPStatus.NO_CONTENT.value
457 elif hasattr(data
, "read"): # file object
459 cherrypy
.response
.headers
["Content-Type"] = _format
460 elif "b" in data
.mode
: # binariy asssumig zip
461 cherrypy
.response
.headers
["Content-Type"] = 'application/zip'
463 cherrypy
.response
.headers
["Content-Type"] = 'text/plain'
464 # TODO check that cherrypy close file. If not implement pending things to close per thread next
467 if "application/json" in accept
:
468 cherrypy
.response
.headers
["Content-Type"] = 'application/json; charset=utf-8'
469 a
= json
.dumps(data
, indent
=4) + "\n"
470 return a
.encode("utf8")
471 elif "text/html" in accept
:
472 return html
.format(data
, cherrypy
.request
, cherrypy
.response
, session
)
474 elif "application/yaml" in accept
or "*/*" in accept
or "text/plain" in accept
:
476 # if there is not any valid accept, raise an error. But if response is already an error, format in yaml
477 elif cherrypy
.response
.status
>= 400:
478 raise cherrypy
.HTTPError(HTTPStatus
.NOT_ACCEPTABLE
.value
,
479 "Only 'Accept' of type 'application/json' or 'application/yaml' "
480 "for output format are available")
481 cherrypy
.response
.headers
["Content-Type"] = 'application/yaml'
482 return yaml
.safe_dump(data
, explicit_start
=True, indent
=4, default_flow_style
=False, tags
=False,
483 encoding
='utf-8', allow_unicode
=True) # , canonical=True, default_style='"'
486 def index(self
, *args
, **kwargs
):
489 if cherrypy
.request
.method
== "GET":
490 session
= self
.authenticator
.authorize()
491 outdata
= "Index page"
493 raise cherrypy
.HTTPError(HTTPStatus
.METHOD_NOT_ALLOWED
.value
,
494 "Method {} not allowed for tokens".format(cherrypy
.request
.method
))
496 return self
._format
_out
(outdata
, session
)
498 except (EngineException
, AuthException
) as e
:
499 cherrypy
.log("index Exception {}".format(e
))
500 cherrypy
.response
.status
= e
.http_code
.value
501 return self
._format
_out
("Welcome to OSM!", session
)
504 def version(self
, *args
, **kwargs
):
505 # TODO consider to remove and provide version using the static version file
506 global __version__
, version_date
508 if cherrypy
.request
.method
!= "GET":
509 raise NbiException("Only method GET is allowed", HTTPStatus
.METHOD_NOT_ALLOWED
)
511 raise NbiException("Invalid URL or query string for version", HTTPStatus
.METHOD_NOT_ALLOWED
)
512 return __version__
+ " " + version_date
513 except NbiException
as e
:
514 cherrypy
.response
.status
= e
.http_code
.value
516 "code": e
.http_code
.name
,
517 "status": e
.http_code
.value
,
520 return self
._format
_out
(problem_details
, None)
523 def token(self
, method
, token_id
=None, kwargs
=None):
525 # self.engine.load_dbase(cherrypy.request.app.config)
526 indata
= self
._format
_in
(kwargs
)
527 if not isinstance(indata
, dict):
528 raise NbiException("Expected application/yaml or application/json Content-Type", HTTPStatus
.BAD_REQUEST
)
531 session
= self
.authenticator
.authorize()
533 outdata
= self
.authenticator
.get_token(session
, token_id
)
535 outdata
= self
.authenticator
.get_token_list(session
)
536 elif method
== "POST":
538 session
= self
.authenticator
.authorize()
542 indata
.update(kwargs
)
543 outdata
= self
.authenticator
.new_token(session
, indata
, cherrypy
.request
.remote
)
545 cherrypy
.session
['Authorization'] = outdata
["_id"]
546 self
._set
_location
_header
("admin", "v1", "tokens", outdata
["_id"])
547 # cherrypy.response.cookie["Authorization"] = outdata["id"]
548 # cherrypy.response.cookie["Authorization"]['expires'] = 3600
549 elif method
== "DELETE":
550 if not token_id
and "id" in kwargs
:
551 token_id
= kwargs
["id"]
553 session
= self
.authenticator
.authorize()
554 token_id
= session
["_id"]
555 outdata
= self
.authenticator
.del_token(token_id
)
557 cherrypy
.session
['Authorization'] = "logout"
558 # cherrypy.response.cookie["Authorization"] = token_id
559 # cherrypy.response.cookie["Authorization"]['expires'] = 0
561 raise NbiException("Method {} not allowed for token".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
562 return self
._format
_out
(outdata
, session
)
563 except (NbiException
, EngineException
, DbException
, AuthException
) as e
:
564 cherrypy
.log("tokens Exception {}".format(e
))
565 cherrypy
.response
.status
= e
.http_code
.value
567 "code": e
.http_code
.name
,
568 "status": e
.http_code
.value
,
571 return self
._format
_out
(problem_details
, session
)
574 def test(self
, *args
, **kwargs
):
576 if args
and args
[0] == "help":
577 return "<html><pre>\ninit\nfile/<name> download file\ndb-clear/table\nfs-clear[/folder]\nlogin\nlogin2\n"\
578 "sleep/<time>\nmessage/topic\n</pre></html>"
580 elif args
and args
[0] == "init":
582 # self.engine.load_dbase(cherrypy.request.app.config)
583 self
.engine
.create_admin()
584 return "Done. User 'admin', password 'admin' created"
586 cherrypy
.response
.status
= HTTPStatus
.FORBIDDEN
.value
587 return self
._format
_out
("Database already initialized")
588 elif args
and args
[0] == "file":
589 return cherrypy
.lib
.static
.serve_file(cherrypy
.tree
.apps
['/osm'].config
["storage"]["path"] + "/" + args
[1],
590 "text/plain", "attachment")
591 elif args
and args
[0] == "file2":
592 f_path
= cherrypy
.tree
.apps
['/osm'].config
["storage"]["path"] + "/" + args
[1]
593 f
= open(f_path
, "r")
594 cherrypy
.response
.headers
["Content-type"] = "text/plain"
597 elif len(args
) == 2 and args
[0] == "db-clear":
598 deleted_info
= self
.engine
.db
.del_list(args
[1], kwargs
)
599 return "{} {} deleted\n".format(deleted_info
["deleted"], args
[1])
600 elif len(args
) and args
[0] == "fs-clear":
604 folders
= self
.engine
.fs
.dir_ls(".")
605 for folder
in folders
:
606 self
.engine
.fs
.file_delete(folder
)
607 return ",".join(folders
) + " folders deleted\n"
608 elif args
and args
[0] == "login":
609 if not cherrypy
.request
.headers
.get("Authorization"):
610 cherrypy
.response
.headers
["WWW-Authenticate"] = 'Basic realm="Access to OSM site", charset="UTF-8"'
611 cherrypy
.response
.status
= HTTPStatus
.UNAUTHORIZED
.value
612 elif args
and args
[0] == "login2":
613 if not cherrypy
.request
.headers
.get("Authorization"):
614 cherrypy
.response
.headers
["WWW-Authenticate"] = 'Bearer realm="Access to OSM site"'
615 cherrypy
.response
.status
= HTTPStatus
.UNAUTHORIZED
.value
616 elif args
and args
[0] == "sleep":
619 sleep_time
= int(args
[1])
621 cherrypy
.response
.status
= HTTPStatus
.FORBIDDEN
.value
622 return self
._format
_out
("Database already initialized")
623 thread_info
= cherrypy
.thread_data
625 time
.sleep(sleep_time
)
627 elif len(args
) >= 2 and args
[0] == "message":
629 return_text
= "<html><pre>{} ->\n".format(main_topic
)
631 if cherrypy
.request
.method
== 'POST':
632 to_send
= yaml
.load(cherrypy
.request
.body
)
633 for k
, v
in to_send
.items():
634 self
.engine
.msg
.write(main_topic
, k
, v
)
635 return_text
+= " {}: {}\n".format(k
, v
)
636 elif cherrypy
.request
.method
== 'GET':
637 for k
, v
in kwargs
.items():
638 self
.engine
.msg
.write(main_topic
, k
, yaml
.load(v
))
639 return_text
+= " {}: {}\n".format(k
, yaml
.load(v
))
640 except Exception as e
:
641 return_text
+= "Error: " + str(e
)
642 return_text
+= "</pre></html>\n"
646 "<html><pre>\nheaders:\n args: {}\n".format(args
) +
647 " kwargs: {}\n".format(kwargs
) +
648 " headers: {}\n".format(cherrypy
.request
.headers
) +
649 " path_info: {}\n".format(cherrypy
.request
.path_info
) +
650 " query_string: {}\n".format(cherrypy
.request
.query_string
) +
651 " session: {}\n".format(cherrypy
.session
) +
652 " cookie: {}\n".format(cherrypy
.request
.cookie
) +
653 " method: {}\n".format(cherrypy
.request
.method
) +
654 " session: {}\n".format(cherrypy
.session
.get('fieldname')) +
656 return_text
+= " length: {}\n".format(cherrypy
.request
.body
.length
)
657 if cherrypy
.request
.body
.length
:
658 return_text
+= " content: {}\n".format(
659 str(cherrypy
.request
.body
.read(int(cherrypy
.request
.headers
.get('Content-Length', 0)))))
661 return_text
+= "thread: {}\n".format(thread_info
)
662 return_text
+= "</pre></html>"
665 def _check_valid_url_method(self
, method
, *args
):
667 raise NbiException("URL must contain at least 'main_topic/version/topic'", HTTPStatus
.METHOD_NOT_ALLOWED
)
669 reference
= self
.valid_methods
673 if not isinstance(reference
, dict):
674 raise NbiException("URL contains unexpected extra items '{}'".format(arg
),
675 HTTPStatus
.METHOD_NOT_ALLOWED
)
678 reference
= reference
[arg
]
679 elif "<ID>" in reference
:
680 reference
= reference
["<ID>"]
681 elif "*" in reference
:
682 reference
= reference
["*"]
685 raise NbiException("Unexpected URL item {}".format(arg
), HTTPStatus
.METHOD_NOT_ALLOWED
)
686 if "TODO" in reference
and method
in reference
["TODO"]:
687 raise NbiException("Method {} not supported yet for this URL".format(method
), HTTPStatus
.NOT_IMPLEMENTED
)
688 elif "METHODS" in reference
and method
not in reference
["METHODS"]:
689 raise NbiException("Method {} not supported for this URL".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
693 def _set_location_header(main_topic
, version
, topic
, id):
695 Insert response header Location with the URL of created item base on URL params
702 # Use cherrypy.request.base for absoluted path and make use of request.header HOST just in case behind aNAT
703 cherrypy
.response
.headers
["Location"] = "/osm/{}/{}/{}/{}".format(main_topic
, version
, topic
, id)
707 def _manage_admin_query(session
, kwargs
, method
, _id
):
709 Processes the administrator query inputs (if any) of FORCE, ADMIN, PUBLIC, SET_PROJECT
710 Check that users has rights to use them and returs the admin_query
711 :param session: session rights obtained by token
712 :param kwargs: query string input.
713 :param method: http method: GET, POSST, PUT, ...
715 :return: admin_query dictionary with keys:
716 public: True, False or None
718 project_id: tuple with projects used for accessing an element
719 set_project: tuple with projects that a created element will belong to
720 method: show, list, delete, write
722 admin_query
= {"force": False, "project_id": (session
["project_id"], ), "username": session
["username"],
723 "admin": session
["admin"], "public": None}
726 if "FORCE" in kwargs
:
727 if kwargs
["FORCE"].lower() != "false": # if None or True set force to True
728 admin_query
["force"] = True
731 if "PUBLIC" in kwargs
:
732 if kwargs
["PUBLIC"].lower() != "false": # if None or True set public to True
733 admin_query
["public"] = True
735 admin_query
["public"] = False
738 if "ADMIN" in kwargs
:
739 behave_as
= kwargs
.pop("ADMIN")
740 if behave_as
.lower() != "false":
741 if not session
["admin"]:
742 raise NbiException("Only admin projects can use 'ADMIN' query string", HTTPStatus
.UNAUTHORIZED
)
743 if not behave_as
or behave_as
.lower() == "true": # convert True, None to empty list
744 admin_query
["project_id"] = ()
745 elif isinstance(behave_as
, (list, tuple)):
746 admin_query
["project_id"] = behave_as
747 else: # isinstance(behave_as, str)
748 admin_query
["project_id"] = (behave_as
, )
749 if "SET_PROJECT" in kwargs
:
750 set_project
= kwargs
.pop("SET_PROJECT")
752 admin_query
["set_project"] = list(admin_query
["project_id"])
754 if isinstance(set_project
, str):
755 set_project
= (set_project
, )
756 if admin_query
["project_id"]:
757 for p
in set_project
:
758 if p
not in admin_query
["project_id"]:
759 raise NbiException("Unauthorized for 'SET_PROJECT={p}'. Try with 'ADMIN=True' or "
760 "'ADMIN='{p}'".format(p
=p
), HTTPStatus
.UNAUTHORIZED
)
761 admin_query
["set_project"] = set_project
764 # if "PROJECT_READ" in kwargs:
765 # admin_query["project"] = kwargs.pop("project")
766 # if admin_query["project"] == session["project_id"]:
769 admin_query
["method"] = "show"
771 admin_query
["method"] = "list"
772 elif method
== "DELETE":
773 admin_query
["method"] = "delete"
775 admin_query
["method"] = "write"
779 def default(self
, main_topic
=None, version
=None, topic
=None, _id
=None, item
=None, *args
, **kwargs
):
788 if not main_topic
or not version
or not topic
:
789 raise NbiException("URL must contain at least 'main_topic/version/topic'",
790 HTTPStatus
.METHOD_NOT_ALLOWED
)
791 if main_topic
not in ("admin", "vnfpkgm", "nsd", "nslcm", "pdu", "nst", "nsilcm", "nspm"):
792 raise NbiException("URL main_topic '{}' not supported".format(main_topic
),
793 HTTPStatus
.METHOD_NOT_ALLOWED
)
795 raise NbiException("URL version '{}' not supported".format(version
), HTTPStatus
.METHOD_NOT_ALLOWED
)
797 if kwargs
and "METHOD" in kwargs
and kwargs
["METHOD"] in ("PUT", "POST", "DELETE", "GET", "PATCH"):
798 method
= kwargs
.pop("METHOD")
800 method
= cherrypy
.request
.method
802 self
._check
_valid
_url
_method
(method
, main_topic
, version
, topic
, _id
, item
, *args
)
804 if main_topic
== "admin" and topic
== "tokens":
805 return self
.token(method
, _id
, kwargs
)
807 # self.engine.load_dbase(cherrypy.request.app.config)
808 session
= self
.authenticator
.authorize()
809 session
= self
._manage
_admin
_query
(session
, kwargs
, method
, _id
)
810 indata
= self
._format
_in
(kwargs
)
812 if topic
== "subscriptions":
813 engine_topic
= main_topic
+ "_" + topic
814 if item
and topic
!= "pm_jobs":
817 if main_topic
== "nsd":
818 engine_topic
= "nsds"
819 elif main_topic
== "vnfpkgm":
820 engine_topic
= "vnfds"
821 elif main_topic
== "nslcm":
822 engine_topic
= "nsrs"
823 if topic
== "ns_lcm_op_occs":
824 engine_topic
= "nslcmops"
825 if topic
== "vnfrs" or topic
== "vnf_instances":
826 engine_topic
= "vnfrs"
827 elif main_topic
== "nst":
828 engine_topic
= "nsts"
829 elif main_topic
== "nsilcm":
830 engine_topic
= "nsis"
831 if topic
== "nsi_lcm_op_occs":
832 engine_topic
= "nsilcmops"
833 elif main_topic
== "pdu":
834 engine_topic
= "pdus"
835 if engine_topic
== "vims": # TODO this is for backward compatibility, it will be removed in the future
836 engine_topic
= "vim_accounts"
839 if item
in ("nsd_content", "package_content", "artifacts", "vnfd", "nsd", "nst", "nst_content"):
840 if item
in ("vnfd", "nsd", "nst"):
844 elif item
== "artifacts":
848 file, _format
= self
.engine
.get_file(session
, engine_topic
, _id
, path
,
849 cherrypy
.request
.headers
.get("Accept"))
852 outdata
= self
.engine
.get_item_list(session
, engine_topic
, kwargs
)
854 if item
== "reports":
855 # TODO check that project_id (_id in this context) has permissions
857 outdata
= self
.engine
.get_item(session
, engine_topic
, _id
)
858 elif method
== "POST":
859 if topic
in ("ns_descriptors_content", "vnf_packages_content", "netslice_templates_content"):
860 _id
= cherrypy
.request
.headers
.get("Transaction-Id")
862 _id
= self
.engine
.new_item(rollback
, session
, engine_topic
, {}, None, cherrypy
.request
.headers
)
863 completed
= self
.engine
.upload_content(session
, engine_topic
, _id
, indata
, kwargs
,
864 cherrypy
.request
.headers
)
866 self
._set
_location
_header
(main_topic
, version
, topic
, _id
)
868 cherrypy
.response
.headers
["Transaction-Id"] = _id
869 outdata
= {"id": _id
}
870 elif topic
== "ns_instances_content":
872 _id
= self
.engine
.new_item(rollback
, session
, engine_topic
, indata
, kwargs
)
874 indata
["lcmOperationType"] = "instantiate"
875 indata
["nsInstanceId"] = _id
876 self
.engine
.new_item(rollback
, session
, "nslcmops", indata
, None)
877 self
._set
_location
_header
(main_topic
, version
, topic
, _id
)
878 outdata
= {"id": _id
}
879 elif topic
== "ns_instances" and item
:
880 indata
["lcmOperationType"] = item
881 indata
["nsInstanceId"] = _id
882 _id
= self
.engine
.new_item(rollback
, session
, "nslcmops", indata
, kwargs
)
883 self
._set
_location
_header
(main_topic
, version
, "ns_lcm_op_occs", _id
)
884 outdata
= {"id": _id
}
885 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
886 elif topic
== "netslice_instances_content":
887 # creates NetSlice_Instance_record (NSIR)
888 _id
= self
.engine
.new_item(rollback
, session
, engine_topic
, indata
, kwargs
)
889 self
._set
_location
_header
(main_topic
, version
, topic
, _id
)
890 indata
["lcmOperationType"] = "instantiate"
891 indata
["nsiInstanceId"] = _id
892 self
.engine
.new_item(rollback
, session
, "nsilcmops", indata
, kwargs
)
893 outdata
= {"id": _id
}
895 elif topic
== "netslice_instances" and item
:
896 indata
["lcmOperationType"] = item
897 indata
["nsiInstanceId"] = _id
898 _id
= self
.engine
.new_item(rollback
, session
, "nsilcmops", indata
, kwargs
)
899 self
._set
_location
_header
(main_topic
, version
, "nsi_lcm_op_occs", _id
)
900 outdata
= {"id": _id
}
901 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
903 _id
= self
.engine
.new_item(rollback
, session
, engine_topic
, indata
, kwargs
,
904 cherrypy
.request
.headers
)
905 self
._set
_location
_header
(main_topic
, version
, topic
, _id
)
906 outdata
= {"id": _id
}
907 # TODO form NsdInfo when topic in ("ns_descriptors", "vnf_packages")
908 cherrypy
.response
.status
= HTTPStatus
.CREATED
.value
910 elif method
== "DELETE":
912 outdata
= self
.engine
.del_item_list(session
, engine_topic
, kwargs
)
913 cherrypy
.response
.status
= HTTPStatus
.OK
.value
914 else: # len(args) > 1
915 delete_in_process
= False
916 if topic
== "ns_instances_content" and not session
["force"]:
918 "lcmOperationType": "terminate",
922 opp_id
= self
.engine
.new_item(rollback
, session
, "nslcmops", nslcmop_desc
, None)
924 delete_in_process
= True
925 outdata
= {"_id": opp_id
}
926 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
927 elif topic
== "netslice_instances_content" and not session
["force"]:
929 "lcmOperationType": "terminate",
930 "nsiInstanceId": _id
,
933 opp_id
= self
.engine
.new_item(rollback
, session
, "nsilcmops", nsilcmop_desc
, None)
935 delete_in_process
= True
936 outdata
= {"_id": opp_id
}
937 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
938 if not delete_in_process
:
939 self
.engine
.del_item(session
, engine_topic
, _id
)
940 cherrypy
.response
.status
= HTTPStatus
.NO_CONTENT
.value
941 if engine_topic
in ("vim_accounts", "wim_accounts", "sdns"):
942 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
944 elif method
in ("PUT", "PATCH"):
946 if not indata
and not kwargs
and not session
.get("set_project"):
947 raise NbiException("Nothing to update. Provide payload and/or query string",
948 HTTPStatus
.BAD_REQUEST
)
949 if item
in ("nsd_content", "package_content", "nst_content") and method
== "PUT":
950 completed
= self
.engine
.upload_content(session
, engine_topic
, _id
, indata
, kwargs
,
951 cherrypy
.request
.headers
)
953 cherrypy
.response
.headers
["Transaction-Id"] = id
955 self
.engine
.edit_item(session
, engine_topic
, _id
, indata
, kwargs
)
956 cherrypy
.response
.status
= HTTPStatus
.NO_CONTENT
.value
958 raise NbiException("Method {} not allowed".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
959 return self
._format
_out
(outdata
, session
, _format
)
960 except Exception as e
:
961 if isinstance(e
, (NbiException
, EngineException
, DbException
, FsException
, MsgException
, AuthException
,
963 http_code_value
= cherrypy
.response
.status
= e
.http_code
.value
964 http_code_name
= e
.http_code
.name
965 cherrypy
.log("Exception {}".format(e
))
967 http_code_value
= cherrypy
.response
.status
= HTTPStatus
.BAD_REQUEST
.value
# INTERNAL_SERVER_ERROR
968 cherrypy
.log("CRITICAL: Exception {}".format(e
), traceback
=True)
969 http_code_name
= HTTPStatus
.BAD_REQUEST
.name
970 if hasattr(outdata
, "close"): # is an open file
974 for rollback_item
in rollback
:
976 if rollback_item
.get("operation") == "set":
977 self
.engine
.db
.set_one(rollback_item
["topic"], {"_id": rollback_item
["_id"]},
978 rollback_item
["content"], fail_on_empty
=False)
980 self
.engine
.db
.del_one(rollback_item
["topic"], {"_id": rollback_item
["_id"]},
982 except Exception as e2
:
983 rollback_error_text
= "Rollback Exception {}: {}".format(rollback_item
, e2
)
984 cherrypy
.log(rollback_error_text
)
985 error_text
+= ". " + rollback_error_text
986 # if isinstance(e, MsgException):
987 # error_text = "{} has been '{}' but other modules cannot be informed because an error on bus".format(
988 # engine_topic[:-1], method, error_text)
990 "code": http_code_name
,
991 "status": http_code_value
,
992 "detail": error_text
,
994 return self
._format
_out
(problem_details
, session
)
995 # raise cherrypy.HTTPError(e.http_code.value, str(e))
998 def _start_service():
1000 Callback function called when cherrypy.engine starts
1001 Override configuration with env variables
1002 Set database, storage, message configuration
1003 Init database with admin/admin user password
1006 global subscription_thread
1007 cherrypy
.log
.error("Starting osm_nbi")
1008 # update general cherrypy configuration
1011 engine_config
= cherrypy
.tree
.apps
['/osm'].config
1012 for k
, v
in environ
.items():
1013 if not k
.startswith("OSMNBI_"):
1015 k1
, _
, k2
= k
[7:].lower().partition("_")
1019 # update static configuration
1020 if k
== 'OSMNBI_STATIC_DIR':
1021 engine_config
["/static"]['tools.staticdir.dir'] = v
1022 engine_config
["/static"]['tools.staticdir.on'] = True
1023 elif k
== 'OSMNBI_SOCKET_PORT' or k
== 'OSMNBI_SERVER_PORT':
1024 update_dict
['server.socket_port'] = int(v
)
1025 elif k
== 'OSMNBI_SOCKET_HOST' or k
== 'OSMNBI_SERVER_HOST':
1026 update_dict
['server.socket_host'] = v
1027 elif k1
in ("server", "test", "auth", "log"):
1028 update_dict
[k1
+ '.' + k2
] = v
1029 elif k1
in ("message", "database", "storage", "authentication"):
1030 # k2 = k2.replace('_', '.')
1031 if k2
in ("port", "db_port"):
1032 engine_config
[k1
][k2
] = int(v
)
1034 engine_config
[k1
][k2
] = v
1036 except ValueError as e
:
1037 cherrypy
.log
.error("Ignoring environ '{}': " + str(e
))
1038 except Exception as e
:
1039 cherrypy
.log
.warn("skipping environ '{}' on exception '{}'".format(k
, e
))
1042 cherrypy
.config
.update(update_dict
)
1043 engine_config
["global"].update(update_dict
)
1046 log_format_simple
= "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(message)s"
1047 log_formatter_simple
= logging
.Formatter(log_format_simple
, datefmt
='%Y-%m-%dT%H:%M:%S')
1048 logger_server
= logging
.getLogger("cherrypy.error")
1049 logger_access
= logging
.getLogger("cherrypy.access")
1050 logger_cherry
= logging
.getLogger("cherrypy")
1051 logger_nbi
= logging
.getLogger("nbi")
1053 if "log.file" in engine_config
["global"]:
1054 file_handler
= logging
.handlers
.RotatingFileHandler(engine_config
["global"]["log.file"],
1055 maxBytes
=100e6
, backupCount
=9, delay
=0)
1056 file_handler
.setFormatter(log_formatter_simple
)
1057 logger_cherry
.addHandler(file_handler
)
1058 logger_nbi
.addHandler(file_handler
)
1059 # log always to standard output
1060 for format_
, logger
in {"nbi.server %(filename)s:%(lineno)s": logger_server
,
1061 "nbi.access %(filename)s:%(lineno)s": logger_access
,
1062 "%(name)s %(filename)s:%(lineno)s": logger_nbi
1064 log_format_cherry
= "%(asctime)s %(levelname)s {} %(message)s".format(format_
)
1065 log_formatter_cherry
= logging
.Formatter(log_format_cherry
, datefmt
='%Y-%m-%dT%H:%M:%S')
1066 str_handler
= logging
.StreamHandler()
1067 str_handler
.setFormatter(log_formatter_cherry
)
1068 logger
.addHandler(str_handler
)
1070 if engine_config
["global"].get("log.level"):
1071 logger_cherry
.setLevel(engine_config
["global"]["log.level"])
1072 logger_nbi
.setLevel(engine_config
["global"]["log.level"])
1074 # logging other modules
1075 for k1
, logname
in {"message": "nbi.msg", "database": "nbi.db", "storage": "nbi.fs"}.items():
1076 engine_config
[k1
]["logger_name"] = logname
1077 logger_module
= logging
.getLogger(logname
)
1078 if "logfile" in engine_config
[k1
]:
1079 file_handler
= logging
.handlers
.RotatingFileHandler(engine_config
[k1
]["logfile"],
1080 maxBytes
=100e6
, backupCount
=9, delay
=0)
1081 file_handler
.setFormatter(log_formatter_simple
)
1082 logger_module
.addHandler(file_handler
)
1083 if "loglevel" in engine_config
[k1
]:
1084 logger_module
.setLevel(engine_config
[k1
]["loglevel"])
1085 # TODO add more entries, e.g.: storage
1086 cherrypy
.tree
.apps
['/osm'].root
.engine
.start(engine_config
)
1087 cherrypy
.tree
.apps
['/osm'].root
.authenticator
.start(engine_config
)
1088 cherrypy
.tree
.apps
['/osm'].root
.engine
.init_db(target_version
=database_version
)
1089 cherrypy
.tree
.apps
['/osm'].root
.authenticator
.init_db(target_version
=auth_database_version
)
1091 # start subscriptions thread:
1092 subscription_thread
= SubscriptionThread(config
=engine_config
, engine
=nbi_server
.engine
)
1093 subscription_thread
.start()
1094 # Do not capture except SubscriptionException
1096 # load and print version. Ignore possible errors, e.g. file not found
1098 with
open("{}/version".format(engine_config
["/static"]['tools.staticdir.dir'])) as version_file
:
1099 version_data
= version_file
.read()
1100 cherrypy
.log
.error("Starting OSM NBI Version: {}".format(version_data
.replace("\n", " ")))
1105 def _stop_service():
1107 Callback function called when cherrypy.engine stops
1108 TODO: Ending database connections.
1110 global subscription_thread
1111 if subscription_thread
:
1112 subscription_thread
.terminate()
1113 subscription_thread
= None
1114 cherrypy
.tree
.apps
['/osm'].root
.engine
.stop()
1115 cherrypy
.log
.error("Stopping osm_nbi")
1118 def nbi(config_file
):
1122 # #'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
1123 # 'tools.sessions.on': True,
1124 # 'tools.response_headers.on': True,
1125 # # 'tools.response_headers.headers': [('Content-Type', 'text/plain')],
1128 # cherrypy.Server.ssl_module = 'builtin'
1129 # cherrypy.Server.ssl_certificate = "http/cert.pem"
1130 # cherrypy.Server.ssl_private_key = "http/privkey.pem"
1131 # cherrypy.Server.thread_pool = 10
1132 # cherrypy.config.update({'Server.socket_port': config["port"], 'Server.socket_host': config["host"]})
1134 # cherrypy.config.update({'tools.auth_basic.on': True,
1135 # 'tools.auth_basic.realm': 'localhost',
1136 # 'tools.auth_basic.checkpassword': validate_password})
1137 nbi_server
= Server()
1138 cherrypy
.engine
.subscribe('start', _start_service
)
1139 cherrypy
.engine
.subscribe('stop', _stop_service
)
1140 cherrypy
.quickstart(nbi_server
, '/osm', config_file
)
1144 print("""Usage: {} [options]
1145 -c|--config [configuration_file]: loads the configuration file (default: ./nbi.cfg)
1146 -h|--help: shows this help
1147 """.format(sys
.argv
[0]))
1148 # --log-socket-host HOST: send logs to this host")
1149 # --log-socket-port PORT: send logs using this port (default: 9022)")
1152 if __name__
== '__main__':
1154 # load parameters and configuration
1155 opts
, args
= getopt
.getopt(sys
.argv
[1:], "hvc:", ["config=", "help"])
1156 # TODO add "log-socket-host=", "log-socket-port=", "log-file="
1159 if o
in ("-h", "--help"):
1162 elif o
in ("-c", "--config"):
1164 # elif o == "--log-socket-port":
1165 # log_socket_port = a
1166 # elif o == "--log-socket-host":
1167 # log_socket_host = a
1168 # elif o == "--log-file":
1171 assert False, "Unhandled option"
1173 if not path
.isfile(config_file
):
1174 print("configuration file '{}' that not exist".format(config_file
), file=sys
.stderr
)
1177 for config_file
in (__file__
[:__file__
.rfind(".")] + ".cfg", "./nbi.cfg", "/etc/osm/nbi.cfg"):
1178 if path
.isfile(config_file
):
1181 print("No configuration file 'nbi.cfg' found neither at local folder nor at /etc/osm/", file=sys
.stderr
)
1184 except getopt
.GetoptError
as e
:
1185 print(str(e
), file=sys
.stderr
)