blob: 2a2c076a1747db747170d7730a482690e6d78650 [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 },
palaciosj8f2060b2022-02-24 12:05:59 +0000102 "recreate": {
103 "<ID>": {
104 "METHODS": ("POST"),
105 "ROLE_PERMISSION": "recreate:id:",
106 "<ID>": {
107 "METHODS": ("GET",),
108 "ROLE_PERMISSION": "recreate:id:id:",
109 },
110 },
111 },
tierno1d213f42020-04-24 14:02:51 +0000112 }
113 },
114}
115
116
117class RoException(Exception):
tierno1d213f42020-04-24 14:02:51 +0000118 def __init__(self, message, http_code=HTTPStatus.METHOD_NOT_ALLOWED):
119 Exception.__init__(self, message)
120 self.http_code = http_code
121
122
123class AuthException(RoException):
124 pass
125
126
127class Authenticator:
tierno1d213f42020-04-24 14:02:51 +0000128 def __init__(self, valid_url_methods, valid_query_string):
129 self.valid_url_methods = valid_url_methods
130 self.valid_query_string = valid_query_string
131
132 def authorize(self, *args, **kwargs):
133 return {"token": "ok", "id": "ok"}
sousaedu80135b92021-02-17 15:05:18 +0100134
tierno1d213f42020-04-24 14:02:51 +0000135 def new_token(self, token_info, indata, remote):
sousaedu80135b92021-02-17 15:05:18 +0100136 return {"token": "ok", "id": "ok", "remote": remote}
tierno1d213f42020-04-24 14:02:51 +0000137
138 def del_token(self, token_id):
139 pass
140
141 def start(self, engine_config):
142 pass
143
144
145class Server(object):
146 instance = 0
147 # to decode bytes to str
148 reader = getreader("utf-8")
149
150 def __init__(self):
151 self.instance += 1
152 self.authenticator = Authenticator(valid_url_methods, valid_query_string)
153 self.ns = Ns()
154 self.map_operation = {
155 "token:post": self.new_token,
156 "token:id:delete": self.del_token,
157 "deploy:get": self.ns.get_deploy,
158 "deploy:id:get": self.ns.get_actions,
159 "deploy:id:post": self.ns.deploy,
160 "deploy:id:delete": self.ns.delete,
161 "deploy:id:id:get": self.ns.status,
162 "deploy:id:id:cancel:post": self.ns.cancel,
palaciosj8f2060b2022-02-24 12:05:59 +0000163 "recreate:id:post": self.ns.recreate,
164 "recreate:id:id:get": self.ns.recreate_status,
tierno1d213f42020-04-24 14:02:51 +0000165 }
166
167 def _format_in(self, kwargs):
168 try:
169 indata = None
sousaedu80135b92021-02-17 15:05:18 +0100170
tierno1d213f42020-04-24 14:02:51 +0000171 if cherrypy.request.body.length:
172 error_text = "Invalid input format "
173
174 if "Content-Type" in cherrypy.request.headers:
175 if "application/json" in cherrypy.request.headers["Content-Type"]:
176 error_text = "Invalid json format "
177 indata = json.load(self.reader(cherrypy.request.body))
178 cherrypy.request.headers.pop("Content-File-MD5", None)
179 elif "application/yaml" in cherrypy.request.headers["Content-Type"]:
180 error_text = "Invalid yaml format "
sousaedu80135b92021-02-17 15:05:18 +0100181 indata = yaml.load(
182 cherrypy.request.body, Loader=yaml.SafeLoader
183 )
tierno1d213f42020-04-24 14:02:51 +0000184 cherrypy.request.headers.pop("Content-File-MD5", None)
sousaedu80135b92021-02-17 15:05:18 +0100185 elif (
186 "application/binary" in cherrypy.request.headers["Content-Type"]
187 or "application/gzip"
188 in cherrypy.request.headers["Content-Type"]
189 or "application/zip" in cherrypy.request.headers["Content-Type"]
190 or "text/plain" in cherrypy.request.headers["Content-Type"]
191 ):
tierno1d213f42020-04-24 14:02:51 +0000192 indata = cherrypy.request.body # .read()
sousaedu80135b92021-02-17 15:05:18 +0100193 elif (
194 "multipart/form-data"
195 in cherrypy.request.headers["Content-Type"]
196 ):
tierno1d213f42020-04-24 14:02:51 +0000197 if "descriptor_file" in kwargs:
198 filecontent = kwargs.pop("descriptor_file")
sousaedu80135b92021-02-17 15:05:18 +0100199
tierno1d213f42020-04-24 14:02:51 +0000200 if not filecontent.file:
sousaedu80135b92021-02-17 15:05:18 +0100201 raise RoException(
202 "empty file or content", HTTPStatus.BAD_REQUEST
203 )
204
tierno1d213f42020-04-24 14:02:51 +0000205 indata = filecontent.file # .read()
sousaedu80135b92021-02-17 15:05:18 +0100206
tierno1d213f42020-04-24 14:02:51 +0000207 if filecontent.content_type.value:
sousaedu80135b92021-02-17 15:05:18 +0100208 cherrypy.request.headers[
209 "Content-Type"
210 ] = filecontent.content_type.value
tierno1d213f42020-04-24 14:02:51 +0000211 else:
212 # raise cherrypy.HTTPError(HTTPStatus.Not_Acceptable,
213 # "Only 'Content-Type' of type 'application/json' or
214 # 'application/yaml' for input format are available")
215 error_text = "Invalid yaml format "
sousaedu80135b92021-02-17 15:05:18 +0100216 indata = yaml.load(
217 cherrypy.request.body, Loader=yaml.SafeLoader
218 )
tierno1d213f42020-04-24 14:02:51 +0000219 cherrypy.request.headers.pop("Content-File-MD5", None)
220 else:
221 error_text = "Invalid yaml format "
222 indata = yaml.load(cherrypy.request.body, Loader=yaml.SafeLoader)
223 cherrypy.request.headers.pop("Content-File-MD5", None)
sousaedu80135b92021-02-17 15:05:18 +0100224
tierno1d213f42020-04-24 14:02:51 +0000225 if not indata:
226 indata = {}
227
228 format_yaml = False
229 if cherrypy.request.headers.get("Query-String-Format") == "yaml":
230 format_yaml = True
231
232 for k, v in kwargs.items():
233 if isinstance(v, str):
234 if v == "":
235 kwargs[k] = None
236 elif format_yaml:
237 try:
238 kwargs[k] = yaml.load(v, Loader=yaml.SafeLoader)
239 except Exception:
240 pass
sousaedu80135b92021-02-17 15:05:18 +0100241 elif (
242 k.endswith(".gt")
243 or k.endswith(".lt")
244 or k.endswith(".gte")
245 or k.endswith(".lte")
246 ):
tierno1d213f42020-04-24 14:02:51 +0000247 try:
248 kwargs[k] = int(v)
249 except Exception:
250 try:
251 kwargs[k] = float(v)
252 except Exception:
253 pass
254 elif v.find(",") > 0:
255 kwargs[k] = v.split(",")
256 elif isinstance(v, (list, tuple)):
257 for index in range(0, len(v)):
258 if v[index] == "":
259 v[index] = None
260 elif format_yaml:
261 try:
262 v[index] = yaml.load(v[index], Loader=yaml.SafeLoader)
263 except Exception:
264 pass
265
266 return indata
267 except (ValueError, yaml.YAMLError) as exc:
268 raise RoException(error_text + str(exc), HTTPStatus.BAD_REQUEST)
269 except KeyError as exc:
270 raise RoException("Query string error: " + str(exc), HTTPStatus.BAD_REQUEST)
271 except Exception as exc:
272 raise RoException(error_text + str(exc), HTTPStatus.BAD_REQUEST)
273
274 @staticmethod
275 def _format_out(data, token_info=None, _format=None):
276 """
277 return string of dictionary data according to requested json, yaml, xml. By default json
278 :param data: response to be sent. Can be a dict, text or file
279 :param token_info: Contains among other username and project
280 :param _format: The format to be set as Content-Type if data is a file
281 :return: None
282 """
283 accept = cherrypy.request.headers.get("Accept")
sousaedu80135b92021-02-17 15:05:18 +0100284
tierno1d213f42020-04-24 14:02:51 +0000285 if data is None:
286 if accept and "text/html" in accept:
sousaedu80135b92021-02-17 15:05:18 +0100287 return html.format(
288 data, cherrypy.request, cherrypy.response, token_info
289 )
290
tierno1d213f42020-04-24 14:02:51 +0000291 # cherrypy.response.status = HTTPStatus.NO_CONTENT.value
292 return
293 elif hasattr(data, "read"): # file object
294 if _format:
295 cherrypy.response.headers["Content-Type"] = _format
296 elif "b" in data.mode: # binariy asssumig zip
sousaedu80135b92021-02-17 15:05:18 +0100297 cherrypy.response.headers["Content-Type"] = "application/zip"
tierno1d213f42020-04-24 14:02:51 +0000298 else:
sousaedu80135b92021-02-17 15:05:18 +0100299 cherrypy.response.headers["Content-Type"] = "text/plain"
300
tierno1d213f42020-04-24 14:02:51 +0000301 # TODO check that cherrypy close file. If not implement pending things to close per thread next
302 return data
sousaedu80135b92021-02-17 15:05:18 +0100303
tierno1d213f42020-04-24 14:02:51 +0000304 if accept:
305 if "application/json" in accept:
sousaedu80135b92021-02-17 15:05:18 +0100306 cherrypy.response.headers[
307 "Content-Type"
308 ] = "application/json; charset=utf-8"
tierno1d213f42020-04-24 14:02:51 +0000309 a = json.dumps(data, indent=4) + "\n"
sousaedu80135b92021-02-17 15:05:18 +0100310
tierno1d213f42020-04-24 14:02:51 +0000311 return a.encode("utf8")
312 elif "text/html" in accept:
sousaedu80135b92021-02-17 15:05:18 +0100313 return html.format(
314 data, cherrypy.request, cherrypy.response, token_info
315 )
316 elif (
317 "application/yaml" in accept
318 or "*/*" in accept
319 or "text/plain" in accept
320 ):
tierno1d213f42020-04-24 14:02:51 +0000321 pass
322 # if there is not any valid accept, raise an error. But if response is already an error, format in yaml
323 elif cherrypy.response.status >= 400:
sousaedu80135b92021-02-17 15:05:18 +0100324 raise cherrypy.HTTPError(
325 HTTPStatus.NOT_ACCEPTABLE.value,
326 "Only 'Accept' of type 'application/json' or 'application/yaml' "
327 "for output format are available",
328 )
329
330 cherrypy.response.headers["Content-Type"] = "application/yaml"
331
332 return yaml.safe_dump(
333 data,
334 explicit_start=True,
335 indent=4,
336 default_flow_style=False,
337 tags=False,
338 encoding="utf-8",
339 allow_unicode=True,
340 ) # , canonical=True, default_style='"'
tierno1d213f42020-04-24 14:02:51 +0000341
342 @cherrypy.expose
343 def index(self, *args, **kwargs):
344 token_info = None
sousaedu80135b92021-02-17 15:05:18 +0100345
tierno1d213f42020-04-24 14:02:51 +0000346 try:
347 if cherrypy.request.method == "GET":
348 token_info = self.authenticator.authorize()
sousaedu80135b92021-02-17 15:05:18 +0100349 outdata = token_info # Home page
tierno1d213f42020-04-24 14:02:51 +0000350 else:
sousaedu80135b92021-02-17 15:05:18 +0100351 raise cherrypy.HTTPError(
352 HTTPStatus.METHOD_NOT_ALLOWED.value,
353 "Method {} not allowed for tokens".format(cherrypy.request.method),
354 )
tierno1d213f42020-04-24 14:02:51 +0000355
356 return self._format_out(outdata, token_info)
tierno1d213f42020-04-24 14:02:51 +0000357 except (NsException, AuthException) as e:
358 # cherrypy.log("index Exception {}".format(e))
359 cherrypy.response.status = e.http_code.value
sousaedu80135b92021-02-17 15:05:18 +0100360
tierno1d213f42020-04-24 14:02:51 +0000361 return self._format_out("Welcome to OSM!", token_info)
362
363 @cherrypy.expose
364 def version(self, *args, **kwargs):
365 # TODO consider to remove and provide version using the static version file
366 try:
367 if cherrypy.request.method != "GET":
sousaedu80135b92021-02-17 15:05:18 +0100368 raise RoException(
369 "Only method GET is allowed",
370 HTTPStatus.METHOD_NOT_ALLOWED,
371 )
tierno1d213f42020-04-24 14:02:51 +0000372 elif args or kwargs:
sousaedu80135b92021-02-17 15:05:18 +0100373 raise RoException(
374 "Invalid URL or query string for version",
375 HTTPStatus.METHOD_NOT_ALLOWED,
376 )
377
tierno1d213f42020-04-24 14:02:51 +0000378 # TODO include version of other modules, pick up from some kafka admin message
379 osm_ng_ro_version = {"version": ro_version, "date": ro_version_date}
sousaedu80135b92021-02-17 15:05:18 +0100380
tierno1d213f42020-04-24 14:02:51 +0000381 return self._format_out(osm_ng_ro_version)
382 except RoException as e:
383 cherrypy.response.status = e.http_code.value
384 problem_details = {
385 "code": e.http_code.name,
386 "status": e.http_code.value,
387 "detail": str(e),
388 }
sousaedu80135b92021-02-17 15:05:18 +0100389
tierno1d213f42020-04-24 14:02:51 +0000390 return self._format_out(problem_details, None)
391
392 def new_token(self, engine_session, indata, *args, **kwargs):
393 token_info = None
394
395 try:
396 token_info = self.authenticator.authorize()
397 except Exception:
398 token_info = None
sousaedu80135b92021-02-17 15:05:18 +0100399
tierno1d213f42020-04-24 14:02:51 +0000400 if kwargs:
401 indata.update(kwargs)
sousaedu80135b92021-02-17 15:05:18 +0100402
tierno1d213f42020-04-24 14:02:51 +0000403 # This is needed to log the user when authentication fails
404 cherrypy.request.login = "{}".format(indata.get("username", "-"))
sousaedu80135b92021-02-17 15:05:18 +0100405 token_info = self.authenticator.new_token(
406 token_info, indata, cherrypy.request.remote
407 )
408 cherrypy.session["Authorization"] = token_info["id"]
tierno1d213f42020-04-24 14:02:51 +0000409 self._set_location_header("admin", "v1", "tokens", token_info["id"])
410 # for logging
411
412 # cherrypy.response.cookie["Authorization"] = outdata["id"]
413 # cherrypy.response.cookie["Authorization"]['expires'] = 3600
sousaedu80135b92021-02-17 15:05:18 +0100414
tierno1d213f42020-04-24 14:02:51 +0000415 return token_info, token_info["id"], True
416
417 def del_token(self, engine_session, indata, version, _id, *args, **kwargs):
418 token_id = _id
sousaedu80135b92021-02-17 15:05:18 +0100419
tierno1d213f42020-04-24 14:02:51 +0000420 if not token_id and "id" in kwargs:
421 token_id = kwargs["id"]
422 elif not token_id:
423 token_info = self.authenticator.authorize()
424 # for logging
425 token_id = token_info["id"]
sousaedu80135b92021-02-17 15:05:18 +0100426
tierno1d213f42020-04-24 14:02:51 +0000427 self.authenticator.del_token(token_id)
428 token_info = None
sousaedu80135b92021-02-17 15:05:18 +0100429 cherrypy.session["Authorization"] = "logout"
tierno1d213f42020-04-24 14:02:51 +0000430 # cherrypy.response.cookie["Authorization"] = token_id
431 # cherrypy.response.cookie["Authorization"]['expires'] = 0
sousaedu80135b92021-02-17 15:05:18 +0100432
tierno1d213f42020-04-24 14:02:51 +0000433 return None, None, True
sousaedu80135b92021-02-17 15:05:18 +0100434
tierno1d213f42020-04-24 14:02:51 +0000435 @cherrypy.expose
436 def test(self, *args, **kwargs):
sousaedu80135b92021-02-17 15:05:18 +0100437 if not cherrypy.config.get("server.enable_test") or (
438 isinstance(cherrypy.config["server.enable_test"], str)
439 and cherrypy.config["server.enable_test"].lower() == "false"
440 ):
tierno1d213f42020-04-24 14:02:51 +0000441 cherrypy.response.status = HTTPStatus.METHOD_NOT_ALLOWED.value
tierno1d213f42020-04-24 14:02:51 +0000442
sousaedu80135b92021-02-17 15:05:18 +0100443 return "test URL is disabled"
444
445 thread_info = None
446
447 if args and args[0] == "help":
448 return (
449 "<html><pre>\ninit\nfile/<name> download file\ndb-clear/table\nfs-clear[/folder]\nlogin\nlogin2\n"
450 "sleep/<time>\nmessage/topic\n</pre></html>"
451 )
tierno1d213f42020-04-24 14:02:51 +0000452 elif args and args[0] == "init":
453 try:
454 # self.ns.load_dbase(cherrypy.request.app.config)
455 self.ns.create_admin()
sousaedu80135b92021-02-17 15:05:18 +0100456
tierno1d213f42020-04-24 14:02:51 +0000457 return "Done. User 'admin', password 'admin' created"
458 except Exception:
459 cherrypy.response.status = HTTPStatus.FORBIDDEN.value
sousaedu80135b92021-02-17 15:05:18 +0100460
tierno1d213f42020-04-24 14:02:51 +0000461 return self._format_out("Database already initialized")
462 elif args and args[0] == "file":
sousaedu80135b92021-02-17 15:05:18 +0100463 return cherrypy.lib.static.serve_file(
464 cherrypy.tree.apps["/ro"].config["storage"]["path"] + "/" + args[1],
465 "text/plain",
466 "attachment",
467 )
tierno1d213f42020-04-24 14:02:51 +0000468 elif args and args[0] == "file2":
sousaedu80135b92021-02-17 15:05:18 +0100469 f_path = cherrypy.tree.apps["/ro"].config["storage"]["path"] + "/" + args[1]
tierno1d213f42020-04-24 14:02:51 +0000470 f = open(f_path, "r")
471 cherrypy.response.headers["Content-type"] = "text/plain"
472 return f
473
474 elif len(args) == 2 and args[0] == "db-clear":
475 deleted_info = self.ns.db.del_list(args[1], kwargs)
476 return "{} {} deleted\n".format(deleted_info["deleted"], args[1])
477 elif len(args) and args[0] == "fs-clear":
478 if len(args) >= 2:
479 folders = (args[1],)
480 else:
481 folders = self.ns.fs.dir_ls(".")
sousaedu80135b92021-02-17 15:05:18 +0100482
tierno1d213f42020-04-24 14:02:51 +0000483 for folder in folders:
484 self.ns.fs.file_delete(folder)
sousaedu80135b92021-02-17 15:05:18 +0100485
tierno1d213f42020-04-24 14:02:51 +0000486 return ",".join(folders) + " folders deleted\n"
487 elif args and args[0] == "login":
488 if not cherrypy.request.headers.get("Authorization"):
sousaedu80135b92021-02-17 15:05:18 +0100489 cherrypy.response.headers[
490 "WWW-Authenticate"
491 ] = 'Basic realm="Access to OSM site", charset="UTF-8"'
tierno1d213f42020-04-24 14:02:51 +0000492 cherrypy.response.status = HTTPStatus.UNAUTHORIZED.value
493 elif args and args[0] == "login2":
494 if not cherrypy.request.headers.get("Authorization"):
sousaedu80135b92021-02-17 15:05:18 +0100495 cherrypy.response.headers[
496 "WWW-Authenticate"
497 ] = 'Bearer realm="Access to OSM site"'
tierno1d213f42020-04-24 14:02:51 +0000498 cherrypy.response.status = HTTPStatus.UNAUTHORIZED.value
499 elif args and args[0] == "sleep":
500 sleep_time = 5
sousaedu80135b92021-02-17 15:05:18 +0100501
tierno1d213f42020-04-24 14:02:51 +0000502 try:
503 sleep_time = int(args[1])
504 except Exception:
505 cherrypy.response.status = HTTPStatus.FORBIDDEN.value
506 return self._format_out("Database already initialized")
sousaedu80135b92021-02-17 15:05:18 +0100507
tierno1d213f42020-04-24 14:02:51 +0000508 thread_info = cherrypy.thread_data
509 print(thread_info)
510 time.sleep(sleep_time)
511 # thread_info
512 elif len(args) >= 2 and args[0] == "message":
513 main_topic = args[1]
514 return_text = "<html><pre>{} ->\n".format(main_topic)
sousaedu80135b92021-02-17 15:05:18 +0100515
tierno1d213f42020-04-24 14:02:51 +0000516 try:
sousaedu80135b92021-02-17 15:05:18 +0100517 if cherrypy.request.method == "POST":
tierno1d213f42020-04-24 14:02:51 +0000518 to_send = yaml.load(cherrypy.request.body, Loader=yaml.SafeLoader)
519 for k, v in to_send.items():
520 self.ns.msg.write(main_topic, k, v)
521 return_text += " {}: {}\n".format(k, v)
sousaedu80135b92021-02-17 15:05:18 +0100522 elif cherrypy.request.method == "GET":
tierno1d213f42020-04-24 14:02:51 +0000523 for k, v in kwargs.items():
sousaedu80135b92021-02-17 15:05:18 +0100524 self.ns.msg.write(
525 main_topic, k, yaml.load(v, Loader=yaml.SafeLoader)
526 )
527 return_text += " {}: {}\n".format(
528 k, yaml.load(v, Loader=yaml.SafeLoader)
529 )
tierno1d213f42020-04-24 14:02:51 +0000530 except Exception as e:
531 return_text += "Error: " + str(e)
sousaedu80135b92021-02-17 15:05:18 +0100532
tierno1d213f42020-04-24 14:02:51 +0000533 return_text += "</pre></html>\n"
sousaedu80135b92021-02-17 15:05:18 +0100534
tierno1d213f42020-04-24 14:02:51 +0000535 return return_text
536
537 return_text = (
sousaedu80135b92021-02-17 15:05:18 +0100538 "<html><pre>\nheaders:\n args: {}\n".format(args)
539 + " kwargs: {}\n".format(kwargs)
540 + " headers: {}\n".format(cherrypy.request.headers)
541 + " path_info: {}\n".format(cherrypy.request.path_info)
542 + " query_string: {}\n".format(cherrypy.request.query_string)
543 + " session: {}\n".format(cherrypy.session)
544 + " cookie: {}\n".format(cherrypy.request.cookie)
545 + " method: {}\n".format(cherrypy.request.method)
546 + " session: {}\n".format(cherrypy.session.get("fieldname"))
547 + " body:\n"
548 )
tierno1d213f42020-04-24 14:02:51 +0000549 return_text += " length: {}\n".format(cherrypy.request.body.length)
sousaedu80135b92021-02-17 15:05:18 +0100550
tierno1d213f42020-04-24 14:02:51 +0000551 if cherrypy.request.body.length:
552 return_text += " content: {}\n".format(
sousaedu80135b92021-02-17 15:05:18 +0100553 str(
554 cherrypy.request.body.read(
555 int(cherrypy.request.headers.get("Content-Length", 0))
556 )
557 )
558 )
559
tierno1d213f42020-04-24 14:02:51 +0000560 if thread_info:
561 return_text += "thread: {}\n".format(thread_info)
sousaedu80135b92021-02-17 15:05:18 +0100562
tierno1d213f42020-04-24 14:02:51 +0000563 return_text += "</pre></html>"
sousaedu80135b92021-02-17 15:05:18 +0100564
tierno1d213f42020-04-24 14:02:51 +0000565 return return_text
566
567 @staticmethod
568 def _check_valid_url_method(method, *args):
569 if len(args) < 3:
sousaedu80135b92021-02-17 15:05:18 +0100570 raise RoException(
571 "URL must contain at least 'main_topic/version/topic'",
572 HTTPStatus.METHOD_NOT_ALLOWED,
573 )
tierno1d213f42020-04-24 14:02:51 +0000574
575 reference = valid_url_methods
576 for arg in args:
577 if arg is None:
578 break
sousaedu80135b92021-02-17 15:05:18 +0100579
tierno1d213f42020-04-24 14:02:51 +0000580 if not isinstance(reference, dict):
sousaedu80135b92021-02-17 15:05:18 +0100581 raise RoException(
582 "URL contains unexpected extra items '{}'".format(arg),
583 HTTPStatus.METHOD_NOT_ALLOWED,
584 )
tierno1d213f42020-04-24 14:02:51 +0000585
586 if arg in reference:
587 reference = reference[arg]
588 elif "<ID>" in reference:
589 reference = reference["<ID>"]
590 elif "*" in reference:
591 # reference = reference["*"]
592 break
593 else:
sousaedu80135b92021-02-17 15:05:18 +0100594 raise RoException(
595 "Unexpected URL item {}".format(arg),
596 HTTPStatus.METHOD_NOT_ALLOWED,
597 )
598
tierno1d213f42020-04-24 14:02:51 +0000599 if "TODO" in reference and method in reference["TODO"]:
sousaedu80135b92021-02-17 15:05:18 +0100600 raise RoException(
601 "Method {} not supported yet for this URL".format(method),
602 HTTPStatus.NOT_IMPLEMENTED,
603 )
tierno1d213f42020-04-24 14:02:51 +0000604 elif "METHODS" not in reference or method not in reference["METHODS"]:
sousaedu80135b92021-02-17 15:05:18 +0100605 raise RoException(
606 "Method {} not supported for this URL".format(method),
607 HTTPStatus.METHOD_NOT_ALLOWED,
608 )
609
tierno1d213f42020-04-24 14:02:51 +0000610 return reference["ROLE_PERMISSION"] + method.lower()
611
612 @staticmethod
613 def _set_location_header(main_topic, version, topic, id):
614 """
615 Insert response header Location with the URL of created item base on URL params
616 :param main_topic:
617 :param version:
618 :param topic:
619 :param id:
620 :return: None
621 """
622 # 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 +0100623 cherrypy.response.headers["Location"] = "/ro/{}/{}/{}/{}".format(
624 main_topic, version, topic, id
625 )
626
tierno1d213f42020-04-24 14:02:51 +0000627 return
628
629 @cherrypy.expose
sousaedu80135b92021-02-17 15:05:18 +0100630 def default(
631 self,
632 main_topic=None,
633 version=None,
634 topic=None,
635 _id=None,
636 _id2=None,
637 *args,
638 **kwargs,
639 ):
tierno1d213f42020-04-24 14:02:51 +0000640 token_info = None
641 outdata = None
642 _format = None
643 method = "DONE"
644 rollback = []
645 engine_session = None
sousaedu80135b92021-02-17 15:05:18 +0100646
tierno1d213f42020-04-24 14:02:51 +0000647 try:
648 if not main_topic or not version or not topic:
sousaedu80135b92021-02-17 15:05:18 +0100649 raise RoException(
650 "URL must contain at least 'main_topic/version/topic'",
651 HTTPStatus.METHOD_NOT_ALLOWED,
652 )
tierno1d213f42020-04-24 14:02:51 +0000653
sousaedu80135b92021-02-17 15:05:18 +0100654 if main_topic not in (
655 "admin",
656 "ns",
657 ):
658 raise RoException(
659 "URL main_topic '{}' not supported".format(main_topic),
660 HTTPStatus.METHOD_NOT_ALLOWED,
661 )
662
663 if version != "v1":
664 raise RoException(
665 "URL version '{}' not supported".format(version),
666 HTTPStatus.METHOD_NOT_ALLOWED,
667 )
668
669 if (
670 kwargs
671 and "METHOD" in kwargs
672 and kwargs["METHOD"] in ("PUT", "POST", "DELETE", "GET", "PATCH")
673 ):
tierno1d213f42020-04-24 14:02:51 +0000674 method = kwargs.pop("METHOD")
675 else:
676 method = cherrypy.request.method
677
sousaedu80135b92021-02-17 15:05:18 +0100678 role_permission = self._check_valid_url_method(
679 method, main_topic, version, topic, _id, _id2, *args, **kwargs
680 )
tierno1d213f42020-04-24 14:02:51 +0000681 # skip token validation if requesting a token
682 indata = self._format_in(kwargs)
sousaedu80135b92021-02-17 15:05:18 +0100683
tierno1d213f42020-04-24 14:02:51 +0000684 if main_topic != "admin" or topic != "tokens":
685 token_info = self.authenticator.authorize(role_permission, _id)
sousaedu80135b92021-02-17 15:05:18 +0100686
tierno1d213f42020-04-24 14:02:51 +0000687 outdata, created_id, done = self.map_operation[role_permission](
sousaedu80135b92021-02-17 15:05:18 +0100688 engine_session, indata, version, _id, _id2, *args, *kwargs
689 )
690
tierno1d213f42020-04-24 14:02:51 +0000691 if created_id:
692 self._set_location_header(main_topic, version, topic, _id)
sousaedu80135b92021-02-17 15:05:18 +0100693
694 cherrypy.response.status = (
695 HTTPStatus.ACCEPTED.value
696 if not done
697 else HTTPStatus.OK.value
698 if outdata is not None
699 else HTTPStatus.NO_CONTENT.value
700 )
701
tierno1d213f42020-04-24 14:02:51 +0000702 return self._format_out(outdata, token_info, _format)
703 except Exception as e:
sousaedu80135b92021-02-17 15:05:18 +0100704 if isinstance(
705 e,
706 (
707 RoException,
708 NsException,
709 DbException,
710 FsException,
711 MsgException,
712 AuthException,
713 ValidationError,
714 ),
715 ):
tierno1d213f42020-04-24 14:02:51 +0000716 http_code_value = cherrypy.response.status = e.http_code.value
717 http_code_name = e.http_code.name
718 cherrypy.log("Exception {}".format(e))
719 else:
sousaedu80135b92021-02-17 15:05:18 +0100720 http_code_value = (
721 cherrypy.response.status
722 ) = HTTPStatus.BAD_REQUEST.value # INTERNAL_SERVER_ERROR
tierno1d213f42020-04-24 14:02:51 +0000723 cherrypy.log("CRITICAL: Exception {}".format(e), traceback=True)
724 http_code_name = HTTPStatus.BAD_REQUEST.name
sousaedu80135b92021-02-17 15:05:18 +0100725
tierno1d213f42020-04-24 14:02:51 +0000726 if hasattr(outdata, "close"): # is an open file
727 outdata.close()
sousaedu80135b92021-02-17 15:05:18 +0100728
tierno1d213f42020-04-24 14:02:51 +0000729 error_text = str(e)
730 rollback.reverse()
sousaedu80135b92021-02-17 15:05:18 +0100731
tierno1d213f42020-04-24 14:02:51 +0000732 for rollback_item in rollback:
733 try:
734 if rollback_item.get("operation") == "set":
sousaedu80135b92021-02-17 15:05:18 +0100735 self.ns.db.set_one(
736 rollback_item["topic"],
737 {"_id": rollback_item["_id"]},
738 rollback_item["content"],
739 fail_on_empty=False,
740 )
tierno1d213f42020-04-24 14:02:51 +0000741 else:
sousaedu80135b92021-02-17 15:05:18 +0100742 self.ns.db.del_one(
743 rollback_item["topic"],
744 {"_id": rollback_item["_id"]},
745 fail_on_empty=False,
746 )
tierno1d213f42020-04-24 14:02:51 +0000747 except Exception as e2:
sousaedu80135b92021-02-17 15:05:18 +0100748 rollback_error_text = "Rollback Exception {}: {}".format(
749 rollback_item, e2
750 )
tierno1d213f42020-04-24 14:02:51 +0000751 cherrypy.log(rollback_error_text)
752 error_text += ". " + rollback_error_text
sousaedu80135b92021-02-17 15:05:18 +0100753
tierno1d213f42020-04-24 14:02:51 +0000754 # if isinstance(e, MsgException):
755 # error_text = "{} has been '{}' but other modules cannot be informed because an error on bus".format(
756 # engine_topic[:-1], method, error_text)
757 problem_details = {
758 "code": http_code_name,
759 "status": http_code_value,
760 "detail": error_text,
761 }
sousaedu80135b92021-02-17 15:05:18 +0100762
tierno1d213f42020-04-24 14:02:51 +0000763 return self._format_out(problem_details, token_info)
764 # raise cherrypy.HTTPError(e.http_code.value, str(e))
765 finally:
766 if token_info:
767 if method in ("PUT", "PATCH", "POST") and isinstance(outdata, dict):
768 for logging_id in ("id", "op_id", "nsilcmop_id", "nslcmop_id"):
769 if outdata.get(logging_id):
sousaedu80135b92021-02-17 15:05:18 +0100770 cherrypy.request.login += ";{}={}".format(
771 logging_id, outdata[logging_id][:36]
772 )
tierno1d213f42020-04-24 14:02:51 +0000773
774
775def _start_service():
776 """
777 Callback function called when cherrypy.engine starts
778 Override configuration with env variables
779 Set database, storage, message configuration
780 Init database with admin/admin user password
781 """
tierno70eeb182020-10-19 16:38:00 +0000782 global ro_server, vim_admin_thread
tierno1d213f42020-04-24 14:02:51 +0000783 # global vim_threads
784 cherrypy.log.error("Starting osm_ng_ro")
785 # update general cherrypy configuration
786 update_dict = {}
sousaedu80135b92021-02-17 15:05:18 +0100787 engine_config = cherrypy.tree.apps["/ro"].config
tierno1d213f42020-04-24 14:02:51 +0000788
tierno1d213f42020-04-24 14:02:51 +0000789 for k, v in environ.items():
790 if not k.startswith("OSMRO_"):
791 continue
sousaedu80135b92021-02-17 15:05:18 +0100792
tierno1d213f42020-04-24 14:02:51 +0000793 k1, _, k2 = k[6:].lower().partition("_")
sousaedu80135b92021-02-17 15:05:18 +0100794
tierno1d213f42020-04-24 14:02:51 +0000795 if not k2:
796 continue
sousaedu80135b92021-02-17 15:05:18 +0100797
tierno1d213f42020-04-24 14:02:51 +0000798 try:
799 if k1 in ("server", "test", "auth", "log"):
800 # update [global] configuration
sousaedu80135b92021-02-17 15:05:18 +0100801 update_dict[k1 + "." + k2] = yaml.safe_load(v)
tierno1d213f42020-04-24 14:02:51 +0000802 elif k1 == "static":
803 # update [/static] configuration
804 engine_config["/static"]["tools.staticdir." + k2] = yaml.safe_load(v)
805 elif k1 == "tools":
806 # update [/] configuration
sousaedu80135b92021-02-17 15:05:18 +0100807 engine_config["/"]["tools." + k2.replace("_", ".")] = yaml.safe_load(v)
tierno1d213f42020-04-24 14:02:51 +0000808 elif k1 in ("message", "database", "storage", "authentication"):
tierno70eeb182020-10-19 16:38:00 +0000809 engine_config[k1][k2] = yaml.safe_load(v)
tierno1d213f42020-04-24 14:02:51 +0000810
811 except Exception as e:
812 raise RoException("Cannot load env '{}': {}".format(k, e))
813
814 if update_dict:
815 cherrypy.config.update(update_dict)
816 engine_config["global"].update(update_dict)
817
818 # logging cherrypy
sousaedu80135b92021-02-17 15:05:18 +0100819 log_format_simple = (
820 "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(message)s"
821 )
822 log_formatter_simple = logging.Formatter(
823 log_format_simple, datefmt="%Y-%m-%dT%H:%M:%S"
824 )
tierno1d213f42020-04-24 14:02:51 +0000825 logger_server = logging.getLogger("cherrypy.error")
826 logger_access = logging.getLogger("cherrypy.access")
827 logger_cherry = logging.getLogger("cherrypy")
sousaedue493e9b2021-02-09 15:30:01 +0100828 logger = logging.getLogger("ro")
tierno1d213f42020-04-24 14:02:51 +0000829
830 if "log.file" in engine_config["global"]:
sousaedu80135b92021-02-17 15:05:18 +0100831 file_handler = logging.handlers.RotatingFileHandler(
832 engine_config["global"]["log.file"], maxBytes=100e6, backupCount=9, delay=0
833 )
tierno1d213f42020-04-24 14:02:51 +0000834 file_handler.setFormatter(log_formatter_simple)
835 logger_cherry.addHandler(file_handler)
sousaedue493e9b2021-02-09 15:30:01 +0100836 logger.addHandler(file_handler)
sousaedu80135b92021-02-17 15:05:18 +0100837
tierno1d213f42020-04-24 14:02:51 +0000838 # log always to standard output
sousaedu80135b92021-02-17 15:05:18 +0100839 for format_, logger in {
840 "ro.server %(filename)s:%(lineno)s": logger_server,
841 "ro.access %(filename)s:%(lineno)s": logger_access,
842 "%(name)s %(filename)s:%(lineno)s": logger,
843 }.items():
tierno1d213f42020-04-24 14:02:51 +0000844 log_format_cherry = "%(asctime)s %(levelname)s {} %(message)s".format(format_)
sousaedu80135b92021-02-17 15:05:18 +0100845 log_formatter_cherry = logging.Formatter(
846 log_format_cherry, datefmt="%Y-%m-%dT%H:%M:%S"
847 )
tierno1d213f42020-04-24 14:02:51 +0000848 str_handler = logging.StreamHandler()
849 str_handler.setFormatter(log_formatter_cherry)
850 logger.addHandler(str_handler)
851
852 if engine_config["global"].get("log.level"):
853 logger_cherry.setLevel(engine_config["global"]["log.level"])
sousaedue493e9b2021-02-09 15:30:01 +0100854 logger.setLevel(engine_config["global"]["log.level"])
sousaedu80135b92021-02-17 15:05:18 +0100855
tierno1d213f42020-04-24 14:02:51 +0000856 # logging other modules
sousaedu80135b92021-02-17 15:05:18 +0100857 for k1, logname in {
858 "message": "ro.msg",
859 "database": "ro.db",
860 "storage": "ro.fs",
861 }.items():
tierno1d213f42020-04-24 14:02:51 +0000862 engine_config[k1]["logger_name"] = logname
863 logger_module = logging.getLogger(logname)
sousaedu80135b92021-02-17 15:05:18 +0100864
tierno1d213f42020-04-24 14:02:51 +0000865 if "logfile" in engine_config[k1]:
sousaedu80135b92021-02-17 15:05:18 +0100866 file_handler = logging.handlers.RotatingFileHandler(
867 engine_config[k1]["logfile"], maxBytes=100e6, backupCount=9, delay=0
868 )
tierno1d213f42020-04-24 14:02:51 +0000869 file_handler.setFormatter(log_formatter_simple)
870 logger_module.addHandler(file_handler)
sousaedu80135b92021-02-17 15:05:18 +0100871
tierno1d213f42020-04-24 14:02:51 +0000872 if "loglevel" in engine_config[k1]:
873 logger_module.setLevel(engine_config[k1]["loglevel"])
874 # TODO add more entries, e.g.: storage
875
876 engine_config["assignment"] = {}
877 # ^ 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 +0100878 cherrypy.tree.apps["/ro"].root.ns.start(engine_config)
879 cherrypy.tree.apps["/ro"].root.authenticator.start(engine_config)
880 cherrypy.tree.apps["/ro"].root.ns.init_db(target_version=database_version)
tierno1d213f42020-04-24 14:02:51 +0000881
882 # # start subscriptions thread:
tierno70eeb182020-10-19 16:38:00 +0000883 vim_admin_thread = VimAdminThread(config=engine_config, engine=ro_server.ns)
884 vim_admin_thread.start()
tierno1d213f42020-04-24 14:02:51 +0000885 # # Do not capture except SubscriptionException
886
tierno70eeb182020-10-19 16:38:00 +0000887 # backend = engine_config["authentication"]["backend"]
888 # cherrypy.log.error("Starting OSM NBI Version '{} {}' with '{}' authentication backend"
889 # .format(ro_version, ro_version_date, backend))
tierno1d213f42020-04-24 14:02:51 +0000890
891
892def _stop_service():
893 """
894 Callback function called when cherrypy.engine stops
895 TODO: Ending database connections.
896 """
tierno70eeb182020-10-19 16:38:00 +0000897 global vim_admin_thread
sousaedu80135b92021-02-17 15:05:18 +0100898
tierno70eeb182020-10-19 16:38:00 +0000899 # terminate vim_admin_thread
900 if vim_admin_thread:
901 vim_admin_thread.terminate()
sousaedu80135b92021-02-17 15:05:18 +0100902
tierno70eeb182020-10-19 16:38:00 +0000903 vim_admin_thread = None
sousaedu80135b92021-02-17 15:05:18 +0100904 cherrypy.tree.apps["/ro"].root.ns.stop()
tierno1d213f42020-04-24 14:02:51 +0000905 cherrypy.log.error("Stopping osm_ng_ro")
906
907
908def ro_main(config_file):
909 global ro_server
sousaedu80135b92021-02-17 15:05:18 +0100910
tierno1d213f42020-04-24 14:02:51 +0000911 ro_server = Server()
sousaedu80135b92021-02-17 15:05:18 +0100912 cherrypy.engine.subscribe("start", _start_service)
913 cherrypy.engine.subscribe("stop", _stop_service)
914 cherrypy.quickstart(ro_server, "/ro", config_file)
tierno1d213f42020-04-24 14:02:51 +0000915
916
917def usage():
sousaedu80135b92021-02-17 15:05:18 +0100918 print(
919 """Usage: {} [options]
tierno1d213f42020-04-24 14:02:51 +0000920 -c|--config [configuration_file]: loads the configuration file (default: ./ro.cfg)
921 -h|--help: shows this help
sousaedu80135b92021-02-17 15:05:18 +0100922 """.format(
923 sys.argv[0]
924 )
925 )
tierno1d213f42020-04-24 14:02:51 +0000926 # --log-socket-host HOST: send logs to this host")
927 # --log-socket-port PORT: send logs using this port (default: 9022)")
928
929
sousaedu80135b92021-02-17 15:05:18 +0100930if __name__ == "__main__":
tierno1d213f42020-04-24 14:02:51 +0000931 try:
932 # load parameters and configuration
933 opts, args = getopt.getopt(sys.argv[1:], "hvc:", ["config=", "help"])
934 # TODO add "log-socket-host=", "log-socket-port=", "log-file="
935 config_file = None
sousaedu80135b92021-02-17 15:05:18 +0100936
tierno1d213f42020-04-24 14:02:51 +0000937 for o, a in opts:
938 if o in ("-h", "--help"):
939 usage()
940 sys.exit()
941 elif o in ("-c", "--config"):
942 config_file = a
943 else:
944 assert False, "Unhandled option"
sousaedu80135b92021-02-17 15:05:18 +0100945
tierno1d213f42020-04-24 14:02:51 +0000946 if config_file:
947 if not path.isfile(config_file):
sousaedu80135b92021-02-17 15:05:18 +0100948 print(
949 "configuration file '{}' that not exist".format(config_file),
950 file=sys.stderr,
951 )
tierno1d213f42020-04-24 14:02:51 +0000952 exit(1)
953 else:
sousaedu80135b92021-02-17 15:05:18 +0100954 for config_file in (
955 path.dirname(__file__) + "/ro.cfg",
956 "./ro.cfg",
957 "/etc/osm/ro.cfg",
958 ):
tierno1d213f42020-04-24 14:02:51 +0000959 if path.isfile(config_file):
960 break
961 else:
sousaedu80135b92021-02-17 15:05:18 +0100962 print(
963 "No configuration file 'ro.cfg' found neither at local folder nor at /etc/osm/",
964 file=sys.stderr,
965 )
tierno1d213f42020-04-24 14:02:51 +0000966 exit(1)
sousaedu80135b92021-02-17 15:05:18 +0100967
tierno1d213f42020-04-24 14:02:51 +0000968 ro_main(config_file)
tierno70eeb182020-10-19 16:38:00 +0000969 except KeyboardInterrupt:
970 print("KeyboardInterrupt. Finishing", file=sys.stderr)
tierno1d213f42020-04-24 14:02:51 +0000971 except getopt.GetoptError as e:
972 print(str(e), file=sys.stderr)
973 # usage()
974 exit(1)