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