blob: 28fee6a217b61ac3febbd3980cc92f3a50447ede [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
tiernof5298be2018-05-16 14:43:57 +020010import logging.handlers
11import getopt
12import sys
Eduardo Sousa2f988212018-07-26 01:04:11 +010013
Eduardo Sousa819d34c2018-07-31 01:20:02 +010014from authconn import AuthException
Eduardo Sousa2f988212018-07-26 01:04:11 +010015from auth import Authenticator
tiernoc94c3df2018-02-09 15:38:54 +010016from engine import Engine, EngineException
tiernoa8d63632018-05-10 13:12:32 +020017from osm_common.dbbase import DbException
18from osm_common.fsbase import FsException
19from osm_common.msgbase import MsgException
tiernoc94c3df2018-02-09 15:38:54 +010020from http import HTTPStatus
tiernoc94c3df2018-02-09 15:38:54 +010021from codecs import getreader
tiernof5298be2018-05-16 14:43:57 +020022from os import environ, path
tiernoc94c3df2018-02-09 15:38:54 +010023
24__author__ = "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
tiernodfe09572018-04-24 10:41:10 +020025
26# TODO consider to remove and provide version using the static version file
27__version__ = "0.1.3"
tierno55945e72018-04-06 16:40:27 +020028version_date = "Apr 2018"
tierno4a946e42018-04-12 17:48:49 +020029database_version = '1.0'
Eduardo Sousa819d34c2018-07-31 01:20:02 +010030auth_database_version = '1.0'
tiernoc94c3df2018-02-09 15:38:54 +010031
32"""
tiernof27c79b2018-03-12 17:08:42 +010033North Bound Interface (O: OSM specific; 5,X: SOL005 not implemented yet; O5: SOL005 implemented)
tiernoc94c3df2018-02-09 15:38:54 +010034URL: /osm GET POST PUT DELETE PATCH
garciadeblas9750c5a2018-10-15 16:20:35 +020035 /nsd/v1
tierno2236d202018-05-16 19:05:16 +020036 /ns_descriptors_content O O
37 /<nsdInfoId> O O O O
tiernoc94c3df2018-02-09 15:38:54 +010038 /ns_descriptors O5 O5
39 /<nsdInfoId> O5 O5 5
40 /nsd_content O5 O5
tiernof27c79b2018-03-12 17:08:42 +010041 /nsd O
42 /artifacts[/<artifactPath>] O
tiernoc94c3df2018-02-09 15:38:54 +010043 /pnf_descriptors 5 5
44 /<pnfdInfoId> 5 5 5
45 /pnfd_content 5 5
tiernof27c79b2018-03-12 17:08:42 +010046 /subscriptions 5 5
47 /<subscriptionId> 5 X
tiernoc94c3df2018-02-09 15:38:54 +010048
49 /vnfpkgm/v1
tierno55945e72018-04-06 16:40:27 +020050 /vnf_packages_content O O
tierno2236d202018-05-16 19:05:16 +020051 /<vnfPkgId> O O
tiernoc94c3df2018-02-09 15:38:54 +010052 /vnf_packages O5 O5
53 /<vnfPkgId> O5 O5 5
tiernoc94c3df2018-02-09 15:38:54 +010054 /package_content O5 O5
55 /upload_from_uri X
tiernof27c79b2018-03-12 17:08:42 +010056 /vnfd O5
57 /artifacts[/<artifactPath>] O5
58 /subscriptions X X
59 /<subscriptionId> X X
tiernoc94c3df2018-02-09 15:38:54 +010060
61 /nslcm/v1
tiernof27c79b2018-03-12 17:08:42 +010062 /ns_instances_content O O
tierno2236d202018-05-16 19:05:16 +020063 /<nsInstanceId> O O
tiernof27c79b2018-03-12 17:08:42 +010064 /ns_instances 5 5
tierno95692442018-05-24 18:05:28 +020065 /<nsInstanceId> O5 O5
tierno65acb4d2018-04-06 16:42:40 +020066 instantiate O5
67 terminate O5
68 action O
69 scale O5
70 heal 5
tiernoc94c3df2018-02-09 15:38:54 +010071 /ns_lcm_op_occs 5 5
72 /<nsLcmOpOccId> 5 5 5
73 TO BE COMPLETED 5 5
tiernof759d822018-06-11 18:54:54 +020074 /vnf_instances (also vnfrs for compatibility) O
75 /<vnfInstanceId> O
tiernof27c79b2018-03-12 17:08:42 +010076 /subscriptions 5 5
77 /<subscriptionId> 5 X
garciadeblas9750c5a2018-10-15 16:20:35 +020078
tiernocb83c942018-09-24 17:28:13 +020079 /pdu/v1
80 /pdu_descriptor O O
81 /<id> O O O O
garciadeblas9750c5a2018-10-15 16:20:35 +020082
tiernof27c79b2018-03-12 17:08:42 +010083 /admin/v1
84 /tokens O O
tierno2236d202018-05-16 19:05:16 +020085 /<id> O O
tiernof27c79b2018-03-12 17:08:42 +010086 /users O O
tiernocd54a4a2018-09-12 16:40:35 +020087 /<id> O O O O
tiernof27c79b2018-03-12 17:08:42 +010088 /projects O O
tierno2236d202018-05-16 19:05:16 +020089 /<id> O O
tierno09c073e2018-04-26 13:36:48 +020090 /vims_accounts (also vims for compatibility) O O
tierno2236d202018-05-16 19:05:16 +020091 /<id> O O O
tierno0f98af52018-03-19 10:28:22 +010092 /sdns O O
tierno2236d202018-05-16 19:05:16 +020093 /<id> O O O
tiernoc94c3df2018-02-09 15:38:54 +010094
garciadeblas9750c5a2018-10-15 16:20:35 +020095 /nst/v1 O O
96 /netslice_templates_content O O
97 /<nstInfoId> O O O O
98 /netslice_templates O O
99 /<nstInfoId> O O O
100 /nst_content O O
101 /nst O
102 /artifacts[/<artifactPath>] O
103 /subscriptions X X
104 /<subscriptionId> X X
105
106 /nsilcm/v1
107 /netslice_instances_content O O
108 /<SliceInstanceId> O O
109 /netslice_instances O O
110 /<SliceInstanceId> O O
111 instantiate O
112 terminate O
113 action O
114 /nsi_lcm_op_occs O O
115 /<nsiLcmOpOccId> O O O
116 /subscriptions X X
117 /<subscriptionId> X X
118
tierno2236d202018-05-16 19:05:16 +0200119query string:
120 Follows SOL005 section 4.3.2 It contains extra METHOD to override http method, FORCE to force.
121 For filtering inside array, it must select the element of the array, or add ANYINDEX to apply the filtering over any
122 item of the array, that is, pass if any item of the array pass the filter.
123 It allows both ne and neq for not equal
124 TODO: 4.3.3 Attribute selectors
125 all_fields, fields=x,y,.., exclude_default, exclude_fields=x,y,...
tiernoc94c3df2018-02-09 15:38:54 +0100126 (none) … same as “exclude_default”
127 all_fields … all attributes.
tierno2236d202018-05-16 19:05:16 +0200128 fields=<list> … all attributes except all complex attributes with minimum cardinality of zero that are not
129 conditionally mandatory, and that are not provided in <list>.
130 exclude_fields=<list> … all attributes except those complex attributes with a minimum cardinality of zero that
131 are not conditionally mandatory, and that are provided in <list>.
132 exclude_default … all attributes except those complex attributes with a minimum cardinality of zero that are not
133 conditionally mandatory, and that are part of the "default exclude set" defined in the present specification for
134 the particular resource
135 exclude_default and include=<list> … all attributes except those complex attributes with a minimum cardinality
136 of zero that are not conditionally mandatory and that are part of the "default exclude set" defined in the
137 present specification for the particular resource, but that are not part of <list>
tiernoc94c3df2018-02-09 15:38:54 +0100138Header field name Reference Example Descriptions
139 Accept IETF RFC 7231 [19] application/json Content-Types that are acceptable for the response.
140 This header field shall be present if the response is expected to have a non-empty message body.
141 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the request.
142 This header field shall be present if the request has a non-empty message body.
tierno2236d202018-05-16 19:05:16 +0200143 Authorization IETF RFC 7235 [22] Bearer mF_9.B5f-4.1JqM The authorization token for the request.
144 Details are specified in clause 4.5.3.
tiernoc94c3df2018-02-09 15:38:54 +0100145 Range IETF RFC 7233 [21] 1000-2000 Requested range of bytes from a file
146Header field name Reference Example Descriptions
147 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the response.
148 This header field shall be present if the response has a non-empty message body.
tierno2236d202018-05-16 19:05:16 +0200149 Location IETF RFC 7231 [19] http://www.example.com/vnflcm/v1/vnf_instances/123 Used in redirection, or when a
150 new resource has been created.
tiernoc94c3df2018-02-09 15:38:54 +0100151 This header field shall be present if the response status code is 201 or 3xx.
tierno2236d202018-05-16 19:05:16 +0200152 In the present document this header field is also used if the response status code is 202 and a new resource was
153 created.
154 WWW-Authenticate IETF RFC 7235 [22] Bearer realm="example" Challenge if the corresponding HTTP request has not
155 provided authorization, or error details if the corresponding HTTP request has provided an invalid authorization
156 token.
157 Accept-Ranges IETF RFC 7233 [21] bytes Used by the Server to signal whether or not it supports ranges for
158 certain resources.
159 Content-Range IETF RFC 7233 [21] bytes 21010-47021/ 47022 Signals the byte range that is contained in the
160 response, and the total length of the file.
tiernoc94c3df2018-02-09 15:38:54 +0100161 Retry-After IETF RFC 7231 [19] Fri, 31 Dec 1999 23:59:59 GMT
tiernoc94c3df2018-02-09 15:38:54 +0100162"""
163
164
165class NbiException(Exception):
166
167 def __init__(self, message, http_code=HTTPStatus.METHOD_NOT_ALLOWED):
168 Exception.__init__(self, message)
169 self.http_code = http_code
170
171
172class Server(object):
173 instance = 0
174 # to decode bytes to str
175 reader = getreader("utf-8")
176
177 def __init__(self):
178 self.instance += 1
179 self.engine = Engine()
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100180 self.authenticator = Authenticator()
tiernof27c79b2018-03-12 17:08:42 +0100181 self.valid_methods = { # contains allowed URL and methods
182 "admin": {
183 "v1": {
tierno0f98af52018-03-19 10:28:22 +0100184 "tokens": {"METHODS": ("GET", "POST", "DELETE"),
tierno2236d202018-05-16 19:05:16 +0200185 "<ID>": {"METHODS": ("GET", "DELETE")}
186 },
tierno0f98af52018-03-19 10:28:22 +0100187 "users": {"METHODS": ("GET", "POST"),
tiernocd54a4a2018-09-12 16:40:35 +0200188 "<ID>": {"METHODS": ("GET", "POST", "DELETE", "PATCH", "PUT")}
tierno2236d202018-05-16 19:05:16 +0200189 },
tierno0f98af52018-03-19 10:28:22 +0100190 "projects": {"METHODS": ("GET", "POST"),
tierno2236d202018-05-16 19:05:16 +0200191 "<ID>": {"METHODS": ("GET", "DELETE")}
192 },
tierno0f98af52018-03-19 10:28:22 +0100193 "vims": {"METHODS": ("GET", "POST"),
tierno7ae10112018-05-18 14:36:02 +0200194 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
tierno2236d202018-05-16 19:05:16 +0200195 },
tierno09c073e2018-04-26 13:36:48 +0200196 "vim_accounts": {"METHODS": ("GET", "POST"),
tierno7ae10112018-05-18 14:36:02 +0200197 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
tierno2236d202018-05-16 19:05:16 +0200198 },
tierno0f98af52018-03-19 10:28:22 +0100199 "sdns": {"METHODS": ("GET", "POST"),
tierno7ae10112018-05-18 14:36:02 +0200200 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
tierno2236d202018-05-16 19:05:16 +0200201 },
tiernof27c79b2018-03-12 17:08:42 +0100202 }
203 },
tiernocb83c942018-09-24 17:28:13 +0200204 "pdu": {
205 "v1": {
206 "pdu_descriptors": {"METHODS": ("GET", "POST"),
207 "<ID>": {"METHODS": ("GET", "POST", "DELETE", "PATCH", "PUT")}
208 },
209 }
210 },
tiernof27c79b2018-03-12 17:08:42 +0100211 "nsd": {
212 "v1": {
tierno2236d202018-05-16 19:05:16 +0200213 "ns_descriptors_content": {"METHODS": ("GET", "POST"),
214 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
215 },
216 "ns_descriptors": {"METHODS": ("GET", "POST"),
217 "<ID>": {"METHODS": ("GET", "DELETE"), "TODO": "PATCH",
218 "nsd_content": {"METHODS": ("GET", "PUT")},
219 "nsd": {"METHODS": "GET"}, # descriptor inside package
220 "artifacts": {"*": {"METHODS": "GET"}}
221 }
222 },
tiernof27c79b2018-03-12 17:08:42 +0100223 "pnf_descriptors": {"TODO": ("GET", "POST"),
tierno2236d202018-05-16 19:05:16 +0200224 "<ID>": {"TODO": ("GET", "DELETE", "PATCH"),
225 "pnfd_content": {"TODO": ("GET", "PUT")}
226 }
227 },
tiernof27c79b2018-03-12 17:08:42 +0100228 "subscriptions": {"TODO": ("GET", "POST"),
tierno2236d202018-05-16 19:05:16 +0200229 "<ID>": {"TODO": ("GET", "DELETE")}
230 },
tiernof27c79b2018-03-12 17:08:42 +0100231 }
232 },
233 "vnfpkgm": {
234 "v1": {
tierno2236d202018-05-16 19:05:16 +0200235 "vnf_packages_content": {"METHODS": ("GET", "POST"),
236 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
237 },
238 "vnf_packages": {"METHODS": ("GET", "POST"),
tiernoaae4dc42018-06-11 18:55:50 +0200239 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"), # GET: vnfPkgInfo
tierno2236d202018-05-16 19:05:16 +0200240 "package_content": {"METHODS": ("GET", "PUT"), # package
241 "upload_from_uri": {"TODO": "POST"}
242 },
243 "vnfd": {"METHODS": "GET"}, # descriptor inside package
244 "artifacts": {"*": {"METHODS": "GET"}}
245 }
246 },
tiernof27c79b2018-03-12 17:08:42 +0100247 "subscriptions": {"TODO": ("GET", "POST"),
tierno2236d202018-05-16 19:05:16 +0200248 "<ID>": {"TODO": ("GET", "DELETE")}
249 },
tiernof27c79b2018-03-12 17:08:42 +0100250 }
251 },
252 "nslcm": {
253 "v1": {
254 "ns_instances_content": {"METHODS": ("GET", "POST"),
tierno2236d202018-05-16 19:05:16 +0200255 "<ID>": {"METHODS": ("GET", "DELETE")}
256 },
tierno65acb4d2018-04-06 16:42:40 +0200257 "ns_instances": {"METHODS": ("GET", "POST"),
tierno95692442018-05-24 18:05:28 +0200258 "<ID>": {"METHODS": ("GET", "DELETE"),
tiernof759d822018-06-11 18:54:54 +0200259 "scale": {"METHODS": "POST"},
tierno2236d202018-05-16 19:05:16 +0200260 "terminate": {"METHODS": "POST"},
261 "instantiate": {"METHODS": "POST"},
262 "action": {"METHODS": "POST"},
263 }
264 },
tierno65acb4d2018-04-06 16:42:40 +0200265 "ns_lcm_op_occs": {"METHODS": "GET",
tierno2236d202018-05-16 19:05:16 +0200266 "<ID>": {"METHODS": "GET"},
267 },
tierno0ffaa992018-05-09 13:21:56 +0200268 "vnfrs": {"METHODS": ("GET"),
tierno2236d202018-05-16 19:05:16 +0200269 "<ID>": {"METHODS": ("GET")}
270 },
tiernof759d822018-06-11 18:54:54 +0200271 "vnf_instances": {"METHODS": ("GET"),
272 "<ID>": {"METHODS": ("GET")}
273 },
tiernof27c79b2018-03-12 17:08:42 +0100274 }
275 },
garciadeblas9750c5a2018-10-15 16:20:35 +0200276 "nst": {
277 "v1": {
278 "netslice_templates_content": {"METHODS": ("GET", "POST"),
279 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
280 },
281 "netslice_templates": {"METHODS": ("GET", "POST"),
282 "<ID>": {"METHODS": ("GET", "DELETE"), "TODO": "PATCH",
283 "nst_content": {"METHODS": ("GET", "PUT")},
284 "nst": {"METHODS": "GET"}, # descriptor inside package
285 "artifacts": {"*": {"METHODS": "GET"}}
286 }
287 },
288 "subscriptions": {"TODO": ("GET", "POST"),
289 "<ID>": {"TODO": ("GET", "DELETE")}
290 },
291 }
292 },
293 "nsilcm": {
294 "v1": {
295 "netslice_instances_content": {"METHODS": ("GET", "POST"),
296 "<ID>": {"METHODS": ("GET", "DELETE")}
297 },
298 "netslice_instances": {"METHODS": ("GET", "POST"),
299 "<ID>": {"METHODS": ("GET", "DELETE"),
300 "terminate": {"METHODS": "POST"},
301 "instantiate": {"METHODS": "POST"},
302 "action": {"METHODS": "POST"},
303 }
304 },
305 "nsi_lcm_op_occs": {"METHODS": "GET",
306 "<ID>": {"METHODS": "GET"},
307 },
308 }
309 },
tiernof27c79b2018-03-12 17:08:42 +0100310 }
tiernoc94c3df2018-02-09 15:38:54 +0100311
tiernoc94c3df2018-02-09 15:38:54 +0100312 def _format_in(self, kwargs):
313 try:
314 indata = None
315 if cherrypy.request.body.length:
316 error_text = "Invalid input format "
317
318 if "Content-Type" in cherrypy.request.headers:
319 if "application/json" in cherrypy.request.headers["Content-Type"]:
320 error_text = "Invalid json format "
321 indata = json.load(self.reader(cherrypy.request.body))
322 elif "application/yaml" in cherrypy.request.headers["Content-Type"]:
323 error_text = "Invalid yaml format "
324 indata = yaml.load(cherrypy.request.body)
325 elif "application/binary" in cherrypy.request.headers["Content-Type"] or \
326 "application/gzip" in cherrypy.request.headers["Content-Type"] or \
tiernof27c79b2018-03-12 17:08:42 +0100327 "application/zip" in cherrypy.request.headers["Content-Type"] or \
328 "text/plain" in cherrypy.request.headers["Content-Type"]:
329 indata = cherrypy.request.body # .read()
tiernoc94c3df2018-02-09 15:38:54 +0100330 elif "multipart/form-data" in cherrypy.request.headers["Content-Type"]:
331 if "descriptor_file" in kwargs:
332 filecontent = kwargs.pop("descriptor_file")
333 if not filecontent.file:
334 raise NbiException("empty file or content", HTTPStatus.BAD_REQUEST)
tiernof27c79b2018-03-12 17:08:42 +0100335 indata = filecontent.file # .read()
tiernoc94c3df2018-02-09 15:38:54 +0100336 if filecontent.content_type.value:
337 cherrypy.request.headers["Content-Type"] = filecontent.content_type.value
338 else:
339 # raise cherrypy.HTTPError(HTTPStatus.Not_Acceptable,
340 # "Only 'Content-Type' of type 'application/json' or
341 # 'application/yaml' for input format are available")
342 error_text = "Invalid yaml format "
343 indata = yaml.load(cherrypy.request.body)
344 else:
345 error_text = "Invalid yaml format "
346 indata = yaml.load(cherrypy.request.body)
347 if not indata:
348 indata = {}
349
tiernoc94c3df2018-02-09 15:38:54 +0100350 format_yaml = False
351 if cherrypy.request.headers.get("Query-String-Format") == "yaml":
352 format_yaml = True
353
354 for k, v in kwargs.items():
355 if isinstance(v, str):
356 if v == "":
357 kwargs[k] = None
358 elif format_yaml:
359 try:
360 kwargs[k] = yaml.load(v)
tiernoe1281182018-05-22 12:24:36 +0200361 except Exception:
tiernoc94c3df2018-02-09 15:38:54 +0100362 pass
363 elif k.endswith(".gt") or k.endswith(".lt") or k.endswith(".gte") or k.endswith(".lte"):
364 try:
365 kwargs[k] = int(v)
tiernoe1281182018-05-22 12:24:36 +0200366 except Exception:
tiernoc94c3df2018-02-09 15:38:54 +0100367 try:
368 kwargs[k] = float(v)
tiernoe1281182018-05-22 12:24:36 +0200369 except Exception:
tiernoc94c3df2018-02-09 15:38:54 +0100370 pass
371 elif v.find(",") > 0:
372 kwargs[k] = v.split(",")
373 elif isinstance(v, (list, tuple)):
374 for index in range(0, len(v)):
375 if v[index] == "":
376 v[index] = None
377 elif format_yaml:
378 try:
379 v[index] = yaml.load(v[index])
tiernoe1281182018-05-22 12:24:36 +0200380 except Exception:
tiernoc94c3df2018-02-09 15:38:54 +0100381 pass
382
tiernof27c79b2018-03-12 17:08:42 +0100383 return indata
tiernoc94c3df2018-02-09 15:38:54 +0100384 except (ValueError, yaml.YAMLError) as exc:
385 raise NbiException(error_text + str(exc), HTTPStatus.BAD_REQUEST)
386 except KeyError as exc:
387 raise NbiException("Query string error: " + str(exc), HTTPStatus.BAD_REQUEST)
tiernob92094f2018-05-11 13:44:22 +0200388 except Exception as exc:
389 raise NbiException(error_text + str(exc), HTTPStatus.BAD_REQUEST)
tiernoc94c3df2018-02-09 15:38:54 +0100390
391 @staticmethod
tiernof27c79b2018-03-12 17:08:42 +0100392 def _format_out(data, session=None, _format=None):
tiernoc94c3df2018-02-09 15:38:54 +0100393 """
394 return string of dictionary data according to requested json, yaml, xml. By default json
tiernof27c79b2018-03-12 17:08:42 +0100395 :param data: response to be sent. Can be a dict, text or file
tiernoc94c3df2018-02-09 15:38:54 +0100396 :param session:
tiernof27c79b2018-03-12 17:08:42 +0100397 :param _format: The format to be set as Content-Type ir data is a file
tiernoc94c3df2018-02-09 15:38:54 +0100398 :return: None
399 """
tierno0f98af52018-03-19 10:28:22 +0100400 accept = cherrypy.request.headers.get("Accept")
tiernof27c79b2018-03-12 17:08:42 +0100401 if data is None:
tierno0f98af52018-03-19 10:28:22 +0100402 if accept and "text/html" in accept:
403 return html.format(data, cherrypy.request, cherrypy.response, session)
tierno09c073e2018-04-26 13:36:48 +0200404 # cherrypy.response.status = HTTPStatus.NO_CONTENT.value
tiernof27c79b2018-03-12 17:08:42 +0100405 return
406 elif hasattr(data, "read"): # file object
407 if _format:
408 cherrypy.response.headers["Content-Type"] = _format
409 elif "b" in data.mode: # binariy asssumig zip
410 cherrypy.response.headers["Content-Type"] = 'application/zip'
411 else:
412 cherrypy.response.headers["Content-Type"] = 'text/plain'
413 # TODO check that cherrypy close file. If not implement pending things to close per thread next
414 return data
tierno0f98af52018-03-19 10:28:22 +0100415 if accept:
tiernoc94c3df2018-02-09 15:38:54 +0100416 if "application/json" in accept:
417 cherrypy.response.headers["Content-Type"] = 'application/json; charset=utf-8'
418 a = json.dumps(data, indent=4) + "\n"
419 return a.encode("utf8")
420 elif "text/html" in accept:
421 return html.format(data, cherrypy.request, cherrypy.response, session)
422
tiernof27c79b2018-03-12 17:08:42 +0100423 elif "application/yaml" in accept or "*/*" in accept or "text/plain" in accept:
tiernoc94c3df2018-02-09 15:38:54 +0100424 pass
425 else:
426 raise cherrypy.HTTPError(HTTPStatus.NOT_ACCEPTABLE.value,
427 "Only 'Accept' of type 'application/json' or 'application/yaml' "
428 "for output format are available")
429 cherrypy.response.headers["Content-Type"] = 'application/yaml'
430 return yaml.safe_dump(data, explicit_start=True, indent=4, default_flow_style=False, tags=False,
431 encoding='utf-8', allow_unicode=True) # , canonical=True, default_style='"'
432
433 @cherrypy.expose
434 def index(self, *args, **kwargs):
435 session = None
436 try:
437 if cherrypy.request.method == "GET":
Eduardo Sousa2f988212018-07-26 01:04:11 +0100438 session = self.authenticator.authorize()
tiernoc94c3df2018-02-09 15:38:54 +0100439 outdata = "Index page"
440 else:
441 raise cherrypy.HTTPError(HTTPStatus.METHOD_NOT_ALLOWED.value,
tierno2236d202018-05-16 19:05:16 +0200442 "Method {} not allowed for tokens".format(cherrypy.request.method))
tiernoc94c3df2018-02-09 15:38:54 +0100443
444 return self._format_out(outdata, session)
445
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100446 except (EngineException, AuthException) as e:
tiernoc94c3df2018-02-09 15:38:54 +0100447 cherrypy.log("index Exception {}".format(e))
448 cherrypy.response.status = e.http_code.value
449 return self._format_out("Welcome to OSM!", session)
450
451 @cherrypy.expose
tierno55945e72018-04-06 16:40:27 +0200452 def version(self, *args, **kwargs):
tiernodfe09572018-04-24 10:41:10 +0200453 # TODO consider to remove and provide version using the static version file
tierno55945e72018-04-06 16:40:27 +0200454 global __version__, version_date
455 try:
456 if cherrypy.request.method != "GET":
457 raise NbiException("Only method GET is allowed", HTTPStatus.METHOD_NOT_ALLOWED)
458 elif args or kwargs:
459 raise NbiException("Invalid URL or query string for version", HTTPStatus.METHOD_NOT_ALLOWED)
460 return __version__ + " " + version_date
461 except NbiException as e:
462 cherrypy.response.status = e.http_code.value
463 problem_details = {
464 "code": e.http_code.name,
465 "status": e.http_code.value,
466 "detail": str(e),
467 }
468 return self._format_out(problem_details, None)
469
470 @cherrypy.expose
tiernof27c79b2018-03-12 17:08:42 +0100471 def token(self, method, token_id=None, kwargs=None):
tiernoc94c3df2018-02-09 15:38:54 +0100472 session = None
473 # self.engine.load_dbase(cherrypy.request.app.config)
tiernof27c79b2018-03-12 17:08:42 +0100474 indata = self._format_in(kwargs)
475 if not isinstance(indata, dict):
476 raise NbiException("Expected application/yaml or application/json Content-Type", HTTPStatus.BAD_REQUEST)
tiernoc94c3df2018-02-09 15:38:54 +0100477 try:
tiernoc94c3df2018-02-09 15:38:54 +0100478 if method == "GET":
Eduardo Sousa2f988212018-07-26 01:04:11 +0100479 session = self.authenticator.authorize()
tiernof27c79b2018-03-12 17:08:42 +0100480 if token_id:
Eduardo Sousa2f988212018-07-26 01:04:11 +0100481 outdata = self.authenticator.get_token(session, token_id)
tiernoc94c3df2018-02-09 15:38:54 +0100482 else:
Eduardo Sousa2f988212018-07-26 01:04:11 +0100483 outdata = self.authenticator.get_token_list(session)
tiernoc94c3df2018-02-09 15:38:54 +0100484 elif method == "POST":
485 try:
Eduardo Sousa2f988212018-07-26 01:04:11 +0100486 session = self.authenticator.authorize()
tiernoe1281182018-05-22 12:24:36 +0200487 except Exception:
tiernoc94c3df2018-02-09 15:38:54 +0100488 session = None
489 if kwargs:
490 indata.update(kwargs)
Eduardo Sousa2f988212018-07-26 01:04:11 +0100491 outdata = self.authenticator.new_token(session, indata, cherrypy.request.remote)
tiernoc94c3df2018-02-09 15:38:54 +0100492 session = outdata
493 cherrypy.session['Authorization'] = outdata["_id"]
tierno0f98af52018-03-19 10:28:22 +0100494 self._set_location_header("admin", "v1", "tokens", outdata["_id"])
tiernoc94c3df2018-02-09 15:38:54 +0100495 # cherrypy.response.cookie["Authorization"] = outdata["id"]
496 # cherrypy.response.cookie["Authorization"]['expires'] = 3600
497 elif method == "DELETE":
tiernof27c79b2018-03-12 17:08:42 +0100498 if not token_id and "id" in kwargs:
tiernoc94c3df2018-02-09 15:38:54 +0100499 token_id = kwargs["id"]
tiernof27c79b2018-03-12 17:08:42 +0100500 elif not token_id:
Eduardo Sousa2f988212018-07-26 01:04:11 +0100501 session = self.authenticator.authorize()
tiernoc94c3df2018-02-09 15:38:54 +0100502 token_id = session["_id"]
Eduardo Sousa2f988212018-07-26 01:04:11 +0100503 outdata = self.authenticator.del_token(token_id)
tiernoc94c3df2018-02-09 15:38:54 +0100504 session = None
505 cherrypy.session['Authorization'] = "logout"
506 # cherrypy.response.cookie["Authorization"] = token_id
507 # cherrypy.response.cookie["Authorization"]['expires'] = 0
508 else:
509 raise NbiException("Method {} not allowed for token".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
510 return self._format_out(outdata, session)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100511 except (NbiException, EngineException, DbException, AuthException) as e:
tiernoc94c3df2018-02-09 15:38:54 +0100512 cherrypy.log("tokens Exception {}".format(e))
513 cherrypy.response.status = e.http_code.value
514 problem_details = {
515 "code": e.http_code.name,
516 "status": e.http_code.value,
517 "detail": str(e),
518 }
519 return self._format_out(problem_details, session)
520
521 @cherrypy.expose
522 def test(self, *args, **kwargs):
523 thread_info = None
tiernof27c79b2018-03-12 17:08:42 +0100524 if args and args[0] == "help":
525 return "<html><pre>\ninit\nfile/<name> download file\ndb-clear/table\nprune\nlogin\nlogin2\n"\
tiernoe1281182018-05-22 12:24:36 +0200526 "sleep/<time>\nmessage/topic\n</pre></html>"
tiernof27c79b2018-03-12 17:08:42 +0100527
528 elif args and args[0] == "init":
tiernoc94c3df2018-02-09 15:38:54 +0100529 try:
530 # self.engine.load_dbase(cherrypy.request.app.config)
531 self.engine.create_admin()
532 return "Done. User 'admin', password 'admin' created"
533 except Exception:
534 cherrypy.response.status = HTTPStatus.FORBIDDEN.value
535 return self._format_out("Database already initialized")
tiernof27c79b2018-03-12 17:08:42 +0100536 elif args and args[0] == "file":
537 return cherrypy.lib.static.serve_file(cherrypy.tree.apps['/osm'].config["storage"]["path"] + "/" + args[1],
538 "text/plain", "attachment")
539 elif args and args[0] == "file2":
540 f_path = cherrypy.tree.apps['/osm'].config["storage"]["path"] + "/" + args[1]
541 f = open(f_path, "r")
542 cherrypy.response.headers["Content-type"] = "text/plain"
tiernof27c79b2018-03-12 17:08:42 +0100543 return f
tierno55945e72018-04-06 16:40:27 +0200544
tiernof27c79b2018-03-12 17:08:42 +0100545 elif len(args) == 2 and args[0] == "db-clear":
tiernob24258a2018-10-04 18:39:49 +0200546 return self.engine.db.del_list(args[1], kwargs)
tiernoc94c3df2018-02-09 15:38:54 +0100547 elif args and args[0] == "prune":
548 return self.engine.prune()
549 elif args and args[0] == "login":
550 if not cherrypy.request.headers.get("Authorization"):
551 cherrypy.response.headers["WWW-Authenticate"] = 'Basic realm="Access to OSM site", charset="UTF-8"'
552 cherrypy.response.status = HTTPStatus.UNAUTHORIZED.value
553 elif args and args[0] == "login2":
554 if not cherrypy.request.headers.get("Authorization"):
555 cherrypy.response.headers["WWW-Authenticate"] = 'Bearer realm="Access to OSM site"'
556 cherrypy.response.status = HTTPStatus.UNAUTHORIZED.value
557 elif args and args[0] == "sleep":
558 sleep_time = 5
559 try:
560 sleep_time = int(args[1])
561 except Exception:
562 cherrypy.response.status = HTTPStatus.FORBIDDEN.value
563 return self._format_out("Database already initialized")
564 thread_info = cherrypy.thread_data
565 print(thread_info)
566 time.sleep(sleep_time)
567 # thread_info
568 elif len(args) >= 2 and args[0] == "message":
tiernob24258a2018-10-04 18:39:49 +0200569 main_topic = args[1]
570 return_text = "<html><pre>{} ->\n".format(main_topic)
tiernoc94c3df2018-02-09 15:38:54 +0100571 try:
tierno55945e72018-04-06 16:40:27 +0200572 if cherrypy.request.method == 'POST':
573 to_send = yaml.load(cherrypy.request.body)
574 for k, v in to_send.items():
tiernob24258a2018-10-04 18:39:49 +0200575 self.engine.msg.write(main_topic, k, v)
tierno55945e72018-04-06 16:40:27 +0200576 return_text += " {}: {}\n".format(k, v)
577 elif cherrypy.request.method == 'GET':
578 for k, v in kwargs.items():
tiernob24258a2018-10-04 18:39:49 +0200579 self.engine.msg.write(main_topic, k, yaml.load(v))
tierno55945e72018-04-06 16:40:27 +0200580 return_text += " {}: {}\n".format(k, yaml.load(v))
tiernoc94c3df2018-02-09 15:38:54 +0100581 except Exception as e:
tierno55945e72018-04-06 16:40:27 +0200582 return_text += "Error: " + str(e)
583 return_text += "</pre></html>\n"
584 return return_text
tiernoc94c3df2018-02-09 15:38:54 +0100585
586 return_text = (
587 "<html><pre>\nheaders:\n args: {}\n".format(args) +
588 " kwargs: {}\n".format(kwargs) +
589 " headers: {}\n".format(cherrypy.request.headers) +
590 " path_info: {}\n".format(cherrypy.request.path_info) +
591 " query_string: {}\n".format(cherrypy.request.query_string) +
592 " session: {}\n".format(cherrypy.session) +
593 " cookie: {}\n".format(cherrypy.request.cookie) +
594 " method: {}\n".format(cherrypy.request.method) +
595 " session: {}\n".format(cherrypy.session.get('fieldname')) +
596 " body:\n")
597 return_text += " length: {}\n".format(cherrypy.request.body.length)
598 if cherrypy.request.body.length:
599 return_text += " content: {}\n".format(
600 str(cherrypy.request.body.read(int(cherrypy.request.headers.get('Content-Length', 0)))))
601 if thread_info:
602 return_text += "thread: {}\n".format(thread_info)
603 return_text += "</pre></html>"
604 return return_text
605
tiernof27c79b2018-03-12 17:08:42 +0100606 def _check_valid_url_method(self, method, *args):
607 if len(args) < 3:
tiernob24258a2018-10-04 18:39:49 +0200608 raise NbiException("URL must contain at least 'main_topic/version/topic'", HTTPStatus.METHOD_NOT_ALLOWED)
tiernof27c79b2018-03-12 17:08:42 +0100609
610 reference = self.valid_methods
611 for arg in args:
612 if arg is None:
613 break
614 if not isinstance(reference, dict):
615 raise NbiException("URL contains unexpected extra items '{}'".format(arg),
616 HTTPStatus.METHOD_NOT_ALLOWED)
617
618 if arg in reference:
619 reference = reference[arg]
620 elif "<ID>" in reference:
621 reference = reference["<ID>"]
622 elif "*" in reference:
623 reference = reference["*"]
624 break
625 else:
626 raise NbiException("Unexpected URL item {}".format(arg), HTTPStatus.METHOD_NOT_ALLOWED)
627 if "TODO" in reference and method in reference["TODO"]:
628 raise NbiException("Method {} not supported yet for this URL".format(method), HTTPStatus.NOT_IMPLEMENTED)
tierno2236d202018-05-16 19:05:16 +0200629 elif "METHODS" in reference and method not in reference["METHODS"]:
tiernof27c79b2018-03-12 17:08:42 +0100630 raise NbiException("Method {} not supported for this URL".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
631 return
632
633 @staticmethod
tiernob24258a2018-10-04 18:39:49 +0200634 def _set_location_header(main_topic, version, topic, id):
tiernof27c79b2018-03-12 17:08:42 +0100635 """
636 Insert response header Location with the URL of created item base on URL params
tiernob24258a2018-10-04 18:39:49 +0200637 :param main_topic:
tiernof27c79b2018-03-12 17:08:42 +0100638 :param version:
tiernob24258a2018-10-04 18:39:49 +0200639 :param topic:
tiernof27c79b2018-03-12 17:08:42 +0100640 :param id:
641 :return: None
642 """
643 # Use cherrypy.request.base for absoluted path and make use of request.header HOST just in case behind aNAT
tiernob24258a2018-10-04 18:39:49 +0200644 cherrypy.response.headers["Location"] = "/osm/{}/{}/{}/{}".format(main_topic, version, topic, id)
tiernof27c79b2018-03-12 17:08:42 +0100645 return
646
tiernoc94c3df2018-02-09 15:38:54 +0100647 @cherrypy.expose
tiernob24258a2018-10-04 18:39:49 +0200648 def default(self, main_topic=None, version=None, topic=None, _id=None, item=None, *args, **kwargs):
tiernoc94c3df2018-02-09 15:38:54 +0100649 session = None
tiernof27c79b2018-03-12 17:08:42 +0100650 outdata = None
651 _format = None
tierno0f98af52018-03-19 10:28:22 +0100652 method = "DONE"
tiernob24258a2018-10-04 18:39:49 +0200653 engine_topic = None
tiernodb9dc582018-06-20 17:27:29 +0200654 rollback = []
655 session = None
tiernoc94c3df2018-02-09 15:38:54 +0100656 try:
tiernob24258a2018-10-04 18:39:49 +0200657 if not main_topic or not version or not topic:
658 raise NbiException("URL must contain at least 'main_topic/version/topic'",
659 HTTPStatus.METHOD_NOT_ALLOWED)
660 if main_topic not in ("admin", "vnfpkgm", "nsd", "nslcm"):
661 raise NbiException("URL main_topic '{}' not supported".format(main_topic),
662 HTTPStatus.METHOD_NOT_ALLOWED)
tiernoc94c3df2018-02-09 15:38:54 +0100663 if version != 'v1':
664 raise NbiException("URL version '{}' not supported".format(version), HTTPStatus.METHOD_NOT_ALLOWED)
665
tiernof27c79b2018-03-12 17:08:42 +0100666 if kwargs and "METHOD" in kwargs and kwargs["METHOD"] in ("PUT", "POST", "DELETE", "GET", "PATCH"):
667 method = kwargs.pop("METHOD")
668 else:
669 method = cherrypy.request.method
tiernob92094f2018-05-11 13:44:22 +0200670 if kwargs and "FORCE" in kwargs:
671 force = kwargs.pop("FORCE")
672 else:
673 force = False
tiernof27c79b2018-03-12 17:08:42 +0100674
tiernob24258a2018-10-04 18:39:49 +0200675 self._check_valid_url_method(method, main_topic, version, topic, _id, item, *args)
tiernof27c79b2018-03-12 17:08:42 +0100676
tiernob24258a2018-10-04 18:39:49 +0200677 if main_topic == "admin" and topic == "tokens":
tiernof27c79b2018-03-12 17:08:42 +0100678 return self.token(method, _id, kwargs)
679
tiernoc94c3df2018-02-09 15:38:54 +0100680 # self.engine.load_dbase(cherrypy.request.app.config)
Eduardo Sousa2f988212018-07-26 01:04:11 +0100681 session = self.authenticator.authorize()
tiernof27c79b2018-03-12 17:08:42 +0100682 indata = self._format_in(kwargs)
tiernob24258a2018-10-04 18:39:49 +0200683 engine_topic = topic
684 if topic == "subscriptions":
685 engine_topic = main_topic + "_" + topic
686 if item:
687 engine_topic = item
tiernoc94c3df2018-02-09 15:38:54 +0100688
tiernob24258a2018-10-04 18:39:49 +0200689 if main_topic == "nsd":
690 engine_topic = "nsds"
691 elif main_topic == "vnfpkgm":
692 engine_topic = "vnfds"
693 elif main_topic == "nslcm":
694 engine_topic = "nsrs"
695 if topic == "ns_lcm_op_occs":
696 engine_topic = "nslcmops"
697 if topic == "vnfrs" or topic == "vnf_instances":
698 engine_topic = "vnfrs"
garciadeblas9750c5a2018-10-15 16:20:35 +0200699 elif main_topic == "nst":
700 engine_topic = "nsts"
701 elif main_topic == "nsilcm":
702 engine_topic = "nsis"
703 if topic == "nsi_lcm_op_occs":
704 engine_topic = "nsilcmops"
tiernob24258a2018-10-04 18:39:49 +0200705 elif main_topic == "pdu":
706 engine_topic = "pdus"
707 if engine_topic == "vims": # TODO this is for backward compatibility, it will remove in the future
708 engine_topic = "vim_accounts"
tiernoc94c3df2018-02-09 15:38:54 +0100709
710 if method == "GET":
garciadeblas9750c5a2018-10-15 16:20:35 +0200711 if item in ("nsd_content", "package_content", "artifacts", "vnfd", "nsd", "nst", "nst_content"):
712 if item in ("vnfd", "nsd", "nst"):
tiernof27c79b2018-03-12 17:08:42 +0100713 path = "$DESCRIPTOR"
714 elif args:
715 path = args
tiernob24258a2018-10-04 18:39:49 +0200716 elif item == "artifacts":
tiernof27c79b2018-03-12 17:08:42 +0100717 path = ()
718 else:
719 path = None
tiernob24258a2018-10-04 18:39:49 +0200720 file, _format = self.engine.get_file(session, engine_topic, _id, path,
tierno2236d202018-05-16 19:05:16 +0200721 cherrypy.request.headers.get("Accept"))
tiernof27c79b2018-03-12 17:08:42 +0100722 outdata = file
723 elif not _id:
tiernob24258a2018-10-04 18:39:49 +0200724 outdata = self.engine.get_item_list(session, engine_topic, kwargs)
tiernoc94c3df2018-02-09 15:38:54 +0100725 else:
tiernob24258a2018-10-04 18:39:49 +0200726 outdata = self.engine.get_item(session, engine_topic, _id)
tiernof27c79b2018-03-12 17:08:42 +0100727 elif method == "POST":
garciadeblas9750c5a2018-10-15 16:20:35 +0200728 if topic in ("ns_descriptors_content", "vnf_packages_content", "netslice_templates_content"):
tiernof27c79b2018-03-12 17:08:42 +0100729 _id = cherrypy.request.headers.get("Transaction-Id")
730 if not _id:
tiernob24258a2018-10-04 18:39:49 +0200731 _id = self.engine.new_item(rollback, session, engine_topic, {}, None, cherrypy.request.headers,
tiernob92094f2018-05-11 13:44:22 +0200732 force=force)
tiernob24258a2018-10-04 18:39:49 +0200733 completed = self.engine.upload_content(session, engine_topic, _id, indata, kwargs,
734 cherrypy.request.headers, force=force)
tiernof27c79b2018-03-12 17:08:42 +0100735 if completed:
tiernob24258a2018-10-04 18:39:49 +0200736 self._set_location_header(main_topic, version, topic, _id)
tiernof27c79b2018-03-12 17:08:42 +0100737 else:
738 cherrypy.response.headers["Transaction-Id"] = _id
739 outdata = {"id": _id}
tiernob24258a2018-10-04 18:39:49 +0200740 elif topic == "ns_instances_content":
741 # creates NSR
742 _id = self.engine.new_item(rollback, session, engine_topic, indata, kwargs, force=force)
743 # creates nslcmop
744 indata["lcmOperationType"] = "instantiate"
745 indata["nsInstanceId"] = _id
746 self.engine.new_item(rollback, session, "nslcmops", indata, None)
747 self._set_location_header(main_topic, version, topic, _id)
tiernof27c79b2018-03-12 17:08:42 +0100748 outdata = {"id": _id}
tiernob24258a2018-10-04 18:39:49 +0200749 elif topic == "ns_instances" and item:
750 indata["lcmOperationType"] = item
751 indata["nsInstanceId"] = _id
752 _id = self.engine.new_item(rollback, session, "nslcmops", indata, kwargs)
753 self._set_location_header(main_topic, version, "ns_lcm_op_occs", _id)
tierno65acb4d2018-04-06 16:42:40 +0200754 outdata = {"id": _id}
755 cherrypy.response.status = HTTPStatus.ACCEPTED.value
garciadeblas9750c5a2018-10-15 16:20:35 +0200756 elif topic == "netslice_instances_content":
757 # creates NSI
758 _id = self.engine.new_item(rollback, session, engine_topic, indata, kwargs, force=force)
759 # creates nsilcmop
760 indata["lcmOperationType"] = "instantiate"
761 indata["nsiInstanceId"] = _id
762 self.engine.new_item(rollback, session, "nsilcmops", indata, None)
763 self._set_location_header(main_topic, version, topic, _id)
764 outdata = {"id": _id}
765 elif topic == "netslice_instances" and item:
766 indata["lcmOperationType"] = item
767 indata["nsiInstanceId"] = _id
768 _id = self.engine.new_item(rollback, session, "nsilcmops", indata, kwargs)
769 self._set_location_header(main_topic, version, "nsi_lcm_op_occs", _id)
770 outdata = {"id": _id}
771 cherrypy.response.status = HTTPStatus.ACCEPTED.value
tiernof27c79b2018-03-12 17:08:42 +0100772 else:
tiernob24258a2018-10-04 18:39:49 +0200773 _id = self.engine.new_item(rollback, session, engine_topic, indata, kwargs,
774 cherrypy.request.headers, force=force)
775 self._set_location_header(main_topic, version, topic, _id)
tiernof27c79b2018-03-12 17:08:42 +0100776 outdata = {"id": _id}
tiernob24258a2018-10-04 18:39:49 +0200777 # TODO form NsdInfo when topic in ("ns_descriptors", "vnf_packages")
tiernof27c79b2018-03-12 17:08:42 +0100778 cherrypy.response.status = HTTPStatus.CREATED.value
tierno09c073e2018-04-26 13:36:48 +0200779
tiernoc94c3df2018-02-09 15:38:54 +0100780 elif method == "DELETE":
781 if not _id:
tiernob24258a2018-10-04 18:39:49 +0200782 outdata = self.engine.del_item_list(session, engine_topic, kwargs)
tierno09c073e2018-04-26 13:36:48 +0200783 cherrypy.response.status = HTTPStatus.OK.value
tiernoc94c3df2018-02-09 15:38:54 +0100784 else: # len(args) > 1
tiernob24258a2018-10-04 18:39:49 +0200785 if topic == "ns_instances_content" and not force:
786 nslcmop_desc = {
787 "lcmOperationType": "terminate",
788 "nsInstanceId": _id,
789 "autoremove": True
790 }
791 opp_id = self.engine.new_item(rollback, session, "nslcmops", nslcmop_desc, None)
tierno3ace63c2018-05-03 17:51:43 +0200792 outdata = {"_id": opp_id}
tierno09c073e2018-04-26 13:36:48 +0200793 cherrypy.response.status = HTTPStatus.ACCEPTED.value
garciadeblas9750c5a2018-10-15 16:20:35 +0200794 elif topic == "netslice_instances_content" and not force:
795 nsilcmop_desc = {
796 "lcmOperationType": "terminate",
797 "nsiInstanceId": _id,
798 "autoremove": True
799 }
800 opp_id = self.engine.new_item(rollback, session, "nsilcmops", nsilcmop_desc, None)
801 outdata = {"_id": opp_id}
802 cherrypy.response.status = HTTPStatus.ACCEPTED.value
tierno65acb4d2018-04-06 16:42:40 +0200803 else:
tiernob24258a2018-10-04 18:39:49 +0200804 self.engine.del_item(session, engine_topic, _id, force)
tierno09c073e2018-04-26 13:36:48 +0200805 cherrypy.response.status = HTTPStatus.NO_CONTENT.value
tiernob24258a2018-10-04 18:39:49 +0200806 if engine_topic in ("vim_accounts", "sdns"):
tierno09c073e2018-04-26 13:36:48 +0200807 cherrypy.response.status = HTTPStatus.ACCEPTED.value
808
tierno7ae10112018-05-18 14:36:02 +0200809 elif method in ("PUT", "PATCH"):
tiernocd54a4a2018-09-12 16:40:35 +0200810 outdata = None
tiernof27c79b2018-03-12 17:08:42 +0100811 if not indata and not kwargs:
tiernoc94c3df2018-02-09 15:38:54 +0100812 raise NbiException("Nothing to update. Provide payload and/or query string",
813 HTTPStatus.BAD_REQUEST)
garciadeblas9750c5a2018-10-15 16:20:35 +0200814 if item in ("nsd_content", "package_content", "nst_content") and method == "PUT":
tiernob24258a2018-10-04 18:39:49 +0200815 completed = self.engine.upload_content(session, engine_topic, _id, indata, kwargs,
816 cherrypy.request.headers, force=force)
tiernof27c79b2018-03-12 17:08:42 +0100817 if not completed:
818 cherrypy.response.headers["Transaction-Id"] = id
tiernof27c79b2018-03-12 17:08:42 +0100819 else:
tiernob24258a2018-10-04 18:39:49 +0200820 self.engine.edit_item(session, engine_topic, _id, indata, kwargs, force=force)
tiernocd54a4a2018-09-12 16:40:35 +0200821 cherrypy.response.status = HTTPStatus.NO_CONTENT.value
tiernoc94c3df2018-02-09 15:38:54 +0100822 else:
823 raise NbiException("Method {} not allowed".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
tiernof27c79b2018-03-12 17:08:42 +0100824 return self._format_out(outdata, session, _format)
tiernob24258a2018-10-04 18:39:49 +0200825 except Exception as e:
826 if isinstance(e, (NbiException, EngineException, DbException, FsException, MsgException, AuthException)):
827 http_code_value = cherrypy.response.status = e.http_code.value
828 http_code_name = e.http_code.name
829 cherrypy.log("Exception {}".format(e))
830 else:
831 http_code_value = cherrypy.response.status = HTTPStatus.BAD_REQUEST.value # INTERNAL_SERVER_ERROR
832 cherrypy.log("CRITICAL: Exception {}".format(e))
833 http_code_name = HTTPStatus.BAD_REQUEST.name
tierno3ace63c2018-05-03 17:51:43 +0200834 if hasattr(outdata, "close"): # is an open file
835 outdata.close()
tiernob24258a2018-10-04 18:39:49 +0200836 error_text = str(e)
837 rollback.reverse()
tiernodb9dc582018-06-20 17:27:29 +0200838 for rollback_item in rollback:
tierno3ace63c2018-05-03 17:51:43 +0200839 try:
tiernodb9dc582018-06-20 17:27:29 +0200840 self.engine.del_item(**rollback_item, session=session, force=True)
tierno3ace63c2018-05-03 17:51:43 +0200841 except Exception as e2:
tiernob24258a2018-10-04 18:39:49 +0200842 rollback_error_text = "Rollback Exception {}: {}".format(rollback_item, e2)
843 cherrypy.log(rollback_error_text)
844 error_text += ". " + rollback_error_text
845 # if isinstance(e, MsgException):
846 # error_text = "{} has been '{}' but other modules cannot be informed because an error on bus".format(
847 # engine_topic[:-1], method, error_text)
tiernoc94c3df2018-02-09 15:38:54 +0100848 problem_details = {
tiernob24258a2018-10-04 18:39:49 +0200849 "code": http_code_name,
850 "status": http_code_value,
851 "detail": error_text,
tiernoc94c3df2018-02-09 15:38:54 +0100852 }
853 return self._format_out(problem_details, session)
854 # raise cherrypy.HTTPError(e.http_code.value, str(e))
855
856
857# def validate_password(realm, username, password):
858# cherrypy.log("realm "+ str(realm))
859# if username == "admin" and password == "admin":
860# return True
861# return False
862
863
864def _start_service():
865 """
866 Callback function called when cherrypy.engine starts
867 Override configuration with env variables
868 Set database, storage, message configuration
869 Init database with admin/admin user password
870 """
871 cherrypy.log.error("Starting osm_nbi")
872 # update general cherrypy configuration
873 update_dict = {}
874
875 engine_config = cherrypy.tree.apps['/osm'].config
876 for k, v in environ.items():
877 if not k.startswith("OSMNBI_"):
878 continue
tiernoe1281182018-05-22 12:24:36 +0200879 k1, _, k2 = k[7:].lower().partition("_")
tiernoc94c3df2018-02-09 15:38:54 +0100880 if not k2:
881 continue
882 try:
883 # update static configuration
884 if k == 'OSMNBI_STATIC_DIR':
885 engine_config["/static"]['tools.staticdir.dir'] = v
886 engine_config["/static"]['tools.staticdir.on'] = True
887 elif k == 'OSMNBI_SOCKET_PORT' or k == 'OSMNBI_SERVER_PORT':
888 update_dict['server.socket_port'] = int(v)
889 elif k == 'OSMNBI_SOCKET_HOST' or k == 'OSMNBI_SERVER_HOST':
890 update_dict['server.socket_host'] = v
tiernof5298be2018-05-16 14:43:57 +0200891 elif k1 in ("server", "test", "auth", "log"):
892 update_dict[k1 + '.' + k2] = v
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100893 elif k1 in ("message", "database", "storage", "authentication"):
tiernof5298be2018-05-16 14:43:57 +0200894 # k2 = k2.replace('_', '.')
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100895 if k2 in ("port", "db_port"):
tiernoc94c3df2018-02-09 15:38:54 +0100896 engine_config[k1][k2] = int(v)
897 else:
898 engine_config[k1][k2] = v
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100899
tiernoc94c3df2018-02-09 15:38:54 +0100900 except ValueError as e:
901 cherrypy.log.error("Ignoring environ '{}': " + str(e))
902 except Exception as e:
903 cherrypy.log.warn("skipping environ '{}' on exception '{}'".format(k, e))
904
905 if update_dict:
906 cherrypy.config.update(update_dict)
tiernof5298be2018-05-16 14:43:57 +0200907 engine_config["global"].update(update_dict)
tiernoc94c3df2018-02-09 15:38:54 +0100908
909 # logging cherrypy
910 log_format_simple = "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(message)s"
911 log_formatter_simple = logging.Formatter(log_format_simple, datefmt='%Y-%m-%dT%H:%M:%S')
912 logger_server = logging.getLogger("cherrypy.error")
913 logger_access = logging.getLogger("cherrypy.access")
914 logger_cherry = logging.getLogger("cherrypy")
915 logger_nbi = logging.getLogger("nbi")
916
tiernof5298be2018-05-16 14:43:57 +0200917 if "log.file" in engine_config["global"]:
918 file_handler = logging.handlers.RotatingFileHandler(engine_config["global"]["log.file"],
tiernoc94c3df2018-02-09 15:38:54 +0100919 maxBytes=100e6, backupCount=9, delay=0)
920 file_handler.setFormatter(log_formatter_simple)
921 logger_cherry.addHandler(file_handler)
922 logger_nbi.addHandler(file_handler)
tiernob24258a2018-10-04 18:39:49 +0200923 # log always to standard output
924 for format_, logger in {"nbi.server %(filename)s:%(lineno)s": logger_server,
925 "nbi.access %(filename)s:%(lineno)s": logger_access,
926 "%(name)s %(filename)s:%(lineno)s": logger_nbi
927 }.items():
928 log_format_cherry = "%(asctime)s %(levelname)s {} %(message)s".format(format_)
929 log_formatter_cherry = logging.Formatter(log_format_cherry, datefmt='%Y-%m-%dT%H:%M:%S')
930 str_handler = logging.StreamHandler()
931 str_handler.setFormatter(log_formatter_cherry)
932 logger.addHandler(str_handler)
tiernoc94c3df2018-02-09 15:38:54 +0100933
tiernof5298be2018-05-16 14:43:57 +0200934 if engine_config["global"].get("log.level"):
935 logger_cherry.setLevel(engine_config["global"]["log.level"])
936 logger_nbi.setLevel(engine_config["global"]["log.level"])
tiernoc94c3df2018-02-09 15:38:54 +0100937
938 # logging other modules
939 for k1, logname in {"message": "nbi.msg", "database": "nbi.db", "storage": "nbi.fs"}.items():
940 engine_config[k1]["logger_name"] = logname
941 logger_module = logging.getLogger(logname)
942 if "logfile" in engine_config[k1]:
943 file_handler = logging.handlers.RotatingFileHandler(engine_config[k1]["logfile"],
tierno2236d202018-05-16 19:05:16 +0200944 maxBytes=100e6, backupCount=9, delay=0)
tiernoc94c3df2018-02-09 15:38:54 +0100945 file_handler.setFormatter(log_formatter_simple)
946 logger_module.addHandler(file_handler)
947 if "loglevel" in engine_config[k1]:
948 logger_module.setLevel(engine_config[k1]["loglevel"])
949 # TODO add more entries, e.g.: storage
950 cherrypy.tree.apps['/osm'].root.engine.start(engine_config)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100951 cherrypy.tree.apps['/osm'].root.authenticator.start(engine_config)
tiernoc94c3df2018-02-09 15:38:54 +0100952 try:
tierno4a946e42018-04-12 17:48:49 +0200953 cherrypy.tree.apps['/osm'].root.engine.init_db(target_version=database_version)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100954 cherrypy.tree.apps['/osm'].root.authenticator.init_db(target_version=auth_database_version)
955 except (EngineException, AuthException):
tiernoc94c3df2018-02-09 15:38:54 +0100956 pass
957 # getenv('OSMOPENMANO_TENANT', None)
958
959
960def _stop_service():
961 """
962 Callback function called when cherrypy.engine stops
963 TODO: Ending database connections.
964 """
965 cherrypy.tree.apps['/osm'].root.engine.stop()
966 cherrypy.log.error("Stopping osm_nbi")
967
tierno2236d202018-05-16 19:05:16 +0200968
tiernof5298be2018-05-16 14:43:57 +0200969def nbi(config_file):
tiernoc94c3df2018-02-09 15:38:54 +0100970 # conf = {
971 # '/': {
972 # #'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
973 # 'tools.sessions.on': True,
974 # 'tools.response_headers.on': True,
975 # # 'tools.response_headers.headers': [('Content-Type', 'text/plain')],
976 # }
977 # }
978 # cherrypy.Server.ssl_module = 'builtin'
979 # cherrypy.Server.ssl_certificate = "http/cert.pem"
980 # cherrypy.Server.ssl_private_key = "http/privkey.pem"
981 # cherrypy.Server.thread_pool = 10
982 # cherrypy.config.update({'Server.socket_port': config["port"], 'Server.socket_host': config["host"]})
983
984 # cherrypy.config.update({'tools.auth_basic.on': True,
985 # 'tools.auth_basic.realm': 'localhost',
986 # 'tools.auth_basic.checkpassword': validate_password})
987 cherrypy.engine.subscribe('start', _start_service)
988 cherrypy.engine.subscribe('stop', _stop_service)
tiernof5298be2018-05-16 14:43:57 +0200989 cherrypy.quickstart(Server(), '/osm', config_file)
990
991
992def usage():
993 print("""Usage: {} [options]
994 -c|--config [configuration_file]: loads the configuration file (default: ./nbi.cfg)
995 -h|--help: shows this help
996 """.format(sys.argv[0]))
tierno2236d202018-05-16 19:05:16 +0200997 # --log-socket-host HOST: send logs to this host")
998 # --log-socket-port PORT: send logs using this port (default: 9022)")
tiernoc94c3df2018-02-09 15:38:54 +0100999
1000
1001if __name__ == '__main__':
tiernof5298be2018-05-16 14:43:57 +02001002 try:
1003 # load parameters and configuration
1004 opts, args = getopt.getopt(sys.argv[1:], "hvc:", ["config=", "help"])
1005 # TODO add "log-socket-host=", "log-socket-port=", "log-file="
1006 config_file = None
1007 for o, a in opts:
1008 if o in ("-h", "--help"):
1009 usage()
1010 sys.exit()
1011 elif o in ("-c", "--config"):
1012 config_file = a
1013 # elif o == "--log-socket-port":
1014 # log_socket_port = a
1015 # elif o == "--log-socket-host":
1016 # log_socket_host = a
1017 # elif o == "--log-file":
1018 # log_file = a
1019 else:
1020 assert False, "Unhandled option"
1021 if config_file:
1022 if not path.isfile(config_file):
1023 print("configuration file '{}' that not exist".format(config_file), file=sys.stderr)
1024 exit(1)
1025 else:
1026 for config_file in (__file__[:__file__.rfind(".")] + ".cfg", "./nbi.cfg", "/etc/osm/nbi.cfg"):
1027 if path.isfile(config_file):
1028 break
1029 else:
1030 print("No configuration file 'nbi.cfg' found neither at local folder nor at /etc/osm/", file=sys.stderr)
1031 exit(1)
1032 nbi(config_file)
1033 except getopt.GetoptError as e:
1034 print(str(e), file=sys.stderr)
1035 # usage()
1036 exit(1)