Repository: osbuild/osbuild-composer
Pull request #1131 (2020-12-08 16:41:31)

distro/rhel84: use a random uuid for XFS partition
Imagine this situation: You have a RHEL system booted from an image produced
by osbuild-composer. On this system, you want to use osbuild-composer to
create another image of RHEL.

However, there's currently something funny with partitions:

All RHEL images built by osbuild-composer contain a root xfs partition. The
interesting bit is that they all share the same xfs partition UUID. This might
sound like a good thing for reproducibility but it has a quirk.

The issue appears when osbuild runs the qemu assembler: it needs to mount all
partitions of the future image to copy the OS tree into it.

Imagine that osbuild-composer is running on a system booted from an image
produced by osbuild-composer. This means that its root xfs partition has this
uuid:

efe8afea-c0a8-45dc-8e6e-499279f6fa5d

When osbuild-composer builds an image on this system, it runs osbuild that
runs the qemu assembler at some point. As I said previously, it will mount
all partitions of the future image. That means that it will also try to
mount the root xfs partition with this uuid:

efe8afea-c0a8-45dc-8e6e-499279f6fa5d

Do you remember this one? Yeah, it's the same one as before. However, the xfs
kernel driver doesn't like that. It contains a global table[1] of all xfs
partitions that forbids to mount 2 xfs partitions with the same uuid.

I mean... uuids are meant to be unique, right?

This commit changes the way we build RHEL 8.4 images: Each one now has a
unique uuid. It's now literally a unique universally unique identifier. haha

\[1\]: https://github.com/torvalds/linux/blob/a349e4c659609fd20e4beea89e5c4a4038e33a95/fs/xfs/xfs_mount.c#L51

jkozol: Using a "unique universally unique identifier" makes sense to me. I think you will also need to update the image tests. I believe they will fail since the uuid in the manifests will be different than the uuid generated by the tests.
teg: I support this! However, note that these things are still not actually universally unique, as the produced images will be copied and deployed repeatedly, so if someone were to somehow mount one of these images inside themselves we'd have the same problem (but that's always the case).
ondrejbudai: This is actually harder than I originally thought. The code is more or less done now but there are no commit messages and the code is not commented. I pushed this just to trigger this CI. I'm marking this as a draft and will finish the documentation part of this PR tomorrow.
ondrejbudai: This now ready to be reviewed.

A big issue with a unique UUID for root partition was that we hardcoded the UUID at several places around `rhel84/distro.go`. If we want to use a randomly generated one, we would need to pass it around somehow.

I decided to not implement any ugly hacks and do it in the sanest way I could imagine: The `rhel84.pipeline()` method now firstly generates a partition table and then the root FS UUID is taken from it. Also, the partition table is used to generate `fstab` (this is imho pretty cool).

This has two pretty nice benefits:

1) We define the partition table in one place. No magic constants all around the code.

2) If we ever want to implement partition table customization, this is a very good first step.
ondrejbudai: RHEL 8 koji-osbuild build failed because we use an old snapshot, see https://github.com/osbuild/koji-osbuild/pull/54
Fedora 32 tests failed because of https://github.com/osbuild/osbuild-composer/issues/1135

None of these issues are connected with this PR, let's merge this.

Patch:
diff --git a/cmd/osbuild-pipeline/main.go b/cmd/osbuild-pipeline/main.go
index 56d2c0869..e90153eeb 100644
--- a/cmd/osbuild-pipeline/main.go
+++ b/cmd/osbuild-pipeline/main.go
@@ -44,6 +44,8 @@ type rpmMD struct {
 func main() {
 	var rpmmdArg bool
 	flag.BoolVar(&rpmmdArg, "rpmmd", false, "output rpmmd struct instead of pipeline manifest")
+	var seedArg int64
+	flag.Int64Var(&seedArg, "seed", 0, "seed for generating manifests (default: 0)")
 	flag.Parse()
 
 	// Path to composeRequet or '-' for stdin
@@ -152,7 +154,8 @@ func main() {
 			},
 			repos,
 			packageSpecs,
-			buildPackageSpecs)
+			buildPackageSpecs,
+			seedArg)
 		if err != nil {
 			panic(err.Error())
 		}
