
Subcloud collect bundles have an extra level of directory heirarchy. This update refactors the report.py bundle search and extraction handling to support both single and multi host and subcloud collect bundles. Typical used is report.py <bundle pointer option> /path/to/bundle Bundle pointer options --bundle Use this option to point to a 'directory' that 'contains' host tarball files. --directory Use this option when a collect bundle 'tar file' is in a in a specific 'directory'. --file Use this option to point to a specific collect bundle tar file to analyze. The following additional changes / improvements were made: - improved report.py code structure - improved management of the input and output dirs - improved debug and error logging (new --state option) - removed --clean option that can fail due to bundle file permissions - added --bundle option to support pointing to a directory containing a set of host tarballs. - modified collect to use the new --bundle option when --report option is used. - implement tool logfile migration from /tmp to bundle output_dir - create report_analysis dir in final output_dir only - fix file permissions to allow execution from git - order plugin analysis output based on size - added additional error checking and handling Test Plan: PASS: Verify collect --report (std system, AIO and subcloud) PASS: Verify report analysis PASS: Verify report run on-system, git and cached copy PASS: Verify on and off system analysis of PASS: ... single-host collect bundle with --file option PASS: ... multi-host collect bundle with --file option PASS: ... single-subcloud collect bundle with --file option PASS: ... multi-subcloud collect bundle with --file option PASS: ... single-host collect bundle with --directory option PASS: ... multi-host collect bundle with --directory option PASS: ... single-subcloud collect bundle with --directory option PASS: ... multi-subcloud collect bundle with --directory option PASS: ... single-host collect bundle with --bundle option PASS: ... multi-host collect bundle with --bundle option PASS: Verify --directory option handling when PASS: ... there are multiple bundles to select from (pass) PASS: ... there are is a bundle without the date_time (prompt) PASS: ... there are extra non-bundle files in target dir (ignore) PASS: ... the target dir only contains host tarballs (fail) PASS: ... the target dir has no tar files or extracted bundle (fail) PASS: ... the target dir does not exist (fail) PASS: Verify --bundle option handling when PASS: ... there are host tarballs in the target directory (pass) PASS: ... there are only extracted host dirs in target dir (pass) PASS: ... there are no host tarballs or dirs in target dir (fail) PASS: ... the target dir does not have a dated host dir (fail) PASS: ... the target dir does not exist (fail) PASS: ... the target is a file rather than a dir (fail) PASS: Verify --file option handling when PASS: ... the target tar file is found (pass) PASS: ... the target tar file is not date_time named (prompt) PASS: ... the target tar file does not exists (fail) PASS: ... the target tar is not a collect bundle (fail) PASS: Verify tar file(s) in a single and multi-subcloud collect with the --report option each include a report analysis. PASS: Verify logging with and without --debug and --state options PASS: Verify error handling when no -b, -f or -d option is specified Story: 2010533 Task: 48187 Change-Id: I4924034aa27577f94e97928265c752c204a447c7 Signed-off-by: Eric MacDonald <eric.macdonald@windriver.com>
905 lines
33 KiB
Python
Executable File
905 lines
33 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
########################################################################
|
|
#
|
|
# Copyright (c) 2022 - 2023 Wind River Systems, Inc.
|
|
#
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
#
|
|
########################################################################
|
|
#
|
|
# Description: The Report tool is used to gather relevant log, events
|
|
# and information about the system from a collect bundle
|
|
# and present that data for quick / easy issue analysis.
|
|
#
|
|
# Overview:
|
|
#
|
|
# The report tool implements an 'Execution Engine' runs 'Algorithms'
|
|
# against 'Plugins' to gather logs, events & system information that
|
|
# the 'Correlator' analyzes to produce a summary of events, alarms,
|
|
# state changes and failures found by the plugins.
|
|
#
|
|
# Report Tool: report.py
|
|
#
|
|
# Parses command line arguments, sets up logging, extracts the top
|
|
# level collect bundle, initializes the execution engine, loads the
|
|
# plugins and invokes the execution engine.
|
|
#
|
|
# Execution Engine: execution_engine.py
|
|
#
|
|
# Initialization extracts the bundle's host tarballs, finds the
|
|
# active controller and host types from each tarball. When executed
|
|
# runs the algorithms specified by each of the loaded plugins and
|
|
# then calls the correlator.
|
|
#
|
|
# Correlator: correlator.py
|
|
#
|
|
# Analyzes the data and logs gathered by the plugins and produces
|
|
# and displays a report_analysis that contains a summary of:
|
|
#
|
|
# - Alarms ... when and what alarms were found to have occurred
|
|
# - Events ... noteworthy events; Graceful Recovery, MNFA
|
|
# - Failures ... host or service management failures; swacts
|
|
# - State ... summary of host state changes; enable -> disable
|
|
#
|
|
# Algorithms: algorithms.py
|
|
#
|
|
# The report tool supports a set of built-in algorithms used to
|
|
# gather collect bundle events, logs and data.
|
|
#
|
|
# The following algorithms in 'plugin_algs' directory are supported:
|
|
#
|
|
# - audit.py ............. counts dcmanager audit events
|
|
# - alarm.py ............. summarizes alarm state transitions and when
|
|
# - heartbeat_loss.py ..... gathers maintenance heartbeat failures
|
|
# - daemon_failures.py .... gathers various common daemon log errors
|
|
# - maintenance_errors.py . gathers maintenance error logs
|
|
# - puppet_errors.py ...... gathers puppet failures and logs
|
|
# - state_changes.py ...... gathers a summary of host state changes
|
|
# - swact_activity.py ..... identifies various swact occurrences
|
|
# - process_failures.py ... gathers pmond process failure logs
|
|
# - substring.py ....... gathers substring plugin specified info
|
|
# - system_info.py ........ gathers system info ; type, mode, etc
|
|
#
|
|
# Plugins: plugins.py
|
|
#
|
|
# Plugins are small label based text files that specify an algorithm
|
|
# and other applicable labels used to find specific data, logs or
|
|
# events for that plugin.
|
|
#
|
|
# The following default internal plugins are automatically included
|
|
# with the report tool stored in the 'plugins' directory.
|
|
#
|
|
# - alarm ............ specifies alarms to look for
|
|
# - audit .............. find dcmanager audit events
|
|
# - daemon_failures ....... runs the daemon failure algorithm
|
|
# - heartbeat_loss ........ runs the mtce heartbeat loss algorithm
|
|
# - maintenance_errors .... find specific maintenance logs
|
|
# - process_failures ...... find pmon or sm process failures
|
|
# - puppet_errors ......... find configuration failure puppet logs
|
|
# - state_changes ......... find host state changes
|
|
# - substring ............. find logs containing named substrings
|
|
# - swact_activity ........ find swact failure and events
|
|
# - system_info ........... gather system information
|
|
#
|
|
# The report tool will also run additional (optional) user defined
|
|
# plugins developed and placed in the localhost's filesystem at
|
|
# /etc/collect/plugins.
|
|
#
|
|
# Typical Usage:
|
|
#
|
|
# command line functionality
|
|
# ------------------------------- ----------------------------------
|
|
# > report.py --help - help message
|
|
# > report.py -b /path/to/bundle - path to dir containing host tarballs
|
|
# > report.py -d /path/to/bundle - path to dir containing tar bundle(s)
|
|
# > report.py -f /path/to/bundle.tar - specify path to a bundle tar file
|
|
# > report.py -d <dir> [plugin ...] - Run only specified plugins
|
|
# > report.py -d <dir> <algs> [labels]- Run algorithm with labels
|
|
# > report.py <algorithm> --help - algorithm specific help
|
|
#
|
|
# See --help output for a complete list of full and abbreviated
|
|
# command line options and examples of plugins.
|
|
#
|
|
# Refer to README file for more usage and output examples
|
|
#######################################################################
|
|
|
|
import argparse
|
|
from datetime import datetime
|
|
from datetime import timedelta
|
|
from datetime import timezone
|
|
import logging
|
|
import os
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tarfile
|
|
import tempfile
|
|
import time
|
|
|
|
from execution_engine import ExecutionEngine
|
|
from plugin import Plugin
|
|
|
|
# Globals
|
|
now = datetime.now(timezone.utc)
|
|
report_dir = os.path.dirname(os.path.realpath(__file__))
|
|
analysis_folder_name = "report_analysis"
|
|
plugins = []
|
|
output_dir = None
|
|
tmp_report_log = tempfile.mkstemp()
|
|
|
|
|
|
parser = argparse.ArgumentParser(
|
|
description="Report Tool:",
|
|
epilog="Analyzes data collected by the plugins and produces a "
|
|
"report_analysis stored with the collect bundle. The report tool "
|
|
"can be run either on or off system by specifying the bundle to "
|
|
"analyze using the --directory, --bundle or --file command options.",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--debug",
|
|
action="store_true",
|
|
help="Enable debug logs",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--bundle", "-b",
|
|
default="",
|
|
required=False,
|
|
help="Specify the full path to a directory containing a collect "
|
|
"bundle to analyze. Use this option when pointing to a directory "
|
|
"with host .tgz files that are already extracted from a tar file.",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--directory", "-d",
|
|
default="",
|
|
required=False,
|
|
help="Specify the full path to a directory containing collect "
|
|
"bundles to analyze.",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--file", "-f",
|
|
default="",
|
|
required=False,
|
|
help="Specify the path to and filename of the tar bundle to analyze",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--end", "-e",
|
|
default=datetime.strftime(now + timedelta(days=1), "%Y%m%d"),
|
|
help="Specify an end date in YYYYMMDD format for analysis "
|
|
"(default: current date)",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--hostname",
|
|
default="all",
|
|
help="Specify host for correlator to find significant events and "
|
|
"state changes for (default: all hosts)",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--plugin", "-p",
|
|
default=None,
|
|
nargs="*",
|
|
help="Specify comma separated list of plugins to run "
|
|
"(default: runs all found plugins)",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--start", "-s",
|
|
default="20000101",
|
|
help="Specify a start date in YYYYMMDD format for analysis "
|
|
"(default:20000101)",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--state",
|
|
action="store_true",
|
|
help="Debug option to dump object state during execution",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--verbose", "-v",
|
|
action="store_true",
|
|
help="Enable verbose output",
|
|
)
|
|
|
|
subparsers = parser.add_subparsers(help="algorithms", dest="algorithm")
|
|
|
|
# substring algorithm arguments
|
|
parser_substring = subparsers.add_parser(
|
|
"substring",
|
|
formatter_class=argparse.RawTextHelpFormatter,
|
|
help="""Searches through specified files for lines containing specified
|
|
substring. There will be an output file for each host of the host
|
|
type specified.""",
|
|
epilog="Plugin file example:\n"
|
|
" algorithm=substring\n"
|
|
" files=var/log/mtcAgent.log, var/log/sm.log\n"
|
|
" hosts=controllers\n"
|
|
" substring=operation failed\n"
|
|
" substring=Failed to send message",
|
|
)
|
|
substring_required = parser_substring.add_argument_group("required arguments")
|
|
substring_required.add_argument(
|
|
"--files",
|
|
required=True,
|
|
nargs="+",
|
|
help="Files to perform substring analysis on (required)",
|
|
)
|
|
substring_required.add_argument(
|
|
"--substring", nargs="+", required=True,
|
|
help="Substrings to search for (required)"
|
|
)
|
|
substring_required.add_argument(
|
|
"--hosts",
|
|
choices=["controllers", "workers", "storages", "all"],
|
|
required=True,
|
|
nargs="+",
|
|
help="Host types to perform analysis on (required)",
|
|
)
|
|
|
|
|
|
# alarm algorithm arguments
|
|
parser_alarm = subparsers.add_parser(
|
|
"alarm",
|
|
formatter_class=argparse.RawTextHelpFormatter,
|
|
help="Searches through fm.db.sql.txt for alarms and logs except for those "
|
|
"specified. There are 2 output files: 'alarm', and 'log'",
|
|
epilog="Plugin file example:\n"
|
|
" algorithm=alarm\n"
|
|
" alarm_exclude=400., 800.\n"
|
|
" entity_exclude=subsystem=vim\n",
|
|
)
|
|
parser_alarm.add_argument(
|
|
"--alarm_exclude",
|
|
nargs="+",
|
|
required=False,
|
|
default=[],
|
|
help="Alarm id patterns to not search for (not required)",
|
|
)
|
|
parser_alarm.add_argument(
|
|
"--entity_exclude",
|
|
nargs="+",
|
|
required=False,
|
|
default=[],
|
|
help="Entity id patterns to not search for (not required)",
|
|
)
|
|
|
|
# system info algorithm
|
|
parser_system_info = subparsers.add_parser(
|
|
"system_info",
|
|
formatter_class=argparse.RawTextHelpFormatter,
|
|
help="Presents information about the system",
|
|
epilog="Plugin file example:\n" " algorithm=system_info\n",
|
|
)
|
|
|
|
# swact activity algorithm
|
|
parser_swact_activity = subparsers.add_parser(
|
|
"swact_activity",
|
|
formatter_class=argparse.RawTextHelpFormatter,
|
|
help="Presents system swacting activity",
|
|
epilog="Plugin file example:\n" " algorithm=swact_activity\n",
|
|
)
|
|
|
|
# puppet errors algorithm
|
|
parser_puppet_errors = subparsers.add_parser(
|
|
"puppet_errors",
|
|
formatter_class=argparse.RawTextHelpFormatter,
|
|
help="Presents any puppet errors",
|
|
epilog="Plugin file example:\n" " algorithm=puppet_errors\n",
|
|
)
|
|
|
|
# process failures algorithm
|
|
parser_process_failures = subparsers.add_parser(
|
|
"process_failures",
|
|
formatter_class=argparse.RawTextHelpFormatter,
|
|
help="Presents any process failures from pmond.log",
|
|
epilog="Plugin file example:\n" " algorithm=process_failures\n",
|
|
)
|
|
|
|
# daemon failures algorithm
|
|
parser_daemon_failures = subparsers.add_parser(
|
|
"daemon_failures",
|
|
formatter_class=argparse.RawTextHelpFormatter,
|
|
help="Presents any puppet manifest failures from daemon.log",
|
|
epilog="Plugin file example:\n" " algorithm=daemon_failures\n",
|
|
)
|
|
|
|
# heartbeat loss algorithm
|
|
parser_heartbeat_loss = subparsers.add_parser(
|
|
"heartbeat_loss",
|
|
formatter_class=argparse.RawTextHelpFormatter,
|
|
help="Presents any heartbeat loss error messages from hbsAgent.log",
|
|
epilog="Plugin file example:\n" " algorithm=heartbeat_loss\n",
|
|
)
|
|
|
|
# maintenance errors algorithm
|
|
parser_maintenance_errors = subparsers.add_parser(
|
|
"maintenance_errors",
|
|
formatter_class=argparse.RawTextHelpFormatter,
|
|
help="Presents errors and other relevant messages from mtcAgent.log and "
|
|
"mtcClient.log",
|
|
epilog="Plugin file example:\n" " algorithm=maintenance_errors\n",
|
|
)
|
|
|
|
# state changes algorithm
|
|
parser_state_changes = subparsers.add_parser(
|
|
"state_changes",
|
|
formatter_class=argparse.RawTextHelpFormatter,
|
|
help="Presents any messages from mtcAgent.log regarding the state of "
|
|
"hosts, such as enabled/disabled",
|
|
epilog="Plugin file example:\n" " algorithm=state_changes\n",
|
|
)
|
|
|
|
# audit algorithm
|
|
parser_audit = subparsers.add_parser(
|
|
"audit",
|
|
formatter_class=argparse.RawTextHelpFormatter,
|
|
help="Presents information about audit events in dcmanager.\n"
|
|
"The rates and totals represents the sum of audits on all subclouds ",
|
|
epilog="Plugin file example:\n"
|
|
" algorithm=audit\n"
|
|
" start=2022-06-01 10:00:00\n"
|
|
" end=2022-06-01 04:00:00\n",
|
|
)
|
|
parser_audit.add_argument(
|
|
"--start",
|
|
required=False,
|
|
default=datetime.strftime(now - timedelta(days=7), "%Y-%m-%d %H:%M:%S"),
|
|
type=str,
|
|
help="Specify a start date in YYYY-MM-DD HH:MM:SS format for analysis "
|
|
"(not required, default: 1 week ago)"
|
|
)
|
|
parser_audit.add_argument(
|
|
"--end",
|
|
required=False,
|
|
default=datetime.strftime(now, "%Y-%m-%d %H:%M:%S"),
|
|
type=str,
|
|
help="Specify an end date in YYYY-MM-DD HH:MM:SS format for analysis "
|
|
"(not required, default: today)"
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
args.start = datetime.strptime(args.start, "%Y%m%d").strftime(
|
|
"%Y-%m-%dT%H:%M:%S")
|
|
args.end = datetime.strptime(args.end, "%Y%m%d").strftime("%Y-%m-%dT%H:%M:%S")
|
|
|
|
###########################################################
|
|
# Args error checking
|
|
###########################################################
|
|
if args.file:
|
|
if not os.path.exists(args.file):
|
|
exit_msg = "Error: Specified file (" + args.file + ") does not exist."
|
|
sys.exit(exit_msg)
|
|
elif os.path.isdir(args.file):
|
|
exit_msg = "Error: Specified file (" + args.file + ") is a directory."
|
|
exit_msg += "\nPlease specify the full path to a tar file when using "
|
|
exit_msg += "the --file option.\nOtherwise, use the --directory option"
|
|
exit_msg += " instead."
|
|
sys.exit(exit_msg)
|
|
elif not tarfile.is_tarfile(args.file):
|
|
exit_msg = "Error: Specified file (" + args.file + ") is not a tar "
|
|
exit_msg += "file.\nPlease specify a tar file using the --file option."
|
|
sys.exit(exit_msg)
|
|
else:
|
|
try:
|
|
input_dir = os.path.dirname(args.file)
|
|
output_dir = os.path.join(input_dir, analysis_folder_name)
|
|
subprocess.run(["tar", "xfC", args.file], check=True)
|
|
except subprocess.CalledProcessError as e:
|
|
print(e)
|
|
except PermissionError as e:
|
|
print(e)
|
|
sys.exit("Permission Error: Unable to extract bundle")
|
|
|
|
|
|
elif args.directory:
|
|
# Get the bundle input and report output dirs
|
|
output_dir = os.path.join(args.directory, analysis_folder_name)
|
|
input_dir = os.path.join(args.directory)
|
|
if not os.path.isdir(input_dir):
|
|
sys.exit("Error: Specified input directory is not a directory")
|
|
elif args.bundle:
|
|
output_dir = os.path.join(args.bundle, analysis_folder_name)
|
|
input_dir = os.path.join(args.bundle)
|
|
else:
|
|
exit_msg = "Error: Please use either the --file or --directory option to "
|
|
exit_msg += "specify a\ncollect bundle file or directory containing a "
|
|
exit_msg += "collect bundle file to analyze."
|
|
sys.exit(exit_msg)
|
|
|
|
|
|
###########################################################
|
|
# Setup logging
|
|
###########################################################
|
|
logger = logging.getLogger()
|
|
|
|
|
|
def remove_logging():
|
|
"""Move logging to a different location ; from /tmp to the bundle"""
|
|
|
|
logger = logging.getLogger()
|
|
for handler in logger.handlers[:]:
|
|
logger.removeHandler(handler)
|
|
|
|
|
|
def setup_logging(logfile):
|
|
"""Setup logging"""
|
|
|
|
# setting up logger
|
|
formatter = logging.Formatter("%(message)s")
|
|
|
|
logging.basicConfig(
|
|
filename=logfile,
|
|
level=logging.DEBUG if args.debug else logging.INFO,
|
|
format="%(asctime)s %(levelname)s: %(message)s",
|
|
datefmt="%Y-%m-%dT%H:%M:%S",
|
|
)
|
|
logging.Formatter.converter = time.gmtime
|
|
|
|
console_handler = logging.StreamHandler()
|
|
if args.debug:
|
|
console_handler.setLevel(logging.DEBUG)
|
|
else:
|
|
console_handler.setLevel(logging.INFO)
|
|
console_handler.setFormatter(formatter)
|
|
logger.addHandler(console_handler)
|
|
|
|
|
|
setup_logging(tmp_report_log[1])
|
|
|
|
if args.debug:
|
|
logger.debug("Arguments : %s", args)
|
|
logger.debug("Report Dir: %s", report_dir)
|
|
logger.debug("Input Dir: %s", input_dir)
|
|
logger.debug("Output Dir: %s", output_dir)
|
|
|
|
###########################################################
|
|
# Find and extract the bundle to analyze
|
|
###########################################################
|
|
|
|
|
|
# List of directories to ignore
|
|
ignore_list = [analysis_folder_name]
|
|
ignore_list += ["apps", "horizon", "lighttpd", "lost+found", "sysinv-tmpdir"]
|
|
ignore_list += ["patch-api-proxy-tmpdir", "platform-api-proxy-tmpdir"]
|
|
regex_get_bundle_date = r".*_\d{8}\.\d{6}$"
|
|
|
|
|
|
class BundleObject:
|
|
def __init__(self, input_dir):
|
|
self.input_base_dir = input_dir # the first specified input dir
|
|
self.input_dir = input_dir # current input_dir ; can change
|
|
self.tar_file_found = False # True if <bundle>.tar file present
|
|
self.subcloud_bundle = False # host vs subcloud bundle
|
|
self.bundle_name = None # full path of current bundle
|
|
self.bundle_names = [] # list of bundle names
|
|
self.bundle_info = ["", []] # tarfile bundle info [name,[files]]
|
|
self.bundles = [] # list of bundles
|
|
self.tars = 0 # number of tar files found
|
|
self.tgzs = 0 # number of host tgz files found
|
|
|
|
def debug_state(self, func):
|
|
if args.state:
|
|
logger.debug("State:%10s: input_base_dir : %s",
|
|
func, self.input_base_dir)
|
|
logger.debug("State:%10s: input_dir : %s",
|
|
func, self.input_dir)
|
|
logger.debug("State:%10s: output_dir : %s",
|
|
func, output_dir)
|
|
logger.debug("State:%10s: tar_file_found : %s",
|
|
func, self.tar_file_found)
|
|
logger.debug("State:%10s: subcloud_bundle: %s",
|
|
func, self.subcloud_bundle)
|
|
logger.debug("State:%10s: bundle_name : %s",
|
|
func, self.bundle_name)
|
|
logger.debug("State:%10s: bundle_names : %s",
|
|
func, self.bundle_names)
|
|
logger.debug("State:%10s: bundle_info : %s",
|
|
func, self.bundle_info)
|
|
logger.debug("State:%10s: bundles : %s",
|
|
func, self.bundles)
|
|
logger.debug("State:%10s: tars-n-tgzs : %s:%s",
|
|
func, self.tars, self.tgzs)
|
|
|
|
def update_io_dirs(self, new_dir):
|
|
"""Update the input_dir and output_dir dirs
|
|
|
|
Parameters:
|
|
new_dir (string): path to change input_dir to
|
|
"""
|
|
self.debug_state("get_bundles")
|
|
global output_dir
|
|
if self.input_dir != new_dir:
|
|
str1 = "input_dir change: " + self.input_dir + " -> " + new_dir
|
|
self.input_dir = new_dir
|
|
old_output_dir = output_dir
|
|
output_dir = os.path.join(self.input_dir, analysis_folder_name)
|
|
str2 = "output_dir change: " + old_output_dir + " -> " + output_dir
|
|
else:
|
|
str1 = "input_dir change is null"
|
|
str2 = "output_dir change is null"
|
|
logger.debug(str1)
|
|
logger.debug(str2)
|
|
self.debug_state("update_io_dirs")
|
|
|
|
def get_bundles(self):
|
|
"""Get a list of all collect bundle from input_dir"""
|
|
|
|
self.debug_state("get_bundles")
|
|
logger.debug("get_bundles: %s", self.input_dir)
|
|
for obj in (os.scandir(self.input_dir)):
|
|
# Don't display dirs from the ignore list.
|
|
# This makes the bundle selection list cleaner when
|
|
# report is run against /scratch
|
|
ignored = False
|
|
for ignore in ignore_list:
|
|
if obj.name == ignore:
|
|
ignored = True
|
|
break
|
|
if ignored is True:
|
|
continue
|
|
|
|
if obj.is_dir(follow_symlinks=False):
|
|
date_time = obj.name[-15:]
|
|
if args.debug:
|
|
logger.debug("found dir : %s : %s", obj.name, date_time)
|
|
elif os.path.islink(obj.path):
|
|
# ignore sym links
|
|
continue
|
|
else:
|
|
if not tarfile.is_tarfile(obj.path):
|
|
continue
|
|
filename = os.path.splitext(obj.name)[0]
|
|
date_time = filename[-15:]
|
|
if args.debug:
|
|
logger.debug("found file: %s : %s", obj.name, date_time)
|
|
|
|
# Add this bundle to the list. Avoid duplicates
|
|
found = False
|
|
name = obj.name
|
|
if obj.name.endswith('.tar'):
|
|
self.tar_file_found = True
|
|
name = os.path.splitext(obj.name)[0]
|
|
if obj.name.endswith('.tgz'):
|
|
continue
|
|
for bundle in self.bundles:
|
|
if bundle == name:
|
|
found = True
|
|
break
|
|
if found is False:
|
|
if re.match(regex_get_bundle_date, name):
|
|
self.bundles.append(name)
|
|
self.bundle_names.append(name)
|
|
elif not obj.is_dir(follow_symlinks=False):
|
|
logger.info("unexpected bundle name '%s'", name)
|
|
logger.info("... collect bundles name should include "
|
|
"'_YYYYMMDD.HHMMSS'")
|
|
select = str(input('accept as bundle (Y/N): '))
|
|
if select[0] == 'Y' or select[0] == 'y':
|
|
self.bundles.append(name)
|
|
self.bundle_names.append(name)
|
|
else:
|
|
logger.warning("not a bundle")
|
|
|
|
if args.debug:
|
|
logger.debug("bundles %2d: %s", len(self.bundles), self.bundles)
|
|
logger.debug("bundle sel: %s", self.bundle_names)
|
|
self.debug_state("get_bundles")
|
|
|
|
def get_bundle(self):
|
|
"""Get a list of all collect bundles from input_dir
|
|
|
|
Parameters:
|
|
input_dir (string): path to the directory to analyze
|
|
"""
|
|
self.debug_state("get_bundle")
|
|
logger.debug("get_bundle %s", self.input_dir)
|
|
|
|
if self.tar_file_found is False:
|
|
# If a collect bundle .tar file is not found then treat this
|
|
# case as though the input_dir is a hosts tarball directory
|
|
# like would be seen when running report on the system during
|
|
# the collect operation.
|
|
logger.debug("get_bundle tar file not found")
|
|
self.bundle_name = self.input_dir
|
|
|
|
elif len(self.bundles) > 1:
|
|
retry = True
|
|
while retry is True:
|
|
logger.info("0 - exit")
|
|
idx = 1
|
|
for bundle in self.bundle_names:
|
|
if bundle.endswith(('.tar', '.tgz', '.gz')):
|
|
logger.info("%d - %s",
|
|
idx, os.path.splitext(bundle)[0])
|
|
else:
|
|
logger.info("%d - %s", idx, bundle)
|
|
idx += 1
|
|
try:
|
|
select = int(input('Please select bundle to analyze: '))
|
|
except ValueError:
|
|
logger.info("Invalid input; integer between 1 "
|
|
"and %d required", len(self.bundles))
|
|
continue
|
|
if not select:
|
|
sys.exit()
|
|
if select <= len(self.bundles):
|
|
idx = 0
|
|
for bundle in self.bundle_names:
|
|
if idx == select-1:
|
|
logger.info("%s selected", bundle)
|
|
self.bundle_name = bundle
|
|
break
|
|
else:
|
|
idx += 1
|
|
retry = False
|
|
else:
|
|
logger.info("Invalid selection (%s) idx=%d",
|
|
select, idx)
|
|
# single bundle found
|
|
else:
|
|
self.bundle_name = self.bundle_names[0]
|
|
logger.debug("bundle name: %s", self.bundle_name)
|
|
self.debug_state("get_bundle")
|
|
|
|
def get_bundle_info(self, bundle):
|
|
"""Returns a list containing the tar file content
|
|
|
|
This is required for cases where the name of the supplied
|
|
tar file extracts its contents to a directory that is not
|
|
the same (without the extension) as the original tar file
|
|
|
|
Returns:
|
|
bundle_info (list): the bundle info [ "dir", [ files ]]
|
|
bundle_info[0] ( string) 'directory' found in tar file
|
|
bundle_info[1] (list) a list of files found in 'directory'
|
|
"""
|
|
self.debug_state("get_bundle_info")
|
|
|
|
bundle_tar = os.path.join(self.input_dir, self.bundle_name) + ".tar"
|
|
logger.debug("get_bundle_info %s", bundle_tar)
|
|
|
|
if not os.path.exists(bundle_tar):
|
|
logger.error("Error: No collect tar bundle found: %s", bundle_tar)
|
|
sys.exit()
|
|
try:
|
|
result = subprocess.run(["tar", "tf", bundle_tar],
|
|
stdout=subprocess.PIPE)
|
|
output = result.stdout.decode('utf-8').splitlines()
|
|
logger.debug("... bundle info: %s", output)
|
|
except subprocess.CalledProcessError as e:
|
|
logger.error(e)
|
|
except subprocess.PermissionError as e:
|
|
logger.error(e)
|
|
|
|
if output != []:
|
|
for item in output:
|
|
dir, file = item.split("/", 1)
|
|
if dir is None:
|
|
continue
|
|
if self.bundle_info[0] == "":
|
|
self.bundle_info[0] = dir
|
|
if self.bundle_info[0] != dir:
|
|
logger.warning("ignoring unexpected extra directory "
|
|
"only one directory permitted in a "
|
|
"collect bundle ; %s is != %s",
|
|
self.bundle_info[0], dir)
|
|
continue
|
|
elif file.endswith(('.tar')):
|
|
logger.debug("tar contains tar: %s", file)
|
|
self.bundle_info[1].append(file)
|
|
elif file.endswith(('.tgz')):
|
|
logger.debug("tar contains tgz: %s", file)
|
|
if self.bundle_info[0] is None:
|
|
self.bundle_info[0] = dir
|
|
self.bundle_info[1].append(file)
|
|
else:
|
|
if self.bundle_info[0] is None:
|
|
self.bundle_info[0] = dir
|
|
if file:
|
|
self.bundle_info[1].append(file)
|
|
self.debug_state("get_bundle_info")
|
|
|
|
def extract_bundle(self):
|
|
"""Extract bundle if not already extracted"""
|
|
|
|
logger.debug("bundle name: %s", self.bundle_name)
|
|
|
|
# extract the bundle if not already extracted
|
|
bundle_tar = os.path.join(self.input_dir, self.bundle_name) + ".tar"
|
|
if os.path.exists(bundle_tar):
|
|
if not os.access(self.input_dir, os.W_OK):
|
|
logger.error("Permission Error: Bundle dir not writable: %s",
|
|
self.input_dir)
|
|
sys.exit("Collect bundle must be writable for analysis.")
|
|
try:
|
|
|
|
logger.info("extracting %s", bundle_tar)
|
|
untar_data = subprocess.run(
|
|
["tar", "xfC", bundle_tar, self.input_dir],
|
|
check=True, stdout=subprocess.PIPE)
|
|
logger.debug(untar_data)
|
|
except subprocess.CalledProcessError as e:
|
|
logger.error(e)
|
|
except PermissionError as e:
|
|
logger.error(e)
|
|
sys.exit("Permission Error: Unable to extract bundle")
|
|
|
|
elif args.debug:
|
|
logger.debug("already extracted: %s", bundle_tar)
|
|
|
|
def get_bundle_type(self):
|
|
"""Determine the bundle type ; host or subcloud
|
|
|
|
Subcloud bundles contain one or more tar files rather
|
|
than tgz files ; at this level.
|
|
|
|
However rather than fail the report if both are found,
|
|
which is unlikely, the code favors treating as a normal
|
|
host bundle with the tgz check first.
|
|
"""
|
|
if self.tgzs:
|
|
self.extract_bundle()
|
|
self.bundle_name = os.path.join(self.input_dir,
|
|
self.bundle_info[0])
|
|
logger.debug("Host bundle: %s", self.bundle_name)
|
|
elif self.tars:
|
|
self.extract_bundle()
|
|
self.bundle_name = os.path.join(self.input_dir,
|
|
self.bundle_info[0])
|
|
self.subcloud_bundle = True
|
|
logger.debug("Subcloud bundle: %s", self.bundle_name)
|
|
else:
|
|
sys.exit("Error: bundle contains no .tar files")
|
|
|
|
self.update_io_dirs(self.bundle_name)
|
|
if self.subcloud_bundle is True:
|
|
# clear current bundle lists, etc. in prep for the
|
|
# selected subcloud bundle
|
|
self.bundle_names = []
|
|
self.bundles = []
|
|
self.bundle_name = None
|
|
self.tar_file_found = False
|
|
|
|
# get the subcloud bundle(s) and select one
|
|
# if more than one is present.
|
|
self.get_bundles()
|
|
if self.bundles:
|
|
self.get_bundle()
|
|
|
|
# handle the no bundles found case ; unlikely
|
|
if self.bundle_name is None:
|
|
sys.exit("No valid collect subcloud bundles found.")
|
|
|
|
# extract the subcloud bundle if needed
|
|
self.extract_bundle()
|
|
|
|
# add the full path to the bundle name.
|
|
# can't use self.bundle_info[0] because that is the
|
|
# bundle name that contians the subcloud tars
|
|
self.bundle_name = os.path.join(self.input_dir, self.bundle_name)
|
|
|
|
# update the input directory to point top the subcloud folder
|
|
self.update_io_dirs(self.bundle_name)
|
|
|
|
self.debug_state("get_bundle_type")
|
|
|
|
|
|
# Initialize the Bundle Object. Logging starts in /tmp
|
|
obj = BundleObject(input_dir)
|
|
with open(os.path.join(output_dir, tmp_report_log[1]), "a") as logfile:
|
|
|
|
obj.debug_state("init")
|
|
|
|
if args.bundle:
|
|
logger.info("Bundle: %s", args.bundle)
|
|
obj.input_dir = input_dir
|
|
|
|
elif args.file:
|
|
# Note: The args.file has already been validated at this point.
|
|
basename = os.path.splitext(os.path.basename(args.file))
|
|
if re.match(regex_get_bundle_date, basename[0]):
|
|
obj.bundles.append(basename[0])
|
|
obj.bundle_names.append(basename[0])
|
|
obj.tar_file_found = True
|
|
else:
|
|
logger.info("unexpected bundle name '%s'", basename[0])
|
|
logger.info("... collect bundles name should include "
|
|
"'_YYYYMMDD.HHMMSS'")
|
|
select = str(input('accept as bundle (Y/N): '))
|
|
if select[0] == 'Y' or select[0] == 'y':
|
|
obj.bundles.append(basename[0])
|
|
obj.bundle_names.append(basename[0])
|
|
obj.tar_file_found = True
|
|
else:
|
|
sys.exit("rejected ; exiting ...")
|
|
else:
|
|
# get the bundles
|
|
obj.get_bundles()
|
|
|
|
if not args.bundle:
|
|
if obj.bundles:
|
|
obj.get_bundle()
|
|
|
|
# handle the no bundles found case
|
|
if obj.bundle_name is None:
|
|
sys.exit("No valid collect host bundles found.")
|
|
|
|
obj.get_bundle_info(obj.bundle_name)
|
|
logger.debug("bundle info: %s", obj.bundle_info)
|
|
for file in obj.bundle_info[1]:
|
|
if file.endswith(('.tar')):
|
|
logger.debug("bundle tar file: %s", file)
|
|
obj.tars += 1
|
|
elif file.endswith(('.tgz')):
|
|
logger.debug("bundle tgz file: %s", file)
|
|
obj.tgzs += 1
|
|
|
|
if not args.bundle:
|
|
obj.get_bundle_type()
|
|
|
|
# now that the output directory is established create the analysis
|
|
# folder, move the existing log files there and record the untar data
|
|
if not os.path.exists(output_dir):
|
|
try:
|
|
result = os.makedirs(output_dir, exist_ok=True)
|
|
except PermissionError as e:
|
|
logger.error(e)
|
|
sys.exit("Permission Error: Unable to create report")
|
|
|
|
# relocate logging to the selected bundle directory
|
|
remove_logging()
|
|
new_log_file = output_dir + "/report.log"
|
|
shutil.move(tmp_report_log[1], new_log_file)
|
|
setup_logging(new_log_file)
|
|
|
|
logger.info("")
|
|
logger.info("Report: %s ", output_dir)
|
|
logger.info("")
|
|
|
|
# initialize the execution engine
|
|
try:
|
|
engine = ExecutionEngine(args, obj.input_dir, output_dir)
|
|
except ValueError as e:
|
|
logger.error(str(e))
|
|
logger.error("Confirm you are running the report tool on a collect bundle")
|
|
|
|
if args.algorithm:
|
|
plugins.append(Plugin(opts=vars(args)))
|
|
elif args.plugin:
|
|
for p in args.plugin:
|
|
path = os.path.join(report_dir, "plugins", p)
|
|
if os.path.exists(path):
|
|
try:
|
|
plugins.append(Plugin(path))
|
|
except Exception as e:
|
|
logger.error(str(e))
|
|
|
|
else:
|
|
logger.warning("%s plugin does not exist", p)
|
|
else:
|
|
# load builtin plugins
|
|
builtin_plugins = os.path.join(report_dir, "plugins")
|
|
if os.path.exists(builtin_plugins):
|
|
for file in os.listdir(builtin_plugins):
|
|
plugins.append(Plugin(os.path.join(builtin_plugins, file)))
|
|
logger.debug("loading built-in plugin: %s", file)
|
|
|
|
# add localhost plugins
|
|
localhost_plugins = os.path.join("/etc/collect", "plugins")
|
|
if os.path.exists(localhost_plugins):
|
|
for file in os.listdir(localhost_plugins):
|
|
plugins.append(Plugin(os.path.join(localhost_plugins, file)))
|
|
logger.debug("loading localhost plugin: %s", file)
|
|
|
|
# analyze the collect bundle
|
|
engine.execute(plugins, output_dir)
|
|
|
|
sys.exit()
|