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