blob: eb5ff6c8d5e27a0a958c4d29f9e1e12e3e4c92ed [file] [log] [blame]
tierno1d213f42020-04-24 14:02:51 +00001#!/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
sousaedu049cbb12022-01-05 11:39:35 +000021
22from codecs import getreader
23import getopt
24from http import HTTPStatus
tierno1d213f42020-04-24 14:02:51 +000025import json
tierno1d213f42020-04-24 14:02:51 +000026import logging
27import logging.handlers
sousaedu049cbb12022-01-05 11:39:35 +000028from os import environ, path
tierno1d213f42020-04-24 14:02:51 +000029import sys
sousaedu049cbb12022-01-05 11:39:35 +000030import time
tierno1d213f42020-04-24 14:02:51 +000031
sousaedu049cbb12022-01-05 11:39:35 +000032import cherrypy
tierno1d213f42020-04-24 14:02:51 +000033from osm_common.dbbase import DbException
34from osm_common.fsbase import FsException
35from osm_common.msgbase import MsgException
tierno1d213f42020-04-24 14:02:51 +000036from osm_ng_ro import version as ro_version, version_date as ro_version_date
sousaedu049cbb12022-01-05 11:39:35 +000037import osm_ng_ro.html_out as html
38from osm_ng_ro.ns import Ns, NsException
39from osm_ng_ro.validation import ValidationError
40from osm_ng_ro.vim_admin import VimAdminThread
41import yaml
42
tierno1d213f42020-04-24 14:02:51 +000043
44__author__ = "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
sousaedu80135b92021-02-17 15:05:18 +010045__version__ = "0.1." # file version, not NBI version
tierno1d213f42020-04-24 14:02:51 +000046version_date = "May 2020"
47
sousaedu80135b92021-02-17 15:05:18 +010048database_version = "1.2"
49auth_database_version = "1.0"
50ro_server = None # instance of Server class
51vim_admin_thread = None # instance of VimAdminThread class
tierno70eeb182020-10-19 16:38:00 +000052
tierno1d213f42020-04-24 14:02:51 +000053# vim_threads = None # instance of VimThread class
54
55"""
56RO North Bound Interface
57URL: /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
65valid_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
73valid_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:",
sousaedu80135b92021-02-17 15:05:18 +010080 "<ID>": {"METHODS": ("DELETE",), "ROLE_PERMISSION": "tokens:id:"},
tierno1d213f42020-04-24 14:02:51 +000081 },
82 }
83 },
84 "ns": {
85 "v1": {
86 "deploy": {
87 "METHODS": ("GET",),
88 "ROLE_PERMISSION": "deploy:",
89 "<ID>": {
90 "METHODS": ("GET", "POST", "DELETE"),
91 "ROLE_PERMISSION": "deploy:id:",
92 "<ID>": {
93 "METHODS": ("GET",),
94 "ROLE_PERMISSION": "deploy:id:id:",
95 "cancel": {
96 "METHODS": ("POST",),
97 "ROLE_PERMISSION": "deploy:id:id:cancel",
sousaedu80135b92021-02-17 15:05:18 +010098 },
99 },
100 },
tierno1d213f42020-04-24 14:02:51 +0000101 },
102 }
103 },
104}
105
106
107class RoException(Exception):
tierno1d213f42020-04-24 14:02:51 +0000108 def __init__(self, message, http_code=HTTPStatus.METHOD_NOT_ALLOWED):
109 Exception.__init__(self, message)
110 self.http_code = http_code
111
112
113class AuthException(RoException):
114 pass
115
116
117class Authenticator:
tierno1d213f42020-04-24 14:02:51 +0000118 def __init__(self, valid_url_methods, valid_query_string):
119 self.valid_url_methods = valid_url_methods
120 self.valid_query_string = valid_query_string
121
122 def authorize(self, *args, **kwargs):
123 return {"token": "ok", "id": "ok"}
sousaedu80135b92021-02-17 15:05:18 +0100124
tierno1d213f42020-04-24 14:02:51 +0000125 def new_token(self, token_info, indata, remote):
sousaedu80135b92021-02-17 15:05:18 +0100126 return {"token": "ok", "id": "ok", "remote": remote}
tierno1d213f42020-04-24 14:02:51 +0000127
128 def del_token(self, token_id):
129 pass
130
131 def start(self, engine_config):
132 pass
133
134
135class Server(object):
136 instance = 0
137 # to decode bytes to str
138 reader = getreader("utf-8")
139
140 def __init__(self):
141 self.instance += 1
142 self.authenticator = Authenticator(valid_url_methods, valid_query_string)
143 self.ns = Ns()
144 self.map_operation = {
145 "token:post": self.new_token,
146 "token:id:delete": self.del_token,
147 "deploy:get": self.ns.get_deploy,
148 "deploy:id:get": self.ns.get_actions,
149 "deploy:id:post": self.ns.deploy,
150 "deploy:id:delete": self.ns.delete,
151 "deploy:id:id:get": self.ns.status,
152 "deploy:id:id:cancel:post": self.ns.cancel,
153 }
154
155 def _format_in(self, kwargs):
156 try:
157 indata = None
sousaedu80135b92021-02-17 15:05:18 +0100158
tierno1d213f42020-04-24 14:02:51 +0000159 if cherrypy.request.body.length:
160 error_text = "Invalid input format "
161
162 if "Content-Type" in cherrypy.request.headers:
163 if "application/json" in cherrypy.request.headers["Content-Type"]:
164 error_text = "Invalid json format "
165 indata = json.load(self.reader(cherrypy.request.body))
166 cherrypy.request.headers.pop("Content-File-MD5", None)
167 elif "application/yaml" in cherrypy.request.headers["Content-Type"]:
168 error_text = "Invalid yaml format "
sousaedu80135b92021-02-17 15:05:18 +0100169 indata = yaml.load(
170 cherrypy.request.body, Loader=yaml.SafeLoader
171 )
tierno1d213f42020-04-24 14:02:51 +0000172 cherrypy.request.headers.pop("Content-File-MD5", None)
sousaedu80135b92021-02-17 15:05:18 +0100173 elif (
174 "application/binary" in cherrypy.request.headers["Content-Type"]
175 or "application/gzip"
176 in cherrypy.request.headers["Content-Type"]
177 or "application/zip" in cherrypy.request.headers["Content-Type"]
178 or "text/plain" in cherrypy.request.headers["Content-Type"]
179 ):
tierno1d213f42020-04-24 14:02:51 +0000180 indata = cherrypy.request.body # .read()
sousaedu80135b92021-02-17 15:05:18 +0100181 elif (
182 "multipart/form-data"
183 in cherrypy.request.headers["Content-Type"]
184 ):
tierno1d213f42020-04-24 14:02:51 +0000185 if "descriptor_file" in kwargs:
186 filecontent = kwargs.pop("descriptor_file")
sousaedu80135b92021-02-17 15:05:18 +0100187
tierno1d213f42020-04-24 14:02:51 +0000188 if not filecontent.file:
sousaedu80135b92021-02-17 15:05:18 +0100189 raise RoException(
190 "empty file or content", HTTPStatus.BAD_REQUEST
191 )
192
tierno1d213f42020-04-24 14:02:51 +0000193 indata = filecontent.file # .read()
sousaedu80135b92021-02-17 15:05:18 +0100194
tierno1d213f42020-04-24 14:02:51 +0000195 if filecontent.content_type.value:
sousaedu80135b92021-02-17 15:05:18 +0100196 cherrypy.request.headers[
197 "Content-Type"
198 ] = filecontent.content_type.value
tierno1d213f42020-04-24 14:02:51 +0000199 else:
200 # raise cherrypy.HTTPError(HTTPStatus.Not_Acceptable,
201 # "Only 'Content-Type' of type 'application/json' or
202 # 'application/yaml' for input format are available")
203 error_text = "Invalid yaml format "
sousaedu80135b92021-02-17 15:05:18 +0100204 indata = yaml.load(
205 cherrypy.request.body, Loader=yaml.SafeLoader
206 )
tierno1d213f42020-04-24 14:02:51 +0000207 cherrypy.request.headers.pop("Content-File-MD5", None)
208 else:
209 error_text = "Invalid yaml format "
210 indata = yaml.load(cherrypy.request.body, Loader=yaml.SafeLoader)
211 cherrypy.request.headers.pop("Content-File-MD5", None)
sousaedu80135b92021-02-17 15:05:18 +0100212
tierno1d213f42020-04-24 14:02:51 +0000213 if not indata:
214 indata = {}
215
216 format_yaml = False
217 if cherrypy.request.headers.get("Query-String-Format") == "yaml":
218 format_yaml = True
219
220 for k, v in kwargs.items():
221 if isinstance(v, str):
222 if v == "":
223 kwargs[k] = None
224 elif format_yaml:
225 try:
226 kwargs[k] = yaml.load(v, Loader=yaml.SafeLoader)
227 except Exception:
228 pass
sousaedu80135b92021-02-17 15:05:18 +0100229 elif (
230 k.endswith(".gt")
231 or k.endswith(".lt")
232 or k.endswith(".gte")
233 or k.endswith(".lte")
234 ):
tierno1d213f42020-04-24 14:02:51 +0000235 try:
236 kwargs[k] = int(v)
237 except Exception:
238 try:
239 kwargs[k] = float(v)
240 except Exception:
241 pass
242 elif v.find(",") > 0:
243 kwargs[k] = v.split(",")
244 elif isinstance(v, (list, tuple)):
245 for index in range(0, len(v)):
246 if v[index] == "":
247 v[index] = None
248 elif format_yaml:
249 try:
250 v[index] = yaml.load(v[index], Loader=yaml.SafeLoader)
251 except Exception:
252 pass
253
254 return indata
255 except (ValueError, yaml.YAMLError) as exc:
256 raise RoException(error_text + str(exc), HTTPStatus.BAD_REQUEST)
257 except KeyError as exc:
258 raise RoException("Query string error: " + str(exc), HTTPStatus.BAD_REQUEST)
259 except Exception as exc:
260 raise RoException(error_text + str(exc), HTTPStatus.BAD_REQUEST)
261
262 @staticmethod
263 def _format_out(data, token_info=None, _format=None):
264 """
265 return string of dictionary data according to requested json, yaml, xml. By default json
266 :param data: response to be sent. Can be a dict, text or file
267 :param token_info: Contains among other username and project
268 :param _format: The format to be set as Content-Type if data is a file
269 :return: None
270 """
271 accept = cherrypy.request.headers.get("Accept")
sousaedu80135b92021-02-17 15:05:18 +0100272
tierno1d213f42020-04-24 14:02:51 +0000273 if data is None:
274 if accept and "text/html" in accept:
sousaedu80135b92021-02-17 15:05:18 +0100275 return html.format(
276 data, cherrypy.request, cherrypy.response, token_info
277 )
278
tierno1d213f42020-04-24 14:02:51 +0000279 # cherrypy.response.status = HTTPStatus.NO_CONTENT.value
280 return
281 elif hasattr(data, "read"): # file object
282 if _format:
283 cherrypy.response.headers["Content-Type"] = _format
284 elif "b" in data.mode: # binariy asssumig zip
sousaedu80135b92021-02-17 15:05:18 +0100285 cherrypy.response.headers["Content-Type"] = "application/zip"
tierno1d213f42020-04-24 14:02:51 +0000286 else:
sousaedu80135b92021-02-17 15:05:18 +0100287 cherrypy.response.headers["Content-Type"] = "text/plain"
288
tierno1d213f42020-04-24 14:02:51 +0000289 # TODO check that cherrypy close file. If not implement pending things to close per thread next
290 return data
sousaedu80135b92021-02-17 15:05:18 +0100291
tierno1d213f42020-04-24 14:02:51 +0000292 if accept:
293 if "application/json" in accept:
sousaedu80135b92021-02-17 15:05:18 +0100294 cherrypy.response.headers[
295 "Content-Type"
296 ] = "application/json; charset=utf-8"
tierno1d213f42020-04-24 14:02:51 +0000297 a = json.dumps(data, indent=4) + "\n"
sousaedu80135b92021-02-17 15:05:18 +0100298
tierno1d213f42020-04-24 14:02:51 +0000299 return a.encode("utf8")
300 elif "text/html" in accept:
sousaedu80135b92021-02-17 15:05:18 +0100301 return html.format(
302 data, cherrypy.request, cherrypy.response, token_info
303 )
304 elif (
305 "application/yaml" in accept
306 or "*/*" in accept
307 or "text/plain" in accept
308 ):
tierno1d213f42020-04-24 14:02:51 +0000309 pass
310 # if there is not any valid accept, raise an error. But if response is already an error, format in yaml
311 elif cherrypy.response.status >= 400:
sousaedu80135b92021-02-17 15:05:18 +0100312 raise cherrypy.HTTPError(
313 HTTPStatus.NOT_ACCEPTABLE.value,
314 "Only 'Accept' of type 'application/json' or 'application/yaml' "
315 "for output format are available",
316 )
317
318 cherrypy.response.headers["Content-Type"] = "application/yaml"
319
320 return yaml.safe_dump(
321 data,
322 explicit_start=True,
323 indent=4,
324 default_flow_style=False,
325 tags=False,
326 encoding="utf-8",
327 allow_unicode=True,
328 ) # , canonical=True, default_style='"'
tierno1d213f42020-04-24 14:02:51 +0000329
330 @cherrypy.expose
331 def index(self, *args, **kwargs):
332 token_info = None
sousaedu80135b92021-02-17 15:05:18 +0100333
tierno1d213f42020-04-24 14:02:51 +0000334 try:
335 if cherrypy.request.method == "GET":
336 token_info = self.authenticator.authorize()
sousaedu80135b92021-02-17 15:05:18 +0100337 outdata = token_info # Home page
tierno1d213f42020-04-24 14:02:51 +0000338 else:
sousaedu80135b92021-02-17 15:05:18 +0100339 raise cherrypy.HTTPError(
340 HTTPStatus.METHOD_NOT_ALLOWED.value,
341 "Method {} not allowed for tokens".format(cherrypy.request.method),
342 )
tierno1d213f42020-04-24 14:02:51 +0000343
344 return self._format_out(outdata, token_info)
tierno1d213f42020-04-24 14:02:51 +0000345 except (NsException, AuthException) as e:
346 # cherrypy.log("index Exception {}".format(e))
347 cherrypy.response.status = e.http_code.value
sousaedu80135b92021-02-17 15:05:18 +0100348
tierno1d213f42020-04-24 14:02:51 +0000349 return self._format_out("Welcome to OSM!", token_info)
350
351 @cherrypy.expose
352 def version(self, *args, **kwargs):
353 # TODO consider to remove and provide version using the static version file
354 try:
355 if cherrypy.request.method != "GET":
sousaedu80135b92021-02-17 15:05:18 +0100356 raise RoException(
357 "Only method GET is allowed",
358 HTTPStatus.METHOD_NOT_ALLOWED,
359 )
tierno1d213f42020-04-24 14:02:51 +0000360 elif args or kwargs:
sousaedu80135b92021-02-17 15:05:18 +0100361 raise RoException(
362 "Invalid URL or query string for version",
363 HTTPStatus.METHOD_NOT_ALLOWED,
364 )
365
tierno1d213f42020-04-24 14:02:51 +0000366 # TODO include version of other modules, pick up from some kafka admin message
367 osm_ng_ro_version = {"version": ro_version, "date": ro_version_date}
sousaedu80135b92021-02-17 15:05:18 +0100368
tierno1d213f42020-04-24 14:02:51 +0000369 return self._format_out(osm_ng_ro_version)
370 except RoException as e:
371 cherrypy.response.status = e.http_code.value
372 problem_details = {
373 "code": e.http_code.name,
374 "status": e.http_code.value,
375 "detail": str(e),
376 }
sousaedu80135b92021-02-17 15:05:18 +0100377
tierno1d213f42020-04-24 14:02:51 +0000378 return self._format_out(problem_details, None)
379
380 def new_token(self, engine_session, indata, *args, **kwargs):
381 token_info = None
382
383 try:
384 token_info = self.authenticator.authorize()
385 except Exception:
386 token_info = None
sousaedu80135b92021-02-17 15:05:18 +0100387
tierno1d213f42020-04-24 14:02:51 +0000388 if kwargs:
389 indata.update(kwargs)
sousaedu80135b92021-02-17 15:05:18 +0100390
tierno1d213f42020-04-24 14:02:51 +0000391 # This is needed to log the user when authentication fails
392 cherrypy.request.login = "{}".format(indata.get("username", "-"))
sousaedu80135b92021-02-17 15:05:18 +0100393 token_info = self.authenticator.new_token(
394 token_info, indata, cherrypy.request.remote
395 )
396 cherrypy.session["Authorization"] = token_info["id"]
tierno1d213f42020-04-24 14:02:51 +0000397 self._set_location_header("admin", "v1", "tokens", token_info["id"])
398 # for logging
399
400 # cherrypy.response.cookie["Authorization"] = outdata["id"]
401 # cherrypy.response.cookie["Authorization"]['expires'] = 3600
sousaedu80135b92021-02-17 15:05:18 +0100402
tierno1d213f42020-04-24 14:02:51 +0000403 return token_info, token_info["id"], True
404
405 def del_token(self, engine_session, indata, version, _id, *args, **kwargs):
406 token_id = _id
sousaedu80135b92021-02-17 15:05:18 +0100407
tierno1d213f42020-04-24 14:02:51 +0000408 if not token_id and "id" in kwargs:
409 token_id = kwargs["id"]
410 elif not token_id:
411 token_info = self.authenticator.authorize()
412 # for logging
413 token_id = token_info["id"]
sousaedu80135b92021-02-17 15:05:18 +0100414
tierno1d213f42020-04-24 14:02:51 +0000415 self.authenticator.del_token(token_id)
416 token_info = None
sousaedu80135b92021-02-17 15:05:18 +0100417 cherrypy.session["Authorization"] = "logout"
tierno1d213f42020-04-24 14:02:51 +0000418 # cherrypy.response.cookie["Authorization"] = token_id
419 # cherrypy.response.cookie["Authorization"]['expires'] = 0
sousaedu80135b92021-02-17 15:05:18 +0100420
tierno1d213f42020-04-24 14:02:51 +0000421 return None, None, True
sousaedu80135b92021-02-17 15:05:18 +0100422
tierno1d213f42020-04-24 14:02:51 +0000423 @cherrypy.expose
424 def test(self, *args, **kwargs):
sousaedu80135b92021-02-17 15:05:18 +0100425 if not cherrypy.config.get("server.enable_test") or (
426 isinstance(cherrypy.config["server.enable_test"], str)
427 and cherrypy.config["server.enable_test"].lower() == "false"
428 ):
tierno1d213f42020-04-24 14:02:51 +0000429 cherrypy.response.status = HTTPStatus.METHOD_NOT_ALLOWED.value
tierno1d213f42020-04-24 14:02:51 +0000430
sousaedu80135b92021-02-17 15:05:18 +0100431 return "test URL is disabled"
432
433 thread_info = None
434
435 if args and args[0] == "help":
436 return (
437 "<html><pre>\ninit\nfile/<name> download file\ndb-clear/table\nfs-clear[/folder]\nlogin\nlogin2\n"
438 "sleep/<time>\nmessage/topic\n</pre></html>"
439 )
tierno1d213f42020-04-24 14:02:51 +0000440 elif args and args[0] == "init":
441 try:
442 # self.ns.load_dbase(cherrypy.request.app.config)
443 self.ns.create_admin()
sousaedu80135b92021-02-17 15:05:18 +0100444
tierno1d213f42020-04-24 14:02:51 +0000445 return "Done. User 'admin', password 'admin' created"
446 except Exception:
447 cherrypy.response.status = HTTPStatus.FORBIDDEN.value
sousaedu80135b92021-02-17 15:05:18 +0100448
tierno1d213f42020-04-24 14:02:51 +0000449 return self._format_out("Database already initialized")
450 elif args and args[0] == "file":
sousaedu80135b92021-02-17 15:05:18 +0100451 return cherrypy.lib.static.serve_file(
452 cherrypy.tree.apps["/ro"].config["storage"]["path"] + "/" + args[1],
453 "text/plain",
454 "attachment",
455 )
tierno1d213f42020-04-24 14:02:51 +0000456 elif args and args[0] == "file2":
sousaedu80135b92021-02-17 15:05:18 +0100457 f_path = cherrypy.tree.apps["/ro"].config["storage"]["path"] + "/" + args[1]
tierno1d213f42020-04-24 14:02:51 +0000458 f = open(f_path, "r")
459 cherrypy.response.headers["Content-type"] = "text/plain"
460 return f
461
462 elif len(args) == 2 and args[0] == "db-clear":
463 deleted_info = self.ns.db.del_list(args[1], kwargs)
464 return "{} {} deleted\n".format(deleted_info["deleted"], args[1])
465 elif len(args) and args[0] == "fs-clear":
466 if len(args) >= 2:
467 folders = (args[1],)
468 else:
469 folders = self.ns.fs.dir_ls(".")
sousaedu80135b92021-02-17 15:05:18 +0100470
tierno1d213f42020-04-24 14:02:51 +0000471 for folder in folders:
472 self.ns.fs.file_delete(folder)
sousaedu80135b92021-02-17 15:05:18 +0100473
tierno1d213f42020-04-24 14:02:51 +0000474 return ",".join(folders) + " folders deleted\n"
475 elif args and args[0] == "login":
476 if not cherrypy.request.headers.get("Authorization"):
sousaedu80135b92021-02-17 15:05:18 +0100477 cherrypy.response.headers[
478 "WWW-Authenticate"
479 ] = 'Basic realm="Access to OSM site", charset="UTF-8"'
tierno1d213f42020-04-24 14:02:51 +0000480 cherrypy.response.status = HTTPStatus.UNAUTHORIZED.value
481 elif args and args[0] == "login2":
482 if not cherrypy.request.headers.get("Authorization"):
sousaedu80135b92021-02-17 15:05:18 +0100483 cherrypy.response.headers[
484 "WWW-Authenticate"
485 ] = 'Bearer realm="Access to OSM site"'
tierno1d213f42020-04-24 14:02:51 +0000486 cherrypy.response.status = HTTPStatus.UNAUTHORIZED.value
487 elif args and args[0] == "sleep":
488 sleep_time = 5
sousaedu80135b92021-02-17 15:05:18 +0100489
tierno1d213f42020-04-24 14:02:51 +0000490 try:
491 sleep_time = int(args[1])
492 except Exception:
493 cherrypy.response.status = HTTPStatus.FORBIDDEN.value
494 return self._format_out("Database already initialized")
sousaedu80135b92021-02-17 15:05:18 +0100495
tierno1d213f42020-04-24 14:02:51 +0000496 thread_info = cherrypy.thread_data
497 print(thread_info)
498 time.sleep(sleep_time)
499 # thread_info
500 elif len(args) >= 2 and args[0] == "message":
501 main_topic = args[1]
502 return_text = "<html><pre>{} ->\n".format(main_topic)
sousaedu80135b92021-02-17 15:05:18 +0100503
tierno1d213f42020-04-24 14:02:51 +0000504 try:
sousaedu80135b92021-02-17 15:05:18 +0100505 if cherrypy.request.method == "POST":
tierno1d213f42020-04-24 14:02:51 +0000506 to_send = yaml.load(cherrypy.request.body, Loader=yaml.SafeLoader)
507 for k, v in to_send.items():
508 self.ns.msg.write(main_topic, k, v)
509 return_text += " {}: {}\n".format(k, v)
sousaedu80135b92021-02-17 15:05:18 +0100510 elif cherrypy.request.method == "GET":
tierno1d213f42020-04-24 14:02:51 +0000511 for k, v in kwargs.items():
sousaedu80135b92021-02-17 15:05:18 +0100512 self.ns.msg.write(
513 main_topic, k, yaml.load(v, Loader=yaml.SafeLoader)
514 )
515 return_text += " {}: {}\n".format(
516 k, yaml.load(v, Loader=yaml.SafeLoader)
517 )
tierno1d213f42020-04-24 14:02:51 +0000518 except Exception as e:
519 return_text += "Error: " + str(e)
sousaedu80135b92021-02-17 15:05:18 +0100520
tierno1d213f42020-04-24 14:02:51 +0000521 return_text += "</pre></html>\n"
sousaedu80135b92021-02-17 15:05:18 +0100522
tierno1d213f42020-04-24 14:02:51 +0000523 return return_text
524
525 return_text = (
sousaedu80135b92021-02-17 15:05:18 +0100526 "<html><pre>\nheaders:\n args: {}\n".format(args)
527 + " kwargs: {}\n".format(kwargs)
528 + " headers: {}\n".format(cherrypy.request.headers)
529 + " path_info: {}\n".format(cherrypy.request.path_info)
530 + " query_string: {}\n".format(cherrypy.request.query_string)
531 + " session: {}\n".format(cherrypy.session)
532 + " cookie: {}\n".format(cherrypy.request.cookie)
533 + " method: {}\n".format(cherrypy.request.method)
534 + " session: {}\n".format(cherrypy.session.get("fieldname"))
535 + " body:\n"
536 )
tierno1d213f42020-04-24 14:02:51 +0000537 return_text += " length: {}\n".format(cherrypy.request.body.length)
sousaedu80135b92021-02-17 15:05:18 +0100538
tierno1d213f42020-04-24 14:02:51 +0000539 if cherrypy.request.body.length:
540 return_text += " content: {}\n".format(
sousaedu80135b92021-02-17 15:05:18 +0100541 str(
542 cherrypy.request.body.read(
543 int(cherrypy.request.headers.get("Content-Length", 0))
544 )
545 )
546 )
547
tierno1d213f42020-04-24 14:02:51 +0000548 if thread_info:
549 return_text += "thread: {}\n".format(thread_info)
sousaedu80135b92021-02-17 15:05:18 +0100550
tierno1d213f42020-04-24 14:02:51 +0000551 return_text += "</pre></html>"
sousaedu80135b92021-02-17 15:05:18 +0100552
tierno1d213f42020-04-24 14:02:51 +0000553 return return_text
554
555 @staticmethod
556 def _check_valid_url_method(method, *args):
557 if len(args) < 3:
sousaedu80135b92021-02-17 15:05:18 +0100558 raise RoException(
559 "URL must contain at least 'main_topic/version/topic'",
560 HTTPStatus.METHOD_NOT_ALLOWED,
561 )
tierno1d213f42020-04-24 14:02:51 +0000562
563 reference = valid_url_methods
564 for arg in args:
565 if arg is None:
566 break
sousaedu80135b92021-02-17 15:05:18 +0100567
tierno1d213f42020-04-24 14:02:51 +0000568 if not isinstance(reference, dict):
sousaedu80135b92021-02-17 15:05:18 +0100569 raise RoException(
570 "URL contains unexpected extra items '{}'".format(arg),
571 HTTPStatus.METHOD_NOT_ALLOWED,
572 )
tierno1d213f42020-04-24 14:02:51 +0000573
574 if arg in reference:
575 reference = reference[arg]
576 elif "<ID>" in reference:
577 reference = reference["<ID>"]
578 elif "*" in reference:
579 # reference = reference["*"]
580 break
581 else:
sousaedu80135b92021-02-17 15:05:18 +0100582 raise RoException(
583 "Unexpected URL item {}".format(arg),
584 HTTPStatus.METHOD_NOT_ALLOWED,
585 )
586
tierno1d213f42020-04-24 14:02:51 +0000587 if "TODO" in reference and method in reference["TODO"]:
sousaedu80135b92021-02-17 15:05:18 +0100588 raise RoException(
589 "Method {} not supported yet for this URL".format(method),
590 HTTPStatus.NOT_IMPLEMENTED,
591 )
tierno1d213f42020-04-24 14:02:51 +0000592 elif "METHODS" not in reference or method not in reference["METHODS"]:
sousaedu80135b92021-02-17 15:05:18 +0100593 raise RoException(
594 "Method {} not supported for this URL".format(method),
595 HTTPStatus.METHOD_NOT_ALLOWED,
596 )
597
tierno1d213f42020-04-24 14:02:51 +0000598 return reference["ROLE_PERMISSION"] + method.lower()
599
600 @staticmethod
601 def _set_location_header(main_topic, version, topic, id):
602 """
603 Insert response header Location with the URL of created item base on URL params
604 :param main_topic:
605 :param version:
606 :param topic:
607 :param id:
608 :return: None
609 """
610 # Use cherrypy.request.base for absoluted path and make use of request.header HOST just in case behind aNAT
sousaedu80135b92021-02-17 15:05:18 +0100611 cherrypy.response.headers["Location"] = "/ro/{}/{}/{}/{}".format(
612 main_topic, version, topic, id
613 )
614
tierno1d213f42020-04-24 14:02:51 +0000615 return
616
617 @cherrypy.expose
sousaedu80135b92021-02-17 15:05:18 +0100618 def default(
619 self,
620 main_topic=None,
621 version=None,
622 topic=None,
623 _id=None,
624 _id2=None,
625 *args,
626 **kwargs,
627 ):
tierno1d213f42020-04-24 14:02:51 +0000628 token_info = None
629 outdata = None
630 _format = None
631 method = "DONE"
632 rollback = []
633 engine_session = None
sousaedu80135b92021-02-17 15:05:18 +0100634
tierno1d213f42020-04-24 14:02:51 +0000635 try:
636 if not main_topic or not version or not topic:
sousaedu80135b92021-02-17 15:05:18 +0100637 raise RoException(
638 "URL must contain at least 'main_topic/version/topic'",
639 HTTPStatus.METHOD_NOT_ALLOWED,
640 )
tierno1d213f42020-04-24 14:02:51 +0000641
sousaedu80135b92021-02-17 15:05:18 +0100642 if main_topic not in (
643 "admin",
644 "ns",
645 ):
646 raise RoException(
647 "URL main_topic '{}' not supported".format(main_topic),
648 HTTPStatus.METHOD_NOT_ALLOWED,
649 )
650
651 if version != "v1":
652 raise RoException(
653 "URL version '{}' not supported".format(version),
654 HTTPStatus.METHOD_NOT_ALLOWED,
655 )
656
657 if (
658 kwargs
659 and "METHOD" in kwargs
660 and kwargs["METHOD"] in ("PUT", "POST", "DELETE", "GET", "PATCH")
661 ):
tierno1d213f42020-04-24 14:02:51 +0000662 method = kwargs.pop("METHOD")
663 else:
664 method = cherrypy.request.method
665
sousaedu80135b92021-02-17 15:05:18 +0100666 role_permission = self._check_valid_url_method(
667 method, main_topic, version, topic, _id, _id2, *args, **kwargs
668 )
tierno1d213f42020-04-24 14:02:51 +0000669 # skip token validation if requesting a token
670 indata = self._format_in(kwargs)
sousaedu80135b92021-02-17 15:05:18 +0100671
tierno1d213f42020-04-24 14:02:51 +0000672 if main_topic != "admin" or topic != "tokens":
673 token_info = self.authenticator.authorize(role_permission, _id)
sousaedu80135b92021-02-17 15:05:18 +0100674
tierno1d213f42020-04-24 14:02:51 +0000675 outdata, created_id, done = self.map_operation[role_permission](
sousaedu80135b92021-02-17 15:05:18 +0100676 engine_session, indata, version, _id, _id2, *args, *kwargs
677 )
678
tierno1d213f42020-04-24 14:02:51 +0000679 if created_id:
680 self._set_location_header(main_topic, version, topic, _id)
sousaedu80135b92021-02-17 15:05:18 +0100681
682 cherrypy.response.status = (
683 HTTPStatus.ACCEPTED.value
684 if not done
685 else HTTPStatus.OK.value
686 if outdata is not None
687 else HTTPStatus.NO_CONTENT.value
688 )
689
tierno1d213f42020-04-24 14:02:51 +0000690 return self._format_out(outdata, token_info, _format)
691 except Exception as e:
sousaedu80135b92021-02-17 15:05:18 +0100692 if isinstance(
693 e,
694 (
695 RoException,
696 NsException,
697 DbException,
698 FsException,
699 MsgException,
700 AuthException,
701 ValidationError,
702 ),
703 ):
tierno1d213f42020-04-24 14:02:51 +0000704 http_code_value = cherrypy.response.status = e.http_code.value
705 http_code_name = e.http_code.name
706 cherrypy.log("Exception {}".format(e))
707 else:
sousaedu80135b92021-02-17 15:05:18 +0100708 http_code_value = (
709 cherrypy.response.status
710 ) = HTTPStatus.BAD_REQUEST.value # INTERNAL_SERVER_ERROR
tierno1d213f42020-04-24 14:02:51 +0000711 cherrypy.log("CRITICAL: Exception {}".format(e), traceback=True)
712 http_code_name = HTTPStatus.BAD_REQUEST.name
sousaedu80135b92021-02-17 15:05:18 +0100713
tierno1d213f42020-04-24 14:02:51 +0000714 if hasattr(outdata, "close"): # is an open file
715 outdata.close()
sousaedu80135b92021-02-17 15:05:18 +0100716
tierno1d213f42020-04-24 14:02:51 +0000717 error_text = str(e)
718 rollback.reverse()
sousaedu80135b92021-02-17 15:05:18 +0100719
tierno1d213f42020-04-24 14:02:51 +0000720 for rollback_item in rollback:
721 try:
722 if rollback_item.get("operation") == "set":
sousaedu80135b92021-02-17 15:05:18 +0100723 self.ns.db.set_one(
724 rollback_item["topic"],
725 {"_id": rollback_item["_id"]},
726 rollback_item["content"],
727 fail_on_empty=False,
728 )
tierno1d213f42020-04-24 14:02:51 +0000729 else:
sousaedu80135b92021-02-17 15:05:18 +0100730 self.ns.db.del_one(
731 rollback_item["topic"],
732 {"_id": rollback_item["_id"]},
733 fail_on_empty=False,
734 )
tierno1d213f42020-04-24 14:02:51 +0000735 except Exception as e2:
sousaedu80135b92021-02-17 15:05:18 +0100736 rollback_error_text = "Rollback Exception {}: {}".format(
737 rollback_item, e2
738 )
tierno1d213f42020-04-24 14:02:51 +0000739 cherrypy.log(rollback_error_text)
740 error_text += ". " + rollback_error_text
sousaedu80135b92021-02-17 15:05:18 +0100741
tierno1d213f42020-04-24 14:02:51 +0000742 # if isinstance(e, MsgException):
743 # error_text = "{} has been '{}' but other modules cannot be informed because an error on bus".format(
744 # engine_topic[:-1], method, error_text)
745 problem_details = {
746 "code": http_code_name,
747 "status": http_code_value,
748 "detail": error_text,
749 }
sousaedu80135b92021-02-17 15:05:18 +0100750
tierno1d213f42020-04-24 14:02:51 +0000751 return self._format_out(problem_details, token_info)
752 # raise cherrypy.HTTPError(e.http_code.value, str(e))
753 finally:
754 if token_info:
755 if method in ("PUT", "PATCH", "POST") and isinstance(outdata, dict):
756 for logging_id in ("id", "op_id", "nsilcmop_id", "nslcmop_id"):
757 if outdata.get(logging_id):
sousaedu80135b92021-02-17 15:05:18 +0100758 cherrypy.request.login += ";{}={}".format(
759 logging_id, outdata[logging_id][:36]
760 )
tierno1d213f42020-04-24 14:02:51 +0000761
762
763def _start_service():
764 """
765 Callback function called when cherrypy.engine starts
766 Override configuration with env variables
767 Set database, storage, message configuration
768 Init database with admin/admin user password
769 """
tierno70eeb182020-10-19 16:38:00 +0000770 global ro_server, vim_admin_thread
tierno1d213f42020-04-24 14:02:51 +0000771 # global vim_threads
772 cherrypy.log.error("Starting osm_ng_ro")
773 # update general cherrypy configuration
774 update_dict = {}
sousaedu80135b92021-02-17 15:05:18 +0100775 engine_config = cherrypy.tree.apps["/ro"].config
tierno1d213f42020-04-24 14:02:51 +0000776
tierno1d213f42020-04-24 14:02:51 +0000777 for k, v in environ.items():
778 if not k.startswith("OSMRO_"):
779 continue
sousaedu80135b92021-02-17 15:05:18 +0100780
tierno1d213f42020-04-24 14:02:51 +0000781 k1, _, k2 = k[6:].lower().partition("_")
sousaedu80135b92021-02-17 15:05:18 +0100782
tierno1d213f42020-04-24 14:02:51 +0000783 if not k2:
784 continue
sousaedu80135b92021-02-17 15:05:18 +0100785
tierno1d213f42020-04-24 14:02:51 +0000786 try:
787 if k1 in ("server", "test", "auth", "log"):
788 # update [global] configuration
sousaedu80135b92021-02-17 15:05:18 +0100789 update_dict[k1 + "." + k2] = yaml.safe_load(v)
tierno1d213f42020-04-24 14:02:51 +0000790 elif k1 == "static":
791 # update [/static] configuration
792 engine_config["/static"]["tools.staticdir." + k2] = yaml.safe_load(v)
793 elif k1 == "tools":
794 # update [/] configuration
sousaedu80135b92021-02-17 15:05:18 +0100795 engine_config["/"]["tools." + k2.replace("_", ".")] = yaml.safe_load(v)
tierno1d213f42020-04-24 14:02:51 +0000796 elif k1 in ("message", "database", "storage", "authentication"):
tierno70eeb182020-10-19 16:38:00 +0000797 engine_config[k1][k2] = yaml.safe_load(v)
tierno1d213f42020-04-24 14:02:51 +0000798
799 except Exception as e:
800 raise RoException("Cannot load env '{}': {}".format(k, e))
801
802 if update_dict:
803 cherrypy.config.update(update_dict)
804 engine_config["global"].update(update_dict)
805
806 # logging cherrypy
sousaedu80135b92021-02-17 15:05:18 +0100807 log_format_simple = (
808 "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(message)s"
809 )
810 log_formatter_simple = logging.Formatter(
811 log_format_simple, datefmt="%Y-%m-%dT%H:%M:%S"
812 )
tierno1d213f42020-04-24 14:02:51 +0000813 logger_server = logging.getLogger("cherrypy.error")
814 logger_access = logging.getLogger("cherrypy.access")
815 logger_cherry = logging.getLogger("cherrypy")
sousaedue493e9b2021-02-09 15:30:01 +0100816 logger = logging.getLogger("ro")
tierno1d213f42020-04-24 14:02:51 +0000817
818 if "log.file" in engine_config["global"]:
sousaedu80135b92021-02-17 15:05:18 +0100819 file_handler = logging.handlers.RotatingFileHandler(
820 engine_config["global"]["log.file"], maxBytes=100e6, backupCount=9, delay=0
821 )
tierno1d213f42020-04-24 14:02:51 +0000822 file_handler.setFormatter(log_formatter_simple)
823 logger_cherry.addHandler(file_handler)
sousaedue493e9b2021-02-09 15:30:01 +0100824 logger.addHandler(file_handler)
sousaedu80135b92021-02-17 15:05:18 +0100825
tierno1d213f42020-04-24 14:02:51 +0000826 # log always to standard output
sousaedu80135b92021-02-17 15:05:18 +0100827 for format_, logger in {
828 "ro.server %(filename)s:%(lineno)s": logger_server,
829 "ro.access %(filename)s:%(lineno)s": logger_access,
830 "%(name)s %(filename)s:%(lineno)s": logger,
831 }.items():
tierno1d213f42020-04-24 14:02:51 +0000832 log_format_cherry = "%(asctime)s %(levelname)s {} %(message)s".format(format_)
sousaedu80135b92021-02-17 15:05:18 +0100833 log_formatter_cherry = logging.Formatter(
834 log_format_cherry, datefmt="%Y-%m-%dT%H:%M:%S"
835 )
tierno1d213f42020-04-24 14:02:51 +0000836 str_handler = logging.StreamHandler()
837 str_handler.setFormatter(log_formatter_cherry)
838 logger.addHandler(str_handler)
839
840 if engine_config["global"].get("log.level"):
841 logger_cherry.setLevel(engine_config["global"]["log.level"])
sousaedue493e9b2021-02-09 15:30:01 +0100842 logger.setLevel(engine_config["global"]["log.level"])
sousaedu80135b92021-02-17 15:05:18 +0100843
tierno1d213f42020-04-24 14:02:51 +0000844 # logging other modules
sousaedu80135b92021-02-17 15:05:18 +0100845 for k1, logname in {
846 "message": "ro.msg",
847 "database": "ro.db",
848 "storage": "ro.fs",
849 }.items():
tierno1d213f42020-04-24 14:02:51 +0000850 engine_config[k1]["logger_name"] = logname
851 logger_module = logging.getLogger(logname)
sousaedu80135b92021-02-17 15:05:18 +0100852
tierno1d213f42020-04-24 14:02:51 +0000853 if "logfile" in engine_config[k1]:
sousaedu80135b92021-02-17 15:05:18 +0100854 file_handler = logging.handlers.RotatingFileHandler(
855 engine_config[k1]["logfile"], maxBytes=100e6, backupCount=9, delay=0
856 )
tierno1d213f42020-04-24 14:02:51 +0000857 file_handler.setFormatter(log_formatter_simple)
858 logger_module.addHandler(file_handler)
sousaedu80135b92021-02-17 15:05:18 +0100859
tierno1d213f42020-04-24 14:02:51 +0000860 if "loglevel" in engine_config[k1]:
861 logger_module.setLevel(engine_config[k1]["loglevel"])
862 # TODO add more entries, e.g.: storage
863
864 engine_config["assignment"] = {}
865 # ^ each VIM, SDNc will be assigned one worker id. Ns class will add items and VimThread will auto-assign
sousaedu80135b92021-02-17 15:05:18 +0100866 cherrypy.tree.apps["/ro"].root.ns.start(engine_config)
867 cherrypy.tree.apps["/ro"].root.authenticator.start(engine_config)
868 cherrypy.tree.apps["/ro"].root.ns.init_db(target_version=database_version)
tierno1d213f42020-04-24 14:02:51 +0000869
870 # # start subscriptions thread:
tierno70eeb182020-10-19 16:38:00 +0000871 vim_admin_thread = VimAdminThread(config=engine_config, engine=ro_server.ns)
872 vim_admin_thread.start()
tierno1d213f42020-04-24 14:02:51 +0000873 # # Do not capture except SubscriptionException
874
tierno70eeb182020-10-19 16:38:00 +0000875 # backend = engine_config["authentication"]["backend"]
876 # cherrypy.log.error("Starting OSM NBI Version '{} {}' with '{}' authentication backend"
877 # .format(ro_version, ro_version_date, backend))
tierno1d213f42020-04-24 14:02:51 +0000878
879
880def _stop_service():
881 """
882 Callback function called when cherrypy.engine stops
883 TODO: Ending database connections.
884 """
tierno70eeb182020-10-19 16:38:00 +0000885 global vim_admin_thread
sousaedu80135b92021-02-17 15:05:18 +0100886
tierno70eeb182020-10-19 16:38:00 +0000887 # terminate vim_admin_thread
888 if vim_admin_thread:
889 vim_admin_thread.terminate()
sousaedu80135b92021-02-17 15:05:18 +0100890
tierno70eeb182020-10-19 16:38:00 +0000891 vim_admin_thread = None
sousaedu80135b92021-02-17 15:05:18 +0100892 cherrypy.tree.apps["/ro"].root.ns.stop()
tierno1d213f42020-04-24 14:02:51 +0000893 cherrypy.log.error("Stopping osm_ng_ro")
894
895
896def ro_main(config_file):
897 global ro_server
sousaedu80135b92021-02-17 15:05:18 +0100898
tierno1d213f42020-04-24 14:02:51 +0000899 ro_server = Server()
sousaedu80135b92021-02-17 15:05:18 +0100900 cherrypy.engine.subscribe("start", _start_service)
901 cherrypy.engine.subscribe("stop", _stop_service)
902 cherrypy.quickstart(ro_server, "/ro", config_file)
tierno1d213f42020-04-24 14:02:51 +0000903
904
905def usage():
sousaedu80135b92021-02-17 15:05:18 +0100906 print(
907 """Usage: {} [options]
tierno1d213f42020-04-24 14:02:51 +0000908 -c|--config [configuration_file]: loads the configuration file (default: ./ro.cfg)
909 -h|--help: shows this help
sousaedu80135b92021-02-17 15:05:18 +0100910 """.format(
911 sys.argv[0]
912 )
913 )
tierno1d213f42020-04-24 14:02:51 +0000914 # --log-socket-host HOST: send logs to this host")
915 # --log-socket-port PORT: send logs using this port (default: 9022)")
916
917
sousaedu80135b92021-02-17 15:05:18 +0100918if __name__ == "__main__":
tierno1d213f42020-04-24 14:02:51 +0000919 try:
920 # load parameters and configuration
921 opts, args = getopt.getopt(sys.argv[1:], "hvc:", ["config=", "help"])
922 # TODO add "log-socket-host=", "log-socket-port=", "log-file="
923 config_file = None
sousaedu80135b92021-02-17 15:05:18 +0100924
tierno1d213f42020-04-24 14:02:51 +0000925 for o, a in opts:
926 if o in ("-h", "--help"):
927 usage()
928 sys.exit()
929 elif o in ("-c", "--config"):
930 config_file = a
931 else:
932 assert False, "Unhandled option"
sousaedu80135b92021-02-17 15:05:18 +0100933
tierno1d213f42020-04-24 14:02:51 +0000934 if config_file:
935 if not path.isfile(config_file):
sousaedu80135b92021-02-17 15:05:18 +0100936 print(
937 "configuration file '{}' that not exist".format(config_file),
938 file=sys.stderr,
939 )
tierno1d213f42020-04-24 14:02:51 +0000940 exit(1)
941 else:
sousaedu80135b92021-02-17 15:05:18 +0100942 for config_file in (
943 path.dirname(__file__) + "/ro.cfg",
944 "./ro.cfg",
945 "/etc/osm/ro.cfg",
946 ):
tierno1d213f42020-04-24 14:02:51 +0000947 if path.isfile(config_file):
948 break
949 else:
sousaedu80135b92021-02-17 15:05:18 +0100950 print(
951 "No configuration file 'ro.cfg' found neither at local folder nor at /etc/osm/",
952 file=sys.stderr,
953 )
tierno1d213f42020-04-24 14:02:51 +0000954 exit(1)
sousaedu80135b92021-02-17 15:05:18 +0100955
tierno1d213f42020-04-24 14:02:51 +0000956 ro_main(config_file)
tierno70eeb182020-10-19 16:38:00 +0000957 except KeyboardInterrupt:
958 print("KeyboardInterrupt. Finishing", file=sys.stderr)
tierno1d213f42020-04-24 14:02:51 +0000959 except getopt.GetoptError as e:
960 print(str(e), file=sys.stderr)
961 # usage()
962 exit(1)