2 # -*- coding: utf-8 -*-
8 import html_out
as html
10 from engine
import Engine
, EngineException
11 from dbbase
import DbException
12 from fsbase
import FsException
13 from msgbase
import MsgException
14 from base64
import standard_b64decode
15 #from os import getenv
16 from http
import HTTPStatus
17 #from http.client import responses as http_responses
18 from codecs
import getreader
19 from os
import environ
21 __author__
= "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
23 # TODO consider to remove and provide version using the static version file
25 version_date
= "Apr 2018"
26 database_version
= '1.0'
29 North Bound Interface (O: OSM specific; 5,X: SOL005 not implemented yet; O5: SOL005 implemented)
30 URL: /osm GET POST PUT DELETE PATCH
32 /ns_descriptors_content O O
38 /artifacts[/<artifactPath>] O
46 /vnf_packages_content O O
50 /package_content O5 O5
53 /artifacts[/<artifactPath>] O5
58 /ns_instances_content O O
79 /vims_accounts (also vims for compatibility) O O
85 <attrName>[.<attrName>...]*[.<op>]=<value>[,<value>...]&...
86 op: "eq"(or empty to one or the values) | "neq" (to any of the values) | "gt" | "lt" | "gte" | "lte" | "cont" | "ncont"
87 all_fields, fields=x,y,.., exclude_default, exclude_fields=x,y,...
88 (none) … same as “exclude_default”
89 all_fields … all attributes.
90 fields=<list> … all attributes except all complex attributes with minimum cardinality of zero that are not conditionally mandatory, and that are not provided in <list>.
91 exclude_fields=<list> … all attributes except those complex attributes with a minimum cardinality of zero that are not conditionally mandatory, and that are provided in <list>.
92 exclude_default … all attributes except those complex attributes with a minimum cardinality of zero that are not conditionally mandatory, and that are part of the "default exclude set" defined in the present specification for the particular resource
93 exclude_default and include=<list> … all attributes except those complex attributes with a minimum cardinality of zero that are not conditionally mandatory and that are part of the "default exclude set" defined in the present specification for the particular resource, but that are not part of <list>
94 Header field name Reference Example Descriptions
95 Accept IETF RFC 7231 [19] application/json Content-Types that are acceptable for the response.
96 This header field shall be present if the response is expected to have a non-empty message body.
97 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the request.
98 This header field shall be present if the request has a non-empty message body.
99 Authorization IETF RFC 7235 [22] Bearer mF_9.B5f-4.1JqM The authorization token for the request. Details are specified in clause 4.5.3.
100 Range IETF RFC 7233 [21] 1000-2000 Requested range of bytes from a file
101 Header field name Reference Example Descriptions
102 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the response.
103 This header field shall be present if the response has a non-empty message body.
104 Location IETF RFC 7231 [19] http://www.example.com/vnflcm/v1/vnf_instances/123 Used in redirection, or when a new resource has been created.
105 This header field shall be present if the response status code is 201 or 3xx.
106 In the present document this header field is also used if the response status code is 202 and a new resource was created.
107 WWW-Authenticate IETF RFC 7235 [22] Bearer realm="example" Challenge if the corresponding HTTP request has not provided authorization, or error details if the corresponding HTTP request has provided an invalid authorization token.
108 Accept-Ranges IETF RFC 7233 [21] bytes Used by the Server to signal whether or not it supports ranges for certain resources.
109 Content-Range IETF RFC 7233 [21] bytes 21010-47021/ 47022 Signals the byte range that is contained in the response, and the total length of the file.
110 Retry-After IETF RFC 7231 [19] Fri, 31 Dec 1999 23:59:59 GMT
114 120 Used to indicate how long the user agent ought to wait before making a follow-up request.
115 It can be used with 503 responses.
116 The value of this field can be an HTTP-date or a number of seconds to delay after the response is received.
118 #TODO http header for partial uploads: Content-Range: "bytes 0-1199/15000". Id is returned first time and send in following chunks
122 class NbiException(Exception):
124 def __init__(self
, message
, http_code
=HTTPStatus
.METHOD_NOT_ALLOWED
):
125 Exception.__init
__(self
, message
)
126 self
.http_code
= http_code
129 class Server(object):
131 # to decode bytes to str
132 reader
= getreader("utf-8")
136 self
.engine
= Engine()
137 self
.valid_methods
= { # contains allowed URL and methods
140 "tokens": {"METHODS": ("GET", "POST", "DELETE"),
141 "<ID>": { "METHODS": ("GET", "DELETE")}
143 "users": {"METHODS": ("GET", "POST"),
144 "<ID>": {"METHODS": ("GET", "POST", "DELETE")}
146 "projects": {"METHODS": ("GET", "POST"),
147 "<ID>": {"METHODS": ("GET", "DELETE")}
149 "vims": {"METHODS": ("GET", "POST"),
150 "<ID>": {"METHODS": ("GET", "DELETE")}
152 "vim_accounts": {"METHODS": ("GET", "POST"),
153 "<ID>": {"METHODS": ("GET", "DELETE")}
155 "sdns": {"METHODS": ("GET", "POST"),
156 "<ID>": {"METHODS": ("GET", "DELETE")}
162 "ns_descriptors_content": { "METHODS": ("GET", "POST"),
163 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
165 "ns_descriptors": { "METHODS": ("GET", "POST"),
166 "<ID>": {"METHODS": ("GET", "DELETE"), "TODO": "PATCH",
167 "nsd_content": { "METHODS": ("GET", "PUT")},
168 "nsd": {"METHODS": "GET"}, # descriptor inside package
169 "artifacts": {"*": {"METHODS": "GET"}}
173 "pnf_descriptors": {"TODO": ("GET", "POST"),
174 "<ID>": {"TODO": ("GET", "DELETE", "PATCH"),
175 "pnfd_content": {"TODO": ("GET", "PUT")}
178 "subscriptions": {"TODO": ("GET", "POST"),
179 "<ID>": {"TODO": ("GET", "DELETE"),}
185 "vnf_packages_content": { "METHODS": ("GET", "POST"),
186 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
188 "vnf_packages": { "METHODS": ("GET", "POST"),
189 "<ID>": { "METHODS": ("GET", "DELETE"), "TODO": "PATCH", # GET: vnfPkgInfo
190 "package_content": { "METHODS": ("GET", "PUT"), # package
191 "upload_from_uri": {"TODO": "POST"}
193 "vnfd": {"METHODS": "GET"}, # descriptor inside package
194 "artifacts": {"*": {"METHODS": "GET"}}
198 "subscriptions": {"TODO": ("GET", "POST"),
199 "<ID>": {"TODO": ("GET", "DELETE"),}
205 "ns_instances_content": {"METHODS": ("GET", "POST"),
206 "<ID>": {"METHODS": ("GET", "DELETE")}
208 "ns_instances": {"METHODS": ("GET", "POST"),
209 "<ID>": {"TODO": ("GET", "DELETE"),
210 "scale": {"TODO": "POST"},
211 "terminate": {"METHODS": "POST"},
212 "instantiate": {"METHODS": "POST"},
213 "action": {"METHODS": "POST"},
216 "ns_lcm_op_occs": {"METHODS": "GET",
217 "<ID>": {"METHODS": "GET"},
223 def _authorization(self
):
227 # 1. Get token Authorization bearer
228 auth
= cherrypy
.request
.headers
.get("Authorization")
230 auth_list
= auth
.split(" ")
231 if auth_list
[0].lower() == "bearer":
232 token
= auth_list
[-1]
233 elif auth_list
[0].lower() == "basic":
234 user_passwd64
= auth_list
[-1]
236 if cherrypy
.session
.get("Authorization"):
237 # 2. Try using session before request a new token. If not, basic authentication will generate
238 token
= cherrypy
.session
.get("Authorization")
239 if token
== "logout":
240 token
= None # force Unauthorized response to insert user pasword again
241 elif user_passwd64
and cherrypy
.request
.config
.get("auth.allow_basic_authentication"):
242 # 3. Get new token from user password
246 user_passwd
= standard_b64decode(user_passwd64
).decode()
247 user
, _
, passwd
= user_passwd
.partition(":")
250 outdata
= self
.engine
.new_token(None, {"username": user
, "password": passwd
})
251 token
= outdata
["id"]
252 cherrypy
.session
['Authorization'] = token
253 # 4. Get token from cookie
255 # auth_cookie = cherrypy.request.cookie.get("Authorization")
257 # token = auth_cookie.value
258 return self
.engine
.authorize(token
)
259 except EngineException
as e
:
260 if cherrypy
.session
.get('Authorization'):
261 del cherrypy
.session
['Authorization']
262 cherrypy
.response
.headers
["WWW-Authenticate"] = 'Bearer realm="{}"'.format(e
)
265 def _format_in(self
, kwargs
):
268 if cherrypy
.request
.body
.length
:
269 error_text
= "Invalid input format "
271 if "Content-Type" in cherrypy
.request
.headers
:
272 if "application/json" in cherrypy
.request
.headers
["Content-Type"]:
273 error_text
= "Invalid json format "
274 indata
= json
.load(self
.reader(cherrypy
.request
.body
))
275 elif "application/yaml" in cherrypy
.request
.headers
["Content-Type"]:
276 error_text
= "Invalid yaml format "
277 indata
= yaml
.load(cherrypy
.request
.body
)
278 elif "application/binary" in cherrypy
.request
.headers
["Content-Type"] or \
279 "application/gzip" in cherrypy
.request
.headers
["Content-Type"] or \
280 "application/zip" in cherrypy
.request
.headers
["Content-Type"] or \
281 "text/plain" in cherrypy
.request
.headers
["Content-Type"]:
282 indata
= cherrypy
.request
.body
# .read()
283 elif "multipart/form-data" in cherrypy
.request
.headers
["Content-Type"]:
284 if "descriptor_file" in kwargs
:
285 filecontent
= kwargs
.pop("descriptor_file")
286 if not filecontent
.file:
287 raise NbiException("empty file or content", HTTPStatus
.BAD_REQUEST
)
288 indata
= filecontent
.file # .read()
289 if filecontent
.content_type
.value
:
290 cherrypy
.request
.headers
["Content-Type"] = filecontent
.content_type
.value
292 # raise cherrypy.HTTPError(HTTPStatus.Not_Acceptable,
293 # "Only 'Content-Type' of type 'application/json' or
294 # 'application/yaml' for input format are available")
295 error_text
= "Invalid yaml format "
296 indata
= yaml
.load(cherrypy
.request
.body
)
298 error_text
= "Invalid yaml format "
299 indata
= yaml
.load(cherrypy
.request
.body
)
304 if cherrypy
.request
.headers
.get("Query-String-Format") == "yaml":
307 for k
, v
in kwargs
.items():
308 if isinstance(v
, str):
313 kwargs
[k
] = yaml
.load(v
)
316 elif k
.endswith(".gt") or k
.endswith(".lt") or k
.endswith(".gte") or k
.endswith(".lte"):
324 elif v
.find(",") > 0:
325 kwargs
[k
] = v
.split(",")
326 elif isinstance(v
, (list, tuple)):
327 for index
in range(0, len(v
)):
332 v
[index
] = yaml
.load(v
[index
])
337 except (ValueError, yaml
.YAMLError
) as exc
:
338 raise NbiException(error_text
+ str(exc
), HTTPStatus
.BAD_REQUEST
)
339 except KeyError as exc
:
340 raise NbiException("Query string error: " + str(exc
), HTTPStatus
.BAD_REQUEST
)
343 def _format_out(data
, session
=None, _format
=None):
345 return string of dictionary data according to requested json, yaml, xml. By default json
346 :param data: response to be sent. Can be a dict, text or file
348 :param _format: The format to be set as Content-Type ir data is a file
351 accept
= cherrypy
.request
.headers
.get("Accept")
353 if accept
and "text/html" in accept
:
354 return html
.format(data
, cherrypy
.request
, cherrypy
.response
, session
)
355 # cherrypy.response.status = HTTPStatus.NO_CONTENT.value
357 elif hasattr(data
, "read"): # file object
359 cherrypy
.response
.headers
["Content-Type"] = _format
360 elif "b" in data
.mode
: # binariy asssumig zip
361 cherrypy
.response
.headers
["Content-Type"] = 'application/zip'
363 cherrypy
.response
.headers
["Content-Type"] = 'text/plain'
364 # TODO check that cherrypy close file. If not implement pending things to close per thread next
367 if "application/json" in accept
:
368 cherrypy
.response
.headers
["Content-Type"] = 'application/json; charset=utf-8'
369 a
= json
.dumps(data
, indent
=4) + "\n"
370 return a
.encode("utf8")
371 elif "text/html" in accept
:
372 return html
.format(data
, cherrypy
.request
, cherrypy
.response
, session
)
374 elif "application/yaml" in accept
or "*/*" in accept
or "text/plain" in accept
:
377 raise cherrypy
.HTTPError(HTTPStatus
.NOT_ACCEPTABLE
.value
,
378 "Only 'Accept' of type 'application/json' or 'application/yaml' "
379 "for output format are available")
380 cherrypy
.response
.headers
["Content-Type"] = 'application/yaml'
381 return yaml
.safe_dump(data
, explicit_start
=True, indent
=4, default_flow_style
=False, tags
=False,
382 encoding
='utf-8', allow_unicode
=True) # , canonical=True, default_style='"'
385 def index(self
, *args
, **kwargs
):
388 if cherrypy
.request
.method
== "GET":
389 session
= self
._authorization
()
390 outdata
= "Index page"
392 raise cherrypy
.HTTPError(HTTPStatus
.METHOD_NOT_ALLOWED
.value
,
393 "Method {} not allowed for tokens".format(cherrypy
.request
.method
))
395 return self
._format
_out
(outdata
, session
)
397 except EngineException
as e
:
398 cherrypy
.log("index Exception {}".format(e
))
399 cherrypy
.response
.status
= e
.http_code
.value
400 return self
._format
_out
("Welcome to OSM!", session
)
403 def version(self
, *args
, **kwargs
):
404 # TODO consider to remove and provide version using the static version file
405 global __version__
, version_date
407 if cherrypy
.request
.method
!= "GET":
408 raise NbiException("Only method GET is allowed", HTTPStatus
.METHOD_NOT_ALLOWED
)
410 raise NbiException("Invalid URL or query string for version", HTTPStatus
.METHOD_NOT_ALLOWED
)
411 return __version__
+ " " + version_date
412 except NbiException
as e
:
413 cherrypy
.response
.status
= e
.http_code
.value
415 "code": e
.http_code
.name
,
416 "status": e
.http_code
.value
,
419 return self
._format
_out
(problem_details
, None)
422 def token(self
, method
, token_id
=None, kwargs
=None):
424 # self.engine.load_dbase(cherrypy.request.app.config)
425 indata
= self
._format
_in
(kwargs
)
426 if not isinstance(indata
, dict):
427 raise NbiException("Expected application/yaml or application/json Content-Type", HTTPStatus
.BAD_REQUEST
)
430 session
= self
._authorization
()
432 outdata
= self
.engine
.get_token(session
, token_id
)
434 outdata
= self
.engine
.get_token_list(session
)
435 elif method
== "POST":
437 session
= self
._authorization
()
441 indata
.update(kwargs
)
442 outdata
= self
.engine
.new_token(session
, indata
, cherrypy
.request
.remote
)
444 cherrypy
.session
['Authorization'] = outdata
["_id"]
445 self
._set
_location
_header
("admin", "v1", "tokens", outdata
["_id"])
446 # cherrypy.response.cookie["Authorization"] = outdata["id"]
447 # cherrypy.response.cookie["Authorization"]['expires'] = 3600
448 elif method
== "DELETE":
449 if not token_id
and "id" in kwargs
:
450 token_id
= kwargs
["id"]
452 session
= self
._authorization
()
453 token_id
= session
["_id"]
454 outdata
= self
.engine
.del_token(token_id
)
457 cherrypy
.session
['Authorization'] = "logout"
458 # cherrypy.response.cookie["Authorization"] = token_id
459 # cherrypy.response.cookie["Authorization"]['expires'] = 0
461 raise NbiException("Method {} not allowed for token".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
462 return self
._format
_out
(outdata
, session
)
463 except (NbiException
, EngineException
, DbException
) as e
:
464 cherrypy
.log("tokens Exception {}".format(e
))
465 cherrypy
.response
.status
= e
.http_code
.value
467 "code": e
.http_code
.name
,
468 "status": e
.http_code
.value
,
471 return self
._format
_out
(problem_details
, session
)
474 def test(self
, *args
, **kwargs
):
476 if args
and args
[0] == "help":
477 return "<html><pre>\ninit\nfile/<name> download file\ndb-clear/table\nprune\nlogin\nlogin2\n"\
478 "sleep/<time>\nmessage/topic\n</pre></html>"
480 elif args
and args
[0] == "init":
482 # self.engine.load_dbase(cherrypy.request.app.config)
483 self
.engine
.create_admin()
484 return "Done. User 'admin', password 'admin' created"
486 cherrypy
.response
.status
= HTTPStatus
.FORBIDDEN
.value
487 return self
._format
_out
("Database already initialized")
488 elif args
and args
[0] == "file":
489 return cherrypy
.lib
.static
.serve_file(cherrypy
.tree
.apps
['/osm'].config
["storage"]["path"] + "/" + args
[1],
490 "text/plain", "attachment")
491 elif args
and args
[0] == "file2":
492 f_path
= cherrypy
.tree
.apps
['/osm'].config
["storage"]["path"] + "/" + args
[1]
493 f
= open(f_path
, "r")
494 cherrypy
.response
.headers
["Content-type"] = "text/plain"
497 elif len(args
) == 2 and args
[0] == "db-clear":
498 return self
.engine
.del_item_list({"project_id": "admin"}, args
[1], {})
499 elif args
and args
[0] == "prune":
500 return self
.engine
.prune()
501 elif args
and args
[0] == "login":
502 if not cherrypy
.request
.headers
.get("Authorization"):
503 cherrypy
.response
.headers
["WWW-Authenticate"] = 'Basic realm="Access to OSM site", charset="UTF-8"'
504 cherrypy
.response
.status
= HTTPStatus
.UNAUTHORIZED
.value
505 elif args
and args
[0] == "login2":
506 if not cherrypy
.request
.headers
.get("Authorization"):
507 cherrypy
.response
.headers
["WWW-Authenticate"] = 'Bearer realm="Access to OSM site"'
508 cherrypy
.response
.status
= HTTPStatus
.UNAUTHORIZED
.value
509 elif args
and args
[0] == "sleep":
512 sleep_time
= int(args
[1])
514 cherrypy
.response
.status
= HTTPStatus
.FORBIDDEN
.value
515 return self
._format
_out
("Database already initialized")
516 thread_info
= cherrypy
.thread_data
518 time
.sleep(sleep_time
)
520 elif len(args
) >= 2 and args
[0] == "message":
522 return_text
= "<html><pre>{} ->\n".format(topic
)
524 if cherrypy
.request
.method
== 'POST':
525 to_send
= yaml
.load(cherrypy
.request
.body
)
526 for k
, v
in to_send
.items():
527 self
.engine
.msg
.write(topic
, k
, v
)
528 return_text
+= " {}: {}\n".format(k
, v
)
529 elif cherrypy
.request
.method
== 'GET':
530 for k
, v
in kwargs
.items():
531 self
.engine
.msg
.write(topic
, k
, yaml
.load(v
))
532 return_text
+= " {}: {}\n".format(k
, yaml
.load(v
))
533 except Exception as e
:
534 return_text
+= "Error: " + str(e
)
535 return_text
+= "</pre></html>\n"
539 "<html><pre>\nheaders:\n args: {}\n".format(args
) +
540 " kwargs: {}\n".format(kwargs
) +
541 " headers: {}\n".format(cherrypy
.request
.headers
) +
542 " path_info: {}\n".format(cherrypy
.request
.path_info
) +
543 " query_string: {}\n".format(cherrypy
.request
.query_string
) +
544 " session: {}\n".format(cherrypy
.session
) +
545 " cookie: {}\n".format(cherrypy
.request
.cookie
) +
546 " method: {}\n".format(cherrypy
.request
.method
) +
547 " session: {}\n".format(cherrypy
.session
.get('fieldname')) +
549 return_text
+= " length: {}\n".format(cherrypy
.request
.body
.length
)
550 if cherrypy
.request
.body
.length
:
551 return_text
+= " content: {}\n".format(
552 str(cherrypy
.request
.body
.read(int(cherrypy
.request
.headers
.get('Content-Length', 0)))))
554 return_text
+= "thread: {}\n".format(thread_info
)
555 return_text
+= "</pre></html>"
558 def _check_valid_url_method(self
, method
, *args
):
560 raise NbiException("URL must contain at least 'topic/version/item'", HTTPStatus
.METHOD_NOT_ALLOWED
)
562 reference
= self
.valid_methods
566 if not isinstance(reference
, dict):
567 raise NbiException("URL contains unexpected extra items '{}'".format(arg
),
568 HTTPStatus
.METHOD_NOT_ALLOWED
)
571 reference
= reference
[arg
]
572 elif "<ID>" in reference
:
573 reference
= reference
["<ID>"]
574 elif "*" in reference
:
575 reference
= reference
["*"]
578 raise NbiException("Unexpected URL item {}".format(arg
), HTTPStatus
.METHOD_NOT_ALLOWED
)
579 if "TODO" in reference
and method
in reference
["TODO"]:
580 raise NbiException("Method {} not supported yet for this URL".format(method
), HTTPStatus
.NOT_IMPLEMENTED
)
581 elif "METHODS" in reference
and not method
in reference
["METHODS"]:
582 raise NbiException("Method {} not supported for this URL".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
586 def _set_location_header(topic
, version
, item
, id):
588 Insert response header Location with the URL of created item base on URL params
595 # Use cherrypy.request.base for absoluted path and make use of request.header HOST just in case behind aNAT
596 cherrypy
.response
.headers
["Location"] = "/osm/{}/{}/{}/{}".format(topic
, version
, item
, id)
600 def default(self
, topic
=None, version
=None, item
=None, _id
=None, item2
=None, *args
, **kwargs
):
608 if not topic
or not version
or not item
:
609 raise NbiException("URL must contain at least 'topic/version/item'", HTTPStatus
.METHOD_NOT_ALLOWED
)
610 if topic
not in ("admin", "vnfpkgm", "nsd", "nslcm"):
611 raise NbiException("URL topic '{}' not supported".format(topic
), HTTPStatus
.METHOD_NOT_ALLOWED
)
613 raise NbiException("URL version '{}' not supported".format(version
), HTTPStatus
.METHOD_NOT_ALLOWED
)
615 if kwargs
and "METHOD" in kwargs
and kwargs
["METHOD"] in ("PUT", "POST", "DELETE", "GET", "PATCH"):
616 method
= kwargs
.pop("METHOD")
618 method
= cherrypy
.request
.method
620 self
._check
_valid
_url
_method
(method
, topic
, version
, item
, _id
, item2
, *args
)
622 if topic
== "admin" and item
== "tokens":
623 return self
.token(method
, _id
, kwargs
)
625 # self.engine.load_dbase(cherrypy.request.app.config)
626 session
= self
._authorization
()
627 indata
= self
._format
_in
(kwargs
)
629 if item
== "subscriptions":
630 engine_item
= topic
+ "_" + item
636 elif topic
== "vnfpkgm":
637 engine_item
= "vnfds"
638 elif topic
== "nslcm":
640 if item
== "ns_lcm_op_occs":
641 engine_item
= "nslcmops"
642 if engine_item
== "vims": # TODO this is for backward compatibility, it will remove in the future
643 engine_item
= "vim_accounts"
646 if item2
in ("nsd_content", "package_content", "artifacts", "vnfd", "nsd"):
647 if item2
in ("vnfd", "nsd"):
651 elif item2
== "artifacts":
655 file, _format
= self
.engine
.get_file(session
, engine_item
, _id
, path
,
656 cherrypy
.request
.headers
.get("Accept"))
659 outdata
= self
.engine
.get_item_list(session
, engine_item
, kwargs
)
661 outdata
= self
.engine
.get_item(session
, engine_item
, _id
)
662 elif method
== "POST":
663 if item
in ("ns_descriptors_content", "vnf_packages_content"):
664 _id
= cherrypy
.request
.headers
.get("Transaction-Id")
666 _id
= self
.engine
.new_item(session
, engine_item
, {}, None, cherrypy
.request
.headers
)
667 rollback
= {"session": session
, "item": engine_item
, "_id": _id
, "force": True}
668 completed
= self
.engine
.upload_content(session
, engine_item
, _id
, indata
, kwargs
, cherrypy
.request
.headers
)
670 self
._set
_location
_header
(topic
, version
, item
, _id
)
672 cherrypy
.response
.headers
["Transaction-Id"] = _id
673 outdata
= {"id": _id
}
674 elif item
== "ns_instances_content":
675 _id
= self
.engine
.new_item(session
, engine_item
, indata
, kwargs
)
676 rollback
= {"session": session
, "item": engine_item
, "_id": _id
, "force": True}
677 self
.engine
.ns_action(session
, _id
, "instantiate", {}, None)
678 self
._set
_location
_header
(topic
, version
, item
, _id
)
679 outdata
= {"id": _id
}
680 elif item
== "ns_instances" and item2
:
681 _id
= self
.engine
.ns_action(session
, _id
, item2
, indata
, kwargs
)
682 self
._set
_location
_header
(topic
, version
, "ns_lcm_op_occs", _id
)
683 outdata
= {"id": _id
}
684 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
686 _id
= self
.engine
.new_item(session
, engine_item
, indata
, kwargs
, cherrypy
.request
.headers
)
687 self
._set
_location
_header
(topic
, version
, item
, _id
)
688 outdata
= {"id": _id
}
689 # TODO form NsdInfo when item in ("ns_descriptors", "vnf_packages")
690 cherrypy
.response
.status
= HTTPStatus
.CREATED
.value
692 elif method
== "DELETE":
694 outdata
= self
.engine
.del_item_list(session
, engine_item
, kwargs
)
695 cherrypy
.response
.status
= HTTPStatus
.OK
.value
696 else: # len(args) > 1
697 if item
== "ns_instances_content":
698 opp_id
= self
.engine
.ns_action(session
, _id
, "terminate", {"autoremove": True}, None)
699 outdata
= {"_id": opp_id
}
700 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
702 force
= kwargs
.get("FORCE")
703 self
.engine
.del_item(session
, engine_item
, _id
, force
)
704 cherrypy
.response
.status
= HTTPStatus
.NO_CONTENT
.value
705 if engine_item
in ("vim_accounts", "sdns"):
706 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
708 elif method
== "PUT":
709 if not indata
and not kwargs
:
710 raise NbiException("Nothing to update. Provide payload and/or query string",
711 HTTPStatus
.BAD_REQUEST
)
712 if item2
in ("nsd_content", "package_content"):
713 completed
= self
.engine
.upload_content(session
, engine_item
, _id
, indata
, kwargs
, cherrypy
.request
.headers
)
715 cherrypy
.response
.headers
["Transaction-Id"] = id
716 cherrypy
.response
.status
= HTTPStatus
.NO_CONTENT
.value
719 outdata
= {"id": self
.engine
.edit_item(session
, engine_item
, args
[1], indata
, kwargs
)}
721 raise NbiException("Method {} not allowed".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
722 return self
._format
_out
(outdata
, session
, _format
)
723 except (NbiException
, EngineException
, DbException
, FsException
, MsgException
) as e
:
724 cherrypy
.log("Exception {}".format(e
))
725 cherrypy
.response
.status
= e
.http_code
.value
726 if hasattr(outdata
, "close"): # is an open file
730 self
.engine
.del_item(**rollback
)
731 except Exception as e2
:
732 cherrypy
.log("Rollback Exception {}: {}".format(rollback
, e2
))
734 if isinstance(e
, MsgException
):
735 error_text
= "{} has been '{}' but other modules cannot be informed because an error on bus".format(
736 engine_item
[:-1], method
, error_text
)
738 "code": e
.http_code
.name
,
739 "status": e
.http_code
.value
,
742 return self
._format
_out
(problem_details
, session
)
743 # raise cherrypy.HTTPError(e.http_code.value, str(e))
746 # def validate_password(realm, username, password):
747 # cherrypy.log("realm "+ str(realm))
748 # if username == "admin" and password == "admin":
753 def _start_service():
755 Callback function called when cherrypy.engine starts
756 Override configuration with env variables
757 Set database, storage, message configuration
758 Init database with admin/admin user password
760 cherrypy
.log
.error("Starting osm_nbi")
761 # update general cherrypy configuration
764 engine_config
= cherrypy
.tree
.apps
['/osm'].config
765 for k
, v
in environ
.items():
766 if not k
.startswith("OSMNBI_"):
768 k1
, _
, k2
= k
[7:].lower().partition("_")
772 # update static configuration
773 if k
== 'OSMNBI_STATIC_DIR':
774 engine_config
["/static"]['tools.staticdir.dir'] = v
775 engine_config
["/static"]['tools.staticdir.on'] = True
776 elif k
== 'OSMNBI_SOCKET_PORT' or k
== 'OSMNBI_SERVER_PORT':
777 update_dict
['server.socket_port'] = int(v
)
778 elif k
== 'OSMNBI_SOCKET_HOST' or k
== 'OSMNBI_SERVER_HOST':
779 update_dict
['server.socket_host'] = v
781 update_dict
['server' + k2
] = v
782 # TODO add more entries
783 elif k1
in ("message", "database", "storage"):
785 engine_config
[k1
][k2
] = int(v
)
787 engine_config
[k1
][k2
] = v
788 except ValueError as e
:
789 cherrypy
.log
.error("Ignoring environ '{}': " + str(e
))
790 except Exception as e
:
791 cherrypy
.log
.warn("skipping environ '{}' on exception '{}'".format(k
, e
))
794 cherrypy
.config
.update(update_dict
)
797 log_format_simple
= "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(message)s"
798 log_formatter_simple
= logging
.Formatter(log_format_simple
, datefmt
='%Y-%m-%dT%H:%M:%S')
799 logger_server
= logging
.getLogger("cherrypy.error")
800 logger_access
= logging
.getLogger("cherrypy.access")
801 logger_cherry
= logging
.getLogger("cherrypy")
802 logger_nbi
= logging
.getLogger("nbi")
804 if "logfile" in engine_config
["global"]:
805 file_handler
= logging
.handlers
.RotatingFileHandler(engine_config
["global"]["logfile"],
806 maxBytes
=100e6
, backupCount
=9, delay
=0)
807 file_handler
.setFormatter(log_formatter_simple
)
808 logger_cherry
.addHandler(file_handler
)
809 logger_nbi
.addHandler(file_handler
)
811 for format_
, logger
in {"nbi.server": logger_server
,
812 "nbi.access": logger_access
,
813 "%(name)s %(filename)s:%(lineno)s": logger_nbi
815 log_format_cherry
= "%(asctime)s %(levelname)s {} %(message)s".format(format_
)
816 log_formatter_cherry
= logging
.Formatter(log_format_cherry
, datefmt
='%Y-%m-%dT%H:%M:%S')
817 str_handler
= logging
.StreamHandler()
818 str_handler
.setFormatter(log_formatter_cherry
)
819 logger
.addHandler(str_handler
)
821 if engine_config
["global"].get("loglevel"):
822 logger_cherry
.setLevel(engine_config
["global"]["loglevel"])
823 logger_nbi
.setLevel(engine_config
["global"]["loglevel"])
825 # logging other modules
826 for k1
, logname
in {"message": "nbi.msg", "database": "nbi.db", "storage": "nbi.fs"}.items():
827 engine_config
[k1
]["logger_name"] = logname
828 logger_module
= logging
.getLogger(logname
)
829 if "logfile" in engine_config
[k1
]:
830 file_handler
= logging
.handlers
.RotatingFileHandler(engine_config
[k1
]["logfile"],
831 maxBytes
=100e6
, backupCount
=9, delay
=0)
832 file_handler
.setFormatter(log_formatter_simple
)
833 logger_module
.addHandler(file_handler
)
834 if "loglevel" in engine_config
[k1
]:
835 logger_module
.setLevel(engine_config
[k1
]["loglevel"])
836 # TODO add more entries, e.g.: storage
837 cherrypy
.tree
.apps
['/osm'].root
.engine
.start(engine_config
)
839 cherrypy
.tree
.apps
['/osm'].root
.engine
.init_db(target_version
=database_version
)
840 except EngineException
:
842 # getenv('OSMOPENMANO_TENANT', None)
847 Callback function called when cherrypy.engine stops
848 TODO: Ending database connections.
850 cherrypy
.tree
.apps
['/osm'].root
.engine
.stop()
851 cherrypy
.log
.error("Stopping osm_nbi")
856 # #'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
857 # 'tools.sessions.on': True,
858 # 'tools.response_headers.on': True,
859 # # 'tools.response_headers.headers': [('Content-Type', 'text/plain')],
862 # cherrypy.Server.ssl_module = 'builtin'
863 # cherrypy.Server.ssl_certificate = "http/cert.pem"
864 # cherrypy.Server.ssl_private_key = "http/privkey.pem"
865 # cherrypy.Server.thread_pool = 10
866 # cherrypy.config.update({'Server.socket_port': config["port"], 'Server.socket_host': config["host"]})
868 # cherrypy.config.update({'tools.auth_basic.on': True,
869 # 'tools.auth_basic.realm': 'localhost',
870 # 'tools.auth_basic.checkpassword': validate_password})
871 cherrypy
.engine
.subscribe('start', _start_service
)
872 cherrypy
.engine
.subscribe('stop', _stop_service
)
873 cherrypy
.quickstart(Server(), '/osm', "nbi.cfg")
876 if __name__
== '__main__':