Adding slice templates to NBI
[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", "nst", "nsilcm"):
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 "netslice_instances"):
717 if item in ("vnfd", "nsd", "nst"):
718 path = "$DESCRIPTOR"
719 elif args:
720 path = args
721 elif item == "artifacts":
722 path = ()
723 else:
724 path = None
725 file, _format = self.engine.get_file(session, engine_topic, _id, path,
726 cherrypy.request.headers.get("Accept"))
727 outdata = file
728 elif not _id:
729 outdata = self.engine.get_item_list(session, engine_topic, kwargs)
730 else:
731 outdata = self.engine.get_item(session, engine_topic, _id)
732 elif method == "POST":
733 if topic in ("ns_descriptors_content", "vnf_packages_content", "netslice_templates_content"):
734 _id = cherrypy.request.headers.get("Transaction-Id")
735 if not _id:
736 _id = self.engine.new_item(rollback, session, engine_topic, {}, None, cherrypy.request.headers,
737 force=force)
738 completed = self.engine.upload_content(session, engine_topic, _id, indata, kwargs,
739 cherrypy.request.headers, force=force)
740 if completed:
741 self._set_location_header(main_topic, version, topic, _id)
742 else:
743 cherrypy.response.headers["Transaction-Id"] = _id
744 outdata = {"id": _id}
745 elif topic == "ns_instances_content":
746 # creates NSR
747 _id = self.engine.new_item(rollback, session, engine_topic, indata, kwargs, force=force)
748 # creates nslcmop
749 indata["lcmOperationType"] = "instantiate"
750 indata["nsInstanceId"] = _id
751 self.engine.new_item(rollback, session, "nslcmops", indata, None)
752 self._set_location_header(main_topic, version, topic, _id)
753 outdata = {"id": _id}
754 elif topic == "ns_instances" and item:
755 indata["lcmOperationType"] = item
756 indata["nsInstanceId"] = _id
757 _id = self.engine.new_item(rollback, session, "nslcmops", indata, kwargs)
758 self._set_location_header(main_topic, version, "ns_lcm_op_occs", _id)
759 outdata = {"id": _id}
760 cherrypy.response.status = HTTPStatus.ACCEPTED.value
761 elif topic == "netslice_instances_content":
762 # creates NSI
763 _id = self.engine.new_item(rollback, session, engine_topic, indata, kwargs, force=force)
764 # creates nsilcmop
765 indata["lcmOperationType"] = "instantiate"
766 indata["nsiInstanceId"] = _id
767 self.engine.new_item(rollback, session, "nsilcmops", indata, None)
768 self._set_location_header(main_topic, version, topic, _id)
769 outdata = {"id": _id}
770 elif topic == "netslice_instances" and item:
771 indata["lcmOperationType"] = item
772 indata["nsiInstanceId"] = _id
773 _id = self.engine.new_item(rollback, session, "nsilcmops", indata, kwargs)
774 self._set_location_header(main_topic, version, "nsi_lcm_op_occs", _id)
775 outdata = {"id": _id}
776 cherrypy.response.status = HTTPStatus.ACCEPTED.value
777 else:
778 _id = self.engine.new_item(rollback, session, engine_topic, indata, kwargs,
779 cherrypy.request.headers, force=force)
780 self._set_location_header(main_topic, version, topic, _id)
781 outdata = {"id": _id}
782 # TODO form NsdInfo when topic in ("ns_descriptors", "vnf_packages")
783 cherrypy.response.status = HTTPStatus.CREATED.value
784
785 elif method == "DELETE":
786 if not _id:
787 outdata = self.engine.del_item_list(session, engine_topic, kwargs)
788 cherrypy.response.status = HTTPStatus.OK.value
789 else: # len(args) > 1
790 if topic == "ns_instances_content" and not force:
791 nslcmop_desc = {
792 "lcmOperationType": "terminate",
793 "nsInstanceId": _id,
794 "autoremove": True
795 }
796 opp_id = self.engine.new_item(rollback, session, "nslcmops", nslcmop_desc, None)
797 outdata = {"_id": opp_id}
798 cherrypy.response.status = HTTPStatus.ACCEPTED.value
799 elif topic == "netslice_instances_content" and not force:
800 nsilcmop_desc = {
801 "lcmOperationType": "terminate",
802 "nsiInstanceId": _id,
803 "autoremove": True
804 }
805 opp_id = self.engine.new_item(rollback, session, "nsilcmops", nsilcmop_desc, None)
806 outdata = {"_id": opp_id}
807 cherrypy.response.status = HTTPStatus.ACCEPTED.value
808 else:
809 self.engine.del_item(session, engine_topic, _id, force)
810 cherrypy.response.status = HTTPStatus.NO_CONTENT.value
811 if engine_topic in ("vim_accounts", "sdns"):
812 cherrypy.response.status = HTTPStatus.ACCEPTED.value
813
814 elif method in ("PUT", "PATCH"):
815 outdata = None
816 if not indata and not kwargs:
817 raise NbiException("Nothing to update. Provide payload and/or query string",
818 HTTPStatus.BAD_REQUEST)
819 if item in ("nsd_content", "package_content", "nst_content") and method == "PUT":
820 completed = self.engine.upload_content(session, engine_topic, _id, indata, kwargs,
821 cherrypy.request.headers, force=force)
822 if not completed:
823 cherrypy.response.headers["Transaction-Id"] = id
824 else:
825 self.engine.edit_item(session, engine_topic, _id, indata, kwargs, force=force)
826 cherrypy.response.status = HTTPStatus.NO_CONTENT.value
827 else:
828 raise NbiException("Method {} not allowed".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
829 return self._format_out(outdata, session, _format)
830 except Exception as e:
831 if isinstance(e, (NbiException, EngineException, DbException, FsException, MsgException, AuthException)):
832 http_code_value = cherrypy.response.status = e.http_code.value
833 http_code_name = e.http_code.name
834 cherrypy.log("Exception {}".format(e))
835 else:
836 http_code_value = cherrypy.response.status = HTTPStatus.BAD_REQUEST.value # INTERNAL_SERVER_ERROR
837 cherrypy.log("CRITICAL: Exception {}".format(e))
838 http_code_name = HTTPStatus.BAD_REQUEST.name
839 if hasattr(outdata, "close"): # is an open file
840 outdata.close()
841 error_text = str(e)
842 rollback.reverse()
843 for rollback_item in rollback:
844 try:
845 if rollback_item.get("operation") == "set":
846 self.engine.db.set_one(rollback_item["topic"], {"_id": rollback_item["_id"]},
847 rollback_item["content"], fail_on_empty=False)
848 else:
849 self.engine.del_item(**rollback_item, session=session, force=True)
850 except Exception as e2:
851 rollback_error_text = "Rollback Exception {}: {}".format(rollback_item, e2)
852 cherrypy.log(rollback_error_text)
853 error_text += ". " + rollback_error_text
854 # if isinstance(e, MsgException):
855 # error_text = "{} has been '{}' but other modules cannot be informed because an error on bus".format(
856 # engine_topic[:-1], method, error_text)
857 problem_details = {
858 "code": http_code_name,
859 "status": http_code_value,
860 "detail": error_text,
861 }
862 return self._format_out(problem_details, session)
863 # raise cherrypy.HTTPError(e.http_code.value, str(e))
864
865
866 # def validate_password(realm, username, password):
867 # cherrypy.log("realm "+ str(realm))
868 # if username == "admin" and password == "admin":
869 # return True
870 # return False
871
872
873 def _start_service():
874 """
875 Callback function called when cherrypy.engine starts
876 Override configuration with env variables
877 Set database, storage, message configuration
878 Init database with admin/admin user password
879 """
880 cherrypy.log.error("Starting osm_nbi")
881 # update general cherrypy configuration
882 update_dict = {}
883
884 engine_config = cherrypy.tree.apps['/osm'].config
885 for k, v in environ.items():
886 if not k.startswith("OSMNBI_"):
887 continue
888 k1, _, k2 = k[7:].lower().partition("_")
889 if not k2:
890 continue
891 try:
892 # update static configuration
893 if k == 'OSMNBI_STATIC_DIR':
894 engine_config["/static"]['tools.staticdir.dir'] = v
895 engine_config["/static"]['tools.staticdir.on'] = True
896 elif k == 'OSMNBI_SOCKET_PORT' or k == 'OSMNBI_SERVER_PORT':
897 update_dict['server.socket_port'] = int(v)
898 elif k == 'OSMNBI_SOCKET_HOST' or k == 'OSMNBI_SERVER_HOST':
899 update_dict['server.socket_host'] = v
900 elif k1 in ("server", "test", "auth", "log"):
901 update_dict[k1 + '.' + k2] = v
902 elif k1 in ("message", "database", "storage", "authentication"):
903 # k2 = k2.replace('_', '.')
904 if k2 in ("port", "db_port"):
905 engine_config[k1][k2] = int(v)
906 else:
907 engine_config[k1][k2] = v
908
909 except ValueError as e:
910 cherrypy.log.error("Ignoring environ '{}': " + str(e))
911 except Exception as e:
912 cherrypy.log.warn("skipping environ '{}' on exception '{}'".format(k, e))
913
914 if update_dict:
915 cherrypy.config.update(update_dict)
916 engine_config["global"].update(update_dict)
917
918 # logging cherrypy
919 log_format_simple = "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(message)s"
920 log_formatter_simple = logging.Formatter(log_format_simple, datefmt='%Y-%m-%dT%H:%M:%S')
921 logger_server = logging.getLogger("cherrypy.error")
922 logger_access = logging.getLogger("cherrypy.access")
923 logger_cherry = logging.getLogger("cherrypy")
924 logger_nbi = logging.getLogger("nbi")
925
926 if "log.file" in engine_config["global"]:
927 file_handler = logging.handlers.RotatingFileHandler(engine_config["global"]["log.file"],
928 maxBytes=100e6, backupCount=9, delay=0)
929 file_handler.setFormatter(log_formatter_simple)
930 logger_cherry.addHandler(file_handler)
931 logger_nbi.addHandler(file_handler)
932 # log always to standard output
933 for format_, logger in {"nbi.server %(filename)s:%(lineno)s": logger_server,
934 "nbi.access %(filename)s:%(lineno)s": logger_access,
935 "%(name)s %(filename)s:%(lineno)s": logger_nbi
936 }.items():
937 log_format_cherry = "%(asctime)s %(levelname)s {} %(message)s".format(format_)
938 log_formatter_cherry = logging.Formatter(log_format_cherry, datefmt='%Y-%m-%dT%H:%M:%S')
939 str_handler = logging.StreamHandler()
940 str_handler.setFormatter(log_formatter_cherry)
941 logger.addHandler(str_handler)
942
943 if engine_config["global"].get("log.level"):
944 logger_cherry.setLevel(engine_config["global"]["log.level"])
945 logger_nbi.setLevel(engine_config["global"]["log.level"])
946
947 # logging other modules
948 for k1, logname in {"message": "nbi.msg", "database": "nbi.db", "storage": "nbi.fs"}.items():
949 engine_config[k1]["logger_name"] = logname
950 logger_module = logging.getLogger(logname)
951 if "logfile" in engine_config[k1]:
952 file_handler = logging.handlers.RotatingFileHandler(engine_config[k1]["logfile"],
953 maxBytes=100e6, backupCount=9, delay=0)
954 file_handler.setFormatter(log_formatter_simple)
955 logger_module.addHandler(file_handler)
956 if "loglevel" in engine_config[k1]:
957 logger_module.setLevel(engine_config[k1]["loglevel"])
958 # TODO add more entries, e.g.: storage
959 cherrypy.tree.apps['/osm'].root.engine.start(engine_config)
960 cherrypy.tree.apps['/osm'].root.authenticator.start(engine_config)
961 cherrypy.tree.apps['/osm'].root.engine.init_db(target_version=database_version)
962 cherrypy.tree.apps['/osm'].root.authenticator.init_db(target_version=auth_database_version)
963 # getenv('OSMOPENMANO_TENANT', None)
964
965
966 def _stop_service():
967 """
968 Callback function called when cherrypy.engine stops
969 TODO: Ending database connections.
970 """
971 cherrypy.tree.apps['/osm'].root.engine.stop()
972 cherrypy.log.error("Stopping osm_nbi")
973
974
975 def nbi(config_file):
976 # conf = {
977 # '/': {
978 # #'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
979 # 'tools.sessions.on': True,
980 # 'tools.response_headers.on': True,
981 # # 'tools.response_headers.headers': [('Content-Type', 'text/plain')],
982 # }
983 # }
984 # cherrypy.Server.ssl_module = 'builtin'
985 # cherrypy.Server.ssl_certificate = "http/cert.pem"
986 # cherrypy.Server.ssl_private_key = "http/privkey.pem"
987 # cherrypy.Server.thread_pool = 10
988 # cherrypy.config.update({'Server.socket_port': config["port"], 'Server.socket_host': config["host"]})
989
990 # cherrypy.config.update({'tools.auth_basic.on': True,
991 # 'tools.auth_basic.realm': 'localhost',
992 # 'tools.auth_basic.checkpassword': validate_password})
993 cherrypy.engine.subscribe('start', _start_service)
994 cherrypy.engine.subscribe('stop', _stop_service)
995 cherrypy.quickstart(Server(), '/osm', config_file)
996
997
998 def usage():
999 print("""Usage: {} [options]
1000 -c|--config [configuration_file]: loads the configuration file (default: ./nbi.cfg)
1001 -h|--help: shows this help
1002 """.format(sys.argv[0]))
1003 # --log-socket-host HOST: send logs to this host")
1004 # --log-socket-port PORT: send logs using this port (default: 9022)")
1005
1006
1007 if __name__ == '__main__':
1008 try:
1009 # load parameters and configuration
1010 opts, args = getopt.getopt(sys.argv[1:], "hvc:", ["config=", "help"])
1011 # TODO add "log-socket-host=", "log-socket-port=", "log-file="
1012 config_file = None
1013 for o, a in opts:
1014 if o in ("-h", "--help"):
1015 usage()
1016 sys.exit()
1017 elif o in ("-c", "--config"):
1018 config_file = a
1019 # elif o == "--log-socket-port":
1020 # log_socket_port = a
1021 # elif o == "--log-socket-host":
1022 # log_socket_host = a
1023 # elif o == "--log-file":
1024 # log_file = a
1025 else:
1026 assert False, "Unhandled option"
1027 if config_file:
1028 if not path.isfile(config_file):
1029 print("configuration file '{}' that not exist".format(config_file), file=sys.stderr)
1030 exit(1)
1031 else:
1032 for config_file in (__file__[:__file__.rfind(".")] + ".cfg", "./nbi.cfg", "/etc/osm/nbi.cfg"):
1033 if path.isfile(config_file):
1034 break
1035 else:
1036 print("No configuration file 'nbi.cfg' found neither at local folder nor at /etc/osm/", file=sys.stderr)
1037 exit(1)
1038 nbi(config_file)
1039 except getopt.GetoptError as e:
1040 print(str(e), file=sys.stderr)
1041 # usage()
1042 exit(1)