osbuild2: add new Grub2LegacyStage for org.osbuild.grub2.legacy

Expose osbuild's `org.osbuild.grub2.legacy` stage.
This commit is contained in:
Christian Kellner 2022-05-27 18:12:02 +02:00
parent 75d7bbaaa3
commit 5082c36ca5
2 changed files with 261 additions and 0 deletions

View file

@ -0,0 +1,227 @@
package osbuild2
import (
"fmt"
"strings"
"github.com/google/uuid"
"github.com/osbuild/osbuild-composer/internal/common"
"github.com/osbuild/osbuild-composer/internal/disk"
)
type GRUB2FSDesc struct {
Device string `json:"device,omitempty"`
Label string `json:"label,omitempty"`
UUID *uuid.UUID `json:"uuid,omitempty"`
}
func (d GRUB2FSDesc) validate() error {
have := make([]string, 0, 3)
if d.Device != "" {
have = append(have, "`device`")
}
if d.Label != "" {
have = append(have, "`label`")
}
if d.UUID != nil {
have = append(have, "`uuid`")
}
count := len(have)
if count == 0 {
return fmt.Errorf("need `device`, `label`, or `uuid`")
} else if count > 1 {
return fmt.Errorf("must only specify one of %s", strings.Join(have, ", "))
}
return nil
}
type GRUB2Product struct {
Name string `json:"name"`
Version string `json:"version"`
Nick string `json:"nick,omitempty"`
}
func (p GRUB2Product) validate() error {
if p.Name == "" {
return fmt.Errorf("need `Name`")
}
if p.Version == "" {
return fmt.Errorf("need `Version`")
}
return nil
}
type GRUB2MenuEntry struct {
Default *bool `json:"default,omitempty"`
Id string `json:"id,omitempty"`
Kernel string `json:"kernel,omitempty"`
Product GRUB2Product `json:"product,omitempty"`
}
func (e GRUB2MenuEntry) validate() (err error) {
if e.Id == "" {
return fmt.Errorf("need `Id`")
}
if e.Kernel == "" {
return fmt.Errorf("need `Kernel`")
}
if err = e.Product.validate(); err != nil {
return fmt.Errorf("`Product` error: %w", err)
}
return nil
}
type GRUB2BIOS struct {
Platform string `json:"platform,"`
}
type GRUB2LegacyConfig struct {
GRUB2Config
CmdLine string `json:"cmdline,omitempty"`
Distributor string `json:"distributor,omitempty"`
}
type GRUB2LegacyStageOptions struct {
// Required
RootFS GRUB2FSDesc `json:"rootfs"`
Entries []GRUB2MenuEntry `json:"entries"`
// One of
BIOS *GRUB2BIOS `json:"bios,omitempty"`
UEFI *GRUB2UEFI `json:"uefi,omitempty"`
// Optional
BootFS *GRUB2FSDesc `json:"bootfs,omitempty"`
WriteDefaults *bool `json:"write_defaults,omitempty"`
Config *GRUB2LegacyConfig `json:"config,omitempty"`
}
func (GRUB2LegacyStageOptions) isStageOptions() {}
func MakeGrub2MenuEntries(id string, kernelVer string, product GRUB2Product, rescue bool) []GRUB2MenuEntry {
entries := []GRUB2MenuEntry{
{
Default: common.BoolToPtr(true),
Id: id,
Product: product,
Kernel: kernelVer,
},
}
if rescue {
entry := GRUB2MenuEntry{
Id: id,
Product: product,
Kernel: "0-rescue-ffffffffffffffffffffffffffffffff",
}
entries = append(entries, entry)
}
return entries
}
func NewGrub2LegacyStageOptions(cfg *GRUB2Config,
pt *disk.PartitionTable,
kernelOptions []string,
legacy string,
uefi string,
entries []GRUB2MenuEntry) *GRUB2LegacyStageOptions {
rootFs := pt.FindMountable("/")
if rootFs == nil {
panic("root filesystem must be defined for grub2 stage, this is a programming error")
}
kopts := strings.Join(kernelOptions, " ")
rootFsUUID := uuid.MustParse(rootFs.GetFSSpec().UUID)
stageOptions := GRUB2LegacyStageOptions{
RootFS: GRUB2FSDesc{UUID: &rootFsUUID},
Entries: entries,
Config: &GRUB2LegacyConfig{
CmdLine: kopts,
Distributor: "$(sed 's, release .*$,,g' /etc/system-release)",
},
}
if cfg != nil {
stageOptions.Config.GRUB2Config = *cfg
}
bootFs := pt.FindMountable("/boot")
if bootFs != nil {
bootFsUUID := uuid.MustParse(bootFs.GetFSSpec().UUID)
stageOptions.BootFS = &GRUB2FSDesc{UUID: &bootFsUUID}
}
if legacy != "" {
stageOptions.BIOS = &GRUB2BIOS{
Platform: legacy,
}
}
if uefi != "" {
stageOptions.UEFI = &GRUB2UEFI{
Vendor: uefi,
}
}
return &stageOptions
}
func (o GRUB2LegacyStageOptions) validate() error {
// Check we have the required options
err := o.RootFS.validate()
if err != nil {
return fmt.Errorf("`rootfs` error: %w", err)
}
if o.BIOS == nil && o.UEFI == nil {
return fmt.Errorf("need `BIOS` or `UEFI`")
}
if o.BIOS != nil && o.BIOS.Platform == "" {
return fmt.Errorf("need `BIOS.Platform`")
}
if o.UEFI != nil && o.UEFI.Vendor == "" {
return fmt.Errorf("need `UEFI.Vendor`")
}
if len(o.Entries) == 0 {
return fmt.Errorf("at least one entry is required")
}
for i, entry := range o.Entries {
if err = entry.validate(); err != nil {
return fmt.Errorf("menu entry %d: %w", i, err)
}
}
// check optional arguments
if o.BootFS != nil {
err = o.BootFS.validate()
if err != nil {
return fmt.Errorf("`bootfs` error: %w", err)
}
}
return nil
}
func NewGrub2LegacyStage(options *GRUB2LegacyStageOptions) *Stage {
if err := options.validate(); err != nil {
panic(fmt.Errorf("grub2.legacy validation failed: %w", err))
}
return &Stage{
Type: "org.osbuild.grub2.legacy",
Options: options,
}
}

View file

@ -0,0 +1,34 @@
package osbuild2
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestGrub2LegacyStage_Validation(t *testing.T) {
options := GRUB2LegacyStageOptions{}
err := options.validate()
assert.Error(t, err)
options.RootFS.Device = "/dev/sda"
err = options.validate()
assert.Error(t, err)
prod := GRUB2Product{
Name: "Fedora",
Nick: "Foo",
Version: "1",
}
options.Entries = MakeGrub2MenuEntries("id", "kernel", prod, false)
err = options.validate()
assert.Error(t, err)
options.BIOS = &GRUB2BIOS{
Platform: "i386-pc",
}
err = options.validate()
assert.NoError(t, err)
}