Seed code for yarf
[ta/yarf.git] / src / yarf / app.py
1 # Copyright 2019 Nokia
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 #     http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14 #
15
16 import sys
17 import logging
18 import socket
19 from OpenSSL import SSL
20 from flask import Flask, request
21 from flask_restful import Api
22 from werkzeug.exceptions import InternalServerError
23 from yarf.handlers.pluginhandler import PluginLoader
24 from yarf.iniloader import ConfigError
25 import yarf.restfulargs as restfulconfig
26 import yarf.restfullogger as restlog
27 from yarf.helpers import remove_secrets
28
29 CRIT_RESP_LEN = 150000
30
31 app = Flask(__name__)
32 api = Api(app)
33 auth_method = None
34
35 def handle_excp(failure):
36     if isinstance(failure, socket.error):
37         app.logger.warning("Socket error, ignoring")
38         return
39     elif failure:
40         app.logger.error("Internal error: %s ", failure)
41     else:
42         app.logger.info("Failure not defined... Ignoring.")
43         return
44     raise InternalServerError()
45
46 def get_config(args, logger):
47     try:
48         config = restfulconfig.RestConfig()
49         if args:
50             config.parse(sys.argv[1:])
51         else:
52             config.parse()
53     except ConfigError as error:
54         logger.error("Failed to start %s" % error)
55         return None
56     return config
57
58 def request_logger():
59     app.logger.info('Request: remote_addr: %s method: %s endpoint: %s, user: %s', request.remote_addr,
60                     request.method, remove_secrets(request.full_path), get_username())
61
62 def response_logger(response):
63     app.logger.info('Response: status: %s (Associated Request: remote_addr: %s, method: %s, endpoint: %s, user: %s)',
64                     response.status, request.remote_addr, request.method,
65                     remove_secrets(request.full_path), get_username())
66
67     if len(response.data) > CRIT_RESP_LEN:
68         app.logger.debug('Response\'s data is too big, truncating!')
69         app.logger.debug('Response\'s truncated data: %s', response.data[:CRIT_RESP_LEN])
70     else:
71         app.logger.debug('Response\'s data: %s', response.data)
72
73     response.headers["Server"] = "Restapi"
74
75     return response
76
77 def get_username():
78     try:
79         return auth_method.get_authentication(request)[1]
80     except Exception as err: # pylint: disable=broad-except
81         app.logger.warn("Failed to get username from request returning empty. Err: %s", str(err))
82     return ''
83
84
85 def initialize(config, logger):
86     logger.info("Initializing...")
87     loglevel = logging.INFO if not config.get_debug() else logging.DEBUG
88     app.logger.setLevel(loglevel)
89     app.register_error_handler(Exception, handle_excp)
90     app.before_request(request_logger)
91     app.after_request(response_logger)
92     logger.error("%s", config.get_handler_dir())
93     p = PluginLoader(config.get_handler_dir(), api, config.get_auth_method())
94     auth_handler = p.get_auth_method()
95     handlers = p.get_modules()
96     for handler in handlers:
97         p.init_handler(handler)
98
99     for handler in restlog.get_log_handlers():
100         app.logger.addHandler(handler)
101     p.create_api_versionhandlers(handlers)
102     logger.info("Starting up...")
103
104
105 def get_wsgi_application():
106     logger = restlog.get_logger()
107     config = get_config(None, logger)
108     initialize(config, logger)
109     return app
110
111 def main():
112     logger = restlog.get_logger()
113     config = get_config(sys.argv[1:], logger)
114     if not config:
115         raise ConfigError("Failed to read config file")
116     initialize(config, logger)
117     run_params = {}
118     run_params["debug"] = config.get_debug()
119     run_params["port"] = config.get_port()
120     run_params["host"] = config.get_ip()
121     # When this https://github.com/pallets/werkzeug/issues/954 is fixed then the error handling
122     # can be done in the error handler of app level
123     passthrough_errors = config.get_passthrough_errors()
124     run_params["passthrough_errors"] = passthrough_errors
125     run_params["threaded"] = config.is_threaded()
126     logger.debug("%s %s %s", run_params["debug"], run_params["port"], run_params["threaded"])
127     if config.use_ssl():
128         context = SSL.Context(SSL.SSLv23_METHOD)
129         context.use_privatekey_file(config.get_private_key())
130         context.use_certificate_file(config.get_certificate())
131         run_params['ssl_context'] = context
132     while True:
133         try:
134             app.run(**run_params)
135         except Exception as err: # pylint: disable=broad-except
136             logger.warning("Caught exception but starting again %s", err)
137             if passthrough_errors:
138                 handle_excp(err)
139             else:
140                 raise err
141             logger.warning("Die in piece %s", err)
142             func = request.environ.get('werkzeug.server.shutdown')
143             if func is None:
144                raise RuntimeError('Not running with the Werkzeug Server')
145             func()
146
147     return 0
148
149 if __name__ == '__main__':
150     try:
151         sys.exit(main())
152     except Exception as error:# pylint: disable=broad-except
153         print "Failure: %s" % error
154         sys.exit(255)