29fcacd2a8a5a58b93869dbe41d1a086c4bb4f33
[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 "nst": {
486 "v1": {
487 "netslice_templates_content": {
488 "METHODS": ("GET", "POST"),
489 "ROLE_PERMISSION": "slice_templates:",
490 "<ID>": {
491 "METHODS": ("GET", "PUT", "DELETE"),
492 "ROLE_PERMISSION": "slice_templates:id:",
493 },
494 },
495 "netslice_templates": {
496 "METHODS": ("GET", "POST"),
497 "ROLE_PERMISSION": "slice_templates:",
498 "<ID>": {
499 "METHODS": ("GET", "DELETE"),
500 "TODO": ("PATCH",),
501 "ROLE_PERMISSION": "slice_templates:id:",
502 "nst_content": {
503 "METHODS": ("GET", "PUT"),
504 "ROLE_PERMISSION": "slice_templates:id:content:",
505 },
506 "nst": {
507 "METHODS": ("GET",), # descriptor inside package
508 "ROLE_PERMISSION": "slice_templates:id:content:",
509 },
510 "artifacts": {
511 "METHODS": ("GET",),
512 "ROLE_PERMISSION": "slice_templates:id:content:",
513 "*": None,
514 },
515 },
516 },
517 "subscriptions": {
518 "TODO": ("GET", "POST"),
519 "<ID>": {"TODO": ("GET", "DELETE")},
520 },
521 }
522 },
523 "nsilcm": {
524 "v1": {
525 "netslice_instances_content": {
526 "METHODS": ("GET", "POST"),
527 "ROLE_PERMISSION": "slice_instances:",
528 "<ID>": {
529 "METHODS": ("GET", "DELETE"),
530 "ROLE_PERMISSION": "slice_instances:id:",
531 },
532 },
533 "netslice_instances": {
534 "METHODS": ("GET", "POST"),
535 "ROLE_PERMISSION": "slice_instances:",
536 "<ID>": {
537 "METHODS": ("GET", "DELETE"),
538 "ROLE_PERMISSION": "slice_instances:id:",
539 "terminate": {
540 "METHODS": ("POST",),
541 "ROLE_PERMISSION": "slice_instances:id:terminate:",
542 },
543 "instantiate": {
544 "METHODS": ("POST",),
545 "ROLE_PERMISSION": "slice_instances:id:instantiate:",
546 },
547 "action": {
548 "METHODS": ("POST",),
549 "ROLE_PERMISSION": "slice_instances:id:action:",
550 },
551 },
552 },
553 "nsi_lcm_op_occs": {
554 "METHODS": ("GET",),
555 "ROLE_PERMISSION": "slice_instances:opps:",
556 "<ID>": {
557 "METHODS": ("GET",),
558 "ROLE_PERMISSION": "slice_instances:opps:id:",
559 },
560 },
561 }
562 },
563 "nspm": {
564 "v1": {
565 "pm_jobs": {
566 "<ID>": {
567 "reports": {
568 "<ID>": {
569 "METHODS": ("GET",),
570 "ROLE_PERMISSION": "reports:id:",
571 }
572 }
573 },
574 },
575 },
576 },
577 "nsfm": {
578 "v1": {
579 "alarms": {"METHODS": ("GET", "PATCH"),
580 "ROLE_PERMISSION": "alarms:",
581 "<ID>": {"METHODS": ("GET", "PATCH"),
582 "ROLE_PERMISSION": "alarms:id:",
583 },
584 }
585 },
586 },
587 }
588
589
590 class NbiException(Exception):
591 def __init__(self, message, http_code=HTTPStatus.METHOD_NOT_ALLOWED):
592 Exception.__init__(self, message)
593 self.http_code = http_code
594
595
596 class Server(object):
597 instance = 0
598 # to decode bytes to str
599 reader = getreader("utf-8")
600
601 def __init__(self):
602 self.instance += 1
603 self.authenticator = Authenticator(valid_url_methods, valid_query_string)
604 self.engine = Engine(self.authenticator)
605
606 def _format_in(self, kwargs):
607 try:
608 indata = None
609 if cherrypy.request.body.length:
610 error_text = "Invalid input format "
611
612 if "Content-Type" in cherrypy.request.headers:
613 if "application/json" in cherrypy.request.headers["Content-Type"]:
614 error_text = "Invalid json format "
615 indata = json.load(self.reader(cherrypy.request.body))
616 cherrypy.request.headers.pop("Content-File-MD5", None)
617 elif "application/yaml" in cherrypy.request.headers["Content-Type"]:
618 error_text = "Invalid yaml format "
619 indata = yaml.load(
620 cherrypy.request.body, Loader=yaml.SafeLoader
621 )
622 cherrypy.request.headers.pop("Content-File-MD5", None)
623 elif (
624 "application/binary" in cherrypy.request.headers["Content-Type"]
625 or "application/gzip"
626 in cherrypy.request.headers["Content-Type"]
627 or "application/zip" in cherrypy.request.headers["Content-Type"]
628 or "text/plain" in cherrypy.request.headers["Content-Type"]
629 ):
630 indata = cherrypy.request.body # .read()
631 elif (
632 "multipart/form-data"
633 in cherrypy.request.headers["Content-Type"]
634 ):
635 if "descriptor_file" in kwargs:
636 filecontent = kwargs.pop("descriptor_file")
637 if not filecontent.file:
638 raise NbiException(
639 "empty file or content", HTTPStatus.BAD_REQUEST
640 )
641 indata = filecontent.file # .read()
642 if filecontent.content_type.value:
643 cherrypy.request.headers[
644 "Content-Type"
645 ] = filecontent.content_type.value
646 else:
647 # raise cherrypy.HTTPError(HTTPStatus.Not_Acceptable,
648 # "Only 'Content-Type' of type 'application/json' or
649 # 'application/yaml' for input format are available")
650 error_text = "Invalid yaml format "
651 indata = yaml.load(
652 cherrypy.request.body, Loader=yaml.SafeLoader
653 )
654 cherrypy.request.headers.pop("Content-File-MD5", None)
655 else:
656 error_text = "Invalid yaml format "
657 indata = yaml.load(cherrypy.request.body, Loader=yaml.SafeLoader)
658 cherrypy.request.headers.pop("Content-File-MD5", None)
659 if not indata:
660 indata = {}
661
662 format_yaml = False
663 if cherrypy.request.headers.get("Query-String-Format") == "yaml":
664 format_yaml = True
665
666 for k, v in kwargs.items():
667 if isinstance(v, str):
668 if v == "":
669 kwargs[k] = None
670 elif format_yaml:
671 try:
672 kwargs[k] = yaml.load(v, Loader=yaml.SafeLoader)
673 except Exception:
674 pass
675 elif (
676 k.endswith(".gt")
677 or k.endswith(".lt")
678 or k.endswith(".gte")
679 or k.endswith(".lte")
680 ):
681 try:
682 kwargs[k] = int(v)
683 except Exception:
684 try:
685 kwargs[k] = float(v)
686 except Exception:
687 pass
688 elif v.find(",") > 0:
689 kwargs[k] = v.split(",")
690 elif isinstance(v, (list, tuple)):
691 for index in range(0, len(v)):
692 if v[index] == "":
693 v[index] = None
694 elif format_yaml:
695 try:
696 v[index] = yaml.load(v[index], Loader=yaml.SafeLoader)
697 except Exception:
698 pass
699
700 return indata
701 except (ValueError, yaml.YAMLError) as exc:
702 raise NbiException(error_text + str(exc), HTTPStatus.BAD_REQUEST)
703 except KeyError as exc:
704 raise NbiException(
705 "Query string error: " + str(exc), HTTPStatus.BAD_REQUEST
706 )
707 except Exception as exc:
708 raise NbiException(error_text + str(exc), HTTPStatus.BAD_REQUEST)
709
710 @staticmethod
711 def _format_out(data, token_info=None, _format=None):
712 """
713 return string of dictionary data according to requested json, yaml, xml. By default json
714 :param data: response to be sent. Can be a dict, text or file
715 :param token_info: Contains among other username and project
716 :param _format: The format to be set as Content-Type if data is a file
717 :return: None
718 """
719 accept = cherrypy.request.headers.get("Accept")
720 if data is None:
721 if accept and "text/html" in accept:
722 return html.format(
723 data, cherrypy.request, cherrypy.response, token_info
724 )
725 # cherrypy.response.status = HTTPStatus.NO_CONTENT.value
726 return
727 elif hasattr(data, "read"): # file object
728 if _format:
729 cherrypy.response.headers["Content-Type"] = _format
730 elif "b" in data.mode: # binariy asssumig zip
731 cherrypy.response.headers["Content-Type"] = "application/zip"
732 else:
733 cherrypy.response.headers["Content-Type"] = "text/plain"
734 # TODO check that cherrypy close file. If not implement pending things to close per thread next
735 return data
736 if accept:
737 if "text/html" in accept:
738 return html.format(
739 data, cherrypy.request, cherrypy.response, token_info
740 )
741 elif "application/yaml" in accept or "*/*" in accept:
742 pass
743 elif "application/json" in accept or (
744 cherrypy.response.status and cherrypy.response.status >= 300
745 ):
746 cherrypy.response.headers[
747 "Content-Type"
748 ] = "application/json; charset=utf-8"
749 a = json.dumps(data, indent=4) + "\n"
750 return a.encode("utf8")
751 cherrypy.response.headers["Content-Type"] = "application/yaml"
752 return yaml.safe_dump(
753 data,
754 explicit_start=True,
755 indent=4,
756 default_flow_style=False,
757 tags=False,
758 encoding="utf-8",
759 allow_unicode=True,
760 ) # , canonical=True, default_style='"'
761
762 @cherrypy.expose
763 def index(self, *args, **kwargs):
764 token_info = None
765 try:
766 if cherrypy.request.method == "GET":
767 token_info = self.authenticator.authorize()
768 outdata = token_info # Home page
769 else:
770 raise cherrypy.HTTPError(
771 HTTPStatus.METHOD_NOT_ALLOWED.value,
772 "Method {} not allowed for tokens".format(cherrypy.request.method),
773 )
774
775 return self._format_out(outdata, token_info)
776
777 except (EngineException, AuthException) as e:
778 # cherrypy.log("index Exception {}".format(e))
779 cherrypy.response.status = e.http_code.value
780 return self._format_out("Welcome to OSM!", token_info)
781
782 @cherrypy.expose
783 def version(self, *args, **kwargs):
784 # TODO consider to remove and provide version using the static version file
785 try:
786 if cherrypy.request.method != "GET":
787 raise NbiException(
788 "Only method GET is allowed", HTTPStatus.METHOD_NOT_ALLOWED
789 )
790 elif args or kwargs:
791 raise NbiException(
792 "Invalid URL or query string for version",
793 HTTPStatus.METHOD_NOT_ALLOWED,
794 )
795 # TODO include version of other modules, pick up from some kafka admin message
796 osm_nbi_version = {"version": nbi_version, "date": nbi_version_date}
797 return self._format_out(osm_nbi_version)
798 except NbiException as e:
799 cherrypy.response.status = e.http_code.value
800 problem_details = {
801 "code": e.http_code.name,
802 "status": e.http_code.value,
803 "detail": str(e),
804 }
805 return self._format_out(problem_details, None)
806
807 def domain(self):
808 try:
809 domains = {
810 "user_domain_name": cherrypy.tree.apps["/osm"]
811 .config["authentication"]
812 .get("user_domain_name"),
813 "project_domain_name": cherrypy.tree.apps["/osm"]
814 .config["authentication"]
815 .get("project_domain_name"),
816 }
817 return self._format_out(domains)
818 except NbiException as e:
819 cherrypy.response.status = e.http_code.value
820 problem_details = {
821 "code": e.http_code.name,
822 "status": e.http_code.value,
823 "detail": str(e),
824 }
825 return self._format_out(problem_details, None)
826
827 @staticmethod
828 def _format_login(token_info):
829 """
830 Changes cherrypy.request.login to include username/project_name;session so that cherrypy access log will
831 log this information
832 :param token_info: Dictionary with token content
833 :return: None
834 """
835 cherrypy.request.login = token_info.get("username", "-")
836 if token_info.get("project_name"):
837 cherrypy.request.login += "/" + token_info["project_name"]
838 if token_info.get("id"):
839 cherrypy.request.login += ";session=" + token_info["id"][0:12]
840
841 # NS Fault Management
842 @cherrypy.expose
843 def nsfm(self, version=None, topic=None, uuid=None, project_name=None, ns_id=None, *args, **kwargs):
844 if topic == 'alarms':
845 try:
846 method = cherrypy.request.method
847 role_permission = self._check_valid_url_method(method, "nsfm", version, topic, None, None, *args)
848 query_string_operations = self._extract_query_string_operations(kwargs, method)
849
850 self.authenticator.authorize(role_permission, query_string_operations, None)
851
852 # to handle get request
853 if cherrypy.request.method == 'GET':
854 # if request is on basis of uuid
855 if uuid and uuid != 'None':
856 try:
857 alarm = self.engine.db.get_one("alarms", {"uuid": uuid})
858 alarm_action = self.engine.db.get_one("alarms_action", {"uuid": uuid})
859 alarm.update(alarm_action)
860 vnf = self.engine.db.get_one("vnfrs", {"nsr-id-ref": alarm["tags"]["ns_id"]})
861 alarm["vnf-id"] = vnf["_id"]
862 return self._format_out(str(alarm))
863 except Exception:
864 return self._format_out("Please provide valid alarm uuid")
865 elif ns_id and ns_id != 'None':
866 # if request is on basis of ns_id
867 try:
868 alarms = self.engine.db.get_list("alarms", {"tags.ns_id": ns_id})
869 for alarm in alarms:
870 alarm_action = self.engine.db.get_one("alarms_action", {"uuid": alarm['uuid']})
871 alarm.update(alarm_action)
872 return self._format_out(str(alarms))
873 except Exception:
874 return self._format_out("Please provide valid ns id")
875 else:
876 # to return only alarm which are related to given project
877 project = self.engine.db.get_one("projects", {"name": project_name})
878 project_id = project.get('_id')
879 ns_list = self.engine.db.get_list("nsrs", {"_admin.projects_read": project_id})
880 ns_ids = []
881 for ns in ns_list:
882 ns_ids.append(ns.get("_id"))
883 alarms = self.engine.db.get_list("alarms")
884 alarm_list = [alarm for alarm in alarms if alarm["tags"]["ns_id"] in ns_ids]
885 for alrm in alarm_list:
886 action = self.engine.db.get_one("alarms_action", {"uuid": alrm.get("uuid")})
887 alrm.update(action)
888 return self._format_out(str(alarm_list))
889 # to handle patch request for alarm update
890 elif cherrypy.request.method == 'PATCH':
891 data = yaml.load(cherrypy.request.body, Loader=yaml.SafeLoader)
892 try:
893 # check if uuid is valid
894 self.engine.db.get_one("alarms", {"uuid": data.get("uuid")})
895 except Exception:
896 return self._format_out("Please provide valid alarm uuid.")
897 if data.get("is_enable") is not None:
898 if data.get("is_enable"):
899 alarm_status = 'ok'
900 else:
901 alarm_status = 'disabled'
902 self.engine.db.set_one("alarms", {"uuid": data.get("uuid")},
903 {"alarm_status": alarm_status})
904 else:
905 self.engine.db.set_one("alarms", {"uuid": data.get("uuid")},
906 {"threshold": data.get("threshold")})
907 return self._format_out("Alarm updated")
908 except Exception as e:
909 cherrypy.response.status = e.http_code.value
910 if isinstance(e, (NbiException, EngineException, DbException, FsException, MsgException, AuthException,
911 ValidationError, AuthconnException)):
912 http_code_value = cherrypy.response.status = e.http_code.value
913 http_code_name = e.http_code.name
914 cherrypy.log("Exception {}".format(e))
915 else:
916 http_code_value = cherrypy.response.status = HTTPStatus.BAD_REQUEST.value # INTERNAL_SERVER_ERROR
917 cherrypy.log("CRITICAL: Exception {}".format(e), traceback=True)
918 http_code_name = HTTPStatus.BAD_REQUEST.name
919 problem_details = {
920 "code": http_code_name,
921 "status": http_code_value,
922 "detail": str(e),
923 }
924 return self._format_out(problem_details)
925
926 @cherrypy.expose
927 def token(self, method, token_id=None, kwargs=None):
928 token_info = None
929 # self.engine.load_dbase(cherrypy.request.app.config)
930 indata = self._format_in(kwargs)
931 if not isinstance(indata, dict):
932 raise NbiException(
933 "Expected application/yaml or application/json Content-Type",
934 HTTPStatus.BAD_REQUEST,
935 )
936
937 if method == "GET":
938 token_info = self.authenticator.authorize()
939 # for logging
940 self._format_login(token_info)
941 if token_id:
942 outdata = self.authenticator.get_token(token_info, token_id)
943 else:
944 outdata = self.authenticator.get_token_list(token_info)
945 elif method == "POST":
946 try:
947 token_info = self.authenticator.authorize()
948 except Exception:
949 token_info = None
950 if kwargs:
951 indata.update(kwargs)
952 # This is needed to log the user when authentication fails
953 cherrypy.request.login = "{}".format(indata.get("username", "-"))
954 outdata = token_info = self.authenticator.new_token(
955 token_info, indata, cherrypy.request.remote
956 )
957 cherrypy.session["Authorization"] = outdata["_id"]
958 self._set_location_header("admin", "v1", "tokens", outdata["_id"])
959 # for logging
960 self._format_login(token_info)
961 # password expiry check
962 if self.authenticator.check_password_expiry(outdata):
963 outdata = {"id": outdata["id"],
964 "message": "change_password",
965 "user_id": outdata["user_id"]
966 }
967 # cherrypy.response.cookie["Authorization"] = outdata["id"]
968 # cherrypy.response.cookie["Authorization"]['expires'] = 3600
969 elif method == "DELETE":
970 if not token_id and "id" in kwargs:
971 token_id = kwargs["id"]
972 elif not token_id:
973 token_info = self.authenticator.authorize()
974 # for logging
975 self._format_login(token_info)
976 token_id = token_info["_id"]
977 outdata = self.authenticator.del_token(token_id)
978 token_info = None
979 cherrypy.session["Authorization"] = "logout"
980 # cherrypy.response.cookie["Authorization"] = token_id
981 # cherrypy.response.cookie["Authorization"]['expires'] = 0
982 else:
983 raise NbiException(
984 "Method {} not allowed for token".format(method),
985 HTTPStatus.METHOD_NOT_ALLOWED,
986 )
987 return self._format_out(outdata, token_info)
988
989 @cherrypy.expose
990 def test(self, *args, **kwargs):
991 if not cherrypy.config.get("server.enable_test") or (
992 isinstance(cherrypy.config["server.enable_test"], str)
993 and cherrypy.config["server.enable_test"].lower() == "false"
994 ):
995 cherrypy.response.status = HTTPStatus.METHOD_NOT_ALLOWED.value
996 return "test URL is disabled"
997 thread_info = None
998 if args and args[0] == "help":
999 return (
1000 "<html><pre>\ninit\nfile/<name> download file\ndb-clear/table\nfs-clear[/folder]\nlogin\nlogin2\n"
1001 "sleep/<time>\nmessage/topic\n</pre></html>"
1002 )
1003
1004 elif args and args[0] == "init":
1005 try:
1006 # self.engine.load_dbase(cherrypy.request.app.config)
1007 self.engine.create_admin()
1008 return "Done. User 'admin', password 'admin' created"
1009 except Exception:
1010 cherrypy.response.status = HTTPStatus.FORBIDDEN.value
1011 return self._format_out("Database already initialized")
1012 elif args and args[0] == "file":
1013 return cherrypy.lib.static.serve_file(
1014 cherrypy.tree.apps["/osm"].config["storage"]["path"] + "/" + args[1],
1015 "text/plain",
1016 "attachment",
1017 )
1018 elif args and args[0] == "file2":
1019 f_path = (
1020 cherrypy.tree.apps["/osm"].config["storage"]["path"] + "/" + args[1]
1021 )
1022 f = open(f_path, "r")
1023 cherrypy.response.headers["Content-type"] = "text/plain"
1024 return f
1025
1026 elif len(args) == 2 and args[0] == "db-clear":
1027 deleted_info = self.engine.db.del_list(args[1], kwargs)
1028 return "{} {} deleted\n".format(deleted_info["deleted"], args[1])
1029 elif len(args) and args[0] == "fs-clear":
1030 if len(args) >= 2:
1031 folders = (args[1],)
1032 else:
1033 folders = self.engine.fs.dir_ls(".")
1034 for folder in folders:
1035 self.engine.fs.file_delete(folder)
1036 return ",".join(folders) + " folders deleted\n"
1037 elif args and args[0] == "login":
1038 if not cherrypy.request.headers.get("Authorization"):
1039 cherrypy.response.headers[
1040 "WWW-Authenticate"
1041 ] = 'Basic realm="Access to OSM site", charset="UTF-8"'
1042 cherrypy.response.status = HTTPStatus.UNAUTHORIZED.value
1043 elif args and args[0] == "login2":
1044 if not cherrypy.request.headers.get("Authorization"):
1045 cherrypy.response.headers[
1046 "WWW-Authenticate"
1047 ] = 'Bearer realm="Access to OSM site"'
1048 cherrypy.response.status = HTTPStatus.UNAUTHORIZED.value
1049 elif args and args[0] == "sleep":
1050 sleep_time = 5
1051 try:
1052 sleep_time = int(args[1])
1053 except Exception:
1054 cherrypy.response.status = HTTPStatus.FORBIDDEN.value
1055 return self._format_out("Database already initialized")
1056 thread_info = cherrypy.thread_data
1057 print(thread_info)
1058 time.sleep(sleep_time)
1059 # thread_info
1060 elif len(args) >= 2 and args[0] == "message":
1061 main_topic = args[1]
1062 return_text = "<html><pre>{} ->\n".format(main_topic)
1063 try:
1064 if cherrypy.request.method == "POST":
1065 to_send = yaml.load(cherrypy.request.body, Loader=yaml.SafeLoader)
1066 for k, v in to_send.items():
1067 self.engine.msg.write(main_topic, k, v)
1068 return_text += " {}: {}\n".format(k, v)
1069 elif cherrypy.request.method == "GET":
1070 for k, v in kwargs.items():
1071 v_dict = yaml.load(v, Loader=yaml.SafeLoader)
1072 self.engine.msg.write(main_topic, k, v_dict)
1073 return_text += " {}: {}\n".format(k, v_dict)
1074 except Exception as e:
1075 return_text += "Error: " + str(e)
1076 return_text += "</pre></html>\n"
1077 return return_text
1078
1079 return_text = (
1080 "<html><pre>\nheaders:\n args: {}\n".format(args)
1081 + " kwargs: {}\n".format(kwargs)
1082 + " headers: {}\n".format(cherrypy.request.headers)
1083 + " path_info: {}\n".format(cherrypy.request.path_info)
1084 + " query_string: {}\n".format(cherrypy.request.query_string)
1085 + " session: {}\n".format(cherrypy.session)
1086 + " cookie: {}\n".format(cherrypy.request.cookie)
1087 + " method: {}\n".format(cherrypy.request.method)
1088 + " session: {}\n".format(cherrypy.session.get("fieldname"))
1089 + " body:\n"
1090 )
1091 return_text += " length: {}\n".format(cherrypy.request.body.length)
1092 if cherrypy.request.body.length:
1093 return_text += " content: {}\n".format(
1094 str(
1095 cherrypy.request.body.read(
1096 int(cherrypy.request.headers.get("Content-Length", 0))
1097 )
1098 )
1099 )
1100 if thread_info:
1101 return_text += "thread: {}\n".format(thread_info)
1102 return_text += "</pre></html>"
1103 return return_text
1104
1105 @staticmethod
1106 def _check_valid_url_method(method, *args):
1107 if len(args) < 3:
1108 raise NbiException(
1109 "URL must contain at least 'main_topic/version/topic'",
1110 HTTPStatus.METHOD_NOT_ALLOWED,
1111 )
1112
1113 reference = valid_url_methods
1114 for arg in args:
1115 if arg is None:
1116 break
1117 if not isinstance(reference, dict):
1118 raise NbiException(
1119 "URL contains unexpected extra items '{}'".format(arg),
1120 HTTPStatus.METHOD_NOT_ALLOWED,
1121 )
1122
1123 if arg in reference:
1124 reference = reference[arg]
1125 elif "<ID>" in reference:
1126 reference = reference["<ID>"]
1127 elif "*" in reference:
1128 # if there is content
1129 if reference["*"]:
1130 reference = reference["*"]
1131 break
1132 else:
1133 raise NbiException(
1134 "Unexpected URL item {}".format(arg), HTTPStatus.METHOD_NOT_ALLOWED
1135 )
1136 if "TODO" in reference and method in reference["TODO"]:
1137 raise NbiException(
1138 "Method {} not supported yet for this URL".format(method),
1139 HTTPStatus.NOT_IMPLEMENTED,
1140 )
1141 elif "METHODS" in reference and method not in reference["METHODS"]:
1142 raise NbiException(
1143 "Method {} not supported for this URL".format(method),
1144 HTTPStatus.METHOD_NOT_ALLOWED,
1145 )
1146 return reference["ROLE_PERMISSION"] + method.lower()
1147
1148 @staticmethod
1149 def _set_location_header(main_topic, version, topic, id):
1150 """
1151 Insert response header Location with the URL of created item base on URL params
1152 :param main_topic:
1153 :param version:
1154 :param topic:
1155 :param id:
1156 :return: None
1157 """
1158 # Use cherrypy.request.base for absoluted path and make use of request.header HOST just in case behind aNAT
1159 cherrypy.response.headers["Location"] = "/osm/{}/{}/{}/{}".format(
1160 main_topic, version, topic, id
1161 )
1162 return
1163
1164 @staticmethod
1165 def _extract_query_string_operations(kwargs, method):
1166 """
1167
1168 :param kwargs:
1169 :return:
1170 """
1171 query_string_operations = []
1172 if kwargs:
1173 for qs in ("FORCE", "PUBLIC", "ADMIN", "SET_PROJECT"):
1174 if qs in kwargs and kwargs[qs].lower() != "false":
1175 query_string_operations.append(qs.lower() + ":" + method.lower())
1176 return query_string_operations
1177
1178 @staticmethod
1179 def _manage_admin_query(token_info, kwargs, method, _id):
1180 """
1181 Processes the administrator query inputs (if any) of FORCE, ADMIN, PUBLIC, SET_PROJECT
1182 Check that users has rights to use them and returs the admin_query
1183 :param token_info: token_info rights obtained by token
1184 :param kwargs: query string input.
1185 :param method: http method: GET, POSST, PUT, ...
1186 :param _id:
1187 :return: admin_query dictionary with keys:
1188 public: True, False or None
1189 force: True or False
1190 project_id: tuple with projects used for accessing an element
1191 set_project: tuple with projects that a created element will belong to
1192 method: show, list, delete, write
1193 """
1194 admin_query = {
1195 "force": False,
1196 "project_id": (token_info["project_id"],),
1197 "username": token_info["username"],
1198 "admin": token_info["admin"],
1199 "public": None,
1200 "allow_show_user_project_role": token_info["allow_show_user_project_role"],
1201 }
1202 if kwargs:
1203 # FORCE
1204 if "FORCE" in kwargs:
1205 if (
1206 kwargs["FORCE"].lower() != "false"
1207 ): # if None or True set force to True
1208 admin_query["force"] = True
1209 del kwargs["FORCE"]
1210 # PUBLIC
1211 if "PUBLIC" in kwargs:
1212 if (
1213 kwargs["PUBLIC"].lower() != "false"
1214 ): # if None or True set public to True
1215 admin_query["public"] = True
1216 else:
1217 admin_query["public"] = False
1218 del kwargs["PUBLIC"]
1219 # ADMIN
1220 if "ADMIN" in kwargs:
1221 behave_as = kwargs.pop("ADMIN")
1222 if behave_as.lower() != "false":
1223 if not token_info["admin"]:
1224 raise NbiException(
1225 "Only admin projects can use 'ADMIN' query string",
1226 HTTPStatus.UNAUTHORIZED,
1227 )
1228 if (
1229 not behave_as or behave_as.lower() == "true"
1230 ): # convert True, None to empty list
1231 admin_query["project_id"] = ()
1232 elif isinstance(behave_as, (list, tuple)):
1233 admin_query["project_id"] = behave_as
1234 else: # isinstance(behave_as, str)
1235 admin_query["project_id"] = (behave_as,)
1236 if "SET_PROJECT" in kwargs:
1237 set_project = kwargs.pop("SET_PROJECT")
1238 if not set_project:
1239 admin_query["set_project"] = list(admin_query["project_id"])
1240 else:
1241 if isinstance(set_project, str):
1242 set_project = (set_project,)
1243 if admin_query["project_id"]:
1244 for p in set_project:
1245 if p not in admin_query["project_id"]:
1246 raise NbiException(
1247 "Unauthorized for 'SET_PROJECT={p}'. Try with 'ADMIN=True' or "
1248 "'ADMIN='{p}'".format(p=p),
1249 HTTPStatus.UNAUTHORIZED,
1250 )
1251 admin_query["set_project"] = set_project
1252
1253 # PROJECT_READ
1254 # if "PROJECT_READ" in kwargs:
1255 # admin_query["project"] = kwargs.pop("project")
1256 # if admin_query["project"] == token_info["project_id"]:
1257 if method == "GET":
1258 if _id:
1259 admin_query["method"] = "show"
1260 else:
1261 admin_query["method"] = "list"
1262 elif method == "DELETE":
1263 admin_query["method"] = "delete"
1264 else:
1265 admin_query["method"] = "write"
1266 return admin_query
1267
1268 @cherrypy.expose
1269 def default(
1270 self,
1271 main_topic=None,
1272 version=None,
1273 topic=None,
1274 _id=None,
1275 item=None,
1276 *args,
1277 **kwargs
1278 ):
1279 token_info = None
1280 outdata = None
1281 _format = None
1282 method = "DONE"
1283 engine_topic = None
1284 rollback = []
1285 engine_session = None
1286 try:
1287 if not main_topic or not version or not topic:
1288 raise NbiException(
1289 "URL must contain at least 'main_topic/version/topic'",
1290 HTTPStatus.METHOD_NOT_ALLOWED,
1291 )
1292 if main_topic not in (
1293 "admin",
1294 "vnfpkgm",
1295 "nsd",
1296 "nslcm",
1297 "pdu",
1298 "nst",
1299 "nsilcm",
1300 "nspm",
1301 ):
1302 raise NbiException(
1303 "URL main_topic '{}' not supported".format(main_topic),
1304 HTTPStatus.METHOD_NOT_ALLOWED,
1305 )
1306 if version != "v1":
1307 raise NbiException(
1308 "URL version '{}' not supported".format(version),
1309 HTTPStatus.METHOD_NOT_ALLOWED,
1310 )
1311
1312 if (
1313 kwargs
1314 and "METHOD" in kwargs
1315 and kwargs["METHOD"] in ("PUT", "POST", "DELETE", "GET", "PATCH")
1316 ):
1317 method = kwargs.pop("METHOD")
1318 else:
1319 method = cherrypy.request.method
1320
1321 role_permission = self._check_valid_url_method(
1322 method, main_topic, version, topic, _id, item, *args
1323 )
1324 query_string_operations = self._extract_query_string_operations(
1325 kwargs, method
1326 )
1327 if main_topic == "admin" and topic == "tokens":
1328 return self.token(method, _id, kwargs)
1329 token_info = self.authenticator.authorize(
1330 role_permission, query_string_operations, _id
1331 )
1332 if main_topic == "admin" and topic == "domains":
1333 return self.domain()
1334 engine_session = self._manage_admin_query(token_info, kwargs, method, _id)
1335 indata = self._format_in(kwargs)
1336 engine_topic = topic
1337
1338 if item and topic != "pm_jobs":
1339 engine_topic = item
1340
1341 if main_topic == "nsd":
1342 engine_topic = "nsds"
1343 elif main_topic == "vnfpkgm":
1344 engine_topic = "vnfds"
1345 if topic == "vnfpkg_op_occs":
1346 engine_topic = "vnfpkgops"
1347 if topic == "vnf_packages" and item == "action":
1348 engine_topic = "vnfpkgops"
1349 elif main_topic == "nslcm":
1350 engine_topic = "nsrs"
1351 if topic == "ns_lcm_op_occs":
1352 engine_topic = "nslcmops"
1353 if topic == "vnfrs" or topic == "vnf_instances":
1354 engine_topic = "vnfrs"
1355 elif main_topic == "nst":
1356 engine_topic = "nsts"
1357 elif main_topic == "nsilcm":
1358 engine_topic = "nsis"
1359 if topic == "nsi_lcm_op_occs":
1360 engine_topic = "nsilcmops"
1361 elif main_topic == "pdu":
1362 engine_topic = "pdus"
1363 if (
1364 engine_topic == "vims"
1365 ): # TODO this is for backward compatibility, it will be removed in the future
1366 engine_topic = "vim_accounts"
1367
1368 if topic == "subscriptions":
1369 engine_topic = main_topic + "_" + topic
1370
1371 if method == "GET":
1372 if item in (
1373 "nsd_content",
1374 "package_content",
1375 "artifacts",
1376 "vnfd",
1377 "nsd",
1378 "nst",
1379 "nst_content",
1380 ):
1381 if item in ("vnfd", "nsd", "nst"):
1382 path = "$DESCRIPTOR"
1383 elif args:
1384 path = args
1385 elif item == "artifacts":
1386 path = ()
1387 else:
1388 path = None
1389 file, _format = self.engine.get_file(
1390 engine_session,
1391 engine_topic,
1392 _id,
1393 path,
1394 cherrypy.request.headers.get("Accept"),
1395 )
1396 outdata = file
1397 elif not _id:
1398 outdata = self.engine.get_item_list(
1399 engine_session, engine_topic, kwargs, api_req=True
1400 )
1401 else:
1402 if item == "reports":
1403 # TODO check that project_id (_id in this context) has permissions
1404 _id = args[0]
1405 filter_q = None
1406 if "vcaStatusRefresh" in kwargs:
1407 filter_q = {"vcaStatusRefresh": kwargs["vcaStatusRefresh"]}
1408 outdata = self.engine.get_item(engine_session, engine_topic, _id, filter_q, True)
1409
1410 elif method == "POST":
1411 cherrypy.response.status = HTTPStatus.CREATED.value
1412 if topic in (
1413 "ns_descriptors_content",
1414 "vnf_packages_content",
1415 "netslice_templates_content",
1416 ):
1417 _id = cherrypy.request.headers.get("Transaction-Id")
1418 if not _id:
1419 _id, _ = self.engine.new_item(
1420 rollback,
1421 engine_session,
1422 engine_topic,
1423 {},
1424 None,
1425 cherrypy.request.headers,
1426 )
1427 completed = self.engine.upload_content(
1428 engine_session,
1429 engine_topic,
1430 _id,
1431 indata,
1432 kwargs,
1433 cherrypy.request.headers,
1434 )
1435 if completed:
1436 self._set_location_header(main_topic, version, topic, _id)
1437 else:
1438 cherrypy.response.headers["Transaction-Id"] = _id
1439 outdata = {"id": _id}
1440 elif topic == "ns_instances_content":
1441 # creates NSR
1442 _id, _ = self.engine.new_item(
1443 rollback, engine_session, engine_topic, indata, kwargs
1444 )
1445 # creates nslcmop
1446 indata["lcmOperationType"] = "instantiate"
1447 indata["nsInstanceId"] = _id
1448 nslcmop_id, _ = self.engine.new_item(
1449 rollback, engine_session, "nslcmops", indata, None
1450 )
1451 self._set_location_header(main_topic, version, topic, _id)
1452 outdata = {"id": _id, "nslcmop_id": nslcmop_id}
1453 elif topic == "ns_instances" and item:
1454 indata["lcmOperationType"] = item
1455 indata["nsInstanceId"] = _id
1456 _id, _ = self.engine.new_item(
1457 rollback, engine_session, "nslcmops", indata, kwargs
1458 )
1459 self._set_location_header(
1460 main_topic, version, "ns_lcm_op_occs", _id
1461 )
1462 outdata = {"id": _id}
1463 cherrypy.response.status = HTTPStatus.ACCEPTED.value
1464 elif topic == "netslice_instances_content":
1465 # creates NetSlice_Instance_record (NSIR)
1466 _id, _ = self.engine.new_item(
1467 rollback, engine_session, engine_topic, indata, kwargs
1468 )
1469 self._set_location_header(main_topic, version, topic, _id)
1470 indata["lcmOperationType"] = "instantiate"
1471 indata["netsliceInstanceId"] = _id
1472 nsilcmop_id, _ = self.engine.new_item(
1473 rollback, engine_session, "nsilcmops", indata, kwargs
1474 )
1475 outdata = {"id": _id, "nsilcmop_id": nsilcmop_id}
1476 elif topic == "netslice_instances" and item:
1477 indata["lcmOperationType"] = item
1478 indata["netsliceInstanceId"] = _id
1479 _id, _ = self.engine.new_item(
1480 rollback, engine_session, "nsilcmops", indata, kwargs
1481 )
1482 self._set_location_header(
1483 main_topic, version, "nsi_lcm_op_occs", _id
1484 )
1485 outdata = {"id": _id}
1486 cherrypy.response.status = HTTPStatus.ACCEPTED.value
1487 elif topic == "vnf_packages" and item == "action":
1488 indata["lcmOperationType"] = item
1489 indata["vnfPkgId"] = _id
1490 _id, _ = self.engine.new_item(
1491 rollback, engine_session, "vnfpkgops", indata, kwargs
1492 )
1493 self._set_location_header(
1494 main_topic, version, "vnfpkg_op_occs", _id
1495 )
1496 outdata = {"id": _id}
1497 cherrypy.response.status = HTTPStatus.ACCEPTED.value
1498 elif topic == "subscriptions":
1499 _id, _ = self.engine.new_item(
1500 rollback, engine_session, engine_topic, indata, kwargs
1501 )
1502 self._set_location_header(main_topic, version, topic, _id)
1503 link = {}
1504 link["self"] = cherrypy.response.headers["Location"]
1505 outdata = {
1506 "id": _id,
1507 "filter": indata["filter"],
1508 "callbackUri": indata["CallbackUri"],
1509 "_links": link,
1510 }
1511 cherrypy.response.status = HTTPStatus.CREATED.value
1512 else:
1513 _id, op_id = self.engine.new_item(
1514 rollback,
1515 engine_session,
1516 engine_topic,
1517 indata,
1518 kwargs,
1519 cherrypy.request.headers,
1520 )
1521 self._set_location_header(main_topic, version, topic, _id)
1522 outdata = {"id": _id}
1523 if op_id:
1524 outdata["op_id"] = op_id
1525 cherrypy.response.status = HTTPStatus.ACCEPTED.value
1526 # TODO form NsdInfo when topic in ("ns_descriptors", "vnf_packages")
1527
1528 elif method == "DELETE":
1529 if not _id:
1530 outdata = self.engine.del_item_list(
1531 engine_session, engine_topic, kwargs
1532 )
1533 cherrypy.response.status = HTTPStatus.OK.value
1534 else: # len(args) > 1
1535 # for NS NSI generate an operation
1536 op_id = None
1537 if topic == "ns_instances_content" and not engine_session["force"]:
1538 nslcmop_desc = {
1539 "lcmOperationType": "terminate",
1540 "nsInstanceId": _id,
1541 "autoremove": True,
1542 }
1543 op_id, _ = self.engine.new_item(
1544 rollback, engine_session, "nslcmops", nslcmop_desc, kwargs
1545 )
1546 if op_id:
1547 outdata = {"_id": op_id}
1548 elif (
1549 topic == "netslice_instances_content"
1550 and not engine_session["force"]
1551 ):
1552 nsilcmop_desc = {
1553 "lcmOperationType": "terminate",
1554 "netsliceInstanceId": _id,
1555 "autoremove": True,
1556 }
1557 op_id, _ = self.engine.new_item(
1558 rollback, engine_session, "nsilcmops", nsilcmop_desc, None
1559 )
1560 if op_id:
1561 outdata = {"_id": op_id}
1562 # if there is not any deletion in process, delete
1563 if not op_id:
1564 op_id = self.engine.del_item(engine_session, engine_topic, _id)
1565 if op_id:
1566 outdata = {"op_id": op_id}
1567 cherrypy.response.status = (
1568 HTTPStatus.ACCEPTED.value
1569 if op_id
1570 else HTTPStatus.NO_CONTENT.value
1571 )
1572
1573 elif method in ("PUT", "PATCH"):
1574 op_id = None
1575 if not indata and not kwargs and not engine_session.get("set_project"):
1576 raise NbiException(
1577 "Nothing to update. Provide payload and/or query string",
1578 HTTPStatus.BAD_REQUEST,
1579 )
1580 if (
1581 item in ("nsd_content", "package_content", "nst_content")
1582 and method == "PUT"
1583 ):
1584 completed = self.engine.upload_content(
1585 engine_session,
1586 engine_topic,
1587 _id,
1588 indata,
1589 kwargs,
1590 cherrypy.request.headers,
1591 )
1592 if not completed:
1593 cherrypy.response.headers["Transaction-Id"] = id
1594 else:
1595 op_id = self.engine.edit_item(
1596 engine_session, engine_topic, _id, indata, kwargs
1597 )
1598
1599 if op_id:
1600 cherrypy.response.status = HTTPStatus.ACCEPTED.value
1601 outdata = {"op_id": op_id}
1602 else:
1603 cherrypy.response.status = HTTPStatus.NO_CONTENT.value
1604 outdata = None
1605 else:
1606 raise NbiException(
1607 "Method {} not allowed".format(method),
1608 HTTPStatus.METHOD_NOT_ALLOWED,
1609 )
1610
1611 # if Role information changes, it is needed to reload the information of roles
1612 if topic == "roles" and method != "GET":
1613 self.authenticator.load_operation_to_allowed_roles()
1614
1615 if (
1616 topic == "projects"
1617 and method == "DELETE"
1618 or topic in ["users", "roles"]
1619 and method in ["PUT", "PATCH", "DELETE"]
1620 ):
1621 self.authenticator.remove_token_from_cache()
1622
1623 return self._format_out(outdata, token_info, _format)
1624 except Exception as e:
1625 if isinstance(
1626 e,
1627 (
1628 NbiException,
1629 EngineException,
1630 DbException,
1631 FsException,
1632 MsgException,
1633 AuthException,
1634 ValidationError,
1635 AuthconnException,
1636 ),
1637 ):
1638 http_code_value = cherrypy.response.status = e.http_code.value
1639 http_code_name = e.http_code.name
1640 cherrypy.log("Exception {}".format(e))
1641 else:
1642 http_code_value = (
1643 cherrypy.response.status
1644 ) = HTTPStatus.BAD_REQUEST.value # INTERNAL_SERVER_ERROR
1645 cherrypy.log("CRITICAL: Exception {}".format(e), traceback=True)
1646 http_code_name = HTTPStatus.BAD_REQUEST.name
1647 if hasattr(outdata, "close"): # is an open file
1648 outdata.close()
1649 error_text = str(e)
1650 rollback.reverse()
1651 for rollback_item in rollback:
1652 try:
1653 if rollback_item.get("operation") == "set":
1654 self.engine.db.set_one(
1655 rollback_item["topic"],
1656 {"_id": rollback_item["_id"]},
1657 rollback_item["content"],
1658 fail_on_empty=False,
1659 )
1660 elif rollback_item.get("operation") == "del_list":
1661 self.engine.db.del_list(
1662 rollback_item["topic"],
1663 rollback_item["filter"],
1664 fail_on_empty=False,
1665 )
1666 else:
1667 self.engine.db.del_one(
1668 rollback_item["topic"],
1669 {"_id": rollback_item["_id"]},
1670 fail_on_empty=False,
1671 )
1672 except Exception as e2:
1673 rollback_error_text = "Rollback Exception {}: {}".format(
1674 rollback_item, e2
1675 )
1676 cherrypy.log(rollback_error_text)
1677 error_text += ". " + rollback_error_text
1678 # if isinstance(e, MsgException):
1679 # error_text = "{} has been '{}' but other modules cannot be informed because an error on bus".format(
1680 # engine_topic[:-1], method, error_text)
1681 problem_details = {
1682 "code": http_code_name,
1683 "status": http_code_value,
1684 "detail": error_text,
1685 }
1686 return self._format_out(problem_details, token_info)
1687 # raise cherrypy.HTTPError(e.http_code.value, str(e))
1688 finally:
1689 if token_info:
1690 self._format_login(token_info)
1691 if method in ("PUT", "PATCH", "POST") and isinstance(outdata, dict):
1692 for logging_id in ("id", "op_id", "nsilcmop_id", "nslcmop_id"):
1693 if outdata.get(logging_id):
1694 cherrypy.request.login += ";{}={}".format(
1695 logging_id, outdata[logging_id][:36]
1696 )
1697
1698
1699 def _start_service():
1700 """
1701 Callback function called when cherrypy.engine starts
1702 Override configuration with env variables
1703 Set database, storage, message configuration
1704 Init database with admin/admin user password
1705 """
1706 global nbi_server
1707 global subscription_thread
1708 cherrypy.log.error("Starting osm_nbi")
1709 # update general cherrypy configuration
1710 update_dict = {}
1711
1712 engine_config = cherrypy.tree.apps["/osm"].config
1713 for k, v in environ.items():
1714 if not k.startswith("OSMNBI_"):
1715 continue
1716 k1, _, k2 = k[7:].lower().partition("_")
1717 if not k2:
1718 continue
1719 try:
1720 # update static configuration
1721 if k == "OSMNBI_STATIC_DIR":
1722 engine_config["/static"]["tools.staticdir.dir"] = v
1723 engine_config["/static"]["tools.staticdir.on"] = True
1724 elif k == "OSMNBI_SOCKET_PORT" or k == "OSMNBI_SERVER_PORT":
1725 update_dict["server.socket_port"] = int(v)
1726 elif k == "OSMNBI_SOCKET_HOST" or k == "OSMNBI_SERVER_HOST":
1727 update_dict["server.socket_host"] = v
1728 elif k1 in ("server", "test", "auth", "log"):
1729 update_dict[k1 + "." + k2] = v
1730 elif k1 in ("message", "database", "storage", "authentication"):
1731 # k2 = k2.replace('_', '.')
1732 if k2 in ("port", "db_port"):
1733 engine_config[k1][k2] = int(v)
1734 else:
1735 engine_config[k1][k2] = v
1736
1737 except ValueError as e:
1738 cherrypy.log.error("Ignoring environ '{}': " + str(e))
1739 except Exception as e:
1740 cherrypy.log.warn("skipping environ '{}' on exception '{}'".format(k, e))
1741
1742 if update_dict:
1743 cherrypy.config.update(update_dict)
1744 engine_config["global"].update(update_dict)
1745
1746 # logging cherrypy
1747 log_format_simple = (
1748 "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(message)s"
1749 )
1750 log_formatter_simple = logging.Formatter(
1751 log_format_simple, datefmt="%Y-%m-%dT%H:%M:%S"
1752 )
1753 logger_server = logging.getLogger("cherrypy.error")
1754 logger_access = logging.getLogger("cherrypy.access")
1755 logger_cherry = logging.getLogger("cherrypy")
1756 logger_nbi = logging.getLogger("nbi")
1757
1758 if "log.file" in engine_config["global"]:
1759 file_handler = logging.handlers.RotatingFileHandler(
1760 engine_config["global"]["log.file"], maxBytes=100e6, backupCount=9, delay=0
1761 )
1762 file_handler.setFormatter(log_formatter_simple)
1763 logger_cherry.addHandler(file_handler)
1764 logger_nbi.addHandler(file_handler)
1765 # log always to standard output
1766 for format_, logger in {
1767 "nbi.server %(filename)s:%(lineno)s": logger_server,
1768 "nbi.access %(filename)s:%(lineno)s": logger_access,
1769 "%(name)s %(filename)s:%(lineno)s": logger_nbi,
1770 }.items():
1771 log_format_cherry = "%(asctime)s %(levelname)s {} %(message)s".format(format_)
1772 log_formatter_cherry = logging.Formatter(
1773 log_format_cherry, datefmt="%Y-%m-%dT%H:%M:%S"
1774 )
1775 str_handler = logging.StreamHandler()
1776 str_handler.setFormatter(log_formatter_cherry)
1777 logger.addHandler(str_handler)
1778
1779 if engine_config["global"].get("log.level"):
1780 logger_cherry.setLevel(engine_config["global"]["log.level"])
1781 logger_nbi.setLevel(engine_config["global"]["log.level"])
1782
1783 # logging other modules
1784 for k1, logname in {
1785 "message": "nbi.msg",
1786 "database": "nbi.db",
1787 "storage": "nbi.fs",
1788 }.items():
1789 engine_config[k1]["logger_name"] = logname
1790 logger_module = logging.getLogger(logname)
1791 if "logfile" in engine_config[k1]:
1792 file_handler = logging.handlers.RotatingFileHandler(
1793 engine_config[k1]["logfile"], maxBytes=100e6, backupCount=9, delay=0
1794 )
1795 file_handler.setFormatter(log_formatter_simple)
1796 logger_module.addHandler(file_handler)
1797 if "loglevel" in engine_config[k1]:
1798 logger_module.setLevel(engine_config[k1]["loglevel"])
1799 # TODO add more entries, e.g.: storage
1800 cherrypy.tree.apps["/osm"].root.engine.start(engine_config)
1801 cherrypy.tree.apps["/osm"].root.authenticator.start(engine_config)
1802 cherrypy.tree.apps["/osm"].root.engine.init_db(target_version=database_version)
1803 cherrypy.tree.apps["/osm"].root.authenticator.init_db(
1804 target_version=auth_database_version
1805 )
1806
1807 # start subscriptions thread:
1808 subscription_thread = SubscriptionThread(
1809 config=engine_config, engine=nbi_server.engine
1810 )
1811 subscription_thread.start()
1812 # Do not capture except SubscriptionException
1813
1814 backend = engine_config["authentication"]["backend"]
1815 cherrypy.log.error(
1816 "Starting OSM NBI Version '{} {}' with '{}' authentication backend".format(
1817 nbi_version, nbi_version_date, backend
1818 )
1819 )
1820
1821
1822 def _stop_service():
1823 """
1824 Callback function called when cherrypy.engine stops
1825 TODO: Ending database connections.
1826 """
1827 global subscription_thread
1828 if subscription_thread:
1829 subscription_thread.terminate()
1830 subscription_thread = None
1831 cherrypy.tree.apps["/osm"].root.engine.stop()
1832 cherrypy.log.error("Stopping osm_nbi")
1833
1834
1835 def nbi(config_file):
1836 global nbi_server
1837 # conf = {
1838 # '/': {
1839 # #'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
1840 # 'tools.sessions.on': True,
1841 # 'tools.response_headers.on': True,
1842 # # 'tools.response_headers.headers': [('Content-Type', 'text/plain')],
1843 # }
1844 # }
1845 # cherrypy.Server.ssl_module = 'builtin'
1846 # cherrypy.Server.ssl_certificate = "http/cert.pem"
1847 # cherrypy.Server.ssl_private_key = "http/privkey.pem"
1848 # cherrypy.Server.thread_pool = 10
1849 # cherrypy.config.update({'Server.socket_port': config["port"], 'Server.socket_host': config["host"]})
1850
1851 # cherrypy.config.update({'tools.auth_basic.on': True,
1852 # 'tools.auth_basic.realm': 'localhost',
1853 # 'tools.auth_basic.checkpassword': validate_password})
1854 nbi_server = Server()
1855 cherrypy.engine.subscribe("start", _start_service)
1856 cherrypy.engine.subscribe("stop", _stop_service)
1857 cherrypy.quickstart(nbi_server, "/osm", config_file)
1858
1859
1860 def usage():
1861 print(
1862 """Usage: {} [options]
1863 -c|--config [configuration_file]: loads the configuration file (default: ./nbi.cfg)
1864 -h|--help: shows this help
1865 """.format(
1866 sys.argv[0]
1867 )
1868 )
1869 # --log-socket-host HOST: send logs to this host")
1870 # --log-socket-port PORT: send logs using this port (default: 9022)")
1871
1872
1873 if __name__ == "__main__":
1874 try:
1875 # load parameters and configuration
1876 opts, args = getopt.getopt(sys.argv[1:], "hvc:", ["config=", "help"])
1877 # TODO add "log-socket-host=", "log-socket-port=", "log-file="
1878 config_file = None
1879 for o, a in opts:
1880 if o in ("-h", "--help"):
1881 usage()
1882 sys.exit()
1883 elif o in ("-c", "--config"):
1884 config_file = a
1885 # elif o == "--log-socket-port":
1886 # log_socket_port = a
1887 # elif o == "--log-socket-host":
1888 # log_socket_host = a
1889 # elif o == "--log-file":
1890 # log_file = a
1891 else:
1892 assert False, "Unhandled option"
1893 if config_file:
1894 if not path.isfile(config_file):
1895 print(
1896 "configuration file '{}' that not exist".format(config_file),
1897 file=sys.stderr,
1898 )
1899 exit(1)
1900 else:
1901 for config_file in (
1902 __file__[: __file__.rfind(".")] + ".cfg",
1903 "./nbi.cfg",
1904 "/etc/osm/nbi.cfg",
1905 ):
1906 if path.isfile(config_file):
1907 break
1908 else:
1909 print(
1910 "No configuration file 'nbi.cfg' found neither at local folder nor at /etc/osm/",
1911 file=sys.stderr,
1912 )
1913 exit(1)
1914 nbi(config_file)
1915 except getopt.GetoptError as e:
1916 print(str(e), file=sys.stderr)
1917 # usage()
1918 exit(1)