blob: caf4c2d9510156658e61ba8f91b1370eb2027674 [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
tierno09c073e2018-04-26 13:36:48 +0200104 /vims_accounts (also vims for compatibility) O O
tierno2236d202018-05-16 19:05:16 +0200105 /<id> O O O
tierno0f98af52018-03-19 10:28:22 +0100106 /sdns O O
tierno2236d202018-05-16 19:05:16 +0200107 /<id> O O O
tiernoc94c3df2018-02-09 15:38:54 +0100108
garciadeblas9750c5a2018-10-15 16:20:35 +0200109 /nst/v1 O O
110 /netslice_templates_content O O
111 /<nstInfoId> O O O O
112 /netslice_templates O O
113 /<nstInfoId> O O O
114 /nst_content O O
115 /nst O
116 /artifacts[/<artifactPath>] O
117 /subscriptions X X
118 /<subscriptionId> X X
119
120 /nsilcm/v1
121 /netslice_instances_content O O
122 /<SliceInstanceId> O O
123 /netslice_instances O O
124 /<SliceInstanceId> O O
125 instantiate O
126 terminate O
127 action O
128 /nsi_lcm_op_occs O O
129 /<nsiLcmOpOccId> O O O
130 /subscriptions X X
131 /<subscriptionId> X X
132
tierno2236d202018-05-16 19:05:16 +0200133query string:
134 Follows SOL005 section 4.3.2 It contains extra METHOD to override http method, FORCE to force.
tierno36ec8602018-11-02 17:27:11 +0100135 simpleFilterExpr := <attrName>["."<attrName>]*["."<op>]"="<value>[","<value>]*
136 filterExpr := <simpleFilterExpr>["&"<simpleFilterExpr>]*
137 op := "eq" | "neq" (or "ne") | "gt" | "lt" | "gte" | "lte" | "cont" | "ncont"
138 attrName := string
tierno2236d202018-05-16 19:05:16 +0200139 For filtering inside array, it must select the element of the array, or add ANYINDEX to apply the filtering over any
140 item of the array, that is, pass if any item of the array pass the filter.
141 It allows both ne and neq for not equal
142 TODO: 4.3.3 Attribute selectors
143 all_fields, fields=x,y,.., exclude_default, exclude_fields=x,y,...
tiernoc94c3df2018-02-09 15:38:54 +0100144 (none) … same as “exclude_default”
145 all_fields … all attributes.
tierno2236d202018-05-16 19:05:16 +0200146 fields=<list> … all attributes except all complex attributes with minimum cardinality of zero that are not
147 conditionally mandatory, and that are not provided in <list>.
148 exclude_fields=<list> … all attributes except those complex attributes with a minimum cardinality of zero that
149 are not conditionally mandatory, and that are provided in <list>.
150 exclude_default … all attributes except those complex attributes with a minimum cardinality of zero that are not
151 conditionally mandatory, and that are part of the "default exclude set" defined in the present specification for
152 the particular resource
153 exclude_default and include=<list> … all attributes except those complex attributes with a minimum cardinality
154 of zero that are not conditionally mandatory and that are part of the "default exclude set" defined in the
155 present specification for the particular resource, but that are not part of <list>
tiernoc94c3df2018-02-09 15:38:54 +0100156Header field name Reference Example Descriptions
157 Accept IETF RFC 7231 [19] application/json Content-Types that are acceptable for the response.
158 This header field shall be present if the response is expected to have a non-empty message body.
159 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the request.
160 This header field shall be present if the request has a non-empty message body.
tierno2236d202018-05-16 19:05:16 +0200161 Authorization IETF RFC 7235 [22] Bearer mF_9.B5f-4.1JqM The authorization token for the request.
162 Details are specified in clause 4.5.3.
tiernoc94c3df2018-02-09 15:38:54 +0100163 Range IETF RFC 7233 [21] 1000-2000 Requested range of bytes from a file
164Header field name Reference Example Descriptions
165 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the response.
166 This header field shall be present if the response has a non-empty message body.
tierno2236d202018-05-16 19:05:16 +0200167 Location IETF RFC 7231 [19] http://www.example.com/vnflcm/v1/vnf_instances/123 Used in redirection, or when a
168 new resource has been created.
tiernoc94c3df2018-02-09 15:38:54 +0100169 This header field shall be present if the response status code is 201 or 3xx.
tierno2236d202018-05-16 19:05:16 +0200170 In the present document this header field is also used if the response status code is 202 and a new resource was
171 created.
172 WWW-Authenticate IETF RFC 7235 [22] Bearer realm="example" Challenge if the corresponding HTTP request has not
173 provided authorization, or error details if the corresponding HTTP request has provided an invalid authorization
174 token.
175 Accept-Ranges IETF RFC 7233 [21] bytes Used by the Server to signal whether or not it supports ranges for
176 certain resources.
177 Content-Range IETF RFC 7233 [21] bytes 21010-47021/ 47022 Signals the byte range that is contained in the
178 response, and the total length of the file.
tiernoc94c3df2018-02-09 15:38:54 +0100179 Retry-After IETF RFC 7231 [19] Fri, 31 Dec 1999 23:59:59 GMT
tiernoc94c3df2018-02-09 15:38:54 +0100180"""
181
182
183class NbiException(Exception):
184
185 def __init__(self, message, http_code=HTTPStatus.METHOD_NOT_ALLOWED):
186 Exception.__init__(self, message)
187 self.http_code = http_code
188
189
190class Server(object):
191 instance = 0
192 # to decode bytes to str
193 reader = getreader("utf-8")
194
195 def __init__(self):
196 self.instance += 1
197 self.engine = Engine()
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100198 self.authenticator = Authenticator()
tiernof27c79b2018-03-12 17:08:42 +0100199 self.valid_methods = { # contains allowed URL and methods
200 "admin": {
201 "v1": {
tierno0f98af52018-03-19 10:28:22 +0100202 "tokens": {"METHODS": ("GET", "POST", "DELETE"),
tierno2236d202018-05-16 19:05:16 +0200203 "<ID>": {"METHODS": ("GET", "DELETE")}
204 },
tierno0f98af52018-03-19 10:28:22 +0100205 "users": {"METHODS": ("GET", "POST"),
tiernocd54a4a2018-09-12 16:40:35 +0200206 "<ID>": {"METHODS": ("GET", "POST", "DELETE", "PATCH", "PUT")}
tierno2236d202018-05-16 19:05:16 +0200207 },
tierno0f98af52018-03-19 10:28:22 +0100208 "projects": {"METHODS": ("GET", "POST"),
tierno2236d202018-05-16 19:05:16 +0200209 "<ID>": {"METHODS": ("GET", "DELETE")}
210 },
tierno0f98af52018-03-19 10:28:22 +0100211 "vims": {"METHODS": ("GET", "POST"),
tierno7ae10112018-05-18 14:36:02 +0200212 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
tierno2236d202018-05-16 19:05:16 +0200213 },
tierno09c073e2018-04-26 13:36:48 +0200214 "vim_accounts": {"METHODS": ("GET", "POST"),
tierno7ae10112018-05-18 14:36:02 +0200215 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
tierno2236d202018-05-16 19:05:16 +0200216 },
tierno0f98af52018-03-19 10:28:22 +0100217 "sdns": {"METHODS": ("GET", "POST"),
tierno7ae10112018-05-18 14:36:02 +0200218 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
tierno2236d202018-05-16 19:05:16 +0200219 },
tiernof27c79b2018-03-12 17:08:42 +0100220 }
221 },
tiernocb83c942018-09-24 17:28:13 +0200222 "pdu": {
223 "v1": {
224 "pdu_descriptors": {"METHODS": ("GET", "POST"),
225 "<ID>": {"METHODS": ("GET", "POST", "DELETE", "PATCH", "PUT")}
226 },
227 }
228 },
tiernof27c79b2018-03-12 17:08:42 +0100229 "nsd": {
230 "v1": {
tierno2236d202018-05-16 19:05:16 +0200231 "ns_descriptors_content": {"METHODS": ("GET", "POST"),
232 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
233 },
234 "ns_descriptors": {"METHODS": ("GET", "POST"),
tierno36ec8602018-11-02 17:27:11 +0100235 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"),
tierno2236d202018-05-16 19:05:16 +0200236 "nsd_content": {"METHODS": ("GET", "PUT")},
237 "nsd": {"METHODS": "GET"}, # descriptor inside package
238 "artifacts": {"*": {"METHODS": "GET"}}
239 }
240 },
tiernof27c79b2018-03-12 17:08:42 +0100241 "pnf_descriptors": {"TODO": ("GET", "POST"),
tierno2236d202018-05-16 19:05:16 +0200242 "<ID>": {"TODO": ("GET", "DELETE", "PATCH"),
243 "pnfd_content": {"TODO": ("GET", "PUT")}
244 }
245 },
tiernof27c79b2018-03-12 17:08:42 +0100246 "subscriptions": {"TODO": ("GET", "POST"),
tierno2236d202018-05-16 19:05:16 +0200247 "<ID>": {"TODO": ("GET", "DELETE")}
248 },
tiernof27c79b2018-03-12 17:08:42 +0100249 }
250 },
251 "vnfpkgm": {
252 "v1": {
tierno2236d202018-05-16 19:05:16 +0200253 "vnf_packages_content": {"METHODS": ("GET", "POST"),
254 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
255 },
256 "vnf_packages": {"METHODS": ("GET", "POST"),
tiernoaae4dc42018-06-11 18:55:50 +0200257 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"), # GET: vnfPkgInfo
tierno2236d202018-05-16 19:05:16 +0200258 "package_content": {"METHODS": ("GET", "PUT"), # package
259 "upload_from_uri": {"TODO": "POST"}
260 },
261 "vnfd": {"METHODS": "GET"}, # descriptor inside package
262 "artifacts": {"*": {"METHODS": "GET"}}
263 }
264 },
tiernof27c79b2018-03-12 17:08:42 +0100265 "subscriptions": {"TODO": ("GET", "POST"),
tierno2236d202018-05-16 19:05:16 +0200266 "<ID>": {"TODO": ("GET", "DELETE")}
267 },
tiernof27c79b2018-03-12 17:08:42 +0100268 }
269 },
270 "nslcm": {
271 "v1": {
272 "ns_instances_content": {"METHODS": ("GET", "POST"),
tierno2236d202018-05-16 19:05:16 +0200273 "<ID>": {"METHODS": ("GET", "DELETE")}
274 },
tierno65acb4d2018-04-06 16:42:40 +0200275 "ns_instances": {"METHODS": ("GET", "POST"),
tierno95692442018-05-24 18:05:28 +0200276 "<ID>": {"METHODS": ("GET", "DELETE"),
tiernof759d822018-06-11 18:54:54 +0200277 "scale": {"METHODS": "POST"},
tierno2236d202018-05-16 19:05:16 +0200278 "terminate": {"METHODS": "POST"},
279 "instantiate": {"METHODS": "POST"},
280 "action": {"METHODS": "POST"},
281 }
282 },
tierno65acb4d2018-04-06 16:42:40 +0200283 "ns_lcm_op_occs": {"METHODS": "GET",
tierno2236d202018-05-16 19:05:16 +0200284 "<ID>": {"METHODS": "GET"},
285 },
tierno0ffaa992018-05-09 13:21:56 +0200286 "vnfrs": {"METHODS": ("GET"),
tierno2236d202018-05-16 19:05:16 +0200287 "<ID>": {"METHODS": ("GET")}
288 },
tiernof759d822018-06-11 18:54:54 +0200289 "vnf_instances": {"METHODS": ("GET"),
290 "<ID>": {"METHODS": ("GET")}
291 },
tiernof27c79b2018-03-12 17:08:42 +0100292 }
293 },
garciadeblas9750c5a2018-10-15 16:20:35 +0200294 "nst": {
295 "v1": {
296 "netslice_templates_content": {"METHODS": ("GET", "POST"),
297 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
298 },
299 "netslice_templates": {"METHODS": ("GET", "POST"),
300 "<ID>": {"METHODS": ("GET", "DELETE"), "TODO": "PATCH",
301 "nst_content": {"METHODS": ("GET", "PUT")},
302 "nst": {"METHODS": "GET"}, # descriptor inside package
303 "artifacts": {"*": {"METHODS": "GET"}}
304 }
305 },
306 "subscriptions": {"TODO": ("GET", "POST"),
307 "<ID>": {"TODO": ("GET", "DELETE")}
308 },
309 }
310 },
311 "nsilcm": {
312 "v1": {
313 "netslice_instances_content": {"METHODS": ("GET", "POST"),
314 "<ID>": {"METHODS": ("GET", "DELETE")}
315 },
316 "netslice_instances": {"METHODS": ("GET", "POST"),
317 "<ID>": {"METHODS": ("GET", "DELETE"),
318 "terminate": {"METHODS": "POST"},
319 "instantiate": {"METHODS": "POST"},
320 "action": {"METHODS": "POST"},
321 }
322 },
323 "nsi_lcm_op_occs": {"METHODS": "GET",
324 "<ID>": {"METHODS": "GET"},
325 },
326 }
327 },
tiernof27c79b2018-03-12 17:08:42 +0100328 }
tiernoc94c3df2018-02-09 15:38:54 +0100329
tiernoc94c3df2018-02-09 15:38:54 +0100330 def _format_in(self, kwargs):
331 try:
332 indata = None
333 if cherrypy.request.body.length:
334 error_text = "Invalid input format "
335
336 if "Content-Type" in cherrypy.request.headers:
337 if "application/json" in cherrypy.request.headers["Content-Type"]:
338 error_text = "Invalid json format "
339 indata = json.load(self.reader(cherrypy.request.body))
gcalvinode4adfe2018-10-30 11:46:09 +0100340 cherrypy.request.headers.pop("Content-File-MD5", None)
tiernoc94c3df2018-02-09 15:38:54 +0100341 elif "application/yaml" in cherrypy.request.headers["Content-Type"]:
342 error_text = "Invalid yaml format "
343 indata = yaml.load(cherrypy.request.body)
gcalvinode4adfe2018-10-30 11:46:09 +0100344 cherrypy.request.headers.pop("Content-File-MD5", None)
tiernoc94c3df2018-02-09 15:38:54 +0100345 elif "application/binary" in cherrypy.request.headers["Content-Type"] or \
346 "application/gzip" in cherrypy.request.headers["Content-Type"] or \
tiernof27c79b2018-03-12 17:08:42 +0100347 "application/zip" in cherrypy.request.headers["Content-Type"] or \
348 "text/plain" in cherrypy.request.headers["Content-Type"]:
349 indata = cherrypy.request.body # .read()
tiernoc94c3df2018-02-09 15:38:54 +0100350 elif "multipart/form-data" in cherrypy.request.headers["Content-Type"]:
351 if "descriptor_file" in kwargs:
352 filecontent = kwargs.pop("descriptor_file")
353 if not filecontent.file:
354 raise NbiException("empty file or content", HTTPStatus.BAD_REQUEST)
tiernof27c79b2018-03-12 17:08:42 +0100355 indata = filecontent.file # .read()
tiernoc94c3df2018-02-09 15:38:54 +0100356 if filecontent.content_type.value:
357 cherrypy.request.headers["Content-Type"] = filecontent.content_type.value
358 else:
359 # raise cherrypy.HTTPError(HTTPStatus.Not_Acceptable,
360 # "Only 'Content-Type' of type 'application/json' or
361 # 'application/yaml' for input format are available")
362 error_text = "Invalid yaml format "
363 indata = yaml.load(cherrypy.request.body)
gcalvinode4adfe2018-10-30 11:46:09 +0100364 cherrypy.request.headers.pop("Content-File-MD5", None)
tiernoc94c3df2018-02-09 15:38:54 +0100365 else:
366 error_text = "Invalid yaml format "
367 indata = yaml.load(cherrypy.request.body)
gcalvinode4adfe2018-10-30 11:46:09 +0100368 cherrypy.request.headers.pop("Content-File-MD5", None)
tiernoc94c3df2018-02-09 15:38:54 +0100369 if not indata:
370 indata = {}
371
tiernoc94c3df2018-02-09 15:38:54 +0100372 format_yaml = False
373 if cherrypy.request.headers.get("Query-String-Format") == "yaml":
374 format_yaml = True
375
376 for k, v in kwargs.items():
377 if isinstance(v, str):
378 if v == "":
379 kwargs[k] = None
380 elif format_yaml:
381 try:
382 kwargs[k] = yaml.load(v)
tiernoe1281182018-05-22 12:24:36 +0200383 except Exception:
tiernoc94c3df2018-02-09 15:38:54 +0100384 pass
385 elif k.endswith(".gt") or k.endswith(".lt") or k.endswith(".gte") or k.endswith(".lte"):
386 try:
387 kwargs[k] = int(v)
tiernoe1281182018-05-22 12:24:36 +0200388 except Exception:
tiernoc94c3df2018-02-09 15:38:54 +0100389 try:
390 kwargs[k] = float(v)
tiernoe1281182018-05-22 12:24:36 +0200391 except Exception:
tiernoc94c3df2018-02-09 15:38:54 +0100392 pass
393 elif v.find(",") > 0:
394 kwargs[k] = v.split(",")
395 elif isinstance(v, (list, tuple)):
396 for index in range(0, len(v)):
397 if v[index] == "":
398 v[index] = None
399 elif format_yaml:
400 try:
401 v[index] = yaml.load(v[index])
tiernoe1281182018-05-22 12:24:36 +0200402 except Exception:
tiernoc94c3df2018-02-09 15:38:54 +0100403 pass
404
tiernof27c79b2018-03-12 17:08:42 +0100405 return indata
tiernoc94c3df2018-02-09 15:38:54 +0100406 except (ValueError, yaml.YAMLError) as exc:
407 raise NbiException(error_text + str(exc), HTTPStatus.BAD_REQUEST)
408 except KeyError as exc:
409 raise NbiException("Query string error: " + str(exc), HTTPStatus.BAD_REQUEST)
tiernob92094f2018-05-11 13:44:22 +0200410 except Exception as exc:
411 raise NbiException(error_text + str(exc), HTTPStatus.BAD_REQUEST)
tiernoc94c3df2018-02-09 15:38:54 +0100412
413 @staticmethod
tiernof27c79b2018-03-12 17:08:42 +0100414 def _format_out(data, session=None, _format=None):
tiernoc94c3df2018-02-09 15:38:54 +0100415 """
416 return string of dictionary data according to requested json, yaml, xml. By default json
tiernof27c79b2018-03-12 17:08:42 +0100417 :param data: response to be sent. Can be a dict, text or file
tiernoc94c3df2018-02-09 15:38:54 +0100418 :param session:
tiernof27c79b2018-03-12 17:08:42 +0100419 :param _format: The format to be set as Content-Type ir data is a file
tiernoc94c3df2018-02-09 15:38:54 +0100420 :return: None
421 """
tierno0f98af52018-03-19 10:28:22 +0100422 accept = cherrypy.request.headers.get("Accept")
tiernof27c79b2018-03-12 17:08:42 +0100423 if data is None:
tierno0f98af52018-03-19 10:28:22 +0100424 if accept and "text/html" in accept:
425 return html.format(data, cherrypy.request, cherrypy.response, session)
tierno09c073e2018-04-26 13:36:48 +0200426 # cherrypy.response.status = HTTPStatus.NO_CONTENT.value
tiernof27c79b2018-03-12 17:08:42 +0100427 return
428 elif hasattr(data, "read"): # file object
429 if _format:
430 cherrypy.response.headers["Content-Type"] = _format
431 elif "b" in data.mode: # binariy asssumig zip
432 cherrypy.response.headers["Content-Type"] = 'application/zip'
433 else:
434 cherrypy.response.headers["Content-Type"] = 'text/plain'
435 # TODO check that cherrypy close file. If not implement pending things to close per thread next
436 return data
tierno0f98af52018-03-19 10:28:22 +0100437 if accept:
tiernoc94c3df2018-02-09 15:38:54 +0100438 if "application/json" in accept:
439 cherrypy.response.headers["Content-Type"] = 'application/json; charset=utf-8'
440 a = json.dumps(data, indent=4) + "\n"
441 return a.encode("utf8")
442 elif "text/html" in accept:
443 return html.format(data, cherrypy.request, cherrypy.response, session)
444
tiernof27c79b2018-03-12 17:08:42 +0100445 elif "application/yaml" in accept or "*/*" in accept or "text/plain" in accept:
tiernoc94c3df2018-02-09 15:38:54 +0100446 pass
447 else:
448 raise cherrypy.HTTPError(HTTPStatus.NOT_ACCEPTABLE.value,
449 "Only 'Accept' of type 'application/json' or 'application/yaml' "
450 "for output format are available")
451 cherrypy.response.headers["Content-Type"] = 'application/yaml'
452 return yaml.safe_dump(data, explicit_start=True, indent=4, default_flow_style=False, tags=False,
453 encoding='utf-8', allow_unicode=True) # , canonical=True, default_style='"'
454
455 @cherrypy.expose
456 def index(self, *args, **kwargs):
457 session = None
458 try:
459 if cherrypy.request.method == "GET":
Eduardo Sousa2f988212018-07-26 01:04:11 +0100460 session = self.authenticator.authorize()
tiernoc94c3df2018-02-09 15:38:54 +0100461 outdata = "Index page"
462 else:
463 raise cherrypy.HTTPError(HTTPStatus.METHOD_NOT_ALLOWED.value,
tierno2236d202018-05-16 19:05:16 +0200464 "Method {} not allowed for tokens".format(cherrypy.request.method))
tiernoc94c3df2018-02-09 15:38:54 +0100465
466 return self._format_out(outdata, session)
467
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100468 except (EngineException, AuthException) as e:
tiernoc94c3df2018-02-09 15:38:54 +0100469 cherrypy.log("index Exception {}".format(e))
470 cherrypy.response.status = e.http_code.value
471 return self._format_out("Welcome to OSM!", session)
472
473 @cherrypy.expose
tierno55945e72018-04-06 16:40:27 +0200474 def version(self, *args, **kwargs):
tiernodfe09572018-04-24 10:41:10 +0200475 # TODO consider to remove and provide version using the static version file
tierno55945e72018-04-06 16:40:27 +0200476 global __version__, version_date
477 try:
478 if cherrypy.request.method != "GET":
479 raise NbiException("Only method GET is allowed", HTTPStatus.METHOD_NOT_ALLOWED)
480 elif args or kwargs:
481 raise NbiException("Invalid URL or query string for version", HTTPStatus.METHOD_NOT_ALLOWED)
482 return __version__ + " " + version_date
483 except NbiException as e:
484 cherrypy.response.status = e.http_code.value
485 problem_details = {
486 "code": e.http_code.name,
487 "status": e.http_code.value,
488 "detail": str(e),
489 }
490 return self._format_out(problem_details, None)
491
492 @cherrypy.expose
tiernof27c79b2018-03-12 17:08:42 +0100493 def token(self, method, token_id=None, kwargs=None):
tiernoc94c3df2018-02-09 15:38:54 +0100494 session = None
495 # self.engine.load_dbase(cherrypy.request.app.config)
tiernof27c79b2018-03-12 17:08:42 +0100496 indata = self._format_in(kwargs)
497 if not isinstance(indata, dict):
498 raise NbiException("Expected application/yaml or application/json Content-Type", HTTPStatus.BAD_REQUEST)
tiernoc94c3df2018-02-09 15:38:54 +0100499 try:
tiernoc94c3df2018-02-09 15:38:54 +0100500 if method == "GET":
Eduardo Sousa2f988212018-07-26 01:04:11 +0100501 session = self.authenticator.authorize()
tiernof27c79b2018-03-12 17:08:42 +0100502 if token_id:
Eduardo Sousa2f988212018-07-26 01:04:11 +0100503 outdata = self.authenticator.get_token(session, token_id)
tiernoc94c3df2018-02-09 15:38:54 +0100504 else:
Eduardo Sousa2f988212018-07-26 01:04:11 +0100505 outdata = self.authenticator.get_token_list(session)
tiernoc94c3df2018-02-09 15:38:54 +0100506 elif method == "POST":
507 try:
Eduardo Sousa2f988212018-07-26 01:04:11 +0100508 session = self.authenticator.authorize()
tiernoe1281182018-05-22 12:24:36 +0200509 except Exception:
tiernoc94c3df2018-02-09 15:38:54 +0100510 session = None
511 if kwargs:
512 indata.update(kwargs)
Eduardo Sousa2f988212018-07-26 01:04:11 +0100513 outdata = self.authenticator.new_token(session, indata, cherrypy.request.remote)
tiernoc94c3df2018-02-09 15:38:54 +0100514 session = outdata
515 cherrypy.session['Authorization'] = outdata["_id"]
tierno0f98af52018-03-19 10:28:22 +0100516 self._set_location_header("admin", "v1", "tokens", outdata["_id"])
tiernoc94c3df2018-02-09 15:38:54 +0100517 # cherrypy.response.cookie["Authorization"] = outdata["id"]
518 # cherrypy.response.cookie["Authorization"]['expires'] = 3600
519 elif method == "DELETE":
tiernof27c79b2018-03-12 17:08:42 +0100520 if not token_id and "id" in kwargs:
tiernoc94c3df2018-02-09 15:38:54 +0100521 token_id = kwargs["id"]
tiernof27c79b2018-03-12 17:08:42 +0100522 elif not token_id:
Eduardo Sousa2f988212018-07-26 01:04:11 +0100523 session = self.authenticator.authorize()
tiernoc94c3df2018-02-09 15:38:54 +0100524 token_id = session["_id"]
Eduardo Sousa2f988212018-07-26 01:04:11 +0100525 outdata = self.authenticator.del_token(token_id)
tiernoc94c3df2018-02-09 15:38:54 +0100526 session = None
527 cherrypy.session['Authorization'] = "logout"
528 # cherrypy.response.cookie["Authorization"] = token_id
529 # cherrypy.response.cookie["Authorization"]['expires'] = 0
530 else:
531 raise NbiException("Method {} not allowed for token".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
532 return self._format_out(outdata, session)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100533 except (NbiException, EngineException, DbException, AuthException) as e:
tiernoc94c3df2018-02-09 15:38:54 +0100534 cherrypy.log("tokens Exception {}".format(e))
535 cherrypy.response.status = e.http_code.value
536 problem_details = {
537 "code": e.http_code.name,
538 "status": e.http_code.value,
539 "detail": str(e),
540 }
541 return self._format_out(problem_details, session)
542
543 @cherrypy.expose
544 def test(self, *args, **kwargs):
545 thread_info = None
tiernof27c79b2018-03-12 17:08:42 +0100546 if args and args[0] == "help":
547 return "<html><pre>\ninit\nfile/<name> download file\ndb-clear/table\nprune\nlogin\nlogin2\n"\
tiernoe1281182018-05-22 12:24:36 +0200548 "sleep/<time>\nmessage/topic\n</pre></html>"
tiernof27c79b2018-03-12 17:08:42 +0100549
550 elif args and args[0] == "init":
tiernoc94c3df2018-02-09 15:38:54 +0100551 try:
552 # self.engine.load_dbase(cherrypy.request.app.config)
553 self.engine.create_admin()
554 return "Done. User 'admin', password 'admin' created"
555 except Exception:
556 cherrypy.response.status = HTTPStatus.FORBIDDEN.value
557 return self._format_out("Database already initialized")
tiernof27c79b2018-03-12 17:08:42 +0100558 elif args and args[0] == "file":
559 return cherrypy.lib.static.serve_file(cherrypy.tree.apps['/osm'].config["storage"]["path"] + "/" + args[1],
560 "text/plain", "attachment")
561 elif args and args[0] == "file2":
562 f_path = cherrypy.tree.apps['/osm'].config["storage"]["path"] + "/" + args[1]
563 f = open(f_path, "r")
564 cherrypy.response.headers["Content-type"] = "text/plain"
tiernof27c79b2018-03-12 17:08:42 +0100565 return f
tierno55945e72018-04-06 16:40:27 +0200566
tiernof27c79b2018-03-12 17:08:42 +0100567 elif len(args) == 2 and args[0] == "db-clear":
tiernob24258a2018-10-04 18:39:49 +0200568 return self.engine.db.del_list(args[1], kwargs)
tiernoc94c3df2018-02-09 15:38:54 +0100569 elif args and args[0] == "prune":
570 return self.engine.prune()
571 elif args and args[0] == "login":
572 if not cherrypy.request.headers.get("Authorization"):
573 cherrypy.response.headers["WWW-Authenticate"] = 'Basic realm="Access to OSM site", charset="UTF-8"'
574 cherrypy.response.status = HTTPStatus.UNAUTHORIZED.value
575 elif args and args[0] == "login2":
576 if not cherrypy.request.headers.get("Authorization"):
577 cherrypy.response.headers["WWW-Authenticate"] = 'Bearer realm="Access to OSM site"'
578 cherrypy.response.status = HTTPStatus.UNAUTHORIZED.value
579 elif args and args[0] == "sleep":
580 sleep_time = 5
581 try:
582 sleep_time = int(args[1])
583 except Exception:
584 cherrypy.response.status = HTTPStatus.FORBIDDEN.value
585 return self._format_out("Database already initialized")
586 thread_info = cherrypy.thread_data
587 print(thread_info)
588 time.sleep(sleep_time)
589 # thread_info
590 elif len(args) >= 2 and args[0] == "message":
tiernob24258a2018-10-04 18:39:49 +0200591 main_topic = args[1]
592 return_text = "<html><pre>{} ->\n".format(main_topic)
tiernoc94c3df2018-02-09 15:38:54 +0100593 try:
tierno55945e72018-04-06 16:40:27 +0200594 if cherrypy.request.method == 'POST':
595 to_send = yaml.load(cherrypy.request.body)
596 for k, v in to_send.items():
tiernob24258a2018-10-04 18:39:49 +0200597 self.engine.msg.write(main_topic, k, v)
tierno55945e72018-04-06 16:40:27 +0200598 return_text += " {}: {}\n".format(k, v)
599 elif cherrypy.request.method == 'GET':
600 for k, v in kwargs.items():
tiernob24258a2018-10-04 18:39:49 +0200601 self.engine.msg.write(main_topic, k, yaml.load(v))
tierno55945e72018-04-06 16:40:27 +0200602 return_text += " {}: {}\n".format(k, yaml.load(v))
tiernoc94c3df2018-02-09 15:38:54 +0100603 except Exception as e:
tierno55945e72018-04-06 16:40:27 +0200604 return_text += "Error: " + str(e)
605 return_text += "</pre></html>\n"
606 return return_text
tiernoc94c3df2018-02-09 15:38:54 +0100607
608 return_text = (
609 "<html><pre>\nheaders:\n args: {}\n".format(args) +
610 " kwargs: {}\n".format(kwargs) +
611 " headers: {}\n".format(cherrypy.request.headers) +
612 " path_info: {}\n".format(cherrypy.request.path_info) +
613 " query_string: {}\n".format(cherrypy.request.query_string) +
614 " session: {}\n".format(cherrypy.session) +
615 " cookie: {}\n".format(cherrypy.request.cookie) +
616 " method: {}\n".format(cherrypy.request.method) +
617 " session: {}\n".format(cherrypy.session.get('fieldname')) +
618 " body:\n")
619 return_text += " length: {}\n".format(cherrypy.request.body.length)
620 if cherrypy.request.body.length:
621 return_text += " content: {}\n".format(
622 str(cherrypy.request.body.read(int(cherrypy.request.headers.get('Content-Length', 0)))))
623 if thread_info:
624 return_text += "thread: {}\n".format(thread_info)
625 return_text += "</pre></html>"
626 return return_text
627
tiernof27c79b2018-03-12 17:08:42 +0100628 def _check_valid_url_method(self, method, *args):
629 if len(args) < 3:
tiernob24258a2018-10-04 18:39:49 +0200630 raise NbiException("URL must contain at least 'main_topic/version/topic'", HTTPStatus.METHOD_NOT_ALLOWED)
tiernof27c79b2018-03-12 17:08:42 +0100631
632 reference = self.valid_methods
633 for arg in args:
634 if arg is None:
635 break
636 if not isinstance(reference, dict):
637 raise NbiException("URL contains unexpected extra items '{}'".format(arg),
638 HTTPStatus.METHOD_NOT_ALLOWED)
639
640 if arg in reference:
641 reference = reference[arg]
642 elif "<ID>" in reference:
643 reference = reference["<ID>"]
644 elif "*" in reference:
645 reference = reference["*"]
646 break
647 else:
648 raise NbiException("Unexpected URL item {}".format(arg), HTTPStatus.METHOD_NOT_ALLOWED)
649 if "TODO" in reference and method in reference["TODO"]:
650 raise NbiException("Method {} not supported yet for this URL".format(method), HTTPStatus.NOT_IMPLEMENTED)
tierno2236d202018-05-16 19:05:16 +0200651 elif "METHODS" in reference and method not in reference["METHODS"]:
tiernof27c79b2018-03-12 17:08:42 +0100652 raise NbiException("Method {} not supported for this URL".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
653 return
654
655 @staticmethod
tiernob24258a2018-10-04 18:39:49 +0200656 def _set_location_header(main_topic, version, topic, id):
tiernof27c79b2018-03-12 17:08:42 +0100657 """
658 Insert response header Location with the URL of created item base on URL params
tiernob24258a2018-10-04 18:39:49 +0200659 :param main_topic:
tiernof27c79b2018-03-12 17:08:42 +0100660 :param version:
tiernob24258a2018-10-04 18:39:49 +0200661 :param topic:
tiernof27c79b2018-03-12 17:08:42 +0100662 :param id:
663 :return: None
664 """
665 # 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 +0200666 cherrypy.response.headers["Location"] = "/osm/{}/{}/{}/{}".format(main_topic, version, topic, id)
tiernof27c79b2018-03-12 17:08:42 +0100667 return
668
tiernoc94c3df2018-02-09 15:38:54 +0100669 @cherrypy.expose
tiernob24258a2018-10-04 18:39:49 +0200670 def default(self, main_topic=None, version=None, topic=None, _id=None, item=None, *args, **kwargs):
tiernoc94c3df2018-02-09 15:38:54 +0100671 session = None
tiernof27c79b2018-03-12 17:08:42 +0100672 outdata = None
673 _format = None
tierno0f98af52018-03-19 10:28:22 +0100674 method = "DONE"
tiernob24258a2018-10-04 18:39:49 +0200675 engine_topic = None
tiernodb9dc582018-06-20 17:27:29 +0200676 rollback = []
677 session = None
tiernoc94c3df2018-02-09 15:38:54 +0100678 try:
tiernob24258a2018-10-04 18:39:49 +0200679 if not main_topic or not version or not topic:
680 raise NbiException("URL must contain at least 'main_topic/version/topic'",
681 HTTPStatus.METHOD_NOT_ALLOWED)
tiernocd65be32018-11-16 16:34:49 +0100682 if main_topic not in ("admin", "vnfpkgm", "nsd", "nslcm", "pdu", "nst", "nsilcm"):
tiernob24258a2018-10-04 18:39:49 +0200683 raise NbiException("URL main_topic '{}' not supported".format(main_topic),
684 HTTPStatus.METHOD_NOT_ALLOWED)
tiernoc94c3df2018-02-09 15:38:54 +0100685 if version != 'v1':
686 raise NbiException("URL version '{}' not supported".format(version), HTTPStatus.METHOD_NOT_ALLOWED)
687
tiernof27c79b2018-03-12 17:08:42 +0100688 if kwargs and "METHOD" in kwargs and kwargs["METHOD"] in ("PUT", "POST", "DELETE", "GET", "PATCH"):
689 method = kwargs.pop("METHOD")
690 else:
691 method = cherrypy.request.method
tiernob92094f2018-05-11 13:44:22 +0200692 if kwargs and "FORCE" in kwargs:
693 force = kwargs.pop("FORCE")
694 else:
695 force = False
tiernob24258a2018-10-04 18:39:49 +0200696 self._check_valid_url_method(method, main_topic, version, topic, _id, item, *args)
tiernob24258a2018-10-04 18:39:49 +0200697 if main_topic == "admin" and topic == "tokens":
tiernof27c79b2018-03-12 17:08:42 +0100698 return self.token(method, _id, kwargs)
699
tiernoc94c3df2018-02-09 15:38:54 +0100700 # self.engine.load_dbase(cherrypy.request.app.config)
Eduardo Sousa2f988212018-07-26 01:04:11 +0100701 session = self.authenticator.authorize()
tiernof27c79b2018-03-12 17:08:42 +0100702 indata = self._format_in(kwargs)
tiernob24258a2018-10-04 18:39:49 +0200703 engine_topic = topic
704 if topic == "subscriptions":
705 engine_topic = main_topic + "_" + topic
706 if item:
707 engine_topic = item
tiernoc94c3df2018-02-09 15:38:54 +0100708
tiernob24258a2018-10-04 18:39:49 +0200709 if main_topic == "nsd":
710 engine_topic = "nsds"
711 elif main_topic == "vnfpkgm":
712 engine_topic = "vnfds"
713 elif main_topic == "nslcm":
714 engine_topic = "nsrs"
715 if topic == "ns_lcm_op_occs":
716 engine_topic = "nslcmops"
717 if topic == "vnfrs" or topic == "vnf_instances":
718 engine_topic = "vnfrs"
garciadeblas9750c5a2018-10-15 16:20:35 +0200719 elif main_topic == "nst":
720 engine_topic = "nsts"
721 elif main_topic == "nsilcm":
722 engine_topic = "nsis"
723 if topic == "nsi_lcm_op_occs":
Felipe Vicensb57758d2018-10-16 16:00:20 +0200724 engine_topic = "nsilcmops"
tiernob24258a2018-10-04 18:39:49 +0200725 elif main_topic == "pdu":
726 engine_topic = "pdus"
727 if engine_topic == "vims": # TODO this is for backward compatibility, it will remove in the future
728 engine_topic = "vim_accounts"
tiernoc94c3df2018-02-09 15:38:54 +0100729
730 if method == "GET":
Felipe Vicens07f31722018-10-29 15:16:44 +0100731 if item in ("nsd_content", "package_content", "artifacts", "vnfd", "nsd", "nst", "nst_content"):
garciadeblas9750c5a2018-10-15 16:20:35 +0200732 if item in ("vnfd", "nsd", "nst"):
tiernof27c79b2018-03-12 17:08:42 +0100733 path = "$DESCRIPTOR"
734 elif args:
735 path = args
tiernob24258a2018-10-04 18:39:49 +0200736 elif item == "artifacts":
tiernof27c79b2018-03-12 17:08:42 +0100737 path = ()
738 else:
739 path = None
tiernob24258a2018-10-04 18:39:49 +0200740 file, _format = self.engine.get_file(session, engine_topic, _id, path,
tierno2236d202018-05-16 19:05:16 +0200741 cherrypy.request.headers.get("Accept"))
tiernof27c79b2018-03-12 17:08:42 +0100742 outdata = file
743 elif not _id:
tiernob24258a2018-10-04 18:39:49 +0200744 outdata = self.engine.get_item_list(session, engine_topic, kwargs)
tiernoc94c3df2018-02-09 15:38:54 +0100745 else:
tiernob24258a2018-10-04 18:39:49 +0200746 outdata = self.engine.get_item(session, engine_topic, _id)
tiernof27c79b2018-03-12 17:08:42 +0100747 elif method == "POST":
garciadeblas9750c5a2018-10-15 16:20:35 +0200748 if topic in ("ns_descriptors_content", "vnf_packages_content", "netslice_templates_content"):
tiernof27c79b2018-03-12 17:08:42 +0100749 _id = cherrypy.request.headers.get("Transaction-Id")
750 if not _id:
tiernob24258a2018-10-04 18:39:49 +0200751 _id = self.engine.new_item(rollback, session, engine_topic, {}, None, cherrypy.request.headers,
tiernob92094f2018-05-11 13:44:22 +0200752 force=force)
tiernob24258a2018-10-04 18:39:49 +0200753 completed = self.engine.upload_content(session, engine_topic, _id, indata, kwargs,
754 cherrypy.request.headers, force=force)
tiernof27c79b2018-03-12 17:08:42 +0100755 if completed:
tiernob24258a2018-10-04 18:39:49 +0200756 self._set_location_header(main_topic, version, topic, _id)
tiernof27c79b2018-03-12 17:08:42 +0100757 else:
758 cherrypy.response.headers["Transaction-Id"] = _id
759 outdata = {"id": _id}
tiernob24258a2018-10-04 18:39:49 +0200760 elif topic == "ns_instances_content":
761 # creates NSR
762 _id = self.engine.new_item(rollback, session, engine_topic, indata, kwargs, force=force)
763 # creates nslcmop
764 indata["lcmOperationType"] = "instantiate"
765 indata["nsInstanceId"] = _id
766 self.engine.new_item(rollback, session, "nslcmops", indata, None)
767 self._set_location_header(main_topic, version, topic, _id)
tiernof27c79b2018-03-12 17:08:42 +0100768 outdata = {"id": _id}
tiernob24258a2018-10-04 18:39:49 +0200769 elif topic == "ns_instances" and item:
770 indata["lcmOperationType"] = item
771 indata["nsInstanceId"] = _id
772 _id = self.engine.new_item(rollback, session, "nslcmops", indata, kwargs)
773 self._set_location_header(main_topic, version, "ns_lcm_op_occs", _id)
tierno65acb4d2018-04-06 16:42:40 +0200774 outdata = {"id": _id}
775 cherrypy.response.status = HTTPStatus.ACCEPTED.value
garciadeblas9750c5a2018-10-15 16:20:35 +0200776 elif topic == "netslice_instances_content":
Felipe Vicens07f31722018-10-29 15:16:44 +0100777 # creates NetSlice_Instance_record (NSIR)
garciadeblas9750c5a2018-10-15 16:20:35 +0200778 _id = self.engine.new_item(rollback, session, engine_topic, indata, kwargs, force=force)
Felipe Vicens07f31722018-10-29 15:16:44 +0100779 self._set_location_header(main_topic, version, topic, _id)
garciadeblas9750c5a2018-10-15 16:20:35 +0200780 indata["lcmOperationType"] = "instantiate"
781 indata["nsiInstanceId"] = _id
Felipe Vicens07f31722018-10-29 15:16:44 +0100782 self.engine.new_item(rollback, session, "nsilcmops", indata, kwargs)
garciadeblas9750c5a2018-10-15 16:20:35 +0200783 outdata = {"id": _id}
Felipe Vicens07f31722018-10-29 15:16:44 +0100784
garciadeblas9750c5a2018-10-15 16:20:35 +0200785 elif topic == "netslice_instances" and item:
786 indata["lcmOperationType"] = item
787 indata["nsiInstanceId"] = _id
788 _id = self.engine.new_item(rollback, session, "nsilcmops", indata, kwargs)
789 self._set_location_header(main_topic, version, "nsi_lcm_op_occs", _id)
790 outdata = {"id": _id}
791 cherrypy.response.status = HTTPStatus.ACCEPTED.value
tiernof27c79b2018-03-12 17:08:42 +0100792 else:
tiernob24258a2018-10-04 18:39:49 +0200793 _id = self.engine.new_item(rollback, session, engine_topic, indata, kwargs,
794 cherrypy.request.headers, force=force)
795 self._set_location_header(main_topic, version, topic, _id)
tiernof27c79b2018-03-12 17:08:42 +0100796 outdata = {"id": _id}
tiernob24258a2018-10-04 18:39:49 +0200797 # TODO form NsdInfo when topic in ("ns_descriptors", "vnf_packages")
tiernof27c79b2018-03-12 17:08:42 +0100798 cherrypy.response.status = HTTPStatus.CREATED.value
tierno09c073e2018-04-26 13:36:48 +0200799
tiernoc94c3df2018-02-09 15:38:54 +0100800 elif method == "DELETE":
801 if not _id:
tiernob24258a2018-10-04 18:39:49 +0200802 outdata = self.engine.del_item_list(session, engine_topic, kwargs)
tierno09c073e2018-04-26 13:36:48 +0200803 cherrypy.response.status = HTTPStatus.OK.value
tiernoc94c3df2018-02-09 15:38:54 +0100804 else: # len(args) > 1
tiernob24258a2018-10-04 18:39:49 +0200805 if topic == "ns_instances_content" and not force:
806 nslcmop_desc = {
807 "lcmOperationType": "terminate",
808 "nsInstanceId": _id,
809 "autoremove": True
810 }
811 opp_id = self.engine.new_item(rollback, session, "nslcmops", nslcmop_desc, None)
tierno3ace63c2018-05-03 17:51:43 +0200812 outdata = {"_id": opp_id}
tierno09c073e2018-04-26 13:36:48 +0200813 cherrypy.response.status = HTTPStatus.ACCEPTED.value
garciadeblas9750c5a2018-10-15 16:20:35 +0200814 elif topic == "netslice_instances_content" and not force:
815 nsilcmop_desc = {
816 "lcmOperationType": "terminate",
817 "nsiInstanceId": _id,
818 "autoremove": True
819 }
820 opp_id = self.engine.new_item(rollback, session, "nsilcmops", nsilcmop_desc, None)
821 outdata = {"_id": opp_id}
822 cherrypy.response.status = HTTPStatus.ACCEPTED.value
tierno65acb4d2018-04-06 16:42:40 +0200823 else:
tiernob24258a2018-10-04 18:39:49 +0200824 self.engine.del_item(session, engine_topic, _id, force)
tierno09c073e2018-04-26 13:36:48 +0200825 cherrypy.response.status = HTTPStatus.NO_CONTENT.value
tiernob24258a2018-10-04 18:39:49 +0200826 if engine_topic in ("vim_accounts", "sdns"):
tierno09c073e2018-04-26 13:36:48 +0200827 cherrypy.response.status = HTTPStatus.ACCEPTED.value
828
tierno7ae10112018-05-18 14:36:02 +0200829 elif method in ("PUT", "PATCH"):
tiernocd54a4a2018-09-12 16:40:35 +0200830 outdata = None
tiernof27c79b2018-03-12 17:08:42 +0100831 if not indata and not kwargs:
tiernoc94c3df2018-02-09 15:38:54 +0100832 raise NbiException("Nothing to update. Provide payload and/or query string",
833 HTTPStatus.BAD_REQUEST)
garciadeblas9750c5a2018-10-15 16:20:35 +0200834 if item in ("nsd_content", "package_content", "nst_content") and method == "PUT":
tiernob24258a2018-10-04 18:39:49 +0200835 completed = self.engine.upload_content(session, engine_topic, _id, indata, kwargs,
836 cherrypy.request.headers, force=force)
tiernof27c79b2018-03-12 17:08:42 +0100837 if not completed:
838 cherrypy.response.headers["Transaction-Id"] = id
tiernof27c79b2018-03-12 17:08:42 +0100839 else:
tiernob24258a2018-10-04 18:39:49 +0200840 self.engine.edit_item(session, engine_topic, _id, indata, kwargs, force=force)
tiernocd54a4a2018-09-12 16:40:35 +0200841 cherrypy.response.status = HTTPStatus.NO_CONTENT.value
tiernoc94c3df2018-02-09 15:38:54 +0100842 else:
843 raise NbiException("Method {} not allowed".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
tiernof27c79b2018-03-12 17:08:42 +0100844 return self._format_out(outdata, session, _format)
tiernob24258a2018-10-04 18:39:49 +0200845 except Exception as e:
tierno36ec8602018-11-02 17:27:11 +0100846 if isinstance(e, (NbiException, EngineException, DbException, FsException, MsgException, AuthException,
847 ValidationError)):
tiernob24258a2018-10-04 18:39:49 +0200848 http_code_value = cherrypy.response.status = e.http_code.value
849 http_code_name = e.http_code.name
850 cherrypy.log("Exception {}".format(e))
851 else:
852 http_code_value = cherrypy.response.status = HTTPStatus.BAD_REQUEST.value # INTERNAL_SERVER_ERROR
Felipe Vicense36ab852018-11-23 14:12:09 +0100853 cherrypy.log("CRITICAL: Exception {}".format(e), traceback=True)
tiernob24258a2018-10-04 18:39:49 +0200854 http_code_name = HTTPStatus.BAD_REQUEST.name
tierno3ace63c2018-05-03 17:51:43 +0200855 if hasattr(outdata, "close"): # is an open file
856 outdata.close()
tiernob24258a2018-10-04 18:39:49 +0200857 error_text = str(e)
858 rollback.reverse()
tiernodb9dc582018-06-20 17:27:29 +0200859 for rollback_item in rollback:
tierno3ace63c2018-05-03 17:51:43 +0200860 try:
tiernocc103432018-10-19 14:10:35 +0200861 if rollback_item.get("operation") == "set":
862 self.engine.db.set_one(rollback_item["topic"], {"_id": rollback_item["_id"]},
863 rollback_item["content"], fail_on_empty=False)
864 else:
865 self.engine.del_item(**rollback_item, session=session, force=True)
tierno3ace63c2018-05-03 17:51:43 +0200866 except Exception as e2:
tiernob24258a2018-10-04 18:39:49 +0200867 rollback_error_text = "Rollback Exception {}: {}".format(rollback_item, e2)
868 cherrypy.log(rollback_error_text)
869 error_text += ". " + rollback_error_text
870 # if isinstance(e, MsgException):
871 # error_text = "{} has been '{}' but other modules cannot be informed because an error on bus".format(
872 # engine_topic[:-1], method, error_text)
tiernoc94c3df2018-02-09 15:38:54 +0100873 problem_details = {
tiernob24258a2018-10-04 18:39:49 +0200874 "code": http_code_name,
875 "status": http_code_value,
876 "detail": error_text,
tiernoc94c3df2018-02-09 15:38:54 +0100877 }
878 return self._format_out(problem_details, session)
879 # raise cherrypy.HTTPError(e.http_code.value, str(e))
880
881
882# def validate_password(realm, username, password):
883# cherrypy.log("realm "+ str(realm))
884# if username == "admin" and password == "admin":
885# return True
886# return False
887
888
889def _start_service():
890 """
891 Callback function called when cherrypy.engine starts
892 Override configuration with env variables
893 Set database, storage, message configuration
894 Init database with admin/admin user password
895 """
896 cherrypy.log.error("Starting osm_nbi")
897 # update general cherrypy configuration
898 update_dict = {}
899
900 engine_config = cherrypy.tree.apps['/osm'].config
901 for k, v in environ.items():
902 if not k.startswith("OSMNBI_"):
903 continue
tiernoe1281182018-05-22 12:24:36 +0200904 k1, _, k2 = k[7:].lower().partition("_")
tiernoc94c3df2018-02-09 15:38:54 +0100905 if not k2:
906 continue
907 try:
908 # update static configuration
909 if k == 'OSMNBI_STATIC_DIR':
910 engine_config["/static"]['tools.staticdir.dir'] = v
911 engine_config["/static"]['tools.staticdir.on'] = True
912 elif k == 'OSMNBI_SOCKET_PORT' or k == 'OSMNBI_SERVER_PORT':
913 update_dict['server.socket_port'] = int(v)
914 elif k == 'OSMNBI_SOCKET_HOST' or k == 'OSMNBI_SERVER_HOST':
915 update_dict['server.socket_host'] = v
tiernof5298be2018-05-16 14:43:57 +0200916 elif k1 in ("server", "test", "auth", "log"):
917 update_dict[k1 + '.' + k2] = v
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100918 elif k1 in ("message", "database", "storage", "authentication"):
tiernof5298be2018-05-16 14:43:57 +0200919 # k2 = k2.replace('_', '.')
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100920 if k2 in ("port", "db_port"):
tiernoc94c3df2018-02-09 15:38:54 +0100921 engine_config[k1][k2] = int(v)
922 else:
923 engine_config[k1][k2] = v
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100924
tiernoc94c3df2018-02-09 15:38:54 +0100925 except ValueError as e:
926 cherrypy.log.error("Ignoring environ '{}': " + str(e))
927 except Exception as e:
928 cherrypy.log.warn("skipping environ '{}' on exception '{}'".format(k, e))
929
930 if update_dict:
931 cherrypy.config.update(update_dict)
tiernof5298be2018-05-16 14:43:57 +0200932 engine_config["global"].update(update_dict)
tiernoc94c3df2018-02-09 15:38:54 +0100933
934 # logging cherrypy
935 log_format_simple = "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(message)s"
936 log_formatter_simple = logging.Formatter(log_format_simple, datefmt='%Y-%m-%dT%H:%M:%S')
937 logger_server = logging.getLogger("cherrypy.error")
938 logger_access = logging.getLogger("cherrypy.access")
939 logger_cherry = logging.getLogger("cherrypy")
940 logger_nbi = logging.getLogger("nbi")
941
tiernof5298be2018-05-16 14:43:57 +0200942 if "log.file" in engine_config["global"]:
943 file_handler = logging.handlers.RotatingFileHandler(engine_config["global"]["log.file"],
tiernoc94c3df2018-02-09 15:38:54 +0100944 maxBytes=100e6, backupCount=9, delay=0)
945 file_handler.setFormatter(log_formatter_simple)
946 logger_cherry.addHandler(file_handler)
947 logger_nbi.addHandler(file_handler)
tiernob24258a2018-10-04 18:39:49 +0200948 # log always to standard output
949 for format_, logger in {"nbi.server %(filename)s:%(lineno)s": logger_server,
950 "nbi.access %(filename)s:%(lineno)s": logger_access,
951 "%(name)s %(filename)s:%(lineno)s": logger_nbi
952 }.items():
953 log_format_cherry = "%(asctime)s %(levelname)s {} %(message)s".format(format_)
954 log_formatter_cherry = logging.Formatter(log_format_cherry, datefmt='%Y-%m-%dT%H:%M:%S')
955 str_handler = logging.StreamHandler()
956 str_handler.setFormatter(log_formatter_cherry)
957 logger.addHandler(str_handler)
tiernoc94c3df2018-02-09 15:38:54 +0100958
tiernof5298be2018-05-16 14:43:57 +0200959 if engine_config["global"].get("log.level"):
960 logger_cherry.setLevel(engine_config["global"]["log.level"])
961 logger_nbi.setLevel(engine_config["global"]["log.level"])
tiernoc94c3df2018-02-09 15:38:54 +0100962
963 # logging other modules
964 for k1, logname in {"message": "nbi.msg", "database": "nbi.db", "storage": "nbi.fs"}.items():
965 engine_config[k1]["logger_name"] = logname
966 logger_module = logging.getLogger(logname)
967 if "logfile" in engine_config[k1]:
968 file_handler = logging.handlers.RotatingFileHandler(engine_config[k1]["logfile"],
tierno2236d202018-05-16 19:05:16 +0200969 maxBytes=100e6, backupCount=9, delay=0)
tiernoc94c3df2018-02-09 15:38:54 +0100970 file_handler.setFormatter(log_formatter_simple)
971 logger_module.addHandler(file_handler)
972 if "loglevel" in engine_config[k1]:
973 logger_module.setLevel(engine_config[k1]["loglevel"])
974 # TODO add more entries, e.g.: storage
975 cherrypy.tree.apps['/osm'].root.engine.start(engine_config)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100976 cherrypy.tree.apps['/osm'].root.authenticator.start(engine_config)
tiernod985a8d2018-10-19 14:12:28 +0200977 cherrypy.tree.apps['/osm'].root.engine.init_db(target_version=database_version)
978 cherrypy.tree.apps['/osm'].root.authenticator.init_db(target_version=auth_database_version)
tiernoc94c3df2018-02-09 15:38:54 +0100979 # getenv('OSMOPENMANO_TENANT', None)
980
981
982def _stop_service():
983 """
984 Callback function called when cherrypy.engine stops
985 TODO: Ending database connections.
986 """
987 cherrypy.tree.apps['/osm'].root.engine.stop()
988 cherrypy.log.error("Stopping osm_nbi")
989
tierno2236d202018-05-16 19:05:16 +0200990
tiernof5298be2018-05-16 14:43:57 +0200991def nbi(config_file):
tiernoc94c3df2018-02-09 15:38:54 +0100992 # conf = {
993 # '/': {
994 # #'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
995 # 'tools.sessions.on': True,
996 # 'tools.response_headers.on': True,
997 # # 'tools.response_headers.headers': [('Content-Type', 'text/plain')],
998 # }
999 # }
1000 # cherrypy.Server.ssl_module = 'builtin'
1001 # cherrypy.Server.ssl_certificate = "http/cert.pem"
1002 # cherrypy.Server.ssl_private_key = "http/privkey.pem"
1003 # cherrypy.Server.thread_pool = 10
1004 # cherrypy.config.update({'Server.socket_port': config["port"], 'Server.socket_host': config["host"]})
1005
1006 # cherrypy.config.update({'tools.auth_basic.on': True,
1007 # 'tools.auth_basic.realm': 'localhost',
1008 # 'tools.auth_basic.checkpassword': validate_password})
1009 cherrypy.engine.subscribe('start', _start_service)
1010 cherrypy.engine.subscribe('stop', _stop_service)
tiernof5298be2018-05-16 14:43:57 +02001011 cherrypy.quickstart(Server(), '/osm', config_file)
1012
1013
1014def usage():
1015 print("""Usage: {} [options]
1016 -c|--config [configuration_file]: loads the configuration file (default: ./nbi.cfg)
1017 -h|--help: shows this help
1018 """.format(sys.argv[0]))
tierno2236d202018-05-16 19:05:16 +02001019 # --log-socket-host HOST: send logs to this host")
1020 # --log-socket-port PORT: send logs using this port (default: 9022)")
tiernoc94c3df2018-02-09 15:38:54 +01001021
1022
1023if __name__ == '__main__':
tiernof5298be2018-05-16 14:43:57 +02001024 try:
1025 # load parameters and configuration
1026 opts, args = getopt.getopt(sys.argv[1:], "hvc:", ["config=", "help"])
1027 # TODO add "log-socket-host=", "log-socket-port=", "log-file="
1028 config_file = None
1029 for o, a in opts:
1030 if o in ("-h", "--help"):
1031 usage()
1032 sys.exit()
1033 elif o in ("-c", "--config"):
1034 config_file = a
1035 # elif o == "--log-socket-port":
1036 # log_socket_port = a
1037 # elif o == "--log-socket-host":
1038 # log_socket_host = a
1039 # elif o == "--log-file":
1040 # log_file = a
1041 else:
1042 assert False, "Unhandled option"
1043 if config_file:
1044 if not path.isfile(config_file):
1045 print("configuration file '{}' that not exist".format(config_file), file=sys.stderr)
1046 exit(1)
1047 else:
1048 for config_file in (__file__[:__file__.rfind(".")] + ".cfg", "./nbi.cfg", "/etc/osm/nbi.cfg"):
1049 if path.isfile(config_file):
1050 break
1051 else:
1052 print("No configuration file 'nbi.cfg' found neither at local folder nor at /etc/osm/", file=sys.stderr)
1053 exit(1)
1054 nbi(config_file)
1055 except getopt.GetoptError as e:
1056 print(str(e), file=sys.stderr)
1057 # usage()
1058 exit(1)