Coverage for NG-RO/osm_ng_ro/ro_main.py: 0%

459 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2024-06-28 09:51 +0000

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 

22from codecs import getreader 

23import getopt 

24from http import HTTPStatus 

25import json 

26import logging 

27import logging.handlers 

28from os import environ, path 

29import sys 

30import time 

31 

32import cherrypy 

33from osm_common.dbbase import DbException 

34from osm_common.fsbase import FsException 

35from osm_common.msgbase import MsgException 

36from osm_ng_ro import version as ro_version, version_date as ro_version_date 

37import osm_ng_ro.html_out as html 

38from osm_ng_ro.monitor import start_monitoring, stop_monitoring 

39from osm_ng_ro.ns import Ns, NsException 

40from osm_ng_ro.validation import ValidationError 

41from osm_ng_ro.vim_admin import VimAdminThread 

42import yaml 

43 

44 

45__author__ = "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>" 

46__version__ = "0.1." # file version, not NBI version 

47version_date = "May 2020" 

48 

49database_version = "1.2" 

50auth_database_version = "1.0" 

51ro_server = None # instance of Server class 

52vim_admin_thread = None # instance of VimAdminThread class 

53 

54# vim_threads = None # instance of VimThread class 

55 

56""" 

57RO North Bound Interface 

58URL: /ro GET POST PUT DELETE PATCH 

59 /ns/v1/deploy O 

60 /<nsrs_id> O O O 

61 /<action_id> O 

62 /cancel O 

63 

64""" 

65 

66valid_query_string = ("ADMIN", "SET_PROJECT", "FORCE", "PUBLIC") 

67# ^ Contains possible administrative query string words: 

68# ADMIN=True(by default)|Project|Project-list: See all elements, or elements of a project 

69# (not owned by my session project). 

70# PUBLIC=True(by default)|False: See/hide public elements. Set/Unset a topic to be public 

71# FORCE=True(by default)|False: Force edition/deletion operations 

72# SET_PROJECT=Project|Project-list: Add/Delete the topic to the projects portfolio 

73 

74valid_url_methods = { 

75 # contains allowed URL and methods, and the role_permission name 

76 "admin": { 

77 "v1": { 

78 "tokens": { 

79 "METHODS": ("POST",), 

80 "ROLE_PERMISSION": "tokens:", 

81 "<ID>": {"METHODS": ("DELETE",), "ROLE_PERMISSION": "tokens:id:"}, 

82 }, 

83 } 

84 }, 

85 "ns": { 

86 "v1": { 

87 "rebuild": { 

88 "METHODS": ("POST",), 

89 "ROLE_PERMISSION": "rebuild:", 

90 "<ID>": { 

91 "METHODS": ("POST",), 

92 "ROLE_PERMISSION": "rebuild:id:", 

93 }, 

94 }, 

95 "start": { 

96 "METHODS": ("POST",), 

97 "ROLE_PERMISSION": "start:", 

98 "<ID>": { 

99 "METHODS": ("POST",), 

100 "ROLE_PERMISSION": "start:id:", 

101 }, 

102 }, 

103 "stop": { 

104 "METHODS": ("POST",), 

105 "ROLE_PERMISSION": "stop:", 

106 "<ID>": { 

107 "METHODS": ("POST",), 

108 "ROLE_PERMISSION": "stop:id:", 

109 }, 

110 }, 

111 "deploy": { 

112 "METHODS": ("GET",), 

113 "ROLE_PERMISSION": "deploy:", 

114 "<ID>": { 

115 "METHODS": ("GET", "POST", "DELETE"), 

116 "ROLE_PERMISSION": "deploy:id:", 

117 "<ID>": { 

118 "METHODS": ("GET",), 

119 "ROLE_PERMISSION": "deploy:id:id:", 

120 "cancel": { 

121 "METHODS": ("POST",), 

122 "ROLE_PERMISSION": "deploy:id:id:cancel", 

123 }, 

124 }, 

125 }, 

126 }, 

127 "recreate": { 

128 "<ID>": { 

129 "METHODS": ("POST"), 

130 "ROLE_PERMISSION": "recreate:id:", 

131 "<ID>": { 

132 "METHODS": ("GET",), 

133 "ROLE_PERMISSION": "recreate:id:id:", 

134 }, 

135 }, 

136 }, 

137 "migrate": { 

138 "<ID>": { 

139 "METHODS": ("POST"), 

140 "ROLE_PERMISSION": "migrate:id:", 

141 "<ID>": { 

142 "METHODS": ("GET",), 

143 "ROLE_PERMISSION": "migrate:id:id:", 

144 }, 

145 }, 

146 }, 

147 "verticalscale": { 

148 "<ID>": { 

149 "METHODS": ("POST"), 

150 "ROLE_PERMISSION": "verticalscale:id:", 

151 "<ID>": { 

152 "METHODS": ("GET",), 

153 "ROLE_PERMISSION": "verticalscale:id:id:", 

154 }, 

155 }, 

156 }, 

157 } 

158 }, 

159} 

