Implement feature 5949
[osm/RO.git] / openmanod
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 ##
5 # Copyright 2015 Telefónica Investigación y Desarrollo, S.A.U.
6 # 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
25 """
26 openmano server.
27 Main program that implements a reference NFVO (Network Functions Virtualisation Orchestrator).
28 It interfaces with an NFV VIM through its API and offers a northbound interface, based on REST (openmano API),
29 where NFV services are offered including the creation and deletion of VNF templates, VNF instances,
30 network service templates and network service instances.
31
32 It loads the configuration file and launches the http_server thread that will listen requests using openmano API.
33 """
34
35 import time
36 import sys
37 import getopt
38 import yaml
39 from os import getenv as os_getenv, path as os_path
40 from jsonschema import validate as js_v, exceptions as js_e
41 import logging
42 import logging.handlers as log_handlers
43 import socket
44 from osm_ro import httpserver, nfvo, nfvo_db
45 from osm_ro.openmano_schemas import config_schema
46 from osm_ro.db_base import db_base_Exception
47 from osm_ro.wim.engine import WimEngine
48 from osm_ro.wim.persistence import WimPersistence
49 import osm_ro
50
51 __author__ = "Alfonso Tierno, Gerardo Garcia, Pablo Montes"
52 __date__ = "$26-aug-2014 11:09:29$"
53 __version__ = "0.6.00"
54 version_date = "Sep 2018"
55 database_version = 34      # expected database schema version
56
57
58 global global_config
59 global logger
60
61
62 class LoadConfigurationException(Exception):
63     pass
64
65
66 def load_configuration(configuration_file):
67     default_tokens = {'http_port': 9090,
68                       'http_host': 'localhost',
69                       'http_console_proxy': True,
70                       'http_console_host': None,
71                       'log_level': 'DEBUG',
72                       'log_socket_port': 9022,
73                       'auto_push_VNF_to_VIMs': True,
74                       'db_host': 'localhost',
75                       'db_ovim_host': 'localhost'
76     }
77     try:
78         #Check config file exists
79         with open(configuration_file, 'r') as f:
80             config_str = f.read()
81         #Parse configuration file
82         config = yaml.load(config_str)
83         #Validate configuration file with the config_schema
84         js_v(config, config_schema)
85
86         #Add default values tokens
87         for k,v in default_tokens.items():
88             if k not in config:
89                 config[k]=v
90         return config
91
92     except yaml.YAMLError as e:
93         error_pos = ""
94         if hasattr(e, 'problem_mark'):
95             mark = e.problem_mark
96             error_pos = " at line:{} column:{}".format(mark.line+1, mark.column+1)
97         raise LoadConfigurationException("Bad YAML format at configuration file '{file}'{pos}: {message}".format(
98             file=configuration_file, pos=error_pos, message=e))
99     except js_e.ValidationError as e:
100         error_pos = ""
101         if e.path:
102             error_pos=" at '" + ":".join(map(str, e.path))+"'"
103         raise LoadConfigurationException("Invalid field at configuration file '{file}'{pos} {message}".format(
104             file=configuration_file, pos=error_pos, message=e))
105     except Exception as e:
106         raise LoadConfigurationException("Cannot load configuration file '{file}' {message}".format(
107             file=configuration_file, message=e))
108
109
110 def console_port_iterator():
111     '''this iterator deals with the http_console_ports
112     returning the ports one by one
113     '''
114     index = 0
115     while index < len(global_config["http_console_ports"]):
116         port = global_config["http_console_ports"][index]
117         #print("ports -> ", port)
118         if type(port) is int:
119             yield port
120         else: #this is dictionary with from to keys
121             port2 = port["from"]
122             #print("ports -> ", port, port2)
123             while port2 <= port["to"]:
124                 #print("ports -> ", port, port2)
125                 yield port2
126                 port2 += 1
127         index += 1
128
129
130 def usage():
131     print("Usage: ", sys.argv[0], "[options]")
132     print("      -v|--version: prints current version")
133     print("      -c|--config [configuration_file]: loads the configuration file (default: openmanod.cfg)")
134     print("      -h|--help: shows this help")
135     print("      -p|--port [port_number]: changes port number and overrides the port number in the configuration file (default: 9090)")
136     print("      -P|--adminport [port_number]: changes admin port number and overrides the port number in the configuration file (default: 9095)")
137     # print( "      -V|--vnf-repository: changes the path of the vnf-repository and overrides the path in the configuration file")
138     print("      --log-socket-host HOST: send logs to this host")
139     print("      --log-socket-port PORT: send logs using this port (default: 9022)")
140     print("      --log-file FILE: send logs to this file")
141     print("      --create-tenant NAME: Try to creates this tenant name before starting, ignoring any errors as e.g. conflict")
142     return
143
144
145 def set_logging_file(log_file):
146     try:
147         file_handler = logging.handlers.RotatingFileHandler(log_file, maxBytes=100e6, backupCount=9, delay=0)
148         file_handler.setFormatter(log_formatter_simple)
149         logger.addHandler(file_handler)
150         # logger.debug("moving logs to '%s'", global_config["log_file"])
151         # remove initial stream handler
152         logging.root.removeHandler(logging.root.handlers[0])
153         print ("logging on '{}'".format(log_file))
154     except IOError as e:
155         raise LoadConfigurationException(
156             "Cannot open logging file '{}': {}. Check folder exist and permissions".format(log_file, e))
157
158
159 if __name__=="__main__":
160     env_config = {
161         'db_host': 'RO_DB_HOST',
162         'db_name': 'RO_DB_NAME',
163         'db_user': 'RO_DB_USER',
164         'db_passwd': 'RO_DB_PASSWORD',
165         'db_ovim_host': 'RO_DB_OVIM_HOST',
166         'db_ovim_name': 'RO_DB_OVIM_NAME',
167         'db_ovim_user': 'RO_DB_OVIM_USER',
168         'db_ovim_passwd': 'RO_DB_OVIM_PASSWORD',
169         'db_port': 'RO_DB_PORT',
170         'db_port': 'RO_DB_PORT',
171     }
172     # Configure logging step 1
173     hostname = socket.gethostname()
174     # streamformat = "%(levelname)s (%(module)s:%(lineno)d) %(message)s"
175     # "%(asctime)s %(name)s %(levelname)s %(filename)s:%(lineno)d %(funcName)s %(process)d: %(message)s"
176     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')
180     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')
184     logger.setLevel(logging.DEBUG)
185     socket_handler = None
186     # Read parameters and configuration file
187     httpthread = None
188     try:
189         # 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="])
193         port=None
194         port_admin = None
195         config_file = 'osm_ro/openmanod.cfg'
196         vnf_repository = None
197         log_file = None
198         log_socket_host = None
199         log_socket_port = None
200         create_tenant = None
201
202         for o, a in opts:
203             if o in ("-v", "--version"):
204                 print ("openmanod version " + __version__ + ' ' + version_date)
205                 print ("(c) Copyright Telefonica")
206                 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
218             elif o == "--log-socket-port":
219                 log_socket_port = a
220             elif o == "--log-socket-host":
221                 log_socket_host = a
222             elif o == "--log-file":
223                 log_file = a
224             elif o == "--create-tenant":
225                 create_tenant = a
226             else:
227                 assert False, "Unhandled option"
228         if log_file:
229             set_logging_file(log_file)
230         global_config = load_configuration(config_file)
231         global_config["version"] = __version__
232         global_config["version_date"] = version_date
233         #print global_config
234         # Override parameters obtained by command line on ENV
235         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
243
244         # override with ENV
245         for config_key, env_var in env_config.items():
246             if os_getenv(env_var):
247                 global_config[config_key] = os_getenv(env_var)
248
249
250 #         if vnf_repository is not None:
251 #             global_config['vnf_repository'] = vnf_repository
252 #         else:
253 #             if not 'vnf_repository' in global_config:
254 #                 logger.error( os.getcwd() )
255 #                 global_config['vnf_repository'] = os.getcwd()+'/vnfrepo'
256 #         #print global_config
257 #         if not os.path.exists(global_config['vnf_repository']):
258 #             logger.error( "Creating folder vnf_repository folder: '%s'.", global_config['vnf_repository'])
259 #             try:
260 #                 os.makedirs(global_config['vnf_repository'])
261 #             except Exception as e:
262 #                 logger.error( "Error '%s'. Ensure the path 'vnf_repository' is properly set at %s",e.args[1], config_file)
263 #                 exit(-1)
264
265         global_config["console_port_iterator"] = console_port_iterator
266         global_config["console_thread"]={}
267         global_config["console_ports"]={}
268         if not global_config["http_console_host"]:
269             global_config["http_console_host"] = global_config["http_host"]
270             if global_config["http_host"]=="0.0.0.0":
271                 global_config["http_console_host"] = socket.gethostname()
272
273         # Configure logging STEP 2
274         if "log_host" in global_config:
275             socket_handler= log_handlers.SocketHandler(global_config["log_socket_host"], global_config["log_socket_port"])
276             socket_handler.setFormatter(log_formatter_complete)
277             if global_config.get("log_socket_level") and global_config["log_socket_level"] != global_config["log_level"]:
278                 socket_handler.setLevel(global_config["log_socket_level"])
279             logger.addHandler(socket_handler)
280
281         # logger.addHandler(log_handlers.SysLogHandler())
282         if log_file:
283             global_config['log_file'] = log_file
284         elif global_config.get('log_file'):
285             set_logging_file(global_config['log_file'])
286
287         # logging.basicConfig(level = getattr(logging, global_config.get('log_level',"debug")))
288         logger.setLevel(getattr(logging, global_config['log_level']))
289         logger.critical("Starting openmano server version: '%s %s' command: '%s'",
290                          __version__, version_date, " ".join(sys.argv))
291
292         for log_module in ("nfvo", "http", "vim", "wim", "db", "console", "ovim"):
293             log_level_module = "log_level_" + log_module
294             log_file_module = "log_file_" + log_module
295             logger_module = logging.getLogger('openmano.' + log_module)
296             if log_level_module in global_config:
297                 logger_module.setLevel(global_config[log_level_module])
298             if log_file_module in global_config:
299                 try:
300                     file_handler = logging.handlers.RotatingFileHandler(global_config[log_file_module],
301                                                                         maxBytes=100e6, backupCount=9, delay=0)
302                     file_handler.setFormatter(log_formatter_simple)
303                     logger_module.addHandler(file_handler)
304                 except IOError as e:
305                     raise LoadConfigurationException(
306                         "Cannot open logging file '{}': {}. Check folder exist and permissions".format(
307                             global_config[log_file_module], str(e)) )
308             global_config["logger_"+log_module] = logger_module
309         #httpserver.logger = global_config["logger_http"]
310         #nfvo.logger = global_config["logger_nfvo"]
311
312         # Initialize DB connection
313         mydb = nfvo_db.nfvo_db();
314         mydb.connect(global_config['db_host'], global_config['db_user'], global_config['db_passwd'], global_config['db_name'])
315         db_path = osm_ro.__path__[0] + "/database_utils"
316         if not os_path.exists(db_path + "/migrate_mano_db.sh"):
317             db_path = osm_ro.__path__[0] + "/../database_utils"
318         try:
319             r = mydb.get_db_version()
320             if r[0] != database_version:
321                 logger.critical("DATABASE wrong version '{current}'. Try to upgrade/downgrade to version '{target}'"
322                                 " with '{db_path}/migrate_mano_db.sh {target}'".format(
323                                 current=r[0], target=database_version,  db_path=db_path))
324                 exit(-1)
325         except db_base_Exception as e:
326             logger.critical("DATABASE is not valid. If you think it is corrupted, you can init it with"
327                             " '{db_path}/init_mano_db.sh' script".format(db_path=db_path))
328             exit(-1)
329
330         nfvo.global_config=global_config
331         if create_tenant:
332             try:
333                 nfvo.new_tenant(mydb, {"name": create_tenant})
334             except Exception as e:
335                 if isinstance(e, nfvo.NfvoException) and e.http_code == 409:
336                     pass  # if tenant exist (NfvoException error 409), ignore
337                 else:     # otherwise print and error and continue
338                     logger.error("Cannot create tenant '{}': {}".format(create_tenant, e))
339
340         # WIM module
341         wim_persistence = WimPersistence(mydb)
342         wim_engine = WimEngine(wim_persistence)
343         # ---
344         nfvo.start_service(mydb, wim_persistence, wim_engine)
345
346         httpthread = httpserver.httpserver(
347             mydb, False,
348             global_config['http_host'], global_config['http_port'],
349             wim_persistence, wim_engine
350         )
351
352         httpthread.start()
353         if 'http_admin_port' in global_config:
354             httpthreadadmin = httpserver.httpserver(mydb, True, global_config['http_host'], global_config['http_admin_port'])
355             httpthreadadmin.start()
356         time.sleep(1)
357         logger.info('Waiting for http clients')
358         print('Waiting for http clients')
359         print('openmanod ready')
360         print('====================')
361         time.sleep(20)
362         sys.stdout.flush()
363
364         #TODO: Interactive console must be implemented here instead of join or sleep
365
366         #httpthread.join()
367         #if 'http_admin_port' in global_config:
368         #    httpthreadadmin.join()
369         while True:
370             time.sleep(86400)
371
372     except KeyboardInterrupt as e:
373         logger.info(str(e))
374     except SystemExit:
375         pass
376     except getopt.GetoptError as e:
377         logger.critical(str(e)) # will print something like "option -a not recognized"
378         #usage()
379         exit(-1)
380     except LoadConfigurationException as e:
381         logger.critical(str(e))
382         exit(-1)
383     except db_base_Exception as e:
384         logger.critical(str(e))
385         exit(-1)
386     except nfvo.NfvoException as e:
387         logger.critical(str(e), exc_info=True)
388         exit(-1)
389     nfvo.stop_service()
390     if httpthread:
391         httpthread.join(1)
392