aeff32c955389fd4a289ce22296ba58584477815
[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 import logging.handlers
11 import getopt
12 import sys
13
14 from authconn import AuthException
15 from auth import Authenticator
16 from engine import Engine, EngineException
17 from osm_common.dbbase import DbException
18 from osm_common.fsbase import FsException
19 from osm_common.msgbase import MsgException
20 from http import HTTPStatus
21 from codecs import getreader
22 from os import environ, path
23
24 __author__ = "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
25
26 # TODO consider to remove and provide version using the static version file
27 __version__ = "0.1.3"
28 version_date = "Apr 2018"
29 database_version = '1.0'
30 auth_database_version = '1.0'
31
32 """
33 North Bound Interface (O: OSM specific; 5,X: SOL005 not implemented yet; O5: SOL005 implemented)
34 URL: /osm GET POST PUT DELETE PATCH
35 /nsd/v1
36 /ns_descriptors_content O O
37 /<nsdInfoId> O O O O
38 /ns_descriptors O5 O5
39 /<nsdInfoId> O5 O5 5
40 /nsd_content O5 O5
41 /nsd O
42 /artifacts[/<artifactPath>] O
43 /pnf_descriptors 5 5
44 /<pnfdInfoId> 5 5 5
45 /pnfd_content 5 5
46 /subscriptions 5 5
47 /<subscriptionId> 5 X
48
49 /vnfpkgm/v1
50 /vnf_packages_content O O
51 /<vnfPkgId> O O
52 /vnf_packages O5 O5
53 /<vnfPkgId> O5 O5 5
54 /package_content O5 O5
55 /upload_from_uri X
56 /vnfd O5
57 /artifacts[/<artifactPath>] O5
58 /subscriptions X X
59 /<subscriptionId> X X
60
61 /nslcm/v1
62 /ns_instances_content O O
63 /<nsInstanceId> O O
64 /ns_instances 5 5
65 /<nsInstanceId> O5 O5
66 instantiate O5
67 terminate O5
68 action O
69 scale O5
70 heal 5
71 /ns_lcm_op_occs 5 5
72 /<nsLcmOpOccId> 5 5 5
73 TO BE COMPLETED 5 5
74 /vnf_instances (also vnfrs for compatibility) O
75 /<vnfInstanceId> O
76 /subscriptions 5 5
77 /<subscriptionId> 5 X
78
79 /pdu/v1
80 /pdu_descriptor O O
81 /<id> O O O O
82
83 /admin/v1
84 /tokens O O
85 /<id> O O
86 /users O O
87 /<id> O O O O
88 /projects O O
89 /<id> O O
90 /vims_accounts (also vims for compatibility) O O
91 /<id> O O O
92 /sdns O O
93 /<id> O O O
94
95 /nst/v1 O O
96 /netslice_templates_content O O
97 /<nstInfoId> O O O O
98 /netslice_templates O O
99 /<nstInfoId> O O O
100 /nst_content O O
101 /nst O
102 /artifacts[/<artifactPath>] O
103 /subscriptions X X
104 /<subscriptionId> X X
105
106 /nsilcm/v1
107 /netslice_instances_content O O
108 /<SliceInstanceId> O O
109 /netslice_instances O O
110 /<SliceInstanceId> O O
111 instantiate O
112 terminate O
113 action O
114 /nsi_lcm_op_occs O O
115 /<nsiLcmOpOccId> O O O
116 /subscriptions X X
117 /<subscriptionId> X X
118
119 query string:
120 Follows SOL005 section 4.3.2 It contains extra METHOD to override http method, FORCE to force.
121 For filtering inside array, it must select the element of the array, or add ANYINDEX to apply the filtering over any
122 item of the array, that is, pass if any item of the array pass the filter.
123 It allows both ne and neq for not equal
124 TODO: 4.3.3 Attribute selectors
125 all_fields, fields=x,y,.., exclude_default, exclude_fields=x,y,...
126 (none) … same as “exclude_default”
127 all_fields … all attributes.
128 fields=<list> … all attributes except all complex attributes with minimum cardinality of zero that are not
129 conditionally mandatory, and that are not provided in <list>.
130 exclude_fields=<list> … all attributes except those complex attributes with a minimum cardinality of zero that
131 are not conditionally mandatory, and that are provided in <list>.
132 exclude_default … all attributes except those complex attributes with a minimum cardinality of zero that are not
133 conditionally mandatory, and that are part of the "default exclude set" defined in the present specification for
134 the particular resource
135 exclude_default and include=<list> … all attributes except those complex attributes with a minimum cardinality
136 of zero that are not conditionally mandatory and that are part of the "default exclude set" defined in the
137 present specification for the particular resource, but that are not part of <list>
138 Header field name Reference Example Descriptions
139 Accept IETF RFC 7231 [19] application/json Content-Types that are acceptable for the response.
140 This header field shall be present if the response is expected to have a non-empty message body.
141 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the request.
142 This header field shall be present if the request has a non-empty message body.
143 Authorization IETF RFC 7235 [22] Bearer mF_9.B5f-4.1JqM The authorization token for the request.
144 Details are specified in clause 4.5.3.
145 Range IETF RFC 7233 [21] 1000-2000 Requested range of bytes from a file
146 Header field name Reference Example Descriptions
147 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the response.
148 This header field shall be present if the response has a non-empty message body.
149 Location IETF RFC 7231 [19] http://www.example.com/vnflcm/v1/vnf_instances/123 Used in redirection, or when a
150 new resource has been created.
151 This header field shall be present if the response status code is 201 or 3xx.
152 In the present document this header field is also used if the response status code is 202 and a new resource was
153 created.
154 WWW-Authenticate IETF RFC 7235 [22] Bearer realm="example" Challenge if the corresponding HTTP request has not
155 provided authorization, or error details if the corresponding HTTP request has provided an invalid authorization
156 token.
157 Accept-Ranges IETF RFC 7233 [21] bytes Used by the Server to signal whether or not it supports ranges for
158 certain resources.
159 Content-Range IETF RFC 7233 [21] bytes 21010-47021/ 47022 Signals the byte range that is contained in the
160 response, and the total length of the file.
161 Retry-After IETF RFC 7231 [19] Fri, 31 Dec 1999 23:59:59 GMT
162 """
163
164
165 class NbiException(Exception):
166
167 def __init__(self, message, http_code=HTTPStatus.METHOD_NOT_ALLOWED):
168 Exception.__init__(self, message)
169 self.http_code = http_code
170
171
172 class Server(object):
173 instance = 0
174 # to decode bytes to str
175 reader = getreader("utf-8")
176
177 def __init__(self):
178 self.instance += 1
179 self.engine = Engine()
180 self.authenticator = Authenticator()
181 self.valid_methods = { # contains allowed URL and methods
182 "admin": {
183 "v1": {
184 "tokens": {"METHODS": ("GET", "POST", "DELETE"),
185 "<ID>": {"METHODS": ("GET", "DELETE")}
186 },
187 "users": {"METHODS": ("GET", "POST"),
188 "<ID>": {"METHODS": ("GET", "POST", "DELETE", "PATCH", "PUT")}
189 },
190 "projects": {"METHODS": ("GET", "POST"),
191 "<ID>": {"METHODS": ("GET", "DELETE")}
192 },
193 "vims": {"METHODS": ("GET", "POST"),
194 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
195 },
196 "vim_accounts": {"METHODS": ("GET", "POST"),
197 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
198 },
199 "sdns": {"METHODS": ("GET", "POST"),
200 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
201 },
202 }
203 },
204 "pdu": {
205 "v1": {
206 "pdu_descriptors": {"METHODS": ("GET", "POST"),
207 "<ID>": {"METHODS": ("GET", "POST", "DELETE", "PATCH", "PUT")}
208 },
209 }
210 },
211 "nsd": {
212 "v1": {
213 "ns_descriptors_content": {"METHODS": ("GET", "POST"),
214 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
215 },
216 "ns_descriptors": {"METHODS": ("GET", "POST"),
217 "<ID>": {"METHODS": ("GET", "DELETE"), "TODO": "PATCH",
218 "nsd_content": {"METHODS": ("GET", "PUT")},
219 "nsd": {"METHODS": "GET"}, # descriptor inside package
220 "artifacts": {"*": {"METHODS": "GET"}}
221 }
222 },
223 "pnf_descriptors": {"TODO": ("GET", "POST"),
224 "<ID>": {"TODO": ("GET", "DELETE", "PATCH"),
225 "pnfd_content": {"TODO": ("GET", "PUT")}
226 }
227 },
228 "subscriptions": {"TODO": ("GET", "POST"),
229 "<ID>": {"TODO": ("GET", "DELETE")}
230 },
231 }
232 },
233 "vnfpkgm": {
234 "v1": {
235 "vnf_packages_content": {"METHODS": ("GET", "POST"),
236 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
237 },
238 "vnf_packages": {"METHODS": ("GET", "POST"),
239 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"), # GET: vnfPkgInfo
240 "package_content": {"METHODS": ("GET", "PUT"), # package
241 "upload_from_uri": {"TODO": "POST"}
242 },
243 "vnfd": {"METHODS": "GET"}, # descriptor inside package
244 "artifacts": {"*": {"METHODS": "GET"}}
245 }
246 },
247 "subscriptions": {"TODO": ("GET", "POST"),
248 "<ID>": {"TODO": ("GET", "DELETE")}
249 },
250 }
251 },
252 "nslcm": {
253 "v1": {
254 "ns_instances_content": {"METHODS": ("GET", "POST"),
255 "<ID>": {"METHODS": ("GET", "DELETE")}
256 },
257 "ns_instances": {"METHODS": ("GET", "POST"),
258 "<ID>": {"METHODS": ("GET", "DELETE"),
259 "scale": {"METHODS": "POST"},
260 "terminate": {"METHODS": "POST"},
261 "instantiate": {"METHODS": "POST"},
262 "action": {"METHODS": "POST"},
263 }
264 },
265 "ns_lcm_op_occs": {"METHODS": "GET",
266 "<ID>": {"METHODS": "GET"},
267 },
268 "vnfrs": {"METHODS": ("GET"),
269 "<ID>": {"METHODS": ("GET")}
270 },
271 "vnf_instances": {"METHODS": ("GET"),
272 "<ID>": {"METHODS": ("GET")}
273 },
274 }
275 },
276 "nst": {
277 "v1": {
278 "netslice_templates_content": {"METHODS": ("GET", "POST"),
279 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
280 },
281 "netslice_templates": {"METHODS": ("GET", "POST"),
282 "<ID>": {"METHODS": ("GET", "DELETE"), "TODO": "PATCH",
283 "nst_content": {"METHODS": ("GET", "PUT")},
284 "nst": {"METHODS": "GET"}, # descriptor inside package
285 "artifacts": {"*": {"METHODS": "GET"}}
286 }
287 },
288 "subscriptions": {"TODO": ("GET", "POST"),
289 "<ID>": {"TODO": ("GET", "DELETE")}
290 },
291 }
292 },
293 "nsilcm": {
294 "v1": {
295 "netslice_instances_content": {"METHODS": ("GET", "POST"),
296 "<ID>": {"METHODS": ("GET", "DELETE")}
297 },
298 "netslice_instances": {"METHODS": ("GET", "POST"),
299 "<ID>": {"METHODS": ("GET", "DELETE"),
300 "terminate": {"METHODS": "POST"},
301 "instantiate": {"METHODS": "POST"},
302 "action": {"METHODS": "POST"},
303 }
304 },
305 "nsi_lcm_op_occs": {"METHODS": "GET",
306 "<ID>": {"METHODS": "GET"},
307 },
308 }
309 },
310 }
311
312 def _format_in(self, kwargs):
313 try:
314 indata = None
315 if cherrypy.request.body.length:
316 error_text = "Invalid input format "
317
318 if "Content-Type" in cherrypy.request.headers:
319 if "application/json" in cherrypy.request.headers["Content-Type"]:
320 error_text = "Invalid json format "
321 indata = json.load(self.reader(cherrypy.request.body))
322 cherrypy.request.headers.pop("Content-File-MD5", None)
323 elif "application/yaml" in cherrypy.request.headers["Content-Type"]:
324 error_text = "Invalid yaml format "
325 indata = yaml.load(cherrypy.request.body)
326 cherrypy.request.headers.pop("Content-File-MD5", None)
327 elif "application/binary" in cherrypy.request.headers["Content-Type"] or \
328 "application/gzip" in cherrypy.request.headers["Content-Type"] or \
329 "application/zip" in cherrypy.request.headers["Content-Type"] or \
330 "text/plain" in cherrypy.request.headers["Content-Type"]:
331 indata = cherrypy.request.body # .read()
332 elif "multipart/form-data" in cherrypy.request.headers["Content-Type"]:
333 if "descriptor_file" in kwargs:
334 filecontent = kwargs.pop("descriptor_file")
335 if not filecontent.file:
336 raise NbiException("empty file or content", HTTPStatus.BAD_REQUEST)
337 indata = filecontent.file # .read()
338 if filecontent.content_type.value:
339 cherrypy.request.headers["Content-Type"] = filecontent.content_type.value
340 else:
341 # raise cherrypy.HTTPError(HTTPStatus.Not_Acceptable,
342 # "Only 'Content-Type' of type 'application/json' or
343 # 'application/yaml' for input format are available")
344 error_text = "Invalid yaml format "
345 indata = yaml.load(cherrypy.request.body)
346 cherrypy.request.headers.pop("Content-File-MD5", None)
347 else:
348 error_text = "Invalid yaml format "
349 indata = yaml.load(cherrypy.request.body)
350 cherrypy.request.headers.pop("Content-File-MD5", None)
351 if not indata:
352 indata = {}
353
354 format_yaml = False
355 if cherrypy.request.headers.get("Query-String-Format") == "yaml":
356 format_yaml = True
357
358 for k, v in kwargs.items():
359 if isinstance(v, str):
360 if v == "":
361 kwargs[k] = None
362 elif format_yaml:
363 try:
364 kwargs[k] = yaml.load(v)
365 except Exception:
366 pass
367 elif k.endswith(".gt") or k.endswith(".lt") or k.endswith(".gte") or k.endswith(".lte"):
368 try:
369 kwargs[k] = int(v)
370 except Exception:
371 try:
372 kwargs[k] = float(v)
373 except Exception:
374 pass
375 elif v.find(",") > 0:
376 kwargs[k] = v.split(",")
377 elif isinstance(v, (list, tuple)):
378 for index in range(0, len(v)):
379 if v[index] == "":
380 v[index] = None
381 elif format_yaml:
382 try:
383 v[index] = yaml.load(v[index])
384 except Exception:
385 pass
386
387 return indata
388 except (ValueError, yaml.YAMLError) as exc:
389 raise NbiException(error_text + str(exc), HTTPStatus.BAD_REQUEST)
390 except KeyError as exc:
391 raise NbiException("Query string error: " + str(exc), HTTPStatus.BAD_REQUEST)
392 except Exception as exc:
393 raise NbiException(error_text + str(exc), HTTPStatus.BAD_REQUEST)
394
395 @staticmethod
396 def _format_out(data, session=None, _format=None):
397 """
398 return string of dictionary data according to requested json, yaml, xml. By default json
399 :param data: response to be sent. Can be a dict, text or file
400 :param session:
401 :param _format: The format to be set as Content-Type ir data is a file
402 :return: None
403 """
404 accept = cherrypy.request.headers.get("Accept")
405 if data is None:
406 if accept and "text/html" in accept:
407 return html.format(data, cherrypy.request, cherrypy.response, session)
408 # cherrypy.response.status = HTTPStatus.NO_CONTENT.value
409 return
410 elif hasattr(data, "read"): # file object
411 if _format:
412 cherrypy.response.headers["Content-Type"] = _format
413 elif "b" in data.mode: # binariy asssumig zip
414 cherrypy.response.headers["Content-Type"] = 'application/zip'
415 else:
416 cherrypy.response.headers["Content-Type"] = 'text/plain'
417 # TODO check that cherrypy close file. If not implement pending things to close per thread next
418 return data
419 if accept:
420 if "application/json" in accept:
421 cherrypy.response.headers["Content-Type"] = 'application/json; charset=utf-8'
422 a = json.dumps(data, indent=4) + "\n"
423 return a.encode("utf8")
424 elif "text/html" in accept:
425 return html.format(data, cherrypy.request, cherrypy.response, session)
426
427 elif "application/yaml" in accept or "*/*" in accept or "text/plain" in accept:
428 pass
429 else:
430 raise cherrypy.HTTPError(HTTPStatus.NOT_ACCEPTABLE.value,
431 "Only 'Accept' of type 'application/json' or 'application/yaml' "
432 "for output format are available")
433 cherrypy.response.headers["Content-Type"] = 'application/yaml'
434 return yaml.safe_dump(data, explicit_start=True, indent=4, default_flow_style=False, tags=False,
435 encoding='utf-8', allow_unicode=True) # , canonical=True, default_style='"'
436
437 @cherrypy.expose
438 def index(self, *args, **kwargs):
439 session = None
440 try:
441 if cherrypy.request.method == "GET":
442 session = self.authenticator.authorize()
443 outdata = "Index page"
444 else:
445 raise cherrypy.HTTPError(HTTPStatus.METHOD_NOT_ALLOWED.value,
446 "Method {} not allowed for tokens".format(cherrypy.request.method))
447
448 return self._format_out(outdata, session)
449
450 except (EngineException, AuthException) as e:
451 cherrypy.log("index Exception {}".format(e))
452 cherrypy.response.status = e.http_code.value
453 return self._format_out("Welcome to OSM!", session)
454
455 @cherrypy.expose
456 def version(self, *args, **kwargs):
457 # TODO consider to remove and provide version using the static version file
458 global __version__, version_date
459 try:
460 if cherrypy.request.method != "GET":
461 raise NbiException("Only method GET is allowed", HTTPStatus.METHOD_NOT_ALLOWED)
462 elif args or kwargs:
463 raise NbiException("Invalid URL or query string for version", HTTPStatus.METHOD_NOT_ALLOWED)
464 return __version__ + " " + version_date
465 except NbiException as e:
466 cherrypy.response.status = e.http_code.value
467 problem_details = {
468 "code": e.http_code.name,
469 "status": e.http_code.value,
470 "detail": str(e),
471 }
472 return self._format_out(problem_details, None)
473
474 @cherrypy.expose
475 def token(self, method, token_id=None, kwargs=None):
476 session = None
477 # self.engine.load_dbase(cherrypy.request.app.config)
478 indata = self._format_in(kwargs)
479 if not isinstance(indata, dict):
480 raise NbiException("Expected application/yaml or application/json Content-Type", HTTPStatus.BAD_REQUEST)
481 try:
482 if method == "GET":
483 session = self.authenticator.authorize()
484 if token_id:
485 outdata = self.authenticator.get_token(session, token_id)
486 else:
487 outdata = self.authenticator.get_token_list(session)
488 elif method == "POST":
489 try:
490 session = self.authenticator.authorize()
491 except Exception:
492 session = None
493 if kwargs:
494 indata.update(kwargs)
495 outdata = self.authenticator.new_token(session, indata, cherrypy.request.remote)
496 session = outdata
497 cherrypy.session['Authorization'] = outdata["_id"]
498 self._set_location_header("admin", "v1", "tokens", outdata["_id"])
499 # cherrypy.response.cookie["Authorization"] = outdata["id"]
500 # cherrypy.response.cookie["Authorization"]['expires'] = 3600
501 elif method == "DELETE":
502 if not token_id and "id" in kwargs:
503 token_id = kwargs["id"]
504 elif not token_id:
505 session = self.authenticator.authorize()
506 token_id = session["_id"]
507 outdata = self.authenticator.del_token(token_id)
508 session = None
509 cherrypy.session['Authorization'] = "logout"
510 # cherrypy.response.cookie["Authorization"] = token_id
511 # cherrypy.response.cookie["Authorization"]['expires'] = 0
512 else:
513 raise NbiException("Method {} not allowed for token".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
514 return self._format_out(outdata, session)
515 except (NbiException, EngineException, DbException, AuthException) as e:
516 cherrypy.log("tokens Exception {}".format(e))
517 cherrypy.response.status = e.http_code.value
518 problem_details = {
519 "code": e.http_code.name,
520 "status": e.http_code.value,
521 "detail": str(e),
522 }
523 return self._format_out(problem_details, session)
524
525 @cherrypy.expose
526 def test(self, *args, **kwargs):
527 thread_info = None
528 if args and args[0] == "help":
529 return "<html><pre>\ninit\nfile/<name> download file\ndb-clear/table\nprune\nlogin\nlogin2\n"\
530 "sleep/<time>\nmessage/topic\n</pre></html>"
531
532 elif args and args[0] == "init":
533 try:
534 # self.engine.load_dbase(cherrypy.request.app.config)
535 self.engine.create_admin()
536 return "Done. User 'admin', password 'admin' created"
537 except Exception:
538 cherrypy.response.status = HTTPStatus.FORBIDDEN.value
539 return self._format_out("Database already initialized")
540 elif args and args[0] == "file":
541 return cherrypy.lib.static.serve_file(cherrypy.tree.apps['/osm'].config["storage"]["path"] + "/" + args[1],
542 "text/plain", "attachment")
543 elif args and args[0] == "file2":
544 f_path = cherrypy.tree.apps['/osm'].config["storage"]["path"] + "/" + args[1]
545 f = open(f_path, "r")
546 cherrypy.response.headers["Content-type"] = "text/plain"
547 return f
548
549 elif len(args) == 2 and args[0] == "db-clear":
550 return self.engine.db.del_list(args[1], kwargs)
551 elif args and args[0] == "prune":
552 return self.engine.prune()
553 elif args and args[0] == "login":
554 if not cherrypy.request.headers.get("Authorization"):
555 cherrypy.response.headers["WWW-Authenticate"] = 'Basic realm="Access to OSM site", charset="UTF-8"'
556 cherrypy.response.status = HTTPStatus.UNAUTHORIZED.value
557 elif args and args[0] == "login2":
558 if not cherrypy.request.headers.get("Authorization"):
559 cherrypy.response.headers["WWW-Authenticate"] = 'Bearer realm="Access to OSM site"'
560 cherrypy.response.status = HTTPStatus.UNAUTHORIZED.value
561 elif args and args[0] == "sleep":
562 sleep_time = 5
563 try:
564 sleep_time = int(args[1])
565 except Exception:
566 cherrypy.response.status = HTTPStatus.FORBIDDEN.value
567 return self._format_out("Database already initialized")
568 thread_info = cherrypy.thread_data
569 print(thread_info)
570 time.sleep(sleep_time)
571 # thread_info
572 elif len(args) >= 2 and args[0] == "message":
573 main_topic = args[1]
574 return_text = "<html><pre>{} ->\n".format(main_topic)
575 try:
576 if cherrypy.request.method == 'POST':
577 to_send = yaml.load(cherrypy.request.body)
578 for k, v in to_send.items():
579 self.engine.msg.write(main_topic, k, v)
580 return_text += " {}: {}\n".format(k, v)
581 elif cherrypy.request.method == 'GET':
582 for k, v in kwargs.items():
583 self.engine.msg.write(main_topic, k, yaml.load(v))
584 return_text += " {}: {}\n".format(k, yaml.load(v))
585 except Exception as e:
586 return_text += "Error: " + str(e)
587 return_text += "</pre></html>\n"
588 return return_text
589
590 return_text = (
591 "<html><pre>\nheaders:\n args: {}\n".format(args) +
592 " kwargs: {}\n".format(kwargs) +
593 " headers: {}\n".format(cherrypy.request.headers) +
594 " path_info: {}\n".format(cherrypy.request.path_info) +
595 " query_string: {}\n".format(cherrypy.request.query_string) +
596 " session: {}\n".format(cherrypy.session) +
597 " cookie: {}\n".format(cherrypy.request.cookie) +
598 " method: {}\n".format(cherrypy.request.method) +
599 " session: {}\n".format(cherrypy.session.get('fieldname')) +
600 " body:\n")
601 return_text += " length: {}\n".format(cherrypy.request.body.length)
602 if cherrypy.request.body.length:
603 return_text += " content: {}\n".format(
604 str(cherrypy.request.body.read(int(cherrypy.request.headers.get('Content-Length', 0)))))
605 if thread_info:
606 return_text += "thread: {}\n".format(thread_info)
607 return_text += "</pre></html>"
608 return return_text
609
610 def _check_valid_url_method(self, method, *args):
611 if len(args) < 3:
612 raise NbiException("URL must contain at least 'main_topic/version/topic'", HTTPStatus.METHOD_NOT_ALLOWED)
613
614 reference = self.valid_methods
615 for arg in args:
616 if arg is None:
617 break
618 if not isinstance(reference, dict):
619 raise NbiException("URL contains unexpected extra items '{}'".format(arg),
620 HTTPStatus.METHOD_NOT_ALLOWED)
621
622 if arg in reference:
623 reference = reference[arg]
624 elif "<ID>" in reference:
625 reference = reference["<ID>"]
626 elif "*" in reference:
627 reference = reference["*"]
628 break
629 else:
630 raise NbiException("Unexpected URL item {}".format(arg), HTTPStatus.METHOD_NOT_ALLOWED)
631 if "TODO" in reference and method in reference["TODO"]:
632 raise NbiException("Method {} not supported yet for this URL".format(method), HTTPStatus.NOT_IMPLEMENTED)
633 elif "METHODS" in reference and method not in reference["METHODS"]:
634 raise NbiException("Method {} not supported for this URL".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
635 return
636
637 @staticmethod
638 def _set_location_header(main_topic, version, topic, id):
639 """
640 Insert response header Location with the URL of created item base on URL params
641 :param main_topic:
642 :param version:
643 :param topic:
644 :param id:
645 :return: None
646 """
647 # Use cherrypy.request.base for absoluted path and make use of request.header HOST just in case behind aNAT
648 cherrypy.response.headers["Location"] = "/osm/{}/{}/{}/{}".format(main_topic, version, topic, id)
649 return
650
651 @cherrypy.expose
652 def default(self, main_topic=None, version=None, topic=None, _id=None, item=None, *args, **kwargs):
653 session = None
654 outdata = None
655 _format = None
656 method = "DONE"
657 engine_topic = None
658 rollback = []
659 session = None
660 try:
661 if not main_topic or not version or not topic:
662 raise NbiException("URL must contain at least 'main_topic/version/topic'",
663 HTTPStatus.METHOD_NOT_ALLOWED)
664 if main_topic not in ("admin", "vnfpkgm", "nsd", "nslcm"):
665 raise NbiException("URL main_topic '{}' not supported".format(main_topic),
666 HTTPStatus.METHOD_NOT_ALLOWED)
667 if version != 'v1':
668 raise NbiException("URL version '{}' not supported".format(version), HTTPStatus.METHOD_NOT_ALLOWED)
669
670 if kwargs and "METHOD" in kwargs and kwargs["METHOD"] in ("PUT", "POST", "DELETE", "GET", "PATCH"):
671 method = kwargs.pop("METHOD")
672 else:
673 method = cherrypy.request.method
674 if kwargs and "FORCE" in kwargs:
675 force = kwargs.pop("FORCE")
676 else:
677 force = False
678
679 self._check_valid_url_method(method, main_topic, version, topic, _id, item, *args)
680
681 if main_topic == "admin" and topic == "tokens":
682 return self.token(method, _id, kwargs)
683
684 # self.engine.load_dbase(cherrypy.request.app.config)
685 session = self.authenticator.authorize()
686 indata = self._format_in(kwargs)
687 engine_topic = topic
688 if topic == "subscriptions":
689 engine_topic = main_topic + "_" + topic
690 if item:
691 engine_topic = item
692
693 if main_topic == "nsd":
694 engine_topic = "nsds"
695 elif main_topic == "vnfpkgm":
696 engine_topic = "vnfds"
697 elif main_topic == "nslcm":
698 engine_topic = "nsrs"
699 if topic == "ns_lcm_op_occs":
700 engine_topic = "nslcmops"
701 if topic == "vnfrs" or topic == "vnf_instances":
702 engine_topic = "vnfrs"
703 elif main_topic == "nst":
704 engine_topic = "nsts"
705 elif main_topic == "nsilcm":
706 engine_topic = "nsis"
707 if topic == "nsi_lcm_op_occs":
708 engine_topic = "nsilcmops"
709 elif main_topic == "pdu":
710 engine_topic = "pdus"
711 if engine_topic == "vims": # TODO this is for backward compatibility, it will remove in the future
712 engine_topic = "vim_accounts"
713
714 if method == "GET":
715 if item in ("nsd_content", "package_content", "artifacts", "vnfd", "nsd", "nst", "nst_content"):
716 if item in ("vnfd", "nsd", "nst"):
717 path = "$DESCRIPTOR"
718 elif args:
719 path = args
720 elif item == "artifacts":
721 path = ()
722 else:
723 path = None
724 file, _format = self.engine.get_file(session, engine_topic, _id, path,
725 cherrypy.request.headers.get("Accept"))
726 outdata = file
727 elif not _id:
728 outdata = self.engine.get_item_list(session, engine_topic, kwargs)
729 else:
730 outdata = self.engine.get_item(session, engine_topic, _id)
731 elif method == "POST":
732 if topic in ("ns_descriptors_content", "vnf_packages_content", "netslice_templates_content"):
733 _id = cherrypy.request.headers.get("Transaction-Id")
734 if not _id:
735 _id = self.engine.new_item(rollback, session, engine_topic, {}, None, cherrypy.request.headers,
736 force=force)
737 completed = self.engine.upload_content(session, engine_topic, _id, indata, kwargs,
738 cherrypy.request.headers, force=force)
739 if completed:
740 self._set_location_header(main_topic, version, topic, _id)
741 else:
742 cherrypy.response.headers["Transaction-Id"] = _id
743 outdata = {"id": _id}
744 elif topic == "ns_instances_content":
745 # creates NSR
746 _id = self.engine.new_item(rollback, session, engine_topic, indata, kwargs, force=force)
747 # creates nslcmop
748 indata["lcmOperationType"] = "instantiate"
749 indata["nsInstanceId"] = _id
750 self.engine.new_item(rollback, session, "nslcmops", indata, None)
751 self._set_location_header(main_topic, version, topic, _id)
752 outdata = {"id": _id}
753 elif topic == "ns_instances" and item:
754 indata["lcmOperationType"] = item
755 indata["nsInstanceId"] = _id
756 _id = self.engine.new_item(rollback, session, "nslcmops", indata, kwargs)
757 self._set_location_header(main_topic, version, "ns_lcm_op_occs", _id)
758 outdata = {"id": _id}
759 cherrypy.response.status = HTTPStatus.ACCEPTED.value
760 elif topic == "netslice_instances_content":
761 # creates NSI
762 _id = self.engine.new_item(rollback, session, engine_topic, indata, kwargs, force=force)
763 # creates nsilcmop
764 indata["lcmOperationType"] = "instantiate"
765 indata["nsiInstanceId"] = _id
766 self.engine.new_item(rollback, session, "nsilcmops", indata, None)
767 self._set_location_header(main_topic, version, topic, _id)
768 outdata = {"id": _id}
769 elif topic == "netslice_instances" and item:
770 indata["lcmOperationType"] = item
771 indata["nsiInstanceId"] = _id
772 _id = self.engine.new_item(rollback, session, "nsilcmops", indata, kwargs)
773 self._set_location_header(main_topic, version, "nsi_lcm_op_occs", _id)
774 outdata = {"id": _id}
775 cherrypy.response.status = HTTPStatus.ACCEPTED.value
776 else:
777 _id = self.engine.new_item(rollback, session, engine_topic, indata, kwargs,
778 cherrypy.request.headers, force=force)
779 self._set_location_header(main_topic, version, topic, _id)
780 outdata = {"id": _id}
781 # TODO form NsdInfo when topic in ("ns_descriptors", "vnf_packages")
782 cherrypy.response.status = HTTPStatus.CREATED.value
783
784 elif method == "DELETE":
785 if not _id:
786 outdata = self.engine.del_item_list(session, engine_topic, kwargs)
787 cherrypy.response.status = HTTPStatus.OK.value
788 else: # len(args) > 1
789 if topic == "ns_instances_content" and not force:
790 nslcmop_desc = {
791 "lcmOperationType": "terminate",
792 "nsInstanceId": _id,
793 "autoremove": True
794 }
795 opp_id = self.engine.new_item(rollback, session, "nslcmops", nslcmop_desc, None)
796 outdata = {"_id": opp_id}
797 cherrypy.response.status = HTTPStatus.ACCEPTED.value
798 elif topic == "netslice_instances_content" and not force:
799 nsilcmop_desc = {
800 "lcmOperationType": "terminate",
801 "nsiInstanceId": _id,
802 "autoremove": True
803 }
804 opp_id = self.engine.new_item(rollback, session, "nsilcmops", nsilcmop_desc, None)
805 outdata = {"_id": opp_id}
806 cherrypy.response.status = HTTPStatus.ACCEPTED.value
807 else:
808 self.engine.del_item(session, engine_topic, _id, force)
809 cherrypy.response.status = HTTPStatus.NO_CONTENT.value
810 if engine_topic in ("vim_accounts", "sdns"):
811 cherrypy.response.status = HTTPStatus.ACCEPTED.value
812
813 elif method in ("PUT", "PATCH"):
814 outdata = None
815 if not indata and not kwargs:
816 raise NbiException("Nothing to update. Provide payload and/or query string",
817 HTTPStatus.BAD_REQUEST)
818 if item in ("nsd_content", "package_content", "nst_content") and method == "PUT":
819 completed = self.engine.upload_content(session, engine_topic, _id, indata, kwargs,
820 cherrypy.request.headers, force=force)
821 if not completed:
822 cherrypy.response.headers["Transaction-Id"] = id
823 else:
824 self.engine.edit_item(session, engine_topic, _id, indata, kwargs, force=force)
825 cherrypy.response.status = HTTPStatus.NO_CONTENT.value
826 else:
827 raise NbiException("Method {} not allowed".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
828 return self._format_out(outdata, session, _format)
829 except Exception as e:
830 if isinstance(e, (NbiException, EngineException, DbException, FsException, MsgException, AuthException)):
831 http_code_value = cherrypy.response.status = e.http_code.value
832 http_code_name = e.http_code.name
833 cherrypy.log("Exception {}".format(e))
834 else:
835 http_code_value = cherrypy.response.status = HTTPStatus.BAD_REQUEST.value # INTERNAL_SERVER_ERROR
836 cherrypy.log("CRITICAL: Exception {}".format(e))
837 http_code_name = HTTPStatus.BAD_REQUEST.name
838 if hasattr(outdata, "close"): # is an open file
839 outdata.close()
840 error_text = str(e)
841 rollback.reverse()
842 for rollback_item in rollback:
843 try:
844 if rollback_item.get("operation") == "set":
845 self.engine.db.set_one(rollback_item["topic"], {"_id": rollback_item["_id"]},
846 rollback_item["content"], fail_on_empty=False)
847 else:
848 self.engine.del_item(**rollback_item, session=session, force=True)
849 except Exception as e2:
850 rollback_error_text = "Rollback Exception {}: {}".format(rollback_item, e2)
851 cherrypy.log(rollback_error_text)
852 error_text += ". " + rollback_error_text
853 # if isinstance(e, MsgException):
854 # error_text = "{} has been '{}' but other modules cannot be informed because an error on bus".format(
855 # engine_topic[:-1], method, error_text)
856 problem_details = {
857 "code": http_code_name,
858 "status": http_code_value,
859 "detail": error_text,
860 }
861 return self._format_out(problem_details, session)
862 # raise cherrypy.HTTPError(e.http_code.value, str(e))
863
864
865 # def validate_password(realm, username, password):
866 # cherrypy.log("realm "+ str(realm))
867 # if username == "admin" and password == "admin":
868 # return True
869 # return False
870
871
872 def _start_service():
873 """
874 Callback function called when cherrypy.engine starts
875 Override configuration with env variables
876 Set database, storage, message configuration
877 Init database with admin/admin user password
878 """
879 cherrypy.log.error("Starting osm_nbi")
880 # update general cherrypy configuration
881 update_dict = {}
882
883 engine_config = cherrypy.tree.apps['/osm'].config
884 for k, v in environ.items():
885 if not k.startswith("OSMNBI_"):
886 continue
887 k1, _, k2 = k[7:].lower().partition("_")
888 if not k2:
889 continue
890 try:
891 # update static configuration
892 if k == 'OSMNBI_STATIC_DIR':
893 engine_config["/static"]['tools.staticdir.dir'] = v
894 engine_config["/static"]['tools.staticdir.on'] = True
895 elif k == 'OSMNBI_SOCKET_PORT' or k == 'OSMNBI_SERVER_PORT':
896 update_dict['server.socket_port'] = int(v)
897 elif k == 'OSMNBI_SOCKET_HOST' or k == 'OSMNBI_SERVER_HOST':
898 update_dict['server.socket_host'] = v
899 elif k1 in ("server", "test", "auth", "log"):
900 update_dict[k1 + '.' + k2] = v
901 elif k1 in ("message", "database", "storage", "authentication"):
902 # k2 = k2.replace('_', '.')
903 if k2 in ("port", "db_port"):
904 engine_config[k1][k2] = int(v)
905 else:
906 engine_config[k1][k2] = v
907
908 except ValueError as e:
909 cherrypy.log.error("Ignoring environ '{}': " + str(e))
910 except Exception as e:
911 cherrypy.log.warn("skipping environ '{}' on exception '{}'".format(k, e))
912
913 if update_dict:
914 cherrypy.config.update(update_dict)
915 engine_config["global"].update(update_dict)
916
917 # logging cherrypy
918 log_format_simple = "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(message)s"
919 log_formatter_simple = logging.Formatter(log_format_simple, datefmt='%Y-%m-%dT%H:%M:%S')
920 logger_server = logging.getLogger("cherrypy.error")
921 logger_access = logging.getLogger("cherrypy.access")
922 logger_cherry = logging.getLogger("cherrypy")
923 logger_nbi = logging.getLogger("nbi")
924
925 if "log.file" in engine_config["global"]:
926 file_handler = logging.handlers.RotatingFileHandler(engine_config["global"]["log.file"],
927 maxBytes=100e6, backupCount=9, delay=0)
928 file_handler.setFormatter(log_formatter_simple)
929 logger_cherry.addHandler(file_handler)
930 logger_nbi.addHandler(file_handler)
931 # log always to standard output
932 for format_, logger in {"nbi.server %(filename)s:%(lineno)s": logger_server,
933 "nbi.access %(filename)s:%(lineno)s": logger_access,
934 "%(name)s %(filename)s:%(lineno)s": logger_nbi
935 }.items():
936 log_format_cherry = "%(asctime)s %(levelname)s {} %(message)s".format(format_)
937 log_formatter_cherry = logging.Formatter(log_format_cherry, datefmt='%Y-%m-%dT%H:%M:%S')
938 str_handler = logging.StreamHandler()
939 str_handler.setFormatter(log_formatter_cherry)
940 logger.addHandler(str_handler)
941
942 if engine_config["global"].get("log.level"):
943 logger_cherry.setLevel(engine_config["global"]["log.level"])
944 logger_nbi.setLevel(engine_config["global"]["log.level"])
945
946 # logging other modules
947 for k1, logname in {"message": "nbi.msg", "database": "nbi.db", "storage": "nbi.fs"}.items():
948 engine_config[k1]["logger_name"] = logname
949 logger_module = logging.getLogger(logname)
950 if "logfile" in engine_config[k1]:
951 file_handler = logging.handlers.RotatingFileHandler(engine_config[k1]["logfile"],
952 maxBytes=100e6, backupCount=9, delay=0)
953 file_handler.setFormatter(log_formatter_simple)
954 logger_module.addHandler(file_handler)
955 if "loglevel" in engine_config[k1]:
956 logger_module.setLevel(engine_config[k1]["loglevel"])
957 # TODO add more entries, e.g.: storage
958 cherrypy.tree.apps['/osm'].root.engine.start(engine_config)
959 cherrypy.tree.apps['/osm'].root.authenticator.start(engine_config)
960 cherrypy.tree.apps['/osm'].root.engine.init_db(target_version=database_version)
961 cherrypy.tree.apps['/osm'].root.authenticator.init_db(target_version=auth_database_version)
962 # getenv('OSMOPENMANO_TENANT', None)
963
964
965 def _stop_service():
966 """
967 Callback function called when cherrypy.engine stops
968 TODO: Ending database connections.
969 """
970 cherrypy.tree.apps['/osm'].root.engine.stop()
971 cherrypy.log.error("Stopping osm_nbi")
972
973
974 def nbi(config_file):
975 # conf = {
976 # '/': {
977 # #'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
978 # 'tools.sessions.on': True,
979 # 'tools.response_headers.on': True,
980 # # 'tools.response_headers.headers': [('Content-Type', 'text/plain')],
981 # }
982 # }
983 # cherrypy.Server.ssl_module = 'builtin'
984 # cherrypy.Server.ssl_certificate = "http/cert.pem"
985 # cherrypy.Server.ssl_private_key = "http/privkey.pem"
986 # cherrypy.Server.thread_pool = 10
987 # cherrypy.config.update({'Server.socket_port': config["port"], 'Server.socket_host': config["host"]})
988
989 # cherrypy.config.update({'tools.auth_basic.on': True,
990 # 'tools.auth_basic.realm': 'localhost',
991 # 'tools.auth_basic.checkpassword': validate_password})
992 cherrypy.engine.subscribe('start', _start_service)
993 cherrypy.engine.subscribe('stop', _stop_service)
994 cherrypy.quickstart(Server(), '/osm', config_file)
995
996
997 def usage():
998 print("""Usage: {} [options]
999 -c|--config [configuration_file]: loads the configuration file (default: ./nbi.cfg)
1000 -h|--help: shows this help
1001 """.format(sys.argv[0]))
1002 # --log-socket-host HOST: send logs to this host")
1003 # --log-socket-port PORT: send logs using this port (default: 9022)")
1004
1005
1006 if __name__ == '__main__':
1007 try:
1008 # load parameters and configuration
1009 opts, args = getopt.getopt(sys.argv[1:], "hvc:", ["config=", "help"])
1010 # TODO add "log-socket-host=", "log-socket-port=", "log-file="
1011 config_file = None
1012 for o, a in opts:
1013 if o in ("-h", "--help"):
1014 usage()
1015 sys.exit()
1016 elif o in ("-c", "--config"):
1017 config_file = a
1018 # elif o == "--log-socket-port":
1019 # log_socket_port = a
1020 # elif o == "--log-socket-host":
1021 # log_socket_host = a
1022 # elif o == "--log-file":
1023 # log_file = a
1024 else:
1025 assert False, "Unhandled option"
1026 if config_file:
1027 if not path.isfile(config_file):
1028 print("configuration file '{}' that not exist".format(config_file), file=sys.stderr)
1029 exit(1)
1030 else:
1031 for config_file in (__file__[:__file__.rfind(".")] + ".cfg", "./nbi.cfg", "/etc/osm/nbi.cfg"):
1032 if path.isfile(config_file):
1033 break
1034 else:
1035 print("No configuration file 'nbi.cfg' found neither at local folder nor at /etc/osm/", file=sys.stderr)
1036 exit(1)
1037 nbi(config_file)
1038 except getopt.GetoptError as e:
1039 print(str(e), file=sys.stderr)
1040 # usage()
1041 exit(1)