Skip to content
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
5 changes: 5 additions & 0 deletions internal/testdisk/partition.go
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,11 @@ func MakeFakePartitionTable(mntPoints ...string) *disk.PartitionTable {
}
swap.GenUUID(rng)
payload = swap
case "raw":
payload = &disk.Raw{
SourcePipeline: "build",
SourcePath: "/usr/lib/modules/5.0/aboot.img",
}
default:
payload = &disk.Filesystem{
Type: "ext4",
Expand Down
45 changes: 45 additions & 0 deletions pkg/bib/osinfo/osinfo.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,17 @@ type OSRelease struct {
IDLike []string
}

type KernelInfo struct {
Version string
HasAbootImg bool
}

type Info struct {
OSRelease OSRelease
UEFIVendor string
SELinuxPolicy string
ImageCustomization *blueprint.Customizations
KernelInfo *KernelInfo
}

func validateOSRelease(osrelease map[string]string) error {
Expand Down Expand Up @@ -119,6 +125,39 @@ func readImageCustomization(root string) (*blueprint.Customizations, error) {
return config.Customizations, nil
}

func readKernelInfo(root string) (*KernelInfo, error) {
modulesDir := path.Join(root, "usr/lib/modules")
entries, err := os.ReadDir(modulesDir)
if err != nil {
return nil, err
}

for _, e := range entries {
Comment thread
mvo5 marked this conversation as resolved.
if !e.IsDir() {
continue
}

// A kernel dir is valid if there is a vmlinuz in it.
// bootc checks that there is only one such dir, so we
// pick the first here
kernelDir := path.Join(modulesDir, e.Name())
kernelPath := path.Join(kernelDir, "vmlinuz")
_, err := os.Stat(kernelPath)
if err == nil {

abootPath := path.Join(kernelDir, "aboot.img")
_, err := os.Stat(abootPath)
hasAbootImg := err == nil
return &KernelInfo{
Version: e.Name(),
HasAbootImg: hasAbootImg,
}, nil
}
}

return nil, fmt.Errorf("no valid kernel modules directory")
}

func Load(root string) (*Info, error) {
osrelease, err := distro.ReadOSReleaseFromTree(root)
if err != nil {
Expand All @@ -138,6 +177,11 @@ func Load(root string) (*Info, error) {
return nil, err
}

kernelInfo, err := readKernelInfo(root)
if err != nil {
logrus.Debugf("cannot read kernel info: %v", err)
}

selinuxPolicy, err := readSelinuxPolicy(root)
if err != nil {
logrus.Debugf("cannot read selinux policy: %v, setting it to none", err)
Expand All @@ -161,5 +205,6 @@ func Load(root string) (*Info, error) {
UEFIVendor: vendor,
SELinuxPolicy: selinuxPolicy,
ImageCustomization: customization,
KernelInfo: kernelInfo,
}, nil
}
44 changes: 44 additions & 0 deletions pkg/bib/osinfo/osinfo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,3 +168,47 @@ func TestLoadInfo(t *testing.T) {
})
}
}

func TestLoadInfoKernel(t *testing.T) {
type testCase struct {
desc string
dirs []string
files []string
expected *KernelInfo
}

cases := []testCase{
// Incorrect kernel trees
{"nodir", []string{}, []string{"not-a-dir"}, nil},
{"novmlinuz", []string{"6.15.9-201.fc42.x86_64"}, []string{}, nil},
{"novmlinuz2", []string{"6.15.9-201.fc42.x86_64", "6.14.11-300.fc42.x86_64"}, []string{"not-a-dir"}, nil},
{"novmlinuz3", []string{"6.15.9-201.fc42.x86_64", "6.14.11-300.fc42.x86_64"}, []string{"6.15.9-201.fc42.x86_64/not-vmlinuz"}, nil},
// Correct kernel trees
{"noaboot", []string{"6.15.9-201.fc42.x86_64"}, []string{"6.15.9-201.fc42.x86_64/vmlinuz"}, &KernelInfo{"6.15.9-201.fc42.x86_64", false}},
{"aboot", []string{"6.15.9-201.fc42.x86_64"}, []string{"6.15.9-201.fc42.x86_64/vmlinuz", "6.15.9-201.fc42.x86_64/aboot.img"}, &KernelInfo{"6.15.9-201.fc42.x86_64", true}},
{"severaldirs", []string{"6.15.9-201.fc42.x86_64", "6.14.11-300.fc42.x86_64"}, []string{"6.14.11-300.fc42.x86_64/vmlinuz"}, &KernelInfo{"6.14.11-300.fc42.x86_64", false}},
}
for _, c := range cases {
t.Run(c.desc, func(t *testing.T) {
root := t.TempDir()
baseDir := path.Join(root, "usr/lib/modules")
require.NoError(t, os.MkdirAll(baseDir, 0755))
for _, dir := range c.dirs {
dirPath := path.Join(baseDir, dir)
require.NoError(t, os.MkdirAll(dirPath, 0755))
}
for _, file := range c.files {
filePath := path.Join(baseDir, file)
require.NoError(t, os.WriteFile(filePath, nil, 0644))
}
info, err := readKernelInfo(root)
if c.expected == nil {
require.Error(t, err)
assert.Nil(t, info)
} else {
require.NoError(t, err)
assert.Equal(t, info, c.expected)
}
})
}
}
31 changes: 31 additions & 0 deletions pkg/disk/raw.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package disk

import (
"reflect"
)

// Raw defines the payload for a raw partition. It's similar to a
// [Filesystem] but with fewer fields. It is a [PayloadEntity].
Comment thread
mvo5 marked this conversation as resolved.
type Raw struct {
SourcePipeline string
SourcePath string
}

func init() {
payloadEntityMap["raw"] = reflect.TypeOf(Raw{})
}

func (s *Raw) EntityName() string {
return "raw"
}

func (s *Raw) Clone() Entity {
if s == nil {
return nil
}

return &Raw{
SourcePipeline: s.SourcePipeline,
SourcePath: s.SourcePath,
}
}
11 changes: 11 additions & 0 deletions pkg/disk/raw_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package disk_test

import (
"testing"

"github.com/osbuild/images/pkg/disk"
)

func TestImplementsInterfacesCompileTimeCheckRaw(t *testing.T) {
var _ = disk.PayloadEntity(&disk.Raw{})
}
4 changes: 2 additions & 2 deletions pkg/osbuild/bootupd_stage.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,13 +118,13 @@ func genMountsForBootupd(source string, pt *disk.PartitionTable) ([]Mount, error
}
mount.Source = lv.Name
mounts = append(mounts, *mount)
case *disk.Swap:
case *disk.Swap, *disk.Raw:
// nothing to do
Comment thread
mvo5 marked this conversation as resolved.
default:
return nil, fmt.Errorf("expected LV payload %+[1]v to be mountable or swap, got %[1]T", lv.Payload)
}
}
case *disk.Swap:
case *disk.Swap, *disk.Raw:
// nothing to do
default:
return nil, fmt.Errorf("type %T not supported by bootupd handling yet", part.Payload)
Expand Down
2 changes: 2 additions & 0 deletions pkg/osbuild/device.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,8 @@ func deviceName(p disk.Entity) string {
return "btrfs-" + payload.UUID[:4]
case *disk.Swap:
return "swap-" + payload.UUID[:4]
case *disk.Raw:
return "raw-" + pathEscape(payload.SourcePath)
}
panic(fmt.Sprintf("unsupported device type in deviceName: '%T'", p))
}
Expand Down
54 changes: 28 additions & 26 deletions pkg/osbuild/mkfs_stage.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,29 @@ package osbuild

