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