blob: 1354f2cae797c6e7251703841285dc94e8cee4e7 [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))
gcalvinode4adfe2018-10-30 11:46:09 +0100322 cherrypy.request.headers.pop("Content-File-MD5", None)
tiernoc94c3df2018-02-09 15:38:54 +0100323 elif "application/yaml" in cherrypy.request.headers["Content-Type"]:
324 error_text = "Invalid yaml format "
325 indata = yaml.load(cherrypy.request.body)
gcalvinode4adfe2018-10-30 11:46:09 +0100326 cherrypy.request.headers.pop("Content-File-MD5", None)
tiernoc94c3df2018-02-09 15:38:54 +0100327 elif "application/binary" in cherrypy.request.headers["Content-Type"] or \
328 "application/gzip" in cherrypy.request.headers["Content-Type"] or \
tiernof27c79b2018-03-12 17:08:42 +0100329 "application/zip" in cherrypy.request.headers["Content-Type"] or \
330 "text/plain" in cherrypy.request.headers["Content-Type"]:
331 indata = cherrypy.request.body # .read()
tiernoc94c3df2018-02-09 15:38:54 +0100332 elif "multipart/form-data" in cherrypy.request.headers["Content-Type"]:
333 if "descriptor_file" in kwargs:
334 filecontent = kwargs.pop("descriptor_file")
335 if not filecontent.file:
336 raise NbiException("empty file or content", HTTPStatus.BAD_REQUEST)
tiernof27c79b2018-03-12 17:08:42 +0100337 indata = filecontent.file # .read()
tiernoc94c3df2018-02-09 15:38:54 +0100338 if filecontent.content_type.value:
339 cherrypy.request.headers["Content-Type"] = filecontent.content_type.value
340 else:
341 # raise cherrypy.HTTPError(HTTPStatus.Not_Acceptable,
342 # "Only 'Content-Type' of type 'application/json' or
343 # 'application/yaml' for input format are available")
344 error_text = "Invalid yaml format "
345 indata = yaml.load(cherrypy.request.body)
gcalvinode4adfe2018-10-30 11:46:09 +0100346 cherrypy.request.headers.pop("Content-File-MD5", None)
tiernoc94c3df2018-02-09 15:38:54 +0100347 else:
348 error_text = "Invalid yaml format "
349 indata = yaml.load(cherrypy.request.body)
gcalvinode4adfe2018-10-30 11:46:09 +0100350 cherrypy.request.headers.pop("Content-File-MD5", None)
tiernoc94c3df2018-02-09 15:38:54 +0100351 if not indata:
352 indata = {}
353
tiernoc94c3df2018-02-09 15:38:54 +0100354 format_yaml = False
355 if cherrypy.request.headers.get("Query-String-Format") == "yaml":
356 format_yaml = True
357
358 for k, v in kwargs.items():
359 if isinstance(v, str):
360 if v == "":
361 kwargs[k] = None
362 elif format_yaml:
363 try:
364 kwargs[k] = yaml.load(v)
tiernoe1281182018-05-22 12:24:36 +0200365 except Exception:
tiernoc94c3df2018-02-09 15:38:54 +0100366 pass
367 elif k.endswith(".gt") or k.endswith(".lt") or k.endswith(".gte") or k.endswith(".lte"):
368 try:
369 kwargs[k] = int(v)
tiernoe1281182018-05-22 12:24:36 +0200370 except Exception:
tiernoc94c3df2018-02-09 15:38:54 +0100371 try:
372 kwargs[k] = float(v)
tiernoe1281182018-05-22 12:24:36 +0200373 except Exception:
tiernoc94c3df2018-02-09 15:38:54 +0100374 pass
375 elif v.find(",") > 0:
376 kwargs[k] = v.split(",")
377 elif isinstance(v, (list, tuple)):
378 for index in range(0, len(v)):
379 if v[index] == "":
380 v[index] = None
381 elif format_yaml:
382 try:
383 v[index] = yaml.load(v[index])
tiernoe1281182018-05-22 12:24:36 +0200384 except Exception:
tiernoc94c3df2018-02-09 15:38:54 +0100385 pass
386
tiernof27c79b2018-03-12 17:08:42 +0100387 return indata
tiernoc94c3df2018-02-09 15:38:54 +0100388 except (ValueError, yaml.YAMLError) as exc:
389 raise NbiException(error_text + str(exc), HTTPStatus.BAD_REQUEST)
390 except KeyError as exc:
391 raise NbiException("Query string error: " + str(exc), HTTPStatus.BAD_REQUEST)
tiernob92094f2018-05-11 13:44:22 +0200392 except Exception as exc:
393 raise NbiException(error_text + str(exc), HTTPStatus.BAD_REQUEST)
tiernoc94c3df2018-02-09 15:38:54 +0100394
395 @staticmethod
tiernof27c79b2018-03-12 17:08:42 +0100396 def _format_out(data, session=None, _format=None):
tiernoc94c3df2018-02-09 15:38:54 +0100397 """
398 return string of dictionary data according to requested json, yaml, xml. By default json
tiernof27c79b2018-03-12 17:08:42 +0100399 :param data: response to be sent. Can be a dict, text or file
tiernoc94c3df2018-02-09 15:38:54 +0100400 :param session:
tiernof27c79b2018-03-12 17:08:42 +0100401 :param _format: The format to be set as Content-Type ir data is a file
tiernoc94c3df2018-02-09 15:38:54 +0100402 :return: None
403 """
tierno0f98af52018-03-19 10:28:22 +0100404 accept = cherrypy.request.headers.get("Accept")
tiernof27c79b2018-03-12 17:08:42 +0100405 if data is None:
tierno0f98af52018-03-19 10:28:22 +0100406 if accept and "text/html" in accept:
407 return html.format(data, cherrypy.request, cherrypy.response, session)
tierno09c073e2018-04-26 13:36:48 +0200408 # cherrypy.response.status = HTTPStatus.NO_CONTENT.value
tiernof27c79b2018-03-12 17:08:42 +0100409 return
410 elif hasattr(data, "read"): # file object
411 if _format:
412 cherrypy.response.headers["Content-Type"] = _format
413 elif "b" in data.mode: # binariy asssumig zip
414 cherrypy.response.headers["Content-Type"] = 'application/zip'
415 else:
416 cherrypy.response.headers["Content-Type"] = 'text/plain'
417 # TODO check that cherrypy close file. If not implement pending things to close per thread next
418 return data
tierno0f98af52018-03-19 10:28:22 +0100419 if accept:
tiernoc94c3df2018-02-09 15:38:54 +0100420 if "application/json" in accept:
421 cherrypy.response.headers["Content-Type"] = 'application/json; charset=utf-8'
422 a = json.dumps(data, indent=4) + "\n"
423 return a.encode("utf8")
424 elif "text/html" in accept:
425 return html.format(data, cherrypy.request, cherrypy.response, session)
426
tiernof27c79b2018-03-12 17:08:42 +0100427 elif "application/yaml" in accept or "*/*" in accept or "text/plain" in accept:
tiernoc94c3df2018-02-09 15:38:54 +0100428 pass
429 else:
430 raise cherrypy.HTTPError(HTTPStatus.NOT_ACCEPTABLE.value,
431 "Only 'Accept' of type 'application/json' or 'application/yaml' "
432 "for output format are available")
433 cherrypy.response.headers["Content-Type"] = 'application/yaml'
434 return yaml.safe_dump(data, explicit_start=True, indent=4, default_flow_style=False, tags=False,
435 encoding='utf-8', allow_unicode=True) # , canonical=True, default_style='"'
436
437 @cherrypy.expose
438 def index(self, *args, **kwargs):
439 session = None
440 try:
441 if cherrypy.request.method == "GET":
Eduardo Sousa2f988212018-07-26 01:04:11 +0100442 session = self.authenticator.authorize()
tiernoc94c3df2018-02-09 15:38:54 +0100443 outdata = "Index page"
444 else:
445 raise cherrypy.HTTPError(HTTPStatus.METHOD_NOT_ALLOWED.value,
tierno2236d202018-05-16 19:05:16 +0200446 "Method {} not allowed for tokens".format(cherrypy.request.method))
tiernoc94c3df2018-02-09 15:38:54 +0100447
448 return self._format_out(outdata, session)
449
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100450 except (EngineException, AuthException) as e:
tiernoc94c3df2018-02-09 15:38:54 +0100451 cherrypy.log("index Exception {}".format(e))
452 cherrypy.response.status = e.http_code.value
453 return self._format_out("Welcome to OSM!", session)
454
455 @cherrypy.expose
tierno55945e72018-04-06 16:40:27 +0200456 def version(self, *args, **kwargs):
tiernodfe09572018-04-24 10:41:10 +0200457 # TODO consider to remove and provide version using the static version file
tierno55945e72018-04-06 16:40:27 +0200458 global __version__, version_date
459 try:
460 if cherrypy.request.method != "GET":
461 raise NbiException("Only method GET is allowed", HTTPStatus.METHOD_NOT_ALLOWED)
462 elif args or kwargs:
463 raise NbiException("Invalid URL or query string for version", HTTPStatus.METHOD_NOT_ALLOWED)
464 return __version__ + " " + version_date
465 except NbiException as e:
466 cherrypy.response.status = e.http_code.value
467 problem_details = {
468 "code": e.http_code.name,
469 "status": e.http_code.value,
470 "detail": str(e),
471 }
472 return self._format_out(problem_details, None)
473
474 @cherrypy.expose
tiernof27c79b2018-03-12 17:08:42 +0100475 def token(self, method, token_id=None, kwargs=None):
tiernoc94c3df2018-02-09 15:38:54 +0100476 session = None
477 # self.engine.load_dbase(cherrypy.request.app.config)
tiernof27c79b2018-03-12 17:08:42 +0100478 indata = self._format_in(kwargs)
479 if not isinstance(indata, dict):
480 raise NbiException("Expected application/yaml or application/json Content-Type", HTTPStatus.BAD_REQUEST)
tiernoc94c3df2018-02-09 15:38:54 +0100481 try:
tiernoc94c3df2018-02-09 15:38:54 +0100482 if method == "GET":
Eduardo Sousa2f988212018-07-26 01:04:11 +0100483 session = self.authenticator.authorize()
tiernof27c79b2018-03-12 17:08:42 +0100484 if token_id:
Eduardo Sousa2f988212018-07-26 01:04:11 +0100485 outdata = self.authenticator.get_token(session, token_id)
tiernoc94c3df2018-02-09 15:38:54 +0100486 else:
Eduardo Sousa2f988212018-07-26 01:04:11 +0100487 outdata = self.authenticator.get_token_list(session)
tiernoc94c3df2018-02-09 15:38:54 +0100488 elif method == "POST":
489 try:
Eduardo Sousa2f988212018-07-26 01:04:11 +0100490 session = self.authenticator.authorize()
tiernoe1281182018-05-22 12:24:36 +0200491 except Exception:
tiernoc94c3df2018-02-09 15:38:54 +0100492 session = None
493 if kwargs:
494 indata.update(kwargs)
Eduardo Sousa2f988212018-07-26 01:04:11 +0100495 outdata = self.authenticator.new_token(session, indata, cherrypy.request.remote)
tiernoc94c3df2018-02-09 15:38:54 +0100496 session = outdata
497 cherrypy.session['Authorization'] = outdata["_id"]
tierno0f98af52018-03-19 10:28:22 +0100498 self._set_location_header("admin", "v1", "tokens", outdata["_id"])
tiernoc94c3df2018-02-09 15:38:54 +0100499 # cherrypy.response.cookie["Authorization"] = outdata["id"]
500 # cherrypy.response.cookie["Authorization"]['expires'] = 3600
501 elif method == "DELETE":
tiernof27c79b2018-03-12 17:08:42 +0100502 if not token_id and "id" in kwargs:
tiernoc94c3df2018-02-09 15:38:54 +0100503 token_id = kwargs["id"]
tiernof27c79b2018-03-12 17:08:42 +0100504 elif not token_id:
Eduardo Sousa2f988212018-07-26 01:04:11 +0100505 session = self.authenticator.authorize()
tiernoc94c3df2018-02-09 15:38:54 +0100506 token_id = session["_id"]
Eduardo Sousa2f988212018-07-26 01:04:11 +0100507 outdata = self.authenticator.del_token(token_id)
tiernoc94c3df2018-02-09 15:38:54 +0100508 session = None
509 cherrypy.session['Authorization'] = "logout"
510 # cherrypy.response.cookie["Authorization"] = token_id
511 # cherrypy.response.cookie["Authorization"]['expires'] = 0
512 else:
513 raise NbiException("Method {} not allowed for token".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
514 return self._format_out(outdata, session)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100515 except (NbiException, EngineException, DbException, AuthException) as e:
tiernoc94c3df2018-02-09 15:38:54 +0100516 cherrypy.log("tokens Exception {}".format(e))
517 cherrypy.response.status = e.http_code.value
518 problem_details = {
519 "code": e.http_code.name,
520 "status": e.http_code.value,
521 "detail": str(e),
522 }
523 return self._format_out(problem_details, session)
524
525 @cherrypy.expose
526 def test(self, *args, **kwargs):
527 thread_info = None
tiernof27c79b2018-03-12 17:08:42 +0100528 if args and args[0] == "help":
529 return "<html><pre>\ninit\nfile/<name> download file\ndb-clear/table\nprune\nlogin\nlogin2\n"\
tiernoe1281182018-05-22 12:24:36 +0200530 "sleep/<time>\nmessage/topic\n</pre></html>"
tiernof27c79b2018-03-12 17:08:42 +0100531
532 elif args and args[0] == "init":
tiernoc94c3df2018-02-09 15:38:54 +0100533 try:
534 # self.engine.load_dbase(cherrypy.request.app.config)
535 self.engine.create_admin()
536 return "Done. User 'admin', password 'admin' created"
537 except Exception:
538 cherrypy.response.status = HTTPStatus.FORBIDDEN.value
539 return self._format_out("Database already initialized")
tiernof27c79b2018-03-12 17:08:42 +0100540 elif args and args[0] == "file":
541 return cherrypy.lib.static.serve_file(cherrypy.tree.apps['/osm'].config["storage"]["path"] + "/" + args[1],
542 "text/plain", "attachment")
543 elif args and args[0] == "file2":
544 f_path = cherrypy.tree.apps['/osm'].config["storage"]["path"] + "/" + args[1]
545 f = open(f_path, "r")
546 cherrypy.response.headers["Content-type"] = "text/plain"
tiernof27c79b2018-03-12 17:08:42 +0100547 return f
tierno55945e72018-04-06 16:40:27 +0200548
tiernof27c79b2018-03-12 17:08:42 +0100549 elif len(args) == 2 and args[0] == "db-clear":
tiernob24258a2018-10-04 18:39:49 +0200550 return self.engine.db.del_list(args[1], kwargs)
tiernoc94c3df2018-02-09 15:38:54 +0100551 elif args and args[0] == "prune":
552 return self.engine.prune()
553 elif args and args[0] == "login":
554 if not cherrypy.request.headers.get("Authorization"):
555 cherrypy.response.headers["WWW-Authenticate"] = 'Basic realm="Access to OSM site", charset="UTF-8"'
556 cherrypy.response.status = HTTPStatus.UNAUTHORIZED.value
557 elif args and args[0] == "login2":
558 if not cherrypy.request.headers.get("Authorization"):
559 cherrypy.response.headers["WWW-Authenticate"] = 'Bearer realm="Access to OSM site"'
560 cherrypy.response.status = HTTPStatus.UNAUTHORIZED.value
561 elif args and args[0] == "sleep":
562 sleep_time = 5
563 try:
564 sleep_time = int(args[1])
565 except Exception:
566 cherrypy.response.status = HTTPStatus.FORBIDDEN.value
567 return self._format_out("Database already initialized")
568 thread_info = cherrypy.thread_data
569 print(thread_info)
570 time.sleep(sleep_time)
571 # thread_info
572 elif len(args) >= 2 and args[0] == "message":
tiernob24258a2018-10-04 18:39:49 +0200573 main_topic = args[1]
574 return_text = "<html><pre>{} ->\n".format(main_topic)
tiernoc94c3df2018-02-09 15:38:54 +0100575 try:
tierno55945e72018-04-06 16:40:27 +0200576 if cherrypy.request.method == 'POST':
577 to_send = yaml.load(cherrypy.request.body)
578 for k, v in to_send.items():
tiernob24258a2018-10-04 18:39:49 +0200579 self.engine.msg.write(main_topic, k, v)
tierno55945e72018-04-06 16:40:27 +0200580 return_text += " {}: {}\n".format(k, v)
581 elif cherrypy.request.method == 'GET':
582 for k, v in kwargs.items():
tiernob24258a2018-10-04 18:39:49 +0200583 self.engine.msg.write(main_topic, k, yaml.load(v))
tierno55945e72018-04-06 16:40:27 +0200584 return_text += " {}: {}\n".format(k, yaml.load(v))
tiernoc94c3df2018-02-09 15:38:54 +0100585 except Exception as e:
tierno55945e72018-04-06 16:40:27 +0200586 return_text += "Error: " + str(e)
587 return_text += "</pre></html>\n"
588 return return_text
tiernoc94c3df2018-02-09 15:38:54 +0100589
590 return_text = (
591 "<html><pre>\nheaders:\n args: {}\n".format(args) +
592 " kwargs: {}\n".format(kwargs) +
593 " headers: {}\n".format(cherrypy.request.headers) +
594 " path_info: {}\n".format(cherrypy.request.path_info) +
595 " query_string: {}\n".format(cherrypy.request.query_string) +
596 " session: {}\n".format(cherrypy.session) +
597 " cookie: {}\n".format(cherrypy.request.cookie) +
598 " method: {}\n".format(cherrypy.request.method) +
599 " session: {}\n".format(cherrypy.session.get('fieldname')) +
600 " body:\n")
601 return_text += " length: {}\n".format(cherrypy.request.body.length)
602 if cherrypy.request.body.length:
603 return_text += " content: {}\n".format(
604 str(cherrypy.request.body.read(int(cherrypy.request.headers.get('Content-Length', 0)))))
605 if thread_info:
606 return_text += "thread: {}\n".format(thread_info)
607 return_text += "</pre></html>"
608 return return_text
609
tiernof27c79b2018-03-12 17:08:42 +0100610 def _check_valid_url_method(self, method, *args):
611 if len(args) < 3:
tiernob24258a2018-10-04 18:39:49 +0200612 raise NbiException("URL must contain at least 'main_topic/version/topic'", HTTPStatus.METHOD_NOT_ALLOWED)
tiernof27c79b2018-03-12 17:08:42 +0100613
614 reference = self.valid_methods
615 for arg in args:
616 if arg is None:
617 break
618 if not isinstance(reference, dict):
619 raise NbiException("URL contains unexpected extra items '{}'".format(arg),
620 HTTPStatus.METHOD_NOT_ALLOWED)
621
622 if arg in reference:
623 reference = reference[arg]
624 elif "<ID>" in reference:
625 reference = reference["<ID>"]
626 elif "*" in reference:
627 reference = reference["*"]
628 break
629 else:
630 raise NbiException("Unexpected URL item {}".format(arg), HTTPStatus.METHOD_NOT_ALLOWED)
631 if "TODO" in reference and method in reference["TODO"]:
632 raise NbiException("Method {} not supported yet for this URL".format(method), HTTPStatus.NOT_IMPLEMENTED)
tierno2236d202018-05-16 19:05:16 +0200633 elif "METHODS" in reference and method not in reference["METHODS"]:
tiernof27c79b2018-03-12 17:08:42 +0100634 raise NbiException("Method {} not supported for this URL".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
635 return
636
637 @staticmethod
tiernob24258a2018-10-04 18:39:49 +0200638 def _set_location_header(main_topic, version, topic, id):
tiernof27c79b2018-03-12 17:08:42 +0100639 """
640 Insert response header Location with the URL of created item base on URL params
tiernob24258a2018-10-04 18:39:49 +0200641 :param main_topic:
tiernof27c79b2018-03-12 17:08:42 +0100642 :param version:
tiernob24258a2018-10-04 18:39:49 +0200643 :param topic:
tiernof27c79b2018-03-12 17:08:42 +0100644 :param id:
645 :return: None
646 """
647 # 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 +0200648 cherrypy.response.headers["Location"] = "/osm/{}/{}/{}/{}".format(main_topic, version, topic, id)
tiernof27c79b2018-03-12 17:08:42 +0100649 return
650
tiernoc94c3df2018-02-09 15:38:54 +0100651 @cherrypy.expose
tiernob24258a2018-10-04 18:39:49 +0200652 def default(self, main_topic=None, version=None, topic=None, _id=None, item=None, *args, **kwargs):
tiernoc94c3df2018-02-09 15:38:54 +0100653 session = None
tiernof27c79b2018-03-12 17:08:42 +0100654 outdata = None
655 _format = None
tierno0f98af52018-03-19 10:28:22 +0100656 method = "DONE"
tiernob24258a2018-10-04 18:39:49 +0200657 engine_topic = None
tiernodb9dc582018-06-20 17:27:29 +0200658 rollback = []
659 session = None
tiernoc94c3df2018-02-09 15:38:54 +0100660 try:
tiernob24258a2018-10-04 18:39:49 +0200661 if not main_topic or not version or not topic:
662 raise NbiException("URL must contain at least 'main_topic/version/topic'",
663 HTTPStatus.METHOD_NOT_ALLOWED)
Felipe Vicensb57758d2018-10-16 16:00:20 +0200664 if main_topic not in ("admin", "vnfpkgm", "nsd", "nslcm", "nst", "nsilcm"):
tiernob24258a2018-10-04 18:39:49 +0200665 raise NbiException("URL main_topic '{}' not supported".format(main_topic),
666 HTTPStatus.METHOD_NOT_ALLOWED)
tiernoc94c3df2018-02-09 15:38:54 +0100667 if version != 'v1':
668 raise NbiException("URL version '{}' not supported".format(version), HTTPStatus.METHOD_NOT_ALLOWED)
669
tiernof27c79b2018-03-12 17:08:42 +0100670 if kwargs and "METHOD" in kwargs and kwargs["METHOD"] in ("PUT", "POST", "DELETE", "GET", "PATCH"):
671 method = kwargs.pop("METHOD")
672 else:
673 method = cherrypy.request.method
tiernob92094f2018-05-11 13:44:22 +0200674 if kwargs and "FORCE" in kwargs:
675 force = kwargs.pop("FORCE")
676 else:
677 force = False
tiernob24258a2018-10-04 18:39:49 +0200678 self._check_valid_url_method(method, main_topic, version, topic, _id, item, *args)
tiernob24258a2018-10-04 18:39:49 +0200679 if main_topic == "admin" and topic == "tokens":
tiernof27c79b2018-03-12 17:08:42 +0100680 return self.token(method, _id, kwargs)
681
tiernoc94c3df2018-02-09 15:38:54 +0100682 # self.engine.load_dbase(cherrypy.request.app.config)
Eduardo Sousa2f988212018-07-26 01:04:11 +0100683 session = self.authenticator.authorize()
tiernof27c79b2018-03-12 17:08:42 +0100684 indata = self._format_in(kwargs)
tiernob24258a2018-10-04 18:39:49 +0200685 engine_topic = topic
686 if topic == "subscriptions":
687 engine_topic = main_topic + "_" + topic
688 if item:
689 engine_topic = item
tiernoc94c3df2018-02-09 15:38:54 +0100690
tiernob24258a2018-10-04 18:39:49 +0200691 if main_topic == "nsd":
692 engine_topic = "nsds"
693 elif main_topic == "vnfpkgm":
694 engine_topic = "vnfds"
695 elif main_topic == "nslcm":
696 engine_topic = "nsrs"
697 if topic == "ns_lcm_op_occs":
698 engine_topic = "nslcmops"
699 if topic == "vnfrs" or topic == "vnf_instances":
700 engine_topic = "vnfrs"
garciadeblas9750c5a2018-10-15 16:20:35 +0200701 elif main_topic == "nst":
702 engine_topic = "nsts"
703 elif main_topic == "nsilcm":
704 engine_topic = "nsis"
705 if topic == "nsi_lcm_op_occs":
Felipe Vicensb57758d2018-10-16 16:00:20 +0200706 engine_topic = "nsilcmops"
tiernob24258a2018-10-04 18:39:49 +0200707 elif main_topic == "pdu":
708 engine_topic = "pdus"
709 if engine_topic == "vims": # TODO this is for backward compatibility, it will remove in the future
710 engine_topic = "vim_accounts"
tiernoc94c3df2018-02-09 15:38:54 +0100711
712 if method == "GET":
Felipe Vicens07f31722018-10-29 15:16:44 +0100713 if item in ("nsd_content", "package_content", "artifacts", "vnfd", "nsd", "nst", "nst_content"):
garciadeblas9750c5a2018-10-15 16:20:35 +0200714 if item in ("vnfd", "nsd", "nst"):
tiernof27c79b2018-03-12 17:08:42 +0100715 path = "$DESCRIPTOR"
716 elif args:
717 path = args
tiernob24258a2018-10-04 18:39:49 +0200718 elif item == "artifacts":
tiernof27c79b2018-03-12 17:08:42 +0100719 path = ()
720 else:
721 path = None
tiernob24258a2018-10-04 18:39:49 +0200722 file, _format = self.engine.get_file(session, engine_topic, _id, path,
tierno2236d202018-05-16 19:05:16 +0200723 cherrypy.request.headers.get("Accept"))
tiernof27c79b2018-03-12 17:08:42 +0100724 outdata = file
725 elif not _id:
tiernob24258a2018-10-04 18:39:49 +0200726 outdata = self.engine.get_item_list(session, engine_topic, kwargs)
tiernoc94c3df2018-02-09 15:38:54 +0100727 else:
tiernob24258a2018-10-04 18:39:49 +0200728 outdata = self.engine.get_item(session, engine_topic, _id)
tiernof27c79b2018-03-12 17:08:42 +0100729 elif method == "POST":
garciadeblas9750c5a2018-10-15 16:20:35 +0200730 if topic in ("ns_descriptors_content", "vnf_packages_content", "netslice_templates_content"):
tiernof27c79b2018-03-12 17:08:42 +0100731 _id = cherrypy.request.headers.get("Transaction-Id")
732 if not _id:
tiernob24258a2018-10-04 18:39:49 +0200733 _id = self.engine.new_item(rollback, session, engine_topic, {}, None, cherrypy.request.headers,
tiernob92094f2018-05-11 13:44:22 +0200734 force=force)
tiernob24258a2018-10-04 18:39:49 +0200735 completed = self.engine.upload_content(session, engine_topic, _id, indata, kwargs,
736 cherrypy.request.headers, force=force)
tiernof27c79b2018-03-12 17:08:42 +0100737 if completed:
tiernob24258a2018-10-04 18:39:49 +0200738 self._set_location_header(main_topic, version, topic, _id)
tiernof27c79b2018-03-12 17:08:42 +0100739 else:
740 cherrypy.response.headers["Transaction-Id"] = _id
741 outdata = {"id": _id}
tiernob24258a2018-10-04 18:39:49 +0200742 elif topic == "ns_instances_content":
743 # creates NSR
744 _id = self.engine.new_item(rollback, session, engine_topic, indata, kwargs, force=force)
745 # creates nslcmop
746 indata["lcmOperationType"] = "instantiate"
747 indata["nsInstanceId"] = _id
748 self.engine.new_item(rollback, session, "nslcmops", indata, None)
749 self._set_location_header(main_topic, version, topic, _id)
tiernof27c79b2018-03-12 17:08:42 +0100750 outdata = {"id": _id}
tiernob24258a2018-10-04 18:39:49 +0200751 elif topic == "ns_instances" and item:
752 indata["lcmOperationType"] = item
753 indata["nsInstanceId"] = _id
754 _id = self.engine.new_item(rollback, session, "nslcmops", indata, kwargs)
755 self._set_location_header(main_topic, version, "ns_lcm_op_occs", _id)
tierno65acb4d2018-04-06 16:42:40 +0200756 outdata = {"id": _id}
757 cherrypy.response.status = HTTPStatus.ACCEPTED.value
garciadeblas9750c5a2018-10-15 16:20:35 +0200758 elif topic == "netslice_instances_content":
Felipe Vicens07f31722018-10-29 15:16:44 +0100759 # creates NetSlice_Instance_record (NSIR)
garciadeblas9750c5a2018-10-15 16:20:35 +0200760 _id = self.engine.new_item(rollback, session, engine_topic, indata, kwargs, force=force)
Felipe Vicens07f31722018-10-29 15:16:44 +0100761 self._set_location_header(main_topic, version, topic, _id)
garciadeblas9750c5a2018-10-15 16:20:35 +0200762 indata["lcmOperationType"] = "instantiate"
763 indata["nsiInstanceId"] = _id
Felipe Vicens07f31722018-10-29 15:16:44 +0100764 self.engine.new_item(rollback, session, "nsilcmops", indata, kwargs)
garciadeblas9750c5a2018-10-15 16:20:35 +0200765 outdata = {"id": _id}
Felipe Vicens07f31722018-10-29 15:16:44 +0100766
garciadeblas9750c5a2018-10-15 16:20:35 +0200767 elif topic == "netslice_instances" and item:
768 indata["lcmOperationType"] = item
769 indata["nsiInstanceId"] = _id
770 _id = self.engine.new_item(rollback, session, "nsilcmops", indata, kwargs)
771 self._set_location_header(main_topic, version, "nsi_lcm_op_occs", _id)
772 outdata = {"id": _id}
773 cherrypy.response.status = HTTPStatus.ACCEPTED.value
tiernof27c79b2018-03-12 17:08:42 +0100774 else:
tiernob24258a2018-10-04 18:39:49 +0200775 _id = self.engine.new_item(rollback, session, engine_topic, indata, kwargs,
776 cherrypy.request.headers, force=force)
777 self._set_location_header(main_topic, version, topic, _id)
tiernof27c79b2018-03-12 17:08:42 +0100778 outdata = {"id": _id}
tiernob24258a2018-10-04 18:39:49 +0200779 # TODO form NsdInfo when topic in ("ns_descriptors", "vnf_packages")
tiernof27c79b2018-03-12 17:08:42 +0100780 cherrypy.response.status = HTTPStatus.CREATED.value
tierno09c073e2018-04-26 13:36:48 +0200781
tiernoc94c3df2018-02-09 15:38:54 +0100782 elif method == "DELETE":
783 if not _id:
tiernob24258a2018-10-04 18:39:49 +0200784 outdata = self.engine.del_item_list(session, engine_topic, kwargs)
tierno09c073e2018-04-26 13:36:48 +0200785 cherrypy.response.status = HTTPStatus.OK.value
tiernoc94c3df2018-02-09 15:38:54 +0100786 else: # len(args) > 1
tiernob24258a2018-10-04 18:39:49 +0200787 if topic == "ns_instances_content" and not force:
788 nslcmop_desc = {
789 "lcmOperationType": "terminate",
790 "nsInstanceId": _id,
791 "autoremove": True
792 }
793 opp_id = self.engine.new_item(rollback, session, "nslcmops", nslcmop_desc, None)
tierno3ace63c2018-05-03 17:51:43 +0200794 outdata = {"_id": opp_id}
tierno09c073e2018-04-26 13:36:48 +0200795 cherrypy.response.status = HTTPStatus.ACCEPTED.value
garciadeblas9750c5a2018-10-15 16:20:35 +0200796 elif topic == "netslice_instances_content" and not force:
797 nsilcmop_desc = {
798 "lcmOperationType": "terminate",
799 "nsiInstanceId": _id,
800 "autoremove": True
801 }
802 opp_id = self.engine.new_item(rollback, session, "nsilcmops", nsilcmop_desc, None)
803 outdata = {"_id": opp_id}
804 cherrypy.response.status = HTTPStatus.ACCEPTED.value
tierno65acb4d2018-04-06 16:42:40 +0200805 else:
tiernob24258a2018-10-04 18:39:49 +0200806 self.engine.del_item(session, engine_topic, _id, force)
tierno09c073e2018-04-26 13:36:48 +0200807 cherrypy.response.status = HTTPStatus.NO_CONTENT.value
tiernob24258a2018-10-04 18:39:49 +0200808 if engine_topic in ("vim_accounts", "sdns"):
tierno09c073e2018-04-26 13:36:48 +0200809 cherrypy.response.status = HTTPStatus.ACCEPTED.value
810
tierno7ae10112018-05-18 14:36:02 +0200811 elif method in ("PUT", "PATCH"):
tiernocd54a4a2018-09-12 16:40:35 +0200812 outdata = None
tiernof27c79b2018-03-12 17:08:42 +0100813 if not indata and not kwargs:
tiernoc94c3df2018-02-09 15:38:54 +0100814 raise NbiException("Nothing to update. Provide payload and/or query string",
815 HTTPStatus.BAD_REQUEST)
garciadeblas9750c5a2018-10-15 16:20:35 +0200816 if item in ("nsd_content", "package_content", "nst_content") and method == "PUT":
tiernob24258a2018-10-04 18:39:49 +0200817 completed = self.engine.upload_content(session, engine_topic, _id, indata, kwargs,
818 cherrypy.request.headers, force=force)
tiernof27c79b2018-03-12 17:08:42 +0100819 if not completed:
820 cherrypy.response.headers["Transaction-Id"] = id
tiernof27c79b2018-03-12 17:08:42 +0100821 else:
tiernob24258a2018-10-04 18:39:49 +0200822 self.engine.edit_item(session, engine_topic, _id, indata, kwargs, force=force)
tiernocd54a4a2018-09-12 16:40:35 +0200823 cherrypy.response.status = HTTPStatus.NO_CONTENT.value
tiernoc94c3df2018-02-09 15:38:54 +0100824 else:
825 raise NbiException("Method {} not allowed".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
tiernof27c79b2018-03-12 17:08:42 +0100826 return self._format_out(outdata, session, _format)
tiernob24258a2018-10-04 18:39:49 +0200827 except Exception as e:
828 if isinstance(e, (NbiException, EngineException, DbException, FsException, MsgException, AuthException)):
829 http_code_value = cherrypy.response.status = e.http_code.value
830 http_code_name = e.http_code.name
831 cherrypy.log("Exception {}".format(e))
832 else:
833 http_code_value = cherrypy.response.status = HTTPStatus.BAD_REQUEST.value # INTERNAL_SERVER_ERROR
834 cherrypy.log("CRITICAL: Exception {}".format(e))
835 http_code_name = HTTPStatus.BAD_REQUEST.name
tierno3ace63c2018-05-03 17:51:43 +0200836 if hasattr(outdata, "close"): # is an open file
837 outdata.close()
tiernob24258a2018-10-04 18:39:49 +0200838 error_text = str(e)
839 rollback.reverse()
tiernodb9dc582018-06-20 17:27:29 +0200840 for rollback_item in rollback:
tierno3ace63c2018-05-03 17:51:43 +0200841 try:
tiernocc103432018-10-19 14:10:35 +0200842 if rollback_item.get("operation") == "set":
843 self.engine.db.set_one(rollback_item["topic"], {"_id": rollback_item["_id"]},
844 rollback_item["content"], fail_on_empty=False)
845 else:
846 self.engine.del_item(**rollback_item, session=session, force=True)
tierno3ace63c2018-05-03 17:51:43 +0200847 except Exception as e2:
tiernob24258a2018-10-04 18:39:49 +0200848 rollback_error_text = "Rollback Exception {}: {}".format(rollback_item, e2)
849 cherrypy.log(rollback_error_text)
850 error_text += ". " + rollback_error_text
851 # if isinstance(e, MsgException):
852 # error_text = "{} has been '{}' but other modules cannot be informed because an error on bus".format(
853 # engine_topic[:-1], method, error_text)
tiernoc94c3df2018-02-09 15:38:54 +0100854 problem_details = {
tiernob24258a2018-10-04 18:39:49 +0200855 "code": http_code_name,
856 "status": http_code_value,
857 "detail": error_text,
tiernoc94c3df2018-02-09 15:38:54 +0100858 }
859 return self._format_out(problem_details, session)
860 # raise cherrypy.HTTPError(e.http_code.value, str(e))
861
862
863# def validate_password(realm, username, password):
864# cherrypy.log("realm "+ str(realm))
865# if username == "admin" and password == "admin":
866# return True
867# return False
868
869
870def _start_service():
871 """
872 Callback function called when cherrypy.engine starts
873 Override configuration with env variables
874 Set database, storage, message configuration
875 Init database with admin/admin user password
876 """
877 cherrypy.log.error("Starting osm_nbi")
878 # update general cherrypy configuration
879 update_dict = {}
880
881 engine_config = cherrypy.tree.apps['/osm'].config
882 for k, v in environ.items():
883 if not k.startswith("OSMNBI_"):
884 continue
tiernoe1281182018-05-22 12:24:36 +0200885 k1, _, k2 = k[7:].lower().partition("_")
tiernoc94c3df2018-02-09 15:38:54 +0100886 if not k2:
887 continue
888 try:
889 # update static configuration
890 if k == 'OSMNBI_STATIC_DIR':
891 engine_config["/static"]['tools.staticdir.dir'] = v
892 engine_config["/static"]['tools.staticdir.on'] = True
893 elif k == 'OSMNBI_SOCKET_PORT' or k == 'OSMNBI_SERVER_PORT':
894 update_dict['server.socket_port'] = int(v)
895 elif k == 'OSMNBI_SOCKET_HOST' or k == 'OSMNBI_SERVER_HOST':
896 update_dict['server.socket_host'] = v
tiernof5298be2018-05-16 14:43:57 +0200897 elif k1 in ("server", "test", "auth", "log"):
898 update_dict[k1 + '.' + k2] = v
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100899 elif k1 in ("message", "database", "storage", "authentication"):
tiernof5298be2018-05-16 14:43:57 +0200900 # k2 = k2.replace('_', '.')
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100901 if k2 in ("port", "db_port"):
tiernoc94c3df2018-02-09 15:38:54 +0100902 engine_config[k1][k2] = int(v)
903 else:
904 engine_config[k1][k2] = v
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100905
tiernoc94c3df2018-02-09 15:38:54 +0100906 except ValueError as e:
907 cherrypy.log.error("Ignoring environ '{}': " + str(e))
908 except Exception as e:
909 cherrypy.log.warn("skipping environ '{}' on exception '{}'".format(k, e))
910
911 if update_dict:
912 cherrypy.config.update(update_dict)
tiernof5298be2018-05-16 14:43:57 +0200913 engine_config["global"].update(update_dict)
tiernoc94c3df2018-02-09 15:38:54 +0100914
915 # logging cherrypy
916 log_format_simple = "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(message)s"
917 log_formatter_simple = logging.Formatter(log_format_simple, datefmt='%Y-%m-%dT%H:%M:%S')
918 logger_server = logging.getLogger("cherrypy.error")
919 logger_access = logging.getLogger("cherrypy.access")
920 logger_cherry = logging.getLogger("cherrypy")
921 logger_nbi = logging.getLogger("nbi")
922
tiernof5298be2018-05-16 14:43:57 +0200923 if "log.file" in engine_config["global"]:
924 file_handler = logging.handlers.RotatingFileHandler(engine_config["global"]["log.file"],
tiernoc94c3df2018-02-09 15:38:54 +0100925 maxBytes=100e6, backupCount=9, delay=0)
926 file_handler.setFormatter(log_formatter_simple)
927 logger_cherry.addHandler(file_handler)
928 logger_nbi.addHandler(file_handler)
tiernob24258a2018-10-04 18:39:49 +0200929 # log always to standard output
930 for format_, logger in {"nbi.server %(filename)s:%(lineno)s": logger_server,
931 "nbi.access %(filename)s:%(lineno)s": logger_access,
932 "%(name)s %(filename)s:%(lineno)s": logger_nbi
933 }.items():
934 log_format_cherry = "%(asctime)s %(levelname)s {} %(message)s".format(format_)
935 log_formatter_cherry = logging.Formatter(log_format_cherry, datefmt='%Y-%m-%dT%H:%M:%S')
936 str_handler = logging.StreamHandler()
937 str_handler.setFormatter(log_formatter_cherry)
938 logger.addHandler(str_handler)
tiernoc94c3df2018-02-09 15:38:54 +0100939
tiernof5298be2018-05-16 14:43:57 +0200940 if engine_config["global"].get("log.level"):
941 logger_cherry.setLevel(engine_config["global"]["log.level"])
942 logger_nbi.setLevel(engine_config["global"]["log.level"])
tiernoc94c3df2018-02-09 15:38:54 +0100943
944 # logging other modules
945 for k1, logname in {"message": "nbi.msg", "database": "nbi.db", "storage": "nbi.fs"}.items():
946 engine_config[k1]["logger_name"] = logname
947 logger_module = logging.getLogger(logname)
948 if "logfile" in engine_config[k1]:
949 file_handler = logging.handlers.RotatingFileHandler(engine_config[k1]["logfile"],
tierno2236d202018-05-16 19:05:16 +0200950 maxBytes=100e6, backupCount=9, delay=0)
tiernoc94c3df2018-02-09 15:38:54 +0100951 file_handler.setFormatter(log_formatter_simple)
952 logger_module.addHandler(file_handler)
953 if "loglevel" in engine_config[k1]:
954 logger_module.setLevel(engine_config[k1]["loglevel"])
955 # TODO add more entries, e.g.: storage
956 cherrypy.tree.apps['/osm'].root.engine.start(engine_config)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100957 cherrypy.tree.apps['/osm'].root.authenticator.start(engine_config)
tiernod985a8d2018-10-19 14:12:28 +0200958 cherrypy.tree.apps['/osm'].root.engine.init_db(target_version=database_version)
959 cherrypy.tree.apps['/osm'].root.authenticator.init_db(target_version=auth_database_version)
tiernoc94c3df2018-02-09 15:38:54 +0100960 # getenv('OSMOPENMANO_TENANT', None)
961
962
963def _stop_service():
964 """
965 Callback function called when cherrypy.engine stops
966 TODO: Ending database connections.
967 """
968 cherrypy.tree.apps['/osm'].root.engine.stop()
969 cherrypy.log.error("Stopping osm_nbi")
970
tierno2236d202018-05-16 19:05:16 +0200971
tiernof5298be2018-05-16 14:43:57 +0200972def nbi(config_file):
tiernoc94c3df2018-02-09 15:38:54 +0100973 # conf = {
974 # '/': {
975 # #'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
976 # 'tools.sessions.on': True,
977 # 'tools.response_headers.on': True,
978 # # 'tools.response_headers.headers': [('Content-Type', 'text/plain')],
979 # }
980 # }
981 # cherrypy.Server.ssl_module = 'builtin'
982 # cherrypy.Server.ssl_certificate = "http/cert.pem"
983 # cherrypy.Server.ssl_private_key = "http/privkey.pem"
984 # cherrypy.Server.thread_pool = 10
985 # cherrypy.config.update({'Server.socket_port': config["port"], 'Server.socket_host': config["host"]})
986
987 # cherrypy.config.update({'tools.auth_basic.on': True,
988 # 'tools.auth_basic.realm': 'localhost',
989 # 'tools.auth_basic.checkpassword': validate_password})
990 cherrypy.engine.subscribe('start', _start_service)
991 cherrypy.engine.subscribe('stop', _stop_service)
tiernof5298be2018-05-16 14:43:57 +0200992 cherrypy.quickstart(Server(), '/osm', config_file)
993
994
995def usage():
996 print("""Usage: {} [options]
997 -c|--config [configuration_file]: loads the configuration file (default: ./nbi.cfg)
998 -h|--help: shows this help
999 """.format(sys.argv[0]))
tierno2236d202018-05-16 19:05:16 +02001000 # --log-socket-host HOST: send logs to this host")
1001 # --log-socket-port PORT: send logs using this port (default: 9022)")
tiernoc94c3df2018-02-09 15:38:54 +01001002
1003
1004if __name__ == '__main__':
tiernof5298be2018-05-16 14:43:57 +02001005 try:
1006 # load parameters and configuration
1007 opts, args = getopt.getopt(sys.argv[1:], "hvc:", ["config=", "help"])
1008 # TODO add "log-socket-host=", "log-socket-port=", "log-file="
1009 config_file = None
1010 for o, a in opts:
1011 if o in ("-h", "--help"):
1012 usage()
1013 sys.exit()
1014 elif o in ("-c", "--config"):
1015 config_file = a
1016 # elif o == "--log-socket-port":
1017 # log_socket_port = a
1018 # elif o == "--log-socket-host":
1019 # log_socket_host = a
1020 # elif o == "--log-file":
1021 # log_file = a
1022 else:
1023 assert False, "Unhandled option"
1024 if config_file:
1025 if not path.isfile(config_file):
1026 print("configuration file '{}' that not exist".format(config_file), file=sys.stderr)
1027 exit(1)
1028 else:
1029 for config_file in (__file__[:__file__.rfind(".")] + ".cfg", "./nbi.cfg", "/etc/osm/nbi.cfg"):
1030 if path.isfile(config_file):
1031 break
1032 else:
1033 print("No configuration file 'nbi.cfg' found neither at local folder nor at /etc/osm/", file=sys.stderr)
1034 exit(1)
1035 nbi(config_file)
1036 except getopt.GetoptError as e:
1037 print(str(e), file=sys.stderr)
1038 # usage()
1039 exit(1)