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