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 "<ID>": {"METHODS": ("GET", "DELETE", "PUT")}
222 "roles": {"METHODS": ("GET", "POST"),
223 "<ID>": {"METHODS": ("GET", "POST", "DELETE", "PUT")}
225 "vims": {"METHODS": ("GET", "POST"),
226 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
228 "vim_accounts": {"METHODS": ("GET", "POST"),
229 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
231 "wim_accounts": {"METHODS": ("GET", "POST"),
232 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
234 "sdns": {"METHODS": ("GET", "POST"),
235 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
241 "pdu_descriptors": {"METHODS": ("GET", "POST"),
242 "<ID>": {"METHODS": ("GET", "POST", "DELETE", "PATCH", "PUT")}
248 "ns_descriptors_content": {"METHODS": ("GET", "POST"),
249 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
251 "ns_descriptors": {"METHODS": ("GET", "POST"),
252 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"),
253 "nsd_content": {"METHODS": ("GET", "PUT")},
254 "nsd": {"METHODS": "GET"}, # descriptor inside package
255 "artifacts": {"*": {"METHODS": "GET"}}
258 "pnf_descriptors": {"TODO": ("GET", "POST"),
259 "<ID>": {"TODO": ("GET", "DELETE", "PATCH"),
260 "pnfd_content": {"TODO": ("GET", "PUT")}
263 "subscriptions": {"TODO": ("GET", "POST"),
264 "<ID>": {"TODO": ("GET", "DELETE")}
270 "vnf_packages_content": {"METHODS": ("GET", "POST"),
271 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
273 "vnf_packages": {"METHODS": ("GET", "POST"),
274 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"), # GET: vnfPkgInfo
275 "package_content": {"METHODS": ("GET", "PUT"), # package
276 "upload_from_uri": {"TODO": "POST"}
278 "vnfd": {"METHODS": "GET"}, # descriptor inside package
279 "artifacts": {"*": {"METHODS": "GET"}}
282 "subscriptions": {"TODO": ("GET", "POST"),
283 "<ID>": {"TODO": ("GET", "DELETE")}
289 "ns_instances_content": {"METHODS": ("GET", "POST"),
290 "<ID>": {"METHODS": ("GET", "DELETE")}
292 "ns_instances": {"METHODS": ("GET", "POST"),
293 "<ID>": {"METHODS": ("GET", "DELETE"),
294 "scale": {"METHODS": "POST"},
295 "terminate": {"METHODS": "POST"},
296 "instantiate": {"METHODS": "POST"},
297 "action": {"METHODS": "POST"},
300 "ns_lcm_op_occs": {"METHODS": "GET",
301 "<ID>": {"METHODS": "GET"},
303 "vnfrs": {"METHODS": ("GET"),
304 "<ID>": {"METHODS": ("GET")}
306 "vnf_instances": {"METHODS": ("GET"),
307 "<ID>": {"METHODS": ("GET")}
313 "netslice_templates_content": {"METHODS": ("GET", "POST"),
314 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
316 "netslice_templates": {"METHODS": ("GET", "POST"),
317 "<ID>": {"METHODS": ("GET", "DELETE"), "TODO": "PATCH",
318 "nst_content": {"METHODS": ("GET", "PUT")},
319 "nst": {"METHODS": "GET"}, # descriptor inside package
320 "artifacts": {"*": {"METHODS": "GET"}}
323 "subscriptions": {"TODO": ("GET", "POST"),
324 "<ID>": {"TODO": ("GET", "DELETE")}
330 "netslice_instances_content": {"METHODS": ("GET", "POST"),
331 "<ID>": {"METHODS": ("GET", "DELETE")}
333 "netslice_instances": {"METHODS": ("GET", "POST"),
334 "<ID>": {"METHODS": ("GET", "DELETE"),
335 "terminate": {"METHODS": "POST"},
336 "instantiate": {"METHODS": "POST"},
337 "action": {"METHODS": "POST"},
340 "nsi_lcm_op_occs": {"METHODS": "GET",
341 "<ID>": {"METHODS": "GET"},
350 "<ID>": {"METHODS": ("GET")}
358 def _format_in(self
, kwargs
):
361 if cherrypy
.request
.body
.length
:
362 error_text
= "Invalid input format "
364 if "Content-Type" in cherrypy
.request
.headers
:
365 if "application/json" in cherrypy
.request
.headers
["Content-Type"]:
366 error_text
= "Invalid json format "
367 indata
= json
.load(self
.reader(cherrypy
.request
.body
))
368 cherrypy
.request
.headers
.pop("Content-File-MD5", None)
369 elif "application/yaml" in cherrypy
.request
.headers
["Content-Type"]:
370 error_text
= "Invalid yaml format "
371 indata
= yaml
.load(cherrypy
.request
.body
)
372 cherrypy
.request
.headers
.pop("Content-File-MD5", None)
373 elif "application/binary" in cherrypy
.request
.headers
["Content-Type"] or \
374 "application/gzip" in cherrypy
.request
.headers
["Content-Type"] or \
375 "application/zip" in cherrypy
.request
.headers
["Content-Type"] or \
376 "text/plain" in cherrypy
.request
.headers
["Content-Type"]:
377 indata
= cherrypy
.request
.body
# .read()
378 elif "multipart/form-data" in cherrypy
.request
.headers
["Content-Type"]:
379 if "descriptor_file" in kwargs
:
380 filecontent
= kwargs
.pop("descriptor_file")
381 if not filecontent
.file:
382 raise NbiException("empty file or content", HTTPStatus
.BAD_REQUEST
)
383 indata
= filecontent
.file # .read()
384 if filecontent
.content_type
.value
:
385 cherrypy
.request
.headers
["Content-Type"] = filecontent
.content_type
.value
387 # raise cherrypy.HTTPError(HTTPStatus.Not_Acceptable,
388 # "Only 'Content-Type' of type 'application/json' or
389 # 'application/yaml' for input format are available")
390 error_text
= "Invalid yaml format "
391 indata
= yaml
.load(cherrypy
.request
.body
)
392 cherrypy
.request
.headers
.pop("Content-File-MD5", None)
394 error_text
= "Invalid yaml format "
395 indata
= yaml
.load(cherrypy
.request
.body
)
396 cherrypy
.request
.headers
.pop("Content-File-MD5", None)
401 if cherrypy
.request
.headers
.get("Query-String-Format") == "yaml":
404 for k
, v
in kwargs
.items():
405 if isinstance(v
, str):
410 kwargs
[k
] = yaml
.load(v
)
413 elif k
.endswith(".gt") or k
.endswith(".lt") or k
.endswith(".gte") or k
.endswith(".lte"):
421 elif v
.find(",") > 0:
422 kwargs
[k
] = v
.split(",")
423 elif isinstance(v
, (list, tuple)):
424 for index
in range(0, len(v
)):
429 v
[index
] = yaml
.load(v
[index
])
434 except (ValueError, yaml
.YAMLError
) as exc
:
435 raise NbiException(error_text
+ str(exc
), HTTPStatus
.BAD_REQUEST
)
436 except KeyError as exc
:
437 raise NbiException("Query string error: " + str(exc
), HTTPStatus
.BAD_REQUEST
)
438 except Exception as exc
:
439 raise NbiException(error_text
+ str(exc
), HTTPStatus
.BAD_REQUEST
)
442 def _format_out(data
, session
=None, _format
=None):
444 return string of dictionary data according to requested json, yaml, xml. By default json
445 :param data: response to be sent. Can be a dict, text or file
447 :param _format: The format to be set as Content-Type ir data is a file
450 accept
= cherrypy
.request
.headers
.get("Accept")
452 if accept
and "text/html" in accept
:
453 return html
.format(data
, cherrypy
.request
, cherrypy
.response
, session
)
454 # cherrypy.response.status = HTTPStatus.NO_CONTENT.value
456 elif hasattr(data
, "read"): # file object
458 cherrypy
.response
.headers
["Content-Type"] = _format
459 elif "b" in data
.mode
: # binariy asssumig zip
460 cherrypy
.response
.headers
["Content-Type"] = 'application/zip'
462 cherrypy
.response
.headers
["Content-Type"] = 'text/plain'
463 # TODO check that cherrypy close file. If not implement pending things to close per thread next
466 if "application/json" in accept
:
467 cherrypy
.response
.headers
["Content-Type"] = 'application/json; charset=utf-8'
468 a
= json
.dumps(data
, indent
=4) + "\n"
469 return a
.encode("utf8")
470 elif "text/html" in accept
:
471 return html
.format(data
, cherrypy
.request
, cherrypy
.response
, session
)
473 elif "application/yaml" in accept
or "*/*" in accept
or "text/plain" in accept
:
475 # if there is not any valid accept, raise an error. But if response is already an error, format in yaml
476 elif cherrypy
.response
.status
>= 400:
477 raise cherrypy
.HTTPError(HTTPStatus
.NOT_ACCEPTABLE
.value
,
478 "Only 'Accept' of type 'application/json' or 'application/yaml' "
479 "for output format are available")
480 cherrypy
.response
.headers
["Content-Type"] = 'application/yaml'
481 return yaml
.safe_dump(data
, explicit_start
=True, indent
=4, default_flow_style
=False, tags
=False,
482 encoding
='utf-8', allow_unicode
=True) # , canonical=True, default_style='"'
485 def index(self
, *args
, **kwargs
):
488 if cherrypy
.request
.method
== "GET":
489 session
= self
.authenticator
.authorize()
490 outdata
= "Index page"
492 raise cherrypy
.HTTPError(HTTPStatus
.METHOD_NOT_ALLOWED
.value
,
493 "Method {} not allowed for tokens".format(cherrypy
.request
.method
))
495 return self
._format
_out
(outdata
, session
)
497 except (EngineException
, AuthException
) as e
:
498 cherrypy
.log("index Exception {}".format(e
))
499 cherrypy
.response
.status
= e
.http_code
.value
500 return self
._format
_out
("Welcome to OSM!", session
)
503 def version(self
, *args
, **kwargs
):
504 # TODO consider to remove and provide version using the static version file
505 global __version__
, version_date
507 if cherrypy
.request
.method
!= "GET":
508 raise NbiException("Only method GET is allowed", HTTPStatus
.METHOD_NOT_ALLOWED
)
510 raise NbiException("Invalid URL or query string for version", HTTPStatus
.METHOD_NOT_ALLOWED
)
511 return __version__
+ " " + version_date
512 except NbiException
as 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
, None)
522 def token(self
, method
, token_id
=None, kwargs
=None):
524 # self.engine.load_dbase(cherrypy.request.app.config)
525 indata
= self
._format
_in
(kwargs
)
526 if not isinstance(indata
, dict):
527 raise NbiException("Expected application/yaml or application/json Content-Type", HTTPStatus
.BAD_REQUEST
)
530 session
= self
.authenticator
.authorize()
532 outdata
= self
.authenticator
.get_token(session
, token_id
)
534 outdata
= self
.authenticator
.get_token_list(session
)
535 elif method
== "POST":
537 session
= self
.authenticator
.authorize()
541 indata
.update(kwargs
)
542 outdata
= self
.authenticator
.new_token(session
, indata
, cherrypy
.request
.remote
)
544 cherrypy
.session
['Authorization'] = outdata
["_id"]
545 self
._set
_location
_header
("admin", "v1", "tokens", outdata
["_id"])
546 # cherrypy.response.cookie["Authorization"] = outdata["id"]
547 # cherrypy.response.cookie["Authorization"]['expires'] = 3600
548 elif method
== "DELETE":
549 if not token_id
and "id" in kwargs
:
550 token_id
= kwargs
["id"]
552 session
= self
.authenticator
.authorize()
553 token_id
= session
["_id"]
554 outdata
= self
.authenticator
.del_token(token_id
)
556 cherrypy
.session
['Authorization'] = "logout"
557 # cherrypy.response.cookie["Authorization"] = token_id
558 # cherrypy.response.cookie["Authorization"]['expires'] = 0
560 raise NbiException("Method {} not allowed for token".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
561 return self
._format
_out
(outdata
, session
)
562 except (NbiException
, EngineException
, DbException
, AuthException
) as e
:
563 cherrypy
.log("tokens Exception {}".format(e
))
564 cherrypy
.response
.status
= e
.http_code
.value
566 "code": e
.http_code
.name
,
567 "status": e
.http_code
.value
,
570 return self
._format
_out
(problem_details
, session
)
573 def test(self
, *args
, **kwargs
):
575 if args
and args
[0] == "help":
576 return "<html><pre>\ninit\nfile/<name> download file\ndb-clear/table\nfs-clear[/folder]\nlogin\nlogin2\n"\
577 "sleep/<time>\nmessage/topic\n</pre></html>"
579 elif args
and args
[0] == "init":
581 # self.engine.load_dbase(cherrypy.request.app.config)
582 self
.engine
.create_admin()
583 return "Done. User 'admin', password 'admin' created"
585 cherrypy
.response
.status
= HTTPStatus
.FORBIDDEN
.value
586 return self
._format
_out
("Database already initialized")
587 elif args
and args
[0] == "file":
588 return cherrypy
.lib
.static
.serve_file(cherrypy
.tree
.apps
['/osm'].config
["storage"]["path"] + "/" + args
[1],
589 "text/plain", "attachment")
590 elif args
and args
[0] == "file2":
591 f_path
= cherrypy
.tree
.apps
['/osm'].config
["storage"]["path"] + "/" + args
[1]
592 f
= open(f_path
, "r")
593 cherrypy
.response
.headers
["Content-type"] = "text/plain"
596 elif len(args
) == 2 and args
[0] == "db-clear":
597 deleted_info
= self
.engine
.db
.del_list(args
[1], kwargs
)
598 return "{} {} deleted\n".format(deleted_info
["deleted"], args
[1])
599 elif len(args
) and args
[0] == "fs-clear":
603 folders
= self
.engine
.fs
.dir_ls(".")
604 for folder
in folders
:
605 self
.engine
.fs
.file_delete(folder
)
606 return ",".join(folders
) + " folders deleted\n"
607 elif args
and args
[0] == "login":
608 if not cherrypy
.request
.headers
.get("Authorization"):
609 cherrypy
.response
.headers
["WWW-Authenticate"] = 'Basic realm="Access to OSM site", charset="UTF-8"'
610 cherrypy
.response
.status
= HTTPStatus
.UNAUTHORIZED
.value
611 elif args
and args
[0] == "login2":
612 if not cherrypy
.request
.headers
.get("Authorization"):
613 cherrypy
.response
.headers
["WWW-Authenticate"] = 'Bearer realm="Access to OSM site"'
614 cherrypy
.response
.status
= HTTPStatus
.UNAUTHORIZED
.value
615 elif args
and args
[0] == "sleep":
618 sleep_time
= int(args
[1])
620 cherrypy
.response
.status
= HTTPStatus
.FORBIDDEN
.value
621 return self
._format
_out
("Database already initialized")
622 thread_info
= cherrypy
.thread_data
624 time
.sleep(sleep_time
)
626 elif len(args
) >= 2 and args
[0] == "message":
628 return_text
= "<html><pre>{} ->\n".format(main_topic
)
630 if cherrypy
.request
.method
== 'POST':
631 to_send
= yaml
.load(cherrypy
.request
.body
)
632 for k
, v
in to_send
.items():
633 self
.engine
.msg
.write(main_topic
, k
, v
)
634 return_text
+= " {}: {}\n".format(k
, v
)
635 elif cherrypy
.request
.method
== 'GET':
636 for k
, v
in kwargs
.items():
637 self
.engine
.msg
.write(main_topic
, k
, yaml
.load(v
))
638 return_text
+= " {}: {}\n".format(k
, yaml
.load(v
))
639 except Exception as e
:
640 return_text
+= "Error: " + str(e
)
641 return_text
+= "</pre></html>\n"
645 "<html><pre>\nheaders:\n args: {}\n".format(args
) +
646 " kwargs: {}\n".format(kwargs
) +
647 " headers: {}\n".format(cherrypy
.request
.headers
) +
648 " path_info: {}\n".format(cherrypy
.request
.path_info
) +
649 " query_string: {}\n".format(cherrypy
.request
.query_string
) +
650 " session: {}\n".format(cherrypy
.session
) +
651 " cookie: {}\n".format(cherrypy
.request
.cookie
) +
652 " method: {}\n".format(cherrypy
.request
.method
) +
653 " session: {}\n".format(cherrypy
.session
.get('fieldname')) +
655 return_text
+= " length: {}\n".format(cherrypy
.request
.body
.length
)
656 if cherrypy
.request
.body
.length
:
657 return_text
+= " content: {}\n".format(
658 str(cherrypy
.request
.body
.read(int(cherrypy
.request
.headers
.get('Content-Length', 0)))))
660 return_text
+= "thread: {}\n".format(thread_info
)
661 return_text
+= "</pre></html>"
664 def _check_valid_url_method(self
, method
, *args
):
666 raise NbiException("URL must contain at least 'main_topic/version/topic'", HTTPStatus
.METHOD_NOT_ALLOWED
)
668 reference
= self
.valid_methods
672 if not isinstance(reference
, dict):
673 raise NbiException("URL contains unexpected extra items '{}'".format(arg
),
674 HTTPStatus
.METHOD_NOT_ALLOWED
)
677 reference
= reference
[arg
]
678 elif "<ID>" in reference
:
679 reference
= reference
["<ID>"]
680 elif "*" in reference
:
681 reference
= reference
["*"]
684 raise NbiException("Unexpected URL item {}".format(arg
), HTTPStatus
.METHOD_NOT_ALLOWED
)
685 if "TODO" in reference
and method
in reference
["TODO"]:
686 raise NbiException("Method {} not supported yet for this URL".format(method
), HTTPStatus
.NOT_IMPLEMENTED
)
687 elif "METHODS" in reference
and method
not in reference
["METHODS"]:
688 raise NbiException("Method {} not supported for this URL".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
692 def _set_location_header(main_topic
, version
, topic
, id):
694 Insert response header Location with the URL of created item base on URL params
701 # Use cherrypy.request.base for absoluted path and make use of request.header HOST just in case behind aNAT
702 cherrypy
.response
.headers
["Location"] = "/osm/{}/{}/{}/{}".format(main_topic
, version
, topic
, id)
706 def _manage_admin_query(session
, kwargs
, method
, _id
):
708 Processes the administrator query inputs (if any) of FORCE, ADMIN, PUBLIC, SET_PROJECT
709 Check that users has rights to use them and returs the admin_query
710 :param session: session rights obtained by token
711 :param kwargs: query string input.
712 :param method: http method: GET, POSST, PUT, ...
714 :return: admin_query dictionary with keys:
715 public: True, False or None
717 project_id: tuple with projects used for accessing an element
718 set_project: tuple with projects that a created element will belong to
719 method: show, list, delete, write
721 admin_query
= {"force": False, "project_id": (session
["project_id"], ), "username": session
["username"],
722 "admin": session
["admin"], "public": None}
725 if "FORCE" in kwargs
:
726 if kwargs
["FORCE"].lower() != "false": # if None or True set force to True
727 admin_query
["force"] = True
730 if "PUBLIC" in kwargs
:
731 if kwargs
["PUBLIC"].lower() != "false": # if None or True set public to True
732 admin_query
["public"] = True
734 admin_query
["public"] = False
737 if "ADMIN" in kwargs
:
738 behave_as
= kwargs
.pop("ADMIN")
739 if behave_as
.lower() != "false":
740 if not session
["admin"]:
741 raise NbiException("Only admin projects can use 'ADMIN' query string", HTTPStatus
.UNAUTHORIZED
)
742 if not behave_as
or behave_as
.lower() == "true": # convert True, None to empty list
743 admin_query
["project_id"] = ()
744 elif isinstance(behave_as
, (list, tuple)):
745 admin_query
["project_id"] = behave_as
746 else: # isinstance(behave_as, str)
747 admin_query
["project_id"] = (behave_as
, )
748 if "SET_PROJECT" in kwargs
:
749 set_project
= kwargs
.pop("SET_PROJECT")
751 admin_query
["set_project"] = list(admin_query
["project_id"])
753 if isinstance(set_project
, str):
754 set_project
= (set_project
, )
755 if admin_query
["project_id"]:
756 for p
in set_project
:
757 if p
not in admin_query
["project_id"]:
758 raise NbiException("Unauthorized for 'SET_PROJECT={p}'. Try with 'ADMIN=True' or "
759 "'ADMIN='{p}'".format(p
=p
), HTTPStatus
.UNAUTHORIZED
)
760 admin_query
["set_project"] = set_project
763 # if "PROJECT_READ" in kwargs:
764 # admin_query["project"] = kwargs.pop("project")
765 # if admin_query["project"] == session["project_id"]:
768 admin_query
["method"] = "show"
770 admin_query
["method"] = "list"
771 elif method
== "DELETE":
772 admin_query
["method"] = "delete"
774 admin_query
["method"] = "write"
778 def default(self
, main_topic
=None, version
=None, topic
=None, _id
=None, item
=None, *args
, **kwargs
):
787 if not main_topic
or not version
or not topic
:
788 raise NbiException("URL must contain at least 'main_topic/version/topic'",
789 HTTPStatus
.METHOD_NOT_ALLOWED
)
790 if main_topic
not in ("admin", "vnfpkgm", "nsd", "nslcm", "pdu", "nst", "nsilcm", "nspm"):
791 raise NbiException("URL main_topic '{}' not supported".format(main_topic
),
792 HTTPStatus
.METHOD_NOT_ALLOWED
)
794 raise NbiException("URL version '{}' not supported".format(version
), HTTPStatus
.METHOD_NOT_ALLOWED
)
796 if kwargs
and "METHOD" in kwargs
and kwargs
["METHOD"] in ("PUT", "POST", "DELETE", "GET", "PATCH"):
797 method
= kwargs
.pop("METHOD")
799 method
= cherrypy
.request
.method
801 self
._check
_valid
_url
_method
(method
, main_topic
, version
, topic
, _id
, item
, *args
)
803 if main_topic
== "admin" and topic
== "tokens":
804 return self
.token(method
, _id
, kwargs
)
806 # self.engine.load_dbase(cherrypy.request.app.config)
807 session
= self
.authenticator
.authorize()
808 session
= self
._manage
_admin
_query
(session
, kwargs
, method
, _id
)
809 indata
= self
._format
_in
(kwargs
)
811 if topic
== "subscriptions":
812 engine_topic
= main_topic
+ "_" + topic
813 if item
and topic
!= "pm_jobs":
816 if main_topic
== "nsd":
817 engine_topic
= "nsds"
818 elif main_topic
== "vnfpkgm":
819 engine_topic
= "vnfds"
820 elif main_topic
== "nslcm":
821 engine_topic
= "nsrs"
822 if topic
== "ns_lcm_op_occs":
823 engine_topic
= "nslcmops"
824 if topic
== "vnfrs" or topic
== "vnf_instances":
825 engine_topic
= "vnfrs"
826 elif main_topic
== "nst":
827 engine_topic
= "nsts"
828 elif main_topic
== "nsilcm":
829 engine_topic
= "nsis"
830 if topic
== "nsi_lcm_op_occs":
831 engine_topic
= "nsilcmops"
832 elif main_topic
== "pdu":
833 engine_topic
= "pdus"
834 if engine_topic
== "vims": # TODO this is for backward compatibility, it will be removed in the future
835 engine_topic
= "vim_accounts"
838 if item
in ("nsd_content", "package_content", "artifacts", "vnfd", "nsd", "nst", "nst_content"):
839 if item
in ("vnfd", "nsd", "nst"):
843 elif item
== "artifacts":
847 file, _format
= self
.engine
.get_file(session
, engine_topic
, _id
, path
,
848 cherrypy
.request
.headers
.get("Accept"))
851 outdata
= self
.engine
.get_item_list(session
, engine_topic
, kwargs
)
853 if item
== "reports":
854 # TODO check that project_id (_id in this context) has permissions
856 outdata
= self
.engine
.get_item(session
, engine_topic
, _id
)
857 elif method
== "POST":
858 if topic
in ("ns_descriptors_content", "vnf_packages_content", "netslice_templates_content"):
859 _id
= cherrypy
.request
.headers
.get("Transaction-Id")
861 _id
= self
.engine
.new_item(rollback
, session
, engine_topic
, {}, None, cherrypy
.request
.headers
)
862 completed
= self
.engine
.upload_content(session
, engine_topic
, _id
, indata
, kwargs
,
863 cherrypy
.request
.headers
)
865 self
._set
_location
_header
(main_topic
, version
, topic
, _id
)
867 cherrypy
.response
.headers
["Transaction-Id"] = _id
868 outdata
= {"id": _id
}
869 elif topic
== "ns_instances_content":
871 _id
= self
.engine
.new_item(rollback
, session
, engine_topic
, indata
, kwargs
)
873 indata
["lcmOperationType"] = "instantiate"
874 indata
["nsInstanceId"] = _id
875 nslcmop_id
= self
.engine
.new_item(rollback
, session
, "nslcmops", indata
, None)
876 self
._set
_location
_header
(main_topic
, version
, topic
, _id
)
877 outdata
= {"id": _id
, "nslcmop_id": nslcmop_id
}
878 elif topic
== "ns_instances" and item
:
879 indata
["lcmOperationType"] = item
880 indata
["nsInstanceId"] = _id
881 _id
= self
.engine
.new_item(rollback
, session
, "nslcmops", indata
, kwargs
)
882 self
._set
_location
_header
(main_topic
, version
, "ns_lcm_op_occs", _id
)
883 outdata
= {"id": _id
}
884 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
885 elif topic
== "netslice_instances_content":
886 # creates NetSlice_Instance_record (NSIR)
887 _id
= self
.engine
.new_item(rollback
, session
, engine_topic
, indata
, kwargs
)
888 self
._set
_location
_header
(main_topic
, version
, topic
, _id
)
889 indata
["lcmOperationType"] = "instantiate"
890 indata
["netsliceInstanceId"] = _id
891 nsilcmop_id
= self
.engine
.new_item(rollback
, session
, "nsilcmops", indata
, kwargs
)
892 outdata
= {"id": _id
, "nsilcmop_id": nsilcmop_id
}
894 elif topic
== "netslice_instances" and item
:
895 indata
["lcmOperationType"] = item
896 indata
["netsliceInstanceId"] = _id
897 _id
= self
.engine
.new_item(rollback
, session
, "nsilcmops", indata
, kwargs
)
898 self
._set
_location
_header
(main_topic
, version
, "nsi_lcm_op_occs", _id
)
899 outdata
= {"id": _id
}
900 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
902 _id
= self
.engine
.new_item(rollback
, session
, engine_topic
, indata
, kwargs
,
903 cherrypy
.request
.headers
)
904 self
._set
_location
_header
(main_topic
, version
, topic
, _id
)
905 outdata
= {"id": _id
}
906 # TODO form NsdInfo when topic in ("ns_descriptors", "vnf_packages")
907 cherrypy
.response
.status
= HTTPStatus
.CREATED
.value
909 elif method
== "DELETE":
911 outdata
= self
.engine
.del_item_list(session
, engine_topic
, kwargs
)
912 cherrypy
.response
.status
= HTTPStatus
.OK
.value
913 else: # len(args) > 1
914 delete_in_process
= False
915 if topic
== "ns_instances_content" and not session
["force"]:
917 "lcmOperationType": "terminate",
921 opp_id
= self
.engine
.new_item(rollback
, session
, "nslcmops", nslcmop_desc
, None)
923 delete_in_process
= True
924 outdata
= {"_id": opp_id
}
925 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
926 elif topic
== "netslice_instances_content" and not session
["force"]:
928 "lcmOperationType": "terminate",
929 "netsliceInstanceId": _id
,
932 opp_id
= self
.engine
.new_item(rollback
, session
, "nsilcmops", nsilcmop_desc
, None)
934 delete_in_process
= True
935 outdata
= {"_id": opp_id
}
936 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
937 if not delete_in_process
:
938 self
.engine
.del_item(session
, engine_topic
, _id
)
939 cherrypy
.response
.status
= HTTPStatus
.NO_CONTENT
.value
940 if engine_topic
in ("vim_accounts", "wim_accounts", "sdns"):
941 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
943 elif method
in ("PUT", "PATCH"):
945 if not indata
and not kwargs
and not session
.get("set_project"):
946 raise NbiException("Nothing to update. Provide payload and/or query string",
947 HTTPStatus
.BAD_REQUEST
)
948 if item
in ("nsd_content", "package_content", "nst_content") and method
== "PUT":
949 completed
= self
.engine
.upload_content(session
, engine_topic
, _id
, indata
, kwargs
,
950 cherrypy
.request
.headers
)
952 cherrypy
.response
.headers
["Transaction-Id"] = id
954 self
.engine
.edit_item(session
, engine_topic
, _id
, indata
, kwargs
)
955 cherrypy
.response
.status
= HTTPStatus
.NO_CONTENT
.value
957 raise NbiException("Method {} not allowed".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
958 return self
._format
_out
(outdata
, session
, _format
)
959 except Exception as e
:
960 if isinstance(e
, (NbiException
, EngineException
, DbException
, FsException
, MsgException
, AuthException
,
962 http_code_value
= cherrypy
.response
.status
= e
.http_code
.value
963 http_code_name
= e
.http_code
.name
964 cherrypy
.log("Exception {}".format(e
))
966 http_code_value
= cherrypy
.response
.status
= HTTPStatus
.BAD_REQUEST
.value
# INTERNAL_SERVER_ERROR
967 cherrypy
.log("CRITICAL: Exception {}".format(e
), traceback
=True)
968 http_code_name
= HTTPStatus
.BAD_REQUEST
.name
969 if hasattr(outdata
, "close"): # is an open file
973 for rollback_item
in rollback
:
975 if rollback_item
.get("operation") == "set":
976 self
.engine
.db
.set_one(rollback_item
["topic"], {"_id": rollback_item
["_id"]},
977 rollback_item
["content"], fail_on_empty
=False)
979 self
.engine
.db
.del_one(rollback_item
["topic"], {"_id": rollback_item
["_id"]},
981 except Exception as e2
:
982 rollback_error_text
= "Rollback Exception {}: {}".format(rollback_item
, e2
)
983 cherrypy
.log(rollback_error_text
)
984 error_text
+= ". " + rollback_error_text
985 # if isinstance(e, MsgException):
986 # error_text = "{} has been '{}' but other modules cannot be informed because an error on bus".format(
987 # engine_topic[:-1], method, error_text)
989 "code": http_code_name
,
990 "status": http_code_value
,
991 "detail": error_text
,
993 return self
._format
_out
(problem_details
, session
)
994 # raise cherrypy.HTTPError(e.http_code.value, str(e))
997 def _start_service():
999 Callback function called when cherrypy.engine starts
1000 Override configuration with env variables
1001 Set database, storage, message configuration
1002 Init database with admin/admin user password
1005 global subscription_thread
1006 cherrypy
.log
.error("Starting osm_nbi")
1007 # update general cherrypy configuration
1010 engine_config
= cherrypy
.tree
.apps
['/osm'].config
1011 for k
, v
in environ
.items():
1012 if not k
.startswith("OSMNBI_"):
1014 k1
, _
, k2
= k
[7:].lower().partition("_")
1018 # update static configuration
1019 if k
== 'OSMNBI_STATIC_DIR':
1020 engine_config
["/static"]['tools.staticdir.dir'] = v
1021 engine_config
["/static"]['tools.staticdir.on'] = True
1022 elif k
== 'OSMNBI_SOCKET_PORT' or k
== 'OSMNBI_SERVER_PORT':
1023 update_dict
['server.socket_port'] = int(v
)
1024 elif k
== 'OSMNBI_SOCKET_HOST' or k
== 'OSMNBI_SERVER_HOST':
1025 update_dict
['server.socket_host'] = v
1026 elif k1
in ("server", "test", "auth", "log"):
1027 update_dict
[k1
+ '.' + k2
] = v
1028 elif k1
in ("message", "database", "storage", "authentication"):
1029 # k2 = k2.replace('_', '.')
1030 if k2
in ("port", "db_port"):
1031 engine_config
[k1
][k2
] = int(v
)
1033 engine_config
[k1
][k2
] = v
1035 except ValueError as e
:
1036 cherrypy
.log
.error("Ignoring environ '{}': " + str(e
))
1037 except Exception as e
:
1038 cherrypy
.log
.warn("skipping environ '{}' on exception '{}'".format(k
, e
))
1041 cherrypy
.config
.update(update_dict
)
1042 engine_config
["global"].update(update_dict
)
1045 log_format_simple
= "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(message)s"
1046 log_formatter_simple
= logging
.Formatter(log_format_simple
, datefmt
='%Y-%m-%dT%H:%M:%S')
1047 logger_server
= logging
.getLogger("cherrypy.error")
1048 logger_access
= logging
.getLogger("cherrypy.access")
1049 logger_cherry
= logging
.getLogger("cherrypy")
1050 logger_nbi
= logging
.getLogger("nbi")
1052 if "log.file" in engine_config
["global"]:
1053 file_handler
= logging
.handlers
.RotatingFileHandler(engine_config
["global"]["log.file"],
1054 maxBytes
=100e6
, backupCount
=9, delay
=0)
1055 file_handler
.setFormatter(log_formatter_simple
)
1056 logger_cherry
.addHandler(file_handler
)
1057 logger_nbi
.addHandler(file_handler
)
1058 # log always to standard output
1059 for format_
, logger
in {"nbi.server %(filename)s:%(lineno)s": logger_server
,
1060 "nbi.access %(filename)s:%(lineno)s": logger_access
,
1061 "%(name)s %(filename)s:%(lineno)s": logger_nbi
1063 log_format_cherry
= "%(asctime)s %(levelname)s {} %(message)s".format(format_
)
1064 log_formatter_cherry
= logging
.Formatter(log_format_cherry
, datefmt
='%Y-%m-%dT%H:%M:%S')
1065 str_handler
= logging
.StreamHandler()
1066 str_handler
.setFormatter(log_formatter_cherry
)
1067 logger
.addHandler(str_handler
)
1069 if engine_config
["global"].get("log.level"):
1070 logger_cherry
.setLevel(engine_config
["global"]["log.level"])
1071 logger_nbi
.setLevel(engine_config
["global"]["log.level"])
1073 # logging other modules
1074 for k1
, logname
in {"message": "nbi.msg", "database": "nbi.db", "storage": "nbi.fs"}.items():
1075 engine_config
[k1
]["logger_name"] = logname
1076 logger_module
= logging
.getLogger(logname
)
1077 if "logfile" in engine_config
[k1
]:
1078 file_handler
= logging
.handlers
.RotatingFileHandler(engine_config
[k1
]["logfile"],
1079 maxBytes
=100e6
, backupCount
=9, delay
=0)
1080 file_handler
.setFormatter(log_formatter_simple
)
1081 logger_module
.addHandler(file_handler
)
1082 if "loglevel" in engine_config
[k1
]:
1083 logger_module
.setLevel(engine_config
[k1
]["loglevel"])
1084 # TODO add more entries, e.g.: storage
1085 cherrypy
.tree
.apps
['/osm'].root
.engine
.start(engine_config
)
1086 cherrypy
.tree
.apps
['/osm'].root
.authenticator
.start(engine_config
)
1087 cherrypy
.tree
.apps
['/osm'].root
.engine
.init_db(target_version
=database_version
)
1088 cherrypy
.tree
.apps
['/osm'].root
.authenticator
.init_db(target_version
=auth_database_version
)
1090 # start subscriptions thread:
1091 subscription_thread
= SubscriptionThread(config
=engine_config
, engine
=nbi_server
.engine
)
1092 subscription_thread
.start()
1093 # Do not capture except SubscriptionException
1095 # load and print version. Ignore possible errors, e.g. file not found
1097 with
open("{}/version".format(engine_config
["/static"]['tools.staticdir.dir'])) as version_file
:
1098 version_data
= version_file
.read()
1099 cherrypy
.log
.error("Starting OSM NBI Version: {}".format(version_data
.replace("\n", " ")))
1104 def _stop_service():
1106 Callback function called when cherrypy.engine stops
1107 TODO: Ending database connections.
1109 global subscription_thread
1110 if subscription_thread
:
1111 subscription_thread
.terminate()
1112 subscription_thread
= None
1113 cherrypy
.tree
.apps
['/osm'].root
.engine
.stop()
1114 cherrypy
.log
.error("Stopping osm_nbi")
1117 def nbi(config_file
):
1121 # #'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
1122 # 'tools.sessions.on': True,
1123 # 'tools.response_headers.on': True,
1124 # # 'tools.response_headers.headers': [('Content-Type', 'text/plain')],
1127 # cherrypy.Server.ssl_module = 'builtin'
1128 # cherrypy.Server.ssl_certificate = "http/cert.pem"
1129 # cherrypy.Server.ssl_private_key = "http/privkey.pem"
1130 # cherrypy.Server.thread_pool = 10
1131 # cherrypy.config.update({'Server.socket_port': config["port"], 'Server.socket_host': config["host"]})
1133 # cherrypy.config.update({'tools.auth_basic.on': True,
1134 # 'tools.auth_basic.realm': 'localhost',
1135 # 'tools.auth_basic.checkpassword': validate_password})
1136 nbi_server
= Server()
1137 cherrypy
.engine
.subscribe('start', _start_service
)
1138 cherrypy
.engine
.subscribe('stop', _stop_service
)
1139 cherrypy
.quickstart(nbi_server
, '/osm', config_file
)
1143 print("""Usage: {} [options]
1144 -c|--config [configuration_file]: loads the configuration file (default: ./nbi.cfg)
1145 -h|--help: shows this help
1146 """.format(sys
.argv
[0]))
1147 # --log-socket-host HOST: send logs to this host")
1148 # --log-socket-port PORT: send logs using this port (default: 9022)")
1151 if __name__
== '__main__':
1153 # load parameters and configuration
1154 opts
, args
= getopt
.getopt(sys
.argv
[1:], "hvc:", ["config=", "help"])
1155 # TODO add "log-socket-host=", "log-socket-port=", "log-file="
1158 if o
in ("-h", "--help"):
1161 elif o
in ("-c", "--config"):
1163 # elif o == "--log-socket-port":
1164 # log_socket_port = a
1165 # elif o == "--log-socket-host":
1166 # log_socket_host = a
1167 # elif o == "--log-file":
1170 assert False, "Unhandled option"
1172 if not path
.isfile(config_file
):
1173 print("configuration file '{}' that not exist".format(config_file
), file=sys
.stderr
)
1176 for config_file
in (__file__
[:__file__
.rfind(".")] + ".cfg", "./nbi.cfg", "/etc/osm/nbi.cfg"):
1177 if path
.isfile(config_file
):
1180 print("No configuration file 'nbi.cfg' found neither at local folder nor at /etc/osm/", file=sys
.stderr
)
1183 except getopt
.GetoptError
as e
:
1184 print(str(e
), file=sys
.stderr
)