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
85 /vims_accounts (also vims for compatibility) O O
91 Follows SOL005 section 4.3.2 It contains extra METHOD to override http method, FORCE to force.
92 For filtering inside array, it must select the element of the array, or add ANYINDEX to apply the filtering over any
93 item of the array, that is, pass if any item of the array pass the filter.
94 It allows both ne and neq for not equal
95 TODO: 4.3.3 Attribute selectors
96 all_fields, fields=x,y,.., exclude_default, exclude_fields=x,y,...
97 (none) … same as “exclude_default”
98 all_fields … all attributes.
99 fields=<list> … all attributes except all complex attributes with minimum cardinality of zero that are not
100 conditionally mandatory, and that are not provided in <list>.
101 exclude_fields=<list> … all attributes except those complex attributes with a minimum cardinality of zero that
102 are not conditionally mandatory, and that are provided in <list>.
103 exclude_default … all attributes except those complex attributes with a minimum cardinality of zero that are not
104 conditionally mandatory, and that are part of the "default exclude set" defined in the present specification for
105 the particular resource
106 exclude_default and include=<list> … all attributes except those complex attributes with a minimum cardinality
107 of zero that are not conditionally mandatory and that are part of the "default exclude set" defined in the
108 present specification for the particular resource, but that are not part of <list>
109 Header field name Reference Example Descriptions
110 Accept IETF RFC 7231 [19] application/json Content-Types that are acceptable for the response.
111 This header field shall be present if the response is expected to have a non-empty message body.
112 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the request.
113 This header field shall be present if the request has a non-empty message body.
114 Authorization IETF RFC 7235 [22] Bearer mF_9.B5f-4.1JqM The authorization token for the request.
115 Details are specified in clause 4.5.3.
116 Range IETF RFC 7233 [21] 1000-2000 Requested range of bytes from a file
117 Header field name Reference Example Descriptions
118 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the response.
119 This header field shall be present if the response has a non-empty message body.
120 Location IETF RFC 7231 [19] http://www.example.com/vnflcm/v1/vnf_instances/123 Used in redirection, or when a
121 new resource has been created.
122 This header field shall be present if the response status code is 201 or 3xx.
123 In the present document this header field is also used if the response status code is 202 and a new resource was
125 WWW-Authenticate IETF RFC 7235 [22] Bearer realm="example" Challenge if the corresponding HTTP request has not
126 provided authorization, or error details if the corresponding HTTP request has provided an invalid authorization
128 Accept-Ranges IETF RFC 7233 [21] bytes Used by the Server to signal whether or not it supports ranges for
130 Content-Range IETF RFC 7233 [21] bytes 21010-47021/ 47022 Signals the byte range that is contained in the
131 response, and the total length of the file.
132 Retry-After IETF RFC 7231 [19] Fri, 31 Dec 1999 23:59:59 GMT
136 class NbiException(Exception):
138 def __init__(self
, message
, http_code
=HTTPStatus
.METHOD_NOT_ALLOWED
):
139 Exception.__init
__(self
, message
)
140 self
.http_code
= http_code
143 class Server(object):
145 # to decode bytes to str
146 reader
= getreader("utf-8")
150 self
.engine
= Engine()
151 self
.authenticator
= Authenticator()
152 self
.valid_methods
= { # contains allowed URL and methods
155 "tokens": {"METHODS": ("GET", "POST", "DELETE"),
156 "<ID>": {"METHODS": ("GET", "DELETE")}
158 "users": {"METHODS": ("GET", "POST"),
159 "<ID>": {"METHODS": ("GET", "POST", "DELETE", "PATCH", "PUT")}
161 "projects": {"METHODS": ("GET", "POST"),
162 "<ID>": {"METHODS": ("GET", "DELETE")}
164 "vims": {"METHODS": ("GET", "POST"),
165 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
167 "vim_accounts": {"METHODS": ("GET", "POST"),
168 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
170 "sdns": {"METHODS": ("GET", "POST"),
171 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
177 "ns_descriptors_content": {"METHODS": ("GET", "POST"),
178 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
180 "ns_descriptors": {"METHODS": ("GET", "POST"),
181 "<ID>": {"METHODS": ("GET", "DELETE"), "TODO": "PATCH",
182 "nsd_content": {"METHODS": ("GET", "PUT")},
183 "nsd": {"METHODS": "GET"}, # descriptor inside package
184 "artifacts": {"*": {"METHODS": "GET"}}
187 "pnf_descriptors": {"TODO": ("GET", "POST"),
188 "<ID>": {"TODO": ("GET", "DELETE", "PATCH"),
189 "pnfd_content": {"TODO": ("GET", "PUT")}
192 "subscriptions": {"TODO": ("GET", "POST"),
193 "<ID>": {"TODO": ("GET", "DELETE")}
199 "vnf_packages_content": {"METHODS": ("GET", "POST"),
200 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
202 "vnf_packages": {"METHODS": ("GET", "POST"),
203 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"), # GET: vnfPkgInfo
204 "package_content": {"METHODS": ("GET", "PUT"), # package
205 "upload_from_uri": {"TODO": "POST"}
207 "vnfd": {"METHODS": "GET"}, # descriptor inside package
208 "artifacts": {"*": {"METHODS": "GET"}}
211 "subscriptions": {"TODO": ("GET", "POST"),
212 "<ID>": {"TODO": ("GET", "DELETE")}
218 "ns_instances_content": {"METHODS": ("GET", "POST"),
219 "<ID>": {"METHODS": ("GET", "DELETE")}
221 "ns_instances": {"METHODS": ("GET", "POST"),
222 "<ID>": {"METHODS": ("GET", "DELETE"),
223 "scale": {"METHODS": "POST"},
224 "terminate": {"METHODS": "POST"},
225 "instantiate": {"METHODS": "POST"},
226 "action": {"METHODS": "POST"},
229 "ns_lcm_op_occs": {"METHODS": "GET",
230 "<ID>": {"METHODS": "GET"},
232 "vnfrs": {"METHODS": ("GET"),
233 "<ID>": {"METHODS": ("GET")}
235 "vnf_instances": {"METHODS": ("GET"),
236 "<ID>": {"METHODS": ("GET")}
242 def _format_in(self
, kwargs
):
245 if cherrypy
.request
.body
.length
:
246 error_text
= "Invalid input format "
248 if "Content-Type" in cherrypy
.request
.headers
:
249 if "application/json" in cherrypy
.request
.headers
["Content-Type"]:
250 error_text
= "Invalid json format "
251 indata
= json
.load(self
.reader(cherrypy
.request
.body
))
252 elif "application/yaml" in cherrypy
.request
.headers
["Content-Type"]:
253 error_text
= "Invalid yaml format "
254 indata
= yaml
.load(cherrypy
.request
.body
)
255 elif "application/binary" in cherrypy
.request
.headers
["Content-Type"] or \
256 "application/gzip" in cherrypy
.request
.headers
["Content-Type"] or \
257 "application/zip" in cherrypy
.request
.headers
["Content-Type"] or \
258 "text/plain" in cherrypy
.request
.headers
["Content-Type"]:
259 indata
= cherrypy
.request
.body
# .read()
260 elif "multipart/form-data" in cherrypy
.request
.headers
["Content-Type"]:
261 if "descriptor_file" in kwargs
:
262 filecontent
= kwargs
.pop("descriptor_file")
263 if not filecontent
.file:
264 raise NbiException("empty file or content", HTTPStatus
.BAD_REQUEST
)
265 indata
= filecontent
.file # .read()
266 if filecontent
.content_type
.value
:
267 cherrypy
.request
.headers
["Content-Type"] = filecontent
.content_type
.value
269 # raise cherrypy.HTTPError(HTTPStatus.Not_Acceptable,
270 # "Only 'Content-Type' of type 'application/json' or
271 # 'application/yaml' for input format are available")
272 error_text
= "Invalid yaml format "
273 indata
= yaml
.load(cherrypy
.request
.body
)
275 error_text
= "Invalid yaml format "
276 indata
= yaml
.load(cherrypy
.request
.body
)
281 if cherrypy
.request
.headers
.get("Query-String-Format") == "yaml":
284 for k
, v
in kwargs
.items():
285 if isinstance(v
, str):
290 kwargs
[k
] = yaml
.load(v
)
293 elif k
.endswith(".gt") or k
.endswith(".lt") or k
.endswith(".gte") or k
.endswith(".lte"):
301 elif v
.find(",") > 0:
302 kwargs
[k
] = v
.split(",")
303 elif isinstance(v
, (list, tuple)):
304 for index
in range(0, len(v
)):
309 v
[index
] = yaml
.load(v
[index
])
314 except (ValueError, yaml
.YAMLError
) as exc
:
315 raise NbiException(error_text
+ str(exc
), HTTPStatus
.BAD_REQUEST
)
316 except KeyError as exc
:
317 raise NbiException("Query string error: " + str(exc
), HTTPStatus
.BAD_REQUEST
)
318 except Exception as exc
:
319 raise NbiException(error_text
+ str(exc
), HTTPStatus
.BAD_REQUEST
)
322 def _format_out(data
, session
=None, _format
=None):
324 return string of dictionary data according to requested json, yaml, xml. By default json
325 :param data: response to be sent. Can be a dict, text or file
327 :param _format: The format to be set as Content-Type ir data is a file
330 accept
= cherrypy
.request
.headers
.get("Accept")
332 if accept
and "text/html" in accept
:
333 return html
.format(data
, cherrypy
.request
, cherrypy
.response
, session
)
334 # cherrypy.response.status = HTTPStatus.NO_CONTENT.value
336 elif hasattr(data
, "read"): # file object
338 cherrypy
.response
.headers
["Content-Type"] = _format
339 elif "b" in data
.mode
: # binariy asssumig zip
340 cherrypy
.response
.headers
["Content-Type"] = 'application/zip'
342 cherrypy
.response
.headers
["Content-Type"] = 'text/plain'
343 # TODO check that cherrypy close file. If not implement pending things to close per thread next
346 if "application/json" in accept
:
347 cherrypy
.response
.headers
["Content-Type"] = 'application/json; charset=utf-8'
348 a
= json
.dumps(data
, indent
=4) + "\n"
349 return a
.encode("utf8")
350 elif "text/html" in accept
:
351 return html
.format(data
, cherrypy
.request
, cherrypy
.response
, session
)
353 elif "application/yaml" in accept
or "*/*" in accept
or "text/plain" in accept
:
356 raise cherrypy
.HTTPError(HTTPStatus
.NOT_ACCEPTABLE
.value
,
357 "Only 'Accept' of type 'application/json' or 'application/yaml' "
358 "for output format are available")
359 cherrypy
.response
.headers
["Content-Type"] = 'application/yaml'
360 return yaml
.safe_dump(data
, explicit_start
=True, indent
=4, default_flow_style
=False, tags
=False,
361 encoding
='utf-8', allow_unicode
=True) # , canonical=True, default_style='"'
364 def index(self
, *args
, **kwargs
):
367 if cherrypy
.request
.method
== "GET":
368 session
= self
.authenticator
.authorize()
369 outdata
= "Index page"
371 raise cherrypy
.HTTPError(HTTPStatus
.METHOD_NOT_ALLOWED
.value
,
372 "Method {} not allowed for tokens".format(cherrypy
.request
.method
))
374 return self
._format
_out
(outdata
, session
)
376 except (EngineException
, AuthException
) as e
:
377 cherrypy
.log("index Exception {}".format(e
))
378 cherrypy
.response
.status
= e
.http_code
.value
379 return self
._format
_out
("Welcome to OSM!", session
)
382 def version(self
, *args
, **kwargs
):
383 # TODO consider to remove and provide version using the static version file
384 global __version__
, version_date
386 if cherrypy
.request
.method
!= "GET":
387 raise NbiException("Only method GET is allowed", HTTPStatus
.METHOD_NOT_ALLOWED
)
389 raise NbiException("Invalid URL or query string for version", HTTPStatus
.METHOD_NOT_ALLOWED
)
390 return __version__
+ " " + version_date
391 except NbiException
as e
:
392 cherrypy
.response
.status
= e
.http_code
.value
394 "code": e
.http_code
.name
,
395 "status": e
.http_code
.value
,
398 return self
._format
_out
(problem_details
, None)
401 def token(self
, method
, token_id
=None, kwargs
=None):
403 # self.engine.load_dbase(cherrypy.request.app.config)
404 indata
= self
._format
_in
(kwargs
)
405 if not isinstance(indata
, dict):
406 raise NbiException("Expected application/yaml or application/json Content-Type", HTTPStatus
.BAD_REQUEST
)
409 session
= self
.authenticator
.authorize()
411 outdata
= self
.authenticator
.get_token(session
, token_id
)
413 outdata
= self
.authenticator
.get_token_list(session
)
414 elif method
== "POST":
416 session
= self
.authenticator
.authorize()
420 indata
.update(kwargs
)
421 outdata
= self
.authenticator
.new_token(session
, indata
, cherrypy
.request
.remote
)
423 cherrypy
.session
['Authorization'] = outdata
["_id"]
424 self
._set
_location
_header
("admin", "v1", "tokens", outdata
["_id"])
425 # cherrypy.response.cookie["Authorization"] = outdata["id"]
426 # cherrypy.response.cookie["Authorization"]['expires'] = 3600
427 elif method
== "DELETE":
428 if not token_id
and "id" in kwargs
:
429 token_id
= kwargs
["id"]
431 session
= self
.authenticator
.authorize()
432 token_id
= session
["_id"]
433 outdata
= self
.authenticator
.del_token(token_id
)
435 cherrypy
.session
['Authorization'] = "logout"
436 # cherrypy.response.cookie["Authorization"] = token_id
437 # cherrypy.response.cookie["Authorization"]['expires'] = 0
439 raise NbiException("Method {} not allowed for token".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
440 return self
._format
_out
(outdata
, session
)
441 except (NbiException
, EngineException
, DbException
, AuthException
) as e
:
442 cherrypy
.log("tokens Exception {}".format(e
))
443 cherrypy
.response
.status
= e
.http_code
.value
445 "code": e
.http_code
.name
,
446 "status": e
.http_code
.value
,
449 return self
._format
_out
(problem_details
, session
)
452 def test(self
, *args
, **kwargs
):
454 if args
and args
[0] == "help":
455 return "<html><pre>\ninit\nfile/<name> download file\ndb-clear/table\nprune\nlogin\nlogin2\n"\
456 "sleep/<time>\nmessage/topic\n</pre></html>"
458 elif args
and args
[0] == "init":
460 # self.engine.load_dbase(cherrypy.request.app.config)
461 self
.engine
.create_admin()
462 return "Done. User 'admin', password 'admin' created"
464 cherrypy
.response
.status
= HTTPStatus
.FORBIDDEN
.value
465 return self
._format
_out
("Database already initialized")
466 elif args
and args
[0] == "file":
467 return cherrypy
.lib
.static
.serve_file(cherrypy
.tree
.apps
['/osm'].config
["storage"]["path"] + "/" + args
[1],
468 "text/plain", "attachment")
469 elif args
and args
[0] == "file2":
470 f_path
= cherrypy
.tree
.apps
['/osm'].config
["storage"]["path"] + "/" + args
[1]
471 f
= open(f_path
, "r")
472 cherrypy
.response
.headers
["Content-type"] = "text/plain"
475 elif len(args
) == 2 and args
[0] == "db-clear":
476 return self
.engine
.del_item_list({"project_id": "admin", "admin": True}, args
[1], kwargs
)
477 elif args
and args
[0] == "prune":
478 return self
.engine
.prune()
479 elif args
and args
[0] == "login":
480 if not cherrypy
.request
.headers
.get("Authorization"):
481 cherrypy
.response
.headers
["WWW-Authenticate"] = 'Basic realm="Access to OSM site", charset="UTF-8"'
482 cherrypy
.response
.status
= HTTPStatus
.UNAUTHORIZED
.value
483 elif args
and args
[0] == "login2":
484 if not cherrypy
.request
.headers
.get("Authorization"):
485 cherrypy
.response
.headers
["WWW-Authenticate"] = 'Bearer realm="Access to OSM site"'
486 cherrypy
.response
.status
= HTTPStatus
.UNAUTHORIZED
.value
487 elif args
and args
[0] == "sleep":
490 sleep_time
= int(args
[1])
492 cherrypy
.response
.status
= HTTPStatus
.FORBIDDEN
.value
493 return self
._format
_out
("Database already initialized")
494 thread_info
= cherrypy
.thread_data
496 time
.sleep(sleep_time
)
498 elif len(args
) >= 2 and args
[0] == "message":
500 return_text
= "<html><pre>{} ->\n".format(topic
)
502 if cherrypy
.request
.method
== 'POST':
503 to_send
= yaml
.load(cherrypy
.request
.body
)
504 for k
, v
in to_send
.items():
505 self
.engine
.msg
.write(topic
, k
, v
)
506 return_text
+= " {}: {}\n".format(k
, v
)
507 elif cherrypy
.request
.method
== 'GET':
508 for k
, v
in kwargs
.items():
509 self
.engine
.msg
.write(topic
, k
, yaml
.load(v
))
510 return_text
+= " {}: {}\n".format(k
, yaml
.load(v
))
511 except Exception as e
:
512 return_text
+= "Error: " + str(e
)
513 return_text
+= "</pre></html>\n"
517 "<html><pre>\nheaders:\n args: {}\n".format(args
) +
518 " kwargs: {}\n".format(kwargs
) +
519 " headers: {}\n".format(cherrypy
.request
.headers
) +
520 " path_info: {}\n".format(cherrypy
.request
.path_info
) +
521 " query_string: {}\n".format(cherrypy
.request
.query_string
) +
522 " session: {}\n".format(cherrypy
.session
) +
523 " cookie: {}\n".format(cherrypy
.request
.cookie
) +
524 " method: {}\n".format(cherrypy
.request
.method
) +
525 " session: {}\n".format(cherrypy
.session
.get('fieldname')) +
527 return_text
+= " length: {}\n".format(cherrypy
.request
.body
.length
)
528 if cherrypy
.request
.body
.length
:
529 return_text
+= " content: {}\n".format(
530 str(cherrypy
.request
.body
.read(int(cherrypy
.request
.headers
.get('Content-Length', 0)))))
532 return_text
+= "thread: {}\n".format(thread_info
)
533 return_text
+= "</pre></html>"
536 def _check_valid_url_method(self
, method
, *args
):
538 raise NbiException("URL must contain at least 'topic/version/item'", HTTPStatus
.METHOD_NOT_ALLOWED
)
540 reference
= self
.valid_methods
544 if not isinstance(reference
, dict):
545 raise NbiException("URL contains unexpected extra items '{}'".format(arg
),
546 HTTPStatus
.METHOD_NOT_ALLOWED
)
549 reference
= reference
[arg
]
550 elif "<ID>" in reference
:
551 reference
= reference
["<ID>"]
552 elif "*" in reference
:
553 reference
= reference
["*"]
556 raise NbiException("Unexpected URL item {}".format(arg
), HTTPStatus
.METHOD_NOT_ALLOWED
)
557 if "TODO" in reference
and method
in reference
["TODO"]:
558 raise NbiException("Method {} not supported yet for this URL".format(method
), HTTPStatus
.NOT_IMPLEMENTED
)
559 elif "METHODS" in reference
and method
not in reference
["METHODS"]:
560 raise NbiException("Method {} not supported for this URL".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
564 def _set_location_header(topic
, version
, item
, id):
566 Insert response header Location with the URL of created item base on URL params
573 # Use cherrypy.request.base for absoluted path and make use of request.header HOST just in case behind aNAT
574 cherrypy
.response
.headers
["Location"] = "/osm/{}/{}/{}/{}".format(topic
, version
, item
, id)
578 def default(self
, topic
=None, version
=None, item
=None, _id
=None, item2
=None, *args
, **kwargs
):
587 if not topic
or not version
or not item
:
588 raise NbiException("URL must contain at least 'topic/version/item'", HTTPStatus
.METHOD_NOT_ALLOWED
)
589 if topic
not in ("admin", "vnfpkgm", "nsd", "nslcm"):
590 raise NbiException("URL topic '{}' not supported".format(topic
), HTTPStatus
.METHOD_NOT_ALLOWED
)
592 raise NbiException("URL version '{}' not supported".format(version
), HTTPStatus
.METHOD_NOT_ALLOWED
)
594 if kwargs
and "METHOD" in kwargs
and kwargs
["METHOD"] in ("PUT", "POST", "DELETE", "GET", "PATCH"):
595 method
= kwargs
.pop("METHOD")
597 method
= cherrypy
.request
.method
598 if kwargs
and "FORCE" in kwargs
:
599 force
= kwargs
.pop("FORCE")
603 self
._check
_valid
_url
_method
(method
, topic
, version
, item
, _id
, item2
, *args
)
605 if topic
== "admin" and item
== "tokens":
606 return self
.token(method
, _id
, kwargs
)
608 # self.engine.load_dbase(cherrypy.request.app.config)
609 session
= self
.authenticator
.authorize()
610 indata
= self
._format
_in
(kwargs
)
612 if item
== "subscriptions":
613 engine_item
= topic
+ "_" + item
619 elif topic
== "vnfpkgm":
620 engine_item
= "vnfds"
621 elif topic
== "nslcm":
623 if item
== "ns_lcm_op_occs":
624 engine_item
= "nslcmops"
625 if item
== "vnfrs" or item
== "vnf_instances":
626 engine_item
= "vnfrs"
627 if engine_item
== "vims": # TODO this is for backward compatibility, it will remove in the future
628 engine_item
= "vim_accounts"
631 if item2
in ("nsd_content", "package_content", "artifacts", "vnfd", "nsd"):
632 if item2
in ("vnfd", "nsd"):
636 elif item2
== "artifacts":
640 file, _format
= self
.engine
.get_file(session
, engine_item
, _id
, path
,
641 cherrypy
.request
.headers
.get("Accept"))
644 outdata
= self
.engine
.get_item_list(session
, engine_item
, kwargs
)
646 outdata
= self
.engine
.get_item(session
, engine_item
, _id
)
647 elif method
== "POST":
648 if item
in ("ns_descriptors_content", "vnf_packages_content"):
649 _id
= cherrypy
.request
.headers
.get("Transaction-Id")
651 _id
= self
.engine
.new_item(rollback
, session
, engine_item
, {}, None, cherrypy
.request
.headers
,
653 completed
= self
.engine
.upload_content(session
, engine_item
, _id
, indata
, kwargs
,
654 cherrypy
.request
.headers
)
656 self
._set
_location
_header
(topic
, version
, item
, _id
)
658 cherrypy
.response
.headers
["Transaction-Id"] = _id
659 outdata
= {"id": _id
}
660 elif item
== "ns_instances_content":
661 _id
= self
.engine
.new_item(rollback
, session
, engine_item
, indata
, kwargs
, force
=force
)
662 self
.engine
.ns_operation(rollback
, session
, _id
, "instantiate", indata
, None)
663 self
._set
_location
_header
(topic
, version
, item
, _id
)
664 outdata
= {"id": _id
}
665 elif item
== "ns_instances" and item2
:
666 _id
= self
.engine
.ns_operation(rollback
, session
, _id
, item2
, indata
, kwargs
)
667 self
._set
_location
_header
(topic
, version
, "ns_lcm_op_occs", _id
)
668 outdata
= {"id": _id
}
669 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
671 _id
= self
.engine
.new_item(rollback
, session
, engine_item
, indata
, kwargs
, cherrypy
.request
.headers
,
673 self
._set
_location
_header
(topic
, version
, item
, _id
)
674 outdata
= {"id": _id
}
675 # TODO form NsdInfo when item in ("ns_descriptors", "vnf_packages")
676 cherrypy
.response
.status
= HTTPStatus
.CREATED
.value
678 elif method
== "DELETE":
680 outdata
= self
.engine
.del_item_list(session
, engine_item
, kwargs
)
681 cherrypy
.response
.status
= HTTPStatus
.OK
.value
682 else: # len(args) > 1
683 if item
== "ns_instances_content" and not force
:
684 opp_id
= self
.engine
.ns_operation(rollback
, session
, _id
, "terminate", {"autoremove": True},
686 outdata
= {"_id": opp_id
}
687 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
689 self
.engine
.del_item(session
, engine_item
, _id
, force
)
690 cherrypy
.response
.status
= HTTPStatus
.NO_CONTENT
.value
691 if engine_item
in ("vim_accounts", "sdns"):
692 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
694 elif method
in ("PUT", "PATCH"):
696 if not indata
and not kwargs
:
697 raise NbiException("Nothing to update. Provide payload and/or query string",
698 HTTPStatus
.BAD_REQUEST
)
699 if item2
in ("nsd_content", "package_content") and method
== "PUT":
700 completed
= self
.engine
.upload_content(session
, engine_item
, _id
, indata
, kwargs
,
701 cherrypy
.request
.headers
)
703 cherrypy
.response
.headers
["Transaction-Id"] = id
705 self
.engine
.edit_item(session
, engine_item
, _id
, indata
, kwargs
, force
=force
)
706 cherrypy
.response
.status
= HTTPStatus
.NO_CONTENT
.value
708 raise NbiException("Method {} not allowed".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
709 return self
._format
_out
(outdata
, session
, _format
)
710 except (NbiException
, EngineException
, DbException
, FsException
, MsgException
, AuthException
) as e
:
711 cherrypy
.log("Exception {}".format(e
))
712 cherrypy
.response
.status
= e
.http_code
.value
713 if hasattr(outdata
, "close"): # is an open file
715 for rollback_item
in rollback
:
717 self
.engine
.del_item(**rollback_item
, session
=session
, force
=True)
718 except Exception as e2
:
719 cherrypy
.log("Rollback Exception {}: {}".format(rollback_item
, e2
))
721 if isinstance(e
, MsgException
):
722 error_text
= "{} has been '{}' but other modules cannot be informed because an error on bus".format(
723 engine_item
[:-1], method
, error_text
)
725 "code": e
.http_code
.name
,
726 "status": e
.http_code
.value
,
729 return self
._format
_out
(problem_details
, session
)
730 # raise cherrypy.HTTPError(e.http_code.value, str(e))
733 # def validate_password(realm, username, password):
734 # cherrypy.log("realm "+ str(realm))
735 # if username == "admin" and password == "admin":
740 def _start_service():
742 Callback function called when cherrypy.engine starts
743 Override configuration with env variables
744 Set database, storage, message configuration
745 Init database with admin/admin user password
747 cherrypy
.log
.error("Starting osm_nbi")
748 # update general cherrypy configuration
751 engine_config
= cherrypy
.tree
.apps
['/osm'].config
752 for k
, v
in environ
.items():
753 if not k
.startswith("OSMNBI_"):
755 k1
, _
, k2
= k
[7:].lower().partition("_")
759 # update static configuration
760 if k
== 'OSMNBI_STATIC_DIR':
761 engine_config
["/static"]['tools.staticdir.dir'] = v
762 engine_config
["/static"]['tools.staticdir.on'] = True
763 elif k
== 'OSMNBI_SOCKET_PORT' or k
== 'OSMNBI_SERVER_PORT':
764 update_dict
['server.socket_port'] = int(v
)
765 elif k
== 'OSMNBI_SOCKET_HOST' or k
== 'OSMNBI_SERVER_HOST':
766 update_dict
['server.socket_host'] = v
767 elif k1
in ("server", "test", "auth", "log"):
768 update_dict
[k1
+ '.' + k2
] = v
769 elif k1
in ("message", "database", "storage", "authentication"):
770 # k2 = k2.replace('_', '.')
771 if k2
in ("port", "db_port"):
772 engine_config
[k1
][k2
] = int(v
)
774 engine_config
[k1
][k2
] = v
776 except ValueError as e
:
777 cherrypy
.log
.error("Ignoring environ '{}': " + str(e
))
778 except Exception as e
:
779 cherrypy
.log
.warn("skipping environ '{}' on exception '{}'".format(k
, e
))
782 cherrypy
.config
.update(update_dict
)
783 engine_config
["global"].update(update_dict
)
786 log_format_simple
= "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(message)s"
787 log_formatter_simple
= logging
.Formatter(log_format_simple
, datefmt
='%Y-%m-%dT%H:%M:%S')
788 logger_server
= logging
.getLogger("cherrypy.error")
789 logger_access
= logging
.getLogger("cherrypy.access")
790 logger_cherry
= logging
.getLogger("cherrypy")
791 logger_nbi
= logging
.getLogger("nbi")
793 if "log.file" in engine_config
["global"]:
794 file_handler
= logging
.handlers
.RotatingFileHandler(engine_config
["global"]["log.file"],
795 maxBytes
=100e6
, backupCount
=9, delay
=0)
796 file_handler
.setFormatter(log_formatter_simple
)
797 logger_cherry
.addHandler(file_handler
)
798 logger_nbi
.addHandler(file_handler
)
800 for format_
, logger
in {"nbi.server": logger_server
,
801 "nbi.access": logger_access
,
802 "%(name)s %(filename)s:%(lineno)s": logger_nbi
804 log_format_cherry
= "%(asctime)s %(levelname)s {} %(message)s".format(format_
)
805 log_formatter_cherry
= logging
.Formatter(log_format_cherry
, datefmt
='%Y-%m-%dT%H:%M:%S')
806 str_handler
= logging
.StreamHandler()
807 str_handler
.setFormatter(log_formatter_cherry
)
808 logger
.addHandler(str_handler
)
810 if engine_config
["global"].get("log.level"):
811 logger_cherry
.setLevel(engine_config
["global"]["log.level"])
812 logger_nbi
.setLevel(engine_config
["global"]["log.level"])
814 # logging other modules
815 for k1
, logname
in {"message": "nbi.msg", "database": "nbi.db", "storage": "nbi.fs"}.items():
816 engine_config
[k1
]["logger_name"] = logname
817 logger_module
= logging
.getLogger(logname
)
818 if "logfile" in engine_config
[k1
]:
819 file_handler
= logging
.handlers
.RotatingFileHandler(engine_config
[k1
]["logfile"],
820 maxBytes
=100e6
, backupCount
=9, delay
=0)
821 file_handler
.setFormatter(log_formatter_simple
)
822 logger_module
.addHandler(file_handler
)
823 if "loglevel" in engine_config
[k1
]:
824 logger_module
.setLevel(engine_config
[k1
]["loglevel"])
825 # TODO add more entries, e.g.: storage
826 cherrypy
.tree
.apps
['/osm'].root
.engine
.start(engine_config
)
827 cherrypy
.tree
.apps
['/osm'].root
.authenticator
.start(engine_config
)
829 cherrypy
.tree
.apps
['/osm'].root
.engine
.init_db(target_version
=database_version
)
830 cherrypy
.tree
.apps
['/osm'].root
.authenticator
.init_db(target_version
=auth_database_version
)
831 except (EngineException
, AuthException
):
833 # getenv('OSMOPENMANO_TENANT', None)
838 Callback function called when cherrypy.engine stops
839 TODO: Ending database connections.
841 cherrypy
.tree
.apps
['/osm'].root
.engine
.stop()
842 cherrypy
.log
.error("Stopping osm_nbi")
845 def nbi(config_file
):
848 # #'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
849 # 'tools.sessions.on': True,
850 # 'tools.response_headers.on': True,
851 # # 'tools.response_headers.headers': [('Content-Type', 'text/plain')],
854 # cherrypy.Server.ssl_module = 'builtin'
855 # cherrypy.Server.ssl_certificate = "http/cert.pem"
856 # cherrypy.Server.ssl_private_key = "http/privkey.pem"
857 # cherrypy.Server.thread_pool = 10
858 # cherrypy.config.update({'Server.socket_port': config["port"], 'Server.socket_host': config["host"]})
860 # cherrypy.config.update({'tools.auth_basic.on': True,
861 # 'tools.auth_basic.realm': 'localhost',
862 # 'tools.auth_basic.checkpassword': validate_password})
863 cherrypy
.engine
.subscribe('start', _start_service
)
864 cherrypy
.engine
.subscribe('stop', _stop_service
)
865 cherrypy
.quickstart(Server(), '/osm', config_file
)
869 print("""Usage: {} [options]
870 -c|--config [configuration_file]: loads the configuration file (default: ./nbi.cfg)
871 -h|--help: shows this help
872 """.format(sys
.argv
[0]))
873 # --log-socket-host HOST: send logs to this host")
874 # --log-socket-port PORT: send logs using this port (default: 9022)")
877 if __name__
== '__main__':
879 # load parameters and configuration
880 opts
, args
= getopt
.getopt(sys
.argv
[1:], "hvc:", ["config=", "help"])
881 # TODO add "log-socket-host=", "log-socket-port=", "log-file="
884 if o
in ("-h", "--help"):
887 elif o
in ("-c", "--config"):
889 # elif o == "--log-socket-port":
890 # log_socket_port = a
891 # elif o == "--log-socket-host":
892 # log_socket_host = a
893 # elif o == "--log-file":
896 assert False, "Unhandled option"
898 if not path
.isfile(config_file
):
899 print("configuration file '{}' that not exist".format(config_file
), file=sys
.stderr
)
902 for config_file
in (__file__
[:__file__
.rfind(".")] + ".cfg", "./nbi.cfg", "/etc/osm/nbi.cfg"):
903 if path
.isfile(config_file
):
906 print("No configuration file 'nbi.cfg' found neither at local folder nor at /etc/osm/", file=sys
.stderr
)
909 except getopt
.GetoptError
as e
:
910 print(str(e
), file=sys
.stderr
)