Add thread id to RO log format
[osm/RO.git] / openmanod
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 ##
5 # Copyright 2015 Telefonica Investigacion 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 environ, 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.80-r590"
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     # 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',
171         'RO_LOG_LEVEL': 'log_level',
172         'RO_LOG_FILE': 'log_file',
173     }
174     # Configure logging step 1
175     hostname = socket.gethostname()
176     # streamformat = "%(levelname)s (%(module)s:%(lineno)d) %(message)s"
177     # "%(asctime)s %(name)s %(levelname)s %(filename)s:%(lineno)d %(funcName)s %(process)d: %(message)s"
178     log_formatter_complete = logging.Formatter('%(asctime)s.%(msecs)03d00Z[{host}@openmanod] %(filename)s:%(lineno)s '
179                                                'severity:%(levelname)s logger:%(name)s log:%(message)s'.format(
180                                                     host=hostname),
181                                                datefmt='%Y-%m-%dT%H:%M:%S')
182     log_format_simple =  "%(asctime)s %(levelname)s  %(name)s %(thread)d %(filename)s:%(lineno)s %(message)s"
183     log_formatter_simple = logging.Formatter(log_format_simple, datefmt='%Y-%m-%dT%H:%M:%S')
184     logging.basicConfig(format=log_format_simple, level= logging.DEBUG)
185     logger = logging.getLogger('openmano')
186     logger.setLevel(logging.DEBUG)
187     socket_handler = None
188     # Read parameters and configuration file
189     httpthread = None
190     try:
191         # load parameters and configuration
192         opts, args = getopt.getopt(sys.argv[1:], "hvc:V:p:P:",
193                                    ["config=", "help", "version", "port=", "vnf-repository=", "adminport=",
194                                     "log-socket-host=", "log-socket-port=", "log-file=", "create-tenant="])
195         port=None
196         port_admin = None
197         config_file = 'osm_ro/openmanod.cfg'
198         vnf_repository = None
199         log_file = None
200         log_socket_host = None
201         log_socket_port = None
202         create_tenant = None
203
204         for o, a in opts:
205             if o in ("-v", "--version"):
206                 print ("openmanod version " + __version__ + ' ' + version_date)
207                 print ("(c) Copyright Telefonica")
208                 sys.exit()
209             elif o in ("-h", "--help"):
210                 usage()
211                 sys.exit()
212             elif o in ("-V", "--vnf-repository"):
213                 vnf_repository = a
214             elif o in ("-c", "--config"):
215                 config_file = a
216             elif o in ("-p", "--port"):
217                 port = a
218             elif o in ("-P", "--adminport"):
219                 port_admin = a
220             elif o == "--log-socket-port":
221                 log_socket_port = a
222             elif o == "--log-socket-host":
223                 log_socket_host = a
224             elif o == "--log-file":
225                 log_file = a
226             elif o == "--create-tenant":
227                 create_tenant = a
228             else:
229                 assert False, "Unhandled option"
230         if log_file:
231             set_logging_file(log_file)
232         global_config = load_configuration(config_file)
233         global_config["version"] = __version__
234         global_config["version_date"] = version_date
235         #print global_config
236         # Override parameters obtained by command line on ENV
237         if port:
238             global_config['http_port'] = port
239         if port_admin:
240             global_config['http_admin_port'] = port_admin
241         if log_socket_host:
242             global_config['log_socket_host'] = log_socket_host
243         if log_socket_port:
244             global_config['log_socket_port'] = log_socket_port
245
246         # override with ENV
247         for env_k, env_v in environ.items():
248             try:
249                 if not env_k.startswith("RO_") or env_k not in env2config or not env_v:
250                     continue
251                 global_config[env2config[env_k]] = env_v
252                 if env_k.endswith("PORT"):    # convert to int, skip if not possible
253                     global_config[env2config[env_k]] = int(env_v)
254             except Exception as e:
255                 logger.warn("skipping environ '{}={}' because exception '{}'".format(env_k, env_v, e))
256
257 #         if vnf_repository is not None:
258 #             global_config['vnf_repository'] = vnf_repository
259 #         else:
260 #             if not 'vnf_repository' in global_config:
261 #                 logger.error( os.getcwd() )
262 #                 global_config['vnf_repository'] = os.getcwd()+'/vnfrepo'
263 #         #print global_config
264 #         if not os.path.exists(global_config['vnf_repository']):
265 #             logger.error( "Creating folder vnf_repository folder: '%s'.", global_config['vnf_repository'])
266 #             try:
267 #                 os.makedirs(global_config['vnf_repository'])
268 #             except Exception as e:
269 #                 logger.error( "Error '%s'. Ensure the path 'vnf_repository' is properly set at %s",e.args[1], config_file)
270 #                 exit(-1)
271
272         global_config["console_port_iterator"] = console_port_iterator
273         global_config["console_thread"]={}
274         global_config["console_ports"]={}
275         if not global_config["http_console_host"]:
276             global_config["http_console_host"] = global_config["http_host"]
277             if global_config["http_host"]=="0.0.0.0":
278                 global_config["http_console_host"] = socket.gethostname()
279
280         # Configure logging STEP 2
281         if "log_host" in global_config:
282             socket_handler= log_handlers.SocketHandler(global_config["log_socket_host"], global_config["log_socket_port"])
283             socket_handler.setFormatter(log_formatter_complete)
284             if global_config.get("log_socket_level") and global_config["log_socket_level"] != global_config["log_level"]:
285                 socket_handler.setLevel(global_config["log_socket_level"])
286             logger.addHandler(socket_handler)
287
288         # logger.addHandler(log_handlers.SysLogHandler())
289         if log_file:
290             global_config['log_file'] = log_file
291         elif global_config.get('log_file'):
292             set_logging_file(global_config['log_file'])
293
294         # logging.basicConfig(level = getattr(logging, global_config.get('log_level',"debug")))
295         logger.setLevel(getattr(logging, global_config['log_level']))
296         logger.critical("Starting openmano server version: '%s %s' command: '%s'",
297                          __version__, version_date, " ".join(sys.argv))
298
299         for log_module in ("nfvo", "http", "vim", "db", "console", "ovim"):
300             log_level_module = "log_level_" + log_module
301             log_file_module = "log_file_" + log_module
302             logger_module = logging.getLogger('openmano.' + log_module)
303             if log_level_module in global_config:
304                 logger_module.setLevel(global_config[log_level_module])
305             if log_file_module in global_config:
306                 try:
307                     file_handler = logging.handlers.RotatingFileHandler(global_config[log_file_module],
308                                                                         maxBytes=100e6, backupCount=9, delay=0)
309                     file_handler.setFormatter(log_formatter_simple)
310                     logger_module.addHandler(file_handler)
311                 except IOError as e:
312                     raise LoadConfigurationException(
313                         "Cannot open logging file '{}': {}. Check folder exist and permissions".format(
314                             global_config[log_file_module], str(e)) )
315             global_config["logger_"+log_module] = logger_module
316         #httpserver.logger = global_config["logger_http"]
317         #nfvo.logger = global_config["logger_nfvo"]
318
319         # Initialize DB connection
320         mydb = nfvo_db.nfvo_db();
321         mydb.connect(global_config['db_host'], global_config['db_user'], global_config['db_passwd'], global_config['db_name'])
322         db_path = osm_ro.__path__[0] + "/database_utils"
323         if not os_path.exists(db_path + "/migrate_mano_db.sh"):
324             db_path = osm_ro.__path__[0] + "/../database_utils"
325         try:
326             r = mydb.get_db_version()
327             if r[0] != database_version:
328                 logger.critical("DATABASE wrong version '{current}'. Try to upgrade/downgrade to version '{target}'"
329                                 " with '{db_path}/migrate_mano_db.sh {target}'".format(
330                                 current=r[0], target=database_version,  db_path=db_path))
331                 exit(-1)
332         except db_base_Exception as e:
333             logger.critical("DATABASE is not valid. If you think it is corrupted, you can init it with"
334                             " '{db_path}/init_mano_db.sh' script".format(db_path=db_path))
335             exit(-1)
336
337         nfvo.global_config=global_config
338         if create_tenant:
339             try:
340                 nfvo.new_tenant(mydb, {"name": create_tenant})
341             except Exception as e:
342                 if isinstance(e, nfvo.NfvoException) and e.http_code == 409:
343                     pass  # if tenant exist (NfvoException error 409), ignore
344                 else:     # otherwise print and error and continue
345                     logger.error("Cannot create tenant '{}': {}".format(create_tenant, e))
346         nfvo.start_service(mydb)
347
348         httpthread = httpserver.httpserver(mydb, False, global_config['http_host'], global_config['http_port'])
349
350         httpthread.start()
351         if 'http_admin_port' in global_config:
352             httpthreadadmin = httpserver.httpserver(mydb, True, global_config['http_host'], global_config['http_admin_port'])
353             httpthreadadmin.start()
354         time.sleep(1)
355         logger.info('Waiting for http clients')
356         print('Waiting for http clients')
357         print('openmanod ready')
358         print('====================')
359         time.sleep(20)
360         sys.stdout.flush()
361
362         #TODO: Interactive console must be implemented here instead of join or sleep
363
364         #httpthread.join()
365         #if 'http_admin_port' in global_config:
366         #    httpthreadadmin.join()
367         while True:
368             time.sleep(86400)
369
370     except KeyboardInterrupt as e:
371         logger.info(str(e))
372     except SystemExit:
373         pass
374     except getopt.GetoptError as e:
375         logger.critical(str(e)) # will print something like "option -a not recognized"
376         #usage()
377         exit(-1)
378     except LoadConfigurationException as e:
379         logger.critical(str(e))
380         exit(-1)
381     except db_base_Exception as e:
382         logger.critical(str(e))
383         exit(-1)
384     except nfvo.NfvoException as e:
385         logger.critical(str(e), exc_info=True)
386         exit(-1)
387     nfvo.stop_service()
388     if httpthread:
389         httpthread.join(1)
390