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 /vim_accounts (also vims for compatibility) O O
112 /netslice_templates_content O O
114 /netslice_templates O O
118 /artifacts[/<artifactPath>] O
120 /<subscriptionId> X X
123 /netslice_instances_content O O
124 /<SliceInstanceId> O O
125 /netslice_instances O O
126 /<SliceInstanceId> O O
131 /<nsiLcmOpOccId> O O O
133 /<subscriptionId> X X
136 Follows SOL005 section 4.3.2 It contains extra METHOD to override http method, FORCE to force.
137 simpleFilterExpr := <attrName>["."<attrName>]*["."<op>]"="<value>[","<value>]*
138 filterExpr := <simpleFilterExpr>["&"<simpleFilterExpr>]*
139 op := "eq" | "neq" (or "ne") | "gt" | "lt" | "gte" | "lte" | "cont" | "ncont"
141 For filtering inside array, it must select the element of the array, or add ANYINDEX to apply the filtering over any
142 item of the array, that is, pass if any item of the array pass the filter.
143 It allows both ne and neq for not equal
144 TODO: 4.3.3 Attribute selectors
145 all_fields, fields=x,y,.., exclude_default, exclude_fields=x,y,...
146 (none) … same as “exclude_default”
147 all_fields … all attributes.
148 fields=<list> … all attributes except all complex attributes with minimum cardinality of zero that are not
149 conditionally mandatory, and that are not provided in <list>.
150 exclude_fields=<list> … all attributes except those complex attributes with a minimum cardinality of zero that
151 are not conditionally mandatory, and that are provided in <list>.
152 exclude_default … all attributes except those complex attributes with a minimum cardinality of zero that are not
153 conditionally mandatory, and that are part of the "default exclude set" defined in the present specification for
154 the particular resource
155 exclude_default and include=<list> … all attributes except those complex attributes with a minimum cardinality
156 of zero that are not conditionally mandatory and that are part of the "default exclude set" defined in the
157 present specification for the particular resource, but that are not part of <list>
158 Header field name Reference Example Descriptions
159 Accept IETF RFC 7231 [19] application/json Content-Types that are acceptable for the response.
160 This header field shall be present if the response is expected to have a non-empty message body.
161 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the request.
162 This header field shall be present if the request has a non-empty message body.
163 Authorization IETF RFC 7235 [22] Bearer mF_9.B5f-4.1JqM The authorization token for the request.
164 Details are specified in clause 4.5.3.
165 Range IETF RFC 7233 [21] 1000-2000 Requested range of bytes from a file
166 Header field name Reference Example Descriptions
167 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the response.
168 This header field shall be present if the response has a non-empty message body.
169 Location IETF RFC 7231 [19] http://www.example.com/vnflcm/v1/vnf_instances/123 Used in redirection, or when a
170 new resource has been created.
171 This header field shall be present if the response status code is 201 or 3xx.
172 In the present document this header field is also used if the response status code is 202 and a new resource was
174 WWW-Authenticate IETF RFC 7235 [22] Bearer realm="example" Challenge if the corresponding HTTP request has not
175 provided authorization, or error details if the corresponding HTTP request has provided an invalid authorization
177 Accept-Ranges IETF RFC 7233 [21] bytes Used by the Server to signal whether or not it supports ranges for
179 Content-Range IETF RFC 7233 [21] bytes 21010-47021/ 47022 Signals the byte range that is contained in the
180 response, and the total length of the file.
181 Retry-After IETF RFC 7231 [19] Fri, 31 Dec 1999 23:59:59 GMT
185 class NbiException(Exception):
187 def __init__(self
, message
, http_code
=HTTPStatus
.METHOD_NOT_ALLOWED
):
188 Exception.__init
__(self
, message
)
189 self
.http_code
= http_code
192 class Server(object):
194 # to decode bytes to str
195 reader
= getreader("utf-8")
199 self
.engine
= Engine()
200 self
.authenticator
= Authenticator()
201 self
.valid_methods
= { # contains allowed URL and methods
204 "tokens": {"METHODS": ("GET", "POST", "DELETE"),
205 "<ID>": {"METHODS": ("GET", "DELETE")}
207 "users": {"METHODS": ("GET", "POST"),
208 "<ID>": {"METHODS": ("GET", "POST", "DELETE", "PATCH", "PUT")}
210 "projects": {"METHODS": ("GET", "POST"),
211 "<ID>": {"METHODS": ("GET", "DELETE")}
213 "vims": {"METHODS": ("GET", "POST"),
214 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
216 "vim_accounts": {"METHODS": ("GET", "POST"),
217 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
219 "wim_accounts": {"METHODS": ("GET", "POST"),
220 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
222 "sdns": {"METHODS": ("GET", "POST"),
223 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
229 "pdu_descriptors": {"METHODS": ("GET", "POST"),
230 "<ID>": {"METHODS": ("GET", "POST", "DELETE", "PATCH", "PUT")}
236 "ns_descriptors_content": {"METHODS": ("GET", "POST"),
237 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
239 "ns_descriptors": {"METHODS": ("GET", "POST"),
240 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"),
241 "nsd_content": {"METHODS": ("GET", "PUT")},
242 "nsd": {"METHODS": "GET"}, # descriptor inside package
243 "artifacts": {"*": {"METHODS": "GET"}}
246 "pnf_descriptors": {"TODO": ("GET", "POST"),
247 "<ID>": {"TODO": ("GET", "DELETE", "PATCH"),
248 "pnfd_content": {"TODO": ("GET", "PUT")}
251 "subscriptions": {"TODO": ("GET", "POST"),
252 "<ID>": {"TODO": ("GET", "DELETE")}
258 "vnf_packages_content": {"METHODS": ("GET", "POST"),
259 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
261 "vnf_packages": {"METHODS": ("GET", "POST"),
262 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"), # GET: vnfPkgInfo
263 "package_content": {"METHODS": ("GET", "PUT"), # package
264 "upload_from_uri": {"TODO": "POST"}
266 "vnfd": {"METHODS": "GET"}, # descriptor inside package
267 "artifacts": {"*": {"METHODS": "GET"}}
270 "subscriptions": {"TODO": ("GET", "POST"),
271 "<ID>": {"TODO": ("GET", "DELETE")}
277 "ns_instances_content": {"METHODS": ("GET", "POST"),
278 "<ID>": {"METHODS": ("GET", "DELETE")}
280 "ns_instances": {"METHODS": ("GET", "POST"),
281 "<ID>": {"METHODS": ("GET", "DELETE"),
282 "scale": {"METHODS": "POST"},
283 "terminate": {"METHODS": "POST"},
284 "instantiate": {"METHODS": "POST"},
285 "action": {"METHODS": "POST"},
288 "ns_lcm_op_occs": {"METHODS": "GET",
289 "<ID>": {"METHODS": "GET"},
291 "vnfrs": {"METHODS": ("GET"),
292 "<ID>": {"METHODS": ("GET")}
294 "vnf_instances": {"METHODS": ("GET"),
295 "<ID>": {"METHODS": ("GET")}
301 "netslice_templates_content": {"METHODS": ("GET", "POST"),
302 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
304 "netslice_templates": {"METHODS": ("GET", "POST"),
305 "<ID>": {"METHODS": ("GET", "DELETE"), "TODO": "PATCH",
306 "nst_content": {"METHODS": ("GET", "PUT")},
307 "nst": {"METHODS": "GET"}, # descriptor inside package
308 "artifacts": {"*": {"METHODS": "GET"}}
311 "subscriptions": {"TODO": ("GET", "POST"),
312 "<ID>": {"TODO": ("GET", "DELETE")}
318 "netslice_instances_content": {"METHODS": ("GET", "POST"),
319 "<ID>": {"METHODS": ("GET", "DELETE")}
321 "netslice_instances": {"METHODS": ("GET", "POST"),
322 "<ID>": {"METHODS": ("GET", "DELETE"),
323 "terminate": {"METHODS": "POST"},
324 "instantiate": {"METHODS": "POST"},
325 "action": {"METHODS": "POST"},
328 "nsi_lcm_op_occs": {"METHODS": "GET",
329 "<ID>": {"METHODS": "GET"},
335 def _format_in(self
, kwargs
):
338 if cherrypy
.request
.body
.length
:
339 error_text
= "Invalid input format "
341 if "Content-Type" in cherrypy
.request
.headers
:
342 if "application/json" in cherrypy
.request
.headers
["Content-Type"]:
343 error_text
= "Invalid json format "
344 indata
= json
.load(self
.reader(cherrypy
.request
.body
))
345 cherrypy
.request
.headers
.pop("Content-File-MD5", None)
346 elif "application/yaml" in cherrypy
.request
.headers
["Content-Type"]:
347 error_text
= "Invalid yaml format "
348 indata
= yaml
.load(cherrypy
.request
.body
)
349 cherrypy
.request
.headers
.pop("Content-File-MD5", None)
350 elif "application/binary" in cherrypy
.request
.headers
["Content-Type"] or \
351 "application/gzip" in cherrypy
.request
.headers
["Content-Type"] or \
352 "application/zip" in cherrypy
.request
.headers
["Content-Type"] or \
353 "text/plain" in cherrypy
.request
.headers
["Content-Type"]:
354 indata
= cherrypy
.request
.body
# .read()
355 elif "multipart/form-data" in cherrypy
.request
.headers
["Content-Type"]:
356 if "descriptor_file" in kwargs
:
357 filecontent
= kwargs
.pop("descriptor_file")
358 if not filecontent
.file:
359 raise NbiException("empty file or content", HTTPStatus
.BAD_REQUEST
)
360 indata
= filecontent
.file # .read()
361 if filecontent
.content_type
.value
:
362 cherrypy
.request
.headers
["Content-Type"] = filecontent
.content_type
.value
364 # raise cherrypy.HTTPError(HTTPStatus.Not_Acceptable,
365 # "Only 'Content-Type' of type 'application/json' or
366 # 'application/yaml' for input format are available")
367 error_text
= "Invalid yaml format "
368 indata
= yaml
.load(cherrypy
.request
.body
)
369 cherrypy
.request
.headers
.pop("Content-File-MD5", None)
371 error_text
= "Invalid yaml format "
372 indata
= yaml
.load(cherrypy
.request
.body
)
373 cherrypy
.request
.headers
.pop("Content-File-MD5", None)
378 if cherrypy
.request
.headers
.get("Query-String-Format") == "yaml":
381 for k
, v
in kwargs
.items():
382 if isinstance(v
, str):
387 kwargs
[k
] = yaml
.load(v
)
390 elif k
.endswith(".gt") or k
.endswith(".lt") or k
.endswith(".gte") or k
.endswith(".lte"):
398 elif v
.find(",") > 0:
399 kwargs
[k
] = v
.split(",")
400 elif isinstance(v
, (list, tuple)):
401 for index
in range(0, len(v
)):
406 v
[index
] = yaml
.load(v
[index
])
411 except (ValueError, yaml
.YAMLError
) as exc
:
412 raise NbiException(error_text
+ str(exc
), HTTPStatus
.BAD_REQUEST
)
413 except KeyError as exc
:
414 raise NbiException("Query string error: " + str(exc
), HTTPStatus
.BAD_REQUEST
)
415 except Exception as exc
:
416 raise NbiException(error_text
+ str(exc
), HTTPStatus
.BAD_REQUEST
)
419 def _format_out(data
, session
=None, _format
=None):
421 return string of dictionary data according to requested json, yaml, xml. By default json
422 :param data: response to be sent. Can be a dict, text or file
424 :param _format: The format to be set as Content-Type ir data is a file
427 accept
= cherrypy
.request
.headers
.get("Accept")
429 if accept
and "text/html" in accept
:
430 return html
.format(data
, cherrypy
.request
, cherrypy
.response
, session
)
431 # cherrypy.response.status = HTTPStatus.NO_CONTENT.value
433 elif hasattr(data
, "read"): # file object
435 cherrypy
.response
.headers
["Content-Type"] = _format
436 elif "b" in data
.mode
: # binariy asssumig zip
437 cherrypy
.response
.headers
["Content-Type"] = 'application/zip'
439 cherrypy
.response
.headers
["Content-Type"] = 'text/plain'
440 # TODO check that cherrypy close file. If not implement pending things to close per thread next
443 if "application/json" in accept
:
444 cherrypy
.response
.headers
["Content-Type"] = 'application/json; charset=utf-8'
445 a
= json
.dumps(data
, indent
=4) + "\n"
446 return a
.encode("utf8")
447 elif "text/html" in accept
:
448 return html
.format(data
, cherrypy
.request
, cherrypy
.response
, session
)
450 elif "application/yaml" in accept
or "*/*" in accept
or "text/plain" in accept
:
452 # if there is not any valid accept, raise an error. But if response is already an error, format in yaml
453 elif cherrypy
.response
.status
>= 400:
454 raise cherrypy
.HTTPError(HTTPStatus
.NOT_ACCEPTABLE
.value
,
455 "Only 'Accept' of type 'application/json' or 'application/yaml' "
456 "for output format are available")
457 cherrypy
.response
.headers
["Content-Type"] = 'application/yaml'
458 return yaml
.safe_dump(data
, explicit_start
=True, indent
=4, default_flow_style
=False, tags
=False,
459 encoding
='utf-8', allow_unicode
=True) # , canonical=True, default_style='"'
462 def index(self
, *args
, **kwargs
):
465 if cherrypy
.request
.method
== "GET":
466 session
= self
.authenticator
.authorize()
467 outdata
= "Index page"
469 raise cherrypy
.HTTPError(HTTPStatus
.METHOD_NOT_ALLOWED
.value
,
470 "Method {} not allowed for tokens".format(cherrypy
.request
.method
))
472 return self
._format
_out
(outdata
, session
)
474 except (EngineException
, AuthException
) as e
:
475 cherrypy
.log("index Exception {}".format(e
))
476 cherrypy
.response
.status
= e
.http_code
.value
477 return self
._format
_out
("Welcome to OSM!", session
)
480 def version(self
, *args
, **kwargs
):
481 # TODO consider to remove and provide version using the static version file
482 global __version__
, version_date
484 if cherrypy
.request
.method
!= "GET":
485 raise NbiException("Only method GET is allowed", HTTPStatus
.METHOD_NOT_ALLOWED
)
487 raise NbiException("Invalid URL or query string for version", HTTPStatus
.METHOD_NOT_ALLOWED
)
488 return __version__
+ " " + version_date
489 except NbiException
as e
:
490 cherrypy
.response
.status
= e
.http_code
.value
492 "code": e
.http_code
.name
,
493 "status": e
.http_code
.value
,
496 return self
._format
_out
(problem_details
, None)
499 def token(self
, method
, token_id
=None, kwargs
=None):
501 # self.engine.load_dbase(cherrypy.request.app.config)
502 indata
= self
._format
_in
(kwargs
)
503 if not isinstance(indata
, dict):
504 raise NbiException("Expected application/yaml or application/json Content-Type", HTTPStatus
.BAD_REQUEST
)
507 session
= self
.authenticator
.authorize()
509 outdata
= self
.authenticator
.get_token(session
, token_id
)
511 outdata
= self
.authenticator
.get_token_list(session
)
512 elif method
== "POST":
514 session
= self
.authenticator
.authorize()
518 indata
.update(kwargs
)
519 outdata
= self
.authenticator
.new_token(session
, indata
, cherrypy
.request
.remote
)
521 cherrypy
.session
['Authorization'] = outdata
["_id"]
522 self
._set
_location
_header
("admin", "v1", "tokens", outdata
["_id"])
523 # cherrypy.response.cookie["Authorization"] = outdata["id"]
524 # cherrypy.response.cookie["Authorization"]['expires'] = 3600
525 elif method
== "DELETE":
526 if not token_id
and "id" in kwargs
:
527 token_id
= kwargs
["id"]
529 session
= self
.authenticator
.authorize()
530 token_id
= session
["_id"]
531 outdata
= self
.authenticator
.del_token(token_id
)
533 cherrypy
.session
['Authorization'] = "logout"
534 # cherrypy.response.cookie["Authorization"] = token_id
535 # cherrypy.response.cookie["Authorization"]['expires'] = 0
537 raise NbiException("Method {} not allowed for token".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
538 return self
._format
_out
(outdata
, session
)
539 except (NbiException
, EngineException
, DbException
, AuthException
) as e
:
540 cherrypy
.log("tokens Exception {}".format(e
))
541 cherrypy
.response
.status
= e
.http_code
.value
543 "code": e
.http_code
.name
,
544 "status": e
.http_code
.value
,
547 return self
._format
_out
(problem_details
, session
)
550 def test(self
, *args
, **kwargs
):
552 if args
and args
[0] == "help":
553 return "<html><pre>\ninit\nfile/<name> download file\ndb-clear/table\nfs-clear[/folder]\nlogin\nlogin2\n"\
554 "sleep/<time>\nmessage/topic\n</pre></html>"
556 elif args
and args
[0] == "init":
558 # self.engine.load_dbase(cherrypy.request.app.config)
559 self
.engine
.create_admin()
560 return "Done. User 'admin', password 'admin' created"
562 cherrypy
.response
.status
= HTTPStatus
.FORBIDDEN
.value
563 return self
._format
_out
("Database already initialized")
564 elif args
and args
[0] == "file":
565 return cherrypy
.lib
.static
.serve_file(cherrypy
.tree
.apps
['/osm'].config
["storage"]["path"] + "/" + args
[1],
566 "text/plain", "attachment")
567 elif args
and args
[0] == "file2":
568 f_path
= cherrypy
.tree
.apps
['/osm'].config
["storage"]["path"] + "/" + args
[1]
569 f
= open(f_path
, "r")
570 cherrypy
.response
.headers
["Content-type"] = "text/plain"
573 elif len(args
) == 2 and args
[0] == "db-clear":
574 deleted_info
= self
.engine
.db
.del_list(args
[1], kwargs
)
575 return "{} {} deleted\n".format(deleted_info
["deleted"], args
[1])
576 elif len(args
) and args
[0] == "fs-clear":
580 folders
= self
.engine
.fs
.dir_ls(".")
581 for folder
in folders
:
582 self
.engine
.fs
.file_delete(folder
)
583 return ",".join(folders
) + " folders deleted\n"
584 elif args
and args
[0] == "login":
585 if not cherrypy
.request
.headers
.get("Authorization"):
586 cherrypy
.response
.headers
["WWW-Authenticate"] = 'Basic realm="Access to OSM site", charset="UTF-8"'
587 cherrypy
.response
.status
= HTTPStatus
.UNAUTHORIZED
.value
588 elif args
and args
[0] == "login2":
589 if not cherrypy
.request
.headers
.get("Authorization"):
590 cherrypy
.response
.headers
["WWW-Authenticate"] = 'Bearer realm="Access to OSM site"'
591 cherrypy
.response
.status
= HTTPStatus
.UNAUTHORIZED
.value
592 elif args
and args
[0] == "sleep":
595 sleep_time
= int(args
[1])
597 cherrypy
.response
.status
= HTTPStatus
.FORBIDDEN
.value
598 return self
._format
_out
("Database already initialized")
599 thread_info
= cherrypy
.thread_data
601 time
.sleep(sleep_time
)
603 elif len(args
) >= 2 and args
[0] == "message":
605 return_text
= "<html><pre>{} ->\n".format(main_topic
)
607 if cherrypy
.request
.method
== 'POST':
608 to_send
= yaml
.load(cherrypy
.request
.body
)
609 for k
, v
in to_send
.items():
610 self
.engine
.msg
.write(main_topic
, k
, v
)
611 return_text
+= " {}: {}\n".format(k
, v
)
612 elif cherrypy
.request
.method
== 'GET':
613 for k
, v
in kwargs
.items():
614 self
.engine
.msg
.write(main_topic
, k
, yaml
.load(v
))
615 return_text
+= " {}: {}\n".format(k
, yaml
.load(v
))
616 except Exception as e
:
617 return_text
+= "Error: " + str(e
)
618 return_text
+= "</pre></html>\n"
622 "<html><pre>\nheaders:\n args: {}\n".format(args
) +
623 " kwargs: {}\n".format(kwargs
) +
624 " headers: {}\n".format(cherrypy
.request
.headers
) +
625 " path_info: {}\n".format(cherrypy
.request
.path_info
) +
626 " query_string: {}\n".format(cherrypy
.request
.query_string
) +
627 " session: {}\n".format(cherrypy
.session
) +
628 " cookie: {}\n".format(cherrypy
.request
.cookie
) +
629 " method: {}\n".format(cherrypy
.request
.method
) +
630 " session: {}\n".format(cherrypy
.session
.get('fieldname')) +
632 return_text
+= " length: {}\n".format(cherrypy
.request
.body
.length
)
633 if cherrypy
.request
.body
.length
:
634 return_text
+= " content: {}\n".format(
635 str(cherrypy
.request
.body
.read(int(cherrypy
.request
.headers
.get('Content-Length', 0)))))
637 return_text
+= "thread: {}\n".format(thread_info
)
638 return_text
+= "</pre></html>"
641 def _check_valid_url_method(self
, method
, *args
):
643 raise NbiException("URL must contain at least 'main_topic/version/topic'", HTTPStatus
.METHOD_NOT_ALLOWED
)
645 reference
= self
.valid_methods
649 if not isinstance(reference
, dict):
650 raise NbiException("URL contains unexpected extra items '{}'".format(arg
),
651 HTTPStatus
.METHOD_NOT_ALLOWED
)
654 reference
= reference
[arg
]
655 elif "<ID>" in reference
:
656 reference
= reference
["<ID>"]
657 elif "*" in reference
:
658 reference
= reference
["*"]
661 raise NbiException("Unexpected URL item {}".format(arg
), HTTPStatus
.METHOD_NOT_ALLOWED
)
662 if "TODO" in reference
and method
in reference
["TODO"]:
663 raise NbiException("Method {} not supported yet for this URL".format(method
), HTTPStatus
.NOT_IMPLEMENTED
)
664 elif "METHODS" in reference
and method
not in reference
["METHODS"]:
665 raise NbiException("Method {} not supported for this URL".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
669 def _set_location_header(main_topic
, version
, topic
, id):
671 Insert response header Location with the URL of created item base on URL params
678 # Use cherrypy.request.base for absoluted path and make use of request.header HOST just in case behind aNAT
679 cherrypy
.response
.headers
["Location"] = "/osm/{}/{}/{}/{}".format(main_topic
, version
, topic
, id)
683 def default(self
, main_topic
=None, version
=None, topic
=None, _id
=None, item
=None, *args
, **kwargs
):
692 if not main_topic
or not version
or not topic
:
693 raise NbiException("URL must contain at least 'main_topic/version/topic'",
694 HTTPStatus
.METHOD_NOT_ALLOWED
)
695 if main_topic
not in ("admin", "vnfpkgm", "nsd", "nslcm", "pdu", "nst", "nsilcm"):
696 raise NbiException("URL main_topic '{}' not supported".format(main_topic
),
697 HTTPStatus
.METHOD_NOT_ALLOWED
)
699 raise NbiException("URL version '{}' not supported".format(version
), HTTPStatus
.METHOD_NOT_ALLOWED
)
701 if kwargs
and "METHOD" in kwargs
and kwargs
["METHOD"] in ("PUT", "POST", "DELETE", "GET", "PATCH"):
702 method
= kwargs
.pop("METHOD")
704 method
= cherrypy
.request
.method
705 if kwargs
and "FORCE" in kwargs
:
706 force
= kwargs
.pop("FORCE")
709 self
._check
_valid
_url
_method
(method
, main_topic
, version
, topic
, _id
, item
, *args
)
710 if main_topic
== "admin" and topic
== "tokens":
711 return self
.token(method
, _id
, kwargs
)
713 # self.engine.load_dbase(cherrypy.request.app.config)
714 session
= self
.authenticator
.authorize()
715 indata
= self
._format
_in
(kwargs
)
717 if topic
== "subscriptions":
718 engine_topic
= main_topic
+ "_" + topic
722 if main_topic
== "nsd":
723 engine_topic
= "nsds"
724 elif main_topic
== "vnfpkgm":
725 engine_topic
= "vnfds"
726 elif main_topic
== "nslcm":
727 engine_topic
= "nsrs"
728 if topic
== "ns_lcm_op_occs":
729 engine_topic
= "nslcmops"
730 if topic
== "vnfrs" or topic
== "vnf_instances":
731 engine_topic
= "vnfrs"
732 elif main_topic
== "nst":
733 engine_topic
= "nsts"
734 elif main_topic
== "nsilcm":
735 engine_topic
= "nsis"
736 if topic
== "nsi_lcm_op_occs":
737 engine_topic
= "nsilcmops"
738 elif main_topic
== "pdu":
739 engine_topic
= "pdus"
740 if engine_topic
== "vims": # TODO this is for backward compatibility, it will remove in the future
741 engine_topic
= "vim_accounts"
744 if item
in ("nsd_content", "package_content", "artifacts", "vnfd", "nsd", "nst", "nst_content"):
745 if item
in ("vnfd", "nsd", "nst"):
749 elif item
== "artifacts":
753 file, _format
= self
.engine
.get_file(session
, engine_topic
, _id
, path
,
754 cherrypy
.request
.headers
.get("Accept"))
757 outdata
= self
.engine
.get_item_list(session
, engine_topic
, kwargs
)
759 outdata
= self
.engine
.get_item(session
, engine_topic
, _id
)
760 elif method
== "POST":
761 if topic
in ("ns_descriptors_content", "vnf_packages_content", "netslice_templates_content"):
762 _id
= cherrypy
.request
.headers
.get("Transaction-Id")
764 _id
= self
.engine
.new_item(rollback
, session
, engine_topic
, {}, None, cherrypy
.request
.headers
,
766 completed
= self
.engine
.upload_content(session
, engine_topic
, _id
, indata
, kwargs
,
767 cherrypy
.request
.headers
, force
=force
)
769 self
._set
_location
_header
(main_topic
, version
, topic
, _id
)
771 cherrypy
.response
.headers
["Transaction-Id"] = _id
772 outdata
= {"id": _id
}
773 elif topic
== "ns_instances_content":
775 _id
= self
.engine
.new_item(rollback
, session
, engine_topic
, indata
, kwargs
, force
=force
)
777 indata
["lcmOperationType"] = "instantiate"
778 indata
["nsInstanceId"] = _id
779 self
.engine
.new_item(rollback
, session
, "nslcmops", indata
, None)
780 self
._set
_location
_header
(main_topic
, version
, topic
, _id
)
781 outdata
= {"id": _id
}
782 elif topic
== "ns_instances" and item
:
783 indata
["lcmOperationType"] = item
784 indata
["nsInstanceId"] = _id
785 _id
= self
.engine
.new_item(rollback
, session
, "nslcmops", indata
, kwargs
)
786 self
._set
_location
_header
(main_topic
, version
, "ns_lcm_op_occs", _id
)
787 outdata
= {"id": _id
}
788 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
789 elif topic
== "netslice_instances_content":
790 # creates NetSlice_Instance_record (NSIR)
791 _id
= self
.engine
.new_item(rollback
, session
, engine_topic
, indata
, kwargs
, force
=force
)
792 self
._set
_location
_header
(main_topic
, version
, topic
, _id
)
793 indata
["lcmOperationType"] = "instantiate"
794 indata
["nsiInstanceId"] = _id
795 self
.engine
.new_item(rollback
, session
, "nsilcmops", indata
, kwargs
)
796 outdata
= {"id": _id
}
798 elif topic
== "netslice_instances" and item
:
799 indata
["lcmOperationType"] = item
800 indata
["nsiInstanceId"] = _id
801 _id
= self
.engine
.new_item(rollback
, session
, "nsilcmops", indata
, kwargs
)
802 self
._set
_location
_header
(main_topic
, version
, "nsi_lcm_op_occs", _id
)
803 outdata
= {"id": _id
}
804 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
806 _id
= self
.engine
.new_item(rollback
, session
, engine_topic
, indata
, kwargs
,
807 cherrypy
.request
.headers
, force
=force
)
808 self
._set
_location
_header
(main_topic
, version
, topic
, _id
)
809 outdata
= {"id": _id
}
810 # TODO form NsdInfo when topic in ("ns_descriptors", "vnf_packages")
811 cherrypy
.response
.status
= HTTPStatus
.CREATED
.value
813 elif method
== "DELETE":
815 outdata
= self
.engine
.del_item_list(session
, engine_topic
, kwargs
)
816 cherrypy
.response
.status
= HTTPStatus
.OK
.value
817 else: # len(args) > 1
818 delete_in_process
= False
819 if topic
== "ns_instances_content" and not force
:
821 "lcmOperationType": "terminate",
825 opp_id
= self
.engine
.new_item(rollback
, session
, "nslcmops", nslcmop_desc
, None)
827 delete_in_process
= True
828 outdata
= {"_id": opp_id
}
829 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
830 elif topic
== "netslice_instances_content" and not force
:
832 "lcmOperationType": "terminate",
833 "nsiInstanceId": _id
,
836 opp_id
= self
.engine
.new_item(rollback
, session
, "nsilcmops", nsilcmop_desc
, None)
838 delete_in_process
= True
839 outdata
= {"_id": opp_id
}
840 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
841 if not delete_in_process
:
842 self
.engine
.del_item(session
, engine_topic
, _id
, force
)
843 cherrypy
.response
.status
= HTTPStatus
.NO_CONTENT
.value
844 if engine_topic
in ("vim_accounts", "wim_accounts", "sdns"):
845 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
847 elif method
in ("PUT", "PATCH"):
849 if not indata
and not kwargs
:
850 raise NbiException("Nothing to update. Provide payload and/or query string",
851 HTTPStatus
.BAD_REQUEST
)
852 if item
in ("nsd_content", "package_content", "nst_content") and method
== "PUT":
853 completed
= self
.engine
.upload_content(session
, engine_topic
, _id
, indata
, kwargs
,
854 cherrypy
.request
.headers
, force
=force
)
856 cherrypy
.response
.headers
["Transaction-Id"] = id
858 self
.engine
.edit_item(session
, engine_topic
, _id
, indata
, kwargs
, force
=force
)
859 cherrypy
.response
.status
= HTTPStatus
.NO_CONTENT
.value
861 raise NbiException("Method {} not allowed".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
862 return self
._format
_out
(outdata
, session
, _format
)
863 except Exception as e
:
864 if isinstance(e
, (NbiException
, EngineException
, DbException
, FsException
, MsgException
, AuthException
,
866 http_code_value
= cherrypy
.response
.status
= e
.http_code
.value
867 http_code_name
= e
.http_code
.name
868 cherrypy
.log("Exception {}".format(e
))
870 http_code_value
= cherrypy
.response
.status
= HTTPStatus
.BAD_REQUEST
.value
# INTERNAL_SERVER_ERROR
871 cherrypy
.log("CRITICAL: Exception {}".format(e
), traceback
=True)
872 http_code_name
= HTTPStatus
.BAD_REQUEST
.name
873 if hasattr(outdata
, "close"): # is an open file
877 for rollback_item
in rollback
:
879 if rollback_item
.get("operation") == "set":
880 self
.engine
.db
.set_one(rollback_item
["topic"], {"_id": rollback_item
["_id"]},
881 rollback_item
["content"], fail_on_empty
=False)
883 self
.engine
.db
.del_one(rollback_item
["topic"], {"_id": rollback_item
["_id"]},
885 except Exception as e2
:
886 rollback_error_text
= "Rollback Exception {}: {}".format(rollback_item
, e2
)
887 cherrypy
.log(rollback_error_text
)
888 error_text
+= ". " + rollback_error_text
889 # if isinstance(e, MsgException):
890 # error_text = "{} has been '{}' but other modules cannot be informed because an error on bus".format(
891 # engine_topic[:-1], method, error_text)
893 "code": http_code_name
,
894 "status": http_code_value
,
895 "detail": error_text
,
897 return self
._format
_out
(problem_details
, session
)
898 # raise cherrypy.HTTPError(e.http_code.value, str(e))
901 # def validate_password(realm, username, password):
902 # cherrypy.log("realm "+ str(realm))
903 # if username == "admin" and password == "admin":
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
915 cherrypy
.log
.error("Starting osm_nbi")
916 # update general cherrypy configuration
919 engine_config
= cherrypy
.tree
.apps
['/osm'].config
920 for k
, v
in environ
.items():
921 if not k
.startswith("OSMNBI_"):
923 k1
, _
, k2
= k
[7:].lower().partition("_")
927 # update static configuration
928 if k
== 'OSMNBI_STATIC_DIR':
929 engine_config
["/static"]['tools.staticdir.dir'] = v
930 engine_config
["/static"]['tools.staticdir.on'] = True
931 elif k
== 'OSMNBI_SOCKET_PORT' or k
== 'OSMNBI_SERVER_PORT':
932 update_dict
['server.socket_port'] = int(v
)
933 elif k
== 'OSMNBI_SOCKET_HOST' or k
== 'OSMNBI_SERVER_HOST':
934 update_dict
['server.socket_host'] = v
935 elif k1
in ("server", "test", "auth", "log"):
936 update_dict
[k1
+ '.' + k2
] = v
937 elif k1
in ("message", "database", "storage", "authentication"):
938 # k2 = k2.replace('_', '.')
939 if k2
in ("port", "db_port"):
940 engine_config
[k1
][k2
] = int(v
)
942 engine_config
[k1
][k2
] = v
944 except ValueError as e
:
945 cherrypy
.log
.error("Ignoring environ '{}': " + str(e
))
946 except Exception as e
:
947 cherrypy
.log
.warn("skipping environ '{}' on exception '{}'".format(k
, e
))
950 cherrypy
.config
.update(update_dict
)
951 engine_config
["global"].update(update_dict
)
954 log_format_simple
= "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(message)s"
955 log_formatter_simple
= logging
.Formatter(log_format_simple
, datefmt
='%Y-%m-%dT%H:%M:%S')
956 logger_server
= logging
.getLogger("cherrypy.error")
957 logger_access
= logging
.getLogger("cherrypy.access")
958 logger_cherry
= logging
.getLogger("cherrypy")
959 logger_nbi
= logging
.getLogger("nbi")
961 if "log.file" in engine_config
["global"]:
962 file_handler
= logging
.handlers
.RotatingFileHandler(engine_config
["global"]["log.file"],
963 maxBytes
=100e6
, backupCount
=9, delay
=0)
964 file_handler
.setFormatter(log_formatter_simple
)
965 logger_cherry
.addHandler(file_handler
)
966 logger_nbi
.addHandler(file_handler
)
967 # log always to standard output
968 for format_
, logger
in {"nbi.server %(filename)s:%(lineno)s": logger_server
,
969 "nbi.access %(filename)s:%(lineno)s": logger_access
,
970 "%(name)s %(filename)s:%(lineno)s": logger_nbi
972 log_format_cherry
= "%(asctime)s %(levelname)s {} %(message)s".format(format_
)
973 log_formatter_cherry
= logging
.Formatter(log_format_cherry
, datefmt
='%Y-%m-%dT%H:%M:%S')
974 str_handler
= logging
.StreamHandler()
975 str_handler
.setFormatter(log_formatter_cherry
)
976 logger
.addHandler(str_handler
)
978 if engine_config
["global"].get("log.level"):
979 logger_cherry
.setLevel(engine_config
["global"]["log.level"])
980 logger_nbi
.setLevel(engine_config
["global"]["log.level"])
982 # logging other modules
983 for k1
, logname
in {"message": "nbi.msg", "database": "nbi.db", "storage": "nbi.fs"}.items():
984 engine_config
[k1
]["logger_name"] = logname
985 logger_module
= logging
.getLogger(logname
)
986 if "logfile" in engine_config
[k1
]:
987 file_handler
= logging
.handlers
.RotatingFileHandler(engine_config
[k1
]["logfile"],
988 maxBytes
=100e6
, backupCount
=9, delay
=0)
989 file_handler
.setFormatter(log_formatter_simple
)
990 logger_module
.addHandler(file_handler
)
991 if "loglevel" in engine_config
[k1
]:
992 logger_module
.setLevel(engine_config
[k1
]["loglevel"])
993 # TODO add more entries, e.g.: storage
994 cherrypy
.tree
.apps
['/osm'].root
.engine
.start(engine_config
)
995 cherrypy
.tree
.apps
['/osm'].root
.authenticator
.start(engine_config
)
996 cherrypy
.tree
.apps
['/osm'].root
.engine
.init_db(target_version
=database_version
)
997 cherrypy
.tree
.apps
['/osm'].root
.authenticator
.init_db(target_version
=auth_database_version
)
998 # getenv('OSMOPENMANO_TENANT', None)
1001 def _stop_service():
1003 Callback function called when cherrypy.engine stops
1004 TODO: Ending database connections.
1006 cherrypy
.tree
.apps
['/osm'].root
.engine
.stop()
1007 cherrypy
.log
.error("Stopping osm_nbi")
1010 def nbi(config_file
):
1013 # #'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
1014 # 'tools.sessions.on': True,
1015 # 'tools.response_headers.on': True,
1016 # # 'tools.response_headers.headers': [('Content-Type', 'text/plain')],
1019 # cherrypy.Server.ssl_module = 'builtin'
1020 # cherrypy.Server.ssl_certificate = "http/cert.pem"
1021 # cherrypy.Server.ssl_private_key = "http/privkey.pem"
1022 # cherrypy.Server.thread_pool = 10
1023 # cherrypy.config.update({'Server.socket_port': config["port"], 'Server.socket_host': config["host"]})
1025 # cherrypy.config.update({'tools.auth_basic.on': True,
1026 # 'tools.auth_basic.realm': 'localhost',
1027 # 'tools.auth_basic.checkpassword': validate_password})
1028 cherrypy
.engine
.subscribe('start', _start_service
)
1029 cherrypy
.engine
.subscribe('stop', _stop_service
)
1030 cherrypy
.quickstart(Server(), '/osm', config_file
)
1034 print("""Usage: {} [options]
1035 -c|--config [configuration_file]: loads the configuration file (default: ./nbi.cfg)
1036 -h|--help: shows this help
1037 """.format(sys
.argv
[0]))
1038 # --log-socket-host HOST: send logs to this host")
1039 # --log-socket-port PORT: send logs using this port (default: 9022)")
1042 if __name__
== '__main__':
1044 # load parameters and configuration
1045 opts
, args
= getopt
.getopt(sys
.argv
[1:], "hvc:", ["config=", "help"])
1046 # TODO add "log-socket-host=", "log-socket-port=", "log-file="
1049 if o
in ("-h", "--help"):
1052 elif o
in ("-c", "--config"):
1054 # elif o == "--log-socket-port":
1055 # log_socket_port = a
1056 # elif o == "--log-socket-host":
1057 # log_socket_host = a
1058 # elif o == "--log-file":
1061 assert False, "Unhandled option"
1063 if not path
.isfile(config_file
):
1064 print("configuration file '{}' that not exist".format(config_file
), file=sys
.stderr
)
1067 for config_file
in (__file__
[:__file__
.rfind(".")] + ".cfg", "./nbi.cfg", "/etc/osm/nbi.cfg"):
1068 if path
.isfile(config_file
):
1071 print("No configuration file 'nbi.cfg' found neither at local folder nor at /etc/osm/", file=sys
.stderr
)
1074 except getopt
.GetoptError
as e
:
1075 print(str(e
), file=sys
.stderr
)