nbi.py: initial commit for NetSlices with definition
[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 elif "application/yaml" in cherrypy.request.headers["Content-Type"]:
323 error_text = "Invalid yaml format "
324 indata = yaml.load(cherrypy.request.body)
325 elif "application/binary" in cherrypy.request.headers["Content-Type"] or \
326 "application/gzip" in cherrypy.request.headers["Content-Type"] or \
327 "application/zip" in cherrypy.request.headers["Content-Type"] or \
328 "text/plain" in cherrypy.request.headers["Content-Type"]:
329 indata = cherrypy.request.body # .read()
330 elif "multipart/form-data" in cherrypy.request.headers["Content-Type"]:
331 if "descriptor_file" in kwargs:
332 filecontent = kwargs.pop("descriptor_file")
333 if not filecontent.file:
334 raise NbiException("empty file or content", HTTPStatus.BAD_REQUEST)
335 indata = filecontent.file # .read()
336 if filecontent.content_type.value:
337 cherrypy.request.headers["Content-Type"] = filecontent.content_type.value
338 else:
339 # raise cherrypy.HTTPError(HTTPStatus.Not_Acceptable,
340 # "Only 'Content-Type' of type 'application/json' or
341 # 'application/yaml' for input format are available")
342 error_text = "Invalid yaml format "
343 indata = yaml.load(cherrypy.request.body)
344 else:
345 error_text = "Invalid yaml format "
346 indata = yaml.load(cherrypy.request.body)
347 if not indata:
348 indata = {}
349
350 format_yaml = False
351 if cherrypy.request.headers.get("Query-String-Format") == "yaml":
352 format_yaml = True
353
354 for k, v in kwargs.items():
355 if isinstance(v, str):
356 if v == "":
357 kwargs[k] = None
358 elif format_yaml:
359 try:
360 kwargs[k] = yaml.load(v)
361 except Exception:
362 pass
363 elif k.endswith(".gt") or k.endswith(".lt") or k.endswith(".gte") or k.endswith(".lte"):
364 try:
365 kwargs[k] = int(v)
366 except Exception:
367 try:
368 kwargs[k] = float(v)
369 except Exception:
370 pass
371 elif v.find(",") > 0:
372 kwargs[k] = v.split(",")
373 elif isinstance(v, (list, tuple)):
374 for index in range(0, len(v)):
375 if v[index] == "":
376 v[index] = None
377 elif format_yaml:
378 try:
379 v[index] = yaml.load(v[index])
380 except Exception:
381 pass
382
383 return indata
384 except (ValueError, yaml.YAMLError) as exc:
385 raise NbiException(error_text + str(exc), HTTPStatus.BAD_REQUEST)
386 except KeyError as exc:
387 raise NbiException("Query string error: " + str(exc), HTTPStatus.BAD_REQUEST)
388 except Exception as exc:
389 raise NbiException(error_text + str(exc), HTTPStatus.BAD_REQUEST)
390
391 @staticmethod
392 def _format_out(data, session=None, _format=None):
393 """
394 return string of dictionary data according to requested json, yaml, xml. By default json
395 :param data: response to be sent. Can be a dict, text or file
396 :param session:
397 :param _format: The format to be set as Content-Type ir data is a file
398 :return: None
399 """
400 accept = cherrypy.request.headers.get("Accept")
401 if data is None:
402 if accept and "text/html" in accept:
403 return html.format(data, cherrypy.request, cherrypy.response, session)
404 # cherrypy.response.status = HTTPStatus.NO_CONTENT.value
405 return
406 elif hasattr(data, "read"): # file object
407 if _format:
408 cherrypy.response.headers["Content-Type"] = _format
409 elif "b" in data.mode: # binariy asssumig zip
410 cherrypy.response.headers["Content-Type"] = 'application/zip'
411 else:
412 cherrypy.response.headers["Content-Type"] = 'text/plain'
413 # TODO check that cherrypy close file. If not implement pending things to close per thread next
414 return data
415 if accept:
416 if "application/json" in accept:
417 cherrypy.response.headers["Content-Type"] = 'application/json; charset=utf-8'
418 a = json.dumps(data, indent=4) + "\n"
419 return a.encode("utf8")
420 elif "text/html" in accept:
421 return html.format(data, cherrypy.request, cherrypy.response, session)
422
423 elif "application/yaml" in accept or "*/*" in accept or "text/plain" in accept:
424 pass
425 else:
426 raise cherrypy.HTTPError(HTTPStatus.NOT_ACCEPTABLE.value,
427 "Only 'Accept' of type 'application/json' or 'application/yaml' "
428 "for output format are available")
429 cherrypy.response.headers["Content-Type"] = 'application/yaml'
430 return yaml.safe_dump(data, explicit_start=True, indent=4, default_flow_style=False, tags=False,
431 encoding='utf-8', allow_unicode=True) # , canonical=True, default_style='"'
432
433 @cherrypy.expose
434 def index(self, *args, **kwargs):
435 session = None
436 try:
437 if cherrypy.request.method == "GET":
438 session = self.authenticator.authorize()
439 outdata = "Index page"
440 else:
441 raise cherrypy.HTTPError(HTTPStatus.METHOD_NOT_ALLOWED.value,
442 "Method {} not allowed for tokens".format(cherrypy.request.method))
443
444 return self._format_out(outdata, session)
445
446 except (EngineException, AuthException) as e:
447 cherrypy.log("index Exception {}".format(e))
448 cherrypy.response.status = e.http_code.value
449 return self._format_out("Welcome to OSM!", session)
450
451 @cherrypy.expose
452 def version(self, *args, **kwargs):
453 # TODO consider to remove and provide version using the static version file
454 global __version__, version_date
455 try:
456 if cherrypy.request.method != "GET":
457 raise NbiException("Only method GET is allowed", HTTPStatus.METHOD_NOT_ALLOWED)
458 elif args or kwargs:
459 raise NbiException("Invalid URL or query string for version", HTTPStatus.METHOD_NOT_ALLOWED)
460 return __version__ + " " + version_date
461 except NbiException as e:
462 cherrypy.response.status = e.http_code.value
463 problem_details = {
464 "code": e.http_code.name,
465 "status": e.http_code.value,
466 "detail": str(e),
467 }
468 return self._format_out(problem_details, None)
469
470 @cherrypy.expose
471 def token(self, method, token_id=None, kwargs=None):
472 session = None
473 # self.engine.load_dbase(cherrypy.request.app.config)
474 indata = self._format_in(kwargs)
475 if not isinstance(indata, dict):
476 raise NbiException("Expected application/yaml or application/json Content-Type", HTTPStatus.BAD_REQUEST)
477 try:
478 if method == "GET":
479 session = self.authenticator.authorize()
480 if token_id:
481 outdata = self.authenticator.get_token(session, token_id)
482 else:
483 outdata = self.authenticator.get_token_list(session)
484 elif method == "POST":
485 try:
486 session = self.authenticator.authorize()
487 except Exception:
488 session = None
489 if kwargs:
490 indata.update(kwargs)
491 outdata = self.authenticator.new_token(session, indata, cherrypy.request.remote)
492 session = outdata
493 cherrypy.session['Authorization'] = outdata["_id"]
494 self._set_location_header("admin", "v1", "tokens", outdata["_id"])
495 # cherrypy.response.cookie["Authorization"] = outdata["id"]
496 # cherrypy.response.cookie["Authorization"]['expires'] = 3600
497 elif method == "DELETE":
498 if not token_id and "id" in kwargs:
499 token_id = kwargs["id"]
500 elif not token_id:
501 session = self.authenticator.authorize()
502 token_id = session["_id"]
503 outdata = self.authenticator.del_token(token_id)
504 session = None
505 cherrypy.session['Authorization'] = "logout"
506 # cherrypy.response.cookie["Authorization"] = token_id
507 # cherrypy.response.cookie["Authorization"]['expires'] = 0
508 else:
509 raise NbiException("Method {} not allowed for token".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
510 return self._format_out(outdata, session)
511 except (NbiException, EngineException, DbException, AuthException) as e:
512 cherrypy.log("tokens Exception {}".format(e))
513 cherrypy.response.status = e.http_code.value
514 problem_details = {
515 "code": e.http_code.name,
516 "status": e.http_code.value,
517 "detail": str(e),
518 }
519 return self._format_out(problem_details, session)
520
521 @cherrypy.expose
522 def test(self, *args, **kwargs):
523 thread_info = None
524 if args and args[0] == "help":
525 return "<html><pre>\ninit\nfile/<name> download file\ndb-clear/table\nprune\nlogin\nlogin2\n"\
526 "sleep/<time>\nmessage/topic\n</pre></html>"
527
528 elif args and args[0] == "init":
529 try:
530 # self.engine.load_dbase(cherrypy.request.app.config)
531 self.engine.create_admin()
532 return "Done. User 'admin', password 'admin' created"
533 except Exception:
534 cherrypy.response.status = HTTPStatus.FORBIDDEN.value
535 return self._format_out("Database already initialized")
536 elif args and args[0] == "file":
537 return cherrypy.lib.static.serve_file(cherrypy.tree.apps['/osm'].config["storage"]["path"] + "/" + args[1],
538 "text/plain", "attachment")
539 elif args and args[0] == "file2":
540 f_path = cherrypy.tree.apps['/osm'].config["storage"]["path"] + "/" + args[1]
541 f = open(f_path, "r")
542 cherrypy.response.headers["Content-type"] = "text/plain"
543 return f
544
545 elif len(args) == 2 and args[0] == "db-clear":
546 return self.engine.db.del_list(args[1], kwargs)
547 elif args and args[0] == "prune":
548 return self.engine.prune()
549 elif args and args[0] == "login":
550 if not cherrypy.request.headers.get("Authorization"):
551 cherrypy.response.headers["WWW-Authenticate"] = 'Basic realm="Access to OSM site", charset="UTF-8"'
552 cherrypy.response.status = HTTPStatus.UNAUTHORIZED.value
553 elif args and args[0] == "login2":
554 if not cherrypy.request.headers.get("Authorization"):
555 cherrypy.response.headers["WWW-Authenticate"] = 'Bearer realm="Access to OSM site"'
556 cherrypy.response.status = HTTPStatus.UNAUTHORIZED.value
557 elif args and args[0] == "sleep":
558 sleep_time = 5
559 try:
560 sleep_time = int(args[1])
561 except Exception:
562 cherrypy.response.status = HTTPStatus.FORBIDDEN.value
563 return self._format_out("Database already initialized")
564 thread_info = cherrypy.thread_data
565 print(thread_info)
566 time.sleep(sleep_time)
567 # thread_info
568 elif len(args) >= 2 and args[0] == "message":
569 main_topic = args[1]
570 return_text = "<html><pre>{} ->\n".format(main_topic)
571 try:
572 if cherrypy.request.method == 'POST':
573 to_send = yaml.load(cherrypy.request.body)
574 for k, v in to_send.items():
575 self.engine.msg.write(main_topic, k, v)
576 return_text += " {}: {}\n".format(k, v)
577 elif cherrypy.request.method == 'GET':
578 for k, v in kwargs.items():
579 self.engine.msg.write(main_topic, k, yaml.load(v))
580 return_text += " {}: {}\n".format(k, yaml.load(v))
581 except Exception as e:
582 return_text += "Error: " + str(e)
583 return_text += "</pre></html>\n"
584 return return_text
585
586 return_text = (
587 "<html><pre>\nheaders:\n args: {}\n".format(args) +
588 " kwargs: {}\n".format(kwargs) +
589 " headers: {}\n".format(cherrypy.request.headers) +
590 " path_info: {}\n".format(cherrypy.request.path_info) +
591 " query_string: {}\n".format(cherrypy.request.query_string) +
592 " session: {}\n".format(cherrypy.session) +
593 " cookie: {}\n".format(cherrypy.request.cookie) +
594 " method: {}\n".format(cherrypy.request.method) +
595 " session: {}\n".format(cherrypy.session.get('fieldname')) +
596 " body:\n")
597 return_text += " length: {}\n".format(cherrypy.request.body.length)
598 if cherrypy.request.body.length:
599 return_text += " content: {}\n".format(
600 str(cherrypy.request.body.read(int(cherrypy.request.headers.get('Content-Length', 0)))))
601 if thread_info:
602 return_text += "thread: {}\n".format(thread_info)
603 return_text += "</pre></html>"
604 return return_text
605
606 def _check_valid_url_method(self, method, *args):
607 if len(args) < 3:
608 raise NbiException("URL must contain at least 'main_topic/version/topic'", HTTPStatus.METHOD_NOT_ALLOWED)
609
610 reference = self.valid_methods
611 for arg in args:
612 if arg is None:
613 break
614 if not isinstance(reference, dict):
615 raise NbiException("URL contains unexpected extra items '{}'".format(arg),
616 HTTPStatus.METHOD_NOT_ALLOWED)
617
618 if arg in reference:
619 reference = reference[arg]
620 elif "<ID>" in reference:
621 reference = reference["<ID>"]
622 elif "*" in reference:
623 reference = reference["*"]
624 break
625 else:
626 raise NbiException("Unexpected URL item {}".format(arg), HTTPStatus.METHOD_NOT_ALLOWED)
627 if "TODO" in reference and method in reference["TODO"]:
628 raise NbiException("Method {} not supported yet for this URL".format(method), HTTPStatus.NOT_IMPLEMENTED)
629 elif "METHODS" in reference and method not in reference["METHODS"]:
630 raise NbiException("Method {} not supported for this URL".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
631 return
632
633 @staticmethod
634 def _set_location_header(main_topic, version, topic, id):
635 """
636 Insert response header Location with the URL of created item base on URL params
637 :param main_topic:
638 :param version:
639 :param topic:
640 :param id:
641 :return: None
642 """
643 # Use cherrypy.request.base for absoluted path and make use of request.header HOST just in case behind aNAT
644 cherrypy.response.headers["Location"] = "/osm/{}/{}/{}/{}".format(main_topic, version, topic, id)
645 return
646
647 @cherrypy.expose
648 def default(self, main_topic=None, version=None, topic=None, _id=None, item=None, *args, **kwargs):
649 session = None
650 outdata = None
651 _format = None
652 method = "DONE"
653 engine_topic = None
654 rollback = []
655 session = None
656 try:
657 if not main_topic or not version or not topic:
658 raise NbiException("URL must contain at least 'main_topic/version/topic'",
659 HTTPStatus.METHOD_NOT_ALLOWED)
660 if main_topic not in ("admin", "vnfpkgm", "nsd", "nslcm"):
661 raise NbiException("URL main_topic '{}' not supported".format(main_topic),
662 HTTPStatus.METHOD_NOT_ALLOWED)
663 if version != 'v1':
664 raise NbiException("URL version '{}' not supported".format(version), HTTPStatus.METHOD_NOT_ALLOWED)
665
666 if kwargs and "METHOD" in kwargs and kwargs["METHOD"] in ("PUT", "POST", "DELETE", "GET", "PATCH"):
667 method = kwargs.pop("METHOD")
668 else:
669 method = cherrypy.request.method
670 if kwargs and "FORCE" in kwargs:
671 force = kwargs.pop("FORCE")
672 else:
673 force = False
674
675 self._check_valid_url_method(method, main_topic, version, topic, _id, item, *args)
676
677 if main_topic == "admin" and topic == "tokens":
678 return self.token(method, _id, kwargs)
679
680 # self.engine.load_dbase(cherrypy.request.app.config)
681 session = self.authenticator.authorize()
682 indata = self._format_in(kwargs)
683 engine_topic = topic
684 if topic == "subscriptions":
685 engine_topic = main_topic + "_" + topic
686 if item:
687 engine_topic = item
688
689 if main_topic == "nsd":
690 engine_topic = "nsds"
691 elif main_topic == "vnfpkgm":
692 engine_topic = "vnfds"
693 elif main_topic == "nslcm":
694 engine_topic = "nsrs"
695 if topic == "ns_lcm_op_occs":
696 engine_topic = "nslcmops"
697 if topic == "vnfrs" or topic == "vnf_instances":
698 engine_topic = "vnfrs"
699 elif main_topic == "nst":
700 engine_topic = "nsts"
701 elif main_topic == "nsilcm":
702 engine_topic = "nsis"
703 if topic == "nsi_lcm_op_occs":
704 engine_topic = "nsilcmops"
705 elif main_topic == "pdu":
706 engine_topic = "pdus"
707 if engine_topic == "vims": # TODO this is for backward compatibility, it will remove in the future
708 engine_topic = "vim_accounts"
709
710 if method == "GET":
711 if item in ("nsd_content", "package_content", "artifacts", "vnfd", "nsd", "nst", "nst_content"):
712 if item in ("vnfd", "nsd", "nst"):
713 path = "$DESCRIPTOR"
714 elif args:
715 path = args
716 elif item == "artifacts":
717 path = ()
718 else:
719 path = None
720 file, _format = self.engine.get_file(session, engine_topic, _id, path,
721 cherrypy.request.headers.get("Accept"))
722 outdata = file
723 elif not _id:
724 outdata = self.engine.get_item_list(session, engine_topic, kwargs)
725 else:
726 outdata = self.engine.get_item(session, engine_topic, _id)
727 elif method == "POST":
728 if topic in ("ns_descriptors_content", "vnf_packages_content", "netslice_templates_content"):
729 _id = cherrypy.request.headers.get("Transaction-Id")
730 if not _id:
731 _id = self.engine.new_item(rollback, session, engine_topic, {}, None, cherrypy.request.headers,
732 force=force)
733 completed = self.engine.upload_content(session, engine_topic, _id, indata, kwargs,
734 cherrypy.request.headers, force=force)
735 if completed:
736 self._set_location_header(main_topic, version, topic, _id)
737 else:
738 cherrypy.response.headers["Transaction-Id"] = _id
739 outdata = {"id": _id}
740 elif topic == "ns_instances_content":
741 # creates NSR
742 _id = self.engine.new_item(rollback, session, engine_topic, indata, kwargs, force=force)
743 # creates nslcmop
744 indata["lcmOperationType"] = "instantiate"
745 indata["nsInstanceId"] = _id
746 self.engine.new_item(rollback, session, "nslcmops", indata, None)
747 self._set_location_header(main_topic, version, topic, _id)
748 outdata = {"id": _id}
749 elif topic == "ns_instances" and item:
750 indata["lcmOperationType"] = item
751 indata["nsInstanceId"] = _id
752 _id = self.engine.new_item(rollback, session, "nslcmops", indata, kwargs)
753 self._set_location_header(main_topic, version, "ns_lcm_op_occs", _id)
754 outdata = {"id": _id}
755 cherrypy.response.status = HTTPStatus.ACCEPTED.value
756 elif topic == "netslice_instances_content":
757 # creates NSI
758 _id = self.engine.new_item(rollback, session, engine_topic, indata, kwargs, force=force)
759 # creates nsilcmop
760 indata["lcmOperationType"] = "instantiate"
761 indata["nsiInstanceId"] = _id
762 self.engine.new_item(rollback, session, "nsilcmops", indata, None)
763 self._set_location_header(main_topic, version, topic, _id)
764 outdata = {"id": _id}
765 elif topic == "netslice_instances" and item:
766 indata["lcmOperationType"] = item
767 indata["nsiInstanceId"] = _id
768 _id = self.engine.new_item(rollback, session, "nsilcmops", indata, kwargs)
769 self._set_location_header(main_topic, version, "nsi_lcm_op_occs", _id)
770 outdata = {"id": _id}
771 cherrypy.response.status = HTTPStatus.ACCEPTED.value
772 else:
773 _id = self.engine.new_item(rollback, session, engine_topic, indata, kwargs,
774 cherrypy.request.headers, force=force)
775 self._set_location_header(main_topic, version, topic, _id)
776 outdata = {"id": _id}
777 # TODO form NsdInfo when topic in ("ns_descriptors", "vnf_packages")
778 cherrypy.response.status = HTTPStatus.CREATED.value
779
780 elif method == "DELETE":
781 if not _id:
782 outdata = self.engine.del_item_list(session, engine_topic, kwargs)
783 cherrypy.response.status = HTTPStatus.OK.value
784 else: # len(args) > 1
785 if topic == "ns_instances_content" and not force:
786 nslcmop_desc = {
787 "lcmOperationType": "terminate",
788 "nsInstanceId": _id,
789 "autoremove": True
790 }
791 opp_id = self.engine.new_item(rollback, session, "nslcmops", nslcmop_desc, None)
792 outdata = {"_id": opp_id}
793 cherrypy.response.status = HTTPStatus.ACCEPTED.value
794 elif topic == "netslice_instances_content" and not force:
795 nsilcmop_desc = {
796 "lcmOperationType": "terminate",
797 "nsiInstanceId": _id,
798 "autoremove": True
799 }
800 opp_id = self.engine.new_item(rollback, session, "nsilcmops", nsilcmop_desc, None)
801 outdata = {"_id": opp_id}
802 cherrypy.response.status = HTTPStatus.ACCEPTED.value
803 else:
804 self.engine.del_item(session, engine_topic, _id, force)
805 cherrypy.response.status = HTTPStatus.NO_CONTENT.value
806 if engine_topic in ("vim_accounts", "sdns"):
807 cherrypy.response.status = HTTPStatus.ACCEPTED.value
808
809 elif method in ("PUT", "PATCH"):
810 outdata = None
811 if not indata and not kwargs:
812 raise NbiException("Nothing to update. Provide payload and/or query string",
813 HTTPStatus.BAD_REQUEST)
814 if item in ("nsd_content", "package_content", "nst_content") and method == "PUT":
815 completed = self.engine.upload_content(session, engine_topic, _id, indata, kwargs,
816 cherrypy.request.headers, force=force)
817 if not completed:
818 cherrypy.response.headers["Transaction-Id"] = id
819 else:
820 self.engine.edit_item(session, engine_topic, _id, indata, kwargs, force=force)
821 cherrypy.response.status = HTTPStatus.NO_CONTENT.value
822 else:
823 raise NbiException("Method {} not allowed".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
824 return self._format_out(outdata, session, _format)
825 except Exception as e:
826 if isinstance(e, (NbiException, EngineException, DbException, FsException, MsgException, AuthException)):
827 http_code_value = cherrypy.response.status = e.http_code.value
828 http_code_name = e.http_code.name
829 cherrypy.log("Exception {}".format(e))
830 else:
831 http_code_value = cherrypy.response.status = HTTPStatus.BAD_REQUEST.value # INTERNAL_SERVER_ERROR
832 cherrypy.log("CRITICAL: Exception {}".format(e))
833 http_code_name = HTTPStatus.BAD_REQUEST.name
834 if hasattr(outdata, "close"): # is an open file
835 outdata.close()
836 error_text = str(e)
837 rollback.reverse()
838 for rollback_item in rollback:
839 try:
840 self.engine.del_item(**rollback_item, session=session, force=True)
841 except Exception as e2:
842 rollback_error_text = "Rollback Exception {}: {}".format(rollback_item, e2)
843 cherrypy.log(rollback_error_text)
844 error_text += ". " + rollback_error_text
845 # if isinstance(e, MsgException):
846 # error_text = "{} has been '{}' but other modules cannot be informed because an error on bus".format(
847 # engine_topic[:-1], method, error_text)
848 problem_details = {
849 "code": http_code_name,
850 "status": http_code_value,
851 "detail": error_text,
852 }
853 return self._format_out(problem_details, session)
854 # raise cherrypy.HTTPError(e.http_code.value, str(e))
855
856
857 # def validate_password(realm, username, password):
858 # cherrypy.log("realm "+ str(realm))
859 # if username == "admin" and password == "admin":
860 # return True
861 # return False
862
863
864 def _start_service():
865 """
866 Callback function called when cherrypy.engine starts
867 Override configuration with env variables
868 Set database, storage, message configuration
869 Init database with admin/admin user password
870 """
871 cherrypy.log.error("Starting osm_nbi")
872 # update general cherrypy configuration
873 update_dict = {}
874
875 engine_config = cherrypy.tree.apps['/osm'].config
876 for k, v in environ.items():
877 if not k.startswith("OSMNBI_"):
878 continue
879 k1, _, k2 = k[7:].lower().partition("_")
880 if not k2:
881 continue
882 try:
883 # update static configuration
884 if k == 'OSMNBI_STATIC_DIR':
885 engine_config["/static"]['tools.staticdir.dir'] = v
886 engine_config["/static"]['tools.staticdir.on'] = True
887 elif k == 'OSMNBI_SOCKET_PORT' or k == 'OSMNBI_SERVER_PORT':
888 update_dict['server.socket_port'] = int(v)
889 elif k == 'OSMNBI_SOCKET_HOST' or k == 'OSMNBI_SERVER_HOST':
890 update_dict['server.socket_host'] = v
891 elif k1 in ("server", "test", "auth", "log"):
892 update_dict[k1 + '.' + k2] = v
893 elif k1 in ("message", "database", "storage", "authentication"):
894 # k2 = k2.replace('_', '.')
895 if k2 in ("port", "db_port"):
896 engine_config[k1][k2] = int(v)
897 else:
898 engine_config[k1][k2] = v
899
900 except ValueError as e:
901 cherrypy.log.error("Ignoring environ '{}': " + str(e))
902 except Exception as e:
903 cherrypy.log.warn("skipping environ '{}' on exception '{}'".format(k, e))
904
905 if update_dict:
906 cherrypy.config.update(update_dict)
907 engine_config["global"].update(update_dict)
908
909 # logging cherrypy
910 log_format_simple = "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(message)s"
911 log_formatter_simple = logging.Formatter(log_format_simple, datefmt='%Y-%m-%dT%H:%M:%S')
912 logger_server = logging.getLogger("cherrypy.error")
913 logger_access = logging.getLogger("cherrypy.access")
914 logger_cherry = logging.getLogger("cherrypy")
915 logger_nbi = logging.getLogger("nbi")
916
917 if "log.file" in engine_config["global"]:
918 file_handler = logging.handlers.RotatingFileHandler(engine_config["global"]["log.file"],
919 maxBytes=100e6, backupCount=9, delay=0)
920 file_handler.setFormatter(log_formatter_simple)
921 logger_cherry.addHandler(file_handler)
922 logger_nbi.addHandler(file_handler)
923 # log always to standard output
924 for format_, logger in {"nbi.server %(filename)s:%(lineno)s": logger_server,
925 "nbi.access %(filename)s:%(lineno)s": logger_access,
926 "%(name)s %(filename)s:%(lineno)s": logger_nbi
927 }.items():
928 log_format_cherry = "%(asctime)s %(levelname)s {} %(message)s".format(format_)
929 log_formatter_cherry = logging.Formatter(log_format_cherry, datefmt='%Y-%m-%dT%H:%M:%S')
930 str_handler = logging.StreamHandler()
931 str_handler.setFormatter(log_formatter_cherry)
932 logger.addHandler(str_handler)
933
934 if engine_config["global"].get("log.level"):
935 logger_cherry.setLevel(engine_config["global"]["log.level"])
936 logger_nbi.setLevel(engine_config["global"]["log.level"])
937
938 # logging other modules
939 for k1, logname in {"message": "nbi.msg", "database": "nbi.db", "storage": "nbi.fs"}.items():
940 engine_config[k1]["logger_name"] = logname
941 logger_module = logging.getLogger(logname)
942 if "logfile" in engine_config[k1]:
943 file_handler = logging.handlers.RotatingFileHandler(engine_config[k1]["logfile"],
944 maxBytes=100e6, backupCount=9, delay=0)
945 file_handler.setFormatter(log_formatter_simple)
946 logger_module.addHandler(file_handler)
947 if "loglevel" in engine_config[k1]:
948 logger_module.setLevel(engine_config[k1]["loglevel"])
949 # TODO add more entries, e.g.: storage
950 cherrypy.tree.apps['/osm'].root.engine.start(engine_config)
951 cherrypy.tree.apps['/osm'].root.authenticator.start(engine_config)
952 try:
953 cherrypy.tree.apps['/osm'].root.engine.init_db(target_version=database_version)
954 cherrypy.tree.apps['/osm'].root.authenticator.init_db(target_version=auth_database_version)
955 except (EngineException, AuthException):
956 pass
957 # getenv('OSMOPENMANO_TENANT', None)
958
959
960 def _stop_service():
961 """
962 Callback function called when cherrypy.engine stops
963 TODO: Ending database connections.
964 """
965 cherrypy.tree.apps['/osm'].root.engine.stop()
966 cherrypy.log.error("Stopping osm_nbi")
967
968
969 def nbi(config_file):
970 # conf = {
971 # '/': {
972 # #'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
973 # 'tools.sessions.on': True,
974 # 'tools.response_headers.on': True,
975 # # 'tools.response_headers.headers': [('Content-Type', 'text/plain')],
976 # }
977 # }
978 # cherrypy.Server.ssl_module = 'builtin'
979 # cherrypy.Server.ssl_certificate = "http/cert.pem"
980 # cherrypy.Server.ssl_private_key = "http/privkey.pem"
981 # cherrypy.Server.thread_pool = 10
982 # cherrypy.config.update({'Server.socket_port': config["port"], 'Server.socket_host': config["host"]})
983
984 # cherrypy.config.update({'tools.auth_basic.on': True,
985 # 'tools.auth_basic.realm': 'localhost',
986 # 'tools.auth_basic.checkpassword': validate_password})
987 cherrypy.engine.subscribe('start', _start_service)
988 cherrypy.engine.subscribe('stop', _stop_service)
989 cherrypy.quickstart(Server(), '/osm', config_file)
990
991
992 def usage():
993 print("""Usage: {} [options]
994 -c|--config [configuration_file]: loads the configuration file (default: ./nbi.cfg)
995 -h|--help: shows this help
996 """.format(sys.argv[0]))
997 # --log-socket-host HOST: send logs to this host")
998 # --log-socket-port PORT: send logs using this port (default: 9022)")
999
1000
1001 if __name__ == '__main__':
1002 try:
1003 # load parameters and configuration
1004 opts, args = getopt.getopt(sys.argv[1:], "hvc:", ["config=", "help"])
1005 # TODO add "log-socket-host=", "log-socket-port=", "log-file="
1006 config_file = None
1007 for o, a in opts:
1008 if o in ("-h", "--help"):
1009 usage()
1010 sys.exit()
1011 elif o in ("-c", "--config"):
1012 config_file = a
1013 # elif o == "--log-socket-port":
1014 # log_socket_port = a
1015 # elif o == "--log-socket-host":
1016 # log_socket_host = a
1017 # elif o == "--log-file":
1018 # log_file = a
1019 else:
1020 assert False, "Unhandled option"
1021 if config_file:
1022 if not path.isfile(config_file):
1023 print("configuration file '{}' that not exist".format(config_file), file=sys.stderr)
1024 exit(1)
1025 else:
1026 for config_file in (__file__[:__file__.rfind(".")] + ".cfg", "./nbi.cfg", "/etc/osm/nbi.cfg"):
1027 if path.isfile(config_file):
1028 break
1029 else:
1030 print("No configuration file 'nbi.cfg' found neither at local folder nor at /etc/osm/", file=sys.stderr)
1031 exit(1)
1032 nbi(config_file)
1033 except getopt.GetoptError as e:
1034 print(str(e), file=sys.stderr)
1035 # usage()
1036 exit(1)