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
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")}
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"), "TODO": "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": {"TODO": "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")}
235 def _authorization(self
):
239 # 1. Get token Authorization bearer
240 auth
= cherrypy
.request
.headers
.get("Authorization")
242 auth_list
= auth
.split(" ")
243 if auth_list
[0].lower() == "bearer":
244 token
= auth_list
[-1]
245 elif auth_list
[0].lower() == "basic":
246 user_passwd64
= auth_list
[-1]
248 if cherrypy
.session
.get("Authorization"):
249 # 2. Try using session before request a new token. If not, basic authentication will generate
250 token
= cherrypy
.session
.get("Authorization")
251 if token
== "logout":
252 token
= None # force Unauthorized response to insert user pasword again
253 elif user_passwd64
and cherrypy
.request
.config
.get("auth.allow_basic_authentication"):
254 # 3. Get new token from user password
258 user_passwd
= standard_b64decode(user_passwd64
).decode()
259 user
, _
, passwd
= user_passwd
.partition(":")
262 outdata
= self
.engine
.new_token(None, {"username": user
, "password": passwd
})
263 token
= outdata
["id"]
264 cherrypy
.session
['Authorization'] = token
265 # 4. Get token from cookie
267 # auth_cookie = cherrypy.request.cookie.get("Authorization")
269 # token = auth_cookie.value
270 return self
.engine
.authorize(token
)
271 except EngineException
as e
:
272 if cherrypy
.session
.get('Authorization'):
273 del cherrypy
.session
['Authorization']
274 cherrypy
.response
.headers
["WWW-Authenticate"] = 'Bearer realm="{}"'.format(e
)
277 def _format_in(self
, kwargs
):
280 if cherrypy
.request
.body
.length
:
281 error_text
= "Invalid input format "
283 if "Content-Type" in cherrypy
.request
.headers
:
284 if "application/json" in cherrypy
.request
.headers
["Content-Type"]:
285 error_text
= "Invalid json format "
286 indata
= json
.load(self
.reader(cherrypy
.request
.body
))
287 elif "application/yaml" in cherrypy
.request
.headers
["Content-Type"]:
288 error_text
= "Invalid yaml format "
289 indata
= yaml
.load(cherrypy
.request
.body
)
290 elif "application/binary" in cherrypy
.request
.headers
["Content-Type"] or \
291 "application/gzip" in cherrypy
.request
.headers
["Content-Type"] or \
292 "application/zip" in cherrypy
.request
.headers
["Content-Type"] or \
293 "text/plain" in cherrypy
.request
.headers
["Content-Type"]:
294 indata
= cherrypy
.request
.body
# .read()
295 elif "multipart/form-data" in cherrypy
.request
.headers
["Content-Type"]:
296 if "descriptor_file" in kwargs
:
297 filecontent
= kwargs
.pop("descriptor_file")
298 if not filecontent
.file:
299 raise NbiException("empty file or content", HTTPStatus
.BAD_REQUEST
)
300 indata
= filecontent
.file # .read()
301 if filecontent
.content_type
.value
:
302 cherrypy
.request
.headers
["Content-Type"] = filecontent
.content_type
.value
304 # raise cherrypy.HTTPError(HTTPStatus.Not_Acceptable,
305 # "Only 'Content-Type' of type 'application/json' or
306 # 'application/yaml' for input format are available")
307 error_text
= "Invalid yaml format "
308 indata
= yaml
.load(cherrypy
.request
.body
)
310 error_text
= "Invalid yaml format "
311 indata
= yaml
.load(cherrypy
.request
.body
)
316 if cherrypy
.request
.headers
.get("Query-String-Format") == "yaml":
319 for k
, v
in kwargs
.items():
320 if isinstance(v
, str):
325 kwargs
[k
] = yaml
.load(v
)
328 elif k
.endswith(".gt") or k
.endswith(".lt") or k
.endswith(".gte") or k
.endswith(".lte"):
336 elif v
.find(",") > 0:
337 kwargs
[k
] = v
.split(",")
338 elif isinstance(v
, (list, tuple)):
339 for index
in range(0, len(v
)):
344 v
[index
] = yaml
.load(v
[index
])
349 except (ValueError, yaml
.YAMLError
) as exc
:
350 raise NbiException(error_text
+ str(exc
), HTTPStatus
.BAD_REQUEST
)
351 except KeyError as exc
:
352 raise NbiException("Query string error: " + str(exc
), HTTPStatus
.BAD_REQUEST
)
353 except Exception as exc
:
354 raise NbiException(error_text
+ str(exc
), HTTPStatus
.BAD_REQUEST
)
357 def _format_out(data
, session
=None, _format
=None):
359 return string of dictionary data according to requested json, yaml, xml. By default json
360 :param data: response to be sent. Can be a dict, text or file
362 :param _format: The format to be set as Content-Type ir data is a file
365 accept
= cherrypy
.request
.headers
.get("Accept")
367 if accept
and "text/html" in accept
:
368 return html
.format(data
, cherrypy
.request
, cherrypy
.response
, session
)
369 # cherrypy.response.status = HTTPStatus.NO_CONTENT.value
371 elif hasattr(data
, "read"): # file object
373 cherrypy
.response
.headers
["Content-Type"] = _format
374 elif "b" in data
.mode
: # binariy asssumig zip
375 cherrypy
.response
.headers
["Content-Type"] = 'application/zip'
377 cherrypy
.response
.headers
["Content-Type"] = 'text/plain'
378 # TODO check that cherrypy close file. If not implement pending things to close per thread next
381 if "application/json" in accept
:
382 cherrypy
.response
.headers
["Content-Type"] = 'application/json; charset=utf-8'
383 a
= json
.dumps(data
, indent
=4) + "\n"
384 return a
.encode("utf8")
385 elif "text/html" in accept
:
386 return html
.format(data
, cherrypy
.request
, cherrypy
.response
, session
)
388 elif "application/yaml" in accept
or "*/*" in accept
or "text/plain" in accept
:
391 raise cherrypy
.HTTPError(HTTPStatus
.NOT_ACCEPTABLE
.value
,
392 "Only 'Accept' of type 'application/json' or 'application/yaml' "
393 "for output format are available")
394 cherrypy
.response
.headers
["Content-Type"] = 'application/yaml'
395 return yaml
.safe_dump(data
, explicit_start
=True, indent
=4, default_flow_style
=False, tags
=False,
396 encoding
='utf-8', allow_unicode
=True) # , canonical=True, default_style='"'
399 def index(self
, *args
, **kwargs
):
402 if cherrypy
.request
.method
== "GET":
403 session
= self
._authorization
()
404 outdata
= "Index page"
406 raise cherrypy
.HTTPError(HTTPStatus
.METHOD_NOT_ALLOWED
.value
,
407 "Method {} not allowed for tokens".format(cherrypy
.request
.method
))
409 return self
._format
_out
(outdata
, session
)
411 except EngineException
as e
:
412 cherrypy
.log("index Exception {}".format(e
))
413 cherrypy
.response
.status
= e
.http_code
.value
414 return self
._format
_out
("Welcome to OSM!", session
)
417 def version(self
, *args
, **kwargs
):
418 # TODO consider to remove and provide version using the static version file
419 global __version__
, version_date
421 if cherrypy
.request
.method
!= "GET":
422 raise NbiException("Only method GET is allowed", HTTPStatus
.METHOD_NOT_ALLOWED
)
424 raise NbiException("Invalid URL or query string for version", HTTPStatus
.METHOD_NOT_ALLOWED
)
425 return __version__
+ " " + version_date
426 except NbiException
as e
:
427 cherrypy
.response
.status
= e
.http_code
.value
429 "code": e
.http_code
.name
,
430 "status": e
.http_code
.value
,
433 return self
._format
_out
(problem_details
, None)
436 def token(self
, method
, token_id
=None, kwargs
=None):
438 # self.engine.load_dbase(cherrypy.request.app.config)
439 indata
= self
._format
_in
(kwargs
)
440 if not isinstance(indata
, dict):
441 raise NbiException("Expected application/yaml or application/json Content-Type", HTTPStatus
.BAD_REQUEST
)
444 session
= self
._authorization
()
446 outdata
= self
.engine
.get_token(session
, token_id
)
448 outdata
= self
.engine
.get_token_list(session
)
449 elif method
== "POST":
451 session
= self
._authorization
()
455 indata
.update(kwargs
)
456 outdata
= self
.engine
.new_token(session
, indata
, cherrypy
.request
.remote
)
458 cherrypy
.session
['Authorization'] = outdata
["_id"]
459 self
._set
_location
_header
("admin", "v1", "tokens", outdata
["_id"])
460 # cherrypy.response.cookie["Authorization"] = outdata["id"]
461 # cherrypy.response.cookie["Authorization"]['expires'] = 3600
462 elif method
== "DELETE":
463 if not token_id
and "id" in kwargs
:
464 token_id
= kwargs
["id"]
466 session
= self
._authorization
()
467 token_id
= session
["_id"]
468 outdata
= self
.engine
.del_token(token_id
)
470 cherrypy
.session
['Authorization'] = "logout"
471 # cherrypy.response.cookie["Authorization"] = token_id
472 # cherrypy.response.cookie["Authorization"]['expires'] = 0
474 raise NbiException("Method {} not allowed for token".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
475 return self
._format
_out
(outdata
, session
)
476 except (NbiException
, EngineException
, DbException
) as e
:
477 cherrypy
.log("tokens Exception {}".format(e
))
478 cherrypy
.response
.status
= e
.http_code
.value
480 "code": e
.http_code
.name
,
481 "status": e
.http_code
.value
,
484 return self
._format
_out
(problem_details
, session
)
487 def test(self
, *args
, **kwargs
):
489 if args
and args
[0] == "help":
490 return "<html><pre>\ninit\nfile/<name> download file\ndb-clear/table\nprune\nlogin\nlogin2\n"\
491 "sleep/<time>\nmessage/topic\n</pre></html>"
493 elif args
and args
[0] == "init":
495 # self.engine.load_dbase(cherrypy.request.app.config)
496 self
.engine
.create_admin()
497 return "Done. User 'admin', password 'admin' created"
499 cherrypy
.response
.status
= HTTPStatus
.FORBIDDEN
.value
500 return self
._format
_out
("Database already initialized")
501 elif args
and args
[0] == "file":
502 return cherrypy
.lib
.static
.serve_file(cherrypy
.tree
.apps
['/osm'].config
["storage"]["path"] + "/" + args
[1],
503 "text/plain", "attachment")
504 elif args
and args
[0] == "file2":
505 f_path
= cherrypy
.tree
.apps
['/osm'].config
["storage"]["path"] + "/" + args
[1]
506 f
= open(f_path
, "r")
507 cherrypy
.response
.headers
["Content-type"] = "text/plain"
510 elif len(args
) == 2 and args
[0] == "db-clear":
511 return self
.engine
.del_item_list({"project_id": "admin"}, args
[1], {})
512 elif args
and args
[0] == "prune":
513 return self
.engine
.prune()
514 elif args
and args
[0] == "login":
515 if not cherrypy
.request
.headers
.get("Authorization"):
516 cherrypy
.response
.headers
["WWW-Authenticate"] = 'Basic realm="Access to OSM site", charset="UTF-8"'
517 cherrypy
.response
.status
= HTTPStatus
.UNAUTHORIZED
.value
518 elif args
and args
[0] == "login2":
519 if not cherrypy
.request
.headers
.get("Authorization"):
520 cherrypy
.response
.headers
["WWW-Authenticate"] = 'Bearer realm="Access to OSM site"'
521 cherrypy
.response
.status
= HTTPStatus
.UNAUTHORIZED
.value
522 elif args
and args
[0] == "sleep":
525 sleep_time
= int(args
[1])
527 cherrypy
.response
.status
= HTTPStatus
.FORBIDDEN
.value
528 return self
._format
_out
("Database already initialized")
529 thread_info
= cherrypy
.thread_data
531 time
.sleep(sleep_time
)
533 elif len(args
) >= 2 and args
[0] == "message":
535 return_text
= "<html><pre>{} ->\n".format(topic
)
537 if cherrypy
.request
.method
== 'POST':
538 to_send
= yaml
.load(cherrypy
.request
.body
)
539 for k
, v
in to_send
.items():
540 self
.engine
.msg
.write(topic
, k
, v
)
541 return_text
+= " {}: {}\n".format(k
, v
)
542 elif cherrypy
.request
.method
== 'GET':
543 for k
, v
in kwargs
.items():
544 self
.engine
.msg
.write(topic
, k
, yaml
.load(v
))
545 return_text
+= " {}: {}\n".format(k
, yaml
.load(v
))
546 except Exception as e
:
547 return_text
+= "Error: " + str(e
)
548 return_text
+= "</pre></html>\n"
552 "<html><pre>\nheaders:\n args: {}\n".format(args
) +
553 " kwargs: {}\n".format(kwargs
) +
554 " headers: {}\n".format(cherrypy
.request
.headers
) +
555 " path_info: {}\n".format(cherrypy
.request
.path_info
) +
556 " query_string: {}\n".format(cherrypy
.request
.query_string
) +
557 " session: {}\n".format(cherrypy
.session
) +
558 " cookie: {}\n".format(cherrypy
.request
.cookie
) +
559 " method: {}\n".format(cherrypy
.request
.method
) +
560 " session: {}\n".format(cherrypy
.session
.get('fieldname')) +
562 return_text
+= " length: {}\n".format(cherrypy
.request
.body
.length
)
563 if cherrypy
.request
.body
.length
:
564 return_text
+= " content: {}\n".format(
565 str(cherrypy
.request
.body
.read(int(cherrypy
.request
.headers
.get('Content-Length', 0)))))
567 return_text
+= "thread: {}\n".format(thread_info
)
568 return_text
+= "</pre></html>"
571 def _check_valid_url_method(self
, method
, *args
):
573 raise NbiException("URL must contain at least 'topic/version/item'", HTTPStatus
.METHOD_NOT_ALLOWED
)
575 reference
= self
.valid_methods
579 if not isinstance(reference
, dict):
580 raise NbiException("URL contains unexpected extra items '{}'".format(arg
),
581 HTTPStatus
.METHOD_NOT_ALLOWED
)
584 reference
= reference
[arg
]
585 elif "<ID>" in reference
:
586 reference
= reference
["<ID>"]
587 elif "*" in reference
:
588 reference
= reference
["*"]
591 raise NbiException("Unexpected URL item {}".format(arg
), HTTPStatus
.METHOD_NOT_ALLOWED
)
592 if "TODO" in reference
and method
in reference
["TODO"]:
593 raise NbiException("Method {} not supported yet for this URL".format(method
), HTTPStatus
.NOT_IMPLEMENTED
)
594 elif "METHODS" in reference
and method
not in reference
["METHODS"]:
595 raise NbiException("Method {} not supported for this URL".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
599 def _set_location_header(topic
, version
, item
, id):
601 Insert response header Location with the URL of created item base on URL params
608 # Use cherrypy.request.base for absoluted path and make use of request.header HOST just in case behind aNAT
609 cherrypy
.response
.headers
["Location"] = "/osm/{}/{}/{}/{}".format(topic
, version
, item
, id)
613 def default(self
, topic
=None, version
=None, item
=None, _id
=None, item2
=None, *args
, **kwargs
):
621 if not topic
or not version
or not item
:
622 raise NbiException("URL must contain at least 'topic/version/item'", HTTPStatus
.METHOD_NOT_ALLOWED
)
623 if topic
not in ("admin", "vnfpkgm", "nsd", "nslcm"):
624 raise NbiException("URL topic '{}' not supported".format(topic
), HTTPStatus
.METHOD_NOT_ALLOWED
)
626 raise NbiException("URL version '{}' not supported".format(version
), HTTPStatus
.METHOD_NOT_ALLOWED
)
628 if kwargs
and "METHOD" in kwargs
and kwargs
["METHOD"] in ("PUT", "POST", "DELETE", "GET", "PATCH"):
629 method
= kwargs
.pop("METHOD")
631 method
= cherrypy
.request
.method
632 if kwargs
and "FORCE" in kwargs
:
633 force
= kwargs
.pop("FORCE")
637 self
._check
_valid
_url
_method
(method
, topic
, version
, item
, _id
, item2
, *args
)
639 if topic
== "admin" and item
== "tokens":
640 return self
.token(method
, _id
, kwargs
)
642 # self.engine.load_dbase(cherrypy.request.app.config)
643 session
= self
._authorization
()
644 indata
= self
._format
_in
(kwargs
)
646 if item
== "subscriptions":
647 engine_item
= topic
+ "_" + item
653 elif topic
== "vnfpkgm":
654 engine_item
= "vnfds"
655 elif topic
== "nslcm":
657 if item
== "ns_lcm_op_occs":
658 engine_item
= "nslcmops"
660 engine_item
= "vnfrs"
661 if engine_item
== "vims": # TODO this is for backward compatibility, it will remove in the future
662 engine_item
= "vim_accounts"
665 if item2
in ("nsd_content", "package_content", "artifacts", "vnfd", "nsd"):
666 if item2
in ("vnfd", "nsd"):
670 elif item2
== "artifacts":
674 file, _format
= self
.engine
.get_file(session
, engine_item
, _id
, path
,
675 cherrypy
.request
.headers
.get("Accept"))
678 outdata
= self
.engine
.get_item_list(session
, engine_item
, kwargs
)
680 outdata
= self
.engine
.get_item(session
, engine_item
, _id
)
681 elif method
== "POST":
682 if item
in ("ns_descriptors_content", "vnf_packages_content"):
683 _id
= cherrypy
.request
.headers
.get("Transaction-Id")
685 _id
= self
.engine
.new_item(session
, engine_item
, {}, None, cherrypy
.request
.headers
,
687 rollback
= {"session": session
, "item": engine_item
, "_id": _id
, "force": True}
688 completed
= self
.engine
.upload_content(session
, engine_item
, _id
, indata
, kwargs
,
689 cherrypy
.request
.headers
)
691 self
._set
_location
_header
(topic
, version
, item
, _id
)
693 cherrypy
.response
.headers
["Transaction-Id"] = _id
694 outdata
= {"id": _id
}
695 elif item
== "ns_instances_content":
696 _id
= self
.engine
.new_item(session
, engine_item
, indata
, kwargs
, force
=force
)
697 rollback
= {"session": session
, "item": engine_item
, "_id": _id
, "force": True}
698 self
.engine
.ns_operation(session
, _id
, "instantiate", {}, None)
699 self
._set
_location
_header
(topic
, version
, item
, _id
)
700 outdata
= {"id": _id
}
701 elif item
== "ns_instances" and item2
:
702 _id
= self
.engine
.ns_operation(session
, _id
, item2
, indata
, kwargs
)
703 self
._set
_location
_header
(topic
, version
, "ns_lcm_op_occs", _id
)
704 outdata
= {"id": _id
}
705 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
707 _id
= self
.engine
.new_item(session
, engine_item
, indata
, kwargs
, cherrypy
.request
.headers
,
709 self
._set
_location
_header
(topic
, version
, item
, _id
)
710 outdata
= {"id": _id
}
711 # TODO form NsdInfo when item in ("ns_descriptors", "vnf_packages")
712 cherrypy
.response
.status
= HTTPStatus
.CREATED
.value
714 elif method
== "DELETE":
716 outdata
= self
.engine
.del_item_list(session
, engine_item
, kwargs
)
717 cherrypy
.response
.status
= HTTPStatus
.OK
.value
718 else: # len(args) > 1
719 if item
== "ns_instances_content" and not force
:
720 opp_id
= self
.engine
.ns_operation(session
, _id
, "terminate", {"autoremove": True}, None)
721 outdata
= {"_id": opp_id
}
722 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
724 self
.engine
.del_item(session
, engine_item
, _id
, force
)
725 cherrypy
.response
.status
= HTTPStatus
.NO_CONTENT
.value
726 if engine_item
in ("vim_accounts", "sdns"):
727 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
729 elif method
in ("PUT", "PATCH"):
730 if not indata
and not kwargs
:
731 raise NbiException("Nothing to update. Provide payload and/or query string",
732 HTTPStatus
.BAD_REQUEST
)
733 if item2
in ("nsd_content", "package_content") and method
== "PUT":
734 completed
= self
.engine
.upload_content(session
, engine_item
, _id
, indata
, kwargs
,
735 cherrypy
.request
.headers
)
737 cherrypy
.response
.headers
["Transaction-Id"] = id
738 cherrypy
.response
.status
= HTTPStatus
.NO_CONTENT
.value
741 outdata
= {"id": self
.engine
.edit_item(session
, engine_item
, _id
, indata
, kwargs
, force
=force
)}
743 raise NbiException("Method {} not allowed".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
744 return self
._format
_out
(outdata
, session
, _format
)
745 except (NbiException
, EngineException
, DbException
, FsException
, MsgException
) as e
:
746 cherrypy
.log("Exception {}".format(e
))
747 cherrypy
.response
.status
= e
.http_code
.value
748 if hasattr(outdata
, "close"): # is an open file
752 self
.engine
.del_item(**rollback
)
753 except Exception as e2
:
754 cherrypy
.log("Rollback Exception {}: {}".format(rollback
, e2
))
756 if isinstance(e
, MsgException
):
757 error_text
= "{} has been '{}' but other modules cannot be informed because an error on bus".format(
758 engine_item
[:-1], method
, error_text
)
760 "code": e
.http_code
.name
,
761 "status": e
.http_code
.value
,
764 return self
._format
_out
(problem_details
, session
)
765 # raise cherrypy.HTTPError(e.http_code.value, str(e))
768 # def validate_password(realm, username, password):
769 # cherrypy.log("realm "+ str(realm))
770 # if username == "admin" and password == "admin":
775 def _start_service():
777 Callback function called when cherrypy.engine starts
778 Override configuration with env variables
779 Set database, storage, message configuration
780 Init database with admin/admin user password
782 cherrypy
.log
.error("Starting osm_nbi")
783 # update general cherrypy configuration
786 engine_config
= cherrypy
.tree
.apps
['/osm'].config
787 for k
, v
in environ
.items():
788 if not k
.startswith("OSMNBI_"):
790 k1
, _
, k2
= k
[7:].lower().partition("_")
794 # update static configuration
795 if k
== 'OSMNBI_STATIC_DIR':
796 engine_config
["/static"]['tools.staticdir.dir'] = v
797 engine_config
["/static"]['tools.staticdir.on'] = True
798 elif k
== 'OSMNBI_SOCKET_PORT' or k
== 'OSMNBI_SERVER_PORT':
799 update_dict
['server.socket_port'] = int(v
)
800 elif k
== 'OSMNBI_SOCKET_HOST' or k
== 'OSMNBI_SERVER_HOST':
801 update_dict
['server.socket_host'] = v
802 elif k1
in ("server", "test", "auth", "log"):
803 update_dict
[k1
+ '.' + k2
] = v
804 elif k1
in ("message", "database", "storage"):
805 # k2 = k2.replace('_', '.')
807 engine_config
[k1
][k2
] = int(v
)
809 engine_config
[k1
][k2
] = v
810 except ValueError as e
:
811 cherrypy
.log
.error("Ignoring environ '{}': " + str(e
))
812 except Exception as e
:
813 cherrypy
.log
.warn("skipping environ '{}' on exception '{}'".format(k
, e
))
816 cherrypy
.config
.update(update_dict
)
817 engine_config
["global"].update(update_dict
)
820 log_format_simple
= "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(message)s"
821 log_formatter_simple
= logging
.Formatter(log_format_simple
, datefmt
='%Y-%m-%dT%H:%M:%S')
822 logger_server
= logging
.getLogger("cherrypy.error")
823 logger_access
= logging
.getLogger("cherrypy.access")
824 logger_cherry
= logging
.getLogger("cherrypy")
825 logger_nbi
= logging
.getLogger("nbi")
827 if "log.file" in engine_config
["global"]:
828 file_handler
= logging
.handlers
.RotatingFileHandler(engine_config
["global"]["log.file"],
829 maxBytes
=100e6
, backupCount
=9, delay
=0)
830 file_handler
.setFormatter(log_formatter_simple
)
831 logger_cherry
.addHandler(file_handler
)
832 logger_nbi
.addHandler(file_handler
)
834 for format_
, logger
in {"nbi.server": logger_server
,
835 "nbi.access": logger_access
,
836 "%(name)s %(filename)s:%(lineno)s": logger_nbi
838 log_format_cherry
= "%(asctime)s %(levelname)s {} %(message)s".format(format_
)
839 log_formatter_cherry
= logging
.Formatter(log_format_cherry
, datefmt
='%Y-%m-%dT%H:%M:%S')
840 str_handler
= logging
.StreamHandler()
841 str_handler
.setFormatter(log_formatter_cherry
)
842 logger
.addHandler(str_handler
)
844 if engine_config
["global"].get("log.level"):
845 logger_cherry
.setLevel(engine_config
["global"]["log.level"])
846 logger_nbi
.setLevel(engine_config
["global"]["log.level"])
848 # logging other modules
849 for k1
, logname
in {"message": "nbi.msg", "database": "nbi.db", "storage": "nbi.fs"}.items():
850 engine_config
[k1
]["logger_name"] = logname
851 logger_module
= logging
.getLogger(logname
)
852 if "logfile" in engine_config
[k1
]:
853 file_handler
= logging
.handlers
.RotatingFileHandler(engine_config
[k1
]["logfile"],
854 maxBytes
=100e6
, backupCount
=9, delay
=0)
855 file_handler
.setFormatter(log_formatter_simple
)
856 logger_module
.addHandler(file_handler
)
857 if "loglevel" in engine_config
[k1
]:
858 logger_module
.setLevel(engine_config
[k1
]["loglevel"])
859 # TODO add more entries, e.g.: storage
860 cherrypy
.tree
.apps
['/osm'].root
.engine
.start(engine_config
)
862 cherrypy
.tree
.apps
['/osm'].root
.engine
.init_db(target_version
=database_version
)
863 except EngineException
:
865 # getenv('OSMOPENMANO_TENANT', None)
870 Callback function called when cherrypy.engine stops
871 TODO: Ending database connections.
873 cherrypy
.tree
.apps
['/osm'].root
.engine
.stop()
874 cherrypy
.log
.error("Stopping osm_nbi")
877 def nbi(config_file
):
880 # #'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
881 # 'tools.sessions.on': True,
882 # 'tools.response_headers.on': True,
883 # # 'tools.response_headers.headers': [('Content-Type', 'text/plain')],
886 # cherrypy.Server.ssl_module = 'builtin'
887 # cherrypy.Server.ssl_certificate = "http/cert.pem"
888 # cherrypy.Server.ssl_private_key = "http/privkey.pem"
889 # cherrypy.Server.thread_pool = 10
890 # cherrypy.config.update({'Server.socket_port': config["port"], 'Server.socket_host': config["host"]})
892 # cherrypy.config.update({'tools.auth_basic.on': True,
893 # 'tools.auth_basic.realm': 'localhost',
894 # 'tools.auth_basic.checkpassword': validate_password})
895 cherrypy
.engine
.subscribe('start', _start_service
)
896 cherrypy
.engine
.subscribe('stop', _stop_service
)
897 cherrypy
.quickstart(Server(), '/osm', config_file
)
901 print("""Usage: {} [options]
902 -c|--config [configuration_file]: loads the configuration file (default: ./nbi.cfg)
903 -h|--help: shows this help
904 """.format(sys
.argv
[0]))
905 # --log-socket-host HOST: send logs to this host")
906 # --log-socket-port PORT: send logs using this port (default: 9022)")
909 if __name__
== '__main__':
911 # load parameters and configuration
912 opts
, args
= getopt
.getopt(sys
.argv
[1:], "hvc:", ["config=", "help"])
913 # TODO add "log-socket-host=", "log-socket-port=", "log-file="
916 if o
in ("-h", "--help"):
919 elif o
in ("-c", "--config"):
921 # elif o == "--log-socket-port":
922 # log_socket_port = a
923 # elif o == "--log-socket-host":
924 # log_socket_host = a
925 # elif o == "--log-file":
928 assert False, "Unhandled option"
930 if not path
.isfile(config_file
):
931 print("configuration file '{}' that not exist".format(config_file
), file=sys
.stderr
)
934 for config_file
in (__file__
[:__file__
.rfind(".")] + ".cfg", "./nbi.cfg", "/etc/osm/nbi.cfg"):
935 if path
.isfile(config_file
):
938 print("No configuration file 'nbi.cfg' found neither at local folder nor at /etc/osm/", file=sys
.stderr
)
941 except getopt
.GetoptError
as e
:
942 print(str(e
), file=sys
.stderr
)