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 "vims": {"METHODS": ("GET", "POST"),
217 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
219 "vim_accounts": {"METHODS": ("GET", "POST"),
220 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
222 "wim_accounts": {"METHODS": ("GET", "POST"),
223 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
225 "sdns": {"METHODS": ("GET", "POST"),
226 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
232 "pdu_descriptors": {"METHODS": ("GET", "POST"),
233 "<ID>": {"METHODS": ("GET", "POST", "DELETE", "PATCH", "PUT")}
239 "ns_descriptors_content": {"METHODS": ("GET", "POST"),
240 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
242 "ns_descriptors": {"METHODS": ("GET", "POST"),
243 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"),
244 "nsd_content": {"METHODS": ("GET", "PUT")},
245 "nsd": {"METHODS": "GET"}, # descriptor inside package
246 "artifacts": {"*": {"METHODS": "GET"}}
249 "pnf_descriptors": {"TODO": ("GET", "POST"),
250 "<ID>": {"TODO": ("GET", "DELETE", "PATCH"),
251 "pnfd_content": {"TODO": ("GET", "PUT")}
254 "subscriptions": {"TODO": ("GET", "POST"),
255 "<ID>": {"TODO": ("GET", "DELETE")}
261 "vnf_packages_content": {"METHODS": ("GET", "POST"),
262 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
264 "vnf_packages": {"METHODS": ("GET", "POST"),
265 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"), # GET: vnfPkgInfo
266 "package_content": {"METHODS": ("GET", "PUT"), # package
267 "upload_from_uri": {"TODO": "POST"}
269 "vnfd": {"METHODS": "GET"}, # descriptor inside package
270 "artifacts": {"*": {"METHODS": "GET"}}
273 "subscriptions": {"TODO": ("GET", "POST"),
274 "<ID>": {"TODO": ("GET", "DELETE")}
280 "ns_instances_content": {"METHODS": ("GET", "POST"),
281 "<ID>": {"METHODS": ("GET", "DELETE")}
283 "ns_instances": {"METHODS": ("GET", "POST"),
284 "<ID>": {"METHODS": ("GET", "DELETE"),
285 "scale": {"METHODS": "POST"},
286 "terminate": {"METHODS": "POST"},
287 "instantiate": {"METHODS": "POST"},
288 "action": {"METHODS": "POST"},
291 "ns_lcm_op_occs": {"METHODS": "GET",
292 "<ID>": {"METHODS": "GET"},
294 "vnfrs": {"METHODS": ("GET"),
295 "<ID>": {"METHODS": ("GET")}
297 "vnf_instances": {"METHODS": ("GET"),
298 "<ID>": {"METHODS": ("GET")}
304 "netslice_templates_content": {"METHODS": ("GET", "POST"),
305 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
307 "netslice_templates": {"METHODS": ("GET", "POST"),
308 "<ID>": {"METHODS": ("GET", "DELETE"), "TODO": "PATCH",
309 "nst_content": {"METHODS": ("GET", "PUT")},
310 "nst": {"METHODS": "GET"}, # descriptor inside package
311 "artifacts": {"*": {"METHODS": "GET"}}
314 "subscriptions": {"TODO": ("GET", "POST"),
315 "<ID>": {"TODO": ("GET", "DELETE")}
321 "netslice_instances_content": {"METHODS": ("GET", "POST"),
322 "<ID>": {"METHODS": ("GET", "DELETE")}
324 "netslice_instances": {"METHODS": ("GET", "POST"),
325 "<ID>": {"METHODS": ("GET", "DELETE"),
326 "terminate": {"METHODS": "POST"},
327 "instantiate": {"METHODS": "POST"},
328 "action": {"METHODS": "POST"},
331 "nsi_lcm_op_occs": {"METHODS": "GET",
332 "<ID>": {"METHODS": "GET"},
338 def _format_in(self
, kwargs
):
341 if cherrypy
.request
.body
.length
:
342 error_text
= "Invalid input format "
344 if "Content-Type" in cherrypy
.request
.headers
:
345 if "application/json" in cherrypy
.request
.headers
["Content-Type"]:
346 error_text
= "Invalid json format "
347 indata
= json
.load(self
.reader(cherrypy
.request
.body
))
348 cherrypy
.request
.headers
.pop("Content-File-MD5", None)
349 elif "application/yaml" in cherrypy
.request
.headers
["Content-Type"]:
350 error_text
= "Invalid yaml format "
351 indata
= yaml
.load(cherrypy
.request
.body
)
352 cherrypy
.request
.headers
.pop("Content-File-MD5", None)
353 elif "application/binary" in cherrypy
.request
.headers
["Content-Type"] or \
354 "application/gzip" in cherrypy
.request
.headers
["Content-Type"] or \
355 "application/zip" in cherrypy
.request
.headers
["Content-Type"] or \
356 "text/plain" in cherrypy
.request
.headers
["Content-Type"]:
357 indata
= cherrypy
.request
.body
# .read()
358 elif "multipart/form-data" in cherrypy
.request
.headers
["Content-Type"]:
359 if "descriptor_file" in kwargs
:
360 filecontent
= kwargs
.pop("descriptor_file")
361 if not filecontent
.file:
362 raise NbiException("empty file or content", HTTPStatus
.BAD_REQUEST
)
363 indata
= filecontent
.file # .read()
364 if filecontent
.content_type
.value
:
365 cherrypy
.request
.headers
["Content-Type"] = filecontent
.content_type
.value
367 # raise cherrypy.HTTPError(HTTPStatus.Not_Acceptable,
368 # "Only 'Content-Type' of type 'application/json' or
369 # 'application/yaml' for input format are available")
370 error_text
= "Invalid yaml format "
371 indata
= yaml
.load(cherrypy
.request
.body
)
372 cherrypy
.request
.headers
.pop("Content-File-MD5", None)
374 error_text
= "Invalid yaml format "
375 indata
= yaml
.load(cherrypy
.request
.body
)
376 cherrypy
.request
.headers
.pop("Content-File-MD5", None)
381 if cherrypy
.request
.headers
.get("Query-String-Format") == "yaml":
384 for k
, v
in kwargs
.items():
385 if isinstance(v
, str):
390 kwargs
[k
] = yaml
.load(v
)
393 elif k
.endswith(".gt") or k
.endswith(".lt") or k
.endswith(".gte") or k
.endswith(".lte"):
401 elif v
.find(",") > 0:
402 kwargs
[k
] = v
.split(",")
403 elif isinstance(v
, (list, tuple)):
404 for index
in range(0, len(v
)):
409 v
[index
] = yaml
.load(v
[index
])
414 except (ValueError, yaml
.YAMLError
) as exc
:
415 raise NbiException(error_text
+ str(exc
), HTTPStatus
.BAD_REQUEST
)
416 except KeyError as exc
:
417 raise NbiException("Query string error: " + str(exc
), HTTPStatus
.BAD_REQUEST
)
418 except Exception as exc
:
419 raise NbiException(error_text
+ str(exc
), HTTPStatus
.BAD_REQUEST
)
422 def _format_out(data
, session
=None, _format
=None):
424 return string of dictionary data according to requested json, yaml, xml. By default json
425 :param data: response to be sent. Can be a dict, text or file
427 :param _format: The format to be set as Content-Type ir data is a file
430 accept
= cherrypy
.request
.headers
.get("Accept")
432 if accept
and "text/html" in accept
:
433 return html
.format(data
, cherrypy
.request
, cherrypy
.response
, session
)
434 # cherrypy.response.status = HTTPStatus.NO_CONTENT.value
436 elif hasattr(data
, "read"): # file object
438 cherrypy
.response
.headers
["Content-Type"] = _format
439 elif "b" in data
.mode
: # binariy asssumig zip
440 cherrypy
.response
.headers
["Content-Type"] = 'application/zip'
442 cherrypy
.response
.headers
["Content-Type"] = 'text/plain'
443 # TODO check that cherrypy close file. If not implement pending things to close per thread next
446 if "application/json" in accept
:
447 cherrypy
.response
.headers
["Content-Type"] = 'application/json; charset=utf-8'
448 a
= json
.dumps(data
, indent
=4) + "\n"
449 return a
.encode("utf8")
450 elif "text/html" in accept
:
451 return html
.format(data
, cherrypy
.request
, cherrypy
.response
, session
)
453 elif "application/yaml" in accept
or "*/*" in accept
or "text/plain" in accept
:
455 # if there is not any valid accept, raise an error. But if response is already an error, format in yaml
456 elif cherrypy
.response
.status
>= 400:
457 raise cherrypy
.HTTPError(HTTPStatus
.NOT_ACCEPTABLE
.value
,
458 "Only 'Accept' of type 'application/json' or 'application/yaml' "
459 "for output format are available")
460 cherrypy
.response
.headers
["Content-Type"] = 'application/yaml'
461 return yaml
.safe_dump(data
, explicit_start
=True, indent
=4, default_flow_style
=False, tags
=False,
462 encoding
='utf-8', allow_unicode
=True) # , canonical=True, default_style='"'
465 def index(self
, *args
, **kwargs
):
468 if cherrypy
.request
.method
== "GET":
469 session
= self
.authenticator
.authorize()
470 outdata
= "Index page"
472 raise cherrypy
.HTTPError(HTTPStatus
.METHOD_NOT_ALLOWED
.value
,
473 "Method {} not allowed for tokens".format(cherrypy
.request
.method
))
475 return self
._format
_out
(outdata
, session
)
477 except (EngineException
, AuthException
) as e
:
478 cherrypy
.log("index Exception {}".format(e
))
479 cherrypy
.response
.status
= e
.http_code
.value
480 return self
._format
_out
("Welcome to OSM!", session
)
483 def version(self
, *args
, **kwargs
):
484 # TODO consider to remove and provide version using the static version file
485 global __version__
, version_date
487 if cherrypy
.request
.method
!= "GET":
488 raise NbiException("Only method GET is allowed", HTTPStatus
.METHOD_NOT_ALLOWED
)
490 raise NbiException("Invalid URL or query string for version", HTTPStatus
.METHOD_NOT_ALLOWED
)
491 return __version__
+ " " + version_date
492 except NbiException
as e
:
493 cherrypy
.response
.status
= e
.http_code
.value
495 "code": e
.http_code
.name
,
496 "status": e
.http_code
.value
,
499 return self
._format
_out
(problem_details
, None)
502 def token(self
, method
, token_id
=None, kwargs
=None):
504 # self.engine.load_dbase(cherrypy.request.app.config)
505 indata
= self
._format
_in
(kwargs
)
506 if not isinstance(indata
, dict):
507 raise NbiException("Expected application/yaml or application/json Content-Type", HTTPStatus
.BAD_REQUEST
)
510 session
= self
.authenticator
.authorize()
512 outdata
= self
.authenticator
.get_token(session
, token_id
)
514 outdata
= self
.authenticator
.get_token_list(session
)
515 elif method
== "POST":
517 session
= self
.authenticator
.authorize()
521 indata
.update(kwargs
)
522 outdata
= self
.authenticator
.new_token(session
, indata
, cherrypy
.request
.remote
)
524 cherrypy
.session
['Authorization'] = outdata
["_id"]
525 self
._set
_location
_header
("admin", "v1", "tokens", outdata
["_id"])
526 # cherrypy.response.cookie["Authorization"] = outdata["id"]
527 # cherrypy.response.cookie["Authorization"]['expires'] = 3600
528 elif method
== "DELETE":
529 if not token_id
and "id" in kwargs
:
530 token_id
= kwargs
["id"]
532 session
= self
.authenticator
.authorize()
533 token_id
= session
["_id"]
534 outdata
= self
.authenticator
.del_token(token_id
)
536 cherrypy
.session
['Authorization'] = "logout"
537 # cherrypy.response.cookie["Authorization"] = token_id
538 # cherrypy.response.cookie["Authorization"]['expires'] = 0
540 raise NbiException("Method {} not allowed for token".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
541 return self
._format
_out
(outdata
, session
)
542 except (NbiException
, EngineException
, DbException
, AuthException
) as e
:
543 cherrypy
.log("tokens Exception {}".format(e
))
544 cherrypy
.response
.status
= e
.http_code
.value
546 "code": e
.http_code
.name
,
547 "status": e
.http_code
.value
,
550 return self
._format
_out
(problem_details
, session
)
553 def test(self
, *args
, **kwargs
):
555 if args
and args
[0] == "help":
556 return "<html><pre>\ninit\nfile/<name> download file\ndb-clear/table\nfs-clear[/folder]\nlogin\nlogin2\n"\
557 "sleep/<time>\nmessage/topic\n</pre></html>"
559 elif args
and args
[0] == "init":
561 # self.engine.load_dbase(cherrypy.request.app.config)
562 self
.engine
.create_admin()
563 return "Done. User 'admin', password 'admin' created"
565 cherrypy
.response
.status
= HTTPStatus
.FORBIDDEN
.value
566 return self
._format
_out
("Database already initialized")
567 elif args
and args
[0] == "file":
568 return cherrypy
.lib
.static
.serve_file(cherrypy
.tree
.apps
['/osm'].config
["storage"]["path"] + "/" + args
[1],
569 "text/plain", "attachment")
570 elif args
and args
[0] == "file2":
571 f_path
= cherrypy
.tree
.apps
['/osm'].config
["storage"]["path"] + "/" + args
[1]
572 f
= open(f_path
, "r")
573 cherrypy
.response
.headers
["Content-type"] = "text/plain"
576 elif len(args
) == 2 and args
[0] == "db-clear":
577 deleted_info
= self
.engine
.db
.del_list(args
[1], kwargs
)
578 return "{} {} deleted\n".format(deleted_info
["deleted"], args
[1])
579 elif len(args
) and args
[0] == "fs-clear":
583 folders
= self
.engine
.fs
.dir_ls(".")
584 for folder
in folders
:
585 self
.engine
.fs
.file_delete(folder
)
586 return ",".join(folders
) + " folders deleted\n"
587 elif args
and args
[0] == "login":
588 if not cherrypy
.request
.headers
.get("Authorization"):
589 cherrypy
.response
.headers
["WWW-Authenticate"] = 'Basic realm="Access to OSM site", charset="UTF-8"'
590 cherrypy
.response
.status
= HTTPStatus
.UNAUTHORIZED
.value
591 elif args
and args
[0] == "login2":
592 if not cherrypy
.request
.headers
.get("Authorization"):
593 cherrypy
.response
.headers
["WWW-Authenticate"] = 'Bearer realm="Access to OSM site"'
594 cherrypy
.response
.status
= HTTPStatus
.UNAUTHORIZED
.value
595 elif args
and args
[0] == "sleep":
598 sleep_time
= int(args
[1])
600 cherrypy
.response
.status
= HTTPStatus
.FORBIDDEN
.value
601 return self
._format
_out
("Database already initialized")
602 thread_info
= cherrypy
.thread_data
604 time
.sleep(sleep_time
)
606 elif len(args
) >= 2 and args
[0] == "message":
608 return_text
= "<html><pre>{} ->\n".format(main_topic
)
610 if cherrypy
.request
.method
== 'POST':
611 to_send
= yaml
.load(cherrypy
.request
.body
)
612 for k
, v
in to_send
.items():
613 self
.engine
.msg
.write(main_topic
, k
, v
)
614 return_text
+= " {}: {}\n".format(k
, v
)
615 elif cherrypy
.request
.method
== 'GET':
616 for k
, v
in kwargs
.items():
617 self
.engine
.msg
.write(main_topic
, k
, yaml
.load(v
))
618 return_text
+= " {}: {}\n".format(k
, yaml
.load(v
))
619 except Exception as e
:
620 return_text
+= "Error: " + str(e
)
621 return_text
+= "</pre></html>\n"
625 "<html><pre>\nheaders:\n args: {}\n".format(args
) +
626 " kwargs: {}\n".format(kwargs
) +
627 " headers: {}\n".format(cherrypy
.request
.headers
) +
628 " path_info: {}\n".format(cherrypy
.request
.path_info
) +
629 " query_string: {}\n".format(cherrypy
.request
.query_string
) +
630 " session: {}\n".format(cherrypy
.session
) +
631 " cookie: {}\n".format(cherrypy
.request
.cookie
) +
632 " method: {}\n".format(cherrypy
.request
.method
) +
633 " session: {}\n".format(cherrypy
.session
.get('fieldname')) +
635 return_text
+= " length: {}\n".format(cherrypy
.request
.body
.length
)
636 if cherrypy
.request
.body
.length
:
637 return_text
+= " content: {}\n".format(
638 str(cherrypy
.request
.body
.read(int(cherrypy
.request
.headers
.get('Content-Length', 0)))))
640 return_text
+= "thread: {}\n".format(thread_info
)
641 return_text
+= "</pre></html>"
644 def _check_valid_url_method(self
, method
, *args
):
646 raise NbiException("URL must contain at least 'main_topic/version/topic'", HTTPStatus
.METHOD_NOT_ALLOWED
)
648 reference
= self
.valid_methods
652 if not isinstance(reference
, dict):
653 raise NbiException("URL contains unexpected extra items '{}'".format(arg
),
654 HTTPStatus
.METHOD_NOT_ALLOWED
)
657 reference
= reference
[arg
]
658 elif "<ID>" in reference
:
659 reference
= reference
["<ID>"]
660 elif "*" in reference
:
661 reference
= reference
["*"]
664 raise NbiException("Unexpected URL item {}".format(arg
), HTTPStatus
.METHOD_NOT_ALLOWED
)
665 if "TODO" in reference
and method
in reference
["TODO"]:
666 raise NbiException("Method {} not supported yet for this URL".format(method
), HTTPStatus
.NOT_IMPLEMENTED
)
667 elif "METHODS" in reference
and method
not in reference
["METHODS"]:
668 raise NbiException("Method {} not supported for this URL".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
672 def _set_location_header(main_topic
, version
, topic
, id):
674 Insert response header Location with the URL of created item base on URL params
681 # Use cherrypy.request.base for absoluted path and make use of request.header HOST just in case behind aNAT
682 cherrypy
.response
.headers
["Location"] = "/osm/{}/{}/{}/{}".format(main_topic
, version
, topic
, id)
686 def default(self
, main_topic
=None, version
=None, topic
=None, _id
=None, item
=None, *args
, **kwargs
):
695 if not main_topic
or not version
or not topic
:
696 raise NbiException("URL must contain at least 'main_topic/version/topic'",
697 HTTPStatus
.METHOD_NOT_ALLOWED
)
698 if main_topic
not in ("admin", "vnfpkgm", "nsd", "nslcm", "pdu", "nst", "nsilcm"):
699 raise NbiException("URL main_topic '{}' not supported".format(main_topic
),
700 HTTPStatus
.METHOD_NOT_ALLOWED
)
702 raise NbiException("URL version '{}' not supported".format(version
), HTTPStatus
.METHOD_NOT_ALLOWED
)
704 if kwargs
and "METHOD" in kwargs
and kwargs
["METHOD"] in ("PUT", "POST", "DELETE", "GET", "PATCH"):
705 method
= kwargs
.pop("METHOD")
707 method
= cherrypy
.request
.method
708 if kwargs
and "FORCE" in kwargs
:
709 force
= kwargs
.pop("FORCE")
712 self
._check
_valid
_url
_method
(method
, main_topic
, version
, topic
, _id
, item
, *args
)
713 if main_topic
== "admin" and topic
== "tokens":
714 return self
.token(method
, _id
, kwargs
)
716 # self.engine.load_dbase(cherrypy.request.app.config)
717 session
= self
.authenticator
.authorize()
718 indata
= self
._format
_in
(kwargs
)
720 if topic
== "subscriptions":
721 engine_topic
= main_topic
+ "_" + topic
725 if main_topic
== "nsd":
726 engine_topic
= "nsds"
727 elif main_topic
== "vnfpkgm":
728 engine_topic
= "vnfds"
729 elif main_topic
== "nslcm":
730 engine_topic
= "nsrs"
731 if topic
== "ns_lcm_op_occs":
732 engine_topic
= "nslcmops"
733 if topic
== "vnfrs" or topic
== "vnf_instances":
734 engine_topic
= "vnfrs"
735 elif main_topic
== "nst":
736 engine_topic
= "nsts"
737 elif main_topic
== "nsilcm":
738 engine_topic
= "nsis"
739 if topic
== "nsi_lcm_op_occs":
740 engine_topic
= "nsilcmops"
741 elif main_topic
== "pdu":
742 engine_topic
= "pdus"
743 if engine_topic
== "vims": # TODO this is for backward compatibility, it will remove in the future
744 engine_topic
= "vim_accounts"
747 if item
in ("nsd_content", "package_content", "artifacts", "vnfd", "nsd", "nst", "nst_content"):
748 if item
in ("vnfd", "nsd", "nst"):
752 elif item
== "artifacts":
756 file, _format
= self
.engine
.get_file(session
, engine_topic
, _id
, path
,
757 cherrypy
.request
.headers
.get("Accept"))
760 outdata
= self
.engine
.get_item_list(session
, engine_topic
, kwargs
)
762 outdata
= self
.engine
.get_item(session
, engine_topic
, _id
)
763 elif method
== "POST":
764 if topic
in ("ns_descriptors_content", "vnf_packages_content", "netslice_templates_content"):
765 _id
= cherrypy
.request
.headers
.get("Transaction-Id")
767 _id
= self
.engine
.new_item(rollback
, session
, engine_topic
, {}, None, cherrypy
.request
.headers
,
769 completed
= self
.engine
.upload_content(session
, engine_topic
, _id
, indata
, kwargs
,
770 cherrypy
.request
.headers
, force
=force
)
772 self
._set
_location
_header
(main_topic
, version
, topic
, _id
)
774 cherrypy
.response
.headers
["Transaction-Id"] = _id
775 outdata
= {"id": _id
}
776 elif topic
== "ns_instances_content":
778 _id
= self
.engine
.new_item(rollback
, session
, engine_topic
, indata
, kwargs
, force
=force
)
780 indata
["lcmOperationType"] = "instantiate"
781 indata
["nsInstanceId"] = _id
782 self
.engine
.new_item(rollback
, session
, "nslcmops", indata
, None)
783 self
._set
_location
_header
(main_topic
, version
, topic
, _id
)
784 outdata
= {"id": _id
}
785 elif topic
== "ns_instances" and item
:
786 indata
["lcmOperationType"] = item
787 indata
["nsInstanceId"] = _id
788 _id
= self
.engine
.new_item(rollback
, session
, "nslcmops", indata
, kwargs
)
789 self
._set
_location
_header
(main_topic
, version
, "ns_lcm_op_occs", _id
)
790 outdata
= {"id": _id
}
791 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
792 elif topic
== "netslice_instances_content":
793 # creates NetSlice_Instance_record (NSIR)
794 _id
= self
.engine
.new_item(rollback
, session
, engine_topic
, indata
, kwargs
, force
=force
)
795 self
._set
_location
_header
(main_topic
, version
, topic
, _id
)
796 indata
["lcmOperationType"] = "instantiate"
797 indata
["nsiInstanceId"] = _id
798 self
.engine
.new_item(rollback
, session
, "nsilcmops", indata
, kwargs
)
799 outdata
= {"id": _id
}
801 elif topic
== "netslice_instances" and item
:
802 indata
["lcmOperationType"] = item
803 indata
["nsiInstanceId"] = _id
804 _id
= self
.engine
.new_item(rollback
, session
, "nsilcmops", indata
, kwargs
)
805 self
._set
_location
_header
(main_topic
, version
, "nsi_lcm_op_occs", _id
)
806 outdata
= {"id": _id
}
807 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
809 _id
= self
.engine
.new_item(rollback
, session
, engine_topic
, indata
, kwargs
,
810 cherrypy
.request
.headers
, force
=force
)
811 self
._set
_location
_header
(main_topic
, version
, topic
, _id
)
812 outdata
= {"id": _id
}
813 # TODO form NsdInfo when topic in ("ns_descriptors", "vnf_packages")
814 cherrypy
.response
.status
= HTTPStatus
.CREATED
.value
816 elif method
== "DELETE":
818 outdata
= self
.engine
.del_item_list(session
, engine_topic
, kwargs
)
819 cherrypy
.response
.status
= HTTPStatus
.OK
.value
820 else: # len(args) > 1
821 delete_in_process
= False
822 if topic
== "ns_instances_content" and not force
:
824 "lcmOperationType": "terminate",
828 opp_id
= self
.engine
.new_item(rollback
, session
, "nslcmops", nslcmop_desc
, None)
830 delete_in_process
= True
831 outdata
= {"_id": opp_id
}
832 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
833 elif topic
== "netslice_instances_content" and not force
:
835 "lcmOperationType": "terminate",
836 "nsiInstanceId": _id
,
839 opp_id
= self
.engine
.new_item(rollback
, session
, "nsilcmops", nsilcmop_desc
, None)
841 delete_in_process
= True
842 outdata
= {"_id": opp_id
}
843 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
844 if not delete_in_process
:
845 self
.engine
.del_item(session
, engine_topic
, _id
, force
)
846 cherrypy
.response
.status
= HTTPStatus
.NO_CONTENT
.value
847 if engine_topic
in ("vim_accounts", "wim_accounts", "sdns"):
848 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
850 elif method
in ("PUT", "PATCH"):
852 if not indata
and not kwargs
:
853 raise NbiException("Nothing to update. Provide payload and/or query string",
854 HTTPStatus
.BAD_REQUEST
)
855 if item
in ("nsd_content", "package_content", "nst_content") and method
== "PUT":
856 completed
= self
.engine
.upload_content(session
, engine_topic
, _id
, indata
, kwargs
,
857 cherrypy
.request
.headers
, force
=force
)
859 cherrypy
.response
.headers
["Transaction-Id"] = id
861 self
.engine
.edit_item(session
, engine_topic
, _id
, indata
, kwargs
, force
=force
)
862 cherrypy
.response
.status
= HTTPStatus
.NO_CONTENT
.value
864 raise NbiException("Method {} not allowed".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
865 return self
._format
_out
(outdata
, session
, _format
)
866 except Exception as e
:
867 if isinstance(e
, (NbiException
, EngineException
, DbException
, FsException
, MsgException
, AuthException
,
869 http_code_value
= cherrypy
.response
.status
= e
.http_code
.value
870 http_code_name
= e
.http_code
.name
871 cherrypy
.log("Exception {}".format(e
))
873 http_code_value
= cherrypy
.response
.status
= HTTPStatus
.BAD_REQUEST
.value
# INTERNAL_SERVER_ERROR
874 cherrypy
.log("CRITICAL: Exception {}".format(e
), traceback
=True)
875 http_code_name
= HTTPStatus
.BAD_REQUEST
.name
876 if hasattr(outdata
, "close"): # is an open file
880 for rollback_item
in rollback
:
882 if rollback_item
.get("operation") == "set":
883 self
.engine
.db
.set_one(rollback_item
["topic"], {"_id": rollback_item
["_id"]},
884 rollback_item
["content"], fail_on_empty
=False)
886 self
.engine
.db
.del_one(rollback_item
["topic"], {"_id": rollback_item
["_id"]},
888 except Exception as e2
:
889 rollback_error_text
= "Rollback Exception {}: {}".format(rollback_item
, e2
)
890 cherrypy
.log(rollback_error_text
)
891 error_text
+= ". " + rollback_error_text
892 # if isinstance(e, MsgException):
893 # error_text = "{} has been '{}' but other modules cannot be informed because an error on bus".format(
894 # engine_topic[:-1], method, error_text)
896 "code": http_code_name
,
897 "status": http_code_value
,
898 "detail": error_text
,
900 return self
._format
_out
(problem_details
, session
)
901 # raise cherrypy.HTTPError(e.http_code.value, str(e))
904 def _start_service():
906 Callback function called when cherrypy.engine starts
907 Override configuration with env variables
908 Set database, storage, message configuration
909 Init database with admin/admin user password
912 global subscription_thread
913 cherrypy
.log
.error("Starting osm_nbi")
914 # update general cherrypy configuration
917 engine_config
= cherrypy
.tree
.apps
['/osm'].config
918 for k
, v
in environ
.items():
919 if not k
.startswith("OSMNBI_"):
921 k1
, _
, k2
= k
[7:].lower().partition("_")
925 # update static configuration
926 if k
== 'OSMNBI_STATIC_DIR':
927 engine_config
["/static"]['tools.staticdir.dir'] = v
928 engine_config
["/static"]['tools.staticdir.on'] = True
929 elif k
== 'OSMNBI_SOCKET_PORT' or k
== 'OSMNBI_SERVER_PORT':
930 update_dict
['server.socket_port'] = int(v
)
931 elif k
== 'OSMNBI_SOCKET_HOST' or k
== 'OSMNBI_SERVER_HOST':
932 update_dict
['server.socket_host'] = v
933 elif k1
in ("server", "test", "auth", "log"):
934 update_dict
[k1
+ '.' + k2
] = v
935 elif k1
in ("message", "database", "storage", "authentication"):
936 # k2 = k2.replace('_', '.')
937 if k2
in ("port", "db_port"):
938 engine_config
[k1
][k2
] = int(v
)
940 engine_config
[k1
][k2
] = v
942 except ValueError as e
:
943 cherrypy
.log
.error("Ignoring environ '{}': " + str(e
))
944 except Exception as e
:
945 cherrypy
.log
.warn("skipping environ '{}' on exception '{}'".format(k
, e
))
948 cherrypy
.config
.update(update_dict
)
949 engine_config
["global"].update(update_dict
)
952 log_format_simple
= "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(message)s"
953 log_formatter_simple
= logging
.Formatter(log_format_simple
, datefmt
='%Y-%m-%dT%H:%M:%S')
954 logger_server
= logging
.getLogger("cherrypy.error")
955 logger_access
= logging
.getLogger("cherrypy.access")
956 logger_cherry
= logging
.getLogger("cherrypy")
957 logger_nbi
= logging
.getLogger("nbi")
959 if "log.file" in engine_config
["global"]:
960 file_handler
= logging
.handlers
.RotatingFileHandler(engine_config
["global"]["log.file"],
961 maxBytes
=100e6
, backupCount
=9, delay
=0)
962 file_handler
.setFormatter(log_formatter_simple
)
963 logger_cherry
.addHandler(file_handler
)
964 logger_nbi
.addHandler(file_handler
)
965 # log always to standard output
966 for format_
, logger
in {"nbi.server %(filename)s:%(lineno)s": logger_server
,
967 "nbi.access %(filename)s:%(lineno)s": logger_access
,
968 "%(name)s %(filename)s:%(lineno)s": logger_nbi
970 log_format_cherry
= "%(asctime)s %(levelname)s {} %(message)s".format(format_
)
971 log_formatter_cherry
= logging
.Formatter(log_format_cherry
, datefmt
='%Y-%m-%dT%H:%M:%S')
972 str_handler
= logging
.StreamHandler()
973 str_handler
.setFormatter(log_formatter_cherry
)
974 logger
.addHandler(str_handler
)
976 if engine_config
["global"].get("log.level"):
977 logger_cherry
.setLevel(engine_config
["global"]["log.level"])
978 logger_nbi
.setLevel(engine_config
["global"]["log.level"])
980 # logging other modules
981 for k1
, logname
in {"message": "nbi.msg", "database": "nbi.db", "storage": "nbi.fs"}.items():
982 engine_config
[k1
]["logger_name"] = logname
983 logger_module
= logging
.getLogger(logname
)
984 if "logfile" in engine_config
[k1
]:
985 file_handler
= logging
.handlers
.RotatingFileHandler(engine_config
[k1
]["logfile"],
986 maxBytes
=100e6
, backupCount
=9, delay
=0)
987 file_handler
.setFormatter(log_formatter_simple
)
988 logger_module
.addHandler(file_handler
)
989 if "loglevel" in engine_config
[k1
]:
990 logger_module
.setLevel(engine_config
[k1
]["loglevel"])
991 # TODO add more entries, e.g.: storage
992 cherrypy
.tree
.apps
['/osm'].root
.engine
.start(engine_config
)
993 cherrypy
.tree
.apps
['/osm'].root
.authenticator
.start(engine_config
)
994 cherrypy
.tree
.apps
['/osm'].root
.engine
.init_db(target_version
=database_version
)
995 cherrypy
.tree
.apps
['/osm'].root
.authenticator
.init_db(target_version
=auth_database_version
)
997 # start subscriptions thread:
998 subscription_thread
= SubscriptionThread(config
=engine_config
, engine
=nbi_server
.engine
)
999 subscription_thread
.start()
1000 # Do not capture except SubscriptionException
1002 # load and print version. Ignore possible errors, e.g. file not found
1004 with
open("{}/version".format(engine_config
["/static"]['tools.staticdir.dir'])) as version_file
:
1005 version_data
= version_file
.read()
1006 cherrypy
.log
.error("Starting OSM NBI Version: {}".format(version_data
.replace("\n", " ")))
1011 def _stop_service():
1013 Callback function called when cherrypy.engine stops
1014 TODO: Ending database connections.
1016 global subscription_thread
1017 subscription_thread
.terminate()
1018 subscription_thread
= None
1019 cherrypy
.tree
.apps
['/osm'].root
.engine
.stop()
1020 cherrypy
.log
.error("Stopping osm_nbi")
1023 def nbi(config_file
):
1027 # #'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
1028 # 'tools.sessions.on': True,
1029 # 'tools.response_headers.on': True,
1030 # # 'tools.response_headers.headers': [('Content-Type', 'text/plain')],
1033 # cherrypy.Server.ssl_module = 'builtin'
1034 # cherrypy.Server.ssl_certificate = "http/cert.pem"
1035 # cherrypy.Server.ssl_private_key = "http/privkey.pem"
1036 # cherrypy.Server.thread_pool = 10
1037 # cherrypy.config.update({'Server.socket_port': config["port"], 'Server.socket_host': config["host"]})
1039 # cherrypy.config.update({'tools.auth_basic.on': True,
1040 # 'tools.auth_basic.realm': 'localhost',
1041 # 'tools.auth_basic.checkpassword': validate_password})
1042 nbi_server
= Server()
1043 cherrypy
.engine
.subscribe('start', _start_service
)
1044 cherrypy
.engine
.subscribe('stop', _stop_service
)
1045 cherrypy
.quickstart(nbi_server
, '/osm', config_file
)
1049 print("""Usage: {} [options]
1050 -c|--config [configuration_file]: loads the configuration file (default: ./nbi.cfg)
1051 -h|--help: shows this help
1052 """.format(sys
.argv
[0]))
1053 # --log-socket-host HOST: send logs to this host")
1054 # --log-socket-port PORT: send logs using this port (default: 9022)")
1057 if __name__
== '__main__':
1059 # load parameters and configuration
1060 opts
, args
= getopt
.getopt(sys
.argv
[1:], "hvc:", ["config=", "help"])
1061 # TODO add "log-socket-host=", "log-socket-port=", "log-file="
1064 if o
in ("-h", "--help"):
1067 elif o
in ("-c", "--config"):
1069 # elif o == "--log-socket-port":
1070 # log_socket_port = a
1071 # elif o == "--log-socket-host":
1072 # log_socket_host = a
1073 # elif o == "--log-file":
1076 assert False, "Unhandled option"
1078 if not path
.isfile(config_file
):
1079 print("configuration file '{}' that not exist".format(config_file
), file=sys
.stderr
)
1082 for config_file
in (__file__
[:__file__
.rfind(".")] + ".cfg", "./nbi.cfg", "/etc/osm/nbi.cfg"):
1083 if path
.isfile(config_file
):
1086 print("No configuration file 'nbi.cfg' found neither at local folder nor at /etc/osm/", file=sys
.stderr
)
1089 except getopt
.GetoptError
as e
:
1090 print(str(e
), file=sys
.stderr
)