blob: f842acc7331f2236e5fe414ac28d93348d90377f [file] [log] [blame]
tierno7edb6752016-03-21 17:37:52 +01001#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3
4##
tierno92021022018-09-12 16:29:23 +02005# Copyright 2015 Telefonica Investigacion y Desarrollo, S.A.U.
tierno7edb6752016-03-21 17:37:52 +01006# This file is part of openmano
7# All Rights Reserved.
8#
9# Licensed under the Apache License, Version 2.0 (the "License"); you may
10# not use this file except in compliance with the License. You may obtain
11# a copy of the License at
12#
13# http://www.apache.org/licenses/LICENSE-2.0
14#
15# Unless required by applicable law or agreed to in writing, software
16# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
17# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
18# License for the specific language governing permissions and limitations
19# under the License.
20#
21# For those usages not covered by the Apache License, Version 2.0 please
22# contact with: nfvlabs@tid.es
23##
24
tierno99314902017-04-26 13:23:09 +020025"""
tierno7edb6752016-03-21 17:37:52 +010026openmano server.
27Main program that implements a reference NFVO (Network Functions Virtualisation Orchestrator).
28It interfaces with an NFV VIM through its API and offers a northbound interface, based on REST (openmano API),
29where NFV services are offered including the creation and deletion of VNF templates, VNF instances,
30network service templates and network service instances.
31
32It loads the configuration file and launches the http_server thread that will listen requests using openmano API.
tierno99314902017-04-26 13:23:09 +020033"""
tierno7edb6752016-03-21 17:37:52 +010034
tierno7edb6752016-03-21 17:37:52 +010035import time
tierno7edb6752016-03-21 17:37:52 +010036import sys
37import getopt
38import yaml
tierno92021022018-09-12 16:29:23 +020039from os import environ, path as os_path
tierno7edb6752016-03-21 17:37:52 +010040from jsonschema import validate as js_v, exceptions as js_e
tiernoae4a8d12016-07-08 12:30:39 +020041import logging
tiernof97fd272016-07-11 14:32:37 +020042import logging.handlers as log_handlers
tierno72f35a52016-07-15 13:18:30 +020043import socket
garciadeblas2c290ca2017-04-06 03:12:51 +020044from osm_ro import httpserver, nfvo, nfvo_db
45from osm_ro.openmano_schemas import config_schema
46from osm_ro.db_base import db_base_Exception
garciadeblas4b6216b2017-04-20 16:41:52 +020047import osm_ro
tierno7edb6752016-03-21 17:37:52 +010048
tierno11f81f62017-04-27 17:22:14 +020049__author__ = "Alfonso Tierno, Gerardo Garcia, Pablo Montes"
50__date__ = "$26-aug-2014 11:09:29$"
tierno20a608c2018-10-02 13:43:47 +020051__version__ = "0.5.79-r589"
52version_date = "Oct 2018"
tiernofc5f80b2018-05-29 16:00:43 +020053database_version = 32 # expected database schema version
gcalvinoe580c7d2017-09-22 14:09:51 +020054
tierno99314902017-04-26 13:23:09 +020055
tierno7edb6752016-03-21 17:37:52 +010056global global_config
tiernof97fd272016-07-11 14:32:37 +020057global logger
tiernoae4a8d12016-07-08 12:30:39 +020058
mirabal29356312017-07-27 12:21:22 +020059
tiernoae4a8d12016-07-08 12:30:39 +020060class LoadConfigurationException(Exception):
61 pass
tierno7edb6752016-03-21 17:37:52 +010062
tiernoad6bdd42018-01-10 10:43:46 +010063
tierno7edb6752016-03-21 17:37:52 +010064def load_configuration(configuration_file):
tiernoad6bdd42018-01-10 10:43:46 +010065 default_tokens = {'http_port': 9090,
66 'http_host': 'localhost',
tierno639520f2017-04-05 19:55:36 +020067 'http_console_proxy': True,
68 'http_console_host': None,
69 'log_level': 'DEBUG',
70 'log_socket_port': 9022,
71 'auto_push_VNF_to_VIMs': True,
72 'db_host': 'localhost',
73 'db_ovim_host': 'localhost'
74 }
tierno7edb6752016-03-21 17:37:52 +010075 try:
76 #Check config file exists
tierno72f35a52016-07-15 13:18:30 +020077 with open(configuration_file, 'r') as f:
78 config_str = f.read()
tierno7edb6752016-03-21 17:37:52 +010079 #Parse configuration file
tierno72f35a52016-07-15 13:18:30 +020080 config = yaml.load(config_str)
tierno7edb6752016-03-21 17:37:52 +010081 #Validate configuration file with the config_schema
tierno72f35a52016-07-15 13:18:30 +020082 js_v(config, config_schema)
tierno46df9672017-05-26 13:12:21 +020083
tierno72f35a52016-07-15 13:18:30 +020084 #Add default values tokens
tierno7edb6752016-03-21 17:37:52 +010085 for k,v in default_tokens.items():
tierno72f35a52016-07-15 13:18:30 +020086 if k not in config:
87 config[k]=v
88 return config
tierno46df9672017-05-26 13:12:21 +020089
tierno72f35a52016-07-15 13:18:30 +020090 except yaml.YAMLError as e:
91 error_pos = ""
92 if hasattr(e, 'problem_mark'):
93 mark = e.problem_mark
94 error_pos = " at line:{} column:{}".format(mark.line+1, mark.column+1)
tierno46df9672017-05-26 13:12:21 +020095 raise LoadConfigurationException("Bad YAML format at configuration file '{file}'{pos}: {message}".format(
96 file=configuration_file, pos=error_pos, message=e))
tierno72f35a52016-07-15 13:18:30 +020097 except js_e.ValidationError as e:
98 error_pos = ""
99 if e.path:
100 error_pos=" at '" + ":".join(map(str, e.path))+"'"
tierno46df9672017-05-26 13:12:21 +0200101 raise LoadConfigurationException("Invalid field at configuration file '{file}'{pos} {message}".format(
102 file=configuration_file, pos=error_pos, message=e))
tierno72f35a52016-07-15 13:18:30 +0200103 except Exception as e:
tierno46df9672017-05-26 13:12:21 +0200104 raise LoadConfigurationException("Cannot load configuration file '{file}' {message}".format(
105 file=configuration_file, message=e))
106
tierno7edb6752016-03-21 17:37:52 +0100107
108def console_port_iterator():
109 '''this iterator deals with the http_console_ports
110 returning the ports one by one
111 '''
112 index = 0
113 while index < len(global_config["http_console_ports"]):
114 port = global_config["http_console_ports"][index]
tiernoae4a8d12016-07-08 12:30:39 +0200115 #print("ports -> ", port)
tierno7edb6752016-03-21 17:37:52 +0100116 if type(port) is int:
117 yield port
118 else: #this is dictionary with from to keys
119 port2 = port["from"]
tiernoae4a8d12016-07-08 12:30:39 +0200120 #print("ports -> ", port, port2)
tierno7edb6752016-03-21 17:37:52 +0100121 while port2 <= port["to"]:
tiernoae4a8d12016-07-08 12:30:39 +0200122 #print("ports -> ", port, port2)
tierno7edb6752016-03-21 17:37:52 +0100123 yield port2
124 port2 += 1
125 index += 1
tierno46df9672017-05-26 13:12:21 +0200126
127
tierno7edb6752016-03-21 17:37:52 +0100128def usage():
tiernoae4a8d12016-07-08 12:30:39 +0200129 print("Usage: ", sys.argv[0], "[options]")
tiernod4d5e252018-01-23 17:07:46 +0100130 print(" -v|--version: prints current version")
131 print(" -c|--config [configuration_file]: loads the configuration file (default: openmanod.cfg)")
132 print(" -h|--help: shows this help")
133 print(" -p|--port [port_number]: changes port number and overrides the port number in the configuration file (default: 9090)")
134 print(" -P|--adminport [port_number]: changes admin port number and overrides the port number in the configuration file (default: 9095)")
135 # print( " -V|--vnf-repository: changes the path of the vnf-repository and overrides the path in the configuration file")
136 print(" --log-socket-host HOST: send logs to this host")
137 print(" --log-socket-port PORT: send logs using this port (default: 9022)")
138 print(" --log-file FILE: send logs to this file")
tierno9c5c8322018-03-23 15:44:03 +0100139 print(" --create-tenant NAME: Try to creates this tenant name before starting, ignoring any errors as e.g. conflict")
tierno7edb6752016-03-21 17:37:52 +0100140 return
tierno46df9672017-05-26 13:12:21 +0200141
142
143def set_logging_file(log_file):
144 try:
145 file_handler = logging.handlers.RotatingFileHandler(log_file, maxBytes=100e6, backupCount=9, delay=0)
146 file_handler.setFormatter(log_formatter_simple)
147 logger.addHandler(file_handler)
148 # logger.debug("moving logs to '%s'", global_config["log_file"])
149 # remove initial stream handler
150 logging.root.removeHandler(logging.root.handlers[0])
151 print ("logging on '{}'".format(log_file))
152 except IOError as e:
153 raise LoadConfigurationException(
154 "Cannot open logging file '{}': {}. Check folder exist and permissions".format(log_file, e))
155
156
tierno7edb6752016-03-21 17:37:52 +0100157if __name__=="__main__":
tierno92021022018-09-12 16:29:23 +0200158 # env2config contains envioron variable names and the correspondence with configuration file openmanod.cfg keys.
159 # If this environ is defined, this value is taken instead of the one at at configuration file
160 env2config = {
161 'RO_DB_HOST': 'db_host',
162 'RO_DB_NAME': 'db_name',
163 'RO_DB_USER': 'db_user',
164 'RO_DB_PASSWORD': 'db_passwd',
165 # 'RO_DB_PORT': 'db_port',
166 'RO_DB_OVIM_HOST': 'db_ovim_host',
167 'RO_DB_OVIM_NAME': 'db_ovim_name',
168 'RO_DB_OVIM_USER': 'db_ovim_user',
169 'RO_DB_OVIM_PASSWORD': 'db_ovim_passwd',
170 # 'RO_DB_OVIM_PORT': 'db_ovim_port',
tiernod4d5e252018-01-23 17:07:46 +0100171 }
tierno46df9672017-05-26 13:12:21 +0200172 # Configure logging step 1
tierno72f35a52016-07-15 13:18:30 +0200173 hostname = socket.gethostname()
tierno46df9672017-05-26 13:12:21 +0200174 # streamformat = "%(levelname)s (%(module)s:%(lineno)d) %(message)s"
tierno72f35a52016-07-15 13:18:30 +0200175 # "%(asctime)s %(name)s %(levelname)s %(filename)s:%(lineno)d %(funcName)s %(process)d: %(message)s"
tierno46df9672017-05-26 13:12:21 +0200176 log_formatter_complete = logging.Formatter('%(asctime)s.%(msecs)03d00Z[{host}@openmanod] %(filename)s:%(lineno)s '
177 'severity:%(levelname)s logger:%(name)s log:%(message)s'.format(
178 host=hostname),
179 datefmt='%Y-%m-%dT%H:%M:%S')
tierno72f35a52016-07-15 13:18:30 +0200180 log_format_simple = "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(message)s"
181 log_formatter_simple = logging.Formatter(log_format_simple, datefmt='%Y-%m-%dT%H:%M:%S')
182 logging.basicConfig(format=log_format_simple, level= logging.DEBUG)
183 logger = logging.getLogger('openmano')
tiernoae4a8d12016-07-08 12:30:39 +0200184 logger.setLevel(logging.DEBUG)
tierno72f35a52016-07-15 13:18:30 +0200185 socket_handler = None
tierno42026a02017-02-10 15:13:40 +0100186 # Read parameters and configuration file
187 httpthread = None
tierno7edb6752016-03-21 17:37:52 +0100188 try:
tierno9c5c8322018-03-23 15:44:03 +0100189 # load parameters and configuration
190 opts, args = getopt.getopt(sys.argv[1:], "hvc:V:p:P:",
191 ["config=", "help", "version", "port=", "vnf-repository=", "adminport=",
192 "log-socket-host=", "log-socket-port=", "log-file=", "create-tenant="])
tiernoae4a8d12016-07-08 12:30:39 +0200193 port=None
194 port_admin = None
garciadeblas4b6216b2017-04-20 16:41:52 +0200195 config_file = 'osm_ro/openmanod.cfg'
tiernoae4a8d12016-07-08 12:30:39 +0200196 vnf_repository = None
tierno205d1022016-07-21 11:26:22 +0200197 log_file = None
tierno72f35a52016-07-15 13:18:30 +0200198 log_socket_host = None
199 log_socket_port = None
tierno9c5c8322018-03-23 15:44:03 +0100200 create_tenant = None
tierno46df9672017-05-26 13:12:21 +0200201
tiernoae4a8d12016-07-08 12:30:39 +0200202 for o, a in opts:
203 if o in ("-v", "--version"):
tierno72f35a52016-07-15 13:18:30 +0200204 print ("openmanod version " + __version__ + ' ' + version_date)
205 print ("(c) Copyright Telefonica")
tiernoae4a8d12016-07-08 12:30:39 +0200206 sys.exit()
207 elif o in ("-h", "--help"):
208 usage()
209 sys.exit()
210 elif o in ("-V", "--vnf-repository"):
211 vnf_repository = a
212 elif o in ("-c", "--config"):
213 config_file = a
214 elif o in ("-p", "--port"):
215 port = a
216 elif o in ("-P", "--adminport"):
217 port_admin = a
tierno72f35a52016-07-15 13:18:30 +0200218 elif o == "--log-socket-port":
219 log_socket_port = a
tiernoa9a7e622016-09-30 08:43:32 +0000220 elif o == "--log-socket-host":
tierno72f35a52016-07-15 13:18:30 +0200221 log_socket_host = a
tierno205d1022016-07-21 11:26:22 +0200222 elif o == "--log-file":
223 log_file = a
tierno9c5c8322018-03-23 15:44:03 +0100224 elif o == "--create-tenant":
225 create_tenant = a
tiernoae4a8d12016-07-08 12:30:39 +0200226 else:
227 assert False, "Unhandled option"
tierno46df9672017-05-26 13:12:21 +0200228 if log_file:
229 set_logging_file(log_file)
tiernoae4a8d12016-07-08 12:30:39 +0200230 global_config = load_configuration(config_file)
tierno6ddeded2017-05-16 15:40:26 +0200231 global_config["version"] = __version__
232 global_config["version_date"] = version_date
tierno7edb6752016-03-21 17:37:52 +0100233 #print global_config
tiernod4d5e252018-01-23 17:07:46 +0100234 # Override parameters obtained by command line on ENV
tierno72f35a52016-07-15 13:18:30 +0200235 if port:
236 global_config['http_port'] = port
237 if port_admin:
238 global_config['http_admin_port'] = port_admin
239 if log_socket_host:
240 global_config['log_socket_host'] = log_socket_host
241 if log_socket_port:
242 global_config['log_socket_port'] = log_socket_port
tiernod4d5e252018-01-23 17:07:46 +0100243
244 # override with ENV
tierno92021022018-09-12 16:29:23 +0200245 for env_k, env_v in environ.items():
246 try:
247 if not env_k.startswith("RO_") or env_k not in env2config or not env_v:
248 continue
249 global_config[env2config[env_k]] = env_v
250 if env_k.endswith("PORT"): # convert to int, skip if not possible
251 global_config[env2config[env_k]] = int(env_v)
252 except Exception as e:
253 logger.warn("skipping environ '{}={}' because exception '{}'".format(env_k, env_v, e))
tiernod4d5e252018-01-23 17:07:46 +0100254
tierno72f35a52016-07-15 13:18:30 +0200255# if vnf_repository is not None:
256# global_config['vnf_repository'] = vnf_repository
257# else:
tierno46df9672017-05-26 13:12:21 +0200258# if not 'vnf_repository' in global_config:
tierno72f35a52016-07-15 13:18:30 +0200259# logger.error( os.getcwd() )
260# global_config['vnf_repository'] = os.getcwd()+'/vnfrepo'
261# #print global_config
262# if not os.path.exists(global_config['vnf_repository']):
263# logger.error( "Creating folder vnf_repository folder: '%s'.", global_config['vnf_repository'])
264# try:
265# os.makedirs(global_config['vnf_repository'])
266# except Exception as e:
267# logger.error( "Error '%s'. Ensure the path 'vnf_repository' is properly set at %s",e.args[1], config_file)
268# exit(-1)
tierno46df9672017-05-26 13:12:21 +0200269
tierno72f35a52016-07-15 13:18:30 +0200270 global_config["console_port_iterator"] = console_port_iterator
271 global_config["console_thread"]={}
272 global_config["console_ports"]={}
tierno20fc2a22016-08-19 17:02:35 +0200273 if not global_config["http_console_host"]:
274 global_config["http_console_host"] = global_config["http_host"]
275 if global_config["http_host"]=="0.0.0.0":
276 global_config["http_console_host"] = socket.gethostname()
tierno46df9672017-05-26 13:12:21 +0200277
278 # Configure logging STEP 2
tiernof97fd272016-07-11 14:32:37 +0200279 if "log_host" in global_config:
tierno72f35a52016-07-15 13:18:30 +0200280 socket_handler= log_handlers.SocketHandler(global_config["log_socket_host"], global_config["log_socket_port"])
281 socket_handler.setFormatter(log_formatter_complete)
tierno46df9672017-05-26 13:12:21 +0200282 if global_config.get("log_socket_level") and global_config["log_socket_level"] != global_config["log_level"]:
tierno72f35a52016-07-15 13:18:30 +0200283 socket_handler.setLevel(global_config["log_socket_level"])
tiernof97fd272016-07-11 14:32:37 +0200284 logger.addHandler(socket_handler)
tierno46df9672017-05-26 13:12:21 +0200285
286 # logger.addHandler(log_handlers.SysLogHandler())
287 if log_file:
288 global_config['log_file'] = log_file
289 elif global_config.get('log_file'):
290 set_logging_file(global_config['log_file'])
291
292 # logging.basicConfig(level = getattr(logging, global_config.get('log_level',"debug")))
tierno205d1022016-07-21 11:26:22 +0200293 logger.setLevel(getattr(logging, global_config['log_level']))
tierno46df9672017-05-26 13:12:21 +0200294 logger.critical("Starting openmano server version: '%s %s' command: '%s'",
tiernoaceb4642016-10-20 15:12:30 +0000295 __version__, version_date, " ".join(sys.argv))
tierno46df9672017-05-26 13:12:21 +0200296
tierno639520f2017-04-05 19:55:36 +0200297 for log_module in ("nfvo", "http", "vim", "db", "console", "ovim"):
tierno73ad9e42016-09-12 18:11:11 +0200298 log_level_module = "log_level_" + log_module
299 log_file_module = "log_file_" + log_module
300 logger_module = logging.getLogger('openmano.' + log_module)
301 if log_level_module in global_config:
302 logger_module.setLevel(global_config[log_level_module])
303 if log_file_module in global_config:
304 try:
tierno46df9672017-05-26 13:12:21 +0200305 file_handler = logging.handlers.RotatingFileHandler(global_config[log_file_module],
306 maxBytes=100e6, backupCount=9, delay=0)
tierno73ad9e42016-09-12 18:11:11 +0200307 file_handler.setFormatter(log_formatter_simple)
308 logger_module.addHandler(file_handler)
309 except IOError as e:
tierno46df9672017-05-26 13:12:21 +0200310 raise LoadConfigurationException(
311 "Cannot open logging file '{}': {}. Check folder exist and permissions".format(
312 global_config[log_file_module], str(e)) )
tierno73ad9e42016-09-12 18:11:11 +0200313 global_config["logger_"+log_module] = logger_module
314 #httpserver.logger = global_config["logger_http"]
315 #nfvo.logger = global_config["logger_nfvo"]
tierno46df9672017-05-26 13:12:21 +0200316
tierno7edb6752016-03-21 17:37:52 +0100317 # Initialize DB connection
tiernob13f3cc2016-09-26 10:14:44 +0200318 mydb = nfvo_db.nfvo_db();
tierno44528e42016-10-11 12:06:25 +0000319 mydb.connect(global_config['db_host'], global_config['db_user'], global_config['db_passwd'], global_config['db_name'])
tierno11f81f62017-04-27 17:22:14 +0200320 db_path = osm_ro.__path__[0] + "/database_utils"
tiernod4d5e252018-01-23 17:07:46 +0100321 if not os_path.exists(db_path + "/migrate_mano_db.sh"):
tierno11f81f62017-04-27 17:22:14 +0200322 db_path = osm_ro.__path__[0] + "/../database_utils"
tierno44528e42016-10-11 12:06:25 +0000323 try:
324 r = mydb.get_db_version()
tierno11f81f62017-04-27 17:22:14 +0200325 if r[0] != database_version:
326 logger.critical("DATABASE wrong version '{current}'. Try to upgrade/downgrade to version '{target}'"
327 " with '{db_path}/migrate_mano_db.sh {target}'".format(
328 current=r[0], target=database_version, db_path=db_path))
tierno44528e42016-10-11 12:06:25 +0000329 exit(-1)
330 except db_base_Exception as e:
tierno11f81f62017-04-27 17:22:14 +0200331 logger.critical("DATABASE is not valid. If you think it is corrupted, you can init it with"
332 " '{db_path}/init_mano_db.sh' script".format(db_path=db_path))
tierno7edb6752016-03-21 17:37:52 +0100333 exit(-1)
tierno44528e42016-10-11 12:06:25 +0000334
tierno7edb6752016-03-21 17:37:52 +0100335 nfvo.global_config=global_config
tierno9c5c8322018-03-23 15:44:03 +0100336 if create_tenant:
337 try:
338 nfvo.new_tenant(mydb, {"name": create_tenant})
339 except Exception as e:
340 if isinstance(e, nfvo.NfvoException) and e.http_code == 409:
341 pass # if tenant exist (NfvoException error 409), ignore
342 else: # otherwise print and error and continue
343 logger.error("Cannot create tenant '{}': {}".format(create_tenant, e))
tierno42026a02017-02-10 15:13:40 +0100344 nfvo.start_service(mydb)
tierno46df9672017-05-26 13:12:21 +0200345
tierno7edb6752016-03-21 17:37:52 +0100346 httpthread = httpserver.httpserver(mydb, False, global_config['http_host'], global_config['http_port'])
tierno46df9672017-05-26 13:12:21 +0200347
tierno7edb6752016-03-21 17:37:52 +0100348 httpthread.start()
tierno46df9672017-05-26 13:12:21 +0200349 if 'http_admin_port' in global_config:
tierno7edb6752016-03-21 17:37:52 +0100350 httpthreadadmin = httpserver.httpserver(mydb, True, global_config['http_host'], global_config['http_admin_port'])
351 httpthreadadmin.start()
tierno46df9672017-05-26 13:12:21 +0200352 time.sleep(1)
tiernoae4a8d12016-07-08 12:30:39 +0200353 logger.info('Waiting for http clients')
tierno205d1022016-07-21 11:26:22 +0200354 print('Waiting for http clients')
tiernoae4a8d12016-07-08 12:30:39 +0200355 print('openmanod ready')
356 print('====================')
tierno7edb6752016-03-21 17:37:52 +0100357 time.sleep(20)
358 sys.stdout.flush()
359
360 #TODO: Interactive console must be implemented here instead of join or sleep
361
362 #httpthread.join()
tierno42026a02017-02-10 15:13:40 +0100363 #if 'http_admin_port' in global_config:
tierno7edb6752016-03-21 17:37:52 +0100364 # httpthreadadmin.join()
365 while True:
366 time.sleep(86400)
tierno7edb6752016-03-21 17:37:52 +0100367
tierno72f35a52016-07-15 13:18:30 +0200368 except KeyboardInterrupt as e:
369 logger.info(str(e))
tierno809a7802016-07-08 13:31:24 +0200370 except SystemExit:
371 pass
tiernoae4a8d12016-07-08 12:30:39 +0200372 except getopt.GetoptError as e:
tierno72f35a52016-07-15 13:18:30 +0200373 logger.critical(str(e)) # will print something like "option -a not recognized"
tiernoae4a8d12016-07-08 12:30:39 +0200374 #usage()
375 exit(-1)
376 except LoadConfigurationException as e:
tierno72f35a52016-07-15 13:18:30 +0200377 logger.critical(str(e))
tiernoae4a8d12016-07-08 12:30:39 +0200378 exit(-1)
tierno44528e42016-10-11 12:06:25 +0000379 except db_base_Exception as e:
380 logger.critical(str(e))
381 exit(-1)
tierno46df9672017-05-26 13:12:21 +0200382 except nfvo.NfvoException as e:
383 logger.critical(str(e), exc_info=True)
384 exit(-1)
tierno42026a02017-02-10 15:13:40 +0100385 nfvo.stop_service()
386 if httpthread:
387 httpthread.join(1)
tierno7edb6752016-03-21 17:37:52 +0100388