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