bootloader/grub: implement composing of grub command line

The patch implements a helper for composing the kernel command line when using a
grub bootloader.

The command line is composed of 3 main elements, the static part defined in the
boot asset, plus extra arguments (usually set by snapd in the boot environment)
and mode arguments

Signed-off-by: Maciej Borzecki <maciej.zenon.borzecki@canonical.com>
This commit is contained in:
Maciej Borzecki
2020-07-08 14:14:29 +02:00
parent fb0c709b8f
commit b26f2ade50
3 changed files with 219 additions and 4 deletions

View File

@@ -104,7 +104,9 @@ func LkRuntimeMode(b Bootloader) bool {
}
var (
EditionFromDiskConfigAsset = editionFromDiskConfigAsset
EditionFromConfigAsset = editionFromConfigAsset
ConfigAssetFrom = configAssetFrom
EditionFromDiskConfigAsset = editionFromDiskConfigAsset
EditionFromConfigAsset = editionFromConfigAsset
ConfigAssetFrom = configAssetFrom
StaticCommandLineFromGrubAsset = staticCommandLineFromGrubAsset
SortSnapdKernelCommandLineArgsForGrub = sortSnapdKernelCommandLineArgsForGrub
)

View File

@@ -20,13 +20,18 @@
package bootloader
import (
"bufio"
"bytes"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/snapcore/snapd/bootloader/assets"
"github.com/snapcore/snapd/bootloader/grubenv"
"github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/strutil"
)
// sanity - grub implements the required interfaces
@@ -388,5 +393,91 @@ func (g *grub) ManagedAssets() []string {
//
// Implements ManagedAssetsBootloader for the grub bootloader.
func (g *grub) CommandLine(modeArgs, extraArgs string) (string, error) {
return "", fmt.Errorf("not implemented")
// we do not trust the on disk asset, use the built-in one
assetName := "grub.cfg"
if g.recovery {
assetName = "grub-recovery.cfg"
}
staticCmdline, err := staticCommandLineFromGrubAsset(assetName)
if err != nil {
return "", fmt.Errorf("cannot extract static command line element: %v", err)
}
args, err := strutil.KernelCommandLineSplit(modeArgs + " " + staticCmdline + " " + extraArgs)
if err != nil {
return "", fmt.Errorf("cannot use badly formatted kernel command line: %v", err)
}
// sort arguments so that they match their positions
args = sortSnapdKernelCommandLineArgsForGrub(args)
// join all argument with a single space, see
// grub-core/lib/cmdline.c:grub_create_loader_cmdline() for reference,
// arguments are separated by a single space, the space after last is
// replaced with terminating NULL
return strings.Join(args, " "), nil
}
// static command line is defined as:
// set snapd_static_cmdline_args='arg arg arg'\n
// or
// set snapd_static_cmdline_args='arg'\n
const grubStaticCmdlinePrefix = `set snapd_static_cmdline_args=`
const grubStaticCmdlineQuote = `'`
// staticCommandLineFromGrubAsset extracts the static command line element from
// grub boot config asset on disk.
func staticCommandLineFromGrubAsset(asset string) (string, error) {
gbc := assets.Internal(asset)
if gbc == nil {
return "", fmt.Errorf("internal error: asset %q not found", asset)
}
scanner := bufio.NewScanner(bytes.NewReader(gbc))
for scanner.Scan() {
line := scanner.Text()
if !strings.HasPrefix(line, grubStaticCmdlinePrefix) {
continue
}
// minimal length is the prefix + suffix with no content
minLength := len(grubStaticCmdlinePrefix) + len(grubStaticCmdlineQuote)*2
if !strings.HasPrefix(line, grubStaticCmdlinePrefix+grubStaticCmdlineQuote) ||
!strings.HasSuffix(line, grubStaticCmdlineQuote) ||
len(line) < minLength {
return "", fmt.Errorf("incorrect static command line format: %q", line)
}
cmdline := line[len(grubStaticCmdlinePrefix+grubStaticCmdlineQuote) : len(line)-len(grubStaticCmdlineQuote)]
return cmdline, nil
}
if err := scanner.Err(); err != nil {
return "", err
}
return "", nil
}
// sortSnapdKernelCommandLineArgsForGrub sorts the command line arguments so
// that the snapd_recovery_mode/system arguments are placed at the location that
// matches the built-in grub boot config assets. Other arguments remain in the order
// they appear.
func sortSnapdKernelCommandLineArgsForGrub(args []string) []string {
out := make([]string, 0, len(args))
modeArgs := []string(nil)
for _, arg := range args {
if strings.HasPrefix(arg, "snapd_recovery_") {
modeArgs = append(modeArgs, arg)
} else {
out = append(out, arg)
}
}
// see grub.cfg and grub-recovery.cfg assets, the order is:
// for run mode: snapd_recovery_mode=run <args>
// for recovery mode: snapd_recovery_mode=recover snapd_recovery_system=<label> <args>
for _, prefixOrder := range []string{"snapd_recovery_system=", "snapd_recovery_mode="} {
for i, marg := range modeArgs {
if strings.HasPrefix(marg, prefixOrder) {
modeArgs = append(modeArgs[:i], modeArgs[i+1:]...)
modeArgs = append([]string{marg}, modeArgs...)
break
}
}
}
return append(modeArgs, out...)
}

View File

@@ -821,3 +821,125 @@ this is updated grub.cfg
c.Assert(err, ErrorMatches, `open .*/EFI/ubuntu/grub.cfg\..+: permission denied`)
c.Assert(filepath.Join(s.grubEFINativeDir(), "grub.cfg"), testutil.FileEquals, oldConfig)
}
func (s *grubTestSuite) TestStaticCmdlineFromDiskAsset(c *C) {
grubCfg := assets.Internal("grub.cfg")
c.Assert(grubCfg, NotNil)
grubRecoveryCfg := assets.Internal("grub-recovery.cfg")
c.Assert(grubRecoveryCfg, NotNil)
longBootConfig := `# Snapd-Boot-Config-Edition: 2
this is a long boot config
set snapd_static_cmdline_args='foo bar baz'
`
for idx, tc := range []struct {
content string
cmdline string
errStr string
}{
{content: string(grubCfg), cmdline: "console=ttyS0 console=tty1 panic=-1"},
{content: string(grubRecoveryCfg), cmdline: "console=ttyS0 console=tty1 panic=-1"},
{
content: "set snapd_static_cmdline_args='foo=1 bar=2 baz=0x123'\n",
cmdline: "foo=1 bar=2 baz=0x123",
},
{
content: `set snapd_static_cmdline_args='foo=BIOS\x20Boot'`,
cmdline: `foo=BIOS\x20Boot`,
},
{
content: `set snapd_static_cmdline_args='addr=1$123'`,
cmdline: `addr=1$123`,
},
{
content: longBootConfig, cmdline: "foo bar baz",
},
// no static args
{content: "set snapd_static_cmdline_args=''\n", cmdline: ""},
{content: "some random script", cmdline: ""},
// malformed
{
content: "set snapd_static_cmdline_args=\n",
errStr: "incorrect static command line format: \"set snapd.*\"",
},
{
content: "set snapd_static_cmdline_args='\n",
errStr: "incorrect static command line format: \"set snapd.*\"",
},
} {
c.Logf("tc: %v", idx)
restore := assets.MockInternal("asset", []byte(tc.content))
cmdline, err := bootloader.StaticCommandLineFromGrubAsset("asset")
restore()
if tc.errStr != "" {
c.Assert(err, ErrorMatches, tc.errStr)
} else {
c.Assert(err, IsNil)
c.Check(cmdline, Equals, tc.cmdline)
}
}
}
func (s *grubTestSuite) TestCommandLineHappy(c *C) {
// pretend there's more than one space between arguments
grubCfg := `# Snapd-Boot-Config-Edition: 2
set snapd_static_cmdline_args='arg1 foo=123 panic=-1 arg2="with spaces "'
boot script
`
restore := assets.MockInternal("grub.cfg", []byte(grubCfg))
defer restore()
grubRecoveryCfg := `# Snapd-Boot-Config-Edition: 2
set snapd_static_cmdline_args='recovery config panic=-1 '
boot script
`
restore = assets.MockInternal("grub-recovery.cfg", []byte(grubRecoveryCfg))
defer restore()
// native EFI/ubuntu setup
s.makeFakeGrubEFINativeEnv(c, []byte(grubCfg))
opts := &bootloader.Options{NoSlashBoot: true}
g := bootloader.NewGrub(s.rootdir, opts)
c.Assert(g, NotNil)
mg, ok := g.(bootloader.ManagedAssetsBootloader)
c.Assert(ok, Equals, true)
modeArgs := ""
extraArgs := `extra_arg=1 extra_foo=-1 panic=3 baz="more spaces"`
args, err := mg.CommandLine(modeArgs, extraArgs)
c.Assert(err, IsNil)
c.Check(args, Equals, `arg1 foo=123 panic=-1 arg2="with spaces " extra_arg=1 extra_foo=-1 panic=3 baz="more spaces"`)
modeArgs = "snapd_recovery_mode=run"
args, err = mg.CommandLine(modeArgs, extraArgs)
c.Assert(err, IsNil)
c.Check(args, Equals, `snapd_recovery_mode=run arg1 foo=123 panic=-1 arg2="with spaces " extra_arg=1 extra_foo=-1 panic=3 baz="more spaces"`)
modeArgs = "snapd_recovery_system=20200202 snapd_recovery_mode=recover"
args, err = mg.CommandLine(modeArgs, extraArgs)
c.Assert(err, IsNil)
c.Check(args, Equals, `snapd_recovery_mode=recover snapd_recovery_system=20200202 arg1 foo=123 panic=-1 arg2="with spaces " extra_arg=1 extra_foo=-1 panic=3 baz="more spaces"`)
// now check the recovery bootloader
opts = &bootloader.Options{NoSlashBoot: true, Recovery: true}
mrg := bootloader.NewGrub(s.rootdir, opts).(bootloader.ManagedAssetsBootloader)
args, err = mrg.CommandLine(modeArgs, extraArgs)
c.Assert(err, IsNil)
// static command line from recovery asset
c.Check(args, Equals, `snapd_recovery_mode=recover snapd_recovery_system=20200202 recovery config panic=-1 extra_arg=1 extra_foo=-1 panic=3 baz="more spaces"`)
}
func (s *grubTestSuite) TestSortArgsForGrub(c *C) {
out := bootloader.SortSnapdKernelCommandLineArgsForGrub([]string{"foo", "bar", "snapd_recovery_mode=run", "panic=-1"})
c.Assert(out, DeepEquals, []string{"snapd_recovery_mode=run", "foo", "bar", "panic=-1"})
// recovery mode
out = bootloader.SortSnapdKernelCommandLineArgsForGrub([]string{
"snapd_recovery_system=1234", "foo",
"snapd_recovery_mode=recover", "panic=-1",
})
c.Assert(out, DeepEquals, []string{
"snapd_recovery_mode=recover", "snapd_recovery_system=1234",
"foo", "panic=-1",
})
}