blob: 63a6eca3c282748af88f831f141a9eda6a987c7b [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
tierno932499c2019-01-28 17:28:10 +000030from subscriptions import SubscriptionThread
tierno36ec8602018-11-02 17:27:11 +010031from validation import ValidationError
tiernoa8d63632018-05-10 13:12:32 +020032from osm_common.dbbase import DbException
33from osm_common.fsbase import FsException
34from osm_common.msgbase import MsgException
tiernoc94c3df2018-02-09 15:38:54 +010035from http import HTTPStatus
tiernoc94c3df2018-02-09 15:38:54 +010036from codecs import getreader
tiernof5298be2018-05-16 14:43:57 +020037from os import environ, path
tiernoc94c3df2018-02-09 15:38:54 +010038
39__author__ = "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
tiernodfe09572018-04-24 10:41:10 +020040
tiernodfe09572018-04-24 10:41:10 +020041__version__ = "0.1.3"
tiernobee508e2019-01-21 11:21:49 +000042version_date = "Jan 2019"
tierno4a946e42018-04-12 17:48:49 +020043database_version = '1.0'
Eduardo Sousa819d34c2018-07-31 01:20:02 +010044auth_database_version = '1.0'
tierno932499c2019-01-28 17:28:10 +000045nbi_server = None # instance of Server class
46subscription_thread = None # instance of SubscriptionThread class
47
tiernoc94c3df2018-02-09 15:38:54 +010048
49"""
tiernof27c79b2018-03-12 17:08:42 +010050North Bound Interface (O: OSM specific; 5,X: SOL005 not implemented yet; O5: SOL005 implemented)
tiernoc94c3df2018-02-09 15:38:54 +010051URL: /osm GET POST PUT DELETE PATCH
garciadeblas9750c5a2018-10-15 16:20:35 +020052 /nsd/v1
tierno2236d202018-05-16 19:05:16 +020053 /ns_descriptors_content O O
54 /<nsdInfoId> O O O O
tiernoc94c3df2018-02-09 15:38:54 +010055 /ns_descriptors O5 O5
56 /<nsdInfoId> O5 O5 5
57 /nsd_content O5 O5
tiernof27c79b2018-03-12 17:08:42 +010058 /nsd O
59 /artifacts[/<artifactPath>] O
tiernoc94c3df2018-02-09 15:38:54 +010060 /pnf_descriptors 5 5
61 /<pnfdInfoId> 5 5 5
62 /pnfd_content 5 5
tiernof27c79b2018-03-12 17:08:42 +010063 /subscriptions 5 5
64 /<subscriptionId> 5 X
tiernoc94c3df2018-02-09 15:38:54 +010065
66 /vnfpkgm/v1
tierno55945e72018-04-06 16:40:27 +020067 /vnf_packages_content O O
tierno2236d202018-05-16 19:05:16 +020068 /<vnfPkgId> O O
tiernoc94c3df2018-02-09 15:38:54 +010069 /vnf_packages O5 O5
70 /<vnfPkgId> O5 O5 5
tiernoc94c3df2018-02-09 15:38:54 +010071 /package_content O5 O5
72 /upload_from_uri X
tiernof27c79b2018-03-12 17:08:42 +010073 /vnfd O5
74 /artifacts[/<artifactPath>] O5
75 /subscriptions X X
76 /<subscriptionId> X X
tiernoc94c3df2018-02-09 15:38:54 +010077
78 /nslcm/v1
tiernof27c79b2018-03-12 17:08:42 +010079 /ns_instances_content O O
tierno2236d202018-05-16 19:05:16 +020080 /<nsInstanceId> O O
tiernof27c79b2018-03-12 17:08:42 +010081 /ns_instances 5 5
tierno95692442018-05-24 18:05:28 +020082 /<nsInstanceId> O5 O5
tierno65acb4d2018-04-06 16:42:40 +020083 instantiate O5
84 terminate O5
85 action O
86 scale O5
87 heal 5
tiernoc94c3df2018-02-09 15:38:54 +010088 /ns_lcm_op_occs 5 5
89 /<nsLcmOpOccId> 5 5 5
90 TO BE COMPLETED 5 5
tiernof759d822018-06-11 18:54:54 +020091 /vnf_instances (also vnfrs for compatibility) O
92 /<vnfInstanceId> O
tiernof27c79b2018-03-12 17:08:42 +010093 /subscriptions 5 5
94 /<subscriptionId> 5 X
garciadeblas9750c5a2018-10-15 16:20:35 +020095
tiernocb83c942018-09-24 17:28:13 +020096 /pdu/v1
tierno032916c2019-03-22 13:27:12 +000097 /pdu_descriptors O O
tiernocb83c942018-09-24 17:28:13 +020098 /<id> O O O O
garciadeblas9750c5a2018-10-15 16:20:35 +020099
tiernof27c79b2018-03-12 17:08:42 +0100100 /admin/v1
101 /tokens O O
tierno2236d202018-05-16 19:05:16 +0200102 /<id> O O
tiernof27c79b2018-03-12 17:08:42 +0100103 /users O O
tiernocd54a4a2018-09-12 16:40:35 +0200104 /<id> O O O O
tiernof27c79b2018-03-12 17:08:42 +0100105 /projects O O
tierno2236d202018-05-16 19:05:16 +0200106 /<id> O O
tierno55ba2e62018-12-11 17:22:22 +0000107 /vim_accounts (also vims for compatibility) O O
108 /<id> O O O
109 /wim_accounts O O
tierno2236d202018-05-16 19:05:16 +0200110 /<id> O O O
tierno0f98af52018-03-19 10:28:22 +0100111 /sdns O O
tierno2236d202018-05-16 19:05:16 +0200112 /<id> O O O
tiernoc94c3df2018-02-09 15:38:54 +0100113
garciadeblas9750c5a2018-10-15 16:20:35 +0200114 /nst/v1 O O
115 /netslice_templates_content O O
116 /<nstInfoId> O O O O
117 /netslice_templates O O
118 /<nstInfoId> O O O
119 /nst_content O O
120 /nst O
121 /artifacts[/<artifactPath>] O
122 /subscriptions X X
123 /<subscriptionId> X X
124
125 /nsilcm/v1
126 /netslice_instances_content O O
127 /<SliceInstanceId> O O
128 /netslice_instances O O
129 /<SliceInstanceId> O O
130 instantiate O
131 terminate O
132 action O
133 /nsi_lcm_op_occs O O
134 /<nsiLcmOpOccId> O O O
135 /subscriptions X X
136 /<subscriptionId> X X
137
tierno2236d202018-05-16 19:05:16 +0200138query string:
139 Follows SOL005 section 4.3.2 It contains extra METHOD to override http method, FORCE to force.
tierno36ec8602018-11-02 17:27:11 +0100140 simpleFilterExpr := <attrName>["."<attrName>]*["."<op>]"="<value>[","<value>]*
141 filterExpr := <simpleFilterExpr>["&"<simpleFilterExpr>]*
142 op := "eq" | "neq" (or "ne") | "gt" | "lt" | "gte" | "lte" | "cont" | "ncont"
143 attrName := string
tierno2236d202018-05-16 19:05:16 +0200144 For filtering inside array, it must select the element of the array, or add ANYINDEX to apply the filtering over any
145 item of the array, that is, pass if any item of the array pass the filter.
146 It allows both ne and neq for not equal
147 TODO: 4.3.3 Attribute selectors
148 all_fields, fields=x,y,.., exclude_default, exclude_fields=x,y,...
tiernoc94c3df2018-02-09 15:38:54 +0100149 (none) … same as “exclude_default”
150 all_fields … all attributes.
tierno2236d202018-05-16 19:05:16 +0200151 fields=<list> … all attributes except all complex attributes with minimum cardinality of zero that are not
152 conditionally mandatory, and that are not provided in <list>.
153 exclude_fields=<list> … all attributes except those complex attributes with a minimum cardinality of zero that
154 are not conditionally mandatory, and that are provided in <list>.
155 exclude_default … all attributes except those complex attributes with a minimum cardinality of zero that are not
156 conditionally mandatory, and that are part of the "default exclude set" defined in the present specification for
157 the particular resource
158 exclude_default and include=<list> … all attributes except those complex attributes with a minimum cardinality
159 of zero that are not conditionally mandatory and that are part of the "default exclude set" defined in the
160 present specification for the particular resource, but that are not part of <list>
tiernoc94c3df2018-02-09 15:38:54 +0100161Header field name Reference Example Descriptions
162 Accept IETF RFC 7231 [19] application/json Content-Types that are acceptable for the response.
163 This header field shall be present if the response is expected to have a non-empty message body.
164 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the request.
165 This header field shall be present if the request has a non-empty message body.
tierno2236d202018-05-16 19:05:16 +0200166 Authorization IETF RFC 7235 [22] Bearer mF_9.B5f-4.1JqM The authorization token for the request.
167 Details are specified in clause 4.5.3.
tiernoc94c3df2018-02-09 15:38:54 +0100168 Range IETF RFC 7233 [21] 1000-2000 Requested range of bytes from a file
169Header field name Reference Example Descriptions
170 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the response.
171 This header field shall be present if the response has a non-empty message body.
tierno2236d202018-05-16 19:05:16 +0200172 Location IETF RFC 7231 [19] http://www.example.com/vnflcm/v1/vnf_instances/123 Used in redirection, or when a
173 new resource has been created.
tiernoc94c3df2018-02-09 15:38:54 +0100174 This header field shall be present if the response status code is 201 or 3xx.
tierno2236d202018-05-16 19:05:16 +0200175 In the present document this header field is also used if the response status code is 202 and a new resource was
176 created.
177 WWW-Authenticate IETF RFC 7235 [22] Bearer realm="example" Challenge if the corresponding HTTP request has not
178 provided authorization, or error details if the corresponding HTTP request has provided an invalid authorization
179 token.
180 Accept-Ranges IETF RFC 7233 [21] bytes Used by the Server to signal whether or not it supports ranges for
181 certain resources.
182 Content-Range IETF RFC 7233 [21] bytes 21010-47021/ 47022 Signals the byte range that is contained in the
183 response, and the total length of the file.
tiernoc94c3df2018-02-09 15:38:54 +0100184 Retry-After IETF RFC 7231 [19] Fri, 31 Dec 1999 23:59:59 GMT
tiernoc94c3df2018-02-09 15:38:54 +0100185"""
186
187
188class NbiException(Exception):
189
190 def __init__(self, message, http_code=HTTPStatus.METHOD_NOT_ALLOWED):
191 Exception.__init__(self, message)
192 self.http_code = http_code
193
194
195class Server(object):
196 instance = 0
197 # to decode bytes to str
198 reader = getreader("utf-8")
199
200 def __init__(self):
201 self.instance += 1
202 self.engine = Engine()
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100203 self.authenticator = Authenticator()
tiernof27c79b2018-03-12 17:08:42 +0100204 self.valid_methods = { # contains allowed URL and methods
205 "admin": {
206 "v1": {
tierno0f98af52018-03-19 10:28:22 +0100207 "tokens": {"METHODS": ("GET", "POST", "DELETE"),
tierno2236d202018-05-16 19:05:16 +0200208 "<ID>": {"METHODS": ("GET", "DELETE")}
209 },
tierno0f98af52018-03-19 10:28:22 +0100210 "users": {"METHODS": ("GET", "POST"),
tiernocd54a4a2018-09-12 16:40:35 +0200211 "<ID>": {"METHODS": ("GET", "POST", "DELETE", "PATCH", "PUT")}
tierno2236d202018-05-16 19:05:16 +0200212 },
tierno0f98af52018-03-19 10:28:22 +0100213 "projects": {"METHODS": ("GET", "POST"),
tierno2236d202018-05-16 19:05:16 +0200214 "<ID>": {"METHODS": ("GET", "DELETE")}
215 },
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100216 "roles": {"METHODS": ("GET", "POST"),
217 "<ID>": {"METHODS": ("GET", "POST", "DELETE")}
218 },
tierno0f98af52018-03-19 10:28:22 +0100219 "vims": {"METHODS": ("GET", "POST"),
tierno7ae10112018-05-18 14:36:02 +0200220 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
tierno2236d202018-05-16 19:05:16 +0200221 },
tierno09c073e2018-04-26 13:36:48 +0200222 "vim_accounts": {"METHODS": ("GET", "POST"),
tierno7ae10112018-05-18 14:36:02 +0200223 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
tierno2236d202018-05-16 19:05:16 +0200224 },
tierno55ba2e62018-12-11 17:22:22 +0000225 "wim_accounts": {"METHODS": ("GET", "POST"),
226 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
227 },
tierno0f98af52018-03-19 10:28:22 +0100228 "sdns": {"METHODS": ("GET", "POST"),
tierno7ae10112018-05-18 14:36:02 +0200229 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
tierno2236d202018-05-16 19:05:16 +0200230 },
tiernof27c79b2018-03-12 17:08:42 +0100231 }
232 },
tiernocb83c942018-09-24 17:28:13 +0200233 "pdu": {
234 "v1": {
235 "pdu_descriptors": {"METHODS": ("GET", "POST"),
236 "<ID>": {"METHODS": ("GET", "POST", "DELETE", "PATCH", "PUT")}
237 },
238 }
239 },
tiernof27c79b2018-03-12 17:08:42 +0100240 "nsd": {
241 "v1": {
tierno2236d202018-05-16 19:05:16 +0200242 "ns_descriptors_content": {"METHODS": ("GET", "POST"),
243 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
244 },
245 "ns_descriptors": {"METHODS": ("GET", "POST"),
tierno36ec8602018-11-02 17:27:11 +0100246 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"),
tierno2236d202018-05-16 19:05:16 +0200247 "nsd_content": {"METHODS": ("GET", "PUT")},
248 "nsd": {"METHODS": "GET"}, # descriptor inside package
249 "artifacts": {"*": {"METHODS": "GET"}}
250 }
251 },
tiernof27c79b2018-03-12 17:08:42 +0100252 "pnf_descriptors": {"TODO": ("GET", "POST"),
tierno2236d202018-05-16 19:05:16 +0200253 "<ID>": {"TODO": ("GET", "DELETE", "PATCH"),
254 "pnfd_content": {"TODO": ("GET", "PUT")}
255 }
256 },
tiernof27c79b2018-03-12 17:08:42 +0100257 "subscriptions": {"TODO": ("GET", "POST"),
tierno2236d202018-05-16 19:05:16 +0200258 "<ID>": {"TODO": ("GET", "DELETE")}
259 },
tiernof27c79b2018-03-12 17:08:42 +0100260 }
261 },
262 "vnfpkgm": {
263 "v1": {
tierno2236d202018-05-16 19:05:16 +0200264 "vnf_packages_content": {"METHODS": ("GET", "POST"),
265 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
266 },
267 "vnf_packages": {"METHODS": ("GET", "POST"),
tiernoaae4dc42018-06-11 18:55:50 +0200268 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"), # GET: vnfPkgInfo
tierno2236d202018-05-16 19:05:16 +0200269 "package_content": {"METHODS": ("GET", "PUT"), # package
270 "upload_from_uri": {"TODO": "POST"}
271 },
272 "vnfd": {"METHODS": "GET"}, # descriptor inside package
273 "artifacts": {"*": {"METHODS": "GET"}}
274 }
275 },
tiernof27c79b2018-03-12 17:08:42 +0100276 "subscriptions": {"TODO": ("GET", "POST"),
tierno2236d202018-05-16 19:05:16 +0200277 "<ID>": {"TODO": ("GET", "DELETE")}
278 },
tiernof27c79b2018-03-12 17:08:42 +0100279 }
280 },
281 "nslcm": {
282 "v1": {
283 "ns_instances_content": {"METHODS": ("GET", "POST"),
tierno2236d202018-05-16 19:05:16 +0200284 "<ID>": {"METHODS": ("GET", "DELETE")}
285 },
tierno65acb4d2018-04-06 16:42:40 +0200286 "ns_instances": {"METHODS": ("GET", "POST"),
tierno95692442018-05-24 18:05:28 +0200287 "<ID>": {"METHODS": ("GET", "DELETE"),
tiernof759d822018-06-11 18:54:54 +0200288 "scale": {"METHODS": "POST"},
tierno2236d202018-05-16 19:05:16 +0200289 "terminate": {"METHODS": "POST"},
290 "instantiate": {"METHODS": "POST"},
291 "action": {"METHODS": "POST"},
292 }
293 },
tierno65acb4d2018-04-06 16:42:40 +0200294 "ns_lcm_op_occs": {"METHODS": "GET",
tierno2236d202018-05-16 19:05:16 +0200295 "<ID>": {"METHODS": "GET"},
296 },
tierno0ffaa992018-05-09 13:21:56 +0200297 "vnfrs": {"METHODS": ("GET"),
tierno2236d202018-05-16 19:05:16 +0200298 "<ID>": {"METHODS": ("GET")}
299 },
tiernof759d822018-06-11 18:54:54 +0200300 "vnf_instances": {"METHODS": ("GET"),
301 "<ID>": {"METHODS": ("GET")}
302 },
tiernof27c79b2018-03-12 17:08:42 +0100303 }
304 },
garciadeblas9750c5a2018-10-15 16:20:35 +0200305 "nst": {
306 "v1": {
307 "netslice_templates_content": {"METHODS": ("GET", "POST"),
308 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
309 },
310 "netslice_templates": {"METHODS": ("GET", "POST"),
311 "<ID>": {"METHODS": ("GET", "DELETE"), "TODO": "PATCH",
312 "nst_content": {"METHODS": ("GET", "PUT")},
313 "nst": {"METHODS": "GET"}, # descriptor inside package
314 "artifacts": {"*": {"METHODS": "GET"}}
315 }
316 },
317 "subscriptions": {"TODO": ("GET", "POST"),
318 "<ID>": {"TODO": ("GET", "DELETE")}
319 },
320 }
321 },
322 "nsilcm": {
323 "v1": {
324 "netslice_instances_content": {"METHODS": ("GET", "POST"),
325 "<ID>": {"METHODS": ("GET", "DELETE")}
326 },
327 "netslice_instances": {"METHODS": ("GET", "POST"),
328 "<ID>": {"METHODS": ("GET", "DELETE"),
329 "terminate": {"METHODS": "POST"},
330 "instantiate": {"METHODS": "POST"},
331 "action": {"METHODS": "POST"},
332 }
333 },
334 "nsi_lcm_op_occs": {"METHODS": "GET",
335 "<ID>": {"METHODS": "GET"},
336 },
337 }
338 },
tiernof27c79b2018-03-12 17:08:42 +0100339 }
tiernoc94c3df2018-02-09 15:38:54 +0100340
tiernoc94c3df2018-02-09 15:38:54 +0100341 def _format_in(self, kwargs):
342 try:
343 indata = None
344 if cherrypy.request.body.length:
345 error_text = "Invalid input format "
346
347 if "Content-Type" in cherrypy.request.headers:
348 if "application/json" in cherrypy.request.headers["Content-Type"]:
349 error_text = "Invalid json format "
350 indata = json.load(self.reader(cherrypy.request.body))
gcalvinode4adfe2018-10-30 11:46:09 +0100351 cherrypy.request.headers.pop("Content-File-MD5", None)
tiernoc94c3df2018-02-09 15:38:54 +0100352 elif "application/yaml" in cherrypy.request.headers["Content-Type"]:
353 error_text = "Invalid yaml format "
354 indata = yaml.load(cherrypy.request.body)
gcalvinode4adfe2018-10-30 11:46:09 +0100355 cherrypy.request.headers.pop("Content-File-MD5", None)
tiernoc94c3df2018-02-09 15:38:54 +0100356 elif "application/binary" in cherrypy.request.headers["Content-Type"] or \
357 "application/gzip" in cherrypy.request.headers["Content-Type"] or \
tiernof27c79b2018-03-12 17:08:42 +0100358 "application/zip" in cherrypy.request.headers["Content-Type"] or \
359 "text/plain" in cherrypy.request.headers["Content-Type"]:
360 indata = cherrypy.request.body # .read()
tiernoc94c3df2018-02-09 15:38:54 +0100361 elif "multipart/form-data" in cherrypy.request.headers["Content-Type"]:
362 if "descriptor_file" in kwargs:
363 filecontent = kwargs.pop("descriptor_file")
364 if not filecontent.file:
365 raise NbiException("empty file or content", HTTPStatus.BAD_REQUEST)
tiernof27c79b2018-03-12 17:08:42 +0100366 indata = filecontent.file # .read()
tiernoc94c3df2018-02-09 15:38:54 +0100367 if filecontent.content_type.value:
368 cherrypy.request.headers["Content-Type"] = filecontent.content_type.value
369 else:
370 # raise cherrypy.HTTPError(HTTPStatus.Not_Acceptable,
371 # "Only 'Content-Type' of type 'application/json' or
372 # 'application/yaml' for input format are available")
373 error_text = "Invalid yaml format "
374 indata = yaml.load(cherrypy.request.body)
gcalvinode4adfe2018-10-30 11:46:09 +0100375 cherrypy.request.headers.pop("Content-File-MD5", None)
tiernoc94c3df2018-02-09 15:38:54 +0100376 else:
377 error_text = "Invalid yaml format "
378 indata = yaml.load(cherrypy.request.body)
gcalvinode4adfe2018-10-30 11:46:09 +0100379 cherrypy.request.headers.pop("Content-File-MD5", None)
tiernoc94c3df2018-02-09 15:38:54 +0100380 if not indata:
381 indata = {}
382
tiernoc94c3df2018-02-09 15:38:54 +0100383 format_yaml = False
384 if cherrypy.request.headers.get("Query-String-Format") == "yaml":
385 format_yaml = True
386
387 for k, v in kwargs.items():
388 if isinstance(v, str):
389 if v == "":
390 kwargs[k] = None
391 elif format_yaml:
392 try:
393 kwargs[k] = yaml.load(v)
tiernoe1281182018-05-22 12:24:36 +0200394 except Exception:
tiernoc94c3df2018-02-09 15:38:54 +0100395 pass
396 elif k.endswith(".gt") or k.endswith(".lt") or k.endswith(".gte") or k.endswith(".lte"):
397 try:
398 kwargs[k] = int(v)
tiernoe1281182018-05-22 12:24:36 +0200399 except Exception:
tiernoc94c3df2018-02-09 15:38:54 +0100400 try:
401 kwargs[k] = float(v)
tiernoe1281182018-05-22 12:24:36 +0200402 except Exception:
tiernoc94c3df2018-02-09 15:38:54 +0100403 pass
404 elif v.find(",") > 0:
405 kwargs[k] = v.split(",")
406 elif isinstance(v, (list, tuple)):
407 for index in range(0, len(v)):
408 if v[index] == "":
409 v[index] = None
410 elif format_yaml:
411 try:
412 v[index] = yaml.load(v[index])
tiernoe1281182018-05-22 12:24:36 +0200413 except Exception:
tiernoc94c3df2018-02-09 15:38:54 +0100414 pass
415
tiernof27c79b2018-03-12 17:08:42 +0100416 return indata
tiernoc94c3df2018-02-09 15:38:54 +0100417 except (ValueError, yaml.YAMLError) as exc:
418 raise NbiException(error_text + str(exc), HTTPStatus.BAD_REQUEST)
419 except KeyError as exc:
420 raise NbiException("Query string error: " + str(exc), HTTPStatus.BAD_REQUEST)
tiernob92094f2018-05-11 13:44:22 +0200421 except Exception as exc:
422 raise NbiException(error_text + str(exc), HTTPStatus.BAD_REQUEST)
tiernoc94c3df2018-02-09 15:38:54 +0100423
424 @staticmethod
tiernof27c79b2018-03-12 17:08:42 +0100425 def _format_out(data, session=None, _format=None):
tiernoc94c3df2018-02-09 15:38:54 +0100426 """
427 return string of dictionary data according to requested json, yaml, xml. By default json
tiernof27c79b2018-03-12 17:08:42 +0100428 :param data: response to be sent. Can be a dict, text or file
tiernoc94c3df2018-02-09 15:38:54 +0100429 :param session:
tiernof27c79b2018-03-12 17:08:42 +0100430 :param _format: The format to be set as Content-Type ir data is a file
tiernoc94c3df2018-02-09 15:38:54 +0100431 :return: None
432 """
tierno0f98af52018-03-19 10:28:22 +0100433 accept = cherrypy.request.headers.get("Accept")
tiernof27c79b2018-03-12 17:08:42 +0100434 if data is None:
tierno0f98af52018-03-19 10:28:22 +0100435 if accept and "text/html" in accept:
436 return html.format(data, cherrypy.request, cherrypy.response, session)
tierno09c073e2018-04-26 13:36:48 +0200437 # cherrypy.response.status = HTTPStatus.NO_CONTENT.value
tiernof27c79b2018-03-12 17:08:42 +0100438 return
439 elif hasattr(data, "read"): # file object
440 if _format:
441 cherrypy.response.headers["Content-Type"] = _format
442 elif "b" in data.mode: # binariy asssumig zip
443 cherrypy.response.headers["Content-Type"] = 'application/zip'
444 else:
445 cherrypy.response.headers["Content-Type"] = 'text/plain'
446 # TODO check that cherrypy close file. If not implement pending things to close per thread next
447 return data
tierno0f98af52018-03-19 10:28:22 +0100448 if accept:
tiernoc94c3df2018-02-09 15:38:54 +0100449 if "application/json" in accept:
450 cherrypy.response.headers["Content-Type"] = 'application/json; charset=utf-8'
451 a = json.dumps(data, indent=4) + "\n"
452 return a.encode("utf8")
453 elif "text/html" in accept:
454 return html.format(data, cherrypy.request, cherrypy.response, session)
455
tiernof27c79b2018-03-12 17:08:42 +0100456 elif "application/yaml" in accept or "*/*" in accept or "text/plain" in accept:
tiernoc94c3df2018-02-09 15:38:54 +0100457 pass
tiernof717cbe2018-12-03 16:35:42 +0000458 # if there is not any valid accept, raise an error. But if response is already an error, format in yaml
459 elif cherrypy.response.status >= 400:
tiernoc94c3df2018-02-09 15:38:54 +0100460 raise cherrypy.HTTPError(HTTPStatus.NOT_ACCEPTABLE.value,
461 "Only 'Accept' of type 'application/json' or 'application/yaml' "
462 "for output format are available")
463 cherrypy.response.headers["Content-Type"] = 'application/yaml'
464 return yaml.safe_dump(data, explicit_start=True, indent=4, default_flow_style=False, tags=False,
465 encoding='utf-8', allow_unicode=True) # , canonical=True, default_style='"'
466
467 @cherrypy.expose
468 def index(self, *args, **kwargs):
469 session = None
470 try:
471 if cherrypy.request.method == "GET":
Eduardo Sousa2f988212018-07-26 01:04:11 +0100472 session = self.authenticator.authorize()
tiernoc94c3df2018-02-09 15:38:54 +0100473 outdata = "Index page"
474 else:
475 raise cherrypy.HTTPError(HTTPStatus.METHOD_NOT_ALLOWED.value,
tierno2236d202018-05-16 19:05:16 +0200476 "Method {} not allowed for tokens".format(cherrypy.request.method))
tiernoc94c3df2018-02-09 15:38:54 +0100477
478 return self._format_out(outdata, session)
479
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100480 except (EngineException, AuthException) as e:
tiernoc94c3df2018-02-09 15:38:54 +0100481 cherrypy.log("index Exception {}".format(e))
482 cherrypy.response.status = e.http_code.value
483 return self._format_out("Welcome to OSM!", session)
484
485 @cherrypy.expose
tierno55945e72018-04-06 16:40:27 +0200486 def version(self, *args, **kwargs):
tiernodfe09572018-04-24 10:41:10 +0200487 # TODO consider to remove and provide version using the static version file
tierno55945e72018-04-06 16:40:27 +0200488 global __version__, version_date
489 try:
490 if cherrypy.request.method != "GET":
491 raise NbiException("Only method GET is allowed", HTTPStatus.METHOD_NOT_ALLOWED)
492 elif args or kwargs:
493 raise NbiException("Invalid URL or query string for version", HTTPStatus.METHOD_NOT_ALLOWED)
494 return __version__ + " " + version_date
495 except NbiException as e:
496 cherrypy.response.status = e.http_code.value
497 problem_details = {
498 "code": e.http_code.name,
499 "status": e.http_code.value,
500 "detail": str(e),
501 }
502 return self._format_out(problem_details, None)
503
504 @cherrypy.expose
tiernof27c79b2018-03-12 17:08:42 +0100505 def token(self, method, token_id=None, kwargs=None):
tiernoc94c3df2018-02-09 15:38:54 +0100506 session = None
507 # self.engine.load_dbase(cherrypy.request.app.config)
tiernof27c79b2018-03-12 17:08:42 +0100508 indata = self._format_in(kwargs)
509 if not isinstance(indata, dict):
510 raise NbiException("Expected application/yaml or application/json Content-Type", HTTPStatus.BAD_REQUEST)
tiernoc94c3df2018-02-09 15:38:54 +0100511 try:
tiernoc94c3df2018-02-09 15:38:54 +0100512 if method == "GET":
Eduardo Sousa2f988212018-07-26 01:04:11 +0100513 session = self.authenticator.authorize()
tiernof27c79b2018-03-12 17:08:42 +0100514 if token_id:
Eduardo Sousa2f988212018-07-26 01:04:11 +0100515 outdata = self.authenticator.get_token(session, token_id)
tiernoc94c3df2018-02-09 15:38:54 +0100516 else:
Eduardo Sousa2f988212018-07-26 01:04:11 +0100517 outdata = self.authenticator.get_token_list(session)
tiernoc94c3df2018-02-09 15:38:54 +0100518 elif method == "POST":
519 try:
Eduardo Sousa2f988212018-07-26 01:04:11 +0100520 session = self.authenticator.authorize()
tiernoe1281182018-05-22 12:24:36 +0200521 except Exception:
tiernoc94c3df2018-02-09 15:38:54 +0100522 session = None
523 if kwargs:
524 indata.update(kwargs)
Eduardo Sousa2f988212018-07-26 01:04:11 +0100525 outdata = self.authenticator.new_token(session, indata, cherrypy.request.remote)
tiernoc94c3df2018-02-09 15:38:54 +0100526 session = outdata
527 cherrypy.session['Authorization'] = outdata["_id"]
tierno0f98af52018-03-19 10:28:22 +0100528 self._set_location_header("admin", "v1", "tokens", outdata["_id"])
tiernoc94c3df2018-02-09 15:38:54 +0100529 # cherrypy.response.cookie["Authorization"] = outdata["id"]
530 # cherrypy.response.cookie["Authorization"]['expires'] = 3600
531 elif method == "DELETE":
tiernof27c79b2018-03-12 17:08:42 +0100532 if not token_id and "id" in kwargs:
tiernoc94c3df2018-02-09 15:38:54 +0100533 token_id = kwargs["id"]
tiernof27c79b2018-03-12 17:08:42 +0100534 elif not token_id:
Eduardo Sousa2f988212018-07-26 01:04:11 +0100535 session = self.authenticator.authorize()
tiernoc94c3df2018-02-09 15:38:54 +0100536 token_id = session["_id"]
Eduardo Sousa2f988212018-07-26 01:04:11 +0100537 outdata = self.authenticator.del_token(token_id)
tiernoc94c3df2018-02-09 15:38:54 +0100538 session = None
539 cherrypy.session['Authorization'] = "logout"
540 # cherrypy.response.cookie["Authorization"] = token_id
541 # cherrypy.response.cookie["Authorization"]['expires'] = 0
542 else:
543 raise NbiException("Method {} not allowed for token".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
544 return self._format_out(outdata, session)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100545 except (NbiException, EngineException, DbException, AuthException) as e:
tiernoc94c3df2018-02-09 15:38:54 +0100546 cherrypy.log("tokens Exception {}".format(e))
547 cherrypy.response.status = e.http_code.value
548 problem_details = {
549 "code": e.http_code.name,
550 "status": e.http_code.value,
551 "detail": str(e),
552 }
553 return self._format_out(problem_details, session)
554
555 @cherrypy.expose
556 def test(self, *args, **kwargs):
557 thread_info = None
tiernof27c79b2018-03-12 17:08:42 +0100558 if args and args[0] == "help":
tiernof717cbe2018-12-03 16:35:42 +0000559 return "<html><pre>\ninit\nfile/<name> download file\ndb-clear/table\nfs-clear[/folder]\nlogin\nlogin2\n"\
tiernoe1281182018-05-22 12:24:36 +0200560 "sleep/<time>\nmessage/topic\n</pre></html>"
tiernof27c79b2018-03-12 17:08:42 +0100561
562 elif args and args[0] == "init":
tiernoc94c3df2018-02-09 15:38:54 +0100563 try:
564 # self.engine.load_dbase(cherrypy.request.app.config)
565 self.engine.create_admin()
566 return "Done. User 'admin', password 'admin' created"
567 except Exception:
568 cherrypy.response.status = HTTPStatus.FORBIDDEN.value
569 return self._format_out("Database already initialized")
tiernof27c79b2018-03-12 17:08:42 +0100570 elif args and args[0] == "file":
571 return cherrypy.lib.static.serve_file(cherrypy.tree.apps['/osm'].config["storage"]["path"] + "/" + args[1],
572 "text/plain", "attachment")
573 elif args and args[0] == "file2":
574 f_path = cherrypy.tree.apps['/osm'].config["storage"]["path"] + "/" + args[1]
575 f = open(f_path, "r")
576 cherrypy.response.headers["Content-type"] = "text/plain"
tiernof27c79b2018-03-12 17:08:42 +0100577 return f
tierno55945e72018-04-06 16:40:27 +0200578
tiernof27c79b2018-03-12 17:08:42 +0100579 elif len(args) == 2 and args[0] == "db-clear":
tiernof717cbe2018-12-03 16:35:42 +0000580 deleted_info = self.engine.db.del_list(args[1], kwargs)
581 return "{} {} deleted\n".format(deleted_info["deleted"], args[1])
582 elif len(args) and args[0] == "fs-clear":
583 if len(args) >= 2:
584 folders = (args[1],)
585 else:
586 folders = self.engine.fs.dir_ls(".")
587 for folder in folders:
588 self.engine.fs.file_delete(folder)
589 return ",".join(folders) + " folders deleted\n"
tiernoc94c3df2018-02-09 15:38:54 +0100590 elif args and args[0] == "login":
591 if not cherrypy.request.headers.get("Authorization"):
592 cherrypy.response.headers["WWW-Authenticate"] = 'Basic realm="Access to OSM site", charset="UTF-8"'
593 cherrypy.response.status = HTTPStatus.UNAUTHORIZED.value
594 elif args and args[0] == "login2":
595 if not cherrypy.request.headers.get("Authorization"):
596 cherrypy.response.headers["WWW-Authenticate"] = 'Bearer realm="Access to OSM site"'
597 cherrypy.response.status = HTTPStatus.UNAUTHORIZED.value
598 elif args and args[0] == "sleep":
599 sleep_time = 5
600 try:
601 sleep_time = int(args[1])
602 except Exception:
603 cherrypy.response.status = HTTPStatus.FORBIDDEN.value
604 return self._format_out("Database already initialized")
605 thread_info = cherrypy.thread_data
606 print(thread_info)
607 time.sleep(sleep_time)
608 # thread_info
609 elif len(args) >= 2 and args[0] == "message":
tiernob24258a2018-10-04 18:39:49 +0200610 main_topic = args[1]
611 return_text = "<html><pre>{} ->\n".format(main_topic)
tiernoc94c3df2018-02-09 15:38:54 +0100612 try:
tierno55945e72018-04-06 16:40:27 +0200613 if cherrypy.request.method == 'POST':
614 to_send = yaml.load(cherrypy.request.body)
615 for k, v in to_send.items():
tiernob24258a2018-10-04 18:39:49 +0200616 self.engine.msg.write(main_topic, k, v)
tierno55945e72018-04-06 16:40:27 +0200617 return_text += " {}: {}\n".format(k, v)
618 elif cherrypy.request.method == 'GET':
619 for k, v in kwargs.items():
tiernob24258a2018-10-04 18:39:49 +0200620 self.engine.msg.write(main_topic, k, yaml.load(v))
tierno55945e72018-04-06 16:40:27 +0200621 return_text += " {}: {}\n".format(k, yaml.load(v))
tiernoc94c3df2018-02-09 15:38:54 +0100622 except Exception as e:
tierno55945e72018-04-06 16:40:27 +0200623 return_text += "Error: " + str(e)
624 return_text += "</pre></html>\n"
625 return return_text
tiernoc94c3df2018-02-09 15:38:54 +0100626
627 return_text = (
628 "<html><pre>\nheaders:\n args: {}\n".format(args) +
629 " kwargs: {}\n".format(kwargs) +
630 " headers: {}\n".format(cherrypy.request.headers) +
631 " path_info: {}\n".format(cherrypy.request.path_info) +
632 " query_string: {}\n".format(cherrypy.request.query_string) +
633 " session: {}\n".format(cherrypy.session) +
634 " cookie: {}\n".format(cherrypy.request.cookie) +
635 " method: {}\n".format(cherrypy.request.method) +
tiernof717cbe2018-12-03 16:35:42 +0000636 " session: {}\n".format(cherrypy.session.get('fieldname')) +
tiernoc94c3df2018-02-09 15:38:54 +0100637 " body:\n")
638 return_text += " length: {}\n".format(cherrypy.request.body.length)
639 if cherrypy.request.body.length:
640 return_text += " content: {}\n".format(
641 str(cherrypy.request.body.read(int(cherrypy.request.headers.get('Content-Length', 0)))))
642 if thread_info:
643 return_text += "thread: {}\n".format(thread_info)
644 return_text += "</pre></html>"
645 return return_text
646
tiernof27c79b2018-03-12 17:08:42 +0100647 def _check_valid_url_method(self, method, *args):
648 if len(args) < 3:
tiernob24258a2018-10-04 18:39:49 +0200649 raise NbiException("URL must contain at least 'main_topic/version/topic'", HTTPStatus.METHOD_NOT_ALLOWED)
tiernof27c79b2018-03-12 17:08:42 +0100650
651 reference = self.valid_methods
652 for arg in args:
653 if arg is None:
654 break
655 if not isinstance(reference, dict):
656 raise NbiException("URL contains unexpected extra items '{}'".format(arg),
657 HTTPStatus.METHOD_NOT_ALLOWED)
658
659 if arg in reference:
660 reference = reference[arg]
661 elif "<ID>" in reference:
662 reference = reference["<ID>"]
663 elif "*" in reference:
664 reference = reference["*"]
665 break
666 else:
667 raise NbiException("Unexpected URL item {}".format(arg), HTTPStatus.METHOD_NOT_ALLOWED)
668 if "TODO" in reference and method in reference["TODO"]:
669 raise NbiException("Method {} not supported yet for this URL".format(method), HTTPStatus.NOT_IMPLEMENTED)
tierno2236d202018-05-16 19:05:16 +0200670 elif "METHODS" in reference and method not in reference["METHODS"]:
tiernof27c79b2018-03-12 17:08:42 +0100671 raise NbiException("Method {} not supported for this URL".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
672 return
673
674 @staticmethod
tiernob24258a2018-10-04 18:39:49 +0200675 def _set_location_header(main_topic, version, topic, id):
tiernof27c79b2018-03-12 17:08:42 +0100676 """
677 Insert response header Location with the URL of created item base on URL params
tiernob24258a2018-10-04 18:39:49 +0200678 :param main_topic:
tiernof27c79b2018-03-12 17:08:42 +0100679 :param version:
tiernob24258a2018-10-04 18:39:49 +0200680 :param topic:
tiernof27c79b2018-03-12 17:08:42 +0100681 :param id:
682 :return: None
683 """
684 # 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 +0200685 cherrypy.response.headers["Location"] = "/osm/{}/{}/{}/{}".format(main_topic, version, topic, id)
tiernof27c79b2018-03-12 17:08:42 +0100686 return
687
tiernoc94c3df2018-02-09 15:38:54 +0100688 @cherrypy.expose
tiernob24258a2018-10-04 18:39:49 +0200689 def default(self, main_topic=None, version=None, topic=None, _id=None, item=None, *args, **kwargs):
tiernoc94c3df2018-02-09 15:38:54 +0100690 session = None
tiernof27c79b2018-03-12 17:08:42 +0100691 outdata = None
692 _format = None
tierno0f98af52018-03-19 10:28:22 +0100693 method = "DONE"
tiernob24258a2018-10-04 18:39:49 +0200694 engine_topic = None
tiernodb9dc582018-06-20 17:27:29 +0200695 rollback = []
696 session = None
tiernoc94c3df2018-02-09 15:38:54 +0100697 try:
tiernob24258a2018-10-04 18:39:49 +0200698 if not main_topic or not version or not topic:
699 raise NbiException("URL must contain at least 'main_topic/version/topic'",
700 HTTPStatus.METHOD_NOT_ALLOWED)
tiernocd65be32018-11-16 16:34:49 +0100701 if main_topic not in ("admin", "vnfpkgm", "nsd", "nslcm", "pdu", "nst", "nsilcm"):
tiernob24258a2018-10-04 18:39:49 +0200702 raise NbiException("URL main_topic '{}' not supported".format(main_topic),
703 HTTPStatus.METHOD_NOT_ALLOWED)
tiernoc94c3df2018-02-09 15:38:54 +0100704 if version != 'v1':
705 raise NbiException("URL version '{}' not supported".format(version), HTTPStatus.METHOD_NOT_ALLOWED)
706
tiernof27c79b2018-03-12 17:08:42 +0100707 if kwargs and "METHOD" in kwargs and kwargs["METHOD"] in ("PUT", "POST", "DELETE", "GET", "PATCH"):
708 method = kwargs.pop("METHOD")
709 else:
710 method = cherrypy.request.method
tiernob92094f2018-05-11 13:44:22 +0200711 if kwargs and "FORCE" in kwargs:
712 force = kwargs.pop("FORCE")
713 else:
714 force = False
tiernob24258a2018-10-04 18:39:49 +0200715 self._check_valid_url_method(method, main_topic, version, topic, _id, item, *args)
tiernob24258a2018-10-04 18:39:49 +0200716 if main_topic == "admin" and topic == "tokens":
tiernof27c79b2018-03-12 17:08:42 +0100717 return self.token(method, _id, kwargs)
718
tiernoc94c3df2018-02-09 15:38:54 +0100719 # self.engine.load_dbase(cherrypy.request.app.config)
Eduardo Sousa2f988212018-07-26 01:04:11 +0100720 session = self.authenticator.authorize()
tiernof27c79b2018-03-12 17:08:42 +0100721 indata = self._format_in(kwargs)
tiernob24258a2018-10-04 18:39:49 +0200722 engine_topic = topic
723 if topic == "subscriptions":
724 engine_topic = main_topic + "_" + topic
725 if item:
726 engine_topic = item
tiernoc94c3df2018-02-09 15:38:54 +0100727
tiernob24258a2018-10-04 18:39:49 +0200728 if main_topic == "nsd":
729 engine_topic = "nsds"
730 elif main_topic == "vnfpkgm":
731 engine_topic = "vnfds"
732 elif main_topic == "nslcm":
733 engine_topic = "nsrs"
734 if topic == "ns_lcm_op_occs":
735 engine_topic = "nslcmops"
736 if topic == "vnfrs" or topic == "vnf_instances":
737 engine_topic = "vnfrs"
garciadeblas9750c5a2018-10-15 16:20:35 +0200738 elif main_topic == "nst":
739 engine_topic = "nsts"
740 elif main_topic == "nsilcm":
741 engine_topic = "nsis"
742 if topic == "nsi_lcm_op_occs":
Felipe Vicensb57758d2018-10-16 16:00:20 +0200743 engine_topic = "nsilcmops"
tiernob24258a2018-10-04 18:39:49 +0200744 elif main_topic == "pdu":
745 engine_topic = "pdus"
746 if engine_topic == "vims": # TODO this is for backward compatibility, it will remove in the future
747 engine_topic = "vim_accounts"
tiernoc94c3df2018-02-09 15:38:54 +0100748
749 if method == "GET":
Felipe Vicens07f31722018-10-29 15:16:44 +0100750 if item in ("nsd_content", "package_content", "artifacts", "vnfd", "nsd", "nst", "nst_content"):
garciadeblas9750c5a2018-10-15 16:20:35 +0200751 if item in ("vnfd", "nsd", "nst"):
tiernof27c79b2018-03-12 17:08:42 +0100752 path = "$DESCRIPTOR"
753 elif args:
754 path = args
tiernob24258a2018-10-04 18:39:49 +0200755 elif item == "artifacts":
tiernof27c79b2018-03-12 17:08:42 +0100756 path = ()
757 else:
758 path = None
tiernob24258a2018-10-04 18:39:49 +0200759 file, _format = self.engine.get_file(session, engine_topic, _id, path,
tierno2236d202018-05-16 19:05:16 +0200760 cherrypy.request.headers.get("Accept"))
tiernof27c79b2018-03-12 17:08:42 +0100761 outdata = file
762 elif not _id:
tiernob24258a2018-10-04 18:39:49 +0200763 outdata = self.engine.get_item_list(session, engine_topic, kwargs)
tiernoc94c3df2018-02-09 15:38:54 +0100764 else:
tiernob24258a2018-10-04 18:39:49 +0200765 outdata = self.engine.get_item(session, engine_topic, _id)
tiernof27c79b2018-03-12 17:08:42 +0100766 elif method == "POST":
garciadeblas9750c5a2018-10-15 16:20:35 +0200767 if topic in ("ns_descriptors_content", "vnf_packages_content", "netslice_templates_content"):
tiernof27c79b2018-03-12 17:08:42 +0100768 _id = cherrypy.request.headers.get("Transaction-Id")
769 if not _id:
tiernob24258a2018-10-04 18:39:49 +0200770 _id = self.engine.new_item(rollback, session, engine_topic, {}, None, cherrypy.request.headers,
tiernob92094f2018-05-11 13:44:22 +0200771 force=force)
tiernob24258a2018-10-04 18:39:49 +0200772 completed = self.engine.upload_content(session, engine_topic, _id, indata, kwargs,
773 cherrypy.request.headers, force=force)
tiernof27c79b2018-03-12 17:08:42 +0100774 if completed:
tiernob24258a2018-10-04 18:39:49 +0200775 self._set_location_header(main_topic, version, topic, _id)
tiernof27c79b2018-03-12 17:08:42 +0100776 else:
777 cherrypy.response.headers["Transaction-Id"] = _id
778 outdata = {"id": _id}
tiernob24258a2018-10-04 18:39:49 +0200779 elif topic == "ns_instances_content":
780 # creates NSR
781 _id = self.engine.new_item(rollback, session, engine_topic, indata, kwargs, force=force)
782 # creates nslcmop
783 indata["lcmOperationType"] = "instantiate"
784 indata["nsInstanceId"] = _id
785 self.engine.new_item(rollback, session, "nslcmops", indata, None)
786 self._set_location_header(main_topic, version, topic, _id)
tiernof27c79b2018-03-12 17:08:42 +0100787 outdata = {"id": _id}
tiernob24258a2018-10-04 18:39:49 +0200788 elif topic == "ns_instances" and item:
789 indata["lcmOperationType"] = item
790 indata["nsInstanceId"] = _id
791 _id = self.engine.new_item(rollback, session, "nslcmops", indata, kwargs)
792 self._set_location_header(main_topic, version, "ns_lcm_op_occs", _id)
tierno65acb4d2018-04-06 16:42:40 +0200793 outdata = {"id": _id}
794 cherrypy.response.status = HTTPStatus.ACCEPTED.value
garciadeblas9750c5a2018-10-15 16:20:35 +0200795 elif topic == "netslice_instances_content":
Felipe Vicens07f31722018-10-29 15:16:44 +0100796 # creates NetSlice_Instance_record (NSIR)
garciadeblas9750c5a2018-10-15 16:20:35 +0200797 _id = self.engine.new_item(rollback, session, engine_topic, indata, kwargs, force=force)
Felipe Vicens07f31722018-10-29 15:16:44 +0100798 self._set_location_header(main_topic, version, topic, _id)
garciadeblas9750c5a2018-10-15 16:20:35 +0200799 indata["lcmOperationType"] = "instantiate"
800 indata["nsiInstanceId"] = _id
Felipe Vicens07f31722018-10-29 15:16:44 +0100801 self.engine.new_item(rollback, session, "nsilcmops", indata, kwargs)
garciadeblas9750c5a2018-10-15 16:20:35 +0200802 outdata = {"id": _id}
Felipe Vicens07f31722018-10-29 15:16:44 +0100803
garciadeblas9750c5a2018-10-15 16:20:35 +0200804 elif topic == "netslice_instances" and item:
805 indata["lcmOperationType"] = item
806 indata["nsiInstanceId"] = _id
807 _id = self.engine.new_item(rollback, session, "nsilcmops", indata, kwargs)
808 self._set_location_header(main_topic, version, "nsi_lcm_op_occs", _id)
809 outdata = {"id": _id}
810 cherrypy.response.status = HTTPStatus.ACCEPTED.value
tiernof27c79b2018-03-12 17:08:42 +0100811 else:
tiernob24258a2018-10-04 18:39:49 +0200812 _id = self.engine.new_item(rollback, session, engine_topic, indata, kwargs,
813 cherrypy.request.headers, force=force)
814 self._set_location_header(main_topic, version, topic, _id)
tiernof27c79b2018-03-12 17:08:42 +0100815 outdata = {"id": _id}
tiernob24258a2018-10-04 18:39:49 +0200816 # TODO form NsdInfo when topic in ("ns_descriptors", "vnf_packages")
tiernof27c79b2018-03-12 17:08:42 +0100817 cherrypy.response.status = HTTPStatus.CREATED.value
tierno09c073e2018-04-26 13:36:48 +0200818
tiernoc94c3df2018-02-09 15:38:54 +0100819 elif method == "DELETE":
820 if not _id:
tiernob24258a2018-10-04 18:39:49 +0200821 outdata = self.engine.del_item_list(session, engine_topic, kwargs)
tierno09c073e2018-04-26 13:36:48 +0200822 cherrypy.response.status = HTTPStatus.OK.value
tiernoc94c3df2018-02-09 15:38:54 +0100823 else: # len(args) > 1
tiernoe8631782018-12-21 13:31:52 +0000824 delete_in_process = False
tiernob24258a2018-10-04 18:39:49 +0200825 if topic == "ns_instances_content" and not force:
826 nslcmop_desc = {
827 "lcmOperationType": "terminate",
828 "nsInstanceId": _id,
829 "autoremove": True
830 }
831 opp_id = self.engine.new_item(rollback, session, "nslcmops", nslcmop_desc, None)
tiernoe8631782018-12-21 13:31:52 +0000832 if opp_id:
833 delete_in_process = True
834 outdata = {"_id": opp_id}
835 cherrypy.response.status = HTTPStatus.ACCEPTED.value
garciadeblas9750c5a2018-10-15 16:20:35 +0200836 elif topic == "netslice_instances_content" and not force:
837 nsilcmop_desc = {
838 "lcmOperationType": "terminate",
839 "nsiInstanceId": _id,
840 "autoremove": True
841 }
842 opp_id = self.engine.new_item(rollback, session, "nsilcmops", nsilcmop_desc, None)
tiernoe8631782018-12-21 13:31:52 +0000843 if opp_id:
844 delete_in_process = True
845 outdata = {"_id": opp_id}
846 cherrypy.response.status = HTTPStatus.ACCEPTED.value
847 if not delete_in_process:
tiernob24258a2018-10-04 18:39:49 +0200848 self.engine.del_item(session, engine_topic, _id, force)
tierno09c073e2018-04-26 13:36:48 +0200849 cherrypy.response.status = HTTPStatus.NO_CONTENT.value
tierno55ba2e62018-12-11 17:22:22 +0000850 if engine_topic in ("vim_accounts", "wim_accounts", "sdns"):
tierno09c073e2018-04-26 13:36:48 +0200851 cherrypy.response.status = HTTPStatus.ACCEPTED.value
852
tierno7ae10112018-05-18 14:36:02 +0200853 elif method in ("PUT", "PATCH"):
tiernocd54a4a2018-09-12 16:40:35 +0200854 outdata = None
tiernof27c79b2018-03-12 17:08:42 +0100855 if not indata and not kwargs:
tiernoc94c3df2018-02-09 15:38:54 +0100856 raise NbiException("Nothing to update. Provide payload and/or query string",
857 HTTPStatus.BAD_REQUEST)
garciadeblas9750c5a2018-10-15 16:20:35 +0200858 if item in ("nsd_content", "package_content", "nst_content") and method == "PUT":
tiernob24258a2018-10-04 18:39:49 +0200859 completed = self.engine.upload_content(session, engine_topic, _id, indata, kwargs,
860 cherrypy.request.headers, force=force)
tiernof27c79b2018-03-12 17:08:42 +0100861 if not completed:
862 cherrypy.response.headers["Transaction-Id"] = id
tiernof27c79b2018-03-12 17:08:42 +0100863 else:
tiernob24258a2018-10-04 18:39:49 +0200864 self.engine.edit_item(session, engine_topic, _id, indata, kwargs, force=force)
tiernocd54a4a2018-09-12 16:40:35 +0200865 cherrypy.response.status = HTTPStatus.NO_CONTENT.value
tiernoc94c3df2018-02-09 15:38:54 +0100866 else:
867 raise NbiException("Method {} not allowed".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
tiernof27c79b2018-03-12 17:08:42 +0100868 return self._format_out(outdata, session, _format)
tiernob24258a2018-10-04 18:39:49 +0200869 except Exception as e:
tierno36ec8602018-11-02 17:27:11 +0100870 if isinstance(e, (NbiException, EngineException, DbException, FsException, MsgException, AuthException,
871 ValidationError)):
tiernob24258a2018-10-04 18:39:49 +0200872 http_code_value = cherrypy.response.status = e.http_code.value
873 http_code_name = e.http_code.name
874 cherrypy.log("Exception {}".format(e))
875 else:
876 http_code_value = cherrypy.response.status = HTTPStatus.BAD_REQUEST.value # INTERNAL_SERVER_ERROR
Felipe Vicense36ab852018-11-23 14:12:09 +0100877 cherrypy.log("CRITICAL: Exception {}".format(e), traceback=True)
tiernob24258a2018-10-04 18:39:49 +0200878 http_code_name = HTTPStatus.BAD_REQUEST.name
tierno3ace63c2018-05-03 17:51:43 +0200879 if hasattr(outdata, "close"): # is an open file
880 outdata.close()
tiernob24258a2018-10-04 18:39:49 +0200881 error_text = str(e)
882 rollback.reverse()
tiernodb9dc582018-06-20 17:27:29 +0200883 for rollback_item in rollback:
tierno3ace63c2018-05-03 17:51:43 +0200884 try:
tiernocc103432018-10-19 14:10:35 +0200885 if rollback_item.get("operation") == "set":
886 self.engine.db.set_one(rollback_item["topic"], {"_id": rollback_item["_id"]},
887 rollback_item["content"], fail_on_empty=False)
888 else:
tiernoe8631782018-12-21 13:31:52 +0000889 self.engine.db.del_one(rollback_item["topic"], {"_id": rollback_item["_id"]},
890 fail_on_empty=False)
tierno3ace63c2018-05-03 17:51:43 +0200891 except Exception as e2:
tiernob24258a2018-10-04 18:39:49 +0200892 rollback_error_text = "Rollback Exception {}: {}".format(rollback_item, e2)
893 cherrypy.log(rollback_error_text)
894 error_text += ". " + rollback_error_text
895 # if isinstance(e, MsgException):
896 # error_text = "{} has been '{}' but other modules cannot be informed because an error on bus".format(
897 # engine_topic[:-1], method, error_text)
tiernoc94c3df2018-02-09 15:38:54 +0100898 problem_details = {
tiernob24258a2018-10-04 18:39:49 +0200899 "code": http_code_name,
900 "status": http_code_value,
901 "detail": error_text,
tiernoc94c3df2018-02-09 15:38:54 +0100902 }
903 return self._format_out(problem_details, session)
904 # raise cherrypy.HTTPError(e.http_code.value, str(e))
905
906
tiernoc94c3df2018-02-09 15:38:54 +0100907def _start_service():
908 """
909 Callback function called when cherrypy.engine starts
910 Override configuration with env variables
911 Set database, storage, message configuration
912 Init database with admin/admin user password
913 """
tierno932499c2019-01-28 17:28:10 +0000914 global nbi_server
915 global subscription_thread
tiernoc94c3df2018-02-09 15:38:54 +0100916 cherrypy.log.error("Starting osm_nbi")
917 # update general cherrypy configuration
918 update_dict = {}
919
920 engine_config = cherrypy.tree.apps['/osm'].config
921 for k, v in environ.items():
922 if not k.startswith("OSMNBI_"):
923 continue
tiernoe1281182018-05-22 12:24:36 +0200924 k1, _, k2 = k[7:].lower().partition("_")
tiernoc94c3df2018-02-09 15:38:54 +0100925 if not k2:
926 continue
927 try:
928 # update static configuration
929 if k == 'OSMNBI_STATIC_DIR':
930 engine_config["/static"]['tools.staticdir.dir'] = v
931 engine_config["/static"]['tools.staticdir.on'] = True
932 elif k == 'OSMNBI_SOCKET_PORT' or k == 'OSMNBI_SERVER_PORT':
933 update_dict['server.socket_port'] = int(v)
934 elif k == 'OSMNBI_SOCKET_HOST' or k == 'OSMNBI_SERVER_HOST':
935 update_dict['server.socket_host'] = v
tiernof5298be2018-05-16 14:43:57 +0200936 elif k1 in ("server", "test", "auth", "log"):
937 update_dict[k1 + '.' + k2] = v
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100938 elif k1 in ("message", "database", "storage", "authentication"):
tiernof5298be2018-05-16 14:43:57 +0200939 # k2 = k2.replace('_', '.')
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100940 if k2 in ("port", "db_port"):
tiernoc94c3df2018-02-09 15:38:54 +0100941 engine_config[k1][k2] = int(v)
942 else:
943 engine_config[k1][k2] = v
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100944
tiernoc94c3df2018-02-09 15:38:54 +0100945 except ValueError as e:
946 cherrypy.log.error("Ignoring environ '{}': " + str(e))
947 except Exception as e:
948 cherrypy.log.warn("skipping environ '{}' on exception '{}'".format(k, e))
949
950 if update_dict:
951 cherrypy.config.update(update_dict)
tiernof5298be2018-05-16 14:43:57 +0200952 engine_config["global"].update(update_dict)
tiernoc94c3df2018-02-09 15:38:54 +0100953
954 # logging cherrypy
955 log_format_simple = "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(message)s"
956 log_formatter_simple = logging.Formatter(log_format_simple, datefmt='%Y-%m-%dT%H:%M:%S')
957 logger_server = logging.getLogger("cherrypy.error")
958 logger_access = logging.getLogger("cherrypy.access")
959 logger_cherry = logging.getLogger("cherrypy")
960 logger_nbi = logging.getLogger("nbi")
961
tiernof5298be2018-05-16 14:43:57 +0200962 if "log.file" in engine_config["global"]:
963 file_handler = logging.handlers.RotatingFileHandler(engine_config["global"]["log.file"],
tiernoc94c3df2018-02-09 15:38:54 +0100964 maxBytes=100e6, backupCount=9, delay=0)
965 file_handler.setFormatter(log_formatter_simple)
966 logger_cherry.addHandler(file_handler)
967 logger_nbi.addHandler(file_handler)
tiernob24258a2018-10-04 18:39:49 +0200968 # log always to standard output
969 for format_, logger in {"nbi.server %(filename)s:%(lineno)s": logger_server,
970 "nbi.access %(filename)s:%(lineno)s": logger_access,
971 "%(name)s %(filename)s:%(lineno)s": logger_nbi
972 }.items():
973 log_format_cherry = "%(asctime)s %(levelname)s {} %(message)s".format(format_)
974 log_formatter_cherry = logging.Formatter(log_format_cherry, datefmt='%Y-%m-%dT%H:%M:%S')
975 str_handler = logging.StreamHandler()
976 str_handler.setFormatter(log_formatter_cherry)
977 logger.addHandler(str_handler)
tiernoc94c3df2018-02-09 15:38:54 +0100978
tiernof5298be2018-05-16 14:43:57 +0200979 if engine_config["global"].get("log.level"):
980 logger_cherry.setLevel(engine_config["global"]["log.level"])
981 logger_nbi.setLevel(engine_config["global"]["log.level"])
tiernoc94c3df2018-02-09 15:38:54 +0100982
983 # logging other modules
984 for k1, logname in {"message": "nbi.msg", "database": "nbi.db", "storage": "nbi.fs"}.items():
985 engine_config[k1]["logger_name"] = logname
986 logger_module = logging.getLogger(logname)
987 if "logfile" in engine_config[k1]:
988 file_handler = logging.handlers.RotatingFileHandler(engine_config[k1]["logfile"],
tierno2236d202018-05-16 19:05:16 +0200989 maxBytes=100e6, backupCount=9, delay=0)
tiernoc94c3df2018-02-09 15:38:54 +0100990 file_handler.setFormatter(log_formatter_simple)
991 logger_module.addHandler(file_handler)
992 if "loglevel" in engine_config[k1]:
993 logger_module.setLevel(engine_config[k1]["loglevel"])
994 # TODO add more entries, e.g.: storage
995 cherrypy.tree.apps['/osm'].root.engine.start(engine_config)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100996 cherrypy.tree.apps['/osm'].root.authenticator.start(engine_config)
tiernod985a8d2018-10-19 14:12:28 +0200997 cherrypy.tree.apps['/osm'].root.engine.init_db(target_version=database_version)
998 cherrypy.tree.apps['/osm'].root.authenticator.init_db(target_version=auth_database_version)
tiernobee508e2019-01-21 11:21:49 +0000999
tierno932499c2019-01-28 17:28:10 +00001000 # start subscriptions thread:
1001 subscription_thread = SubscriptionThread(config=engine_config, engine=nbi_server.engine)
1002 subscription_thread.start()
1003 # Do not capture except SubscriptionException
1004
tiernobee508e2019-01-21 11:21:49 +00001005 # load and print version. Ignore possible errors, e.g. file not found
1006 try:
1007 with open("{}/version".format(engine_config["/static"]['tools.staticdir.dir'])) as version_file:
1008 version_data = version_file.read()
1009 cherrypy.log.error("Starting OSM NBI Version: {}".format(version_data.replace("\n", " ")))
1010 except Exception:
1011 pass
tiernoc94c3df2018-02-09 15:38:54 +01001012
1013
1014def _stop_service():
1015 """
1016 Callback function called when cherrypy.engine stops
1017 TODO: Ending database connections.
1018 """
tierno932499c2019-01-28 17:28:10 +00001019 global subscription_thread
1020 subscription_thread.terminate()
1021 subscription_thread = None
tiernoc94c3df2018-02-09 15:38:54 +01001022 cherrypy.tree.apps['/osm'].root.engine.stop()
1023 cherrypy.log.error("Stopping osm_nbi")
1024
tierno2236d202018-05-16 19:05:16 +02001025
tiernof5298be2018-05-16 14:43:57 +02001026def nbi(config_file):
tierno932499c2019-01-28 17:28:10 +00001027 global nbi_server
tiernoc94c3df2018-02-09 15:38:54 +01001028 # conf = {
1029 # '/': {
1030 # #'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
1031 # 'tools.sessions.on': True,
1032 # 'tools.response_headers.on': True,
1033 # # 'tools.response_headers.headers': [('Content-Type', 'text/plain')],
1034 # }
1035 # }
1036 # cherrypy.Server.ssl_module = 'builtin'
1037 # cherrypy.Server.ssl_certificate = "http/cert.pem"
1038 # cherrypy.Server.ssl_private_key = "http/privkey.pem"
1039 # cherrypy.Server.thread_pool = 10
1040 # cherrypy.config.update({'Server.socket_port': config["port"], 'Server.socket_host': config["host"]})
1041
1042 # cherrypy.config.update({'tools.auth_basic.on': True,
1043 # 'tools.auth_basic.realm': 'localhost',
1044 # 'tools.auth_basic.checkpassword': validate_password})
tierno932499c2019-01-28 17:28:10 +00001045 nbi_server = Server()
tiernoc94c3df2018-02-09 15:38:54 +01001046 cherrypy.engine.subscribe('start', _start_service)
1047 cherrypy.engine.subscribe('stop', _stop_service)
tierno932499c2019-01-28 17:28:10 +00001048 cherrypy.quickstart(nbi_server, '/osm', config_file)
tiernof5298be2018-05-16 14:43:57 +02001049
1050
1051def usage():
1052 print("""Usage: {} [options]
1053 -c|--config [configuration_file]: loads the configuration file (default: ./nbi.cfg)
1054 -h|--help: shows this help
1055 """.format(sys.argv[0]))
tierno2236d202018-05-16 19:05:16 +02001056 # --log-socket-host HOST: send logs to this host")
1057 # --log-socket-port PORT: send logs using this port (default: 9022)")
tiernoc94c3df2018-02-09 15:38:54 +01001058
1059
1060if __name__ == '__main__':
tiernof5298be2018-05-16 14:43:57 +02001061 try:
1062 # load parameters and configuration
1063 opts, args = getopt.getopt(sys.argv[1:], "hvc:", ["config=", "help"])
1064 # TODO add "log-socket-host=", "log-socket-port=", "log-file="
1065 config_file = None
1066 for o, a in opts:
1067 if o in ("-h", "--help"):
1068 usage()
1069 sys.exit()
1070 elif o in ("-c", "--config"):
1071 config_file = a
1072 # elif o == "--log-socket-port":
1073 # log_socket_port = a
1074 # elif o == "--log-socket-host":
1075 # log_socket_host = a
1076 # elif o == "--log-file":
1077 # log_file = a
1078 else:
1079 assert False, "Unhandled option"
1080 if config_file:
1081 if not path.isfile(config_file):
1082 print("configuration file '{}' that not exist".format(config_file), file=sys.stderr)
1083 exit(1)
1084 else:
1085 for config_file in (__file__[:__file__.rfind(".")] + ".cfg", "./nbi.cfg", "/etc/osm/nbi.cfg"):
1086 if path.isfile(config_file):
1087 break
1088 else:
1089 print("No configuration file 'nbi.cfg' found neither at local folder nor at /etc/osm/", file=sys.stderr)
1090 exit(1)
1091 nbi(config_file)
1092 except getopt.GetoptError as e:
1093 print(str(e), file=sys.stderr)
1094 # usage()
1095 exit(1)