diff --git a/boot/boot_test.go b/boot/boot_test.go index 3fd7081a76d..5a499ce3c83 100644 --- a/boot/boot_test.go +++ b/boot/boot_test.go @@ -21,6 +21,7 @@ package boot_test import ( "errors" + "os" "path/filepath" "testing" @@ -405,6 +406,15 @@ func (s *bootSetSuite) TestCoreParticipant20SetNextSameKernelSnap(c *C) { coreDev := boottest.MockUC20Device("pc-kernel") c.Assert(coreDev.HasModeenv(), Equals, true) + // default modeenv state + m := &boot.Modeenv{ + Base: "core20_1.snap", + CurrentKernels: []string{"pc-kernel_1.snap"}, + } + err := m.Write("") + c.Assert(err, IsNil) + defer os.Remove(dirs.SnapModeenvFileUnder(dirs.GlobalRootDir)) + // set the current kernel kernel, err := snap.ParsePlaceInfoFromSnapFileName("pc-kernel_1.snap") c.Assert(err, IsNil) @@ -435,6 +445,11 @@ func (s *bootSetSuite) TestCoreParticipant20SetNextSameKernelSnap(c *C) { _, enableKernelCalls := s.bootloader.GetRunKernelImageFunctionSnapCalls("EnableTryKernel") c.Assert(enableKernelCalls, Equals, 0) + // the modeenv is still the same as well + m2, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Assert(m2.CurrentKernels, DeepEquals, []string{"pc-kernel_1.snap"}) + // finally we didn't call SetBootVars on the bootloader because nothing // changed c.Assert(s.bootloader.SetBootVarsCalls, Equals, 0) @@ -444,6 +459,15 @@ func (s *bootSetSuite) TestCoreParticipant20SetNextNewKernelSnap(c *C) { coreDev := boottest.MockUC20Device("pc-kernel") c.Assert(coreDev.HasModeenv(), Equals, true) + // default modeenv state + m := &boot.Modeenv{ + Base: "core20_1.snap", + CurrentKernels: []string{"pc-kernel_1.snap"}, + } + err := m.Write("") + c.Assert(err, IsNil) + defer os.Remove(dirs.SnapModeenvFileUnder(dirs.GlobalRootDir)) + // set the current kernel kernel, err := snap.ParsePlaceInfoFromSnapFileName("pc-kernel_1.snap") c.Assert(err, IsNil) @@ -477,6 +501,11 @@ func (s *bootSetSuite) TestCoreParticipant20SetNextNewKernelSnap(c *C) { // and we were asked to enable kernel2 as the try kernel actual, _ := s.bootloader.GetRunKernelImageFunctionSnapCalls("EnableTryKernel") c.Assert(actual, DeepEquals, []snap.PlaceInfo{kernel2}) + + // and that the modeenv now has this kernel listed + m2, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Assert(m2.CurrentKernels, DeepEquals, []string{"pc-kernel_1.snap", "pc-kernel_2.snap"}) } func (s *bootSetSuite) TestMarkBootSuccessful20KernelStatusTryingNoKernelSnapCleansUp(c *C) { @@ -663,9 +692,10 @@ func (s *bootSetSuite) TestMarkBootSuccessful20AllSnap(c *C) { // we were trying a base snap m := &boot.Modeenv{ - Base: "core20_1.snap", - TryBase: "core20_2.snap", - BaseStatus: boot.TryingStatus, + Base: "core20_1.snap", + TryBase: "core20_2.snap", + BaseStatus: boot.TryingStatus, + CurrentKernels: []string{"pc-kernel_1.snap", "pc-kernel_2.snap"}, } err := m.Write("") c.Assert(err, IsNil) @@ -708,6 +738,7 @@ func (s *bootSetSuite) TestMarkBootSuccessful20AllSnap(c *C) { c.Assert(m2.Base, Equals, "core20_2.snap") c.Assert(m2.TryBase, Equals, "") c.Assert(m2.BaseStatus, Equals, boot.DefaultStatus) + c.Assert(m2.CurrentKernels, DeepEquals, []string{"pc-kernel_2.snap"}) // do it again, verify its still valid err = boot.MarkBootSuccessful(coreDev) @@ -766,12 +797,13 @@ func (s *bootSetSuite) TestMarkBootSuccessfulBaseUpdate(c *C) { } func (s *bootSetSuite) TestMarkBootSuccessful20KernelUpdate(c *C) { - r := boottest.ForceModeenv(dirs.GlobalRootDir, &boot.Modeenv{ - Mode: "run", - RecoverySystem: "20191018", + // default modeenv + m := &boot.Modeenv{ Base: "core20_1.snap", - }) - defer r() + CurrentKernels: []string{"pc-kernel_1.snap", "pc-kernel_2.snap"}, + } + err := m.Write("") + c.Assert(err, IsNil) coreDev := boottest.MockUC20Device("some-snap") c.Assert(coreDev.HasModeenv(), Equals, true) @@ -782,7 +814,7 @@ func (s *bootSetSuite) TestMarkBootSuccessful20KernelUpdate(c *C) { // set the current Kernel kernel1, err := snap.ParsePlaceInfoFromSnapFileName("pc-kernel_1.snap") c.Assert(err, IsNil) - r = s.bootloader.SetRunKernelImageEnabledKernel(kernel1) + r := s.bootloader.SetRunKernelImageEnabledKernel(kernel1) defer r() // set the current try kernel @@ -807,6 +839,11 @@ func (s *bootSetSuite) TestMarkBootSuccessful20KernelUpdate(c *C) { _, nDisableTryCalls := s.bootloader.GetRunKernelImageFunctionSnapCalls("DisableTryKernel") c.Assert(nDisableTryCalls, Equals, 1) + // check that the new kernel is the only one in modeenv + m2, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Assert(m2.CurrentKernels, DeepEquals, []string{"pc-kernel_2.snap"}) + // do it again, verify its still valid err = boot.MarkBootSuccessful(coreDev) c.Assert(err, IsNil) diff --git a/boot/bootstate20.go b/boot/bootstate20.go index 3086391e154..840b04be65b 100644 --- a/boot/bootstate20.go +++ b/boot/bootstate20.go @@ -23,7 +23,6 @@ import ( "fmt" "github.com/snapcore/snapd/bootloader" - "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/snap" ) @@ -38,6 +37,28 @@ func newBootState20(typ snap.Type) bootState { } } +// +// modeenv methods +// + +type bootState20Modeenv struct { + modeenv *Modeenv +} + +func (bsm *bootState20Modeenv) loadModeenv() error { + // don't read modeenv multiple times + if bsm.modeenv != nil { + return nil + } + modeenv, err := ReadModeenv("") + if err != nil { + return fmt.Errorf("cannot get snap revision: unable to read modeenv: %v", err) + } + bsm.modeenv = modeenv + + return nil +} + // // kernel snap methods // @@ -62,6 +83,12 @@ type bootState20Kernel struct { // the kernel snap to try for setNext() tryKernelSnap snap.PlaceInfo + + // don't embed this struct - it will conflict with embedding + // bootState20Modeenv in bootState20Base when both bootState20Base and + // bootState20Kernel are embedded in bootState20MarkSuccessful + // also we only need to use it with setNext() + kModeenv bootState20Modeenv } func (ks20 *bootState20Kernel) loadBootenv() error { @@ -109,12 +136,12 @@ func (ks20 *bootState20Kernel) revisions() (curSnap, trySnap snap.PlaceInfo, try return nil, nil, "", fmt.Errorf("cannot identify kernel snap with bootloader %s: %v", ks20.ebl.Name(), err) } - tryKernel, tryKernelExists, err := ks20.ebl.TryKernel() - if err != nil { + tryKernel, err := ks20.ebl.TryKernel() + if err != nil && err != bootloader.ErrNoTryKernelRef { return nil, nil, "", fmt.Errorf("cannot identify try kernel snap with bootloader %s: %v", ks20.ebl.Name(), err) } - if tryKernelExists { + if err == nil { tryBootSn = tryKernel } @@ -129,15 +156,23 @@ func (ks20 *bootState20Kernel) markSuccessful(update bootStateUpdate) (bootState } // u should always be non-nil if err is nil + // save the tried kernel snap here u.triedKernelSnap = sn return u, nil } func (ks20 *bootState20Kernel) setNext(next snap.PlaceInfo) (rebootRequired bool, u bootStateUpdate, err error) { + // commit() for setNext() also needs to add to the kernels in modeenv + err = ks20.kModeenv.loadModeenv() + if err != nil { + return false, nil, err + } + nextStatus, err := genericSetNext(ks20, next) if err != nil { return false, nil, err } + // if we are setting a snap as a try snap, then we need to reboot rebootRequired = false if nextStatus == TryStatus { @@ -158,22 +193,38 @@ func (ks20 *bootState20Kernel) commit() error { // If we are about to try an update, and need to add the try-kernel symlink, // we need to do things in this order: - // 1. Add try-kernel symlink - // 2. Update kernel_status to "try" + // 1. Add the kernel snap to the modeenv + // 2. Create try-kernel symlink + // 3. Update kernel_status to "try" // - // This is because if we get rebooted in between 1 and 2, kernel_status - // is still unset and boot scripts proceeds to boot with the old kernel, - // effectively ignoring the try-kernel symlink. + // This is because if we get rebooted in before 3, kernel_status is still + // unset and boot scripts proceeds to boot with the old kernel, effectively + // ignoring the try-kernel symlink. // If we did it in the opposite order however, we would set kernel_status to // "try" and then get rebooted before we could create the try-kernel // symlink, so the bootloader would try to boot from the non-existent // try-kernel symlink and become broken. + // + // Adding the kernel snap to the modeenv's list of trusted kernel snaps can + // effectively happen any time before we update the kernel_status to "try" + // for the same reasoning as for creating the try-kernel symlink. Putting it + // first is currently a purely aesthetic choice. - // add the try-kernel symlink - // trySnap could be nil here if we called setNext on the current kernel - // snap + // add the kernel to the modeenv and add the try-kernel symlink + // tryKernelSnap could be nil here if we called setNext on the current + // kernel snap if ks20.tryKernelSnap != nil { - err := ks20.ebl.EnableTryKernel(ks20.tryKernelSnap) + // add the kernel to the modeenv + ks20.kModeenv.modeenv.CurrentKernels = append( + ks20.kModeenv.modeenv.CurrentKernels, + ks20.tryKernelSnap.Filename(), + ) + err := ks20.kModeenv.modeenv.Write("") + if err != nil { + return err + } + + err = ks20.ebl.EnableTryKernel(ks20.tryKernelSnap) if err != nil { return err } @@ -202,8 +253,7 @@ func (ks20 *bootState20Kernel) commit() error { // note that for markSuccessful() a different bootStateUpdate implementation is // returned, see bootState20MarkSuccessful type bootState20Base struct { - // the modeenv for the base snap, initialized with loadModeenv() - modeenv *Modeenv + bootState20Modeenv // the base_status to be written to the modeenv, stored separately to // eliminate unnecessary writes to the modeenv when it's already in the @@ -222,7 +272,7 @@ func (bs20 *bootState20Base) loadModeenv() error { if bs20.modeenv != nil { return nil } - modeenv, err := ReadModeenv(dirs.GlobalRootDir) + modeenv, err := ReadModeenv("") if err != nil { return fmt.Errorf("cannot get snap revision: unable to read modeenv: %v", err) } @@ -424,6 +474,10 @@ func genericMarkSuccessful(b bootState, update bootStateUpdate) (bsmark *bootSta // this could end up auto-cleaning status variables for something it shouldn't // be. func (bsmark *bootState20MarkSuccessful) commit() error { + // the base and kernel snap updates will modify the modeenv, so we only + // issue a single write at the end if something changed + modeenvChanged := false + // kernel snap first, slightly higher priority // the ordering here is very important for boot reliability! @@ -435,6 +489,7 @@ func (bsmark *bootState20MarkSuccessful) commit() error { // 1. Update kernel_status to "" // 2. Move kernel symlink to point to the new try kernel // 3. Remove try-kernel symlink + // 4. Remove old kernel from modeenv // // If we got rebooted after step 1, then the bootloader is booting the wrong // kernel, but is at least booting a known good kernel and snapd in @@ -450,6 +505,13 @@ func (bsmark *bootState20MarkSuccessful) commit() error { // the boot failed, and revert to booting using the kernel symlink, but that // now points to the new kernel we were trying and we did not successfully // boot from that kernel to know we should trust it. + // + // Removing the old kernel from the modeenv needs to happen after it is + // impossible for the bootloader to boot from that kernel, otherwise we + // could end up in a state where the bootloader doesn't want to boot the + // new kernel, but the initramfs doesn't trust the old kernel and we are + // stuck. As such, do this last, after the symlink no longer exists. + // // The try-kernel symlink removal should happen last because it will not // affect anything, except that if it was removed before updating // kernel_status to "", the bootloader will think that the try kernel failed @@ -478,11 +540,15 @@ func (bsmark *bootState20MarkSuccessful) commit() error { return err } - // finally disable the try kernel symlink + // disable the try kernel symlink err = bsmark.ebl.DisableTryKernel() if err != nil { return err } + + // finally set current_kernels to be just this new kernel snap + bsmark.modeenv.CurrentKernels = []string{bsmark.triedKernelSnap.Filename()} + modeenvChanged = true } // base snap next @@ -491,13 +557,11 @@ func (bsmark *bootState20MarkSuccessful) commit() error { // atomic file writing operation, so it's not a concern if we get // rebooted during this snippet like it is with the kernel snap above - baseChanged := false - // always clear the base_status when marking successful, this has the useful // side-effect of cleaning up if we have base_status=trying but no try_base // set if bsmark.modeenv.BaseStatus != DefaultStatus { - baseChanged = true + modeenvChanged = true bsmark.modeenv.BaseStatus = DefaultStatus } @@ -506,18 +570,18 @@ func (bsmark *bootState20MarkSuccessful) commit() error { tryBase := bsmark.triedBaseSnap.Filename() if bsmark.modeenv.Base != tryBase { bsmark.modeenv.Base = tryBase - baseChanged = true + modeenvChanged = true } // clear the TryBase if bsmark.modeenv.TryBase != "" { bsmark.modeenv.TryBase = "" - baseChanged = true + modeenvChanged = true } } // write the modeenv - if baseChanged { + if modeenvChanged { return bsmark.modeenv.Write("") } diff --git a/boot/boottest/modeenv.go b/boot/boottest/modeenv.go index e6fb8543d43..997efda6973 100644 --- a/boot/boottest/modeenv.go +++ b/boot/boottest/modeenv.go @@ -23,6 +23,7 @@ import ( "os" "github.com/snapcore/snapd/boot" + "github.com/snapcore/snapd/dirs" ) // ForceModeenv forces ReadModeenv to always return a specific Modeenv for a @@ -30,7 +31,7 @@ import ( // If rootdir is empty, then all invocations return the specified modeenv func ForceModeenv(rootdir string, m *boot.Modeenv) (restore func()) { mock := func(callerrootdir string) (*boot.Modeenv, error) { - if rootdir == "" || callerrootdir == rootdir { + if rootdir == "" || rootdir == dirs.GlobalRootDir || callerrootdir == rootdir { return m, nil } diff --git a/boot/kernel_os_test.go b/boot/kernel_os_test.go index 2b0c351d79a..e0caf8e2164 100644 --- a/boot/kernel_os_test.go +++ b/boot/kernel_os_test.go @@ -22,6 +22,7 @@ package boot_test import ( "errors" "io/ioutil" + "os" "path/filepath" . "gopkg.in/check.v1" @@ -167,6 +168,15 @@ func (s *coreBootSetSuite) TestSetNextBoot20ForKernel(c *C) { coreDev := boottest.MockUC20Device("pc-kernel") c.Assert(coreDev.HasModeenv(), Equals, true) + // default modeenv state + m := &boot.Modeenv{ + Base: "core20_1.snap", + CurrentKernels: []string{"pc-kernel_1.snap"}, + } + err := m.Write("") + c.Assert(err, IsNil) + defer os.Remove(dirs.SnapModeenvFileUnder(dirs.GlobalRootDir)) + // setup current kernel kernel1, err := snap.ParsePlaceInfoFromSnapFileName("pc-kernel_1.snap") c.Assert(err, IsNil) @@ -202,6 +212,11 @@ func (s *coreBootSetSuite) TestSetNextBoot20ForKernel(c *C) { // check that SetNextBoot asked the bootloader for a kernel _, nKernelCalls := s.bootloader.GetRunKernelImageFunctionSnapCalls("Kernel") c.Assert(nKernelCalls, Equals, 1) + + // and that the modeenv now has this kernel listed + m2, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Assert(m2.CurrentKernels, DeepEquals, []string{"pc-kernel_1.snap", "pc-kernel_2.snap"}) } func (s *coreBootSetSuite) TestSetNextBootForKernelForTheSameKernel(c *C) { @@ -231,6 +246,15 @@ func (s *coreBootSetSuite) TestSetNextBoot20ForKernelForTheSameKernel(c *C) { coreDev := boottest.MockUC20Device("pc-kernel") c.Assert(coreDev.HasModeenv(), Equals, true) + // default modeenv state + m := &boot.Modeenv{ + Base: "core20_1.snap", + CurrentKernels: []string{"pc-kernel_1.snap"}, + } + err := m.Write("") + c.Assert(err, IsNil) + defer os.Remove(dirs.SnapModeenvFileUnder(dirs.GlobalRootDir)) + // setup current kernel kernel1, err := snap.ParsePlaceInfoFromSnapFileName("pc-kernel_1.snap") c.Assert(err, IsNil) @@ -262,6 +286,11 @@ func (s *coreBootSetSuite) TestSetNextBoot20ForKernelForTheSameKernel(c *C) { // check that SetNextBoot asked the bootloader for a kernel _, nKernelCalls := s.bootloader.GetRunKernelImageFunctionSnapCalls("Kernel") c.Assert(nKernelCalls, Equals, 1) + + // and that the modeenv now has this kernel listed + m2, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Assert(m2.CurrentKernels, DeepEquals, []string{"pc-kernel_1.snap"}) } func (s *coreBootSetSuite) TestSetNextBootForKernelForTheSameKernelTryMode(c *C) { @@ -296,6 +325,15 @@ func (s *coreBootSetSuite) TestSetNextBoot20ForKernelForTheSameKernelTryMode(c * coreDev := boottest.MockUC20Device("pc-kernel") c.Assert(coreDev.HasModeenv(), Equals, true) + // default modeenv state + m := &boot.Modeenv{ + Base: "core20_1.snap", + CurrentKernels: []string{"pc-kernel_1.snap"}, + } + err := m.Write("") + c.Assert(err, IsNil) + defer os.Remove(dirs.SnapModeenvFileUnder(dirs.GlobalRootDir)) + // setup current kernel kernel1, err := snap.ParsePlaceInfoFromSnapFileName("pc-kernel_1.snap") c.Assert(err, IsNil) @@ -332,6 +370,11 @@ func (s *coreBootSetSuite) TestSetNextBoot20ForKernelForTheSameKernelTryMode(c * // check that SetNextBoot asked the bootloader for a kernel _, nKernelCalls := s.bootloader.GetRunKernelImageFunctionSnapCalls("Kernel") c.Assert(nKernelCalls, Equals, 1) + + // and that the modeenv didn't change + m2, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Assert(m2.CurrentKernels, DeepEquals, m.CurrentKernels) } // ubootBootSetSuite tests the uboot specific code in the bootloader handling diff --git a/boot/makebootable.go b/boot/makebootable.go index 9f82f981918..1e61a8ce349 100644 --- a/boot/makebootable.go +++ b/boot/makebootable.go @@ -211,6 +211,7 @@ func makeBootable20RunMode(model *asserts.Model, rootdir string, bootWith *Boota Mode: "run", RecoverySystem: filepath.Base(bootWith.RecoverySystemDir), Base: filepath.Base(bootWith.BasePath), + CurrentKernels: []string{bootWith.Kernel.Filename()}, } if err := modeenv.Write(filepath.Join(runMnt, "ubuntu-data", "system-data")); err != nil { return fmt.Errorf("cannot write modeenv: %v", err) diff --git a/boot/makebootable_test.go b/boot/makebootable_test.go index de020c8b1d7..303a8eb93c3 100644 --- a/boot/makebootable_test.go +++ b/boot/makebootable_test.go @@ -356,5 +356,6 @@ version: 5.0 c.Check(ubuntuDataModeEnvPath, testutil.FileEquals, `mode=run recovery_system=20191216 base=core20_3.snap +current_kernels=pc-kernel_5.snap `) } diff --git a/boot/modeenv_test.go b/boot/modeenv_test.go index fb42629e311..fd4fe450756 100644 --- a/boot/modeenv_test.go +++ b/boot/modeenv_test.go @@ -110,7 +110,7 @@ base_status=try c.Check(modeenv.RecoverySystem, Equals, "20191126") c.Check(modeenv.Base, Equals, "core20_123.snap") c.Check(modeenv.TryBase, Equals, "core20_124.snap") - c.Check(modeenv.BaseStatus, Equals, "try") + c.Check(modeenv.BaseStatus, Equals, boot.TryStatus) } func (s *modeenvSuite) TestReadModeWithCurrentKernels(c *C) { @@ -186,7 +186,7 @@ func (s *modeenvSuite) TestWriteNonExistingFull(c *C) { RecoverySystem: "20191128", Base: "core20_321.snap", TryBase: "core20_322.snap", - BaseStatus: "try", + BaseStatus: boot.TryStatus, CurrentKernels: []string{"pc-kernel_1.snap", "pc-kernel_2.snap"}, } err := modeenv.Write(s.tmpdir) diff --git a/bootloader/androidboot_test.go b/bootloader/androidboot_test.go index 8ebfb959c37..0963904f48d 100644 --- a/bootloader/androidboot_test.go +++ b/bootloader/androidboot_test.go @@ -24,6 +24,7 @@ import ( . "gopkg.in/check.v1" + "github.com/snapcore/snapd/boot" "github.com/snapcore/snapd/bootloader" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/snap" @@ -55,13 +56,13 @@ func (s *androidBootTestSuite) TestNewAndroidboot(c *C) { func (s *androidBootTestSuite) TestSetGetBootVar(c *C) { a := bootloader.NewAndroidBoot(s.rootdir) - bootVars := map[string]string{"snap_mode": "try"} + bootVars := map[string]string{"snap_mode": boot.TryStatus} a.SetBootVars(bootVars) v, err := a.GetBootVars("snap_mode") c.Assert(err, IsNil) c.Check(v, HasLen, 1) - c.Check(v["snap_mode"], Equals, "try") + c.Check(v["snap_mode"], Equals, boot.TryStatus) } func (s *androidBootTestSuite) TestExtractKernelAssetsNoUnpacksKernel(c *C) { diff --git a/bootloader/bootloader.go b/bootloader/bootloader.go index cef70057f1d..c3d7068c4b5 100644 --- a/bootloader/bootloader.go +++ b/bootloader/bootloader.go @@ -31,8 +31,12 @@ import ( ) var ( - // ErrBootloader is returned if the bootloader can not be determined + // ErrBootloader is returned if the bootloader can not be determined. ErrBootloader = errors.New("cannot determine bootloader") + + // ErrNoTryKernelRef is returned if the bootloader finds no enabled + // try-kernel. + ErrNoTryKernelRef = errors.New("no try-kernel referenced") ) // Options carries bootloader options. @@ -55,18 +59,18 @@ type Options struct { } // Bootloader provides an interface to interact with the system -// bootloader +// bootloader. type Bootloader interface { - // Return the value of the specified bootloader variable + // Return the value of the specified bootloader variable. GetBootVars(names ...string) (map[string]string, error) - // Set the value of the specified bootloader variable + // Set the value of the specified bootloader variable. SetBootVars(values map[string]string) error - // Name returns the bootloader name + // Name returns the bootloader name. Name() string - // ConfigFile returns the name of the config file + // ConfigFile returns the name of the config file. ConfigFile() string // InstallBootConfig will try to install the boot config in the @@ -91,13 +95,43 @@ type RecoveryAwareBootloader interface { SetRecoverySystemEnv(recoverySystemDir string, values map[string]string) error } +// ExtractedRunKernelImageBootloader is a Bootloader that also supports specific +// methods needed to setup booting from an extracted kernel, which is needed to +// implement encryption and/or secure boot. Prototypical implementation is UC20 +// grub implementation with FDE. type ExtractedRunKernelImageBootloader interface { Bootloader - EnableKernel(snap.PlaceInfo) error // makes the symlink - EnableTryKernel(snap.PlaceInfo) error // makes the symlink - Kernel() (snap.PlaceInfo, error) // gives the symlink - TryKernel() (snap.PlaceInfo, bool, error) // gives the symlink (if exists) - DisableTryKernel() error // removes the symlink + + // EnableKernel enables the specified kernel on ubuntu-boot to be used + // during normal boots. The specified kernel should already have been + // extracted. This is usually implemented with a "kernel.efi" symlink + // pointing to the extracted kernel image. + EnableKernel(snap.PlaceInfo) error + + // EnableTryKernel enables the specified kernel on ubuntu-boot to be + // tried by the bootloader on a reboot, to be used in conjunction with + // setting "kernel_status" to "try". The specified kernel should already + // have been extracted. This is usually implemented with a + // "try-kernel.efi" symlink pointing to the extracted kernel image. + EnableTryKernel(snap.PlaceInfo) error + + // Kernel returns the current enabled kernel on the bootloader, not + // necessarily the kernel that was used to boot the current session, but the + // kernel that is enabled to boot on "normal" boots. + // If error is not nil, the first argument shall be non-nil. + Kernel() (snap.PlaceInfo, error) + + // TryKernel returns the current enabled try-kernel on the bootloader, if + // there is no such enabled try-kernel, then ErrNoTryKernelRef is returned. + // If error is not nil, the first argument shall be non-nil. + TryKernel() (snap.PlaceInfo, error) + + // DisableTryKernel disables the current enabled try-kernel on the + // bootloader, if it exists. It does not need to return an error if the + // enabled try-kernel does not exist or is in an inconsistent state before + // disabling it, errors should only be returned when the implementation + // fails to disable the try-kernel. + DisableTryKernel() error } func genericInstallBootConfig(gadgetFile, systemFile string) (bool, error) { diff --git a/bootloader/bootloader_test.go b/bootloader/bootloader_test.go index 2a609dbfd65..e431f1c9b3f 100644 --- a/bootloader/bootloader_test.go +++ b/bootloader/bootloader_test.go @@ -29,7 +29,6 @@ import ( "github.com/snapcore/snapd/bootloader" "github.com/snapcore/snapd/bootloader/bootloadertest" - //"github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/testutil" diff --git a/bootloader/bootloadertest/bootloadertest.go b/bootloader/bootloadertest/bootloadertest.go index b0cc7dc81ef..8e4eaa83248 100644 --- a/bootloader/bootloadertest/bootloadertest.go +++ b/bootloader/bootloadertest/bootloadertest.go @@ -23,6 +23,7 @@ import ( "fmt" "path/filepath" + "github.com/snapcore/snapd/boot" "github.com/snapcore/snapd/bootloader" "github.com/snapcore/snapd/snap" ) @@ -60,6 +61,7 @@ type MockBootloader struct { // ensure MockBootloader implements the Bootloader interface var _ bootloader.Bootloader = (*MockBootloader)(nil) +var _ bootloader.ExtractedRunKernelImageBootloader = (*MockBootloader)(nil) func Mock(name, bootdir string) *MockBootloader { return &MockBootloader{ @@ -121,10 +123,10 @@ func (b *MockBootloader) SetBootBase(base string) { } func (b *MockBootloader) SetTryingDuringReboot() error { - if b.BootVars["snap_mode"] != "try" { + if b.BootVars["snap_mode"] != boot.TryStatus { return fmt.Errorf("bootloader must be in 'try' mode") } - b.BootVars["snap_mode"] = "trying" + b.BootVars["snap_mode"] = boot.TryingStatus return nil } @@ -134,14 +136,14 @@ func (b *MockBootloader) SetTryingDuringReboot() error { // "snap_try_{core,kernel}" and "snap_mode" which means the "old" kernel,core // in "snap_{core,kernel}" will be used. func (b *MockBootloader) SetRollbackAcrossReboot() error { - if b.BootVars["snap_mode"] != "try" { + if b.BootVars["snap_mode"] != boot.TryStatus { return fmt.Errorf("rollback can only be simulated in 'try' mode") } if b.BootVars["snap_core"] == "" && b.BootVars["snap_kernel"] == "" { return fmt.Errorf("rollback can only be simulated if either snap_core or snap_kernel is set") } // clean try bootvars and snap_mode - b.BootVars["snap_mode"] = "" + b.BootVars["snap_mode"] = boot.DefaultStatus b.BootVars["snap_try_core"] = "" b.BootVars["snap_try_kernel"] = "" return nil @@ -249,16 +251,16 @@ func (b *MockBootloader) Kernel() (snap.PlaceInfo, error) { // TryKernel returns the current kernel set in the bootloader; part of // ExtractedRunKernelImageBootloader. -func (b *MockBootloader) TryKernel() (snap.PlaceInfo, bool, error) { +func (b *MockBootloader) TryKernel() (snap.PlaceInfo, error) { b.runKernelImageMockedNumCalls["TryKernel"]++ err := b.runKernelImageMockedErrs["TryKernel"] if err != nil { - return nil, false, err + return nil, err } if b.runKernelImageEnabledTryKernel == nil { - return nil, false, nil + return nil, bootloader.ErrNoTryKernelRef } - return b.runKernelImageEnabledTryKernel, true, nil + return b.runKernelImageEnabledTryKernel, nil } // DisableTryKernel removes the current try-kernel "symlink" set in the diff --git a/bootloader/grub.go b/bootloader/grub.go index b5039347674..13343a90f33 100644 --- a/bootloader/grub.go +++ b/bootloader/grub.go @@ -285,7 +285,7 @@ func (g *grub) Kernel() (snap.PlaceInfo, error) { // TryKernel will return the kernel snap currently being tried if it exists and // false if there is not currently a try-kernel.efi symlink. Note if the symlink // exists but does not point to an existing file an error will be returned. -func (g *grub) TryKernel() (snap.PlaceInfo, bool, error) { +func (g *grub) TryKernel() (snap.PlaceInfo, error) { // check that the _symlink_ exists, not that it points to something real // we check for whether it is a dangling symlink inside readKernelSymlink, // which returns an error when the symlink is dangling @@ -295,9 +295,9 @@ func (g *grub) TryKernel() (snap.PlaceInfo, bool, error) { // if we failed to read the symlink, then the try kernel isn't usable, // so return err because the symlink is there if err != nil { - return nil, false, err + return nil, err } - return p, true, nil + return p, nil } - return nil, false, nil + return nil, ErrNoTryKernelRef } diff --git a/bootloader/grub_test.go b/bootloader/grub_test.go index 75da8476c05..7c352833bac 100644 --- a/bootloader/grub_test.go +++ b/bootloader/grub_test.go @@ -348,9 +348,8 @@ func (s *grubTestSuite) TestGrubExtractedRunKernelImageTryKernel(c *C) { c.Assert(ok, Equals, true) // ensure it doesn't return anything when the symlink doesn't exist - _, exists, err := eg.TryKernel() - c.Assert(err, IsNil) - c.Assert(exists, Equals, false) + _, err := eg.TryKernel() + c.Assert(err, Equals, bootloader.ErrNoTryKernelRef) // when a bad kernel snap name is in the extracted path, it will complain // appropriately @@ -366,9 +365,8 @@ func (s *grubTestSuite) TestGrubExtractedRunKernelImageTryKernel(c *C) { err = os.Symlink("bad_snap_rev_name/kernel.efi", tryKernelSymlink) c.Assert(err, IsNil) - _, exists, err = eg.TryKernel() + _, err = eg.TryKernel() c.Assert(err, ErrorMatches, "cannot parse kernel snap file name from symlink target \"bad_snap_rev_name\": .*") - c.Assert(exists, Equals, false) // remove the bad symlink err = os.Remove(tryKernelSymlink) @@ -378,16 +376,14 @@ func (s *grubTestSuite) TestGrubExtractedRunKernelImageTryKernel(c *C) { tryKernel := s.makeKernelAssetSnapAndSymlink(c, "pc-kernel_2.snap", "try-kernel.efi") // ensure that the returned kernel is the same as the one we put there - sn, exists, err := eg.TryKernel() + sn, err := eg.TryKernel() c.Assert(err, IsNil) - c.Assert(exists, Equals, true) c.Assert(sn, DeepEquals, tryKernel) // if the destination of the symlink is removed, we get an error err = os.Remove(filepath.Join(s.grubDir(), "pc-kernel_2.snap", "kernel.efi")) c.Assert(err, IsNil) - _, exists, err = eg.TryKernel() - c.Assert(exists, Equals, false) + _, err = eg.TryKernel() c.Assert(err, ErrorMatches, "cannot read dangling symlink try-kernel.efi") } diff --git a/bootloader/lk_test.go b/bootloader/lk_test.go index f4956a3896e..04abf50c668 100644 --- a/bootloader/lk_test.go +++ b/bootloader/lk_test.go @@ -27,6 +27,7 @@ import ( . "gopkg.in/check.v1" + "github.com/snapcore/snapd/boot" "github.com/snapcore/snapd/bootloader" "github.com/snapcore/snapd/bootloader/lkenv" "github.com/snapcore/snapd/osutil" @@ -67,13 +68,13 @@ func (s *lkTestSuite) TestNewLkImageBuildingTime(c *C) { func (s *lkTestSuite) TestSetGetBootVar(c *C) { bootloader.MockLkFiles(c, s.rootdir, nil) l := bootloader.NewLk(s.rootdir, nil) - bootVars := map[string]string{"snap_mode": "try"} + bootVars := map[string]string{"snap_mode": boot.TryStatus} l.SetBootVars(bootVars) v, err := l.GetBootVars("snap_mode") c.Assert(err, IsNil) c.Check(v, HasLen, 1) - c.Check(v["snap_mode"], Equals, "try") + c.Check(v["snap_mode"], Equals, boot.TryStatus) } func (s *lkTestSuite) TestExtractKernelAssetsUnpacksBootimgImageBuilding(c *C) { diff --git a/bootloader/lkenv/lkenv_test.go b/bootloader/lkenv/lkenv_test.go index cb7238f3ba9..edacbd9d8f1 100644 --- a/bootloader/lkenv/lkenv_test.go +++ b/bootloader/lkenv/lkenv_test.go @@ -22,12 +22,14 @@ package lkenv_test import ( "bytes" "compress/gzip" - . "gopkg.in/check.v1" "io" "io/ioutil" "path/filepath" "testing" + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/boot" "github.com/snapcore/snapd/bootloader/lkenv" ) @@ -66,8 +68,8 @@ func (l *lkenvTestSuite) TestSet(c *C) { env := lkenv.NewEnv(l.envPath) c.Check(env, NotNil) - env.Set("snap_mode", "try") - c.Check(env.Get("snap_mode"), Equals, "try") + env.Set("snap_mode", boot.TryStatus) + c.Check(env.Get("snap_mode"), Equals, boot.TryStatus) } func (l *lkenvTestSuite) TestSave(c *C) { diff --git a/build-aux/snap/snapcraft.yaml b/build-aux/snap/snapcraft.yaml index 696379bd305..8b9a539e25c 100644 --- a/build-aux/snap/snapcraft.yaml +++ b/build-aux/snap/snapcraft.yaml @@ -1,6 +1,5 @@ name: snapd -# TODO: enable this when the store is ready -# type: snapd +type: snapd summary: Daemon and tooling that enable snap packages description: | Install, configure, refresh and remove snap packages. Snaps are @@ -9,7 +8,7 @@ description: | cloud, servers, desktops and the internet of things. Start with 'snap list' to see installed snaps. -adopt-info: snapd +adopt-info: snapd-deb # build-base is needed here for snapcraft to build this snap as with "modern" # snapcraft build-base: core @@ -27,7 +26,7 @@ license: GPL-3.0 # See the comments from jdstrand in # https://forum.snapcraft.io/t/5547/10 parts: - snapd: + snapd-deb: plugin: nil source: . build-snaps: [go/1.10/stable] @@ -45,7 +44,7 @@ parts: sudo apt-get build-dep -y ./ ./get-deps.sh --skip-unused-check # set version after installing dependencies so we have all the tools here - snapcraftctl set-version $(./mkversion.sh --output-only) + snapcraftctl set-version "$(./mkversion.sh --output-only)" override-build: | # unset the LD_FLAGS and LD_LIBRARY_PATH vars that snapcraft sets for us # as those will point to the $SNAPCRAFT_STAGE which on re-builds will @@ -54,7 +53,8 @@ parts: # TODO: should we unset $PATH to not include $SNAPCRAFT_STAGE too? unset LD_FLAGS unset LD_LIBRARY_PATH - # if we are root, disable tests + # if we are root, disable tests because a number of them fail when run as + # root if [ "$(id -u)" = "0" ]; then DEB_BUILD_OPTIONS=nocheck export DEB_BUILD_OPTIONS diff --git a/client/snap_op.go b/client/snap_op.go index 91774187a3f..db3e82dc0d6 100644 --- a/client/snap_op.go +++ b/client/snap_op.go @@ -314,20 +314,33 @@ type snapRevisionOptions struct { } type downloadAction struct { - SnapName string `json:"snap-name,omitempty"` + SnapName string `json:"snap-name"` + snapRevisionOptions + + HeaderPeek bool `json:"header-peek,omitempty"` + ResumeToken string `json:"resume-token,omitempty"` } type DownloadInfo struct { SuggestedFileName string Size int64 Sha3_384 string + ResumeToken string +} + +type DownloadOptions struct { + SnapOptions + + HeaderPeek bool + ResumeToken string + Resume int64 } // Download will stream the given snap to the client -func (client *Client) Download(name string, options *SnapOptions) (dlInfo *DownloadInfo, r io.ReadCloser, err error) { +func (client *Client) Download(name string, options *DownloadOptions) (dlInfo *DownloadInfo, r io.ReadCloser, err error) { if options == nil { - options = &SnapOptions{} + options = &DownloadOptions{} } action := downloadAction{ SnapName: name, @@ -336,6 +349,8 @@ func (client *Client) Download(name string, options *SnapOptions) (dlInfo *Downl CohortKey: options.CohortKey, Revision: options.Revision, }, + HeaderPeek: options.HeaderPeek, + ResumeToken: options.ResumeToken, } data, err := json.Marshal(&action) if err != nil { @@ -344,6 +359,9 @@ func (client *Client) Download(name string, options *SnapOptions) (dlInfo *Downl headers := map[string]string{ "Content-Type": "application/json", } + if options.Resume > 0 { + headers["range"] = fmt.Sprintf("bytes: %d-", options.Resume) + } // no deadline for downloads ctx := context.Background() @@ -369,6 +387,7 @@ func (client *Client) Download(name string, options *SnapOptions) (dlInfo *Downl SuggestedFileName: matches[1], Size: rsp.ContentLength, Sha3_384: rsp.Header.Get("Snap-Sha3-384"), + ResumeToken: rsp.Header.Get("Snap-Download-Token"), } return dlInfo, rsp.Body, nil diff --git a/client/snap_op_test.go b/client/snap_op_test.go index c04ba3366e1..4790a3166b8 100644 --- a/client/snap_op_test.go +++ b/client/snap_op_test.go @@ -446,24 +446,30 @@ func (cs *clientSuite) TestClientOpDownload(c *check.C) { cs.header = http.Header{ "Content-Disposition": {"attachment; filename=foo_2.snap"}, "Snap-Sha3-384": {"sha3sha3sha3"}, + "Snap-Download-Token": {"some-token"}, } cs.contentLength = 1234 cs.rsp = `lots-of-foo-data` - dlInfo, rc, err := cs.cli.Download("foo", &client.SnapOptions{ - Revision: "2", - Channel: "edge", + dlInfo, rc, err := cs.cli.Download("foo", &client.DownloadOptions{ + SnapOptions: client.SnapOptions{ + Revision: "2", + Channel: "edge", + }, + HeaderPeek: true, }) c.Check(err, check.IsNil) c.Check(dlInfo, check.DeepEquals, &client.DownloadInfo{ SuggestedFileName: "foo_2.snap", Size: 1234, Sha3_384: "sha3sha3sha3", + ResumeToken: "some-token", }) // check we posted the right stuff c.Assert(cs.req.Header.Get("Content-Type"), check.Equals, "application/json") + c.Assert(cs.req.Header.Get("range"), check.Equals, "") body, err := ioutil.ReadAll(cs.req.Body) c.Assert(err, check.IsNil) var jsonBody client.DownloadAction @@ -472,6 +478,56 @@ func (cs *clientSuite) TestClientOpDownload(c *check.C) { c.Check(jsonBody.SnapName, check.DeepEquals, "foo") c.Check(jsonBody.Revision, check.Equals, "2") c.Check(jsonBody.Channel, check.Equals, "edge") + c.Check(jsonBody.HeaderPeek, check.Equals, true) + + // ensure we can read the response + content, err := ioutil.ReadAll(rc) + c.Assert(err, check.IsNil) + c.Check(string(content), check.Equals, cs.rsp) + // and we can close it + c.Check(rc.Close(), check.IsNil) +} + +func (cs *clientSuite) TestClientOpDownloadResume(c *check.C) { + cs.status = 200 + cs.header = http.Header{ + "Content-Disposition": {"attachment; filename=foo_2.snap"}, + "Snap-Sha3-384": {"sha3sha3sha3"}, + } + // we resume + cs.contentLength = 1234 - 64 + + cs.rsp = `lots-of-foo-data` + + dlInfo, rc, err := cs.cli.Download("foo", &client.DownloadOptions{ + SnapOptions: client.SnapOptions{ + Revision: "2", + Channel: "edge", + }, + HeaderPeek: true, + ResumeToken: "some-token", + Resume: 64, + }) + c.Check(err, check.IsNil) + c.Check(dlInfo, check.DeepEquals, &client.DownloadInfo{ + SuggestedFileName: "foo_2.snap", + Size: 1234 - 64, + Sha3_384: "sha3sha3sha3", + }) + + // check we posted the right stuff + c.Assert(cs.req.Header.Get("Content-Type"), check.Equals, "application/json") + c.Assert(cs.req.Header.Get("range"), check.Equals, "bytes: 64-") + body, err := ioutil.ReadAll(cs.req.Body) + c.Assert(err, check.IsNil) + var jsonBody client.DownloadAction + err = json.Unmarshal(body, &jsonBody) + c.Assert(err, check.IsNil) + c.Check(jsonBody.SnapName, check.DeepEquals, "foo") + c.Check(jsonBody.Revision, check.Equals, "2") + c.Check(jsonBody.Channel, check.Equals, "edge") + c.Check(jsonBody.HeaderPeek, check.Equals, true) + c.Check(jsonBody.ResumeToken, check.Equals, "some-token") // ensure we can read the response content, err := ioutil.ReadAll(rc) diff --git a/cmd/libsnap-confine-private/locking-test.c b/cmd/libsnap-confine-private/locking-test.c index c2f10e6aa69..72378a430ed 100644 --- a/cmd/libsnap-confine-private/locking-test.c +++ b/cmd/libsnap-confine-private/locking-test.c @@ -68,6 +68,11 @@ static const char *sc_test_use_fake_lock_dir(void) // Check that locking a namespace actually flock's the mutex with LOCK_EX static void test_sc_lock_unlock(void) { + if (geteuid() != 0) { + g_test_skip("this test only runs as root"); + return; + } + const char *lock_dir = sc_test_use_fake_lock_dir(); int fd = sc_lock_generic("foo", 123); // Construct the name of the lock file @@ -95,6 +100,11 @@ static void test_sc_lock_unlock(void) // Check that holding a lock is properly detected. static void test_sc_verify_snap_lock__locked(void) { + if (geteuid() != 0) { + g_test_skip("this test only runs as root"); + return; + } + (void)sc_test_use_fake_lock_dir(); int fd = sc_lock_snap("foo"); sc_verify_snap_lock("foo"); @@ -104,6 +114,11 @@ static void test_sc_verify_snap_lock__locked(void) // Check that holding a lock is properly detected. static void test_sc_verify_snap_lock__unlocked(void) { + if (geteuid() != 0) { + g_test_skip("this test only runs as root"); + return; + } + (void)sc_test_use_fake_lock_dir(); if (g_test_subprocess()) { sc_verify_snap_lock("foo"); @@ -117,6 +132,11 @@ static void test_sc_verify_snap_lock__unlocked(void) static void test_sc_enable_sanity_timeout(void) { + if (geteuid() != 0) { + g_test_skip("this test only runs as root"); + return; + } + if (g_test_subprocess()) { sc_enable_sanity_timeout(); debug("waiting..."); diff --git a/cmd/snap-bootstrap/cmd_initramfs_mounts.go b/cmd/snap-bootstrap/cmd_initramfs_mounts.go index 7b0e2bb7dbe..d5dd97c0d4b 100644 --- a/cmd/snap-bootstrap/cmd_initramfs_mounts.go +++ b/cmd/snap-bootstrap/cmd_initramfs_mounts.go @@ -30,6 +30,7 @@ import ( "github.com/snapcore/snapd/boot" "github.com/snapcore/snapd/bootloader" "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/seed" "github.com/snapcore/snapd/snap" @@ -68,6 +69,31 @@ var ( osutilIsMounted = osutil.IsMounted ) +func recoverySystemEssentialSnaps(seedDir, recoverySystem string, essentialTypes []snap.Type) ([]*seed.Snap, error) { + systemSeed, err := seed.Open(seedDir, recoverySystem) + if err != nil { + return nil, err + } + + seed20, ok := systemSeed.(seed.EssentialMetaLoaderSeed) + if !ok { + return nil, fmt.Errorf("internal error: UC20 seed must implement EssentialMetaLoaderSeed") + } + + // load assertions into a temporary database + if err := systemSeed.LoadAssertions(nil, nil); err != nil { + return nil, err + } + + // load and verify metadata only for the relevant essential snaps + perf := timings.New(nil) + if err := seed20.LoadEssentialMeta(essentialTypes, perf); err != nil { + return nil, err + } + + return seed20.EssentialSnaps(), nil +} + // generateMountsMode* is called multiple times from initramfs until it // no longer generates more mount points and just returns an empty output. func generateMountsModeInstall(recoverySystem string) error { @@ -97,46 +123,33 @@ func generateMountsModeInstall(recoverySystem string) error { return err } if !isBaseMounted || !isKernelMounted || !isSnapdMounted { - // load the recovery system and generate mounts for kernel/base - systemSeed, err := seed.Open(seedDir, recoverySystem) - if err != nil { - return err + // load the recovery system and generate mounts for kernel/base + // and snapd + var whichTypes []snap.Type + if !isBaseMounted { + whichTypes = append(whichTypes, snap.TypeBase) } - // load assertions into a temporary database - if err := systemSeed.LoadAssertions(nil, nil); err != nil { - return err + if !isKernelMounted { + whichTypes = append(whichTypes, snap.TypeKernel) } - perf := timings.New(nil) - // TODO:UC20: LoadMeta will verify all the snaps in the - // seed, that is probably too much. We can expose more - // dedicated helpers for this later. - if err := systemSeed.LoadMeta(perf); err != nil { - return err + if !isSnapdMounted { + whichTypes = append(whichTypes, snap.TypeSnapd) } + essSnaps, err := recoverySystemEssentialSnaps(seedDir, recoverySystem, whichTypes) + if err != nil { + return fmt.Errorf("cannot load metadata and verify essential bootstrap snaps %v: %v", whichTypes, err) + } + // TODO:UC20: do we need more cross checks here? - for _, essentialSnap := range systemSeed.EssentialSnaps() { - snapf, err := snap.Open(essentialSnap.Path) - if err != nil { - return err - } - info, err := snap.ReadInfoFromSnapFile(snapf, essentialSnap.SideInfo) - if err != nil { - return err - } - switch info.GetType() { + for _, essentialSnap := range essSnaps { + switch essentialSnap.EssentialType { case snap.TypeBase: - if !isBaseMounted { - fmt.Fprintf(stdout, "%s %s\n", essentialSnap.Path, filepath.Join(runMnt, "base")) - } + fmt.Fprintf(stdout, "%s %s\n", essentialSnap.Path, filepath.Join(runMnt, "base")) case snap.TypeKernel: - if !isKernelMounted { - // XXX: we need to cross-check the kernel path with snapd_recovery_kernel used by grub - fmt.Fprintf(stdout, "%s %s\n", essentialSnap.Path, filepath.Join(runMnt, "kernel")) - } + // TODO:UC20: we need to cross-check the kernel path with snapd_recovery_kernel used by grub + fmt.Fprintf(stdout, "%s %s\n", essentialSnap.Path, filepath.Join(runMnt, "kernel")) case snap.TypeSnapd: - if !isSnapdMounted { - fmt.Fprintf(stdout, "%s %s\n", essentialSnap.Path, filepath.Join(runMnt, "snapd")) - } + fmt.Fprintf(stdout, "%s %s\n", essentialSnap.Path, filepath.Join(runMnt, "snapd")) } } } @@ -218,7 +231,7 @@ func generateMountsModeRun() error { // we have no fallback base! return fmt.Errorf("modeenv corrupt: missing base setting") } - if modeEnv.BaseStatus == "try" { + if modeEnv.BaseStatus == boot.TryStatus { // then we are trying a base snap update and there should be a // try_base set in the modeenv too if modeEnv.TryBase != "" { @@ -227,17 +240,21 @@ func generateMountsModeRun() error { if osutil.FileExists(tryBaseSnapPath) { // set the TryBase and have the initramfs mount this base // snap - modeEnv.BaseStatus = "trying" + modeEnv.BaseStatus = boot.TryingStatus base = modeEnv.TryBase + } else { + logger.Noticef("try-base snap %q does not exist", modeEnv.TryBase) } - // TODO:UC20: log a message somewhere if try base snap does not - // exist? + } else { + logger.Noticef("try-base snap is empty, but \"base_status\" is \"trying\"") } // TODO:UC20: log a message if try_base is unset here? - } else if modeEnv.BaseStatus == "trying" { + } else if modeEnv.BaseStatus == boot.TryingStatus { // snapd failed to start with the base snap update, so we need to // fallback to the old base snap and clear base_status - modeEnv.BaseStatus = "" + modeEnv.BaseStatus = boot.DefaultStatus + } else if modeEnv.BaseStatus != boot.DefaultStatus { + logger.Noticef("\"base_status\" has an invalid setting: %q", modeEnv.BaseStatus) } baseSnapPath := filepath.Join(dataDir, "system-data", dirs.SnapBlobDir, base) @@ -250,6 +267,12 @@ func generateMountsModeRun() error { return err } if !isKernelMounted { + // make a map to easily check if a kernel snap is valid or not + validKernels := make(map[string]bool, len(modeEnv.CurrentKernels)) + for _, validKernel := range modeEnv.CurrentKernels { + validKernels[validKernel] = true + } + // find ubuntu-boot bootloader to get the kernel_status and kernel.efi // status so we can determine the right kernel snap to have mounted @@ -278,27 +301,61 @@ func generateMountsModeRun() error { return fmt.Errorf("no fallback kernel snap: %v", err) } + kernelFile := kernel.Filename() + if !validKernels[kernelFile] { + // we don't trust the fallback kernel! + return fmt.Errorf("fallback kernel snap %q is not trusted in the modeenv", kernelFile) + } + // get kernel_status m, err := ebl.GetBootVars("kernel_status") if err != nil { return fmt.Errorf("cannot get kernel_status from bootloader %s", ebl.Name()) } - if m["kernel_status"] == "trying" { + if m["kernel_status"] == boot.TryingStatus { // check for the try kernel - tryKernel, tryKernelExists, err := ebl.TryKernel() - // TODO:UC20: can we log somewhere if err != nil here? - if tryKernelExists && err == nil { - kernel = tryKernel + tryKernel, err := ebl.TryKernel() + if err == nil { + tryKernelFile := tryKernel.Filename() + if validKernels[tryKernelFile] { + kernelFile = tryKernelFile + } else { + logger.Noticef("try-kernel %q is not trusted in the modeenv", tryKernelFile) + } + } else if err != bootloader.ErrNoTryKernelRef { + logger.Noticef("missing try-kernel, even though \"kernel_status\" is \"trying\"") } // if we didn't have a try kernel, but we do have kernel_status == // trying we just fallback to using the normal kernel + // same goes for try kernel being untrusted - we will fallback to + // the normal kernel snap } - kernelPath := filepath.Join(dataDir, "system-data", dirs.SnapBlobDir, kernel.Filename()) + kernelPath := filepath.Join(dataDir, "system-data", dirs.SnapBlobDir, kernelFile) fmt.Fprintf(stdout, "%s %s\n", kernelPath, filepath.Join(runMnt, "kernel")) } - // 3.1 Write the modeenv out again + + // 3.1 Maybe mount the snapd snap on first boot of run-mode + // TODO:UC20: Make RecoverySystem empty after successful first boot + // somewhere in devicestate + if modeEnv.RecoverySystem != "" { + isSnapdMounted, err := osutilIsMounted(filepath.Join(runMnt, "snapd")) + if err != nil { + return err + } + + if !isSnapdMounted { + // load the recovery system and generate mount for snapd + essSnaps, err := recoverySystemEssentialSnaps(seedDir, modeEnv.RecoverySystem, []snap.Type{snap.TypeSnapd}) + if err != nil { + return fmt.Errorf("cannot load metadata and verify snapd snap: %v", err) + } + fmt.Fprintf(stdout, "%s %s\n", essSnaps[0].Path, filepath.Join(runMnt, "snapd")) + } + } + + // 4.1 Write the modeenv out again return modeEnv.Write(filepath.Join(dataDir, "system-data")) } diff --git a/cmd/snap-bootstrap/cmd_initramfs_mounts_test.go b/cmd/snap-bootstrap/cmd_initramfs_mounts_test.go index c0d5efb6bfc..fc6008422f4 100644 --- a/cmd/snap-bootstrap/cmd_initramfs_mounts_test.go +++ b/cmd/snap-bootstrap/cmd_initramfs_mounts_test.go @@ -279,6 +279,9 @@ func (s *initramfsMountsSuite) TestInitramfsMountsRunModeStep2(c *C) { case 5: c.Check(path, Equals, filepath.Join(s.runMnt, "kernel")) return false, nil + case 6: + c.Check(path, Equals, filepath.Join(s.runMnt, "snapd")) + return false, nil } return false, fmt.Errorf("unexpected number of calls: %v", n) }) @@ -286,7 +289,9 @@ func (s *initramfsMountsSuite) TestInitramfsMountsRunModeStep2(c *C) { // write modeenv modeEnv := boot.Modeenv{ - Base: "core20_123.snap", + RecoverySystem: "20191118", + Base: "core20_123.snap", + CurrentKernels: []string{"pc-kernel_1.snap"}, } err := modeEnv.Write(filepath.Join(s.runMnt, "ubuntu-data", "system-data")) c.Assert(err, IsNil) @@ -304,9 +309,10 @@ func (s *initramfsMountsSuite) TestInitramfsMountsRunModeStep2(c *C) { _, err = main.Parser().ParseArgs([]string{"initramfs-mounts"}) c.Assert(err, IsNil) - c.Assert(n, Equals, 5) + c.Assert(n, Equals, 6) c.Check(s.Stdout.String(), Equals, fmt.Sprintf(`%[1]s/ubuntu-data/system-data/var/lib/snapd/snaps/core20_123.snap %[1]s/base %[1]s/ubuntu-data/system-data/var/lib/snapd/snaps/pc-kernel_1.snap %[1]s/kernel +%[1]s/ubuntu-seed/snaps/snapd_1.snap %[1]s/snapd `, s.runMnt)) } @@ -593,7 +599,8 @@ func (s *initramfsMountsSuite) TestInitramfsMountsRunModeKernelSnapUpgradeHappy( // write modeenv modeEnv := &boot.Modeenv{ - Base: "core20_123.snap", + Base: "core20_123.snap", + CurrentKernels: []string{"pc-kernel_1.snap", "pc-kernel_2.snap"}, } err := modeEnv.Write(filepath.Join(s.runMnt, "ubuntu-data", "system-data")) c.Assert(err, IsNil) @@ -630,3 +637,177 @@ func (s *initramfsMountsSuite) TestInitramfsMountsRunModeKernelSnapUpgradeHappy( c.Check(s.Stdout.String(), Equals, fmt.Sprintf(`%[1]s/ubuntu-data/system-data/var/lib/snapd/snaps/pc-kernel_2.snap %[1]s/kernel `, s.runMnt)) } + +func (s *initramfsMountsSuite) TestInitramfsMountsRunModeUntrustedKernelSnap(c *C) { + n := 0 + s.mockProcCmdlineContent(c, "snapd_recovery_mode=run") + + restore := main.MockOsutilIsMounted(func(path string) (bool, error) { + n++ + switch n { + case 1: + c.Check(path, Equals, filepath.Join(s.runMnt, "ubuntu-seed")) + return true, nil + case 2: + c.Check(path, Equals, filepath.Join(s.runMnt, "ubuntu-boot")) + return true, nil + case 3: + c.Check(path, Equals, filepath.Join(s.runMnt, "ubuntu-data")) + return true, nil + case 4: + c.Check(path, Equals, filepath.Join(s.runMnt, "base")) + return true, nil + case 5: + c.Check(path, Equals, filepath.Join(s.runMnt, "kernel")) + return false, nil + } + return false, fmt.Errorf("unexpected number of calls: %v", n) + }) + defer restore() + + // write modeenv + modeEnv := boot.Modeenv{ + Base: "core20_123.snap", + CurrentKernels: []string{"pc-kernel_1.snap"}, + } + err := modeEnv.Write(filepath.Join(s.runMnt, "ubuntu-data", "system-data")) + c.Assert(err, IsNil) + + // mock a bootloader + bloader := bootloadertest.Mock("mock", c.MkDir()) + bootloader.Force(bloader) + defer bootloader.Force(nil) + + // set the current kernel as a kernel not in CurrentKernels + kernel, err := snap.ParsePlaceInfoFromSnapFileName("pc-kernel_2.snap") + c.Assert(err, IsNil) + r := bloader.SetRunKernelImageEnabledKernel(kernel) + defer r() + + _, err = main.Parser().ParseArgs([]string{"initramfs-mounts"}) + c.Assert(err, ErrorMatches, fmt.Sprintf("fallback kernel snap %q is not trusted in the modeenv", "pc-kernel_2.snap")) + c.Assert(n, Equals, 5) +} + +func (s *initramfsMountsSuite) TestInitramfsMountsRunModeUntrustedTryKernelSnapFallsBack(c *C) { + n := 0 + s.mockProcCmdlineContent(c, "snapd_recovery_mode=run") + + restore := main.MockOsutilIsMounted(func(path string) (bool, error) { + n++ + switch n { + case 1: + c.Check(path, Equals, filepath.Join(s.runMnt, "ubuntu-seed")) + return true, nil + case 2: + c.Check(path, Equals, filepath.Join(s.runMnt, "ubuntu-boot")) + return true, nil + case 3: + c.Check(path, Equals, filepath.Join(s.runMnt, "ubuntu-data")) + return true, nil + case 4: + c.Check(path, Equals, filepath.Join(s.runMnt, "base")) + return true, nil + case 5: + c.Check(path, Equals, filepath.Join(s.runMnt, "kernel")) + return false, nil + } + return false, fmt.Errorf("unexpected number of calls: %v", n) + }) + defer restore() + + // write modeenv + modeEnv := boot.Modeenv{ + Base: "core20_123.snap", + CurrentKernels: []string{"pc-kernel_1.snap"}, + } + err := modeEnv.Write(filepath.Join(s.runMnt, "ubuntu-data", "system-data")) + c.Assert(err, IsNil) + + // mock a bootloader + bloader := bootloadertest.Mock("mock", c.MkDir()) + bootloader.Force(bloader) + defer bootloader.Force(nil) + + // set the try kernel as a kernel not in CurrentKernels + kernel2, err := snap.ParsePlaceInfoFromSnapFileName("pc-kernel_2.snap") + c.Assert(err, IsNil) + r := bloader.SetRunKernelImageEnabledTryKernel(kernel2) + defer r() + + // set the normal kernel as a valid kernel + kernel1, err := snap.ParsePlaceInfoFromSnapFileName("pc-kernel_1.snap") + c.Assert(err, IsNil) + r = bloader.SetRunKernelImageEnabledKernel(kernel1) + defer r() + + _, err = main.Parser().ParseArgs([]string{"initramfs-mounts"}) + + // TODO:UC20: if we have somewhere to log errors from snap-bootstrap during + // the initramfs, check that log here + c.Assert(err, IsNil) + c.Assert(n, Equals, 5) + c.Check(s.Stdout.String(), Equals, fmt.Sprintf(`%[1]s/ubuntu-data/system-data/var/lib/snapd/snaps/pc-kernel_1.snap %[1]s/kernel +`, s.runMnt)) +} + +func (s *initramfsMountsSuite) TestInitramfsMountsRunModeKernelStatusTryingNoTryKernel(c *C) { + n := 0 + s.mockProcCmdlineContent(c, "snapd_recovery_mode=run") + + restore := main.MockOsutilIsMounted(func(path string) (bool, error) { + n++ + switch n { + case 1: + c.Check(path, Equals, filepath.Join(s.runMnt, "ubuntu-seed")) + return true, nil + case 2: + c.Check(path, Equals, filepath.Join(s.runMnt, "ubuntu-boot")) + return true, nil + case 3: + c.Check(path, Equals, filepath.Join(s.runMnt, "ubuntu-data")) + return true, nil + case 4: + c.Check(path, Equals, filepath.Join(s.runMnt, "base")) + return true, nil + case 5: + c.Check(path, Equals, filepath.Join(s.runMnt, "kernel")) + return false, nil + } + return false, fmt.Errorf("unexpected number of calls: %v", n) + }) + defer restore() + + // write modeenv + modeEnv := boot.Modeenv{ + Base: "core20_123.snap", + CurrentKernels: []string{"pc-kernel_1.snap"}, + } + err := modeEnv.Write(filepath.Join(s.runMnt, "ubuntu-data", "system-data")) + c.Assert(err, IsNil) + + // mock a bootloader + bloader := bootloadertest.Mock("mock", c.MkDir()) + bootloader.Force(bloader) + defer bootloader.Force(nil) + + // we are in trying mode, but don't set a try-kernel so we fallback to the + // fallback kernel + err = bloader.SetBootVars(map[string]string{"kernel_status": boot.TryingStatus}) + c.Assert(err, IsNil) + + // set the normal kernel as a valid kernel + kernel, err := snap.ParsePlaceInfoFromSnapFileName("pc-kernel_1.snap") + c.Assert(err, IsNil) + r := bloader.SetRunKernelImageEnabledKernel(kernel) + defer r() + + _, err = main.Parser().ParseArgs([]string{"initramfs-mounts"}) + + // TODO:UC20: if we have somewhere to log errors from snap-bootstrap during + // the initramfs, check that log here + c.Assert(err, IsNil) + c.Assert(n, Equals, 5) + c.Check(s.Stdout.String(), Equals, fmt.Sprintf(`%[1]s/ubuntu-data/system-data/var/lib/snapd/snaps/pc-kernel_1.snap %[1]s/kernel +`, s.runMnt)) +} diff --git a/cmd/snap-bootstrap/cmd_recovery_chooser_trigger.go b/cmd/snap-bootstrap/cmd_recovery_chooser_trigger.go new file mode 100644 index 00000000000..b0b4cb724a4 --- /dev/null +++ b/cmd/snap-bootstrap/cmd_recovery_chooser_trigger.go @@ -0,0 +1,106 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 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 . + * + */ + +package main + +import ( + "fmt" + "os" + "time" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/cmd/snap-bootstrap/triggerwatch" + "github.com/snapcore/snapd/logger" +) + +func init() { + const ( + short = "Detect Ubuntu Core recovery chooser trigger" + long = "" + ) + + addCommandBuilder(func(parser *flags.Parser) { + if _, err := parser.AddCommand("recovery-chooser-trigger", short, long, &cmdRecoveryChooserTrigger{}); err != nil { + panic(err) + } + }) +} + +var ( + triggerwatchWait = triggerwatch.Wait + + // default trigger wait timeout + defaultTimeout = 10 * time.Second + + // default marker file location + defaultMarkerFile = "/run/snapd-recovery-chooser-triggered" +) + +type cmdRecoveryChooserTrigger struct { + MarkerFile string `long:"marker-file" value-name:"filename" description:"trigger marker file location"` + WaitTimeout string `long:"wait-timeout" value-name:"duration" description:"trigger wait timeout"` +} + +func (c *cmdRecoveryChooserTrigger) Execute(args []string) error { + // TODO:UC20: check in the gadget if there is a hook or some binary we + // should run for trigger detection. This will require some design work + // and also thinking if/how such a hook can be confined. + + timeout := defaultTimeout + markerFile := defaultMarkerFile + + if c.WaitTimeout != "" { + userTimeout, err := time.ParseDuration(c.WaitTimeout) + if err != nil { + logger.Noticef("cannot parse duration %q, using default", c.WaitTimeout) + } else { + timeout = userTimeout + } + } + if c.MarkerFile != "" { + markerFile = c.MarkerFile + } + logger.Noticef("trigger timeout %v", timeout) + logger.Noticef("marker file %v", markerFile) + + _, err := os.Stat(markerFile) + if err == nil { + logger.Noticef("marker already present") + return nil + } + + err = triggerwatchWait(timeout) + if err != nil { + if err == triggerwatch.ErrTriggerNotDetected { + logger.Noticef("trigger not detected") + return nil + } + return err + } + + // got the trigger, try to create the marker file + m, err := os.Create(markerFile) + if err != nil { + return fmt.Errorf("cannot create the marker file: %q", err) + } + m.Close() + + return nil +} diff --git a/cmd/snap-bootstrap/cmd_recovery_chooser_trigger_test.go b/cmd/snap-bootstrap/cmd_recovery_chooser_trigger_test.go new file mode 100644 index 00000000000..7a9dae77180 --- /dev/null +++ b/cmd/snap-bootstrap/cmd_recovery_chooser_trigger_test.go @@ -0,0 +1,145 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 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 . + * + */ + +package main_test + +import ( + "errors" + "io/ioutil" + "path/filepath" + "time" + + . "gopkg.in/check.v1" + + main "github.com/snapcore/snapd/cmd/snap-bootstrap" + "github.com/snapcore/snapd/cmd/snap-bootstrap/triggerwatch" + "github.com/snapcore/snapd/testutil" +) + +func (s *cmdSuite) TestRecoveryChooserTriggerDefaults(c *C) { + n := 0 + marker := filepath.Join(c.MkDir(), "marker") + passedTimeout := time.Duration(0) + + restore := main.MockDefaultMarkerFile(marker) + defer restore() + restore = main.MockTriggerwatchWait(func(timeout time.Duration) error { + passedTimeout = timeout + n++ + // trigger happened + return nil + }) + defer restore() + + rest, err := main.Parser().ParseArgs([]string{"recovery-chooser-trigger"}) + c.Assert(err, IsNil) + c.Assert(rest, HasLen, 0) + c.Check(n, Equals, 1) + c.Check(passedTimeout, Equals, main.DefaultTimeout) + c.Check(marker, testutil.FilePresent) +} + +func (s *cmdSuite) TestRecoveryChooserTriggerNoTrigger(c *C) { + n := 0 + marker := filepath.Join(c.MkDir(), "marker") + + restore := main.MockDefaultMarkerFile(marker) + defer restore() + restore = main.MockTriggerwatchWait(func(_ time.Duration) error { + n++ + // trigger did not happen + return triggerwatch.ErrTriggerNotDetected + }) + defer restore() + + _, err := main.Parser().ParseArgs([]string{"recovery-chooser-trigger"}) + c.Assert(err, IsNil) + c.Check(n, Equals, 1) + c.Check(marker, testutil.FileAbsent) +} + +func (s *cmdSuite) TestRecoveryChooserTriggerTakesOptions(c *C) { + marker := filepath.Join(c.MkDir(), "foobar") + n := 0 + passedTimeout := time.Duration(0) + + restore := main.MockTriggerwatchWait(func(timeout time.Duration) error { + passedTimeout = timeout + n++ + // trigger happened + return nil + }) + defer restore() + + rest, err := main.Parser().ParseArgs([]string{ + "recovery-chooser-trigger", + "--wait-timeout", "2m", + "--marker-file", marker, + }) + c.Assert(err, IsNil) + c.Assert(rest, HasLen, 0) + c.Check(n, Equals, 1) + c.Check(passedTimeout, Equals, 2*time.Minute) + c.Check(marker, testutil.FilePresent) +} + +func (s *cmdSuite) TestRecoveryChooserTriggerDoesNothingWhenMarkerPresent(c *C) { + marker := filepath.Join(c.MkDir(), "foobar") + n := 0 + restore := main.MockTriggerwatchWait(func(_ time.Duration) error { + n++ + return errors.New("unexpected call") + }) + defer restore() + + err := ioutil.WriteFile(marker, nil, 0644) + c.Assert(err, IsNil) + + rest, err := main.Parser().ParseArgs([]string{ + "recovery-chooser-trigger", + "--marker-file", marker, + }) + c.Assert(err, IsNil) + c.Assert(rest, HasLen, 0) + // not called + c.Check(n, Equals, 0) +} + +func (s *cmdSuite) TestRecoveryChooserTriggerBadDurationFallback(c *C) { + n := 0 + passedTimeout := time.Duration(0) + restore := main.MockDefaultMarkerFile(filepath.Join(c.MkDir(), "marker")) + defer restore() + + restore = main.MockTriggerwatchWait(func(timeout time.Duration) error { + passedTimeout = timeout + n++ + // trigger happened + return triggerwatch.ErrTriggerNotDetected + }) + defer restore() + + _, err := main.Parser().ParseArgs([]string{ + "recovery-chooser-trigger", + "--wait-timeout=foobar", + }) + c.Assert(err, IsNil) + c.Check(n, Equals, 1) + c.Check(passedTimeout, Equals, main.DefaultTimeout) +} diff --git a/cmd/snap-bootstrap/export_test.go b/cmd/snap-bootstrap/export_test.go index 93a6496ff86..f90d0ed6848 100644 --- a/cmd/snap-bootstrap/export_test.go +++ b/cmd/snap-bootstrap/export_test.go @@ -21,6 +21,7 @@ package main import ( "io" + "time" "github.com/snapcore/snapd/cmd/snap-bootstrap/bootstrap" ) @@ -60,3 +61,21 @@ func MockRunMnt(newRunMnt string) (restore func()) { runMnt = oldRunMnt } } + +func MockTriggerwatchWait(f func(_ time.Duration) error) (restore func()) { + oldTriggerwatchWait := triggerwatchWait + triggerwatchWait = f + return func() { + triggerwatchWait = oldTriggerwatchWait + } +} + +var DefaultTimeout = defaultTimeout + +func MockDefaultMarkerFile(p string) (restore func()) { + old := defaultMarkerFile + defaultMarkerFile = p + return func() { + defaultMarkerFile = old + } +} diff --git a/cmd/snap-bootstrap/main.go b/cmd/snap-bootstrap/main.go index c6426455d26..27367d40203 100644 --- a/cmd/snap-bootstrap/main.go +++ b/cmd/snap-bootstrap/main.go @@ -23,6 +23,8 @@ import ( "os" "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/logger" ) var ( @@ -36,6 +38,13 @@ such as initramfs. commandBuilders []func(*flags.Parser) ) +func init() { + err := logger.SimpleSetup() + if err != nil { + fmt.Fprintf(os.Stderr, "WARNING: failed to activate logging: %s\n", err) + } +} + func main() { err := run(os.Args[1:]) if err != nil { diff --git a/cmd/snap-bootstrap/main_test.go b/cmd/snap-bootstrap/main_test.go index 783046e895c..739442c69e9 100644 --- a/cmd/snap-bootstrap/main_test.go +++ b/cmd/snap-bootstrap/main_test.go @@ -25,15 +25,25 @@ import ( . "gopkg.in/check.v1" main "github.com/snapcore/snapd/cmd/snap-bootstrap" + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/testutil" ) // Hook up check.v1 into the "go test" runner func Test(t *testing.T) { TestingT(t) } -type cmdSuite struct{} +type cmdSuite struct { + testutil.BaseTest +} var _ = Suite(&cmdSuite{}) +func (s *cmdSuite) SetUpTest(c *C) { + s.BaseTest.SetUpTest(c) + _, r := logger.MockLogger() + s.AddCleanup(r) +} + func (s *cmdSuite) TestNoArgsErrors(c *C) { _, err := main.Parser().ParseArgs(nil) c.Assert(err, ErrorMatches, "Please specify .*") diff --git a/cmd/snap-bootstrap/triggerwatch/evdev.go b/cmd/snap-bootstrap/triggerwatch/evdev.go new file mode 100644 index 00000000000..a4fc432d917 --- /dev/null +++ b/cmd/snap-bootstrap/triggerwatch/evdev.go @@ -0,0 +1,244 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 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 . + * + */ + +package triggerwatch + +import ( + "fmt" + "syscall" + "time" + "unsafe" + + // TODO:UC20: not packaged, reimplement the minimal things we need? + evdev "github.com/gvalkov/golang-evdev" + + "github.com/snapcore/snapd/logger" +) + +type keyEvent struct { + Dev triggerDevice + Err error +} + +type triggerEventFilter struct { + Key string +} + +var ( + strToKey = map[string]int{ + "KEY_ESC": evdev.KEY_ESC, + "KEY_1": evdev.KEY_1, + "KEY_2": evdev.KEY_2, + "KEY_3": evdev.KEY_3, + "KEY_4": evdev.KEY_4, + "KEY_5": evdev.KEY_5, + "KEY_6": evdev.KEY_6, + "KEY_7": evdev.KEY_7, + "KEY_8": evdev.KEY_8, + "KEY_9": evdev.KEY_9, + "KEY_0": evdev.KEY_0, + } + evKeyCapability = evdev.CapabilityType{Type: evdev.EV_KEY, Name: "EV_KEY"} + + // hold time needed to trigger the event + holdToTrigger = 2 * time.Second +) + +func init() { + trigger = &evdevInput{} +} + +type evdevKeyboardInputDevice struct { + keyCode uint16 + dev *evdev.InputDevice +} + +func (e *evdevKeyboardInputDevice) probeKeyState() (bool, error) { + // XXX: evdev defines EVIOCGKEY using MAX_NAME_SIZE which is larger than + // what is needed to store the key bitmap with KEY_MAX bits, but we need + // to play along since the value is already encoded + keyBitmap := new([evdev.MAX_NAME_SIZE]byte) + + // obtain the large bitmap with all key states + // https://elixir.bootlin.com/linux/v5.5.5/source/drivers/input/evdev.c#L1163 + _, _, err := syscall.RawSyscall(syscall.SYS_IOCTL, e.dev.File.Fd(), uintptr(evdev.EVIOCGKEY), uintptr(unsafe.Pointer(keyBitmap))) + if err != 0 { + return false, err + } + byteIdx := e.keyCode / 8 + keyMask := byte(1 << (e.keyCode % 8)) + isDown := keyBitmap[byteIdx]&keyMask != 0 + return isDown, nil +} + +func (e *evdevKeyboardInputDevice) WaitForTrigger(ch chan keyEvent) { + logger.Noticef("%s: starting wait", e) + + // XXX: do not mess with setting the key repeat rate, as it's cumbersome + // and golang-evdev SetRepeatRate() parameter order is actually reversed + // wrt. what the kernel does. The evdev interprets EVIOCSREP arguments + // as (delay, repeat) + // https://elixir.bootlin.com/linux/latest/source/drivers/input/evdev.c#L1072 + // but the wrapper is passing is passing (repeat, delay) + // https://github.com/gvalkov/golang-evdev/blob/287e62b94bcb850ab42e711bd74b2875da83af2c/device.go#L226-L230 + + keyDown, err := e.probeKeyState() + if err != nil { + ch <- keyEvent{Err: fmt.Errorf("cannot obtain initial key state: %v", err), Dev: e} + } + if keyDown { + // looks like the key is pressed initially, we don't know when + // that happened, but pretend it happened just now + logger.Noticef("%s: key is already down", e) + } + + type evdevEvent struct { + kev *evdev.KeyEvent + err error + } + + // buffer large enough to collect some events + evChan := make(chan evdevEvent, 10) + + monitorKey := func() { + for { + ies, err := e.dev.Read() + if err != nil { + evChan <- evdevEvent{err: err} + break + } + for _, ie := range ies { + if ie.Type != evdev.EV_KEY || ie.Code != e.keyCode { + continue + } + kev := evdev.NewKeyEvent(&ie) + evChan <- evdevEvent{kev: kev} + } + } + close(evChan) + } + + go monitorKey() + + holdTimer := time.NewTimer(holdToTrigger) + // no sense to keep it running later either + defer holdTimer.Stop() + + if !keyDown { + // key isn't held yet, stop the timer + holdTimer.Stop() + } + + // invariant: tholdTimer is running iff keyDown is true, otherwise is stopped +Loop: + for { + select { + case ev := <-evChan: + if ev.err != nil { + holdTimer.Stop() + ch <- keyEvent{Err: err, Dev: e} + break Loop + } + kev := ev.kev + switch kev.State { + case evdev.KeyDown: + if keyDown { + // unexpected, but possible if we missed + // a key up event right after checking + // the initial keyboard state when the + // key was still down + if !holdTimer.Stop() { + // drain the channel before the + // timer gets reset + <-holdTimer.C + } + } + keyDown = true + // timer is stopped at this point + holdTimer.Reset(holdToTrigger) + logger.Noticef("%s: trigger key down", e) + case evdev.KeyHold: + if !keyDown { + keyDown = true + // timer is not running yet at this point + holdTimer.Reset(holdToTrigger) + logger.Noticef("%s: unexpected hold without down", e) + } + case evdev.KeyUp: + // no need to drain the channel, if it expired, + // we'll handle it in next iteration + holdTimer.Stop() + keyDown = false + logger.Noticef("%s: trigger key up", e) + } + case <-holdTimer.C: + logger.Noticef("%s: hold complete", e) + ch <- keyEvent{Dev: e} + break Loop + } + } +} + +func (e *evdevKeyboardInputDevice) String() string { + return fmt.Sprintf("%s: %s", e.dev.Phys, e.dev.Name) +} + +func (e *evdevKeyboardInputDevice) Close() { + e.dev.File.Close() +} + +type evdevInput struct{} + +func (e *evdevInput) FindMatchingDevices(filter triggerEventFilter) ([]triggerDevice, error) { + devices, err := evdev.ListInputDevices() + if err != nil { + return nil, fmt.Errorf("cannot list input devices: %v", err) + } + + // NOTE: this supports so far only key input devices + + kc, ok := strToKey[filter.Key] + if !ok { + return nil, fmt.Errorf("cannot find a key matching the filter %q", filter.Key) + } + cap := evdev.CapabilityCode{Code: kc, Name: filter.Key} + + match := func(dev *evdev.InputDevice) triggerDevice { + for _, cc := range dev.Capabilities[evKeyCapability] { + if cc == cap { + return &evdevKeyboardInputDevice{ + dev: dev, + keyCode: uint16(cap.Code), + } + } + } + return nil + } + // collect all input devices that can emit the trigger key + var devs []triggerDevice + for _, dev := range devices { + idev := match(dev) + if idev != nil { + devs = append(devs, idev) + } else { + defer dev.File.Close() + } + } + return devs, nil +} diff --git a/cmd/snap-bootstrap/triggerwatch/export_test.go b/cmd/snap-bootstrap/triggerwatch/export_test.go new file mode 100644 index 00000000000..1cad33c2ccb --- /dev/null +++ b/cmd/snap-bootstrap/triggerwatch/export_test.go @@ -0,0 +1,32 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 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 . + * + */ +package triggerwatch + +func MockInput(newInput TriggerProvider) (restore func()) { + oldInput := trigger + trigger = newInput + return func() { + trigger = oldInput + } +} + +type TriggerProvider = triggerProvider +type TriggerDevice = triggerDevice +type TriggerCapabilityFilter = triggerEventFilter +type KeyEvent = keyEvent diff --git a/cmd/snap-bootstrap/triggerwatch/triggerwatch.go b/cmd/snap-bootstrap/triggerwatch/triggerwatch.go new file mode 100644 index 00000000000..13601e18127 --- /dev/null +++ b/cmd/snap-bootstrap/triggerwatch/triggerwatch.go @@ -0,0 +1,88 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 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 . + * + */ + +package triggerwatch + +import ( + "errors" + "fmt" + "time" + + "github.com/snapcore/snapd/logger" +) + +type triggerProvider interface { + FindMatchingDevices(filter triggerEventFilter) ([]triggerDevice, error) +} + +type triggerDevice interface { + WaitForTrigger(chan keyEvent) + String() string + Close() +} + +var ( + // trigger mechanism + trigger triggerProvider + + // wait for '1' to be pressed + triggerFilter = triggerEventFilter{Key: "KEY_1"} + + ErrTriggerNotDetected = errors.New("trigger not detected") +) + +// Wait waits for a trigger on the available trigger devices for a given amount +// of time. Returns nil if one was detected, ErrTriggerNotDetected if timeout +// was hit, or other non-nil error. +func Wait(timeout time.Duration) error { + if trigger == nil { + logger.Panicf("trigger is unset") + } + + devices, err := trigger.FindMatchingDevices(triggerFilter) + if err != nil { + return fmt.Errorf("cannot list trigger devices: %v", err) + } + if devices == nil { + return fmt.Errorf("cannot find matching devices") + } + + logger.Noticef("waiting for trigger key: %v", triggerFilter.Key) + + // wait for a couple of second for the key + detectKeyCh := make(chan keyEvent, len(devices)) + + for _, dev := range devices { + go dev.WaitForTrigger(detectKeyCh) + defer dev.Close() + } + + select { + case kev := <-detectKeyCh: + if kev.Err != nil { + return err + } + // channel got closed without an error + logger.Noticef("%s: + got trigger key %v", kev.Dev, triggerFilter.Key) + case <-time.After(timeout): + return ErrTriggerNotDetected + } + + return nil +} diff --git a/cmd/snap-bootstrap/triggerwatch/triggerwatch_test.go b/cmd/snap-bootstrap/triggerwatch/triggerwatch_test.go new file mode 100644 index 00000000000..2a3c6d704ca --- /dev/null +++ b/cmd/snap-bootstrap/triggerwatch/triggerwatch_test.go @@ -0,0 +1,130 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 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 . + * + */ + +package triggerwatch_test + +import ( + "fmt" + "testing" + "time" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/cmd/snap-bootstrap/triggerwatch" +) + +// Hook up check.v1 into the "go test" runner +func Test(t *testing.T) { TestingT(t) } + +type triggerwatchSuite struct{} + +var _ = Suite(&triggerwatchSuite{}) + +type mockTriggerDevice struct { + waitForTriggerCalls int + closeCalls int + ev *triggerwatch.KeyEvent +} + +func (m *mockTriggerDevice) WaitForTrigger(n chan triggerwatch.KeyEvent) { + m.waitForTriggerCalls++ + if m.ev != nil { + ev := *m.ev + ev.Dev = m + n <- ev + } +} + +func (m *mockTriggerDevice) String() string { return "mock-device" } +func (m *mockTriggerDevice) Close() { m.closeCalls++ } + +type mockTrigger struct { + f triggerwatch.TriggerCapabilityFilter + d *mockTriggerDevice + err error + + findMatchingCalls int +} + +func (m *mockTrigger) FindMatchingDevices(f triggerwatch.TriggerCapabilityFilter) ([]triggerwatch.TriggerDevice, error) { + m.findMatchingCalls++ + + m.f = f + if m.err != nil { + return nil, m.err + } + if m.d != nil { + return []triggerwatch.TriggerDevice{m.d}, nil + } + return nil, nil +} + +const testTriggerTimeout = 5 * time.Millisecond + +func (s *triggerwatchSuite) TestNoDevsWaitKey(c *C) { + md := &mockTriggerDevice{ev: &triggerwatch.KeyEvent{}} + mi := &mockTrigger{d: md} + restore := triggerwatch.MockInput(mi) + defer restore() + + err := triggerwatch.Wait(testTriggerTimeout) + c.Assert(err, IsNil) + c.Assert(mi.findMatchingCalls, Equals, 1) + c.Assert(md.waitForTriggerCalls, Equals, 1) + c.Assert(md.closeCalls, Equals, 1) +} + +func (s *triggerwatchSuite) TestNoDevsWaitKeyTimeout(c *C) { + md := &mockTriggerDevice{} + mi := &mockTrigger{d: md} + restore := triggerwatch.MockInput(mi) + defer restore() + + err := triggerwatch.Wait(testTriggerTimeout) + c.Assert(err, Equals, triggerwatch.ErrTriggerNotDetected) + c.Assert(mi.findMatchingCalls, Equals, 1) + c.Assert(md.waitForTriggerCalls, Equals, 1) + c.Assert(md.closeCalls, Equals, 1) +} + +func (s *triggerwatchSuite) TestNoDevsWaitNoMatching(c *C) { + mi := &mockTrigger{} + restore := triggerwatch.MockInput(mi) + defer restore() + + err := triggerwatch.Wait(testTriggerTimeout) + c.Assert(err, ErrorMatches, "cannot find matching devices") +} + +func (s *triggerwatchSuite) TestNoDevsWaitMatchingError(c *C) { + mi := &mockTrigger{err: fmt.Errorf("failed")} + restore := triggerwatch.MockInput(mi) + defer restore() + + err := triggerwatch.Wait(testTriggerTimeout) + c.Assert(err, ErrorMatches, "cannot list trigger devices: failed") +} + +func (s *triggerwatchSuite) TestChecksInput(c *C) { + restore := triggerwatch.MockInput(nil) + defer restore() + + c.Assert(func() { triggerwatch.Wait(testTriggerTimeout) }, + Panics, "trigger is unset") +} diff --git a/cmd/snap-confine/snap-confine.apparmor.in b/cmd/snap-confine/snap-confine.apparmor.in index b3b92ba7b69..9f41bcb060c 100644 --- a/cmd/snap-confine/snap-confine.apparmor.in +++ b/cmd/snap-confine/snap-confine.apparmor.in @@ -565,4 +565,12 @@ # silence noisy denials breaks nested lxd. Until the cause is determined, # do not use an explicit deny for unix. (LP: #1855355) #deny unix, + + # Explicitly deny these accesses which show up on Arch to silence the + # denials for this unneeded access. + deny /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libnss_files-[0-9]*.so* mr, + deny /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libnss_mymachines.[0-9]*.so* mr, + deny /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libnss_systemd.[0-9]*.so* mr, + deny /etc/nsswitch.conf r, + deny /etc/passwd r, } diff --git a/cmd/snap-failure/cmd_snapd.go b/cmd/snap-failure/cmd_snapd.go index 14253aa69f5..cccdf4201d4 100644 --- a/cmd/snap-failure/cmd_snapd.go +++ b/cmd/snap-failure/cmd_snapd.go @@ -126,6 +126,7 @@ func (c *cmdSnapd) Execute(args []string) error { cmd := exec.Command(snapdPath) cmd.Env = os.Environ() cmd.Env = append(cmd.Env, "SNAPD_REVERT_TO_REV="+prevRev) + cmd.Env = append(cmd.Env, "SNAPD_DEBUG=1") cmd.Stdout = Stdout cmd.Stderr = Stderr if err = cmd.Run(); err != nil { @@ -133,14 +134,25 @@ func (c *cmdSnapd) Execute(args []string) error { } logger.Noticef("restarting snapd socket") - // we need to reset the failure state to be able to restart again - if output, err := exec.Command("systemctl", "reset-failed", "snapd.socket").CombinedOutput(); err != nil { - return osutil.OutputErr(output, err) - } // at this point our manually started snapd stopped and - // removed the /run/snap* sockets (this is a feature of + // should have removed the /run/snap* sockets (this is a feature of // golang) - we need to restart snapd.socket to make them // available again. + + // we need to reset the failure state to be able to restart again + if output, err := exec.Command("systemctl", "reset-failed", "snapd.socket").CombinedOutput(); err != nil { + logger.Noticef("failed to reset-failed snapd.socket: %v", osutil.OutputErr(output, err)) + // don't die if we fail to reset the failed state of snapd.socket, as + // the restart itself could still work + } + // be extra robust and if the socket file still somehow exists delete it + // before restarting, otherwise the restart command will fail because the + // systemd can't create the file + // always remove to avoid TOCTOU issues but don't complain about ENOENT + err = os.Remove(dirs.SnapdSocket) + if err != nil && !os.IsNotExist(err) { + logger.Noticef("snapd socket still exists before restarting socket service, but unable to remove: %v", err) + } output, err = exec.Command("systemctl", "restart", "snapd.socket").CombinedOutput() if err != nil { return osutil.OutputErr(output, err) diff --git a/cmd/snap-failure/cmd_snapd_test.go b/cmd/snap-failure/cmd_snapd_test.go index db92a500724..aab1620c0ec 100644 --- a/cmd/snap-failure/cmd_snapd_test.go +++ b/cmd/snap-failure/cmd_snapd_test.go @@ -29,6 +29,7 @@ import ( failure "github.com/snapcore/snapd/cmd/snap-failure" "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/testutil" ) @@ -235,3 +236,43 @@ exit 123 {"systemctl", "stop", "snapd.socket"}, }) } + +func (r *failureSuite) TestStickySnapdSocket(c *C) { + origArgs := os.Args + defer func() { os.Args = origArgs }() + + writeSeqFile(c, "snapd", snap.R(123), []*snap.SideInfo{ + {Revision: snap.R(100)}, + {Revision: snap.R(123)}, + }) + + err := os.MkdirAll(filepath.Dir(dirs.SnapdSocket), 0755) + c.Assert(err, IsNil) + err = ioutil.WriteFile(dirs.SnapdSocket, []byte{}, 0755) + c.Assert(err, IsNil) + + // mock snapd in the core snap + snapdCmd := testutil.MockCommand(c, filepath.Join(dirs.SnapMountDir, "snapd", "100", "/usr/lib/snapd/snapd"), + `test "$SNAPD_REVERT_TO_REV" = "100"`) + defer snapdCmd.Restore() + + systemctlCmd := testutil.MockCommand(c, "systemctl", "") + defer systemctlCmd.Restore() + + os.Args = []string{"snap-failure", "snapd"} + err = failure.Run() + c.Check(err, IsNil) + c.Check(r.Stderr(), HasLen, 0) + + c.Check(snapdCmd.Calls(), DeepEquals, [][]string{ + {"snapd"}, + }) + c.Check(systemctlCmd.Calls(), DeepEquals, [][]string{ + {"systemctl", "stop", "snapd.socket"}, + {"systemctl", "reset-failed", "snapd.socket"}, + {"systemctl", "restart", "snapd.socket"}, + }) + + // make sure the socket file was deleted + c.Assert(osutil.FileExists(dirs.SnapdSocket), Equals, false) +} diff --git a/cmd/snap-preseed/main_test.go b/cmd/snap-preseed/main_test.go index 5460b828c85..f5172062c9e 100644 --- a/cmd/snap-preseed/main_test.go +++ b/cmd/snap-preseed/main_test.go @@ -31,6 +31,7 @@ import ( . "gopkg.in/check.v1" "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/asserts/assertstest" "github.com/snapcore/snapd/cmd/snap-preseed" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/seed" @@ -171,6 +172,7 @@ func (s *startPreseedSuite) TestRunPreseedHappy(c *C) { } type Fake16Seed struct { + AssertsModel *asserts.Model Essential []*seed.Snap LoadMetaErr error LoadAssertionsErr error @@ -179,12 +181,25 @@ type Fake16Seed struct { // Fake implementation of seed.Seed interface +func mockClassicModel() *asserts.Model { + headers := map[string]interface{}{ + "type": "model", + "authority-id": "brand", + "series": "16", + "brand-id": "brand", + "model": "classicbaz-3000", + "classic": "true", + "timestamp": "2018-01-01T08:00:00+00:00", + } + return assertstest.FakeAssertion(headers, nil).(*asserts.Model) +} + func (fs *Fake16Seed) LoadAssertions(db asserts.RODatabase, commitTo func(*asserts.Batch) error) error { return fs.LoadAssertionsErr } func (fs *Fake16Seed) Model() (*asserts.Model, error) { - panic("not implemented") + return fs.AssertsModel, nil } func (fs *Fake16Seed) LoadMeta(tm timings.Measurer) error { @@ -208,7 +223,8 @@ func (s *startPreseedSuite) TestSystemSnapFromSeed(c *C) { restore := main.MockSeedOpen(func(rootDir, label string) (seed.Seed, error) { return &Fake16Seed{ - Essential: []*seed.Snap{{Path: "/some/path/core", SideInfo: &snap.SideInfo{RealName: "core"}}}, + AssertsModel: mockClassicModel(), + Essential: []*seed.Snap{{Path: "/some/path/core", SideInfo: &snap.SideInfo{RealName: "core"}}}, }, nil }) defer restore() @@ -218,6 +234,23 @@ func (s *startPreseedSuite) TestSystemSnapFromSeed(c *C) { c.Check(path, Equals, "/some/path/core") } +func (s *startPreseedSuite) TestSystemSnapFromSnapdSeed(c *C) { + tmpDir := c.MkDir() + + restore := main.MockSeedOpen(func(rootDir, label string) (seed.Seed, error) { + return &Fake16Seed{ + AssertsModel: mockClassicModel(), + Essential: []*seed.Snap{{Path: "/some/path/snapd.snap", SideInfo: &snap.SideInfo{RealName: "snapd"}}}, + UsesSnapd: true, + }, nil + }) + defer restore() + + path, err := main.SystemSnapFromSeed(tmpDir) + c.Assert(err, IsNil) + c.Check(path, Equals, "/some/path/snapd.snap") +} + func (s *startPreseedSuite) TestSystemSnapFromSeedOpenError(c *C) { tmpDir := c.MkDir() @@ -232,6 +265,7 @@ func (s *startPreseedSuite) TestSystemSnapFromSeedErrors(c *C) { tmpDir := c.MkDir() fakeSeed := &Fake16Seed{} + fakeSeed.AssertsModel = mockClassicModel() restore := main.MockSeedOpen(func(rootDir, label string) (seed.Seed, error) { return fakeSeed, nil }) defer restore() @@ -244,10 +278,6 @@ func (s *startPreseedSuite) TestSystemSnapFromSeedErrors(c *C) { _, err = main.SystemSnapFromSeed(tmpDir) c.Assert(err, ErrorMatches, "core snap not found") - fakeSeed.UsesSnapd = true - _, err = main.SystemSnapFromSeed(tmpDir) - c.Assert(err, ErrorMatches, "preseeding with snapd snap is not supported yet") - fakeSeed.LoadMetaErr = fmt.Errorf("load meta failed") _, err = main.SystemSnapFromSeed(tmpDir) c.Assert(err, ErrorMatches, "load meta failed") @@ -258,6 +288,31 @@ func (s *startPreseedSuite) TestSystemSnapFromSeedErrors(c *C) { c.Assert(err, ErrorMatches, "load assertions failed") } +func (s *startPreseedSuite) TestClassicRequired(c *C) { + tmpDir := c.MkDir() + + headers := map[string]interface{}{ + "type": "model", + "authority-id": "brand", + "series": "16", + "brand-id": "brand", + "model": "baz-3000", + "architecture": "armhf", + "gadget": "brand-gadget", + "kernel": "kernel", + "timestamp": "2018-01-01T08:00:00+00:00", + } + + fakeSeed := &Fake16Seed{} + fakeSeed.AssertsModel = assertstest.FakeAssertion(headers, nil).(*asserts.Model) + + restore := main.MockSeedOpen(func(rootDir, label string) (seed.Seed, error) { return fakeSeed, nil }) + defer restore() + + _, err := main.SystemSnapFromSeed(tmpDir) + c.Assert(err, ErrorMatches, "preseeding is only supported on classic systems") +} + func (s *startPreseedSuite) TestRunPreseedUnsupportedVersion(c *C) { tmpDir := c.MkDir() dirs.SetRootDir(tmpDir) diff --git a/cmd/snap-preseed/preseed_linux.go b/cmd/snap-preseed/preseed_linux.go index cbb3317071e..2749e077820 100644 --- a/cmd/snap-preseed/preseed_linux.go +++ b/cmd/snap-preseed/preseed_linux.go @@ -85,24 +85,37 @@ var systemSnapFromSeed = func(rootDir string) (string, error) { return "", err } - // TODO: handle core18, snapd snap. + model, err := seed.Model() + if err != nil { + return "", err + } + + // TODO: implement preseeding for core. + if !model.Classic() { + return "", fmt.Errorf("preseeding is only supported on classic systems") + } + + var required string if seed.UsesSnapdSnap() { - return "", fmt.Errorf("preseeding with snapd snap is not supported yet") + required = "snapd" + } else { + required = "core" } - var coreSnapPath string + var snapPath string ess := seed.EssentialSnaps() if len(ess) > 0 { - if ess[0].SnapName() == "core" { - coreSnapPath = ess[0].Path + // core / snapd snap is the first essential snap. + if ess[0].SnapName() == required { + snapPath = ess[0].Path } } - if coreSnapPath == "" { - return "", fmt.Errorf("core snap not found") + if snapPath == "" { + return "", fmt.Errorf("%s snap not found", required) } - return coreSnapPath, nil + return snapPath, nil } const snapdPreseedSupportVer = `2.43.3+` diff --git a/data/selinux/snappy.te b/data/selinux/snappy.te index d9f5c11376a..30cf93e990f 100644 --- a/data/selinux/snappy.te +++ b/data/selinux/snappy.te @@ -484,6 +484,11 @@ kernel_read_net_sysctls(snappy_mount_t) kernel_search_network_sysctl(snappy_mount_t) dev_read_sysfs(snappy_mount_t) +# snap-update-ns is started using a file descriptor, meaning ld.so runs in the +# mount ns and may try to read/mmap cache files inside +fs_read_tmpfs_files(snappy_mount_t) +mmap_read_files_pattern(snappy_mount_t, tmpfs_t, tmpfs_t) + ######################################## # # snap-confine local policy diff --git a/data/systemd/snapd.recovery-chooser-trigger.service.in b/data/systemd/snapd.recovery-chooser-trigger.service.in new file mode 100644 index 00000000000..1ac7964f46f --- /dev/null +++ b/data/systemd/snapd.recovery-chooser-trigger.service.in @@ -0,0 +1,17 @@ +[Unit] +Description=Wait for the Ubuntu Core chooser trigger +Before=snapd.service +# don't run on classic or uc16/uc18 +ConditionKernelCommandLine=snapd_recovery_mode + +[Service] +# blocks the service startup until a trigger is detected or a timeout is hit +Type=oneshot +ExecStart=@libexecdir@/snapd/snap-bootstrap recovery-chooser-trigger +RemainAfterExit=true + +[Install] +WantedBy=multi-user.target + +# started on boot only +# X-Snapd-Snap: do-not-start diff --git a/data/systemd/snapd.service.in b/data/systemd/snapd.service.in index cd10596cfd8..f9bd0555283 100644 --- a/data/systemd/snapd.service.in +++ b/data/systemd/snapd.service.in @@ -1,5 +1,5 @@ [Unit] -Description=Snappy daemon +Description=Snap Daemon Requires=snapd.socket OnFailure=snapd.failure.service # This is handled by snapd diff --git a/image/image_test.go b/image/image_test.go index 64111b44f6a..d7b1cff21bd 100644 --- a/image/image_test.go +++ b/image/image_test.go @@ -586,8 +586,9 @@ func (s *imageSuite) TestSetupSeed(c *C) { SideInfo: &info.SideInfo, - Essential: true, - Required: true, + EssentialType: info.GetType(), + Essential: true, + Required: true, Channel: stableChannel, }) @@ -707,6 +708,7 @@ func (s *imageSuite) TestSetupSeedLocalCoreBrandKernel(c *C) { info := s.AssertedSnapInfo(name) var pinfo snap.PlaceInfo = info var sideInfo *snap.SideInfo + var snapType snap.Type if info == nil { switch name { case "core_x1.snap": @@ -715,9 +717,11 @@ func (s *imageSuite) TestSetupSeedLocalCoreBrandKernel(c *C) { RealName: "core", } channel = "" + snapType = snap.TypeOS } } else { sideInfo = &info.SideInfo + snapType = info.GetType() } fn := pinfo.Filename() @@ -728,8 +732,9 @@ func (s *imageSuite) TestSetupSeedLocalCoreBrandKernel(c *C) { SideInfo: sideInfo, - Essential: true, - Required: true, + EssentialType: snapType, + Essential: true, + Required: true, Channel: channel, }) @@ -792,11 +797,12 @@ func (s *imageSuite) TestSetupSeedDevmodeSnap(c *C) { for i, name := range []string{"core", "pc-kernel", "pc"} { info := s.AssertedSnapInfo(name) c.Check(essSnaps[i], DeepEquals, &seed.Snap{ - Path: filepath.Join(seedsnapsdir, info.Filename()), - SideInfo: &info.SideInfo, - Essential: true, - Required: true, - Channel: "beta", + Path: filepath.Join(seedsnapsdir, info.Filename()), + SideInfo: &info.SideInfo, + EssentialType: info.GetType(), + Essential: true, + Required: true, + Channel: "beta", }) } c.Check(runSnaps[0], DeepEquals, &seed.Snap{ @@ -891,6 +897,7 @@ func (s *imageSuite) TestSetupSeedWithBase(c *C) { RealName: "core18", Revision: snap.R("18"), }, + SnapType: snap.TypeBase, } } } @@ -899,11 +906,12 @@ func (s *imageSuite) TestSetupSeedWithBase(c *C) { p := filepath.Join(seedsnapsdir, fn) c.Check(p, testutil.FilePresent) c.Check(essSnaps[i], DeepEquals, &seed.Snap{ - Path: p, - SideInfo: &info.SideInfo, - Essential: true, - Required: true, - Channel: stableChannel, + Path: p, + SideInfo: &info.SideInfo, + EssentialType: info.GetType(), + Essential: true, + Required: true, + Channel: stableChannel, }) } c.Check(runSnaps[0], DeepEquals, &seed.Snap{ @@ -1050,6 +1058,7 @@ func (s *imageSuite) TestSetupSeedWithBaseLegacySnap(c *C) { RealName: "core18", Revision: snap.R("18"), }, + SnapType: snap.TypeBase, } } } @@ -1058,11 +1067,12 @@ func (s *imageSuite) TestSetupSeedWithBaseLegacySnap(c *C) { p := filepath.Join(seedsnapsdir, fn) c.Check(p, testutil.FilePresent) c.Check(essSnaps[i], DeepEquals, &seed.Snap{ - Path: p, - SideInfo: &info.SideInfo, - Essential: true, - Required: true, - Channel: stableChannel, + Path: p, + SideInfo: &info.SideInfo, + EssentialType: info.GetType(), + Essential: true, + Required: true, + Channel: stableChannel, }) } c.Check(runSnaps[0], DeepEquals, &seed.Snap{ @@ -1213,6 +1223,7 @@ func (s *imageSuite) TestSetupSeedLocalSnapsWithStoreAsserts(c *C) { SnapID: s.AssertedSnapID("core"), Revision: snap.R(3), }, + SnapType: snap.TypeOS, } default: c.Errorf("cannot have %s", name) @@ -1223,11 +1234,12 @@ func (s *imageSuite) TestSetupSeedLocalSnapsWithStoreAsserts(c *C) { p := filepath.Join(seedsnapsdir, fn) c.Check(p, testutil.FilePresent) c.Check(essSnaps[i], DeepEquals, &seed.Snap{ - Path: p, - SideInfo: &info.SideInfo, - Essential: true, - Required: true, - Channel: stableChannel, + Path: p, + SideInfo: &info.SideInfo, + EssentialType: info.GetType(), + Essential: true, + Required: true, + Channel: stableChannel, }) } c.Check(runSnaps[0], DeepEquals, &seed.Snap{ @@ -1307,6 +1319,7 @@ func (s *imageSuite) TestSetupSeedLocalSnapsWithChannels(c *C) { SnapID: s.AssertedSnapID("core"), Revision: snap.R(3), }, + SnapType: snap.TypeOS, } channel = "candidate" default: @@ -1318,11 +1331,12 @@ func (s *imageSuite) TestSetupSeedLocalSnapsWithChannels(c *C) { p := filepath.Join(seedsnapsdir, fn) c.Check(p, testutil.FilePresent) c.Check(essSnaps[i], DeepEquals, &seed.Snap{ - Path: p, - SideInfo: &info.SideInfo, - Essential: true, - Required: true, - Channel: channel, + Path: p, + SideInfo: &info.SideInfo, + EssentialType: info.GetType(), + Essential: true, + Required: true, + Channel: channel, }) } c.Check(runSnaps[0], DeepEquals, &seed.Snap{ @@ -1492,25 +1506,28 @@ func (s *imageSuite) TestSetupSeedWithKernelAndGadgetTrack(c *C) { c.Check(runSnaps, HasLen, 0) c.Check(essSnaps[0], DeepEquals, &seed.Snap{ - Path: filepath.Join(seedsnapsdir, "core_3.snap"), - SideInfo: &s.AssertedSnapInfo("core").SideInfo, - Essential: true, - Required: true, - Channel: "stable", + Path: filepath.Join(seedsnapsdir, "core_3.snap"), + SideInfo: &s.AssertedSnapInfo("core").SideInfo, + EssentialType: snap.TypeOS, + Essential: true, + Required: true, + Channel: "stable", }) c.Check(essSnaps[1], DeepEquals, &seed.Snap{ - Path: filepath.Join(seedsnapsdir, "pc-kernel_2.snap"), - SideInfo: &s.AssertedSnapInfo("pc-kernel").SideInfo, - Essential: true, - Required: true, - Channel: "18/stable", + Path: filepath.Join(seedsnapsdir, "pc-kernel_2.snap"), + SideInfo: &s.AssertedSnapInfo("pc-kernel").SideInfo, + EssentialType: snap.TypeKernel, + Essential: true, + Required: true, + Channel: "18/stable", }) c.Check(essSnaps[2], DeepEquals, &seed.Snap{ - Path: filepath.Join(seedsnapsdir, "pc_1.snap"), - SideInfo: &s.AssertedSnapInfo("pc").SideInfo, - Essential: true, - Required: true, - Channel: "18/stable", + Path: filepath.Join(seedsnapsdir, "pc_1.snap"), + SideInfo: &s.AssertedSnapInfo("pc").SideInfo, + EssentialType: snap.TypeGadget, + Essential: true, + Required: true, + Channel: "18/stable", }) // check the downloads @@ -1566,25 +1583,28 @@ func (s *imageSuite) TestSetupSeedWithKernelTrackWithDefaultChannel(c *C) { c.Check(runSnaps, HasLen, 0) c.Check(essSnaps[0], DeepEquals, &seed.Snap{ - Path: filepath.Join(seedsnapsdir, "core_3.snap"), - SideInfo: &s.AssertedSnapInfo("core").SideInfo, - Essential: true, - Required: true, - Channel: "edge", + Path: filepath.Join(seedsnapsdir, "core_3.snap"), + SideInfo: &s.AssertedSnapInfo("core").SideInfo, + EssentialType: snap.TypeOS, + Essential: true, + Required: true, + Channel: "edge", }) c.Check(essSnaps[1], DeepEquals, &seed.Snap{ - Path: filepath.Join(seedsnapsdir, "pc-kernel_2.snap"), - SideInfo: &s.AssertedSnapInfo("pc-kernel").SideInfo, - Essential: true, - Required: true, - Channel: "18/edge", + Path: filepath.Join(seedsnapsdir, "pc-kernel_2.snap"), + SideInfo: &s.AssertedSnapInfo("pc-kernel").SideInfo, + EssentialType: snap.TypeKernel, + Essential: true, + Required: true, + Channel: "18/edge", }) c.Check(essSnaps[2], DeepEquals, &seed.Snap{ - Path: filepath.Join(seedsnapsdir, "pc_1.snap"), - SideInfo: &s.AssertedSnapInfo("pc").SideInfo, - Essential: true, - Required: true, - Channel: "edge", + Path: filepath.Join(seedsnapsdir, "pc_1.snap"), + SideInfo: &s.AssertedSnapInfo("pc").SideInfo, + EssentialType: snap.TypeGadget, + Essential: true, + Required: true, + Channel: "edge", }) } @@ -1626,18 +1646,20 @@ func (s *imageSuite) TestSetupSeedWithKernelTrackOnLocalSnap(c *C) { c.Check(runSnaps, HasLen, 0) c.Check(essSnaps[0], DeepEquals, &seed.Snap{ - Path: filepath.Join(seedsnapsdir, "core_3.snap"), - SideInfo: &s.AssertedSnapInfo("core").SideInfo, - Essential: true, - Required: true, - Channel: "beta", + Path: filepath.Join(seedsnapsdir, "core_3.snap"), + SideInfo: &s.AssertedSnapInfo("core").SideInfo, + EssentialType: snap.TypeOS, + Essential: true, + Required: true, + Channel: "beta", }) c.Check(essSnaps[1], DeepEquals, &seed.Snap{ - Path: filepath.Join(seedsnapsdir, "pc-kernel_2.snap"), - SideInfo: &s.AssertedSnapInfo("pc-kernel").SideInfo, - Essential: true, - Required: true, - Channel: "18/beta", + Path: filepath.Join(seedsnapsdir, "pc-kernel_2.snap"), + SideInfo: &s.AssertedSnapInfo("pc-kernel").SideInfo, + EssentialType: snap.TypeKernel, + Essential: true, + Required: true, + Channel: "18/beta", }) } @@ -2022,19 +2044,21 @@ func (s *imageSuite) TestSetupSeedClassic(c *C) { // check the files are in place c.Check(essSnaps[0], DeepEquals, &seed.Snap{ - Path: filepath.Join(seedsnapsdir, "core_3.snap"), - SideInfo: &s.AssertedSnapInfo("core").SideInfo, - Essential: true, - Required: true, - Channel: stableChannel, + Path: filepath.Join(seedsnapsdir, "core_3.snap"), + SideInfo: &s.AssertedSnapInfo("core").SideInfo, + EssentialType: snap.TypeOS, + Essential: true, + Required: true, + Channel: stableChannel, }) c.Check(essSnaps[0].Path, testutil.FilePresent) c.Check(essSnaps[1], DeepEquals, &seed.Snap{ - Path: filepath.Join(seedsnapsdir, "classic-gadget_5.snap"), - SideInfo: &s.AssertedSnapInfo("classic-gadget").SideInfo, - Essential: true, - Required: true, - Channel: stableChannel, + Path: filepath.Join(seedsnapsdir, "classic-gadget_5.snap"), + SideInfo: &s.AssertedSnapInfo("classic-gadget").SideInfo, + EssentialType: snap.TypeGadget, + Essential: true, + Required: true, + Channel: stableChannel, }) c.Check(essSnaps[1].Path, testutil.FilePresent) c.Check(runSnaps[0], DeepEquals, &seed.Snap{ @@ -2096,11 +2120,12 @@ func (s *imageSuite) TestSetupSeedClassicWithLocalClassicSnap(c *C) { c.Check(runSnaps, HasLen, 1) c.Check(essSnaps[0], DeepEquals, &seed.Snap{ - Path: filepath.Join(seedsnapsdir, "core_3.snap"), - SideInfo: &s.AssertedSnapInfo("core").SideInfo, - Essential: true, - Required: true, - Channel: stableChannel, + Path: filepath.Join(seedsnapsdir, "core_3.snap"), + SideInfo: &s.AssertedSnapInfo("core").SideInfo, + EssentialType: snap.TypeOS, + Essential: true, + Required: true, + Channel: stableChannel, }) c.Check(essSnaps[0].Path, testutil.FilePresent) @@ -2166,11 +2191,12 @@ func (s *imageSuite) TestSetupSeedClassicSnapdOnly(c *C) { p := filepath.Join(seedsnapsdir, fn) c.Check(p, testutil.FilePresent) c.Check(essSnaps[i], DeepEquals, &seed.Snap{ - Path: p, - SideInfo: &info.SideInfo, - Essential: true, - Required: true, - Channel: stableChannel, + Path: p, + SideInfo: &info.SideInfo, + EssentialType: info.GetType(), + Essential: true, + Required: true, + Channel: stableChannel, }) } c.Check(runSnaps[0], DeepEquals, &seed.Snap{ @@ -2390,11 +2416,12 @@ func (s *imageSuite) TestSetupSeedCore20(c *C) { p := filepath.Join(seedsnapsdir, fn) c.Check(p, testutil.FilePresent) c.Check(essSnaps[i], DeepEquals, &seed.Snap{ - Path: p, - SideInfo: &info.SideInfo, - Essential: true, - Required: true, - Channel: channel, + Path: p, + SideInfo: &info.SideInfo, + EssentialType: info.GetType(), + Essential: true, + Required: true, + Channel: channel, }) } c.Check(runSnaps[0], DeepEquals, &seed.Snap{ diff --git a/osutil/udev/netlink/conn_test.go b/osutil/udev/netlink/conn_test.go index df0ffff5b06..25cccd362e2 100644 --- a/osutil/udev/netlink/conn_test.go +++ b/osutil/udev/netlink/conn_test.go @@ -1,6 +1,7 @@ package netlink import ( + "syscall" "testing" "time" ) @@ -33,3 +34,33 @@ func TestMonitorStop(t *testing.T) { t.Fatal("stop timed out instead of working") } } + +func TestMonitorSelectTimeoutIsHarmless(t *testing.T) { + conn := new(UEventConn) + if err := conn.Connect(UdevEvent); err != nil { + t.Fatal("unable to subscribe to netlink uevent, err:", err) + } + defer conn.Close() + + selectCalled := 0 + oldStopperSelectTimeout := stopperSelectTimeout + stopperSelectTimeout = func() *syscall.Timeval { + selectCalled += 1 + return &syscall.Timeval{ + Usec: 10 * 1000, // 10ms + } + } + defer func() { + stopperSelectTimeout = oldStopperSelectTimeout + }() + + stop := conn.Monitor(nil, nil, nil) + time.Sleep(100 * time.Millisecond) + ok := stop(200 * time.Millisecond) + if !ok { + t.Fatal("stop timed out instead of working") + } + if selectCalled <= 1 { + t.Fatal("select->read->select should have been exercised at least once") + } +} diff --git a/osutil/udev/netlink/rawsockstop.go b/osutil/udev/netlink/rawsockstop.go index d448d4e2eb6..5b431f117db 100644 --- a/osutil/udev/netlink/rawsockstop.go +++ b/osutil/udev/netlink/rawsockstop.go @@ -36,8 +36,6 @@ func RawSockStopper(fd int) (readableOrStop func() (bool, error), stop func(), e return readableOrStop, stop, nil } -var stopperSelectTimeout *syscall.Timeval - func stopperSelectReadable(fd, stopFd int) (bool, error) { maxFd := fd if maxFd < stopFd { @@ -51,11 +49,12 @@ func stopperSelectReadable(fd, stopFd int) (bool, error) { stopFdIdx := stopFd / bits.UintSize stopFdShift := uint(stopFd) % bits.UintSize readable := false + tout := stopperSelectTimeout() for { var r syscall.FdSet r.Bits[fdIdx] = 1 << fdShift r.Bits[stopFdIdx] |= 1 << stopFdShift - _, err := syscall.Select(maxFd+1, &r, nil, nil, stopperSelectTimeout) + _, err := syscall.Select(maxFd+1, &r, nil, nil, tout) if errno, ok := err.(syscall.Errno); ok && errno.Temporary() { continue } diff --git a/osutil/udev/netlink/rawsockstop_arm64.go b/osutil/udev/netlink/rawsockstop_arm64.go new file mode 100644 index 00000000000..49ef2124a9a --- /dev/null +++ b/osutil/udev/netlink/rawsockstop_arm64.go @@ -0,0 +1,14 @@ +package netlink + +import ( + "math" + "syscall" +) + +// workaround a bug in go1.10 where syscall.Select() with nil Timeval +// panics (c.f. https://github.com/golang/go/issues/24189) +var stopperSelectTimeout = func() *syscall.Timeval { + return &syscall.Timeval{ + Sec: math.MaxInt64, + } +} diff --git a/osutil/udev/netlink/rawsockstop_other.go b/osutil/udev/netlink/rawsockstop_other.go new file mode 100644 index 00000000000..9eaacc6339e --- /dev/null +++ b/osutil/udev/netlink/rawsockstop_other.go @@ -0,0 +1,14 @@ +// +build !arm64 +// don't remove the newline between the above statement and the package statement +// or else the build constraint will be ignored and assumed to be part of the package comment! + +package netlink + +import "syscall" + +// once we use something other than go1.10 we can move this back into +// rawsocketstop.go and remove rawsocketstop_arm64.go, see +// rawsocketstop_arm64.go for details +var stopperSelectTimeout = func() *syscall.Timeval { + return nil +} diff --git a/osutil/udev/netlink/rawsockstop_test.go b/osutil/udev/netlink/rawsockstop_test.go index 067301d7a02..3ab44462cf5 100644 --- a/osutil/udev/netlink/rawsockstop_test.go +++ b/osutil/udev/netlink/rawsockstop_test.go @@ -12,11 +12,14 @@ func TestRawSockStopperReadable(t *testing.T) { t.Fatalf("cannot make test pipe: %v", err) } - stopperSelectTimeout = &syscall.Timeval{ - Usec: 50 * 1000, // 50ms + oldStopperSelectTimeout := stopperSelectTimeout + stopperSelectTimeout = func() *syscall.Timeval { + return &syscall.Timeval{ + Usec: 50 * 1000, // 50ms + } } defer func() { - stopperSelectTimeout = nil + stopperSelectTimeout = oldStopperSelectTimeout }() readableOrStop, _, err := RawSockStopper(int(r.Fd())) diff --git a/overlord/devicestate/devicestate_test.go b/overlord/devicestate/devicestate_test.go index a585269ea70..5cff159c46b 100644 --- a/overlord/devicestate/devicestate_test.go +++ b/overlord/devicestate/devicestate_test.go @@ -411,7 +411,7 @@ func (s *deviceMgrSuite) TestDeviceManagerEnsureBootOkBootloaderHappy(c *C) { s.setPCModelInState(c) s.bootloader.SetBootVars(map[string]string{ - "snap_mode": "trying", + "snap_mode": boot.TryingStatus, "snap_try_core": "core_1.snap", }) @@ -478,7 +478,7 @@ func (s *deviceMgrSuite) TestDeviceManagerEnsureBootOkNotRunAgain(c *C) { s.setPCModelInState(c) s.bootloader.SetBootVars(map[string]string{ - "snap_mode": "trying", + "snap_mode": boot.TryingStatus, "snap_try_core": "core_1.snap", }) s.bootloader.SetErr = fmt.Errorf("ensure bootloader is not used") diff --git a/overlord/devicestate/firstboot.go b/overlord/devicestate/firstboot.go index cb3fccc2b85..f177ee6cb1f 100644 --- a/overlord/devicestate/firstboot.go +++ b/overlord/devicestate/firstboot.go @@ -147,9 +147,6 @@ func populateStateFromSeedImpl(st *state.State, opts *populateStateFromSeedOptio return nil, err } - // collected snap infos - infos := make([]*snap.Info, 0, len(essentialSeedSnaps)+len(seedSnaps)) - tsAll := []*state.TaskSet{} configTss := []*state.TaskSet{} @@ -204,6 +201,9 @@ func populateStateFromSeedImpl(st *state.State, opts *populateStateFromSeedOptio } } + // collected snap infos + infos := make([]*snap.Info, 0, len(essentialSeedSnaps)+len(seedSnaps)) + infoToTs := make(map[*snap.Info]*state.TaskSet, len(essentialSeedSnaps)) if len(essentialSeedSnaps) != 0 { diff --git a/overlord/devicestate/firstboot20_test.go b/overlord/devicestate/firstboot20_test.go index 23d918635ab..fa01461fca3 100644 --- a/overlord/devicestate/firstboot20_test.go +++ b/overlord/devicestate/firstboot20_test.go @@ -26,7 +26,6 @@ import ( . "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/dirs" @@ -118,12 +117,14 @@ volumes: } func (s *firstBoot20Suite) TestPopulateFromSeedCore20Happy(c *C) { - r := boottest.ForceModeenv("", &boot.Modeenv{ + m := boot.Modeenv{ Mode: "run", RecoverySystem: "20191018", Base: "core20_1.snap", - }) - defer r() + } + err := m.Write("") + c.Assert(err, IsNil) + defer os.Remove(dirs.SnapModeenvFileUnder(dirs.GlobalRootDir)) // restart overlord to pick up the modeenv s.startOverlord(c) @@ -149,7 +150,7 @@ func (s *firstBoot20Suite) TestPopulateFromSeedCore20Happy(c *C) { // bootloader, so set the current kernel there kernel, err := snap.ParsePlaceInfoFromSnapFileName("pc-kernel_1.snap") c.Assert(err, IsNil) - r = bloader.SetRunKernelImageEnabledKernel(kernel) + r := bloader.SetRunKernelImageEnabledKernel(kernel) defer r() opts := devicestate.PopulateStateFromSeedOptions{ @@ -237,6 +238,16 @@ func (s *firstBoot20Suite) TestPopulateFromSeedCore20Happy(c *C) { c.Assert(err, IsNil) c.Check(seedTime.IsZero(), Equals, false) + // check that we removed recovery_system from modeenv + m2, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Assert(m2.RecoverySystem, Equals, "") + c.Assert(m2.Base, Equals, m.Base) + c.Assert(m2.Mode, Equals, m.Mode) + // Note that we don't check CurrentKernels in the modeenv, even though in a + // real first boot that would also be set here, because setting that is done + // in the snapstate manager, not the devicestate manager + // check that the default device ctx has a Modeenv dev, err := devicestate.DeviceCtx(s.overlord.State(), nil, nil) c.Assert(err, IsNil) diff --git a/overlord/devicestate/firstboot_preseed_test.go b/overlord/devicestate/firstboot_preseed_test.go index 7e54bb88391..578addf6367 100644 --- a/overlord/devicestate/firstboot_preseed_test.go +++ b/overlord/devicestate/firstboot_preseed_test.go @@ -102,6 +102,7 @@ func checkPreseedTaskStates(c *C, st *state.State) { c.Fatalf("unhandled task kind %s", t.Kind()) } } + // sanity: check that doneTasks is not declaring more tasks than // actually expected. c.Check(doneTasks, DeepEquals, seenDone) @@ -183,6 +184,7 @@ func checkPreseedOrder(c *C, tsAll []*state.TaskSet, snaps ...string) { c.Check(waitTasks, HasLen, 0) } else { c.Assert(waitTasks, HasLen, 1) + c.Assert(waitTasks[0].Kind(), Equals, prevTask.Kind()) c.Check(waitTasks[0], Equals, prevTask) } @@ -306,4 +308,118 @@ snaps: c.Assert(chg.Err(), IsNil) checkPreseedTaskStates(c, st) + c.Check(chg.Status(), Equals, state.DoingStatus) + + // verify + r, err := os.Open(dirs.SnapStateFile) + c.Assert(err, IsNil) + diskState, err := state.ReadState(nil, r) + c.Assert(err, IsNil) + + diskState.Lock() + defer diskState.Unlock() + + // seeded snaps are installed + _, err = snapstate.CurrentInfo(diskState, "core") + c.Check(err, IsNil) + _, err = snapstate.CurrentInfo(diskState, "foo") + c.Check(err, IsNil) + + // but we're not considered seeded + var seeded bool + err = diskState.Get("seeded", &seeded) + c.Assert(err, Equals, state.ErrNoState) +} + +func (s *firstbootPreseed16Suite) TestPreseedClassicWithSnapdOnlyHappy(c *C) { + restorePreseedMode := release.MockPreseedMode(func() bool { return true }) + defer restorePreseedMode() + + restore := release.MockOnClassic(true) + defer restore() + + mockMountCmd := testutil.MockCommand(c, "mount", "") + defer mockMountCmd.Restore() + + mockUmountCmd := testutil.MockCommand(c, "umount", "") + defer mockUmountCmd.Restore() + + core18Fname, snapdFname, _, _ := s.makeCore18Snaps(c, &core18SnapsOpts{ + classic: true, + }) + + // put a firstboot snap into the SnapBlobDir + snapYaml := `name: foo +version: 1.0 +base: core18 +` + fooFname, fooDecl, fooRev := s.MakeAssertedSnap(c, snapYaml, nil, snap.R(128), "developerid") + s.WriteAssertions("foo.asserts", s.devAcct, fooRev, fooDecl) + + // add a model assertion and its chain + assertsChain := s.makeModelAssertionChain(c, "my-model-classic", nil) + s.WriteAssertions("model.asserts", assertsChain...) + + // create a seed.yaml + content := []byte(fmt.Sprintf(` +snaps: + - name: snapd + file: %s + - name: foo + file: %s + - name: core18 + file: %s +`, snapdFname, fooFname, core18Fname)) + err := ioutil.WriteFile(filepath.Join(dirs.SnapSeedDir, "seed.yaml"), content, 0644) + c.Assert(err, IsNil) + + // run the firstboot stuff + s.startOverlord(c) + st := s.overlord.State() + st.Lock() + defer st.Unlock() + + opts := &devicestate.PopulateStateFromSeedOptions{Preseed: true} + tsAll, err := devicestate.PopulateStateFromSeedImpl(st, opts, s.perfTimings) + c.Assert(err, IsNil) + + checkPreseedOrder(c, tsAll, "snapd", "core18", "foo") + + // now run the change and check the result + chg := st.NewChange("seed", "run the populate from seed changes") + for _, ts := range tsAll { + chg.AddAll(ts) + } + c.Assert(st.Changes(), HasLen, 1) + c.Assert(chg.Err(), IsNil) + + st.Unlock() + err = s.overlord.Settle(settleTimeout) + st.Lock() + c.Assert(err, IsNil) + + checkPreseedTaskStates(c, st) + c.Check(chg.Status(), Equals, state.DoingStatus) + + // verify + r, err := os.Open(dirs.SnapStateFile) + c.Assert(err, IsNil) + diskState, err := state.ReadState(nil, r) + c.Assert(err, IsNil) + + diskState.Lock() + defer diskState.Unlock() + + // seeded snaps are installed + _, err = snapstate.CurrentInfo(diskState, "snapd") + c.Check(err, IsNil) + _, err = snapstate.CurrentInfo(diskState, "core18") + c.Check(err, IsNil) + _, err = snapstate.CurrentInfo(diskState, "foo") + c.Check(err, IsNil) + + // but we're not considered seeded + var seeded bool + err = diskState.Get("seeded", &seeded) + c.Assert(err, Equals, state.ErrNoState) } diff --git a/overlord/devicestate/firstboot_test.go b/overlord/devicestate/firstboot_test.go index 961a603bb58..a76d622b18f 100644 --- a/overlord/devicestate/firstboot_test.go +++ b/overlord/devicestate/firstboot_test.go @@ -1160,7 +1160,7 @@ type core18SnapsOpts struct { gadget bool } -func (s *firstBoot16Suite) makeCore18Snaps(c *C, opts *core18SnapsOpts) (core18Fn, snapdFn, kernelFn, gadgetFn string) { +func (s *firstBoot16BaseTest) makeCore18Snaps(c *C, opts *core18SnapsOpts) (core18Fn, snapdFn, kernelFn, gadgetFn string) { if opts == nil { opts = &core18SnapsOpts{} } diff --git a/overlord/devicestate/handlers.go b/overlord/devicestate/handlers.go index 96a3ba61314..1647ae15486 100644 --- a/overlord/devicestate/handlers.go +++ b/overlord/devicestate/handlers.go @@ -99,9 +99,20 @@ func (m *DeviceManager) doMarkSeeded(t *state.Task, _ *tomb.Tomb) error { if m.preseed { return fmt.Errorf("internal error: mark-seeded task not expected in pre-seeding mode") } - // TODO:UC20: update "modeenv" and remove "recovery_system" from - // it because this information is only needed for the initial - // seeding. + + deviceCtx, err := DeviceCtx(st, t, nil) + if err != nil { + return fmt.Errorf("cannot get device context: %v", err) + } + + if deviceCtx.HasModeenv() && deviceCtx.RunMode() { + // unset recovery_system because that is only needed during install mode + m.modeEnv.RecoverySystem = "" + err := m.modeEnv.Write("") + if err != nil { + return err + } + } st.Set("seed-time", time.Now()) st.Set("seeded", true) diff --git a/overlord/managers_test.go b/overlord/managers_test.go index ed4f205c207..0d1cae859c7 100644 --- a/overlord/managers_test.go +++ b/overlord/managers_test.go @@ -45,6 +45,7 @@ import ( "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/asserts/assertstest" "github.com/snapcore/snapd/asserts/sysdb" + "github.com/snapcore/snapd/boot" "github.com/snapcore/snapd/bootloader" "github.com/snapcore/snapd/bootloader/bootloadertest" "github.com/snapcore/snapd/client" @@ -1710,12 +1711,12 @@ type: os c.Assert(bloader.BootVars, DeepEquals, map[string]string{ "snap_core": "core_99.snap", "snap_try_core": "core_x1.snap", - "snap_mode": "try", + "snap_mode": boot.TryStatus, }) // simulate successful restart happened state.MockRestarting(st, state.RestartUnset) - bloader.BootVars["snap_mode"] = "" + bloader.BootVars["snap_mode"] = boot.DefaultStatus bloader.SetBootBase("core_x1.snap") st.Unlock() @@ -1785,7 +1786,7 @@ type: kernel` bloader.BootVars = map[string]string{ "snap_core": "core18_2.snap", "snap_kernel": "pc-kernel_123.snap", - "snap_mode": "", + "snap_mode": boot.DefaultStatus, } si1 := &snap.SideInfo{RealName: "pc-kernel", Revision: snap.R(123)} snapstate.Set(st, "pc-kernel", &snapstate.SnapState{ @@ -1834,7 +1835,7 @@ type: kernel` "snap_core": "core18_2.snap", "snap_kernel": "pc-kernel_123.snap", "snap_try_kernel": "pc-kernel_x1.snap", - "snap_mode": "try", + "snap_mode": boot.TryStatus, }) // pretend we restarted s.mockSuccessfulReboot(c, bloader) @@ -1877,7 +1878,7 @@ type: kernel` bloader.BootVars = map[string]string{ "snap_core": "core18_2.snap", "snap_kernel": "pc-kernel_123.snap", - "snap_mode": "", + "snap_mode": boot.DefaultStatus, } si1 := &snap.SideInfo{RealName: "pc-kernel", Revision: snap.R(123)} snapstate.Set(st, "pc-kernel", &snapstate.SnapState{ @@ -1926,7 +1927,7 @@ type: kernel` "snap_core": "core18_2.snap", "snap_kernel": "pc-kernel_123.snap", "snap_try_kernel": "pc-kernel_x1.snap", - "snap_mode": "try", + "snap_mode": boot.TryStatus, }) // we are in restarting state and the change is not done yet @@ -1949,7 +1950,7 @@ type: kernel` "snap_try_core": "", "snap_try_kernel": "pc-kernel_123.snap", "snap_kernel": "pc-kernel_x1.snap", - "snap_mode": "try", + "snap_mode": boot.TryStatus, }) restarting, _ = st.Restarting() c.Check(restarting, Equals, true) @@ -3794,7 +3795,7 @@ func (ms *mgrsSuite) TestRemodelSwitchToDifferentBase(c *C) { bootloader.Force(bloader) defer bootloader.Force(nil) bloader.SetBootVars(map[string]string{ - "snap_mode": "", + "snap_mode": boot.DefaultStatus, "snap_core": "core18_1.snap", "snap_kernel": "pc-kernel_1.snap", }) @@ -3890,7 +3891,7 @@ version: 20.04` bvars, err := bloader.GetBootVars("snap_mode", "snap_core", "snap_try_core", "snap_kernel", "snap_try_kernel") c.Assert(err, IsNil) c.Assert(bvars, DeepEquals, map[string]string{ - "snap_mode": "try", + "snap_mode": boot.TryStatus, "snap_core": "core18_1.snap", "snap_try_core": "core20_2.snap", "snap_kernel": "pc-kernel_1.snap", @@ -3901,7 +3902,7 @@ version: 20.04` // got updated state.MockRestarting(st, state.RestartUnset) bloader.SetBootVars(map[string]string{ - "snap_mode": "", + "snap_mode": boot.DefaultStatus, "snap_core": "core20_2.snap", "snap_kernel": "pc-kernel_1.snap", }) @@ -3937,7 +3938,7 @@ func (ms *mgrsSuite) TestRemodelSwitchToDifferentBaseUndo(c *C) { bootloader.Force(bloader) defer bootloader.Force(nil) bloader.SetBootVars(map[string]string{ - "snap_mode": "", + "snap_mode": boot.DefaultStatus, "snap_core": "core18_1.snap", "snap_kernel": "pc-kernel_1.snap", }) @@ -4036,7 +4037,7 @@ version: 20.04` // check that the boot vars got updated as expected c.Assert(bloader.BootVars, DeepEquals, map[string]string{ - "snap_mode": "try", + "snap_mode": boot.TryStatus, "snap_core": "core18_1.snap", "snap_try_core": "core20_2.snap", "snap_kernel": "pc-kernel_1.snap", @@ -4044,7 +4045,7 @@ version: 20.04` // simulate successful restart happened ms.mockSuccessfulReboot(c, bloader) c.Assert(bloader.BootVars, DeepEquals, map[string]string{ - "snap_mode": "", + "snap_mode": boot.DefaultStatus, "snap_core": "core20_2.snap", "snap_try_core": "", "snap_kernel": "pc-kernel_1.snap", @@ -4070,7 +4071,7 @@ version: 20.04` "snap_try_core": "core18_1.snap", "snap_kernel": "pc-kernel_1.snap", "snap_try_kernel": "", - "snap_mode": "try", + "snap_mode": boot.TryStatus, }) } @@ -4079,7 +4080,7 @@ func (ms *mgrsSuite) TestRemodelSwitchToDifferentBaseUndoOnRollback(c *C) { bootloader.Force(bloader) defer bootloader.Force(nil) bloader.SetBootVars(map[string]string{ - "snap_mode": "", + "snap_mode": boot.DefaultStatus, "snap_core": "core18_1.snap", "snap_kernel": "pc-kernel_1.snap", }) @@ -4175,7 +4176,7 @@ version: 20.04` // check that the boot vars got updated as expected c.Assert(bloader.BootVars, DeepEquals, map[string]string{ - "snap_mode": "try", + "snap_mode": boot.TryStatus, "snap_core": "core18_1.snap", "snap_try_core": "core20_2.snap", "snap_kernel": "pc-kernel_1.snap", @@ -4183,7 +4184,7 @@ version: 20.04` // simulate successful restart happened ms.mockRollbackAcrossReboot(c, bloader) c.Assert(bloader.BootVars, DeepEquals, map[string]string{ - "snap_mode": "", + "snap_mode": boot.DefaultStatus, "snap_core": "core18_1.snap", "snap_try_core": "", "snap_kernel": "pc-kernel_1.snap", @@ -4203,7 +4204,7 @@ version: 20.04` c.Check(restarting, Equals, false) // bootvars unchanged c.Assert(bloader.BootVars, DeepEquals, map[string]string{ - "snap_mode": "", + "snap_mode": boot.DefaultStatus, "snap_core": "core18_1.snap", "snap_try_core": "", "snap_kernel": "pc-kernel_1.snap", @@ -4384,7 +4385,7 @@ func (ms *kernelSuite) TestRemodelSwitchToDifferentKernel(c *C) { "snap_core": "core_1.snap", "snap_kernel": "pc-kernel_1.snap", "snap_try_kernel": "brand-kernel_2.snap", - "snap_mode": "try", + "snap_mode": boot.TryStatus, }) // simulate successful system-restart bootenv updates (those // vars will be cleared by snapd on a restart) @@ -4395,7 +4396,7 @@ func (ms *kernelSuite) TestRemodelSwitchToDifferentKernel(c *C) { "snap_kernel": "brand-kernel_2.snap", "snap_try_core": "", "snap_try_kernel": "", - "snap_mode": "", + "snap_mode": boot.DefaultStatus, }) // continue @@ -4413,7 +4414,7 @@ func (ms *kernelSuite) TestRemodelSwitchToDifferentKernel(c *C) { "snap_kernel": "brand-kernel_2.snap", "snap_try_kernel": "", "snap_try_core": "", - "snap_mode": "", + "snap_mode": boot.DefaultStatus, }) // ensure tasks were run in the right order @@ -4493,7 +4494,7 @@ func (ms *kernelSuite) TestRemodelSwitchToDifferentKernelUndo(c *C) { "snap_try_core": "", "snap_try_kernel": "pc-kernel_1.snap", "snap_kernel": "brand-kernel_2.snap", - "snap_mode": "try", + "snap_mode": boot.TryStatus, }) } @@ -4548,7 +4549,7 @@ func (ms *kernelSuite) TestRemodelSwitchToDifferentKernelUndoOnRollback(c *C) { "snap_try_core": "", "snap_kernel": "pc-kernel_1.snap", "snap_try_kernel": "", - "snap_mode": "", + "snap_mode": boot.DefaultStatus, }) } @@ -5354,7 +5355,7 @@ type: kernel` bloader.BootVars = map[string]string{ "snap_core": "core18_2.snap", "snap_kernel": "pc-kernel_123.snap", - "snap_mode": "", + "snap_mode": boot.DefaultStatus, } si1 := &snap.SideInfo{RealName: "pc-kernel", Revision: snap.R(123)} snapstate.Set(st, "pc-kernel", &snapstate.SnapState{ @@ -5400,7 +5401,7 @@ type: kernel` "snap_core": "core18_2.snap", "snap_kernel": "pc-kernel_123.snap", "snap_try_kernel": "pc-kernel_x1.snap", - "snap_mode": "try", + "snap_mode": boot.TryStatus, }) // we are in restarting state and the change is not done yet @@ -5428,7 +5429,7 @@ type: kernel` c.Check(bloader.BootVars, DeepEquals, map[string]string{ "snap_core": "core18_2.snap", "snap_kernel": "pc-kernel_123.snap", - "snap_mode": "", + "snap_mode": boot.DefaultStatus, "snap_try_core": "", "snap_try_kernel": "", }) diff --git a/overlord/snapstate/booted_test.go b/overlord/snapstate/booted_test.go index 74fe9a6a32b..b30fbb46f98 100644 --- a/overlord/snapstate/booted_test.go +++ b/overlord/snapstate/booted_test.go @@ -29,6 +29,7 @@ import ( . "gopkg.in/check.v1" + "github.com/snapcore/snapd/boot" "github.com/snapcore/snapd/bootloader" "github.com/snapcore/snapd/bootloader/bootloadertest" "github.com/snapcore/snapd/dirs" @@ -304,7 +305,7 @@ func (bs *bootedSuite) TestWaitRestartCore(c *C) { // core snap, restarted, waiting for current core revision state.MockRestarting(st, state.RestartUnset) - bs.bootloader.BootVars["snap_mode"] = "trying" + bs.bootloader.BootVars["snap_mode"] = boot.TryingStatus err = snapstate.WaitRestart(task, snapsup) c.Check(err, DeepEquals, &state.Retry{After: 5 * time.Second}) @@ -355,7 +356,7 @@ func (bs *bootedSuite) TestWaitRestartBootableBase(c *C) { // core snap, restarted, waiting for current core revision state.MockRestarting(st, state.RestartUnset) - bs.bootloader.BootVars["snap_mode"] = "trying" + bs.bootloader.BootVars["snap_mode"] = boot.TryingStatus err = snapstate.WaitRestart(task, snapsup) c.Check(err, DeepEquals, &state.Retry{After: 5 * time.Second}) @@ -407,7 +408,7 @@ func (bs *bootedSuite) TestWaitRestartKernel(c *C) { // kernel snap, restarted, waiting for current core revision state.MockRestarting(st, state.RestartUnset) - bs.bootloader.BootVars["snap_mode"] = "trying" + bs.bootloader.BootVars["snap_mode"] = boot.TryingStatus err = snapstate.WaitRestart(task, snapsup) c.Check(err, DeepEquals, &state.Retry{After: 5 * time.Second}) diff --git a/overlord/snapstate/handlers.go b/overlord/snapstate/handlers.go index 686941809b2..6a53a8179cf 100644 --- a/overlord/snapstate/handlers.go +++ b/overlord/snapstate/handlers.go @@ -1622,7 +1622,9 @@ func (m *SnapManager) undoLinkSnap(t *state.Task, _ *tomb.Tomb) error { return err } - if newInfo.GetType() == snap.TypeSnapd { + // only restart here if snapd was the installed and needs to + // get uninstalled again + if linkCtx.FirstInstall && newInfo.GetType() == snap.TypeSnapd { // only way to get deviceCtx, err := DeviceCtx(st, t, nil) if err != nil { diff --git a/overlord/snapstate/handlers_link_test.go b/overlord/snapstate/handlers_link_test.go index b02c58049e5..f871eea66bb 100644 --- a/overlord/snapstate/handlers_link_test.go +++ b/overlord/snapstate/handlers_link_test.go @@ -28,6 +28,7 @@ import ( . "gopkg.in/check.v1" + "github.com/snapcore/snapd/boot" "github.com/snapcore/snapd/bootloader" "github.com/snapcore/snapd/bootloader/bootloadertest" "github.com/snapcore/snapd/dirs" @@ -1184,12 +1185,12 @@ func (s *linkSnapSuite) TestUndoLinkSnapdNthInstall(c *C) { c.Check(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops()) c.Check(s.fakeBackend.ops, DeepEquals, expected) - // 2 restarts, one from link snap, another one from undo - c.Check(s.stateBackend.restartRequested, DeepEquals, []state.RestartType{state.RestartDaemon, state.RestartDaemon}) - c.Check(t.Log(), HasLen, 3) + // 1 restarts, one from link snap, the other restart happens + // in undoUnlinkCurrentSnap (not tested here) + c.Check(s.stateBackend.restartRequested, DeepEquals, []state.RestartType{state.RestartDaemon}) + c.Check(t.Log(), HasLen, 2) c.Check(t.Log()[0], Matches, `.*INFO Requested daemon restart \(snapd snap\)\.`) c.Check(t.Log()[1], Matches, `.*INFO unlink`) - c.Check(t.Log()[2], Matches, `.*INFO Requested daemon restart \(snapd snap\)\.`) } func (s *linkSnapSuite) TestDoUnlinkSnapRefreshAwarenessHardCheck(c *C) { @@ -1365,7 +1366,7 @@ func (s *linkSnapSuite) TestMaybeUndoRemodelBootChangesNeedsUndo(c *C) { // that will schedule a boot into the previous kernel c.Assert(bloader.BootVars, DeepEquals, map[string]string{ - "snap_mode": "try", + "snap_mode": boot.TryStatus, "snap_kernel": "new-kernel_1.snap", "snap_try_kernel": "kernel_1.snap", }) diff --git a/packaging/centos-8 b/packaging/centos-8 new file mode 120000 index 00000000000..100fe0cd7bb --- /dev/null +++ b/packaging/centos-8 @@ -0,0 +1 @@ +fedora \ No newline at end of file diff --git a/packaging/fedora/snapd.spec b/packaging/fedora/snapd.spec index 29869280e4e..74f6829f88d 100644 --- a/packaging/fedora/snapd.spec +++ b/packaging/fedora/snapd.spec @@ -620,6 +620,7 @@ install -m 644 -D data/sysctl/rhel7-snap.conf %{buildroot}%{_sysctldir}/99-snap. rm -fv %{buildroot}%{_unitdir}/snapd.system-shutdown.service rm -fv %{buildroot}%{_unitdir}/snapd.snap-repair.* rm -fv %{buildroot}%{_unitdir}/snapd.core-fixup.* +rm -fv %{buildroot}%{_unitdir}/snapd.recovery-chooser-trigger.service # Remove snappy core specific scripts rm %{buildroot}%{_libexecdir}/snapd/snapd.core-fixup.sh diff --git a/packaging/snapd.mk b/packaging/snapd.mk index 52c4c3c9518..485c569a694 100644 --- a/packaging/snapd.mk +++ b/packaging/snapd.mk @@ -155,7 +155,7 @@ install:: ifeq ($(with_core_bits),0) # Remove systemd units that are only used on core devices. install:: - rm -f $(addprefix $(DESTDIR)$(unitdir)/,snapd.autoimport.service snapd.system-shutdown.service snapd.snap-repair.timer snapd.snap-repair.service snapd.core-fixup.service) + rm -f $(addprefix $(DESTDIR)$(unitdir)/,snapd.autoimport.service snapd.system-shutdown.service snapd.snap-repair.timer snapd.snap-repair.service snapd.core-fixup.service snapd.recovery-chooser-trigger.service) # Remove fixup script that is only used on core devices. install:: diff --git a/packaging/ubuntu-16.04/changelog b/packaging/ubuntu-16.04/changelog index 58fb99346c2..4d0136e503c 100644 --- a/packaging/ubuntu-16.04/changelog +++ b/packaging/ubuntu-16.04/changelog @@ -1,3 +1,9 @@ +snapd (2.44~pre1) UNRELEASED; urgency=medium + + * placeholder changelog + + -- Michael Vogt Wed, 26 Feb 2020 09:17:32 +0100 + snapd (2.43.3) xenial; urgency=medium * New upstream release, LP: #1856159 diff --git a/run-checks b/run-checks index 72f0bfc320a..f2f017477ad 100755 --- a/run-checks +++ b/run-checks @@ -155,22 +155,18 @@ if [ "$STATIC" = 1 ]; then ./check-pr-title.py "$TRAVIS_PULL_REQUEST" fi - echo Checking formatting - fmt="" - for dir in $(go list -f '{{.Dir}}' ./... | grep -v '/vendor/' ); do - s="$(${GOFMT:-gofmt} -s -l -d "$dir" | grep -v /vendor/ || true)" - if [ -n "$s" ]; then - fmt="$s\\n$fmt" - fi - done - - if [ -n "$fmt" ]; then - echo "Formatting wrong in following files:" - echo "$fmt" | sed -e 's/\\n/\n/g' - if [ -z "${SKIP_GOFMT:-}" ]; then - exit 1 - else - echo "Ignoring gofmt errors as requested" + if [ -z "${SKIP_GOFMT:-}" ]; then + echo Checking formatting + fmt="" + for dir in $(go list -f '{{.Dir}}' ./... | grep -v '/vendor/' ); do + s="$(${GOFMT:-gofmt} -s -l -d "$dir" | grep -v /vendor/ || true)" + if [ -n "$s" ]; then + fmt="$s\\n$fmt" + fi + done + if [ -n "$fmt" ]; then + echo "Formatting wrong in following files:" + echo "$fmt" | sed -e 's/\\n/\n/g' fi fi diff --git a/seed/helpers.go b/seed/helpers.go index d84e413c1e1..bbb88cee55e 100644 --- a/seed/helpers.go +++ b/seed/helpers.go @@ -100,3 +100,42 @@ func readInfo(snapPath string, si *snap.SideInfo) (*snap.Info, error) { } return snap.ReadInfoFromSnapFile(snapf, si) } + +func snapTypeFromModel(modSnap *asserts.ModelSnap) snap.Type { + switch modSnap.SnapType { + case "base": + return snap.TypeBase + case "core": + return snap.TypeOS + case "gadget": + return snap.TypeGadget + case "kernel": + return snap.TypeKernel + case "snapd": + return snap.TypeSnapd + default: + return snap.TypeApp + } +} + +func essentialSnapTypesToModelFilter(essentialTypes []snap.Type) func(modSnap *asserts.ModelSnap) bool { + m := make(map[string]bool, len(essentialTypes)) + for _, t := range essentialTypes { + switch t { + case snap.TypeBase: + m["base"] = true + case snap.TypeOS: + m["core"] = true + case snap.TypeGadget: + m["gadget"] = true + case snap.TypeKernel: + m["kernel"] = true + case snap.TypeSnapd: + m["snapd"] = true + } + } + + return func(modSnap *asserts.ModelSnap) bool { + return m[modSnap.SnapType] + } +} diff --git a/seed/seed.go b/seed/seed.go index 4eb7ba44f8b..c36ef712391 100644 --- a/seed/seed.go +++ b/seed/seed.go @@ -40,6 +40,10 @@ type Snap struct { SideInfo *snap.SideInfo + // EssentialType is the type of the snap as specified by the model. + // Provided only for essential snaps (Essential = true). + EssentialType snap.Type + Essential bool Required bool @@ -73,7 +77,8 @@ type Seed interface { // error to call Model before LoadAssertions. Model() (*asserts.Model, error) - // LoadMeta loads the seed and seed's snaps metadata. It can + // LoadMeta loads the seed and seed's snaps metadata while + // verifying the underlying snaps against assertions. It can // return ErrNoMeta if there is no metadata nor snaps in the // seed, this is legitimate only on classic. It is an error to // call LoadMeta before LoadAssertions. @@ -92,6 +97,20 @@ type Seed interface { ModeSnaps(mode string) ([]*Snap, error) } +// EssentialMetaLoaderSeed is a Seed that can be asked to load and verify +// only a subset of the essential model snaps via LoadEssentialMeta. +type EssentialMetaLoaderSeed interface { + Seed + + // LoadEssentialMeta loads the seed's snaps metadata for the + // essential snaps with types in the essentialTypes set while + // verifying them against assertions. It can return ErrNoMeta + // if there is no metadata nor snaps in the seed, this is + // legitimate only on classic. It is an error to call LoadMeta + // before LoadAssertions or to mix it with LoadMeta. + LoadEssentialMeta(essentialTypes []snap.Type, tm timings.Measurer) error +} + // Open returns a Seed implementation for the seed at seedDir. // label if not empty is used to identify a Core 20 recovery system seed. func Open(seedDir, label string) (Seed, error) { diff --git a/seed/seed16.go b/seed/seed16.go index 01acdcedd1e..38d16feab90 100644 --- a/seed/seed16.go +++ b/seed/seed16.go @@ -196,7 +196,7 @@ func (s *seed16) LoadMeta(tm timings.Measurer) error { } // add the essential snaps - addEssential := func(snapName string, pinnedTrack string) (*Snap, error) { + addEssential := func(snapName string, pinnedTrack string, essType snap.Type) (*Snap, error) { // be idempotent if added[snapName] { return nil, nil @@ -211,6 +211,11 @@ func (s *seed16) LoadMeta(tm timings.Measurer) error { return nil, err } + if essType == snap.TypeBase && snapName == "core" { + essType = snap.TypeOS + } + + seedSnap.EssentialType = essType seedSnap.Essential = true seedSnap.Required = true added[snapName] = true @@ -222,25 +227,25 @@ func (s *seed16) LoadMeta(tm timings.Measurer) error { if len(yamlSnaps) != 0 { // ensure "snapd" snap is installed first if model.Base() != "" || classicWithSnapd { - if _, err := addEssential("snapd", ""); err != nil { + if _, err := addEssential("snapd", "", snap.TypeSnapd); err != nil { return err } } if !classicWithSnapd { - if _, err := addEssential(baseSnap, ""); err != nil { + if _, err := addEssential(baseSnap, "", snap.TypeBase); err != nil { return err } } } if kernelName := model.Kernel(); kernelName != "" { - if _, err := addEssential(kernelName, model.KernelTrack()); err != nil { + if _, err := addEssential(kernelName, model.KernelTrack(), snap.TypeKernel); err != nil { return err } } if gadgetName := model.Gadget(); gadgetName != "" { - gadget, err := addEssential(gadgetName, model.GadgetTrack()) + gadget, err := addEssential(gadgetName, model.GadgetTrack(), snap.TypeGadget) if err != nil { return err } @@ -260,7 +265,7 @@ func (s *seed16) LoadMeta(tm timings.Measurer) error { if baseSnap != "" && gadgetBase != baseSnap { return fmt.Errorf("cannot use gadget snap because its base %q is different from model base %q", gadgetBase, model.Base()) } - if _, err = addEssential(gadgetBase, ""); err != nil { + if _, err = addEssential(gadgetBase, "", snap.TypeBase); err != nil { return err } } diff --git a/seed/seed16_test.go b/seed/seed16_test.go index 78cb4891e56..e9325452f8d 100644 --- a/seed/seed16_test.go +++ b/seed/seed16_test.go @@ -441,23 +441,26 @@ func (s *seed16Suite) TestLoadMetaCore16Minimal(c *C) { c.Check(essSnaps, DeepEquals, []*seed.Snap{ { - Path: s.expectedPath("core"), - SideInfo: &s.AssertedSnapInfo("core").SideInfo, - Essential: true, - Required: true, - Channel: "stable", + Path: s.expectedPath("core"), + SideInfo: &s.AssertedSnapInfo("core").SideInfo, + EssentialType: snap.TypeOS, + Essential: true, + Required: true, + Channel: "stable", }, { - Path: s.expectedPath("pc-kernel"), - SideInfo: &s.AssertedSnapInfo("pc-kernel").SideInfo, - Essential: true, - Required: true, - Channel: "stable", + Path: s.expectedPath("pc-kernel"), + SideInfo: &s.AssertedSnapInfo("pc-kernel").SideInfo, + EssentialType: snap.TypeKernel, + Essential: true, + Required: true, + Channel: "stable", }, { - Path: s.expectedPath("pc"), - SideInfo: &s.AssertedSnapInfo("pc").SideInfo, - Essential: true, - Required: true, - Channel: "stable", + Path: s.expectedPath("pc"), + SideInfo: &s.AssertedSnapInfo("pc").SideInfo, + EssentialType: snap.TypeGadget, + Essential: true, + Required: true, + Channel: "stable", }, }) @@ -514,29 +517,33 @@ func (s *seed16Suite) TestLoadMetaCore18Minimal(c *C) { c.Check(essSnaps, DeepEquals, []*seed.Snap{ { - Path: s.expectedPath("snapd"), - SideInfo: &s.AssertedSnapInfo("snapd").SideInfo, - Essential: true, - Required: true, - Channel: "stable", + Path: s.expectedPath("snapd"), + SideInfo: &s.AssertedSnapInfo("snapd").SideInfo, + EssentialType: snap.TypeSnapd, + Essential: true, + Required: true, + Channel: "stable", }, { - Path: s.expectedPath("core18"), - SideInfo: &s.AssertedSnapInfo("core18").SideInfo, - Essential: true, - Required: true, - Channel: "stable", + Path: s.expectedPath("core18"), + SideInfo: &s.AssertedSnapInfo("core18").SideInfo, + EssentialType: snap.TypeBase, + Essential: true, + Required: true, + Channel: "stable", }, { - Path: s.expectedPath("pc-kernel"), - SideInfo: &s.AssertedSnapInfo("pc-kernel").SideInfo, - Essential: true, - Required: true, - Channel: "18", + Path: s.expectedPath("pc-kernel"), + SideInfo: &s.AssertedSnapInfo("pc-kernel").SideInfo, + EssentialType: snap.TypeKernel, + Essential: true, + Required: true, + Channel: "18", }, { - Path: s.expectedPath("pc"), - SideInfo: &s.AssertedSnapInfo("pc").SideInfo, - Essential: true, - Required: true, - Channel: "18", + Path: s.expectedPath("pc"), + SideInfo: &s.AssertedSnapInfo("pc").SideInfo, + EssentialType: snap.TypeGadget, + Essential: true, + Required: true, + Channel: "18", }, }) @@ -564,29 +571,33 @@ func (s *seed16Suite) TestLoadMetaCore18(c *C) { c.Check(essSnaps, DeepEquals, []*seed.Snap{ { - Path: s.expectedPath("snapd"), - SideInfo: &s.AssertedSnapInfo("snapd").SideInfo, - Essential: true, - Required: true, - Channel: "stable", + Path: s.expectedPath("snapd"), + SideInfo: &s.AssertedSnapInfo("snapd").SideInfo, + EssentialType: snap.TypeSnapd, + Essential: true, + Required: true, + Channel: "stable", }, { - Path: s.expectedPath("core18"), - SideInfo: &s.AssertedSnapInfo("core18").SideInfo, - Essential: true, - Required: true, - Channel: "stable", + Path: s.expectedPath("core18"), + SideInfo: &s.AssertedSnapInfo("core18").SideInfo, + EssentialType: snap.TypeBase, + Essential: true, + Required: true, + Channel: "stable", }, { - Path: s.expectedPath("pc-kernel"), - SideInfo: &s.AssertedSnapInfo("pc-kernel").SideInfo, - Essential: true, - Required: true, - Channel: "18", + Path: s.expectedPath("pc-kernel"), + SideInfo: &s.AssertedSnapInfo("pc-kernel").SideInfo, + EssentialType: snap.TypeKernel, + Essential: true, + Required: true, + Channel: "18", }, { - Path: s.expectedPath("pc"), - SideInfo: &s.AssertedSnapInfo("pc").SideInfo, - Essential: true, - Required: true, - Channel: "18", + Path: s.expectedPath("pc"), + SideInfo: &s.AssertedSnapInfo("pc").SideInfo, + EssentialType: snap.TypeGadget, + Essential: true, + Required: true, + Channel: "18", }, }) @@ -653,11 +664,12 @@ func (s *seed16Suite) TestLoadMetaClassicCore(c *C) { c.Check(essSnaps, HasLen, 1) c.Check(essSnaps, DeepEquals, []*seed.Snap{ { - Path: s.expectedPath("core"), - SideInfo: &s.AssertedSnapInfo("core").SideInfo, - Essential: true, - Required: true, - Channel: "stable", + Path: s.expectedPath("core"), + SideInfo: &s.AssertedSnapInfo("core").SideInfo, + EssentialType: snap.TypeOS, + Essential: true, + Required: true, + Channel: "stable", }, }) @@ -693,18 +705,20 @@ func (s *seed16Suite) TestLoadMetaClassicCoreWithGadget(c *C) { c.Check(essSnaps, HasLen, 2) c.Check(essSnaps, DeepEquals, []*seed.Snap{ { - Path: s.expectedPath("core"), - SideInfo: &s.AssertedSnapInfo("core").SideInfo, - Essential: true, - Required: true, - Channel: "stable", + Path: s.expectedPath("core"), + SideInfo: &s.AssertedSnapInfo("core").SideInfo, + EssentialType: snap.TypeOS, + Essential: true, + Required: true, + Channel: "stable", }, { - Path: s.expectedPath("classic-gadget"), - SideInfo: &s.AssertedSnapInfo("classic-gadget").SideInfo, - Essential: true, - Required: true, - Channel: "stable", + Path: s.expectedPath("classic-gadget"), + SideInfo: &s.AssertedSnapInfo("classic-gadget").SideInfo, + EssentialType: snap.TypeGadget, + Essential: true, + Required: true, + Channel: "stable", }, }) @@ -731,11 +745,12 @@ func (s *seed16Suite) TestLoadMetaClassicSnapd(c *C) { c.Check(essSnaps, HasLen, 1) c.Check(essSnaps, DeepEquals, []*seed.Snap{ { - Path: s.expectedPath("snapd"), - SideInfo: &s.AssertedSnapInfo("snapd").SideInfo, - Essential: true, - Required: true, - Channel: "stable", + Path: s.expectedPath("snapd"), + SideInfo: &s.AssertedSnapInfo("snapd").SideInfo, + EssentialType: snap.TypeSnapd, + Essential: true, + Required: true, + Channel: "stable", }, }) @@ -775,20 +790,26 @@ func (s *seed16Suite) TestLoadMetaClassicSnapdWithGadget(c *C) { c.Check(essSnaps, HasLen, 3) c.Check(essSnaps, DeepEquals, []*seed.Snap{ { - Path: s.expectedPath("snapd"), - SideInfo: &s.AssertedSnapInfo("snapd").SideInfo, + Path: s.expectedPath("snapd"), + SideInfo: &s.AssertedSnapInfo("snapd").SideInfo, + EssentialType: snap.TypeSnapd, + Essential: true, Required: true, Channel: "stable", }, { - Path: s.expectedPath("classic-gadget"), - SideInfo: &s.AssertedSnapInfo("classic-gadget").SideInfo, + Path: s.expectedPath("classic-gadget"), + SideInfo: &s.AssertedSnapInfo("classic-gadget").SideInfo, + EssentialType: snap.TypeGadget, + Essential: true, Required: true, Channel: "stable", }, { - Path: s.expectedPath("core"), - SideInfo: &s.AssertedSnapInfo("core").SideInfo, + Path: s.expectedPath("core"), + SideInfo: &s.AssertedSnapInfo("core").SideInfo, + EssentialType: snap.TypeOS, + Essential: true, Required: true, Channel: "stable", @@ -819,20 +840,26 @@ func (s *seed16Suite) TestLoadMetaClassicSnapdWithGadget18(c *C) { c.Check(essSnaps, HasLen, 3) c.Check(essSnaps, DeepEquals, []*seed.Snap{ { - Path: s.expectedPath("snapd"), - SideInfo: &s.AssertedSnapInfo("snapd").SideInfo, + Path: s.expectedPath("snapd"), + SideInfo: &s.AssertedSnapInfo("snapd").SideInfo, + EssentialType: snap.TypeSnapd, + Essential: true, Required: true, Channel: "stable", }, { - Path: s.expectedPath("classic-gadget18"), - SideInfo: &s.AssertedSnapInfo("classic-gadget18").SideInfo, + Path: s.expectedPath("classic-gadget18"), + SideInfo: &s.AssertedSnapInfo("classic-gadget18").SideInfo, + EssentialType: snap.TypeGadget, + Essential: true, Required: true, Channel: "stable", }, { - Path: s.expectedPath("core18"), - SideInfo: &s.AssertedSnapInfo("core18").SideInfo, + Path: s.expectedPath("core18"), + SideInfo: &s.AssertedSnapInfo("core18").SideInfo, + EssentialType: snap.TypeBase, + Essential: true, Required: true, Channel: "stable", @@ -881,29 +908,33 @@ func (s *seed16Suite) TestLoadMetaCore18Local(c *C) { c.Check(essSnaps, DeepEquals, []*seed.Snap{ { - Path: s.expectedPath("snapd"), - SideInfo: &s.AssertedSnapInfo("snapd").SideInfo, - Essential: true, - Required: true, - Channel: "stable", + Path: s.expectedPath("snapd"), + SideInfo: &s.AssertedSnapInfo("snapd").SideInfo, + EssentialType: snap.TypeSnapd, + Essential: true, + Required: true, + Channel: "stable", }, { - Path: s.expectedPath("core18"), - SideInfo: &s.AssertedSnapInfo("core18").SideInfo, - Essential: true, - Required: true, - Channel: "stable", + Path: s.expectedPath("core18"), + SideInfo: &s.AssertedSnapInfo("core18").SideInfo, + EssentialType: snap.TypeBase, + Essential: true, + Required: true, + Channel: "stable", }, { - Path: s.expectedPath("pc-kernel"), - SideInfo: &s.AssertedSnapInfo("pc-kernel").SideInfo, - Essential: true, - Required: true, - Channel: "18", + Path: s.expectedPath("pc-kernel"), + SideInfo: &s.AssertedSnapInfo("pc-kernel").SideInfo, + EssentialType: snap.TypeKernel, + Essential: true, + Required: true, + Channel: "18", }, { - Path: s.expectedPath("pc"), - SideInfo: &s.AssertedSnapInfo("pc").SideInfo, - Essential: true, - Required: true, - Channel: "18", + Path: s.expectedPath("pc"), + SideInfo: &s.AssertedSnapInfo("pc").SideInfo, + EssentialType: snap.TypeGadget, + Essential: true, + Required: true, + Channel: "18", }, }) @@ -991,29 +1022,33 @@ func (s *seed16Suite) TestLoadMetaCore18EnforcePinnedTracks(c *C) { c.Check(essSnaps, DeepEquals, []*seed.Snap{ { - Path: s.expectedPath("snapd"), - SideInfo: &s.AssertedSnapInfo("snapd").SideInfo, - Essential: true, - Required: true, - Channel: "stable", + Path: s.expectedPath("snapd"), + SideInfo: &s.AssertedSnapInfo("snapd").SideInfo, + EssentialType: snap.TypeSnapd, + Essential: true, + Required: true, + Channel: "stable", }, { - Path: s.expectedPath("core18"), - SideInfo: &s.AssertedSnapInfo("core18").SideInfo, - Essential: true, - Required: true, - Channel: "stable", + Path: s.expectedPath("core18"), + SideInfo: &s.AssertedSnapInfo("core18").SideInfo, + EssentialType: snap.TypeBase, + Essential: true, + Required: true, + Channel: "stable", }, { - Path: s.expectedPath("pc-kernel"), - SideInfo: &s.AssertedSnapInfo("pc-kernel").SideInfo, - Essential: true, - Required: true, - Channel: "18", + Path: s.expectedPath("pc-kernel"), + SideInfo: &s.AssertedSnapInfo("pc-kernel").SideInfo, + EssentialType: snap.TypeKernel, + Essential: true, + Required: true, + Channel: "18", }, { - Path: s.expectedPath("pc"), - SideInfo: &s.AssertedSnapInfo("pc").SideInfo, - Essential: true, - Required: true, - Channel: "18/edge", + Path: s.expectedPath("pc"), + SideInfo: &s.AssertedSnapInfo("pc").SideInfo, + EssentialType: snap.TypeGadget, + Essential: true, + Required: true, + Channel: "18/edge", }, }) diff --git a/seed/seed20.go b/seed/seed20.go index 292fea5e53b..a98be5c804c 100644 --- a/seed/seed20.go +++ b/seed/seed20.go @@ -31,6 +31,7 @@ package seed import ( "encoding/json" + "errors" "fmt" "os" "path/filepath" @@ -352,8 +353,13 @@ func (s *seed20) addSnap(snapRef naming.SnapRef, optSnap *internal.Snap20, modes return seedSnap, nil } -func (s *seed20) addModelSnap(modelSnap *asserts.ModelSnap, essential bool, tm timings.Measurer) (*Snap, error) { +var errFiltered = errors.New("filtered out") + +func (s *seed20) addModelSnap(modelSnap *asserts.ModelSnap, essential bool, filter func(*asserts.ModelSnap) bool, tm timings.Measurer) (*Snap, error) { optSnap, _ := s.nextOptSnap(modelSnap) + if !filter(modelSnap) { + return nil, errFiltered + } seedSnap, err := s.addSnap(modelSnap, optSnap, modelSnap.Modes, modelSnap.DefaultChannel, "../../snaps", tm) if err != nil { return nil, err @@ -362,6 +368,7 @@ func (s *seed20) addModelSnap(modelSnap *asserts.ModelSnap, essential bool, tm t seedSnap.Essential = essential seedSnap.Required = essential || modelSnap.Presence == "required" if essential { + seedSnap.EssentialType = snapTypeFromModel(modelSnap) s.essentialSnapsNum++ } @@ -369,6 +376,43 @@ func (s *seed20) addModelSnap(modelSnap *asserts.ModelSnap, essential bool, tm t } func (s *seed20) LoadMeta(tm timings.Measurer) error { + if err := s.loadModelMeta(nil, tm); err != nil { + return err + } + + // extra snaps + runMode := []string{"run"} + for { + optSnap, done := s.nextOptSnap(nil) + if done { + break + } + + _, err := s.addSnap(optSnap, optSnap, runMode, "latest/stable", "snaps", tm) + if err != nil { + return err + } + } + + return nil +} + +func (s *seed20) LoadEssentialMeta(essentialTypes []snap.Type, tm timings.Measurer) error { + filterEssential := essentialSnapTypesToModelFilter(essentialTypes) + + if err := s.loadModelMeta(filterEssential, tm); err != nil { + return err + } + + if s.essentialSnapsNum != len(essentialTypes) { + // did not find all the explicitly asked essential types + return fmt.Errorf("model does not specify all the requested essential snaps: %v", essentialTypes) + } + + return nil +} + +func (s *seed20) loadModelMeta(filterEssential func(*asserts.ModelSnap) bool, tm timings.Measurer) error { model, err := s.Model() if err != nil { return err @@ -382,19 +426,31 @@ func (s *seed20) LoadMeta(tm timings.Measurer) error { return err } + essentialOnly := filterEssential != nil + filter := filterEssential + if !essentialOnly { + // no filtering + filter = func(*asserts.ModelSnap) bool { + return true + } + } + allSnaps := model.AllSnaps() // an explicit snapd is the first of all of snaps if allSnaps[0].SnapType != "snapd" { snapdSnap := internal.MakeSystemSnap("snapd", "latest/stable", []string{"run", "ephemeral"}) - if _, err := s.addModelSnap(snapdSnap, true, tm); err != nil { + if _, err := s.addModelSnap(snapdSnap, true, filter, tm); err != nil && err != errFiltered { return err } } essential := true for _, modelSnap := range allSnaps { - seedSnap, err := s.addModelSnap(modelSnap, essential, tm) + seedSnap, err := s.addModelSnap(modelSnap, essential, filter, tm) if err != nil { + if err == errFiltered { + continue + } if _, ok := err.(*NoSnapDeclarationError); ok && modelSnap.Presence == "optional" { // skipped optional snap is ok continue @@ -413,22 +469,12 @@ func (s *seed20) LoadMeta(tm timings.Measurer) error { // TODO: when we allow extend models for classic // we need to add the gadget base here - // done with essential snaps + // done with essential snaps, gadget is the last one essential = false - } - } - - // extra snaps - runMode := []string{"run"} - for { - optSnap, done := s.nextOptSnap(nil) - if done { - break - } - - _, err := s.addSnap(optSnap, optSnap, runMode, "latest/stable", "snaps", tm) - if err != nil { - return err + if essentialOnly { + // will not find more + break + } } } diff --git a/seed/seed20_test.go b/seed/seed20_test.go index 77e5de84de2..ddca1454192 100644 --- a/seed/seed20_test.go +++ b/seed/seed20_test.go @@ -137,29 +137,33 @@ func (s *seed20Suite) TestLoadMetaCore20Minimal(c *C) { c.Check(essSnaps, DeepEquals, []*seed.Snap{ { - Path: s.expectedPath("snapd"), - SideInfo: &s.AssertedSnapInfo("snapd").SideInfo, - Essential: true, - Required: true, - Channel: "latest/stable", + Path: s.expectedPath("snapd"), + SideInfo: &s.AssertedSnapInfo("snapd").SideInfo, + EssentialType: snap.TypeSnapd, + Essential: true, + Required: true, + Channel: "latest/stable", }, { - Path: s.expectedPath("pc-kernel"), - SideInfo: &s.AssertedSnapInfo("pc-kernel").SideInfo, - Essential: true, - Required: true, - Channel: "20", + Path: s.expectedPath("pc-kernel"), + SideInfo: &s.AssertedSnapInfo("pc-kernel").SideInfo, + EssentialType: snap.TypeKernel, + Essential: true, + Required: true, + Channel: "20", }, { - Path: s.expectedPath("core20"), - SideInfo: &s.AssertedSnapInfo("core20").SideInfo, - Essential: true, - Required: true, - Channel: "latest/stable", + Path: s.expectedPath("core20"), + SideInfo: &s.AssertedSnapInfo("core20").SideInfo, + EssentialType: snap.TypeBase, + Essential: true, + Required: true, + Channel: "latest/stable", }, { - Path: s.expectedPath("pc"), - SideInfo: &s.AssertedSnapInfo("pc").SideInfo, - Essential: true, - Required: true, - Channel: "20", + Path: s.expectedPath("pc"), + SideInfo: &s.AssertedSnapInfo("pc").SideInfo, + EssentialType: snap.TypeGadget, + Essential: true, + Required: true, + Channel: "20", }, }) @@ -577,29 +581,33 @@ func (s *seed20Suite) TestLoadMetaCore20(c *C) { c.Check(essSnaps, DeepEquals, []*seed.Snap{ { - Path: s.expectedPath("snapd"), - SideInfo: &s.AssertedSnapInfo("snapd").SideInfo, - Essential: true, - Required: true, - Channel: "latest/stable", + Path: s.expectedPath("snapd"), + SideInfo: &s.AssertedSnapInfo("snapd").SideInfo, + EssentialType: snap.TypeSnapd, + Essential: true, + Required: true, + Channel: "latest/stable", }, { - Path: s.expectedPath("pc-kernel"), - SideInfo: &s.AssertedSnapInfo("pc-kernel").SideInfo, - Essential: true, - Required: true, - Channel: "20", + Path: s.expectedPath("pc-kernel"), + SideInfo: &s.AssertedSnapInfo("pc-kernel").SideInfo, + EssentialType: snap.TypeKernel, + Essential: true, + Required: true, + Channel: "20", }, { - Path: s.expectedPath("core20"), - SideInfo: &s.AssertedSnapInfo("core20").SideInfo, - Essential: true, - Required: true, - Channel: "latest/stable", + Path: s.expectedPath("core20"), + SideInfo: &s.AssertedSnapInfo("core20").SideInfo, + EssentialType: snap.TypeBase, + Essential: true, + Required: true, + Channel: "latest/stable", }, { - Path: s.expectedPath("pc"), - SideInfo: &s.AssertedSnapInfo("pc").SideInfo, - Essential: true, - Required: true, - Channel: "20", + Path: s.expectedPath("pc"), + SideInfo: &s.AssertedSnapInfo("pc").SideInfo, + EssentialType: snap.TypeGadget, + Essential: true, + Required: true, + Channel: "20", }, }) @@ -621,6 +629,144 @@ func (s *seed20Suite) TestLoadMetaCore20(c *C) { c.Check(installSnaps, HasLen, 0) } +func hideSnaps(c *C, all []*seed.Snap, keepTypes []snap.Type) (unhide func()) { + var hidden [][]string +Hiding: + for _, sn := range all { + for _, t := range keepTypes { + if sn.EssentialType == t { + continue Hiding + } + } + origFn := sn.Path + hiddenFn := sn.Path + ".hidden" + err := os.Rename(origFn, hiddenFn) + c.Assert(err, IsNil) + hidden = append(hidden, []string{origFn, hiddenFn}) + } + return func() { + for _, h := range hidden { + err := os.Rename(h[1], h[0]) + c.Assert(err, IsNil) + } + } +} + +func (s *seed20Suite) TestLoadEssentialMetaCore20(c *C) { + s.makeSnap(c, "snapd", "") + s.makeSnap(c, "core20", "") + s.makeSnap(c, "pc-kernel=20", "") + s.makeSnap(c, "pc=20", "") + s.makeSnap(c, "required20", "developerid") + + sysLabel := "20191018" + s.MakeSeed(c, sysLabel, "my-brand", "my-model", map[string]interface{}{ + "display-name": "my model", + "architecture": "amd64", + "base": "core20", + "snaps": []interface{}{ + map[string]interface{}{ + "name": "pc-kernel", + "id": s.AssertedSnapID("pc-kernel"), + "type": "kernel", + "default-channel": "20", + }, + map[string]interface{}{ + "name": "pc", + "id": s.AssertedSnapID("pc"), + "type": "gadget", + "default-channel": "20", + }, + map[string]interface{}{ + "name": "required20", + "id": s.AssertedSnapID("required20"), + }}, + }, nil) + + snapdSnap := &seed.Snap{ + Path: s.expectedPath("snapd"), + SideInfo: &s.AssertedSnapInfo("snapd").SideInfo, + EssentialType: snap.TypeSnapd, + Essential: true, + Required: true, + Channel: "latest/stable", + } + pcKernelSnap := &seed.Snap{ + Path: s.expectedPath("pc-kernel"), + SideInfo: &s.AssertedSnapInfo("pc-kernel").SideInfo, + EssentialType: snap.TypeKernel, + Essential: true, + Required: true, + Channel: "20", + } + core20Snap := &seed.Snap{Path: s.expectedPath("core20"), + SideInfo: &s.AssertedSnapInfo("core20").SideInfo, + EssentialType: snap.TypeBase, + Essential: true, + Required: true, + Channel: "latest/stable", + } + pcSnap := &seed.Snap{ + Path: s.expectedPath("pc"), + SideInfo: &s.AssertedSnapInfo("pc").SideInfo, + EssentialType: snap.TypeGadget, + Essential: true, + Required: true, + Channel: "20", + } + required20Snap := &seed.Snap{ + Path: s.expectedPath("required20"), + } + + all := []*seed.Snap{snapdSnap, pcKernelSnap, core20Snap, pcSnap, required20Snap} + + tests := []struct { + onlyTypes []snap.Type + expected []*seed.Snap + }{ + {[]snap.Type{snap.TypeSnapd}, []*seed.Snap{snapdSnap}}, + {[]snap.Type{snap.TypeKernel}, []*seed.Snap{pcKernelSnap}}, + {[]snap.Type{snap.TypeBase}, []*seed.Snap{core20Snap}}, + {[]snap.Type{snap.TypeGadget}, []*seed.Snap{pcSnap}}, + {[]snap.Type{snap.TypeSnapd, snap.TypeKernel, snap.TypeBase}, []*seed.Snap{snapdSnap, pcKernelSnap, core20Snap}}, + // the order in essentialTypes is not relevant + {[]snap.Type{snap.TypeGadget, snap.TypeKernel}, []*seed.Snap{pcKernelSnap, pcSnap}}, + // degenerate case + {[]snap.Type{}, []*seed.Snap(nil)}, + } + + for _, t := range tests { + // hide the non-requested snaps to make sure they are not + // accessed + unhide := hideSnaps(c, all, t.onlyTypes) + + seed20, err := seed.Open(s.SeedDir, sysLabel) + c.Assert(err, IsNil) + + essSeed20, ok := seed20.(seed.EssentialMetaLoaderSeed) + c.Assert(ok, Equals, true) + + err = essSeed20.LoadAssertions(s.db, s.commitTo) + c.Assert(err, IsNil) + + err = essSeed20.LoadEssentialMeta(t.onlyTypes, s.perfTimings) + c.Assert(err, IsNil) + + c.Check(essSeed20.UsesSnapdSnap(), Equals, true) + + essSnaps := essSeed20.EssentialSnaps() + c.Check(essSnaps, HasLen, len(t.expected)) + + c.Check(essSnaps, DeepEquals, t.expected) + + runSnaps, err := essSeed20.ModeSnaps("run") + c.Assert(err, IsNil) + c.Check(runSnaps, HasLen, 0) + + unhide() + } +} + func (s *seed20Suite) makeLocalSnap(c *C, yamlKey string) (fname string) { return snaptest.MakeTestSnapWithFiles(c, snapYaml[yamlKey], nil) } @@ -675,29 +821,33 @@ func (s *seed20Suite) TestLoadMetaCore20LocalSnaps(c *C) { c.Check(essSnaps, DeepEquals, []*seed.Snap{ { - Path: s.expectedPath("snapd"), - SideInfo: &s.AssertedSnapInfo("snapd").SideInfo, - Essential: true, - Required: true, - Channel: "latest/stable", + Path: s.expectedPath("snapd"), + SideInfo: &s.AssertedSnapInfo("snapd").SideInfo, + EssentialType: snap.TypeSnapd, + Essential: true, + Required: true, + Channel: "latest/stable", }, { - Path: s.expectedPath("pc-kernel"), - SideInfo: &s.AssertedSnapInfo("pc-kernel").SideInfo, - Essential: true, - Required: true, - Channel: "20", + Path: s.expectedPath("pc-kernel"), + SideInfo: &s.AssertedSnapInfo("pc-kernel").SideInfo, + EssentialType: snap.TypeKernel, + Essential: true, + Required: true, + Channel: "20", }, { - Path: s.expectedPath("core20"), - SideInfo: &s.AssertedSnapInfo("core20").SideInfo, - Essential: true, - Required: true, - Channel: "latest/stable", + Path: s.expectedPath("core20"), + SideInfo: &s.AssertedSnapInfo("core20").SideInfo, + EssentialType: snap.TypeBase, + Essential: true, + Required: true, + Channel: "latest/stable", }, { - Path: s.expectedPath("pc"), - SideInfo: &s.AssertedSnapInfo("pc").SideInfo, - Essential: true, - Required: true, - Channel: "20", + Path: s.expectedPath("pc"), + SideInfo: &s.AssertedSnapInfo("pc").SideInfo, + EssentialType: snap.TypeGadget, + Essential: true, + Required: true, + Channel: "20", }, }) @@ -766,29 +916,33 @@ func (s *seed20Suite) TestLoadMetaCore20ChannelOverride(c *C) { c.Check(essSnaps, DeepEquals, []*seed.Snap{ { - Path: s.expectedPath("snapd"), - SideInfo: &s.AssertedSnapInfo("snapd").SideInfo, - Essential: true, - Required: true, - Channel: "latest/stable", + Path: s.expectedPath("snapd"), + SideInfo: &s.AssertedSnapInfo("snapd").SideInfo, + EssentialType: snap.TypeSnapd, + Essential: true, + Required: true, + Channel: "latest/stable", }, { - Path: s.expectedPath("pc-kernel"), - SideInfo: &s.AssertedSnapInfo("pc-kernel").SideInfo, - Essential: true, - Required: true, - Channel: "20", + Path: s.expectedPath("pc-kernel"), + SideInfo: &s.AssertedSnapInfo("pc-kernel").SideInfo, + EssentialType: snap.TypeKernel, + Essential: true, + Required: true, + Channel: "20", }, { - Path: s.expectedPath("core20"), - SideInfo: &s.AssertedSnapInfo("core20").SideInfo, - Essential: true, - Required: true, - Channel: "latest/stable", + Path: s.expectedPath("core20"), + SideInfo: &s.AssertedSnapInfo("core20").SideInfo, + EssentialType: snap.TypeBase, + Essential: true, + Required: true, + Channel: "latest/stable", }, { - Path: s.expectedPath("pc"), - SideInfo: &s.AssertedSnapInfo("pc").SideInfo, - Essential: true, - Required: true, - Channel: "20experimental/edge", + Path: s.expectedPath("pc"), + SideInfo: &s.AssertedSnapInfo("pc").SideInfo, + EssentialType: snap.TypeGadget, + Essential: true, + Required: true, + Channel: "20experimental/edge", }, }) @@ -857,29 +1011,33 @@ func (s *seed20Suite) TestLoadMetaCore20ChannelOverrideSnapd(c *C) { c.Check(essSnaps, DeepEquals, []*seed.Snap{ { - Path: s.expectedPath("snapd"), - SideInfo: &s.AssertedSnapInfo("snapd").SideInfo, - Essential: true, - Required: true, - Channel: "20experimental/edge", + Path: s.expectedPath("snapd"), + SideInfo: &s.AssertedSnapInfo("snapd").SideInfo, + EssentialType: snap.TypeSnapd, + Essential: true, + Required: true, + Channel: "20experimental/edge", }, { - Path: s.expectedPath("pc-kernel"), - SideInfo: &s.AssertedSnapInfo("pc-kernel").SideInfo, - Essential: true, - Required: true, - Channel: "20", + Path: s.expectedPath("pc-kernel"), + SideInfo: &s.AssertedSnapInfo("pc-kernel").SideInfo, + EssentialType: snap.TypeKernel, + Essential: true, + Required: true, + Channel: "20", }, { - Path: s.expectedPath("core20"), - SideInfo: &s.AssertedSnapInfo("core20").SideInfo, - Essential: true, - Required: true, - Channel: "latest/stable", + Path: s.expectedPath("core20"), + SideInfo: &s.AssertedSnapInfo("core20").SideInfo, + EssentialType: snap.TypeBase, + Essential: true, + Required: true, + Channel: "latest/stable", }, { - Path: s.expectedPath("pc"), - SideInfo: &s.AssertedSnapInfo("pc").SideInfo, - Essential: true, - Required: true, - Channel: "20", + Path: s.expectedPath("pc"), + SideInfo: &s.AssertedSnapInfo("pc").SideInfo, + EssentialType: snap.TypeGadget, + Essential: true, + Required: true, + Channel: "20", }, }) @@ -941,28 +1099,32 @@ func (s *seed20Suite) TestLoadMetaCore20LocalSnapd(c *C) { c.Check(essSnaps, DeepEquals, []*seed.Snap{ { - Path: filepath.Join(s.SeedDir, "systems", sysLabel, "snaps", "snapd_1.0.snap"), - SideInfo: &snap.SideInfo{RealName: "snapd"}, - Essential: true, - Required: true, + Path: filepath.Join(s.SeedDir, "systems", sysLabel, "snaps", "snapd_1.0.snap"), + SideInfo: &snap.SideInfo{RealName: "snapd"}, + Essential: true, + EssentialType: snap.TypeSnapd, + Required: true, }, { - Path: s.expectedPath("pc-kernel"), - SideInfo: &s.AssertedSnapInfo("pc-kernel").SideInfo, - Essential: true, - Required: true, - Channel: "20", + Path: s.expectedPath("pc-kernel"), + SideInfo: &s.AssertedSnapInfo("pc-kernel").SideInfo, + EssentialType: snap.TypeKernel, + Essential: true, + Required: true, + Channel: "20", }, { - Path: s.expectedPath("core20"), - SideInfo: &s.AssertedSnapInfo("core20").SideInfo, - Essential: true, - Required: true, - Channel: "latest/stable", + Path: s.expectedPath("core20"), + SideInfo: &s.AssertedSnapInfo("core20").SideInfo, + EssentialType: snap.TypeBase, + Essential: true, + Required: true, + Channel: "latest/stable", }, { - Path: s.expectedPath("pc"), - SideInfo: &s.AssertedSnapInfo("pc").SideInfo, - Essential: true, - Required: true, - Channel: "20", + Path: s.expectedPath("pc"), + SideInfo: &s.AssertedSnapInfo("pc").SideInfo, + EssentialType: snap.TypeGadget, + Essential: true, + Required: true, + Channel: "20", }, }) @@ -1019,29 +1181,33 @@ func (s *seed20Suite) TestLoadMetaCore20ModelOverrideSnapd(c *C) { c.Check(essSnaps, DeepEquals, []*seed.Snap{ { - Path: s.expectedPath("snapd"), - SideInfo: &s.AssertedSnapInfo("snapd").SideInfo, - Essential: true, - Required: true, - Channel: "latest/edge", + Path: s.expectedPath("snapd"), + SideInfo: &s.AssertedSnapInfo("snapd").SideInfo, + EssentialType: snap.TypeSnapd, + Essential: true, + Required: true, + Channel: "latest/edge", }, { - Path: s.expectedPath("pc-kernel"), - SideInfo: &s.AssertedSnapInfo("pc-kernel").SideInfo, - Essential: true, - Required: true, - Channel: "20", + Path: s.expectedPath("pc-kernel"), + SideInfo: &s.AssertedSnapInfo("pc-kernel").SideInfo, + EssentialType: snap.TypeKernel, + Essential: true, + Required: true, + Channel: "20", }, { - Path: s.expectedPath("core20"), - SideInfo: &s.AssertedSnapInfo("core20").SideInfo, - Essential: true, - Required: true, - Channel: "latest/stable", + Path: s.expectedPath("core20"), + SideInfo: &s.AssertedSnapInfo("core20").SideInfo, + EssentialType: snap.TypeBase, + Essential: true, + Required: true, + Channel: "latest/stable", }, { - Path: s.expectedPath("pc"), - SideInfo: &s.AssertedSnapInfo("pc").SideInfo, - Essential: true, - Required: true, - Channel: "20", + Path: s.expectedPath("pc"), + SideInfo: &s.AssertedSnapInfo("pc").SideInfo, + EssentialType: snap.TypeGadget, + Essential: true, + Required: true, + Channel: "20", }, }) @@ -1107,29 +1273,33 @@ func (s *seed20Suite) TestLoadMetaCore20OptionalSnaps(c *C) { c.Check(essSnaps, DeepEquals, []*seed.Snap{ { - Path: s.expectedPath("snapd"), - SideInfo: &s.AssertedSnapInfo("snapd").SideInfo, - Essential: true, - Required: true, - Channel: "latest/stable", + Path: s.expectedPath("snapd"), + SideInfo: &s.AssertedSnapInfo("snapd").SideInfo, + EssentialType: snap.TypeSnapd, + Essential: true, + Required: true, + Channel: "latest/stable", }, { - Path: s.expectedPath("pc-kernel"), - SideInfo: &s.AssertedSnapInfo("pc-kernel").SideInfo, - Essential: true, - Required: true, - Channel: "20", + Path: s.expectedPath("pc-kernel"), + SideInfo: &s.AssertedSnapInfo("pc-kernel").SideInfo, + EssentialType: snap.TypeKernel, + Essential: true, + Required: true, + Channel: "20", }, { - Path: s.expectedPath("core20"), - SideInfo: &s.AssertedSnapInfo("core20").SideInfo, - Essential: true, - Required: true, - Channel: "latest/stable", + Path: s.expectedPath("core20"), + SideInfo: &s.AssertedSnapInfo("core20").SideInfo, + EssentialType: snap.TypeBase, + Essential: true, + Required: true, + Channel: "latest/stable", }, { - Path: s.expectedPath("pc"), - SideInfo: &s.AssertedSnapInfo("pc").SideInfo, - Essential: true, - Required: true, - Channel: "20", + Path: s.expectedPath("pc"), + SideInfo: &s.AssertedSnapInfo("pc").SideInfo, + EssentialType: snap.TypeGadget, + Essential: true, + Required: true, + Channel: "20", }, }) @@ -1203,29 +1373,33 @@ func (s *seed20Suite) TestLoadMetaCore20OptionalSnapsLocal(c *C) { c.Check(essSnaps, DeepEquals, []*seed.Snap{ { - Path: s.expectedPath("snapd"), - SideInfo: &s.AssertedSnapInfo("snapd").SideInfo, - Essential: true, - Required: true, - Channel: "latest/stable", + Path: s.expectedPath("snapd"), + SideInfo: &s.AssertedSnapInfo("snapd").SideInfo, + EssentialType: snap.TypeSnapd, + Essential: true, + Required: true, + Channel: "latest/stable", }, { - Path: s.expectedPath("pc-kernel"), - SideInfo: &s.AssertedSnapInfo("pc-kernel").SideInfo, - Essential: true, - Required: true, - Channel: "20", + Path: s.expectedPath("pc-kernel"), + SideInfo: &s.AssertedSnapInfo("pc-kernel").SideInfo, + EssentialType: snap.TypeKernel, + Essential: true, + Required: true, + Channel: "20", }, { - Path: s.expectedPath("core20"), - SideInfo: &s.AssertedSnapInfo("core20").SideInfo, - Essential: true, - Required: true, - Channel: "latest/stable", + Path: s.expectedPath("core20"), + SideInfo: &s.AssertedSnapInfo("core20").SideInfo, + EssentialType: snap.TypeBase, + Essential: true, + Required: true, + Channel: "latest/stable", }, { - Path: s.expectedPath("pc"), - SideInfo: &s.AssertedSnapInfo("pc").SideInfo, - Essential: true, - Required: true, - Channel: "20", + Path: s.expectedPath("pc"), + SideInfo: &s.AssertedSnapInfo("pc").SideInfo, + EssentialType: snap.TypeGadget, + Essential: true, + Required: true, + Channel: "20", }, }) @@ -1292,29 +1466,33 @@ func (s *seed20Suite) TestLoadMetaCore20ExtraSnaps(c *C) { c.Check(essSnaps, DeepEquals, []*seed.Snap{ { - Path: s.expectedPath("snapd"), - SideInfo: &s.AssertedSnapInfo("snapd").SideInfo, - Essential: true, - Required: true, - Channel: "latest/stable", + Path: s.expectedPath("snapd"), + SideInfo: &s.AssertedSnapInfo("snapd").SideInfo, + EssentialType: snap.TypeSnapd, + Essential: true, + Required: true, + Channel: "latest/stable", }, { - Path: s.expectedPath("pc-kernel"), - SideInfo: &s.AssertedSnapInfo("pc-kernel").SideInfo, - Essential: true, - Required: true, - Channel: "20", + Path: s.expectedPath("pc-kernel"), + SideInfo: &s.AssertedSnapInfo("pc-kernel").SideInfo, + EssentialType: snap.TypeKernel, + Essential: true, + Required: true, + Channel: "20", }, { - Path: s.expectedPath("core20"), - SideInfo: &s.AssertedSnapInfo("core20").SideInfo, - Essential: true, - Required: true, - Channel: "latest/stable", + Path: s.expectedPath("core20"), + SideInfo: &s.AssertedSnapInfo("core20").SideInfo, + EssentialType: snap.TypeBase, + Essential: true, + Required: true, + Channel: "latest/stable", }, { - Path: s.expectedPath("pc"), - SideInfo: &s.AssertedSnapInfo("pc").SideInfo, - Essential: true, - Required: true, - Channel: "20", + Path: s.expectedPath("pc"), + SideInfo: &s.AssertedSnapInfo("pc").SideInfo, + EssentialType: snap.TypeGadget, + Essential: true, + Required: true, + Channel: "20", }, }) @@ -1411,29 +1589,33 @@ func (s *seed20Suite) TestLoadMetaCore20NotRunSnaps(c *C) { c.Check(essSnaps, DeepEquals, []*seed.Snap{ { - Path: s.expectedPath("snapd"), - SideInfo: &s.AssertedSnapInfo("snapd").SideInfo, - Essential: true, - Required: true, - Channel: "latest/stable", + Path: s.expectedPath("snapd"), + SideInfo: &s.AssertedSnapInfo("snapd").SideInfo, + EssentialType: snap.TypeSnapd, + Essential: true, + Required: true, + Channel: "latest/stable", }, { - Path: s.expectedPath("pc-kernel"), - SideInfo: &s.AssertedSnapInfo("pc-kernel").SideInfo, - Essential: true, - Required: true, - Channel: "20", + Path: s.expectedPath("pc-kernel"), + SideInfo: &s.AssertedSnapInfo("pc-kernel").SideInfo, + EssentialType: snap.TypeKernel, + Essential: true, + Required: true, + Channel: "20", }, { - Path: s.expectedPath("core20"), - SideInfo: &s.AssertedSnapInfo("core20").SideInfo, - Essential: true, - Required: true, - Channel: "latest/stable", + Path: s.expectedPath("core20"), + SideInfo: &s.AssertedSnapInfo("core20").SideInfo, + EssentialType: snap.TypeBase, + Essential: true, + Required: true, + Channel: "latest/stable", }, { - Path: s.expectedPath("pc"), - SideInfo: &s.AssertedSnapInfo("pc").SideInfo, - Essential: true, - Required: true, - Channel: "20", + Path: s.expectedPath("pc"), + SideInfo: &s.AssertedSnapInfo("pc").SideInfo, + EssentialType: snap.TypeGadget, + Essential: true, + Required: true, + Channel: "20", }, }) @@ -1539,29 +1721,33 @@ func (s *seed20Suite) TestLoadMetaCore20LocalAssertedSnaps(c *C) { c.Check(essSnaps, DeepEquals, []*seed.Snap{ { - Path: s.expectedPath("snapd"), - SideInfo: &s.AssertedSnapInfo("snapd").SideInfo, - Essential: true, - Required: true, - Channel: "latest/stable", + Path: s.expectedPath("snapd"), + SideInfo: &s.AssertedSnapInfo("snapd").SideInfo, + EssentialType: snap.TypeSnapd, + Essential: true, + Required: true, + Channel: "latest/stable", }, { - Path: s.expectedPath("pc-kernel"), - SideInfo: &s.AssertedSnapInfo("pc-kernel").SideInfo, - Essential: true, - Required: true, - Channel: "20", + Path: s.expectedPath("pc-kernel"), + SideInfo: &s.AssertedSnapInfo("pc-kernel").SideInfo, + EssentialType: snap.TypeKernel, + Essential: true, + Required: true, + Channel: "20", }, { - Path: s.expectedPath("core20"), - SideInfo: &s.AssertedSnapInfo("core20").SideInfo, - Essential: true, - Required: true, - Channel: "latest/stable", + Path: s.expectedPath("core20"), + SideInfo: &s.AssertedSnapInfo("core20").SideInfo, + EssentialType: snap.TypeBase, + Essential: true, + Required: true, + Channel: "latest/stable", }, { - Path: s.expectedPath("pc"), - SideInfo: &s.AssertedSnapInfo("pc").SideInfo, - Essential: true, - Required: true, - Channel: "20/edge", + Path: s.expectedPath("pc"), + SideInfo: &s.AssertedSnapInfo("pc").SideInfo, + EssentialType: snap.TypeGadget, + Essential: true, + Required: true, + Channel: "20/edge", }, }) diff --git a/spread.yaml b/spread.yaml index c5a279e8054..dac665c01e5 100644 --- a/spread.yaml +++ b/spread.yaml @@ -99,31 +99,33 @@ backends: - opensuse-15.0-64: workers: 6 manual: true - - opensuse-15.1-64: - workers: 6 - - opensuse-tumbleweed-64: - workers: 6 - arch-linux-64: - manual: true workers: 6 + - amazon-linux-2-64: + workers: 6 + storage: preserve-size - centos-7-64: workers: 6 image: centos-7-64 + - centos-8-64: + workers: 4 + image: centos-8-64 # TODO: spread is really unhappy when it sees a backend without any systems, # so this block is intentially kept commented out, until we need to add # systems to it # google-unstable: - type: google - key: '$(HOST: echo "$SPREAD_GOOGLE_KEY")' - location: computeengine/us-east1-b - halt-timeout: 2h - systems: - - amazon-linux-2-64: + type: google + key: '$(HOST: echo "$SPREAD_GOOGLE_KEY")' + location: computeengine/us-east1-b + halt-timeout: 2h + systems: + - opensuse-15.1-64: + workers: 6 + - opensuse-tumbleweed-64: workers: 6 - storage: preserve-size google-tpm: @@ -202,7 +204,7 @@ backends: username: ubuntu password: ubuntu - ubuntu-core-20-64: - image: ubuntu-18.04-64 + image: ubuntu-20.04-64 username: ubuntu password: ubuntu flags: [virtio] @@ -586,6 +588,11 @@ prepare: | apt-get update && apt-get install -y eatmydata fi + if [[ "$SPREAD_SYSTEM" == centos-* ]]; then + # make sure EPEL is enabled + yum install -y epel-release + fi + # Unpack delta, or move content out of the prefixed directory (see rename and repack above). # (needs to be in spread.yaml directly because there's nothing else on the filesystem yet) if [ -f current.delta ]; then @@ -597,12 +604,12 @@ prepare: | apt-get update >& "$tf" || ( cat "$tf"; exit 1 ) apt-get install -y xdelta3 curl eatmydata >& "$tf" || ( cat "$tf"; exit 1 ) ;; - fedora-*) - dnf install --refresh -y xdelta curl &> "$tf" || (cat "$tf"; exit 1) - ;; - amazon-*|centos-*) + amazon-*|centos-7-*) yum install -y xdelta curl &> "$tf" || (cat "$tf"; exit 1) ;; + fedora-*|centos-*) + dnf install --refresh -y xdelta curl &> "$tf" || (cat "$tf"; exit 1) + ;; opensuse-*) zypper -q --gpg-auto-import-keys refresh zypper -q install -y xdelta3 curl &> "$tf" || (cat "$tf"; exit 1) diff --git a/tests/core/apt/task.yaml b/tests/core/apt/task.yaml index cf4d2c876ac..82bd8c05198 100644 --- a/tests/core/apt/task.yaml +++ b/tests/core/apt/task.yaml @@ -1,16 +1,24 @@ summary: Ensure that the apt output on UC16 is correct -# TODO:UC20: test that apt-get doesn't exist on UC18 / UC20 in this tests and -# re-enable -systems: [ubuntu-core-16-*] - execute: | - expected="Ubuntu Core does not use apt-get, see 'snap --help'!" - if apt-get update > output.txt; then - echo "apt should exit 1 but did not" - exit 1 - fi - if [ "$(cat output.txt)" != "$expected" ]; then - echo "Unexpected apt output: $(cat output.txt)" - exit 1 + # shellcheck source=tests/lib/systems.sh + . "$TESTSLIB/systems.sh" + + if is_core16_system; then + # UC16 has apt-get output a message + expected="Ubuntu Core does not use apt-get, see 'snap --help'!" + if apt-get update > output.txt; then + echo "apt should exit 1 but did not" + exit 1 + fi + if [ "$(cat output.txt)" != "$expected" ]; then + echo "Unexpected apt output: $(cat output.txt)" + exit 1 + fi + else + # UC18+ does not have apt-get at all + if command -v apt-get; then + echo "apt-get command exists on Ubuntu Core, but should not!" + exit 1 + fi fi diff --git a/tests/core/basic20/task.yaml b/tests/core/basic20/task.yaml index ba8b1e8bf8b..35d5df5aa9e 100644 --- a/tests/core/basic20/task.yaml +++ b/tests/core/basic20/task.yaml @@ -27,9 +27,9 @@ execute: | echo "Ensure passwd/group is available for snaps" test-snapd-sh-core18.sh -c 'cat /var/lib/extrausers/passwd' | MATCH test - # TODO:UC20: also check for /boot/grub/kernel.efi symlink once it's there - echo "Check that we have an extracted kernel" + echo "Ensure extracted kernel.efi exists" test -e /boot/grub/pc-kernel*/kernel.efi + test -e /boot/grub/kernel.efi # ensure that our the-tool (and thus our snap-bootstrap ran) echo "Check that we booted with the rebuilt initramfs in the kernel snap" diff --git a/tests/core/classic-snap16/task.yaml b/tests/core/classic-snap16/task.yaml index ede25d47601..9d7a1a8d34c 100644 --- a/tests/core/classic-snap16/task.yaml +++ b/tests/core/classic-snap16/task.yaml @@ -1,5 +1,7 @@ summary: Ensure classic dimension works correctly +# classic snap is not maintained for UC18+, for these releases the lxd snaps is +# recommended instead of the classic snap systems: [ubuntu-core-16-*] environment: diff --git a/tests/core/config-defaults-once/task.yaml b/tests/core/config-defaults-once/task.yaml index 7dd583bdad3..547e7f836b4 100644 --- a/tests/core/config-defaults-once/task.yaml +++ b/tests/core/config-defaults-once/task.yaml @@ -1,7 +1,9 @@ summary: | - Test that configuration defaults are only applied once. + Test that configuration defaults are only applied once. -# ubuntu-core-16: it is not yet possible to install snapd on core16 +# it is not yet possible to install snapd on UC16 +# TODO:UC20: enable for UC20, currently fails because there is no seed.yaml in +# the same place as UC18 systems: [ubuntu-core-18-*] environment: diff --git a/tests/core/create-user-2/task.yaml b/tests/core/create-user-2/task.yaml index 4449de6bcab..ca7e016bc88 100644 --- a/tests/core/create-user-2/task.yaml +++ b/tests/core/create-user-2/task.yaml @@ -8,6 +8,9 @@ environment: USER_NAME: mvo restore: | + if [ -e managed.device ]; then + exit 0 + fi snap remove-user "$USER_NAME" execute: | @@ -15,7 +18,7 @@ execute: | expected="error: while creating user: access denied" if obtained=$(su - test /bin/sh -c "snap create-user $USER_EMAIL 2>&1"); then echo "create-user command should have failed" - fi + fi [[ "$obtained" =~ $expected ]] if [ "$(snap managed)" = "true" ]; then @@ -25,6 +28,10 @@ execute: | exit 1 fi MATCH "cannot create user: device already managed" < create.error + + # Leave a file indicating the device was initially managed + touch managed.device + exit 0 fi diff --git a/tests/core/create-user/task.yaml b/tests/core/create-user/task.yaml index 535c9e59fcd..3501c41e0fa 100644 --- a/tests/core/create-user/task.yaml +++ b/tests/core/create-user/task.yaml @@ -4,21 +4,27 @@ summary: Ensure that snap create-user works in ubuntu-core # TODO:UC20: enable for UC20 systems: [ubuntu-core-1*] +environment: + USER_EMAIL: mvo@ubuntu.com + USER_NAME: mvo + restore: | - # FIXME: use deluser here now that it supports --extrausers - sed -i '/^mvo/d' /var/lib/extrausers/passwd - sed -i '/^mvo/d' /var/lib/extrausers/shadow - sed -i '/^mvo/d' /var/lib/extrausers/group - rm -rf /home/mvo - rm -f create.error + if [ -e managed.device ]; then + exit 0 + fi + snap remove-user "$USER_NAME" execute: | if [ "$MANAGED_DEVICE" = "true" ]; then - if snap create-user --sudoer mvo@ubuntu.com 2>create.error; then + if snap create-user --sudoer "$USER_EMAIL" 2>create.error; then echo "Did not get expected error creating user in managed device" exit 1 fi MATCH "cannot create user: device already managed" < create.error + + # Leave a file indicating the device was initially managed + touch managed.device + exit 0 fi echo "Adding invalid user" @@ -30,23 +36,23 @@ execute: | MATCH "$expected" <<<"$output" echo "Adding valid user" - expected='created user "mvo"' - output=$(snap create-user --sudoer mvo@ubuntu.com) + expected="created user \"$USER_NAME\"" + output=$(snap create-user --sudoer "$USER_EMAIL") if [ "$output" != "$expected" ]; then echo "Unexpected output $output" exit 1 fi echo "Ensure there are ssh keys imported" - MATCH ssh-rsa < /home/mvo/.ssh/authorized_keys + MATCH ssh-rsa < /home/"$USER_NAME"/.ssh/authorized_keys echo "Ensure the user is a sudo user" - sudo -u mvo sudo true + sudo -u "$USER_NAME" sudo true echo "ensure the user's home directory exists" - test -d /home/mvo + test -d /home/"$USER_NAME" echo "ensure ~/.snap/auth.json was created" - test -f /home/mvo/.snap/auth.json + test -f /home/"$USER_NAME"/.snap/auth.json echo "ensure user's email was stored in ~/.snap/auth.json" - MATCH '"email":"mvo@ubuntu.com"' < /home/mvo/.snap/auth.json + MATCH "\"email\":\"$USER_EMAIL\"" < /home/"$USER_NAME"/.snap/auth.json diff --git a/tests/core/custom-device-reg-extras/task.yaml b/tests/core/custom-device-reg-extras/task.yaml index 871678c3edd..fab4f069d8e 100644 --- a/tests/core/custom-device-reg-extras/task.yaml +++ b/tests/core/custom-device-reg-extras/task.yaml @@ -3,7 +3,8 @@ summary: | with the prepare-device gadget hook and this can set request headers, a proposed serial and the body of the serial assertion -# TODO:UC20: enable for UC20 +# TODO:UC20: enable for UC20, it assumes /var/lib/snapd/seed/assertions/model +# which we don't have currently systems: [ubuntu-core-1*-64] prepare: | diff --git a/tests/core/custom-device-reg/task.yaml b/tests/core/custom-device-reg/task.yaml index c081546a094..05c35fd0e1b 100644 --- a/tests/core/custom-device-reg/task.yaml +++ b/tests/core/custom-device-reg/task.yaml @@ -2,8 +2,9 @@ summary: | Test that device initialisation and registration can be customized with the prepare-device gadget hook -# TODO:UC20: enable for UC20 -systems: [ubuntu-core-1*-64] +# TODO:UC20: enable for UC20, it assumes /var/lib/snapd/seed/assertions/model +# which we don't have currently +systems: [ubuntu-core-1*] prepare: | if [ "$TRUST_TEST_KEYS" = "false" ]; then diff --git a/tests/core/device-reg/task.yaml b/tests/core/device-reg/task.yaml index 2c25d0e1b2d..fdc503933b4 100644 --- a/tests/core/device-reg/task.yaml +++ b/tests/core/device-reg/task.yaml @@ -2,8 +2,8 @@ summary: | Ensure after device initialisation registration worked and we have a serial and can acquire a session macaroon -# TODO:UC20: enable for UC20 -systems: [ubuntu-core-1*] +# TODO:UC20: enable on arm when we have the model names for these devices +systems: [-ubuntu-core-20-arm*] execute: | #shellcheck source=tests/lib/names.sh @@ -29,6 +29,9 @@ execute: | snap model --serial --assertion | MATCH "authority-id: canonical" snap model --serial --assertion | MATCH "brand-id: canonical" case "$SPREAD_SYSTEM" in + ubuntu-core-20-64) + snap model --serial --assertion | MATCH "model: ubuntu-core-20-amd64" + ;; ubuntu-core-18-64) snap model --serial --assertion | MATCH "model: ubuntu-core-18-amd64" ;; diff --git a/tests/core/grub-no-unpacked-assets/task.yaml b/tests/core/grub-no-unpacked-assets/task.yaml index 48bdf91c329..a1e37e8975e 100644 --- a/tests/core/grub-no-unpacked-assets/task.yaml +++ b/tests/core/grub-no-unpacked-assets/task.yaml @@ -1,7 +1,6 @@ summary: Ensure we have no unpacked kernel.img/initrd.img on grub systems -# TODO:UC20: enable for UC20 on ubuntu-seed grub? -systems: [ubuntu-core-1*-64] +systems: [ubuntu-core-*-64] environment: NAME/initrdimg: initrd.img* diff --git a/tests/core/kernel-ver/task.yaml b/tests/core/kernel-ver/task.yaml new file mode 100644 index 00000000000..5fb78b2bee4 --- /dev/null +++ b/tests/core/kernel-ver/task.yaml @@ -0,0 +1,18 @@ +summary: Ensure that we have the right kernel + +# TODO: enable for arm? +systems: [ubuntu-core-*-64] + +execute: | + # shellcheck source=tests/lib/systems.sh + . "$TESTSLIB/systems.sh" + + echo "Check kernel version" + if is_core16_system; then + VER="^4.4" + elif is_core18_system; then + VER="^4.15" + elif is_core20_system; then + VER="^5.4" + fi + uname -r | MATCH $VER diff --git a/tests/core/kernel/task.yaml b/tests/core/kernel/task.yaml deleted file mode 100644 index 8bc9a1bd55e..00000000000 --- a/tests/core/kernel/task.yaml +++ /dev/null @@ -1,9 +0,0 @@ -summary: Ensure that we have the right kernel - -# TODO:UC20: add 5.X series kernel for UC20 -# TODO: add 4.4 series kernel check for UC16 -systems: [ubuntu-core-18-*] - -execute: | - echo "Check kernel version" - uname -r | MATCH "^4.15" diff --git a/tests/core/network-config/task.yaml b/tests/core/network-config/task.yaml index 79dfbfe607f..ba4451774d7 100644 --- a/tests/core/network-config/task.yaml +++ b/tests/core/network-config/task.yaml @@ -1,9 +1,5 @@ summary: Check that `snap set {system,core} network.disable-ipv6` works -# the test is only meaningful on core devices -# TODO:UC20: enable for UC20 -systems: [ubuntu-core-1*] - environment: snap_nick/system: system snap_nick/core: core @@ -11,7 +7,7 @@ environment: execute: | echo "Ensure we have inet6" ip addr | MATCH inet6 - + echo "Disable ipv6" # shellcheck disable=SC2154 snap set "$snap_nick" network.disable-ipv6=true diff --git a/tests/core/os-release/task.yaml b/tests/core/os-release/task.yaml index 468d5da5806..7301f907804 100644 --- a/tests/core/os-release/task.yaml +++ b/tests/core/os-release/task.yaml @@ -1,15 +1,20 @@ summary: check that os-release is correct -# TODO:UC20: enable with DISTRIB_RELEASE=20 -systems: [ubuntu-core-1*] +debug: | + cat /etc/lsb-release execute: | #shellcheck source=tests/lib/systems.sh . "$TESTSLIB"/systems.sh - if is_core18_system; then + if is_core16_system; then + MATCH "DISTRIB_RELEASE=16" < /etc/lsb-release + elif is_core18_system; then MATCH "DISTRIB_RELEASE=18" < /etc/lsb-release + elif is_core20_system; then + MATCH "DISTRIB_RELEASE=20" < /etc/lsb-release else - MATCH "DISTRIB_RELEASE=16" < /etc/lsb-release + echo "Unknown Ubuntu Core system!" + exit 1 fi MATCH "ID=ubuntu-core" < /etc/os-release diff --git a/tests/core/reboot/task.yaml b/tests/core/reboot/task.yaml index 7178cba98c1..fad7cf37d6c 100644 --- a/tests/core/reboot/task.yaml +++ b/tests/core/reboot/task.yaml @@ -1,8 +1,5 @@ summary: Ensure that service and apparmor profiles work after a reboot -# TODO:UC20: enable for UC20 -systems: [ubuntu-core-1*] - # Start early as it takes a long time. priority: 100 diff --git a/tests/core/remove/task.yaml b/tests/core/remove/task.yaml index 710f8554657..69eebd1abda 100644 --- a/tests/core/remove/task.yaml +++ b/tests/core/remove/task.yaml @@ -1,17 +1,26 @@ summary: Check that removal of essential snaps does not work -# TODO:UC20: enable for UC20+ with core20 as an unremovable base snap -systems: [ubuntu-core-18-*] +# UC16 does not seed the snapd snap (yet) +systems: [-ubuntu-core-16-*] execute: | + # shellcheck source=tests/lib/systems.sh + . "$TESTSLIB/systems.sh" + echo "Ensure snapd cannot be removed" if snap remove --purge snapd; then echo "The snapd snap should not be removable" exit 1 fi - echo "Ensure core18 cannot be removed" - if snap remove --purge core18; then - echo "The core18 snap should not removable" + + if is_core18_system; then + base=core18 + elif is_core20_system; then + base=core20 + fi + echo "Ensure $base cannot be removed" + if snap remove --purge "$base"; then + echo "The $base snap should not removable" exit 1 fi @@ -19,7 +28,7 @@ execute: | snap install test-snapd-sh snap list | MATCH '^core ' if snap remove --purge core; then - echo "core should not be removalble because test-snapd-tools needs it" + echo "core should not be removable because test-snapd-tools needs it" exit 1 fi diff --git a/tests/core/services/task.yaml b/tests/core/services/task.yaml index 50b6feeecc7..3fa0ae2d86b 100644 --- a/tests/core/services/task.yaml +++ b/tests/core/services/task.yaml @@ -1,8 +1,5 @@ summary: Ensure all services on Core are active -# TODO:UC20: enable for UC20 -systems: [ubuntu-core-1*] - execute: | echo "Ensure one-shot services are working" for oneshot in snapd.autoimport.service snapd.sshd-keygen.service; do @@ -11,6 +8,6 @@ execute: | echo "Ensure services are working" systemctl status snapd.service |MATCH active - + echo "Ensure timers are working" systemctl is-active snapd.snap-repair.timer diff --git a/tests/core/snap-auto-import-asserts-spools/task.yaml b/tests/core/snap-auto-import-asserts-spools/task.yaml index ae2cb890ffb..4083d9137e1 100644 --- a/tests/core/snap-auto-import-asserts-spools/task.yaml +++ b/tests/core/snap-auto-import-asserts-spools/task.yaml @@ -1,7 +1,6 @@ summary: Check that `snap auto-import` works as expected -# TODO:UC20: enable for UC20 -systems: [ubuntu-core-1*-64] +systems: [ubuntu-core-*-64] prepare: | if [ "$TRUST_TEST_KEYS" = "false" ]; then diff --git a/tests/core/snap-auto-import-asserts/task.yaml b/tests/core/snap-auto-import-asserts/task.yaml index 4673db83dc9..1b7dd46dd95 100644 --- a/tests/core/snap-auto-import-asserts/task.yaml +++ b/tests/core/snap-auto-import-asserts/task.yaml @@ -1,7 +1,6 @@ summary: Check that `snap auto-import` works as expected -# TODO:UC20: enable for UC20 -systems: [ubuntu-core-1*-64] +systems: [ubuntu-core-*-64] prepare: | if [ "$TRUST_TEST_KEYS" = "false" ]; then diff --git a/tests/core/snap-auto-mount/task.yaml b/tests/core/snap-auto-mount/task.yaml index bdf81c49996..480df3657e2 100644 --- a/tests/core/snap-auto-mount/task.yaml +++ b/tests/core/snap-auto-mount/task.yaml @@ -1,6 +1,7 @@ summary: Check that `snap auto-import` works as expected -# TODO:UC20: enable for UC20 +# TODO:UC20: fix for UC20 and enable, currently fails because mkfs.vfat is not +# available on UC20 systems: [ubuntu-core-1*-64] prepare: | diff --git a/tests/core/snap-core-fixup/task.yaml b/tests/core/snap-core-fixup/task.yaml index a80c7874f5d..3c30f63ac67 100644 --- a/tests/core/snap-core-fixup/task.yaml +++ b/tests/core/snap-core-fixup/task.yaml @@ -1,6 +1,6 @@ summary: Check that the core-fixup-sh script works -# TODO:UC20: enable for UC20 +# TODO:UC20: enable for UC20, currently fails with "Image not repaired" systems: [ubuntu-core-1*] restore: | diff --git a/tests/core/snap-set-core-config/task.yaml b/tests/core/snap-set-core-config/task.yaml index 4fec7470971..3f793bd146a 100644 --- a/tests/core/snap-set-core-config/task.yaml +++ b/tests/core/snap-set-core-config/task.yaml @@ -1,6 +1,5 @@ summary: Ensure "snap set core" works -# the test is only meaningful on core devices # TODO:UC20: enable for UC20 systems: [ubuntu-core-1*] diff --git a/tests/core/snapd-failover/task.yaml b/tests/core/snapd-failover/task.yaml index b36320a1437..c125fda02d6 100644 --- a/tests/core/snapd-failover/task.yaml +++ b/tests/core/snapd-failover/task.yaml @@ -1,16 +1,18 @@ summary: Check that snapd failure handling works -# TODO:UC20: enable for UC20, currently snapd.failure.service does not work on -# UC20, it currently isn't activated for some reason # UC16 still uses the core snap rather than the snapd snap, so disable this test # for UC16 -systems: [-ubuntu-core-20-*, -ubuntu-core-16-*] +systems: [-ubuntu-core-16-*] debug: | # dump failure data journalctl -u snapd.failure.service journalctl -u snapd.socket || true ls -l /snap/snapd/ + cat /etc/systemd/system/snapd.service + cat /etc/systemd/system/usr-lib-snapd.mount + /snap/snapd/x1/usr/bin/snap debug state /var/lib/snapd/state.json || true + /snap/snapd/x1/usr/bin/snap debug state --change="$(/snap/snapd/x1/usr/bin/snap debug state /var/lib/snapd/state.json|tail -n1|awk '{print $1}')" /var/lib/snapd/state.json || true prepare: | cp -a /etc/systemd/system/snapd.service.d/local.conf local.conf.bak diff --git a/tests/main/ubuntu-core-snapd-failover/task.yaml b/tests/core/snapd-failover16/task.yaml similarity index 100% rename from tests/main/ubuntu-core-snapd-failover/task.yaml rename to tests/core/snapd-failover16/task.yaml diff --git a/tests/main/ubuntu-core-snapd/task.yaml b/tests/core/snapd16/task.yaml similarity index 96% rename from tests/main/ubuntu-core-snapd/task.yaml rename to tests/core/snapd16/task.yaml index fcfa5358ac3..6fb48df856e 100644 --- a/tests/main/ubuntu-core-snapd/task.yaml +++ b/tests/core/snapd16/task.yaml @@ -28,7 +28,7 @@ execute: | # RESTORE CODE # we cannot restore in "restore:" because we need a reboot # right now to get a clean state again - systemctl stop snapd.service snapd.socket snapd.autoimport.service snapd.snap-repair.service + systemctl stop snapd.service snapd.socket snapd.autoimport.service snapd.snap-repair.service snapd.snap-repair.timer umount "/snap/snapd/$(readlink /snap/snapd/current)" rm -f /etc/systemd/system/usr-lib-snapd.mount diff --git a/tests/core/writablepaths/task.yaml b/tests/core/writablepaths/task.yaml index a9ab84b755b..0309380d2a7 100644 --- a/tests/core/writablepaths/task.yaml +++ b/tests/core/writablepaths/task.yaml @@ -1,8 +1,5 @@ summary: Ensure that the writable paths on the image are correct -# TODO:UC20: enable for uc20 -systems: [ubuntu-core-1*] - execute: | echo "Ensure everything in writable-paths is actually writable" #shellcheck disable=SC2002 diff --git a/tests/lib/assertions/ubuntu-core-20-amd64.model b/tests/lib/assertions/ubuntu-core-20-amd64.model index db24a42b7e3..60bfa5df9d3 100644 --- a/tests/lib/assertions/ubuntu-core-20-amd64.model +++ b/tests/lib/assertions/ubuntu-core-20-amd64.model @@ -1,42 +1,42 @@ type: model -authority-id: vxj7EkYOlg15uAHRswTXIUpzWqO2sTBi +authority-id: canonical series: 16 -brand-id: vxj7EkYOlg15uAHRswTXIUpzWqO2sTBi -model: ubuntu-core-20-mvo-amd64 +brand-id: canonical +model: ubuntu-core-20-amd64-dangerous architecture: amd64 base: core20 grade: dangerous snaps: - - default-channel: 20/edge + default-channel: 20/stable id: UqFziVZDHLSyO3TqSWgNBoAdHbLI4dAH name: pc type: gadget - - default-channel: latest/edge - id: DLqre5XGLbDqg9jPtiAhRRjDuPVa5X1q - name: core20 - type: base - - - default-channel: 20/edge + default-channel: 20/stable id: pYVQrBcKmBa0mZ4CCN7ExT6jH8rY1hza name: pc-kernel type: kernel - - default-channel: latest/edge + default-channel: latest/stable + id: DLqre5XGLbDqg9jPtiAhRRjDuPVa5X1q + name: core20 + type: base + - + default-channel: latest/stable id: PMrrV4ml8uWuEUDBT8dSGnKUYbevVhc4 name: snapd type: snapd -timestamp: 2019-08-26T00:00:00.0Z -sign-key-sha3-384: 2XxnFfNz1CCA6OZ5v03QjbFaK_A6-FjnNzNYJSMhSm_RC77iriSBh9X_olPNmupv +timestamp: 2020-01-13T15:47:00.0Z +sign-key-sha3-384: 9tydnLa6MTJ-jaQTFUXEwHl1yRx7ZS4K5cyFDhYDcPzhS7uyEkDxdUjg9g08BtNn -AcLBcwQAAQoAHRYhBNPJP7iUC6HvARf0BQG87hgg441ZBQJeFYBTAAoJEAG87hgg441Zh8EQAKUj -XYZAf8OlKDRD5uRPjV0eX4RA+UGieu/l392cNWsN18ngEMewFEKtLXltqeLF9SO/mwTVWPVn8jl4 -RbJESfxA8jDKYnfmMNT/kiytRMSxXkOcrxpW54LuFSg9LkZx4vQOQR1Snjic7z2HHLpq3Ain5Pry -zjWddYi68ew2MDM2Y6jxGa/V651w5B1YTev4rNcmVkvXQFxFuZe5CYaFswpfZMtqk1N6K7pPF2RT -OQh1Wl9fYA7tBjgt4jFcEr6L9qWj4oQbFXElhlvZGbKkjdv3A0b3BGyqKxA/1ptsAcp9DfWUNcXd -/ya28u+n3bZkgKxJY/pL5Q9R90DaoIxoDKK4vzzQQ6twVQi/6ug0J7F8obkhuvGrMwhhmEltn2hr -z5xMO6EZcQY1YxLXsPmH/5uZI4V4wH3+DAmpOBzNMRRFZOQjC8ngJIeKQI1e8rzH9qUzKXNvJRXC -GMoHE7zqwe9z2lTAN1wHAHpctwepb/XNe+tRIcxxBpo83rbwii6zkikNRhsSF9djk0hUF1OM7dst -e3wC0wi0wpp3tbmEvKOhWXbCQK0lZywkV6KpASM5uXn4Q4vC+Y8sp6CPhEnXCDCrfM7WolmjUeUC -KHTZSZjNA94TZ/KUYvqeBWC2G9D9NMhdUVN+77y8AERz9mm9wjbMQuStpUM4dQXw5GEB5fBU +AcLBXAQAAQoABgUCXiaAHgAKCRDgT5vottzAEtjfEACV1WqxZ5sfPNeCJJoyuvrq559j7puqz0l7 +VH7EhBGFJgUd+RgtBwVczcz5+93qpGJXdWn2NsTJ9YqJH7nDXMCmzueZn7ZggEvbIitXpjCgatcu +Dv0QmtvZcEG27GVsTiENZqLrwAJsg1mSohj/N/ghBYC8mXEXP7mtF8GjRSnkCJTcjCjXQj1qXff5 +TEjBgYhbvbrw8iLzJjigWfhowEy31EE7SNzh5mT/Np91SIl3Y4EKGNmanI3xmvskNNULoHttfjZ0 +GTBoJrXsIak3tqKkC8saWurI4PDGpt8XsfegAP27f+PVOVgCx1vj/i+xaaskgisk+lW4BzIxkcm6 +ZkXaWWaW7xbFIutiluZLAxVX34XjAQUaAScfUAGG2FEQzi7sbJQTkXJtepoftMWH1ZVNBz4w4HIl +t6FeHqsuLDnC5QSMZwds7fh+TOKW8g3EQTQ99cwiF6j7GUJiOR0r/z1YFXOvsi2ZYAD7wsCQrKS/ +eApEre3kTtHbTsZlp621/e/cIe+aIZ+Ian40rYLQ2RdLelEWLYvT6axnW6SceQAgnOsMEn8sApfI +lmk02GpkGOA7kVTcaN/XhQOLY+kL3tFGFIQ/XG4hYorzZgd9sCGMZ2mG9WTT7fmA+ivBkf/Dk5iR +PS8bi4ci6fyYjaMdL0HnYWRk26Pxm/fL+kRC/mt1FA== diff --git a/tests/lib/pkgdb.sh b/tests/lib/pkgdb.sh index 6530acb7e5b..6c173cc1bf4 100755 --- a/tests/lib/pkgdb.sh +++ b/tests/lib/pkgdb.sh @@ -128,12 +128,12 @@ distro_name_package() { ubuntu-*|debian-*) debian_name_package "$@" ;; - fedora-*) - fedora_name_package "$@" - ;; - amazon-*|centos-*) + amazon-*|centos-7-*) amazon_name_package "$@" ;; + fedora-*|centos-*) + fedora_name_package "$@" + ;; opensuse-*) opensuse_name_package "$@" ;; @@ -174,12 +174,12 @@ distro_install_local_package() { # shellcheck disable=SC2086 apt install $flags "$@" ;; - fedora-*) - quiet dnf -y install --setopt=install_weak_deps=False "$@" - ;; - amazon-*|centos-*) + amazon-*|centos-7-*) quiet yum -y localinstall "$@" ;; + fedora-*|centos-*) + quiet dnf -y install --setopt=install_weak_deps=False "$@" + ;; opensuse-*) quiet rpm -i --replacepkgs "$@" ;; @@ -269,13 +269,13 @@ distro_install_package() { # shellcheck disable=SC2086 quiet eatmydata apt-get install $APT_FLAGS -y "${pkg_names[@]}" ;; - fedora-*) + amazon-*|centos-7-*) # shellcheck disable=SC2086 - quiet dnf -y --refresh install $DNF_FLAGS "${pkg_names[@]}" + quiet yum -y install $YUM_FLAGS "${pkg_names[@]}" ;; - amazon-*|centos-*) + fedora-*|centos-*) # shellcheck disable=SC2086 - quiet yum -y install $YUM_FLAGS "${pkg_names[@]}" + quiet dnf -y --refresh install $DNF_FLAGS "${pkg_names[@]}" ;; opensuse-*) # packages may be downgraded in the repositories, which would be @@ -319,13 +319,13 @@ distro_purge_package() { ubuntu-*|debian-*) quiet eatmydata apt-get remove -y --purge -y "$@" ;; - fedora-*) + amazon-*|centos-7-*) + quiet yum -y remove "$@" + ;; + fedora-*|centos-*) quiet dnf -y remove "$@" quiet dnf clean all ;; - amazon-*|centos-*) - quiet yum -y remove "$@" - ;; opensuse-*) quiet zypper remove -y "$@" ;; @@ -344,14 +344,14 @@ distro_update_package_db() { ubuntu-*|debian-*) quiet eatmydata apt-get update ;; - fedora-*) - quiet dnf clean all - quiet dnf makecache - ;; - amazon-*|centos-*) + amazon-*|centos-7-*) quiet yum clean all quiet yum makecache ;; + fedora-*|centos-*) + quiet dnf clean all + quiet dnf makecache + ;; opensuse-*) quiet zypper --gpg-auto-import-keys refresh ;; @@ -370,12 +370,12 @@ distro_clean_package_cache() { ubuntu-*|debian-*) quiet eatmydata apt-get clean ;; - fedora-*) - dnf clean all - ;; - amazon-*|centos-*) + amazon-*|centos-7-*) yum clean all ;; + fedora-*|centos-*) + dnf clean all + ;; opensuse-*) zypper -q clean --all ;; @@ -394,12 +394,12 @@ distro_auto_remove_packages() { ubuntu-*|debian-*) quiet eatmydata apt-get -y autoremove ;; - fedora-*) - quiet dnf -y autoremove - ;; - amazon-*|centos-*) + amazon-*|centos-7-*) quiet yum -y autoremove ;; + fedora-*|centos-*) + quiet dnf -y autoremove + ;; opensuse-*) ;; arch-*) @@ -416,12 +416,12 @@ distro_query_package_info() { ubuntu-*|debian-*) apt-cache policy "$1" ;; - fedora-*) - dnf info "$1" - ;; - amazon-*|centos-*) + amazon-*|centos-7-*) yum info "$1" ;; + fedora-*|centos-*) + dnf info "$1" + ;; opensuse-*) zypper info "$1" ;; @@ -593,6 +593,7 @@ pkg_dependencies_ubuntu_classic(){ weston xdg-user-dirs xdg-utils + zsh " case "$SPREAD_SYSTEM" in @@ -700,8 +701,8 @@ pkg_dependencies_fedora(){ jq iptables-services man - mock net-tools + nmap-ncat nfs-utils PackageKit python3-yaml @@ -714,6 +715,7 @@ pkg_dependencies_fedora(){ xdg-user-dirs xdg-utils strace + zsh " } @@ -730,7 +732,6 @@ pkg_dependencies_amazon(){ jq iptables-services man - mock nc net-tools nfs-utils @@ -741,6 +742,7 @@ pkg_dependencies_amazon(){ xdg-utils udisks2 upower + zsh " } @@ -768,6 +770,7 @@ pkg_dependencies_opensuse(){ uuidd xdg-user-dirs xdg-utils + zsh " } @@ -788,6 +791,7 @@ pkg_dependencies_arch(){ libseccomp libcap libx11 + man net-tools nfs-utils openbsd-netcat @@ -806,6 +810,7 @@ pkg_dependencies_arch(){ xdg-utils xfsprogs apparmor + zsh " } @@ -819,12 +824,12 @@ pkg_dependencies(){ pkg_dependencies_ubuntu_generic pkg_dependencies_ubuntu_classic ;; - fedora-*) - pkg_dependencies_fedora - ;; - amazon-*|centos-*) + amazon-*|centos-7-*) pkg_dependencies_amazon ;; + fedora-*|centos-*) + pkg_dependencies_fedora + ;; opensuse-*) pkg_dependencies_opensuse ;; diff --git a/tests/lib/prepare.sh b/tests/lib/prepare.sh index 4427a1c74b3..408cd866b45 100755 --- a/tests/lib/prepare.sh +++ b/tests/lib/prepare.sh @@ -641,10 +641,6 @@ EOF setup_reflash_magic() { # install the stuff we need distro_install_package kpartx busybox-static - # for core20 we need debootstrap to build the initramfs in the kernel snap - if is_core20_system; then - distro_install_package debootstrap - fi distro_install_local_package "$GOHOME"/snapd_*.deb distro_clean_package_cache @@ -652,16 +648,23 @@ setup_reflash_magic() { # need to be seeded to proceed with snap install snap wait system seed.loaded + # download the snapd snap for all uc systems except uc16 + if ! is_core16_system; then + snap download "--channel=${SNAPD_CHANNEL}" snapd + fi + # we cannot use "names.sh" here because no snaps are installed yet core_name="core" if is_core18_system; then - snap download "--channel=${SNAPD_CHANNEL}" snapd core_name="core18" - elif is_core20_system; then - snap download "--channel=${SNAPD_CHANNEL}" snapd + elif is_core20_system; then core_name="core20" fi snap install "--channel=${CORE_CHANNEL}" "$core_name" + if is_core16_system || is_core18_system; then + UNPACK_DIR="/tmp/$core_name-snap" + unsquashfs -no-progress -d "$UNPACK_DIR" /var/lib/snapd/snaps/${core_name}_*.snap + fi # install ubuntu-image snap install --classic --edge ubuntu-image @@ -685,15 +688,9 @@ setup_reflash_magic() { IMAGE=core18-amd64.img elif is_core20_system; then repack_snapd_snap_with_deb_content_and_run_mode_firstboot_tweaks "$IMAGE_HOME" - # TODO:UC20: use canonical model instead of "mvo" one cp "$TESTSLIB/assertions/ubuntu-core-20-amd64.model" "$IMAGE_HOME/pc.model" IMAGE=core20-amd64.img else - # modify the core snap so that the current root-pw works there - # for spread to do the first login - UNPACK_DIR="/tmp/core-snap" - unsquashfs -no-progress -d "$UNPACK_DIR" /var/lib/snapd/snaps/core_*.snap - # FIXME: install would be better but we don't have dpkg on # the image # unpack our freshly build snapd into the new snapd snap @@ -783,9 +780,7 @@ EOF # /dev/loop19 0 0 1 1 /var/lib/snapd/snaps/test-snapd-netplan-apply_75.snap 0 512 devloop=$(losetup --list --noheadings | grep "$IMAGE_HOME/$IMAGE" | awk '{print $1}') dev=$(basename "$devloop") - if is_core18_system; then - mount "/dev/mapper/${dev}p3" /mnt - elif is_core20_system; then + if is_core20_system; then # (ab)use ubuntu-seed mount "/dev/mapper/${dev}p2" /mnt else @@ -838,12 +833,7 @@ EOF /home/gopath /root/test-etc /var/lib/extrausers fi - # now modify the image writable partition - if is_core18_system; then - UNPACK_DIR="/tmp/core18-snap" - unsquashfs -no-progress -d "$UNPACK_DIR" /var/lib/snapd/snaps/core18_*.snap - fi - # modifying "writable" is only possible on uc16/uc18 + # now modify the image writable partition - only possible on uc16 / uc18 if is_core16_system || is_core18_system; then # modify the writable partition of "core" so that we have the # test user diff --git a/tests/lib/reset.sh b/tests/lib/reset.sh index 2916d216bf8..f1a3a97db3c 100755 --- a/tests/lib/reset.sh +++ b/tests/lib/reset.sh @@ -50,6 +50,7 @@ reset_classic() { ls -lR "$SNAP_MOUNT_DIR"/ /var/snap/ exit 1 fi + rm -rf /tmp/snap.* case "$SPREAD_SYSTEM" in fedora-*|centos-*) @@ -160,6 +161,7 @@ reset_all_snap() { systemctl stop snapd.service snapd.socket restore_snapd_state rm -rf /root/.snap + rm -rf /tmp/snap.* if [ "$1" != "--keep-stopped" ]; then systemctl start snapd.service snapd.socket fi diff --git a/tests/lib/user.sh b/tests/lib/user.sh index 3d84c5c356c..95337f135a4 100644 --- a/tests/lib/user.sh +++ b/tests/lib/user.sh @@ -60,3 +60,9 @@ as_user() { as_user_simple() { su -l -c "$*" "$TEST_USER" } + +as_given_user() { + local user="$1" + shift + su -l -c "$*" "$user" +} diff --git a/tests/main/install-store/task.yaml b/tests/main/install-store/task.yaml index c99c36b14de..20f0a3e21b7 100644 --- a/tests/main/install-store/task.yaml +++ b/tests/main/install-store/task.yaml @@ -13,7 +13,7 @@ environment: execute: | echo "Install from different channels" - expected="(?s)$SNAP_NAME .* from Canonical\\* installed\\n" + expected="(?s)$SNAP_NAME .* from Test snaps \(test-snaps-canonical\) installed\\n" for channel in edge beta candidate stable do snap install --unicode=never "$SNAP_NAME" --channel=$channel | grep -Pzq "$expected" @@ -21,7 +21,7 @@ execute: | done echo "Install non-devmode snap with devmode option" - expected="(?s)$SNAP_NAME .* from Canonical\\* installed\\n" + expected="(?s)$SNAP_NAME .* from Test snaps \(test-snaps-canonical\) installed\\n" snap install --unicode=never "$SNAP_NAME" --devmode | grep -Pzq "$expected" echo "Install devmode snap without devmode option" diff --git a/tests/main/interfaces-packagekit-control/task.yaml b/tests/main/interfaces-packagekit-control/task.yaml index bd7c129a5f9..24de9fefed4 100644 --- a/tests/main/interfaces-packagekit-control/task.yaml +++ b/tests/main/interfaces-packagekit-control/task.yaml @@ -5,6 +5,8 @@ systems: - -ubuntu-core-* # Ubuntu 14.04 PackageKit seems to be too old - -ubuntu-14.04-* + # PackageKit is buggy https://bugzilla.redhat.com/show_bug.cgi?id=1807864 + - -centos-8-* execute: | echo "Installing test-snapd-packagekit" diff --git a/tests/main/local-install-w-metadata/task.yaml b/tests/main/local-install-w-metadata/task.yaml index 6a454cd7ba4..fbc07730226 100644 --- a/tests/main/local-install-w-metadata/task.yaml +++ b/tests/main/local-install-w-metadata/task.yaml @@ -17,7 +17,7 @@ execute: | snap install test-snapd-sh_*.snap echo "The revision is not a local revision" - snap list|MATCH '^test-snapd-sh.* [0-9]+\s+-\s+cachio' + snap list|MATCH '^test-snapd-sh.* [0-9]+\s+-\s+test-snaps-canonical' echo "Test it" test-snapd-sh.sh -c 'true' diff --git a/tests/main/preseed-snapd-snap/seed.yaml.in b/tests/main/preseed-snapd-snap/seed.yaml.in new file mode 100644 index 00000000000..353e8a05395 --- /dev/null +++ b/tests/main/preseed-snapd-snap/seed.yaml.in @@ -0,0 +1,7 @@ +snaps: + - name: snapd + file: @SNAPD@ + unasserted: true + - name: core18 + file: @CORE@ + unasserted: true diff --git a/tests/main/preseed-snapd-snap/task.yaml b/tests/main/preseed-snapd-snap/task.yaml new file mode 100644 index 00000000000..52d74195e3f --- /dev/null +++ b/tests/main/preseed-snapd-snap/task.yaml @@ -0,0 +1,110 @@ +summary: | + Check that preseeding of current ubuntu cloud image works with snapd + and core18. + +description: | + This test checks that preseeding of Ubuntu cloud images with snap-preseed + command works, up to the point where the image is ready to be booted. + The test assumes cloud image with a core and lxd snaps in its seeds/. + +systems: [ubuntu-19.10-*, ubuntu-20.04-*] + +environment: + IMAGE_MOUNTPOINT: /mnt/cloudimg + +prepare: | + # the get_image_url_for_nested_vm is a convenient helper that returns + # a cloud image url matching current $SPREAD_SYSTEM. + #shellcheck source=tests/lib/nested.sh + . "$TESTSLIB/nested.sh" + wget "$(get_image_url_for_nested_vm)" -O cloudimg.img + mkdir -p "$IMAGE_MOUNTPOINT" + +restore: | + #shellcheck source=tests/lib/preseed.sh + . "$TESTSLIB/preseed.sh" + + # any of the restore commands can fail depending on where execute part stopped, + # account for that with ||true. + umount_ubuntu_image "$IMAGE_MOUNTPOINT" || true + +execute: | + setup_preseeding_core18() { + local IMAGE_MOUNTPOINT=$1 + local SEEDS_DIR + SEEDS_DIR="$IMAGE_MOUNTPOINT/var/lib/snapd/seed/" + rm -f "$SEEDS_DIR"/snaps/*.snap + rm -f "$SEEDS_DIR"/seed.yaml + + snap download --edge snapd + snap download --edge core18 + + CORE=$(ls core18*.snap) + SNAPD=$(ls snapd*.snap) + sed seed.yaml.in -E -e "s/@CORE@/$CORE/" -e "s/@SNAPD@/$SNAPD/" > "$SEEDS_DIR"/seed.yaml + + cp "$CORE" "$SNAPD" "$SEEDS_DIR"/snaps/ + } + + #shellcheck source=tests/lib/preseed.sh + . "$TESTSLIB/preseed.sh" + mount_ubuntu_image cloudimg.img "$IMAGE_MOUNTPOINT" + setup_preseeding_core18 "$IMAGE_MOUNTPOINT" + + echo "Running pre-seeeding" + /usr/lib/snapd/snap-preseed "$IMAGE_MOUNTPOINT" + + # sanity, core snap mounted by snap-preseed got unmounted + mount | not MATCH "snap-preseed" + + snap debug state "$IMAGE_MOUNTPOINT"/var/lib/snapd/state.json --change=1 > tasks.log + + echo "Check that the tasks of preseeded snapd have expected statuses" + # Note, these checks match statuses, but not the order + MATCH "Done .+ prerequisites +Ensure prerequisites for \"snapd\" are available" < tasks.log + MATCH "Done .+ prepare-snap +Prepare snap \"/var/lib/snapd/seed/snaps/snapd_[0-9]+.snap" < tasks.log + MATCH "Done .+ mount-snap +Mount snap \"snapd\"" < tasks.log + MATCH "Done .+ copy-snap-data +Copy snap \"snapd\" data" < tasks.log + MATCH "Done .+ setup-profiles +Setup snap \"snapd\" \(unset\) security profiles" < tasks.log + MATCH "Done .+ link-snap +Make snap \"snapd\" \(unset\) available to the system" < tasks.log + MATCH "Done .+ auto-connect +Automatically connect eligible plugs and slots of snap \"snapd\"" < tasks.log + MATCH "Done .+ set-auto-aliases +Set automatic aliases for snap \"snapd\"" < tasks.log + MATCH "Done .+ setup-aliases +Setup snap \"snapd\" aliases" < tasks.log + MATCH "Done .+ prerequisites +Ensure prerequisites for \"core18\" are available" < tasks.log + MATCH "Done .+ prepare-snap +Prepare snap \"/var/lib/snapd/seed/snaps/core18_[0-9]+.snap\"" < tasks.log + MATCH "Done .+ mount-snap +Mount snap \"core18\" \(unset\)" < tasks.log + MATCH "Done .+ copy-snap-data +Copy snap \"core18\" data" < tasks.log + MATCH "Done .+ setup-profiles +Setup snap \"core18\" \(unset\) security profiles" < tasks.log + MATCH "Done .+ link-snap +Make snap \"core18\" \(unset\) available to the system" < tasks.log + MATCH "Done .+ auto-connect +Automatically connect eligible plugs and slots of snap \"core18\"" < tasks.log + MATCH "Done .+ setup-profiles +Setup snap \"core18\" \(unset\) security profiles" < tasks.log + MATCH "Done .+ set-auto-aliases +Set automatic aliases for snap \"core18\"" < tasks.log + MATCH "Done .+ setup-aliases +Setup snap \"core18\" aliases" < tasks.log + + echo "Checking that there were no other 'Done' tasks when preseeding" + [ "$(grep -c ' Done ' tasks.log)" = "18" ] + + # mark-preseeded task is where snap-preseed stopped, therefore it's in Doing. + MATCH "Doing .+ mark-preseeded +Mark system pre-seeded" < tasks.log + + # everything below is pending execution on first boot + MATCH "Do .+ run-hook +Run install hook of \"snapd\" snap if present" < tasks.log + MATCH "Do .+ start-snap-services +Start snap \"snapd\" \(unset\) services" < tasks.log + MATCH "Do .+ run-hook +Run configure hook of \"core\" snap if present" < tasks.log + MATCH "Do .+ run-hook +Run install hook of \"core18\" snap if present" < tasks.log + MATCH "Do .+ start-snap-services +Start snap \"core18\" \(unset\) services" < tasks.log + MATCH "Do .+ run-hook +Run health check of \"core18\" snap" < tasks.log + MATCH "Do .+ mark-seeded +Mark system seeded" < tasks.log + + echo "Checking that mount units have been created on the target image" + SYSTEMD_UNITS="$IMAGE_MOUNTPOINT"/etc/systemd + test -f "$SYSTEMD_UNITS"/system/snap-snapd-*.mount + test -f "$SYSTEMD_UNITS"/system/snap-core18-*.mount + + echo "Checking enabled systemd mount units" + test -L "$SYSTEMD_UNITS"/system/multi-user.target.wants/snap-snapd-*.mount + test -L "$SYSTEMD_UNITS"/system/multi-user.target.wants/snap-core18-*.mount + + #shellcheck source=tests/lib/preseed.sh + . "$TESTSLIB/preseed.sh" + umount_ubuntu_image "$IMAGE_MOUNTPOINT" diff --git a/tests/main/remove-user/task.yaml b/tests/main/remove-user/task.yaml index 58b89feebe4..d1739a2335c 100644 --- a/tests/main/remove-user/task.yaml +++ b/tests/main/remove-user/task.yaml @@ -9,18 +9,32 @@ environment: USER_NAME: mvo prepare: | + # Note: make this test work with the user already created in the device + if [ "$(snap managed)" = "true" ]; then + # Leave a file indicating the device was initially managed + touch managed.device + + exit 0 + fi snap create-user --sudoer "$USER_EMAIL" restore: | + if [ -e managed.device ]; then + exit 0 + fi userdel --extrausers -r "$USER_NAME" || true rm -rf "/etc/sudoers.d/create-user-$USER_NAME" execute: | + if [ -e managed.device ]; then + exit 0 + fi + echo "sanity check: user in passwd" id "$USER_NAME" echo "sanity check: has sudoer file" test -f "/etc/sudoers.d/create-user-$USER_NAME" - echo "sanity check: user has a home" + echo "sanity check: user has a home" test -d "/home/$USER_NAME" echo "snap remove-user fails when run as non-root user without sudo" diff --git a/tests/main/retry-network/task.yaml b/tests/main/retry-network/task.yaml index 806c3f939a0..7490c5969d4 100644 --- a/tests/main/retry-network/task.yaml +++ b/tests/main/retry-network/task.yaml @@ -33,11 +33,13 @@ execute: | # be paranoid and look at the low-level go error as well MATCH "(Temporary failure in name resolution|network is unreachable)" < stderr - echo "Now without DNS resolver" - UBUNTU_IP="$(getent hosts www.ubuntu.com|awk '{print $1 }' | head -n1)" + echo "Now without DNS resolver with ipv4" + UBUNTU_IP="$(getent ahostsv4 www.ubuntu.com|awk '{print $1 }' | head -n1)" SNAPD_DEBUG=1 nsenter --net=/var/run/netns/testns ./detect-retry "http://$UBUNTU_IP" > output.txt 2>stderr # XXX: ShouldRetryError is slightly misbehaving, see comment above MATCH "ShouldRetryError: true" < output.txt MATCH "NoNetwork: true" < output.txt # be paranoid and look at the low-level go error as well MATCH "network is unreachable" < stderr + + # XXX: also test with ipv6 diff --git a/tests/main/security-private-tmp/task.yaml b/tests/main/security-private-tmp/task.yaml index 2e90679b414..e0d5bd4982a 100644 --- a/tests/main/security-private-tmp/task.yaml +++ b/tests/main/security-private-tmp/task.yaml @@ -20,7 +20,7 @@ prepare: | snap install --dangerous not-test-snapd-sh_1.0_all.snap restore: | - rm -rf "$SNAP_INSTALL_DIR" /tmp/foo + rm -rf "$SNAP_INSTALL_DIR" /tmp/foo /tmp/snap.not-test-snapd-sh execute: | #shellcheck source=tests/lib/dirs.sh diff --git a/tests/main/selinux-lxd/task.yaml b/tests/main/selinux-lxd/task.yaml index 51f6e324853..6751774c502 100644 --- a/tests/main/selinux-lxd/task.yaml +++ b/tests/main/selinux-lxd/task.yaml @@ -29,6 +29,14 @@ restore: | lxd-tool undo-lxd-mount-changes + rm -f avc-after + +debug: | + if [ -e avc-after ]; then + echo "AVC log from the test" + cat avc-after + fi + execute: | snap install lxd ausearch -i --checkpoint stamp --start checkpoint -m AVC 2>&1 | MATCH 'no matches' @@ -65,6 +73,9 @@ execute: | # there is a known problem with the reference policy that disallows systemd # from creating a BPF map for unconfined_service_t, see: # https://bugzilla.redhat.com/show_bug.cgi?id=1694115 - ausearch --checkpoint stamp --start checkpoint -m AVC 2>&1 | \ - grep -v -E 'avc: denied { map_create } for pid=[0-9]+ comm="systemd"' | \ + # + # ausearch exits with 1 when there are no denials, which we expect on + # centos-8, but not on centos-7 + ausearch --checkpoint stamp --start checkpoint -m AVC > avc-after 2>&1 || true + grep -v -E 'avc: denied { map_create } for pid=[0-9]+ comm="systemd"' < avc-after | \ not MATCH 'type=AVC' diff --git a/tests/main/snap-confine-undesired-mode-group/task.yaml b/tests/main/snap-confine-undesired-mode-group/task.yaml index a71047834df..c3548bdf7f5 100644 --- a/tests/main/snap-confine-undesired-mode-group/task.yaml +++ b/tests/main/snap-confine-undesired-mode-group/task.yaml @@ -7,8 +7,6 @@ prepare: | snap install --dangerous ./test-snapd-app_1.0_all.snap snap connect test-snapd-app:opengl snap connect test-snapd-app:joystick - # Remove any stale temp file left over by other runs. - rm -rf /tmp/snap.test-snapd-app execute: | # Run the snap as a non-root user. su test -c 'snap run test-snapd-app.sh -c /bin/true' diff --git a/tests/main/user-session-env/task.yaml b/tests/main/user-session-env/task.yaml new file mode 100644 index 00000000000..2692e0a8245 --- /dev/null +++ b/tests/main/user-session-env/task.yaml @@ -0,0 +1,84 @@ +summary: verify that user environment settings are added + +description: | + User environment variables are added via /etc/profile.d/snapd.sh (bash/sh + specific) or via /lib/environment.d/ helpers. Make sure that at least one of + the mechanisms works and XDG_DATA_DIRS and PATH are updated accordingly + inside the user session, no matter the shell they use. + +systems: + # cannot install zsh + - -ubuntu-core-* + +environment: + TEST_ZSH_USER: test-zsh + +prepare: | + #shellcheck source=tests/lib/user.sh + . "$TESTSLIB/user.sh" + + echo "create a user with a different shell" + useradd --create-home --user-group -s /bin/zsh "$TEST_ZSH_USER" + + if ! has_user_session_support; then + echo "NOTE: no user session support, skipping session setup" + exit 0 + fi + + touch ./has-sessions + + # setup for systems supporting user sessions + for uid in $(id -u test) $(id -u "$TEST_ZSH_USER"); do + mkdir -p "/run/user/$uid" + chown -R "$uid:$uid" "/run/user/$uid" + systemctl start "user@$uid.service" + done + +restore: | + userdel -f -r "$TEST_ZSH_USER" + + rm -f test-profile-env test-profile-zsh-env test-session-env test-zsh-session-env + + if [ -e has-sessions ]; then + for uid in $(id -u test) $(id -u "$TEST_ZSH_USER"); do + systemctl stop "user@$uid.service" + done + fi + rm -f ./has-sessions + +execute: | + #shellcheck source=tests/lib/dirs.sh + . "$TESTSLIB/dirs.sh" + #shellcheck source=tests/lib/user.sh + . "$TESTSLIB/user.sh" + + for user in test "$TEST_ZSH_USER"; do + if [ -e ./has-sessions ]; then + uid=$(id -u "$user") + # dump the environment set up by the user session manager + as_given_user "$user" "XDG_RUNTIME_DIR=/run/user/$uid systemctl --user show-environment" > "${user}-session-env" + fi + as_given_user "$user" "env" > "${user}-profile-env" + done + + for user in test "$TEST_ZSH_USER"; do + # even though there's user session support, systemd may be too old and + # not support user-environment-generators (specifically systemd versions + # earlier than 233) + if [ -e ./has-sessions ] && [ -d /usr/lib/systemd/user-environment-generators ]; then + MATCH 'XDG_DATA_DIRS=.*[:]?/var/lib/snapd/desktop[:]?.*' < "${user}-session-env" + MATCH "PATH=.*[:]?${SNAP_MOUNT_DIR}/bin[:]?.*" < "${user}-session-env" + fi + # profile should also be correctly set up + case "$user:$SPREAD_SYSTEM" in + test-zsh:ubuntu-*|test-zsh:debian-*) + # due to https://bugs.launchpad.net/ubuntu/+source/zsh/+bug/1640514 + not MATCH 'XDG_DATA_DIRS=.*[:]?/var/lib/snapd/desktop[:]?.*' < "${user}-profile-env" + not MATCH "PATH=.*[:]?${SNAP_MOUNT_DIR}/bin[:]?.*" < "${user}-profile-env" + ;; + *) + MATCH 'XDG_DATA_DIRS=.*[:]?/var/lib/snapd/desktop[:]?.*' < "${user}-profile-env" + MATCH "PATH=.*[:]?${SNAP_MOUNT_DIR}/bin[:]?.*" < "${user}-profile-env" + ;; + esac + done diff --git a/tests/unit/c-unit-tests-clang/task.yaml b/tests/unit/c-unit-tests-clang/task.yaml index 6b6f306531f..ca51ff1ffa0 100644 --- a/tests/unit/c-unit-tests-clang/task.yaml +++ b/tests/unit/c-unit-tests-clang/task.yaml @@ -27,5 +27,7 @@ execute: | build_dir="$SPREAD_PATH/cmd/autogarbage" BUILD_DIR=$build_dir CC=clang ./autogen.sh cd "$build_dir" - # Build and run unit tests + # Build and run unit tests as root and as a user make check + chown -R test.test "$build_dir" + su test -c 'make check' diff --git a/tests/unit/c-unit-tests-gcc/task.yaml b/tests/unit/c-unit-tests-gcc/task.yaml index 3e5f0758cce..6534c7f07ac 100644 --- a/tests/unit/c-unit-tests-gcc/task.yaml +++ b/tests/unit/c-unit-tests-gcc/task.yaml @@ -27,5 +27,7 @@ execute: | build_dir="$SPREAD_PATH/cmd/autogarbage" BUILD_DIR=$build_dir ./autogen.sh cd "$build_dir" - # Build and run unit tests + # Build and run unit tests as root and as a user make check + chown -R test.test "$build_dir" + su test -c 'make check' diff --git a/vendor/vendor.json b/vendor/vendor.json index 62ec5dfbcf5..1e1a5b02b44 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -30,6 +30,12 @@ "revision": "d83b6ffe499a29cc05fc977988d0392851779620", "revisionTime": "2019-07-01T20:26:33Z" }, + { + "checksumSHA1": "Q2fk2rjypvQOYxSMD59/h9Et5OM=", + "path": "github.com/gvalkov/golang-evdev", + "revision": "287e62b94bcb850ab42e711bd74b2875da83af2c", + "revisionTime": "2019-11-14T12:45:02Z" + }, { "checksumSHA1": "GcYF2BhpiQkshVVXxHPS/Oq491c=", "path": "github.com/jessevdk/go-flags",