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