1.0 backport: Refactor configuration and instanciate it

This is a squash of 4 commits backported from 1.0 and adapted to work
against the stable release of ARA:
- Instanciate the configuration loading into classes
- Refactor configuration
- Import ARA configuration and views "just-in-time"
- The callback no longer persists a json file to cache the playbook id
  for the purpose of ara_record and ara_read, this is instead done
  in-memory in a new _cache key of the flask application context.

In summary:
- ARA now ships a default (and vastly improved!) logging configuration
- Flask context_processors, filters and errorhandlers have been folded
  back into the webapp module
- The configuration has been exploded into submodules that are
  instanciated on a need basis rather than imported.
- Since the configuration is now instanciated, this resolves issues
  with the configuration "leaking" into what was thought to be
  different processes/forks/instances of the ARA application.

Parts of the configuration are now loaded/imported/instanciated
"just in time" because we do not need to load and configure all the
components all the time.
For example, if we're working with an application context, we don't
want to re-import/re-configure everything.

 (cherry picked from commit 0db256a7b0)
 (cherry picked from commit e34ee7ff29)
 (cherry picked from commit 2c418edee5)
 (cherry picked from commit 71ef323675)

Change-Id: Ifee5ec6d1251cd6d0c6933b7c9c371cf43591213
This commit is contained in:
David Moreau Simard 2018-04-04 17:22:26 -04:00
parent 66cdf1c38a
commit 218f93d000
No known key found for this signature in database
GPG Key ID: 33A07694CBB71ECC
21 changed files with 628 additions and 797 deletions

View File

@ -1,171 +0,0 @@
# Copyright (c) 2017 Red Hat, Inc.
#
# This file is part of ARA: Ansible Run Analysis.
#
# ARA is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# ARA is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with ARA. If not, see <http://www.gnu.org/licenses/>.
import os
import xstatic.main
import xstatic.pkg.bootstrap_scss
import xstatic.pkg.datatables
import xstatic.pkg.jquery
import xstatic.pkg.patternfly
import xstatic.pkg.patternfly_bootstrap_treeview
from ansible import __version__ as ansible_version
from ansible.constants import get_config
try:
from ansible.constants import load_config_file
except ImportError:
# Ansible 2.4 no longer provides load_config_file, this is handled further
# down
from ansible.config.manager import find_ini_config_file
# Also, don't scream deprecated things at us
import ansible.constants
ansible.constants._deprecated = lambda *args: None
from distutils.version import LooseVersion
from six.moves import configparser
def _ara_config(config, key, env_var, default=None, section='ara',
value_type=None):
"""
Wrapper around Ansible's get_config backward/forward compatibility
"""
if default is None:
try:
# We're using env_var as keys in the DEFAULTS dict
default = DEFAULTS.get(env_var)
except KeyError as e:
msg = 'There is no default value for {0}: {1}'.format(key, str(e))
raise KeyError(msg)
# >= 2.3.0.0 (NOTE: Ansible trunk versioning scheme has 3 digits, not 4)
if LooseVersion(ansible_version) >= LooseVersion('2.3.0'):
return get_config(config, section, key, env_var, default,
value_type=value_type)
# < 2.3.0.0 compatibility
if value_type is None:
return get_config(config, section, key, env_var, default)
args = {
'boolean': dict(boolean=True),
'integer': dict(integer=True),
'list': dict(islist=True),
'tmppath': dict(istmppath=True)
}
return get_config(config, section, key, env_var, default,
**args[value_type])
DEFAULTS = {
'ARA_AUTOCREATE_DATABASE': True,
'ARA_DIR': os.path.expanduser('~/.ara'),
'ARA_ENABLE_DEBUG_VIEW': False,
'ARA_HOST': '127.0.0.1',
'ARA_IGNORE_EMPTY_GENERATION': True,
'ARA_IGNORE_MIMETYPE_WARNINGS': True,
'ARA_IGNORE_PARAMETERS': ['extra_vars'],
'ARA_LOG_CONFIG': None,
'ARA_LOG_FORMAT': '%(asctime)s - %(name)s - %(levelname)s - %(message)s',
'ARA_LOG_LEVEL': 'INFO',
'ARA_PATH_MAX': 40,
'ARA_PLAYBOOK_OVERRIDE': None,
'ARA_PLAYBOOK_PER_PAGE': 10,
'ARA_PORT': '9191',
'ARA_RESULT_PER_PAGE': 25,
'ARA_SQL_DEBUG': False,
'ARA_TMP_DIR': os.path.expanduser('~/.ansible/tmp')
}
# Bootstrap Ansible configuration
# Ansible >=2.4 takes care of loading the configuration file itself
if LooseVersion(ansible_version) < LooseVersion('2.4.0'):
config, path = load_config_file()
else:
path = find_ini_config_file()
config = configparser.ConfigParser()
if path is not None:
config.read(path)
# Some defaults need to be based on top of a "processed" ARA_DIR
ARA_DIR = _ara_config(config, 'dir', 'ARA_DIR')
database_path = os.path.join(ARA_DIR, 'ansible.sqlite')
DEFAULTS.update({
'ARA_LOG_FILE': os.path.join(ARA_DIR, 'ara.log'),
'ARA_DATABASE': 'sqlite:///{}'.format(database_path)
})
ARA_AUTOCREATE_DATABASE = _ara_config(config, 'autocreate_database',
'ARA_AUTOCREATE_DATABASE',
value_type='boolean')
ARA_ENABLE_DEBUG_VIEW = _ara_config(config, 'enable_debug_view',
'ARA_ENABLE_DEBUG_VIEW',
value_type='boolean')
ARA_HOST = _ara_config(config, 'host', 'ARA_HOST')
ARA_IGNORE_PARAMETERS = _ara_config(config, 'ignore_parameters',
'ARA_IGNORE_PARAMETERS',
value_type='list')
ARA_LOG_CONFIG = _ara_config(config, 'logconfig', 'ARA_LOG_CONFIG')
ARA_LOG_FILE = _ara_config(config, 'logfile', 'ARA_LOG_FILE')
ARA_LOG_FORMAT = _ara_config(config, 'logformat', 'ARA_LOG_FORMAT')
ARA_LOG_LEVEL = _ara_config(config, 'loglevel', 'ARA_LOG_LEVEL')
ARA_PLAYBOOK_OVERRIDE = _ara_config(config, 'playbook_override',
'ARA_PLAYBOOK_OVERRIDE',
value_type='list')
ARA_PLAYBOOK_PER_PAGE = _ara_config(config, 'playbook_per_page',
'ARA_PLAYBOOK_PER_PAGE',
value_type='integer')
ARA_PORT = _ara_config(config, 'port', 'ARA_PORT')
ARA_RESULT_PER_PAGE = _ara_config(config, 'result_per_page',
'ARA_RESULT_PER_PAGE',
value_type='integer')
ARA_TMP_DIR = _ara_config(config, 'local_tmp', 'ANSIBLE_LOCAL_TEMP',
default=DEFAULTS['ARA_TMP_DIR'],
section='defaults',
value_type='tmppath')
# Static generation with flask-frozen
ARA_IGNORE_EMPTY_GENERATION = _ara_config(config,
'ignore_empty_generation',
'ARA_IGNORE_EMPTY_GENERATION',
value_type='boolean')
FREEZER_DEFAULT_MIMETYPE = 'text/html'
FREEZER_IGNORE_MIMETYPE_WARNINGS = _ara_config(config,
'ignore_mimetype_warnings',
'ARA_IGNORE_MIMETYPE_WARNINGS',
value_type='boolean')
FREEZER_RELATIVE_URLS = True
FREEZER_IGNORE_404_NOT_FOUND = True
# SQLAlchemy/Alembic settings
SQLALCHEMY_DATABASE_URI = _ara_config(config, 'database', 'ARA_DATABASE')
SQLALCHEMY_ECHO = _ara_config(config, 'sqldebug',
'ARA_SQL_DEBUG',
value_type='boolean')
SQLALCHEMY_TRACK_MODIFICATIONS = False
INSTALL_PATH = os.path.dirname(os.path.realpath(__file__))
DB_MIGRATIONS = os.path.join(INSTALL_PATH, 'db')
# Xstatic configuration
treeview = xstatic.pkg.patternfly_bootstrap_treeview
XSTATIC = dict(
bootstrap=xstatic.main.XStatic(xstatic.pkg.bootstrap_scss).base_dir,
datatables=xstatic.main.XStatic(xstatic.pkg.datatables).base_dir,
jquery=xstatic.main.XStatic(xstatic.pkg.jquery).base_dir,
patternfly=xstatic.main.XStatic(xstatic.pkg.patternfly).base_dir,
patternfly_bootstrap_treeview=xstatic.main.XStatic(treeview).base_dir,
)

