566fb2b1f88b3dc41a31d8eb125de8efac688836
[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", "PATCH")}
156 },
157 "sdns": {"METHODS": ("GET", "POST"),
158 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH")}
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 except Exception as exc:
347 raise NbiException(error_text + str(exc), HTTPStatus.BAD_REQUEST)
348
349 @staticmethod
350 def _format_out(data, session=None, _format=None):
351 """
352 return string of dictionary data according to requested json, yaml, xml. By default json
353 :param data: response to be sent. Can be a dict, text or file
354 :param session:
355 :param _format: The format to be set as Content-Type ir data is a file
356 :return: None
357 """
358 accept = cherrypy.request.headers.get("Accept")
359 if data is None:
360 if accept and "text/html" in accept:
361 return html.format(data, cherrypy.request, cherrypy.response, session)
362 # cherrypy.response.status = HTTPStatus.NO_CONTENT.value
363 return
364 elif hasattr(data, "read"): # file object
365 if _format:
366 cherrypy.response.headers["Content-Type"] = _format
367 elif "b" in data.mode: # binariy asssumig zip
368 cherrypy.response.headers["Content-Type"] = 'application/zip'
369 else:
370 cherrypy.response.headers["Content-Type"] = 'text/plain'
371 # TODO check that cherrypy close file. If not implement pending things to close per thread next
372 return data
373 if accept:
374 if "application/json" in accept:
375 cherrypy.response.headers["Content-Type"] = 'application/json; charset=utf-8'
376 a = json.dumps(data, indent=4) + "\n"
377 return a.encode("utf8")
378 elif "text/html" in accept:
379 return html.format(data, cherrypy.request, cherrypy.response, session)
380
381 elif "application/yaml" in accept or "*/*" in accept or "text/plain" in accept:
382 pass
383 else:
384 raise cherrypy.HTTPError(HTTPStatus.NOT_ACCEPTABLE.value,
385 "Only 'Accept' of type 'application/json' or 'application/yaml' "
386 "for output format are available")
387 cherrypy.response.headers["Content-Type"] = 'application/yaml'
388 return yaml.safe_dump(data, explicit_start=True, indent=4, default_flow_style=False, tags=False,
389 encoding='utf-8', allow_unicode=True) # , canonical=True, default_style='"'
390
391 @cherrypy.expose
392 def index(self, *args, **kwargs):
393 session = None
394 try:
395 if cherrypy.request.method == "GET":
396 session = self._authorization()
397 outdata = "Index page"
398 else:
399 raise cherrypy.HTTPError(HTTPStatus.METHOD_NOT_ALLOWED.value,
400 "Method {} not allowed for tokens".format(cherrypy.request.method))
401
402 return self._format_out(outdata, session)
403
404 except EngineException as e:
405 cherrypy.log("index Exception {}".format(e))
406 cherrypy.response.status = e.http_code.value
407 return self._format_out("Welcome to OSM!", session)
408
409 @cherrypy.expose
410 def version(self, *args, **kwargs):
411 # TODO consider to remove and provide version using the static version file
412 global __version__, version_date
413 try:
414 if cherrypy.request.method != "GET":
415 raise NbiException("Only method GET is allowed", HTTPStatus.METHOD_NOT_ALLOWED)
416 elif args or kwargs:
417 raise NbiException("Invalid URL or query string for version", HTTPStatus.METHOD_NOT_ALLOWED)
418 return __version__ + " " + version_date
419 except NbiException as e:
420 cherrypy.response.status = e.http_code.value
421 problem_details = {
422 "code": e.http_code.name,
423 "status": e.http_code.value,
424 "detail": str(e),
425 }
426 return self._format_out(problem_details, None)
427
428 @cherrypy.expose
429 def token(self, method, token_id=None, kwargs=None):
430 session = None
431 # self.engine.load_dbase(cherrypy.request.app.config)
432 indata = self._format_in(kwargs)
433 if not isinstance(indata, dict):
434 raise NbiException("Expected application/yaml or application/json Content-Type", HTTPStatus.BAD_REQUEST)
435 try:
436 if method == "GET":
437 session = self._authorization()
438 if token_id:
439 outdata = self.engine.get_token(session, token_id)
440 else:
441 outdata = self.engine.get_token_list(session)
442 elif method == "POST":
443 try:
444 session = self._authorization()
445 except:
446 session = None
447 if kwargs:
448 indata.update(kwargs)
449 outdata = self.engine.new_token(session, indata, cherrypy.request.remote)
450 session = outdata
451 cherrypy.session['Authorization'] = outdata["_id"]
452 self._set_location_header("admin", "v1", "tokens", outdata["_id"])
453 # cherrypy.response.cookie["Authorization"] = outdata["id"]
454 # cherrypy.response.cookie["Authorization"]['expires'] = 3600
455 elif method == "DELETE":
456 if not token_id and "id" in kwargs:
457 token_id = kwargs["id"]
458 elif not token_id:
459 session = self._authorization()
460 token_id = session["_id"]
461 outdata = self.engine.del_token(token_id)
462 oudata = None
463 session = None
464 cherrypy.session['Authorization'] = "logout"
465 # cherrypy.response.cookie["Authorization"] = token_id
466 # cherrypy.response.cookie["Authorization"]['expires'] = 0
467 else:
468 raise NbiException("Method {} not allowed for token".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
469 return self._format_out(outdata, session)
470 except (NbiException, EngineException, DbException) as e:
471 cherrypy.log("tokens Exception {}".format(e))
472 cherrypy.response.status = e.http_code.value
473 problem_details = {
474 "code": e.http_code.name,
475 "status": e.http_code.value,
476 "detail": str(e),
477 }
478 return self._format_out(problem_details, session)
479
480 @cherrypy.expose
481 def test(self, *args, **kwargs):
482 thread_info = None
483 if args and args[0] == "help":
484 return "<html><pre>\ninit\nfile/<name> download file\ndb-clear/table\nprune\nlogin\nlogin2\n"\
485 "sleep/<time>\nmessage/topic\n</pre></html>"
486
487 elif args and args[0] == "init":
488 try:
489 # self.engine.load_dbase(cherrypy.request.app.config)
490 self.engine.create_admin()
491 return "Done. User 'admin', password 'admin' created"
492 except Exception:
493 cherrypy.response.status = HTTPStatus.FORBIDDEN.value
494 return self._format_out("Database already initialized")
495 elif args and args[0] == "file":
496 return cherrypy.lib.static.serve_file(cherrypy.tree.apps['/osm'].config["storage"]["path"] + "/" + args[1],
497 "text/plain", "attachment")
498 elif args and args[0] == "file2":
499 f_path = cherrypy.tree.apps['/osm'].config["storage"]["path"] + "/" + args[1]
500 f = open(f_path, "r")
501 cherrypy.response.headers["Content-type"] = "text/plain"
502 return f
503
504 elif len(args) == 2 and args[0] == "db-clear":
505 return self.engine.del_item_list({"project_id": "admin"}, args[1], {})
506 elif args and args[0] == "prune":
507 return self.engine.prune()
508 elif args and args[0] == "login":
509 if not cherrypy.request.headers.get("Authorization"):
510 cherrypy.response.headers["WWW-Authenticate"] = 'Basic realm="Access to OSM site", charset="UTF-8"'
511 cherrypy.response.status = HTTPStatus.UNAUTHORIZED.value
512 elif args and args[0] == "login2":
513 if not cherrypy.request.headers.get("Authorization"):
514 cherrypy.response.headers["WWW-Authenticate"] = 'Bearer realm="Access to OSM site"'
515 cherrypy.response.status = HTTPStatus.UNAUTHORIZED.value
516 elif args and args[0] == "sleep":
517 sleep_time = 5
518 try:
519 sleep_time = int(args[1])
520 except Exception:
521 cherrypy.response.status = HTTPStatus.FORBIDDEN.value
522 return self._format_out("Database already initialized")
523 thread_info = cherrypy.thread_data
524 print(thread_info)
525 time.sleep(sleep_time)
526 # thread_info
527 elif len(args) >= 2 and args[0] == "message":
528 topic = args[1]
529 return_text = "<html><pre>{} ->\n".format(topic)
530 try:
531 if cherrypy.request.method == 'POST':
532 to_send = yaml.load(cherrypy.request.body)
533 for k, v in to_send.items():
534 self.engine.msg.write(topic, k, v)
535 return_text += " {}: {}\n".format(k, v)
536 elif cherrypy.request.method == 'GET':
537 for k, v in kwargs.items():
538 self.engine.msg.write(topic, k, yaml.load(v))
539 return_text += " {}: {}\n".format(k, yaml.load(v))
540 except Exception as e:
541 return_text += "Error: " + str(e)
542 return_text += "</pre></html>\n"
543 return return_text
544
545 return_text = (
546 "<html><pre>\nheaders:\n args: {}\n".format(args) +
547 " kwargs: {}\n".format(kwargs) +
548 " headers: {}\n".format(cherrypy.request.headers) +
549 " path_info: {}\n".format(cherrypy.request.path_info) +
550 " query_string: {}\n".format(cherrypy.request.query_string) +
551 " session: {}\n".format(cherrypy.session) +
552 " cookie: {}\n".format(cherrypy.request.cookie) +
553 " method: {}\n".format(cherrypy.request.method) +
554 " session: {}\n".format(cherrypy.session.get('fieldname')) +
555 " body:\n")
556 return_text += " length: {}\n".format(cherrypy.request.body.length)
557 if cherrypy.request.body.length:
558 return_text += " content: {}\n".format(
559 str(cherrypy.request.body.read(int(cherrypy.request.headers.get('Content-Length', 0)))))
560 if thread_info:
561 return_text += "thread: {}\n".format(thread_info)
562 return_text += "</pre></html>"
563 return return_text
564
565 def _check_valid_url_method(self, method, *args):
566 if len(args) < 3:
567 raise NbiException("URL must contain at least 'topic/version/item'", HTTPStatus.METHOD_NOT_ALLOWED)
568
569 reference = self.valid_methods
570 for arg in args:
571 if arg is None:
572 break
573 if not isinstance(reference, dict):
574 raise NbiException("URL contains unexpected extra items '{}'".format(arg),
575 HTTPStatus.METHOD_NOT_ALLOWED)
576
577 if arg in reference:
578 reference = reference[arg]
579 elif "<ID>" in reference:
580 reference = reference["<ID>"]
581 elif "*" in reference:
582 reference = reference["*"]
583 break
584 else:
585 raise NbiException("Unexpected URL item {}".format(arg), HTTPStatus.METHOD_NOT_ALLOWED)
586 if "TODO" in reference and method in reference["TODO"]:
587 raise NbiException("Method {} not supported yet for this URL".format(method), HTTPStatus.NOT_IMPLEMENTED)
588 elif "METHODS" in reference and not method in reference["METHODS"]:
589 raise NbiException("Method {} not supported for this URL".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
590 return
591
592 @staticmethod
593 def _set_location_header(topic, version, item, id):
594 """
595 Insert response header Location with the URL of created item base on URL params
596 :param topic:
597 :param version:
598 :param item:
599 :param id:
600 :return: None
601 """
602 # Use cherrypy.request.base for absoluted path and make use of request.header HOST just in case behind aNAT
603 cherrypy.response.headers["Location"] = "/osm/{}/{}/{}/{}".format(topic, version, item, id)
604 return
605
606 @cherrypy.expose
607 def default(self, topic=None, version=None, item=None, _id=None, item2=None, *args, **kwargs):
608 session = None
609 outdata = None
610 _format = None
611 method = "DONE"
612 engine_item = None
613 rollback = None
614 try:
615 if not topic or not version or not item:
616 raise NbiException("URL must contain at least 'topic/version/item'", HTTPStatus.METHOD_NOT_ALLOWED)
617 if topic not in ("admin", "vnfpkgm", "nsd", "nslcm"):
618 raise NbiException("URL topic '{}' not supported".format(topic), HTTPStatus.METHOD_NOT_ALLOWED)
619 if version != 'v1':
620 raise NbiException("URL version '{}' not supported".format(version), HTTPStatus.METHOD_NOT_ALLOWED)
621
622 if kwargs and "METHOD" in kwargs and kwargs["METHOD"] in ("PUT", "POST", "DELETE", "GET", "PATCH"):
623 method = kwargs.pop("METHOD")
624 else:
625 method = cherrypy.request.method
626 if kwargs and "FORCE" in kwargs:
627 force = kwargs.pop("FORCE")
628 else:
629 force = False
630
631 self._check_valid_url_method(method, topic, version, item, _id, item2, *args)
632
633 if topic == "admin" and item == "tokens":
634 return self.token(method, _id, kwargs)
635
636 # self.engine.load_dbase(cherrypy.request.app.config)
637 session = self._authorization()
638 indata = self._format_in(kwargs)
639 engine_item = item
640 if item == "subscriptions":
641 engine_item = topic + "_" + item
642 if item2:
643 engine_item = item2
644
645 if topic == "nsd":
646 engine_item = "nsds"
647 elif topic == "vnfpkgm":
648 engine_item = "vnfds"
649 elif topic == "nslcm":
650 engine_item = "nsrs"
651 if item == "ns_lcm_op_occs":
652 engine_item = "nslcmops"
653 if item == "vnfrs":
654 engine_item = "vnfrs"
655 if engine_item == "vims": # TODO this is for backward compatibility, it will remove in the future
656 engine_item = "vim_accounts"
657
658 if method == "GET":
659 if item2 in ("nsd_content", "package_content", "artifacts", "vnfd", "nsd"):
660 if item2 in ("vnfd", "nsd"):
661 path = "$DESCRIPTOR"
662 elif args:
663 path = args
664 elif item2 == "artifacts":
665 path = ()
666 else:
667 path = None
668 file, _format = self.engine.get_file(session, engine_item, _id, path,
669 cherrypy.request.headers.get("Accept"))
670 outdata = file
671 elif not _id:
672 outdata = self.engine.get_item_list(session, engine_item, kwargs)
673 else:
674 outdata = self.engine.get_item(session, engine_item, _id)
675 elif method == "POST":
676 if item in ("ns_descriptors_content", "vnf_packages_content"):
677 _id = cherrypy.request.headers.get("Transaction-Id")
678 if not _id:
679 _id = self.engine.new_item(session, engine_item, {}, None, cherrypy.request.headers,
680 force=force)
681 rollback = {"session": session, "item": engine_item, "_id": _id, "force": True}
682 completed = self.engine.upload_content(session, engine_item, _id, indata, kwargs, cherrypy.request.headers)
683 if completed:
684 self._set_location_header(topic, version, item, _id)
685 else:
686 cherrypy.response.headers["Transaction-Id"] = _id
687 outdata = {"id": _id}
688 elif item == "ns_instances_content":
689 _id = self.engine.new_item(session, engine_item, indata, kwargs, force=force)
690 rollback = {"session": session, "item": engine_item, "_id": _id, "force": True}
691 self.engine.ns_action(session, _id, "instantiate", {}, None)
692 self._set_location_header(topic, version, item, _id)
693 outdata = {"id": _id}
694 elif item == "ns_instances" and item2:
695 _id = self.engine.ns_action(session, _id, item2, indata, kwargs)
696 self._set_location_header(topic, version, "ns_lcm_op_occs", _id)
697 outdata = {"id": _id}
698 cherrypy.response.status = HTTPStatus.ACCEPTED.value
699 else:
700 _id = self.engine.new_item(session, engine_item, indata, kwargs, cherrypy.request.headers,
701 force=force)
702 self._set_location_header(topic, version, item, _id)
703 outdata = {"id": _id}
704 # TODO form NsdInfo when item in ("ns_descriptors", "vnf_packages")
705 cherrypy.response.status = HTTPStatus.CREATED.value
706
707 elif method == "DELETE":
708 if not _id:
709 outdata = self.engine.del_item_list(session, engine_item, kwargs)
710 cherrypy.response.status = HTTPStatus.OK.value
711 else: # len(args) > 1
712 if item == "ns_instances_content":
713 opp_id = self.engine.ns_action(session, _id, "terminate", {"autoremove": True}, None)
714 outdata = {"_id": opp_id}
715 cherrypy.response.status = HTTPStatus.ACCEPTED.value
716 else:
717 self.engine.del_item(session, engine_item, _id, force)
718 cherrypy.response.status = HTTPStatus.NO_CONTENT.value
719 if engine_item in ("vim_accounts", "sdns"):
720 cherrypy.response.status = HTTPStatus.ACCEPTED.value
721
722 elif method == "PUT":
723 if not indata and not kwargs:
724 raise NbiException("Nothing to update. Provide payload and/or query string",
725 HTTPStatus.BAD_REQUEST)
726 if item2 in ("nsd_content", "package_content"):
727 completed = self.engine.upload_content(session, engine_item, _id, indata, kwargs, cherrypy.request.headers)
728 if not completed:
729 cherrypy.response.headers["Transaction-Id"] = id
730 cherrypy.response.status = HTTPStatus.NO_CONTENT.value
731 outdata = None
732 else:
733 outdata = {"id": self.engine.edit_item(session, engine_item, _id, indata, kwargs, force=force)}
734 elif method == "PATCH":
735 if not indata and not kwargs:
736 raise NbiException("Nothing to update. Provide payload and/or query string",
737 HTTPStatus.BAD_REQUEST)
738 outdata = {"id": self.engine.edit_item(session, engine_item, _id, indata, kwargs, force=force)}
739 else:
740 raise NbiException("Method {} not allowed".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
741 return self._format_out(outdata, session, _format)
742 except (NbiException, EngineException, DbException, FsException, MsgException) as e:
743 cherrypy.log("Exception {}".format(e))
744 cherrypy.response.status = e.http_code.value
745 if hasattr(outdata, "close"): # is an open file
746 outdata.close()
747 if rollback:
748 try:
749 self.engine.del_item(**rollback)
750 except Exception as e2:
751 cherrypy.log("Rollback Exception {}: {}".format(rollback, e2))
752 error_text = str(e)
753 if isinstance(e, MsgException):
754 error_text = "{} has been '{}' but other modules cannot be informed because an error on bus".format(
755 engine_item[:-1], method, error_text)
756 problem_details = {
757 "code": e.http_code.name,
758 "status": e.http_code.value,
759 "detail": str(e),
760 }
761 return self._format_out(problem_details, session)
762 # raise cherrypy.HTTPError(e.http_code.value, str(e))
763
764
765 # def validate_password(realm, username, password):
766 # cherrypy.log("realm "+ str(realm))
767 # if username == "admin" and password == "admin":
768 # return True
769 # return False
770
771
772 def _start_service():
773 """
774 Callback function called when cherrypy.engine starts
775 Override configuration with env variables
776 Set database, storage, message configuration
777 Init database with admin/admin user password
778 """
779 cherrypy.log.error("Starting osm_nbi")
780 # update general cherrypy configuration
781 update_dict = {}
782
783 engine_config = cherrypy.tree.apps['/osm'].config
784 for k, v in environ.items():
785 if not k.startswith("OSMNBI_"):
786 continue
787 k1, _, k2 = k[7:].lower().partition("_")
788 if not k2:
789 continue
790 try:
791 # update static configuration
792 if k == 'OSMNBI_STATIC_DIR':
793 engine_config["/static"]['tools.staticdir.dir'] = v
794 engine_config["/static"]['tools.staticdir.on'] = True
795 elif k == 'OSMNBI_SOCKET_PORT' or k == 'OSMNBI_SERVER_PORT':
796 update_dict['server.socket_port'] = int(v)
797 elif k == 'OSMNBI_SOCKET_HOST' or k == 'OSMNBI_SERVER_HOST':
798 update_dict['server.socket_host'] = v
799 elif k1 == "server":
800 update_dict['server' + k2] = v
801 # TODO add more entries
802 elif k1 in ("message", "database", "storage"):
803 if k2 == "port":
804 engine_config[k1][k2] = int(v)
805 else:
806 engine_config[k1][k2] = v
807 except ValueError as e:
808 cherrypy.log.error("Ignoring environ '{}': " + str(e))
809 except Exception as e:
810 cherrypy.log.warn("skipping environ '{}' on exception '{}'".format(k, e))
811
812 if update_dict:
813 cherrypy.config.update(update_dict)
814
815 # logging cherrypy
816 log_format_simple = "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(message)s"
817 log_formatter_simple = logging.Formatter(log_format_simple, datefmt='%Y-%m-%dT%H:%M:%S')
818 logger_server = logging.getLogger("cherrypy.error")
819 logger_access = logging.getLogger("cherrypy.access")
820 logger_cherry = logging.getLogger("cherrypy")
821 logger_nbi = logging.getLogger("nbi")
822
823 if "logfile" in engine_config["global"]:
824 file_handler = logging.handlers.RotatingFileHandler(engine_config["global"]["logfile"],
825 maxBytes=100e6, backupCount=9, delay=0)
826 file_handler.setFormatter(log_formatter_simple)
827 logger_cherry.addHandler(file_handler)
828 logger_nbi.addHandler(file_handler)
829 else:
830 for format_, logger in {"nbi.server": logger_server,
831 "nbi.access": logger_access,
832 "%(name)s %(filename)s:%(lineno)s": logger_nbi
833 }.items():
834 log_format_cherry = "%(asctime)s %(levelname)s {} %(message)s".format(format_)
835 log_formatter_cherry = logging.Formatter(log_format_cherry, datefmt='%Y-%m-%dT%H:%M:%S')
836 str_handler = logging.StreamHandler()
837 str_handler.setFormatter(log_formatter_cherry)
838 logger.addHandler(str_handler)
839
840 if engine_config["global"].get("loglevel"):
841 logger_cherry.setLevel(engine_config["global"]["loglevel"])
842 logger_nbi.setLevel(engine_config["global"]["loglevel"])
843
844 # logging other modules
845 for k1, logname in {"message": "nbi.msg", "database": "nbi.db", "storage": "nbi.fs"}.items():
846 engine_config[k1]["logger_name"] = logname
847 logger_module = logging.getLogger(logname)
848 if "logfile" in engine_config[k1]:
849 file_handler = logging.handlers.RotatingFileHandler(engine_config[k1]["logfile"],
850 maxBytes=100e6, backupCount=9, delay=0)
851 file_handler.setFormatter(log_formatter_simple)
852 logger_module.addHandler(file_handler)
853 if "loglevel" in engine_config[k1]:
854 logger_module.setLevel(engine_config[k1]["loglevel"])
855 # TODO add more entries, e.g.: storage
856 cherrypy.tree.apps['/osm'].root.engine.start(engine_config)
857 try:
858 cherrypy.tree.apps['/osm'].root.engine.init_db(target_version=database_version)
859 except EngineException:
860 pass
861 # getenv('OSMOPENMANO_TENANT', None)
862
863
864 def _stop_service():
865 """
866 Callback function called when cherrypy.engine stops
867 TODO: Ending database connections.
868 """
869 cherrypy.tree.apps['/osm'].root.engine.stop()
870 cherrypy.log.error("Stopping osm_nbi")
871
872 def nbi():
873 # conf = {
874 # '/': {
875 # #'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
876 # 'tools.sessions.on': True,
877 # 'tools.response_headers.on': True,
878 # # 'tools.response_headers.headers': [('Content-Type', 'text/plain')],
879 # }
880 # }
881 # cherrypy.Server.ssl_module = 'builtin'
882 # cherrypy.Server.ssl_certificate = "http/cert.pem"
883 # cherrypy.Server.ssl_private_key = "http/privkey.pem"
884 # cherrypy.Server.thread_pool = 10
885 # cherrypy.config.update({'Server.socket_port': config["port"], 'Server.socket_host': config["host"]})
886
887 # cherrypy.config.update({'tools.auth_basic.on': True,
888 # 'tools.auth_basic.realm': 'localhost',
889 # 'tools.auth_basic.checkpassword': validate_password})
890 cherrypy.engine.subscribe('start', _start_service)
891 cherrypy.engine.subscribe('stop', _stop_service)
892 cherrypy.quickstart(Server(), '/osm', "nbi.cfg")
893
894
895 if __name__ == '__main__':
896 nbi()