Skip to content

Commit

Permalink
Add a filesystem check script
Browse files Browse the repository at this point in the history
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
tescande committed Dec 5, 2023
1 parent 80556b1 commit 25d544a
Showing 1 changed file with 232 additions and 0 deletions.
232 changes: 232 additions & 0 deletions scripts/xcpng-fs-check.py
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())

0 comments on commit 25d544a

Please sign in to comment.