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