Skip to content
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

boot,image: support image.Customizations.BootFlags #10225

Merged
merged 3 commits into from
May 3, 2021
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
20 changes: 20 additions & 0 deletions boot/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,26 @@ var (
SetNextBootFlags = setNextBootFlags
)

func SetBootFlagsInBootloader(flags []string, rootDir string) error {
blVars := make(map[string]string, 1)

if err := setImageBootFlags(flags, blVars); err != nil {
return err
}

// now find the recovery bootloader in the system dir and set the value on
// it
opts := &bootloader.Options{
Role: bootloader.RoleRecovery,
}
bl, err := bootloader.Find(rootDir, opts)
if err != nil {
return err
}

return bl.SetBootVars(blVars)
}

func (b *bootChain) SetModelAssertion(model *asserts.Model) {
b.model = model
}
Expand Down
22 changes: 5 additions & 17 deletions boot/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,9 @@ func serializeBootFlags(flags []string) string {
return strings.Join(nonEmptyFlags, ",")
}

// setImageBootFlags writes the provided flags to the bootenv of the recovery
// bootloader in the specified system rootDir. It is only meant to be called at
// prepare-image customization time by ubuntu-image/prepare-image.
func setImageBootFlags(flags []string, rootDir string) error {
// setImageBootFlags sets the provided flags in the provided
// bootenv-representing map. It first checks them.
func setImageBootFlags(flags []string, blVars map[string]string) error {
// check that the flagList is supported
if _, err := checkBootFlagList(flags, understoodBootFlags); err != nil {
return err
Expand All @@ -122,19 +121,8 @@ func setImageBootFlags(flags []string, rootDir string) error {
return fmt.Errorf("internal error: boot flags too large to fit inside bootenv value")
}

// now find the recovery bootloader in the system dir and set the value on
// it
opts := &bootloader.Options{
Role: bootloader.RoleRecovery,
}
bl, err := bootloader.Find(rootDir, opts)
if err != nil {
return err
}

return bl.SetBootVars(map[string]string{
"snapd_boot_flags": s,
})
blVars["snapd_boot_flags"] = s
return nil
}

// InitramfsActiveBootFlags returns the set of boot flags that are currently set
Expand Down
19 changes: 4 additions & 15 deletions boot/flags_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ func (s *bootFlagsSuite) TestInitramfsActiveBootFlagsUC20InstallModeHappy(c *C)
// if we set some flags via ubuntu-image customizations then we get them
// back

err = boot.SetImageBootFlags([]string{"factory"}, blDir)
err = boot.SetBootFlagsInBootloader([]string{"factory"}, blDir)
c.Assert(err, IsNil)

flags, err = boot.InitramfsActiveBootFlags(boot.ModeInstall)
Expand All @@ -122,11 +122,6 @@ func (s *bootFlagsSuite) TestInitramfsActiveBootFlagsUC20InstallModeHappy(c *C)
}

func (s *bootFlagsSuite) TestSetImageBootFlagsVerification(c *C) {
dir := c.MkDir()

dirs.SetRootDir(dir)
defer func() { dirs.SetRootDir("") }()

longVal := "longer-than-256-char-value"
for i := 0; i < 256; i++ {
longVal += "X"
Expand All @@ -135,18 +130,12 @@ func (s *bootFlagsSuite) TestSetImageBootFlagsVerification(c *C) {
r := boot.MockAdditionalBootFlags([]string{longVal})
defer r()

blDir := boot.InitramfsUbuntuSeedDir

setupRealGrub(c, blDir, "EFI/ubuntu", &bootloader.Options{Role: bootloader.RoleRecovery})

flags, err := boot.InitramfsActiveBootFlags(boot.ModeInstall)
c.Assert(err, IsNil)
c.Assert(flags, HasLen, 0)
blVars := make(map[string]string)

err = boot.SetImageBootFlags([]string{"not-a-real-flag"}, blDir)
err := boot.SetImageBootFlags([]string{"not-a-real-flag"}, blVars)
c.Assert(err, ErrorMatches, `unknown boot flags \[not-a-real-flag\] not allowed`)

err = boot.SetImageBootFlags([]string{longVal}, blDir)
err = boot.SetImageBootFlags([]string{longVal}, blVars)
c.Assert(err, ErrorMatches, "internal error: boot flags too large to fit inside bootenv value")
}

Expand Down
26 changes: 18 additions & 8 deletions boot/makebootable.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,15 +60,20 @@ type BootableSet struct {
// system bootable. It does not make a run system bootable, for that
// functionality see MakeRunnableSystem, which is meant to be used at runtime
// from UC20 install mode.
func MakeBootableImage(model *asserts.Model, rootdir string, bootWith *BootableSet, sealer *TrustedAssetsInstallObserver) error {
// For a UC20 image a set of boot flags that will be set in the recovery
// boot environment can be specified.
func MakeBootableImage(model *asserts.Model, rootdir string, bootWith *BootableSet, bootFlags []string) error {
if model.Grade() == asserts.ModelGradeUnset {
if len(bootFlags) != 0 {
return fmt.Errorf("no boot flags support for UC16/18")
}
return makeBootable16(model, rootdir, bootWith)
}

if !bootWith.Recovery {
return fmt.Errorf("internal error: MakeBootableImage called at runtime, use MakeRunnableSystem instead")
}
return makeBootable20(model, rootdir, bootWith)
return makeBootable20(model, rootdir, bootWith, bootFlags)
}

// makeBootable16 setups the image filesystem for boot with UC16
Expand Down Expand Up @@ -148,7 +153,7 @@ func makeBootable16(model *asserts.Model, rootdir string, bootWith *BootableSet)
return nil
}

func makeBootable20(model *asserts.Model, rootdir string, bootWith *BootableSet) error {
func makeBootable20(model *asserts.Model, rootdir string, bootWith *BootableSet, bootFlags []string) error {
// we can only make a single recovery system bootable right now
recoverySystems, err := filepath.Glob(filepath.Join(rootdir, "systems/*"))
if err != nil {
Expand All @@ -162,6 +167,13 @@ func makeBootable20(model *asserts.Model, rootdir string, bootWith *BootableSet)
return fmt.Errorf("internal error: recovery system label unset")
}

blVars := make(map[string]string, 3)
if len(bootFlags) != 0 {
if err := setImageBootFlags(bootFlags, blVars); err != nil {
return err
}
}

opts := &bootloader.Options{
PrepareImageTime: true,
// setup the recovery bootloader
Expand All @@ -185,11 +197,9 @@ func makeBootable20(model *asserts.Model, rootdir string, bootWith *BootableSet)
// bootloader, this env var is set on the ubuntu-seed root grubenv, and
// not on the recovery system grubenv in the systems/20200314/ subdir on
// ubuntu-seed
blVars := map[string]string{
"snapd_recovery_system": bootWith.RecoverySystemLabel,
// always set the mode as install
"snapd_recovery_mode": ModeInstall,
}
blVars["snapd_recovery_system"] = bootWith.RecoverySystemLabel
// always set the mode as install
blVars["snapd_recovery_mode"] = ModeInstall
if err := bl.SetBootVars(blVars); err != nil {
return fmt.Errorf("cannot set recovery environment: %v", err)
}
Expand Down
67 changes: 67 additions & 0 deletions boot/makebootable_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,73 @@ version: 5.0
c.Check(systemGenv.Get("snapd_recovery_kernel"), Equals, "/snaps/pc-kernel_5.snap")
}

func (s *makeBootable20Suite) TestMakeBootableImage20BootFlags(c *C) {
bootloader.Force(nil)
model := boottest.MakeMockUC20Model()

unpackedGadgetDir := c.MkDir()
grubRecoveryCfg := "#grub-recovery cfg"
grubRecoveryCfgAsset := "#grub-recovery cfg from assets"
grubCfg := "#grub cfg"
snaptest.PopulateDir(unpackedGadgetDir, [][]string{
{"grub-recovery.conf", grubRecoveryCfg},
{"grub.conf", grubCfg},
{"meta/snap.yaml", gadgetSnapYaml},
})
restore := assets.MockInternal("grub-recovery.cfg", []byte(grubRecoveryCfgAsset))
defer restore()

// on uc20 the seed layout if different
seedSnapsDirs := filepath.Join(s.rootdir, "/snaps")
err := os.MkdirAll(seedSnapsDirs, 0755)
c.Assert(err, IsNil)

baseFn, baseInfo := makeSnap(c, "core20", `name: core20
type: base
version: 5.0
`, snap.R(3))
baseInSeed := filepath.Join(seedSnapsDirs, baseInfo.Filename())
err = os.Rename(baseFn, baseInSeed)
c.Assert(err, IsNil)
kernelFn, kernelInfo := makeSnapWithFiles(c, "pc-kernel", `name: pc-kernel
type: kernel
version: 5.0
`, snap.R(5), [][]string{
{"kernel.efi", "I'm a kernel.efi"},
})
kernelInSeed := filepath.Join(seedSnapsDirs, kernelInfo.Filename())
err = os.Rename(kernelFn, kernelInSeed)
c.Assert(err, IsNil)

label := "20191209"
recoverySystemDir := filepath.Join("/systems", label)
bootWith := &boot.BootableSet{
Base: baseInfo,
BasePath: baseInSeed,
Kernel: kernelInfo,
KernelPath: kernelInSeed,
RecoverySystemDir: recoverySystemDir,
RecoverySystemLabel: label,
UnpackedGadgetDir: unpackedGadgetDir,
Recovery: true,
}
bootFlags := []string{"factory"}

err = boot.MakeBootableImage(model, s.rootdir, bootWith, bootFlags)
c.Assert(err, IsNil)

// ensure the correct recovery system configuration was set
seedGenv := grubenv.NewEnv(filepath.Join(s.rootdir, "EFI/ubuntu/grubenv"))
c.Assert(seedGenv.Load(), IsNil)
c.Check(seedGenv.Get("snapd_recovery_system"), Equals, label)
c.Check(seedGenv.Get("snapd_boot_flags"), Equals, "factory")

systemGenv := grubenv.NewEnv(filepath.Join(s.rootdir, recoverySystemDir, "grubenv"))
c.Assert(systemGenv.Load(), IsNil)
c.Check(systemGenv.Get("snapd_recovery_kernel"), Equals, "/snaps/pc-kernel_5.snap")

}

func (s *makeBootable20Suite) testMakeBootableImage20CustomKernelArgs(c *C, whichFile, content, errMsg string) {
bootloader.Force(nil)
model := boottest.MakeMockUC20Model()
Expand Down
39 changes: 28 additions & 11 deletions image/image_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,23 +54,37 @@ var (

func (custo *Customizations) validate(model *asserts.Model) error {
core20 := model.Grade() != asserts.ModelGradeUnset
switch {
case core20:
// TODO:UC20: consider supporting these with grade dangerous?
var unsupported []string
var unsupported []string
unsupportedConsoleConfDisable := func() {
if custo.ConsoleConf == "disabled" {
unsupported = append(unsupported, "console-conf disable")
}
}
unsupportedBootFlags := func() {
if len(custo.BootFlags) != 0 {
unsupported = append(unsupported, fmt.Sprintf("boot flags (%s)", strings.Join(custo.BootFlags, " ")))
}
}

kind := "UC16/18"
switch {
case core20:
Copy link
Contributor

Choose a reason for hiding this comment

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

nice

kind = "UC20"
// TODO:UC20: consider supporting these with grade dangerous?
unsupportedConsoleConfDisable()
if custo.CloudInitUserData != "" {
unsupported = append(unsupported, "cloud-init user-data")
}
if len(unsupported) != 0 {
return fmt.Errorf("cannot support with UC20 model requested customizations: %s", strings.Join(unsupported, ", "))
}
case model.Classic():
if custo.ConsoleConf == "disabled" {
return fmt.Errorf("cannot support with classic model console-conf disable")
}
kind = "classic"
unsupportedConsoleConfDisable()
unsupportedBootFlags()
default:
// UC16/18
unsupportedBootFlags()
}
if len(unsupported) != 0 {
return fmt.Errorf("cannot support with %s model requested customizations: %s", kind, strings.Join(unsupported, ", "))
}
return nil
}
Expand Down Expand Up @@ -174,6 +188,9 @@ func installCloudConfig(rootDir, gadgetDir string) error {
func customizeImage(rootDir, defaultsDir string, custo *Customizations) error {
// customize with cloud-init user-data
if custo.CloudInitUserData != "" {
// See
// https://cloudinit.readthedocs.io/en/latest/topics/dir_layout.html
// https://cloudinit.readthedocs.io/en/latest/topics/datasources/nocloud.html
varCloudDir := filepath.Join(rootDir, "/var/lib/cloud/seed/nocloud-net")
if err := os.MkdirAll(varCloudDir, 0755); err != nil {
return err
Expand Down Expand Up @@ -479,7 +496,7 @@ func setupSeed(tsto *ToolingStore, model *asserts.Model, opts *Options) error {
return err
}

if err := boot.MakeBootableImage(model, bootRootDir, bootWith, nil); err != nil {
if err := boot.MakeBootableImage(model, bootRootDir, bootWith, opts.Customizations.BootFlags); err != nil {
return err
}

Expand Down
36 changes: 35 additions & 1 deletion image/image_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1213,9 +1213,35 @@ func (s *imageSuite) TestPrepareClassicCustomizationsUnsupported(c *C) {
Customizations: image.Customizations{
ConsoleConf: "disabled",
CloudInitUserData: "cloud-init-user-data",
BootFlags: []string{"boot-flag"},
},
})
c.Assert(err, ErrorMatches, `cannot support with classic model console-conf disable`)
c.Assert(err, ErrorMatches, `cannot support with classic model requested customizations: console-conf disable, boot flags \(boot-flag\)`)
}

func (s *imageSuite) TestPrepareUC18CustomizationsUnsupported(c *C) {
restore := image.MockTrusted(s.StoreSigning.Trusted)
defer restore()

model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{
"architecture": "amd64",
"gadget": "pc18",
"kernel": "pc-kernel",
"base": "core18",
})
fn := filepath.Join(c.MkDir(), "model.assertion")
err := ioutil.WriteFile(fn, asserts.Encode(model), 0644)
c.Assert(err, IsNil)

err = image.Prepare(&image.Options{
ModelFile: fn,
Customizations: image.Customizations{
ConsoleConf: "disabled",
CloudInitUserData: "cloud-init-user-data",
BootFlags: []string{"boot-flag"},
},
})
c.Assert(err, ErrorMatches, `cannot support with UC16/18 model requested customizations: boot flags \(boot-flag\)`)
}

func (s *imageSuite) TestSetupSeedWithBaseLegacySnap(c *C) {
Expand Down Expand Up @@ -2730,6 +2756,9 @@ func (s *imageSuite) TestSetupSeedCore20Grub(c *C) {

opts := &image.Options{
PrepareDir: prepareDir,
Customizations: image.Customizations{
BootFlags: []string{"factory"},
},
}

err := image.SetupSeed(s.tsto, model, opts)
Expand Down Expand Up @@ -2802,6 +2831,7 @@ func (s *imageSuite) TestSetupSeedCore20Grub(c *C) {
c.Assert(seedGenv.Load(), IsNil)
c.Check(seedGenv.Get("snapd_recovery_system"), Equals, filepath.Base(systems[0]))
c.Check(seedGenv.Get("snapd_recovery_mode"), Equals, "install")
c.Check(seedGenv.Get("snapd_boot_flags"), Equals, "factory")

c.Check(s.stderr.String(), Equals, "")

Expand Down Expand Up @@ -2886,6 +2916,9 @@ func (s *imageSuite) TestSetupSeedCore20UBoot(c *C) {

opts := &image.Options{
PrepareDir: prepareDir,
Customizations: image.Customizations{
BootFlags: []string{"factory"},
},
}

err := image.SetupSeed(s.tsto, model, opts)
Expand Down Expand Up @@ -2915,6 +2948,7 @@ func (s *imageSuite) TestSetupSeedCore20UBoot(c *C) {
c.Assert(err, IsNil)
c.Assert(env.Get("snapd_recovery_system"), Equals, expectedLabel)
c.Assert(env.Get("snapd_recovery_mode"), Equals, "install")
c.Assert(env.Get("snapd_boot_flags"), Equals, "factory")

// check recovery system specific config
systems, err := filepath.Glob(filepath.Join(seeddir, "systems", "*"))
Expand Down
4 changes: 4 additions & 0 deletions image/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,8 @@ type Customizations struct {
// CloudInitUserData can optionally point to cloud init user-data
// (UC16/18 only)
CloudInitUserData string `json:"cloud-init-user-data"`
// BootFlags can be set to a list of boot flags
// to set in the recovery bootloader (UC20 only).
// Currently only the "factory" hint flag is supported.
BootFlags []string `json:"boot-flags"`
}