feature 5956. Split engine in several files
[osm/NBI.git] / osm_nbi / nbi.py
1 #!/usr/bin/python3
2 # -*- coding: utf-8 -*-
3
4 import cherrypy
5 import time
6 import json
7 import yaml
8 import html_out as html
9 import logging
10 import logging.handlers
11 import getopt
12 import sys
13
14 from authconn import AuthException
15 from auth import Authenticator
16 from engine import Engine, EngineException
17 from osm_common.dbbase import DbException
18 from osm_common.fsbase import FsException
19 from osm_common.msgbase import MsgException
20 from http import HTTPStatus
21 from codecs import getreader
22 from os import environ, path
23
24 __author__ = "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
25
26 # TODO consider to remove and provide version using the static version file
27 __version__ = "0.1.3"
28 version_date = "Apr 2018"
29 database_version = '1.0'
30 auth_database_version = '1.0'
31
32 """
33 North Bound Interface (O: OSM specific; 5,X: SOL005 not implemented yet; O5: SOL005 implemented)
34 URL: /osm GET POST PUT DELETE PATCH
35 /nsd/v1 O O
36 /ns_descriptors_content O O
37 /<nsdInfoId> O O O O
38 /ns_descriptors O5 O5
39 /<nsdInfoId> O5 O5 5
40 /nsd_content O5 O5
41 /nsd O
42 /artifacts[/<artifactPath>] O
43 /pnf_descriptors 5 5
44 /<pnfdInfoId> 5 5 5
45 /pnfd_content 5 5
46 /subscriptions 5 5
47 /<subscriptionId> 5 X
48
49 /vnfpkgm/v1
50 /vnf_packages_content O O
51 /<vnfPkgId> O O
52 /vnf_packages O5 O5
53 /<vnfPkgId> O5 O5 5
54 /package_content O5 O5
55 /upload_from_uri X
56 /vnfd O5
57 /artifacts[/<artifactPath>] O5
58 /subscriptions X X
59 /<subscriptionId> X X
60
61 /nslcm/v1
62 /ns_instances_content O O
63 /<nsInstanceId> O O
64 /ns_instances 5 5
65 /<nsInstanceId> O5 O5
66 instantiate O5
67 terminate O5
68 action O
69 scale O5
70 heal 5
71 /ns_lcm_op_occs 5 5
72 /<nsLcmOpOccId> 5 5 5
73 TO BE COMPLETED 5 5
74 /vnf_instances (also vnfrs for compatibility) O
75 /<vnfInstanceId> O
76 /subscriptions 5 5
77 /<subscriptionId> 5 X
78 /pdu/v1
79 /pdu_descriptor O O
80 /<id> O O O O
81 /admin/v1
82 /tokens O O
83 /<id> O O
84 /users O O
85 /<id> O O O O
86 /projects O O
87 /<id> O O
88 /vims_accounts (also vims for compatibility) O O
89 /<id> O O O
90 /sdns O O
91 /<id> O O O
92
93 query string:
94 Follows SOL005 section 4.3.2 It contains extra METHOD to override http method, FORCE to force.
95 For filtering inside array, it must select the element of the array, or add ANYINDEX to apply the filtering over any
96 item of the array, that is, pass if any item of the array pass the filter.
97 It allows both ne and neq for not equal
98 TODO: 4.3.3 Attribute selectors
99 all_fields, fields=x,y,.., exclude_default, exclude_fields=x,y,...
100 (none) … same as “exclude_default”
101 all_fields … all attributes.
102 fields=<list> … all attributes except all complex attributes with minimum cardinality of zero that are not
103 conditionally mandatory, and that are not provided in <list>.
104 exclude_fields=<list> … all attributes except those complex attributes with a minimum cardinality of zero that
105 are not conditionally mandatory, and that are provided in <list>.
106 exclude_default … all attributes except those complex attributes with a minimum cardinality of zero that are not
107 conditionally mandatory, and that are part of the "default exclude set" defined in the present specification for
108 the particular resource
109 exclude_default and include=<list> … all attributes except those complex attributes with a minimum cardinality
110 of zero that are not conditionally mandatory and that are part of the "default exclude set" defined in the
111 present specification for the particular resource, but that are not part of <list>
112 Header field name Reference Example Descriptions
113 Accept IETF RFC 7231 [19] application/json Content-Types that are acceptable for the response.
114 This header field shall be present if the response is expected to have a non-empty message body.
115 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the request.
116 This header field shall be present if the request has a non-empty message body.
117 Authorization IETF RFC 7235 [22] Bearer mF_9.B5f-4.1JqM The authorization token for the request.
118 Details are specified in clause 4.5.3.
119 Range IETF RFC 7233 [21] 1000-2000 Requested range of bytes from a file
120 Header field name Reference Example Descriptions
121 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the response.
122 This header field shall be present if the response has a non-empty message body.
123 Location IETF RFC 7231 [19] http://www.example.com/vnflcm/v1/vnf_instances/123 Used in redirection, or when a
124 new resource has been created.
125 This header field shall be present if the response status code is 201 or 3xx.
126 In the present document this header field is also used if the response status code is 202 and a new resource was
127 created.
128 WWW-Authenticate IETF RFC 7235 [22] Bearer realm="example" Challenge if the corresponding HTTP request has not
129 provided authorization, or error details if the corresponding HTTP request has provided an invalid authorization
130 token.
131 Accept-Ranges IETF RFC 7233 [21] bytes Used by the Server to signal whether or not it supports ranges for
132 certain resources.
133 Content-Range IETF RFC 7233 [21] bytes 21010-47021/ 47022 Signals the byte range that is contained in the
134 response, and the total length of the file.
135 Retry-After IETF RFC 7231 [19] Fri, 31 Dec 1999 23:59:59 GMT
136 """
137
138
139 class NbiException(Exception):
140
141 def __init__(self, message, http_code=HTTPStatus.METHOD_NOT_ALLOWED):
142 Exception.__init__(self, message)
143 self.http_code = http_code
144
145
146 class Server(object):
147 instance = 0
148 # to decode bytes to str
149 reader = getreader("utf-8")
150
151 def __init__(self):
152 self.instance += 1
153 self.engine = Engine()
154 self.authenticator = Authenticator()
155 self.valid_methods = { # contains allowed URL and methods
156 "admin": {
157 "v1": {
158 "tokens": {"METHODS": ("GET", "POST", "DELETE"),
159 "<ID>": {"METHODS": ("GET", "DELETE")}
160 },
161 "users": {"METHODS": ("GET", "POST"),
162 "<ID>": {"METHODS": ("GET", "POST", "DELETE", "PATCH", "PUT")}
163 },
164 "projects": {"METHODS": ("GET", "POST"),
165 "<ID>": {"METHODS": ("GET", "DELETE")}
166 },
167 "vims": {"METHODS": ("GET", "POST"),
168 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
169 },
170 "vim_accounts": {"METHODS": ("GET", "POST"),
171 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
172 },
173 "sdns": {"METHODS": ("GET", "POST"),
174 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
175 },
176 }
177 },
178 "pdu": {
179 "v1": {
180 "pdu_descriptors": {"METHODS": ("GET", "POST"),
181 "<ID>": {"METHODS": ("GET", "POST", "DELETE", "PATCH", "PUT")}
182 },
183 }
184 },
185 "nsd": {
186 "v1": {
187 "ns_descriptors_content": {"METHODS": ("GET", "POST"),
188 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
189 },
190 "ns_descriptors": {"METHODS": ("GET", "POST"),
191 "<ID>": {"METHODS": ("GET", "DELETE"), "TODO": "PATCH",
192 "nsd_content": {"METHODS": ("GET", "PUT")},
193 "nsd": {"METHODS": "GET"}, # descriptor inside package
194 "artifacts": {"*": {"METHODS": "GET"}}
195 }
196 },
197 "pnf_descriptors": {"TODO": ("GET", "POST"),
198 "<ID>": {"TODO": ("GET", "DELETE", "PATCH"),
199 "pnfd_content": {"TODO": ("GET", "PUT")}
200 }
201 },
202 "subscriptions": {"TODO": ("GET", "POST"),
203 "<ID>": {"TODO": ("GET", "DELETE")}
204 },
205 }
206 },
207 "vnfpkgm": {
208 "v1": {
209 "vnf_packages_content": {"METHODS": ("GET", "POST"),
210 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
211 },
212 "vnf_packages": {"METHODS": ("GET", "POST"),
213 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"), # GET: vnfPkgInfo
214 "package_content": {"METHODS": ("GET", "PUT"), # package
215 "upload_from_uri": {"TODO": "POST"}
216 },
217 "vnfd": {"METHODS": "GET"}, # descriptor inside package
218 "artifacts": {"*": {"METHODS": "GET"}}
219 }
220 },
221 "subscriptions": {"TODO": ("GET", "POST"),
222 "<ID>": {"TODO": ("GET", "DELETE")}
223 },
224 }
225 },
226 "nslcm": {
227 "v1": {
228 "ns_instances_content": {"METHODS": ("GET", "POST"),
229 "<ID>": {"METHODS": ("GET", "DELETE")}
230 },
231 "ns_instances": {"METHODS": ("GET", "POST"),
232 "<ID>": {"METHODS": ("GET", "DELETE"),
233 "scale": {"METHODS": "POST"},
234 "terminate": {"METHODS": "POST"},
235 "instantiate": {"METHODS": "POST"},
236 "action": {"METHODS": "POST"},
237 }
238 },
239 "ns_lcm_op_occs": {"METHODS": "GET",
240 "<ID>": {"METHODS": "GET"},
241 },
242 "vnfrs": {"METHODS": ("GET"),
243 "<ID>": {"METHODS": ("GET")}
244 },
245 "vnf_instances": {"METHODS": ("GET"),
246 "<ID>": {"METHODS": ("GET")}
247 },
248 }
249 },
250 }
251
252 def _format_in(self, kwargs):
253 try:
254 indata = None
255 if cherrypy.request.body.length:
256 error_text = "Invalid input format "
257
258 if "Content-Type" in cherrypy.request.headers:
259 if "application/json" in cherrypy.request.headers["Content-Type"]:
260 error_text = "Invalid json format "
261 indata = json.load(self.reader(cherrypy.request.body))
262 elif "application/yaml" in cherrypy.request.headers["Content-Type"]:
263 error_text = "Invalid yaml format "
264 indata = yaml.load(cherrypy.request.body)
265 elif "application/binary" in cherrypy.request.headers["Content-Type"] or \
266 "application/gzip" in cherrypy.request.headers["Content-Type"] or \
267 "application/zip" in cherrypy.request.headers["Content-Type"] or \
268 "text/plain" in cherrypy.request.headers["Content-Type"]:
269 indata = cherrypy.request.body # .read()
270 elif "multipart/form-data" in cherrypy.request.headers["Content-Type"]:
271 if "descriptor_file" in kwargs:
272 filecontent = kwargs.pop("descriptor_file")
273 if not filecontent.file:
274 raise NbiException("empty file or content", HTTPStatus.BAD_REQUEST)
275 indata = filecontent.file # .read()
276 if filecontent.content_type.value:
277 cherrypy.request.headers["Content-Type"] = filecontent.content_type.value
278 else:
279 # raise cherrypy.HTTPError(HTTPStatus.Not_Acceptable,
280 # "Only 'Content-Type' of type 'application/json' or
281 # 'application/yaml' for input format are available")
282 error_text = "Invalid yaml format "
283 indata = yaml.load(cherrypy.request.body)
284 else:
285 error_text = "Invalid yaml format "
286 indata = yaml.load(cherrypy.request.body)
287 if not indata:
288 indata = {}
289
290 format_yaml = False
291 if cherrypy.request.headers.get("Query-String-Format") == "yaml":
292 format_yaml = True
293
294 for k, v in kwargs.items():
295 if isinstance(v, str):
296 if v == "":
297 kwargs[k] = None
298 elif format_yaml:
299 try:
300 kwargs[k] = yaml.load(v)
301 except Exception:
302 pass
303 elif k.endswith(".gt") or k.endswith(".lt") or k.endswith(".gte") or k.endswith(".lte"):
304 try:
305 kwargs[k] = int(v)
306 except Exception:
307 try:
308 kwargs[k] = float(v)
309 except Exception:
310 pass
311 elif v.find(",") > 0:
312 kwargs[k] = v.split(",")
313 elif isinstance(v, (list, tuple)):
314 for index in range(0, len(v)):
315 if v[index] == "":
316 v[index] = None
317 elif format_yaml:
318 try:
319 v[index] = yaml.load(v[index])
320 except Exception:
321 pass
322
323 return indata
324 except (ValueError, yaml.YAMLError) as exc:
325 raise NbiException(error_text + str(exc), HTTPStatus.BAD_REQUEST)
326 except KeyError as exc:
327 raise NbiException("Query string error: " + str(exc), HTTPStatus.BAD_REQUEST)
328 except Exception as exc:
329 raise NbiException(error_text + str(exc), HTTPStatus.BAD_REQUEST)
330
331 @staticmethod
332 def _format_out(data, session=None, _format=None):
333 """
334 return string of dictionary data according to requested json, yaml, xml. By default json
335 :param data: response to be sent. Can be a dict, text or file
336 :param session:
337 :param _format: The format to be set as Content-Type ir data is a file
338 :return: None
339 """
340 accept = cherrypy.request.headers.get("Accept")
341 if data is None:
342 if accept and "text/html" in accept:
343 return html.format(data, cherrypy.request, cherrypy.response, session)
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 # TODO check that cherrypy close file. If not implement pending things to close per thread next
354 return data
355 if accept:
356 if "application/json" in accept:
357 cherrypy.response.headers["Content-Type"] = 'application/json; charset=utf-8'
358 a = json.dumps(data, indent=4) + "\n"
359 return a.encode("utf8")
360 elif "text/html" in accept:
361 return html.format(data, cherrypy.request, cherrypy.response, session)
362
363 elif "application/yaml" in accept or "*/*" in accept or "text/plain" in accept:
364 pass
365 else:
366 raise cherrypy.HTTPError(HTTPStatus.NOT_ACCEPTABLE.value,
367 "Only 'Accept' of type 'application/json' or 'application/yaml' "
368 "for output format are available")
369 cherrypy.response.headers["Content-Type"] = 'application/yaml'
370 return yaml.safe_dump(data, explicit_start=True, indent=4, default_flow_style=False, tags=False,
371 encoding='utf-8', allow_unicode=True) # , canonical=True, default_style='"'
372
373 @cherrypy.expose
374 def index(self, *args, **kwargs):
375 session = None
376 try:
377 if cherrypy.request.method == "GET":
378 session = self.authenticator.authorize()
379 outdata = "Index page"
380 else:
381 raise cherrypy.HTTPError(HTTPStatus.METHOD_NOT_ALLOWED.value,
382 "Method {} not allowed for tokens".format(cherrypy.request.method))
383
384 return self._format_out(outdata, session)
385
386 except (EngineException, AuthException) as e:
387 cherrypy.log("index Exception {}".format(e))
388 cherrypy.response.status = e.http_code.value
389 return self._format_out("Welcome to OSM!", session)
390
391 @cherrypy.expose
392 def version(self, *args, **kwargs):
393 # TODO consider to remove and provide version using the static version file
394 global __version__, version_date
395 try:
396 if cherrypy.request.method != "GET":
397 raise NbiException("Only method GET is allowed", HTTPStatus.METHOD_NOT_ALLOWED)
398 elif args or kwargs:
399 raise NbiException("Invalid URL or query string for version", HTTPStatus.METHOD_NOT_ALLOWED)
400 return __version__ + " " + version_date
401 except NbiException as e:
402 cherrypy.response.status = e.http_code.value
403 problem_details = {
404 "code": e.http_code.name,
405 "status": e.http_code.value,
406 "detail": str(e),
407 }
408 return self._format_out(problem_details, None)
409
410 @cherrypy.expose
411 def token(self, method, token_id=None, kwargs=None):
412 session = None
413 # self.engine.load_dbase(cherrypy.request.app.config)
414 indata = self._format_in(kwargs)
415 if not isinstance(indata, dict):
416 raise NbiException("Expected application/yaml or application/json Content-Type", HTTPStatus.BAD_REQUEST)
417 try:
418 if method == "GET":
419 session = self.authenticator.authorize()
420 if token_id:
421 outdata = self.authenticator.get_token(session, token_id)
422 else:
423 outdata = self.authenticator.get_token_list(session)
424 elif method == "POST":
425 try:
426 session = self.authenticator.authorize()
427 except Exception:
428 session = None
429 if kwargs:
430 indata.update(kwargs)
431 outdata = self.authenticator.new_token(session, indata, cherrypy.request.remote)
432 session = outdata
433 cherrypy.session['Authorization'] = outdata["_id"]
434 self._set_location_header("admin", "v1", "tokens", outdata["_id"])
435 # cherrypy.response.cookie["Authorization"] = outdata["id"]
436 # cherrypy.response.cookie["Authorization"]['expires'] = 3600
437 elif method == "DELETE":
438 if not token_id and "id" in kwargs:
439 token_id = kwargs["id"]
440 elif not token_id:
441 session = self.authenticator.authorize()
442 token_id = session["_id"]
443 outdata = self.authenticator.del_token(token_id)
444 session = None
445 cherrypy.session['Authorization'] = "logout"
446 # cherrypy.response.cookie["Authorization"] = token_id
447 # cherrypy.response.cookie["Authorization"]['expires'] = 0
448 else:
449 raise NbiException("Method {} not allowed for token".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
450 return self._format_out(outdata, session)
451 except (NbiException, EngineException, DbException, AuthException) as e:
452 cherrypy.log("tokens Exception {}".format(e))
453 cherrypy.response.status = e.http_code.value
454 problem_details = {
455 "code": e.http_code.name,
456 "status": e.http_code.value,
457 "detail": str(e),
458 }
459 return self._format_out(problem_details, session)
460
461 @cherrypy.expose
462 def test(self, *args, **kwargs):
463 thread_info = None
464 if args and args[0] == "help":
465 return "<html><pre>\ninit\nfile/<name> download file\ndb-clear/table\nprune\nlogin\nlogin2\n"\
466 "sleep/<time>\nmessage/topic\n</pre></html>"
467
468 elif args and args[0] == "init":
469 try:
470 # self.engine.load_dbase(cherrypy.request.app.config)
471 self.engine.create_admin()
472 return "Done. User 'admin', password 'admin' created"
473 except Exception:
474 cherrypy.response.status = HTTPStatus.FORBIDDEN.value
475 return self._format_out("Database already initialized")
476 elif args and args[0] == "file":
477 return cherrypy.lib.static.serve_file(cherrypy.tree.apps['/osm'].config["storage"]["path"] + "/" + args[1],
478 "text/plain", "attachment")
479 elif args and args[0] == "file2":
480 f_path = cherrypy.tree.apps['/osm'].config["storage"]["path"] + "/" + args[1]
481 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 return self.engine.db.del_list(args[1], kwargs)
487 elif args and args[0] == "prune":
488 return self.engine.prune()
489 elif args and args[0] == "login":
490 if not cherrypy.request.headers.get("Authorization"):
491 cherrypy.response.headers["WWW-Authenticate"] = 'Basic realm="Access to OSM site", charset="UTF-8"'
492 cherrypy.response.status = HTTPStatus.UNAUTHORIZED.value
493 elif args and args[0] == "login2":
494 if not cherrypy.request.headers.get("Authorization"):
495 cherrypy.response.headers["WWW-Authenticate"] = 'Bearer realm="Access to OSM site"'
496 cherrypy.response.status = HTTPStatus.UNAUTHORIZED.value
497 elif args and args[0] == "sleep":
498 sleep_time = 5
499 try:
500 sleep_time = int(args[1])
501 except Exception:
502 cherrypy.response.status = HTTPStatus.FORBIDDEN.value
503 return self._format_out("Database already initialized")
504 thread_info = cherrypy.thread_data
505 print(thread_info)
506 time.sleep(sleep_time)
507 # thread_info
508 elif len(args) >= 2 and args[0] == "message":
509 main_topic = args[1]
510 return_text = "<html><pre>{} ->\n".format(main_topic)
511 try:
512 if cherrypy.request.method == 'POST':
513 to_send = yaml.load(cherrypy.request.body)
514 for k, v in to_send.items():
515 self.engine.msg.write(main_topic, k, v)
516 return_text += " {}: {}\n".format(k, v)
517 elif cherrypy.request.method == 'GET':
518 for k, v in kwargs.items():
519 self.engine.msg.write(main_topic, k, yaml.load(v))
520 return_text += " {}: {}\n".format(k, yaml.load(v))
521 except Exception as e:
522 return_text += "Error: " + str(e)
523 return_text += "</pre></html>\n"
524 return return_text
525
526 return_text = (
527 "<html><pre>\nheaders:\n args: {}\n".format(args) +
528 " kwargs: {}\n".format(kwargs) +
529 " headers: {}\n".format(cherrypy.request.headers) +
530 " path_info: {}\n".format(cherrypy.request.path_info) +
531 " query_string: {}\n".format(cherrypy.request.query_string) +
532 " session: {}\n".format(cherrypy.session) +
533 " cookie: {}\n".format(cherrypy.request.cookie) +
534 " method: {}\n".format(cherrypy.request.method) +
535 " session: {}\n".format(cherrypy.session.get('fieldname')) +
536 " body:\n")
537 return_text += " length: {}\n".format(cherrypy.request.body.length)
538 if cherrypy.request.body.length:
539 return_text += " content: {}\n".format(
540 str(cherrypy.request.body.read(int(cherrypy.request.headers.get('Content-Length', 0)))))
541 if thread_info:
542 return_text += "thread: {}\n".format(thread_info)
543 return_text += "</pre></html>"
544 return return_text
545
546 def _check_valid_url_method(self, method, *args):
547 if len(args) < 3:
548 raise NbiException("URL must contain at least 'main_topic/version/topic'", HTTPStatus.METHOD_NOT_ALLOWED)
549
550 reference = self.valid_methods
551 for arg in args:
552 if arg is None:
553 break
554 if not isinstance(reference, dict):
555 raise NbiException("URL contains unexpected extra items '{}'".format(arg),
556 HTTPStatus.METHOD_NOT_ALLOWED)
557
558 if arg in reference:
559 reference = reference[arg]
560 elif "<ID>" in reference:
561 reference = reference["<ID>"]
562 elif "*" in reference:
563 reference = reference["*"]
564 break
565 else:
566 raise NbiException("Unexpected URL item {}".format(arg), HTTPStatus.METHOD_NOT_ALLOWED)
567 if "TODO" in reference and method in reference["TODO"]:
568 raise NbiException("Method {} not supported yet for this URL".format(method), HTTPStatus.NOT_IMPLEMENTED)
569 elif "METHODS" in reference and method not in reference["METHODS"]:
570 raise NbiException("Method {} not supported for this URL".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
571 return
572
573 @staticmethod
574 def _set_location_header(main_topic, version, topic, id):
575 """
576 Insert response header Location with the URL of created item base on URL params
577 :param main_topic:
578 :param version:
579 :param topic:
580 :param id:
581 :return: None
582 """
583 # Use cherrypy.request.base for absoluted path and make use of request.header HOST just in case behind aNAT
584 cherrypy.response.headers["Location"] = "/osm/{}/{}/{}/{}".format(main_topic, version, topic, id)
585 return
586
587 @cherrypy.expose
588 def default(self, main_topic=None, version=None, topic=None, _id=None, item=None, *args, **kwargs):
589 session = None
590 outdata = None
591 _format = None
592 method = "DONE"
593 engine_topic = None
594 rollback = []
595 session = None
596 try:
597 if not main_topic or not version or not topic:
598 raise NbiException("URL must contain at least 'main_topic/version/topic'",
599 HTTPStatus.METHOD_NOT_ALLOWED)
600 if main_topic not in ("admin", "vnfpkgm", "nsd", "nslcm"):
601 raise NbiException("URL main_topic '{}' not supported".format(main_topic),
602 HTTPStatus.METHOD_NOT_ALLOWED)
603 if version != 'v1':
604 raise NbiException("URL version '{}' not supported".format(version), HTTPStatus.METHOD_NOT_ALLOWED)
605
606 if kwargs and "METHOD" in kwargs and kwargs["METHOD"] in ("PUT", "POST", "DELETE", "GET", "PATCH"):
607 method = kwargs.pop("METHOD")
608 else:
609 method = cherrypy.request.method
610 if kwargs and "FORCE" in kwargs:
611 force = kwargs.pop("FORCE")
612 else:
613 force = False
614
615 self._check_valid_url_method(method, main_topic, version, topic, _id, item, *args)
616
617 if main_topic == "admin" and topic == "tokens":
618 return self.token(method, _id, kwargs)
619
620 # self.engine.load_dbase(cherrypy.request.app.config)
621 session = self.authenticator.authorize()
622 indata = self._format_in(kwargs)
623 engine_topic = topic
624 if topic == "subscriptions":
625 engine_topic = main_topic + "_" + topic
626 if item:
627 engine_topic = item
628
629 if main_topic == "nsd":
630 engine_topic = "nsds"
631 elif main_topic == "vnfpkgm":
632 engine_topic = "vnfds"
633 elif main_topic == "nslcm":
634 engine_topic = "nsrs"
635 if topic == "ns_lcm_op_occs":
636 engine_topic = "nslcmops"
637 if topic == "vnfrs" or topic == "vnf_instances":
638 engine_topic = "vnfrs"
639 elif main_topic == "pdu":
640 engine_topic = "pdus"
641 if engine_topic == "vims": # TODO this is for backward compatibility, it will remove in the future
642 engine_topic = "vim_accounts"
643
644 if method == "GET":
645 if item in ("nsd_content", "package_content", "artifacts", "vnfd", "nsd"):
646 if item in ("vnfd", "nsd"):
647 path = "$DESCRIPTOR"
648 elif args:
649 path = args
650 elif item == "artifacts":
651 path = ()
652 else:
653 path = None
654 file, _format = self.engine.get_file(session, engine_topic, _id, path,
655 cherrypy.request.headers.get("Accept"))
656 outdata = file
657 elif not _id:
658 outdata = self.engine.get_item_list(session, engine_topic, kwargs)
659 else:
660 outdata = self.engine.get_item(session, engine_topic, _id)
661 elif method == "POST":
662 if topic in ("ns_descriptors_content", "vnf_packages_content"):
663 _id = cherrypy.request.headers.get("Transaction-Id")
664 if not _id:
665 _id = self.engine.new_item(rollback, session, engine_topic, {}, None, cherrypy.request.headers,
666 force=force)
667 completed = self.engine.upload_content(session, engine_topic, _id, indata, kwargs,
668 cherrypy.request.headers, force=force)
669 if completed:
670 self._set_location_header(main_topic, version, topic, _id)
671 else:
672 cherrypy.response.headers["Transaction-Id"] = _id
673 outdata = {"id": _id}
674 elif topic == "ns_instances_content":
675 # creates NSR
676 _id = self.engine.new_item(rollback, session, engine_topic, indata, kwargs, force=force)
677 # creates nslcmop
678 indata["lcmOperationType"] = "instantiate"
679 indata["nsInstanceId"] = _id
680 self.engine.new_item(rollback, session, "nslcmops", indata, None)
681 self._set_location_header(main_topic, version, topic, _id)
682 outdata = {"id": _id}
683 elif topic == "ns_instances" and item:
684 indata["lcmOperationType"] = item
685 indata["nsInstanceId"] = _id
686 _id = self.engine.new_item(rollback, session, "nslcmops", indata, kwargs)
687 self._set_location_header(main_topic, version, "ns_lcm_op_occs", _id)
688 outdata = {"id": _id}
689 cherrypy.response.status = HTTPStatus.ACCEPTED.value
690 else:
691 _id = self.engine.new_item(rollback, session, engine_topic, indata, kwargs,
692 cherrypy.request.headers, force=force)
693 self._set_location_header(main_topic, version, topic, _id)
694 outdata = {"id": _id}
695 # TODO form NsdInfo when topic in ("ns_descriptors", "vnf_packages")
696 cherrypy.response.status = HTTPStatus.CREATED.value
697
698 elif method == "DELETE":
699 if not _id:
700 outdata = self.engine.del_item_list(session, engine_topic, kwargs)
701 cherrypy.response.status = HTTPStatus.OK.value
702 else: # len(args) > 1
703 if topic == "ns_instances_content" and not force:
704 nslcmop_desc = {
705 "lcmOperationType": "terminate",
706 "nsInstanceId": _id,
707 "autoremove": True
708 }
709 opp_id = self.engine.new_item(rollback, session, "nslcmops", nslcmop_desc, None)
710 outdata = {"_id": opp_id}
711 cherrypy.response.status = HTTPStatus.ACCEPTED.value
712 else:
713 self.engine.del_item(session, engine_topic, _id, force)
714 cherrypy.response.status = HTTPStatus.NO_CONTENT.value
715 if engine_topic in ("vim_accounts", "sdns"):
716 cherrypy.response.status = HTTPStatus.ACCEPTED.value
717
718 elif method in ("PUT", "PATCH"):
719 outdata = None
720 if not indata and not kwargs:
721 raise NbiException("Nothing to update. Provide payload and/or query string",
722 HTTPStatus.BAD_REQUEST)
723 if item in ("nsd_content", "package_content") and method == "PUT":
724 completed = self.engine.upload_content(session, engine_topic, _id, indata, kwargs,
725 cherrypy.request.headers, force=force)
726 if not completed:
727 cherrypy.response.headers["Transaction-Id"] = id
728 else:
729 self.engine.edit_item(session, engine_topic, _id, indata, kwargs, force=force)
730 cherrypy.response.status = HTTPStatus.NO_CONTENT.value
731 else:
732 raise NbiException("Method {} not allowed".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
733 return self._format_out(outdata, session, _format)
734 except Exception as e:
735 if isinstance(e, (NbiException, EngineException, DbException, FsException, MsgException, AuthException)):
736 http_code_value = cherrypy.response.status = e.http_code.value
737 http_code_name = e.http_code.name
738 cherrypy.log("Exception {}".format(e))
739 else:
740 http_code_value = cherrypy.response.status = HTTPStatus.BAD_REQUEST.value # INTERNAL_SERVER_ERROR
741 cherrypy.log("CRITICAL: Exception {}".format(e))
742 http_code_name = HTTPStatus.BAD_REQUEST.name
743 if hasattr(outdata, "close"): # is an open file
744 outdata.close()
745 error_text = str(e)
746 rollback.reverse()
747 for rollback_item in rollback:
748 try:
749 self.engine.del_item(**rollback_item, session=session, force=True)
750 except Exception as e2:
751 rollback_error_text = "Rollback Exception {}: {}".format(rollback_item, e2)
752 cherrypy.log(rollback_error_text)
753 error_text += ". " + rollback_error_text
754 # if isinstance(e, MsgException):
755 # error_text = "{} has been '{}' but other modules cannot be informed because an error on bus".format(
756 # engine_topic[:-1], method, error_text)
757 problem_details = {
758 "code": http_code_name,
759 "status": http_code_value,
760 "detail": error_text,
761 }
762 return self._format_out(problem_details, session)
763 # raise cherrypy.HTTPError(e.http_code.value, str(e))
764
765
766 # def validate_password(realm, username, password):
767 # cherrypy.log("realm "+ str(realm))
768 # if username == "admin" and password == "admin":
769 # return True
770 # return False
771
772
773 def _start_service():
774 """
775 Callback function called when cherrypy.engine starts
776 Override configuration with env variables
777 Set database, storage, message configuration
778 Init database with admin/admin user password
779 """
780 cherrypy.log.error("Starting osm_nbi")
781 # update general cherrypy configuration
782 update_dict = {}
783
784 engine_config = cherrypy.tree.apps['/osm'].config
785 for k, v in environ.items():
786 if not k.startswith("OSMNBI_"):
787 continue
788 k1, _, k2 = k[7:].lower().partition("_")
789 if not k2:
790 continue
791 try:
792 # update static configuration
793 if k == 'OSMNBI_STATIC_DIR':
794 engine_config["/static"]['tools.staticdir.dir'] = v
795 engine_config["/static"]['tools.staticdir.on'] = True
796 elif k == 'OSMNBI_SOCKET_PORT' or k == 'OSMNBI_SERVER_PORT':
797 update_dict['server.socket_port'] = int(v)
798 elif k == 'OSMNBI_SOCKET_HOST' or k == 'OSMNBI_SERVER_HOST':
799 update_dict['server.socket_host'] = v
800 elif k1 in ("server", "test", "auth", "log"):
801 update_dict[k1 + '.' + k2] = v
802 elif k1 in ("message", "database", "storage", "authentication"):
803 # k2 = k2.replace('_', '.')
804 if k2 in ("port", "db_port"):
805 engine_config[k1][k2] = int(v)
806 else:
807 engine_config[k1][k2] = v
808
809 except ValueError as e:
810 cherrypy.log.error("Ignoring environ '{}': " + str(e))
811 except Exception as e:
812 cherrypy.log.warn("skipping environ '{}' on exception '{}'".format(k, e))
813
814 if update_dict:
815 cherrypy.config.update(update_dict)
816 engine_config["global"].update(update_dict)
817
818 # logging cherrypy
819 log_format_simple = "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(message)s"
820 log_formatter_simple = logging.Formatter(log_format_simple, datefmt='%Y-%m-%dT%H:%M:%S')
821 logger_server = logging.getLogger("cherrypy.error")
822 logger_access = logging.getLogger("cherrypy.access")
823 logger_cherry = logging.getLogger("cherrypy")
824 logger_nbi = logging.getLogger("nbi")
825
826 if "log.file" in engine_config["global"]:
827 file_handler = logging.handlers.RotatingFileHandler(engine_config["global"]["log.file"],
828 maxBytes=100e6, backupCount=9, delay=0)
829 file_handler.setFormatter(log_formatter_simple)
830 logger_cherry.addHandler(file_handler)
831 logger_nbi.addHandler(file_handler)
832 # log always to standard output
833 for format_, logger in {"nbi.server %(filename)s:%(lineno)s": logger_server,
834 "nbi.access %(filename)s:%(lineno)s": logger_access,
835 "%(name)s %(filename)s:%(lineno)s": logger_nbi
836 }.items():
837 log_format_cherry = "%(asctime)s %(levelname)s {} %(message)s".format(format_)
838 log_formatter_cherry = logging.Formatter(log_format_cherry, datefmt='%Y-%m-%dT%H:%M:%S')
839 str_handler = logging.StreamHandler()
840 str_handler.setFormatter(log_formatter_cherry)
841 logger.addHandler(str_handler)
842
843 if engine_config["global"].get("log.level"):
844 logger_cherry.setLevel(engine_config["global"]["log.level"])
845 logger_nbi.setLevel(engine_config["global"]["log.level"])
846
847 # logging other modules
848 for k1, logname in {"message": "nbi.msg", "database": "nbi.db", "storage": "nbi.fs"}.items():
849 engine_config[k1]["logger_name"] = logname
850 logger_module = logging.getLogger(logname)
851 if "logfile" in engine_config[k1]:
852 file_handler = logging.handlers.RotatingFileHandler(engine_config[k1]["logfile"],
853 maxBytes=100e6, backupCount=9, delay=0)
854 file_handler.setFormatter(log_formatter_simple)
855 logger_module.addHandler(file_handler)
856 if "loglevel" in engine_config[k1]:
857 logger_module.setLevel(engine_config[k1]["loglevel"])
858 # TODO add more entries, e.g.: storage
859 cherrypy.tree.apps['/osm'].root.engine.start(engine_config)
860 cherrypy.tree.apps['/osm'].root.authenticator.start(engine_config)
861 try:
862 cherrypy.tree.apps['/osm'].root.engine.init_db(target_version=database_version)
863 cherrypy.tree.apps['/osm'].root.authenticator.init_db(target_version=auth_database_version)
864 except (EngineException, AuthException):
865 pass
866 # getenv('OSMOPENMANO_TENANT', None)
867
868
869 def _stop_service():
870 """
871 Callback function called when cherrypy.engine stops
872 TODO: Ending database connections.
873 """
874 cherrypy.tree.apps['/osm'].root.engine.stop()
875 cherrypy.log.error("Stopping osm_nbi")
876
877
878 def nbi(config_file):
879 # conf = {
880 # '/': {
881 # #'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
882 # 'tools.sessions.on': True,
883 # 'tools.response_headers.on': True,
884 # # 'tools.response_headers.headers': [('Content-Type', 'text/plain')],
885 # }
886 # }
887 # cherrypy.Server.ssl_module = 'builtin'
888 # cherrypy.Server.ssl_certificate = "http/cert.pem"
889 # cherrypy.Server.ssl_private_key = "http/privkey.pem"
890 # cherrypy.Server.thread_pool = 10
891 # cherrypy.config.update({'Server.socket_port': config["port"], 'Server.socket_host': config["host"]})
892
893 # cherrypy.config.update({'tools.auth_basic.on': True,
894 # 'tools.auth_basic.realm': 'localhost',
895 # 'tools.auth_basic.checkpassword': validate_password})
896 cherrypy.engine.subscribe('start', _start_service)
897 cherrypy.engine.subscribe('stop', _stop_service)
898 cherrypy.quickstart(Server(), '/osm', config_file)
899
900
901 def usage():
902 print("""Usage: {} [options]
903 -c|--config [configuration_file]: loads the configuration file (default: ./nbi.cfg)
904 -h|--help: shows this help
905 """.format(sys.argv[0]))
906 # --log-socket-host HOST: send logs to this host")
907 # --log-socket-port PORT: send logs using this port (default: 9022)")
908
909
910 if __name__ == '__main__':
911 try:
912 # load parameters and configuration
913 opts, args = getopt.getopt(sys.argv[1:], "hvc:", ["config=", "help"])
914 # TODO add "log-socket-host=", "log-socket-port=", "log-file="
915 config_file = None
916 for o, a in opts:
917 if o in ("-h", "--help"):
918 usage()
919 sys.exit()
920 elif o in ("-c", "--config"):
921 config_file = a
922 # elif o == "--log-socket-port":
923 # log_socket_port = a
924 # elif o == "--log-socket-host":
925 # log_socket_host = a
926 # elif o == "--log-file":
927 # log_file = a
928 else:
929 assert False, "Unhandled option"
930 if config_file:
931 if not path.isfile(config_file):
932 print("configuration file '{}' that not exist".format(config_file), file=sys.stderr)
933 exit(1)
934 else:
935 for config_file in (__file__[:__file__.rfind(".")] + ".cfg", "./nbi.cfg", "/etc/osm/nbi.cfg"):
936 if path.isfile(config_file):
937 break
938 else:
939 print("No configuration file 'nbi.cfg' found neither at local folder nor at /etc/osm/", file=sys.stderr)
940 exit(1)
941 nbi(config_file)
942 except getopt.GetoptError as e:
943 print(str(e), file=sys.stderr)
944 # usage()
945 exit(1)