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