blob: 30acd2b6100132a56f7f4db542a828346a863591 [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 },
elumalai8658c2c2022-04-28 19:09:31 +0530112 "migrate": {
113 "<ID>": {
114 "METHODS": ("POST"),
115 "ROLE_PERMISSION": "migrate:id:",
116 "<ID>": {
117 "METHODS": ("GET",),
118 "ROLE_PERMISSION": "migrate:id:id:",
119 },
120 },
121 },
tierno1d213f42020-04-24 14:02:51 +0000122 }
123 },
124}
125
126
127class RoException(Exception):
tierno1d213f42020-04-24 14:02:51 +0000128 def __init__(self, message, http_code=HTTPStatus.METHOD_NOT_ALLOWED):
129 Exception.__init__(self, message)
130 self.http_code = http_code
131
132
133class AuthException(RoException):
134 pass
135
136
137class Authenticator:
tierno1d213f42020-04-24 14:02:51 +0000138 def __init__(self, valid_url_methods, valid_query_string):
139 self.valid_url_methods = valid_url_methods
140 self.valid_query_string = valid_query_string
141
142 def authorize(self, *args, **kwargs):
143 return {"token": "ok", "id": "ok"}
sousaedu80135b92021-02-17 15:05:18 +0100144
tierno1d213f42020-04-24 14:02:51 +0000145 def new_token(self, token_info, indata, remote):
sousaedu80135b92021-02-17 15:05:18 +0100146 return {"token": "ok", "id": "ok", "remote": remote}
tierno1d213f42020-04-24 14:02:51 +0000147
148 def del_token(self, token_id):
149 pass
150
151 def start(self, engine_config):
152 pass
153
154
155class Server(object):
156 instance = 0
157 # to decode bytes to str
158 reader = getreader("utf-8")
159
160 def __init__(self):
161 self.instance += 1
162 self.authenticator = Authenticator(valid_url_methods, valid_query_string)
163 self.ns = Ns()
164 self.map_operation = {
165 "token:post": self.new_token,
166 "token:id:delete": self.del_token,
167 "deploy:get": self.ns.get_deploy,
168 "deploy:id:get": self.ns.get_actions,
169 "deploy:id:post": self.ns.deploy,
170 "deploy:id:delete": self.ns.delete,
171 "deploy:id:id:get": self.ns.status,
172 "deploy:id:id:cancel:post": self.ns.cancel,
palaciosj8f2060b2022-02-24 12:05:59 +0000173 "recreate:id:post": self.ns.recreate,
174 "recreate:id:id:get": self.ns.recreate_status,
elumalai8658c2c2022-04-28 19:09:31 +0530175 "migrate:id:post": self.ns.migrate,
tierno1d213f42020-04-24 14:02:51 +0000176 }
177
178 def _format_in(self, kwargs):
179 try:
180 indata = None
sousaedu80135b92021-02-17 15:05:18 +0100181
tierno1d213f42020-04-24 14:02:51 +0000182 if cherrypy.request.body.length:
183 error_text = "Invalid input format "
184
185 if "Content-Type" in cherrypy.request.headers:
186 if "application/json" in cherrypy.request.headers["Content-Type"]:
187 error_text = "Invalid json format "
188 indata = json.load(self.reader(cherrypy.request.body))
189 cherrypy.request.headers.pop("Content-File-MD5", None)
190 elif "application/yaml" in cherrypy.request.headers["Content-Type"]:
191 error_text = "Invalid yaml format "
sousaedu80135b92021-02-17 15:05:18 +0100192 indata = yaml.load(
193 cherrypy.request.body, Loader=yaml.SafeLoader
194 )
tierno1d213f42020-04-24 14:02:51 +0000195 cherrypy.request.headers.pop("Content-File-MD5", None)
sousaedu80135b92021-02-17 15:05:18 +0100196 elif (
197 "application/binary" in cherrypy.request.headers["Content-Type"]
198 or "application/gzip"
199 in cherrypy.request.headers["Content-Type"]
200 or "application/zip" in cherrypy.request.headers["Content-Type"]
201 or "text/plain" in cherrypy.request.headers["Content-Type"]
202 ):
tierno1d213f42020-04-24 14:02:51 +0000203 indata = cherrypy.request.body # .read()
sousaedu80135b92021-02-17 15:05:18 +0100204 elif (
205 "multipart/form-data"
206 in cherrypy.request.headers["Content-Type"]
207 ):
tierno1d213f42020-04-24 14:02:51 +0000208 if "descriptor_file" in kwargs:
209 filecontent = kwargs.pop("descriptor_file")
sousaedu80135b92021-02-17 15:05:18 +0100210
tierno1d213f42020-04-24 14:02:51 +0000211 if not filecontent.file:
sousaedu80135b92021-02-17 15:05:18 +0100212 raise RoException(
213 "empty file or content", HTTPStatus.BAD_REQUEST
214 )
215
tierno1d213f42020-04-24 14:02:51 +0000216 indata = filecontent.file # .read()
sousaedu80135b92021-02-17 15:05:18 +0100217
tierno1d213f42020-04-24 14:02:51 +0000218 if filecontent.content_type.value:
sousaedu80135b92021-02-17 15:05:18 +0100219 cherrypy.request.headers[
220 "Content-Type"
221 ] = filecontent.content_type.value
tierno1d213f42020-04-24 14:02:51 +0000222 else:
223 # raise cherrypy.HTTPError(HTTPStatus.Not_Acceptable,
224 # "Only 'Content-Type' of type 'application/json' or
225 # 'application/yaml' for input format are available")
226 error_text = "Invalid yaml format "
sousaedu80135b92021-02-17 15:05:18 +0100227 indata = yaml.load(
228 cherrypy.request.body, Loader=yaml.SafeLoader
229 )
tierno1d213f42020-04-24 14:02:51 +0000230 cherrypy.request.headers.pop("Content-File-MD5", None)
231 else:
232 error_text = "Invalid yaml format "
233 indata = yaml.load(cherrypy.request.body, Loader=yaml.SafeLoader)
234 cherrypy.request.headers.pop("Content-File-MD5", None)
sousaedu80135b92021-02-17 15:05:18 +0100235
tierno1d213f42020-04-24 14:02:51 +0000236 if not indata:
237 indata = {}
238
239 format_yaml = False
240 if cherrypy.request.headers.get("Query-String-Format") == "yaml":
241 format_yaml = True
242
243 for k, v in kwargs.items():
244 if isinstance(v, str):
245 if v == "":
246 kwargs[k] = None
247 elif format_yaml:
248 try:
249 kwargs[k] = yaml.load(v, Loader=yaml.SafeLoader)
250 except Exception:
251 pass
sousaedu80135b92021-02-17 15:05:18 +0100252 elif (
253 k.endswith(".gt")
254 or k.endswith(".lt")
255 or k.endswith(".gte")
256 or k.endswith(".lte")
257 ):
tierno1d213f42020-04-24 14:02:51 +0000258 try:
259 kwargs[k] = int(v)
260 except Exception:
261 try:
262 kwargs[k] = float(v)
263 except Exception:
264 pass
265 elif v.find(",") > 0:
266 kwargs[k] = v.split(",")
267 elif isinstance(v, (list, tuple)):
268 for index in range(0, len(v)):
269 if v[index] == "":
270 v[index] = None
271 elif format_yaml:
272 try:
273 v[index] = yaml.load(v[index], Loader=yaml.SafeLoader)
274 except Exception:
275 pass
276
277 return indata
278 except (ValueError, yaml.YAMLError) as exc:
279 raise RoException(error_text + str(exc), HTTPStatus.BAD_REQUEST)
280 except KeyError as exc:
281 raise RoException("Query string error: " + str(exc), HTTPStatus.BAD_REQUEST)
282 except Exception as exc:
283 raise RoException(error_text + str(exc), HTTPStatus.BAD_REQUEST)
284
285 @staticmethod
286 def _format_out(data, token_info=None, _format=None):
287 """
288 return string of dictionary data according to requested json, yaml, xml. By default json
289 :param data: response to be sent. Can be a dict, text or file
290 :param token_info: Contains among other username and project
291 :param _format: The format to be set as Content-Type if data is a file
292 :return: None
293 """
294 accept = cherrypy.request.headers.get("Accept")
sousaedu80135b92021-02-17 15:05:18 +0100295
tierno1d213f42020-04-24 14:02:51 +0000296 if data is None:
297 if accept and "text/html" in accept:
sousaedu80135b92021-02-17 15:05:18 +0100298 return html.format(
299 data, cherrypy.request, cherrypy.response, token_info
300 )
301
tierno1d213f42020-04-24 14:02:51 +0000302 # cherrypy.response.status = HTTPStatus.NO_CONTENT.value
303 return
304 elif hasattr(data, "read"): # file object
305 if _format:
306 cherrypy.response.headers["Content-Type"] = _format
307 elif "b" in data.mode: # binariy asssumig zip
sousaedu80135b92021-02-17 15:05:18 +0100308 cherrypy.response.headers["Content-Type"] = "application/zip"
tierno1d213f42020-04-24 14:02:51 +0000309 else:
sousaedu80135b92021-02-17 15:05:18 +0100310 cherrypy.response.headers["Content-Type"] = "text/plain"
311
tierno1d213f42020-04-24 14:02:51 +0000312 # TODO check that cherrypy close file. If not implement pending things to close per thread next
313 return data
sousaedu80135b92021-02-17 15:05:18 +0100314
tierno1d213f42020-04-24 14:02:51 +0000315 if accept:
316 if "application/json" in accept:
sousaedu80135b92021-02-17 15:05:18 +0100317 cherrypy.response.headers[
318 "Content-Type"
319 ] = "application/json; charset=utf-8"
tierno1d213f42020-04-24 14:02:51 +0000320 a = json.dumps(data, indent=4) + "\n"
sousaedu80135b92021-02-17 15:05:18 +0100321
tierno1d213f42020-04-24 14:02:51 +0000322 return a.encode("utf8")
323 elif "text/html" in accept:
sousaedu80135b92021-02-17 15:05:18 +0100324 return html.format(
325 data, cherrypy.request, cherrypy.response, token_info
326 )
327 elif (
328 "application/yaml" in accept
329 or "*/*" in accept
330 or "text/plain" in accept
331 ):
tierno1d213f42020-04-24 14:02:51 +0000332 pass
333 # if there is not any valid accept, raise an error. But if response is already an error, format in yaml
334 elif cherrypy.response.status >= 400:
sousaedu80135b92021-02-17 15:05:18 +0100335 raise cherrypy.HTTPError(
336 HTTPStatus.NOT_ACCEPTABLE.value,
337 "Only 'Accept' of type 'application/json' or 'application/yaml' "
338 "for output format are available",
339 )
340
341 cherrypy.response.headers["Content-Type"] = "application/yaml"
342
343 return yaml.safe_dump(
344 data,
345 explicit_start=True,
346 indent=4,
347 default_flow_style=False,
348 tags=False,
349 encoding="utf-8",
350 allow_unicode=True,
351 ) # , canonical=True, default_style='"'
tierno1d213f42020-04-24 14:02:51 +0000352
353 @cherrypy.expose
354 def index(self, *args, **kwargs):
355 token_info = None
sousaedu80135b92021-02-17 15:05:18 +0100356
tierno1d213f42020-04-24 14:02:51 +0000357 try:
358 if cherrypy.request.method == "GET":
359 token_info = self.authenticator.authorize()
sousaedu80135b92021-02-17 15:05:18 +0100360 outdata = token_info # Home page
tierno1d213f42020-04-24 14:02:51 +0000361 else:
sousaedu80135b92021-02-17 15:05:18 +0100362 raise cherrypy.HTTPError(
363 HTTPStatus.METHOD_NOT_ALLOWED.value,
364 "Method {} not allowed for tokens".format(cherrypy.request.method),
365 )
tierno1d213f42020-04-24 14:02:51 +0000366
367 return self._format_out(outdata, token_info)
tierno1d213f42020-04-24 14:02:51 +0000368 except (NsException, AuthException) as e:
369 # cherrypy.log("index Exception {}".format(e))
370 cherrypy.response.status = e.http_code.value
sousaedu80135b92021-02-17 15:05:18 +0100371
tierno1d213f42020-04-24 14:02:51 +0000372 return self._format_out("Welcome to OSM!", token_info)
373
374 @cherrypy.expose
375 def version(self, *args, **kwargs):
376 # TODO consider to remove and provide version using the static version file
377 try:
378 if cherrypy.request.method != "GET":
sousaedu80135b92021-02-17 15:05:18 +0100379 raise RoException(
380 "Only method GET is allowed",
381 HTTPStatus.METHOD_NOT_ALLOWED,
382 )
tierno1d213f42020-04-24 14:02:51 +0000383 elif args or kwargs:
sousaedu80135b92021-02-17 15:05:18 +0100384 raise RoException(
385 "Invalid URL or query string for version",
386 HTTPStatus.METHOD_NOT_ALLOWED,
387 )
388
tierno1d213f42020-04-24 14:02:51 +0000389 # TODO include version of other modules, pick up from some kafka admin message
390 osm_ng_ro_version = {"version": ro_version, "date": ro_version_date}
sousaedu80135b92021-02-17 15:05:18 +0100391
tierno1d213f42020-04-24 14:02:51 +0000392 return self._format_out(osm_ng_ro_version)
393 except RoException as e:
394 cherrypy.response.status = e.http_code.value
395 problem_details = {
396 "code": e.http_code.name,
397 "status": e.http_code.value,
398 "detail": str(e),
399 }
sousaedu80135b92021-02-17 15:05:18 +0100400
tierno1d213f42020-04-24 14:02:51 +0000401 return self._format_out(problem_details, None)
402
403 def new_token(self, engine_session, indata, *args, **kwargs):
404 token_info = None
405
406 try:
407 token_info = self.authenticator.authorize()
408 except Exception:
409 token_info = None
sousaedu80135b92021-02-17 15:05:18 +0100410
tierno1d213f42020-04-24 14:02:51 +0000411 if kwargs:
412 indata.update(kwargs)
sousaedu80135b92021-02-17 15:05:18 +0100413
tierno1d213f42020-04-24 14:02:51 +0000414 # This is needed to log the user when authentication fails
415 cherrypy.request.login = "{}".format(indata.get("username", "-"))
sousaedu80135b92021-02-17 15:05:18 +0100416 token_info = self.authenticator.new_token(
417 token_info, indata, cherrypy.request.remote
418 )
419 cherrypy.session["Authorization"] = token_info["id"]
tierno1d213f42020-04-24 14:02:51 +0000420 self._set_location_header("admin", "v1", "tokens", token_info["id"])
421 # for logging
422
423 # cherrypy.response.cookie["Authorization"] = outdata["id"]
424 # cherrypy.response.cookie["Authorization"]['expires'] = 3600
sousaedu80135b92021-02-17 15:05:18 +0100425
tierno1d213f42020-04-24 14:02:51 +0000426 return token_info, token_info["id"], True
427
428 def del_token(self, engine_session, indata, version, _id, *args, **kwargs):
429 token_id = _id
sousaedu80135b92021-02-17 15:05:18 +0100430
tierno1d213f42020-04-24 14:02:51 +0000431 if not token_id and "id" in kwargs:
432 token_id = kwargs["id"]
433 elif not token_id:
434 token_info = self.authenticator.authorize()
435 # for logging
436 token_id = token_info["id"]
sousaedu80135b92021-02-17 15:05:18 +0100437
tierno1d213f42020-04-24 14:02:51 +0000438 self.authenticator.del_token(token_id)
439 token_info = None
sousaedu80135b92021-02-17 15:05:18 +0100440 cherrypy.session["Authorization"] = "logout"
tierno1d213f42020-04-24 14:02:51 +0000441 # cherrypy.response.cookie["Authorization"] = token_id
442 # cherrypy.response.cookie["Authorization"]['expires'] = 0
sousaedu80135b92021-02-17 15:05:18 +0100443
tierno1d213f42020-04-24 14:02:51 +0000444 return None, None, True
sousaedu80135b92021-02-17 15:05:18 +0100445
tierno1d213f42020-04-24 14:02:51 +0000446 @cherrypy.expose
447 def test(self, *args, **kwargs):
sousaedu80135b92021-02-17 15:05:18 +0100448 if not cherrypy.config.get("server.enable_test") or (
449 isinstance(cherrypy.config["server.enable_test"], str)
450 and cherrypy.config["server.enable_test"].lower() == "false"
451 ):
tierno1d213f42020-04-24 14:02:51 +0000452 cherrypy.response.status = HTTPStatus.METHOD_NOT_ALLOWED.value
tierno1d213f42020-04-24 14:02:51 +0000453
sousaedu80135b92021-02-17 15:05:18 +0100454 return "test URL is disabled"
455
456 thread_info = None
457
458 if args and args[0] == "help":
459 return (
460 "<html><pre>\ninit\nfile/<name> download file\ndb-clear/table\nfs-clear[/folder]\nlogin\nlogin2\n"
461 "sleep/<time>\nmessage/topic\n</pre></html>"
462 )
tierno1d213f42020-04-24 14:02:51 +0000463 elif args and args[0] == "init":
464 try:
465 # self.ns.load_dbase(cherrypy.request.app.config)
466 self.ns.create_admin()
sousaedu80135b92021-02-17 15:05:18 +0100467
tierno1d213f42020-04-24 14:02:51 +0000468 return "Done. User 'admin', password 'admin' created"
469 except Exception:
470 cherrypy.response.status = HTTPStatus.FORBIDDEN.value
sousaedu80135b92021-02-17 15:05:18 +0100471
tierno1d213f42020-04-24 14:02:51 +0000472 return self._format_out("Database already initialized")
473 elif args and args[0] == "file":
sousaedu80135b92021-02-17 15:05:18 +0100474 return cherrypy.lib.static.serve_file(
475 cherrypy.tree.apps["/ro"].config["storage"]["path"] + "/" + args[1],
476 "text/plain",
477 "attachment",
478 )
tierno1d213f42020-04-24 14:02:51 +0000479 elif args and args[0] == "file2":
sousaedu80135b92021-02-17 15:05:18 +0100480 f_path = cherrypy.tree.apps["/ro"].config["storage"]["path"] + "/" + args[1]
tierno1d213f42020-04-24 14:02:51 +0000481 f = open(f_path, "r")
482 cherrypy.response.headers["Content-type"] = "text/plain"
483 return f
484
485 elif len(args) == 2 and args[0] == "db-clear":
486 deleted_info = self.ns.db.del_list(args[1], kwargs)
487 return "{} {} deleted\n".format(deleted_info["deleted"], args[1])
488 elif len(args) and args[0] == "fs-clear":
489 if len(args) >= 2:
490 folders = (args[1],)
491 else:
492 folders = self.ns.fs.dir_ls(".")
sousaedu80135b92021-02-17 15:05:18 +0100493
tierno1d213f42020-04-24 14:02:51 +0000494 for folder in folders:
495 self.ns.fs.file_delete(folder)
sousaedu80135b92021-02-17 15:05:18 +0100496
tierno1d213f42020-04-24 14:02:51 +0000497 return ",".join(folders) + " folders deleted\n"
498 elif args and args[0] == "login":
499 if not cherrypy.request.headers.get("Authorization"):
sousaedu80135b92021-02-17 15:05:18 +0100500 cherrypy.response.headers[
501 "WWW-Authenticate"
502 ] = 'Basic realm="Access to OSM site", charset="UTF-8"'
tierno1d213f42020-04-24 14:02:51 +0000503 cherrypy.response.status = HTTPStatus.UNAUTHORIZED.value
504 elif args and args[0] == "login2":
505 if not cherrypy.request.headers.get("Authorization"):
sousaedu80135b92021-02-17 15:05:18 +0100506 cherrypy.response.headers[
507 "WWW-Authenticate"
508 ] = 'Bearer realm="Access to OSM site"'
tierno1d213f42020-04-24 14:02:51 +0000509 cherrypy.response.status = HTTPStatus.UNAUTHORIZED.value
510 elif args and args[0] == "sleep":
511 sleep_time = 5
sousaedu80135b92021-02-17 15:05:18 +0100512
tierno1d213f42020-04-24 14:02:51 +0000513 try:
514 sleep_time = int(args[1])
515 except Exception:
516 cherrypy.response.status = HTTPStatus.FORBIDDEN.value
517 return self._format_out("Database already initialized")
sousaedu80135b92021-02-17 15:05:18 +0100518
tierno1d213f42020-04-24 14:02:51 +0000519 thread_info = cherrypy.thread_data
520 print(thread_info)
521 time.sleep(sleep_time)
522 # thread_info
523 elif len(args) >= 2 and args[0] == "message":
524 main_topic = args[1]
525 return_text = "<html><pre>{} ->\n".format(main_topic)
sousaedu80135b92021-02-17 15:05:18 +0100526
tierno1d213f42020-04-24 14:02:51 +0000527 try:
sousaedu80135b92021-02-17 15:05:18 +0100528 if cherrypy.request.method == "POST":
tierno1d213f42020-04-24 14:02:51 +0000529 to_send = yaml.load(cherrypy.request.body, Loader=yaml.SafeLoader)
530 for k, v in to_send.items():
531 self.ns.msg.write(main_topic, k, v)
532 return_text += " {}: {}\n".format(k, v)
sousaedu80135b92021-02-17 15:05:18 +0100533 elif cherrypy.request.method == "GET":
tierno1d213f42020-04-24 14:02:51 +0000534 for k, v in kwargs.items():
sousaedu80135b92021-02-17 15:05:18 +0100535 self.ns.msg.write(
536 main_topic, k, yaml.load(v, Loader=yaml.SafeLoader)
537 )
538 return_text += " {}: {}\n".format(
539 k, yaml.load(v, Loader=yaml.SafeLoader)
540 )
tierno1d213f42020-04-24 14:02:51 +0000541 except Exception as e:
542 return_text += "Error: " + str(e)
sousaedu80135b92021-02-17 15:05:18 +0100543
tierno1d213f42020-04-24 14:02:51 +0000544 return_text += "</pre></html>\n"
sousaedu80135b92021-02-17 15:05:18 +0100545
tierno1d213f42020-04-24 14:02:51 +0000546 return return_text
547
548 return_text = (
sousaedu80135b92021-02-17 15:05:18 +0100549 "<html><pre>\nheaders:\n args: {}\n".format(args)
550 + " kwargs: {}\n".format(kwargs)
551 + " headers: {}\n".format(cherrypy.request.headers)
552 + " path_info: {}\n".format(cherrypy.request.path_info)
553 + " query_string: {}\n".format(cherrypy.request.query_string)
554 + " session: {}\n".format(cherrypy.session)
555 + " cookie: {}\n".format(cherrypy.request.cookie)
556 + " method: {}\n".format(cherrypy.request.method)
557 + " session: {}\n".format(cherrypy.session.get("fieldname"))
558 + " body:\n"
559 )
tierno1d213f42020-04-24 14:02:51 +0000560 return_text += " length: {}\n".format(cherrypy.request.body.length)
sousaedu80135b92021-02-17 15:05:18 +0100561
tierno1d213f42020-04-24 14:02:51 +0000562 if cherrypy.request.body.length:
563 return_text += " content: {}\n".format(
sousaedu80135b92021-02-17 15:05:18 +0100564 str(
565 cherrypy.request.body.read(
566 int(cherrypy.request.headers.get("Content-Length", 0))
567 )
568 )
569 )
570
tierno1d213f42020-04-24 14:02:51 +0000571 if thread_info:
572 return_text += "thread: {}\n".format(thread_info)
sousaedu80135b92021-02-17 15:05:18 +0100573
tierno1d213f42020-04-24 14:02:51 +0000574 return_text += "</pre></html>"
sousaedu80135b92021-02-17 15:05:18 +0100575
tierno1d213f42020-04-24 14:02:51 +0000576 return return_text
577
578 @staticmethod
579 def _check_valid_url_method(method, *args):
580 if len(args) < 3:
sousaedu80135b92021-02-17 15:05:18 +0100581 raise RoException(
582 "URL must contain at least 'main_topic/version/topic'",
583 HTTPStatus.METHOD_NOT_ALLOWED,
584 )
tierno1d213f42020-04-24 14:02:51 +0000585
586 reference = valid_url_methods
587 for arg in args:
588 if arg is None:
589 break
sousaedu80135b92021-02-17 15:05:18 +0100590
tierno1d213f42020-04-24 14:02:51 +0000591 if not isinstance(reference, dict):
sousaedu80135b92021-02-17 15:05:18 +0100592 raise RoException(
593 "URL contains unexpected extra items '{}'".format(arg),
594 HTTPStatus.METHOD_NOT_ALLOWED,
595 )
tierno1d213f42020-04-24 14:02:51 +0000596
597 if arg in reference:
598 reference = reference[arg]
599 elif "<ID>" in reference:
600 reference = reference["<ID>"]
601 elif "*" in reference:
602 # reference = reference["*"]
603 break
604 else:
sousaedu80135b92021-02-17 15:05:18 +0100605 raise RoException(
606 "Unexpected URL item {}".format(arg),
607 HTTPStatus.METHOD_NOT_ALLOWED,
608 )
609
tierno1d213f42020-04-24 14:02:51 +0000610 if "TODO" in reference and method in reference["TODO"]:
sousaedu80135b92021-02-17 15:05:18 +0100611 raise RoException(
612 "Method {} not supported yet for this URL".format(method),
613 HTTPStatus.NOT_IMPLEMENTED,
614 )
tierno1d213f42020-04-24 14:02:51 +0000615 elif "METHODS" not in reference or method not in reference["METHODS"]:
sousaedu80135b92021-02-17 15:05:18 +0100616 raise RoException(
617 "Method {} not supported for this URL".format(method),
618 HTTPStatus.METHOD_NOT_ALLOWED,
619 )
620
tierno1d213f42020-04-24 14:02:51 +0000621 return reference["ROLE_PERMISSION"] + method.lower()
622
623 @staticmethod
624 def _set_location_header(main_topic, version, topic, id):
625 """
626 Insert response header Location with the URL of created item base on URL params
627 :param main_topic:
628 :param version:
629 :param topic:
630 :param id:
631 :return: None
632 """
633 # 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 +0100634 cherrypy.response.headers["Location"] = "/ro/{}/{}/{}/{}".format(
635 main_topic, version, topic, id
636 )
637
tierno1d213f42020-04-24 14:02:51 +0000638 return
639
640 @cherrypy.expose
sousaedu80135b92021-02-17 15:05:18 +0100641 def default(
642 self,
643 main_topic=None,
644 version=None,
645 topic=None,
646 _id=None,
647 _id2=None,
648 *args,
649 **kwargs,
650 ):
tierno1d213f42020-04-24 14:02:51 +0000651 token_info = None
652 outdata = None
653 _format = None
654 method = "DONE"
655 rollback = []
656 engine_session = None
sousaedu80135b92021-02-17 15:05:18 +0100657
tierno1d213f42020-04-24 14:02:51 +0000658 try:
659 if not main_topic or not version or not topic:
sousaedu80135b92021-02-17 15:05:18 +0100660 raise RoException(
661 "URL must contain at least 'main_topic/version/topic'",
662 HTTPStatus.METHOD_NOT_ALLOWED,
663 )
tierno1d213f42020-04-24 14:02:51 +0000664
sousaedu80135b92021-02-17 15:05:18 +0100665 if main_topic not in (
666 "admin",
667 "ns",
668 ):
669 raise RoException(
670 "URL main_topic '{}' not supported".format(main_topic),
671 HTTPStatus.METHOD_NOT_ALLOWED,
672 )
673
674 if version != "v1":
675 raise RoException(
676 "URL version '{}' not supported".format(version),
677 HTTPStatus.METHOD_NOT_ALLOWED,
678 )
679
680 if (
681 kwargs
682 and "METHOD" in kwargs
683 and kwargs["METHOD"] in ("PUT", "POST", "DELETE", "GET", "PATCH")
684 ):
tierno1d213f42020-04-24 14:02:51 +0000685 method = kwargs.pop("METHOD")
686 else:
687 method = cherrypy.request.method
688
sousaedu80135b92021-02-17 15:05:18 +0100689 role_permission = self._check_valid_url_method(
690 method, main_topic, version, topic, _id, _id2, *args, **kwargs
691 )
tierno1d213f42020-04-24 14:02:51 +0000692 # skip token validation if requesting a token
693 indata = self._format_in(kwargs)
sousaedu80135b92021-02-17 15:05:18 +0100694
tierno1d213f42020-04-24 14:02:51 +0000695 if main_topic != "admin" or topic != "tokens":
696 token_info = self.authenticator.authorize(role_permission, _id)
sousaedu80135b92021-02-17 15:05:18 +0100697
tierno1d213f42020-04-24 14:02:51 +0000698 outdata, created_id, done = self.map_operation[role_permission](
sousaedu80135b92021-02-17 15:05:18 +0100699 engine_session, indata, version, _id, _id2, *args, *kwargs
700 )
701
tierno1d213f42020-04-24 14:02:51 +0000702 if created_id:
703 self._set_location_header(main_topic, version, topic, _id)
sousaedu80135b92021-02-17 15:05:18 +0100704
705 cherrypy.response.status = (
706 HTTPStatus.ACCEPTED.value
707 if not done
708 else HTTPStatus.OK.value
709 if outdata is not None
710 else HTTPStatus.NO_CONTENT.value
711 )
712
tierno1d213f42020-04-24 14:02:51 +0000713 return self._format_out(outdata, token_info, _format)
714 except Exception as e:
sousaedu80135b92021-02-17 15:05:18 +0100715 if isinstance(
716 e,
717 (
718 RoException,
719 NsException,
720 DbException,
721 FsException,
722 MsgException,
723 AuthException,
724 ValidationError,
725 ),
726 ):
tierno1d213f42020-04-24 14:02:51 +0000727 http_code_value = cherrypy.response.status = e.http_code.value
728 http_code_name = e.http_code.name
729 cherrypy.log("Exception {}".format(e))
730 else:
sousaedu80135b92021-02-17 15:05:18 +0100731 http_code_value = (
732 cherrypy.response.status
733 ) = HTTPStatus.BAD_REQUEST.value # INTERNAL_SERVER_ERROR
tierno1d213f42020-04-24 14:02:51 +0000734 cherrypy.log("CRITICAL: Exception {}".format(e), traceback=True)
735 http_code_name = HTTPStatus.BAD_REQUEST.name
sousaedu80135b92021-02-17 15:05:18 +0100736
tierno1d213f42020-04-24 14:02:51 +0000737 if hasattr(outdata, "close"): # is an open file
738 outdata.close()
sousaedu80135b92021-02-17 15:05:18 +0100739
tierno1d213f42020-04-24 14:02:51 +0000740 error_text = str(e)
741 rollback.reverse()
sousaedu80135b92021-02-17 15:05:18 +0100742
tierno1d213f42020-04-24 14:02:51 +0000743 for rollback_item in rollback:
744 try:
745 if rollback_item.get("operation") == "set":
sousaedu80135b92021-02-17 15:05:18 +0100746 self.ns.db.set_one(
747 rollback_item["topic"],
748 {"_id": rollback_item["_id"]},
749 rollback_item["content"],
750 fail_on_empty=False,
751 )
tierno1d213f42020-04-24 14:02:51 +0000752 else:
sousaedu80135b92021-02-17 15:05:18 +0100753 self.ns.db.del_one(
754 rollback_item["topic"],
755 {"_id": rollback_item["_id"]},
756 fail_on_empty=False,
757 )
tierno1d213f42020-04-24 14:02:51 +0000758 except Exception as e2:
sousaedu80135b92021-02-17 15:05:18 +0100759 rollback_error_text = "Rollback Exception {}: {}".format(
760 rollback_item, e2
761 )
tierno1d213f42020-04-24 14:02:51 +0000762 cherrypy.log(rollback_error_text)
763 error_text += ". " + rollback_error_text
sousaedu80135b92021-02-17 15:05:18 +0100764
tierno1d213f42020-04-24 14:02:51 +0000765 # if isinstance(e, MsgException):
766 # error_text = "{} has been '{}' but other modules cannot be informed because an error on bus".format(
767 # engine_topic[:-1], method, error_text)
768 problem_details = {
769 "code": http_code_name,
770 "status": http_code_value,
771 "detail": error_text,
772 }
sousaedu80135b92021-02-17 15:05:18 +0100773
tierno1d213f42020-04-24 14:02:51 +0000774 return self._format_out(problem_details, token_info)
775 # raise cherrypy.HTTPError(e.http_code.value, str(e))
776 finally:
777 if token_info:
778 if method in ("PUT", "PATCH", "POST") and isinstance(outdata, dict):
779 for logging_id in ("id", "op_id", "nsilcmop_id", "nslcmop_id"):
780 if outdata.get(logging_id):
sousaedu80135b92021-02-17 15:05:18 +0100781 cherrypy.request.login += ";{}={}".format(
782 logging_id, outdata[logging_id][:36]
783 )
tierno1d213f42020-04-24 14:02:51 +0000784
785
786def _start_service():
787 """
788 Callback function called when cherrypy.engine starts
789 Override configuration with env variables
790 Set database, storage, message configuration
791 Init database with admin/admin user password
792 """
tierno70eeb182020-10-19 16:38:00 +0000793 global ro_server, vim_admin_thread
tierno1d213f42020-04-24 14:02:51 +0000794 # global vim_threads
795 cherrypy.log.error("Starting osm_ng_ro")
796 # update general cherrypy configuration
797 update_dict = {}
sousaedu80135b92021-02-17 15:05:18 +0100798 engine_config = cherrypy.tree.apps["/ro"].config
tierno1d213f42020-04-24 14:02:51 +0000799
tierno1d213f42020-04-24 14:02:51 +0000800 for k, v in environ.items():
801 if not k.startswith("OSMRO_"):
802 continue
sousaedu80135b92021-02-17 15:05:18 +0100803
tierno1d213f42020-04-24 14:02:51 +0000804 k1, _, k2 = k[6:].lower().partition("_")
sousaedu80135b92021-02-17 15:05:18 +0100805
tierno1d213f42020-04-24 14:02:51 +0000806 if not k2:
807 continue
sousaedu80135b92021-02-17 15:05:18 +0100808
tierno1d213f42020-04-24 14:02:51 +0000809 try:
810 if k1 in ("server", "test", "auth", "log"):
811 # update [global] configuration
sousaedu80135b92021-02-17 15:05:18 +0100812 update_dict[k1 + "." + k2] = yaml.safe_load(v)
tierno1d213f42020-04-24 14:02:51 +0000813 elif k1 == "static":
814 # update [/static] configuration
815 engine_config["/static"]["tools.staticdir." + k2] = yaml.safe_load(v)
816 elif k1 == "tools":
817 # update [/] configuration
sousaedu80135b92021-02-17 15:05:18 +0100818 engine_config["/"]["tools." + k2.replace("_", ".")] = yaml.safe_load(v)
tierno1d213f42020-04-24 14:02:51 +0000819 elif k1 in ("message", "database", "storage", "authentication"):
tierno70eeb182020-10-19 16:38:00 +0000820 engine_config[k1][k2] = yaml.safe_load(v)
tierno1d213f42020-04-24 14:02:51 +0000821
822 except Exception as e:
823 raise RoException("Cannot load env '{}': {}".format(k, e))
824
825 if update_dict:
826 cherrypy.config.update(update_dict)
827 engine_config["global"].update(update_dict)
828
829 # logging cherrypy
sousaedu80135b92021-02-17 15:05:18 +0100830 log_format_simple = (
831 "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(message)s"
832 )
833 log_formatter_simple = logging.Formatter(
834 log_format_simple, datefmt="%Y-%m-%dT%H:%M:%S"
835 )
tierno1d213f42020-04-24 14:02:51 +0000836 logger_server = logging.getLogger("cherrypy.error")
837 logger_access = logging.getLogger("cherrypy.access")
838 logger_cherry = logging.getLogger("cherrypy")
sousaedue493e9b2021-02-09 15:30:01 +0100839 logger = logging.getLogger("ro")
tierno1d213f42020-04-24 14:02:51 +0000840
841 if "log.file" in engine_config["global"]:
sousaedu80135b92021-02-17 15:05:18 +0100842 file_handler = logging.handlers.RotatingFileHandler(
843 engine_config["global"]["log.file"], maxBytes=100e6, backupCount=9, delay=0
844 )
tierno1d213f42020-04-24 14:02:51 +0000845 file_handler.setFormatter(log_formatter_simple)
846 logger_cherry.addHandler(file_handler)
sousaedue493e9b2021-02-09 15:30:01 +0100847 logger.addHandler(file_handler)
sousaedu80135b92021-02-17 15:05:18 +0100848
tierno1d213f42020-04-24 14:02:51 +0000849 # log always to standard output
sousaedu80135b92021-02-17 15:05:18 +0100850 for format_, logger in {
851 "ro.server %(filename)s:%(lineno)s": logger_server,
852 "ro.access %(filename)s:%(lineno)s": logger_access,
853 "%(name)s %(filename)s:%(lineno)s": logger,
854 }.items():
tierno1d213f42020-04-24 14:02:51 +0000855 log_format_cherry = "%(asctime)s %(levelname)s {} %(message)s".format(format_)
sousaedu80135b92021-02-17 15:05:18 +0100856 log_formatter_cherry = logging.Formatter(
857 log_format_cherry, datefmt="%Y-%m-%dT%H:%M:%S"
858 )
tierno1d213f42020-04-24 14:02:51 +0000859 str_handler = logging.StreamHandler()
860 str_handler.setFormatter(log_formatter_cherry)
861 logger.addHandler(str_handler)
862
863 if engine_config["global"].get("log.level"):
864 logger_cherry.setLevel(engine_config["global"]["log.level"])
sousaedue493e9b2021-02-09 15:30:01 +0100865 logger.setLevel(engine_config["global"]["log.level"])
sousaedu80135b92021-02-17 15:05:18 +0100866
tierno1d213f42020-04-24 14:02:51 +0000867 # logging other modules
sousaedu80135b92021-02-17 15:05:18 +0100868 for k1, logname in {
869 "message": "ro.msg",
870 "database": "ro.db",
871 "storage": "ro.fs",
872 }.items():
tierno1d213f42020-04-24 14:02:51 +0000873 engine_config[k1]["logger_name"] = logname
874 logger_module = logging.getLogger(logname)
sousaedu80135b92021-02-17 15:05:18 +0100875
tierno1d213f42020-04-24 14:02:51 +0000876 if "logfile" in engine_config[k1]:
sousaedu80135b92021-02-17 15:05:18 +0100877 file_handler = logging.handlers.RotatingFileHandler(
878 engine_config[k1]["logfile"], maxBytes=100e6, backupCount=9, delay=0
879 )
tierno1d213f42020-04-24 14:02:51 +0000880 file_handler.setFormatter(log_formatter_simple)
881 logger_module.addHandler(file_handler)
sousaedu80135b92021-02-17 15:05:18 +0100882
tierno1d213f42020-04-24 14:02:51 +0000883 if "loglevel" in engine_config[k1]:
884 logger_module.setLevel(engine_config[k1]["loglevel"])
885 # TODO add more entries, e.g.: storage
886
887 engine_config["assignment"] = {}
888 # ^ 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 +0100889 cherrypy.tree.apps["/ro"].root.ns.start(engine_config)
890 cherrypy.tree.apps["/ro"].root.authenticator.start(engine_config)
891 cherrypy.tree.apps["/ro"].root.ns.init_db(target_version=database_version)
tierno1d213f42020-04-24 14:02:51 +0000892
893 # # start subscriptions thread:
tierno70eeb182020-10-19 16:38:00 +0000894 vim_admin_thread = VimAdminThread(config=engine_config, engine=ro_server.ns)
895 vim_admin_thread.start()
tierno1d213f42020-04-24 14:02:51 +0000896 # # Do not capture except SubscriptionException
897
tierno70eeb182020-10-19 16:38:00 +0000898 # backend = engine_config["authentication"]["backend"]
899 # cherrypy.log.error("Starting OSM NBI Version '{} {}' with '{}' authentication backend"
900 # .format(ro_version, ro_version_date, backend))
tierno1d213f42020-04-24 14:02:51 +0000901
902
903def _stop_service():
904 """
905 Callback function called when cherrypy.engine stops
906 TODO: Ending database connections.
907 """
tierno70eeb182020-10-19 16:38:00 +0000908 global vim_admin_thread
sousaedu80135b92021-02-17 15:05:18 +0100909
tierno70eeb182020-10-19 16:38:00 +0000910 # terminate vim_admin_thread
911 if vim_admin_thread:
912 vim_admin_thread.terminate()
sousaedu80135b92021-02-17 15:05:18 +0100913
tierno70eeb182020-10-19 16:38:00 +0000914 vim_admin_thread = None
sousaedu80135b92021-02-17 15:05:18 +0100915 cherrypy.tree.apps["/ro"].root.ns.stop()
tierno1d213f42020-04-24 14:02:51 +0000916 cherrypy.log.error("Stopping osm_ng_ro")
917
918
919def ro_main(config_file):
920 global ro_server
sousaedu80135b92021-02-17 15:05:18 +0100921
tierno1d213f42020-04-24 14:02:51 +0000922 ro_server = Server()
sousaedu80135b92021-02-17 15:05:18 +0100923 cherrypy.engine.subscribe("start", _start_service)
924 cherrypy.engine.subscribe("stop", _stop_service)
925 cherrypy.quickstart(ro_server, "/ro", config_file)
tierno1d213f42020-04-24 14:02:51 +0000926
927
928def usage():
sousaedu80135b92021-02-17 15:05:18 +0100929 print(
930 """Usage: {} [options]
tierno1d213f42020-04-24 14:02:51 +0000931 -c|--config [configuration_file]: loads the configuration file (default: ./ro.cfg)
932 -h|--help: shows this help
sousaedu80135b92021-02-17 15:05:18 +0100933 """.format(
934 sys.argv[0]
935 )
936 )
tierno1d213f42020-04-24 14:02:51 +0000937 # --log-socket-host HOST: send logs to this host")
938 # --log-socket-port PORT: send logs using this port (default: 9022)")
939
940
sousaedu80135b92021-02-17 15:05:18 +0100941if __name__ == "__main__":
tierno1d213f42020-04-24 14:02:51 +0000942 try:
943 # load parameters and configuration
944 opts, args = getopt.getopt(sys.argv[1:], "hvc:", ["config=", "help"])
945 # TODO add "log-socket-host=", "log-socket-port=", "log-file="
946 config_file = None
sousaedu80135b92021-02-17 15:05:18 +0100947
tierno1d213f42020-04-24 14:02:51 +0000948 for o, a in opts:
949 if o in ("-h", "--help"):
950 usage()
951 sys.exit()
952 elif o in ("-c", "--config"):
953 config_file = a
954 else:
955 assert False, "Unhandled option"
sousaedu80135b92021-02-17 15:05:18 +0100956
tierno1d213f42020-04-24 14:02:51 +0000957 if config_file:
958 if not path.isfile(config_file):
sousaedu80135b92021-02-17 15:05:18 +0100959 print(
960 "configuration file '{}' that not exist".format(config_file),
961 file=sys.stderr,
962 )
tierno1d213f42020-04-24 14:02:51 +0000963 exit(1)
964 else:
sousaedu80135b92021-02-17 15:05:18 +0100965 for config_file in (
966 path.dirname(__file__) + "/ro.cfg",
967 "./ro.cfg",
968 "/etc/osm/ro.cfg",
969 ):
tierno1d213f42020-04-24 14:02:51 +0000970 if path.isfile(config_file):
971 break
972 else:
sousaedu80135b92021-02-17 15:05:18 +0100973 print(
974 "No configuration file 'ro.cfg' found neither at local folder nor at /etc/osm/",
975 file=sys.stderr,
976 )
tierno1d213f42020-04-24 14:02:51 +0000977 exit(1)
sousaedu80135b92021-02-17 15:05:18 +0100978
tierno1d213f42020-04-24 14:02:51 +0000979 ro_main(config_file)
tierno70eeb182020-10-19 16:38:00 +0000980 except KeyboardInterrupt:
981 print("KeyboardInterrupt. Finishing", file=sys.stderr)
tierno1d213f42020-04-24 14:02:51 +0000982 except getopt.GetoptError as e:
983 print(str(e), file=sys.stderr)
984 # usage()
985 exit(1)