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