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