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.0'
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 Header field name Reference Example Descriptions
162 Accept IETF RFC 7231 [19] application/json Content-Types that are acceptable for the response.
163 This header field shall be present if the response is expected to have a non-empty message body.
164 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the request.
165 This header field shall be present if the request has a non-empty message body.
166 Authorization IETF RFC 7235 [22] Bearer mF_9.B5f-4.1JqM The authorization token for the request.
167 Details are specified in clause 4.5.3.
168 Range IETF RFC 7233 [21] 1000-2000 Requested range of bytes from a file
169 Header field name Reference Example Descriptions
170 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the response.
171 This header field shall be present if the response has a non-empty message body.
172 Location IETF RFC 7231 [19] http://www.example.com/vnflcm/v1/vnf_instances/123 Used in redirection, or when a
173 new resource has been created.
174 This header field shall be present if the response status code is 201 or 3xx.
175 In the present document this header field is also used if the response status code is 202 and a new resource was
177 WWW-Authenticate IETF RFC 7235 [22] Bearer realm="example" Challenge if the corresponding HTTP request has not
178 provided authorization, or error details if the corresponding HTTP request has provided an invalid authorization
180 Accept-Ranges IETF RFC 7233 [21] bytes Used by the Server to signal whether or not it supports ranges for
182 Content-Range IETF RFC 7233 [21] bytes 21010-47021/ 47022 Signals the byte range that is contained in the
183 response, and the total length of the file.
184 Retry-After IETF RFC 7231 [19] Fri, 31 Dec 1999 23:59:59 GMT
188 class NbiException(Exception):
190 def __init__(self
, message
, http_code
=HTTPStatus
.METHOD_NOT_ALLOWED
):
191 Exception.__init
__(self
, message
)
192 self
.http_code
= http_code
195 class Server(object):
197 # to decode bytes to str
198 reader
= getreader("utf-8")
202 self
.engine
= Engine()
203 self
.authenticator
= Authenticator()
204 self
.valid_methods
= { # contains allowed URL and methods
207 "tokens": {"METHODS": ("GET", "POST", "DELETE"),
208 "<ID>": {"METHODS": ("GET", "DELETE")}
210 "users": {"METHODS": ("GET", "POST"),
211 "<ID>": {"METHODS": ("GET", "POST", "DELETE", "PATCH", "PUT")}
213 "projects": {"METHODS": ("GET", "POST"),
214 "<ID>": {"METHODS": ("GET", "DELETE")}
216 "roles": {"METHODS": ("GET", "POST"),
217 "<ID>": {"METHODS": ("GET", "POST", "DELETE")}
219 "vims": {"METHODS": ("GET", "POST"),
220 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
222 "vim_accounts": {"METHODS": ("GET", "POST"),
223 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
225 "wim_accounts": {"METHODS": ("GET", "POST"),
226 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
228 "sdns": {"METHODS": ("GET", "POST"),
229 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
235 "pdu_descriptors": {"METHODS": ("GET", "POST"),
236 "<ID>": {"METHODS": ("GET", "POST", "DELETE", "PATCH", "PUT")}
242 "ns_descriptors_content": {"METHODS": ("GET", "POST"),
243 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
245 "ns_descriptors": {"METHODS": ("GET", "POST"),
246 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"),
247 "nsd_content": {"METHODS": ("GET", "PUT")},
248 "nsd": {"METHODS": "GET"}, # descriptor inside package
249 "artifacts": {"*": {"METHODS": "GET"}}
252 "pnf_descriptors": {"TODO": ("GET", "POST"),
253 "<ID>": {"TODO": ("GET", "DELETE", "PATCH"),
254 "pnfd_content": {"TODO": ("GET", "PUT")}
257 "subscriptions": {"TODO": ("GET", "POST"),
258 "<ID>": {"TODO": ("GET", "DELETE")}
264 "vnf_packages_content": {"METHODS": ("GET", "POST"),
265 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
267 "vnf_packages": {"METHODS": ("GET", "POST"),
268 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"), # GET: vnfPkgInfo
269 "package_content": {"METHODS": ("GET", "PUT"), # package
270 "upload_from_uri": {"TODO": "POST"}
272 "vnfd": {"METHODS": "GET"}, # descriptor inside package
273 "artifacts": {"*": {"METHODS": "GET"}}
276 "subscriptions": {"TODO": ("GET", "POST"),
277 "<ID>": {"TODO": ("GET", "DELETE")}
283 "ns_instances_content": {"METHODS": ("GET", "POST"),
284 "<ID>": {"METHODS": ("GET", "DELETE")}
286 "ns_instances": {"METHODS": ("GET", "POST"),
287 "<ID>": {"METHODS": ("GET", "DELETE"),
288 "scale": {"METHODS": "POST"},
289 "terminate": {"METHODS": "POST"},
290 "instantiate": {"METHODS": "POST"},
291 "action": {"METHODS": "POST"},
294 "ns_lcm_op_occs": {"METHODS": "GET",
295 "<ID>": {"METHODS": "GET"},
297 "vnfrs": {"METHODS": ("GET"),
298 "<ID>": {"METHODS": ("GET")}
300 "vnf_instances": {"METHODS": ("GET"),
301 "<ID>": {"METHODS": ("GET")}
307 "netslice_templates_content": {"METHODS": ("GET", "POST"),
308 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
310 "netslice_templates": {"METHODS": ("GET", "POST"),
311 "<ID>": {"METHODS": ("GET", "DELETE"), "TODO": "PATCH",
312 "nst_content": {"METHODS": ("GET", "PUT")},
313 "nst": {"METHODS": "GET"}, # descriptor inside package
314 "artifacts": {"*": {"METHODS": "GET"}}
317 "subscriptions": {"TODO": ("GET", "POST"),
318 "<ID>": {"TODO": ("GET", "DELETE")}
324 "netslice_instances_content": {"METHODS": ("GET", "POST"),
325 "<ID>": {"METHODS": ("GET", "DELETE")}
327 "netslice_instances": {"METHODS": ("GET", "POST"),
328 "<ID>": {"METHODS": ("GET", "DELETE"),
329 "terminate": {"METHODS": "POST"},
330 "instantiate": {"METHODS": "POST"},
331 "action": {"METHODS": "POST"},
334 "nsi_lcm_op_occs": {"METHODS": "GET",
335 "<ID>": {"METHODS": "GET"},
341 def _format_in(self
, kwargs
):
344 if cherrypy
.request
.body
.length
:
345 error_text
= "Invalid input format "
347 if "Content-Type" in cherrypy
.request
.headers
:
348 if "application/json" in cherrypy
.request
.headers
["Content-Type"]:
349 error_text
= "Invalid json format "
350 indata
= json
.load(self
.reader(cherrypy
.request
.body
))
351 cherrypy
.request
.headers
.pop("Content-File-MD5", None)
352 elif "application/yaml" in cherrypy
.request
.headers
["Content-Type"]:
353 error_text
= "Invalid yaml format "
354 indata
= yaml
.load(cherrypy
.request
.body
)
355 cherrypy
.request
.headers
.pop("Content-File-MD5", None)
356 elif "application/binary" in cherrypy
.request
.headers
["Content-Type"] or \
357 "application/gzip" in cherrypy
.request
.headers
["Content-Type"] or \
358 "application/zip" in cherrypy
.request
.headers
["Content-Type"] or \
359 "text/plain" in cherrypy
.request
.headers
["Content-Type"]:
360 indata
= cherrypy
.request
.body
# .read()
361 elif "multipart/form-data" in cherrypy
.request
.headers
["Content-Type"]:
362 if "descriptor_file" in kwargs
:
363 filecontent
= kwargs
.pop("descriptor_file")
364 if not filecontent
.file:
365 raise NbiException("empty file or content", HTTPStatus
.BAD_REQUEST
)
366 indata
= filecontent
.file # .read()
367 if filecontent
.content_type
.value
:
368 cherrypy
.request
.headers
["Content-Type"] = filecontent
.content_type
.value
370 # raise cherrypy.HTTPError(HTTPStatus.Not_Acceptable,
371 # "Only 'Content-Type' of type 'application/json' or
372 # 'application/yaml' for input format are available")
373 error_text
= "Invalid yaml format "
374 indata
= yaml
.load(cherrypy
.request
.body
)
375 cherrypy
.request
.headers
.pop("Content-File-MD5", None)
377 error_text
= "Invalid yaml format "
378 indata
= yaml
.load(cherrypy
.request
.body
)
379 cherrypy
.request
.headers
.pop("Content-File-MD5", None)
384 if cherrypy
.request
.headers
.get("Query-String-Format") == "yaml":
387 for k
, v
in kwargs
.items():
388 if isinstance(v
, str):
393 kwargs
[k
] = yaml
.load(v
)
396 elif k
.endswith(".gt") or k
.endswith(".lt") or k
.endswith(".gte") or k
.endswith(".lte"):
404 elif v
.find(",") > 0:
405 kwargs
[k
] = v
.split(",")
406 elif isinstance(v
, (list, tuple)):
407 for index
in range(0, len(v
)):
412 v
[index
] = yaml
.load(v
[index
])
417 except (ValueError, yaml
.YAMLError
) as exc
:
418 raise NbiException(error_text
+ str(exc
), HTTPStatus
.BAD_REQUEST
)
419 except KeyError as exc
:
420 raise NbiException("Query string error: " + str(exc
), HTTPStatus
.BAD_REQUEST
)
421 except Exception as exc
:
422 raise NbiException(error_text
+ str(exc
), HTTPStatus
.BAD_REQUEST
)
425 def _format_out(data
, session
=None, _format
=None):
427 return string of dictionary data according to requested json, yaml, xml. By default json
428 :param data: response to be sent. Can be a dict, text or file
430 :param _format: The format to be set as Content-Type ir data is a file
433 accept
= cherrypy
.request
.headers
.get("Accept")
435 if accept
and "text/html" in accept
:
436 return html
.format(data
, cherrypy
.request
, cherrypy
.response
, session
)
437 # cherrypy.response.status = HTTPStatus.NO_CONTENT.value
439 elif hasattr(data
, "read"): # file object
441 cherrypy
.response
.headers
["Content-Type"] = _format
442 elif "b" in data
.mode
: # binariy asssumig zip
443 cherrypy
.response
.headers
["Content-Type"] = 'application/zip'
445 cherrypy
.response
.headers
["Content-Type"] = 'text/plain'
446 # TODO check that cherrypy close file. If not implement pending things to close per thread next
449 if "application/json" in accept
:
450 cherrypy
.response
.headers
["Content-Type"] = 'application/json; charset=utf-8'
451 a
= json
.dumps(data
, indent
=4) + "\n"
452 return a
.encode("utf8")
453 elif "text/html" in accept
:
454 return html
.format(data
, cherrypy
.request
, cherrypy
.response
, session
)
456 elif "application/yaml" in accept
or "*/*" in accept
or "text/plain" in accept
:
458 # if there is not any valid accept, raise an error. But if response is already an error, format in yaml
459 elif cherrypy
.response
.status
>= 400:
460 raise cherrypy
.HTTPError(HTTPStatus
.NOT_ACCEPTABLE
.value
,
461 "Only 'Accept' of type 'application/json' or 'application/yaml' "
462 "for output format are available")
463 cherrypy
.response
.headers
["Content-Type"] = 'application/yaml'
464 return yaml
.safe_dump(data
, explicit_start
=True, indent
=4, default_flow_style
=False, tags
=False,
465 encoding
='utf-8', allow_unicode
=True) # , canonical=True, default_style='"'
468 def index(self
, *args
, **kwargs
):
471 if cherrypy
.request
.method
== "GET":
472 session
= self
.authenticator
.authorize()
473 outdata
= "Index page"
475 raise cherrypy
.HTTPError(HTTPStatus
.METHOD_NOT_ALLOWED
.value
,
476 "Method {} not allowed for tokens".format(cherrypy
.request
.method
))
478 return self
._format
_out
(outdata
, session
)
480 except (EngineException
, AuthException
) as e
:
481 cherrypy
.log("index Exception {}".format(e
))
482 cherrypy
.response
.status
= e
.http_code
.value
483 return self
._format
_out
("Welcome to OSM!", session
)
486 def version(self
, *args
, **kwargs
):
487 # TODO consider to remove and provide version using the static version file
488 global __version__
, version_date
490 if cherrypy
.request
.method
!= "GET":
491 raise NbiException("Only method GET is allowed", HTTPStatus
.METHOD_NOT_ALLOWED
)
493 raise NbiException("Invalid URL or query string for version", HTTPStatus
.METHOD_NOT_ALLOWED
)
494 return __version__
+ " " + version_date
495 except NbiException
as e
:
496 cherrypy
.response
.status
= e
.http_code
.value
498 "code": e
.http_code
.name
,
499 "status": e
.http_code
.value
,
502 return self
._format
_out
(problem_details
, None)
505 def token(self
, method
, token_id
=None, kwargs
=None):
507 # self.engine.load_dbase(cherrypy.request.app.config)
508 indata
= self
._format
_in
(kwargs
)
509 if not isinstance(indata
, dict):
510 raise NbiException("Expected application/yaml or application/json Content-Type", HTTPStatus
.BAD_REQUEST
)
513 session
= self
.authenticator
.authorize()
515 outdata
= self
.authenticator
.get_token(session
, token_id
)
517 outdata
= self
.authenticator
.get_token_list(session
)
518 elif method
== "POST":
520 session
= self
.authenticator
.authorize()
524 indata
.update(kwargs
)
525 outdata
= self
.authenticator
.new_token(session
, indata
, cherrypy
.request
.remote
)
527 cherrypy
.session
['Authorization'] = outdata
["_id"]
528 self
._set
_location
_header
("admin", "v1", "tokens", outdata
["_id"])
529 # cherrypy.response.cookie["Authorization"] = outdata["id"]
530 # cherrypy.response.cookie["Authorization"]['expires'] = 3600
531 elif method
== "DELETE":
532 if not token_id
and "id" in kwargs
:
533 token_id
= kwargs
["id"]
535 session
= self
.authenticator
.authorize()
536 token_id
= session
["_id"]
537 outdata
= self
.authenticator
.del_token(token_id
)
539 cherrypy
.session
['Authorization'] = "logout"
540 # cherrypy.response.cookie["Authorization"] = token_id
541 # cherrypy.response.cookie["Authorization"]['expires'] = 0
543 raise NbiException("Method {} not allowed for token".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
544 return self
._format
_out
(outdata
, session
)
545 except (NbiException
, EngineException
, DbException
, AuthException
) as e
:
546 cherrypy
.log("tokens Exception {}".format(e
))
547 cherrypy
.response
.status
= e
.http_code
.value
549 "code": e
.http_code
.name
,
550 "status": e
.http_code
.value
,
553 return self
._format
_out
(problem_details
, session
)
556 def test(self
, *args
, **kwargs
):
558 if args
and args
[0] == "help":
559 return "<html><pre>\ninit\nfile/<name> download file\ndb-clear/table\nfs-clear[/folder]\nlogin\nlogin2\n"\
560 "sleep/<time>\nmessage/topic\n</pre></html>"
562 elif args
and args
[0] == "init":
564 # self.engine.load_dbase(cherrypy.request.app.config)
565 self
.engine
.create_admin()
566 return "Done. User 'admin', password 'admin' created"
568 cherrypy
.response
.status
= HTTPStatus
.FORBIDDEN
.value
569 return self
._format
_out
("Database already initialized")
570 elif args
and args
[0] == "file":
571 return cherrypy
.lib
.static
.serve_file(cherrypy
.tree
.apps
['/osm'].config
["storage"]["path"] + "/" + args
[1],
572 "text/plain", "attachment")
573 elif args
and args
[0] == "file2":
574 f_path
= cherrypy
.tree
.apps
['/osm'].config
["storage"]["path"] + "/" + args
[1]
575 f
= open(f_path
, "r")
576 cherrypy
.response
.headers
["Content-type"] = "text/plain"
579 elif len(args
) == 2 and args
[0] == "db-clear":
580 deleted_info
= self
.engine
.db
.del_list(args
[1], kwargs
)
581 return "{} {} deleted\n".format(deleted_info
["deleted"], args
[1])
582 elif len(args
) and args
[0] == "fs-clear":
586 folders
= self
.engine
.fs
.dir_ls(".")
587 for folder
in folders
:
588 self
.engine
.fs
.file_delete(folder
)
589 return ",".join(folders
) + " folders deleted\n"
590 elif args
and args
[0] == "login":
591 if not cherrypy
.request
.headers
.get("Authorization"):
592 cherrypy
.response
.headers
["WWW-Authenticate"] = 'Basic realm="Access to OSM site", charset="UTF-8"'
593 cherrypy
.response
.status
= HTTPStatus
.UNAUTHORIZED
.value
594 elif args
and args
[0] == "login2":
595 if not cherrypy
.request
.headers
.get("Authorization"):
596 cherrypy
.response
.headers
["WWW-Authenticate"] = 'Bearer realm="Access to OSM site"'
597 cherrypy
.response
.status
= HTTPStatus
.UNAUTHORIZED
.value
598 elif args
and args
[0] == "sleep":
601 sleep_time
= int(args
[1])
603 cherrypy
.response
.status
= HTTPStatus
.FORBIDDEN
.value
604 return self
._format
_out
("Database already initialized")
605 thread_info
= cherrypy
.thread_data
607 time
.sleep(sleep_time
)
609 elif len(args
) >= 2 and args
[0] == "message":
611 return_text
= "<html><pre>{} ->\n".format(main_topic
)
613 if cherrypy
.request
.method
== 'POST':
614 to_send
= yaml
.load(cherrypy
.request
.body
)
615 for k
, v
in to_send
.items():
616 self
.engine
.msg
.write(main_topic
, k
, v
)
617 return_text
+= " {}: {}\n".format(k
, v
)
618 elif cherrypy
.request
.method
== 'GET':
619 for k
, v
in kwargs
.items():
620 self
.engine
.msg
.write(main_topic
, k
, yaml
.load(v
))
621 return_text
+= " {}: {}\n".format(k
, yaml
.load(v
))
622 except Exception as e
:
623 return_text
+= "Error: " + str(e
)
624 return_text
+= "</pre></html>\n"
628 "<html><pre>\nheaders:\n args: {}\n".format(args
) +
629 " kwargs: {}\n".format(kwargs
) +
630 " headers: {}\n".format(cherrypy
.request
.headers
) +
631 " path_info: {}\n".format(cherrypy
.request
.path_info
) +
632 " query_string: {}\n".format(cherrypy
.request
.query_string
) +
633 " session: {}\n".format(cherrypy
.session
) +
634 " cookie: {}\n".format(cherrypy
.request
.cookie
) +
635 " method: {}\n".format(cherrypy
.request
.method
) +
636 " session: {}\n".format(cherrypy
.session
.get('fieldname')) +
638 return_text
+= " length: {}\n".format(cherrypy
.request
.body
.length
)
639 if cherrypy
.request
.body
.length
:
640 return_text
+= " content: {}\n".format(
641 str(cherrypy
.request
.body
.read(int(cherrypy
.request
.headers
.get('Content-Length', 0)))))
643 return_text
+= "thread: {}\n".format(thread_info
)
644 return_text
+= "</pre></html>"
647 def _check_valid_url_method(self
, method
, *args
):
649 raise NbiException("URL must contain at least 'main_topic/version/topic'", HTTPStatus
.METHOD_NOT_ALLOWED
)
651 reference
= self
.valid_methods
655 if not isinstance(reference
, dict):
656 raise NbiException("URL contains unexpected extra items '{}'".format(arg
),
657 HTTPStatus
.METHOD_NOT_ALLOWED
)
660 reference
= reference
[arg
]
661 elif "<ID>" in reference
:
662 reference
= reference
["<ID>"]
663 elif "*" in reference
:
664 reference
= reference
["*"]
667 raise NbiException("Unexpected URL item {}".format(arg
), HTTPStatus
.METHOD_NOT_ALLOWED
)
668 if "TODO" in reference
and method
in reference
["TODO"]:
669 raise NbiException("Method {} not supported yet for this URL".format(method
), HTTPStatus
.NOT_IMPLEMENTED
)
670 elif "METHODS" in reference
and method
not in reference
["METHODS"]:
671 raise NbiException("Method {} not supported for this URL".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
675 def _set_location_header(main_topic
, version
, topic
, id):
677 Insert response header Location with the URL of created item base on URL params
684 # Use cherrypy.request.base for absoluted path and make use of request.header HOST just in case behind aNAT
685 cherrypy
.response
.headers
["Location"] = "/osm/{}/{}/{}/{}".format(main_topic
, version
, topic
, id)
689 def default(self
, main_topic
=None, version
=None, topic
=None, _id
=None, item
=None, *args
, **kwargs
):
698 if not main_topic
or not version
or not topic
:
699 raise NbiException("URL must contain at least 'main_topic/version/topic'",
700 HTTPStatus
.METHOD_NOT_ALLOWED
)
701 if main_topic
not in ("admin", "vnfpkgm", "nsd", "nslcm", "pdu", "nst", "nsilcm"):
702 raise NbiException("URL main_topic '{}' not supported".format(main_topic
),
703 HTTPStatus
.METHOD_NOT_ALLOWED
)
705 raise NbiException("URL version '{}' not supported".format(version
), HTTPStatus
.METHOD_NOT_ALLOWED
)
707 if kwargs
and "METHOD" in kwargs
and kwargs
["METHOD"] in ("PUT", "POST", "DELETE", "GET", "PATCH"):
708 method
= kwargs
.pop("METHOD")
710 method
= cherrypy
.request
.method
711 if kwargs
and "FORCE" in kwargs
:
712 force
= kwargs
.pop("FORCE")
715 self
._check
_valid
_url
_method
(method
, main_topic
, version
, topic
, _id
, item
, *args
)
716 if main_topic
== "admin" and topic
== "tokens":
717 return self
.token(method
, _id
, kwargs
)
719 # self.engine.load_dbase(cherrypy.request.app.config)
720 session
= self
.authenticator
.authorize()
721 indata
= self
._format
_in
(kwargs
)
723 if topic
== "subscriptions":
724 engine_topic
= main_topic
+ "_" + topic
728 if main_topic
== "nsd":
729 engine_topic
= "nsds"
730 elif main_topic
== "vnfpkgm":
731 engine_topic
= "vnfds"
732 elif main_topic
== "nslcm":
733 engine_topic
= "nsrs"
734 if topic
== "ns_lcm_op_occs":
735 engine_topic
= "nslcmops"
736 if topic
== "vnfrs" or topic
== "vnf_instances":
737 engine_topic
= "vnfrs"
738 elif main_topic
== "nst":
739 engine_topic
= "nsts"
740 elif main_topic
== "nsilcm":
741 engine_topic
= "nsis"
742 if topic
== "nsi_lcm_op_occs":
743 engine_topic
= "nsilcmops"
744 elif main_topic
== "pdu":
745 engine_topic
= "pdus"
746 if engine_topic
== "vims": # TODO this is for backward compatibility, it will remove in the future
747 engine_topic
= "vim_accounts"
750 if item
in ("nsd_content", "package_content", "artifacts", "vnfd", "nsd", "nst", "nst_content"):
751 if item
in ("vnfd", "nsd", "nst"):
755 elif item
== "artifacts":
759 file, _format
= self
.engine
.get_file(session
, engine_topic
, _id
, path
,
760 cherrypy
.request
.headers
.get("Accept"))
763 outdata
= self
.engine
.get_item_list(session
, engine_topic
, kwargs
)
765 outdata
= self
.engine
.get_item(session
, engine_topic
, _id
)
766 elif method
== "POST":
767 if topic
in ("ns_descriptors_content", "vnf_packages_content", "netslice_templates_content"):
768 _id
= cherrypy
.request
.headers
.get("Transaction-Id")
770 _id
= self
.engine
.new_item(rollback
, session
, engine_topic
, {}, None, cherrypy
.request
.headers
,
772 completed
= self
.engine
.upload_content(session
, engine_topic
, _id
, indata
, kwargs
,
773 cherrypy
.request
.headers
, force
=force
)
775 self
._set
_location
_header
(main_topic
, version
, topic
, _id
)
777 cherrypy
.response
.headers
["Transaction-Id"] = _id
778 outdata
= {"id": _id
}
779 elif topic
== "ns_instances_content":
781 _id
= self
.engine
.new_item(rollback
, session
, engine_topic
, indata
, kwargs
, force
=force
)
783 indata
["lcmOperationType"] = "instantiate"
784 indata
["nsInstanceId"] = _id
785 self
.engine
.new_item(rollback
, session
, "nslcmops", indata
, None)
786 self
._set
_location
_header
(main_topic
, version
, topic
, _id
)
787 outdata
= {"id": _id
}
788 elif topic
== "ns_instances" and item
:
789 indata
["lcmOperationType"] = item
790 indata
["nsInstanceId"] = _id
791 _id
= self
.engine
.new_item(rollback
, session
, "nslcmops", indata
, kwargs
)
792 self
._set
_location
_header
(main_topic
, version
, "ns_lcm_op_occs", _id
)
793 outdata
= {"id": _id
}
794 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
795 elif topic
== "netslice_instances_content":
796 # creates NetSlice_Instance_record (NSIR)
797 _id
= self
.engine
.new_item(rollback
, session
, engine_topic
, indata
, kwargs
, force
=force
)
798 self
._set
_location
_header
(main_topic
, version
, topic
, _id
)
799 indata
["lcmOperationType"] = "instantiate"
800 indata
["nsiInstanceId"] = _id
801 self
.engine
.new_item(rollback
, session
, "nsilcmops", indata
, kwargs
)
802 outdata
= {"id": _id
}
804 elif topic
== "netslice_instances" and item
:
805 indata
["lcmOperationType"] = item
806 indata
["nsiInstanceId"] = _id
807 _id
= self
.engine
.new_item(rollback
, session
, "nsilcmops", indata
, kwargs
)
808 self
._set
_location
_header
(main_topic
, version
, "nsi_lcm_op_occs", _id
)
809 outdata
= {"id": _id
}
810 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
812 _id
= self
.engine
.new_item(rollback
, session
, engine_topic
, indata
, kwargs
,
813 cherrypy
.request
.headers
, force
=force
)
814 self
._set
_location
_header
(main_topic
, version
, topic
, _id
)
815 outdata
= {"id": _id
}
816 # TODO form NsdInfo when topic in ("ns_descriptors", "vnf_packages")
817 cherrypy
.response
.status
= HTTPStatus
.CREATED
.value
819 elif method
== "DELETE":
821 outdata
= self
.engine
.del_item_list(session
, engine_topic
, kwargs
)
822 cherrypy
.response
.status
= HTTPStatus
.OK
.value
823 else: # len(args) > 1
824 delete_in_process
= False
825 if topic
== "ns_instances_content" and not force
:
827 "lcmOperationType": "terminate",
831 opp_id
= self
.engine
.new_item(rollback
, session
, "nslcmops", nslcmop_desc
, None)
833 delete_in_process
= True
834 outdata
= {"_id": opp_id
}
835 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
836 elif topic
== "netslice_instances_content" and not force
:
838 "lcmOperationType": "terminate",
839 "nsiInstanceId": _id
,
842 opp_id
= self
.engine
.new_item(rollback
, session
, "nsilcmops", nsilcmop_desc
, None)
844 delete_in_process
= True
845 outdata
= {"_id": opp_id
}
846 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
847 if not delete_in_process
:
848 self
.engine
.del_item(session
, engine_topic
, _id
, force
)
849 cherrypy
.response
.status
= HTTPStatus
.NO_CONTENT
.value
850 if engine_topic
in ("vim_accounts", "wim_accounts", "sdns"):
851 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
853 elif method
in ("PUT", "PATCH"):
855 if not indata
and not kwargs
:
856 raise NbiException("Nothing to update. Provide payload and/or query string",
857 HTTPStatus
.BAD_REQUEST
)
858 if item
in ("nsd_content", "package_content", "nst_content") and method
== "PUT":
859 completed
= self
.engine
.upload_content(session
, engine_topic
, _id
, indata
, kwargs
,
860 cherrypy
.request
.headers
, force
=force
)
862 cherrypy
.response
.headers
["Transaction-Id"] = id
864 self
.engine
.edit_item(session
, engine_topic
, _id
, indata
, kwargs
, force
=force
)
865 cherrypy
.response
.status
= HTTPStatus
.NO_CONTENT
.value
867 raise NbiException("Method {} not allowed".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
868 return self
._format
_out
(outdata
, session
, _format
)
869 except Exception as e
:
870 if isinstance(e
, (NbiException
, EngineException
, DbException
, FsException
, MsgException
, AuthException
,
872 http_code_value
= cherrypy
.response
.status
= e
.http_code
.value
873 http_code_name
= e
.http_code
.name
874 cherrypy
.log("Exception {}".format(e
))
876 http_code_value
= cherrypy
.response
.status
= HTTPStatus
.BAD_REQUEST
.value
# INTERNAL_SERVER_ERROR
877 cherrypy
.log("CRITICAL: Exception {}".format(e
), traceback
=True)
878 http_code_name
= HTTPStatus
.BAD_REQUEST
.name
879 if hasattr(outdata
, "close"): # is an open file
883 for rollback_item
in rollback
:
885 if rollback_item
.get("operation") == "set":
886 self
.engine
.db
.set_one(rollback_item
["topic"], {"_id": rollback_item
["_id"]},
887 rollback_item
["content"], fail_on_empty
=False)
889 self
.engine
.db
.del_one(rollback_item
["topic"], {"_id": rollback_item
["_id"]},
891 except Exception as e2
:
892 rollback_error_text
= "Rollback Exception {}: {}".format(rollback_item
, e2
)
893 cherrypy
.log(rollback_error_text
)
894 error_text
+= ". " + rollback_error_text
895 # if isinstance(e, MsgException):
896 # error_text = "{} has been '{}' but other modules cannot be informed because an error on bus".format(
897 # engine_topic[:-1], method, error_text)
899 "code": http_code_name
,
900 "status": http_code_value
,
901 "detail": error_text
,
903 return self
._format
_out
(problem_details
, session
)
904 # raise cherrypy.HTTPError(e.http_code.value, str(e))
907 def _start_service():
909 Callback function called when cherrypy.engine starts
910 Override configuration with env variables
911 Set database, storage, message configuration
912 Init database with admin/admin user password
915 global subscription_thread
916 cherrypy
.log
.error("Starting osm_nbi")
917 # update general cherrypy configuration
920 engine_config
= cherrypy
.tree
.apps
['/osm'].config
921 for k
, v
in environ
.items():
922 if not k
.startswith("OSMNBI_"):
924 k1
, _
, k2
= k
[7:].lower().partition("_")
928 # update static configuration
929 if k
== 'OSMNBI_STATIC_DIR':
930 engine_config
["/static"]['tools.staticdir.dir'] = v
931 engine_config
["/static"]['tools.staticdir.on'] = True
932 elif k
== 'OSMNBI_SOCKET_PORT' or k
== 'OSMNBI_SERVER_PORT':
933 update_dict
['server.socket_port'] = int(v
)
934 elif k
== 'OSMNBI_SOCKET_HOST' or k
== 'OSMNBI_SERVER_HOST':
935 update_dict
['server.socket_host'] = v
936 elif k1
in ("server", "test", "auth", "log"):
937 update_dict
[k1
+ '.' + k2
] = v
938 elif k1
in ("message", "database", "storage", "authentication"):
939 # k2 = k2.replace('_', '.')
940 if k2
in ("port", "db_port"):
941 engine_config
[k1
][k2
] = int(v
)
943 engine_config
[k1
][k2
] = v
945 except ValueError as e
:
946 cherrypy
.log
.error("Ignoring environ '{}': " + str(e
))
947 except Exception as e
:
948 cherrypy
.log
.warn("skipping environ '{}' on exception '{}'".format(k
, e
))
951 cherrypy
.config
.update(update_dict
)
952 engine_config
["global"].update(update_dict
)
955 log_format_simple
= "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(message)s"
956 log_formatter_simple
= logging
.Formatter(log_format_simple
, datefmt
='%Y-%m-%dT%H:%M:%S')
957 logger_server
= logging
.getLogger("cherrypy.error")
958 logger_access
= logging
.getLogger("cherrypy.access")
959 logger_cherry
= logging
.getLogger("cherrypy")
960 logger_nbi
= logging
.getLogger("nbi")
962 if "log.file" in engine_config
["global"]:
963 file_handler
= logging
.handlers
.RotatingFileHandler(engine_config
["global"]["log.file"],
964 maxBytes
=100e6
, backupCount
=9, delay
=0)
965 file_handler
.setFormatter(log_formatter_simple
)
966 logger_cherry
.addHandler(file_handler
)
967 logger_nbi
.addHandler(file_handler
)
968 # log always to standard output
969 for format_
, logger
in {"nbi.server %(filename)s:%(lineno)s": logger_server
,
970 "nbi.access %(filename)s:%(lineno)s": logger_access
,
971 "%(name)s %(filename)s:%(lineno)s": logger_nbi
973 log_format_cherry
= "%(asctime)s %(levelname)s {} %(message)s".format(format_
)
974 log_formatter_cherry
= logging
.Formatter(log_format_cherry
, datefmt
='%Y-%m-%dT%H:%M:%S')
975 str_handler
= logging
.StreamHandler()
976 str_handler
.setFormatter(log_formatter_cherry
)
977 logger
.addHandler(str_handler
)
979 if engine_config
["global"].get("log.level"):
980 logger_cherry
.setLevel(engine_config
["global"]["log.level"])
981 logger_nbi
.setLevel(engine_config
["global"]["log.level"])
983 # logging other modules
984 for k1
, logname
in {"message": "nbi.msg", "database": "nbi.db", "storage": "nbi.fs"}.items():
985 engine_config
[k1
]["logger_name"] = logname
986 logger_module
= logging
.getLogger(logname
)
987 if "logfile" in engine_config
[k1
]:
988 file_handler
= logging
.handlers
.RotatingFileHandler(engine_config
[k1
]["logfile"],
989 maxBytes
=100e6
, backupCount
=9, delay
=0)
990 file_handler
.setFormatter(log_formatter_simple
)
991 logger_module
.addHandler(file_handler
)
992 if "loglevel" in engine_config
[k1
]:
993 logger_module
.setLevel(engine_config
[k1
]["loglevel"])
994 # TODO add more entries, e.g.: storage
995 cherrypy
.tree
.apps
['/osm'].root
.engine
.start(engine_config
)
996 cherrypy
.tree
.apps
['/osm'].root
.authenticator
.start(engine_config
)
997 cherrypy
.tree
.apps
['/osm'].root
.engine
.init_db(target_version
=database_version
)
998 cherrypy
.tree
.apps
['/osm'].root
.authenticator
.init_db(target_version
=auth_database_version
)
1000 # start subscriptions thread:
1001 subscription_thread
= SubscriptionThread(config
=engine_config
, engine
=nbi_server
.engine
)
1002 subscription_thread
.start()
1003 # Do not capture except SubscriptionException
1005 # load and print version. Ignore possible errors, e.g. file not found
1007 with
open("{}/version".format(engine_config
["/static"]['tools.staticdir.dir'])) as version_file
:
1008 version_data
= version_file
.read()
1009 cherrypy
.log
.error("Starting OSM NBI Version: {}".format(version_data
.replace("\n", " ")))
1014 def _stop_service():
1016 Callback function called when cherrypy.engine stops
1017 TODO: Ending database connections.
1019 global subscription_thread
1020 subscription_thread
.terminate()
1021 subscription_thread
= None
1022 cherrypy
.tree
.apps
['/osm'].root
.engine
.stop()
1023 cherrypy
.log
.error("Stopping osm_nbi")
1026 def nbi(config_file
):
1030 # #'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
1031 # 'tools.sessions.on': True,
1032 # 'tools.response_headers.on': True,
1033 # # 'tools.response_headers.headers': [('Content-Type', 'text/plain')],
1036 # cherrypy.Server.ssl_module = 'builtin'
1037 # cherrypy.Server.ssl_certificate = "http/cert.pem"
1038 # cherrypy.Server.ssl_private_key = "http/privkey.pem"
1039 # cherrypy.Server.thread_pool = 10
1040 # cherrypy.config.update({'Server.socket_port': config["port"], 'Server.socket_host': config["host"]})
1042 # cherrypy.config.update({'tools.auth_basic.on': True,
1043 # 'tools.auth_basic.realm': 'localhost',
1044 # 'tools.auth_basic.checkpassword': validate_password})
1045 nbi_server
= Server()
1046 cherrypy
.engine
.subscribe('start', _start_service
)
1047 cherrypy
.engine
.subscribe('stop', _stop_service
)
1048 cherrypy
.quickstart(nbi_server
, '/osm', config_file
)
1052 print("""Usage: {} [options]
1053 -c|--config [configuration_file]: loads the configuration file (default: ./nbi.cfg)
1054 -h|--help: shows this help
1055 """.format(sys
.argv
[0]))
1056 # --log-socket-host HOST: send logs to this host")
1057 # --log-socket-port PORT: send logs using this port (default: 9022)")
1060 if __name__
== '__main__':
1062 # load parameters and configuration
1063 opts
, args
= getopt
.getopt(sys
.argv
[1:], "hvc:", ["config=", "help"])
1064 # TODO add "log-socket-host=", "log-socket-port=", "log-file="
1067 if o
in ("-h", "--help"):
1070 elif o
in ("-c", "--config"):
1072 # elif o == "--log-socket-port":
1073 # log_socket_port = a
1074 # elif o == "--log-socket-host":
1075 # log_socket_host = a
1076 # elif o == "--log-file":
1079 assert False, "Unhandled option"
1081 if not path
.isfile(config_file
):
1082 print("configuration file '{}' that not exist".format(config_file
), file=sys
.stderr
)
1085 for config_file
in (__file__
[:__file__
.rfind(".")] + ".cfg", "./nbi.cfg", "/etc/osm/nbi.cfg"):
1086 if path
.isfile(config_file
):
1089 print("No configuration file 'nbi.cfg' found neither at local folder nor at /etc/osm/", file=sys
.stderr
)
1092 except getopt
.GetoptError
as e
:
1093 print(str(e
), file=sys
.stderr
)