-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This script checks for file system tree differences between 2 XCP-ng hosts. The comparison can be made against a reference installation (let say a fresh ISO install) and another XCP-ng of the same version but obtained by upgrading an existing installation, or to check differences between ISO and Net installations. Supported options are: --reference-host REF_HOST, -r REF_HOST The XCP-ng host used as reference --test-host TEST_HOST, -t TEST_HOST The XCP-ng host to be tested after install or upgrade --save-reference SAVE_REF, -s SAVE_REF Save filesystem information of the reference host to a file --load-reference LOAD_REF, -l LOAD_REF Load reference filesystem information from a file Option -r HOST is used to pass the hostname or ip address of the XCP-ng host used as reference. With -s FILENAME, the reference host filesyistem information is saved to the file FILENAME passed as parameter. Instead of -r HOST, one can use the reference file created with -s by using the option -f FILENAME. The host to be tested is specified with the option -t HOST. Signed-off-by: Thierry Escande <[email protected]>
- Loading branch information
Showing
1 changed file
with
232 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,232 @@ | ||
#!/usr/bin/env python3 | ||
|
||
# | ||
# Data structure format: | ||
# | ||
# The files to be checked are stored in Python dictionnaries with 4 main entries. | ||
# - 'file' entry for standard files and the file md5 sum | ||
# - 'file_symlink', valid symlinks to standard files and the file md5 sum | ||
# - 'dir_symlink', valid symlinks to directory and the directory pointed at | ||
# - 'broken_symlink', broken symlinks to directory and the file pointed at | ||
# | ||
# { | ||
# "files": { | ||
# "/opt/pbis/docs/pbis-open-installation-and-administration-guide.pdf": "7f9eaec58fd3422b79c88126cefdf503", | ||
# "/opt/pbis/docs/pbis-quick-start-guide-for-linux.pdf": "8bbd04e4c73cedefeeb35ed74a8a3d4b", | ||
# ... | ||
# }, | ||
# "file_symlinks": { | ||
# "/opt/pbis/lib/libldap-2.4.so.2": "a417e1e86cec9735a76059b72d8e1cbf", | ||
# "/opt/pbis/lib/liblsaclient_ntlm.so.0": "9601bfc4827f0b9cff50cc591b2b6f11", | ||
# ... | ||
# }, | ||
# "dir_symlinks": { | ||
# "/usr/tmp": "../var/tmp", | ||
# "/usr/lib/kbd/keymaps/legacy/ppc": "mac", | ||
# ... | ||
# }, | ||
# "broken_symlinks": { | ||
# "/usr/lib/firmware/a300_pfp.fw": "qcom/a300_pfp.fw", | ||
# "/usr/lib/firmware/a300_pm4.fw": "qcom/a300_pm4.fw", | ||
# ... | ||
# } | ||
# } | ||
# | ||
|
||
import argparse | ||
import sys | ||
import subprocess | ||
import json | ||
from fnmatch import fnmatch | ||
from enum import Enum | ||
|
||
class FileType(Enum): | ||
FILE=0 | ||
FILE_SYMLINK=1 | ||
DIR_SYMLINK=2 | ||
BROKEN_SYMLINK=3 | ||
|
||
def ignore_file(filename): | ||
ignored_files = [ | ||
'/boot/initrd-*', | ||
'/boot/grub/*', | ||
'/boot/vmlinuz-fallback', | ||
'/boot/xen-fallback.gz', | ||
'/etc/chrony.conf', | ||
'/etc/firstboot.d/data/default-storage.conf', | ||
'/etc/firstboot.d/data/iqn.conf', | ||
'/etc/fstab', | ||
'/etc/group*', | ||
'/etc/grub.cfg', | ||
'/etc/gshadow*', | ||
'/etc/hostname', | ||
'/etc/iscsi/initiatorname.iscsi', | ||
'/etc/issue', | ||
'/etc/krb5.conf', | ||
'/etc/lvm/backup/*', | ||
'/etc/mtab', | ||
'/etc/machine-id', | ||
'/etc/passwd*', | ||
'/etc/pki/ca-trust/extracted/java/cacerts', | ||
'/etc/pki/java/cacerts', | ||
'/etc/shadow*', | ||
'/etc/ssh/ssh_host_*_key.pub', | ||
'/etc/ssh/ssh_host_*_key', | ||
'/etc/sysconfig/network', | ||
'/etc/sysconfig/network-scripts/interface-rename-data/*', | ||
'/etc/sysconfig/xencommons', | ||
'/etc/vconsole.conf', | ||
'/etc/xensource-inventory', | ||
'/etc/xensource/boot_time_cpus', | ||
'/etc/xensource/ptoken', | ||
'/etc/xensource/xapi-ssl.pem', | ||
'/opt/xensource/gpg/trustdb.gpg', | ||
] | ||
|
||
for i in ignored_files: | ||
if fnmatch(filename, i): | ||
return True | ||
|
||
return False | ||
|
||
def ssh_get_files(host, file_type): | ||
md5sum = False | ||
readlink = False | ||
folders = "/boot /etc /opt /usr" | ||
|
||
match file_type: | ||
case FileType.FILE: | ||
find_type = "-type f" | ||
md5sum = True | ||
case FileType.FILE_SYMLINK: | ||
find_type = "-type l -xtype f" | ||
md5sum = True | ||
case FileType.DIR_SYMLINK: | ||
find_type = "-type l -xtype d" | ||
readlink = True | ||
case FileType.BROKEN_SYMLINK: | ||
find_type = "-xtype l" | ||
readlink = True | ||
case _: | ||
print("Unknown file type: ") | ||
return None | ||
|
||
find_cmd = "find {} {}".format(folders, find_type) | ||
if readlink: | ||
find_cmd += " -exec readlink -n {} \; -exec echo -n ' ' \; -print" | ||
|
||
args = [ "ssh", "root@{}".format(host), find_cmd] | ||
|
||
# This will make one call to md5sum with all files passed as parameter | ||
# This is much more efficient than using find '-exec md5sum {}' | ||
if md5sum: | ||
args += [ "|", "xargs", "md5sum" ] | ||
|
||
cmdres = subprocess.run(args, capture_output=True, text=True) | ||
if cmdres.returncode: | ||
raise Exception(cmdres.stderr) | ||
|
||
res = dict() | ||
for line in cmdres.stdout.splitlines(): | ||
entry = line.split() | ||
res[entry[1]] = entry[0] | ||
|
||
return res | ||
|
||
def get_files(host): | ||
ref_files = dict() | ||
|
||
try: | ||
ref_files['file'] = ssh_get_files(host, FileType.FILE) | ||
ref_files['file_symlink'] = ssh_get_files(host, FileType.FILE_SYMLINK) | ||
ref_files['dir_symlink'] = ssh_get_files(host, FileType.DIR_SYMLINK) | ||
ref_files['broken_symlink'] = ssh_get_files(host, FileType.BROKEN_SYMLINK) | ||
except Exception as e: | ||
print(e) | ||
exit(-1) | ||
|
||
return ref_files | ||
|
||
def compare_files(ref_files, test_files): | ||
for ftype in test_files: | ||
for file in test_files[ftype]: | ||
if ignore_file(file): | ||
continue | ||
|
||
if not file in ref_files[ftype]: | ||
print("{} doesn't exist on reference host: {}".format(ftype, file)) | ||
continue | ||
|
||
if ref_files[ftype][file] != test_files[ftype][file]: | ||
print("{} differs: {}".format(ftype, file)) | ||
|
||
ref_files[ftype][file] = None | ||
|
||
# Check for files that only exist on the reference host | ||
for ftype in ref_files: | ||
for file, val in ref_files[ftype].items(): | ||
if ignore_file(file): | ||
continue | ||
|
||
if val is not None: | ||
print("{} doesn't exist on tested host: {}".format(ftype, file)) | ||
|
||
# Load a previously saved json file containing a the reference files | ||
def load_reference_files(filename): | ||
try: | ||
with open(filename, 'r') as fd: | ||
return json.load(fd) | ||
except Exception as e: | ||
print("Error: {}".format(e)) | ||
exit(-1) | ||
|
||
# Save files from a reference host in json format | ||
def save_reference_files(files, filename): | ||
try: | ||
with open(filename, 'w') as fd: | ||
json.dump(files, fd, indent=4) | ||
except Exception as e: | ||
print("Error: {}".format(e)) | ||
exit(-1) | ||
|
||
def main(): | ||
ref_files = None | ||
|
||
parser = argparse.ArgumentParser(description='Spot filesystem differences between 2 XCP-ng hosts') | ||
parser.add_argument('--reference-host', '-r', dest='ref_host', | ||
help='The XCP-ng host used as reference') | ||
parser.add_argument('--test-host', '-t', dest='test_host', | ||
help='The XCP-ng host to be tested after install or upgrade') | ||
parser.add_argument('--save-reference', '-s', dest='save_ref', | ||
help='Save filesystem information of the reference host to a file') | ||
parser.add_argument('--load-reference', '-l', dest='load_ref', | ||
help='Load reference filesystem information from a file') | ||
|
||
args = parser.parse_args(sys.argv[1:]) | ||
|
||
if args.load_ref: | ||
print("Loading reference files from {}".format(args.load_ref)) | ||
ref_files = load_reference_files(args.load_ref) | ||
elif args.ref_host: | ||
print("Loading reference files from {}".format(args.ref_host)) | ||
ref_files = get_files(args.ref_host) | ||
|
||
if args.save_ref: | ||
print("Saving reference files to {}".format(args.save_ref)) | ||
save_reference_files(ref_files, args.save_ref) | ||
return 0 | ||
|
||
if ref_files is None or args.test_host is None: | ||
print("\nMissing parameters. Try --help\n", file=sys.stderr) | ||
return -1 | ||
|
||
print("Loading host files from {}".format(args.test_host)) | ||
test_files = get_files(args.test_host) | ||
|
||
print("\nResults:") | ||
compare_files(ref_files, test_files) | ||
|
||
return 0 | ||
|
||
if __name__ == '__main__': | ||
exit(main()) |