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