79
ara/config/base.py Normal file
View File

@ -0,0 +1,79 @@
# Copyright (c) 2017 Red Hat, Inc.
#
# This file is part of ARA: Ansible Run Analysis.
#
# ARA is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# ARA is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with ARA. If not, see <http://www.gnu.org/licenses/>.
import os
from ara.config.compat import ara_config
from ara.setup import path as ara_location
class BaseConfig(object):
def __init__(self):
self.ARA_DIR = ara_config(
'dir',
'ARA_DIR',
os.path.expanduser('~/.ara')
)
database_path = os.path.join(self.ARA_DIR, 'ansible.sqlite')
self.ARA_DATABASE = ara_config(
'database',
'ARA_DATABASE',
'sqlite:///%s' % database_path
)
self.ARA_AUTOCREATE_DATABASE = ara_config(
'autocreate_database',
'ARA_AUTOCREATE_DATABASE',
True,
value_type='boolean'
)
self.SQLALCHEMY_DATABASE_URI = self.ARA_DATABASE
self.SQLALCHEMY_TRACK_MODIFICATIONS = False
self.DB_MIGRATIONS = os.path.join(ara_location, 'db')
self.ARA_HOST = ara_config('host', 'ARA_HOST', '127.0.0.1')
self.ARA_PORT = ara_config('port', 'ARA_PORT', '9191')
self.ARA_IGNORE_PARAMETERS = ara_config(
'ignore_parameters',
'ARA_IGNORE_PARAMETERS',
['extra_vars'],
value_type='list'
)
# Static generation with flask-frozen
self.ARA_IGNORE_EMPTY_GENERATION = ara_config(
'ignore_empty_generation',
'ARA_IGNORE_EMPTY_GENERATION',
True,
value_type='boolean'
)
self.FREEZER_DEFAULT_MIMETYPE = 'text/html'
self.FREEZER_IGNORE_MIMETYPE_WARNINGS = ara_config(
'ignore_mimetype_warnings',
'ARA_IGNORE_MIMETYPE_WARNINGS',
True,
value_type='boolean'
)
self.FREEZER_RELATIVE_URLS = True
self.FREEZER_IGNORE_404_NOT_FOUND = True
@property
def config(self):
""" Returns a dictionary for the loaded configuration """
return {
key: self.__dict__[key]
for key in dir(self)
if key.isupper()
}

65
ara/config/compat.py Normal file
View File

@ -0,0 +1,65 @@
# Copyright (c) 2017 Red Hat, Inc.
#
# This file is part of ARA: Ansible Run Analysis.
#
# ARA is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# ARA is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with ARA. If not, see <http://www.gnu.org/licenses/>.
# Compatibility layer between ARA and the different version of Ansible
from ansible import __version__ as ansible_version
from ansible.constants import get_config
try:
from ansible.constants import load_config_file
except ImportError:
# Ansible 2.4 no longer provides load_config_file, this is handled further
# down
from ansible.config.manager import find_ini_config_file
# Also, don't scream deprecated things at us
import ansible.constants
ansible.constants._deprecated = lambda *args: None
from distutils.version import LooseVersion
from six.moves import configparser
def ara_config(key, env_var, default, section='ara', value_type=None):
"""
Wrapper around Ansible's get_config backward/forward compatibility
"""
# Bootstrap Ansible configuration
# Ansible >=2.4 takes care of loading the configuration file itself
if LooseVersion(ansible_version) < LooseVersion('2.4.0'):
config, path = load_config_file()
else:
path = find_ini_config_file()
config = configparser.ConfigParser()
if path is not None:
config.read(path)
# >= 2.3.0.0 (NOTE: Ansible trunk versioning scheme has 3 digits, not 4)
if LooseVersion(ansible_version) >= LooseVersion('2.3.0'):
return get_config(config, section, key, env_var, default,
value_type=value_type)
# < 2.3.0.0 compatibility
if value_type is None:
return get_config(config, section, key, env_var, default)
args = {
'boolean': dict(boolean=True),
'integer': dict(integer=True),
'list': dict(islist=True),
'tmppath': dict(istmppath=True)
}
return get_config(config, section, key, env_var, default,
**args[value_type])

