Files
snapd/boot/cmdline_test.go

511 lines
15 KiB
Go

// -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2021 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package boot_test
import (
"fmt"
"os"
"path/filepath"
. "gopkg.in/check.v1"
"github.com/snapcore/snapd/boot"
"github.com/snapcore/snapd/boot/boottest"
"github.com/snapcore/snapd/bootloader"
"github.com/snapcore/snapd/bootloader/bootloadertest"
"github.com/snapcore/snapd/gadget/gadgettest"
"github.com/snapcore/snapd/osutil/kcmdline"
"github.com/snapcore/snapd/snap/snaptest"
"github.com/snapcore/snapd/testutil"
)
var _ = Suite(&kernelCommandLineSuite{})
// baseBootSuite is used to setup the common test environment
type kernelCommandLineSuite struct {
testutil.BaseTest
rootDir string
}
func (s *kernelCommandLineSuite) SetUpTest(c *C) {
s.BaseTest.SetUpTest(c)
s.rootDir = c.MkDir()
err := os.MkdirAll(filepath.Join(s.rootDir, "proc"), 0755)
c.Assert(err, IsNil)
restore := kcmdline.MockProcCmdline(filepath.Join(s.rootDir, "proc/cmdline"))
s.AddCleanup(restore)
}
func (s *kernelCommandLineSuite) mockProcCmdlineContent(c *C, newContent string) {
mockProcCmdline := filepath.Join(s.rootDir, "proc/cmdline")
err := os.WriteFile(mockProcCmdline, []byte(newContent), 0644)
c.Assert(err, IsNil)
}
func (s *kernelCommandLineSuite) TestModeAndLabel(c *C) {
for _, tc := range []struct {
cmd string
mode string
label string
err string
}{{
cmd: "snapd_recovery_mode= snapd_recovery_system=this-is-a-label other-option=foo",
mode: boot.ModeInstall,
label: "this-is-a-label",
}, {
cmd: "snapd_recovery_system=label foo=bar foobaz=\\0\\0123 snapd_recovery_mode=install",
label: "label",
mode: boot.ModeInstall,
}, {
cmd: "snapd_recovery_mode=run snapd_recovery_system=1234",
mode: boot.ModeRun,
}, {
cmd: "snapd_recovery_mode=recover snapd_recovery_system=1234",
label: "1234",
mode: boot.ModeRecover,
}, {
cmd: "snapd_recovery_mode=factory-reset snapd_recovery_system=1234",
label: "1234",
mode: boot.ModeFactoryReset,
}, {
cmd: "option=1 other-option=\0123 none",
err: "cannot detect mode nor recovery system to use",
}, {
cmd: "snapd_recovery_mode=install-foo",
err: `cannot use unknown mode "install-foo"`,
}, {
// no recovery system label
cmd: "snapd_recovery_mode=install foo=bar",
err: `cannot specify install mode without system label`,
}, {
cmd: "snapd_recovery_system=1234",
err: `cannot specify system label without a mode`,
}, {
// multiple kernel command line params end up using the last one - this
// effectively matches the kernel handling too
cmd: "snapd_recovery_mode=install snapd_recovery_system=1234 snapd_recovery_mode=run",
mode: "run",
// label gets unset because it's not used for run mode
label: "",
}, {
cmd: "snapd_recovery_system=not-this-one snapd_recovery_mode=install snapd_recovery_system=1234",
mode: "install",
label: "1234",
}, {
cmd: "snapd_recovery_mode=cloudimg-rootfs",
mode: boot.ModeRunCVM,
}} {
c.Logf("tc: %q", tc)
s.mockProcCmdlineContent(c, tc.cmd)
mode, label, err := boot.ModeAndRecoverySystemFromKernelCommandLine()
if tc.err == "" {
c.Assert(err, IsNil)
c.Check(mode, Equals, tc.mode)
c.Check(label, Equals, tc.label)
} else {
c.Assert(err, ErrorMatches, tc.err)
}
}
}
func (s *kernelCommandLineSuite) TestComposeCommandLineNotManagedHappy(c *C) {
model := boottest.MakeMockUC20Model()
bl := bootloadertest.Mock("btloader", c.MkDir())
bootloader.Force(bl)
defer bootloader.Force(nil)
cmdline, err := boot.ComposeRecoveryCommandLine(model, "20200314", "")
c.Assert(err, IsNil)
c.Assert(cmdline, Equals, "")
cmdline, err = boot.ComposeCommandLine(model, "")
c.Assert(err, IsNil)
c.Assert(cmdline, Equals, "")
tbl := bl.WithTrustedAssets()
bootloader.Force(tbl)
cmdline, err = boot.ComposeRecoveryCommandLine(model, "20200314", "")
c.Assert(err, IsNil)
c.Assert(cmdline, Equals, "snapd_recovery_mode=recover snapd_recovery_system=20200314")
cmdline, err = boot.ComposeCommandLine(model, "")
c.Assert(err, IsNil)
c.Assert(cmdline, Equals, "snapd_recovery_mode=run")
}
func (s *kernelCommandLineSuite) TestComposeCommandLineNotUC20(c *C) {
model := boottest.MakeMockModel()
bl := bootloadertest.Mock("btloader", c.MkDir())
bootloader.Force(bl)
defer bootloader.Force(nil)
cmdline, err := boot.ComposeRecoveryCommandLine(model, "20200314", "")
c.Assert(err, IsNil)
c.Check(cmdline, Equals, "")
cmdline, err = boot.ComposeCommandLine(model, "")
c.Assert(err, IsNil)
c.Check(cmdline, Equals, "")
}
func (s *kernelCommandLineSuite) TestComposeCommandLineManagedHappy(c *C) {
model := boottest.MakeMockUC20Model()
tbl := bootloadertest.Mock("btloader", c.MkDir()).WithTrustedAssets()
bootloader.Force(tbl)
defer bootloader.Force(nil)
tbl.StaticCommandLine = "panic=-1"
cmdline, err := boot.ComposeRecoveryCommandLine(model, "20200314", "")
c.Assert(err, IsNil)
c.Assert(cmdline, Equals, "snapd_recovery_mode=recover snapd_recovery_system=20200314 panic=-1")
cmdline, err = boot.ComposeCommandLine(model, "")
c.Assert(err, IsNil)
c.Assert(cmdline, Equals, "snapd_recovery_mode=run panic=-1")
cmdline, err = boot.ComposeRecoveryCommandLine(model, "20200314", "")
c.Assert(err, IsNil)
c.Assert(cmdline, Equals, "snapd_recovery_mode=recover snapd_recovery_system=20200314 panic=-1")
cmdline, err = boot.ComposeCommandLine(model, "")
c.Assert(err, IsNil)
c.Assert(cmdline, Equals, "snapd_recovery_mode=run panic=-1")
}
func (s *kernelCommandLineSuite) TestComposeCandidateCommandLineManagedHappy(c *C) {
model := boottest.MakeMockUC20Model()
tbl := bootloadertest.Mock("btloader", c.MkDir()).WithTrustedAssets()
bootloader.Force(tbl)
defer bootloader.Force(nil)
tbl.StaticCommandLine = "panic=-1"
tbl.CandidateStaticCommandLine = "candidate panic=0"
cmdline, err := boot.ComposeCandidateCommandLine(model, "")
c.Assert(err, IsNil)
c.Assert(cmdline, Equals, "snapd_recovery_mode=run candidate panic=0")
}
func (s *kernelCommandLineSuite) TestComposeCandidateRecoveryCommandLineManagedHappy(c *C) {
model := boottest.MakeMockUC20Model()
tbl := bootloadertest.Mock("btloader", c.MkDir()).WithTrustedAssets()
bootloader.Force(tbl)
defer bootloader.Force(nil)
tbl.StaticCommandLine = "panic=-1"
tbl.CandidateStaticCommandLine = "candidate panic=0"
cmdline, err := boot.ComposeCandidateRecoveryCommandLine(model, "1234", "")
c.Assert(err, IsNil)
c.Check(cmdline, Equals, "snapd_recovery_mode=recover snapd_recovery_system=1234 candidate panic=0")
cmdline, err = boot.ComposeCandidateRecoveryCommandLine(model, "", "")
c.Assert(err, ErrorMatches, "internal error: system is unset")
c.Check(cmdline, Equals, "")
}
const gadgetSnapYaml = `name: gadget
version: 1.0
type: gadget
`
func (s *kernelCommandLineSuite) TestComposeCommandLineWithGadget(c *C) {
model := boottest.MakeMockUC20Model()
mockGadgetYaml := `
volumes:
volumename:
bootloader: grub
`
tbl := bootloadertest.Mock("btloader", c.MkDir()).WithTrustedAssets()
bootloader.Force(tbl)
defer bootloader.Force(nil)
tbl.StaticCommandLine = "panic=-1"
tbl.CandidateStaticCommandLine = "candidate panic=0"
for _, tc := range []struct {
which string
files [][]string
expCommandLine string
errMsg string
}{{
which: "current",
files: [][]string{
{"cmdline.extra", "cmdline extra"},
},
expCommandLine: "snapd_recovery_mode=run panic=-1 cmdline extra",
}, {
which: "candidate",
files: [][]string{
{"cmdline.extra", "cmdline extra"},
},
expCommandLine: "snapd_recovery_mode=run candidate panic=0 cmdline extra",
}, {
which: "current",
files: [][]string{
{"cmdline.full", "cmdline full"},
},
expCommandLine: "snapd_recovery_mode=run cmdline full",
}, {
which: "candidate",
files: [][]string{
{"cmdline.full", "cmdline full"},
},
expCommandLine: "snapd_recovery_mode=run cmdline full",
}, {
which: "candidate",
files: [][]string{
{"cmdline.extra", `bad-quote="`},
},
errMsg: `cannot use kernel command line from gadget: invalid kernel command line in cmdline.extra: unbalanced quoting`,
}} {
sf := snaptest.MakeTestSnapWithFiles(c, gadgetSnapYaml, append([][]string{
{"meta/snap.yaml", gadgetSnapYaml},
{"meta/gadget.yaml", mockGadgetYaml},
}, tc.files...))
var cmdline string
var err error
switch tc.which {
case "current":
cmdline, err = boot.ComposeCommandLine(model, sf)
case "candidate":
cmdline, err = boot.ComposeCandidateCommandLine(model, sf)
default:
c.Fatalf("unexpected command line type")
}
if tc.errMsg == "" {
c.Assert(err, IsNil)
c.Assert(cmdline, Equals, tc.expCommandLine)
} else {
c.Assert(err, ErrorMatches, tc.errMsg)
}
}
}
func (s *kernelCommandLineSuite) TestComposeRecoveryCommandLineWithGadget(c *C) {
model := boottest.MakeMockUC20Model()
mockGadgetYaml := `
volumes:
volumename:
bootloader: grub
`
tbl := bootloadertest.Mock("btloader", c.MkDir()).WithTrustedAssets()
bootloader.Force(tbl)
defer bootloader.Force(nil)
tbl.StaticCommandLine = "panic=-1"
tbl.CandidateStaticCommandLine = "candidate panic=0"
system := "1234"
for _, tc := range []struct {
which string
files [][]string
expCommandLine string
errMsg string
}{{
which: "current",
files: [][]string{
{"cmdline.extra", "cmdline extra"},
},
expCommandLine: "snapd_recovery_mode=recover snapd_recovery_system=1234 panic=-1 cmdline extra",
}, {
which: "candidate",
files: [][]string{
{"cmdline.extra", "cmdline extra"},
},
expCommandLine: "snapd_recovery_mode=recover snapd_recovery_system=1234 candidate panic=0 cmdline extra",
}, {
which: "current",
files: [][]string{
{"cmdline.full", "cmdline full"},
},
expCommandLine: "snapd_recovery_mode=recover snapd_recovery_system=1234 cmdline full",
}, {
which: "candidate",
files: [][]string{
{"cmdline.full", "cmdline full"},
},
expCommandLine: "snapd_recovery_mode=recover snapd_recovery_system=1234 cmdline full",
}, {
which: "candidate",
files: [][]string{
{"cmdline.extra", `bad-quote="`},
},
errMsg: `cannot use kernel command line from gadget: invalid kernel command line in cmdline.extra: unbalanced quoting`,
}} {
sf := snaptest.MakeTestSnapWithFiles(c, gadgetSnapYaml, append([][]string{
{"meta/snap.yaml", gadgetSnapYaml},
{"meta/gadget.yaml", mockGadgetYaml},
}, tc.files...))
var cmdline string
var err error
switch tc.which {
case "current":
cmdline, err = boot.ComposeRecoveryCommandLine(model, system, sf)
case "candidate":
cmdline, err = boot.ComposeCandidateRecoveryCommandLine(model, system, sf)
default:
c.Fatalf("unexpected command line type")
}
if tc.errMsg == "" {
c.Assert(err, IsNil)
c.Assert(cmdline, Equals, tc.expCommandLine)
} else {
c.Assert(err, ErrorMatches, tc.errMsg)
}
}
}
func (s *kernelCommandLineSuite) TestBootVarsForGadgetCommandLine(c *C) {
model := &gadgettest.ModelCharacteristics{}
mockGadgetYaml := `
volumes:
volumename:
bootloader: grub
`
for _, tc := range []struct {
errMsg string
files [][]string
cmdlineAppend string
expectedVars map[string]string
append []string
remove []string
}{{
files: [][]string{
{"cmdline.extra", "foo bar baz"},
},
expectedVars: map[string]string{
"snapd_extra_cmdline_args": "",
"snapd_full_cmdline_args": "default foo bar baz",
},
}, {
files: [][]string{
{"cmdline.extra", "snapd.debug=1"},
},
expectedVars: map[string]string{
"snapd_extra_cmdline_args": "",
"snapd_full_cmdline_args": "default snapd.debug=1",
},
}, {
files: [][]string{
{"cmdline.extra", "snapd_foo"},
},
errMsg: `cannot use kernel command line from gadget: invalid kernel command line in cmdline.extra: disallowed kernel argument \"snapd_foo\"`,
}, {
files: [][]string{
{"cmdline.full", "full foo bar baz"},
},
expectedVars: map[string]string{
"snapd_extra_cmdline_args": "",
"snapd_full_cmdline_args": "full foo bar baz",
},
}, {
cmdlineAppend: "foo bar baz",
expectedVars: map[string]string{
"snapd_extra_cmdline_args": "",
"snapd_full_cmdline_args": "default foo bar baz",
},
}, {
files: [][]string{
{"cmdline.extra", "foo bar baz"},
},
cmdlineAppend: "x=y z",
expectedVars: map[string]string{
"snapd_extra_cmdline_args": "",
"snapd_full_cmdline_args": "default foo bar baz x=y z",
},
}, {
files: [][]string{
{"cmdline.full", "full foo bar baz"},
},
cmdlineAppend: "x=y z",
expectedVars: map[string]string{
"snapd_extra_cmdline_args": "",
"snapd_full_cmdline_args": "full foo bar baz x=y z",
},
}, {
// with no arguments boot variables should be cleared
files: [][]string{},
expectedVars: map[string]string{
"snapd_extra_cmdline_args": "",
"snapd_full_cmdline_args": "default",
},
}, {
expectedVars: map[string]string{
"snapd_extra_cmdline_args": "",
"snapd_full_cmdline_args": `default bar baz=* "with spaces"`,
},
append: []string{"bar", "baz=*", `'"with spaces"'`},
}, {
expectedVars: map[string]string{
"snapd_extra_cmdline_args": "",
"snapd_full_cmdline_args": "nodefault",
},
append: []string{"nodefault"},
remove: []string{"default"},
}, {
expectedVars: map[string]string{
"snapd_extra_cmdline_args": "",
"snapd_full_cmdline_args": " ",
},
remove: []string{"default"},
}} {
gadgetYaml := mockGadgetYaml
if len(tc.append) > 0 || len(tc.remove) > 0 {
gadgetYaml = fmt.Sprintf("%skernel-cmdline:\n", gadgetYaml)
}
if len(tc.append) > 0 {
gadgetYaml = fmt.Sprintf("%s append:\n", gadgetYaml)
}
for _, append := range tc.append {
gadgetYaml = fmt.Sprintf("%s - %s\n", gadgetYaml, append)
}
if len(tc.remove) > 0 {
gadgetYaml = fmt.Sprintf("%s remove:\n", gadgetYaml)
}
for _, remove := range tc.remove {
gadgetYaml = fmt.Sprintf("%s - %s\n", gadgetYaml, remove)
}
sf := snaptest.MakeTestSnapWithFiles(c, gadgetSnapYaml, append([][]string{
{"meta/snap.yaml", gadgetSnapYaml},
{"meta/gadget.yaml", gadgetYaml},
}, tc.files...))
vars, err := boot.BootVarsForTrustedCommandLineFromGadget(sf, tc.cmdlineAppend, "default", model)
if tc.errMsg == "" {
c.Assert(err, IsNil)
c.Assert(vars, DeepEquals, tc.expectedVars)
} else {
c.Assert(err, ErrorMatches, tc.errMsg)
}
}
}