blob: 4a5b0cc63939c55cdcf6c024521712663861806e [file] [log] [blame]
tiernoc94c3df2018-02-09 15:38:54 +01001#!/usr/bin/python3
2# -*- coding: utf-8 -*-
3
4import cherrypy
5import time
6import json
7import yaml
8import html_out as html
9import logging
tiernof5298be2018-05-16 14:43:57 +020010import logging.handlers
11import getopt
12import sys
Eduardo Sousa2f988212018-07-26 01:04:11 +010013
Eduardo Sousa819d34c2018-07-31 01:20:02 +010014from authconn import AuthException
Eduardo Sousa2f988212018-07-26 01:04:11 +010015from auth import Authenticator
tiernoc94c3df2018-02-09 15:38:54 +010016from engine import Engine, EngineException
tiernoa8d63632018-05-10 13:12:32 +020017from osm_common.dbbase import DbException
18from osm_common.fsbase import FsException
19from osm_common.msgbase import MsgException
tiernoc94c3df2018-02-09 15:38:54 +010020from http import HTTPStatus
tiernoc94c3df2018-02-09 15:38:54 +010021from codecs import getreader
tiernof5298be2018-05-16 14:43:57 +020022from os import environ, path
tiernoc94c3df2018-02-09 15:38:54 +010023
24__author__ = "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
tiernodfe09572018-04-24 10:41:10 +020025
26# TODO consider to remove and provide version using the static version file
27__version__ = "0.1.3"
tierno55945e72018-04-06 16:40:27 +020028version_date = "Apr 2018"
tierno4a946e42018-04-12 17:48:49 +020029database_version = '1.0'
Eduardo Sousa819d34c2018-07-31 01:20:02 +010030auth_database_version = '1.0'
tiernoc94c3df2018-02-09 15:38:54 +010031
32"""
tiernof27c79b2018-03-12 17:08:42 +010033North Bound Interface (O: OSM specific; 5,X: SOL005 not implemented yet; O5: SOL005 implemented)
tiernoc94c3df2018-02-09 15:38:54 +010034URL: /osm GET POST PUT DELETE PATCH
tiernof27c79b2018-03-12 17:08:42 +010035 /nsd/v1 O O
tierno2236d202018-05-16 19:05:16 +020036 /ns_descriptors_content O O
37 /<nsdInfoId> O O O O
tiernoc94c3df2018-02-09 15:38:54 +010038 /ns_descriptors O5 O5
39 /<nsdInfoId> O5 O5 5
40 /nsd_content O5 O5
tiernof27c79b2018-03-12 17:08:42 +010041 /nsd O
42 /artifacts[/<artifactPath>] O
tiernoc94c3df2018-02-09 15:38:54 +010043 /pnf_descriptors 5 5
44 /<pnfdInfoId> 5 5 5
45 /pnfd_content 5 5
tiernof27c79b2018-03-12 17:08:42 +010046 /subscriptions 5 5
47 /<subscriptionId> 5 X
tiernoc94c3df2018-02-09 15:38:54 +010048
49 /vnfpkgm/v1
tierno55945e72018-04-06 16:40:27 +020050 /vnf_packages_content O O
tierno2236d202018-05-16 19:05:16 +020051 /<vnfPkgId> O O
tiernoc94c3df2018-02-09 15:38:54 +010052 /vnf_packages O5 O5
53 /<vnfPkgId> O5 O5 5
tiernoc94c3df2018-02-09 15:38:54 +010054 /package_content O5 O5
55 /upload_from_uri X
tiernof27c79b2018-03-12 17:08:42 +010056 /vnfd O5
57 /artifacts[/<artifactPath>] O5
58 /subscriptions X X
59 /<subscriptionId> X X
tiernoc94c3df2018-02-09 15:38:54 +010060
61 /nslcm/v1
tiernof27c79b2018-03-12 17:08:42 +010062 /ns_instances_content O O
tierno2236d202018-05-16 19:05:16 +020063 /<nsInstanceId> O O
tiernof27c79b2018-03-12 17:08:42 +010064 /ns_instances 5 5
tierno95692442018-05-24 18:05:28 +020065 /<nsInstanceId> O5 O5
tierno65acb4d2018-04-06 16:42:40 +020066 instantiate O5
67 terminate O5
68 action O
69 scale O5
70 heal 5
tiernoc94c3df2018-02-09 15:38:54 +010071 /ns_lcm_op_occs 5 5
72 /<nsLcmOpOccId> 5 5 5
73 TO BE COMPLETED 5 5
tiernof759d822018-06-11 18:54:54 +020074 /vnf_instances (also vnfrs for compatibility) O
75 /<vnfInstanceId> O
tiernof27c79b2018-03-12 17:08:42 +010076 /subscriptions 5 5
77 /<subscriptionId> 5 X
tiernocb83c942018-09-24 17:28:13 +020078 /pdu/v1
79 /pdu_descriptor O O
80 /<id> O O O O
tiernof27c79b2018-03-12 17:08:42 +010081 /admin/v1
82 /tokens O O
tierno2236d202018-05-16 19:05:16 +020083 /<id> O O
tiernof27c79b2018-03-12 17:08:42 +010084 /users O O
tiernocd54a4a2018-09-12 16:40:35 +020085 /<id> O O O O
tiernof27c79b2018-03-12 17:08:42 +010086 /projects O O
tierno2236d202018-05-16 19:05:16 +020087 /<id> O O
tierno09c073e2018-04-26 13:36:48 +020088 /vims_accounts (also vims for compatibility) O O
tierno2236d202018-05-16 19:05:16 +020089 /<id> O O O
tierno0f98af52018-03-19 10:28:22 +010090 /sdns O O
tierno2236d202018-05-16 19:05:16 +020091 /<id> O O O
tiernoc94c3df2018-02-09 15:38:54 +010092
tierno2236d202018-05-16 19:05:16 +020093query string:
94 Follows SOL005 section 4.3.2 It contains extra METHOD to override http method, FORCE to force.
95 For filtering inside array, it must select the element of the array, or add ANYINDEX to apply the filtering over any
96 item of the array, that is, pass if any item of the array pass the filter.
97 It allows both ne and neq for not equal
98 TODO: 4.3.3 Attribute selectors
99 all_fields, fields=x,y,.., exclude_default, exclude_fields=x,y,...
tiernoc94c3df2018-02-09 15:38:54 +0100100 (none) … same as “exclude_default”
101 all_fields … all attributes.
tierno2236d202018-05-16 19:05:16 +0200102 fields=<list> … all attributes except all complex attributes with minimum cardinality of zero that are not
103 conditionally mandatory, and that are not provided in <list>.
104 exclude_fields=<list> … all attributes except those complex attributes with a minimum cardinality of zero that
105 are not conditionally mandatory, and that are provided in <list>.
106 exclude_default … all attributes except those complex attributes with a minimum cardinality of zero that are not
107 conditionally mandatory, and that are part of the "default exclude set" defined in the present specification for
108 the particular resource
109 exclude_default and include=<list> … all attributes except those complex attributes with a minimum cardinality
110 of zero that are not conditionally mandatory and that are part of the "default exclude set" defined in the
111 present specification for the particular resource, but that are not part of <list>
tiernoc94c3df2018-02-09 15:38:54 +0100112Header field name Reference Example Descriptions
113 Accept IETF RFC 7231 [19] application/json Content-Types that are acceptable for the response.
114 This header field shall be present if the response is expected to have a non-empty message body.
115 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the request.
116 This header field shall be present if the request has a non-empty message body.
tierno2236d202018-05-16 19:05:16 +0200117 Authorization IETF RFC 7235 [22] Bearer mF_9.B5f-4.1JqM The authorization token for the request.
118 Details are specified in clause 4.5.3.
tiernoc94c3df2018-02-09 15:38:54 +0100119 Range IETF RFC 7233 [21] 1000-2000 Requested range of bytes from a file
120Header field name Reference Example Descriptions
121 Content-Type IETF RFC 7231 [19] application/json The MIME type of the body of the response.
122 This header field shall be present if the response has a non-empty message body.
tierno2236d202018-05-16 19:05:16 +0200123 Location IETF RFC 7231 [19] http://www.example.com/vnflcm/v1/vnf_instances/123 Used in redirection, or when a
124 new resource has been created.
tiernoc94c3df2018-02-09 15:38:54 +0100125 This header field shall be present if the response status code is 201 or 3xx.
tierno2236d202018-05-16 19:05:16 +0200126 In the present document this header field is also used if the response status code is 202 and a new resource was
127 created.
128 WWW-Authenticate IETF RFC 7235 [22] Bearer realm="example" Challenge if the corresponding HTTP request has not
129 provided authorization, or error details if the corresponding HTTP request has provided an invalid authorization
130 token.
131 Accept-Ranges IETF RFC 7233 [21] bytes Used by the Server to signal whether or not it supports ranges for
132 certain resources.
133 Content-Range IETF RFC 7233 [21] bytes 21010-47021/ 47022 Signals the byte range that is contained in the
134 response, and the total length of the file.
tiernoc94c3df2018-02-09 15:38:54 +0100135 Retry-After IETF RFC 7231 [19] Fri, 31 Dec 1999 23:59:59 GMT
tiernoc94c3df2018-02-09 15:38:54 +0100136"""
137
138
139class NbiException(Exception):
140
141 def __init__(self, message, http_code=HTTPStatus.METHOD_NOT_ALLOWED):
142 Exception.__init__(self, message)
143 self.http_code = http_code
144
145
146class Server(object):
147 instance = 0
148 # to decode bytes to str
149 reader = getreader("utf-8")
150
151 def __init__(self):
152 self.instance += 1
153 self.engine = Engine()
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100154 self.authenticator = Authenticator()
tiernof27c79b2018-03-12 17:08:42 +0100155 self.valid_methods = { # contains allowed URL and methods
156 "admin": {
157 "v1": {
tierno0f98af52018-03-19 10:28:22 +0100158 "tokens": {"METHODS": ("GET", "POST", "DELETE"),
tierno2236d202018-05-16 19:05:16 +0200159 "<ID>": {"METHODS": ("GET", "DELETE")}
160 },
tierno0f98af52018-03-19 10:28:22 +0100161 "users": {"METHODS": ("GET", "POST"),
tiernocd54a4a2018-09-12 16:40:35 +0200162 "<ID>": {"METHODS": ("GET", "POST", "DELETE", "PATCH", "PUT")}
tierno2236d202018-05-16 19:05:16 +0200163 },
tierno0f98af52018-03-19 10:28:22 +0100164 "projects": {"METHODS": ("GET", "POST"),
tierno2236d202018-05-16 19:05:16 +0200165 "<ID>": {"METHODS": ("GET", "DELETE")}
166 },
tierno0f98af52018-03-19 10:28:22 +0100167 "vims": {"METHODS": ("GET", "POST"),
tierno7ae10112018-05-18 14:36:02 +0200168 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
tierno2236d202018-05-16 19:05:16 +0200169 },
tierno09c073e2018-04-26 13:36:48 +0200170 "vim_accounts": {"METHODS": ("GET", "POST"),
tierno7ae10112018-05-18 14:36:02 +0200171 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
tierno2236d202018-05-16 19:05:16 +0200172 },
tierno0f98af52018-03-19 10:28:22 +0100173 "sdns": {"METHODS": ("GET", "POST"),
tierno7ae10112018-05-18 14:36:02 +0200174 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
tierno2236d202018-05-16 19:05:16 +0200175 },
tiernof27c79b2018-03-12 17:08:42 +0100176 }
177 },
tiernocb83c942018-09-24 17:28:13 +0200178 "pdu": {
179 "v1": {
180 "pdu_descriptors": {"METHODS": ("GET", "POST"),
181 "<ID>": {"METHODS": ("GET", "POST", "DELETE", "PATCH", "PUT")}
182 },
183 }
184 },
tiernof27c79b2018-03-12 17:08:42 +0100185 "nsd": {
186 "v1": {
tierno2236d202018-05-16 19:05:16 +0200187 "ns_descriptors_content": {"METHODS": ("GET", "POST"),
188 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
189 },
190 "ns_descriptors": {"METHODS": ("GET", "POST"),
191 "<ID>": {"METHODS": ("GET", "DELETE"), "TODO": "PATCH",
192 "nsd_content": {"METHODS": ("GET", "PUT")},
193 "nsd": {"METHODS": "GET"}, # descriptor inside package
194 "artifacts": {"*": {"METHODS": "GET"}}
195 }
196 },
tiernof27c79b2018-03-12 17:08:42 +0100197 "pnf_descriptors": {"TODO": ("GET", "POST"),
tierno2236d202018-05-16 19:05:16 +0200198 "<ID>": {"TODO": ("GET", "DELETE", "PATCH"),
199 "pnfd_content": {"TODO": ("GET", "PUT")}
200 }
201 },
tiernof27c79b2018-03-12 17:08:42 +0100202 "subscriptions": {"TODO": ("GET", "POST"),
tierno2236d202018-05-16 19:05:16 +0200203 "<ID>": {"TODO": ("GET", "DELETE")}
204 },
tiernof27c79b2018-03-12 17:08:42 +0100205 }
206 },
207 "vnfpkgm": {
208 "v1": {
tierno2236d202018-05-16 19:05:16 +0200209 "vnf_packages_content": {"METHODS": ("GET", "POST"),
210 "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
211 },
212 "vnf_packages": {"METHODS": ("GET", "POST"),
tiernoaae4dc42018-06-11 18:55:50 +0200213 "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"), # GET: vnfPkgInfo
tierno2236d202018-05-16 19:05:16 +0200214 "package_content": {"METHODS": ("GET", "PUT"), # package
215 "upload_from_uri": {"TODO": "POST"}
216 },
217 "vnfd": {"METHODS": "GET"}, # descriptor inside package
218 "artifacts": {"*": {"METHODS": "GET"}}
219 }
220 },
tiernof27c79b2018-03-12 17:08:42 +0100221 "subscriptions": {"TODO": ("GET", "POST"),
tierno2236d202018-05-16 19:05:16 +0200222 "<ID>": {"TODO": ("GET", "DELETE")}
223 },
tiernof27c79b2018-03-12 17:08:42 +0100224 }
225 },
226 "nslcm": {
227 "v1": {
228 "ns_instances_content": {"METHODS": ("GET", "POST"),
tierno2236d202018-05-16 19:05:16 +0200229 "<ID>": {"METHODS": ("GET", "DELETE")}
230 },
tierno65acb4d2018-04-06 16:42:40 +0200231 "ns_instances": {"METHODS": ("GET", "POST"),
tierno95692442018-05-24 18:05:28 +0200232 "<ID>": {"METHODS": ("GET", "DELETE"),
tiernof759d822018-06-11 18:54:54 +0200233 "scale": {"METHODS": "POST"},
tierno2236d202018-05-16 19:05:16 +0200234 "terminate": {"METHODS": "POST"},
235 "instantiate": {"METHODS": "POST"},
236 "action": {"METHODS": "POST"},
237 }
238 },
tierno65acb4d2018-04-06 16:42:40 +0200239 "ns_lcm_op_occs": {"METHODS": "GET",
tierno2236d202018-05-16 19:05:16 +0200240 "<ID>": {"METHODS": "GET"},
241 },
tierno0ffaa992018-05-09 13:21:56 +0200242 "vnfrs": {"METHODS": ("GET"),
tierno2236d202018-05-16 19:05:16 +0200243 "<ID>": {"METHODS": ("GET")}
244 },
tiernof759d822018-06-11 18:54:54 +0200245 "vnf_instances": {"METHODS": ("GET"),
246 "<ID>": {"METHODS": ("GET")}
247 },
tiernof27c79b2018-03-12 17:08:42 +0100248 }
249 },
250 }
tiernoc94c3df2018-02-09 15:38:54 +0100251
tiernoc94c3df2018-02-09 15:38:54 +0100252 def _format_in(self, kwargs):
253 try:
254 indata = None
255 if cherrypy.request.body.length:
256 error_text = "Invalid input format "
257
258 if "Content-Type" in cherrypy.request.headers:
259 if "application/json" in cherrypy.request.headers["Content-Type"]:
260 error_text = "Invalid json format "
261 indata = json.load(self.reader(cherrypy.request.body))
262 elif "application/yaml" in cherrypy.request.headers["Content-Type"]:
263 error_text = "Invalid yaml format "
264 indata = yaml.load(cherrypy.request.body)
265 elif "application/binary" in cherrypy.request.headers["Content-Type"] or \
266 "application/gzip" in cherrypy.request.headers["Content-Type"] or \
tiernof27c79b2018-03-12 17:08:42 +0100267 "application/zip" in cherrypy.request.headers["Content-Type"] or \
268 "text/plain" in cherrypy.request.headers["Content-Type"]:
269 indata = cherrypy.request.body # .read()
tiernoc94c3df2018-02-09 15:38:54 +0100270 elif "multipart/form-data" in cherrypy.request.headers["Content-Type"]:
271 if "descriptor_file" in kwargs:
272 filecontent = kwargs.pop("descriptor_file")
273 if not filecontent.file:
274 raise NbiException("empty file or content", HTTPStatus.BAD_REQUEST)
tiernof27c79b2018-03-12 17:08:42 +0100275 indata = filecontent.file # .read()
tiernoc94c3df2018-02-09 15:38:54 +0100276 if filecontent.content_type.value:
277 cherrypy.request.headers["Content-Type"] = filecontent.content_type.value
278 else:
279 # raise cherrypy.HTTPError(HTTPStatus.Not_Acceptable,
280 # "Only 'Content-Type' of type 'application/json' or
281 # 'application/yaml' for input format are available")
282 error_text = "Invalid yaml format "
283 indata = yaml.load(cherrypy.request.body)
284 else:
285 error_text = "Invalid yaml format "
286 indata = yaml.load(cherrypy.request.body)
287 if not indata:
288 indata = {}
289
tiernoc94c3df2018-02-09 15:38:54 +0100290 format_yaml = False
291 if cherrypy.request.headers.get("Query-String-Format") == "yaml":
292 format_yaml = True
293
294 for k, v in kwargs.items():
295 if isinstance(v, str):
296 if v == "":
297 kwargs[k] = None
298 elif format_yaml:
299 try:
300 kwargs[k] = yaml.load(v)
tiernoe1281182018-05-22 12:24:36 +0200301 except Exception:
tiernoc94c3df2018-02-09 15:38:54 +0100302 pass
303 elif k.endswith(".gt") or k.endswith(".lt") or k.endswith(".gte") or k.endswith(".lte"):
304 try:
305 kwargs[k] = int(v)
tiernoe1281182018-05-22 12:24:36 +0200306 except Exception:
tiernoc94c3df2018-02-09 15:38:54 +0100307 try:
308 kwargs[k] = float(v)
tiernoe1281182018-05-22 12:24:36 +0200309 except Exception:
tiernoc94c3df2018-02-09 15:38:54 +0100310 pass
311 elif v.find(",") > 0:
312 kwargs[k] = v.split(",")
313 elif isinstance(v, (list, tuple)):
314 for index in range(0, len(v)):
315 if v[index] == "":
316 v[index] = None
317 elif format_yaml:
318 try:
319 v[index] = yaml.load(v[index])
tiernoe1281182018-05-22 12:24:36 +0200320 except Exception:
tiernoc94c3df2018-02-09 15:38:54 +0100321 pass
322
tiernof27c79b2018-03-12 17:08:42 +0100323 return indata
tiernoc94c3df2018-02-09 15:38:54 +0100324 except (ValueError, yaml.YAMLError) as exc:
325 raise NbiException(error_text + str(exc), HTTPStatus.BAD_REQUEST)
326 except KeyError as exc:
327 raise NbiException("Query string error: " + str(exc), HTTPStatus.BAD_REQUEST)
tiernob92094f2018-05-11 13:44:22 +0200328 except Exception as exc:
329 raise NbiException(error_text + str(exc), HTTPStatus.BAD_REQUEST)
tiernoc94c3df2018-02-09 15:38:54 +0100330
331 @staticmethod
tiernof27c79b2018-03-12 17:08:42 +0100332 def _format_out(data, session=None, _format=None):
tiernoc94c3df2018-02-09 15:38:54 +0100333 """
334 return string of dictionary data according to requested json, yaml, xml. By default json
tiernof27c79b2018-03-12 17:08:42 +0100335 :param data: response to be sent. Can be a dict, text or file
tiernoc94c3df2018-02-09 15:38:54 +0100336 :param session:
tiernof27c79b2018-03-12 17:08:42 +0100337 :param _format: The format to be set as Content-Type ir data is a file
tiernoc94c3df2018-02-09 15:38:54 +0100338 :return: None
339 """
tierno0f98af52018-03-19 10:28:22 +0100340 accept = cherrypy.request.headers.get("Accept")
tiernof27c79b2018-03-12 17:08:42 +0100341 if data is None:
tierno0f98af52018-03-19 10:28:22 +0100342 if accept and "text/html" in accept:
343 return html.format(data, cherrypy.request, cherrypy.response, session)
tierno09c073e2018-04-26 13:36:48 +0200344 # cherrypy.response.status = HTTPStatus.NO_CONTENT.value
tiernof27c79b2018-03-12 17:08:42 +0100345 return
346 elif hasattr(data, "read"): # file object
347 if _format:
348 cherrypy.response.headers["Content-Type"] = _format
349 elif "b" in data.mode: # binariy asssumig zip
350 cherrypy.response.headers["Content-Type"] = 'application/zip'
351 else:
352 cherrypy.response.headers["Content-Type"] = 'text/plain'
353 # TODO check that cherrypy close file. If not implement pending things to close per thread next
354 return data
tierno0f98af52018-03-19 10:28:22 +0100355 if accept:
tiernoc94c3df2018-02-09 15:38:54 +0100356 if "application/json" in accept:
357 cherrypy.response.headers["Content-Type"] = 'application/json; charset=utf-8'
358 a = json.dumps(data, indent=4) + "\n"
359 return a.encode("utf8")
360 elif "text/html" in accept:
361 return html.format(data, cherrypy.request, cherrypy.response, session)
362
tiernof27c79b2018-03-12 17:08:42 +0100363 elif "application/yaml" in accept or "*/*" in accept or "text/plain" in accept:
tiernoc94c3df2018-02-09 15:38:54 +0100364 pass
365 else:
366 raise cherrypy.HTTPError(HTTPStatus.NOT_ACCEPTABLE.value,
367 "Only 'Accept' of type 'application/json' or 'application/yaml' "
368 "for output format are available")
369 cherrypy.response.headers["Content-Type"] = 'application/yaml'
370 return yaml.safe_dump(data, explicit_start=True, indent=4, default_flow_style=False, tags=False,
371 encoding='utf-8', allow_unicode=True) # , canonical=True, default_style='"'
372
373 @cherrypy.expose
374 def index(self, *args, **kwargs):
375 session = None
376 try:
377 if cherrypy.request.method == "GET":
Eduardo Sousa2f988212018-07-26 01:04:11 +0100378 session = self.authenticator.authorize()
tiernoc94c3df2018-02-09 15:38:54 +0100379 outdata = "Index page"
380 else:
381 raise cherrypy.HTTPError(HTTPStatus.METHOD_NOT_ALLOWED.value,
tierno2236d202018-05-16 19:05:16 +0200382 "Method {} not allowed for tokens".format(cherrypy.request.method))
tiernoc94c3df2018-02-09 15:38:54 +0100383
384 return self._format_out(outdata, session)
385
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100386 except (EngineException, AuthException) as e:
tiernoc94c3df2018-02-09 15:38:54 +0100387 cherrypy.log("index Exception {}".format(e))
388 cherrypy.response.status = e.http_code.value
389 return self._format_out("Welcome to OSM!", session)
390
391 @cherrypy.expose
tierno55945e72018-04-06 16:40:27 +0200392 def version(self, *args, **kwargs):
tiernodfe09572018-04-24 10:41:10 +0200393 # TODO consider to remove and provide version using the static version file
tierno55945e72018-04-06 16:40:27 +0200394 global __version__, version_date
395 try:
396 if cherrypy.request.method != "GET":
397 raise NbiException("Only method GET is allowed", HTTPStatus.METHOD_NOT_ALLOWED)
398 elif args or kwargs:
399 raise NbiException("Invalid URL or query string for version", HTTPStatus.METHOD_NOT_ALLOWED)
400 return __version__ + " " + version_date
401 except NbiException as e:
402 cherrypy.response.status = e.http_code.value
403 problem_details = {
404 "code": e.http_code.name,
405 "status": e.http_code.value,
406 "detail": str(e),
407 }
408 return self._format_out(problem_details, None)
409
410 @cherrypy.expose
tiernof27c79b2018-03-12 17:08:42 +0100411 def token(self, method, token_id=None, kwargs=None):
tiernoc94c3df2018-02-09 15:38:54 +0100412 session = None
413 # self.engine.load_dbase(cherrypy.request.app.config)
tiernof27c79b2018-03-12 17:08:42 +0100414 indata = self._format_in(kwargs)
415 if not isinstance(indata, dict):
416 raise NbiException("Expected application/yaml or application/json Content-Type", HTTPStatus.BAD_REQUEST)
tiernoc94c3df2018-02-09 15:38:54 +0100417 try:
tiernoc94c3df2018-02-09 15:38:54 +0100418 if method == "GET":
Eduardo Sousa2f988212018-07-26 01:04:11 +0100419 session = self.authenticator.authorize()
tiernof27c79b2018-03-12 17:08:42 +0100420 if token_id:
Eduardo Sousa2f988212018-07-26 01:04:11 +0100421 outdata = self.authenticator.get_token(session, token_id)
tiernoc94c3df2018-02-09 15:38:54 +0100422 else:
Eduardo Sousa2f988212018-07-26 01:04:11 +0100423 outdata = self.authenticator.get_token_list(session)
tiernoc94c3df2018-02-09 15:38:54 +0100424 elif method == "POST":
425 try:
Eduardo Sousa2f988212018-07-26 01:04:11 +0100426 session = self.authenticator.authorize()
tiernoe1281182018-05-22 12:24:36 +0200427 except Exception:
tiernoc94c3df2018-02-09 15:38:54 +0100428 session = None
429 if kwargs:
430 indata.update(kwargs)
Eduardo Sousa2f988212018-07-26 01:04:11 +0100431 outdata = self.authenticator.new_token(session, indata, cherrypy.request.remote)
tiernoc94c3df2018-02-09 15:38:54 +0100432 session = outdata
433 cherrypy.session['Authorization'] = outdata["_id"]
tierno0f98af52018-03-19 10:28:22 +0100434 self._set_location_header("admin", "v1", "tokens", outdata["_id"])
tiernoc94c3df2018-02-09 15:38:54 +0100435 # cherrypy.response.cookie["Authorization"] = outdata["id"]
436 # cherrypy.response.cookie["Authorization"]['expires'] = 3600
437 elif method == "DELETE":
tiernof27c79b2018-03-12 17:08:42 +0100438 if not token_id and "id" in kwargs:
tiernoc94c3df2018-02-09 15:38:54 +0100439 token_id = kwargs["id"]
tiernof27c79b2018-03-12 17:08:42 +0100440 elif not token_id:
Eduardo Sousa2f988212018-07-26 01:04:11 +0100441 session = self.authenticator.authorize()
tiernoc94c3df2018-02-09 15:38:54 +0100442 token_id = session["_id"]
Eduardo Sousa2f988212018-07-26 01:04:11 +0100443 outdata = self.authenticator.del_token(token_id)
tiernoc94c3df2018-02-09 15:38:54 +0100444 session = None
445 cherrypy.session['Authorization'] = "logout"
446 # cherrypy.response.cookie["Authorization"] = token_id
447 # cherrypy.response.cookie["Authorization"]['expires'] = 0
448 else:
449 raise NbiException("Method {} not allowed for token".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
450 return self._format_out(outdata, session)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100451 except (NbiException, EngineException, DbException, AuthException) as e:
tiernoc94c3df2018-02-09 15:38:54 +0100452 cherrypy.log("tokens Exception {}".format(e))
453 cherrypy.response.status = e.http_code.value
454 problem_details = {
455 "code": e.http_code.name,
456 "status": e.http_code.value,
457 "detail": str(e),
458 }
459 return self._format_out(problem_details, session)
460
461 @cherrypy.expose
462 def test(self, *args, **kwargs):
463 thread_info = None
tiernof27c79b2018-03-12 17:08:42 +0100464 if args and args[0] == "help":
465 return "<html><pre>\ninit\nfile/<name> download file\ndb-clear/table\nprune\nlogin\nlogin2\n"\
tiernoe1281182018-05-22 12:24:36 +0200466 "sleep/<time>\nmessage/topic\n</pre></html>"
tiernof27c79b2018-03-12 17:08:42 +0100467
468 elif args and args[0] == "init":
tiernoc94c3df2018-02-09 15:38:54 +0100469 try:
470 # self.engine.load_dbase(cherrypy.request.app.config)
471 self.engine.create_admin()
472 return "Done. User 'admin', password 'admin' created"
473 except Exception:
474 cherrypy.response.status = HTTPStatus.FORBIDDEN.value
475 return self._format_out("Database already initialized")
tiernof27c79b2018-03-12 17:08:42 +0100476 elif args and args[0] == "file":
477 return cherrypy.lib.static.serve_file(cherrypy.tree.apps['/osm'].config["storage"]["path"] + "/" + args[1],
478 "text/plain", "attachment")
479 elif args and args[0] == "file2":
480 f_path = cherrypy.tree.apps['/osm'].config["storage"]["path"] + "/" + args[1]
481 f = open(f_path, "r")
482 cherrypy.response.headers["Content-type"] = "text/plain"
tiernof27c79b2018-03-12 17:08:42 +0100483 return f
tierno55945e72018-04-06 16:40:27 +0200484
tiernof27c79b2018-03-12 17:08:42 +0100485 elif len(args) == 2 and args[0] == "db-clear":
tiernob24258a2018-10-04 18:39:49 +0200486 return self.engine.db.del_list(args[1], kwargs)
tiernoc94c3df2018-02-09 15:38:54 +0100487 elif args and args[0] == "prune":
488 return self.engine.prune()
489 elif args and args[0] == "login":
490 if not cherrypy.request.headers.get("Authorization"):
491 cherrypy.response.headers["WWW-Authenticate"] = 'Basic realm="Access to OSM site", charset="UTF-8"'
492 cherrypy.response.status = HTTPStatus.UNAUTHORIZED.value
493 elif args and args[0] == "login2":
494 if not cherrypy.request.headers.get("Authorization"):
495 cherrypy.response.headers["WWW-Authenticate"] = 'Bearer realm="Access to OSM site"'
496 cherrypy.response.status = HTTPStatus.UNAUTHORIZED.value
497 elif args and args[0] == "sleep":
498 sleep_time = 5
499 try:
500 sleep_time = int(args[1])
501 except Exception:
502 cherrypy.response.status = HTTPStatus.FORBIDDEN.value
503 return self._format_out("Database already initialized")
504 thread_info = cherrypy.thread_data
505 print(thread_info)
506 time.sleep(sleep_time)
507 # thread_info
508 elif len(args) >= 2 and args[0] == "message":
tiernob24258a2018-10-04 18:39:49 +0200509 main_topic = args[1]
510 return_text = "<html><pre>{} ->\n".format(main_topic)
tiernoc94c3df2018-02-09 15:38:54 +0100511 try:
tierno55945e72018-04-06 16:40:27 +0200512 if cherrypy.request.method == 'POST':
513 to_send = yaml.load(cherrypy.request.body)
514 for k, v in to_send.items():
tiernob24258a2018-10-04 18:39:49 +0200515 self.engine.msg.write(main_topic, k, v)
tierno55945e72018-04-06 16:40:27 +0200516 return_text += " {}: {}\n".format(k, v)
517 elif cherrypy.request.method == 'GET':
518 for k, v in kwargs.items():
tiernob24258a2018-10-04 18:39:49 +0200519 self.engine.msg.write(main_topic, k, yaml.load(v))
tierno55945e72018-04-06 16:40:27 +0200520 return_text += " {}: {}\n".format(k, yaml.load(v))
tiernoc94c3df2018-02-09 15:38:54 +0100521 except Exception as e:
tierno55945e72018-04-06 16:40:27 +0200522 return_text += "Error: " + str(e)
523 return_text += "</pre></html>\n"
524 return return_text
tiernoc94c3df2018-02-09 15:38:54 +0100525
526 return_text = (
527 "<html><pre>\nheaders:\n args: {}\n".format(args) +
528 " kwargs: {}\n".format(kwargs) +
529 " headers: {}\n".format(cherrypy.request.headers) +
530 " path_info: {}\n".format(cherrypy.request.path_info) +
531 " query_string: {}\n".format(cherrypy.request.query_string) +
532 " session: {}\n".format(cherrypy.session) +
533 " cookie: {}\n".format(cherrypy.request.cookie) +
534 " method: {}\n".format(cherrypy.request.method) +
535 " session: {}\n".format(cherrypy.session.get('fieldname')) +
536 " body:\n")
537 return_text += " length: {}\n".format(cherrypy.request.body.length)
538 if cherrypy.request.body.length:
539 return_text += " content: {}\n".format(
540 str(cherrypy.request.body.read(int(cherrypy.request.headers.get('Content-Length', 0)))))
541 if thread_info:
542 return_text += "thread: {}\n".format(thread_info)
543 return_text += "</pre></html>"
544 return return_text
545
tiernof27c79b2018-03-12 17:08:42 +0100546 def _check_valid_url_method(self, method, *args):
547 if len(args) < 3:
tiernob24258a2018-10-04 18:39:49 +0200548 raise NbiException("URL must contain at least 'main_topic/version/topic'", HTTPStatus.METHOD_NOT_ALLOWED)
tiernof27c79b2018-03-12 17:08:42 +0100549
550 reference = self.valid_methods
551 for arg in args:
552 if arg is None:
553 break
554 if not isinstance(reference, dict):
555 raise NbiException("URL contains unexpected extra items '{}'".format(arg),
556 HTTPStatus.METHOD_NOT_ALLOWED)
557
558 if arg in reference:
559 reference = reference[arg]
560 elif "<ID>" in reference:
561 reference = reference["<ID>"]
562 elif "*" in reference:
563 reference = reference["*"]
564 break
565 else:
566 raise NbiException("Unexpected URL item {}".format(arg), HTTPStatus.METHOD_NOT_ALLOWED)
567 if "TODO" in reference and method in reference["TODO"]:
568 raise NbiException("Method {} not supported yet for this URL".format(method), HTTPStatus.NOT_IMPLEMENTED)
tierno2236d202018-05-16 19:05:16 +0200569 elif "METHODS" in reference and method not in reference["METHODS"]:
tiernof27c79b2018-03-12 17:08:42 +0100570 raise NbiException("Method {} not supported for this URL".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
571 return
572
573 @staticmethod
tiernob24258a2018-10-04 18:39:49 +0200574 def _set_location_header(main_topic, version, topic, id):
tiernof27c79b2018-03-12 17:08:42 +0100575 """
576 Insert response header Location with the URL of created item base on URL params
tiernob24258a2018-10-04 18:39:49 +0200577 :param main_topic:
tiernof27c79b2018-03-12 17:08:42 +0100578 :param version:
tiernob24258a2018-10-04 18:39:49 +0200579 :param topic:
tiernof27c79b2018-03-12 17:08:42 +0100580 :param id:
581 :return: None
582 """
583 # 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 +0200584 cherrypy.response.headers["Location"] = "/osm/{}/{}/{}/{}".format(main_topic, version, topic, id)
tiernof27c79b2018-03-12 17:08:42 +0100585 return
586
tiernoc94c3df2018-02-09 15:38:54 +0100587 @cherrypy.expose
tiernob24258a2018-10-04 18:39:49 +0200588 def default(self, main_topic=None, version=None, topic=None, _id=None, item=None, *args, **kwargs):
tiernoc94c3df2018-02-09 15:38:54 +0100589 session = None
tiernof27c79b2018-03-12 17:08:42 +0100590 outdata = None
591 _format = None
tierno0f98af52018-03-19 10:28:22 +0100592 method = "DONE"
tiernob24258a2018-10-04 18:39:49 +0200593 engine_topic = None
tiernodb9dc582018-06-20 17:27:29 +0200594 rollback = []
595 session = None
tiernoc94c3df2018-02-09 15:38:54 +0100596 try:
tiernob24258a2018-10-04 18:39:49 +0200597 if not main_topic or not version or not topic:
598 raise NbiException("URL must contain at least 'main_topic/version/topic'",
599 HTTPStatus.METHOD_NOT_ALLOWED)
600 if main_topic not in ("admin", "vnfpkgm", "nsd", "nslcm"):
601 raise NbiException("URL main_topic '{}' not supported".format(main_topic),
602 HTTPStatus.METHOD_NOT_ALLOWED)
tiernoc94c3df2018-02-09 15:38:54 +0100603 if version != 'v1':
604 raise NbiException("URL version '{}' not supported".format(version), HTTPStatus.METHOD_NOT_ALLOWED)
605
tiernof27c79b2018-03-12 17:08:42 +0100606 if kwargs and "METHOD" in kwargs and kwargs["METHOD"] in ("PUT", "POST", "DELETE", "GET", "PATCH"):
607 method = kwargs.pop("METHOD")
608 else:
609 method = cherrypy.request.method
tiernob92094f2018-05-11 13:44:22 +0200610 if kwargs and "FORCE" in kwargs:
611 force = kwargs.pop("FORCE")
612 else:
613 force = False
tiernof27c79b2018-03-12 17:08:42 +0100614
tiernob24258a2018-10-04 18:39:49 +0200615 self._check_valid_url_method(method, main_topic, version, topic, _id, item, *args)
tiernof27c79b2018-03-12 17:08:42 +0100616
tiernob24258a2018-10-04 18:39:49 +0200617 if main_topic == "admin" and topic == "tokens":
tiernof27c79b2018-03-12 17:08:42 +0100618 return self.token(method, _id, kwargs)
619
tiernoc94c3df2018-02-09 15:38:54 +0100620 # self.engine.load_dbase(cherrypy.request.app.config)
Eduardo Sousa2f988212018-07-26 01:04:11 +0100621 session = self.authenticator.authorize()
tiernof27c79b2018-03-12 17:08:42 +0100622 indata = self._format_in(kwargs)
tiernob24258a2018-10-04 18:39:49 +0200623 engine_topic = topic
624 if topic == "subscriptions":
625 engine_topic = main_topic + "_" + topic
626 if item:
627 engine_topic = item
tiernoc94c3df2018-02-09 15:38:54 +0100628
tiernob24258a2018-10-04 18:39:49 +0200629 if main_topic == "nsd":
630 engine_topic = "nsds"
631 elif main_topic == "vnfpkgm":
632 engine_topic = "vnfds"
633 elif main_topic == "nslcm":
634 engine_topic = "nsrs"
635 if topic == "ns_lcm_op_occs":
636 engine_topic = "nslcmops"
637 if topic == "vnfrs" or topic == "vnf_instances":
638 engine_topic = "vnfrs"
639 elif main_topic == "pdu":
640 engine_topic = "pdus"
641 if engine_topic == "vims": # TODO this is for backward compatibility, it will remove in the future
642 engine_topic = "vim_accounts"
tiernoc94c3df2018-02-09 15:38:54 +0100643
644 if method == "GET":
tiernob24258a2018-10-04 18:39:49 +0200645 if item in ("nsd_content", "package_content", "artifacts", "vnfd", "nsd"):
646 if item in ("vnfd", "nsd"):
tiernof27c79b2018-03-12 17:08:42 +0100647 path = "$DESCRIPTOR"
648 elif args:
649 path = args
tiernob24258a2018-10-04 18:39:49 +0200650 elif item == "artifacts":
tiernof27c79b2018-03-12 17:08:42 +0100651 path = ()
652 else:
653 path = None
tiernob24258a2018-10-04 18:39:49 +0200654 file, _format = self.engine.get_file(session, engine_topic, _id, path,
tierno2236d202018-05-16 19:05:16 +0200655 cherrypy.request.headers.get("Accept"))
tiernof27c79b2018-03-12 17:08:42 +0100656 outdata = file
657 elif not _id:
tiernob24258a2018-10-04 18:39:49 +0200658 outdata = self.engine.get_item_list(session, engine_topic, kwargs)
tiernoc94c3df2018-02-09 15:38:54 +0100659 else:
tiernob24258a2018-10-04 18:39:49 +0200660 outdata = self.engine.get_item(session, engine_topic, _id)
tiernof27c79b2018-03-12 17:08:42 +0100661 elif method == "POST":
tiernob24258a2018-10-04 18:39:49 +0200662 if topic in ("ns_descriptors_content", "vnf_packages_content"):
tiernof27c79b2018-03-12 17:08:42 +0100663 _id = cherrypy.request.headers.get("Transaction-Id")
664 if not _id:
tiernob24258a2018-10-04 18:39:49 +0200665 _id = self.engine.new_item(rollback, session, engine_topic, {}, None, cherrypy.request.headers,
tiernob92094f2018-05-11 13:44:22 +0200666 force=force)
tiernob24258a2018-10-04 18:39:49 +0200667 completed = self.engine.upload_content(session, engine_topic, _id, indata, kwargs,
668 cherrypy.request.headers, force=force)
tiernof27c79b2018-03-12 17:08:42 +0100669 if completed:
tiernob24258a2018-10-04 18:39:49 +0200670 self._set_location_header(main_topic, version, topic, _id)
tiernof27c79b2018-03-12 17:08:42 +0100671 else:
672 cherrypy.response.headers["Transaction-Id"] = _id
673 outdata = {"id": _id}
tiernob24258a2018-10-04 18:39:49 +0200674 elif topic == "ns_instances_content":
675 # creates NSR
676 _id = self.engine.new_item(rollback, session, engine_topic, indata, kwargs, force=force)
677 # creates nslcmop
678 indata["lcmOperationType"] = "instantiate"
679 indata["nsInstanceId"] = _id
680 self.engine.new_item(rollback, session, "nslcmops", indata, None)
681 self._set_location_header(main_topic, version, topic, _id)
tiernof27c79b2018-03-12 17:08:42 +0100682 outdata = {"id": _id}
tiernob24258a2018-10-04 18:39:49 +0200683 elif topic == "ns_instances" and item:
684 indata["lcmOperationType"] = item
685 indata["nsInstanceId"] = _id
686 _id = self.engine.new_item(rollback, session, "nslcmops", indata, kwargs)
687 self._set_location_header(main_topic, version, "ns_lcm_op_occs", _id)
tierno65acb4d2018-04-06 16:42:40 +0200688 outdata = {"id": _id}
689 cherrypy.response.status = HTTPStatus.ACCEPTED.value
tiernof27c79b2018-03-12 17:08:42 +0100690 else:
tiernob24258a2018-10-04 18:39:49 +0200691 _id = self.engine.new_item(rollback, session, engine_topic, indata, kwargs,
692 cherrypy.request.headers, force=force)
693 self._set_location_header(main_topic, version, topic, _id)
tiernof27c79b2018-03-12 17:08:42 +0100694 outdata = {"id": _id}
tiernob24258a2018-10-04 18:39:49 +0200695 # TODO form NsdInfo when topic in ("ns_descriptors", "vnf_packages")
tiernof27c79b2018-03-12 17:08:42 +0100696 cherrypy.response.status = HTTPStatus.CREATED.value
tierno09c073e2018-04-26 13:36:48 +0200697
tiernoc94c3df2018-02-09 15:38:54 +0100698 elif method == "DELETE":
699 if not _id:
tiernob24258a2018-10-04 18:39:49 +0200700 outdata = self.engine.del_item_list(session, engine_topic, kwargs)
tierno09c073e2018-04-26 13:36:48 +0200701 cherrypy.response.status = HTTPStatus.OK.value
tiernoc94c3df2018-02-09 15:38:54 +0100702 else: # len(args) > 1
tiernob24258a2018-10-04 18:39:49 +0200703 if topic == "ns_instances_content" and not force:
704 nslcmop_desc = {
705 "lcmOperationType": "terminate",
706 "nsInstanceId": _id,
707 "autoremove": True
708 }
709 opp_id = self.engine.new_item(rollback, session, "nslcmops", nslcmop_desc, None)
tierno3ace63c2018-05-03 17:51:43 +0200710 outdata = {"_id": opp_id}
tierno09c073e2018-04-26 13:36:48 +0200711 cherrypy.response.status = HTTPStatus.ACCEPTED.value
tierno65acb4d2018-04-06 16:42:40 +0200712 else:
tiernob24258a2018-10-04 18:39:49 +0200713 self.engine.del_item(session, engine_topic, _id, force)
tierno09c073e2018-04-26 13:36:48 +0200714 cherrypy.response.status = HTTPStatus.NO_CONTENT.value
tiernob24258a2018-10-04 18:39:49 +0200715 if engine_topic in ("vim_accounts", "sdns"):
tierno09c073e2018-04-26 13:36:48 +0200716 cherrypy.response.status = HTTPStatus.ACCEPTED.value
717
tierno7ae10112018-05-18 14:36:02 +0200718 elif method in ("PUT", "PATCH"):
tiernocd54a4a2018-09-12 16:40:35 +0200719 outdata = None
tiernof27c79b2018-03-12 17:08:42 +0100720 if not indata and not kwargs:
tiernoc94c3df2018-02-09 15:38:54 +0100721 raise NbiException("Nothing to update. Provide payload and/or query string",
722 HTTPStatus.BAD_REQUEST)
tiernob24258a2018-10-04 18:39:49 +0200723 if item in ("nsd_content", "package_content") and method == "PUT":
724 completed = self.engine.upload_content(session, engine_topic, _id, indata, kwargs,
725 cherrypy.request.headers, force=force)
tiernof27c79b2018-03-12 17:08:42 +0100726 if not completed:
727 cherrypy.response.headers["Transaction-Id"] = id
tiernof27c79b2018-03-12 17:08:42 +0100728 else:
tiernob24258a2018-10-04 18:39:49 +0200729 self.engine.edit_item(session, engine_topic, _id, indata, kwargs, force=force)
tiernocd54a4a2018-09-12 16:40:35 +0200730 cherrypy.response.status = HTTPStatus.NO_CONTENT.value
tiernoc94c3df2018-02-09 15:38:54 +0100731 else:
732 raise NbiException("Method {} not allowed".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
tiernof27c79b2018-03-12 17:08:42 +0100733 return self._format_out(outdata, session, _format)
tiernob24258a2018-10-04 18:39:49 +0200734 except Exception as e:
735 if isinstance(e, (NbiException, EngineException, DbException, FsException, MsgException, AuthException)):
736 http_code_value = cherrypy.response.status = e.http_code.value
737 http_code_name = e.http_code.name
738 cherrypy.log("Exception {}".format(e))
739 else:
740 http_code_value = cherrypy.response.status = HTTPStatus.BAD_REQUEST.value # INTERNAL_SERVER_ERROR
741 cherrypy.log("CRITICAL: Exception {}".format(e))
742 http_code_name = HTTPStatus.BAD_REQUEST.name
tierno3ace63c2018-05-03 17:51:43 +0200743 if hasattr(outdata, "close"): # is an open file
744 outdata.close()
tiernob24258a2018-10-04 18:39:49 +0200745 error_text = str(e)
746 rollback.reverse()
tiernodb9dc582018-06-20 17:27:29 +0200747 for rollback_item in rollback:
tierno3ace63c2018-05-03 17:51:43 +0200748 try:
tiernodb9dc582018-06-20 17:27:29 +0200749 self.engine.del_item(**rollback_item, session=session, force=True)
tierno3ace63c2018-05-03 17:51:43 +0200750 except Exception as e2:
tiernob24258a2018-10-04 18:39:49 +0200751 rollback_error_text = "Rollback Exception {}: {}".format(rollback_item, e2)
752 cherrypy.log(rollback_error_text)
753 error_text += ". " + rollback_error_text
754 # if isinstance(e, MsgException):
755 # error_text = "{} has been '{}' but other modules cannot be informed because an error on bus".format(
756 # engine_topic[:-1], method, error_text)
tiernoc94c3df2018-02-09 15:38:54 +0100757 problem_details = {
tiernob24258a2018-10-04 18:39:49 +0200758 "code": http_code_name,
759 "status": http_code_value,
760 "detail": error_text,
tiernoc94c3df2018-02-09 15:38:54 +0100761 }
762 return self._format_out(problem_details, session)
763 # raise cherrypy.HTTPError(e.http_code.value, str(e))
764
765
766# def validate_password(realm, username, password):
767# cherrypy.log("realm "+ str(realm))
768# if username == "admin" and password == "admin":
769# return True
770# return False
771
772
773def _start_service():
774 """
775 Callback function called when cherrypy.engine starts
776 Override configuration with env variables
777 Set database, storage, message configuration
778 Init database with admin/admin user password
779 """
780 cherrypy.log.error("Starting osm_nbi")
781 # update general cherrypy configuration
782 update_dict = {}
783
784 engine_config = cherrypy.tree.apps['/osm'].config
785 for k, v in environ.items():
786 if not k.startswith("OSMNBI_"):
787 continue
tiernoe1281182018-05-22 12:24:36 +0200788 k1, _, k2 = k[7:].lower().partition("_")
tiernoc94c3df2018-02-09 15:38:54 +0100789 if not k2:
790 continue
791 try:
792 # update static configuration
793 if k == 'OSMNBI_STATIC_DIR':
794 engine_config["/static"]['tools.staticdir.dir'] = v
795 engine_config["/static"]['tools.staticdir.on'] = True
796 elif k == 'OSMNBI_SOCKET_PORT' or k == 'OSMNBI_SERVER_PORT':
797 update_dict['server.socket_port'] = int(v)
798 elif k == 'OSMNBI_SOCKET_HOST' or k == 'OSMNBI_SERVER_HOST':
799 update_dict['server.socket_host'] = v
tiernof5298be2018-05-16 14:43:57 +0200800 elif k1 in ("server", "test", "auth", "log"):
801 update_dict[k1 + '.' + k2] = v
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100802 elif k1 in ("message", "database", "storage", "authentication"):
tiernof5298be2018-05-16 14:43:57 +0200803 # k2 = k2.replace('_', '.')
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100804 if k2 in ("port", "db_port"):
tiernoc94c3df2018-02-09 15:38:54 +0100805 engine_config[k1][k2] = int(v)
806 else:
807 engine_config[k1][k2] = v
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100808
tiernoc94c3df2018-02-09 15:38:54 +0100809 except ValueError as e:
810 cherrypy.log.error("Ignoring environ '{}': " + str(e))
811 except Exception as e:
812 cherrypy.log.warn("skipping environ '{}' on exception '{}'".format(k, e))
813
814 if update_dict:
815 cherrypy.config.update(update_dict)
tiernof5298be2018-05-16 14:43:57 +0200816 engine_config["global"].update(update_dict)
tiernoc94c3df2018-02-09 15:38:54 +0100817
818 # logging cherrypy
819 log_format_simple = "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(message)s"
820 log_formatter_simple = logging.Formatter(log_format_simple, datefmt='%Y-%m-%dT%H:%M:%S')
821 logger_server = logging.getLogger("cherrypy.error")
822 logger_access = logging.getLogger("cherrypy.access")
823 logger_cherry = logging.getLogger("cherrypy")
824 logger_nbi = logging.getLogger("nbi")
825
tiernof5298be2018-05-16 14:43:57 +0200826 if "log.file" in engine_config["global"]:
827 file_handler = logging.handlers.RotatingFileHandler(engine_config["global"]["log.file"],
tiernoc94c3df2018-02-09 15:38:54 +0100828 maxBytes=100e6, backupCount=9, delay=0)
829 file_handler.setFormatter(log_formatter_simple)
830 logger_cherry.addHandler(file_handler)
831 logger_nbi.addHandler(file_handler)
tiernob24258a2018-10-04 18:39:49 +0200832 # log always to standard output
833 for format_, logger in {"nbi.server %(filename)s:%(lineno)s": logger_server,
834 "nbi.access %(filename)s:%(lineno)s": logger_access,
835 "%(name)s %(filename)s:%(lineno)s": logger_nbi
836 }.items():
837 log_format_cherry = "%(asctime)s %(levelname)s {} %(message)s".format(format_)
838 log_formatter_cherry = logging.Formatter(log_format_cherry, datefmt='%Y-%m-%dT%H:%M:%S')
839 str_handler = logging.StreamHandler()
840 str_handler.setFormatter(log_formatter_cherry)
841 logger.addHandler(str_handler)
tiernoc94c3df2018-02-09 15:38:54 +0100842
tiernof5298be2018-05-16 14:43:57 +0200843 if engine_config["global"].get("log.level"):
844 logger_cherry.setLevel(engine_config["global"]["log.level"])
845 logger_nbi.setLevel(engine_config["global"]["log.level"])
tiernoc94c3df2018-02-09 15:38:54 +0100846
847 # logging other modules
848 for k1, logname in {"message": "nbi.msg", "database": "nbi.db", "storage": "nbi.fs"}.items():
849 engine_config[k1]["logger_name"] = logname
850 logger_module = logging.getLogger(logname)
851 if "logfile" in engine_config[k1]:
852 file_handler = logging.handlers.RotatingFileHandler(engine_config[k1]["logfile"],
tierno2236d202018-05-16 19:05:16 +0200853 maxBytes=100e6, backupCount=9, delay=0)
tiernoc94c3df2018-02-09 15:38:54 +0100854 file_handler.setFormatter(log_formatter_simple)
855 logger_module.addHandler(file_handler)
856 if "loglevel" in engine_config[k1]:
857 logger_module.setLevel(engine_config[k1]["loglevel"])
858 # TODO add more entries, e.g.: storage
859 cherrypy.tree.apps['/osm'].root.engine.start(engine_config)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100860 cherrypy.tree.apps['/osm'].root.authenticator.start(engine_config)
tiernod985a8d2018-10-19 14:12:28 +0200861 cherrypy.tree.apps['/osm'].root.engine.init_db(target_version=database_version)
862 cherrypy.tree.apps['/osm'].root.authenticator.init_db(target_version=auth_database_version)
tiernoc94c3df2018-02-09 15:38:54 +0100863 # getenv('OSMOPENMANO_TENANT', None)
864
865
866def _stop_service():
867 """
868 Callback function called when cherrypy.engine stops
869 TODO: Ending database connections.
870 """
871 cherrypy.tree.apps['/osm'].root.engine.stop()
872 cherrypy.log.error("Stopping osm_nbi")
873
tierno2236d202018-05-16 19:05:16 +0200874
tiernof5298be2018-05-16 14:43:57 +0200875def nbi(config_file):
tiernoc94c3df2018-02-09 15:38:54 +0100876 # conf = {
877 # '/': {
878 # #'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
879 # 'tools.sessions.on': True,
880 # 'tools.response_headers.on': True,
881 # # 'tools.response_headers.headers': [('Content-Type', 'text/plain')],
882 # }
883 # }
884 # cherrypy.Server.ssl_module = 'builtin'
885 # cherrypy.Server.ssl_certificate = "http/cert.pem"
886 # cherrypy.Server.ssl_private_key = "http/privkey.pem"
887 # cherrypy.Server.thread_pool = 10
888 # cherrypy.config.update({'Server.socket_port': config["port"], 'Server.socket_host': config["host"]})
889
890 # cherrypy.config.update({'tools.auth_basic.on': True,
891 # 'tools.auth_basic.realm': 'localhost',
892 # 'tools.auth_basic.checkpassword': validate_password})
893 cherrypy.engine.subscribe('start', _start_service)
894 cherrypy.engine.subscribe('stop', _stop_service)
tiernof5298be2018-05-16 14:43:57 +0200895 cherrypy.quickstart(Server(), '/osm', config_file)
896
897
898def usage():
899 print("""Usage: {} [options]
900 -c|--config [configuration_file]: loads the configuration file (default: ./nbi.cfg)
901 -h|--help: shows this help
902 """.format(sys.argv[0]))
tierno2236d202018-05-16 19:05:16 +0200903 # --log-socket-host HOST: send logs to this host")
904 # --log-socket-port PORT: send logs using this port (default: 9022)")
tiernoc94c3df2018-02-09 15:38:54 +0100905
906
907if __name__ == '__main__':
tiernof5298be2018-05-16 14:43:57 +0200908 try:
909 # load parameters and configuration
910 opts, args = getopt.getopt(sys.argv[1:], "hvc:", ["config=", "help"])
911 # TODO add "log-socket-host=", "log-socket-port=", "log-file="
912 config_file = None
913 for o, a in opts:
914 if o in ("-h", "--help"):
915 usage()
916 sys.exit()
917 elif o in ("-c", "--config"):
918 config_file = a
919 # elif o == "--log-socket-port":
920 # log_socket_port = a
921 # elif o == "--log-socket-host":
922 # log_socket_host = a
923 # elif o == "--log-file":
924 # log_file = a
925 else:
926 assert False, "Unhandled option"
927 if config_file:
928 if not path.isfile(config_file):
929 print("configuration file '{}' that not exist".format(config_file), file=sys.stderr)
930 exit(1)
931 else:
932 for config_file in (__file__[:__file__.rfind(".")] + ".cfg", "./nbi.cfg", "/etc/osm/nbi.cfg"):
933 if path.isfile(config_file):
934 break
935 else:
936 print("No configuration file 'nbi.cfg' found neither at local folder nor at /etc/osm/", file=sys.stderr)
937 exit(1)
938 nbi(config_file)
939 except getopt.GetoptError as e:
940 print(str(e), file=sys.stderr)
941 # usage()
942 exit(1)