4747import tempfile
4848from enum import StrEnum , auto
4949from fnmatch import fnmatch
50+ from subprocess import CompletedProcess
51+
52+ from typing import Any , cast
5053
5154class DataType (StrEnum ):
5255 FILE = auto ()
@@ -55,10 +58,10 @@ class DataType(StrEnum):
5558 BROKEN_SYMLINK = auto ()
5659 PACKAGE = auto ()
5760
58- def ignore_file (filename , ignored_files ) :
61+ def ignore_file (filename : str , ignored_files : list [ str ]) -> bool :
5962 return any (fnmatch (filename , i ) for i in ignored_files )
6063
61- def ssh_cmd (host , cmd ) :
64+ def ssh_cmd (host : str , cmd : str ) -> str :
6265 args = ["ssh" , f"root@{ host } " , cmd ]
6366
6467 cmdres = subprocess .run (args , capture_output = True , text = True )
@@ -67,10 +70,10 @@ def ssh_cmd(host, cmd):
6770
6871 return cmdres .stdout
6972
70- def ssh_get_files (host , file_type , folders ) :
73+ def ssh_get_files (host : str , file_type : DataType , folders : list [ str ]) -> dict [ str , str ] | None :
7174 md5sum = False
7275 readlink = False
73- folders = " " .join (folders )
76+ folders_concatenated = " " .join (folders )
7477
7578 match file_type :
7679 case DataType .FILE :
@@ -89,7 +92,7 @@ def ssh_get_files(host, file_type, folders):
8992 print ("Unknown file type: " , file = sys .stderr )
9093 return None
9194
92- find_cmd = f"find { folders } { find_type } "
95+ find_cmd = f"find { folders_concatenated } { find_type } "
9396 if readlink :
9497 find_cmd += " -exec readlink -n {} \\ ; -exec echo -n ' ' \\ ; -print"
9598 elif md5sum :
@@ -99,14 +102,14 @@ def ssh_get_files(host, file_type, folders):
99102
100103 rawres = ssh_cmd (host , find_cmd )
101104
102- res = dict ()
105+ res : dict [ str , str ] = dict ()
103106 for line in rawres .splitlines ():
104107 entry = line .split (' ' , 1 )
105108 res [entry [1 ].strip ()] = entry [0 ].strip ()
106109
107110 return res
108111
109- def ssh_get_packages (host ) :
112+ def ssh_get_packages (host : str ) -> dict [ str , str ] :
110113 packages = dict ()
111114
112115 res = ssh_cmd (host , "rpm -qa --queryformat '%{NAME} %{VERSION}\n '" )
@@ -116,8 +119,8 @@ def ssh_get_packages(host):
116119
117120 return packages
118121
119- def get_data (host , folders ) :
120- ref_data = dict ()
122+ def get_data (host : str , folders : list [ str ]) -> dict [ DataType , dict [ str , str ] | None ] :
123+ ref_data : dict [ DataType , dict [ str , str ] | None ] = dict ()
121124
122125 try :
123126 ref_data [DataType .FILE ] = ssh_get_files (host , DataType .FILE , folders )
@@ -131,7 +134,7 @@ def get_data(host, folders):
131134
132135 return ref_data
133136
134- def sftp_get (host , remote_file , local_file ) :
137+ def sftp_get (host : str , remote_file : str , local_file : str ) -> CompletedProcess [ bytes ] :
135138 opts = '-o "StrictHostKeyChecking no" -o "LogLevel ERROR" -o "UserKnownHostsFile /dev/null"'
136139
137140 args = f"sftp { opts } -b - root@{ host } "
@@ -150,7 +153,7 @@ def sftp_get(host, remote_file, local_file):
150153
151154 return res
152155
153- def remote_diff (host_ref , host_test , filename ) :
156+ def remote_diff (host_ref : str , host_test : str , filename : str ) -> None :
154157 file_ref = None
155158 file_test = None
156159 try :
@@ -189,7 +192,7 @@ def remote_diff(host_ref, host_test, filename):
189192 if file_test is not None and os .path .exists (file_test ):
190193 os .remove (file_test )
191194
192- def print_results (results , show_diff , show_ignored ) :
195+ def print_results (results : dict [ str , Any ], show_diff : bool , show_ignored : bool ) -> None :
193196 # Print what differs
194197 for dtype in DataType :
195198 if dtype == DataType .PACKAGE :
@@ -213,11 +216,13 @@ def print_results(results, show_diff, show_ignored):
213216 for f in results ['ignored_files' ]:
214217 print (f"{ f } " )
215218
216- def compare_data (ref , test , ignored_file_patterns ):
219+ def compare_data (
220+ ref : dict [str , Any ], test : dict [str , Any ], ignored_file_patterns : list [str ]
221+ ) -> tuple [dict [str , Any ], int ]:
217222 ref_data = ref ['data' ]
218223 test_data = test ['data' ]
219224 err = 0
220- results = {
225+ results : dict [ str | DataType , Any ] = {
221226 'host' : {
222227 'ref' : ref ['host' ],
223228 'test' : test ['host' ]
@@ -282,27 +287,27 @@ def compare_data(ref, test, ignored_file_patterns):
282287 return results , err
283288
284289# Load a previously saved json file containing reference data
285- def load_reference_files (filename ) :
290+ def load_reference_files (filename : str ) -> dict [ DataType , dict [ str , str ] | None ] :
286291 try :
287292 with open (filename , 'r' ) as fd :
288- return json .load (fd )
293+ return cast ( dict [ DataType , dict [ str , str ] | None ], json .load (fd ) )
289294 except Exception as e :
290295 print (f"Error: { e } " , file = sys .stderr )
291296 exit (- 1 )
292297
293298# Save files from a reference host in json format
294- def save_reference_data (files , filename ) :
299+ def save_reference_data (files : dict [ DataType , dict [ str , str ] | None ], filename : str ) -> None :
295300 try :
296301 with open (filename , 'w' ) as fd :
297302 json .dump (files , fd , indent = 4 )
298303 except Exception as e :
299304 print (f"Error: { e } " , file = sys .stderr )
300305 exit (- 1 )
301306
302- def main ():
303- ref_data = None
304- folders = ["/boot" , "/etc" , "/opt" , "/usr" ]
305- ignored_file_patterns = [
307+ def main () -> int :
308+ ref_data : dict [ DataType , dict [ str , str ] | None ] | None = None
309+ folders : list [ str ] = ["/boot" , "/etc" , "/opt" , "/usr" ]
310+ ignored_file_patterns : list [ str ] = [
306311 '/boot/initrd-*' ,
307312 '/boot/efi/*' ,
308313 '/boot/grub/*' ,
@@ -361,13 +366,13 @@ def main():
361366 ]
362367
363368 parser = argparse .ArgumentParser (description = 'Spot filesystem differences between 2 XCP-ng hosts' )
364- parser .add_argument ('--reference-host' , '-r' , dest = 'ref_host' ,
369+ parser .add_argument ('--reference-host' , '-r' , dest = 'ref_host' , type = str ,
365370 help = 'The XCP-ng host used as reference' )
366- parser .add_argument ('--test-host' , '-t' , dest = 'test_host' ,
371+ parser .add_argument ('--test-host' , '-t' , dest = 'test_host' , type = str ,
367372 help = 'The XCP-ng host to be tested after install or upgrade' )
368- parser .add_argument ('--save-reference' , '-s' , dest = 'save_ref' ,
373+ parser .add_argument ('--save-reference' , '-s' , dest = 'save_ref' , type = str ,
369374 help = 'Save filesystem information of the reference host to a file' )
370- parser .add_argument ('--load-reference' , '-l' , dest = 'load_ref' ,
375+ parser .add_argument ('--load-reference' , '-l' , dest = 'load_ref' , type = str ,
371376 help = 'Load reference filesystem information from a file' )
372377 parser .add_argument ('--show-diff' , '-d' , action = 'store_true' , dest = 'show_diff' ,
373378 help = 'Show diff of text files that differ. A reference host must be supplied with -r' )
@@ -412,8 +417,8 @@ def main():
412417 print (f"Get test host data from { args .test_host } " )
413418 test_data = get_data (args .test_host , args .folders )
414419
415- ref = dict ([('data' , ref_data ), ('host' , args .ref_host )])
416- test = dict ([('data' , test_data ), ('host' , args .test_host )])
420+ ref : dict [ str , Any ] = dict ([('data' , ref_data ), ('host' , args .ref_host )])
421+ test : dict [ str , Any ] = dict ([('data' , test_data ), ('host' , args .test_host )])
417422
418423 results , err = compare_data (ref , test , args .ignored_file_patterns )
419424
0 commit comments