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 os import getenv
19 from http
import HTTPStatus
20 #from http.client import responses as http_responses
21 from codecs
import getreader
22 from os
import environ
, path
24 __author__
= "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
26 # TODO consider to remove and provide version using the static version file
28 version_date
= "Apr 2018"
29 database_version
= '1.0'
32 North Bound Interface (O: OSM specific; 5,X: SOL005 not implemented yet; O5: SOL005 implemented)
33 URL: /osm GET POST PUT DELETE PATCH
35 /ns_descriptors_content O O
41 /artifacts[/<artifactPath>] O
49 /vnf_packages_content O O
53 /package_content O5 O5
56 /artifacts[/<artifactPath>] O5
61 /ns_instances_content O O
84 /vims_accounts (also vims for compatibility) O O
90 <attrName>[.<attrName>...]*[.<op>]=<value>[,<value>...]&...
91 op: "eq"(or empty to one or the values) | "neq" (to any of the values) | "gt" | "lt" | "gte" | "lte" | "cont" | "ncont"
92 all_fields, fields=x,y,.., exclude_default, exclude_fields=x,y,...
93 (none) … same as “exclude_default”
94 all_fields … all attributes.
95 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>.
96 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>.
97 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
98 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>
99 Header field name Reference Example Descriptions
100 Accept IETF RFC 7231 [19] application/json Content-Types that are acceptable for the response.
101 This header field shall be present if the response is expected to have a non-empty message body.
102 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the request.
103 This header field shall be present if the request has a non-empty message body.
104 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.
105 Range IETF RFC 7233 [21] 1000-2000 Requested range of bytes from a file
106 Header field name Reference Example Descriptions
107 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the response.
108 This header field shall be present if the response has a non-empty message body.
109 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.
110 This header field shall be present if the response status code is 201 or 3xx.
111 In the present document this header field is also used if the response status code is 202 and a new resource was created.
112 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.
113 Accept-Ranges IETF RFC 7233 [21] bytes Used by the Server to signal whether or not it supports ranges for certain resources.
114 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.
115 Retry-After IETF RFC 7231 [19] Fri, 31 Dec 1999 23:59:59 GMT
119 120 Used to indicate how long the user agent ought to wait before making a follow-up request.
120 It can be used with 503 responses.
121 The value of this field can be an HTTP-date or a number of seconds to delay after the response is received.
123 #TODO http header for partial uploads: Content-Range: "bytes 0-1199/15000". Id is returned first time and send in following chunks
127 class NbiException(Exception):
129 def __init__(self
, message
, http_code
=HTTPStatus
.METHOD_NOT_ALLOWED
):
130 Exception.__init
__(self
, message
)
131 self
.http_code
= http_code
134 class Server(object):
136 # to decode bytes to str
137 reader
= getreader("utf-8")
141 self
.engine
= Engine()
142 self
.valid_methods
= { # contains allowed URL and methods
145 "tokens": {"METHODS": ("GET", "POST", "DELETE"),
146 "<ID>": { "METHODS": ("GET", "DELETE")}
148 "users": {"METHODS": ("GET", "POST"),
149 "<ID>": {"METHODS": ("GET", "POST", "DELETE")}
151 "projects": {"METHODS": ("GET", "POST"),
152 "<ID>": {"METHODS": ("GET", "DELETE")}
154 "vims": {"METHODS": ("GET", "POST"),
155 "<ID>": {"METHODS": ("GET", "DELETE")}
157 "vim_accounts": {"METHODS": ("GET", "POST"),
158 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH")}
160 "sdns": {"METHODS": ("GET", "POST"),
161 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH")}
167 "ns_descriptors_content": { "METHODS": ("GET", "POST"),
168 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
170 "ns_descriptors": { "METHODS": ("GET", "POST"),
171 "<ID>": {"METHODS": ("GET", "DELETE"), "TODO": "PATCH",
172 "nsd_content": { "METHODS": ("GET", "PUT")},
173 "nsd": {"METHODS": "GET"}, # descriptor inside package
174 "artifacts": {"*": {"METHODS": "GET"}}
178 "pnf_descriptors": {"TODO": ("GET", "POST"),
179 "<ID>": {"TODO": ("GET", "DELETE", "PATCH"),
180 "pnfd_content": {"TODO": ("GET", "PUT")}
183 "subscriptions": {"TODO": ("GET", "POST"),
184 "<ID>": {"TODO": ("GET", "DELETE"),}
190 "vnf_packages_content": { "METHODS": ("GET", "POST"),
191 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
193 "vnf_packages": { "METHODS": ("GET", "POST"),
194 "<ID>": { "METHODS": ("GET", "DELETE"), "TODO": "PATCH", # GET: vnfPkgInfo
195 "package_content": { "METHODS": ("GET", "PUT"), # package
196 "upload_from_uri": {"TODO": "POST"}
198 "vnfd": {"METHODS": "GET"}, # descriptor inside package
199 "artifacts": {"*": {"METHODS": "GET"}}
203 "subscriptions": {"TODO": ("GET", "POST"),
204 "<ID>": {"TODO": ("GET", "DELETE"),}
210 "ns_instances_content": {"METHODS": ("GET", "POST"),
211 "<ID>": {"METHODS": ("GET", "DELETE")}
213 "ns_instances": {"METHODS": ("GET", "POST"),
214 "<ID>": {"TODO": ("GET", "DELETE"),
215 "scale": {"TODO": "POST"},
216 "terminate": {"METHODS": "POST"},
217 "instantiate": {"METHODS": "POST"},
218 "action": {"METHODS": "POST"},
221 "ns_lcm_op_occs": {"METHODS": "GET",
222 "<ID>": {"METHODS": "GET"},
224 "vnfrs": {"METHODS": ("GET"),
225 "<ID>": {"METHODS": ("GET")}
231 def _authorization(self
):
235 # 1. Get token Authorization bearer
236 auth
= cherrypy
.request
.headers
.get("Authorization")
238 auth_list
= auth
.split(" ")
239 if auth_list
[0].lower() == "bearer":
240 token
= auth_list
[-1]
241 elif auth_list
[0].lower() == "basic":
242 user_passwd64
= auth_list
[-1]
244 if cherrypy
.session
.get("Authorization"):
245 # 2. Try using session before request a new token. If not, basic authentication will generate
246 token
= cherrypy
.session
.get("Authorization")
247 if token
== "logout":
248 token
= None # force Unauthorized response to insert user pasword again
249 elif user_passwd64
and cherrypy
.request
.config
.get("auth.allow_basic_authentication"):
250 # 3. Get new token from user password
254 user_passwd
= standard_b64decode(user_passwd64
).decode()
255 user
, _
, passwd
= user_passwd
.partition(":")
258 outdata
= self
.engine
.new_token(None, {"username": user
, "password": passwd
})
259 token
= outdata
["id"]
260 cherrypy
.session
['Authorization'] = token
261 # 4. Get token from cookie
263 # auth_cookie = cherrypy.request.cookie.get("Authorization")
265 # token = auth_cookie.value
266 return self
.engine
.authorize(token
)
267 except EngineException
as e
:
268 if cherrypy
.session
.get('Authorization'):
269 del cherrypy
.session
['Authorization']
270 cherrypy
.response
.headers
["WWW-Authenticate"] = 'Bearer realm="{}"'.format(e
)
273 def _format_in(self
, kwargs
):
276 if cherrypy
.request
.body
.length
:
277 error_text
= "Invalid input format "
279 if "Content-Type" in cherrypy
.request
.headers
:
280 if "application/json" in cherrypy
.request
.headers
["Content-Type"]:
281 error_text
= "Invalid json format "
282 indata
= json
.load(self
.reader(cherrypy
.request
.body
))
283 elif "application/yaml" in cherrypy
.request
.headers
["Content-Type"]:
284 error_text
= "Invalid yaml format "
285 indata
= yaml
.load(cherrypy
.request
.body
)
286 elif "application/binary" in cherrypy
.request
.headers
["Content-Type"] or \
287 "application/gzip" in cherrypy
.request
.headers
["Content-Type"] or \
288 "application/zip" in cherrypy
.request
.headers
["Content-Type"] or \
289 "text/plain" in cherrypy
.request
.headers
["Content-Type"]:
290 indata
= cherrypy
.request
.body
# .read()
291 elif "multipart/form-data" in cherrypy
.request
.headers
["Content-Type"]:
292 if "descriptor_file" in kwargs
:
293 filecontent
= kwargs
.pop("descriptor_file")
294 if not filecontent
.file:
295 raise NbiException("empty file or content", HTTPStatus
.BAD_REQUEST
)
296 indata
= filecontent
.file # .read()
297 if filecontent
.content_type
.value
:
298 cherrypy
.request
.headers
["Content-Type"] = filecontent
.content_type
.value
300 # raise cherrypy.HTTPError(HTTPStatus.Not_Acceptable,
301 # "Only 'Content-Type' of type 'application/json' or
302 # 'application/yaml' for input format are available")
303 error_text
= "Invalid yaml format "
304 indata
= yaml
.load(cherrypy
.request
.body
)
306 error_text
= "Invalid yaml format "
307 indata
= yaml
.load(cherrypy
.request
.body
)
312 if cherrypy
.request
.headers
.get("Query-String-Format") == "yaml":
315 for k
, v
in kwargs
.items():
316 if isinstance(v
, str):
321 kwargs
[k
] = yaml
.load(v
)
324 elif k
.endswith(".gt") or k
.endswith(".lt") or k
.endswith(".gte") or k
.endswith(".lte"):
332 elif v
.find(",") > 0:
333 kwargs
[k
] = v
.split(",")
334 elif isinstance(v
, (list, tuple)):
335 for index
in range(0, len(v
)):
340 v
[index
] = yaml
.load(v
[index
])
345 except (ValueError, yaml
.YAMLError
) as exc
:
346 raise NbiException(error_text
+ str(exc
), HTTPStatus
.BAD_REQUEST
)
347 except KeyError as exc
:
348 raise NbiException("Query string error: " + str(exc
), HTTPStatus
.BAD_REQUEST
)
349 except Exception as exc
:
350 raise NbiException(error_text
+ str(exc
), HTTPStatus
.BAD_REQUEST
)
353 def _format_out(data
, session
=None, _format
=None):
355 return string of dictionary data according to requested json, yaml, xml. By default json
356 :param data: response to be sent. Can be a dict, text or file
358 :param _format: The format to be set as Content-Type ir data is a file
361 accept
= cherrypy
.request
.headers
.get("Accept")
363 if accept
and "text/html" in accept
:
364 return html
.format(data
, cherrypy
.request
, cherrypy
.response
, session
)
365 # cherrypy.response.status = HTTPStatus.NO_CONTENT.value
367 elif hasattr(data
, "read"): # file object
369 cherrypy
.response
.headers
["Content-Type"] = _format
370 elif "b" in data
.mode
: # binariy asssumig zip
371 cherrypy
.response
.headers
["Content-Type"] = 'application/zip'
373 cherrypy
.response
.headers
["Content-Type"] = 'text/plain'
374 # TODO check that cherrypy close file. If not implement pending things to close per thread next
377 if "application/json" in accept
:
378 cherrypy
.response
.headers
["Content-Type"] = 'application/json; charset=utf-8'
379 a
= json
.dumps(data
, indent
=4) + "\n"
380 return a
.encode("utf8")
381 elif "text/html" in accept
:
382 return html
.format(data
, cherrypy
.request
, cherrypy
.response
, session
)
384 elif "application/yaml" in accept
or "*/*" in accept
or "text/plain" in accept
:
387 raise cherrypy
.HTTPError(HTTPStatus
.NOT_ACCEPTABLE
.value
,
388 "Only 'Accept' of type 'application/json' or 'application/yaml' "
389 "for output format are available")
390 cherrypy
.response
.headers
["Content-Type"] = 'application/yaml'
391 return yaml
.safe_dump(data
, explicit_start
=True, indent
=4, default_flow_style
=False, tags
=False,
392 encoding
='utf-8', allow_unicode
=True) # , canonical=True, default_style='"'
395 def index(self
, *args
, **kwargs
):
398 if cherrypy
.request
.method
== "GET":
399 session
= self
._authorization
()
400 outdata
= "Index page"
402 raise cherrypy
.HTTPError(HTTPStatus
.METHOD_NOT_ALLOWED
.value
,
403 "Method {} not allowed for tokens".format(cherrypy
.request
.method
))
405 return self
._format
_out
(outdata
, session
)
407 except EngineException
as e
:
408 cherrypy
.log("index Exception {}".format(e
))
409 cherrypy
.response
.status
= e
.http_code
.value
410 return self
._format
_out
("Welcome to OSM!", session
)
413 def version(self
, *args
, **kwargs
):
414 # TODO consider to remove and provide version using the static version file
415 global __version__
, version_date
417 if cherrypy
.request
.method
!= "GET":
418 raise NbiException("Only method GET is allowed", HTTPStatus
.METHOD_NOT_ALLOWED
)
420 raise NbiException("Invalid URL or query string for version", HTTPStatus
.METHOD_NOT_ALLOWED
)
421 return __version__
+ " " + version_date
422 except NbiException
as e
:
423 cherrypy
.response
.status
= e
.http_code
.value
425 "code": e
.http_code
.name
,
426 "status": e
.http_code
.value
,
429 return self
._format
_out
(problem_details
, None)
432 def token(self
, method
, token_id
=None, kwargs
=None):
434 # self.engine.load_dbase(cherrypy.request.app.config)
435 indata
= self
._format
_in
(kwargs
)
436 if not isinstance(indata
, dict):
437 raise NbiException("Expected application/yaml or application/json Content-Type", HTTPStatus
.BAD_REQUEST
)
440 session
= self
._authorization
()
442 outdata
= self
.engine
.get_token(session
, token_id
)
444 outdata
= self
.engine
.get_token_list(session
)
445 elif method
== "POST":
447 session
= self
._authorization
()
451 indata
.update(kwargs
)
452 outdata
= self
.engine
.new_token(session
, indata
, cherrypy
.request
.remote
)
454 cherrypy
.session
['Authorization'] = outdata
["_id"]
455 self
._set
_location
_header
("admin", "v1", "tokens", outdata
["_id"])
456 # cherrypy.response.cookie["Authorization"] = outdata["id"]
457 # cherrypy.response.cookie["Authorization"]['expires'] = 3600
458 elif method
== "DELETE":
459 if not token_id
and "id" in kwargs
:
460 token_id
= kwargs
["id"]
462 session
= self
._authorization
()
463 token_id
= session
["_id"]
464 outdata
= self
.engine
.del_token(token_id
)
467 cherrypy
.session
['Authorization'] = "logout"
468 # cherrypy.response.cookie["Authorization"] = token_id
469 # cherrypy.response.cookie["Authorization"]['expires'] = 0
471 raise NbiException("Method {} not allowed for token".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
472 return self
._format
_out
(outdata
, session
)
473 except (NbiException
, EngineException
, DbException
) as e
:
474 cherrypy
.log("tokens Exception {}".format(e
))
475 cherrypy
.response
.status
= e
.http_code
.value
477 "code": e
.http_code
.name
,
478 "status": e
.http_code
.value
,
481 return self
._format
_out
(problem_details
, session
)
484 def test(self
, *args
, **kwargs
):
486 if args
and args
[0] == "help":
487 return "<html><pre>\ninit\nfile/<name> download file\ndb-clear/table\nprune\nlogin\nlogin2\n"\
488 "sleep/<time>\nmessage/topic\n</pre></html>"
490 elif args
and args
[0] == "init":
492 # self.engine.load_dbase(cherrypy.request.app.config)
493 self
.engine
.create_admin()
494 return "Done. User 'admin', password 'admin' created"
496 cherrypy
.response
.status
= HTTPStatus
.FORBIDDEN
.value
497 return self
._format
_out
("Database already initialized")
498 elif args
and args
[0] == "file":
499 return cherrypy
.lib
.static
.serve_file(cherrypy
.tree
.apps
['/osm'].config
["storage"]["path"] + "/" + args
[1],
500 "text/plain", "attachment")
501 elif args
and args
[0] == "file2":
502 f_path
= cherrypy
.tree
.apps
['/osm'].config
["storage"]["path"] + "/" + args
[1]
503 f
= open(f_path
, "r")
504 cherrypy
.response
.headers
["Content-type"] = "text/plain"
507 elif len(args
) == 2 and args
[0] == "db-clear":
508 return self
.engine
.del_item_list({"project_id": "admin"}, args
[1], {})
509 elif args
and args
[0] == "prune":
510 return self
.engine
.prune()
511 elif args
and args
[0] == "login":
512 if not cherrypy
.request
.headers
.get("Authorization"):
513 cherrypy
.response
.headers
["WWW-Authenticate"] = 'Basic realm="Access to OSM site", charset="UTF-8"'
514 cherrypy
.response
.status
= HTTPStatus
.UNAUTHORIZED
.value
515 elif args
and args
[0] == "login2":
516 if not cherrypy
.request
.headers
.get("Authorization"):
517 cherrypy
.response
.headers
["WWW-Authenticate"] = 'Bearer realm="Access to OSM site"'
518 cherrypy
.response
.status
= HTTPStatus
.UNAUTHORIZED
.value
519 elif args
and args
[0] == "sleep":
522 sleep_time
= int(args
[1])
524 cherrypy
.response
.status
= HTTPStatus
.FORBIDDEN
.value
525 return self
._format
_out
("Database already initialized")
526 thread_info
= cherrypy
.thread_data
528 time
.sleep(sleep_time
)
530 elif len(args
) >= 2 and args
[0] == "message":
532 return_text
= "<html><pre>{} ->\n".format(topic
)
534 if cherrypy
.request
.method
== 'POST':
535 to_send
= yaml
.load(cherrypy
.request
.body
)
536 for k
, v
in to_send
.items():
537 self
.engine
.msg
.write(topic
, k
, v
)
538 return_text
+= " {}: {}\n".format(k
, v
)
539 elif cherrypy
.request
.method
== 'GET':
540 for k
, v
in kwargs
.items():
541 self
.engine
.msg
.write(topic
, k
, yaml
.load(v
))
542 return_text
+= " {}: {}\n".format(k
, yaml
.load(v
))
543 except Exception as e
:
544 return_text
+= "Error: " + str(e
)
545 return_text
+= "</pre></html>\n"
549 "<html><pre>\nheaders:\n args: {}\n".format(args
) +
550 " kwargs: {}\n".format(kwargs
) +
551 " headers: {}\n".format(cherrypy
.request
.headers
) +
552 " path_info: {}\n".format(cherrypy
.request
.path_info
) +
553 " query_string: {}\n".format(cherrypy
.request
.query_string
) +
554 " session: {}\n".format(cherrypy
.session
) +
555 " cookie: {}\n".format(cherrypy
.request
.cookie
) +
556 " method: {}\n".format(cherrypy
.request
.method
) +
557 " session: {}\n".format(cherrypy
.session
.get('fieldname')) +
559 return_text
+= " length: {}\n".format(cherrypy
.request
.body
.length
)
560 if cherrypy
.request
.body
.length
:
561 return_text
+= " content: {}\n".format(
562 str(cherrypy
.request
.body
.read(int(cherrypy
.request
.headers
.get('Content-Length', 0)))))
564 return_text
+= "thread: {}\n".format(thread_info
)
565 return_text
+= "</pre></html>"
568 def _check_valid_url_method(self
, method
, *args
):
570 raise NbiException("URL must contain at least 'topic/version/item'", HTTPStatus
.METHOD_NOT_ALLOWED
)
572 reference
= self
.valid_methods
576 if not isinstance(reference
, dict):
577 raise NbiException("URL contains unexpected extra items '{}'".format(arg
),
578 HTTPStatus
.METHOD_NOT_ALLOWED
)
581 reference
= reference
[arg
]
582 elif "<ID>" in reference
:
583 reference
= reference
["<ID>"]
584 elif "*" in reference
:
585 reference
= reference
["*"]
588 raise NbiException("Unexpected URL item {}".format(arg
), HTTPStatus
.METHOD_NOT_ALLOWED
)
589 if "TODO" in reference
and method
in reference
["TODO"]:
590 raise NbiException("Method {} not supported yet for this URL".format(method
), HTTPStatus
.NOT_IMPLEMENTED
)
591 elif "METHODS" in reference
and not method
in reference
["METHODS"]:
592 raise NbiException("Method {} not supported for this URL".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
596 def _set_location_header(topic
, version
, item
, id):
598 Insert response header Location with the URL of created item base on URL params
605 # Use cherrypy.request.base for absoluted path and make use of request.header HOST just in case behind aNAT
606 cherrypy
.response
.headers
["Location"] = "/osm/{}/{}/{}/{}".format(topic
, version
, item
, id)
610 def default(self
, topic
=None, version
=None, item
=None, _id
=None, item2
=None, *args
, **kwargs
):
618 if not topic
or not version
or not item
:
619 raise NbiException("URL must contain at least 'topic/version/item'", HTTPStatus
.METHOD_NOT_ALLOWED
)
620 if topic
not in ("admin", "vnfpkgm", "nsd", "nslcm"):
621 raise NbiException("URL topic '{}' not supported".format(topic
), HTTPStatus
.METHOD_NOT_ALLOWED
)
623 raise NbiException("URL version '{}' not supported".format(version
), HTTPStatus
.METHOD_NOT_ALLOWED
)
625 if kwargs
and "METHOD" in kwargs
and kwargs
["METHOD"] in ("PUT", "POST", "DELETE", "GET", "PATCH"):
626 method
= kwargs
.pop("METHOD")
628 method
= cherrypy
.request
.method
629 if kwargs
and "FORCE" in kwargs
:
630 force
= kwargs
.pop("FORCE")
634 self
._check
_valid
_url
_method
(method
, topic
, version
, item
, _id
, item2
, *args
)
636 if topic
== "admin" and item
== "tokens":
637 return self
.token(method
, _id
, kwargs
)
639 # self.engine.load_dbase(cherrypy.request.app.config)
640 session
= self
._authorization
()
641 indata
= self
._format
_in
(kwargs
)
643 if item
== "subscriptions":
644 engine_item
= topic
+ "_" + item
650 elif topic
== "vnfpkgm":
651 engine_item
= "vnfds"
652 elif topic
== "nslcm":
654 if item
== "ns_lcm_op_occs":
655 engine_item
= "nslcmops"
657 engine_item
= "vnfrs"
658 if engine_item
== "vims": # TODO this is for backward compatibility, it will remove in the future
659 engine_item
= "vim_accounts"
662 if item2
in ("nsd_content", "package_content", "artifacts", "vnfd", "nsd"):
663 if item2
in ("vnfd", "nsd"):
667 elif item2
== "artifacts":
671 file, _format
= self
.engine
.get_file(session
, engine_item
, _id
, path
,
672 cherrypy
.request
.headers
.get("Accept"))
675 outdata
= self
.engine
.get_item_list(session
, engine_item
, kwargs
)
677 outdata
= self
.engine
.get_item(session
, engine_item
, _id
)
678 elif method
== "POST":
679 if item
in ("ns_descriptors_content", "vnf_packages_content"):
680 _id
= cherrypy
.request
.headers
.get("Transaction-Id")
682 _id
= self
.engine
.new_item(session
, engine_item
, {}, None, cherrypy
.request
.headers
,
684 rollback
= {"session": session
, "item": engine_item
, "_id": _id
, "force": True}
685 completed
= self
.engine
.upload_content(session
, engine_item
, _id
, indata
, kwargs
, cherrypy
.request
.headers
)
687 self
._set
_location
_header
(topic
, version
, item
, _id
)
689 cherrypy
.response
.headers
["Transaction-Id"] = _id
690 outdata
= {"id": _id
}
691 elif item
== "ns_instances_content":
692 _id
= self
.engine
.new_item(session
, engine_item
, indata
, kwargs
, force
=force
)
693 rollback
= {"session": session
, "item": engine_item
, "_id": _id
, "force": True}
694 self
.engine
.ns_operation(session
, _id
, "instantiate", {}, None)
695 self
._set
_location
_header
(topic
, version
, item
, _id
)
696 outdata
= {"id": _id
}
697 elif item
== "ns_instances" and item2
:
698 _id
= self
.engine
.ns_operation(session
, _id
, item2
, indata
, kwargs
)
699 self
._set
_location
_header
(topic
, version
, "ns_lcm_op_occs", _id
)
700 outdata
= {"id": _id
}
701 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
703 _id
= self
.engine
.new_item(session
, engine_item
, indata
, kwargs
, cherrypy
.request
.headers
,
705 self
._set
_location
_header
(topic
, version
, item
, _id
)
706 outdata
= {"id": _id
}
707 # TODO form NsdInfo when item in ("ns_descriptors", "vnf_packages")
708 cherrypy
.response
.status
= HTTPStatus
.CREATED
.value
710 elif method
== "DELETE":
712 outdata
= self
.engine
.del_item_list(session
, engine_item
, kwargs
)
713 cherrypy
.response
.status
= HTTPStatus
.OK
.value
714 else: # len(args) > 1
715 if item
== "ns_instances_content":
716 opp_id
= self
.engine
.ns_operation(session
, _id
, "terminate", {"autoremove": True}, None)
717 outdata
= {"_id": opp_id
}
718 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
720 self
.engine
.del_item(session
, engine_item
, _id
, force
)
721 cherrypy
.response
.status
= HTTPStatus
.NO_CONTENT
.value
722 if engine_item
in ("vim_accounts", "sdns"):
723 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
725 elif method
== "PUT":
726 if not indata
and not kwargs
:
727 raise NbiException("Nothing to update. Provide payload and/or query string",
728 HTTPStatus
.BAD_REQUEST
)
729 if item2
in ("nsd_content", "package_content"):
730 completed
= self
.engine
.upload_content(session
, engine_item
, _id
, indata
, kwargs
, cherrypy
.request
.headers
)
732 cherrypy
.response
.headers
["Transaction-Id"] = id
733 cherrypy
.response
.status
= HTTPStatus
.NO_CONTENT
.value
736 outdata
= {"id": self
.engine
.edit_item(session
, engine_item
, _id
, indata
, kwargs
, force
=force
)}
737 elif method
== "PATCH":
738 if not indata
and not kwargs
:
739 raise NbiException("Nothing to update. Provide payload and/or query string",
740 HTTPStatus
.BAD_REQUEST
)
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")
876 def nbi(config_file
):
879 # #'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
880 # 'tools.sessions.on': True,
881 # 'tools.response_headers.on': True,
882 # # 'tools.response_headers.headers': [('Content-Type', 'text/plain')],
885 # cherrypy.Server.ssl_module = 'builtin'
886 # cherrypy.Server.ssl_certificate = "http/cert.pem"
887 # cherrypy.Server.ssl_private_key = "http/privkey.pem"
888 # cherrypy.Server.thread_pool = 10
889 # cherrypy.config.update({'Server.socket_port': config["port"], 'Server.socket_host': config["host"]})
891 # cherrypy.config.update({'tools.auth_basic.on': True,
892 # 'tools.auth_basic.realm': 'localhost',
893 # 'tools.auth_basic.checkpassword': validate_password})
894 cherrypy
.engine
.subscribe('start', _start_service
)
895 cherrypy
.engine
.subscribe('stop', _stop_service
)
896 cherrypy
.quickstart(Server(), '/osm', config_file
)
900 print("""Usage: {} [options]
901 -c|--config [configuration_file]: loads the configuration file (default: ./nbi.cfg)
902 -h|--help: shows this help
903 """.format(sys
.argv
[0]))
904 # --log-socket-host HOST: send logs to this host")
905 # --log-socket-port PORT: send logs using this port (default: 9022)")
908 if __name__
== '__main__':
910 # load parameters and configuration
911 opts
, args
= getopt
.getopt(sys
.argv
[1:], "hvc:", ["config=", "help"])
912 # TODO add "log-socket-host=", "log-socket-port=", "log-file="
915 if o
in ("-h", "--help"):
918 elif o
in ("-c", "--config"):
920 # elif o == "--log-socket-port":
921 # log_socket_port = a
922 # elif o == "--log-socket-host":
923 # log_socket_host = a
924 # elif o == "--log-file":
927 assert False, "Unhandled option"
929 if not path
.isfile(config_file
):
930 print("configuration file '{}' that not exist".format(config_file
), file=sys
.stderr
)
933 for config_file
in (__file__
[:__file__
.rfind(".")] + ".cfg", "./nbi.cfg", "/etc/osm/nbi.cfg"):
934 if path
.isfile(config_file
):
937 print("No configuration file 'nbi.cfg' found neither at local folder nor at /etc/osm/", file=sys
.stderr
)
940 except getopt
.GetoptError
as e
:
941 print(str(e
), file=sys
.stderr
)