2 # -*- coding: utf-8 -*-
5 # Copyright 2020 Telefonica Investigacion y Desarrollo, S.A.U.
7 # Licensed under the Apache License, Version 2.0 (the "License");
8 # you may not use this file except in compliance with the License.
9 # You may obtain a copy of the License at
11 # http://www.apache.org/licenses/LICENSE-2.0
13 # Unless required by applicable law or agreed to in writing, software
14 # distributed under the License is distributed on an "AS IS" BASIS,
15 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
25 import osm_ng_ro
.html_out
as html
27 import logging
.handlers
31 from osm_ng_ro
.ns
import Ns
, NsException
32 from osm_ng_ro
.validation
import ValidationError
33 from osm_ng_ro
.vim_admin
import VimAdminThread
34 from osm_common
.dbbase
import DbException
35 from osm_common
.fsbase
import FsException
36 from osm_common
.msgbase
import MsgException
37 from http
import HTTPStatus
38 from codecs
import getreader
39 from os
import environ
, path
40 from osm_ng_ro
import version
as ro_version
, version_date
as ro_version_date
42 __author__
= "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
44 __version__
= "0.1." # file version, not NBI version
45 version_date
= "May 2020"
47 database_version
= '1.2'
48 auth_database_version
= '1.0'
49 ro_server
= None # instance of Server class
50 vim_admin_thread
= None # instance of VimAdminThread class
52 # vim_threads = None # instance of VimThread class
55 RO North Bound Interface
56 URL: /ro GET POST PUT DELETE PATCH
64 valid_query_string
= ("ADMIN", "SET_PROJECT", "FORCE", "PUBLIC")
65 # ^ Contains possible administrative query string words:
66 # ADMIN=True(by default)|Project|Project-list: See all elements, or elements of a project
67 # (not owned by my session project).
68 # PUBLIC=True(by default)|False: See/hide public elements. Set/Unset a topic to be public
69 # FORCE=True(by default)|False: Force edition/deletion operations
70 # SET_PROJECT=Project|Project-list: Add/Delete the topic to the projects portfolio
73 # contains allowed URL and methods, and the role_permission name
78 "ROLE_PERMISSION": "tokens:",
80 "METHODS": ("DELETE",),
81 "ROLE_PERMISSION": "tokens:id:"
90 "ROLE_PERMISSION": "deploy:",
92 "METHODS": ("GET", "POST", "DELETE"),
93 "ROLE_PERMISSION": "deploy:id:",
96 "ROLE_PERMISSION": "deploy:id:id:",
99 "ROLE_PERMISSION": "deploy:id:id:cancel",
109 class RoException(Exception):
111 def __init__(self
, message
, http_code
=HTTPStatus
.METHOD_NOT_ALLOWED
):
112 Exception.__init
__(self
, message
)
113 self
.http_code
= http_code
116 class AuthException(RoException
):
122 def __init__(self
, valid_url_methods
, valid_query_string
):
123 self
.valid_url_methods
= valid_url_methods
124 self
.valid_query_string
= valid_query_string
126 def authorize(self
, *args
, **kwargs
):
127 return {"token": "ok", "id": "ok"}
129 def new_token(self
, token_info
, indata
, remote
):
130 return {"token": "ok",
134 def del_token(self
, token_id
):
137 def start(self
, engine_config
):
141 class Server(object):
143 # to decode bytes to str
144 reader
= getreader("utf-8")
148 self
.authenticator
= Authenticator(valid_url_methods
, valid_query_string
)
150 self
.map_operation
= {
151 "token:post": self
.new_token
,
152 "token:id:delete": self
.del_token
,
153 "deploy:get": self
.ns
.get_deploy
,
154 "deploy:id:get": self
.ns
.get_actions
,
155 "deploy:id:post": self
.ns
.deploy
,
156 "deploy:id:delete": self
.ns
.delete
,
157 "deploy:id:id:get": self
.ns
.status
,
158 "deploy:id:id:cancel:post": self
.ns
.cancel
,
161 def _format_in(self
, kwargs
):
164 if cherrypy
.request
.body
.length
:
165 error_text
= "Invalid input format "
167 if "Content-Type" in cherrypy
.request
.headers
:
168 if "application/json" in cherrypy
.request
.headers
["Content-Type"]:
169 error_text
= "Invalid json format "
170 indata
= json
.load(self
.reader(cherrypy
.request
.body
))
171 cherrypy
.request
.headers
.pop("Content-File-MD5", None)
172 elif "application/yaml" in cherrypy
.request
.headers
["Content-Type"]:
173 error_text
= "Invalid yaml format "
174 indata
= yaml
.load(cherrypy
.request
.body
, Loader
=yaml
.SafeLoader
)
175 cherrypy
.request
.headers
.pop("Content-File-MD5", None)
176 elif "application/binary" in cherrypy
.request
.headers
["Content-Type"] or \
177 "application/gzip" in cherrypy
.request
.headers
["Content-Type"] or \
178 "application/zip" in cherrypy
.request
.headers
["Content-Type"] or \
179 "text/plain" in cherrypy
.request
.headers
["Content-Type"]:
180 indata
= cherrypy
.request
.body
# .read()
181 elif "multipart/form-data" in cherrypy
.request
.headers
["Content-Type"]:
182 if "descriptor_file" in kwargs
:
183 filecontent
= kwargs
.pop("descriptor_file")
184 if not filecontent
.file:
185 raise RoException("empty file or content", HTTPStatus
.BAD_REQUEST
)
186 indata
= filecontent
.file # .read()
187 if filecontent
.content_type
.value
:
188 cherrypy
.request
.headers
["Content-Type"] = filecontent
.content_type
.value
190 # raise cherrypy.HTTPError(HTTPStatus.Not_Acceptable,
191 # "Only 'Content-Type' of type 'application/json' or
192 # 'application/yaml' for input format are available")
193 error_text
= "Invalid yaml format "
194 indata
= yaml
.load(cherrypy
.request
.body
, Loader
=yaml
.SafeLoader
)
195 cherrypy
.request
.headers
.pop("Content-File-MD5", None)
197 error_text
= "Invalid yaml format "
198 indata
= yaml
.load(cherrypy
.request
.body
, Loader
=yaml
.SafeLoader
)
199 cherrypy
.request
.headers
.pop("Content-File-MD5", None)
204 if cherrypy
.request
.headers
.get("Query-String-Format") == "yaml":
207 for k
, v
in kwargs
.items():
208 if isinstance(v
, str):
213 kwargs
[k
] = yaml
.load(v
, Loader
=yaml
.SafeLoader
)
216 elif k
.endswith(".gt") or k
.endswith(".lt") or k
.endswith(".gte") or k
.endswith(".lte"):
224 elif v
.find(",") > 0:
225 kwargs
[k
] = v
.split(",")
226 elif isinstance(v
, (list, tuple)):
227 for index
in range(0, len(v
)):
232 v
[index
] = yaml
.load(v
[index
], Loader
=yaml
.SafeLoader
)
237 except (ValueError, yaml
.YAMLError
) as exc
:
238 raise RoException(error_text
+ str(exc
), HTTPStatus
.BAD_REQUEST
)
239 except KeyError as exc
:
240 raise RoException("Query string error: " + str(exc
), HTTPStatus
.BAD_REQUEST
)
241 except Exception as exc
:
242 raise RoException(error_text
+ str(exc
), HTTPStatus
.BAD_REQUEST
)
245 def _format_out(data
, token_info
=None, _format
=None):
247 return string of dictionary data according to requested json, yaml, xml. By default json
248 :param data: response to be sent. Can be a dict, text or file
249 :param token_info: Contains among other username and project
250 :param _format: The format to be set as Content-Type if data is a file
253 accept
= cherrypy
.request
.headers
.get("Accept")
255 if accept
and "text/html" in accept
:
256 return html
.format(data
, cherrypy
.request
, cherrypy
.response
, token_info
)
257 # cherrypy.response.status = HTTPStatus.NO_CONTENT.value
259 elif hasattr(data
, "read"): # file object
261 cherrypy
.response
.headers
["Content-Type"] = _format
262 elif "b" in data
.mode
: # binariy asssumig zip
263 cherrypy
.response
.headers
["Content-Type"] = 'application/zip'
265 cherrypy
.response
.headers
["Content-Type"] = 'text/plain'
266 # TODO check that cherrypy close file. If not implement pending things to close per thread next
269 if "application/json" in accept
:
270 cherrypy
.response
.headers
["Content-Type"] = 'application/json; charset=utf-8'
271 a
= json
.dumps(data
, indent
=4) + "\n"
272 return a
.encode("utf8")
273 elif "text/html" in accept
:
274 return html
.format(data
, cherrypy
.request
, cherrypy
.response
, token_info
)
276 elif "application/yaml" in accept
or "*/*" in accept
or "text/plain" in accept
:
278 # if there is not any valid accept, raise an error. But if response is already an error, format in yaml
279 elif cherrypy
.response
.status
>= 400:
280 raise cherrypy
.HTTPError(HTTPStatus
.NOT_ACCEPTABLE
.value
,
281 "Only 'Accept' of type 'application/json' or 'application/yaml' "
282 "for output format are available")
283 cherrypy
.response
.headers
["Content-Type"] = 'application/yaml'
284 return yaml
.safe_dump(data
, explicit_start
=True, indent
=4, default_flow_style
=False, tags
=False,
285 encoding
='utf-8', allow_unicode
=True) # , canonical=True, default_style='"'
288 def index(self
, *args
, **kwargs
):
291 if cherrypy
.request
.method
== "GET":
292 token_info
= self
.authenticator
.authorize()
293 outdata
= token_info
# Home page
295 raise cherrypy
.HTTPError(HTTPStatus
.METHOD_NOT_ALLOWED
.value
,
296 "Method {} not allowed for tokens".format(cherrypy
.request
.method
))
298 return self
._format
_out
(outdata
, token_info
)
300 except (NsException
, AuthException
) as e
:
301 # cherrypy.log("index Exception {}".format(e))
302 cherrypy
.response
.status
= e
.http_code
.value
303 return self
._format
_out
("Welcome to OSM!", token_info
)
306 def version(self
, *args
, **kwargs
):
307 # TODO consider to remove and provide version using the static version file
309 if cherrypy
.request
.method
!= "GET":
310 raise RoException("Only method GET is allowed", HTTPStatus
.METHOD_NOT_ALLOWED
)
312 raise RoException("Invalid URL or query string for version", HTTPStatus
.METHOD_NOT_ALLOWED
)
313 # TODO include version of other modules, pick up from some kafka admin message
314 osm_ng_ro_version
= {"version": ro_version
, "date": ro_version_date
}
315 return self
._format
_out
(osm_ng_ro_version
)
316 except RoException
as e
:
317 cherrypy
.response
.status
= e
.http_code
.value
319 "code": e
.http_code
.name
,
320 "status": e
.http_code
.value
,
323 return self
._format
_out
(problem_details
, None)
325 def new_token(self
, engine_session
, indata
, *args
, **kwargs
):
329 token_info
= self
.authenticator
.authorize()
333 indata
.update(kwargs
)
334 # This is needed to log the user when authentication fails
335 cherrypy
.request
.login
= "{}".format(indata
.get("username", "-"))
336 token_info
= self
.authenticator
.new_token(token_info
, indata
, cherrypy
.request
.remote
)
337 cherrypy
.session
['Authorization'] = token_info
["id"]
338 self
._set
_location
_header
("admin", "v1", "tokens", token_info
["id"])
341 # cherrypy.response.cookie["Authorization"] = outdata["id"]
342 # cherrypy.response.cookie["Authorization"]['expires'] = 3600
343 return token_info
, token_info
["id"], True
345 def del_token(self
, engine_session
, indata
, version
, _id
, *args
, **kwargs
):
347 if not token_id
and "id" in kwargs
:
348 token_id
= kwargs
["id"]
350 token_info
= self
.authenticator
.authorize()
352 token_id
= token_info
["id"]
353 self
.authenticator
.del_token(token_id
)
355 cherrypy
.session
['Authorization'] = "logout"
356 # cherrypy.response.cookie["Authorization"] = token_id
357 # cherrypy.response.cookie["Authorization"]['expires'] = 0
358 return None, None, True
361 def test(self
, *args
, **kwargs
):
362 if not cherrypy
.config
.get("server.enable_test") or (isinstance(cherrypy
.config
["server.enable_test"], str) and
363 cherrypy
.config
["server.enable_test"].lower() == "false"):
364 cherrypy
.response
.status
= HTTPStatus
.METHOD_NOT_ALLOWED
.value
365 return "test URL is disabled"
367 if args
and args
[0] == "help":
368 return "<html><pre>\ninit\nfile/<name> download file\ndb-clear/table\nfs-clear[/folder]\nlogin\nlogin2\n"\
369 "sleep/<time>\nmessage/topic\n</pre></html>"
371 elif args
and args
[0] == "init":
373 # self.ns.load_dbase(cherrypy.request.app.config)
374 self
.ns
.create_admin()
375 return "Done. User 'admin', password 'admin' created"
377 cherrypy
.response
.status
= HTTPStatus
.FORBIDDEN
.value
378 return self
._format
_out
("Database already initialized")
379 elif args
and args
[0] == "file":
380 return cherrypy
.lib
.static
.serve_file(cherrypy
.tree
.apps
['/ro'].config
["storage"]["path"] + "/" + args
[1],
381 "text/plain", "attachment")
382 elif args
and args
[0] == "file2":
383 f_path
= cherrypy
.tree
.apps
['/ro'].config
["storage"]["path"] + "/" + args
[1]
384 f
= open(f_path
, "r")
385 cherrypy
.response
.headers
["Content-type"] = "text/plain"
388 elif len(args
) == 2 and args
[0] == "db-clear":
389 deleted_info
= self
.ns
.db
.del_list(args
[1], kwargs
)
390 return "{} {} deleted\n".format(deleted_info
["deleted"], args
[1])
391 elif len(args
) and args
[0] == "fs-clear":
395 folders
= self
.ns
.fs
.dir_ls(".")
396 for folder
in folders
:
397 self
.ns
.fs
.file_delete(folder
)
398 return ",".join(folders
) + " folders deleted\n"
399 elif args
and args
[0] == "login":
400 if not cherrypy
.request
.headers
.get("Authorization"):
401 cherrypy
.response
.headers
["WWW-Authenticate"] = 'Basic realm="Access to OSM site", charset="UTF-8"'
402 cherrypy
.response
.status
= HTTPStatus
.UNAUTHORIZED
.value
403 elif args
and args
[0] == "login2":
404 if not cherrypy
.request
.headers
.get("Authorization"):
405 cherrypy
.response
.headers
["WWW-Authenticate"] = 'Bearer realm="Access to OSM site"'
406 cherrypy
.response
.status
= HTTPStatus
.UNAUTHORIZED
.value
407 elif args
and args
[0] == "sleep":
410 sleep_time
= int(args
[1])
412 cherrypy
.response
.status
= HTTPStatus
.FORBIDDEN
.value
413 return self
._format
_out
("Database already initialized")
414 thread_info
= cherrypy
.thread_data
416 time
.sleep(sleep_time
)
418 elif len(args
) >= 2 and args
[0] == "message":
420 return_text
= "<html><pre>{} ->\n".format(main_topic
)
422 if cherrypy
.request
.method
== 'POST':
423 to_send
= yaml
.load(cherrypy
.request
.body
, Loader
=yaml
.SafeLoader
)
424 for k
, v
in to_send
.items():
425 self
.ns
.msg
.write(main_topic
, k
, v
)
426 return_text
+= " {}: {}\n".format(k
, v
)
427 elif cherrypy
.request
.method
== 'GET':
428 for k
, v
in kwargs
.items():
429 self
.ns
.msg
.write(main_topic
, k
, yaml
.load(v
, Loader
=yaml
.SafeLoader
))
430 return_text
+= " {}: {}\n".format(k
, yaml
.load(v
, Loader
=yaml
.SafeLoader
))
431 except Exception as e
:
432 return_text
+= "Error: " + str(e
)
433 return_text
+= "</pre></html>\n"
437 "<html><pre>\nheaders:\n args: {}\n".format(args
) +
438 " kwargs: {}\n".format(kwargs
) +
439 " headers: {}\n".format(cherrypy
.request
.headers
) +
440 " path_info: {}\n".format(cherrypy
.request
.path_info
) +
441 " query_string: {}\n".format(cherrypy
.request
.query_string
) +
442 " session: {}\n".format(cherrypy
.session
) +
443 " cookie: {}\n".format(cherrypy
.request
.cookie
) +
444 " method: {}\n".format(cherrypy
.request
.method
) +
445 " session: {}\n".format(cherrypy
.session
.get('fieldname')) +
447 return_text
+= " length: {}\n".format(cherrypy
.request
.body
.length
)
448 if cherrypy
.request
.body
.length
:
449 return_text
+= " content: {}\n".format(
450 str(cherrypy
.request
.body
.read(int(cherrypy
.request
.headers
.get('Content-Length', 0)))))
452 return_text
+= "thread: {}\n".format(thread_info
)
453 return_text
+= "</pre></html>"
457 def _check_valid_url_method(method
, *args
):
459 raise RoException("URL must contain at least 'main_topic/version/topic'", HTTPStatus
.METHOD_NOT_ALLOWED
)
461 reference
= valid_url_methods
465 if not isinstance(reference
, dict):
466 raise RoException("URL contains unexpected extra items '{}'".format(arg
),
467 HTTPStatus
.METHOD_NOT_ALLOWED
)
470 reference
= reference
[arg
]
471 elif "<ID>" in reference
:
472 reference
= reference
["<ID>"]
473 elif "*" in reference
:
474 # reference = reference["*"]
477 raise RoException("Unexpected URL item {}".format(arg
), HTTPStatus
.METHOD_NOT_ALLOWED
)
478 if "TODO" in reference
and method
in reference
["TODO"]:
479 raise RoException("Method {} not supported yet for this URL".format(method
), HTTPStatus
.NOT_IMPLEMENTED
)
480 elif "METHODS" not in reference
or method
not in reference
["METHODS"]:
481 raise RoException("Method {} not supported for this URL".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
482 return reference
["ROLE_PERMISSION"] + method
.lower()
485 def _set_location_header(main_topic
, version
, topic
, id):
487 Insert response header Location with the URL of created item base on URL params
494 # Use cherrypy.request.base for absoluted path and make use of request.header HOST just in case behind aNAT
495 cherrypy
.response
.headers
["Location"] = "/ro/{}/{}/{}/{}".format(main_topic
, version
, topic
, id)
499 def default(self
, main_topic
=None, version
=None, topic
=None, _id
=None, _id2
=None, *args
, **kwargs
):
505 engine_session
= None
507 if not main_topic
or not version
or not topic
:
508 raise RoException("URL must contain at least 'main_topic/version/topic'",
509 HTTPStatus
.METHOD_NOT_ALLOWED
)
510 if main_topic
not in ("admin", "ns",):
511 raise RoException("URL main_topic '{}' not supported".format(main_topic
),
512 HTTPStatus
.METHOD_NOT_ALLOWED
)
514 raise RoException("URL version '{}' not supported".format(version
), HTTPStatus
.METHOD_NOT_ALLOWED
)
516 if kwargs
and "METHOD" in kwargs
and kwargs
["METHOD"] in ("PUT", "POST", "DELETE", "GET", "PATCH"):
517 method
= kwargs
.pop("METHOD")
519 method
= cherrypy
.request
.method
521 role_permission
= self
._check
_valid
_url
_method
(method
, main_topic
, version
, topic
, _id
, _id2
, *args
,
523 # skip token validation if requesting a token
524 indata
= self
._format
_in
(kwargs
)
525 if main_topic
!= "admin" or topic
!= "tokens":
526 token_info
= self
.authenticator
.authorize(role_permission
, _id
)
527 outdata
, created_id
, done
= self
.map_operation
[role_permission
](
528 engine_session
, indata
, version
, _id
, _id2
, *args
, *kwargs
)
530 self
._set
_location
_header
(main_topic
, version
, topic
, _id
)
531 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
if not done
else HTTPStatus
.OK
.value
if \
532 outdata
is not None else HTTPStatus
.NO_CONTENT
.value
533 return self
._format
_out
(outdata
, token_info
, _format
)
534 except Exception as e
:
535 if isinstance(e
, (RoException
, NsException
, DbException
, FsException
, MsgException
, AuthException
,
537 http_code_value
= cherrypy
.response
.status
= e
.http_code
.value
538 http_code_name
= e
.http_code
.name
539 cherrypy
.log("Exception {}".format(e
))
541 http_code_value
= cherrypy
.response
.status
= HTTPStatus
.BAD_REQUEST
.value
# INTERNAL_SERVER_ERROR
542 cherrypy
.log("CRITICAL: Exception {}".format(e
), traceback
=True)
543 http_code_name
= HTTPStatus
.BAD_REQUEST
.name
544 if hasattr(outdata
, "close"): # is an open file
548 for rollback_item
in rollback
:
550 if rollback_item
.get("operation") == "set":
551 self
.ns
.db
.set_one(rollback_item
["topic"], {"_id": rollback_item
["_id"]},
552 rollback_item
["content"], fail_on_empty
=False)
554 self
.ns
.db
.del_one(rollback_item
["topic"], {"_id": rollback_item
["_id"]},
556 except Exception as e2
:
557 rollback_error_text
= "Rollback Exception {}: {}".format(rollback_item
, e2
)
558 cherrypy
.log(rollback_error_text
)
559 error_text
+= ". " + rollback_error_text
560 # if isinstance(e, MsgException):
561 # error_text = "{} has been '{}' but other modules cannot be informed because an error on bus".format(
562 # engine_topic[:-1], method, error_text)
564 "code": http_code_name
,
565 "status": http_code_value
,
566 "detail": error_text
,
568 return self
._format
_out
(problem_details
, token_info
)
569 # raise cherrypy.HTTPError(e.http_code.value, str(e))
572 if method
in ("PUT", "PATCH", "POST") and isinstance(outdata
, dict):
573 for logging_id
in ("id", "op_id", "nsilcmop_id", "nslcmop_id"):
574 if outdata
.get(logging_id
):
575 cherrypy
.request
.login
+= ";{}={}".format(logging_id
, outdata
[logging_id
][:36])
578 def _start_service():
580 Callback function called when cherrypy.engine starts
581 Override configuration with env variables
582 Set database, storage, message configuration
583 Init database with admin/admin user password
585 global ro_server
, vim_admin_thread
587 cherrypy
.log
.error("Starting osm_ng_ro")
588 # update general cherrypy configuration
591 engine_config
= cherrypy
.tree
.apps
['/ro'].config
592 for k
, v
in environ
.items():
593 if not k
.startswith("OSMRO_"):
595 k1
, _
, k2
= k
[6:].lower().partition("_")
599 if k1
in ("server", "test", "auth", "log"):
600 # update [global] configuration
601 update_dict
[k1
+ '.' + k2
] = yaml
.safe_load(v
)
603 # update [/static] configuration
604 engine_config
["/static"]["tools.staticdir." + k2
] = yaml
.safe_load(v
)
606 # update [/] configuration
607 engine_config
["/"]["tools." + k2
.replace('_', '.')] = yaml
.safe_load(v
)
608 elif k1
in ("message", "database", "storage", "authentication"):
609 engine_config
[k1
][k2
] = yaml
.safe_load(v
)
611 except Exception as e
:
612 raise RoException("Cannot load env '{}': {}".format(k
, e
))
615 cherrypy
.config
.update(update_dict
)
616 engine_config
["global"].update(update_dict
)
619 log_format_simple
= "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(message)s"
620 log_formatter_simple
= logging
.Formatter(log_format_simple
, datefmt
='%Y-%m-%dT%H:%M:%S')
621 logger_server
= logging
.getLogger("cherrypy.error")
622 logger_access
= logging
.getLogger("cherrypy.access")
623 logger_cherry
= logging
.getLogger("cherrypy")
624 logger_nbi
= logging
.getLogger("ro")
626 if "log.file" in engine_config
["global"]:
627 file_handler
= logging
.handlers
.RotatingFileHandler(engine_config
["global"]["log.file"],
628 maxBytes
=100e6
, backupCount
=9, delay
=0)
629 file_handler
.setFormatter(log_formatter_simple
)
630 logger_cherry
.addHandler(file_handler
)
631 logger_nbi
.addHandler(file_handler
)
632 # log always to standard output
633 for format_
, logger
in {"ro.server %(filename)s:%(lineno)s": logger_server
,
634 "ro.access %(filename)s:%(lineno)s": logger_access
,
635 "%(name)s %(filename)s:%(lineno)s": logger_nbi
637 log_format_cherry
= "%(asctime)s %(levelname)s {} %(message)s".format(format_
)
638 log_formatter_cherry
= logging
.Formatter(log_format_cherry
, datefmt
='%Y-%m-%dT%H:%M:%S')
639 str_handler
= logging
.StreamHandler()
640 str_handler
.setFormatter(log_formatter_cherry
)
641 logger
.addHandler(str_handler
)
643 if engine_config
["global"].get("log.level"):
644 logger_cherry
.setLevel(engine_config
["global"]["log.level"])
645 logger_nbi
.setLevel(engine_config
["global"]["log.level"])
646 # logging other modules
647 for k1
, logname
in {"message": "ro.msg", "database": "ro.db", "storage": "ro.fs"}.items():
648 engine_config
[k1
]["logger_name"] = logname
649 logger_module
= logging
.getLogger(logname
)
650 if "logfile" in engine_config
[k1
]:
651 file_handler
= logging
.handlers
.RotatingFileHandler(engine_config
[k1
]["logfile"],
652 maxBytes
=100e6
, backupCount
=9, delay
=0)
653 file_handler
.setFormatter(log_formatter_simple
)
654 logger_module
.addHandler(file_handler
)
655 if "loglevel" in engine_config
[k1
]:
656 logger_module
.setLevel(engine_config
[k1
]["loglevel"])
657 # TODO add more entries, e.g.: storage
659 engine_config
["assignment"] = {}
660 # ^ each VIM, SDNc will be assigned one worker id. Ns class will add items and VimThread will auto-assign
661 cherrypy
.tree
.apps
['/ro'].root
.ns
.start(engine_config
)
662 cherrypy
.tree
.apps
['/ro'].root
.authenticator
.start(engine_config
)
663 cherrypy
.tree
.apps
['/ro'].root
.ns
.init_db(target_version
=database_version
)
665 # # start subscriptions thread:
666 vim_admin_thread
= VimAdminThread(config
=engine_config
, engine
=ro_server
.ns
)
667 vim_admin_thread
.start()
668 # # Do not capture except SubscriptionException
670 # backend = engine_config["authentication"]["backend"]
671 # cherrypy.log.error("Starting OSM NBI Version '{} {}' with '{}' authentication backend"
672 # .format(ro_version, ro_version_date, backend))
677 Callback function called when cherrypy.engine stops
678 TODO: Ending database connections.
680 global vim_admin_thread
681 # terminate vim_admin_thread
683 vim_admin_thread
.terminate()
684 vim_admin_thread
= None
685 cherrypy
.tree
.apps
['/ro'].root
.ns
.stop()
686 cherrypy
.log
.error("Stopping osm_ng_ro")
689 def ro_main(config_file
):
692 cherrypy
.engine
.subscribe('start', _start_service
)
693 cherrypy
.engine
.subscribe('stop', _stop_service
)
694 cherrypy
.quickstart(ro_server
, '/ro', config_file
)
698 print("""Usage: {} [options]
699 -c|--config [configuration_file]: loads the configuration file (default: ./ro.cfg)
700 -h|--help: shows this help
701 """.format(sys
.argv
[0]))
702 # --log-socket-host HOST: send logs to this host")
703 # --log-socket-port PORT: send logs using this port (default: 9022)")
706 if __name__
== '__main__':
708 # load parameters and configuration
709 opts
, args
= getopt
.getopt(sys
.argv
[1:], "hvc:", ["config=", "help"])
710 # TODO add "log-socket-host=", "log-socket-port=", "log-file="
713 if o
in ("-h", "--help"):
716 elif o
in ("-c", "--config"):
719 assert False, "Unhandled option"
721 if not path
.isfile(config_file
):
722 print("configuration file '{}' that not exist".format(config_file
), file=sys
.stderr
)
725 for config_file
in (path
.dirname(__file__
) + "/ro.cfg", "./ro.cfg", "/etc/osm/ro.cfg"):
726 if path
.isfile(config_file
):
729 print("No configuration file 'ro.cfg' found neither at local folder nor at /etc/osm/", file=sys
.stderr
)
732 except KeyboardInterrupt:
733 print("KeyboardInterrupt. Finishing", file=sys
.stderr
)
734 except getopt
.GetoptError
as e
:
735 print(str(e
), file=sys
.stderr
)