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