f5d229338dcf89c8a169bb209ba0a7d49585b2f1
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 osm_nbi
.html_out
as html
23 import logging
.handlers
27 from osm_nbi
.authconn
import AuthException
, AuthconnException
28 from osm_nbi
.auth
import Authenticator
29 from osm_nbi
.engine
import Engine
, EngineException
30 from osm_nbi
.subscriptions
import SubscriptionThread
31 from osm_nbi
.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
38 from osm_nbi
import version
as _nbi_version
, version_date
as nbi_version_date
40 __author__
= "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
42 __version__
= "0.1.3" # file version, not NBI version
43 version_date
= "Aug 2019"
45 database_version
= '1.2'
46 auth_database_version
= '1.0'
47 nbi_server
= None # instance of Server class
48 subscription_thread
= None # instance of SubscriptionThread class
49 nbi_version
= _nbi_version
# by default this is fixed in the code
53 North Bound Interface (O: OSM specific; 5,X: SOL005 not implemented yet; O5: SOL005 implemented)
54 URL: /osm GET POST PUT DELETE PATCH
56 /ns_descriptors_content O O
62 /artifacts[/<artifactPath>] O
70 /vnf_packages_content O O
74 /package_content O5 O5
77 /artifacts[/<artifactPath>] O5
82 /ns_instances_content O O
94 /vnf_instances (also vnfrs for compatibility) O
110 /vim_accounts (also vims for compatibility) O O
122 /netslice_templates_content O O
124 /netslice_templates O O
128 /artifacts[/<artifactPath>] O
130 /<subscriptionId> X X
133 /netslice_instances_content O O
134 /<SliceInstanceId> O O
135 /netslice_instances O O
136 /<SliceInstanceId> O O
141 /<nsiLcmOpOccId> O O O
143 /<subscriptionId> X X
146 Follows SOL005 section 4.3.2 It contains extra METHOD to override http method, FORCE to force.
147 simpleFilterExpr := <attrName>["."<attrName>]*["."<op>]"="<value>[","<value>]*
148 filterExpr := <simpleFilterExpr>["&"<simpleFilterExpr>]*
149 op := "eq" | "neq" (or "ne") | "gt" | "lt" | "gte" | "lte" | "cont" | "ncont"
151 For filtering inside array, it must select the element of the array, or add ANYINDEX to apply the filtering over any
152 item of the array, that is, pass if any item of the array pass the filter.
153 It allows both ne and neq for not equal
154 TODO: 4.3.3 Attribute selectors
155 all_fields, fields=x,y,.., exclude_default, exclude_fields=x,y,...
156 (none) … same as “exclude_default”
157 all_fields … all attributes.
158 fields=<list> … all attributes except all complex attributes with minimum cardinality of zero that are not
159 conditionally mandatory, and that are not provided in <list>.
160 exclude_fields=<list> … all attributes except those complex attributes with a minimum cardinality of zero that
161 are not conditionally mandatory, and that are provided in <list>.
162 exclude_default … all attributes except those complex attributes with a minimum cardinality of zero that are not
163 conditionally mandatory, and that are part of the "default exclude set" defined in the present specification for
164 the particular resource
165 exclude_default and include=<list> … all attributes except those complex attributes with a minimum cardinality
166 of zero that are not conditionally mandatory and that are part of the "default exclude set" defined in the
167 present specification for the particular resource, but that are not part of <list>
168 Additionally it admits some administrator values:
169 FORCE: To force operations skipping dependency checkings
170 ADMIN: To act as an administrator or a different project
171 PUBLIC: To get public descriptors or set a descriptor as public
172 SET_PROJECT: To make a descriptor available for other project
174 Header field name Reference Example Descriptions
175 Accept IETF RFC 7231 [19] application/json Content-Types that are acceptable for the response.
176 This header field shall be present if the response is expected to have a non-empty message body.
177 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the request.
178 This header field shall be present if the request has a non-empty message body.
179 Authorization IETF RFC 7235 [22] Bearer mF_9.B5f-4.1JqM The authorization token for the request.
180 Details are specified in clause 4.5.3.
181 Range IETF RFC 7233 [21] 1000-2000 Requested range of bytes from a file
182 Header field name Reference Example Descriptions
183 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the response.
184 This header field shall be present if the response has a non-empty message body.
185 Location IETF RFC 7231 [19] http://www.example.com/vnflcm/v1/vnf_instances/123 Used in redirection, or when a
186 new resource has been created.
187 This header field shall be present if the response status code is 201 or 3xx.
188 In the present document this header field is also used if the response status code is 202 and a new resource was
190 WWW-Authenticate IETF RFC 7235 [22] Bearer realm="example" Challenge if the corresponding HTTP request has not
191 provided authorization, or error details if the corresponding HTTP request has provided an invalid authorization
193 Accept-Ranges IETF RFC 7233 [21] bytes Used by the Server to signal whether or not it supports ranges for
195 Content-Range IETF RFC 7233 [21] bytes 21010-47021/ 47022 Signals the byte range that is contained in the
196 response, and the total length of the file.
197 Retry-After IETF RFC 7231 [19] Fri, 31 Dec 1999 23:59:59 GMT
200 valid_query_string
= ("ADMIN", "SET_PROJECT", "FORCE", "PUBLIC")
201 # ^ Contains possible administrative query string words:
202 # ADMIN=True(by default)|Project|Project-list: See all elements, or elements of a project
203 # (not owned by my session project).
204 # PUBLIC=True(by default)|False: See/hide public elements. Set/Unset a topic to be public
205 # FORCE=True(by default)|False: Force edition/deletion operations
206 # SET_PROJECT=Project|Project-list: Add/Delete the topic to the projects portfolio
208 valid_url_methods
= {
209 # contains allowed URL and methods, and the role_permission name
212 "tokens": {"METHODS": ("GET", "POST", "DELETE"),
213 "ROLE_PERMISSION": "tokens:",
214 "<ID>": {"METHODS": ("GET", "DELETE"),
215 "ROLE_PERMISSION": "tokens:id:"
218 "users": {"METHODS": ("GET", "POST"),
219 "ROLE_PERMISSION": "users:",
220 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"),
221 "ROLE_PERMISSION": "users:id:"
224 "projects": {"METHODS": ("GET", "POST"),
225 "ROLE_PERMISSION": "projects:",
226 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"),
227 "ROLE_PERMISSION": "projects:id:"}
229 "roles": {"METHODS": ("GET", "POST"),
230 "ROLE_PERMISSION": "roles:",
231 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"),
232 "ROLE_PERMISSION": "roles:id:"
235 "vims": {"METHODS": ("GET", "POST"),
236 "ROLE_PERMISSION": "vims:",
237 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"),
238 "ROLE_PERMISSION": "vims:id:"
241 "vim_accounts": {"METHODS": ("GET", "POST"),
242 "ROLE_PERMISSION": "vim_accounts:",
243 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"),
244 "ROLE_PERMISSION": "vim_accounts:id:"
247 "wim_accounts": {"METHODS": ("GET", "POST"),
248 "ROLE_PERMISSION": "wim_accounts:",
249 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"),
250 "ROLE_PERMISSION": "wim_accounts:id:"
253 "sdns": {"METHODS": ("GET", "POST"),
254 "ROLE_PERMISSION": "sdn_controllers:",
255 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"),
256 "ROLE_PERMISSION": "sdn_controllers:id:"
259 "k8sclusters": {"METHODS": ("GET", "POST"),
260 "ROLE_PERMISSION": "k8sclusters:",
261 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"),
262 "ROLE_PERMISSION": "k8sclusters:id:"
265 "k8srepos": {"METHODS": ("GET", "POST"),
266 "ROLE_PERMISSION": "k8srepos:",
267 "<ID>": {"METHODS": ("GET", "DELETE"),
268 "ROLE_PERMISSION": "k8srepos:id:"
276 "pdu_descriptors": {"METHODS": ("GET", "POST"),
277 "ROLE_PERMISSION": "pduds:",
278 "<ID>": {"METHODS": ("GET", "POST", "DELETE", "PATCH", "PUT"),
279 "ROLE_PERMISSION": "pduds:id:"
286 "ns_descriptors_content": {"METHODS": ("GET", "POST"),
287 "ROLE_PERMISSION": "nsds:",
288 "<ID>": {"METHODS": ("GET", "PUT", "DELETE"),
289 "ROLE_PERMISSION": "nsds:id:"
292 "ns_descriptors": {"METHODS": ("GET", "POST"),
293 "ROLE_PERMISSION": "nsds:",
294 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"),
295 "ROLE_PERMISSION": "nsds:id:",
296 "nsd_content": {"METHODS": ("GET", "PUT"),
297 "ROLE_PERMISSION": "nsds:id:content:",
299 "nsd": {"METHODS": ("GET",), # descriptor inside package
300 "ROLE_PERMISSION": "nsds:id:content:"
302 "artifacts": {"*": {"METHODS": ("GET",),
303 "ROLE_PERMISSION": "nsds:id:nsd_artifact:"
308 "pnf_descriptors": {"TODO": ("GET", "POST"),
309 "<ID>": {"TODO": ("GET", "DELETE", "PATCH"),
310 "pnfd_content": {"TODO": ("GET", "PUT")}
313 "subscriptions": {"TODO": ("GET", "POST"),
314 "<ID>": {"TODO": ("GET", "DELETE")}
320 "vnf_packages_content": {"METHODS": ("GET", "POST"),
321 "ROLE_PERMISSION": "vnfds:",
322 "<ID>": {"METHODS": ("GET", "PUT", "DELETE"),
323 "ROLE_PERMISSION": "vnfds:id:"}
325 "vnf_packages": {"METHODS": ("GET", "POST"),
326 "ROLE_PERMISSION": "vnfds:",
327 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"), # GET: vnfPkgInfo
328 "ROLE_PERMISSION": "vnfds:id:",
329 "package_content": {"METHODS": ("GET", "PUT"), # package
330 "ROLE_PERMISSION": "vnfds:id:",
331 "upload_from_uri": {"METHODS": (),
333 "ROLE_PERMISSION": "vnfds:id:upload:"
336 "vnfd": {"METHODS": ("GET", ), # descriptor inside package
337 "ROLE_PERMISSION": "vnfds:id:content:"
339 "artifacts": {"*": {"METHODS": ("GET", ),
340 "ROLE_PERMISSION": "vnfds:id:vnfd_artifact:"
345 "subscriptions": {"TODO": ("GET", "POST"),
346 "<ID>": {"TODO": ("GET", "DELETE")}
352 "ns_instances_content": {"METHODS": ("GET", "POST"),
353 "ROLE_PERMISSION": "ns_instances:",
354 "<ID>": {"METHODS": ("GET", "DELETE"),
355 "ROLE_PERMISSION": "ns_instances:id:"
358 "ns_instances": {"METHODS": ("GET", "POST"),
359 "ROLE_PERMISSION": "ns_instances:",
360 "<ID>": {"METHODS": ("GET", "DELETE"),
361 "ROLE_PERMISSION": "ns_instances:id:",
362 "scale": {"METHODS": ("POST",),
363 "ROLE_PERMISSION": "ns_instances:id:scale:"
365 "terminate": {"METHODS": ("POST",),
366 "ROLE_PERMISSION": "ns_instances:id:terminate:"
368 "instantiate": {"METHODS": ("POST",),
369 "ROLE_PERMISSION": "ns_instances:id:instantiate:"
371 "action": {"METHODS": ("POST",),
372 "ROLE_PERMISSION": "ns_instances:id:action:"
376 "ns_lcm_op_occs": {"METHODS": ("GET",),
377 "ROLE_PERMISSION": "ns_instances:opps:",
378 "<ID>": {"METHODS": ("GET",),
379 "ROLE_PERMISSION": "ns_instances:opps:id:"
382 "vnfrs": {"METHODS": ("GET",),
383 "ROLE_PERMISSION": "vnf_instances:",
384 "<ID>": {"METHODS": ("GET",),
385 "ROLE_PERMISSION": "vnf_instances:id:"
388 "vnf_instances": {"METHODS": ("GET",),
389 "ROLE_PERMISSION": "vnf_instances:",
390 "<ID>": {"METHODS": ("GET",),
391 "ROLE_PERMISSION": "vnf_instances:id:"
398 "netslice_templates_content": {"METHODS": ("GET", "POST"),
399 "ROLE_PERMISSION": "slice_templates:",
400 "<ID>": {"METHODS": ("GET", "PUT", "DELETE"),
401 "ROLE_PERMISSION": "slice_templates:id:", }
403 "netslice_templates": {"METHODS": ("GET", "POST"),
404 "ROLE_PERMISSION": "slice_templates:",
405 "<ID>": {"METHODS": ("GET", "DELETE"),
407 "ROLE_PERMISSION": "slice_templates:id:",
408 "nst_content": {"METHODS": ("GET", "PUT"),
409 "ROLE_PERMISSION": "slice_templates:id:content:"
411 "nst": {"METHODS": ("GET",), # descriptor inside package
412 "ROLE_PERMISSION": "slice_templates:id:content:"
414 "artifacts": {"*": {"METHODS": ("GET",),
415 "ROLE_PERMISSION": "slice_templates:id:content:"
420 "subscriptions": {"TODO": ("GET", "POST"),
421 "<ID>": {"TODO": ("GET", "DELETE")}
427 "netslice_instances_content": {"METHODS": ("GET", "POST"),
428 "ROLE_PERMISSION": "slice_instances:",
429 "<ID>": {"METHODS": ("GET", "DELETE"),
430 "ROLE_PERMISSION": "slice_instances:id:"
433 "netslice_instances": {"METHODS": ("GET", "POST"),
434 "ROLE_PERMISSION": "slice_instances:",
435 "<ID>": {"METHODS": ("GET", "DELETE"),
436 "ROLE_PERMISSION": "slice_instances:id:",
437 "terminate": {"METHODS": ("POST",),
438 "ROLE_PERMISSION": "slice_instances:id:terminate:"
440 "instantiate": {"METHODS": ("POST",),
441 "ROLE_PERMISSION": "slice_instances:id:instantiate:"
443 "action": {"METHODS": ("POST",),
444 "ROLE_PERMISSION": "slice_instances:id:action:"
448 "nsi_lcm_op_occs": {"METHODS": ("GET",),
449 "ROLE_PERMISSION": "slice_instances:opps:",
450 "<ID>": {"METHODS": ("GET",),
451 "ROLE_PERMISSION": "slice_instances:opps:id:",
461 "<ID>": {"METHODS": ("GET",),
462 "ROLE_PERMISSION": "reports:id:",
472 class NbiException(Exception):
474 def __init__(self
, message
, http_code
=HTTPStatus
.METHOD_NOT_ALLOWED
):
475 Exception.__init
__(self
, message
)
476 self
.http_code
= http_code
479 class Server(object):
481 # to decode bytes to str
482 reader
= getreader("utf-8")
486 self
.authenticator
= Authenticator(valid_url_methods
, valid_query_string
)
487 self
.engine
= Engine(self
.authenticator
)
489 def _format_in(self
, kwargs
):
492 if cherrypy
.request
.body
.length
:
493 error_text
= "Invalid input format "
495 if "Content-Type" in cherrypy
.request
.headers
:
496 if "application/json" in cherrypy
.request
.headers
["Content-Type"]:
497 error_text
= "Invalid json format "
498 indata
= json
.load(self
.reader(cherrypy
.request
.body
))
499 cherrypy
.request
.headers
.pop("Content-File-MD5", None)
500 elif "application/yaml" in cherrypy
.request
.headers
["Content-Type"]:
501 error_text
= "Invalid yaml format "
502 indata
= yaml
.load(cherrypy
.request
.body
, Loader
=yaml
.SafeLoader
)
503 cherrypy
.request
.headers
.pop("Content-File-MD5", None)
504 elif "application/binary" in cherrypy
.request
.headers
["Content-Type"] or \
505 "application/gzip" in cherrypy
.request
.headers
["Content-Type"] or \
506 "application/zip" in cherrypy
.request
.headers
["Content-Type"] or \
507 "text/plain" in cherrypy
.request
.headers
["Content-Type"]:
508 indata
= cherrypy
.request
.body
# .read()
509 elif "multipart/form-data" in cherrypy
.request
.headers
["Content-Type"]:
510 if "descriptor_file" in kwargs
:
511 filecontent
= kwargs
.pop("descriptor_file")
512 if not filecontent
.file:
513 raise NbiException("empty file or content", HTTPStatus
.BAD_REQUEST
)
514 indata
= filecontent
.file # .read()
515 if filecontent
.content_type
.value
:
516 cherrypy
.request
.headers
["Content-Type"] = filecontent
.content_type
.value
518 # raise cherrypy.HTTPError(HTTPStatus.Not_Acceptable,
519 # "Only 'Content-Type' of type 'application/json' or
520 # 'application/yaml' for input format are available")
521 error_text
= "Invalid yaml format "
522 indata
= yaml
.load(cherrypy
.request
.body
, Loader
=yaml
.SafeLoader
)
523 cherrypy
.request
.headers
.pop("Content-File-MD5", None)
525 error_text
= "Invalid yaml format "
526 indata
= yaml
.load(cherrypy
.request
.body
, Loader
=yaml
.SafeLoader
)
527 cherrypy
.request
.headers
.pop("Content-File-MD5", None)
532 if cherrypy
.request
.headers
.get("Query-String-Format") == "yaml":
535 for k
, v
in kwargs
.items():
536 if isinstance(v
, str):
541 kwargs
[k
] = yaml
.load(v
, Loader
=yaml
.SafeLoader
)
544 elif k
.endswith(".gt") or k
.endswith(".lt") or k
.endswith(".gte") or k
.endswith(".lte"):
552 elif v
.find(",") > 0:
553 kwargs
[k
] = v
.split(",")
554 elif isinstance(v
, (list, tuple)):
555 for index
in range(0, len(v
)):
560 v
[index
] = yaml
.load(v
[index
], Loader
=yaml
.SafeLoader
)
565 except (ValueError, yaml
.YAMLError
) as exc
:
566 raise NbiException(error_text
+ str(exc
), HTTPStatus
.BAD_REQUEST
)
567 except KeyError as exc
:
568 raise NbiException("Query string error: " + str(exc
), HTTPStatus
.BAD_REQUEST
)
569 except Exception as exc
:
570 raise NbiException(error_text
+ str(exc
), HTTPStatus
.BAD_REQUEST
)
573 def _format_out(data
, token_info
=None, _format
=None):
575 return string of dictionary data according to requested json, yaml, xml. By default json
576 :param data: response to be sent. Can be a dict, text or file
577 :param token_info: Contains among other username and project
578 :param _format: The format to be set as Content-Type if data is a file
581 accept
= cherrypy
.request
.headers
.get("Accept")
583 if accept
and "text/html" in accept
:
584 return html
.format(data
, cherrypy
.request
, cherrypy
.response
, token_info
)
585 # cherrypy.response.status = HTTPStatus.NO_CONTENT.value
587 elif hasattr(data
, "read"): # file object
589 cherrypy
.response
.headers
["Content-Type"] = _format
590 elif "b" in data
.mode
: # binariy asssumig zip
591 cherrypy
.response
.headers
["Content-Type"] = 'application/zip'
593 cherrypy
.response
.headers
["Content-Type"] = 'text/plain'
594 # TODO check that cherrypy close file. If not implement pending things to close per thread next
597 if "application/json" in accept
:
598 cherrypy
.response
.headers
["Content-Type"] = 'application/json; charset=utf-8'
599 a
= json
.dumps(data
, indent
=4) + "\n"
600 return a
.encode("utf8")
601 elif "text/html" in accept
:
602 return html
.format(data
, cherrypy
.request
, cherrypy
.response
, token_info
)
604 elif "application/yaml" in accept
or "*/*" in accept
or "text/plain" in accept
:
606 # if there is not any valid accept, raise an error. But if response is already an error, format in yaml
607 elif cherrypy
.response
.status
>= 400:
608 raise cherrypy
.HTTPError(HTTPStatus
.NOT_ACCEPTABLE
.value
,
609 "Only 'Accept' of type 'application/json' or 'application/yaml' "
610 "for output format are available")
611 cherrypy
.response
.headers
["Content-Type"] = 'application/yaml'
612 return yaml
.safe_dump(data
, explicit_start
=True, indent
=4, default_flow_style
=False, tags
=False,
613 encoding
='utf-8', allow_unicode
=True) # , canonical=True, default_style='"'
616 def index(self
, *args
, **kwargs
):
619 if cherrypy
.request
.method
== "GET":
620 token_info
= self
.authenticator
.authorize()
621 outdata
= token_info
# Home page
623 raise cherrypy
.HTTPError(HTTPStatus
.METHOD_NOT_ALLOWED
.value
,
624 "Method {} not allowed for tokens".format(cherrypy
.request
.method
))
626 return self
._format
_out
(outdata
, token_info
)
628 except (EngineException
, AuthException
) as e
:
629 # cherrypy.log("index Exception {}".format(e))
630 cherrypy
.response
.status
= e
.http_code
.value
631 return self
._format
_out
("Welcome to OSM!", token_info
)
634 def version(self
, *args
, **kwargs
):
635 # TODO consider to remove and provide version using the static version file
637 if cherrypy
.request
.method
!= "GET":
638 raise NbiException("Only method GET is allowed", HTTPStatus
.METHOD_NOT_ALLOWED
)
640 raise NbiException("Invalid URL or query string for version", HTTPStatus
.METHOD_NOT_ALLOWED
)
641 # TODO include version of other modules, pick up from some kafka admin message
642 osm_nbi_version
= {"version": nbi_version
, "date": nbi_version_date
}
643 return self
._format
_out
(osm_nbi_version
)
644 except NbiException
as e
:
645 cherrypy
.response
.status
= e
.http_code
.value
647 "code": e
.http_code
.name
,
648 "status": e
.http_code
.value
,
651 return self
._format
_out
(problem_details
, None)
654 def _format_login(token_info
):
656 Changes cherrypy.request.login to include username/project_name;session so that cherrypy access log will
658 :param token_info: Dictionary with token content
661 cherrypy
.request
.login
= token_info
.get("username", "-")
662 if token_info
.get("project_name"):
663 cherrypy
.request
.login
+= "/" + token_info
["project_name"]
664 if token_info
.get("id"):
665 cherrypy
.request
.login
+= ";session=" + token_info
["id"][0:12]
668 def token(self
, method
, token_id
=None, kwargs
=None):
670 # self.engine.load_dbase(cherrypy.request.app.config)
671 indata
= self
._format
_in
(kwargs
)
672 if not isinstance(indata
, dict):
673 raise NbiException("Expected application/yaml or application/json Content-Type", HTTPStatus
.BAD_REQUEST
)
676 token_info
= self
.authenticator
.authorize()
678 self
._format
_login
(token_info
)
680 outdata
= self
.authenticator
.get_token(token_info
, token_id
)
682 outdata
= self
.authenticator
.get_token_list(token_info
)
683 elif method
== "POST":
685 token_info
= self
.authenticator
.authorize()
689 indata
.update(kwargs
)
690 # This is needed to log the user when authentication fails
691 cherrypy
.request
.login
= "{}".format(indata
.get("username", "-"))
692 outdata
= token_info
= self
.authenticator
.new_token(token_info
, indata
, cherrypy
.request
.remote
)
693 cherrypy
.session
['Authorization'] = outdata
["_id"]
694 self
._set
_location
_header
("admin", "v1", "tokens", outdata
["_id"])
696 self
._format
_login
(token_info
)
698 # cherrypy.response.cookie["Authorization"] = outdata["id"]
699 # cherrypy.response.cookie["Authorization"]['expires'] = 3600
700 elif method
== "DELETE":
701 if not token_id
and "id" in kwargs
:
702 token_id
= kwargs
["id"]
704 token_info
= self
.authenticator
.authorize()
706 self
._format
_login
(token_info
)
707 token_id
= token_info
["_id"]
708 outdata
= self
.authenticator
.del_token(token_id
)
710 cherrypy
.session
['Authorization'] = "logout"
711 # cherrypy.response.cookie["Authorization"] = token_id
712 # cherrypy.response.cookie["Authorization"]['expires'] = 0
714 raise NbiException("Method {} not allowed for token".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
715 return self
._format
_out
(outdata
, token_info
)
718 def test(self
, *args
, **kwargs
):
719 if not cherrypy
.config
.get("server.enable_test") or (isinstance(cherrypy
.config
["server.enable_test"], str) and
720 cherrypy
.config
["server.enable_test"].lower() == "false"):
721 cherrypy
.response
.status
= HTTPStatus
.METHOD_NOT_ALLOWED
.value
722 return "test URL is disabled"
724 if args
and args
[0] == "help":
725 return "<html><pre>\ninit\nfile/<name> download file\ndb-clear/table\nfs-clear[/folder]\nlogin\nlogin2\n"\
726 "sleep/<time>\nmessage/topic\n</pre></html>"
728 elif args
and args
[0] == "init":
730 # self.engine.load_dbase(cherrypy.request.app.config)
731 self
.engine
.create_admin()
732 return "Done. User 'admin', password 'admin' created"
734 cherrypy
.response
.status
= HTTPStatus
.FORBIDDEN
.value
735 return self
._format
_out
("Database already initialized")
736 elif args
and args
[0] == "file":
737 return cherrypy
.lib
.static
.serve_file(cherrypy
.tree
.apps
['/osm'].config
["storage"]["path"] + "/" + args
[1],
738 "text/plain", "attachment")
739 elif args
and args
[0] == "file2":
740 f_path
= cherrypy
.tree
.apps
['/osm'].config
["storage"]["path"] + "/" + args
[1]
741 f
= open(f_path
, "r")
742 cherrypy
.response
.headers
["Content-type"] = "text/plain"
745 elif len(args
) == 2 and args
[0] == "db-clear":
746 deleted_info
= self
.engine
.db
.del_list(args
[1], kwargs
)
747 return "{} {} deleted\n".format(deleted_info
["deleted"], args
[1])
748 elif len(args
) and args
[0] == "fs-clear":
752 folders
= self
.engine
.fs
.dir_ls(".")
753 for folder
in folders
:
754 self
.engine
.fs
.file_delete(folder
)
755 return ",".join(folders
) + " folders deleted\n"
756 elif args
and args
[0] == "login":
757 if not cherrypy
.request
.headers
.get("Authorization"):
758 cherrypy
.response
.headers
["WWW-Authenticate"] = 'Basic realm="Access to OSM site", charset="UTF-8"'
759 cherrypy
.response
.status
= HTTPStatus
.UNAUTHORIZED
.value
760 elif args
and args
[0] == "login2":
761 if not cherrypy
.request
.headers
.get("Authorization"):
762 cherrypy
.response
.headers
["WWW-Authenticate"] = 'Bearer realm="Access to OSM site"'
763 cherrypy
.response
.status
= HTTPStatus
.UNAUTHORIZED
.value
764 elif args
and args
[0] == "sleep":
767 sleep_time
= int(args
[1])
769 cherrypy
.response
.status
= HTTPStatus
.FORBIDDEN
.value
770 return self
._format
_out
("Database already initialized")
771 thread_info
= cherrypy
.thread_data
773 time
.sleep(sleep_time
)
775 elif len(args
) >= 2 and args
[0] == "message":
777 return_text
= "<html><pre>{} ->\n".format(main_topic
)
779 if cherrypy
.request
.method
== 'POST':
780 to_send
= yaml
.load(cherrypy
.request
.body
, Loader
=yaml
.SafeLoader
)
781 for k
, v
in to_send
.items():
782 self
.engine
.msg
.write(main_topic
, k
, v
)
783 return_text
+= " {}: {}\n".format(k
, v
)
784 elif cherrypy
.request
.method
== 'GET':
785 for k
, v
in kwargs
.items():
786 self
.engine
.msg
.write(main_topic
, k
, yaml
.load(v
), Loader
=yaml
.SafeLoader
)
787 return_text
+= " {}: {}\n".format(k
, yaml
.load(v
), Loader
=yaml
.SafeLoader
)
788 except Exception as e
:
789 return_text
+= "Error: " + str(e
)
790 return_text
+= "</pre></html>\n"
794 "<html><pre>\nheaders:\n args: {}\n".format(args
) +
795 " kwargs: {}\n".format(kwargs
) +
796 " headers: {}\n".format(cherrypy
.request
.headers
) +
797 " path_info: {}\n".format(cherrypy
.request
.path_info
) +
798 " query_string: {}\n".format(cherrypy
.request
.query_string
) +
799 " session: {}\n".format(cherrypy
.session
) +
800 " cookie: {}\n".format(cherrypy
.request
.cookie
) +
801 " method: {}\n".format(cherrypy
.request
.method
) +
802 " session: {}\n".format(cherrypy
.session
.get('fieldname')) +
804 return_text
+= " length: {}\n".format(cherrypy
.request
.body
.length
)
805 if cherrypy
.request
.body
.length
:
806 return_text
+= " content: {}\n".format(
807 str(cherrypy
.request
.body
.read(int(cherrypy
.request
.headers
.get('Content-Length', 0)))))
809 return_text
+= "thread: {}\n".format(thread_info
)
810 return_text
+= "</pre></html>"
814 def _check_valid_url_method(method
, *args
):
816 raise NbiException("URL must contain at least 'main_topic/version/topic'", HTTPStatus
.METHOD_NOT_ALLOWED
)
818 reference
= valid_url_methods
822 if not isinstance(reference
, dict):
823 raise NbiException("URL contains unexpected extra items '{}'".format(arg
),
824 HTTPStatus
.METHOD_NOT_ALLOWED
)
827 reference
= reference
[arg
]
828 elif "<ID>" in reference
:
829 reference
= reference
["<ID>"]
830 elif "*" in reference
:
831 reference
= reference
["*"]
834 raise NbiException("Unexpected URL item {}".format(arg
), HTTPStatus
.METHOD_NOT_ALLOWED
)
835 if "TODO" in reference
and method
in reference
["TODO"]:
836 raise NbiException("Method {} not supported yet for this URL".format(method
), HTTPStatus
.NOT_IMPLEMENTED
)
837 elif "METHODS" in reference
and method
not in reference
["METHODS"]:
838 raise NbiException("Method {} not supported for this URL".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
839 return reference
["ROLE_PERMISSION"] + method
.lower()
842 def _set_location_header(main_topic
, version
, topic
, id):
844 Insert response header Location with the URL of created item base on URL params
851 # Use cherrypy.request.base for absoluted path and make use of request.header HOST just in case behind aNAT
852 cherrypy
.response
.headers
["Location"] = "/osm/{}/{}/{}/{}".format(main_topic
, version
, topic
, id)
856 def _extract_query_string_operations(kwargs
, method
):
862 query_string_operations
= []
864 for qs
in ("FORCE", "PUBLIC", "ADMIN", "SET_PROJECT"):
865 if qs
in kwargs
and kwargs
[qs
].lower() != "false":
866 query_string_operations
.append(qs
.lower() + ":" + method
.lower())
867 return query_string_operations
870 def _manage_admin_query(token_info
, kwargs
, method
, _id
):
872 Processes the administrator query inputs (if any) of FORCE, ADMIN, PUBLIC, SET_PROJECT
873 Check that users has rights to use them and returs the admin_query
874 :param token_info: token_info rights obtained by token
875 :param kwargs: query string input.
876 :param method: http method: GET, POSST, PUT, ...
878 :return: admin_query dictionary with keys:
879 public: True, False or None
881 project_id: tuple with projects used for accessing an element
882 set_project: tuple with projects that a created element will belong to
883 method: show, list, delete, write
885 admin_query
= {"force": False, "project_id": (token_info
["project_id"], ), "username": token_info
["username"],
886 "admin": token_info
["admin"], "public": None,
887 "allow_show_user_project_role": token_info
["allow_show_user_project_role"]}
890 if "FORCE" in kwargs
:
891 if kwargs
["FORCE"].lower() != "false": # if None or True set force to True
892 admin_query
["force"] = True
895 if "PUBLIC" in kwargs
:
896 if kwargs
["PUBLIC"].lower() != "false": # if None or True set public to True
897 admin_query
["public"] = True
899 admin_query
["public"] = False
902 if "ADMIN" in kwargs
:
903 behave_as
= kwargs
.pop("ADMIN")
904 if behave_as
.lower() != "false":
905 if not token_info
["admin"]:
906 raise NbiException("Only admin projects can use 'ADMIN' query string", HTTPStatus
.UNAUTHORIZED
)
907 if not behave_as
or behave_as
.lower() == "true": # convert True, None to empty list
908 admin_query
["project_id"] = ()
909 elif isinstance(behave_as
, (list, tuple)):
910 admin_query
["project_id"] = behave_as
911 else: # isinstance(behave_as, str)
912 admin_query
["project_id"] = (behave_as
, )
913 if "SET_PROJECT" in kwargs
:
914 set_project
= kwargs
.pop("SET_PROJECT")
916 admin_query
["set_project"] = list(admin_query
["project_id"])
918 if isinstance(set_project
, str):
919 set_project
= (set_project
, )
920 if admin_query
["project_id"]:
921 for p
in set_project
:
922 if p
not in admin_query
["project_id"]:
923 raise NbiException("Unauthorized for 'SET_PROJECT={p}'. Try with 'ADMIN=True' or "
924 "'ADMIN='{p}'".format(p
=p
), HTTPStatus
.UNAUTHORIZED
)
925 admin_query
["set_project"] = set_project
928 # if "PROJECT_READ" in kwargs:
929 # admin_query["project"] = kwargs.pop("project")
930 # if admin_query["project"] == token_info["project_id"]:
933 admin_query
["method"] = "show"
935 admin_query
["method"] = "list"
936 elif method
== "DELETE":
937 admin_query
["method"] = "delete"
939 admin_query
["method"] = "write"
943 def default(self
, main_topic
=None, version
=None, topic
=None, _id
=None, item
=None, *args
, **kwargs
):
950 engine_session
= None
952 if not main_topic
or not version
or not topic
:
953 raise NbiException("URL must contain at least 'main_topic/version/topic'",
954 HTTPStatus
.METHOD_NOT_ALLOWED
)
955 if main_topic
not in ("admin", "vnfpkgm", "nsd", "nslcm", "pdu", "nst", "nsilcm", "nspm"):
956 raise NbiException("URL main_topic '{}' not supported".format(main_topic
),
957 HTTPStatus
.METHOD_NOT_ALLOWED
)
959 raise NbiException("URL version '{}' not supported".format(version
), HTTPStatus
.METHOD_NOT_ALLOWED
)
961 if kwargs
and "METHOD" in kwargs
and kwargs
["METHOD"] in ("PUT", "POST", "DELETE", "GET", "PATCH"):
962 method
= kwargs
.pop("METHOD")
964 method
= cherrypy
.request
.method
966 role_permission
= self
._check
_valid
_url
_method
(method
, main_topic
, version
, topic
, _id
, item
, *args
)
967 query_string_operations
= self
._extract
_query
_string
_operations
(kwargs
, method
)
968 if main_topic
== "admin" and topic
== "tokens":
969 return self
.token(method
, _id
, kwargs
)
970 token_info
= self
.authenticator
.authorize(role_permission
, query_string_operations
, _id
)
971 engine_session
= self
._manage
_admin
_query
(token_info
, kwargs
, method
, _id
)
972 indata
= self
._format
_in
(kwargs
)
974 if topic
== "subscriptions":
975 engine_topic
= main_topic
+ "_" + topic
976 if item
and topic
!= "pm_jobs":
979 if main_topic
== "nsd":
980 engine_topic
= "nsds"
981 elif main_topic
== "vnfpkgm":
982 engine_topic
= "vnfds"
983 elif main_topic
== "nslcm":
984 engine_topic
= "nsrs"
985 if topic
== "ns_lcm_op_occs":
986 engine_topic
= "nslcmops"
987 if topic
== "vnfrs" or topic
== "vnf_instances":
988 engine_topic
= "vnfrs"
989 elif main_topic
== "nst":
990 engine_topic
= "nsts"
991 elif main_topic
== "nsilcm":
992 engine_topic
= "nsis"
993 if topic
== "nsi_lcm_op_occs":
994 engine_topic
= "nsilcmops"
995 elif main_topic
== "pdu":
996 engine_topic
= "pdus"
997 if engine_topic
== "vims": # TODO this is for backward compatibility, it will be removed in the future
998 engine_topic
= "vim_accounts"
1001 if item
in ("nsd_content", "package_content", "artifacts", "vnfd", "nsd", "nst", "nst_content"):
1002 if item
in ("vnfd", "nsd", "nst"):
1003 path
= "$DESCRIPTOR"
1006 elif item
== "artifacts":
1010 file, _format
= self
.engine
.get_file(engine_session
, engine_topic
, _id
, path
,
1011 cherrypy
.request
.headers
.get("Accept"))
1014 outdata
= self
.engine
.get_item_list(engine_session
, engine_topic
, kwargs
)
1016 if item
== "reports":
1017 # TODO check that project_id (_id in this context) has permissions
1019 outdata
= self
.engine
.get_item(engine_session
, engine_topic
, _id
)
1020 elif method
== "POST":
1021 cherrypy
.response
.status
= HTTPStatus
.CREATED
.value
1022 if topic
in ("ns_descriptors_content", "vnf_packages_content", "netslice_templates_content"):
1023 _id
= cherrypy
.request
.headers
.get("Transaction-Id")
1025 _id
, _
= self
.engine
.new_item(rollback
, engine_session
, engine_topic
, {}, None,
1026 cherrypy
.request
.headers
)
1027 completed
= self
.engine
.upload_content(engine_session
, engine_topic
, _id
, indata
, kwargs
,
1028 cherrypy
.request
.headers
)
1030 self
._set
_location
_header
(main_topic
, version
, topic
, _id
)
1032 cherrypy
.response
.headers
["Transaction-Id"] = _id
1033 outdata
= {"id": _id
}
1034 elif topic
== "ns_instances_content":
1036 _id
, _
= self
.engine
.new_item(rollback
, engine_session
, engine_topic
, indata
, kwargs
)
1038 indata
["lcmOperationType"] = "instantiate"
1039 indata
["nsInstanceId"] = _id
1040 nslcmop_id
, _
= self
.engine
.new_item(rollback
, engine_session
, "nslcmops", indata
, None)
1041 self
._set
_location
_header
(main_topic
, version
, topic
, _id
)
1042 outdata
= {"id": _id
, "nslcmop_id": nslcmop_id
}
1043 elif topic
== "ns_instances" and item
:
1044 indata
["lcmOperationType"] = item
1045 indata
["nsInstanceId"] = _id
1046 _id
, _
= self
.engine
.new_item(rollback
, engine_session
, "nslcmops", indata
, kwargs
)
1047 self
._set
_location
_header
(main_topic
, version
, "ns_lcm_op_occs", _id
)
1048 outdata
= {"id": _id
}
1049 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
1050 elif topic
== "netslice_instances_content":
1051 # creates NetSlice_Instance_record (NSIR)
1052 _id
, _
= self
.engine
.new_item(rollback
, engine_session
, engine_topic
, indata
, kwargs
)
1053 self
._set
_location
_header
(main_topic
, version
, topic
, _id
)
1054 indata
["lcmOperationType"] = "instantiate"
1055 indata
["netsliceInstanceId"] = _id
1056 nsilcmop_id
, _
= self
.engine
.new_item(rollback
, engine_session
, "nsilcmops", indata
, kwargs
)
1057 outdata
= {"id": _id
, "nsilcmop_id": nsilcmop_id
}
1059 elif topic
== "netslice_instances" and item
:
1060 indata
["lcmOperationType"] = item
1061 indata
["netsliceInstanceId"] = _id
1062 _id
, _
= self
.engine
.new_item(rollback
, engine_session
, "nsilcmops", indata
, kwargs
)
1063 self
._set
_location
_header
(main_topic
, version
, "nsi_lcm_op_occs", _id
)
1064 outdata
= {"id": _id
}
1065 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
1067 _id
, op_id
= self
.engine
.new_item(rollback
, engine_session
, engine_topic
, indata
, kwargs
,
1068 cherrypy
.request
.headers
)
1069 self
._set
_location
_header
(main_topic
, version
, topic
, _id
)
1070 outdata
= {"id": _id
}
1072 outdata
["op_id"] = op_id
1073 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
1074 # TODO form NsdInfo when topic in ("ns_descriptors", "vnf_packages")
1076 elif method
== "DELETE":
1078 outdata
= self
.engine
.del_item_list(engine_session
, engine_topic
, kwargs
)
1079 cherrypy
.response
.status
= HTTPStatus
.OK
.value
1080 else: # len(args) > 1
1081 delete_in_process
= False
1082 if topic
== "ns_instances_content" and not engine_session
["force"]:
1084 "lcmOperationType": "terminate",
1085 "nsInstanceId": _id
,
1088 opp_id
, _
= self
.engine
.new_item(rollback
, engine_session
, "nslcmops", nslcmop_desc
, None)
1090 delete_in_process
= True
1091 outdata
= {"_id": opp_id
}
1092 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
1093 elif topic
== "netslice_instances_content" and not engine_session
["force"]:
1095 "lcmOperationType": "terminate",
1096 "netsliceInstanceId": _id
,
1099 opp_id
, _
= self
.engine
.new_item(rollback
, engine_session
, "nsilcmops", nsilcmop_desc
, None)
1101 delete_in_process
= True
1102 outdata
= {"_id": opp_id
}
1103 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
1104 if not delete_in_process
:
1105 self
.engine
.del_item(engine_session
, engine_topic
, _id
)
1106 cherrypy
.response
.status
= HTTPStatus
.NO_CONTENT
.value
1107 if engine_topic
in ("vim_accounts", "wim_accounts", "sdns", "k8sclusters", "k8srepos"):
1108 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
1110 elif method
in ("PUT", "PATCH"):
1112 if not indata
and not kwargs
and not engine_session
.get("set_project"):
1113 raise NbiException("Nothing to update. Provide payload and/or query string",
1114 HTTPStatus
.BAD_REQUEST
)
1115 if item
in ("nsd_content", "package_content", "nst_content") and method
== "PUT":
1116 completed
= self
.engine
.upload_content(engine_session
, engine_topic
, _id
, indata
, kwargs
,
1117 cherrypy
.request
.headers
)
1119 cherrypy
.response
.headers
["Transaction-Id"] = id
1121 op_id
= self
.engine
.edit_item(engine_session
, engine_topic
, _id
, indata
, kwargs
)
1124 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
1125 outdata
= {"op_id": op_id
}
1127 cherrypy
.response
.status
= HTTPStatus
.NO_CONTENT
.value
1130 raise NbiException("Method {} not allowed".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
1132 # if Role information changes, it is needed to reload the information of roles
1133 if topic
== "roles" and method
!= "GET":
1134 self
.authenticator
.load_operation_to_allowed_roles()
1136 if topic
== "projects" and method
== "DELETE" \
1137 or topic
in ["users", "roles"] and method
in ["PUT", "PATCH", "DELETE"]:
1138 self
.authenticator
.remove_token_from_cache()
1140 return self
._format
_out
(outdata
, token_info
, _format
)
1141 except Exception as e
:
1142 if isinstance(e
, (NbiException
, EngineException
, DbException
, FsException
, MsgException
, AuthException
,
1143 ValidationError
, AuthconnException
)):
1144 http_code_value
= cherrypy
.response
.status
= e
.http_code
.value
1145 http_code_name
= e
.http_code
.name
1146 cherrypy
.log("Exception {}".format(e
))
1148 http_code_value
= cherrypy
.response
.status
= HTTPStatus
.BAD_REQUEST
.value
# INTERNAL_SERVER_ERROR
1149 cherrypy
.log("CRITICAL: Exception {}".format(e
), traceback
=True)
1150 http_code_name
= HTTPStatus
.BAD_REQUEST
.name
1151 if hasattr(outdata
, "close"): # is an open file
1155 for rollback_item
in rollback
:
1157 if rollback_item
.get("operation") == "set":
1158 self
.engine
.db
.set_one(rollback_item
["topic"], {"_id": rollback_item
["_id"]},
1159 rollback_item
["content"], fail_on_empty
=False)
1161 self
.engine
.db
.del_one(rollback_item
["topic"], {"_id": rollback_item
["_id"]},
1162 fail_on_empty
=False)
1163 except Exception as e2
:
1164 rollback_error_text
= "Rollback Exception {}: {}".format(rollback_item
, e2
)
1165 cherrypy
.log(rollback_error_text
)
1166 error_text
+= ". " + rollback_error_text
1167 # if isinstance(e, MsgException):
1168 # error_text = "{} has been '{}' but other modules cannot be informed because an error on bus".format(
1169 # engine_topic[:-1], method, error_text)
1171 "code": http_code_name
,
1172 "status": http_code_value
,
1173 "detail": error_text
,
1175 return self
._format
_out
(problem_details
, token_info
)
1176 # raise cherrypy.HTTPError(e.http_code.value, str(e))
1179 self
._format
_login
(token_info
)
1180 if method
in ("PUT", "PATCH", "POST") and isinstance(outdata
, dict):
1181 for logging_id
in ("id", "op_id", "nsilcmop_id", "nslcmop_id"):
1182 if outdata
.get(logging_id
):
1183 cherrypy
.request
.login
+= ";{}={}".format(logging_id
, outdata
[logging_id
][:36])
1188 Try to get version from package using pkg_resources (available with setuptools)
1192 from pkg_resources
import get_distribution
1193 nbi_version
= get_distribution("osm_nbi").version
1198 def _start_service():
1200 Callback function called when cherrypy.engine starts
1201 Override configuration with env variables
1202 Set database, storage, message configuration
1203 Init database with admin/admin user password
1206 global subscription_thread
1207 cherrypy
.log
.error("Starting osm_nbi")
1208 # update general cherrypy configuration
1211 engine_config
= cherrypy
.tree
.apps
['/osm'].config
1212 for k
, v
in environ
.items():
1213 if not k
.startswith("OSMNBI_"):
1215 k1
, _
, k2
= k
[7:].lower().partition("_")
1219 # update static configuration
1220 if k
== 'OSMNBI_STATIC_DIR':
1221 engine_config
["/static"]['tools.staticdir.dir'] = v
1222 engine_config
["/static"]['tools.staticdir.on'] = True
1223 elif k
== 'OSMNBI_SOCKET_PORT' or k
== 'OSMNBI_SERVER_PORT':
1224 update_dict
['server.socket_port'] = int(v
)
1225 elif k
== 'OSMNBI_SOCKET_HOST' or k
== 'OSMNBI_SERVER_HOST':
1226 update_dict
['server.socket_host'] = v
1227 elif k1
in ("server", "test", "auth", "log"):
1228 update_dict
[k1
+ '.' + k2
] = v
1229 elif k1
in ("message", "database", "storage", "authentication"):
1230 # k2 = k2.replace('_', '.')
1231 if k2
in ("port", "db_port"):
1232 engine_config
[k1
][k2
] = int(v
)
1234 engine_config
[k1
][k2
] = v
1236 except ValueError as e
:
1237 cherrypy
.log
.error("Ignoring environ '{}': " + str(e
))
1238 except Exception as e
:
1239 cherrypy
.log
.warn("skipping environ '{}' on exception '{}'".format(k
, e
))
1242 cherrypy
.config
.update(update_dict
)
1243 engine_config
["global"].update(update_dict
)
1246 log_format_simple
= "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(message)s"
1247 log_formatter_simple
= logging
.Formatter(log_format_simple
, datefmt
='%Y-%m-%dT%H:%M:%S')
1248 logger_server
= logging
.getLogger("cherrypy.error")
1249 logger_access
= logging
.getLogger("cherrypy.access")
1250 logger_cherry
= logging
.getLogger("cherrypy")
1251 logger_nbi
= logging
.getLogger("nbi")
1253 if "log.file" in engine_config
["global"]:
1254 file_handler
= logging
.handlers
.RotatingFileHandler(engine_config
["global"]["log.file"],
1255 maxBytes
=100e6
, backupCount
=9, delay
=0)
1256 file_handler
.setFormatter(log_formatter_simple
)
1257 logger_cherry
.addHandler(file_handler
)
1258 logger_nbi
.addHandler(file_handler
)
1259 # log always to standard output
1260 for format_
, logger
in {"nbi.server %(filename)s:%(lineno)s": logger_server
,
1261 "nbi.access %(filename)s:%(lineno)s": logger_access
,
1262 "%(name)s %(filename)s:%(lineno)s": logger_nbi
1264 log_format_cherry
= "%(asctime)s %(levelname)s {} %(message)s".format(format_
)
1265 log_formatter_cherry
= logging
.Formatter(log_format_cherry
, datefmt
='%Y-%m-%dT%H:%M:%S')
1266 str_handler
= logging
.StreamHandler()
1267 str_handler
.setFormatter(log_formatter_cherry
)
1268 logger
.addHandler(str_handler
)
1270 if engine_config
["global"].get("log.level"):
1271 logger_cherry
.setLevel(engine_config
["global"]["log.level"])
1272 logger_nbi
.setLevel(engine_config
["global"]["log.level"])
1274 # logging other modules
1275 for k1
, logname
in {"message": "nbi.msg", "database": "nbi.db", "storage": "nbi.fs"}.items():
1276 engine_config
[k1
]["logger_name"] = logname
1277 logger_module
= logging
.getLogger(logname
)
1278 if "logfile" in engine_config
[k1
]:
1279 file_handler
= logging
.handlers
.RotatingFileHandler(engine_config
[k1
]["logfile"],
1280 maxBytes
=100e6
, backupCount
=9, delay
=0)
1281 file_handler
.setFormatter(log_formatter_simple
)
1282 logger_module
.addHandler(file_handler
)
1283 if "loglevel" in engine_config
[k1
]:
1284 logger_module
.setLevel(engine_config
[k1
]["loglevel"])
1285 # TODO add more entries, e.g.: storage
1286 cherrypy
.tree
.apps
['/osm'].root
.engine
.start(engine_config
)
1287 cherrypy
.tree
.apps
['/osm'].root
.authenticator
.start(engine_config
)
1288 cherrypy
.tree
.apps
['/osm'].root
.engine
.init_db(target_version
=database_version
)
1289 cherrypy
.tree
.apps
['/osm'].root
.authenticator
.init_db(target_version
=auth_database_version
)
1291 # start subscriptions thread:
1292 subscription_thread
= SubscriptionThread(config
=engine_config
, engine
=nbi_server
.engine
)
1293 subscription_thread
.start()
1294 # Do not capture except SubscriptionException
1296 # load and print version. Ignore possible errors, e.g. file not found
1299 backend
= engine_config
["authentication"]["backend"]
1300 cherrypy
.log
.error("Starting OSM NBI Version '{}' with '{}' authentication backend"
1301 .format(nbi_version
+ " " + nbi_version_date
, backend
))
1306 def _stop_service():
1308 Callback function called when cherrypy.engine stops
1309 TODO: Ending database connections.
1311 global subscription_thread
1312 if subscription_thread
:
1313 subscription_thread
.terminate()
1314 subscription_thread
= None
1315 cherrypy
.tree
.apps
['/osm'].root
.engine
.stop()
1316 cherrypy
.log
.error("Stopping osm_nbi")
1319 def nbi(config_file
):
1323 # #'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
1324 # 'tools.sessions.on': True,
1325 # 'tools.response_headers.on': True,
1326 # # 'tools.response_headers.headers': [('Content-Type', 'text/plain')],
1329 # cherrypy.Server.ssl_module = 'builtin'
1330 # cherrypy.Server.ssl_certificate = "http/cert.pem"
1331 # cherrypy.Server.ssl_private_key = "http/privkey.pem"
1332 # cherrypy.Server.thread_pool = 10
1333 # cherrypy.config.update({'Server.socket_port': config["port"], 'Server.socket_host': config["host"]})
1335 # cherrypy.config.update({'tools.auth_basic.on': True,
1336 # 'tools.auth_basic.realm': 'localhost',
1337 # 'tools.auth_basic.checkpassword': validate_password})
1338 nbi_server
= Server()
1339 cherrypy
.engine
.subscribe('start', _start_service
)
1340 cherrypy
.engine
.subscribe('stop', _stop_service
)
1341 cherrypy
.quickstart(nbi_server
, '/osm', config_file
)
1345 print("""Usage: {} [options]
1346 -c|--config [configuration_file]: loads the configuration file (default: ./nbi.cfg)
1347 -h|--help: shows this help
1348 """.format(sys
.argv
[0]))
1349 # --log-socket-host HOST: send logs to this host")
1350 # --log-socket-port PORT: send logs using this port (default: 9022)")
1353 if __name__
== '__main__':
1355 # load parameters and configuration
1356 opts
, args
= getopt
.getopt(sys
.argv
[1:], "hvc:", ["config=", "help"])
1357 # TODO add "log-socket-host=", "log-socket-port=", "log-file="
1360 if o
in ("-h", "--help"):
1363 elif o
in ("-c", "--config"):
1365 # elif o == "--log-socket-port":
1366 # log_socket_port = a
1367 # elif o == "--log-socket-host":
1368 # log_socket_host = a
1369 # elif o == "--log-file":
1372 assert False, "Unhandled option"
1374 if not path
.isfile(config_file
):
1375 print("configuration file '{}' that not exist".format(config_file
), file=sys
.stderr
)
1378 for config_file
in (__file__
[:__file__
.rfind(".")] + ".cfg", "./nbi.cfg", "/etc/osm/nbi.cfg"):
1379 if path
.isfile(config_file
):
1382 print("No configuration file 'nbi.cfg' found neither at local folder nor at /etc/osm/", file=sys
.stderr
)
1385 except getopt
.GetoptError
as e
:
1386 print(str(e
), file=sys
.stderr
)