142
ara/config/logger.py Normal file
View File

@ -0,0 +1,142 @@
# Copyright (c) 2017 Red Hat, Inc.
#
# This file is part of ARA: Ansible Run Analysis.
#
# ARA is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# ARA is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with ARA. If not, see <http://www.gnu.org/licenses/>.
# Note: This file tries to import itself if it's named logging, thus, logger.
import logging
import logging.config
import os
import yaml
from ara.config.compat import ara_config
DEFAULT_LOG_CONFIG = """
---
version: 1
formatters:
normal:
format: '%(asctime)s %(levelname)s %(name)s: %(message)s'
console:
format: '%(asctime)s %(levelname)s %(name)s: %(message)s'
handlers:
console:
class: logging.StreamHandler
formatter: console
level: INFO
stream: ext://sys.stdout
normal:
class: logging.handlers.TimedRotatingFileHandler
formatter: normal
level: DEBUG
filename: '{dir}/{file}'
when: 'midnight'
interval: 1
backupCount: 30
loggers:
ara:
handlers:
- console
- normal
level: {level}
propagate: 0
alembic:
handlers:
- console
- normal
level: WARN
propagate: 0
sqlalchemy.engine:
handlers:
- console
- normal
level: WARN
propagate: 0
werkzeug:
handlers:
- console
- normal
level: INFO
propagate: 0
root:
handlers:
- normal
level: {level}
"""
class LogConfig(object):
def __init__(self):
default_dir = ara_config('dir', 'ARA_DIR',
os.path.expanduser('~/.ara'))
self.ARA_LOG_CONFIG = ara_config(
'logconfig', 'ARA_LOG_CONFIG', os.path.join(default_dir,
'logging.yml')
)
self.ARA_LOG_DIR = ara_config('logdir', 'ARA_LOG_DIR', default_dir)
self.ARA_LOG_FILE = ara_config('logfile', 'ARA_LOG_FILE', 'ara.log')
self.ARA_LOG_LEVEL = ara_config('loglevel', 'ARA_LOG_LEVEL', 'INFO')
if self.ARA_LOG_LEVEL == 'DEBUG':
self.SQLALCHEMY_ECHO = True
self.ARA_ENABLE_DEBUG_VIEW = True
else:
self.SQLALCHEMY_ECHO = False
self.ARA_ENABLE_DEBUG_VIEW = False
@property
def config(self):
""" Returns a dictionary for the loaded configuration """
return {
key: self.__dict__[key]
for key in dir(self)
if key.isupper()
}
def setup_logging(config=None):
if config is None:
config = LogConfig().config
if not os.path.isdir(config['ARA_LOG_DIR']):
os.makedirs(config['ARA_LOG_DIR'], mode=0o750)
if not os.path.exists(config['ARA_LOG_CONFIG']):
default_config = DEFAULT_LOG_CONFIG.format(
dir=config['ARA_LOG_DIR'],
file=config['ARA_LOG_FILE'],
level=config['ARA_LOG_LEVEL']
)
with open(config['ARA_LOG_CONFIG'], 'w') as log_config:
log_config.write(default_config.lstrip())
ext = os.path.splitext(config['ARA_LOG_CONFIG'])[1]
if ext in ('.yml', '.yaml', '.json'):
# yaml.safe_load can load json as well as yaml
logging.config.dictConfig(yaml.safe_load(
open(config['ARA_LOG_CONFIG'], 'r')
))
else:
logging.config.fileConfig(config['ARA_LOG_CONFIG'])
logger = logging.getLogger('ara.logging')
msg = 'Logging: Level {level} from {config}, logging to {dir}/{file}'
msg = msg.format(
level=config['ARA_LOG_LEVEL'],
config=config['ARA_LOG_CONFIG'],
dir=config['ARA_LOG_DIR'],
file=config['ARA_LOG_FILE'],
)
logger.debug(msg)

64
ara/config/webapp.py Normal file
View File

@ -0,0 +1,64 @@
# Copyright (c) 2017 Red Hat, Inc.
#
# This file is part of ARA: Ansible Run Analysis.
#
# ARA is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# ARA is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with ARA. If not, see <http://www.gnu.org/licenses/>.
from xstatic import main as xs
import xstatic.pkg.bootstrap_scss
import xstatic.pkg.datatables
import xstatic.pkg.jquery
import xstatic.pkg.patternfly
import xstatic.pkg.patternfly_bootstrap_treeview
from ara.config.compat import ara_config
class WebAppConfig(object):
def __init__(self):
self.ARA_PLAYBOOK_PER_PAGE = ara_config(
'playbook_per_page',
'ARA_PLAYBOOK_PER_PAGE',
10,
value_type='integer'
)
self.ARA_RESULT_PER_PAGE = ara_config(
'result_per_page',
'ARA_RESULT_PER_PAGE',
25,
value_type='integer'
)
self.ARA_PLAYBOOK_OVERRIDE = ara_config(
'playbook_override',
'ARA_PLAYBOOK_OVERRIDE',
None,
value_type='list'
)
treeview = xstatic.pkg.patternfly_bootstrap_treeview
self.XSTATIC = dict(
bootstrap=xs.XStatic(xstatic.pkg.bootstrap_scss).base_dir,
datatables=xs.XStatic(xstatic.pkg.datatables).base_dir,
jquery=xs.XStatic(xstatic.pkg.jquery).base_dir,
patternfly=xs.XStatic(xstatic.pkg.patternfly).base_dir,
patternfly_bootstrap_treeview=xs.XStatic(treeview).base_dir,
)
@property
def config(self):
""" Returns a dictionary for the loaded configuration """
return {
key: self.__dict__[key]
for key in dir(self)
if key.isupper()
}

View File

@ -1,45 +0,0 @@
# Copyright (c) 2017 Red Hat, Inc.
#
# This file is part of ARA: Ansible Run Analysis.
#
# ARA is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# ARA is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with ARA. If not, see <http://www.gnu.org/licenses/>.
import sys
from ansible import __version__ as ansible_version
from ara import __release__ as ara_release
from ara import models
def configure_context_processors(app):
@app.context_processor
def ctx_add_nav_data():
"""
Returns standard data that will be available in every template view.
"""
try:
models.Playbook.query.one()
empty_database = False
except models.MultipleResultsFound:
empty_database = False
except models.NoResultFound:
empty_database = True
# Get python version info
major, minor, micro, release, serial = sys.version_info
return dict(ara_version=ara_release,
ansible_version=ansible_version,
python_version="{0}.{1}".format(major, minor),
empty_database=empty_database)

