blob: a72fa58440ed4f215685fbadae36459e165d0676 [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
11from dbbase import DbException
tiernof27c79b2018-03-12 17:08:42 +010012from fsbase import FsException
tierno0f98af52018-03-19 10:28:22 +010013from 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
tiernof27c79b2018-03-12 17:08:42 +010070 /subscriptions 5 5
71 /<subscriptionId> 5 X
72 /admin/v1
73 /tokens O O
74 /<id> O O
75 /users O O
76 /<id> O O
77 /projects O O
78 /<id> O O
tierno09c073e2018-04-26 13:36:48 +020079 /vims_accounts (also vims for compatibility) O O
tierno0f98af52018-03-19 10:28:22 +010080 /<id> O O O
81 /sdns O O
82 /<id> O O O
tiernoc94c3df2018-02-09 15:38:54 +010083
84query string.
85 <attrName>[.<attrName>...]*[.<op>]=<value>[,<value>...]&...
86 op: "eq"(or empty to one or the values) | "neq" (to any of the values) | "gt" | "lt" | "gte" | "lte" | "cont" | "ncont"
87 all_fields, fields=x,y,.., exclude_default, exclude_fields=x,y,...
88 (none) … same as “exclude_default”
89 all_fields … all attributes.
90 fields=<list> … all attributes except all complex attributes with minimum cardinality of zero that are not conditionally mandatory, and that are not provided in <list>.
91 exclude_fields=<list> … all attributes except those complex attributes with a minimum cardinality of zero that are not conditionally mandatory, and that are provided in <list>.
92 exclude_default … all attributes except those complex attributes with a minimum cardinality of zero that are not conditionally mandatory, and that are part of the "default exclude set" defined in the present specification for the particular resource
93 exclude_default and include=<list> … all attributes except those complex attributes with a minimum cardinality of zero that are not conditionally mandatory and that are part of the "default exclude set" defined in the present specification for the particular resource, but that are not part of <list>
94Header field name Reference Example Descriptions
95 Accept IETF RFC 7231 [19] application/json Content-Types that are acceptable for the response.
96 This header field shall be present if the response is expected to have a non-empty message body.
97 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the request.
98 This header field shall be present if the request has a non-empty message body.
99 Authorization IETF RFC 7235 [22] Bearer mF_9.B5f-4.1JqM The authorization token for the request. Details are specified in clause 4.5.3.
100 Range IETF RFC 7233 [21] 1000-2000 Requested range of bytes from a file
101Header field name Reference Example Descriptions
102 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the response.
103 This header field shall be present if the response has a non-empty message body.
104 Location IETF RFC 7231 [19] http://www.example.com/vnflcm/v1/vnf_instances/123 Used in redirection, or when a new resource has been created.
105 This header field shall be present if the response status code is 201 or 3xx.
106 In the present document this header field is also used if the response status code is 202 and a new resource was created.
107 WWW-Authenticate IETF RFC 7235 [22] Bearer realm="example" Challenge if the corresponding HTTP request has not provided authorization, or error details if the corresponding HTTP request has provided an invalid authorization token.
108 Accept-Ranges IETF RFC 7233 [21] bytes Used by the Server to signal whether or not it supports ranges for certain resources.
109 Content-Range IETF RFC 7233 [21] bytes 21010-47021/ 47022 Signals the byte range that is contained in the response, and the total length of the file.
110 Retry-After IETF RFC 7231 [19] Fri, 31 Dec 1999 23:59:59 GMT
111
112 or
113
114 120 Used to indicate how long the user agent ought to wait before making a follow-up request.
115 It can be used with 503 responses.
116 The value of this field can be an HTTP-date or a number of seconds to delay after the response is received.
117
118 #TODO http header for partial uploads: Content-Range: "bytes 0-1199/15000". Id is returned first time and send in following chunks
119"""
120
121
122class NbiException(Exception):
123
124 def __init__(self, message, http_code=HTTPStatus.METHOD_NOT_ALLOWED):
125 Exception.__init__(self, message)
126 self.http_code = http_code
127
128
129class Server(object):
130 instance = 0
131 # to decode bytes to str
132 reader = getreader("utf-8")
133
134 def __init__(self):
135 self.instance += 1
136 self.engine = Engine()
tiernof27c79b2018-03-12 17:08:42 +0100137 self.valid_methods = { # contains allowed URL and methods
138 "admin": {
139 "v1": {
tierno0f98af52018-03-19 10:28:22 +0100140 "tokens": {"METHODS": ("GET", "POST", "DELETE"),
tiernof27c79b2018-03-12 17:08:42 +0100141 "<ID>": { "METHODS": ("GET", "DELETE")}
142 },
tierno0f98af52018-03-19 10:28:22 +0100143 "users": {"METHODS": ("GET", "POST"),
tiernof27c79b2018-03-12 17:08:42 +0100144 "<ID>": {"METHODS": ("GET", "POST", "DELETE")}
145 },
tierno0f98af52018-03-19 10:28:22 +0100146 "projects": {"METHODS": ("GET", "POST"),
147 "<ID>": {"METHODS": ("GET", "DELETE")}
148 },
149 "vims": {"METHODS": ("GET", "POST"),
150 "<ID>": {"METHODS": ("GET", "DELETE")}
151 },
tierno09c073e2018-04-26 13:36:48 +0200152 "vim_accounts": {"METHODS": ("GET", "POST"),
153 "<ID>": {"METHODS": ("GET", "DELETE")}
154 },
tierno0f98af52018-03-19 10:28:22 +0100155 "sdns": {"METHODS": ("GET", "POST"),
156 "<ID>": {"METHODS": ("GET", "DELETE")}
tiernof27c79b2018-03-12 17:08:42 +0100157 },
158 }
159 },
160 "nsd": {
161 "v1": {
162 "ns_descriptors_content": { "METHODS": ("GET", "POST"),
163 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
164 },
165 "ns_descriptors": { "METHODS": ("GET", "POST"),
tierno0f98af52018-03-19 10:28:22 +0100166 "<ID>": {"METHODS": ("GET", "DELETE"), "TODO": "PATCH",
tiernof27c79b2018-03-12 17:08:42 +0100167 "nsd_content": { "METHODS": ("GET", "PUT")},
168 "nsd": {"METHODS": "GET"}, # descriptor inside package
169 "artifacts": {"*": {"METHODS": "GET"}}
170 }
171
172 },
173 "pnf_descriptors": {"TODO": ("GET", "POST"),
174 "<ID>": {"TODO": ("GET", "DELETE", "PATCH"),
175 "pnfd_content": {"TODO": ("GET", "PUT")}
176 }
177 },
178 "subscriptions": {"TODO": ("GET", "POST"),
179 "<ID>": {"TODO": ("GET", "DELETE"),}
180 },
181 }
182 },
183 "vnfpkgm": {
184 "v1": {
185 "vnf_packages_content": { "METHODS": ("GET", "POST"),
186 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
187 },
188 "vnf_packages": { "METHODS": ("GET", "POST"),
189 "<ID>": { "METHODS": ("GET", "DELETE"), "TODO": "PATCH", # GET: vnfPkgInfo
190 "package_content": { "METHODS": ("GET", "PUT"), # package
191 "upload_from_uri": {"TODO": "POST"}
192 },
193 "vnfd": {"METHODS": "GET"}, # descriptor inside package
194 "artifacts": {"*": {"METHODS": "GET"}}
195 }
196
197 },
198 "subscriptions": {"TODO": ("GET", "POST"),
199 "<ID>": {"TODO": ("GET", "DELETE"),}
200 },
201 }
202 },
203 "nslcm": {
204 "v1": {
205 "ns_instances_content": {"METHODS": ("GET", "POST"),
206 "<ID>": {"METHODS": ("GET", "DELETE")}
207 },
tierno65acb4d2018-04-06 16:42:40 +0200208 "ns_instances": {"METHODS": ("GET", "POST"),
209 "<ID>": {"TODO": ("GET", "DELETE"),
210 "scale": {"TODO": "POST"},
211 "terminate": {"METHODS": "POST"},
212 "instantiate": {"METHODS": "POST"},
213 "action": {"METHODS": "POST"},
214 }
215 },
216 "ns_lcm_op_occs": {"METHODS": "GET",
217 "<ID>": {"METHODS": "GET"},
tiernof27c79b2018-03-12 17:08:42 +0100218 }
219 }
220 },
221 }
tiernoc94c3df2018-02-09 15:38:54 +0100222
223 def _authorization(self):
224 token = None
225 user_passwd64 = None
226 try:
227 # 1. Get token Authorization bearer
228 auth = cherrypy.request.headers.get("Authorization")
229 if auth:
230 auth_list = auth.split(" ")
231 if auth_list[0].lower() == "bearer":
232 token = auth_list[-1]
233 elif auth_list[0].lower() == "basic":
234 user_passwd64 = auth_list[-1]
235 if not token:
236 if cherrypy.session.get("Authorization"):
237 # 2. Try using session before request a new token. If not, basic authentication will generate
238 token = cherrypy.session.get("Authorization")
239 if token == "logout":
240 token = None # force Unauthorized response to insert user pasword again
241 elif user_passwd64 and cherrypy.request.config.get("auth.allow_basic_authentication"):
242 # 3. Get new token from user password
243 user = None
244 passwd = None
245 try:
246 user_passwd = standard_b64decode(user_passwd64).decode()
247 user, _, passwd = user_passwd.partition(":")
248 except:
249 pass
250 outdata = self.engine.new_token(None, {"username": user, "password": passwd})
251 token = outdata["id"]
252 cherrypy.session['Authorization'] = token
253 # 4. Get token from cookie
254 # if not token:
255 # auth_cookie = cherrypy.request.cookie.get("Authorization")
256 # if auth_cookie:
257 # token = auth_cookie.value
258 return self.engine.authorize(token)
259 except EngineException as e:
260 if cherrypy.session.get('Authorization'):
261 del cherrypy.session['Authorization']
262 cherrypy.response.headers["WWW-Authenticate"] = 'Bearer realm="{}"'.format(e)
263 raise
264
265 def _format_in(self, kwargs):
266 try:
267 indata = None
268 if cherrypy.request.body.length:
269 error_text = "Invalid input format "
270
271 if "Content-Type" in cherrypy.request.headers:
272 if "application/json" in cherrypy.request.headers["Content-Type"]:
273 error_text = "Invalid json format "
274 indata = json.load(self.reader(cherrypy.request.body))
275 elif "application/yaml" in cherrypy.request.headers["Content-Type"]:
276 error_text = "Invalid yaml format "
277 indata = yaml.load(cherrypy.request.body)
278 elif "application/binary" in cherrypy.request.headers["Content-Type"] or \
279 "application/gzip" in cherrypy.request.headers["Content-Type"] or \
tiernof27c79b2018-03-12 17:08:42 +0100280 "application/zip" in cherrypy.request.headers["Content-Type"] or \
281 "text/plain" in cherrypy.request.headers["Content-Type"]:
282 indata = cherrypy.request.body # .read()
tiernoc94c3df2018-02-09 15:38:54 +0100283 elif "multipart/form-data" in cherrypy.request.headers["Content-Type"]:
284 if "descriptor_file" in kwargs:
285 filecontent = kwargs.pop("descriptor_file")
286 if not filecontent.file:
287 raise NbiException("empty file or content", HTTPStatus.BAD_REQUEST)
tiernof27c79b2018-03-12 17:08:42 +0100288 indata = filecontent.file # .read()
tiernoc94c3df2018-02-09 15:38:54 +0100289 if filecontent.content_type.value:
290 cherrypy.request.headers["Content-Type"] = filecontent.content_type.value
291 else:
292 # raise cherrypy.HTTPError(HTTPStatus.Not_Acceptable,
293 # "Only 'Content-Type' of type 'application/json' or
294 # 'application/yaml' for input format are available")
295 error_text = "Invalid yaml format "
296 indata = yaml.load(cherrypy.request.body)
297 else:
298 error_text = "Invalid yaml format "
299 indata = yaml.load(cherrypy.request.body)
300 if not indata:
301 indata = {}
302
tiernoc94c3df2018-02-09 15:38:54 +0100303 format_yaml = False
304 if cherrypy.request.headers.get("Query-String-Format") == "yaml":
305 format_yaml = True
306
307 for k, v in kwargs.items():
308 if isinstance(v, str):
309 if v == "":
310 kwargs[k] = None
311 elif format_yaml:
312 try:
313 kwargs[k] = yaml.load(v)
314 except:
315 pass
316 elif k.endswith(".gt") or k.endswith(".lt") or k.endswith(".gte") or k.endswith(".lte"):
317 try:
318 kwargs[k] = int(v)
319 except:
320 try:
321 kwargs[k] = float(v)
322 except:
323 pass
324 elif v.find(",") > 0:
325 kwargs[k] = v.split(",")
326 elif isinstance(v, (list, tuple)):
327 for index in range(0, len(v)):
328 if v[index] == "":
329 v[index] = None
330 elif format_yaml:
331 try:
332 v[index] = yaml.load(v[index])
333 except:
334 pass
335
tiernof27c79b2018-03-12 17:08:42 +0100336 return indata
tiernoc94c3df2018-02-09 15:38:54 +0100337 except (ValueError, yaml.YAMLError) as exc:
338 raise NbiException(error_text + str(exc), HTTPStatus.BAD_REQUEST)
339 except KeyError as exc:
340 raise NbiException("Query string error: " + str(exc), HTTPStatus.BAD_REQUEST)
341
342 @staticmethod
tiernof27c79b2018-03-12 17:08:42 +0100343 def _format_out(data, session=None, _format=None):
tiernoc94c3df2018-02-09 15:38:54 +0100344 """
345 return string of dictionary data according to requested json, yaml, xml. By default json
tiernof27c79b2018-03-12 17:08:42 +0100346 :param data: response to be sent. Can be a dict, text or file
tiernoc94c3df2018-02-09 15:38:54 +0100347 :param session:
tiernof27c79b2018-03-12 17:08:42 +0100348 :param _format: The format to be set as Content-Type ir data is a file
tiernoc94c3df2018-02-09 15:38:54 +0100349 :return: None
350 """
tierno0f98af52018-03-19 10:28:22 +0100351 accept = cherrypy.request.headers.get("Accept")
tiernof27c79b2018-03-12 17:08:42 +0100352 if data is None:
tierno0f98af52018-03-19 10:28:22 +0100353 if accept and "text/html" in accept:
354 return html.format(data, cherrypy.request, cherrypy.response, session)
tierno09c073e2018-04-26 13:36:48 +0200355 # cherrypy.response.status = HTTPStatus.NO_CONTENT.value
tiernof27c79b2018-03-12 17:08:42 +0100356 return
357 elif hasattr(data, "read"): # file object
358 if _format:
359 cherrypy.response.headers["Content-Type"] = _format
360 elif "b" in data.mode: # binariy asssumig zip
361 cherrypy.response.headers["Content-Type"] = 'application/zip'
362 else:
363 cherrypy.response.headers["Content-Type"] = 'text/plain'
364 # TODO check that cherrypy close file. If not implement pending things to close per thread next
365 return data
tierno0f98af52018-03-19 10:28:22 +0100366 if accept:
tiernoc94c3df2018-02-09 15:38:54 +0100367 if "application/json" in accept:
368 cherrypy.response.headers["Content-Type"] = 'application/json; charset=utf-8'
369 a = json.dumps(data, indent=4) + "\n"
370 return a.encode("utf8")
371 elif "text/html" in accept:
372 return html.format(data, cherrypy.request, cherrypy.response, session)
373
tiernof27c79b2018-03-12 17:08:42 +0100374 elif "application/yaml" in accept or "*/*" in accept or "text/plain" in accept:
tiernoc94c3df2018-02-09 15:38:54 +0100375 pass
376 else:
377 raise cherrypy.HTTPError(HTTPStatus.NOT_ACCEPTABLE.value,
378 "Only 'Accept' of type 'application/json' or 'application/yaml' "
379 "for output format are available")
380 cherrypy.response.headers["Content-Type"] = 'application/yaml'
381 return yaml.safe_dump(data, explicit_start=True, indent=4, default_flow_style=False, tags=False,
382 encoding='utf-8', allow_unicode=True) # , canonical=True, default_style='"'
383
384 @cherrypy.expose
385 def index(self, *args, **kwargs):
386 session = None
387 try:
388 if cherrypy.request.method == "GET":
389 session = self._authorization()
390 outdata = "Index page"
391 else:
392 raise cherrypy.HTTPError(HTTPStatus.METHOD_NOT_ALLOWED.value,
393 "Method {} not allowed for tokens".format(cherrypy.request.method))
394
395 return self._format_out(outdata, session)
396
397 except EngineException as e:
398 cherrypy.log("index Exception {}".format(e))
399 cherrypy.response.status = e.http_code.value
400 return self._format_out("Welcome to OSM!", session)
401
402 @cherrypy.expose
tierno55945e72018-04-06 16:40:27 +0200403 def version(self, *args, **kwargs):
tiernodfe09572018-04-24 10:41:10 +0200404 # TODO consider to remove and provide version using the static version file
tierno55945e72018-04-06 16:40:27 +0200405 global __version__, version_date
406 try:
407 if cherrypy.request.method != "GET":
408 raise NbiException("Only method GET is allowed", HTTPStatus.METHOD_NOT_ALLOWED)
409 elif args or kwargs:
410 raise NbiException("Invalid URL or query string for version", HTTPStatus.METHOD_NOT_ALLOWED)
411 return __version__ + " " + version_date
412 except NbiException as e:
413 cherrypy.response.status = e.http_code.value
414 problem_details = {
415 "code": e.http_code.name,
416 "status": e.http_code.value,
417 "detail": str(e),
418 }
419 return self._format_out(problem_details, None)
420
421 @cherrypy.expose
tiernof27c79b2018-03-12 17:08:42 +0100422 def token(self, method, token_id=None, kwargs=None):
tiernoc94c3df2018-02-09 15:38:54 +0100423 session = None
424 # self.engine.load_dbase(cherrypy.request.app.config)
tiernof27c79b2018-03-12 17:08:42 +0100425 indata = self._format_in(kwargs)
426 if not isinstance(indata, dict):
427 raise NbiException("Expected application/yaml or application/json Content-Type", HTTPStatus.BAD_REQUEST)
tiernoc94c3df2018-02-09 15:38:54 +0100428 try:
tiernoc94c3df2018-02-09 15:38:54 +0100429 if method == "GET":
430 session = self._authorization()
tiernof27c79b2018-03-12 17:08:42 +0100431 if token_id:
432 outdata = self.engine.get_token(session, token_id)
tiernoc94c3df2018-02-09 15:38:54 +0100433 else:
434 outdata = self.engine.get_token_list(session)
435 elif method == "POST":
436 try:
437 session = self._authorization()
438 except:
439 session = None
440 if kwargs:
441 indata.update(kwargs)
442 outdata = self.engine.new_token(session, indata, cherrypy.request.remote)
443 session = outdata
444 cherrypy.session['Authorization'] = outdata["_id"]
tierno0f98af52018-03-19 10:28:22 +0100445 self._set_location_header("admin", "v1", "tokens", outdata["_id"])
tiernoc94c3df2018-02-09 15:38:54 +0100446 # cherrypy.response.cookie["Authorization"] = outdata["id"]
447 # cherrypy.response.cookie["Authorization"]['expires'] = 3600
448 elif method == "DELETE":
tiernof27c79b2018-03-12 17:08:42 +0100449 if not token_id and "id" in kwargs:
tiernoc94c3df2018-02-09 15:38:54 +0100450 token_id = kwargs["id"]
tiernof27c79b2018-03-12 17:08:42 +0100451 elif not token_id:
tiernoc94c3df2018-02-09 15:38:54 +0100452 session = self._authorization()
453 token_id = session["_id"]
454 outdata = self.engine.del_token(token_id)
tierno0f98af52018-03-19 10:28:22 +0100455 oudata = None
tiernoc94c3df2018-02-09 15:38:54 +0100456 session = None
457 cherrypy.session['Authorization'] = "logout"
458 # cherrypy.response.cookie["Authorization"] = token_id
459 # cherrypy.response.cookie["Authorization"]['expires'] = 0
460 else:
461 raise NbiException("Method {} not allowed for token".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
462 return self._format_out(outdata, session)
463 except (NbiException, EngineException, DbException) as e:
464 cherrypy.log("tokens Exception {}".format(e))
465 cherrypy.response.status = e.http_code.value
466 problem_details = {
467 "code": e.http_code.name,
468 "status": e.http_code.value,
469 "detail": str(e),
470 }
471 return self._format_out(problem_details, session)
472
473 @cherrypy.expose
474 def test(self, *args, **kwargs):
475 thread_info = None
tiernof27c79b2018-03-12 17:08:42 +0100476 if args and args[0] == "help":
477 return "<html><pre>\ninit\nfile/<name> download file\ndb-clear/table\nprune\nlogin\nlogin2\n"\
tierno55945e72018-04-06 16:40:27 +0200478 "sleep/<time>\nmessage/topic\n</pre></html>"
tiernof27c79b2018-03-12 17:08:42 +0100479
480 elif args and args[0] == "init":
tiernoc94c3df2018-02-09 15:38:54 +0100481 try:
482 # self.engine.load_dbase(cherrypy.request.app.config)
483 self.engine.create_admin()
484 return "Done. User 'admin', password 'admin' created"
485 except Exception:
486 cherrypy.response.status = HTTPStatus.FORBIDDEN.value
487 return self._format_out("Database already initialized")
tiernof27c79b2018-03-12 17:08:42 +0100488 elif args and args[0] == "file":
489 return cherrypy.lib.static.serve_file(cherrypy.tree.apps['/osm'].config["storage"]["path"] + "/" + args[1],
490 "text/plain", "attachment")
491 elif args and args[0] == "file2":
492 f_path = cherrypy.tree.apps['/osm'].config["storage"]["path"] + "/" + args[1]
493 f = open(f_path, "r")
494 cherrypy.response.headers["Content-type"] = "text/plain"
tiernof27c79b2018-03-12 17:08:42 +0100495 return f
tierno55945e72018-04-06 16:40:27 +0200496
tiernof27c79b2018-03-12 17:08:42 +0100497 elif len(args) == 2 and args[0] == "db-clear":
498 return self.engine.del_item_list({"project_id": "admin"}, args[1], {})
tiernoc94c3df2018-02-09 15:38:54 +0100499 elif args and args[0] == "prune":
500 return self.engine.prune()
501 elif args and args[0] == "login":
502 if not cherrypy.request.headers.get("Authorization"):
503 cherrypy.response.headers["WWW-Authenticate"] = 'Basic realm="Access to OSM site", charset="UTF-8"'
504 cherrypy.response.status = HTTPStatus.UNAUTHORIZED.value
505 elif args and args[0] == "login2":
506 if not cherrypy.request.headers.get("Authorization"):
507 cherrypy.response.headers["WWW-Authenticate"] = 'Bearer realm="Access to OSM site"'
508 cherrypy.response.status = HTTPStatus.UNAUTHORIZED.value
509 elif args and args[0] == "sleep":
510 sleep_time = 5
511 try:
512 sleep_time = int(args[1])
513 except Exception:
514 cherrypy.response.status = HTTPStatus.FORBIDDEN.value
515 return self._format_out("Database already initialized")
516 thread_info = cherrypy.thread_data
517 print(thread_info)
518 time.sleep(sleep_time)
519 # thread_info
520 elif len(args) >= 2 and args[0] == "message":
521 topic = args[1]
tierno55945e72018-04-06 16:40:27 +0200522 return_text = "<html><pre>{} ->\n".format(topic)
tiernoc94c3df2018-02-09 15:38:54 +0100523 try:
tierno55945e72018-04-06 16:40:27 +0200524 if cherrypy.request.method == 'POST':
525 to_send = yaml.load(cherrypy.request.body)
526 for k, v in to_send.items():
527 self.engine.msg.write(topic, k, v)
528 return_text += " {}: {}\n".format(k, v)
529 elif cherrypy.request.method == 'GET':
530 for k, v in kwargs.items():
531 self.engine.msg.write(topic, k, yaml.load(v))
532 return_text += " {}: {}\n".format(k, yaml.load(v))
tiernoc94c3df2018-02-09 15:38:54 +0100533 except Exception as e:
tierno55945e72018-04-06 16:40:27 +0200534 return_text += "Error: " + str(e)
535 return_text += "</pre></html>\n"
536 return return_text
tiernoc94c3df2018-02-09 15:38:54 +0100537
538 return_text = (
539 "<html><pre>\nheaders:\n args: {}\n".format(args) +
540 " kwargs: {}\n".format(kwargs) +
541 " headers: {}\n".format(cherrypy.request.headers) +
542 " path_info: {}\n".format(cherrypy.request.path_info) +
543 " query_string: {}\n".format(cherrypy.request.query_string) +
544 " session: {}\n".format(cherrypy.session) +
545 " cookie: {}\n".format(cherrypy.request.cookie) +
546 " method: {}\n".format(cherrypy.request.method) +
547 " session: {}\n".format(cherrypy.session.get('fieldname')) +
548 " body:\n")
549 return_text += " length: {}\n".format(cherrypy.request.body.length)
550 if cherrypy.request.body.length:
551 return_text += " content: {}\n".format(
552 str(cherrypy.request.body.read(int(cherrypy.request.headers.get('Content-Length', 0)))))
553 if thread_info:
554 return_text += "thread: {}\n".format(thread_info)
555 return_text += "</pre></html>"
556 return return_text
557
tiernof27c79b2018-03-12 17:08:42 +0100558 def _check_valid_url_method(self, method, *args):
559 if len(args) < 3:
560 raise NbiException("URL must contain at least 'topic/version/item'", HTTPStatus.METHOD_NOT_ALLOWED)
561
562 reference = self.valid_methods
563 for arg in args:
564 if arg is None:
565 break
566 if not isinstance(reference, dict):
567 raise NbiException("URL contains unexpected extra items '{}'".format(arg),
568 HTTPStatus.METHOD_NOT_ALLOWED)
569
570 if arg in reference:
571 reference = reference[arg]
572 elif "<ID>" in reference:
573 reference = reference["<ID>"]
574 elif "*" in reference:
575 reference = reference["*"]
576 break
577 else:
578 raise NbiException("Unexpected URL item {}".format(arg), HTTPStatus.METHOD_NOT_ALLOWED)
579 if "TODO" in reference and method in reference["TODO"]:
580 raise NbiException("Method {} not supported yet for this URL".format(method), HTTPStatus.NOT_IMPLEMENTED)
581 elif "METHODS" in reference and not method in reference["METHODS"]:
582 raise NbiException("Method {} not supported for this URL".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
583 return
584
585 @staticmethod
586 def _set_location_header(topic, version, item, id):
587 """
588 Insert response header Location with the URL of created item base on URL params
589 :param topic:
590 :param version:
591 :param item:
592 :param id:
593 :return: None
594 """
595 # Use cherrypy.request.base for absoluted path and make use of request.header HOST just in case behind aNAT
596 cherrypy.response.headers["Location"] = "/osm/{}/{}/{}/{}".format(topic, version, item, id)
597 return
598
tiernoc94c3df2018-02-09 15:38:54 +0100599 @cherrypy.expose
tiernof27c79b2018-03-12 17:08:42 +0100600 def default(self, topic=None, version=None, item=None, _id=None, item2=None, *args, **kwargs):
tiernoc94c3df2018-02-09 15:38:54 +0100601 session = None
tiernof27c79b2018-03-12 17:08:42 +0100602 outdata = None
603 _format = None
tierno0f98af52018-03-19 10:28:22 +0100604 method = "DONE"
605 engine_item = None
tiernoc94c3df2018-02-09 15:38:54 +0100606 try:
tiernof27c79b2018-03-12 17:08:42 +0100607 if not topic or not version or not item:
608 raise NbiException("URL must contain at least 'topic/version/item'", HTTPStatus.METHOD_NOT_ALLOWED)
609 if topic not in ("admin", "vnfpkgm", "nsd", "nslcm"):
610 raise NbiException("URL topic '{}' not supported".format(topic), HTTPStatus.METHOD_NOT_ALLOWED)
tiernoc94c3df2018-02-09 15:38:54 +0100611 if version != 'v1':
612 raise NbiException("URL version '{}' not supported".format(version), HTTPStatus.METHOD_NOT_ALLOWED)
613
tiernof27c79b2018-03-12 17:08:42 +0100614 if kwargs and "METHOD" in kwargs and kwargs["METHOD"] in ("PUT", "POST", "DELETE", "GET", "PATCH"):
615 method = kwargs.pop("METHOD")
616 else:
617 method = cherrypy.request.method
618
619 self._check_valid_url_method(method, topic, version, item, _id, item2, *args)
620
621 if topic == "admin" and item == "tokens":
622 return self.token(method, _id, kwargs)
623
tiernoc94c3df2018-02-09 15:38:54 +0100624 # self.engine.load_dbase(cherrypy.request.app.config)
625 session = self._authorization()
tiernof27c79b2018-03-12 17:08:42 +0100626 indata = self._format_in(kwargs)
627 engine_item = item
628 if item == "subscriptions":
629 engine_item = topic + "_" + item
630 if item2:
631 engine_item = item2
tiernoc94c3df2018-02-09 15:38:54 +0100632
tiernof27c79b2018-03-12 17:08:42 +0100633 if topic == "nsd":
634 engine_item = "nsds"
635 elif topic == "vnfpkgm":
636 engine_item = "vnfds"
637 elif topic == "nslcm":
638 engine_item = "nsrs"
tierno65acb4d2018-04-06 16:42:40 +0200639 if item == "ns_lcm_op_occs":
640 engine_item = "nslcmops"
tierno09c073e2018-04-26 13:36:48 +0200641 if engine_item == "vims": # TODO this is for backward compatibility, it will remove in the future
642 engine_item = "vim_accounts"
tiernoc94c3df2018-02-09 15:38:54 +0100643
644 if method == "GET":
tiernof27c79b2018-03-12 17:08:42 +0100645 if item2 in ("nsd_content", "package_content", "artifacts", "vnfd", "nsd"):
646 if item2 in ("vnfd", "nsd"):
647 path = "$DESCRIPTOR"
648 elif args:
649 path = args
650 elif item2 == "artifacts":
651 path = ()
652 else:
653 path = None
654 file, _format = self.engine.get_file(session, engine_item, _id, path,
655 cherrypy.request.headers.get("Accept"))
656 outdata = file
657 elif not _id:
658 outdata = self.engine.get_item_list(session, engine_item, kwargs)
tiernoc94c3df2018-02-09 15:38:54 +0100659 else:
tiernof27c79b2018-03-12 17:08:42 +0100660 outdata = self.engine.get_item(session, engine_item, _id)
661 elif method == "POST":
662 if item in ("ns_descriptors_content", "vnf_packages_content"):
663 _id = cherrypy.request.headers.get("Transaction-Id")
664 if not _id:
665 _id = self.engine.new_item(session, engine_item, {}, None, cherrypy.request.headers)
666 completed = self.engine.upload_content(session, engine_item, _id, indata, kwargs, cherrypy.request.headers)
667 if completed:
668 self._set_location_header(topic, version, item, _id)
669 else:
670 cherrypy.response.headers["Transaction-Id"] = _id
671 outdata = {"id": _id}
tierno65acb4d2018-04-06 16:42:40 +0200672 elif item == "ns_instances_content":
673 _id = self.engine.new_item(session, engine_item, indata, kwargs)
674 self.engine.ns_action(session, _id, "instantiate", {}, None)
tiernof27c79b2018-03-12 17:08:42 +0100675 self._set_location_header(topic, version, item, _id)
tiernof27c79b2018-03-12 17:08:42 +0100676 outdata = {"id": _id}
tierno65acb4d2018-04-06 16:42:40 +0200677 elif item == "ns_instances" and item2:
678 _id = self.engine.ns_action(session, _id, item2, indata, kwargs)
679 self._set_location_header(topic, version, "ns_lcm_op_occs", _id)
680 outdata = {"id": _id}
681 cherrypy.response.status = HTTPStatus.ACCEPTED.value
tiernof27c79b2018-03-12 17:08:42 +0100682 else:
683 _id = self.engine.new_item(session, engine_item, indata, kwargs, cherrypy.request.headers)
684 self._set_location_header(topic, version, item, _id)
685 outdata = {"id": _id}
tierno65acb4d2018-04-06 16:42:40 +0200686 # TODO form NsdInfo when item in ("ns_descriptors", "vnf_packages")
tiernof27c79b2018-03-12 17:08:42 +0100687 cherrypy.response.status = HTTPStatus.CREATED.value
tierno09c073e2018-04-26 13:36:48 +0200688
tiernoc94c3df2018-02-09 15:38:54 +0100689 elif method == "DELETE":
690 if not _id:
tiernof27c79b2018-03-12 17:08:42 +0100691 outdata = self.engine.del_item_list(session, engine_item, kwargs)
tierno09c073e2018-04-26 13:36:48 +0200692 cherrypy.response.status = HTTPStatus.OK.value
tiernoc94c3df2018-02-09 15:38:54 +0100693 else: # len(args) > 1
tierno65acb4d2018-04-06 16:42:40 +0200694 if item == "ns_instances_content":
695 self.engine.ns_action(session, _id, "terminate", {"autoremove": True}, None)
tierno09c073e2018-04-26 13:36:48 +0200696 cherrypy.response.status = HTTPStatus.ACCEPTED.value
tierno65acb4d2018-04-06 16:42:40 +0200697 else:
698 force = kwargs.get("FORCE")
699 self.engine.del_item(session, engine_item, _id, force)
tierno09c073e2018-04-26 13:36:48 +0200700 cherrypy.response.status = HTTPStatus.NO_CONTENT.value
701 if engine_item in ("vim_accounts", "sdns"):
702 cherrypy.response.status = HTTPStatus.ACCEPTED.value
703
tiernoc94c3df2018-02-09 15:38:54 +0100704 elif method == "PUT":
tiernof27c79b2018-03-12 17:08:42 +0100705 if not indata and not kwargs:
tiernoc94c3df2018-02-09 15:38:54 +0100706 raise NbiException("Nothing to update. Provide payload and/or query string",
707 HTTPStatus.BAD_REQUEST)
tiernof27c79b2018-03-12 17:08:42 +0100708 if item2 in ("nsd_content", "package_content"):
709 completed = self.engine.upload_content(session, engine_item, _id, indata, kwargs, cherrypy.request.headers)
710 if not completed:
711 cherrypy.response.headers["Transaction-Id"] = id
tierno09c073e2018-04-26 13:36:48 +0200712 cherrypy.response.status = HTTPStatus.NO_CONTENT.value
tiernof27c79b2018-03-12 17:08:42 +0100713 outdata = None
714 else:
715 outdata = {"id": self.engine.edit_item(session, engine_item, args[1], indata, kwargs)}
tiernoc94c3df2018-02-09 15:38:54 +0100716 else:
717 raise NbiException("Method {} not allowed".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
tiernof27c79b2018-03-12 17:08:42 +0100718 return self._format_out(outdata, session, _format)
tierno0f98af52018-03-19 10:28:22 +0100719 except (NbiException, EngineException, DbException, FsException, MsgException) as e:
tiernof27c79b2018-03-12 17:08:42 +0100720 if hasattr(outdata, "close"): # is an open file
721 outdata.close()
tiernoc94c3df2018-02-09 15:38:54 +0100722 cherrypy.log("Exception {}".format(e))
723 cherrypy.response.status = e.http_code.value
tierno0f98af52018-03-19 10:28:22 +0100724 error_text = str(e)
725 if isinstance(e, MsgException):
726 error_text = "{} has been '{}' but other modules cannot be informed because an error on bus".format(
727 engine_item[:-1], method, error_text)
tiernoc94c3df2018-02-09 15:38:54 +0100728 problem_details = {
729 "code": e.http_code.name,
730 "status": e.http_code.value,
731 "detail": str(e),
732 }
733 return self._format_out(problem_details, session)
734 # raise cherrypy.HTTPError(e.http_code.value, str(e))
735
736
737# def validate_password(realm, username, password):
738# cherrypy.log("realm "+ str(realm))
739# if username == "admin" and password == "admin":
740# return True
741# return False
742
743
744def _start_service():
745 """
746 Callback function called when cherrypy.engine starts
747 Override configuration with env variables
748 Set database, storage, message configuration
749 Init database with admin/admin user password
750 """
751 cherrypy.log.error("Starting osm_nbi")
752 # update general cherrypy configuration
753 update_dict = {}
754
755 engine_config = cherrypy.tree.apps['/osm'].config
756 for k, v in environ.items():
757 if not k.startswith("OSMNBI_"):
758 continue
759 k1, _, k2 = k[7:].lower().partition("_")
760 if not k2:
761 continue
762 try:
763 # update static configuration
764 if k == 'OSMNBI_STATIC_DIR':
765 engine_config["/static"]['tools.staticdir.dir'] = v
766 engine_config["/static"]['tools.staticdir.on'] = True
767 elif k == 'OSMNBI_SOCKET_PORT' or k == 'OSMNBI_SERVER_PORT':
768 update_dict['server.socket_port'] = int(v)
769 elif k == 'OSMNBI_SOCKET_HOST' or k == 'OSMNBI_SERVER_HOST':
770 update_dict['server.socket_host'] = v
771 elif k1 == "server":
772 update_dict['server' + k2] = v
773 # TODO add more entries
774 elif k1 in ("message", "database", "storage"):
775 if k2 == "port":
776 engine_config[k1][k2] = int(v)
777 else:
778 engine_config[k1][k2] = v
779 except ValueError as e:
780 cherrypy.log.error("Ignoring environ '{}': " + str(e))
781 except Exception as e:
782 cherrypy.log.warn("skipping environ '{}' on exception '{}'".format(k, e))
783
784 if update_dict:
785 cherrypy.config.update(update_dict)
786
787 # logging cherrypy
788 log_format_simple = "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(message)s"
789 log_formatter_simple = logging.Formatter(log_format_simple, datefmt='%Y-%m-%dT%H:%M:%S')
790 logger_server = logging.getLogger("cherrypy.error")
791 logger_access = logging.getLogger("cherrypy.access")
792 logger_cherry = logging.getLogger("cherrypy")
793 logger_nbi = logging.getLogger("nbi")
794
795 if "logfile" in engine_config["global"]:
796 file_handler = logging.handlers.RotatingFileHandler(engine_config["global"]["logfile"],
797 maxBytes=100e6, backupCount=9, delay=0)
798 file_handler.setFormatter(log_formatter_simple)
799 logger_cherry.addHandler(file_handler)
800 logger_nbi.addHandler(file_handler)
801 else:
802 for format_, logger in {"nbi.server": logger_server,
803 "nbi.access": logger_access,
804 "%(name)s %(filename)s:%(lineno)s": logger_nbi
805 }.items():
806 log_format_cherry = "%(asctime)s %(levelname)s {} %(message)s".format(format_)
807 log_formatter_cherry = logging.Formatter(log_format_cherry, datefmt='%Y-%m-%dT%H:%M:%S')
808 str_handler = logging.StreamHandler()
809 str_handler.setFormatter(log_formatter_cherry)
810 logger.addHandler(str_handler)
811
812 if engine_config["global"].get("loglevel"):
813 logger_cherry.setLevel(engine_config["global"]["loglevel"])
814 logger_nbi.setLevel(engine_config["global"]["loglevel"])
815
816 # logging other modules
817 for k1, logname in {"message": "nbi.msg", "database": "nbi.db", "storage": "nbi.fs"}.items():
818 engine_config[k1]["logger_name"] = logname
819 logger_module = logging.getLogger(logname)
820 if "logfile" in engine_config[k1]:
821 file_handler = logging.handlers.RotatingFileHandler(engine_config[k1]["logfile"],
822 maxBytes=100e6, backupCount=9, delay=0)
823 file_handler.setFormatter(log_formatter_simple)
824 logger_module.addHandler(file_handler)
825 if "loglevel" in engine_config[k1]:
826 logger_module.setLevel(engine_config[k1]["loglevel"])
827 # TODO add more entries, e.g.: storage
828 cherrypy.tree.apps['/osm'].root.engine.start(engine_config)
829 try:
tierno4a946e42018-04-12 17:48:49 +0200830 cherrypy.tree.apps['/osm'].root.engine.init_db(target_version=database_version)
tiernoc94c3df2018-02-09 15:38:54 +0100831 except EngineException:
832 pass
833 # getenv('OSMOPENMANO_TENANT', None)
834
835
836def _stop_service():
837 """
838 Callback function called when cherrypy.engine stops
839 TODO: Ending database connections.
840 """
841 cherrypy.tree.apps['/osm'].root.engine.stop()
842 cherrypy.log.error("Stopping osm_nbi")
843
844def nbi():
845 # conf = {
846 # '/': {
847 # #'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
848 # 'tools.sessions.on': True,
849 # 'tools.response_headers.on': True,
850 # # 'tools.response_headers.headers': [('Content-Type', 'text/plain')],
851 # }
852 # }
853 # cherrypy.Server.ssl_module = 'builtin'
854 # cherrypy.Server.ssl_certificate = "http/cert.pem"
855 # cherrypy.Server.ssl_private_key = "http/privkey.pem"
856 # cherrypy.Server.thread_pool = 10
857 # cherrypy.config.update({'Server.socket_port': config["port"], 'Server.socket_host': config["host"]})
858
859 # cherrypy.config.update({'tools.auth_basic.on': True,
860 # 'tools.auth_basic.realm': 'localhost',
861 # 'tools.auth_basic.checkpassword': validate_password})
862 cherrypy.engine.subscribe('start', _start_service)
863 cherrypy.engine.subscribe('stop', _stop_service)
864 cherrypy.quickstart(Server(), '/osm', "nbi.cfg")
865
866
867if __name__ == '__main__':
868 nbi()