160 

161 

162class RoException(Exception): 

163 def __init__(self, message, http_code=HTTPStatus.METHOD_NOT_ALLOWED): 

164 Exception.__init__(self, message) 

165 self.http_code = http_code 

166 

167 

168class AuthException(RoException): 

169 pass 

170 

171 

172class Authenticator: 

173 def __init__(self, valid_url_methods, valid_query_string): 

174 self.valid_url_methods = valid_url_methods 

175 self.valid_query_string = valid_query_string 

176 

177 def authorize(self, *args, **kwargs): 

178 return {"token": "ok", "id": "ok"} 

179 

180 def new_token(self, token_info, indata, remote): 

181 return {"token": "ok", "id": "ok", "remote": remote} 

182 

183 def del_token(self, token_id): 

184 pass 

185 

186 def start(self, engine_config): 

187 pass 

188 

189 

190class Server(object): 

191 instance = 0 

192 # to decode bytes to str 

193 reader = getreader("utf-8") 

194 

195 def __init__(self): 

196 self.instance += 1 

197 self.authenticator = Authenticator(valid_url_methods, valid_query_string) 

198 self.ns = Ns() 

199 self.map_operation = { 

200 "token:post": self.new_token, 

201 "token:id:delete": self.del_token, 

202 "deploy:get": self.ns.get_deploy, 

203 "deploy:id:get": self.ns.get_actions, 

204 "deploy:id:post": self.ns.deploy, 

205 "deploy:id:delete": self.ns.delete, 

206 "deploy:id:id:get": self.ns.status, 

207 "deploy:id:id:cancel:post": self.ns.cancel, 

208 "rebuild:id:post": self.ns.rebuild_start_stop, 

209 "start:id:post": self.ns.rebuild_start_stop, 

210 "stop:id:post": self.ns.rebuild_start_stop, 

211 "recreate:id:post": self.ns.recreate, 

212 "recreate:id:id:get": self.ns.recreate_status, 

213 "migrate:id:post": self.ns.migrate, 

214 "verticalscale:id:post": self.ns.verticalscale, 

215 } 

216 

217 def _format_in(self, kwargs): 

218 error_text = "" 

219 try: 

220 indata = None 

221 

222 if cherrypy.request.body.length: 

223 error_text = "Invalid input format " 

224 

225 if "Content-Type" in cherrypy.request.headers: 

226 if "application/json" in cherrypy.request.headers["Content-Type"]: 

227 error_text = "Invalid json format " 

228 indata = json.load(self.reader(cherrypy.request.body)) 

229 cherrypy.request.headers.pop("Content-File-MD5", None) 

230 elif "application/yaml" in cherrypy.request.headers["Content-Type"]: 

231 error_text = "Invalid yaml format " 

232 indata = yaml.safe_load(cherrypy.request.body) 

233 cherrypy.request.headers.pop("Content-File-MD5", None) 

234 elif ( 

235 "application/binary" in cherrypy.request.headers["Content-Type"] 

236 or "application/gzip" 

237 in cherrypy.request.headers["Content-Type"] 

238 or "application/zip" in cherrypy.request.headers["Content-Type"] 

239 or "text/plain" in cherrypy.request.headers["Content-Type"] 

240 ): 

241 indata = cherrypy.request.body # .read() 

242 elif ( 

243 "multipart/form-data" 

244 in cherrypy.request.headers["Content-Type"] 

245 ): 

246 if "descriptor_file" in kwargs: 

247 filecontent = kwargs.pop("descriptor_file") 

248 

249 if not filecontent.file: 

250 raise RoException( 

251 "empty file or content", HTTPStatus.BAD_REQUEST 

252 ) 

253 

254 indata = filecontent.file # .read() 

255 

256 if filecontent.content_type.value: 

257 cherrypy.request.headers[ 

258 "Content-Type" 

259 ] = filecontent.content_type.value 

260 else: 

261 # raise cherrypy.HTTPError(HTTPStatus.Not_Acceptable, 

262 # "Only 'Content-Type' of type 'application/json' or 

263 # 'application/yaml' for input format are available") 

264 error_text = "Invalid yaml format " 

265 indata = yaml.safe_load(cherrypy.request.body) 

266 cherrypy.request.headers.pop("Content-File-MD5", None) 

267 else: 

268 error_text = "Invalid yaml format " 

269 indata = yaml.safe_load(cherrypy.request.body) 

270 cherrypy.request.headers.pop("Content-File-MD5", None) 

