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 imaged
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]: a349e4c659/fs/xfs/xfs_mount.c (L51)
This commit is contained in:
Ondřej Budai 2020-12-14 22:52:12 +01:00 committed by Ondřej Budai
parent ae0d1b8663
commit 973639d372
23 changed files with 150 additions and 114 deletions

View file

@ -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

View file

@ -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

View file

@ -16,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))
@ -89,7 +91,8 @@ 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)

View file

@ -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

View file

@ -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

View file

@ -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{

View file

@ -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

View file

@ -4,6 +4,8 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"math/rand"
"sort"
"github.com/osbuild/osbuild-composer/internal/disk"
@ -50,7 +52,7 @@ type imageType struct {
bootable bool
rpmOstree bool
defaultSize uint64
partitionTableGenerator func(imageOptions distro.ImageOptions, arch distro.Arch) disk.PartitionTable
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
}
@ -188,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
}
@ -230,10 +235,10 @@ 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)
table := t.partitionTableGenerator(options, t.arch, rng)
pt = &table
}
@ -497,7 +502,7 @@ func (t *imageType) selinuxStageOptions() *osbuild.SELinuxStageOptions {
}
}
func defaultPartitionTable(imageOptions distro.ImageOptions, arch distro.Arch) disk.PartitionTable {
func defaultPartitionTable(imageOptions distro.ImageOptions, arch distro.Arch, rng *rand.Rand) disk.PartitionTable {
if arch.Name() == "x86_64" {
return disk.PartitionTable{
Size: imageOptions.Size,
@ -531,7 +536,7 @@ func defaultPartitionTable(imageOptions distro.ImageOptions, arch distro.Arch) d
UUID: "6264D520-3FB9-423F-8AB8-7A0A8E3D3562",
Filesystem: &disk.Filesystem{
Type: "xfs",
UUID: "efe8afea-c0a8-45dc-8e6e-499279f6fa5d",
UUID: uuid.Must(newRandomUUIDFromReader(rng)).String(),
Label: "root",
Mountpoint: "/",
FSTabOptions: "defaults",
@ -567,7 +572,7 @@ func defaultPartitionTable(imageOptions distro.ImageOptions, arch distro.Arch) d
UUID: "6264D520-3FB9-423F-8AB8-7A0A8E3D3562",
Filesystem: &disk.Filesystem{
Type: "xfs",
UUID: "efe8afea-c0a8-45dc-8e6e-499279f6fa5d",
UUID: uuid.Must(newRandomUUIDFromReader(rng)).String(),
Label: "root",
Mountpoint: "/",
FSTabOptions: "defaults",
@ -592,7 +597,7 @@ func defaultPartitionTable(imageOptions distro.ImageOptions, arch distro.Arch) d
Start: 10240,
Filesystem: &disk.Filesystem{
Type: "xfs",
UUID: "efe8afea-c0a8-45dc-8e6e-499279f6fa5d",
UUID: uuid.Must(newRandomUUIDFromReader(rng)).String(),
Mountpoint: "/",
FSTabOptions: "defaults",
FSTabFreq: 0,
@ -612,7 +617,7 @@ func defaultPartitionTable(imageOptions distro.ImageOptions, arch distro.Arch) d
Bootable: true,
Filesystem: &disk.Filesystem{
Type: "xfs",
UUID: "efe8afea-c0a8-45dc-8e6e-499279f6fa5d",
UUID: uuid.Must(newRandomUUIDFromReader(rng)).String(),
Mountpoint: "/",
FSTabOptions: "defaults",
FSTabFreq: 0,
@ -673,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

View file

@ -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{},

View file

@ -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))
}

View file

@ -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")
}

View file

@ -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",
}

View file

@ -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",

View file

@ -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{

View file

@ -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")
}