2 # -*- coding: utf-8 -*-
8 import html_out
as html
10 import logging
.handlers
13 from engine
import Engine
, EngineException
14 from osm_common
.dbbase
import DbException
15 from osm_common
.fsbase
import FsException
16 from osm_common
.msgbase
import MsgException
17 from base64
import standard_b64decode
18 from http
import HTTPStatus
19 from codecs
import getreader
20 from os
import environ
, path
22 __author__
= "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
24 # TODO consider to remove and provide version using the static version file
26 version_date
= "Apr 2018"
27 database_version
= '1.0'
30 North Bound Interface (O: OSM specific; 5,X: SOL005 not implemented yet; O5: SOL005 implemented)
31 URL: /osm GET POST PUT DELETE PATCH
33 /ns_descriptors_content O O
39 /artifacts[/<artifactPath>] O
47 /vnf_packages_content O O
51 /package_content O5 O5
54 /artifacts[/<artifactPath>] O5
59 /ns_instances_content O O
71 /vnf_instances (also vnfrs for compatibility) O
82 /vims_accounts (also vims for compatibility) O O
88 Follows SOL005 section 4.3.2 It contains extra METHOD to override http method, FORCE to force.
89 For filtering inside array, it must select the element of the array, or add ANYINDEX to apply the filtering over any
90 item of the array, that is, pass if any item of the array pass the filter.
91 It allows both ne and neq for not equal
92 TODO: 4.3.3 Attribute selectors
93 all_fields, fields=x,y,.., exclude_default, exclude_fields=x,y,...
94 (none) … same as “exclude_default”
95 all_fields … all attributes.
96 fields=<list> … all attributes except all complex attributes with minimum cardinality of zero that are not
97 conditionally mandatory, and that are not provided in <list>.
98 exclude_fields=<list> … all attributes except those complex attributes with a minimum cardinality of zero that
99 are not conditionally mandatory, and that are provided in <list>.
100 exclude_default … all attributes except those complex attributes with a minimum cardinality of zero that are not
101 conditionally mandatory, and that are part of the "default exclude set" defined in the present specification for
102 the particular resource
103 exclude_default and include=<list> … all attributes except those complex attributes with a minimum cardinality
104 of zero that are not conditionally mandatory and that are part of the "default exclude set" defined in the
105 present specification for the particular resource, but that are not part of <list>
106 Header field name Reference Example Descriptions
107 Accept IETF RFC 7231 [19] application/json Content-Types that are acceptable for the response.
108 This header field shall be present if the response is expected to have a non-empty message body.
109 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the request.
110 This header field shall be present if the request has a non-empty message body.
111 Authorization IETF RFC 7235 [22] Bearer mF_9.B5f-4.1JqM The authorization token for the request.
112 Details are specified in clause 4.5.3.
113 Range IETF RFC 7233 [21] 1000-2000 Requested range of bytes from a file
114 Header field name Reference Example Descriptions
115 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the response.
116 This header field shall be present if the response has a non-empty message body.
117 Location IETF RFC 7231 [19] http://www.example.com/vnflcm/v1/vnf_instances/123 Used in redirection, or when a
118 new resource has been created.
119 This header field shall be present if the response status code is 201 or 3xx.
120 In the present document this header field is also used if the response status code is 202 and a new resource was
122 WWW-Authenticate IETF RFC 7235 [22] Bearer realm="example" Challenge if the corresponding HTTP request has not
123 provided authorization, or error details if the corresponding HTTP request has provided an invalid authorization
125 Accept-Ranges IETF RFC 7233 [21] bytes Used by the Server to signal whether or not it supports ranges for
127 Content-Range IETF RFC 7233 [21] bytes 21010-47021/ 47022 Signals the byte range that is contained in the
128 response, and the total length of the file.
129 Retry-After IETF RFC 7231 [19] Fri, 31 Dec 1999 23:59:59 GMT
133 class NbiException(Exception):
135 def __init__(self
, message
, http_code
=HTTPStatus
.METHOD_NOT_ALLOWED
):
136 Exception.__init
__(self
, message
)
137 self
.http_code
= http_code
140 class Server(object):
142 # to decode bytes to str
143 reader
= getreader("utf-8")
147 self
.engine
= Engine()
148 self
.valid_methods
= { # contains allowed URL and methods
151 "tokens": {"METHODS": ("GET", "POST", "DELETE"),
152 "<ID>": {"METHODS": ("GET", "DELETE")}
154 "users": {"METHODS": ("GET", "POST"),
155 "<ID>": {"METHODS": ("GET", "POST", "DELETE", "PATCH", "PUT")}
157 "projects": {"METHODS": ("GET", "POST"),
158 "<ID>": {"METHODS": ("GET", "DELETE")}
160 "vims": {"METHODS": ("GET", "POST"),
161 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
163 "vim_accounts": {"METHODS": ("GET", "POST"),
164 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
166 "sdns": {"METHODS": ("GET", "POST"),
167 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
173 "ns_descriptors_content": {"METHODS": ("GET", "POST"),
174 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
176 "ns_descriptors": {"METHODS": ("GET", "POST"),
177 "<ID>": {"METHODS": ("GET", "DELETE"), "TODO": "PATCH",
178 "nsd_content": {"METHODS": ("GET", "PUT")},
179 "nsd": {"METHODS": "GET"}, # descriptor inside package
180 "artifacts": {"*": {"METHODS": "GET"}}
183 "pnf_descriptors": {"TODO": ("GET", "POST"),
184 "<ID>": {"TODO": ("GET", "DELETE", "PATCH"),
185 "pnfd_content": {"TODO": ("GET", "PUT")}
188 "subscriptions": {"TODO": ("GET", "POST"),
189 "<ID>": {"TODO": ("GET", "DELETE")}
195 "vnf_packages_content": {"METHODS": ("GET", "POST"),
196 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
198 "vnf_packages": {"METHODS": ("GET", "POST"),
199 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"), # GET: vnfPkgInfo
200 "package_content": {"METHODS": ("GET", "PUT"), # package
201 "upload_from_uri": {"TODO": "POST"}
203 "vnfd": {"METHODS": "GET"}, # descriptor inside package
204 "artifacts": {"*": {"METHODS": "GET"}}
207 "subscriptions": {"TODO": ("GET", "POST"),
208 "<ID>": {"TODO": ("GET", "DELETE")}
214 "ns_instances_content": {"METHODS": ("GET", "POST"),
215 "<ID>": {"METHODS": ("GET", "DELETE")}
217 "ns_instances": {"METHODS": ("GET", "POST"),
218 "<ID>": {"METHODS": ("GET", "DELETE"),
219 "scale": {"METHODS": "POST"},
220 "terminate": {"METHODS": "POST"},
221 "instantiate": {"METHODS": "POST"},
222 "action": {"METHODS": "POST"},
225 "ns_lcm_op_occs": {"METHODS": "GET",
226 "<ID>": {"METHODS": "GET"},
228 "vnfrs": {"METHODS": ("GET"),
229 "<ID>": {"METHODS": ("GET")}
231 "vnf_instances": {"METHODS": ("GET"),
232 "<ID>": {"METHODS": ("GET")}
238 def _authorization(self
):
242 # 1. Get token Authorization bearer
243 auth
= cherrypy
.request
.headers
.get("Authorization")
245 auth_list
= auth
.split(" ")
246 if auth_list
[0].lower() == "bearer":
247 token
= auth_list
[-1]
248 elif auth_list
[0].lower() == "basic":
249 user_passwd64
= auth_list
[-1]
251 if cherrypy
.session
.get("Authorization"):
252 # 2. Try using session before request a new token. If not, basic authentication will generate
253 token
= cherrypy
.session
.get("Authorization")
254 if token
== "logout":
255 token
= None # force Unauthorized response to insert user pasword again
256 elif user_passwd64
and cherrypy
.request
.config
.get("auth.allow_basic_authentication"):
257 # 3. Get new token from user password
261 user_passwd
= standard_b64decode(user_passwd64
).decode()
262 user
, _
, passwd
= user_passwd
.partition(":")
265 outdata
= self
.engine
.new_token(None, {"username": user
, "password": passwd
})
266 token
= outdata
["id"]
267 cherrypy
.session
['Authorization'] = token
268 # 4. Get token from cookie
270 # auth_cookie = cherrypy.request.cookie.get("Authorization")
272 # token = auth_cookie.value
273 return self
.engine
.authorize(token
)
274 except EngineException
as e
:
275 if cherrypy
.session
.get('Authorization'):
276 del cherrypy
.session
['Authorization']
277 cherrypy
.response
.headers
["WWW-Authenticate"] = 'Bearer realm="{}"'.format(e
)
280 def _format_in(self
, kwargs
):
283 if cherrypy
.request
.body
.length
:
284 error_text
= "Invalid input format "
286 if "Content-Type" in cherrypy
.request
.headers
:
287 if "application/json" in cherrypy
.request
.headers
["Content-Type"]:
288 error_text
= "Invalid json format "
289 indata
= json
.load(self
.reader(cherrypy
.request
.body
))
290 elif "application/yaml" in cherrypy
.request
.headers
["Content-Type"]:
291 error_text
= "Invalid yaml format "
292 indata
= yaml
.load(cherrypy
.request
.body
)
293 elif "application/binary" in cherrypy
.request
.headers
["Content-Type"] or \
294 "application/gzip" in cherrypy
.request
.headers
["Content-Type"] or \
295 "application/zip" in cherrypy
.request
.headers
["Content-Type"] or \
296 "text/plain" in cherrypy
.request
.headers
["Content-Type"]:
297 indata
= cherrypy
.request
.body
# .read()
298 elif "multipart/form-data" in cherrypy
.request
.headers
["Content-Type"]:
299 if "descriptor_file" in kwargs
:
300 filecontent
= kwargs
.pop("descriptor_file")
301 if not filecontent
.file:
302 raise NbiException("empty file or content", HTTPStatus
.BAD_REQUEST
)
303 indata
= filecontent
.file # .read()
304 if filecontent
.content_type
.value
:
305 cherrypy
.request
.headers
["Content-Type"] = filecontent
.content_type
.value
307 # raise cherrypy.HTTPError(HTTPStatus.Not_Acceptable,
308 # "Only 'Content-Type' of type 'application/json' or
309 # 'application/yaml' for input format are available")
310 error_text
= "Invalid yaml format "
311 indata
= yaml
.load(cherrypy
.request
.body
)
313 error_text
= "Invalid yaml format "
314 indata
= yaml
.load(cherrypy
.request
.body
)
319 if cherrypy
.request
.headers
.get("Query-String-Format") == "yaml":
322 for k
, v
in kwargs
.items():
323 if isinstance(v
, str):
328 kwargs
[k
] = yaml
.load(v
)
331 elif k
.endswith(".gt") or k
.endswith(".lt") or k
.endswith(".gte") or k
.endswith(".lte"):
339 elif v
.find(",") > 0:
340 kwargs
[k
] = v
.split(",")
341 elif isinstance(v
, (list, tuple)):
342 for index
in range(0, len(v
)):
347 v
[index
] = yaml
.load(v
[index
])
352 except (ValueError, yaml
.YAMLError
) as exc
:
353 raise NbiException(error_text
+ str(exc
), HTTPStatus
.BAD_REQUEST
)
354 except KeyError as exc
:
355 raise NbiException("Query string error: " + str(exc
), HTTPStatus
.BAD_REQUEST
)
356 except Exception as exc
:
357 raise NbiException(error_text
+ str(exc
), HTTPStatus
.BAD_REQUEST
)
360 def _format_out(data
, session
=None, _format
=None):
362 return string of dictionary data according to requested json, yaml, xml. By default json
363 :param data: response to be sent. Can be a dict, text or file
365 :param _format: The format to be set as Content-Type ir data is a file
368 accept
= cherrypy
.request
.headers
.get("Accept")
370 if accept
and "text/html" in accept
:
371 return html
.format(data
, cherrypy
.request
, cherrypy
.response
, session
)
372 # cherrypy.response.status = HTTPStatus.NO_CONTENT.value
374 elif hasattr(data
, "read"): # file object
376 cherrypy
.response
.headers
["Content-Type"] = _format
377 elif "b" in data
.mode
: # binariy asssumig zip
378 cherrypy
.response
.headers
["Content-Type"] = 'application/zip'
380 cherrypy
.response
.headers
["Content-Type"] = 'text/plain'
381 # TODO check that cherrypy close file. If not implement pending things to close per thread next
384 if "application/json" in accept
:
385 cherrypy
.response
.headers
["Content-Type"] = 'application/json; charset=utf-8'
386 a
= json
.dumps(data
, indent
=4) + "\n"
387 return a
.encode("utf8")
388 elif "text/html" in accept
:
389 return html
.format(data
, cherrypy
.request
, cherrypy
.response
, session
)
391 elif "application/yaml" in accept
or "*/*" in accept
or "text/plain" in accept
:
394 raise cherrypy
.HTTPError(HTTPStatus
.NOT_ACCEPTABLE
.value
,
395 "Only 'Accept' of type 'application/json' or 'application/yaml' "
396 "for output format are available")
397 cherrypy
.response
.headers
["Content-Type"] = 'application/yaml'
398 return yaml
.safe_dump(data
, explicit_start
=True, indent
=4, default_flow_style
=False, tags
=False,
399 encoding
='utf-8', allow_unicode
=True) # , canonical=True, default_style='"'
402 def index(self
, *args
, **kwargs
):
405 if cherrypy
.request
.method
== "GET":
406 session
= self
._authorization
()
407 outdata
= "Index page"
409 raise cherrypy
.HTTPError(HTTPStatus
.METHOD_NOT_ALLOWED
.value
,
410 "Method {} not allowed for tokens".format(cherrypy
.request
.method
))
412 return self
._format
_out
(outdata
, session
)
414 except EngineException
as e
:
415 cherrypy
.log("index Exception {}".format(e
))
416 cherrypy
.response
.status
= e
.http_code
.value
417 return self
._format
_out
("Welcome to OSM!", session
)
420 def version(self
, *args
, **kwargs
):
421 # TODO consider to remove and provide version using the static version file
422 global __version__
, version_date
424 if cherrypy
.request
.method
!= "GET":
425 raise NbiException("Only method GET is allowed", HTTPStatus
.METHOD_NOT_ALLOWED
)
427 raise NbiException("Invalid URL or query string for version", HTTPStatus
.METHOD_NOT_ALLOWED
)
428 return __version__
+ " " + version_date
429 except NbiException
as e
:
430 cherrypy
.response
.status
= e
.http_code
.value
432 "code": e
.http_code
.name
,
433 "status": e
.http_code
.value
,
436 return self
._format
_out
(problem_details
, None)
439 def token(self
, method
, token_id
=None, kwargs
=None):
441 # self.engine.load_dbase(cherrypy.request.app.config)
442 indata
= self
._format
_in
(kwargs
)
443 if not isinstance(indata
, dict):
444 raise NbiException("Expected application/yaml or application/json Content-Type", HTTPStatus
.BAD_REQUEST
)
447 session
= self
._authorization
()
449 outdata
= self
.engine
.get_token(session
, token_id
)
451 outdata
= self
.engine
.get_token_list(session
)
452 elif method
== "POST":
454 session
= self
._authorization
()
458 indata
.update(kwargs
)
459 outdata
= self
.engine
.new_token(session
, indata
, cherrypy
.request
.remote
)
461 cherrypy
.session
['Authorization'] = outdata
["_id"]
462 self
._set
_location
_header
("admin", "v1", "tokens", outdata
["_id"])
463 # cherrypy.response.cookie["Authorization"] = outdata["id"]
464 # cherrypy.response.cookie["Authorization"]['expires'] = 3600
465 elif method
== "DELETE":
466 if not token_id
and "id" in kwargs
:
467 token_id
= kwargs
["id"]
469 session
= self
._authorization
()
470 token_id
= session
["_id"]
471 outdata
= self
.engine
.del_token(token_id
)
473 cherrypy
.session
['Authorization'] = "logout"
474 # cherrypy.response.cookie["Authorization"] = token_id
475 # cherrypy.response.cookie["Authorization"]['expires'] = 0
477 raise NbiException("Method {} not allowed for token".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
478 return self
._format
_out
(outdata
, session
)
479 except (NbiException
, EngineException
, DbException
) as e
:
480 cherrypy
.log("tokens Exception {}".format(e
))
481 cherrypy
.response
.status
= e
.http_code
.value
483 "code": e
.http_code
.name
,
484 "status": e
.http_code
.value
,
487 return self
._format
_out
(problem_details
, session
)
490 def test(self
, *args
, **kwargs
):
492 if args
and args
[0] == "help":
493 return "<html><pre>\ninit\nfile/<name> download file\ndb-clear/table\nprune\nlogin\nlogin2\n"\
494 "sleep/<time>\nmessage/topic\n</pre></html>"
496 elif args
and args
[0] == "init":
498 # self.engine.load_dbase(cherrypy.request.app.config)
499 self
.engine
.create_admin()
500 return "Done. User 'admin', password 'admin' created"
502 cherrypy
.response
.status
= HTTPStatus
.FORBIDDEN
.value
503 return self
._format
_out
("Database already initialized")
504 elif args
and args
[0] == "file":
505 return cherrypy
.lib
.static
.serve_file(cherrypy
.tree
.apps
['/osm'].config
["storage"]["path"] + "/" + args
[1],
506 "text/plain", "attachment")
507 elif args
and args
[0] == "file2":
508 f_path
= cherrypy
.tree
.apps
['/osm'].config
["storage"]["path"] + "/" + args
[1]
509 f
= open(f_path
, "r")
510 cherrypy
.response
.headers
["Content-type"] = "text/plain"
513 elif len(args
) == 2 and args
[0] == "db-clear":
514 return self
.engine
.del_item_list({"project_id": "admin", "admin": True}, args
[1], kwargs
)
515 elif args
and args
[0] == "prune":
516 return self
.engine
.prune()
517 elif args
and args
[0] == "login":
518 if not cherrypy
.request
.headers
.get("Authorization"):
519 cherrypy
.response
.headers
["WWW-Authenticate"] = 'Basic realm="Access to OSM site", charset="UTF-8"'
520 cherrypy
.response
.status
= HTTPStatus
.UNAUTHORIZED
.value
521 elif args
and args
[0] == "login2":
522 if not cherrypy
.request
.headers
.get("Authorization"):
523 cherrypy
.response
.headers
["WWW-Authenticate"] = 'Bearer realm="Access to OSM site"'
524 cherrypy
.response
.status
= HTTPStatus
.UNAUTHORIZED
.value
525 elif args
and args
[0] == "sleep":
528 sleep_time
= int(args
[1])
530 cherrypy
.response
.status
= HTTPStatus
.FORBIDDEN
.value
531 return self
._format
_out
("Database already initialized")
532 thread_info
= cherrypy
.thread_data
534 time
.sleep(sleep_time
)
536 elif len(args
) >= 2 and args
[0] == "message":
538 return_text
= "<html><pre>{} ->\n".format(topic
)
540 if cherrypy
.request
.method
== 'POST':
541 to_send
= yaml
.load(cherrypy
.request
.body
)
542 for k
, v
in to_send
.items():
543 self
.engine
.msg
.write(topic
, k
, v
)
544 return_text
+= " {}: {}\n".format(k
, v
)
545 elif cherrypy
.request
.method
== 'GET':
546 for k
, v
in kwargs
.items():
547 self
.engine
.msg
.write(topic
, k
, yaml
.load(v
))
548 return_text
+= " {}: {}\n".format(k
, yaml
.load(v
))
549 except Exception as e
:
550 return_text
+= "Error: " + str(e
)
551 return_text
+= "</pre></html>\n"
555 "<html><pre>\nheaders:\n args: {}\n".format(args
) +
556 " kwargs: {}\n".format(kwargs
) +
557 " headers: {}\n".format(cherrypy
.request
.headers
) +
558 " path_info: {}\n".format(cherrypy
.request
.path_info
) +
559 " query_string: {}\n".format(cherrypy
.request
.query_string
) +
560 " session: {}\n".format(cherrypy
.session
) +
561 " cookie: {}\n".format(cherrypy
.request
.cookie
) +
562 " method: {}\n".format(cherrypy
.request
.method
) +
563 " session: {}\n".format(cherrypy
.session
.get('fieldname')) +
565 return_text
+= " length: {}\n".format(cherrypy
.request
.body
.length
)
566 if cherrypy
.request
.body
.length
:
567 return_text
+= " content: {}\n".format(
568 str(cherrypy
.request
.body
.read(int(cherrypy
.request
.headers
.get('Content-Length', 0)))))
570 return_text
+= "thread: {}\n".format(thread_info
)
571 return_text
+= "</pre></html>"
574 def _check_valid_url_method(self
, method
, *args
):
576 raise NbiException("URL must contain at least 'topic/version/item'", HTTPStatus
.METHOD_NOT_ALLOWED
)
578 reference
= self
.valid_methods
582 if not isinstance(reference
, dict):
583 raise NbiException("URL contains unexpected extra items '{}'".format(arg
),
584 HTTPStatus
.METHOD_NOT_ALLOWED
)
587 reference
= reference
[arg
]
588 elif "<ID>" in reference
:
589 reference
= reference
["<ID>"]
590 elif "*" in reference
:
591 reference
= reference
["*"]
594 raise NbiException("Unexpected URL item {}".format(arg
), HTTPStatus
.METHOD_NOT_ALLOWED
)
595 if "TODO" in reference
and method
in reference
["TODO"]:
596 raise NbiException("Method {} not supported yet for this URL".format(method
), HTTPStatus
.NOT_IMPLEMENTED
)
597 elif "METHODS" in reference
and method
not in reference
["METHODS"]:
598 raise NbiException("Method {} not supported for this URL".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
602 def _set_location_header(topic
, version
, item
, id):
604 Insert response header Location with the URL of created item base on URL params
611 # Use cherrypy.request.base for absoluted path and make use of request.header HOST just in case behind aNAT
612 cherrypy
.response
.headers
["Location"] = "/osm/{}/{}/{}/{}".format(topic
, version
, item
, id)
616 def default(self
, topic
=None, version
=None, item
=None, _id
=None, item2
=None, *args
, **kwargs
):
625 if not topic
or not version
or not item
:
626 raise NbiException("URL must contain at least 'topic/version/item'", HTTPStatus
.METHOD_NOT_ALLOWED
)
627 if topic
not in ("admin", "vnfpkgm", "nsd", "nslcm"):
628 raise NbiException("URL topic '{}' not supported".format(topic
), HTTPStatus
.METHOD_NOT_ALLOWED
)
630 raise NbiException("URL version '{}' not supported".format(version
), HTTPStatus
.METHOD_NOT_ALLOWED
)
632 if kwargs
and "METHOD" in kwargs
and kwargs
["METHOD"] in ("PUT", "POST", "DELETE", "GET", "PATCH"):
633 method
= kwargs
.pop("METHOD")
635 method
= cherrypy
.request
.method
636 if kwargs
and "FORCE" in kwargs
:
637 force
= kwargs
.pop("FORCE")
641 self
._check
_valid
_url
_method
(method
, topic
, version
, item
, _id
, item2
, *args
)
643 if topic
== "admin" and item
== "tokens":
644 return self
.token(method
, _id
, kwargs
)
646 # self.engine.load_dbase(cherrypy.request.app.config)
647 session
= self
._authorization
()
648 indata
= self
._format
_in
(kwargs
)
650 if item
== "subscriptions":
651 engine_item
= topic
+ "_" + item
657 elif topic
== "vnfpkgm":
658 engine_item
= "vnfds"
659 elif topic
== "nslcm":
661 if item
== "ns_lcm_op_occs":
662 engine_item
= "nslcmops"
663 if item
== "vnfrs" or item
== "vnf_instances":
664 engine_item
= "vnfrs"
665 if engine_item
== "vims": # TODO this is for backward compatibility, it will remove in the future
666 engine_item
= "vim_accounts"
669 if item2
in ("nsd_content", "package_content", "artifacts", "vnfd", "nsd"):
670 if item2
in ("vnfd", "nsd"):
674 elif item2
== "artifacts":
678 file, _format
= self
.engine
.get_file(session
, engine_item
, _id
, path
,
679 cherrypy
.request
.headers
.get("Accept"))
682 outdata
= self
.engine
.get_item_list(session
, engine_item
, kwargs
)
684 outdata
= self
.engine
.get_item(session
, engine_item
, _id
)
685 elif method
== "POST":
686 if item
in ("ns_descriptors_content", "vnf_packages_content"):
687 _id
= cherrypy
.request
.headers
.get("Transaction-Id")
689 _id
= self
.engine
.new_item(rollback
, session
, engine_item
, {}, None, cherrypy
.request
.headers
,
691 completed
= self
.engine
.upload_content(session
, engine_item
, _id
, indata
, kwargs
,
692 cherrypy
.request
.headers
)
694 self
._set
_location
_header
(topic
, version
, item
, _id
)
696 cherrypy
.response
.headers
["Transaction-Id"] = _id
697 outdata
= {"id": _id
}
698 elif item
== "ns_instances_content":
699 _id
= self
.engine
.new_item(rollback
, session
, engine_item
, indata
, kwargs
, force
=force
)
700 self
.engine
.ns_operation(rollback
, session
, _id
, "instantiate", indata
, None)
701 self
._set
_location
_header
(topic
, version
, item
, _id
)
702 outdata
= {"id": _id
}
703 elif item
== "ns_instances" and item2
:
704 _id
= self
.engine
.ns_operation(rollback
, session
, _id
, item2
, indata
, kwargs
)
705 self
._set
_location
_header
(topic
, version
, "ns_lcm_op_occs", _id
)
706 outdata
= {"id": _id
}
707 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
709 _id
= self
.engine
.new_item(rollback
, session
, engine_item
, indata
, kwargs
, cherrypy
.request
.headers
,
711 self
._set
_location
_header
(topic
, version
, item
, _id
)
712 outdata
= {"id": _id
}
713 # TODO form NsdInfo when item in ("ns_descriptors", "vnf_packages")
714 cherrypy
.response
.status
= HTTPStatus
.CREATED
.value
716 elif method
== "DELETE":
718 outdata
= self
.engine
.del_item_list(session
, engine_item
, kwargs
)
719 cherrypy
.response
.status
= HTTPStatus
.OK
.value
720 else: # len(args) > 1
721 if item
== "ns_instances_content" and not force
:
722 opp_id
= self
.engine
.ns_operation(rollback
, session
, _id
, "terminate", {"autoremove": True},
724 outdata
= {"_id": opp_id
}
725 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
727 self
.engine
.del_item(session
, engine_item
, _id
, force
)
728 cherrypy
.response
.status
= HTTPStatus
.NO_CONTENT
.value
729 if engine_item
in ("vim_accounts", "sdns"):
730 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
732 elif method
in ("PUT", "PATCH"):
734 if not indata
and not kwargs
:
735 raise NbiException("Nothing to update. Provide payload and/or query string",
736 HTTPStatus
.BAD_REQUEST
)
737 if item2
in ("nsd_content", "package_content") and method
== "PUT":
738 completed
= self
.engine
.upload_content(session
, engine_item
, _id
, indata
, kwargs
,
739 cherrypy
.request
.headers
)
741 cherrypy
.response
.headers
["Transaction-Id"] = id
743 self
.engine
.edit_item(session
, engine_item
, _id
, indata
, kwargs
, force
=force
)
744 cherrypy
.response
.status
= HTTPStatus
.NO_CONTENT
.value
746 raise NbiException("Method {} not allowed".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
747 return self
._format
_out
(outdata
, session
, _format
)
748 except (NbiException
, EngineException
, DbException
, FsException
, MsgException
) as e
:
749 cherrypy
.log("Exception {}".format(e
))
750 cherrypy
.response
.status
= e
.http_code
.value
751 if hasattr(outdata
, "close"): # is an open file
753 for rollback_item
in rollback
:
755 self
.engine
.del_item(**rollback_item
, session
=session
, force
=True)
756 except Exception as e2
:
757 cherrypy
.log("Rollback Exception {}: {}".format(rollback_item
, e2
))
759 if isinstance(e
, MsgException
):
760 error_text
= "{} has been '{}' but other modules cannot be informed because an error on bus".format(
761 engine_item
[:-1], method
, error_text
)
763 "code": e
.http_code
.name
,
764 "status": e
.http_code
.value
,
767 return self
._format
_out
(problem_details
, session
)
768 # raise cherrypy.HTTPError(e.http_code.value, str(e))
771 # def validate_password(realm, username, password):
772 # cherrypy.log("realm "+ str(realm))
773 # if username == "admin" and password == "admin":
778 def _start_service():
780 Callback function called when cherrypy.engine starts
781 Override configuration with env variables
782 Set database, storage, message configuration
783 Init database with admin/admin user password
785 cherrypy
.log
.error("Starting osm_nbi")
786 # update general cherrypy configuration
789 engine_config
= cherrypy
.tree
.apps
['/osm'].config
790 for k
, v
in environ
.items():
791 if not k
.startswith("OSMNBI_"):
793 k1
, _
, k2
= k
[7:].lower().partition("_")
797 # update static configuration
798 if k
== 'OSMNBI_STATIC_DIR':
799 engine_config
["/static"]['tools.staticdir.dir'] = v
800 engine_config
["/static"]['tools.staticdir.on'] = True
801 elif k
== 'OSMNBI_SOCKET_PORT' or k
== 'OSMNBI_SERVER_PORT':
802 update_dict
['server.socket_port'] = int(v
)
803 elif k
== 'OSMNBI_SOCKET_HOST' or k
== 'OSMNBI_SERVER_HOST':
804 update_dict
['server.socket_host'] = v
805 elif k1
in ("server", "test", "auth", "log"):
806 update_dict
[k1
+ '.' + k2
] = v
807 elif k1
in ("message", "database", "storage"):
808 # k2 = k2.replace('_', '.')
810 engine_config
[k1
][k2
] = int(v
)
812 engine_config
[k1
][k2
] = v
813 except ValueError as e
:
814 cherrypy
.log
.error("Ignoring environ '{}': " + str(e
))
815 except Exception as e
:
816 cherrypy
.log
.warn("skipping environ '{}' on exception '{}'".format(k
, e
))
819 cherrypy
.config
.update(update_dict
)
820 engine_config
["global"].update(update_dict
)
823 log_format_simple
= "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(message)s"
824 log_formatter_simple
= logging
.Formatter(log_format_simple
, datefmt
='%Y-%m-%dT%H:%M:%S')
825 logger_server
= logging
.getLogger("cherrypy.error")
826 logger_access
= logging
.getLogger("cherrypy.access")
827 logger_cherry
= logging
.getLogger("cherrypy")
828 logger_nbi
= logging
.getLogger("nbi")
830 if "log.file" in engine_config
["global"]:
831 file_handler
= logging
.handlers
.RotatingFileHandler(engine_config
["global"]["log.file"],
832 maxBytes
=100e6
, backupCount
=9, delay
=0)
833 file_handler
.setFormatter(log_formatter_simple
)
834 logger_cherry
.addHandler(file_handler
)
835 logger_nbi
.addHandler(file_handler
)
837 for format_
, logger
in {"nbi.server": logger_server
,
838 "nbi.access": logger_access
,
839 "%(name)s %(filename)s:%(lineno)s": logger_nbi
841 log_format_cherry
= "%(asctime)s %(levelname)s {} %(message)s".format(format_
)
842 log_formatter_cherry
= logging
.Formatter(log_format_cherry
, datefmt
='%Y-%m-%dT%H:%M:%S')
843 str_handler
= logging
.StreamHandler()
844 str_handler
.setFormatter(log_formatter_cherry
)
845 logger
.addHandler(str_handler
)
847 if engine_config
["global"].get("log.level"):
848 logger_cherry
.setLevel(engine_config
["global"]["log.level"])
849 logger_nbi
.setLevel(engine_config
["global"]["log.level"])
851 # logging other modules
852 for k1
, logname
in {"message": "nbi.msg", "database": "nbi.db", "storage": "nbi.fs"}.items():
853 engine_config
[k1
]["logger_name"] = logname
854 logger_module
= logging
.getLogger(logname
)
855 if "logfile" in engine_config
[k1
]:
856 file_handler
= logging
.handlers
.RotatingFileHandler(engine_config
[k1
]["logfile"],
857 maxBytes
=100e6
, backupCount
=9, delay
=0)
858 file_handler
.setFormatter(log_formatter_simple
)
859 logger_module
.addHandler(file_handler
)
860 if "loglevel" in engine_config
[k1
]:
861 logger_module
.setLevel(engine_config
[k1
]["loglevel"])
862 # TODO add more entries, e.g.: storage
863 cherrypy
.tree
.apps
['/osm'].root
.engine
.start(engine_config
)
865 cherrypy
.tree
.apps
['/osm'].root
.engine
.init_db(target_version
=database_version
)
866 except EngineException
:
868 # getenv('OSMOPENMANO_TENANT', None)
873 Callback function called when cherrypy.engine stops
874 TODO: Ending database connections.
876 cherrypy
.tree
.apps
['/osm'].root
.engine
.stop()
877 cherrypy
.log
.error("Stopping osm_nbi")
880 def nbi(config_file
):
883 # #'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
884 # 'tools.sessions.on': True,
885 # 'tools.response_headers.on': True,
886 # # 'tools.response_headers.headers': [('Content-Type', 'text/plain')],
889 # cherrypy.Server.ssl_module = 'builtin'
890 # cherrypy.Server.ssl_certificate = "http/cert.pem"
891 # cherrypy.Server.ssl_private_key = "http/privkey.pem"
892 # cherrypy.Server.thread_pool = 10
893 # cherrypy.config.update({'Server.socket_port': config["port"], 'Server.socket_host': config["host"]})
895 # cherrypy.config.update({'tools.auth_basic.on': True,
896 # 'tools.auth_basic.realm': 'localhost',
897 # 'tools.auth_basic.checkpassword': validate_password})
898 cherrypy
.engine
.subscribe('start', _start_service
)
899 cherrypy
.engine
.subscribe('stop', _stop_service
)
900 cherrypy
.quickstart(Server(), '/osm', config_file
)
904 print("""Usage: {} [options]
905 -c|--config [configuration_file]: loads the configuration file (default: ./nbi.cfg)
906 -h|--help: shows this help
907 """.format(sys
.argv
[0]))
908 # --log-socket-host HOST: send logs to this host")
909 # --log-socket-port PORT: send logs using this port (default: 9022)")
912 if __name__
== '__main__':
914 # load parameters and configuration
915 opts
, args
= getopt
.getopt(sys
.argv
[1:], "hvc:", ["config=", "help"])
916 # TODO add "log-socket-host=", "log-socket-port=", "log-file="
919 if o
in ("-h", "--help"):
922 elif o
in ("-c", "--config"):
924 # elif o == "--log-socket-port":
925 # log_socket_port = a
926 # elif o == "--log-socket-host":
927 # log_socket_host = a
928 # elif o == "--log-file":
931 assert False, "Unhandled option"
933 if not path
.isfile(config_file
):
934 print("configuration file '{}' that not exist".format(config_file
), file=sys
.stderr
)
937 for config_file
in (__file__
[:__file__
.rfind(".")] + ".cfg", "./nbi.cfg", "/etc/osm/nbi.cfg"):
938 if path
.isfile(config_file
):
941 print("No configuration file 'nbi.cfg' found neither at local folder nor at /etc/osm/", file=sys
.stderr
)
944 except getopt
.GetoptError
as e
:
945 print(str(e
), file=sys
.stderr
)