271 

272 if not indata: 

273 indata = {} 

274 

275 format_yaml = False 

276 if cherrypy.request.headers.get("Query-String-Format") == "yaml": 

277 format_yaml = True 

278 

279 for k, v in kwargs.items(): 

280 if isinstance(v, str): 

281 if v == "": 

282 kwargs[k] = None 

283 elif format_yaml: 

284 try: 

285 kwargs[k] = yaml.safe_load(v) 

286 except Exception as yaml_error: 

287 logging.exception( 

288 f"{yaml_error} occured while parsing the yaml" 

289 ) 

290 elif ( 

291 k.endswith(".gt") 

292 or k.endswith(".lt") 

293 or k.endswith(".gte") 

294 or k.endswith(".lte") 

295 ): 

296 try: 

297 kwargs[k] = int(v) 

298 except Exception: 

299 try: 

300 kwargs[k] = float(v) 

301 except Exception as keyword_error: 

302 logging.exception( 

303 f"{keyword_error} occured while getting the keyword arguments" 

304 ) 

305 elif v.find(",") > 0: 

306 kwargs[k] = v.split(",") 

307 elif isinstance(v, (list, tuple)): 

308 for index in range(0, len(v)): 

309 if v[index] == "": 

310 v[index] = None 

311 elif format_yaml: 

312 try: 

313 v[index] = yaml.safe_load(v[index]) 

314 except Exception as error: 

315 logging.exception( 

316 f"{error} occured while parsing the yaml" 

317 ) 

318 

319 return indata 

320 except (ValueError, yaml.YAMLError) as exc: 

321 raise RoException(error_text + str(exc), HTTPStatus.BAD_REQUEST) 

322 except KeyError as exc: 

323 raise RoException("Query string error: " + str(exc), HTTPStatus.BAD_REQUEST) 

324 except Exception as exc: 

325 raise RoException(error_text + str(exc), HTTPStatus.BAD_REQUEST) 

326 

327 @staticmethod 

328 def _format_out(data, token_info=None, _format=None): 

329 """ 

330 return string of dictionary data according to requested json, yaml, xml. By default json 

331 :param data: response to be sent. Can be a dict, text or file 

332 :param token_info: Contains among other username and project 

333 :param _format: The format to be set as Content-Type if data is a file 

334 :return: None 

335 """ 

336 accept = cherrypy.request.headers.get("Accept") 

337 

338 if data is None: 

339 if accept and "text/html" in accept: 

340 return html.format( 

341 data, cherrypy.request, cherrypy.response, token_info 

342 ) 

343 

344 # cherrypy.response.status = HTTPStatus.NO_CONTENT.value 

345 return 

346 elif hasattr(data, "read"): # file object 

347 if _format: 

348 cherrypy.response.headers["Content-Type"] = _format 

349 elif "b" in data.mode: # binariy asssumig zip 

350 cherrypy.response.headers["Content-Type"] = "application/zip" 

351 else: 

352 cherrypy.response.headers["Content-Type"] = "text/plain" 

353 

354 # TODO check that cherrypy close file. If not implement pending things to close per thread next 

355 return data 

356 

357 if accept: 

358 if "application/json" in accept: 

359 cherrypy.response.headers[ 

360 "Content-Type" 

361 ] = "application/json; charset=utf-8" 

362 a = json.dumps(data, indent=4) + "\n" 

363 

364 return a.encode("utf8") 

365 elif "text/html" in accept: 

366 return html.format( 

367 data, cherrypy.request, cherrypy.response, token_info 

368 ) 

369 elif ( 

370 "application/yaml" in accept 

371 or "*/*" in accept 

372 or "text/plain" in accept 

373 ): 

374 pass 

375 # if there is not any valid accept, raise an error. But if response is already an error, format in yaml 

376 elif cherrypy.response.status >= 400: 

377 raise cherrypy.HTTPError( 

378 HTTPStatus.NOT_ACCEPTABLE.value, 

379 "Only 'Accept' of type 'application/json' or 'application/yaml' " 

380 "for output format are available", 

381 ) 

382 

383 cherrypy.response.headers["Content-Type"] = "application/yaml" 

384 

385 return yaml.safe_dump( 

386 data, 

387 explicit_start=True, 

388 indent=4, 

389 default_flow_style=False, 

390 tags=False, 

391 encoding="utf-8", 

392 allow_unicode=True, 

393 ) # , canonical=True, default_style='"' 

394 

395 @cherrypy.expose 

396 def index(self, *args, **kwargs): 

397 token_info = None 

398 

399 try: 

400 if cherrypy.request.method == "GET": 

401 token_info = self.authenticator.authorize() 

402 outdata = token_info # Home page 

403 else: 

