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