Revert "Revert "Feature 10915: SOL003 STD Support for OSM""
[osm/NBI.git] / osm_nbi / nbi.py
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 import cherrypy
18 import time
19 import json
20 import yaml
21 import osm_nbi.html_out as html
22 import logging
23 import logging.handlers
24 import getopt
25 import sys
26
27 from osm_nbi.authconn import AuthException, AuthconnException
28 from osm_nbi.auth import Authenticator
29 from osm_nbi.engine import Engine, EngineException
30 from osm_nbi.subscriptions import SubscriptionThread
31 from osm_nbi.validation import ValidationError
32 from osm_common.dbbase import DbException
33 from osm_common.fsbase import FsException
34 from osm_common.msgbase import MsgException
35 from http import HTTPStatus
36 from codecs import getreader
37 from os import environ, path
38 from osm_nbi import version as nbi_version, version_date as nbi_version_date
39
40 __author__ = "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
41
42 __version__ = "0.1.3" # file version, not NBI version
43 version_date = "Aug 2019"
44
45 database_version = "1.2"
46 auth_database_version = "1.0"
47 nbi_server = None # instance of Server class
48 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 update 05
90 /ns_lcm_op_occs 5 5
91 /<nsLcmOpOccId> 5 5 5
92 TO BE COMPLETED 5 5
93 /vnf_instances (also vnfrs for compatibility) O
94 /<vnfInstanceId> O
95 /subscriptions 5 5
96 /<subscriptionId> 5 X
97
98 /pdu/v1
99 /pdu_descriptors O O
100 /<id> O O O O
101
102 /admin/v1
103 /tokens O O
104 /<id> O O
105 /users O O
106 /<id> O O O O
107 /projects O O
108 /<id> O O
109 /vim_accounts (also vims for compatibility) O O
110 /<id> O O O
111 /wim_accounts O O
112 /<id> O O O
113 /sdns O O
114 /<id> O O O
115 /k8sclusters O O
116 /<id> O O O
117 /k8srepos O O
118 /<id> O O
119 /osmrepos O O
120 /<id> O O
121
122 /nst/v1 O O
123 /netslice_templates_content O O
124 /<nstInfoId> O O O O
125 /netslice_templates O O
126 /<nstInfoId> O O O
127 /nst_content O O
128 /nst O
129 /artifacts[/<artifactPath>] O
130 /subscriptions X X
131 /<subscriptionId> X X
132
133 /nsilcm/v1
134 /netslice_instances_content O O
135 /<SliceInstanceId> O O
136 /netslice_instances O O
137 /<SliceInstanceId> O O
138 instantiate O
139 terminate O
140 action O
141 /nsi_lcm_op_occs O O
142 /<nsiLcmOpOccId> O O O
143 /subscriptions X X
144 /<subscriptionId> X X
145
146 query string:
147 Follows SOL005 section 4.3.2 It contains extra METHOD to override http method, FORCE to force.
148 simpleFilterExpr := <attrName>["."<attrName>]*["."<op>]"="<value>[","<value>]*
149 filterExpr := <simpleFilterExpr>["&"<simpleFilterExpr>]*
150 op := "eq" | "neq" (or "ne") | "gt" | "lt" | "gte" | "lte" | "cont" | "ncont"
151 attrName := string
152 For filtering inside array, it must select the element of the array, or add ANYINDEX to apply the filtering over any
153 item of the array, that is, pass if any item of the array pass the filter.
154 It allows both ne and neq for not equal
155 TODO: 4.3.3 Attribute selectors
156 all_fields, fields=x,y,.., exclude_default, exclude_fields=x,y,...
157 (none) … same as “exclude_default”
158 all_fields … all attributes.
159 fields=<list> … all attributes except all complex attributes with minimum cardinality of zero that are not
160 conditionally mandatory, and that are not provided in <list>.
161 exclude_fields=<list> … all attributes except those complex attributes with a minimum cardinality of zero that
162 are not conditionally mandatory, and that are provided in <list>.
163 exclude_default … all attributes except those complex attributes with a minimum cardinality of zero that are not
164 conditionally mandatory, and that are part of the "default exclude set" defined in the present specification for
165 the particular resource
166 exclude_default and include=<list> … all attributes except those complex attributes with a minimum cardinality
167 of zero that are not conditionally mandatory and that are part of the "default exclude set" defined in the
168 present specification for the particular resource, but that are not part of <list>
169 Additionally it admits some administrator values:
170 FORCE: To force operations skipping dependency checkings
171 ADMIN: To act as an administrator or a different project
172 PUBLIC: To get public descriptors or set a descriptor as public
173 SET_PROJECT: To make a descriptor available for other project
174
175 Header field name Reference Example Descriptions
176 Accept IETF RFC 7231 [19] application/json Content-Types that are acceptable for the response.
177 This header field shall be present if the response is expected to have a non-empty message body.
178 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the request.
179 This header field shall be present if the request has a non-empty message body.
180 Authorization IETF RFC 7235 [22] Bearer mF_9.B5f-4.1JqM The authorization token for the request.
181 Details are specified in clause 4.5.3.
182 Range IETF RFC 7233 [21] 1000-2000 Requested range of bytes from a file
183 Header field name Reference Example Descriptions
184 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the response.
185 This header field shall be present if the response has a non-empty message body.
186 Location IETF RFC 7231 [19] http://www.example.com/vnflcm/v1/vnf_instances/123 Used in redirection, or when a
187 new resource has been created.
188 This header field shall be present if the response status code is 201 or 3xx.
189 In the present document this header field is also used if the response status code is 202 and a new resource was
190 created.
191 WWW-Authenticate IETF RFC 7235 [22] Bearer realm="example" Challenge if the corresponding HTTP request has not
192 provided authorization, or error details if the corresponding HTTP request has provided an invalid authorization
193 token.
194 Accept-Ranges IETF RFC 7233 [21] bytes Used by the Server to signal whether or not it supports ranges for
195 certain resources.
196 Content-Range IETF RFC 7233 [21] bytes 21010-47021/ 47022 Signals the byte range that is contained in the
197 response, and the total length of the file.
198 Retry-After IETF RFC 7231 [19] Fri, 31 Dec 1999 23:59:59 GMT
199 """
200
201 valid_query_string = ("ADMIN", "SET_PROJECT", "FORCE", "PUBLIC")
202 # ^ Contains possible administrative query string words:
203 # ADMIN=True(by default)|Project|Project-list: See all elements, or elements of a project
204 # (not owned by my session project).
205 # PUBLIC=True(by default)|False: See/hide public elements. Set/Unset a topic to be public
206 # FORCE=True(by default)|False: Force edition/deletion operations
207 # SET_PROJECT=Project|Project-list: Add/Delete the topic to the projects portfolio
208
209 valid_url_methods = {
210 # contains allowed URL and methods, and the role_permission name
211 "admin": {
212 "v1": {
213 "tokens": {
214 "METHODS": ("GET", "POST", "DELETE"),
215 "ROLE_PERMISSION": "tokens:",
216 "<ID>": {"METHODS": ("GET", "DELETE"), "ROLE_PERMISSION": "tokens:id:"},
217 },
218 "users": {
219 "METHODS": ("GET", "POST"),
220 "ROLE_PERMISSION": "users:",
221 "<ID>": {
222 "METHODS": ("GET", "DELETE", "PATCH"),
223 "ROLE_PERMISSION": "users:id:",
224 },
225 },
226 "projects": {
227 "METHODS": ("GET", "POST"),
228 "ROLE_PERMISSION": "projects:",
229 "<ID>": {
230 "METHODS": ("GET", "DELETE", "PATCH"),
231 "ROLE_PERMISSION": "projects:id:",
232 },
233 },
234 "roles": {
235 "METHODS": ("GET", "POST"),
236 "ROLE_PERMISSION": "roles:",
237 "<ID>": {
238 "METHODS": ("GET", "DELETE", "PATCH"),
239 "ROLE_PERMISSION": "roles:id:",
240 },
241 },
242 "vims": {
243 "METHODS": ("GET", "POST"),
244 "ROLE_PERMISSION": "vims:",
245 "<ID>": {
246 "METHODS": ("GET", "DELETE", "PATCH"),
247 "ROLE_PERMISSION": "vims:id:",
248 },
249 },
250 "vim_accounts": {
251 "METHODS": ("GET", "POST"),
252 "ROLE_PERMISSION": "vim_accounts:",
253 "<ID>": {
254 "METHODS": ("GET", "DELETE", "PATCH"),
255 "ROLE_PERMISSION": "vim_accounts:id:",
256 },
257 },
258 "wim_accounts": {
259 "METHODS": ("GET", "POST"),
260 "ROLE_PERMISSION": "wim_accounts:",
261 "<ID>": {
262 "METHODS": ("GET", "DELETE", "PATCH"),
263 "ROLE_PERMISSION": "wim_accounts:id:",
264 },
265 },
266 "sdns": {
267 "METHODS": ("GET", "POST"),
268 "ROLE_PERMISSION": "sdn_controllers:",
269 "<ID>": {
270 "METHODS": ("GET", "DELETE", "PATCH"),
271 "ROLE_PERMISSION": "sdn_controllers:id:",
272 },
273 },
274 "k8sclusters": {
275 "METHODS": ("GET", "POST"),
276 "ROLE_PERMISSION": "k8sclusters:",
277 "<ID>": {
278 "METHODS": ("GET", "DELETE", "PATCH"),
279 "ROLE_PERMISSION": "k8sclusters:id:",
280 },
281 },
282 "vca": {
283 "METHODS": ("GET", "POST"),
284 "ROLE_PERMISSION": "vca:",
285 "<ID>": {
286 "METHODS": ("GET", "DELETE", "PATCH"),
287 "ROLE_PERMISSION": "vca:id:",
288 },
289 },
290 "k8srepos": {
291 "METHODS": ("GET", "POST"),
292 "ROLE_PERMISSION": "k8srepos:",
293 "<ID>": {
294 "METHODS": ("GET", "DELETE"),
295 "ROLE_PERMISSION": "k8srepos:id:",
296 },
297 },
298 "osmrepos": {
299 "METHODS": ("GET", "POST"),
300 "ROLE_PERMISSION": "osmrepos:",
301 "<ID>": {
302 "METHODS": ("GET", "DELETE", "PATCH"),
303 "ROLE_PERMISSION": "osmrepos:id:",
304 },
305 },
306 "domains": {
307 "METHODS": ("GET",),
308 "ROLE_PERMISSION": "domains:",
309 },
310 }
311 },
312 "pdu": {
313 "v1": {
314 "pdu_descriptors": {
315 "METHODS": ("GET", "POST"),
316 "ROLE_PERMISSION": "pduds:",
317 "<ID>": {
318 "METHODS": ("GET", "POST", "DELETE", "PATCH", "PUT"),
319 "ROLE_PERMISSION": "pduds:id:",
320 },
321 },
322 }
323 },
324 "nsd": {
325 "v1": {
326 "ns_descriptors_content": {
327 "METHODS": ("GET", "POST"),
328 "ROLE_PERMISSION": "nsds:",
329 "<ID>": {
330 "METHODS": ("GET", "PUT", "DELETE"),
331 "ROLE_PERMISSION": "nsds:id:",
332 },
333 },
334 "ns_descriptors": {
335 "METHODS": ("GET", "POST"),
336 "ROLE_PERMISSION": "nsds:",
337 "<ID>": {
338 "METHODS": ("GET", "DELETE", "PATCH"),
339 "ROLE_PERMISSION": "nsds:id:",
340 "nsd_content": {
341 "METHODS": ("GET", "PUT"),
342 "ROLE_PERMISSION": "nsds:id:content:",
343 },
344 "nsd": {
345 "METHODS": ("GET",), # descriptor inside package
346 "ROLE_PERMISSION": "nsds:id:content:",
347 },
348 "artifacts": {
349 "METHODS": ("GET",),
350 "ROLE_PERMISSION": "nsds:id:nsd_artifact:",
351 "*": None,
352 },
353 },
354 },
355 "pnf_descriptors": {
356 "TODO": ("GET", "POST"),
357 "<ID>": {
358 "TODO": ("GET", "DELETE", "PATCH"),
359 "pnfd_content": {"TODO": ("GET", "PUT")},
360 },
361 },
362 "subscriptions": {
363 "TODO": ("GET", "POST"),
364 "<ID>": {"TODO": ("GET", "DELETE")},
365 },
366 }
367 },
368 "vnfpkgm": {
369 "v1": {
370 "vnf_packages_content": {
371 "METHODS": ("GET", "POST"),
372 "ROLE_PERMISSION": "vnfds:",
373 "<ID>": {
374 "METHODS": ("GET", "PUT", "DELETE"),
375 "ROLE_PERMISSION": "vnfds:id:",
376 },
377 },
378 "vnf_packages": {
379 "METHODS": ("GET", "POST"),
380 "ROLE_PERMISSION": "vnfds:",
381 "<ID>": {
382 "METHODS": ("GET", "DELETE", "PATCH"), # GET: vnfPkgInfo
383 "ROLE_PERMISSION": "vnfds:id:",
384 "package_content": {
385 "METHODS": ("GET", "PUT"), # package
386 "ROLE_PERMISSION": "vnfds:id:",
387 "upload_from_uri": {
388 "METHODS": (),
389 "TODO": ("POST",),
390 "ROLE_PERMISSION": "vnfds:id:upload:",
391 },
392 },
393 "vnfd": {
394 "METHODS": ("GET",), # descriptor inside package
395 "ROLE_PERMISSION": "vnfds:id:content:",
396 },
397 "artifacts": {
398 "METHODS": ("GET",),
399 "ROLE_PERMISSION": "vnfds:id:vnfd_artifact:",
400 "*": None,
401 },
402 "action": {
403 "METHODS": ("POST",),
404 "ROLE_PERMISSION": "vnfds:id:action:",
405 },
406 },
407 },
408 "subscriptions": {
409 "TODO": ("GET", "POST"),
410 "<ID>": {"TODO": ("GET", "DELETE")},
411 },
412 "vnfpkg_op_occs": {
413 "METHODS": ("GET",),
414 "ROLE_PERMISSION": "vnfds:vnfpkgops:",
415 "<ID>": {"METHODS": ("GET",), "ROLE_PERMISSION": "vnfds:vnfpkgops:id:"},
416 },
417 }
418 },
419 "nslcm": {
420 "v1": {
421 "ns_instances_content": {
422 "METHODS": ("GET", "POST"),
423 "ROLE_PERMISSION": "ns_instances:",
424 "<ID>": {
425 "METHODS": ("GET", "DELETE"),
426 "ROLE_PERMISSION": "ns_instances:id:",
427 },
428 },
429 "ns_instances": {
430 "METHODS": ("GET", "POST"),
431 "ROLE_PERMISSION": "ns_instances:",
432 "<ID>": {
433 "METHODS": ("GET", "DELETE"),
434 "ROLE_PERMISSION": "ns_instances:id:",
435 "scale": {
436 "METHODS": ("POST",),
437 "ROLE_PERMISSION": "ns_instances:id:scale:",
438 },
439 "terminate": {
440 "METHODS": ("POST",),
441 "ROLE_PERMISSION": "ns_instances:id:terminate:",
442 },
443 "instantiate": {
444 "METHODS": ("POST",),
445 "ROLE_PERMISSION": "ns_instances:id:instantiate:",
446 },
447 "action": {
448 "METHODS": ("POST",),
449 "ROLE_PERMISSION": "ns_instances:id:action:",
450 },
451 "update": {
452 "METHODS": ("POST",),
453 "ROLE_PERMISSION": "ns_instances:id:update:",
454 },
455 },
456 },
457 "ns_lcm_op_occs": {
458 "METHODS": ("GET",),
459 "ROLE_PERMISSION": "ns_instances:opps:",
460 "<ID>": {
461 "METHODS": ("GET",),
462 "ROLE_PERMISSION": "ns_instances:opps:id:",
463 },
464 },
465 "vnfrs": {
466 "METHODS": ("GET",),
467 "ROLE_PERMISSION": "vnf_instances:",
468 "<ID>": {"METHODS": ("GET",), "ROLE_PERMISSION": "vnf_instances:id:"},
469 },
470 "vnf_instances": {
471 "METHODS": ("GET",),
472 "ROLE_PERMISSION": "vnf_instances:",
473 "<ID>": {"METHODS": ("GET",), "ROLE_PERMISSION": "vnf_instances:id:"},
474 },
475 "subscriptions": {
476 "METHODS": ("GET", "POST"),
477 "ROLE_PERMISSION": "ns_subscriptions:",
478 "<ID>": {
479 "METHODS": ("GET", "DELETE"),
480 "ROLE_PERMISSION": "ns_subscriptions:id:",
481 },
482 },
483 }
484 },
485 "vnflcm": {
486 "v1": {
487 "vnf_instances": {"METHODS": ("GET", "POST"),
488 "ROLE_PERMISSION": "vnflcm_instances:",
489 "<ID>": {"METHODS": ("GET", "DELETE"),
490 "ROLE_PERMISSION": "vnflcm_instances:id:",
491 "scale": {"METHODS": ("POST",),
492 "ROLE_PERMISSION": "vnflcm_instances:id:scale:"
493 },
494 "terminate": {"METHODS": ("POST",),
495 "ROLE_PERMISSION": "vnflcm_instances:id:terminate:"
496 },
497 "instantiate": {"METHODS": ("POST",),
498 "ROLE_PERMISSION": "vnflcm_instances:id:instantiate:"
499 },
500 }
501 },
502 "vnf_lcm_op_occs": {"METHODS": ("GET",),
503 "ROLE_PERMISSION": "vnf_instances:opps:",
504 "<ID>": {"METHODS": ("GET",),
505 "ROLE_PERMISSION": "vnf_instances:opps:id:"
506 },
507 },
508 }
509 },
510 "nst": {
511 "v1": {
512 "netslice_templates_content": {
513 "METHODS": ("GET", "POST"),
514 "ROLE_PERMISSION": "slice_templates:",
515 "<ID>": {
516 "METHODS": ("GET", "PUT", "DELETE"),
517 "ROLE_PERMISSION": "slice_templates:id:",
518 },
519 },
520 "netslice_templates": {
521 "METHODS": ("GET", "POST"),
522 "ROLE_PERMISSION": "slice_templates:",
523 "<ID>": {
524 "METHODS": ("GET", "DELETE"),
525 "TODO": ("PATCH",),
526 "ROLE_PERMISSION": "slice_templates:id:",
527 "nst_content": {
528 "METHODS": ("GET", "PUT"),
529 "ROLE_PERMISSION": "slice_templates:id:content:",
530 },
531 "nst": {
532 "METHODS": ("GET",), # descriptor inside package
533 "ROLE_PERMISSION": "slice_templates:id:content:",
534 },
535 "artifacts": {
536 "METHODS": ("GET",),
537 "ROLE_PERMISSION": "slice_templates:id:content:",
538 "*": None,
539 },
540 },
541 },
542 "subscriptions": {
543 "TODO": ("GET", "POST"),
544 "<ID>": {"TODO": ("GET", "DELETE")},
545 },
546 }
547 },
548 "nsilcm": {
549 "v1": {
550 "netslice_instances_content": {
551 "METHODS": ("GET", "POST"),
552 "ROLE_PERMISSION": "slice_instances:",
553 "<ID>": {
554 "METHODS": ("GET", "DELETE"),
555 "ROLE_PERMISSION": "slice_instances:id:",
556 },
557 },
558 "netslice_instances": {
559 "METHODS": ("GET", "POST"),
560 "ROLE_PERMISSION": "slice_instances:",
561 "<ID>": {
562 "METHODS": ("GET", "DELETE"),
563 "ROLE_PERMISSION": "slice_instances:id:",
564 "terminate": {
565 "METHODS": ("POST",),
566 "ROLE_PERMISSION": "slice_instances:id:terminate:",
567 },
568 "instantiate": {
569 "METHODS": ("POST",),
570 "ROLE_PERMISSION": "slice_instances:id:instantiate:",
571 },
572 "action": {
573 "METHODS": ("POST",),
574 "ROLE_PERMISSION": "slice_instances:id:action:",
575 },
576 },
577 },
578 "nsi_lcm_op_occs": {
579 "METHODS": ("GET",),
580 "ROLE_PERMISSION": "slice_instances:opps:",
581 "<ID>": {
582 "METHODS": ("GET",),
583 "ROLE_PERMISSION": "slice_instances:opps:id:",
584 },
585 },
586 }
587 },
588 "nspm": {
589 "v1": {
590 "pm_jobs": {
591 "<ID>": {
592 "reports": {
593 "<ID>": {
594 "METHODS": ("GET",),
595 "ROLE_PERMISSION": "reports:id:",
596 }
597 }
598 },
599 },
600 },
601 },
602 "nsfm": {
603 "v1": {
604 "alarms": {"METHODS": ("GET", "PATCH"),
605 "ROLE_PERMISSION": "alarms:",
606 "<ID>": {"METHODS": ("GET", "PATCH"),
607 "ROLE_PERMISSION": "alarms:id:",
608 },
609 }
610 },
611 },
612 }
613
614
615 class NbiException(Exception):
616 def __init__(self, message, http_code=HTTPStatus.METHOD_NOT_ALLOWED):
617 Exception.__init__(self, message)
618 self.http_code = http_code
619
620
621 class Server(object):
622 instance = 0
623 # to decode bytes to str
624 reader = getreader("utf-8")
625
626 def __init__(self):
627 self.instance += 1
628 self.authenticator = Authenticator(valid_url_methods, valid_query_string)
629 self.engine = Engine(self.authenticator)
630
631 def _format_in(self, kwargs):
632 try:
633 indata = None
634 if cherrypy.request.body.length:
635 error_text = "Invalid input format "
636
637 if "Content-Type" in cherrypy.request.headers:
638 if "application/json" in cherrypy.request.headers["Content-Type"]:
639 error_text = "Invalid json format "
640 indata = json.load(self.reader(cherrypy.request.body))
641 cherrypy.request.headers.pop("Content-File-MD5", None)
642 elif "application/yaml" in cherrypy.request.headers["Content-Type"]:
643 error_text = "Invalid yaml format "
644 indata = yaml.load(
645 cherrypy.request.body, Loader=yaml.SafeLoader
646 )
647 cherrypy.request.headers.pop("Content-File-MD5", None)
648 elif (
649 "application/binary" in cherrypy.request.headers["Content-Type"]
650 or "application/gzip"
651 in cherrypy.request.headers["Content-Type"]
652 or "application/zip" in cherrypy.request.headers["Content-Type"]
653 or "text/plain" in cherrypy.request.headers["Content-Type"]
654 ):
655 indata = cherrypy.request.body # .read()
656 elif (
657 "multipart/form-data"
658 in cherrypy.request.headers["Content-Type"]
659 ):
660 if "descriptor_file" in kwargs:
661 filecontent = kwargs.pop("descriptor_file")
662 if not filecontent.file:
663 raise NbiException(
664 "empty file or content", HTTPStatus.BAD_REQUEST
665 )
666 indata = filecontent.file # .read()
667 if filecontent.content_type.value:
668 cherrypy.request.headers[
669 "Content-Type"
670 ] = filecontent.content_type.value
671 else:
672 # raise cherrypy.HTTPError(HTTPStatus.Not_Acceptable,
673 # "Only 'Content-Type' of type 'application/json' or
674 # 'application/yaml' for input format are available")
675 error_text = "Invalid yaml format "
676 indata = yaml.load(
677 cherrypy.request.body, Loader=yaml.SafeLoader
678 )
679 cherrypy.request.headers.pop("Content-File-MD5", None)
680 else:
681 error_text = "Invalid yaml format "
682 indata = yaml.load(cherrypy.request.body, Loader=yaml.SafeLoader)
683 cherrypy.request.headers.pop("Content-File-MD5", None)
684 if not indata:
685 indata = {}
686
687 format_yaml = False
688 if cherrypy.request.headers.get("Query-String-Format") == "yaml":
689 format_yaml = True
690
691 for k, v in kwargs.items():
692 if isinstance(v, str):
693 if v == "":
694 kwargs[k] = None
695 elif format_yaml:
696 try:
697 kwargs[k] = yaml.load(v, Loader=yaml.SafeLoader)
698 except Exception:
699 pass
700 elif (
701 k.endswith(".gt")
702 or k.endswith(".lt")
703 or k.endswith(".gte")
704 or k.endswith(".lte")
705 ):
706 try:
707 kwargs[k] = int(v)
708 except Exception:
709 try:
710 kwargs[k] = float(v)
711 except Exception:
712 pass
713 elif v.find(",") > 0:
714 kwargs[k] = v.split(",")
715 elif isinstance(v, (list, tuple)):
716 for index in range(0, len(v)):
717 if v[index] == "":
718 v[index] = None
719 elif format_yaml:
720 try:
721 v[index] = yaml.load(v[index], Loader=yaml.SafeLoader)
722 except Exception:
723 pass
724
725 return indata
726 except (ValueError, yaml.YAMLError) as exc:
727 raise NbiException(error_text + str(exc), HTTPStatus.BAD_REQUEST)
728 except KeyError as exc:
729 raise NbiException(
730 "Query string error: " + str(exc), HTTPStatus.BAD_REQUEST
731 )
732 except Exception as exc:
733 raise NbiException(error_text + str(exc), HTTPStatus.BAD_REQUEST)
734
735 @staticmethod
736 def _format_out(data, token_info=None, _format=None):
737 """
738 return string of dictionary data according to requested json, yaml, xml. By default json
739 :param data: response to be sent. Can be a dict, text or file
740 :param token_info: Contains among other username and project
741 :param _format: The format to be set as Content-Type if data is a file
742 :return: None
743 """
744 accept = cherrypy.request.headers.get("Accept")
745 if data is None:
746 if accept and "text/html" in accept:
747 return html.format(
748 data, cherrypy.request, cherrypy.response, token_info
749 )
750 # cherrypy.response.status = HTTPStatus.NO_CONTENT.value
751 return
752 elif hasattr(data, "read"): # file object
753 if _format:
754 cherrypy.response.headers["Content-Type"] = _format
755 elif "b" in data.mode: # binariy asssumig zip
756 cherrypy.response.headers["Content-Type"] = "application/zip"
757 else:
758 cherrypy.response.headers["Content-Type"] = "text/plain"
759 # TODO check that cherrypy close file. If not implement pending things to close per thread next
760 return data
761 if accept:
762 if "text/html" in accept:
763 return html.format(
764 data, cherrypy.request, cherrypy.response, token_info
765 )
766 elif "application/yaml" in accept or "*/*" in accept:
767 pass
768 elif "application/json" in accept or (
769 cherrypy.response.status and cherrypy.response.status >= 300
770 ):
771 cherrypy.response.headers[
772 "Content-Type"
773 ] = "application/json; charset=utf-8"
774 a = json.dumps(data, indent=4) + "\n"
775 return a.encode("utf8")
776 cherrypy.response.headers["Content-Type"] = "application/yaml"
777 return yaml.safe_dump(
778 data,
779 explicit_start=True,
780 indent=4,
781 default_flow_style=False,
782 tags=False,
783 encoding="utf-8",
784 allow_unicode=True,
785 ) # , canonical=True, default_style='"'
786
787 @cherrypy.expose
788 def index(self, *args, **kwargs):
789 token_info = None
790 try:
791 if cherrypy.request.method == "GET":
792 token_info = self.authenticator.authorize()
793 outdata = token_info # Home page
794 else:
795 raise cherrypy.HTTPError(
796 HTTPStatus.METHOD_NOT_ALLOWED.value,
797 "Method {} not allowed for tokens".format(cherrypy.request.method),
798 )
799
800 return self._format_out(outdata, token_info)
801
802 except (EngineException, AuthException) as e:
803 # cherrypy.log("index Exception {}".format(e))
804 cherrypy.response.status = e.http_code.value
805 return self._format_out("Welcome to OSM!", token_info)
806
807 @cherrypy.expose
808 def version(self, *args, **kwargs):
809 # TODO consider to remove and provide version using the static version file
810 try:
811 if cherrypy.request.method != "GET":
812 raise NbiException(
813 "Only method GET is allowed", HTTPStatus.METHOD_NOT_ALLOWED
814 )
815 elif args or kwargs:
816 raise NbiException(
817 "Invalid URL or query string for version",
818 HTTPStatus.METHOD_NOT_ALLOWED,
819 )
820 # TODO include version of other modules, pick up from some kafka admin message
821 osm_nbi_version = {"version": nbi_version, "date": nbi_version_date}
822 return self._format_out(osm_nbi_version)
823 except NbiException as e:
824 cherrypy.response.status = e.http_code.value
825 problem_details = {
826 "code": e.http_code.name,
827 "status": e.http_code.value,
828 "detail": str(e),
829 }
830 return self._format_out(problem_details, None)
831
832 def domain(self):
833 try:
834 domains = {
835 "user_domain_name": cherrypy.tree.apps["/osm"]
836 .config["authentication"]
837 .get("user_domain_name"),
838 "project_domain_name": cherrypy.tree.apps["/osm"]
839 .config["authentication"]
840 .get("project_domain_name"),
841 }
842 return self._format_out(domains)
843 except NbiException as e:
844 cherrypy.response.status = e.http_code.value
845 problem_details = {
846 "code": e.http_code.name,
847 "status": e.http_code.value,
848 "detail": str(e),
849 }
850 return self._format_out(problem_details, None)
851
852 @staticmethod
853 def _format_login(token_info):
854 """
855 Changes cherrypy.request.login to include username/project_name;session so that cherrypy access log will
856 log this information
857 :param token_info: Dictionary with token content
858 :return: None
859 """
860 cherrypy.request.login = token_info.get("username", "-")
861 if token_info.get("project_name"):
862 cherrypy.request.login += "/" + token_info["project_name"]
863 if token_info.get("id"):
864 cherrypy.request.login += ";session=" + token_info["id"][0:12]
865
866 # NS Fault Management
867 @cherrypy.expose
868 def nsfm(self, version=None, topic=None, uuid=None, project_name=None, ns_id=None, *args, **kwargs):
869 if topic == 'alarms':
870 try:
871 method = cherrypy.request.method
872 role_permission = self._check_valid_url_method(method, "nsfm", version, topic, None, None, *args)
873 query_string_operations = self._extract_query_string_operations(kwargs, method)
874
875 self.authenticator.authorize(role_permission, query_string_operations, None)
876
877 # to handle get request
878 if cherrypy.request.method == 'GET':
879 # if request is on basis of uuid
880 if uuid and uuid != 'None':
881 try:
882 alarm = self.engine.db.get_one("alarms", {"uuid": uuid})
883 alarm_action = self.engine.db.get_one("alarms_action", {"uuid": uuid})
884 alarm.update(alarm_action)
885 vnf = self.engine.db.get_one("vnfrs", {"nsr-id-ref": alarm["tags"]["ns_id"]})
886 alarm["vnf-id"] = vnf["_id"]
887 return self._format_out(str(alarm))
888 except Exception:
889 return self._format_out("Please provide valid alarm uuid")
890 elif ns_id and ns_id != 'None':
891 # if request is on basis of ns_id
892 try:
893 alarms = self.engine.db.get_list("alarms", {"tags.ns_id": ns_id})
894 for alarm in alarms:
895 alarm_action = self.engine.db.get_one("alarms_action", {"uuid": alarm['uuid']})
896 alarm.update(alarm_action)
897 return self._format_out(str(alarms))
898 except Exception:
899 return self._format_out("Please provide valid ns id")
900 else:
901 # to return only alarm which are related to given project
902 project = self.engine.db.get_one("projects", {"name": project_name})
903 project_id = project.get('_id')
904 ns_list = self.engine.db.get_list("nsrs", {"_admin.projects_read": project_id})
905 ns_ids = []
906 for ns in ns_list:
907 ns_ids.append(ns.get("_id"))
908 alarms = self.engine.db.get_list("alarms")
909 alarm_list = [alarm for alarm in alarms if alarm["tags"]["ns_id"] in ns_ids]
910 for alrm in alarm_list:
911 action = self.engine.db.get_one("alarms_action", {"uuid": alrm.get("uuid")})
912 alrm.update(action)
913 return self._format_out(str(alarm_list))
914 # to handle patch request for alarm update
915 elif cherrypy.request.method == 'PATCH':
916 data = yaml.load(cherrypy.request.body, Loader=yaml.SafeLoader)
917 try:
918 # check if uuid is valid
919 self.engine.db.get_one("alarms", {"uuid": data.get("uuid")})
920 except Exception:
921 return self._format_out("Please provide valid alarm uuid.")
922 if data.get("is_enable") is not None:
923 if data.get("is_enable"):
924 alarm_status = 'ok'
925 else:
926 alarm_status = 'disabled'
927 self.engine.db.set_one("alarms", {"uuid": data.get("uuid")},
928 {"alarm_status": alarm_status})
929 else:
930 self.engine.db.set_one("alarms", {"uuid": data.get("uuid")},
931 {"threshold": data.get("threshold")})
932 return self._format_out("Alarm updated")
933 except Exception as e:
934 cherrypy.response.status = e.http_code.value
935 if isinstance(e, (NbiException, EngineException, DbException, FsException, MsgException, AuthException,
936 ValidationError, AuthconnException)):
937 http_code_value = cherrypy.response.status = e.http_code.value
938 http_code_name = e.http_code.name
939 cherrypy.log("Exception {}".format(e))
940 else:
941 http_code_value = cherrypy.response.status = HTTPStatus.BAD_REQUEST.value # INTERNAL_SERVER_ERROR
942 cherrypy.log("CRITICAL: Exception {}".format(e), traceback=True)
943 http_code_name = HTTPStatus.BAD_REQUEST.name
944 problem_details = {
945 "code": http_code_name,
946 "status": http_code_value,
947 "detail": str(e),
948 }
949 return self._format_out(problem_details)
950
951 @cherrypy.expose
952 def token(self, method, token_id=None, kwargs=None):
953 token_info = None
954 # self.engine.load_dbase(cherrypy.request.app.config)
955 indata = self._format_in(kwargs)
956 if not isinstance(indata, dict):
957 raise NbiException(
958 "Expected application/yaml or application/json Content-Type",
959 HTTPStatus.BAD_REQUEST,
960 )
961
962 if method == "GET":
963 token_info = self.authenticator.authorize()
964 # for logging
965 self._format_login(token_info)
966 if token_id:
967 outdata = self.authenticator.get_token(token_info, token_id)
968 else:
969 outdata = self.authenticator.get_token_list(token_info)
970 elif method == "POST":
971 try:
972 token_info = self.authenticator.authorize()
973 except Exception:
974 token_info = None
975 if kwargs:
976 indata.update(kwargs)
977 # This is needed to log the user when authentication fails
978 cherrypy.request.login = "{}".format(indata.get("username", "-"))
979 outdata = token_info = self.authenticator.new_token(
980 token_info, indata, cherrypy.request.remote
981 )
982 cherrypy.session["Authorization"] = outdata["_id"]
983 self._set_location_header("admin", "v1", "tokens", outdata["_id"])
984 # for logging
985 self._format_login(token_info)
986 # password expiry check
987 if self.authenticator.check_password_expiry(outdata):
988 outdata = {"id": outdata["id"],
989 "message": "change_password",
990 "user_id": outdata["user_id"]
991 }
992 # cherrypy.response.cookie["Authorization"] = outdata["id"]
993 # cherrypy.response.cookie["Authorization"]['expires'] = 3600
994 elif method == "DELETE":
995 if not token_id and "id" in kwargs:
996 token_id = kwargs["id"]
997 elif not token_id:
998 token_info = self.authenticator.authorize()
999 # for logging
1000 self._format_login(token_info)
1001 token_id = token_info["_id"]
1002 outdata = self.authenticator.del_token(token_id)
1003 token_info = None
1004 cherrypy.session["Authorization"] = "logout"
1005 # cherrypy.response.cookie["Authorization"] = token_id
1006 # cherrypy.response.cookie["Authorization"]['expires'] = 0
1007 else:
1008 raise NbiException(
1009 "Method {} not allowed for token".format(method),
1010 HTTPStatus.METHOD_NOT_ALLOWED,
1011 )
1012 return self._format_out(outdata, token_info)
1013
1014 @cherrypy.expose
1015 def test(self, *args, **kwargs):
1016 if not cherrypy.config.get("server.enable_test") or (
1017 isinstance(cherrypy.config["server.enable_test"], str)
1018 and cherrypy.config["server.enable_test"].lower() == "false"
1019 ):
1020 cherrypy.response.status = HTTPStatus.METHOD_NOT_ALLOWED.value
1021 return "test URL is disabled"
1022 thread_info = None
1023 if args and args[0] == "help":
1024 return (
1025 "<html><pre>\ninit\nfile/<name> download file\ndb-clear/table\nfs-clear[/folder]\nlogin\nlogin2\n"
1026 "sleep/<time>\nmessage/topic\n</pre></html>"
1027 )
1028
1029 elif args and args[0] == "init":
1030 try:
1031 # self.engine.load_dbase(cherrypy.request.app.config)
1032 self.engine.create_admin()
1033 return "Done. User 'admin', password 'admin' created"
1034 except Exception:
1035 cherrypy.response.status = HTTPStatus.FORBIDDEN.value
1036 return self._format_out("Database already initialized")
1037 elif args and args[0] == "file":
1038 return cherrypy.lib.static.serve_file(
1039 cherrypy.tree.apps["/osm"].config["storage"]["path"] + "/" + args[1],
1040 "text/plain",
1041 "attachment",
1042 )
1043 elif args and args[0] == "file2":
1044 f_path = (
1045 cherrypy.tree.apps["/osm"].config["storage"]["path"] + "/" + args[1]
1046 )
1047 f = open(f_path, "r")
1048 cherrypy.response.headers["Content-type"] = "text/plain"
1049 return f
1050
1051 elif len(args) == 2 and args[0] == "db-clear":
1052 deleted_info = self.engine.db.del_list(args[1], kwargs)
1053 return "{} {} deleted\n".format(deleted_info["deleted"], args[1])
1054 elif len(args) and args[0] == "fs-clear":
1055 if len(args) >= 2:
1056 folders = (args[1],)
1057 else:
1058 folders = self.engine.fs.dir_ls(".")
1059 for folder in folders:
1060 self.engine.fs.file_delete(folder)
1061 return ",".join(folders) + " folders deleted\n"
1062 elif args and args[0] == "login":
1063 if not cherrypy.request.headers.get("Authorization"):
1064 cherrypy.response.headers[
1065 "WWW-Authenticate"
1066 ] = 'Basic realm="Access to OSM site", charset="UTF-8"'
1067 cherrypy.response.status = HTTPStatus.UNAUTHORIZED.value
1068 elif args and args[0] == "login2":
1069 if not cherrypy.request.headers.get("Authorization"):
1070 cherrypy.response.headers[
1071 "WWW-Authenticate"
1072 ] = 'Bearer realm="Access to OSM site"'
1073 cherrypy.response.status = HTTPStatus.UNAUTHORIZED.value
1074 elif args and args[0] == "sleep":
1075 sleep_time = 5
1076 try:
1077 sleep_time = int(args[1])
1078 except Exception:
1079 cherrypy.response.status = HTTPStatus.FORBIDDEN.value
1080 return self._format_out("Database already initialized")
1081 thread_info = cherrypy.thread_data
1082 print(thread_info)
1083 time.sleep(sleep_time)
1084 # thread_info
1085 elif len(args) >= 2 and args[0] == "message":
1086 main_topic = args[1]
1087 return_text = "<html><pre>{} ->\n".format(main_topic)
1088 try:
1089 if cherrypy.request.method == "POST":
1090 to_send = yaml.load(cherrypy.request.body, Loader=yaml.SafeLoader)
1091 for k, v in to_send.items():
1092 self.engine.msg.write(main_topic, k, v)
1093 return_text += " {}: {}\n".format(k, v)
1094 elif cherrypy.request.method == "GET":
1095 for k, v in kwargs.items():
1096 v_dict = yaml.load(v, Loader=yaml.SafeLoader)
1097 self.engine.msg.write(main_topic, k, v_dict)
1098 return_text += " {}: {}\n".format(k, v_dict)
1099 except Exception as e:
1100 return_text += "Error: " + str(e)
1101 return_text += "</pre></html>\n"
1102 return return_text
1103
1104 return_text = (
1105 "<html><pre>\nheaders:\n args: {}\n".format(args)
1106 + " kwargs: {}\n".format(kwargs)
1107 + " headers: {}\n".format(cherrypy.request.headers)
1108 + " path_info: {}\n".format(cherrypy.request.path_info)
1109 + " query_string: {}\n".format(cherrypy.request.query_string)
1110 + " session: {}\n".format(cherrypy.session)
1111 + " cookie: {}\n".format(cherrypy.request.cookie)
1112 + " method: {}\n".format(cherrypy.request.method)
1113 + " session: {}\n".format(cherrypy.session.get("fieldname"))
1114 + " body:\n"
1115 )
1116 return_text += " length: {}\n".format(cherrypy.request.body.length)
1117 if cherrypy.request.body.length:
1118 return_text += " content: {}\n".format(
1119 str(
1120 cherrypy.request.body.read(
1121 int(cherrypy.request.headers.get("Content-Length", 0))
1122 )
1123 )
1124 )
1125 if thread_info:
1126 return_text += "thread: {}\n".format(thread_info)
1127 return_text += "</pre></html>"
1128 return return_text
1129
1130 @staticmethod
1131 def _check_valid_url_method(method, *args):
1132 if len(args) < 3:
1133 raise NbiException(
1134 "URL must contain at least 'main_topic/version/topic'",
1135 HTTPStatus.METHOD_NOT_ALLOWED,
1136 )
1137
1138 reference = valid_url_methods
1139 for arg in args:
1140 if arg is None:
1141 break
1142 if not isinstance(reference, dict):
1143 raise NbiException(
1144 "URL contains unexpected extra items '{}'".format(arg),
1145 HTTPStatus.METHOD_NOT_ALLOWED,
1146 )
1147
1148 if arg in reference:
1149 reference = reference[arg]
1150 elif "<ID>" in reference:
1151 reference = reference["<ID>"]
1152 elif "*" in reference:
1153 # if there is content
1154 if reference["*"]:
1155 reference = reference["*"]
1156 break
1157 else:
1158 raise NbiException(
1159 "Unexpected URL item {}".format(arg), HTTPStatus.METHOD_NOT_ALLOWED
1160 )
1161 if "TODO" in reference and method in reference["TODO"]:
1162 raise NbiException(
1163 "Method {} not supported yet for this URL".format(method),
1164 HTTPStatus.NOT_IMPLEMENTED,
1165 )
1166 elif "METHODS" in reference and method not in reference["METHODS"]:
1167 raise NbiException(
1168 "Method {} not supported for this URL".format(method),
1169 HTTPStatus.METHOD_NOT_ALLOWED,
1170 )
1171 return reference["ROLE_PERMISSION"] + method.lower()
1172
1173 @staticmethod
1174 def _set_location_header(main_topic, version, topic, id):
1175 """
1176 Insert response header Location with the URL of created item base on URL params
1177 :param main_topic:
1178 :param version:
1179 :param topic:
1180 :param id:
1181 :return: None
1182 """
1183 # Use cherrypy.request.base for absoluted path and make use of request.header HOST just in case behind aNAT
1184 cherrypy.response.headers["Location"] = "/osm/{}/{}/{}/{}".format(
1185 main_topic, version, topic, id
1186 )
1187 return
1188
1189 @staticmethod
1190 def _extract_query_string_operations(kwargs, method):
1191 """
1192
1193 :param kwargs:
1194 :return:
1195 """
1196 query_string_operations = []
1197 if kwargs:
1198 for qs in ("FORCE", "PUBLIC", "ADMIN", "SET_PROJECT"):
1199 if qs in kwargs and kwargs[qs].lower() != "false":
1200 query_string_operations.append(qs.lower() + ":" + method.lower())
1201 return query_string_operations
1202
1203 @staticmethod
1204 def _manage_admin_query(token_info, kwargs, method, _id):
1205 """
1206 Processes the administrator query inputs (if any) of FORCE, ADMIN, PUBLIC, SET_PROJECT
1207 Check that users has rights to use them and returs the admin_query
1208 :param token_info: token_info rights obtained by token
1209 :param kwargs: query string input.
1210 :param method: http method: GET, POSST, PUT, ...
1211 :param _id:
1212 :return: admin_query dictionary with keys:
1213 public: True, False or None
1214 force: True or False
1215 project_id: tuple with projects used for accessing an element
1216 set_project: tuple with projects that a created element will belong to
1217 method: show, list, delete, write
1218 """
1219 admin_query = {
1220 "force": False,
1221 "project_id": (token_info["project_id"],),
1222 "username": token_info["username"],
1223 "admin": token_info["admin"],
1224 "public": None,
1225 "allow_show_user_project_role": token_info["allow_show_user_project_role"],
1226 }
1227 if kwargs:
1228 # FORCE
1229 if "FORCE" in kwargs:
1230 if (
1231 kwargs["FORCE"].lower() != "false"
1232 ): # if None or True set force to True
1233 admin_query["force"] = True
1234 del kwargs["FORCE"]
1235 # PUBLIC
1236 if "PUBLIC" in kwargs:
1237 if (
1238 kwargs["PUBLIC"].lower() != "false"
1239 ): # if None or True set public to True
1240 admin_query["public"] = True
1241 else:
1242 admin_query["public"] = False
1243 del kwargs["PUBLIC"]
1244 # ADMIN
1245 if "ADMIN" in kwargs:
1246 behave_as = kwargs.pop("ADMIN")
1247 if behave_as.lower() != "false":
1248 if not token_info["admin"]:
1249 raise NbiException(
1250 "Only admin projects can use 'ADMIN' query string",
1251 HTTPStatus.UNAUTHORIZED,
1252 )
1253 if (
1254 not behave_as or behave_as.lower() == "true"
1255 ): # convert True, None to empty list
1256 admin_query["project_id"] = ()
1257 elif isinstance(behave_as, (list, tuple)):
1258 admin_query["project_id"] = behave_as
1259 else: # isinstance(behave_as, str)
1260 admin_query["project_id"] = (behave_as,)
1261 if "SET_PROJECT" in kwargs:
1262 set_project = kwargs.pop("SET_PROJECT")
1263 if not set_project:
1264 admin_query["set_project"] = list(admin_query["project_id"])
1265 else:
1266 if isinstance(set_project, str):
1267 set_project = (set_project,)
1268 if admin_query["project_id"]:
1269 for p in set_project:
1270 if p not in admin_query["project_id"]:
1271 raise NbiException(
1272 "Unauthorized for 'SET_PROJECT={p}'. Try with 'ADMIN=True' or "
1273 "'ADMIN='{p}'".format(p=p),
1274 HTTPStatus.UNAUTHORIZED,
1275 )
1276 admin_query["set_project"] = set_project
1277
1278 # PROJECT_READ
1279 # if "PROJECT_READ" in kwargs:
1280 # admin_query["project"] = kwargs.pop("project")
1281 # if admin_query["project"] == token_info["project_id"]:
1282 if method == "GET":
1283 if _id:
1284 admin_query["method"] = "show"
1285 else:
1286 admin_query["method"] = "list"
1287 elif method == "DELETE":
1288 admin_query["method"] = "delete"
1289 else:
1290 admin_query["method"] = "write"
1291 return admin_query
1292
1293 @cherrypy.expose
1294 def default(
1295 self,
1296 main_topic=None,
1297 version=None,
1298 topic=None,
1299 _id=None,
1300 item=None,
1301 *args,
1302 **kwargs
1303 ):
1304 token_info = None
1305 outdata = None
1306 _format = None
1307 method = "DONE"
1308 engine_topic = None
1309 rollback = []
1310 engine_session = None
1311 try:
1312 if not main_topic or not version or not topic:
1313 raise NbiException(
1314 "URL must contain at least 'main_topic/version/topic'",
1315 HTTPStatus.METHOD_NOT_ALLOWED,
1316 )
1317 if main_topic not in (
1318 "admin",
1319 "vnfpkgm",
1320 "nsd",
1321 "nslcm",
1322 "pdu",
1323 "nst",
1324 "nsilcm",
1325 "nspm",
1326 "vnflcm",
1327 ):
1328 raise NbiException(
1329 "URL main_topic '{}' not supported".format(main_topic),
1330 HTTPStatus.METHOD_NOT_ALLOWED,
1331 )
1332 if version != "v1":
1333 raise NbiException(
1334 "URL version '{}' not supported".format(version),
1335 HTTPStatus.METHOD_NOT_ALLOWED,
1336 )
1337
1338 if (
1339 kwargs
1340 and "METHOD" in kwargs
1341 and kwargs["METHOD"] in ("PUT", "POST", "DELETE", "GET", "PATCH")
1342 ):
1343 method = kwargs.pop("METHOD")
1344 else:
1345 method = cherrypy.request.method
1346
1347 role_permission = self._check_valid_url_method(
1348 method, main_topic, version, topic, _id, item, *args
1349 )
1350 query_string_operations = self._extract_query_string_operations(
1351 kwargs, method
1352 )
1353 if main_topic == "admin" and topic == "tokens":
1354 return self.token(method, _id, kwargs)
1355 token_info = self.authenticator.authorize(
1356 role_permission, query_string_operations, _id
1357 )
1358 if main_topic == "admin" and topic == "domains":
1359 return self.domain()
1360 engine_session = self._manage_admin_query(token_info, kwargs, method, _id)
1361 indata = self._format_in(kwargs)
1362 engine_topic = topic
1363
1364 if item and topic != "pm_jobs":
1365 engine_topic = item
1366
1367 if main_topic == "nsd":
1368 engine_topic = "nsds"
1369 elif main_topic == "vnfpkgm":
1370 engine_topic = "vnfds"
1371 if topic == "vnfpkg_op_occs":
1372 engine_topic = "vnfpkgops"
1373 if topic == "vnf_packages" and item == "action":
1374 engine_topic = "vnfpkgops"
1375 elif main_topic == "nslcm":
1376 engine_topic = "nsrs"
1377 if topic == "ns_lcm_op_occs":
1378 engine_topic = "nslcmops"
1379 if topic == "vnfrs" or topic == "vnf_instances":
1380 engine_topic = "vnfrs"
1381 elif main_topic == "vnflcm":
1382 if topic == "vnf_lcm_op_occs":
1383 engine_topic = "vnflcmops"
1384 elif main_topic == "nst":
1385 engine_topic = "nsts"
1386 elif main_topic == "nsilcm":
1387 engine_topic = "nsis"
1388 if topic == "nsi_lcm_op_occs":
1389 engine_topic = "nsilcmops"
1390 elif main_topic == "pdu":
1391 engine_topic = "pdus"
1392 if (
1393 engine_topic == "vims"
1394 ): # TODO this is for backward compatibility, it will be removed in the future
1395 engine_topic = "vim_accounts"
1396
1397 if topic == "subscriptions":
1398 engine_topic = main_topic + "_" + topic
1399
1400 if method == "GET":
1401 if item in (
1402 "nsd_content",
1403 "package_content",
1404 "artifacts",
1405 "vnfd",
1406 "nsd",
1407 "nst",
1408 "nst_content",
1409 ):
1410 if item in ("vnfd", "nsd", "nst"):
1411 path = "$DESCRIPTOR"
1412 elif args:
1413 path = args
1414 elif item == "artifacts":
1415 path = ()
1416 else:
1417 path = None
1418 file, _format = self.engine.get_file(
1419 engine_session,
1420 engine_topic,
1421 _id,
1422 path,
1423 cherrypy.request.headers.get("Accept"),
1424 )
1425 outdata = file
1426 elif not _id:
1427 outdata = self.engine.get_item_list(
1428 engine_session, engine_topic, kwargs, api_req=True
1429 )
1430 else:
1431 if item == "reports":
1432 # TODO check that project_id (_id in this context) has permissions
1433 _id = args[0]
1434 filter_q = None
1435 if "vcaStatusRefresh" in kwargs:
1436 filter_q = {"vcaStatusRefresh": kwargs["vcaStatusRefresh"]}
1437 outdata = self.engine.get_item(engine_session, engine_topic, _id, filter_q, True)
1438
1439 elif method == "POST":
1440 cherrypy.response.status = HTTPStatus.CREATED.value
1441 if topic in (
1442 "ns_descriptors_content",
1443 "vnf_packages_content",
1444 "netslice_templates_content",
1445 ):
1446 _id = cherrypy.request.headers.get("Transaction-Id")
1447 if not _id:
1448 _id, _ = self.engine.new_item(
1449 rollback,
1450 engine_session,
1451 engine_topic,
1452 {},
1453 None,
1454 cherrypy.request.headers,
1455 )
1456 completed = self.engine.upload_content(
1457 engine_session,
1458 engine_topic,
1459 _id,
1460 indata,
1461 kwargs,
1462 cherrypy.request.headers,
1463 )
1464 if completed:
1465 self._set_location_header(main_topic, version, topic, _id)
1466 else:
1467 cherrypy.response.headers["Transaction-Id"] = _id
1468 outdata = {"id": _id}
1469 elif topic == "ns_instances_content":
1470 # creates NSR
1471 _id, _ = self.engine.new_item(
1472 rollback, engine_session, engine_topic, indata, kwargs
1473 )
1474 # creates nslcmop
1475 indata["lcmOperationType"] = "instantiate"
1476 indata["nsInstanceId"] = _id
1477 nslcmop_id, _ = self.engine.new_item(
1478 rollback, engine_session, "nslcmops", indata, None
1479 )
1480 self._set_location_header(main_topic, version, topic, _id)
1481 outdata = {"id": _id, "nslcmop_id": nslcmop_id}
1482 elif topic == "ns_instances" and item:
1483 indata["lcmOperationType"] = item
1484 indata["nsInstanceId"] = _id
1485 _id, _ = self.engine.new_item(
1486 rollback, engine_session, "nslcmops", indata, kwargs
1487 )
1488 self._set_location_header(
1489 main_topic, version, "ns_lcm_op_occs", _id
1490 )
1491 outdata = {"id": _id}
1492 cherrypy.response.status = HTTPStatus.ACCEPTED.value
1493 elif topic == "netslice_instances_content":
1494 # creates NetSlice_Instance_record (NSIR)
1495 _id, _ = self.engine.new_item(
1496 rollback, engine_session, engine_topic, indata, kwargs
1497 )
1498 self._set_location_header(main_topic, version, topic, _id)
1499 indata["lcmOperationType"] = "instantiate"
1500 indata["netsliceInstanceId"] = _id
1501 nsilcmop_id, _ = self.engine.new_item(
1502 rollback, engine_session, "nsilcmops", indata, kwargs
1503 )
1504 outdata = {"id": _id, "nsilcmop_id": nsilcmop_id}
1505 elif topic == "netslice_instances" and item:
1506 indata["lcmOperationType"] = item
1507 indata["netsliceInstanceId"] = _id
1508 _id, _ = self.engine.new_item(
1509 rollback, engine_session, "nsilcmops", indata, kwargs
1510 )
1511 self._set_location_header(
1512 main_topic, version, "nsi_lcm_op_occs", _id
1513 )
1514 outdata = {"id": _id}
1515 cherrypy.response.status = HTTPStatus.ACCEPTED.value
1516 elif topic == "vnf_packages" and item == "action":
1517 indata["lcmOperationType"] = item
1518 indata["vnfPkgId"] = _id
1519 _id, _ = self.engine.new_item(
1520 rollback, engine_session, "vnfpkgops", indata, kwargs
1521 )
1522 self._set_location_header(
1523 main_topic, version, "vnfpkg_op_occs", _id
1524 )
1525 outdata = {"id": _id}
1526 cherrypy.response.status = HTTPStatus.ACCEPTED.value
1527 elif topic == "subscriptions":
1528 _id, _ = self.engine.new_item(
1529 rollback, engine_session, engine_topic, indata, kwargs
1530 )
1531 self._set_location_header(main_topic, version, topic, _id)
1532 link = {}
1533 link["self"] = cherrypy.response.headers["Location"]
1534 outdata = {
1535 "id": _id,
1536 "filter": indata["filter"],
1537 "callbackUri": indata["CallbackUri"],
1538 "_links": link,
1539 }
1540 cherrypy.response.status = HTTPStatus.CREATED.value
1541 elif topic == "vnf_instances" and item:
1542 indata["lcmOperationType"] = item
1543 indata["vnfInstanceId"] = _id
1544 _id, _ = self.engine.new_item(rollback, engine_session, "vnflcmops", indata, kwargs)
1545 self._set_location_header(main_topic, version, "vnf_lcm_op_occs", _id)
1546 outdata = {"id": _id}
1547 cherrypy.response.status = HTTPStatus.ACCEPTED.value
1548 else:
1549 _id, op_id = self.engine.new_item(
1550 rollback,
1551 engine_session,
1552 engine_topic,
1553 indata,
1554 kwargs,
1555 cherrypy.request.headers,
1556 )
1557 self._set_location_header(main_topic, version, topic, _id)
1558 outdata = {"id": _id}
1559 if op_id:
1560 outdata["op_id"] = op_id
1561 cherrypy.response.status = HTTPStatus.ACCEPTED.value
1562 # TODO form NsdInfo when topic in ("ns_descriptors", "vnf_packages")
1563
1564 elif method == "DELETE":
1565 if not _id:
1566 outdata = self.engine.del_item_list(
1567 engine_session, engine_topic, kwargs
1568 )
1569 cherrypy.response.status = HTTPStatus.OK.value
1570 else: # len(args) > 1
1571 # for NS NSI generate an operation
1572 op_id = None
1573 if topic == "ns_instances_content" and not engine_session["force"]:
1574 nslcmop_desc = {
1575 "lcmOperationType": "terminate",
1576 "nsInstanceId": _id,
1577 "autoremove": True,
1578 }
1579 op_id, _ = self.engine.new_item(
1580 rollback, engine_session, "nslcmops", nslcmop_desc, kwargs
1581 )
1582 if op_id:
1583 outdata = {"_id": op_id}
1584 elif (
1585 topic == "netslice_instances_content"
1586 and not engine_session["force"]
1587 ):
1588 nsilcmop_desc = {
1589 "lcmOperationType": "terminate",
1590 "netsliceInstanceId": _id,
1591 "autoremove": True,
1592 }
1593 op_id, _ = self.engine.new_item(
1594 rollback, engine_session, "nsilcmops", nsilcmop_desc, None
1595 )
1596 if op_id:
1597 outdata = {"_id": op_id}
1598 # if there is not any deletion in process, delete
1599 if not op_id:
1600 op_id = self.engine.del_item(engine_session, engine_topic, _id)
1601 if op_id:
1602 outdata = {"op_id": op_id}
1603 cherrypy.response.status = (
1604 HTTPStatus.ACCEPTED.value
1605 if op_id
1606 else HTTPStatus.NO_CONTENT.value
1607 )
1608
1609 elif method in ("PUT", "PATCH"):
1610 op_id = None
1611 if not indata and not kwargs and not engine_session.get("set_project"):
1612 raise NbiException(
1613 "Nothing to update. Provide payload and/or query string",
1614 HTTPStatus.BAD_REQUEST,
1615 )
1616 if (
1617 item in ("nsd_content", "package_content", "nst_content")
1618 and method == "PUT"
1619 ):
1620 completed = self.engine.upload_content(
1621 engine_session,
1622 engine_topic,
1623 _id,
1624 indata,
1625 kwargs,
1626 cherrypy.request.headers,
1627 )
1628 if not completed:
1629 cherrypy.response.headers["Transaction-Id"] = id
1630 else:
1631 op_id = self.engine.edit_item(
1632 engine_session, engine_topic, _id, indata, kwargs
1633 )
1634
1635 if op_id:
1636 cherrypy.response.status = HTTPStatus.ACCEPTED.value
1637 outdata = {"op_id": op_id}
1638 else:
1639 cherrypy.response.status = HTTPStatus.NO_CONTENT.value
1640 outdata = None
1641 else:
1642 raise NbiException(
1643 "Method {} not allowed".format(method),
1644 HTTPStatus.METHOD_NOT_ALLOWED,
1645 )
1646
1647 # if Role information changes, it is needed to reload the information of roles
1648 if topic == "roles" and method != "GET":
1649 self.authenticator.load_operation_to_allowed_roles()
1650
1651 if (
1652 topic == "projects"
1653 and method == "DELETE"
1654 or topic in ["users", "roles"]
1655 and method in ["PUT", "PATCH", "DELETE"]
1656 ):
1657 self.authenticator.remove_token_from_cache()
1658
1659 return self._format_out(outdata, token_info, _format)
1660 except Exception as e:
1661 if isinstance(
1662 e,
1663 (
1664 NbiException,
1665 EngineException,
1666 DbException,
1667 FsException,
1668 MsgException,
1669 AuthException,
1670 ValidationError,
1671 AuthconnException,
1672 ),
1673 ):
1674 http_code_value = cherrypy.response.status = e.http_code.value
1675 http_code_name = e.http_code.name
1676 cherrypy.log("Exception {}".format(e))
1677 else:
1678 http_code_value = (
1679 cherrypy.response.status
1680 ) = HTTPStatus.BAD_REQUEST.value # INTERNAL_SERVER_ERROR
1681 cherrypy.log("CRITICAL: Exception {}".format(e), traceback=True)
1682 http_code_name = HTTPStatus.BAD_REQUEST.name
1683 if hasattr(outdata, "close"): # is an open file
1684 outdata.close()
1685 error_text = str(e)
1686 rollback.reverse()
1687 for rollback_item in rollback:
1688 try:
1689 if rollback_item.get("operation") == "set":
1690 self.engine.db.set_one(
1691 rollback_item["topic"],
1692 {"_id": rollback_item["_id"]},
1693 rollback_item["content"],
1694 fail_on_empty=False,
1695 )
1696 elif rollback_item.get("operation") == "del_list":
1697 self.engine.db.del_list(
1698 rollback_item["topic"],
1699 rollback_item["filter"],
1700 fail_on_empty=False,
1701 )
1702 else:
1703 self.engine.db.del_one(
1704 rollback_item["topic"],
1705 {"_id": rollback_item["_id"]},
1706 fail_on_empty=False,
1707 )
1708 except Exception as e2:
1709 rollback_error_text = "Rollback Exception {}: {}".format(
1710 rollback_item, e2
1711 )
1712 cherrypy.log(rollback_error_text)
1713 error_text += ". " + rollback_error_text
1714 # if isinstance(e, MsgException):
1715 # error_text = "{} has been '{}' but other modules cannot be informed because an error on bus".format(
1716 # engine_topic[:-1], method, error_text)
1717 problem_details = {
1718 "code": http_code_name,
1719 "status": http_code_value,
1720 "detail": error_text,
1721 }
1722 return self._format_out(problem_details, token_info)
1723 # raise cherrypy.HTTPError(e.http_code.value, str(e))
1724 finally:
1725 if token_info:
1726 self._format_login(token_info)
1727 if method in ("PUT", "PATCH", "POST") and isinstance(outdata, dict):
1728 for logging_id in ("id", "op_id", "nsilcmop_id", "nslcmop_id"):
1729 if outdata.get(logging_id):
1730 cherrypy.request.login += ";{}={}".format(
1731 logging_id, outdata[logging_id][:36]
1732 )
1733
1734
1735 def _start_service():
1736 """
1737 Callback function called when cherrypy.engine starts
1738 Override configuration with env variables
1739 Set database, storage, message configuration
1740 Init database with admin/admin user password
1741 """
1742 global nbi_server
1743 global subscription_thread
1744 cherrypy.log.error("Starting osm_nbi")
1745 # update general cherrypy configuration
1746 update_dict = {}
1747
1748 engine_config = cherrypy.tree.apps["/osm"].config
1749 for k, v in environ.items():
1750 if not k.startswith("OSMNBI_"):
1751 continue
1752 k1, _, k2 = k[7:].lower().partition("_")
1753 if not k2:
1754 continue
1755 try:
1756 # update static configuration
1757 if k == "OSMNBI_STATIC_DIR":
1758 engine_config["/static"]["tools.staticdir.dir"] = v
1759 engine_config["/static"]["tools.staticdir.on"] = True
1760 elif k == "OSMNBI_SOCKET_PORT" or k == "OSMNBI_SERVER_PORT":
1761 update_dict["server.socket_port"] = int(v)
1762 elif k == "OSMNBI_SOCKET_HOST" or k == "OSMNBI_SERVER_HOST":
1763 update_dict["server.socket_host"] = v
1764 elif k1 in ("server", "test", "auth", "log"):
1765 update_dict[k1 + "." + k2] = v
1766 elif k1 in ("message", "database", "storage", "authentication"):
1767 # k2 = k2.replace('_', '.')
1768 if k2 in ("port", "db_port"):
1769 engine_config[k1][k2] = int(v)
1770 else:
1771 engine_config[k1][k2] = v
1772
1773 except ValueError as e:
1774 cherrypy.log.error("Ignoring environ '{}': " + str(e))
1775 except Exception as e:
1776 cherrypy.log.warn("skipping environ '{}' on exception '{}'".format(k, e))
1777
1778 if update_dict:
1779 cherrypy.config.update(update_dict)
1780 engine_config["global"].update(update_dict)
1781
1782 # logging cherrypy
1783 log_format_simple = (
1784 "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(message)s"
1785 )
1786 log_formatter_simple = logging.Formatter(
1787 log_format_simple, datefmt="%Y-%m-%dT%H:%M:%S"
1788 )
1789 logger_server = logging.getLogger("cherrypy.error")
1790 logger_access = logging.getLogger("cherrypy.access")
1791 logger_cherry = logging.getLogger("cherrypy")
1792 logger_nbi = logging.getLogger("nbi")
1793
1794 if "log.file" in engine_config["global"]:
1795 file_handler = logging.handlers.RotatingFileHandler(
1796 engine_config["global"]["log.file"], maxBytes=100e6, backupCount=9, delay=0
1797 )
1798 file_handler.setFormatter(log_formatter_simple)
1799 logger_cherry.addHandler(file_handler)
1800 logger_nbi.addHandler(file_handler)
1801 # log always to standard output
1802 for format_, logger in {
1803 "nbi.server %(filename)s:%(lineno)s": logger_server,
1804 "nbi.access %(filename)s:%(lineno)s": logger_access,
1805 "%(name)s %(filename)s:%(lineno)s": logger_nbi,
1806 }.items():
1807 log_format_cherry = "%(asctime)s %(levelname)s {} %(message)s".format(format_)
1808 log_formatter_cherry = logging.Formatter(
1809 log_format_cherry, datefmt="%Y-%m-%dT%H:%M:%S"
1810 )
1811 str_handler = logging.StreamHandler()
1812 str_handler.setFormatter(log_formatter_cherry)
1813 logger.addHandler(str_handler)
1814
1815 if engine_config["global"].get("log.level"):
1816 logger_cherry.setLevel(engine_config["global"]["log.level"])
1817 logger_nbi.setLevel(engine_config["global"]["log.level"])
1818
1819 # logging other modules
1820 for k1, logname in {
1821 "message": "nbi.msg",
1822 "database": "nbi.db",
1823 "storage": "nbi.fs",
1824 }.items():
1825 engine_config[k1]["logger_name"] = logname
1826 logger_module = logging.getLogger(logname)
1827 if "logfile" in engine_config[k1]:
1828 file_handler = logging.handlers.RotatingFileHandler(
1829 engine_config[k1]["logfile"], maxBytes=100e6, backupCount=9, delay=0
1830 )
1831 file_handler.setFormatter(log_formatter_simple)
1832 logger_module.addHandler(file_handler)
1833 if "loglevel" in engine_config[k1]:
1834 logger_module.setLevel(engine_config[k1]["loglevel"])
1835 # TODO add more entries, e.g.: storage
1836 cherrypy.tree.apps["/osm"].root.engine.start(engine_config)
1837 cherrypy.tree.apps["/osm"].root.authenticator.start(engine_config)
1838 cherrypy.tree.apps["/osm"].root.engine.init_db(target_version=database_version)
1839 cherrypy.tree.apps["/osm"].root.authenticator.init_db(
1840 target_version=auth_database_version
1841 )
1842
1843 # start subscriptions thread:
1844 subscription_thread = SubscriptionThread(
1845 config=engine_config, engine=nbi_server.engine
1846 )
1847 subscription_thread.start()
1848 # Do not capture except SubscriptionException
1849
1850 backend = engine_config["authentication"]["backend"]
1851 cherrypy.log.error(
1852 "Starting OSM NBI Version '{} {}' with '{}' authentication backend".format(
1853 nbi_version, nbi_version_date, backend
1854 )
1855 )
1856
1857
1858 def _stop_service():
1859 """
1860 Callback function called when cherrypy.engine stops
1861 TODO: Ending database connections.
1862 """
1863 global subscription_thread
1864 if subscription_thread:
1865 subscription_thread.terminate()
1866 subscription_thread = None
1867 cherrypy.tree.apps["/osm"].root.engine.stop()
1868 cherrypy.log.error("Stopping osm_nbi")
1869
1870
1871 def nbi(config_file):
1872 global nbi_server
1873 # conf = {
1874 # '/': {
1875 # #'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
1876 # 'tools.sessions.on': True,
1877 # 'tools.response_headers.on': True,
1878 # # 'tools.response_headers.headers': [('Content-Type', 'text/plain')],
1879 # }
1880 # }
1881 # cherrypy.Server.ssl_module = 'builtin'
1882 # cherrypy.Server.ssl_certificate = "http/cert.pem"
1883 # cherrypy.Server.ssl_private_key = "http/privkey.pem"
1884 # cherrypy.Server.thread_pool = 10
1885 # cherrypy.config.update({'Server.socket_port': config["port"], 'Server.socket_host': config["host"]})
1886
1887 # cherrypy.config.update({'tools.auth_basic.on': True,
1888 # 'tools.auth_basic.realm': 'localhost',
1889 # 'tools.auth_basic.checkpassword': validate_password})
1890 nbi_server = Server()
1891 cherrypy.engine.subscribe("start", _start_service)
1892 cherrypy.engine.subscribe("stop", _stop_service)
1893 cherrypy.quickstart(nbi_server, "/osm", config_file)
1894
1895
1896 def usage():
1897 print(
1898 """Usage: {} [options]
1899 -c|--config [configuration_file]: loads the configuration file (default: ./nbi.cfg)
1900 -h|--help: shows this help
1901 """.format(
1902 sys.argv[0]
1903 )
1904 )
1905 # --log-socket-host HOST: send logs to this host")
1906 # --log-socket-port PORT: send logs using this port (default: 9022)")
1907
1908
1909 if __name__ == "__main__":
1910 try:
1911 # load parameters and configuration
1912 opts, args = getopt.getopt(sys.argv[1:], "hvc:", ["config=", "help"])
1913 # TODO add "log-socket-host=", "log-socket-port=", "log-file="
1914 config_file = None
1915 for o, a in opts:
1916 if o in ("-h", "--help"):
1917 usage()
1918 sys.exit()
1919 elif o in ("-c", "--config"):
1920 config_file = a
1921 # elif o == "--log-socket-port":
1922 # log_socket_port = a
1923 # elif o == "--log-socket-host":
1924 # log_socket_host = a
1925 # elif o == "--log-file":
1926 # log_file = a
1927 else:
1928 assert False, "Unhandled option"
1929 if config_file:
1930 if not path.isfile(config_file):
1931 print(
1932 "configuration file '{}' that not exist".format(config_file),
1933 file=sys.stderr,
1934 )
1935 exit(1)
1936 else:
1937 for config_file in (
1938 __file__[: __file__.rfind(".")] + ".cfg",
1939 "./nbi.cfg",
1940 "/etc/osm/nbi.cfg",
1941 ):
1942 if path.isfile(config_file):
1943 break
1944 else:
1945 print(
1946 "No configuration file 'nbi.cfg' found neither at local folder nor at /etc/osm/",
1947 file=sys.stderr,
1948 )
1949 exit(1)
1950 nbi(config_file)
1951 except getopt.GetoptError as e:
1952 print(str(e), file=sys.stderr)
1953 # usage()
1954 exit(1)