c2cb1aff6cff174f047a4374b7da2c26664745fd
[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")}
162 },
163 "vim_accounts": {"METHODS": ("GET", "POST"),
164 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH")}
165 },
166 "sdns": {"METHODS": ("GET", "POST"),
167 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH")}
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 == "PUT":
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"):
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 elif method == "PATCH":
743 if not indata and not kwargs:
744 raise NbiException("Nothing to update. Provide payload and/or query string",
745 HTTPStatus.BAD_REQUEST)
746 outdata = {"id": self.engine.edit_item(session, engine_item, _id, indata, kwargs, force=force)}
747 else:
748 raise NbiException("Method {} not allowed".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
749 return self._format_out(outdata, session, _format)
750 except (NbiException, EngineException, DbException, FsException, MsgException) as e:
751 cherrypy.log("Exception {}".format(e))
752 cherrypy.response.status = e.http_code.value
753 if hasattr(outdata, "close"): # is an open file
754 outdata.close()
755 if rollback:
756 try:
757 self.engine.del_item(**rollback)
758 except Exception as e2:
759 cherrypy.log("Rollback Exception {}: {}".format(rollback, e2))
760 error_text = str(e)
761 if isinstance(e, MsgException):
762 error_text = "{} has been '{}' but other modules cannot be informed because an error on bus".format(
763 engine_item[:-1], method, error_text)
764 problem_details = {
765 "code": e.http_code.name,
766 "status": e.http_code.value,
767 "detail": str(e),
768 }
769 return self._format_out(problem_details, session)
770 # raise cherrypy.HTTPError(e.http_code.value, str(e))
771
772
773 # def validate_password(realm, username, password):
774 # cherrypy.log("realm "+ str(realm))
775 # if username == "admin" and password == "admin":
776 # return True
777 # return False
778
779
780 def _start_service():
781 """
782 Callback function called when cherrypy.engine starts
783 Override configuration with env variables
784 Set database, storage, message configuration
785 Init database with admin/admin user password
786 """
787 cherrypy.log.error("Starting osm_nbi")
788 # update general cherrypy configuration
789 update_dict = {}
790
791 engine_config = cherrypy.tree.apps['/osm'].config
792 for k, v in environ.items():
793 if not k.startswith("OSMNBI_"):
794 continue
795 k1, _, k2 = k[7:].lower().partition("_")
796 if not k2:
797 continue
798 try:
799 # update static configuration
800 if k == 'OSMNBI_STATIC_DIR':
801 engine_config["/static"]['tools.staticdir.dir'] = v
802 engine_config["/static"]['tools.staticdir.on'] = True
803 elif k == 'OSMNBI_SOCKET_PORT' or k == 'OSMNBI_SERVER_PORT':
804 update_dict['server.socket_port'] = int(v)
805 elif k == 'OSMNBI_SOCKET_HOST' or k == 'OSMNBI_SERVER_HOST':
806 update_dict['server.socket_host'] = v
807 elif k1 in ("server", "test", "auth", "log"):
808 update_dict[k1 + '.' + k2] = v
809 elif k1 in ("message", "database", "storage"):
810 # k2 = k2.replace('_', '.')
811 if k2 == "port":
812 engine_config[k1][k2] = int(v)
813 else:
814 engine_config[k1][k2] = v
815 except ValueError as e:
816 cherrypy.log.error("Ignoring environ '{}': " + str(e))
817 except Exception as e:
818 cherrypy.log.warn("skipping environ '{}' on exception '{}'".format(k, e))
819
820 if update_dict:
821 cherrypy.config.update(update_dict)
822 engine_config["global"].update(update_dict)
823
824 # logging cherrypy
825 log_format_simple = "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(message)s"
826 log_formatter_simple = logging.Formatter(log_format_simple, datefmt='%Y-%m-%dT%H:%M:%S')
827 logger_server = logging.getLogger("cherrypy.error")
828 logger_access = logging.getLogger("cherrypy.access")
829 logger_cherry = logging.getLogger("cherrypy")
830 logger_nbi = logging.getLogger("nbi")
831
832 if "log.file" in engine_config["global"]:
833 file_handler = logging.handlers.RotatingFileHandler(engine_config["global"]["log.file"],
834 maxBytes=100e6, backupCount=9, delay=0)
835 file_handler.setFormatter(log_formatter_simple)
836 logger_cherry.addHandler(file_handler)
837 logger_nbi.addHandler(file_handler)
838 else:
839 for format_, logger in {"nbi.server": logger_server,
840 "nbi.access": logger_access,
841 "%(name)s %(filename)s:%(lineno)s": logger_nbi
842 }.items():
843 log_format_cherry = "%(asctime)s %(levelname)s {} %(message)s".format(format_)
844 log_formatter_cherry = logging.Formatter(log_format_cherry, datefmt='%Y-%m-%dT%H:%M:%S')
845 str_handler = logging.StreamHandler()
846 str_handler.setFormatter(log_formatter_cherry)
847 logger.addHandler(str_handler)
848
849 if engine_config["global"].get("log.level"):
850 logger_cherry.setLevel(engine_config["global"]["log.level"])
851 logger_nbi.setLevel(engine_config["global"]["log.level"])
852
853 # logging other modules
854 for k1, logname in {"message": "nbi.msg", "database": "nbi.db", "storage": "nbi.fs"}.items():
855 engine_config[k1]["logger_name"] = logname
856 logger_module = logging.getLogger(logname)
857 if "logfile" in engine_config[k1]:
858 file_handler = logging.handlers.RotatingFileHandler(engine_config[k1]["logfile"],
859 maxBytes=100e6, backupCount=9, delay=0)
860 file_handler.setFormatter(log_formatter_simple)
861 logger_module.addHandler(file_handler)
862 if "loglevel" in engine_config[k1]:
863 logger_module.setLevel(engine_config[k1]["loglevel"])
864 # TODO add more entries, e.g.: storage
865 cherrypy.tree.apps['/osm'].root.engine.start(engine_config)
866 try:
867 cherrypy.tree.apps['/osm'].root.engine.init_db(target_version=database_version)
868 except EngineException:
869 pass
870 # getenv('OSMOPENMANO_TENANT', None)
871
872
873 def _stop_service():
874 """
875 Callback function called when cherrypy.engine stops
876 TODO: Ending database connections.
877 """
878 cherrypy.tree.apps['/osm'].root.engine.stop()
879 cherrypy.log.error("Stopping osm_nbi")
880
881
882 def nbi(config_file):
883 # conf = {
884 # '/': {
885 # #'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
886 # 'tools.sessions.on': True,
887 # 'tools.response_headers.on': True,
888 # # 'tools.response_headers.headers': [('Content-Type', 'text/plain')],
889 # }
890 # }
891 # cherrypy.Server.ssl_module = 'builtin'
892 # cherrypy.Server.ssl_certificate = "http/cert.pem"
893 # cherrypy.Server.ssl_private_key = "http/privkey.pem"
894 # cherrypy.Server.thread_pool = 10
895 # cherrypy.config.update({'Server.socket_port': config["port"], 'Server.socket_host': config["host"]})
896
897 # cherrypy.config.update({'tools.auth_basic.on': True,
898 # 'tools.auth_basic.realm': 'localhost',
899 # 'tools.auth_basic.checkpassword': validate_password})
900 cherrypy.engine.subscribe('start', _start_service)
901 cherrypy.engine.subscribe('stop', _stop_service)
902 cherrypy.quickstart(Server(), '/osm', config_file)
903
904
905 def usage():
906 print("""Usage: {} [options]
907 -c|--config [configuration_file]: loads the configuration file (default: ./nbi.cfg)
908 -h|--help: shows this help
909 """.format(sys.argv[0]))
910 # --log-socket-host HOST: send logs to this host")
911 # --log-socket-port PORT: send logs using this port (default: 9022)")
912
913
914 if __name__ == '__main__':
915 try:
916 # load parameters and configuration
917 opts, args = getopt.getopt(sys.argv[1:], "hvc:", ["config=", "help"])
918 # TODO add "log-socket-host=", "log-socket-port=", "log-file="
919 config_file = None
920 for o, a in opts:
921 if o in ("-h", "--help"):
922 usage()
923 sys.exit()
924 elif o in ("-c", "--config"):
925 config_file = a
926 # elif o == "--log-socket-port":
927 # log_socket_port = a
928 # elif o == "--log-socket-host":
929 # log_socket_host = a
930 # elif o == "--log-file":
931 # log_file = a
932 else:
933 assert False, "Unhandled option"
934 if config_file:
935 if not path.isfile(config_file):
936 print("configuration file '{}' that not exist".format(config_file), file=sys.stderr)
937 exit(1)
938 else:
939 for config_file in (__file__[:__file__.rfind(".")] + ".cfg", "./nbi.cfg", "/etc/osm/nbi.cfg"):
940 if path.isfile(config_file):
941 break
942 else:
943 print("No configuration file 'nbi.cfg' found neither at local folder nor at /etc/osm/", file=sys.stderr)
944 exit(1)
945 nbi(config_file)
946 except getopt.GetoptError as e:
947 print(str(e), file=sys.stderr)
948 # usage()
949 exit(1)