404 raise cherrypy.HTTPError( 

405 HTTPStatus.METHOD_NOT_ALLOWED.value, 

406 "Method {} not allowed for tokens".format(cherrypy.request.method), 

407 ) 

408 

409 return self._format_out(outdata, token_info) 

410 except (NsException, AuthException) as e: 

411 # cherrypy.log("index Exception {}".format(e)) 

412 cherrypy.response.status = e.http_code.value 

413 

414 return self._format_out("Welcome to OSM!", token_info) 

415 

416 @cherrypy.expose 

417 def version(self, *args, **kwargs): 

418 # TODO consider to remove and provide version using the static version file 

419 try: 

420 if cherrypy.request.method != "GET": 

421 raise RoException( 

422 "Only method GET is allowed", 

423 HTTPStatus.METHOD_NOT_ALLOWED, 

424 ) 

425 elif args or kwargs: 

426 raise RoException( 

427 "Invalid URL or query string for version", 

428 HTTPStatus.METHOD_NOT_ALLOWED, 

429 ) 

430 

431 # TODO include version of other modules, pick up from some kafka admin message 

432 osm_ng_ro_version = {"version": ro_version, "date": ro_version_date} 

433 

434 return self._format_out(osm_ng_ro_version) 

435 except RoException as e: 

436 cherrypy.response.status = e.http_code.value 

437 problem_details = { 

438 "code": e.http_code.name, 

439 "status": e.http_code.value, 

440 "detail": str(e), 

441 } 

442 

443 return self._format_out(problem_details, None) 

444 

445 def new_token(self, engine_session, indata, *args, **kwargs): 

446 token_info = None 

447 

448 try: 

449 token_info = self.authenticator.authorize() 

450 except Exception: 

451 token_info = None 

452 

453 if kwargs: 

454 indata.update(kwargs) 

455 

456 # This is needed to log the user when authentication fails 

457 cherrypy.request.login = "{}".format(indata.get("username", "-")) 

458 token_info = self.authenticator.new_token( 

459 token_info, indata, cherrypy.request.remote 

460 ) 

461 cherrypy.session["Authorization"] = token_info["id"] 

462 self._set_location_header("admin", "v1", "tokens", token_info["id"]) 

463 # for logging 

464 

465 # cherrypy.response.cookie["Authorization"] = outdata["id"] 

466 # cherrypy.response.cookie["Authorization"]['expires'] = 3600 

467 

468 return token_info, token_info["id"], True 

469 

470 def del_token(self, engine_session, indata, version, _id, *args, **kwargs): 

471 token_id = _id 

472 

473 if not token_id and "id" in kwargs: 

474 token_id = kwargs["id"] 

475 elif not token_id: 

476 token_info = self.authenticator.authorize() 

477 # for logging 

478 token_id = token_info["id"] 

479 

480 self.authenticator.del_token(token_id) 

481 token_info = None 

482 cherrypy.session["Authorization"] = "logout" 

483 # cherrypy.response.cookie["Authorization"] = token_id 

484 # cherrypy.response.cookie["Authorization"]['expires'] = 0 

485 

486 return None, None, True 

487 

488 @cherrypy.expose 

489 def test(self, *args, **kwargs): 

490 if not cherrypy.config.get("server.enable_test") or ( 

491 isinstance(cherrypy.config["server.enable_test"], str) 

492 and cherrypy.config["server.enable_test"].lower() == "false" 

493 ): 

494 cherrypy.response.status = HTTPStatus.METHOD_NOT_ALLOWED.value 

495 

496 return "test URL is disabled" 

497 

498 thread_info = None 

499 

500 if args and args[0] == "help": 

501 return ( 

502 "<html><pre>\ninit\nfile/<name> download file\ndb-clear/table\nfs-clear[/folder]\nlogin\nlogin2\n" 

503 "sleep/<time>\nmessage/topic\n</pre></html>" 

504 ) 

505 elif args and args[0] == "init": 

506 try: 

507 # self.ns.load_dbase(cherrypy.request.app.config) 

508 self.ns.create_admin() 

509 

510 return "Done. User 'admin', password 'admin' created" 

511 except Exception: 

512 cherrypy.response.status = HTTPStatus.FORBIDDEN.value 

513 

514 return self._format_out("Database already initialized") 

515 elif args and args[0] == "file": 

516 return cherrypy.lib.static.serve_file( 

517 cherrypy.tree.apps["/ro"].config["storage"]["path"] + "/" + args[1], 

518 "text/plain", 

519 "attachment", 

520 ) 

521 elif args and args[0] == "file2": 

522 f_path = cherrypy.tree.apps["/ro"].config["storage"]["path"] + "/" + args[1] 

523 f = open(f_path, "r") 

524 cherrypy.response.headers["Content-type"] = "text/plain" 

