blob: 5b165442075acd8ea79e41184eb5d0d15ea9e29d [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
tierno7fd24872019-12-10 10:57:17 +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
tierno7fd24872019-12-10 10:57:17 +000049nbi_version = _nbi_version # by default this is fixed in the code
tierno932499c2019-01-28 17:28:10 +000050
tiernoc94c3df2018-02-09 15:38:54 +010051
52"""
tiernof27c79b2018-03-12 17:08:42 +010053North Bound Interface (O: OSM specific; 5,X: SOL005 not implemented yet; O5: SOL005 implemented)
tiernoc94c3df2018-02-09 15:38:54 +010054URL: /osm GET POST PUT DELETE PATCH
garciadeblas9750c5a2018-10-15 16:20:35 +020055 /nsd/v1
tierno2236d202018-05-16 19:05:16 +020056 /ns_descriptors_content O O
57 /<nsdInfoId> O O O O
tiernoc94c3df2018-02-09 15:38:54 +010058 /ns_descriptors O5 O5
59 /<nsdInfoId> O5 O5 5
60 /nsd_content O5 O5
tiernof27c79b2018-03-12 17:08:42 +010061 /nsd O
62 /artifacts[/<artifactPath>] O
tiernoc94c3df2018-02-09 15:38:54 +010063 /pnf_descriptors 5 5
64 /<pnfdInfoId> 5 5 5
65 /pnfd_content 5 5
tiernof27c79b2018-03-12 17:08:42 +010066 /subscriptions 5 5
67 /<subscriptionId> 5 X
tiernoc94c3df2018-02-09 15:38:54 +010068
69 /vnfpkgm/v1
tierno55945e72018-04-06 16:40:27 +020070 /vnf_packages_content O O
tierno2236d202018-05-16 19:05:16 +020071 /<vnfPkgId> O O
tiernoc94c3df2018-02-09 15:38:54 +010072 /vnf_packages O5 O5
73 /<vnfPkgId> O5 O5 5
tiernoc94c3df2018-02-09 15:38:54 +010074 /package_content O5 O5
75 /upload_from_uri X
tiernof27c79b2018-03-12 17:08:42 +010076 /vnfd O5
77 /artifacts[/<artifactPath>] O5
78 /subscriptions X X
79 /<subscriptionId> X X
tiernoc94c3df2018-02-09 15:38:54 +010080
81 /nslcm/v1
tiernof27c79b2018-03-12 17:08:42 +010082 /ns_instances_content O O
tierno2236d202018-05-16 19:05:16 +020083 /<nsInstanceId> O O
tiernof27c79b2018-03-12 17:08:42 +010084 /ns_instances 5 5
tierno95692442018-05-24 18:05:28 +020085 /<nsInstanceId> O5 O5
tierno65acb4d2018-04-06 16:42:40 +020086 instantiate O5
87 terminate O5
88 action O
89 scale O5
90 heal 5
tiernoc94c3df2018-02-09 15:38:54 +010091 /ns_lcm_op_occs 5 5
92 /<nsLcmOpOccId> 5 5 5
93 TO BE COMPLETED 5 5
tiernof759d822018-06-11 18:54:54 +020094 /vnf_instances (also vnfrs for compatibility) O
95 /<vnfInstanceId> O
tiernof27c79b2018-03-12 17:08:42 +010096 /subscriptions 5 5
97 /<subscriptionId> 5 X
garciadeblas9750c5a2018-10-15 16:20:35 +020098
tiernocb83c942018-09-24 17:28:13 +020099 /pdu/v1
tierno032916c2019-03-22 13:27:12 +0000100 /pdu_descriptors O O
tiernocb83c942018-09-24 17:28:13 +0200101 /<id> O O O O
garciadeblas9750c5a2018-10-15 16:20:35 +0200102
tiernof27c79b2018-03-12 17:08:42 +0100103 /admin/v1
104 /tokens O O
tierno2236d202018-05-16 19:05:16 +0200105 /<id> O O
tiernof27c79b2018-03-12 17:08:42 +0100106 /users O O
tiernocd54a4a2018-09-12 16:40:35 +0200107 /<id> O O O O
tiernof27c79b2018-03-12 17:08:42 +0100108 /projects O O
tierno2236d202018-05-16 19:05:16 +0200109 /<id> O O
tierno55ba2e62018-12-11 17:22:22 +0000110 /vim_accounts (also vims for compatibility) O O
111 /<id> O O O
112 /wim_accounts O O
tierno2236d202018-05-16 19:05:16 +0200113 /<id> O O O
tierno0f98af52018-03-19 10:28:22 +0100114 /sdns O O
tierno2236d202018-05-16 19:05:16 +0200115 /<id> O O O
delacruzramofe598fe2019-10-23 18:25:11 +0200116 /k8sclusters O O
117 /<id> O O O
118 /k8srepos O O
119 /<id> O O
tiernoc94c3df2018-02-09 15:38:54 +0100120
garciadeblas9750c5a2018-10-15 16:20:35 +0200121 /nst/v1 O O
122 /netslice_templates_content O O
123 /<nstInfoId> O O O O
124 /netslice_templates O O
125 /<nstInfoId> O O O
126 /nst_content O O
127 /nst O
128 /artifacts[/<artifactPath>] O
129 /subscriptions X X
130 /<subscriptionId> X X
131
132 /nsilcm/v1
133 /netslice_instances_content O O
134 /<SliceInstanceId> O O
135 /netslice_instances O O
136 /<SliceInstanceId> O O
137 instantiate O
138 terminate O
139 action O
140 /nsi_lcm_op_occs O O
141 /<nsiLcmOpOccId> O O O
142 /subscriptions X X
143 /<subscriptionId> X X
144
tierno2236d202018-05-16 19:05:16 +0200145query string:
146 Follows SOL005 section 4.3.2 It contains extra METHOD to override http method, FORCE to force.
tierno36ec8602018-11-02 17:27:11 +0100147 simpleFilterExpr := <attrName>["."<attrName>]*["."<op>]"="<value>[","<value>]*
148 filterExpr := <simpleFilterExpr>["&"<simpleFilterExpr>]*
149 op := "eq" | "neq" (or "ne") | "gt" | "lt" | "gte" | "lte" | "cont" | "ncont"
150 attrName := string
tierno2236d202018-05-16 19:05:16 +0200151 For filtering inside array, it must select the element of the array, or add ANYINDEX to apply the filtering over any
152 item of the array, that is, pass if any item of the array pass the filter.
153 It allows both ne and neq for not equal
154 TODO: 4.3.3 Attribute selectors
155 all_fields, fields=x,y,.., exclude_default, exclude_fields=x,y,...
tiernoc94c3df2018-02-09 15:38:54 +0100156 (none) … same as “exclude_default”
157 all_fields … all attributes.
tierno2236d202018-05-16 19:05:16 +0200158 fields=<list> … all attributes except all complex attributes with minimum cardinality of zero that are not
159 conditionally mandatory, and that are not provided in <list>.
160 exclude_fields=<list> … all attributes except those complex attributes with a minimum cardinality of zero that
161 are not conditionally mandatory, and that are provided in <list>.
162 exclude_default … all attributes except those complex attributes with a minimum cardinality of zero that are not
163 conditionally mandatory, and that are part of the "default exclude set" defined in the present specification for
164 the particular resource
165 exclude_default and include=<list> … all attributes except those complex attributes with a minimum cardinality
166 of zero that are not conditionally mandatory and that are part of the "default exclude set" defined in the
167 present specification for the particular resource, but that are not part of <list>
tierno65ca36d2019-02-12 19:27:52 +0100168 Additionally it admits some administrator values:
169 FORCE: To force operations skipping dependency checkings
170 ADMIN: To act as an administrator or a different project
171 PUBLIC: To get public descriptors or set a descriptor as public
172 SET_PROJECT: To make a descriptor available for other project
173
tiernoc94c3df2018-02-09 15:38:54 +0100174Header field name Reference Example Descriptions
175 Accept IETF RFC 7231 [19] application/json Content-Types that are acceptable for the response.
176 This header field shall be present if the response is expected to have a non-empty message body.
177 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the request.
178 This header field shall be present if the request has a non-empty message body.
tierno2236d202018-05-16 19:05:16 +0200179 Authorization IETF RFC 7235 [22] Bearer mF_9.B5f-4.1JqM The authorization token for the request.
180 Details are specified in clause 4.5.3.
tiernoc94c3df2018-02-09 15:38:54 +0100181 Range IETF RFC 7233 [21] 1000-2000 Requested range of bytes from a file
182Header field name Reference Example Descriptions
183 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the response.
184 This header field shall be present if the response has a non-empty message body.
tierno2236d202018-05-16 19:05:16 +0200185 Location IETF RFC 7231 [19] http://www.example.com/vnflcm/v1/vnf_instances/123 Used in redirection, or when a
186 new resource has been created.
tiernoc94c3df2018-02-09 15:38:54 +0100187 This header field shall be present if the response status code is 201 or 3xx.
tierno2236d202018-05-16 19:05:16 +0200188 In the present document this header field is also used if the response status code is 202 and a new resource was
189 created.
190 WWW-Authenticate IETF RFC 7235 [22] Bearer realm="example" Challenge if the corresponding HTTP request has not
191 provided authorization, or error details if the corresponding HTTP request has provided an invalid authorization
192 token.
193 Accept-Ranges IETF RFC 7233 [21] bytes Used by the Server to signal whether or not it supports ranges for
194 certain resources.
195 Content-Range IETF RFC 7233 [21] bytes 21010-47021/ 47022 Signals the byte range that is contained in the
196 response, and the total length of the file.
tiernoc94c3df2018-02-09 15:38:54 +0100197 Retry-After IETF RFC 7231 [19] Fri, 31 Dec 1999 23:59:59 GMT
tiernoc94c3df2018-02-09 15:38:54 +0100198"""
199
tierno701018c2019-06-25 11:13:14 +0000200valid_query_string = ("ADMIN", "SET_PROJECT", "FORCE", "PUBLIC")
201# ^ Contains possible administrative query string words:
202# ADMIN=True(by default)|Project|Project-list: See all elements, or elements of a project
203# (not owned by my session project).
204# PUBLIC=True(by default)|False: See/hide public elements. Set/Unset a topic to be public
205# FORCE=True(by default)|False: Force edition/deletion operations
206# SET_PROJECT=Project|Project-list: Add/Delete the topic to the projects portfolio
207
208valid_url_methods = {
209 # contains allowed URL and methods, and the role_permission name
210 "admin": {
211 "v1": {
212 "tokens": {"METHODS": ("GET", "POST", "DELETE"),
213 "ROLE_PERMISSION": "tokens:",
214 "<ID>": {"METHODS": ("GET", "DELETE"),
215 "ROLE_PERMISSION": "tokens:id:"
216 }
217 },
218 "users": {"METHODS": ("GET", "POST"),
219 "ROLE_PERMISSION": "users:",
delacruzramo1459d602019-10-03 14:22:00 +0200220 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"),
tierno701018c2019-06-25 11:13:14 +0000221 "ROLE_PERMISSION": "users:id:"
222 }
223 },
224 "projects": {"METHODS": ("GET", "POST"),
225 "ROLE_PERMISSION": "projects:",
delacruzramo1459d602019-10-03 14:22:00 +0200226 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"),
tierno701018c2019-06-25 11:13:14 +0000227 "ROLE_PERMISSION": "projects:id:"}
228 },
229 "roles": {"METHODS": ("GET", "POST"),
230 "ROLE_PERMISSION": "roles:",
delacruzramo1459d602019-10-03 14:22:00 +0200231 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"),
tierno701018c2019-06-25 11:13:14 +0000232 "ROLE_PERMISSION": "roles:id:"
233 }
234 },
235 "vims": {"METHODS": ("GET", "POST"),
236 "ROLE_PERMISSION": "vims:",
delacruzramo1459d602019-10-03 14:22:00 +0200237 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"),
tierno701018c2019-06-25 11:13:14 +0000238 "ROLE_PERMISSION": "vims:id:"
239 }
240 },
241 "vim_accounts": {"METHODS": ("GET", "POST"),
242 "ROLE_PERMISSION": "vim_accounts:",
delacruzramo1459d602019-10-03 14:22:00 +0200243 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"),
tierno701018c2019-06-25 11:13:14 +0000244 "ROLE_PERMISSION": "vim_accounts:id:"
245 }
246 },
247 "wim_accounts": {"METHODS": ("GET", "POST"),
248 "ROLE_PERMISSION": "wim_accounts:",
delacruzramo1459d602019-10-03 14:22:00 +0200249 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"),
tierno701018c2019-06-25 11:13:14 +0000250 "ROLE_PERMISSION": "wim_accounts:id:"
251 }
252 },
253 "sdns": {"METHODS": ("GET", "POST"),
254 "ROLE_PERMISSION": "sdn_controllers:",
delacruzramo1459d602019-10-03 14:22:00 +0200255 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"),
tierno701018c2019-06-25 11:13:14 +0000256 "ROLE_PERMISSION": "sdn_controllers:id:"
257 }
258 },
delacruzramofe598fe2019-10-23 18:25:11 +0200259 "k8sclusters": {"METHODS": ("GET", "POST"),
260 "ROLE_PERMISSION": "k8sclusters:",
delacruzramo549dda62019-11-14 09:57:35 +0100261 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"),
delacruzramofe598fe2019-10-23 18:25:11 +0200262 "ROLE_PERMISSION": "k8sclusters:id:"
263 }
264 },
265 "k8srepos": {"METHODS": ("GET", "POST"),
266 "ROLE_PERMISSION": "k8srepos:",
267 "<ID>": {"METHODS": ("GET", "DELETE"),
268 "ROLE_PERMISSION": "k8srepos:id:"
269 }
270 },
271
tierno701018c2019-06-25 11:13:14 +0000272 }
273 },
274 "pdu": {
275 "v1": {
276 "pdu_descriptors": {"METHODS": ("GET", "POST"),
277 "ROLE_PERMISSION": "pduds:",
278 "<ID>": {"METHODS": ("GET", "POST", "DELETE", "PATCH", "PUT"),
279 "ROLE_PERMISSION": "pduds:id:"
280 }
281 },
282 }
283 },
284 "nsd": {
285 "v1": {
286 "ns_descriptors_content": {"METHODS": ("GET", "POST"),
287 "ROLE_PERMISSION": "nsds:",
288 "<ID>": {"METHODS": ("GET", "PUT", "DELETE"),
289 "ROLE_PERMISSION": "nsds:id:"
290 }
291 },
292 "ns_descriptors": {"METHODS": ("GET", "POST"),
293 "ROLE_PERMISSION": "nsds:",
294 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"),
295 "ROLE_PERMISSION": "nsds:id:",
296 "nsd_content": {"METHODS": ("GET", "PUT"),
297 "ROLE_PERMISSION": "nsds:id:content:",
298 },
299 "nsd": {"METHODS": ("GET",), # descriptor inside package
300 "ROLE_PERMISSION": "nsds:id:content:"
301 },
302 "artifacts": {"*": {"METHODS": ("GET",),
303 "ROLE_PERMISSION": "nsds:id:nsd_artifact:"
304 }
305 }
306 }
307 },
308 "pnf_descriptors": {"TODO": ("GET", "POST"),
309 "<ID>": {"TODO": ("GET", "DELETE", "PATCH"),
310 "pnfd_content": {"TODO": ("GET", "PUT")}
311 }
312 },
313 "subscriptions": {"TODO": ("GET", "POST"),
314 "<ID>": {"TODO": ("GET", "DELETE")}
315 },
316 }
317 },
318 "vnfpkgm": {
319 "v1": {
320 "vnf_packages_content": {"METHODS": ("GET", "POST"),
321 "ROLE_PERMISSION": "vnfds:",
322 "<ID>": {"METHODS": ("GET", "PUT", "DELETE"),
323 "ROLE_PERMISSION": "vnfds:id:"}
324 },
325 "vnf_packages": {"METHODS": ("GET", "POST"),
326 "ROLE_PERMISSION": "vnfds:",
327 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"), # GET: vnfPkgInfo
328 "ROLE_PERMISSION": "vnfds:id:",
329 "package_content": {"METHODS": ("GET", "PUT"), # package
330 "ROLE_PERMISSION": "vnfds:id:",
331 "upload_from_uri": {"METHODS": (),
332 "TODO": ("POST", ),
333 "ROLE_PERMISSION": "vnfds:id:upload:"
334 }
335 },
336 "vnfd": {"METHODS": ("GET", ), # descriptor inside package
337 "ROLE_PERMISSION": "vnfds:id:content:"
338 },
339 "artifacts": {"*": {"METHODS": ("GET", ),
340 "ROLE_PERMISSION": "vnfds:id:vnfd_artifact:"
341 }
342 }
343 }
344 },
345 "subscriptions": {"TODO": ("GET", "POST"),
346 "<ID>": {"TODO": ("GET", "DELETE")}
347 },
348 }
349 },
350 "nslcm": {
351 "v1": {
352 "ns_instances_content": {"METHODS": ("GET", "POST"),
353 "ROLE_PERMISSION": "ns_instances:",
354 "<ID>": {"METHODS": ("GET", "DELETE"),
355 "ROLE_PERMISSION": "ns_instances:id:"
356 }
357 },
358 "ns_instances": {"METHODS": ("GET", "POST"),
359 "ROLE_PERMISSION": "ns_instances:",
360 "<ID>": {"METHODS": ("GET", "DELETE"),
361 "ROLE_PERMISSION": "ns_instances:id:",
362 "scale": {"METHODS": ("POST",),
363 "ROLE_PERMISSION": "ns_instances:id:scale:"
364 },
365 "terminate": {"METHODS": ("POST",),
366 "ROLE_PERMISSION": "ns_instances:id:terminate:"
367 },
368 "instantiate": {"METHODS": ("POST",),
369 "ROLE_PERMISSION": "ns_instances:id:instantiate:"
370 },
371 "action": {"METHODS": ("POST",),
372 "ROLE_PERMISSION": "ns_instances:id:action:"
373 },
374 }
375 },
376 "ns_lcm_op_occs": {"METHODS": ("GET",),
377 "ROLE_PERMISSION": "ns_instances:opps:",
378 "<ID>": {"METHODS": ("GET",),
379 "ROLE_PERMISSION": "ns_instances:opps:id:"
380 },
381 },
382 "vnfrs": {"METHODS": ("GET",),
383 "ROLE_PERMISSION": "vnf_instances:",
384 "<ID>": {"METHODS": ("GET",),
385 "ROLE_PERMISSION": "vnf_instances:id:"
386 }
387 },
388 "vnf_instances": {"METHODS": ("GET",),
389 "ROLE_PERMISSION": "vnf_instances:",
390 "<ID>": {"METHODS": ("GET",),
391 "ROLE_PERMISSION": "vnf_instances:id:"
392 }
393 },
394 }
395 },
396 "nst": {
397 "v1": {
398 "netslice_templates_content": {"METHODS": ("GET", "POST"),
399 "ROLE_PERMISSION": "slice_templates:",
400 "<ID>": {"METHODS": ("GET", "PUT", "DELETE"),
401 "ROLE_PERMISSION": "slice_templates:id:", }
402 },
403 "netslice_templates": {"METHODS": ("GET", "POST"),
404 "ROLE_PERMISSION": "slice_templates:",
405 "<ID>": {"METHODS": ("GET", "DELETE"),
406 "TODO": ("PATCH",),
407 "ROLE_PERMISSION": "slice_templates:id:",
408 "nst_content": {"METHODS": ("GET", "PUT"),
409 "ROLE_PERMISSION": "slice_templates:id:content:"
410 },
411 "nst": {"METHODS": ("GET",), # descriptor inside package
412 "ROLE_PERMISSION": "slice_templates:id:content:"
413 },
414 "artifacts": {"*": {"METHODS": ("GET",),
415 "ROLE_PERMISSION": "slice_templates:id:content:"
416 }
417 }
418 }
419 },
420 "subscriptions": {"TODO": ("GET", "POST"),
421 "<ID>": {"TODO": ("GET", "DELETE")}
422 },
423 }
424 },
425 "nsilcm": {
426 "v1": {
427 "netslice_instances_content": {"METHODS": ("GET", "POST"),
428 "ROLE_PERMISSION": "slice_instances:",
429 "<ID>": {"METHODS": ("GET", "DELETE"),
430 "ROLE_PERMISSION": "slice_instances:id:"
431 }
432 },
433 "netslice_instances": {"METHODS": ("GET", "POST"),
434 "ROLE_PERMISSION": "slice_instances:",
435 "<ID>": {"METHODS": ("GET", "DELETE"),
436 "ROLE_PERMISSION": "slice_instances:id:",
437 "terminate": {"METHODS": ("POST",),
438 "ROLE_PERMISSION": "slice_instances:id:terminate:"
439 },
440 "instantiate": {"METHODS": ("POST",),
441 "ROLE_PERMISSION": "slice_instances:id:instantiate:"
442 },
443 "action": {"METHODS": ("POST",),
444 "ROLE_PERMISSION": "slice_instances:id:action:"
445 },
446 }
447 },
448 "nsi_lcm_op_occs": {"METHODS": ("GET",),
449 "ROLE_PERMISSION": "slice_instances:opps:",
450 "<ID>": {"METHODS": ("GET",),
451 "ROLE_PERMISSION": "slice_instances:opps:id:",
452 },
453 },
454 }
455 },
456 "nspm": {
457 "v1": {
458 "pm_jobs": {
459 "<ID>": {
460 "reports": {
461 "<ID>": {"METHODS": ("GET",),
462 "ROLE_PERMISSION": "reports:id:",
463 }
464 }
465 },
466 },
467 },
468 },
469}
470
tiernoc94c3df2018-02-09 15:38:54 +0100471
472class NbiException(Exception):
473
474 def __init__(self, message, http_code=HTTPStatus.METHOD_NOT_ALLOWED):
475 Exception.__init__(self, message)
476 self.http_code = http_code
477
478
479class Server(object):
480 instance = 0
481 # to decode bytes to str
482 reader = getreader("utf-8")
483
484 def __init__(self):
485 self.instance += 1
tierno701018c2019-06-25 11:13:14 +0000486 self.authenticator = Authenticator(valid_url_methods, valid_query_string)
delacruzramo3d6881c2019-12-04 13:42:26 +0100487 self.engine = Engine(self.authenticator.tokens_cache)
tiernoc94c3df2018-02-09 15:38:54 +0100488
tiernoc94c3df2018-02-09 15:38:54 +0100489 def _format_in(self, kwargs):
490 try:
491 indata = None
492 if cherrypy.request.body.length:
493 error_text = "Invalid input format "
494
495 if "Content-Type" in cherrypy.request.headers:
496 if "application/json" in cherrypy.request.headers["Content-Type"]:
497 error_text = "Invalid json format "
498 indata = json.load(self.reader(cherrypy.request.body))
gcalvinode4adfe2018-10-30 11:46:09 +0100499 cherrypy.request.headers.pop("Content-File-MD5", None)
tiernoc94c3df2018-02-09 15:38:54 +0100500 elif "application/yaml" in cherrypy.request.headers["Content-Type"]:
501 error_text = "Invalid yaml format "
delacruzramob19cadc2019-10-08 10:18:02 +0200502 indata = yaml.load(cherrypy.request.body, Loader=yaml.SafeLoader)
gcalvinode4adfe2018-10-30 11:46:09 +0100503 cherrypy.request.headers.pop("Content-File-MD5", None)
tiernoc94c3df2018-02-09 15:38:54 +0100504 elif "application/binary" in cherrypy.request.headers["Content-Type"] or \
505 "application/gzip" in cherrypy.request.headers["Content-Type"] or \
tiernof27c79b2018-03-12 17:08:42 +0100506 "application/zip" in cherrypy.request.headers["Content-Type"] or \
507 "text/plain" in cherrypy.request.headers["Content-Type"]:
508 indata = cherrypy.request.body # .read()
tiernoc94c3df2018-02-09 15:38:54 +0100509 elif "multipart/form-data" in cherrypy.request.headers["Content-Type"]:
510 if "descriptor_file" in kwargs:
511 filecontent = kwargs.pop("descriptor_file")
512 if not filecontent.file:
513 raise NbiException("empty file or content", HTTPStatus.BAD_REQUEST)
tiernof27c79b2018-03-12 17:08:42 +0100514 indata = filecontent.file # .read()
tiernoc94c3df2018-02-09 15:38:54 +0100515 if filecontent.content_type.value:
516 cherrypy.request.headers["Content-Type"] = filecontent.content_type.value
517 else:
518 # raise cherrypy.HTTPError(HTTPStatus.Not_Acceptable,
519 # "Only 'Content-Type' of type 'application/json' or
520 # 'application/yaml' for input format are available")
521 error_text = "Invalid yaml format "
delacruzramob19cadc2019-10-08 10:18:02 +0200522 indata = yaml.load(cherrypy.request.body, Loader=yaml.SafeLoader)
gcalvinode4adfe2018-10-30 11:46:09 +0100523 cherrypy.request.headers.pop("Content-File-MD5", None)
tiernoc94c3df2018-02-09 15:38:54 +0100524 else:
525 error_text = "Invalid yaml format "
delacruzramob19cadc2019-10-08 10:18:02 +0200526 indata = yaml.load(cherrypy.request.body, Loader=yaml.SafeLoader)
gcalvinode4adfe2018-10-30 11:46:09 +0100527 cherrypy.request.headers.pop("Content-File-MD5", None)
tiernoc94c3df2018-02-09 15:38:54 +0100528 if not indata:
529 indata = {}
530
tiernoc94c3df2018-02-09 15:38:54 +0100531 format_yaml = False
532 if cherrypy.request.headers.get("Query-String-Format") == "yaml":
533 format_yaml = True
534
535 for k, v in kwargs.items():
536 if isinstance(v, str):
537 if v == "":
538 kwargs[k] = None
539 elif format_yaml:
540 try:
delacruzramob19cadc2019-10-08 10:18:02 +0200541 kwargs[k] = yaml.load(v, Loader=yaml.SafeLoader)
tiernoe1281182018-05-22 12:24:36 +0200542 except Exception:
tiernoc94c3df2018-02-09 15:38:54 +0100543 pass
544 elif k.endswith(".gt") or k.endswith(".lt") or k.endswith(".gte") or k.endswith(".lte"):
545 try:
546 kwargs[k] = int(v)
tiernoe1281182018-05-22 12:24:36 +0200547 except Exception:
tiernoc94c3df2018-02-09 15:38:54 +0100548 try:
549 kwargs[k] = float(v)
tiernoe1281182018-05-22 12:24:36 +0200550 except Exception:
tiernoc94c3df2018-02-09 15:38:54 +0100551 pass
552 elif v.find(",") > 0:
553 kwargs[k] = v.split(",")
554 elif isinstance(v, (list, tuple)):
555 for index in range(0, len(v)):
556 if v[index] == "":
557 v[index] = None
558 elif format_yaml:
559 try:
delacruzramob19cadc2019-10-08 10:18:02 +0200560 v[index] = yaml.load(v[index], Loader=yaml.SafeLoader)
tiernoe1281182018-05-22 12:24:36 +0200561 except Exception:
tiernoc94c3df2018-02-09 15:38:54 +0100562 pass
563
tiernof27c79b2018-03-12 17:08:42 +0100564 return indata
tiernoc94c3df2018-02-09 15:38:54 +0100565 except (ValueError, yaml.YAMLError) as exc:
566 raise NbiException(error_text + str(exc), HTTPStatus.BAD_REQUEST)
567 except KeyError as exc:
568 raise NbiException("Query string error: " + str(exc), HTTPStatus.BAD_REQUEST)
tiernob92094f2018-05-11 13:44:22 +0200569 except Exception as exc:
570 raise NbiException(error_text + str(exc), HTTPStatus.BAD_REQUEST)
tiernoc94c3df2018-02-09 15:38:54 +0100571
572 @staticmethod
tierno701018c2019-06-25 11:13:14 +0000573 def _format_out(data, token_info=None, _format=None):
tiernoc94c3df2018-02-09 15:38:54 +0100574 """
575 return string of dictionary data according to requested json, yaml, xml. By default json
tiernof27c79b2018-03-12 17:08:42 +0100576 :param data: response to be sent. Can be a dict, text or file
tierno701018c2019-06-25 11:13:14 +0000577 :param token_info: Contains among other username and project
tierno5792d7d2019-08-30 15:37:12 +0000578 :param _format: The format to be set as Content-Type if data is a file
tiernoc94c3df2018-02-09 15:38:54 +0100579 :return: None
580 """
tierno0f98af52018-03-19 10:28:22 +0100581 accept = cherrypy.request.headers.get("Accept")
tiernof27c79b2018-03-12 17:08:42 +0100582 if data is None:
tierno0f98af52018-03-19 10:28:22 +0100583 if accept and "text/html" in accept:
tierno701018c2019-06-25 11:13:14 +0000584 return html.format(data, cherrypy.request, cherrypy.response, token_info)
tierno09c073e2018-04-26 13:36:48 +0200585 # cherrypy.response.status = HTTPStatus.NO_CONTENT.value
tiernof27c79b2018-03-12 17:08:42 +0100586 return
587 elif hasattr(data, "read"): # file object
588 if _format:
589 cherrypy.response.headers["Content-Type"] = _format
590 elif "b" in data.mode: # binariy asssumig zip
591 cherrypy.response.headers["Content-Type"] = 'application/zip'
592 else:
593 cherrypy.response.headers["Content-Type"] = 'text/plain'
594 # TODO check that cherrypy close file. If not implement pending things to close per thread next
595 return data
tierno0f98af52018-03-19 10:28:22 +0100596 if accept:
tiernoc94c3df2018-02-09 15:38:54 +0100597 if "application/json" in accept:
598 cherrypy.response.headers["Content-Type"] = 'application/json; charset=utf-8'
599 a = json.dumps(data, indent=4) + "\n"
600 return a.encode("utf8")
601 elif "text/html" in accept:
tierno701018c2019-06-25 11:13:14 +0000602 return html.format(data, cherrypy.request, cherrypy.response, token_info)
tiernoc94c3df2018-02-09 15:38:54 +0100603
tiernof27c79b2018-03-12 17:08:42 +0100604 elif "application/yaml" in accept or "*/*" in accept or "text/plain" in accept:
tiernoc94c3df2018-02-09 15:38:54 +0100605 pass
tiernof717cbe2018-12-03 16:35:42 +0000606 # if there is not any valid accept, raise an error. But if response is already an error, format in yaml
607 elif cherrypy.response.status >= 400:
tiernoc94c3df2018-02-09 15:38:54 +0100608 raise cherrypy.HTTPError(HTTPStatus.NOT_ACCEPTABLE.value,
609 "Only 'Accept' of type 'application/json' or 'application/yaml' "
610 "for output format are available")
611 cherrypy.response.headers["Content-Type"] = 'application/yaml'
612 return yaml.safe_dump(data, explicit_start=True, indent=4, default_flow_style=False, tags=False,
613 encoding='utf-8', allow_unicode=True) # , canonical=True, default_style='"'
614
615 @cherrypy.expose
616 def index(self, *args, **kwargs):
tierno701018c2019-06-25 11:13:14 +0000617 token_info = None
tiernoc94c3df2018-02-09 15:38:54 +0100618 try:
619 if cherrypy.request.method == "GET":
tierno701018c2019-06-25 11:13:14 +0000620 token_info = self.authenticator.authorize()
delacruzramo01b15d32019-07-02 14:37:47 +0200621 outdata = token_info # Home page
tiernoc94c3df2018-02-09 15:38:54 +0100622 else:
623 raise cherrypy.HTTPError(HTTPStatus.METHOD_NOT_ALLOWED.value,
tierno2236d202018-05-16 19:05:16 +0200624 "Method {} not allowed for tokens".format(cherrypy.request.method))
tiernoc94c3df2018-02-09 15:38:54 +0100625
tierno701018c2019-06-25 11:13:14 +0000626 return self._format_out(outdata, token_info)
tiernoc94c3df2018-02-09 15:38:54 +0100627
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100628 except (EngineException, AuthException) as e:
tierno701018c2019-06-25 11:13:14 +0000629 # cherrypy.log("index Exception {}".format(e))
tiernoc94c3df2018-02-09 15:38:54 +0100630 cherrypy.response.status = e.http_code.value
tierno701018c2019-06-25 11:13:14 +0000631 return self._format_out("Welcome to OSM!", token_info)
tiernoc94c3df2018-02-09 15:38:54 +0100632
633 @cherrypy.expose
tierno55945e72018-04-06 16:40:27 +0200634 def version(self, *args, **kwargs):
tiernodfe09572018-04-24 10:41:10 +0200635 # TODO consider to remove and provide version using the static version file
tierno55945e72018-04-06 16:40:27 +0200636 try:
637 if cherrypy.request.method != "GET":
638 raise NbiException("Only method GET is allowed", HTTPStatus.METHOD_NOT_ALLOWED)
639 elif args or kwargs:
640 raise NbiException("Invalid URL or query string for version", HTTPStatus.METHOD_NOT_ALLOWED)
tierno9c630112019-08-29 14:21:41 +0000641 # TODO include version of other modules, pick up from some kafka admin message
tierno5792d7d2019-08-30 15:37:12 +0000642 osm_nbi_version = {"version": nbi_version, "date": nbi_version_date}
643 return self._format_out(osm_nbi_version)
tierno55945e72018-04-06 16:40:27 +0200644 except NbiException as e:
645 cherrypy.response.status = e.http_code.value
646 problem_details = {
647 "code": e.http_code.name,
648 "status": e.http_code.value,
649 "detail": str(e),
650 }
651 return self._format_out(problem_details, None)
652
tiernoa5035702019-07-29 08:54:42 +0000653 @staticmethod
654 def _format_login(token_info):
655 """
656 Changes cherrypy.request.login to include username/project_name;session so that cherrypy access log will
657 log this information
658 :param token_info: Dictionary with token content
659 :return: None
660 """
661 cherrypy.request.login = token_info.get("username", "-")
662 if token_info.get("project_name"):
663 cherrypy.request.login += "/" + token_info["project_name"]
664 if token_info.get("id"):
665 cherrypy.request.login += ";session=" + token_info["id"][0:12]
666
tierno55945e72018-04-06 16:40:27 +0200667 @cherrypy.expose
tiernof27c79b2018-03-12 17:08:42 +0100668 def token(self, method, token_id=None, kwargs=None):
tierno701018c2019-06-25 11:13:14 +0000669 token_info = None
tiernoc94c3df2018-02-09 15:38:54 +0100670 # self.engine.load_dbase(cherrypy.request.app.config)
tiernof27c79b2018-03-12 17:08:42 +0100671 indata = self._format_in(kwargs)
672 if not isinstance(indata, dict):
673 raise NbiException("Expected application/yaml or application/json Content-Type", HTTPStatus.BAD_REQUEST)
tiernoa5035702019-07-29 08:54:42 +0000674
675 if method == "GET":
676 token_info = self.authenticator.authorize()
677 # for logging
678 self._format_login(token_info)
679 if token_id:
680 outdata = self.authenticator.get_token(token_info, token_id)
tiernoc94c3df2018-02-09 15:38:54 +0100681 else:
tiernoa5035702019-07-29 08:54:42 +0000682 outdata = self.authenticator.get_token_list(token_info)
683 elif method == "POST":
684 try:
685 token_info = self.authenticator.authorize()
686 except Exception:
687 token_info = None
688 if kwargs:
689 indata.update(kwargs)
690 # This is needed to log the user when authentication fails
691 cherrypy.request.login = "{}".format(indata.get("username", "-"))
692 outdata = token_info = self.authenticator.new_token(token_info, indata, cherrypy.request.remote)
693 cherrypy.session['Authorization'] = outdata["_id"]
694 self._set_location_header("admin", "v1", "tokens", outdata["_id"])
695 # for logging
696 self._format_login(token_info)
697
698 # cherrypy.response.cookie["Authorization"] = outdata["id"]
699 # cherrypy.response.cookie["Authorization"]['expires'] = 3600
700 elif method == "DELETE":
701 if not token_id and "id" in kwargs:
702 token_id = kwargs["id"]
703 elif not token_id:
704 token_info = self.authenticator.authorize()
705 # for logging
706 self._format_login(token_info)
707 token_id = token_info["_id"]
708 outdata = self.authenticator.del_token(token_id)
709 token_info = None
710 cherrypy.session['Authorization'] = "logout"
711 # cherrypy.response.cookie["Authorization"] = token_id
712 # cherrypy.response.cookie["Authorization"]['expires'] = 0
713 else:
714 raise NbiException("Method {} not allowed for token".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
715 return self._format_out(outdata, token_info)
tiernoc94c3df2018-02-09 15:38:54 +0100716
717 @cherrypy.expose
718 def test(self, *args, **kwargs):
719 thread_info = None
tiernof27c79b2018-03-12 17:08:42 +0100720 if args and args[0] == "help":
tiernof717cbe2018-12-03 16:35:42 +0000721 return "<html><pre>\ninit\nfile/<name> download file\ndb-clear/table\nfs-clear[/folder]\nlogin\nlogin2\n"\
tiernoe1281182018-05-22 12:24:36 +0200722 "sleep/<time>\nmessage/topic\n</pre></html>"
tiernof27c79b2018-03-12 17:08:42 +0100723
724 elif args and args[0] == "init":
tiernoc94c3df2018-02-09 15:38:54 +0100725 try:
726 # self.engine.load_dbase(cherrypy.request.app.config)
727 self.engine.create_admin()
728 return "Done. User 'admin', password 'admin' created"
729 except Exception:
730 cherrypy.response.status = HTTPStatus.FORBIDDEN.value
731 return self._format_out("Database already initialized")
tiernof27c79b2018-03-12 17:08:42 +0100732 elif args and args[0] == "file":
733 return cherrypy.lib.static.serve_file(cherrypy.tree.apps['/osm'].config["storage"]["path"] + "/" + args[1],
734 "text/plain", "attachment")
735 elif args and args[0] == "file2":
736 f_path = cherrypy.tree.apps['/osm'].config["storage"]["path"] + "/" + args[1]
737 f = open(f_path, "r")
738 cherrypy.response.headers["Content-type"] = "text/plain"
tiernof27c79b2018-03-12 17:08:42 +0100739 return f
tierno55945e72018-04-06 16:40:27 +0200740
tiernof27c79b2018-03-12 17:08:42 +0100741 elif len(args) == 2 and args[0] == "db-clear":
tiernof717cbe2018-12-03 16:35:42 +0000742 deleted_info = self.engine.db.del_list(args[1], kwargs)
743 return "{} {} deleted\n".format(deleted_info["deleted"], args[1])
744 elif len(args) and args[0] == "fs-clear":
745 if len(args) >= 2:
746 folders = (args[1],)
747 else:
748 folders = self.engine.fs.dir_ls(".")
749 for folder in folders:
750 self.engine.fs.file_delete(folder)
751 return ",".join(folders) + " folders deleted\n"
tiernoc94c3df2018-02-09 15:38:54 +0100752 elif args and args[0] == "login":
753 if not cherrypy.request.headers.get("Authorization"):
754 cherrypy.response.headers["WWW-Authenticate"] = 'Basic realm="Access to OSM site", charset="UTF-8"'
755 cherrypy.response.status = HTTPStatus.UNAUTHORIZED.value
756 elif args and args[0] == "login2":
757 if not cherrypy.request.headers.get("Authorization"):
758 cherrypy.response.headers["WWW-Authenticate"] = 'Bearer realm="Access to OSM site"'
759 cherrypy.response.status = HTTPStatus.UNAUTHORIZED.value
760 elif args and args[0] == "sleep":
761 sleep_time = 5
762 try:
763 sleep_time = int(args[1])
764 except Exception:
765 cherrypy.response.status = HTTPStatus.FORBIDDEN.value
766 return self._format_out("Database already initialized")
767 thread_info = cherrypy.thread_data
768 print(thread_info)
769 time.sleep(sleep_time)
770 # thread_info
771 elif len(args) >= 2 and args[0] == "message":
tiernob24258a2018-10-04 18:39:49 +0200772 main_topic = args[1]
773 return_text = "<html><pre>{} ->\n".format(main_topic)
tiernoc94c3df2018-02-09 15:38:54 +0100774 try:
tierno55945e72018-04-06 16:40:27 +0200775 if cherrypy.request.method == 'POST':
delacruzramob19cadc2019-10-08 10:18:02 +0200776 to_send = yaml.load(cherrypy.request.body, Loader=yaml.SafeLoader)
tierno55945e72018-04-06 16:40:27 +0200777 for k, v in to_send.items():
tiernob24258a2018-10-04 18:39:49 +0200778 self.engine.msg.write(main_topic, k, v)
tierno55945e72018-04-06 16:40:27 +0200779 return_text += " {}: {}\n".format(k, v)
780 elif cherrypy.request.method == 'GET':
781 for k, v in kwargs.items():
delacruzramob19cadc2019-10-08 10:18:02 +0200782 self.engine.msg.write(main_topic, k, yaml.load(v), Loader=yaml.SafeLoader)
783 return_text += " {}: {}\n".format(k, yaml.load(v), Loader=yaml.SafeLoader)
tiernoc94c3df2018-02-09 15:38:54 +0100784 except Exception as e:
tierno55945e72018-04-06 16:40:27 +0200785 return_text += "Error: " + str(e)
786 return_text += "</pre></html>\n"
787 return return_text
tiernoc94c3df2018-02-09 15:38:54 +0100788
789 return_text = (
790 "<html><pre>\nheaders:\n args: {}\n".format(args) +
791 " kwargs: {}\n".format(kwargs) +
792 " headers: {}\n".format(cherrypy.request.headers) +
793 " path_info: {}\n".format(cherrypy.request.path_info) +
794 " query_string: {}\n".format(cherrypy.request.query_string) +
795 " session: {}\n".format(cherrypy.session) +
796 " cookie: {}\n".format(cherrypy.request.cookie) +
797 " method: {}\n".format(cherrypy.request.method) +
tiernof717cbe2018-12-03 16:35:42 +0000798 " session: {}\n".format(cherrypy.session.get('fieldname')) +
tiernoc94c3df2018-02-09 15:38:54 +0100799 " body:\n")
800 return_text += " length: {}\n".format(cherrypy.request.body.length)
801 if cherrypy.request.body.length:
802 return_text += " content: {}\n".format(
803 str(cherrypy.request.body.read(int(cherrypy.request.headers.get('Content-Length', 0)))))
804 if thread_info:
805 return_text += "thread: {}\n".format(thread_info)
806 return_text += "</pre></html>"
807 return return_text
808
tierno701018c2019-06-25 11:13:14 +0000809 @staticmethod
810 def _check_valid_url_method(method, *args):
tiernof27c79b2018-03-12 17:08:42 +0100811 if len(args) < 3:
tiernob24258a2018-10-04 18:39:49 +0200812 raise NbiException("URL must contain at least 'main_topic/version/topic'", HTTPStatus.METHOD_NOT_ALLOWED)
tiernof27c79b2018-03-12 17:08:42 +0100813
tierno701018c2019-06-25 11:13:14 +0000814 reference = valid_url_methods
tiernof27c79b2018-03-12 17:08:42 +0100815 for arg in args:
816 if arg is None:
817 break
818 if not isinstance(reference, dict):
819 raise NbiException("URL contains unexpected extra items '{}'".format(arg),
820 HTTPStatus.METHOD_NOT_ALLOWED)
821
822 if arg in reference:
823 reference = reference[arg]
824 elif "<ID>" in reference:
825 reference = reference["<ID>"]
826 elif "*" in reference:
827 reference = reference["*"]
828 break
829 else:
830 raise NbiException("Unexpected URL item {}".format(arg), HTTPStatus.METHOD_NOT_ALLOWED)
831 if "TODO" in reference and method in reference["TODO"]:
832 raise NbiException("Method {} not supported yet for this URL".format(method), HTTPStatus.NOT_IMPLEMENTED)
tierno2236d202018-05-16 19:05:16 +0200833 elif "METHODS" in reference and method not in reference["METHODS"]:
tiernof27c79b2018-03-12 17:08:42 +0100834 raise NbiException("Method {} not supported for this URL".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
tierno701018c2019-06-25 11:13:14 +0000835 return reference["ROLE_PERMISSION"] + method.lower()
tiernof27c79b2018-03-12 17:08:42 +0100836
837 @staticmethod
tiernob24258a2018-10-04 18:39:49 +0200838 def _set_location_header(main_topic, version, topic, id):
tiernof27c79b2018-03-12 17:08:42 +0100839 """
840 Insert response header Location with the URL of created item base on URL params
tiernob24258a2018-10-04 18:39:49 +0200841 :param main_topic:
tiernof27c79b2018-03-12 17:08:42 +0100842 :param version:
tiernob24258a2018-10-04 18:39:49 +0200843 :param topic:
tiernof27c79b2018-03-12 17:08:42 +0100844 :param id:
845 :return: None
846 """
847 # Use cherrypy.request.base for absoluted path and make use of request.header HOST just in case behind aNAT
tiernob24258a2018-10-04 18:39:49 +0200848 cherrypy.response.headers["Location"] = "/osm/{}/{}/{}/{}".format(main_topic, version, topic, id)
tiernof27c79b2018-03-12 17:08:42 +0100849 return
850
tierno65ca36d2019-02-12 19:27:52 +0100851 @staticmethod
tierno701018c2019-06-25 11:13:14 +0000852 def _extract_query_string_operations(kwargs, method):
853 """
854
855 :param kwargs:
856 :return:
857 """
858 query_string_operations = []
859 if kwargs:
860 for qs in ("FORCE", "PUBLIC", "ADMIN", "SET_PROJECT"):
861 if qs in kwargs and kwargs[qs].lower() != "false":
862 query_string_operations.append(qs.lower() + ":" + method.lower())
863 return query_string_operations
864
865 @staticmethod
866 def _manage_admin_query(token_info, kwargs, method, _id):
tierno65ca36d2019-02-12 19:27:52 +0100867 """
868 Processes the administrator query inputs (if any) of FORCE, ADMIN, PUBLIC, SET_PROJECT
869 Check that users has rights to use them and returs the admin_query
tierno701018c2019-06-25 11:13:14 +0000870 :param token_info: token_info rights obtained by token
tierno65ca36d2019-02-12 19:27:52 +0100871 :param kwargs: query string input.
872 :param method: http method: GET, POSST, PUT, ...
873 :param _id:
874 :return: admin_query dictionary with keys:
875 public: True, False or None
876 force: True or False
877 project_id: tuple with projects used for accessing an element
878 set_project: tuple with projects that a created element will belong to
879 method: show, list, delete, write
880 """
tierno701018c2019-06-25 11:13:14 +0000881 admin_query = {"force": False, "project_id": (token_info["project_id"], ), "username": token_info["username"],
delacruzramo029405d2019-09-26 10:52:56 +0200882 "admin": token_info["admin"], "public": None,
883 "allow_show_user_project_role": token_info["allow_show_user_project_role"]}
tierno65ca36d2019-02-12 19:27:52 +0100884 if kwargs:
885 # FORCE
886 if "FORCE" in kwargs:
887 if kwargs["FORCE"].lower() != "false": # if None or True set force to True
888 admin_query["force"] = True
889 del kwargs["FORCE"]
890 # PUBLIC
891 if "PUBLIC" in kwargs:
892 if kwargs["PUBLIC"].lower() != "false": # if None or True set public to True
893 admin_query["public"] = True
894 else:
895 admin_query["public"] = False
896 del kwargs["PUBLIC"]
897 # ADMIN
898 if "ADMIN" in kwargs:
899 behave_as = kwargs.pop("ADMIN")
900 if behave_as.lower() != "false":
tierno701018c2019-06-25 11:13:14 +0000901 if not token_info["admin"]:
tierno65ca36d2019-02-12 19:27:52 +0100902 raise NbiException("Only admin projects can use 'ADMIN' query string", HTTPStatus.UNAUTHORIZED)
903 if not behave_as or behave_as.lower() == "true": # convert True, None to empty list
904 admin_query["project_id"] = ()
905 elif isinstance(behave_as, (list, tuple)):
906 admin_query["project_id"] = behave_as
907 else: # isinstance(behave_as, str)
908 admin_query["project_id"] = (behave_as, )
909 if "SET_PROJECT" in kwargs:
910 set_project = kwargs.pop("SET_PROJECT")
911 if not set_project:
912 admin_query["set_project"] = list(admin_query["project_id"])
913 else:
914 if isinstance(set_project, str):
915 set_project = (set_project, )
916 if admin_query["project_id"]:
917 for p in set_project:
918 if p not in admin_query["project_id"]:
919 raise NbiException("Unauthorized for 'SET_PROJECT={p}'. Try with 'ADMIN=True' or "
920 "'ADMIN='{p}'".format(p=p), HTTPStatus.UNAUTHORIZED)
921 admin_query["set_project"] = set_project
922
923 # PROJECT_READ
924 # if "PROJECT_READ" in kwargs:
925 # admin_query["project"] = kwargs.pop("project")
tierno701018c2019-06-25 11:13:14 +0000926 # if admin_query["project"] == token_info["project_id"]:
tierno65ca36d2019-02-12 19:27:52 +0100927 if method == "GET":
928 if _id:
929 admin_query["method"] = "show"
930 else:
931 admin_query["method"] = "list"
932 elif method == "DELETE":
933 admin_query["method"] = "delete"
934 else:
935 admin_query["method"] = "write"
936 return admin_query
937
tiernoc94c3df2018-02-09 15:38:54 +0100938 @cherrypy.expose
tiernob24258a2018-10-04 18:39:49 +0200939 def default(self, main_topic=None, version=None, topic=None, _id=None, item=None, *args, **kwargs):
tierno701018c2019-06-25 11:13:14 +0000940 token_info = None
tiernof27c79b2018-03-12 17:08:42 +0100941 outdata = None
942 _format = None
tierno0f98af52018-03-19 10:28:22 +0100943 method = "DONE"
tiernob24258a2018-10-04 18:39:49 +0200944 engine_topic = None
tiernodb9dc582018-06-20 17:27:29 +0200945 rollback = []
tierno701018c2019-06-25 11:13:14 +0000946 engine_session = None
tiernoc94c3df2018-02-09 15:38:54 +0100947 try:
tiernob24258a2018-10-04 18:39:49 +0200948 if not main_topic or not version or not topic:
949 raise NbiException("URL must contain at least 'main_topic/version/topic'",
950 HTTPStatus.METHOD_NOT_ALLOWED)
vijay.r35ef2f72019-04-30 17:55:49 +0530951 if main_topic not in ("admin", "vnfpkgm", "nsd", "nslcm", "pdu", "nst", "nsilcm", "nspm"):
tiernob24258a2018-10-04 18:39:49 +0200952 raise NbiException("URL main_topic '{}' not supported".format(main_topic),
953 HTTPStatus.METHOD_NOT_ALLOWED)
tiernoc94c3df2018-02-09 15:38:54 +0100954 if version != 'v1':
955 raise NbiException("URL version '{}' not supported".format(version), HTTPStatus.METHOD_NOT_ALLOWED)
956
tiernof27c79b2018-03-12 17:08:42 +0100957 if kwargs and "METHOD" in kwargs and kwargs["METHOD"] in ("PUT", "POST", "DELETE", "GET", "PATCH"):
958 method = kwargs.pop("METHOD")
959 else:
960 method = cherrypy.request.method
tierno65ca36d2019-02-12 19:27:52 +0100961
tierno701018c2019-06-25 11:13:14 +0000962 role_permission = self._check_valid_url_method(method, main_topic, version, topic, _id, item, *args)
963 query_string_operations = self._extract_query_string_operations(kwargs, method)
tiernob24258a2018-10-04 18:39:49 +0200964 if main_topic == "admin" and topic == "tokens":
tiernof27c79b2018-03-12 17:08:42 +0100965 return self.token(method, _id, kwargs)
delacruzramo029405d2019-09-26 10:52:56 +0200966 token_info = self.authenticator.authorize(role_permission, query_string_operations, _id)
tierno701018c2019-06-25 11:13:14 +0000967 engine_session = self._manage_admin_query(token_info, kwargs, method, _id)
tiernof27c79b2018-03-12 17:08:42 +0100968 indata = self._format_in(kwargs)
tiernob24258a2018-10-04 18:39:49 +0200969 engine_topic = topic
970 if topic == "subscriptions":
971 engine_topic = main_topic + "_" + topic
vijay.r35ef2f72019-04-30 17:55:49 +0530972 if item and topic != "pm_jobs":
tiernob24258a2018-10-04 18:39:49 +0200973 engine_topic = item
tiernoc94c3df2018-02-09 15:38:54 +0100974
tiernob24258a2018-10-04 18:39:49 +0200975 if main_topic == "nsd":
976 engine_topic = "nsds"
977 elif main_topic == "vnfpkgm":
978 engine_topic = "vnfds"
979 elif main_topic == "nslcm":
980 engine_topic = "nsrs"
981 if topic == "ns_lcm_op_occs":
982 engine_topic = "nslcmops"
983 if topic == "vnfrs" or topic == "vnf_instances":
984 engine_topic = "vnfrs"
garciadeblas9750c5a2018-10-15 16:20:35 +0200985 elif main_topic == "nst":
986 engine_topic = "nsts"
987 elif main_topic == "nsilcm":
988 engine_topic = "nsis"
989 if topic == "nsi_lcm_op_occs":
delacruzramoc061f562019-04-05 11:00:02 +0200990 engine_topic = "nsilcmops"
tiernob24258a2018-10-04 18:39:49 +0200991 elif main_topic == "pdu":
992 engine_topic = "pdus"
tierno65ca36d2019-02-12 19:27:52 +0100993 if engine_topic == "vims": # TODO this is for backward compatibility, it will be removed in the future
tiernob24258a2018-10-04 18:39:49 +0200994 engine_topic = "vim_accounts"
tiernoc94c3df2018-02-09 15:38:54 +0100995
996 if method == "GET":
Felipe Vicens07f31722018-10-29 15:16:44 +0100997 if item in ("nsd_content", "package_content", "artifacts", "vnfd", "nsd", "nst", "nst_content"):
garciadeblas9750c5a2018-10-15 16:20:35 +0200998 if item in ("vnfd", "nsd", "nst"):
tiernof27c79b2018-03-12 17:08:42 +0100999 path = "$DESCRIPTOR"
1000 elif args:
1001 path = args
tiernob24258a2018-10-04 18:39:49 +02001002 elif item == "artifacts":
tiernof27c79b2018-03-12 17:08:42 +01001003 path = ()
1004 else:
1005 path = None
tierno701018c2019-06-25 11:13:14 +00001006 file, _format = self.engine.get_file(engine_session, engine_topic, _id, path,
tierno2236d202018-05-16 19:05:16 +02001007 cherrypy.request.headers.get("Accept"))
tiernof27c79b2018-03-12 17:08:42 +01001008 outdata = file
1009 elif not _id:
tierno701018c2019-06-25 11:13:14 +00001010 outdata = self.engine.get_item_list(engine_session, engine_topic, kwargs)
tiernoc94c3df2018-02-09 15:38:54 +01001011 else:
vijay.r35ef2f72019-04-30 17:55:49 +05301012 if item == "reports":
1013 # TODO check that project_id (_id in this context) has permissions
1014 _id = args[0]
tierno701018c2019-06-25 11:13:14 +00001015 outdata = self.engine.get_item(engine_session, engine_topic, _id)
tiernof27c79b2018-03-12 17:08:42 +01001016 elif method == "POST":
tiernobdebce92019-07-01 15:36:49 +00001017 cherrypy.response.status = HTTPStatus.CREATED.value
garciadeblas9750c5a2018-10-15 16:20:35 +02001018 if topic in ("ns_descriptors_content", "vnf_packages_content", "netslice_templates_content"):
tiernof27c79b2018-03-12 17:08:42 +01001019 _id = cherrypy.request.headers.get("Transaction-Id")
1020 if not _id:
tiernobdebce92019-07-01 15:36:49 +00001021 _id, _ = self.engine.new_item(rollback, engine_session, engine_topic, {}, None,
1022 cherrypy.request.headers)
tierno701018c2019-06-25 11:13:14 +00001023 completed = self.engine.upload_content(engine_session, engine_topic, _id, indata, kwargs,
tierno65ca36d2019-02-12 19:27:52 +01001024 cherrypy.request.headers)
tiernof27c79b2018-03-12 17:08:42 +01001025 if completed:
tiernob24258a2018-10-04 18:39:49 +02001026 self._set_location_header(main_topic, version, topic, _id)
tiernof27c79b2018-03-12 17:08:42 +01001027 else:
1028 cherrypy.response.headers["Transaction-Id"] = _id
1029 outdata = {"id": _id}
tiernob24258a2018-10-04 18:39:49 +02001030 elif topic == "ns_instances_content":
1031 # creates NSR
tiernobdebce92019-07-01 15:36:49 +00001032 _id, _ = self.engine.new_item(rollback, engine_session, engine_topic, indata, kwargs)
tiernob24258a2018-10-04 18:39:49 +02001033 # creates nslcmop
1034 indata["lcmOperationType"] = "instantiate"
1035 indata["nsInstanceId"] = _id
tiernobdebce92019-07-01 15:36:49 +00001036 nslcmop_id, _ = self.engine.new_item(rollback, engine_session, "nslcmops", indata, None)
tiernob24258a2018-10-04 18:39:49 +02001037 self._set_location_header(main_topic, version, topic, _id)
kuuse078f55e2019-05-16 19:24:21 +02001038 outdata = {"id": _id, "nslcmop_id": nslcmop_id}
tiernob24258a2018-10-04 18:39:49 +02001039 elif topic == "ns_instances" and item:
1040 indata["lcmOperationType"] = item
1041 indata["nsInstanceId"] = _id
tiernobdebce92019-07-01 15:36:49 +00001042 _id, _ = self.engine.new_item(rollback, engine_session, "nslcmops", indata, kwargs)
tiernob24258a2018-10-04 18:39:49 +02001043 self._set_location_header(main_topic, version, "ns_lcm_op_occs", _id)
tierno65acb4d2018-04-06 16:42:40 +02001044 outdata = {"id": _id}
1045 cherrypy.response.status = HTTPStatus.ACCEPTED.value
garciadeblas9750c5a2018-10-15 16:20:35 +02001046 elif topic == "netslice_instances_content":
Felipe Vicens07f31722018-10-29 15:16:44 +01001047 # creates NetSlice_Instance_record (NSIR)
tiernobdebce92019-07-01 15:36:49 +00001048 _id, _ = self.engine.new_item(rollback, engine_session, engine_topic, indata, kwargs)
Felipe Vicens07f31722018-10-29 15:16:44 +01001049 self._set_location_header(main_topic, version, topic, _id)
garciadeblas9750c5a2018-10-15 16:20:35 +02001050 indata["lcmOperationType"] = "instantiate"
Felipe Vicens126af572019-06-05 19:13:04 +02001051 indata["netsliceInstanceId"] = _id
tiernobdebce92019-07-01 15:36:49 +00001052 nsilcmop_id, _ = self.engine.new_item(rollback, engine_session, "nsilcmops", indata, kwargs)
kuuse078f55e2019-05-16 19:24:21 +02001053 outdata = {"id": _id, "nsilcmop_id": nsilcmop_id}
delacruzramoc061f562019-04-05 11:00:02 +02001054
garciadeblas9750c5a2018-10-15 16:20:35 +02001055 elif topic == "netslice_instances" and item:
1056 indata["lcmOperationType"] = item
Felipe Vicens126af572019-06-05 19:13:04 +02001057 indata["netsliceInstanceId"] = _id
tiernobdebce92019-07-01 15:36:49 +00001058 _id, _ = self.engine.new_item(rollback, engine_session, "nsilcmops", indata, kwargs)
garciadeblas9750c5a2018-10-15 16:20:35 +02001059 self._set_location_header(main_topic, version, "nsi_lcm_op_occs", _id)
1060 outdata = {"id": _id}
1061 cherrypy.response.status = HTTPStatus.ACCEPTED.value
tiernof27c79b2018-03-12 17:08:42 +01001062 else:
tiernobdebce92019-07-01 15:36:49 +00001063 _id, op_id = self.engine.new_item(rollback, engine_session, engine_topic, indata, kwargs,
1064 cherrypy.request.headers)
tiernob24258a2018-10-04 18:39:49 +02001065 self._set_location_header(main_topic, version, topic, _id)
tiernof27c79b2018-03-12 17:08:42 +01001066 outdata = {"id": _id}
tiernobdebce92019-07-01 15:36:49 +00001067 if op_id:
1068 outdata["op_id"] = op_id
1069 cherrypy.response.status = HTTPStatus.ACCEPTED.value
tiernob24258a2018-10-04 18:39:49 +02001070 # TODO form NsdInfo when topic in ("ns_descriptors", "vnf_packages")
tierno09c073e2018-04-26 13:36:48 +02001071
tiernoc94c3df2018-02-09 15:38:54 +01001072 elif method == "DELETE":
1073 if not _id:
tierno701018c2019-06-25 11:13:14 +00001074 outdata = self.engine.del_item_list(engine_session, engine_topic, kwargs)
tierno09c073e2018-04-26 13:36:48 +02001075 cherrypy.response.status = HTTPStatus.OK.value
tiernoc94c3df2018-02-09 15:38:54 +01001076 else: # len(args) > 1
tiernoe8631782018-12-21 13:31:52 +00001077 delete_in_process = False
tierno701018c2019-06-25 11:13:14 +00001078 if topic == "ns_instances_content" and not engine_session["force"]:
tiernob24258a2018-10-04 18:39:49 +02001079 nslcmop_desc = {
1080 "lcmOperationType": "terminate",
1081 "nsInstanceId": _id,
1082 "autoremove": True
1083 }
tiernobdebce92019-07-01 15:36:49 +00001084 opp_id, _ = self.engine.new_item(rollback, engine_session, "nslcmops", nslcmop_desc, None)
tiernoe8631782018-12-21 13:31:52 +00001085 if opp_id:
1086 delete_in_process = True
1087 outdata = {"_id": opp_id}
1088 cherrypy.response.status = HTTPStatus.ACCEPTED.value
tierno701018c2019-06-25 11:13:14 +00001089 elif topic == "netslice_instances_content" and not engine_session["force"]:
garciadeblas9750c5a2018-10-15 16:20:35 +02001090 nsilcmop_desc = {
1091 "lcmOperationType": "terminate",
Felipe Vicens126af572019-06-05 19:13:04 +02001092 "netsliceInstanceId": _id,
garciadeblas9750c5a2018-10-15 16:20:35 +02001093 "autoremove": True
1094 }
tiernobdebce92019-07-01 15:36:49 +00001095 opp_id, _ = self.engine.new_item(rollback, engine_session, "nsilcmops", nsilcmop_desc, None)
tiernoe8631782018-12-21 13:31:52 +00001096 if opp_id:
1097 delete_in_process = True
1098 outdata = {"_id": opp_id}
1099 cherrypy.response.status = HTTPStatus.ACCEPTED.value
1100 if not delete_in_process:
tierno701018c2019-06-25 11:13:14 +00001101 self.engine.del_item(engine_session, engine_topic, _id)
tierno09c073e2018-04-26 13:36:48 +02001102 cherrypy.response.status = HTTPStatus.NO_CONTENT.value
delacruzramofe598fe2019-10-23 18:25:11 +02001103 if engine_topic in ("vim_accounts", "wim_accounts", "sdns", "k8sclusters", "k8srepos"):
tierno09c073e2018-04-26 13:36:48 +02001104 cherrypy.response.status = HTTPStatus.ACCEPTED.value
1105
tierno7ae10112018-05-18 14:36:02 +02001106 elif method in ("PUT", "PATCH"):
tiernobdebce92019-07-01 15:36:49 +00001107 op_id = None
tierno701018c2019-06-25 11:13:14 +00001108 if not indata and not kwargs and not engine_session.get("set_project"):
tiernoc94c3df2018-02-09 15:38:54 +01001109 raise NbiException("Nothing to update. Provide payload and/or query string",
1110 HTTPStatus.BAD_REQUEST)
garciadeblas9750c5a2018-10-15 16:20:35 +02001111 if item in ("nsd_content", "package_content", "nst_content") and method == "PUT":
tierno701018c2019-06-25 11:13:14 +00001112 completed = self.engine.upload_content(engine_session, engine_topic, _id, indata, kwargs,
tierno65ca36d2019-02-12 19:27:52 +01001113 cherrypy.request.headers)
tiernof27c79b2018-03-12 17:08:42 +01001114 if not completed:
1115 cherrypy.response.headers["Transaction-Id"] = id
tiernof27c79b2018-03-12 17:08:42 +01001116 else:
tiernobdebce92019-07-01 15:36:49 +00001117 op_id = self.engine.edit_item(engine_session, engine_topic, _id, indata, kwargs)
1118
1119 if op_id:
1120 cherrypy.response.status = HTTPStatus.ACCEPTED.value
1121 outdata = {"op_id": op_id}
1122 else:
1123 cherrypy.response.status = HTTPStatus.NO_CONTENT.value
1124 outdata = None
tiernoc94c3df2018-02-09 15:38:54 +01001125 else:
1126 raise NbiException("Method {} not allowed".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
tiernoa6bb45d2019-06-14 09:45:39 +00001127
1128 # if Role information changes, it is needed to reload the information of roles
1129 if topic == "roles" and method != "GET":
1130 self.authenticator.load_operation_to_allowed_roles()
tierno701018c2019-06-25 11:13:14 +00001131 return self._format_out(outdata, token_info, _format)
tiernob24258a2018-10-04 18:39:49 +02001132 except Exception as e:
tierno36ec8602018-11-02 17:27:11 +01001133 if isinstance(e, (NbiException, EngineException, DbException, FsException, MsgException, AuthException,
delacruzramo01b15d32019-07-02 14:37:47 +02001134 ValidationError, AuthconnException)):
tiernob24258a2018-10-04 18:39:49 +02001135 http_code_value = cherrypy.response.status = e.http_code.value
1136 http_code_name = e.http_code.name
1137 cherrypy.log("Exception {}".format(e))
1138 else:
1139 http_code_value = cherrypy.response.status = HTTPStatus.BAD_REQUEST.value # INTERNAL_SERVER_ERROR
Felipe Vicense36ab852018-11-23 14:12:09 +01001140 cherrypy.log("CRITICAL: Exception {}".format(e), traceback=True)
tiernob24258a2018-10-04 18:39:49 +02001141 http_code_name = HTTPStatus.BAD_REQUEST.name
tierno3ace63c2018-05-03 17:51:43 +02001142 if hasattr(outdata, "close"): # is an open file
1143 outdata.close()
tiernob24258a2018-10-04 18:39:49 +02001144 error_text = str(e)
1145 rollback.reverse()
tiernodb9dc582018-06-20 17:27:29 +02001146 for rollback_item in rollback:
tierno3ace63c2018-05-03 17:51:43 +02001147 try:
tiernocc103432018-10-19 14:10:35 +02001148 if rollback_item.get("operation") == "set":
1149 self.engine.db.set_one(rollback_item["topic"], {"_id": rollback_item["_id"]},
1150 rollback_item["content"], fail_on_empty=False)
1151 else:
tiernoe8631782018-12-21 13:31:52 +00001152 self.engine.db.del_one(rollback_item["topic"], {"_id": rollback_item["_id"]},
1153 fail_on_empty=False)
tierno3ace63c2018-05-03 17:51:43 +02001154 except Exception as e2:
tiernob24258a2018-10-04 18:39:49 +02001155 rollback_error_text = "Rollback Exception {}: {}".format(rollback_item, e2)
1156 cherrypy.log(rollback_error_text)
1157 error_text += ". " + rollback_error_text
1158 # if isinstance(e, MsgException):
1159 # error_text = "{} has been '{}' but other modules cannot be informed because an error on bus".format(
1160 # engine_topic[:-1], method, error_text)
tiernoc94c3df2018-02-09 15:38:54 +01001161 problem_details = {
tiernob24258a2018-10-04 18:39:49 +02001162 "code": http_code_name,
1163 "status": http_code_value,
1164 "detail": error_text,
tiernoc94c3df2018-02-09 15:38:54 +01001165 }
tierno701018c2019-06-25 11:13:14 +00001166 return self._format_out(problem_details, token_info)
tiernoc94c3df2018-02-09 15:38:54 +01001167 # raise cherrypy.HTTPError(e.http_code.value, str(e))
tiernoa5035702019-07-29 08:54:42 +00001168 finally:
1169 if token_info:
1170 self._format_login(token_info)
1171 if method in ("PUT", "PATCH", "POST") and isinstance(outdata, dict):
1172 for logging_id in ("id", "op_id", "nsilcmop_id", "nslcmop_id"):
1173 if outdata.get(logging_id):
1174 cherrypy.request.login += ";{}={}".format(logging_id, outdata[logging_id][:36])
tiernoc94c3df2018-02-09 15:38:54 +01001175
1176
tierno7fd24872019-12-10 10:57:17 +00001177def _get_version():
1178 """
1179 Try to get version from package using pkg_resources (available with setuptools)
1180 """
1181 global nbi_version
1182 try:
1183 from pkg_resources import get_distribution
1184 nbi_version = get_distribution("osm_nbi").version
1185 except Exception:
1186 pass
1187
1188
tiernoc94c3df2018-02-09 15:38:54 +01001189def _start_service():
1190 """
1191 Callback function called when cherrypy.engine starts
1192 Override configuration with env variables
1193 Set database, storage, message configuration
1194 Init database with admin/admin user password
1195 """
tierno932499c2019-01-28 17:28:10 +00001196 global nbi_server
1197 global subscription_thread
tiernoc94c3df2018-02-09 15:38:54 +01001198 cherrypy.log.error("Starting osm_nbi")
1199 # update general cherrypy configuration
1200 update_dict = {}
1201
1202 engine_config = cherrypy.tree.apps['/osm'].config
1203 for k, v in environ.items():
1204 if not k.startswith("OSMNBI_"):
1205 continue
tiernoe1281182018-05-22 12:24:36 +02001206 k1, _, k2 = k[7:].lower().partition("_")
tiernoc94c3df2018-02-09 15:38:54 +01001207 if not k2:
1208 continue
1209 try:
1210 # update static configuration
1211 if k == 'OSMNBI_STATIC_DIR':
1212 engine_config["/static"]['tools.staticdir.dir'] = v
1213 engine_config["/static"]['tools.staticdir.on'] = True
1214 elif k == 'OSMNBI_SOCKET_PORT' or k == 'OSMNBI_SERVER_PORT':
1215 update_dict['server.socket_port'] = int(v)
1216 elif k == 'OSMNBI_SOCKET_HOST' or k == 'OSMNBI_SERVER_HOST':
1217 update_dict['server.socket_host'] = v
tiernof5298be2018-05-16 14:43:57 +02001218 elif k1 in ("server", "test", "auth", "log"):
1219 update_dict[k1 + '.' + k2] = v
Eduardo Sousa819d34c2018-07-31 01:20:02 +01001220 elif k1 in ("message", "database", "storage", "authentication"):
tiernof5298be2018-05-16 14:43:57 +02001221 # k2 = k2.replace('_', '.')
Eduardo Sousa819d34c2018-07-31 01:20:02 +01001222 if k2 in ("port", "db_port"):
tiernoc94c3df2018-02-09 15:38:54 +01001223 engine_config[k1][k2] = int(v)
1224 else:
1225 engine_config[k1][k2] = v
Eduardo Sousa819d34c2018-07-31 01:20:02 +01001226
tiernoc94c3df2018-02-09 15:38:54 +01001227 except ValueError as e:
1228 cherrypy.log.error("Ignoring environ '{}': " + str(e))
1229 except Exception as e:
1230 cherrypy.log.warn("skipping environ '{}' on exception '{}'".format(k, e))
1231
1232 if update_dict:
1233 cherrypy.config.update(update_dict)
tiernof5298be2018-05-16 14:43:57 +02001234 engine_config["global"].update(update_dict)
tiernoc94c3df2018-02-09 15:38:54 +01001235
1236 # logging cherrypy
1237 log_format_simple = "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(message)s"
1238 log_formatter_simple = logging.Formatter(log_format_simple, datefmt='%Y-%m-%dT%H:%M:%S')
1239 logger_server = logging.getLogger("cherrypy.error")
1240 logger_access = logging.getLogger("cherrypy.access")
1241 logger_cherry = logging.getLogger("cherrypy")
1242 logger_nbi = logging.getLogger("nbi")
1243
tiernof5298be2018-05-16 14:43:57 +02001244 if "log.file" in engine_config["global"]:
1245 file_handler = logging.handlers.RotatingFileHandler(engine_config["global"]["log.file"],
tiernoc94c3df2018-02-09 15:38:54 +01001246 maxBytes=100e6, backupCount=9, delay=0)
1247 file_handler.setFormatter(log_formatter_simple)
1248 logger_cherry.addHandler(file_handler)
1249 logger_nbi.addHandler(file_handler)
tiernob24258a2018-10-04 18:39:49 +02001250 # log always to standard output
1251 for format_, logger in {"nbi.server %(filename)s:%(lineno)s": logger_server,
1252 "nbi.access %(filename)s:%(lineno)s": logger_access,
1253 "%(name)s %(filename)s:%(lineno)s": logger_nbi
1254 }.items():
1255 log_format_cherry = "%(asctime)s %(levelname)s {} %(message)s".format(format_)
1256 log_formatter_cherry = logging.Formatter(log_format_cherry, datefmt='%Y-%m-%dT%H:%M:%S')
1257 str_handler = logging.StreamHandler()
1258 str_handler.setFormatter(log_formatter_cherry)
1259 logger.addHandler(str_handler)
tiernoc94c3df2018-02-09 15:38:54 +01001260
tiernof5298be2018-05-16 14:43:57 +02001261 if engine_config["global"].get("log.level"):
1262 logger_cherry.setLevel(engine_config["global"]["log.level"])
1263 logger_nbi.setLevel(engine_config["global"]["log.level"])
tiernoc94c3df2018-02-09 15:38:54 +01001264
1265 # logging other modules
1266 for k1, logname in {"message": "nbi.msg", "database": "nbi.db", "storage": "nbi.fs"}.items():
1267 engine_config[k1]["logger_name"] = logname
1268 logger_module = logging.getLogger(logname)
1269 if "logfile" in engine_config[k1]:
1270 file_handler = logging.handlers.RotatingFileHandler(engine_config[k1]["logfile"],
tierno2236d202018-05-16 19:05:16 +02001271 maxBytes=100e6, backupCount=9, delay=0)
tiernoc94c3df2018-02-09 15:38:54 +01001272 file_handler.setFormatter(log_formatter_simple)
1273 logger_module.addHandler(file_handler)
1274 if "loglevel" in engine_config[k1]:
1275 logger_module.setLevel(engine_config[k1]["loglevel"])
1276 # TODO add more entries, e.g.: storage
1277 cherrypy.tree.apps['/osm'].root.engine.start(engine_config)
Eduardo Sousa819d34c2018-07-31 01:20:02 +01001278 cherrypy.tree.apps['/osm'].root.authenticator.start(engine_config)
tiernod985a8d2018-10-19 14:12:28 +02001279 cherrypy.tree.apps['/osm'].root.engine.init_db(target_version=database_version)
1280 cherrypy.tree.apps['/osm'].root.authenticator.init_db(target_version=auth_database_version)
tiernobee508e2019-01-21 11:21:49 +00001281
tierno932499c2019-01-28 17:28:10 +00001282 # start subscriptions thread:
1283 subscription_thread = SubscriptionThread(config=engine_config, engine=nbi_server.engine)
1284 subscription_thread.start()
1285 # Do not capture except SubscriptionException
1286
tiernobee508e2019-01-21 11:21:49 +00001287 # load and print version. Ignore possible errors, e.g. file not found
1288 try:
tierno7fd24872019-12-10 10:57:17 +00001289 _get_version()
tierno9c630112019-08-29 14:21:41 +00001290 backend = engine_config["authentication"]["backend"]
tierno9c630112019-08-29 14:21:41 +00001291 cherrypy.log.error("Starting OSM NBI Version '{}' with '{}' authentication backend"
1292 .format(nbi_version + " " + nbi_version_date, backend))
tiernobee508e2019-01-21 11:21:49 +00001293 except Exception:
1294 pass
tiernoc94c3df2018-02-09 15:38:54 +01001295
1296
1297def _stop_service():
1298 """
1299 Callback function called when cherrypy.engine stops
1300 TODO: Ending database connections.
1301 """
tierno932499c2019-01-28 17:28:10 +00001302 global subscription_thread
tierno65ca36d2019-02-12 19:27:52 +01001303 if subscription_thread:
1304 subscription_thread.terminate()
tierno932499c2019-01-28 17:28:10 +00001305 subscription_thread = None
tiernoc94c3df2018-02-09 15:38:54 +01001306 cherrypy.tree.apps['/osm'].root.engine.stop()
1307 cherrypy.log.error("Stopping osm_nbi")
1308
tierno2236d202018-05-16 19:05:16 +02001309
tiernof5298be2018-05-16 14:43:57 +02001310def nbi(config_file):
tierno932499c2019-01-28 17:28:10 +00001311 global nbi_server
tiernoc94c3df2018-02-09 15:38:54 +01001312 # conf = {
1313 # '/': {
1314 # #'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
1315 # 'tools.sessions.on': True,
1316 # 'tools.response_headers.on': True,
1317 # # 'tools.response_headers.headers': [('Content-Type', 'text/plain')],
1318 # }
1319 # }
1320 # cherrypy.Server.ssl_module = 'builtin'
1321 # cherrypy.Server.ssl_certificate = "http/cert.pem"
1322 # cherrypy.Server.ssl_private_key = "http/privkey.pem"
1323 # cherrypy.Server.thread_pool = 10
1324 # cherrypy.config.update({'Server.socket_port': config["port"], 'Server.socket_host': config["host"]})
1325
1326 # cherrypy.config.update({'tools.auth_basic.on': True,
1327 # 'tools.auth_basic.realm': 'localhost',
1328 # 'tools.auth_basic.checkpassword': validate_password})
tierno932499c2019-01-28 17:28:10 +00001329 nbi_server = Server()
tiernoc94c3df2018-02-09 15:38:54 +01001330 cherrypy.engine.subscribe('start', _start_service)
1331 cherrypy.engine.subscribe('stop', _stop_service)
tierno932499c2019-01-28 17:28:10 +00001332 cherrypy.quickstart(nbi_server, '/osm', config_file)
tiernof5298be2018-05-16 14:43:57 +02001333
1334
1335def usage():
1336 print("""Usage: {} [options]
1337 -c|--config [configuration_file]: loads the configuration file (default: ./nbi.cfg)
1338 -h|--help: shows this help
1339 """.format(sys.argv[0]))
tierno2236d202018-05-16 19:05:16 +02001340 # --log-socket-host HOST: send logs to this host")
1341 # --log-socket-port PORT: send logs using this port (default: 9022)")
tiernoc94c3df2018-02-09 15:38:54 +01001342
1343
1344if __name__ == '__main__':
tiernof5298be2018-05-16 14:43:57 +02001345 try:
1346 # load parameters and configuration
1347 opts, args = getopt.getopt(sys.argv[1:], "hvc:", ["config=", "help"])
1348 # TODO add "log-socket-host=", "log-socket-port=", "log-file="
1349 config_file = None
1350 for o, a in opts:
1351 if o in ("-h", "--help"):
1352 usage()
1353 sys.exit()
1354 elif o in ("-c", "--config"):
1355 config_file = a
1356 # elif o == "--log-socket-port":
1357 # log_socket_port = a
1358 # elif o == "--log-socket-host":
1359 # log_socket_host = a
1360 # elif o == "--log-file":
1361 # log_file = a
1362 else:
1363 assert False, "Unhandled option"
1364 if config_file:
1365 if not path.isfile(config_file):
1366 print("configuration file '{}' that not exist".format(config_file), file=sys.stderr)
1367 exit(1)
1368 else:
1369 for config_file in (__file__[:__file__.rfind(".")] + ".cfg", "./nbi.cfg", "/etc/osm/nbi.cfg"):
1370 if path.isfile(config_file):
1371 break
1372 else:
1373 print("No configuration file 'nbi.cfg' found neither at local folder nor at /etc/osm/", file=sys.stderr)
1374 exit(1)
1375 nbi(config_file)
1376 except getopt.GetoptError as e:
1377 print(str(e), file=sys.stderr)
1378 # usage()
1379 exit(1)