Skip to content

cosa diff: add support for diffing metal images #4226

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions src/cmd-diff
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@ import shutil
import subprocess
import sys
import tempfile
import time
from multiprocessing import Process

from dataclasses import dataclass
from enum import IntEnum
import guestfs
from typing import Callable

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
Expand Down Expand Up @@ -304,6 +307,100 @@ def diff_live_sysroot(diff_from, diff_to):
git_diff(dir_from, dir_to)


def get_metal_path(build_target):
metal_file = build_target.meta.get('images', {}).get('metal')

if not metal_file:
raise Exception(f"Could not find metal image for build {build_target.id}")
return os.path.join(build_target.dir, metal_file['path'])


def diff_metal_partitions(diff_from, diff_to):
metal_from = get_metal_path(diff_from)
metal_to = get_metal_path(diff_to)
diff_cmd_outputs(['sgdisk', '-p'], metal_from, metal_to)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should use sfdisk here instead.



def run_guestfs_mount(image_path, mount_target):
"""This function runs in a background thread."""
g = None
try:
g = guestfs.GuestFS(python_return_dict=True)
g.set_backend("direct")
g.add_drive_opts(image_path, readonly=1)
g.launch()

# Mount the disks in the guestfs VM
root = g.findfs_label("root")
g.mount_ro(root, "/")
boot = g.findfs_label("boot")
g.mount_ro(boot, "/boot")
efi = g.findfs_label("EFI-SYSTEM")
g.mount_ro(efi, "/boot/efi")
Comment on lines +333 to +339
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Totally fine to start, though this will need adjustments to make it usable on other arches.

And we should definitely check other arches as part of coreos/fedora-coreos-tracker#1827.


# This is a blocking call that runs the FUSE server
g.mount_local(mount_target)
g.mount_local_run()

except Exception as e:
print(f"Error in guestfs process for {image_path}: {e}", file=sys.stderr)
finally:
if g:
g.close()


def diff_metal(diff_from, diff_to):
metal_from = get_metal_path(diff_from)
metal_to = get_metal_path(diff_to)

mount_dir_from = os.path.join(cache_dir("metal"), diff_from.id)
mount_dir_to = os.path.join(cache_dir("metal"), diff_to.id)

for d in [mount_dir_from, mount_dir_to]:
if os.path.exists(d):
shutil.rmtree(d)
os.makedirs(d)

# As the libreguest mount call is blocking until unmounted, let's
# do that in a separate thread
p_from = Process(target=run_guestfs_mount, args=(metal_from, mount_dir_from))
p_to = Process(target=run_guestfs_mount, args=(metal_to, mount_dir_to))

try:
p_from.start()
p_to.start()
# Wait for the FUSE mounts to be ready. We'll check for a known file.
for i, d in enumerate([mount_dir_from, mount_dir_to]):
p = p_from if i == 0 else p_to
Comment on lines +373 to +374
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor/optional: I think a cleaner way to do this is:

for (p, d) in [(p_from, mount_dir_from), (p_to, mount_dir_to)]:

timeout = 60 # seconds
start_time = time.time()
check_file = os.path.join(d, 'ostree')
while not os.path.exists(check_file):
time.sleep(1)
if time.time() - start_time > timeout:
raise Exception(f"Timeout waiting for mount in {d}")
if not p.is_alive():
raise Exception(f"A guestfs process for {os.path.basename(d)} died unexpectedly.")

# Now that the mounts are live, we can diff them
git_diff(mount_dir_from, mount_dir_to)

finally:
# Unmount the FUSE binds, this will make the guestfs mount calls return
runcmd(['fusermount', '-u', mount_dir_from], check=False)
runcmd(['fusermount', '-u', mount_dir_to], check=False)

# Ensure the background processes are terminated
def shutdown_process(process):
process.join(timeout=5)
if process.is_alive():
process.terminate()
process.join()

shutdown_process(p_from)
shutdown_process(p_to)


def diff_cmd_outputs(cmd, file_from, file_to):
with tempfile.NamedTemporaryFile(prefix=cmd[0] + '-') as f_from, \
tempfile.NamedTemporaryFile(prefix=cmd[0] + '-') as f_to:
Expand Down Expand Up @@ -356,6 +453,10 @@ DIFFERS = [
needs_ostree=OSTreeImport.NO, function=diff_live_sysroot_tree),
Differ("live-sysroot", "Diff live '/root.[ero|squash]fs' (embed into live-rootfs) content",
needs_ostree=OSTreeImport.NO, function=diff_live_sysroot),
Differ("metal-part-table", "Diff metal disk image partition tables",
needs_ostree=OSTreeImport.NO, function=diff_metal_partitions),
Differ("metal", "Diff metal disk image content",
needs_ostree=OSTreeImport.NO, function=diff_metal),
]

if __name__ == '__main__':
Expand Down
3 changes: 3 additions & 0 deletions src/deps.txt
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,6 @@ erofs-utils

# Support for copr build in coreos-ci
copr-cli

# To mount metal disk images in cmd-diff
python3-libguestfs
Loading