View File

@ -1,24 +0,0 @@
# Copyright (c) 2017 Red Hat, Inc.
#
# This file is part of ARA: Ansible Run Analysis.
#
# ARA is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# ARA is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with ARA. If not, see <http://www.gnu.org/licenses/>.
from flask import render_template
def configure_errorhandlers(app):
@app.errorhandler(404)
def page_not_found(error):
return render_template('errors/404.html', error=error), 404

View File

@ -1,122 +0,0 @@
# Copyright (c) 2017 Red Hat, Inc.
#
# This file is part of ARA: Ansible Run Analysis.
#
# ARA is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# ARA is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with ARA. If not, see <http://www.gnu.org/licenses/>.
import datetime
import logging
import six
from ara.utils import fast_count
from ara.utils import playbook_treeview
from jinja2 import Markup
from os import path
from oslo_serialization import jsonutils
from pygments import highlight
from pygments.formatters import HtmlFormatter
from pygments.lexers import YamlLexer
from pygments.lexers import JsonLexer
from pygments.lexers.special import TextLexer
def configure_template_filters(app):
log = logging.getLogger('%s.filters' % app.logger_name)
@app.template_filter('datefmt')
def jinja_date_formatter(timestamp, format='%Y-%m-%d %H:%M:%S'):
""" Reformats a datetime timestamp from str(datetime.datetime) """
if timestamp is None:
return 'n/a'
else:
return datetime.datetime.strftime(timestamp, format)
@app.template_filter('timefmt')
def jinja_time_formatter(timestamp):
""" Reformats a datetime timedelta """
if timestamp is None:
return 'n/a'
else:
date = datetime.timedelta(seconds=int(timestamp.total_seconds()))
return str(date)
@app.template_filter('to_nice_json')
def jinja_to_nice_json(result):
""" Tries to format a result as a pretty printed JSON. """
try:
return jsonutils.dumps(jsonutils.loads(result),
indent=4,
sort_keys=True)
except (ValueError, TypeError):
try:
return jsonutils.dumps(result, indent=4, sort_keys=True)
except TypeError as err:
log.error('failed to dump json: %s', err)
return result
@app.template_filter('from_json')
def jinja_from_json(val):
try:
return jsonutils.loads(val)
except ValueError as err:
log.error('failed to load json: %s', err)
return val
@app.template_filter('yamlhighlight')
def jinja_yamlhighlight(code):
formatter = HtmlFormatter(linenos='table',
anchorlinenos=True,
lineanchors='line',
linespans='line',
cssclass='codehilite')
if not code:
code = ''
return highlight(Markup(code).unescape(),
YamlLexer(stripall=True),
formatter)
@app.template_filter('pygments_formatter')
def jinja_pygments_formatter(data):
formatter = HtmlFormatter(cssclass='codehilite')
if isinstance(data, dict) or isinstance(data, list):
data = jsonutils.dumps(data, indent=4, sort_keys=True)
lexer = JsonLexer()
elif six.string_types or six.text_type:
try:
data = jsonutils.dumps(jsonutils.loads(data),
indent=4,
sort_keys=True)
lexer = JsonLexer()
except (ValueError, TypeError):
lexer = TextLexer()
else:
lexer = TextLexer()
lexer.stripall = True
return highlight(Markup(data).unescape(), lexer, formatter)
@app.template_filter('fast_count')
def jinja_fast_count(query):
return fast_count(query)
@app.template_filter('basename')
def jinja_basename(pathname):
return path.basename(pathname)
@app.template_filter('treeview')
def jinja_treeview(playbook):
return playbook_treeview(playbook)

View File

