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