blob: 0520ef8ce68031ea8fb23da137664453baa623b4 [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>"
tierno0f98af52018-03-19 10:28:22 +010022__version__ = "0.3"
tierno55945e72018-04-06 16:40:27 +020023version_date = "Apr 2018"
tierno4a946e42018-04-12 17:48:49 +020024database_version = '1.0'
tiernoc94c3df2018-02-09 15:38:54 +010025
26"""
tiernof27c79b2018-03-12 17:08:42 +010027North Bound Interface (O: OSM specific; 5,X: SOL005 not implemented yet; O5: SOL005 implemented)
tiernoc94c3df2018-02-09 15:38:54 +010028URL: /osm GET POST PUT DELETE PATCH
tiernof27c79b2018-03-12 17:08:42 +010029 /nsd/v1 O O
30 /ns_descriptors_content O O
31 /<nsdInfoId> O O O O
tiernoc94c3df2018-02-09 15:38:54 +010032 /ns_descriptors O5 O5
33 /<nsdInfoId> O5 O5 5
34 /nsd_content O5 O5
tiernof27c79b2018-03-12 17:08:42 +010035 /nsd O
36 /artifacts[/<artifactPath>] O
tiernoc94c3df2018-02-09 15:38:54 +010037 /pnf_descriptors 5 5
38 /<pnfdInfoId> 5 5 5
39 /pnfd_content 5 5
tiernof27c79b2018-03-12 17:08:42 +010040 /subscriptions 5 5
41 /<subscriptionId> 5 X
tiernoc94c3df2018-02-09 15:38:54 +010042
43 /vnfpkgm/v1
tierno55945e72018-04-06 16:40:27 +020044 /vnf_packages_content O O
45 /<vnfPkgId> O O
tiernoc94c3df2018-02-09 15:38:54 +010046 /vnf_packages O5 O5
47 /<vnfPkgId> O5 O5 5
tiernoc94c3df2018-02-09 15:38:54 +010048 /package_content O5 O5
49 /upload_from_uri X
tiernof27c79b2018-03-12 17:08:42 +010050 /vnfd O5
51 /artifacts[/<artifactPath>] O5
52 /subscriptions X X
53 /<subscriptionId> X X
tiernoc94c3df2018-02-09 15:38:54 +010054
55 /nslcm/v1
tiernof27c79b2018-03-12 17:08:42 +010056 /ns_instances_content O O
57 /<nsInstanceId> O O
58 /ns_instances 5 5
59 /<nsInstanceId> 5 5
tiernoc94c3df2018-02-09 15:38:54 +010060 TO BE COMPLETED
61 /ns_lcm_op_occs 5 5
62 /<nsLcmOpOccId> 5 5 5
63 TO BE COMPLETED 5 5
tiernof27c79b2018-03-12 17:08:42 +010064 /subscriptions 5 5
65 /<subscriptionId> 5 X
66 /admin/v1
67 /tokens O O
68 /<id> O O
69 /users O O
70 /<id> O O
71 /projects O O
72 /<id> O O
tierno0f98af52018-03-19 10:28:22 +010073 /vims O O
74 /<id> O O O
75 /sdns O O
76 /<id> O O O
tiernoc94c3df2018-02-09 15:38:54 +010077
78query string.
79 <attrName>[.<attrName>...]*[.<op>]=<value>[,<value>...]&...
80 op: "eq"(or empty to one or the values) | "neq" (to any of the values) | "gt" | "lt" | "gte" | "lte" | "cont" | "ncont"
81 all_fields, fields=x,y,.., exclude_default, exclude_fields=x,y,...
82 (none) … same as “exclude_default”
83 all_fields … all attributes.
84 fields=<list> … all attributes except all complex attributes with minimum cardinality of zero that are not conditionally mandatory, and that are not provided in <list>.
85 exclude_fields=<list> … all attributes except those complex attributes with a minimum cardinality of zero that are not conditionally mandatory, and that are provided in <list>.
86 exclude_default … all attributes except those complex attributes with a minimum cardinality of zero that are not conditionally mandatory, and that are part of the "default exclude set" defined in the present specification for the particular resource
87 exclude_default and include=<list> … all attributes except those complex attributes with a minimum cardinality of zero that are not conditionally mandatory and that are part of the "default exclude set" defined in the present specification for the particular resource, but that are not part of <list>
88Header field name Reference Example Descriptions
89 Accept IETF RFC 7231 [19] application/json Content-Types that are acceptable for the response.
90 This header field shall be present if the response is expected to have a non-empty message body.
91 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the request.
92 This header field shall be present if the request has a non-empty message body.
93 Authorization IETF RFC 7235 [22] Bearer mF_9.B5f-4.1JqM The authorization token for the request. Details are specified in clause 4.5.3.
94 Range IETF RFC 7233 [21] 1000-2000 Requested range of bytes from a file
95Header field name Reference Example Descriptions
96 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the response.
97 This header field shall be present if the response has a non-empty message body.
98 Location IETF RFC 7231 [19] http://www.example.com/vnflcm/v1/vnf_instances/123 Used in redirection, or when a new resource has been created.
99 This header field shall be present if the response status code is 201 or 3xx.
100 In the present document this header field is also used if the response status code is 202 and a new resource was created.
101 WWW-Authenticate IETF RFC 7235 [22] Bearer realm="example" Challenge if the corresponding HTTP request has not provided authorization, or error details if the corresponding HTTP request has provided an invalid authorization token.
102 Accept-Ranges IETF RFC 7233 [21] bytes Used by the Server to signal whether or not it supports ranges for certain resources.
103 Content-Range IETF RFC 7233 [21] bytes 21010-47021/ 47022 Signals the byte range that is contained in the response, and the total length of the file.
104 Retry-After IETF RFC 7231 [19] Fri, 31 Dec 1999 23:59:59 GMT
105
106 or
107
108 120 Used to indicate how long the user agent ought to wait before making a follow-up request.
109 It can be used with 503 responses.
110 The value of this field can be an HTTP-date or a number of seconds to delay after the response is received.
111
112 #TODO http header for partial uploads: Content-Range: "bytes 0-1199/15000". Id is returned first time and send in following chunks
113"""
114
115
116class NbiException(Exception):
117
118 def __init__(self, message, http_code=HTTPStatus.METHOD_NOT_ALLOWED):
119 Exception.__init__(self, message)
120 self.http_code = http_code
121
122
123class Server(object):
124 instance = 0
125 # to decode bytes to str
126 reader = getreader("utf-8")
127
128 def __init__(self):
129 self.instance += 1
130 self.engine = Engine()
tiernof27c79b2018-03-12 17:08:42 +0100131 self.valid_methods = { # contains allowed URL and methods
132 "admin": {
133 "v1": {
tierno0f98af52018-03-19 10:28:22 +0100134 "tokens": {"METHODS": ("GET", "POST", "DELETE"),
tiernof27c79b2018-03-12 17:08:42 +0100135 "<ID>": { "METHODS": ("GET", "DELETE")}
136 },
tierno0f98af52018-03-19 10:28:22 +0100137 "users": {"METHODS": ("GET", "POST"),
tiernof27c79b2018-03-12 17:08:42 +0100138 "<ID>": {"METHODS": ("GET", "POST", "DELETE")}
139 },
tierno0f98af52018-03-19 10:28:22 +0100140 "projects": {"METHODS": ("GET", "POST"),
141 "<ID>": {"METHODS": ("GET", "DELETE")}
142 },
143 "vims": {"METHODS": ("GET", "POST"),
144 "<ID>": {"METHODS": ("GET", "DELETE")}
145 },
146 "sdns": {"METHODS": ("GET", "POST"),
147 "<ID>": {"METHODS": ("GET", "DELETE")}
tiernof27c79b2018-03-12 17:08:42 +0100148 },
149 }
150 },
151 "nsd": {
152 "v1": {
153 "ns_descriptors_content": { "METHODS": ("GET", "POST"),
154 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
155 },
156 "ns_descriptors": { "METHODS": ("GET", "POST"),
tierno0f98af52018-03-19 10:28:22 +0100157 "<ID>": {"METHODS": ("GET", "DELETE"), "TODO": "PATCH",
tiernof27c79b2018-03-12 17:08:42 +0100158 "nsd_content": { "METHODS": ("GET", "PUT")},
159 "nsd": {"METHODS": "GET"}, # descriptor inside package
160 "artifacts": {"*": {"METHODS": "GET"}}
161 }
162
163 },
164 "pnf_descriptors": {"TODO": ("GET", "POST"),
165 "<ID>": {"TODO": ("GET", "DELETE", "PATCH"),
166 "pnfd_content": {"TODO": ("GET", "PUT")}
167 }
168 },
169 "subscriptions": {"TODO": ("GET", "POST"),
170 "<ID>": {"TODO": ("GET", "DELETE"),}
171 },
172 }
173 },
174 "vnfpkgm": {
175 "v1": {
176 "vnf_packages_content": { "METHODS": ("GET", "POST"),
177 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
178 },
179 "vnf_packages": { "METHODS": ("GET", "POST"),
180 "<ID>": { "METHODS": ("GET", "DELETE"), "TODO": "PATCH", # GET: vnfPkgInfo
181 "package_content": { "METHODS": ("GET", "PUT"), # package
182 "upload_from_uri": {"TODO": "POST"}
183 },
184 "vnfd": {"METHODS": "GET"}, # descriptor inside package
185 "artifacts": {"*": {"METHODS": "GET"}}
186 }
187
188 },
189 "subscriptions": {"TODO": ("GET", "POST"),
190 "<ID>": {"TODO": ("GET", "DELETE"),}
191 },
192 }
193 },
194 "nslcm": {
195 "v1": {
196 "ns_instances_content": {"METHODS": ("GET", "POST"),
197 "<ID>": {"METHODS": ("GET", "DELETE")}
198 },
199 "ns_instances": {"TODO": ("GET", "POST"),
200 "<ID>": {"TODO": ("GET", "DELETE")}
201 }
202 }
203 },
204 }
tiernoc94c3df2018-02-09 15:38:54 +0100205
206 def _authorization(self):
207 token = None
208 user_passwd64 = None
209 try:
210 # 1. Get token Authorization bearer
211 auth = cherrypy.request.headers.get("Authorization")
212 if auth:
213 auth_list = auth.split(" ")
214 if auth_list[0].lower() == "bearer":
215 token = auth_list[-1]
216 elif auth_list[0].lower() == "basic":
217 user_passwd64 = auth_list[-1]
218 if not token:
219 if cherrypy.session.get("Authorization"):
220 # 2. Try using session before request a new token. If not, basic authentication will generate
221 token = cherrypy.session.get("Authorization")
222 if token == "logout":
223 token = None # force Unauthorized response to insert user pasword again
224 elif user_passwd64 and cherrypy.request.config.get("auth.allow_basic_authentication"):
225 # 3. Get new token from user password
226 user = None
227 passwd = None
228 try:
229 user_passwd = standard_b64decode(user_passwd64).decode()
230 user, _, passwd = user_passwd.partition(":")
231 except:
232 pass
233 outdata = self.engine.new_token(None, {"username": user, "password": passwd})
234 token = outdata["id"]
235 cherrypy.session['Authorization'] = token
236 # 4. Get token from cookie
237 # if not token:
238 # auth_cookie = cherrypy.request.cookie.get("Authorization")
239 # if auth_cookie:
240 # token = auth_cookie.value
241 return self.engine.authorize(token)
242 except EngineException as e:
243 if cherrypy.session.get('Authorization'):
244 del cherrypy.session['Authorization']
245 cherrypy.response.headers["WWW-Authenticate"] = 'Bearer realm="{}"'.format(e)
246 raise
247
248 def _format_in(self, kwargs):
249 try:
250 indata = None
251 if cherrypy.request.body.length:
252 error_text = "Invalid input format "
253
254 if "Content-Type" in cherrypy.request.headers:
255 if "application/json" in cherrypy.request.headers["Content-Type"]:
256 error_text = "Invalid json format "
257 indata = json.load(self.reader(cherrypy.request.body))
258 elif "application/yaml" in cherrypy.request.headers["Content-Type"]:
259 error_text = "Invalid yaml format "
260 indata = yaml.load(cherrypy.request.body)
261 elif "application/binary" in cherrypy.request.headers["Content-Type"] or \
262 "application/gzip" in cherrypy.request.headers["Content-Type"] or \
tiernof27c79b2018-03-12 17:08:42 +0100263 "application/zip" in cherrypy.request.headers["Content-Type"] or \
264 "text/plain" in cherrypy.request.headers["Content-Type"]:
265 indata = cherrypy.request.body # .read()
tiernoc94c3df2018-02-09 15:38:54 +0100266 elif "multipart/form-data" in cherrypy.request.headers["Content-Type"]:
267 if "descriptor_file" in kwargs:
268 filecontent = kwargs.pop("descriptor_file")
269 if not filecontent.file:
270 raise NbiException("empty file or content", HTTPStatus.BAD_REQUEST)
tiernof27c79b2018-03-12 17:08:42 +0100271 indata = filecontent.file # .read()
tiernoc94c3df2018-02-09 15:38:54 +0100272 if filecontent.content_type.value:
273 cherrypy.request.headers["Content-Type"] = filecontent.content_type.value
274 else:
275 # raise cherrypy.HTTPError(HTTPStatus.Not_Acceptable,
276 # "Only 'Content-Type' of type 'application/json' or
277 # 'application/yaml' for input format are available")
278 error_text = "Invalid yaml format "
279 indata = yaml.load(cherrypy.request.body)
280 else:
281 error_text = "Invalid yaml format "
282 indata = yaml.load(cherrypy.request.body)
283 if not indata:
284 indata = {}
285
tiernoc94c3df2018-02-09 15:38:54 +0100286 format_yaml = False
287 if cherrypy.request.headers.get("Query-String-Format") == "yaml":
288 format_yaml = True
289
290 for k, v in kwargs.items():
291 if isinstance(v, str):
292 if v == "":
293 kwargs[k] = None
294 elif format_yaml:
295 try:
296 kwargs[k] = yaml.load(v)
297 except:
298 pass
299 elif k.endswith(".gt") or k.endswith(".lt") or k.endswith(".gte") or k.endswith(".lte"):
300 try:
301 kwargs[k] = int(v)
302 except:
303 try:
304 kwargs[k] = float(v)
305 except:
306 pass
307 elif v.find(",") > 0:
308 kwargs[k] = v.split(",")
309 elif isinstance(v, (list, tuple)):
310 for index in range(0, len(v)):
311 if v[index] == "":
312 v[index] = None
313 elif format_yaml:
314 try:
315 v[index] = yaml.load(v[index])
316 except:
317 pass
318
tiernof27c79b2018-03-12 17:08:42 +0100319 return indata
tiernoc94c3df2018-02-09 15:38:54 +0100320 except (ValueError, yaml.YAMLError) as exc:
321 raise NbiException(error_text + str(exc), HTTPStatus.BAD_REQUEST)
322 except KeyError as exc:
323 raise NbiException("Query string error: " + str(exc), HTTPStatus.BAD_REQUEST)
324
325 @staticmethod
tiernof27c79b2018-03-12 17:08:42 +0100326 def _format_out(data, session=None, _format=None):
tiernoc94c3df2018-02-09 15:38:54 +0100327 """
328 return string of dictionary data according to requested json, yaml, xml. By default json
tiernof27c79b2018-03-12 17:08:42 +0100329 :param data: response to be sent. Can be a dict, text or file
tiernoc94c3df2018-02-09 15:38:54 +0100330 :param session:
tiernof27c79b2018-03-12 17:08:42 +0100331 :param _format: The format to be set as Content-Type ir data is a file
tiernoc94c3df2018-02-09 15:38:54 +0100332 :return: None
333 """
tierno0f98af52018-03-19 10:28:22 +0100334 accept = cherrypy.request.headers.get("Accept")
tiernof27c79b2018-03-12 17:08:42 +0100335 if data is None:
tierno0f98af52018-03-19 10:28:22 +0100336 if accept and "text/html" in accept:
337 return html.format(data, cherrypy.request, cherrypy.response, session)
tiernof27c79b2018-03-12 17:08:42 +0100338 cherrypy.response.status = HTTPStatus.NO_CONTENT.value
339 return
340 elif hasattr(data, "read"): # file object
341 if _format:
342 cherrypy.response.headers["Content-Type"] = _format
343 elif "b" in data.mode: # binariy asssumig zip
344 cherrypy.response.headers["Content-Type"] = 'application/zip'
345 else:
346 cherrypy.response.headers["Content-Type"] = 'text/plain'
347 # TODO check that cherrypy close file. If not implement pending things to close per thread next
348 return data
tierno0f98af52018-03-19 10:28:22 +0100349 if accept:
tiernoc94c3df2018-02-09 15:38:54 +0100350 if "application/json" in accept:
351 cherrypy.response.headers["Content-Type"] = 'application/json; charset=utf-8'
352 a = json.dumps(data, indent=4) + "\n"
353 return a.encode("utf8")
354 elif "text/html" in accept:
355 return html.format(data, cherrypy.request, cherrypy.response, session)
356
tiernof27c79b2018-03-12 17:08:42 +0100357 elif "application/yaml" in accept or "*/*" in accept or "text/plain" in accept:
tiernoc94c3df2018-02-09 15:38:54 +0100358 pass
359 else:
360 raise cherrypy.HTTPError(HTTPStatus.NOT_ACCEPTABLE.value,
361 "Only 'Accept' of type 'application/json' or 'application/yaml' "
362 "for output format are available")
363 cherrypy.response.headers["Content-Type"] = 'application/yaml'
364 return yaml.safe_dump(data, explicit_start=True, indent=4, default_flow_style=False, tags=False,
365 encoding='utf-8', allow_unicode=True) # , canonical=True, default_style='"'
366
367 @cherrypy.expose
368 def index(self, *args, **kwargs):
369 session = None
370 try:
371 if cherrypy.request.method == "GET":
372 session = self._authorization()
373 outdata = "Index page"
374 else:
375 raise cherrypy.HTTPError(HTTPStatus.METHOD_NOT_ALLOWED.value,
376 "Method {} not allowed for tokens".format(cherrypy.request.method))
377
378 return self._format_out(outdata, session)
379
380 except EngineException as e:
381 cherrypy.log("index Exception {}".format(e))
382 cherrypy.response.status = e.http_code.value
383 return self._format_out("Welcome to OSM!", session)
384
385 @cherrypy.expose
tierno55945e72018-04-06 16:40:27 +0200386 def version(self, *args, **kwargs):
387 global __version__, version_date
388 try:
389 if cherrypy.request.method != "GET":
390 raise NbiException("Only method GET is allowed", HTTPStatus.METHOD_NOT_ALLOWED)
391 elif args or kwargs:
392 raise NbiException("Invalid URL or query string for version", HTTPStatus.METHOD_NOT_ALLOWED)
393 return __version__ + " " + version_date
394 except NbiException as e:
395 cherrypy.response.status = e.http_code.value
396 problem_details = {
397 "code": e.http_code.name,
398 "status": e.http_code.value,
399 "detail": str(e),
400 }
401 return self._format_out(problem_details, None)
402
403 @cherrypy.expose
tiernof27c79b2018-03-12 17:08:42 +0100404 def token(self, method, token_id=None, kwargs=None):
tiernoc94c3df2018-02-09 15:38:54 +0100405 session = None
406 # self.engine.load_dbase(cherrypy.request.app.config)
tiernof27c79b2018-03-12 17:08:42 +0100407 indata = self._format_in(kwargs)
408 if not isinstance(indata, dict):
409 raise NbiException("Expected application/yaml or application/json Content-Type", HTTPStatus.BAD_REQUEST)
tiernoc94c3df2018-02-09 15:38:54 +0100410 try:
tiernoc94c3df2018-02-09 15:38:54 +0100411 if method == "GET":
412 session = self._authorization()
tiernof27c79b2018-03-12 17:08:42 +0100413 if token_id:
414 outdata = self.engine.get_token(session, token_id)
tiernoc94c3df2018-02-09 15:38:54 +0100415 else:
416 outdata = self.engine.get_token_list(session)
417 elif method == "POST":
418 try:
419 session = self._authorization()
420 except:
421 session = None
422 if kwargs:
423 indata.update(kwargs)
424 outdata = self.engine.new_token(session, indata, cherrypy.request.remote)
425 session = outdata
426 cherrypy.session['Authorization'] = outdata["_id"]
tierno0f98af52018-03-19 10:28:22 +0100427 self._set_location_header("admin", "v1", "tokens", outdata["_id"])
tiernoc94c3df2018-02-09 15:38:54 +0100428 # cherrypy.response.cookie["Authorization"] = outdata["id"]
429 # cherrypy.response.cookie["Authorization"]['expires'] = 3600
430 elif method == "DELETE":
tiernof27c79b2018-03-12 17:08:42 +0100431 if not token_id and "id" in kwargs:
tiernoc94c3df2018-02-09 15:38:54 +0100432 token_id = kwargs["id"]
tiernof27c79b2018-03-12 17:08:42 +0100433 elif not token_id:
tiernoc94c3df2018-02-09 15:38:54 +0100434 session = self._authorization()
435 token_id = session["_id"]
436 outdata = self.engine.del_token(token_id)
tierno0f98af52018-03-19 10:28:22 +0100437 oudata = None
tiernoc94c3df2018-02-09 15:38:54 +0100438 session = None
439 cherrypy.session['Authorization'] = "logout"
440 # cherrypy.response.cookie["Authorization"] = token_id
441 # cherrypy.response.cookie["Authorization"]['expires'] = 0
442 else:
443 raise NbiException("Method {} not allowed for token".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
444 return self._format_out(outdata, session)
445 except (NbiException, EngineException, DbException) as e:
446 cherrypy.log("tokens Exception {}".format(e))
447 cherrypy.response.status = e.http_code.value
448 problem_details = {
449 "code": e.http_code.name,
450 "status": e.http_code.value,
451 "detail": str(e),
452 }
453 return self._format_out(problem_details, session)
454
455 @cherrypy.expose
456 def test(self, *args, **kwargs):
457 thread_info = None
tiernof27c79b2018-03-12 17:08:42 +0100458 if args and args[0] == "help":
459 return "<html><pre>\ninit\nfile/<name> download file\ndb-clear/table\nprune\nlogin\nlogin2\n"\
tierno55945e72018-04-06 16:40:27 +0200460 "sleep/<time>\nmessage/topic\n</pre></html>"
tiernof27c79b2018-03-12 17:08:42 +0100461
462 elif args and args[0] == "init":
tiernoc94c3df2018-02-09 15:38:54 +0100463 try:
464 # self.engine.load_dbase(cherrypy.request.app.config)
465 self.engine.create_admin()
466 return "Done. User 'admin', password 'admin' created"
467 except Exception:
468 cherrypy.response.status = HTTPStatus.FORBIDDEN.value
469 return self._format_out("Database already initialized")
tiernof27c79b2018-03-12 17:08:42 +0100470 elif args and args[0] == "file":
471 return cherrypy.lib.static.serve_file(cherrypy.tree.apps['/osm'].config["storage"]["path"] + "/" + args[1],
472 "text/plain", "attachment")
473 elif args and args[0] == "file2":
474 f_path = cherrypy.tree.apps['/osm'].config["storage"]["path"] + "/" + args[1]
475 f = open(f_path, "r")
476 cherrypy.response.headers["Content-type"] = "text/plain"
tiernof27c79b2018-03-12 17:08:42 +0100477 return f
tierno55945e72018-04-06 16:40:27 +0200478
tiernof27c79b2018-03-12 17:08:42 +0100479 elif len(args) == 2 and args[0] == "db-clear":
480 return self.engine.del_item_list({"project_id": "admin"}, args[1], {})
tiernoc94c3df2018-02-09 15:38:54 +0100481 elif args and args[0] == "prune":
482 return self.engine.prune()
483 elif args and args[0] == "login":
484 if not cherrypy.request.headers.get("Authorization"):
485 cherrypy.response.headers["WWW-Authenticate"] = 'Basic realm="Access to OSM site", charset="UTF-8"'
486 cherrypy.response.status = HTTPStatus.UNAUTHORIZED.value
487 elif args and args[0] == "login2":
488 if not cherrypy.request.headers.get("Authorization"):
489 cherrypy.response.headers["WWW-Authenticate"] = 'Bearer realm="Access to OSM site"'
490 cherrypy.response.status = HTTPStatus.UNAUTHORIZED.value
491 elif args and args[0] == "sleep":
492 sleep_time = 5
493 try:
494 sleep_time = int(args[1])
495 except Exception:
496 cherrypy.response.status = HTTPStatus.FORBIDDEN.value
497 return self._format_out("Database already initialized")
498 thread_info = cherrypy.thread_data
499 print(thread_info)
500 time.sleep(sleep_time)
501 # thread_info
502 elif len(args) >= 2 and args[0] == "message":
503 topic = args[1]
tierno55945e72018-04-06 16:40:27 +0200504 return_text = "<html><pre>{} ->\n".format(topic)
tiernoc94c3df2018-02-09 15:38:54 +0100505 try:
tierno55945e72018-04-06 16:40:27 +0200506 if cherrypy.request.method == 'POST':
507 to_send = yaml.load(cherrypy.request.body)
508 for k, v in to_send.items():
509 self.engine.msg.write(topic, k, v)
510 return_text += " {}: {}\n".format(k, v)
511 elif cherrypy.request.method == 'GET':
512 for k, v in kwargs.items():
513 self.engine.msg.write(topic, k, yaml.load(v))
514 return_text += " {}: {}\n".format(k, yaml.load(v))
tiernoc94c3df2018-02-09 15:38:54 +0100515 except Exception as e:
tierno55945e72018-04-06 16:40:27 +0200516 return_text += "Error: " + str(e)
517 return_text += "</pre></html>\n"
518 return return_text
tiernoc94c3df2018-02-09 15:38:54 +0100519
520 return_text = (
521 "<html><pre>\nheaders:\n args: {}\n".format(args) +
522 " kwargs: {}\n".format(kwargs) +
523 " headers: {}\n".format(cherrypy.request.headers) +
524 " path_info: {}\n".format(cherrypy.request.path_info) +
525 " query_string: {}\n".format(cherrypy.request.query_string) +
526 " session: {}\n".format(cherrypy.session) +
527 " cookie: {}\n".format(cherrypy.request.cookie) +
528 " method: {}\n".format(cherrypy.request.method) +
529 " session: {}\n".format(cherrypy.session.get('fieldname')) +
530 " body:\n")
531 return_text += " length: {}\n".format(cherrypy.request.body.length)
532 if cherrypy.request.body.length:
533 return_text += " content: {}\n".format(
534 str(cherrypy.request.body.read(int(cherrypy.request.headers.get('Content-Length', 0)))))
535 if thread_info:
536 return_text += "thread: {}\n".format(thread_info)
537 return_text += "</pre></html>"
538 return return_text
539
tiernof27c79b2018-03-12 17:08:42 +0100540 def _check_valid_url_method(self, method, *args):
541 if len(args) < 3:
542 raise NbiException("URL must contain at least 'topic/version/item'", HTTPStatus.METHOD_NOT_ALLOWED)
543
544 reference = self.valid_methods
545 for arg in args:
546 if arg is None:
547 break
548 if not isinstance(reference, dict):
549 raise NbiException("URL contains unexpected extra items '{}'".format(arg),
550 HTTPStatus.METHOD_NOT_ALLOWED)
551
552 if arg in reference:
553 reference = reference[arg]
554 elif "<ID>" in reference:
555 reference = reference["<ID>"]
556 elif "*" in reference:
557 reference = reference["*"]
558 break
559 else:
560 raise NbiException("Unexpected URL item {}".format(arg), HTTPStatus.METHOD_NOT_ALLOWED)
561 if "TODO" in reference and method in reference["TODO"]:
562 raise NbiException("Method {} not supported yet for this URL".format(method), HTTPStatus.NOT_IMPLEMENTED)
563 elif "METHODS" in reference and not method in reference["METHODS"]:
564 raise NbiException("Method {} not supported for this URL".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
565 return
566
567 @staticmethod
568 def _set_location_header(topic, version, item, id):
569 """
570 Insert response header Location with the URL of created item base on URL params
571 :param topic:
572 :param version:
573 :param item:
574 :param id:
575 :return: None
576 """
577 # Use cherrypy.request.base for absoluted path and make use of request.header HOST just in case behind aNAT
578 cherrypy.response.headers["Location"] = "/osm/{}/{}/{}/{}".format(topic, version, item, id)
579 return
580
tiernoc94c3df2018-02-09 15:38:54 +0100581 @cherrypy.expose
tiernof27c79b2018-03-12 17:08:42 +0100582 def default(self, topic=None, version=None, item=None, _id=None, item2=None, *args, **kwargs):
tiernoc94c3df2018-02-09 15:38:54 +0100583 session = None
tiernof27c79b2018-03-12 17:08:42 +0100584 outdata = None
585 _format = None
tierno0f98af52018-03-19 10:28:22 +0100586 method = "DONE"
587 engine_item = None
tiernoc94c3df2018-02-09 15:38:54 +0100588 try:
tiernof27c79b2018-03-12 17:08:42 +0100589 if not topic or not version or not item:
590 raise NbiException("URL must contain at least 'topic/version/item'", HTTPStatus.METHOD_NOT_ALLOWED)
591 if topic not in ("admin", "vnfpkgm", "nsd", "nslcm"):
592 raise NbiException("URL topic '{}' not supported".format(topic), HTTPStatus.METHOD_NOT_ALLOWED)
tiernoc94c3df2018-02-09 15:38:54 +0100593 if version != 'v1':
594 raise NbiException("URL version '{}' not supported".format(version), HTTPStatus.METHOD_NOT_ALLOWED)
595
tiernof27c79b2018-03-12 17:08:42 +0100596 if kwargs and "METHOD" in kwargs and kwargs["METHOD"] in ("PUT", "POST", "DELETE", "GET", "PATCH"):
597 method = kwargs.pop("METHOD")
598 else:
599 method = cherrypy.request.method
600
601 self._check_valid_url_method(method, topic, version, item, _id, item2, *args)
602
603 if topic == "admin" and item == "tokens":
604 return self.token(method, _id, kwargs)
605
tiernoc94c3df2018-02-09 15:38:54 +0100606 # self.engine.load_dbase(cherrypy.request.app.config)
607 session = self._authorization()
tiernof27c79b2018-03-12 17:08:42 +0100608 indata = self._format_in(kwargs)
609 engine_item = item
610 if item == "subscriptions":
611 engine_item = topic + "_" + item
612 if item2:
613 engine_item = item2
tiernoc94c3df2018-02-09 15:38:54 +0100614
tiernof27c79b2018-03-12 17:08:42 +0100615 if topic == "nsd":
616 engine_item = "nsds"
617 elif topic == "vnfpkgm":
618 engine_item = "vnfds"
619 elif topic == "nslcm":
620 engine_item = "nsrs"
tiernoc94c3df2018-02-09 15:38:54 +0100621
622 if method == "GET":
tiernof27c79b2018-03-12 17:08:42 +0100623 if item2 in ("nsd_content", "package_content", "artifacts", "vnfd", "nsd"):
624 if item2 in ("vnfd", "nsd"):
625 path = "$DESCRIPTOR"
626 elif args:
627 path = args
628 elif item2 == "artifacts":
629 path = ()
630 else:
631 path = None
632 file, _format = self.engine.get_file(session, engine_item, _id, path,
633 cherrypy.request.headers.get("Accept"))
634 outdata = file
635 elif not _id:
636 outdata = self.engine.get_item_list(session, engine_item, kwargs)
tiernoc94c3df2018-02-09 15:38:54 +0100637 else:
tiernof27c79b2018-03-12 17:08:42 +0100638 outdata = self.engine.get_item(session, engine_item, _id)
639 elif method == "POST":
640 if item in ("ns_descriptors_content", "vnf_packages_content"):
641 _id = cherrypy.request.headers.get("Transaction-Id")
642 if not _id:
643 _id = self.engine.new_item(session, engine_item, {}, None, cherrypy.request.headers)
644 completed = self.engine.upload_content(session, engine_item, _id, indata, kwargs, cherrypy.request.headers)
645 if completed:
646 self._set_location_header(topic, version, item, _id)
647 else:
648 cherrypy.response.headers["Transaction-Id"] = _id
649 outdata = {"id": _id}
650 elif item in ("ns_descriptors", "vnf_packages"):
651 _id = self.engine.new_item(session, engine_item, indata, kwargs, cherrypy.request.headers)
652 self._set_location_header(topic, version, item, _id)
653 #TODO form NsdInfo
654 outdata = {"id": _id}
655 else:
656 _id = self.engine.new_item(session, engine_item, indata, kwargs, cherrypy.request.headers)
657 self._set_location_header(topic, version, item, _id)
658 outdata = {"id": _id}
659 cherrypy.response.status = HTTPStatus.CREATED.value
tiernoc94c3df2018-02-09 15:38:54 +0100660 elif method == "DELETE":
661 if not _id:
tiernof27c79b2018-03-12 17:08:42 +0100662 outdata = self.engine.del_item_list(session, engine_item, kwargs)
tiernoc94c3df2018-02-09 15:38:54 +0100663 else: # len(args) > 1
tierno0f98af52018-03-19 10:28:22 +0100664 # TODO return 202 ACCEPTED for nsrs vims
665 self.engine.del_item(session, engine_item, _id)
tiernof27c79b2018-03-12 17:08:42 +0100666 outdata = None
tiernoc94c3df2018-02-09 15:38:54 +0100667 elif method == "PUT":
tiernof27c79b2018-03-12 17:08:42 +0100668 if not indata and not kwargs:
tiernoc94c3df2018-02-09 15:38:54 +0100669 raise NbiException("Nothing to update. Provide payload and/or query string",
670 HTTPStatus.BAD_REQUEST)
tiernof27c79b2018-03-12 17:08:42 +0100671 if item2 in ("nsd_content", "package_content"):
672 completed = self.engine.upload_content(session, engine_item, _id, indata, kwargs, cherrypy.request.headers)
673 if not completed:
674 cherrypy.response.headers["Transaction-Id"] = id
675 outdata = None
676 else:
677 outdata = {"id": self.engine.edit_item(session, engine_item, args[1], indata, kwargs)}
tiernoc94c3df2018-02-09 15:38:54 +0100678 else:
679 raise NbiException("Method {} not allowed".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
tiernof27c79b2018-03-12 17:08:42 +0100680 return self._format_out(outdata, session, _format)
tierno0f98af52018-03-19 10:28:22 +0100681 except (NbiException, EngineException, DbException, FsException, MsgException) as e:
tiernof27c79b2018-03-12 17:08:42 +0100682 if hasattr(outdata, "close"): # is an open file
683 outdata.close()
tiernoc94c3df2018-02-09 15:38:54 +0100684 cherrypy.log("Exception {}".format(e))
685 cherrypy.response.status = e.http_code.value
tierno0f98af52018-03-19 10:28:22 +0100686 error_text = str(e)
687 if isinstance(e, MsgException):
688 error_text = "{} has been '{}' but other modules cannot be informed because an error on bus".format(
689 engine_item[:-1], method, error_text)
tiernoc94c3df2018-02-09 15:38:54 +0100690 problem_details = {
691 "code": e.http_code.name,
692 "status": e.http_code.value,
693 "detail": str(e),
694 }
695 return self._format_out(problem_details, session)
696 # raise cherrypy.HTTPError(e.http_code.value, str(e))
697
698
699# def validate_password(realm, username, password):
700# cherrypy.log("realm "+ str(realm))
701# if username == "admin" and password == "admin":
702# return True
703# return False
704
705
706def _start_service():
707 """
708 Callback function called when cherrypy.engine starts
709 Override configuration with env variables
710 Set database, storage, message configuration
711 Init database with admin/admin user password
712 """
713 cherrypy.log.error("Starting osm_nbi")
714 # update general cherrypy configuration
715 update_dict = {}
716
717 engine_config = cherrypy.tree.apps['/osm'].config
718 for k, v in environ.items():
719 if not k.startswith("OSMNBI_"):
720 continue
721 k1, _, k2 = k[7:].lower().partition("_")
722 if not k2:
723 continue
724 try:
725 # update static configuration
726 if k == 'OSMNBI_STATIC_DIR':
727 engine_config["/static"]['tools.staticdir.dir'] = v
728 engine_config["/static"]['tools.staticdir.on'] = True
729 elif k == 'OSMNBI_SOCKET_PORT' or k == 'OSMNBI_SERVER_PORT':
730 update_dict['server.socket_port'] = int(v)
731 elif k == 'OSMNBI_SOCKET_HOST' or k == 'OSMNBI_SERVER_HOST':
732 update_dict['server.socket_host'] = v
733 elif k1 == "server":
734 update_dict['server' + k2] = v
735 # TODO add more entries
736 elif k1 in ("message", "database", "storage"):
737 if k2 == "port":
738 engine_config[k1][k2] = int(v)
739 else:
740 engine_config[k1][k2] = v
741 except ValueError as e:
742 cherrypy.log.error("Ignoring environ '{}': " + str(e))
743 except Exception as e:
744 cherrypy.log.warn("skipping environ '{}' on exception '{}'".format(k, e))
745
746 if update_dict:
747 cherrypy.config.update(update_dict)
748
749 # logging cherrypy
750 log_format_simple = "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(message)s"
751 log_formatter_simple = logging.Formatter(log_format_simple, datefmt='%Y-%m-%dT%H:%M:%S')
752 logger_server = logging.getLogger("cherrypy.error")
753 logger_access = logging.getLogger("cherrypy.access")
754 logger_cherry = logging.getLogger("cherrypy")
755 logger_nbi = logging.getLogger("nbi")
756
757 if "logfile" in engine_config["global"]:
758 file_handler = logging.handlers.RotatingFileHandler(engine_config["global"]["logfile"],
759 maxBytes=100e6, backupCount=9, delay=0)
760 file_handler.setFormatter(log_formatter_simple)
761 logger_cherry.addHandler(file_handler)
762 logger_nbi.addHandler(file_handler)
763 else:
764 for format_, logger in {"nbi.server": logger_server,
765 "nbi.access": logger_access,
766 "%(name)s %(filename)s:%(lineno)s": logger_nbi
767 }.items():
768 log_format_cherry = "%(asctime)s %(levelname)s {} %(message)s".format(format_)
769 log_formatter_cherry = logging.Formatter(log_format_cherry, datefmt='%Y-%m-%dT%H:%M:%S')
770 str_handler = logging.StreamHandler()
771 str_handler.setFormatter(log_formatter_cherry)
772 logger.addHandler(str_handler)
773
774 if engine_config["global"].get("loglevel"):
775 logger_cherry.setLevel(engine_config["global"]["loglevel"])
776 logger_nbi.setLevel(engine_config["global"]["loglevel"])
777
778 # logging other modules
779 for k1, logname in {"message": "nbi.msg", "database": "nbi.db", "storage": "nbi.fs"}.items():
780 engine_config[k1]["logger_name"] = logname
781 logger_module = logging.getLogger(logname)
782 if "logfile" in engine_config[k1]:
783 file_handler = logging.handlers.RotatingFileHandler(engine_config[k1]["logfile"],
784 maxBytes=100e6, backupCount=9, delay=0)
785 file_handler.setFormatter(log_formatter_simple)
786 logger_module.addHandler(file_handler)
787 if "loglevel" in engine_config[k1]:
788 logger_module.setLevel(engine_config[k1]["loglevel"])
789 # TODO add more entries, e.g.: storage
790 cherrypy.tree.apps['/osm'].root.engine.start(engine_config)
791 try:
tierno4a946e42018-04-12 17:48:49 +0200792 cherrypy.tree.apps['/osm'].root.engine.init_db(target_version=database_version)
tiernoc94c3df2018-02-09 15:38:54 +0100793 except EngineException:
794 pass
795 # getenv('OSMOPENMANO_TENANT', None)
796
797
798def _stop_service():
799 """
800 Callback function called when cherrypy.engine stops
801 TODO: Ending database connections.
802 """
803 cherrypy.tree.apps['/osm'].root.engine.stop()
804 cherrypy.log.error("Stopping osm_nbi")
805
806def nbi():
807 # conf = {
808 # '/': {
809 # #'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
810 # 'tools.sessions.on': True,
811 # 'tools.response_headers.on': True,
812 # # 'tools.response_headers.headers': [('Content-Type', 'text/plain')],
813 # }
814 # }
815 # cherrypy.Server.ssl_module = 'builtin'
816 # cherrypy.Server.ssl_certificate = "http/cert.pem"
817 # cherrypy.Server.ssl_private_key = "http/privkey.pem"
818 # cherrypy.Server.thread_pool = 10
819 # cherrypy.config.update({'Server.socket_port': config["port"], 'Server.socket_host': config["host"]})
820
821 # cherrypy.config.update({'tools.auth_basic.on': True,
822 # 'tools.auth_basic.realm': 'localhost',
823 # 'tools.auth_basic.checkpassword': validate_password})
824 cherrypy.engine.subscribe('start', _start_service)
825 cherrypy.engine.subscribe('stop', _stop_service)
826 cherrypy.quickstart(Server(), '/osm', "nbi.cfg")
827
828
829if __name__ == '__main__':
830 nbi()