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>"
40 # TODO consider to remove and provide version using the static version file
42 version_date
= "Apr 2018"
43 database_version
= '1.0'
44 auth_database_version
= '1.0'
47 North Bound Interface (O: OSM specific; 5,X: SOL005 not implemented yet; O5: SOL005 implemented)
48 URL: /osm GET POST PUT DELETE PATCH
50 /ns_descriptors_content O O
56 /artifacts[/<artifactPath>] O
64 /vnf_packages_content O O
68 /package_content O5 O5
71 /artifacts[/<artifactPath>] O5
76 /ns_instances_content O O
88 /vnf_instances (also vnfrs for compatibility) O
104 /vims_accounts (also vims for compatibility) O O
110 /netslice_templates_content O O
112 /netslice_templates O O
116 /artifacts[/<artifactPath>] O
118 /<subscriptionId> X X
121 /netslice_instances_content O O
122 /<SliceInstanceId> O O
123 /netslice_instances O O
124 /<SliceInstanceId> O O
129 /<nsiLcmOpOccId> O O O
131 /<subscriptionId> X X
134 Follows SOL005 section 4.3.2 It contains extra METHOD to override http method, FORCE to force.
135 simpleFilterExpr := <attrName>["."<attrName>]*["."<op>]"="<value>[","<value>]*
136 filterExpr := <simpleFilterExpr>["&"<simpleFilterExpr>]*
137 op := "eq" | "neq" (or "ne") | "gt" | "lt" | "gte" | "lte" | "cont" | "ncont"
139 For filtering inside array, it must select the element of the array, or add ANYINDEX to apply the filtering over any
140 item of the array, that is, pass if any item of the array pass the filter.
141 It allows both ne and neq for not equal
142 TODO: 4.3.3 Attribute selectors
143 all_fields, fields=x,y,.., exclude_default, exclude_fields=x,y,...
144 (none) … same as “exclude_default”
145 all_fields … all attributes.
146 fields=<list> … all attributes except all complex attributes with minimum cardinality of zero that are not
147 conditionally mandatory, and that are not provided in <list>.
148 exclude_fields=<list> … all attributes except those complex attributes with a minimum cardinality of zero that
149 are not conditionally mandatory, and that are provided in <list>.
150 exclude_default … all attributes except those complex attributes with a minimum cardinality of zero that are not
151 conditionally mandatory, and that are part of the "default exclude set" defined in the present specification for
152 the particular resource
153 exclude_default and include=<list> … all attributes except those complex attributes with a minimum cardinality
154 of zero that are not conditionally mandatory and that are part of the "default exclude set" defined in the
155 present specification for the particular resource, but that are not part of <list>
156 Header field name Reference Example Descriptions
157 Accept IETF RFC 7231 [19] application/json Content-Types that are acceptable for the response.
158 This header field shall be present if the response is expected to have a non-empty message body.
159 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the request.
160 This header field shall be present if the request has a non-empty message body.
161 Authorization IETF RFC 7235 [22] Bearer mF_9.B5f-4.1JqM The authorization token for the request.
162 Details are specified in clause 4.5.3.
163 Range IETF RFC 7233 [21] 1000-2000 Requested range of bytes from a file
164 Header field name Reference Example Descriptions
165 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the response.
166 This header field shall be present if the response has a non-empty message body.
167 Location IETF RFC 7231 [19] http://www.example.com/vnflcm/v1/vnf_instances/123 Used in redirection, or when a
168 new resource has been created.
169 This header field shall be present if the response status code is 201 or 3xx.
170 In the present document this header field is also used if the response status code is 202 and a new resource was
172 WWW-Authenticate IETF RFC 7235 [22] Bearer realm="example" Challenge if the corresponding HTTP request has not
173 provided authorization, or error details if the corresponding HTTP request has provided an invalid authorization
175 Accept-Ranges IETF RFC 7233 [21] bytes Used by the Server to signal whether or not it supports ranges for
177 Content-Range IETF RFC 7233 [21] bytes 21010-47021/ 47022 Signals the byte range that is contained in the
178 response, and the total length of the file.
179 Retry-After IETF RFC 7231 [19] Fri, 31 Dec 1999 23:59:59 GMT
183 class NbiException(Exception):
185 def __init__(self
, message
, http_code
=HTTPStatus
.METHOD_NOT_ALLOWED
):
186 Exception.__init
__(self
, message
)
187 self
.http_code
= http_code
190 class Server(object):
192 # to decode bytes to str
193 reader
= getreader("utf-8")
197 self
.engine
= Engine()
198 self
.authenticator
= Authenticator()
199 self
.valid_methods
= { # contains allowed URL and methods
202 "tokens": {"METHODS": ("GET", "POST", "DELETE"),
203 "<ID>": {"METHODS": ("GET", "DELETE")}
205 "users": {"METHODS": ("GET", "POST"),
206 "<ID>": {"METHODS": ("GET", "POST", "DELETE", "PATCH", "PUT")}
208 "projects": {"METHODS": ("GET", "POST"),
209 "<ID>": {"METHODS": ("GET", "DELETE")}
211 "vims": {"METHODS": ("GET", "POST"),
212 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
214 "vim_accounts": {"METHODS": ("GET", "POST"),
215 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
217 "sdns": {"METHODS": ("GET", "POST"),
218 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
224 "pdu_descriptors": {"METHODS": ("GET", "POST"),
225 "<ID>": {"METHODS": ("GET", "POST", "DELETE", "PATCH", "PUT")}
231 "ns_descriptors_content": {"METHODS": ("GET", "POST"),
232 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
234 "ns_descriptors": {"METHODS": ("GET", "POST"),
235 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"),
236 "nsd_content": {"METHODS": ("GET", "PUT")},
237 "nsd": {"METHODS": "GET"}, # descriptor inside package
238 "artifacts": {"*": {"METHODS": "GET"}}
241 "pnf_descriptors": {"TODO": ("GET", "POST"),
242 "<ID>": {"TODO": ("GET", "DELETE", "PATCH"),
243 "pnfd_content": {"TODO": ("GET", "PUT")}
246 "subscriptions": {"TODO": ("GET", "POST"),
247 "<ID>": {"TODO": ("GET", "DELETE")}
253 "vnf_packages_content": {"METHODS": ("GET", "POST"),
254 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
256 "vnf_packages": {"METHODS": ("GET", "POST"),
257 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"), # GET: vnfPkgInfo
258 "package_content": {"METHODS": ("GET", "PUT"), # package
259 "upload_from_uri": {"TODO": "POST"}
261 "vnfd": {"METHODS": "GET"}, # descriptor inside package
262 "artifacts": {"*": {"METHODS": "GET"}}
265 "subscriptions": {"TODO": ("GET", "POST"),
266 "<ID>": {"TODO": ("GET", "DELETE")}
272 "ns_instances_content": {"METHODS": ("GET", "POST"),
273 "<ID>": {"METHODS": ("GET", "DELETE")}
275 "ns_instances": {"METHODS": ("GET", "POST"),
276 "<ID>": {"METHODS": ("GET", "DELETE"),
277 "scale": {"METHODS": "POST"},
278 "terminate": {"METHODS": "POST"},
279 "instantiate": {"METHODS": "POST"},
280 "action": {"METHODS": "POST"},
283 "ns_lcm_op_occs": {"METHODS": "GET",
284 "<ID>": {"METHODS": "GET"},
286 "vnfrs": {"METHODS": ("GET"),
287 "<ID>": {"METHODS": ("GET")}
289 "vnf_instances": {"METHODS": ("GET"),
290 "<ID>": {"METHODS": ("GET")}
296 "netslice_templates_content": {"METHODS": ("GET", "POST"),
297 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
299 "netslice_templates": {"METHODS": ("GET", "POST"),
300 "<ID>": {"METHODS": ("GET", "DELETE"), "TODO": "PATCH",
301 "nst_content": {"METHODS": ("GET", "PUT")},
302 "nst": {"METHODS": "GET"}, # descriptor inside package
303 "artifacts": {"*": {"METHODS": "GET"}}
306 "subscriptions": {"TODO": ("GET", "POST"),
307 "<ID>": {"TODO": ("GET", "DELETE")}
313 "netslice_instances_content": {"METHODS": ("GET", "POST"),
314 "<ID>": {"METHODS": ("GET", "DELETE")}
316 "netslice_instances": {"METHODS": ("GET", "POST"),
317 "<ID>": {"METHODS": ("GET", "DELETE"),
318 "terminate": {"METHODS": "POST"},
319 "instantiate": {"METHODS": "POST"},
320 "action": {"METHODS": "POST"},
323 "nsi_lcm_op_occs": {"METHODS": "GET",
324 "<ID>": {"METHODS": "GET"},
330 def _format_in(self
, kwargs
):
333 if cherrypy
.request
.body
.length
:
334 error_text
= "Invalid input format "
336 if "Content-Type" in cherrypy
.request
.headers
:
337 if "application/json" in cherrypy
.request
.headers
["Content-Type"]:
338 error_text
= "Invalid json format "
339 indata
= json
.load(self
.reader(cherrypy
.request
.body
))
340 cherrypy
.request
.headers
.pop("Content-File-MD5", None)
341 elif "application/yaml" in cherrypy
.request
.headers
["Content-Type"]:
342 error_text
= "Invalid yaml format "
343 indata
= yaml
.load(cherrypy
.request
.body
)
344 cherrypy
.request
.headers
.pop("Content-File-MD5", None)
345 elif "application/binary" in cherrypy
.request
.headers
["Content-Type"] or \
346 "application/gzip" in cherrypy
.request
.headers
["Content-Type"] or \
347 "application/zip" in cherrypy
.request
.headers
["Content-Type"] or \
348 "text/plain" in cherrypy
.request
.headers
["Content-Type"]:
349 indata
= cherrypy
.request
.body
# .read()
350 elif "multipart/form-data" in cherrypy
.request
.headers
["Content-Type"]:
351 if "descriptor_file" in kwargs
:
352 filecontent
= kwargs
.pop("descriptor_file")
353 if not filecontent
.file:
354 raise NbiException("empty file or content", HTTPStatus
.BAD_REQUEST
)
355 indata
= filecontent
.file # .read()
356 if filecontent
.content_type
.value
:
357 cherrypy
.request
.headers
["Content-Type"] = filecontent
.content_type
.value
359 # raise cherrypy.HTTPError(HTTPStatus.Not_Acceptable,
360 # "Only 'Content-Type' of type 'application/json' or
361 # 'application/yaml' for input format are available")
362 error_text
= "Invalid yaml format "
363 indata
= yaml
.load(cherrypy
.request
.body
)
364 cherrypy
.request
.headers
.pop("Content-File-MD5", None)
366 error_text
= "Invalid yaml format "
367 indata
= yaml
.load(cherrypy
.request
.body
)
368 cherrypy
.request
.headers
.pop("Content-File-MD5", None)
373 if cherrypy
.request
.headers
.get("Query-String-Format") == "yaml":
376 for k
, v
in kwargs
.items():
377 if isinstance(v
, str):
382 kwargs
[k
] = yaml
.load(v
)
385 elif k
.endswith(".gt") or k
.endswith(".lt") or k
.endswith(".gte") or k
.endswith(".lte"):
393 elif v
.find(",") > 0:
394 kwargs
[k
] = v
.split(",")
395 elif isinstance(v
, (list, tuple)):
396 for index
in range(0, len(v
)):
401 v
[index
] = yaml
.load(v
[index
])
406 except (ValueError, yaml
.YAMLError
) as exc
:
407 raise NbiException(error_text
+ str(exc
), HTTPStatus
.BAD_REQUEST
)
408 except KeyError as exc
:
409 raise NbiException("Query string error: " + str(exc
), HTTPStatus
.BAD_REQUEST
)
410 except Exception as exc
:
411 raise NbiException(error_text
+ str(exc
), HTTPStatus
.BAD_REQUEST
)
414 def _format_out(data
, session
=None, _format
=None):
416 return string of dictionary data according to requested json, yaml, xml. By default json
417 :param data: response to be sent. Can be a dict, text or file
419 :param _format: The format to be set as Content-Type ir data is a file
422 accept
= cherrypy
.request
.headers
.get("Accept")
424 if accept
and "text/html" in accept
:
425 return html
.format(data
, cherrypy
.request
, cherrypy
.response
, session
)
426 # cherrypy.response.status = HTTPStatus.NO_CONTENT.value
428 elif hasattr(data
, "read"): # file object
430 cherrypy
.response
.headers
["Content-Type"] = _format
431 elif "b" in data
.mode
: # binariy asssumig zip
432 cherrypy
.response
.headers
["Content-Type"] = 'application/zip'
434 cherrypy
.response
.headers
["Content-Type"] = 'text/plain'
435 # TODO check that cherrypy close file. If not implement pending things to close per thread next
438 if "application/json" in accept
:
439 cherrypy
.response
.headers
["Content-Type"] = 'application/json; charset=utf-8'
440 a
= json
.dumps(data
, indent
=4) + "\n"
441 return a
.encode("utf8")
442 elif "text/html" in accept
:
443 return html
.format(data
, cherrypy
.request
, cherrypy
.response
, session
)
445 elif "application/yaml" in accept
or "*/*" in accept
or "text/plain" in accept
:
447 # if there is not any valid accept, raise an error. But if response is already an error, format in yaml
448 elif cherrypy
.response
.status
>= 400:
449 raise cherrypy
.HTTPError(HTTPStatus
.NOT_ACCEPTABLE
.value
,
450 "Only 'Accept' of type 'application/json' or 'application/yaml' "
451 "for output format are available")
452 cherrypy
.response
.headers
["Content-Type"] = 'application/yaml'
453 return yaml
.safe_dump(data
, explicit_start
=True, indent
=4, default_flow_style
=False, tags
=False,
454 encoding
='utf-8', allow_unicode
=True) # , canonical=True, default_style='"'
457 def index(self
, *args
, **kwargs
):
460 if cherrypy
.request
.method
== "GET":
461 session
= self
.authenticator
.authorize()
462 outdata
= "Index page"
464 raise cherrypy
.HTTPError(HTTPStatus
.METHOD_NOT_ALLOWED
.value
,
465 "Method {} not allowed for tokens".format(cherrypy
.request
.method
))
467 return self
._format
_out
(outdata
, session
)
469 except (EngineException
, AuthException
) as e
:
470 cherrypy
.log("index Exception {}".format(e
))
471 cherrypy
.response
.status
= e
.http_code
.value
472 return self
._format
_out
("Welcome to OSM!", session
)
475 def version(self
, *args
, **kwargs
):
476 # TODO consider to remove and provide version using the static version file
477 global __version__
, version_date
479 if cherrypy
.request
.method
!= "GET":
480 raise NbiException("Only method GET is allowed", HTTPStatus
.METHOD_NOT_ALLOWED
)
482 raise NbiException("Invalid URL or query string for version", HTTPStatus
.METHOD_NOT_ALLOWED
)
483 return __version__
+ " " + version_date
484 except NbiException
as e
:
485 cherrypy
.response
.status
= e
.http_code
.value
487 "code": e
.http_code
.name
,
488 "status": e
.http_code
.value
,
491 return self
._format
_out
(problem_details
, None)
494 def token(self
, method
, token_id
=None, kwargs
=None):
496 # self.engine.load_dbase(cherrypy.request.app.config)
497 indata
= self
._format
_in
(kwargs
)
498 if not isinstance(indata
, dict):
499 raise NbiException("Expected application/yaml or application/json Content-Type", HTTPStatus
.BAD_REQUEST
)
502 session
= self
.authenticator
.authorize()
504 outdata
= self
.authenticator
.get_token(session
, token_id
)
506 outdata
= self
.authenticator
.get_token_list(session
)
507 elif method
== "POST":
509 session
= self
.authenticator
.authorize()
513 indata
.update(kwargs
)
514 outdata
= self
.authenticator
.new_token(session
, indata
, cherrypy
.request
.remote
)
516 cherrypy
.session
['Authorization'] = outdata
["_id"]
517 self
._set
_location
_header
("admin", "v1", "tokens", outdata
["_id"])
518 # cherrypy.response.cookie["Authorization"] = outdata["id"]
519 # cherrypy.response.cookie["Authorization"]['expires'] = 3600
520 elif method
== "DELETE":
521 if not token_id
and "id" in kwargs
:
522 token_id
= kwargs
["id"]
524 session
= self
.authenticator
.authorize()
525 token_id
= session
["_id"]
526 outdata
= self
.authenticator
.del_token(token_id
)
528 cherrypy
.session
['Authorization'] = "logout"
529 # cherrypy.response.cookie["Authorization"] = token_id
530 # cherrypy.response.cookie["Authorization"]['expires'] = 0
532 raise NbiException("Method {} not allowed for token".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
533 return self
._format
_out
(outdata
, session
)
534 except (NbiException
, EngineException
, DbException
, AuthException
) as e
:
535 cherrypy
.log("tokens Exception {}".format(e
))
536 cherrypy
.response
.status
= e
.http_code
.value
538 "code": e
.http_code
.name
,
539 "status": e
.http_code
.value
,
542 return self
._format
_out
(problem_details
, session
)
545 def test(self
, *args
, **kwargs
):
547 if args
and args
[0] == "help":
548 return "<html><pre>\ninit\nfile/<name> download file\ndb-clear/table\nfs-clear[/folder]\nlogin\nlogin2\n"\
549 "sleep/<time>\nmessage/topic\n</pre></html>"
551 elif args
and args
[0] == "init":
553 # self.engine.load_dbase(cherrypy.request.app.config)
554 self
.engine
.create_admin()
555 return "Done. User 'admin', password 'admin' created"
557 cherrypy
.response
.status
= HTTPStatus
.FORBIDDEN
.value
558 return self
._format
_out
("Database already initialized")
559 elif args
and args
[0] == "file":
560 return cherrypy
.lib
.static
.serve_file(cherrypy
.tree
.apps
['/osm'].config
["storage"]["path"] + "/" + args
[1],
561 "text/plain", "attachment")
562 elif args
and args
[0] == "file2":
563 f_path
= cherrypy
.tree
.apps
['/osm'].config
["storage"]["path"] + "/" + args
[1]
564 f
= open(f_path
, "r")
565 cherrypy
.response
.headers
["Content-type"] = "text/plain"
568 elif len(args
) == 2 and args
[0] == "db-clear":
569 deleted_info
= self
.engine
.db
.del_list(args
[1], kwargs
)
570 return "{} {} deleted\n".format(deleted_info
["deleted"], args
[1])
571 elif len(args
) and args
[0] == "fs-clear":
575 folders
= self
.engine
.fs
.dir_ls(".")
576 for folder
in folders
:
577 self
.engine
.fs
.file_delete(folder
)
578 return ",".join(folders
) + " folders deleted\n"
579 elif args
and args
[0] == "login":
580 if not cherrypy
.request
.headers
.get("Authorization"):
581 cherrypy
.response
.headers
["WWW-Authenticate"] = 'Basic realm="Access to OSM site", charset="UTF-8"'
582 cherrypy
.response
.status
= HTTPStatus
.UNAUTHORIZED
.value
583 elif args
and args
[0] == "login2":
584 if not cherrypy
.request
.headers
.get("Authorization"):
585 cherrypy
.response
.headers
["WWW-Authenticate"] = 'Bearer realm="Access to OSM site"'
586 cherrypy
.response
.status
= HTTPStatus
.UNAUTHORIZED
.value
587 elif args
and args
[0] == "sleep":
590 sleep_time
= int(args
[1])
592 cherrypy
.response
.status
= HTTPStatus
.FORBIDDEN
.value
593 return self
._format
_out
("Database already initialized")
594 thread_info
= cherrypy
.thread_data
596 time
.sleep(sleep_time
)
598 elif len(args
) >= 2 and args
[0] == "message":
600 return_text
= "<html><pre>{} ->\n".format(main_topic
)
602 if cherrypy
.request
.method
== 'POST':
603 to_send
= yaml
.load(cherrypy
.request
.body
)
604 for k
, v
in to_send
.items():
605 self
.engine
.msg
.write(main_topic
, k
, v
)
606 return_text
+= " {}: {}\n".format(k
, v
)
607 elif cherrypy
.request
.method
== 'GET':
608 for k
, v
in kwargs
.items():
609 self
.engine
.msg
.write(main_topic
, k
, yaml
.load(v
))
610 return_text
+= " {}: {}\n".format(k
, yaml
.load(v
))
611 except Exception as e
:
612 return_text
+= "Error: " + str(e
)
613 return_text
+= "</pre></html>\n"
617 "<html><pre>\nheaders:\n args: {}\n".format(args
) +
618 " kwargs: {}\n".format(kwargs
) +
619 " headers: {}\n".format(cherrypy
.request
.headers
) +
620 " path_info: {}\n".format(cherrypy
.request
.path_info
) +
621 " query_string: {}\n".format(cherrypy
.request
.query_string
) +
622 " session: {}\n".format(cherrypy
.session
) +
623 " cookie: {}\n".format(cherrypy
.request
.cookie
) +
624 " method: {}\n".format(cherrypy
.request
.method
) +
625 " session: {}\n".format(cherrypy
.session
.get('fieldname')) +
627 return_text
+= " length: {}\n".format(cherrypy
.request
.body
.length
)
628 if cherrypy
.request
.body
.length
:
629 return_text
+= " content: {}\n".format(
630 str(cherrypy
.request
.body
.read(int(cherrypy
.request
.headers
.get('Content-Length', 0)))))
632 return_text
+= "thread: {}\n".format(thread_info
)
633 return_text
+= "</pre></html>"
636 def _check_valid_url_method(self
, method
, *args
):
638 raise NbiException("URL must contain at least 'main_topic/version/topic'", HTTPStatus
.METHOD_NOT_ALLOWED
)
640 reference
= self
.valid_methods
644 if not isinstance(reference
, dict):
645 raise NbiException("URL contains unexpected extra items '{}'".format(arg
),
646 HTTPStatus
.METHOD_NOT_ALLOWED
)
649 reference
= reference
[arg
]
650 elif "<ID>" in reference
:
651 reference
= reference
["<ID>"]
652 elif "*" in reference
:
653 reference
= reference
["*"]
656 raise NbiException("Unexpected URL item {}".format(arg
), HTTPStatus
.METHOD_NOT_ALLOWED
)
657 if "TODO" in reference
and method
in reference
["TODO"]:
658 raise NbiException("Method {} not supported yet for this URL".format(method
), HTTPStatus
.NOT_IMPLEMENTED
)
659 elif "METHODS" in reference
and method
not in reference
["METHODS"]:
660 raise NbiException("Method {} not supported for this URL".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
664 def _set_location_header(main_topic
, version
, topic
, id):
666 Insert response header Location with the URL of created item base on URL params
673 # Use cherrypy.request.base for absoluted path and make use of request.header HOST just in case behind aNAT
674 cherrypy
.response
.headers
["Location"] = "/osm/{}/{}/{}/{}".format(main_topic
, version
, topic
, id)
678 def default(self
, main_topic
=None, version
=None, topic
=None, _id
=None, item
=None, *args
, **kwargs
):
687 if not main_topic
or not version
or not topic
:
688 raise NbiException("URL must contain at least 'main_topic/version/topic'",
689 HTTPStatus
.METHOD_NOT_ALLOWED
)
690 if main_topic
not in ("admin", "vnfpkgm", "nsd", "nslcm", "pdu", "nst", "nsilcm"):
691 raise NbiException("URL main_topic '{}' not supported".format(main_topic
),
692 HTTPStatus
.METHOD_NOT_ALLOWED
)
694 raise NbiException("URL version '{}' not supported".format(version
), HTTPStatus
.METHOD_NOT_ALLOWED
)
696 if kwargs
and "METHOD" in kwargs
and kwargs
["METHOD"] in ("PUT", "POST", "DELETE", "GET", "PATCH"):
697 method
= kwargs
.pop("METHOD")
699 method
= cherrypy
.request
.method
700 if kwargs
and "FORCE" in kwargs
:
701 force
= kwargs
.pop("FORCE")
704 self
._check
_valid
_url
_method
(method
, main_topic
, version
, topic
, _id
, item
, *args
)
705 if main_topic
== "admin" and topic
== "tokens":
706 return self
.token(method
, _id
, kwargs
)
708 # self.engine.load_dbase(cherrypy.request.app.config)
709 session
= self
.authenticator
.authorize()
710 indata
= self
._format
_in
(kwargs
)
712 if topic
== "subscriptions":
713 engine_topic
= main_topic
+ "_" + topic
717 if main_topic
== "nsd":
718 engine_topic
= "nsds"
719 elif main_topic
== "vnfpkgm":
720 engine_topic
= "vnfds"
721 elif main_topic
== "nslcm":
722 engine_topic
= "nsrs"
723 if topic
== "ns_lcm_op_occs":
724 engine_topic
= "nslcmops"
725 if topic
== "vnfrs" or topic
== "vnf_instances":
726 engine_topic
= "vnfrs"
727 elif main_topic
== "nst":
728 engine_topic
= "nsts"
729 elif main_topic
== "nsilcm":
730 engine_topic
= "nsis"
731 if topic
== "nsi_lcm_op_occs":
732 engine_topic
= "nsilcmops"
733 elif main_topic
== "pdu":
734 engine_topic
= "pdus"
735 if engine_topic
== "vims": # TODO this is for backward compatibility, it will remove in the future
736 engine_topic
= "vim_accounts"
739 if item
in ("nsd_content", "package_content", "artifacts", "vnfd", "nsd", "nst", "nst_content"):
740 if item
in ("vnfd", "nsd", "nst"):
744 elif item
== "artifacts":
748 file, _format
= self
.engine
.get_file(session
, engine_topic
, _id
, path
,
749 cherrypy
.request
.headers
.get("Accept"))
752 outdata
= self
.engine
.get_item_list(session
, engine_topic
, kwargs
)
754 outdata
= self
.engine
.get_item(session
, engine_topic
, _id
)
755 elif method
== "POST":
756 if topic
in ("ns_descriptors_content", "vnf_packages_content", "netslice_templates_content"):
757 _id
= cherrypy
.request
.headers
.get("Transaction-Id")
759 _id
= self
.engine
.new_item(rollback
, session
, engine_topic
, {}, None, cherrypy
.request
.headers
,
761 completed
= self
.engine
.upload_content(session
, engine_topic
, _id
, indata
, kwargs
,
762 cherrypy
.request
.headers
, force
=force
)
764 self
._set
_location
_header
(main_topic
, version
, topic
, _id
)
766 cherrypy
.response
.headers
["Transaction-Id"] = _id
767 outdata
= {"id": _id
}
768 elif topic
== "ns_instances_content":
770 _id
= self
.engine
.new_item(rollback
, session
, engine_topic
, indata
, kwargs
, force
=force
)
772 indata
["lcmOperationType"] = "instantiate"
773 indata
["nsInstanceId"] = _id
774 self
.engine
.new_item(rollback
, session
, "nslcmops", indata
, None)
775 self
._set
_location
_header
(main_topic
, version
, topic
, _id
)
776 outdata
= {"id": _id
}
777 elif topic
== "ns_instances" and item
:
778 indata
["lcmOperationType"] = item
779 indata
["nsInstanceId"] = _id
780 _id
= self
.engine
.new_item(rollback
, session
, "nslcmops", indata
, kwargs
)
781 self
._set
_location
_header
(main_topic
, version
, "ns_lcm_op_occs", _id
)
782 outdata
= {"id": _id
}
783 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
784 elif topic
== "netslice_instances_content":
785 # creates NetSlice_Instance_record (NSIR)
786 _id
= self
.engine
.new_item(rollback
, session
, engine_topic
, indata
, kwargs
, force
=force
)
787 self
._set
_location
_header
(main_topic
, version
, topic
, _id
)
788 indata
["lcmOperationType"] = "instantiate"
789 indata
["nsiInstanceId"] = _id
790 self
.engine
.new_item(rollback
, session
, "nsilcmops", indata
, kwargs
)
791 outdata
= {"id": _id
}
793 elif topic
== "netslice_instances" and item
:
794 indata
["lcmOperationType"] = item
795 indata
["nsiInstanceId"] = _id
796 _id
= self
.engine
.new_item(rollback
, session
, "nsilcmops", indata
, kwargs
)
797 self
._set
_location
_header
(main_topic
, version
, "nsi_lcm_op_occs", _id
)
798 outdata
= {"id": _id
}
799 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
801 _id
= self
.engine
.new_item(rollback
, session
, engine_topic
, indata
, kwargs
,
802 cherrypy
.request
.headers
, force
=force
)
803 self
._set
_location
_header
(main_topic
, version
, topic
, _id
)
804 outdata
= {"id": _id
}
805 # TODO form NsdInfo when topic in ("ns_descriptors", "vnf_packages")
806 cherrypy
.response
.status
= HTTPStatus
.CREATED
.value
808 elif method
== "DELETE":
810 outdata
= self
.engine
.del_item_list(session
, engine_topic
, kwargs
)
811 cherrypy
.response
.status
= HTTPStatus
.OK
.value
812 else: # len(args) > 1
813 if topic
== "ns_instances_content" and not force
:
815 "lcmOperationType": "terminate",
819 opp_id
= self
.engine
.new_item(rollback
, session
, "nslcmops", nslcmop_desc
, None)
820 outdata
= {"_id": opp_id
}
821 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
822 elif topic
== "netslice_instances_content" and not force
:
824 "lcmOperationType": "terminate",
825 "nsiInstanceId": _id
,
828 opp_id
= self
.engine
.new_item(rollback
, session
, "nsilcmops", nsilcmop_desc
, None)
829 outdata
= {"_id": opp_id
}
830 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
832 self
.engine
.del_item(session
, engine_topic
, _id
, force
)
833 cherrypy
.response
.status
= HTTPStatus
.NO_CONTENT
.value
834 if engine_topic
in ("vim_accounts", "sdns"):
835 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
837 elif method
in ("PUT", "PATCH"):
839 if not indata
and not kwargs
:
840 raise NbiException("Nothing to update. Provide payload and/or query string",
841 HTTPStatus
.BAD_REQUEST
)
842 if item
in ("nsd_content", "package_content", "nst_content") and method
== "PUT":
843 completed
= self
.engine
.upload_content(session
, engine_topic
, _id
, indata
, kwargs
,
844 cherrypy
.request
.headers
, force
=force
)
846 cherrypy
.response
.headers
["Transaction-Id"] = id
848 self
.engine
.edit_item(session
, engine_topic
, _id
, indata
, kwargs
, force
=force
)
849 cherrypy
.response
.status
= HTTPStatus
.NO_CONTENT
.value
851 raise NbiException("Method {} not allowed".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
852 return self
._format
_out
(outdata
, session
, _format
)
853 except Exception as e
:
854 if isinstance(e
, (NbiException
, EngineException
, DbException
, FsException
, MsgException
, AuthException
,
856 http_code_value
= cherrypy
.response
.status
= e
.http_code
.value
857 http_code_name
= e
.http_code
.name
858 cherrypy
.log("Exception {}".format(e
))
860 http_code_value
= cherrypy
.response
.status
= HTTPStatus
.BAD_REQUEST
.value
# INTERNAL_SERVER_ERROR
861 cherrypy
.log("CRITICAL: Exception {}".format(e
), traceback
=True)
862 http_code_name
= HTTPStatus
.BAD_REQUEST
.name
863 if hasattr(outdata
, "close"): # is an open file
867 for rollback_item
in rollback
:
869 if rollback_item
.get("operation") == "set":
870 self
.engine
.db
.set_one(rollback_item
["topic"], {"_id": rollback_item
["_id"]},
871 rollback_item
["content"], fail_on_empty
=False)
873 self
.engine
.del_item(**rollback_item
, session
=session
, force
=True)
874 except Exception as e2
:
875 rollback_error_text
= "Rollback Exception {}: {}".format(rollback_item
, e2
)
876 cherrypy
.log(rollback_error_text
)
877 error_text
+= ". " + rollback_error_text
878 # if isinstance(e, MsgException):
879 # error_text = "{} has been '{}' but other modules cannot be informed because an error on bus".format(
880 # engine_topic[:-1], method, error_text)
882 "code": http_code_name
,
883 "status": http_code_value
,
884 "detail": error_text
,
886 return self
._format
_out
(problem_details
, session
)
887 # raise cherrypy.HTTPError(e.http_code.value, str(e))
890 # def validate_password(realm, username, password):
891 # cherrypy.log("realm "+ str(realm))
892 # if username == "admin" and password == "admin":
897 def _start_service():
899 Callback function called when cherrypy.engine starts
900 Override configuration with env variables
901 Set database, storage, message configuration
902 Init database with admin/admin user password
904 cherrypy
.log
.error("Starting osm_nbi")
905 # update general cherrypy configuration
908 engine_config
= cherrypy
.tree
.apps
['/osm'].config
909 for k
, v
in environ
.items():
910 if not k
.startswith("OSMNBI_"):
912 k1
, _
, k2
= k
[7:].lower().partition("_")
916 # update static configuration
917 if k
== 'OSMNBI_STATIC_DIR':
918 engine_config
["/static"]['tools.staticdir.dir'] = v
919 engine_config
["/static"]['tools.staticdir.on'] = True
920 elif k
== 'OSMNBI_SOCKET_PORT' or k
== 'OSMNBI_SERVER_PORT':
921 update_dict
['server.socket_port'] = int(v
)
922 elif k
== 'OSMNBI_SOCKET_HOST' or k
== 'OSMNBI_SERVER_HOST':
923 update_dict
['server.socket_host'] = v
924 elif k1
in ("server", "test", "auth", "log"):
925 update_dict
[k1
+ '.' + k2
] = v
926 elif k1
in ("message", "database", "storage", "authentication"):
927 # k2 = k2.replace('_', '.')
928 if k2
in ("port", "db_port"):
929 engine_config
[k1
][k2
] = int(v
)
931 engine_config
[k1
][k2
] = v
933 except ValueError as e
:
934 cherrypy
.log
.error("Ignoring environ '{}': " + str(e
))
935 except Exception as e
:
936 cherrypy
.log
.warn("skipping environ '{}' on exception '{}'".format(k
, e
))
939 cherrypy
.config
.update(update_dict
)
940 engine_config
["global"].update(update_dict
)
943 log_format_simple
= "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(message)s"
944 log_formatter_simple
= logging
.Formatter(log_format_simple
, datefmt
='%Y-%m-%dT%H:%M:%S')
945 logger_server
= logging
.getLogger("cherrypy.error")
946 logger_access
= logging
.getLogger("cherrypy.access")
947 logger_cherry
= logging
.getLogger("cherrypy")
948 logger_nbi
= logging
.getLogger("nbi")
950 if "log.file" in engine_config
["global"]:
951 file_handler
= logging
.handlers
.RotatingFileHandler(engine_config
["global"]["log.file"],
952 maxBytes
=100e6
, backupCount
=9, delay
=0)
953 file_handler
.setFormatter(log_formatter_simple
)
954 logger_cherry
.addHandler(file_handler
)
955 logger_nbi
.addHandler(file_handler
)
956 # log always to standard output
957 for format_
, logger
in {"nbi.server %(filename)s:%(lineno)s": logger_server
,
958 "nbi.access %(filename)s:%(lineno)s": logger_access
,
959 "%(name)s %(filename)s:%(lineno)s": logger_nbi
961 log_format_cherry
= "%(asctime)s %(levelname)s {} %(message)s".format(format_
)
962 log_formatter_cherry
= logging
.Formatter(log_format_cherry
, datefmt
='%Y-%m-%dT%H:%M:%S')
963 str_handler
= logging
.StreamHandler()
964 str_handler
.setFormatter(log_formatter_cherry
)
965 logger
.addHandler(str_handler
)
967 if engine_config
["global"].get("log.level"):
968 logger_cherry
.setLevel(engine_config
["global"]["log.level"])
969 logger_nbi
.setLevel(engine_config
["global"]["log.level"])
971 # logging other modules
972 for k1
, logname
in {"message": "nbi.msg", "database": "nbi.db", "storage": "nbi.fs"}.items():
973 engine_config
[k1
]["logger_name"] = logname
974 logger_module
= logging
.getLogger(logname
)
975 if "logfile" in engine_config
[k1
]:
976 file_handler
= logging
.handlers
.RotatingFileHandler(engine_config
[k1
]["logfile"],
977 maxBytes
=100e6
, backupCount
=9, delay
=0)
978 file_handler
.setFormatter(log_formatter_simple
)
979 logger_module
.addHandler(file_handler
)
980 if "loglevel" in engine_config
[k1
]:
981 logger_module
.setLevel(engine_config
[k1
]["loglevel"])
982 # TODO add more entries, e.g.: storage
983 cherrypy
.tree
.apps
['/osm'].root
.engine
.start(engine_config
)
984 cherrypy
.tree
.apps
['/osm'].root
.authenticator
.start(engine_config
)
985 cherrypy
.tree
.apps
['/osm'].root
.engine
.init_db(target_version
=database_version
)
986 cherrypy
.tree
.apps
['/osm'].root
.authenticator
.init_db(target_version
=auth_database_version
)
987 # getenv('OSMOPENMANO_TENANT', None)
992 Callback function called when cherrypy.engine stops
993 TODO: Ending database connections.
995 cherrypy
.tree
.apps
['/osm'].root
.engine
.stop()
996 cherrypy
.log
.error("Stopping osm_nbi")
999 def nbi(config_file
):
1002 # #'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
1003 # 'tools.sessions.on': True,
1004 # 'tools.response_headers.on': True,
1005 # # 'tools.response_headers.headers': [('Content-Type', 'text/plain')],
1008 # cherrypy.Server.ssl_module = 'builtin'
1009 # cherrypy.Server.ssl_certificate = "http/cert.pem"
1010 # cherrypy.Server.ssl_private_key = "http/privkey.pem"
1011 # cherrypy.Server.thread_pool = 10
1012 # cherrypy.config.update({'Server.socket_port': config["port"], 'Server.socket_host': config["host"]})
1014 # cherrypy.config.update({'tools.auth_basic.on': True,
1015 # 'tools.auth_basic.realm': 'localhost',
1016 # 'tools.auth_basic.checkpassword': validate_password})
1017 cherrypy
.engine
.subscribe('start', _start_service
)
1018 cherrypy
.engine
.subscribe('stop', _stop_service
)
1019 cherrypy
.quickstart(Server(), '/osm', config_file
)
1023 print("""Usage: {} [options]
1024 -c|--config [configuration_file]: loads the configuration file (default: ./nbi.cfg)
1025 -h|--help: shows this help
1026 """.format(sys
.argv
[0]))
1027 # --log-socket-host HOST: send logs to this host")
1028 # --log-socket-port PORT: send logs using this port (default: 9022)")
1031 if __name__
== '__main__':
1033 # load parameters and configuration
1034 opts
, args
= getopt
.getopt(sys
.argv
[1:], "hvc:", ["config=", "help"])
1035 # TODO add "log-socket-host=", "log-socket-port=", "log-file="
1038 if o
in ("-h", "--help"):
1041 elif o
in ("-c", "--config"):
1043 # elif o == "--log-socket-port":
1044 # log_socket_port = a
1045 # elif o == "--log-socket-host":
1046 # log_socket_host = a
1047 # elif o == "--log-file":
1050 assert False, "Unhandled option"
1052 if not path
.isfile(config_file
):
1053 print("configuration file '{}' that not exist".format(config_file
), file=sys
.stderr
)
1056 for config_file
in (__file__
[:__file__
.rfind(".")] + ".cfg", "./nbi.cfg", "/etc/osm/nbi.cfg"):
1057 if path
.isfile(config_file
):
1060 print("No configuration file 'nbi.cfg' found neither at local folder nor at /etc/osm/", file=sys
.stderr
)
1063 except getopt
.GetoptError
as e
:
1064 print(str(e
), file=sys
.stderr
)