afc65c011861bf5421218cba48c1fe01d7628386
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.2'
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
)
959 # if Role information changes, it is needed to reload the information of roles
960 if topic
== "roles" and method
!= "GET":
961 self
.authenticator
.load_operation_to_allowed_roles()
962 return self
._format
_out
(outdata
, session
, _format
)
963 except Exception as e
:
964 if isinstance(e
, (NbiException
, EngineException
, DbException
, FsException
, MsgException
, AuthException
,
966 http_code_value
= cherrypy
.response
.status
= e
.http_code
.value
967 http_code_name
= e
.http_code
.name
968 cherrypy
.log("Exception {}".format(e
))
970 http_code_value
= cherrypy
.response
.status
= HTTPStatus
.BAD_REQUEST
.value
# INTERNAL_SERVER_ERROR
971 cherrypy
.log("CRITICAL: Exception {}".format(e
), traceback
=True)
972 http_code_name
= HTTPStatus
.BAD_REQUEST
.name
973 if hasattr(outdata
, "close"): # is an open file
977 for rollback_item
in rollback
:
979 if rollback_item
.get("operation") == "set":
980 self
.engine
.db
.set_one(rollback_item
["topic"], {"_id": rollback_item
["_id"]},
981 rollback_item
["content"], fail_on_empty
=False)
983 self
.engine
.db
.del_one(rollback_item
["topic"], {"_id": rollback_item
["_id"]},
985 except Exception as e2
:
986 rollback_error_text
= "Rollback Exception {}: {}".format(rollback_item
, e2
)
987 cherrypy
.log(rollback_error_text
)
988 error_text
+= ". " + rollback_error_text
989 # if isinstance(e, MsgException):
990 # error_text = "{} has been '{}' but other modules cannot be informed because an error on bus".format(
991 # engine_topic[:-1], method, error_text)
993 "code": http_code_name
,
994 "status": http_code_value
,
995 "detail": error_text
,
997 return self
._format
_out
(problem_details
, session
)
998 # raise cherrypy.HTTPError(e.http_code.value, str(e))
1001 def _start_service():
1003 Callback function called when cherrypy.engine starts
1004 Override configuration with env variables
1005 Set database, storage, message configuration
1006 Init database with admin/admin user password
1009 global subscription_thread
1010 cherrypy
.log
.error("Starting osm_nbi")
1011 # update general cherrypy configuration
1014 engine_config
= cherrypy
.tree
.apps
['/osm'].config
1015 for k
, v
in environ
.items():
1016 if not k
.startswith("OSMNBI_"):
1018 k1
, _
, k2
= k
[7:].lower().partition("_")
1022 # update static configuration
1023 if k
== 'OSMNBI_STATIC_DIR':
1024 engine_config
["/static"]['tools.staticdir.dir'] = v
1025 engine_config
["/static"]['tools.staticdir.on'] = True
1026 elif k
== 'OSMNBI_SOCKET_PORT' or k
== 'OSMNBI_SERVER_PORT':
1027 update_dict
['server.socket_port'] = int(v
)
1028 elif k
== 'OSMNBI_SOCKET_HOST' or k
== 'OSMNBI_SERVER_HOST':
1029 update_dict
['server.socket_host'] = v
1030 elif k1
in ("server", "test", "auth", "log"):
1031 update_dict
[k1
+ '.' + k2
] = v
1032 elif k1
in ("message", "database", "storage", "authentication"):
1033 # k2 = k2.replace('_', '.')
1034 if k2
in ("port", "db_port"):
1035 engine_config
[k1
][k2
] = int(v
)
1037 engine_config
[k1
][k2
] = v
1039 except ValueError as e
:
1040 cherrypy
.log
.error("Ignoring environ '{}': " + str(e
))
1041 except Exception as e
:
1042 cherrypy
.log
.warn("skipping environ '{}' on exception '{}'".format(k
, e
))
1045 cherrypy
.config
.update(update_dict
)
1046 engine_config
["global"].update(update_dict
)
1049 log_format_simple
= "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(message)s"
1050 log_formatter_simple
= logging
.Formatter(log_format_simple
, datefmt
='%Y-%m-%dT%H:%M:%S')
1051 logger_server
= logging
.getLogger("cherrypy.error")
1052 logger_access
= logging
.getLogger("cherrypy.access")
1053 logger_cherry
= logging
.getLogger("cherrypy")
1054 logger_nbi
= logging
.getLogger("nbi")
1056 if "log.file" in engine_config
["global"]:
1057 file_handler
= logging
.handlers
.RotatingFileHandler(engine_config
["global"]["log.file"],
1058 maxBytes
=100e6
, backupCount
=9, delay
=0)
1059 file_handler
.setFormatter(log_formatter_simple
)
1060 logger_cherry
.addHandler(file_handler
)
1061 logger_nbi
.addHandler(file_handler
)
1062 # log always to standard output
1063 for format_
, logger
in {"nbi.server %(filename)s:%(lineno)s": logger_server
,
1064 "nbi.access %(filename)s:%(lineno)s": logger_access
,
1065 "%(name)s %(filename)s:%(lineno)s": logger_nbi
1067 log_format_cherry
= "%(asctime)s %(levelname)s {} %(message)s".format(format_
)
1068 log_formatter_cherry
= logging
.Formatter(log_format_cherry
, datefmt
='%Y-%m-%dT%H:%M:%S')
1069 str_handler
= logging
.StreamHandler()
1070 str_handler
.setFormatter(log_formatter_cherry
)
1071 logger
.addHandler(str_handler
)
1073 if engine_config
["global"].get("log.level"):
1074 logger_cherry
.setLevel(engine_config
["global"]["log.level"])
1075 logger_nbi
.setLevel(engine_config
["global"]["log.level"])
1077 # logging other modules
1078 for k1
, logname
in {"message": "nbi.msg", "database": "nbi.db", "storage": "nbi.fs"}.items():
1079 engine_config
[k1
]["logger_name"] = logname
1080 logger_module
= logging
.getLogger(logname
)
1081 if "logfile" in engine_config
[k1
]:
1082 file_handler
= logging
.handlers
.RotatingFileHandler(engine_config
[k1
]["logfile"],
1083 maxBytes
=100e6
, backupCount
=9, delay
=0)
1084 file_handler
.setFormatter(log_formatter_simple
)
1085 logger_module
.addHandler(file_handler
)
1086 if "loglevel" in engine_config
[k1
]:
1087 logger_module
.setLevel(engine_config
[k1
]["loglevel"])
1088 # TODO add more entries, e.g.: storage
1089 cherrypy
.tree
.apps
['/osm'].root
.engine
.start(engine_config
)
1090 cherrypy
.tree
.apps
['/osm'].root
.authenticator
.start(engine_config
)
1091 cherrypy
.tree
.apps
['/osm'].root
.engine
.init_db(target_version
=database_version
)
1092 cherrypy
.tree
.apps
['/osm'].root
.authenticator
.init_db(target_version
=auth_database_version
)
1094 # start subscriptions thread:
1095 subscription_thread
= SubscriptionThread(config
=engine_config
, engine
=nbi_server
.engine
)
1096 subscription_thread
.start()
1097 # Do not capture except SubscriptionException
1099 # load and print version. Ignore possible errors, e.g. file not found
1101 with
open("{}/version".format(engine_config
["/static"]['tools.staticdir.dir'])) as version_file
:
1102 version_data
= version_file
.read()
1103 cherrypy
.log
.error("Starting OSM NBI Version: {}".format(version_data
.replace("\n", " ")))
1108 def _stop_service():
1110 Callback function called when cherrypy.engine stops
1111 TODO: Ending database connections.
1113 global subscription_thread
1114 if subscription_thread
:
1115 subscription_thread
.terminate()
1116 subscription_thread
= None
1117 cherrypy
.tree
.apps
['/osm'].root
.engine
.stop()
1118 cherrypy
.log
.error("Stopping osm_nbi")
1121 def nbi(config_file
):
1125 # #'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
1126 # 'tools.sessions.on': True,
1127 # 'tools.response_headers.on': True,
1128 # # 'tools.response_headers.headers': [('Content-Type', 'text/plain')],
1131 # cherrypy.Server.ssl_module = 'builtin'
1132 # cherrypy.Server.ssl_certificate = "http/cert.pem"
1133 # cherrypy.Server.ssl_private_key = "http/privkey.pem"
1134 # cherrypy.Server.thread_pool = 10
1135 # cherrypy.config.update({'Server.socket_port': config["port"], 'Server.socket_host': config["host"]})
1137 # cherrypy.config.update({'tools.auth_basic.on': True,
1138 # 'tools.auth_basic.realm': 'localhost',
1139 # 'tools.auth_basic.checkpassword': validate_password})
1140 nbi_server
= Server()
1141 cherrypy
.engine
.subscribe('start', _start_service
)
1142 cherrypy
.engine
.subscribe('stop', _stop_service
)
1143 cherrypy
.quickstart(nbi_server
, '/osm', config_file
)
1147 print("""Usage: {} [options]
1148 -c|--config [configuration_file]: loads the configuration file (default: ./nbi.cfg)
1149 -h|--help: shows this help
1150 """.format(sys
.argv
[0]))
1151 # --log-socket-host HOST: send logs to this host")
1152 # --log-socket-port PORT: send logs using this port (default: 9022)")
1155 if __name__
== '__main__':
1157 # load parameters and configuration
1158 opts
, args
= getopt
.getopt(sys
.argv
[1:], "hvc:", ["config=", "help"])
1159 # TODO add "log-socket-host=", "log-socket-port=", "log-file="
1162 if o
in ("-h", "--help"):
1165 elif o
in ("-c", "--config"):
1167 # elif o == "--log-socket-port":
1168 # log_socket_port = a
1169 # elif o == "--log-socket-host":
1170 # log_socket_host = a
1171 # elif o == "--log-file":
1174 assert False, "Unhandled option"
1176 if not path
.isfile(config_file
):
1177 print("configuration file '{}' that not exist".format(config_file
), file=sys
.stderr
)
1180 for config_file
in (__file__
[:__file__
.rfind(".")] + ".cfg", "./nbi.cfg", "/etc/osm/nbi.cfg"):
1181 if path
.isfile(config_file
):
1184 print("No configuration file 'nbi.cfg' found neither at local folder nor at /etc/osm/", file=sys
.stderr
)
1187 except getopt
.GetoptError
as e
:
1188 print(str(e
), file=sys
.stderr
)