1d53396f05321122f1400c26f5b772b1b4247f6e
[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 cherrypy.request.headers.pop("Content-File-MD5", None)
263 elif "application/yaml" in cherrypy.request.headers["Content-Type"]:
264 error_text = "Invalid yaml format "
265 indata = yaml.load(cherrypy.request.body)
266 cherrypy.request.headers.pop("Content-File-MD5", None)
267 elif "application/binary" in cherrypy.request.headers["Content-Type"] or \
268 "application/gzip" in cherrypy.request.headers["Content-Type"] or \
269 "application/zip" in cherrypy.request.headers["Content-Type"] or \
270 "text/plain" in cherrypy.request.headers["Content-Type"]:
271 indata = cherrypy.request.body # .read()
272 elif "multipart/form-data" in cherrypy.request.headers["Content-Type"]:
273 if "descriptor_file" in kwargs:
274 filecontent = kwargs.pop("descriptor_file")
275 if not filecontent.file:
276 raise NbiException("empty file or content", HTTPStatus.BAD_REQUEST)
277 indata = filecontent.file # .read()
278 if filecontent.content_type.value:
279 cherrypy.request.headers["Content-Type"] = filecontent.content_type.value
280 else:
281 # raise cherrypy.HTTPError(HTTPStatus.Not_Acceptable,
282 # "Only 'Content-Type' of type 'application/json' or
283 # 'application/yaml' for input format are available")
284 error_text = "Invalid yaml format "
285 indata = yaml.load(cherrypy.request.body)
286 cherrypy.request.headers.pop("Content-File-MD5", None)
287 else:
288 error_text = "Invalid yaml format "
289 indata = yaml.load(cherrypy.request.body)
290 cherrypy.request.headers.pop("Content-File-MD5", None)
291 if not indata:
292 indata = {}
293
294 format_yaml = False
295 if cherrypy.request.headers.get("Query-String-Format") == "yaml":
296 format_yaml = True
297
298 for k, v in kwargs.items():
299 if isinstance(v, str):
300 if v == "":
301 kwargs[k] = None
302 elif format_yaml:
303 try:
304 kwargs[k] = yaml.load(v)
305 except Exception:
306 pass
307 elif k.endswith(".gt") or k.endswith(".lt") or k.endswith(".gte") or k.endswith(".lte"):
308 try:
309 kwargs[k] = int(v)
310 except Exception:
311 try:
312 kwargs[k] = float(v)
313 except Exception:
314 pass
315 elif v.find(",") > 0:
316 kwargs[k] = v.split(",")
317 elif isinstance(v, (list, tuple)):
318 for index in range(0, len(v)):
319 if v[index] == "":
320 v[index] = None
321 elif format_yaml:
322 try:
323 v[index] = yaml.load(v[index])
324 except Exception:
325 pass
326
327 return indata
328 except (ValueError, yaml.YAMLError) as exc:
329 raise NbiException(error_text + str(exc), HTTPStatus.BAD_REQUEST)
330 except KeyError as exc:
331 raise NbiException("Query string error: " + str(exc), HTTPStatus.BAD_REQUEST)
332 except Exception as exc:
333 raise NbiException(error_text + str(exc), HTTPStatus.BAD_REQUEST)
334
335 @staticmethod
336 def _format_out(data, session=None, _format=None):
337 """
338 return string of dictionary data according to requested json, yaml, xml. By default json
339 :param data: response to be sent. Can be a dict, text or file
340 :param session:
341 :param _format: The format to be set as Content-Type ir data is a file
342 :return: None
343 """
344 accept = cherrypy.request.headers.get("Accept")
345 if data is None:
346 if accept and "text/html" in accept:
347 return html.format(data, cherrypy.request, cherrypy.response, session)
348 # cherrypy.response.status = HTTPStatus.NO_CONTENT.value
349 return
350 elif hasattr(data, "read"): # file object
351 if _format:
352 cherrypy.response.headers["Content-Type"] = _format
353 elif "b" in data.mode: # binariy asssumig zip
354 cherrypy.response.headers["Content-Type"] = 'application/zip'
355 else:
356 cherrypy.response.headers["Content-Type"] = 'text/plain'
357 # TODO check that cherrypy close file. If not implement pending things to close per thread next
358 return data
359 if accept:
360 if "application/json" in accept:
361 cherrypy.response.headers["Content-Type"] = 'application/json; charset=utf-8'
362 a = json.dumps(data, indent=4) + "\n"
363 return a.encode("utf8")
364 elif "text/html" in accept:
365 return html.format(data, cherrypy.request, cherrypy.response, session)
366
367 elif "application/yaml" in accept or "*/*" in accept or "text/plain" in accept:
368 pass
369 else:
370 raise cherrypy.HTTPError(HTTPStatus.NOT_ACCEPTABLE.value,
371 "Only 'Accept' of type 'application/json' or 'application/yaml' "
372 "for output format are available")
373 cherrypy.response.headers["Content-Type"] = 'application/yaml'
374 return yaml.safe_dump(data, explicit_start=True, indent=4, default_flow_style=False, tags=False,
375 encoding='utf-8', allow_unicode=True) # , canonical=True, default_style='"'
376
377 @cherrypy.expose
378 def index(self, *args, **kwargs):
379 session = None
380 try:
381 if cherrypy.request.method == "GET":
382 session = self.authenticator.authorize()
383 outdata = "Index page"
384 else:
385 raise cherrypy.HTTPError(HTTPStatus.METHOD_NOT_ALLOWED.value,
386 "Method {} not allowed for tokens".format(cherrypy.request.method))
387
388 return self._format_out(outdata, session)
389
390 except (EngineException, AuthException) as e:
391 cherrypy.log("index Exception {}".format(e))
392 cherrypy.response.status = e.http_code.value
393 return self._format_out("Welcome to OSM!", session)
394
395 @cherrypy.expose
396 def version(self, *args, **kwargs):
397 # TODO consider to remove and provide version using the static version file
398 global __version__, version_date
399 try:
400 if cherrypy.request.method != "GET":
401 raise NbiException("Only method GET is allowed", HTTPStatus.METHOD_NOT_ALLOWED)
402 elif args or kwargs:
403 raise NbiException("Invalid URL or query string for version", HTTPStatus.METHOD_NOT_ALLOWED)
404 return __version__ + " " + version_date
405 except NbiException as e:
406 cherrypy.response.status = e.http_code.value
407 problem_details = {
408 "code": e.http_code.name,
409 "status": e.http_code.value,
410 "detail": str(e),
411 }
412 return self._format_out(problem_details, None)
413
414 @cherrypy.expose
415 def token(self, method, token_id=None, kwargs=None):
416 session = None
417 # self.engine.load_dbase(cherrypy.request.app.config)
418 indata = self._format_in(kwargs)
419 if not isinstance(indata, dict):
420 raise NbiException("Expected application/yaml or application/json Content-Type", HTTPStatus.BAD_REQUEST)
421 try:
422 if method == "GET":
423 session = self.authenticator.authorize()
424 if token_id:
425 outdata = self.authenticator.get_token(session, token_id)
426 else:
427 outdata = self.authenticator.get_token_list(session)
428 elif method == "POST":
429 try:
430 session = self.authenticator.authorize()
431 except Exception:
432 session = None
433 if kwargs:
434 indata.update(kwargs)
435 outdata = self.authenticator.new_token(session, indata, cherrypy.request.remote)
436 session = outdata
437 cherrypy.session['Authorization'] = outdata["_id"]
438 self._set_location_header("admin", "v1", "tokens", outdata["_id"])
439 # cherrypy.response.cookie["Authorization"] = outdata["id"]
440 # cherrypy.response.cookie["Authorization"]['expires'] = 3600
441 elif method == "DELETE":
442 if not token_id and "id" in kwargs:
443 token_id = kwargs["id"]
444 elif not token_id:
445 session = self.authenticator.authorize()
446 token_id = session["_id"]
447 outdata = self.authenticator.del_token(token_id)
448 session = None
449 cherrypy.session['Authorization'] = "logout"
450 # cherrypy.response.cookie["Authorization"] = token_id
451 # cherrypy.response.cookie["Authorization"]['expires'] = 0
452 else:
453 raise NbiException("Method {} not allowed for token".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
454 return self._format_out(outdata, session)
455 except (NbiException, EngineException, DbException, AuthException) as e:
456 cherrypy.log("tokens Exception {}".format(e))
457 cherrypy.response.status = e.http_code.value
458 problem_details = {
459 "code": e.http_code.name,
460 "status": e.http_code.value,
461 "detail": str(e),
462 }
463 return self._format_out(problem_details, session)
464
465 @cherrypy.expose
466 def test(self, *args, **kwargs):
467 thread_info = None
468 if args and args[0] == "help":
469 return "<html><pre>\ninit\nfile/<name> download file\ndb-clear/table\nprune\nlogin\nlogin2\n"\
470 "sleep/<time>\nmessage/topic\n</pre></html>"
471
472 elif args and args[0] == "init":
473 try:
474 # self.engine.load_dbase(cherrypy.request.app.config)
475 self.engine.create_admin()
476 return "Done. User 'admin', password 'admin' created"
477 except Exception:
478 cherrypy.response.status = HTTPStatus.FORBIDDEN.value
479 return self._format_out("Database already initialized")
480 elif args and args[0] == "file":
481 return cherrypy.lib.static.serve_file(cherrypy.tree.apps['/osm'].config["storage"]["path"] + "/" + args[1],
482 "text/plain", "attachment")
483 elif args and args[0] == "file2":
484 f_path = cherrypy.tree.apps['/osm'].config["storage"]["path"] + "/" + args[1]
485 f = open(f_path, "r")
486 cherrypy.response.headers["Content-type"] = "text/plain"
487 return f
488
489 elif len(args) == 2 and args[0] == "db-clear":
490 return self.engine.db.del_list(args[1], kwargs)
491 elif args and args[0] == "prune":
492 return self.engine.prune()
493 elif args and args[0] == "login":
494 if not cherrypy.request.headers.get("Authorization"):
495 cherrypy.response.headers["WWW-Authenticate"] = 'Basic realm="Access to OSM site", charset="UTF-8"'
496 cherrypy.response.status = HTTPStatus.UNAUTHORIZED.value
497 elif args and args[0] == "login2":
498 if not cherrypy.request.headers.get("Authorization"):
499 cherrypy.response.headers["WWW-Authenticate"] = 'Bearer realm="Access to OSM site"'
500 cherrypy.response.status = HTTPStatus.UNAUTHORIZED.value
501 elif args and args[0] == "sleep":
502 sleep_time = 5
503 try:
504 sleep_time = int(args[1])
505 except Exception:
506 cherrypy.response.status = HTTPStatus.FORBIDDEN.value
507 return self._format_out("Database already initialized")
508 thread_info = cherrypy.thread_data
509 print(thread_info)
510 time.sleep(sleep_time)
511 # thread_info
512 elif len(args) >= 2 and args[0] == "message":
513 main_topic = args[1]
514 return_text = "<html><pre>{} ->\n".format(main_topic)
515 try:
516 if cherrypy.request.method == 'POST':
517 to_send = yaml.load(cherrypy.request.body)
518 for k, v in to_send.items():
519 self.engine.msg.write(main_topic, k, v)
520 return_text += " {}: {}\n".format(k, v)
521 elif cherrypy.request.method == 'GET':
522 for k, v in kwargs.items():
523 self.engine.msg.write(main_topic, k, yaml.load(v))
524 return_text += " {}: {}\n".format(k, yaml.load(v))
525 except Exception as e:
526 return_text += "Error: " + str(e)
527 return_text += "</pre></html>\n"
528 return return_text
529
530 return_text = (
531 "<html><pre>\nheaders:\n args: {}\n".format(args) +
532 " kwargs: {}\n".format(kwargs) +
533 " headers: {}\n".format(cherrypy.request.headers) +
534 " path_info: {}\n".format(cherrypy.request.path_info) +
535 " query_string: {}\n".format(cherrypy.request.query_string) +
536 " session: {}\n".format(cherrypy.session) +
537 " cookie: {}\n".format(cherrypy.request.cookie) +
538 " method: {}\n".format(cherrypy.request.method) +
539 " session: {}\n".format(cherrypy.session.get('fieldname')) +
540 " body:\n")
541 return_text += " length: {}\n".format(cherrypy.request.body.length)
542 if cherrypy.request.body.length:
543 return_text += " content: {}\n".format(
544 str(cherrypy.request.body.read(int(cherrypy.request.headers.get('Content-Length', 0)))))
545 if thread_info:
546 return_text += "thread: {}\n".format(thread_info)
547 return_text += "</pre></html>"
548 return return_text
549
550 def _check_valid_url_method(self, method, *args):
551 if len(args) < 3:
552 raise NbiException("URL must contain at least 'main_topic/version/topic'", HTTPStatus.METHOD_NOT_ALLOWED)
553
554 reference = self.valid_methods
555 for arg in args:
556 if arg is None:
557 break
558 if not isinstance(reference, dict):
559 raise NbiException("URL contains unexpected extra items '{}'".format(arg),
560 HTTPStatus.METHOD_NOT_ALLOWED)
561
562 if arg in reference:
563 reference = reference[arg]
564 elif "<ID>" in reference:
565 reference = reference["<ID>"]
566 elif "*" in reference:
567 reference = reference["*"]
568 break
569 else:
570 raise NbiException("Unexpected URL item {}".format(arg), HTTPStatus.METHOD_NOT_ALLOWED)
571 if "TODO" in reference and method in reference["TODO"]:
572 raise NbiException("Method {} not supported yet for this URL".format(method), HTTPStatus.NOT_IMPLEMENTED)
573 elif "METHODS" in reference and method not in reference["METHODS"]:
574 raise NbiException("Method {} not supported for this URL".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
575 return
576
577 @staticmethod
578 def _set_location_header(main_topic, version, topic, id):
579 """
580 Insert response header Location with the URL of created item base on URL params
581 :param main_topic:
582 :param version:
583 :param topic:
584 :param id:
585 :return: None
586 """
587 # Use cherrypy.request.base for absoluted path and make use of request.header HOST just in case behind aNAT
588 cherrypy.response.headers["Location"] = "/osm/{}/{}/{}/{}".format(main_topic, version, topic, id)
589 return
590
591 @cherrypy.expose
592 def default(self, main_topic=None, version=None, topic=None, _id=None, item=None, *args, **kwargs):
593 session = None
594 outdata = None
595 _format = None
596 method = "DONE"
597 engine_topic = None
598 rollback = []
599 session = None
600 try:
601 if not main_topic or not version or not topic:
602 raise NbiException("URL must contain at least 'main_topic/version/topic'",
603 HTTPStatus.METHOD_NOT_ALLOWED)
604 if main_topic not in ("admin", "vnfpkgm", "nsd", "nslcm"):
605 raise NbiException("URL main_topic '{}' not supported".format(main_topic),
606 HTTPStatus.METHOD_NOT_ALLOWED)
607 if version != 'v1':
608 raise NbiException("URL version '{}' not supported".format(version), HTTPStatus.METHOD_NOT_ALLOWED)
609
610 if kwargs and "METHOD" in kwargs and kwargs["METHOD"] in ("PUT", "POST", "DELETE", "GET", "PATCH"):
611 method = kwargs.pop("METHOD")
612 else:
613 method = cherrypy.request.method
614 if kwargs and "FORCE" in kwargs:
615 force = kwargs.pop("FORCE")
616 else:
617 force = False
618
619 self._check_valid_url_method(method, main_topic, version, topic, _id, item, *args)
620
621 if main_topic == "admin" and topic == "tokens":
622 return self.token(method, _id, kwargs)
623
624 # self.engine.load_dbase(cherrypy.request.app.config)
625 session = self.authenticator.authorize()
626 indata = self._format_in(kwargs)
627 engine_topic = topic
628 if topic == "subscriptions":
629 engine_topic = main_topic + "_" + topic
630 if item:
631 engine_topic = item
632
633 if main_topic == "nsd":
634 engine_topic = "nsds"
635 elif main_topic == "vnfpkgm":
636 engine_topic = "vnfds"
637 elif main_topic == "nslcm":
638 engine_topic = "nsrs"
639 if topic == "ns_lcm_op_occs":
640 engine_topic = "nslcmops"
641 if topic == "vnfrs" or topic == "vnf_instances":
642 engine_topic = "vnfrs"
643 elif main_topic == "pdu":
644 engine_topic = "pdus"
645 if engine_topic == "vims": # TODO this is for backward compatibility, it will remove in the future
646 engine_topic = "vim_accounts"
647
648 if method == "GET":
649 if item in ("nsd_content", "package_content", "artifacts", "vnfd", "nsd"):
650 if item in ("vnfd", "nsd"):
651 path = "$DESCRIPTOR"
652 elif args:
653 path = args
654 elif item == "artifacts":
655 path = ()
656 else:
657 path = None
658 file, _format = self.engine.get_file(session, engine_topic, _id, path,
659 cherrypy.request.headers.get("Accept"))
660 outdata = file
661 elif not _id:
662 outdata = self.engine.get_item_list(session, engine_topic, kwargs)
663 else:
664 outdata = self.engine.get_item(session, engine_topic, _id)
665 elif method == "POST":
666 if topic in ("ns_descriptors_content", "vnf_packages_content"):
667 _id = cherrypy.request.headers.get("Transaction-Id")
668 if not _id:
669 _id = self.engine.new_item(rollback, session, engine_topic, {}, None, cherrypy.request.headers,
670 force=force)
671 completed = self.engine.upload_content(session, engine_topic, _id, indata, kwargs,
672 cherrypy.request.headers, force=force)
673 if completed:
674 self._set_location_header(main_topic, version, topic, _id)
675 else:
676 cherrypy.response.headers["Transaction-Id"] = _id
677 outdata = {"id": _id}
678 elif topic == "ns_instances_content":
679 # creates NSR
680 _id = self.engine.new_item(rollback, session, engine_topic, indata, kwargs, force=force)
681 # creates nslcmop
682 indata["lcmOperationType"] = "instantiate"
683 indata["nsInstanceId"] = _id
684 self.engine.new_item(rollback, session, "nslcmops", indata, None)
685 self._set_location_header(main_topic, version, topic, _id)
686 outdata = {"id": _id}
687 elif topic == "ns_instances" and item:
688 indata["lcmOperationType"] = item
689 indata["nsInstanceId"] = _id
690 _id = self.engine.new_item(rollback, session, "nslcmops", indata, kwargs)
691 self._set_location_header(main_topic, version, "ns_lcm_op_occs", _id)
692 outdata = {"id": _id}
693 cherrypy.response.status = HTTPStatus.ACCEPTED.value
694 else:
695 _id = self.engine.new_item(rollback, session, engine_topic, indata, kwargs,
696 cherrypy.request.headers, force=force)
697 self._set_location_header(main_topic, version, topic, _id)
698 outdata = {"id": _id}
699 # TODO form NsdInfo when topic in ("ns_descriptors", "vnf_packages")
700 cherrypy.response.status = HTTPStatus.CREATED.value
701
702 elif method == "DELETE":
703 if not _id:
704 outdata = self.engine.del_item_list(session, engine_topic, kwargs)
705 cherrypy.response.status = HTTPStatus.OK.value
706 else: # len(args) > 1
707 if topic == "ns_instances_content" and not force:
708 nslcmop_desc = {
709 "lcmOperationType": "terminate",
710 "nsInstanceId": _id,
711 "autoremove": True
712 }
713 opp_id = self.engine.new_item(rollback, session, "nslcmops", nslcmop_desc, None)
714 outdata = {"_id": opp_id}
715 cherrypy.response.status = HTTPStatus.ACCEPTED.value
716 else:
717 self.engine.del_item(session, engine_topic, _id, force)
718 cherrypy.response.status = HTTPStatus.NO_CONTENT.value
719 if engine_topic in ("vim_accounts", "sdns"):
720 cherrypy.response.status = HTTPStatus.ACCEPTED.value
721
722 elif method in ("PUT", "PATCH"):
723 outdata = None
724 if not indata and not kwargs:
725 raise NbiException("Nothing to update. Provide payload and/or query string",
726 HTTPStatus.BAD_REQUEST)
727 if item in ("nsd_content", "package_content") and method == "PUT":
728 completed = self.engine.upload_content(session, engine_topic, _id, indata, kwargs,
729 cherrypy.request.headers, force=force)
730 if not completed:
731 cherrypy.response.headers["Transaction-Id"] = id
732 else:
733 self.engine.edit_item(session, engine_topic, _id, indata, kwargs, force=force)
734 cherrypy.response.status = HTTPStatus.NO_CONTENT.value
735 else:
736 raise NbiException("Method {} not allowed".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
737 return self._format_out(outdata, session, _format)
738 except Exception as e:
739 if isinstance(e, (NbiException, EngineException, DbException, FsException, MsgException, AuthException)):
740 http_code_value = cherrypy.response.status = e.http_code.value
741 http_code_name = e.http_code.name
742 cherrypy.log("Exception {}".format(e))
743 else:
744 http_code_value = cherrypy.response.status = HTTPStatus.BAD_REQUEST.value # INTERNAL_SERVER_ERROR
745 cherrypy.log("CRITICAL: Exception {}".format(e))
746 http_code_name = HTTPStatus.BAD_REQUEST.name
747 if hasattr(outdata, "close"): # is an open file
748 outdata.close()
749 error_text = str(e)
750 rollback.reverse()
751 for rollback_item in rollback:
752 try:
753 if rollback_item.get("operation") == "set":
754 self.engine.db.set_one(rollback_item["topic"], {"_id": rollback_item["_id"]},
755 rollback_item["content"], fail_on_empty=False)
756 else:
757 self.engine.del_item(**rollback_item, session=session, force=True)
758 except Exception as e2:
759 rollback_error_text = "Rollback Exception {}: {}".format(rollback_item, e2)
760 cherrypy.log(rollback_error_text)
761 error_text += ". " + rollback_error_text
762 # if isinstance(e, MsgException):
763 # error_text = "{} has been '{}' but other modules cannot be informed because an error on bus".format(
764 # engine_topic[:-1], method, error_text)
765 problem_details = {
766 "code": http_code_name,
767 "status": http_code_value,
768 "detail": error_text,
769 }
770 return self._format_out(problem_details, session)
771 # raise cherrypy.HTTPError(e.http_code.value, str(e))
772
773
774 # def validate_password(realm, username, password):
775 # cherrypy.log("realm "+ str(realm))
776 # if username == "admin" and password == "admin":
777 # return True
778 # return False
779
780
781 def _start_service():
782 """
783 Callback function called when cherrypy.engine starts
784 Override configuration with env variables
785 Set database, storage, message configuration
786 Init database with admin/admin user password
787 """
788 cherrypy.log.error("Starting osm_nbi")
789 # update general cherrypy configuration
790 update_dict = {}
791
792 engine_config = cherrypy.tree.apps['/osm'].config
793 for k, v in environ.items():
794 if not k.startswith("OSMNBI_"):
795 continue
796 k1, _, k2 = k[7:].lower().partition("_")
797 if not k2:
798 continue
799 try:
800 # update static configuration
801 if k == 'OSMNBI_STATIC_DIR':
802 engine_config["/static"]['tools.staticdir.dir'] = v
803 engine_config["/static"]['tools.staticdir.on'] = True
804 elif k == 'OSMNBI_SOCKET_PORT' or k == 'OSMNBI_SERVER_PORT':
805 update_dict['server.socket_port'] = int(v)
806 elif k == 'OSMNBI_SOCKET_HOST' or k == 'OSMNBI_SERVER_HOST':
807 update_dict['server.socket_host'] = v
808 elif k1 in ("server", "test", "auth", "log"):
809 update_dict[k1 + '.' + k2] = v
810 elif k1 in ("message", "database", "storage", "authentication"):
811 # k2 = k2.replace('_', '.')
812 if k2 in ("port", "db_port"):
813 engine_config[k1][k2] = int(v)
814 else:
815 engine_config[k1][k2] = v
816
817 except ValueError as e:
818 cherrypy.log.error("Ignoring environ '{}': " + str(e))
819 except Exception as e:
820 cherrypy.log.warn("skipping environ '{}' on exception '{}'".format(k, e))
821
822 if update_dict:
823 cherrypy.config.update(update_dict)
824 engine_config["global"].update(update_dict)
825
826 # logging cherrypy
827 log_format_simple = "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(message)s"
828 log_formatter_simple = logging.Formatter(log_format_simple, datefmt='%Y-%m-%dT%H:%M:%S')
829 logger_server = logging.getLogger("cherrypy.error")
830 logger_access = logging.getLogger("cherrypy.access")
831 logger_cherry = logging.getLogger("cherrypy")
832 logger_nbi = logging.getLogger("nbi")
833
834 if "log.file" in engine_config["global"]:
835 file_handler = logging.handlers.RotatingFileHandler(engine_config["global"]["log.file"],
836 maxBytes=100e6, backupCount=9, delay=0)
837 file_handler.setFormatter(log_formatter_simple)
838 logger_cherry.addHandler(file_handler)
839 logger_nbi.addHandler(file_handler)
840 # log always to standard output
841 for format_, logger in {"nbi.server %(filename)s:%(lineno)s": logger_server,
842 "nbi.access %(filename)s:%(lineno)s": logger_access,
843 "%(name)s %(filename)s:%(lineno)s": logger_nbi
844 }.items():
845 log_format_cherry = "%(asctime)s %(levelname)s {} %(message)s".format(format_)
846 log_formatter_cherry = logging.Formatter(log_format_cherry, datefmt='%Y-%m-%dT%H:%M:%S')
847 str_handler = logging.StreamHandler()
848 str_handler.setFormatter(log_formatter_cherry)
849 logger.addHandler(str_handler)
850
851 if engine_config["global"].get("log.level"):
852 logger_cherry.setLevel(engine_config["global"]["log.level"])
853 logger_nbi.setLevel(engine_config["global"]["log.level"])
854
855 # logging other modules
856 for k1, logname in {"message": "nbi.msg", "database": "nbi.db", "storage": "nbi.fs"}.items():
857 engine_config[k1]["logger_name"] = logname
858 logger_module = logging.getLogger(logname)
859 if "logfile" in engine_config[k1]:
860 file_handler = logging.handlers.RotatingFileHandler(engine_config[k1]["logfile"],
861 maxBytes=100e6, backupCount=9, delay=0)
862 file_handler.setFormatter(log_formatter_simple)
863 logger_module.addHandler(file_handler)
864 if "loglevel" in engine_config[k1]:
865 logger_module.setLevel(engine_config[k1]["loglevel"])
866 # TODO add more entries, e.g.: storage
867 cherrypy.tree.apps['/osm'].root.engine.start(engine_config)
868 cherrypy.tree.apps['/osm'].root.authenticator.start(engine_config)
869 cherrypy.tree.apps['/osm'].root.engine.init_db(target_version=database_version)
870 cherrypy.tree.apps['/osm'].root.authenticator.init_db(target_version=auth_database_version)
871 # getenv('OSMOPENMANO_TENANT', None)
872
873
874 def _stop_service():
875 """
876 Callback function called when cherrypy.engine stops
877 TODO: Ending database connections.
878 """
879 cherrypy.tree.apps['/osm'].root.engine.stop()
880 cherrypy.log.error("Stopping osm_nbi")
881
882
883 def nbi(config_file):
884 # conf = {
885 # '/': {
886 # #'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
887 # 'tools.sessions.on': True,
888 # 'tools.response_headers.on': True,
889 # # 'tools.response_headers.headers': [('Content-Type', 'text/plain')],
890 # }
891 # }
892 # cherrypy.Server.ssl_module = 'builtin'
893 # cherrypy.Server.ssl_certificate = "http/cert.pem"
894 # cherrypy.Server.ssl_private_key = "http/privkey.pem"
895 # cherrypy.Server.thread_pool = 10
896 # cherrypy.config.update({'Server.socket_port': config["port"], 'Server.socket_host': config["host"]})
897
898 # cherrypy.config.update({'tools.auth_basic.on': True,
899 # 'tools.auth_basic.realm': 'localhost',
900 # 'tools.auth_basic.checkpassword': validate_password})
901 cherrypy.engine.subscribe('start', _start_service)
902 cherrypy.engine.subscribe('stop', _stop_service)
903 cherrypy.quickstart(Server(), '/osm', config_file)
904
905
906 def usage():
907 print("""Usage: {} [options]
908 -c|--config [configuration_file]: loads the configuration file (default: ./nbi.cfg)
909 -h|--help: shows this help
910 """.format(sys.argv[0]))
911 # --log-socket-host HOST: send logs to this host")
912 # --log-socket-port PORT: send logs using this port (default: 9022)")
913
914
915 if __name__ == '__main__':
916 try:
917 # load parameters and configuration
918 opts, args = getopt.getopt(sys.argv[1:], "hvc:", ["config=", "help"])
919 # TODO add "log-socket-host=", "log-socket-port=", "log-file="
920 config_file = None
921 for o, a in opts:
922 if o in ("-h", "--help"):
923 usage()
924 sys.exit()
925 elif o in ("-c", "--config"):
926 config_file = a
927 # elif o == "--log-socket-port":
928 # log_socket_port = a
929 # elif o == "--log-socket-host":
930 # log_socket_host = a
931 # elif o == "--log-file":
932 # log_file = a
933 else:
934 assert False, "Unhandled option"
935 if config_file:
936 if not path.isfile(config_file):
937 print("configuration file '{}' that not exist".format(config_file), file=sys.stderr)
938 exit(1)
939 else:
940 for config_file in (__file__[:__file__.rfind(".")] + ".cfg", "./nbi.cfg", "/etc/osm/nbi.cfg"):
941 if path.isfile(config_file):
942 break
943 else:
944 print("No configuration file 'nbi.cfg' found neither at local folder nor at /etc/osm/", file=sys.stderr)
945 exit(1)
946 nbi(config_file)
947 except getopt.GetoptError as e:
948 print(str(e), file=sys.stderr)
949 # usage()
950 exit(1)