fix bug 555 Change extended pci validation schema for sdn port mapping
[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.79-r589"
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     }
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 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))
254
255 #         if vnf_repository is not None:
256 #             global_config['vnf_repository'] = vnf_repository
257 #         else:
258 #             if not 'vnf_repository' in global_config:
259 #                 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)
269
270         global_config["console_port_iterator"] = console_port_iterator
271         global_config["console_thread"]={}
272         global_config["console_ports"]={}
273         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()
277
278         # Configure logging STEP 2
279         if "log_host" in global_config:
280             socket_handler= log_handlers.SocketHandler(global_config["log_socket_host"], global_config["log_socket_port"])
281             socket_handler.setFormatter(log_formatter_complete)
282             if global_config.get("log_socket_level") and global_config["log_socket_level"] != global_config["log_level"]:
283                 socket_handler.setLevel(global_config["log_socket_level"])
284             logger.addHandler(socket_handler)
285
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")))
293         logger.setLevel(getattr(logging, global_config['log_level']))
294         logger.critical("Starting openmano server version: '%s %s' command: '%s'",
295                          __version__, version_date, " ".join(sys.argv))
296
297         for log_module in ("nfvo", "http", "vim", "db", "console", "ovim"):
298             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:
305                     file_handler = logging.handlers.RotatingFileHandler(global_config[log_file_module],
306                                                                         maxBytes=100e6, backupCount=9, delay=0)
307                     file_handler.setFormatter(log_formatter_simple)
308                     logger_module.addHandler(file_handler)
309                 except IOError as e:
310                     raise LoadConfigurationException(
311                         "Cannot open logging file '{}': {}. Check folder exist and permissions".format(
312                             global_config[log_file_module], str(e)) )
313             global_config["logger_"+log_module] = logger_module
314         #httpserver.logger = global_config["logger_http"]
315         #nfvo.logger = global_config["logger_nfvo"]
316
317         # Initialize DB connection
318         mydb = nfvo_db.nfvo_db();
319         mydb.connect(global_config['db_host'], global_config['db_user'], global_config['db_passwd'], global_config['db_name'])
320         db_path = osm_ro.__path__[0] + "/database_utils"
321         if not os_path.exists(db_path + "/migrate_mano_db.sh"):
322             db_path = osm_ro.__path__[0] + "/../database_utils"
323         try:
324             r = mydb.get_db_version()
325             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))
329                 exit(-1)
330         except db_base_Exception as e:
331             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))
333             exit(-1)
334
335         nfvo.global_config=global_config
336         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))
344         nfvo.start_service(mydb)
345
346         httpthread = httpserver.httpserver(mydb, False, global_config['http_host'], global_config['http_port'])
347
348         httpthread.start()
349         if 'http_admin_port' in global_config:
350             httpthreadadmin = httpserver.httpserver(mydb, True, global_config['http_host'], global_config['http_admin_port'])
351             httpthreadadmin.start()
352         time.sleep(1)
353         logger.info('Waiting for http clients')
354         print('Waiting for http clients')
355         print('openmanod ready')
356         print('====================')
357         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()
363         #if 'http_admin_port' in global_config:
364         #    httpthreadadmin.join()
365         while True:
366             time.sleep(86400)
367
368     except KeyboardInterrupt as e:
369         logger.info(str(e))
370     except SystemExit:
371         pass
372     except getopt.GetoptError as e:
373         logger.critical(str(e)) # will print something like "option -a not recognized"
374         #usage()
375         exit(-1)
376     except LoadConfigurationException as e:
377         logger.critical(str(e))
378         exit(-1)
379     except db_base_Exception as e:
380         logger.critical(str(e))
381         exit(-1)
382     except nfvo.NfvoException as e:
383         logger.critical(str(e), exc_info=True)
384         exit(-1)
385     nfvo.stop_service()
386     if httpthread:
387         httpthread.join(1)
388