270 lines
10 KiB
Python
270 lines
10 KiB
Python
import inspect
|
|
import json
|
|
import logging
|
|
import os
|
|
import os.path
|
|
import tempfile
|
|
import shutil
|
|
import requests
|
|
|
|
from django.conf import settings
|
|
from django.utils.timezone import now, timedelta
|
|
from rest_framework.exceptions import PermissionDenied
|
|
|
|
from awx.conf.license import get_license
|
|
from awx.main.models import Job
|
|
from awx.main.access import access_registry
|
|
from awx.main.utils import get_awx_http_client_headers, set_environ
|
|
|
|
__all__ = ['register', 'gather', 'ship']
|
|
|
|
|
|
logger = logging.getLogger('awx.main.analytics')
|
|
|
|
|
|
def _valid_license():
|
|
try:
|
|
if get_license().get('license_type', 'UNLICENSED') == 'open':
|
|
return False
|
|
access_registry[Job](None).check_license()
|
|
except PermissionDenied:
|
|
logger.exception("A valid license was not found:")
|
|
return False
|
|
return True
|
|
|
|
|
|
def all_collectors():
|
|
from awx.main.analytics import collectors
|
|
|
|
collector_dict = {}
|
|
module = collectors
|
|
for name, func in inspect.getmembers(module):
|
|
if inspect.isfunction(func) and hasattr(func, '__awx_analytics_key__'):
|
|
key = func.__awx_analytics_key__
|
|
desc = func.__awx_analytics_description__ or ''
|
|
version = func.__awx_analytics_version__
|
|
collector_dict[key] = { 'name': key, 'version': version, 'description': desc}
|
|
return collector_dict
|
|
|
|
|
|
def expensive_collectors():
|
|
from awx.main.analytics import collectors
|
|
|
|
ret = []
|
|
module = collectors
|
|
for name, func in inspect.getmembers(module):
|
|
if inspect.isfunction(func) and hasattr(func, '__awx_analytics_key__') and func.__awx_expensive__:
|
|
ret.append(func.__awx_analytics_key__)
|
|
return ret
|
|
|
|
|
|
def register(key, version, description=None, format='json', expensive=False):
|
|
"""
|
|
A decorator used to register a function as a metric collector.
|
|
|
|
Decorated functions should do the following based on format:
|
|
- json: return JSON-serializable objects.
|
|
- csv: write CSV data to a filename named 'key'
|
|
|
|
@register('projects_by_scm_type', 1)
|
|
def projects_by_scm_type():
|
|
return {'git': 5, 'svn': 1}
|
|
"""
|
|
|
|
def decorate(f):
|
|
f.__awx_analytics_key__ = key
|
|
f.__awx_analytics_version__ = version
|
|
f.__awx_analytics_description__ = description
|
|
f.__awx_analytics_type__ = format
|
|
f.__awx_expensive__ = expensive
|
|
return f
|
|
|
|
return decorate
|
|
|
|
|
|
def gather(dest=None, module=None, subset = None, since = None, until = now(), collection_type='scheduled'):
|
|
"""
|
|
Gather all defined metrics and write them as JSON files in a .tgz
|
|
|
|
:param dest: the (optional) absolute path to write a compressed tarball
|
|
:param module: the module to search for registered analytic collector
|
|
functions; defaults to awx.main.analytics.collectors
|
|
"""
|
|
def _write_manifest(destdir, manifest):
|
|
path = os.path.join(destdir, 'manifest.json')
|
|
with open(path, 'w', encoding='utf-8') as f:
|
|
try:
|
|
json.dump(manifest, f)
|
|
except Exception:
|
|
f.close()
|
|
os.remove(f.name)
|
|
logger.exception("Could not generate manifest.json")
|
|
|
|
last_run = since or settings.AUTOMATION_ANALYTICS_LAST_GATHER or (now() - timedelta(weeks=4))
|
|
logger.debug("Last analytics run was: {}".format(settings.AUTOMATION_ANALYTICS_LAST_GATHER))
|
|
|
|
if _valid_license() is False:
|
|
logger.exception("Invalid License provided, or No License Provided")
|
|
return None
|
|
|
|
if collection_type != 'dry-run' and not settings.INSIGHTS_TRACKING_STATE:
|
|
logger.error("Automation Analytics not enabled. Use --dry-run to gather locally without sending.")
|
|
return None
|
|
|
|
collector_list = []
|
|
if module:
|
|
collector_module = module
|
|
else:
|
|
from awx.main.analytics import collectors
|
|
collector_module = collectors
|
|
for name, func in inspect.getmembers(collector_module):
|
|
if (
|
|
inspect.isfunction(func) and
|
|
hasattr(func, '__awx_analytics_key__') and
|
|
(not subset or name in subset)
|
|
):
|
|
collector_list.append((name, func))
|
|
|
|
manifest = dict()
|
|
dest = dest or tempfile.mkdtemp(prefix='awx_analytics')
|
|
gather_dir = os.path.join(dest, 'stage')
|
|
os.mkdir(gather_dir, 0o700)
|
|
num_splits = 1
|
|
for name, func in collector_list:
|
|
if func.__awx_analytics_type__ == 'json':
|
|
key = func.__awx_analytics_key__
|
|
path = '{}.json'.format(os.path.join(gather_dir, key))
|
|
with open(path, 'w', encoding='utf-8') as f:
|
|
try:
|
|
json.dump(func(last_run, collection_type=collection_type, until=until), f)
|
|
manifest['{}.json'.format(key)] = func.__awx_analytics_version__
|
|
except Exception:
|
|
logger.exception("Could not generate metric {}.json".format(key))
|
|
f.close()
|
|
os.remove(f.name)
|
|
elif func.__awx_analytics_type__ == 'csv':
|
|
key = func.__awx_analytics_key__
|
|
try:
|
|
files = func(last_run, full_path=gather_dir, until=until)
|
|
if files:
|
|
manifest['{}.csv'.format(key)] = func.__awx_analytics_version__
|
|
if len(files) > num_splits:
|
|
num_splits = len(files)
|
|
except Exception:
|
|
logger.exception("Could not generate metric {}.csv".format(key))
|
|
|
|
if not manifest:
|
|
# No data was collected
|
|
logger.warning("No data from {} to {}".format(last_run, until))
|
|
shutil.rmtree(dest)
|
|
return None
|
|
|
|
# Always include config.json if we're using our collectors
|
|
if 'config.json' not in manifest.keys() and not module:
|
|
from awx.main.analytics import collectors
|
|
config = collectors.config
|
|
path = '{}.json'.format(os.path.join(gather_dir, config.__awx_analytics_key__))
|
|
with open(path, 'w', encoding='utf-8') as f:
|
|
try:
|
|
json.dump(collectors.config(last_run), f)
|
|
manifest['config.json'] = config.__awx_analytics_version__
|
|
except Exception:
|
|
logger.exception("Could not generate metric {}.json".format(key))
|
|
f.close()
|
|
os.remove(f.name)
|
|
shutil.rmtree(dest)
|
|
return None
|
|
|
|
stage_dirs = [gather_dir]
|
|
if num_splits > 1:
|
|
for i in range(0, num_splits):
|
|
split_path = os.path.join(dest, 'split{}'.format(i))
|
|
os.mkdir(split_path, 0o700)
|
|
filtered_manifest = {}
|
|
shutil.copy(os.path.join(gather_dir, 'config.json'), split_path)
|
|
filtered_manifest['config.json'] = manifest['config.json']
|
|
suffix = '_split{}'.format(i)
|
|
for file in os.listdir(gather_dir):
|
|
if file.endswith(suffix):
|
|
old_file = os.path.join(gather_dir, file)
|
|
new_filename = file.replace(suffix, '')
|
|
new_file = os.path.join(split_path, new_filename)
|
|
shutil.move(old_file, new_file)
|
|
filtered_manifest[new_filename] = manifest[new_filename]
|
|
_write_manifest(split_path, filtered_manifest)
|
|
stage_dirs.append(split_path)
|
|
|
|
for item in list(manifest.keys()):
|
|
if not os.path.exists(os.path.join(gather_dir, item)):
|
|
manifest.pop(item)
|
|
_write_manifest(gather_dir, manifest)
|
|
|
|
tarfiles = []
|
|
try:
|
|
for i in range(0, len(stage_dirs)):
|
|
stage_dir = stage_dirs[i]
|
|
# can't use isoformat() since it has colons, which GNU tar doesn't like
|
|
tarname = '_'.join([
|
|
settings.SYSTEM_UUID,
|
|
until.strftime('%Y-%m-%d-%H%M%S%z'),
|
|
str(i)
|
|
])
|
|
tgz = shutil.make_archive(
|
|
os.path.join(os.path.dirname(dest), tarname),
|
|
'gztar',
|
|
stage_dir
|
|
)
|
|
tarfiles.append(tgz)
|
|
except Exception:
|
|
shutil.rmtree(stage_dir, ignore_errors = True)
|
|
logger.exception("Failed to write analytics archive file")
|
|
finally:
|
|
shutil.rmtree(dest, ignore_errors = True)
|
|
return tarfiles
|
|
|
|
|
|
def ship(path):
|
|
"""
|
|
Ship gathered metrics to the Insights API
|
|
"""
|
|
if not path:
|
|
logger.error('Automation Analytics TAR not found')
|
|
return
|
|
if not os.path.exists(path):
|
|
logger.error('Automation Analytics TAR {} not found'.format(path))
|
|
return
|
|
if "Error:" in str(path):
|
|
return
|
|
try:
|
|
logger.debug('shipping analytics file: {}'.format(path))
|
|
url = getattr(settings, 'AUTOMATION_ANALYTICS_URL', None)
|
|
if not url:
|
|
logger.error('AUTOMATION_ANALYTICS_URL is not set')
|
|
return
|
|
rh_user = getattr(settings, 'REDHAT_USERNAME', None)
|
|
rh_password = getattr(settings, 'REDHAT_PASSWORD', None)
|
|
if not rh_user:
|
|
return logger.error('REDHAT_USERNAME is not set')
|
|
if not rh_password:
|
|
return logger.error('REDHAT_PASSWORD is not set')
|
|
with open(path, 'rb') as f:
|
|
files = {'file': (os.path.basename(path), f, settings.INSIGHTS_AGENT_MIME)}
|
|
s = requests.Session()
|
|
s.headers = get_awx_http_client_headers()
|
|
s.headers.pop('Content-Type')
|
|
with set_environ(**settings.AWX_TASK_ENV):
|
|
response = s.post(url,
|
|
files=files,
|
|
verify="/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem",
|
|
auth=(rh_user, rh_password),
|
|
headers=s.headers,
|
|
timeout=(31, 31))
|
|
# Accept 2XX status_codes
|
|
if response.status_code >= 300:
|
|
return logger.exception('Upload failed with status {}, {}'.format(response.status_code,
|
|
response.text))
|
|
finally:
|
|
# cleanup tar.gz
|
|
if os.path.exists(path):
|
|
os.remove(path)
|