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