2 # -*- coding: utf-8 -*-
8 import html_out
as html
10 import logging
.handlers
14 from auth
import Authenticator
15 from engine
import Engine
, EngineException
16 from osm_common
.dbbase
import DbException
17 from osm_common
.fsbase
import FsException
18 from osm_common
.msgbase
import MsgException
19 from http
import HTTPStatus
20 from codecs
import getreader
21 from os
import environ
, path
23 __author__
= "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
25 # TODO consider to remove and provide version using the static version file
27 version_date
= "Apr 2018"
28 database_version
= '1.0'
31 North Bound Interface (O: OSM specific; 5,X: SOL005 not implemented yet; O5: SOL005 implemented)
32 URL: /osm GET POST PUT DELETE PATCH
34 /ns_descriptors_content O O
40 /artifacts[/<artifactPath>] O
48 /vnf_packages_content O O
52 /package_content O5 O5
55 /artifacts[/<artifactPath>] O5
60 /ns_instances_content O O
72 /vnf_instances (also vnfrs for compatibility) O
83 /vims_accounts (also vims for compatibility) O O
89 Follows SOL005 section 4.3.2 It contains extra METHOD to override http method, FORCE to force.
90 For filtering inside array, it must select the element of the array, or add ANYINDEX to apply the filtering over any
91 item of the array, that is, pass if any item of the array pass the filter.
92 It allows both ne and neq for not equal
93 TODO: 4.3.3 Attribute selectors
94 all_fields, fields=x,y,.., exclude_default, exclude_fields=x,y,...
95 (none) … same as “exclude_default”
96 all_fields … all attributes.
97 fields=<list> … all attributes except all complex attributes with minimum cardinality of zero that are not
98 conditionally mandatory, and that are not provided in <list>.
99 exclude_fields=<list> … all attributes except those complex attributes with a minimum cardinality of zero that
100 are not conditionally mandatory, and that are provided in <list>.
101 exclude_default … all attributes except those complex attributes with a minimum cardinality of zero that are not
102 conditionally mandatory, and that are part of the "default exclude set" defined in the present specification for
103 the particular resource
104 exclude_default and include=<list> … all attributes except those complex attributes with a minimum cardinality
105 of zero that are not conditionally mandatory and that are part of the "default exclude set" defined in the
106 present specification for the particular resource, but that are not part of <list>
107 Header field name Reference Example Descriptions
108 Accept IETF RFC 7231 [19] application/json Content-Types that are acceptable for the response.
109 This header field shall be present if the response is expected to have a non-empty message body.
110 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the request.
111 This header field shall be present if the request has a non-empty message body.
112 Authorization IETF RFC 7235 [22] Bearer mF_9.B5f-4.1JqM The authorization token for the request.
113 Details are specified in clause 4.5.3.
114 Range IETF RFC 7233 [21] 1000-2000 Requested range of bytes from a file
115 Header field name Reference Example Descriptions
116 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the response.
117 This header field shall be present if the response has a non-empty message body.
118 Location IETF RFC 7231 [19] http://www.example.com/vnflcm/v1/vnf_instances/123 Used in redirection, or when a
119 new resource has been created.
120 This header field shall be present if the response status code is 201 or 3xx.
121 In the present document this header field is also used if the response status code is 202 and a new resource was
123 WWW-Authenticate IETF RFC 7235 [22] Bearer realm="example" Challenge if the corresponding HTTP request has not
124 provided authorization, or error details if the corresponding HTTP request has provided an invalid authorization
126 Accept-Ranges IETF RFC 7233 [21] bytes Used by the Server to signal whether or not it supports ranges for
128 Content-Range IETF RFC 7233 [21] bytes 21010-47021/ 47022 Signals the byte range that is contained in the
129 response, and the total length of the file.
130 Retry-After IETF RFC 7231 [19] Fri, 31 Dec 1999 23:59:59 GMT
134 class NbiException(Exception):
136 def __init__(self
, message
, http_code
=HTTPStatus
.METHOD_NOT_ALLOWED
):
137 Exception.__init
__(self
, message
)
138 self
.http_code
= http_code
141 class Server(object):
143 # to decode bytes to str
144 reader
= getreader("utf-8")
148 self
.engine
= Engine()
149 self
.authenticator
= Authenticator(self
.engine
)
150 self
.valid_methods
= { # contains allowed URL and methods
153 "tokens": {"METHODS": ("GET", "POST", "DELETE"),
154 "<ID>": {"METHODS": ("GET", "DELETE")}
156 "users": {"METHODS": ("GET", "POST"),
157 "<ID>": {"METHODS": ("GET", "POST", "DELETE")}
159 "projects": {"METHODS": ("GET", "POST"),
160 "<ID>": {"METHODS": ("GET", "DELETE")}
162 "vims": {"METHODS": ("GET", "POST"),
163 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
165 "vim_accounts": {"METHODS": ("GET", "POST"),
166 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
168 "sdns": {"METHODS": ("GET", "POST"),
169 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
175 "ns_descriptors_content": {"METHODS": ("GET", "POST"),
176 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
178 "ns_descriptors": {"METHODS": ("GET", "POST"),
179 "<ID>": {"METHODS": ("GET", "DELETE"), "TODO": "PATCH",
180 "nsd_content": {"METHODS": ("GET", "PUT")},
181 "nsd": {"METHODS": "GET"}, # descriptor inside package
182 "artifacts": {"*": {"METHODS": "GET"}}
185 "pnf_descriptors": {"TODO": ("GET", "POST"),
186 "<ID>": {"TODO": ("GET", "DELETE", "PATCH"),
187 "pnfd_content": {"TODO": ("GET", "PUT")}
190 "subscriptions": {"TODO": ("GET", "POST"),
191 "<ID>": {"TODO": ("GET", "DELETE")}
197 "vnf_packages_content": {"METHODS": ("GET", "POST"),
198 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
200 "vnf_packages": {"METHODS": ("GET", "POST"),
201 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"), # GET: vnfPkgInfo
202 "package_content": {"METHODS": ("GET", "PUT"), # package
203 "upload_from_uri": {"TODO": "POST"}
205 "vnfd": {"METHODS": "GET"}, # descriptor inside package
206 "artifacts": {"*": {"METHODS": "GET"}}
209 "subscriptions": {"TODO": ("GET", "POST"),
210 "<ID>": {"TODO": ("GET", "DELETE")}
216 "ns_instances_content": {"METHODS": ("GET", "POST"),
217 "<ID>": {"METHODS": ("GET", "DELETE")}
219 "ns_instances": {"METHODS": ("GET", "POST"),
220 "<ID>": {"METHODS": ("GET", "DELETE"),
221 "scale": {"METHODS": "POST"},
222 "terminate": {"METHODS": "POST"},
223 "instantiate": {"METHODS": "POST"},
224 "action": {"METHODS": "POST"},
227 "ns_lcm_op_occs": {"METHODS": "GET",
228 "<ID>": {"METHODS": "GET"},
230 "vnfrs": {"METHODS": ("GET"),
231 "<ID>": {"METHODS": ("GET")}
233 "vnf_instances": {"METHODS": ("GET"),
234 "<ID>": {"METHODS": ("GET")}
240 def _format_in(self
, kwargs
):
243 if cherrypy
.request
.body
.length
:
244 error_text
= "Invalid input format "
246 if "Content-Type" in cherrypy
.request
.headers
:
247 if "application/json" in cherrypy
.request
.headers
["Content-Type"]:
248 error_text
= "Invalid json format "
249 indata
= json
.load(self
.reader(cherrypy
.request
.body
))
250 elif "application/yaml" in cherrypy
.request
.headers
["Content-Type"]:
251 error_text
= "Invalid yaml format "
252 indata
= yaml
.load(cherrypy
.request
.body
)
253 elif "application/binary" in cherrypy
.request
.headers
["Content-Type"] or \
254 "application/gzip" in cherrypy
.request
.headers
["Content-Type"] or \
255 "application/zip" in cherrypy
.request
.headers
["Content-Type"] or \
256 "text/plain" in cherrypy
.request
.headers
["Content-Type"]:
257 indata
= cherrypy
.request
.body
# .read()
258 elif "multipart/form-data" in cherrypy
.request
.headers
["Content-Type"]:
259 if "descriptor_file" in kwargs
:
260 filecontent
= kwargs
.pop("descriptor_file")
261 if not filecontent
.file:
262 raise NbiException("empty file or content", HTTPStatus
.BAD_REQUEST
)
263 indata
= filecontent
.file # .read()
264 if filecontent
.content_type
.value
:
265 cherrypy
.request
.headers
["Content-Type"] = filecontent
.content_type
.value
267 # raise cherrypy.HTTPError(HTTPStatus.Not_Acceptable,
268 # "Only 'Content-Type' of type 'application/json' or
269 # 'application/yaml' for input format are available")
270 error_text
= "Invalid yaml format "
271 indata
= yaml
.load(cherrypy
.request
.body
)
273 error_text
= "Invalid yaml format "
274 indata
= yaml
.load(cherrypy
.request
.body
)
279 if cherrypy
.request
.headers
.get("Query-String-Format") == "yaml":
282 for k
, v
in kwargs
.items():
283 if isinstance(v
, str):
288 kwargs
[k
] = yaml
.load(v
)
291 elif k
.endswith(".gt") or k
.endswith(".lt") or k
.endswith(".gte") or k
.endswith(".lte"):
299 elif v
.find(",") > 0:
300 kwargs
[k
] = v
.split(",")
301 elif isinstance(v
, (list, tuple)):
302 for index
in range(0, len(v
)):
307 v
[index
] = yaml
.load(v
[index
])
312 except (ValueError, yaml
.YAMLError
) as exc
:
313 raise NbiException(error_text
+ str(exc
), HTTPStatus
.BAD_REQUEST
)
314 except KeyError as exc
:
315 raise NbiException("Query string error: " + str(exc
), HTTPStatus
.BAD_REQUEST
)
316 except Exception as exc
:
317 raise NbiException(error_text
+ str(exc
), HTTPStatus
.BAD_REQUEST
)
320 def _format_out(data
, session
=None, _format
=None):
322 return string of dictionary data according to requested json, yaml, xml. By default json
323 :param data: response to be sent. Can be a dict, text or file
325 :param _format: The format to be set as Content-Type ir data is a file
328 accept
= cherrypy
.request
.headers
.get("Accept")
330 if accept
and "text/html" in accept
:
331 return html
.format(data
, cherrypy
.request
, cherrypy
.response
, session
)
332 # cherrypy.response.status = HTTPStatus.NO_CONTENT.value
334 elif hasattr(data
, "read"): # file object
336 cherrypy
.response
.headers
["Content-Type"] = _format
337 elif "b" in data
.mode
: # binariy asssumig zip
338 cherrypy
.response
.headers
["Content-Type"] = 'application/zip'
340 cherrypy
.response
.headers
["Content-Type"] = 'text/plain'
341 # TODO check that cherrypy close file. If not implement pending things to close per thread next
344 if "application/json" in accept
:
345 cherrypy
.response
.headers
["Content-Type"] = 'application/json; charset=utf-8'
346 a
= json
.dumps(data
, indent
=4) + "\n"
347 return a
.encode("utf8")
348 elif "text/html" in accept
:
349 return html
.format(data
, cherrypy
.request
, cherrypy
.response
, session
)
351 elif "application/yaml" in accept
or "*/*" in accept
or "text/plain" in accept
:
354 raise cherrypy
.HTTPError(HTTPStatus
.NOT_ACCEPTABLE
.value
,
355 "Only 'Accept' of type 'application/json' or 'application/yaml' "
356 "for output format are available")
357 cherrypy
.response
.headers
["Content-Type"] = 'application/yaml'
358 return yaml
.safe_dump(data
, explicit_start
=True, indent
=4, default_flow_style
=False, tags
=False,
359 encoding
='utf-8', allow_unicode
=True) # , canonical=True, default_style='"'
362 def index(self
, *args
, **kwargs
):
365 if cherrypy
.request
.method
== "GET":
366 session
= self
.authenticator
.authorize()
367 outdata
= "Index page"
369 raise cherrypy
.HTTPError(HTTPStatus
.METHOD_NOT_ALLOWED
.value
,
370 "Method {} not allowed for tokens".format(cherrypy
.request
.method
))
372 return self
._format
_out
(outdata
, session
)
374 except EngineException
as e
:
375 cherrypy
.log("index Exception {}".format(e
))
376 cherrypy
.response
.status
= e
.http_code
.value
377 return self
._format
_out
("Welcome to OSM!", session
)
380 def version(self
, *args
, **kwargs
):
381 # TODO consider to remove and provide version using the static version file
382 global __version__
, version_date
384 if cherrypy
.request
.method
!= "GET":
385 raise NbiException("Only method GET is allowed", HTTPStatus
.METHOD_NOT_ALLOWED
)
387 raise NbiException("Invalid URL or query string for version", HTTPStatus
.METHOD_NOT_ALLOWED
)
388 return __version__
+ " " + version_date
389 except NbiException
as e
:
390 cherrypy
.response
.status
= e
.http_code
.value
392 "code": e
.http_code
.name
,
393 "status": e
.http_code
.value
,
396 return self
._format
_out
(problem_details
, None)
399 def token(self
, method
, token_id
=None, kwargs
=None):
401 # self.engine.load_dbase(cherrypy.request.app.config)
402 indata
= self
._format
_in
(kwargs
)
403 if not isinstance(indata
, dict):
404 raise NbiException("Expected application/yaml or application/json Content-Type", HTTPStatus
.BAD_REQUEST
)
407 session
= self
.authenticator
.authorize()
409 outdata
= self
.authenticator
.get_token(session
, token_id
)
411 outdata
= self
.authenticator
.get_token_list(session
)
412 elif method
== "POST":
414 session
= self
.authenticator
.authorize()
418 indata
.update(kwargs
)
419 outdata
= self
.authenticator
.new_token(session
, indata
, cherrypy
.request
.remote
)
421 cherrypy
.session
['Authorization'] = outdata
["_id"]
422 self
._set
_location
_header
("admin", "v1", "tokens", outdata
["_id"])
423 # cherrypy.response.cookie["Authorization"] = outdata["id"]
424 # cherrypy.response.cookie["Authorization"]['expires'] = 3600
425 elif method
== "DELETE":
426 if not token_id
and "id" in kwargs
:
427 token_id
= kwargs
["id"]
429 session
= self
.authenticator
.authorize()
430 token_id
= session
["_id"]
431 outdata
= self
.authenticator
.del_token(token_id
)
433 cherrypy
.session
['Authorization'] = "logout"
434 # cherrypy.response.cookie["Authorization"] = token_id
435 # cherrypy.response.cookie["Authorization"]['expires'] = 0
437 raise NbiException("Method {} not allowed for token".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
438 return self
._format
_out
(outdata
, session
)
439 except (NbiException
, EngineException
, DbException
) as e
:
440 cherrypy
.log("tokens Exception {}".format(e
))
441 cherrypy
.response
.status
= e
.http_code
.value
443 "code": e
.http_code
.name
,
444 "status": e
.http_code
.value
,
447 return self
._format
_out
(problem_details
, session
)
450 def test(self
, *args
, **kwargs
):
452 if args
and args
[0] == "help":
453 return "<html><pre>\ninit\nfile/<name> download file\ndb-clear/table\nprune\nlogin\nlogin2\n"\
454 "sleep/<time>\nmessage/topic\n</pre></html>"
456 elif args
and args
[0] == "init":
458 # self.engine.load_dbase(cherrypy.request.app.config)
459 self
.engine
.create_admin()
460 return "Done. User 'admin', password 'admin' created"
462 cherrypy
.response
.status
= HTTPStatus
.FORBIDDEN
.value
463 return self
._format
_out
("Database already initialized")
464 elif args
and args
[0] == "file":
465 return cherrypy
.lib
.static
.serve_file(cherrypy
.tree
.apps
['/osm'].config
["storage"]["path"] + "/" + args
[1],
466 "text/plain", "attachment")
467 elif args
and args
[0] == "file2":
468 f_path
= cherrypy
.tree
.apps
['/osm'].config
["storage"]["path"] + "/" + args
[1]
469 f
= open(f_path
, "r")
470 cherrypy
.response
.headers
["Content-type"] = "text/plain"
473 elif len(args
) == 2 and args
[0] == "db-clear":
474 return self
.engine
.del_item_list({"project_id": "admin"}, args
[1], {})
475 elif args
and args
[0] == "prune":
476 return self
.engine
.prune()
477 elif args
and args
[0] == "login":
478 if not cherrypy
.request
.headers
.get("Authorization"):
479 cherrypy
.response
.headers
["WWW-Authenticate"] = 'Basic realm="Access to OSM site", charset="UTF-8"'
480 cherrypy
.response
.status
= HTTPStatus
.UNAUTHORIZED
.value
481 elif args
and args
[0] == "login2":
482 if not cherrypy
.request
.headers
.get("Authorization"):
483 cherrypy
.response
.headers
["WWW-Authenticate"] = 'Bearer realm="Access to OSM site"'
484 cherrypy
.response
.status
= HTTPStatus
.UNAUTHORIZED
.value
485 elif args
and args
[0] == "sleep":
488 sleep_time
= int(args
[1])
490 cherrypy
.response
.status
= HTTPStatus
.FORBIDDEN
.value
491 return self
._format
_out
("Database already initialized")
492 thread_info
= cherrypy
.thread_data
494 time
.sleep(sleep_time
)
496 elif len(args
) >= 2 and args
[0] == "message":
498 return_text
= "<html><pre>{} ->\n".format(topic
)
500 if cherrypy
.request
.method
== 'POST':
501 to_send
= yaml
.load(cherrypy
.request
.body
)
502 for k
, v
in to_send
.items():
503 self
.engine
.msg
.write(topic
, k
, v
)
504 return_text
+= " {}: {}\n".format(k
, v
)
505 elif cherrypy
.request
.method
== 'GET':
506 for k
, v
in kwargs
.items():
507 self
.engine
.msg
.write(topic
, k
, yaml
.load(v
))
508 return_text
+= " {}: {}\n".format(k
, yaml
.load(v
))
509 except Exception as e
:
510 return_text
+= "Error: " + str(e
)
511 return_text
+= "</pre></html>\n"
515 "<html><pre>\nheaders:\n args: {}\n".format(args
) +
516 " kwargs: {}\n".format(kwargs
) +
517 " headers: {}\n".format(cherrypy
.request
.headers
) +
518 " path_info: {}\n".format(cherrypy
.request
.path_info
) +
519 " query_string: {}\n".format(cherrypy
.request
.query_string
) +
520 " session: {}\n".format(cherrypy
.session
) +
521 " cookie: {}\n".format(cherrypy
.request
.cookie
) +
522 " method: {}\n".format(cherrypy
.request
.method
) +
523 " session: {}\n".format(cherrypy
.session
.get('fieldname')) +
525 return_text
+= " length: {}\n".format(cherrypy
.request
.body
.length
)
526 if cherrypy
.request
.body
.length
:
527 return_text
+= " content: {}\n".format(
528 str(cherrypy
.request
.body
.read(int(cherrypy
.request
.headers
.get('Content-Length', 0)))))
530 return_text
+= "thread: {}\n".format(thread_info
)
531 return_text
+= "</pre></html>"
534 def _check_valid_url_method(self
, method
, *args
):
536 raise NbiException("URL must contain at least 'topic/version/item'", HTTPStatus
.METHOD_NOT_ALLOWED
)
538 reference
= self
.valid_methods
542 if not isinstance(reference
, dict):
543 raise NbiException("URL contains unexpected extra items '{}'".format(arg
),
544 HTTPStatus
.METHOD_NOT_ALLOWED
)
547 reference
= reference
[arg
]
548 elif "<ID>" in reference
:
549 reference
= reference
["<ID>"]
550 elif "*" in reference
:
551 reference
= reference
["*"]
554 raise NbiException("Unexpected URL item {}".format(arg
), HTTPStatus
.METHOD_NOT_ALLOWED
)
555 if "TODO" in reference
and method
in reference
["TODO"]:
556 raise NbiException("Method {} not supported yet for this URL".format(method
), HTTPStatus
.NOT_IMPLEMENTED
)
557 elif "METHODS" in reference
and method
not in reference
["METHODS"]:
558 raise NbiException("Method {} not supported for this URL".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
562 def _set_location_header(topic
, version
, item
, id):
564 Insert response header Location with the URL of created item base on URL params
571 # Use cherrypy.request.base for absoluted path and make use of request.header HOST just in case behind aNAT
572 cherrypy
.response
.headers
["Location"] = "/osm/{}/{}/{}/{}".format(topic
, version
, item
, id)
576 def default(self
, topic
=None, version
=None, item
=None, _id
=None, item2
=None, *args
, **kwargs
):
585 if not topic
or not version
or not item
:
586 raise NbiException("URL must contain at least 'topic/version/item'", HTTPStatus
.METHOD_NOT_ALLOWED
)
587 if topic
not in ("admin", "vnfpkgm", "nsd", "nslcm"):
588 raise NbiException("URL topic '{}' not supported".format(topic
), HTTPStatus
.METHOD_NOT_ALLOWED
)
590 raise NbiException("URL version '{}' not supported".format(version
), HTTPStatus
.METHOD_NOT_ALLOWED
)
592 if kwargs
and "METHOD" in kwargs
and kwargs
["METHOD"] in ("PUT", "POST", "DELETE", "GET", "PATCH"):
593 method
= kwargs
.pop("METHOD")
595 method
= cherrypy
.request
.method
596 if kwargs
and "FORCE" in kwargs
:
597 force
= kwargs
.pop("FORCE")
601 self
._check
_valid
_url
_method
(method
, topic
, version
, item
, _id
, item2
, *args
)
603 if topic
== "admin" and item
== "tokens":
604 return self
.token(method
, _id
, kwargs
)
606 # self.engine.load_dbase(cherrypy.request.app.config)
607 session
= self
.authenticator
.authorize()
608 indata
= self
._format
_in
(kwargs
)
610 if item
== "subscriptions":
611 engine_item
= topic
+ "_" + item
617 elif topic
== "vnfpkgm":
618 engine_item
= "vnfds"
619 elif topic
== "nslcm":
621 if item
== "ns_lcm_op_occs":
622 engine_item
= "nslcmops"
623 if item
== "vnfrs" or item
== "vnf_instances":
624 engine_item
= "vnfrs"
625 if engine_item
== "vims": # TODO this is for backward compatibility, it will remove in the future
626 engine_item
= "vim_accounts"
629 if item2
in ("nsd_content", "package_content", "artifacts", "vnfd", "nsd"):
630 if item2
in ("vnfd", "nsd"):
634 elif item2
== "artifacts":
638 file, _format
= self
.engine
.get_file(session
, engine_item
, _id
, path
,
639 cherrypy
.request
.headers
.get("Accept"))
642 outdata
= self
.engine
.get_item_list(session
, engine_item
, kwargs
)
644 outdata
= self
.engine
.get_item(session
, engine_item
, _id
)
645 elif method
== "POST":
646 if item
in ("ns_descriptors_content", "vnf_packages_content"):
647 _id
= cherrypy
.request
.headers
.get("Transaction-Id")
649 _id
= self
.engine
.new_item(rollback
, session
, engine_item
, {}, None, cherrypy
.request
.headers
,
651 completed
= self
.engine
.upload_content(session
, engine_item
, _id
, indata
, kwargs
,
652 cherrypy
.request
.headers
)
654 self
._set
_location
_header
(topic
, version
, item
, _id
)
656 cherrypy
.response
.headers
["Transaction-Id"] = _id
657 outdata
= {"id": _id
}
658 elif item
== "ns_instances_content":
659 _id
= self
.engine
.new_item(rollback
, session
, engine_item
, indata
, kwargs
, force
=force
)
660 self
.engine
.ns_operation(rollback
, session
, _id
, "instantiate", indata
, None)
661 self
._set
_location
_header
(topic
, version
, item
, _id
)
662 outdata
= {"id": _id
}
663 elif item
== "ns_instances" and item2
:
664 _id
= self
.engine
.ns_operation(rollback
, session
, _id
, item2
, indata
, kwargs
)
665 self
._set
_location
_header
(topic
, version
, "ns_lcm_op_occs", _id
)
666 outdata
= {"id": _id
}
667 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
669 _id
= self
.engine
.new_item(rollback
, session
, engine_item
, indata
, kwargs
, cherrypy
.request
.headers
,
671 self
._set
_location
_header
(topic
, version
, item
, _id
)
672 outdata
= {"id": _id
}
673 # TODO form NsdInfo when item in ("ns_descriptors", "vnf_packages")
674 cherrypy
.response
.status
= HTTPStatus
.CREATED
.value
676 elif method
== "DELETE":
678 outdata
= self
.engine
.del_item_list(session
, engine_item
, kwargs
)
679 cherrypy
.response
.status
= HTTPStatus
.OK
.value
680 else: # len(args) > 1
681 if item
== "ns_instances_content" and not force
:
682 opp_id
= self
.engine
.ns_operation(rollback
, session
, _id
, "terminate", {"autoremove": True},
684 outdata
= {"_id": opp_id
}
685 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
687 self
.engine
.del_item(session
, engine_item
, _id
, force
)
688 cherrypy
.response
.status
= HTTPStatus
.NO_CONTENT
.value
689 if engine_item
in ("vim_accounts", "sdns"):
690 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
692 elif method
in ("PUT", "PATCH"):
693 if not indata
and not kwargs
:
694 raise NbiException("Nothing to update. Provide payload and/or query string",
695 HTTPStatus
.BAD_REQUEST
)
696 if item2
in ("nsd_content", "package_content") and method
== "PUT":
697 completed
= self
.engine
.upload_content(session
, engine_item
, _id
, indata
, kwargs
,
698 cherrypy
.request
.headers
)
700 cherrypy
.response
.headers
["Transaction-Id"] = id
701 cherrypy
.response
.status
= HTTPStatus
.NO_CONTENT
.value
704 outdata
= {"id": self
.engine
.edit_item(session
, engine_item
, _id
, indata
, kwargs
, force
=force
)}
706 raise NbiException("Method {} not allowed".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
707 return self
._format
_out
(outdata
, session
, _format
)
708 except (NbiException
, EngineException
, DbException
, FsException
, MsgException
) as e
:
709 cherrypy
.log("Exception {}".format(e
))
710 cherrypy
.response
.status
= e
.http_code
.value
711 if hasattr(outdata
, "close"): # is an open file
713 for rollback_item
in rollback
:
715 self
.engine
.del_item(**rollback_item
, session
=session
, force
=True)
716 except Exception as e2
:
717 cherrypy
.log("Rollback Exception {}: {}".format(rollback_item
, e2
))
719 if isinstance(e
, MsgException
):
720 error_text
= "{} has been '{}' but other modules cannot be informed because an error on bus".format(
721 engine_item
[:-1], method
, error_text
)
723 "code": e
.http_code
.name
,
724 "status": e
.http_code
.value
,
727 return self
._format
_out
(problem_details
, session
)
728 # raise cherrypy.HTTPError(e.http_code.value, str(e))
731 # def validate_password(realm, username, password):
732 # cherrypy.log("realm "+ str(realm))
733 # if username == "admin" and password == "admin":
738 def _start_service():
740 Callback function called when cherrypy.engine starts
741 Override configuration with env variables
742 Set database, storage, message configuration
743 Init database with admin/admin user password
745 cherrypy
.log
.error("Starting osm_nbi")
746 # update general cherrypy configuration
749 engine_config
= cherrypy
.tree
.apps
['/osm'].config
750 for k
, v
in environ
.items():
751 if not k
.startswith("OSMNBI_"):
753 k1
, _
, k2
= k
[7:].lower().partition("_")
757 # update static configuration
758 if k
== 'OSMNBI_STATIC_DIR':
759 engine_config
["/static"]['tools.staticdir.dir'] = v
760 engine_config
["/static"]['tools.staticdir.on'] = True
761 elif k
== 'OSMNBI_SOCKET_PORT' or k
== 'OSMNBI_SERVER_PORT':
762 update_dict
['server.socket_port'] = int(v
)
763 elif k
== 'OSMNBI_SOCKET_HOST' or k
== 'OSMNBI_SERVER_HOST':
764 update_dict
['server.socket_host'] = v
765 elif k1
in ("server", "test", "auth", "log"):
766 update_dict
[k1
+ '.' + k2
] = v
767 elif k1
in ("message", "database", "storage"):
768 # k2 = k2.replace('_', '.')
770 engine_config
[k1
][k2
] = int(v
)
772 engine_config
[k1
][k2
] = v
773 except ValueError as e
:
774 cherrypy
.log
.error("Ignoring environ '{}': " + str(e
))
775 except Exception as e
:
776 cherrypy
.log
.warn("skipping environ '{}' on exception '{}'".format(k
, e
))
779 cherrypy
.config
.update(update_dict
)
780 engine_config
["global"].update(update_dict
)
783 log_format_simple
= "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(message)s"
784 log_formatter_simple
= logging
.Formatter(log_format_simple
, datefmt
='%Y-%m-%dT%H:%M:%S')
785 logger_server
= logging
.getLogger("cherrypy.error")
786 logger_access
= logging
.getLogger("cherrypy.access")
787 logger_cherry
= logging
.getLogger("cherrypy")
788 logger_nbi
= logging
.getLogger("nbi")
790 if "log.file" in engine_config
["global"]:
791 file_handler
= logging
.handlers
.RotatingFileHandler(engine_config
["global"]["log.file"],
792 maxBytes
=100e6
, backupCount
=9, delay
=0)
793 file_handler
.setFormatter(log_formatter_simple
)
794 logger_cherry
.addHandler(file_handler
)
795 logger_nbi
.addHandler(file_handler
)
797 for format_
, logger
in {"nbi.server": logger_server
,
798 "nbi.access": logger_access
,
799 "%(name)s %(filename)s:%(lineno)s": logger_nbi
801 log_format_cherry
= "%(asctime)s %(levelname)s {} %(message)s".format(format_
)
802 log_formatter_cherry
= logging
.Formatter(log_format_cherry
, datefmt
='%Y-%m-%dT%H:%M:%S')
803 str_handler
= logging
.StreamHandler()
804 str_handler
.setFormatter(log_formatter_cherry
)
805 logger
.addHandler(str_handler
)
807 if engine_config
["global"].get("log.level"):
808 logger_cherry
.setLevel(engine_config
["global"]["log.level"])
809 logger_nbi
.setLevel(engine_config
["global"]["log.level"])
811 # logging other modules
812 for k1
, logname
in {"message": "nbi.msg", "database": "nbi.db", "storage": "nbi.fs"}.items():
813 engine_config
[k1
]["logger_name"] = logname
814 logger_module
= logging
.getLogger(logname
)
815 if "logfile" in engine_config
[k1
]:
816 file_handler
= logging
.handlers
.RotatingFileHandler(engine_config
[k1
]["logfile"],
817 maxBytes
=100e6
, backupCount
=9, delay
=0)
818 file_handler
.setFormatter(log_formatter_simple
)
819 logger_module
.addHandler(file_handler
)
820 if "loglevel" in engine_config
[k1
]:
821 logger_module
.setLevel(engine_config
[k1
]["loglevel"])
822 # TODO add more entries, e.g.: storage
823 cherrypy
.tree
.apps
['/osm'].root
.engine
.start(engine_config
)
825 cherrypy
.tree
.apps
['/osm'].root
.engine
.init_db(target_version
=database_version
)
826 except EngineException
:
828 # getenv('OSMOPENMANO_TENANT', None)
833 Callback function called when cherrypy.engine stops
834 TODO: Ending database connections.
836 cherrypy
.tree
.apps
['/osm'].root
.engine
.stop()
837 cherrypy
.log
.error("Stopping osm_nbi")
840 def nbi(config_file
):
843 # #'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
844 # 'tools.sessions.on': True,
845 # 'tools.response_headers.on': True,
846 # # 'tools.response_headers.headers': [('Content-Type', 'text/plain')],
849 # cherrypy.Server.ssl_module = 'builtin'
850 # cherrypy.Server.ssl_certificate = "http/cert.pem"
851 # cherrypy.Server.ssl_private_key = "http/privkey.pem"
852 # cherrypy.Server.thread_pool = 10
853 # cherrypy.config.update({'Server.socket_port': config["port"], 'Server.socket_host': config["host"]})
855 # cherrypy.config.update({'tools.auth_basic.on': True,
856 # 'tools.auth_basic.realm': 'localhost',
857 # 'tools.auth_basic.checkpassword': validate_password})
858 cherrypy
.engine
.subscribe('start', _start_service
)
859 cherrypy
.engine
.subscribe('stop', _stop_service
)
860 cherrypy
.quickstart(Server(), '/osm', config_file
)
864 print("""Usage: {} [options]
865 -c|--config [configuration_file]: loads the configuration file (default: ./nbi.cfg)
866 -h|--help: shows this help
867 """.format(sys
.argv
[0]))
868 # --log-socket-host HOST: send logs to this host")
869 # --log-socket-port PORT: send logs using this port (default: 9022)")
872 if __name__
== '__main__':
874 # load parameters and configuration
875 opts
, args
= getopt
.getopt(sys
.argv
[1:], "hvc:", ["config=", "help"])
876 # TODO add "log-socket-host=", "log-socket-port=", "log-file="
879 if o
in ("-h", "--help"):
882 elif o
in ("-c", "--config"):
884 # elif o == "--log-socket-port":
885 # log_socket_port = a
886 # elif o == "--log-socket-host":
887 # log_socket_host = a
888 # elif o == "--log-file":
891 assert False, "Unhandled option"
893 if not path
.isfile(config_file
):
894 print("configuration file '{}' that not exist".format(config_file
), file=sys
.stderr
)
897 for config_file
in (__file__
[:__file__
.rfind(".")] + ".cfg", "./nbi.cfg", "/etc/osm/nbi.cfg"):
898 if path
.isfile(config_file
):
901 print("No configuration file 'nbi.cfg' found neither at local folder nor at /etc/osm/", file=sys
.stderr
)
904 except getopt
.GetoptError
as e
:
905 print(str(e
), file=sys
.stderr
)