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/454
100%
0/0

Coverage Breakdown by Class

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