blob: 4ee6436f2277176bcfd98d72eb4863d9ad6eb49b [file] [log] [blame]
import io
import logging
import logging.handlers
import sys
import time
import traceback
from logging import Formatter
import datetime
import flask
import jinja2
from flask import current_app
from flask import g
from flask import session
from flask import request
from flask import jsonify
from flask import render_template
from flask_restful import Api
from sqlalchemy.ext.declarative import DeclarativeMeta
import lnt
import lnt.server.db.rules_manager
import lnt.server.db.v4db
import lnt.server.instance
import lnt.server.ui.filters
import lnt.server.ui.globals
import lnt.server.ui.profile_views
import lnt.server.ui.regression_views
import lnt.server.ui.views
from lnt.server.ui.api import load_api_resources
from lnt.util import logger
# The default name of the log file.
LOG_FILENAME = "lnt.log"
class RootSlashPatchMiddleware(object):
def __init__(self, app):
self.app = app
def __call__(self, environ, start_response):
if environ['PATH_INFO'] == '':
return flask.redirect(environ['SCRIPT_NAME'] + '/')(
environ, start_response)
return self.app(environ, start_response)
class LNTObjectJSONEncoder(flask.json.JSONEncoder):
"""Take SQLAlchemy objects and jsonify them. If the object has an __json__
method, use that instead."""
def __init__(self, *args, **kwargs):
super(LNTObjectJSONEncoder, self).__init__(*args, **kwargs)
def default(self, obj):
if hasattr(obj, '__json__'):
return obj.__json__()
if isinstance(obj, datetime.datetime):
return obj.isoformat()
if isinstance(obj.__class__, DeclarativeMeta):
fields = {}
for field in [x for x in dir(obj)
if not x.startswith('_') and x != 'metadata']:
data = obj.__getattribute__(field)
if isinstance(data, datetime.datetime):
fields[field] = data.isoformat()
else:
try:
flask.json.dumps(data)
fields[field] = data
except TypeError:
fields[field] = None
return fields
return flask.json.JSONEncoder.default(self, obj)
class Request(flask.Request):
def __init__(self, *args, **kwargs):
super(Request, self).__init__(*args, **kwargs)
self.request_time = time.time()
self.db = None
self.testsuite = None
def elapsed_time(self):
return time.time() - self.request_time
# Utility Methods
def get_db(self):
assert self.db is not None
# Enable SQL logging with db_log.
#
# FIXME: Conditionalize on an is_production variable.
show_sql = bool(self.args.get('db_log') or self.form.get('db_log'))
if show_sql:
g.db_log = io.StringIO()
logger = logging.getLogger("sqlalchemy")
logger.addHandler(logging.StreamHandler(g.db_log))
return self.db
def get_testsuite(self):
"""
get_testsuite() -> server.db.testsuite.TestSuite
Get the active testsuite.
"""
if self.testsuite is None:
testsuites = self.db.testsuite
if g.testsuite_name not in testsuites:
flask.abort(404)
self.testsuite = testsuites[g.testsuite_name]
return self.testsuite
def close(self):
t = self.elapsed_time()
if t > 10:
logger.warning("Request {} took {}s".format(self.url, t))
db = getattr(self, 'db', None)
if db is not None:
db.close()
return super(Request, self).close()
class LNTExceptionLoggerFlask(flask.Flask):
def log_exception(self, exc_info):
# We need to stringify the traceback, since logs are sent via
# pickle.
logger.error("Exception: " + traceback.format_exc())
class App(LNTExceptionLoggerFlask):
@staticmethod
def create_with_instance(instance):
# Construct the application.
app = App(__name__)
app.json_encoder = LNTObjectJSONEncoder
# Register additional filters.
create_jinja_environment(app.jinja_env)
# Set up strict undefined mode for templates.
app.jinja_env.undefined = jinja2.StrictUndefined
# Load the application configuration.
app.load_config(instance)
# Load the application routes.
app.register_blueprint(lnt.server.ui.views.frontend)
# Load the flaskRESTful API.
app.api = Api(app)
load_api_resources(app.api)
@app.before_request
def set_session():
"""Make our session cookies last."""
session.permanent = True
@app.errorhandler(404)
def page_not_found(e):
message = "{}: {}".format(e.name, e.description)
if request.accept_mimetypes.accept_json and \
not request.accept_mimetypes.accept_html:
response = jsonify({
'error': 'The page you are looking for does not exist.',
})
response.status_code = 404
return response
return render_template('error.html', message=message), 404
@app.errorhandler(500)
def internal_server_error(e):
if request.accept_mimetypes.accept_json and \
not request.accept_mimetypes.accept_html:
response = jsonify({
'error': 'internal server error',
'message': repr(e),
})
response.status_code = 500
return response
return render_template('error.html', message=repr(e)), 500
return app
@staticmethod
def create_standalone(config_path, log_file=None):
""" Create an instance of a lnt Flask application from a config file.
:param config_path: path to lnt config (directory or config file).
:param log_file: instead of setting up logging, use this log file.
when running in a multiprocess server like gunicorn, you need to use
gunicorn's logging instead (since it is multiprocess safe. In this case
LNT will print to to stderr and it can be collected by gunicorn. The
LNT logs page will show this unified log page.
:return: a LNT Flask App, ready to be loaded into a wsgi server.
"""
instance = lnt.server.instance.Instance.frompath(config_path)
app = App.create_with_instance(instance)
app.start_file_logging(log_file)
return app
def __init__(self, name):
super(App, self).__init__(name)
self.start_time = time.time()
# Override the request class.
self.request_class = Request
# Store a few global things we want available to templates.
self.version = lnt.__version__
# Inject a fix for missing slashes on the root URL (see Flask issue
# #169).
self.wsgi_app = RootSlashPatchMiddleware(self.wsgi_app)
self.logger.setLevel(logging.DEBUG)
def load_config(self, instance):
self.instance = instance
self.old_config = self.instance.config
self.jinja_env.globals.update(
app=current_app,
old_config=self.old_config)
# Set the application secret key.
self.secret_key = self.old_config.secretKey
lnt.server.db.rules_manager.register_hooks()
def start_file_logging(self, log_file_name):
"""Start server production logging. At this point flask already logs
to stderr, so just log to a file as well.
"""
# Always Print to screen.
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)
ch.setFormatter(Formatter('%(levelname)s: %(message)s '
'[in %(filename)s:%(lineno)d %(asctime)s]'))
self.logger.addHandler(ch)
# When running in a server config, use the server to setup the log
# file. If there is more than one process running, this will not work
# well.
if not log_file_name:
self.config['log_file_name'] = LOG_FILENAME
try:
rotating = logging.handlers.RotatingFileHandler(
LOG_FILENAME, maxBytes=1048576, backupCount=5)
rotating.setFormatter(Formatter(
'%(asctime)s %(levelname)s: %(message)s '
'[in %(filename)s:%(lineno)d]'
))
rotating.setLevel(logging.DEBUG)
self.logger.addHandler(rotating)
except (OSError, IOError) as e:
print("Error making log file",
LOG_FILENAME, str(e), file=sys.stderr)
print("Will not log to file.", file=sys.stderr)
else:
self.logger.info("Started file logging.")
print("Logging to :", LOG_FILENAME)
else:
self.config['log_file_name'] = log_file_name
def create_jinja_environment(env=None):
"""
create_jinja_environment([env]) -> jinja2.Environment
Create (or modify) a new Jinja2 environment suitable for rendering the LNT
templates.
"""
if env is None:
env = jinja2.Environment(loader=jinja2.PackageLoader(
'lnt.server.ui', 'templates'))
lnt.server.ui.globals.register(env)
lnt.server.ui.filters.register(env)
return env