blob: bd0f2a13851d7376662c55b9eac1ea2aa3164eed [file] [log] [blame]
tiernoc94c3df2018-02-09 15:38:54 +01001#!/usr/bin/python3
2# -*- coding: utf-8 -*-
3
tiernod125caf2018-11-22 16:05:54 +00004# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
13# implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
tiernoc94c3df2018-02-09 15:38:54 +010017import cherrypy
18import time
19import json
20import yaml
tierno9c630112019-08-29 14:21:41 +000021import osm_nbi.html_out as html
tiernoc94c3df2018-02-09 15:38:54 +010022import logging
tiernof5298be2018-05-16 14:43:57 +020023import logging.handlers
24import getopt
25import sys
Eduardo Sousa2f988212018-07-26 01:04:11 +010026
tierno9c630112019-08-29 14:21:41 +000027from osm_nbi.authconn import AuthException, AuthconnException
28from osm_nbi.auth import Authenticator
29from osm_nbi.engine import Engine, EngineException
30from osm_nbi.subscriptions import SubscriptionThread
31from osm_nbi.validation import ValidationError
tiernoa8d63632018-05-10 13:12:32 +020032from osm_common.dbbase import DbException
33from osm_common.fsbase import FsException
34from osm_common.msgbase import MsgException
tiernoc94c3df2018-02-09 15:38:54 +010035from http import HTTPStatus
tiernoc94c3df2018-02-09 15:38:54 +010036from codecs import getreader
tiernof5298be2018-05-16 14:43:57 +020037from os import environ, path
tiernob2e48bd2020-02-04 15:47:18 +000038from osm_nbi import version as nbi_version, version_date as nbi_version_date
tiernoc94c3df2018-02-09 15:38:54 +010039
40__author__ = "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
tiernodfe09572018-04-24 10:41:10 +020041
tierno9c630112019-08-29 14:21:41 +000042__version__ = "0.1.3" # file version, not NBI version
43version_date = "Aug 2019"
44
tierno1f029d82019-06-13 22:37:04 +000045database_version = '1.2'
Eduardo Sousa819d34c2018-07-31 01:20:02 +010046auth_database_version = '1.0'
tierno932499c2019-01-28 17:28:10 +000047nbi_server = None # instance of Server class
48subscription_thread = None # instance of SubscriptionThread class
tiernoc94c3df2018-02-09 15:38:54 +010049
50"""
tiernof27c79b2018-03-12 17:08:42 +010051North Bound Interface (O: OSM specific; 5,X: SOL005 not implemented yet; O5: SOL005 implemented)
tiernoc94c3df2018-02-09 15:38:54 +010052URL: /osm GET POST PUT DELETE PATCH
garciadeblas9750c5a2018-10-15 16:20:35 +020053 /nsd/v1
tierno2236d202018-05-16 19:05:16 +020054 /ns_descriptors_content O O
55 /<nsdInfoId> O O O O
tiernoc94c3df2018-02-09 15:38:54 +010056 /ns_descriptors O5 O5
57 /<nsdInfoId> O5 O5 5
58 /nsd_content O5 O5
tiernof27c79b2018-03-12 17:08:42 +010059 /nsd O
60 /artifacts[/<artifactPath>] O
tiernoc94c3df2018-02-09 15:38:54 +010061 /pnf_descriptors 5 5
62 /<pnfdInfoId> 5 5 5
63 /pnfd_content 5 5
tiernof27c79b2018-03-12 17:08:42 +010064 /subscriptions 5 5
65 /<subscriptionId> 5 X
tiernoc94c3df2018-02-09 15:38:54 +010066
67 /vnfpkgm/v1
tierno55945e72018-04-06 16:40:27 +020068 /vnf_packages_content O O
tierno2236d202018-05-16 19:05:16 +020069 /<vnfPkgId> O O
tiernoc94c3df2018-02-09 15:38:54 +010070 /vnf_packages O5 O5
71 /<vnfPkgId> O5 O5 5
tiernoc94c3df2018-02-09 15:38:54 +010072 /package_content O5 O5
73 /upload_from_uri X
tiernof27c79b2018-03-12 17:08:42 +010074 /vnfd O5
75 /artifacts[/<artifactPath>] O5
76 /subscriptions X X
77 /<subscriptionId> X X
tiernoc94c3df2018-02-09 15:38:54 +010078
79 /nslcm/v1
tiernof27c79b2018-03-12 17:08:42 +010080 /ns_instances_content O O
tierno2236d202018-05-16 19:05:16 +020081 /<nsInstanceId> O O
tiernof27c79b2018-03-12 17:08:42 +010082 /ns_instances 5 5
tierno95692442018-05-24 18:05:28 +020083 /<nsInstanceId> O5 O5
tierno65acb4d2018-04-06 16:42:40 +020084 instantiate O5
85 terminate O5
86 action O
87 scale O5
88 heal 5
tiernoc94c3df2018-02-09 15:38:54 +010089 /ns_lcm_op_occs 5 5
90 /<nsLcmOpOccId> 5 5 5
91 TO BE COMPLETED 5 5
tiernof759d822018-06-11 18:54:54 +020092 /vnf_instances (also vnfrs for compatibility) O
93 /<vnfInstanceId> O
tiernof27c79b2018-03-12 17:08:42 +010094 /subscriptions 5 5
95 /<subscriptionId> 5 X
garciadeblas9750c5a2018-10-15 16:20:35 +020096
tiernocb83c942018-09-24 17:28:13 +020097 /pdu/v1
tierno032916c2019-03-22 13:27:12 +000098 /pdu_descriptors O O
tiernocb83c942018-09-24 17:28:13 +020099 /<id> O O O O
garciadeblas9750c5a2018-10-15 16:20:35 +0200100
tiernof27c79b2018-03-12 17:08:42 +0100101 /admin/v1
102 /tokens O O
tierno2236d202018-05-16 19:05:16 +0200103 /<id> O O
tiernof27c79b2018-03-12 17:08:42 +0100104 /users O O
tiernocd54a4a2018-09-12 16:40:35 +0200105 /<id> O O O O
tiernof27c79b2018-03-12 17:08:42 +0100106 /projects O O
tierno2236d202018-05-16 19:05:16 +0200107 /<id> O O
tierno55ba2e62018-12-11 17:22:22 +0000108 /vim_accounts (also vims for compatibility) O O
109 /<id> O O O
110 /wim_accounts O O
tierno2236d202018-05-16 19:05:16 +0200111 /<id> O O O
tierno0f98af52018-03-19 10:28:22 +0100112 /sdns O O
tierno2236d202018-05-16 19:05:16 +0200113 /<id> O O O
delacruzramofe598fe2019-10-23 18:25:11 +0200114 /k8sclusters O O
115 /<id> O O O
116 /k8srepos O O
117 /<id> O O
tiernoc94c3df2018-02-09 15:38:54 +0100118
garciadeblas9750c5a2018-10-15 16:20:35 +0200119 /nst/v1 O O
120 /netslice_templates_content O O
121 /<nstInfoId> O O O O
122 /netslice_templates O O
123 /<nstInfoId> O O O
124 /nst_content O O
125 /nst O
126 /artifacts[/<artifactPath>] O
127 /subscriptions X X
128 /<subscriptionId> X X
129
130 /nsilcm/v1
131 /netslice_instances_content O O
132 /<SliceInstanceId> O O
133 /netslice_instances O O
134 /<SliceInstanceId> O O
135 instantiate O
136 terminate O
137 action O
138 /nsi_lcm_op_occs O O
139 /<nsiLcmOpOccId> O O O
140 /subscriptions X X
141 /<subscriptionId> X X
142
tierno2236d202018-05-16 19:05:16 +0200143query string:
144 Follows SOL005 section 4.3.2 It contains extra METHOD to override http method, FORCE to force.
tierno36ec8602018-11-02 17:27:11 +0100145 simpleFilterExpr := <attrName>["."<attrName>]*["."<op>]"="<value>[","<value>]*
146 filterExpr := <simpleFilterExpr>["&"<simpleFilterExpr>]*
147 op := "eq" | "neq" (or "ne") | "gt" | "lt" | "gte" | "lte" | "cont" | "ncont"
148 attrName := string
tierno2236d202018-05-16 19:05:16 +0200149 For filtering inside array, it must select the element of the array, or add ANYINDEX to apply the filtering over any
150 item of the array, that is, pass if any item of the array pass the filter.
151 It allows both ne and neq for not equal
152 TODO: 4.3.3 Attribute selectors
153 all_fields, fields=x,y,.., exclude_default, exclude_fields=x,y,...
tiernoc94c3df2018-02-09 15:38:54 +0100154 (none) … same as “exclude_default”
155 all_fields … all attributes.
tierno2236d202018-05-16 19:05:16 +0200156 fields=<list> … all attributes except all complex attributes with minimum cardinality of zero that are not
157 conditionally mandatory, and that are not provided in <list>.
158 exclude_fields=<list> … all attributes except those complex attributes with a minimum cardinality of zero that
159 are not conditionally mandatory, and that are provided in <list>.
160 exclude_default … all attributes except those complex attributes with a minimum cardinality of zero that are not
161 conditionally mandatory, and that are part of the "default exclude set" defined in the present specification for
162 the particular resource
163 exclude_default and include=<list> … all attributes except those complex attributes with a minimum cardinality
164 of zero that are not conditionally mandatory and that are part of the "default exclude set" defined in the
165 present specification for the particular resource, but that are not part of <list>
tierno65ca36d2019-02-12 19:27:52 +0100166 Additionally it admits some administrator values:
167 FORCE: To force operations skipping dependency checkings
168 ADMIN: To act as an administrator or a different project
169 PUBLIC: To get public descriptors or set a descriptor as public
170 SET_PROJECT: To make a descriptor available for other project
171
tiernoc94c3df2018-02-09 15:38:54 +0100172Header field name Reference Example Descriptions
173 Accept IETF RFC 7231 [19] application/json Content-Types that are acceptable for the response.
174 This header field shall be present if the response is expected to have a non-empty message body.
175 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the request.
176 This header field shall be present if the request has a non-empty message body.
tierno2236d202018-05-16 19:05:16 +0200177 Authorization IETF RFC 7235 [22] Bearer mF_9.B5f-4.1JqM The authorization token for the request.
178 Details are specified in clause 4.5.3.
tiernoc94c3df2018-02-09 15:38:54 +0100179 Range IETF RFC 7233 [21] 1000-2000 Requested range of bytes from a file
180Header field name Reference Example Descriptions
181 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the response.
182 This header field shall be present if the response has a non-empty message body.
tierno2236d202018-05-16 19:05:16 +0200183 Location IETF RFC 7231 [19] http://www.example.com/vnflcm/v1/vnf_instances/123 Used in redirection, or when a
184 new resource has been created.
tiernoc94c3df2018-02-09 15:38:54 +0100185 This header field shall be present if the response status code is 201 or 3xx.
tierno2236d202018-05-16 19:05:16 +0200186 In the present document this header field is also used if the response status code is 202 and a new resource was
187 created.
188 WWW-Authenticate IETF RFC 7235 [22] Bearer realm="example" Challenge if the corresponding HTTP request has not
189 provided authorization, or error details if the corresponding HTTP request has provided an invalid authorization
190 token.
191 Accept-Ranges IETF RFC 7233 [21] bytes Used by the Server to signal whether or not it supports ranges for
192 certain resources.
193 Content-Range IETF RFC 7233 [21] bytes 21010-47021/ 47022 Signals the byte range that is contained in the
194 response, and the total length of the file.
tiernoc94c3df2018-02-09 15:38:54 +0100195 Retry-After IETF RFC 7231 [19] Fri, 31 Dec 1999 23:59:59 GMT
tiernoc94c3df2018-02-09 15:38:54 +0100196"""
197
tierno701018c2019-06-25 11:13:14 +0000198valid_query_string = ("ADMIN", "SET_PROJECT", "FORCE", "PUBLIC")
199# ^ Contains possible administrative query string words:
200# ADMIN=True(by default)|Project|Project-list: See all elements, or elements of a project
201# (not owned by my session project).
202# PUBLIC=True(by default)|False: See/hide public elements. Set/Unset a topic to be public
203# FORCE=True(by default)|False: Force edition/deletion operations
204# SET_PROJECT=Project|Project-list: Add/Delete the topic to the projects portfolio
205
206valid_url_methods = {
207 # contains allowed URL and methods, and the role_permission name
208 "admin": {
209 "v1": {
210 "tokens": {"METHODS": ("GET", "POST", "DELETE"),
211 "ROLE_PERMISSION": "tokens:",
212 "<ID>": {"METHODS": ("GET", "DELETE"),
213 "ROLE_PERMISSION": "tokens:id:"
214 }
215 },
216 "users": {"METHODS": ("GET", "POST"),
217 "ROLE_PERMISSION": "users:",
delacruzramo1459d602019-10-03 14:22:00 +0200218 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"),
tierno701018c2019-06-25 11:13:14 +0000219 "ROLE_PERMISSION": "users:id:"
220 }
221 },
222 "projects": {"METHODS": ("GET", "POST"),
223 "ROLE_PERMISSION": "projects:",
delacruzramo1459d602019-10-03 14:22:00 +0200224 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"),
tierno701018c2019-06-25 11:13:14 +0000225 "ROLE_PERMISSION": "projects:id:"}
226 },
227 "roles": {"METHODS": ("GET", "POST"),
228 "ROLE_PERMISSION": "roles:",
delacruzramo1459d602019-10-03 14:22:00 +0200229 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"),
tierno701018c2019-06-25 11:13:14 +0000230 "ROLE_PERMISSION": "roles:id:"
231 }
232 },
233 "vims": {"METHODS": ("GET", "POST"),
234 "ROLE_PERMISSION": "vims:",
delacruzramo1459d602019-10-03 14:22:00 +0200235 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"),
tierno701018c2019-06-25 11:13:14 +0000236 "ROLE_PERMISSION": "vims:id:"
237 }
238 },
239 "vim_accounts": {"METHODS": ("GET", "POST"),
240 "ROLE_PERMISSION": "vim_accounts:",
delacruzramo1459d602019-10-03 14:22:00 +0200241 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"),
tierno701018c2019-06-25 11:13:14 +0000242 "ROLE_PERMISSION": "vim_accounts:id:"
243 }
244 },
245 "wim_accounts": {"METHODS": ("GET", "POST"),
246 "ROLE_PERMISSION": "wim_accounts:",
delacruzramo1459d602019-10-03 14:22:00 +0200247 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"),
tierno701018c2019-06-25 11:13:14 +0000248 "ROLE_PERMISSION": "wim_accounts:id:"
249 }
250 },
251 "sdns": {"METHODS": ("GET", "POST"),
252 "ROLE_PERMISSION": "sdn_controllers:",
delacruzramo1459d602019-10-03 14:22:00 +0200253 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"),
tierno701018c2019-06-25 11:13:14 +0000254 "ROLE_PERMISSION": "sdn_controllers:id:"
255 }
256 },
delacruzramofe598fe2019-10-23 18:25:11 +0200257 "k8sclusters": {"METHODS": ("GET", "POST"),
258 "ROLE_PERMISSION": "k8sclusters:",
delacruzramo549dda62019-11-14 09:57:35 +0100259 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"),
delacruzramofe598fe2019-10-23 18:25:11 +0200260 "ROLE_PERMISSION": "k8sclusters:id:"
261 }
262 },
263 "k8srepos": {"METHODS": ("GET", "POST"),
264 "ROLE_PERMISSION": "k8srepos:",
265 "<ID>": {"METHODS": ("GET", "DELETE"),
266 "ROLE_PERMISSION": "k8srepos:id:"
267 }
268 },
tierno12eac3c2020-03-19 23:22:08 +0000269 "domains": {"METHODS": ("GET", ),
270 "ROLE_PERMISSION": "domains:",
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 }
delacruzramo271d2002019-12-02 21:00:37 +0100342 },
343 "action": {"METHODS": ("POST", ),
344 "ROLE_PERMISSION": "vnfds:id:action:"
345 },
tierno701018c2019-06-25 11:13:14 +0000346 }
347 },
348 "subscriptions": {"TODO": ("GET", "POST"),
349 "<ID>": {"TODO": ("GET", "DELETE")}
350 },
delacruzramo271d2002019-12-02 21:00:37 +0100351 "vnfpkg_op_occs": {"METHODS": ("GET", ),
352 "ROLE_PERMISSION": "vnfds:vnfpkgops:",
353 "<ID>": {"METHODS": ("GET", ),
354 "ROLE_PERMISSION": "vnfds:vnfpkgops:id:"
355 }
356 },
tierno701018c2019-06-25 11:13:14 +0000357 }
358 },
359 "nslcm": {
360 "v1": {
361 "ns_instances_content": {"METHODS": ("GET", "POST"),
362 "ROLE_PERMISSION": "ns_instances:",
363 "<ID>": {"METHODS": ("GET", "DELETE"),
364 "ROLE_PERMISSION": "ns_instances:id:"
365 }
366 },
367 "ns_instances": {"METHODS": ("GET", "POST"),
368 "ROLE_PERMISSION": "ns_instances:",
369 "<ID>": {"METHODS": ("GET", "DELETE"),
370 "ROLE_PERMISSION": "ns_instances:id:",
371 "scale": {"METHODS": ("POST",),
372 "ROLE_PERMISSION": "ns_instances:id:scale:"
373 },
374 "terminate": {"METHODS": ("POST",),
375 "ROLE_PERMISSION": "ns_instances:id:terminate:"
376 },
377 "instantiate": {"METHODS": ("POST",),
378 "ROLE_PERMISSION": "ns_instances:id:instantiate:"
379 },
380 "action": {"METHODS": ("POST",),
381 "ROLE_PERMISSION": "ns_instances:id:action:"
382 },
383 }
384 },
385 "ns_lcm_op_occs": {"METHODS": ("GET",),
386 "ROLE_PERMISSION": "ns_instances:opps:",
387 "<ID>": {"METHODS": ("GET",),
388 "ROLE_PERMISSION": "ns_instances:opps:id:"
389 },
390 },
391 "vnfrs": {"METHODS": ("GET",),
392 "ROLE_PERMISSION": "vnf_instances:",
393 "<ID>": {"METHODS": ("GET",),
394 "ROLE_PERMISSION": "vnf_instances:id:"
395 }
396 },
397 "vnf_instances": {"METHODS": ("GET",),
398 "ROLE_PERMISSION": "vnf_instances:",
399 "<ID>": {"METHODS": ("GET",),
400 "ROLE_PERMISSION": "vnf_instances:id:"
401 }
402 },
403 }
404 },
405 "nst": {
406 "v1": {
407 "netslice_templates_content": {"METHODS": ("GET", "POST"),
408 "ROLE_PERMISSION": "slice_templates:",
409 "<ID>": {"METHODS": ("GET", "PUT", "DELETE"),
410 "ROLE_PERMISSION": "slice_templates:id:", }
411 },
412 "netslice_templates": {"METHODS": ("GET", "POST"),
413 "ROLE_PERMISSION": "slice_templates:",
414 "<ID>": {"METHODS": ("GET", "DELETE"),
415 "TODO": ("PATCH",),
416 "ROLE_PERMISSION": "slice_templates:id:",
417 "nst_content": {"METHODS": ("GET", "PUT"),
418 "ROLE_PERMISSION": "slice_templates:id:content:"
419 },
420 "nst": {"METHODS": ("GET",), # descriptor inside package
421 "ROLE_PERMISSION": "slice_templates:id:content:"
422 },
423 "artifacts": {"*": {"METHODS": ("GET",),
424 "ROLE_PERMISSION": "slice_templates:id:content:"
425 }
426 }
427 }
428 },
429 "subscriptions": {"TODO": ("GET", "POST"),
430 "<ID>": {"TODO": ("GET", "DELETE")}
431 },
432 }
433 },
434 "nsilcm": {
435 "v1": {
436 "netslice_instances_content": {"METHODS": ("GET", "POST"),
437 "ROLE_PERMISSION": "slice_instances:",
438 "<ID>": {"METHODS": ("GET", "DELETE"),
439 "ROLE_PERMISSION": "slice_instances:id:"
440 }
441 },
442 "netslice_instances": {"METHODS": ("GET", "POST"),
443 "ROLE_PERMISSION": "slice_instances:",
444 "<ID>": {"METHODS": ("GET", "DELETE"),
445 "ROLE_PERMISSION": "slice_instances:id:",
446 "terminate": {"METHODS": ("POST",),
447 "ROLE_PERMISSION": "slice_instances:id:terminate:"
448 },
449 "instantiate": {"METHODS": ("POST",),
450 "ROLE_PERMISSION": "slice_instances:id:instantiate:"
451 },
452 "action": {"METHODS": ("POST",),
453 "ROLE_PERMISSION": "slice_instances:id:action:"
454 },
455 }
456 },
457 "nsi_lcm_op_occs": {"METHODS": ("GET",),
458 "ROLE_PERMISSION": "slice_instances:opps:",
459 "<ID>": {"METHODS": ("GET",),
460 "ROLE_PERMISSION": "slice_instances:opps:id:",
461 },
462 },
463 }
464 },
465 "nspm": {
466 "v1": {
467 "pm_jobs": {
468 "<ID>": {
469 "reports": {
470 "<ID>": {"METHODS": ("GET",),
471 "ROLE_PERMISSION": "reports:id:",
472 }
473 }
474 },
475 },
476 },
477 },
478}
479
tiernoc94c3df2018-02-09 15:38:54 +0100480
481class NbiException(Exception):
482
483 def __init__(self, message, http_code=HTTPStatus.METHOD_NOT_ALLOWED):
484 Exception.__init__(self, message)
485 self.http_code = http_code
486
487
488class Server(object):
489 instance = 0
490 # to decode bytes to str
491 reader = getreader("utf-8")
492
493 def __init__(self):
494 self.instance += 1
tierno701018c2019-06-25 11:13:14 +0000495 self.authenticator = Authenticator(valid_url_methods, valid_query_string)
delacruzramoad682a52019-12-10 16:26:34 +0100496 self.engine = Engine(self.authenticator)
tiernoc94c3df2018-02-09 15:38:54 +0100497
tiernoc94c3df2018-02-09 15:38:54 +0100498 def _format_in(self, kwargs):
499 try:
500 indata = None
501 if cherrypy.request.body.length:
502 error_text = "Invalid input format "
503
504 if "Content-Type" in cherrypy.request.headers:
505 if "application/json" in cherrypy.request.headers["Content-Type"]:
506 error_text = "Invalid json format "
507 indata = json.load(self.reader(cherrypy.request.body))
gcalvinode4adfe2018-10-30 11:46:09 +0100508 cherrypy.request.headers.pop("Content-File-MD5", None)
tiernoc94c3df2018-02-09 15:38:54 +0100509 elif "application/yaml" in cherrypy.request.headers["Content-Type"]:
510 error_text = "Invalid yaml format "
delacruzramob19cadc2019-10-08 10:18:02 +0200511 indata = yaml.load(cherrypy.request.body, Loader=yaml.SafeLoader)
gcalvinode4adfe2018-10-30 11:46:09 +0100512 cherrypy.request.headers.pop("Content-File-MD5", None)
tiernoc94c3df2018-02-09 15:38:54 +0100513 elif "application/binary" in cherrypy.request.headers["Content-Type"] or \
514 "application/gzip" in cherrypy.request.headers["Content-Type"] or \
tiernof27c79b2018-03-12 17:08:42 +0100515 "application/zip" in cherrypy.request.headers["Content-Type"] or \
516 "text/plain" in cherrypy.request.headers["Content-Type"]:
517 indata = cherrypy.request.body # .read()
tiernoc94c3df2018-02-09 15:38:54 +0100518 elif "multipart/form-data" in cherrypy.request.headers["Content-Type"]:
519 if "descriptor_file" in kwargs:
520 filecontent = kwargs.pop("descriptor_file")
521 if not filecontent.file:
522 raise NbiException("empty file or content", HTTPStatus.BAD_REQUEST)
tiernof27c79b2018-03-12 17:08:42 +0100523 indata = filecontent.file # .read()
tiernoc94c3df2018-02-09 15:38:54 +0100524 if filecontent.content_type.value:
525 cherrypy.request.headers["Content-Type"] = filecontent.content_type.value
526 else:
527 # raise cherrypy.HTTPError(HTTPStatus.Not_Acceptable,
528 # "Only 'Content-Type' of type 'application/json' or
529 # 'application/yaml' for input format are available")
530 error_text = "Invalid yaml format "
delacruzramob19cadc2019-10-08 10:18:02 +0200531 indata = yaml.load(cherrypy.request.body, Loader=yaml.SafeLoader)
gcalvinode4adfe2018-10-30 11:46:09 +0100532 cherrypy.request.headers.pop("Content-File-MD5", None)
tiernoc94c3df2018-02-09 15:38:54 +0100533 else:
534 error_text = "Invalid yaml format "
delacruzramob19cadc2019-10-08 10:18:02 +0200535 indata = yaml.load(cherrypy.request.body, Loader=yaml.SafeLoader)
gcalvinode4adfe2018-10-30 11:46:09 +0100536 cherrypy.request.headers.pop("Content-File-MD5", None)
tiernoc94c3df2018-02-09 15:38:54 +0100537 if not indata:
538 indata = {}
539
tiernoc94c3df2018-02-09 15:38:54 +0100540 format_yaml = False
541 if cherrypy.request.headers.get("Query-String-Format") == "yaml":
542 format_yaml = True
543
544 for k, v in kwargs.items():
545 if isinstance(v, str):
546 if v == "":
547 kwargs[k] = None
548 elif format_yaml:
549 try:
delacruzramob19cadc2019-10-08 10:18:02 +0200550 kwargs[k] = yaml.load(v, Loader=yaml.SafeLoader)
tiernoe1281182018-05-22 12:24:36 +0200551 except Exception:
tiernoc94c3df2018-02-09 15:38:54 +0100552 pass
553 elif k.endswith(".gt") or k.endswith(".lt") or k.endswith(".gte") or k.endswith(".lte"):
554 try:
555 kwargs[k] = int(v)
tiernoe1281182018-05-22 12:24:36 +0200556 except Exception:
tiernoc94c3df2018-02-09 15:38:54 +0100557 try:
558 kwargs[k] = float(v)
tiernoe1281182018-05-22 12:24:36 +0200559 except Exception:
tiernoc94c3df2018-02-09 15:38:54 +0100560 pass
561 elif v.find(",") > 0:
562 kwargs[k] = v.split(",")
563 elif isinstance(v, (list, tuple)):
564 for index in range(0, len(v)):
565 if v[index] == "":
566 v[index] = None
567 elif format_yaml:
568 try:
delacruzramob19cadc2019-10-08 10:18:02 +0200569 v[index] = yaml.load(v[index], Loader=yaml.SafeLoader)
tiernoe1281182018-05-22 12:24:36 +0200570 except Exception:
tiernoc94c3df2018-02-09 15:38:54 +0100571 pass
572
tiernof27c79b2018-03-12 17:08:42 +0100573 return indata
tiernoc94c3df2018-02-09 15:38:54 +0100574 except (ValueError, yaml.YAMLError) as exc:
575 raise NbiException(error_text + str(exc), HTTPStatus.BAD_REQUEST)
576 except KeyError as exc:
577 raise NbiException("Query string error: " + str(exc), HTTPStatus.BAD_REQUEST)
tiernob92094f2018-05-11 13:44:22 +0200578 except Exception as exc:
579 raise NbiException(error_text + str(exc), HTTPStatus.BAD_REQUEST)
tiernoc94c3df2018-02-09 15:38:54 +0100580
581 @staticmethod
tierno701018c2019-06-25 11:13:14 +0000582 def _format_out(data, token_info=None, _format=None):
tiernoc94c3df2018-02-09 15:38:54 +0100583 """
584 return string of dictionary data according to requested json, yaml, xml. By default json
tiernof27c79b2018-03-12 17:08:42 +0100585 :param data: response to be sent. Can be a dict, text or file
tierno701018c2019-06-25 11:13:14 +0000586 :param token_info: Contains among other username and project
tierno5792d7d2019-08-30 15:37:12 +0000587 :param _format: The format to be set as Content-Type if data is a file
tiernoc94c3df2018-02-09 15:38:54 +0100588 :return: None
589 """
tierno0f98af52018-03-19 10:28:22 +0100590 accept = cherrypy.request.headers.get("Accept")
tiernof27c79b2018-03-12 17:08:42 +0100591 if data is None:
tierno0f98af52018-03-19 10:28:22 +0100592 if accept and "text/html" in accept:
tierno701018c2019-06-25 11:13:14 +0000593 return html.format(data, cherrypy.request, cherrypy.response, token_info)
tierno09c073e2018-04-26 13:36:48 +0200594 # cherrypy.response.status = HTTPStatus.NO_CONTENT.value
tiernof27c79b2018-03-12 17:08:42 +0100595 return
596 elif hasattr(data, "read"): # file object
597 if _format:
598 cherrypy.response.headers["Content-Type"] = _format
599 elif "b" in data.mode: # binariy asssumig zip
600 cherrypy.response.headers["Content-Type"] = 'application/zip'
601 else:
602 cherrypy.response.headers["Content-Type"] = 'text/plain'
603 # TODO check that cherrypy close file. If not implement pending things to close per thread next
604 return data
tierno0f98af52018-03-19 10:28:22 +0100605 if accept:
tiernoc94c3df2018-02-09 15:38:54 +0100606 if "application/json" in accept:
607 cherrypy.response.headers["Content-Type"] = 'application/json; charset=utf-8'
608 a = json.dumps(data, indent=4) + "\n"
609 return a.encode("utf8")
610 elif "text/html" in accept:
tierno701018c2019-06-25 11:13:14 +0000611 return html.format(data, cherrypy.request, cherrypy.response, token_info)
tiernoc94c3df2018-02-09 15:38:54 +0100612
tiernof27c79b2018-03-12 17:08:42 +0100613 elif "application/yaml" in accept or "*/*" in accept or "text/plain" in accept:
tiernoc94c3df2018-02-09 15:38:54 +0100614 pass
tiernof717cbe2018-12-03 16:35:42 +0000615 # if there is not any valid accept, raise an error. But if response is already an error, format in yaml
616 elif cherrypy.response.status >= 400:
tiernoc94c3df2018-02-09 15:38:54 +0100617 raise cherrypy.HTTPError(HTTPStatus.NOT_ACCEPTABLE.value,
618 "Only 'Accept' of type 'application/json' or 'application/yaml' "
619 "for output format are available")
620 cherrypy.response.headers["Content-Type"] = 'application/yaml'
621 return yaml.safe_dump(data, explicit_start=True, indent=4, default_flow_style=False, tags=False,
622 encoding='utf-8', allow_unicode=True) # , canonical=True, default_style='"'
623
624 @cherrypy.expose
625 def index(self, *args, **kwargs):
tierno701018c2019-06-25 11:13:14 +0000626 token_info = None
tiernoc94c3df2018-02-09 15:38:54 +0100627 try:
628 if cherrypy.request.method == "GET":
tierno701018c2019-06-25 11:13:14 +0000629 token_info = self.authenticator.authorize()
delacruzramo01b15d32019-07-02 14:37:47 +0200630 outdata = token_info # Home page
tiernoc94c3df2018-02-09 15:38:54 +0100631 else:
632 raise cherrypy.HTTPError(HTTPStatus.METHOD_NOT_ALLOWED.value,
tierno2236d202018-05-16 19:05:16 +0200633 "Method {} not allowed for tokens".format(cherrypy.request.method))
tiernoc94c3df2018-02-09 15:38:54 +0100634
tierno701018c2019-06-25 11:13:14 +0000635 return self._format_out(outdata, token_info)
tiernoc94c3df2018-02-09 15:38:54 +0100636
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100637 except (EngineException, AuthException) as e:
tierno701018c2019-06-25 11:13:14 +0000638 # cherrypy.log("index Exception {}".format(e))
tiernoc94c3df2018-02-09 15:38:54 +0100639 cherrypy.response.status = e.http_code.value
tierno701018c2019-06-25 11:13:14 +0000640 return self._format_out("Welcome to OSM!", token_info)
tiernoc94c3df2018-02-09 15:38:54 +0100641
642 @cherrypy.expose
tierno55945e72018-04-06 16:40:27 +0200643 def version(self, *args, **kwargs):
tiernodfe09572018-04-24 10:41:10 +0200644 # TODO consider to remove and provide version using the static version file
tierno55945e72018-04-06 16:40:27 +0200645 try:
646 if cherrypy.request.method != "GET":
647 raise NbiException("Only method GET is allowed", HTTPStatus.METHOD_NOT_ALLOWED)
648 elif args or kwargs:
649 raise NbiException("Invalid URL or query string for version", HTTPStatus.METHOD_NOT_ALLOWED)
tierno9c630112019-08-29 14:21:41 +0000650 # TODO include version of other modules, pick up from some kafka admin message
tierno5792d7d2019-08-30 15:37:12 +0000651 osm_nbi_version = {"version": nbi_version, "date": nbi_version_date}
652 return self._format_out(osm_nbi_version)
tierno55945e72018-04-06 16:40:27 +0200653 except NbiException as e:
654 cherrypy.response.status = e.http_code.value
655 problem_details = {
656 "code": e.http_code.name,
657 "status": e.http_code.value,
658 "detail": str(e),
659 }
660 return self._format_out(problem_details, None)
661
tierno12eac3c2020-03-19 23:22:08 +0000662 def domain(self):
663 try:
664 domains = {
665 "user_domain_name": cherrypy.tree.apps['/osm'].config["authentication"].get("user_domain_name"),
666 "project_domain_name": cherrypy.tree.apps['/osm'].config["authentication"].get("project_domain_name")}
667 return self._format_out(domains)
668 except NbiException as e:
669 cherrypy.response.status = e.http_code.value
670 problem_details = {
671 "code": e.http_code.name,
672 "status": e.http_code.value,
673 "detail": str(e),
674 }
675 return self._format_out(problem_details, None)
676
tiernoa5035702019-07-29 08:54:42 +0000677 @staticmethod
678 def _format_login(token_info):
679 """
680 Changes cherrypy.request.login to include username/project_name;session so that cherrypy access log will
681 log this information
682 :param token_info: Dictionary with token content
683 :return: None
684 """
685 cherrypy.request.login = token_info.get("username", "-")
686 if token_info.get("project_name"):
687 cherrypy.request.login += "/" + token_info["project_name"]
688 if token_info.get("id"):
689 cherrypy.request.login += ";session=" + token_info["id"][0:12]
690
tierno55945e72018-04-06 16:40:27 +0200691 @cherrypy.expose
tiernof27c79b2018-03-12 17:08:42 +0100692 def token(self, method, token_id=None, kwargs=None):
tierno701018c2019-06-25 11:13:14 +0000693 token_info = None
tiernoc94c3df2018-02-09 15:38:54 +0100694 # self.engine.load_dbase(cherrypy.request.app.config)
tiernof27c79b2018-03-12 17:08:42 +0100695 indata = self._format_in(kwargs)
696 if not isinstance(indata, dict):
697 raise NbiException("Expected application/yaml or application/json Content-Type", HTTPStatus.BAD_REQUEST)
tiernoa5035702019-07-29 08:54:42 +0000698
699 if method == "GET":
700 token_info = self.authenticator.authorize()
701 # for logging
702 self._format_login(token_info)
703 if token_id:
704 outdata = self.authenticator.get_token(token_info, token_id)
tiernoc94c3df2018-02-09 15:38:54 +0100705 else:
tiernoa5035702019-07-29 08:54:42 +0000706 outdata = self.authenticator.get_token_list(token_info)
707 elif method == "POST":
708 try:
709 token_info = self.authenticator.authorize()
710 except Exception:
711 token_info = None
712 if kwargs:
713 indata.update(kwargs)
714 # This is needed to log the user when authentication fails
715 cherrypy.request.login = "{}".format(indata.get("username", "-"))
716 outdata = token_info = self.authenticator.new_token(token_info, indata, cherrypy.request.remote)
717 cherrypy.session['Authorization'] = outdata["_id"]
718 self._set_location_header("admin", "v1", "tokens", outdata["_id"])
719 # for logging
720 self._format_login(token_info)
721
722 # cherrypy.response.cookie["Authorization"] = outdata["id"]
723 # cherrypy.response.cookie["Authorization"]['expires'] = 3600
724 elif method == "DELETE":
725 if not token_id and "id" in kwargs:
726 token_id = kwargs["id"]
727 elif not token_id:
728 token_info = self.authenticator.authorize()
729 # for logging
730 self._format_login(token_info)
731 token_id = token_info["_id"]
732 outdata = self.authenticator.del_token(token_id)
733 token_info = None
734 cherrypy.session['Authorization'] = "logout"
735 # cherrypy.response.cookie["Authorization"] = token_id
736 # cherrypy.response.cookie["Authorization"]['expires'] = 0
737 else:
738 raise NbiException("Method {} not allowed for token".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
739 return self._format_out(outdata, token_info)
tiernoc94c3df2018-02-09 15:38:54 +0100740
741 @cherrypy.expose
742 def test(self, *args, **kwargs):
tierno4836bac2020-01-15 14:41:48 +0000743 if not cherrypy.config.get("server.enable_test") or (isinstance(cherrypy.config["server.enable_test"], str) and
744 cherrypy.config["server.enable_test"].lower() == "false"):
745 cherrypy.response.status = HTTPStatus.METHOD_NOT_ALLOWED.value
746 return "test URL is disabled"
tiernoc94c3df2018-02-09 15:38:54 +0100747 thread_info = None
tiernof27c79b2018-03-12 17:08:42 +0100748 if args and args[0] == "help":
tiernof717cbe2018-12-03 16:35:42 +0000749 return "<html><pre>\ninit\nfile/<name> download file\ndb-clear/table\nfs-clear[/folder]\nlogin\nlogin2\n"\
tiernoe1281182018-05-22 12:24:36 +0200750 "sleep/<time>\nmessage/topic\n</pre></html>"
tiernof27c79b2018-03-12 17:08:42 +0100751
752 elif args and args[0] == "init":
tiernoc94c3df2018-02-09 15:38:54 +0100753 try:
754 # self.engine.load_dbase(cherrypy.request.app.config)
755 self.engine.create_admin()
756 return "Done. User 'admin', password 'admin' created"
757 except Exception:
758 cherrypy.response.status = HTTPStatus.FORBIDDEN.value
759 return self._format_out("Database already initialized")
tiernof27c79b2018-03-12 17:08:42 +0100760 elif args and args[0] == "file":
761 return cherrypy.lib.static.serve_file(cherrypy.tree.apps['/osm'].config["storage"]["path"] + "/" + args[1],
762 "text/plain", "attachment")
763 elif args and args[0] == "file2":
764 f_path = cherrypy.tree.apps['/osm'].config["storage"]["path"] + "/" + args[1]
765 f = open(f_path, "r")
766 cherrypy.response.headers["Content-type"] = "text/plain"
tiernof27c79b2018-03-12 17:08:42 +0100767 return f
tierno55945e72018-04-06 16:40:27 +0200768
tiernof27c79b2018-03-12 17:08:42 +0100769 elif len(args) == 2 and args[0] == "db-clear":
tiernof717cbe2018-12-03 16:35:42 +0000770 deleted_info = self.engine.db.del_list(args[1], kwargs)
771 return "{} {} deleted\n".format(deleted_info["deleted"], args[1])
772 elif len(args) and args[0] == "fs-clear":
773 if len(args) >= 2:
774 folders = (args[1],)
775 else:
776 folders = self.engine.fs.dir_ls(".")
777 for folder in folders:
778 self.engine.fs.file_delete(folder)
779 return ",".join(folders) + " folders deleted\n"
tiernoc94c3df2018-02-09 15:38:54 +0100780 elif args and args[0] == "login":
781 if not cherrypy.request.headers.get("Authorization"):
782 cherrypy.response.headers["WWW-Authenticate"] = 'Basic realm="Access to OSM site", charset="UTF-8"'
783 cherrypy.response.status = HTTPStatus.UNAUTHORIZED.value
784 elif args and args[0] == "login2":
785 if not cherrypy.request.headers.get("Authorization"):
786 cherrypy.response.headers["WWW-Authenticate"] = 'Bearer realm="Access to OSM site"'
787 cherrypy.response.status = HTTPStatus.UNAUTHORIZED.value
788 elif args and args[0] == "sleep":
789 sleep_time = 5
790 try:
791 sleep_time = int(args[1])
792 except Exception:
793 cherrypy.response.status = HTTPStatus.FORBIDDEN.value
794 return self._format_out("Database already initialized")
795 thread_info = cherrypy.thread_data
796 print(thread_info)
797 time.sleep(sleep_time)
798 # thread_info
799 elif len(args) >= 2 and args[0] == "message":
tiernob24258a2018-10-04 18:39:49 +0200800 main_topic = args[1]
801 return_text = "<html><pre>{} ->\n".format(main_topic)
tiernoc94c3df2018-02-09 15:38:54 +0100802 try:
tierno55945e72018-04-06 16:40:27 +0200803 if cherrypy.request.method == 'POST':
delacruzramob19cadc2019-10-08 10:18:02 +0200804 to_send = yaml.load(cherrypy.request.body, Loader=yaml.SafeLoader)
tierno55945e72018-04-06 16:40:27 +0200805 for k, v in to_send.items():
tiernob24258a2018-10-04 18:39:49 +0200806 self.engine.msg.write(main_topic, k, v)
tierno55945e72018-04-06 16:40:27 +0200807 return_text += " {}: {}\n".format(k, v)
808 elif cherrypy.request.method == 'GET':
809 for k, v in kwargs.items():
delacruzramob19cadc2019-10-08 10:18:02 +0200810 self.engine.msg.write(main_topic, k, yaml.load(v), Loader=yaml.SafeLoader)
811 return_text += " {}: {}\n".format(k, yaml.load(v), Loader=yaml.SafeLoader)
tiernoc94c3df2018-02-09 15:38:54 +0100812 except Exception as e:
tierno55945e72018-04-06 16:40:27 +0200813 return_text += "Error: " + str(e)
814 return_text += "</pre></html>\n"
815 return return_text
tiernoc94c3df2018-02-09 15:38:54 +0100816
817 return_text = (
818 "<html><pre>\nheaders:\n args: {}\n".format(args) +
819 " kwargs: {}\n".format(kwargs) +
820 " headers: {}\n".format(cherrypy.request.headers) +
821 " path_info: {}\n".format(cherrypy.request.path_info) +
822 " query_string: {}\n".format(cherrypy.request.query_string) +
823 " session: {}\n".format(cherrypy.session) +
824 " cookie: {}\n".format(cherrypy.request.cookie) +
825 " method: {}\n".format(cherrypy.request.method) +
tiernof717cbe2018-12-03 16:35:42 +0000826 " session: {}\n".format(cherrypy.session.get('fieldname')) +
tiernoc94c3df2018-02-09 15:38:54 +0100827 " body:\n")
828 return_text += " length: {}\n".format(cherrypy.request.body.length)
829 if cherrypy.request.body.length:
830 return_text += " content: {}\n".format(
831 str(cherrypy.request.body.read(int(cherrypy.request.headers.get('Content-Length', 0)))))
832 if thread_info:
833 return_text += "thread: {}\n".format(thread_info)
834 return_text += "</pre></html>"
835 return return_text
836
tierno701018c2019-06-25 11:13:14 +0000837 @staticmethod
838 def _check_valid_url_method(method, *args):
tiernof27c79b2018-03-12 17:08:42 +0100839 if len(args) < 3:
tiernob24258a2018-10-04 18:39:49 +0200840 raise NbiException("URL must contain at least 'main_topic/version/topic'", HTTPStatus.METHOD_NOT_ALLOWED)
tiernof27c79b2018-03-12 17:08:42 +0100841
tierno701018c2019-06-25 11:13:14 +0000842 reference = valid_url_methods
tiernof27c79b2018-03-12 17:08:42 +0100843 for arg in args:
844 if arg is None:
845 break
846 if not isinstance(reference, dict):
847 raise NbiException("URL contains unexpected extra items '{}'".format(arg),
848 HTTPStatus.METHOD_NOT_ALLOWED)
849
850 if arg in reference:
851 reference = reference[arg]
852 elif "<ID>" in reference:
853 reference = reference["<ID>"]
854 elif "*" in reference:
855 reference = reference["*"]
856 break
857 else:
858 raise NbiException("Unexpected URL item {}".format(arg), HTTPStatus.METHOD_NOT_ALLOWED)
859 if "TODO" in reference and method in reference["TODO"]:
860 raise NbiException("Method {} not supported yet for this URL".format(method), HTTPStatus.NOT_IMPLEMENTED)
tierno2236d202018-05-16 19:05:16 +0200861 elif "METHODS" in reference and method not in reference["METHODS"]:
tiernof27c79b2018-03-12 17:08:42 +0100862 raise NbiException("Method {} not supported for this URL".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
tierno701018c2019-06-25 11:13:14 +0000863 return reference["ROLE_PERMISSION"] + method.lower()
tiernof27c79b2018-03-12 17:08:42 +0100864
865 @staticmethod
tiernob24258a2018-10-04 18:39:49 +0200866 def _set_location_header(main_topic, version, topic, id):
tiernof27c79b2018-03-12 17:08:42 +0100867 """
868 Insert response header Location with the URL of created item base on URL params
tiernob24258a2018-10-04 18:39:49 +0200869 :param main_topic:
tiernof27c79b2018-03-12 17:08:42 +0100870 :param version:
tiernob24258a2018-10-04 18:39:49 +0200871 :param topic:
tiernof27c79b2018-03-12 17:08:42 +0100872 :param id:
873 :return: None
874 """
875 # 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 +0200876 cherrypy.response.headers["Location"] = "/osm/{}/{}/{}/{}".format(main_topic, version, topic, id)
tiernof27c79b2018-03-12 17:08:42 +0100877 return
878
tierno65ca36d2019-02-12 19:27:52 +0100879 @staticmethod
tierno701018c2019-06-25 11:13:14 +0000880 def _extract_query_string_operations(kwargs, method):
881 """
882
883 :param kwargs:
884 :return:
885 """
886 query_string_operations = []
887 if kwargs:
888 for qs in ("FORCE", "PUBLIC", "ADMIN", "SET_PROJECT"):
889 if qs in kwargs and kwargs[qs].lower() != "false":
890 query_string_operations.append(qs.lower() + ":" + method.lower())
891 return query_string_operations
892
893 @staticmethod
894 def _manage_admin_query(token_info, kwargs, method, _id):
tierno65ca36d2019-02-12 19:27:52 +0100895 """
896 Processes the administrator query inputs (if any) of FORCE, ADMIN, PUBLIC, SET_PROJECT
897 Check that users has rights to use them and returs the admin_query
tierno701018c2019-06-25 11:13:14 +0000898 :param token_info: token_info rights obtained by token
tierno65ca36d2019-02-12 19:27:52 +0100899 :param kwargs: query string input.
900 :param method: http method: GET, POSST, PUT, ...
901 :param _id:
902 :return: admin_query dictionary with keys:
903 public: True, False or None
904 force: True or False
905 project_id: tuple with projects used for accessing an element
906 set_project: tuple with projects that a created element will belong to
907 method: show, list, delete, write
908 """
tierno701018c2019-06-25 11:13:14 +0000909 admin_query = {"force": False, "project_id": (token_info["project_id"], ), "username": token_info["username"],
delacruzramo029405d2019-09-26 10:52:56 +0200910 "admin": token_info["admin"], "public": None,
911 "allow_show_user_project_role": token_info["allow_show_user_project_role"]}
tierno65ca36d2019-02-12 19:27:52 +0100912 if kwargs:
913 # FORCE
914 if "FORCE" in kwargs:
915 if kwargs["FORCE"].lower() != "false": # if None or True set force to True
916 admin_query["force"] = True
917 del kwargs["FORCE"]
918 # PUBLIC
919 if "PUBLIC" in kwargs:
920 if kwargs["PUBLIC"].lower() != "false": # if None or True set public to True
921 admin_query["public"] = True
922 else:
923 admin_query["public"] = False
924 del kwargs["PUBLIC"]
925 # ADMIN
926 if "ADMIN" in kwargs:
927 behave_as = kwargs.pop("ADMIN")
928 if behave_as.lower() != "false":
tierno701018c2019-06-25 11:13:14 +0000929 if not token_info["admin"]:
tierno65ca36d2019-02-12 19:27:52 +0100930 raise NbiException("Only admin projects can use 'ADMIN' query string", HTTPStatus.UNAUTHORIZED)
931 if not behave_as or behave_as.lower() == "true": # convert True, None to empty list
932 admin_query["project_id"] = ()
933 elif isinstance(behave_as, (list, tuple)):
934 admin_query["project_id"] = behave_as
935 else: # isinstance(behave_as, str)
936 admin_query["project_id"] = (behave_as, )
937 if "SET_PROJECT" in kwargs:
938 set_project = kwargs.pop("SET_PROJECT")
939 if not set_project:
940 admin_query["set_project"] = list(admin_query["project_id"])
941 else:
942 if isinstance(set_project, str):
943 set_project = (set_project, )
944 if admin_query["project_id"]:
945 for p in set_project:
946 if p not in admin_query["project_id"]:
947 raise NbiException("Unauthorized for 'SET_PROJECT={p}'. Try with 'ADMIN=True' or "
948 "'ADMIN='{p}'".format(p=p), HTTPStatus.UNAUTHORIZED)
949 admin_query["set_project"] = set_project
950
951 # PROJECT_READ
952 # if "PROJECT_READ" in kwargs:
953 # admin_query["project"] = kwargs.pop("project")
tierno701018c2019-06-25 11:13:14 +0000954 # if admin_query["project"] == token_info["project_id"]:
tierno65ca36d2019-02-12 19:27:52 +0100955 if method == "GET":
956 if _id:
957 admin_query["method"] = "show"
958 else:
959 admin_query["method"] = "list"
960 elif method == "DELETE":
961 admin_query["method"] = "delete"
962 else:
963 admin_query["method"] = "write"
964 return admin_query
965
tiernoc94c3df2018-02-09 15:38:54 +0100966 @cherrypy.expose
tiernob24258a2018-10-04 18:39:49 +0200967 def default(self, main_topic=None, version=None, topic=None, _id=None, item=None, *args, **kwargs):
tierno701018c2019-06-25 11:13:14 +0000968 token_info = None
tiernof27c79b2018-03-12 17:08:42 +0100969 outdata = None
970 _format = None
tierno0f98af52018-03-19 10:28:22 +0100971 method = "DONE"
tiernob24258a2018-10-04 18:39:49 +0200972 engine_topic = None
tiernodb9dc582018-06-20 17:27:29 +0200973 rollback = []
tierno701018c2019-06-25 11:13:14 +0000974 engine_session = None
tiernoc94c3df2018-02-09 15:38:54 +0100975 try:
tiernob24258a2018-10-04 18:39:49 +0200976 if not main_topic or not version or not topic:
977 raise NbiException("URL must contain at least 'main_topic/version/topic'",
978 HTTPStatus.METHOD_NOT_ALLOWED)
vijay.r35ef2f72019-04-30 17:55:49 +0530979 if main_topic not in ("admin", "vnfpkgm", "nsd", "nslcm", "pdu", "nst", "nsilcm", "nspm"):
tiernob24258a2018-10-04 18:39:49 +0200980 raise NbiException("URL main_topic '{}' not supported".format(main_topic),
981 HTTPStatus.METHOD_NOT_ALLOWED)
tiernoc94c3df2018-02-09 15:38:54 +0100982 if version != 'v1':
983 raise NbiException("URL version '{}' not supported".format(version), HTTPStatus.METHOD_NOT_ALLOWED)
984
tiernof27c79b2018-03-12 17:08:42 +0100985 if kwargs and "METHOD" in kwargs and kwargs["METHOD"] in ("PUT", "POST", "DELETE", "GET", "PATCH"):
986 method = kwargs.pop("METHOD")
987 else:
988 method = cherrypy.request.method
tierno65ca36d2019-02-12 19:27:52 +0100989
tierno701018c2019-06-25 11:13:14 +0000990 role_permission = self._check_valid_url_method(method, main_topic, version, topic, _id, item, *args)
991 query_string_operations = self._extract_query_string_operations(kwargs, method)
tiernob24258a2018-10-04 18:39:49 +0200992 if main_topic == "admin" and topic == "tokens":
tiernof27c79b2018-03-12 17:08:42 +0100993 return self.token(method, _id, kwargs)
delacruzramo029405d2019-09-26 10:52:56 +0200994 token_info = self.authenticator.authorize(role_permission, query_string_operations, _id)
tierno12eac3c2020-03-19 23:22:08 +0000995 if main_topic == "admin" and topic == "domains":
996 return self.domain()
tierno701018c2019-06-25 11:13:14 +0000997 engine_session = self._manage_admin_query(token_info, kwargs, method, _id)
tiernof27c79b2018-03-12 17:08:42 +0100998 indata = self._format_in(kwargs)
tiernob24258a2018-10-04 18:39:49 +0200999 engine_topic = topic
1000 if topic == "subscriptions":
1001 engine_topic = main_topic + "_" + topic
vijay.r35ef2f72019-04-30 17:55:49 +05301002 if item and topic != "pm_jobs":
tiernob24258a2018-10-04 18:39:49 +02001003 engine_topic = item
tiernoc94c3df2018-02-09 15:38:54 +01001004
tiernob24258a2018-10-04 18:39:49 +02001005 if main_topic == "nsd":
1006 engine_topic = "nsds"
1007 elif main_topic == "vnfpkgm":
1008 engine_topic = "vnfds"
delacruzramo271d2002019-12-02 21:00:37 +01001009 if topic == "vnfpkg_op_occs":
1010 engine_topic = "vnfpkgops"
1011 if topic == "vnf_packages" and item == "action":
1012 engine_topic = "vnfpkgops"
tiernob24258a2018-10-04 18:39:49 +02001013 elif main_topic == "nslcm":
1014 engine_topic = "nsrs"
1015 if topic == "ns_lcm_op_occs":
1016 engine_topic = "nslcmops"
1017 if topic == "vnfrs" or topic == "vnf_instances":
1018 engine_topic = "vnfrs"
garciadeblas9750c5a2018-10-15 16:20:35 +02001019 elif main_topic == "nst":
1020 engine_topic = "nsts"
1021 elif main_topic == "nsilcm":
1022 engine_topic = "nsis"
1023 if topic == "nsi_lcm_op_occs":
delacruzramoc061f562019-04-05 11:00:02 +02001024 engine_topic = "nsilcmops"
tiernob24258a2018-10-04 18:39:49 +02001025 elif main_topic == "pdu":
1026 engine_topic = "pdus"
tierno65ca36d2019-02-12 19:27:52 +01001027 if engine_topic == "vims": # TODO this is for backward compatibility, it will be removed in the future
tiernob24258a2018-10-04 18:39:49 +02001028 engine_topic = "vim_accounts"
tiernoc94c3df2018-02-09 15:38:54 +01001029
1030 if method == "GET":
Felipe Vicens07f31722018-10-29 15:16:44 +01001031 if item in ("nsd_content", "package_content", "artifacts", "vnfd", "nsd", "nst", "nst_content"):
garciadeblas9750c5a2018-10-15 16:20:35 +02001032 if item in ("vnfd", "nsd", "nst"):
tiernof27c79b2018-03-12 17:08:42 +01001033 path = "$DESCRIPTOR"
1034 elif args:
1035 path = args
tiernob24258a2018-10-04 18:39:49 +02001036 elif item == "artifacts":
tiernof27c79b2018-03-12 17:08:42 +01001037 path = ()
1038 else:
1039 path = None
tierno701018c2019-06-25 11:13:14 +00001040 file, _format = self.engine.get_file(engine_session, engine_topic, _id, path,
tierno2236d202018-05-16 19:05:16 +02001041 cherrypy.request.headers.get("Accept"))
tiernof27c79b2018-03-12 17:08:42 +01001042 outdata = file
1043 elif not _id:
tierno701018c2019-06-25 11:13:14 +00001044 outdata = self.engine.get_item_list(engine_session, engine_topic, kwargs)
tiernoc94c3df2018-02-09 15:38:54 +01001045 else:
vijay.r35ef2f72019-04-30 17:55:49 +05301046 if item == "reports":
1047 # TODO check that project_id (_id in this context) has permissions
1048 _id = args[0]
tierno701018c2019-06-25 11:13:14 +00001049 outdata = self.engine.get_item(engine_session, engine_topic, _id)
delacruzramo271d2002019-12-02 21:00:37 +01001050
tiernof27c79b2018-03-12 17:08:42 +01001051 elif method == "POST":
tiernobdebce92019-07-01 15:36:49 +00001052 cherrypy.response.status = HTTPStatus.CREATED.value
garciadeblas9750c5a2018-10-15 16:20:35 +02001053 if topic in ("ns_descriptors_content", "vnf_packages_content", "netslice_templates_content"):
tiernof27c79b2018-03-12 17:08:42 +01001054 _id = cherrypy.request.headers.get("Transaction-Id")
1055 if not _id:
tiernobdebce92019-07-01 15:36:49 +00001056 _id, _ = self.engine.new_item(rollback, engine_session, engine_topic, {}, None,
1057 cherrypy.request.headers)
tierno701018c2019-06-25 11:13:14 +00001058 completed = self.engine.upload_content(engine_session, engine_topic, _id, indata, kwargs,
tierno65ca36d2019-02-12 19:27:52 +01001059 cherrypy.request.headers)
tiernof27c79b2018-03-12 17:08:42 +01001060 if completed:
tiernob24258a2018-10-04 18:39:49 +02001061 self._set_location_header(main_topic, version, topic, _id)
tiernof27c79b2018-03-12 17:08:42 +01001062 else:
1063 cherrypy.response.headers["Transaction-Id"] = _id
1064 outdata = {"id": _id}
tiernob24258a2018-10-04 18:39:49 +02001065 elif topic == "ns_instances_content":
1066 # creates NSR
tiernobdebce92019-07-01 15:36:49 +00001067 _id, _ = self.engine.new_item(rollback, engine_session, engine_topic, indata, kwargs)
tiernob24258a2018-10-04 18:39:49 +02001068 # creates nslcmop
1069 indata["lcmOperationType"] = "instantiate"
1070 indata["nsInstanceId"] = _id
tiernobdebce92019-07-01 15:36:49 +00001071 nslcmop_id, _ = self.engine.new_item(rollback, engine_session, "nslcmops", indata, None)
tiernob24258a2018-10-04 18:39:49 +02001072 self._set_location_header(main_topic, version, topic, _id)
kuuse078f55e2019-05-16 19:24:21 +02001073 outdata = {"id": _id, "nslcmop_id": nslcmop_id}
tiernob24258a2018-10-04 18:39:49 +02001074 elif topic == "ns_instances" and item:
1075 indata["lcmOperationType"] = item
1076 indata["nsInstanceId"] = _id
tiernobdebce92019-07-01 15:36:49 +00001077 _id, _ = self.engine.new_item(rollback, engine_session, "nslcmops", indata, kwargs)
tiernob24258a2018-10-04 18:39:49 +02001078 self._set_location_header(main_topic, version, "ns_lcm_op_occs", _id)
tierno65acb4d2018-04-06 16:42:40 +02001079 outdata = {"id": _id}
1080 cherrypy.response.status = HTTPStatus.ACCEPTED.value
garciadeblas9750c5a2018-10-15 16:20:35 +02001081 elif topic == "netslice_instances_content":
Felipe Vicens07f31722018-10-29 15:16:44 +01001082 # creates NetSlice_Instance_record (NSIR)
tiernobdebce92019-07-01 15:36:49 +00001083 _id, _ = self.engine.new_item(rollback, engine_session, engine_topic, indata, kwargs)
Felipe Vicens07f31722018-10-29 15:16:44 +01001084 self._set_location_header(main_topic, version, topic, _id)
garciadeblas9750c5a2018-10-15 16:20:35 +02001085 indata["lcmOperationType"] = "instantiate"
Felipe Vicens126af572019-06-05 19:13:04 +02001086 indata["netsliceInstanceId"] = _id
tiernobdebce92019-07-01 15:36:49 +00001087 nsilcmop_id, _ = self.engine.new_item(rollback, engine_session, "nsilcmops", indata, kwargs)
kuuse078f55e2019-05-16 19:24:21 +02001088 outdata = {"id": _id, "nsilcmop_id": nsilcmop_id}
garciadeblas9750c5a2018-10-15 16:20:35 +02001089 elif topic == "netslice_instances" and item:
1090 indata["lcmOperationType"] = item
Felipe Vicens126af572019-06-05 19:13:04 +02001091 indata["netsliceInstanceId"] = _id
tiernobdebce92019-07-01 15:36:49 +00001092 _id, _ = self.engine.new_item(rollback, engine_session, "nsilcmops", indata, kwargs)
garciadeblas9750c5a2018-10-15 16:20:35 +02001093 self._set_location_header(main_topic, version, "nsi_lcm_op_occs", _id)
1094 outdata = {"id": _id}
1095 cherrypy.response.status = HTTPStatus.ACCEPTED.value
delacruzramo271d2002019-12-02 21:00:37 +01001096 elif topic == "vnf_packages" and item == "action":
1097 indata["lcmOperationType"] = item
1098 indata["vnfPkgId"] = _id
1099 _id, _ = self.engine.new_item(rollback, engine_session, "vnfpkgops", indata, kwargs)
1100 self._set_location_header(main_topic, version, "vnfpkg_op_occs", _id)
1101 outdata = {"id": _id}
1102 cherrypy.response.status = HTTPStatus.ACCEPTED.value
tiernof27c79b2018-03-12 17:08:42 +01001103 else:
tiernobdebce92019-07-01 15:36:49 +00001104 _id, op_id = self.engine.new_item(rollback, engine_session, engine_topic, indata, kwargs,
1105 cherrypy.request.headers)
tiernob24258a2018-10-04 18:39:49 +02001106 self._set_location_header(main_topic, version, topic, _id)
tiernof27c79b2018-03-12 17:08:42 +01001107 outdata = {"id": _id}
tiernobdebce92019-07-01 15:36:49 +00001108 if op_id:
1109 outdata["op_id"] = op_id
1110 cherrypy.response.status = HTTPStatus.ACCEPTED.value
tiernob24258a2018-10-04 18:39:49 +02001111 # TODO form NsdInfo when topic in ("ns_descriptors", "vnf_packages")
tierno09c073e2018-04-26 13:36:48 +02001112
tiernoc94c3df2018-02-09 15:38:54 +01001113 elif method == "DELETE":
1114 if not _id:
tierno701018c2019-06-25 11:13:14 +00001115 outdata = self.engine.del_item_list(engine_session, engine_topic, kwargs)
tierno09c073e2018-04-26 13:36:48 +02001116 cherrypy.response.status = HTTPStatus.OK.value
tiernoc94c3df2018-02-09 15:38:54 +01001117 else: # len(args) > 1
tiernoe8631782018-12-21 13:31:52 +00001118 delete_in_process = False
tierno701018c2019-06-25 11:13:14 +00001119 if topic == "ns_instances_content" and not engine_session["force"]:
tiernob24258a2018-10-04 18:39:49 +02001120 nslcmop_desc = {
1121 "lcmOperationType": "terminate",
1122 "nsInstanceId": _id,
1123 "autoremove": True
1124 }
tierno1c38f2f2020-03-24 11:51:39 +00001125 opp_id, _ = self.engine.new_item(rollback, engine_session, "nslcmops", nslcmop_desc, kwargs)
tiernoe8631782018-12-21 13:31:52 +00001126 if opp_id:
1127 delete_in_process = True
1128 outdata = {"_id": opp_id}
1129 cherrypy.response.status = HTTPStatus.ACCEPTED.value
tierno701018c2019-06-25 11:13:14 +00001130 elif topic == "netslice_instances_content" and not engine_session["force"]:
garciadeblas9750c5a2018-10-15 16:20:35 +02001131 nsilcmop_desc = {
1132 "lcmOperationType": "terminate",
Felipe Vicens126af572019-06-05 19:13:04 +02001133 "netsliceInstanceId": _id,
garciadeblas9750c5a2018-10-15 16:20:35 +02001134 "autoremove": True
1135 }
tiernobdebce92019-07-01 15:36:49 +00001136 opp_id, _ = self.engine.new_item(rollback, engine_session, "nsilcmops", nsilcmop_desc, None)
tiernoe8631782018-12-21 13:31:52 +00001137 if opp_id:
1138 delete_in_process = True
1139 outdata = {"_id": opp_id}
1140 cherrypy.response.status = HTTPStatus.ACCEPTED.value
1141 if not delete_in_process:
tierno701018c2019-06-25 11:13:14 +00001142 self.engine.del_item(engine_session, engine_topic, _id)
tierno09c073e2018-04-26 13:36:48 +02001143 cherrypy.response.status = HTTPStatus.NO_CONTENT.value
delacruzramofe598fe2019-10-23 18:25:11 +02001144 if engine_topic in ("vim_accounts", "wim_accounts", "sdns", "k8sclusters", "k8srepos"):
tierno09c073e2018-04-26 13:36:48 +02001145 cherrypy.response.status = HTTPStatus.ACCEPTED.value
1146
tierno7ae10112018-05-18 14:36:02 +02001147 elif method in ("PUT", "PATCH"):
tiernobdebce92019-07-01 15:36:49 +00001148 op_id = None
tierno701018c2019-06-25 11:13:14 +00001149 if not indata and not kwargs and not engine_session.get("set_project"):
tiernoc94c3df2018-02-09 15:38:54 +01001150 raise NbiException("Nothing to update. Provide payload and/or query string",
1151 HTTPStatus.BAD_REQUEST)
garciadeblas9750c5a2018-10-15 16:20:35 +02001152 if item in ("nsd_content", "package_content", "nst_content") and method == "PUT":
tierno701018c2019-06-25 11:13:14 +00001153 completed = self.engine.upload_content(engine_session, engine_topic, _id, indata, kwargs,
tierno65ca36d2019-02-12 19:27:52 +01001154 cherrypy.request.headers)
tiernof27c79b2018-03-12 17:08:42 +01001155 if not completed:
1156 cherrypy.response.headers["Transaction-Id"] = id
tiernof27c79b2018-03-12 17:08:42 +01001157 else:
tiernobdebce92019-07-01 15:36:49 +00001158 op_id = self.engine.edit_item(engine_session, engine_topic, _id, indata, kwargs)
1159
1160 if op_id:
1161 cherrypy.response.status = HTTPStatus.ACCEPTED.value
1162 outdata = {"op_id": op_id}
1163 else:
1164 cherrypy.response.status = HTTPStatus.NO_CONTENT.value
1165 outdata = None
tiernoc94c3df2018-02-09 15:38:54 +01001166 else:
1167 raise NbiException("Method {} not allowed".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
tiernoa6bb45d2019-06-14 09:45:39 +00001168
1169 # if Role information changes, it is needed to reload the information of roles
1170 if topic == "roles" and method != "GET":
1171 self.authenticator.load_operation_to_allowed_roles()
delacruzramoad682a52019-12-10 16:26:34 +01001172
1173 if topic == "projects" and method == "DELETE" \
1174 or topic in ["users", "roles"] and method in ["PUT", "PATCH", "DELETE"]:
1175 self.authenticator.remove_token_from_cache()
1176
tierno701018c2019-06-25 11:13:14 +00001177 return self._format_out(outdata, token_info, _format)
tiernob24258a2018-10-04 18:39:49 +02001178 except Exception as e:
tierno36ec8602018-11-02 17:27:11 +01001179 if isinstance(e, (NbiException, EngineException, DbException, FsException, MsgException, AuthException,
delacruzramo01b15d32019-07-02 14:37:47 +02001180 ValidationError, AuthconnException)):
tiernob24258a2018-10-04 18:39:49 +02001181 http_code_value = cherrypy.response.status = e.http_code.value
1182 http_code_name = e.http_code.name
1183 cherrypy.log("Exception {}".format(e))
1184 else:
1185 http_code_value = cherrypy.response.status = HTTPStatus.BAD_REQUEST.value # INTERNAL_SERVER_ERROR
Felipe Vicense36ab852018-11-23 14:12:09 +01001186 cherrypy.log("CRITICAL: Exception {}".format(e), traceback=True)
tiernob24258a2018-10-04 18:39:49 +02001187 http_code_name = HTTPStatus.BAD_REQUEST.name
tierno3ace63c2018-05-03 17:51:43 +02001188 if hasattr(outdata, "close"): # is an open file
1189 outdata.close()
tiernob24258a2018-10-04 18:39:49 +02001190 error_text = str(e)
1191 rollback.reverse()
tiernodb9dc582018-06-20 17:27:29 +02001192 for rollback_item in rollback:
tierno3ace63c2018-05-03 17:51:43 +02001193 try:
tiernocc103432018-10-19 14:10:35 +02001194 if rollback_item.get("operation") == "set":
1195 self.engine.db.set_one(rollback_item["topic"], {"_id": rollback_item["_id"]},
1196 rollback_item["content"], fail_on_empty=False)
1197 else:
tiernoe8631782018-12-21 13:31:52 +00001198 self.engine.db.del_one(rollback_item["topic"], {"_id": rollback_item["_id"]},
1199 fail_on_empty=False)
tierno3ace63c2018-05-03 17:51:43 +02001200 except Exception as e2:
tiernob24258a2018-10-04 18:39:49 +02001201 rollback_error_text = "Rollback Exception {}: {}".format(rollback_item, e2)
1202 cherrypy.log(rollback_error_text)
1203 error_text += ". " + rollback_error_text
1204 # if isinstance(e, MsgException):
1205 # error_text = "{} has been '{}' but other modules cannot be informed because an error on bus".format(
1206 # engine_topic[:-1], method, error_text)
tiernoc94c3df2018-02-09 15:38:54 +01001207 problem_details = {
tiernob24258a2018-10-04 18:39:49 +02001208 "code": http_code_name,
1209 "status": http_code_value,
1210 "detail": error_text,
tiernoc94c3df2018-02-09 15:38:54 +01001211 }
tierno701018c2019-06-25 11:13:14 +00001212 return self._format_out(problem_details, token_info)
tiernoc94c3df2018-02-09 15:38:54 +01001213 # raise cherrypy.HTTPError(e.http_code.value, str(e))
tiernoa5035702019-07-29 08:54:42 +00001214 finally:
1215 if token_info:
1216 self._format_login(token_info)
1217 if method in ("PUT", "PATCH", "POST") and isinstance(outdata, dict):
1218 for logging_id in ("id", "op_id", "nsilcmop_id", "nslcmop_id"):
1219 if outdata.get(logging_id):
1220 cherrypy.request.login += ";{}={}".format(logging_id, outdata[logging_id][:36])
tiernoc94c3df2018-02-09 15:38:54 +01001221
1222
tiernoc94c3df2018-02-09 15:38:54 +01001223def _start_service():
1224 """
1225 Callback function called when cherrypy.engine starts
1226 Override configuration with env variables
1227 Set database, storage, message configuration
1228 Init database with admin/admin user password
1229 """
tierno932499c2019-01-28 17:28:10 +00001230 global nbi_server
1231 global subscription_thread
tiernoc94c3df2018-02-09 15:38:54 +01001232 cherrypy.log.error("Starting osm_nbi")
1233 # update general cherrypy configuration
1234 update_dict = {}
1235
1236 engine_config = cherrypy.tree.apps['/osm'].config
1237 for k, v in environ.items():
1238 if not k.startswith("OSMNBI_"):
1239 continue
tiernoe1281182018-05-22 12:24:36 +02001240 k1, _, k2 = k[7:].lower().partition("_")
tiernoc94c3df2018-02-09 15:38:54 +01001241 if not k2:
1242 continue
1243 try:
1244 # update static configuration
1245 if k == 'OSMNBI_STATIC_DIR':
1246 engine_config["/static"]['tools.staticdir.dir'] = v
1247 engine_config["/static"]['tools.staticdir.on'] = True
1248 elif k == 'OSMNBI_SOCKET_PORT' or k == 'OSMNBI_SERVER_PORT':
1249 update_dict['server.socket_port'] = int(v)
1250 elif k == 'OSMNBI_SOCKET_HOST' or k == 'OSMNBI_SERVER_HOST':
1251 update_dict['server.socket_host'] = v
tiernof5298be2018-05-16 14:43:57 +02001252 elif k1 in ("server", "test", "auth", "log"):
1253 update_dict[k1 + '.' + k2] = v
Eduardo Sousa819d34c2018-07-31 01:20:02 +01001254 elif k1 in ("message", "database", "storage", "authentication"):
tiernof5298be2018-05-16 14:43:57 +02001255 # k2 = k2.replace('_', '.')
Eduardo Sousa819d34c2018-07-31 01:20:02 +01001256 if k2 in ("port", "db_port"):
tiernoc94c3df2018-02-09 15:38:54 +01001257 engine_config[k1][k2] = int(v)
1258 else:
1259 engine_config[k1][k2] = v
Eduardo Sousa819d34c2018-07-31 01:20:02 +01001260
tiernoc94c3df2018-02-09 15:38:54 +01001261 except ValueError as e:
1262 cherrypy.log.error("Ignoring environ '{}': " + str(e))
1263 except Exception as e:
1264 cherrypy.log.warn("skipping environ '{}' on exception '{}'".format(k, e))
1265
1266 if update_dict:
1267 cherrypy.config.update(update_dict)
tiernof5298be2018-05-16 14:43:57 +02001268 engine_config["global"].update(update_dict)
tiernoc94c3df2018-02-09 15:38:54 +01001269
1270 # logging cherrypy
1271 log_format_simple = "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(message)s"
1272 log_formatter_simple = logging.Formatter(log_format_simple, datefmt='%Y-%m-%dT%H:%M:%S')
1273 logger_server = logging.getLogger("cherrypy.error")
1274 logger_access = logging.getLogger("cherrypy.access")
1275 logger_cherry = logging.getLogger("cherrypy")
1276 logger_nbi = logging.getLogger("nbi")
1277
tiernof5298be2018-05-16 14:43:57 +02001278 if "log.file" in engine_config["global"]:
1279 file_handler = logging.handlers.RotatingFileHandler(engine_config["global"]["log.file"],
tiernoc94c3df2018-02-09 15:38:54 +01001280 maxBytes=100e6, backupCount=9, delay=0)
1281 file_handler.setFormatter(log_formatter_simple)
1282 logger_cherry.addHandler(file_handler)
1283 logger_nbi.addHandler(file_handler)
tiernob24258a2018-10-04 18:39:49 +02001284 # log always to standard output
1285 for format_, logger in {"nbi.server %(filename)s:%(lineno)s": logger_server,
1286 "nbi.access %(filename)s:%(lineno)s": logger_access,
1287 "%(name)s %(filename)s:%(lineno)s": logger_nbi
1288 }.items():
1289 log_format_cherry = "%(asctime)s %(levelname)s {} %(message)s".format(format_)
1290 log_formatter_cherry = logging.Formatter(log_format_cherry, datefmt='%Y-%m-%dT%H:%M:%S')
1291 str_handler = logging.StreamHandler()
1292 str_handler.setFormatter(log_formatter_cherry)
1293 logger.addHandler(str_handler)
tiernoc94c3df2018-02-09 15:38:54 +01001294
tiernof5298be2018-05-16 14:43:57 +02001295 if engine_config["global"].get("log.level"):
1296 logger_cherry.setLevel(engine_config["global"]["log.level"])
1297 logger_nbi.setLevel(engine_config["global"]["log.level"])
tiernoc94c3df2018-02-09 15:38:54 +01001298
1299 # logging other modules
1300 for k1, logname in {"message": "nbi.msg", "database": "nbi.db", "storage": "nbi.fs"}.items():
1301 engine_config[k1]["logger_name"] = logname
1302 logger_module = logging.getLogger(logname)
1303 if "logfile" in engine_config[k1]:
1304 file_handler = logging.handlers.RotatingFileHandler(engine_config[k1]["logfile"],
tierno2236d202018-05-16 19:05:16 +02001305 maxBytes=100e6, backupCount=9, delay=0)
tiernoc94c3df2018-02-09 15:38:54 +01001306 file_handler.setFormatter(log_formatter_simple)
1307 logger_module.addHandler(file_handler)
1308 if "loglevel" in engine_config[k1]:
1309 logger_module.setLevel(engine_config[k1]["loglevel"])
1310 # TODO add more entries, e.g.: storage
1311 cherrypy.tree.apps['/osm'].root.engine.start(engine_config)
Eduardo Sousa819d34c2018-07-31 01:20:02 +01001312 cherrypy.tree.apps['/osm'].root.authenticator.start(engine_config)
tiernod985a8d2018-10-19 14:12:28 +02001313 cherrypy.tree.apps['/osm'].root.engine.init_db(target_version=database_version)
1314 cherrypy.tree.apps['/osm'].root.authenticator.init_db(target_version=auth_database_version)
tiernobee508e2019-01-21 11:21:49 +00001315
tierno932499c2019-01-28 17:28:10 +00001316 # start subscriptions thread:
1317 subscription_thread = SubscriptionThread(config=engine_config, engine=nbi_server.engine)
1318 subscription_thread.start()
1319 # Do not capture except SubscriptionException
1320
tiernob2e48bd2020-02-04 15:47:18 +00001321 backend = engine_config["authentication"]["backend"]
1322 cherrypy.log.error("Starting OSM NBI Version '{} {}' with '{}' authentication backend"
1323 .format(nbi_version, nbi_version_date, backend))
tiernoc94c3df2018-02-09 15:38:54 +01001324
1325
1326def _stop_service():
1327 """
1328 Callback function called when cherrypy.engine stops
1329 TODO: Ending database connections.
1330 """
tierno932499c2019-01-28 17:28:10 +00001331 global subscription_thread
tierno65ca36d2019-02-12 19:27:52 +01001332 if subscription_thread:
1333 subscription_thread.terminate()
tierno932499c2019-01-28 17:28:10 +00001334 subscription_thread = None
tiernoc94c3df2018-02-09 15:38:54 +01001335 cherrypy.tree.apps['/osm'].root.engine.stop()
1336 cherrypy.log.error("Stopping osm_nbi")
1337
tierno2236d202018-05-16 19:05:16 +02001338
tiernof5298be2018-05-16 14:43:57 +02001339def nbi(config_file):
tierno932499c2019-01-28 17:28:10 +00001340 global nbi_server
tiernoc94c3df2018-02-09 15:38:54 +01001341 # conf = {
1342 # '/': {
1343 # #'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
1344 # 'tools.sessions.on': True,
1345 # 'tools.response_headers.on': True,
1346 # # 'tools.response_headers.headers': [('Content-Type', 'text/plain')],
1347 # }
1348 # }
1349 # cherrypy.Server.ssl_module = 'builtin'
1350 # cherrypy.Server.ssl_certificate = "http/cert.pem"
1351 # cherrypy.Server.ssl_private_key = "http/privkey.pem"
1352 # cherrypy.Server.thread_pool = 10
1353 # cherrypy.config.update({'Server.socket_port': config["port"], 'Server.socket_host': config["host"]})
1354
1355 # cherrypy.config.update({'tools.auth_basic.on': True,
1356 # 'tools.auth_basic.realm': 'localhost',
1357 # 'tools.auth_basic.checkpassword': validate_password})
tierno932499c2019-01-28 17:28:10 +00001358 nbi_server = Server()
tiernoc94c3df2018-02-09 15:38:54 +01001359 cherrypy.engine.subscribe('start', _start_service)
1360 cherrypy.engine.subscribe('stop', _stop_service)
tierno932499c2019-01-28 17:28:10 +00001361 cherrypy.quickstart(nbi_server, '/osm', config_file)
tiernof5298be2018-05-16 14:43:57 +02001362
1363
1364def usage():
1365 print("""Usage: {} [options]
1366 -c|--config [configuration_file]: loads the configuration file (default: ./nbi.cfg)
1367 -h|--help: shows this help
1368 """.format(sys.argv[0]))
tierno2236d202018-05-16 19:05:16 +02001369 # --log-socket-host HOST: send logs to this host")
1370 # --log-socket-port PORT: send logs using this port (default: 9022)")
tiernoc94c3df2018-02-09 15:38:54 +01001371
1372
1373if __name__ == '__main__':
tiernof5298be2018-05-16 14:43:57 +02001374 try:
1375 # load parameters and configuration
1376 opts, args = getopt.getopt(sys.argv[1:], "hvc:", ["config=", "help"])
1377 # TODO add "log-socket-host=", "log-socket-port=", "log-file="
1378 config_file = None
1379 for o, a in opts:
1380 if o in ("-h", "--help"):
1381 usage()
1382 sys.exit()
1383 elif o in ("-c", "--config"):
1384 config_file = a
1385 # elif o == "--log-socket-port":
1386 # log_socket_port = a
1387 # elif o == "--log-socket-host":
1388 # log_socket_host = a
1389 # elif o == "--log-file":
1390 # log_file = a
1391 else:
1392 assert False, "Unhandled option"
1393 if config_file:
1394 if not path.isfile(config_file):
1395 print("configuration file '{}' that not exist".format(config_file), file=sys.stderr)
1396 exit(1)
1397 else:
1398 for config_file in (__file__[:__file__.rfind(".")] + ".cfg", "./nbi.cfg", "/etc/osm/nbi.cfg"):
1399 if path.isfile(config_file):
1400 break
1401 else:
1402 print("No configuration file 'nbi.cfg' found neither at local folder nor at /etc/osm/", file=sys.stderr)
1403 exit(1)
1404 nbi(config_file)
1405 except getopt.GetoptError as e:
1406 print(str(e), file=sys.stderr)
1407 # usage()
1408 exit(1)