163 lines
5.1 KiB
Python
163 lines
5.1 KiB
Python
# Copyright (c) 2018 Red Hat, Inc.
|
|
#
|
|
# This file is part of ARA Records Ansible.
|
|
#
|
|
# 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 ara import models
|
|
from oslo_serialization import jsonutils
|
|
from oslo_utils import encodeutils
|
|
from sqlalchemy import func
|
|
|
|
import hashlib
|
|
import pyfakefs.fake_filesystem as fake_filesystem
|
|
|
|
|
|
def generate_identifier(result):
|
|
"""
|
|
Returns a fixed length identifier based on a hash of a combined set of
|
|
playbook/task values which are as close as we can guess to unique for each
|
|
task.
|
|
"""
|
|
# Determine the playbook file path to use for the ID
|
|
if result.task.playbook and result.task.playbook.path:
|
|
playbook_file = result.task.playbook.path
|
|
else:
|
|
playbook_file = ''
|
|
play_path = u'%s.%s' % (playbook_file, result.task.play.name)
|
|
|
|
# Determine the task file path to use for the ID
|
|
if result.task.file and result.task.file.path:
|
|
task_file = result.task.file.path
|
|
else:
|
|
task_file = ''
|
|
task_path = u'%s.%s' % (task_file, result.task.name)
|
|
|
|
# Combine both of the above for a full path
|
|
identifier_path = u'%s.%s' % (play_path, task_path)
|
|
|
|
# Assign the identifier as a hash of the fully unique path.
|
|
identifier = hashlib.sha1(encodeutils.to_utf8(identifier_path)).hexdigest()
|
|
|
|
return identifier
|
|
|
|
|
|
def get_summary_stats(items, attr):
|
|
"""
|
|
Returns a dictionary of aggregated statistics for 'items' filtered by
|
|
"attr'. For example, it will aggregate statistics for a host across all
|
|
the playbook runs it has been a member of, with the following structure:
|
|
|
|
data[host.id] = {
|
|
'ok': 4
|
|
'changed': 4
|
|
...
|
|
}
|
|
"""
|
|
data = {}
|
|
for item in items:
|
|
stats = models.Stats.query.filter_by(**{attr: item.id})
|
|
data[item.id] = {
|
|
'ok': sum([int(stat.ok) for stat in stats]),
|
|
'changed': sum([int(stat.changed) for stat in stats]),
|
|
'failed': sum([int(stat.failed) for stat in stats]),
|
|
'skipped': sum([int(stat.skipped) for stat in stats]),
|
|
'unreachable': sum([int(stat.unreachable) for stat in stats])
|
|
}
|
|
|
|
# If we're aggregating stats for a playbook, also infer status
|
|
if attr is "playbook_id":
|
|
data[item.id]['status'] = _infer_status(item, data[item.id])
|
|
|
|
return data
|
|
|
|
|
|
def _infer_status(playbook, playbook_stats):
|
|
"""
|
|
Infer the status of a playbook run based on it's statistics or completion
|
|
"""
|
|
if not playbook.complete:
|
|
return 'incomplete'
|
|
|
|
if playbook_stats['failed'] >= 1 or playbook_stats['unreachable'] >= 1:
|
|
return 'failed'
|
|
else:
|
|
return 'success'
|
|
|
|
|
|
def fast_count(query):
|
|
"""
|
|
It turns out the built-in SQLAlchemy function for query.count() is pretty
|
|
slow. Alternatively, use this optimized function instead.
|
|
"""
|
|
count_query = (query
|
|
.statement.with_only_columns([func.count()]).order_by(None))
|
|
count = query.session.execute(count_query).scalar()
|
|
return count
|
|
|
|
|
|
def generate_tree(root, paths, mock_os):
|
|
"""
|
|
Given a file path, returns a JSON structure suitable for bootstrap-treeview
|
|
mock_os represents a faked filesystem generated from the playbook_treeview
|
|
method.
|
|
Credit: Mohammed Naser & David Moreau Simard
|
|
"""
|
|
tree = []
|
|
dentries = mock_os.listdir(root)
|
|
|
|
for d in dentries:
|
|
full_path = mock_os.path.join(root, d)
|
|
node = {
|
|
'text': d,
|
|
'href': '#%s' % d,
|
|
'state': {
|
|
'expanded': True
|
|
}
|
|
}
|
|
|
|
if mock_os.path.isdir(full_path):
|
|
node['nodes'] = generate_tree(full_path, paths, mock_os)
|
|
else:
|
|
node['icon'] = 'fa fa-file-code-o'
|
|
node['href'] = '#'
|
|
node['color'] = '#0088ce'
|
|
node['dataAttr'] = {
|
|
'toggle': 'modal',
|
|
'target': '#file_modal',
|
|
'load': paths[full_path] + '/'
|
|
}
|
|
tree.append(node)
|
|
return tree
|
|
|
|
|
|
def playbook_treeview(playbook):
|
|
"""
|
|
Creates a fake filesystem with playbook files and uses generate_tree() to
|
|
recurse and return a JSON structure suitable for bootstrap-treeview.
|
|
"""
|
|
fs = fake_filesystem.FakeFilesystem()
|
|
mock_os = fake_filesystem.FakeOsModule(fs)
|
|
|
|
files = models.File.query.filter(models.File.playbook_id.in_([playbook]))
|
|
|
|
paths = {}
|
|
for file in files:
|
|
fs.CreateFile(file.path)
|
|
paths[file.path] = file.id
|
|
|
|
return jsonutils.dumps(generate_tree('/', paths, mock_os),
|
|
sort_keys=True,
|
|
indent=2)
|