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 version_date
= "Apr 2018"
24 database_version
= '1.0'
27 North Bound Interface (O: OSM specific; 5,X: SOL005 not implemented yet; O5: SOL005 implemented)
28 URL: /osm GET POST PUT DELETE PATCH
30 /ns_descriptors_content O O
36 /artifacts[/<artifactPath>] O
44 /vnf_packages_content O O
48 /package_content O5 O5
51 /artifacts[/<artifactPath>] O5
56 /ns_instances_content O O
79 <attrName>[.<attrName>...]*[.<op>]=<value>[,<value>...]&...
80 op: "eq"(or empty to one or the values) | "neq" (to any of the values) | "gt" | "lt" | "gte" | "lte" | "cont" | "ncont"
81 all_fields, fields=x,y,.., exclude_default, exclude_fields=x,y,...
82 (none) … same as “exclude_default”
83 all_fields … all attributes.
84 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>.
85 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>.
86 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
87 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>
88 Header field name Reference Example Descriptions
89 Accept IETF RFC 7231 [19] application/json Content-Types that are acceptable for the response.
90 This header field shall be present if the response is expected to have a non-empty message body.
91 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the request.
92 This header field shall be present if the request has a non-empty message body.
93 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.
94 Range IETF RFC 7233 [21] 1000-2000 Requested range of bytes from a file
95 Header field name Reference Example Descriptions
96 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the response.
97 This header field shall be present if the response has a non-empty message body.
98 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.
99 This header field shall be present if the response status code is 201 or 3xx.
100 In the present document this header field is also used if the response status code is 202 and a new resource was created.
101 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.
102 Accept-Ranges IETF RFC 7233 [21] bytes Used by the Server to signal whether or not it supports ranges for certain resources.
103 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.
104 Retry-After IETF RFC 7231 [19] Fri, 31 Dec 1999 23:59:59 GMT
108 120 Used to indicate how long the user agent ought to wait before making a follow-up request.
109 It can be used with 503 responses.
110 The value of this field can be an HTTP-date or a number of seconds to delay after the response is received.
112 #TODO http header for partial uploads: Content-Range: "bytes 0-1199/15000". Id is returned first time and send in following chunks
116 class NbiException(Exception):
118 def __init__(self
, message
, http_code
=HTTPStatus
.METHOD_NOT_ALLOWED
):
119 Exception.__init
__(self
, message
)
120 self
.http_code
= http_code
123 class Server(object):
125 # to decode bytes to str
126 reader
= getreader("utf-8")
130 self
.engine
= Engine()
131 self
.valid_methods
= { # contains allowed URL and methods
134 "tokens": {"METHODS": ("GET", "POST", "DELETE"),
135 "<ID>": { "METHODS": ("GET", "DELETE")}
137 "users": {"METHODS": ("GET", "POST"),
138 "<ID>": {"METHODS": ("GET", "POST", "DELETE")}
140 "projects": {"METHODS": ("GET", "POST"),
141 "<ID>": {"METHODS": ("GET", "DELETE")}
143 "vims": {"METHODS": ("GET", "POST"),
144 "<ID>": {"METHODS": ("GET", "DELETE")}
146 "sdns": {"METHODS": ("GET", "POST"),
147 "<ID>": {"METHODS": ("GET", "DELETE")}
153 "ns_descriptors_content": { "METHODS": ("GET", "POST"),
154 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
156 "ns_descriptors": { "METHODS": ("GET", "POST"),
157 "<ID>": {"METHODS": ("GET", "DELETE"), "TODO": "PATCH",
158 "nsd_content": { "METHODS": ("GET", "PUT")},
159 "nsd": {"METHODS": "GET"}, # descriptor inside package
160 "artifacts": {"*": {"METHODS": "GET"}}
164 "pnf_descriptors": {"TODO": ("GET", "POST"),
165 "<ID>": {"TODO": ("GET", "DELETE", "PATCH"),
166 "pnfd_content": {"TODO": ("GET", "PUT")}
169 "subscriptions": {"TODO": ("GET", "POST"),
170 "<ID>": {"TODO": ("GET", "DELETE"),}
176 "vnf_packages_content": { "METHODS": ("GET", "POST"),
177 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
179 "vnf_packages": { "METHODS": ("GET", "POST"),
180 "<ID>": { "METHODS": ("GET", "DELETE"), "TODO": "PATCH", # GET: vnfPkgInfo
181 "package_content": { "METHODS": ("GET", "PUT"), # package
182 "upload_from_uri": {"TODO": "POST"}
184 "vnfd": {"METHODS": "GET"}, # descriptor inside package
185 "artifacts": {"*": {"METHODS": "GET"}}
189 "subscriptions": {"TODO": ("GET", "POST"),
190 "<ID>": {"TODO": ("GET", "DELETE"),}
196 "ns_instances_content": {"METHODS": ("GET", "POST"),
197 "<ID>": {"METHODS": ("GET", "DELETE")}
199 "ns_instances": {"TODO": ("GET", "POST"),
200 "<ID>": {"TODO": ("GET", "DELETE")}
206 def _authorization(self
):
210 # 1. Get token Authorization bearer
211 auth
= cherrypy
.request
.headers
.get("Authorization")
213 auth_list
= auth
.split(" ")
214 if auth_list
[0].lower() == "bearer":
215 token
= auth_list
[-1]
216 elif auth_list
[0].lower() == "basic":
217 user_passwd64
= auth_list
[-1]
219 if cherrypy
.session
.get("Authorization"):
220 # 2. Try using session before request a new token. If not, basic authentication will generate
221 token
= cherrypy
.session
.get("Authorization")
222 if token
== "logout":
223 token
= None # force Unauthorized response to insert user pasword again
224 elif user_passwd64
and cherrypy
.request
.config
.get("auth.allow_basic_authentication"):
225 # 3. Get new token from user password
229 user_passwd
= standard_b64decode(user_passwd64
).decode()
230 user
, _
, passwd
= user_passwd
.partition(":")
233 outdata
= self
.engine
.new_token(None, {"username": user
, "password": passwd
})
234 token
= outdata
["id"]
235 cherrypy
.session
['Authorization'] = token
236 # 4. Get token from cookie
238 # auth_cookie = cherrypy.request.cookie.get("Authorization")
240 # token = auth_cookie.value
241 return self
.engine
.authorize(token
)
242 except EngineException
as e
:
243 if cherrypy
.session
.get('Authorization'):
244 del cherrypy
.session
['Authorization']
245 cherrypy
.response
.headers
["WWW-Authenticate"] = 'Bearer realm="{}"'.format(e
)
248 def _format_in(self
, kwargs
):
251 if cherrypy
.request
.body
.length
:
252 error_text
= "Invalid input format "
254 if "Content-Type" in cherrypy
.request
.headers
:
255 if "application/json" in cherrypy
.request
.headers
["Content-Type"]:
256 error_text
= "Invalid json format "
257 indata
= json
.load(self
.reader(cherrypy
.request
.body
))
258 elif "application/yaml" in cherrypy
.request
.headers
["Content-Type"]:
259 error_text
= "Invalid yaml format "
260 indata
= yaml
.load(cherrypy
.request
.body
)
261 elif "application/binary" in cherrypy
.request
.headers
["Content-Type"] or \
262 "application/gzip" in cherrypy
.request
.headers
["Content-Type"] or \
263 "application/zip" in cherrypy
.request
.headers
["Content-Type"] or \
264 "text/plain" in cherrypy
.request
.headers
["Content-Type"]:
265 indata
= cherrypy
.request
.body
# .read()
266 elif "multipart/form-data" in cherrypy
.request
.headers
["Content-Type"]:
267 if "descriptor_file" in kwargs
:
268 filecontent
= kwargs
.pop("descriptor_file")
269 if not filecontent
.file:
270 raise NbiException("empty file or content", HTTPStatus
.BAD_REQUEST
)
271 indata
= filecontent
.file # .read()
272 if filecontent
.content_type
.value
:
273 cherrypy
.request
.headers
["Content-Type"] = filecontent
.content_type
.value
275 # raise cherrypy.HTTPError(HTTPStatus.Not_Acceptable,
276 # "Only 'Content-Type' of type 'application/json' or
277 # 'application/yaml' for input format are available")
278 error_text
= "Invalid yaml format "
279 indata
= yaml
.load(cherrypy
.request
.body
)
281 error_text
= "Invalid yaml format "
282 indata
= yaml
.load(cherrypy
.request
.body
)
287 if cherrypy
.request
.headers
.get("Query-String-Format") == "yaml":
290 for k
, v
in kwargs
.items():
291 if isinstance(v
, str):
296 kwargs
[k
] = yaml
.load(v
)
299 elif k
.endswith(".gt") or k
.endswith(".lt") or k
.endswith(".gte") or k
.endswith(".lte"):
307 elif v
.find(",") > 0:
308 kwargs
[k
] = v
.split(",")
309 elif isinstance(v
, (list, tuple)):
310 for index
in range(0, len(v
)):
315 v
[index
] = yaml
.load(v
[index
])
320 except (ValueError, yaml
.YAMLError
) as exc
:
321 raise NbiException(error_text
+ str(exc
), HTTPStatus
.BAD_REQUEST
)
322 except KeyError as exc
:
323 raise NbiException("Query string error: " + str(exc
), HTTPStatus
.BAD_REQUEST
)
326 def _format_out(data
, session
=None, _format
=None):
328 return string of dictionary data according to requested json, yaml, xml. By default json
329 :param data: response to be sent. Can be a dict, text or file
331 :param _format: The format to be set as Content-Type ir data is a file
334 accept
= cherrypy
.request
.headers
.get("Accept")
336 if accept
and "text/html" in accept
:
337 return html
.format(data
, cherrypy
.request
, cherrypy
.response
, session
)
338 cherrypy
.response
.status
= HTTPStatus
.NO_CONTENT
.value
340 elif hasattr(data
, "read"): # file object
342 cherrypy
.response
.headers
["Content-Type"] = _format
343 elif "b" in data
.mode
: # binariy asssumig zip
344 cherrypy
.response
.headers
["Content-Type"] = 'application/zip'
346 cherrypy
.response
.headers
["Content-Type"] = 'text/plain'
347 # TODO check that cherrypy close file. If not implement pending things to close per thread next
350 if "application/json" in accept
:
351 cherrypy
.response
.headers
["Content-Type"] = 'application/json; charset=utf-8'
352 a
= json
.dumps(data
, indent
=4) + "\n"
353 return a
.encode("utf8")
354 elif "text/html" in accept
:
355 return html
.format(data
, cherrypy
.request
, cherrypy
.response
, session
)
357 elif "application/yaml" in accept
or "*/*" in accept
or "text/plain" in accept
:
360 raise cherrypy
.HTTPError(HTTPStatus
.NOT_ACCEPTABLE
.value
,
361 "Only 'Accept' of type 'application/json' or 'application/yaml' "
362 "for output format are available")
363 cherrypy
.response
.headers
["Content-Type"] = 'application/yaml'
364 return yaml
.safe_dump(data
, explicit_start
=True, indent
=4, default_flow_style
=False, tags
=False,
365 encoding
='utf-8', allow_unicode
=True) # , canonical=True, default_style='"'
368 def index(self
, *args
, **kwargs
):
371 if cherrypy
.request
.method
== "GET":
372 session
= self
._authorization
()
373 outdata
= "Index page"
375 raise cherrypy
.HTTPError(HTTPStatus
.METHOD_NOT_ALLOWED
.value
,
376 "Method {} not allowed for tokens".format(cherrypy
.request
.method
))
378 return self
._format
_out
(outdata
, session
)
380 except EngineException
as e
:
381 cherrypy
.log("index Exception {}".format(e
))
382 cherrypy
.response
.status
= e
.http_code
.value
383 return self
._format
_out
("Welcome to OSM!", session
)
386 def version(self
, *args
, **kwargs
):
387 global __version__
, version_date
389 if cherrypy
.request
.method
!= "GET":
390 raise NbiException("Only method GET is allowed", HTTPStatus
.METHOD_NOT_ALLOWED
)
392 raise NbiException("Invalid URL or query string for version", HTTPStatus
.METHOD_NOT_ALLOWED
)
393 return __version__
+ " " + version_date
394 except NbiException
as e
:
395 cherrypy
.response
.status
= e
.http_code
.value
397 "code": e
.http_code
.name
,
398 "status": e
.http_code
.value
,
401 return self
._format
_out
(problem_details
, None)
404 def token(self
, method
, token_id
=None, kwargs
=None):
406 # self.engine.load_dbase(cherrypy.request.app.config)
407 indata
= self
._format
_in
(kwargs
)
408 if not isinstance(indata
, dict):
409 raise NbiException("Expected application/yaml or application/json Content-Type", HTTPStatus
.BAD_REQUEST
)
412 session
= self
._authorization
()
414 outdata
= self
.engine
.get_token(session
, token_id
)
416 outdata
= self
.engine
.get_token_list(session
)
417 elif method
== "POST":
419 session
= self
._authorization
()
423 indata
.update(kwargs
)
424 outdata
= self
.engine
.new_token(session
, indata
, cherrypy
.request
.remote
)
426 cherrypy
.session
['Authorization'] = outdata
["_id"]
427 self
._set
_location
_header
("admin", "v1", "tokens", outdata
["_id"])
428 # cherrypy.response.cookie["Authorization"] = outdata["id"]
429 # cherrypy.response.cookie["Authorization"]['expires'] = 3600
430 elif method
== "DELETE":
431 if not token_id
and "id" in kwargs
:
432 token_id
= kwargs
["id"]
434 session
= self
._authorization
()
435 token_id
= session
["_id"]
436 outdata
= self
.engine
.del_token(token_id
)
439 cherrypy
.session
['Authorization'] = "logout"
440 # cherrypy.response.cookie["Authorization"] = token_id
441 # cherrypy.response.cookie["Authorization"]['expires'] = 0
443 raise NbiException("Method {} not allowed for token".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
444 return self
._format
_out
(outdata
, session
)
445 except (NbiException
, EngineException
, DbException
) as e
:
446 cherrypy
.log("tokens Exception {}".format(e
))
447 cherrypy
.response
.status
= e
.http_code
.value
449 "code": e
.http_code
.name
,
450 "status": e
.http_code
.value
,
453 return self
._format
_out
(problem_details
, session
)
456 def test(self
, *args
, **kwargs
):
458 if args
and args
[0] == "help":
459 return "<html><pre>\ninit\nfile/<name> download file\ndb-clear/table\nprune\nlogin\nlogin2\n"\
460 "sleep/<time>\nmessage/topic\n</pre></html>"
462 elif args
and args
[0] == "init":
464 # self.engine.load_dbase(cherrypy.request.app.config)
465 self
.engine
.create_admin()
466 return "Done. User 'admin', password 'admin' created"
468 cherrypy
.response
.status
= HTTPStatus
.FORBIDDEN
.value
469 return self
._format
_out
("Database already initialized")
470 elif args
and args
[0] == "file":
471 return cherrypy
.lib
.static
.serve_file(cherrypy
.tree
.apps
['/osm'].config
["storage"]["path"] + "/" + args
[1],
472 "text/plain", "attachment")
473 elif args
and args
[0] == "file2":
474 f_path
= cherrypy
.tree
.apps
['/osm'].config
["storage"]["path"] + "/" + args
[1]
475 f
= open(f_path
, "r")
476 cherrypy
.response
.headers
["Content-type"] = "text/plain"
479 elif len(args
) == 2 and args
[0] == "db-clear":
480 return self
.engine
.del_item_list({"project_id": "admin"}, args
[1], {})
481 elif args
and args
[0] == "prune":
482 return self
.engine
.prune()
483 elif args
and args
[0] == "login":
484 if not cherrypy
.request
.headers
.get("Authorization"):
485 cherrypy
.response
.headers
["WWW-Authenticate"] = 'Basic realm="Access to OSM site", charset="UTF-8"'
486 cherrypy
.response
.status
= HTTPStatus
.UNAUTHORIZED
.value
487 elif args
and args
[0] == "login2":
488 if not cherrypy
.request
.headers
.get("Authorization"):
489 cherrypy
.response
.headers
["WWW-Authenticate"] = 'Bearer realm="Access to OSM site"'
490 cherrypy
.response
.status
= HTTPStatus
.UNAUTHORIZED
.value
491 elif args
and args
[0] == "sleep":
494 sleep_time
= int(args
[1])
496 cherrypy
.response
.status
= HTTPStatus
.FORBIDDEN
.value
497 return self
._format
_out
("Database already initialized")
498 thread_info
= cherrypy
.thread_data
500 time
.sleep(sleep_time
)
502 elif len(args
) >= 2 and args
[0] == "message":
504 return_text
= "<html><pre>{} ->\n".format(topic
)
506 if cherrypy
.request
.method
== 'POST':
507 to_send
= yaml
.load(cherrypy
.request
.body
)
508 for k
, v
in to_send
.items():
509 self
.engine
.msg
.write(topic
, k
, v
)
510 return_text
+= " {}: {}\n".format(k
, v
)
511 elif cherrypy
.request
.method
== 'GET':
512 for k
, v
in kwargs
.items():
513 self
.engine
.msg
.write(topic
, k
, yaml
.load(v
))
514 return_text
+= " {}: {}\n".format(k
, yaml
.load(v
))
515 except Exception as e
:
516 return_text
+= "Error: " + str(e
)
517 return_text
+= "</pre></html>\n"
521 "<html><pre>\nheaders:\n args: {}\n".format(args
) +
522 " kwargs: {}\n".format(kwargs
) +
523 " headers: {}\n".format(cherrypy
.request
.headers
) +
524 " path_info: {}\n".format(cherrypy
.request
.path_info
) +
525 " query_string: {}\n".format(cherrypy
.request
.query_string
) +
526 " session: {}\n".format(cherrypy
.session
) +
527 " cookie: {}\n".format(cherrypy
.request
.cookie
) +
528 " method: {}\n".format(cherrypy
.request
.method
) +
529 " session: {}\n".format(cherrypy
.session
.get('fieldname')) +
531 return_text
+= " length: {}\n".format(cherrypy
.request
.body
.length
)
532 if cherrypy
.request
.body
.length
:
533 return_text
+= " content: {}\n".format(
534 str(cherrypy
.request
.body
.read(int(cherrypy
.request
.headers
.get('Content-Length', 0)))))
536 return_text
+= "thread: {}\n".format(thread_info
)
537 return_text
+= "</pre></html>"
540 def _check_valid_url_method(self
, method
, *args
):
542 raise NbiException("URL must contain at least 'topic/version/item'", HTTPStatus
.METHOD_NOT_ALLOWED
)
544 reference
= self
.valid_methods
548 if not isinstance(reference
, dict):
549 raise NbiException("URL contains unexpected extra items '{}'".format(arg
),
550 HTTPStatus
.METHOD_NOT_ALLOWED
)
553 reference
= reference
[arg
]
554 elif "<ID>" in reference
:
555 reference
= reference
["<ID>"]
556 elif "*" in reference
:
557 reference
= reference
["*"]
560 raise NbiException("Unexpected URL item {}".format(arg
), HTTPStatus
.METHOD_NOT_ALLOWED
)
561 if "TODO" in reference
and method
in reference
["TODO"]:
562 raise NbiException("Method {} not supported yet for this URL".format(method
), HTTPStatus
.NOT_IMPLEMENTED
)
563 elif "METHODS" in reference
and not method
in reference
["METHODS"]:
564 raise NbiException("Method {} not supported for this URL".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
568 def _set_location_header(topic
, version
, item
, id):
570 Insert response header Location with the URL of created item base on URL params
577 # Use cherrypy.request.base for absoluted path and make use of request.header HOST just in case behind aNAT
578 cherrypy
.response
.headers
["Location"] = "/osm/{}/{}/{}/{}".format(topic
, version
, item
, id)
582 def default(self
, topic
=None, version
=None, item
=None, _id
=None, item2
=None, *args
, **kwargs
):
589 if not topic
or not version
or not item
:
590 raise NbiException("URL must contain at least 'topic/version/item'", HTTPStatus
.METHOD_NOT_ALLOWED
)
591 if topic
not in ("admin", "vnfpkgm", "nsd", "nslcm"):
592 raise NbiException("URL topic '{}' not supported".format(topic
), HTTPStatus
.METHOD_NOT_ALLOWED
)
594 raise NbiException("URL version '{}' not supported".format(version
), HTTPStatus
.METHOD_NOT_ALLOWED
)
596 if kwargs
and "METHOD" in kwargs
and kwargs
["METHOD"] in ("PUT", "POST", "DELETE", "GET", "PATCH"):
597 method
= kwargs
.pop("METHOD")
599 method
= cherrypy
.request
.method
601 self
._check
_valid
_url
_method
(method
, topic
, version
, item
, _id
, item2
, *args
)
603 if topic
== "admin" and item
== "tokens":
604 return self
.token(method
, _id
, kwargs
)
606 # self.engine.load_dbase(cherrypy.request.app.config)
607 session
= self
._authorization
()
608 indata
= self
._format
_in
(kwargs
)
610 if item
== "subscriptions":
611 engine_item
= topic
+ "_" + item
617 elif topic
== "vnfpkgm":
618 engine_item
= "vnfds"
619 elif topic
== "nslcm":
623 if item2
in ("nsd_content", "package_content", "artifacts", "vnfd", "nsd"):
624 if item2
in ("vnfd", "nsd"):
628 elif item2
== "artifacts":
632 file, _format
= self
.engine
.get_file(session
, engine_item
, _id
, path
,
633 cherrypy
.request
.headers
.get("Accept"))
636 outdata
= self
.engine
.get_item_list(session
, engine_item
, kwargs
)
638 outdata
= self
.engine
.get_item(session
, engine_item
, _id
)
639 elif method
== "POST":
640 if item
in ("ns_descriptors_content", "vnf_packages_content"):
641 _id
= cherrypy
.request
.headers
.get("Transaction-Id")
643 _id
= self
.engine
.new_item(session
, engine_item
, {}, None, cherrypy
.request
.headers
)
644 completed
= self
.engine
.upload_content(session
, engine_item
, _id
, indata
, kwargs
, cherrypy
.request
.headers
)
646 self
._set
_location
_header
(topic
, version
, item
, _id
)
648 cherrypy
.response
.headers
["Transaction-Id"] = _id
649 outdata
= {"id": _id
}
650 elif item
in ("ns_descriptors", "vnf_packages"):
651 _id
= self
.engine
.new_item(session
, engine_item
, indata
, kwargs
, cherrypy
.request
.headers
)
652 self
._set
_location
_header
(topic
, version
, item
, _id
)
654 outdata
= {"id": _id
}
656 _id
= self
.engine
.new_item(session
, engine_item
, indata
, kwargs
, cherrypy
.request
.headers
)
657 self
._set
_location
_header
(topic
, version
, item
, _id
)
658 outdata
= {"id": _id
}
659 cherrypy
.response
.status
= HTTPStatus
.CREATED
.value
660 elif method
== "DELETE":
662 outdata
= self
.engine
.del_item_list(session
, engine_item
, kwargs
)
663 else: # len(args) > 1
664 # TODO return 202 ACCEPTED for nsrs vims
665 self
.engine
.del_item(session
, engine_item
, _id
)
667 elif method
== "PUT":
668 if not indata
and not kwargs
:
669 raise NbiException("Nothing to update. Provide payload and/or query string",
670 HTTPStatus
.BAD_REQUEST
)
671 if item2
in ("nsd_content", "package_content"):
672 completed
= self
.engine
.upload_content(session
, engine_item
, _id
, indata
, kwargs
, cherrypy
.request
.headers
)
674 cherrypy
.response
.headers
["Transaction-Id"] = id
677 outdata
= {"id": self
.engine
.edit_item(session
, engine_item
, args
[1], indata
, kwargs
)}
679 raise NbiException("Method {} not allowed".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
680 return self
._format
_out
(outdata
, session
, _format
)
681 except (NbiException
, EngineException
, DbException
, FsException
, MsgException
) as e
:
682 if hasattr(outdata
, "close"): # is an open file
684 cherrypy
.log("Exception {}".format(e
))
685 cherrypy
.response
.status
= e
.http_code
.value
687 if isinstance(e
, MsgException
):
688 error_text
= "{} has been '{}' but other modules cannot be informed because an error on bus".format(
689 engine_item
[:-1], method
, error_text
)
691 "code": e
.http_code
.name
,
692 "status": e
.http_code
.value
,
695 return self
._format
_out
(problem_details
, session
)
696 # raise cherrypy.HTTPError(e.http_code.value, str(e))
699 # def validate_password(realm, username, password):
700 # cherrypy.log("realm "+ str(realm))
701 # if username == "admin" and password == "admin":
706 def _start_service():
708 Callback function called when cherrypy.engine starts
709 Override configuration with env variables
710 Set database, storage, message configuration
711 Init database with admin/admin user password
713 cherrypy
.log
.error("Starting osm_nbi")
714 # update general cherrypy configuration
717 engine_config
= cherrypy
.tree
.apps
['/osm'].config
718 for k
, v
in environ
.items():
719 if not k
.startswith("OSMNBI_"):
721 k1
, _
, k2
= k
[7:].lower().partition("_")
725 # update static configuration
726 if k
== 'OSMNBI_STATIC_DIR':
727 engine_config
["/static"]['tools.staticdir.dir'] = v
728 engine_config
["/static"]['tools.staticdir.on'] = True
729 elif k
== 'OSMNBI_SOCKET_PORT' or k
== 'OSMNBI_SERVER_PORT':
730 update_dict
['server.socket_port'] = int(v
)
731 elif k
== 'OSMNBI_SOCKET_HOST' or k
== 'OSMNBI_SERVER_HOST':
732 update_dict
['server.socket_host'] = v
734 update_dict
['server' + k2
] = v
735 # TODO add more entries
736 elif k1
in ("message", "database", "storage"):
738 engine_config
[k1
][k2
] = int(v
)
740 engine_config
[k1
][k2
] = v
741 except ValueError as e
:
742 cherrypy
.log
.error("Ignoring environ '{}': " + str(e
))
743 except Exception as e
:
744 cherrypy
.log
.warn("skipping environ '{}' on exception '{}'".format(k
, e
))
747 cherrypy
.config
.update(update_dict
)
750 log_format_simple
= "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(message)s"
751 log_formatter_simple
= logging
.Formatter(log_format_simple
, datefmt
='%Y-%m-%dT%H:%M:%S')
752 logger_server
= logging
.getLogger("cherrypy.error")
753 logger_access
= logging
.getLogger("cherrypy.access")
754 logger_cherry
= logging
.getLogger("cherrypy")
755 logger_nbi
= logging
.getLogger("nbi")
757 if "logfile" in engine_config
["global"]:
758 file_handler
= logging
.handlers
.RotatingFileHandler(engine_config
["global"]["logfile"],
759 maxBytes
=100e6
, backupCount
=9, delay
=0)
760 file_handler
.setFormatter(log_formatter_simple
)
761 logger_cherry
.addHandler(file_handler
)
762 logger_nbi
.addHandler(file_handler
)
764 for format_
, logger
in {"nbi.server": logger_server
,
765 "nbi.access": logger_access
,
766 "%(name)s %(filename)s:%(lineno)s": logger_nbi
768 log_format_cherry
= "%(asctime)s %(levelname)s {} %(message)s".format(format_
)
769 log_formatter_cherry
= logging
.Formatter(log_format_cherry
, datefmt
='%Y-%m-%dT%H:%M:%S')
770 str_handler
= logging
.StreamHandler()
771 str_handler
.setFormatter(log_formatter_cherry
)
772 logger
.addHandler(str_handler
)
774 if engine_config
["global"].get("loglevel"):
775 logger_cherry
.setLevel(engine_config
["global"]["loglevel"])
776 logger_nbi
.setLevel(engine_config
["global"]["loglevel"])
778 # logging other modules
779 for k1
, logname
in {"message": "nbi.msg", "database": "nbi.db", "storage": "nbi.fs"}.items():
780 engine_config
[k1
]["logger_name"] = logname
781 logger_module
= logging
.getLogger(logname
)
782 if "logfile" in engine_config
[k1
]:
783 file_handler
= logging
.handlers
.RotatingFileHandler(engine_config
[k1
]["logfile"],
784 maxBytes
=100e6
, backupCount
=9, delay
=0)
785 file_handler
.setFormatter(log_formatter_simple
)
786 logger_module
.addHandler(file_handler
)
787 if "loglevel" in engine_config
[k1
]:
788 logger_module
.setLevel(engine_config
[k1
]["loglevel"])
789 # TODO add more entries, e.g.: storage
790 cherrypy
.tree
.apps
['/osm'].root
.engine
.start(engine_config
)
792 cherrypy
.tree
.apps
['/osm'].root
.engine
.init_db(target_version
=database_version
)
793 except EngineException
:
795 # getenv('OSMOPENMANO_TENANT', None)
800 Callback function called when cherrypy.engine stops
801 TODO: Ending database connections.
803 cherrypy
.tree
.apps
['/osm'].root
.engine
.stop()
804 cherrypy
.log
.error("Stopping osm_nbi")
809 # #'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
810 # 'tools.sessions.on': True,
811 # 'tools.response_headers.on': True,
812 # # 'tools.response_headers.headers': [('Content-Type', 'text/plain')],
815 # cherrypy.Server.ssl_module = 'builtin'
816 # cherrypy.Server.ssl_certificate = "http/cert.pem"
817 # cherrypy.Server.ssl_private_key = "http/privkey.pem"
818 # cherrypy.Server.thread_pool = 10
819 # cherrypy.config.update({'Server.socket_port': config["port"], 'Server.socket_host': config["host"]})
821 # cherrypy.config.update({'tools.auth_basic.on': True,
822 # 'tools.auth_basic.realm': 'localhost',
823 # 'tools.auth_basic.checkpassword': validate_password})
824 cherrypy
.engine
.subscribe('start', _start_service
)
825 cherrypy
.engine
.subscribe('stop', _stop_service
)
826 cherrypy
.quickstart(Server(), '/osm', "nbi.cfg")
829 if __name__
== '__main__':