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