blob: a9e2b244ca8143dba5d771cebb3808be16f3941a [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 },
tierno0f98af52018-03-19 10:28:22 +0100216 "vims": {"METHODS": ("GET", "POST"),
tierno7ae10112018-05-18 14:36:02 +0200217 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
tierno2236d202018-05-16 19:05:16 +0200218 },
tierno09c073e2018-04-26 13:36:48 +0200219 "vim_accounts": {"METHODS": ("GET", "POST"),
tierno7ae10112018-05-18 14:36:02 +0200220 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
tierno2236d202018-05-16 19:05:16 +0200221 },
tierno55ba2e62018-12-11 17:22:22 +0000222 "wim_accounts": {"METHODS": ("GET", "POST"),
223 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
224 },
tierno0f98af52018-03-19 10:28:22 +0100225 "sdns": {"METHODS": ("GET", "POST"),
tierno7ae10112018-05-18 14:36:02 +0200226 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
tierno2236d202018-05-16 19:05:16 +0200227 },
tiernof27c79b2018-03-12 17:08:42 +0100228 }
229 },
tiernocb83c942018-09-24 17:28:13 +0200230 "pdu": {
231 "v1": {
232 "pdu_descriptors": {"METHODS": ("GET", "POST"),
233 "<ID>": {"METHODS": ("GET", "POST", "DELETE", "PATCH", "PUT")}
234 },
235 }
236 },
tiernof27c79b2018-03-12 17:08:42 +0100237 "nsd": {
238 "v1": {
tierno2236d202018-05-16 19:05:16 +0200239 "ns_descriptors_content": {"METHODS": ("GET", "POST"),
240 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
241 },
242 "ns_descriptors": {"METHODS": ("GET", "POST"),
tierno36ec8602018-11-02 17:27:11 +0100243 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"),
tierno2236d202018-05-16 19:05:16 +0200244 "nsd_content": {"METHODS": ("GET", "PUT")},
245 "nsd": {"METHODS": "GET"}, # descriptor inside package
246 "artifacts": {"*": {"METHODS": "GET"}}
247 }
248 },
tiernof27c79b2018-03-12 17:08:42 +0100249 "pnf_descriptors": {"TODO": ("GET", "POST"),
tierno2236d202018-05-16 19:05:16 +0200250 "<ID>": {"TODO": ("GET", "DELETE", "PATCH"),
251 "pnfd_content": {"TODO": ("GET", "PUT")}
252 }
253 },
tiernof27c79b2018-03-12 17:08:42 +0100254 "subscriptions": {"TODO": ("GET", "POST"),
tierno2236d202018-05-16 19:05:16 +0200255 "<ID>": {"TODO": ("GET", "DELETE")}
256 },
tiernof27c79b2018-03-12 17:08:42 +0100257 }
258 },
259 "vnfpkgm": {
260 "v1": {
tierno2236d202018-05-16 19:05:16 +0200261 "vnf_packages_content": {"METHODS": ("GET", "POST"),
262 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
263 },
264 "vnf_packages": {"METHODS": ("GET", "POST"),
tiernoaae4dc42018-06-11 18:55:50 +0200265 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"), # GET: vnfPkgInfo
tierno2236d202018-05-16 19:05:16 +0200266 "package_content": {"METHODS": ("GET", "PUT"), # package
267 "upload_from_uri": {"TODO": "POST"}
268 },
269 "vnfd": {"METHODS": "GET"}, # descriptor inside package
270 "artifacts": {"*": {"METHODS": "GET"}}
271 }
272 },
tiernof27c79b2018-03-12 17:08:42 +0100273 "subscriptions": {"TODO": ("GET", "POST"),
tierno2236d202018-05-16 19:05:16 +0200274 "<ID>": {"TODO": ("GET", "DELETE")}
275 },
tiernof27c79b2018-03-12 17:08:42 +0100276 }
277 },
278 "nslcm": {
279 "v1": {
280 "ns_instances_content": {"METHODS": ("GET", "POST"),
tierno2236d202018-05-16 19:05:16 +0200281 "<ID>": {"METHODS": ("GET", "DELETE")}
282 },
tierno65acb4d2018-04-06 16:42:40 +0200283 "ns_instances": {"METHODS": ("GET", "POST"),
tierno95692442018-05-24 18:05:28 +0200284 "<ID>": {"METHODS": ("GET", "DELETE"),
tiernof759d822018-06-11 18:54:54 +0200285 "scale": {"METHODS": "POST"},
tierno2236d202018-05-16 19:05:16 +0200286 "terminate": {"METHODS": "POST"},
287 "instantiate": {"METHODS": "POST"},
288 "action": {"METHODS": "POST"},
289 }
290 },
tierno65acb4d2018-04-06 16:42:40 +0200291 "ns_lcm_op_occs": {"METHODS": "GET",
tierno2236d202018-05-16 19:05:16 +0200292 "<ID>": {"METHODS": "GET"},
293 },
tierno0ffaa992018-05-09 13:21:56 +0200294 "vnfrs": {"METHODS": ("GET"),
tierno2236d202018-05-16 19:05:16 +0200295 "<ID>": {"METHODS": ("GET")}
296 },
tiernof759d822018-06-11 18:54:54 +0200297 "vnf_instances": {"METHODS": ("GET"),
298 "<ID>": {"METHODS": ("GET")}
299 },
tiernof27c79b2018-03-12 17:08:42 +0100300 }
301 },
garciadeblas9750c5a2018-10-15 16:20:35 +0200302 "nst": {
303 "v1": {
304 "netslice_templates_content": {"METHODS": ("GET", "POST"),
305 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
306 },
307 "netslice_templates": {"METHODS": ("GET", "POST"),
308 "<ID>": {"METHODS": ("GET", "DELETE"), "TODO": "PATCH",
309 "nst_content": {"METHODS": ("GET", "PUT")},
310 "nst": {"METHODS": "GET"}, # descriptor inside package
311 "artifacts": {"*": {"METHODS": "GET"}}
312 }
313 },
314 "subscriptions": {"TODO": ("GET", "POST"),
315 "<ID>": {"TODO": ("GET", "DELETE")}
316 },
317 }
318 },
319 "nsilcm": {
320 "v1": {
321 "netslice_instances_content": {"METHODS": ("GET", "POST"),
322 "<ID>": {"METHODS": ("GET", "DELETE")}
323 },
324 "netslice_instances": {"METHODS": ("GET", "POST"),
325 "<ID>": {"METHODS": ("GET", "DELETE"),
326 "terminate": {"METHODS": "POST"},
327 "instantiate": {"METHODS": "POST"},
328 "action": {"METHODS": "POST"},
329 }
330 },
331 "nsi_lcm_op_occs": {"METHODS": "GET",
332 "<ID>": {"METHODS": "GET"},
333 },
334 }
335 },
tiernof27c79b2018-03-12 17:08:42 +0100336 }
tiernoc94c3df2018-02-09 15:38:54 +0100337
tiernoc94c3df2018-02-09 15:38:54 +0100338 def _format_in(self, kwargs):
339 try:
340 indata = None
341 if cherrypy.request.body.length:
342 error_text = "Invalid input format "
343
344 if "Content-Type" in cherrypy.request.headers:
345 if "application/json" in cherrypy.request.headers["Content-Type"]:
346 error_text = "Invalid json format "
347 indata = json.load(self.reader(cherrypy.request.body))
gcalvinode4adfe2018-10-30 11:46:09 +0100348 cherrypy.request.headers.pop("Content-File-MD5", None)
tiernoc94c3df2018-02-09 15:38:54 +0100349 elif "application/yaml" in cherrypy.request.headers["Content-Type"]:
350 error_text = "Invalid yaml format "
351 indata = yaml.load(cherrypy.request.body)
gcalvinode4adfe2018-10-30 11:46:09 +0100352 cherrypy.request.headers.pop("Content-File-MD5", None)
tiernoc94c3df2018-02-09 15:38:54 +0100353 elif "application/binary" in cherrypy.request.headers["Content-Type"] or \
354 "application/gzip" in cherrypy.request.headers["Content-Type"] or \
tiernof27c79b2018-03-12 17:08:42 +0100355 "application/zip" in cherrypy.request.headers["Content-Type"] or \
356 "text/plain" in cherrypy.request.headers["Content-Type"]:
357 indata = cherrypy.request.body # .read()
tiernoc94c3df2018-02-09 15:38:54 +0100358 elif "multipart/form-data" in cherrypy.request.headers["Content-Type"]:
359 if "descriptor_file" in kwargs:
360 filecontent = kwargs.pop("descriptor_file")
361 if not filecontent.file:
362 raise NbiException("empty file or content", HTTPStatus.BAD_REQUEST)
tiernof27c79b2018-03-12 17:08:42 +0100363 indata = filecontent.file # .read()
tiernoc94c3df2018-02-09 15:38:54 +0100364 if filecontent.content_type.value:
365 cherrypy.request.headers["Content-Type"] = filecontent.content_type.value
366 else:
367 # raise cherrypy.HTTPError(HTTPStatus.Not_Acceptable,
368 # "Only 'Content-Type' of type 'application/json' or
369 # 'application/yaml' for input format are available")
370 error_text = "Invalid yaml format "
371 indata = yaml.load(cherrypy.request.body)
gcalvinode4adfe2018-10-30 11:46:09 +0100372 cherrypy.request.headers.pop("Content-File-MD5", None)
tiernoc94c3df2018-02-09 15:38:54 +0100373 else:
374 error_text = "Invalid yaml format "
375 indata = yaml.load(cherrypy.request.body)
gcalvinode4adfe2018-10-30 11:46:09 +0100376 cherrypy.request.headers.pop("Content-File-MD5", None)
tiernoc94c3df2018-02-09 15:38:54 +0100377 if not indata:
378 indata = {}
379
tiernoc94c3df2018-02-09 15:38:54 +0100380 format_yaml = False
381 if cherrypy.request.headers.get("Query-String-Format") == "yaml":
382 format_yaml = True
383
384 for k, v in kwargs.items():
385 if isinstance(v, str):
386 if v == "":
387 kwargs[k] = None
388 elif format_yaml:
389 try:
390 kwargs[k] = yaml.load(v)
tiernoe1281182018-05-22 12:24:36 +0200391 except Exception:
tiernoc94c3df2018-02-09 15:38:54 +0100392 pass
393 elif k.endswith(".gt") or k.endswith(".lt") or k.endswith(".gte") or k.endswith(".lte"):
394 try:
395 kwargs[k] = int(v)
tiernoe1281182018-05-22 12:24:36 +0200396 except Exception:
tiernoc94c3df2018-02-09 15:38:54 +0100397 try:
398 kwargs[k] = float(v)
tiernoe1281182018-05-22 12:24:36 +0200399 except Exception:
tiernoc94c3df2018-02-09 15:38:54 +0100400 pass
401 elif v.find(",") > 0:
402 kwargs[k] = v.split(",")
403 elif isinstance(v, (list, tuple)):
404 for index in range(0, len(v)):
405 if v[index] == "":
406 v[index] = None
407 elif format_yaml:
408 try:
409 v[index] = yaml.load(v[index])
tiernoe1281182018-05-22 12:24:36 +0200410 except Exception:
tiernoc94c3df2018-02-09 15:38:54 +0100411 pass
412
tiernof27c79b2018-03-12 17:08:42 +0100413 return indata
tiernoc94c3df2018-02-09 15:38:54 +0100414 except (ValueError, yaml.YAMLError) as exc:
415 raise NbiException(error_text + str(exc), HTTPStatus.BAD_REQUEST)
416 except KeyError as exc:
417 raise NbiException("Query string error: " + str(exc), HTTPStatus.BAD_REQUEST)
tiernob92094f2018-05-11 13:44:22 +0200418 except Exception as exc:
419 raise NbiException(error_text + str(exc), HTTPStatus.BAD_REQUEST)
tiernoc94c3df2018-02-09 15:38:54 +0100420
421 @staticmethod
tiernof27c79b2018-03-12 17:08:42 +0100422 def _format_out(data, session=None, _format=None):
tiernoc94c3df2018-02-09 15:38:54 +0100423 """
424 return string of dictionary data according to requested json, yaml, xml. By default json
tiernof27c79b2018-03-12 17:08:42 +0100425 :param data: response to be sent. Can be a dict, text or file
tiernoc94c3df2018-02-09 15:38:54 +0100426 :param session:
tiernof27c79b2018-03-12 17:08:42 +0100427 :param _format: The format to be set as Content-Type ir data is a file
tiernoc94c3df2018-02-09 15:38:54 +0100428 :return: None
429 """
tierno0f98af52018-03-19 10:28:22 +0100430 accept = cherrypy.request.headers.get("Accept")
tiernof27c79b2018-03-12 17:08:42 +0100431 if data is None:
tierno0f98af52018-03-19 10:28:22 +0100432 if accept and "text/html" in accept:
433 return html.format(data, cherrypy.request, cherrypy.response, session)
tierno09c073e2018-04-26 13:36:48 +0200434 # cherrypy.response.status = HTTPStatus.NO_CONTENT.value
tiernof27c79b2018-03-12 17:08:42 +0100435 return
436 elif hasattr(data, "read"): # file object
437 if _format:
438 cherrypy.response.headers["Content-Type"] = _format
439 elif "b" in data.mode: # binariy asssumig zip
440 cherrypy.response.headers["Content-Type"] = 'application/zip'
441 else:
442 cherrypy.response.headers["Content-Type"] = 'text/plain'
443 # TODO check that cherrypy close file. If not implement pending things to close per thread next
444 return data
tierno0f98af52018-03-19 10:28:22 +0100445 if accept:
tiernoc94c3df2018-02-09 15:38:54 +0100446 if "application/json" in accept:
447 cherrypy.response.headers["Content-Type"] = 'application/json; charset=utf-8'
448 a = json.dumps(data, indent=4) + "\n"
449 return a.encode("utf8")
450 elif "text/html" in accept:
451 return html.format(data, cherrypy.request, cherrypy.response, session)
452
tiernof27c79b2018-03-12 17:08:42 +0100453 elif "application/yaml" in accept or "*/*" in accept or "text/plain" in accept:
tiernoc94c3df2018-02-09 15:38:54 +0100454 pass
tiernof717cbe2018-12-03 16:35:42 +0000455 # if there is not any valid accept, raise an error. But if response is already an error, format in yaml
456 elif cherrypy.response.status >= 400:
tiernoc94c3df2018-02-09 15:38:54 +0100457 raise cherrypy.HTTPError(HTTPStatus.NOT_ACCEPTABLE.value,
458 "Only 'Accept' of type 'application/json' or 'application/yaml' "
459 "for output format are available")
460 cherrypy.response.headers["Content-Type"] = 'application/yaml'
461 return yaml.safe_dump(data, explicit_start=True, indent=4, default_flow_style=False, tags=False,
462 encoding='utf-8', allow_unicode=True) # , canonical=True, default_style='"'
463
464 @cherrypy.expose
465 def index(self, *args, **kwargs):
466 session = None
467 try:
468 if cherrypy.request.method == "GET":
Eduardo Sousa2f988212018-07-26 01:04:11 +0100469 session = self.authenticator.authorize()
tiernoc94c3df2018-02-09 15:38:54 +0100470 outdata = "Index page"
471 else:
472 raise cherrypy.HTTPError(HTTPStatus.METHOD_NOT_ALLOWED.value,
tierno2236d202018-05-16 19:05:16 +0200473 "Method {} not allowed for tokens".format(cherrypy.request.method))
tiernoc94c3df2018-02-09 15:38:54 +0100474
475 return self._format_out(outdata, session)
476
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100477 except (EngineException, AuthException) as e:
tiernoc94c3df2018-02-09 15:38:54 +0100478 cherrypy.log("index Exception {}".format(e))
479 cherrypy.response.status = e.http_code.value
480 return self._format_out("Welcome to OSM!", session)
481
482 @cherrypy.expose
tierno55945e72018-04-06 16:40:27 +0200483 def version(self, *args, **kwargs):
tiernodfe09572018-04-24 10:41:10 +0200484 # TODO consider to remove and provide version using the static version file
tierno55945e72018-04-06 16:40:27 +0200485 global __version__, version_date
486 try:
487 if cherrypy.request.method != "GET":
488 raise NbiException("Only method GET is allowed", HTTPStatus.METHOD_NOT_ALLOWED)
489 elif args or kwargs:
490 raise NbiException("Invalid URL or query string for version", HTTPStatus.METHOD_NOT_ALLOWED)
491 return __version__ + " " + version_date
492 except NbiException as e:
493 cherrypy.response.status = e.http_code.value
494 problem_details = {
495 "code": e.http_code.name,
496 "status": e.http_code.value,
497 "detail": str(e),
498 }
499 return self._format_out(problem_details, None)
500
501 @cherrypy.expose
tiernof27c79b2018-03-12 17:08:42 +0100502 def token(self, method, token_id=None, kwargs=None):
tiernoc94c3df2018-02-09 15:38:54 +0100503 session = None
504 # self.engine.load_dbase(cherrypy.request.app.config)
tiernof27c79b2018-03-12 17:08:42 +0100505 indata = self._format_in(kwargs)
506 if not isinstance(indata, dict):
507 raise NbiException("Expected application/yaml or application/json Content-Type", HTTPStatus.BAD_REQUEST)
tiernoc94c3df2018-02-09 15:38:54 +0100508 try:
tiernoc94c3df2018-02-09 15:38:54 +0100509 if method == "GET":
Eduardo Sousa2f988212018-07-26 01:04:11 +0100510 session = self.authenticator.authorize()
tiernof27c79b2018-03-12 17:08:42 +0100511 if token_id:
Eduardo Sousa2f988212018-07-26 01:04:11 +0100512 outdata = self.authenticator.get_token(session, token_id)
tiernoc94c3df2018-02-09 15:38:54 +0100513 else:
Eduardo Sousa2f988212018-07-26 01:04:11 +0100514 outdata = self.authenticator.get_token_list(session)
tiernoc94c3df2018-02-09 15:38:54 +0100515 elif method == "POST":
516 try:
Eduardo Sousa2f988212018-07-26 01:04:11 +0100517 session = self.authenticator.authorize()
tiernoe1281182018-05-22 12:24:36 +0200518 except Exception:
tiernoc94c3df2018-02-09 15:38:54 +0100519 session = None
520 if kwargs:
521 indata.update(kwargs)
Eduardo Sousa2f988212018-07-26 01:04:11 +0100522 outdata = self.authenticator.new_token(session, indata, cherrypy.request.remote)
tiernoc94c3df2018-02-09 15:38:54 +0100523 session = outdata
524 cherrypy.session['Authorization'] = outdata["_id"]
tierno0f98af52018-03-19 10:28:22 +0100525 self._set_location_header("admin", "v1", "tokens", outdata["_id"])
tiernoc94c3df2018-02-09 15:38:54 +0100526 # cherrypy.response.cookie["Authorization"] = outdata["id"]
527 # cherrypy.response.cookie["Authorization"]['expires'] = 3600
528 elif method == "DELETE":
tiernof27c79b2018-03-12 17:08:42 +0100529 if not token_id and "id" in kwargs:
tiernoc94c3df2018-02-09 15:38:54 +0100530 token_id = kwargs["id"]
tiernof27c79b2018-03-12 17:08:42 +0100531 elif not token_id:
Eduardo Sousa2f988212018-07-26 01:04:11 +0100532 session = self.authenticator.authorize()
tiernoc94c3df2018-02-09 15:38:54 +0100533 token_id = session["_id"]
Eduardo Sousa2f988212018-07-26 01:04:11 +0100534 outdata = self.authenticator.del_token(token_id)
tiernoc94c3df2018-02-09 15:38:54 +0100535 session = None
536 cherrypy.session['Authorization'] = "logout"
537 # cherrypy.response.cookie["Authorization"] = token_id
538 # cherrypy.response.cookie["Authorization"]['expires'] = 0
539 else:
540 raise NbiException("Method {} not allowed for token".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
541 return self._format_out(outdata, session)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100542 except (NbiException, EngineException, DbException, AuthException) as e:
tiernoc94c3df2018-02-09 15:38:54 +0100543 cherrypy.log("tokens Exception {}".format(e))
544 cherrypy.response.status = e.http_code.value
545 problem_details = {
546 "code": e.http_code.name,
547 "status": e.http_code.value,
548 "detail": str(e),
549 }
550 return self._format_out(problem_details, session)
551
552 @cherrypy.expose
553 def test(self, *args, **kwargs):
554 thread_info = None
tiernof27c79b2018-03-12 17:08:42 +0100555 if args and args[0] == "help":
tiernof717cbe2018-12-03 16:35:42 +0000556 return "<html><pre>\ninit\nfile/<name> download file\ndb-clear/table\nfs-clear[/folder]\nlogin\nlogin2\n"\
tiernoe1281182018-05-22 12:24:36 +0200557 "sleep/<time>\nmessage/topic\n</pre></html>"
tiernof27c79b2018-03-12 17:08:42 +0100558
559 elif args and args[0] == "init":
tiernoc94c3df2018-02-09 15:38:54 +0100560 try:
561 # self.engine.load_dbase(cherrypy.request.app.config)
562 self.engine.create_admin()
563 return "Done. User 'admin', password 'admin' created"
564 except Exception:
565 cherrypy.response.status = HTTPStatus.FORBIDDEN.value
566 return self._format_out("Database already initialized")
tiernof27c79b2018-03-12 17:08:42 +0100567 elif args and args[0] == "file":
568 return cherrypy.lib.static.serve_file(cherrypy.tree.apps['/osm'].config["storage"]["path"] + "/" + args[1],
569 "text/plain", "attachment")
570 elif args and args[0] == "file2":
571 f_path = cherrypy.tree.apps['/osm'].config["storage"]["path"] + "/" + args[1]
572 f = open(f_path, "r")
573 cherrypy.response.headers["Content-type"] = "text/plain"
tiernof27c79b2018-03-12 17:08:42 +0100574 return f
tierno55945e72018-04-06 16:40:27 +0200575
tiernof27c79b2018-03-12 17:08:42 +0100576 elif len(args) == 2 and args[0] == "db-clear":
tiernof717cbe2018-12-03 16:35:42 +0000577 deleted_info = self.engine.db.del_list(args[1], kwargs)
578 return "{} {} deleted\n".format(deleted_info["deleted"], args[1])
579 elif len(args) and args[0] == "fs-clear":
580 if len(args) >= 2:
581 folders = (args[1],)
582 else:
583 folders = self.engine.fs.dir_ls(".")
584 for folder in folders:
585 self.engine.fs.file_delete(folder)
586 return ",".join(folders) + " folders deleted\n"
tiernoc94c3df2018-02-09 15:38:54 +0100587 elif args and args[0] == "login":
588 if not cherrypy.request.headers.get("Authorization"):
589 cherrypy.response.headers["WWW-Authenticate"] = 'Basic realm="Access to OSM site", charset="UTF-8"'
590 cherrypy.response.status = HTTPStatus.UNAUTHORIZED.value
591 elif args and args[0] == "login2":
592 if not cherrypy.request.headers.get("Authorization"):
593 cherrypy.response.headers["WWW-Authenticate"] = 'Bearer realm="Access to OSM site"'
594 cherrypy.response.status = HTTPStatus.UNAUTHORIZED.value
595 elif args and args[0] == "sleep":
596 sleep_time = 5
597 try:
598 sleep_time = int(args[1])
599 except Exception:
600 cherrypy.response.status = HTTPStatus.FORBIDDEN.value
601 return self._format_out("Database already initialized")
602 thread_info = cherrypy.thread_data
603 print(thread_info)
604 time.sleep(sleep_time)
605 # thread_info
606 elif len(args) >= 2 and args[0] == "message":
tiernob24258a2018-10-04 18:39:49 +0200607 main_topic = args[1]
608 return_text = "<html><pre>{} ->\n".format(main_topic)
tiernoc94c3df2018-02-09 15:38:54 +0100609 try:
tierno55945e72018-04-06 16:40:27 +0200610 if cherrypy.request.method == 'POST':
611 to_send = yaml.load(cherrypy.request.body)
612 for k, v in to_send.items():
tiernob24258a2018-10-04 18:39:49 +0200613 self.engine.msg.write(main_topic, k, v)
tierno55945e72018-04-06 16:40:27 +0200614 return_text += " {}: {}\n".format(k, v)
615 elif cherrypy.request.method == 'GET':
616 for k, v in kwargs.items():
tiernob24258a2018-10-04 18:39:49 +0200617 self.engine.msg.write(main_topic, k, yaml.load(v))
tierno55945e72018-04-06 16:40:27 +0200618 return_text += " {}: {}\n".format(k, yaml.load(v))
tiernoc94c3df2018-02-09 15:38:54 +0100619 except Exception as e:
tierno55945e72018-04-06 16:40:27 +0200620 return_text += "Error: " + str(e)
621 return_text += "</pre></html>\n"
622 return return_text
tiernoc94c3df2018-02-09 15:38:54 +0100623
624 return_text = (
625 "<html><pre>\nheaders:\n args: {}\n".format(args) +
626 " kwargs: {}\n".format(kwargs) +
627 " headers: {}\n".format(cherrypy.request.headers) +
628 " path_info: {}\n".format(cherrypy.request.path_info) +
629 " query_string: {}\n".format(cherrypy.request.query_string) +
630 " session: {}\n".format(cherrypy.session) +
631 " cookie: {}\n".format(cherrypy.request.cookie) +
632 " method: {}\n".format(cherrypy.request.method) +
tiernof717cbe2018-12-03 16:35:42 +0000633 " session: {}\n".format(cherrypy.session.get('fieldname')) +
tiernoc94c3df2018-02-09 15:38:54 +0100634 " body:\n")
635 return_text += " length: {}\n".format(cherrypy.request.body.length)
636 if cherrypy.request.body.length:
637 return_text += " content: {}\n".format(
638 str(cherrypy.request.body.read(int(cherrypy.request.headers.get('Content-Length', 0)))))
639 if thread_info:
640 return_text += "thread: {}\n".format(thread_info)
641 return_text += "</pre></html>"
642 return return_text
643
tiernof27c79b2018-03-12 17:08:42 +0100644 def _check_valid_url_method(self, method, *args):
645 if len(args) < 3:
tiernob24258a2018-10-04 18:39:49 +0200646 raise NbiException("URL must contain at least 'main_topic/version/topic'", HTTPStatus.METHOD_NOT_ALLOWED)
tiernof27c79b2018-03-12 17:08:42 +0100647
648 reference = self.valid_methods
649 for arg in args:
650 if arg is None:
651 break
652 if not isinstance(reference, dict):
653 raise NbiException("URL contains unexpected extra items '{}'".format(arg),
654 HTTPStatus.METHOD_NOT_ALLOWED)
655
656 if arg in reference:
657 reference = reference[arg]
658 elif "<ID>" in reference:
659 reference = reference["<ID>"]
660 elif "*" in reference:
661 reference = reference["*"]
662 break
663 else:
664 raise NbiException("Unexpected URL item {}".format(arg), HTTPStatus.METHOD_NOT_ALLOWED)
665 if "TODO" in reference and method in reference["TODO"]:
666 raise NbiException("Method {} not supported yet for this URL".format(method), HTTPStatus.NOT_IMPLEMENTED)
tierno2236d202018-05-16 19:05:16 +0200667 elif "METHODS" in reference and method not in reference["METHODS"]:
tiernof27c79b2018-03-12 17:08:42 +0100668 raise NbiException("Method {} not supported for this URL".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
669 return
670
671 @staticmethod
tiernob24258a2018-10-04 18:39:49 +0200672 def _set_location_header(main_topic, version, topic, id):
tiernof27c79b2018-03-12 17:08:42 +0100673 """
674 Insert response header Location with the URL of created item base on URL params
tiernob24258a2018-10-04 18:39:49 +0200675 :param main_topic:
tiernof27c79b2018-03-12 17:08:42 +0100676 :param version:
tiernob24258a2018-10-04 18:39:49 +0200677 :param topic:
tiernof27c79b2018-03-12 17:08:42 +0100678 :param id:
679 :return: None
680 """
681 # 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 +0200682 cherrypy.response.headers["Location"] = "/osm/{}/{}/{}/{}".format(main_topic, version, topic, id)
tiernof27c79b2018-03-12 17:08:42 +0100683 return
684
tiernoc94c3df2018-02-09 15:38:54 +0100685 @cherrypy.expose
tiernob24258a2018-10-04 18:39:49 +0200686 def default(self, main_topic=None, version=None, topic=None, _id=None, item=None, *args, **kwargs):
tiernoc94c3df2018-02-09 15:38:54 +0100687 session = None
tiernof27c79b2018-03-12 17:08:42 +0100688 outdata = None
689 _format = None
tierno0f98af52018-03-19 10:28:22 +0100690 method = "DONE"
tiernob24258a2018-10-04 18:39:49 +0200691 engine_topic = None
tiernodb9dc582018-06-20 17:27:29 +0200692 rollback = []
693 session = None
tiernoc94c3df2018-02-09 15:38:54 +0100694 try:
tiernob24258a2018-10-04 18:39:49 +0200695 if not main_topic or not version or not topic:
696 raise NbiException("URL must contain at least 'main_topic/version/topic'",
697 HTTPStatus.METHOD_NOT_ALLOWED)
tiernocd65be32018-11-16 16:34:49 +0100698 if main_topic not in ("admin", "vnfpkgm", "nsd", "nslcm", "pdu", "nst", "nsilcm"):
tiernob24258a2018-10-04 18:39:49 +0200699 raise NbiException("URL main_topic '{}' not supported".format(main_topic),
700 HTTPStatus.METHOD_NOT_ALLOWED)
tiernoc94c3df2018-02-09 15:38:54 +0100701 if version != 'v1':
702 raise NbiException("URL version '{}' not supported".format(version), HTTPStatus.METHOD_NOT_ALLOWED)
703
tiernof27c79b2018-03-12 17:08:42 +0100704 if kwargs and "METHOD" in kwargs and kwargs["METHOD"] in ("PUT", "POST", "DELETE", "GET", "PATCH"):
705 method = kwargs.pop("METHOD")
706 else:
707 method = cherrypy.request.method
tiernob92094f2018-05-11 13:44:22 +0200708 if kwargs and "FORCE" in kwargs:
709 force = kwargs.pop("FORCE")
710 else:
711 force = False
tiernob24258a2018-10-04 18:39:49 +0200712 self._check_valid_url_method(method, main_topic, version, topic, _id, item, *args)
tiernob24258a2018-10-04 18:39:49 +0200713 if main_topic == "admin" and topic == "tokens":
tiernof27c79b2018-03-12 17:08:42 +0100714 return self.token(method, _id, kwargs)
715
tiernoc94c3df2018-02-09 15:38:54 +0100716 # self.engine.load_dbase(cherrypy.request.app.config)
Eduardo Sousa2f988212018-07-26 01:04:11 +0100717 session = self.authenticator.authorize()
tiernof27c79b2018-03-12 17:08:42 +0100718 indata = self._format_in(kwargs)
tiernob24258a2018-10-04 18:39:49 +0200719 engine_topic = topic
720 if topic == "subscriptions":
721 engine_topic = main_topic + "_" + topic
722 if item:
723 engine_topic = item
tiernoc94c3df2018-02-09 15:38:54 +0100724
tiernob24258a2018-10-04 18:39:49 +0200725 if main_topic == "nsd":
726 engine_topic = "nsds"
727 elif main_topic == "vnfpkgm":
728 engine_topic = "vnfds"
729 elif main_topic == "nslcm":
730 engine_topic = "nsrs"
731 if topic == "ns_lcm_op_occs":
732 engine_topic = "nslcmops"
733 if topic == "vnfrs" or topic == "vnf_instances":
734 engine_topic = "vnfrs"
garciadeblas9750c5a2018-10-15 16:20:35 +0200735 elif main_topic == "nst":
736 engine_topic = "nsts"
737 elif main_topic == "nsilcm":
738 engine_topic = "nsis"
739 if topic == "nsi_lcm_op_occs":
Felipe Vicensb57758d2018-10-16 16:00:20 +0200740 engine_topic = "nsilcmops"
tiernob24258a2018-10-04 18:39:49 +0200741 elif main_topic == "pdu":
742 engine_topic = "pdus"
743 if engine_topic == "vims": # TODO this is for backward compatibility, it will remove in the future
744 engine_topic = "vim_accounts"
tiernoc94c3df2018-02-09 15:38:54 +0100745
746 if method == "GET":
Felipe Vicens07f31722018-10-29 15:16:44 +0100747 if item in ("nsd_content", "package_content", "artifacts", "vnfd", "nsd", "nst", "nst_content"):
garciadeblas9750c5a2018-10-15 16:20:35 +0200748 if item in ("vnfd", "nsd", "nst"):
tiernof27c79b2018-03-12 17:08:42 +0100749 path = "$DESCRIPTOR"
750 elif args:
751 path = args
tiernob24258a2018-10-04 18:39:49 +0200752 elif item == "artifacts":
tiernof27c79b2018-03-12 17:08:42 +0100753 path = ()
754 else:
755 path = None
tiernob24258a2018-10-04 18:39:49 +0200756 file, _format = self.engine.get_file(session, engine_topic, _id, path,
tierno2236d202018-05-16 19:05:16 +0200757 cherrypy.request.headers.get("Accept"))
tiernof27c79b2018-03-12 17:08:42 +0100758 outdata = file
759 elif not _id:
tiernob24258a2018-10-04 18:39:49 +0200760 outdata = self.engine.get_item_list(session, engine_topic, kwargs)
tiernoc94c3df2018-02-09 15:38:54 +0100761 else:
tiernob24258a2018-10-04 18:39:49 +0200762 outdata = self.engine.get_item(session, engine_topic, _id)
tiernof27c79b2018-03-12 17:08:42 +0100763 elif method == "POST":
garciadeblas9750c5a2018-10-15 16:20:35 +0200764 if topic in ("ns_descriptors_content", "vnf_packages_content", "netslice_templates_content"):
tiernof27c79b2018-03-12 17:08:42 +0100765 _id = cherrypy.request.headers.get("Transaction-Id")
766 if not _id:
tiernob24258a2018-10-04 18:39:49 +0200767 _id = self.engine.new_item(rollback, session, engine_topic, {}, None, cherrypy.request.headers,
tiernob92094f2018-05-11 13:44:22 +0200768 force=force)
tiernob24258a2018-10-04 18:39:49 +0200769 completed = self.engine.upload_content(session, engine_topic, _id, indata, kwargs,
770 cherrypy.request.headers, force=force)
tiernof27c79b2018-03-12 17:08:42 +0100771 if completed:
tiernob24258a2018-10-04 18:39:49 +0200772 self._set_location_header(main_topic, version, topic, _id)
tiernof27c79b2018-03-12 17:08:42 +0100773 else:
774 cherrypy.response.headers["Transaction-Id"] = _id
775 outdata = {"id": _id}
tiernob24258a2018-10-04 18:39:49 +0200776 elif topic == "ns_instances_content":
777 # creates NSR
778 _id = self.engine.new_item(rollback, session, engine_topic, indata, kwargs, force=force)
779 # creates nslcmop
780 indata["lcmOperationType"] = "instantiate"
781 indata["nsInstanceId"] = _id
782 self.engine.new_item(rollback, session, "nslcmops", indata, None)
783 self._set_location_header(main_topic, version, topic, _id)
tiernof27c79b2018-03-12 17:08:42 +0100784 outdata = {"id": _id}
tiernob24258a2018-10-04 18:39:49 +0200785 elif topic == "ns_instances" and item:
786 indata["lcmOperationType"] = item
787 indata["nsInstanceId"] = _id
788 _id = self.engine.new_item(rollback, session, "nslcmops", indata, kwargs)
789 self._set_location_header(main_topic, version, "ns_lcm_op_occs", _id)
tierno65acb4d2018-04-06 16:42:40 +0200790 outdata = {"id": _id}
791 cherrypy.response.status = HTTPStatus.ACCEPTED.value
garciadeblas9750c5a2018-10-15 16:20:35 +0200792 elif topic == "netslice_instances_content":
Felipe Vicens07f31722018-10-29 15:16:44 +0100793 # creates NetSlice_Instance_record (NSIR)
garciadeblas9750c5a2018-10-15 16:20:35 +0200794 _id = self.engine.new_item(rollback, session, engine_topic, indata, kwargs, force=force)
Felipe Vicens07f31722018-10-29 15:16:44 +0100795 self._set_location_header(main_topic, version, topic, _id)
garciadeblas9750c5a2018-10-15 16:20:35 +0200796 indata["lcmOperationType"] = "instantiate"
797 indata["nsiInstanceId"] = _id
Felipe Vicens07f31722018-10-29 15:16:44 +0100798 self.engine.new_item(rollback, session, "nsilcmops", indata, kwargs)
garciadeblas9750c5a2018-10-15 16:20:35 +0200799 outdata = {"id": _id}
Felipe Vicens07f31722018-10-29 15:16:44 +0100800
garciadeblas9750c5a2018-10-15 16:20:35 +0200801 elif topic == "netslice_instances" and item:
802 indata["lcmOperationType"] = item
803 indata["nsiInstanceId"] = _id
804 _id = self.engine.new_item(rollback, session, "nsilcmops", indata, kwargs)
805 self._set_location_header(main_topic, version, "nsi_lcm_op_occs", _id)
806 outdata = {"id": _id}
807 cherrypy.response.status = HTTPStatus.ACCEPTED.value
tiernof27c79b2018-03-12 17:08:42 +0100808 else:
tiernob24258a2018-10-04 18:39:49 +0200809 _id = self.engine.new_item(rollback, session, engine_topic, indata, kwargs,
810 cherrypy.request.headers, force=force)
811 self._set_location_header(main_topic, version, topic, _id)
tiernof27c79b2018-03-12 17:08:42 +0100812 outdata = {"id": _id}
tiernob24258a2018-10-04 18:39:49 +0200813 # TODO form NsdInfo when topic in ("ns_descriptors", "vnf_packages")
tiernof27c79b2018-03-12 17:08:42 +0100814 cherrypy.response.status = HTTPStatus.CREATED.value
tierno09c073e2018-04-26 13:36:48 +0200815
tiernoc94c3df2018-02-09 15:38:54 +0100816 elif method == "DELETE":
817 if not _id:
tiernob24258a2018-10-04 18:39:49 +0200818 outdata = self.engine.del_item_list(session, engine_topic, kwargs)
tierno09c073e2018-04-26 13:36:48 +0200819 cherrypy.response.status = HTTPStatus.OK.value
tiernoc94c3df2018-02-09 15:38:54 +0100820 else: # len(args) > 1
tiernoe8631782018-12-21 13:31:52 +0000821 delete_in_process = False
tiernob24258a2018-10-04 18:39:49 +0200822 if topic == "ns_instances_content" and not force:
823 nslcmop_desc = {
824 "lcmOperationType": "terminate",
825 "nsInstanceId": _id,
826 "autoremove": True
827 }
828 opp_id = self.engine.new_item(rollback, session, "nslcmops", nslcmop_desc, None)
tiernoe8631782018-12-21 13:31:52 +0000829 if opp_id:
830 delete_in_process = True
831 outdata = {"_id": opp_id}
832 cherrypy.response.status = HTTPStatus.ACCEPTED.value
garciadeblas9750c5a2018-10-15 16:20:35 +0200833 elif topic == "netslice_instances_content" and not force:
834 nsilcmop_desc = {
835 "lcmOperationType": "terminate",
836 "nsiInstanceId": _id,
837 "autoremove": True
838 }
839 opp_id = self.engine.new_item(rollback, session, "nsilcmops", nsilcmop_desc, None)
tiernoe8631782018-12-21 13:31:52 +0000840 if opp_id:
841 delete_in_process = True
842 outdata = {"_id": opp_id}
843 cherrypy.response.status = HTTPStatus.ACCEPTED.value
844 if not delete_in_process:
tiernob24258a2018-10-04 18:39:49 +0200845 self.engine.del_item(session, engine_topic, _id, force)
tierno09c073e2018-04-26 13:36:48 +0200846 cherrypy.response.status = HTTPStatus.NO_CONTENT.value
tierno55ba2e62018-12-11 17:22:22 +0000847 if engine_topic in ("vim_accounts", "wim_accounts", "sdns"):
tierno09c073e2018-04-26 13:36:48 +0200848 cherrypy.response.status = HTTPStatus.ACCEPTED.value
849
tierno7ae10112018-05-18 14:36:02 +0200850 elif method in ("PUT", "PATCH"):
tiernocd54a4a2018-09-12 16:40:35 +0200851 outdata = None
tiernof27c79b2018-03-12 17:08:42 +0100852 if not indata and not kwargs:
tiernoc94c3df2018-02-09 15:38:54 +0100853 raise NbiException("Nothing to update. Provide payload and/or query string",
854 HTTPStatus.BAD_REQUEST)
garciadeblas9750c5a2018-10-15 16:20:35 +0200855 if item in ("nsd_content", "package_content", "nst_content") and method == "PUT":
tiernob24258a2018-10-04 18:39:49 +0200856 completed = self.engine.upload_content(session, engine_topic, _id, indata, kwargs,
857 cherrypy.request.headers, force=force)
tiernof27c79b2018-03-12 17:08:42 +0100858 if not completed:
859 cherrypy.response.headers["Transaction-Id"] = id
tiernof27c79b2018-03-12 17:08:42 +0100860 else:
tiernob24258a2018-10-04 18:39:49 +0200861 self.engine.edit_item(session, engine_topic, _id, indata, kwargs, force=force)
tiernocd54a4a2018-09-12 16:40:35 +0200862 cherrypy.response.status = HTTPStatus.NO_CONTENT.value
tiernoc94c3df2018-02-09 15:38:54 +0100863 else:
864 raise NbiException("Method {} not allowed".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
tiernof27c79b2018-03-12 17:08:42 +0100865 return self._format_out(outdata, session, _format)
tiernob24258a2018-10-04 18:39:49 +0200866 except Exception as e:
tierno36ec8602018-11-02 17:27:11 +0100867 if isinstance(e, (NbiException, EngineException, DbException, FsException, MsgException, AuthException,
868 ValidationError)):
tiernob24258a2018-10-04 18:39:49 +0200869 http_code_value = cherrypy.response.status = e.http_code.value
870 http_code_name = e.http_code.name
871 cherrypy.log("Exception {}".format(e))
872 else:
873 http_code_value = cherrypy.response.status = HTTPStatus.BAD_REQUEST.value # INTERNAL_SERVER_ERROR
Felipe Vicense36ab852018-11-23 14:12:09 +0100874 cherrypy.log("CRITICAL: Exception {}".format(e), traceback=True)
tiernob24258a2018-10-04 18:39:49 +0200875 http_code_name = HTTPStatus.BAD_REQUEST.name
tierno3ace63c2018-05-03 17:51:43 +0200876 if hasattr(outdata, "close"): # is an open file
877 outdata.close()
tiernob24258a2018-10-04 18:39:49 +0200878 error_text = str(e)
879 rollback.reverse()
tiernodb9dc582018-06-20 17:27:29 +0200880 for rollback_item in rollback:
tierno3ace63c2018-05-03 17:51:43 +0200881 try:
tiernocc103432018-10-19 14:10:35 +0200882 if rollback_item.get("operation") == "set":
883 self.engine.db.set_one(rollback_item["topic"], {"_id": rollback_item["_id"]},
884 rollback_item["content"], fail_on_empty=False)
885 else:
tiernoe8631782018-12-21 13:31:52 +0000886 self.engine.db.del_one(rollback_item["topic"], {"_id": rollback_item["_id"]},
887 fail_on_empty=False)
tierno3ace63c2018-05-03 17:51:43 +0200888 except Exception as e2:
tiernob24258a2018-10-04 18:39:49 +0200889 rollback_error_text = "Rollback Exception {}: {}".format(rollback_item, e2)
890 cherrypy.log(rollback_error_text)
891 error_text += ". " + rollback_error_text
892 # if isinstance(e, MsgException):
893 # error_text = "{} has been '{}' but other modules cannot be informed because an error on bus".format(
894 # engine_topic[:-1], method, error_text)
tiernoc94c3df2018-02-09 15:38:54 +0100895 problem_details = {
tiernob24258a2018-10-04 18:39:49 +0200896 "code": http_code_name,
897 "status": http_code_value,
898 "detail": error_text,
tiernoc94c3df2018-02-09 15:38:54 +0100899 }
900 return self._format_out(problem_details, session)
901 # raise cherrypy.HTTPError(e.http_code.value, str(e))
902
903
tiernoc94c3df2018-02-09 15:38:54 +0100904def _start_service():
905 """
906 Callback function called when cherrypy.engine starts
907 Override configuration with env variables
908 Set database, storage, message configuration
909 Init database with admin/admin user password
910 """
tierno932499c2019-01-28 17:28:10 +0000911 global nbi_server
912 global subscription_thread
tiernoc94c3df2018-02-09 15:38:54 +0100913 cherrypy.log.error("Starting osm_nbi")
914 # update general cherrypy configuration
915 update_dict = {}
916
917 engine_config = cherrypy.tree.apps['/osm'].config
918 for k, v in environ.items():
919 if not k.startswith("OSMNBI_"):
920 continue
tiernoe1281182018-05-22 12:24:36 +0200921 k1, _, k2 = k[7:].lower().partition("_")
tiernoc94c3df2018-02-09 15:38:54 +0100922 if not k2:
923 continue
924 try:
925 # update static configuration
926 if k == 'OSMNBI_STATIC_DIR':
927 engine_config["/static"]['tools.staticdir.dir'] = v
928 engine_config["/static"]['tools.staticdir.on'] = True
929 elif k == 'OSMNBI_SOCKET_PORT' or k == 'OSMNBI_SERVER_PORT':
930 update_dict['server.socket_port'] = int(v)
931 elif k == 'OSMNBI_SOCKET_HOST' or k == 'OSMNBI_SERVER_HOST':
932 update_dict['server.socket_host'] = v
tiernof5298be2018-05-16 14:43:57 +0200933 elif k1 in ("server", "test", "auth", "log"):
934 update_dict[k1 + '.' + k2] = v
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100935 elif k1 in ("message", "database", "storage", "authentication"):
tiernof5298be2018-05-16 14:43:57 +0200936 # k2 = k2.replace('_', '.')
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100937 if k2 in ("port", "db_port"):
tiernoc94c3df2018-02-09 15:38:54 +0100938 engine_config[k1][k2] = int(v)
939 else:
940 engine_config[k1][k2] = v
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100941
tiernoc94c3df2018-02-09 15:38:54 +0100942 except ValueError as e:
943 cherrypy.log.error("Ignoring environ '{}': " + str(e))
944 except Exception as e:
945 cherrypy.log.warn("skipping environ '{}' on exception '{}'".format(k, e))
946
947 if update_dict:
948 cherrypy.config.update(update_dict)
tiernof5298be2018-05-16 14:43:57 +0200949 engine_config["global"].update(update_dict)
tiernoc94c3df2018-02-09 15:38:54 +0100950
951 # logging cherrypy
952 log_format_simple = "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(message)s"
953 log_formatter_simple = logging.Formatter(log_format_simple, datefmt='%Y-%m-%dT%H:%M:%S')
954 logger_server = logging.getLogger("cherrypy.error")
955 logger_access = logging.getLogger("cherrypy.access")
956 logger_cherry = logging.getLogger("cherrypy")
957 logger_nbi = logging.getLogger("nbi")
958
tiernof5298be2018-05-16 14:43:57 +0200959 if "log.file" in engine_config["global"]:
960 file_handler = logging.handlers.RotatingFileHandler(engine_config["global"]["log.file"],
tiernoc94c3df2018-02-09 15:38:54 +0100961 maxBytes=100e6, backupCount=9, delay=0)
962 file_handler.setFormatter(log_formatter_simple)
963 logger_cherry.addHandler(file_handler)
964 logger_nbi.addHandler(file_handler)
tiernob24258a2018-10-04 18:39:49 +0200965 # log always to standard output
966 for format_, logger in {"nbi.server %(filename)s:%(lineno)s": logger_server,
967 "nbi.access %(filename)s:%(lineno)s": logger_access,
968 "%(name)s %(filename)s:%(lineno)s": logger_nbi
969 }.items():
970 log_format_cherry = "%(asctime)s %(levelname)s {} %(message)s".format(format_)
971 log_formatter_cherry = logging.Formatter(log_format_cherry, datefmt='%Y-%m-%dT%H:%M:%S')
972 str_handler = logging.StreamHandler()
973 str_handler.setFormatter(log_formatter_cherry)
974 logger.addHandler(str_handler)
tiernoc94c3df2018-02-09 15:38:54 +0100975
tiernof5298be2018-05-16 14:43:57 +0200976 if engine_config["global"].get("log.level"):
977 logger_cherry.setLevel(engine_config["global"]["log.level"])
978 logger_nbi.setLevel(engine_config["global"]["log.level"])
tiernoc94c3df2018-02-09 15:38:54 +0100979
980 # logging other modules
981 for k1, logname in {"message": "nbi.msg", "database": "nbi.db", "storage": "nbi.fs"}.items():
982 engine_config[k1]["logger_name"] = logname
983 logger_module = logging.getLogger(logname)
984 if "logfile" in engine_config[k1]:
985 file_handler = logging.handlers.RotatingFileHandler(engine_config[k1]["logfile"],
tierno2236d202018-05-16 19:05:16 +0200986 maxBytes=100e6, backupCount=9, delay=0)
tiernoc94c3df2018-02-09 15:38:54 +0100987 file_handler.setFormatter(log_formatter_simple)
988 logger_module.addHandler(file_handler)
989 if "loglevel" in engine_config[k1]:
990 logger_module.setLevel(engine_config[k1]["loglevel"])
991 # TODO add more entries, e.g.: storage
992 cherrypy.tree.apps['/osm'].root.engine.start(engine_config)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100993 cherrypy.tree.apps['/osm'].root.authenticator.start(engine_config)
tiernod985a8d2018-10-19 14:12:28 +0200994 cherrypy.tree.apps['/osm'].root.engine.init_db(target_version=database_version)
995 cherrypy.tree.apps['/osm'].root.authenticator.init_db(target_version=auth_database_version)
tiernobee508e2019-01-21 11:21:49 +0000996
tierno932499c2019-01-28 17:28:10 +0000997 # start subscriptions thread:
998 subscription_thread = SubscriptionThread(config=engine_config, engine=nbi_server.engine)
999 subscription_thread.start()
1000 # Do not capture except SubscriptionException
1001
tiernobee508e2019-01-21 11:21:49 +00001002 # load and print version. Ignore possible errors, e.g. file not found
1003 try:
1004 with open("{}/version".format(engine_config["/static"]['tools.staticdir.dir'])) as version_file:
1005 version_data = version_file.read()
1006 cherrypy.log.error("Starting OSM NBI Version: {}".format(version_data.replace("\n", " ")))
1007 except Exception:
1008 pass
tiernoc94c3df2018-02-09 15:38:54 +01001009
1010
1011def _stop_service():
1012 """
1013 Callback function called when cherrypy.engine stops
1014 TODO: Ending database connections.
1015 """
tierno932499c2019-01-28 17:28:10 +00001016 global subscription_thread
1017 subscription_thread.terminate()
1018 subscription_thread = None
tiernoc94c3df2018-02-09 15:38:54 +01001019 cherrypy.tree.apps['/osm'].root.engine.stop()
1020 cherrypy.log.error("Stopping osm_nbi")
1021
tierno2236d202018-05-16 19:05:16 +02001022
tiernof5298be2018-05-16 14:43:57 +02001023def nbi(config_file):
tierno932499c2019-01-28 17:28:10 +00001024 global nbi_server
tiernoc94c3df2018-02-09 15:38:54 +01001025 # conf = {
1026 # '/': {
1027 # #'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
1028 # 'tools.sessions.on': True,
1029 # 'tools.response_headers.on': True,
1030 # # 'tools.response_headers.headers': [('Content-Type', 'text/plain')],
1031 # }
1032 # }
1033 # cherrypy.Server.ssl_module = 'builtin'
1034 # cherrypy.Server.ssl_certificate = "http/cert.pem"
1035 # cherrypy.Server.ssl_private_key = "http/privkey.pem"
1036 # cherrypy.Server.thread_pool = 10
1037 # cherrypy.config.update({'Server.socket_port': config["port"], 'Server.socket_host': config["host"]})
1038
1039 # cherrypy.config.update({'tools.auth_basic.on': True,
1040 # 'tools.auth_basic.realm': 'localhost',
1041 # 'tools.auth_basic.checkpassword': validate_password})
tierno932499c2019-01-28 17:28:10 +00001042 nbi_server = Server()
tiernoc94c3df2018-02-09 15:38:54 +01001043 cherrypy.engine.subscribe('start', _start_service)
1044 cherrypy.engine.subscribe('stop', _stop_service)
tierno932499c2019-01-28 17:28:10 +00001045 cherrypy.quickstart(nbi_server, '/osm', config_file)
tiernof5298be2018-05-16 14:43:57 +02001046
1047
1048def usage():
1049 print("""Usage: {} [options]
1050 -c|--config [configuration_file]: loads the configuration file (default: ./nbi.cfg)
1051 -h|--help: shows this help
1052 """.format(sys.argv[0]))
tierno2236d202018-05-16 19:05:16 +02001053 # --log-socket-host HOST: send logs to this host")
1054 # --log-socket-port PORT: send logs using this port (default: 9022)")
tiernoc94c3df2018-02-09 15:38:54 +01001055
1056
1057if __name__ == '__main__':
tiernof5298be2018-05-16 14:43:57 +02001058 try:
1059 # load parameters and configuration
1060 opts, args = getopt.getopt(sys.argv[1:], "hvc:", ["config=", "help"])
1061 # TODO add "log-socket-host=", "log-socket-port=", "log-file="
1062 config_file = None
1063 for o, a in opts:
1064 if o in ("-h", "--help"):
1065 usage()
1066 sys.exit()
1067 elif o in ("-c", "--config"):
1068 config_file = a
1069 # elif o == "--log-socket-port":
1070 # log_socket_port = a
1071 # elif o == "--log-socket-host":
1072 # log_socket_host = a
1073 # elif o == "--log-file":
1074 # log_file = a
1075 else:
1076 assert False, "Unhandled option"
1077 if config_file:
1078 if not path.isfile(config_file):
1079 print("configuration file '{}' that not exist".format(config_file), file=sys.stderr)
1080 exit(1)
1081 else:
1082 for config_file in (__file__[:__file__.rfind(".")] + ".cfg", "./nbi.cfg", "/etc/osm/nbi.cfg"):
1083 if path.isfile(config_file):
1084 break
1085 else:
1086 print("No configuration file 'nbi.cfg' found neither at local folder nor at /etc/osm/", file=sys.stderr)
1087 exit(1)
1088 nbi(config_file)
1089 except getopt.GetoptError as e:
1090 print(str(e), file=sys.stderr)
1091 # usage()
1092 exit(1)