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