2 # -*- coding: utf-8 -*-
8 import html_out
as html
10 import logging
.handlers
14 from authconn
import AuthException
15 from auth
import Authenticator
16 from engine
import Engine
, EngineException
17 from osm_common
.dbbase
import DbException
18 from osm_common
.fsbase
import FsException
19 from osm_common
.msgbase
import MsgException
20 from http
import HTTPStatus
21 from codecs
import getreader
22 from os
import environ
, path
24 __author__
= "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
26 # TODO consider to remove and provide version using the static version file
28 version_date
= "Apr 2018"
29 database_version
= '1.0'
30 auth_database_version
= '1.0'
33 North Bound Interface (O: OSM specific; 5,X: SOL005 not implemented yet; O5: SOL005 implemented)
34 URL: /osm GET POST PUT DELETE PATCH
36 /ns_descriptors_content O O
42 /artifacts[/<artifactPath>] O
50 /vnf_packages_content O O
54 /package_content O5 O5
57 /artifacts[/<artifactPath>] O5
62 /ns_instances_content O O
74 /vnf_instances (also vnfrs for compatibility) O
88 /vims_accounts (also vims for compatibility) O O
94 Follows SOL005 section 4.3.2 It contains extra METHOD to override http method, FORCE to force.
95 For filtering inside array, it must select the element of the array, or add ANYINDEX to apply the filtering over any
96 item of the array, that is, pass if any item of the array pass the filter.
97 It allows both ne and neq for not equal
98 TODO: 4.3.3 Attribute selectors
99 all_fields, fields=x,y,.., exclude_default, exclude_fields=x,y,...
100 (none) … same as “exclude_default”
101 all_fields … all attributes.
102 fields=<list> … all attributes except all complex attributes with minimum cardinality of zero that are not
103 conditionally mandatory, and that are not provided in <list>.
104 exclude_fields=<list> … all attributes except those complex attributes with a minimum cardinality of zero that
105 are not conditionally mandatory, and that are provided in <list>.
106 exclude_default … all attributes except those complex attributes with a minimum cardinality of zero that are not
107 conditionally mandatory, and that are part of the "default exclude set" defined in the present specification for
108 the particular resource
109 exclude_default and include=<list> … all attributes except those complex attributes with a minimum cardinality
110 of zero that are not conditionally mandatory and that are part of the "default exclude set" defined in the
111 present specification for the particular resource, but that are not part of <list>
112 Header field name Reference Example Descriptions
113 Accept IETF RFC 7231 [19] application/json Content-Types that are acceptable for the response.
114 This header field shall be present if the response is expected to have a non-empty message body.
115 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the request.
116 This header field shall be present if the request has a non-empty message body.
117 Authorization IETF RFC 7235 [22] Bearer mF_9.B5f-4.1JqM The authorization token for the request.
118 Details are specified in clause 4.5.3.
119 Range IETF RFC 7233 [21] 1000-2000 Requested range of bytes from a file
120 Header field name Reference Example Descriptions
121 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the response.
122 This header field shall be present if the response has a non-empty message body.
123 Location IETF RFC 7231 [19] http://www.example.com/vnflcm/v1/vnf_instances/123 Used in redirection, or when a
124 new resource has been created.
125 This header field shall be present if the response status code is 201 or 3xx.
126 In the present document this header field is also used if the response status code is 202 and a new resource was
128 WWW-Authenticate IETF RFC 7235 [22] Bearer realm="example" Challenge if the corresponding HTTP request has not
129 provided authorization, or error details if the corresponding HTTP request has provided an invalid authorization
131 Accept-Ranges IETF RFC 7233 [21] bytes Used by the Server to signal whether or not it supports ranges for
133 Content-Range IETF RFC 7233 [21] bytes 21010-47021/ 47022 Signals the byte range that is contained in the
134 response, and the total length of the file.
135 Retry-After IETF RFC 7231 [19] Fri, 31 Dec 1999 23:59:59 GMT
139 class NbiException(Exception):
141 def __init__(self
, message
, http_code
=HTTPStatus
.METHOD_NOT_ALLOWED
):
142 Exception.__init
__(self
, message
)
143 self
.http_code
= http_code
146 class Server(object):
148 # to decode bytes to str
149 reader
= getreader("utf-8")
153 self
.engine
= Engine()
154 self
.authenticator
= Authenticator()
155 self
.valid_methods
= { # contains allowed URL and methods
158 "tokens": {"METHODS": ("GET", "POST", "DELETE"),
159 "<ID>": {"METHODS": ("GET", "DELETE")}
161 "users": {"METHODS": ("GET", "POST"),
162 "<ID>": {"METHODS": ("GET", "POST", "DELETE", "PATCH", "PUT")}
164 "projects": {"METHODS": ("GET", "POST"),
165 "<ID>": {"METHODS": ("GET", "DELETE")}
167 "vims": {"METHODS": ("GET", "POST"),
168 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
170 "vim_accounts": {"METHODS": ("GET", "POST"),
171 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
173 "sdns": {"METHODS": ("GET", "POST"),
174 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
180 "pdu_descriptors": {"METHODS": ("GET", "POST"),
181 "<ID>": {"METHODS": ("GET", "POST", "DELETE", "PATCH", "PUT")}
187 "ns_descriptors_content": {"METHODS": ("GET", "POST"),
188 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
190 "ns_descriptors": {"METHODS": ("GET", "POST"),
191 "<ID>": {"METHODS": ("GET", "DELETE"), "TODO": "PATCH",
192 "nsd_content": {"METHODS": ("GET", "PUT")},
193 "nsd": {"METHODS": "GET"}, # descriptor inside package
194 "artifacts": {"*": {"METHODS": "GET"}}
197 "pnf_descriptors": {"TODO": ("GET", "POST"),
198 "<ID>": {"TODO": ("GET", "DELETE", "PATCH"),
199 "pnfd_content": {"TODO": ("GET", "PUT")}
202 "subscriptions": {"TODO": ("GET", "POST"),
203 "<ID>": {"TODO": ("GET", "DELETE")}
209 "vnf_packages_content": {"METHODS": ("GET", "POST"),
210 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
212 "vnf_packages": {"METHODS": ("GET", "POST"),
213 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"), # GET: vnfPkgInfo
214 "package_content": {"METHODS": ("GET", "PUT"), # package
215 "upload_from_uri": {"TODO": "POST"}
217 "vnfd": {"METHODS": "GET"}, # descriptor inside package
218 "artifacts": {"*": {"METHODS": "GET"}}
221 "subscriptions": {"TODO": ("GET", "POST"),
222 "<ID>": {"TODO": ("GET", "DELETE")}
228 "ns_instances_content": {"METHODS": ("GET", "POST"),
229 "<ID>": {"METHODS": ("GET", "DELETE")}
231 "ns_instances": {"METHODS": ("GET", "POST"),
232 "<ID>": {"METHODS": ("GET", "DELETE"),
233 "scale": {"METHODS": "POST"},
234 "terminate": {"METHODS": "POST"},
235 "instantiate": {"METHODS": "POST"},
236 "action": {"METHODS": "POST"},
239 "ns_lcm_op_occs": {"METHODS": "GET",
240 "<ID>": {"METHODS": "GET"},
242 "vnfrs": {"METHODS": ("GET"),
243 "<ID>": {"METHODS": ("GET")}
245 "vnf_instances": {"METHODS": ("GET"),
246 "<ID>": {"METHODS": ("GET")}
252 def _format_in(self
, kwargs
):
255 if cherrypy
.request
.body
.length
:
256 error_text
= "Invalid input format "
258 if "Content-Type" in cherrypy
.request
.headers
:
259 if "application/json" in cherrypy
.request
.headers
["Content-Type"]:
260 error_text
= "Invalid json format "
261 indata
= json
.load(self
.reader(cherrypy
.request
.body
))
262 cherrypy
.request
.headers
.pop("Content-File-MD5", None)
263 elif "application/yaml" in cherrypy
.request
.headers
["Content-Type"]:
264 error_text
= "Invalid yaml format "
265 indata
= yaml
.load(cherrypy
.request
.body
)
266 cherrypy
.request
.headers
.pop("Content-File-MD5", None)
267 elif "application/binary" in cherrypy
.request
.headers
["Content-Type"] or \
268 "application/gzip" in cherrypy
.request
.headers
["Content-Type"] or \
269 "application/zip" in cherrypy
.request
.headers
["Content-Type"] or \
270 "text/plain" in cherrypy
.request
.headers
["Content-Type"]:
271 indata
= cherrypy
.request
.body
# .read()
272 elif "multipart/form-data" in cherrypy
.request
.headers
["Content-Type"]:
273 if "descriptor_file" in kwargs
:
274 filecontent
= kwargs
.pop("descriptor_file")
275 if not filecontent
.file:
276 raise NbiException("empty file or content", HTTPStatus
.BAD_REQUEST
)
277 indata
= filecontent
.file # .read()
278 if filecontent
.content_type
.value
:
279 cherrypy
.request
.headers
["Content-Type"] = filecontent
.content_type
.value
281 # raise cherrypy.HTTPError(HTTPStatus.Not_Acceptable,
282 # "Only 'Content-Type' of type 'application/json' or
283 # 'application/yaml' for input format are available")
284 error_text
= "Invalid yaml format "
285 indata
= yaml
.load(cherrypy
.request
.body
)
286 cherrypy
.request
.headers
.pop("Content-File-MD5", None)
288 error_text
= "Invalid yaml format "
289 indata
= yaml
.load(cherrypy
.request
.body
)
290 cherrypy
.request
.headers
.pop("Content-File-MD5", None)
295 if cherrypy
.request
.headers
.get("Query-String-Format") == "yaml":
298 for k
, v
in kwargs
.items():
299 if isinstance(v
, str):
304 kwargs
[k
] = yaml
.load(v
)
307 elif k
.endswith(".gt") or k
.endswith(".lt") or k
.endswith(".gte") or k
.endswith(".lte"):
315 elif v
.find(",") > 0:
316 kwargs
[k
] = v
.split(",")
317 elif isinstance(v
, (list, tuple)):
318 for index
in range(0, len(v
)):
323 v
[index
] = yaml
.load(v
[index
])
328 except (ValueError, yaml
.YAMLError
) as exc
:
329 raise NbiException(error_text
+ str(exc
), HTTPStatus
.BAD_REQUEST
)
330 except KeyError as exc
:
331 raise NbiException("Query string error: " + str(exc
), HTTPStatus
.BAD_REQUEST
)
332 except Exception as exc
:
333 raise NbiException(error_text
+ str(exc
), HTTPStatus
.BAD_REQUEST
)
336 def _format_out(data
, session
=None, _format
=None):
338 return string of dictionary data according to requested json, yaml, xml. By default json
339 :param data: response to be sent. Can be a dict, text or file
341 :param _format: The format to be set as Content-Type ir data is a file
344 accept
= cherrypy
.request
.headers
.get("Accept")
346 if accept
and "text/html" in accept
:
347 return html
.format(data
, cherrypy
.request
, cherrypy
.response
, session
)
348 # cherrypy.response.status = HTTPStatus.NO_CONTENT.value
350 elif hasattr(data
, "read"): # file object
352 cherrypy
.response
.headers
["Content-Type"] = _format
353 elif "b" in data
.mode
: # binariy asssumig zip
354 cherrypy
.response
.headers
["Content-Type"] = 'application/zip'
356 cherrypy
.response
.headers
["Content-Type"] = 'text/plain'
357 # TODO check that cherrypy close file. If not implement pending things to close per thread next
360 if "application/json" in accept
:
361 cherrypy
.response
.headers
["Content-Type"] = 'application/json; charset=utf-8'
362 a
= json
.dumps(data
, indent
=4) + "\n"
363 return a
.encode("utf8")
364 elif "text/html" in accept
:
365 return html
.format(data
, cherrypy
.request
, cherrypy
.response
, session
)
367 elif "application/yaml" in accept
or "*/*" in accept
or "text/plain" in accept
:
370 raise cherrypy
.HTTPError(HTTPStatus
.NOT_ACCEPTABLE
.value
,
371 "Only 'Accept' of type 'application/json' or 'application/yaml' "
372 "for output format are available")
373 cherrypy
.response
.headers
["Content-Type"] = 'application/yaml'
374 return yaml
.safe_dump(data
, explicit_start
=True, indent
=4, default_flow_style
=False, tags
=False,
375 encoding
='utf-8', allow_unicode
=True) # , canonical=True, default_style='"'
378 def index(self
, *args
, **kwargs
):
381 if cherrypy
.request
.method
== "GET":
382 session
= self
.authenticator
.authorize()
383 outdata
= "Index page"
385 raise cherrypy
.HTTPError(HTTPStatus
.METHOD_NOT_ALLOWED
.value
,
386 "Method {} not allowed for tokens".format(cherrypy
.request
.method
))
388 return self
._format
_out
(outdata
, session
)
390 except (EngineException
, AuthException
) as e
:
391 cherrypy
.log("index Exception {}".format(e
))
392 cherrypy
.response
.status
= e
.http_code
.value
393 return self
._format
_out
("Welcome to OSM!", session
)
396 def version(self
, *args
, **kwargs
):
397 # TODO consider to remove and provide version using the static version file
398 global __version__
, version_date
400 if cherrypy
.request
.method
!= "GET":
401 raise NbiException("Only method GET is allowed", HTTPStatus
.METHOD_NOT_ALLOWED
)
403 raise NbiException("Invalid URL or query string for version", HTTPStatus
.METHOD_NOT_ALLOWED
)
404 return __version__
+ " " + version_date
405 except NbiException
as e
:
406 cherrypy
.response
.status
= e
.http_code
.value
408 "code": e
.http_code
.name
,
409 "status": e
.http_code
.value
,
412 return self
._format
_out
(problem_details
, None)
415 def token(self
, method
, token_id
=None, kwargs
=None):
417 # self.engine.load_dbase(cherrypy.request.app.config)
418 indata
= self
._format
_in
(kwargs
)
419 if not isinstance(indata
, dict):
420 raise NbiException("Expected application/yaml or application/json Content-Type", HTTPStatus
.BAD_REQUEST
)
423 session
= self
.authenticator
.authorize()
425 outdata
= self
.authenticator
.get_token(session
, token_id
)
427 outdata
= self
.authenticator
.get_token_list(session
)
428 elif method
== "POST":
430 session
= self
.authenticator
.authorize()
434 indata
.update(kwargs
)
435 outdata
= self
.authenticator
.new_token(session
, indata
, cherrypy
.request
.remote
)
437 cherrypy
.session
['Authorization'] = outdata
["_id"]
438 self
._set
_location
_header
("admin", "v1", "tokens", outdata
["_id"])
439 # cherrypy.response.cookie["Authorization"] = outdata["id"]
440 # cherrypy.response.cookie["Authorization"]['expires'] = 3600
441 elif method
== "DELETE":
442 if not token_id
and "id" in kwargs
:
443 token_id
= kwargs
["id"]
445 session
= self
.authenticator
.authorize()
446 token_id
= session
["_id"]
447 outdata
= self
.authenticator
.del_token(token_id
)
449 cherrypy
.session
['Authorization'] = "logout"
450 # cherrypy.response.cookie["Authorization"] = token_id
451 # cherrypy.response.cookie["Authorization"]['expires'] = 0
453 raise NbiException("Method {} not allowed for token".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
454 return self
._format
_out
(outdata
, session
)
455 except (NbiException
, EngineException
, DbException
, AuthException
) as e
:
456 cherrypy
.log("tokens Exception {}".format(e
))
457 cherrypy
.response
.status
= e
.http_code
.value
459 "code": e
.http_code
.name
,
460 "status": e
.http_code
.value
,
463 return self
._format
_out
(problem_details
, session
)
466 def test(self
, *args
, **kwargs
):
468 if args
and args
[0] == "help":
469 return "<html><pre>\ninit\nfile/<name> download file\ndb-clear/table\nprune\nlogin\nlogin2\n"\
470 "sleep/<time>\nmessage/topic\n</pre></html>"
472 elif args
and args
[0] == "init":
474 # self.engine.load_dbase(cherrypy.request.app.config)
475 self
.engine
.create_admin()
476 return "Done. User 'admin', password 'admin' created"
478 cherrypy
.response
.status
= HTTPStatus
.FORBIDDEN
.value
479 return self
._format
_out
("Database already initialized")
480 elif args
and args
[0] == "file":
481 return cherrypy
.lib
.static
.serve_file(cherrypy
.tree
.apps
['/osm'].config
["storage"]["path"] + "/" + args
[1],
482 "text/plain", "attachment")
483 elif args
and args
[0] == "file2":
484 f_path
= cherrypy
.tree
.apps
['/osm'].config
["storage"]["path"] + "/" + args
[1]
485 f
= open(f_path
, "r")
486 cherrypy
.response
.headers
["Content-type"] = "text/plain"
489 elif len(args
) == 2 and args
[0] == "db-clear":
490 return self
.engine
.db
.del_list(args
[1], kwargs
)
491 elif args
and args
[0] == "prune":
492 return self
.engine
.prune()
493 elif args
and args
[0] == "login":
494 if not cherrypy
.request
.headers
.get("Authorization"):
495 cherrypy
.response
.headers
["WWW-Authenticate"] = 'Basic realm="Access to OSM site", charset="UTF-8"'
496 cherrypy
.response
.status
= HTTPStatus
.UNAUTHORIZED
.value
497 elif args
and args
[0] == "login2":
498 if not cherrypy
.request
.headers
.get("Authorization"):
499 cherrypy
.response
.headers
["WWW-Authenticate"] = 'Bearer realm="Access to OSM site"'
500 cherrypy
.response
.status
= HTTPStatus
.UNAUTHORIZED
.value
501 elif args
and args
[0] == "sleep":
504 sleep_time
= int(args
[1])
506 cherrypy
.response
.status
= HTTPStatus
.FORBIDDEN
.value
507 return self
._format
_out
("Database already initialized")
508 thread_info
= cherrypy
.thread_data
510 time
.sleep(sleep_time
)
512 elif len(args
) >= 2 and args
[0] == "message":
514 return_text
= "<html><pre>{} ->\n".format(main_topic
)
516 if cherrypy
.request
.method
== 'POST':
517 to_send
= yaml
.load(cherrypy
.request
.body
)
518 for k
, v
in to_send
.items():
519 self
.engine
.msg
.write(main_topic
, k
, v
)
520 return_text
+= " {}: {}\n".format(k
, v
)
521 elif cherrypy
.request
.method
== 'GET':
522 for k
, v
in kwargs
.items():
523 self
.engine
.msg
.write(main_topic
, k
, yaml
.load(v
))
524 return_text
+= " {}: {}\n".format(k
, yaml
.load(v
))
525 except Exception as e
:
526 return_text
+= "Error: " + str(e
)
527 return_text
+= "</pre></html>\n"
531 "<html><pre>\nheaders:\n args: {}\n".format(args
) +
532 " kwargs: {}\n".format(kwargs
) +
533 " headers: {}\n".format(cherrypy
.request
.headers
) +
534 " path_info: {}\n".format(cherrypy
.request
.path_info
) +
535 " query_string: {}\n".format(cherrypy
.request
.query_string
) +
536 " session: {}\n".format(cherrypy
.session
) +
537 " cookie: {}\n".format(cherrypy
.request
.cookie
) +
538 " method: {}\n".format(cherrypy
.request
.method
) +
539 " session: {}\n".format(cherrypy
.session
.get('fieldname')) +
541 return_text
+= " length: {}\n".format(cherrypy
.request
.body
.length
)
542 if cherrypy
.request
.body
.length
:
543 return_text
+= " content: {}\n".format(
544 str(cherrypy
.request
.body
.read(int(cherrypy
.request
.headers
.get('Content-Length', 0)))))
546 return_text
+= "thread: {}\n".format(thread_info
)
547 return_text
+= "</pre></html>"
550 def _check_valid_url_method(self
, method
, *args
):
552 raise NbiException("URL must contain at least 'main_topic/version/topic'", HTTPStatus
.METHOD_NOT_ALLOWED
)
554 reference
= self
.valid_methods
558 if not isinstance(reference
, dict):
559 raise NbiException("URL contains unexpected extra items '{}'".format(arg
),
560 HTTPStatus
.METHOD_NOT_ALLOWED
)
563 reference
= reference
[arg
]
564 elif "<ID>" in reference
:
565 reference
= reference
["<ID>"]
566 elif "*" in reference
:
567 reference
= reference
["*"]
570 raise NbiException("Unexpected URL item {}".format(arg
), HTTPStatus
.METHOD_NOT_ALLOWED
)
571 if "TODO" in reference
and method
in reference
["TODO"]:
572 raise NbiException("Method {} not supported yet for this URL".format(method
), HTTPStatus
.NOT_IMPLEMENTED
)
573 elif "METHODS" in reference
and method
not in reference
["METHODS"]:
574 raise NbiException("Method {} not supported for this URL".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
578 def _set_location_header(main_topic
, version
, topic
, id):
580 Insert response header Location with the URL of created item base on URL params
587 # Use cherrypy.request.base for absoluted path and make use of request.header HOST just in case behind aNAT
588 cherrypy
.response
.headers
["Location"] = "/osm/{}/{}/{}/{}".format(main_topic
, version
, topic
, id)
592 def default(self
, main_topic
=None, version
=None, topic
=None, _id
=None, item
=None, *args
, **kwargs
):
601 if not main_topic
or not version
or not topic
:
602 raise NbiException("URL must contain at least 'main_topic/version/topic'",
603 HTTPStatus
.METHOD_NOT_ALLOWED
)
604 if main_topic
not in ("admin", "vnfpkgm", "nsd", "nslcm"):
605 raise NbiException("URL main_topic '{}' not supported".format(main_topic
),
606 HTTPStatus
.METHOD_NOT_ALLOWED
)
608 raise NbiException("URL version '{}' not supported".format(version
), HTTPStatus
.METHOD_NOT_ALLOWED
)
610 if kwargs
and "METHOD" in kwargs
and kwargs
["METHOD"] in ("PUT", "POST", "DELETE", "GET", "PATCH"):
611 method
= kwargs
.pop("METHOD")
613 method
= cherrypy
.request
.method
614 if kwargs
and "FORCE" in kwargs
:
615 force
= kwargs
.pop("FORCE")
619 self
._check
_valid
_url
_method
(method
, main_topic
, version
, topic
, _id
, item
, *args
)
621 if main_topic
== "admin" and topic
== "tokens":
622 return self
.token(method
, _id
, kwargs
)
624 # self.engine.load_dbase(cherrypy.request.app.config)
625 session
= self
.authenticator
.authorize()
626 indata
= self
._format
_in
(kwargs
)
628 if topic
== "subscriptions":
629 engine_topic
= main_topic
+ "_" + topic
633 if main_topic
== "nsd":
634 engine_topic
= "nsds"
635 elif main_topic
== "vnfpkgm":
636 engine_topic
= "vnfds"
637 elif main_topic
== "nslcm":
638 engine_topic
= "nsrs"
639 if topic
== "ns_lcm_op_occs":
640 engine_topic
= "nslcmops"
641 if topic
== "vnfrs" or topic
== "vnf_instances":
642 engine_topic
= "vnfrs"
643 elif main_topic
== "pdu":
644 engine_topic
= "pdus"
645 if engine_topic
== "vims": # TODO this is for backward compatibility, it will remove in the future
646 engine_topic
= "vim_accounts"
649 if item
in ("nsd_content", "package_content", "artifacts", "vnfd", "nsd"):
650 if item
in ("vnfd", "nsd"):
654 elif item
== "artifacts":
658 file, _format
= self
.engine
.get_file(session
, engine_topic
, _id
, path
,
659 cherrypy
.request
.headers
.get("Accept"))
662 outdata
= self
.engine
.get_item_list(session
, engine_topic
, kwargs
)
664 outdata
= self
.engine
.get_item(session
, engine_topic
, _id
)
665 elif method
== "POST":
666 if topic
in ("ns_descriptors_content", "vnf_packages_content"):
667 _id
= cherrypy
.request
.headers
.get("Transaction-Id")
669 _id
= self
.engine
.new_item(rollback
, session
, engine_topic
, {}, None, cherrypy
.request
.headers
,
671 completed
= self
.engine
.upload_content(session
, engine_topic
, _id
, indata
, kwargs
,
672 cherrypy
.request
.headers
, force
=force
)
674 self
._set
_location
_header
(main_topic
, version
, topic
, _id
)
676 cherrypy
.response
.headers
["Transaction-Id"] = _id
677 outdata
= {"id": _id
}
678 elif topic
== "ns_instances_content":
680 _id
= self
.engine
.new_item(rollback
, session
, engine_topic
, indata
, kwargs
, force
=force
)
682 indata
["lcmOperationType"] = "instantiate"
683 indata
["nsInstanceId"] = _id
684 self
.engine
.new_item(rollback
, session
, "nslcmops", indata
, None)
685 self
._set
_location
_header
(main_topic
, version
, topic
, _id
)
686 outdata
= {"id": _id
}
687 elif topic
== "ns_instances" and item
:
688 indata
["lcmOperationType"] = item
689 indata
["nsInstanceId"] = _id
690 _id
= self
.engine
.new_item(rollback
, session
, "nslcmops", indata
, kwargs
)
691 self
._set
_location
_header
(main_topic
, version
, "ns_lcm_op_occs", _id
)
692 outdata
= {"id": _id
}
693 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
695 _id
= self
.engine
.new_item(rollback
, session
, engine_topic
, indata
, kwargs
,
696 cherrypy
.request
.headers
, force
=force
)
697 self
._set
_location
_header
(main_topic
, version
, topic
, _id
)
698 outdata
= {"id": _id
}
699 # TODO form NsdInfo when topic in ("ns_descriptors", "vnf_packages")
700 cherrypy
.response
.status
= HTTPStatus
.CREATED
.value
702 elif method
== "DELETE":
704 outdata
= self
.engine
.del_item_list(session
, engine_topic
, kwargs
)
705 cherrypy
.response
.status
= HTTPStatus
.OK
.value
706 else: # len(args) > 1
707 if topic
== "ns_instances_content" and not force
:
709 "lcmOperationType": "terminate",
713 opp_id
= self
.engine
.new_item(rollback
, session
, "nslcmops", nslcmop_desc
, None)
714 outdata
= {"_id": opp_id
}
715 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
717 self
.engine
.del_item(session
, engine_topic
, _id
, force
)
718 cherrypy
.response
.status
= HTTPStatus
.NO_CONTENT
.value
719 if engine_topic
in ("vim_accounts", "sdns"):
720 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
722 elif method
in ("PUT", "PATCH"):
724 if not indata
and not kwargs
:
725 raise NbiException("Nothing to update. Provide payload and/or query string",
726 HTTPStatus
.BAD_REQUEST
)
727 if item
in ("nsd_content", "package_content") and method
== "PUT":
728 completed
= self
.engine
.upload_content(session
, engine_topic
, _id
, indata
, kwargs
,
729 cherrypy
.request
.headers
, force
=force
)
731 cherrypy
.response
.headers
["Transaction-Id"] = id
733 self
.engine
.edit_item(session
, engine_topic
, _id
, indata
, kwargs
, force
=force
)
734 cherrypy
.response
.status
= HTTPStatus
.NO_CONTENT
.value
736 raise NbiException("Method {} not allowed".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
737 return self
._format
_out
(outdata
, session
, _format
)
738 except Exception as e
:
739 if isinstance(e
, (NbiException
, EngineException
, DbException
, FsException
, MsgException
, AuthException
)):
740 http_code_value
= cherrypy
.response
.status
= e
.http_code
.value
741 http_code_name
= e
.http_code
.name
742 cherrypy
.log("Exception {}".format(e
))
744 http_code_value
= cherrypy
.response
.status
= HTTPStatus
.BAD_REQUEST
.value
# INTERNAL_SERVER_ERROR
745 cherrypy
.log("CRITICAL: Exception {}".format(e
))
746 http_code_name
= HTTPStatus
.BAD_REQUEST
.name
747 if hasattr(outdata
, "close"): # is an open file
751 for rollback_item
in rollback
:
753 if rollback_item
.get("operation") == "set":
754 self
.engine
.db
.set_one(rollback_item
["topic"], {"_id": rollback_item
["_id"]},
755 rollback_item
["content"], fail_on_empty
=False)
757 self
.engine
.del_item(**rollback_item
, session
=session
, force
=True)
758 except Exception as e2
:
759 rollback_error_text
= "Rollback Exception {}: {}".format(rollback_item
, e2
)
760 cherrypy
.log(rollback_error_text
)
761 error_text
+= ". " + rollback_error_text
762 # if isinstance(e, MsgException):
763 # error_text = "{} has been '{}' but other modules cannot be informed because an error on bus".format(
764 # engine_topic[:-1], method, error_text)
766 "code": http_code_name
,
767 "status": http_code_value
,
768 "detail": error_text
,
770 return self
._format
_out
(problem_details
, session
)
771 # raise cherrypy.HTTPError(e.http_code.value, str(e))
774 # def validate_password(realm, username, password):
775 # cherrypy.log("realm "+ str(realm))
776 # if username == "admin" and password == "admin":
781 def _start_service():
783 Callback function called when cherrypy.engine starts
784 Override configuration with env variables
785 Set database, storage, message configuration
786 Init database with admin/admin user password
788 cherrypy
.log
.error("Starting osm_nbi")
789 # update general cherrypy configuration
792 engine_config
= cherrypy
.tree
.apps
['/osm'].config
793 for k
, v
in environ
.items():
794 if not k
.startswith("OSMNBI_"):
796 k1
, _
, k2
= k
[7:].lower().partition("_")
800 # update static configuration
801 if k
== 'OSMNBI_STATIC_DIR':
802 engine_config
["/static"]['tools.staticdir.dir'] = v
803 engine_config
["/static"]['tools.staticdir.on'] = True
804 elif k
== 'OSMNBI_SOCKET_PORT' or k
== 'OSMNBI_SERVER_PORT':
805 update_dict
['server.socket_port'] = int(v
)
806 elif k
== 'OSMNBI_SOCKET_HOST' or k
== 'OSMNBI_SERVER_HOST':
807 update_dict
['server.socket_host'] = v
808 elif k1
in ("server", "test", "auth", "log"):
809 update_dict
[k1
+ '.' + k2
] = v
810 elif k1
in ("message", "database", "storage", "authentication"):
811 # k2 = k2.replace('_', '.')
812 if k2
in ("port", "db_port"):
813 engine_config
[k1
][k2
] = int(v
)
815 engine_config
[k1
][k2
] = v
817 except ValueError as e
:
818 cherrypy
.log
.error("Ignoring environ '{}': " + str(e
))
819 except Exception as e
:
820 cherrypy
.log
.warn("skipping environ '{}' on exception '{}'".format(k
, e
))
823 cherrypy
.config
.update(update_dict
)
824 engine_config
["global"].update(update_dict
)
827 log_format_simple
= "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(message)s"
828 log_formatter_simple
= logging
.Formatter(log_format_simple
, datefmt
='%Y-%m-%dT%H:%M:%S')
829 logger_server
= logging
.getLogger("cherrypy.error")
830 logger_access
= logging
.getLogger("cherrypy.access")
831 logger_cherry
= logging
.getLogger("cherrypy")
832 logger_nbi
= logging
.getLogger("nbi")
834 if "log.file" in engine_config
["global"]:
835 file_handler
= logging
.handlers
.RotatingFileHandler(engine_config
["global"]["log.file"],
836 maxBytes
=100e6
, backupCount
=9, delay
=0)
837 file_handler
.setFormatter(log_formatter_simple
)
838 logger_cherry
.addHandler(file_handler
)
839 logger_nbi
.addHandler(file_handler
)
840 # log always to standard output
841 for format_
, logger
in {"nbi.server %(filename)s:%(lineno)s": logger_server
,
842 "nbi.access %(filename)s:%(lineno)s": logger_access
,
843 "%(name)s %(filename)s:%(lineno)s": logger_nbi
845 log_format_cherry
= "%(asctime)s %(levelname)s {} %(message)s".format(format_
)
846 log_formatter_cherry
= logging
.Formatter(log_format_cherry
, datefmt
='%Y-%m-%dT%H:%M:%S')
847 str_handler
= logging
.StreamHandler()
848 str_handler
.setFormatter(log_formatter_cherry
)
849 logger
.addHandler(str_handler
)
851 if engine_config
["global"].get("log.level"):
852 logger_cherry
.setLevel(engine_config
["global"]["log.level"])
853 logger_nbi
.setLevel(engine_config
["global"]["log.level"])
855 # logging other modules
856 for k1
, logname
in {"message": "nbi.msg", "database": "nbi.db", "storage": "nbi.fs"}.items():
857 engine_config
[k1
]["logger_name"] = logname
858 logger_module
= logging
.getLogger(logname
)
859 if "logfile" in engine_config
[k1
]:
860 file_handler
= logging
.handlers
.RotatingFileHandler(engine_config
[k1
]["logfile"],
861 maxBytes
=100e6
, backupCount
=9, delay
=0)
862 file_handler
.setFormatter(log_formatter_simple
)
863 logger_module
.addHandler(file_handler
)
864 if "loglevel" in engine_config
[k1
]:
865 logger_module
.setLevel(engine_config
[k1
]["loglevel"])
866 # TODO add more entries, e.g.: storage
867 cherrypy
.tree
.apps
['/osm'].root
.engine
.start(engine_config
)
868 cherrypy
.tree
.apps
['/osm'].root
.authenticator
.start(engine_config
)
869 cherrypy
.tree
.apps
['/osm'].root
.engine
.init_db(target_version
=database_version
)
870 cherrypy
.tree
.apps
['/osm'].root
.authenticator
.init_db(target_version
=auth_database_version
)
871 # getenv('OSMOPENMANO_TENANT', None)
876 Callback function called when cherrypy.engine stops
877 TODO: Ending database connections.
879 cherrypy
.tree
.apps
['/osm'].root
.engine
.stop()
880 cherrypy
.log
.error("Stopping osm_nbi")
883 def nbi(config_file
):
886 # #'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
887 # 'tools.sessions.on': True,
888 # 'tools.response_headers.on': True,
889 # # 'tools.response_headers.headers': [('Content-Type', 'text/plain')],
892 # cherrypy.Server.ssl_module = 'builtin'
893 # cherrypy.Server.ssl_certificate = "http/cert.pem"
894 # cherrypy.Server.ssl_private_key = "http/privkey.pem"
895 # cherrypy.Server.thread_pool = 10
896 # cherrypy.config.update({'Server.socket_port': config["port"], 'Server.socket_host': config["host"]})
898 # cherrypy.config.update({'tools.auth_basic.on': True,
899 # 'tools.auth_basic.realm': 'localhost',
900 # 'tools.auth_basic.checkpassword': validate_password})
901 cherrypy
.engine
.subscribe('start', _start_service
)
902 cherrypy
.engine
.subscribe('stop', _stop_service
)
903 cherrypy
.quickstart(Server(), '/osm', config_file
)
907 print("""Usage: {} [options]
908 -c|--config [configuration_file]: loads the configuration file (default: ./nbi.cfg)
909 -h|--help: shows this help
910 """.format(sys
.argv
[0]))
911 # --log-socket-host HOST: send logs to this host")
912 # --log-socket-port PORT: send logs using this port (default: 9022)")
915 if __name__
== '__main__':
917 # load parameters and configuration
918 opts
, args
= getopt
.getopt(sys
.argv
[1:], "hvc:", ["config=", "help"])
919 # TODO add "log-socket-host=", "log-socket-port=", "log-file="
922 if o
in ("-h", "--help"):
925 elif o
in ("-c", "--config"):
927 # elif o == "--log-socket-port":
928 # log_socket_port = a
929 # elif o == "--log-socket-host":
930 # log_socket_host = a
931 # elif o == "--log-file":
934 assert False, "Unhandled option"
936 if not path
.isfile(config_file
):
937 print("configuration file '{}' that not exist".format(config_file
), file=sys
.stderr
)
940 for config_file
in (__file__
[:__file__
.rfind(".")] + ".cfg", "./nbi.cfg", "/etc/osm/nbi.cfg"):
941 if path
.isfile(config_file
):
944 print("No configuration file 'nbi.cfg' found neither at local folder nor at /etc/osm/", file=sys
.stderr
)
947 except getopt
.GetoptError
as e
:
948 print(str(e
), file=sys
.stderr
)