Bug 2024
[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
50 """
51 North Bound Interface (O: OSM specific; 5,X: SOL005 not implemented yet; O5: SOL005 implemented)
52 URL: /osm GET POST PUT DELETE PATCH
53 /nsd/v1
54 /ns_descriptors_content O O
55 /<nsdInfoId> O O O O
56 /ns_descriptors O5 O5
57 /<nsdInfoId> O5 O5 5
58 /nsd_content O5 O5
59 /nsd O
60 /artifacts[/<artifactPath>] O
61 /pnf_descriptors 5 5
62 /<pnfdInfoId> 5 5 5
63 /pnfd_content 5 5
64 /subscriptions 5 5
65 /<subscriptionId> 5 X
66
67 /vnfpkgm/v1
68 /vnf_packages_content O O
69 /<vnfPkgId> O O
70 /vnf_packages O5 O5
71 /<vnfPkgId> O5 O5 5
72 /package_content O5 O5
73 /upload_from_uri X
74 /vnfd O5
75 /artifacts[/<artifactPath>] O5
76 /subscriptions X X
77 /<subscriptionId> X X
78
79 /nslcm/v1
80 /ns_instances_content O O
81 /<nsInstanceId> O O
82 /ns_instances 5 5
83 /<nsInstanceId> O5 O5
84 instantiate O5
85 terminate O5
86 action O
87 scale O5
88 migrate O
89 heal 5
90 update 05
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 /osmrepos O O
121 /<id> O O
122
123 /nst/v1 O O
124 /netslice_templates_content O O
125 /<nstInfoId> O O O O
126 /netslice_templates O O
127 /<nstInfoId> O O O
128 /nst_content O O
129 /nst O
130 /artifacts[/<artifactPath>] O
131 /subscriptions X X
132 /<subscriptionId> X X
133
134 /nsilcm/v1
135 /netslice_instances_content O O
136 /<SliceInstanceId> O O
137 /netslice_instances O O
138 /<SliceInstanceId> O O
139 instantiate O
140 terminate O
141 action O
142 /nsi_lcm_op_occs O O
143 /<nsiLcmOpOccId> O O O
144 /subscriptions X X
145 /<subscriptionId> X X
146
147 query string:
148 Follows SOL005 section 4.3.2 It contains extra METHOD to override http method, FORCE to force.
149 simpleFilterExpr := <attrName>["."<attrName>]*["."<op>]"="<value>[","<value>]*
150 filterExpr := <simpleFilterExpr>["&"<simpleFilterExpr>]*
151 op := "eq" | "neq" (or "ne") | "gt" | "lt" | "gte" | "lte" | "cont" | "ncont"
152 attrName := string
153 For filtering inside array, it must select the element of the array, or add ANYINDEX to apply the filtering over any
154 item of the array, that is, pass if any item of the array pass the filter.
155 It allows both ne and neq for not equal
156 TODO: 4.3.3 Attribute selectors
157 all_fields, fields=x,y,.., exclude_default, exclude_fields=x,y,...
158 (none) … same as “exclude_default”
159 all_fields … all attributes.
160 fields=<list> … all attributes except all complex attributes with minimum cardinality of zero that are not
161 conditionally mandatory, and that are not provided in <list>.
162 exclude_fields=<list> … all attributes except those complex attributes with a minimum cardinality of zero that
163 are not conditionally mandatory, and that are provided in <list>.
164 exclude_default … all attributes except those complex attributes with a minimum cardinality of zero that are not
165 conditionally mandatory, and that are part of the "default exclude set" defined in the present specification for
166 the particular resource
167 exclude_default and include=<list> … all attributes except those complex attributes with a minimum cardinality
168 of zero that are not conditionally mandatory and that are part of the "default exclude set" defined in the
169 present specification for the particular resource, but that are not part of <list>
170 Additionally it admits some administrator values:
171 FORCE: To force operations skipping dependency checkings
172 ADMIN: To act as an administrator or a different project
173 PUBLIC: To get public descriptors or set a descriptor as public
174 SET_PROJECT: To make a descriptor available for other project
175
176 Header field name Reference Example Descriptions
177 Accept IETF RFC 7231 [19] application/json Content-Types that are acceptable for the response.
178 This header field shall be present if the response is expected to have a non-empty message body.
179 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the request.
180 This header field shall be present if the request has a non-empty message body.
181 Authorization IETF RFC 7235 [22] Bearer mF_9.B5f-4.1JqM The authorization token for the request.
182 Details are specified in clause 4.5.3.
183 Range IETF RFC 7233 [21] 1000-2000 Requested range of bytes from a file
184 Header field name Reference Example Descriptions
185 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the response.
186 This header field shall be present if the response has a non-empty message body.
187 Location IETF RFC 7231 [19] http://www.example.com/vnflcm/v1/vnf_instances/123 Used in redirection, or when a
188 new resource has been created.
189 This header field shall be present if the response status code is 201 or 3xx.
190 In the present document this header field is also used if the response status code is 202 and a new resource was
191 created.
192 WWW-Authenticate IETF RFC 7235 [22] Bearer realm="example" Challenge if the corresponding HTTP request has not
193 provided authorization, or error details if the corresponding HTTP request has provided an invalid authorization
194 token.
195 Accept-Ranges IETF RFC 7233 [21] bytes Used by the Server to signal whether or not it supports ranges for
196 certain resources.
197 Content-Range IETF RFC 7233 [21] bytes 21010-47021/ 47022 Signals the byte range that is contained in the
198 response, and the total length of the file.
199 Retry-After IETF RFC 7231 [19] Fri, 31 Dec 1999 23:59:59 GMT
200 """
201
202 valid_query_string = ("ADMIN", "SET_PROJECT", "FORCE", "PUBLIC")
203 # ^ Contains possible administrative query string words:
204 # ADMIN=True(by default)|Project|Project-list: See all elements, or elements of a project
205 # (not owned by my session project).
206 # PUBLIC=True(by default)|False: See/hide public elements. Set/Unset a topic to be public
207 # FORCE=True(by default)|False: Force edition/deletion operations
208 # SET_PROJECT=Project|Project-list: Add/Delete the topic to the projects portfolio
209
210 valid_url_methods = {
211 # contains allowed URL and methods, and the role_permission name
212 "admin": {
213 "v1": {
214 "tokens": {
215 "METHODS": ("GET", "POST", "DELETE"),
216 "ROLE_PERMISSION": "tokens:",
217 "<ID>": {"METHODS": ("GET", "DELETE"), "ROLE_PERMISSION": "tokens:id:"},
218 },
219 "users": {
220 "METHODS": ("GET", "POST"),
221 "ROLE_PERMISSION": "users:",
222 "<ID>": {
223 "METHODS": ("GET", "DELETE", "PATCH"),
224 "ROLE_PERMISSION": "users:id:",
225 },
226 },
227 "projects": {
228 "METHODS": ("GET", "POST"),
229 "ROLE_PERMISSION": "projects:",
230 "<ID>": {
231 "METHODS": ("GET", "DELETE", "PATCH"),
232 "ROLE_PERMISSION": "projects:id:",
233 },
234 },
235 "roles": {
236 "METHODS": ("GET", "POST"),
237 "ROLE_PERMISSION": "roles:",
238 "<ID>": {
239 "METHODS": ("GET", "DELETE", "PATCH"),
240 "ROLE_PERMISSION": "roles:id:",
241 },
242 },
243 "vims": {
244 "METHODS": ("GET", "POST"),
245 "ROLE_PERMISSION": "vims:",
246 "<ID>": {
247 "METHODS": ("GET", "DELETE", "PATCH"),
248 "ROLE_PERMISSION": "vims:id:",
249 },
250 },
251 "vim_accounts": {
252 "METHODS": ("GET", "POST"),
253 "ROLE_PERMISSION": "vim_accounts:",
254 "<ID>": {
255 "METHODS": ("GET", "DELETE", "PATCH"),
256 "ROLE_PERMISSION": "vim_accounts:id:",
257 },
258 },
259 "wim_accounts": {
260 "METHODS": ("GET", "POST"),
261 "ROLE_PERMISSION": "wim_accounts:",
262 "<ID>": {
263 "METHODS": ("GET", "DELETE", "PATCH"),
264 "ROLE_PERMISSION": "wim_accounts:id:",
265 },
266 },
267 "sdns": {
268 "METHODS": ("GET", "POST"),
269 "ROLE_PERMISSION": "sdn_controllers:",
270 "<ID>": {
271 "METHODS": ("GET", "DELETE", "PATCH"),
272 "ROLE_PERMISSION": "sdn_controllers:id:",
273 },
274 },
275 "k8sclusters": {
276 "METHODS": ("GET", "POST"),
277 "ROLE_PERMISSION": "k8sclusters:",
278 "<ID>": {
279 "METHODS": ("GET", "DELETE", "PATCH"),
280 "ROLE_PERMISSION": "k8sclusters:id:",
281 },
282 },
283 "vca": {
284 "METHODS": ("GET", "POST"),
285 "ROLE_PERMISSION": "vca:",
286 "<ID>": {
287 "METHODS": ("GET", "DELETE", "PATCH"),
288 "ROLE_PERMISSION": "vca:id:",
289 },
290 },
291 "k8srepos": {
292 "METHODS": ("GET", "POST"),
293 "ROLE_PERMISSION": "k8srepos:",
294 "<ID>": {
295 "METHODS": ("GET", "DELETE"),
296 "ROLE_PERMISSION": "k8srepos:id:",
297 },
298 },
299 "osmrepos": {
300 "METHODS": ("GET", "POST"),
301 "ROLE_PERMISSION": "osmrepos:",
302 "<ID>": {
303 "METHODS": ("GET", "DELETE", "PATCH"),
304 "ROLE_PERMISSION": "osmrepos:id:",
305 },
306 },
307 "domains": {
308 "METHODS": ("GET",),
309 "ROLE_PERMISSION": "domains:",
310 },
311 }
312 },
313 "pdu": {
314 "v1": {
315 "pdu_descriptors": {
316 "METHODS": ("GET", "POST"),
317 "ROLE_PERMISSION": "pduds:",
318 "<ID>": {
319 "METHODS": ("GET", "POST", "DELETE", "PATCH", "PUT"),
320 "ROLE_PERMISSION": "pduds:id:",
321 },
322 },
323 }
324 },
325 "nsd": {
326 "v1": {
327 "ns_descriptors_content": {
328 "METHODS": ("GET", "POST"),
329 "ROLE_PERMISSION": "nsds:",
330 "<ID>": {
331 "METHODS": ("GET", "PUT", "DELETE"),
332 "ROLE_PERMISSION": "nsds:id:",
333 },
334 },
335 "ns_descriptors": {
336 "METHODS": ("GET", "POST"),
337 "ROLE_PERMISSION": "nsds:",
338 "<ID>": {
339 "METHODS": ("GET", "DELETE", "PATCH"),
340 "ROLE_PERMISSION": "nsds:id:",
341 "nsd_content": {
342 "METHODS": ("GET", "PUT"),
343 "ROLE_PERMISSION": "nsds:id:content:",
344 },
345 "nsd": {
346 "METHODS": ("GET",), # descriptor inside package
347 "ROLE_PERMISSION": "nsds:id:content:",
348 },
349 "artifacts": {
350 "METHODS": ("GET",),
351 "ROLE_PERMISSION": "nsds:id:nsd_artifact:",
352 "*": None,
353 },
354 },
355 },
356 "pnf_descriptors": {
357 "TODO": ("GET", "POST"),
358 "<ID>": {
359 "TODO": ("GET", "DELETE", "PATCH"),
360 "pnfd_content": {"TODO": ("GET", "PUT")},
361 },
362 },
363 "subscriptions": {
364 "TODO": ("GET", "POST"),
365 "<ID>": {"TODO": ("GET", "DELETE")},
366 },
367 }
368 },
369 "vnfpkgm": {
370 "v1": {
371 "vnf_packages_content": {
372 "METHODS": ("GET", "POST"),
373 "ROLE_PERMISSION": "vnfds:",
374 "<ID>": {
375 "METHODS": ("GET", "PUT", "DELETE"),
376 "ROLE_PERMISSION": "vnfds:id:",
377 },
378 },
379 "vnf_packages": {
380 "METHODS": ("GET", "POST"),
381 "ROLE_PERMISSION": "vnfds:",
382 "<ID>": {
383 "METHODS": ("GET", "DELETE", "PATCH"), # GET: vnfPkgInfo
384 "ROLE_PERMISSION": "vnfds:id:",
385 "package_content": {
386 "METHODS": ("GET", "PUT"), # package
387 "ROLE_PERMISSION": "vnfds:id:",
388 "upload_from_uri": {
389 "METHODS": (),
390 "TODO": ("POST",),
391 "ROLE_PERMISSION": "vnfds:id:upload:",
392 },
393 },
394 "vnfd": {
395 "METHODS": ("GET",), # descriptor inside package
396 "ROLE_PERMISSION": "vnfds:id:content:",
397 },
398 "artifacts": {
399 "METHODS": ("GET",),
400 "ROLE_PERMISSION": "vnfds:id:vnfd_artifact:",
401 "*": None,
402 },
403 "action": {
404 "METHODS": ("POST",),
405 "ROLE_PERMISSION": "vnfds:id:action:",
406 },
407 },
408 },
409 "subscriptions": {
410 "TODO": ("GET", "POST"),
411 "<ID>": {"TODO": ("GET", "DELETE")},
412 },
413 "vnfpkg_op_occs": {
414 "METHODS": ("GET",),
415 "ROLE_PERMISSION": "vnfds:vnfpkgops:",
416 "<ID>": {"METHODS": ("GET",), "ROLE_PERMISSION": "vnfds:vnfpkgops:id:"},
417 },
418 }
419 },
420 "nslcm": {
421 "v1": {
422 "ns_instances_content": {
423 "METHODS": ("GET", "POST"),
424 "ROLE_PERMISSION": "ns_instances:",
425 "<ID>": {
426 "METHODS": ("GET", "DELETE"),
427 "ROLE_PERMISSION": "ns_instances:id:",
428 },
429 },
430 "ns_instances": {
431 "METHODS": ("GET", "POST"),
432 "ROLE_PERMISSION": "ns_instances:",
433 "<ID>": {
434 "METHODS": ("GET", "DELETE"),
435 "ROLE_PERMISSION": "ns_instances:id:",
436 "scale": {
437 "METHODS": ("POST",),
438 "ROLE_PERMISSION": "ns_instances:id:scale:",
439 },
440 "terminate": {
441 "METHODS": ("POST",),
442 "ROLE_PERMISSION": "ns_instances:id:terminate:",
443 },
444 "instantiate": {
445 "METHODS": ("POST",),
446 "ROLE_PERMISSION": "ns_instances:id:instantiate:",
447 },
448 "migrate": {
449 "METHODS": ("POST",),
450 "ROLE_PERMISSION": "ns_instances:id:migrate:",
451 },
452 "action": {
453 "METHODS": ("POST",),
454 "ROLE_PERMISSION": "ns_instances:id:action:",
455 },
456 "update": {
457 "METHODS": ("POST",),
458 "ROLE_PERMISSION": "ns_instances:id:update:",
459 },
460 },
461 },
462 "ns_lcm_op_occs": {
463 "METHODS": ("GET",),
464 "ROLE_PERMISSION": "ns_instances:opps:",
465 "<ID>": {
466 "METHODS": ("GET",),
467 "ROLE_PERMISSION": "ns_instances:opps:id:",
468 },
469 },
470 "vnfrs": {
471 "METHODS": ("GET",),
472 "ROLE_PERMISSION": "vnf_instances:",
473 "<ID>": {"METHODS": ("GET",), "ROLE_PERMISSION": "vnf_instances:id:"},
474 },
475 "vnf_instances": {
476 "METHODS": ("GET",),
477 "ROLE_PERMISSION": "vnf_instances:",
478 "<ID>": {"METHODS": ("GET",), "ROLE_PERMISSION": "vnf_instances:id:"},
479 },
480 "subscriptions": {
481 "METHODS": ("GET", "POST"),
482 "ROLE_PERMISSION": "ns_subscriptions:",
483 "<ID>": {
484 "METHODS": ("GET", "DELETE"),
485 "ROLE_PERMISSION": "ns_subscriptions:id:",
486 },
487 },
488 }
489 },
490 "vnflcm": {
491 "v1": {
492 "vnf_instances": {"METHODS": ("GET", "POST"),
493 "ROLE_PERMISSION": "vnflcm_instances:",
494 "<ID>": {"METHODS": ("GET", "DELETE"),
495 "ROLE_PERMISSION": "vnflcm_instances:id:",
496 "scale": {"METHODS": ("POST",),
497 "ROLE_PERMISSION": "vnflcm_instances:id:scale:"
498 },
499 "terminate": {"METHODS": ("POST",),
500 "ROLE_PERMISSION": "vnflcm_instances:id:terminate:"
501 },
502 "instantiate": {"METHODS": ("POST",),
503 "ROLE_PERMISSION": "vnflcm_instances:id:instantiate:"
504 },
505 }
506 },
507 "vnf_lcm_op_occs": {"METHODS": ("GET",),
508 "ROLE_PERMISSION": "vnf_instances:opps:",
509 "<ID>": {"METHODS": ("GET",),
510 "ROLE_PERMISSION": "vnf_instances:opps:id:"
511 },
512 },
513 "subscriptions": {"METHODS": ("GET", "POST"),
514 "ROLE_PERMISSION": "vnflcm_subscriptions:",
515 "<ID>": {"METHODS": ("GET", "DELETE"),
516 "ROLE_PERMISSION": "vnflcm_subscriptions:id:"
517 }
518 },
519 }
520 },
521 "nst": {
522 "v1": {
523 "netslice_templates_content": {
524 "METHODS": ("GET", "POST"),
525 "ROLE_PERMISSION": "slice_templates:",
526 "<ID>": {
527 "METHODS": ("GET", "PUT", "DELETE"),
528 "ROLE_PERMISSION": "slice_templates:id:",
529 },
530 },
531 "netslice_templates": {
532 "METHODS": ("GET", "POST"),
533 "ROLE_PERMISSION": "slice_templates:",
534 "<ID>": {
535 "METHODS": ("GET", "DELETE"),
536 "TODO": ("PATCH",),
537 "ROLE_PERMISSION": "slice_templates:id:",
538 "nst_content": {
539 "METHODS": ("GET", "PUT"),
540 "ROLE_PERMISSION": "slice_templates:id:content:",
541 },
542 "nst": {
543 "METHODS": ("GET",), # descriptor inside package
544 "ROLE_PERMISSION": "slice_templates:id:content:",
545 },
546 "artifacts": {
547 "METHODS": ("GET",),
548 "ROLE_PERMISSION": "slice_templates:id:content:",
549 "*": None,
550 },
551 },
552 },
553 "subscriptions": {
554 "TODO": ("GET", "POST"),
555 "<ID>": {"TODO": ("GET", "DELETE")},
556 },
557 }
558 },
559 "nsilcm": {
560 "v1": {
561 "netslice_instances_content": {
562 "METHODS": ("GET", "POST"),
563 "ROLE_PERMISSION": "slice_instances:",
564 "<ID>": {
565 "METHODS": ("GET", "DELETE"),
566 "ROLE_PERMISSION": "slice_instances:id:",
567 },
568 },
569 "netslice_instances": {
570 "METHODS": ("GET", "POST"),
571 "ROLE_PERMISSION": "slice_instances:",
572 "<ID>": {
573 "METHODS": ("GET", "DELETE"),
574 "ROLE_PERMISSION": "slice_instances:id:",
575 "terminate": {
576 "METHODS": ("POST",),
577 "ROLE_PERMISSION": "slice_instances:id:terminate:",
578 },
579 "instantiate": {
580 "METHODS": ("POST",),
581 "ROLE_PERMISSION": "slice_instances:id:instantiate:",
582 },
583 "action": {
584 "METHODS": ("POST",),
585 "ROLE_PERMISSION": "slice_instances:id:action:",
586 },
587 },
588 },
589 "nsi_lcm_op_occs": {
590 "METHODS": ("GET",),
591 "ROLE_PERMISSION": "slice_instances:opps:",
592 "<ID>": {
593 "METHODS": ("GET",),
594 "ROLE_PERMISSION": "slice_instances:opps:id:",
595 },
596 },
597 }
598 },
599 "nspm": {
600 "v1": {
601 "pm_jobs": {
602 "<ID>": {
603 "reports": {
604 "<ID>": {
605 "METHODS": ("GET",),
606 "ROLE_PERMISSION": "reports:id:",
607 }
608 }
609 },
610 },
611 },
612 },
613 "nsfm": {
614 "v1": {
615 "alarms": {"METHODS": ("GET", "PATCH"),
616 "ROLE_PERMISSION": "alarms:",
617 "<ID>": {"METHODS": ("GET", "PATCH"),
618 "ROLE_PERMISSION": "alarms:id:",
619 },
620 }
621 },
622 },
623 }
624
625
626 class NbiException(Exception):
627 def __init__(self, message, http_code=HTTPStatus.METHOD_NOT_ALLOWED):
628 Exception.__init__(self, message)
629 self.http_code = http_code
630
631
632 class Server(object):
633 instance = 0
634 # to decode bytes to str
635 reader = getreader("utf-8")
636
637 def __init__(self):
638 self.instance += 1
639 self.authenticator = Authenticator(valid_url_methods, valid_query_string)
640 self.engine = Engine(self.authenticator)
641
642 def _format_in(self, kwargs):
643 try:
644 indata = None
645 if cherrypy.request.body.length:
646 error_text = "Invalid input format "
647
648 if "Content-Type" in cherrypy.request.headers:
649 if "application/json" in cherrypy.request.headers["Content-Type"]:
650 error_text = "Invalid json format "
651 indata = json.load(self.reader(cherrypy.request.body))
652 cherrypy.request.headers.pop("Content-File-MD5", None)
653 elif "application/yaml" in cherrypy.request.headers["Content-Type"]:
654 error_text = "Invalid yaml format "
655 indata = yaml.load(
656 cherrypy.request.body, Loader=yaml.SafeLoader
657 )
658 cherrypy.request.headers.pop("Content-File-MD5", None)
659 elif (
660 "application/binary" in cherrypy.request.headers["Content-Type"]
661 or "application/gzip"
662 in cherrypy.request.headers["Content-Type"]
663 or "application/zip" in cherrypy.request.headers["Content-Type"]
664 or "text/plain" in cherrypy.request.headers["Content-Type"]
665 ):
666 indata = cherrypy.request.body # .read()
667 elif (
668 "multipart/form-data"
669 in cherrypy.request.headers["Content-Type"]
670 ):
671 if "descriptor_file" in kwargs:
672 filecontent = kwargs.pop("descriptor_file")
673 if not filecontent.file:
674 raise NbiException(
675 "empty file or content", HTTPStatus.BAD_REQUEST
676 )
677 indata = filecontent.file # .read()
678 if filecontent.content_type.value:
679 cherrypy.request.headers[
680 "Content-Type"
681 ] = filecontent.content_type.value
682 else:
683 # raise cherrypy.HTTPError(HTTPStatus.Not_Acceptable,
684 # "Only 'Content-Type' of type 'application/json' or
685 # 'application/yaml' for input format are available")
686 error_text = "Invalid yaml format "
687 indata = yaml.load(
688 cherrypy.request.body, Loader=yaml.SafeLoader
689 )
690 cherrypy.request.headers.pop("Content-File-MD5", None)
691 else:
692 error_text = "Invalid yaml format "
693 indata = yaml.load(cherrypy.request.body, Loader=yaml.SafeLoader)
694 cherrypy.request.headers.pop("Content-File-MD5", None)
695 if not indata:
696 indata = {}
697
698 format_yaml = False
699 if cherrypy.request.headers.get("Query-String-Format") == "yaml":
700 format_yaml = True
701
702 for k, v in kwargs.items():
703 if isinstance(v, str):
704 if v == "":
705 kwargs[k] = None
706 elif format_yaml:
707 try:
708 kwargs[k] = yaml.load(v, Loader=yaml.SafeLoader)
709 except Exception:
710 pass
711 elif (
712 k.endswith(".gt")
713 or k.endswith(".lt")
714 or k.endswith(".gte")
715 or k.endswith(".lte")
716 ):
717 try:
718 kwargs[k] = int(v)
719 except Exception:
720 try:
721 kwargs[k] = float(v)
722 except Exception:
723 pass
724 elif v.find(",") > 0:
725 kwargs[k] = v.split(",")
726 elif isinstance(v, (list, tuple)):
727 for index in range(0, len(v)):
728 if v[index] == "":
729 v[index] = None
730 elif format_yaml:
731 try:
732 v[index] = yaml.load(v[index], Loader=yaml.SafeLoader)
733 except Exception:
734 pass
735
736 return indata
737 except (ValueError, yaml.YAMLError) as exc:
738 raise NbiException(error_text + str(exc), HTTPStatus.BAD_REQUEST)
739 except KeyError as exc:
740 raise NbiException(
741 "Query string error: " + str(exc), HTTPStatus.BAD_REQUEST
742 )
743 except Exception as exc:
744 raise NbiException(error_text + str(exc), HTTPStatus.BAD_REQUEST)
745
746 @staticmethod
747 def _format_out(data, token_info=None, _format=None):
748 """
749 return string of dictionary data according to requested json, yaml, xml. By default json
750 :param data: response to be sent. Can be a dict, text or file
751 :param token_info: Contains among other username and project
752 :param _format: The format to be set as Content-Type if data is a file
753 :return: None
754 """
755 accept = cherrypy.request.headers.get("Accept")
756 if data is None:
757 if accept and "text/html" in accept:
758 return html.format(
759 data, cherrypy.request, cherrypy.response, token_info
760 )
761 # cherrypy.response.status = HTTPStatus.NO_CONTENT.value
762 return
763 elif hasattr(data, "read"): # file object
764 if _format:
765 cherrypy.response.headers["Content-Type"] = _format
766 elif "b" in data.mode: # binariy asssumig zip
767 cherrypy.response.headers["Content-Type"] = "application/zip"
768 else:
769 cherrypy.response.headers["Content-Type"] = "text/plain"
770 # TODO check that cherrypy close file. If not implement pending things to close per thread next
771 return data
772 if accept:
773 if "text/html" in accept:
774 return html.format(
775 data, cherrypy.request, cherrypy.response, token_info
776 )
777 elif "application/yaml" in accept or "*/*" in accept:
778 pass
779 elif "application/json" in accept or (
780 cherrypy.response.status and cherrypy.response.status >= 300
781 ):
782 cherrypy.response.headers[
783 "Content-Type"
784 ] = "application/json; charset=utf-8"
785 a = json.dumps(data, indent=4) + "\n"
786 return a.encode("utf8")
787 cherrypy.response.headers["Content-Type"] = "application/yaml"
788 return yaml.safe_dump(
789 data,
790 explicit_start=True,
791 indent=4,
792 default_flow_style=False,
793 tags=False,
794 encoding="utf-8",
795 allow_unicode=True,
796 ) # , canonical=True, default_style='"'
797
798 @cherrypy.expose
799 def index(self, *args, **kwargs):
800 token_info = None
801 try:
802 if cherrypy.request.method == "GET":
803 token_info = self.authenticator.authorize()
804 outdata = token_info # Home page
805 else:
806 raise cherrypy.HTTPError(
807 HTTPStatus.METHOD_NOT_ALLOWED.value,
808 "Method {} not allowed for tokens".format(cherrypy.request.method),
809 )
810
811 return self._format_out(outdata, token_info)
812
813 except (EngineException, AuthException) as e:
814 # cherrypy.log("index Exception {}".format(e))
815 cherrypy.response.status = e.http_code.value
816 return self._format_out("Welcome to OSM!", token_info)
817
818 @cherrypy.expose
819 def version(self, *args, **kwargs):
820 # TODO consider to remove and provide version using the static version file
821 try:
822 if cherrypy.request.method != "GET":
823 raise NbiException(
824 "Only method GET is allowed", HTTPStatus.METHOD_NOT_ALLOWED
825 )
826 elif args or kwargs:
827 raise NbiException(
828 "Invalid URL or query string for version",
829 HTTPStatus.METHOD_NOT_ALLOWED,
830 )
831 # TODO include version of other modules, pick up from some kafka admin message
832 osm_nbi_version = {"version": nbi_version, "date": nbi_version_date}
833 return self._format_out(osm_nbi_version)
834 except NbiException as e:
835 cherrypy.response.status = e.http_code.value
836 problem_details = {
837 "code": e.http_code.name,
838 "status": e.http_code.value,
839 "detail": str(e),
840 }
841 return self._format_out(problem_details, None)
842
843 def domain(self):
844 try:
845 domains = {
846 "user_domain_name": cherrypy.tree.apps["/osm"]
847 .config["authentication"]
848 .get("user_domain_name"),
849 "project_domain_name": cherrypy.tree.apps["/osm"]
850 .config["authentication"]
851 .get("project_domain_name"),
852 }
853 return self._format_out(domains)
854 except NbiException as e:
855 cherrypy.response.status = e.http_code.value
856 problem_details = {
857 "code": e.http_code.name,
858 "status": e.http_code.value,
859 "detail": str(e),
860 }
861 return self._format_out(problem_details, None)
862
863 @staticmethod
864 def _format_login(token_info):
865 """
866 Changes cherrypy.request.login to include username/project_name;session so that cherrypy access log will
867 log this information
868 :param token_info: Dictionary with token content
869 :return: None
870 """
871 cherrypy.request.login = token_info.get("username", "-")
872 if token_info.get("project_name"):
873 cherrypy.request.login += "/" + token_info["project_name"]
874 if token_info.get("id"):
875 cherrypy.request.login += ";session=" + token_info["id"][0:12]
876
877 # NS Fault Management
878 @cherrypy.expose
879 def nsfm(self, version=None, topic=None, uuid=None, project_name=None, ns_id=None, *args, **kwargs):
880 if topic == 'alarms':
881 try:
882 method = cherrypy.request.method
883 role_permission = self._check_valid_url_method(method, "nsfm", version, topic, None, None, *args)
884 query_string_operations = self._extract_query_string_operations(kwargs, method)
885
886 self.authenticator.authorize(role_permission, query_string_operations, None)
887
888 # to handle get request
889 if cherrypy.request.method == 'GET':
890 # if request is on basis of uuid
891 if uuid and uuid != 'None':
892 try:
893 alarm = self.engine.db.get_one("alarms", {"uuid": uuid})
894 alarm_action = self.engine.db.get_one("alarms_action", {"uuid": uuid})
895 alarm.update(alarm_action)
896 vnf = self.engine.db.get_one("vnfrs", {"nsr-id-ref": alarm["tags"]["ns_id"]})
897 alarm["vnf-id"] = vnf["_id"]
898 return self._format_out(str(alarm))
899 except Exception:
900 return self._format_out("Please provide valid alarm uuid")
901 elif ns_id and ns_id != 'None':
902 # if request is on basis of ns_id
903 try:
904 alarms = self.engine.db.get_list("alarms", {"tags.ns_id": ns_id})
905 for alarm in alarms:
906 alarm_action = self.engine.db.get_one("alarms_action", {"uuid": alarm['uuid']})
907 alarm.update(alarm_action)
908 return self._format_out(str(alarms))
909 except Exception:
910 return self._format_out("Please provide valid ns id")
911 else:
912 # to return only alarm which are related to given project
913 project = self.engine.db.get_one("projects", {"name": project_name})
914 project_id = project.get('_id')
915 ns_list = self.engine.db.get_list("nsrs", {"_admin.projects_read": project_id})
916 ns_ids = []
917 for ns in ns_list:
918 ns_ids.append(ns.get("_id"))
919 alarms = self.engine.db.get_list("alarms")
920 alarm_list = [alarm for alarm in alarms if alarm["tags"]["ns_id"] in ns_ids]
921 for alrm in alarm_list:
922 action = self.engine.db.get_one("alarms_action", {"uuid": alrm.get("uuid")})
923 alrm.update(action)
924 return self._format_out(str(alarm_list))
925 # to handle patch request for alarm update
926 elif cherrypy.request.method == 'PATCH':
927 data = yaml.load(cherrypy.request.body, Loader=yaml.SafeLoader)
928 try:
929 # check if uuid is valid
930 self.engine.db.get_one("alarms", {"uuid": data.get("uuid")})
931 except Exception:
932 return self._format_out("Please provide valid alarm uuid.")
933 if data.get("is_enable") is not None:
934 if data.get("is_enable"):
935 alarm_status = 'ok'
936 else:
937 alarm_status = 'disabled'
938 self.engine.db.set_one("alarms", {"uuid": data.get("uuid")},
939 {"alarm_status": alarm_status})
940 else:
941 self.engine.db.set_one("alarms", {"uuid": data.get("uuid")},
942 {"threshold": data.get("threshold")})
943 return self._format_out("Alarm updated")
944 except Exception as e:
945 cherrypy.response.status = e.http_code.value
946 if isinstance(e, (NbiException, EngineException, DbException, FsException, MsgException, AuthException,
947 ValidationError, AuthconnException)):
948 http_code_value = cherrypy.response.status = e.http_code.value
949 http_code_name = e.http_code.name
950 cherrypy.log("Exception {}".format(e))
951 else:
952 http_code_value = cherrypy.response.status = HTTPStatus.BAD_REQUEST.value # INTERNAL_SERVER_ERROR
953 cherrypy.log("CRITICAL: Exception {}".format(e), traceback=True)
954 http_code_name = HTTPStatus.BAD_REQUEST.name
955 problem_details = {
956 "code": http_code_name,
957 "status": http_code_value,
958 "detail": str(e),
959 }
960 return self._format_out(problem_details)
961
962 @cherrypy.expose
963 def token(self, method, token_id=None, kwargs=None):
964 token_info = None
965 # self.engine.load_dbase(cherrypy.request.app.config)
966 indata = self._format_in(kwargs)
967 if not isinstance(indata, dict):
968 raise NbiException(
969 "Expected application/yaml or application/json Content-Type",
970 HTTPStatus.BAD_REQUEST,
971 )
972
973 if method == "GET":
974 token_info = self.authenticator.authorize()
975 # for logging
976 self._format_login(token_info)
977 if token_id:
978 outdata = self.authenticator.get_token(token_info, token_id)
979 else:
980 outdata = self.authenticator.get_token_list(token_info)
981 elif method == "POST":
982 try:
983 token_info = self.authenticator.authorize()
984 except Exception:
985 token_info = None
986 if kwargs:
987 indata.update(kwargs)
988 # This is needed to log the user when authentication fails
989 cherrypy.request.login = "{}".format(indata.get("username", "-"))
990 outdata = token_info = self.authenticator.new_token(
991 token_info, indata, cherrypy.request.remote
992 )
993 cherrypy.session["Authorization"] = outdata["_id"]
994 self._set_location_header("admin", "v1", "tokens", outdata["_id"])
995 # for logging
996 self._format_login(token_info)
997 # password expiry check
998 if self.authenticator.check_password_expiry(outdata):
999 outdata = {"id": outdata["id"],
1000 "message": "change_password",
1001 "user_id": outdata["user_id"]
1002 }
1003 # cherrypy.response.cookie["Authorization"] = outdata["id"]
1004 # cherrypy.response.cookie["Authorization"]['expires'] = 3600
1005 elif method == "DELETE":
1006 if not token_id and "id" in kwargs:
1007 token_id = kwargs["id"]
1008 elif not token_id:
1009 token_info = self.authenticator.authorize()
1010 # for logging
1011 self._format_login(token_info)
1012 token_id = token_info["_id"]
1013 outdata = self.authenticator.del_token(token_id)
1014 token_info = None
1015 cherrypy.session["Authorization"] = "logout"
1016 # cherrypy.response.cookie["Authorization"] = token_id
1017 # cherrypy.response.cookie["Authorization"]['expires'] = 0
1018 else:
1019 raise NbiException(
1020 "Method {} not allowed for token".format(method),
1021 HTTPStatus.METHOD_NOT_ALLOWED,
1022 )
1023 return self._format_out(outdata, token_info)
1024
1025 @cherrypy.expose
1026 def test(self, *args, **kwargs):
1027 if not cherrypy.config.get("server.enable_test") or (
1028 isinstance(cherrypy.config["server.enable_test"], str)
1029 and cherrypy.config["server.enable_test"].lower() == "false"
1030 ):
1031 cherrypy.response.status = HTTPStatus.METHOD_NOT_ALLOWED.value
1032 return "test URL is disabled"
1033 thread_info = None
1034 if args and args[0] == "help":
1035 return (
1036 "<html><pre>\ninit\nfile/<name> download file\ndb-clear/table\nfs-clear[/folder]\nlogin\nlogin2\n"
1037 "sleep/<time>\nmessage/topic\n</pre></html>"
1038 )
1039
1040 elif args and args[0] == "init":
1041 try:
1042 # self.engine.load_dbase(cherrypy.request.app.config)
1043 self.engine.create_admin()
1044 return "Done. User 'admin', password 'admin' created"
1045 except Exception:
1046 cherrypy.response.status = HTTPStatus.FORBIDDEN.value
1047 return self._format_out("Database already initialized")
1048 elif args and args[0] == "file":
1049 return cherrypy.lib.static.serve_file(
1050 cherrypy.tree.apps["/osm"].config["storage"]["path"] + "/" + args[1],
1051 "text/plain",
1052 "attachment",
1053 )
1054 elif args and args[0] == "file2":
1055 f_path = (
1056 cherrypy.tree.apps["/osm"].config["storage"]["path"] + "/" + args[1]
1057 )
1058 f = open(f_path, "r")
1059 cherrypy.response.headers["Content-type"] = "text/plain"
1060 return f
1061
1062 elif len(args) == 2 and args[0] == "db-clear":
1063 deleted_info = self.engine.db.del_list(args[1], kwargs)
1064 return "{} {} deleted\n".format(deleted_info["deleted"], args[1])
1065 elif len(args) and args[0] == "fs-clear":
1066 if len(args) >= 2:
1067 folders = (args[1],)
1068 else:
1069 folders = self.engine.fs.dir_ls(".")
1070 for folder in folders:
1071 self.engine.fs.file_delete(folder)
1072 return ",".join(folders) + " folders deleted\n"
1073 elif args and args[0] == "login":
1074 if not cherrypy.request.headers.get("Authorization"):
1075 cherrypy.response.headers[
1076 "WWW-Authenticate"
1077 ] = 'Basic realm="Access to OSM site", charset="UTF-8"'
1078 cherrypy.response.status = HTTPStatus.UNAUTHORIZED.value
1079 elif args and args[0] == "login2":
1080 if not cherrypy.request.headers.get("Authorization"):
1081 cherrypy.response.headers[
1082 "WWW-Authenticate"
1083 ] = 'Bearer realm="Access to OSM site"'
1084 cherrypy.response.status = HTTPStatus.UNAUTHORIZED.value
1085 elif args and args[0] == "sleep":
1086 sleep_time = 5
1087 try:
1088 sleep_time = int(args[1])
1089 except Exception:
1090 cherrypy.response.status = HTTPStatus.FORBIDDEN.value
1091 return self._format_out("Database already initialized")
1092 thread_info = cherrypy.thread_data
1093 print(thread_info)
1094 time.sleep(sleep_time)
1095 # thread_info
1096 elif len(args) >= 2 and args[0] == "message":
1097 main_topic = args[1]
1098 return_text = "<html><pre>{} ->\n".format(main_topic)
1099 try:
1100 if cherrypy.request.method == "POST":
1101 to_send = yaml.load(cherrypy.request.body, Loader=yaml.SafeLoader)
1102 for k, v in to_send.items():
1103 self.engine.msg.write(main_topic, k, v)
1104 return_text += " {}: {}\n".format(k, v)
1105 elif cherrypy.request.method == "GET":
1106 for k, v in kwargs.items():
1107 v_dict = yaml.load(v, Loader=yaml.SafeLoader)
1108 self.engine.msg.write(main_topic, k, v_dict)
1109 return_text += " {}: {}\n".format(k, v_dict)
1110 except Exception as e:
1111 return_text += "Error: " + str(e)
1112 return_text += "</pre></html>\n"
1113 return return_text
1114
1115 return_text = (
1116 "<html><pre>\nheaders:\n args: {}\n".format(args)
1117 + " kwargs: {}\n".format(kwargs)
1118 + " headers: {}\n".format(cherrypy.request.headers)
1119 + " path_info: {}\n".format(cherrypy.request.path_info)
1120 + " query_string: {}\n".format(cherrypy.request.query_string)
1121 + " session: {}\n".format(cherrypy.session)
1122 + " cookie: {}\n".format(cherrypy.request.cookie)
1123 + " method: {}\n".format(cherrypy.request.method)
1124 + " session: {}\n".format(cherrypy.session.get("fieldname"))
1125 + " body:\n"
1126 )
1127 return_text += " length: {}\n".format(cherrypy.request.body.length)
1128 if cherrypy.request.body.length:
1129 return_text += " content: {}\n".format(
1130 str(
1131 cherrypy.request.body.read(
1132 int(cherrypy.request.headers.get("Content-Length", 0))
1133 )
1134 )
1135 )
1136 if thread_info:
1137 return_text += "thread: {}\n".format(thread_info)
1138 return_text += "</pre></html>"
1139 return return_text
1140
1141 @staticmethod
1142 def _check_valid_url_method(method, *args):
1143 if len(args) < 3:
1144 raise NbiException(
1145 "URL must contain at least 'main_topic/version/topic'",
1146 HTTPStatus.METHOD_NOT_ALLOWED,
1147 )
1148
1149 reference = valid_url_methods
1150 for arg in args:
1151 if arg is None:
1152 break
1153 if not isinstance(reference, dict):
1154 raise NbiException(
1155 "URL contains unexpected extra items '{}'".format(arg),
1156 HTTPStatus.METHOD_NOT_ALLOWED,
1157 )
1158
1159 if arg in reference:
1160 reference = reference[arg]
1161 elif "<ID>" in reference:
1162 reference = reference["<ID>"]
1163 elif "*" in reference:
1164 # if there is content
1165 if reference["*"]:
1166 reference = reference["*"]
1167 break
1168 else:
1169 raise NbiException(
1170 "Unexpected URL item {}".format(arg), HTTPStatus.METHOD_NOT_ALLOWED
1171 )
1172 if "TODO" in reference and method in reference["TODO"]:
1173 raise NbiException(
1174 "Method {} not supported yet for this URL".format(method),
1175 HTTPStatus.NOT_IMPLEMENTED,
1176 )
1177 elif "METHODS" in reference and method not in reference["METHODS"]:
1178 raise NbiException(
1179 "Method {} not supported for this URL".format(method),
1180 HTTPStatus.METHOD_NOT_ALLOWED,
1181 )
1182 return reference["ROLE_PERMISSION"] + method.lower()
1183
1184 @staticmethod
1185 def _set_location_header(main_topic, version, topic, id):
1186 """
1187 Insert response header Location with the URL of created item base on URL params
1188 :param main_topic:
1189 :param version:
1190 :param topic:
1191 :param id:
1192 :return: None
1193 """
1194 # Use cherrypy.request.base for absoluted path and make use of request.header HOST just in case behind aNAT
1195 cherrypy.response.headers["Location"] = "/osm/{}/{}/{}/{}".format(
1196 main_topic, version, topic, id
1197 )
1198 return
1199
1200 @staticmethod
1201 def _extract_query_string_operations(kwargs, method):
1202 """
1203
1204 :param kwargs:
1205 :return:
1206 """
1207 query_string_operations = []
1208 if kwargs:
1209 for qs in ("FORCE", "PUBLIC", "ADMIN", "SET_PROJECT"):
1210 if qs in kwargs and kwargs[qs].lower() != "false":
1211 query_string_operations.append(qs.lower() + ":" + method.lower())
1212 return query_string_operations
1213
1214 @staticmethod
1215 def _manage_admin_query(token_info, kwargs, method, _id):
1216 """
1217 Processes the administrator query inputs (if any) of FORCE, ADMIN, PUBLIC, SET_PROJECT
1218 Check that users has rights to use them and returs the admin_query
1219 :param token_info: token_info rights obtained by token
1220 :param kwargs: query string input.
1221 :param method: http method: GET, POSST, PUT, ...
1222 :param _id:
1223 :return: admin_query dictionary with keys:
1224 public: True, False or None
1225 force: True or False
1226 project_id: tuple with projects used for accessing an element
1227 set_project: tuple with projects that a created element will belong to
1228 method: show, list, delete, write
1229 """
1230 admin_query = {
1231 "force": False,
1232 "project_id": (token_info["project_id"],),
1233 "username": token_info["username"],
1234 "admin": token_info["admin"],
1235 "public": None,
1236 "allow_show_user_project_role": token_info["allow_show_user_project_role"],
1237 }
1238 if kwargs:
1239 # FORCE
1240 if "FORCE" in kwargs:
1241 if (
1242 kwargs["FORCE"].lower() != "false"
1243 ): # if None or True set force to True
1244 admin_query["force"] = True
1245 del kwargs["FORCE"]
1246 # PUBLIC
1247 if "PUBLIC" in kwargs:
1248 if (
1249 kwargs["PUBLIC"].lower() != "false"
1250 ): # if None or True set public to True
1251 admin_query["public"] = True
1252 else:
1253 admin_query["public"] = False
1254 del kwargs["PUBLIC"]
1255 # ADMIN
1256 if "ADMIN" in kwargs:
1257 behave_as = kwargs.pop("ADMIN")
1258 if behave_as.lower() != "false":
1259 if not token_info["admin"]:
1260 raise NbiException(
1261 "Only admin projects can use 'ADMIN' query string",
1262 HTTPStatus.UNAUTHORIZED,
1263 )
1264 if (
1265 not behave_as or behave_as.lower() == "true"
1266 ): # convert True, None to empty list
1267 admin_query["project_id"] = ()
1268 elif isinstance(behave_as, (list, tuple)):
1269 admin_query["project_id"] = behave_as
1270 else: # isinstance(behave_as, str)
1271 admin_query["project_id"] = (behave_as,)
1272 if "SET_PROJECT" in kwargs:
1273 set_project = kwargs.pop("SET_PROJECT")
1274 if not set_project:
1275 admin_query["set_project"] = list(admin_query["project_id"])
1276 else:
1277 if isinstance(set_project, str):
1278 set_project = (set_project,)
1279 if admin_query["project_id"]:
1280 for p in set_project:
1281 if p not in admin_query["project_id"]:
1282 raise NbiException(
1283 "Unauthorized for 'SET_PROJECT={p}'. Try with 'ADMIN=True' or "
1284 "'ADMIN='{p}'".format(p=p),
1285 HTTPStatus.UNAUTHORIZED,
1286 )
1287 admin_query["set_project"] = set_project
1288
1289 # PROJECT_READ
1290 # if "PROJECT_READ" in kwargs:
1291 # admin_query["project"] = kwargs.pop("project")
1292 # if admin_query["project"] == token_info["project_id"]:
1293 if method == "GET":
1294 if _id:
1295 admin_query["method"] = "show"
1296 else:
1297 admin_query["method"] = "list"
1298 elif method == "DELETE":
1299 admin_query["method"] = "delete"
1300 else:
1301 admin_query["method"] = "write"
1302 return admin_query
1303
1304 @cherrypy.expose
1305 def default(
1306 self,
1307 main_topic=None,
1308 version=None,
1309 topic=None,
1310 _id=None,
1311 item=None,
1312 *args,
1313 **kwargs
1314 ):
1315 token_info = None
1316 outdata = None
1317 _format = None
1318 method = "DONE"
1319 engine_topic = None
1320 rollback = []
1321 engine_session = None
1322 try:
1323 if not main_topic or not version or not topic:
1324 raise NbiException(
1325 "URL must contain at least 'main_topic/version/topic'",
1326 HTTPStatus.METHOD_NOT_ALLOWED,
1327 )
1328 if main_topic not in (
1329 "admin",
1330 "vnfpkgm",
1331 "nsd",
1332 "nslcm",
1333 "pdu",
1334 "nst",
1335 "nsilcm",
1336 "nspm",
1337 "vnflcm",
1338 ):
1339 raise NbiException(
1340 "URL main_topic '{}' not supported".format(main_topic),
1341 HTTPStatus.METHOD_NOT_ALLOWED,
1342 )
1343 if version != "v1":
1344 raise NbiException(
1345 "URL version '{}' not supported".format(version),
1346 HTTPStatus.METHOD_NOT_ALLOWED,
1347 )
1348
1349 if (
1350 kwargs
1351 and "METHOD" in kwargs
1352 and kwargs["METHOD"] in ("PUT", "POST", "DELETE", "GET", "PATCH")
1353 ):
1354 method = kwargs.pop("METHOD")
1355 else:
1356 method = cherrypy.request.method
1357
1358 role_permission = self._check_valid_url_method(
1359 method, main_topic, version, topic, _id, item, *args
1360 )
1361 query_string_operations = self._extract_query_string_operations(
1362 kwargs, method
1363 )
1364 if main_topic == "admin" and topic == "tokens":
1365 return self.token(method, _id, kwargs)
1366 token_info = self.authenticator.authorize(
1367 role_permission, query_string_operations, _id
1368 )
1369 if main_topic == "admin" and topic == "domains":
1370 return self.domain()
1371 engine_session = self._manage_admin_query(token_info, kwargs, method, _id)
1372 indata = self._format_in(kwargs)
1373 engine_topic = topic
1374
1375 if item and topic != "pm_jobs":
1376 engine_topic = item
1377
1378 if main_topic == "nsd":
1379 engine_topic = "nsds"
1380 elif main_topic == "vnfpkgm":
1381 engine_topic = "vnfds"
1382 if topic == "vnfpkg_op_occs":
1383 engine_topic = "vnfpkgops"
1384 if topic == "vnf_packages" and item == "action":
1385 engine_topic = "vnfpkgops"
1386 elif main_topic == "nslcm":
1387 engine_topic = "nsrs"
1388 if topic == "ns_lcm_op_occs":
1389 engine_topic = "nslcmops"
1390 if topic == "vnfrs" or topic == "vnf_instances":
1391 engine_topic = "vnfrs"
1392 elif main_topic == "vnflcm":
1393 if topic == "vnf_lcm_op_occs":
1394 engine_topic = "vnflcmops"
1395 elif main_topic == "nst":
1396 engine_topic = "nsts"
1397 elif main_topic == "nsilcm":
1398 engine_topic = "nsis"
1399 if topic == "nsi_lcm_op_occs":
1400 engine_topic = "nsilcmops"
1401 elif main_topic == "pdu":
1402 engine_topic = "pdus"
1403 if (
1404 engine_topic == "vims"
1405 ): # TODO this is for backward compatibility, it will be removed in the future
1406 engine_topic = "vim_accounts"
1407
1408 if topic == "subscriptions":
1409 engine_topic = main_topic + "_" + topic
1410
1411 if method == "GET":
1412 if item in (
1413 "nsd_content",
1414 "package_content",
1415 "artifacts",
1416 "vnfd",
1417 "nsd",
1418 "nst",
1419 "nst_content",
1420 ):
1421 if item in ("vnfd", "nsd", "nst"):
1422 path = "$DESCRIPTOR"
1423 elif args:
1424 path = args
1425 elif item == "artifacts":
1426 path = ()
1427 else:
1428 path = None
1429 file, _format = self.engine.get_file(
1430 engine_session,
1431 engine_topic,
1432 _id,
1433 path,
1434 cherrypy.request.headers.get("Accept"),
1435 )
1436 outdata = file
1437 elif not _id:
1438 outdata = self.engine.get_item_list(
1439 engine_session, engine_topic, kwargs, api_req=True
1440 )
1441 else:
1442 if item == "reports":
1443 # TODO check that project_id (_id in this context) has permissions
1444 _id = args[0]
1445 filter_q = None
1446 if "vcaStatusRefresh" in kwargs:
1447 filter_q = {"vcaStatusRefresh": kwargs["vcaStatusRefresh"]}
1448 outdata = self.engine.get_item(engine_session, engine_topic, _id, filter_q, True)
1449
1450 elif method == "POST":
1451 cherrypy.response.status = HTTPStatus.CREATED.value
1452 if topic in (
1453 "ns_descriptors_content",
1454 "vnf_packages_content",
1455 "netslice_templates_content",
1456 ):
1457 _id = cherrypy.request.headers.get("Transaction-Id")
1458 if not _id:
1459 _id, _ = self.engine.new_item(
1460 rollback,
1461 engine_session,
1462 engine_topic,
1463 {},
1464 None,
1465 cherrypy.request.headers,
1466 )
1467 completed = self.engine.upload_content(
1468 engine_session,
1469 engine_topic,
1470 _id,
1471 indata,
1472 kwargs,
1473 cherrypy.request.headers,
1474 )
1475 if completed:
1476 self._set_location_header(main_topic, version, topic, _id)
1477 else:
1478 cherrypy.response.headers["Transaction-Id"] = _id
1479 outdata = {"id": _id}
1480 elif topic == "ns_instances_content":
1481 # creates NSR
1482 _id, _ = self.engine.new_item(
1483 rollback, engine_session, engine_topic, indata, kwargs
1484 )
1485 # creates nslcmop
1486 indata["lcmOperationType"] = "instantiate"
1487 indata["nsInstanceId"] = _id
1488 nslcmop_id, _ = self.engine.new_item(
1489 rollback, engine_session, "nslcmops", indata, None
1490 )
1491 self._set_location_header(main_topic, version, topic, _id)
1492 outdata = {"id": _id, "nslcmop_id": nslcmop_id}
1493 elif topic == "ns_instances" and item:
1494 indata["lcmOperationType"] = item
1495 indata["nsInstanceId"] = _id
1496 _id, _ = self.engine.new_item(
1497 rollback, engine_session, "nslcmops", indata, kwargs
1498 )
1499 self._set_location_header(
1500 main_topic, version, "ns_lcm_op_occs", _id
1501 )
1502 outdata = {"id": _id}
1503 cherrypy.response.status = HTTPStatus.ACCEPTED.value
1504 elif topic == "netslice_instances_content":
1505 # creates NetSlice_Instance_record (NSIR)
1506 _id, _ = self.engine.new_item(
1507 rollback, engine_session, engine_topic, indata, kwargs
1508 )
1509 self._set_location_header(main_topic, version, topic, _id)
1510 indata["lcmOperationType"] = "instantiate"
1511 indata["netsliceInstanceId"] = _id
1512 nsilcmop_id, _ = self.engine.new_item(
1513 rollback, engine_session, "nsilcmops", indata, kwargs
1514 )
1515 outdata = {"id": _id, "nsilcmop_id": nsilcmop_id}
1516 elif topic == "netslice_instances" and item:
1517 indata["lcmOperationType"] = item
1518 indata["netsliceInstanceId"] = _id
1519 _id, _ = self.engine.new_item(
1520 rollback, engine_session, "nsilcmops", indata, kwargs
1521 )
1522 self._set_location_header(
1523 main_topic, version, "nsi_lcm_op_occs", _id
1524 )
1525 outdata = {"id": _id}
1526 cherrypy.response.status = HTTPStatus.ACCEPTED.value
1527 elif topic == "vnf_packages" and item == "action":
1528 indata["lcmOperationType"] = item
1529 indata["vnfPkgId"] = _id
1530 _id, _ = self.engine.new_item(
1531 rollback, engine_session, "vnfpkgops", indata, kwargs
1532 )
1533 self._set_location_header(
1534 main_topic, version, "vnfpkg_op_occs", _id
1535 )
1536 outdata = {"id": _id}
1537 cherrypy.response.status = HTTPStatus.ACCEPTED.value
1538 elif topic == "subscriptions":
1539 _id, _ = self.engine.new_item(
1540 rollback, engine_session, engine_topic, indata, kwargs
1541 )
1542 self._set_location_header(main_topic, version, topic, _id)
1543 link = {}
1544 link["self"] = cherrypy.response.headers["Location"]
1545 outdata = {
1546 "id": _id,
1547 "filter": indata["filter"],
1548 "callbackUri": indata["CallbackUri"],
1549 "_links": link,
1550 }
1551 cherrypy.response.status = HTTPStatus.CREATED.value
1552 elif topic == "vnf_instances" and item:
1553 indata["lcmOperationType"] = item
1554 indata["vnfInstanceId"] = _id
1555 _id, _ = self.engine.new_item(rollback, engine_session, "vnflcmops", indata, kwargs)
1556 self._set_location_header(main_topic, version, "vnf_lcm_op_occs", _id)
1557 outdata = {"id": _id}
1558 cherrypy.response.status = HTTPStatus.ACCEPTED.value
1559 else:
1560 _id, op_id = self.engine.new_item(
1561 rollback,
1562 engine_session,
1563 engine_topic,
1564 indata,
1565 kwargs,
1566 cherrypy.request.headers,
1567 )
1568 self._set_location_header(main_topic, version, topic, _id)
1569 outdata = {"id": _id}
1570 if op_id:
1571 outdata["op_id"] = op_id
1572 cherrypy.response.status = HTTPStatus.ACCEPTED.value
1573 # TODO form NsdInfo when topic in ("ns_descriptors", "vnf_packages")
1574
1575 elif method == "DELETE":
1576 if not _id:
1577 outdata = self.engine.del_item_list(
1578 engine_session, engine_topic, kwargs
1579 )
1580 cherrypy.response.status = HTTPStatus.OK.value
1581 else: # len(args) > 1
1582 # for NS NSI generate an operation
1583 op_id = None
1584 if topic == "ns_instances_content" and not engine_session["force"]:
1585 nslcmop_desc = {
1586 "lcmOperationType": "terminate",
1587 "nsInstanceId": _id,
1588 "autoremove": True,
1589 }
1590 op_id, _ = self.engine.new_item(
1591 rollback, engine_session, "nslcmops", nslcmop_desc, kwargs
1592 )
1593 if op_id:
1594 outdata = {"_id": op_id}
1595 elif (
1596 topic == "netslice_instances_content"
1597 and not engine_session["force"]
1598 ):
1599 nsilcmop_desc = {
1600 "lcmOperationType": "terminate",
1601 "netsliceInstanceId": _id,
1602 "autoremove": True,
1603 }
1604 op_id, _ = self.engine.new_item(
1605 rollback, engine_session, "nsilcmops", nsilcmop_desc, None
1606 )
1607 if op_id:
1608 outdata = {"_id": op_id}
1609 # if there is not any deletion in process, delete
1610 if not op_id:
1611 op_id = self.engine.del_item(engine_session, engine_topic, _id)
1612 if op_id:
1613 outdata = {"op_id": op_id}
1614 cherrypy.response.status = (
1615 HTTPStatus.ACCEPTED.value
1616 if op_id
1617 else HTTPStatus.NO_CONTENT.value
1618 )
1619
1620 elif method in ("PUT", "PATCH"):
1621 op_id = None
1622 if not indata and not kwargs and not engine_session.get("set_project"):
1623 raise NbiException(
1624 "Nothing to update. Provide payload and/or query string",
1625 HTTPStatus.BAD_REQUEST,
1626 )
1627 if (
1628 item in ("nsd_content", "package_content", "nst_content")
1629 and method == "PUT"
1630 ):
1631 completed = self.engine.upload_content(
1632 engine_session,
1633 engine_topic,
1634 _id,
1635 indata,
1636 kwargs,
1637 cherrypy.request.headers,
1638 )
1639 if not completed:
1640 cherrypy.response.headers["Transaction-Id"] = id
1641 else:
1642 op_id = self.engine.edit_item(
1643 engine_session, engine_topic, _id, indata, kwargs
1644 )
1645
1646 if op_id:
1647 cherrypy.response.status = HTTPStatus.ACCEPTED.value
1648 outdata = {"op_id": op_id}
1649 else:
1650 cherrypy.response.status = HTTPStatus.NO_CONTENT.value
1651 outdata = None
1652 else:
1653 raise NbiException(
1654 "Method {} not allowed".format(method),
1655 HTTPStatus.METHOD_NOT_ALLOWED,
1656 )
1657
1658 # if Role information changes, it is needed to reload the information of roles
1659 if topic == "roles" and method != "GET":
1660 self.authenticator.load_operation_to_allowed_roles()
1661
1662 if (
1663 topic == "projects"
1664 and method == "DELETE"
1665 or topic in ["users", "roles"]
1666 and method in ["PUT", "PATCH", "DELETE"]
1667 ):
1668 self.authenticator.remove_token_from_cache()
1669
1670 return self._format_out(outdata, token_info, _format)
1671 except Exception as e:
1672 if isinstance(
1673 e,
1674 (
1675 NbiException,
1676 EngineException,
1677 DbException,
1678 FsException,
1679 MsgException,
1680 AuthException,
1681 ValidationError,
1682 AuthconnException,
1683 ),
1684 ):
1685 http_code_value = cherrypy.response.status = e.http_code.value
1686 http_code_name = e.http_code.name
1687 cherrypy.log("Exception {}".format(e))
1688 else:
1689 http_code_value = (
1690 cherrypy.response.status
1691 ) = HTTPStatus.BAD_REQUEST.value # INTERNAL_SERVER_ERROR
1692 cherrypy.log("CRITICAL: Exception {}".format(e), traceback=True)
1693 http_code_name = HTTPStatus.BAD_REQUEST.name
1694 if hasattr(outdata, "close"): # is an open file
1695 outdata.close()
1696 error_text = str(e)
1697 rollback.reverse()
1698 for rollback_item in rollback:
1699 try:
1700 if rollback_item.get("operation") == "set":
1701 self.engine.db.set_one(
1702 rollback_item["topic"],
1703 {"_id": rollback_item["_id"]},
1704 rollback_item["content"],
1705 fail_on_empty=False,
1706 )
1707 elif rollback_item.get("operation") == "del_list":
1708 self.engine.db.del_list(
1709 rollback_item["topic"],
1710 rollback_item["filter"],
1711 fail_on_empty=False,
1712 )
1713 else:
1714 self.engine.db.del_one(
1715 rollback_item["topic"],
1716 {"_id": rollback_item["_id"]},
1717 fail_on_empty=False,
1718 )
1719 except Exception as e2:
1720 rollback_error_text = "Rollback Exception {}: {}".format(
1721 rollback_item, e2
1722 )
1723 cherrypy.log(rollback_error_text)
1724 error_text += ". " + rollback_error_text
1725 # if isinstance(e, MsgException):
1726 # error_text = "{} has been '{}' but other modules cannot be informed because an error on bus".format(
1727 # engine_topic[:-1], method, error_text)
1728 problem_details = {
1729 "code": http_code_name,
1730 "status": http_code_value,
1731 "detail": error_text,
1732 }
1733 return self._format_out(problem_details, token_info)
1734 # raise cherrypy.HTTPError(e.http_code.value, str(e))
1735 finally:
1736 if token_info:
1737 self._format_login(token_info)
1738 if method in ("PUT", "PATCH", "POST") and isinstance(outdata, dict):
1739 for logging_id in ("id", "op_id", "nsilcmop_id", "nslcmop_id"):
1740 if outdata.get(logging_id):
1741 cherrypy.request.login += ";{}={}".format(
1742 logging_id, outdata[logging_id][:36]
1743 )
1744
1745
1746 def _start_service():
1747 """
1748 Callback function called when cherrypy.engine starts
1749 Override configuration with env variables
1750 Set database, storage, message configuration
1751 Init database with admin/admin user password
1752 """
1753 global nbi_server
1754 global subscription_thread
1755 cherrypy.log.error("Starting osm_nbi")
1756 # update general cherrypy configuration
1757 update_dict = {}
1758
1759 engine_config = cherrypy.tree.apps["/osm"].config
1760 for k, v in environ.items():
1761 if not k.startswith("OSMNBI_"):
1762 continue
1763 k1, _, k2 = k[7:].lower().partition("_")
1764 if not k2:
1765 continue
1766 try:
1767 # update static configuration
1768 if k == "OSMNBI_STATIC_DIR":
1769 engine_config["/static"]["tools.staticdir.dir"] = v
1770 engine_config["/static"]["tools.staticdir.on"] = True
1771 elif k == "OSMNBI_SOCKET_PORT" or k == "OSMNBI_SERVER_PORT":
1772 update_dict["server.socket_port"] = int(v)
1773 elif k == "OSMNBI_SOCKET_HOST" or k == "OSMNBI_SERVER_HOST":
1774 update_dict["server.socket_host"] = v
1775 elif k1 in ("server", "test", "auth", "log"):
1776 update_dict[k1 + "." + k2] = v
1777 elif k1 in ("message", "database", "storage", "authentication"):
1778 # k2 = k2.replace('_', '.')
1779 if k2 in ("port", "db_port"):
1780 engine_config[k1][k2] = int(v)
1781 else:
1782 engine_config[k1][k2] = v
1783
1784 except ValueError as e:
1785 cherrypy.log.error("Ignoring environ '{}': " + str(e))
1786 except Exception as e:
1787 cherrypy.log.warn("skipping environ '{}' on exception '{}'".format(k, e))
1788
1789 if update_dict:
1790 cherrypy.config.update(update_dict)
1791 engine_config["global"].update(update_dict)
1792
1793 # logging cherrypy
1794 log_format_simple = (
1795 "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(message)s"
1796 )
1797 log_formatter_simple = logging.Formatter(
1798 log_format_simple, datefmt="%Y-%m-%dT%H:%M:%S"
1799 )
1800 logger_server = logging.getLogger("cherrypy.error")
1801 logger_access = logging.getLogger("cherrypy.access")
1802 logger_cherry = logging.getLogger("cherrypy")
1803 logger_nbi = logging.getLogger("nbi")
1804
1805 if "log.file" in engine_config["global"]:
1806 file_handler = logging.handlers.RotatingFileHandler(
1807 engine_config["global"]["log.file"], maxBytes=100e6, backupCount=9, delay=0
1808 )
1809 file_handler.setFormatter(log_formatter_simple)
1810 logger_cherry.addHandler(file_handler)
1811 logger_nbi.addHandler(file_handler)
1812 # log always to standard output
1813 for format_, logger in {
1814 "nbi.server %(filename)s:%(lineno)s": logger_server,
1815 "nbi.access %(filename)s:%(lineno)s": logger_access,
1816 "%(name)s %(filename)s:%(lineno)s": logger_nbi,
1817 }.items():
1818 log_format_cherry = "%(asctime)s %(levelname)s {} %(message)s".format(format_)
1819 log_formatter_cherry = logging.Formatter(
1820 log_format_cherry, datefmt="%Y-%m-%dT%H:%M:%S"
1821 )
1822 str_handler = logging.StreamHandler()
1823 str_handler.setFormatter(log_formatter_cherry)
1824 logger.addHandler(str_handler)
1825
1826 if engine_config["global"].get("log.level"):
1827 logger_cherry.setLevel(engine_config["global"]["log.level"])
1828 logger_nbi.setLevel(engine_config["global"]["log.level"])
1829
1830 # logging other modules
1831 for k1, logname in {
1832 "message": "nbi.msg",
1833 "database": "nbi.db",
1834 "storage": "nbi.fs",
1835 }.items():
1836 engine_config[k1]["logger_name"] = logname
1837 logger_module = logging.getLogger(logname)
1838 if "logfile" in engine_config[k1]:
1839 file_handler = logging.handlers.RotatingFileHandler(
1840 engine_config[k1]["logfile"], maxBytes=100e6, backupCount=9, delay=0
1841 )
1842 file_handler.setFormatter(log_formatter_simple)
1843 logger_module.addHandler(file_handler)
1844 if "loglevel" in engine_config[k1]:
1845 logger_module.setLevel(engine_config[k1]["loglevel"])
1846 # TODO add more entries, e.g.: storage
1847 cherrypy.tree.apps["/osm"].root.engine.start(engine_config)
1848 cherrypy.tree.apps["/osm"].root.authenticator.start(engine_config)
1849 cherrypy.tree.apps["/osm"].root.engine.init_db(target_version=database_version)
1850 cherrypy.tree.apps["/osm"].root.authenticator.init_db(
1851 target_version=auth_database_version
1852 )
1853
1854 # start subscriptions thread:
1855 subscription_thread = SubscriptionThread(
1856 config=engine_config, engine=nbi_server.engine
1857 )
1858 subscription_thread.start()
1859 # Do not capture except SubscriptionException
1860
1861 backend = engine_config["authentication"]["backend"]
1862 cherrypy.log.error(
1863 "Starting OSM NBI Version '{} {}' with '{}' authentication backend".format(
1864 nbi_version, nbi_version_date, backend
1865 )
1866 )
1867
1868
1869 def _stop_service():
1870 """
1871 Callback function called when cherrypy.engine stops
1872 TODO: Ending database connections.
1873 """
1874 global subscription_thread
1875 if subscription_thread:
1876 subscription_thread.terminate()
1877 subscription_thread = None
1878 cherrypy.tree.apps["/osm"].root.engine.stop()
1879 cherrypy.log.error("Stopping osm_nbi")
1880
1881
1882 def nbi(config_file):
1883 global nbi_server
1884 # conf = {
1885 # '/': {
1886 # #'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
1887 # 'tools.sessions.on': True,
1888 # 'tools.response_headers.on': True,
1889 # # 'tools.response_headers.headers': [('Content-Type', 'text/plain')],
1890 # }
1891 # }
1892 # cherrypy.Server.ssl_module = 'builtin'
1893 # cherrypy.Server.ssl_certificate = "http/cert.pem"
1894 # cherrypy.Server.ssl_private_key = "http/privkey.pem"
1895 # cherrypy.Server.thread_pool = 10
1896 # cherrypy.config.update({'Server.socket_port': config["port"], 'Server.socket_host': config["host"]})
1897
1898 # cherrypy.config.update({'tools.auth_basic.on': True,
1899 # 'tools.auth_basic.realm': 'localhost',
1900 # 'tools.auth_basic.checkpassword': validate_password})
1901 nbi_server = Server()
1902 cherrypy.engine.subscribe("start", _start_service)
1903 cherrypy.engine.subscribe("stop", _stop_service)
1904 cherrypy.quickstart(nbi_server, "/osm", config_file)
1905
1906
1907 def usage():
1908 print(
1909 """Usage: {} [options]
1910 -c|--config [configuration_file]: loads the configuration file (default: ./nbi.cfg)
1911 -h|--help: shows this help
1912 """.format(
1913 sys.argv[0]
1914 )
1915 )
1916 # --log-socket-host HOST: send logs to this host")
1917 # --log-socket-port PORT: send logs using this port (default: 9022)")
1918
1919
1920 if __name__ == "__main__":
1921 try:
1922 # load parameters and configuration
1923 opts, args = getopt.getopt(sys.argv[1:], "hvc:", ["config=", "help"])
1924 # TODO add "log-socket-host=", "log-socket-port=", "log-file="
1925 config_file = None
1926 for o, a in opts:
1927 if o in ("-h", "--help"):
1928 usage()
1929 sys.exit()
1930 elif o in ("-c", "--config"):
1931 config_file = a
1932 # elif o == "--log-socket-port":
1933 # log_socket_port = a
1934 # elif o == "--log-socket-host":
1935 # log_socket_host = a
1936 # elif o == "--log-file":
1937 # log_file = a
1938 else:
1939 assert False, "Unhandled option"
1940 if config_file:
1941 if not path.isfile(config_file):
1942 print(
1943 "configuration file '{}' that not exist".format(config_file),
1944 file=sys.stderr,
1945 )
1946 exit(1)
1947 else:
1948 for config_file in (
1949 __file__[: __file__.rfind(".")] + ".cfg",
1950 "./nbi.cfg",
1951 "/etc/osm/nbi.cfg",
1952 ):
1953 if path.isfile(config_file):
1954 break
1955 else:
1956 print(
1957 "No configuration file 'nbi.cfg' found neither at local folder nor at /etc/osm/",
1958 file=sys.stderr,
1959 )
1960 exit(1)
1961 nbi(config_file)
1962 except getopt.GetoptError as e:
1963 print(str(e), file=sys.stderr)
1964 # usage()
1965 exit(1)