blob: 46c7c9b5c21bbb78f711e4dd5858819f9ec43348 [file] [log] [blame]
tiernoc94c3df2018-02-09 15:38:54 +01001#!/usr/bin/python3
2# -*- coding: utf-8 -*-
3
tiernod125caf2018-11-22 16:05:54 +00004# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
13# implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
tiernoc94c3df2018-02-09 15:38:54 +010017import cherrypy
18import time
19import json
20import yaml
21import html_out as html
22import logging
tiernof5298be2018-05-16 14:43:57 +020023import logging.handlers
24import getopt
25import sys
Eduardo Sousa2f988212018-07-26 01:04:11 +010026
Eduardo Sousa819d34c2018-07-31 01:20:02 +010027from authconn import AuthException
Eduardo Sousa2f988212018-07-26 01:04:11 +010028from auth import Authenticator
tiernoc94c3df2018-02-09 15:38:54 +010029from engine import Engine, EngineException
tierno36ec8602018-11-02 17:27:11 +010030from validation import ValidationError
tiernoa8d63632018-05-10 13:12:32 +020031from osm_common.dbbase import DbException
32from osm_common.fsbase import FsException
33from osm_common.msgbase import MsgException
tiernoc94c3df2018-02-09 15:38:54 +010034from http import HTTPStatus
tiernoc94c3df2018-02-09 15:38:54 +010035from codecs import getreader
tiernof5298be2018-05-16 14:43:57 +020036from os import environ, path
tiernoc94c3df2018-02-09 15:38:54 +010037
38__author__ = "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
tiernodfe09572018-04-24 10:41:10 +020039
40# TODO consider to remove and provide version using the static version file
41__version__ = "0.1.3"
tierno55945e72018-04-06 16:40:27 +020042version_date = "Apr 2018"
tierno4a946e42018-04-12 17:48:49 +020043database_version = '1.0'
Eduardo Sousa819d34c2018-07-31 01:20:02 +010044auth_database_version = '1.0'
tiernoc94c3df2018-02-09 15:38:54 +010045
46"""
tiernof27c79b2018-03-12 17:08:42 +010047North Bound Interface (O: OSM specific; 5,X: SOL005 not implemented yet; O5: SOL005 implemented)
tiernoc94c3df2018-02-09 15:38:54 +010048URL: /osm GET POST PUT DELETE PATCH
garciadeblas9750c5a2018-10-15 16:20:35 +020049 /nsd/v1
tierno2236d202018-05-16 19:05:16 +020050 /ns_descriptors_content O O
51 /<nsdInfoId> O O O O
tiernoc94c3df2018-02-09 15:38:54 +010052 /ns_descriptors O5 O5
53 /<nsdInfoId> O5 O5 5
54 /nsd_content O5 O5
tiernof27c79b2018-03-12 17:08:42 +010055 /nsd O
56 /artifacts[/<artifactPath>] O
tiernoc94c3df2018-02-09 15:38:54 +010057 /pnf_descriptors 5 5
58 /<pnfdInfoId> 5 5 5
59 /pnfd_content 5 5
tiernof27c79b2018-03-12 17:08:42 +010060 /subscriptions 5 5
61 /<subscriptionId> 5 X
tiernoc94c3df2018-02-09 15:38:54 +010062
63 /vnfpkgm/v1
tierno55945e72018-04-06 16:40:27 +020064 /vnf_packages_content O O
tierno2236d202018-05-16 19:05:16 +020065 /<vnfPkgId> O O
tiernoc94c3df2018-02-09 15:38:54 +010066 /vnf_packages O5 O5
67 /<vnfPkgId> O5 O5 5
tiernoc94c3df2018-02-09 15:38:54 +010068 /package_content O5 O5
69 /upload_from_uri X
tiernof27c79b2018-03-12 17:08:42 +010070 /vnfd O5
71 /artifacts[/<artifactPath>] O5
72 /subscriptions X X
73 /<subscriptionId> X X
tiernoc94c3df2018-02-09 15:38:54 +010074
75 /nslcm/v1
tiernof27c79b2018-03-12 17:08:42 +010076 /ns_instances_content O O
tierno2236d202018-05-16 19:05:16 +020077 /<nsInstanceId> O O
tiernof27c79b2018-03-12 17:08:42 +010078 /ns_instances 5 5
tierno95692442018-05-24 18:05:28 +020079 /<nsInstanceId> O5 O5
tierno65acb4d2018-04-06 16:42:40 +020080 instantiate O5
81 terminate O5
82 action O
83 scale O5
84 heal 5
tiernoc94c3df2018-02-09 15:38:54 +010085 /ns_lcm_op_occs 5 5
86 /<nsLcmOpOccId> 5 5 5
87 TO BE COMPLETED 5 5
tiernof759d822018-06-11 18:54:54 +020088 /vnf_instances (also vnfrs for compatibility) O
89 /<vnfInstanceId> O
tiernof27c79b2018-03-12 17:08:42 +010090 /subscriptions 5 5
91 /<subscriptionId> 5 X
garciadeblas9750c5a2018-10-15 16:20:35 +020092
tiernocb83c942018-09-24 17:28:13 +020093 /pdu/v1
94 /pdu_descriptor O O
95 /<id> O O O O
garciadeblas9750c5a2018-10-15 16:20:35 +020096
tiernof27c79b2018-03-12 17:08:42 +010097 /admin/v1
98 /tokens O O
tierno2236d202018-05-16 19:05:16 +020099 /<id> O O
tiernof27c79b2018-03-12 17:08:42 +0100100 /users O O
tiernocd54a4a2018-09-12 16:40:35 +0200101 /<id> O O O O
tiernof27c79b2018-03-12 17:08:42 +0100102 /projects O O
tierno2236d202018-05-16 19:05:16 +0200103 /<id> O O
tierno55ba2e62018-12-11 17:22:22 +0000104 /vim_accounts (also vims for compatibility) O O
105 /<id> O O O
106 /wim_accounts O O
tierno2236d202018-05-16 19:05:16 +0200107 /<id> O O O
tierno0f98af52018-03-19 10:28:22 +0100108 /sdns O O
tierno2236d202018-05-16 19:05:16 +0200109 /<id> O O O
tiernoc94c3df2018-02-09 15:38:54 +0100110
garciadeblas9750c5a2018-10-15 16:20:35 +0200111 /nst/v1 O O
112 /netslice_templates_content O O
113 /<nstInfoId> O O O O
114 /netslice_templates O O
115 /<nstInfoId> O O O
116 /nst_content O O
117 /nst O
118 /artifacts[/<artifactPath>] O
119 /subscriptions X X
120 /<subscriptionId> X X
121
122 /nsilcm/v1
123 /netslice_instances_content O O
124 /<SliceInstanceId> O O
125 /netslice_instances O O
126 /<SliceInstanceId> O O
127 instantiate O
128 terminate O
129 action O
130 /nsi_lcm_op_occs O O
131 /<nsiLcmOpOccId> O O O
132 /subscriptions X X
133 /<subscriptionId> X X
134
tierno2236d202018-05-16 19:05:16 +0200135query string:
136 Follows SOL005 section 4.3.2 It contains extra METHOD to override http method, FORCE to force.
tierno36ec8602018-11-02 17:27:11 +0100137 simpleFilterExpr := <attrName>["."<attrName>]*["."<op>]"="<value>[","<value>]*
138 filterExpr := <simpleFilterExpr>["&"<simpleFilterExpr>]*
139 op := "eq" | "neq" (or "ne") | "gt" | "lt" | "gte" | "lte" | "cont" | "ncont"
140 attrName := string
tierno2236d202018-05-16 19:05:16 +0200141 For filtering inside array, it must select the element of the array, or add ANYINDEX to apply the filtering over any
142 item of the array, that is, pass if any item of the array pass the filter.
143 It allows both ne and neq for not equal
144 TODO: 4.3.3 Attribute selectors
145 all_fields, fields=x,y,.., exclude_default, exclude_fields=x,y,...
tiernoc94c3df2018-02-09 15:38:54 +0100146 (none) … same as “exclude_default”
147 all_fields … all attributes.
tierno2236d202018-05-16 19:05:16 +0200148 fields=<list> … all attributes except all complex attributes with minimum cardinality of zero that are not
149 conditionally mandatory, and that are not provided in <list>.
150 exclude_fields=<list> … all attributes except those complex attributes with a minimum cardinality of zero that
151 are not conditionally mandatory, and that are provided in <list>.
152 exclude_default … all attributes except those complex attributes with a minimum cardinality of zero that are not
153 conditionally mandatory, and that are part of the "default exclude set" defined in the present specification for
154 the particular resource
155 exclude_default and include=<list> … all attributes except those complex attributes with a minimum cardinality
156 of zero that are not conditionally mandatory and that are part of the "default exclude set" defined in the
157 present specification for the particular resource, but that are not part of <list>
tiernoc94c3df2018-02-09 15:38:54 +0100158Header field name Reference Example Descriptions
159 Accept IETF RFC 7231 [19] application/json Content-Types that are acceptable for the response.
160 This header field shall be present if the response is expected to have a non-empty message body.
161 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the request.
162 This header field shall be present if the request has a non-empty message body.
tierno2236d202018-05-16 19:05:16 +0200163 Authorization IETF RFC 7235 [22] Bearer mF_9.B5f-4.1JqM The authorization token for the request.
164 Details are specified in clause 4.5.3.
tiernoc94c3df2018-02-09 15:38:54 +0100165 Range IETF RFC 7233 [21] 1000-2000 Requested range of bytes from a file
166Header field name Reference Example Descriptions
167 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the response.
168 This header field shall be present if the response has a non-empty message body.
tierno2236d202018-05-16 19:05:16 +0200169 Location IETF RFC 7231 [19] http://www.example.com/vnflcm/v1/vnf_instances/123 Used in redirection, or when a
170 new resource has been created.
tiernoc94c3df2018-02-09 15:38:54 +0100171 This header field shall be present if the response status code is 201 or 3xx.
tierno2236d202018-05-16 19:05:16 +0200172 In the present document this header field is also used if the response status code is 202 and a new resource was
173 created.
174 WWW-Authenticate IETF RFC 7235 [22] Bearer realm="example" Challenge if the corresponding HTTP request has not
175 provided authorization, or error details if the corresponding HTTP request has provided an invalid authorization
176 token.
177 Accept-Ranges IETF RFC 7233 [21] bytes Used by the Server to signal whether or not it supports ranges for
178 certain resources.
179 Content-Range IETF RFC 7233 [21] bytes 21010-47021/ 47022 Signals the byte range that is contained in the
180 response, and the total length of the file.
tiernoc94c3df2018-02-09 15:38:54 +0100181 Retry-After IETF RFC 7231 [19] Fri, 31 Dec 1999 23:59:59 GMT
tiernoc94c3df2018-02-09 15:38:54 +0100182"""
183
184
185class NbiException(Exception):
186
187 def __init__(self, message, http_code=HTTPStatus.METHOD_NOT_ALLOWED):
188 Exception.__init__(self, message)
189 self.http_code = http_code
190
191
192class Server(object):
193 instance = 0
194 # to decode bytes to str
195 reader = getreader("utf-8")
196
197 def __init__(self):
198 self.instance += 1
199 self.engine = Engine()
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100200 self.authenticator = Authenticator()
tiernof27c79b2018-03-12 17:08:42 +0100201 self.valid_methods = { # contains allowed URL and methods
202 "admin": {
203 "v1": {
tierno0f98af52018-03-19 10:28:22 +0100204 "tokens": {"METHODS": ("GET", "POST", "DELETE"),
tierno2236d202018-05-16 19:05:16 +0200205 "<ID>": {"METHODS": ("GET", "DELETE")}
206 },
tierno0f98af52018-03-19 10:28:22 +0100207 "users": {"METHODS": ("GET", "POST"),
tiernocd54a4a2018-09-12 16:40:35 +0200208 "<ID>": {"METHODS": ("GET", "POST", "DELETE", "PATCH", "PUT")}
tierno2236d202018-05-16 19:05:16 +0200209 },
tierno0f98af52018-03-19 10:28:22 +0100210 "projects": {"METHODS": ("GET", "POST"),
tierno2236d202018-05-16 19:05:16 +0200211 "<ID>": {"METHODS": ("GET", "DELETE")}
212 },
tierno0f98af52018-03-19 10:28:22 +0100213 "vims": {"METHODS": ("GET", "POST"),
tierno7ae10112018-05-18 14:36:02 +0200214 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
tierno2236d202018-05-16 19:05:16 +0200215 },
tierno09c073e2018-04-26 13:36:48 +0200216 "vim_accounts": {"METHODS": ("GET", "POST"),
tierno7ae10112018-05-18 14:36:02 +0200217 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
tierno2236d202018-05-16 19:05:16 +0200218 },
tierno55ba2e62018-12-11 17:22:22 +0000219 "wim_accounts": {"METHODS": ("GET", "POST"),
220 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
221 },
tierno0f98af52018-03-19 10:28:22 +0100222 "sdns": {"METHODS": ("GET", "POST"),
tierno7ae10112018-05-18 14:36:02 +0200223 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
tierno2236d202018-05-16 19:05:16 +0200224 },
tiernof27c79b2018-03-12 17:08:42 +0100225 }
226 },
tiernocb83c942018-09-24 17:28:13 +0200227 "pdu": {
228 "v1": {
229 "pdu_descriptors": {"METHODS": ("GET", "POST"),
230 "<ID>": {"METHODS": ("GET", "POST", "DELETE", "PATCH", "PUT")}
231 },
232 }
233 },
tiernof27c79b2018-03-12 17:08:42 +0100234 "nsd": {
235 "v1": {
tierno2236d202018-05-16 19:05:16 +0200236 "ns_descriptors_content": {"METHODS": ("GET", "POST"),
237 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
238 },
239 "ns_descriptors": {"METHODS": ("GET", "POST"),
tierno36ec8602018-11-02 17:27:11 +0100240 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"),
tierno2236d202018-05-16 19:05:16 +0200241 "nsd_content": {"METHODS": ("GET", "PUT")},
242 "nsd": {"METHODS": "GET"}, # descriptor inside package
243 "artifacts": {"*": {"METHODS": "GET"}}
244 }
245 },
tiernof27c79b2018-03-12 17:08:42 +0100246 "pnf_descriptors": {"TODO": ("GET", "POST"),
tierno2236d202018-05-16 19:05:16 +0200247 "<ID>": {"TODO": ("GET", "DELETE", "PATCH"),
248 "pnfd_content": {"TODO": ("GET", "PUT")}
249 }
250 },
tiernof27c79b2018-03-12 17:08:42 +0100251 "subscriptions": {"TODO": ("GET", "POST"),
tierno2236d202018-05-16 19:05:16 +0200252 "<ID>": {"TODO": ("GET", "DELETE")}
253 },
tiernof27c79b2018-03-12 17:08:42 +0100254 }
255 },
256 "vnfpkgm": {
257 "v1": {
tierno2236d202018-05-16 19:05:16 +0200258 "vnf_packages_content": {"METHODS": ("GET", "POST"),
259 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
260 },
261 "vnf_packages": {"METHODS": ("GET", "POST"),
tiernoaae4dc42018-06-11 18:55:50 +0200262 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"), # GET: vnfPkgInfo
tierno2236d202018-05-16 19:05:16 +0200263 "package_content": {"METHODS": ("GET", "PUT"), # package
264 "upload_from_uri": {"TODO": "POST"}
265 },
266 "vnfd": {"METHODS": "GET"}, # descriptor inside package
267 "artifacts": {"*": {"METHODS": "GET"}}
268 }
269 },
tiernof27c79b2018-03-12 17:08:42 +0100270 "subscriptions": {"TODO": ("GET", "POST"),
tierno2236d202018-05-16 19:05:16 +0200271 "<ID>": {"TODO": ("GET", "DELETE")}
272 },
tiernof27c79b2018-03-12 17:08:42 +0100273 }
274 },
275 "nslcm": {
276 "v1": {
277 "ns_instances_content": {"METHODS": ("GET", "POST"),
tierno2236d202018-05-16 19:05:16 +0200278 "<ID>": {"METHODS": ("GET", "DELETE")}
279 },
tierno65acb4d2018-04-06 16:42:40 +0200280 "ns_instances": {"METHODS": ("GET", "POST"),
tierno95692442018-05-24 18:05:28 +0200281 "<ID>": {"METHODS": ("GET", "DELETE"),
tiernof759d822018-06-11 18:54:54 +0200282 "scale": {"METHODS": "POST"},
tierno2236d202018-05-16 19:05:16 +0200283 "terminate": {"METHODS": "POST"},
284 "instantiate": {"METHODS": "POST"},
285 "action": {"METHODS": "POST"},
286 }
287 },
tierno65acb4d2018-04-06 16:42:40 +0200288 "ns_lcm_op_occs": {"METHODS": "GET",
tierno2236d202018-05-16 19:05:16 +0200289 "<ID>": {"METHODS": "GET"},
290 },
tierno0ffaa992018-05-09 13:21:56 +0200291 "vnfrs": {"METHODS": ("GET"),
tierno2236d202018-05-16 19:05:16 +0200292 "<ID>": {"METHODS": ("GET")}
293 },
tiernof759d822018-06-11 18:54:54 +0200294 "vnf_instances": {"METHODS": ("GET"),
295 "<ID>": {"METHODS": ("GET")}
296 },
tiernof27c79b2018-03-12 17:08:42 +0100297 }
298 },
garciadeblas9750c5a2018-10-15 16:20:35 +0200299 "nst": {
300 "v1": {
301 "netslice_templates_content": {"METHODS": ("GET", "POST"),
302 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
303 },
304 "netslice_templates": {"METHODS": ("GET", "POST"),
305 "<ID>": {"METHODS": ("GET", "DELETE"), "TODO": "PATCH",
306 "nst_content": {"METHODS": ("GET", "PUT")},
307 "nst": {"METHODS": "GET"}, # descriptor inside package
308 "artifacts": {"*": {"METHODS": "GET"}}
309 }
310 },
311 "subscriptions": {"TODO": ("GET", "POST"),
312 "<ID>": {"TODO": ("GET", "DELETE")}
313 },
314 }
315 },
316 "nsilcm": {
317 "v1": {
318 "netslice_instances_content": {"METHODS": ("GET", "POST"),
319 "<ID>": {"METHODS": ("GET", "DELETE")}
320 },
321 "netslice_instances": {"METHODS": ("GET", "POST"),
322 "<ID>": {"METHODS": ("GET", "DELETE"),
323 "terminate": {"METHODS": "POST"},
324 "instantiate": {"METHODS": "POST"},
325 "action": {"METHODS": "POST"},
326 }
327 },
328 "nsi_lcm_op_occs": {"METHODS": "GET",
329 "<ID>": {"METHODS": "GET"},
330 },
331 }
332 },
tiernof27c79b2018-03-12 17:08:42 +0100333 }
tiernoc94c3df2018-02-09 15:38:54 +0100334
tiernoc94c3df2018-02-09 15:38:54 +0100335 def _format_in(self, kwargs):
336 try:
337 indata = None
338 if cherrypy.request.body.length:
339 error_text = "Invalid input format "
340
341 if "Content-Type" in cherrypy.request.headers:
342 if "application/json" in cherrypy.request.headers["Content-Type"]:
343 error_text = "Invalid json format "
344 indata = json.load(self.reader(cherrypy.request.body))
gcalvinode4adfe2018-10-30 11:46:09 +0100345 cherrypy.request.headers.pop("Content-File-MD5", None)
tiernoc94c3df2018-02-09 15:38:54 +0100346 elif "application/yaml" in cherrypy.request.headers["Content-Type"]:
347 error_text = "Invalid yaml format "
348 indata = yaml.load(cherrypy.request.body)
gcalvinode4adfe2018-10-30 11:46:09 +0100349 cherrypy.request.headers.pop("Content-File-MD5", None)
tiernoc94c3df2018-02-09 15:38:54 +0100350 elif "application/binary" in cherrypy.request.headers["Content-Type"] or \
351 "application/gzip" in cherrypy.request.headers["Content-Type"] or \
tiernof27c79b2018-03-12 17:08:42 +0100352 "application/zip" in cherrypy.request.headers["Content-Type"] or \
353 "text/plain" in cherrypy.request.headers["Content-Type"]:
354 indata = cherrypy.request.body # .read()
tiernoc94c3df2018-02-09 15:38:54 +0100355 elif "multipart/form-data" in cherrypy.request.headers["Content-Type"]:
356 if "descriptor_file" in kwargs:
357 filecontent = kwargs.pop("descriptor_file")
358 if not filecontent.file:
359 raise NbiException("empty file or content", HTTPStatus.BAD_REQUEST)
tiernof27c79b2018-03-12 17:08:42 +0100360 indata = filecontent.file # .read()
tiernoc94c3df2018-02-09 15:38:54 +0100361 if filecontent.content_type.value:
362 cherrypy.request.headers["Content-Type"] = filecontent.content_type.value
363 else:
364 # raise cherrypy.HTTPError(HTTPStatus.Not_Acceptable,
365 # "Only 'Content-Type' of type 'application/json' or
366 # 'application/yaml' for input format are available")
367 error_text = "Invalid yaml format "
368 indata = yaml.load(cherrypy.request.body)
gcalvinode4adfe2018-10-30 11:46:09 +0100369 cherrypy.request.headers.pop("Content-File-MD5", None)
tiernoc94c3df2018-02-09 15:38:54 +0100370 else:
371 error_text = "Invalid yaml format "
372 indata = yaml.load(cherrypy.request.body)
gcalvinode4adfe2018-10-30 11:46:09 +0100373 cherrypy.request.headers.pop("Content-File-MD5", None)
tiernoc94c3df2018-02-09 15:38:54 +0100374 if not indata:
375 indata = {}
376
tiernoc94c3df2018-02-09 15:38:54 +0100377 format_yaml = False
378 if cherrypy.request.headers.get("Query-String-Format") == "yaml":
379 format_yaml = True
380
381 for k, v in kwargs.items():
382 if isinstance(v, str):
383 if v == "":
384 kwargs[k] = None
385 elif format_yaml:
386 try:
387 kwargs[k] = yaml.load(v)
tiernoe1281182018-05-22 12:24:36 +0200388 except Exception:
tiernoc94c3df2018-02-09 15:38:54 +0100389 pass
390 elif k.endswith(".gt") or k.endswith(".lt") or k.endswith(".gte") or k.endswith(".lte"):
391 try:
392 kwargs[k] = int(v)
tiernoe1281182018-05-22 12:24:36 +0200393 except Exception:
tiernoc94c3df2018-02-09 15:38:54 +0100394 try:
395 kwargs[k] = float(v)
tiernoe1281182018-05-22 12:24:36 +0200396 except Exception:
tiernoc94c3df2018-02-09 15:38:54 +0100397 pass
398 elif v.find(",") > 0:
399 kwargs[k] = v.split(",")
400 elif isinstance(v, (list, tuple)):
401 for index in range(0, len(v)):
402 if v[index] == "":
403 v[index] = None
404 elif format_yaml:
405 try:
406 v[index] = yaml.load(v[index])
tiernoe1281182018-05-22 12:24:36 +0200407 except Exception:
tiernoc94c3df2018-02-09 15:38:54 +0100408 pass
409
tiernof27c79b2018-03-12 17:08:42 +0100410 return indata
tiernoc94c3df2018-02-09 15:38:54 +0100411 except (ValueError, yaml.YAMLError) as exc:
412 raise NbiException(error_text + str(exc), HTTPStatus.BAD_REQUEST)
413 except KeyError as exc:
414 raise NbiException("Query string error: " + str(exc), HTTPStatus.BAD_REQUEST)
tiernob92094f2018-05-11 13:44:22 +0200415 except Exception as exc:
416 raise NbiException(error_text + str(exc), HTTPStatus.BAD_REQUEST)
tiernoc94c3df2018-02-09 15:38:54 +0100417
418 @staticmethod
tiernof27c79b2018-03-12 17:08:42 +0100419 def _format_out(data, session=None, _format=None):
tiernoc94c3df2018-02-09 15:38:54 +0100420 """
421 return string of dictionary data according to requested json, yaml, xml. By default json
tiernof27c79b2018-03-12 17:08:42 +0100422 :param data: response to be sent. Can be a dict, text or file
tiernoc94c3df2018-02-09 15:38:54 +0100423 :param session:
tiernof27c79b2018-03-12 17:08:42 +0100424 :param _format: The format to be set as Content-Type ir data is a file
tiernoc94c3df2018-02-09 15:38:54 +0100425 :return: None
426 """
tierno0f98af52018-03-19 10:28:22 +0100427 accept = cherrypy.request.headers.get("Accept")
tiernof27c79b2018-03-12 17:08:42 +0100428 if data is None:
tierno0f98af52018-03-19 10:28:22 +0100429 if accept and "text/html" in accept:
430 return html.format(data, cherrypy.request, cherrypy.response, session)
tierno09c073e2018-04-26 13:36:48 +0200431 # cherrypy.response.status = HTTPStatus.NO_CONTENT.value
tiernof27c79b2018-03-12 17:08:42 +0100432 return
433 elif hasattr(data, "read"): # file object
434 if _format:
435 cherrypy.response.headers["Content-Type"] = _format
436 elif "b" in data.mode: # binariy asssumig zip
437 cherrypy.response.headers["Content-Type"] = 'application/zip'
438 else:
439 cherrypy.response.headers["Content-Type"] = 'text/plain'
440 # TODO check that cherrypy close file. If not implement pending things to close per thread next
441 return data
tierno0f98af52018-03-19 10:28:22 +0100442 if accept:
tiernoc94c3df2018-02-09 15:38:54 +0100443 if "application/json" in accept:
444 cherrypy.response.headers["Content-Type"] = 'application/json; charset=utf-8'
445 a = json.dumps(data, indent=4) + "\n"
446 return a.encode("utf8")
447 elif "text/html" in accept:
448 return html.format(data, cherrypy.request, cherrypy.response, session)
449
tiernof27c79b2018-03-12 17:08:42 +0100450 elif "application/yaml" in accept or "*/*" in accept or "text/plain" in accept:
tiernoc94c3df2018-02-09 15:38:54 +0100451 pass
tiernof717cbe2018-12-03 16:35:42 +0000452 # if there is not any valid accept, raise an error. But if response is already an error, format in yaml
453 elif cherrypy.response.status >= 400:
tiernoc94c3df2018-02-09 15:38:54 +0100454 raise cherrypy.HTTPError(HTTPStatus.NOT_ACCEPTABLE.value,
455 "Only 'Accept' of type 'application/json' or 'application/yaml' "
456 "for output format are available")
457 cherrypy.response.headers["Content-Type"] = 'application/yaml'
458 return yaml.safe_dump(data, explicit_start=True, indent=4, default_flow_style=False, tags=False,
459 encoding='utf-8', allow_unicode=True) # , canonical=True, default_style='"'
460
461 @cherrypy.expose
462 def index(self, *args, **kwargs):
463 session = None
464 try:
465 if cherrypy.request.method == "GET":
Eduardo Sousa2f988212018-07-26 01:04:11 +0100466 session = self.authenticator.authorize()
tiernoc94c3df2018-02-09 15:38:54 +0100467 outdata = "Index page"
468 else:
469 raise cherrypy.HTTPError(HTTPStatus.METHOD_NOT_ALLOWED.value,
tierno2236d202018-05-16 19:05:16 +0200470 "Method {} not allowed for tokens".format(cherrypy.request.method))
tiernoc94c3df2018-02-09 15:38:54 +0100471
472 return self._format_out(outdata, session)
473
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100474 except (EngineException, AuthException) as e:
tiernoc94c3df2018-02-09 15:38:54 +0100475 cherrypy.log("index Exception {}".format(e))
476 cherrypy.response.status = e.http_code.value
477 return self._format_out("Welcome to OSM!", session)
478
479 @cherrypy.expose
tierno55945e72018-04-06 16:40:27 +0200480 def version(self, *args, **kwargs):
tiernodfe09572018-04-24 10:41:10 +0200481 # TODO consider to remove and provide version using the static version file
tierno55945e72018-04-06 16:40:27 +0200482 global __version__, version_date
483 try:
484 if cherrypy.request.method != "GET":
485 raise NbiException("Only method GET is allowed", HTTPStatus.METHOD_NOT_ALLOWED)
486 elif args or kwargs:
487 raise NbiException("Invalid URL or query string for version", HTTPStatus.METHOD_NOT_ALLOWED)
488 return __version__ + " " + version_date
489 except NbiException as e:
490 cherrypy.response.status = e.http_code.value
491 problem_details = {
492 "code": e.http_code.name,
493 "status": e.http_code.value,
494 "detail": str(e),
495 }
496 return self._format_out(problem_details, None)
497
498 @cherrypy.expose
tiernof27c79b2018-03-12 17:08:42 +0100499 def token(self, method, token_id=None, kwargs=None):
tiernoc94c3df2018-02-09 15:38:54 +0100500 session = None
501 # self.engine.load_dbase(cherrypy.request.app.config)
tiernof27c79b2018-03-12 17:08:42 +0100502 indata = self._format_in(kwargs)
503 if not isinstance(indata, dict):
504 raise NbiException("Expected application/yaml or application/json Content-Type", HTTPStatus.BAD_REQUEST)
tiernoc94c3df2018-02-09 15:38:54 +0100505 try:
tiernoc94c3df2018-02-09 15:38:54 +0100506 if method == "GET":
Eduardo Sousa2f988212018-07-26 01:04:11 +0100507 session = self.authenticator.authorize()
tiernof27c79b2018-03-12 17:08:42 +0100508 if token_id:
Eduardo Sousa2f988212018-07-26 01:04:11 +0100509 outdata = self.authenticator.get_token(session, token_id)
tiernoc94c3df2018-02-09 15:38:54 +0100510 else:
Eduardo Sousa2f988212018-07-26 01:04:11 +0100511 outdata = self.authenticator.get_token_list(session)
tiernoc94c3df2018-02-09 15:38:54 +0100512 elif method == "POST":
513 try:
Eduardo Sousa2f988212018-07-26 01:04:11 +0100514 session = self.authenticator.authorize()
tiernoe1281182018-05-22 12:24:36 +0200515 except Exception:
tiernoc94c3df2018-02-09 15:38:54 +0100516 session = None
517 if kwargs:
518 indata.update(kwargs)
Eduardo Sousa2f988212018-07-26 01:04:11 +0100519 outdata = self.authenticator.new_token(session, indata, cherrypy.request.remote)
tiernoc94c3df2018-02-09 15:38:54 +0100520 session = outdata
521 cherrypy.session['Authorization'] = outdata["_id"]
tierno0f98af52018-03-19 10:28:22 +0100522 self._set_location_header("admin", "v1", "tokens", outdata["_id"])
tiernoc94c3df2018-02-09 15:38:54 +0100523 # cherrypy.response.cookie["Authorization"] = outdata["id"]
524 # cherrypy.response.cookie["Authorization"]['expires'] = 3600
525 elif method == "DELETE":
tiernof27c79b2018-03-12 17:08:42 +0100526 if not token_id and "id" in kwargs:
tiernoc94c3df2018-02-09 15:38:54 +0100527 token_id = kwargs["id"]
tiernof27c79b2018-03-12 17:08:42 +0100528 elif not token_id:
Eduardo Sousa2f988212018-07-26 01:04:11 +0100529 session = self.authenticator.authorize()
tiernoc94c3df2018-02-09 15:38:54 +0100530 token_id = session["_id"]
Eduardo Sousa2f988212018-07-26 01:04:11 +0100531 outdata = self.authenticator.del_token(token_id)
tiernoc94c3df2018-02-09 15:38:54 +0100532 session = None
533 cherrypy.session['Authorization'] = "logout"
534 # cherrypy.response.cookie["Authorization"] = token_id
535 # cherrypy.response.cookie["Authorization"]['expires'] = 0
536 else:
537 raise NbiException("Method {} not allowed for token".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
538 return self._format_out(outdata, session)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100539 except (NbiException, EngineException, DbException, AuthException) as e:
tiernoc94c3df2018-02-09 15:38:54 +0100540 cherrypy.log("tokens Exception {}".format(e))
541 cherrypy.response.status = e.http_code.value
542 problem_details = {
543 "code": e.http_code.name,
544 "status": e.http_code.value,
545 "detail": str(e),
546 }
547 return self._format_out(problem_details, session)
548
549 @cherrypy.expose
550 def test(self, *args, **kwargs):
551 thread_info = None
tiernof27c79b2018-03-12 17:08:42 +0100552 if args and args[0] == "help":
tiernof717cbe2018-12-03 16:35:42 +0000553 return "<html><pre>\ninit\nfile/<name> download file\ndb-clear/table\nfs-clear[/folder]\nlogin\nlogin2\n"\
tiernoe1281182018-05-22 12:24:36 +0200554 "sleep/<time>\nmessage/topic\n</pre></html>"
tiernof27c79b2018-03-12 17:08:42 +0100555
556 elif args and args[0] == "init":
tiernoc94c3df2018-02-09 15:38:54 +0100557 try:
558 # self.engine.load_dbase(cherrypy.request.app.config)
559 self.engine.create_admin()
560 return "Done. User 'admin', password 'admin' created"
561 except Exception:
562 cherrypy.response.status = HTTPStatus.FORBIDDEN.value
563 return self._format_out("Database already initialized")
tiernof27c79b2018-03-12 17:08:42 +0100564 elif args and args[0] == "file":
565 return cherrypy.lib.static.serve_file(cherrypy.tree.apps['/osm'].config["storage"]["path"] + "/" + args[1],
566 "text/plain", "attachment")
567 elif args and args[0] == "file2":
568 f_path = cherrypy.tree.apps['/osm'].config["storage"]["path"] + "/" + args[1]
569 f = open(f_path, "r")
570 cherrypy.response.headers["Content-type"] = "text/plain"
tiernof27c79b2018-03-12 17:08:42 +0100571 return f
tierno55945e72018-04-06 16:40:27 +0200572
tiernof27c79b2018-03-12 17:08:42 +0100573 elif len(args) == 2 and args[0] == "db-clear":
tiernof717cbe2018-12-03 16:35:42 +0000574 deleted_info = self.engine.db.del_list(args[1], kwargs)
575 return "{} {} deleted\n".format(deleted_info["deleted"], args[1])
576 elif len(args) and args[0] == "fs-clear":
577 if len(args) >= 2:
578 folders = (args[1],)
579 else:
580 folders = self.engine.fs.dir_ls(".")
581 for folder in folders:
582 self.engine.fs.file_delete(folder)
583 return ",".join(folders) + " folders deleted\n"
tiernoc94c3df2018-02-09 15:38:54 +0100584 elif args and args[0] == "login":
585 if not cherrypy.request.headers.get("Authorization"):
586 cherrypy.response.headers["WWW-Authenticate"] = 'Basic realm="Access to OSM site", charset="UTF-8"'
587 cherrypy.response.status = HTTPStatus.UNAUTHORIZED.value
588 elif args and args[0] == "login2":
589 if not cherrypy.request.headers.get("Authorization"):
590 cherrypy.response.headers["WWW-Authenticate"] = 'Bearer realm="Access to OSM site"'
591 cherrypy.response.status = HTTPStatus.UNAUTHORIZED.value
592 elif args and args[0] == "sleep":
593 sleep_time = 5
594 try:
595 sleep_time = int(args[1])
596 except Exception:
597 cherrypy.response.status = HTTPStatus.FORBIDDEN.value
598 return self._format_out("Database already initialized")
599 thread_info = cherrypy.thread_data
600 print(thread_info)
601 time.sleep(sleep_time)
602 # thread_info
603 elif len(args) >= 2 and args[0] == "message":
tiernob24258a2018-10-04 18:39:49 +0200604 main_topic = args[1]
605 return_text = "<html><pre>{} ->\n".format(main_topic)
tiernoc94c3df2018-02-09 15:38:54 +0100606 try:
tierno55945e72018-04-06 16:40:27 +0200607 if cherrypy.request.method == 'POST':
608 to_send = yaml.load(cherrypy.request.body)
609 for k, v in to_send.items():
tiernob24258a2018-10-04 18:39:49 +0200610 self.engine.msg.write(main_topic, k, v)
tierno55945e72018-04-06 16:40:27 +0200611 return_text += " {}: {}\n".format(k, v)
612 elif cherrypy.request.method == 'GET':
613 for k, v in kwargs.items():
tiernob24258a2018-10-04 18:39:49 +0200614 self.engine.msg.write(main_topic, k, yaml.load(v))
tierno55945e72018-04-06 16:40:27 +0200615 return_text += " {}: {}\n".format(k, yaml.load(v))
tiernoc94c3df2018-02-09 15:38:54 +0100616 except Exception as e:
tierno55945e72018-04-06 16:40:27 +0200617 return_text += "Error: " + str(e)
618 return_text += "</pre></html>\n"
619 return return_text
tiernoc94c3df2018-02-09 15:38:54 +0100620
621 return_text = (
622 "<html><pre>\nheaders:\n args: {}\n".format(args) +
623 " kwargs: {}\n".format(kwargs) +
624 " headers: {}\n".format(cherrypy.request.headers) +
625 " path_info: {}\n".format(cherrypy.request.path_info) +
626 " query_string: {}\n".format(cherrypy.request.query_string) +
627 " session: {}\n".format(cherrypy.session) +
628 " cookie: {}\n".format(cherrypy.request.cookie) +
629 " method: {}\n".format(cherrypy.request.method) +
tiernof717cbe2018-12-03 16:35:42 +0000630 " session: {}\n".format(cherrypy.session.get('fieldname')) +
tiernoc94c3df2018-02-09 15:38:54 +0100631 " body:\n")
632 return_text += " length: {}\n".format(cherrypy.request.body.length)
633 if cherrypy.request.body.length:
634 return_text += " content: {}\n".format(
635 str(cherrypy.request.body.read(int(cherrypy.request.headers.get('Content-Length', 0)))))
636 if thread_info:
637 return_text += "thread: {}\n".format(thread_info)
638 return_text += "</pre></html>"
639 return return_text
640
tiernof27c79b2018-03-12 17:08:42 +0100641 def _check_valid_url_method(self, method, *args):
642 if len(args) < 3:
tiernob24258a2018-10-04 18:39:49 +0200643 raise NbiException("URL must contain at least 'main_topic/version/topic'", HTTPStatus.METHOD_NOT_ALLOWED)
tiernof27c79b2018-03-12 17:08:42 +0100644
645 reference = self.valid_methods
646 for arg in args:
647 if arg is None:
648 break
649 if not isinstance(reference, dict):
650 raise NbiException("URL contains unexpected extra items '{}'".format(arg),
651 HTTPStatus.METHOD_NOT_ALLOWED)
652
653 if arg in reference:
654 reference = reference[arg]
655 elif "<ID>" in reference:
656 reference = reference["<ID>"]
657 elif "*" in reference:
658 reference = reference["*"]
659 break
660 else:
661 raise NbiException("Unexpected URL item {}".format(arg), HTTPStatus.METHOD_NOT_ALLOWED)
662 if "TODO" in reference and method in reference["TODO"]:
663 raise NbiException("Method {} not supported yet for this URL".format(method), HTTPStatus.NOT_IMPLEMENTED)
tierno2236d202018-05-16 19:05:16 +0200664 elif "METHODS" in reference and method not in reference["METHODS"]:
tiernof27c79b2018-03-12 17:08:42 +0100665 raise NbiException("Method {} not supported for this URL".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
666 return
667
668 @staticmethod
tiernob24258a2018-10-04 18:39:49 +0200669 def _set_location_header(main_topic, version, topic, id):
tiernof27c79b2018-03-12 17:08:42 +0100670 """
671 Insert response header Location with the URL of created item base on URL params
tiernob24258a2018-10-04 18:39:49 +0200672 :param main_topic:
tiernof27c79b2018-03-12 17:08:42 +0100673 :param version:
tiernob24258a2018-10-04 18:39:49 +0200674 :param topic:
tiernof27c79b2018-03-12 17:08:42 +0100675 :param id:
676 :return: None
677 """
678 # 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 +0200679 cherrypy.response.headers["Location"] = "/osm/{}/{}/{}/{}".format(main_topic, version, topic, id)
tiernof27c79b2018-03-12 17:08:42 +0100680 return
681
tiernoc94c3df2018-02-09 15:38:54 +0100682 @cherrypy.expose
tiernob24258a2018-10-04 18:39:49 +0200683 def default(self, main_topic=None, version=None, topic=None, _id=None, item=None, *args, **kwargs):
tiernoc94c3df2018-02-09 15:38:54 +0100684 session = None
tiernof27c79b2018-03-12 17:08:42 +0100685 outdata = None
686 _format = None
tierno0f98af52018-03-19 10:28:22 +0100687 method = "DONE"
tiernob24258a2018-10-04 18:39:49 +0200688 engine_topic = None
tiernodb9dc582018-06-20 17:27:29 +0200689 rollback = []
690 session = None
tiernoc94c3df2018-02-09 15:38:54 +0100691 try:
tiernob24258a2018-10-04 18:39:49 +0200692 if not main_topic or not version or not topic:
693 raise NbiException("URL must contain at least 'main_topic/version/topic'",
694 HTTPStatus.METHOD_NOT_ALLOWED)
tiernocd65be32018-11-16 16:34:49 +0100695 if main_topic not in ("admin", "vnfpkgm", "nsd", "nslcm", "pdu", "nst", "nsilcm"):
tiernob24258a2018-10-04 18:39:49 +0200696 raise NbiException("URL main_topic '{}' not supported".format(main_topic),
697 HTTPStatus.METHOD_NOT_ALLOWED)
tiernoc94c3df2018-02-09 15:38:54 +0100698 if version != 'v1':
699 raise NbiException("URL version '{}' not supported".format(version), HTTPStatus.METHOD_NOT_ALLOWED)
700
tiernof27c79b2018-03-12 17:08:42 +0100701 if kwargs and "METHOD" in kwargs and kwargs["METHOD"] in ("PUT", "POST", "DELETE", "GET", "PATCH"):
702 method = kwargs.pop("METHOD")
703 else:
704 method = cherrypy.request.method
tiernob92094f2018-05-11 13:44:22 +0200705 if kwargs and "FORCE" in kwargs:
706 force = kwargs.pop("FORCE")
707 else:
708 force = False
tiernob24258a2018-10-04 18:39:49 +0200709 self._check_valid_url_method(method, main_topic, version, topic, _id, item, *args)
tiernob24258a2018-10-04 18:39:49 +0200710 if main_topic == "admin" and topic == "tokens":
tiernof27c79b2018-03-12 17:08:42 +0100711 return self.token(method, _id, kwargs)
712
tiernoc94c3df2018-02-09 15:38:54 +0100713 # self.engine.load_dbase(cherrypy.request.app.config)
Eduardo Sousa2f988212018-07-26 01:04:11 +0100714 session = self.authenticator.authorize()
tiernof27c79b2018-03-12 17:08:42 +0100715 indata = self._format_in(kwargs)
tiernob24258a2018-10-04 18:39:49 +0200716 engine_topic = topic
717 if topic == "subscriptions":
718 engine_topic = main_topic + "_" + topic
719 if item:
720 engine_topic = item
tiernoc94c3df2018-02-09 15:38:54 +0100721
tiernob24258a2018-10-04 18:39:49 +0200722 if main_topic == "nsd":
723 engine_topic = "nsds"
724 elif main_topic == "vnfpkgm":
725 engine_topic = "vnfds"
726 elif main_topic == "nslcm":
727 engine_topic = "nsrs"
728 if topic == "ns_lcm_op_occs":
729 engine_topic = "nslcmops"
730 if topic == "vnfrs" or topic == "vnf_instances":
731 engine_topic = "vnfrs"
garciadeblas9750c5a2018-10-15 16:20:35 +0200732 elif main_topic == "nst":
733 engine_topic = "nsts"
734 elif main_topic == "nsilcm":
735 engine_topic = "nsis"
736 if topic == "nsi_lcm_op_occs":
Felipe Vicensb57758d2018-10-16 16:00:20 +0200737 engine_topic = "nsilcmops"
tiernob24258a2018-10-04 18:39:49 +0200738 elif main_topic == "pdu":
739 engine_topic = "pdus"
740 if engine_topic == "vims": # TODO this is for backward compatibility, it will remove in the future
741 engine_topic = "vim_accounts"
tiernoc94c3df2018-02-09 15:38:54 +0100742
743 if method == "GET":
Felipe Vicens07f31722018-10-29 15:16:44 +0100744 if item in ("nsd_content", "package_content", "artifacts", "vnfd", "nsd", "nst", "nst_content"):
garciadeblas9750c5a2018-10-15 16:20:35 +0200745 if item in ("vnfd", "nsd", "nst"):
tiernof27c79b2018-03-12 17:08:42 +0100746 path = "$DESCRIPTOR"
747 elif args:
748 path = args
tiernob24258a2018-10-04 18:39:49 +0200749 elif item == "artifacts":
tiernof27c79b2018-03-12 17:08:42 +0100750 path = ()
751 else:
752 path = None
tiernob24258a2018-10-04 18:39:49 +0200753 file, _format = self.engine.get_file(session, engine_topic, _id, path,
tierno2236d202018-05-16 19:05:16 +0200754 cherrypy.request.headers.get("Accept"))
tiernof27c79b2018-03-12 17:08:42 +0100755 outdata = file
756 elif not _id:
tiernob24258a2018-10-04 18:39:49 +0200757 outdata = self.engine.get_item_list(session, engine_topic, kwargs)
tiernoc94c3df2018-02-09 15:38:54 +0100758 else:
tiernob24258a2018-10-04 18:39:49 +0200759 outdata = self.engine.get_item(session, engine_topic, _id)
tiernof27c79b2018-03-12 17:08:42 +0100760 elif method == "POST":
garciadeblas9750c5a2018-10-15 16:20:35 +0200761 if topic in ("ns_descriptors_content", "vnf_packages_content", "netslice_templates_content"):
tiernof27c79b2018-03-12 17:08:42 +0100762 _id = cherrypy.request.headers.get("Transaction-Id")
763 if not _id:
tiernob24258a2018-10-04 18:39:49 +0200764 _id = self.engine.new_item(rollback, session, engine_topic, {}, None, cherrypy.request.headers,
tiernob92094f2018-05-11 13:44:22 +0200765 force=force)
tiernob24258a2018-10-04 18:39:49 +0200766 completed = self.engine.upload_content(session, engine_topic, _id, indata, kwargs,
767 cherrypy.request.headers, force=force)
tiernof27c79b2018-03-12 17:08:42 +0100768 if completed:
tiernob24258a2018-10-04 18:39:49 +0200769 self._set_location_header(main_topic, version, topic, _id)
tiernof27c79b2018-03-12 17:08:42 +0100770 else:
771 cherrypy.response.headers["Transaction-Id"] = _id
772 outdata = {"id": _id}
tiernob24258a2018-10-04 18:39:49 +0200773 elif topic == "ns_instances_content":
774 # creates NSR
775 _id = self.engine.new_item(rollback, session, engine_topic, indata, kwargs, force=force)
776 # creates nslcmop
777 indata["lcmOperationType"] = "instantiate"
778 indata["nsInstanceId"] = _id
779 self.engine.new_item(rollback, session, "nslcmops", indata, None)
780 self._set_location_header(main_topic, version, topic, _id)
tiernof27c79b2018-03-12 17:08:42 +0100781 outdata = {"id": _id}
tiernob24258a2018-10-04 18:39:49 +0200782 elif topic == "ns_instances" and item:
783 indata["lcmOperationType"] = item
784 indata["nsInstanceId"] = _id
785 _id = self.engine.new_item(rollback, session, "nslcmops", indata, kwargs)
786 self._set_location_header(main_topic, version, "ns_lcm_op_occs", _id)
tierno65acb4d2018-04-06 16:42:40 +0200787 outdata = {"id": _id}
788 cherrypy.response.status = HTTPStatus.ACCEPTED.value
garciadeblas9750c5a2018-10-15 16:20:35 +0200789 elif topic == "netslice_instances_content":
Felipe Vicens07f31722018-10-29 15:16:44 +0100790 # creates NetSlice_Instance_record (NSIR)
garciadeblas9750c5a2018-10-15 16:20:35 +0200791 _id = self.engine.new_item(rollback, session, engine_topic, indata, kwargs, force=force)
Felipe Vicens07f31722018-10-29 15:16:44 +0100792 self._set_location_header(main_topic, version, topic, _id)
garciadeblas9750c5a2018-10-15 16:20:35 +0200793 indata["lcmOperationType"] = "instantiate"
794 indata["nsiInstanceId"] = _id
Felipe Vicens07f31722018-10-29 15:16:44 +0100795 self.engine.new_item(rollback, session, "nsilcmops", indata, kwargs)
garciadeblas9750c5a2018-10-15 16:20:35 +0200796 outdata = {"id": _id}
Felipe Vicens07f31722018-10-29 15:16:44 +0100797
garciadeblas9750c5a2018-10-15 16:20:35 +0200798 elif topic == "netslice_instances" and item:
799 indata["lcmOperationType"] = item
800 indata["nsiInstanceId"] = _id
801 _id = self.engine.new_item(rollback, session, "nsilcmops", indata, kwargs)
802 self._set_location_header(main_topic, version, "nsi_lcm_op_occs", _id)
803 outdata = {"id": _id}
804 cherrypy.response.status = HTTPStatus.ACCEPTED.value
tiernof27c79b2018-03-12 17:08:42 +0100805 else:
tiernob24258a2018-10-04 18:39:49 +0200806 _id = self.engine.new_item(rollback, session, engine_topic, indata, kwargs,
807 cherrypy.request.headers, force=force)
808 self._set_location_header(main_topic, version, topic, _id)
tiernof27c79b2018-03-12 17:08:42 +0100809 outdata = {"id": _id}
tiernob24258a2018-10-04 18:39:49 +0200810 # TODO form NsdInfo when topic in ("ns_descriptors", "vnf_packages")
tiernof27c79b2018-03-12 17:08:42 +0100811 cherrypy.response.status = HTTPStatus.CREATED.value
tierno09c073e2018-04-26 13:36:48 +0200812
tiernoc94c3df2018-02-09 15:38:54 +0100813 elif method == "DELETE":
814 if not _id:
tiernob24258a2018-10-04 18:39:49 +0200815 outdata = self.engine.del_item_list(session, engine_topic, kwargs)
tierno09c073e2018-04-26 13:36:48 +0200816 cherrypy.response.status = HTTPStatus.OK.value
tiernoc94c3df2018-02-09 15:38:54 +0100817 else: # len(args) > 1
tiernoe8631782018-12-21 13:31:52 +0000818 delete_in_process = False
tiernob24258a2018-10-04 18:39:49 +0200819 if topic == "ns_instances_content" and not force:
820 nslcmop_desc = {
821 "lcmOperationType": "terminate",
822 "nsInstanceId": _id,
823 "autoremove": True
824 }
825 opp_id = self.engine.new_item(rollback, session, "nslcmops", nslcmop_desc, None)
tiernoe8631782018-12-21 13:31:52 +0000826 if opp_id:
827 delete_in_process = True
828 outdata = {"_id": opp_id}
829 cherrypy.response.status = HTTPStatus.ACCEPTED.value
garciadeblas9750c5a2018-10-15 16:20:35 +0200830 elif topic == "netslice_instances_content" and not force:
831 nsilcmop_desc = {
832 "lcmOperationType": "terminate",
833 "nsiInstanceId": _id,
834 "autoremove": True
835 }
836 opp_id = self.engine.new_item(rollback, session, "nsilcmops", nsilcmop_desc, None)
tiernoe8631782018-12-21 13:31:52 +0000837 if opp_id:
838 delete_in_process = True
839 outdata = {"_id": opp_id}
840 cherrypy.response.status = HTTPStatus.ACCEPTED.value
841 if not delete_in_process:
tiernob24258a2018-10-04 18:39:49 +0200842 self.engine.del_item(session, engine_topic, _id, force)
tierno09c073e2018-04-26 13:36:48 +0200843 cherrypy.response.status = HTTPStatus.NO_CONTENT.value
tierno55ba2e62018-12-11 17:22:22 +0000844 if engine_topic in ("vim_accounts", "wim_accounts", "sdns"):
tierno09c073e2018-04-26 13:36:48 +0200845 cherrypy.response.status = HTTPStatus.ACCEPTED.value
846
tierno7ae10112018-05-18 14:36:02 +0200847 elif method in ("PUT", "PATCH"):
tiernocd54a4a2018-09-12 16:40:35 +0200848 outdata = None
tiernof27c79b2018-03-12 17:08:42 +0100849 if not indata and not kwargs:
tiernoc94c3df2018-02-09 15:38:54 +0100850 raise NbiException("Nothing to update. Provide payload and/or query string",
851 HTTPStatus.BAD_REQUEST)
garciadeblas9750c5a2018-10-15 16:20:35 +0200852 if item in ("nsd_content", "package_content", "nst_content") and method == "PUT":
tiernob24258a2018-10-04 18:39:49 +0200853 completed = self.engine.upload_content(session, engine_topic, _id, indata, kwargs,
854 cherrypy.request.headers, force=force)
tiernof27c79b2018-03-12 17:08:42 +0100855 if not completed:
856 cherrypy.response.headers["Transaction-Id"] = id
tiernof27c79b2018-03-12 17:08:42 +0100857 else:
tiernob24258a2018-10-04 18:39:49 +0200858 self.engine.edit_item(session, engine_topic, _id, indata, kwargs, force=force)
tiernocd54a4a2018-09-12 16:40:35 +0200859 cherrypy.response.status = HTTPStatus.NO_CONTENT.value
tiernoc94c3df2018-02-09 15:38:54 +0100860 else:
861 raise NbiException("Method {} not allowed".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
tiernof27c79b2018-03-12 17:08:42 +0100862 return self._format_out(outdata, session, _format)
tiernob24258a2018-10-04 18:39:49 +0200863 except Exception as e:
tierno36ec8602018-11-02 17:27:11 +0100864 if isinstance(e, (NbiException, EngineException, DbException, FsException, MsgException, AuthException,
865 ValidationError)):
tiernob24258a2018-10-04 18:39:49 +0200866 http_code_value = cherrypy.response.status = e.http_code.value
867 http_code_name = e.http_code.name
868 cherrypy.log("Exception {}".format(e))
869 else:
870 http_code_value = cherrypy.response.status = HTTPStatus.BAD_REQUEST.value # INTERNAL_SERVER_ERROR
Felipe Vicense36ab852018-11-23 14:12:09 +0100871 cherrypy.log("CRITICAL: Exception {}".format(e), traceback=True)
tiernob24258a2018-10-04 18:39:49 +0200872 http_code_name = HTTPStatus.BAD_REQUEST.name
tierno3ace63c2018-05-03 17:51:43 +0200873 if hasattr(outdata, "close"): # is an open file
874 outdata.close()
tiernob24258a2018-10-04 18:39:49 +0200875 error_text = str(e)
876 rollback.reverse()
tiernodb9dc582018-06-20 17:27:29 +0200877 for rollback_item in rollback:
tierno3ace63c2018-05-03 17:51:43 +0200878 try:
tiernocc103432018-10-19 14:10:35 +0200879 if rollback_item.get("operation") == "set":
880 self.engine.db.set_one(rollback_item["topic"], {"_id": rollback_item["_id"]},
881 rollback_item["content"], fail_on_empty=False)
882 else:
tiernoe8631782018-12-21 13:31:52 +0000883 self.engine.db.del_one(rollback_item["topic"], {"_id": rollback_item["_id"]},
884 fail_on_empty=False)
tierno3ace63c2018-05-03 17:51:43 +0200885 except Exception as e2:
tiernob24258a2018-10-04 18:39:49 +0200886 rollback_error_text = "Rollback Exception {}: {}".format(rollback_item, e2)
887 cherrypy.log(rollback_error_text)
888 error_text += ". " + rollback_error_text
889 # if isinstance(e, MsgException):
890 # error_text = "{} has been '{}' but other modules cannot be informed because an error on bus".format(
891 # engine_topic[:-1], method, error_text)
tiernoc94c3df2018-02-09 15:38:54 +0100892 problem_details = {
tiernob24258a2018-10-04 18:39:49 +0200893 "code": http_code_name,
894 "status": http_code_value,
895 "detail": error_text,
tiernoc94c3df2018-02-09 15:38:54 +0100896 }
897 return self._format_out(problem_details, session)
898 # raise cherrypy.HTTPError(e.http_code.value, str(e))
899
900
901# def validate_password(realm, username, password):
902# cherrypy.log("realm "+ str(realm))
903# if username == "admin" and password == "admin":
904# return True
905# return False
906
907
908def _start_service():
909 """
910 Callback function called when cherrypy.engine starts
911 Override configuration with env variables
912 Set database, storage, message configuration
913 Init database with admin/admin user password
914 """
915 cherrypy.log.error("Starting osm_nbi")
916 # update general cherrypy configuration
917 update_dict = {}
918
919 engine_config = cherrypy.tree.apps['/osm'].config
920 for k, v in environ.items():
921 if not k.startswith("OSMNBI_"):
922 continue
tiernoe1281182018-05-22 12:24:36 +0200923 k1, _, k2 = k[7:].lower().partition("_")
tiernoc94c3df2018-02-09 15:38:54 +0100924 if not k2:
925 continue
926 try:
927 # update static configuration
928 if k == 'OSMNBI_STATIC_DIR':
929 engine_config["/static"]['tools.staticdir.dir'] = v
930 engine_config["/static"]['tools.staticdir.on'] = True
931 elif k == 'OSMNBI_SOCKET_PORT' or k == 'OSMNBI_SERVER_PORT':
932 update_dict['server.socket_port'] = int(v)
933 elif k == 'OSMNBI_SOCKET_HOST' or k == 'OSMNBI_SERVER_HOST':
934 update_dict['server.socket_host'] = v
tiernof5298be2018-05-16 14:43:57 +0200935 elif k1 in ("server", "test", "auth", "log"):
936 update_dict[k1 + '.' + k2] = v
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100937 elif k1 in ("message", "database", "storage", "authentication"):
tiernof5298be2018-05-16 14:43:57 +0200938 # k2 = k2.replace('_', '.')
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100939 if k2 in ("port", "db_port"):
tiernoc94c3df2018-02-09 15:38:54 +0100940 engine_config[k1][k2] = int(v)
941 else:
942 engine_config[k1][k2] = v
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100943
tiernoc94c3df2018-02-09 15:38:54 +0100944 except ValueError as e:
945 cherrypy.log.error("Ignoring environ '{}': " + str(e))
946 except Exception as e:
947 cherrypy.log.warn("skipping environ '{}' on exception '{}'".format(k, e))
948
949 if update_dict:
950 cherrypy.config.update(update_dict)
tiernof5298be2018-05-16 14:43:57 +0200951 engine_config["global"].update(update_dict)
tiernoc94c3df2018-02-09 15:38:54 +0100952
953 # logging cherrypy
954 log_format_simple = "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(message)s"
955 log_formatter_simple = logging.Formatter(log_format_simple, datefmt='%Y-%m-%dT%H:%M:%S')
956 logger_server = logging.getLogger("cherrypy.error")
957 logger_access = logging.getLogger("cherrypy.access")
958 logger_cherry = logging.getLogger("cherrypy")
959 logger_nbi = logging.getLogger("nbi")
960
tiernof5298be2018-05-16 14:43:57 +0200961 if "log.file" in engine_config["global"]:
962 file_handler = logging.handlers.RotatingFileHandler(engine_config["global"]["log.file"],
tiernoc94c3df2018-02-09 15:38:54 +0100963 maxBytes=100e6, backupCount=9, delay=0)
964 file_handler.setFormatter(log_formatter_simple)
965 logger_cherry.addHandler(file_handler)
966 logger_nbi.addHandler(file_handler)
tiernob24258a2018-10-04 18:39:49 +0200967 # log always to standard output
968 for format_, logger in {"nbi.server %(filename)s:%(lineno)s": logger_server,
969 "nbi.access %(filename)s:%(lineno)s": logger_access,
970 "%(name)s %(filename)s:%(lineno)s": logger_nbi
971 }.items():
972 log_format_cherry = "%(asctime)s %(levelname)s {} %(message)s".format(format_)
973 log_formatter_cherry = logging.Formatter(log_format_cherry, datefmt='%Y-%m-%dT%H:%M:%S')
974 str_handler = logging.StreamHandler()
975 str_handler.setFormatter(log_formatter_cherry)
976 logger.addHandler(str_handler)
tiernoc94c3df2018-02-09 15:38:54 +0100977
tiernof5298be2018-05-16 14:43:57 +0200978 if engine_config["global"].get("log.level"):
979 logger_cherry.setLevel(engine_config["global"]["log.level"])
980 logger_nbi.setLevel(engine_config["global"]["log.level"])
tiernoc94c3df2018-02-09 15:38:54 +0100981
982 # logging other modules
983 for k1, logname in {"message": "nbi.msg", "database": "nbi.db", "storage": "nbi.fs"}.items():
984 engine_config[k1]["logger_name"] = logname
985 logger_module = logging.getLogger(logname)
986 if "logfile" in engine_config[k1]:
987 file_handler = logging.handlers.RotatingFileHandler(engine_config[k1]["logfile"],
tierno2236d202018-05-16 19:05:16 +0200988 maxBytes=100e6, backupCount=9, delay=0)
tiernoc94c3df2018-02-09 15:38:54 +0100989 file_handler.setFormatter(log_formatter_simple)
990 logger_module.addHandler(file_handler)
991 if "loglevel" in engine_config[k1]:
992 logger_module.setLevel(engine_config[k1]["loglevel"])
993 # TODO add more entries, e.g.: storage
994 cherrypy.tree.apps['/osm'].root.engine.start(engine_config)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100995 cherrypy.tree.apps['/osm'].root.authenticator.start(engine_config)
tiernod985a8d2018-10-19 14:12:28 +0200996 cherrypy.tree.apps['/osm'].root.engine.init_db(target_version=database_version)
997 cherrypy.tree.apps['/osm'].root.authenticator.init_db(target_version=auth_database_version)
tiernoc94c3df2018-02-09 15:38:54 +0100998 # getenv('OSMOPENMANO_TENANT', None)
999
1000
1001def _stop_service():
1002 """
1003 Callback function called when cherrypy.engine stops
1004 TODO: Ending database connections.
1005 """
1006 cherrypy.tree.apps['/osm'].root.engine.stop()
1007 cherrypy.log.error("Stopping osm_nbi")
1008
tierno2236d202018-05-16 19:05:16 +02001009
tiernof5298be2018-05-16 14:43:57 +02001010def nbi(config_file):
tiernoc94c3df2018-02-09 15:38:54 +01001011 # conf = {
1012 # '/': {
1013 # #'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
1014 # 'tools.sessions.on': True,
1015 # 'tools.response_headers.on': True,
1016 # # 'tools.response_headers.headers': [('Content-Type', 'text/plain')],
1017 # }
1018 # }
1019 # cherrypy.Server.ssl_module = 'builtin'
1020 # cherrypy.Server.ssl_certificate = "http/cert.pem"
1021 # cherrypy.Server.ssl_private_key = "http/privkey.pem"
1022 # cherrypy.Server.thread_pool = 10
1023 # cherrypy.config.update({'Server.socket_port': config["port"], 'Server.socket_host': config["host"]})
1024
1025 # cherrypy.config.update({'tools.auth_basic.on': True,
1026 # 'tools.auth_basic.realm': 'localhost',
1027 # 'tools.auth_basic.checkpassword': validate_password})
1028 cherrypy.engine.subscribe('start', _start_service)
1029 cherrypy.engine.subscribe('stop', _stop_service)
tiernof5298be2018-05-16 14:43:57 +02001030 cherrypy.quickstart(Server(), '/osm', config_file)
1031
1032
1033def usage():
1034 print("""Usage: {} [options]
1035 -c|--config [configuration_file]: loads the configuration file (default: ./nbi.cfg)
1036 -h|--help: shows this help
1037 """.format(sys.argv[0]))
tierno2236d202018-05-16 19:05:16 +02001038 # --log-socket-host HOST: send logs to this host")
1039 # --log-socket-port PORT: send logs using this port (default: 9022)")
tiernoc94c3df2018-02-09 15:38:54 +01001040
1041
1042if __name__ == '__main__':
tiernof5298be2018-05-16 14:43:57 +02001043 try:
1044 # load parameters and configuration
1045 opts, args = getopt.getopt(sys.argv[1:], "hvc:", ["config=", "help"])
1046 # TODO add "log-socket-host=", "log-socket-port=", "log-file="
1047 config_file = None
1048 for o, a in opts:
1049 if o in ("-h", "--help"):
1050 usage()
1051 sys.exit()
1052 elif o in ("-c", "--config"):
1053 config_file = a
1054 # elif o == "--log-socket-port":
1055 # log_socket_port = a
1056 # elif o == "--log-socket-host":
1057 # log_socket_host = a
1058 # elif o == "--log-file":
1059 # log_file = a
1060 else:
1061 assert False, "Unhandled option"
1062 if config_file:
1063 if not path.isfile(config_file):
1064 print("configuration file '{}' that not exist".format(config_file), file=sys.stderr)
1065 exit(1)
1066 else:
1067 for config_file in (__file__[:__file__.rfind(".")] + ".cfg", "./nbi.cfg", "/etc/osm/nbi.cfg"):
1068 if path.isfile(config_file):
1069 break
1070 else:
1071 print("No configuration file 'nbi.cfg' found neither at local folder nor at /etc/osm/", file=sys.stderr)
1072 exit(1)
1073 nbi(config_file)
1074 except getopt.GetoptError as e:
1075 print(str(e), file=sys.stderr)
1076 # usage()
1077 exit(1)