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 commit0db256a7b0
) (cherry picked from commite34ee7ff29
) (cherry picked from commit2c418edee5
) (cherry picked from commit71ef323675
) Change-Id: Ifee5ec6d1251cd6d0c6933b7c9c371cf43591213
This commit is contained in:
parent
66cdf1c38a
commit
218f93d000
171
ara/config.py
171
ara/config.py
|
@ -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,
|
||||
)
|
|
@ -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()
|
||||
}
|
|
@ -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])
|
|
@ -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)
|
|
@ -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()
|
||||
}
|
|
@ -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)
|
|
@ -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
|
122
ara/filters.py
122
ara/filters.py
|
@ -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)
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
"""
|
|
@ -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" }
|
||||
"""
|
|
@ -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">
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
|
|
275
ara/webapp.py
275
ara/webapp.py
|
@ -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 = {}
|
||||
|
|
Loading…
Reference in New Issue