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