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 elif "application/yaml" in cherrypy
.request
.headers
["Content-Type"]:
263 error_text
= "Invalid yaml format "
264 indata
= yaml
.load(cherrypy
.request
.body
)
265 elif "application/binary" in cherrypy
.request
.headers
["Content-Type"] or \
266 "application/gzip" in cherrypy
.request
.headers
["Content-Type"] or \
267 "application/zip" in cherrypy
.request
.headers
["Content-Type"] or \
268 "text/plain" in cherrypy
.request
.headers
["Content-Type"]:
269 indata
= cherrypy
.request
.body
# .read()
270 elif "multipart/form-data" in cherrypy
.request
.headers
["Content-Type"]:
271 if "descriptor_file" in kwargs
:
272 filecontent
= kwargs
.pop("descriptor_file")
273 if not filecontent
.file:
274 raise NbiException("empty file or content", HTTPStatus
.BAD_REQUEST
)
275 indata
= filecontent
.file # .read()
276 if filecontent
.content_type
.value
:
277 cherrypy
.request
.headers
["Content-Type"] = filecontent
.content_type
.value
279 # raise cherrypy.HTTPError(HTTPStatus.Not_Acceptable,
280 # "Only 'Content-Type' of type 'application/json' or
281 # 'application/yaml' for input format are available")
282 error_text
= "Invalid yaml format "
283 indata
= yaml
.load(cherrypy
.request
.body
)
285 error_text
= "Invalid yaml format "
286 indata
= yaml
.load(cherrypy
.request
.body
)
291 if cherrypy
.request
.headers
.get("Query-String-Format") == "yaml":
294 for k
, v
in kwargs
.items():
295 if isinstance(v
, str):
300 kwargs
[k
] = yaml
.load(v
)
303 elif k
.endswith(".gt") or k
.endswith(".lt") or k
.endswith(".gte") or k
.endswith(".lte"):
311 elif v
.find(",") > 0:
312 kwargs
[k
] = v
.split(",")
313 elif isinstance(v
, (list, tuple)):
314 for index
in range(0, len(v
)):
319 v
[index
] = yaml
.load(v
[index
])
324 except (ValueError, yaml
.YAMLError
) as exc
:
325 raise NbiException(error_text
+ str(exc
), HTTPStatus
.BAD_REQUEST
)
326 except KeyError as exc
:
327 raise NbiException("Query string error: " + str(exc
), HTTPStatus
.BAD_REQUEST
)
328 except Exception as exc
:
329 raise NbiException(error_text
+ str(exc
), HTTPStatus
.BAD_REQUEST
)
332 def _format_out(data
, session
=None, _format
=None):
334 return string of dictionary data according to requested json, yaml, xml. By default json
335 :param data: response to be sent. Can be a dict, text or file
337 :param _format: The format to be set as Content-Type ir data is a file
340 accept
= cherrypy
.request
.headers
.get("Accept")
342 if accept
and "text/html" in accept
:
343 return html
.format(data
, cherrypy
.request
, cherrypy
.response
, session
)
344 # cherrypy.response.status = HTTPStatus.NO_CONTENT.value
346 elif hasattr(data
, "read"): # file object
348 cherrypy
.response
.headers
["Content-Type"] = _format
349 elif "b" in data
.mode
: # binariy asssumig zip
350 cherrypy
.response
.headers
["Content-Type"] = 'application/zip'
352 cherrypy
.response
.headers
["Content-Type"] = 'text/plain'
353 # TODO check that cherrypy close file. If not implement pending things to close per thread next
356 if "application/json" in accept
:
357 cherrypy
.response
.headers
["Content-Type"] = 'application/json; charset=utf-8'
358 a
= json
.dumps(data
, indent
=4) + "\n"
359 return a
.encode("utf8")
360 elif "text/html" in accept
:
361 return html
.format(data
, cherrypy
.request
, cherrypy
.response
, session
)
363 elif "application/yaml" in accept
or "*/*" in accept
or "text/plain" in accept
:
366 raise cherrypy
.HTTPError(HTTPStatus
.NOT_ACCEPTABLE
.value
,
367 "Only 'Accept' of type 'application/json' or 'application/yaml' "
368 "for output format are available")
369 cherrypy
.response
.headers
["Content-Type"] = 'application/yaml'
370 return yaml
.safe_dump(data
, explicit_start
=True, indent
=4, default_flow_style
=False, tags
=False,
371 encoding
='utf-8', allow_unicode
=True) # , canonical=True, default_style='"'
374 def index(self
, *args
, **kwargs
):
377 if cherrypy
.request
.method
== "GET":
378 session
= self
.authenticator
.authorize()
379 outdata
= "Index page"
381 raise cherrypy
.HTTPError(HTTPStatus
.METHOD_NOT_ALLOWED
.value
,
382 "Method {} not allowed for tokens".format(cherrypy
.request
.method
))
384 return self
._format
_out
(outdata
, session
)
386 except (EngineException
, AuthException
) as e
:
387 cherrypy
.log("index Exception {}".format(e
))
388 cherrypy
.response
.status
= e
.http_code
.value
389 return self
._format
_out
("Welcome to OSM!", session
)
392 def version(self
, *args
, **kwargs
):
393 # TODO consider to remove and provide version using the static version file
394 global __version__
, version_date
396 if cherrypy
.request
.method
!= "GET":
397 raise NbiException("Only method GET is allowed", HTTPStatus
.METHOD_NOT_ALLOWED
)
399 raise NbiException("Invalid URL or query string for version", HTTPStatus
.METHOD_NOT_ALLOWED
)
400 return __version__
+ " " + version_date
401 except NbiException
as e
:
402 cherrypy
.response
.status
= e
.http_code
.value
404 "code": e
.http_code
.name
,
405 "status": e
.http_code
.value
,
408 return self
._format
_out
(problem_details
, None)
411 def token(self
, method
, token_id
=None, kwargs
=None):
413 # self.engine.load_dbase(cherrypy.request.app.config)
414 indata
= self
._format
_in
(kwargs
)
415 if not isinstance(indata
, dict):
416 raise NbiException("Expected application/yaml or application/json Content-Type", HTTPStatus
.BAD_REQUEST
)
419 session
= self
.authenticator
.authorize()
421 outdata
= self
.authenticator
.get_token(session
, token_id
)
423 outdata
= self
.authenticator
.get_token_list(session
)
424 elif method
== "POST":
426 session
= self
.authenticator
.authorize()
430 indata
.update(kwargs
)
431 outdata
= self
.authenticator
.new_token(session
, indata
, cherrypy
.request
.remote
)
433 cherrypy
.session
['Authorization'] = outdata
["_id"]
434 self
._set
_location
_header
("admin", "v1", "tokens", outdata
["_id"])
435 # cherrypy.response.cookie["Authorization"] = outdata["id"]
436 # cherrypy.response.cookie["Authorization"]['expires'] = 3600
437 elif method
== "DELETE":
438 if not token_id
and "id" in kwargs
:
439 token_id
= kwargs
["id"]
441 session
= self
.authenticator
.authorize()
442 token_id
= session
["_id"]
443 outdata
= self
.authenticator
.del_token(token_id
)
445 cherrypy
.session
['Authorization'] = "logout"
446 # cherrypy.response.cookie["Authorization"] = token_id
447 # cherrypy.response.cookie["Authorization"]['expires'] = 0
449 raise NbiException("Method {} not allowed for token".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
450 return self
._format
_out
(outdata
, session
)
451 except (NbiException
, EngineException
, DbException
, AuthException
) as e
:
452 cherrypy
.log("tokens Exception {}".format(e
))
453 cherrypy
.response
.status
= e
.http_code
.value
455 "code": e
.http_code
.name
,
456 "status": e
.http_code
.value
,
459 return self
._format
_out
(problem_details
, session
)
462 def test(self
, *args
, **kwargs
):
464 if args
and args
[0] == "help":
465 return "<html><pre>\ninit\nfile/<name> download file\ndb-clear/table\nprune\nlogin\nlogin2\n"\
466 "sleep/<time>\nmessage/topic\n</pre></html>"
468 elif args
and args
[0] == "init":
470 # self.engine.load_dbase(cherrypy.request.app.config)
471 self
.engine
.create_admin()
472 return "Done. User 'admin', password 'admin' created"
474 cherrypy
.response
.status
= HTTPStatus
.FORBIDDEN
.value
475 return self
._format
_out
("Database already initialized")
476 elif args
and args
[0] == "file":
477 return cherrypy
.lib
.static
.serve_file(cherrypy
.tree
.apps
['/osm'].config
["storage"]["path"] + "/" + args
[1],
478 "text/plain", "attachment")
479 elif args
and args
[0] == "file2":
480 f_path
= cherrypy
.tree
.apps
['/osm'].config
["storage"]["path"] + "/" + args
[1]
481 f
= open(f_path
, "r")
482 cherrypy
.response
.headers
["Content-type"] = "text/plain"
485 elif len(args
) == 2 and args
[0] == "db-clear":
486 return self
.engine
.db
.del_list(args
[1], kwargs
)
487 elif args
and args
[0] == "prune":
488 return self
.engine
.prune()
489 elif args
and args
[0] == "login":
490 if not cherrypy
.request
.headers
.get("Authorization"):
491 cherrypy
.response
.headers
["WWW-Authenticate"] = 'Basic realm="Access to OSM site", charset="UTF-8"'
492 cherrypy
.response
.status
= HTTPStatus
.UNAUTHORIZED
.value
493 elif args
and args
[0] == "login2":
494 if not cherrypy
.request
.headers
.get("Authorization"):
495 cherrypy
.response
.headers
["WWW-Authenticate"] = 'Bearer realm="Access to OSM site"'
496 cherrypy
.response
.status
= HTTPStatus
.UNAUTHORIZED
.value
497 elif args
and args
[0] == "sleep":
500 sleep_time
= int(args
[1])
502 cherrypy
.response
.status
= HTTPStatus
.FORBIDDEN
.value
503 return self
._format
_out
("Database already initialized")
504 thread_info
= cherrypy
.thread_data
506 time
.sleep(sleep_time
)
508 elif len(args
) >= 2 and args
[0] == "message":
510 return_text
= "<html><pre>{} ->\n".format(main_topic
)
512 if cherrypy
.request
.method
== 'POST':
513 to_send
= yaml
.load(cherrypy
.request
.body
)
514 for k
, v
in to_send
.items():
515 self
.engine
.msg
.write(main_topic
, k
, v
)
516 return_text
+= " {}: {}\n".format(k
, v
)
517 elif cherrypy
.request
.method
== 'GET':
518 for k
, v
in kwargs
.items():
519 self
.engine
.msg
.write(main_topic
, k
, yaml
.load(v
))
520 return_text
+= " {}: {}\n".format(k
, yaml
.load(v
))
521 except Exception as e
:
522 return_text
+= "Error: " + str(e
)
523 return_text
+= "</pre></html>\n"
527 "<html><pre>\nheaders:\n args: {}\n".format(args
) +
528 " kwargs: {}\n".format(kwargs
) +
529 " headers: {}\n".format(cherrypy
.request
.headers
) +
530 " path_info: {}\n".format(cherrypy
.request
.path_info
) +
531 " query_string: {}\n".format(cherrypy
.request
.query_string
) +
532 " session: {}\n".format(cherrypy
.session
) +
533 " cookie: {}\n".format(cherrypy
.request
.cookie
) +
534 " method: {}\n".format(cherrypy
.request
.method
) +
535 " session: {}\n".format(cherrypy
.session
.get('fieldname')) +
537 return_text
+= " length: {}\n".format(cherrypy
.request
.body
.length
)
538 if cherrypy
.request
.body
.length
:
539 return_text
+= " content: {}\n".format(
540 str(cherrypy
.request
.body
.read(int(cherrypy
.request
.headers
.get('Content-Length', 0)))))
542 return_text
+= "thread: {}\n".format(thread_info
)
543 return_text
+= "</pre></html>"
546 def _check_valid_url_method(self
, method
, *args
):
548 raise NbiException("URL must contain at least 'main_topic/version/topic'", HTTPStatus
.METHOD_NOT_ALLOWED
)
550 reference
= self
.valid_methods
554 if not isinstance(reference
, dict):
555 raise NbiException("URL contains unexpected extra items '{}'".format(arg
),
556 HTTPStatus
.METHOD_NOT_ALLOWED
)
559 reference
= reference
[arg
]
560 elif "<ID>" in reference
:
561 reference
= reference
["<ID>"]
562 elif "*" in reference
:
563 reference
= reference
["*"]
566 raise NbiException("Unexpected URL item {}".format(arg
), HTTPStatus
.METHOD_NOT_ALLOWED
)
567 if "TODO" in reference
and method
in reference
["TODO"]:
568 raise NbiException("Method {} not supported yet for this URL".format(method
), HTTPStatus
.NOT_IMPLEMENTED
)
569 elif "METHODS" in reference
and method
not in reference
["METHODS"]:
570 raise NbiException("Method {} not supported for this URL".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
574 def _set_location_header(main_topic
, version
, topic
, id):
576 Insert response header Location with the URL of created item base on URL params
583 # Use cherrypy.request.base for absoluted path and make use of request.header HOST just in case behind aNAT
584 cherrypy
.response
.headers
["Location"] = "/osm/{}/{}/{}/{}".format(main_topic
, version
, topic
, id)
588 def default(self
, main_topic
=None, version
=None, topic
=None, _id
=None, item
=None, *args
, **kwargs
):
597 if not main_topic
or not version
or not topic
:
598 raise NbiException("URL must contain at least 'main_topic/version/topic'",
599 HTTPStatus
.METHOD_NOT_ALLOWED
)
600 if main_topic
not in ("admin", "vnfpkgm", "nsd", "nslcm"):
601 raise NbiException("URL main_topic '{}' not supported".format(main_topic
),
602 HTTPStatus
.METHOD_NOT_ALLOWED
)
604 raise NbiException("URL version '{}' not supported".format(version
), HTTPStatus
.METHOD_NOT_ALLOWED
)
606 if kwargs
and "METHOD" in kwargs
and kwargs
["METHOD"] in ("PUT", "POST", "DELETE", "GET", "PATCH"):
607 method
= kwargs
.pop("METHOD")
609 method
= cherrypy
.request
.method
610 if kwargs
and "FORCE" in kwargs
:
611 force
= kwargs
.pop("FORCE")
615 self
._check
_valid
_url
_method
(method
, main_topic
, version
, topic
, _id
, item
, *args
)
617 if main_topic
== "admin" and topic
== "tokens":
618 return self
.token(method
, _id
, kwargs
)
620 # self.engine.load_dbase(cherrypy.request.app.config)
621 session
= self
.authenticator
.authorize()
622 indata
= self
._format
_in
(kwargs
)
624 if topic
== "subscriptions":
625 engine_topic
= main_topic
+ "_" + topic
629 if main_topic
== "nsd":
630 engine_topic
= "nsds"
631 elif main_topic
== "vnfpkgm":
632 engine_topic
= "vnfds"
633 elif main_topic
== "nslcm":
634 engine_topic
= "nsrs"
635 if topic
== "ns_lcm_op_occs":
636 engine_topic
= "nslcmops"
637 if topic
== "vnfrs" or topic
== "vnf_instances":
638 engine_topic
= "vnfrs"
639 elif main_topic
== "pdu":
640 engine_topic
= "pdus"
641 if engine_topic
== "vims": # TODO this is for backward compatibility, it will remove in the future
642 engine_topic
= "vim_accounts"
645 if item
in ("nsd_content", "package_content", "artifacts", "vnfd", "nsd"):
646 if item
in ("vnfd", "nsd"):
650 elif item
== "artifacts":
654 file, _format
= self
.engine
.get_file(session
, engine_topic
, _id
, path
,
655 cherrypy
.request
.headers
.get("Accept"))
658 outdata
= self
.engine
.get_item_list(session
, engine_topic
, kwargs
)
660 outdata
= self
.engine
.get_item(session
, engine_topic
, _id
)
661 elif method
== "POST":
662 if topic
in ("ns_descriptors_content", "vnf_packages_content"):
663 _id
= cherrypy
.request
.headers
.get("Transaction-Id")
665 _id
= self
.engine
.new_item(rollback
, session
, engine_topic
, {}, None, cherrypy
.request
.headers
,
667 completed
= self
.engine
.upload_content(session
, engine_topic
, _id
, indata
, kwargs
,
668 cherrypy
.request
.headers
, force
=force
)
670 self
._set
_location
_header
(main_topic
, version
, topic
, _id
)
672 cherrypy
.response
.headers
["Transaction-Id"] = _id
673 outdata
= {"id": _id
}
674 elif topic
== "ns_instances_content":
676 _id
= self
.engine
.new_item(rollback
, session
, engine_topic
, indata
, kwargs
, force
=force
)
678 indata
["lcmOperationType"] = "instantiate"
679 indata
["nsInstanceId"] = _id
680 self
.engine
.new_item(rollback
, session
, "nslcmops", indata
, None)
681 self
._set
_location
_header
(main_topic
, version
, topic
, _id
)
682 outdata
= {"id": _id
}
683 elif topic
== "ns_instances" and item
:
684 indata
["lcmOperationType"] = item
685 indata
["nsInstanceId"] = _id
686 _id
= self
.engine
.new_item(rollback
, session
, "nslcmops", indata
, kwargs
)
687 self
._set
_location
_header
(main_topic
, version
, "ns_lcm_op_occs", _id
)
688 outdata
= {"id": _id
}
689 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
691 _id
= self
.engine
.new_item(rollback
, session
, engine_topic
, indata
, kwargs
,
692 cherrypy
.request
.headers
, force
=force
)
693 self
._set
_location
_header
(main_topic
, version
, topic
, _id
)
694 outdata
= {"id": _id
}
695 # TODO form NsdInfo when topic in ("ns_descriptors", "vnf_packages")
696 cherrypy
.response
.status
= HTTPStatus
.CREATED
.value
698 elif method
== "DELETE":
700 outdata
= self
.engine
.del_item_list(session
, engine_topic
, kwargs
)
701 cherrypy
.response
.status
= HTTPStatus
.OK
.value
702 else: # len(args) > 1
703 if topic
== "ns_instances_content" and not force
:
705 "lcmOperationType": "terminate",
709 opp_id
= self
.engine
.new_item(rollback
, session
, "nslcmops", nslcmop_desc
, None)
710 outdata
= {"_id": opp_id
}
711 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
713 self
.engine
.del_item(session
, engine_topic
, _id
, force
)
714 cherrypy
.response
.status
= HTTPStatus
.NO_CONTENT
.value
715 if engine_topic
in ("vim_accounts", "sdns"):
716 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
718 elif method
in ("PUT", "PATCH"):
720 if not indata
and not kwargs
:
721 raise NbiException("Nothing to update. Provide payload and/or query string",
722 HTTPStatus
.BAD_REQUEST
)
723 if item
in ("nsd_content", "package_content") and method
== "PUT":
724 completed
= self
.engine
.upload_content(session
, engine_topic
, _id
, indata
, kwargs
,
725 cherrypy
.request
.headers
, force
=force
)
727 cherrypy
.response
.headers
["Transaction-Id"] = id
729 self
.engine
.edit_item(session
, engine_topic
, _id
, indata
, kwargs
, force
=force
)
730 cherrypy
.response
.status
= HTTPStatus
.NO_CONTENT
.value
732 raise NbiException("Method {} not allowed".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
733 return self
._format
_out
(outdata
, session
, _format
)
734 except Exception as e
:
735 if isinstance(e
, (NbiException
, EngineException
, DbException
, FsException
, MsgException
, AuthException
)):
736 http_code_value
= cherrypy
.response
.status
= e
.http_code
.value
737 http_code_name
= e
.http_code
.name
738 cherrypy
.log("Exception {}".format(e
))
740 http_code_value
= cherrypy
.response
.status
= HTTPStatus
.BAD_REQUEST
.value
# INTERNAL_SERVER_ERROR
741 cherrypy
.log("CRITICAL: Exception {}".format(e
))
742 http_code_name
= HTTPStatus
.BAD_REQUEST
.name
743 if hasattr(outdata
, "close"): # is an open file
747 for rollback_item
in rollback
:
749 self
.engine
.del_item(**rollback_item
, session
=session
, force
=True)
750 except Exception as e2
:
751 rollback_error_text
= "Rollback Exception {}: {}".format(rollback_item
, e2
)
752 cherrypy
.log(rollback_error_text
)
753 error_text
+= ". " + rollback_error_text
754 # if isinstance(e, MsgException):
755 # error_text = "{} has been '{}' but other modules cannot be informed because an error on bus".format(
756 # engine_topic[:-1], method, error_text)
758 "code": http_code_name
,
759 "status": http_code_value
,
760 "detail": error_text
,
762 return self
._format
_out
(problem_details
, session
)
763 # raise cherrypy.HTTPError(e.http_code.value, str(e))
766 # def validate_password(realm, username, password):
767 # cherrypy.log("realm "+ str(realm))
768 # if username == "admin" and password == "admin":
773 def _start_service():
775 Callback function called when cherrypy.engine starts
776 Override configuration with env variables
777 Set database, storage, message configuration
778 Init database with admin/admin user password
780 cherrypy
.log
.error("Starting osm_nbi")
781 # update general cherrypy configuration
784 engine_config
= cherrypy
.tree
.apps
['/osm'].config
785 for k
, v
in environ
.items():
786 if not k
.startswith("OSMNBI_"):
788 k1
, _
, k2
= k
[7:].lower().partition("_")
792 # update static configuration
793 if k
== 'OSMNBI_STATIC_DIR':
794 engine_config
["/static"]['tools.staticdir.dir'] = v
795 engine_config
["/static"]['tools.staticdir.on'] = True
796 elif k
== 'OSMNBI_SOCKET_PORT' or k
== 'OSMNBI_SERVER_PORT':
797 update_dict
['server.socket_port'] = int(v
)
798 elif k
== 'OSMNBI_SOCKET_HOST' or k
== 'OSMNBI_SERVER_HOST':
799 update_dict
['server.socket_host'] = v
800 elif k1
in ("server", "test", "auth", "log"):
801 update_dict
[k1
+ '.' + k2
] = v
802 elif k1
in ("message", "database", "storage", "authentication"):
803 # k2 = k2.replace('_', '.')
804 if k2
in ("port", "db_port"):
805 engine_config
[k1
][k2
] = int(v
)
807 engine_config
[k1
][k2
] = v
809 except ValueError as e
:
810 cherrypy
.log
.error("Ignoring environ '{}': " + str(e
))
811 except Exception as e
:
812 cherrypy
.log
.warn("skipping environ '{}' on exception '{}'".format(k
, e
))
815 cherrypy
.config
.update(update_dict
)
816 engine_config
["global"].update(update_dict
)
819 log_format_simple
= "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(message)s"
820 log_formatter_simple
= logging
.Formatter(log_format_simple
, datefmt
='%Y-%m-%dT%H:%M:%S')
821 logger_server
= logging
.getLogger("cherrypy.error")
822 logger_access
= logging
.getLogger("cherrypy.access")
823 logger_cherry
= logging
.getLogger("cherrypy")
824 logger_nbi
= logging
.getLogger("nbi")
826 if "log.file" in engine_config
["global"]:
827 file_handler
= logging
.handlers
.RotatingFileHandler(engine_config
["global"]["log.file"],
828 maxBytes
=100e6
, backupCount
=9, delay
=0)
829 file_handler
.setFormatter(log_formatter_simple
)
830 logger_cherry
.addHandler(file_handler
)
831 logger_nbi
.addHandler(file_handler
)
832 # log always to standard output
833 for format_
, logger
in {"nbi.server %(filename)s:%(lineno)s": logger_server
,
834 "nbi.access %(filename)s:%(lineno)s": logger_access
,
835 "%(name)s %(filename)s:%(lineno)s": logger_nbi
837 log_format_cherry
= "%(asctime)s %(levelname)s {} %(message)s".format(format_
)
838 log_formatter_cherry
= logging
.Formatter(log_format_cherry
, datefmt
='%Y-%m-%dT%H:%M:%S')
839 str_handler
= logging
.StreamHandler()
840 str_handler
.setFormatter(log_formatter_cherry
)
841 logger
.addHandler(str_handler
)
843 if engine_config
["global"].get("log.level"):
844 logger_cherry
.setLevel(engine_config
["global"]["log.level"])
845 logger_nbi
.setLevel(engine_config
["global"]["log.level"])
847 # logging other modules
848 for k1
, logname
in {"message": "nbi.msg", "database": "nbi.db", "storage": "nbi.fs"}.items():
849 engine_config
[k1
]["logger_name"] = logname
850 logger_module
= logging
.getLogger(logname
)
851 if "logfile" in engine_config
[k1
]:
852 file_handler
= logging
.handlers
.RotatingFileHandler(engine_config
[k1
]["logfile"],
853 maxBytes
=100e6
, backupCount
=9, delay
=0)
854 file_handler
.setFormatter(log_formatter_simple
)
855 logger_module
.addHandler(file_handler
)
856 if "loglevel" in engine_config
[k1
]:
857 logger_module
.setLevel(engine_config
[k1
]["loglevel"])
858 # TODO add more entries, e.g.: storage
859 cherrypy
.tree
.apps
['/osm'].root
.engine
.start(engine_config
)
860 cherrypy
.tree
.apps
['/osm'].root
.authenticator
.start(engine_config
)
862 cherrypy
.tree
.apps
['/osm'].root
.engine
.init_db(target_version
=database_version
)
863 cherrypy
.tree
.apps
['/osm'].root
.authenticator
.init_db(target_version
=auth_database_version
)
864 except (EngineException
, AuthException
):
866 # getenv('OSMOPENMANO_TENANT', None)
871 Callback function called when cherrypy.engine stops
872 TODO: Ending database connections.
874 cherrypy
.tree
.apps
['/osm'].root
.engine
.stop()
875 cherrypy
.log
.error("Stopping osm_nbi")
878 def nbi(config_file
):
881 # #'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
882 # 'tools.sessions.on': True,
883 # 'tools.response_headers.on': True,
884 # # 'tools.response_headers.headers': [('Content-Type', 'text/plain')],
887 # cherrypy.Server.ssl_module = 'builtin'
888 # cherrypy.Server.ssl_certificate = "http/cert.pem"
889 # cherrypy.Server.ssl_private_key = "http/privkey.pem"
890 # cherrypy.Server.thread_pool = 10
891 # cherrypy.config.update({'Server.socket_port': config["port"], 'Server.socket_host': config["host"]})
893 # cherrypy.config.update({'tools.auth_basic.on': True,
894 # 'tools.auth_basic.realm': 'localhost',
895 # 'tools.auth_basic.checkpassword': validate_password})
896 cherrypy
.engine
.subscribe('start', _start_service
)
897 cherrypy
.engine
.subscribe('stop', _stop_service
)
898 cherrypy
.quickstart(Server(), '/osm', config_file
)
902 print("""Usage: {} [options]
903 -c|--config [configuration_file]: loads the configuration file (default: ./nbi.cfg)
904 -h|--help: shows this help
905 """.format(sys
.argv
[0]))
906 # --log-socket-host HOST: send logs to this host")
907 # --log-socket-port PORT: send logs using this port (default: 9022)")
910 if __name__
== '__main__':
912 # load parameters and configuration
913 opts
, args
= getopt
.getopt(sys
.argv
[1:], "hvc:", ["config=", "help"])
914 # TODO add "log-socket-host=", "log-socket-port=", "log-file="
917 if o
in ("-h", "--help"):
920 elif o
in ("-c", "--config"):
922 # elif o == "--log-socket-port":
923 # log_socket_port = a
924 # elif o == "--log-socket-host":
925 # log_socket_host = a
926 # elif o == "--log-file":
929 assert False, "Unhandled option"
931 if not path
.isfile(config_file
):
932 print("configuration file '{}' that not exist".format(config_file
), file=sys
.stderr
)
935 for config_file
in (__file__
[:__file__
.rfind(".")] + ".cfg", "./nbi.cfg", "/etc/osm/nbi.cfg"):
936 if path
.isfile(config_file
):
939 print("No configuration file 'nbi.cfg' found neither at local folder nor at /etc/osm/", file=sys
.stderr
)
942 except getopt
.GetoptError
as e
:
943 print(str(e
), file=sys
.stderr
)