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"},
345 "<ID>": {"METHODS": ("GET")}
353 def _format_in(self
, kwargs
):
356 if cherrypy
.request
.body
.length
:
357 error_text
= "Invalid input format "
359 if "Content-Type" in cherrypy
.request
.headers
:
360 if "application/json" in cherrypy
.request
.headers
["Content-Type"]:
361 error_text
= "Invalid json format "
362 indata
= json
.load(self
.reader(cherrypy
.request
.body
))
363 cherrypy
.request
.headers
.pop("Content-File-MD5", None)
364 elif "application/yaml" in cherrypy
.request
.headers
["Content-Type"]:
365 error_text
= "Invalid yaml format "
366 indata
= yaml
.load(cherrypy
.request
.body
)
367 cherrypy
.request
.headers
.pop("Content-File-MD5", None)
368 elif "application/binary" in cherrypy
.request
.headers
["Content-Type"] or \
369 "application/gzip" in cherrypy
.request
.headers
["Content-Type"] or \
370 "application/zip" in cherrypy
.request
.headers
["Content-Type"] or \
371 "text/plain" in cherrypy
.request
.headers
["Content-Type"]:
372 indata
= cherrypy
.request
.body
# .read()
373 elif "multipart/form-data" in cherrypy
.request
.headers
["Content-Type"]:
374 if "descriptor_file" in kwargs
:
375 filecontent
= kwargs
.pop("descriptor_file")
376 if not filecontent
.file:
377 raise NbiException("empty file or content", HTTPStatus
.BAD_REQUEST
)
378 indata
= filecontent
.file # .read()
379 if filecontent
.content_type
.value
:
380 cherrypy
.request
.headers
["Content-Type"] = filecontent
.content_type
.value
382 # raise cherrypy.HTTPError(HTTPStatus.Not_Acceptable,
383 # "Only 'Content-Type' of type 'application/json' or
384 # 'application/yaml' for input format are available")
385 error_text
= "Invalid yaml format "
386 indata
= yaml
.load(cherrypy
.request
.body
)
387 cherrypy
.request
.headers
.pop("Content-File-MD5", None)
389 error_text
= "Invalid yaml format "
390 indata
= yaml
.load(cherrypy
.request
.body
)
391 cherrypy
.request
.headers
.pop("Content-File-MD5", None)
396 if cherrypy
.request
.headers
.get("Query-String-Format") == "yaml":
399 for k
, v
in kwargs
.items():
400 if isinstance(v
, str):
405 kwargs
[k
] = yaml
.load(v
)
408 elif k
.endswith(".gt") or k
.endswith(".lt") or k
.endswith(".gte") or k
.endswith(".lte"):
416 elif v
.find(",") > 0:
417 kwargs
[k
] = v
.split(",")
418 elif isinstance(v
, (list, tuple)):
419 for index
in range(0, len(v
)):
424 v
[index
] = yaml
.load(v
[index
])
429 except (ValueError, yaml
.YAMLError
) as exc
:
430 raise NbiException(error_text
+ str(exc
), HTTPStatus
.BAD_REQUEST
)
431 except KeyError as exc
:
432 raise NbiException("Query string error: " + str(exc
), HTTPStatus
.BAD_REQUEST
)
433 except Exception as exc
:
434 raise NbiException(error_text
+ str(exc
), HTTPStatus
.BAD_REQUEST
)
437 def _format_out(data
, session
=None, _format
=None):
439 return string of dictionary data according to requested json, yaml, xml. By default json
440 :param data: response to be sent. Can be a dict, text or file
442 :param _format: The format to be set as Content-Type ir data is a file
445 accept
= cherrypy
.request
.headers
.get("Accept")
447 if accept
and "text/html" in accept
:
448 return html
.format(data
, cherrypy
.request
, cherrypy
.response
, session
)
449 # cherrypy.response.status = HTTPStatus.NO_CONTENT.value
451 elif hasattr(data
, "read"): # file object
453 cherrypy
.response
.headers
["Content-Type"] = _format
454 elif "b" in data
.mode
: # binariy asssumig zip
455 cherrypy
.response
.headers
["Content-Type"] = 'application/zip'
457 cherrypy
.response
.headers
["Content-Type"] = 'text/plain'
458 # TODO check that cherrypy close file. If not implement pending things to close per thread next
461 if "application/json" in accept
:
462 cherrypy
.response
.headers
["Content-Type"] = 'application/json; charset=utf-8'
463 a
= json
.dumps(data
, indent
=4) + "\n"
464 return a
.encode("utf8")
465 elif "text/html" in accept
:
466 return html
.format(data
, cherrypy
.request
, cherrypy
.response
, session
)
468 elif "application/yaml" in accept
or "*/*" in accept
or "text/plain" in accept
:
470 # if there is not any valid accept, raise an error. But if response is already an error, format in yaml
471 elif cherrypy
.response
.status
>= 400:
472 raise cherrypy
.HTTPError(HTTPStatus
.NOT_ACCEPTABLE
.value
,
473 "Only 'Accept' of type 'application/json' or 'application/yaml' "
474 "for output format are available")
475 cherrypy
.response
.headers
["Content-Type"] = 'application/yaml'
476 return yaml
.safe_dump(data
, explicit_start
=True, indent
=4, default_flow_style
=False, tags
=False,
477 encoding
='utf-8', allow_unicode
=True) # , canonical=True, default_style='"'
480 def index(self
, *args
, **kwargs
):
483 if cherrypy
.request
.method
== "GET":
484 session
= self
.authenticator
.authorize()
485 outdata
= "Index page"
487 raise cherrypy
.HTTPError(HTTPStatus
.METHOD_NOT_ALLOWED
.value
,
488 "Method {} not allowed for tokens".format(cherrypy
.request
.method
))
490 return self
._format
_out
(outdata
, session
)
492 except (EngineException
, AuthException
) as e
:
493 cherrypy
.log("index Exception {}".format(e
))
494 cherrypy
.response
.status
= e
.http_code
.value
495 return self
._format
_out
("Welcome to OSM!", session
)
498 def version(self
, *args
, **kwargs
):
499 # TODO consider to remove and provide version using the static version file
500 global __version__
, version_date
502 if cherrypy
.request
.method
!= "GET":
503 raise NbiException("Only method GET is allowed", HTTPStatus
.METHOD_NOT_ALLOWED
)
505 raise NbiException("Invalid URL or query string for version", HTTPStatus
.METHOD_NOT_ALLOWED
)
506 return __version__
+ " " + version_date
507 except NbiException
as e
:
508 cherrypy
.response
.status
= e
.http_code
.value
510 "code": e
.http_code
.name
,
511 "status": e
.http_code
.value
,
514 return self
._format
_out
(problem_details
, None)
517 def token(self
, method
, token_id
=None, kwargs
=None):
519 # self.engine.load_dbase(cherrypy.request.app.config)
520 indata
= self
._format
_in
(kwargs
)
521 if not isinstance(indata
, dict):
522 raise NbiException("Expected application/yaml or application/json Content-Type", HTTPStatus
.BAD_REQUEST
)
525 session
= self
.authenticator
.authorize()
527 outdata
= self
.authenticator
.get_token(session
, token_id
)
529 outdata
= self
.authenticator
.get_token_list(session
)
530 elif method
== "POST":
532 session
= self
.authenticator
.authorize()
536 indata
.update(kwargs
)
537 outdata
= self
.authenticator
.new_token(session
, indata
, cherrypy
.request
.remote
)
539 cherrypy
.session
['Authorization'] = outdata
["_id"]
540 self
._set
_location
_header
("admin", "v1", "tokens", outdata
["_id"])
541 # cherrypy.response.cookie["Authorization"] = outdata["id"]
542 # cherrypy.response.cookie["Authorization"]['expires'] = 3600
543 elif method
== "DELETE":
544 if not token_id
and "id" in kwargs
:
545 token_id
= kwargs
["id"]
547 session
= self
.authenticator
.authorize()
548 token_id
= session
["_id"]
549 outdata
= self
.authenticator
.del_token(token_id
)
551 cherrypy
.session
['Authorization'] = "logout"
552 # cherrypy.response.cookie["Authorization"] = token_id
553 # cherrypy.response.cookie["Authorization"]['expires'] = 0
555 raise NbiException("Method {} not allowed for token".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
556 return self
._format
_out
(outdata
, session
)
557 except (NbiException
, EngineException
, DbException
, AuthException
) as e
:
558 cherrypy
.log("tokens Exception {}".format(e
))
559 cherrypy
.response
.status
= e
.http_code
.value
561 "code": e
.http_code
.name
,
562 "status": e
.http_code
.value
,
565 return self
._format
_out
(problem_details
, session
)
568 def test(self
, *args
, **kwargs
):
570 if args
and args
[0] == "help":
571 return "<html><pre>\ninit\nfile/<name> download file\ndb-clear/table\nfs-clear[/folder]\nlogin\nlogin2\n"\
572 "sleep/<time>\nmessage/topic\n</pre></html>"
574 elif args
and args
[0] == "init":
576 # self.engine.load_dbase(cherrypy.request.app.config)
577 self
.engine
.create_admin()
578 return "Done. User 'admin', password 'admin' created"
580 cherrypy
.response
.status
= HTTPStatus
.FORBIDDEN
.value
581 return self
._format
_out
("Database already initialized")
582 elif args
and args
[0] == "file":
583 return cherrypy
.lib
.static
.serve_file(cherrypy
.tree
.apps
['/osm'].config
["storage"]["path"] + "/" + args
[1],
584 "text/plain", "attachment")
585 elif args
and args
[0] == "file2":
586 f_path
= cherrypy
.tree
.apps
['/osm'].config
["storage"]["path"] + "/" + args
[1]
587 f
= open(f_path
, "r")
588 cherrypy
.response
.headers
["Content-type"] = "text/plain"
591 elif len(args
) == 2 and args
[0] == "db-clear":
592 deleted_info
= self
.engine
.db
.del_list(args
[1], kwargs
)
593 return "{} {} deleted\n".format(deleted_info
["deleted"], args
[1])
594 elif len(args
) and args
[0] == "fs-clear":
598 folders
= self
.engine
.fs
.dir_ls(".")
599 for folder
in folders
:
600 self
.engine
.fs
.file_delete(folder
)
601 return ",".join(folders
) + " folders deleted\n"
602 elif args
and args
[0] == "login":
603 if not cherrypy
.request
.headers
.get("Authorization"):
604 cherrypy
.response
.headers
["WWW-Authenticate"] = 'Basic realm="Access to OSM site", charset="UTF-8"'
605 cherrypy
.response
.status
= HTTPStatus
.UNAUTHORIZED
.value
606 elif args
and args
[0] == "login2":
607 if not cherrypy
.request
.headers
.get("Authorization"):
608 cherrypy
.response
.headers
["WWW-Authenticate"] = 'Bearer realm="Access to OSM site"'
609 cherrypy
.response
.status
= HTTPStatus
.UNAUTHORIZED
.value
610 elif args
and args
[0] == "sleep":
613 sleep_time
= int(args
[1])
615 cherrypy
.response
.status
= HTTPStatus
.FORBIDDEN
.value
616 return self
._format
_out
("Database already initialized")
617 thread_info
= cherrypy
.thread_data
619 time
.sleep(sleep_time
)
621 elif len(args
) >= 2 and args
[0] == "message":
623 return_text
= "<html><pre>{} ->\n".format(main_topic
)
625 if cherrypy
.request
.method
== 'POST':
626 to_send
= yaml
.load(cherrypy
.request
.body
)
627 for k
, v
in to_send
.items():
628 self
.engine
.msg
.write(main_topic
, k
, v
)
629 return_text
+= " {}: {}\n".format(k
, v
)
630 elif cherrypy
.request
.method
== 'GET':
631 for k
, v
in kwargs
.items():
632 self
.engine
.msg
.write(main_topic
, k
, yaml
.load(v
))
633 return_text
+= " {}: {}\n".format(k
, yaml
.load(v
))
634 except Exception as e
:
635 return_text
+= "Error: " + str(e
)
636 return_text
+= "</pre></html>\n"
640 "<html><pre>\nheaders:\n args: {}\n".format(args
) +
641 " kwargs: {}\n".format(kwargs
) +
642 " headers: {}\n".format(cherrypy
.request
.headers
) +
643 " path_info: {}\n".format(cherrypy
.request
.path_info
) +
644 " query_string: {}\n".format(cherrypy
.request
.query_string
) +
645 " session: {}\n".format(cherrypy
.session
) +
646 " cookie: {}\n".format(cherrypy
.request
.cookie
) +
647 " method: {}\n".format(cherrypy
.request
.method
) +
648 " session: {}\n".format(cherrypy
.session
.get('fieldname')) +
650 return_text
+= " length: {}\n".format(cherrypy
.request
.body
.length
)
651 if cherrypy
.request
.body
.length
:
652 return_text
+= " content: {}\n".format(
653 str(cherrypy
.request
.body
.read(int(cherrypy
.request
.headers
.get('Content-Length', 0)))))
655 return_text
+= "thread: {}\n".format(thread_info
)
656 return_text
+= "</pre></html>"
659 def _check_valid_url_method(self
, method
, *args
):
661 raise NbiException("URL must contain at least 'main_topic/version/topic'", HTTPStatus
.METHOD_NOT_ALLOWED
)
663 reference
= self
.valid_methods
667 if not isinstance(reference
, dict):
668 raise NbiException("URL contains unexpected extra items '{}'".format(arg
),
669 HTTPStatus
.METHOD_NOT_ALLOWED
)
672 reference
= reference
[arg
]
673 elif "<ID>" in reference
:
674 reference
= reference
["<ID>"]
675 elif "*" in reference
:
676 reference
= reference
["*"]
679 raise NbiException("Unexpected URL item {}".format(arg
), HTTPStatus
.METHOD_NOT_ALLOWED
)
680 if "TODO" in reference
and method
in reference
["TODO"]:
681 raise NbiException("Method {} not supported yet for this URL".format(method
), HTTPStatus
.NOT_IMPLEMENTED
)
682 elif "METHODS" in reference
and method
not in reference
["METHODS"]:
683 raise NbiException("Method {} not supported for this URL".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
687 def _set_location_header(main_topic
, version
, topic
, id):
689 Insert response header Location with the URL of created item base on URL params
696 # Use cherrypy.request.base for absoluted path and make use of request.header HOST just in case behind aNAT
697 cherrypy
.response
.headers
["Location"] = "/osm/{}/{}/{}/{}".format(main_topic
, version
, topic
, id)
701 def default(self
, main_topic
=None, version
=None, topic
=None, _id
=None, item
=None, *args
, **kwargs
):
710 if not main_topic
or not version
or not topic
:
711 raise NbiException("URL must contain at least 'main_topic/version/topic'",
712 HTTPStatus
.METHOD_NOT_ALLOWED
)
713 if main_topic
not in ("admin", "vnfpkgm", "nsd", "nslcm", "pdu", "nst", "nsilcm", "nspm"):
714 raise NbiException("URL main_topic '{}' not supported".format(main_topic
),
715 HTTPStatus
.METHOD_NOT_ALLOWED
)
717 raise NbiException("URL version '{}' not supported".format(version
), HTTPStatus
.METHOD_NOT_ALLOWED
)
719 if kwargs
and "METHOD" in kwargs
and kwargs
["METHOD"] in ("PUT", "POST", "DELETE", "GET", "PATCH"):
720 method
= kwargs
.pop("METHOD")
722 method
= cherrypy
.request
.method
723 if kwargs
and "FORCE" in kwargs
:
724 force
= kwargs
.pop("FORCE")
727 self
._check
_valid
_url
_method
(method
, main_topic
, version
, topic
, _id
, item
, *args
)
728 if main_topic
== "admin" and topic
== "tokens":
729 return self
.token(method
, _id
, kwargs
)
731 # self.engine.load_dbase(cherrypy.request.app.config)
732 session
= self
.authenticator
.authorize()
733 indata
= self
._format
_in
(kwargs
)
735 if topic
== "subscriptions":
736 engine_topic
= main_topic
+ "_" + topic
737 if item
and topic
!= "pm_jobs":
740 if main_topic
== "nsd":
741 engine_topic
= "nsds"
742 elif main_topic
== "vnfpkgm":
743 engine_topic
= "vnfds"
744 elif main_topic
== "nslcm":
745 engine_topic
= "nsrs"
746 if topic
== "ns_lcm_op_occs":
747 engine_topic
= "nslcmops"
748 if topic
== "vnfrs" or topic
== "vnf_instances":
749 engine_topic
= "vnfrs"
750 elif main_topic
== "nst":
751 engine_topic
= "nsts"
752 elif main_topic
== "nsilcm":
753 engine_topic
= "nsis"
754 if topic
== "nsi_lcm_op_occs":
755 engine_topic
= "nsilcmops"
756 elif main_topic
== "pdu":
757 engine_topic
= "pdus"
758 if engine_topic
== "vims": # TODO this is for backward compatibility, it will remove in the future
759 engine_topic
= "vim_accounts"
762 if item
in ("nsd_content", "package_content", "artifacts", "vnfd", "nsd", "nst", "nst_content"):
763 if item
in ("vnfd", "nsd", "nst"):
767 elif item
== "artifacts":
771 file, _format
= self
.engine
.get_file(session
, engine_topic
, _id
, path
,
772 cherrypy
.request
.headers
.get("Accept"))
775 outdata
= self
.engine
.get_item_list(session
, engine_topic
, kwargs
)
777 if item
== "reports":
778 # TODO check that project_id (_id in this context) has permissions
780 outdata
= self
.engine
.get_item(session
, engine_topic
, _id
)
781 elif method
== "POST":
782 if topic
in ("ns_descriptors_content", "vnf_packages_content", "netslice_templates_content"):
783 _id
= cherrypy
.request
.headers
.get("Transaction-Id")
785 _id
= self
.engine
.new_item(rollback
, session
, engine_topic
, {}, None, cherrypy
.request
.headers
,
787 completed
= self
.engine
.upload_content(session
, engine_topic
, _id
, indata
, kwargs
,
788 cherrypy
.request
.headers
, force
=force
)
790 self
._set
_location
_header
(main_topic
, version
, topic
, _id
)
792 cherrypy
.response
.headers
["Transaction-Id"] = _id
793 outdata
= {"id": _id
}
794 elif topic
== "ns_instances_content":
796 _id
= self
.engine
.new_item(rollback
, session
, engine_topic
, indata
, kwargs
, force
=force
)
798 indata
["lcmOperationType"] = "instantiate"
799 indata
["nsInstanceId"] = _id
800 self
.engine
.new_item(rollback
, session
, "nslcmops", indata
, None)
801 self
._set
_location
_header
(main_topic
, version
, topic
, _id
)
802 outdata
= {"id": _id
}
803 elif topic
== "ns_instances" and item
:
804 indata
["lcmOperationType"] = item
805 indata
["nsInstanceId"] = _id
806 _id
= self
.engine
.new_item(rollback
, session
, "nslcmops", indata
, kwargs
)
807 self
._set
_location
_header
(main_topic
, version
, "ns_lcm_op_occs", _id
)
808 outdata
= {"id": _id
}
809 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
810 elif topic
== "netslice_instances_content":
811 # creates NetSlice_Instance_record (NSIR)
812 _id
= self
.engine
.new_item(rollback
, session
, engine_topic
, indata
, kwargs
, force
=force
)
813 self
._set
_location
_header
(main_topic
, version
, topic
, _id
)
814 indata
["lcmOperationType"] = "instantiate"
815 indata
["nsiInstanceId"] = _id
816 self
.engine
.new_item(rollback
, session
, "nsilcmops", indata
, kwargs
)
817 outdata
= {"id": _id
}
819 elif topic
== "netslice_instances" and item
:
820 indata
["lcmOperationType"] = item
821 indata
["nsiInstanceId"] = _id
822 _id
= self
.engine
.new_item(rollback
, session
, "nsilcmops", indata
, kwargs
)
823 self
._set
_location
_header
(main_topic
, version
, "nsi_lcm_op_occs", _id
)
824 outdata
= {"id": _id
}
825 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
827 _id
= self
.engine
.new_item(rollback
, session
, engine_topic
, indata
, kwargs
,
828 cherrypy
.request
.headers
, force
=force
)
829 self
._set
_location
_header
(main_topic
, version
, topic
, _id
)
830 outdata
= {"id": _id
}
831 # TODO form NsdInfo when topic in ("ns_descriptors", "vnf_packages")
832 cherrypy
.response
.status
= HTTPStatus
.CREATED
.value
834 elif method
== "DELETE":
836 outdata
= self
.engine
.del_item_list(session
, engine_topic
, kwargs
)
837 cherrypy
.response
.status
= HTTPStatus
.OK
.value
838 else: # len(args) > 1
839 delete_in_process
= False
840 if topic
== "ns_instances_content" and not force
:
842 "lcmOperationType": "terminate",
846 opp_id
= self
.engine
.new_item(rollback
, session
, "nslcmops", nslcmop_desc
, None)
848 delete_in_process
= True
849 outdata
= {"_id": opp_id
}
850 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
851 elif topic
== "netslice_instances_content" and not force
:
853 "lcmOperationType": "terminate",
854 "nsiInstanceId": _id
,
857 opp_id
= self
.engine
.new_item(rollback
, session
, "nsilcmops", nsilcmop_desc
, None)
859 delete_in_process
= True
860 outdata
= {"_id": opp_id
}
861 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
862 if not delete_in_process
:
863 self
.engine
.del_item(session
, engine_topic
, _id
, force
)
864 cherrypy
.response
.status
= HTTPStatus
.NO_CONTENT
.value
865 if engine_topic
in ("vim_accounts", "wim_accounts", "sdns"):
866 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
868 elif method
in ("PUT", "PATCH"):
870 if not indata
and not kwargs
:
871 raise NbiException("Nothing to update. Provide payload and/or query string",
872 HTTPStatus
.BAD_REQUEST
)
873 if item
in ("nsd_content", "package_content", "nst_content") and method
== "PUT":
874 completed
= self
.engine
.upload_content(session
, engine_topic
, _id
, indata
, kwargs
,
875 cherrypy
.request
.headers
, force
=force
)
877 cherrypy
.response
.headers
["Transaction-Id"] = id
879 self
.engine
.edit_item(session
, engine_topic
, _id
, indata
, kwargs
, force
=force
)
880 cherrypy
.response
.status
= HTTPStatus
.NO_CONTENT
.value
882 raise NbiException("Method {} not allowed".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
883 return self
._format
_out
(outdata
, session
, _format
)
884 except Exception as e
:
885 if isinstance(e
, (NbiException
, EngineException
, DbException
, FsException
, MsgException
, AuthException
,
887 http_code_value
= cherrypy
.response
.status
= e
.http_code
.value
888 http_code_name
= e
.http_code
.name
889 cherrypy
.log("Exception {}".format(e
))
891 http_code_value
= cherrypy
.response
.status
= HTTPStatus
.BAD_REQUEST
.value
# INTERNAL_SERVER_ERROR
892 cherrypy
.log("CRITICAL: Exception {}".format(e
), traceback
=True)
893 http_code_name
= HTTPStatus
.BAD_REQUEST
.name
894 if hasattr(outdata
, "close"): # is an open file
898 for rollback_item
in rollback
:
900 if rollback_item
.get("operation") == "set":
901 self
.engine
.db
.set_one(rollback_item
["topic"], {"_id": rollback_item
["_id"]},
902 rollback_item
["content"], fail_on_empty
=False)
904 self
.engine
.db
.del_one(rollback_item
["topic"], {"_id": rollback_item
["_id"]},
906 except Exception as e2
:
907 rollback_error_text
= "Rollback Exception {}: {}".format(rollback_item
, e2
)
908 cherrypy
.log(rollback_error_text
)
909 error_text
+= ". " + rollback_error_text
910 # if isinstance(e, MsgException):
911 # error_text = "{} has been '{}' but other modules cannot be informed because an error on bus".format(
912 # engine_topic[:-1], method, error_text)
914 "code": http_code_name
,
915 "status": http_code_value
,
916 "detail": error_text
,
918 return self
._format
_out
(problem_details
, session
)
919 # raise cherrypy.HTTPError(e.http_code.value, str(e))
922 def _start_service():
924 Callback function called when cherrypy.engine starts
925 Override configuration with env variables
926 Set database, storage, message configuration
927 Init database with admin/admin user password
930 global subscription_thread
931 cherrypy
.log
.error("Starting osm_nbi")
932 # update general cherrypy configuration
935 engine_config
= cherrypy
.tree
.apps
['/osm'].config
936 for k
, v
in environ
.items():
937 if not k
.startswith("OSMNBI_"):
939 k1
, _
, k2
= k
[7:].lower().partition("_")
943 # update static configuration
944 if k
== 'OSMNBI_STATIC_DIR':
945 engine_config
["/static"]['tools.staticdir.dir'] = v
946 engine_config
["/static"]['tools.staticdir.on'] = True
947 elif k
== 'OSMNBI_SOCKET_PORT' or k
== 'OSMNBI_SERVER_PORT':
948 update_dict
['server.socket_port'] = int(v
)
949 elif k
== 'OSMNBI_SOCKET_HOST' or k
== 'OSMNBI_SERVER_HOST':
950 update_dict
['server.socket_host'] = v
951 elif k1
in ("server", "test", "auth", "log"):
952 update_dict
[k1
+ '.' + k2
] = v
953 elif k1
in ("message", "database", "storage", "authentication"):
954 # k2 = k2.replace('_', '.')
955 if k2
in ("port", "db_port"):
956 engine_config
[k1
][k2
] = int(v
)
958 engine_config
[k1
][k2
] = v
960 except ValueError as e
:
961 cherrypy
.log
.error("Ignoring environ '{}': " + str(e
))
962 except Exception as e
:
963 cherrypy
.log
.warn("skipping environ '{}' on exception '{}'".format(k
, e
))
966 cherrypy
.config
.update(update_dict
)
967 engine_config
["global"].update(update_dict
)
970 log_format_simple
= "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(message)s"
971 log_formatter_simple
= logging
.Formatter(log_format_simple
, datefmt
='%Y-%m-%dT%H:%M:%S')
972 logger_server
= logging
.getLogger("cherrypy.error")
973 logger_access
= logging
.getLogger("cherrypy.access")
974 logger_cherry
= logging
.getLogger("cherrypy")
975 logger_nbi
= logging
.getLogger("nbi")
977 if "log.file" in engine_config
["global"]:
978 file_handler
= logging
.handlers
.RotatingFileHandler(engine_config
["global"]["log.file"],
979 maxBytes
=100e6
, backupCount
=9, delay
=0)
980 file_handler
.setFormatter(log_formatter_simple
)
981 logger_cherry
.addHandler(file_handler
)
982 logger_nbi
.addHandler(file_handler
)
983 # log always to standard output
984 for format_
, logger
in {"nbi.server %(filename)s:%(lineno)s": logger_server
,
985 "nbi.access %(filename)s:%(lineno)s": logger_access
,
986 "%(name)s %(filename)s:%(lineno)s": logger_nbi
988 log_format_cherry
= "%(asctime)s %(levelname)s {} %(message)s".format(format_
)
989 log_formatter_cherry
= logging
.Formatter(log_format_cherry
, datefmt
='%Y-%m-%dT%H:%M:%S')
990 str_handler
= logging
.StreamHandler()
991 str_handler
.setFormatter(log_formatter_cherry
)
992 logger
.addHandler(str_handler
)
994 if engine_config
["global"].get("log.level"):
995 logger_cherry
.setLevel(engine_config
["global"]["log.level"])
996 logger_nbi
.setLevel(engine_config
["global"]["log.level"])
998 # logging other modules
999 for k1
, logname
in {"message": "nbi.msg", "database": "nbi.db", "storage": "nbi.fs"}.items():
1000 engine_config
[k1
]["logger_name"] = logname
1001 logger_module
= logging
.getLogger(logname
)
1002 if "logfile" in engine_config
[k1
]:
1003 file_handler
= logging
.handlers
.RotatingFileHandler(engine_config
[k1
]["logfile"],
1004 maxBytes
=100e6
, backupCount
=9, delay
=0)
1005 file_handler
.setFormatter(log_formatter_simple
)
1006 logger_module
.addHandler(file_handler
)
1007 if "loglevel" in engine_config
[k1
]:
1008 logger_module
.setLevel(engine_config
[k1
]["loglevel"])
1009 # TODO add more entries, e.g.: storage
1010 cherrypy
.tree
.apps
['/osm'].root
.engine
.start(engine_config
)
1011 cherrypy
.tree
.apps
['/osm'].root
.authenticator
.start(engine_config
)
1012 cherrypy
.tree
.apps
['/osm'].root
.engine
.init_db(target_version
=database_version
)
1013 cherrypy
.tree
.apps
['/osm'].root
.authenticator
.init_db(target_version
=auth_database_version
)
1015 # start subscriptions thread:
1016 subscription_thread
= SubscriptionThread(config
=engine_config
, engine
=nbi_server
.engine
)
1017 subscription_thread
.start()
1018 # Do not capture except SubscriptionException
1020 # load and print version. Ignore possible errors, e.g. file not found
1022 with
open("{}/version".format(engine_config
["/static"]['tools.staticdir.dir'])) as version_file
:
1023 version_data
= version_file
.read()
1024 cherrypy
.log
.error("Starting OSM NBI Version: {}".format(version_data
.replace("\n", " ")))
1029 def _stop_service():
1031 Callback function called when cherrypy.engine stops
1032 TODO: Ending database connections.
1034 global subscription_thread
1035 subscription_thread
.terminate()
1036 subscription_thread
= None
1037 cherrypy
.tree
.apps
['/osm'].root
.engine
.stop()
1038 cherrypy
.log
.error("Stopping osm_nbi")
1041 def nbi(config_file
):
1045 # #'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
1046 # 'tools.sessions.on': True,
1047 # 'tools.response_headers.on': True,
1048 # # 'tools.response_headers.headers': [('Content-Type', 'text/plain')],
1051 # cherrypy.Server.ssl_module = 'builtin'
1052 # cherrypy.Server.ssl_certificate = "http/cert.pem"
1053 # cherrypy.Server.ssl_private_key = "http/privkey.pem"
1054 # cherrypy.Server.thread_pool = 10
1055 # cherrypy.config.update({'Server.socket_port': config["port"], 'Server.socket_host': config["host"]})
1057 # cherrypy.config.update({'tools.auth_basic.on': True,
1058 # 'tools.auth_basic.realm': 'localhost',
1059 # 'tools.auth_basic.checkpassword': validate_password})
1060 nbi_server
= Server()
1061 cherrypy
.engine
.subscribe('start', _start_service
)
1062 cherrypy
.engine
.subscribe('stop', _stop_service
)
1063 cherrypy
.quickstart(nbi_server
, '/osm', config_file
)
1067 print("""Usage: {} [options]
1068 -c|--config [configuration_file]: loads the configuration file (default: ./nbi.cfg)
1069 -h|--help: shows this help
1070 """.format(sys
.argv
[0]))
1071 # --log-socket-host HOST: send logs to this host")
1072 # --log-socket-port PORT: send logs using this port (default: 9022)")
1075 if __name__
== '__main__':
1077 # load parameters and configuration
1078 opts
, args
= getopt
.getopt(sys
.argv
[1:], "hvc:", ["config=", "help"])
1079 # TODO add "log-socket-host=", "log-socket-port=", "log-file="
1082 if o
in ("-h", "--help"):
1085 elif o
in ("-c", "--config"):
1087 # elif o == "--log-socket-port":
1088 # log_socket_port = a
1089 # elif o == "--log-socket-host":
1090 # log_socket_host = a
1091 # elif o == "--log-file":
1094 assert False, "Unhandled option"
1096 if not path
.isfile(config_file
):
1097 print("configuration file '{}' that not exist".format(config_file
), file=sys
.stderr
)
1100 for config_file
in (__file__
[:__file__
.rfind(".")] + ".cfg", "./nbi.cfg", "/etc/osm/nbi.cfg"):
1101 if path
.isfile(config_file
):
1104 print("No configuration file 'nbi.cfg' found neither at local folder nor at /etc/osm/", file=sys
.stderr
)
1107 except getopt
.GetoptError
as e
:
1108 print(str(e
), file=sys
.stderr
)