Code Coverage

Cobertura Coverage Report > NG-RO.osm_ng_ro >

ro_main.py

Trend

File Coverage summary

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

Coverage Breakdown by Class

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