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