blob: 2dfeb22e52bc5398ff740be4d92ea5238203cb1c [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
tiernof717cbe2018-12-03 16:35:42 +0000447 # if there is not any valid accept, raise an error. But if response is already an error, format in yaml
448 elif cherrypy.response.status >= 400:
tiernoc94c3df2018-02-09 15:38:54 +0100449 raise cherrypy.HTTPError(HTTPStatus.NOT_ACCEPTABLE.value,
450 "Only 'Accept' of type 'application/json' or 'application/yaml' "
451 "for output format are available")
452 cherrypy.response.headers["Content-Type"] = 'application/yaml'
453 return yaml.safe_dump(data, explicit_start=True, indent=4, default_flow_style=False, tags=False,
454 encoding='utf-8', allow_unicode=True) # , canonical=True, default_style='"'
455
456 @cherrypy.expose
457 def index(self, *args, **kwargs):
458 session = None
459 try:
460 if cherrypy.request.method == "GET":
Eduardo Sousa2f988212018-07-26 01:04:11 +0100461 session = self.authenticator.authorize()
tiernoc94c3df2018-02-09 15:38:54 +0100462 outdata = "Index page"
463 else:
464 raise cherrypy.HTTPError(HTTPStatus.METHOD_NOT_ALLOWED.value,
tierno2236d202018-05-16 19:05:16 +0200465 "Method {} not allowed for tokens".format(cherrypy.request.method))
tiernoc94c3df2018-02-09 15:38:54 +0100466
467 return self._format_out(outdata, session)
468
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100469 except (EngineException, AuthException) as e:
tiernoc94c3df2018-02-09 15:38:54 +0100470 cherrypy.log("index Exception {}".format(e))
471 cherrypy.response.status = e.http_code.value
472 return self._format_out("Welcome to OSM!", session)
473
474 @cherrypy.expose
tierno55945e72018-04-06 16:40:27 +0200475 def version(self, *args, **kwargs):
tiernodfe09572018-04-24 10:41:10 +0200476 # TODO consider to remove and provide version using the static version file
tierno55945e72018-04-06 16:40:27 +0200477 global __version__, version_date
478 try:
479 if cherrypy.request.method != "GET":
480 raise NbiException("Only method GET is allowed", HTTPStatus.METHOD_NOT_ALLOWED)
481 elif args or kwargs:
482 raise NbiException("Invalid URL or query string for version", HTTPStatus.METHOD_NOT_ALLOWED)
483 return __version__ + " " + version_date
484 except NbiException as e:
485 cherrypy.response.status = e.http_code.value
486 problem_details = {
487 "code": e.http_code.name,
488 "status": e.http_code.value,
489 "detail": str(e),
490 }
491 return self._format_out(problem_details, None)
492
493 @cherrypy.expose
tiernof27c79b2018-03-12 17:08:42 +0100494 def token(self, method, token_id=None, kwargs=None):
tiernoc94c3df2018-02-09 15:38:54 +0100495 session = None
496 # self.engine.load_dbase(cherrypy.request.app.config)
tiernof27c79b2018-03-12 17:08:42 +0100497 indata = self._format_in(kwargs)
498 if not isinstance(indata, dict):
499 raise NbiException("Expected application/yaml or application/json Content-Type", HTTPStatus.BAD_REQUEST)
tiernoc94c3df2018-02-09 15:38:54 +0100500 try:
tiernoc94c3df2018-02-09 15:38:54 +0100501 if method == "GET":
Eduardo Sousa2f988212018-07-26 01:04:11 +0100502 session = self.authenticator.authorize()
tiernof27c79b2018-03-12 17:08:42 +0100503 if token_id:
Eduardo Sousa2f988212018-07-26 01:04:11 +0100504 outdata = self.authenticator.get_token(session, token_id)
tiernoc94c3df2018-02-09 15:38:54 +0100505 else:
Eduardo Sousa2f988212018-07-26 01:04:11 +0100506 outdata = self.authenticator.get_token_list(session)
tiernoc94c3df2018-02-09 15:38:54 +0100507 elif method == "POST":
508 try:
Eduardo Sousa2f988212018-07-26 01:04:11 +0100509 session = self.authenticator.authorize()
tiernoe1281182018-05-22 12:24:36 +0200510 except Exception:
tiernoc94c3df2018-02-09 15:38:54 +0100511 session = None
512 if kwargs:
513 indata.update(kwargs)
Eduardo Sousa2f988212018-07-26 01:04:11 +0100514 outdata = self.authenticator.new_token(session, indata, cherrypy.request.remote)
tiernoc94c3df2018-02-09 15:38:54 +0100515 session = outdata
516 cherrypy.session['Authorization'] = outdata["_id"]
tierno0f98af52018-03-19 10:28:22 +0100517 self._set_location_header("admin", "v1", "tokens", outdata["_id"])
tiernoc94c3df2018-02-09 15:38:54 +0100518 # cherrypy.response.cookie["Authorization"] = outdata["id"]
519 # cherrypy.response.cookie["Authorization"]['expires'] = 3600
520 elif method == "DELETE":
tiernof27c79b2018-03-12 17:08:42 +0100521 if not token_id and "id" in kwargs:
tiernoc94c3df2018-02-09 15:38:54 +0100522 token_id = kwargs["id"]
tiernof27c79b2018-03-12 17:08:42 +0100523 elif not token_id:
Eduardo Sousa2f988212018-07-26 01:04:11 +0100524 session = self.authenticator.authorize()
tiernoc94c3df2018-02-09 15:38:54 +0100525 token_id = session["_id"]
Eduardo Sousa2f988212018-07-26 01:04:11 +0100526 outdata = self.authenticator.del_token(token_id)
tiernoc94c3df2018-02-09 15:38:54 +0100527 session = None
528 cherrypy.session['Authorization'] = "logout"
529 # cherrypy.response.cookie["Authorization"] = token_id
530 # cherrypy.response.cookie["Authorization"]['expires'] = 0
531 else:
532 raise NbiException("Method {} not allowed for token".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
533 return self._format_out(outdata, session)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100534 except (NbiException, EngineException, DbException, AuthException) as e:
tiernoc94c3df2018-02-09 15:38:54 +0100535 cherrypy.log("tokens Exception {}".format(e))
536 cherrypy.response.status = e.http_code.value
537 problem_details = {
538 "code": e.http_code.name,
539 "status": e.http_code.value,
540 "detail": str(e),
541 }
542 return self._format_out(problem_details, session)
543
544 @cherrypy.expose
545 def test(self, *args, **kwargs):
546 thread_info = None
tiernof27c79b2018-03-12 17:08:42 +0100547 if args and args[0] == "help":
tiernof717cbe2018-12-03 16:35:42 +0000548 return "<html><pre>\ninit\nfile/<name> download file\ndb-clear/table\nfs-clear[/folder]\nlogin\nlogin2\n"\
tiernoe1281182018-05-22 12:24:36 +0200549 "sleep/<time>\nmessage/topic\n</pre></html>"
tiernof27c79b2018-03-12 17:08:42 +0100550
551 elif args and args[0] == "init":
tiernoc94c3df2018-02-09 15:38:54 +0100552 try:
553 # self.engine.load_dbase(cherrypy.request.app.config)
554 self.engine.create_admin()
555 return "Done. User 'admin', password 'admin' created"
556 except Exception:
557 cherrypy.response.status = HTTPStatus.FORBIDDEN.value
558 return self._format_out("Database already initialized")
tiernof27c79b2018-03-12 17:08:42 +0100559 elif args and args[0] == "file":
560 return cherrypy.lib.static.serve_file(cherrypy.tree.apps['/osm'].config["storage"]["path"] + "/" + args[1],
561 "text/plain", "attachment")
562 elif args and args[0] == "file2":
563 f_path = cherrypy.tree.apps['/osm'].config["storage"]["path"] + "/" + args[1]
564 f = open(f_path, "r")
565 cherrypy.response.headers["Content-type"] = "text/plain"
tiernof27c79b2018-03-12 17:08:42 +0100566 return f
tierno55945e72018-04-06 16:40:27 +0200567
tiernof27c79b2018-03-12 17:08:42 +0100568 elif len(args) == 2 and args[0] == "db-clear":
tiernof717cbe2018-12-03 16:35:42 +0000569 deleted_info = self.engine.db.del_list(args[1], kwargs)
570 return "{} {} deleted\n".format(deleted_info["deleted"], args[1])
571 elif len(args) and args[0] == "fs-clear":
572 if len(args) >= 2:
573 folders = (args[1],)
574 else:
575 folders = self.engine.fs.dir_ls(".")
576 for folder in folders:
577 self.engine.fs.file_delete(folder)
578 return ",".join(folders) + " folders deleted\n"
tiernoc94c3df2018-02-09 15:38:54 +0100579 elif args and args[0] == "login":
580 if not cherrypy.request.headers.get("Authorization"):
581 cherrypy.response.headers["WWW-Authenticate"] = 'Basic realm="Access to OSM site", charset="UTF-8"'
582 cherrypy.response.status = HTTPStatus.UNAUTHORIZED.value
583 elif args and args[0] == "login2":
584 if not cherrypy.request.headers.get("Authorization"):
585 cherrypy.response.headers["WWW-Authenticate"] = 'Bearer realm="Access to OSM site"'
586 cherrypy.response.status = HTTPStatus.UNAUTHORIZED.value
587 elif args and args[0] == "sleep":
588 sleep_time = 5
589 try:
590 sleep_time = int(args[1])
591 except Exception:
592 cherrypy.response.status = HTTPStatus.FORBIDDEN.value
593 return self._format_out("Database already initialized")
594 thread_info = cherrypy.thread_data
595 print(thread_info)
596 time.sleep(sleep_time)
597 # thread_info
598 elif len(args) >= 2 and args[0] == "message":
tiernob24258a2018-10-04 18:39:49 +0200599 main_topic = args[1]
600 return_text = "<html><pre>{} ->\n".format(main_topic)
tiernoc94c3df2018-02-09 15:38:54 +0100601 try:
tierno55945e72018-04-06 16:40:27 +0200602 if cherrypy.request.method == 'POST':
603 to_send = yaml.load(cherrypy.request.body)
604 for k, v in to_send.items():
tiernob24258a2018-10-04 18:39:49 +0200605 self.engine.msg.write(main_topic, k, v)
tierno55945e72018-04-06 16:40:27 +0200606 return_text += " {}: {}\n".format(k, v)
607 elif cherrypy.request.method == 'GET':
608 for k, v in kwargs.items():
tiernob24258a2018-10-04 18:39:49 +0200609 self.engine.msg.write(main_topic, k, yaml.load(v))
tierno55945e72018-04-06 16:40:27 +0200610 return_text += " {}: {}\n".format(k, yaml.load(v))
tiernoc94c3df2018-02-09 15:38:54 +0100611 except Exception as e:
tierno55945e72018-04-06 16:40:27 +0200612 return_text += "Error: " + str(e)
613 return_text += "</pre></html>\n"
614 return return_text
tiernoc94c3df2018-02-09 15:38:54 +0100615
616 return_text = (
617 "<html><pre>\nheaders:\n args: {}\n".format(args) +
618 " kwargs: {}\n".format(kwargs) +
619 " headers: {}\n".format(cherrypy.request.headers) +
620 " path_info: {}\n".format(cherrypy.request.path_info) +
621 " query_string: {}\n".format(cherrypy.request.query_string) +
622 " session: {}\n".format(cherrypy.session) +
623 " cookie: {}\n".format(cherrypy.request.cookie) +
624 " method: {}\n".format(cherrypy.request.method) +
tiernof717cbe2018-12-03 16:35:42 +0000625 " session: {}\n".format(cherrypy.session.get('fieldname')) +
tiernoc94c3df2018-02-09 15:38:54 +0100626 " body:\n")
627 return_text += " length: {}\n".format(cherrypy.request.body.length)
628 if cherrypy.request.body.length:
629 return_text += " content: {}\n".format(
630 str(cherrypy.request.body.read(int(cherrypy.request.headers.get('Content-Length', 0)))))
631 if thread_info:
632 return_text += "thread: {}\n".format(thread_info)
633 return_text += "</pre></html>"
634 return return_text
635
tiernof27c79b2018-03-12 17:08:42 +0100636 def _check_valid_url_method(self, method, *args):
637 if len(args) < 3:
tiernob24258a2018-10-04 18:39:49 +0200638 raise NbiException("URL must contain at least 'main_topic/version/topic'", HTTPStatus.METHOD_NOT_ALLOWED)
tiernof27c79b2018-03-12 17:08:42 +0100639
640 reference = self.valid_methods
641 for arg in args:
642 if arg is None:
643 break
644 if not isinstance(reference, dict):
645 raise NbiException("URL contains unexpected extra items '{}'".format(arg),
646 HTTPStatus.METHOD_NOT_ALLOWED)
647
648 if arg in reference:
649 reference = reference[arg]
650 elif "<ID>" in reference:
651 reference = reference["<ID>"]
652 elif "*" in reference:
653 reference = reference["*"]
654 break
655 else:
656 raise NbiException("Unexpected URL item {}".format(arg), HTTPStatus.METHOD_NOT_ALLOWED)
657 if "TODO" in reference and method in reference["TODO"]:
658 raise NbiException("Method {} not supported yet for this URL".format(method), HTTPStatus.NOT_IMPLEMENTED)
tierno2236d202018-05-16 19:05:16 +0200659 elif "METHODS" in reference and method not in reference["METHODS"]:
tiernof27c79b2018-03-12 17:08:42 +0100660 raise NbiException("Method {} not supported for this URL".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
661 return
662
663 @staticmethod
tiernob24258a2018-10-04 18:39:49 +0200664 def _set_location_header(main_topic, version, topic, id):
tiernof27c79b2018-03-12 17:08:42 +0100665 """
666 Insert response header Location with the URL of created item base on URL params
tiernob24258a2018-10-04 18:39:49 +0200667 :param main_topic:
tiernof27c79b2018-03-12 17:08:42 +0100668 :param version:
tiernob24258a2018-10-04 18:39:49 +0200669 :param topic:
tiernof27c79b2018-03-12 17:08:42 +0100670 :param id:
671 :return: None
672 """
673 # 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 +0200674 cherrypy.response.headers["Location"] = "/osm/{}/{}/{}/{}".format(main_topic, version, topic, id)
tiernof27c79b2018-03-12 17:08:42 +0100675 return
676
tiernoc94c3df2018-02-09 15:38:54 +0100677 @cherrypy.expose
tiernob24258a2018-10-04 18:39:49 +0200678 def default(self, main_topic=None, version=None, topic=None, _id=None, item=None, *args, **kwargs):
tiernoc94c3df2018-02-09 15:38:54 +0100679 session = None
tiernof27c79b2018-03-12 17:08:42 +0100680 outdata = None
681 _format = None
tierno0f98af52018-03-19 10:28:22 +0100682 method = "DONE"
tiernob24258a2018-10-04 18:39:49 +0200683 engine_topic = None
tiernodb9dc582018-06-20 17:27:29 +0200684 rollback = []
685 session = None
tiernoc94c3df2018-02-09 15:38:54 +0100686 try:
tiernob24258a2018-10-04 18:39:49 +0200687 if not main_topic or not version or not topic:
688 raise NbiException("URL must contain at least 'main_topic/version/topic'",
689 HTTPStatus.METHOD_NOT_ALLOWED)
tiernocd65be32018-11-16 16:34:49 +0100690 if main_topic not in ("admin", "vnfpkgm", "nsd", "nslcm", "pdu", "nst", "nsilcm"):
tiernob24258a2018-10-04 18:39:49 +0200691 raise NbiException("URL main_topic '{}' not supported".format(main_topic),
692 HTTPStatus.METHOD_NOT_ALLOWED)
tiernoc94c3df2018-02-09 15:38:54 +0100693 if version != 'v1':
694 raise NbiException("URL version '{}' not supported".format(version), HTTPStatus.METHOD_NOT_ALLOWED)
695
tiernof27c79b2018-03-12 17:08:42 +0100696 if kwargs and "METHOD" in kwargs and kwargs["METHOD"] in ("PUT", "POST", "DELETE", "GET", "PATCH"):
697 method = kwargs.pop("METHOD")
698 else:
699 method = cherrypy.request.method
tiernob92094f2018-05-11 13:44:22 +0200700 if kwargs and "FORCE" in kwargs:
701 force = kwargs.pop("FORCE")
702 else:
703 force = False
tiernob24258a2018-10-04 18:39:49 +0200704 self._check_valid_url_method(method, main_topic, version, topic, _id, item, *args)
tiernob24258a2018-10-04 18:39:49 +0200705 if main_topic == "admin" and topic == "tokens":
tiernof27c79b2018-03-12 17:08:42 +0100706 return self.token(method, _id, kwargs)
707
tiernoc94c3df2018-02-09 15:38:54 +0100708 # self.engine.load_dbase(cherrypy.request.app.config)
Eduardo Sousa2f988212018-07-26 01:04:11 +0100709 session = self.authenticator.authorize()
tiernof27c79b2018-03-12 17:08:42 +0100710 indata = self._format_in(kwargs)
tiernob24258a2018-10-04 18:39:49 +0200711 engine_topic = topic
712 if topic == "subscriptions":
713 engine_topic = main_topic + "_" + topic
714 if item:
715 engine_topic = item
tiernoc94c3df2018-02-09 15:38:54 +0100716
tiernob24258a2018-10-04 18:39:49 +0200717 if main_topic == "nsd":
718 engine_topic = "nsds"
719 elif main_topic == "vnfpkgm":
720 engine_topic = "vnfds"
721 elif main_topic == "nslcm":
722 engine_topic = "nsrs"
723 if topic == "ns_lcm_op_occs":
724 engine_topic = "nslcmops"
725 if topic == "vnfrs" or topic == "vnf_instances":
726 engine_topic = "vnfrs"
garciadeblas9750c5a2018-10-15 16:20:35 +0200727 elif main_topic == "nst":
728 engine_topic = "nsts"
729 elif main_topic == "nsilcm":
730 engine_topic = "nsis"
731 if topic == "nsi_lcm_op_occs":
Felipe Vicensb57758d2018-10-16 16:00:20 +0200732 engine_topic = "nsilcmops"
tiernob24258a2018-10-04 18:39:49 +0200733 elif main_topic == "pdu":
734 engine_topic = "pdus"
735 if engine_topic == "vims": # TODO this is for backward compatibility, it will remove in the future
736 engine_topic = "vim_accounts"
tiernoc94c3df2018-02-09 15:38:54 +0100737
738 if method == "GET":
Felipe Vicens07f31722018-10-29 15:16:44 +0100739 if item in ("nsd_content", "package_content", "artifacts", "vnfd", "nsd", "nst", "nst_content"):
garciadeblas9750c5a2018-10-15 16:20:35 +0200740 if item in ("vnfd", "nsd", "nst"):
tiernof27c79b2018-03-12 17:08:42 +0100741 path = "$DESCRIPTOR"
742 elif args:
743 path = args
tiernob24258a2018-10-04 18:39:49 +0200744 elif item == "artifacts":
tiernof27c79b2018-03-12 17:08:42 +0100745 path = ()
746 else:
747 path = None
tiernob24258a2018-10-04 18:39:49 +0200748 file, _format = self.engine.get_file(session, engine_topic, _id, path,
tierno2236d202018-05-16 19:05:16 +0200749 cherrypy.request.headers.get("Accept"))
tiernof27c79b2018-03-12 17:08:42 +0100750 outdata = file
751 elif not _id:
tiernob24258a2018-10-04 18:39:49 +0200752 outdata = self.engine.get_item_list(session, engine_topic, kwargs)
tiernoc94c3df2018-02-09 15:38:54 +0100753 else:
tiernob24258a2018-10-04 18:39:49 +0200754 outdata = self.engine.get_item(session, engine_topic, _id)
tiernof27c79b2018-03-12 17:08:42 +0100755 elif method == "POST":
garciadeblas9750c5a2018-10-15 16:20:35 +0200756 if topic in ("ns_descriptors_content", "vnf_packages_content", "netslice_templates_content"):
tiernof27c79b2018-03-12 17:08:42 +0100757 _id = cherrypy.request.headers.get("Transaction-Id")
758 if not _id:
tiernob24258a2018-10-04 18:39:49 +0200759 _id = self.engine.new_item(rollback, session, engine_topic, {}, None, cherrypy.request.headers,
tiernob92094f2018-05-11 13:44:22 +0200760 force=force)
tiernob24258a2018-10-04 18:39:49 +0200761 completed = self.engine.upload_content(session, engine_topic, _id, indata, kwargs,
762 cherrypy.request.headers, force=force)
tiernof27c79b2018-03-12 17:08:42 +0100763 if completed:
tiernob24258a2018-10-04 18:39:49 +0200764 self._set_location_header(main_topic, version, topic, _id)
tiernof27c79b2018-03-12 17:08:42 +0100765 else:
766 cherrypy.response.headers["Transaction-Id"] = _id
767 outdata = {"id": _id}
tiernob24258a2018-10-04 18:39:49 +0200768 elif topic == "ns_instances_content":
769 # creates NSR
770 _id = self.engine.new_item(rollback, session, engine_topic, indata, kwargs, force=force)
771 # creates nslcmop
772 indata["lcmOperationType"] = "instantiate"
773 indata["nsInstanceId"] = _id
774 self.engine.new_item(rollback, session, "nslcmops", indata, None)
775 self._set_location_header(main_topic, version, topic, _id)
tiernof27c79b2018-03-12 17:08:42 +0100776 outdata = {"id": _id}
tiernob24258a2018-10-04 18:39:49 +0200777 elif topic == "ns_instances" and item:
778 indata["lcmOperationType"] = item
779 indata["nsInstanceId"] = _id
780 _id = self.engine.new_item(rollback, session, "nslcmops", indata, kwargs)
781 self._set_location_header(main_topic, version, "ns_lcm_op_occs", _id)
tierno65acb4d2018-04-06 16:42:40 +0200782 outdata = {"id": _id}
783 cherrypy.response.status = HTTPStatus.ACCEPTED.value
garciadeblas9750c5a2018-10-15 16:20:35 +0200784 elif topic == "netslice_instances_content":
Felipe Vicens07f31722018-10-29 15:16:44 +0100785 # creates NetSlice_Instance_record (NSIR)
garciadeblas9750c5a2018-10-15 16:20:35 +0200786 _id = self.engine.new_item(rollback, session, engine_topic, indata, kwargs, force=force)
Felipe Vicens07f31722018-10-29 15:16:44 +0100787 self._set_location_header(main_topic, version, topic, _id)
garciadeblas9750c5a2018-10-15 16:20:35 +0200788 indata["lcmOperationType"] = "instantiate"
789 indata["nsiInstanceId"] = _id
Felipe Vicens07f31722018-10-29 15:16:44 +0100790 self.engine.new_item(rollback, session, "nsilcmops", indata, kwargs)
garciadeblas9750c5a2018-10-15 16:20:35 +0200791 outdata = {"id": _id}
Felipe Vicens07f31722018-10-29 15:16:44 +0100792
garciadeblas9750c5a2018-10-15 16:20:35 +0200793 elif topic == "netslice_instances" and item:
794 indata["lcmOperationType"] = item
795 indata["nsiInstanceId"] = _id
796 _id = self.engine.new_item(rollback, session, "nsilcmops", indata, kwargs)
797 self._set_location_header(main_topic, version, "nsi_lcm_op_occs", _id)
798 outdata = {"id": _id}
799 cherrypy.response.status = HTTPStatus.ACCEPTED.value
tiernof27c79b2018-03-12 17:08:42 +0100800 else:
tiernob24258a2018-10-04 18:39:49 +0200801 _id = self.engine.new_item(rollback, session, engine_topic, indata, kwargs,
802 cherrypy.request.headers, force=force)
803 self._set_location_header(main_topic, version, topic, _id)
tiernof27c79b2018-03-12 17:08:42 +0100804 outdata = {"id": _id}
tiernob24258a2018-10-04 18:39:49 +0200805 # TODO form NsdInfo when topic in ("ns_descriptors", "vnf_packages")
tiernof27c79b2018-03-12 17:08:42 +0100806 cherrypy.response.status = HTTPStatus.CREATED.value
tierno09c073e2018-04-26 13:36:48 +0200807
tiernoc94c3df2018-02-09 15:38:54 +0100808 elif method == "DELETE":
809 if not _id:
tiernob24258a2018-10-04 18:39:49 +0200810 outdata = self.engine.del_item_list(session, engine_topic, kwargs)
tierno09c073e2018-04-26 13:36:48 +0200811 cherrypy.response.status = HTTPStatus.OK.value
tiernoc94c3df2018-02-09 15:38:54 +0100812 else: # len(args) > 1
tiernob24258a2018-10-04 18:39:49 +0200813 if topic == "ns_instances_content" and not force:
814 nslcmop_desc = {
815 "lcmOperationType": "terminate",
816 "nsInstanceId": _id,
817 "autoremove": True
818 }
819 opp_id = self.engine.new_item(rollback, session, "nslcmops", nslcmop_desc, None)
tierno3ace63c2018-05-03 17:51:43 +0200820 outdata = {"_id": opp_id}
tierno09c073e2018-04-26 13:36:48 +0200821 cherrypy.response.status = HTTPStatus.ACCEPTED.value
garciadeblas9750c5a2018-10-15 16:20:35 +0200822 elif topic == "netslice_instances_content" and not force:
823 nsilcmop_desc = {
824 "lcmOperationType": "terminate",
825 "nsiInstanceId": _id,
826 "autoremove": True
827 }
828 opp_id = self.engine.new_item(rollback, session, "nsilcmops", nsilcmop_desc, None)
829 outdata = {"_id": opp_id}
830 cherrypy.response.status = HTTPStatus.ACCEPTED.value
tierno65acb4d2018-04-06 16:42:40 +0200831 else:
tiernob24258a2018-10-04 18:39:49 +0200832 self.engine.del_item(session, engine_topic, _id, force)
tierno09c073e2018-04-26 13:36:48 +0200833 cherrypy.response.status = HTTPStatus.NO_CONTENT.value
tiernob24258a2018-10-04 18:39:49 +0200834 if engine_topic in ("vim_accounts", "sdns"):
tierno09c073e2018-04-26 13:36:48 +0200835 cherrypy.response.status = HTTPStatus.ACCEPTED.value
836
tierno7ae10112018-05-18 14:36:02 +0200837 elif method in ("PUT", "PATCH"):
tiernocd54a4a2018-09-12 16:40:35 +0200838 outdata = None
tiernof27c79b2018-03-12 17:08:42 +0100839 if not indata and not kwargs:
tiernoc94c3df2018-02-09 15:38:54 +0100840 raise NbiException("Nothing to update. Provide payload and/or query string",
841 HTTPStatus.BAD_REQUEST)
garciadeblas9750c5a2018-10-15 16:20:35 +0200842 if item in ("nsd_content", "package_content", "nst_content") and method == "PUT":
tiernob24258a2018-10-04 18:39:49 +0200843 completed = self.engine.upload_content(session, engine_topic, _id, indata, kwargs,
844 cherrypy.request.headers, force=force)
tiernof27c79b2018-03-12 17:08:42 +0100845 if not completed:
846 cherrypy.response.headers["Transaction-Id"] = id
tiernof27c79b2018-03-12 17:08:42 +0100847 else:
tiernob24258a2018-10-04 18:39:49 +0200848 self.engine.edit_item(session, engine_topic, _id, indata, kwargs, force=force)
tiernocd54a4a2018-09-12 16:40:35 +0200849 cherrypy.response.status = HTTPStatus.NO_CONTENT.value
tiernoc94c3df2018-02-09 15:38:54 +0100850 else:
851 raise NbiException("Method {} not allowed".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
tiernof27c79b2018-03-12 17:08:42 +0100852 return self._format_out(outdata, session, _format)
tiernob24258a2018-10-04 18:39:49 +0200853 except Exception as e:
tierno36ec8602018-11-02 17:27:11 +0100854 if isinstance(e, (NbiException, EngineException, DbException, FsException, MsgException, AuthException,
855 ValidationError)):
tiernob24258a2018-10-04 18:39:49 +0200856 http_code_value = cherrypy.response.status = e.http_code.value
857 http_code_name = e.http_code.name
858 cherrypy.log("Exception {}".format(e))
859 else:
860 http_code_value = cherrypy.response.status = HTTPStatus.BAD_REQUEST.value # INTERNAL_SERVER_ERROR
Felipe Vicense36ab852018-11-23 14:12:09 +0100861 cherrypy.log("CRITICAL: Exception {}".format(e), traceback=True)
tiernob24258a2018-10-04 18:39:49 +0200862 http_code_name = HTTPStatus.BAD_REQUEST.name
tierno3ace63c2018-05-03 17:51:43 +0200863 if hasattr(outdata, "close"): # is an open file
864 outdata.close()
tiernob24258a2018-10-04 18:39:49 +0200865 error_text = str(e)
866 rollback.reverse()
tiernodb9dc582018-06-20 17:27:29 +0200867 for rollback_item in rollback:
tierno3ace63c2018-05-03 17:51:43 +0200868 try:
tiernocc103432018-10-19 14:10:35 +0200869 if rollback_item.get("operation") == "set":
870 self.engine.db.set_one(rollback_item["topic"], {"_id": rollback_item["_id"]},
871 rollback_item["content"], fail_on_empty=False)
872 else:
873 self.engine.del_item(**rollback_item, session=session, force=True)
tierno3ace63c2018-05-03 17:51:43 +0200874 except Exception as e2:
tiernob24258a2018-10-04 18:39:49 +0200875 rollback_error_text = "Rollback Exception {}: {}".format(rollback_item, e2)
876 cherrypy.log(rollback_error_text)
877 error_text += ". " + rollback_error_text
878 # if isinstance(e, MsgException):
879 # error_text = "{} has been '{}' but other modules cannot be informed because an error on bus".format(
880 # engine_topic[:-1], method, error_text)
tiernoc94c3df2018-02-09 15:38:54 +0100881 problem_details = {
tiernob24258a2018-10-04 18:39:49 +0200882 "code": http_code_name,
883 "status": http_code_value,
884 "detail": error_text,
tiernoc94c3df2018-02-09 15:38:54 +0100885 }
886 return self._format_out(problem_details, session)
887 # raise cherrypy.HTTPError(e.http_code.value, str(e))
888
889
890# def validate_password(realm, username, password):
891# cherrypy.log("realm "+ str(realm))
892# if username == "admin" and password == "admin":
893# return True
894# return False
895
896
897def _start_service():
898 """
899 Callback function called when cherrypy.engine starts
900 Override configuration with env variables
901 Set database, storage, message configuration
902 Init database with admin/admin user password
903 """
904 cherrypy.log.error("Starting osm_nbi")
905 # update general cherrypy configuration
906 update_dict = {}
907
908 engine_config = cherrypy.tree.apps['/osm'].config
909 for k, v in environ.items():
910 if not k.startswith("OSMNBI_"):
911 continue
tiernoe1281182018-05-22 12:24:36 +0200912 k1, _, k2 = k[7:].lower().partition("_")
tiernoc94c3df2018-02-09 15:38:54 +0100913 if not k2:
914 continue
915 try:
916 # update static configuration
917 if k == 'OSMNBI_STATIC_DIR':
918 engine_config["/static"]['tools.staticdir.dir'] = v
919 engine_config["/static"]['tools.staticdir.on'] = True
920 elif k == 'OSMNBI_SOCKET_PORT' or k == 'OSMNBI_SERVER_PORT':
921 update_dict['server.socket_port'] = int(v)
922 elif k == 'OSMNBI_SOCKET_HOST' or k == 'OSMNBI_SERVER_HOST':
923 update_dict['server.socket_host'] = v
tiernof5298be2018-05-16 14:43:57 +0200924 elif k1 in ("server", "test", "auth", "log"):
925 update_dict[k1 + '.' + k2] = v
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100926 elif k1 in ("message", "database", "storage", "authentication"):
tiernof5298be2018-05-16 14:43:57 +0200927 # k2 = k2.replace('_', '.')
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100928 if k2 in ("port", "db_port"):
tiernoc94c3df2018-02-09 15:38:54 +0100929 engine_config[k1][k2] = int(v)
930 else:
931 engine_config[k1][k2] = v
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100932
tiernoc94c3df2018-02-09 15:38:54 +0100933 except ValueError as e:
934 cherrypy.log.error("Ignoring environ '{}': " + str(e))
935 except Exception as e:
936 cherrypy.log.warn("skipping environ '{}' on exception '{}'".format(k, e))
937
938 if update_dict:
939 cherrypy.config.update(update_dict)
tiernof5298be2018-05-16 14:43:57 +0200940 engine_config["global"].update(update_dict)
tiernoc94c3df2018-02-09 15:38:54 +0100941
942 # logging cherrypy
943 log_format_simple = "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(message)s"
944 log_formatter_simple = logging.Formatter(log_format_simple, datefmt='%Y-%m-%dT%H:%M:%S')
945 logger_server = logging.getLogger("cherrypy.error")
946 logger_access = logging.getLogger("cherrypy.access")
947 logger_cherry = logging.getLogger("cherrypy")
948 logger_nbi = logging.getLogger("nbi")
949
tiernof5298be2018-05-16 14:43:57 +0200950 if "log.file" in engine_config["global"]:
951 file_handler = logging.handlers.RotatingFileHandler(engine_config["global"]["log.file"],
tiernoc94c3df2018-02-09 15:38:54 +0100952 maxBytes=100e6, backupCount=9, delay=0)
953 file_handler.setFormatter(log_formatter_simple)
954 logger_cherry.addHandler(file_handler)
955 logger_nbi.addHandler(file_handler)
tiernob24258a2018-10-04 18:39:49 +0200956 # log always to standard output
957 for format_, logger in {"nbi.server %(filename)s:%(lineno)s": logger_server,
958 "nbi.access %(filename)s:%(lineno)s": logger_access,
959 "%(name)s %(filename)s:%(lineno)s": logger_nbi
960 }.items():
961 log_format_cherry = "%(asctime)s %(levelname)s {} %(message)s".format(format_)
962 log_formatter_cherry = logging.Formatter(log_format_cherry, datefmt='%Y-%m-%dT%H:%M:%S')
963 str_handler = logging.StreamHandler()
964 str_handler.setFormatter(log_formatter_cherry)
965 logger.addHandler(str_handler)
tiernoc94c3df2018-02-09 15:38:54 +0100966
tiernof5298be2018-05-16 14:43:57 +0200967 if engine_config["global"].get("log.level"):
968 logger_cherry.setLevel(engine_config["global"]["log.level"])
969 logger_nbi.setLevel(engine_config["global"]["log.level"])
tiernoc94c3df2018-02-09 15:38:54 +0100970
971 # logging other modules
972 for k1, logname in {"message": "nbi.msg", "database": "nbi.db", "storage": "nbi.fs"}.items():
973 engine_config[k1]["logger_name"] = logname
974 logger_module = logging.getLogger(logname)
975 if "logfile" in engine_config[k1]:
976 file_handler = logging.handlers.RotatingFileHandler(engine_config[k1]["logfile"],
tierno2236d202018-05-16 19:05:16 +0200977 maxBytes=100e6, backupCount=9, delay=0)
tiernoc94c3df2018-02-09 15:38:54 +0100978 file_handler.setFormatter(log_formatter_simple)
979 logger_module.addHandler(file_handler)
980 if "loglevel" in engine_config[k1]:
981 logger_module.setLevel(engine_config[k1]["loglevel"])
982 # TODO add more entries, e.g.: storage
983 cherrypy.tree.apps['/osm'].root.engine.start(engine_config)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100984 cherrypy.tree.apps['/osm'].root.authenticator.start(engine_config)
tiernod985a8d2018-10-19 14:12:28 +0200985 cherrypy.tree.apps['/osm'].root.engine.init_db(target_version=database_version)
986 cherrypy.tree.apps['/osm'].root.authenticator.init_db(target_version=auth_database_version)
tiernoc94c3df2018-02-09 15:38:54 +0100987 # getenv('OSMOPENMANO_TENANT', None)
988
989
990def _stop_service():
991 """
992 Callback function called when cherrypy.engine stops
993 TODO: Ending database connections.
994 """
995 cherrypy.tree.apps['/osm'].root.engine.stop()
996 cherrypy.log.error("Stopping osm_nbi")
997
tierno2236d202018-05-16 19:05:16 +0200998
tiernof5298be2018-05-16 14:43:57 +0200999def nbi(config_file):
tiernoc94c3df2018-02-09 15:38:54 +01001000 # conf = {
1001 # '/': {
1002 # #'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
1003 # 'tools.sessions.on': True,
1004 # 'tools.response_headers.on': True,
1005 # # 'tools.response_headers.headers': [('Content-Type', 'text/plain')],
1006 # }
1007 # }
1008 # cherrypy.Server.ssl_module = 'builtin'
1009 # cherrypy.Server.ssl_certificate = "http/cert.pem"
1010 # cherrypy.Server.ssl_private_key = "http/privkey.pem"
1011 # cherrypy.Server.thread_pool = 10
1012 # cherrypy.config.update({'Server.socket_port': config["port"], 'Server.socket_host': config["host"]})
1013
1014 # cherrypy.config.update({'tools.auth_basic.on': True,
1015 # 'tools.auth_basic.realm': 'localhost',
1016 # 'tools.auth_basic.checkpassword': validate_password})
1017 cherrypy.engine.subscribe('start', _start_service)
1018 cherrypy.engine.subscribe('stop', _stop_service)
tiernof5298be2018-05-16 14:43:57 +02001019 cherrypy.quickstart(Server(), '/osm', config_file)
1020
1021
1022def usage():
1023 print("""Usage: {} [options]
1024 -c|--config [configuration_file]: loads the configuration file (default: ./nbi.cfg)
1025 -h|--help: shows this help
1026 """.format(sys.argv[0]))
tierno2236d202018-05-16 19:05:16 +02001027 # --log-socket-host HOST: send logs to this host")
1028 # --log-socket-port PORT: send logs using this port (default: 9022)")
tiernoc94c3df2018-02-09 15:38:54 +01001029
1030
1031if __name__ == '__main__':
tiernof5298be2018-05-16 14:43:57 +02001032 try:
1033 # load parameters and configuration
1034 opts, args = getopt.getopt(sys.argv[1:], "hvc:", ["config=", "help"])
1035 # TODO add "log-socket-host=", "log-socket-port=", "log-file="
1036 config_file = None
1037 for o, a in opts:
1038 if o in ("-h", "--help"):
1039 usage()
1040 sys.exit()
1041 elif o in ("-c", "--config"):
1042 config_file = a
1043 # elif o == "--log-socket-port":
1044 # log_socket_port = a
1045 # elif o == "--log-socket-host":
1046 # log_socket_host = a
1047 # elif o == "--log-file":
1048 # log_file = a
1049 else:
1050 assert False, "Unhandled option"
1051 if config_file:
1052 if not path.isfile(config_file):
1053 print("configuration file '{}' that not exist".format(config_file), file=sys.stderr)
1054 exit(1)
1055 else:
1056 for config_file in (__file__[:__file__.rfind(".")] + ".cfg", "./nbi.cfg", "/etc/osm/nbi.cfg"):
1057 if path.isfile(config_file):
1058 break
1059 else:
1060 print("No configuration file 'nbi.cfg' found neither at local folder nor at /etc/osm/", file=sys.stderr)
1061 exit(1)
1062 nbi(config_file)
1063 except getopt.GetoptError as e:
1064 print(str(e), file=sys.stderr)
1065 # usage()
1066 exit(1)