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