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 validation
import ValidationError
31 from osm_common
.dbbase
import DbException
32 from osm_common
.fsbase
import FsException
33 from osm_common
.msgbase
import MsgException
34 from http
import HTTPStatus
35 from codecs
import getreader
36 from os
import environ
, path
38 __author__
= "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
41 version_date
= "Jan 2019"
42 database_version
= '1.0'
43 auth_database_version
= '1.0'
46 North Bound Interface (O: OSM specific; 5,X: SOL005 not implemented yet; O5: SOL005 implemented)
47 URL: /osm GET POST PUT DELETE PATCH
49 /ns_descriptors_content O O
55 /artifacts[/<artifactPath>] O
63 /vnf_packages_content O O
67 /package_content O5 O5
70 /artifacts[/<artifactPath>] O5
75 /ns_instances_content O O
87 /vnf_instances (also vnfrs for compatibility) O
103 /vim_accounts (also vims for compatibility) O O
111 /netslice_templates_content O O
113 /netslice_templates O O
117 /artifacts[/<artifactPath>] O
119 /<subscriptionId> X X
122 /netslice_instances_content O O
123 /<SliceInstanceId> O O
124 /netslice_instances O O
125 /<SliceInstanceId> O O
130 /<nsiLcmOpOccId> O O O
132 /<subscriptionId> X X
135 Follows SOL005 section 4.3.2 It contains extra METHOD to override http method, FORCE to force.
136 simpleFilterExpr := <attrName>["."<attrName>]*["."<op>]"="<value>[","<value>]*
137 filterExpr := <simpleFilterExpr>["&"<simpleFilterExpr>]*
138 op := "eq" | "neq" (or "ne") | "gt" | "lt" | "gte" | "lte" | "cont" | "ncont"
140 For filtering inside array, it must select the element of the array, or add ANYINDEX to apply the filtering over any
141 item of the array, that is, pass if any item of the array pass the filter.
142 It allows both ne and neq for not equal
143 TODO: 4.3.3 Attribute selectors
144 all_fields, fields=x,y,.., exclude_default, exclude_fields=x,y,...
145 (none) … same as “exclude_default”
146 all_fields … all attributes.
147 fields=<list> … all attributes except all complex attributes with minimum cardinality of zero that are not
148 conditionally mandatory, and that are not provided in <list>.
149 exclude_fields=<list> … all attributes except those complex attributes with a minimum cardinality of zero that
150 are not conditionally mandatory, and that are provided in <list>.
151 exclude_default … all attributes except those complex attributes with a minimum cardinality of zero that are not
152 conditionally mandatory, and that are part of the "default exclude set" defined in the present specification for
153 the particular resource
154 exclude_default and include=<list> … all attributes except those complex attributes with a minimum cardinality
155 of zero that are not conditionally mandatory and that are part of the "default exclude set" defined in the
156 present specification for the particular resource, but that are not part of <list>
157 Header field name Reference Example Descriptions
158 Accept IETF RFC 7231 [19] application/json Content-Types that are acceptable for the response.
159 This header field shall be present if the response is expected to have a non-empty message body.
160 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the request.
161 This header field shall be present if the request has a non-empty message body.
162 Authorization IETF RFC 7235 [22] Bearer mF_9.B5f-4.1JqM The authorization token for the request.
163 Details are specified in clause 4.5.3.
164 Range IETF RFC 7233 [21] 1000-2000 Requested range of bytes from a file
165 Header field name Reference Example Descriptions
166 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the response.
167 This header field shall be present if the response has a non-empty message body.
168 Location IETF RFC 7231 [19] http://www.example.com/vnflcm/v1/vnf_instances/123 Used in redirection, or when a
169 new resource has been created.
170 This header field shall be present if the response status code is 201 or 3xx.
171 In the present document this header field is also used if the response status code is 202 and a new resource was
173 WWW-Authenticate IETF RFC 7235 [22] Bearer realm="example" Challenge if the corresponding HTTP request has not
174 provided authorization, or error details if the corresponding HTTP request has provided an invalid authorization
176 Accept-Ranges IETF RFC 7233 [21] bytes Used by the Server to signal whether or not it supports ranges for
178 Content-Range IETF RFC 7233 [21] bytes 21010-47021/ 47022 Signals the byte range that is contained in the
179 response, and the total length of the file.
180 Retry-After IETF RFC 7231 [19] Fri, 31 Dec 1999 23:59:59 GMT
184 class NbiException(Exception):
186 def __init__(self
, message
, http_code
=HTTPStatus
.METHOD_NOT_ALLOWED
):
187 Exception.__init
__(self
, message
)
188 self
.http_code
= http_code
191 class Server(object):
193 # to decode bytes to str
194 reader
= getreader("utf-8")
198 self
.engine
= Engine()
199 self
.authenticator
= Authenticator()
200 self
.valid_methods
= { # contains allowed URL and methods
203 "tokens": {"METHODS": ("GET", "POST", "DELETE"),
204 "<ID>": {"METHODS": ("GET", "DELETE")}
206 "users": {"METHODS": ("GET", "POST"),
207 "<ID>": {"METHODS": ("GET", "POST", "DELETE", "PATCH", "PUT")}
209 "projects": {"METHODS": ("GET", "POST"),
210 "<ID>": {"METHODS": ("GET", "DELETE")}
212 "vims": {"METHODS": ("GET", "POST"),
213 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
215 "vim_accounts": {"METHODS": ("GET", "POST"),
216 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
218 "wim_accounts": {"METHODS": ("GET", "POST"),
219 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
221 "sdns": {"METHODS": ("GET", "POST"),
222 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
228 "pdu_descriptors": {"METHODS": ("GET", "POST"),
229 "<ID>": {"METHODS": ("GET", "POST", "DELETE", "PATCH", "PUT")}
235 "ns_descriptors_content": {"METHODS": ("GET", "POST"),
236 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
238 "ns_descriptors": {"METHODS": ("GET", "POST"),
239 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"),
240 "nsd_content": {"METHODS": ("GET", "PUT")},
241 "nsd": {"METHODS": "GET"}, # descriptor inside package
242 "artifacts": {"*": {"METHODS": "GET"}}
245 "pnf_descriptors": {"TODO": ("GET", "POST"),
246 "<ID>": {"TODO": ("GET", "DELETE", "PATCH"),
247 "pnfd_content": {"TODO": ("GET", "PUT")}
250 "subscriptions": {"TODO": ("GET", "POST"),
251 "<ID>": {"TODO": ("GET", "DELETE")}
257 "vnf_packages_content": {"METHODS": ("GET", "POST"),
258 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
260 "vnf_packages": {"METHODS": ("GET", "POST"),
261 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"), # GET: vnfPkgInfo
262 "package_content": {"METHODS": ("GET", "PUT"), # package
263 "upload_from_uri": {"TODO": "POST"}
265 "vnfd": {"METHODS": "GET"}, # descriptor inside package
266 "artifacts": {"*": {"METHODS": "GET"}}
269 "subscriptions": {"TODO": ("GET", "POST"),
270 "<ID>": {"TODO": ("GET", "DELETE")}
276 "ns_instances_content": {"METHODS": ("GET", "POST"),
277 "<ID>": {"METHODS": ("GET", "DELETE")}
279 "ns_instances": {"METHODS": ("GET", "POST"),
280 "<ID>": {"METHODS": ("GET", "DELETE"),
281 "scale": {"METHODS": "POST"},
282 "terminate": {"METHODS": "POST"},
283 "instantiate": {"METHODS": "POST"},
284 "action": {"METHODS": "POST"},
287 "ns_lcm_op_occs": {"METHODS": "GET",
288 "<ID>": {"METHODS": "GET"},
290 "vnfrs": {"METHODS": ("GET"),
291 "<ID>": {"METHODS": ("GET")}
293 "vnf_instances": {"METHODS": ("GET"),
294 "<ID>": {"METHODS": ("GET")}
300 "netslice_templates_content": {"METHODS": ("GET", "POST"),
301 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
303 "netslice_templates": {"METHODS": ("GET", "POST"),
304 "<ID>": {"METHODS": ("GET", "DELETE"), "TODO": "PATCH",
305 "nst_content": {"METHODS": ("GET", "PUT")},
306 "nst": {"METHODS": "GET"}, # descriptor inside package
307 "artifacts": {"*": {"METHODS": "GET"}}
310 "subscriptions": {"TODO": ("GET", "POST"),
311 "<ID>": {"TODO": ("GET", "DELETE")}
317 "netslice_instances_content": {"METHODS": ("GET", "POST"),
318 "<ID>": {"METHODS": ("GET", "DELETE")}
320 "netslice_instances": {"METHODS": ("GET", "POST"),
321 "<ID>": {"METHODS": ("GET", "DELETE"),
322 "terminate": {"METHODS": "POST"},
323 "instantiate": {"METHODS": "POST"},
324 "action": {"METHODS": "POST"},
327 "nsi_lcm_op_occs": {"METHODS": "GET",
328 "<ID>": {"METHODS": "GET"},
334 def _format_in(self
, kwargs
):
337 if cherrypy
.request
.body
.length
:
338 error_text
= "Invalid input format "
340 if "Content-Type" in cherrypy
.request
.headers
:
341 if "application/json" in cherrypy
.request
.headers
["Content-Type"]:
342 error_text
= "Invalid json format "
343 indata
= json
.load(self
.reader(cherrypy
.request
.body
))
344 cherrypy
.request
.headers
.pop("Content-File-MD5", None)
345 elif "application/yaml" in cherrypy
.request
.headers
["Content-Type"]:
346 error_text
= "Invalid yaml format "
347 indata
= yaml
.load(cherrypy
.request
.body
)
348 cherrypy
.request
.headers
.pop("Content-File-MD5", None)
349 elif "application/binary" in cherrypy
.request
.headers
["Content-Type"] or \
350 "application/gzip" in cherrypy
.request
.headers
["Content-Type"] or \
351 "application/zip" in cherrypy
.request
.headers
["Content-Type"] or \
352 "text/plain" in cherrypy
.request
.headers
["Content-Type"]:
353 indata
= cherrypy
.request
.body
# .read()
354 elif "multipart/form-data" in cherrypy
.request
.headers
["Content-Type"]:
355 if "descriptor_file" in kwargs
:
356 filecontent
= kwargs
.pop("descriptor_file")
357 if not filecontent
.file:
358 raise NbiException("empty file or content", HTTPStatus
.BAD_REQUEST
)
359 indata
= filecontent
.file # .read()
360 if filecontent
.content_type
.value
:
361 cherrypy
.request
.headers
["Content-Type"] = filecontent
.content_type
.value
363 # raise cherrypy.HTTPError(HTTPStatus.Not_Acceptable,
364 # "Only 'Content-Type' of type 'application/json' or
365 # 'application/yaml' for input format are available")
366 error_text
= "Invalid yaml format "
367 indata
= yaml
.load(cherrypy
.request
.body
)
368 cherrypy
.request
.headers
.pop("Content-File-MD5", None)
370 error_text
= "Invalid yaml format "
371 indata
= yaml
.load(cherrypy
.request
.body
)
372 cherrypy
.request
.headers
.pop("Content-File-MD5", None)
377 if cherrypy
.request
.headers
.get("Query-String-Format") == "yaml":
380 for k
, v
in kwargs
.items():
381 if isinstance(v
, str):
386 kwargs
[k
] = yaml
.load(v
)
389 elif k
.endswith(".gt") or k
.endswith(".lt") or k
.endswith(".gte") or k
.endswith(".lte"):
397 elif v
.find(",") > 0:
398 kwargs
[k
] = v
.split(",")
399 elif isinstance(v
, (list, tuple)):
400 for index
in range(0, len(v
)):
405 v
[index
] = yaml
.load(v
[index
])
410 except (ValueError, yaml
.YAMLError
) as exc
:
411 raise NbiException(error_text
+ str(exc
), HTTPStatus
.BAD_REQUEST
)
412 except KeyError as exc
:
413 raise NbiException("Query string error: " + str(exc
), HTTPStatus
.BAD_REQUEST
)
414 except Exception as exc
:
415 raise NbiException(error_text
+ str(exc
), HTTPStatus
.BAD_REQUEST
)
418 def _format_out(data
, session
=None, _format
=None):
420 return string of dictionary data according to requested json, yaml, xml. By default json
421 :param data: response to be sent. Can be a dict, text or file
423 :param _format: The format to be set as Content-Type ir data is a file
426 accept
= cherrypy
.request
.headers
.get("Accept")
428 if accept
and "text/html" in accept
:
429 return html
.format(data
, cherrypy
.request
, cherrypy
.response
, session
)
430 # cherrypy.response.status = HTTPStatus.NO_CONTENT.value
432 elif hasattr(data
, "read"): # file object
434 cherrypy
.response
.headers
["Content-Type"] = _format
435 elif "b" in data
.mode
: # binariy asssumig zip
436 cherrypy
.response
.headers
["Content-Type"] = 'application/zip'
438 cherrypy
.response
.headers
["Content-Type"] = 'text/plain'
439 # TODO check that cherrypy close file. If not implement pending things to close per thread next
442 if "application/json" in accept
:
443 cherrypy
.response
.headers
["Content-Type"] = 'application/json; charset=utf-8'
444 a
= json
.dumps(data
, indent
=4) + "\n"
445 return a
.encode("utf8")
446 elif "text/html" in accept
:
447 return html
.format(data
, cherrypy
.request
, cherrypy
.response
, session
)
449 elif "application/yaml" in accept
or "*/*" in accept
or "text/plain" in accept
:
451 # if there is not any valid accept, raise an error. But if response is already an error, format in yaml
452 elif cherrypy
.response
.status
>= 400:
453 raise cherrypy
.HTTPError(HTTPStatus
.NOT_ACCEPTABLE
.value
,
454 "Only 'Accept' of type 'application/json' or 'application/yaml' "
455 "for output format are available")
456 cherrypy
.response
.headers
["Content-Type"] = 'application/yaml'
457 return yaml
.safe_dump(data
, explicit_start
=True, indent
=4, default_flow_style
=False, tags
=False,
458 encoding
='utf-8', allow_unicode
=True) # , canonical=True, default_style='"'
461 def index(self
, *args
, **kwargs
):
464 if cherrypy
.request
.method
== "GET":
465 session
= self
.authenticator
.authorize()
466 outdata
= "Index page"
468 raise cherrypy
.HTTPError(HTTPStatus
.METHOD_NOT_ALLOWED
.value
,
469 "Method {} not allowed for tokens".format(cherrypy
.request
.method
))
471 return self
._format
_out
(outdata
, session
)
473 except (EngineException
, AuthException
) as e
:
474 cherrypy
.log("index Exception {}".format(e
))
475 cherrypy
.response
.status
= e
.http_code
.value
476 return self
._format
_out
("Welcome to OSM!", session
)
479 def version(self
, *args
, **kwargs
):
480 # TODO consider to remove and provide version using the static version file
481 global __version__
, version_date
483 if cherrypy
.request
.method
!= "GET":
484 raise NbiException("Only method GET is allowed", HTTPStatus
.METHOD_NOT_ALLOWED
)
486 raise NbiException("Invalid URL or query string for version", HTTPStatus
.METHOD_NOT_ALLOWED
)
487 return __version__
+ " " + version_date
488 except NbiException
as e
:
489 cherrypy
.response
.status
= e
.http_code
.value
491 "code": e
.http_code
.name
,
492 "status": e
.http_code
.value
,
495 return self
._format
_out
(problem_details
, None)
498 def token(self
, method
, token_id
=None, kwargs
=None):
500 # self.engine.load_dbase(cherrypy.request.app.config)
501 indata
= self
._format
_in
(kwargs
)
502 if not isinstance(indata
, dict):
503 raise NbiException("Expected application/yaml or application/json Content-Type", HTTPStatus
.BAD_REQUEST
)
506 session
= self
.authenticator
.authorize()
508 outdata
= self
.authenticator
.get_token(session
, token_id
)
510 outdata
= self
.authenticator
.get_token_list(session
)
511 elif method
== "POST":
513 session
= self
.authenticator
.authorize()
517 indata
.update(kwargs
)
518 outdata
= self
.authenticator
.new_token(session
, indata
, cherrypy
.request
.remote
)
520 cherrypy
.session
['Authorization'] = outdata
["_id"]
521 self
._set
_location
_header
("admin", "v1", "tokens", outdata
["_id"])
522 # cherrypy.response.cookie["Authorization"] = outdata["id"]
523 # cherrypy.response.cookie["Authorization"]['expires'] = 3600
524 elif method
== "DELETE":
525 if not token_id
and "id" in kwargs
:
526 token_id
= kwargs
["id"]
528 session
= self
.authenticator
.authorize()
529 token_id
= session
["_id"]
530 outdata
= self
.authenticator
.del_token(token_id
)
532 cherrypy
.session
['Authorization'] = "logout"
533 # cherrypy.response.cookie["Authorization"] = token_id
534 # cherrypy.response.cookie["Authorization"]['expires'] = 0
536 raise NbiException("Method {} not allowed for token".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
537 return self
._format
_out
(outdata
, session
)
538 except (NbiException
, EngineException
, DbException
, AuthException
) as e
:
539 cherrypy
.log("tokens Exception {}".format(e
))
540 cherrypy
.response
.status
= e
.http_code
.value
542 "code": e
.http_code
.name
,
543 "status": e
.http_code
.value
,
546 return self
._format
_out
(problem_details
, session
)
549 def test(self
, *args
, **kwargs
):
551 if args
and args
[0] == "help":
552 return "<html><pre>\ninit\nfile/<name> download file\ndb-clear/table\nfs-clear[/folder]\nlogin\nlogin2\n"\
553 "sleep/<time>\nmessage/topic\n</pre></html>"
555 elif args
and args
[0] == "init":
557 # self.engine.load_dbase(cherrypy.request.app.config)
558 self
.engine
.create_admin()
559 return "Done. User 'admin', password 'admin' created"
561 cherrypy
.response
.status
= HTTPStatus
.FORBIDDEN
.value
562 return self
._format
_out
("Database already initialized")
563 elif args
and args
[0] == "file":
564 return cherrypy
.lib
.static
.serve_file(cherrypy
.tree
.apps
['/osm'].config
["storage"]["path"] + "/" + args
[1],
565 "text/plain", "attachment")
566 elif args
and args
[0] == "file2":
567 f_path
= cherrypy
.tree
.apps
['/osm'].config
["storage"]["path"] + "/" + args
[1]
568 f
= open(f_path
, "r")
569 cherrypy
.response
.headers
["Content-type"] = "text/plain"
572 elif len(args
) == 2 and args
[0] == "db-clear":
573 deleted_info
= self
.engine
.db
.del_list(args
[1], kwargs
)
574 return "{} {} deleted\n".format(deleted_info
["deleted"], args
[1])
575 elif len(args
) and args
[0] == "fs-clear":
579 folders
= self
.engine
.fs
.dir_ls(".")
580 for folder
in folders
:
581 self
.engine
.fs
.file_delete(folder
)
582 return ",".join(folders
) + " folders deleted\n"
583 elif args
and args
[0] == "login":
584 if not cherrypy
.request
.headers
.get("Authorization"):
585 cherrypy
.response
.headers
["WWW-Authenticate"] = 'Basic realm="Access to OSM site", charset="UTF-8"'
586 cherrypy
.response
.status
= HTTPStatus
.UNAUTHORIZED
.value
587 elif args
and args
[0] == "login2":
588 if not cherrypy
.request
.headers
.get("Authorization"):
589 cherrypy
.response
.headers
["WWW-Authenticate"] = 'Bearer realm="Access to OSM site"'
590 cherrypy
.response
.status
= HTTPStatus
.UNAUTHORIZED
.value
591 elif args
and args
[0] == "sleep":
594 sleep_time
= int(args
[1])
596 cherrypy
.response
.status
= HTTPStatus
.FORBIDDEN
.value
597 return self
._format
_out
("Database already initialized")
598 thread_info
= cherrypy
.thread_data
600 time
.sleep(sleep_time
)
602 elif len(args
) >= 2 and args
[0] == "message":
604 return_text
= "<html><pre>{} ->\n".format(main_topic
)
606 if cherrypy
.request
.method
== 'POST':
607 to_send
= yaml
.load(cherrypy
.request
.body
)
608 for k
, v
in to_send
.items():
609 self
.engine
.msg
.write(main_topic
, k
, v
)
610 return_text
+= " {}: {}\n".format(k
, v
)
611 elif cherrypy
.request
.method
== 'GET':
612 for k
, v
in kwargs
.items():
613 self
.engine
.msg
.write(main_topic
, k
, yaml
.load(v
))
614 return_text
+= " {}: {}\n".format(k
, yaml
.load(v
))
615 except Exception as e
:
616 return_text
+= "Error: " + str(e
)
617 return_text
+= "</pre></html>\n"
621 "<html><pre>\nheaders:\n args: {}\n".format(args
) +
622 " kwargs: {}\n".format(kwargs
) +
623 " headers: {}\n".format(cherrypy
.request
.headers
) +
624 " path_info: {}\n".format(cherrypy
.request
.path_info
) +
625 " query_string: {}\n".format(cherrypy
.request
.query_string
) +
626 " session: {}\n".format(cherrypy
.session
) +
627 " cookie: {}\n".format(cherrypy
.request
.cookie
) +
628 " method: {}\n".format(cherrypy
.request
.method
) +
629 " session: {}\n".format(cherrypy
.session
.get('fieldname')) +
631 return_text
+= " length: {}\n".format(cherrypy
.request
.body
.length
)
632 if cherrypy
.request
.body
.length
:
633 return_text
+= " content: {}\n".format(
634 str(cherrypy
.request
.body
.read(int(cherrypy
.request
.headers
.get('Content-Length', 0)))))
636 return_text
+= "thread: {}\n".format(thread_info
)
637 return_text
+= "</pre></html>"
640 def _check_valid_url_method(self
, method
, *args
):
642 raise NbiException("URL must contain at least 'main_topic/version/topic'", HTTPStatus
.METHOD_NOT_ALLOWED
)
644 reference
= self
.valid_methods
648 if not isinstance(reference
, dict):
649 raise NbiException("URL contains unexpected extra items '{}'".format(arg
),
650 HTTPStatus
.METHOD_NOT_ALLOWED
)
653 reference
= reference
[arg
]
654 elif "<ID>" in reference
:
655 reference
= reference
["<ID>"]
656 elif "*" in reference
:
657 reference
= reference
["*"]
660 raise NbiException("Unexpected URL item {}".format(arg
), HTTPStatus
.METHOD_NOT_ALLOWED
)
661 if "TODO" in reference
and method
in reference
["TODO"]:
662 raise NbiException("Method {} not supported yet for this URL".format(method
), HTTPStatus
.NOT_IMPLEMENTED
)
663 elif "METHODS" in reference
and method
not in reference
["METHODS"]:
664 raise NbiException("Method {} not supported for this URL".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
668 def _set_location_header(main_topic
, version
, topic
, id):
670 Insert response header Location with the URL of created item base on URL params
677 # Use cherrypy.request.base for absoluted path and make use of request.header HOST just in case behind aNAT
678 cherrypy
.response
.headers
["Location"] = "/osm/{}/{}/{}/{}".format(main_topic
, version
, topic
, id)
682 def default(self
, main_topic
=None, version
=None, topic
=None, _id
=None, item
=None, *args
, **kwargs
):
691 if not main_topic
or not version
or not topic
:
692 raise NbiException("URL must contain at least 'main_topic/version/topic'",
693 HTTPStatus
.METHOD_NOT_ALLOWED
)
694 if main_topic
not in ("admin", "vnfpkgm", "nsd", "nslcm", "pdu", "nst", "nsilcm"):
695 raise NbiException("URL main_topic '{}' not supported".format(main_topic
),
696 HTTPStatus
.METHOD_NOT_ALLOWED
)
698 raise NbiException("URL version '{}' not supported".format(version
), HTTPStatus
.METHOD_NOT_ALLOWED
)
700 if kwargs
and "METHOD" in kwargs
and kwargs
["METHOD"] in ("PUT", "POST", "DELETE", "GET", "PATCH"):
701 method
= kwargs
.pop("METHOD")
703 method
= cherrypy
.request
.method
704 if kwargs
and "FORCE" in kwargs
:
705 force
= kwargs
.pop("FORCE")
708 self
._check
_valid
_url
_method
(method
, main_topic
, version
, topic
, _id
, item
, *args
)
709 if main_topic
== "admin" and topic
== "tokens":
710 return self
.token(method
, _id
, kwargs
)
712 # self.engine.load_dbase(cherrypy.request.app.config)
713 session
= self
.authenticator
.authorize()
714 indata
= self
._format
_in
(kwargs
)
716 if topic
== "subscriptions":
717 engine_topic
= main_topic
+ "_" + topic
721 if main_topic
== "nsd":
722 engine_topic
= "nsds"
723 elif main_topic
== "vnfpkgm":
724 engine_topic
= "vnfds"
725 elif main_topic
== "nslcm":
726 engine_topic
= "nsrs"
727 if topic
== "ns_lcm_op_occs":
728 engine_topic
= "nslcmops"
729 if topic
== "vnfrs" or topic
== "vnf_instances":
730 engine_topic
= "vnfrs"
731 elif main_topic
== "nst":
732 engine_topic
= "nsts"
733 elif main_topic
== "nsilcm":
734 engine_topic
= "nsis"
735 if topic
== "nsi_lcm_op_occs":
736 engine_topic
= "nsilcmops"
737 elif main_topic
== "pdu":
738 engine_topic
= "pdus"
739 if engine_topic
== "vims": # TODO this is for backward compatibility, it will remove in the future
740 engine_topic
= "vim_accounts"
743 if item
in ("nsd_content", "package_content", "artifacts", "vnfd", "nsd", "nst", "nst_content"):
744 if item
in ("vnfd", "nsd", "nst"):
748 elif item
== "artifacts":
752 file, _format
= self
.engine
.get_file(session
, engine_topic
, _id
, path
,
753 cherrypy
.request
.headers
.get("Accept"))
756 outdata
= self
.engine
.get_item_list(session
, engine_topic
, kwargs
)
758 outdata
= self
.engine
.get_item(session
, engine_topic
, _id
)
759 elif method
== "POST":
760 if topic
in ("ns_descriptors_content", "vnf_packages_content", "netslice_templates_content"):
761 _id
= cherrypy
.request
.headers
.get("Transaction-Id")
763 _id
= self
.engine
.new_item(rollback
, session
, engine_topic
, {}, None, cherrypy
.request
.headers
,
765 completed
= self
.engine
.upload_content(session
, engine_topic
, _id
, indata
, kwargs
,
766 cherrypy
.request
.headers
, force
=force
)
768 self
._set
_location
_header
(main_topic
, version
, topic
, _id
)
770 cherrypy
.response
.headers
["Transaction-Id"] = _id
771 outdata
= {"id": _id
}
772 elif topic
== "ns_instances_content":
774 _id
= self
.engine
.new_item(rollback
, session
, engine_topic
, indata
, kwargs
, force
=force
)
776 indata
["lcmOperationType"] = "instantiate"
777 indata
["nsInstanceId"] = _id
778 self
.engine
.new_item(rollback
, session
, "nslcmops", indata
, None)
779 self
._set
_location
_header
(main_topic
, version
, topic
, _id
)
780 outdata
= {"id": _id
}
781 elif topic
== "ns_instances" and item
:
782 indata
["lcmOperationType"] = item
783 indata
["nsInstanceId"] = _id
784 _id
= self
.engine
.new_item(rollback
, session
, "nslcmops", indata
, kwargs
)
785 self
._set
_location
_header
(main_topic
, version
, "ns_lcm_op_occs", _id
)
786 outdata
= {"id": _id
}
787 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
788 elif topic
== "netslice_instances_content":
789 # creates NetSlice_Instance_record (NSIR)
790 _id
= self
.engine
.new_item(rollback
, session
, engine_topic
, indata
, kwargs
, force
=force
)
791 self
._set
_location
_header
(main_topic
, version
, topic
, _id
)
792 indata
["lcmOperationType"] = "instantiate"
793 indata
["nsiInstanceId"] = _id
794 self
.engine
.new_item(rollback
, session
, "nsilcmops", indata
, kwargs
)
795 outdata
= {"id": _id
}
797 elif topic
== "netslice_instances" and item
:
798 indata
["lcmOperationType"] = item
799 indata
["nsiInstanceId"] = _id
800 _id
= self
.engine
.new_item(rollback
, session
, "nsilcmops", indata
, kwargs
)
801 self
._set
_location
_header
(main_topic
, version
, "nsi_lcm_op_occs", _id
)
802 outdata
= {"id": _id
}
803 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
805 _id
= self
.engine
.new_item(rollback
, session
, engine_topic
, indata
, kwargs
,
806 cherrypy
.request
.headers
, force
=force
)
807 self
._set
_location
_header
(main_topic
, version
, topic
, _id
)
808 outdata
= {"id": _id
}
809 # TODO form NsdInfo when topic in ("ns_descriptors", "vnf_packages")
810 cherrypy
.response
.status
= HTTPStatus
.CREATED
.value
812 elif method
== "DELETE":
814 outdata
= self
.engine
.del_item_list(session
, engine_topic
, kwargs
)
815 cherrypy
.response
.status
= HTTPStatus
.OK
.value
816 else: # len(args) > 1
817 delete_in_process
= False
818 if topic
== "ns_instances_content" and not force
:
820 "lcmOperationType": "terminate",
824 opp_id
= self
.engine
.new_item(rollback
, session
, "nslcmops", nslcmop_desc
, None)
826 delete_in_process
= True
827 outdata
= {"_id": opp_id
}
828 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
829 elif topic
== "netslice_instances_content" and not force
:
831 "lcmOperationType": "terminate",
832 "nsiInstanceId": _id
,
835 opp_id
= self
.engine
.new_item(rollback
, session
, "nsilcmops", nsilcmop_desc
, None)
837 delete_in_process
= True
838 outdata
= {"_id": opp_id
}
839 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
840 if not delete_in_process
:
841 self
.engine
.del_item(session
, engine_topic
, _id
, force
)
842 cherrypy
.response
.status
= HTTPStatus
.NO_CONTENT
.value
843 if engine_topic
in ("vim_accounts", "wim_accounts", "sdns"):
844 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
846 elif method
in ("PUT", "PATCH"):
848 if not indata
and not kwargs
:
849 raise NbiException("Nothing to update. Provide payload and/or query string",
850 HTTPStatus
.BAD_REQUEST
)
851 if item
in ("nsd_content", "package_content", "nst_content") and method
== "PUT":
852 completed
= self
.engine
.upload_content(session
, engine_topic
, _id
, indata
, kwargs
,
853 cherrypy
.request
.headers
, force
=force
)
855 cherrypy
.response
.headers
["Transaction-Id"] = id
857 self
.engine
.edit_item(session
, engine_topic
, _id
, indata
, kwargs
, force
=force
)
858 cherrypy
.response
.status
= HTTPStatus
.NO_CONTENT
.value
860 raise NbiException("Method {} not allowed".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
861 return self
._format
_out
(outdata
, session
, _format
)
862 except Exception as e
:
863 if isinstance(e
, (NbiException
, EngineException
, DbException
, FsException
, MsgException
, AuthException
,
865 http_code_value
= cherrypy
.response
.status
= e
.http_code
.value
866 http_code_name
= e
.http_code
.name
867 cherrypy
.log("Exception {}".format(e
))
869 http_code_value
= cherrypy
.response
.status
= HTTPStatus
.BAD_REQUEST
.value
# INTERNAL_SERVER_ERROR
870 cherrypy
.log("CRITICAL: Exception {}".format(e
), traceback
=True)
871 http_code_name
= HTTPStatus
.BAD_REQUEST
.name
872 if hasattr(outdata
, "close"): # is an open file
876 for rollback_item
in rollback
:
878 if rollback_item
.get("operation") == "set":
879 self
.engine
.db
.set_one(rollback_item
["topic"], {"_id": rollback_item
["_id"]},
880 rollback_item
["content"], fail_on_empty
=False)
882 self
.engine
.db
.del_one(rollback_item
["topic"], {"_id": rollback_item
["_id"]},
884 except Exception as e2
:
885 rollback_error_text
= "Rollback Exception {}: {}".format(rollback_item
, e2
)
886 cherrypy
.log(rollback_error_text
)
887 error_text
+= ". " + rollback_error_text
888 # if isinstance(e, MsgException):
889 # error_text = "{} has been '{}' but other modules cannot be informed because an error on bus".format(
890 # engine_topic[:-1], method, error_text)
892 "code": http_code_name
,
893 "status": http_code_value
,
894 "detail": error_text
,
896 return self
._format
_out
(problem_details
, session
)
897 # raise cherrypy.HTTPError(e.http_code.value, str(e))
900 def _start_service():
902 Callback function called when cherrypy.engine starts
903 Override configuration with env variables
904 Set database, storage, message configuration
905 Init database with admin/admin user password
907 cherrypy
.log
.error("Starting osm_nbi")
908 # update general cherrypy configuration
911 engine_config
= cherrypy
.tree
.apps
['/osm'].config
912 for k
, v
in environ
.items():
913 if not k
.startswith("OSMNBI_"):
915 k1
, _
, k2
= k
[7:].lower().partition("_")
919 # update static configuration
920 if k
== 'OSMNBI_STATIC_DIR':
921 engine_config
["/static"]['tools.staticdir.dir'] = v
922 engine_config
["/static"]['tools.staticdir.on'] = True
923 elif k
== 'OSMNBI_SOCKET_PORT' or k
== 'OSMNBI_SERVER_PORT':
924 update_dict
['server.socket_port'] = int(v
)
925 elif k
== 'OSMNBI_SOCKET_HOST' or k
== 'OSMNBI_SERVER_HOST':
926 update_dict
['server.socket_host'] = v
927 elif k1
in ("server", "test", "auth", "log"):
928 update_dict
[k1
+ '.' + k2
] = v
929 elif k1
in ("message", "database", "storage", "authentication"):
930 # k2 = k2.replace('_', '.')
931 if k2
in ("port", "db_port"):
932 engine_config
[k1
][k2
] = int(v
)
934 engine_config
[k1
][k2
] = v
936 except ValueError as e
:
937 cherrypy
.log
.error("Ignoring environ '{}': " + str(e
))
938 except Exception as e
:
939 cherrypy
.log
.warn("skipping environ '{}' on exception '{}'".format(k
, e
))
942 cherrypy
.config
.update(update_dict
)
943 engine_config
["global"].update(update_dict
)
946 log_format_simple
= "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(message)s"
947 log_formatter_simple
= logging
.Formatter(log_format_simple
, datefmt
='%Y-%m-%dT%H:%M:%S')
948 logger_server
= logging
.getLogger("cherrypy.error")
949 logger_access
= logging
.getLogger("cherrypy.access")
950 logger_cherry
= logging
.getLogger("cherrypy")
951 logger_nbi
= logging
.getLogger("nbi")
953 if "log.file" in engine_config
["global"]:
954 file_handler
= logging
.handlers
.RotatingFileHandler(engine_config
["global"]["log.file"],
955 maxBytes
=100e6
, backupCount
=9, delay
=0)
956 file_handler
.setFormatter(log_formatter_simple
)
957 logger_cherry
.addHandler(file_handler
)
958 logger_nbi
.addHandler(file_handler
)
959 # log always to standard output
960 for format_
, logger
in {"nbi.server %(filename)s:%(lineno)s": logger_server
,
961 "nbi.access %(filename)s:%(lineno)s": logger_access
,
962 "%(name)s %(filename)s:%(lineno)s": logger_nbi
964 log_format_cherry
= "%(asctime)s %(levelname)s {} %(message)s".format(format_
)
965 log_formatter_cherry
= logging
.Formatter(log_format_cherry
, datefmt
='%Y-%m-%dT%H:%M:%S')
966 str_handler
= logging
.StreamHandler()
967 str_handler
.setFormatter(log_formatter_cherry
)
968 logger
.addHandler(str_handler
)
970 if engine_config
["global"].get("log.level"):
971 logger_cherry
.setLevel(engine_config
["global"]["log.level"])
972 logger_nbi
.setLevel(engine_config
["global"]["log.level"])
974 # logging other modules
975 for k1
, logname
in {"message": "nbi.msg", "database": "nbi.db", "storage": "nbi.fs"}.items():
976 engine_config
[k1
]["logger_name"] = logname
977 logger_module
= logging
.getLogger(logname
)
978 if "logfile" in engine_config
[k1
]:
979 file_handler
= logging
.handlers
.RotatingFileHandler(engine_config
[k1
]["logfile"],
980 maxBytes
=100e6
, backupCount
=9, delay
=0)
981 file_handler
.setFormatter(log_formatter_simple
)
982 logger_module
.addHandler(file_handler
)
983 if "loglevel" in engine_config
[k1
]:
984 logger_module
.setLevel(engine_config
[k1
]["loglevel"])
985 # TODO add more entries, e.g.: storage
986 cherrypy
.tree
.apps
['/osm'].root
.engine
.start(engine_config
)
987 cherrypy
.tree
.apps
['/osm'].root
.authenticator
.start(engine_config
)
988 cherrypy
.tree
.apps
['/osm'].root
.engine
.init_db(target_version
=database_version
)
989 cherrypy
.tree
.apps
['/osm'].root
.authenticator
.init_db(target_version
=auth_database_version
)
991 # load and print version. Ignore possible errors, e.g. file not found
993 with
open("{}/version".format(engine_config
["/static"]['tools.staticdir.dir'])) as version_file
:
994 version_data
= version_file
.read()
995 cherrypy
.log
.error("Starting OSM NBI Version: {}".format(version_data
.replace("\n", " ")))
1000 def _stop_service():
1002 Callback function called when cherrypy.engine stops
1003 TODO: Ending database connections.
1005 cherrypy
.tree
.apps
['/osm'].root
.engine
.stop()
1006 cherrypy
.log
.error("Stopping osm_nbi")
1009 def nbi(config_file
):
1012 # #'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
1013 # 'tools.sessions.on': True,
1014 # 'tools.response_headers.on': True,
1015 # # 'tools.response_headers.headers': [('Content-Type', 'text/plain')],
1018 # cherrypy.Server.ssl_module = 'builtin'
1019 # cherrypy.Server.ssl_certificate = "http/cert.pem"
1020 # cherrypy.Server.ssl_private_key = "http/privkey.pem"
1021 # cherrypy.Server.thread_pool = 10
1022 # cherrypy.config.update({'Server.socket_port': config["port"], 'Server.socket_host': config["host"]})
1024 # cherrypy.config.update({'tools.auth_basic.on': True,
1025 # 'tools.auth_basic.realm': 'localhost',
1026 # 'tools.auth_basic.checkpassword': validate_password})
1027 cherrypy
.engine
.subscribe('start', _start_service
)
1028 cherrypy
.engine
.subscribe('stop', _stop_service
)
1029 cherrypy
.quickstart(Server(), '/osm', config_file
)
1033 print("""Usage: {} [options]
1034 -c|--config [configuration_file]: loads the configuration file (default: ./nbi.cfg)
1035 -h|--help: shows this help
1036 """.format(sys
.argv
[0]))
1037 # --log-socket-host HOST: send logs to this host")
1038 # --log-socket-port PORT: send logs using this port (default: 9022)")
1041 if __name__
== '__main__':
1043 # load parameters and configuration
1044 opts
, args
= getopt
.getopt(sys
.argv
[1:], "hvc:", ["config=", "help"])
1045 # TODO add "log-socket-host=", "log-socket-port=", "log-file="
1048 if o
in ("-h", "--help"):
1051 elif o
in ("-c", "--config"):
1053 # elif o == "--log-socket-port":
1054 # log_socket_port = a
1055 # elif o == "--log-socket-host":
1056 # log_socket_host = a
1057 # elif o == "--log-file":
1060 assert False, "Unhandled option"
1062 if not path
.isfile(config_file
):
1063 print("configuration file '{}' that not exist".format(config_file
), file=sys
.stderr
)
1066 for config_file
in (__file__
[:__file__
.rfind(".")] + ".cfg", "./nbi.cfg", "/etc/osm/nbi.cfg"):
1067 if path
.isfile(config_file
):
1070 print("No configuration file 'nbi.cfg' found neither at local folder nor at /etc/osm/", file=sys
.stderr
)
1073 except getopt
.GetoptError
as e
:
1074 print(str(e
), file=sys
.stderr
)