Code Coverage

Cobertura Coverage Report > NG-RO.osm_ng_ro >

ro_main.py

Trend

Classes0%
 
Lines0%
 
Conditionals100%
 

File Coverage summary

NameClassesLinesConditionals
ro_main.py
0%
0/1
0%
0/457
100%
0/0

Coverage Breakdown by Class

NameLinesConditionals
ro_main.py
0%
0/457
N/A

Source

NG-RO/osm_ng_ro/ro_main.py
1 #!/usr/bin/python3
2 # -*- coding: utf-8 -*-
3
4 ##
5 # Copyright 2020 Telefonica Investigacion y Desarrollo, S.A.U.
6 #
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
10 #
11 #    http://www.apache.org/licenses/LICENSE-2.0
12 #
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
16 # implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19 ##
20
21 0 from codecs import getreader
22 0 import getopt
23 0 from http import HTTPStatus
24 0 import json
25 0 import logging
26 0 import logging.handlers
27 0 from os import environ, path
28 0 import sys
29 0 import time
30
31 0 import cherrypy
32 0 from osm_common.dbbase import DbException
33 0 from osm_common.fsbase import FsException
34 0 from osm_common.msgbase import MsgException
35 0 from osm_ng_ro import version as ro_version, version_date as ro_version_date
36 0 import osm_ng_ro.html_out as html
37 0 from osm_ng_ro.monitor import start_monitoring, stop_monitoring
38 0 from osm_ng_ro.ns import Ns, NsException
39 0 from osm_ng_ro.validation import ValidationError
40 0 from osm_ng_ro.vim_admin import VimAdminThread
41 0 import yaml
42
43 0 __author__ = "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
44
45 0 __version__ = "0.1."  # file version, not NBI version
46 0 version_date = "May 2020"
47
48 0 database_version = "1.2"
49 0 auth_database_version = "1.0"
50 0 ro_server = None  # instance of Server class
51 0 vim_admin_thread = None  # instance of VimAdminThread class
52
53 # vim_threads = None  # instance of VimThread class
54
55 """
56 RO North Bound Interface
57 URL: /ro                                                       GET     POST    PUT     DELETE  PATCH
58         /ns/v1/deploy                                           O
59             /<nsrs_id>                                          O       O               O
60                 /<action_id>                                    O
61                     /cancel                                             O
62
63 """
64
65 0 valid_query_string = ("ADMIN", "SET_PROJECT", "FORCE", "PUBLIC")
66 # ^ Contains possible administrative query string words:
67 #     ADMIN=True(by default)|Project|Project-list:  See all elements, or elements of a project
68 #           (not owned by my session project).
69 #     PUBLIC=True(by default)|False: See/hide public elements. Set/Unset a topic to be public
70 #     FORCE=True(by default)|False: Force edition/deletion operations
71 #     SET_PROJECT=Project|Project-list: Add/Delete the topic to the projects portfolio
72
73 0 valid_url_methods = {
74     # contains allowed URL and methods, and the role_permission name
75     "admin": {
76         "v1": {
77             "tokens": {
78                 "METHODS": ("POST",),
79                 "ROLE_PERMISSION": "tokens:",
80                 "<ID>": {"METHODS": ("DELETE",), "ROLE_PERMISSION": "tokens:id:"},
81             },
82         }
83     },
84     "ns": {
85         "v1": {
86             "deploy": {
87                 "METHODS": ("GET",),
88                 "ROLE_PERMISSION": "deploy:",
89                 "<ID>": {
90                     "METHODS": ("GET", "POST", "DELETE"),
91                     "ROLE_PERMISSION": "deploy:id:",
92                     "<ID>": {
93                         "METHODS": ("GET",),
94                         "ROLE_PERMISSION": "deploy:id:id:",
95                         "cancel": {
96                             "METHODS": ("POST",),
97                             "ROLE_PERMISSION": "deploy:id:id:cancel",
98                         },
99                     },
100                 },
101             },
102         }
103     },
104 }
105
106
107 0 class RoException(Exception):
108 0     def __init__(self, message, http_code=HTTPStatus.METHOD_NOT_ALLOWED):
109 0         Exception.__init__(self, message)
110 0         self.http_code = http_code
111
112
113 0 class AuthException(RoException):
114 0     pass
115
116
117 0 class Authenticator:
118 0     def __init__(self, valid_url_methods, valid_query_string):
119 0         self.valid_url_methods = valid_url_methods
120 0         self.valid_query_string = valid_query_string
121
122 0     def authorize(self, *args, **kwargs):
123 0         return {"token": "ok", "id": "ok"}
124
125 0     def new_token(self, token_info, indata, remote):
126 0         return {"token": "ok", "id": "ok", "remote": remote}
127
128 0     def del_token(self, token_id):
129 0         pass
130
131 0     def start(self, engine_config):
132 0         pass
133
134
135 0 class Server(object):
136 0     instance = 0
137     # to decode bytes to str
138 0     reader = getreader("utf-8")
139
140 0     def __init__(self):
141 0         self.instance += 1
142 0         self.authenticator = Authenticator(valid_url_methods, valid_query_string)
143 0         self.ns = Ns()
144 0         self.map_operation = {
145             "token:post": self.new_token,
146             "token:id:delete": self.del_token,
147             "deploy:get": self.ns.get_deploy,
148             "deploy:id:get": self.ns.get_actions,
149             "deploy:id:post": self.ns.deploy,
150             "deploy:id:delete": self.ns.delete,
151             "deploy:id:id:get": self.ns.status,
152             "deploy:id:id:cancel:post": self.ns.cancel,
153         }
154
155 0     def _format_in(self, kwargs):
156 0         try:
157 0             indata = None
158
159 0             if cherrypy.request.body.length:
160 0                 error_text = "Invalid input format "
161
162 0                 if "Content-Type" in cherrypy.request.headers:
163 0                     if "application/json" in cherrypy.request.headers["Content-Type"]:
164 0                         error_text = "Invalid json format "
165 0                         indata = json.load(self.reader(cherrypy.request.body))
166 0                         cherrypy.request.headers.pop("Content-File-MD5", None)
167 0                     elif "application/yaml" in cherrypy.request.headers["Content-Type"]:
168 0                         error_text = "Invalid yaml format "
169 0                         indata = yaml.safe_load(cherrypy.request.body)
170 0                         cherrypy.request.headers.pop("Content-File-MD5", None)
171 0                     elif (
172                         "application/binary" in cherrypy.request.headers["Content-Type"]
173                         or "application/gzip"
174                         in cherrypy.request.headers["Content-Type"]
175                         or "application/zip" in cherrypy.request.headers["Content-Type"]
176                         or "text/plain" in cherrypy.request.headers["Content-Type"]
177                     ):
178 0                         indata = cherrypy.request.body  # .read()
179 0                     elif (
180                         "multipart/form-data"
181                         in cherrypy.request.headers["Content-Type"]
182                     ):
183 0                         if "descriptor_file" in kwargs:
184 0                             filecontent = kwargs.pop("descriptor_file")
185
186 0                             if not filecontent.file:
187 0                                 raise RoException(
188                                     "empty file or content", HTTPStatus.BAD_REQUEST
189                                 )
190
191 0                             indata = filecontent.file  # .read()
192
193 0                             if filecontent.content_type.value:
194 0                                 cherrypy.request.headers[
195                                     "Content-Type"
196                                 ] = filecontent.content_type.value
197                     else:
198                         # raise cherrypy.HTTPError(HTTPStatus.Not_Acceptable,
199                         #                          "Only 'Content-Type' of type 'application/json' or
200                         # 'application/yaml' for input format are available")
201 0                         error_text = "Invalid yaml format "
202 0                         indata = yaml.safe_load(cherrypy.request.body)
203 0                         cherrypy.request.headers.pop("Content-File-MD5", None)
204                 else:
205 0                     error_text = "Invalid yaml format "
206 0                     indata = yaml.safe_load(cherrypy.request.body)
207 0                     cherrypy.request.headers.pop("Content-File-MD5", None)
208
209 0             if not indata:
210 0                 indata = {}
211
212 0             format_yaml = False
213 0             if cherrypy.request.headers.get("Query-String-Format") == "yaml":
214 0                 format_yaml = True
215
216 0             for k, v in kwargs.items():
217 0                 if isinstance(v, str):
218 0                     if v == "":
219 0                         kwargs[k] = None
220 0                     elif format_yaml:
221 0                         try:
222 0                             kwargs[k] = yaml.safe_load(v)
223 0                         except Exception as yaml_error:
224 0                             logging.exception(
225                                 f"{yaml_error} occured while parsing the yaml"
226                             )
227 0                     elif (
228                         k.endswith(".gt")
229                         or k.endswith(".lt")
230                         or k.endswith(".gte")
231                         or k.endswith(".lte")
232                     ):
233 0                         try:
234 0                             kwargs[k] = int(v)
235 0                         except Exception:
236 0                             try:
237 0                                 kwargs[k] = float(v)
238 0                             except Exception as keyword_error:
239 0                                 logging.exception(
240                                     f"{keyword_error} occured while getting the keyword arguments"
241                                 )
242 0                     elif v.find(",") > 0:
243 0                         kwargs[k] = v.split(",")
244 0                 elif isinstance(v, (list, tuple)):
245 0                     for index in range(0, len(v)):
246 0                         if v[index] == "":
247 0                             v[index] = None
248 0                         elif format_yaml:
249 0                             try:
250 0                                 v[index] = yaml.safe_load(v[index])
251 0                             except Exception as error:
252 0                                 logging.exception(
253                                     f"{error} occured while parsing the yaml"
254                                 )
255
256 0             return indata
257 0         except (ValueError, yaml.YAMLError) as exc:
258 0             raise RoException(error_text + str(exc), HTTPStatus.BAD_REQUEST)
259 0         except KeyError as exc:
260 0             raise RoException("Query string error: " + str(exc), HTTPStatus.BAD_REQUEST)
261 0         except Exception as exc:
262 0             raise RoException(error_text + str(exc), HTTPStatus.BAD_REQUEST)
263
264 0     @staticmethod
265 0     def _format_out(data, token_info=None, _format=None):
266         """
267         return string of dictionary data according to requested json, yaml, xml. By default json
268         :param data: response to be sent. Can be a dict, text or file
269         :param token_info: Contains among other username and project
270         :param _format: The format to be set as Content-Type if data is a file
271         :return: None
272         """
273 0         accept = cherrypy.request.headers.get("Accept")
274
275 0         if data is None:
276 0             if accept and "text/html" in accept:
277 0                 return html.format(
278                     data, cherrypy.request, cherrypy.response, token_info
279                 )
280
281             # cherrypy.response.status = HTTPStatus.NO_CONTENT.value
282 0             return
283 0         elif hasattr(data, "read"):  # file object
284 0             if _format:
285 0                 cherrypy.response.headers["Content-Type"] = _format
286 0             elif "b" in data.mode:  # binariy asssumig zip
287 0                 cherrypy.response.headers["Content-Type"] = "application/zip"
288             else:
289 0                 cherrypy.response.headers["Content-Type"] = "text/plain"
290
291             # TODO check that cherrypy close file. If not implement pending things to close  per thread next
292 0             return data
293
294 0         if accept:
295 0             if "application/json" in accept:
296 0                 cherrypy.response.headers[
297                     "Content-Type"
298                 ] = "application/json; charset=utf-8"
299 0                 a = json.dumps(data, indent=4) + "\n"
300
301 0                 return a.encode("utf8")
302 0             elif "text/html" in accept:
303 0                 return html.format(
304                     data, cherrypy.request, cherrypy.response, token_info
305                 )
306 0             elif (
307                 "application/yaml" in accept
308                 or "*/*" in accept
309                 or "text/plain" in accept
310             ):
311 0                 pass
312             # if there is not any valid accept, raise an error. But if response is already an error, format in yaml
313 0             elif cherrypy.response.status >= 400:
314 0                 raise cherrypy.HTTPError(
315                     HTTPStatus.NOT_ACCEPTABLE.value,
316                     "Only 'Accept' of type 'application/json' or 'application/yaml' "
317                     "for output format are available",
318                 )
319
320 0         cherrypy.response.headers["Content-Type"] = "application/yaml"
321
322 0         return yaml.safe_dump(
323             data,
324             explicit_start=True,
325             indent=4,
326             default_flow_style=False,
327             tags=False,
328             encoding="utf-8",
329             allow_unicode=True,
330         )  # , canonical=True, default_style='"'
331
332 0     @cherrypy.expose
333 0     def index(self, *args, **kwargs):
334 0         token_info = None
335
336 0         try:
337 0             if cherrypy.request.method == "GET":
338 0                 token_info = self.authenticator.authorize()
339 0                 outdata = token_info  # Home page
340             else:
341 0                 raise cherrypy.HTTPError(
342                     HTTPStatus.METHOD_NOT_ALLOWED.value,
343                     "Method {} not allowed for tokens".format(cherrypy.request.method),
344                 )
345
346 0             return self._format_out(outdata, token_info)
347 0         except (NsException, AuthException) as e:
348             # cherrypy.log("index Exception {}".format(e))
349 0             cherrypy.response.status = e.http_code.value
350
351 0             return self._format_out("Welcome to OSM!", token_info)
352
353 0     @cherrypy.expose
354 0     def version(self, *args, **kwargs):
355         # TODO consider to remove and provide version using the static version file
356 0         try:
357 0             if cherrypy.request.method != "GET":
358 0                 raise RoException(
359                     "Only method GET is allowed",
360                     HTTPStatus.METHOD_NOT_ALLOWED,
361                 )
362 0             elif args or kwargs:
363 0                 raise RoException(
364                     "Invalid URL or query string for version",
365                     HTTPStatus.METHOD_NOT_ALLOWED,
366                 )
367
368             # TODO include version of other modules, pick up from some kafka admin message
369 0             osm_ng_ro_version = {"version": ro_version, "date": ro_version_date}
370
371 0             return self._format_out(osm_ng_ro_version)
372 0         except RoException as e:
373 0             cherrypy.response.status = e.http_code.value
374 0             problem_details = {
375                 "code": e.http_code.name,
376                 "status": e.http_code.value,
377                 "detail": str(e),
378             }
379
380 0             return self._format_out(problem_details, None)
381
382 0     def new_token(self, engine_session, indata, *args, **kwargs):
383 0         token_info = None
384
385 0         try:
386 0             token_info = self.authenticator.authorize()
387 0         except Exception:
388 0             token_info = None
389
390 0         if kwargs:
391 0             indata.update(kwargs)
392
393         # This is needed to log the user when authentication fails
394 0         cherrypy.request.login = "{}".format(indata.get("username", "-"))
395 0         token_info = self.authenticator.new_token(
396             token_info, indata, cherrypy.request.remote
397         )
398 0         cherrypy.session["Authorization"] = token_info["id"]
399 0         self._set_location_header("admin", "v1", "tokens", token_info["id"])
400         # for logging
401
402         # cherrypy.response.cookie["Authorization"] = outdata["id"]
403         # cherrypy.response.cookie["Authorization"]['expires'] = 3600
404
405 0         return token_info, token_info["id"], True
406
407 0     def del_token(self, engine_session, indata, version, _id, *args, **kwargs):
408 0         token_id = _id
409
410 0         if not token_id and "id" in kwargs:
411 0             token_id = kwargs["id"]
412 0         elif not token_id:
413 0             token_info = self.authenticator.authorize()
414             # for logging
415 0             token_id = token_info["id"]
416
417 0         self.authenticator.del_token(token_id)
418 0         token_info = None
419 0         cherrypy.session["Authorization"] = "logout"
420         # cherrypy.response.cookie["Authorization"] = token_id
421         # cherrypy.response.cookie["Authorization"]['expires'] = 0
422
423 0         return None, None, True
424
425 0     @cherrypy.expose
426 0     def test(self, *args, **kwargs):
427 0         if not cherrypy.config.get("server.enable_test") or (
428             isinstance(cherrypy.config["server.enable_test"], str)
429             and cherrypy.config["server.enable_test"].lower() == "false"
430         ):
431 0             cherrypy.response.status = HTTPStatus.METHOD_NOT_ALLOWED.value
432
433 0             return "test URL is disabled"
434
435 0         thread_info = None
436
437 0         if args and args[0] == "help":
438 0             return (
439                 "<html><pre>\ninit\nfile/<name>  download file\ndb-clear/table\nfs-clear[/folder]\nlogin\nlogin2\n"
440                 "sleep/<time>\nmessage/topic\n</pre></html>"
441             )
442 0         elif args and args[0] == "init":
443 0             try:
444                 # self.ns.load_dbase(cherrypy.request.app.config)
445 0                 self.ns.create_admin()
446
447 0                 return "Done. User 'admin', password 'admin' created"
448 0             except Exception:
449 0                 cherrypy.response.status = HTTPStatus.FORBIDDEN.value
450
451 0                 return self._format_out("Database already initialized")
452 0         elif args and args[0] == "file":
453 0             return cherrypy.lib.static.serve_file(
454                 cherrypy.tree.apps["/ro"].config["storage"]["path"] + "/" + args[1],
455                 "text/plain",
456                 "attachment",
457             )
458 0         elif args and args[0] == "file2":
459 0             f_path = cherrypy.tree.apps["/ro"].config["storage"]["path"] + "/" + args[1]
460 0             f = open(f_path, "r")
461 0             cherrypy.response.headers["Content-type"] = "text/plain"
462 0             return f
463
464 0         elif len(args) == 2 and args[0] == "db-clear":
465 0             deleted_info = self.ns.db.del_list(args[1], kwargs)
466 0             return "{} {} deleted\n".format(deleted_info["deleted"], args[1])
467 0         elif len(args) and args[0] == "fs-clear":
468 0             if len(args) >= 2:
469 0                 folders = (args[1],)
470             else:
471 0                 folders = self.ns.fs.dir_ls(".")
472
473 0             for folder in folders:
474 0                 self.ns.fs.file_delete(folder)
475
476 0             return ",".join(folders) + " folders deleted\n"
477 0         elif args and args[0] == "login":
478 0             if not cherrypy.request.headers.get("Authorization"):
479 0                 cherrypy.response.headers[
480                     "WWW-Authenticate"
481                 ] = 'Basic realm="Access to OSM site", charset="UTF-8"'
482 0                 cherrypy.response.status = HTTPStatus.UNAUTHORIZED.value
483 0         elif args and args[0] == "login2":
484 0             if not cherrypy.request.headers.get("Authorization"):
485 0                 cherrypy.response.headers[
486                     "WWW-Authenticate"
487                 ] = 'Bearer realm="Access to OSM site"'
488 0                 cherrypy.response.status = HTTPStatus.UNAUTHORIZED.value
489 0         elif args and args[0] == "sleep":
490 0             sleep_time = 5
491
492 0             try:
493 0                 sleep_time = int(args[1])
494 0             except Exception:
495 0                 cherrypy.response.status = HTTPStatus.FORBIDDEN.value
496 0                 return self._format_out("Database already initialized")
497
498 0             thread_info = cherrypy.thread_data
499 0             print(thread_info)
500 0             time.sleep(sleep_time)
501             # thread_info
502 0         elif len(args) >= 2 and args[0] == "message":
503 0             main_topic = args[1]
504 0             return_text = "<html><pre>{} ->\n".format(main_topic)
505
506 0             try:
507 0                 if cherrypy.request.method == "POST":
508 0                     to_send = yaml.safe_load(cherrypy.request.body)
509 0                     for k, v in to_send.items():
510 0                         self.ns.msg.write(main_topic, k, v)
511 0                         return_text += "  {}: {}\n".format(k, v)
512 0                 elif cherrypy.request.method == "GET":
513 0                     for k, v in kwargs.items():
514 0                         self.ns.msg.write(main_topic, k, yaml.safe_load(v))
515 0                         return_text += "  {}: {}\n".format(k, yaml.safe_load(v))
516 0             except Exception as e:
517 0                 return_text += "Error: " + str(e)
518
519 0             return_text += "</pre></html>\n"
520
521 0             return return_text
522
523 0         return_text = (
524             "<html><pre>\nheaders:\n  args: {}\n".format(args)
525             + "  kwargs: {}\n".format(kwargs)
526             + "  headers: {}\n".format(cherrypy.request.headers)
527             + "  path_info: {}\n".format(cherrypy.request.path_info)
528             + "  query_string: {}\n".format(cherrypy.request.query_string)
529             + "  session: {}\n".format(cherrypy.session)
530             + "  cookie: {}\n".format(cherrypy.request.cookie)
531             + "  method: {}\n".format(cherrypy.request.method)
532             + "  session: {}\n".format(cherrypy.session.get("fieldname"))
533             + "  body:\n"
534         )
535 0         return_text += "    length: {}\n".format(cherrypy.request.body.length)
536
537 0         if cherrypy.request.body.length:
538 0             return_text += "    content: {}\n".format(
539                 str(
540                     cherrypy.request.body.read(
541                         int(cherrypy.request.headers.get("Content-Length", 0))
542                     )
543                 )
544             )
545
546 0         if thread_info:
547 0             return_text += "thread: {}\n".format(thread_info)
548
549 0         return_text += "</pre></html>"
550
551 0         return return_text
552
553 0     @staticmethod
554 0     def _check_valid_url_method(method, *args):
555 0         if len(args) < 3:
556 0             raise RoException(
557                 "URL must contain at least 'main_topic/version/topic'",
558                 HTTPStatus.METHOD_NOT_ALLOWED,
559             )
560
561 0         reference = valid_url_methods
562 0         for arg in args:
563 0             if arg is None:
564 0                 break
565
566 0             if not isinstance(reference, dict):
567 0                 raise RoException(
568                     "URL contains unexpected extra items '{}'".format(arg),
569                     HTTPStatus.METHOD_NOT_ALLOWED,
570                 )
571
572 0             if arg in reference:
573 0                 reference = reference[arg]
574 0             elif "<ID>" in reference:
575 0                 reference = reference["<ID>"]
576 0             elif "*" in reference:
577                 # reference = reference["*"]
578 0                 break
579             else:
580 0                 raise RoException(
581                     "Unexpected URL item {}".format(arg),
582                     HTTPStatus.METHOD_NOT_ALLOWED,
583                 )
584
585 0         if "TODO" in reference and method in reference["TODO"]:
586 0             raise RoException(
587                 "Method {} not supported yet for this URL".format(method),
588                 HTTPStatus.NOT_IMPLEMENTED,
589             )
590 0         elif "METHODS" not in reference or method not in reference["METHODS"]:
591 0             raise RoException(
592                 "Method {} not supported for this URL".format(method),
593                 HTTPStatus.METHOD_NOT_ALLOWED,
594             )
595
596 0         return reference["ROLE_PERMISSION"] + method.lower()
597
598 0     @staticmethod
599 0     def _set_location_header(main_topic, version, topic, id):
600         """
601         Insert response header Location with the URL of created item base on URL params
602         :param main_topic:
603         :param version:
604         :param topic:
605         :param id:
606         :return: None
607         """
608         # Use cherrypy.request.base for absoluted path and make use of request.header HOST just in case behind aNAT
609 0         cherrypy.response.headers["Location"] = "/ro/{}/{}/{}/{}".format(
610             main_topic, version, topic, id
611         )
612
613 0         return
614
615 0     @cherrypy.expose
616 0     def default(
617         self,
618         main_topic=None,
619         version=None,
620         topic=None,
621         _id=None,
622         _id2=None,
623         *args,
624         **kwargs,
625     ):
626 0         token_info = None
627 0         outdata = None
628 0         _format = None
629 0         method = "DONE"
630 0         rollback = []
631 0         engine_session = None
632
633 0         try:
634 0             if not main_topic or not version or not topic:
635 0                 raise RoException(
636                     "URL must contain at least 'main_topic/version/topic'",
637                     HTTPStatus.METHOD_NOT_ALLOWED,
638                 )
639
640 0             if main_topic not in (
641                 "admin",
642                 "ns",
643             ):
644 0                 raise RoException(
645                     "URL main_topic '{}' not supported".format(main_topic),
646                     HTTPStatus.METHOD_NOT_ALLOWED,
647                 )
648
649 0             if version != "v1":
650 0                 raise RoException(
651                     "URL version '{}' not supported".format(version),
652                     HTTPStatus.METHOD_NOT_ALLOWED,
653                 )
654
655 0             if (
656                 kwargs
657                 and "METHOD" in kwargs
658                 and kwargs["METHOD"] in ("PUT", "POST", "DELETE", "GET", "PATCH")
659             ):
660 0                 method = kwargs.pop("METHOD")
661             else:
662 0                 method = cherrypy.request.method
663
664 0             role_permission = self._check_valid_url_method(
665                 method, main_topic, version, topic, _id, _id2, *args, **kwargs
666             )
667             # skip token validation if requesting a token
668 0             indata = self._format_in(kwargs)
669
670 0             if main_topic != "admin" or topic != "tokens":
671 0                 token_info = self.authenticator.authorize(role_permission, _id)
672
673 0             outdata, created_id, done = self.map_operation[role_permission](
674                 engine_session, indata, version, _id, _id2, *args, *kwargs
675             )
676
677 0             if created_id:
678 0                 self._set_location_header(main_topic, version, topic, _id)
679
680 0             cherrypy.response.status = (
681                 HTTPStatus.ACCEPTED.value
682                 if not done
683                 else HTTPStatus.OK.value
684                 if outdata is not None
685                 else HTTPStatus.NO_CONTENT.value
686             )
687
688 0             return self._format_out(outdata, token_info, _format)
689 0         except Exception as e:
690 0             if isinstance(
691                 e,
692                 (
693                     RoException,
694                     NsException,
695                     DbException,
696                     FsException,
697                     MsgException,
698                     AuthException,
699                     ValidationError,
700                 ),
701             ):
702 0                 http_code_value = cherrypy.response.status = e.http_code.value
703 0                 http_code_name = e.http_code.name
704 0                 cherrypy.log("Exception {}".format(e))
705             else:
706 0                 http_code_value = (
707                     cherrypy.response.status
708                 ) = HTTPStatus.BAD_REQUEST.value  # INTERNAL_SERVER_ERROR
709 0                 cherrypy.log("CRITICAL: Exception {}".format(e), traceback=True)
710 0                 http_code_name = HTTPStatus.BAD_REQUEST.name
711
712 0             if hasattr(outdata, "close"):  # is an open file
713 0                 outdata.close()
714
715 0             error_text = str(e)
716 0             rollback.reverse()
717
718 0             for rollback_item in rollback:
719 0                 try:
720 0                     if rollback_item.get("operation") == "set":
721 0                         self.ns.db.set_one(
722                             rollback_item["topic"],
723                             {"_id": rollback_item["_id"]},
724                             rollback_item["content"],
725                             fail_on_empty=False,
726                         )
727                     else:
728 0                         self.ns.db.del_one(
729                             rollback_item["topic"],
730                             {"_id": rollback_item["_id"]},
731                             fail_on_empty=False,
732                         )
733 0                 except Exception as e2:
734 0                     rollback_error_text = "Rollback Exception {}: {}".format(
735                         rollback_item, e2
736                     )
737 0                     cherrypy.log(rollback_error_text)
738 0                     error_text += ". " + rollback_error_text
739
740             # if isinstance(e, MsgException):
741             #     error_text = "{} has been '{}' but other modules cannot be informed because an error on bus".format(
742             #         engine_topic[:-1], method, error_text)
743 0             problem_details = {
744                 "code": http_code_name,
745                 "status": http_code_value,
746                 "detail": error_text,
747             }
748
749 0             return self._format_out(problem_details, token_info)
750             # raise cherrypy.HTTPError(e.http_code.value, str(e))
751         finally:
752 0             if token_info:
753 0                 if method in ("PUT", "PATCH", "POST") and isinstance(outdata, dict):
754 0                     for logging_id in ("id", "op_id", "nsilcmop_id", "nslcmop_id"):
755 0                         if outdata.get(logging_id):
756 0                             cherrypy.request.login += ";{}={}".format(
757                                 logging_id, outdata[logging_id][:36]
758                             )
759
760
761 0 def _start_service():
762     """
763     Callback function called when cherrypy.engine starts
764     Override configuration with env variables
765     Set database, storage, message configuration
766     Init database with admin/admin user password
767     """
768     global ro_server, vim_admin_thread
769     # global vim_threads
770 0     cherrypy.log.error("Starting osm_ng_ro")
771     # update general cherrypy configuration
772 0     update_dict = {}
773 0     engine_config = cherrypy.tree.apps["/ro"].config
774
775 0     for k, v in environ.items():
776 0         if not k.startswith("OSMRO_"):
777 0             continue
778
779 0         k1, _, k2 = k[6:].lower().partition("_")
780
781 0         if not k2:
782 0             continue
783
784 0         try:
785 0             if k1 in ("server", "test", "auth", "log"):
786                 # update [global] configuration
787 0                 update_dict[k1 + "." + k2] = yaml.safe_load(v)
788 0             elif k1 == "static":
789                 # update [/static] configuration
790 0                 engine_config["/static"]["tools.staticdir." + k2] = yaml.safe_load(v)
791 0             elif k1 == "tools":
792                 # update [/] configuration
793 0                 engine_config["/"]["tools." + k2.replace("_", ".")] = yaml.safe_load(v)
794 0             elif k1 in ("message", "database", "storage", "authentication", "period"):
795 0                 engine_config[k1][k2] = yaml.safe_load(v)
796
797 0         except Exception as e:
798 0             raise RoException("Cannot load env '{}': {}".format(k, e))
799
800 0     if update_dict:
801 0         cherrypy.config.update(update_dict)
802 0         engine_config["global"].update(update_dict)
803
804     # logging cherrypy
805 0     log_format_simple = (
806         "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(message)s"
807     )
808 0     log_formatter_simple = logging.Formatter(
809         log_format_simple, datefmt="%Y-%m-%dT%H:%M:%S"
810     )
811 0     logger_server = logging.getLogger("cherrypy.error")
812 0     logger_access = logging.getLogger("cherrypy.access")
813 0     logger_cherry = logging.getLogger("cherrypy")
814 0     logger = logging.getLogger("ro")
815
816 0     if "log.file" in engine_config["global"]:
817 0         file_handler = logging.handlers.RotatingFileHandler(
818             engine_config["global"]["log.file"], maxBytes=100e6, backupCount=9, delay=0
819         )
820 0         file_handler.setFormatter(log_formatter_simple)
821 0         logger_cherry.addHandler(file_handler)
822 0         logger.addHandler(file_handler)
823
824     # log always to standard output
825 0     for format_, logger in {
826         "ro.server %(filename)s:%(lineno)s": logger_server,
827         "ro.access %(filename)s:%(lineno)s": logger_access,
828         "%(name)s %(filename)s:%(lineno)s": logger,
829     }.items():
830 0         log_format_cherry = "%(asctime)s %(levelname)s {} %(message)s".format(format_)
831 0         log_formatter_cherry = logging.Formatter(
832             log_format_cherry, datefmt="%Y-%m-%dT%H:%M:%S"
833         )
834 0         str_handler = logging.StreamHandler()
835 0         str_handler.setFormatter(log_formatter_cherry)
836 0         logger.addHandler(str_handler)
837
838 0     if engine_config["global"].get("log.level"):
839 0         logger_cherry.setLevel(engine_config["global"]["log.level"])
840 0         logger.setLevel(engine_config["global"]["log.level"])
841
842     # logging other modules
843 0     for k1, logname in {
844         "message": "ro.msg",
845         "database": "ro.db",
846         "storage": "ro.fs",
847     }.items():
848 0         engine_config[k1]["logger_name"] = logname
849 0         logger_module = logging.getLogger(logname)
850
851 0         if "logfile" in engine_config[k1]:
852 0             file_handler = logging.handlers.RotatingFileHandler(
853                 engine_config[k1]["logfile"], maxBytes=100e6, backupCount=9, delay=0
854             )
855 0             file_handler.setFormatter(log_formatter_simple)
856 0             logger_module.addHandler(file_handler)
857
858 0         if "loglevel" in engine_config[k1]:
859 0             logger_module.setLevel(engine_config[k1]["loglevel"])
860     # TODO add more entries, e.g.: storage
861
862 0     engine_config["assignment"] = {}
863     # ^ each VIM, SDNc will be assigned one worker id. Ns class will add items and VimThread will auto-assign
864 0     cherrypy.tree.apps["/ro"].root.ns.start(engine_config)
865 0     cherrypy.tree.apps["/ro"].root.authenticator.start(engine_config)
866 0     cherrypy.tree.apps["/ro"].root.ns.init_db(target_version=database_version)
867
868     # # start subscriptions thread:
869 0     vim_admin_thread = VimAdminThread(config=engine_config, engine=ro_server.ns)
870 0     vim_admin_thread.start()
871 0     start_monitoring(config=engine_config)
872
873     # # Do not capture except SubscriptionException
874
875     # backend = engine_config["authentication"]["backend"]
876     # cherrypy.log.error("Starting OSM NBI Version '{} {}' with '{}' authentication backend"
877     #                    .format(ro_version, ro_version_date, backend))
878
879
880 0 def _stop_service():
881     """
882     Callback function called when cherrypy.engine stops
883     TODO: Ending database connections.
884     """
885     global vim_admin_thread
886
887     # terminate vim_admin_thread
888 0     if vim_admin_thread:
889 0         vim_admin_thread.terminate()
890 0     stop_monitoring()
891 0     vim_admin_thread = None
892 0     cherrypy.tree.apps["/ro"].root.ns.stop()
893 0     cherrypy.log.error("Stopping osm_ng_ro")
894
895
896 0 def ro_main(config_file):
897     global ro_server
898
899 0     ro_server = Server()
900 0     cherrypy.engine.subscribe("start", _start_service)
901 0     cherrypy.engine.subscribe("stop", _stop_service)
902 0     cherrypy.quickstart(ro_server, "/ro", config_file)
903
904
905 0 def usage():
906 0     print(
907         """Usage: {} [options]
908         -c|--config [configuration_file]: loads the configuration file (default: ./ro.cfg)
909         -h|--help: shows this help
910         """.format(
911             sys.argv[0]
912         )
913     )
914     # --log-socket-host HOST: send logs to this host")
915     # --log-socket-port PORT: send logs using this port (default: 9022)")
916
917
918 0 if __name__ == "__main__":
919 0     try:
920         # load parameters and configuration
921 0         opts, args = getopt.getopt(sys.argv[1:], "hvc:", ["config=", "help"])
922         # TODO add  "log-socket-host=", "log-socket-port=", "log-file="
923 0         config_file = None
924
925 0         for o, a in opts:
926 0             if o in ("-h", "--help"):
927 0                 usage()
928 0                 sys.exit()
929 0             elif o in ("-c", "--config"):
930 0                 config_file = a
931             else:
932 0                 raise ValueError("Unhandled option")
933
934 0         if config_file:
935 0             if not path.isfile(config_file):
936 0                 print(
937                     "configuration file '{}' that not exist".format(config_file),
938                     file=sys.stderr,
939                 )
940 0                 exit(1)
941         else:
942 0             for config_file in (
943                 path.dirname(__file__) + "/ro.cfg",
944                 "./ro.cfg",
945                 "/etc/osm/ro.cfg",
946             ):
947 0                 if path.isfile(config_file):
948 0                     break
949             else:
950 0                 print(
951                     "No configuration file 'ro.cfg' found neither at local folder nor at /etc/osm/",
952                     file=sys.stderr,
953                 )
954 0                 exit(1)
955
956 0         ro_main(config_file)
957 0     except KeyboardInterrupt:
958 0         print("KeyboardInterrupt. Finishing", file=sys.stderr)
959 0     except getopt.GetoptError as e:
960 0         print(str(e), file=sys.stderr)
961         # usage()
962 0         exit(1)