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