525 return f 

526 

527 elif len(args) == 2 and args[0] == "db-clear": 

528 deleted_info = self.ns.db.del_list(args[1], kwargs) 

529 return "{} {} deleted\n".format(deleted_info["deleted"], args[1]) 

530 elif len(args) and args[0] == "fs-clear": 

531 if len(args) >= 2: 

532 folders = (args[1],) 

533 else: 

534 folders = self.ns.fs.dir_ls(".") 

535 

536 for folder in folders: 

537 self.ns.fs.file_delete(folder) 

538 

539 return ",".join(folders) + " folders deleted\n" 

540 elif args and args[0] == "login": 

541 if not cherrypy.request.headers.get("Authorization"): 

542 cherrypy.response.headers[ 

543 "WWW-Authenticate" 

544 ] = 'Basic realm="Access to OSM site", charset="UTF-8"' 

545 cherrypy.response.status = HTTPStatus.UNAUTHORIZED.value 

546 elif args and args[0] == "login2": 

547 if not cherrypy.request.headers.get("Authorization"): 

548 cherrypy.response.headers[ 

549 "WWW-Authenticate" 

550 ] = 'Bearer realm="Access to OSM site"' 

551 cherrypy.response.status = HTTPStatus.UNAUTHORIZED.value 

552 elif args and args[0] == "sleep": 

553 sleep_time = 5 

554 

555 try: 

556 sleep_time = int(args[1]) 

557 except Exception: 

558 cherrypy.response.status = HTTPStatus.FORBIDDEN.value 

559 return self._format_out("Database already initialized") 

560 

561 thread_info = cherrypy.thread_data 

562 print(thread_info) 

563 time.sleep(sleep_time) 

564 # thread_info 

565 elif len(args) >= 2 and args[0] == "message": 

566 main_topic = args[1] 

567 return_text = "<html><pre>{} ->\n".format(main_topic) 

568 

569 try: 

570 if cherrypy.request.method == "POST": 

571 to_send = yaml.safe_load(cherrypy.request.body) 

572 for k, v in to_send.items(): 

573 self.ns.msg.write(main_topic, k, v) 

574 return_text += " {}: {}\n".format(k, v) 

575 elif cherrypy.request.method == "GET": 

576 for k, v in kwargs.items(): 

577 self.ns.msg.write(main_topic, k, yaml.safe_load(v)) 

578 return_text += " {}: {}\n".format(k, yaml.safe_load(v)) 

579 except Exception as e: 

580 return_text += "Error: " + str(e) 

581 

582 return_text += "</pre></html>\n" 

583 

584 return return_text 

585 

586 return_text = ( 

587 "<html><pre>\nheaders:\n args: {}\n".format(args) 

588 + " kwargs: {}\n".format(kwargs) 

589 + " headers: {}\n".format(cherrypy.request.headers) 

590 + " path_info: {}\n".format(cherrypy.request.path_info) 

591 + " query_string: {}\n".format(cherrypy.request.query_string) 

592 + " session: {}\n".format(cherrypy.session) 

593 + " cookie: {}\n".format(cherrypy.request.cookie) 

594 + " method: {}\n".format(cherrypy.request.method) 

595 + " session: {}\n".format(cherrypy.session.get("fieldname")) 

596 + " body:\n" 

597 ) 

598 return_text += " length: {}\n".format(cherrypy.request.body.length) 

599 

600 if cherrypy.request.body.length: 

601 return_text += " content: {}\n".format( 

602 str( 

603 cherrypy.request.body.read( 

604 int(cherrypy.request.headers.get("Content-Length", 0)) 

605 ) 

606 ) 

607 ) 

608 

609 if thread_info: 

610 return_text += "thread: {}\n".format(thread_info) 

611 

612 return_text += "</pre></html>" 

613 

614 return return_text 

615 

616 @staticmethod 

617 def _check_valid_url_method(method, *args): 

618 if len(args) < 3: 

619 raise RoException( 

620 "URL must contain at least 'main_topic/version/topic'", 

621 HTTPStatus.METHOD_NOT_ALLOWED, 

622 ) 

623 

624 reference = valid_url_methods 

625 for arg in args: 

626 if arg is None: 

627 break 

628 

629 if not isinstance(reference, dict): 

630 raise RoException( 

631 "URL contains unexpected extra items '{}'".format(arg), 

632 HTTPStatus.METHOD_NOT_ALLOWED, 

633 ) 

634 

635 if arg in reference: 

636 reference = reference[arg] 

637 elif "<ID>" in reference: 

638 reference = reference["<ID>"] 

639 elif "*" in reference: 

640 # reference = reference["*"] 

641 break 

642 else: 

