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