705979be5da7719e750eb9a73dc3ce82cf4c1058
[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 "nspm": {
341 "v1": {
342 "pm_jobs": {
343 "<ID>": {
344 "reports": {
345 "<ID>": {"METHODS": ("GET")}
346 }
347 },
348 },
349 },
350 },
351 }
352
353 def _format_in(self, kwargs):
354 try:
355 indata = None
356 if cherrypy.request.body.length:
357 error_text = "Invalid input format "
358
359 if "Content-Type" in cherrypy.request.headers:
360 if "application/json" in cherrypy.request.headers["Content-Type"]:
361 error_text = "Invalid json format "
362 indata = json.load(self.reader(cherrypy.request.body))
363 cherrypy.request.headers.pop("Content-File-MD5", None)
364 elif "application/yaml" in cherrypy.request.headers["Content-Type"]:
365 error_text = "Invalid yaml format "
366 indata = yaml.load(cherrypy.request.body)
367 cherrypy.request.headers.pop("Content-File-MD5", None)
368 elif "application/binary" in cherrypy.request.headers["Content-Type"] or \
369 "application/gzip" in cherrypy.request.headers["Content-Type"] or \
370 "application/zip" in cherrypy.request.headers["Content-Type"] or \
371 "text/plain" in cherrypy.request.headers["Content-Type"]:
372 indata = cherrypy.request.body # .read()
373 elif "multipart/form-data" in cherrypy.request.headers["Content-Type"]:
374 if "descriptor_file" in kwargs:
375 filecontent = kwargs.pop("descriptor_file")
376 if not filecontent.file:
377 raise NbiException("empty file or content", HTTPStatus.BAD_REQUEST)
378 indata = filecontent.file # .read()
379 if filecontent.content_type.value:
380 cherrypy.request.headers["Content-Type"] = filecontent.content_type.value
381 else:
382 # raise cherrypy.HTTPError(HTTPStatus.Not_Acceptable,
383 # "Only 'Content-Type' of type 'application/json' or
384 # 'application/yaml' for input format are available")
385 error_text = "Invalid yaml format "
386 indata = yaml.load(cherrypy.request.body)
387 cherrypy.request.headers.pop("Content-File-MD5", None)
388 else:
389 error_text = "Invalid yaml format "
390 indata = yaml.load(cherrypy.request.body)
391 cherrypy.request.headers.pop("Content-File-MD5", None)
392 if not indata:
393 indata = {}
394
395 format_yaml = False
396 if cherrypy.request.headers.get("Query-String-Format") == "yaml":
397 format_yaml = True
398
399 for k, v in kwargs.items():
400 if isinstance(v, str):
401 if v == "":
402 kwargs[k] = None
403 elif format_yaml:
404 try:
405 kwargs[k] = yaml.load(v)
406 except Exception:
407 pass
408 elif k.endswith(".gt") or k.endswith(".lt") or k.endswith(".gte") or k.endswith(".lte"):
409 try:
410 kwargs[k] = int(v)
411 except Exception:
412 try:
413 kwargs[k] = float(v)
414 except Exception:
415 pass
416 elif v.find(",") > 0:
417 kwargs[k] = v.split(",")
418 elif isinstance(v, (list, tuple)):
419 for index in range(0, len(v)):
420 if v[index] == "":
421 v[index] = None
422 elif format_yaml:
423 try:
424 v[index] = yaml.load(v[index])
425 except Exception:
426 pass
427
428 return indata
429 except (ValueError, yaml.YAMLError) as exc:
430 raise NbiException(error_text + str(exc), HTTPStatus.BAD_REQUEST)
431 except KeyError as exc:
432 raise NbiException("Query string error: " + str(exc), HTTPStatus.BAD_REQUEST)
433 except Exception as exc:
434 raise NbiException(error_text + str(exc), HTTPStatus.BAD_REQUEST)
435
436 @staticmethod
437 def _format_out(data, session=None, _format=None):
438 """
439 return string of dictionary data according to requested json, yaml, xml. By default json
440 :param data: response to be sent. Can be a dict, text or file
441 :param session:
442 :param _format: The format to be set as Content-Type ir data is a file
443 :return: None
444 """
445 accept = cherrypy.request.headers.get("Accept")
446 if data is None:
447 if accept and "text/html" in accept:
448 return html.format(data, cherrypy.request, cherrypy.response, session)
449 # cherrypy.response.status = HTTPStatus.NO_CONTENT.value
450 return
451 elif hasattr(data, "read"): # file object
452 if _format:
453 cherrypy.response.headers["Content-Type"] = _format
454 elif "b" in data.mode: # binariy asssumig zip
455 cherrypy.response.headers["Content-Type"] = 'application/zip'
456 else:
457 cherrypy.response.headers["Content-Type"] = 'text/plain'
458 # TODO check that cherrypy close file. If not implement pending things to close per thread next
459 return data
460 if accept:
461 if "application/json" in accept:
462 cherrypy.response.headers["Content-Type"] = 'application/json; charset=utf-8'
463 a = json.dumps(data, indent=4) + "\n"
464 return a.encode("utf8")
465 elif "text/html" in accept:
466 return html.format(data, cherrypy.request, cherrypy.response, session)
467
468 elif "application/yaml" in accept or "*/*" in accept or "text/plain" in accept:
469 pass
470 # if there is not any valid accept, raise an error. But if response is already an error, format in yaml
471 elif cherrypy.response.status >= 400:
472 raise cherrypy.HTTPError(HTTPStatus.NOT_ACCEPTABLE.value,
473 "Only 'Accept' of type 'application/json' or 'application/yaml' "
474 "for output format are available")
475 cherrypy.response.headers["Content-Type"] = 'application/yaml'
476 return yaml.safe_dump(data, explicit_start=True, indent=4, default_flow_style=False, tags=False,
477 encoding='utf-8', allow_unicode=True) # , canonical=True, default_style='"'
478
479 @cherrypy.expose
480 def index(self, *args, **kwargs):
481 session = None
482 try:
483 if cherrypy.request.method == "GET":
484 session = self.authenticator.authorize()
485 outdata = "Index page"
486 else:
487 raise cherrypy.HTTPError(HTTPStatus.METHOD_NOT_ALLOWED.value,
488 "Method {} not allowed for tokens".format(cherrypy.request.method))
489
490 return self._format_out(outdata, session)
491
492 except (EngineException, AuthException) as e:
493 cherrypy.log("index Exception {}".format(e))
494 cherrypy.response.status = e.http_code.value
495 return self._format_out("Welcome to OSM!", session)
496
497 @cherrypy.expose
498 def version(self, *args, **kwargs):
499 # TODO consider to remove and provide version using the static version file
500 global __version__, version_date
501 try:
502 if cherrypy.request.method != "GET":
503 raise NbiException("Only method GET is allowed", HTTPStatus.METHOD_NOT_ALLOWED)
504 elif args or kwargs:
505 raise NbiException("Invalid URL or query string for version", HTTPStatus.METHOD_NOT_ALLOWED)
506 return __version__ + " " + version_date
507 except NbiException as e:
508 cherrypy.response.status = e.http_code.value
509 problem_details = {
510 "code": e.http_code.name,
511 "status": e.http_code.value,
512 "detail": str(e),
513 }
514 return self._format_out(problem_details, None)
515
516 @cherrypy.expose
517 def token(self, method, token_id=None, kwargs=None):
518 session = None
519 # self.engine.load_dbase(cherrypy.request.app.config)
520 indata = self._format_in(kwargs)
521 if not isinstance(indata, dict):
522 raise NbiException("Expected application/yaml or application/json Content-Type", HTTPStatus.BAD_REQUEST)
523 try:
524 if method == "GET":
525 session = self.authenticator.authorize()
526 if token_id:
527 outdata = self.authenticator.get_token(session, token_id)
528 else:
529 outdata = self.authenticator.get_token_list(session)
530 elif method == "POST":
531 try:
532 session = self.authenticator.authorize()
533 except Exception:
534 session = None
535 if kwargs:
536 indata.update(kwargs)
537 outdata = self.authenticator.new_token(session, indata, cherrypy.request.remote)
538 session = outdata
539 cherrypy.session['Authorization'] = outdata["_id"]
540 self._set_location_header("admin", "v1", "tokens", outdata["_id"])
541 # cherrypy.response.cookie["Authorization"] = outdata["id"]
542 # cherrypy.response.cookie["Authorization"]['expires'] = 3600
543 elif method == "DELETE":
544 if not token_id and "id" in kwargs:
545 token_id = kwargs["id"]
546 elif not token_id:
547 session = self.authenticator.authorize()
548 token_id = session["_id"]
549 outdata = self.authenticator.del_token(token_id)
550 session = None
551 cherrypy.session['Authorization'] = "logout"
552 # cherrypy.response.cookie["Authorization"] = token_id
553 # cherrypy.response.cookie["Authorization"]['expires'] = 0
554 else:
555 raise NbiException("Method {} not allowed for token".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
556 return self._format_out(outdata, session)
557 except (NbiException, EngineException, DbException, AuthException) as e:
558 cherrypy.log("tokens Exception {}".format(e))
559 cherrypy.response.status = e.http_code.value
560 problem_details = {
561 "code": e.http_code.name,
562 "status": e.http_code.value,
563 "detail": str(e),
564 }
565 return self._format_out(problem_details, session)
566
567 @cherrypy.expose
568 def test(self, *args, **kwargs):
569 thread_info = None
570 if args and args[0] == "help":
571 return "<html><pre>\ninit\nfile/<name> download file\ndb-clear/table\nfs-clear[/folder]\nlogin\nlogin2\n"\
572 "sleep/<time>\nmessage/topic\n</pre></html>"
573
574 elif args and args[0] == "init":
575 try:
576 # self.engine.load_dbase(cherrypy.request.app.config)
577 self.engine.create_admin()
578 return "Done. User 'admin', password 'admin' created"
579 except Exception:
580 cherrypy.response.status = HTTPStatus.FORBIDDEN.value
581 return self._format_out("Database already initialized")
582 elif args and args[0] == "file":
583 return cherrypy.lib.static.serve_file(cherrypy.tree.apps['/osm'].config["storage"]["path"] + "/" + args[1],
584 "text/plain", "attachment")
585 elif args and args[0] == "file2":
586 f_path = cherrypy.tree.apps['/osm'].config["storage"]["path"] + "/" + args[1]
587 f = open(f_path, "r")
588 cherrypy.response.headers["Content-type"] = "text/plain"
589 return f
590
591 elif len(args) == 2 and args[0] == "db-clear":
592 deleted_info = self.engine.db.del_list(args[1], kwargs)
593 return "{} {} deleted\n".format(deleted_info["deleted"], args[1])
594 elif len(args) and args[0] == "fs-clear":
595 if len(args) >= 2:
596 folders = (args[1],)
597 else:
598 folders = self.engine.fs.dir_ls(".")
599 for folder in folders:
600 self.engine.fs.file_delete(folder)
601 return ",".join(folders) + " folders deleted\n"
602 elif args and args[0] == "login":
603 if not cherrypy.request.headers.get("Authorization"):
604 cherrypy.response.headers["WWW-Authenticate"] = 'Basic realm="Access to OSM site", charset="UTF-8"'
605 cherrypy.response.status = HTTPStatus.UNAUTHORIZED.value
606 elif args and args[0] == "login2":
607 if not cherrypy.request.headers.get("Authorization"):
608 cherrypy.response.headers["WWW-Authenticate"] = 'Bearer realm="Access to OSM site"'
609 cherrypy.response.status = HTTPStatus.UNAUTHORIZED.value
610 elif args and args[0] == "sleep":
611 sleep_time = 5
612 try:
613 sleep_time = int(args[1])
614 except Exception:
615 cherrypy.response.status = HTTPStatus.FORBIDDEN.value
616 return self._format_out("Database already initialized")
617 thread_info = cherrypy.thread_data
618 print(thread_info)
619 time.sleep(sleep_time)
620 # thread_info
621 elif len(args) >= 2 and args[0] == "message":
622 main_topic = args[1]
623 return_text = "<html><pre>{} ->\n".format(main_topic)
624 try:
625 if cherrypy.request.method == 'POST':
626 to_send = yaml.load(cherrypy.request.body)
627 for k, v in to_send.items():
628 self.engine.msg.write(main_topic, k, v)
629 return_text += " {}: {}\n".format(k, v)
630 elif cherrypy.request.method == 'GET':
631 for k, v in kwargs.items():
632 self.engine.msg.write(main_topic, k, yaml.load(v))
633 return_text += " {}: {}\n".format(k, yaml.load(v))
634 except Exception as e:
635 return_text += "Error: " + str(e)
636 return_text += "</pre></html>\n"
637 return return_text
638
639 return_text = (
640 "<html><pre>\nheaders:\n args: {}\n".format(args) +
641 " kwargs: {}\n".format(kwargs) +
642 " headers: {}\n".format(cherrypy.request.headers) +
643 " path_info: {}\n".format(cherrypy.request.path_info) +
644 " query_string: {}\n".format(cherrypy.request.query_string) +
645 " session: {}\n".format(cherrypy.session) +
646 " cookie: {}\n".format(cherrypy.request.cookie) +
647 " method: {}\n".format(cherrypy.request.method) +
648 " session: {}\n".format(cherrypy.session.get('fieldname')) +
649 " body:\n")
650 return_text += " length: {}\n".format(cherrypy.request.body.length)
651 if cherrypy.request.body.length:
652 return_text += " content: {}\n".format(
653 str(cherrypy.request.body.read(int(cherrypy.request.headers.get('Content-Length', 0)))))
654 if thread_info:
655 return_text += "thread: {}\n".format(thread_info)
656 return_text += "</pre></html>"
657 return return_text
658
659 def _check_valid_url_method(self, method, *args):
660 if len(args) < 3:
661 raise NbiException("URL must contain at least 'main_topic/version/topic'", HTTPStatus.METHOD_NOT_ALLOWED)
662
663 reference = self.valid_methods
664 for arg in args:
665 if arg is None:
666 break
667 if not isinstance(reference, dict):
668 raise NbiException("URL contains unexpected extra items '{}'".format(arg),
669 HTTPStatus.METHOD_NOT_ALLOWED)
670
671 if arg in reference:
672 reference = reference[arg]
673 elif "<ID>" in reference:
674 reference = reference["<ID>"]
675 elif "*" in reference:
676 reference = reference["*"]
677 break
678 else:
679 raise NbiException("Unexpected URL item {}".format(arg), HTTPStatus.METHOD_NOT_ALLOWED)
680 if "TODO" in reference and method in reference["TODO"]:
681 raise NbiException("Method {} not supported yet for this URL".format(method), HTTPStatus.NOT_IMPLEMENTED)
682 elif "METHODS" in reference and method not in reference["METHODS"]:
683 raise NbiException("Method {} not supported for this URL".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
684 return
685
686 @staticmethod
687 def _set_location_header(main_topic, version, topic, id):
688 """
689 Insert response header Location with the URL of created item base on URL params
690 :param main_topic:
691 :param version:
692 :param topic:
693 :param id:
694 :return: None
695 """
696 # Use cherrypy.request.base for absoluted path and make use of request.header HOST just in case behind aNAT
697 cherrypy.response.headers["Location"] = "/osm/{}/{}/{}/{}".format(main_topic, version, topic, id)
698 return
699
700 @cherrypy.expose
701 def default(self, main_topic=None, version=None, topic=None, _id=None, item=None, *args, **kwargs):
702 session = None
703 outdata = None
704 _format = None
705 method = "DONE"
706 engine_topic = None
707 rollback = []
708 session = None
709 try:
710 if not main_topic or not version or not topic:
711 raise NbiException("URL must contain at least 'main_topic/version/topic'",
712 HTTPStatus.METHOD_NOT_ALLOWED)
713 if main_topic not in ("admin", "vnfpkgm", "nsd", "nslcm", "pdu", "nst", "nsilcm", "nspm"):
714 raise NbiException("URL main_topic '{}' not supported".format(main_topic),
715 HTTPStatus.METHOD_NOT_ALLOWED)
716 if version != 'v1':
717 raise NbiException("URL version '{}' not supported".format(version), HTTPStatus.METHOD_NOT_ALLOWED)
718
719 if kwargs and "METHOD" in kwargs and kwargs["METHOD"] in ("PUT", "POST", "DELETE", "GET", "PATCH"):
720 method = kwargs.pop("METHOD")
721 else:
722 method = cherrypy.request.method
723 if kwargs and "FORCE" in kwargs:
724 force = kwargs.pop("FORCE")
725 else:
726 force = False
727 self._check_valid_url_method(method, main_topic, version, topic, _id, item, *args)
728 if main_topic == "admin" and topic == "tokens":
729 return self.token(method, _id, kwargs)
730
731 # self.engine.load_dbase(cherrypy.request.app.config)
732 session = self.authenticator.authorize()
733 indata = self._format_in(kwargs)
734 engine_topic = topic
735 if topic == "subscriptions":
736 engine_topic = main_topic + "_" + topic
737 if item and topic != "pm_jobs":
738 engine_topic = item
739
740 if main_topic == "nsd":
741 engine_topic = "nsds"
742 elif main_topic == "vnfpkgm":
743 engine_topic = "vnfds"
744 elif main_topic == "nslcm":
745 engine_topic = "nsrs"
746 if topic == "ns_lcm_op_occs":
747 engine_topic = "nslcmops"
748 if topic == "vnfrs" or topic == "vnf_instances":
749 engine_topic = "vnfrs"
750 elif main_topic == "nst":
751 engine_topic = "nsts"
752 elif main_topic == "nsilcm":
753 engine_topic = "nsis"
754 if topic == "nsi_lcm_op_occs":
755 engine_topic = "nsilcmops"
756 elif main_topic == "pdu":
757 engine_topic = "pdus"
758 if engine_topic == "vims": # TODO this is for backward compatibility, it will remove in the future
759 engine_topic = "vim_accounts"
760
761 if method == "GET":
762 if item in ("nsd_content", "package_content", "artifacts", "vnfd", "nsd", "nst", "nst_content"):
763 if item in ("vnfd", "nsd", "nst"):
764 path = "$DESCRIPTOR"
765 elif args:
766 path = args
767 elif item == "artifacts":
768 path = ()
769 else:
770 path = None
771 file, _format = self.engine.get_file(session, engine_topic, _id, path,
772 cherrypy.request.headers.get("Accept"))
773 outdata = file
774 elif not _id:
775 outdata = self.engine.get_item_list(session, engine_topic, kwargs)
776 else:
777 if item == "reports":
778 # TODO check that project_id (_id in this context) has permissions
779 _id = args[0]
780 outdata = self.engine.get_item(session, engine_topic, _id)
781 elif method == "POST":
782 if topic in ("ns_descriptors_content", "vnf_packages_content", "netslice_templates_content"):
783 _id = cherrypy.request.headers.get("Transaction-Id")
784 if not _id:
785 _id = self.engine.new_item(rollback, session, engine_topic, {}, None, cherrypy.request.headers,
786 force=force)
787 completed = self.engine.upload_content(session, engine_topic, _id, indata, kwargs,
788 cherrypy.request.headers, force=force)
789 if completed:
790 self._set_location_header(main_topic, version, topic, _id)
791 else:
792 cherrypy.response.headers["Transaction-Id"] = _id
793 outdata = {"id": _id}
794 elif topic == "ns_instances_content":
795 # creates NSR
796 _id = self.engine.new_item(rollback, session, engine_topic, indata, kwargs, force=force)
797 # creates nslcmop
798 indata["lcmOperationType"] = "instantiate"
799 indata["nsInstanceId"] = _id
800 self.engine.new_item(rollback, session, "nslcmops", indata, None)
801 self._set_location_header(main_topic, version, topic, _id)
802 outdata = {"id": _id}
803 elif topic == "ns_instances" and item:
804 indata["lcmOperationType"] = item
805 indata["nsInstanceId"] = _id
806 _id = self.engine.new_item(rollback, session, "nslcmops", indata, kwargs)
807 self._set_location_header(main_topic, version, "ns_lcm_op_occs", _id)
808 outdata = {"id": _id}
809 cherrypy.response.status = HTTPStatus.ACCEPTED.value
810 elif topic == "netslice_instances_content":
811 # creates NetSlice_Instance_record (NSIR)
812 _id = self.engine.new_item(rollback, session, engine_topic, indata, kwargs, force=force)
813 self._set_location_header(main_topic, version, topic, _id)
814 indata["lcmOperationType"] = "instantiate"
815 indata["nsiInstanceId"] = _id
816 self.engine.new_item(rollback, session, "nsilcmops", indata, kwargs)
817 outdata = {"id": _id}
818
819 elif topic == "netslice_instances" and item:
820 indata["lcmOperationType"] = item
821 indata["nsiInstanceId"] = _id
822 _id = self.engine.new_item(rollback, session, "nsilcmops", indata, kwargs)
823 self._set_location_header(main_topic, version, "nsi_lcm_op_occs", _id)
824 outdata = {"id": _id}
825 cherrypy.response.status = HTTPStatus.ACCEPTED.value
826 else:
827 _id = self.engine.new_item(rollback, session, engine_topic, indata, kwargs,
828 cherrypy.request.headers, force=force)
829 self._set_location_header(main_topic, version, topic, _id)
830 outdata = {"id": _id}
831 # TODO form NsdInfo when topic in ("ns_descriptors", "vnf_packages")
832 cherrypy.response.status = HTTPStatus.CREATED.value
833
834 elif method == "DELETE":
835 if not _id:
836 outdata = self.engine.del_item_list(session, engine_topic, kwargs)
837 cherrypy.response.status = HTTPStatus.OK.value
838 else: # len(args) > 1
839 delete_in_process = False
840 if topic == "ns_instances_content" and not force:
841 nslcmop_desc = {
842 "lcmOperationType": "terminate",
843 "nsInstanceId": _id,
844 "autoremove": True
845 }
846 opp_id = self.engine.new_item(rollback, session, "nslcmops", nslcmop_desc, None)
847 if opp_id:
848 delete_in_process = True
849 outdata = {"_id": opp_id}
850 cherrypy.response.status = HTTPStatus.ACCEPTED.value
851 elif topic == "netslice_instances_content" and not force:
852 nsilcmop_desc = {
853 "lcmOperationType": "terminate",
854 "nsiInstanceId": _id,
855 "autoremove": True
856 }
857 opp_id = self.engine.new_item(rollback, session, "nsilcmops", nsilcmop_desc, None)
858 if opp_id:
859 delete_in_process = True
860 outdata = {"_id": opp_id}
861 cherrypy.response.status = HTTPStatus.ACCEPTED.value
862 if not delete_in_process:
863 self.engine.del_item(session, engine_topic, _id, force)
864 cherrypy.response.status = HTTPStatus.NO_CONTENT.value
865 if engine_topic in ("vim_accounts", "wim_accounts", "sdns"):
866 cherrypy.response.status = HTTPStatus.ACCEPTED.value
867
868 elif method in ("PUT", "PATCH"):
869 outdata = None
870 if not indata and not kwargs:
871 raise NbiException("Nothing to update. Provide payload and/or query string",
872 HTTPStatus.BAD_REQUEST)
873 if item in ("nsd_content", "package_content", "nst_content") and method == "PUT":
874 completed = self.engine.upload_content(session, engine_topic, _id, indata, kwargs,
875 cherrypy.request.headers, force=force)
876 if not completed:
877 cherrypy.response.headers["Transaction-Id"] = id
878 else:
879 self.engine.edit_item(session, engine_topic, _id, indata, kwargs, force=force)
880 cherrypy.response.status = HTTPStatus.NO_CONTENT.value
881 else:
882 raise NbiException("Method {} not allowed".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
883 return self._format_out(outdata, session, _format)
884 except Exception as e:
885 if isinstance(e, (NbiException, EngineException, DbException, FsException, MsgException, AuthException,
886 ValidationError)):
887 http_code_value = cherrypy.response.status = e.http_code.value
888 http_code_name = e.http_code.name
889 cherrypy.log("Exception {}".format(e))
890 else:
891 http_code_value = cherrypy.response.status = HTTPStatus.BAD_REQUEST.value # INTERNAL_SERVER_ERROR
892 cherrypy.log("CRITICAL: Exception {}".format(e), traceback=True)
893 http_code_name = HTTPStatus.BAD_REQUEST.name
894 if hasattr(outdata, "close"): # is an open file
895 outdata.close()
896 error_text = str(e)
897 rollback.reverse()
898 for rollback_item in rollback:
899 try:
900 if rollback_item.get("operation") == "set":
901 self.engine.db.set_one(rollback_item["topic"], {"_id": rollback_item["_id"]},
902 rollback_item["content"], fail_on_empty=False)
903 else:
904 self.engine.db.del_one(rollback_item["topic"], {"_id": rollback_item["_id"]},
905 fail_on_empty=False)
906 except Exception as e2:
907 rollback_error_text = "Rollback Exception {}: {}".format(rollback_item, e2)
908 cherrypy.log(rollback_error_text)
909 error_text += ". " + rollback_error_text
910 # if isinstance(e, MsgException):
911 # error_text = "{} has been '{}' but other modules cannot be informed because an error on bus".format(
912 # engine_topic[:-1], method, error_text)
913 problem_details = {
914 "code": http_code_name,
915 "status": http_code_value,
916 "detail": error_text,
917 }
918 return self._format_out(problem_details, session)
919 # raise cherrypy.HTTPError(e.http_code.value, str(e))
920
921
922 def _start_service():
923 """
924 Callback function called when cherrypy.engine starts
925 Override configuration with env variables
926 Set database, storage, message configuration
927 Init database with admin/admin user password
928 """
929 global nbi_server
930 global subscription_thread
931 cherrypy.log.error("Starting osm_nbi")
932 # update general cherrypy configuration
933 update_dict = {}
934
935 engine_config = cherrypy.tree.apps['/osm'].config
936 for k, v in environ.items():
937 if not k.startswith("OSMNBI_"):
938 continue
939 k1, _, k2 = k[7:].lower().partition("_")
940 if not k2:
941 continue
942 try:
943 # update static configuration
944 if k == 'OSMNBI_STATIC_DIR':
945 engine_config["/static"]['tools.staticdir.dir'] = v
946 engine_config["/static"]['tools.staticdir.on'] = True
947 elif k == 'OSMNBI_SOCKET_PORT' or k == 'OSMNBI_SERVER_PORT':
948 update_dict['server.socket_port'] = int(v)
949 elif k == 'OSMNBI_SOCKET_HOST' or k == 'OSMNBI_SERVER_HOST':
950 update_dict['server.socket_host'] = v
951 elif k1 in ("server", "test", "auth", "log"):
952 update_dict[k1 + '.' + k2] = v
953 elif k1 in ("message", "database", "storage", "authentication"):
954 # k2 = k2.replace('_', '.')
955 if k2 in ("port", "db_port"):
956 engine_config[k1][k2] = int(v)
957 else:
958 engine_config[k1][k2] = v
959
960 except ValueError as e:
961 cherrypy.log.error("Ignoring environ '{}': " + str(e))
962 except Exception as e:
963 cherrypy.log.warn("skipping environ '{}' on exception '{}'".format(k, e))
964
965 if update_dict:
966 cherrypy.config.update(update_dict)
967 engine_config["global"].update(update_dict)
968
969 # logging cherrypy
970 log_format_simple = "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(message)s"
971 log_formatter_simple = logging.Formatter(log_format_simple, datefmt='%Y-%m-%dT%H:%M:%S')
972 logger_server = logging.getLogger("cherrypy.error")
973 logger_access = logging.getLogger("cherrypy.access")
974 logger_cherry = logging.getLogger("cherrypy")
975 logger_nbi = logging.getLogger("nbi")
976
977 if "log.file" in engine_config["global"]:
978 file_handler = logging.handlers.RotatingFileHandler(engine_config["global"]["log.file"],
979 maxBytes=100e6, backupCount=9, delay=0)
980 file_handler.setFormatter(log_formatter_simple)
981 logger_cherry.addHandler(file_handler)
982 logger_nbi.addHandler(file_handler)
983 # log always to standard output
984 for format_, logger in {"nbi.server %(filename)s:%(lineno)s": logger_server,
985 "nbi.access %(filename)s:%(lineno)s": logger_access,
986 "%(name)s %(filename)s:%(lineno)s": logger_nbi
987 }.items():
988 log_format_cherry = "%(asctime)s %(levelname)s {} %(message)s".format(format_)
989 log_formatter_cherry = logging.Formatter(log_format_cherry, datefmt='%Y-%m-%dT%H:%M:%S')
990 str_handler = logging.StreamHandler()
991 str_handler.setFormatter(log_formatter_cherry)
992 logger.addHandler(str_handler)
993
994 if engine_config["global"].get("log.level"):
995 logger_cherry.setLevel(engine_config["global"]["log.level"])
996 logger_nbi.setLevel(engine_config["global"]["log.level"])
997
998 # logging other modules
999 for k1, logname in {"message": "nbi.msg", "database": "nbi.db", "storage": "nbi.fs"}.items():
1000 engine_config[k1]["logger_name"] = logname
1001 logger_module = logging.getLogger(logname)
1002 if "logfile" in engine_config[k1]:
1003 file_handler = logging.handlers.RotatingFileHandler(engine_config[k1]["logfile"],
1004 maxBytes=100e6, backupCount=9, delay=0)
1005 file_handler.setFormatter(log_formatter_simple)
1006 logger_module.addHandler(file_handler)
1007 if "loglevel" in engine_config[k1]:
1008 logger_module.setLevel(engine_config[k1]["loglevel"])
1009 # TODO add more entries, e.g.: storage
1010 cherrypy.tree.apps['/osm'].root.engine.start(engine_config)
1011 cherrypy.tree.apps['/osm'].root.authenticator.start(engine_config)
1012 cherrypy.tree.apps['/osm'].root.engine.init_db(target_version=database_version)
1013 cherrypy.tree.apps['/osm'].root.authenticator.init_db(target_version=auth_database_version)
1014
1015 # start subscriptions thread:
1016 subscription_thread = SubscriptionThread(config=engine_config, engine=nbi_server.engine)
1017 subscription_thread.start()
1018 # Do not capture except SubscriptionException
1019
1020 # load and print version. Ignore possible errors, e.g. file not found
1021 try:
1022 with open("{}/version".format(engine_config["/static"]['tools.staticdir.dir'])) as version_file:
1023 version_data = version_file.read()
1024 cherrypy.log.error("Starting OSM NBI Version: {}".format(version_data.replace("\n", " ")))
1025 except Exception:
1026 pass
1027
1028
1029 def _stop_service():
1030 """
1031 Callback function called when cherrypy.engine stops
1032 TODO: Ending database connections.
1033 """
1034 global subscription_thread
1035 subscription_thread.terminate()
1036 subscription_thread = None
1037 cherrypy.tree.apps['/osm'].root.engine.stop()
1038 cherrypy.log.error("Stopping osm_nbi")
1039
1040
1041 def nbi(config_file):
1042 global nbi_server
1043 # conf = {
1044 # '/': {
1045 # #'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
1046 # 'tools.sessions.on': True,
1047 # 'tools.response_headers.on': True,
1048 # # 'tools.response_headers.headers': [('Content-Type', 'text/plain')],
1049 # }
1050 # }
1051 # cherrypy.Server.ssl_module = 'builtin'
1052 # cherrypy.Server.ssl_certificate = "http/cert.pem"
1053 # cherrypy.Server.ssl_private_key = "http/privkey.pem"
1054 # cherrypy.Server.thread_pool = 10
1055 # cherrypy.config.update({'Server.socket_port': config["port"], 'Server.socket_host': config["host"]})
1056
1057 # cherrypy.config.update({'tools.auth_basic.on': True,
1058 # 'tools.auth_basic.realm': 'localhost',
1059 # 'tools.auth_basic.checkpassword': validate_password})
1060 nbi_server = Server()
1061 cherrypy.engine.subscribe('start', _start_service)
1062 cherrypy.engine.subscribe('stop', _stop_service)
1063 cherrypy.quickstart(nbi_server, '/osm', config_file)
1064
1065
1066 def usage():
1067 print("""Usage: {} [options]
1068 -c|--config [configuration_file]: loads the configuration file (default: ./nbi.cfg)
1069 -h|--help: shows this help
1070 """.format(sys.argv[0]))
1071 # --log-socket-host HOST: send logs to this host")
1072 # --log-socket-port PORT: send logs using this port (default: 9022)")
1073
1074
1075 if __name__ == '__main__':
1076 try:
1077 # load parameters and configuration
1078 opts, args = getopt.getopt(sys.argv[1:], "hvc:", ["config=", "help"])
1079 # TODO add "log-socket-host=", "log-socket-port=", "log-file="
1080 config_file = None
1081 for o, a in opts:
1082 if o in ("-h", "--help"):
1083 usage()
1084 sys.exit()
1085 elif o in ("-c", "--config"):
1086 config_file = a
1087 # elif o == "--log-socket-port":
1088 # log_socket_port = a
1089 # elif o == "--log-socket-host":
1090 # log_socket_host = a
1091 # elif o == "--log-file":
1092 # log_file = a
1093 else:
1094 assert False, "Unhandled option"
1095 if config_file:
1096 if not path.isfile(config_file):
1097 print("configuration file '{}' that not exist".format(config_file), file=sys.stderr)
1098 exit(1)
1099 else:
1100 for config_file in (__file__[:__file__.rfind(".")] + ".cfg", "./nbi.cfg", "/etc/osm/nbi.cfg"):
1101 if path.isfile(config_file):
1102 break
1103 else:
1104 print("No configuration file 'nbi.cfg' found neither at local folder nor at /etc/osm/", file=sys.stderr)
1105 exit(1)
1106 nbi(config_file)
1107 except getopt.GetoptError as e:
1108 print(str(e), file=sys.stderr)
1109 # usage()
1110 exit(1)