Skip to content

Commit 3d0140e

Browse files
committed
cosa diff: add support for diffing metal images
This add two flags: `--metal` and `metal-part-table`. The former will mount the disk partitions using guestfs tools to the tmp-diff directory, then call git diff over the two mounted filesystems. It requires multithreading because the FUSE mount call is blocking so we mount the disks in two libreguest VMs that run in separate threads. A simpler approach would have been to copy all the content to the tmp-diff folder but that would copy a lot of data just for diffing. It may be faster though. The second flag `--metal-part-table` will simply show the partition table for the two images.
1 parent 83fdbc4 commit 3d0140e

File tree

2 files changed

+104
-0
lines changed

2 files changed

+104
-0
lines changed

src/cmd-diff

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@ import shutil
66
import subprocess
77
import sys
88
import tempfile
9+
import time
10+
from multiprocessing import Process
911

1012
from dataclasses import dataclass
1113
from enum import IntEnum
14+
import guestfs
1215
from typing import Callable
1316

1417
sys.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.joint(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+
307404
def 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

361462
if __name__ == '__main__':

src/deps.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,6 @@ erofs-utils
104104

105105
# Support for copr build in coreos-ci
106106
copr-cli
107+
108+
# To mount metal disk images in cmd-diff
109+
python3-libguestfs

0 commit comments

Comments
 (0)