blob: b7a6990bf9f4a919bdd2c7af128156deba415d71 [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
tierno36ec8602018-11-02 17:27:11 +010017from validation import ValidationError
tiernoa8d63632018-05-10 13:12:32 +020018from osm_common.dbbase import DbException
19from osm_common.fsbase import FsException
20from osm_common.msgbase import MsgException
tiernoc94c3df2018-02-09 15:38:54 +010021from http import HTTPStatus
tiernoc94c3df2018-02-09 15:38:54 +010022from codecs import getreader
tiernof5298be2018-05-16 14:43:57 +020023from os import environ, path
tiernoc94c3df2018-02-09 15:38:54 +010024
25__author__ = "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
tiernodfe09572018-04-24 10:41:10 +020026
27# TODO consider to remove and provide version using the static version file
28__version__ = "0.1.3"
tierno55945e72018-04-06 16:40:27 +020029version_date = "Apr 2018"
tierno4a946e42018-04-12 17:48:49 +020030database_version = '1.0'
Eduardo Sousa819d34c2018-07-31 01:20:02 +010031auth_database_version = '1.0'
tiernoc94c3df2018-02-09 15:38:54 +010032
33"""
tiernof27c79b2018-03-12 17:08:42 +010034North Bound Interface (O: OSM specific; 5,X: SOL005 not implemented yet; O5: SOL005 implemented)
tiernoc94c3df2018-02-09 15:38:54 +010035URL: /osm GET POST PUT DELETE PATCH
garciadeblas9750c5a2018-10-15 16:20:35 +020036 /nsd/v1
tierno2236d202018-05-16 19:05:16 +020037 /ns_descriptors_content O O
38 /<nsdInfoId> O O O O
tiernoc94c3df2018-02-09 15:38:54 +010039 /ns_descriptors O5 O5
40 /<nsdInfoId> O5 O5 5
41 /nsd_content O5 O5
tiernof27c79b2018-03-12 17:08:42 +010042 /nsd O
43 /artifacts[/<artifactPath>] O
tiernoc94c3df2018-02-09 15:38:54 +010044 /pnf_descriptors 5 5
45 /<pnfdInfoId> 5 5 5
46 /pnfd_content 5 5
tiernof27c79b2018-03-12 17:08:42 +010047 /subscriptions 5 5
48 /<subscriptionId> 5 X
tiernoc94c3df2018-02-09 15:38:54 +010049
50 /vnfpkgm/v1
tierno55945e72018-04-06 16:40:27 +020051 /vnf_packages_content O O
tierno2236d202018-05-16 19:05:16 +020052 /<vnfPkgId> O O
tiernoc94c3df2018-02-09 15:38:54 +010053 /vnf_packages O5 O5
54 /<vnfPkgId> O5 O5 5
tiernoc94c3df2018-02-09 15:38:54 +010055 /package_content O5 O5
56 /upload_from_uri X
tiernof27c79b2018-03-12 17:08:42 +010057 /vnfd O5
58 /artifacts[/<artifactPath>] O5
59 /subscriptions X X
60 /<subscriptionId> X X
tiernoc94c3df2018-02-09 15:38:54 +010061
62 /nslcm/v1
tiernof27c79b2018-03-12 17:08:42 +010063 /ns_instances_content O O
tierno2236d202018-05-16 19:05:16 +020064 /<nsInstanceId> O O
tiernof27c79b2018-03-12 17:08:42 +010065 /ns_instances 5 5
tierno95692442018-05-24 18:05:28 +020066 /<nsInstanceId> O5 O5
tierno65acb4d2018-04-06 16:42:40 +020067 instantiate O5
68 terminate O5
69 action O
70 scale O5
71 heal 5
tiernoc94c3df2018-02-09 15:38:54 +010072 /ns_lcm_op_occs 5 5
73 /<nsLcmOpOccId> 5 5 5
74 TO BE COMPLETED 5 5
tiernof759d822018-06-11 18:54:54 +020075 /vnf_instances (also vnfrs for compatibility) O
76 /<vnfInstanceId> O
tiernof27c79b2018-03-12 17:08:42 +010077 /subscriptions 5 5
78 /<subscriptionId> 5 X
garciadeblas9750c5a2018-10-15 16:20:35 +020079
tiernocb83c942018-09-24 17:28:13 +020080 /pdu/v1
81 /pdu_descriptor O O
82 /<id> O O O O
garciadeblas9750c5a2018-10-15 16:20:35 +020083
tiernof27c79b2018-03-12 17:08:42 +010084 /admin/v1
85 /tokens O O
tierno2236d202018-05-16 19:05:16 +020086 /<id> O O
tiernof27c79b2018-03-12 17:08:42 +010087 /users O O
tiernocd54a4a2018-09-12 16:40:35 +020088 /<id> O O O O
tiernof27c79b2018-03-12 17:08:42 +010089 /projects O O
tierno2236d202018-05-16 19:05:16 +020090 /<id> O O
tierno09c073e2018-04-26 13:36:48 +020091 /vims_accounts (also vims for compatibility) O O
tierno2236d202018-05-16 19:05:16 +020092 /<id> O O O
tierno0f98af52018-03-19 10:28:22 +010093 /sdns O O
tierno2236d202018-05-16 19:05:16 +020094 /<id> O O O
tiernoc94c3df2018-02-09 15:38:54 +010095
garciadeblas9750c5a2018-10-15 16:20:35 +020096 /nst/v1 O O
97 /netslice_templates_content O O
98 /<nstInfoId> O O O O
99 /netslice_templates O O
100 /<nstInfoId> O O O
101 /nst_content O O
102 /nst O
103 /artifacts[/<artifactPath>] O
104 /subscriptions X X
105 /<subscriptionId> X X
106
107 /nsilcm/v1
108 /netslice_instances_content O O
109 /<SliceInstanceId> O O
110 /netslice_instances O O
111 /<SliceInstanceId> O O
112 instantiate O
113 terminate O
114 action O
115 /nsi_lcm_op_occs O O
116 /<nsiLcmOpOccId> O O O
117 /subscriptions X X
118 /<subscriptionId> X X
119
tierno2236d202018-05-16 19:05:16 +0200120query string:
121 Follows SOL005 section 4.3.2 It contains extra METHOD to override http method, FORCE to force.
tierno36ec8602018-11-02 17:27:11 +0100122 simpleFilterExpr := <attrName>["."<attrName>]*["."<op>]"="<value>[","<value>]*
123 filterExpr := <simpleFilterExpr>["&"<simpleFilterExpr>]*
124 op := "eq" | "neq" (or "ne") | "gt" | "lt" | "gte" | "lte" | "cont" | "ncont"
125 attrName := string
tierno2236d202018-05-16 19:05:16 +0200126 For filtering inside array, it must select the element of the array, or add ANYINDEX to apply the filtering over any
127 item of the array, that is, pass if any item of the array pass the filter.
128 It allows both ne and neq for not equal
129 TODO: 4.3.3 Attribute selectors
130 all_fields, fields=x,y,.., exclude_default, exclude_fields=x,y,...
tiernoc94c3df2018-02-09 15:38:54 +0100131 (none) … same as “exclude_default”
132 all_fields … all attributes.
tierno2236d202018-05-16 19:05:16 +0200133 fields=<list> … all attributes except all complex attributes with minimum cardinality of zero that are not
134 conditionally mandatory, and that are not provided in <list>.
135 exclude_fields=<list> … all attributes except those complex attributes with a minimum cardinality of zero that
136 are not conditionally mandatory, and that are provided in <list>.
137 exclude_default … all attributes except those complex attributes with a minimum cardinality of zero that are not
138 conditionally mandatory, and that are part of the "default exclude set" defined in the present specification for
139 the particular resource
140 exclude_default and include=<list> … all attributes except those complex attributes with a minimum cardinality
141 of zero that are not conditionally mandatory and that are part of the "default exclude set" defined in the
142 present specification for the particular resource, but that are not part of <list>
tiernoc94c3df2018-02-09 15:38:54 +0100143Header field name Reference Example Descriptions
144 Accept IETF RFC 7231 [19] application/json Content-Types that are acceptable for the response.
145 This header field shall be present if the response is expected to have a non-empty message body.
146 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the request.
147 This header field shall be present if the request has a non-empty message body.
tierno2236d202018-05-16 19:05:16 +0200148 Authorization IETF RFC 7235 [22] Bearer mF_9.B5f-4.1JqM The authorization token for the request.
149 Details are specified in clause 4.5.3.
tiernoc94c3df2018-02-09 15:38:54 +0100150 Range IETF RFC 7233 [21] 1000-2000 Requested range of bytes from a file
151Header field name Reference Example Descriptions
152 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the response.
153 This header field shall be present if the response has a non-empty message body.
tierno2236d202018-05-16 19:05:16 +0200154 Location IETF RFC 7231 [19] http://www.example.com/vnflcm/v1/vnf_instances/123 Used in redirection, or when a
155 new resource has been created.
tiernoc94c3df2018-02-09 15:38:54 +0100156 This header field shall be present if the response status code is 201 or 3xx.
tierno2236d202018-05-16 19:05:16 +0200157 In the present document this header field is also used if the response status code is 202 and a new resource was
158 created.
159 WWW-Authenticate IETF RFC 7235 [22] Bearer realm="example" Challenge if the corresponding HTTP request has not
160 provided authorization, or error details if the corresponding HTTP request has provided an invalid authorization
161 token.
162 Accept-Ranges IETF RFC 7233 [21] bytes Used by the Server to signal whether or not it supports ranges for
163 certain resources.
164 Content-Range IETF RFC 7233 [21] bytes 21010-47021/ 47022 Signals the byte range that is contained in the
165 response, and the total length of the file.
tiernoc94c3df2018-02-09 15:38:54 +0100166 Retry-After IETF RFC 7231 [19] Fri, 31 Dec 1999 23:59:59 GMT
tiernoc94c3df2018-02-09 15:38:54 +0100167"""
168
169
170class NbiException(Exception):
171
172 def __init__(self, message, http_code=HTTPStatus.METHOD_NOT_ALLOWED):
173 Exception.__init__(self, message)
174 self.http_code = http_code
175
176
177class Server(object):
178 instance = 0
179 # to decode bytes to str
180 reader = getreader("utf-8")
181
182 def __init__(self):
183 self.instance += 1
184 self.engine = Engine()
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100185 self.authenticator = Authenticator()
tiernof27c79b2018-03-12 17:08:42 +0100186 self.valid_methods = { # contains allowed URL and methods
187 "admin": {
188 "v1": {
tierno0f98af52018-03-19 10:28:22 +0100189 "tokens": {"METHODS": ("GET", "POST", "DELETE"),
tierno2236d202018-05-16 19:05:16 +0200190 "<ID>": {"METHODS": ("GET", "DELETE")}
191 },
tierno0f98af52018-03-19 10:28:22 +0100192 "users": {"METHODS": ("GET", "POST"),
tiernocd54a4a2018-09-12 16:40:35 +0200193 "<ID>": {"METHODS": ("GET", "POST", "DELETE", "PATCH", "PUT")}
tierno2236d202018-05-16 19:05:16 +0200194 },
tierno0f98af52018-03-19 10:28:22 +0100195 "projects": {"METHODS": ("GET", "POST"),
tierno2236d202018-05-16 19:05:16 +0200196 "<ID>": {"METHODS": ("GET", "DELETE")}
197 },
tierno0f98af52018-03-19 10:28:22 +0100198 "vims": {"METHODS": ("GET", "POST"),
tierno7ae10112018-05-18 14:36:02 +0200199 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
tierno2236d202018-05-16 19:05:16 +0200200 },
tierno09c073e2018-04-26 13:36:48 +0200201 "vim_accounts": {"METHODS": ("GET", "POST"),
tierno7ae10112018-05-18 14:36:02 +0200202 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
tierno2236d202018-05-16 19:05:16 +0200203 },
tierno0f98af52018-03-19 10:28:22 +0100204 "sdns": {"METHODS": ("GET", "POST"),
tierno7ae10112018-05-18 14:36:02 +0200205 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
tierno2236d202018-05-16 19:05:16 +0200206 },
tiernof27c79b2018-03-12 17:08:42 +0100207 }
208 },
tiernocb83c942018-09-24 17:28:13 +0200209 "pdu": {
210 "v1": {
211 "pdu_descriptors": {"METHODS": ("GET", "POST"),
212 "<ID>": {"METHODS": ("GET", "POST", "DELETE", "PATCH", "PUT")}
213 },
214 }
215 },
tiernof27c79b2018-03-12 17:08:42 +0100216 "nsd": {
217 "v1": {
tierno2236d202018-05-16 19:05:16 +0200218 "ns_descriptors_content": {"METHODS": ("GET", "POST"),
219 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
220 },
221 "ns_descriptors": {"METHODS": ("GET", "POST"),
tierno36ec8602018-11-02 17:27:11 +0100222 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"),
tierno2236d202018-05-16 19:05:16 +0200223 "nsd_content": {"METHODS": ("GET", "PUT")},
224 "nsd": {"METHODS": "GET"}, # descriptor inside package
225 "artifacts": {"*": {"METHODS": "GET"}}
226 }
227 },
tiernof27c79b2018-03-12 17:08:42 +0100228 "pnf_descriptors": {"TODO": ("GET", "POST"),
tierno2236d202018-05-16 19:05:16 +0200229 "<ID>": {"TODO": ("GET", "DELETE", "PATCH"),
230 "pnfd_content": {"TODO": ("GET", "PUT")}
231 }
232 },
tiernof27c79b2018-03-12 17:08:42 +0100233 "subscriptions": {"TODO": ("GET", "POST"),
tierno2236d202018-05-16 19:05:16 +0200234 "<ID>": {"TODO": ("GET", "DELETE")}
235 },
tiernof27c79b2018-03-12 17:08:42 +0100236 }
237 },
238 "vnfpkgm": {
239 "v1": {
tierno2236d202018-05-16 19:05:16 +0200240 "vnf_packages_content": {"METHODS": ("GET", "POST"),
241 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
242 },
243 "vnf_packages": {"METHODS": ("GET", "POST"),
tiernoaae4dc42018-06-11 18:55:50 +0200244 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"), # GET: vnfPkgInfo
tierno2236d202018-05-16 19:05:16 +0200245 "package_content": {"METHODS": ("GET", "PUT"), # package
246 "upload_from_uri": {"TODO": "POST"}
247 },
248 "vnfd": {"METHODS": "GET"}, # descriptor inside package
249 "artifacts": {"*": {"METHODS": "GET"}}
250 }
251 },
tiernof27c79b2018-03-12 17:08:42 +0100252 "subscriptions": {"TODO": ("GET", "POST"),
tierno2236d202018-05-16 19:05:16 +0200253 "<ID>": {"TODO": ("GET", "DELETE")}
254 },
tiernof27c79b2018-03-12 17:08:42 +0100255 }
256 },
257 "nslcm": {
258 "v1": {
259 "ns_instances_content": {"METHODS": ("GET", "POST"),
tierno2236d202018-05-16 19:05:16 +0200260 "<ID>": {"METHODS": ("GET", "DELETE")}
261 },
tierno65acb4d2018-04-06 16:42:40 +0200262 "ns_instances": {"METHODS": ("GET", "POST"),
tierno95692442018-05-24 18:05:28 +0200263 "<ID>": {"METHODS": ("GET", "DELETE"),
tiernof759d822018-06-11 18:54:54 +0200264 "scale": {"METHODS": "POST"},
tierno2236d202018-05-16 19:05:16 +0200265 "terminate": {"METHODS": "POST"},
266 "instantiate": {"METHODS": "POST"},
267 "action": {"METHODS": "POST"},
268 }
269 },
tierno65acb4d2018-04-06 16:42:40 +0200270 "ns_lcm_op_occs": {"METHODS": "GET",
tierno2236d202018-05-16 19:05:16 +0200271 "<ID>": {"METHODS": "GET"},
272 },
tierno0ffaa992018-05-09 13:21:56 +0200273 "vnfrs": {"METHODS": ("GET"),
tierno2236d202018-05-16 19:05:16 +0200274 "<ID>": {"METHODS": ("GET")}
275 },
tiernof759d822018-06-11 18:54:54 +0200276 "vnf_instances": {"METHODS": ("GET"),
277 "<ID>": {"METHODS": ("GET")}
278 },
tiernof27c79b2018-03-12 17:08:42 +0100279 }
280 },
garciadeblas9750c5a2018-10-15 16:20:35 +0200281 "nst": {
282 "v1": {
283 "netslice_templates_content": {"METHODS": ("GET", "POST"),
284 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
285 },
286 "netslice_templates": {"METHODS": ("GET", "POST"),
287 "<ID>": {"METHODS": ("GET", "DELETE"), "TODO": "PATCH",
288 "nst_content": {"METHODS": ("GET", "PUT")},
289 "nst": {"METHODS": "GET"}, # descriptor inside package
290 "artifacts": {"*": {"METHODS": "GET"}}
291 }
292 },
293 "subscriptions": {"TODO": ("GET", "POST"),
294 "<ID>": {"TODO": ("GET", "DELETE")}
295 },
296 }
297 },
298 "nsilcm": {
299 "v1": {
300 "netslice_instances_content": {"METHODS": ("GET", "POST"),
301 "<ID>": {"METHODS": ("GET", "DELETE")}
302 },
303 "netslice_instances": {"METHODS": ("GET", "POST"),
304 "<ID>": {"METHODS": ("GET", "DELETE"),
305 "terminate": {"METHODS": "POST"},
306 "instantiate": {"METHODS": "POST"},
307 "action": {"METHODS": "POST"},
308 }
309 },
310 "nsi_lcm_op_occs": {"METHODS": "GET",
311 "<ID>": {"METHODS": "GET"},
312 },
313 }
314 },
tiernof27c79b2018-03-12 17:08:42 +0100315 }
tiernoc94c3df2018-02-09 15:38:54 +0100316
tiernoc94c3df2018-02-09 15:38:54 +0100317 def _format_in(self, kwargs):
318 try:
319 indata = None
320 if cherrypy.request.body.length:
321 error_text = "Invalid input format "
322
323 if "Content-Type" in cherrypy.request.headers:
324 if "application/json" in cherrypy.request.headers["Content-Type"]:
325 error_text = "Invalid json format "
326 indata = json.load(self.reader(cherrypy.request.body))
gcalvinode4adfe2018-10-30 11:46:09 +0100327 cherrypy.request.headers.pop("Content-File-MD5", None)
tiernoc94c3df2018-02-09 15:38:54 +0100328 elif "application/yaml" in cherrypy.request.headers["Content-Type"]:
329 error_text = "Invalid yaml format "
330 indata = yaml.load(cherrypy.request.body)
gcalvinode4adfe2018-10-30 11:46:09 +0100331 cherrypy.request.headers.pop("Content-File-MD5", None)
tiernoc94c3df2018-02-09 15:38:54 +0100332 elif "application/binary" in cherrypy.request.headers["Content-Type"] or \
333 "application/gzip" in cherrypy.request.headers["Content-Type"] or \
tiernof27c79b2018-03-12 17:08:42 +0100334 "application/zip" in cherrypy.request.headers["Content-Type"] or \
335 "text/plain" in cherrypy.request.headers["Content-Type"]:
336 indata = cherrypy.request.body # .read()
tiernoc94c3df2018-02-09 15:38:54 +0100337 elif "multipart/form-data" in cherrypy.request.headers["Content-Type"]:
338 if "descriptor_file" in kwargs:
339 filecontent = kwargs.pop("descriptor_file")
340 if not filecontent.file:
341 raise NbiException("empty file or content", HTTPStatus.BAD_REQUEST)
tiernof27c79b2018-03-12 17:08:42 +0100342 indata = filecontent.file # .read()
tiernoc94c3df2018-02-09 15:38:54 +0100343 if filecontent.content_type.value:
344 cherrypy.request.headers["Content-Type"] = filecontent.content_type.value
345 else:
346 # raise cherrypy.HTTPError(HTTPStatus.Not_Acceptable,
347 # "Only 'Content-Type' of type 'application/json' or
348 # 'application/yaml' for input format are available")
349 error_text = "Invalid yaml format "
350 indata = yaml.load(cherrypy.request.body)
gcalvinode4adfe2018-10-30 11:46:09 +0100351 cherrypy.request.headers.pop("Content-File-MD5", None)
tiernoc94c3df2018-02-09 15:38:54 +0100352 else:
353 error_text = "Invalid yaml format "
354 indata = yaml.load(cherrypy.request.body)
gcalvinode4adfe2018-10-30 11:46:09 +0100355 cherrypy.request.headers.pop("Content-File-MD5", None)
tiernoc94c3df2018-02-09 15:38:54 +0100356 if not indata:
357 indata = {}
358
tiernoc94c3df2018-02-09 15:38:54 +0100359 format_yaml = False
360 if cherrypy.request.headers.get("Query-String-Format") == "yaml":
361 format_yaml = True
362
363 for k, v in kwargs.items():
364 if isinstance(v, str):
365 if v == "":
366 kwargs[k] = None
367 elif format_yaml:
368 try:
369 kwargs[k] = yaml.load(v)
tiernoe1281182018-05-22 12:24:36 +0200370 except Exception:
tiernoc94c3df2018-02-09 15:38:54 +0100371 pass
372 elif k.endswith(".gt") or k.endswith(".lt") or k.endswith(".gte") or k.endswith(".lte"):
373 try:
374 kwargs[k] = int(v)
tiernoe1281182018-05-22 12:24:36 +0200375 except Exception:
tiernoc94c3df2018-02-09 15:38:54 +0100376 try:
377 kwargs[k] = float(v)
tiernoe1281182018-05-22 12:24:36 +0200378 except Exception:
tiernoc94c3df2018-02-09 15:38:54 +0100379 pass
380 elif v.find(",") > 0:
381 kwargs[k] = v.split(",")
382 elif isinstance(v, (list, tuple)):
383 for index in range(0, len(v)):
384 if v[index] == "":
385 v[index] = None
386 elif format_yaml:
387 try:
388 v[index] = yaml.load(v[index])
tiernoe1281182018-05-22 12:24:36 +0200389 except Exception:
tiernoc94c3df2018-02-09 15:38:54 +0100390 pass
391
tiernof27c79b2018-03-12 17:08:42 +0100392 return indata
tiernoc94c3df2018-02-09 15:38:54 +0100393 except (ValueError, yaml.YAMLError) as exc:
394 raise NbiException(error_text + str(exc), HTTPStatus.BAD_REQUEST)
395 except KeyError as exc:
396 raise NbiException("Query string error: " + str(exc), HTTPStatus.BAD_REQUEST)
tiernob92094f2018-05-11 13:44:22 +0200397 except Exception as exc:
398 raise NbiException(error_text + str(exc), HTTPStatus.BAD_REQUEST)
tiernoc94c3df2018-02-09 15:38:54 +0100399
400 @staticmethod
tiernof27c79b2018-03-12 17:08:42 +0100401 def _format_out(data, session=None, _format=None):
tiernoc94c3df2018-02-09 15:38:54 +0100402 """
403 return string of dictionary data according to requested json, yaml, xml. By default json
tiernof27c79b2018-03-12 17:08:42 +0100404 :param data: response to be sent. Can be a dict, text or file
tiernoc94c3df2018-02-09 15:38:54 +0100405 :param session:
tiernof27c79b2018-03-12 17:08:42 +0100406 :param _format: The format to be set as Content-Type ir data is a file
tiernoc94c3df2018-02-09 15:38:54 +0100407 :return: None
408 """
tierno0f98af52018-03-19 10:28:22 +0100409 accept = cherrypy.request.headers.get("Accept")
tiernof27c79b2018-03-12 17:08:42 +0100410 if data is None:
tierno0f98af52018-03-19 10:28:22 +0100411 if accept and "text/html" in accept:
412 return html.format(data, cherrypy.request, cherrypy.response, session)
tierno09c073e2018-04-26 13:36:48 +0200413 # cherrypy.response.status = HTTPStatus.NO_CONTENT.value
tiernof27c79b2018-03-12 17:08:42 +0100414 return
415 elif hasattr(data, "read"): # file object
416 if _format:
417 cherrypy.response.headers["Content-Type"] = _format
418 elif "b" in data.mode: # binariy asssumig zip
419 cherrypy.response.headers["Content-Type"] = 'application/zip'
420 else:
421 cherrypy.response.headers["Content-Type"] = 'text/plain'
422 # TODO check that cherrypy close file. If not implement pending things to close per thread next
423 return data
tierno0f98af52018-03-19 10:28:22 +0100424 if accept:
tiernoc94c3df2018-02-09 15:38:54 +0100425 if "application/json" in accept:
426 cherrypy.response.headers["Content-Type"] = 'application/json; charset=utf-8'
427 a = json.dumps(data, indent=4) + "\n"
428 return a.encode("utf8")
429 elif "text/html" in accept:
430 return html.format(data, cherrypy.request, cherrypy.response, session)
431
tiernof27c79b2018-03-12 17:08:42 +0100432 elif "application/yaml" in accept or "*/*" in accept or "text/plain" in accept:
tiernoc94c3df2018-02-09 15:38:54 +0100433 pass
434 else:
435 raise cherrypy.HTTPError(HTTPStatus.NOT_ACCEPTABLE.value,
436 "Only 'Accept' of type 'application/json' or 'application/yaml' "
437 "for output format are available")
438 cherrypy.response.headers["Content-Type"] = 'application/yaml'
439 return yaml.safe_dump(data, explicit_start=True, indent=4, default_flow_style=False, tags=False,
440 encoding='utf-8', allow_unicode=True) # , canonical=True, default_style='"'
441
442 @cherrypy.expose
443 def index(self, *args, **kwargs):
444 session = None
445 try:
446 if cherrypy.request.method == "GET":
Eduardo Sousa2f988212018-07-26 01:04:11 +0100447 session = self.authenticator.authorize()
tiernoc94c3df2018-02-09 15:38:54 +0100448 outdata = "Index page"
449 else:
450 raise cherrypy.HTTPError(HTTPStatus.METHOD_NOT_ALLOWED.value,
tierno2236d202018-05-16 19:05:16 +0200451 "Method {} not allowed for tokens".format(cherrypy.request.method))
tiernoc94c3df2018-02-09 15:38:54 +0100452
453 return self._format_out(outdata, session)
454
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100455 except (EngineException, AuthException) as e:
tiernoc94c3df2018-02-09 15:38:54 +0100456 cherrypy.log("index Exception {}".format(e))
457 cherrypy.response.status = e.http_code.value
458 return self._format_out("Welcome to OSM!", session)
459
460 @cherrypy.expose
tierno55945e72018-04-06 16:40:27 +0200461 def version(self, *args, **kwargs):
tiernodfe09572018-04-24 10:41:10 +0200462 # TODO consider to remove and provide version using the static version file
tierno55945e72018-04-06 16:40:27 +0200463 global __version__, version_date
464 try:
465 if cherrypy.request.method != "GET":
466 raise NbiException("Only method GET is allowed", HTTPStatus.METHOD_NOT_ALLOWED)
467 elif args or kwargs:
468 raise NbiException("Invalid URL or query string for version", HTTPStatus.METHOD_NOT_ALLOWED)
469 return __version__ + " " + version_date
470 except NbiException as e:
471 cherrypy.response.status = e.http_code.value
472 problem_details = {
473 "code": e.http_code.name,
474 "status": e.http_code.value,
475 "detail": str(e),
476 }
477 return self._format_out(problem_details, None)
478
479 @cherrypy.expose
tiernof27c79b2018-03-12 17:08:42 +0100480 def token(self, method, token_id=None, kwargs=None):
tiernoc94c3df2018-02-09 15:38:54 +0100481 session = None
482 # self.engine.load_dbase(cherrypy.request.app.config)
tiernof27c79b2018-03-12 17:08:42 +0100483 indata = self._format_in(kwargs)
484 if not isinstance(indata, dict):
485 raise NbiException("Expected application/yaml or application/json Content-Type", HTTPStatus.BAD_REQUEST)
tiernoc94c3df2018-02-09 15:38:54 +0100486 try:
tiernoc94c3df2018-02-09 15:38:54 +0100487 if method == "GET":
Eduardo Sousa2f988212018-07-26 01:04:11 +0100488 session = self.authenticator.authorize()
tiernof27c79b2018-03-12 17:08:42 +0100489 if token_id:
Eduardo Sousa2f988212018-07-26 01:04:11 +0100490 outdata = self.authenticator.get_token(session, token_id)
tiernoc94c3df2018-02-09 15:38:54 +0100491 else:
Eduardo Sousa2f988212018-07-26 01:04:11 +0100492 outdata = self.authenticator.get_token_list(session)
tiernoc94c3df2018-02-09 15:38:54 +0100493 elif method == "POST":
494 try:
Eduardo Sousa2f988212018-07-26 01:04:11 +0100495 session = self.authenticator.authorize()
tiernoe1281182018-05-22 12:24:36 +0200496 except Exception:
tiernoc94c3df2018-02-09 15:38:54 +0100497 session = None
498 if kwargs:
499 indata.update(kwargs)
Eduardo Sousa2f988212018-07-26 01:04:11 +0100500 outdata = self.authenticator.new_token(session, indata, cherrypy.request.remote)
tiernoc94c3df2018-02-09 15:38:54 +0100501 session = outdata
502 cherrypy.session['Authorization'] = outdata["_id"]
tierno0f98af52018-03-19 10:28:22 +0100503 self._set_location_header("admin", "v1", "tokens", outdata["_id"])
tiernoc94c3df2018-02-09 15:38:54 +0100504 # cherrypy.response.cookie["Authorization"] = outdata["id"]
505 # cherrypy.response.cookie["Authorization"]['expires'] = 3600
506 elif method == "DELETE":
tiernof27c79b2018-03-12 17:08:42 +0100507 if not token_id and "id" in kwargs:
tiernoc94c3df2018-02-09 15:38:54 +0100508 token_id = kwargs["id"]
tiernof27c79b2018-03-12 17:08:42 +0100509 elif not token_id:
Eduardo Sousa2f988212018-07-26 01:04:11 +0100510 session = self.authenticator.authorize()
tiernoc94c3df2018-02-09 15:38:54 +0100511 token_id = session["_id"]
Eduardo Sousa2f988212018-07-26 01:04:11 +0100512 outdata = self.authenticator.del_token(token_id)
tiernoc94c3df2018-02-09 15:38:54 +0100513 session = None
514 cherrypy.session['Authorization'] = "logout"
515 # cherrypy.response.cookie["Authorization"] = token_id
516 # cherrypy.response.cookie["Authorization"]['expires'] = 0
517 else:
518 raise NbiException("Method {} not allowed for token".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
519 return self._format_out(outdata, session)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100520 except (NbiException, EngineException, DbException, AuthException) as e:
tiernoc94c3df2018-02-09 15:38:54 +0100521 cherrypy.log("tokens Exception {}".format(e))
522 cherrypy.response.status = e.http_code.value
523 problem_details = {
524 "code": e.http_code.name,
525 "status": e.http_code.value,
526 "detail": str(e),
527 }
528 return self._format_out(problem_details, session)
529
530 @cherrypy.expose
531 def test(self, *args, **kwargs):
532 thread_info = None
tiernof27c79b2018-03-12 17:08:42 +0100533 if args and args[0] == "help":
534 return "<html><pre>\ninit\nfile/<name> download file\ndb-clear/table\nprune\nlogin\nlogin2\n"\
tiernoe1281182018-05-22 12:24:36 +0200535 "sleep/<time>\nmessage/topic\n</pre></html>"
tiernof27c79b2018-03-12 17:08:42 +0100536
537 elif args and args[0] == "init":
tiernoc94c3df2018-02-09 15:38:54 +0100538 try:
539 # self.engine.load_dbase(cherrypy.request.app.config)
540 self.engine.create_admin()
541 return "Done. User 'admin', password 'admin' created"
542 except Exception:
543 cherrypy.response.status = HTTPStatus.FORBIDDEN.value
544 return self._format_out("Database already initialized")
tiernof27c79b2018-03-12 17:08:42 +0100545 elif args and args[0] == "file":
546 return cherrypy.lib.static.serve_file(cherrypy.tree.apps['/osm'].config["storage"]["path"] + "/" + args[1],
547 "text/plain", "attachment")
548 elif args and args[0] == "file2":
549 f_path = cherrypy.tree.apps['/osm'].config["storage"]["path"] + "/" + args[1]
550 f = open(f_path, "r")
551 cherrypy.response.headers["Content-type"] = "text/plain"
tiernof27c79b2018-03-12 17:08:42 +0100552 return f
tierno55945e72018-04-06 16:40:27 +0200553
tiernof27c79b2018-03-12 17:08:42 +0100554 elif len(args) == 2 and args[0] == "db-clear":
tiernob24258a2018-10-04 18:39:49 +0200555 return self.engine.db.del_list(args[1], kwargs)
tiernoc94c3df2018-02-09 15:38:54 +0100556 elif args and args[0] == "prune":
557 return self.engine.prune()
558 elif args and args[0] == "login":
559 if not cherrypy.request.headers.get("Authorization"):
560 cherrypy.response.headers["WWW-Authenticate"] = 'Basic realm="Access to OSM site", charset="UTF-8"'
561 cherrypy.response.status = HTTPStatus.UNAUTHORIZED.value
562 elif args and args[0] == "login2":
563 if not cherrypy.request.headers.get("Authorization"):
564 cherrypy.response.headers["WWW-Authenticate"] = 'Bearer realm="Access to OSM site"'
565 cherrypy.response.status = HTTPStatus.UNAUTHORIZED.value
566 elif args and args[0] == "sleep":
567 sleep_time = 5
568 try:
569 sleep_time = int(args[1])
570 except Exception:
571 cherrypy.response.status = HTTPStatus.FORBIDDEN.value
572 return self._format_out("Database already initialized")
573 thread_info = cherrypy.thread_data
574 print(thread_info)
575 time.sleep(sleep_time)
576 # thread_info
577 elif len(args) >= 2 and args[0] == "message":
tiernob24258a2018-10-04 18:39:49 +0200578 main_topic = args[1]
579 return_text = "<html><pre>{} ->\n".format(main_topic)
tiernoc94c3df2018-02-09 15:38:54 +0100580 try:
tierno55945e72018-04-06 16:40:27 +0200581 if cherrypy.request.method == 'POST':
582 to_send = yaml.load(cherrypy.request.body)
583 for k, v in to_send.items():
tiernob24258a2018-10-04 18:39:49 +0200584 self.engine.msg.write(main_topic, k, v)
tierno55945e72018-04-06 16:40:27 +0200585 return_text += " {}: {}\n".format(k, v)
586 elif cherrypy.request.method == 'GET':
587 for k, v in kwargs.items():
tiernob24258a2018-10-04 18:39:49 +0200588 self.engine.msg.write(main_topic, k, yaml.load(v))
tierno55945e72018-04-06 16:40:27 +0200589 return_text += " {}: {}\n".format(k, yaml.load(v))
tiernoc94c3df2018-02-09 15:38:54 +0100590 except Exception as e:
tierno55945e72018-04-06 16:40:27 +0200591 return_text += "Error: " + str(e)
592 return_text += "</pre></html>\n"
593 return return_text
tiernoc94c3df2018-02-09 15:38:54 +0100594
595 return_text = (
596 "<html><pre>\nheaders:\n args: {}\n".format(args) +
597 " kwargs: {}\n".format(kwargs) +
598 " headers: {}\n".format(cherrypy.request.headers) +
599 " path_info: {}\n".format(cherrypy.request.path_info) +
600 " query_string: {}\n".format(cherrypy.request.query_string) +
601 " session: {}\n".format(cherrypy.session) +
602 " cookie: {}\n".format(cherrypy.request.cookie) +
603 " method: {}\n".format(cherrypy.request.method) +
604 " session: {}\n".format(cherrypy.session.get('fieldname')) +
605 " body:\n")
606 return_text += " length: {}\n".format(cherrypy.request.body.length)
607 if cherrypy.request.body.length:
608 return_text += " content: {}\n".format(
609 str(cherrypy.request.body.read(int(cherrypy.request.headers.get('Content-Length', 0)))))
610 if thread_info:
611 return_text += "thread: {}\n".format(thread_info)
612 return_text += "</pre></html>"
613 return return_text
614
tiernof27c79b2018-03-12 17:08:42 +0100615 def _check_valid_url_method(self, method, *args):
616 if len(args) < 3:
tiernob24258a2018-10-04 18:39:49 +0200617 raise NbiException("URL must contain at least 'main_topic/version/topic'", HTTPStatus.METHOD_NOT_ALLOWED)
tiernof27c79b2018-03-12 17:08:42 +0100618
619 reference = self.valid_methods
620 for arg in args:
621 if arg is None:
622 break
623 if not isinstance(reference, dict):
624 raise NbiException("URL contains unexpected extra items '{}'".format(arg),
625 HTTPStatus.METHOD_NOT_ALLOWED)
626
627 if arg in reference:
628 reference = reference[arg]
629 elif "<ID>" in reference:
630 reference = reference["<ID>"]
631 elif "*" in reference:
632 reference = reference["*"]
633 break
634 else:
635 raise NbiException("Unexpected URL item {}".format(arg), HTTPStatus.METHOD_NOT_ALLOWED)
636 if "TODO" in reference and method in reference["TODO"]:
637 raise NbiException("Method {} not supported yet for this URL".format(method), HTTPStatus.NOT_IMPLEMENTED)
tierno2236d202018-05-16 19:05:16 +0200638 elif "METHODS" in reference and method not in reference["METHODS"]:
tiernof27c79b2018-03-12 17:08:42 +0100639 raise NbiException("Method {} not supported for this URL".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
640 return
641
642 @staticmethod
tiernob24258a2018-10-04 18:39:49 +0200643 def _set_location_header(main_topic, version, topic, id):
tiernof27c79b2018-03-12 17:08:42 +0100644 """
645 Insert response header Location with the URL of created item base on URL params
tiernob24258a2018-10-04 18:39:49 +0200646 :param main_topic:
tiernof27c79b2018-03-12 17:08:42 +0100647 :param version:
tiernob24258a2018-10-04 18:39:49 +0200648 :param topic:
tiernof27c79b2018-03-12 17:08:42 +0100649 :param id:
650 :return: None
651 """
652 # 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 +0200653 cherrypy.response.headers["Location"] = "/osm/{}/{}/{}/{}".format(main_topic, version, topic, id)
tiernof27c79b2018-03-12 17:08:42 +0100654 return
655
tiernoc94c3df2018-02-09 15:38:54 +0100656 @cherrypy.expose
tiernob24258a2018-10-04 18:39:49 +0200657 def default(self, main_topic=None, version=None, topic=None, _id=None, item=None, *args, **kwargs):
tiernoc94c3df2018-02-09 15:38:54 +0100658 session = None
tiernof27c79b2018-03-12 17:08:42 +0100659 outdata = None
660 _format = None
tierno0f98af52018-03-19 10:28:22 +0100661 method = "DONE"
tiernob24258a2018-10-04 18:39:49 +0200662 engine_topic = None
tiernodb9dc582018-06-20 17:27:29 +0200663 rollback = []
664 session = None
tiernoc94c3df2018-02-09 15:38:54 +0100665 try:
tiernob24258a2018-10-04 18:39:49 +0200666 if not main_topic or not version or not topic:
667 raise NbiException("URL must contain at least 'main_topic/version/topic'",
668 HTTPStatus.METHOD_NOT_ALLOWED)
tiernocd65be32018-11-16 16:34:49 +0100669 if main_topic not in ("admin", "vnfpkgm", "nsd", "nslcm", "pdu", "nst", "nsilcm"):
tiernob24258a2018-10-04 18:39:49 +0200670 raise NbiException("URL main_topic '{}' not supported".format(main_topic),
671 HTTPStatus.METHOD_NOT_ALLOWED)
tiernoc94c3df2018-02-09 15:38:54 +0100672 if version != 'v1':
673 raise NbiException("URL version '{}' not supported".format(version), HTTPStatus.METHOD_NOT_ALLOWED)
674
tiernof27c79b2018-03-12 17:08:42 +0100675 if kwargs and "METHOD" in kwargs and kwargs["METHOD"] in ("PUT", "POST", "DELETE", "GET", "PATCH"):
676 method = kwargs.pop("METHOD")
677 else:
678 method = cherrypy.request.method
tiernob92094f2018-05-11 13:44:22 +0200679 if kwargs and "FORCE" in kwargs:
680 force = kwargs.pop("FORCE")
681 else:
682 force = False
tiernob24258a2018-10-04 18:39:49 +0200683 self._check_valid_url_method(method, main_topic, version, topic, _id, item, *args)
tiernob24258a2018-10-04 18:39:49 +0200684 if main_topic == "admin" and topic == "tokens":
tiernof27c79b2018-03-12 17:08:42 +0100685 return self.token(method, _id, kwargs)
686
tiernoc94c3df2018-02-09 15:38:54 +0100687 # self.engine.load_dbase(cherrypy.request.app.config)
Eduardo Sousa2f988212018-07-26 01:04:11 +0100688 session = self.authenticator.authorize()
tiernof27c79b2018-03-12 17:08:42 +0100689 indata = self._format_in(kwargs)
tiernob24258a2018-10-04 18:39:49 +0200690 engine_topic = topic
691 if topic == "subscriptions":
692 engine_topic = main_topic + "_" + topic
693 if item:
694 engine_topic = item
tiernoc94c3df2018-02-09 15:38:54 +0100695
tiernob24258a2018-10-04 18:39:49 +0200696 if main_topic == "nsd":
697 engine_topic = "nsds"
698 elif main_topic == "vnfpkgm":
699 engine_topic = "vnfds"
700 elif main_topic == "nslcm":
701 engine_topic = "nsrs"
702 if topic == "ns_lcm_op_occs":
703 engine_topic = "nslcmops"
704 if topic == "vnfrs" or topic == "vnf_instances":
705 engine_topic = "vnfrs"
garciadeblas9750c5a2018-10-15 16:20:35 +0200706 elif main_topic == "nst":
707 engine_topic = "nsts"
708 elif main_topic == "nsilcm":
709 engine_topic = "nsis"
710 if topic == "nsi_lcm_op_occs":
Felipe Vicensb57758d2018-10-16 16:00:20 +0200711 engine_topic = "nsilcmops"
tiernob24258a2018-10-04 18:39:49 +0200712 elif main_topic == "pdu":
713 engine_topic = "pdus"
714 if engine_topic == "vims": # TODO this is for backward compatibility, it will remove in the future
715 engine_topic = "vim_accounts"
tiernoc94c3df2018-02-09 15:38:54 +0100716
717 if method == "GET":
Felipe Vicens07f31722018-10-29 15:16:44 +0100718 if item in ("nsd_content", "package_content", "artifacts", "vnfd", "nsd", "nst", "nst_content"):
garciadeblas9750c5a2018-10-15 16:20:35 +0200719 if item in ("vnfd", "nsd", "nst"):
tiernof27c79b2018-03-12 17:08:42 +0100720 path = "$DESCRIPTOR"
721 elif args:
722 path = args
tiernob24258a2018-10-04 18:39:49 +0200723 elif item == "artifacts":
tiernof27c79b2018-03-12 17:08:42 +0100724 path = ()
725 else:
726 path = None
tiernob24258a2018-10-04 18:39:49 +0200727 file, _format = self.engine.get_file(session, engine_topic, _id, path,
tierno2236d202018-05-16 19:05:16 +0200728 cherrypy.request.headers.get("Accept"))
tiernof27c79b2018-03-12 17:08:42 +0100729 outdata = file
730 elif not _id:
tiernob24258a2018-10-04 18:39:49 +0200731 outdata = self.engine.get_item_list(session, engine_topic, kwargs)
tiernoc94c3df2018-02-09 15:38:54 +0100732 else:
tiernob24258a2018-10-04 18:39:49 +0200733 outdata = self.engine.get_item(session, engine_topic, _id)
tiernof27c79b2018-03-12 17:08:42 +0100734 elif method == "POST":
garciadeblas9750c5a2018-10-15 16:20:35 +0200735 if topic in ("ns_descriptors_content", "vnf_packages_content", "netslice_templates_content"):
tiernof27c79b2018-03-12 17:08:42 +0100736 _id = cherrypy.request.headers.get("Transaction-Id")
737 if not _id:
tiernob24258a2018-10-04 18:39:49 +0200738 _id = self.engine.new_item(rollback, session, engine_topic, {}, None, cherrypy.request.headers,
tiernob92094f2018-05-11 13:44:22 +0200739 force=force)
tiernob24258a2018-10-04 18:39:49 +0200740 completed = self.engine.upload_content(session, engine_topic, _id, indata, kwargs,
741 cherrypy.request.headers, force=force)
tiernof27c79b2018-03-12 17:08:42 +0100742 if completed:
tiernob24258a2018-10-04 18:39:49 +0200743 self._set_location_header(main_topic, version, topic, _id)
tiernof27c79b2018-03-12 17:08:42 +0100744 else:
745 cherrypy.response.headers["Transaction-Id"] = _id
746 outdata = {"id": _id}
tiernob24258a2018-10-04 18:39:49 +0200747 elif topic == "ns_instances_content":
748 # creates NSR
749 _id = self.engine.new_item(rollback, session, engine_topic, indata, kwargs, force=force)
750 # creates nslcmop
751 indata["lcmOperationType"] = "instantiate"
752 indata["nsInstanceId"] = _id
753 self.engine.new_item(rollback, session, "nslcmops", indata, None)
754 self._set_location_header(main_topic, version, topic, _id)
tiernof27c79b2018-03-12 17:08:42 +0100755 outdata = {"id": _id}
tiernob24258a2018-10-04 18:39:49 +0200756 elif topic == "ns_instances" and item:
757 indata["lcmOperationType"] = item
758 indata["nsInstanceId"] = _id
759 _id = self.engine.new_item(rollback, session, "nslcmops", indata, kwargs)
760 self._set_location_header(main_topic, version, "ns_lcm_op_occs", _id)
tierno65acb4d2018-04-06 16:42:40 +0200761 outdata = {"id": _id}
762 cherrypy.response.status = HTTPStatus.ACCEPTED.value
garciadeblas9750c5a2018-10-15 16:20:35 +0200763 elif topic == "netslice_instances_content":
Felipe Vicens07f31722018-10-29 15:16:44 +0100764 # creates NetSlice_Instance_record (NSIR)
garciadeblas9750c5a2018-10-15 16:20:35 +0200765 _id = self.engine.new_item(rollback, session, engine_topic, indata, kwargs, force=force)
Felipe Vicens07f31722018-10-29 15:16:44 +0100766 self._set_location_header(main_topic, version, topic, _id)
garciadeblas9750c5a2018-10-15 16:20:35 +0200767 indata["lcmOperationType"] = "instantiate"
768 indata["nsiInstanceId"] = _id
Felipe Vicens07f31722018-10-29 15:16:44 +0100769 self.engine.new_item(rollback, session, "nsilcmops", indata, kwargs)
garciadeblas9750c5a2018-10-15 16:20:35 +0200770 outdata = {"id": _id}
Felipe Vicens07f31722018-10-29 15:16:44 +0100771
garciadeblas9750c5a2018-10-15 16:20:35 +0200772 elif topic == "netslice_instances" and item:
773 indata["lcmOperationType"] = item
774 indata["nsiInstanceId"] = _id
775 _id = self.engine.new_item(rollback, session, "nsilcmops", indata, kwargs)
776 self._set_location_header(main_topic, version, "nsi_lcm_op_occs", _id)
777 outdata = {"id": _id}
778 cherrypy.response.status = HTTPStatus.ACCEPTED.value
tiernof27c79b2018-03-12 17:08:42 +0100779 else:
tiernob24258a2018-10-04 18:39:49 +0200780 _id = self.engine.new_item(rollback, session, engine_topic, indata, kwargs,
781 cherrypy.request.headers, force=force)
782 self._set_location_header(main_topic, version, topic, _id)
tiernof27c79b2018-03-12 17:08:42 +0100783 outdata = {"id": _id}
tiernob24258a2018-10-04 18:39:49 +0200784 # TODO form NsdInfo when topic in ("ns_descriptors", "vnf_packages")
tiernof27c79b2018-03-12 17:08:42 +0100785 cherrypy.response.status = HTTPStatus.CREATED.value
tierno09c073e2018-04-26 13:36:48 +0200786
tiernoc94c3df2018-02-09 15:38:54 +0100787 elif method == "DELETE":
788 if not _id:
tiernob24258a2018-10-04 18:39:49 +0200789 outdata = self.engine.del_item_list(session, engine_topic, kwargs)
tierno09c073e2018-04-26 13:36:48 +0200790 cherrypy.response.status = HTTPStatus.OK.value
tiernoc94c3df2018-02-09 15:38:54 +0100791 else: # len(args) > 1
tiernob24258a2018-10-04 18:39:49 +0200792 if topic == "ns_instances_content" and not force:
793 nslcmop_desc = {
794 "lcmOperationType": "terminate",
795 "nsInstanceId": _id,
796 "autoremove": True
797 }
798 opp_id = self.engine.new_item(rollback, session, "nslcmops", nslcmop_desc, None)
tierno3ace63c2018-05-03 17:51:43 +0200799 outdata = {"_id": opp_id}
tierno09c073e2018-04-26 13:36:48 +0200800 cherrypy.response.status = HTTPStatus.ACCEPTED.value
garciadeblas9750c5a2018-10-15 16:20:35 +0200801 elif topic == "netslice_instances_content" and not force:
802 nsilcmop_desc = {
803 "lcmOperationType": "terminate",
804 "nsiInstanceId": _id,
805 "autoremove": True
806 }
807 opp_id = self.engine.new_item(rollback, session, "nsilcmops", nsilcmop_desc, None)
808 outdata = {"_id": opp_id}
809 cherrypy.response.status = HTTPStatus.ACCEPTED.value
tierno65acb4d2018-04-06 16:42:40 +0200810 else:
tiernob24258a2018-10-04 18:39:49 +0200811 self.engine.del_item(session, engine_topic, _id, force)
tierno09c073e2018-04-26 13:36:48 +0200812 cherrypy.response.status = HTTPStatus.NO_CONTENT.value
tiernob24258a2018-10-04 18:39:49 +0200813 if engine_topic in ("vim_accounts", "sdns"):
tierno09c073e2018-04-26 13:36:48 +0200814 cherrypy.response.status = HTTPStatus.ACCEPTED.value
815
tierno7ae10112018-05-18 14:36:02 +0200816 elif method in ("PUT", "PATCH"):
tiernocd54a4a2018-09-12 16:40:35 +0200817 outdata = None
tiernof27c79b2018-03-12 17:08:42 +0100818 if not indata and not kwargs:
tiernoc94c3df2018-02-09 15:38:54 +0100819 raise NbiException("Nothing to update. Provide payload and/or query string",
820 HTTPStatus.BAD_REQUEST)
garciadeblas9750c5a2018-10-15 16:20:35 +0200821 if item in ("nsd_content", "package_content", "nst_content") and method == "PUT":
tiernob24258a2018-10-04 18:39:49 +0200822 completed = self.engine.upload_content(session, engine_topic, _id, indata, kwargs,
823 cherrypy.request.headers, force=force)
tiernof27c79b2018-03-12 17:08:42 +0100824 if not completed:
825 cherrypy.response.headers["Transaction-Id"] = id
tiernof27c79b2018-03-12 17:08:42 +0100826 else:
tiernob24258a2018-10-04 18:39:49 +0200827 self.engine.edit_item(session, engine_topic, _id, indata, kwargs, force=force)
tiernocd54a4a2018-09-12 16:40:35 +0200828 cherrypy.response.status = HTTPStatus.NO_CONTENT.value
tiernoc94c3df2018-02-09 15:38:54 +0100829 else:
830 raise NbiException("Method {} not allowed".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
tiernof27c79b2018-03-12 17:08:42 +0100831 return self._format_out(outdata, session, _format)
tiernob24258a2018-10-04 18:39:49 +0200832 except Exception as e:
tierno36ec8602018-11-02 17:27:11 +0100833 if isinstance(e, (NbiException, EngineException, DbException, FsException, MsgException, AuthException,
834 ValidationError)):
tiernob24258a2018-10-04 18:39:49 +0200835 http_code_value = cherrypy.response.status = e.http_code.value
836 http_code_name = e.http_code.name
837 cherrypy.log("Exception {}".format(e))
838 else:
839 http_code_value = cherrypy.response.status = HTTPStatus.BAD_REQUEST.value # INTERNAL_SERVER_ERROR
840 cherrypy.log("CRITICAL: Exception {}".format(e))
841 http_code_name = HTTPStatus.BAD_REQUEST.name
tierno3ace63c2018-05-03 17:51:43 +0200842 if hasattr(outdata, "close"): # is an open file
843 outdata.close()
tiernob24258a2018-10-04 18:39:49 +0200844 error_text = str(e)
845 rollback.reverse()
tiernodb9dc582018-06-20 17:27:29 +0200846 for rollback_item in rollback:
tierno3ace63c2018-05-03 17:51:43 +0200847 try:
tiernocc103432018-10-19 14:10:35 +0200848 if rollback_item.get("operation") == "set":
849 self.engine.db.set_one(rollback_item["topic"], {"_id": rollback_item["_id"]},
850 rollback_item["content"], fail_on_empty=False)
851 else:
852 self.engine.del_item(**rollback_item, session=session, force=True)
tierno3ace63c2018-05-03 17:51:43 +0200853 except Exception as e2:
tiernob24258a2018-10-04 18:39:49 +0200854 rollback_error_text = "Rollback Exception {}: {}".format(rollback_item, e2)
855 cherrypy.log(rollback_error_text)
856 error_text += ". " + rollback_error_text
857 # if isinstance(e, MsgException):
858 # error_text = "{} has been '{}' but other modules cannot be informed because an error on bus".format(
859 # engine_topic[:-1], method, error_text)
tiernoc94c3df2018-02-09 15:38:54 +0100860 problem_details = {
tiernob24258a2018-10-04 18:39:49 +0200861 "code": http_code_name,
862 "status": http_code_value,
863 "detail": error_text,
tiernoc94c3df2018-02-09 15:38:54 +0100864 }
865 return self._format_out(problem_details, session)
866 # raise cherrypy.HTTPError(e.http_code.value, str(e))
867
868
869# def validate_password(realm, username, password):
870# cherrypy.log("realm "+ str(realm))
871# if username == "admin" and password == "admin":
872# return True
873# return False
874
875
876def _start_service():
877 """
878 Callback function called when cherrypy.engine starts
879 Override configuration with env variables
880 Set database, storage, message configuration
881 Init database with admin/admin user password
882 """
883 cherrypy.log.error("Starting osm_nbi")
884 # update general cherrypy configuration
885 update_dict = {}
886
887 engine_config = cherrypy.tree.apps['/osm'].config
888 for k, v in environ.items():
889 if not k.startswith("OSMNBI_"):
890 continue
tiernoe1281182018-05-22 12:24:36 +0200891 k1, _, k2 = k[7:].lower().partition("_")
tiernoc94c3df2018-02-09 15:38:54 +0100892 if not k2:
893 continue
894 try:
895 # update static configuration
896 if k == 'OSMNBI_STATIC_DIR':
897 engine_config["/static"]['tools.staticdir.dir'] = v
898 engine_config["/static"]['tools.staticdir.on'] = True
899 elif k == 'OSMNBI_SOCKET_PORT' or k == 'OSMNBI_SERVER_PORT':
900 update_dict['server.socket_port'] = int(v)
901 elif k == 'OSMNBI_SOCKET_HOST' or k == 'OSMNBI_SERVER_HOST':
902 update_dict['server.socket_host'] = v
tiernof5298be2018-05-16 14:43:57 +0200903 elif k1 in ("server", "test", "auth", "log"):
904 update_dict[k1 + '.' + k2] = v
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100905 elif k1 in ("message", "database", "storage", "authentication"):
tiernof5298be2018-05-16 14:43:57 +0200906 # k2 = k2.replace('_', '.')
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100907 if k2 in ("port", "db_port"):
tiernoc94c3df2018-02-09 15:38:54 +0100908 engine_config[k1][k2] = int(v)
909 else:
910 engine_config[k1][k2] = v
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100911
tiernoc94c3df2018-02-09 15:38:54 +0100912 except ValueError as e:
913 cherrypy.log.error("Ignoring environ '{}': " + str(e))
914 except Exception as e:
915 cherrypy.log.warn("skipping environ '{}' on exception '{}'".format(k, e))
916
917 if update_dict:
918 cherrypy.config.update(update_dict)
tiernof5298be2018-05-16 14:43:57 +0200919 engine_config["global"].update(update_dict)
tiernoc94c3df2018-02-09 15:38:54 +0100920
921 # logging cherrypy
922 log_format_simple = "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(message)s"
923 log_formatter_simple = logging.Formatter(log_format_simple, datefmt='%Y-%m-%dT%H:%M:%S')
924 logger_server = logging.getLogger("cherrypy.error")
925 logger_access = logging.getLogger("cherrypy.access")
926 logger_cherry = logging.getLogger("cherrypy")
927 logger_nbi = logging.getLogger("nbi")
928
tiernof5298be2018-05-16 14:43:57 +0200929 if "log.file" in engine_config["global"]:
930 file_handler = logging.handlers.RotatingFileHandler(engine_config["global"]["log.file"],
tiernoc94c3df2018-02-09 15:38:54 +0100931 maxBytes=100e6, backupCount=9, delay=0)
932 file_handler.setFormatter(log_formatter_simple)
933 logger_cherry.addHandler(file_handler)
934 logger_nbi.addHandler(file_handler)
tiernob24258a2018-10-04 18:39:49 +0200935 # log always to standard output
936 for format_, logger in {"nbi.server %(filename)s:%(lineno)s": logger_server,
937 "nbi.access %(filename)s:%(lineno)s": logger_access,
938 "%(name)s %(filename)s:%(lineno)s": logger_nbi
939 }.items():
940 log_format_cherry = "%(asctime)s %(levelname)s {} %(message)s".format(format_)
941 log_formatter_cherry = logging.Formatter(log_format_cherry, datefmt='%Y-%m-%dT%H:%M:%S')
942 str_handler = logging.StreamHandler()
943 str_handler.setFormatter(log_formatter_cherry)
944 logger.addHandler(str_handler)
tiernoc94c3df2018-02-09 15:38:54 +0100945
tiernof5298be2018-05-16 14:43:57 +0200946 if engine_config["global"].get("log.level"):
947 logger_cherry.setLevel(engine_config["global"]["log.level"])
948 logger_nbi.setLevel(engine_config["global"]["log.level"])
tiernoc94c3df2018-02-09 15:38:54 +0100949
950 # logging other modules
951 for k1, logname in {"message": "nbi.msg", "database": "nbi.db", "storage": "nbi.fs"}.items():
952 engine_config[k1]["logger_name"] = logname
953 logger_module = logging.getLogger(logname)
954 if "logfile" in engine_config[k1]:
955 file_handler = logging.handlers.RotatingFileHandler(engine_config[k1]["logfile"],
tierno2236d202018-05-16 19:05:16 +0200956 maxBytes=100e6, backupCount=9, delay=0)
tiernoc94c3df2018-02-09 15:38:54 +0100957 file_handler.setFormatter(log_formatter_simple)
958 logger_module.addHandler(file_handler)
959 if "loglevel" in engine_config[k1]:
960 logger_module.setLevel(engine_config[k1]["loglevel"])
961 # TODO add more entries, e.g.: storage
962 cherrypy.tree.apps['/osm'].root.engine.start(engine_config)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100963 cherrypy.tree.apps['/osm'].root.authenticator.start(engine_config)
tiernod985a8d2018-10-19 14:12:28 +0200964 cherrypy.tree.apps['/osm'].root.engine.init_db(target_version=database_version)
965 cherrypy.tree.apps['/osm'].root.authenticator.init_db(target_version=auth_database_version)
tiernoc94c3df2018-02-09 15:38:54 +0100966 # getenv('OSMOPENMANO_TENANT', None)
967
968
969def _stop_service():
970 """
971 Callback function called when cherrypy.engine stops
972 TODO: Ending database connections.
973 """
974 cherrypy.tree.apps['/osm'].root.engine.stop()
975 cherrypy.log.error("Stopping osm_nbi")
976
tierno2236d202018-05-16 19:05:16 +0200977
tiernof5298be2018-05-16 14:43:57 +0200978def nbi(config_file):
tiernoc94c3df2018-02-09 15:38:54 +0100979 # conf = {
980 # '/': {
981 # #'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
982 # 'tools.sessions.on': True,
983 # 'tools.response_headers.on': True,
984 # # 'tools.response_headers.headers': [('Content-Type', 'text/plain')],
985 # }
986 # }
987 # cherrypy.Server.ssl_module = 'builtin'
988 # cherrypy.Server.ssl_certificate = "http/cert.pem"
989 # cherrypy.Server.ssl_private_key = "http/privkey.pem"
990 # cherrypy.Server.thread_pool = 10
991 # cherrypy.config.update({'Server.socket_port': config["port"], 'Server.socket_host': config["host"]})
992
993 # cherrypy.config.update({'tools.auth_basic.on': True,
994 # 'tools.auth_basic.realm': 'localhost',
995 # 'tools.auth_basic.checkpassword': validate_password})
996 cherrypy.engine.subscribe('start', _start_service)
997 cherrypy.engine.subscribe('stop', _stop_service)
tiernof5298be2018-05-16 14:43:57 +0200998 cherrypy.quickstart(Server(), '/osm', config_file)
999
1000
1001def usage():
1002 print("""Usage: {} [options]
1003 -c|--config [configuration_file]: loads the configuration file (default: ./nbi.cfg)
1004 -h|--help: shows this help
1005 """.format(sys.argv[0]))
tierno2236d202018-05-16 19:05:16 +02001006 # --log-socket-host HOST: send logs to this host")
1007 # --log-socket-port PORT: send logs using this port (default: 9022)")
tiernoc94c3df2018-02-09 15:38:54 +01001008
1009
1010if __name__ == '__main__':
tiernof5298be2018-05-16 14:43:57 +02001011 try:
1012 # load parameters and configuration
1013 opts, args = getopt.getopt(sys.argv[1:], "hvc:", ["config=", "help"])
1014 # TODO add "log-socket-host=", "log-socket-port=", "log-file="
1015 config_file = None
1016 for o, a in opts:
1017 if o in ("-h", "--help"):
1018 usage()
1019 sys.exit()
1020 elif o in ("-c", "--config"):
1021 config_file = a
1022 # elif o == "--log-socket-port":
1023 # log_socket_port = a
1024 # elif o == "--log-socket-host":
1025 # log_socket_host = a
1026 # elif o == "--log-file":
1027 # log_file = a
1028 else:
1029 assert False, "Unhandled option"
1030 if config_file:
1031 if not path.isfile(config_file):
1032 print("configuration file '{}' that not exist".format(config_file), file=sys.stderr)
1033 exit(1)
1034 else:
1035 for config_file in (__file__[:__file__.rfind(".")] + ".cfg", "./nbi.cfg", "/etc/osm/nbi.cfg"):
1036 if path.isfile(config_file):
1037 break
1038 else:
1039 print("No configuration file 'nbi.cfg' found neither at local folder nor at /etc/osm/", file=sys.stderr)
1040 exit(1)
1041 nbi(config_file)
1042 except getopt.GetoptError as e:
1043 print(str(e), file=sys.stderr)
1044 # usage()
1045 exit(1)