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