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 # Added PUT to allow Project Name modification
215 "<ID>": {"METHODS": ("GET", "DELETE", "PUT")}
217 "roles": {"METHODS": ("GET", "POST"),
218 "<ID>": {"METHODS": ("GET", "POST", "DELETE")}
220 "vims": {"METHODS": ("GET", "POST"),
221 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
223 "vim_accounts": {"METHODS": ("GET", "POST"),
224 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
226 "wim_accounts": {"METHODS": ("GET", "POST"),
227 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
229 "sdns": {"METHODS": ("GET", "POST"),
230 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
236 "pdu_descriptors": {"METHODS": ("GET", "POST"),
237 "<ID>": {"METHODS": ("GET", "POST", "DELETE", "PATCH", "PUT")}
243 "ns_descriptors_content": {"METHODS": ("GET", "POST"),
244 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
246 "ns_descriptors": {"METHODS": ("GET", "POST"),
247 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"),
248 "nsd_content": {"METHODS": ("GET", "PUT")},
249 "nsd": {"METHODS": "GET"}, # descriptor inside package
250 "artifacts": {"*": {"METHODS": "GET"}}
253 "pnf_descriptors": {"TODO": ("GET", "POST"),
254 "<ID>": {"TODO": ("GET", "DELETE", "PATCH"),
255 "pnfd_content": {"TODO": ("GET", "PUT")}
258 "subscriptions": {"TODO": ("GET", "POST"),
259 "<ID>": {"TODO": ("GET", "DELETE")}
265 "vnf_packages_content": {"METHODS": ("GET", "POST"),
266 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
268 "vnf_packages": {"METHODS": ("GET", "POST"),
269 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"), # GET: vnfPkgInfo
270 "package_content": {"METHODS": ("GET", "PUT"), # package
271 "upload_from_uri": {"TODO": "POST"}
273 "vnfd": {"METHODS": "GET"}, # descriptor inside package
274 "artifacts": {"*": {"METHODS": "GET"}}
277 "subscriptions": {"TODO": ("GET", "POST"),
278 "<ID>": {"TODO": ("GET", "DELETE")}
284 "ns_instances_content": {"METHODS": ("GET", "POST"),
285 "<ID>": {"METHODS": ("GET", "DELETE")}
287 "ns_instances": {"METHODS": ("GET", "POST"),
288 "<ID>": {"METHODS": ("GET", "DELETE"),
289 "scale": {"METHODS": "POST"},
290 "terminate": {"METHODS": "POST"},
291 "instantiate": {"METHODS": "POST"},
292 "action": {"METHODS": "POST"},
295 "ns_lcm_op_occs": {"METHODS": "GET",
296 "<ID>": {"METHODS": "GET"},
298 "vnfrs": {"METHODS": ("GET"),
299 "<ID>": {"METHODS": ("GET")}
301 "vnf_instances": {"METHODS": ("GET"),
302 "<ID>": {"METHODS": ("GET")}
308 "netslice_templates_content": {"METHODS": ("GET", "POST"),
309 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
311 "netslice_templates": {"METHODS": ("GET", "POST"),
312 "<ID>": {"METHODS": ("GET", "DELETE"), "TODO": "PATCH",
313 "nst_content": {"METHODS": ("GET", "PUT")},
314 "nst": {"METHODS": "GET"}, # descriptor inside package
315 "artifacts": {"*": {"METHODS": "GET"}}
318 "subscriptions": {"TODO": ("GET", "POST"),
319 "<ID>": {"TODO": ("GET", "DELETE")}
325 "netslice_instances_content": {"METHODS": ("GET", "POST"),
326 "<ID>": {"METHODS": ("GET", "DELETE")}
328 "netslice_instances": {"METHODS": ("GET", "POST"),
329 "<ID>": {"METHODS": ("GET", "DELETE"),
330 "terminate": {"METHODS": "POST"},
331 "instantiate": {"METHODS": "POST"},
332 "action": {"METHODS": "POST"},
335 "nsi_lcm_op_occs": {"METHODS": "GET",
336 "<ID>": {"METHODS": "GET"},
342 def _format_in(self
, kwargs
):
345 if cherrypy
.request
.body
.length
:
346 error_text
= "Invalid input format "
348 if "Content-Type" in cherrypy
.request
.headers
:
349 if "application/json" in cherrypy
.request
.headers
["Content-Type"]:
350 error_text
= "Invalid json format "
351 indata
= json
.load(self
.reader(cherrypy
.request
.body
))
352 cherrypy
.request
.headers
.pop("Content-File-MD5", None)
353 elif "application/yaml" in cherrypy
.request
.headers
["Content-Type"]:
354 error_text
= "Invalid yaml format "
355 indata
= yaml
.load(cherrypy
.request
.body
)
356 cherrypy
.request
.headers
.pop("Content-File-MD5", None)
357 elif "application/binary" in cherrypy
.request
.headers
["Content-Type"] or \
358 "application/gzip" in cherrypy
.request
.headers
["Content-Type"] or \
359 "application/zip" in cherrypy
.request
.headers
["Content-Type"] or \
360 "text/plain" in cherrypy
.request
.headers
["Content-Type"]:
361 indata
= cherrypy
.request
.body
# .read()
362 elif "multipart/form-data" in cherrypy
.request
.headers
["Content-Type"]:
363 if "descriptor_file" in kwargs
:
364 filecontent
= kwargs
.pop("descriptor_file")
365 if not filecontent
.file:
366 raise NbiException("empty file or content", HTTPStatus
.BAD_REQUEST
)
367 indata
= filecontent
.file # .read()
368 if filecontent
.content_type
.value
:
369 cherrypy
.request
.headers
["Content-Type"] = filecontent
.content_type
.value
371 # raise cherrypy.HTTPError(HTTPStatus.Not_Acceptable,
372 # "Only 'Content-Type' of type 'application/json' or
373 # 'application/yaml' for input format are available")
374 error_text
= "Invalid yaml format "
375 indata
= yaml
.load(cherrypy
.request
.body
)
376 cherrypy
.request
.headers
.pop("Content-File-MD5", None)
378 error_text
= "Invalid yaml format "
379 indata
= yaml
.load(cherrypy
.request
.body
)
380 cherrypy
.request
.headers
.pop("Content-File-MD5", None)
385 if cherrypy
.request
.headers
.get("Query-String-Format") == "yaml":
388 for k
, v
in kwargs
.items():
389 if isinstance(v
, str):
394 kwargs
[k
] = yaml
.load(v
)
397 elif k
.endswith(".gt") or k
.endswith(".lt") or k
.endswith(".gte") or k
.endswith(".lte"):
405 elif v
.find(",") > 0:
406 kwargs
[k
] = v
.split(",")
407 elif isinstance(v
, (list, tuple)):
408 for index
in range(0, len(v
)):
413 v
[index
] = yaml
.load(v
[index
])
418 except (ValueError, yaml
.YAMLError
) as exc
:
419 raise NbiException(error_text
+ str(exc
), HTTPStatus
.BAD_REQUEST
)
420 except KeyError as exc
:
421 raise NbiException("Query string error: " + str(exc
), HTTPStatus
.BAD_REQUEST
)
422 except Exception as exc
:
423 raise NbiException(error_text
+ str(exc
), HTTPStatus
.BAD_REQUEST
)
426 def _format_out(data
, session
=None, _format
=None):
428 return string of dictionary data according to requested json, yaml, xml. By default json
429 :param data: response to be sent. Can be a dict, text or file
431 :param _format: The format to be set as Content-Type ir data is a file
434 accept
= cherrypy
.request
.headers
.get("Accept")
436 if accept
and "text/html" in accept
:
437 return html
.format(data
, cherrypy
.request
, cherrypy
.response
, session
)
438 # cherrypy.response.status = HTTPStatus.NO_CONTENT.value
440 elif hasattr(data
, "read"): # file object
442 cherrypy
.response
.headers
["Content-Type"] = _format
443 elif "b" in data
.mode
: # binariy asssumig zip
444 cherrypy
.response
.headers
["Content-Type"] = 'application/zip'
446 cherrypy
.response
.headers
["Content-Type"] = 'text/plain'
447 # TODO check that cherrypy close file. If not implement pending things to close per thread next
450 if "application/json" in accept
:
451 cherrypy
.response
.headers
["Content-Type"] = 'application/json; charset=utf-8'
452 a
= json
.dumps(data
, indent
=4) + "\n"
453 return a
.encode("utf8")
454 elif "text/html" in accept
:
455 return html
.format(data
, cherrypy
.request
, cherrypy
.response
, session
)
457 elif "application/yaml" in accept
or "*/*" in accept
or "text/plain" in accept
:
459 # if there is not any valid accept, raise an error. But if response is already an error, format in yaml
460 elif cherrypy
.response
.status
>= 400:
461 raise cherrypy
.HTTPError(HTTPStatus
.NOT_ACCEPTABLE
.value
,
462 "Only 'Accept' of type 'application/json' or 'application/yaml' "
463 "for output format are available")
464 cherrypy
.response
.headers
["Content-Type"] = 'application/yaml'
465 return yaml
.safe_dump(data
, explicit_start
=True, indent
=4, default_flow_style
=False, tags
=False,
466 encoding
='utf-8', allow_unicode
=True) # , canonical=True, default_style='"'
469 def index(self
, *args
, **kwargs
):
472 if cherrypy
.request
.method
== "GET":
473 session
= self
.authenticator
.authorize()
474 outdata
= "Index page"
476 raise cherrypy
.HTTPError(HTTPStatus
.METHOD_NOT_ALLOWED
.value
,
477 "Method {} not allowed for tokens".format(cherrypy
.request
.method
))
479 return self
._format
_out
(outdata
, session
)
481 except (EngineException
, AuthException
) as e
:
482 cherrypy
.log("index Exception {}".format(e
))
483 cherrypy
.response
.status
= e
.http_code
.value
484 return self
._format
_out
("Welcome to OSM!", session
)
487 def version(self
, *args
, **kwargs
):
488 # TODO consider to remove and provide version using the static version file
489 global __version__
, version_date
491 if cherrypy
.request
.method
!= "GET":
492 raise NbiException("Only method GET is allowed", HTTPStatus
.METHOD_NOT_ALLOWED
)
494 raise NbiException("Invalid URL or query string for version", HTTPStatus
.METHOD_NOT_ALLOWED
)
495 return __version__
+ " " + version_date
496 except NbiException
as e
:
497 cherrypy
.response
.status
= e
.http_code
.value
499 "code": e
.http_code
.name
,
500 "status": e
.http_code
.value
,
503 return self
._format
_out
(problem_details
, None)
506 def token(self
, method
, token_id
=None, kwargs
=None):
508 # self.engine.load_dbase(cherrypy.request.app.config)
509 indata
= self
._format
_in
(kwargs
)
510 if not isinstance(indata
, dict):
511 raise NbiException("Expected application/yaml or application/json Content-Type", HTTPStatus
.BAD_REQUEST
)
514 session
= self
.authenticator
.authorize()
516 outdata
= self
.authenticator
.get_token(session
, token_id
)
518 outdata
= self
.authenticator
.get_token_list(session
)
519 elif method
== "POST":
521 session
= self
.authenticator
.authorize()
525 indata
.update(kwargs
)
526 outdata
= self
.authenticator
.new_token(session
, indata
, cherrypy
.request
.remote
)
528 cherrypy
.session
['Authorization'] = outdata
["_id"]
529 self
._set
_location
_header
("admin", "v1", "tokens", outdata
["_id"])
530 # cherrypy.response.cookie["Authorization"] = outdata["id"]
531 # cherrypy.response.cookie["Authorization"]['expires'] = 3600
532 elif method
== "DELETE":
533 if not token_id
and "id" in kwargs
:
534 token_id
= kwargs
["id"]
536 session
= self
.authenticator
.authorize()
537 token_id
= session
["_id"]
538 outdata
= self
.authenticator
.del_token(token_id
)
540 cherrypy
.session
['Authorization'] = "logout"
541 # cherrypy.response.cookie["Authorization"] = token_id
542 # cherrypy.response.cookie["Authorization"]['expires'] = 0
544 raise NbiException("Method {} not allowed for token".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
545 return self
._format
_out
(outdata
, session
)
546 except (NbiException
, EngineException
, DbException
, AuthException
) as e
:
547 cherrypy
.log("tokens Exception {}".format(e
))
548 cherrypy
.response
.status
= e
.http_code
.value
550 "code": e
.http_code
.name
,
551 "status": e
.http_code
.value
,
554 return self
._format
_out
(problem_details
, session
)
557 def test(self
, *args
, **kwargs
):
559 if args
and args
[0] == "help":
560 return "<html><pre>\ninit\nfile/<name> download file\ndb-clear/table\nfs-clear[/folder]\nlogin\nlogin2\n"\
561 "sleep/<time>\nmessage/topic\n</pre></html>"
563 elif args
and args
[0] == "init":
565 # self.engine.load_dbase(cherrypy.request.app.config)
566 self
.engine
.create_admin()
567 return "Done. User 'admin', password 'admin' created"
569 cherrypy
.response
.status
= HTTPStatus
.FORBIDDEN
.value
570 return self
._format
_out
("Database already initialized")
571 elif args
and args
[0] == "file":
572 return cherrypy
.lib
.static
.serve_file(cherrypy
.tree
.apps
['/osm'].config
["storage"]["path"] + "/" + args
[1],
573 "text/plain", "attachment")
574 elif args
and args
[0] == "file2":
575 f_path
= cherrypy
.tree
.apps
['/osm'].config
["storage"]["path"] + "/" + args
[1]
576 f
= open(f_path
, "r")
577 cherrypy
.response
.headers
["Content-type"] = "text/plain"
580 elif len(args
) == 2 and args
[0] == "db-clear":
581 deleted_info
= self
.engine
.db
.del_list(args
[1], kwargs
)
582 return "{} {} deleted\n".format(deleted_info
["deleted"], args
[1])
583 elif len(args
) and args
[0] == "fs-clear":
587 folders
= self
.engine
.fs
.dir_ls(".")
588 for folder
in folders
:
589 self
.engine
.fs
.file_delete(folder
)
590 return ",".join(folders
) + " folders deleted\n"
591 elif args
and args
[0] == "login":
592 if not cherrypy
.request
.headers
.get("Authorization"):
593 cherrypy
.response
.headers
["WWW-Authenticate"] = 'Basic realm="Access to OSM site", charset="UTF-8"'
594 cherrypy
.response
.status
= HTTPStatus
.UNAUTHORIZED
.value
595 elif args
and args
[0] == "login2":
596 if not cherrypy
.request
.headers
.get("Authorization"):
597 cherrypy
.response
.headers
["WWW-Authenticate"] = 'Bearer realm="Access to OSM site"'
598 cherrypy
.response
.status
= HTTPStatus
.UNAUTHORIZED
.value
599 elif args
and args
[0] == "sleep":
602 sleep_time
= int(args
[1])
604 cherrypy
.response
.status
= HTTPStatus
.FORBIDDEN
.value
605 return self
._format
_out
("Database already initialized")
606 thread_info
= cherrypy
.thread_data
608 time
.sleep(sleep_time
)
610 elif len(args
) >= 2 and args
[0] == "message":
612 return_text
= "<html><pre>{} ->\n".format(main_topic
)
614 if cherrypy
.request
.method
== 'POST':
615 to_send
= yaml
.load(cherrypy
.request
.body
)
616 for k
, v
in to_send
.items():
617 self
.engine
.msg
.write(main_topic
, k
, v
)
618 return_text
+= " {}: {}\n".format(k
, v
)
619 elif cherrypy
.request
.method
== 'GET':
620 for k
, v
in kwargs
.items():
621 self
.engine
.msg
.write(main_topic
, k
, yaml
.load(v
))
622 return_text
+= " {}: {}\n".format(k
, yaml
.load(v
))
623 except Exception as e
:
624 return_text
+= "Error: " + str(e
)
625 return_text
+= "</pre></html>\n"
629 "<html><pre>\nheaders:\n args: {}\n".format(args
) +
630 " kwargs: {}\n".format(kwargs
) +
631 " headers: {}\n".format(cherrypy
.request
.headers
) +
632 " path_info: {}\n".format(cherrypy
.request
.path_info
) +
633 " query_string: {}\n".format(cherrypy
.request
.query_string
) +
634 " session: {}\n".format(cherrypy
.session
) +
635 " cookie: {}\n".format(cherrypy
.request
.cookie
) +
636 " method: {}\n".format(cherrypy
.request
.method
) +
637 " session: {}\n".format(cherrypy
.session
.get('fieldname')) +
639 return_text
+= " length: {}\n".format(cherrypy
.request
.body
.length
)
640 if cherrypy
.request
.body
.length
:
641 return_text
+= " content: {}\n".format(
642 str(cherrypy
.request
.body
.read(int(cherrypy
.request
.headers
.get('Content-Length', 0)))))
644 return_text
+= "thread: {}\n".format(thread_info
)
645 return_text
+= "</pre></html>"
648 def _check_valid_url_method(self
, method
, *args
):
650 raise NbiException("URL must contain at least 'main_topic/version/topic'", HTTPStatus
.METHOD_NOT_ALLOWED
)
652 reference
= self
.valid_methods
656 if not isinstance(reference
, dict):
657 raise NbiException("URL contains unexpected extra items '{}'".format(arg
),
658 HTTPStatus
.METHOD_NOT_ALLOWED
)
661 reference
= reference
[arg
]
662 elif "<ID>" in reference
:
663 reference
= reference
["<ID>"]
664 elif "*" in reference
:
665 reference
= reference
["*"]
668 raise NbiException("Unexpected URL item {}".format(arg
), HTTPStatus
.METHOD_NOT_ALLOWED
)
669 if "TODO" in reference
and method
in reference
["TODO"]:
670 raise NbiException("Method {} not supported yet for this URL".format(method
), HTTPStatus
.NOT_IMPLEMENTED
)
671 elif "METHODS" in reference
and method
not in reference
["METHODS"]:
672 raise NbiException("Method {} not supported for this URL".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
676 def _set_location_header(main_topic
, version
, topic
, id):
678 Insert response header Location with the URL of created item base on URL params
685 # Use cherrypy.request.base for absoluted path and make use of request.header HOST just in case behind aNAT
686 cherrypy
.response
.headers
["Location"] = "/osm/{}/{}/{}/{}".format(main_topic
, version
, topic
, id)
690 def default(self
, main_topic
=None, version
=None, topic
=None, _id
=None, item
=None, *args
, **kwargs
):
699 if not main_topic
or not version
or not topic
:
700 raise NbiException("URL must contain at least 'main_topic/version/topic'",
701 HTTPStatus
.METHOD_NOT_ALLOWED
)
702 if main_topic
not in ("admin", "vnfpkgm", "nsd", "nslcm", "pdu", "nst", "nsilcm"):
703 raise NbiException("URL main_topic '{}' not supported".format(main_topic
),
704 HTTPStatus
.METHOD_NOT_ALLOWED
)
706 raise NbiException("URL version '{}' not supported".format(version
), HTTPStatus
.METHOD_NOT_ALLOWED
)
708 if kwargs
and "METHOD" in kwargs
and kwargs
["METHOD"] in ("PUT", "POST", "DELETE", "GET", "PATCH"):
709 method
= kwargs
.pop("METHOD")
711 method
= cherrypy
.request
.method
712 if kwargs
and "FORCE" in kwargs
:
713 force
= kwargs
.pop("FORCE")
716 self
._check
_valid
_url
_method
(method
, main_topic
, version
, topic
, _id
, item
, *args
)
717 if main_topic
== "admin" and topic
== "tokens":
718 return self
.token(method
, _id
, kwargs
)
720 # self.engine.load_dbase(cherrypy.request.app.config)
721 session
= self
.authenticator
.authorize()
722 indata
= self
._format
_in
(kwargs
)
724 if topic
== "subscriptions":
725 engine_topic
= main_topic
+ "_" + topic
729 if main_topic
== "nsd":
730 engine_topic
= "nsds"
731 elif main_topic
== "vnfpkgm":
732 engine_topic
= "vnfds"
733 elif main_topic
== "nslcm":
734 engine_topic
= "nsrs"
735 if topic
== "ns_lcm_op_occs":
736 engine_topic
= "nslcmops"
737 if topic
== "vnfrs" or topic
== "vnf_instances":
738 engine_topic
= "vnfrs"
739 elif main_topic
== "nst":
740 engine_topic
= "nsts"
741 elif main_topic
== "nsilcm":
742 engine_topic
= "nsis"
743 if topic
== "nsi_lcm_op_occs":
744 engine_topic
= "nsilcmops"
745 elif main_topic
== "pdu":
746 engine_topic
= "pdus"
747 if engine_topic
== "vims": # TODO this is for backward compatibility, it will remove in the future
748 engine_topic
= "vim_accounts"
751 if item
in ("nsd_content", "package_content", "artifacts", "vnfd", "nsd", "nst", "nst_content"):
752 if item
in ("vnfd", "nsd", "nst"):
756 elif item
== "artifacts":
760 file, _format
= self
.engine
.get_file(session
, engine_topic
, _id
, path
,
761 cherrypy
.request
.headers
.get("Accept"))
764 outdata
= self
.engine
.get_item_list(session
, engine_topic
, kwargs
)
766 outdata
= self
.engine
.get_item(session
, engine_topic
, _id
)
767 elif method
== "POST":
768 if topic
in ("ns_descriptors_content", "vnf_packages_content", "netslice_templates_content"):
769 _id
= cherrypy
.request
.headers
.get("Transaction-Id")
771 _id
= self
.engine
.new_item(rollback
, session
, engine_topic
, {}, None, cherrypy
.request
.headers
,
773 completed
= self
.engine
.upload_content(session
, engine_topic
, _id
, indata
, kwargs
,
774 cherrypy
.request
.headers
, force
=force
)
776 self
._set
_location
_header
(main_topic
, version
, topic
, _id
)
778 cherrypy
.response
.headers
["Transaction-Id"] = _id
779 outdata
= {"id": _id
}
780 elif topic
== "ns_instances_content":
782 _id
= self
.engine
.new_item(rollback
, session
, engine_topic
, indata
, kwargs
, force
=force
)
784 indata
["lcmOperationType"] = "instantiate"
785 indata
["nsInstanceId"] = _id
786 self
.engine
.new_item(rollback
, session
, "nslcmops", indata
, None)
787 self
._set
_location
_header
(main_topic
, version
, topic
, _id
)
788 outdata
= {"id": _id
}
789 elif topic
== "ns_instances" and item
:
790 indata
["lcmOperationType"] = item
791 indata
["nsInstanceId"] = _id
792 _id
= self
.engine
.new_item(rollback
, session
, "nslcmops", indata
, kwargs
)
793 self
._set
_location
_header
(main_topic
, version
, "ns_lcm_op_occs", _id
)
794 outdata
= {"id": _id
}
795 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
796 elif topic
== "netslice_instances_content":
797 # creates NetSlice_Instance_record (NSIR)
798 _id
= self
.engine
.new_item(rollback
, session
, engine_topic
, indata
, kwargs
, force
=force
)
799 self
._set
_location
_header
(main_topic
, version
, topic
, _id
)
800 indata
["lcmOperationType"] = "instantiate"
801 indata
["nsiInstanceId"] = _id
802 self
.engine
.new_item(rollback
, session
, "nsilcmops", indata
, kwargs
)
803 outdata
= {"id": _id
}
805 elif topic
== "netslice_instances" and item
:
806 indata
["lcmOperationType"] = item
807 indata
["nsiInstanceId"] = _id
808 _id
= self
.engine
.new_item(rollback
, session
, "nsilcmops", indata
, kwargs
)
809 self
._set
_location
_header
(main_topic
, version
, "nsi_lcm_op_occs", _id
)
810 outdata
= {"id": _id
}
811 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
813 _id
= self
.engine
.new_item(rollback
, session
, engine_topic
, indata
, kwargs
,
814 cherrypy
.request
.headers
, force
=force
)
815 self
._set
_location
_header
(main_topic
, version
, topic
, _id
)
816 outdata
= {"id": _id
}
817 # TODO form NsdInfo when topic in ("ns_descriptors", "vnf_packages")
818 cherrypy
.response
.status
= HTTPStatus
.CREATED
.value
820 elif method
== "DELETE":
822 outdata
= self
.engine
.del_item_list(session
, engine_topic
, kwargs
)
823 cherrypy
.response
.status
= HTTPStatus
.OK
.value
824 else: # len(args) > 1
825 delete_in_process
= False
826 if topic
== "ns_instances_content" and not force
:
828 "lcmOperationType": "terminate",
832 opp_id
= self
.engine
.new_item(rollback
, session
, "nslcmops", nslcmop_desc
, None)
834 delete_in_process
= True
835 outdata
= {"_id": opp_id
}
836 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
837 elif topic
== "netslice_instances_content" and not force
:
839 "lcmOperationType": "terminate",
840 "nsiInstanceId": _id
,
843 opp_id
= self
.engine
.new_item(rollback
, session
, "nsilcmops", nsilcmop_desc
, None)
845 delete_in_process
= True
846 outdata
= {"_id": opp_id
}
847 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
848 if not delete_in_process
:
849 self
.engine
.del_item(session
, engine_topic
, _id
, force
)
850 cherrypy
.response
.status
= HTTPStatus
.NO_CONTENT
.value
851 if engine_topic
in ("vim_accounts", "wim_accounts", "sdns"):
852 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
854 elif method
in ("PUT", "PATCH"):
856 if not indata
and not kwargs
:
857 raise NbiException("Nothing to update. Provide payload and/or query string",
858 HTTPStatus
.BAD_REQUEST
)
859 if item
in ("nsd_content", "package_content", "nst_content") and method
== "PUT":
860 completed
= self
.engine
.upload_content(session
, engine_topic
, _id
, indata
, kwargs
,
861 cherrypy
.request
.headers
, force
=force
)
863 cherrypy
.response
.headers
["Transaction-Id"] = id
865 self
.engine
.edit_item(session
, engine_topic
, _id
, indata
, kwargs
, force
=force
)
866 cherrypy
.response
.status
= HTTPStatus
.NO_CONTENT
.value
868 raise NbiException("Method {} not allowed".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
869 return self
._format
_out
(outdata
, session
, _format
)
870 except Exception as e
:
871 if isinstance(e
, (NbiException
, EngineException
, DbException
, FsException
, MsgException
, AuthException
,
873 http_code_value
= cherrypy
.response
.status
= e
.http_code
.value
874 http_code_name
= e
.http_code
.name
875 cherrypy
.log("Exception {}".format(e
))
877 http_code_value
= cherrypy
.response
.status
= HTTPStatus
.BAD_REQUEST
.value
# INTERNAL_SERVER_ERROR
878 cherrypy
.log("CRITICAL: Exception {}".format(e
), traceback
=True)
879 http_code_name
= HTTPStatus
.BAD_REQUEST
.name
880 if hasattr(outdata
, "close"): # is an open file
884 for rollback_item
in rollback
:
886 if rollback_item
.get("operation") == "set":
887 self
.engine
.db
.set_one(rollback_item
["topic"], {"_id": rollback_item
["_id"]},
888 rollback_item
["content"], fail_on_empty
=False)
890 self
.engine
.db
.del_one(rollback_item
["topic"], {"_id": rollback_item
["_id"]},
892 except Exception as e2
:
893 rollback_error_text
= "Rollback Exception {}: {}".format(rollback_item
, e2
)
894 cherrypy
.log(rollback_error_text
)
895 error_text
+= ". " + rollback_error_text
896 # if isinstance(e, MsgException):
897 # error_text = "{} has been '{}' but other modules cannot be informed because an error on bus".format(
898 # engine_topic[:-1], method, error_text)
900 "code": http_code_name
,
901 "status": http_code_value
,
902 "detail": error_text
,
904 return self
._format
_out
(problem_details
, session
)
905 # raise cherrypy.HTTPError(e.http_code.value, str(e))
908 def _start_service():
910 Callback function called when cherrypy.engine starts
911 Override configuration with env variables
912 Set database, storage, message configuration
913 Init database with admin/admin user password
916 global subscription_thread
917 cherrypy
.log
.error("Starting osm_nbi")
918 # update general cherrypy configuration
921 engine_config
= cherrypy
.tree
.apps
['/osm'].config
922 for k
, v
in environ
.items():
923 if not k
.startswith("OSMNBI_"):
925 k1
, _
, k2
= k
[7:].lower().partition("_")
929 # update static configuration
930 if k
== 'OSMNBI_STATIC_DIR':
931 engine_config
["/static"]['tools.staticdir.dir'] = v
932 engine_config
["/static"]['tools.staticdir.on'] = True
933 elif k
== 'OSMNBI_SOCKET_PORT' or k
== 'OSMNBI_SERVER_PORT':
934 update_dict
['server.socket_port'] = int(v
)
935 elif k
== 'OSMNBI_SOCKET_HOST' or k
== 'OSMNBI_SERVER_HOST':
936 update_dict
['server.socket_host'] = v
937 elif k1
in ("server", "test", "auth", "log"):
938 update_dict
[k1
+ '.' + k2
] = v
939 elif k1
in ("message", "database", "storage", "authentication"):
940 # k2 = k2.replace('_', '.')
941 if k2
in ("port", "db_port"):
942 engine_config
[k1
][k2
] = int(v
)
944 engine_config
[k1
][k2
] = v
946 except ValueError as e
:
947 cherrypy
.log
.error("Ignoring environ '{}': " + str(e
))
948 except Exception as e
:
949 cherrypy
.log
.warn("skipping environ '{}' on exception '{}'".format(k
, e
))
952 cherrypy
.config
.update(update_dict
)
953 engine_config
["global"].update(update_dict
)
956 log_format_simple
= "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(message)s"
957 log_formatter_simple
= logging
.Formatter(log_format_simple
, datefmt
='%Y-%m-%dT%H:%M:%S')
958 logger_server
= logging
.getLogger("cherrypy.error")
959 logger_access
= logging
.getLogger("cherrypy.access")
960 logger_cherry
= logging
.getLogger("cherrypy")
961 logger_nbi
= logging
.getLogger("nbi")
963 if "log.file" in engine_config
["global"]:
964 file_handler
= logging
.handlers
.RotatingFileHandler(engine_config
["global"]["log.file"],
965 maxBytes
=100e6
, backupCount
=9, delay
=0)
966 file_handler
.setFormatter(log_formatter_simple
)
967 logger_cherry
.addHandler(file_handler
)
968 logger_nbi
.addHandler(file_handler
)
969 # log always to standard output
970 for format_
, logger
in {"nbi.server %(filename)s:%(lineno)s": logger_server
,
971 "nbi.access %(filename)s:%(lineno)s": logger_access
,
972 "%(name)s %(filename)s:%(lineno)s": logger_nbi
974 log_format_cherry
= "%(asctime)s %(levelname)s {} %(message)s".format(format_
)
975 log_formatter_cherry
= logging
.Formatter(log_format_cherry
, datefmt
='%Y-%m-%dT%H:%M:%S')
976 str_handler
= logging
.StreamHandler()
977 str_handler
.setFormatter(log_formatter_cherry
)
978 logger
.addHandler(str_handler
)
980 if engine_config
["global"].get("log.level"):
981 logger_cherry
.setLevel(engine_config
["global"]["log.level"])
982 logger_nbi
.setLevel(engine_config
["global"]["log.level"])
984 # logging other modules
985 for k1
, logname
in {"message": "nbi.msg", "database": "nbi.db", "storage": "nbi.fs"}.items():
986 engine_config
[k1
]["logger_name"] = logname
987 logger_module
= logging
.getLogger(logname
)
988 if "logfile" in engine_config
[k1
]:
989 file_handler
= logging
.handlers
.RotatingFileHandler(engine_config
[k1
]["logfile"],
990 maxBytes
=100e6
, backupCount
=9, delay
=0)
991 file_handler
.setFormatter(log_formatter_simple
)
992 logger_module
.addHandler(file_handler
)
993 if "loglevel" in engine_config
[k1
]:
994 logger_module
.setLevel(engine_config
[k1
]["loglevel"])
995 # TODO add more entries, e.g.: storage
996 cherrypy
.tree
.apps
['/osm'].root
.engine
.start(engine_config
)
997 cherrypy
.tree
.apps
['/osm'].root
.authenticator
.start(engine_config
)
998 cherrypy
.tree
.apps
['/osm'].root
.engine
.init_db(target_version
=database_version
)
999 cherrypy
.tree
.apps
['/osm'].root
.authenticator
.init_db(target_version
=auth_database_version
)
1001 # start subscriptions thread:
1002 subscription_thread
= SubscriptionThread(config
=engine_config
, engine
=nbi_server
.engine
)
1003 subscription_thread
.start()
1004 # Do not capture except SubscriptionException
1006 # load and print version. Ignore possible errors, e.g. file not found
1008 with
open("{}/version".format(engine_config
["/static"]['tools.staticdir.dir'])) as version_file
:
1009 version_data
= version_file
.read()
1010 cherrypy
.log
.error("Starting OSM NBI Version: {}".format(version_data
.replace("\n", " ")))
1015 def _stop_service():
1017 Callback function called when cherrypy.engine stops
1018 TODO: Ending database connections.
1020 global subscription_thread
1021 subscription_thread
.terminate()
1022 subscription_thread
= None
1023 cherrypy
.tree
.apps
['/osm'].root
.engine
.stop()
1024 cherrypy
.log
.error("Stopping osm_nbi")
1027 def nbi(config_file
):
1031 # #'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
1032 # 'tools.sessions.on': True,
1033 # 'tools.response_headers.on': True,
1034 # # 'tools.response_headers.headers': [('Content-Type', 'text/plain')],
1037 # cherrypy.Server.ssl_module = 'builtin'
1038 # cherrypy.Server.ssl_certificate = "http/cert.pem"
1039 # cherrypy.Server.ssl_private_key = "http/privkey.pem"
1040 # cherrypy.Server.thread_pool = 10
1041 # cherrypy.config.update({'Server.socket_port': config["port"], 'Server.socket_host': config["host"]})
1043 # cherrypy.config.update({'tools.auth_basic.on': True,
1044 # 'tools.auth_basic.realm': 'localhost',
1045 # 'tools.auth_basic.checkpassword': validate_password})
1046 nbi_server
= Server()
1047 cherrypy
.engine
.subscribe('start', _start_service
)
1048 cherrypy
.engine
.subscribe('stop', _stop_service
)
1049 cherrypy
.quickstart(nbi_server
, '/osm', config_file
)
1053 print("""Usage: {} [options]
1054 -c|--config [configuration_file]: loads the configuration file (default: ./nbi.cfg)
1055 -h|--help: shows this help
1056 """.format(sys
.argv
[0]))
1057 # --log-socket-host HOST: send logs to this host")
1058 # --log-socket-port PORT: send logs using this port (default: 9022)")
1061 if __name__
== '__main__':
1063 # load parameters and configuration
1064 opts
, args
= getopt
.getopt(sys
.argv
[1:], "hvc:", ["config=", "help"])
1065 # TODO add "log-socket-host=", "log-socket-port=", "log-file="
1068 if o
in ("-h", "--help"):
1071 elif o
in ("-c", "--config"):
1073 # elif o == "--log-socket-port":
1074 # log_socket_port = a
1075 # elif o == "--log-socket-host":
1076 # log_socket_host = a
1077 # elif o == "--log-file":
1080 assert False, "Unhandled option"
1082 if not path
.isfile(config_file
):
1083 print("configuration file '{}' that not exist".format(config_file
), file=sys
.stderr
)
1086 for config_file
in (__file__
[:__file__
.rfind(".")] + ".cfg", "./nbi.cfg", "/etc/osm/nbi.cfg"):
1087 if path
.isfile(config_file
):
1090 print("No configuration file 'nbi.cfg' found neither at local folder nor at /etc/osm/", file=sys
.stderr
)
1093 except getopt
.GetoptError
as e
:
1094 print(str(e
), file=sys
.stderr
)