diff --git a/kernel/kernel.go b/kernel/kernel.go index d7dd00c08b2..0c5f70115cc 100644 --- a/kernel/kernel.go +++ b/kernel/kernel.go @@ -25,7 +25,9 @@ import ( "os" "path/filepath" "regexp" + "strings" + "github.com/snapcore/snapd/snap" "gopkg.in/yaml.v2" ) @@ -77,3 +79,23 @@ func ReadInfo(kernelSnapRootDir string) (*Info, error) { } return InfoFromKernelYaml(content) } + +// KernelVersionFromPlaceInfo returns the kernel version for a mounted kernel +// snap (this would be the output if "uname -r" for a running kernel). +func KernelVersionFromPlaceInfo(spi snap.PlaceInfo) (string, error) { + systemMapPathPrefix := filepath.Join(spi.MountDir(), "System.map-") + matchPath := systemMapPathPrefix + "*" + matches, err := filepath.Glob(matchPath) + if err != nil { + // could be only ErrBadPattern, should not really happen + return "", fmt.Errorf("internal error: %w", err) + } + if len(matches) != 1 { + return "", fmt.Errorf("unexpected number of matches (%d) for glob %s", len(matches), matchPath) + } + version := strings.TrimPrefix(matches[0], systemMapPathPrefix) + if version == "" { + return "", fmt.Errorf("kernel version not set in %s", matches[0]) + } + return version, nil +} diff --git a/kernel/kernel_test.go b/kernel/kernel_test.go index fa8a7056db7..f9a804b3f62 100644 --- a/kernel/kernel_test.go +++ b/kernel/kernel_test.go @@ -26,7 +26,10 @@ import ( . "gopkg.in/check.v1" + "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/kernel" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/testutil" ) func makeMockKernel(c *C, kernelYaml string, filesWithContent map[string]string) string { @@ -47,9 +50,18 @@ func makeMockKernel(c *C, kernelYaml string, filesWithContent map[string]string) return kernelRootDir } -type kernelYamlTestSuite struct{} +type kernelTestSuite struct { + testutil.BaseTest +} + +var _ = Suite(&kernelTestSuite{}) -var _ = Suite(&kernelYamlTestSuite{}) +func (s *kernelTestSuite) SetUpTest(c *C) { + s.BaseTest.SetUpTest(c) + + dirs.SetRootDir(c.MkDir()) + s.AddCleanup(func() { dirs.SetRootDir("") }) +} func TestCommand(t *testing.T) { TestingT(t) } @@ -67,19 +79,19 @@ assets: non-#alphanumeric: `) -func (s *kernelYamlTestSuite) TestInfoFromKernelYamlSad(c *C) { +func (s *kernelTestSuite) TestInfoFromKernelYamlSad(c *C) { ki, err := kernel.InfoFromKernelYaml([]byte("foo")) c.Check(err, ErrorMatches, "(?m)cannot parse kernel metadata: .*") c.Check(ki, IsNil) } -func (s *kernelYamlTestSuite) TestInfoFromKernelYamlBadName(c *C) { +func (s *kernelTestSuite) TestInfoFromKernelYamlBadName(c *C) { ki, err := kernel.InfoFromKernelYaml(mockInvalidKernelYaml) c.Check(err, ErrorMatches, `invalid asset name "non-#alphanumeric", please use only alphanumeric characters and dashes`) c.Check(ki, IsNil) } -func (s *kernelYamlTestSuite) TestInfoFromKernelYamlHappy(c *C) { +func (s *kernelTestSuite) TestInfoFromKernelYamlHappy(c *C) { ki, err := kernel.InfoFromKernelYaml(mockKernelYaml) c.Check(err, IsNil) c.Check(ki, DeepEquals, &kernel.Info{ @@ -95,13 +107,13 @@ func (s *kernelYamlTestSuite) TestInfoFromKernelYamlHappy(c *C) { }) } -func (s *kernelYamlTestSuite) TestReadKernelYamlOptional(c *C) { +func (s *kernelTestSuite) TestReadKernelYamlOptional(c *C) { ki, err := kernel.ReadInfo("this-path-does-not-exist") c.Check(err, IsNil) c.Check(ki, DeepEquals, &kernel.Info{}) } -func (s *kernelYamlTestSuite) TestReadKernelYamlSad(c *C) { +func (s *kernelTestSuite) TestReadKernelYamlSad(c *C) { mockKernelSnapRoot := c.MkDir() kernelYamlPath := filepath.Join(mockKernelSnapRoot, "meta/kernel.yaml") err := os.MkdirAll(filepath.Dir(kernelYamlPath), 0755) @@ -114,7 +126,7 @@ func (s *kernelYamlTestSuite) TestReadKernelYamlSad(c *C) { c.Check(ki, IsNil) } -func (s *kernelYamlTestSuite) TestReadKernelYamlHappy(c *C) { +func (s *kernelTestSuite) TestReadKernelYamlHappy(c *C) { mockKernelSnapRoot := c.MkDir() kernelYamlPath := filepath.Join(mockKernelSnapRoot, "meta/kernel.yaml") err := os.MkdirAll(filepath.Dir(kernelYamlPath), 0755) @@ -136,3 +148,41 @@ func (s *kernelYamlTestSuite) TestReadKernelYamlHappy(c *C) { }, }) } + +func (s *kernelTestSuite) TestKernelVersionFromPlaceInfo(c *C) { + spi := snap.MinimalPlaceInfo("pc-kernel", snap.R(1)) + + c.Assert(os.MkdirAll(spi.MountDir(), 0755), IsNil) + + // No map file + ver, err := kernel.KernelVersionFromPlaceInfo(spi) + c.Check(err, ErrorMatches, `unexpected number of matches \(0\) for glob .*`) + c.Check(ver, Equals, "") + + // Create file so kernel version can be found + c.Assert(os.WriteFile(filepath.Join( + spi.MountDir(), "System.map-5.15.0-78-generic"), []byte{}, 0644), IsNil) + ver, err = kernel.KernelVersionFromPlaceInfo(spi) + c.Check(err, IsNil) + c.Check(ver, Equals, "5.15.0-78-generic") + + // Too many matches + c.Assert(os.WriteFile(filepath.Join( + spi.MountDir(), "System.map-6.8.0-71-generic"), []byte{}, 0644), IsNil) + ver, err = kernel.KernelVersionFromPlaceInfo(spi) + c.Check(err, ErrorMatches, `unexpected number of matches \(2\) for glob .*`) + c.Check(ver, Equals, "") +} + +func (s *kernelTestSuite) TestKernelVersionFromPlaceInfoNotSetInFile(c *C) { + spi := snap.MinimalPlaceInfo("pc-kernel", snap.R(1)) + + c.Assert(os.MkdirAll(spi.MountDir(), 0755), IsNil) + + // Create bad file name + c.Assert(os.WriteFile(filepath.Join( + spi.MountDir(), "System.map-"), []byte{}, 0644), IsNil) + ver, err := kernel.KernelVersionFromPlaceInfo(spi) + c.Check(err, ErrorMatches, `kernel version not set in .*System\.map\-`) + c.Check(ver, Equals, "") +} diff --git a/overlord/snapstate/backend.go b/overlord/snapstate/backend.go index be9b3a2f4ce..39431b8c218 100644 --- a/overlord/snapstate/backend.go +++ b/overlord/snapstate/backend.go @@ -80,6 +80,8 @@ type managerBackend interface { StartServices(svcs []*snap.AppInfo, disabledSvcs []string, meter progress.Meter, tm timings.Measurer) error StopServices(svcs []*snap.AppInfo, reason snap.ServiceStopReason, meter progress.Meter, tm timings.Measurer) error QueryDisabledServices(info *snap.Info, pb progress.Meter) ([]string, error) + SetupKernelModulesComponent(cpi snap.ContainerPlaceInfo, cref naming.ComponentRef, kernelVersion string, meter progress.Meter) (err error) + UndoSetupKernelModulesComponent(cpi snap.ContainerPlaceInfo, cref naming.ComponentRef, kernelVersion string, meter progress.Meter) error // the undoers for install UndoSetupSnap(s snap.PlaceInfo, typ snap.Type, installRecord *backend.InstallRecord, dev snap.Device, meter progress.Meter) error diff --git a/overlord/snapstate/backend/export_test.go b/overlord/snapstate/backend/export_test.go index 1488cddfc2e..152e25074cd 100644 --- a/overlord/snapstate/backend/export_test.go +++ b/overlord/snapstate/backend/export_test.go @@ -73,3 +73,11 @@ func MockMkdirAllChown(f func(string, os.FileMode, sys.UserID, sys.GroupID) erro mkdirAllChown = old } } + +func MockRunDepmod(f func(baseDir, kernelVersion string) error) func() { + old := runDepmod + runDepmod = f + return func() { + runDepmod = old + } +} diff --git a/overlord/snapstate/backend/kernel_modules_components.go b/overlord/snapstate/backend/kernel_modules_components.go new file mode 100644 index 00000000000..d77ee05c3a0 --- /dev/null +++ b/overlord/snapstate/backend/kernel_modules_components.go @@ -0,0 +1,219 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2024 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 backend + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/progress" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/naming" + "github.com/snapcore/snapd/systemd" +) + +type NoKernelDriversError struct { + cref naming.ComponentRef + kernelVersion string +} + +func (e NoKernelDriversError) Error() string { + return fmt.Sprintf("%s does not contain firmware or components for %s", + e.cref, e.kernelVersion) +} + +func cleanupMount(mountDir string, meter progress.Meter) error { + mountDir = filepath.Join(dirs.GlobalRootDir, mountDir) + // this also ensures that the mount unit stops + if err := removeMountUnit(mountDir, meter); err != nil { + return err + } + + if err := os.RemoveAll(mountDir); err != nil { + return err + } + + return nil +} + +type kernelModulesCleanupParts struct { + compMountDir string + modulesMountDir string + rerunDepmod bool +} + +func cleanupKernelModulesSetup(parts *kernelModulesCleanupParts, kernelVersion string, meter progress.Meter) error { + if parts.modulesMountDir != "" { + if err := cleanupMount(parts.modulesMountDir, meter); err != nil { + return err + } + if parts.rerunDepmod { + if err := runDepmod("/usr", kernelVersion); err != nil { + return err + } + } + } + + if parts.compMountDir != "" { + if err := cleanupMount(parts.compMountDir, meter); err != nil { + return err + } + } + + return nil +} + +func checkKernelModulesCompContent(mountDir, kernelVersion string) (bool, bool) { + hasModules := osutil.IsDirectory(filepath.Join(mountDir, "modules", kernelVersion)) + hasFirmware := osutil.IsDirectory(filepath.Join(mountDir, "firmware")) + return hasModules, hasFirmware +} + +func componentMountPoint(componentName, kernelVersion string) string { + return filepath.Join("/run/mnt/kernel-modules/", kernelVersion, componentName) +} + +func modulesMountPoint(componentName, kernelVersion string) string { + return filepath.Join("/usr/lib/modules", kernelVersion, "updates", componentName) +} + +// SetupKernelModulesComponent creates and starts mount units for +// kernel-modules components. +func (b Backend) SetupKernelModulesComponent(cpi snap.ContainerPlaceInfo, cref naming.ComponentRef, kernelVersion string, meter progress.Meter) (err error) { + var sysd systemd.Systemd + if b.preseed { + sysd = systemd.NewEmulationMode(dirs.GlobalRootDir) + } else { + sysd = systemd.New(systemd.SystemMode, meter) + } + + // Restore state if something goes wrong + var cleanOnFailure kernelModulesCleanupParts + defer func() { + if err == nil { + return + } + if err := cleanupKernelModulesSetup(&cleanOnFailure, kernelVersion, meter); err != nil { + logger.Noticef("while cleaning up a failed kernel-modules set-up: %v", err) + } + }() + + // Check that the kernel-modules component really has + // something we must mount on early boot. + hasModules, hasFirmware := checkKernelModulesCompContent(cpi.MountDir(), kernelVersion) + if !hasModules && !hasFirmware { + return &NoKernelDriversError{cref: cref, kernelVersion: kernelVersion} + } + + // Mount the component itself (we need it early, so the mount in /snap cannot + // be used). + componentMount := componentMountPoint(cref.ComponentName, kernelVersion) + addMountUnitOptions := &systemd.MountUnitOptions{ + MountUnitType: systemd.BeforeDriversLoadMountUnit, + Lifetime: systemd.Persistent, + Description: fmt.Sprintf("Mount unit for kernel-modules component %s", cref), + What: cpi.MountFile(), + Where: componentMount, + Fstype: "squashfs", + Options: []string{"nodev,ro,x-gdu.hide,x-gvfs-hide"}, + } + _, err = sysd.EnsureMountUnitFileWithOptions(addMountUnitOptions) + if err != nil { + return fmt.Errorf("cannot create mount in %q: %w", componentMount, err) + } + cleanOnFailure.compMountDir = componentMount + + if hasModules { + // systemd automatically works out dependencies on the "what" + // path too so this mount happens after the component one. + modulesDir := modulesMountPoint(cref.ComponentName, kernelVersion) + addMountUnitOptions = &systemd.MountUnitOptions{ + MountUnitType: systemd.BeforeDriversLoadMountUnit, + Lifetime: systemd.Persistent, + Description: fmt.Sprintf("Mount unit for modules from %s", cref.String()), + What: filepath.Join(componentMount, "modules", kernelVersion), + Where: modulesDir, + Fstype: "none", + Options: []string{"bind"}, + } + _, err = sysd.EnsureMountUnitFileWithOptions(addMountUnitOptions) + if err != nil { + return fmt.Errorf("cannot create mount in %q: %w", modulesDir, err) + } + cleanOnFailure.modulesMountDir = modulesDir + + // Rebuild modinfo files + if err := runDepmod("/usr", kernelVersion); err != nil { + return err + } + cleanOnFailure.rerunDepmod = true + } + + if hasFirmware { + // TODO create recursively symlinks in + // /usr/lib/firmware/updates while checking for conflicts with + // existing files. + } + + return nil +} + +var runDepmod = runDepmodImpl + +func runDepmodImpl(baseDir, kernelVersion string) error { + logger.Debugf("running depmod on %q for kernel %s", baseDir, kernelVersion) + stdout, stderr, err := osutil.RunSplitOutput("depmod", "-b", baseDir, kernelVersion) + logger.Debugf("depmod stderr:\n%s\n\ndepmod stdout:\n%s", + string(stderr), string(stdout)) + if err != nil { + return osutil.OutputErrCombine(stdout, stderr, err) + } + return nil +} + +// UndoSetupKernelModulesComponent undoes the work of SetupKernelModulesComponent +func (b Backend) UndoSetupKernelModulesComponent(cpi snap.ContainerPlaceInfo, cref naming.ComponentRef, kernelVersion string, meter progress.Meter) error { + hasModules, hasFirmware := checkKernelModulesCompContent(cpi.MountDir(), kernelVersion) + var partsToClean kernelModulesCleanupParts + if hasFirmware { + // TODO remove recursively symlinks in + // /usr/lib/firmware/updates (set var in kernelModulesCleanupParts) + } + + // Remove created mount units. If the component had modules we need to + // re-create the modules metainformation by running depmod so it is + // consistent with the currently available modules. + if hasModules { + partsToClean.modulesMountDir = + modulesMountPoint(cref.ComponentName, kernelVersion) + partsToClean.rerunDepmod = true + } + + if hasModules || hasFirmware { + partsToClean.compMountDir = + componentMountPoint(cref.ComponentName, kernelVersion) + } + + return cleanupKernelModulesSetup(&partsToClean, kernelVersion, meter) +} diff --git a/overlord/snapstate/backend/kernel_modules_components_test.go b/overlord/snapstate/backend/kernel_modules_components_test.go new file mode 100644 index 00000000000..156d3c4f7ff --- /dev/null +++ b/overlord/snapstate/backend/kernel_modules_components_test.go @@ -0,0 +1,200 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2024 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 backend_test + +import ( + "errors" + "os" + "path/filepath" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/overlord/snapstate/backend" + "github.com/snapcore/snapd/progress" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/naming" + "github.com/snapcore/snapd/systemd" + "github.com/snapcore/snapd/testutil" +) + +type kernelModulesSuite struct { + testutil.BaseTest + + be backend.Backend + sysctlArgs [][]string +} + +var _ = Suite(&kernelModulesSuite{}) + +func (s *kernelModulesSuite) SetUpTest(c *C) { + s.BaseTest.SetUpTest(c) + + dirs.SetRootDir(c.MkDir()) + s.AddCleanup(func() { dirs.SetRootDir("") }) + + restore := systemd.MockSystemctl(func(cmd ...string) ([]byte, error) { + s.sysctlArgs = append(s.sysctlArgs, cmd) + return []byte{}, nil + }) + s.AddCleanup(restore) + s.AddCleanup(func() { s.sysctlArgs = nil }) + + s.AddCleanup(backend.MockRunDepmod(func(baseDir, kernelVersion string) error { + return nil + })) + + s.AddCleanup(osutil.MockMountInfo("")) +} + +func (s *kernelModulesSuite) TestSetupKernelModulesComponentNoModules(c *C) { + const snapName = "mysnap" + const compName = "mycomp" + const kernelVersion = "5.15.0-78-generic" + + cpi := snap.MinimalComponentContainerPlaceInfo(compName, snap.R(33), snapName, snap.R(1)) + cref := naming.NewComponentRef(snapName, compName) + + err := s.be.SetupKernelModulesComponent(cpi, cref, kernelVersion, progress.Null) + c.Assert(err.Error(), Equals, + "mysnap+mycomp does not contain firmware or components for 5.15.0-78-generic") + _, ok := err.(*backend.NoKernelDriversError) + c.Assert(ok, Equals, true) +} + +func (s *kernelModulesSuite) TestSetupKernelModulesComponentWithModules(c *C) { + const snapName = "mysnap" + const compName = "mycomp" + const kernelVersion = "5.15.0-78-generic" + + cpi := snap.MinimalComponentContainerPlaceInfo(compName, snap.R(33), snapName, snap.R(1)) + cref := naming.NewComponentRef(snapName, compName) + // Make directories so the method thinks there are modules + compDir := filepath.Join(dirs.SnapMountDir, snapName, "components/1", compName) + modsDir := filepath.Join(compDir, "modules", kernelVersion) + fwDir := filepath.Join(compDir, "firmware") + c.Assert(os.MkdirAll(modsDir, os.ModePerm), IsNil) + c.Assert(os.MkdirAll(fwDir, os.ModePerm), IsNil) + + err := s.be.SetupKernelModulesComponent(cpi, cref, kernelVersion, progress.Null) + c.Assert(err, IsNil) + kmodMountUnit := "run-mnt-kernel\\x2dmodules-5.15.0\\x2d78\\x2dgeneric-mycomp.mount" + ktreeMountUnit := "usr-lib-modules-5.15.0\\x2d78\\x2dgeneric-updates-mycomp.mount" + c.Assert(s.sysctlArgs, DeepEquals, [][]string{ + {"daemon-reload"}, + {"--no-reload", "enable", kmodMountUnit}, + {"reload-or-restart", kmodMountUnit}, + {"daemon-reload"}, + {"--no-reload", "enable", ktreeMountUnit}, + {"reload-or-restart", ktreeMountUnit}, + }) + // Check mount files exist + c.Assert(osutil.FileExists(filepath.Join(dirs.SnapServicesDir, kmodMountUnit)), Equals, true) + c.Assert(osutil.FileExists(filepath.Join(dirs.SnapServicesDir, ktreeMountUnit)), Equals, true) + + // Now do a clean-up + s.sysctlArgs = nil + err = s.be.UndoSetupKernelModulesComponent(cpi, cref, kernelVersion, progress.Null) + c.Assert(err, IsNil) + c.Assert(s.sysctlArgs, DeepEquals, [][]string{ + {"--no-reload", "disable", ktreeMountUnit}, + {"daemon-reload"}, + {"--no-reload", "disable", kmodMountUnit}, + {"daemon-reload"}, + }) + // Check mount files do not exist + c.Assert(osutil.FileExists(filepath.Join(dirs.SnapServicesDir, kmodMountUnit)), Equals, false) + c.Assert(osutil.FileExists(filepath.Join(dirs.SnapServicesDir, ktreeMountUnit)), Equals, false) +} + +func (s *kernelModulesSuite) TestSetupKernelModulesComponentJustFirmware(c *C) { + const snapName = "mysnap" + const compName = "mycomp" + const kernelVersion = "5.15.0-78-generic" + + cpi := snap.MinimalComponentContainerPlaceInfo(compName, snap.R(33), snapName, snap.R(1)) + cref := naming.NewComponentRef(snapName, compName) + // Make directories so the method thinks there are modules + compDir := filepath.Join(dirs.SnapMountDir, snapName, "components/1", compName) + fwDir := filepath.Join(compDir, "firmware") + c.Assert(os.MkdirAll(fwDir, os.ModePerm), IsNil) + + err := s.be.SetupKernelModulesComponent(cpi, cref, kernelVersion, progress.Null) + c.Assert(err, IsNil) + kmodMountUnit := "run-mnt-kernel\\x2dmodules-5.15.0\\x2d78\\x2dgeneric-mycomp.mount" + c.Assert(s.sysctlArgs, DeepEquals, [][]string{ + {"daemon-reload"}, + {"--no-reload", "enable", kmodMountUnit}, + {"reload-or-restart", kmodMountUnit}, + }) + // Check mount files exist + c.Assert(osutil.FileExists(filepath.Join(dirs.SnapServicesDir, kmodMountUnit)), Equals, true) + + // Now do a clean-up + s.sysctlArgs = nil + err = s.be.UndoSetupKernelModulesComponent(cpi, cref, kernelVersion, progress.Null) + c.Assert(err, IsNil) + c.Assert(s.sysctlArgs, DeepEquals, [][]string{ + {"--no-reload", "disable", kmodMountUnit}, + {"daemon-reload"}, + }) + // Check mount files do not exist + c.Assert(osutil.FileExists(filepath.Join(dirs.SnapServicesDir, kmodMountUnit)), Equals, false) +} + +func (s *kernelModulesSuite) TestSetupKernelModulesComponentDepmodFailed(c *C) { + const snapName = "mysnap" + const compName = "mycomp" + const kernelVersion = "5.15.0-78-generic" + + cpi := snap.MinimalComponentContainerPlaceInfo(compName, snap.R(33), snapName, snap.R(1)) + cref := naming.NewComponentRef(snapName, compName) + // Make directories so the method thinks there are modules + compDir := filepath.Join(dirs.SnapMountDir, snapName, "components/1", compName) + modsDir := filepath.Join(compDir, "modules", kernelVersion) + fwDir := filepath.Join(compDir, "firmware") + c.Assert(os.MkdirAll(modsDir, os.ModePerm), IsNil) + c.Assert(os.MkdirAll(fwDir, os.ModePerm), IsNil) + // make depmod fail + s.AddCleanup(backend.MockRunDepmod(func(baseDir, kernelVersion string) error { + return errors.New("depmod failure") + })) + + err := s.be.SetupKernelModulesComponent(cpi, cref, kernelVersion, progress.Null) + c.Assert(err.Error(), Equals, "depmod failure") + kmodMountUnit := "run-mnt-kernel\\x2dmodules-5.15.0\\x2d78\\x2dgeneric-mycomp.mount" + ktreeMountUnit := "usr-lib-modules-5.15.0\\x2d78\\x2dgeneric-updates-mycomp.mount" + c.Assert(s.sysctlArgs, DeepEquals, [][]string{ + {"daemon-reload"}, + {"--no-reload", "enable", kmodMountUnit}, + {"reload-or-restart", kmodMountUnit}, + {"daemon-reload"}, + {"--no-reload", "enable", ktreeMountUnit}, + {"reload-or-restart", ktreeMountUnit}, + {"--no-reload", "disable", ktreeMountUnit}, + {"daemon-reload"}, + {"--no-reload", "disable", kmodMountUnit}, + {"daemon-reload"}, + }) + // Check mount files do not exist + c.Assert(osutil.FileExists(filepath.Join(dirs.SnapServicesDir, kmodMountUnit)), Equals, false) + c.Assert(osutil.FileExists(filepath.Join(dirs.SnapServicesDir, ktreeMountUnit)), Equals, false) +} diff --git a/overlord/snapstate/backend_test.go b/overlord/snapstate/backend_test.go index 67e9348e746..20391d2a8f8 100644 --- a/overlord/snapstate/backend_test.go +++ b/overlord/snapstate/backend_test.go @@ -43,6 +43,7 @@ import ( "github.com/snapcore/snapd/progress" "github.com/snapcore/snapd/randutil" "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/naming" "github.com/snapcore/snapd/snap/snapfile" "github.com/snapcore/snapd/store" "github.com/snapcore/snapd/store/storetest" @@ -84,6 +85,13 @@ type fakeOp struct { dirOpts *dirs.SnapDirOptions undoInfo *backend.UndoInfo + + // Used for component related interface calls + compFilePath string + compMountDir string + compMountFile string + cref naming.ComponentRef + kernelVersion string } type fakeOps []fakeOp @@ -904,7 +912,10 @@ func (f *fakeSnappyBackend) SetupSnap(snapFilePath, instanceName string, si *sna func (f *fakeSnappyBackend) SetupComponent(compFilePath string, compPi snap.ContainerPlaceInfo, dev snap.Device, meter progress.Meter) (installRecord *backend.InstallRecord, err error) { meter.Notify("setup-component") f.appendOp(&fakeOp{ - op: "setup-component", + op: "setup-component", + compFilePath: compFilePath, + compMountDir: compPi.MountDir(), + compMountFile: compPi.MountFile(), }) if strings.HasSuffix(compPi.ContainerName(), "+broken") { return nil, fmt.Errorf("cannot set-up component %q", compPi.ContainerName()) @@ -912,10 +923,36 @@ func (f *fakeSnappyBackend) SetupComponent(compFilePath string, compPi snap.Cont return &backend.InstallRecord{}, nil } +func (f *fakeSnappyBackend) SetupKernelModulesComponent(cpi snap.ContainerPlaceInfo, cref naming.ComponentRef, kernelVersion string, meter progress.Meter) (err error) { + meter.Notify("setup-kernel-modules-component") + f.appendOp(&fakeOp{ + op: "setup-kernel-modules-component", + compMountDir: cpi.MountDir(), + compMountFile: cpi.MountFile(), + cref: cref, + kernelVersion: kernelVersion, + }) + return nil +} + +func (f *fakeSnappyBackend) UndoSetupKernelModulesComponent(cpi snap.ContainerPlaceInfo, cref naming.ComponentRef, kernelVersion string, meter progress.Meter) error { + meter.Notify("undo-setup-kernel-modules-component") + f.appendOp(&fakeOp{ + op: "undo-setup-kernel-modules-component", + compMountDir: cpi.MountDir(), + compMountFile: cpi.MountFile(), + cref: cref, + kernelVersion: kernelVersion, + }) + return nil +} + func (f *fakeSnappyBackend) UndoSetupComponent(cpi snap.ContainerPlaceInfo, installRecord *backend.InstallRecord, dev snap.Device, meter progress.Meter) error { meter.Notify("undo-setup-component") f.appendOp(&fakeOp{ - op: "undo-setup-component", + op: "undo-setup-component", + compMountDir: cpi.MountDir(), + compMountFile: cpi.MountFile(), }) if strings.HasSuffix(cpi.ContainerName(), "+brokenundo") { return fmt.Errorf("cannot undo set-up of component %q", cpi.ContainerName()) @@ -925,7 +962,9 @@ func (f *fakeSnappyBackend) UndoSetupComponent(cpi snap.ContainerPlaceInfo, inst func (f *fakeSnappyBackend) RemoveComponentDir(cpi snap.ContainerPlaceInfo) error { f.appendOp(&fakeOp{ - op: "remove-component-dir", + op: "remove-component-dir", + compMountDir: cpi.MountDir(), + compMountFile: cpi.MountFile(), }) return nil } diff --git a/overlord/snapstate/component.go b/overlord/snapstate/component.go index d7a8c9cd85d..cf16270bd0d 100644 --- a/overlord/snapstate/component.go +++ b/overlord/snapstate/component.go @@ -168,6 +168,13 @@ func doInstallComponent(st *state.State, snapst *SnapState, compSetup *Component } } + if compSetup.CompType == snap.KernelModulesComponent { + setupKernMod := st.NewTask("setup-kernel-modules-component", + fmt.Sprintf(i18n.G("Set-up kernel-modules component %q%s"), + compSi.Component, revisionStr)) + addTask(setupKernMod) + } + // TODO hooks for components // We might be replacing a component if a local install, otherwise diff --git a/overlord/snapstate/component_install_test.go b/overlord/snapstate/component_install_test.go index 1f493fb42ac..c439edc62b9 100644 --- a/overlord/snapstate/component_install_test.go +++ b/overlord/snapstate/component_install_test.go @@ -41,6 +41,8 @@ const ( compOptRevisionPresent // Component revision is used by the currently active snap revision compOptIsActive + // The component is of kernel-modules type + compOptKernelModules ) // opts is a bitset with compOpt* as possible values. @@ -56,6 +58,10 @@ func expectedComponentInstallTasks(opts int) []string { if opts&compOptRevisionPresent == 0 { startTasks = append(startTasks, "mount-component") } + // This task is only for kernel-modules + if opts&compOptKernelModules != 0 { + startTasks = append(startTasks, "setup-kernel-modules-component") + } // Component is installed (implicit if compOptRevisionPresent is set) if opts&compOptIsActive != 0 { startTasks = append(startTasks, "unlink-current-component") @@ -103,11 +109,11 @@ func verifyComponentInstallTasks(c *C, opts int, ts *state.TaskSet) { } } -func createTestComponent(c *C, snapName, compName string) (*snap.ComponentInfo, string) { +func createComponent(c *C, snapName, compName string, compType snap.ComponentType) (*snap.ComponentInfo, string) { componentYaml := fmt.Sprintf(`component: %s+%s -type: test +type: %s version: 1.0 -`, snapName, compName) +`, snapName, compName, compType) compPath := snaptest.MakeTestComponent(c, componentYaml) compf, err := snapfile.Open(compPath) c.Assert(err, IsNil) @@ -118,14 +124,18 @@ version: 1.0 return ci, compPath } -func createTestSnapInfoForComponent(c *C, snapName string, snapRev snap.Revision, compName string) *snap.Info { +func createTestComponent(c *C, snapName, compName string) (*snap.ComponentInfo, string) { + return createComponent(c, snapName, compName, snap.TestComponent) +} + +func createSnapInfoForComponent(c *C, snapName string, snapRev snap.Revision, compName string, compType snap.ComponentType) *snap.Info { snapYaml := fmt.Sprintf(`name: %s type: app version: 1.1 components: %s: - type: test -`, snapName, compName) + type: %s +`, snapName, compName, compType) info, err := snap.InfoFromSnapYaml([]byte(snapYaml)) c.Assert(err, IsNil) info.SideInfo = snap.SideInfo{RealName: snapName, Revision: snapRev} @@ -133,6 +143,10 @@ components: return info } +func createTestSnapInfoForComponent(c *C, snapName string, snapRev snap.Revision, compName string) *snap.Info { + return createSnapInfoForComponent(c, snapName, snapRev, compName, snap.TestComponent) +} + func createTestSnapSetup(info *snap.Info, flags snapstate.Flags) *snapstate.SnapSetup { return &snapstate.SnapSetup{ Base: info.Base, @@ -202,6 +216,30 @@ func (s *snapmgrTestSuite) TestInstallComponentPath(c *C) { c.Assert(osutil.FileExists(compPath), Equals, true) } +func (s *snapmgrTestSuite) TestInstallKernelModulesComponentPath(c *C) { + const snapName = "mysnap" + const compName = "mycomp" + snapRev := snap.R(1) + _, compPath := createComponent(c, snapName, compName, snap.KernelModulesComponent) + info := createSnapInfoForComponent(c, snapName, snapRev, compName, snap.KernelModulesComponent) + + s.state.Lock() + defer s.state.Unlock() + + setStateWithOneSnap(s.state, snapName, snapRev) + + csi := snap.NewComponentSideInfo(naming.ComponentRef{ + SnapName: snapName, ComponentName: compName}, snap.R(33)) + ts, err := snapstate.InstallComponentPath(s.state, csi, info, compPath, + snapstate.Flags{}) + c.Assert(err, IsNil) + + verifyComponentInstallTasks(c, compOptIsLocal|compOptKernelModules, ts) + c.Assert(s.state.TaskCount(), Equals, len(ts.Tasks())) + // File is not deleted + c.Assert(osutil.FileExists(compPath), Equals, true) +} + func (s *snapmgrTestSuite) TestInstallComponentPathWrongComponent(c *C) { const snapName = "mysnap" const compName = "mycomp" @@ -336,6 +374,33 @@ func (s *snapmgrTestSuite) TestInstallComponentPathCompRevisionPresent(c *C) { c.Assert(osutil.FileExists(compPath), Equals, false) } +func (s *snapmgrTestSuite) TestInstallKernelModulesComponentPathCompPresent(c *C) { + const snapName = "mysnap" + const compName = "mycomp" + snapRev := snap.R(1) + currentCompRev := snap.R(5) + compRev := snap.R(7) + _, compPath := createComponent(c, snapName, compName, snap.KernelModulesComponent) + info := createSnapInfoForComponent(c, snapName, snapRev, compName, snap.KernelModulesComponent) + + s.state.Lock() + defer s.state.Unlock() + + // There is already an installed component + setStateWithOneComponent(s.state, snapName, snapRev, compName, currentCompRev) + + csi := snap.NewComponentSideInfo(naming.ComponentRef{ + SnapName: snapName, ComponentName: compName}, compRev) + ts, err := snapstate.InstallComponentPath(s.state, csi, info, compPath, + snapstate.Flags{}) + c.Assert(err, IsNil) + + verifyComponentInstallTasks(c, compOptIsLocal|compOptKernelModules|compOptIsActive, ts) + c.Assert(s.state.TaskCount(), Equals, len(ts.Tasks())) + // File is not deleted + c.Assert(osutil.FileExists(compPath), Equals, true) +} + func (s *snapmgrTestSuite) TestInstallComponentPathCompRevisionPresentDiffSnapRev(c *C) { const snapName = "mysnap" const compName = "mycomp" diff --git a/overlord/snapstate/handlers_components.go b/overlord/snapstate/handlers_components.go index 44fe4f26cc6..b83a4bf5083 100644 --- a/overlord/snapstate/handlers_components.go +++ b/overlord/snapstate/handlers_components.go @@ -24,6 +24,7 @@ import ( "fmt" "time" + "github.com/snapcore/snapd/kernel" "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/overlord/snapstate/backend" "github.com/snapcore/snapd/overlord/snapstate/sequence" @@ -396,3 +397,208 @@ func (m *SnapManager) undoUnlinkCurrentComponent(t *state.Task, _ *tomb.Tomb) (e return nil } + +func (m *SnapManager) doSetupKernelModulesComponent(t *state.Task, _ *tomb.Tomb) error { + // kernel snap is expected to be mounted + st := t.State() + st.Lock() + defer st.Unlock() + perfTimings := state.TimingsForTask(t) + + compSetup, snapsup, snapSt, err := compSetupAndState(t) + if err != nil { + return err + } + + csi := compSetup.CompSideInfo + cpi := snap.MinimalComponentContainerPlaceInfo(compSetup.ComponentName(), + csi.Revision, snapsup.InstanceName(), snapsup.Revision()) + pm := NewTaskProgressAdapterUnlocked(t) + + // Find kernel version from matching kernel + spi := snap.MinimalPlaceInfo(snapsup.InstanceName(), snapsup.Revision()) + kernelVersion, err := kernel.KernelVersionFromPlaceInfo(spi) + if err != nil { + return err + } + + st.Unlock() + timings.Run(perfTimings, "setup-kernel-modules-component", + fmt.Sprintf("setup of kernel-modules component %q", csi.Component), + func(timings.Measurer) { + err = m.backend.SetupKernelModulesComponent(cpi, csi.Component, + kernelVersion, pm) + }) + st.Lock() + if err != nil { + // We need to restore the units for the previous component + currentCsi := snapSt.CurrentComponentSideInfo(csi.Component) + if currentCsi != nil { + currentCpi := snap.MinimalComponentContainerPlaceInfo( + currentCsi.Component.ComponentName, + currentCsi.Revision, snapsup.InstanceName(), snapsup.Revision()) + var restoreErr error + timings.Run(perfTimings, "setup-kernel-modules-component", + fmt.Sprintf("restore setup of kernel-modules component %q", currentCsi.Component), + func(timings.Measurer) { + restoreErr = m.backend.SetupKernelModulesComponent(currentCpi, currentCsi.Component, + kernelVersion, pm) + }) + if restoreErr != nil { + t.Logf("error while restoring previous component: %v", restoreErr) + } + } + return err + } + + // Make sure we won't be rerun + t.SetStatus(state.DoneStatus) + + return nil +} + +func (m *SnapManager) undoSetupKernelModulesComponent(t *state.Task, _ *tomb.Tomb) error { + // kernel snap is expected to be mounted + st := t.State() + st.Lock() + defer st.Unlock() + perfTimings := state.TimingsForTask(t) + + compSetup, snapsup, snapSt, err := compSetupAndState(t) + if err != nil { + return err + } + + csi := compSetup.CompSideInfo + cpi := snap.MinimalComponentContainerPlaceInfo(compSetup.ComponentName(), + csi.Revision, snapsup.InstanceName(), snapsup.Revision()) + pm := NewTaskProgressAdapterUnlocked(t) + + // Find kernel version from matching kernel + spi := snap.MinimalPlaceInfo(snapsup.InstanceName(), snapsup.Revision()) + kernelVersion, err := kernel.KernelVersionFromPlaceInfo(spi) + if err != nil { + return err + } + + // Find out if a previously installed component has been restored + // by the undo link step. + restoredCsi := snapSt.CurrentComponentSideInfo(csi.Component) + + st.Unlock() + if restoredCsi == nil { + timings.Run(perfTimings, "undo-setup-kernel-modules-component", + fmt.Sprintf("undo setup of kernel-modules component %q", csi.Component), + func(timings.Measurer) { + err = m.backend.UndoSetupKernelModulesComponent( + cpi, csi.Component, kernelVersion, pm) + }) + } else { + // Set-up will replace the mount units so they point to the + // previous component revision (note that the mount points, + // therefore the file names, are the same). + restoredCpi := snap.MinimalComponentContainerPlaceInfo( + restoredCsi.Component.ComponentName, + restoredCsi.Revision, snapsup.InstanceName(), snapsup.Revision()) + timings.Run(perfTimings, "setup-kernel-modules-component", + fmt.Sprintf("setup of kernel-modules component %q", csi.Component), + func(timings.Measurer) { + err = m.backend.SetupKernelModulesComponent( + restoredCpi, restoredCsi.Component, kernelVersion, pm) + }) + } + st.Lock() + if err != nil { + return err + } + + // Make sure we won't be rerun + t.SetStatus(state.UndoneStatus) + + return nil +} + +func (m *SnapManager) doCleanupKernelModulesComponent(t *state.Task, _ *tomb.Tomb) error { + st := t.State() + st.Lock() + defer st.Unlock() + perfTimings := state.TimingsForTask(t) + + compSetup, snapsup, snapSt, err := compSetupAndState(t) + if err != nil { + return err + } + cref := compSetup.CompSideInfo.Component + + // This is the currently active component + activeComp := snapSt.CurrentComponentSideInfo(cref) + cpi := snap.MinimalComponentContainerPlaceInfo(compSetup.ComponentName(), + activeComp.Revision, snapsup.InstanceName(), snapsup.Revision()) + pm := NewTaskProgressAdapterUnlocked(t) + + // Find kernel version from matching kernel + spi := snap.MinimalPlaceInfo(snapsup.InstanceName(), snapsup.Revision()) + kernelVersion, err := kernel.KernelVersionFromPlaceInfo(spi) + if err != nil { + return err + } + st.Unlock() + timings.Run(perfTimings, "cleanup-kernel-modules-component", + fmt.Sprintf("clean-up kernel-modules component %q", activeComp.Component), + func(timings.Measurer) { + err = m.backend.UndoSetupKernelModulesComponent(cpi, + activeComp.Component, kernelVersion, pm) + }) + st.Lock() + if err != nil { + return err + } + + // Make sure we won't be rerun + t.SetStatus(state.DoneStatus) + + return nil +} + +func (m *SnapManager) undoCleanupKernelModulesComponent(t *state.Task, _ *tomb.Tomb) error { + st := t.State() + st.Lock() + defer st.Unlock() + perfTimings := state.TimingsForTask(t) + + compSetup, snapsup, snapSt, err := compSetupAndState(t) + if err != nil { + return err + } + cref := compSetup.CompSideInfo.Component + + // This is the currently active component (already restored by undo unlink step) + activeComp := snapSt.CurrentComponentSideInfo(cref) + cpi := snap.MinimalComponentContainerPlaceInfo(compSetup.ComponentName(), + activeComp.Revision, snapsup.InstanceName(), snapsup.Revision()) + pm := NewTaskProgressAdapterUnlocked(t) + + // Find kernel version from matching kernel + spi := snap.MinimalPlaceInfo(snapsup.InstanceName(), snapsup.Revision()) + kernelVersion, err := kernel.KernelVersionFromPlaceInfo(spi) + if err != nil { + return err + } + + st.Unlock() + timings.Run(perfTimings, "undo-cleanup-kernel-modules-component", + fmt.Sprintf("undo clean-up of kernel-modules component %q", activeComp.Component), + func(timings.Measurer) { + err = m.backend.SetupKernelModulesComponent(cpi, activeComp.Component, + kernelVersion, pm) + }) + st.Lock() + if err != nil { + return err + } + + // Make sure we won't be rerun + t.SetStatus(state.UndoneStatus) + + return nil +} diff --git a/overlord/snapstate/handlers_components_mount_test.go b/overlord/snapstate/handlers_components_mount_test.go index ff7594d03dc..75154c2ac51 100644 --- a/overlord/snapstate/handlers_components_mount_test.go +++ b/overlord/snapstate/handlers_components_mount_test.go @@ -21,7 +21,9 @@ package snapstate_test import ( "fmt" + "path/filepath" + "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/overlord/snapstate" "github.com/snapcore/snapd/overlord/snapstate/snapstatetest" @@ -76,7 +78,10 @@ func (s *mountCompSnapSuite) TestDoMountComponent(c *C) { // Ensure backend calls have happened with the expected data c.Check(s.fakeBackend.ops, DeepEquals, fakeOps{ { - op: "setup-component", + op: "setup-component", + compFilePath: compPath, + compMountDir: filepath.Join(dirs.SnapMountDir, "mysnap/components/1/mycomp"), + compMountFile: filepath.Join(dirs.GlobalRootDir, "var/lib/snapd/snaps/mysnap+mycomp_7.comp"), }, }) // File not removed @@ -127,13 +132,20 @@ func (s *mountCompSnapSuite) TestDoUndoMountComponent(c *C) { // ensure undo was called the right way c.Check(s.fakeBackend.ops, DeepEquals, fakeOps{ { - op: "setup-component", + op: "setup-component", + compFilePath: compPath, + compMountDir: filepath.Join(dirs.SnapMountDir, "mysnap/components/1/mycomp"), + compMountFile: filepath.Join(dirs.GlobalRootDir, "var/lib/snapd/snaps/mysnap+mycomp_7.comp"), }, { - op: "undo-setup-component", + op: "undo-setup-component", + compMountDir: filepath.Join(dirs.SnapMountDir, "mysnap/components/1/mycomp"), + compMountFile: filepath.Join(dirs.GlobalRootDir, "var/lib/snapd/snaps/mysnap+mycomp_7.comp"), }, { - op: "remove-component-dir", + op: "remove-component-dir", + compMountDir: filepath.Join(dirs.SnapMountDir, "mysnap/components/1/mycomp"), + compMountFile: filepath.Join(dirs.GlobalRootDir, "var/lib/snapd/snaps/mysnap+mycomp_7.comp"), }, }) } @@ -177,10 +189,15 @@ func (s *mountCompSnapSuite) TestDoMountComponentSetupFails(c *C) { // ensure undo was called the right way c.Check(s.fakeBackend.ops, DeepEquals, fakeOps{ { - op: "setup-component", + op: "setup-component", + compFilePath: compPath, + compMountDir: filepath.Join(dirs.SnapMountDir, "mysnap/components/1/broken"), + compMountFile: filepath.Join(dirs.GlobalRootDir, "var/lib/snapd/snaps/mysnap+broken_7.comp"), }, { - op: "remove-component-dir", + op: "remove-component-dir", + compMountDir: filepath.Join(dirs.SnapMountDir, "mysnap/components/1/broken"), + compMountFile: filepath.Join(dirs.GlobalRootDir, "var/lib/snapd/snaps/mysnap+broken_7.comp"), }, }) } @@ -231,10 +248,15 @@ func (s *mountCompSnapSuite) TestDoUndoMountComponentFails(c *C) { // ensure undo was called the right way c.Check(s.fakeBackend.ops, DeepEquals, fakeOps{ { - op: "setup-component", + op: "setup-component", + compFilePath: compPath, + compMountDir: filepath.Join(dirs.SnapMountDir, "mysnap/components/1/brokenundo"), + compMountFile: filepath.Join(dirs.GlobalRootDir, "var/lib/snapd/snaps/mysnap+brokenundo_7.comp"), }, { - op: "undo-setup-component", + op: "undo-setup-component", + compMountDir: filepath.Join(dirs.SnapMountDir, "mysnap/components/1/brokenundo"), + compMountFile: filepath.Join(dirs.GlobalRootDir, "var/lib/snapd/snaps/mysnap+brokenundo_7.comp"), }, }) } @@ -278,13 +300,20 @@ func (s *mountCompSnapSuite) TestDoMountComponentMountFails(c *C) { // ensure undo was called the right way c.Check(s.fakeBackend.ops, DeepEquals, fakeOps{ { - op: "setup-component", + op: "setup-component", + compFilePath: compPath, + compMountDir: filepath.Join(dirs.SnapMountDir, "mysnap/components/1/mycomp"), + compMountFile: filepath.Join(dirs.GlobalRootDir, "var/lib/snapd/snaps/mysnap+mycomp_7.comp"), }, { - op: "undo-setup-component", + op: "undo-setup-component", + compMountDir: filepath.Join(dirs.SnapMountDir, "mysnap/components/1/mycomp"), + compMountFile: filepath.Join(dirs.GlobalRootDir, "var/lib/snapd/snaps/mysnap+mycomp_7.comp"), }, { - op: "remove-component-dir", + op: "remove-component-dir", + compMountDir: filepath.Join(dirs.SnapMountDir, "mysnap/components/1/mycomp"), + compMountFile: filepath.Join(dirs.GlobalRootDir, "var/lib/snapd/snaps/mysnap+mycomp_7.comp"), }, }) } diff --git a/overlord/snapstate/handlers_kernel_modules_components_test.go b/overlord/snapstate/handlers_kernel_modules_components_test.go new file mode 100644 index 00000000000..6482ceb73c5 --- /dev/null +++ b/overlord/snapstate/handlers_kernel_modules_components_test.go @@ -0,0 +1,401 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2024 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 snapstate_test + +import ( + "os" + "path/filepath" + "time" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/overlord/snapstate" + "github.com/snapcore/snapd/overlord/snapstate/snapstatetest" + "github.com/snapcore/snapd/overlord/state" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/naming" +) + +type kernelModulesCompSnapSuite struct { + baseHandlerSuite +} + +var _ = Suite(&kernelModulesCompSnapSuite{}) + +func (s *kernelModulesCompSnapSuite) SetUpTest(c *C) { + s.baseHandlerSuite.SetUpTest(c) + s.AddCleanup(snapstatetest.MockDeviceModel(DefaultModel())) + + var err error + taskRunTime, err := time.Parse(time.RFC3339, "2024-01-01T00:00:00Z") + c.Assert(err, IsNil) + s.AddCleanup(snapstate.MockTimeNow(func() time.Time { + return taskRunTime + })) +} + +func (s *kernelModulesCompSnapSuite) TestDoSetupKernelModulesComponent(c *C) { + const snapName = "mysnap" + const compName = "mycomp" + snapRev := snap.R(1) + compRev := snap.R(7) + ci, compPath := createTestComponent(c, snapName, compName) + si := createTestSnapInfoForComponent(c, snapName, snapRev, compName) + ssu := createTestSnapSetup(si, snapstate.Flags{}) + s.AddCleanup(snapstate.MockReadComponentInfo(func( + compMntDir string) (*snap.ComponentInfo, error) { + return ci, nil + })) + // Create file so kernel version can be found + c.Assert(os.MkdirAll(si.MountDir(), 0755), IsNil) + c.Assert(os.WriteFile(filepath.Join( + si.MountDir(), "System.map-5.15.0-78-generic"), []byte{}, 0644), IsNil) + + s.state.Lock() + + t := s.state.NewTask("setup-kernel-modules-component", "task desc") + cref := naming.NewComponentRef(snapName, compName) + csi := snap.NewComponentSideInfo(cref, compRev) + t.Set("component-setup", + snapstate.NewComponentSetup(csi, snap.TestComponent, compPath)) + t.Set("snap-setup", ssu) + chg := s.state.NewChange("test change", "change desc") + chg.AddTask(t) + + s.state.Unlock() + + s.se.Ensure() + s.se.Wait() + + s.state.Lock() + c.Check(chg.Err(), IsNil) + // State of task is as expected + c.Assert(t.Status(), Equals, state.DoneStatus) + s.state.Unlock() + + // Ensure backend calls have happened with the expected data + c.Check(s.fakeBackend.ops, DeepEquals, fakeOps{ + { + op: "setup-kernel-modules-component", + compMountDir: filepath.Join(dirs.SnapMountDir, "mysnap/components/1/mycomp"), + compMountFile: filepath.Join(dirs.GlobalRootDir, "var/lib/snapd/snaps/mysnap+mycomp_7.comp"), + cref: cref, + kernelVersion: "5.15.0-78-generic", + }, + }) +} + +func (s *kernelModulesCompSnapSuite) TestDoSetupKernelModulesComponentNoKernVersion(c *C) { + const snapName = "mysnap" + const compName = "mycomp" + snapRev := snap.R(1) + compRev := snap.R(7) + ci, compPath := createTestComponent(c, snapName, compName) + si := createTestSnapInfoForComponent(c, snapName, snapRev, compName) + ssu := createTestSnapSetup(si, snapstate.Flags{}) + s.AddCleanup(snapstate.MockReadComponentInfo(func( + compMntDir string) (*snap.ComponentInfo, error) { + return ci, nil + })) + + s.state.Lock() + + t := s.state.NewTask("setup-kernel-modules-component", "task desc") + cref := naming.NewComponentRef(snapName, compName) + csi := snap.NewComponentSideInfo(cref, compRev) + t.Set("component-setup", + snapstate.NewComponentSetup(csi, snap.TestComponent, compPath)) + t.Set("snap-setup", ssu) + chg := s.state.NewChange("test change", "change desc") + chg.AddTask(t) + + s.state.Unlock() + + s.se.Ensure() + s.se.Wait() + + s.state.Lock() + c.Check(chg.Err(), ErrorMatches, "cannot perform the following tasks:\n"+ + `\- task desc \(unexpected number of matches \(0\) for glob .*System.map-\*\)`) + c.Assert(t.Status(), Equals, state.ErrorStatus) + s.state.Unlock() + + c.Check(len(s.fakeBackend.ops), Equals, 0) +} + +func (s *kernelModulesCompSnapSuite) TestDoThenUndoSetupKernelModulesComponent(c *C) { + const snapName = "mysnap" + const compName = "mycomp" + snapRev := snap.R(1) + compRev := snap.R(7) + ci, compPath := createTestComponent(c, snapName, compName) + si := createTestSnapInfoForComponent(c, snapName, snapRev, compName) + ssu := createTestSnapSetup(si, snapstate.Flags{}) + s.AddCleanup(snapstate.MockReadComponentInfo(func( + compMntDir string) (*snap.ComponentInfo, error) { + return ci, nil + })) + // Create file so kernel version can be found + c.Assert(os.MkdirAll(si.MountDir(), 0755), IsNil) + c.Assert(os.WriteFile(filepath.Join( + si.MountDir(), "System.map-5.15.0-78-generic"), []byte{}, 0644), IsNil) + + s.state.Lock() + + t := s.state.NewTask("setup-kernel-modules-component", "task desc") + cref := naming.NewComponentRef(snapName, compName) + csi := snap.NewComponentSideInfo(cref, compRev) + t.Set("component-setup", + snapstate.NewComponentSetup(csi, snap.TestComponent, compPath)) + t.Set("snap-setup", ssu) + chg := s.state.NewChange("test change", "change desc") + chg.AddTask(t) + + terr := s.state.NewTask("error-trigger", "provoking undo setup") + terr.WaitFor(t) + chg.AddTask(terr) + + s.state.Unlock() + + for i := 0; i < 3; i++ { + s.se.Ensure() + s.se.Wait() + } + + s.state.Lock() + + c.Check(chg.Err().Error(), Equals, "cannot perform the following tasks:\n"+ + "- provoking undo setup (error out)") + // State of task is as expected + c.Check(t.Status(), Equals, state.UndoneStatus) + s.state.Unlock() + + // Ensure backend calls have happened with the expected data + c.Check(s.fakeBackend.ops, DeepEquals, fakeOps{ + { + op: "setup-kernel-modules-component", + compMountDir: filepath.Join(dirs.SnapMountDir, "mysnap/components/1/mycomp"), + compMountFile: filepath.Join(dirs.GlobalRootDir, "var/lib/snapd/snaps/mysnap+mycomp_7.comp"), + cref: cref, + kernelVersion: "5.15.0-78-generic", + }, + { + op: "undo-setup-kernel-modules-component", + compMountDir: filepath.Join(dirs.SnapMountDir, "mysnap/components/1/mycomp"), + compMountFile: filepath.Join(dirs.GlobalRootDir, "var/lib/snapd/snaps/mysnap+mycomp_7.comp"), + cref: cref, + kernelVersion: "5.15.0-78-generic", + }, + }) +} + +func (s *kernelModulesCompSnapSuite) TestDoThenUndoSetupKernelModulesComponentInstalled(c *C) { + const snapName = "mysnap" + const compName = "mycomp" + snapRev := snap.R(2) + compRev := snap.R(7) + ci, compPath := createTestComponent(c, snapName, compName) + si := createTestSnapInfoForComponent(c, snapName, snapRev, compName) + ssu := createTestSnapSetup(si, snapstate.Flags{}) + s.AddCleanup(snapstate.MockReadComponentInfo(func( + compMntDir string) (*snap.ComponentInfo, error) { + return ci, nil + })) + // Create file so kernel version can be found + c.Assert(os.MkdirAll(si.MountDir(), 0755), IsNil) + c.Assert(os.WriteFile(filepath.Join( + si.MountDir(), "System.map-5.15.0-78-generic"), []byte{}, 0644), IsNil) + + s.state.Lock() + + // state must contain the component + setStateWithOneComponent(s.state, snapName, snap.R(1), compName, compRev) + + t := s.state.NewTask("setup-kernel-modules-component", "task desc") + cref := naming.NewComponentRef(snapName, compName) + csi := snap.NewComponentSideInfo(cref, compRev) + t.Set("component-setup", + snapstate.NewComponentSetup(csi, snap.TestComponent, compPath)) + t.Set("snap-setup", ssu) + chg := s.state.NewChange("test change", "change desc") + chg.AddTask(t) + + terr := s.state.NewTask("error-trigger", "provoking undo setup") + terr.WaitFor(t) + chg.AddTask(terr) + + s.state.Unlock() + + for i := 0; i < 3; i++ { + s.se.Ensure() + s.se.Wait() + } + + s.state.Lock() + + c.Check(chg.Err().Error(), Equals, "cannot perform the following tasks:\n"+ + "- provoking undo setup (error out)") + // State of task is as expected + c.Check(t.Status(), Equals, state.UndoneStatus) + s.state.Unlock() + + // Ensure backend calls have happened with the expected data + c.Check(s.fakeBackend.ops, DeepEquals, fakeOps{ + { + op: "setup-kernel-modules-component", + compMountDir: filepath.Join(dirs.SnapMountDir, "mysnap/components/2/mycomp"), + compMountFile: filepath.Join(dirs.GlobalRootDir, "var/lib/snapd/snaps/mysnap+mycomp_7.comp"), + cref: cref, + kernelVersion: "5.15.0-78-generic", + }, + { + op: "setup-kernel-modules-component", + compMountDir: filepath.Join(dirs.SnapMountDir, "mysnap/components/2/mycomp"), + compMountFile: filepath.Join(dirs.GlobalRootDir, "var/lib/snapd/snaps/mysnap+mycomp_7.comp"), + cref: cref, + kernelVersion: "5.15.0-78-generic", + }, + }) +} + +func (s *kernelModulesCompSnapSuite) TestDoCleanupKernelModulesComponent(c *C) { + const snapName = "kernelsnap" + const compName = "kmodcomp" + snapRev := snap.R(1) + currentCompRev := snap.R(5) + compRev := snap.R(7) + + ci, _ := createTestComponent(c, snapName, compName) + si := createTestSnapInfoForComponent(c, snapName, snapRev, compName) + ssu := createTestSnapSetup(si, snapstate.Flags{}) + s.AddCleanup(snapstate.MockReadComponentInfo(func( + compMntDir string) (*snap.ComponentInfo, error) { + return ci, nil + })) + // Create file so kernel version can be found + c.Assert(os.MkdirAll(si.MountDir(), 0755), IsNil) + c.Assert(os.WriteFile(filepath.Join( + si.MountDir(), "System.map-5.15.0-78-generic"), []byte{}, 0644), IsNil) + + s.state.Lock() + + // state must contain the component + setStateWithOneComponent(s.state, snapName, snapRev, compName, currentCompRev) + + t := s.state.NewTask("cleanup-kernel-modules-component", "task desc") + cref := naming.NewComponentRef(snapName, compName) + csi := snap.NewComponentSideInfo(cref, compRev) + t.Set("component-setup", snapstate.NewComponentSetup(csi, snap.TestComponent, "")) + t.Set("snap-setup", ssu) + chg := s.state.NewChange("test change", "change desc") + chg.AddTask(t) + + s.state.Unlock() + + s.se.Ensure() + s.se.Wait() + + s.state.Lock() + c.Check(chg.Err(), IsNil) + // State of task is as expected + c.Assert(t.Status(), Equals, state.DoneStatus) + s.state.Unlock() + + // Ensure backend calls have happened with the expected data + c.Check(s.fakeBackend.ops, DeepEquals, fakeOps{ + { + op: "undo-setup-kernel-modules-component", + compMountDir: filepath.Join(dirs.SnapMountDir, "kernelsnap/components/1/kmodcomp"), + compMountFile: filepath.Join(dirs.GlobalRootDir, "var/lib/snapd/snaps/kernelsnap+kmodcomp_5.comp"), + cref: cref, + kernelVersion: "5.15.0-78-generic", + }, + }) +} + +func (s *kernelModulesCompSnapSuite) TestDoThenUndoCleanupKernelModulesComponent(c *C) { + const snapName = "kernelsnap" + const compName = "kmodcomp" + snapRev := snap.R(1) + currentCompRev := snap.R(5) + compRev := snap.R(7) + + ci, _ := createTestComponent(c, snapName, compName) + si := createTestSnapInfoForComponent(c, snapName, snapRev, compName) + ssu := createTestSnapSetup(si, snapstate.Flags{}) + s.AddCleanup(snapstate.MockReadComponentInfo(func( + compMntDir string) (*snap.ComponentInfo, error) { + return ci, nil + })) + // Create file so kernel version can be found + c.Assert(os.MkdirAll(si.MountDir(), 0755), IsNil) + c.Assert(os.WriteFile(filepath.Join( + si.MountDir(), "System.map-5.15.0-78-generic"), []byte{}, 0644), IsNil) + + s.state.Lock() + + // state must contain the component + setStateWithOneComponent(s.state, snapName, snapRev, compName, currentCompRev) + + t := s.state.NewTask("cleanup-kernel-modules-component", "task desc") + cref := naming.NewComponentRef(snapName, compName) + csi := snap.NewComponentSideInfo(cref, compRev) + t.Set("component-setup", snapstate.NewComponentSetup(csi, snap.TestComponent, "")) + t.Set("snap-setup", ssu) + chg := s.state.NewChange("test change", "change desc") + chg.AddTask(t) + + terr := s.state.NewTask("error-trigger", "provoking undo setup") + terr.WaitFor(t) + chg.AddTask(terr) + + s.state.Unlock() + + for i := 0; i < 3; i++ { + s.se.Ensure() + s.se.Wait() + } + + s.state.Lock() + c.Check(chg.Err().Error(), Equals, "cannot perform the following tasks:\n"+ + "- provoking undo setup (error out)") + // State of task is as expected + c.Check(t.Status(), Equals, state.UndoneStatus) + s.state.Unlock() + + // Ensure backend calls have happened with the expected data + c.Check(s.fakeBackend.ops, DeepEquals, fakeOps{ + { + op: "undo-setup-kernel-modules-component", + compMountDir: filepath.Join(dirs.SnapMountDir, "kernelsnap/components/1/kmodcomp"), + compMountFile: filepath.Join(dirs.GlobalRootDir, "var/lib/snapd/snaps/kernelsnap+kmodcomp_5.comp"), + cref: cref, + kernelVersion: "5.15.0-78-generic", + }, + { + op: "setup-kernel-modules-component", + compMountDir: filepath.Join(dirs.SnapMountDir, "kernelsnap/components/1/kmodcomp"), + compMountFile: filepath.Join(dirs.GlobalRootDir, "var/lib/snapd/snaps/kernelsnap+kmodcomp_5.comp"), + cref: cref, + kernelVersion: "5.15.0-78-generic", + }, + }) +} diff --git a/overlord/snapstate/snapmgr.go b/overlord/snapstate/snapmgr.go index 2cbaeea8608..2370b55f6bd 100644 --- a/overlord/snapstate/snapmgr.go +++ b/overlord/snapstate/snapmgr.go @@ -657,6 +657,8 @@ func Manager(st *state.State, runner *state.TaskRunner) (*SnapManager, error) { runner.AddHandler("mount-component", m.doMountComponent, m.undoMountComponent) runner.AddHandler("unlink-current-component", m.doUnlinkCurrentComponent, m.undoUnlinkCurrentComponent) runner.AddHandler("link-component", m.doLinkComponent, m.undoLinkComponent) + runner.AddHandler("setup-kernel-modules-component", m.doSetupKernelModulesComponent, m.undoSetupKernelModulesComponent) + runner.AddHandler("cleanup-kernel-modules-component", m.doCleanupKernelModulesComponent, m.undoCleanupKernelModulesComponent) // control serialisation runner.AddBlocked(m.blockedTask) diff --git a/snap/types.go b/snap/types.go index 1633cdcd354..9d609b9f010 100644 --- a/snap/types.go +++ b/snap/types.go @@ -188,12 +188,12 @@ type ComponentType string const ( // TestComponent is just for testing purposes. - // TODO add here new component when there is more progress on the - // components implementation. TestComponent ComponentType = "test" + // KernelModulesComponent is for components containing modules/firmware + KernelModulesComponent ComponentType = "kernel-modules" ) -var validComponentTypes = [...]ComponentType{TestComponent} +var validComponentTypes = [...]ComponentType{TestComponent, KernelModulesComponent} // Component represents a snap component. type Component struct {