# @copyright (c) 2002-2016 Acronis International GmbH. All rights reserved.
"""Acronis System Info Report Utility."""
import glob, os, sys
import argparse
import subprocess
import shutil
import hashlib
import sqlite3
class _SysInfoReportBase():
# Base class for sys-info reports
_PLATFORM_LINUX, _PLATFORM_WIN, _PLATFORM_MACOS = range(3)
def __init__(self, platform, report_dir, cmd_name):
# Arguments
# - platform: (optional) one of _PLATFORM_* constants
# - report_dir: path to report directory
# - cmd_name: name of report command (f.e. 'collect_configs')
# Used for diagnostic purposes only
self.cmd_name = cmd_name
self.report_dir = os.path.abspath(report_dir)
self.platform = platform if platform is not None else self._detect_platform()
@classmethod
def _detect_platform(cls):
if sys.platform.startswith('win32'):
return cls._PLATFORM_WIN
elif sys.platform.startswith('linux'):
return cls._PLATFORM_LINUX
elif sys.platform.startswith('darwin'):
return cls._PLATFORM_MACOS
assert False, "Unexpected sys.platform name: {}".format(sys.platform)
def _get_install_paths(self):
# get list of Acronis installation locations
if self.platform == self._PLATFORM_LINUX:
self._ETC_DIR = "/etc/Acronis"
self._USR_LIB_DIR = "/usr/lib/Acronis"
self._VAR_LIB_DIR = "/var/lib/Acronis"
self._OPT_DIR = "/opt/acronis"
return [self._ETC_DIR, self._USR_LIB_DIR, self._VAR_LIB_DIR, self._OPT_DIR]
elif self.platform == self._PLATFORM_WIN:
return self._get_install_paths_windows()
else:
self._ACRONIS_DIR = "/Library/Application Support/Acronis"
return [
self._ACRONIS_DIR,
"/Library/Application Support/BackupClient",
"/Library/Logs/Acronis"
]
def _get_install_paths_windows(self):
# on windows product installation path should be taken from registry
import acrobind
import acrort
install_paths = set([])
brand_name = acrort.common.BRAND_NAME
for path_id in ('COMMONPROGRAMFILES', 'COMMONPROGRAMFILES(x86)',
'PROGRAMDATA', 'ALLUSERSPROFILE'):
# paths like "C:\Program Files\Common Files\Acronis"
#
# %PROGRAMDATA% and %ALLUSERSPROFILE% reference the
# same dir: usually "C:\ProgramData". But one of these variables
# may be not present depending on Windows version.
if path_id in os.environ:
install_paths.add(os.path.join(os.environ[path_id], brand_name))
key_path = r"SOFTWARE\{}\Installer".format(brand_name)
val_name = "TargetDir"
product_install_path = acrobind.registry_read_string(key_path, val_name)
if product_install_path:
install_paths.add(product_install_path)
else:
print(
"Warning: Processing '{0}' report command. "
"Product installation dir not found in registry. "
"key_path: {1}, val_name {2}".format(self.cmd_name, key_path, val_name))
return sorted(install_paths)
@staticmethod
def _dump_sqlite3_db(db_path, output_csv, exclude_tables_list):
with sqlite3.connect(db_path) as conn:
cursor = conn.cursor()
tables = cursor.execute("SELECT name FROM sqlite_master WHERE type='table';").fetchall()
if exclude_tables_list is None:
table_names = [table[0] for table in tables]
else:
table_names = [table[0] for table in tables if table[0] not in exclude_tables_list]
with open(output_csv, "w", encoding='utf-8') as csvfile:
for table_name in table_names:
rows = cursor.execute("SELECT * FROM {};".format(table_name)).fetchall()
headers = cursor.execute("PRAGMA table_info('{}');".format(table_name)).fetchall()
header_names = [header[1] for header in headers]
csvfile.write("Table: {}\n".format(table_name))
csvfile.write(",".join(header_names) + "\r\n")
for row in rows:
csvfile.write(",".join(map(str, row)) + "\r\n")
@staticmethod
def _iter_files(top_dir, ignore_dirs, file_extentions=[], ignore_files=[]):
# recursively yield (dir_name, file_name) for files from specified directory
#
# Arguments:
# - top_dir: top-level directory to yield files from
# - ignore_dirs: ignore files in this dir (usefull if report directory
# is inside top_dir or when you want to skip the "mount" dir where
# backups are mounted).
# - file_extentions: (optional) only yield files matching the extentions
# - ignore_files: (optional) ignore files ending with the given paths
for i in range(len(ignore_dirs)):
ignore_dirs[i] = os.path.normpath(ignore_dirs[i])
for i in range(len(ignore_files)):
ignore_files[i] = os.path.normpath(ignore_files[i])
for dir_name, _sub_dirs, file_names in os.walk(top_dir):
if ignore_dirs and \
any(os.path.commonpath([dir_name, ignore_dir]) == ignore_dir
for ignore_dir in ignore_dirs):
continue
for file_name in file_names:
if file_extentions:
if not any(file_name.endswith(ext) for ext in file_extentions):
continue
if ignore_files:
if any(os.path.join(dir_name, file_name).endswith(ignore_file) for ignore_file in ignore_files):
continue
yield (dir_name, file_name)
#########################
# collect conf files
class _CollectConfigFilesReport(_SysInfoReportBase):
# inclde all the Acronis configuration files into the report
def run_report(self):
configs_report_subdir = os.path.join(self.report_dir, "configs")
file_extentions = [".config", ".cfg", ".conf", ".xml", ".json", ".ini", ".yml", ".yaml", ".db"]
ignore_files = [
"ml_analysis.xml",
"AccessVault/config/preferred.json",
"MMS/user.config",
"Agent/var/credentials-store/credentials_store.db",
"Agent/var/atp-downloader/index.db",
"Agent/var/atp-agent/va_pm_db.db",
"Agent/var/atp-agent/va_pm_db_in_use.db"]
install_paths = self._get_install_paths()
always_ignore_relative_dirs = ["/atp-downloader/Cache"]
for i in range(len(always_ignore_relative_dirs)):
always_ignore_relative_dirs[i] = os.path.normpath(always_ignore_relative_dirs[i])
src_2_tgt_dirs = {} # {conf_file_dir: dir_in_report}
ignore_dirs = [self.report_dir]
if self.platform == self._PLATFORM_LINUX:
ignore_dirs.append(os.path.join(self._VAR_LIB_DIR, "mount"))
ignore_dirs.append(os.path.join(self._VAR_LIB_DIR, "NGMP"))
ignore_dirs.extend(glob.glob(os.path.join(self._VAR_LIB_DIR, "sysinfo*")))
if self.platform == self._PLATFORM_MACOS:
ignore_dirs.extend(glob.glob(os.path.join(self._ACRONIS_DIR, "sysinfo*")))
for top_dir in install_paths:
for dir_name, file_name in self._iter_files(top_dir, ignore_dirs, file_extentions, ignore_files):
if any(os.path.normpath(rel_dir) in dir_name for rel_dir in always_ignore_relative_dirs):
continue
if dir_name not in src_2_tgt_dirs:
src_2_tgt_dirs[dir_name] = self._make_tgt_dir_for_configs_report(
dir_name, configs_report_subdir)
tgt_dir = src_2_tgt_dirs[dir_name]
tgt_file = os.path.join(tgt_dir, file_name)
src_file = os.path.join(dir_name, file_name)
if file_name.endswith("account_server.db"):
tgt_file = tgt_file + '.txt'
tables_2_exclude = [
'clients',
'rsa_keys',
'keys_table',
'__client_old',
'backup_servers',
'identity_providers',
'identities',
'refresh_tokens',
'opaque_tokens']
acc_srv_dir = os.path.join(self.report_dir, "AccountServer")
if not os.path.exists(acc_srv_dir):
os.mkdir(acc_srv_dir)
self._dump_sqlite3_db(src_file, os.path.join(acc_srv_dir, "db_dump.txt"), tables_2_exclude)
elif file_name.endswith("api_gateway.json"):
if self.platform in (self._PLATFORM_LINUX, self._PLATFORM_MACOS):
os.system('grep -vwE "{0}" "{1}" > "{2}"'.format('passphrase', src_file, tgt_file))
else:
os.system('findstr -V "{0}" "{1}" > "{2}"'.format('passphrase', src_file, tgt_file))
elif file_name.endswith("Global.config"):
if self.platform in (self._PLATFORM_LINUX, self._PLATFORM_MACOS):
os.system('grep -vwE "{0}" "{1}" > "{2}"'.format('(Username|Password)', src_file, tgt_file))
else:
shutil.copy(src_file, tgt_file)
def _make_tgt_dir_for_configs_report(self, config_dir_name, configs_report_subdir):
# returns abs path of dir in the report to copy the config file to.
# Create the dir if not exist yet.
if self.platform in (self._PLATFORM_LINUX, self._PLATFORM_MACOS):
tgt_file_rel_path = os.path.relpath(config_dir_name, "/")
else: # self.platform == _PLATFORM_WIN
drive = os.path.splitdrive(config_dir_name)[0] # "C:"
drive = os.path.join(drive, os.sep) # "C:\\"
tgt_file_rel_path = os.path.relpath(config_dir_name, drive)
tgt_file_location = os.path.join(configs_report_subdir, tgt_file_rel_path)
os.makedirs(tgt_file_location, exist_ok=True)
return tgt_file_location
#########################
# report Acronis files hashes
class _CollectFileHashes(_SysInfoReportBase):
# calculate hashes of all the Acronis files
def run_report(self):
no_hash_for_exts = [".log", ]
with open(os.path.join(self.report_dir, "file_hashes.txt"), "w+") as out_file:
for file_path in self._iter_installed_files():
skip_hash = (
any(file_path.endswith(ext) for ext in no_hash_for_exts)
or not os.path.isfile(file_path))
if skip_hash:
hexdigest = "n/a"
else:
with open(file_path, "rb") as file_data:
hexdigest = hashlib.md5(file_data.read()).hexdigest()
out_file.write("{0}\t{1}\n".format(file_path, hexdigest))
def _iter_installed_files(self):
# yields all the files in Acronis installation directories
ignore_dirs = [self.report_dir]
if self.platform == self._PLATFORM_LINUX:
ignore_dirs.append(os.path.join(self._VAR_LIB_DIR, "mount"))
ignore_dirs.append(os.path.join(self._VAR_LIB_DIR, "NGMP"))
ignore_dirs.extend(glob.glob(os.path.join(self._VAR_LIB_DIR, "sysinfo*")))
if self.platform == self._PLATFORM_MACOS:
ignore_dirs.extend(glob.glob(os.path.join(self._ACRONIS_DIR, "sysinfo*")))
for top_loc in self._get_install_paths():
for dir_name, file_name in self._iter_files(top_loc, ignore_dirs,
ignore_files=[".pyc", ]):
yield os.path.join(dir_name, file_name)
#########################
# report netstat
class _CollectNetstat(_SysInfoReportBase):
# just report 'netstat -a' output
def run_report(self):
rep_file_path = os.path.join(self.report_dir, "netstat.txt")
options = "-nab"
if self.platform == self._PLATFORM_LINUX:
if os.path.isfile("/bin/acronis"):
options = "-na"
else:
options = "-nap"
netstat_executable = shutil.which("netstat")
if netstat_executable is not None and len(netstat_executable) > 0:
with open(rep_file_path, "w+") as outfile:
subprocess.call([netstat_executable, options], stdout=outfile)
#########################
# common functionality
_REPORT_CLASSES = {
'collect_configs': _CollectConfigFilesReport,
# Disable hash collection because perfomance degradation ABR-121489: Collecting sysinfo loads 100% CPU and woks too long ~ 5 min
# 'collect_filehashes': _CollectFileHashes,
'netstat': _CollectNetstat,
}
def _parse_arguments():
parser = argparse.ArgumentParser(
description=("Part of Acronis sysinfo utility. "
"!!! Not intended to be executed directly !!!"))
parser.add_argument(
"-o", "--output-dir",
dest="output_dir",
help=("(optional) Path to output report directory. "
"Default is current directory."))
platform_names = {
'linux': _SysInfoReportBase._PLATFORM_LINUX,
'macos': _SysInfoReportBase._PLATFORM_MACOS,
'win': _SysInfoReportBase._PLATFORM_WIN}
parser.add_argument(
"-p", "--platform",
dest="platform_name",
choices=sorted(platform_names.keys()))
parser.add_argument(
"--optimized",
dest="optimized",
default=False,
action='store_true',
help='(optional) Optimize data collection.')
parser.add_argument(
"--days ",
dest="days",
type=int,
help='(optional) Collect data for the last <DAYS> prior to current date.')
parser.add_argument(
"commands", nargs='*', metavar='command',
choices=[[]] + sorted(_REPORT_CLASSES.keys()),
help=("(optional) Data collection command. "
"If not specified all commands will be executed."))
args = parser.parse_args()
if args.days and args.days < 1:
raise argparse.ArgumentTypeError("{0} days number is wrong. Minimum days number is 1.".format(args.days))
platform = platform_names.get(args.platform_name)
output_dir = args.output_dir if args.output_dir is not None else os.getcwd()
commands_to_execute = args.commands if args.commands else sorted(_REPORT_CLASSES.keys())
return platform, output_dir, commands_to_execute
if __name__ == '__main__':
platform, output_dir, commands_to_execute = _parse_arguments()
for cmd_name in commands_to_execute:
try:
cmd_report = _REPORT_CLASSES[cmd_name](platform, output_dir, cmd_name)
cmd_report.run_report()
except:
print("Warning: error processing '{0}' report command.".format(cmd_name))
import traceback
traceback.print_exc(file=sys.stdout)
|