2 # -*- coding: utf-8 -*-
8 import html_out
as html
10 import logging
.handlers
14 from auth
import Authenticator
, AuthException
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'
29 auth_database_version
= '1.0'
32 North Bound Interface (O: OSM specific; 5,X: SOL005 not implemented yet; O5: SOL005 implemented)
33 URL: /osm GET POST PUT DELETE PATCH
35 /ns_descriptors_content O O
41 /artifacts[/<artifactPath>] O
49 /vnf_packages_content O O
53 /package_content O5 O5
56 /artifacts[/<artifactPath>] O5
61 /ns_instances_content O O
73 /vnf_instances (also vnfrs for compatibility) O
84 /vims_accounts (also vims for compatibility) O O
90 Follows SOL005 section 4.3.2 It contains extra METHOD to override http method, FORCE to force.
91 For filtering inside array, it must select the element of the array, or add ANYINDEX to apply the filtering over any
92 item of the array, that is, pass if any item of the array pass the filter.
93 It allows both ne and neq for not equal
94 TODO: 4.3.3 Attribute selectors
95 all_fields, fields=x,y,.., exclude_default, exclude_fields=x,y,...
96 (none) … same as “exclude_default”
97 all_fields … all attributes.
98 fields=<list> … all attributes except all complex attributes with minimum cardinality of zero that are not
99 conditionally mandatory, and that are not provided in <list>.
100 exclude_fields=<list> … all attributes except those complex attributes with a minimum cardinality of zero that
101 are not conditionally mandatory, and that are provided in <list>.
102 exclude_default … all attributes except those complex attributes with a minimum cardinality of zero that are not
103 conditionally mandatory, and that are part of the "default exclude set" defined in the present specification for
104 the particular resource
105 exclude_default and include=<list> … all attributes except those complex attributes with a minimum cardinality
106 of zero that are not conditionally mandatory and that are part of the "default exclude set" defined in the
107 present specification for the particular resource, but that are not part of <list>
108 Header field name Reference Example Descriptions
109 Accept IETF RFC 7231 [19] application/json Content-Types that are acceptable for the response.
110 This header field shall be present if the response is expected to have a non-empty message body.
111 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the request.
112 This header field shall be present if the request has a non-empty message body.
113 Authorization IETF RFC 7235 [22] Bearer mF_9.B5f-4.1JqM The authorization token for the request.
114 Details are specified in clause 4.5.3.
115 Range IETF RFC 7233 [21] 1000-2000 Requested range of bytes from a file
116 Header field name Reference Example Descriptions
117 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the response.
118 This header field shall be present if the response has a non-empty message body.
119 Location IETF RFC 7231 [19] http://www.example.com/vnflcm/v1/vnf_instances/123 Used in redirection, or when a
120 new resource has been created.
121 This header field shall be present if the response status code is 201 or 3xx.
122 In the present document this header field is also used if the response status code is 202 and a new resource was
124 WWW-Authenticate IETF RFC 7235 [22] Bearer realm="example" Challenge if the corresponding HTTP request has not
125 provided authorization, or error details if the corresponding HTTP request has provided an invalid authorization
127 Accept-Ranges IETF RFC 7233 [21] bytes Used by the Server to signal whether or not it supports ranges for
129 Content-Range IETF RFC 7233 [21] bytes 21010-47021/ 47022 Signals the byte range that is contained in the
130 response, and the total length of the file.
131 Retry-After IETF RFC 7231 [19] Fri, 31 Dec 1999 23:59:59 GMT
135 class NbiException(Exception):
137 def __init__(self
, message
, http_code
=HTTPStatus
.METHOD_NOT_ALLOWED
):
138 Exception.__init
__(self
, message
)
139 self
.http_code
= http_code
142 class Server(object):
144 # to decode bytes to str
145 reader
= getreader("utf-8")
149 self
.engine
= Engine()
150 self
.authenticator
= Authenticator(self
.engine
)
151 self
.valid_methods
= { # contains allowed URL and methods
154 "tokens": {"METHODS": ("GET", "POST", "DELETE"),
155 "<ID>": {"METHODS": ("GET", "DELETE")}
157 "users": {"METHODS": ("GET", "POST"),
158 "<ID>": {"METHODS": ("GET", "POST", "DELETE")}
160 "projects": {"METHODS": ("GET", "POST"),
161 "<ID>": {"METHODS": ("GET", "DELETE")}
163 "vims": {"METHODS": ("GET", "POST"),
164 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
166 "vim_accounts": {"METHODS": ("GET", "POST"),
167 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
169 "sdns": {"METHODS": ("GET", "POST"),
170 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
176 "ns_descriptors_content": {"METHODS": ("GET", "POST"),
177 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
179 "ns_descriptors": {"METHODS": ("GET", "POST"),
180 "<ID>": {"METHODS": ("GET", "DELETE"), "TODO": "PATCH",
181 "nsd_content": {"METHODS": ("GET", "PUT")},
182 "nsd": {"METHODS": "GET"}, # descriptor inside package
183 "artifacts": {"*": {"METHODS": "GET"}}
186 "pnf_descriptors": {"TODO": ("GET", "POST"),
187 "<ID>": {"TODO": ("GET", "DELETE", "PATCH"),
188 "pnfd_content": {"TODO": ("GET", "PUT")}
191 "subscriptions": {"TODO": ("GET", "POST"),
192 "<ID>": {"TODO": ("GET", "DELETE")}
198 "vnf_packages_content": {"METHODS": ("GET", "POST"),
199 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
201 "vnf_packages": {"METHODS": ("GET", "POST"),
202 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"), # GET: vnfPkgInfo
203 "package_content": {"METHODS": ("GET", "PUT"), # package
204 "upload_from_uri": {"TODO": "POST"}
206 "vnfd": {"METHODS": "GET"}, # descriptor inside package
207 "artifacts": {"*": {"METHODS": "GET"}}
210 "subscriptions": {"TODO": ("GET", "POST"),
211 "<ID>": {"TODO": ("GET", "DELETE")}
217 "ns_instances_content": {"METHODS": ("GET", "POST"),
218 "<ID>": {"METHODS": ("GET", "DELETE")}
220 "ns_instances": {"METHODS": ("GET", "POST"),
221 "<ID>": {"METHODS": ("GET", "DELETE"),
222 "scale": {"METHODS": "POST"},
223 "terminate": {"METHODS": "POST"},
224 "instantiate": {"METHODS": "POST"},
225 "action": {"METHODS": "POST"},
228 "ns_lcm_op_occs": {"METHODS": "GET",
229 "<ID>": {"METHODS": "GET"},
231 "vnfrs": {"METHODS": ("GET"),
232 "<ID>": {"METHODS": ("GET")}
234 "vnf_instances": {"METHODS": ("GET"),
235 "<ID>": {"METHODS": ("GET")}
241 def _format_in(self
, kwargs
):
244 if cherrypy
.request
.body
.length
:
245 error_text
= "Invalid input format "
247 if "Content-Type" in cherrypy
.request
.headers
:
248 if "application/json" in cherrypy
.request
.headers
["Content-Type"]:
249 error_text
= "Invalid json format "
250 indata
= json
.load(self
.reader(cherrypy
.request
.body
))
251 elif "application/yaml" in cherrypy
.request
.headers
["Content-Type"]:
252 error_text
= "Invalid yaml format "
253 indata
= yaml
.load(cherrypy
.request
.body
)
254 elif "application/binary" in cherrypy
.request
.headers
["Content-Type"] or \
255 "application/gzip" in cherrypy
.request
.headers
["Content-Type"] or \
256 "application/zip" in cherrypy
.request
.headers
["Content-Type"] or \
257 "text/plain" in cherrypy
.request
.headers
["Content-Type"]:
258 indata
= cherrypy
.request
.body
# .read()
259 elif "multipart/form-data" in cherrypy
.request
.headers
["Content-Type"]:
260 if "descriptor_file" in kwargs
:
261 filecontent
= kwargs
.pop("descriptor_file")
262 if not filecontent
.file:
263 raise NbiException("empty file or content", HTTPStatus
.BAD_REQUEST
)
264 indata
= filecontent
.file # .read()
265 if filecontent
.content_type
.value
:
266 cherrypy
.request
.headers
["Content-Type"] = filecontent
.content_type
.value
268 # raise cherrypy.HTTPError(HTTPStatus.Not_Acceptable,
269 # "Only 'Content-Type' of type 'application/json' or
270 # 'application/yaml' for input format are available")
271 error_text
= "Invalid yaml format "
272 indata
= yaml
.load(cherrypy
.request
.body
)
274 error_text
= "Invalid yaml format "
275 indata
= yaml
.load(cherrypy
.request
.body
)
280 if cherrypy
.request
.headers
.get("Query-String-Format") == "yaml":
283 for k
, v
in kwargs
.items():
284 if isinstance(v
, str):
289 kwargs
[k
] = yaml
.load(v
)
292 elif k
.endswith(".gt") or k
.endswith(".lt") or k
.endswith(".gte") or k
.endswith(".lte"):
300 elif v
.find(",") > 0:
301 kwargs
[k
] = v
.split(",")
302 elif isinstance(v
, (list, tuple)):
303 for index
in range(0, len(v
)):
308 v
[index
] = yaml
.load(v
[index
])
313 except (ValueError, yaml
.YAMLError
) as exc
:
314 raise NbiException(error_text
+ str(exc
), HTTPStatus
.BAD_REQUEST
)
315 except KeyError as exc
:
316 raise NbiException("Query string error: " + str(exc
), HTTPStatus
.BAD_REQUEST
)
317 except Exception as exc
:
318 raise NbiException(error_text
+ str(exc
), HTTPStatus
.BAD_REQUEST
)
321 def _format_out(data
, session
=None, _format
=None):
323 return string of dictionary data according to requested json, yaml, xml. By default json
324 :param data: response to be sent. Can be a dict, text or file
326 :param _format: The format to be set as Content-Type ir data is a file
329 accept
= cherrypy
.request
.headers
.get("Accept")
331 if accept
and "text/html" in accept
:
332 return html
.format(data
, cherrypy
.request
, cherrypy
.response
, session
)
333 # cherrypy.response.status = HTTPStatus.NO_CONTENT.value
335 elif hasattr(data
, "read"): # file object
337 cherrypy
.response
.headers
["Content-Type"] = _format
338 elif "b" in data
.mode
: # binariy asssumig zip
339 cherrypy
.response
.headers
["Content-Type"] = 'application/zip'
341 cherrypy
.response
.headers
["Content-Type"] = 'text/plain'
342 # TODO check that cherrypy close file. If not implement pending things to close per thread next
345 if "application/json" in accept
:
346 cherrypy
.response
.headers
["Content-Type"] = 'application/json; charset=utf-8'
347 a
= json
.dumps(data
, indent
=4) + "\n"
348 return a
.encode("utf8")
349 elif "text/html" in accept
:
350 return html
.format(data
, cherrypy
.request
, cherrypy
.response
, session
)
352 elif "application/yaml" in accept
or "*/*" in accept
or "text/plain" in accept
:
355 raise cherrypy
.HTTPError(HTTPStatus
.NOT_ACCEPTABLE
.value
,
356 "Only 'Accept' of type 'application/json' or 'application/yaml' "
357 "for output format are available")
358 cherrypy
.response
.headers
["Content-Type"] = 'application/yaml'
359 return yaml
.safe_dump(data
, explicit_start
=True, indent
=4, default_flow_style
=False, tags
=False,
360 encoding
='utf-8', allow_unicode
=True) # , canonical=True, default_style='"'
363 def index(self
, *args
, **kwargs
):
366 if cherrypy
.request
.method
== "GET":
367 session
= self
.authenticator
.authorize()
368 outdata
= "Index page"
370 raise cherrypy
.HTTPError(HTTPStatus
.METHOD_NOT_ALLOWED
.value
,
371 "Method {} not allowed for tokens".format(cherrypy
.request
.method
))
373 return self
._format
_out
(outdata
, session
)
375 except EngineException
as e
:
376 cherrypy
.log("index Exception {}".format(e
))
377 cherrypy
.response
.status
= e
.http_code
.value
378 return self
._format
_out
("Welcome to OSM!", session
)
381 def version(self
, *args
, **kwargs
):
382 # TODO consider to remove and provide version using the static version file
383 global __version__
, version_date
385 if cherrypy
.request
.method
!= "GET":
386 raise NbiException("Only method GET is allowed", HTTPStatus
.METHOD_NOT_ALLOWED
)
388 raise NbiException("Invalid URL or query string for version", HTTPStatus
.METHOD_NOT_ALLOWED
)
389 return __version__
+ " " + version_date
390 except NbiException
as e
:
391 cherrypy
.response
.status
= e
.http_code
.value
393 "code": e
.http_code
.name
,
394 "status": e
.http_code
.value
,
397 return self
._format
_out
(problem_details
, None)
400 def token(self
, method
, token_id
=None, kwargs
=None):
402 # self.engine.load_dbase(cherrypy.request.app.config)
403 indata
= self
._format
_in
(kwargs
)
404 if not isinstance(indata
, dict):
405 raise NbiException("Expected application/yaml or application/json Content-Type", HTTPStatus
.BAD_REQUEST
)
408 session
= self
.authenticator
.authorize()
410 outdata
= self
.authenticator
.get_token(session
, token_id
)
412 outdata
= self
.authenticator
.get_token_list(session
)
413 elif method
== "POST":
415 session
= self
.authenticator
.authorize()
419 indata
.update(kwargs
)
420 outdata
= self
.authenticator
.new_token(session
, indata
, cherrypy
.request
.remote
)
422 cherrypy
.session
['Authorization'] = outdata
["_id"]
423 self
._set
_location
_header
("admin", "v1", "tokens", outdata
["_id"])
424 # cherrypy.response.cookie["Authorization"] = outdata["id"]
425 # cherrypy.response.cookie["Authorization"]['expires'] = 3600
426 elif method
== "DELETE":
427 if not token_id
and "id" in kwargs
:
428 token_id
= kwargs
["id"]
430 session
= self
.authenticator
.authorize()
431 token_id
= session
["_id"]
432 outdata
= self
.authenticator
.del_token(token_id
)
434 cherrypy
.session
['Authorization'] = "logout"
435 # cherrypy.response.cookie["Authorization"] = token_id
436 # cherrypy.response.cookie["Authorization"]['expires'] = 0
438 raise NbiException("Method {} not allowed for token".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
439 return self
._format
_out
(outdata
, session
)
440 except (NbiException
, EngineException
, DbException
) as e
:
441 cherrypy
.log("tokens Exception {}".format(e
))
442 cherrypy
.response
.status
= e
.http_code
.value
444 "code": e
.http_code
.name
,
445 "status": e
.http_code
.value
,
448 return self
._format
_out
(problem_details
, session
)
451 def test(self
, *args
, **kwargs
):
453 if args
and args
[0] == "help":
454 return "<html><pre>\ninit\nfile/<name> download file\ndb-clear/table\nprune\nlogin\nlogin2\n"\
455 "sleep/<time>\nmessage/topic\n</pre></html>"
457 elif args
and args
[0] == "init":
459 # self.engine.load_dbase(cherrypy.request.app.config)
460 self
.engine
.create_admin()
461 return "Done. User 'admin', password 'admin' created"
463 cherrypy
.response
.status
= HTTPStatus
.FORBIDDEN
.value
464 return self
._format
_out
("Database already initialized")
465 elif args
and args
[0] == "file":
466 return cherrypy
.lib
.static
.serve_file(cherrypy
.tree
.apps
['/osm'].config
["storage"]["path"] + "/" + args
[1],
467 "text/plain", "attachment")
468 elif args
and args
[0] == "file2":
469 f_path
= cherrypy
.tree
.apps
['/osm'].config
["storage"]["path"] + "/" + args
[1]
470 f
= open(f_path
, "r")
471 cherrypy
.response
.headers
["Content-type"] = "text/plain"
474 elif len(args
) == 2 and args
[0] == "db-clear":
475 return self
.engine
.del_item_list({"project_id": "admin"}, args
[1], {})
476 elif args
and args
[0] == "prune":
477 return self
.engine
.prune()
478 elif args
and args
[0] == "login":
479 if not cherrypy
.request
.headers
.get("Authorization"):
480 cherrypy
.response
.headers
["WWW-Authenticate"] = 'Basic realm="Access to OSM site", charset="UTF-8"'
481 cherrypy
.response
.status
= HTTPStatus
.UNAUTHORIZED
.value
482 elif args
and args
[0] == "login2":
483 if not cherrypy
.request
.headers
.get("Authorization"):
484 cherrypy
.response
.headers
["WWW-Authenticate"] = 'Bearer realm="Access to OSM site"'
485 cherrypy
.response
.status
= HTTPStatus
.UNAUTHORIZED
.value
486 elif args
and args
[0] == "sleep":
489 sleep_time
= int(args
[1])
491 cherrypy
.response
.status
= HTTPStatus
.FORBIDDEN
.value
492 return self
._format
_out
("Database already initialized")
493 thread_info
= cherrypy
.thread_data
495 time
.sleep(sleep_time
)
497 elif len(args
) >= 2 and args
[0] == "message":
499 return_text
= "<html><pre>{} ->\n".format(topic
)
501 if cherrypy
.request
.method
== 'POST':
502 to_send
= yaml
.load(cherrypy
.request
.body
)
503 for k
, v
in to_send
.items():
504 self
.engine
.msg
.write(topic
, k
, v
)
505 return_text
+= " {}: {}\n".format(k
, v
)
506 elif cherrypy
.request
.method
== 'GET':
507 for k
, v
in kwargs
.items():
508 self
.engine
.msg
.write(topic
, k
, yaml
.load(v
))
509 return_text
+= " {}: {}\n".format(k
, yaml
.load(v
))
510 except Exception as e
:
511 return_text
+= "Error: " + str(e
)
512 return_text
+= "</pre></html>\n"
516 "<html><pre>\nheaders:\n args: {}\n".format(args
) +
517 " kwargs: {}\n".format(kwargs
) +
518 " headers: {}\n".format(cherrypy
.request
.headers
) +
519 " path_info: {}\n".format(cherrypy
.request
.path_info
) +
520 " query_string: {}\n".format(cherrypy
.request
.query_string
) +
521 " session: {}\n".format(cherrypy
.session
) +
522 " cookie: {}\n".format(cherrypy
.request
.cookie
) +
523 " method: {}\n".format(cherrypy
.request
.method
) +
524 " session: {}\n".format(cherrypy
.session
.get('fieldname')) +
526 return_text
+= " length: {}\n".format(cherrypy
.request
.body
.length
)
527 if cherrypy
.request
.body
.length
:
528 return_text
+= " content: {}\n".format(
529 str(cherrypy
.request
.body
.read(int(cherrypy
.request
.headers
.get('Content-Length', 0)))))
531 return_text
+= "thread: {}\n".format(thread_info
)
532 return_text
+= "</pre></html>"
535 def _check_valid_url_method(self
, method
, *args
):
537 raise NbiException("URL must contain at least 'topic/version/item'", HTTPStatus
.METHOD_NOT_ALLOWED
)
539 reference
= self
.valid_methods
543 if not isinstance(reference
, dict):
544 raise NbiException("URL contains unexpected extra items '{}'".format(arg
),
545 HTTPStatus
.METHOD_NOT_ALLOWED
)
548 reference
= reference
[arg
]
549 elif "<ID>" in reference
:
550 reference
= reference
["<ID>"]
551 elif "*" in reference
:
552 reference
= reference
["*"]
555 raise NbiException("Unexpected URL item {}".format(arg
), HTTPStatus
.METHOD_NOT_ALLOWED
)
556 if "TODO" in reference
and method
in reference
["TODO"]:
557 raise NbiException("Method {} not supported yet for this URL".format(method
), HTTPStatus
.NOT_IMPLEMENTED
)
558 elif "METHODS" in reference
and method
not in reference
["METHODS"]:
559 raise NbiException("Method {} not supported for this URL".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
563 def _set_location_header(topic
, version
, item
, id):
565 Insert response header Location with the URL of created item base on URL params
572 # Use cherrypy.request.base for absoluted path and make use of request.header HOST just in case behind aNAT
573 cherrypy
.response
.headers
["Location"] = "/osm/{}/{}/{}/{}".format(topic
, version
, item
, id)
577 def default(self
, topic
=None, version
=None, item
=None, _id
=None, item2
=None, *args
, **kwargs
):
586 if not topic
or not version
or not item
:
587 raise NbiException("URL must contain at least 'topic/version/item'", HTTPStatus
.METHOD_NOT_ALLOWED
)
588 if topic
not in ("admin", "vnfpkgm", "nsd", "nslcm"):
589 raise NbiException("URL topic '{}' not supported".format(topic
), HTTPStatus
.METHOD_NOT_ALLOWED
)
591 raise NbiException("URL version '{}' not supported".format(version
), HTTPStatus
.METHOD_NOT_ALLOWED
)
593 if kwargs
and "METHOD" in kwargs
and kwargs
["METHOD"] in ("PUT", "POST", "DELETE", "GET", "PATCH"):
594 method
= kwargs
.pop("METHOD")
596 method
= cherrypy
.request
.method
597 if kwargs
and "FORCE" in kwargs
:
598 force
= kwargs
.pop("FORCE")
602 self
._check
_valid
_url
_method
(method
, topic
, version
, item
, _id
, item2
, *args
)
604 if topic
== "admin" and item
== "tokens":
605 return self
.token(method
, _id
, kwargs
)
607 # self.engine.load_dbase(cherrypy.request.app.config)
608 session
= self
.authenticator
.authorize()
609 indata
= self
._format
_in
(kwargs
)
611 if item
== "subscriptions":
612 engine_item
= topic
+ "_" + item
618 elif topic
== "vnfpkgm":
619 engine_item
= "vnfds"
620 elif topic
== "nslcm":
622 if item
== "ns_lcm_op_occs":
623 engine_item
= "nslcmops"
624 if item
== "vnfrs" or item
== "vnf_instances":
625 engine_item
= "vnfrs"
626 if engine_item
== "vims": # TODO this is for backward compatibility, it will remove in the future
627 engine_item
= "vim_accounts"
630 if item2
in ("nsd_content", "package_content", "artifacts", "vnfd", "nsd"):
631 if item2
in ("vnfd", "nsd"):
635 elif item2
== "artifacts":
639 file, _format
= self
.engine
.get_file(session
, engine_item
, _id
, path
,
640 cherrypy
.request
.headers
.get("Accept"))
643 outdata
= self
.engine
.get_item_list(session
, engine_item
, kwargs
)
645 outdata
= self
.engine
.get_item(session
, engine_item
, _id
)
646 elif method
== "POST":
647 if item
in ("ns_descriptors_content", "vnf_packages_content"):
648 _id
= cherrypy
.request
.headers
.get("Transaction-Id")
650 _id
= self
.engine
.new_item(rollback
, session
, engine_item
, {}, None, cherrypy
.request
.headers
,
652 completed
= self
.engine
.upload_content(session
, engine_item
, _id
, indata
, kwargs
,
653 cherrypy
.request
.headers
)
655 self
._set
_location
_header
(topic
, version
, item
, _id
)
657 cherrypy
.response
.headers
["Transaction-Id"] = _id
658 outdata
= {"id": _id
}
659 elif item
== "ns_instances_content":
660 _id
= self
.engine
.new_item(rollback
, session
, engine_item
, indata
, kwargs
, force
=force
)
661 self
.engine
.ns_operation(rollback
, session
, _id
, "instantiate", indata
, None)
662 self
._set
_location
_header
(topic
, version
, item
, _id
)
663 outdata
= {"id": _id
}
664 elif item
== "ns_instances" and item2
:
665 _id
= self
.engine
.ns_operation(rollback
, session
, _id
, item2
, indata
, kwargs
)
666 self
._set
_location
_header
(topic
, version
, "ns_lcm_op_occs", _id
)
667 outdata
= {"id": _id
}
668 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
670 _id
= self
.engine
.new_item(rollback
, session
, engine_item
, indata
, kwargs
, cherrypy
.request
.headers
,
672 self
._set
_location
_header
(topic
, version
, item
, _id
)
673 outdata
= {"id": _id
}
674 # TODO form NsdInfo when item in ("ns_descriptors", "vnf_packages")
675 cherrypy
.response
.status
= HTTPStatus
.CREATED
.value
677 elif method
== "DELETE":
679 outdata
= self
.engine
.del_item_list(session
, engine_item
, kwargs
)
680 cherrypy
.response
.status
= HTTPStatus
.OK
.value
681 else: # len(args) > 1
682 if item
== "ns_instances_content" and not force
:
683 opp_id
= self
.engine
.ns_operation(rollback
, session
, _id
, "terminate", {"autoremove": True},
685 outdata
= {"_id": opp_id
}
686 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
688 self
.engine
.del_item(session
, engine_item
, _id
, force
)
689 cherrypy
.response
.status
= HTTPStatus
.NO_CONTENT
.value
690 if engine_item
in ("vim_accounts", "sdns"):
691 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
693 elif method
in ("PUT", "PATCH"):
694 if not indata
and not kwargs
:
695 raise NbiException("Nothing to update. Provide payload and/or query string",
696 HTTPStatus
.BAD_REQUEST
)
697 if item2
in ("nsd_content", "package_content") and method
== "PUT":
698 completed
= self
.engine
.upload_content(session
, engine_item
, _id
, indata
, kwargs
,
699 cherrypy
.request
.headers
)
701 cherrypy
.response
.headers
["Transaction-Id"] = id
702 cherrypy
.response
.status
= HTTPStatus
.NO_CONTENT
.value
705 outdata
= {"id": self
.engine
.edit_item(session
, engine_item
, _id
, indata
, kwargs
, force
=force
)}
707 raise NbiException("Method {} not allowed".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
708 return self
._format
_out
(outdata
, session
, _format
)
709 except (NbiException
, EngineException
, DbException
, FsException
, MsgException
) as e
:
710 cherrypy
.log("Exception {}".format(e
))
711 cherrypy
.response
.status
= e
.http_code
.value
712 if hasattr(outdata
, "close"): # is an open file
714 for rollback_item
in rollback
:
716 self
.engine
.del_item(**rollback_item
, session
=session
, force
=True)
717 except Exception as e2
:
718 cherrypy
.log("Rollback Exception {}: {}".format(rollback_item
, e2
))
720 if isinstance(e
, MsgException
):
721 error_text
= "{} has been '{}' but other modules cannot be informed because an error on bus".format(
722 engine_item
[:-1], method
, error_text
)
724 "code": e
.http_code
.name
,
725 "status": e
.http_code
.value
,
728 return self
._format
_out
(problem_details
, session
)
729 # raise cherrypy.HTTPError(e.http_code.value, str(e))
732 # def validate_password(realm, username, password):
733 # cherrypy.log("realm "+ str(realm))
734 # if username == "admin" and password == "admin":
739 def _start_service():
741 Callback function called when cherrypy.engine starts
742 Override configuration with env variables
743 Set database, storage, message configuration
744 Init database with admin/admin user password
746 cherrypy
.log
.error("Starting osm_nbi")
747 # update general cherrypy configuration
750 engine_config
= cherrypy
.tree
.apps
['/osm'].config
751 for k
, v
in environ
.items():
752 if not k
.startswith("OSMNBI_"):
754 k1
, _
, k2
= k
[7:].lower().partition("_")
758 # update static configuration
759 if k
== 'OSMNBI_STATIC_DIR':
760 engine_config
["/static"]['tools.staticdir.dir'] = v
761 engine_config
["/static"]['tools.staticdir.on'] = True
762 elif k
== 'OSMNBI_SOCKET_PORT' or k
== 'OSMNBI_SERVER_PORT':
763 update_dict
['server.socket_port'] = int(v
)
764 elif k
== 'OSMNBI_SOCKET_HOST' or k
== 'OSMNBI_SERVER_HOST':
765 update_dict
['server.socket_host'] = v
766 elif k1
in ("server", "test", "auth", "log"):
767 update_dict
[k1
+ '.' + k2
] = v
768 elif k1
in ("message", "database", "storage", "authenticator"):
769 # k2 = k2.replace('_', '.')
770 if k2
in ("port", "db_port"):
771 engine_config
[k1
][k2
] = int(v
)
773 engine_config
[k1
][k2
] = v
775 except ValueError as e
:
776 cherrypy
.log
.error("Ignoring environ '{}': " + str(e
))
777 except Exception as e
:
778 cherrypy
.log
.warn("skipping environ '{}' on exception '{}'".format(k
, e
))
781 cherrypy
.config
.update(update_dict
)
782 engine_config
["global"].update(update_dict
)
785 log_format_simple
= "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(message)s"
786 log_formatter_simple
= logging
.Formatter(log_format_simple
, datefmt
='%Y-%m-%dT%H:%M:%S')
787 logger_server
= logging
.getLogger("cherrypy.error")
788 logger_access
= logging
.getLogger("cherrypy.access")
789 logger_cherry
= logging
.getLogger("cherrypy")
790 logger_nbi
= logging
.getLogger("nbi")
792 if "log.file" in engine_config
["global"]:
793 file_handler
= logging
.handlers
.RotatingFileHandler(engine_config
["global"]["log.file"],
794 maxBytes
=100e6
, backupCount
=9, delay
=0)
795 file_handler
.setFormatter(log_formatter_simple
)
796 logger_cherry
.addHandler(file_handler
)
797 logger_nbi
.addHandler(file_handler
)
799 for format_
, logger
in {"nbi.server": logger_server
,
800 "nbi.access": logger_access
,
801 "%(name)s %(filename)s:%(lineno)s": logger_nbi
803 log_format_cherry
= "%(asctime)s %(levelname)s {} %(message)s".format(format_
)
804 log_formatter_cherry
= logging
.Formatter(log_format_cherry
, datefmt
='%Y-%m-%dT%H:%M:%S')
805 str_handler
= logging
.StreamHandler()
806 str_handler
.setFormatter(log_formatter_cherry
)
807 logger
.addHandler(str_handler
)
809 if engine_config
["global"].get("log.level"):
810 logger_cherry
.setLevel(engine_config
["global"]["log.level"])
811 logger_nbi
.setLevel(engine_config
["global"]["log.level"])
813 # logging other modules
814 for k1
, logname
in {"message": "nbi.msg", "database": "nbi.db", "storage": "nbi.fs"}.items():
815 engine_config
[k1
]["logger_name"] = logname
816 logger_module
= logging
.getLogger(logname
)
817 if "logfile" in engine_config
[k1
]:
818 file_handler
= logging
.handlers
.RotatingFileHandler(engine_config
[k1
]["logfile"],
819 maxBytes
=100e6
, backupCount
=9, delay
=0)
820 file_handler
.setFormatter(log_formatter_simple
)
821 logger_module
.addHandler(file_handler
)
822 if "loglevel" in engine_config
[k1
]:
823 logger_module
.setLevel(engine_config
[k1
]["loglevel"])
824 # TODO add more entries, e.g.: storage
825 cherrypy
.tree
.apps
['/osm'].root
.engine
.start(engine_config
)
826 cherrypy
.tree
.apps
['/osm'].root
.authenticator
.start(engine_config
)
828 cherrypy
.tree
.apps
['/osm'].root
.engine
.init_db(target_version
=database_version
)
829 cherrypy
.tree
.apps
['/osm'].root
.authenticator
.init_db(target_version
=auth_database_version
)
830 except (EngineException
, AuthException
):
832 # getenv('OSMOPENMANO_TENANT', None)
837 Callback function called when cherrypy.engine stops
838 TODO: Ending database connections.
840 cherrypy
.tree
.apps
['/osm'].root
.engine
.stop()
841 cherrypy
.log
.error("Stopping osm_nbi")
844 def nbi(config_file
):
847 # #'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
848 # 'tools.sessions.on': True,
849 # 'tools.response_headers.on': True,
850 # # 'tools.response_headers.headers': [('Content-Type', 'text/plain')],
853 # cherrypy.Server.ssl_module = 'builtin'
854 # cherrypy.Server.ssl_certificate = "http/cert.pem"
855 # cherrypy.Server.ssl_private_key = "http/privkey.pem"
856 # cherrypy.Server.thread_pool = 10
857 # cherrypy.config.update({'Server.socket_port': config["port"], 'Server.socket_host': config["host"]})
859 # cherrypy.config.update({'tools.auth_basic.on': True,
860 # 'tools.auth_basic.realm': 'localhost',
861 # 'tools.auth_basic.checkpassword': validate_password})
862 cherrypy
.engine
.subscribe('start', _start_service
)
863 cherrypy
.engine
.subscribe('stop', _stop_service
)
864 cherrypy
.quickstart(Server(), '/osm', config_file
)
868 print("""Usage: {} [options]
869 -c|--config [configuration_file]: loads the configuration file (default: ./nbi.cfg)
870 -h|--help: shows this help
871 """.format(sys
.argv
[0]))
872 # --log-socket-host HOST: send logs to this host")
873 # --log-socket-port PORT: send logs using this port (default: 9022)")
876 if __name__
== '__main__':
878 # load parameters and configuration
879 opts
, args
= getopt
.getopt(sys
.argv
[1:], "hvc:", ["config=", "help"])
880 # TODO add "log-socket-host=", "log-socket-port=", "log-file="
883 if o
in ("-h", "--help"):
886 elif o
in ("-c", "--config"):
888 # elif o == "--log-socket-port":
889 # log_socket_port = a
890 # elif o == "--log-socket-host":
891 # log_socket_host = a
892 # elif o == "--log-file":
895 assert False, "Unhandled option"
897 if not path
.isfile(config_file
):
898 print("configuration file '{}' that not exist".format(config_file
), file=sys
.stderr
)
901 for config_file
in (__file__
[:__file__
.rfind(".")] + ".cfg", "./nbi.cfg", "/etc/osm/nbi.cfg"):
902 if path
.isfile(config_file
):
905 print("No configuration file 'nbi.cfg' found neither at local folder nor at /etc/osm/", file=sys
.stderr
)
908 except getopt
.GetoptError
as e
:
909 print(str(e
), file=sys
.stderr
)