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