diff --git a/cmd/osbuild-store-dump/main.go b/cmd/osbuild-store-dump/main.go
index 508e6fae9..a1c381fe0 100644
--- a/cmd/osbuild-store-dump/main.go
+++ b/cmd/osbuild-store-dump/main.go
@@ -26,7 +26,7 @@ func getManifest(bp blueprint.Blueprint, t distro.ImageType, a distro.Arch, d di
 	if err != nil {
 		panic(err)
 	}
-	manifest, err := t.Manifest(bp.Customizations, distro.ImageOptions{}, repos, pkgs, buildPkgs)
+	manifest, err := t.Manifest(bp.Customizations, distro.ImageOptions{}, repos, pkgs, buildPkgs, 0)
 	if err != nil {
 		panic(err)
 	}
diff --git a/internal/cloudapi/server.go b/internal/cloudapi/server.go
index aa115b3ed..b05ef4eab 100644
--- a/internal/cloudapi/server.go
+++ b/internal/cloudapi/server.go
@@ -5,6 +5,7 @@ package cloudapi
 import (
 	"encoding/json"
 	"fmt"
+	"math/rand"
 	"net/http"
 
 	"github.com/go-chi/chi"
@@ -81,6 +82,9 @@ func (server *Server) Compose(w http.ResponseWriter, r *http.Request) {
 	imageRequests := make([]imageRequest, len(request.ImageRequests))
 	var targets []*target.Target
 
+	// use the same seed for all images so we get the same IDs
+	manifestSeed := rand.Int63()
+
 	for i, ir := range request.ImageRequests {
 		arch, err := distribution.GetArch(ir.Architecture)
 		if err != nil {
@@ -139,7 +143,7 @@ func (server *Server) Compose(w http.ResponseWriter, r *http.Request) {
 			}
 		}
 
-		manifest, err := imageType.Manifest(nil, imageOptions, repositories, packages, buildPackages)
+		manifest, err := imageType.Manifest(nil, imageOptions, repositories, packages, buildPackages, manifestSeed)
 		if err != nil {
 			http.Error(w, fmt.Sprintf("Failed to get manifest for for %s/%s/%s: %s", ir.ImageType, ir.Architecture, request.Distribution, err), http.StatusBadRequest)
 			return
diff --git a/internal/disk/disk.go b/internal/disk/disk.go
new file mode 100644
index 000000000..4ab248430
--- /dev/null
+++ b/internal/disk/disk.go
@@ -0,0 +1,127 @@
+// Disk package contains abstract data-types to define disk-related entities.
+//
+// PartitionTable, Partition and Filesystem types are currently defined.
+// All of them can be 1:1 converted to osbuild.QEMUAssemblerOptions.
+package disk
+
+import (
+	"sort"
+
+	"github.com/osbuild/osbuild-composer/internal/osbuild"
+)
+
+type PartitionTable struct {
+	// Size of the disk.
+	Size uint64
+	UUID string
+	// Partition table type, e.g. dos, gpt.
+	Type       string
+	Partitions []Partition
+}
+
+type Partition struct {
+	Start    uint64
+	Size     uint64
+	Type     string
+	Bootable bool
+	// ID of the partition, dos doesn't use traditional UUIDs, therefore this
+	// is just a string.
+	UUID string
+	// If nil, the partition is raw; It doesn't contain a filesystem.
+	Filesystem *Filesystem
+}
+
+type Filesystem struct {
+	Type string
+	// ID of the filesystem, vfat doesn't use traditional UUIDs, therefore this
+	// is just a string.
+	UUID       string
+	Label      string
+	Mountpoint string
+	// The fourth field of fstab(5); fs_mntops
+	FSTabOptions string
+	// The fifth field of fstab(5); fs_freq
+	FSTabFreq uint64
+	// The sixth field of fstab(5); fs_passno
+	FSTabPassNo uint64
+}
+
+// Converts PartitionTable to osbuild.QEMUAssemblerOptions that encode
+// the same partition table.
+func (pt PartitionTable) QEMUAssemblerOptions() osbuild.QEMUAssemblerOptions {
+	var partitions []osbuild.QEMUPartition
+	for _, p := range pt.Partitions {
+		partitions = append(partitions, p.QEMUPartition())
+	}
+
+	return osbuild.QEMUAssemblerOptions{
+		Size:       pt.Size,
+		PTUUID:     pt.UUID,
+		PTType:     pt.Type,
+		Partitions: partitions,
+	}
+}
+
+// Generates org.osbuild.fstab stage options from this partition table.
+func (pt PartitionTable) FSTabStageOptions() *osbuild.FSTabStageOptions {
+	var options osbuild.FSTabStageOptions
+	for _, p := range pt.Partitions {
+		fs := p.Filesystem
+		if fs == nil {
+			continue
+		}
+
+		options.AddFilesystem(fs.UUID, fs.Type, fs.Mountpoint, fs.FSTabOptions, fs.FSTabFreq, fs.FSTabPassNo)
+	}
+
+	// sort the entries by PassNo to maintain backward compatibility
+	sort.Slice(options.FileSystems, func(i, j int) bool {
+		return options.FileSystems[i].PassNo < options.FileSystems[j].PassNo
+	})
+
+	return &options
+}
+
+// Returns the root partition (the partition whose filesystem has / as
+// a mountpoint) of the partition table. Nil is returned if there's no such
+// partition.
+func (pt PartitionTable) RootPartition() *Partition {
+	for _, p := range pt.Partitions {
+		if p.Filesystem == nil {
+			continue
+		}
+
+		if p.Filesystem.Mountpoint == "/" {
+			return &p
+		}
+	}
+
+	return nil
+}
+
+// Converts Partition to osbuild.QEMUPartition that encodes the same partition.
+func (p Partition) QEMUPartition() osbuild.QEMUPartition {
+	var fs *osbuild.QEMUFilesystem
+	if p.Filesystem != nil {
+		f := p.Filesystem.QEMUFilesystem()
+		fs = &f
+	}
+	return osbuild.QEMUPartition{
+		Start:      p.Start,
+		Size:       p.Size,
+		Type:       p.Type,
+		Bootable:   p.Bootable,
+		UUID:       p.UUID,
+		Filesystem: fs,
+	}
+}
+
+// Converts Filesystem to osbuild.QEMUFilesystem that encodes the same fs.
+func (fs Filesystem) QEMUFilesystem() osbuild.QEMUFilesystem {
+	return osbuild.QEMUFilesystem{
+		Type:       fs.Type,
+		UUID:       fs.UUID,
+		Label:      fs.Label,
+		Mountpoint: fs.Mountpoint,
+	}
+}
diff --git a/internal/distro/distro.go b/internal/distro/distro.go
index 54484cd9b..345459a6a 100644
--- a/internal/distro/distro.go
+++ b/internal/distro/distro.go
@@ -79,7 +79,7 @@ type ImageType interface {
 	// Returns an osbuild manifest, containing the sources and pipeline necessary
 	// to build an image, given output format with all packages and customizations
 	// specified in the given blueprint.
-	Manifest(b *blueprint.Customizations, options ImageOptions, repos []rpmmd.RepoConfig, packageSpecs, buildPackageSpecs []rpmmd.PackageSpec) (Manifest, error)
+	Manifest(b *blueprint.Customizations, options ImageOptions, repos []rpmmd.RepoConfig, packageSpecs, buildPackageSpecs []rpmmd.PackageSpec, seed int64) (Manifest, error)
 }
 
 // The ImageOptions specify options for a specific image build
diff --git a/internal/distro/fedora32/distro.go b/internal/distro/fedora32/distro.go
index 81d13e22c..b2a59deda 100644
--- a/internal/distro/fedora32/distro.go
+++ b/internal/distro/fedora32/distro.go
@@ -183,7 +183,8 @@ func (t *imageType) Manifest(c *blueprint.Customizations,
 	options distro.ImageOptions,
 	repos []rpmmd.RepoConfig,
 	packageSpecs,
-	buildPackageSpecs []rpmmd.PackageSpec) (distro.Manifest, error) {
+	buildPackageSpecs []rpmmd.PackageSpec,
+	seed int64) (distro.Manifest, error) {
 	pipeline, err := t.pipeline(c, options, repos, packageSpecs, buildPackageSpecs)
 	if err != nil {
 		return distro.Manifest{}, err
diff --git a/internal/distro/fedora33/distro.go b/internal/distro/fedora33/distro.go
index 3d27483f3..4ac4ca35e 100644
--- a/internal/distro/fedora33/distro.go
+++ b/internal/distro/fedora33/distro.go
@@ -183,7 +183,8 @@ func (t *imageType) Manifest(c *blueprint.Customizations,
 	options distro.ImageOptions,
 	repos []rpmmd.RepoConfig,
 	packageSpecs,
-	buildPackageSpecs []rpmmd.PackageSpec) (distro.Manifest, error) {
+	buildPackageSpecs []rpmmd.PackageSpec,
+	seed int64) (distro.Manifest, error) {
 	pipeline, err := t.pipeline(c, options, repos, packageSpecs, buildPackageSpecs)
 	if err != nil {
 		return distro.Manifest{}, err
diff --git a/internal/distro/rhel8/distro.go b/internal/distro/rhel8/distro.go
index 7d8b769be..b76a721ba 100644
--- a/internal/distro/rhel8/distro.go
+++ b/internal/distro/rhel8/distro.go
@@ -185,7 +185,8 @@ func (t *imageType) Manifest(c *blueprint.Customizations,
 	options distro.ImageOptions,
 	repos []rpmmd.RepoConfig,
 	packageSpecs,
-	buildPackageSpecs []rpmmd.PackageSpec) (distro.Manifest, error) {
+	buildPackageSpecs []rpmmd.PackageSpec,
+	seed int64) (distro.Manifest, error) {
 	pipeline, err := t.pipeline(c, options, repos, packageSpecs, buildPackageSpecs)
 	if err != nil {
 		return distro.Manifest{}, err
diff --git a/internal/distro/rhel84/distro.go b/internal/distro/rhel84/distro.go
index ba55d964c..575b15b5a 100644
--- a/internal/distro/rhel84/distro.go
+++ b/internal/distro/rhel84/distro.go
@@ -4,8 +4,11 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"io"
+	"math/rand"
 	"sort"
 
+	"github.com/osbuild/osbuild-composer/internal/disk"
 	"github.com/osbuild/osbuild-composer/internal/distro"
 	"github.com/osbuild/osbuild-composer/internal/osbuild"
 
@@ -36,20 +39,21 @@ type architecture struct {
 }
 
 type imageType struct {
-	arch             *architecture
-	name             string
-	filename         string
-	mimeType         string
-	packages         []string
-	excludedPackages []string
-	enabledServices  []string
-	disabledServices []string
-	defaultTarget    string
-	kernelOptions    string
-	bootable         bool
-	rpmOstree        bool
-	defaultSize      uint64
-	assembler        func(options distro.ImageOptions, arch distro.Arch) *osbuild.Assembler
+	arch                    *architecture
+	name                    string
+	filename                string
+	mimeType                string
+	packages                []string
+	excludedPackages        []string
+	enabledServices         []string
+	disabledServices        []string
+	defaultTarget           string
+	kernelOptions           string
+	bootable                bool
+	rpmOstree               bool
+	defaultSize             uint64
+	partitionTableGenerator func(imageOptions distro.ImageOptions, arch distro.Arch, rng *rand.Rand) disk.PartitionTable
+	assembler               func(pt *disk.PartitionTable, options distro.ImageOptions, arch distro.Arch) *osbuild.Assembler
 }
 
 func (a *architecture) Distro() distro.Distro {
@@ -118,20 +122,21 @@ func (a *architecture) setImageTypes(imageTypes ...imageType) {
 	a.imageTypes = map[string]imageType{}
 	for _, it := range imageTypes {
 		a.imageTypes[it.name] = imageType{
-			arch:             a,
-			name:             it.name,
-			filename:         it.filename,
-			mimeType:         it.mimeType,
-			packages:         it.packages,
-			excludedPackages: it.excludedPackages,
-			enabledServices:  it.enabledServices,
-			disabledServices: it.disabledServices,
-			defaultTarget:    it.defaultTarget,
-			kernelOptions:    it.kernelOptions,
-			bootable:         it.bootable,
-			rpmOstree:        it.rpmOstree,
-			defaultSize:      it.defaultSize,
-			assembler:        it.assembler,
+			arch:                    a,
+			name:                    it.name,
+			filename:                it.filename,
+			mimeType:                it.mimeType,
+			packages:                it.packages,
+			excludedPackages:        it.excludedPackages,
+			enabledServices:         it.enabledServices,
+			disabledServices:        it.disabledServices,
+			defaultTarget:           it.defaultTarget,
+			kernelOptions:           it.kernelOptions,
+			bootable:                it.bootable,
+			rpmOstree:               it.rpmOstree,
+			defaultSize:             it.defaultSize,
+			partitionTableGenerator: it.partitionTableGenerator,
+			assembler:               it.assembler,
 		}
 	}
 }
@@ -185,8 +190,11 @@ func (t *imageType) Manifest(c *blueprint.Customizations,
 	options distro.ImageOptions,
 	repos []rpmmd.RepoConfig,
 	packageSpecs,
-	buildPackageSpecs []rpmmd.PackageSpec) (distro.Manifest, error) {
-	pipeline, err := t.pipeline(c, options, repos, packageSpecs, buildPackageSpecs)
+	buildPackageSpecs []rpmmd.PackageSpec,
+	seed int64) (distro.Manifest, error) {
+	source := rand.NewSource(seed)
+	rng := rand.New(source)
+	pipeline, err := t.pipeline(c, options, repos, packageSpecs, buildPackageSpecs, rng)
 	if err != nil {
 		return distro.Manifest{}, err
 	}
@@ -227,13 +235,28 @@ func sources(packages []rpmmd.PackageSpec) *osbuild.Sources {
 	}
 }
 
-func (t *imageType) pipeline(c *blueprint.Customizations, options distro.ImageOptions, repos []rpmmd.RepoConfig, packageSpecs, buildPackageSpecs []rpmmd.PackageSpec) (*osbuild.Pipeline, error) {
+func (t *imageType) pipeline(c *blueprint.Customizations, options distro.ImageOptions, repos []rpmmd.RepoConfig, packageSpecs, buildPackageSpecs []rpmmd.PackageSpec, rng *rand.Rand) (*osbuild.Pipeline, error) {
+	var pt *disk.PartitionTable
+	if t.partitionTableGenerator != nil {
+		table := t.partitionTableGenerator(options, t.arch, rng)
+		pt = &table
+	}
+
 	p := &osbuild.Pipeline{}
 	p.SetBuild(t.buildPipeline(repos, *t.arch, buildPackageSpecs), "org.osbuild.rhel84")
 
 	if t.arch.Name() == "s390x" {
+		if pt == nil {
+			panic("s390x image must have a partition table, this is a programming error")
+		}
+
+		rootPartition := pt.RootPartition()
+		if rootPartition == nil {
+			panic("s390x image must have a root partition, this is a programming error")
+		}
+
 		p.AddStage(osbuild.NewKernelCmdlineStage(&osbuild.KernelCmdlineStageOptions{
-			RootFsUUID: "efe8afea-c0a8-45dc-8e6e-499279f6fa5d",
+			RootFsUUID: rootPartition.UUID,
 			KernelOpts: "net.ifnames=0 crashkernel=auto",
 		}))
 	}
@@ -241,10 +264,13 @@ func (t *imageType) pipeline(c *blueprint.Customizations, options distro.ImageOp
 	p.AddStage(osbuild.NewRPMStage(t.rpmStageOptions(*t.arch, repos, packageSpecs)))
 	p.AddStage(osbuild.NewFixBLSStage())
 
+	if pt != nil {
+		p.AddStage(osbuild.NewFSTabStage(pt.FSTabStageOptions()))
+	}
+
 	if t.bootable {
-		p.AddStage(osbuild.NewFSTabStage(t.fsTabStageOptions(t.arch.uefi)))
 		if t.arch.Name() != "s390x" {
-			p.AddStage(osbuild.NewGRUB2Stage(t.grub2StageOptions(t.kernelOptions, c.GetKernel(), t.arch.uefi, t.arch.legacy)))
+			p.AddStage(osbuild.NewGRUB2Stage(t.grub2StageOptions(pt, t.kernelOptions, c.GetKernel(), t.arch.uefi, t.arch.legacy)))
 		}
 	}
 
@@ -325,7 +351,7 @@ func (t *imageType) pipeline(c *blueprint.Customizations, options distro.ImageOp
 		))
 	}
 
-	p.Assembler = t.assembler(options, t.arch)
+	p.Assembler = t.assembler(pt, options, t.arch)
 
 	return p, nil
 }
@@ -436,17 +462,16 @@ func (t *imageType) systemdStageOptions(enabledServices, disabledServices []stri
 	}
 }
 
-func (t *imageType) fsTabStageOptions(uefi bool) *osbuild.FSTabStageOptions {
-	options := osbuild.FSTabStageOptions{}
-	options.AddFilesystem("efe8afea-c0a8-45dc-8e6e-499279f6fa5d", "xfs", "/", "defaults", 0, 0)
-	if uefi {
-		options.AddFilesystem("7B77-95E7", "vfat", "/boot/efi", "defaults,uid=0,gid=0,umask=077,shortname=winnt", 0, 2)
+func (t *imageType) grub2StageOptions(pt *disk.PartitionTable, kernelOptions string, kernel *blueprint.KernelCustomization, uefi bool, legacy string) *osbuild.GRUB2StageOptions {
+	if pt == nil {
+		panic("partition table must be defined for grub2 stage, this is a programming error")
+	}
+	rootPartition := pt.RootPartition()
+	if rootPartition == nil {
+		panic("root partition must be defined for grub2 stage, this is a programming error")
 	}
-	return &options
-}
 
-func (t *imageType) grub2StageOptions(kernelOptions string, kernel *blueprint.KernelCustomization, uefi bool, legacy string) *osbuild.GRUB2StageOptions {
-	id := uuid.MustParse("efe8afea-c0a8-45dc-8e6e-499279f6fa5d")
+	id := uuid.MustParse(rootPartition.Filesystem.UUID)
 
 	if kernel != nil {
 		kernelOptions += " " + kernel.Append
@@ -477,20 +502,13 @@ func (t *imageType) selinuxStageOptions() *osbuild.SELinuxStageOptions {
 	}
 }
 
-func qemuAssembler(format string, filename string, imageOptions distro.ImageOptions, arch distro.Arch) *osbuild.Assembler {
-	var options osbuild.QEMUAssemblerOptions
-
+func defaultPartitionTable(imageOptions distro.ImageOptions, arch distro.Arch, rng *rand.Rand) disk.PartitionTable {
 	if arch.Name() == "x86_64" {
-		options = osbuild.QEMUAssemblerOptions{
-			Bootloader: &osbuild.QEMUBootloader{
-				Type: "grub2",
-			},
-			Format:   format,
-			Filename: filename,
-			Size:     imageOptions.Size,
-			PTUUID:   "D209C89E-EA5E-4FBD-B161-B461CCE297E0",
-			PTType:   "gpt",
-			Partitions: []osbuild.QEMUPartition{
+		return disk.PartitionTable{
+			Size: imageOptions.Size,
+			UUID: "D209C89E-EA5E-4FBD-B161-B461CCE297E0",
+			Type: "gpt",
+			Partitions: []disk.Partition{
 				{
 					Bootable: true,
 					Size:     2048,
@@ -503,69 +521,73 @@ func qemuAssembler(format string, filename string, imageOptions distro.ImageOpti
 					Size:  204800,
 					Type:  "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
 					UUID:  "68B2905B-DF3E-4FB3-80FA-49D1E773AA33",
-					Filesystem: &osbuild.QEMUFilesystem{
-						Type:       "vfat",
-						UUID:       "7B77-95E7",
-						Mountpoint: "/boot/efi",
+					Filesystem: &disk.Filesystem{
+						Type:         "vfat",
+						UUID:         "7B77-95E7",
+						Mountpoint:   "/boot/efi",
+						FSTabOptions: "defaults,uid=0,gid=0,umask=077,shortname=winnt",
+						FSTabFreq:    0,
+						FSTabPassNo:  2,
 					},
 				},
 				{
 					Start: 208896,
 					Type:  "0FC63DAF-8483-4772-8E79-3D69D8477DE4",
 					UUID:  "6264D520-3FB9-423F-8AB8-7A0A8E3D3562",
-					Filesystem: &osbuild.QEMUFilesystem{
-						Type:       "xfs",
-						UUID:       "efe8afea-c0a8-45dc-8e6e-499279f6fa5d",
-						Label:      "root",
-						Mountpoint: "/",
+					Filesystem: &disk.Filesystem{
+						Type:         "xfs",
+						UUID:         uuid.Must(newRandomUUIDFromReader(rng)).String(),
+						Label:        "root",
+						Mountpoint:   "/",
+						FSTabOptions: "defaults",
+						FSTabFreq:    0,
+						FSTabPassNo:  0,
 					},
 				},
 			},
 		}
 	} else if arch.Name() == "aarch64" {
-		options = osbuild.QEMUAssemblerOptions{
-			Format:   format,
-			Filename: filename,
-			Size:     imageOptions.Size,
-			PTUUID:   "D209C89E-EA5E-4FBD-B161-B461CCE297E0",
-			PTType:   "gpt",
-			Partitions: []osbuild.QEMUPartition{
+		return disk.PartitionTable{
+			Size: imageOptions.Size,
+			UUID: "D209C89E-EA5E-4FBD-B161-B461CCE297E0",
+			Type: "gpt",
+			Partitions: []disk.Partition{
 				{
 					Start: 2048,
 					Size:  204800,
 					Type:  "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
 					UUID:  "68B2905B-DF3E-4FB3-80FA-49D1E773AA33",
-					Filesystem: &osbuild.QEMUFilesystem{
-						Type:       "vfat",
-						UUID:       "7B77-95E7",
-						Mountpoint: "/boot/efi",
+					Filesystem: &disk.Filesystem{
+						Type:         "vfat",
+						UUID:         "7B77-95E7",
+						Mountpoint:   "/boot/efi",
+						FSTabOptions: "defaults,uid=0,gid=0,umask=077,shortname=winnt",
+						FSTabFreq:    0,
+						FSTabPassNo:  2,
 					},
 				},
 				{
 					Start: 206848,
 					Type:  "0FC63DAF-8483-4772-8E79-3D69D8477DE4",
 					UUID:  "6264D520-3FB9-423F-8AB8-7A0A8E3D3562",
-					Filesystem: &osbuild.QEMUFilesystem{
-						Type:       "xfs",
-						UUID:       "efe8afea-c0a8-45dc-8e6e-499279f6fa5d",
-						Label:      "root",
-						Mountpoint: "/",
+					Filesystem: &disk.Filesystem{
+						Type:         "xfs",
+						UUID:         uuid.Must(newRandomUUIDFromReader(rng)).String(),
+						Label:        "root",
+						Mountpoint:   "/",
+						FSTabOptions: "defaults",
+						FSTabFreq:    0,
+						FSTabPassNo:  0,
 					},
 				},
 			},
 		}
 	} else if arch.Name() == "ppc64le" {
-		options = osbuild.QEMUAssemblerOptions{
-			Bootloader: &osbuild.QEMUBootloader{
-				Type:     "grub2",
-				Platform: "powerpc-ieee1275",
-			},
-			Format:   format,
-			Filename: filename,
-			Size:     imageOptions.Size,
-			PTUUID:   "0x14fc63d2",
-			PTType:   "dos",
-			Partitions: []osbuild.QEMUPartition{
+		return disk.PartitionTable{
+			Size: imageOptions.Size,
+			UUID: "0x14fc63d2",
+			Type: "dos",
+			Partitions: []disk.Partition{
 				{
 					Size:     8192,
 					Type:     "41",
@@ -573,37 +595,62 @@ func qemuAssembler(format string, filename string, imageOptions distro.ImageOpti
 				},
 				{
 					Start: 10240,
-					Filesystem: &osbuild.QEMUFilesystem{
-						Type:       "xfs",
-						UUID:       "efe8afea-c0a8-45dc-8e6e-499279f6fa5d",
-						Mountpoint: "/",
+					Filesystem: &disk.Filesystem{
+						Type:         "xfs",
+						UUID:         uuid.Must(newRandomUUIDFromReader(rng)).String(),
+						Mountpoint:   "/",
+						FSTabOptions: "defaults",
+						FSTabFreq:    0,
+						FSTabPassNo:  0,
 					},
 				},
 			},
 		}
 	} else if arch.Name() == "s390x" {
-		options = osbuild.QEMUAssemblerOptions{
-			Bootloader: &osbuild.QEMUBootloader{
-				Type: "zipl",
-			},
-			Format:   format,
-			Filename: filename,
-			Size:     imageOptions.Size,
-			PTUUID:   "0x14fc63d2",
-			PTType:   "dos",
-			Partitions: []osbuild.QEMUPartition{
+		return disk.PartitionTable{
+			Size: imageOptions.Size,
+			UUID: "0x14fc63d2",
+			Type: "dos",
+			Partitions: []disk.Partition{
 				{
 					Start:    2048,
 					Bootable: true,
-					Filesystem: &osbuild.QEMUFilesystem{
-						Type:       "xfs",
-						UUID:       "efe8afea-c0a8-45dc-8e6e-499279f6fa5d",
-						Mountpoint: "/",
+					Filesystem: &disk.Filesystem{
+						Type:         "xfs",
+						UUID:         uuid.Must(newRandomUUIDFromReader(rng)).String(),
+						Mountpoint:   "/",
+						FSTabOptions: "defaults",
+						FSTabFreq:    0,
+						FSTabPassNo:  0,
 					},
 				},
 			},
 		}
 	}
+
+	panic("unknown arch: " + arch.Name())
+}
+
+func qemuAssembler(pt *disk.PartitionTable, format string, filename string, imageOptions distro.ImageOptions, arch distro.Arch) *osbuild.Assembler {
+	options := pt.QEMUAssemblerOptions()
+
+	options.Format = format
+	options.Filename = filename
+
+	if arch.Name() == "x86_64" {
+		options.Bootloader = &osbuild.QEMUBootloader{
+			Type: "grub2",
+		}
+	} else if arch.Name() == "ppc64le" {
+		options.Bootloader = &osbuild.QEMUBootloader{
+			Type:     "grub2",
+			Platform: "powerpc-ieee1275",
+		}
+	} else if arch.Name() == "s390x" {
+		options.Bootloader = &osbuild.QEMUBootloader{
+			Type: "zipl",
+		}
+	}
 	return osbuild.NewQEMUAssembler(&options)
 }
 
@@ -631,6 +678,17 @@ func ostreeCommitAssembler(options distro.ImageOptions, arch distro.Arch) *osbui
 	)
 }
 
+func newRandomUUIDFromReader(r io.Reader) (uuid.UUID, error) {
+	var id uuid.UUID
+	_, err := io.ReadFull(r, id[:])
+	if err != nil {
+		return uuid.Nil, err
+	}
+	id[6] = (id[6] & 0x0f) | 0x40 // Version 4
+	id[8] = (id[8] & 0x3f) | 0x80 // Variant is 10
+	return id, nil
+}
+
 // New creates a new distro object, defining the supported architectures and image types
 func New() distro.Distro {
 	const GigaByte = 1024 * 1024 * 1024
@@ -688,7 +746,7 @@ func New() distro.Distro {
 			"redboot-auto-reboot", "redboot-task-runner",
 		},
 		rpmOstree: true,
-		assembler: func(options distro.ImageOptions, arch distro.Arch) *osbuild.Assembler {
+		assembler: func(pt *disk.PartitionTable, options distro.ImageOptions, arch distro.Arch) *osbuild.Assembler {
 			return ostreeCommitAssembler(options, arch)
 		},
 	}
@@ -743,7 +801,7 @@ func New() distro.Distro {
 			"redboot-auto-reboot", "redboot-task-runner",
 		},
 		rpmOstree: true,
-		assembler: func(options distro.ImageOptions, arch distro.Arch) *osbuild.Assembler {
+		assembler: func(pt *disk.PartitionTable, options distro.ImageOptions, arch distro.Arch) *osbuild.Assembler {
 			return ostreeCommitAssembler(options, arch)
 		},
 	}
@@ -815,12 +873,13 @@ func New() distro.Distro {
 			// https://errata.devel.redhat.com/advisory/47339 lands
 			"timedatex",
 		},
-		defaultTarget: "multi-user.target",
-		kernelOptions: "console=ttyS0,115200n8 console=tty0 net.ifnames=0 rd.blacklist=nouveau nvme_core.io_timeout=4294967295 crashkernel=auto",
-		bootable:      true,
-		defaultSize:   6 * GigaByte,
-		assembler: func(options distro.ImageOptions, arch distro.Arch) *osbuild.Assembler {
-			return qemuAssembler("raw", "image.raw", options, arch)
+		defaultTarget:           "multi-user.target",
+		kernelOptions:           "console=ttyS0,115200n8 console=tty0 net.ifnames=0 rd.blacklist=nouveau nvme_core.io_timeout=4294967295 crashkernel=auto",
+		bootable:                true,
+		defaultSize:             6 * GigaByte,
+		partitionTableGenerator: defaultPartitionTable,
+		assembler: func(pt *disk.PartitionTable, options distro.ImageOptions, arch distro.Arch) *osbuild.Assembler {
+			return qemuAssembler(pt, "raw", "image.raw", options, arch)
 		},
 	}
 
@@ -897,11 +956,12 @@ func New() distro.Distro {
 			// https://errata.devel.redhat.com/advisory/47339 lands
 			"timedatex",
 		},
-		kernelOptions: "console=tty0 console=ttyS0,115200n8 no_timer_check net.ifnames=0 crashkernel=auto",
-		bootable:      true,
-		defaultSize:   10 * GigaByte,
-		assembler: func(options distro.ImageOptions, arch distro.Arch) *osbuild.Assembler {
-			return qemuAssembler("qcow2", "disk.qcow2", options, arch)
+		kernelOptions:           "console=tty0 console=ttyS0,115200n8 no_timer_check net.ifnames=0 crashkernel=auto",
+		bootable:                true,
+		defaultSize:             10 * GigaByte,
+		partitionTableGenerator: defaultPartitionTable,
+		assembler: func(pt *disk.PartitionTable, options distro.ImageOptions, arch distro.Arch) *osbuild.Assembler {
+			return qemuAssembler(pt, "qcow2", "disk.qcow2", options, arch)
 		},
 	}
 
@@ -925,11 +985,12 @@ func New() distro.Distro {
 			"dracut-config-rescue",
 			"rng-tools",
 		},
-		kernelOptions: "ro net.ifnames=0",
-		bootable:      true,
-		defaultSize:   4 * GigaByte,
-		assembler: func(options distro.ImageOptions, arch distro.Arch) *osbuild.Assembler {
-			return qemuAssembler("qcow2", "disk.qcow2", options, arch)
+		kernelOptions:           "ro net.ifnames=0",
+		bootable:                true,
+		defaultSize:             4 * GigaByte,
+		partitionTableGenerator: defaultPartitionTable,
+		assembler: func(pt *disk.PartitionTable, options distro.ImageOptions, arch distro.Arch) *osbuild.Assembler {
+			return qemuAssembler(pt, "qcow2", "disk.qcow2", options, arch)
 		},
 	}
 
@@ -946,7 +1007,7 @@ func New() distro.Distro {
 		},
 		bootable:      false,
 		kernelOptions: "ro net.ifnames=0",
-		assembler: func(options distro.ImageOptions, arch distro.Arch) *osbuild.Assembler {
+		assembler: func(pt *disk.PartitionTable, options distro.ImageOptions, arch distro.Arch) *osbuild.Assembler {
 			return tarAssembler("root.tar.xz", "xz")
 		},
 	}
@@ -983,12 +1044,13 @@ func New() distro.Distro {
 			"sshd",
 			"waagent",
 		},
-		defaultTarget: "multi-user.target",
-		kernelOptions: "ro biosdevname=0 rootdelay=300 console=ttyS0 earlyprintk=ttyS0 net.ifnames=0",
-		bootable:      true,
-		defaultSize:   4 * GigaByte,
-		assembler: func(options distro.ImageOptions, arch distro.Arch) *osbuild.Assembler {
-			return qemuAssembler("vpc", "disk.vhd", options, arch)
+		defaultTarget:           "multi-user.target",
+		kernelOptions:           "ro biosdevname=0 rootdelay=300 console=ttyS0 earlyprintk=ttyS0 net.ifnames=0",
+		bootable:                true,
+		defaultSize:             4 * GigaByte,
+		partitionTableGenerator: defaultPartitionTable,
+		assembler: func(pt *disk.PartitionTable, options distro.ImageOptions, arch distro.Arch) *osbuild.Assembler {
+			return qemuAssembler(pt, "vpc", "disk.vhd", options, arch)
 		},
 	}
 
@@ -1013,11 +1075,12 @@ func New() distro.Distro {
 			// https://errata.devel.redhat.com/advisory/47339 lands
 			"timedatex",
 		},
-		kernelOptions: "ro net.ifnames=0",
-		bootable:      true,
-		defaultSize:   4 * GigaByte,
-		assembler: func(options distro.ImageOptions, arch distro.Arch) *osbuild.Assembler {
-			return qemuAssembler("vmdk", "disk.vmdk", options, arch)
+		kernelOptions:           "ro net.ifnames=0",
+		bootable:                true,
+		defaultSize:             4 * GigaByte,
+		partitionTableGenerator: defaultPartitionTable,
+		assembler: func(pt *disk.PartitionTable, options distro.ImageOptions, arch distro.Arch) *osbuild.Assembler {
+			return qemuAssembler(pt, "vmdk", "disk.vmdk", options, arch)
 		},
 	}
 
diff --git a/internal/kojiapi/server.go b/internal/kojiapi/server.go
index 55d60658b..aaf88f9f2 100644
--- a/internal/kojiapi/server.go
+++ b/internal/kojiapi/server.go
@@ -5,6 +5,7 @@ import (
 	"encoding/json"
 	"fmt"
 	"log"
+	"math/rand"
 	"net/http"
 	"strings"
 	"time"
@@ -87,6 +88,9 @@ func (h *apiHandlers) PostCompose(ctx echo.Context) error {
 	kojiFilenames := make([]string, len(request.ImageRequests))
 	kojiDirectory := "osbuild-composer-koji-" + uuid.New().String()
 
+	// use the same seed for all images so we get the same IDs
+	manifestSeed := rand.Int63()
+
 	for i, ir := range request.ImageRequests {
 		arch, err := d.GetArch(ir.Architecture)
 		if err != nil {
@@ -119,7 +123,7 @@ func (h *apiHandlers) PostCompose(ctx echo.Context) error {
 			return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Failed to depsolve build packages for %s/%s/%s: %s", ir.ImageType, ir.Architecture, request.Distribution, err))
 		}
 
-		manifest, err := imageType.Manifest(nil, distro.ImageOptions{Size: imageType.Size(0)}, repositories, packages, buildPackages)
+		manifest, err := imageType.Manifest(nil, distro.ImageOptions{Size: imageType.Size(0)}, repositories, packages, buildPackages, manifestSeed)
 		if err != nil {
 			return echo.NewHTTPError(http.StatusBadGateway, fmt.Sprintf("Failed to get manifest for for %s/%s/%s: %s", ir.ImageType, ir.Architecture, request.Distribution, err))
 		}
diff --git a/internal/store/fixtures.go b/internal/store/fixtures.go
index 0fc50723b..f72e8a592 100644
--- a/internal/store/fixtures.go
+++ b/internal/store/fixtures.go
@@ -57,7 +57,7 @@ func FixtureBase() *Store {
 	if err != nil {
 		panic("invalid image type qcow2 for x86_64 @ fedoratest")
 	}
-	manifest, err := imgType.Manifest(nil, distro.ImageOptions{}, nil, nil, nil)
+	manifest, err := imgType.Manifest(nil, distro.ImageOptions{}, nil, nil, nil, 0)
 	if err != nil {
 		panic("could not create manifest")
 	}
@@ -160,7 +160,7 @@ func FixtureFinished() *Store {
 	if err != nil {
 		panic("invalid image type qcow2 for x86_64 @ fedoratest")
 	}
-	manifest, err := imgType.Manifest(nil, distro.ImageOptions{}, nil, nil, nil)
+	manifest, err := imgType.Manifest(nil, distro.ImageOptions{}, nil, nil, nil, 0)
 	if err != nil {
 		panic("could not create manifest")
 	}
diff --git a/internal/weldr/api.go b/internal/weldr/api.go
index f13614c6a..f1800aec6 100644
--- a/internal/weldr/api.go
+++ b/internal/weldr/api.go
@@ -8,6 +8,7 @@ import (
 	"fmt"
 	"io"
 	"log"
+	"math/rand"
 	"net"
 	"net/http"
 	"net/url"
@@ -1846,7 +1847,8 @@ func (api *API) composeHandler(writer http.ResponseWriter, request *http.Request
 		},
 		api.allRepositories(),
 		packages,
-		buildPackages)
+		buildPackages,
+		rand.Int63())
 	if err != nil {
 		errors := responseError{
 			ID:  "ManifestCreationFailed",


Test patch:
diff --git a/internal/distro/distro_test_common/distro_test_common.go b/internal/distro/distro_test_common/distro_test_common.go
index b8313fcfe..4dfde1cf4 100644
--- a/internal/distro/distro_test_common/distro_test_common.go
+++ b/internal/distro/distro_test_common/distro_test_common.go
@@ -8,6 +8,7 @@ import (
 	"path/filepath"
 	"testing"
 
+	"github.com/google/go-cmp/cmp"
 	"github.com/osbuild/osbuild-composer/internal/blueprint"
 	"github.com/osbuild/osbuild-composer/internal/distro"
 	"github.com/osbuild/osbuild-composer/internal/rpmmd"
@@ -15,6 +16,8 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
+const RandomTestSeed = 0
+
 func TestDistro_Manifest(t *testing.T, pipelinePath string, prefix string, distros ...distro.Distro) {
 	assert := assert.New(t)
 	fileNames, err := filepath.Glob(filepath.Join(pipelinePath, prefix))
@@ -88,14 +91,22 @@ func TestDistro_Manifest(t *testing.T, pipelinePath string, prefix string, distr
 				},
 				repos,
 				tt.RpmMD.Packages,
-				tt.RpmMD.BuildPackages)
+				tt.RpmMD.BuildPackages,
+				RandomTestSeed)
 
 			if (err == nil && tt.Manifest == nil) || (err != nil && tt.Manifest != nil) {
 				t.Errorf("distro.Manifest() error = %v", err)
 				return
 			}
 			if tt.Manifest != nil {
-				require.JSONEqf(t, string(tt.Manifest), string(got), "Distro: %s\nArch: %s\nImage type: %s\nTest case file: %s\n", d.Name(), arch.Name(), imageType.Name(), fileName)
+				var expected, actual interface{}
+				err = json.Unmarshal(tt.Manifest, &expected)
+				require.NoError(t, err)
+				err = json.Unmarshal(got, &actual)
+				require.NoError(t, err)
+
+				diff := cmp.Diff(expected, actual)
+				require.Emptyf(t, diff, "Distro: %s\nArch: %s\nImage type: %s\nTest case file: %s\n", d.Name(), arch.Name(), imageType.Name(), fileName)
 			}
 		})
 	}
diff --git a/internal/distro/fedoratest/distro.go b/internal/distro/fedoratest/distro.go
index 8cb644915..b262aacd3 100644
--- a/internal/distro/fedoratest/distro.go
+++ b/internal/distro/fedoratest/distro.go
@@ -95,7 +95,8 @@ func (t *imageType) Manifest(c *blueprint.Customizations,
 	options distro.ImageOptions,
 	repos []rpmmd.RepoConfig,
 	packageSpecs,
-	buildPackageSpecs []rpmmd.PackageSpec) (distro.Manifest, error) {
+	buildPackageSpecs []rpmmd.PackageSpec,
+	seed int64) (distro.Manifest, error) {
 
 	return json.Marshal(
 		osbuild.Manifest{
diff --git a/internal/distro/test_distro/distro.go b/internal/distro/test_distro/distro.go
index a0d2c9dd9..a0c610944 100644
--- a/internal/distro/test_distro/distro.go
+++ b/internal/distro/test_distro/distro.go
@@ -75,7 +75,7 @@ func (t *TestImageType) BuildPackages() []string {
 	return nil
 }
 
-func (t *TestImageType) Manifest(b *blueprint.Customizations, options distro.ImageOptions, repos []rpmmd.RepoConfig, packageSpecs, buildPackageSpecs []rpmmd.PackageSpec) (distro.Manifest, error) {
+func (t *TestImageType) Manifest(b *blueprint.Customizations, options distro.ImageOptions, repos []rpmmd.RepoConfig, packageSpecs, buildPackageSpecs []rpmmd.PackageSpec, seed int64) (distro.Manifest, error) {
 	return json.Marshal(
 		osbuild.Manifest{
 			Sources:  osbuild.Sources{},
diff --git a/internal/store/store_test.go b/internal/store/store_test.go
index 674b073cd..9a5844149 100644
--- a/internal/store/store_test.go
+++ b/internal/store/store_test.go
@@ -51,7 +51,7 @@ func (suite *storeTest) SetupSuite() {
 	suite.myDistro = test_distro.New()
 	suite.myArch, _ = suite.myDistro.GetArch("test_arch")
 	suite.myImageType, _ = suite.myArch.GetImageType("test_type")
-	suite.myManifest, _ = suite.myImageType.Manifest(&suite.myCustomizations, suite.myImageOptions, suite.myRepoConfig, nil, suite.myPackageSpec)
+	suite.myManifest, _ = suite.myImageType.Manifest(&suite.myCustomizations, suite.myImageOptions, suite.myRepoConfig, nil, suite.myPackageSpec, 0)
 	suite.mySourceConfig = SourceConfig{
 		Name: "testSourceConfig",
 	}
diff --git a/internal/weldr/api_test.go b/internal/weldr/api_test.go
index a0d7120d0..86bd19470 100644
--- a/internal/weldr/api_test.go
+++ b/internal/weldr/api_test.go
@@ -461,7 +461,7 @@ func TestCompose(t *testing.T) {
 	require.NoError(t, err)
 	imgType, err := arch.GetImageType("qcow2")
 	require.NoError(t, err)
-	manifest, err := imgType.Manifest(nil, distro.ImageOptions{}, nil, nil, nil)
+	manifest, err := imgType.Manifest(nil, distro.ImageOptions{}, nil, nil, nil, 0)
 	require.NoError(t, err)
 	expectedComposeLocal := &store.Compose{
 		Blueprint: &blueprint.Blueprint{
diff --git a/internal/worker/server_test.go b/internal/worker/server_test.go
index 864d3239b..277bd4d85 100644
--- a/internal/worker/server_test.go
+++ b/internal/worker/server_test.go
@@ -60,7 +60,7 @@ func TestCreate(t *testing.T) {
 	if err != nil {
 		t.Fatalf("error getting image type from arch")
 	}
-	manifest, err := imageType.Manifest(nil, distro.ImageOptions{Size: imageType.Size(0)}, nil, nil, nil)
+	manifest, err := imageType.Manifest(nil, distro.ImageOptions{Size: imageType.Size(0)}, nil, nil, nil, 0)
 	if err != nil {
 		t.Fatalf("error creating osbuild manifest")
 	}
@@ -84,7 +84,7 @@ func TestCancel(t *testing.T) {
 	if err != nil {
 		t.Fatalf("error getting image type from arch")
 	}
-	manifest, err := imageType.Manifest(nil, distro.ImageOptions{Size: imageType.Size(0)}, nil, nil, nil)
+	manifest, err := imageType.Manifest(nil, distro.ImageOptions{Size: imageType.Size(0)}, nil, nil, nil, 0)
 	if err != nil {
 		t.Fatalf("error creating osbuild manifest")
 	}
@@ -121,7 +121,7 @@ func TestUpdate(t *testing.T) {
 	if err != nil {
 		t.Fatalf("error getting image type from arch")
 	}
-	manifest, err := imageType.Manifest(nil, distro.ImageOptions{Size: imageType.Size(0)}, nil, nil, nil)
+	manifest, err := imageType.Manifest(nil, distro.ImageOptions{Size: imageType.Size(0)}, nil, nil, nil, 0)
 	if err != nil {
 		t.Fatalf("error creating osbuild manifest")
 	}
@@ -152,7 +152,7 @@ func TestUpload(t *testing.T) {
 	if err != nil {
 		t.Fatalf("error getting image type from arch")
 	}
-	manifest, err := imageType.Manifest(nil, distro.ImageOptions{Size: imageType.Size(0)}, nil, nil, nil)
+	manifest, err := imageType.Manifest(nil, distro.ImageOptions{Size: imageType.Size(0)}, nil, nil, nil, 0)
 	if err != nil {
 		t.Fatalf("error creating osbuild manifest")
 	}
diff --git a/test/data/manifests/rhel_84-x86_64-ami-boot.json b/test/data/manifests/rhel_84-x86_64-ami-boot.json
index 0ae555619..ff1861825 100644
--- a/test/data/manifests/rhel_84-x86_64-ami-boot.json
+++ b/test/data/manifests/rhel_84-x86_64-ami-boot.json
@@ -3065,7 +3065,7 @@
           "options": {
             "filesystems": [
               {
-                "uuid": "efe8afea-c0a8-45dc-8e6e-499279f6fa5d",
+                "uuid": "0194fdc2-fa2f-4cc0-81d3-ff12045b73c8",
                 "vfs_type": "xfs",
                 "path": "/",
                 "options": "defaults"
@@ -3083,7 +3083,7 @@
         {
           "name": "org.osbuild.grub2",
           "options": {
-            "root_fs_uuid": "efe8afea-c0a8-45dc-8e6e-499279f6fa5d",
+            "root_fs_uuid": "0194fdc2-fa2f-4cc0-81d3-ff12045b73c8",
             "kernel_opts": "console=ttyS0,115200n8 console=tty0 net.ifnames=0 rd.blacklist=nouveau nvme_core.io_timeout=4294967295 crashkernel=auto",
             "legacy": "i386-pc",
             "uefi": {
@@ -3140,7 +3140,7 @@
               "uuid": "6264D520-3FB9-423F-8AB8-7A0A8E3D3562",
               "filesystem": {
                 "type": "xfs",
-                "uuid": "efe8afea-c0a8-45dc-8e6e-499279f6fa5d",
+                "uuid": "0194fdc2-fa2f-4cc0-81d3-ff12045b73c8",
                 "label": "root",
                 "mountpoint": "/"
               }
@@ -8589,7 +8589,7 @@
   },
   "image-info": {
     "boot-environment": {
-      "kernelopts": "root=UUID=efe8afea-c0a8-45dc-8e6e-499279f6fa5d console=ttyS0,115200n8 console=tty0 net.ifnames=0 rd.blacklist=nouveau nvme_core.io_timeout=4294967295 crashkernel=auto"
+      "kernelopts": "root=UUID=0194fdc2-fa2f-4cc0-81d3-ff12045b73c8 console=ttyS0,115200n8 console=tty0 net.ifnames=0 rd.blacklist=nouveau nvme_core.io_timeout=4294967295 crashkernel=auto"
     },
     "bootloader": "grub",
     "bootmenu": [
@@ -8606,6 +8606,14 @@
       }
     ],
     "fstab": [
+      [
+        "UUID=0194fdc2-fa2f-4cc0-81d3-ff12045b73c8",
+        "/",
+        "xfs",
+        "defaults",
+        "0",
+        "0"
+      ],
       [
         "UUID=7B77-95E7",
         "/boot/efi",
@@ -8613,14 +8621,6 @@
         "defaults,uid=0,gid=0,umask=077,shortname=winnt",
         "0",
         "2"
-      ],
-      [
-        "UUID=efe8afea-c0a8-45dc-8e6e-499279f6fa5d",
-        "/",
-        "xfs",
-        "defaults",
-        "0",
-        "0"
       ]
     ],
     "groups": [
@@ -9106,7 +9106,7 @@
         "size": 6335479296,
         "start": 106954752,
         "type": "0FC63DAF-8483-4772-8E79-3D69D8477DE4",
-        "uuid": "efe8afea-c0a8-45dc-8e6e-499279f6fa5d"
+        "uuid": "0194fdc2-fa2f-4cc0-81d3-ff12045b73c8"
       }
     ],
     "passwd": [
@@ -9218,4 +9218,4 @@
     ],
     "timezone": "UTC"
   }
-}
\ No newline at end of file
+}
diff --git a/test/data/manifests/rhel_84-x86_64-openstack-boot.json b/test/data/manifests/rhel_84-x86_64-openstack-boot.json
index 482f74542..e9be656f4 100644
--- a/test/data/manifests/rhel_84-x86_64-openstack-boot.json
+++ b/test/data/manifests/rhel_84-x86_64-openstack-boot.json
@@ -3304,7 +3304,7 @@
           "options": {
             "filesystems": [
               {
-                "uuid": "efe8afea-c0a8-45dc-8e6e-499279f6fa5d",
+                "uuid": "0194fdc2-fa2f-4cc0-81d3-ff12045b73c8",
                 "vfs_type": "xfs",
                 "path": "/",
                 "options": "defaults"
@@ -3322,7 +3322,7 @@
         {
           "name": "org.osbuild.grub2",
           "options": {
-            "root_fs_uuid": "efe8afea-c0a8-45dc-8e6e-499279f6fa5d",
+            "root_fs_uuid": "0194fdc2-fa2f-4cc0-81d3-ff12045b73c8",
             "kernel_opts": "ro net.ifnames=0",
             "legacy": "i386-pc",
             "uefi": {
@@ -3389,7 +3389,7 @@
               "uuid": "6264D520-3FB9-423F-8AB8-7A0A8E3D3562",
               "filesystem": {
                 "type": "xfs",
-                "uuid": "efe8afea-c0a8-45dc-8e6e-499279f6fa5d",
+                "uuid": "0194fdc2-fa2f-4cc0-81d3-ff12045b73c8",
                 "label": "root",
                 "mountpoint": "/"
               }
@@ -9171,7 +9171,7 @@
   },
   "image-info": {
     "boot-environment": {
-      "kernelopts": "root=UUID=efe8afea-c0a8-45dc-8e6e-499279f6fa5d ro net.ifnames=0"
+      "kernelopts": "root=UUID=0194fdc2-fa2f-4cc0-81d3-ff12045b73c8 ro net.ifnames=0"
     },
     "bootloader": "grub",
     "bootmenu": [
@@ -9193,6 +9193,14 @@
       "cockpit"
     ],
     "fstab": [
+      [
+        "UUID=0194fdc2-fa2f-4cc0-81d3-ff12045b73c8",
+        "/",
+        "xfs",
+        "defaults",
+        "0",
+        "0"
+      ],
       [
         "UUID=7B77-95E7",
         "/boot/efi",
@@ -9200,14 +9208,6 @@
         "defaults,uid=0,gid=0,umask=077,shortname=winnt",
         "0",
         "2"
-      ],
-      [
-        "UUID=efe8afea-c0a8-45dc-8e6e-499279f6fa5d",
-        "/",
-        "xfs",
-        "defaults",
-        "0",
-        "0"
       ]
     ],
     "groups": [
@@ -9730,7 +9730,7 @@
         "size": 4187995648,
         "start": 106954752,
         "type": "0FC63DAF-8483-4772-8E79-3D69D8477DE4",
-        "uuid": "efe8afea-c0a8-45dc-8e6e-499279f6fa5d"
+        "uuid": "0194fdc2-fa2f-4cc0-81d3-ff12045b73c8"
       }
     ],
     "passwd": [
@@ -9846,4 +9846,4 @@
     ],
     "timezone": "UTC"
   }
-}
\ No newline at end of file
+}
diff --git a/test/data/manifests/rhel_84-x86_64-qcow2-boot.json b/test/data/manifests/rhel_84-x86_64-qcow2-boot.json
index f4483362e..a3f228af8 100644
--- a/test/data/manifests/rhel_84-x86_64-qcow2-boot.json
+++ b/test/data/manifests/rhel_84-x86_64-qcow2-boot.json
@@ -3433,7 +3433,7 @@
           "options": {
             "filesystems": [
               {
-                "uuid": "efe8afea-c0a8-45dc-8e6e-499279f6fa5d",
+                "uuid": "0194fdc2-fa2f-4cc0-81d3-ff12045b73c8",
                 "vfs_type": "xfs",
                 "path": "/",
                 "options": "defaults"
@@ -3451,7 +3451,7 @@
         {
           "name": "org.osbuild.grub2",
           "options": {
-            "root_fs_uuid": "efe8afea-c0a8-45dc-8e6e-499279f6fa5d",
+            "root_fs_uuid": "0194fdc2-fa2f-4cc0-81d3-ff12045b73c8",
             "kernel_opts": "console=tty0 console=ttyS0,115200n8 no_timer_check net.ifnames=0 crashkernel=auto",
             "legacy": "i386-pc",
             "uefi": {
@@ -3518,7 +3518,7 @@
               "uuid": "6264D520-3FB9-423F-8AB8-7A0A8E3D3562",
               "filesystem": {
                 "type": "xfs",
-                "uuid": "efe8afea-c0a8-45dc-8e6e-499279f6fa5d",
+                "uuid": "0194fdc2-fa2f-4cc0-81d3-ff12045b73c8",
                 "label": "root",
                 "mountpoint": "/"
               }
@@ -9507,7 +9507,7 @@
   },
   "image-info": {
     "boot-environment": {
-      "kernelopts": "root=UUID=efe8afea-c0a8-45dc-8e6e-499279f6fa5d console=tty0 console=ttyS0,115200n8 no_timer_check net.ifnames=0 crashkernel=auto"
+      "kernelopts": "root=UUID=0194fdc2-fa2f-4cc0-81d3-ff12045b73c8 console=tty0 console=ttyS0,115200n8 no_timer_check net.ifnames=0 crashkernel=auto"
     },
     "bootloader": "grub",
     "bootmenu": [
@@ -9524,6 +9524,14 @@
       }
     ],
     "fstab": [
+      [
+        "UUID=0194fdc2-fa2f-4cc0-81d3-ff12045b73c8",
+        "/",
+        "xfs",
+        "defaults",
+        "0",
+        "0"
+      ],
       [
         "UUID=7B77-95E7",
         "/boot/efi",
@@ -9531,14 +9539,6 @@
         "defaults,uid=0,gid=0,umask=077,shortname=winnt",
         "0",
         "2"
-      ],
-      [
-        "UUID=efe8afea-c0a8-45dc-8e6e-499279f6fa5d",
-        "/",
-        "xfs",
-        "defaults",
-        "0",
-        "0"
       ]
     ],
     "groups": [
@@ -10091,7 +10091,7 @@
         "size": 10630446592,
         "start": 106954752,
         "type": "0FC63DAF-8483-4772-8E79-3D69D8477DE4",
-        "uuid": "efe8afea-c0a8-45dc-8e6e-499279f6fa5d"
+        "uuid": "0194fdc2-fa2f-4cc0-81d3-ff12045b73c8"
       }
     ],
     "passwd": [
@@ -10219,4 +10219,4 @@
     ],
     "timezone": "UTC"
   }
-}
\ No newline at end of file
+}
diff --git a/test/data/manifests/rhel_84-x86_64-qcow2-customize.json b/test/data/manifests/rhel_84-x86_64-qcow2-customize.json
index 8e7c66918..3dc436645 100644
--- a/test/data/manifests/rhel_84-x86_64-qcow2-customize.json
+++ b/test/data/manifests/rhel_84-x86_64-qcow2-customize.json
@@ -3485,7 +3485,7 @@
           "options": {
             "filesystems": [
               {
-                "uuid": "efe8afea-c0a8-45dc-8e6e-499279f6fa5d",
+                "uuid": "0194fdc2-fa2f-4cc0-81d3-ff12045b73c8",
                 "vfs_type": "xfs",
                 "path": "/",
                 "options": "defaults"
@@ -3503,7 +3503,7 @@
         {
           "name": "org.osbuild.grub2",
           "options": {
-            "root_fs_uuid": "efe8afea-c0a8-45dc-8e6e-499279f6fa5d",
+            "root_fs_uuid": "0194fdc2-fa2f-4cc0-81d3-ff12045b73c8",
             "kernel_opts": "console=tty0 console=ttyS0,115200n8 no_timer_check net.ifnames=0 crashkernel=auto debug",
             "legacy": "i386-pc",
             "uefi": {
@@ -3628,7 +3628,7 @@
               "uuid": "6264D520-3FB9-423F-8AB8-7A0A8E3D3562",
               "filesystem": {
                 "type": "xfs",
-                "uuid": "efe8afea-c0a8-45dc-8e6e-499279f6fa5d",
+                "uuid": "0194fdc2-fa2f-4cc0-81d3-ff12045b73c8",
                 "label": "root",
                 "mountpoint": "/"
               }
@@ -9617,7 +9617,7 @@
   },
   "image-info": {
     "boot-environment": {
-      "kernelopts": "root=UUID=efe8afea-c0a8-45dc-8e6e-499279f6fa5d console=tty0 console=ttyS0,115200n8 no_timer_check net.ifnames=0 crashkernel=auto debug"
+      "kernelopts": "root=UUID=0194fdc2-fa2f-4cc0-81d3-ff12045b73c8 console=tty0 console=ttyS0,115200n8 no_timer_check net.ifnames=0 crashkernel=auto debug"
     },
     "bootloader": "grub",
     "bootmenu": [
@@ -9634,6 +9634,14 @@
       }
     ],
     "fstab": [
+      [
+        "UUID=0194fdc2-fa2f-4cc0-81d3-ff12045b73c8",
+        "/",
+        "xfs",
+        "defaults",
+        "0",
+        "0"
+      ],
       [
         "UUID=7B77-95E7",
         "/boot/efi",
@@ -9641,14 +9649,6 @@
         "defaults,uid=0,gid=0,umask=077,shortname=winnt",
         "0",
         "2"
-      ],
-      [
-        "UUID=efe8afea-c0a8-45dc-8e6e-499279f6fa5d",
-        "/",
-        "xfs",
-        "defaults",
-        "0",
-        "0"
       ]
     ],
     "groups": [
@@ -10203,7 +10203,7 @@
         "size": 10630446592,
         "start": 106954752,
         "type": "0FC63DAF-8483-4772-8E79-3D69D8477DE4",
-        "uuid": "efe8afea-c0a8-45dc-8e6e-499279f6fa5d"
+        "uuid": "0194fdc2-fa2f-4cc0-81d3-ff12045b73c8"
       }
     ],
     "passwd": [
@@ -10333,4 +10333,4 @@
     ],
     "timezone": "London"
   }
-}
\ No newline at end of file
+}
diff --git a/test/data/manifests/rhel_84-x86_64-vhd-boot.json b/test/data/manifests/rhel_84-x86_64-vhd-boot.json
index f323d6e04..a394cb7e1 100644
--- a/test/data/manifests/rhel_84-x86_64-vhd-boot.json
+++ b/test/data/manifests/rhel_84-x86_64-vhd-boot.json
@@ -3259,7 +3259,7 @@
           "options": {
             "filesystems": [
               {
-                "uuid": "efe8afea-c0a8-45dc-8e6e-499279f6fa5d",
+                "uuid": "0194fdc2-fa2f-4cc0-81d3-ff12045b73c8",
                 "vfs_type": "xfs",
                 "path": "/",
                 "options": "defaults"
@@ -3277,7 +3277,7 @@
         {
           "name": "org.osbuild.grub2",
           "options": {
-            "root_fs_uuid": "efe8afea-c0a8-45dc-8e6e-499279f6fa5d",
+            "root_fs_uuid": "0194fdc2-fa2f-4cc0-81d3-ff12045b73c8",
             "kernel_opts": "ro biosdevname=0 rootdelay=300 console=ttyS0 earlyprintk=ttyS0 net.ifnames=0",
             "legacy": "i386-pc",
             "uefi": {
@@ -3354,7 +3354,7 @@
               "uuid": "6264D520-3FB9-423F-8AB8-7A0A8E3D3562",
               "filesystem": {
                 "type": "xfs",
-                "uuid": "efe8afea-c0a8-45dc-8e6e-499279f6fa5d",
+                "uuid": "0194fdc2-fa2f-4cc0-81d3-ff12045b73c8",
                 "label": "root",
                 "mountpoint": "/"
               }
@@ -9082,7 +9082,7 @@
   },
   "image-info": {
     "boot-environment": {
-      "kernelopts": "root=UUID=efe8afea-c0a8-45dc-8e6e-499279f6fa5d ro biosdevname=0 rootdelay=300 console=ttyS0 earlyprintk=ttyS0 net.ifnames=0"
+      "kernelopts": "root=UUID=0194fdc2-fa2f-4cc0-81d3-ff12045b73c8 ro biosdevname=0 rootdelay=300 console=ttyS0 earlyprintk=ttyS0 net.ifnames=0"
     },
     "bootloader": "grub",
     "bootmenu": [
@@ -9104,6 +9104,14 @@
       "cockpit"
     ],
     "fstab": [
+      [
+        "UUID=0194fdc2-fa2f-4cc0-81d3-ff12045b73c8",
+        "/",
+        "xfs",
+        "defaults",
+        "0",
+        "0"
+      ],
       [
         "UUID=7B77-95E7",
         "/boot/efi",
@@ -9111,14 +9119,6 @@
         "defaults,uid=0,gid=0,umask=077,shortname=winnt",
         "0",
         "2"
-      ],
-      [
-        "UUID=efe8afea-c0a8-45dc-8e6e-499279f6fa5d",
-        "/",
-        "xfs",
-        "defaults",
-        "0",
-        "0"
       ]
     ],
     "groups": [
@@ -9636,7 +9636,7 @@
         "size": 4187995648,
         "start": 106954752,
         "type": "0FC63DAF-8483-4772-8E79-3D69D8477DE4",
-        "uuid": "efe8afea-c0a8-45dc-8e6e-499279f6fa5d"
+        "uuid": "0194fdc2-fa2f-4cc0-81d3-ff12045b73c8"
       }
     ],
     "passwd": [
@@ -9756,4 +9756,4 @@
     ],
     "timezone": "UTC"
   }
-}
\ No newline at end of file
+}
diff --git a/test/data/manifests/rhel_84-x86_64-vmdk-boot.json b/test/data/manifests/rhel_84-x86_64-vmdk-boot.json
index 0dddefc82..d22078710 100644
--- a/test/data/manifests/rhel_84-x86_64-vmdk-boot.json
+++ b/test/data/manifests/rhel_84-x86_64-vmdk-boot.json
@@ -3121,7 +3121,7 @@
           "options": {
             "filesystems": [
               {
-                "uuid": "efe8afea-c0a8-45dc-8e6e-499279f6fa5d",
+                "uuid": "0194fdc2-fa2f-4cc0-81d3-ff12045b73c8",
                 "vfs_type": "xfs",
                 "path": "/",
                 "options": "defaults"
@@ -3139,7 +3139,7 @@
         {
           "name": "org.osbuild.grub2",
           "options": {
-            "root_fs_uuid": "efe8afea-c0a8-45dc-8e6e-499279f6fa5d",
+            "root_fs_uuid": "0194fdc2-fa2f-4cc0-81d3-ff12045b73c8",
             "kernel_opts": "ro net.ifnames=0",
             "legacy": "i386-pc",
             "uefi": {
@@ -3206,7 +3206,7 @@
               "uuid": "6264D520-3FB9-423F-8AB8-7A0A8E3D3562",
               "filesystem": {
                 "type": "xfs",
-                "uuid": "efe8afea-c0a8-45dc-8e6e-499279f6fa5d",
+                "uuid": "0194fdc2-fa2f-4cc0-81d3-ff12045b73c8",
                 "label": "root",
                 "mountpoint": "/"
               }
@@ -8718,7 +8718,7 @@
   },
   "image-info": {
     "boot-environment": {
-      "kernelopts": "root=UUID=efe8afea-c0a8-45dc-8e6e-499279f6fa5d ro net.ifnames=0"
+      "kernelopts": "root=UUID=0194fdc2-fa2f-4cc0-81d3-ff12045b73c8 ro net.ifnames=0"
     },
     "bootloader": "grub",
     "bootmenu": [
@@ -8740,6 +8740,14 @@
       "cockpit"
     ],
     "fstab": [
+      [
+        "UUID=0194fdc2-fa2f-4cc0-81d3-ff12045b73c8",
+        "/",
+        "xfs",
+        "defaults",
+        "0",
+        "0"
+      ],
       [
         "UUID=7B77-95E7",
         "/boot/efi",
@@ -8747,14 +8755,6 @@
         "defaults,uid=0,gid=0,umask=077,shortname=winnt",
         "0",
         "2"
-      ],
-      [
-        "UUID=efe8afea-c0a8-45dc-8e6e-499279f6fa5d",
-        "/",
-        "xfs",
-        "defaults",
-        "0",
-        "0"
       ]
     ],
     "groups": [
@@ -9248,7 +9248,7 @@
         "size": 4187995648,
         "start": 106954752,
         "type": "0FC63DAF-8483-4772-8E79-3D69D8477DE4",
-        "uuid": "efe8afea-c0a8-45dc-8e6e-499279f6fa5d"
+        "uuid": "0194fdc2-fa2f-4cc0-81d3-ff12045b73c8"
       }
     ],
     "passwd": [
@@ -9365,4 +9365,4 @@
     ],
     "timezone": "UTC"
   }
-}
\ No newline at end of file
+}