blob: 06b39f3049a676301ab07d4b0ba5f2fd63b2dde2 [file] [log] [blame]
tiernoc94c3df2018-02-09 15:38:54 +01001#!/usr/bin/python3
2# -*- coding: utf-8 -*-
3
tiernod125caf2018-11-22 16:05:54 +00004# 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
tiernoc94c3df2018-02-09 15:38:54 +010017import cherrypy
18import time
19import json
20import yaml
tierno9c630112019-08-29 14:21:41 +000021import osm_nbi.html_out as html
tiernoc94c3df2018-02-09 15:38:54 +010022import logging
tiernof5298be2018-05-16 14:43:57 +020023import logging.handlers
24import getopt
25import sys
Eduardo Sousa2f988212018-07-26 01:04:11 +010026
tierno9c630112019-08-29 14:21:41 +000027from osm_nbi.authconn import AuthException, AuthconnException
28from osm_nbi.auth import Authenticator
29from osm_nbi.engine import Engine, EngineException
30from osm_nbi.subscriptions import SubscriptionThread
31from osm_nbi.validation import ValidationError
tiernoa8d63632018-05-10 13:12:32 +020032from osm_common.dbbase import DbException
33from osm_common.fsbase import FsException
34from osm_common.msgbase import MsgException
tiernoc94c3df2018-02-09 15:38:54 +010035from http import HTTPStatus
tiernoc94c3df2018-02-09 15:38:54 +010036from codecs import getreader
tiernof5298be2018-05-16 14:43:57 +020037from os import environ, path
tiernob2e48bd2020-02-04 15:47:18 +000038from osm_nbi import version as nbi_version, version_date as nbi_version_date
tiernoc94c3df2018-02-09 15:38:54 +010039
40__author__ = "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
tiernodfe09572018-04-24 10:41:10 +020041
tierno9c630112019-08-29 14:21:41 +000042__version__ = "0.1.3" # file version, not NBI version
43version_date = "Aug 2019"
44
tierno1f029d82019-06-13 22:37:04 +000045database_version = '1.2'
Eduardo Sousa819d34c2018-07-31 01:20:02 +010046auth_database_version = '1.0'
tierno932499c2019-01-28 17:28:10 +000047nbi_server = None # instance of Server class
48subscription_thread = None # instance of SubscriptionThread class
tiernoc94c3df2018-02-09 15:38:54 +010049
50"""
tiernof27c79b2018-03-12 17:08:42 +010051North Bound Interface (O: OSM specific; 5,X: SOL005 not implemented yet; O5: SOL005 implemented)
tiernoc94c3df2018-02-09 15:38:54 +010052URL: /osm GET POST PUT DELETE PATCH
garciadeblas9750c5a2018-10-15 16:20:35 +020053 /nsd/v1
tierno2236d202018-05-16 19:05:16 +020054 /ns_descriptors_content O O
55 /<nsdInfoId> O O O O
tiernoc94c3df2018-02-09 15:38:54 +010056 /ns_descriptors O5 O5
57 /<nsdInfoId> O5 O5 5
58 /nsd_content O5 O5
tiernof27c79b2018-03-12 17:08:42 +010059 /nsd O
60 /artifacts[/<artifactPath>] O
tiernoc94c3df2018-02-09 15:38:54 +010061 /pnf_descriptors 5 5
62 /<pnfdInfoId> 5 5 5
63 /pnfd_content 5 5
tiernof27c79b2018-03-12 17:08:42 +010064 /subscriptions 5 5
65 /<subscriptionId> 5 X
tiernoc94c3df2018-02-09 15:38:54 +010066
67 /vnfpkgm/v1
tierno55945e72018-04-06 16:40:27 +020068 /vnf_packages_content O O
tierno2236d202018-05-16 19:05:16 +020069 /<vnfPkgId> O O
tiernoc94c3df2018-02-09 15:38:54 +010070 /vnf_packages O5 O5
71 /<vnfPkgId> O5 O5 5
tiernoc94c3df2018-02-09 15:38:54 +010072 /package_content O5 O5
73 /upload_from_uri X
tiernof27c79b2018-03-12 17:08:42 +010074 /vnfd O5
75 /artifacts[/<artifactPath>] O5
76 /subscriptions X X
77 /<subscriptionId> X X
tiernoc94c3df2018-02-09 15:38:54 +010078
79 /nslcm/v1
tiernof27c79b2018-03-12 17:08:42 +010080 /ns_instances_content O O
tierno2236d202018-05-16 19:05:16 +020081 /<nsInstanceId> O O
tiernof27c79b2018-03-12 17:08:42 +010082 /ns_instances 5 5
tierno95692442018-05-24 18:05:28 +020083 /<nsInstanceId> O5 O5
tierno65acb4d2018-04-06 16:42:40 +020084 instantiate O5
85 terminate O5
86 action O
87 scale O5
88 heal 5
tiernoc94c3df2018-02-09 15:38:54 +010089 /ns_lcm_op_occs 5 5
90 /<nsLcmOpOccId> 5 5 5
91 TO BE COMPLETED 5 5
tiernof759d822018-06-11 18:54:54 +020092 /vnf_instances (also vnfrs for compatibility) O
93 /<vnfInstanceId> O
tiernof27c79b2018-03-12 17:08:42 +010094 /subscriptions 5 5
95 /<subscriptionId> 5 X
garciadeblas9750c5a2018-10-15 16:20:35 +020096
tiernocb83c942018-09-24 17:28:13 +020097 /pdu/v1
tierno032916c2019-03-22 13:27:12 +000098 /pdu_descriptors O O
tiernocb83c942018-09-24 17:28:13 +020099 /<id> O O O O
garciadeblas9750c5a2018-10-15 16:20:35 +0200100
tiernof27c79b2018-03-12 17:08:42 +0100101 /admin/v1
102 /tokens O O
tierno2236d202018-05-16 19:05:16 +0200103 /<id> O O
tiernof27c79b2018-03-12 17:08:42 +0100104 /users O O
tiernocd54a4a2018-09-12 16:40:35 +0200105 /<id> O O O O
tiernof27c79b2018-03-12 17:08:42 +0100106 /projects O O
tierno2236d202018-05-16 19:05:16 +0200107 /<id> O O
tierno55ba2e62018-12-11 17:22:22 +0000108 /vim_accounts (also vims for compatibility) O O
109 /<id> O O O
110 /wim_accounts O O
tierno2236d202018-05-16 19:05:16 +0200111 /<id> O O O
tierno0f98af52018-03-19 10:28:22 +0100112 /sdns O O
tierno2236d202018-05-16 19:05:16 +0200113 /<id> O O O
delacruzramofe598fe2019-10-23 18:25:11 +0200114 /k8sclusters O O
115 /<id> O O O
116 /k8srepos O O
117 /<id> O O
tiernoc94c3df2018-02-09 15:38:54 +0100118
garciadeblas9750c5a2018-10-15 16:20:35 +0200119 /nst/v1 O O
120 /netslice_templates_content O O
121 /<nstInfoId> O O O O
122 /netslice_templates O O
123 /<nstInfoId> O O O
124 /nst_content O O
125 /nst O
126 /artifacts[/<artifactPath>] O
127 /subscriptions X X
128 /<subscriptionId> X X
129
130 /nsilcm/v1
131 /netslice_instances_content O O
132 /<SliceInstanceId> O O
133 /netslice_instances O O
134 /<SliceInstanceId> O O
135 instantiate O
136 terminate O
137 action O
138 /nsi_lcm_op_occs O O
139 /<nsiLcmOpOccId> O O O
140 /subscriptions X X
141 /<subscriptionId> X X
142
tierno2236d202018-05-16 19:05:16 +0200143query string:
144 Follows SOL005 section 4.3.2 It contains extra METHOD to override http method, FORCE to force.
tierno36ec8602018-11-02 17:27:11 +0100145 simpleFilterExpr := <attrName>["."<attrName>]*["."<op>]"="<value>[","<value>]*
146 filterExpr := <simpleFilterExpr>["&"<simpleFilterExpr>]*
147 op := "eq" | "neq" (or "ne") | "gt" | "lt" | "gte" | "lte" | "cont" | "ncont"
148 attrName := string
tierno2236d202018-05-16 19:05:16 +0200149 For filtering inside array, it must select the element of the array, or add ANYINDEX to apply the filtering over any
150 item of the array, that is, pass if any item of the array pass the filter.
151 It allows both ne and neq for not equal
152 TODO: 4.3.3 Attribute selectors
153 all_fields, fields=x,y,.., exclude_default, exclude_fields=x,y,...
tiernoc94c3df2018-02-09 15:38:54 +0100154 (none) … same as “exclude_default”
155 all_fields … all attributes.
tierno2236d202018-05-16 19:05:16 +0200156 fields=<list> … all attributes except all complex attributes with minimum cardinality of zero that are not
157 conditionally mandatory, and that are not provided in <list>.
158 exclude_fields=<list> … all attributes except those complex attributes with a minimum cardinality of zero that
159 are not conditionally mandatory, and that are provided in <list>.
160 exclude_default … all attributes except those complex attributes with a minimum cardinality of zero that are not
161 conditionally mandatory, and that are part of the "default exclude set" defined in the present specification for
162 the particular resource
163 exclude_default and include=<list> … all attributes except those complex attributes with a minimum cardinality
164 of zero that are not conditionally mandatory and that are part of the "default exclude set" defined in the
165 present specification for the particular resource, but that are not part of <list>
tierno65ca36d2019-02-12 19:27:52 +0100166 Additionally it admits some administrator values:
167 FORCE: To force operations skipping dependency checkings
168 ADMIN: To act as an administrator or a different project
169 PUBLIC: To get public descriptors or set a descriptor as public
170 SET_PROJECT: To make a descriptor available for other project
171
tiernoc94c3df2018-02-09 15:38:54 +0100172Header field name Reference Example Descriptions
173 Accept IETF RFC 7231 [19] application/json Content-Types that are acceptable for the response.
174 This header field shall be present if the response is expected to have a non-empty message body.
175 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the request.
176 This header field shall be present if the request has a non-empty message body.
tierno2236d202018-05-16 19:05:16 +0200177 Authorization IETF RFC 7235 [22] Bearer mF_9.B5f-4.1JqM The authorization token for the request.
178 Details are specified in clause 4.5.3.
tiernoc94c3df2018-02-09 15:38:54 +0100179 Range IETF RFC 7233 [21] 1000-2000 Requested range of bytes from a file
180Header field name Reference Example Descriptions
181 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the response.
182 This header field shall be present if the response has a non-empty message body.
tierno2236d202018-05-16 19:05:16 +0200183 Location IETF RFC 7231 [19] http://www.example.com/vnflcm/v1/vnf_instances/123 Used in redirection, or when a
184 new resource has been created.
tiernoc94c3df2018-02-09 15:38:54 +0100185 This header field shall be present if the response status code is 201 or 3xx.
tierno2236d202018-05-16 19:05:16 +0200186 In the present document this header field is also used if the response status code is 202 and a new resource was
187 created.
188 WWW-Authenticate IETF RFC 7235 [22] Bearer realm="example" Challenge if the corresponding HTTP request has not
189 provided authorization, or error details if the corresponding HTTP request has provided an invalid authorization
190 token.
191 Accept-Ranges IETF RFC 7233 [21] bytes Used by the Server to signal whether or not it supports ranges for
192 certain resources.
193 Content-Range IETF RFC 7233 [21] bytes 21010-47021/ 47022 Signals the byte range that is contained in the
194 response, and the total length of the file.
tiernoc94c3df2018-02-09 15:38:54 +0100195 Retry-After IETF RFC 7231 [19] Fri, 31 Dec 1999 23:59:59 GMT
tiernoc94c3df2018-02-09 15:38:54 +0100196"""
197
tierno701018c2019-06-25 11:13:14 +0000198valid_query_string = ("ADMIN", "SET_PROJECT", "FORCE", "PUBLIC")
199# ^ Contains possible administrative query string words:
200# ADMIN=True(by default)|Project|Project-list: See all elements, or elements of a project
201# (not owned by my session project).
202# PUBLIC=True(by default)|False: See/hide public elements. Set/Unset a topic to be public
203# FORCE=True(by default)|False: Force edition/deletion operations
204# SET_PROJECT=Project|Project-list: Add/Delete the topic to the projects portfolio
205
206valid_url_methods = {
207 # contains allowed URL and methods, and the role_permission name
208 "admin": {
209 "v1": {
210 "tokens": {"METHODS": ("GET", "POST", "DELETE"),
211 "ROLE_PERMISSION": "tokens:",
212 "<ID>": {"METHODS": ("GET", "DELETE"),
213 "ROLE_PERMISSION": "tokens:id:"
214 }
215 },
216 "users": {"METHODS": ("GET", "POST"),
217 "ROLE_PERMISSION": "users:",
delacruzramo1459d602019-10-03 14:22:00 +0200218 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"),
tierno701018c2019-06-25 11:13:14 +0000219 "ROLE_PERMISSION": "users:id:"
220 }
221 },
222 "projects": {"METHODS": ("GET", "POST"),
223 "ROLE_PERMISSION": "projects:",
delacruzramo1459d602019-10-03 14:22:00 +0200224 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"),
tierno701018c2019-06-25 11:13:14 +0000225 "ROLE_PERMISSION": "projects:id:"}
226 },
227 "roles": {"METHODS": ("GET", "POST"),
228 "ROLE_PERMISSION": "roles:",
delacruzramo1459d602019-10-03 14:22:00 +0200229 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"),
tierno701018c2019-06-25 11:13:14 +0000230 "ROLE_PERMISSION": "roles:id:"
231 }
232 },
233 "vims": {"METHODS": ("GET", "POST"),
234 "ROLE_PERMISSION": "vims:",
delacruzramo1459d602019-10-03 14:22:00 +0200235 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"),
tierno701018c2019-06-25 11:13:14 +0000236 "ROLE_PERMISSION": "vims:id:"
237 }
238 },
239 "vim_accounts": {"METHODS": ("GET", "POST"),
240 "ROLE_PERMISSION": "vim_accounts:",
delacruzramo1459d602019-10-03 14:22:00 +0200241 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"),
tierno701018c2019-06-25 11:13:14 +0000242 "ROLE_PERMISSION": "vim_accounts:id:"
243 }
244 },
245 "wim_accounts": {"METHODS": ("GET", "POST"),
246 "ROLE_PERMISSION": "wim_accounts:",
delacruzramo1459d602019-10-03 14:22:00 +0200247 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"),
tierno701018c2019-06-25 11:13:14 +0000248 "ROLE_PERMISSION": "wim_accounts:id:"
249 }
250 },
251 "sdns": {"METHODS": ("GET", "POST"),
252 "ROLE_PERMISSION": "sdn_controllers:",
delacruzramo1459d602019-10-03 14:22:00 +0200253 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"),
tierno701018c2019-06-25 11:13:14 +0000254 "ROLE_PERMISSION": "sdn_controllers:id:"
255 }
256 },
delacruzramofe598fe2019-10-23 18:25:11 +0200257 "k8sclusters": {"METHODS": ("GET", "POST"),
258 "ROLE_PERMISSION": "k8sclusters:",
delacruzramo549dda62019-11-14 09:57:35 +0100259 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"),
delacruzramofe598fe2019-10-23 18:25:11 +0200260 "ROLE_PERMISSION": "k8sclusters:id:"
261 }
262 },
263 "k8srepos": {"METHODS": ("GET", "POST"),
264 "ROLE_PERMISSION": "k8srepos:",
265 "<ID>": {"METHODS": ("GET", "DELETE"),
266 "ROLE_PERMISSION": "k8srepos:id:"
267 }
268 },
269
tierno701018c2019-06-25 11:13:14 +0000270 }
271 },
272 "pdu": {
273 "v1": {
274 "pdu_descriptors": {"METHODS": ("GET", "POST"),
275 "ROLE_PERMISSION": "pduds:",
276 "<ID>": {"METHODS": ("GET", "POST", "DELETE", "PATCH", "PUT"),
277 "ROLE_PERMISSION": "pduds:id:"
278 }
279 },
280 }
281 },
282 "nsd": {
283 "v1": {
284 "ns_descriptors_content": {"METHODS": ("GET", "POST"),
285 "ROLE_PERMISSION": "nsds:",
286 "<ID>": {"METHODS": ("GET", "PUT", "DELETE"),
287 "ROLE_PERMISSION": "nsds:id:"
288 }
289 },
290 "ns_descriptors": {"METHODS": ("GET", "POST"),
291 "ROLE_PERMISSION": "nsds:",
292 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"),
293 "ROLE_PERMISSION": "nsds:id:",
294 "nsd_content": {"METHODS": ("GET", "PUT"),
295 "ROLE_PERMISSION": "nsds:id:content:",
296 },
297 "nsd": {"METHODS": ("GET",), # descriptor inside package
298 "ROLE_PERMISSION": "nsds:id:content:"
299 },
300 "artifacts": {"*": {"METHODS": ("GET",),
301 "ROLE_PERMISSION": "nsds:id:nsd_artifact:"
302 }
303 }
304 }
305 },
306 "pnf_descriptors": {"TODO": ("GET", "POST"),
307 "<ID>": {"TODO": ("GET", "DELETE", "PATCH"),
308 "pnfd_content": {"TODO": ("GET", "PUT")}
309 }
310 },
311 "subscriptions": {"TODO": ("GET", "POST"),
312 "<ID>": {"TODO": ("GET", "DELETE")}
313 },
314 }
315 },
316 "vnfpkgm": {
317 "v1": {
318 "vnf_packages_content": {"METHODS": ("GET", "POST"),
319 "ROLE_PERMISSION": "vnfds:",
320 "<ID>": {"METHODS": ("GET", "PUT", "DELETE"),
321 "ROLE_PERMISSION": "vnfds:id:"}
322 },
323 "vnf_packages": {"METHODS": ("GET", "POST"),
324 "ROLE_PERMISSION": "vnfds:",
325 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"), # GET: vnfPkgInfo
326 "ROLE_PERMISSION": "vnfds:id:",
327 "package_content": {"METHODS": ("GET", "PUT"), # package
328 "ROLE_PERMISSION": "vnfds:id:",
329 "upload_from_uri": {"METHODS": (),
330 "TODO": ("POST", ),
331 "ROLE_PERMISSION": "vnfds:id:upload:"
332 }
333 },
334 "vnfd": {"METHODS": ("GET", ), # descriptor inside package
335 "ROLE_PERMISSION": "vnfds:id:content:"
336 },
337 "artifacts": {"*": {"METHODS": ("GET", ),
338 "ROLE_PERMISSION": "vnfds:id:vnfd_artifact:"
339 }
340 }
341 }
342 },
343 "subscriptions": {"TODO": ("GET", "POST"),
344 "<ID>": {"TODO": ("GET", "DELETE")}
345 },
346 }
347 },
348 "nslcm": {
349 "v1": {
350 "ns_instances_content": {"METHODS": ("GET", "POST"),
351 "ROLE_PERMISSION": "ns_instances:",
352 "<ID>": {"METHODS": ("GET", "DELETE"),
353 "ROLE_PERMISSION": "ns_instances:id:"
354 }
355 },
356 "ns_instances": {"METHODS": ("GET", "POST"),
357 "ROLE_PERMISSION": "ns_instances:",
358 "<ID>": {"METHODS": ("GET", "DELETE"),
359 "ROLE_PERMISSION": "ns_instances:id:",
360 "scale": {"METHODS": ("POST",),
361 "ROLE_PERMISSION": "ns_instances:id:scale:"
362 },
363 "terminate": {"METHODS": ("POST",),
364 "ROLE_PERMISSION": "ns_instances:id:terminate:"
365 },
366 "instantiate": {"METHODS": ("POST",),
367 "ROLE_PERMISSION": "ns_instances:id:instantiate:"
368 },
369 "action": {"METHODS": ("POST",),
370 "ROLE_PERMISSION": "ns_instances:id:action:"
371 },
372 }
373 },
374 "ns_lcm_op_occs": {"METHODS": ("GET",),
375 "ROLE_PERMISSION": "ns_instances:opps:",
376 "<ID>": {"METHODS": ("GET",),
377 "ROLE_PERMISSION": "ns_instances:opps:id:"
378 },
379 },
380 "vnfrs": {"METHODS": ("GET",),
381 "ROLE_PERMISSION": "vnf_instances:",
382 "<ID>": {"METHODS": ("GET",),
383 "ROLE_PERMISSION": "vnf_instances:id:"
384 }
385 },
386 "vnf_instances": {"METHODS": ("GET",),
387 "ROLE_PERMISSION": "vnf_instances:",
388 "<ID>": {"METHODS": ("GET",),
389 "ROLE_PERMISSION": "vnf_instances:id:"
390 }
391 },
392 }
393 },
394 "nst": {
395 "v1": {
396 "netslice_templates_content": {"METHODS": ("GET", "POST"),
397 "ROLE_PERMISSION": "slice_templates:",
398 "<ID>": {"METHODS": ("GET", "PUT", "DELETE"),
399 "ROLE_PERMISSION": "slice_templates:id:", }
400 },
401 "netslice_templates": {"METHODS": ("GET", "POST"),
402 "ROLE_PERMISSION": "slice_templates:",
403 "<ID>": {"METHODS": ("GET", "DELETE"),
404 "TODO": ("PATCH",),
405 "ROLE_PERMISSION": "slice_templates:id:",
406 "nst_content": {"METHODS": ("GET", "PUT"),
407 "ROLE_PERMISSION": "slice_templates:id:content:"
408 },
409 "nst": {"METHODS": ("GET",), # descriptor inside package
410 "ROLE_PERMISSION": "slice_templates:id:content:"
411 },
412 "artifacts": {"*": {"METHODS": ("GET",),
413 "ROLE_PERMISSION": "slice_templates:id:content:"
414 }
415 }
416 }
417 },
418 "subscriptions": {"TODO": ("GET", "POST"),
419 "<ID>": {"TODO": ("GET", "DELETE")}
420 },
421 }
422 },
423 "nsilcm": {
424 "v1": {
425 "netslice_instances_content": {"METHODS": ("GET", "POST"),
426 "ROLE_PERMISSION": "slice_instances:",
427 "<ID>": {"METHODS": ("GET", "DELETE"),
428 "ROLE_PERMISSION": "slice_instances:id:"
429 }
430 },
431 "netslice_instances": {"METHODS": ("GET", "POST"),
432 "ROLE_PERMISSION": "slice_instances:",
433 "<ID>": {"METHODS": ("GET", "DELETE"),
434 "ROLE_PERMISSION": "slice_instances:id:",
435 "terminate": {"METHODS": ("POST",),
436 "ROLE_PERMISSION": "slice_instances:id:terminate:"
437 },
438 "instantiate": {"METHODS": ("POST",),
439 "ROLE_PERMISSION": "slice_instances:id:instantiate:"
440 },
441 "action": {"METHODS": ("POST",),
442 "ROLE_PERMISSION": "slice_instances:id:action:"
443 },
444 }
445 },
446 "nsi_lcm_op_occs": {"METHODS": ("GET",),
447 "ROLE_PERMISSION": "slice_instances:opps:",
448 "<ID>": {"METHODS": ("GET",),
449 "ROLE_PERMISSION": "slice_instances:opps:id:",
450 },
451 },
452 }
453 },
454 "nspm": {
455 "v1": {
456 "pm_jobs": {
457 "<ID>": {
458 "reports": {
459 "<ID>": {"METHODS": ("GET",),
460 "ROLE_PERMISSION": "reports:id:",
461 }
462 }
463 },
464 },
465 },
466 },
467}
468
tiernoc94c3df2018-02-09 15:38:54 +0100469
470class NbiException(Exception):
471
472 def __init__(self, message, http_code=HTTPStatus.METHOD_NOT_ALLOWED):
473 Exception.__init__(self, message)
474 self.http_code = http_code
475
476
477class Server(object):
478 instance = 0
479 # to decode bytes to str
480 reader = getreader("utf-8")
481
482 def __init__(self):
483 self.instance += 1
tierno701018c2019-06-25 11:13:14 +0000484 self.authenticator = Authenticator(valid_url_methods, valid_query_string)
delacruzramoad682a52019-12-10 16:26:34 +0100485 self.engine = Engine(self.authenticator)
tiernoc94c3df2018-02-09 15:38:54 +0100486
tiernoc94c3df2018-02-09 15:38:54 +0100487 def _format_in(self, kwargs):
488 try:
489 indata = None
490 if cherrypy.request.body.length:
491 error_text = "Invalid input format "
492
493 if "Content-Type" in cherrypy.request.headers:
494 if "application/json" in cherrypy.request.headers["Content-Type"]:
495 error_text = "Invalid json format "
496 indata = json.load(self.reader(cherrypy.request.body))
gcalvinode4adfe2018-10-30 11:46:09 +0100497 cherrypy.request.headers.pop("Content-File-MD5", None)
tiernoc94c3df2018-02-09 15:38:54 +0100498 elif "application/yaml" in cherrypy.request.headers["Content-Type"]:
499 error_text = "Invalid yaml format "
delacruzramob19cadc2019-10-08 10:18:02 +0200500 indata = yaml.load(cherrypy.request.body, Loader=yaml.SafeLoader)
gcalvinode4adfe2018-10-30 11:46:09 +0100501 cherrypy.request.headers.pop("Content-File-MD5", None)
tiernoc94c3df2018-02-09 15:38:54 +0100502 elif "application/binary" in cherrypy.request.headers["Content-Type"] or \
503 "application/gzip" in cherrypy.request.headers["Content-Type"] or \
tiernof27c79b2018-03-12 17:08:42 +0100504 "application/zip" in cherrypy.request.headers["Content-Type"] or \
505 "text/plain" in cherrypy.request.headers["Content-Type"]:
506 indata = cherrypy.request.body # .read()
tiernoc94c3df2018-02-09 15:38:54 +0100507 elif "multipart/form-data" in cherrypy.request.headers["Content-Type"]:
508 if "descriptor_file" in kwargs:
509 filecontent = kwargs.pop("descriptor_file")
510 if not filecontent.file:
511 raise NbiException("empty file or content", HTTPStatus.BAD_REQUEST)
tiernof27c79b2018-03-12 17:08:42 +0100512 indata = filecontent.file # .read()
tiernoc94c3df2018-02-09 15:38:54 +0100513 if filecontent.content_type.value:
514 cherrypy.request.headers["Content-Type"] = filecontent.content_type.value
515 else:
516 # raise cherrypy.HTTPError(HTTPStatus.Not_Acceptable,
517 # "Only 'Content-Type' of type 'application/json' or
518 # 'application/yaml' for input format are available")
519 error_text = "Invalid yaml format "
delacruzramob19cadc2019-10-08 10:18:02 +0200520 indata = yaml.load(cherrypy.request.body, Loader=yaml.SafeLoader)
gcalvinode4adfe2018-10-30 11:46:09 +0100521 cherrypy.request.headers.pop("Content-File-MD5", None)
tiernoc94c3df2018-02-09 15:38:54 +0100522 else:
523 error_text = "Invalid yaml format "
delacruzramob19cadc2019-10-08 10:18:02 +0200524 indata = yaml.load(cherrypy.request.body, Loader=yaml.SafeLoader)
gcalvinode4adfe2018-10-30 11:46:09 +0100525 cherrypy.request.headers.pop("Content-File-MD5", None)
tiernoc94c3df2018-02-09 15:38:54 +0100526 if not indata:
527 indata = {}
528
tiernoc94c3df2018-02-09 15:38:54 +0100529 format_yaml = False
530 if cherrypy.request.headers.get("Query-String-Format") == "yaml":
531 format_yaml = True
532
533 for k, v in kwargs.items():
534 if isinstance(v, str):
535 if v == "":
536 kwargs[k] = None
537 elif format_yaml:
538 try:
delacruzramob19cadc2019-10-08 10:18:02 +0200539 kwargs[k] = yaml.load(v, Loader=yaml.SafeLoader)
tiernoe1281182018-05-22 12:24:36 +0200540 except Exception:
tiernoc94c3df2018-02-09 15:38:54 +0100541 pass
542 elif k.endswith(".gt") or k.endswith(".lt") or k.endswith(".gte") or k.endswith(".lte"):
543 try:
544 kwargs[k] = int(v)
tiernoe1281182018-05-22 12:24:36 +0200545 except Exception:
tiernoc94c3df2018-02-09 15:38:54 +0100546 try:
547 kwargs[k] = float(v)
tiernoe1281182018-05-22 12:24:36 +0200548 except Exception:
tiernoc94c3df2018-02-09 15:38:54 +0100549 pass
550 elif v.find(",") > 0:
551 kwargs[k] = v.split(",")
552 elif isinstance(v, (list, tuple)):
553 for index in range(0, len(v)):
554 if v[index] == "":
555 v[index] = None
556 elif format_yaml:
557 try:
delacruzramob19cadc2019-10-08 10:18:02 +0200558 v[index] = yaml.load(v[index], Loader=yaml.SafeLoader)
tiernoe1281182018-05-22 12:24:36 +0200559 except Exception:
tiernoc94c3df2018-02-09 15:38:54 +0100560 pass
561
tiernof27c79b2018-03-12 17:08:42 +0100562 return indata
tiernoc94c3df2018-02-09 15:38:54 +0100563 except (ValueError, yaml.YAMLError) as exc:
564 raise NbiException(error_text + str(exc), HTTPStatus.BAD_REQUEST)
565 except KeyError as exc:
566 raise NbiException("Query string error: " + str(exc), HTTPStatus.BAD_REQUEST)
tiernob92094f2018-05-11 13:44:22 +0200567 except Exception as exc:
568 raise NbiException(error_text + str(exc), HTTPStatus.BAD_REQUEST)
tiernoc94c3df2018-02-09 15:38:54 +0100569
570 @staticmethod
tierno701018c2019-06-25 11:13:14 +0000571 def _format_out(data, token_info=None, _format=None):
tiernoc94c3df2018-02-09 15:38:54 +0100572 """
573 return string of dictionary data according to requested json, yaml, xml. By default json
tiernof27c79b2018-03-12 17:08:42 +0100574 :param data: response to be sent. Can be a dict, text or file
tierno701018c2019-06-25 11:13:14 +0000575 :param token_info: Contains among other username and project
tierno5792d7d2019-08-30 15:37:12 +0000576 :param _format: The format to be set as Content-Type if data is a file
tiernoc94c3df2018-02-09 15:38:54 +0100577 :return: None
578 """
tierno0f98af52018-03-19 10:28:22 +0100579 accept = cherrypy.request.headers.get("Accept")
tiernof27c79b2018-03-12 17:08:42 +0100580 if data is None:
tierno0f98af52018-03-19 10:28:22 +0100581 if accept and "text/html" in accept:
tierno701018c2019-06-25 11:13:14 +0000582 return html.format(data, cherrypy.request, cherrypy.response, token_info)
tierno09c073e2018-04-26 13:36:48 +0200583 # cherrypy.response.status = HTTPStatus.NO_CONTENT.value
tiernof27c79b2018-03-12 17:08:42 +0100584 return
585 elif hasattr(data, "read"): # file object
586 if _format:
587 cherrypy.response.headers["Content-Type"] = _format
588 elif "b" in data.mode: # binariy asssumig zip
589 cherrypy.response.headers["Content-Type"] = 'application/zip'
590 else:
591 cherrypy.response.headers["Content-Type"] = 'text/plain'
592 # TODO check that cherrypy close file. If not implement pending things to close per thread next
593 return data
tierno0f98af52018-03-19 10:28:22 +0100594 if accept:
tiernoc94c3df2018-02-09 15:38:54 +0100595 if "application/json" in accept:
596 cherrypy.response.headers["Content-Type"] = 'application/json; charset=utf-8'
597 a = json.dumps(data, indent=4) + "\n"
598 return a.encode("utf8")
599 elif "text/html" in accept:
tierno701018c2019-06-25 11:13:14 +0000600 return html.format(data, cherrypy.request, cherrypy.response, token_info)
tiernoc94c3df2018-02-09 15:38:54 +0100601
tiernof27c79b2018-03-12 17:08:42 +0100602 elif "application/yaml" in accept or "*/*" in accept or "text/plain" in accept:
tiernoc94c3df2018-02-09 15:38:54 +0100603 pass
tiernof717cbe2018-12-03 16:35:42 +0000604 # if there is not any valid accept, raise an error. But if response is already an error, format in yaml
605 elif cherrypy.response.status >= 400:
tiernoc94c3df2018-02-09 15:38:54 +0100606 raise cherrypy.HTTPError(HTTPStatus.NOT_ACCEPTABLE.value,
607 "Only 'Accept' of type 'application/json' or 'application/yaml' "
608 "for output format are available")
609 cherrypy.response.headers["Content-Type"] = 'application/yaml'
610 return yaml.safe_dump(data, explicit_start=True, indent=4, default_flow_style=False, tags=False,
611 encoding='utf-8', allow_unicode=True) # , canonical=True, default_style='"'
612
613 @cherrypy.expose
614 def index(self, *args, **kwargs):
tierno701018c2019-06-25 11:13:14 +0000615 token_info = None
tiernoc94c3df2018-02-09 15:38:54 +0100616 try:
617 if cherrypy.request.method == "GET":
tierno701018c2019-06-25 11:13:14 +0000618 token_info = self.authenticator.authorize()
delacruzramo01b15d32019-07-02 14:37:47 +0200619 outdata = token_info # Home page
tiernoc94c3df2018-02-09 15:38:54 +0100620 else:
621 raise cherrypy.HTTPError(HTTPStatus.METHOD_NOT_ALLOWED.value,
tierno2236d202018-05-16 19:05:16 +0200622 "Method {} not allowed for tokens".format(cherrypy.request.method))
tiernoc94c3df2018-02-09 15:38:54 +0100623
tierno701018c2019-06-25 11:13:14 +0000624 return self._format_out(outdata, token_info)
tiernoc94c3df2018-02-09 15:38:54 +0100625
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100626 except (EngineException, AuthException) as e:
tierno701018c2019-06-25 11:13:14 +0000627 # cherrypy.log("index Exception {}".format(e))
tiernoc94c3df2018-02-09 15:38:54 +0100628 cherrypy.response.status = e.http_code.value
tierno701018c2019-06-25 11:13:14 +0000629 return self._format_out("Welcome to OSM!", token_info)
tiernoc94c3df2018-02-09 15:38:54 +0100630
631 @cherrypy.expose
tierno55945e72018-04-06 16:40:27 +0200632 def version(self, *args, **kwargs):
tiernodfe09572018-04-24 10:41:10 +0200633 # TODO consider to remove and provide version using the static version file
tierno55945e72018-04-06 16:40:27 +0200634 try:
635 if cherrypy.request.method != "GET":
636 raise NbiException("Only method GET is allowed", HTTPStatus.METHOD_NOT_ALLOWED)
637 elif args or kwargs:
638 raise NbiException("Invalid URL or query string for version", HTTPStatus.METHOD_NOT_ALLOWED)
tierno9c630112019-08-29 14:21:41 +0000639 # TODO include version of other modules, pick up from some kafka admin message
tierno5792d7d2019-08-30 15:37:12 +0000640 osm_nbi_version = {"version": nbi_version, "date": nbi_version_date}
641 return self._format_out(osm_nbi_version)
tierno55945e72018-04-06 16:40:27 +0200642 except NbiException as e:
643 cherrypy.response.status = e.http_code.value
644 problem_details = {
645 "code": e.http_code.name,
646 "status": e.http_code.value,
647 "detail": str(e),
648 }
649 return self._format_out(problem_details, None)
650
tiernoa5035702019-07-29 08:54:42 +0000651 @staticmethod
652 def _format_login(token_info):
653 """
654 Changes cherrypy.request.login to include username/project_name;session so that cherrypy access log will
655 log this information
656 :param token_info: Dictionary with token content
657 :return: None
658 """
659 cherrypy.request.login = token_info.get("username", "-")
660 if token_info.get("project_name"):
661 cherrypy.request.login += "/" + token_info["project_name"]
662 if token_info.get("id"):
663 cherrypy.request.login += ";session=" + token_info["id"][0:12]
664
tierno55945e72018-04-06 16:40:27 +0200665 @cherrypy.expose
tiernof27c79b2018-03-12 17:08:42 +0100666 def token(self, method, token_id=None, kwargs=None):
tierno701018c2019-06-25 11:13:14 +0000667 token_info = None
tiernoc94c3df2018-02-09 15:38:54 +0100668 # self.engine.load_dbase(cherrypy.request.app.config)
tiernof27c79b2018-03-12 17:08:42 +0100669 indata = self._format_in(kwargs)
670 if not isinstance(indata, dict):
671 raise NbiException("Expected application/yaml or application/json Content-Type", HTTPStatus.BAD_REQUEST)
tiernoa5035702019-07-29 08:54:42 +0000672
673 if method == "GET":
674 token_info = self.authenticator.authorize()
675 # for logging
676 self._format_login(token_info)
677 if token_id:
678 outdata = self.authenticator.get_token(token_info, token_id)
tiernoc94c3df2018-02-09 15:38:54 +0100679 else:
tiernoa5035702019-07-29 08:54:42 +0000680 outdata = self.authenticator.get_token_list(token_info)
681 elif method == "POST":
682 try:
683 token_info = self.authenticator.authorize()
684 except Exception:
685 token_info = None
686 if kwargs:
687 indata.update(kwargs)
688 # This is needed to log the user when authentication fails
689 cherrypy.request.login = "{}".format(indata.get("username", "-"))
690 outdata = token_info = self.authenticator.new_token(token_info, indata, cherrypy.request.remote)
691 cherrypy.session['Authorization'] = outdata["_id"]
692 self._set_location_header("admin", "v1", "tokens", outdata["_id"])
693 # for logging
694 self._format_login(token_info)
695
696 # cherrypy.response.cookie["Authorization"] = outdata["id"]
697 # cherrypy.response.cookie["Authorization"]['expires'] = 3600
698 elif method == "DELETE":
699 if not token_id and "id" in kwargs:
700 token_id = kwargs["id"]
701 elif not token_id:
702 token_info = self.authenticator.authorize()
703 # for logging
704 self._format_login(token_info)
705 token_id = token_info["_id"]
706 outdata = self.authenticator.del_token(token_id)
707 token_info = None
708 cherrypy.session['Authorization'] = "logout"
709 # cherrypy.response.cookie["Authorization"] = token_id
710 # cherrypy.response.cookie["Authorization"]['expires'] = 0
711 else:
712 raise NbiException("Method {} not allowed for token".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
713 return self._format_out(outdata, token_info)
tiernoc94c3df2018-02-09 15:38:54 +0100714
715 @cherrypy.expose
716 def test(self, *args, **kwargs):
tierno4836bac2020-01-15 14:41:48 +0000717 if not cherrypy.config.get("server.enable_test") or (isinstance(cherrypy.config["server.enable_test"], str) and
718 cherrypy.config["server.enable_test"].lower() == "false"):
719 cherrypy.response.status = HTTPStatus.METHOD_NOT_ALLOWED.value
720 return "test URL is disabled"
tiernoc94c3df2018-02-09 15:38:54 +0100721 thread_info = None
tiernof27c79b2018-03-12 17:08:42 +0100722 if args and args[0] == "help":
tiernof717cbe2018-12-03 16:35:42 +0000723 return "<html><pre>\ninit\nfile/<name> download file\ndb-clear/table\nfs-clear[/folder]\nlogin\nlogin2\n"\
tiernoe1281182018-05-22 12:24:36 +0200724 "sleep/<time>\nmessage/topic\n</pre></html>"
tiernof27c79b2018-03-12 17:08:42 +0100725
726 elif args and args[0] == "init":
tiernoc94c3df2018-02-09 15:38:54 +0100727 try:
728 # self.engine.load_dbase(cherrypy.request.app.config)
729 self.engine.create_admin()
730 return "Done. User 'admin', password 'admin' created"
731 except Exception:
732 cherrypy.response.status = HTTPStatus.FORBIDDEN.value
733 return self._format_out("Database already initialized")
tiernof27c79b2018-03-12 17:08:42 +0100734 elif args and args[0] == "file":
735 return cherrypy.lib.static.serve_file(cherrypy.tree.apps['/osm'].config["storage"]["path"] + "/" + args[1],
736 "text/plain", "attachment")
737 elif args and args[0] == "file2":
738 f_path = cherrypy.tree.apps['/osm'].config["storage"]["path"] + "/" + args[1]
739 f = open(f_path, "r")
740 cherrypy.response.headers["Content-type"] = "text/plain"
tiernof27c79b2018-03-12 17:08:42 +0100741 return f
tierno55945e72018-04-06 16:40:27 +0200742
tiernof27c79b2018-03-12 17:08:42 +0100743 elif len(args) == 2 and args[0] == "db-clear":
tiernof717cbe2018-12-03 16:35:42 +0000744 deleted_info = self.engine.db.del_list(args[1], kwargs)
745 return "{} {} deleted\n".format(deleted_info["deleted"], args[1])
746 elif len(args) and args[0] == "fs-clear":
747 if len(args) >= 2:
748 folders = (args[1],)
749 else:
750 folders = self.engine.fs.dir_ls(".")
751 for folder in folders:
752 self.engine.fs.file_delete(folder)
753 return ",".join(folders) + " folders deleted\n"
tiernoc94c3df2018-02-09 15:38:54 +0100754 elif args and args[0] == "login":
755 if not cherrypy.request.headers.get("Authorization"):
756 cherrypy.response.headers["WWW-Authenticate"] = 'Basic realm="Access to OSM site", charset="UTF-8"'
757 cherrypy.response.status = HTTPStatus.UNAUTHORIZED.value
758 elif args and args[0] == "login2":
759 if not cherrypy.request.headers.get("Authorization"):
760 cherrypy.response.headers["WWW-Authenticate"] = 'Bearer realm="Access to OSM site"'
761 cherrypy.response.status = HTTPStatus.UNAUTHORIZED.value
762 elif args and args[0] == "sleep":
763 sleep_time = 5
764 try:
765 sleep_time = int(args[1])
766 except Exception:
767 cherrypy.response.status = HTTPStatus.FORBIDDEN.value
768 return self._format_out("Database already initialized")
769 thread_info = cherrypy.thread_data
770 print(thread_info)
771 time.sleep(sleep_time)
772 # thread_info
773 elif len(args) >= 2 and args[0] == "message":
tiernob24258a2018-10-04 18:39:49 +0200774 main_topic = args[1]
775 return_text = "<html><pre>{} ->\n".format(main_topic)
tiernoc94c3df2018-02-09 15:38:54 +0100776 try:
tierno55945e72018-04-06 16:40:27 +0200777 if cherrypy.request.method == 'POST':
delacruzramob19cadc2019-10-08 10:18:02 +0200778 to_send = yaml.load(cherrypy.request.body, Loader=yaml.SafeLoader)
tierno55945e72018-04-06 16:40:27 +0200779 for k, v in to_send.items():
tiernob24258a2018-10-04 18:39:49 +0200780 self.engine.msg.write(main_topic, k, v)
tierno55945e72018-04-06 16:40:27 +0200781 return_text += " {}: {}\n".format(k, v)
782 elif cherrypy.request.method == 'GET':
783 for k, v in kwargs.items():
delacruzramob19cadc2019-10-08 10:18:02 +0200784 self.engine.msg.write(main_topic, k, yaml.load(v), Loader=yaml.SafeLoader)
785 return_text += " {}: {}\n".format(k, yaml.load(v), Loader=yaml.SafeLoader)
tiernoc94c3df2018-02-09 15:38:54 +0100786 except Exception as e:
tierno55945e72018-04-06 16:40:27 +0200787 return_text += "Error: " + str(e)
788 return_text += "</pre></html>\n"
789 return return_text
tiernoc94c3df2018-02-09 15:38:54 +0100790
791 return_text = (
792 "<html><pre>\nheaders:\n args: {}\n".format(args) +
793 " kwargs: {}\n".format(kwargs) +
794 " headers: {}\n".format(cherrypy.request.headers) +
795 " path_info: {}\n".format(cherrypy.request.path_info) +
796 " query_string: {}\n".format(cherrypy.request.query_string) +
797 " session: {}\n".format(cherrypy.session) +
798 " cookie: {}\n".format(cherrypy.request.cookie) +
799 " method: {}\n".format(cherrypy.request.method) +
tiernof717cbe2018-12-03 16:35:42 +0000800 " session: {}\n".format(cherrypy.session.get('fieldname')) +
tiernoc94c3df2018-02-09 15:38:54 +0100801 " body:\n")
802 return_text += " length: {}\n".format(cherrypy.request.body.length)
803 if cherrypy.request.body.length:
804 return_text += " content: {}\n".format(
805 str(cherrypy.request.body.read(int(cherrypy.request.headers.get('Content-Length', 0)))))
806 if thread_info:
807 return_text += "thread: {}\n".format(thread_info)
808 return_text += "</pre></html>"
809 return return_text
810
tierno701018c2019-06-25 11:13:14 +0000811 @staticmethod
812 def _check_valid_url_method(method, *args):
tiernof27c79b2018-03-12 17:08:42 +0100813 if len(args) < 3:
tiernob24258a2018-10-04 18:39:49 +0200814 raise NbiException("URL must contain at least 'main_topic/version/topic'", HTTPStatus.METHOD_NOT_ALLOWED)
tiernof27c79b2018-03-12 17:08:42 +0100815
tierno701018c2019-06-25 11:13:14 +0000816 reference = valid_url_methods
tiernof27c79b2018-03-12 17:08:42 +0100817 for arg in args:
818 if arg is None:
819 break
820 if not isinstance(reference, dict):
821 raise NbiException("URL contains unexpected extra items '{}'".format(arg),
822 HTTPStatus.METHOD_NOT_ALLOWED)
823
824 if arg in reference:
825 reference = reference[arg]
826 elif "<ID>" in reference:
827 reference = reference["<ID>"]
828 elif "*" in reference:
829 reference = reference["*"]
830 break
831 else:
832 raise NbiException("Unexpected URL item {}".format(arg), HTTPStatus.METHOD_NOT_ALLOWED)
833 if "TODO" in reference and method in reference["TODO"]:
834 raise NbiException("Method {} not supported yet for this URL".format(method), HTTPStatus.NOT_IMPLEMENTED)
tierno2236d202018-05-16 19:05:16 +0200835 elif "METHODS" in reference and method not in reference["METHODS"]:
tiernof27c79b2018-03-12 17:08:42 +0100836 raise NbiException("Method {} not supported for this URL".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
tierno701018c2019-06-25 11:13:14 +0000837 return reference["ROLE_PERMISSION"] + method.lower()
tiernof27c79b2018-03-12 17:08:42 +0100838
839 @staticmethod
tiernob24258a2018-10-04 18:39:49 +0200840 def _set_location_header(main_topic, version, topic, id):
tiernof27c79b2018-03-12 17:08:42 +0100841 """
842 Insert response header Location with the URL of created item base on URL params
tiernob24258a2018-10-04 18:39:49 +0200843 :param main_topic:
tiernof27c79b2018-03-12 17:08:42 +0100844 :param version:
tiernob24258a2018-10-04 18:39:49 +0200845 :param topic:
tiernof27c79b2018-03-12 17:08:42 +0100846 :param id:
847 :return: None
848 """
849 # Use cherrypy.request.base for absoluted path and make use of request.header HOST just in case behind aNAT
tiernob24258a2018-10-04 18:39:49 +0200850 cherrypy.response.headers["Location"] = "/osm/{}/{}/{}/{}".format(main_topic, version, topic, id)
tiernof27c79b2018-03-12 17:08:42 +0100851 return
852
tierno65ca36d2019-02-12 19:27:52 +0100853 @staticmethod
tierno701018c2019-06-25 11:13:14 +0000854 def _extract_query_string_operations(kwargs, method):
855 """
856
857 :param kwargs:
858 :return:
859 """
860 query_string_operations = []
861 if kwargs:
862 for qs in ("FORCE", "PUBLIC", "ADMIN", "SET_PROJECT"):
863 if qs in kwargs and kwargs[qs].lower() != "false":
864 query_string_operations.append(qs.lower() + ":" + method.lower())
865 return query_string_operations
866
867 @staticmethod
868 def _manage_admin_query(token_info, kwargs, method, _id):
tierno65ca36d2019-02-12 19:27:52 +0100869 """
870 Processes the administrator query inputs (if any) of FORCE, ADMIN, PUBLIC, SET_PROJECT
871 Check that users has rights to use them and returs the admin_query
tierno701018c2019-06-25 11:13:14 +0000872 :param token_info: token_info rights obtained by token
tierno65ca36d2019-02-12 19:27:52 +0100873 :param kwargs: query string input.
874 :param method: http method: GET, POSST, PUT, ...
875 :param _id:
876 :return: admin_query dictionary with keys:
877 public: True, False or None
878 force: True or False
879 project_id: tuple with projects used for accessing an element
880 set_project: tuple with projects that a created element will belong to
881 method: show, list, delete, write
882 """
tierno701018c2019-06-25 11:13:14 +0000883 admin_query = {"force": False, "project_id": (token_info["project_id"], ), "username": token_info["username"],
delacruzramo029405d2019-09-26 10:52:56 +0200884 "admin": token_info["admin"], "public": None,
885 "allow_show_user_project_role": token_info["allow_show_user_project_role"]}
tierno65ca36d2019-02-12 19:27:52 +0100886 if kwargs:
887 # FORCE
888 if "FORCE" in kwargs:
889 if kwargs["FORCE"].lower() != "false": # if None or True set force to True
890 admin_query["force"] = True
891 del kwargs["FORCE"]
892 # PUBLIC
893 if "PUBLIC" in kwargs:
894 if kwargs["PUBLIC"].lower() != "false": # if None or True set public to True
895 admin_query["public"] = True
896 else:
897 admin_query["public"] = False
898 del kwargs["PUBLIC"]
899 # ADMIN
900 if "ADMIN" in kwargs:
901 behave_as = kwargs.pop("ADMIN")
902 if behave_as.lower() != "false":
tierno701018c2019-06-25 11:13:14 +0000903 if not token_info["admin"]:
tierno65ca36d2019-02-12 19:27:52 +0100904 raise NbiException("Only admin projects can use 'ADMIN' query string", HTTPStatus.UNAUTHORIZED)
905 if not behave_as or behave_as.lower() == "true": # convert True, None to empty list
906 admin_query["project_id"] = ()
907 elif isinstance(behave_as, (list, tuple)):
908 admin_query["project_id"] = behave_as
909 else: # isinstance(behave_as, str)
910 admin_query["project_id"] = (behave_as, )
911 if "SET_PROJECT" in kwargs:
912 set_project = kwargs.pop("SET_PROJECT")
913 if not set_project:
914 admin_query["set_project"] = list(admin_query["project_id"])
915 else:
916 if isinstance(set_project, str):
917 set_project = (set_project, )
918 if admin_query["project_id"]:
919 for p in set_project:
920 if p not in admin_query["project_id"]:
921 raise NbiException("Unauthorized for 'SET_PROJECT={p}'. Try with 'ADMIN=True' or "
922 "'ADMIN='{p}'".format(p=p), HTTPStatus.UNAUTHORIZED)
923 admin_query["set_project"] = set_project
924
925 # PROJECT_READ
926 # if "PROJECT_READ" in kwargs:
927 # admin_query["project"] = kwargs.pop("project")
tierno701018c2019-06-25 11:13:14 +0000928 # if admin_query["project"] == token_info["project_id"]:
tierno65ca36d2019-02-12 19:27:52 +0100929 if method == "GET":
930 if _id:
931 admin_query["method"] = "show"
932 else:
933 admin_query["method"] = "list"
934 elif method == "DELETE":
935 admin_query["method"] = "delete"
936 else:
937 admin_query["method"] = "write"
938 return admin_query
939
tiernoc94c3df2018-02-09 15:38:54 +0100940 @cherrypy.expose
tiernob24258a2018-10-04 18:39:49 +0200941 def default(self, main_topic=None, version=None, topic=None, _id=None, item=None, *args, **kwargs):
tierno701018c2019-06-25 11:13:14 +0000942 token_info = None
tiernof27c79b2018-03-12 17:08:42 +0100943 outdata = None
944 _format = None
tierno0f98af52018-03-19 10:28:22 +0100945 method = "DONE"
tiernob24258a2018-10-04 18:39:49 +0200946 engine_topic = None
tiernodb9dc582018-06-20 17:27:29 +0200947 rollback = []
tierno701018c2019-06-25 11:13:14 +0000948 engine_session = None
tiernoc94c3df2018-02-09 15:38:54 +0100949 try:
tiernob24258a2018-10-04 18:39:49 +0200950 if not main_topic or not version or not topic:
951 raise NbiException("URL must contain at least 'main_topic/version/topic'",
952 HTTPStatus.METHOD_NOT_ALLOWED)
vijay.r35ef2f72019-04-30 17:55:49 +0530953 if main_topic not in ("admin", "vnfpkgm", "nsd", "nslcm", "pdu", "nst", "nsilcm", "nspm"):
tiernob24258a2018-10-04 18:39:49 +0200954 raise NbiException("URL main_topic '{}' not supported".format(main_topic),
955 HTTPStatus.METHOD_NOT_ALLOWED)
tiernoc94c3df2018-02-09 15:38:54 +0100956 if version != 'v1':
957 raise NbiException("URL version '{}' not supported".format(version), HTTPStatus.METHOD_NOT_ALLOWED)
958
tiernof27c79b2018-03-12 17:08:42 +0100959 if kwargs and "METHOD" in kwargs and kwargs["METHOD"] in ("PUT", "POST", "DELETE", "GET", "PATCH"):
960 method = kwargs.pop("METHOD")
961 else:
962 method = cherrypy.request.method
tierno65ca36d2019-02-12 19:27:52 +0100963
tierno701018c2019-06-25 11:13:14 +0000964 role_permission = self._check_valid_url_method(method, main_topic, version, topic, _id, item, *args)
965 query_string_operations = self._extract_query_string_operations(kwargs, method)
tiernob24258a2018-10-04 18:39:49 +0200966 if main_topic == "admin" and topic == "tokens":
tiernof27c79b2018-03-12 17:08:42 +0100967 return self.token(method, _id, kwargs)
delacruzramo029405d2019-09-26 10:52:56 +0200968 token_info = self.authenticator.authorize(role_permission, query_string_operations, _id)
tierno701018c2019-06-25 11:13:14 +0000969 engine_session = self._manage_admin_query(token_info, kwargs, method, _id)
tiernof27c79b2018-03-12 17:08:42 +0100970 indata = self._format_in(kwargs)
tiernob24258a2018-10-04 18:39:49 +0200971 engine_topic = topic
972 if topic == "subscriptions":
973 engine_topic = main_topic + "_" + topic
vijay.r35ef2f72019-04-30 17:55:49 +0530974 if item and topic != "pm_jobs":
tiernob24258a2018-10-04 18:39:49 +0200975 engine_topic = item
tiernoc94c3df2018-02-09 15:38:54 +0100976
tiernob24258a2018-10-04 18:39:49 +0200977 if main_topic == "nsd":
978 engine_topic = "nsds"
979 elif main_topic == "vnfpkgm":
980 engine_topic = "vnfds"
981 elif main_topic == "nslcm":
982 engine_topic = "nsrs"
983 if topic == "ns_lcm_op_occs":
984 engine_topic = "nslcmops"
985 if topic == "vnfrs" or topic == "vnf_instances":
986 engine_topic = "vnfrs"
garciadeblas9750c5a2018-10-15 16:20:35 +0200987 elif main_topic == "nst":
988 engine_topic = "nsts"
989 elif main_topic == "nsilcm":
990 engine_topic = "nsis"
991 if topic == "nsi_lcm_op_occs":
delacruzramoc061f562019-04-05 11:00:02 +0200992 engine_topic = "nsilcmops"
tiernob24258a2018-10-04 18:39:49 +0200993 elif main_topic == "pdu":
994 engine_topic = "pdus"
tierno65ca36d2019-02-12 19:27:52 +0100995 if engine_topic == "vims": # TODO this is for backward compatibility, it will be removed in the future
tiernob24258a2018-10-04 18:39:49 +0200996 engine_topic = "vim_accounts"
tiernoc94c3df2018-02-09 15:38:54 +0100997
998 if method == "GET":
Felipe Vicens07f31722018-10-29 15:16:44 +0100999 if item in ("nsd_content", "package_content", "artifacts", "vnfd", "nsd", "nst", "nst_content"):
garciadeblas9750c5a2018-10-15 16:20:35 +02001000 if item in ("vnfd", "nsd", "nst"):
tiernof27c79b2018-03-12 17:08:42 +01001001 path = "$DESCRIPTOR"
1002 elif args:
1003 path = args
tiernob24258a2018-10-04 18:39:49 +02001004 elif item == "artifacts":
tiernof27c79b2018-03-12 17:08:42 +01001005 path = ()
1006 else:
1007 path = None
tierno701018c2019-06-25 11:13:14 +00001008 file, _format = self.engine.get_file(engine_session, engine_topic, _id, path,
tierno2236d202018-05-16 19:05:16 +02001009 cherrypy.request.headers.get("Accept"))
tiernof27c79b2018-03-12 17:08:42 +01001010 outdata = file
1011 elif not _id:
tierno701018c2019-06-25 11:13:14 +00001012 outdata = self.engine.get_item_list(engine_session, engine_topic, kwargs)
tiernoc94c3df2018-02-09 15:38:54 +01001013 else:
vijay.r35ef2f72019-04-30 17:55:49 +05301014 if item == "reports":
1015 # TODO check that project_id (_id in this context) has permissions
1016 _id = args[0]
tierno701018c2019-06-25 11:13:14 +00001017 outdata = self.engine.get_item(engine_session, engine_topic, _id)
tiernof27c79b2018-03-12 17:08:42 +01001018 elif method == "POST":
tiernobdebce92019-07-01 15:36:49 +00001019 cherrypy.response.status = HTTPStatus.CREATED.value
garciadeblas9750c5a2018-10-15 16:20:35 +02001020 if topic in ("ns_descriptors_content", "vnf_packages_content", "netslice_templates_content"):
tiernof27c79b2018-03-12 17:08:42 +01001021 _id = cherrypy.request.headers.get("Transaction-Id")
1022 if not _id:
tiernobdebce92019-07-01 15:36:49 +00001023 _id, _ = self.engine.new_item(rollback, engine_session, engine_topic, {}, None,
1024 cherrypy.request.headers)
tierno701018c2019-06-25 11:13:14 +00001025 completed = self.engine.upload_content(engine_session, engine_topic, _id, indata, kwargs,
tierno65ca36d2019-02-12 19:27:52 +01001026 cherrypy.request.headers)
tiernof27c79b2018-03-12 17:08:42 +01001027 if completed:
tiernob24258a2018-10-04 18:39:49 +02001028 self._set_location_header(main_topic, version, topic, _id)
tiernof27c79b2018-03-12 17:08:42 +01001029 else:
1030 cherrypy.response.headers["Transaction-Id"] = _id
1031 outdata = {"id": _id}
tiernob24258a2018-10-04 18:39:49 +02001032 elif topic == "ns_instances_content":
1033 # creates NSR
tiernobdebce92019-07-01 15:36:49 +00001034 _id, _ = self.engine.new_item(rollback, engine_session, engine_topic, indata, kwargs)
tiernob24258a2018-10-04 18:39:49 +02001035 # creates nslcmop
1036 indata["lcmOperationType"] = "instantiate"
1037 indata["nsInstanceId"] = _id
tiernobdebce92019-07-01 15:36:49 +00001038 nslcmop_id, _ = self.engine.new_item(rollback, engine_session, "nslcmops", indata, None)
tiernob24258a2018-10-04 18:39:49 +02001039 self._set_location_header(main_topic, version, topic, _id)
kuuse078f55e2019-05-16 19:24:21 +02001040 outdata = {"id": _id, "nslcmop_id": nslcmop_id}
tiernob24258a2018-10-04 18:39:49 +02001041 elif topic == "ns_instances" and item:
1042 indata["lcmOperationType"] = item
1043 indata["nsInstanceId"] = _id
tiernobdebce92019-07-01 15:36:49 +00001044 _id, _ = self.engine.new_item(rollback, engine_session, "nslcmops", indata, kwargs)
tiernob24258a2018-10-04 18:39:49 +02001045 self._set_location_header(main_topic, version, "ns_lcm_op_occs", _id)
tierno65acb4d2018-04-06 16:42:40 +02001046 outdata = {"id": _id}
1047 cherrypy.response.status = HTTPStatus.ACCEPTED.value
garciadeblas9750c5a2018-10-15 16:20:35 +02001048 elif topic == "netslice_instances_content":
Felipe Vicens07f31722018-10-29 15:16:44 +01001049 # creates NetSlice_Instance_record (NSIR)
tiernobdebce92019-07-01 15:36:49 +00001050 _id, _ = self.engine.new_item(rollback, engine_session, engine_topic, indata, kwargs)
Felipe Vicens07f31722018-10-29 15:16:44 +01001051 self._set_location_header(main_topic, version, topic, _id)
garciadeblas9750c5a2018-10-15 16:20:35 +02001052 indata["lcmOperationType"] = "instantiate"
Felipe Vicens126af572019-06-05 19:13:04 +02001053 indata["netsliceInstanceId"] = _id
tiernobdebce92019-07-01 15:36:49 +00001054 nsilcmop_id, _ = self.engine.new_item(rollback, engine_session, "nsilcmops", indata, kwargs)
kuuse078f55e2019-05-16 19:24:21 +02001055 outdata = {"id": _id, "nsilcmop_id": nsilcmop_id}
delacruzramoc061f562019-04-05 11:00:02 +02001056
garciadeblas9750c5a2018-10-15 16:20:35 +02001057 elif topic == "netslice_instances" and item:
1058 indata["lcmOperationType"] = item
Felipe Vicens126af572019-06-05 19:13:04 +02001059 indata["netsliceInstanceId"] = _id
tiernobdebce92019-07-01 15:36:49 +00001060 _id, _ = self.engine.new_item(rollback, engine_session, "nsilcmops", indata, kwargs)
garciadeblas9750c5a2018-10-15 16:20:35 +02001061 self._set_location_header(main_topic, version, "nsi_lcm_op_occs", _id)
1062 outdata = {"id": _id}
1063 cherrypy.response.status = HTTPStatus.ACCEPTED.value
tiernof27c79b2018-03-12 17:08:42 +01001064 else:
tiernobdebce92019-07-01 15:36:49 +00001065 _id, op_id = self.engine.new_item(rollback, engine_session, engine_topic, indata, kwargs,
1066 cherrypy.request.headers)
tiernob24258a2018-10-04 18:39:49 +02001067 self._set_location_header(main_topic, version, topic, _id)
tiernof27c79b2018-03-12 17:08:42 +01001068 outdata = {"id": _id}
tiernobdebce92019-07-01 15:36:49 +00001069 if op_id:
1070 outdata["op_id"] = op_id
1071 cherrypy.response.status = HTTPStatus.ACCEPTED.value
tiernob24258a2018-10-04 18:39:49 +02001072 # TODO form NsdInfo when topic in ("ns_descriptors", "vnf_packages")
tierno09c073e2018-04-26 13:36:48 +02001073
tiernoc94c3df2018-02-09 15:38:54 +01001074 elif method == "DELETE":
1075 if not _id:
tierno701018c2019-06-25 11:13:14 +00001076 outdata = self.engine.del_item_list(engine_session, engine_topic, kwargs)
tierno09c073e2018-04-26 13:36:48 +02001077 cherrypy.response.status = HTTPStatus.OK.value
tiernoc94c3df2018-02-09 15:38:54 +01001078 else: # len(args) > 1
tiernoe8631782018-12-21 13:31:52 +00001079 delete_in_process = False
tierno701018c2019-06-25 11:13:14 +00001080 if topic == "ns_instances_content" and not engine_session["force"]:
tiernob24258a2018-10-04 18:39:49 +02001081 nslcmop_desc = {
1082 "lcmOperationType": "terminate",
1083 "nsInstanceId": _id,
1084 "autoremove": True
1085 }
tiernobdebce92019-07-01 15:36:49 +00001086 opp_id, _ = self.engine.new_item(rollback, engine_session, "nslcmops", nslcmop_desc, None)
tiernoe8631782018-12-21 13:31:52 +00001087 if opp_id:
1088 delete_in_process = True
1089 outdata = {"_id": opp_id}
1090 cherrypy.response.status = HTTPStatus.ACCEPTED.value
tierno701018c2019-06-25 11:13:14 +00001091 elif topic == "netslice_instances_content" and not engine_session["force"]:
garciadeblas9750c5a2018-10-15 16:20:35 +02001092 nsilcmop_desc = {
1093 "lcmOperationType": "terminate",
Felipe Vicens126af572019-06-05 19:13:04 +02001094 "netsliceInstanceId": _id,
garciadeblas9750c5a2018-10-15 16:20:35 +02001095 "autoremove": True
1096 }
tiernobdebce92019-07-01 15:36:49 +00001097 opp_id, _ = self.engine.new_item(rollback, engine_session, "nsilcmops", nsilcmop_desc, None)
tiernoe8631782018-12-21 13:31:52 +00001098 if opp_id:
1099 delete_in_process = True
1100 outdata = {"_id": opp_id}
1101 cherrypy.response.status = HTTPStatus.ACCEPTED.value
1102 if not delete_in_process:
tierno701018c2019-06-25 11:13:14 +00001103 self.engine.del_item(engine_session, engine_topic, _id)
tierno09c073e2018-04-26 13:36:48 +02001104 cherrypy.response.status = HTTPStatus.NO_CONTENT.value
delacruzramofe598fe2019-10-23 18:25:11 +02001105 if engine_topic in ("vim_accounts", "wim_accounts", "sdns", "k8sclusters", "k8srepos"):
tierno09c073e2018-04-26 13:36:48 +02001106 cherrypy.response.status = HTTPStatus.ACCEPTED.value
1107
tierno7ae10112018-05-18 14:36:02 +02001108 elif method in ("PUT", "PATCH"):
tiernobdebce92019-07-01 15:36:49 +00001109 op_id = None
tierno701018c2019-06-25 11:13:14 +00001110 if not indata and not kwargs and not engine_session.get("set_project"):
tiernoc94c3df2018-02-09 15:38:54 +01001111 raise NbiException("Nothing to update. Provide payload and/or query string",
1112 HTTPStatus.BAD_REQUEST)
garciadeblas9750c5a2018-10-15 16:20:35 +02001113 if item in ("nsd_content", "package_content", "nst_content") and method == "PUT":
tierno701018c2019-06-25 11:13:14 +00001114 completed = self.engine.upload_content(engine_session, engine_topic, _id, indata, kwargs,
tierno65ca36d2019-02-12 19:27:52 +01001115 cherrypy.request.headers)
tiernof27c79b2018-03-12 17:08:42 +01001116 if not completed:
1117 cherrypy.response.headers["Transaction-Id"] = id
tiernof27c79b2018-03-12 17:08:42 +01001118 else:
tiernobdebce92019-07-01 15:36:49 +00001119 op_id = self.engine.edit_item(engine_session, engine_topic, _id, indata, kwargs)
1120
1121 if op_id:
1122 cherrypy.response.status = HTTPStatus.ACCEPTED.value
1123 outdata = {"op_id": op_id}
1124 else:
1125 cherrypy.response.status = HTTPStatus.NO_CONTENT.value
1126 outdata = None
tiernoc94c3df2018-02-09 15:38:54 +01001127 else:
1128 raise NbiException("Method {} not allowed".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
tiernoa6bb45d2019-06-14 09:45:39 +00001129
1130 # if Role information changes, it is needed to reload the information of roles
1131 if topic == "roles" and method != "GET":
1132 self.authenticator.load_operation_to_allowed_roles()
delacruzramoad682a52019-12-10 16:26:34 +01001133
1134 if topic == "projects" and method == "DELETE" \
1135 or topic in ["users", "roles"] and method in ["PUT", "PATCH", "DELETE"]:
1136 self.authenticator.remove_token_from_cache()
1137
tierno701018c2019-06-25 11:13:14 +00001138 return self._format_out(outdata, token_info, _format)
tiernob24258a2018-10-04 18:39:49 +02001139 except Exception as e:
tierno36ec8602018-11-02 17:27:11 +01001140 if isinstance(e, (NbiException, EngineException, DbException, FsException, MsgException, AuthException,
delacruzramo01b15d32019-07-02 14:37:47 +02001141 ValidationError, AuthconnException)):
tiernob24258a2018-10-04 18:39:49 +02001142 http_code_value = cherrypy.response.status = e.http_code.value
1143 http_code_name = e.http_code.name
1144 cherrypy.log("Exception {}".format(e))
1145 else:
1146 http_code_value = cherrypy.response.status = HTTPStatus.BAD_REQUEST.value # INTERNAL_SERVER_ERROR
Felipe Vicense36ab852018-11-23 14:12:09 +01001147 cherrypy.log("CRITICAL: Exception {}".format(e), traceback=True)
tiernob24258a2018-10-04 18:39:49 +02001148 http_code_name = HTTPStatus.BAD_REQUEST.name
tierno3ace63c2018-05-03 17:51:43 +02001149 if hasattr(outdata, "close"): # is an open file
1150 outdata.close()
tiernob24258a2018-10-04 18:39:49 +02001151 error_text = str(e)
1152 rollback.reverse()
tiernodb9dc582018-06-20 17:27:29 +02001153 for rollback_item in rollback:
tierno3ace63c2018-05-03 17:51:43 +02001154 try:
tiernocc103432018-10-19 14:10:35 +02001155 if rollback_item.get("operation") == "set":
1156 self.engine.db.set_one(rollback_item["topic"], {"_id": rollback_item["_id"]},
1157 rollback_item["content"], fail_on_empty=False)
1158 else:
tiernoe8631782018-12-21 13:31:52 +00001159 self.engine.db.del_one(rollback_item["topic"], {"_id": rollback_item["_id"]},
1160 fail_on_empty=False)
tierno3ace63c2018-05-03 17:51:43 +02001161 except Exception as e2:
tiernob24258a2018-10-04 18:39:49 +02001162 rollback_error_text = "Rollback Exception {}: {}".format(rollback_item, e2)
1163 cherrypy.log(rollback_error_text)
1164 error_text += ". " + rollback_error_text
1165 # if isinstance(e, MsgException):
1166 # error_text = "{} has been '{}' but other modules cannot be informed because an error on bus".format(
1167 # engine_topic[:-1], method, error_text)
tiernoc94c3df2018-02-09 15:38:54 +01001168 problem_details = {
tiernob24258a2018-10-04 18:39:49 +02001169 "code": http_code_name,
1170 "status": http_code_value,
1171 "detail": error_text,
tiernoc94c3df2018-02-09 15:38:54 +01001172 }
tierno701018c2019-06-25 11:13:14 +00001173 return self._format_out(problem_details, token_info)
tiernoc94c3df2018-02-09 15:38:54 +01001174 # raise cherrypy.HTTPError(e.http_code.value, str(e))
tiernoa5035702019-07-29 08:54:42 +00001175 finally:
1176 if token_info:
1177 self._format_login(token_info)
1178 if method in ("PUT", "PATCH", "POST") and isinstance(outdata, dict):
1179 for logging_id in ("id", "op_id", "nsilcmop_id", "nslcmop_id"):
1180 if outdata.get(logging_id):
1181 cherrypy.request.login += ";{}={}".format(logging_id, outdata[logging_id][:36])
tiernoc94c3df2018-02-09 15:38:54 +01001182
1183
tiernoc94c3df2018-02-09 15:38:54 +01001184def _start_service():
1185 """
1186 Callback function called when cherrypy.engine starts
1187 Override configuration with env variables
1188 Set database, storage, message configuration
1189 Init database with admin/admin user password
1190 """
tierno932499c2019-01-28 17:28:10 +00001191 global nbi_server
1192 global subscription_thread
tiernoc94c3df2018-02-09 15:38:54 +01001193 cherrypy.log.error("Starting osm_nbi")
1194 # update general cherrypy configuration
1195 update_dict = {}
1196
1197 engine_config = cherrypy.tree.apps['/osm'].config
1198 for k, v in environ.items():
1199 if not k.startswith("OSMNBI_"):
1200 continue
tiernoe1281182018-05-22 12:24:36 +02001201 k1, _, k2 = k[7:].lower().partition("_")
tiernoc94c3df2018-02-09 15:38:54 +01001202 if not k2:
1203 continue
1204 try:
1205 # update static configuration
1206 if k == 'OSMNBI_STATIC_DIR':
1207 engine_config["/static"]['tools.staticdir.dir'] = v
1208 engine_config["/static"]['tools.staticdir.on'] = True
1209 elif k == 'OSMNBI_SOCKET_PORT' or k == 'OSMNBI_SERVER_PORT':
1210 update_dict['server.socket_port'] = int(v)
1211 elif k == 'OSMNBI_SOCKET_HOST' or k == 'OSMNBI_SERVER_HOST':
1212 update_dict['server.socket_host'] = v
tiernof5298be2018-05-16 14:43:57 +02001213 elif k1 in ("server", "test", "auth", "log"):
1214 update_dict[k1 + '.' + k2] = v
Eduardo Sousa819d34c2018-07-31 01:20:02 +01001215 elif k1 in ("message", "database", "storage", "authentication"):
tiernof5298be2018-05-16 14:43:57 +02001216 # k2 = k2.replace('_', '.')
Eduardo Sousa819d34c2018-07-31 01:20:02 +01001217 if k2 in ("port", "db_port"):
tiernoc94c3df2018-02-09 15:38:54 +01001218 engine_config[k1][k2] = int(v)
1219 else:
1220 engine_config[k1][k2] = v
Eduardo Sousa819d34c2018-07-31 01:20:02 +01001221
tiernoc94c3df2018-02-09 15:38:54 +01001222 except ValueError as e:
1223 cherrypy.log.error("Ignoring environ '{}': " + str(e))
1224 except Exception as e:
1225 cherrypy.log.warn("skipping environ '{}' on exception '{}'".format(k, e))
1226
1227 if update_dict:
1228 cherrypy.config.update(update_dict)
tiernof5298be2018-05-16 14:43:57 +02001229 engine_config["global"].update(update_dict)
tiernoc94c3df2018-02-09 15:38:54 +01001230
1231 # logging cherrypy
1232 log_format_simple = "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(message)s"
1233 log_formatter_simple = logging.Formatter(log_format_simple, datefmt='%Y-%m-%dT%H:%M:%S')
1234 logger_server = logging.getLogger("cherrypy.error")
1235 logger_access = logging.getLogger("cherrypy.access")
1236 logger_cherry = logging.getLogger("cherrypy")
1237 logger_nbi = logging.getLogger("nbi")
1238
tiernof5298be2018-05-16 14:43:57 +02001239 if "log.file" in engine_config["global"]:
1240 file_handler = logging.handlers.RotatingFileHandler(engine_config["global"]["log.file"],
tiernoc94c3df2018-02-09 15:38:54 +01001241 maxBytes=100e6, backupCount=9, delay=0)
1242 file_handler.setFormatter(log_formatter_simple)
1243 logger_cherry.addHandler(file_handler)
1244 logger_nbi.addHandler(file_handler)
tiernob24258a2018-10-04 18:39:49 +02001245 # log always to standard output
1246 for format_, logger in {"nbi.server %(filename)s:%(lineno)s": logger_server,
1247 "nbi.access %(filename)s:%(lineno)s": logger_access,
1248 "%(name)s %(filename)s:%(lineno)s": logger_nbi
1249 }.items():
1250 log_format_cherry = "%(asctime)s %(levelname)s {} %(message)s".format(format_)
1251 log_formatter_cherry = logging.Formatter(log_format_cherry, datefmt='%Y-%m-%dT%H:%M:%S')
1252 str_handler = logging.StreamHandler()
1253 str_handler.setFormatter(log_formatter_cherry)
1254 logger.addHandler(str_handler)
tiernoc94c3df2018-02-09 15:38:54 +01001255
tiernof5298be2018-05-16 14:43:57 +02001256 if engine_config["global"].get("log.level"):
1257 logger_cherry.setLevel(engine_config["global"]["log.level"])
1258 logger_nbi.setLevel(engine_config["global"]["log.level"])
tiernoc94c3df2018-02-09 15:38:54 +01001259
1260 # logging other modules
1261 for k1, logname in {"message": "nbi.msg", "database": "nbi.db", "storage": "nbi.fs"}.items():
1262 engine_config[k1]["logger_name"] = logname
1263 logger_module = logging.getLogger(logname)
1264 if "logfile" in engine_config[k1]:
1265 file_handler = logging.handlers.RotatingFileHandler(engine_config[k1]["logfile"],
tierno2236d202018-05-16 19:05:16 +02001266 maxBytes=100e6, backupCount=9, delay=0)
tiernoc94c3df2018-02-09 15:38:54 +01001267 file_handler.setFormatter(log_formatter_simple)
1268 logger_module.addHandler(file_handler)
1269 if "loglevel" in engine_config[k1]:
1270 logger_module.setLevel(engine_config[k1]["loglevel"])
1271 # TODO add more entries, e.g.: storage
1272 cherrypy.tree.apps['/osm'].root.engine.start(engine_config)
Eduardo Sousa819d34c2018-07-31 01:20:02 +01001273 cherrypy.tree.apps['/osm'].root.authenticator.start(engine_config)
tiernod985a8d2018-10-19 14:12:28 +02001274 cherrypy.tree.apps['/osm'].root.engine.init_db(target_version=database_version)
1275 cherrypy.tree.apps['/osm'].root.authenticator.init_db(target_version=auth_database_version)
tiernobee508e2019-01-21 11:21:49 +00001276
tierno932499c2019-01-28 17:28:10 +00001277 # start subscriptions thread:
1278 subscription_thread = SubscriptionThread(config=engine_config, engine=nbi_server.engine)
1279 subscription_thread.start()
1280 # Do not capture except SubscriptionException
1281
tiernob2e48bd2020-02-04 15:47:18 +00001282 backend = engine_config["authentication"]["backend"]
1283 cherrypy.log.error("Starting OSM NBI Version '{} {}' with '{}' authentication backend"
1284 .format(nbi_version, nbi_version_date, backend))
tiernoc94c3df2018-02-09 15:38:54 +01001285
1286
1287def _stop_service():
1288 """
1289 Callback function called when cherrypy.engine stops
1290 TODO: Ending database connections.
1291 """
tierno932499c2019-01-28 17:28:10 +00001292 global subscription_thread
tierno65ca36d2019-02-12 19:27:52 +01001293 if subscription_thread:
1294 subscription_thread.terminate()
tierno932499c2019-01-28 17:28:10 +00001295 subscription_thread = None
tiernoc94c3df2018-02-09 15:38:54 +01001296 cherrypy.tree.apps['/osm'].root.engine.stop()
1297 cherrypy.log.error("Stopping osm_nbi")
1298
tierno2236d202018-05-16 19:05:16 +02001299
tiernof5298be2018-05-16 14:43:57 +02001300def nbi(config_file):
tierno932499c2019-01-28 17:28:10 +00001301 global nbi_server
tiernoc94c3df2018-02-09 15:38:54 +01001302 # conf = {
1303 # '/': {
1304 # #'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
1305 # 'tools.sessions.on': True,
1306 # 'tools.response_headers.on': True,
1307 # # 'tools.response_headers.headers': [('Content-Type', 'text/plain')],
1308 # }
1309 # }
1310 # cherrypy.Server.ssl_module = 'builtin'
1311 # cherrypy.Server.ssl_certificate = "http/cert.pem"
1312 # cherrypy.Server.ssl_private_key = "http/privkey.pem"
1313 # cherrypy.Server.thread_pool = 10
1314 # cherrypy.config.update({'Server.socket_port': config["port"], 'Server.socket_host': config["host"]})
1315
1316 # cherrypy.config.update({'tools.auth_basic.on': True,
1317 # 'tools.auth_basic.realm': 'localhost',
1318 # 'tools.auth_basic.checkpassword': validate_password})
tierno932499c2019-01-28 17:28:10 +00001319 nbi_server = Server()
tiernoc94c3df2018-02-09 15:38:54 +01001320 cherrypy.engine.subscribe('start', _start_service)
1321 cherrypy.engine.subscribe('stop', _stop_service)
tierno932499c2019-01-28 17:28:10 +00001322 cherrypy.quickstart(nbi_server, '/osm', config_file)
tiernof5298be2018-05-16 14:43:57 +02001323
1324
1325def usage():
1326 print("""Usage: {} [options]
1327 -c|--config [configuration_file]: loads the configuration file (default: ./nbi.cfg)
1328 -h|--help: shows this help
1329 """.format(sys.argv[0]))
tierno2236d202018-05-16 19:05:16 +02001330 # --log-socket-host HOST: send logs to this host")
1331 # --log-socket-port PORT: send logs using this port (default: 9022)")
tiernoc94c3df2018-02-09 15:38:54 +01001332
1333
1334if __name__ == '__main__':
tiernof5298be2018-05-16 14:43:57 +02001335 try:
1336 # load parameters and configuration
1337 opts, args = getopt.getopt(sys.argv[1:], "hvc:", ["config=", "help"])
1338 # TODO add "log-socket-host=", "log-socket-port=", "log-file="
1339 config_file = None
1340 for o, a in opts:
1341 if o in ("-h", "--help"):
1342 usage()
1343 sys.exit()
1344 elif o in ("-c", "--config"):
1345 config_file = a
1346 # elif o == "--log-socket-port":
1347 # log_socket_port = a
1348 # elif o == "--log-socket-host":
1349 # log_socket_host = a
1350 # elif o == "--log-file":
1351 # log_file = a
1352 else:
1353 assert False, "Unhandled option"
1354 if config_file:
1355 if not path.isfile(config_file):
1356 print("configuration file '{}' that not exist".format(config_file), file=sys.stderr)
1357 exit(1)
1358 else:
1359 for config_file in (__file__[:__file__.rfind(".")] + ".cfg", "./nbi.cfg", "/etc/osm/nbi.cfg"):
1360 if path.isfile(config_file):
1361 break
1362 else:
1363 print("No configuration file 'nbi.cfg' found neither at local folder nor at /etc/osm/", file=sys.stderr)
1364 exit(1)
1365 nbi(config_file)
1366 except getopt.GetoptError as e:
1367 print(str(e), file=sys.stderr)
1368 # usage()
1369 exit(1)