643 raise RoException( 

644 "Unexpected URL item {}".format(arg), 

645 HTTPStatus.METHOD_NOT_ALLOWED, 

646 ) 

647 

648 if "TODO" in reference and method in reference["TODO"]: 

649 raise RoException( 

650 "Method {} not supported yet for this URL".format(method), 

651 HTTPStatus.NOT_IMPLEMENTED, 

652 ) 

653 elif "METHODS" not in reference or method not in reference["METHODS"]: 

654 raise RoException( 

655 "Method {} not supported for this URL".format(method), 

656 HTTPStatus.METHOD_NOT_ALLOWED, 

657 ) 

658 

659 return reference["ROLE_PERMISSION"] + method.lower() 

660 

661 @staticmethod 

662 def _set_location_header(main_topic, version, topic, id): 

663 """ 

664 Insert response header Location with the URL of created item base on URL params 

665 :param main_topic: 

666 :param version: 

667 :param topic: 

668 :param id: 

669 :return: None 

670 """ 

671 # Use cherrypy.request.base for absoluted path and make use of request.header HOST just in case behind aNAT 

672 cherrypy.response.headers["Location"] = "/ro/{}/{}/{}/{}".format( 

673 main_topic, version, topic, id 

674 ) 

675 

676 return 

677 

678 @cherrypy.expose 

679 def default( 

680 self, 

681 main_topic=None, 

682 version=None, 

683 topic=None, 

684 _id=None, 

685 _id2=None, 

686 *args, 

687 **kwargs, 

688 ): 

689 token_info = None 

690 outdata = {} 

691 _format = None 

692 method = "DONE" 

693 rollback = [] 

694 engine_session = None 

695 

696 try: 

697 if not main_topic or not version or not topic: 

698 raise RoException( 

699 "URL must contain at least 'main_topic/version/topic'", 

700 HTTPStatus.METHOD_NOT_ALLOWED, 

701 ) 

702 

703 if main_topic not in ( 

704 "admin", 

705 "ns", 

706 ): 

707 raise RoException( 

708 "URL main_topic '{}' not supported".format(main_topic), 

709 HTTPStatus.METHOD_NOT_ALLOWED, 

710 ) 

711 

712 if version != "v1": 

713 raise RoException( 

714 "URL version '{}' not supported".format(version), 

715 HTTPStatus.METHOD_NOT_ALLOWED, 

716 ) 

717 

718 if ( 

719 kwargs 

720 and "METHOD" in kwargs 

721 and kwargs["METHOD"] in ("PUT", "POST", "DELETE", "GET", "PATCH") 

722 ): 

723 method = kwargs.pop("METHOD") 

724 else: 

725 method = cherrypy.request.method 

726 

727 role_permission = self._check_valid_url_method( 

728 method, main_topic, version, topic, _id, _id2, *args, **kwargs 

729 ) 

730 # skip token validation if requesting a token 

731 indata = self._format_in(kwargs) 

732 

733 if main_topic != "admin" or topic != "tokens": 

734 token_info = self.authenticator.authorize(role_permission, _id) 

735 

736 outdata, created_id, done = self.map_operation[role_permission]( 

737 engine_session, indata, version, _id, _id2, *args, *kwargs 

738 ) 

739 

740 if created_id: 

741 self._set_location_header(main_topic, version, topic, _id) 

742 

743 cherrypy.response.status = ( 

744 HTTPStatus.ACCEPTED.value 

745 if not done 

746 else HTTPStatus.OK.value 

747 if outdata is not None 

748 else HTTPStatus.NO_CONTENT.value 

749 ) 

750 

751 return self._format_out(outdata, token_info, _format) 

752 except Exception as e: 

753 if isinstance( 

754 e, 

755 ( 

756 RoException, 

757 NsException, 

758 DbException, 

759 FsException, 

760 MsgException, 

761 AuthException, 

762 ValidationError, 

763 ), 

764 ): 

765 http_code_value = cherrypy.response.status = e.http_code.value 

766 http_code_name = e.http_code.name 

767 cherrypy.log("Exception {}".format(e)) 

768 else: 

769 http_code_value = ( 

770 cherrypy.response.status 

771 ) = HTTPStatus.BAD_REQUEST.value # INTERNAL_SERVER_ERROR 

772 cherrypy.log("CRITICAL: Exception {}".format(e), traceback=True) 

773 http_code_name = HTTPStatus.BAD_REQUEST.name 

774 

775 if hasattr(outdata, "close"): # is an open file 

776 outdata.close() 

777 

778 error_text = str(e) 

779 rollback.reverse() 

780 

781 for rollback_item in rollback: 

782 try: 

783 if rollback_item.get("operation") == "set": 

