2 # -*- coding: utf-8 -*-
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at
8 # http://www.apache.org/licenses/LICENSE-2.0
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
21 import html_out
as html
23 import logging
.handlers
27 from authconn
import AuthException
28 from auth
import Authenticator
29 from engine
import Engine
, EngineException
30 from validation
import ValidationError
31 from osm_common
.dbbase
import DbException
32 from osm_common
.fsbase
import FsException
33 from osm_common
.msgbase
import MsgException
34 from http
import HTTPStatus
35 from codecs
import getreader
36 from os
import environ
, path
38 __author__
= "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
40 # TODO consider to remove and provide version using the static version file
42 version_date
= "Apr 2018"
43 database_version
= '1.0'
44 auth_database_version
= '1.0'
47 North Bound Interface (O: OSM specific; 5,X: SOL005 not implemented yet; O5: SOL005 implemented)
48 URL: /osm GET POST PUT DELETE PATCH
50 /ns_descriptors_content O O
56 /artifacts[/<artifactPath>] O
64 /vnf_packages_content O O
68 /package_content O5 O5
71 /artifacts[/<artifactPath>] O5
76 /ns_instances_content O O
88 /vnf_instances (also vnfrs for compatibility) O
104 /vims_accounts (also vims for compatibility) O O
110 /netslice_templates_content O O
112 /netslice_templates O O
116 /artifacts[/<artifactPath>] O
118 /<subscriptionId> X X
121 /netslice_instances_content O O
122 /<SliceInstanceId> O O
123 /netslice_instances O O
124 /<SliceInstanceId> O O
129 /<nsiLcmOpOccId> O O O
131 /<subscriptionId> X X
134 Follows SOL005 section 4.3.2 It contains extra METHOD to override http method, FORCE to force.
135 simpleFilterExpr := <attrName>["."<attrName>]*["."<op>]"="<value>[","<value>]*
136 filterExpr := <simpleFilterExpr>["&"<simpleFilterExpr>]*
137 op := "eq" | "neq" (or "ne") | "gt" | "lt" | "gte" | "lte" | "cont" | "ncont"
139 For filtering inside array, it must select the element of the array, or add ANYINDEX to apply the filtering over any
140 item of the array, that is, pass if any item of the array pass the filter.
141 It allows both ne and neq for not equal
142 TODO: 4.3.3 Attribute selectors
143 all_fields, fields=x,y,.., exclude_default, exclude_fields=x,y,...
144 (none) … same as “exclude_default”
145 all_fields … all attributes.
146 fields=<list> … all attributes except all complex attributes with minimum cardinality of zero that are not
147 conditionally mandatory, and that are not provided in <list>.
148 exclude_fields=<list> … all attributes except those complex attributes with a minimum cardinality of zero that
149 are not conditionally mandatory, and that are provided in <list>.
150 exclude_default … all attributes except those complex attributes with a minimum cardinality of zero that are not
151 conditionally mandatory, and that are part of the "default exclude set" defined in the present specification for
152 the particular resource
153 exclude_default and include=<list> … all attributes except those complex attributes with a minimum cardinality
154 of zero that are not conditionally mandatory and that are part of the "default exclude set" defined in the
155 present specification for the particular resource, but that are not part of <list>
156 Header field name Reference Example Descriptions
157 Accept IETF RFC 7231 [19] application/json Content-Types that are acceptable for the response.
158 This header field shall be present if the response is expected to have a non-empty message body.
159 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the request.
160 This header field shall be present if the request has a non-empty message body.
161 Authorization IETF RFC 7235 [22] Bearer mF_9.B5f-4.1JqM The authorization token for the request.
162 Details are specified in clause 4.5.3.
163 Range IETF RFC 7233 [21] 1000-2000 Requested range of bytes from a file
164 Header field name Reference Example Descriptions
165 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the response.
166 This header field shall be present if the response has a non-empty message body.
167 Location IETF RFC 7231 [19] http://www.example.com/vnflcm/v1/vnf_instances/123 Used in redirection, or when a
168 new resource has been created.
169 This header field shall be present if the response status code is 201 or 3xx.
170 In the present document this header field is also used if the response status code is 202 and a new resource was
172 WWW-Authenticate IETF RFC 7235 [22] Bearer realm="example" Challenge if the corresponding HTTP request has not
173 provided authorization, or error details if the corresponding HTTP request has provided an invalid authorization
175 Accept-Ranges IETF RFC 7233 [21] bytes Used by the Server to signal whether or not it supports ranges for
177 Content-Range IETF RFC 7233 [21] bytes 21010-47021/ 47022 Signals the byte range that is contained in the
178 response, and the total length of the file.
179 Retry-After IETF RFC 7231 [19] Fri, 31 Dec 1999 23:59:59 GMT
183 class NbiException(Exception):
185 def __init__(self
, message
, http_code
=HTTPStatus
.METHOD_NOT_ALLOWED
):
186 Exception.__init
__(self
, message
)
187 self
.http_code
= http_code
190 class Server(object):
192 # to decode bytes to str
193 reader
= getreader("utf-8")
197 self
.engine
= Engine()
198 self
.authenticator
= Authenticator()
199 self
.valid_methods
= { # contains allowed URL and methods
202 "tokens": {"METHODS": ("GET", "POST", "DELETE"),
203 "<ID>": {"METHODS": ("GET", "DELETE")}
205 "users": {"METHODS": ("GET", "POST"),
206 "<ID>": {"METHODS": ("GET", "POST", "DELETE", "PATCH", "PUT")}
208 "projects": {"METHODS": ("GET", "POST"),
209 "<ID>": {"METHODS": ("GET", "DELETE")}
211 "vims": {"METHODS": ("GET", "POST"),
212 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
214 "vim_accounts": {"METHODS": ("GET", "POST"),
215 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
217 "sdns": {"METHODS": ("GET", "POST"),
218 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
224 "pdu_descriptors": {"METHODS": ("GET", "POST"),
225 "<ID>": {"METHODS": ("GET", "POST", "DELETE", "PATCH", "PUT")}
231 "ns_descriptors_content": {"METHODS": ("GET", "POST"),
232 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
234 "ns_descriptors": {"METHODS": ("GET", "POST"),
235 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"),
236 "nsd_content": {"METHODS": ("GET", "PUT")},
237 "nsd": {"METHODS": "GET"}, # descriptor inside package
238 "artifacts": {"*": {"METHODS": "GET"}}
241 "pnf_descriptors": {"TODO": ("GET", "POST"),
242 "<ID>": {"TODO": ("GET", "DELETE", "PATCH"),
243 "pnfd_content": {"TODO": ("GET", "PUT")}
246 "subscriptions": {"TODO": ("GET", "POST"),
247 "<ID>": {"TODO": ("GET", "DELETE")}
253 "vnf_packages_content": {"METHODS": ("GET", "POST"),
254 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
256 "vnf_packages": {"METHODS": ("GET", "POST"),
257 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"), # GET: vnfPkgInfo
258 "package_content": {"METHODS": ("GET", "PUT"), # package
259 "upload_from_uri": {"TODO": "POST"}
261 "vnfd": {"METHODS": "GET"}, # descriptor inside package
262 "artifacts": {"*": {"METHODS": "GET"}}
265 "subscriptions": {"TODO": ("GET", "POST"),
266 "<ID>": {"TODO": ("GET", "DELETE")}
272 "ns_instances_content": {"METHODS": ("GET", "POST"),
273 "<ID>": {"METHODS": ("GET", "DELETE")}
275 "ns_instances": {"METHODS": ("GET", "POST"),
276 "<ID>": {"METHODS": ("GET", "DELETE"),
277 "scale": {"METHODS": "POST"},
278 "terminate": {"METHODS": "POST"},
279 "instantiate": {"METHODS": "POST"},
280 "action": {"METHODS": "POST"},
283 "ns_lcm_op_occs": {"METHODS": "GET",
284 "<ID>": {"METHODS": "GET"},
286 "vnfrs": {"METHODS": ("GET"),
287 "<ID>": {"METHODS": ("GET")}
289 "vnf_instances": {"METHODS": ("GET"),
290 "<ID>": {"METHODS": ("GET")}
296 "netslice_templates_content": {"METHODS": ("GET", "POST"),
297 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
299 "netslice_templates": {"METHODS": ("GET", "POST"),
300 "<ID>": {"METHODS": ("GET", "DELETE"), "TODO": "PATCH",
301 "nst_content": {"METHODS": ("GET", "PUT")},
302 "nst": {"METHODS": "GET"}, # descriptor inside package
303 "artifacts": {"*": {"METHODS": "GET"}}
306 "subscriptions": {"TODO": ("GET", "POST"),
307 "<ID>": {"TODO": ("GET", "DELETE")}
313 "netslice_instances_content": {"METHODS": ("GET", "POST"),
314 "<ID>": {"METHODS": ("GET", "DELETE")}
316 "netslice_instances": {"METHODS": ("GET", "POST"),
317 "<ID>": {"METHODS": ("GET", "DELETE"),
318 "terminate": {"METHODS": "POST"},
319 "instantiate": {"METHODS": "POST"},
320 "action": {"METHODS": "POST"},
323 "nsi_lcm_op_occs": {"METHODS": "GET",
324 "<ID>": {"METHODS": "GET"},
330 def _format_in(self
, kwargs
):
333 if cherrypy
.request
.body
.length
:
334 error_text
= "Invalid input format "
336 if "Content-Type" in cherrypy
.request
.headers
:
337 if "application/json" in cherrypy
.request
.headers
["Content-Type"]:
338 error_text
= "Invalid json format "
339 indata
= json
.load(self
.reader(cherrypy
.request
.body
))
340 cherrypy
.request
.headers
.pop("Content-File-MD5", None)
341 elif "application/yaml" in cherrypy
.request
.headers
["Content-Type"]:
342 error_text
= "Invalid yaml format "
343 indata
= yaml
.load(cherrypy
.request
.body
)
344 cherrypy
.request
.headers
.pop("Content-File-MD5", None)
345 elif "application/binary" in cherrypy
.request
.headers
["Content-Type"] or \
346 "application/gzip" in cherrypy
.request
.headers
["Content-Type"] or \
347 "application/zip" in cherrypy
.request
.headers
["Content-Type"] or \
348 "text/plain" in cherrypy
.request
.headers
["Content-Type"]:
349 indata
= cherrypy
.request
.body
# .read()
350 elif "multipart/form-data" in cherrypy
.request
.headers
["Content-Type"]:
351 if "descriptor_file" in kwargs
:
352 filecontent
= kwargs
.pop("descriptor_file")
353 if not filecontent
.file:
354 raise NbiException("empty file or content", HTTPStatus
.BAD_REQUEST
)
355 indata
= filecontent
.file # .read()
356 if filecontent
.content_type
.value
:
357 cherrypy
.request
.headers
["Content-Type"] = filecontent
.content_type
.value
359 # raise cherrypy.HTTPError(HTTPStatus.Not_Acceptable,
360 # "Only 'Content-Type' of type 'application/json' or
361 # 'application/yaml' for input format are available")
362 error_text
= "Invalid yaml format "
363 indata
= yaml
.load(cherrypy
.request
.body
)
364 cherrypy
.request
.headers
.pop("Content-File-MD5", None)
366 error_text
= "Invalid yaml format "
367 indata
= yaml
.load(cherrypy
.request
.body
)
368 cherrypy
.request
.headers
.pop("Content-File-MD5", None)
373 if cherrypy
.request
.headers
.get("Query-String-Format") == "yaml":
376 for k
, v
in kwargs
.items():
377 if isinstance(v
, str):
382 kwargs
[k
] = yaml
.load(v
)
385 elif k
.endswith(".gt") or k
.endswith(".lt") or k
.endswith(".gte") or k
.endswith(".lte"):
393 elif v
.find(",") > 0:
394 kwargs
[k
] = v
.split(",")
395 elif isinstance(v
, (list, tuple)):
396 for index
in range(0, len(v
)):
401 v
[index
] = yaml
.load(v
[index
])
406 except (ValueError, yaml
.YAMLError
) as exc
:
407 raise NbiException(error_text
+ str(exc
), HTTPStatus
.BAD_REQUEST
)
408 except KeyError as exc
:
409 raise NbiException("Query string error: " + str(exc
), HTTPStatus
.BAD_REQUEST
)
410 except Exception as exc
:
411 raise NbiException(error_text
+ str(exc
), HTTPStatus
.BAD_REQUEST
)
414 def _format_out(data
, session
=None, _format
=None):
416 return string of dictionary data according to requested json, yaml, xml. By default json
417 :param data: response to be sent. Can be a dict, text or file
419 :param _format: The format to be set as Content-Type ir data is a file
422 accept
= cherrypy
.request
.headers
.get("Accept")
424 if accept
and "text/html" in accept
:
425 return html
.format(data
, cherrypy
.request
, cherrypy
.response
, session
)
426 # cherrypy.response.status = HTTPStatus.NO_CONTENT.value
428 elif hasattr(data
, "read"): # file object
430 cherrypy
.response
.headers
["Content-Type"] = _format
431 elif "b" in data
.mode
: # binariy asssumig zip
432 cherrypy
.response
.headers
["Content-Type"] = 'application/zip'
434 cherrypy
.response
.headers
["Content-Type"] = 'text/plain'
435 # TODO check that cherrypy close file. If not implement pending things to close per thread next
438 if "application/json" in accept
:
439 cherrypy
.response
.headers
["Content-Type"] = 'application/json; charset=utf-8'
440 a
= json
.dumps(data
, indent
=4) + "\n"
441 return a
.encode("utf8")
442 elif "text/html" in accept
:
443 return html
.format(data
, cherrypy
.request
, cherrypy
.response
, session
)
445 elif "application/yaml" in accept
or "*/*" in accept
or "text/plain" in accept
:
448 raise cherrypy
.HTTPError(HTTPStatus
.NOT_ACCEPTABLE
.value
,
449 "Only 'Accept' of type 'application/json' or 'application/yaml' "
450 "for output format are available")
451 cherrypy
.response
.headers
["Content-Type"] = 'application/yaml'
452 return yaml
.safe_dump(data
, explicit_start
=True, indent
=4, default_flow_style
=False, tags
=False,
453 encoding
='utf-8', allow_unicode
=True) # , canonical=True, default_style='"'
456 def index(self
, *args
, **kwargs
):
459 if cherrypy
.request
.method
== "GET":
460 session
= self
.authenticator
.authorize()
461 outdata
= "Index page"
463 raise cherrypy
.HTTPError(HTTPStatus
.METHOD_NOT_ALLOWED
.value
,
464 "Method {} not allowed for tokens".format(cherrypy
.request
.method
))
466 return self
._format
_out
(outdata
, session
)
468 except (EngineException
, AuthException
) as e
:
469 cherrypy
.log("index Exception {}".format(e
))
470 cherrypy
.response
.status
= e
.http_code
.value
471 return self
._format
_out
("Welcome to OSM!", session
)
474 def version(self
, *args
, **kwargs
):
475 # TODO consider to remove and provide version using the static version file
476 global __version__
, version_date
478 if cherrypy
.request
.method
!= "GET":
479 raise NbiException("Only method GET is allowed", HTTPStatus
.METHOD_NOT_ALLOWED
)
481 raise NbiException("Invalid URL or query string for version", HTTPStatus
.METHOD_NOT_ALLOWED
)
482 return __version__
+ " " + version_date
483 except NbiException
as e
:
484 cherrypy
.response
.status
= e
.http_code
.value
486 "code": e
.http_code
.name
,
487 "status": e
.http_code
.value
,
490 return self
._format
_out
(problem_details
, None)
493 def token(self
, method
, token_id
=None, kwargs
=None):
495 # self.engine.load_dbase(cherrypy.request.app.config)
496 indata
= self
._format
_in
(kwargs
)
497 if not isinstance(indata
, dict):
498 raise NbiException("Expected application/yaml or application/json Content-Type", HTTPStatus
.BAD_REQUEST
)
501 session
= self
.authenticator
.authorize()
503 outdata
= self
.authenticator
.get_token(session
, token_id
)
505 outdata
= self
.authenticator
.get_token_list(session
)
506 elif method
== "POST":
508 session
= self
.authenticator
.authorize()
512 indata
.update(kwargs
)
513 outdata
= self
.authenticator
.new_token(session
, indata
, cherrypy
.request
.remote
)
515 cherrypy
.session
['Authorization'] = outdata
["_id"]
516 self
._set
_location
_header
("admin", "v1", "tokens", outdata
["_id"])
517 # cherrypy.response.cookie["Authorization"] = outdata["id"]
518 # cherrypy.response.cookie["Authorization"]['expires'] = 3600
519 elif method
== "DELETE":
520 if not token_id
and "id" in kwargs
:
521 token_id
= kwargs
["id"]
523 session
= self
.authenticator
.authorize()
524 token_id
= session
["_id"]
525 outdata
= self
.authenticator
.del_token(token_id
)
527 cherrypy
.session
['Authorization'] = "logout"
528 # cherrypy.response.cookie["Authorization"] = token_id
529 # cherrypy.response.cookie["Authorization"]['expires'] = 0
531 raise NbiException("Method {} not allowed for token".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
532 return self
._format
_out
(outdata
, session
)
533 except (NbiException
, EngineException
, DbException
, AuthException
) as e
:
534 cherrypy
.log("tokens Exception {}".format(e
))
535 cherrypy
.response
.status
= e
.http_code
.value
537 "code": e
.http_code
.name
,
538 "status": e
.http_code
.value
,
541 return self
._format
_out
(problem_details
, session
)
544 def test(self
, *args
, **kwargs
):
546 if args
and args
[0] == "help":
547 return "<html><pre>\ninit\nfile/<name> download file\ndb-clear/table\nprune\nlogin\nlogin2\n"\
548 "sleep/<time>\nmessage/topic\n</pre></html>"
550 elif args
and args
[0] == "init":
552 # self.engine.load_dbase(cherrypy.request.app.config)
553 self
.engine
.create_admin()
554 return "Done. User 'admin', password 'admin' created"
556 cherrypy
.response
.status
= HTTPStatus
.FORBIDDEN
.value
557 return self
._format
_out
("Database already initialized")
558 elif args
and args
[0] == "file":
559 return cherrypy
.lib
.static
.serve_file(cherrypy
.tree
.apps
['/osm'].config
["storage"]["path"] + "/" + args
[1],
560 "text/plain", "attachment")
561 elif args
and args
[0] == "file2":
562 f_path
= cherrypy
.tree
.apps
['/osm'].config
["storage"]["path"] + "/" + args
[1]
563 f
= open(f_path
, "r")
564 cherrypy
.response
.headers
["Content-type"] = "text/plain"
567 elif len(args
) == 2 and args
[0] == "db-clear":
568 return self
.engine
.db
.del_list(args
[1], kwargs
)
569 elif args
and args
[0] == "prune":
570 return self
.engine
.prune()
571 elif args
and args
[0] == "login":
572 if not cherrypy
.request
.headers
.get("Authorization"):
573 cherrypy
.response
.headers
["WWW-Authenticate"] = 'Basic realm="Access to OSM site", charset="UTF-8"'
574 cherrypy
.response
.status
= HTTPStatus
.UNAUTHORIZED
.value
575 elif args
and args
[0] == "login2":
576 if not cherrypy
.request
.headers
.get("Authorization"):
577 cherrypy
.response
.headers
["WWW-Authenticate"] = 'Bearer realm="Access to OSM site"'
578 cherrypy
.response
.status
= HTTPStatus
.UNAUTHORIZED
.value
579 elif args
and args
[0] == "sleep":
582 sleep_time
= int(args
[1])
584 cherrypy
.response
.status
= HTTPStatus
.FORBIDDEN
.value
585 return self
._format
_out
("Database already initialized")
586 thread_info
= cherrypy
.thread_data
588 time
.sleep(sleep_time
)
590 elif len(args
) >= 2 and args
[0] == "message":
592 return_text
= "<html><pre>{} ->\n".format(main_topic
)
594 if cherrypy
.request
.method
== 'POST':
595 to_send
= yaml
.load(cherrypy
.request
.body
)
596 for k
, v
in to_send
.items():
597 self
.engine
.msg
.write(main_topic
, k
, v
)
598 return_text
+= " {}: {}\n".format(k
, v
)
599 elif cherrypy
.request
.method
== 'GET':
600 for k
, v
in kwargs
.items():
601 self
.engine
.msg
.write(main_topic
, k
, yaml
.load(v
))
602 return_text
+= " {}: {}\n".format(k
, yaml
.load(v
))
603 except Exception as e
:
604 return_text
+= "Error: " + str(e
)
605 return_text
+= "</pre></html>\n"
609 "<html><pre>\nheaders:\n args: {}\n".format(args
) +
610 " kwargs: {}\n".format(kwargs
) +
611 " headers: {}\n".format(cherrypy
.request
.headers
) +
612 " path_info: {}\n".format(cherrypy
.request
.path_info
) +
613 " query_string: {}\n".format(cherrypy
.request
.query_string
) +
614 " session: {}\n".format(cherrypy
.session
) +
615 " cookie: {}\n".format(cherrypy
.request
.cookie
) +
616 " method: {}\n".format(cherrypy
.request
.method
) +
617 " session: {}\n".format(cherrypy
.session
.get('fieldname')) +
619 return_text
+= " length: {}\n".format(cherrypy
.request
.body
.length
)
620 if cherrypy
.request
.body
.length
:
621 return_text
+= " content: {}\n".format(
622 str(cherrypy
.request
.body
.read(int(cherrypy
.request
.headers
.get('Content-Length', 0)))))
624 return_text
+= "thread: {}\n".format(thread_info
)
625 return_text
+= "</pre></html>"
628 def _check_valid_url_method(self
, method
, *args
):
630 raise NbiException("URL must contain at least 'main_topic/version/topic'", HTTPStatus
.METHOD_NOT_ALLOWED
)
632 reference
= self
.valid_methods
636 if not isinstance(reference
, dict):
637 raise NbiException("URL contains unexpected extra items '{}'".format(arg
),
638 HTTPStatus
.METHOD_NOT_ALLOWED
)
641 reference
= reference
[arg
]
642 elif "<ID>" in reference
:
643 reference
= reference
["<ID>"]
644 elif "*" in reference
:
645 reference
= reference
["*"]
648 raise NbiException("Unexpected URL item {}".format(arg
), HTTPStatus
.METHOD_NOT_ALLOWED
)
649 if "TODO" in reference
and method
in reference
["TODO"]:
650 raise NbiException("Method {} not supported yet for this URL".format(method
), HTTPStatus
.NOT_IMPLEMENTED
)
651 elif "METHODS" in reference
and method
not in reference
["METHODS"]:
652 raise NbiException("Method {} not supported for this URL".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
656 def _set_location_header(main_topic
, version
, topic
, id):
658 Insert response header Location with the URL of created item base on URL params
665 # Use cherrypy.request.base for absoluted path and make use of request.header HOST just in case behind aNAT
666 cherrypy
.response
.headers
["Location"] = "/osm/{}/{}/{}/{}".format(main_topic
, version
, topic
, id)
670 def default(self
, main_topic
=None, version
=None, topic
=None, _id
=None, item
=None, *args
, **kwargs
):
679 if not main_topic
or not version
or not topic
:
680 raise NbiException("URL must contain at least 'main_topic/version/topic'",
681 HTTPStatus
.METHOD_NOT_ALLOWED
)
682 if main_topic
not in ("admin", "vnfpkgm", "nsd", "nslcm", "pdu", "nst", "nsilcm"):
683 raise NbiException("URL main_topic '{}' not supported".format(main_topic
),
684 HTTPStatus
.METHOD_NOT_ALLOWED
)
686 raise NbiException("URL version '{}' not supported".format(version
), HTTPStatus
.METHOD_NOT_ALLOWED
)
688 if kwargs
and "METHOD" in kwargs
and kwargs
["METHOD"] in ("PUT", "POST", "DELETE", "GET", "PATCH"):
689 method
= kwargs
.pop("METHOD")
691 method
= cherrypy
.request
.method
692 if kwargs
and "FORCE" in kwargs
:
693 force
= kwargs
.pop("FORCE")
696 self
._check
_valid
_url
_method
(method
, main_topic
, version
, topic
, _id
, item
, *args
)
697 if main_topic
== "admin" and topic
== "tokens":
698 return self
.token(method
, _id
, kwargs
)
700 # self.engine.load_dbase(cherrypy.request.app.config)
701 session
= self
.authenticator
.authorize()
702 indata
= self
._format
_in
(kwargs
)
704 if topic
== "subscriptions":
705 engine_topic
= main_topic
+ "_" + topic
709 if main_topic
== "nsd":
710 engine_topic
= "nsds"
711 elif main_topic
== "vnfpkgm":
712 engine_topic
= "vnfds"
713 elif main_topic
== "nslcm":
714 engine_topic
= "nsrs"
715 if topic
== "ns_lcm_op_occs":
716 engine_topic
= "nslcmops"
717 if topic
== "vnfrs" or topic
== "vnf_instances":
718 engine_topic
= "vnfrs"
719 elif main_topic
== "nst":
720 engine_topic
= "nsts"
721 elif main_topic
== "nsilcm":
722 engine_topic
= "nsis"
723 if topic
== "nsi_lcm_op_occs":
724 engine_topic
= "nsilcmops"
725 elif main_topic
== "pdu":
726 engine_topic
= "pdus"
727 if engine_topic
== "vims": # TODO this is for backward compatibility, it will remove in the future
728 engine_topic
= "vim_accounts"
731 if item
in ("nsd_content", "package_content", "artifacts", "vnfd", "nsd", "nst", "nst_content"):
732 if item
in ("vnfd", "nsd", "nst"):
736 elif item
== "artifacts":
740 file, _format
= self
.engine
.get_file(session
, engine_topic
, _id
, path
,
741 cherrypy
.request
.headers
.get("Accept"))
744 outdata
= self
.engine
.get_item_list(session
, engine_topic
, kwargs
)
746 outdata
= self
.engine
.get_item(session
, engine_topic
, _id
)
747 elif method
== "POST":
748 if topic
in ("ns_descriptors_content", "vnf_packages_content", "netslice_templates_content"):
749 _id
= cherrypy
.request
.headers
.get("Transaction-Id")
751 _id
= self
.engine
.new_item(rollback
, session
, engine_topic
, {}, None, cherrypy
.request
.headers
,
753 completed
= self
.engine
.upload_content(session
, engine_topic
, _id
, indata
, kwargs
,
754 cherrypy
.request
.headers
, force
=force
)
756 self
._set
_location
_header
(main_topic
, version
, topic
, _id
)
758 cherrypy
.response
.headers
["Transaction-Id"] = _id
759 outdata
= {"id": _id
}
760 elif topic
== "ns_instances_content":
762 _id
= self
.engine
.new_item(rollback
, session
, engine_topic
, indata
, kwargs
, force
=force
)
764 indata
["lcmOperationType"] = "instantiate"
765 indata
["nsInstanceId"] = _id
766 self
.engine
.new_item(rollback
, session
, "nslcmops", indata
, None)
767 self
._set
_location
_header
(main_topic
, version
, topic
, _id
)
768 outdata
= {"id": _id
}
769 elif topic
== "ns_instances" and item
:
770 indata
["lcmOperationType"] = item
771 indata
["nsInstanceId"] = _id
772 _id
= self
.engine
.new_item(rollback
, session
, "nslcmops", indata
, kwargs
)
773 self
._set
_location
_header
(main_topic
, version
, "ns_lcm_op_occs", _id
)
774 outdata
= {"id": _id
}
775 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
776 elif topic
== "netslice_instances_content":
777 # creates NetSlice_Instance_record (NSIR)
778 _id
= self
.engine
.new_item(rollback
, session
, engine_topic
, indata
, kwargs
, force
=force
)
779 self
._set
_location
_header
(main_topic
, version
, topic
, _id
)
780 indata
["lcmOperationType"] = "instantiate"
781 indata
["nsiInstanceId"] = _id
782 self
.engine
.new_item(rollback
, session
, "nsilcmops", indata
, kwargs
)
783 outdata
= {"id": _id
}
785 elif topic
== "netslice_instances" and item
:
786 indata
["lcmOperationType"] = item
787 indata
["nsiInstanceId"] = _id
788 _id
= self
.engine
.new_item(rollback
, session
, "nsilcmops", indata
, kwargs
)
789 self
._set
_location
_header
(main_topic
, version
, "nsi_lcm_op_occs", _id
)
790 outdata
= {"id": _id
}
791 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
793 _id
= self
.engine
.new_item(rollback
, session
, engine_topic
, indata
, kwargs
,
794 cherrypy
.request
.headers
, force
=force
)
795 self
._set
_location
_header
(main_topic
, version
, topic
, _id
)
796 outdata
= {"id": _id
}
797 # TODO form NsdInfo when topic in ("ns_descriptors", "vnf_packages")
798 cherrypy
.response
.status
= HTTPStatus
.CREATED
.value
800 elif method
== "DELETE":
802 outdata
= self
.engine
.del_item_list(session
, engine_topic
, kwargs
)
803 cherrypy
.response
.status
= HTTPStatus
.OK
.value
804 else: # len(args) > 1
805 if topic
== "ns_instances_content" and not force
:
807 "lcmOperationType": "terminate",
811 opp_id
= self
.engine
.new_item(rollback
, session
, "nslcmops", nslcmop_desc
, None)
812 outdata
= {"_id": opp_id
}
813 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
814 elif topic
== "netslice_instances_content" and not force
:
816 "lcmOperationType": "terminate",
817 "nsiInstanceId": _id
,
820 opp_id
= self
.engine
.new_item(rollback
, session
, "nsilcmops", nsilcmop_desc
, None)
821 outdata
= {"_id": opp_id
}
822 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
824 self
.engine
.del_item(session
, engine_topic
, _id
, force
)
825 cherrypy
.response
.status
= HTTPStatus
.NO_CONTENT
.value
826 if engine_topic
in ("vim_accounts", "sdns"):
827 cherrypy
.response
.status
= HTTPStatus
.ACCEPTED
.value
829 elif method
in ("PUT", "PATCH"):
831 if not indata
and not kwargs
:
832 raise NbiException("Nothing to update. Provide payload and/or query string",
833 HTTPStatus
.BAD_REQUEST
)
834 if item
in ("nsd_content", "package_content", "nst_content") and method
== "PUT":
835 completed
= self
.engine
.upload_content(session
, engine_topic
, _id
, indata
, kwargs
,
836 cherrypy
.request
.headers
, force
=force
)
838 cherrypy
.response
.headers
["Transaction-Id"] = id
840 self
.engine
.edit_item(session
, engine_topic
, _id
, indata
, kwargs
, force
=force
)
841 cherrypy
.response
.status
= HTTPStatus
.NO_CONTENT
.value
843 raise NbiException("Method {} not allowed".format(method
), HTTPStatus
.METHOD_NOT_ALLOWED
)
844 return self
._format
_out
(outdata
, session
, _format
)
845 except Exception as e
:
846 if isinstance(e
, (NbiException
, EngineException
, DbException
, FsException
, MsgException
, AuthException
,
848 http_code_value
= cherrypy
.response
.status
= e
.http_code
.value
849 http_code_name
= e
.http_code
.name
850 cherrypy
.log("Exception {}".format(e
))
852 http_code_value
= cherrypy
.response
.status
= HTTPStatus
.BAD_REQUEST
.value
# INTERNAL_SERVER_ERROR
853 cherrypy
.log("CRITICAL: Exception {}".format(e
), traceback
=True)
854 http_code_name
= HTTPStatus
.BAD_REQUEST
.name
855 if hasattr(outdata
, "close"): # is an open file
859 for rollback_item
in rollback
:
861 if rollback_item
.get("operation") == "set":
862 self
.engine
.db
.set_one(rollback_item
["topic"], {"_id": rollback_item
["_id"]},
863 rollback_item
["content"], fail_on_empty
=False)
865 self
.engine
.del_item(**rollback_item
, session
=session
, force
=True)
866 except Exception as e2
:
867 rollback_error_text
= "Rollback Exception {}: {}".format(rollback_item
, e2
)
868 cherrypy
.log(rollback_error_text
)
869 error_text
+= ". " + rollback_error_text
870 # if isinstance(e, MsgException):
871 # error_text = "{} has been '{}' but other modules cannot be informed because an error on bus".format(
872 # engine_topic[:-1], method, error_text)
874 "code": http_code_name
,
875 "status": http_code_value
,
876 "detail": error_text
,
878 return self
._format
_out
(problem_details
, session
)
879 # raise cherrypy.HTTPError(e.http_code.value, str(e))
882 # def validate_password(realm, username, password):
883 # cherrypy.log("realm "+ str(realm))
884 # if username == "admin" and password == "admin":
889 def _start_service():
891 Callback function called when cherrypy.engine starts
892 Override configuration with env variables
893 Set database, storage, message configuration
894 Init database with admin/admin user password
896 cherrypy
.log
.error("Starting osm_nbi")
897 # update general cherrypy configuration
900 engine_config
= cherrypy
.tree
.apps
['/osm'].config
901 for k
, v
in environ
.items():
902 if not k
.startswith("OSMNBI_"):
904 k1
, _
, k2
= k
[7:].lower().partition("_")
908 # update static configuration
909 if k
== 'OSMNBI_STATIC_DIR':
910 engine_config
["/static"]['tools.staticdir.dir'] = v
911 engine_config
["/static"]['tools.staticdir.on'] = True
912 elif k
== 'OSMNBI_SOCKET_PORT' or k
== 'OSMNBI_SERVER_PORT':
913 update_dict
['server.socket_port'] = int(v
)
914 elif k
== 'OSMNBI_SOCKET_HOST' or k
== 'OSMNBI_SERVER_HOST':
915 update_dict
['server.socket_host'] = v
916 elif k1
in ("server", "test", "auth", "log"):
917 update_dict
[k1
+ '.' + k2
] = v
918 elif k1
in ("message", "database", "storage", "authentication"):
919 # k2 = k2.replace('_', '.')
920 if k2
in ("port", "db_port"):
921 engine_config
[k1
][k2
] = int(v
)
923 engine_config
[k1
][k2
] = v
925 except ValueError as e
:
926 cherrypy
.log
.error("Ignoring environ '{}': " + str(e
))
927 except Exception as e
:
928 cherrypy
.log
.warn("skipping environ '{}' on exception '{}'".format(k
, e
))
931 cherrypy
.config
.update(update_dict
)
932 engine_config
["global"].update(update_dict
)
935 log_format_simple
= "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(message)s"
936 log_formatter_simple
= logging
.Formatter(log_format_simple
, datefmt
='%Y-%m-%dT%H:%M:%S')
937 logger_server
= logging
.getLogger("cherrypy.error")
938 logger_access
= logging
.getLogger("cherrypy.access")
939 logger_cherry
= logging
.getLogger("cherrypy")
940 logger_nbi
= logging
.getLogger("nbi")
942 if "log.file" in engine_config
["global"]:
943 file_handler
= logging
.handlers
.RotatingFileHandler(engine_config
["global"]["log.file"],
944 maxBytes
=100e6
, backupCount
=9, delay
=0)
945 file_handler
.setFormatter(log_formatter_simple
)
946 logger_cherry
.addHandler(file_handler
)
947 logger_nbi
.addHandler(file_handler
)
948 # log always to standard output
949 for format_
, logger
in {"nbi.server %(filename)s:%(lineno)s": logger_server
,
950 "nbi.access %(filename)s:%(lineno)s": logger_access
,
951 "%(name)s %(filename)s:%(lineno)s": logger_nbi
953 log_format_cherry
= "%(asctime)s %(levelname)s {} %(message)s".format(format_
)
954 log_formatter_cherry
= logging
.Formatter(log_format_cherry
, datefmt
='%Y-%m-%dT%H:%M:%S')
955 str_handler
= logging
.StreamHandler()
956 str_handler
.setFormatter(log_formatter_cherry
)
957 logger
.addHandler(str_handler
)
959 if engine_config
["global"].get("log.level"):
960 logger_cherry
.setLevel(engine_config
["global"]["log.level"])
961 logger_nbi
.setLevel(engine_config
["global"]["log.level"])
963 # logging other modules
964 for k1
, logname
in {"message": "nbi.msg", "database": "nbi.db", "storage": "nbi.fs"}.items():
965 engine_config
[k1
]["logger_name"] = logname
966 logger_module
= logging
.getLogger(logname
)
967 if "logfile" in engine_config
[k1
]:
968 file_handler
= logging
.handlers
.RotatingFileHandler(engine_config
[k1
]["logfile"],
969 maxBytes
=100e6
, backupCount
=9, delay
=0)
970 file_handler
.setFormatter(log_formatter_simple
)
971 logger_module
.addHandler(file_handler
)
972 if "loglevel" in engine_config
[k1
]:
973 logger_module
.setLevel(engine_config
[k1
]["loglevel"])
974 # TODO add more entries, e.g.: storage
975 cherrypy
.tree
.apps
['/osm'].root
.engine
.start(engine_config
)
976 cherrypy
.tree
.apps
['/osm'].root
.authenticator
.start(engine_config
)
977 cherrypy
.tree
.apps
['/osm'].root
.engine
.init_db(target_version
=database_version
)
978 cherrypy
.tree
.apps
['/osm'].root
.authenticator
.init_db(target_version
=auth_database_version
)
979 # getenv('OSMOPENMANO_TENANT', None)
984 Callback function called when cherrypy.engine stops
985 TODO: Ending database connections.
987 cherrypy
.tree
.apps
['/osm'].root
.engine
.stop()
988 cherrypy
.log
.error("Stopping osm_nbi")
991 def nbi(config_file
):
994 # #'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
995 # 'tools.sessions.on': True,
996 # 'tools.response_headers.on': True,
997 # # 'tools.response_headers.headers': [('Content-Type', 'text/plain')],
1000 # cherrypy.Server.ssl_module = 'builtin'
1001 # cherrypy.Server.ssl_certificate = "http/cert.pem"
1002 # cherrypy.Server.ssl_private_key = "http/privkey.pem"
1003 # cherrypy.Server.thread_pool = 10
1004 # cherrypy.config.update({'Server.socket_port': config["port"], 'Server.socket_host': config["host"]})
1006 # cherrypy.config.update({'tools.auth_basic.on': True,
1007 # 'tools.auth_basic.realm': 'localhost',
1008 # 'tools.auth_basic.checkpassword': validate_password})
1009 cherrypy
.engine
.subscribe('start', _start_service
)
1010 cherrypy
.engine
.subscribe('stop', _stop_service
)
1011 cherrypy
.quickstart(Server(), '/osm', config_file
)
1015 print("""Usage: {} [options]
1016 -c|--config [configuration_file]: loads the configuration file (default: ./nbi.cfg)
1017 -h|--help: shows this help
1018 """.format(sys
.argv
[0]))
1019 # --log-socket-host HOST: send logs to this host")
1020 # --log-socket-port PORT: send logs using this port (default: 9022)")
1023 if __name__
== '__main__':
1025 # load parameters and configuration
1026 opts
, args
= getopt
.getopt(sys
.argv
[1:], "hvc:", ["config=", "help"])
1027 # TODO add "log-socket-host=", "log-socket-port=", "log-file="
1030 if o
in ("-h", "--help"):
1033 elif o
in ("-c", "--config"):
1035 # elif o == "--log-socket-port":
1036 # log_socket_port = a
1037 # elif o == "--log-socket-host":
1038 # log_socket_host = a
1039 # elif o == "--log-file":
1042 assert False, "Unhandled option"
1044 if not path
.isfile(config_file
):
1045 print("configuration file '{}' that not exist".format(config_file
), file=sys
.stderr
)
1048 for config_file
in (__file__
[:__file__
.rfind(".")] + ".cfg", "./nbi.cfg", "/etc/osm/nbi.cfg"):
1049 if path
.isfile(config_file
):
1052 print("No configuration file 'nbi.cfg' found neither at local folder nor at /etc/osm/", file=sys
.stderr
)
1055 except getopt
.GetoptError
as e
:
1056 print(str(e
), file=sys
.stderr
)