import (
"fmt"
"path/filepath"
"slices"
"strings"

"github.com/osbuild/images/internal/common"
"github.com/osbuild/images/pkg/disk"
)

// Helper to create the `devices` option for the stage with the right
// name such that the last device is the target.
func getDevicesForFsStage(path []disk.Entity, filename string) map[string]Device {
stageDevices, lastName := getDevices(path, filename, true)

// The last device in the chain must be named "device",
// because that's the device that mkfs and write-device stages
// run on. See the stage schemas for reference.
lastDevice := stageDevices[lastName]
delete(stageDevices, lastName)
stageDevices["device"] = lastDevice

return stageDevices
}

// GenFsStages generates a list of stages that create the filesystem and other
// related entities. Specifically, it creates stages for:
// - org.osbuild.mkfs.*: for all filesystems and btrfs volumes
Expand All @@ -20,15 +36,7 @@ func GenFsStages(pt *disk.PartitionTable, filename string) []*Stage {
genStage := func(ent disk.Entity, path []disk.Entity) error {
switch e := ent.(type) {
case *disk.Filesystem:
// TODO: extract last device renaming into helper
stageDevices, lastName := getDevices(path, filename, true)

// The last device in the chain must be named "device", because that's
// the device that mkfs stages run on. See the stage schemas for
// reference.
lastDevice := stageDevices[lastName]
delete(stageDevices, lastName)
stageDevices["device"] = lastDevice
stageDevices := getDevicesForFsStage(path, filename)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

❤️ Thanks for consolidating this!


switch e.GetFSType() {
case "xfs":
Expand Down Expand Up @@ -57,14 +65,7 @@ func GenFsStages(pt *disk.PartitionTable, filename string) []*Stage {
panic(fmt.Sprintf("unknown fs type: %s", e.GetFSType()))
}
case *disk.Btrfs:
stageDevices, lastName := getDevices(path, filename, true)

// The last device in the chain must be named "device", because that's
// the device that mkfs stages run on. See the stage schemas for
// reference.
lastDevice := stageDevices[lastName]
delete(stageDevices, lastName)
stageDevices["device"] = lastDevice
stageDevices := getDevicesForFsStage(path, filename)

options := &MkfsBtrfsStageOptions{
UUID: e.UUID,
Expand All @@ -84,21 +85,22 @@ func GenFsStages(pt *disk.PartitionTable, filename string) []*Stage {
mount := *NewBtrfsMount("volume", "device", "/", "", "")
stages = append(stages, NewBtrfsSubVol(&BtrfsSubVolOptions{subvolumes}, &stageDevices, &[]Mount{mount}))
case *disk.Swap:
// TODO: extract last device renaming into helper
stageDevices, lastName := getDevices(path, filename, true)

// The last device in the chain must be named "device", because that's
// the device that the mkswap stage runs on. See the stage schema
// for reference.
lastDevice := stageDevices[lastName]
delete(stageDevices, lastName)
stageDevices["device"] = lastDevice
stageDevices := getDevicesForFsStage(path, filename)

options := &MkswapStageOptions{
UUID: e.UUID,
Label: e.Label,
}
stages = append(stages, NewMkswapStage(options, stageDevices))
case *disk.Raw:
stageDevices := getDevicesForFsStage(path, filename)

inputName := "tree"
options := &WriteDeviceStageOptions{
From: fmt.Sprintf("input://%s", filepath.Join(inputName, e.SourcePath)),
}
inputs := NewPipelineTreeInputs(inputName, e.SourcePipeline)
stages = append(stages, NewWriteDeviceStage(options, inputs, stageDevices))
Comment thread
mvo5 marked this conversation as resolved.
}
return nil
}
Expand Down
72 changes: 72 additions & 0 deletions pkg/osbuild/mkfs_stages_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,78 @@ func TestGenFsStagesLVM(t *testing.T) {
}, stages)
}

func TestGenFsStagesRaw(t *testing.T) {
pt := testdisk.MakeFakePartitionTable("/", "/boot", "/boot/efi", "raw")
stages := GenFsStages(pt, "file.img")
assert.Equal(t, []*Stage{
{
Type: "org.osbuild.mkfs.ext4",
Options: &MkfsExt4StageOptions{
UUID: disk.RootPartitionUUID,
},
Devices: map[string]Device{
"device": {
Type: "org.osbuild.loopback",
Options: &LoopbackDeviceOptions{
Filename: "file.img",
Size: testdisk.FakePartitionSize / disk.DefaultSectorSize,
Lock: true,
},
},
},
},
{
Type: "org.osbuild.mkfs.ext4",
Options: &MkfsExt4StageOptions{
UUID: disk.DataPartitionUUID,
},
Devices: map[string]Device{
"device": {
Type: "org.osbuild.loopback",
Options: &LoopbackDeviceOptions{
Filename: "file.img",
Size: testdisk.FakePartitionSize / disk.DefaultSectorSize,
Lock: true,
},
},
},
},
{
Type: "org.osbuild.mkfs.fat",
Options: &MkfsFATStageOptions{
VolID: strings.ReplaceAll(disk.EFIFilesystemUUID, "-", ""),
},
Devices: map[string]Device{
"device": {
Type: "org.osbuild.loopback",
Options: &LoopbackDeviceOptions{
Filename: "file.img",
Size: testdisk.FakePartitionSize / disk.DefaultSectorSize,
Lock: true,
},
},
},
},
{
Type: "org.osbuild.write-device",
Options: &WriteDeviceStageOptions{
From: "input://tree/usr/lib/modules/5.0/aboot.img",
},
Inputs: NewPipelineTreeInputs("tree", "build"),
Devices: map[string]Device{
"device": {
Type: "org.osbuild.loopback",
Options: &LoopbackDeviceOptions{
Filename: "file.img",
Size: testdisk.FakePartitionSize / disk.DefaultSectorSize,
Lock: true,
},
},
},
},
}, stages)
}

func TestGenFsStagesUnhappy(t *testing.T) {
pt := &disk.PartitionTable{
Type: disk.PT_GPT,
Expand Down
16 changes: 16 additions & 0 deletions pkg/osbuild/write_device_stage.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package osbuild

type WriteDeviceStageOptions struct {
From string `json:"from"`
}

func (WriteDeviceStageOptions) isStageOptions() {}

func NewWriteDeviceStage(options *WriteDeviceStageOptions, inputs Inputs, devices map[string]Device) *Stage {
return &Stage{
Type: "org.osbuild.write-device",
Options: options,
Inputs: inputs,
Devices: devices,
}
}
Loading