784 self.ns.db.set_one( 

785 rollback_item["topic"], 

786 {"_id": rollback_item["_id"]}, 

787 rollback_item["content"], 

788 fail_on_empty=False, 

789 ) 

790 else: 

791 self.ns.db.del_one( 

792 rollback_item["topic"], 

793 {"_id": rollback_item["_id"]}, 

794 fail_on_empty=False, 

795 ) 

796 except Exception as e2: 

797 rollback_error_text = "Rollback Exception {}: {}".format( 

798 rollback_item, e2 

799 ) 

800 cherrypy.log(rollback_error_text) 

801 error_text += ". " + rollback_error_text 

802 

803 # if isinstance(e, MsgException): 

804 # error_text = "{} has been '{}' but other modules cannot be informed because an error on bus".format( 

805 # engine_topic[:-1], method, error_text) 

806 problem_details = { 

807 "code": http_code_name, 

808 "status": http_code_value, 

809 "detail": error_text, 

810 } 

811 

812 return self._format_out(problem_details, token_info) 

813 # raise cherrypy.HTTPError(e.http_code.value, str(e)) 

814 finally: 

815 if token_info: 

816 if method in ("PUT", "PATCH", "POST") and isinstance(outdata, dict): 

817 for logging_id in ("id", "op_id", "nsilcmop_id", "nslcmop_id"): 

818 if outdata.get(logging_id): 

819 cherrypy.request.login += ";{}={}".format( 

820 logging_id, outdata[logging_id][:36] 

821 ) 

822 

823 

824def _start_service(): 

825 """ 

826 Callback function called when cherrypy.engine starts 

827 Override configuration with env variables 

828 Set database, storage, message configuration 

829 Init database with admin/admin user password 

830 """ 

831 global ro_server, vim_admin_thread 

832 # global vim_threads 

833 cherrypy.log.error("Starting osm_ng_ro") 

834 # update general cherrypy configuration 

835 update_dict = {} 

836 engine_config = cherrypy.tree.apps["/ro"].config 

837 

838 for k, v in environ.items(): 

839 if not k.startswith("OSMRO_"): 

840 continue 

841 

842 k1, _, k2 = k[6:].lower().partition("_") 

843 

844 if not k2: 

845 continue 

846 

847 try: 

848 if k1 in ("server", "test", "auth", "log"): 

849 # update [global] configuration 

850 update_dict[k1 + "." + k2] = yaml.safe_load(v) 

851 elif k1 == "static": 

852 # update [/static] configuration 

853 engine_config["/static"]["tools.staticdir." + k2] = yaml.safe_load(v) 

854 elif k1 == "tools": 

855 # update [/] configuration 

856 engine_config["/"]["tools." + k2.replace("_", ".")] = yaml.safe_load(v) 

857 elif k1 in ("message", "database", "storage", "authentication", "period"): 

858 engine_config[k1][k2] = yaml.safe_load(v) 

859 

860 except Exception as e: 

861 raise RoException("Cannot load env '{}': {}".format(k, e)) 

862 

863 if update_dict: 

864 cherrypy.config.update(update_dict) 

865 engine_config["global"].update(update_dict) 

866 

867 # logging cherrypy 

868 log_format_simple = ( 

869 "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(message)s" 

870 ) 

871 log_formatter_simple = logging.Formatter( 

872 log_format_simple, datefmt="%Y-%m-%dT%H:%M:%S" 

873 ) 

874 logger_server = logging.getLogger("cherrypy.error") 

875 logger_access = logging.getLogger("cherrypy.access") 

876 logger_cherry = logging.getLogger("cherrypy") 

877 logger = logging.getLogger("ro") 

878 

879 if "log.file" in engine_config["global"]: 

880 file_handler = logging.handlers.RotatingFileHandler( 

881 engine_config["global"]["log.file"], maxBytes=100e6, backupCount=9, delay=0 

882 ) 

883 file_handler.setFormatter(log_formatter_simple) 

884 logger_cherry.addHandler(file_handler) 

885 logger.addHandler(file_handler) 

886 

887 # log always to standard output 

888 for format_, logger in { 

889 "ro.server %(filename)s:%(lineno)s": logger_server, 

890 "ro.access %(filename)s:%(lineno)s": logger_access, 

891 "%(name)s %(filename)s:%(lineno)s": logger, 

892 }.items(): 

893 log_format_cherry = "%(asctime)s %(levelname)s {} %(message)s".format(format_) 

894 log_formatter_cherry = logging.Formatter( 

895 log_format_cherry, datefmt="%Y-%m-%dT%H:%M:%S" 

896 ) 

897 str_handler = logging.StreamHandler() 

898 str_handler.setFormatter(log_formatter_cherry) 

899 logger.addHandler(str_handler) 

900 

901 if engine_config["global"].get("log.level"): 

