blob: 566fb2b1f88b3dc41a31d8eb125de8efac688836 [file] [log] [blame]
tiernoc94c3df2018-02-09 15:38:54 +01001#!/usr/bin/python3
2# -*- coding: utf-8 -*-
3
4import cherrypy
5import time
6import json
7import yaml
8import html_out as html
9import logging
10from engine import Engine, EngineException
tiernoa8d63632018-05-10 13:12:32 +020011from osm_common.dbbase import DbException
12from osm_common.fsbase import FsException
13from osm_common.msgbase import MsgException
tiernoc94c3df2018-02-09 15:38:54 +010014from base64 import standard_b64decode
tiernof27c79b2018-03-12 17:08:42 +010015#from os import getenv
tiernoc94c3df2018-02-09 15:38:54 +010016from http import HTTPStatus
tiernof27c79b2018-03-12 17:08:42 +010017#from http.client import responses as http_responses
tiernoc94c3df2018-02-09 15:38:54 +010018from codecs import getreader
19from os import environ
20
21__author__ = "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
tiernodfe09572018-04-24 10:41:10 +020022
23# TODO consider to remove and provide version using the static version file
24__version__ = "0.1.3"
tierno55945e72018-04-06 16:40:27 +020025version_date = "Apr 2018"
tierno4a946e42018-04-12 17:48:49 +020026database_version = '1.0'
tiernoc94c3df2018-02-09 15:38:54 +010027
28"""
tiernof27c79b2018-03-12 17:08:42 +010029North Bound Interface (O: OSM specific; 5,X: SOL005 not implemented yet; O5: SOL005 implemented)
tiernoc94c3df2018-02-09 15:38:54 +010030URL: /osm GET POST PUT DELETE PATCH
tiernof27c79b2018-03-12 17:08:42 +010031 /nsd/v1 O O
32 /ns_descriptors_content O O
33 /<nsdInfoId> O O O O
tiernoc94c3df2018-02-09 15:38:54 +010034 /ns_descriptors O5 O5
35 /<nsdInfoId> O5 O5 5
36 /nsd_content O5 O5
tiernof27c79b2018-03-12 17:08:42 +010037 /nsd O
38 /artifacts[/<artifactPath>] O
tiernoc94c3df2018-02-09 15:38:54 +010039 /pnf_descriptors 5 5
40 /<pnfdInfoId> 5 5 5
41 /pnfd_content 5 5
tiernof27c79b2018-03-12 17:08:42 +010042 /subscriptions 5 5
43 /<subscriptionId> 5 X
tiernoc94c3df2018-02-09 15:38:54 +010044
45 /vnfpkgm/v1
tierno55945e72018-04-06 16:40:27 +020046 /vnf_packages_content O O
47 /<vnfPkgId> O O
tiernoc94c3df2018-02-09 15:38:54 +010048 /vnf_packages O5 O5
49 /<vnfPkgId> O5 O5 5
tiernoc94c3df2018-02-09 15:38:54 +010050 /package_content O5 O5
51 /upload_from_uri X
tiernof27c79b2018-03-12 17:08:42 +010052 /vnfd O5
53 /artifacts[/<artifactPath>] O5
54 /subscriptions X X
55 /<subscriptionId> X X
tiernoc94c3df2018-02-09 15:38:54 +010056
57 /nslcm/v1
tiernof27c79b2018-03-12 17:08:42 +010058 /ns_instances_content O O
59 /<nsInstanceId> O O
60 /ns_instances 5 5
61 /<nsInstanceId> 5 5
tierno65acb4d2018-04-06 16:42:40 +020062 instantiate O5
63 terminate O5
64 action O
65 scale O5
66 heal 5
tiernoc94c3df2018-02-09 15:38:54 +010067 /ns_lcm_op_occs 5 5
68 /<nsLcmOpOccId> 5 5 5
69 TO BE COMPLETED 5 5
tierno0ffaa992018-05-09 13:21:56 +020070 /vnfrs O
71 /<vnfrId> O
tiernof27c79b2018-03-12 17:08:42 +010072 /subscriptions 5 5
73 /<subscriptionId> 5 X
74 /admin/v1
75 /tokens O O
76 /<id> O O
77 /users O O
78 /<id> O O
79 /projects O O
80 /<id> O O
tierno09c073e2018-04-26 13:36:48 +020081 /vims_accounts (also vims for compatibility) O O
tierno0f98af52018-03-19 10:28:22 +010082 /<id> O O O
83 /sdns O O
84 /<id> O O O
tiernoc94c3df2018-02-09 15:38:54 +010085
86query string.
87 <attrName>[.<attrName>...]*[.<op>]=<value>[,<value>...]&...
88 op: "eq"(or empty to one or the values) | "neq" (to any of the values) | "gt" | "lt" | "gte" | "lte" | "cont" | "ncont"
89 all_fields, fields=x,y,.., exclude_default, exclude_fields=x,y,...
90 (none) … same as “exclude_default”
91 all_fields … all attributes.
92 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>.
93 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>.
94 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
95 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>
96Header field name Reference Example Descriptions
97 Accept IETF RFC 7231 [19] application/json Content-Types that are acceptable for the response.
98 This header field shall be present if the response is expected to have a non-empty message body.
99 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the request.
100 This header field shall be present if the request has a non-empty message body.
101 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.
102 Range IETF RFC 7233 [21] 1000-2000 Requested range of bytes from a file
103Header field name Reference Example Descriptions
104 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the response.
105 This header field shall be present if the response has a non-empty message body.
106 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.
107 This header field shall be present if the response status code is 201 or 3xx.
108 In the present document this header field is also used if the response status code is 202 and a new resource was created.
109 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.
110 Accept-Ranges IETF RFC 7233 [21] bytes Used by the Server to signal whether or not it supports ranges for certain resources.
111 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.
112 Retry-After IETF RFC 7231 [19] Fri, 31 Dec 1999 23:59:59 GMT
113
114 or
115
116 120 Used to indicate how long the user agent ought to wait before making a follow-up request.
117 It can be used with 503 responses.
118 The value of this field can be an HTTP-date or a number of seconds to delay after the response is received.
119
120 #TODO http header for partial uploads: Content-Range: "bytes 0-1199/15000". Id is returned first time and send in following chunks
121"""
122
123
124class NbiException(Exception):
125
126 def __init__(self, message, http_code=HTTPStatus.METHOD_NOT_ALLOWED):
127 Exception.__init__(self, message)
128 self.http_code = http_code
129
130
131class Server(object):
132 instance = 0
133 # to decode bytes to str
134 reader = getreader("utf-8")
135
136 def __init__(self):
137 self.instance += 1
138 self.engine = Engine()
tiernof27c79b2018-03-12 17:08:42 +0100139 self.valid_methods = { # contains allowed URL and methods
140 "admin": {
141 "v1": {
tierno0f98af52018-03-19 10:28:22 +0100142 "tokens": {"METHODS": ("GET", "POST", "DELETE"),
tiernof27c79b2018-03-12 17:08:42 +0100143 "<ID>": { "METHODS": ("GET", "DELETE")}
144 },
tierno0f98af52018-03-19 10:28:22 +0100145 "users": {"METHODS": ("GET", "POST"),
tiernof27c79b2018-03-12 17:08:42 +0100146 "<ID>": {"METHODS": ("GET", "POST", "DELETE")}
147 },
tierno0f98af52018-03-19 10:28:22 +0100148 "projects": {"METHODS": ("GET", "POST"),
149 "<ID>": {"METHODS": ("GET", "DELETE")}
150 },
151 "vims": {"METHODS": ("GET", "POST"),
152 "<ID>": {"METHODS": ("GET", "DELETE")}
153 },
tierno09c073e2018-04-26 13:36:48 +0200154 "vim_accounts": {"METHODS": ("GET", "POST"),
tiernocfb07c62018-05-10 18:30:51 +0200155 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH")}
tierno09c073e2018-04-26 13:36:48 +0200156 },
tierno0f98af52018-03-19 10:28:22 +0100157 "sdns": {"METHODS": ("GET", "POST"),
tiernocfb07c62018-05-10 18:30:51 +0200158 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH")}
tiernof27c79b2018-03-12 17:08:42 +0100159 },
160 }
161 },
162 "nsd": {
163 "v1": {
164 "ns_descriptors_content": { "METHODS": ("GET", "POST"),
165 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
166 },
167 "ns_descriptors": { "METHODS": ("GET", "POST"),
tierno0f98af52018-03-19 10:28:22 +0100168 "<ID>": {"METHODS": ("GET", "DELETE"), "TODO": "PATCH",
tiernof27c79b2018-03-12 17:08:42 +0100169 "nsd_content": { "METHODS": ("GET", "PUT")},
170 "nsd": {"METHODS": "GET"}, # descriptor inside package
171 "artifacts": {"*": {"METHODS": "GET"}}
172 }
173
174 },
175 "pnf_descriptors": {"TODO": ("GET", "POST"),
176 "<ID>": {"TODO": ("GET", "DELETE", "PATCH"),
177 "pnfd_content": {"TODO": ("GET", "PUT")}
178 }
179 },
180 "subscriptions": {"TODO": ("GET", "POST"),
181 "<ID>": {"TODO": ("GET", "DELETE"),}
182 },
183 }
184 },
185 "vnfpkgm": {
186 "v1": {
187 "vnf_packages_content": { "METHODS": ("GET", "POST"),
188 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
189 },
190 "vnf_packages": { "METHODS": ("GET", "POST"),
191 "<ID>": { "METHODS": ("GET", "DELETE"), "TODO": "PATCH", # GET: vnfPkgInfo
192 "package_content": { "METHODS": ("GET", "PUT"), # package
193 "upload_from_uri": {"TODO": "POST"}
194 },
195 "vnfd": {"METHODS": "GET"}, # descriptor inside package
196 "artifacts": {"*": {"METHODS": "GET"}}
197 }
198
199 },
200 "subscriptions": {"TODO": ("GET", "POST"),
201 "<ID>": {"TODO": ("GET", "DELETE"),}
202 },
203 }
204 },
205 "nslcm": {
206 "v1": {
207 "ns_instances_content": {"METHODS": ("GET", "POST"),
208 "<ID>": {"METHODS": ("GET", "DELETE")}
209 },
tierno65acb4d2018-04-06 16:42:40 +0200210 "ns_instances": {"METHODS": ("GET", "POST"),
211 "<ID>": {"TODO": ("GET", "DELETE"),
212 "scale": {"TODO": "POST"},
213 "terminate": {"METHODS": "POST"},
214 "instantiate": {"METHODS": "POST"},
215 "action": {"METHODS": "POST"},
216 }
217 },
218 "ns_lcm_op_occs": {"METHODS": "GET",
219 "<ID>": {"METHODS": "GET"},
tierno0ffaa992018-05-09 13:21:56 +0200220 },
221 "vnfrs": {"METHODS": ("GET"),
222 "<ID>": {"METHODS": ("GET")}
223 },
tiernof27c79b2018-03-12 17:08:42 +0100224 }
225 },
226 }
tiernoc94c3df2018-02-09 15:38:54 +0100227
228 def _authorization(self):
229 token = None
230 user_passwd64 = None
231 try:
232 # 1. Get token Authorization bearer
233 auth = cherrypy.request.headers.get("Authorization")
234 if auth:
235 auth_list = auth.split(" ")
236 if auth_list[0].lower() == "bearer":
237 token = auth_list[-1]
238 elif auth_list[0].lower() == "basic":
239 user_passwd64 = auth_list[-1]
240 if not token:
241 if cherrypy.session.get("Authorization"):
242 # 2. Try using session before request a new token. If not, basic authentication will generate
243 token = cherrypy.session.get("Authorization")
244 if token == "logout":
245 token = None # force Unauthorized response to insert user pasword again
246 elif user_passwd64 and cherrypy.request.config.get("auth.allow_basic_authentication"):
247 # 3. Get new token from user password
248 user = None
249 passwd = None
250 try:
251 user_passwd = standard_b64decode(user_passwd64).decode()
252 user, _, passwd = user_passwd.partition(":")
253 except:
254 pass
255 outdata = self.engine.new_token(None, {"username": user, "password": passwd})
256 token = outdata["id"]
257 cherrypy.session['Authorization'] = token
258 # 4. Get token from cookie
259 # if not token:
260 # auth_cookie = cherrypy.request.cookie.get("Authorization")
261 # if auth_cookie:
262 # token = auth_cookie.value
263 return self.engine.authorize(token)
264 except EngineException as e:
265 if cherrypy.session.get('Authorization'):
266 del cherrypy.session['Authorization']
267 cherrypy.response.headers["WWW-Authenticate"] = 'Bearer realm="{}"'.format(e)
268 raise
269
270 def _format_in(self, kwargs):
271 try:
272 indata = None
273 if cherrypy.request.body.length:
274 error_text = "Invalid input format "
275
276 if "Content-Type" in cherrypy.request.headers:
277 if "application/json" in cherrypy.request.headers["Content-Type"]:
278 error_text = "Invalid json format "
279 indata = json.load(self.reader(cherrypy.request.body))
280 elif "application/yaml" in cherrypy.request.headers["Content-Type"]:
281 error_text = "Invalid yaml format "
282 indata = yaml.load(cherrypy.request.body)
283 elif "application/binary" in cherrypy.request.headers["Content-Type"] or \
284 "application/gzip" in cherrypy.request.headers["Content-Type"] or \
tiernof27c79b2018-03-12 17:08:42 +0100285 "application/zip" in cherrypy.request.headers["Content-Type"] or \
286 "text/plain" in cherrypy.request.headers["Content-Type"]:
287 indata = cherrypy.request.body # .read()
tiernoc94c3df2018-02-09 15:38:54 +0100288 elif "multipart/form-data" in cherrypy.request.headers["Content-Type"]:
289 if "descriptor_file" in kwargs:
290 filecontent = kwargs.pop("descriptor_file")
291 if not filecontent.file:
292 raise NbiException("empty file or content", HTTPStatus.BAD_REQUEST)
tiernof27c79b2018-03-12 17:08:42 +0100293 indata = filecontent.file # .read()
tiernoc94c3df2018-02-09 15:38:54 +0100294 if filecontent.content_type.value:
295 cherrypy.request.headers["Content-Type"] = filecontent.content_type.value
296 else:
297 # raise cherrypy.HTTPError(HTTPStatus.Not_Acceptable,
298 # "Only 'Content-Type' of type 'application/json' or
299 # 'application/yaml' for input format are available")
300 error_text = "Invalid yaml format "
301 indata = yaml.load(cherrypy.request.body)
302 else:
303 error_text = "Invalid yaml format "
304 indata = yaml.load(cherrypy.request.body)
305 if not indata:
306 indata = {}
307
tiernoc94c3df2018-02-09 15:38:54 +0100308 format_yaml = False
309 if cherrypy.request.headers.get("Query-String-Format") == "yaml":
310 format_yaml = True
311
312 for k, v in kwargs.items():
313 if isinstance(v, str):
314 if v == "":
315 kwargs[k] = None
316 elif format_yaml:
317 try:
318 kwargs[k] = yaml.load(v)
319 except:
320 pass
321 elif k.endswith(".gt") or k.endswith(".lt") or k.endswith(".gte") or k.endswith(".lte"):
322 try:
323 kwargs[k] = int(v)
324 except:
325 try:
326 kwargs[k] = float(v)
327 except:
328 pass
329 elif v.find(",") > 0:
330 kwargs[k] = v.split(",")
331 elif isinstance(v, (list, tuple)):
332 for index in range(0, len(v)):
333 if v[index] == "":
334 v[index] = None
335 elif format_yaml:
336 try:
337 v[index] = yaml.load(v[index])
338 except:
339 pass
340
tiernof27c79b2018-03-12 17:08:42 +0100341 return indata
tiernoc94c3df2018-02-09 15:38:54 +0100342 except (ValueError, yaml.YAMLError) as exc:
343 raise NbiException(error_text + str(exc), HTTPStatus.BAD_REQUEST)
344 except KeyError as exc:
345 raise NbiException("Query string error: " + str(exc), HTTPStatus.BAD_REQUEST)
tiernob92094f2018-05-11 13:44:22 +0200346 except Exception as exc:
347 raise NbiException(error_text + str(exc), HTTPStatus.BAD_REQUEST)
tiernoc94c3df2018-02-09 15:38:54 +0100348
349 @staticmethod
tiernof27c79b2018-03-12 17:08:42 +0100350 def _format_out(data, session=None, _format=None):
tiernoc94c3df2018-02-09 15:38:54 +0100351 """
352 return string of dictionary data according to requested json, yaml, xml. By default json
tiernof27c79b2018-03-12 17:08:42 +0100353 :param data: response to be sent. Can be a dict, text or file
tiernoc94c3df2018-02-09 15:38:54 +0100354 :param session:
tiernof27c79b2018-03-12 17:08:42 +0100355 :param _format: The format to be set as Content-Type ir data is a file
tiernoc94c3df2018-02-09 15:38:54 +0100356 :return: None
357 """
tierno0f98af52018-03-19 10:28:22 +0100358 accept = cherrypy.request.headers.get("Accept")
tiernof27c79b2018-03-12 17:08:42 +0100359 if data is None:
tierno0f98af52018-03-19 10:28:22 +0100360 if accept and "text/html" in accept:
361 return html.format(data, cherrypy.request, cherrypy.response, session)
tierno09c073e2018-04-26 13:36:48 +0200362 # cherrypy.response.status = HTTPStatus.NO_CONTENT.value
tiernof27c79b2018-03-12 17:08:42 +0100363 return
364 elif hasattr(data, "read"): # file object
365 if _format:
366 cherrypy.response.headers["Content-Type"] = _format
367 elif "b" in data.mode: # binariy asssumig zip
368 cherrypy.response.headers["Content-Type"] = 'application/zip'
369 else:
370 cherrypy.response.headers["Content-Type"] = 'text/plain'
371 # TODO check that cherrypy close file. If not implement pending things to close per thread next
372 return data
tierno0f98af52018-03-19 10:28:22 +0100373 if accept:
tiernoc94c3df2018-02-09 15:38:54 +0100374 if "application/json" in accept:
375 cherrypy.response.headers["Content-Type"] = 'application/json; charset=utf-8'
376 a = json.dumps(data, indent=4) + "\n"
377 return a.encode("utf8")
378 elif "text/html" in accept:
379 return html.format(data, cherrypy.request, cherrypy.response, session)
380
tiernof27c79b2018-03-12 17:08:42 +0100381 elif "application/yaml" in accept or "*/*" in accept or "text/plain" in accept:
tiernoc94c3df2018-02-09 15:38:54 +0100382 pass
383 else:
384 raise cherrypy.HTTPError(HTTPStatus.NOT_ACCEPTABLE.value,
385 "Only 'Accept' of type 'application/json' or 'application/yaml' "
386 "for output format are available")
387 cherrypy.response.headers["Content-Type"] = 'application/yaml'
388 return yaml.safe_dump(data, explicit_start=True, indent=4, default_flow_style=False, tags=False,
389 encoding='utf-8', allow_unicode=True) # , canonical=True, default_style='"'
390
391 @cherrypy.expose
392 def index(self, *args, **kwargs):
393 session = None
394 try:
395 if cherrypy.request.method == "GET":
396 session = self._authorization()
397 outdata = "Index page"
398 else:
399 raise cherrypy.HTTPError(HTTPStatus.METHOD_NOT_ALLOWED.value,
400 "Method {} not allowed for tokens".format(cherrypy.request.method))
401
402 return self._format_out(outdata, session)
403
404 except EngineException as e:
405 cherrypy.log("index Exception {}".format(e))
406 cherrypy.response.status = e.http_code.value
407 return self._format_out("Welcome to OSM!", session)
408
409 @cherrypy.expose
tierno55945e72018-04-06 16:40:27 +0200410 def version(self, *args, **kwargs):
tiernodfe09572018-04-24 10:41:10 +0200411 # TODO consider to remove and provide version using the static version file
tierno55945e72018-04-06 16:40:27 +0200412 global __version__, version_date
413 try:
414 if cherrypy.request.method != "GET":
415 raise NbiException("Only method GET is allowed", HTTPStatus.METHOD_NOT_ALLOWED)
416 elif args or kwargs:
417 raise NbiException("Invalid URL or query string for version", HTTPStatus.METHOD_NOT_ALLOWED)
418 return __version__ + " " + version_date
419 except NbiException as e:
420 cherrypy.response.status = e.http_code.value
421 problem_details = {
422 "code": e.http_code.name,
423 "status": e.http_code.value,
424 "detail": str(e),
425 }
426 return self._format_out(problem_details, None)
427
428 @cherrypy.expose
tiernof27c79b2018-03-12 17:08:42 +0100429 def token(self, method, token_id=None, kwargs=None):
tiernoc94c3df2018-02-09 15:38:54 +0100430 session = None
431 # self.engine.load_dbase(cherrypy.request.app.config)
tiernof27c79b2018-03-12 17:08:42 +0100432 indata = self._format_in(kwargs)
433 if not isinstance(indata, dict):
434 raise NbiException("Expected application/yaml or application/json Content-Type", HTTPStatus.BAD_REQUEST)
tiernoc94c3df2018-02-09 15:38:54 +0100435 try:
tiernoc94c3df2018-02-09 15:38:54 +0100436 if method == "GET":
437 session = self._authorization()
tiernof27c79b2018-03-12 17:08:42 +0100438 if token_id:
439 outdata = self.engine.get_token(session, token_id)
tiernoc94c3df2018-02-09 15:38:54 +0100440 else:
441 outdata = self.engine.get_token_list(session)
442 elif method == "POST":
443 try:
444 session = self._authorization()
445 except:
446 session = None
447 if kwargs:
448 indata.update(kwargs)
449 outdata = self.engine.new_token(session, indata, cherrypy.request.remote)
450 session = outdata
451 cherrypy.session['Authorization'] = outdata["_id"]
tierno0f98af52018-03-19 10:28:22 +0100452 self._set_location_header("admin", "v1", "tokens", outdata["_id"])
tiernoc94c3df2018-02-09 15:38:54 +0100453 # cherrypy.response.cookie["Authorization"] = outdata["id"]
454 # cherrypy.response.cookie["Authorization"]['expires'] = 3600
455 elif method == "DELETE":
tiernof27c79b2018-03-12 17:08:42 +0100456 if not token_id and "id" in kwargs:
tiernoc94c3df2018-02-09 15:38:54 +0100457 token_id = kwargs["id"]
tiernof27c79b2018-03-12 17:08:42 +0100458 elif not token_id:
tiernoc94c3df2018-02-09 15:38:54 +0100459 session = self._authorization()
460 token_id = session["_id"]
461 outdata = self.engine.del_token(token_id)
tierno0f98af52018-03-19 10:28:22 +0100462 oudata = None
tiernoc94c3df2018-02-09 15:38:54 +0100463 session = None
464 cherrypy.session['Authorization'] = "logout"
465 # cherrypy.response.cookie["Authorization"] = token_id
466 # cherrypy.response.cookie["Authorization"]['expires'] = 0
467 else:
468 raise NbiException("Method {} not allowed for token".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
469 return self._format_out(outdata, session)
470 except (NbiException, EngineException, DbException) as e:
471 cherrypy.log("tokens Exception {}".format(e))
472 cherrypy.response.status = e.http_code.value
473 problem_details = {
474 "code": e.http_code.name,
475 "status": e.http_code.value,
476 "detail": str(e),
477 }
478 return self._format_out(problem_details, session)
479
480 @cherrypy.expose
481 def test(self, *args, **kwargs):
482 thread_info = None
tiernof27c79b2018-03-12 17:08:42 +0100483 if args and args[0] == "help":
484 return "<html><pre>\ninit\nfile/<name> download file\ndb-clear/table\nprune\nlogin\nlogin2\n"\
tierno55945e72018-04-06 16:40:27 +0200485 "sleep/<time>\nmessage/topic\n</pre></html>"
tiernof27c79b2018-03-12 17:08:42 +0100486
487 elif args and args[0] == "init":
tiernoc94c3df2018-02-09 15:38:54 +0100488 try:
489 # self.engine.load_dbase(cherrypy.request.app.config)
490 self.engine.create_admin()
491 return "Done. User 'admin', password 'admin' created"
492 except Exception:
493 cherrypy.response.status = HTTPStatus.FORBIDDEN.value
494 return self._format_out("Database already initialized")
tiernof27c79b2018-03-12 17:08:42 +0100495 elif args and args[0] == "file":
496 return cherrypy.lib.static.serve_file(cherrypy.tree.apps['/osm'].config["storage"]["path"] + "/" + args[1],
497 "text/plain", "attachment")
498 elif args and args[0] == "file2":
499 f_path = cherrypy.tree.apps['/osm'].config["storage"]["path"] + "/" + args[1]
500 f = open(f_path, "r")
501 cherrypy.response.headers["Content-type"] = "text/plain"
tiernof27c79b2018-03-12 17:08:42 +0100502 return f
tierno55945e72018-04-06 16:40:27 +0200503
tiernof27c79b2018-03-12 17:08:42 +0100504 elif len(args) == 2 and args[0] == "db-clear":
505 return self.engine.del_item_list({"project_id": "admin"}, args[1], {})
tiernoc94c3df2018-02-09 15:38:54 +0100506 elif args and args[0] == "prune":
507 return self.engine.prune()
508 elif args and args[0] == "login":
509 if not cherrypy.request.headers.get("Authorization"):
510 cherrypy.response.headers["WWW-Authenticate"] = 'Basic realm="Access to OSM site", charset="UTF-8"'
511 cherrypy.response.status = HTTPStatus.UNAUTHORIZED.value
512 elif args and args[0] == "login2":
513 if not cherrypy.request.headers.get("Authorization"):
514 cherrypy.response.headers["WWW-Authenticate"] = 'Bearer realm="Access to OSM site"'
515 cherrypy.response.status = HTTPStatus.UNAUTHORIZED.value
516 elif args and args[0] == "sleep":
517 sleep_time = 5
518 try:
519 sleep_time = int(args[1])
520 except Exception:
521 cherrypy.response.status = HTTPStatus.FORBIDDEN.value
522 return self._format_out("Database already initialized")
523 thread_info = cherrypy.thread_data
524 print(thread_info)
525 time.sleep(sleep_time)
526 # thread_info
527 elif len(args) >= 2 and args[0] == "message":
528 topic = args[1]
tierno55945e72018-04-06 16:40:27 +0200529 return_text = "<html><pre>{} ->\n".format(topic)
tiernoc94c3df2018-02-09 15:38:54 +0100530 try:
tierno55945e72018-04-06 16:40:27 +0200531 if cherrypy.request.method == 'POST':
532 to_send = yaml.load(cherrypy.request.body)
533 for k, v in to_send.items():
534 self.engine.msg.write(topic, k, v)
535 return_text += " {}: {}\n".format(k, v)
536 elif cherrypy.request.method == 'GET':
537 for k, v in kwargs.items():
538 self.engine.msg.write(topic, k, yaml.load(v))
539 return_text += " {}: {}\n".format(k, yaml.load(v))
tiernoc94c3df2018-02-09 15:38:54 +0100540 except Exception as e:
tierno55945e72018-04-06 16:40:27 +0200541 return_text += "Error: " + str(e)
542 return_text += "</pre></html>\n"
543 return return_text
tiernoc94c3df2018-02-09 15:38:54 +0100544
545 return_text = (
546 "<html><pre>\nheaders:\n args: {}\n".format(args) +
547 " kwargs: {}\n".format(kwargs) +
548 " headers: {}\n".format(cherrypy.request.headers) +
549 " path_info: {}\n".format(cherrypy.request.path_info) +
550 " query_string: {}\n".format(cherrypy.request.query_string) +
551 " session: {}\n".format(cherrypy.session) +
552 " cookie: {}\n".format(cherrypy.request.cookie) +
553 " method: {}\n".format(cherrypy.request.method) +
554 " session: {}\n".format(cherrypy.session.get('fieldname')) +
555 " body:\n")
556 return_text += " length: {}\n".format(cherrypy.request.body.length)
557 if cherrypy.request.body.length:
558 return_text += " content: {}\n".format(
559 str(cherrypy.request.body.read(int(cherrypy.request.headers.get('Content-Length', 0)))))
560 if thread_info:
561 return_text += "thread: {}\n".format(thread_info)
562 return_text += "</pre></html>"
563 return return_text
564
tiernof27c79b2018-03-12 17:08:42 +0100565 def _check_valid_url_method(self, method, *args):
566 if len(args) < 3:
567 raise NbiException("URL must contain at least 'topic/version/item'", HTTPStatus.METHOD_NOT_ALLOWED)
568
569 reference = self.valid_methods
570 for arg in args:
571 if arg is None:
572 break
573 if not isinstance(reference, dict):
574 raise NbiException("URL contains unexpected extra items '{}'".format(arg),
575 HTTPStatus.METHOD_NOT_ALLOWED)
576
577 if arg in reference:
578 reference = reference[arg]
579 elif "<ID>" in reference:
580 reference = reference["<ID>"]
581 elif "*" in reference:
582 reference = reference["*"]
583 break
584 else:
585 raise NbiException("Unexpected URL item {}".format(arg), HTTPStatus.METHOD_NOT_ALLOWED)
586 if "TODO" in reference and method in reference["TODO"]:
587 raise NbiException("Method {} not supported yet for this URL".format(method), HTTPStatus.NOT_IMPLEMENTED)
588 elif "METHODS" in reference and not method in reference["METHODS"]:
589 raise NbiException("Method {} not supported for this URL".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
590 return
591
592 @staticmethod
593 def _set_location_header(topic, version, item, id):
594 """
595 Insert response header Location with the URL of created item base on URL params
596 :param topic:
597 :param version:
598 :param item:
599 :param id:
600 :return: None
601 """
602 # Use cherrypy.request.base for absoluted path and make use of request.header HOST just in case behind aNAT
603 cherrypy.response.headers["Location"] = "/osm/{}/{}/{}/{}".format(topic, version, item, id)
604 return
605
tiernoc94c3df2018-02-09 15:38:54 +0100606 @cherrypy.expose
tiernof27c79b2018-03-12 17:08:42 +0100607 def default(self, topic=None, version=None, item=None, _id=None, item2=None, *args, **kwargs):
tiernoc94c3df2018-02-09 15:38:54 +0100608 session = None
tiernof27c79b2018-03-12 17:08:42 +0100609 outdata = None
610 _format = None
tierno0f98af52018-03-19 10:28:22 +0100611 method = "DONE"
612 engine_item = None
tierno3ace63c2018-05-03 17:51:43 +0200613 rollback = None
tiernoc94c3df2018-02-09 15:38:54 +0100614 try:
tiernof27c79b2018-03-12 17:08:42 +0100615 if not topic or not version or not item:
616 raise NbiException("URL must contain at least 'topic/version/item'", HTTPStatus.METHOD_NOT_ALLOWED)
617 if topic not in ("admin", "vnfpkgm", "nsd", "nslcm"):
618 raise NbiException("URL topic '{}' not supported".format(topic), HTTPStatus.METHOD_NOT_ALLOWED)
tiernoc94c3df2018-02-09 15:38:54 +0100619 if version != 'v1':
620 raise NbiException("URL version '{}' not supported".format(version), HTTPStatus.METHOD_NOT_ALLOWED)
621
tiernof27c79b2018-03-12 17:08:42 +0100622 if kwargs and "METHOD" in kwargs and kwargs["METHOD"] in ("PUT", "POST", "DELETE", "GET", "PATCH"):
623 method = kwargs.pop("METHOD")
624 else:
625 method = cherrypy.request.method
tiernob92094f2018-05-11 13:44:22 +0200626 if kwargs and "FORCE" in kwargs:
627 force = kwargs.pop("FORCE")
628 else:
629 force = False
tiernof27c79b2018-03-12 17:08:42 +0100630
631 self._check_valid_url_method(method, topic, version, item, _id, item2, *args)
632
633 if topic == "admin" and item == "tokens":
634 return self.token(method, _id, kwargs)
635
tiernoc94c3df2018-02-09 15:38:54 +0100636 # self.engine.load_dbase(cherrypy.request.app.config)
637 session = self._authorization()
tiernof27c79b2018-03-12 17:08:42 +0100638 indata = self._format_in(kwargs)
639 engine_item = item
640 if item == "subscriptions":
641 engine_item = topic + "_" + item
642 if item2:
643 engine_item = item2
tiernoc94c3df2018-02-09 15:38:54 +0100644
tiernof27c79b2018-03-12 17:08:42 +0100645 if topic == "nsd":
646 engine_item = "nsds"
647 elif topic == "vnfpkgm":
648 engine_item = "vnfds"
649 elif topic == "nslcm":
650 engine_item = "nsrs"
tierno65acb4d2018-04-06 16:42:40 +0200651 if item == "ns_lcm_op_occs":
652 engine_item = "nslcmops"
tierno0ffaa992018-05-09 13:21:56 +0200653 if item == "vnfrs":
654 engine_item = "vnfrs"
tierno09c073e2018-04-26 13:36:48 +0200655 if engine_item == "vims": # TODO this is for backward compatibility, it will remove in the future
656 engine_item = "vim_accounts"
tiernoc94c3df2018-02-09 15:38:54 +0100657
658 if method == "GET":
tiernof27c79b2018-03-12 17:08:42 +0100659 if item2 in ("nsd_content", "package_content", "artifacts", "vnfd", "nsd"):
660 if item2 in ("vnfd", "nsd"):
661 path = "$DESCRIPTOR"
662 elif args:
663 path = args
664 elif item2 == "artifacts":
665 path = ()
666 else:
667 path = None
668 file, _format = self.engine.get_file(session, engine_item, _id, path,
669 cherrypy.request.headers.get("Accept"))
670 outdata = file
671 elif not _id:
672 outdata = self.engine.get_item_list(session, engine_item, kwargs)
tiernoc94c3df2018-02-09 15:38:54 +0100673 else:
tiernof27c79b2018-03-12 17:08:42 +0100674 outdata = self.engine.get_item(session, engine_item, _id)
675 elif method == "POST":
676 if item in ("ns_descriptors_content", "vnf_packages_content"):
677 _id = cherrypy.request.headers.get("Transaction-Id")
678 if not _id:
tiernob92094f2018-05-11 13:44:22 +0200679 _id = self.engine.new_item(session, engine_item, {}, None, cherrypy.request.headers,
680 force=force)
tierno3ace63c2018-05-03 17:51:43 +0200681 rollback = {"session": session, "item": engine_item, "_id": _id, "force": True}
tiernof27c79b2018-03-12 17:08:42 +0100682 completed = self.engine.upload_content(session, engine_item, _id, indata, kwargs, cherrypy.request.headers)
683 if completed:
684 self._set_location_header(topic, version, item, _id)
685 else:
686 cherrypy.response.headers["Transaction-Id"] = _id
687 outdata = {"id": _id}
tierno65acb4d2018-04-06 16:42:40 +0200688 elif item == "ns_instances_content":
tiernob92094f2018-05-11 13:44:22 +0200689 _id = self.engine.new_item(session, engine_item, indata, kwargs, force=force)
tierno3ace63c2018-05-03 17:51:43 +0200690 rollback = {"session": session, "item": engine_item, "_id": _id, "force": True}
tierno65acb4d2018-04-06 16:42:40 +0200691 self.engine.ns_action(session, _id, "instantiate", {}, None)
tiernof27c79b2018-03-12 17:08:42 +0100692 self._set_location_header(topic, version, item, _id)
tiernof27c79b2018-03-12 17:08:42 +0100693 outdata = {"id": _id}
tierno65acb4d2018-04-06 16:42:40 +0200694 elif item == "ns_instances" and item2:
695 _id = self.engine.ns_action(session, _id, item2, indata, kwargs)
696 self._set_location_header(topic, version, "ns_lcm_op_occs", _id)
697 outdata = {"id": _id}
698 cherrypy.response.status = HTTPStatus.ACCEPTED.value
tiernof27c79b2018-03-12 17:08:42 +0100699 else:
tiernob92094f2018-05-11 13:44:22 +0200700 _id = self.engine.new_item(session, engine_item, indata, kwargs, cherrypy.request.headers,
701 force=force)
tiernof27c79b2018-03-12 17:08:42 +0100702 self._set_location_header(topic, version, item, _id)
703 outdata = {"id": _id}
tierno65acb4d2018-04-06 16:42:40 +0200704 # TODO form NsdInfo when item in ("ns_descriptors", "vnf_packages")
tiernof27c79b2018-03-12 17:08:42 +0100705 cherrypy.response.status = HTTPStatus.CREATED.value
tierno09c073e2018-04-26 13:36:48 +0200706
tiernoc94c3df2018-02-09 15:38:54 +0100707 elif method == "DELETE":
708 if not _id:
tiernof27c79b2018-03-12 17:08:42 +0100709 outdata = self.engine.del_item_list(session, engine_item, kwargs)
tierno09c073e2018-04-26 13:36:48 +0200710 cherrypy.response.status = HTTPStatus.OK.value
tiernoc94c3df2018-02-09 15:38:54 +0100711 else: # len(args) > 1
tierno65acb4d2018-04-06 16:42:40 +0200712 if item == "ns_instances_content":
tierno3ace63c2018-05-03 17:51:43 +0200713 opp_id = self.engine.ns_action(session, _id, "terminate", {"autoremove": True}, None)
714 outdata = {"_id": opp_id}
tierno09c073e2018-04-26 13:36:48 +0200715 cherrypy.response.status = HTTPStatus.ACCEPTED.value
tierno65acb4d2018-04-06 16:42:40 +0200716 else:
tierno65acb4d2018-04-06 16:42:40 +0200717 self.engine.del_item(session, engine_item, _id, force)
tierno09c073e2018-04-26 13:36:48 +0200718 cherrypy.response.status = HTTPStatus.NO_CONTENT.value
719 if engine_item in ("vim_accounts", "sdns"):
720 cherrypy.response.status = HTTPStatus.ACCEPTED.value
721
tiernoc94c3df2018-02-09 15:38:54 +0100722 elif method == "PUT":
tiernof27c79b2018-03-12 17:08:42 +0100723 if not indata and not kwargs:
tiernoc94c3df2018-02-09 15:38:54 +0100724 raise NbiException("Nothing to update. Provide payload and/or query string",
725 HTTPStatus.BAD_REQUEST)
tiernof27c79b2018-03-12 17:08:42 +0100726 if item2 in ("nsd_content", "package_content"):
727 completed = self.engine.upload_content(session, engine_item, _id, indata, kwargs, cherrypy.request.headers)
728 if not completed:
729 cherrypy.response.headers["Transaction-Id"] = id
tierno09c073e2018-04-26 13:36:48 +0200730 cherrypy.response.status = HTTPStatus.NO_CONTENT.value
tiernof27c79b2018-03-12 17:08:42 +0100731 outdata = None
732 else:
tiernob92094f2018-05-11 13:44:22 +0200733 outdata = {"id": self.engine.edit_item(session, engine_item, _id, indata, kwargs, force=force)}
tiernocfb07c62018-05-10 18:30:51 +0200734 elif method == "PATCH":
735 if not indata and not kwargs:
736 raise NbiException("Nothing to update. Provide payload and/or query string",
737 HTTPStatus.BAD_REQUEST)
tiernob92094f2018-05-11 13:44:22 +0200738 outdata = {"id": self.engine.edit_item(session, engine_item, _id, indata, kwargs, force=force)}
tiernoc94c3df2018-02-09 15:38:54 +0100739 else:
740 raise NbiException("Method {} not allowed".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
tiernof27c79b2018-03-12 17:08:42 +0100741 return self._format_out(outdata, session, _format)
tierno0f98af52018-03-19 10:28:22 +0100742 except (NbiException, EngineException, DbException, FsException, MsgException) as e:
tiernoc94c3df2018-02-09 15:38:54 +0100743 cherrypy.log("Exception {}".format(e))
744 cherrypy.response.status = e.http_code.value
tierno3ace63c2018-05-03 17:51:43 +0200745 if hasattr(outdata, "close"): # is an open file
746 outdata.close()
747 if rollback:
748 try:
749 self.engine.del_item(**rollback)
750 except Exception as e2:
751 cherrypy.log("Rollback Exception {}: {}".format(rollback, e2))
tierno0f98af52018-03-19 10:28:22 +0100752 error_text = str(e)
753 if isinstance(e, MsgException):
754 error_text = "{} has been '{}' but other modules cannot be informed because an error on bus".format(
755 engine_item[:-1], method, error_text)
tiernoc94c3df2018-02-09 15:38:54 +0100756 problem_details = {
757 "code": e.http_code.name,
758 "status": e.http_code.value,
759 "detail": str(e),
760 }
761 return self._format_out(problem_details, session)
762 # raise cherrypy.HTTPError(e.http_code.value, str(e))
763
764
765# def validate_password(realm, username, password):
766# cherrypy.log("realm "+ str(realm))
767# if username == "admin" and password == "admin":
768# return True
769# return False
770
771
772def _start_service():
773 """
774 Callback function called when cherrypy.engine starts
775 Override configuration with env variables
776 Set database, storage, message configuration
777 Init database with admin/admin user password
778 """
779 cherrypy.log.error("Starting osm_nbi")
780 # update general cherrypy configuration
781 update_dict = {}
782
783 engine_config = cherrypy.tree.apps['/osm'].config
784 for k, v in environ.items():
785 if not k.startswith("OSMNBI_"):
786 continue
787 k1, _, k2 = k[7:].lower().partition("_")
788 if not k2:
789 continue
790 try:
791 # update static configuration
792 if k == 'OSMNBI_STATIC_DIR':
793 engine_config["/static"]['tools.staticdir.dir'] = v
794 engine_config["/static"]['tools.staticdir.on'] = True
795 elif k == 'OSMNBI_SOCKET_PORT' or k == 'OSMNBI_SERVER_PORT':
796 update_dict['server.socket_port'] = int(v)
797 elif k == 'OSMNBI_SOCKET_HOST' or k == 'OSMNBI_SERVER_HOST':
798 update_dict['server.socket_host'] = v
799 elif k1 == "server":
800 update_dict['server' + k2] = v
801 # TODO add more entries
802 elif k1 in ("message", "database", "storage"):
803 if k2 == "port":
804 engine_config[k1][k2] = int(v)
805 else:
806 engine_config[k1][k2] = v
807 except ValueError as e:
808 cherrypy.log.error("Ignoring environ '{}': " + str(e))
809 except Exception as e:
810 cherrypy.log.warn("skipping environ '{}' on exception '{}'".format(k, e))
811
812 if update_dict:
813 cherrypy.config.update(update_dict)
814
815 # logging cherrypy
816 log_format_simple = "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(message)s"
817 log_formatter_simple = logging.Formatter(log_format_simple, datefmt='%Y-%m-%dT%H:%M:%S')
818 logger_server = logging.getLogger("cherrypy.error")
819 logger_access = logging.getLogger("cherrypy.access")
820 logger_cherry = logging.getLogger("cherrypy")
821 logger_nbi = logging.getLogger("nbi")
822
823 if "logfile" in engine_config["global"]:
824 file_handler = logging.handlers.RotatingFileHandler(engine_config["global"]["logfile"],
825 maxBytes=100e6, backupCount=9, delay=0)
826 file_handler.setFormatter(log_formatter_simple)
827 logger_cherry.addHandler(file_handler)
828 logger_nbi.addHandler(file_handler)
829 else:
830 for format_, logger in {"nbi.server": logger_server,
831 "nbi.access": logger_access,
832 "%(name)s %(filename)s:%(lineno)s": logger_nbi
833 }.items():
834 log_format_cherry = "%(asctime)s %(levelname)s {} %(message)s".format(format_)
835 log_formatter_cherry = logging.Formatter(log_format_cherry, datefmt='%Y-%m-%dT%H:%M:%S')
836 str_handler = logging.StreamHandler()
837 str_handler.setFormatter(log_formatter_cherry)
838 logger.addHandler(str_handler)
839
840 if engine_config["global"].get("loglevel"):
841 logger_cherry.setLevel(engine_config["global"]["loglevel"])
842 logger_nbi.setLevel(engine_config["global"]["loglevel"])
843
844 # logging other modules
845 for k1, logname in {"message": "nbi.msg", "database": "nbi.db", "storage": "nbi.fs"}.items():
846 engine_config[k1]["logger_name"] = logname
847 logger_module = logging.getLogger(logname)
848 if "logfile" in engine_config[k1]:
849 file_handler = logging.handlers.RotatingFileHandler(engine_config[k1]["logfile"],
850 maxBytes=100e6, backupCount=9, delay=0)
851 file_handler.setFormatter(log_formatter_simple)
852 logger_module.addHandler(file_handler)
853 if "loglevel" in engine_config[k1]:
854 logger_module.setLevel(engine_config[k1]["loglevel"])
855 # TODO add more entries, e.g.: storage
856 cherrypy.tree.apps['/osm'].root.engine.start(engine_config)
857 try:
tierno4a946e42018-04-12 17:48:49 +0200858 cherrypy.tree.apps['/osm'].root.engine.init_db(target_version=database_version)
tiernoc94c3df2018-02-09 15:38:54 +0100859 except EngineException:
860 pass
861 # getenv('OSMOPENMANO_TENANT', None)
862
863
864def _stop_service():
865 """
866 Callback function called when cherrypy.engine stops
867 TODO: Ending database connections.
868 """
869 cherrypy.tree.apps['/osm'].root.engine.stop()
870 cherrypy.log.error("Stopping osm_nbi")
871
872def nbi():
873 # conf = {
874 # '/': {
875 # #'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
876 # 'tools.sessions.on': True,
877 # 'tools.response_headers.on': True,
878 # # 'tools.response_headers.headers': [('Content-Type', 'text/plain')],
879 # }
880 # }
881 # cherrypy.Server.ssl_module = 'builtin'
882 # cherrypy.Server.ssl_certificate = "http/cert.pem"
883 # cherrypy.Server.ssl_private_key = "http/privkey.pem"
884 # cherrypy.Server.thread_pool = 10
885 # cherrypy.config.update({'Server.socket_port': config["port"], 'Server.socket_host': config["host"]})
886
887 # cherrypy.config.update({'tools.auth_basic.on': True,
888 # 'tools.auth_basic.realm': 'localhost',
889 # 'tools.auth_basic.checkpassword': validate_password})
890 cherrypy.engine.subscribe('start', _start_service)
891 cherrypy.engine.subscribe('stop', _stop_service)
892 cherrypy.quickstart(Server(), '/osm', "nbi.cfg")
893
894
895if __name__ == '__main__':
896 nbi()