@@ -6,9 +6,12 @@ import shutil
66import subprocess
77import sys
88import tempfile
9+ import time
10+ from multiprocessing import Process
911
1012from dataclasses import dataclass
1113from enum import IntEnum
14+ import guestfs
1215from typing import Callable
1316
1417sys .path .insert (0 , os .path .dirname (os .path .abspath (__file__ )))
@@ -304,6 +307,100 @@ def diff_live_sysroot(diff_from, diff_to):
304307 git_diff (dir_from , dir_to )
305308
306309
310+ def get_metal_path (build_target ):
311+ metal_file = build_target .meta .get ('images' , {}).get ('metal' )
312+
313+ if not metal_file :
314+ raise Exception (f"Could not find metal image for build { build_target .id } " )
315+ return os .path .join (build_target .dir , metal_file ['path' ])
316+
317+
318+ def diff_metal_partitions (diff_from , diff_to ):
319+ metal_from = get_metal_path (diff_from )
320+ metal_to = get_metal_path (diff_to )
321+ diff_cmd_outputs (['sgdisk' , '-p' ], metal_from , metal_to )
322+
323+
324+ def run_guestfs_mount (image_path , mount_target ):
325+ """This function runs in a background thread."""
326+ g = None
327+ try :
328+ g = guestfs .GuestFS (python_return_dict = True )
329+ g .set_backend ("direct" )
330+ g .add_drive_opts (image_path , readonly = 1 )
331+ g .launch ()
332+
333+ # Mount the disks in the guestfs VM
334+ root = g .findfs_label ("root" )
335+ g .mount_ro (root , "/" )
336+ boot = g .findfs_label ("boot" )
337+ g .mount_ro (boot , "/boot" )
338+ efi = g .findfs_label ("EFI-SYSTEM" )
339+ g .mount_ro (efi , "/boot/efi" )
340+
341+ # This is a blocking call that runs the FUSE server
342+ g .mount_local (mount_target )
343+ g .mount_local_run ()
344+
345+ except Exception as e :
346+ print (f"Error in guestfs process for { image_path } : { e } " , file = sys .stderr )
347+ finally :
348+ if g :
349+ g .close ()
350+
351+
352+ def diff_metal (diff_from , diff_to ):
353+ metal_from = get_metal_path (diff_from )
354+ metal_to = get_metal_path (diff_to )
355+
356+ mount_dir_from = os .path .join (cache_dir ("metal" ), diff_from .id )
357+ mount_dir_to = os .path .join (cache_dir ("metal" ), diff_to .id )
358+
359+ for d in [mount_dir_from , mount_dir_to ]:
360+ if os .path .exists (d ):
361+ shutil .rmtree (d )
362+ os .makedirs (d )
363+
364+ # As the libreguest mount call is blocking until unmounted, let's
365+ # do that in a separate thread
366+ p_from = Process (target = run_guestfs_mount , args = (metal_from , mount_dir_from ))
367+ p_to = Process (target = run_guestfs_mount , args = (metal_to , mount_dir_to ))
368+
369+ try :
370+ p_from .start ()
371+ p_to .start ()
372+ # Wait for the FUSE mounts to be ready. We'll check for a known file.
373+ for i , d in enumerate ([mount_dir_from , mount_dir_to ]):
374+ p = p_from if i == 0 else p_to
375+ timeout = 60 # seconds
376+ start_time = time .time ()
377+ check_file = os .path .join (d , 'ostree' )
378+ while not os .path .exists (check_file ):
379+ time .sleep (1 )
380+ if time .time () - start_time > timeout :
381+ raise Exception (f"Timeout waiting for mount in { d } " )
382+ if not p .is_alive ():
383+ raise Exception (f"A guestfs process for { os .path .basename (d )} died unexpectedly." )
384+
385+ # Now that the mounts are live, we can diff them
386+ git_diff (mount_dir_from , mount_dir_to )
387+
388+ finally :
389+ # Unmount the FUSE binds, this will make the guestfs mount calls return
390+ runcmd (['fusermount' , '-u' , mount_dir_from ], check = False )
391+ runcmd (['fusermount' , '-u' , mount_dir_to ], check = False )
392+
393+ # Ensure the background processes are terminated
394+ def shutdown_process (process ):
395+ process .join (timeout = 5 )
396+ if process .is_alive ():
397+ process .terminate ()
398+ process .join ()
399+
400+ shutdown_process (p_from )
401+ shutdown_process (p_to )
402+
403+
307404def diff_cmd_outputs (cmd , file_from , file_to ):
308405 with tempfile .NamedTemporaryFile (prefix = cmd [0 ] + '-' ) as f_from , \
309406 tempfile .NamedTemporaryFile (prefix = cmd [0 ] + '-' ) as f_to :
@@ -356,6 +453,10 @@ DIFFERS = [
356453 needs_ostree = OSTreeImport .NO , function = diff_live_sysroot_tree ),
357454 Differ ("live-sysroot" , "Diff live '/root.[ero|squash]fs' (embed into live-rootfs) content" ,
358455 needs_ostree = OSTreeImport .NO , function = diff_live_sysroot ),
456+ Differ ("metal-part-table" , "Diff metal disk image partition tables" ,
457+ needs_ostree = OSTreeImport .NO , function = diff_metal_partitions ),
458+ Differ ("metal" , "Diff metal disk image content" ,
459+ needs_ostree = OSTreeImport .NO , function = diff_metal ),
359460]
360461
361462if __name__ == '__main__' :
0 commit comments