cherry-pick change default logging to stdout and info level
[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 import osm_ro
48
49 __author__ = "Alfonso Tierno, Gerardo Garcia, Pablo Montes"
50 __date__ = "$26-aug-2014 11:09:29$"
51 __version__ = "0.5.75-r585"
52 version_date = "Oct 2018"
53 database_version = 32      # expected database schema version
54
55
56 global global_config
57 global logger
58
59
60 class LoadConfigurationException(Exception):
61     pass
62
63
64 def load_configuration(configuration_file):
65     default_tokens = {'http_port': 9090,
66                       'http_host': 'localhost',
67                       '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     }
75     try:
76         #Check config file exists
77         with open(configuration_file, 'r') as f:
78             config_str = f.read()
79         #Parse configuration file
80         config = yaml.load(config_str)
81         #Validate configuration file with the config_schema
82         js_v(config, config_schema)
83
84         #Add default values tokens
85         for k,v in default_tokens.items():
86             if k not in config:
87                 config[k]=v
88         return config
89
90     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)
95         raise LoadConfigurationException("Bad YAML format at configuration file '{file}'{pos}: {message}".format(
96             file=configuration_file, pos=error_pos, message=e))
97     except js_e.ValidationError as e:
98         error_pos = ""
99         if e.path:
100             error_pos=" at '" + ":".join(map(str, e.path))+"'"
101         raise LoadConfigurationException("Invalid field at configuration file '{file}'{pos} {message}".format(
102             file=configuration_file, pos=error_pos, message=e))
103     except Exception as e:
104         raise LoadConfigurationException("Cannot load configuration file '{file}' {message}".format(
105             file=configuration_file, message=e))
106
107
108 def 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]
115         #print("ports -> ", port)
116         if type(port) is int:
117             yield port
118         else: #this is dictionary with from to keys
119             port2 = port["from"]
120             #print("ports -> ", port, port2)
121             while port2 <= port["to"]:
122                 #print("ports -> ", port, port2)
123                 yield port2
124                 port2 += 1
125         index += 1
126
127
128 def usage():
129     print("Usage: ", sys.argv[0], "[options]")
130     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")
139     print("      --create-tenant NAME: Try to creates this tenant name before starting, ignoring any errors as e.g. conflict")
140     return
141
142
143 def 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
157 if __name__=="__main__":
158     env_config = {
159         'db_host': 'RO_DB_HOST',
160         'db_name': 'RO_DB_NAME',
161         'db_user': 'RO_DB_USER',
162         'db_passwd': 'RO_DB_PASSWORD',
163         'db_ovim_host': 'RO_DB_OVIM_HOST',
164         'db_ovim_name': 'RO_DB_OVIM_NAME',
165         'db_ovim_user': 'RO_DB_OVIM_USER',
166         'db_ovim_passwd': 'RO_DB_OVIM_PASSWORD',
167         'db_port': 'RO_DB_PORT',
168         'log_level': 'RO_LOG_LEVEL',
169         'log_file': 'RO_LOG_FILE',
170     }
171     # Configure logging step 1
172     hostname = socket.gethostname()
173     # streamformat = "%(levelname)s (%(module)s:%(lineno)d) %(message)s"
174     # "%(asctime)s %(name)s %(levelname)s %(filename)s:%(lineno)d %(funcName)s %(process)d: %(message)s"
175     log_formatter_complete = logging.Formatter('%(asctime)s.%(msecs)03d00Z[{host}@openmanod] %(filename)s:%(lineno)s '
176                                                'severity:%(levelname)s logger:%(name)s log:%(message)s'.format(
177                                                     host=hostname),
178                                                datefmt='%Y-%m-%dT%H:%M:%S')
179     log_format_simple =  "%(asctime)s %(levelname)s  %(name)s %(filename)s:%(lineno)s %(message)s"
180     log_formatter_simple = logging.Formatter(log_format_simple, datefmt='%Y-%m-%dT%H:%M:%S')
181     logging.basicConfig(format=log_format_simple, level= logging.DEBUG)
182     logger = logging.getLogger('openmano')
183     logger.setLevel(logging.DEBUG)
184     socket_handler = None
185     # Read parameters and configuration file
186     httpthread = None
187     try:
188         # load parameters and configuration
189         opts, args = getopt.getopt(sys.argv[1:], "hvc:V:p:P:",
190                                    ["config=", "help", "version", "port=", "vnf-repository=", "adminport=",
191                                     "log-socket-host=", "log-socket-port=", "log-file=", "create-tenant="])
192         port=None
193         port_admin = None
194         config_file = 'osm_ro/openmanod.cfg'
195         vnf_repository = None
196         log_file = None
197         log_socket_host = None
198         log_socket_port = None
199         create_tenant = None
200
201         for o, a in opts:
202             if o in ("-v", "--version"):
203                 print ("openmanod version " + __version__ + ' ' + version_date)
204                 print ("(c) Copyright Telefonica")
205                 sys.exit()
206             elif o in ("-h", "--help"):
207                 usage()
208                 sys.exit()
209             elif o in ("-V", "--vnf-repository"):
210                 vnf_repository = a
211             elif o in ("-c", "--config"):
212                 config_file = a
213             elif o in ("-p", "--port"):
214                 port = a
215             elif o in ("-P", "--adminport"):
216                 port_admin = a
217             elif o == "--log-socket-port":
218                 log_socket_port = a
219             elif o == "--log-socket-host":
220                 log_socket_host = a
221             elif o == "--log-file":
222                 log_file = a
223             elif o == "--create-tenant":
224                 create_tenant = a
225             else:
226                 assert False, "Unhandled option"
227         if log_file:
228             set_logging_file(log_file)
229         global_config = load_configuration(config_file)
230         global_config["version"] = __version__
231         global_config["version_date"] = version_date
232         #print global_config
233         # Override parameters obtained by command line on ENV
234         if port:
235             global_config['http_port'] = port
236         if port_admin:
237             global_config['http_admin_port'] = port_admin
238         if log_socket_host:
239             global_config['log_socket_host'] = log_socket_host
240         if log_socket_port:
241             global_config['log_socket_port'] = log_socket_port
242
243         # override with ENV
244         for config_key, env_var in env_config.items():
245             if os_getenv(env_var):
246                 global_config[config_key] = os_getenv(env_var)
247
248
249 #         if vnf_repository is not None:
250 #             global_config['vnf_repository'] = vnf_repository
251 #         else:
252 #             if not 'vnf_repository' in global_config:
253 #                 logger.error( os.getcwd() )
254 #                 global_config['vnf_repository'] = os.getcwd()+'/vnfrepo'
255 #         #print global_config
256 #         if not os.path.exists(global_config['vnf_repository']):
257 #             logger.error( "Creating folder vnf_repository folder: '%s'.", global_config['vnf_repository'])
258 #             try:
259 #                 os.makedirs(global_config['vnf_repository'])
260 #             except Exception as e:
261 #                 logger.error( "Error '%s'. Ensure the path 'vnf_repository' is properly set at %s",e.args[1], config_file)
262 #                 exit(-1)
263
264         global_config["console_port_iterator"] = console_port_iterator
265         global_config["console_thread"]={}
266         global_config["console_ports"]={}
267         if not global_config["http_console_host"]:
268             global_config["http_console_host"] = global_config["http_host"]
269             if global_config["http_host"]=="0.0.0.0":
270                 global_config["http_console_host"] = socket.gethostname()
271
272         # Configure logging STEP 2
273         if "log_host" in global_config:
274             socket_handler= log_handlers.SocketHandler(global_config["log_socket_host"], global_config["log_socket_port"])
275             socket_handler.setFormatter(log_formatter_complete)
276             if global_config.get("log_socket_level") and global_config["log_socket_level"] != global_config["log_level"]:
277                 socket_handler.setLevel(global_config["log_socket_level"])
278             logger.addHandler(socket_handler)
279
280         # logger.addHandler(log_handlers.SysLogHandler())
281         if log_file:
282             global_config['log_file'] = log_file
283         elif global_config.get('log_file'):
284             set_logging_file(global_config['log_file'])
285
286         # logging.basicConfig(level = getattr(logging, global_config.get('log_level',"debug")))
287         logger.setLevel(getattr(logging, global_config['log_level']))
288         logger.critical("Starting openmano server version: '%s %s' command: '%s'",
289                          __version__, version_date, " ".join(sys.argv))
290
291         for log_module in ("nfvo", "http", "vim", "db", "console", "ovim"):
292             log_level_module = "log_level_" + log_module
293             log_file_module = "log_file_" + log_module
294             logger_module = logging.getLogger('openmano.' + log_module)
295             if log_level_module in global_config:
296                 logger_module.setLevel(global_config[log_level_module])
297             if log_file_module in global_config:
298                 try:
299                     file_handler = logging.handlers.RotatingFileHandler(global_config[log_file_module],
300                                                                         maxBytes=100e6, backupCount=9, delay=0)
301                     file_handler.setFormatter(log_formatter_simple)
302                     logger_module.addHandler(file_handler)
303                 except IOError as e:
304                     raise LoadConfigurationException(
305                         "Cannot open logging file '{}': {}. Check folder exist and permissions".format(
306                             global_config[log_file_module], str(e)) )
307             global_config["logger_"+log_module] = logger_module
308         #httpserver.logger = global_config["logger_http"]
309         #nfvo.logger = global_config["logger_nfvo"]
310
311         # Initialize DB connection
312         mydb = nfvo_db.nfvo_db();
313         mydb.connect(global_config['db_host'], global_config['db_user'], global_config['db_passwd'], global_config['db_name'])
314         db_path = osm_ro.__path__[0] + "/database_utils"
315         if not os_path.exists(db_path + "/migrate_mano_db.sh"):
316             db_path = osm_ro.__path__[0] + "/../database_utils"
317         try:
318             r = mydb.get_db_version()
319             if r[0] != database_version:
320                 logger.critical("DATABASE wrong version '{current}'. Try to upgrade/downgrade to version '{target}'"
321                                 " with '{db_path}/migrate_mano_db.sh {target}'".format(
322                                 current=r[0], target=database_version,  db_path=db_path))
323                 exit(-1)
324         except db_base_Exception as e:
325             logger.critical("DATABASE is not valid. If you think it is corrupted, you can init it with"
326                             " '{db_path}/init_mano_db.sh' script".format(db_path=db_path))
327             exit(-1)
328
329         nfvo.global_config=global_config
330         if create_tenant:
331             try:
332                 nfvo.new_tenant(mydb, {"name": create_tenant})
333             except Exception as e:
334                 if isinstance(e, nfvo.NfvoException) and e.http_code == 409:
335                     pass  # if tenant exist (NfvoException error 409), ignore
336                 else:     # otherwise print and error and continue
337                     logger.error("Cannot create tenant '{}': {}".format(create_tenant, e))
338         nfvo.start_service(mydb)
339
340         httpthread = httpserver.httpserver(mydb, False, global_config['http_host'], global_config['http_port'])
341
342         httpthread.start()
343         if 'http_admin_port' in global_config:
344             httpthreadadmin = httpserver.httpserver(mydb, True, global_config['http_host'], global_config['http_admin_port'])
345             httpthreadadmin.start()
346         time.sleep(1)
347         logger.info('Waiting for http clients')
348         print('Waiting for http clients')
349         print('openmanod ready')
350         print('====================')
351         time.sleep(20)
352         sys.stdout.flush()
353
354         #TODO: Interactive console must be implemented here instead of join or sleep
355
356         #httpthread.join()
357         #if 'http_admin_port' in global_config:
358         #    httpthreadadmin.join()
359         while True:
360             time.sleep(86400)
361
362     except KeyboardInterrupt as e:
363         logger.info(str(e))
364     except SystemExit:
365         pass
366     except getopt.GetoptError as e:
367         logger.critical(str(e)) # will print something like "option -a not recognized"
368         #usage()
369         exit(-1)
370     except LoadConfigurationException as e:
371         logger.critical(str(e))
372         exit(-1)
373     except db_base_Exception as e:
374         logger.critical(str(e))
375         exit(-1)
376     except nfvo.NfvoException as e:
377         logger.critical(str(e), exc_info=True)
378         exit(-1)
379     nfvo.stop_service()
380     if httpthread:
381         httpthread.join(1)
382