blob: f5d229338dcf89c8a169bb209ba0a7d49585b2f1 [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)
delacruzramoad682a52019-12-10 16:26:34 +0100487 self.engine = Engine(self.authenticator)
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):
tierno4836bac2020-01-15 14:41:48 +0000719 if not cherrypy.config.get("server.enable_test") or (isinstance(cherrypy.config["server.enable_test"], str) and
720 cherrypy.config["server.enable_test"].lower() == "false"):
721 cherrypy.response.status = HTTPStatus.METHOD_NOT_ALLOWED.value
722 return "test URL is disabled"
tiernoc94c3df2018-02-09 15:38:54 +0100723 thread_info = None
tiernof27c79b2018-03-12 17:08:42 +0100724 if args and args[0] == "help":
tiernof717cbe2018-12-03 16:35:42 +0000725 return "<html><pre>\ninit\nfile/<name> download file\ndb-clear/table\nfs-clear[/folder]\nlogin\nlogin2\n"\
tiernoe1281182018-05-22 12:24:36 +0200726 "sleep/<time>\nmessage/topic\n</pre></html>"
tiernof27c79b2018-03-12 17:08:42 +0100727
728 elif args and args[0] == "init":
tiernoc94c3df2018-02-09 15:38:54 +0100729 try:
730 # self.engine.load_dbase(cherrypy.request.app.config)
731 self.engine.create_admin()
732 return "Done. User 'admin', password 'admin' created"
733 except Exception:
734 cherrypy.response.status = HTTPStatus.FORBIDDEN.value
735 return self._format_out("Database already initialized")
tiernof27c79b2018-03-12 17:08:42 +0100736 elif args and args[0] == "file":
737 return cherrypy.lib.static.serve_file(cherrypy.tree.apps['/osm'].config["storage"]["path"] + "/" + args[1],
738 "text/plain", "attachment")
739 elif args and args[0] == "file2":
740 f_path = cherrypy.tree.apps['/osm'].config["storage"]["path"] + "/" + args[1]
741 f = open(f_path, "r")
742 cherrypy.response.headers["Content-type"] = "text/plain"
tiernof27c79b2018-03-12 17:08:42 +0100743 return f
tierno55945e72018-04-06 16:40:27 +0200744
tiernof27c79b2018-03-12 17:08:42 +0100745 elif len(args) == 2 and args[0] == "db-clear":
tiernof717cbe2018-12-03 16:35:42 +0000746 deleted_info = self.engine.db.del_list(args[1], kwargs)
747 return "{} {} deleted\n".format(deleted_info["deleted"], args[1])
748 elif len(args) and args[0] == "fs-clear":
749 if len(args) >= 2:
750 folders = (args[1],)
751 else:
752 folders = self.engine.fs.dir_ls(".")
753 for folder in folders:
754 self.engine.fs.file_delete(folder)
755 return ",".join(folders) + " folders deleted\n"
tiernoc94c3df2018-02-09 15:38:54 +0100756 elif args and args[0] == "login":
757 if not cherrypy.request.headers.get("Authorization"):
758 cherrypy.response.headers["WWW-Authenticate"] = 'Basic realm="Access to OSM site", charset="UTF-8"'
759 cherrypy.response.status = HTTPStatus.UNAUTHORIZED.value
760 elif args and args[0] == "login2":
761 if not cherrypy.request.headers.get("Authorization"):
762 cherrypy.response.headers["WWW-Authenticate"] = 'Bearer realm="Access to OSM site"'
763 cherrypy.response.status = HTTPStatus.UNAUTHORIZED.value
764 elif args and args[0] == "sleep":
765 sleep_time = 5
766 try:
767 sleep_time = int(args[1])
768 except Exception:
769 cherrypy.response.status = HTTPStatus.FORBIDDEN.value
770 return self._format_out("Database already initialized")
771 thread_info = cherrypy.thread_data
772 print(thread_info)
773 time.sleep(sleep_time)
774 # thread_info
775 elif len(args) >= 2 and args[0] == "message":
tiernob24258a2018-10-04 18:39:49 +0200776 main_topic = args[1]
777 return_text = "<html><pre>{} ->\n".format(main_topic)
tiernoc94c3df2018-02-09 15:38:54 +0100778 try:
tierno55945e72018-04-06 16:40:27 +0200779 if cherrypy.request.method == 'POST':
delacruzramob19cadc2019-10-08 10:18:02 +0200780 to_send = yaml.load(cherrypy.request.body, Loader=yaml.SafeLoader)
tierno55945e72018-04-06 16:40:27 +0200781 for k, v in to_send.items():
tiernob24258a2018-10-04 18:39:49 +0200782 self.engine.msg.write(main_topic, k, v)
tierno55945e72018-04-06 16:40:27 +0200783 return_text += " {}: {}\n".format(k, v)
784 elif cherrypy.request.method == 'GET':
785 for k, v in kwargs.items():
delacruzramob19cadc2019-10-08 10:18:02 +0200786 self.engine.msg.write(main_topic, k, yaml.load(v), Loader=yaml.SafeLoader)
787 return_text += " {}: {}\n".format(k, yaml.load(v), Loader=yaml.SafeLoader)
tiernoc94c3df2018-02-09 15:38:54 +0100788 except Exception as e:
tierno55945e72018-04-06 16:40:27 +0200789 return_text += "Error: " + str(e)
790 return_text += "</pre></html>\n"
791 return return_text
tiernoc94c3df2018-02-09 15:38:54 +0100792
793 return_text = (
794 "<html><pre>\nheaders:\n args: {}\n".format(args) +
795 " kwargs: {}\n".format(kwargs) +
796 " headers: {}\n".format(cherrypy.request.headers) +
797 " path_info: {}\n".format(cherrypy.request.path_info) +
798 " query_string: {}\n".format(cherrypy.request.query_string) +
799 " session: {}\n".format(cherrypy.session) +
800 " cookie: {}\n".format(cherrypy.request.cookie) +
801 " method: {}\n".format(cherrypy.request.method) +
tiernof717cbe2018-12-03 16:35:42 +0000802 " session: {}\n".format(cherrypy.session.get('fieldname')) +
tiernoc94c3df2018-02-09 15:38:54 +0100803 " body:\n")
804 return_text += " length: {}\n".format(cherrypy.request.body.length)
805 if cherrypy.request.body.length:
806 return_text += " content: {}\n".format(
807 str(cherrypy.request.body.read(int(cherrypy.request.headers.get('Content-Length', 0)))))
808 if thread_info:
809 return_text += "thread: {}\n".format(thread_info)
810 return_text += "</pre></html>"
811 return return_text
812
tierno701018c2019-06-25 11:13:14 +0000813 @staticmethod
814 def _check_valid_url_method(method, *args):
tiernof27c79b2018-03-12 17:08:42 +0100815 if len(args) < 3:
tiernob24258a2018-10-04 18:39:49 +0200816 raise NbiException("URL must contain at least 'main_topic/version/topic'", HTTPStatus.METHOD_NOT_ALLOWED)
tiernof27c79b2018-03-12 17:08:42 +0100817
tierno701018c2019-06-25 11:13:14 +0000818 reference = valid_url_methods
tiernof27c79b2018-03-12 17:08:42 +0100819 for arg in args:
820 if arg is None:
821 break
822 if not isinstance(reference, dict):
823 raise NbiException("URL contains unexpected extra items '{}'".format(arg),
824 HTTPStatus.METHOD_NOT_ALLOWED)
825
826 if arg in reference:
827 reference = reference[arg]
828 elif "<ID>" in reference:
829 reference = reference["<ID>"]
830 elif "*" in reference:
831 reference = reference["*"]
832 break
833 else:
834 raise NbiException("Unexpected URL item {}".format(arg), HTTPStatus.METHOD_NOT_ALLOWED)
835 if "TODO" in reference and method in reference["TODO"]:
836 raise NbiException("Method {} not supported yet for this URL".format(method), HTTPStatus.NOT_IMPLEMENTED)
tierno2236d202018-05-16 19:05:16 +0200837 elif "METHODS" in reference and method not in reference["METHODS"]:
tiernof27c79b2018-03-12 17:08:42 +0100838 raise NbiException("Method {} not supported for this URL".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
tierno701018c2019-06-25 11:13:14 +0000839 return reference["ROLE_PERMISSION"] + method.lower()
tiernof27c79b2018-03-12 17:08:42 +0100840
841 @staticmethod
tiernob24258a2018-10-04 18:39:49 +0200842 def _set_location_header(main_topic, version, topic, id):
tiernof27c79b2018-03-12 17:08:42 +0100843 """
844 Insert response header Location with the URL of created item base on URL params
tiernob24258a2018-10-04 18:39:49 +0200845 :param main_topic:
tiernof27c79b2018-03-12 17:08:42 +0100846 :param version:
tiernob24258a2018-10-04 18:39:49 +0200847 :param topic:
tiernof27c79b2018-03-12 17:08:42 +0100848 :param id:
849 :return: None
850 """
851 # 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 +0200852 cherrypy.response.headers["Location"] = "/osm/{}/{}/{}/{}".format(main_topic, version, topic, id)
tiernof27c79b2018-03-12 17:08:42 +0100853 return
854
tierno65ca36d2019-02-12 19:27:52 +0100855 @staticmethod
tierno701018c2019-06-25 11:13:14 +0000856 def _extract_query_string_operations(kwargs, method):
857 """
858
859 :param kwargs:
860 :return:
861 """
862 query_string_operations = []
863 if kwargs:
864 for qs in ("FORCE", "PUBLIC", "ADMIN", "SET_PROJECT"):
865 if qs in kwargs and kwargs[qs].lower() != "false":
866 query_string_operations.append(qs.lower() + ":" + method.lower())
867 return query_string_operations
868
869 @staticmethod
870 def _manage_admin_query(token_info, kwargs, method, _id):
tierno65ca36d2019-02-12 19:27:52 +0100871 """
872 Processes the administrator query inputs (if any) of FORCE, ADMIN, PUBLIC, SET_PROJECT
873 Check that users has rights to use them and returs the admin_query
tierno701018c2019-06-25 11:13:14 +0000874 :param token_info: token_info rights obtained by token
tierno65ca36d2019-02-12 19:27:52 +0100875 :param kwargs: query string input.
876 :param method: http method: GET, POSST, PUT, ...
877 :param _id:
878 :return: admin_query dictionary with keys:
879 public: True, False or None
880 force: True or False
881 project_id: tuple with projects used for accessing an element
882 set_project: tuple with projects that a created element will belong to
883 method: show, list, delete, write
884 """
tierno701018c2019-06-25 11:13:14 +0000885 admin_query = {"force": False, "project_id": (token_info["project_id"], ), "username": token_info["username"],
delacruzramo029405d2019-09-26 10:52:56 +0200886 "admin": token_info["admin"], "public": None,
887 "allow_show_user_project_role": token_info["allow_show_user_project_role"]}
tierno65ca36d2019-02-12 19:27:52 +0100888 if kwargs:
889 # FORCE
890 if "FORCE" in kwargs:
891 if kwargs["FORCE"].lower() != "false": # if None or True set force to True
892 admin_query["force"] = True
893 del kwargs["FORCE"]
894 # PUBLIC
895 if "PUBLIC" in kwargs:
896 if kwargs["PUBLIC"].lower() != "false": # if None or True set public to True
897 admin_query["public"] = True
898 else:
899 admin_query["public"] = False
900 del kwargs["PUBLIC"]
901 # ADMIN
902 if "ADMIN" in kwargs:
903 behave_as = kwargs.pop("ADMIN")
904 if behave_as.lower() != "false":
tierno701018c2019-06-25 11:13:14 +0000905 if not token_info["admin"]:
tierno65ca36d2019-02-12 19:27:52 +0100906 raise NbiException("Only admin projects can use 'ADMIN' query string", HTTPStatus.UNAUTHORIZED)
907 if not behave_as or behave_as.lower() == "true": # convert True, None to empty list
908 admin_query["project_id"] = ()
909 elif isinstance(behave_as, (list, tuple)):
910 admin_query["project_id"] = behave_as
911 else: # isinstance(behave_as, str)
912 admin_query["project_id"] = (behave_as, )
913 if "SET_PROJECT" in kwargs:
914 set_project = kwargs.pop("SET_PROJECT")
915 if not set_project:
916 admin_query["set_project"] = list(admin_query["project_id"])
917 else:
918 if isinstance(set_project, str):
919 set_project = (set_project, )
920 if admin_query["project_id"]:
921 for p in set_project:
922 if p not in admin_query["project_id"]:
923 raise NbiException("Unauthorized for 'SET_PROJECT={p}'. Try with 'ADMIN=True' or "
924 "'ADMIN='{p}'".format(p=p), HTTPStatus.UNAUTHORIZED)
925 admin_query["set_project"] = set_project
926
927 # PROJECT_READ
928 # if "PROJECT_READ" in kwargs:
929 # admin_query["project"] = kwargs.pop("project")
tierno701018c2019-06-25 11:13:14 +0000930 # if admin_query["project"] == token_info["project_id"]:
tierno65ca36d2019-02-12 19:27:52 +0100931 if method == "GET":
932 if _id:
933 admin_query["method"] = "show"
934 else:
935 admin_query["method"] = "list"
936 elif method == "DELETE":
937 admin_query["method"] = "delete"
938 else:
939 admin_query["method"] = "write"
940 return admin_query
941
tiernoc94c3df2018-02-09 15:38:54 +0100942 @cherrypy.expose
tiernob24258a2018-10-04 18:39:49 +0200943 def default(self, main_topic=None, version=None, topic=None, _id=None, item=None, *args, **kwargs):
tierno701018c2019-06-25 11:13:14 +0000944 token_info = None
tiernof27c79b2018-03-12 17:08:42 +0100945 outdata = None
946 _format = None
tierno0f98af52018-03-19 10:28:22 +0100947 method = "DONE"
tiernob24258a2018-10-04 18:39:49 +0200948 engine_topic = None
tiernodb9dc582018-06-20 17:27:29 +0200949 rollback = []
tierno701018c2019-06-25 11:13:14 +0000950 engine_session = None
tiernoc94c3df2018-02-09 15:38:54 +0100951 try:
tiernob24258a2018-10-04 18:39:49 +0200952 if not main_topic or not version or not topic:
953 raise NbiException("URL must contain at least 'main_topic/version/topic'",
954 HTTPStatus.METHOD_NOT_ALLOWED)
vijay.r35ef2f72019-04-30 17:55:49 +0530955 if main_topic not in ("admin", "vnfpkgm", "nsd", "nslcm", "pdu", "nst", "nsilcm", "nspm"):
tiernob24258a2018-10-04 18:39:49 +0200956 raise NbiException("URL main_topic '{}' not supported".format(main_topic),
957 HTTPStatus.METHOD_NOT_ALLOWED)
tiernoc94c3df2018-02-09 15:38:54 +0100958 if version != 'v1':
959 raise NbiException("URL version '{}' not supported".format(version), HTTPStatus.METHOD_NOT_ALLOWED)
960
tiernof27c79b2018-03-12 17:08:42 +0100961 if kwargs and "METHOD" in kwargs and kwargs["METHOD"] in ("PUT", "POST", "DELETE", "GET", "PATCH"):
962 method = kwargs.pop("METHOD")
963 else:
964 method = cherrypy.request.method
tierno65ca36d2019-02-12 19:27:52 +0100965
tierno701018c2019-06-25 11:13:14 +0000966 role_permission = self._check_valid_url_method(method, main_topic, version, topic, _id, item, *args)
967 query_string_operations = self._extract_query_string_operations(kwargs, method)
tiernob24258a2018-10-04 18:39:49 +0200968 if main_topic == "admin" and topic == "tokens":
tiernof27c79b2018-03-12 17:08:42 +0100969 return self.token(method, _id, kwargs)
delacruzramo029405d2019-09-26 10:52:56 +0200970 token_info = self.authenticator.authorize(role_permission, query_string_operations, _id)
tierno701018c2019-06-25 11:13:14 +0000971 engine_session = self._manage_admin_query(token_info, kwargs, method, _id)
tiernof27c79b2018-03-12 17:08:42 +0100972 indata = self._format_in(kwargs)
tiernob24258a2018-10-04 18:39:49 +0200973 engine_topic = topic
974 if topic == "subscriptions":
975 engine_topic = main_topic + "_" + topic
vijay.r35ef2f72019-04-30 17:55:49 +0530976 if item and topic != "pm_jobs":
tiernob24258a2018-10-04 18:39:49 +0200977 engine_topic = item
tiernoc94c3df2018-02-09 15:38:54 +0100978
tiernob24258a2018-10-04 18:39:49 +0200979 if main_topic == "nsd":
980 engine_topic = "nsds"
981 elif main_topic == "vnfpkgm":
982 engine_topic = "vnfds"
983 elif main_topic == "nslcm":
984 engine_topic = "nsrs"
985 if topic == "ns_lcm_op_occs":
986 engine_topic = "nslcmops"
987 if topic == "vnfrs" or topic == "vnf_instances":
988 engine_topic = "vnfrs"
garciadeblas9750c5a2018-10-15 16:20:35 +0200989 elif main_topic == "nst":
990 engine_topic = "nsts"
991 elif main_topic == "nsilcm":
992 engine_topic = "nsis"
993 if topic == "nsi_lcm_op_occs":
delacruzramoc061f562019-04-05 11:00:02 +0200994 engine_topic = "nsilcmops"
tiernob24258a2018-10-04 18:39:49 +0200995 elif main_topic == "pdu":
996 engine_topic = "pdus"
tierno65ca36d2019-02-12 19:27:52 +0100997 if engine_topic == "vims": # TODO this is for backward compatibility, it will be removed in the future
tiernob24258a2018-10-04 18:39:49 +0200998 engine_topic = "vim_accounts"
tiernoc94c3df2018-02-09 15:38:54 +0100999
1000 if method == "GET":
Felipe Vicens07f31722018-10-29 15:16:44 +01001001 if item in ("nsd_content", "package_content", "artifacts", "vnfd", "nsd", "nst", "nst_content"):
garciadeblas9750c5a2018-10-15 16:20:35 +02001002 if item in ("vnfd", "nsd", "nst"):
tiernof27c79b2018-03-12 17:08:42 +01001003 path = "$DESCRIPTOR"
1004 elif args:
1005 path = args
tiernob24258a2018-10-04 18:39:49 +02001006 elif item == "artifacts":
tiernof27c79b2018-03-12 17:08:42 +01001007 path = ()
1008 else:
1009 path = None
tierno701018c2019-06-25 11:13:14 +00001010 file, _format = self.engine.get_file(engine_session, engine_topic, _id, path,
tierno2236d202018-05-16 19:05:16 +02001011 cherrypy.request.headers.get("Accept"))
tiernof27c79b2018-03-12 17:08:42 +01001012 outdata = file
1013 elif not _id:
tierno701018c2019-06-25 11:13:14 +00001014 outdata = self.engine.get_item_list(engine_session, engine_topic, kwargs)
tiernoc94c3df2018-02-09 15:38:54 +01001015 else:
vijay.r35ef2f72019-04-30 17:55:49 +05301016 if item == "reports":
1017 # TODO check that project_id (_id in this context) has permissions
1018 _id = args[0]
tierno701018c2019-06-25 11:13:14 +00001019 outdata = self.engine.get_item(engine_session, engine_topic, _id)
tiernof27c79b2018-03-12 17:08:42 +01001020 elif method == "POST":
tiernobdebce92019-07-01 15:36:49 +00001021 cherrypy.response.status = HTTPStatus.CREATED.value
garciadeblas9750c5a2018-10-15 16:20:35 +02001022 if topic in ("ns_descriptors_content", "vnf_packages_content", "netslice_templates_content"):
tiernof27c79b2018-03-12 17:08:42 +01001023 _id = cherrypy.request.headers.get("Transaction-Id")
1024 if not _id:
tiernobdebce92019-07-01 15:36:49 +00001025 _id, _ = self.engine.new_item(rollback, engine_session, engine_topic, {}, None,
1026 cherrypy.request.headers)
tierno701018c2019-06-25 11:13:14 +00001027 completed = self.engine.upload_content(engine_session, engine_topic, _id, indata, kwargs,
tierno65ca36d2019-02-12 19:27:52 +01001028 cherrypy.request.headers)
tiernof27c79b2018-03-12 17:08:42 +01001029 if completed:
tiernob24258a2018-10-04 18:39:49 +02001030 self._set_location_header(main_topic, version, topic, _id)
tiernof27c79b2018-03-12 17:08:42 +01001031 else:
1032 cherrypy.response.headers["Transaction-Id"] = _id
1033 outdata = {"id": _id}
tiernob24258a2018-10-04 18:39:49 +02001034 elif topic == "ns_instances_content":
1035 # creates NSR
tiernobdebce92019-07-01 15:36:49 +00001036 _id, _ = self.engine.new_item(rollback, engine_session, engine_topic, indata, kwargs)
tiernob24258a2018-10-04 18:39:49 +02001037 # creates nslcmop
1038 indata["lcmOperationType"] = "instantiate"
1039 indata["nsInstanceId"] = _id
tiernobdebce92019-07-01 15:36:49 +00001040 nslcmop_id, _ = self.engine.new_item(rollback, engine_session, "nslcmops", indata, None)
tiernob24258a2018-10-04 18:39:49 +02001041 self._set_location_header(main_topic, version, topic, _id)
kuuse078f55e2019-05-16 19:24:21 +02001042 outdata = {"id": _id, "nslcmop_id": nslcmop_id}
tiernob24258a2018-10-04 18:39:49 +02001043 elif topic == "ns_instances" and item:
1044 indata["lcmOperationType"] = item
1045 indata["nsInstanceId"] = _id
tiernobdebce92019-07-01 15:36:49 +00001046 _id, _ = self.engine.new_item(rollback, engine_session, "nslcmops", indata, kwargs)
tiernob24258a2018-10-04 18:39:49 +02001047 self._set_location_header(main_topic, version, "ns_lcm_op_occs", _id)
tierno65acb4d2018-04-06 16:42:40 +02001048 outdata = {"id": _id}
1049 cherrypy.response.status = HTTPStatus.ACCEPTED.value
garciadeblas9750c5a2018-10-15 16:20:35 +02001050 elif topic == "netslice_instances_content":
Felipe Vicens07f31722018-10-29 15:16:44 +01001051 # creates NetSlice_Instance_record (NSIR)
tiernobdebce92019-07-01 15:36:49 +00001052 _id, _ = self.engine.new_item(rollback, engine_session, engine_topic, indata, kwargs)
Felipe Vicens07f31722018-10-29 15:16:44 +01001053 self._set_location_header(main_topic, version, topic, _id)
garciadeblas9750c5a2018-10-15 16:20:35 +02001054 indata["lcmOperationType"] = "instantiate"
Felipe Vicens126af572019-06-05 19:13:04 +02001055 indata["netsliceInstanceId"] = _id
tiernobdebce92019-07-01 15:36:49 +00001056 nsilcmop_id, _ = self.engine.new_item(rollback, engine_session, "nsilcmops", indata, kwargs)
kuuse078f55e2019-05-16 19:24:21 +02001057 outdata = {"id": _id, "nsilcmop_id": nsilcmop_id}
delacruzramoc061f562019-04-05 11:00:02 +02001058
garciadeblas9750c5a2018-10-15 16:20:35 +02001059 elif topic == "netslice_instances" and item:
1060 indata["lcmOperationType"] = item
Felipe Vicens126af572019-06-05 19:13:04 +02001061 indata["netsliceInstanceId"] = _id
tiernobdebce92019-07-01 15:36:49 +00001062 _id, _ = self.engine.new_item(rollback, engine_session, "nsilcmops", indata, kwargs)
garciadeblas9750c5a2018-10-15 16:20:35 +02001063 self._set_location_header(main_topic, version, "nsi_lcm_op_occs", _id)
1064 outdata = {"id": _id}
1065 cherrypy.response.status = HTTPStatus.ACCEPTED.value
tiernof27c79b2018-03-12 17:08:42 +01001066 else:
tiernobdebce92019-07-01 15:36:49 +00001067 _id, op_id = self.engine.new_item(rollback, engine_session, engine_topic, indata, kwargs,
1068 cherrypy.request.headers)
tiernob24258a2018-10-04 18:39:49 +02001069 self._set_location_header(main_topic, version, topic, _id)
tiernof27c79b2018-03-12 17:08:42 +01001070 outdata = {"id": _id}
tiernobdebce92019-07-01 15:36:49 +00001071 if op_id:
1072 outdata["op_id"] = op_id
1073 cherrypy.response.status = HTTPStatus.ACCEPTED.value
tiernob24258a2018-10-04 18:39:49 +02001074 # TODO form NsdInfo when topic in ("ns_descriptors", "vnf_packages")
tierno09c073e2018-04-26 13:36:48 +02001075
tiernoc94c3df2018-02-09 15:38:54 +01001076 elif method == "DELETE":
1077 if not _id:
tierno701018c2019-06-25 11:13:14 +00001078 outdata = self.engine.del_item_list(engine_session, engine_topic, kwargs)
tierno09c073e2018-04-26 13:36:48 +02001079 cherrypy.response.status = HTTPStatus.OK.value
tiernoc94c3df2018-02-09 15:38:54 +01001080 else: # len(args) > 1
tiernoe8631782018-12-21 13:31:52 +00001081 delete_in_process = False
tierno701018c2019-06-25 11:13:14 +00001082 if topic == "ns_instances_content" and not engine_session["force"]:
tiernob24258a2018-10-04 18:39:49 +02001083 nslcmop_desc = {
1084 "lcmOperationType": "terminate",
1085 "nsInstanceId": _id,
1086 "autoremove": True
1087 }
tiernobdebce92019-07-01 15:36:49 +00001088 opp_id, _ = self.engine.new_item(rollback, engine_session, "nslcmops", nslcmop_desc, None)
tiernoe8631782018-12-21 13:31:52 +00001089 if opp_id:
1090 delete_in_process = True
1091 outdata = {"_id": opp_id}
1092 cherrypy.response.status = HTTPStatus.ACCEPTED.value
tierno701018c2019-06-25 11:13:14 +00001093 elif topic == "netslice_instances_content" and not engine_session["force"]:
garciadeblas9750c5a2018-10-15 16:20:35 +02001094 nsilcmop_desc = {
1095 "lcmOperationType": "terminate",
Felipe Vicens126af572019-06-05 19:13:04 +02001096 "netsliceInstanceId": _id,
garciadeblas9750c5a2018-10-15 16:20:35 +02001097 "autoremove": True
1098 }
tiernobdebce92019-07-01 15:36:49 +00001099 opp_id, _ = self.engine.new_item(rollback, engine_session, "nsilcmops", nsilcmop_desc, None)
tiernoe8631782018-12-21 13:31:52 +00001100 if opp_id:
1101 delete_in_process = True
1102 outdata = {"_id": opp_id}
1103 cherrypy.response.status = HTTPStatus.ACCEPTED.value
1104 if not delete_in_process:
tierno701018c2019-06-25 11:13:14 +00001105 self.engine.del_item(engine_session, engine_topic, _id)
tierno09c073e2018-04-26 13:36:48 +02001106 cherrypy.response.status = HTTPStatus.NO_CONTENT.value
delacruzramofe598fe2019-10-23 18:25:11 +02001107 if engine_topic in ("vim_accounts", "wim_accounts", "sdns", "k8sclusters", "k8srepos"):
tierno09c073e2018-04-26 13:36:48 +02001108 cherrypy.response.status = HTTPStatus.ACCEPTED.value
1109
tierno7ae10112018-05-18 14:36:02 +02001110 elif method in ("PUT", "PATCH"):
tiernobdebce92019-07-01 15:36:49 +00001111 op_id = None
tierno701018c2019-06-25 11:13:14 +00001112 if not indata and not kwargs and not engine_session.get("set_project"):
tiernoc94c3df2018-02-09 15:38:54 +01001113 raise NbiException("Nothing to update. Provide payload and/or query string",
1114 HTTPStatus.BAD_REQUEST)
garciadeblas9750c5a2018-10-15 16:20:35 +02001115 if item in ("nsd_content", "package_content", "nst_content") and method == "PUT":
tierno701018c2019-06-25 11:13:14 +00001116 completed = self.engine.upload_content(engine_session, engine_topic, _id, indata, kwargs,
tierno65ca36d2019-02-12 19:27:52 +01001117 cherrypy.request.headers)
tiernof27c79b2018-03-12 17:08:42 +01001118 if not completed:
1119 cherrypy.response.headers["Transaction-Id"] = id
tiernof27c79b2018-03-12 17:08:42 +01001120 else:
tiernobdebce92019-07-01 15:36:49 +00001121 op_id = self.engine.edit_item(engine_session, engine_topic, _id, indata, kwargs)
1122
1123 if op_id:
1124 cherrypy.response.status = HTTPStatus.ACCEPTED.value
1125 outdata = {"op_id": op_id}
1126 else:
1127 cherrypy.response.status = HTTPStatus.NO_CONTENT.value
1128 outdata = None
tiernoc94c3df2018-02-09 15:38:54 +01001129 else:
1130 raise NbiException("Method {} not allowed".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
tiernoa6bb45d2019-06-14 09:45:39 +00001131
1132 # if Role information changes, it is needed to reload the information of roles
1133 if topic == "roles" and method != "GET":
1134 self.authenticator.load_operation_to_allowed_roles()
delacruzramoad682a52019-12-10 16:26:34 +01001135
1136 if topic == "projects" and method == "DELETE" \
1137 or topic in ["users", "roles"] and method in ["PUT", "PATCH", "DELETE"]:
1138 self.authenticator.remove_token_from_cache()
1139
tierno701018c2019-06-25 11:13:14 +00001140 return self._format_out(outdata, token_info, _format)
tiernob24258a2018-10-04 18:39:49 +02001141 except Exception as e:
tierno36ec8602018-11-02 17:27:11 +01001142 if isinstance(e, (NbiException, EngineException, DbException, FsException, MsgException, AuthException,
delacruzramo01b15d32019-07-02 14:37:47 +02001143 ValidationError, AuthconnException)):
tiernob24258a2018-10-04 18:39:49 +02001144 http_code_value = cherrypy.response.status = e.http_code.value
1145 http_code_name = e.http_code.name
1146 cherrypy.log("Exception {}".format(e))
1147 else:
1148 http_code_value = cherrypy.response.status = HTTPStatus.BAD_REQUEST.value # INTERNAL_SERVER_ERROR
Felipe Vicense36ab852018-11-23 14:12:09 +01001149 cherrypy.log("CRITICAL: Exception {}".format(e), traceback=True)
tiernob24258a2018-10-04 18:39:49 +02001150 http_code_name = HTTPStatus.BAD_REQUEST.name
tierno3ace63c2018-05-03 17:51:43 +02001151 if hasattr(outdata, "close"): # is an open file
1152 outdata.close()
tiernob24258a2018-10-04 18:39:49 +02001153 error_text = str(e)
1154 rollback.reverse()
tiernodb9dc582018-06-20 17:27:29 +02001155 for rollback_item in rollback:
tierno3ace63c2018-05-03 17:51:43 +02001156 try:
tiernocc103432018-10-19 14:10:35 +02001157 if rollback_item.get("operation") == "set":
1158 self.engine.db.set_one(rollback_item["topic"], {"_id": rollback_item["_id"]},
1159 rollback_item["content"], fail_on_empty=False)
1160 else:
tiernoe8631782018-12-21 13:31:52 +00001161 self.engine.db.del_one(rollback_item["topic"], {"_id": rollback_item["_id"]},
1162 fail_on_empty=False)
tierno3ace63c2018-05-03 17:51:43 +02001163 except Exception as e2:
tiernob24258a2018-10-04 18:39:49 +02001164 rollback_error_text = "Rollback Exception {}: {}".format(rollback_item, e2)
1165 cherrypy.log(rollback_error_text)
1166 error_text += ". " + rollback_error_text
1167 # if isinstance(e, MsgException):
1168 # error_text = "{} has been '{}' but other modules cannot be informed because an error on bus".format(
1169 # engine_topic[:-1], method, error_text)
tiernoc94c3df2018-02-09 15:38:54 +01001170 problem_details = {
tiernob24258a2018-10-04 18:39:49 +02001171 "code": http_code_name,
1172 "status": http_code_value,
1173 "detail": error_text,
tiernoc94c3df2018-02-09 15:38:54 +01001174 }
tierno701018c2019-06-25 11:13:14 +00001175 return self._format_out(problem_details, token_info)
tiernoc94c3df2018-02-09 15:38:54 +01001176 # raise cherrypy.HTTPError(e.http_code.value, str(e))
tiernoa5035702019-07-29 08:54:42 +00001177 finally:
1178 if token_info:
1179 self._format_login(token_info)
1180 if method in ("PUT", "PATCH", "POST") and isinstance(outdata, dict):
1181 for logging_id in ("id", "op_id", "nsilcmop_id", "nslcmop_id"):
1182 if outdata.get(logging_id):
1183 cherrypy.request.login += ";{}={}".format(logging_id, outdata[logging_id][:36])
tiernoc94c3df2018-02-09 15:38:54 +01001184
1185
tierno7fd24872019-12-10 10:57:17 +00001186def _get_version():
1187 """
1188 Try to get version from package using pkg_resources (available with setuptools)
1189 """
1190 global nbi_version
1191 try:
1192 from pkg_resources import get_distribution
1193 nbi_version = get_distribution("osm_nbi").version
1194 except Exception:
1195 pass
1196
1197
tiernoc94c3df2018-02-09 15:38:54 +01001198def _start_service():
1199 """
1200 Callback function called when cherrypy.engine starts
1201 Override configuration with env variables
1202 Set database, storage, message configuration
1203 Init database with admin/admin user password
1204 """
tierno932499c2019-01-28 17:28:10 +00001205 global nbi_server
1206 global subscription_thread
tiernoc94c3df2018-02-09 15:38:54 +01001207 cherrypy.log.error("Starting osm_nbi")
1208 # update general cherrypy configuration
1209 update_dict = {}
1210
1211 engine_config = cherrypy.tree.apps['/osm'].config
1212 for k, v in environ.items():
1213 if not k.startswith("OSMNBI_"):
1214 continue
tiernoe1281182018-05-22 12:24:36 +02001215 k1, _, k2 = k[7:].lower().partition("_")
tiernoc94c3df2018-02-09 15:38:54 +01001216 if not k2:
1217 continue
1218 try:
1219 # update static configuration
1220 if k == 'OSMNBI_STATIC_DIR':
1221 engine_config["/static"]['tools.staticdir.dir'] = v
1222 engine_config["/static"]['tools.staticdir.on'] = True
1223 elif k == 'OSMNBI_SOCKET_PORT' or k == 'OSMNBI_SERVER_PORT':
1224 update_dict['server.socket_port'] = int(v)
1225 elif k == 'OSMNBI_SOCKET_HOST' or k == 'OSMNBI_SERVER_HOST':
1226 update_dict['server.socket_host'] = v
tiernof5298be2018-05-16 14:43:57 +02001227 elif k1 in ("server", "test", "auth", "log"):
1228 update_dict[k1 + '.' + k2] = v
Eduardo Sousa819d34c2018-07-31 01:20:02 +01001229 elif k1 in ("message", "database", "storage", "authentication"):
tiernof5298be2018-05-16 14:43:57 +02001230 # k2 = k2.replace('_', '.')
Eduardo Sousa819d34c2018-07-31 01:20:02 +01001231 if k2 in ("port", "db_port"):
tiernoc94c3df2018-02-09 15:38:54 +01001232 engine_config[k1][k2] = int(v)
1233 else:
1234 engine_config[k1][k2] = v
Eduardo Sousa819d34c2018-07-31 01:20:02 +01001235
tiernoc94c3df2018-02-09 15:38:54 +01001236 except ValueError as e:
1237 cherrypy.log.error("Ignoring environ '{}': " + str(e))
1238 except Exception as e:
1239 cherrypy.log.warn("skipping environ '{}' on exception '{}'".format(k, e))
1240
1241 if update_dict:
1242 cherrypy.config.update(update_dict)
tiernof5298be2018-05-16 14:43:57 +02001243 engine_config["global"].update(update_dict)
tiernoc94c3df2018-02-09 15:38:54 +01001244
1245 # logging cherrypy
1246 log_format_simple = "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(message)s"
1247 log_formatter_simple = logging.Formatter(log_format_simple, datefmt='%Y-%m-%dT%H:%M:%S')
1248 logger_server = logging.getLogger("cherrypy.error")
1249 logger_access = logging.getLogger("cherrypy.access")
1250 logger_cherry = logging.getLogger("cherrypy")
1251 logger_nbi = logging.getLogger("nbi")
1252
tiernof5298be2018-05-16 14:43:57 +02001253 if "log.file" in engine_config["global"]:
1254 file_handler = logging.handlers.RotatingFileHandler(engine_config["global"]["log.file"],
tiernoc94c3df2018-02-09 15:38:54 +01001255 maxBytes=100e6, backupCount=9, delay=0)
1256 file_handler.setFormatter(log_formatter_simple)
1257 logger_cherry.addHandler(file_handler)
1258 logger_nbi.addHandler(file_handler)
tiernob24258a2018-10-04 18:39:49 +02001259 # log always to standard output
1260 for format_, logger in {"nbi.server %(filename)s:%(lineno)s": logger_server,
1261 "nbi.access %(filename)s:%(lineno)s": logger_access,
1262 "%(name)s %(filename)s:%(lineno)s": logger_nbi
1263 }.items():
1264 log_format_cherry = "%(asctime)s %(levelname)s {} %(message)s".format(format_)
1265 log_formatter_cherry = logging.Formatter(log_format_cherry, datefmt='%Y-%m-%dT%H:%M:%S')
1266 str_handler = logging.StreamHandler()
1267 str_handler.setFormatter(log_formatter_cherry)
1268 logger.addHandler(str_handler)
tiernoc94c3df2018-02-09 15:38:54 +01001269
tiernof5298be2018-05-16 14:43:57 +02001270 if engine_config["global"].get("log.level"):
1271 logger_cherry.setLevel(engine_config["global"]["log.level"])
1272 logger_nbi.setLevel(engine_config["global"]["log.level"])
tiernoc94c3df2018-02-09 15:38:54 +01001273
1274 # logging other modules
1275 for k1, logname in {"message": "nbi.msg", "database": "nbi.db", "storage": "nbi.fs"}.items():
1276 engine_config[k1]["logger_name"] = logname
1277 logger_module = logging.getLogger(logname)
1278 if "logfile" in engine_config[k1]:
1279 file_handler = logging.handlers.RotatingFileHandler(engine_config[k1]["logfile"],
tierno2236d202018-05-16 19:05:16 +02001280 maxBytes=100e6, backupCount=9, delay=0)
tiernoc94c3df2018-02-09 15:38:54 +01001281 file_handler.setFormatter(log_formatter_simple)
1282 logger_module.addHandler(file_handler)
1283 if "loglevel" in engine_config[k1]:
1284 logger_module.setLevel(engine_config[k1]["loglevel"])
1285 # TODO add more entries, e.g.: storage
1286 cherrypy.tree.apps['/osm'].root.engine.start(engine_config)
Eduardo Sousa819d34c2018-07-31 01:20:02 +01001287 cherrypy.tree.apps['/osm'].root.authenticator.start(engine_config)
tiernod985a8d2018-10-19 14:12:28 +02001288 cherrypy.tree.apps['/osm'].root.engine.init_db(target_version=database_version)
1289 cherrypy.tree.apps['/osm'].root.authenticator.init_db(target_version=auth_database_version)
tiernobee508e2019-01-21 11:21:49 +00001290
tierno932499c2019-01-28 17:28:10 +00001291 # start subscriptions thread:
1292 subscription_thread = SubscriptionThread(config=engine_config, engine=nbi_server.engine)
1293 subscription_thread.start()
1294 # Do not capture except SubscriptionException
1295
tiernobee508e2019-01-21 11:21:49 +00001296 # load and print version. Ignore possible errors, e.g. file not found
1297 try:
tierno7fd24872019-12-10 10:57:17 +00001298 _get_version()
tierno9c630112019-08-29 14:21:41 +00001299 backend = engine_config["authentication"]["backend"]
tierno9c630112019-08-29 14:21:41 +00001300 cherrypy.log.error("Starting OSM NBI Version '{}' with '{}' authentication backend"
1301 .format(nbi_version + " " + nbi_version_date, backend))
tiernobee508e2019-01-21 11:21:49 +00001302 except Exception:
1303 pass
tiernoc94c3df2018-02-09 15:38:54 +01001304
1305
1306def _stop_service():
1307 """
1308 Callback function called when cherrypy.engine stops
1309 TODO: Ending database connections.
1310 """
tierno932499c2019-01-28 17:28:10 +00001311 global subscription_thread
tierno65ca36d2019-02-12 19:27:52 +01001312 if subscription_thread:
1313 subscription_thread.terminate()
tierno932499c2019-01-28 17:28:10 +00001314 subscription_thread = None
tiernoc94c3df2018-02-09 15:38:54 +01001315 cherrypy.tree.apps['/osm'].root.engine.stop()
1316 cherrypy.log.error("Stopping osm_nbi")
1317
tierno2236d202018-05-16 19:05:16 +02001318
tiernof5298be2018-05-16 14:43:57 +02001319def nbi(config_file):
tierno932499c2019-01-28 17:28:10 +00001320 global nbi_server
tiernoc94c3df2018-02-09 15:38:54 +01001321 # conf = {
1322 # '/': {
1323 # #'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
1324 # 'tools.sessions.on': True,
1325 # 'tools.response_headers.on': True,
1326 # # 'tools.response_headers.headers': [('Content-Type', 'text/plain')],
1327 # }
1328 # }
1329 # cherrypy.Server.ssl_module = 'builtin'
1330 # cherrypy.Server.ssl_certificate = "http/cert.pem"
1331 # cherrypy.Server.ssl_private_key = "http/privkey.pem"
1332 # cherrypy.Server.thread_pool = 10
1333 # cherrypy.config.update({'Server.socket_port': config["port"], 'Server.socket_host': config["host"]})
1334
1335 # cherrypy.config.update({'tools.auth_basic.on': True,
1336 # 'tools.auth_basic.realm': 'localhost',
1337 # 'tools.auth_basic.checkpassword': validate_password})
tierno932499c2019-01-28 17:28:10 +00001338 nbi_server = Server()
tiernoc94c3df2018-02-09 15:38:54 +01001339 cherrypy.engine.subscribe('start', _start_service)
1340 cherrypy.engine.subscribe('stop', _stop_service)
tierno932499c2019-01-28 17:28:10 +00001341 cherrypy.quickstart(nbi_server, '/osm', config_file)
tiernof5298be2018-05-16 14:43:57 +02001342
1343
1344def usage():
1345 print("""Usage: {} [options]
1346 -c|--config [configuration_file]: loads the configuration file (default: ./nbi.cfg)
1347 -h|--help: shows this help
1348 """.format(sys.argv[0]))
tierno2236d202018-05-16 19:05:16 +02001349 # --log-socket-host HOST: send logs to this host")
1350 # --log-socket-port PORT: send logs using this port (default: 9022)")
tiernoc94c3df2018-02-09 15:38:54 +01001351
1352
1353if __name__ == '__main__':
tiernof5298be2018-05-16 14:43:57 +02001354 try:
1355 # load parameters and configuration
1356 opts, args = getopt.getopt(sys.argv[1:], "hvc:", ["config=", "help"])
1357 # TODO add "log-socket-host=", "log-socket-port=", "log-file="
1358 config_file = None
1359 for o, a in opts:
1360 if o in ("-h", "--help"):
1361 usage()
1362 sys.exit()
1363 elif o in ("-c", "--config"):
1364 config_file = a
1365 # elif o == "--log-socket-port":
1366 # log_socket_port = a
1367 # elif o == "--log-socket-host":
1368 # log_socket_host = a
1369 # elif o == "--log-file":
1370 # log_file = a
1371 else:
1372 assert False, "Unhandled option"
1373 if config_file:
1374 if not path.isfile(config_file):
1375 print("configuration file '{}' that not exist".format(config_file), file=sys.stderr)
1376 exit(1)
1377 else:
1378 for config_file in (__file__[:__file__.rfind(".")] + ".cfg", "./nbi.cfg", "/etc/osm/nbi.cfg"):
1379 if path.isfile(config_file):
1380 break
1381 else:
1382 print("No configuration file 'nbi.cfg' found neither at local folder nor at /etc/osm/", file=sys.stderr)
1383 exit(1)
1384 nbi(config_file)
1385 except getopt.GetoptError as e:
1386 print(str(e), file=sys.stderr)
1387 # usage()
1388 exit(1)