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