@ -15,10 +15,7 @@
# You should have received a copy of the GNU General Public License
# along with ARA. If not, see <http://www.gnu.org/licenses/>.
import os
from ansible.plugins.action import ActionBase
from oslo_serialization import jsonutils
try:
from ara import models
@ -109,11 +106,6 @@ class ActionModule(ActionBase):
}
return result
app = create_app()
if not current_app:
context = app.app_context()
context.push()
for arg in self._task.args:
if arg not in self.VALID_ARGS:
result = {
@ -134,12 +126,14 @@ class ActionModule(ActionBase):
result['msg'] = '{0} parameter is required'.format(parameter)
return result
app = create_app()
if not current_app:
context = app.app_context()
context.push()
if playbook_id is None:
# Retrieve the persisted playbook_id from tmpfile
tmpfile = os.path.join(app.config['ARA_TMP_DIR'], 'ara.json')
with open(tmpfile, 'rb') as file:
data = jsonutils.load(file)
playbook_id = data['playbook']['id']
# Retrieve playbook_id from the cached context
playbook_id = current_app._cache['playbook']
try:
data = self.get_key(playbook_id, key)

View File

@ -15,10 +15,7 @@
# You should have received a copy of the GNU General Public License
# along with ARA. If not, see <http://www.gnu.org/licenses/>.
import os
from ansible.plugins.action import ActionBase
from oslo_serialization import jsonutils
try:
from ara import models
@ -143,11 +140,6 @@ class ActionModule(ActionBase):
}
return result
app = create_app()
if not current_app:
context = app.app_context()
context.push()
for arg in self._task.args:
if arg not in self.VALID_ARGS:
result = {
@ -178,12 +170,14 @@ class ActionModule(ActionBase):
result['msg'] = msg
return result
app = create_app()
if not current_app:
context = app.app_context()
context.push()
if playbook_id is None:
# Retrieve the persisted playbook_id from tmpfile
tmpfile = os.path.join(app.config['ARA_TMP_DIR'], 'ara.json')
with open(tmpfile, 'rb') as file:
data = jsonutils.load(file)
playbook_id = data['playbook']['id']
# Retrieve playbook_id from the cached context
playbook_id = current_app._cache['playbook']
try:
self.create_or_update_key(playbook_id, key, value, type)

View File

@ -17,7 +17,6 @@
from __future__ import (absolute_import, division, print_function)
import flask
import itertools
import logging
import os
@ -29,6 +28,7 @@ from ara.models import db
from ara.webapp import create_app
from datetime import datetime
from distutils.version import LooseVersion
from flask import current_app
from oslo_serialization import jsonutils
# To retrieve Ansible CLI options
@ -62,7 +62,7 @@ class CallbackModule(CallbackBase):
def __init__(self):
super(CallbackModule, self).__init__()
if not flask.current_app:
if not current_app:
ctx = app.app_context()
ctx.push()
@ -333,15 +333,8 @@ class CallbackModule(CallbackBase):
file_ = self.get_or_create_file(path)
file_.is_playbook = True
# We need to persist the playbook id so it can be used by the modules
data = {
'playbook': {
'id': self.playbook.id
}
}
tmpfile = os.path.join(app.config['ARA_TMP_DIR'], 'ara.json')
with open(tmpfile, 'w') as file:
file.write(jsonutils.dumps(data))
# Cache the playbook data in memory for ara_record/ara_read
current_app._cache['playbook'] = self.playbook.id
def v2_playbook_on_play_start(self, play):
self.close_task()

View File

@ -1,74 +0,0 @@
# Copyright (c) 2017 Red Hat, Inc.
#
# This file is part of ARA: Ansible Run Analysis.
#
# ARA is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# ARA is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with ARA. If not, see <http://www.gnu.org/licenses/>.
# This file is purposefully left empty due to an Ansible issue
# Details at: https://github.com/ansible/ansible/pull/18208
# TODO: Remove this file and update the documentation when the issue is fixed,
# released and present in all supported versions.
DOCUMENTATION = """
---
module: ara_read
short_description: Ansible module to read recorded persistent data with ARA.
version_added: "2.0"
author: "RDO Community <rdo-list@redhat.com>"
description:
- Ansible module to read recorded persistent data with ARA.
options:
playbook:
description:
- uuid of the playbook to read the key from
required: false
version_added: 0.13.2
key:
description:
- Name of the key to read from
required: true
requirements:
- "python >= 2.6"
- "ara >= 0.10.0"
"""
EXAMPLES = """
# Write data
- ara_record:
key: "foo"
value: "bar"
# Read data
- ara_read:
key: "foo"
register: foo
# Read data from a specific playbook
# (Retrieve playbook uuid's with 'ara playbook list')
- ara_read:
playbook: uuuu-iiii-dddd-0000
key: logs
register: logs
# Use data
- debug:
msg: "{{ item }}"
with_items:
- foo.key
- foo.value
- foo.type
- foo.playbook_id
"""

View File

@ -1,92 +0,0 @@
# Copyright (c) 2017 Red Hat, Inc.
#
# This file is part of ARA: Ansible Run Analysis.
#
# ARA is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# ARA is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with ARA. If not, see <http://www.gnu.org/licenses/>.
# This file is purposefully left empty due to an Ansible issue
# Details at: https://github.com/ansible/ansible/pull/18208
# TODO: Remove this file and update the documentation when the issue is fixed,
# released and present in all supported versions.
DOCUMENTATION = """
---
module: ara_record
short_description: Ansible module to record persistent data with ARA.
version_added: "2.0"
author: "RDO Community <rdo-list@redhat.com>"
description:
- Ansible module to record persistent data with ARA.
options:
playbook:
description:
- uuid of the playbook to write the key to
required: false
version_added: 0.13.2
key:
description:
- Name of the key to write data to
required: true
value:
description:
- Value of the key written to
required: true
type:
description:
- Type of the key
choices: [text, url, json, list, dict]
default: text
requirements:
- "python >= 2.6"
- "ara >= 0.10.0"
"""
EXAMPLES = """
# Write static data
- ara_record:
key: "foo"
value: "bar"
# Write data to a specific (previously run) playbook
# (Retrieve playbook uuid's with 'ara playbook list')
- ara_record:
playbook: uuuu-iiii-dddd-0000
key: logs
value: "{{ lookup('file', '/var/log/ansible.log') }}"
type: text
# Write dynamic data
- shell: cd dev && git rev-parse HEAD
register: git_version
delegate_to: localhost
- ara_record:
key: "git_version"
value: "{{ git_version.stdout }}"
# Write data with a type (otherwise defaults to "text")
# This changes the behavior on how the value is presented in the web interface
- ara_record:
key: "{{ item.key }}"
value: "{{ item.value }}"
type: "{{ item.type }}"
with_items:
- { key: "log", value: "error", type: "text" }
- { key: "website", value: "http://domain.tld", type: "url" }
- { key: "data", value: "{ 'key': 'value' }", type: "json" }
- { key: "somelist", value: ['one', 'two'], type: "list" }
- { key: "somedict", value: {'key': 'value' }, type: "dict" }
"""

View File

@ -33,7 +33,7 @@
Tasks
</button>
<button type="button" class="list-group-item list-group-item-action">
<span class="badge">{{ task_results }}</span>
<span class="badge">{{ results }}</span>
Task results
</button>
<button type="button" class="list-group-item list-group-item-action">
@ -41,7 +41,7 @@
Hosts
</button>
<button type="button" class="list-group-item list-group-item-action">
<span class="badge">{{ host_facts }}</span>
<span class="badge">{{ facts }}</span>
Hosts with gathered facts
</button>
<button type="button" class="list-group-item list-group-item-action">

View File

@ -29,12 +29,14 @@ class TestAra(unittest.TestCase):
Common setup/teardown for ARA tests
"""
def setUp(self):
# TODO: Fix this, it's not used in create_app() and makes the databases
# live on the filesystem rather than memory.
self.config = {
'SQLALCHEMY_DATABASE_URI': 'sqlite://',
'TESTING': True
}
self.app = w.create_app(self)
self.app = w.create_app()
self.app_context = self.app.app_context()
self.app_context.push()
self.client = self.app.test_client()

View File

@ -16,7 +16,6 @@
# along with ARA. If not, see <http://www.gnu.org/licenses/>.
import random
import os
import ara.models as m
import ara.utils as u
@ -25,7 +24,6 @@ import ara.plugins.callbacks.log_ara as l
from ara.tests.unit.common import TestAra
from ara.tests.unit import fakes
from collections import defaultdict
from oslo_serialization import jsonutils
class TestCallback(TestAra):
@ -86,14 +84,6 @@ class TestCallback(TestAra):
self.assertIsNotNone(r_playbook)
self.assertEqual(r_playbook.path, self.playbook.path)
def test_playbook_persistence(self):
r_playbook = m.Playbook.query.first()
tmpfile = os.path.join(self.app.config['ARA_TMP_DIR'], 'ara.json')
with open(tmpfile, 'rb') as file:
data = jsonutils.load(file)
self.assertEqual(r_playbook.id, data['playbook']['id'])
def test_callback_play(self):
r_play = m.Play.query.first()
self.assertIsNotNone(r_play)

View File

@ -16,13 +16,10 @@
# along with ARA. If not, see <http://www.gnu.org/licenses/>.
import os
import pytest
import shutil
import six
import tempfile
from distutils.version import LooseVersion
from flask_frozen import MissingURLGeneratorWarning
from glob import glob
from lxml import etree
from oslo_serialization import jsonutils
@ -679,18 +676,7 @@ class TestCLIGenerate(TestAra):
cmd = ara.cli.generate.GenerateHtml(shell, None)
parser = cmd.get_parser('test')
args = parser.parse_args([dir])
with pytest.warns(MissingURLGeneratorWarning) as warnings:
cmd.take_action(args)
# pytest 3.0 through 3.1 are backwards incompatible here
if LooseVersion(pytest.__version__) >= LooseVersion('3.1.0'):
cat = [item._category_name for item in warnings]
self.assertTrue(any('MissingURLGeneratorWarning' in c
for c in cat))
else:
self.assertTrue(any(MissingURLGeneratorWarning == w.category
for w in warnings))
cmd.take_action(args)
paths = [
os.path.join(dir, 'index.html'),
@ -734,8 +720,7 @@ class TestCLIGenerate(TestAra):
def test_generate_html(self):
""" Roughly ensures the expected files are generated properly """
dir = self.generate_dir
ctx = ansible_run()
ansible_run()
shell = ara.shell.AraCli()
shell.prepare_to_run_command(ara.cli.generate.GenerateHtml)
@ -745,69 +730,18 @@ class TestCLIGenerate(TestAra):
args = parser.parse_args([dir])
cmd.take_action(args)
file_id = ctx['task'].file_id
host_id = ctx['host'].id
result_id = ctx['result'].id
paths = [
os.path.join(dir, 'index.html'),
os.path.join(dir, 'static'),
os.path.join(dir, 'file/index.html'),
os.path.join(dir, 'file/{0}'.format(file_id)),
os.path.join(dir, 'host/index.html'),
os.path.join(dir, 'host/{0}'.format(host_id)),
os.path.join(dir, 'reports/index.html'),
os.path.join(dir, 'result/index.html'),
os.path.join(dir, 'result/{0}'.format(result_id))
]
for path in paths:
self.assertTrue(os.path.exists(path))
def test_generate_html_for_playbook(self):
""" Roughly ensures the expected files are generated properly """
dir = self.generate_dir
# Record two separate playbooks
ctx = ansible_run()
ansible_run()
shell = ara.shell.AraCli()
shell.prepare_to_run_command(ara.cli.generate.GenerateHtml)
cmd = ara.cli.generate.GenerateHtml(shell, None)
parser = cmd.get_parser('test')
args = parser.parse_args([dir, '--playbook', ctx['playbook'].id])
cmd.take_action(args)
file_id = ctx['task'].file_id
host_id = ctx['host'].id
result_id = ctx['result'].id
paths = [
os.path.join(dir, 'index.html'),
os.path.join(dir, 'static'),
os.path.join(dir, 'file/index.html'),
os.path.join(dir, 'file/{0}'.format(file_id)),
os.path.join(dir, 'host/index.html'),
os.path.join(dir, 'host/{0}'.format(host_id)),
os.path.join(dir, 'reports/index.html'),
os.path.join(dir, 'result/index.html'),
os.path.join(dir, 'result/{0}'.format(result_id))
]
for path in paths:
self.assertTrue(os.path.exists(path))
# Test that we effectively have two playbooks
playbooks = m.Playbook.query.all()
self.assertTrue(len(playbooks) == 2)
# Retrieve the other playbook and validate that we haven't generated
# files for it
playbook_two = (m.Playbook.query
.filter(m.Playbook.id != ctx['playbook'].id).one())
path = os.path.join(dir, 'playbook/{0}'.format(playbook_two.id))
self.assertFalse(os.path.exists(path))
def test_generate_junit(self):
""" Roughly ensures the expected xml is generated properly """
tdir = self.generate_dir

View File

@ -17,6 +17,9 @@
import os
from ara.config.base import BaseConfig
from ara.setup import path as ara_location
from ara.tests.unit.common import TestAra
@ -28,40 +31,28 @@ class TestConfig(TestAra):
def tearDown(self):
super(TestConfig, self).tearDown()
def test_default_config(self):
""" Ensure we have expected default parameters """
keys = [
'ARA_AUTOCREATE_DATABASE',
'ARA_DIR',
'ARA_ENABLE_DEBUG_VIEW',
'ARA_HOST',
'ARA_IGNORE_EMPTY_GENERATION',
'ARA_LOG_FILE',
'ARA_LOG_FORMAT',
'ARA_LOG_LEVEL',
'ARA_PORT',
'ARA_PLAYBOOK_OVERRIDE',
'ARA_PLAYBOOK_PER_PAGE',
'ARA_RESULT_PER_PAGE',
]
# TODO: Improve those
def test_config_base(self):
base_config = BaseConfig()
db = "sqlite:///%s/ansible.sqlite" % os.path.expanduser('~/.ara')
defaults = {
"FREEZER_IGNORE_MIMETYPE_WARNINGS": True,
"FREEZER_DEFAULT_MIMETYPE": "text/html",
"FREEZER_IGNORE_404_NOT_FOUND": True,
"ARA_DIR": os.path.expanduser('~/.ara'),
"SQLALCHEMY_DATABASE_URI": db,
"ARA_HOST": "127.0.0.1",
"ARA_AUTOCREATE_DATABASE": True,
"ARA_PORT": "9191",
"ARA_DATABASE": db,
"ARA_IGNORE_EMPTY_GENERATION": True,
"ARA_IGNORE_PARAMETERS": [
"extra_vars"
],
"FREEZER_RELATIVE_URLS": True,
"SQLALCHEMY_TRACK_MODIFICATIONS": False,
"DB_MIGRATIONS": os.path.join(ara_location, 'db')
}
defaults = self.app.config['DEFAULTS']
for key in keys:
self.assertEqual(defaults[key],
self.app.config[key])
self.assertEqual(defaults['ARA_DATABASE'],
self.app.config['SQLALCHEMY_DATABASE_URI'])
self.assertEqual(defaults['ARA_SQL_DEBUG'],
self.app.config['SQLALCHEMY_ECHO'])
self.assertEqual(defaults['ARA_TMP_DIR'],
os.path.split(self.app.config['ARA_TMP_DIR'])[:-1][0])
self.assertEqual(defaults['ARA_IGNORE_MIMETYPE_WARNINGS'],
self.app.config['FREEZER_IGNORE_MIMETYPE_WARNINGS'])
# TODO:
# - Add tests for config from hash (create_app(config))
# - Possibly test config from envvars
# ( Needs config.py not to configure things at import time )
# - Mock out or control filesystem operations (i.e, webapp.configure_dirs)
for key, value in base_config.config.items():
assert value == defaults[key]

View File

@ -15,51 +15,49 @@
# You should have received a copy of the GNU General Public License
# along with ARA. If not, see <http://www.gnu.org/licenses/>.
from ara import models
from ara.utils import fast_count
from flask import Blueprint
from flask import current_app
from flask import render_template
from ara import models
from ara.utils import fast_count
about = Blueprint('about', __name__)
@about.route('/')
def main():
""" Returns the about page """
files = models.File.query
hosts = models.Host.query
facts = models.HostFacts.query
playbooks = models.Playbook.query
records = models.Data.query
tasks = models.Task.query
results = models.TaskResult.query
if current_app.config['ARA_PLAYBOOK_OVERRIDE'] is not None:
override = current_app.config['ARA_PLAYBOOK_OVERRIDE']
files = (models.File.query
.filter(models.File.playbook_id.in_(override)))
host_facts = (models.HostFacts.query
.join(models.Host)
.filter(models.Host.playbook_id.in_(override)))
hosts = (models.Host.query
files = files.filter(models.File.playbook_id.in_(override))
facts = (facts
.join(models.Host)
.filter(models.Host.playbook_id.in_(override)))
playbooks = (models.Playbook.query
.filter(models.Playbook.id.in_(override)))
records = (models.Data.query
.filter(models.Data.playbook_id.in_(override)))
tasks = (models.Task.query
.filter(models.Task.playbook_id.in_(override)))
task_results = (models.TaskResult.query
.join(models.Task)
.filter(models.Task.playbook_id.in_(override)))
else:
files = models.File.query
host_facts = models.HostFacts.query
hosts = models.Host.query
playbooks = models.Playbook.query
records = models.Data.query
tasks = models.Task.query
task_results = models.TaskResult.query
hosts = hosts.filter(models.Host.playbook_id.in_(override))
playbooks = playbooks.filter(models.Playbook.id.in_(override))
records = records.filter(models.Data.playbook_id.in_(override))
tasks = tasks.filter(models.Task.playbook_id.in_(override))
results = (results
.join(models.Task)
.filter(models.Task.playbook_id.in_(override)))
return render_template('about.html',
active='about',
files=fast_count(files),
host_facts=fast_count(host_facts),
hosts=fast_count(hosts),
playbooks=fast_count(playbooks),
records=fast_count(records),
tasks=fast_count(tasks),
task_results=fast_count(task_results))
return render_template(
'about.html',
active='about',
files=fast_count(files),
hosts=fast_count(hosts),
facts=fast_count(facts),
playbooks=fast_count(playbooks),
records=fast_count(records),
tasks=fast_count(tasks),
results=fast_count(results)
)

View File

@ -15,76 +15,68 @@
# You should have received a copy of the GNU General Public License
# along with ARA. If not, see <http://www.gnu.org/licenses/>.
import ara.config
import ara.views
# Note (dmsimard): ARA's configuration, views and API imports are done
# "just-in-time" because we might end up loading them for nothing and it has a
# non-negligible cost.
# It also breaks some assumptions... For example, "ara.config" loads the
# configuration automatically on import which might not be desirable.
import datetime
import flask_migrate
import logging
import logging.config
import os
import yaml
import six
import sys
from ansible import __version__ as ansible_version
from ara import __release__ as ara_release
from ara.config.base import BaseConfig
from ara.config.logger import LogConfig
from ara.config.logger import setup_logging
from ara.config.webapp import WebAppConfig
from ara import models
from ara.utils import fast_count
from ara.utils import playbook_treeview
from alembic.migration import MigrationContext
from alembic.script import ScriptDirectory
from ara.context_processors import configure_context_processors
from ara.errorhandlers import configure_errorhandlers
from ara.filters import configure_template_filters
from ara.models import db
from flask import abort
from flask import current_app
from flask import Flask
from flask import logging as flask_logging
from flask import send_from_directory
from sqlalchemy.engine.reflection import Inspector
DEFAULT_APP_NAME = 'ara'
views = (
(ara.views.about, '/about'),
(ara.views.file, '/file'),
(ara.views.host, '/host'),
(ara.views.reports, ''),
(ara.views.result, '/result'),
)
from flask import abort
from flask import Flask
from flask import render_template
from flask import send_from_directory
from jinja2 import Markup
from oslo_serialization import jsonutils
from pygments import highlight
from pygments.formatters import HtmlFormatter
from pygments.lexers import YamlLexer
from pygments.lexers import JsonLexer
from pygments.lexers.special import TextLexer
def create_app(config=None, app_name=None):
if app_name is None:
app_name = DEFAULT_APP_NAME
def create_app():
app = Flask('ara')
if current_app:
return current_app
app = Flask(app_name)
configure_app(app, config)
configure_app(app)
configure_dirs(app)
configure_logging(app)
configure_errorhandlers(app)
configure_template_filters(app)
configure_context_processors(app)
configure_blueprints(app)
configure_static_route(app)
configure_db(app)
configure_errorhandlers(app)
configure_template_filters(app)
configure_context_processors(app)
configure_cache(app)
return app
def configure_blueprints(app):
for view, prefix in views:
app.register_blueprint(view, url_prefix=prefix)
if app.config.get('ARA_ENABLE_DEBUG_VIEW'):
app.register_blueprint(ara.views.debug, url_prefix='/debug')
def configure_app(app, config):
app.config.from_object(ara.config)
if config is not None:
app.config.from_object(config)
app.config.from_envvar('ARA_CONFIG', silent=True)
def configure_app(app):
app.config.update(BaseConfig().config)
app.config.update(LogConfig().config)
app.config.update(WebAppConfig().config)
def configure_dirs(app):
@ -92,6 +84,148 @@ def configure_dirs(app):
os.makedirs(app.config['ARA_DIR'], mode=0o700)
def configure_logging(app):
setup_logging(app.config)
def configure_errorhandlers(app):
@app.errorhandler(404)
def page_not_found(error):
return render_template('errors/404.html', error=error), 404
def configure_template_filters(app):
log = logging.getLogger('ara.filters')
@app.template_filter('datefmt')
def jinja_date_formatter(timestamp, format='%Y-%m-%d %H:%M:%S'):
""" Reformats a datetime timestamp from str(datetime.datetime) """
if timestamp is None:
return 'n/a'
else:
return datetime.datetime.strftime(timestamp, format)
@app.template_filter('timefmt')
def jinja_time_formatter(timestamp):
""" Reformats a datetime timedelta """
if timestamp is None:
return 'n/a'
else:
date = datetime.timedelta(seconds=int(timestamp.total_seconds()))
return str(date)
@app.template_filter('to_nice_json')
def jinja_to_nice_json(result):
""" Tries to format a result as a pretty printed JSON. """
try:
return jsonutils.dumps(jsonutils.loads(result),
indent=4,
sort_keys=True)
except (ValueError, TypeError):
try:
return jsonutils.dumps(result, indent=4, sort_keys=True)
except TypeError as err:
log.error('failed to dump json: %s', err)
return result
@app.template_filter('from_json')
def jinja_from_json(val):
try:
return jsonutils.loads(val)
except ValueError as err:
log.error('failed to load json: %s', err)
return val
@app.template_filter('yamlhighlight')
def jinja_yamlhighlight(code):
formatter = HtmlFormatter(linenos='table',
anchorlinenos=True,
lineanchors='line',
linespans='line',
cssclass='codehilite')
if not code:
code = ''
return highlight(Markup(code).unescape(),
YamlLexer(stripall=True),
formatter)
@app.template_filter('pygments_formatter')
def jinja_pygments_formatter(data):
formatter = HtmlFormatter(cssclass='codehilite')
if isinstance(data, dict) or isinstance(data, list):
data = jsonutils.dumps(data, indent=4, sort_keys=True)
lexer = JsonLexer()
elif six.string_types or six.text_type:
try:
data = jsonutils.dumps(jsonutils.loads(data),
indent=4,
sort_keys=True)
lexer = JsonLexer()
except (ValueError, TypeError):
lexer = TextLexer()
else:
lexer = TextLexer()
lexer.stripall = True
return highlight(Markup(data).unescape(), lexer, formatter)
# TODO: Remove fast_count, replace by "select id, order desc, limit 1"
@app.template_filter('fast_count')
def jinja_fast_count(query):
return fast_count(query)
@app.template_filter('basename')
def jinja_basename(pathname):
return os.path.basename(pathname)
@app.template_filter('treeview')
def jinja_treeview(playbook):
return playbook_treeview(playbook)
def configure_context_processors(app):
@app.context_processor
def ctx_add_nav_data():
"""
Returns standard data that will be available in every template view.
"""
try:
models.Playbook.query.one()
empty_database = False
except models.MultipleResultsFound:
empty_database = False
except models.NoResultFound:
empty_database = True
# Get python version info
major, minor, micro, release, serial = sys.version_info
return dict(ara_version=ara_release,
ansible_version=ansible_version,
python_version="{0}.{1}".format(major, minor),
empty_database=empty_database)
def configure_blueprints(app):
import ara.views # flake8: noqa
views = (
(ara.views.about, '/about'),
(ara.views.file, '/file'),
(ara.views.host, '/host'),
(ara.views.reports, ''),
(ara.views.result, '/result'),
)
for view, prefix in views:
app.register_blueprint(view, url_prefix=prefix)
if app.config.get('ARA_ENABLE_DEBUG_VIEW'):
app.register_blueprint(ara.views.debug, url_prefix='/debug')
def configure_db(app):
"""
0.10 is the first version of ARA that ships with a stable database schema.
@ -100,17 +234,17 @@ def configure_db(app):
If there is no alembic revision available, assume we are running the first
revision which contains the latest state of the database prior to this.
"""
db.init_app(app)
models.db.init_app(app)
log = logging.getLogger(app.logger_name)
if app.config.get('ARA_AUTOCREATE_DATABASE'):
with app.app_context():
migrations = app.config['DB_MIGRATIONS']
flask_migrate.Migrate(app, db, directory=migrations)
flask_migrate.Migrate(app, models.db, directory=migrations)
config = app.extensions['migrate'].migrate.get_config(migrations)
# Verify if the database tables have been created at all
inspector = Inspector.from_engine(db.engine)
inspector = Inspector.from_engine(models.db.engine)
if len(inspector.get_table_names()) == 0:
log.info('Initializing new DB from scratch')
flask_migrate.upgrade(directory=migrations)
@ -120,7 +254,7 @@ def configure_db(app):
head = script.get_current_head()
# Get current revision, if available
connection = db.engine.connect()
connection = models.db.engine.connect()
context = MigrationContext.configure(connection)
current = context.get_current_revision()
@ -134,33 +268,6 @@ def configure_db(app):
flask_migrate.upgrade(directory=migrations)
def configure_logging(app):
if app.config['ARA_LOG_CONFIG'] and os.path.exists(
app.config['ARA_LOG_CONFIG']):
config_path = app.config['ARA_LOG_CONFIG']
if os.path.splitext(config_path)[1] in ('.yml', '.yaml', '.json'):
# yaml.safe_load can load json as well as yaml
logging.config.dictConfig(yaml.safe_load(open(config_path, 'r')))
else:
logging.config.fileConfig(config_path)
elif app.config['ARA_LOG_FILE']:
handler = logging.FileHandler(app.config['ARA_LOG_FILE'])
# Set the ARA log format or fall back to the flask debugging format
handler.setFormatter(
logging.Formatter(app.config.get(
'ARA_LOG_FORMAT', flask_logging.DEBUG_LOG_FORMAT)))
logger = logging.getLogger(app.logger_name)
logger.setLevel(app.config['ARA_LOG_LEVEL'])
del logger.handlers[:]
logger.addHandler(handler)
for name in ('alembic', 'sqlalchemy.engine'):
other_logger = logging.getLogger(name)
other_logger.setLevel(logging.WARNING)
del other_logger.handlers[:]
other_logger.addHandler(handler)
def configure_static_route(app):
# Note (dmsimard)
# /static/ is provided from in-tree bundled files and libraries.
@ -177,9 +284,15 @@ def configure_static_route(app):
@app.route('/static/packaged/<module>/<path:filename>')
def serve_static_packaged(module, filename):
xstatic = current_app.config['XSTATIC']
xstatic = app.config['XSTATIC']
if module in xstatic:
return send_from_directory(xstatic[module], filename)
else:
abort(404)
def configure_cache(app):
""" Sets up an attribute to cache data in the app context """
if not getattr(app, '_cache', None):
app._cache = {}