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