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