feat: generate kernel module dependency tree

Run `depmod` during install/upgrades when extensions provide kernel
modules and `modules.dep` needs to be re-generated. This also allows
modules of same name from kernel to co-exist. Modules in `extras`
folder takes precedence over `in-built` ones.

Signed-off-by: Noel Georgi <git@frezbo.dev>
This commit is contained in:
Noel Georgi 2023-01-18 16:28:09 +05:30
parent 18122ae73e
commit d4b8b35de7
No known key found for this signature in database
GPG Key ID: 21A9F444075C9E36
8 changed files with 294 additions and 6 deletions

View File

@ -652,6 +652,7 @@ RUN apk add --no-cache --update --no-scripts \
bash \
cpio \
efibootmgr \
kmod \
mtools \
qemu-img \
squashfs-tools \
@ -663,6 +664,7 @@ ARG TARGETARCH
ENV TARGETARCH ${TARGETARCH}
COPY --from=install-artifacts / /
COPY --from=installer-build /installer /bin/installer
COPY --chmod=0644 hack/extra-modules.conf /etc/modules.d/10-extra-modules.conf
RUN ln -s /bin/installer /bin/talosctl
ARG TAG
ENV VERSION ${TAG}

View File

@ -20,24 +20,36 @@ import (
extinterface "github.com/siderolabs/talos/pkg/machinery/extensions"
)
// nolint:gocyclo
func (i *Installer) installExtensions() error {
extensions, err := extensions.List(constants.SystemExtensionsPath)
extensionsList, err := extensions.List(constants.SystemExtensionsPath)
if err != nil {
return fmt.Errorf("error listing extensions: %w", err)
}
if len(extensions) == 0 {
if len(extensionsList) == 0 {
return nil
}
if err = printExtensions(extensions); err != nil {
if err = printExtensions(extensionsList); err != nil {
return err
}
if err = validateExtensions(extensions); err != nil {
if err = validateExtensions(extensionsList); err != nil {
return err
}
extensionsPathWithKernelModules := findExtensionsWithKernelModules(extensionsList)
if len(extensionsPathWithKernelModules) > 0 {
kernelModuleDepExtension, genErr := extensions.GenerateKernelModuleDependencyTreeExtension(extensionsPathWithKernelModules, i.options.Arch)
if genErr != nil {
return genErr
}
extensionsList = append(extensionsList, kernelModuleDepExtension)
}
tempDir, err := os.MkdirTemp("", "ext")
if err != nil {
return err
@ -47,7 +59,7 @@ func (i *Installer) installExtensions() error {
var cfg *extinterface.Config
if cfg, err = compressExtensions(extensions, tempDir); err != nil {
if cfg, err = compressExtensions(extensionsList, tempDir); err != nil {
return err
}
@ -123,6 +135,18 @@ func compressExtensions(extensions []*extensions.Extension, tempDir string) (*ex
return cfg, nil
}
func findExtensionsWithKernelModules(extensions []*extensions.Extension) []string {
var modulesPath []string
for _, ext := range extensions {
if ext.ProvidesKernelModules() {
modulesPath = append(modulesPath, ext.KernelModuleDirectory())
}
}
return modulesPath
}
func buildContents(path string) (io.Reader, error) {
var listing bytes.Buffer

3
go.mod
View File

@ -112,6 +112,7 @@ require (
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.8.1
github.com/u-root/u-root v0.10.0
github.com/ulikunitz/xz v0.5.8
github.com/vishvananda/netlink v1.2.1-beta.2
github.com/vmware-tanzu/sonobuoy v0.56.14
github.com/vmware/govmomi v0.30.0
@ -243,6 +244,7 @@ require (
github.com/opencontainers/selinux v1.10.1 // indirect
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/pierrec/lz4/v4 v4.1.14 // indirect
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
@ -263,7 +265,6 @@ require (
github.com/spf13/viper v1.14.0 // indirect
github.com/subosito/gotenv v1.4.1 // indirect
github.com/u-root/uio v0.0.0-20221213070652-c3537552635f // indirect
github.com/ulikunitz/xz v0.5.8 // indirect
github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f // indirect
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect
github.com/xlab/treeprint v1.1.0 // indirect

2
go.sum
View File

@ -927,6 +927,8 @@ github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvI
github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
github.com/pierrec/lz4/v4 v4.1.14 h1:+fL8AQEZtz/ijeNnpduH0bROTu0O3NZAlPjQxGn8LwE=
github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pin/tftp v2.1.0+incompatible h1:Yng4J7jv6lOc6IF4XoB5mnd3P7ZrF60XQq+my3FAMus=
github.com/pin/tftp v2.1.0+incompatible/go.mod h1:xVpZOMCXTy+A5QMjEVN0Glwa1sUvaJhFXbr/aAxuxGY=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=

1
hack/extra-modules.conf Normal file
View File

@ -0,0 +1 @@
search extras built-in

View File

@ -6,6 +6,8 @@
package extensions
import (
"path/filepath"
"github.com/siderolabs/talos/pkg/machinery/extensions"
)
@ -16,3 +18,16 @@ type Extension struct {
directory string
rootfsPath string
}
func newExtension(rootfsPath, directory string) *Extension {
extension := &Extension{
rootfsPath: rootfsPath,
directory: directory,
}
if extension.directory == "" {
extension.directory = filepath.Base(rootfsPath)
}
return extension
}

View File

@ -0,0 +1,240 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Package extensions provides function to manage system extensions.
package extensions
import (
"bytes"
"errors"
"fmt"
"io"
"log"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/u-root/u-root/pkg/cpio"
"github.com/ulikunitz/xz"
"golang.org/x/sys/unix"
"gopkg.in/freddierice/go-losetup.v1"
"github.com/siderolabs/talos/internal/pkg/mount"
"github.com/siderolabs/talos/pkg/machinery/constants"
"github.com/siderolabs/talos/pkg/machinery/extensions"
)
// ProvidesKernelModules returns true if the extension provides kernel modules.
func (ext *Extension) ProvidesKernelModules() bool {
if _, err := os.Stat(filepath.Join(ext.rootfsPath, constants.DefaultKernelModulesPath)); os.IsNotExist(err) {
return false
}
return true
}
// KernelModuleDirectory returns the path to the kernel modules directory.
func (ext *Extension) KernelModuleDirectory() string {
return filepath.Join(ext.rootfsPath, constants.DefaultKernelModulesPath)
}
// GenerateKernelModuleDependencyTreeExtension generates a kernel module dependency tree extension.
// nolint:gocyclo
func GenerateKernelModuleDependencyTreeExtension(extensionsPathWithKernelModules []string, arch string) (*Extension, error) {
log.Println("preparing to run depmod to generate kernel modules dependency tree")
tempDir, err := os.MkdirTemp("", "ext-modules")
if err != nil {
return nil, err
}
defer logErr(func() error {
return os.RemoveAll(tempDir)
})
initramfsxz, err := os.Open(fmt.Sprintf(constants.InitramfsAssetPath, arch))
if err != nil {
return nil, err
}
defer logErr(func() error {
return initramfsxz.Close()
})
r, err := xz.NewReader(initramfsxz)
if err != nil {
return nil, err
}
var buff bytes.Buffer
if _, err = io.Copy(&buff, r); err != nil {
return nil, err
}
tempRootfsFile := filepath.Join(tempDir, constants.RootfsAsset)
if err = extractRootfsFromInitramfs(buff, tempRootfsFile); err != nil {
return nil, err
}
// now we are ready to mount rootfs.sqsh
// create a mount point under tempDir
rootfsMountPath := filepath.Join(tempDir, "rootfs-mnt")
// create the loopback device from the squashfs file
dev, err := losetup.Attach(tempRootfsFile, 0, true)
if err != nil {
return nil, err
}
defer logErr(func() error {
if err = dev.Detach(); err != nil {
return err
}
return dev.Remove()
})
// setup a temporary mount point for the squashfs file and mount it
m := mount.NewMountPoint(dev.Path(), rootfsMountPath, "squashfs", unix.MS_RDONLY|unix.MS_I_VERSION, "", mount.WithFlags(mount.ReadOnly|mount.Shared))
if err = m.Mount(); err != nil {
return nil, err
}
defer logErr(func() error {
return m.Unmount()
})
// create an overlayfs which contains the rootfs squashfs mount as the base
// and the extension modules as subsequent lower directories
overlays := mount.NewMountPoints()
// writable overlayfs mount inside a container required a tmpfs mount
overlays.Set("overlays-tmpfs", mount.NewMountPoint("tmpfs", constants.VarSystemOverlaysPath, "tmpfs", unix.MS_I_VERSION, ""))
// append the rootfs mount point
extensionsPathWithKernelModules = append(extensionsPathWithKernelModules, filepath.Join(rootfsMountPath, constants.DefaultKernelModulesPath))
// create the overlayfs mount point as read write
mp := mount.NewMountPoint("", strings.Join(extensionsPathWithKernelModules, ":"), "", unix.MS_I_VERSION, "", mount.WithFlags(mount.Overlay|mount.Shared))
overlays.Set("overlays-mnt", mp)
if err = mount.Mount(overlays); err != nil {
return nil, err
}
defer logErr(func() error {
return mount.Unmount(overlays)
})
log.Println("running depmod to generate kernel modules dependency tree")
if err = depmod(mp.Target()); err != nil {
return nil, err
}
// we want this temp directory to be present until the extension is compressed later on, so not removing it here
kernelModulesDependencyTreeStagingDir, err := os.MkdirTemp("", "module-deps")
if err != nil {
return nil, err
}
kernelModulesDepenencyTreeDirectory := filepath.Join(kernelModulesDependencyTreeStagingDir, constants.DefaultKernelModulesPath)
if err := os.MkdirAll(kernelModulesDepenencyTreeDirectory, 0o755); err != nil {
return nil, err
}
if err := findAndMoveKernelModulesDepFiles(kernelModulesDepenencyTreeDirectory, mp.Target()); err != nil {
return nil, err
}
kernelModulesDepTreeExtension := newExtension(kernelModulesDependencyTreeStagingDir, "modules.dep")
kernelModulesDepTreeExtension.Manifest = extensions.Manifest{
Version: constants.DefaultKernelVersion,
Metadata: extensions.Metadata{
Name: "modules.dep",
Version: constants.DefaultKernelVersion,
Author: "Talos Machinery",
Description: "Combined modules.dep for all extensions",
},
}
return kernelModulesDepTreeExtension, nil
}
func logErr(f func() error) {
// if file is already closed, ignore the error
if err := f(); !errors.Is(err, os.ErrClosed) {
log.Println(err)
}
}
func extractRootfsFromInitramfs(input bytes.Buffer, rootfsFilePath string) error {
recReader := cpio.Newc.Reader(bytes.NewReader(input.Bytes()))
if err := cpio.ForEachRecord(recReader, func(r cpio.Record) error {
if r.Name != constants.RootfsAsset {
return nil
}
reader := io.NewSectionReader(r.ReaderAt, 0, int64(r.FileSize))
f, err := os.Create(rootfsFilePath)
if err != nil {
return err
}
defer logErr(func() error {
return f.Close()
})
_, err = io.Copy(f, reader)
if err != nil {
return err
}
return f.Close()
}); err != nil {
return err
}
return nil
}
func depmod(kernelModulesPath string) error {
baseDir := strings.TrimSuffix(kernelModulesPath, constants.DefaultKernelModulesPath)
cmd := exec.Command("depmod", "--all", "--basedir", baseDir, "--config", "/etc/modules.d/10-extra-modules.conf", constants.DefaultKernelVersion)
cmd.Stderr = os.Stderr
return cmd.Run()
}
func findAndMoveKernelModulesDepFiles(dest, kernelModulesPath string) error {
fs, err := os.ReadDir(kernelModulesPath)
if err != nil {
return err
}
for _, f := range fs {
if f.IsDir() {
continue
}
if strings.HasPrefix(f.Name(), "modules.") {
fs, err := f.Info()
if err != nil {
return err
}
if err := moveFile(fs, filepath.Join(kernelModulesPath, f.Name()), filepath.Join(dest, f.Name())); err != nil {
return err
}
}
}
return nil
}

View File

@ -15,6 +15,9 @@ const (
// DefaultKernelVersion is the default Linux kernel version.
DefaultKernelVersion = "6.1.4-talos"
// DefaultKernelModulesPath is the default path to the kernel modules.
DefaultKernelModulesPath = "/lib/modules" + "/" + DefaultKernelVersion
// KernelParamConfig is the kernel parameter name for specifying the URL.
// to the config.
KernelParamConfig = "talos.config"