902 logger_cherry.setLevel(engine_config["global"]["log.level"]) 

903 logger.setLevel(engine_config["global"]["log.level"]) 

904 

905 # logging other modules 

906 for k1, logname in { 

907 "message": "ro.msg", 

908 "database": "ro.db", 

909 "storage": "ro.fs", 

910 }.items(): 

911 engine_config[k1]["logger_name"] = logname 

912 logger_module = logging.getLogger(logname) 

913 

914 if "logfile" in engine_config[k1]: 

915 file_handler = logging.handlers.RotatingFileHandler( 

916 engine_config[k1]["logfile"], maxBytes=100e6, backupCount=9, delay=0 

917 ) 

918 file_handler.setFormatter(log_formatter_simple) 

919 logger_module.addHandler(file_handler) 

920 

921 if "loglevel" in engine_config[k1]: 

922 logger_module.setLevel(engine_config[k1]["loglevel"]) 

923 # TODO add more entries, e.g.: storage 

924 

925 engine_config["assignment"] = {} 

926 # ^ each VIM, SDNc will be assigned one worker id. Ns class will add items and VimThread will auto-assign 

927 cherrypy.tree.apps["/ro"].root.ns.start(engine_config) 

928 cherrypy.tree.apps["/ro"].root.authenticator.start(engine_config) 

929 cherrypy.tree.apps["/ro"].root.ns.init_db(target_version=database_version) 

930 

931 # # start subscriptions thread: 

932 vim_admin_thread = VimAdminThread(config=engine_config, engine=ro_server.ns) 

933 vim_admin_thread.start() 

934 start_monitoring(config=engine_config) 

935 

936 # # Do not capture except SubscriptionException 

937 

938 # backend = engine_config["authentication"]["backend"] 

939 # cherrypy.log.error("Starting OSM NBI Version '{} {}' with '{}' authentication backend" 

940 # .format(ro_version, ro_version_date, backend)) 

941 

942 

943def _stop_service(): 

944 """ 

945 Callback function called when cherrypy.engine stops 

946 TODO: Ending database connections. 

947 """ 

948 global vim_admin_thread 

949 

950 # terminate vim_admin_thread 

951 if vim_admin_thread: 

952 vim_admin_thread.terminate() 

953 stop_monitoring() 

954 vim_admin_thread = None 

955 cherrypy.tree.apps["/ro"].root.ns.stop() 

956 cherrypy.log.error("Stopping osm_ng_ro") 

957 

958 

959def ro_main(config_file): 

960 global ro_server 

961 

962 ro_server = Server() 

963 cherrypy.engine.subscribe("start", _start_service) 

964 cherrypy.engine.subscribe("stop", _stop_service) 

965 cherrypy.quickstart(ro_server, "/ro", config_file) 

966 

967 

968def usage(): 

969 print( 

970 """Usage: {} [options] 

971 -c|--config [configuration_file]: loads the configuration file (default: ./ro.cfg) 

972 -h|--help: shows this help 

973 """.format( 

974 sys.argv[0] 

975 ) 

976 ) 

977 # --log-socket-host HOST: send logs to this host") 

978 # --log-socket-port PORT: send logs using this port (default: 9022)") 

979 

980 

981if __name__ == "__main__": 

982 try: 

983 # load parameters and configuration 

984 opts, args = getopt.getopt(sys.argv[1:], "hvc:", ["config=", "help"]) 

985 # TODO add "log-socket-host=", "log-socket-port=", "log-file=" 

986 config_file = None 

987 

988 for o, a in opts: 

989 if o in ("-h", "--help"): 

990 usage() 

991 sys.exit() 

992 elif o in ("-c", "--config"): 

993 config_file = a 

994 else: 

995 raise ValueError("Unhandled option") 

996 

997 if config_file: 

998 if not path.isfile(config_file): 

999 print( 

1000 "configuration file '{}' that not exist".format(config_file), 

1001 file=sys.stderr, 

1002 ) 

1003 exit(1) 

1004 else: 

1005 for config_file in ( 

1006 path.dirname(__file__) + "/ro.cfg", 

1007 "./ro.cfg", 

1008 "/etc/osm/ro.cfg", 

1009 ): 

1010 if path.isfile(config_file): 

1011 break 

1012 else: 

1013 print( 

1014 "No configuration file 'ro.cfg' found neither at local folder nor at /etc/osm/", 

1015 file=sys.stderr, 

1016 ) 

1017 exit(1) 

1018 

1019 ro_main(config_file) 

1020 except KeyboardInterrupt: 

1021 print("KeyboardInterrupt. Finishing", file=sys.stderr) 

1022 except getopt.GetoptError as e: 

1023 print(str(e), file=sys.stderr) 

1024 # usage() 

1025 exit(1)