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