From b1b7a9e675d40a3086d76189f212f33427c36bc0 Mon Sep 17 00:00:00 2001 From: Mikhail Klementev Date: Sat, 13 May 2023 10:47:47 +0000 Subject: [PATCH] refactor: move kernel functions to submodule --- images.config.go | 7 - images.go | 52 +- images_test.go | 29 - kernel.go | 701 +--------------------- kernel/kernel.go | 689 +++++++++++++++++++++ kernel_linux.go => kernel/kernel_linux.go | 4 +- kernel_macos.go => kernel/kernel_macos.go | 5 +- 7 files changed, 710 insertions(+), 777 deletions(-) delete mode 100644 images.config.go delete mode 100644 images_test.go create mode 100644 kernel/kernel.go rename kernel_linux.go => kernel/kernel_linux.go (97%) rename kernel_macos.go => kernel/kernel_macos.go (79%) diff --git a/images.config.go b/images.config.go deleted file mode 100644 index eb46b7e..0000000 --- a/images.config.go +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright 2019 Mikhail Klementev. All rights reserved. -// Use of this source code is governed by a AGPLv3 license -// (or later) that can be found in the LICENSE file. - -package main - -const imagesBaseURL = "https://out-of-tree.fra1.digitaloceanspaces.com/1.0.0/" diff --git a/images.go b/images.go index ac1630e..ef96b38 100644 --- a/images.go +++ b/images.go @@ -1,4 +1,4 @@ -// Copyright 2019 Mikhail Klementev. All rights reserved. +// Copyright 2023 Mikhail Klementev. All rights reserved. // Use of this source code is governed by a AGPLv3 license // (or later) that can be found in the LICENSE file. @@ -7,17 +7,11 @@ package main import ( "errors" "fmt" - "io/ioutil" - "net/url" "os" - "os/exec" "os/user" "strings" "time" - "github.com/cavaliergopher/grab/v3" - "github.com/rs/zerolog/log" - "code.dumpstack.io/tools/out-of-tree/config" "code.dumpstack.io/tools/out-of-tree/fs" "code.dumpstack.io/tools/out-of-tree/qemu" @@ -126,47 +120,3 @@ func (cmd *ImageEditCmd) Run(g *Globals) (err error) { } return } - -func unpackTar(archive, destination string) (err error) { - // NOTE: If you're change anything in tar command please check also - // BSD tar (or if you're using macOS, do not forget to check GNU Tar) - // Also make sure that sparse files are extracting correctly - cmd := exec.Command("tar", "-Sxf", archive) - cmd.Dir = destination + "/" - - log.Debug().Msgf("%v", cmd) - - rawOutput, err := cmd.CombinedOutput() - if err != nil { - err = fmt.Errorf("%v: %s", err, rawOutput) - return - } - - return -} - -func downloadImage(path, file string) (err error) { - tmp, err := ioutil.TempDir(tempDirBase, "out-of-tree_") - if err != nil { - return - } - defer os.RemoveAll(tmp) - - fileurl, err := url.JoinPath(imagesBaseURL, file+".tar.gz") - if err != nil { - return - } - - resp, err := grab.Get(tmp, fileurl) - if err != nil { - err = fmt.Errorf("Cannot download %s. It looks like you need "+ - "to generate it manually and place it "+ - "to ~/.out-of-tree/images/. "+ - "Check documentation for additional information.", - fileurl) - return - } - - err = unpackTar(resp.Filename, path) - return -} diff --git a/images_test.go b/images_test.go deleted file mode 100644 index f86bc0f..0000000 --- a/images_test.go +++ /dev/null @@ -1,29 +0,0 @@ -package main - -import ( - "io/ioutil" - "os" - "path/filepath" - "testing" - - "code.dumpstack.io/tools/out-of-tree/fs" -) - -func TestDownloadImage(t *testing.T) { - tmp, err := ioutil.TempDir("", "out-of-tree_") - if err != nil { - return - } - defer os.RemoveAll(tmp) - - file := "out_of_tree_ubuntu_12__04.img" - - err = downloadImage(tmp, file) - if err != nil { - t.Fatal(err) - } - - if !fs.PathExists(filepath.Join(tmp, file)) { - t.Fatalf("%s does not exist", file) - } -} diff --git a/kernel.go b/kernel.go index 7efe751..3619a80 100644 --- a/kernel.go +++ b/kernel.go @@ -7,24 +7,13 @@ package main import ( "errors" "fmt" - "io/ioutil" "math" - "math/rand" - "os" - "os/exec" - "os/signal" - "os/user" - "regexp" - "runtime" - "strings" - "time" - "github.com/naoina/toml" "github.com/rs/zerolog/log" "code.dumpstack.io/tools/out-of-tree/config" "code.dumpstack.io/tools/out-of-tree/container" - "code.dumpstack.io/tools/out-of-tree/fs" + "code.dumpstack.io/tools/out-of-tree/kernel" ) type KernelCmd struct { @@ -80,12 +69,12 @@ func (cmd *KernelListRemoteCmd) Run(kernelCmd *KernelCmd, g *Globals) (err error ReleaseMask: ".*", } - _, err = genRootfsImage(container.Image{Name: km.DockerName()}, false) + _, err = kernel.GenRootfsImage(container.Image{Name: km.DockerName()}, false) if err != nil { return } - err = generateBaseDockerImage( + err = kernel.GenerateBaseDockerImage( g.Config.Docker.Registry, g.Config.Docker.Commands, km, @@ -95,7 +84,8 @@ func (cmd *KernelListRemoteCmd) Run(kernelCmd *KernelCmd, g *Globals) (err error return } - pkgs, err := matchPackages(km) + pkgs, err := kernel.MatchPackages(km) + // error check skipped on purpose for _, k := range pkgs { fmt.Println(k) @@ -115,7 +105,7 @@ func (cmd KernelAutogenCmd) Run(kernelCmd *KernelCmd, g *Globals) (err error) { } shutdown := false - setSigintHandler(&shutdown) + kernel.SetSigintHandler(&shutdown) for _, sk := range ka.SupportedKernels { if sk.DistroRelease == "" { @@ -123,7 +113,7 @@ func (cmd KernelAutogenCmd) Run(kernelCmd *KernelCmd, g *Globals) (err error) { return } - err = generateKernels(sk, + err = kernel.GenerateKernels(sk, g.Config.Docker.Registry, g.Config.Docker.Commands, cmd.Max, kernelCmd.Retries, @@ -142,7 +132,7 @@ func (cmd KernelAutogenCmd) Run(kernelCmd *KernelCmd, g *Globals) (err error) { } } - return updateKernelsCfg(kernelCmd.UseHost, !kernelCmd.NoDownload) + return kernel.UpdateKernelsCfg(kernelCmd.UseHost, !kernelCmd.NoDownload) } type KernelGenallCmd struct { @@ -157,14 +147,14 @@ func (cmd *KernelGenallCmd) Run(kernelCmd *KernelCmd, g *Globals) (err error) { } shutdown := false - setSigintHandler(&shutdown) + kernel.SetSigintHandler(&shutdown) km := config.KernelMask{ DistroType: distroType, DistroRelease: cmd.Ver, ReleaseMask: ".*", } - err = generateKernels(km, + err = kernel.GenerateKernels(km, g.Config.Docker.Registry, g.Config.Docker.Commands, math.MaxUint32, kernelCmd.Retries, @@ -179,7 +169,7 @@ func (cmd *KernelGenallCmd) Run(kernelCmd *KernelCmd, g *Globals) (err error) { return } - return updateKernelsCfg(kernelCmd.UseHost, !kernelCmd.NoDownload) + return kernel.UpdateKernelsCfg(kernelCmd.UseHost, !kernelCmd.NoDownload) } type KernelInstallCmd struct { @@ -195,14 +185,14 @@ func (cmd *KernelInstallCmd) Run(kernelCmd *KernelCmd, g *Globals) (err error) { } shutdown := false - setSigintHandler(&shutdown) + kernel.SetSigintHandler(&shutdown) km := config.KernelMask{ DistroType: distroType, DistroRelease: cmd.Ver, ReleaseMask: cmd.Kernel, } - err = generateKernels(km, + err = kernel.GenerateKernels(km, g.Config.Docker.Registry, g.Config.Docker.Commands, math.MaxUint32, kernelCmd.Retries, @@ -217,672 +207,11 @@ func (cmd *KernelInstallCmd) Run(kernelCmd *KernelCmd, g *Globals) (err error) { return } - return updateKernelsCfg(kernelCmd.UseHost, !kernelCmd.NoDownload) + return kernel.UpdateKernelsCfg(kernelCmd.UseHost, !kernelCmd.NoDownload) } type KernelConfigRegenCmd struct{} func (cmd *KernelConfigRegenCmd) Run(kernelCmd *KernelCmd, g *Globals) (err error) { - return updateKernelsCfg(kernelCmd.UseHost, !kernelCmd.NoDownload) -} - -func matchDebImagePkg(containerName, mask string) (pkgs []string, err error) { - - cmd := "apt-cache search --names-only '^linux-image-[0-9\\.\\-]*-generic' | awk '{ print $1 }'" - - // FIXME timeout should be in global out-of-tree config - c, err := container.New(containerName, time.Hour) - if err != nil { - return - } - - output, err := c.Run(tempDirBase, cmd) - if err != nil { - return - } - - r, err := regexp.Compile("linux-image-" + mask) - if err != nil { - return - } - - for _, pkg := range strings.Fields(output) { - if r.MatchString(pkg) || strings.Contains(pkg, mask) { - pkgs = append(pkgs, pkg) - } - } - - return -} - -func matchOracleLinuxPkg(containerName, mask string) ( - pkgs []string, err error) { - - cmd := "yum search kernel --showduplicates " + - "| grep '^kernel-[0-9]\\|^kernel-uek-[0-9]' " + - "| grep -v src " + - "| cut -d ' ' -f 1" - - // FIXME timeout should be in global out-of-tree config - c, err := container.New(containerName, time.Hour) - if err != nil { - return - } - - output, err := c.Run(tempDirBase, cmd) - if err != nil { - return - } - - r, err := regexp.Compile("kernel-" + mask) - if err != nil { - return - } - - for _, pkg := range strings.Fields(output) { - if r.MatchString(pkg) || strings.Contains(pkg, mask) { - log.Trace().Msg(pkg) - pkgs = append(pkgs, pkg) - } - } - - if len(pkgs) == 0 { - log.Warn().Msg("no packages matched") - } - - return -} - -func matchPackages(km config.KernelMask) (pkgs []string, err error) { - switch km.DistroType { - case config.Ubuntu: - pkgs, err = matchDebImagePkg(km.DockerName(), km.ReleaseMask) - case config.OracleLinux, config.CentOS: - pkgs, err = matchOracleLinuxPkg(km.DockerName(), km.ReleaseMask) - default: - err = fmt.Errorf("%s not yet supported", km.DistroType.String()) - } - return -} - -func dockerImagePath(sk config.KernelMask) (path string, err error) { - usr, err := user.Current() - if err != nil { - return - } - - path = usr.HomeDir + "/.out-of-tree/containers/" - path += sk.DistroType.String() + "/" + sk.DistroRelease - return -} - -func vsyscallAvailable() (available bool, err error) { - if runtime.GOOS != "linux" { - // Docker for non-Linux systems is not using the host - // kernel but uses kernel inside a virtual machine, so - // it builds by the Docker team with vsyscall support. - available = true - return - } - - buf, err := ioutil.ReadFile("/proc/self/maps") - if err != nil { - return - } - - available = strings.Contains(string(buf), "[vsyscall]") - return -} - -func generateBaseDockerImage(registry string, commands []config.DockerCommand, - sk config.KernelMask, forceUpdate bool) (err error) { - - imagePath, err := dockerImagePath(sk) - if err != nil { - return - } - dockerPath := imagePath + "/Dockerfile" - - d := "# BASE\n" - - // TODO move as function to container.go - cmd := exec.Command(container.Runtime, "images", "-q", sk.DockerName()) - log.Debug().Msgf("run %v", cmd) - - rawOutput, err := cmd.CombinedOutput() - if err != nil { - return - } - - if fs.PathExists(dockerPath) && string(rawOutput) != "" { - log.Info().Msgf("Base image for %s:%s found", - sk.DistroType.String(), sk.DistroRelease) - if !forceUpdate { - return - } else { - log.Info().Msgf("Update Containerfile") - } - } - - log.Info().Msgf("Base image for %s:%s not found, start generating", - sk.DistroType.String(), sk.DistroRelease) - os.MkdirAll(imagePath, os.ModePerm) - - d += "FROM " - if registry != "" { - d += registry + "/" - } - - d += fmt.Sprintf("%s:%s\n", - strings.ToLower(sk.DistroType.String()), - sk.DistroRelease, - ) - - for _, c := range commands { - d += "RUN " + c.Command + "\n" - } - - switch sk.DistroType { - case config.Ubuntu: - if sk.DistroRelease < "14.04" { - d += "RUN sed -i 's/archive.ubuntu.com/old-releases.ubuntu.com/' /etc/apt/sources.list\n" - } - d += "ENV DEBIAN_FRONTEND=noninteractive\n" - d += "RUN apt-get update\n" - d += "RUN apt-get install -y build-essential libelf-dev\n" - d += "RUN apt-get install -y wget git\n" - // Install a single kernel and headers to ensure all dependencies are cached - if sk.DistroRelease >= "14.04" { - d += "RUN export PKGNAME=$(apt-cache search --names-only '^linux-headers-[0-9\\.\\-]*-generic' | awk '{ print $1 }' | head -n 1); " + - "apt-get install -y $PKGNAME $(echo $PKGNAME | sed 's/headers/image/'); " + - "apt-get remove -y $PKGNAME $(echo $PKGNAME | sed 's/headers/image/')\n" - d += "RUN apt-get install -y libseccomp-dev\n" - } - d += "RUN mkdir -p /lib/modules\n" - case config.CentOS: - var repos []string - - switch sk.DistroRelease { - case "6": - repofmt := "[6.%d-%s]\\nbaseurl=https://vault.centos.org/6.%d/%s/$basearch/\\ngpgcheck=0" - for i := 0; i <= 10; i++ { - repos = append(repos, fmt.Sprintf(repofmt, i, "os", i, "os")) - repos = append(repos, fmt.Sprintf(repofmt, i, "updates", i, "updates")) - } - d += "RUN rm /etc/yum.repos.d/*\n" - case "7": - repofmt := "[%s-%s]\\nbaseurl=https://vault.centos.org/%s/%s/$basearch/\\ngpgcheck=0" - for _, ver := range []string{ - "7.0.1406", "7.1.1503", "7.2.1511", - "7.3.1611", "7.4.1708", "7.5.1804", - "7.6.1810", "7.7.1908", "7.8.2003", - } { - repos = append(repos, fmt.Sprintf(repofmt, ver, "os", ver, "os")) - repos = append(repos, fmt.Sprintf(repofmt, ver, "updates", ver, "updates")) - } - - // FIXME http/gpgcheck=0 - repofmt = "[%s-%s]\\nbaseurl=http://mirror.centos.org/centos-7/%s/%s/$basearch/\\ngpgcheck=0" - repos = append(repos, fmt.Sprintf(repofmt, "7.9.2009", "os", "7.9.2009", "os")) - repos = append(repos, fmt.Sprintf(repofmt, "7.9.2009", "updates", "7.9.2009", "updates")) - case "8": - repofmt := "[%s]\\nbaseurl=https://vault.centos.org/%s/BaseOS/$basearch/os/\\ngpgcheck=0" - - for _, ver := range []string{ - "8.0.1905", "8.1.1911", "8.2.2004", - "8.3.2011", "8.4.2105", "8.5.2111", - } { - repos = append(repos, fmt.Sprintf(repofmt, ver, ver)) - } - default: - err = fmt.Errorf("no support for %s %s", sk.DistroType, sk.DistroRelease) - return - } - - d += "RUN sed -i 's/enabled=1/enabled=0/' /etc/yum.repos.d/* || true\n" - - for _, repo := range repos { - d += fmt.Sprintf("RUN echo -e '%s' >> /etc/yum.repos.d/oot.repo\n", repo) - } - - // do not remove old kernels - d += "RUN sed -i 's;installonly_limit=;installonly_limit=100500;' /etc/yum.conf\n" - d += "RUN yum -y update\n" - - d += "RUN yum -y groupinstall 'Development Tools'\n" - - if sk.DistroRelease < "8" { - d += "RUN yum -y install deltarpm\n" - } else { - d += "RUN yum -y install grub2-tools-minimal " + - "elfutils-libelf-devel\n" - } - - var flags string - if sk.DistroRelease >= "8" { - flags = "--noautoremove" - } - - // Cache kernel package dependencies - d += "RUN export PKGNAME=$(yum search kernel-devel --showduplicates | grep '^kernel-devel' | cut -d ' ' -f 1 | head -n 1); " + - "yum -y install $PKGNAME $(echo $PKGNAME | sed 's/-devel//'); " + - fmt.Sprintf("yum -y remove $PKGNAME "+ - "$(echo $PKGNAME | sed 's/-devel//') "+ - "$(echo $PKGNAME | sed 's/-devel/-modules/') "+ - "$(echo $PKGNAME | sed 's/-devel/-core/') %s\n", flags) - case config.OracleLinux: - if sk.DistroRelease < "6" { - err = fmt.Errorf("no support for pre-EL6") - return - } - d += "RUN sed -i 's/enabled=0/enabled=1/' /etc/yum.repos.d/*\n" - d += "RUN sed -i 's;installonly_limit=;installonly_limit=100500;' /etc/yum.conf /etc/dnf/dnf.conf || true\n" - d += "RUN yum -y update\n" - d += "RUN yum -y groupinstall 'Development Tools'\n" - packages := "linux-firmware grubby" - if sk.DistroRelease <= "7" { - packages += " libdtrace-ctf" - } - d += fmt.Sprintf("RUN yum -y install %s\n", packages) - default: - err = fmt.Errorf("%s not yet supported", sk.DistroType.String()) - return - } - - d += "# END BASE\n\n" - - err = ioutil.WriteFile(dockerPath, []byte(d), 0644) - if err != nil { - return - } - - c, err := container.New(sk.DockerName(), time.Hour) - if err != nil { - return - } - - output, err := c.Build(imagePath) - if err != nil { - log.Error().Err(err).Msgf("Base image for %s:%s generating error", - sk.DistroType.String(), sk.DistroRelease) - log.Fatal().Msg(output) - return - } - - log.Info().Msgf("Base image for %s:%s generating success", - sk.DistroType.String(), sk.DistroRelease) - - return -} - -func installKernel(sk config.KernelMask, pkgname string, force, headers bool) (err error) { - slog := log.With(). - Str("distro_type", sk.DistroType.String()). - Str("distro_release", sk.DistroRelease). - Str("pkg", pkgname). - Logger() - - c, err := container.New(sk.DockerName(), time.Hour) // TODO conf - if err != nil { - return - } - - moddirs, err := ioutil.ReadDir(c.Volumes.LibModules) - if err != nil { - return - } - - for _, krel := range moddirs { - if strings.Contains(pkgname, krel.Name()) { - if force { - slog.Info().Msg("Reinstall") - } else { - slog.Info().Msg("Already installed") - return - } - } - } - - volumes := c.Volumes - - c.Volumes.LibModules = "" - c.Volumes.UsrSrc = "" - c.Volumes.Boot = "" - - slog.Debug().Msgf("Installing kernel") - - cmd := "true" - - switch sk.DistroType { - case config.Ubuntu: - var headerspkg string - if headers { - headerspkg = strings.Replace(pkgname, "image", "headers", -1) - } - - cmd += fmt.Sprintf(" && apt-get install -y %s %s", pkgname, headerspkg) - case config.OracleLinux, config.CentOS: - var headerspkg string - if headers { - if strings.Contains(pkgname, "uek") { - headerspkg = strings.Replace(pkgname, - "kernel-uek", "kernel-uek-devel", -1) - } else { - headerspkg = strings.Replace(pkgname, - "kernel", "kernel-devel", -1) - } - } - - cmd += fmt.Sprintf(" && yum -y install %s %s", pkgname, headerspkg) - - var version string - if strings.Contains(pkgname, "uek") { - version = strings.Replace(pkgname, "kernel-uek-", "", -1) - } else { - version = strings.Replace(pkgname, "kernel-", "", -1) - } - - if sk.DistroRelease <= "7" { - cmd += fmt.Sprintf(" && dracut -v --add-drivers 'e1000 ext4' -f "+ - "/boot/initramfs-%s.img %s", version, version) - } else { - cmd += fmt.Sprintf(" && dracut -v --add-drivers 'ata_piix libata' --force-drivers 'e1000 ext4 sd_mod' -f "+ - "/boot/initramfs-%s.img %s", version, version) - } - default: - err = fmt.Errorf("%s not yet supported", sk.DistroType.String()) - return - } - - c.Args = append(c.Args, "-v", volumes.LibModules+":/target/lib/modules") - c.Args = append(c.Args, "-v", volumes.UsrSrc+":/target/usr/src") - c.Args = append(c.Args, "-v", volumes.Boot+":/target/boot") - - cmd += " && cp -r /boot /target/" - cmd += " && cp -r /lib/modules /target/lib/" - cmd += " && cp -r /usr/src /target/usr/" - - _, err = c.Run("", cmd) - if err != nil { - return - } - - slog.Debug().Msgf("Success") - return -} - -func findKernelFile(files []os.FileInfo, kname string) (name string, err error) { - for _, file := range files { - if strings.HasPrefix(file.Name(), "vmlinuz") { - if strings.Contains(file.Name(), kname) { - name = file.Name() - return - } - } - } - - err = errors.New("cannot find kernel") - return -} - -func findInitrdFile(files []os.FileInfo, kname string) (name string, err error) { - for _, file := range files { - if strings.HasPrefix(file.Name(), "initrd") || - strings.HasPrefix(file.Name(), "initramfs") { - - if strings.Contains(file.Name(), kname) { - name = file.Name() - return - } - } - } - - err = errors.New("cannot find kernel") - return -} - -func genRootfsImage(d container.Image, download bool) (rootfs string, err error) { - usr, err := user.Current() - if err != nil { - return - } - imageFile := d.Name + ".img" - - imagesPath := usr.HomeDir + "/.out-of-tree/images/" - os.MkdirAll(imagesPath, os.ModePerm) - - rootfs = imagesPath + imageFile - if !fs.PathExists(rootfs) { - if download { - log.Info().Msgf("%v not available, start download", imageFile) - err = downloadImage(imagesPath, imageFile) - } - } - return -} - -func updateKernelsCfg(host, download bool) (err error) { - newkcfg := config.KernelConfig{} - - if host { - // Get host kernels - newkcfg, err = genHostKernels(download) - if err != nil { - return - } - } - - // Get docker kernels - dockerImages, err := container.Images() - if err != nil { - return - } - - for _, d := range dockerImages { - err = listContainersKernels(d, &newkcfg, download) - if err != nil { - log.Print("gen kernels", d.Name, ":", err) - continue - } - } - - stripkcfg := config.KernelConfig{} - for _, nk := range newkcfg.Kernels { - if !hasKernel(nk, stripkcfg) { - stripkcfg.Kernels = append(stripkcfg.Kernels, nk) - } - } - - buf, err := toml.Marshal(&stripkcfg) - if err != nil { - return - } - - buf = append([]byte("# Autogenerated\n# DO NOT EDIT\n\n"), buf...) - - usr, err := user.Current() - if err != nil { - return - } - - // TODO move all cfg path values to one provider - kernelsCfgPath := usr.HomeDir + "/.out-of-tree/kernels.toml" - err = ioutil.WriteFile(kernelsCfgPath, buf, 0644) - if err != nil { - return - } - - log.Info().Msgf("%s is successfully updated", kernelsCfgPath) - return -} - -func listContainersKernels(dii container.Image, newkcfg *config.KernelConfig, - download bool) (err error) { - - rootfs, err := genRootfsImage(dii, download) - if err != nil { - return - } - - c, err := container.New(dii.Name, time.Hour) - if err != nil { - return - } - - moddirs, err := ioutil.ReadDir(c.Volumes.LibModules) - if err != nil { - return - } - - bootfiles, err := ioutil.ReadDir(c.Volumes.Boot) - if err != nil { - return - } - - for _, krel := range moddirs { - log.Debug().Msgf("generate config entry for %s", krel.Name()) - - var kernelFile, initrdFile string - kernelFile, err = findKernelFile(bootfiles, krel.Name()) - if err != nil { - log.Warn().Msgf("cannot find kernel %s", krel.Name()) - continue - } - - initrdFile, err = findInitrdFile(bootfiles, krel.Name()) - if err != nil { - log.Warn().Msgf("cannot find initrd %s", krel.Name()) - continue - } - - ki := config.KernelInfo{ - DistroType: dii.DistroType, - DistroRelease: dii.DistroRelease, - KernelRelease: krel.Name(), - ContainerName: dii.Name, - - KernelPath: c.Volumes.Boot + "/" + kernelFile, - InitrdPath: c.Volumes.Boot + "/" + initrdFile, - ModulesPath: c.Volumes.LibModules + "/" + krel.Name(), - - RootFS: rootfs, - } - newkcfg.Kernels = append(newkcfg.Kernels, ki) - } - - for _, cmd := range []string{ - "find /boot -type f -exec chmod a+r {} \\;", - } { - _, err = c.Run(tempDirBase, cmd) - if err != nil { - return - } - } - - return -} - -func hasKernel(ki config.KernelInfo, kcfg config.KernelConfig) bool { - for _, sk := range kcfg.Kernels { - if sk == ki { - return true - } - } - return false -} - -func shuffleStrings(a []string) []string { - // Fisher–Yates shuffle - for i := len(a) - 1; i > 0; i-- { - j := rand.Intn(i + 1) - a[i], a[j] = a[j], a[i] - } - return a -} - -func setSigintHandler(variable *bool) { - c := make(chan os.Signal, 1) - signal.Notify(c, os.Interrupt) - go func() { - counter := 0 - for _ = range c { - if counter == 0 { - *variable = true - log.Warn().Msg("shutdown requested, finishing work") - log.Info().Msg("^C a couple of times more for an unsafe exit") - } else if counter >= 3 { - log.Fatal().Msg("unsafe exit") - } - - counter += 1 - } - }() - -} - -// FIXME too many parameters -func generateKernels(km config.KernelMask, registry string, - commands []config.DockerCommand, max, retries int64, - download, force, headers, shuffle, update bool, - shutdown *bool) (err error) { - - log.Info().Msgf("Generating for kernel mask %v", km) - - _, err = genRootfsImage(container.Image{Name: km.DockerName()}, - download) - if err != nil || *shutdown { - return - } - - err = generateBaseDockerImage(registry, commands, km, update) - if err != nil || *shutdown { - return - } - - pkgs, err := matchPackages(km) - if err != nil || *shutdown { - return - } - - if shuffle { - pkgs = shuffleStrings(pkgs) - } - for i, pkg := range pkgs { - if max <= 0 { - log.Print("Max is reached") - break - } - - if *shutdown { - err = nil - return - } - log.Info().Msgf("%d/%d %s", i+1, len(pkgs), pkg) - - var attempt int64 - for { - attempt++ - - if *shutdown { - err = nil - return - } - - err = installKernel(km, pkg, force, headers) - if err == nil { - max-- - break - } else if attempt >= retries { - log.Error().Err(err).Msg("install kernel") - log.Debug().Msg("skip") - break - } else { - log.Warn().Err(err).Msg("install kernel") - time.Sleep(time.Second) - log.Info().Msg("retry") - } - } - } - - return + return kernel.UpdateKernelsCfg(kernelCmd.UseHost, !kernelCmd.NoDownload) } diff --git a/kernel/kernel.go b/kernel/kernel.go new file mode 100644 index 0000000..36c4770 --- /dev/null +++ b/kernel/kernel.go @@ -0,0 +1,689 @@ +// Copyright 2023 Mikhail Klementev. All rights reserved. +// Use of this source code is governed by a AGPLv3 license +// (or later) that can be found in the LICENSE file. + +package kernel + +import ( + "errors" + "fmt" + "io/ioutil" + "math/rand" + "os" + "os/exec" + "os/signal" + "os/user" + "regexp" + "runtime" + "strings" + "time" + + "github.com/naoina/toml" + "github.com/rs/zerolog/log" + + "code.dumpstack.io/tools/out-of-tree/cache" + "code.dumpstack.io/tools/out-of-tree/config" + "code.dumpstack.io/tools/out-of-tree/container" + "code.dumpstack.io/tools/out-of-tree/fs" +) + +func matchDebImagePkg(containerName, mask string) (pkgs []string, err error) { + + cmd := "apt-cache search --names-only '^linux-image-[0-9\\.\\-]*-generic' | awk '{ print $1 }'" + + // FIXME timeout should be in global out-of-tree config + c, err := container.New(containerName, time.Hour) + if err != nil { + return + } + + output, err := c.Run(config.Dir("tmp"), cmd) + if err != nil { + return + } + + r, err := regexp.Compile("linux-image-" + mask) + if err != nil { + return + } + + for _, pkg := range strings.Fields(output) { + if r.MatchString(pkg) || strings.Contains(pkg, mask) { + pkgs = append(pkgs, pkg) + } + } + + return +} + +func matchOracleLinuxPkg(containerName, mask string) ( + pkgs []string, err error) { + + cmd := "yum search kernel --showduplicates " + + "| grep '^kernel-[0-9]\\|^kernel-uek-[0-9]' " + + "| grep -v src " + + "| cut -d ' ' -f 1" + + // FIXME timeout should be in global out-of-tree config + c, err := container.New(containerName, time.Hour) + if err != nil { + return + } + + output, err := c.Run(config.Dir("tmp"), cmd) + if err != nil { + return + } + + r, err := regexp.Compile("kernel-" + mask) + if err != nil { + return + } + + for _, pkg := range strings.Fields(output) { + if r.MatchString(pkg) || strings.Contains(pkg, mask) { + log.Trace().Msg(pkg) + pkgs = append(pkgs, pkg) + } + } + + if len(pkgs) == 0 { + log.Warn().Msg("no packages matched") + } + + return +} + +func MatchPackages(km config.KernelMask) (pkgs []string, err error) { + switch km.DistroType { + case config.Ubuntu: + pkgs, err = matchDebImagePkg(km.DockerName(), km.ReleaseMask) + case config.OracleLinux, config.CentOS: + pkgs, err = matchOracleLinuxPkg(km.DockerName(), km.ReleaseMask) + default: + err = fmt.Errorf("%s not yet supported", km.DistroType.String()) + } + return +} + +func dockerImagePath(sk config.KernelMask) (path string, err error) { + usr, err := user.Current() + if err != nil { + return + } + + path = usr.HomeDir + "/.out-of-tree/containers/" + path += sk.DistroType.String() + "/" + sk.DistroRelease + return +} + +func vsyscallAvailable() (available bool, err error) { + if runtime.GOOS != "linux" { + // Docker for non-Linux systems is not using the host + // kernel but uses kernel inside a virtual machine, so + // it builds by the Docker team with vsyscall support. + available = true + return + } + + buf, err := ioutil.ReadFile("/proc/self/maps") + if err != nil { + return + } + + available = strings.Contains(string(buf), "[vsyscall]") + return +} + +func GenerateBaseDockerImage(registry string, commands []config.DockerCommand, + sk config.KernelMask, forceUpdate bool) (err error) { + + imagePath, err := dockerImagePath(sk) + if err != nil { + return + } + dockerPath := imagePath + "/Dockerfile" + + d := "# BASE\n" + + // TODO move as function to container.go + cmd := exec.Command(container.Runtime, "images", "-q", sk.DockerName()) + log.Debug().Msgf("run %v", cmd) + + rawOutput, err := cmd.CombinedOutput() + if err != nil { + return + } + + if fs.PathExists(dockerPath) && string(rawOutput) != "" { + log.Info().Msgf("Base image for %s:%s found", + sk.DistroType.String(), sk.DistroRelease) + if !forceUpdate { + return + } else { + log.Info().Msgf("Update Containerfile") + } + } + + log.Info().Msgf("Base image for %s:%s not found, start generating", + sk.DistroType.String(), sk.DistroRelease) + os.MkdirAll(imagePath, os.ModePerm) + + d += "FROM " + if registry != "" { + d += registry + "/" + } + + d += fmt.Sprintf("%s:%s\n", + strings.ToLower(sk.DistroType.String()), + sk.DistroRelease, + ) + + for _, c := range commands { + d += "RUN " + c.Command + "\n" + } + + switch sk.DistroType { + case config.Ubuntu: + if sk.DistroRelease < "14.04" { + d += "RUN sed -i 's/archive.ubuntu.com/old-releases.ubuntu.com/' /etc/apt/sources.list\n" + } + d += "ENV DEBIAN_FRONTEND=noninteractive\n" + d += "RUN apt-get update\n" + d += "RUN apt-get install -y build-essential libelf-dev\n" + d += "RUN apt-get install -y wget git\n" + // Install a single kernel and headers to ensure all dependencies are cached + if sk.DistroRelease >= "14.04" { + d += "RUN export PKGNAME=$(apt-cache search --names-only '^linux-headers-[0-9\\.\\-]*-generic' | awk '{ print $1 }' | head -n 1); " + + "apt-get install -y $PKGNAME $(echo $PKGNAME | sed 's/headers/image/'); " + + "apt-get remove -y $PKGNAME $(echo $PKGNAME | sed 's/headers/image/')\n" + d += "RUN apt-get install -y libseccomp-dev\n" + } + d += "RUN mkdir -p /lib/modules\n" + case config.CentOS: + var repos []string + + switch sk.DistroRelease { + case "6": + repofmt := "[6.%d-%s]\\nbaseurl=https://vault.centos.org/6.%d/%s/$basearch/\\ngpgcheck=0" + for i := 0; i <= 10; i++ { + repos = append(repos, fmt.Sprintf(repofmt, i, "os", i, "os")) + repos = append(repos, fmt.Sprintf(repofmt, i, "updates", i, "updates")) + } + d += "RUN rm /etc/yum.repos.d/*\n" + case "7": + repofmt := "[%s-%s]\\nbaseurl=https://vault.centos.org/%s/%s/$basearch/\\ngpgcheck=0" + for _, ver := range []string{ + "7.0.1406", "7.1.1503", "7.2.1511", + "7.3.1611", "7.4.1708", "7.5.1804", + "7.6.1810", "7.7.1908", "7.8.2003", + } { + repos = append(repos, fmt.Sprintf(repofmt, ver, "os", ver, "os")) + repos = append(repos, fmt.Sprintf(repofmt, ver, "updates", ver, "updates")) + } + + // FIXME http/gpgcheck=0 + repofmt = "[%s-%s]\\nbaseurl=http://mirror.centos.org/centos-7/%s/%s/$basearch/\\ngpgcheck=0" + repos = append(repos, fmt.Sprintf(repofmt, "7.9.2009", "os", "7.9.2009", "os")) + repos = append(repos, fmt.Sprintf(repofmt, "7.9.2009", "updates", "7.9.2009", "updates")) + case "8": + repofmt := "[%s]\\nbaseurl=https://vault.centos.org/%s/BaseOS/$basearch/os/\\ngpgcheck=0" + + for _, ver := range []string{ + "8.0.1905", "8.1.1911", "8.2.2004", + "8.3.2011", "8.4.2105", "8.5.2111", + } { + repos = append(repos, fmt.Sprintf(repofmt, ver, ver)) + } + default: + err = fmt.Errorf("no support for %s %s", sk.DistroType, sk.DistroRelease) + return + } + + d += "RUN sed -i 's/enabled=1/enabled=0/' /etc/yum.repos.d/* || true\n" + + for _, repo := range repos { + d += fmt.Sprintf("RUN echo -e '%s' >> /etc/yum.repos.d/oot.repo\n", repo) + } + + // do not remove old kernels + d += "RUN sed -i 's;installonly_limit=;installonly_limit=100500;' /etc/yum.conf\n" + d += "RUN yum -y update\n" + + d += "RUN yum -y groupinstall 'Development Tools'\n" + + if sk.DistroRelease < "8" { + d += "RUN yum -y install deltarpm\n" + } else { + d += "RUN yum -y install grub2-tools-minimal " + + "elfutils-libelf-devel\n" + } + + var flags string + if sk.DistroRelease >= "8" { + flags = "--noautoremove" + } + + // Cache kernel package dependencies + d += "RUN export PKGNAME=$(yum search kernel-devel --showduplicates | grep '^kernel-devel' | cut -d ' ' -f 1 | head -n 1); " + + "yum -y install $PKGNAME $(echo $PKGNAME | sed 's/-devel//'); " + + fmt.Sprintf("yum -y remove $PKGNAME "+ + "$(echo $PKGNAME | sed 's/-devel//') "+ + "$(echo $PKGNAME | sed 's/-devel/-modules/') "+ + "$(echo $PKGNAME | sed 's/-devel/-core/') %s\n", flags) + case config.OracleLinux: + if sk.DistroRelease < "6" { + err = fmt.Errorf("no support for pre-EL6") + return + } + d += "RUN sed -i 's/enabled=0/enabled=1/' /etc/yum.repos.d/*\n" + d += "RUN sed -i 's;installonly_limit=;installonly_limit=100500;' /etc/yum.conf /etc/dnf/dnf.conf || true\n" + d += "RUN yum -y update\n" + d += "RUN yum -y groupinstall 'Development Tools'\n" + packages := "linux-firmware grubby" + if sk.DistroRelease <= "7" { + packages += " libdtrace-ctf" + } + d += fmt.Sprintf("RUN yum -y install %s\n", packages) + default: + err = fmt.Errorf("%s not yet supported", sk.DistroType.String()) + return + } + + d += "# END BASE\n\n" + + err = ioutil.WriteFile(dockerPath, []byte(d), 0644) + if err != nil { + return + } + + c, err := container.New(sk.DockerName(), time.Hour) + if err != nil { + return + } + + output, err := c.Build(imagePath) + if err != nil { + log.Error().Err(err).Msgf("Base image for %s:%s generating error", + sk.DistroType.String(), sk.DistroRelease) + log.Fatal().Msg(output) + return + } + + log.Info().Msgf("Base image for %s:%s generating success", + sk.DistroType.String(), sk.DistroRelease) + + return +} + +func installKernel(sk config.KernelMask, pkgname string, force, headers bool) (err error) { + slog := log.With(). + Str("distro_type", sk.DistroType.String()). + Str("distro_release", sk.DistroRelease). + Str("pkg", pkgname). + Logger() + + c, err := container.New(sk.DockerName(), time.Hour) // TODO conf + if err != nil { + return + } + + moddirs, err := ioutil.ReadDir(c.Volumes.LibModules) + if err != nil { + return + } + + for _, krel := range moddirs { + if strings.Contains(pkgname, krel.Name()) { + if force { + slog.Info().Msg("Reinstall") + } else { + slog.Info().Msg("Already installed") + return + } + } + } + + volumes := c.Volumes + + c.Volumes.LibModules = "" + c.Volumes.UsrSrc = "" + c.Volumes.Boot = "" + + slog.Debug().Msgf("Installing kernel") + + cmd := "true" + + switch sk.DistroType { + case config.Ubuntu: + var headerspkg string + if headers { + headerspkg = strings.Replace(pkgname, "image", "headers", -1) + } + + cmd += fmt.Sprintf(" && apt-get install -y %s %s", pkgname, headerspkg) + case config.OracleLinux, config.CentOS: + var headerspkg string + if headers { + if strings.Contains(pkgname, "uek") { + headerspkg = strings.Replace(pkgname, + "kernel-uek", "kernel-uek-devel", -1) + } else { + headerspkg = strings.Replace(pkgname, + "kernel", "kernel-devel", -1) + } + } + + cmd += fmt.Sprintf(" && yum -y install %s %s", pkgname, headerspkg) + + var version string + if strings.Contains(pkgname, "uek") { + version = strings.Replace(pkgname, "kernel-uek-", "", -1) + } else { + version = strings.Replace(pkgname, "kernel-", "", -1) + } + + if sk.DistroRelease <= "7" { + cmd += fmt.Sprintf(" && dracut -v --add-drivers 'e1000 ext4' -f "+ + "/boot/initramfs-%s.img %s", version, version) + } else { + cmd += fmt.Sprintf(" && dracut -v --add-drivers 'ata_piix libata' --force-drivers 'e1000 ext4 sd_mod' -f "+ + "/boot/initramfs-%s.img %s", version, version) + } + default: + err = fmt.Errorf("%s not yet supported", sk.DistroType.String()) + return + } + + c.Args = append(c.Args, "-v", volumes.LibModules+":/target/lib/modules") + c.Args = append(c.Args, "-v", volumes.UsrSrc+":/target/usr/src") + c.Args = append(c.Args, "-v", volumes.Boot+":/target/boot") + + cmd += " && cp -r /boot /target/" + cmd += " && cp -r /lib/modules /target/lib/" + cmd += " && cp -r /usr/src /target/usr/" + + _, err = c.Run("", cmd) + if err != nil { + return + } + + slog.Debug().Msgf("Success") + return +} + +func findKernelFile(files []os.FileInfo, kname string) (name string, err error) { + for _, file := range files { + if strings.HasPrefix(file.Name(), "vmlinuz") { + if strings.Contains(file.Name(), kname) { + name = file.Name() + return + } + } + } + + err = errors.New("cannot find kernel") + return +} + +func findInitrdFile(files []os.FileInfo, kname string) (name string, err error) { + for _, file := range files { + if strings.HasPrefix(file.Name(), "initrd") || + strings.HasPrefix(file.Name(), "initramfs") { + + if strings.Contains(file.Name(), kname) { + name = file.Name() + return + } + } + } + + err = errors.New("cannot find kernel") + return +} + +func GenRootfsImage(d container.Image, download bool) (rootfs string, err error) { + usr, err := user.Current() + if err != nil { + return + } + imageFile := d.Name + ".img" + + imagesPath := usr.HomeDir + "/.out-of-tree/images/" + os.MkdirAll(imagesPath, os.ModePerm) + + rootfs = imagesPath + imageFile + if !fs.PathExists(rootfs) { + if download { + log.Info().Msgf("%v not available, start download", imageFile) + err = cache.DownloadQemuImage(imagesPath, imageFile) + } + } + return +} + +func UpdateKernelsCfg(host, download bool) (err error) { + newkcfg := config.KernelConfig{} + + if host { + // Get host kernels + newkcfg, err = genHostKernels(download) + if err != nil { + return + } + } + + // Get docker kernels + dockerImages, err := container.Images() + if err != nil { + return + } + + for _, d := range dockerImages { + err = listContainersKernels(d, &newkcfg, download) + if err != nil { + log.Print("gen kernels", d.Name, ":", err) + continue + } + } + + stripkcfg := config.KernelConfig{} + for _, nk := range newkcfg.Kernels { + if !hasKernel(nk, stripkcfg) { + stripkcfg.Kernels = append(stripkcfg.Kernels, nk) + } + } + + buf, err := toml.Marshal(&stripkcfg) + if err != nil { + return + } + + buf = append([]byte("# Autogenerated\n# DO NOT EDIT\n\n"), buf...) + + usr, err := user.Current() + if err != nil { + return + } + + // TODO move all cfg path values to one provider + kernelsCfgPath := usr.HomeDir + "/.out-of-tree/kernels.toml" + err = ioutil.WriteFile(kernelsCfgPath, buf, 0644) + if err != nil { + return + } + + log.Info().Msgf("%s is successfully updated", kernelsCfgPath) + return +} + +func listContainersKernels(dii container.Image, newkcfg *config.KernelConfig, + download bool) (err error) { + + rootfs, err := GenRootfsImage(dii, download) + if err != nil { + return + } + + c, err := container.New(dii.Name, time.Hour) + if err != nil { + return + } + + moddirs, err := ioutil.ReadDir(c.Volumes.LibModules) + if err != nil { + return + } + + bootfiles, err := ioutil.ReadDir(c.Volumes.Boot) + if err != nil { + return + } + + for _, krel := range moddirs { + log.Debug().Msgf("generate config entry for %s", krel.Name()) + + var kernelFile, initrdFile string + kernelFile, err = findKernelFile(bootfiles, krel.Name()) + if err != nil { + log.Warn().Msgf("cannot find kernel %s", krel.Name()) + continue + } + + initrdFile, err = findInitrdFile(bootfiles, krel.Name()) + if err != nil { + log.Warn().Msgf("cannot find initrd %s", krel.Name()) + continue + } + + ki := config.KernelInfo{ + DistroType: dii.DistroType, + DistroRelease: dii.DistroRelease, + KernelRelease: krel.Name(), + ContainerName: dii.Name, + + KernelPath: c.Volumes.Boot + "/" + kernelFile, + InitrdPath: c.Volumes.Boot + "/" + initrdFile, + ModulesPath: c.Volumes.LibModules + "/" + krel.Name(), + + RootFS: rootfs, + } + newkcfg.Kernels = append(newkcfg.Kernels, ki) + } + + for _, cmd := range []string{ + "find /boot -type f -exec chmod a+r {} \\;", + } { + _, err = c.Run(config.Dir("tmp"), cmd) + if err != nil { + return + } + } + + return +} + +func hasKernel(ki config.KernelInfo, kcfg config.KernelConfig) bool { + for _, sk := range kcfg.Kernels { + if sk == ki { + return true + } + } + return false +} + +func shuffleStrings(a []string) []string { + // Fisher–Yates shuffle + for i := len(a) - 1; i > 0; i-- { + j := rand.Intn(i + 1) + a[i], a[j] = a[j], a[i] + } + return a +} + +func SetSigintHandler(variable *bool) { + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + go func() { + counter := 0 + for _ = range c { + if counter == 0 { + *variable = true + log.Warn().Msg("shutdown requested, finishing work") + log.Info().Msg("^C a couple of times more for an unsafe exit") + } else if counter >= 3 { + log.Fatal().Msg("unsafe exit") + } + + counter += 1 + } + }() + +} + +// FIXME too many parameters +func GenerateKernels(km config.KernelMask, registry string, + commands []config.DockerCommand, max, retries int64, + download, force, headers, shuffle, update bool, + shutdown *bool) (err error) { + + log.Info().Msgf("Generating for kernel mask %v", km) + + _, err = GenRootfsImage(container.Image{Name: km.DockerName()}, + download) + if err != nil || *shutdown { + return + } + + err = GenerateBaseDockerImage(registry, commands, km, update) + if err != nil || *shutdown { + return + } + + pkgs, err := MatchPackages(km) + if err != nil || *shutdown { + return + } + + if shuffle { + pkgs = shuffleStrings(pkgs) + } + for i, pkg := range pkgs { + if max <= 0 { + log.Print("Max is reached") + break + } + + if *shutdown { + err = nil + return + } + log.Info().Msgf("%d/%d %s", i+1, len(pkgs), pkg) + + var attempt int64 + for { + attempt++ + + if *shutdown { + err = nil + return + } + + err = installKernel(km, pkg, force, headers) + if err == nil { + max-- + break + } else if attempt >= retries { + log.Error().Err(err).Msg("install kernel") + log.Debug().Msg("skip") + break + } else { + log.Warn().Err(err).Msg("install kernel") + time.Sleep(time.Second) + log.Info().Msg("retry") + } + } + } + + return +} diff --git a/kernel_linux.go b/kernel/kernel_linux.go similarity index 97% rename from kernel_linux.go rename to kernel/kernel_linux.go index feca01f..269ce28 100644 --- a/kernel_linux.go +++ b/kernel/kernel_linux.go @@ -5,7 +5,7 @@ //go:build linux // +build linux -package main +package kernel import ( "io/ioutil" @@ -52,7 +52,7 @@ func genHostKernels(download bool) (kcfg config.KernelConfig, err error) { }.DockerName(), } - rootfs, err := genRootfsImage(dii, download) + rootfs, err := GenRootfsImage(dii, download) if err != nil { return } diff --git a/kernel_macos.go b/kernel/kernel_macos.go similarity index 79% rename from kernel_macos.go rename to kernel/kernel_macos.go index c78b67f..d58d8d5 100644 --- a/kernel_macos.go +++ b/kernel/kernel_macos.go @@ -1,10 +1,11 @@ -// Copyright 2018 Mikhail Klementev. All rights reserved. +// Copyright 2023 Mikhail Klementev. All rights reserved. // Use of this source code is governed by a AGPLv3 license // (or later) that can be found in the LICENSE file. +//go:build darwin // +build darwin -package main +package kernel import ( "errors"