1
0
out-of-tree/kernel.go

718 lines
16 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Copyright 2018 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
import (
"errors"
"fmt"
"io/ioutil"
"log"
"math"
"math/rand"
"os"
"os/exec"
"os/user"
"regexp"
"runtime"
"strings"
"time"
"github.com/naoina/toml"
"code.dumpstack.io/tools/out-of-tree/config"
)
const kernelsAll int64 = math.MaxInt64
func kernelListHandler(kcfg config.KernelConfig) (err error) {
if len(kcfg.Kernels) == 0 {
return errors.New("No kernels found")
}
for _, k := range kcfg.Kernels {
fmt.Println(k.DistroType, k.DistroRelease, k.KernelRelease)
}
return
}
func matchDebianHeadersPkg(container, mask string, generic bool) (
pkgs []string, err error) {
cmd := "apt-cache search linux-headers | cut -d ' ' -f 1"
output, err := dockerRun(time.Minute, container, "/tmp", cmd)
if err != nil {
return
}
r, err := regexp.Compile("linux-headers-" + mask)
if err != nil {
return
}
kernels := r.FindAll([]byte(output), -1)
for _, k := range kernels {
pkg := string(k)
if generic && !strings.HasSuffix(pkg, "generic") {
continue
}
if pkg == "linux-headers-generic" {
continue
}
pkgs = append(pkgs, pkg)
}
return
}
func matchCentOSDevelPkg(container, mask string, generic bool) (
pkgs []string, err error) {
cmd := "yum search kernel-devel --showduplicates | " +
"grep '^kernel-devel' | cut -d ' ' -f 1"
output, err := dockerRun(time.Minute, container, "/tmp", cmd)
if err != nil {
return
}
r, err := regexp.Compile("kernel-devel-" + mask)
if err != nil {
return
}
for _, k := range r.FindAll([]byte(output), -1) {
pkgs = append(pkgs, string(k))
}
return
}
func dockerImagePath(sk config.KernelMask) (path string, err error) {
usr, err := user.Current()
if err != nil {
return
}
path = usr.HomeDir + "/.out-of-tree/"
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) (err error) {
imagePath, err := dockerImagePath(sk)
if err != nil {
return
}
dockerPath := imagePath + "/Dockerfile"
d := "# BASE\n"
if exists(dockerPath) {
log.Printf("Base image for %s:%s found",
sk.DistroType.String(), sk.DistroRelease)
return
}
log.Printf("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,
)
vsyscall, err := vsyscallAvailable()
if err != nil {
return
}
for _, c := range commands {
switch c.DistroType {
case config.Ubuntu:
d += "RUN " + c.Command + "\n"
case config.CentOS:
d += "RUN " + c.Command + "\n"
case config.Debian:
d += "RUN " + c.Command + "\n"
default:
err = fmt.Errorf("%s not yet supported",
sk.DistroType.String())
return
}
}
switch sk.DistroType {
case config.Ubuntu:
d += "ENV DEBIAN_FRONTEND=noninteractive\n"
if sk.DistroRelease >= "16.04" {
from := "http://.*ubuntu/"
to := "mirror://mirrors.ubuntu.com/mirrors.txt"
file := "/etc/apt/sources.list"
s := fmt.Sprintf("sed -i 's;%s;%s;' %s", from, to, file)
d += "RUN " + s + "\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"
if sk.DistroRelease >= "14.04" {
d += "RUN apt-get install -y libseccomp-dev\n"
}
d += "RUN mkdir -p /lib/modules\n"
case config.CentOS:
if sk.DistroRelease < "7" && !vsyscall {
log.Println("Old CentOS requires `vsyscall=emulate` " +
"on the latest kernels")
log.Println("Check out `A note about vsyscall` " +
"at https://hub.docker.com/_/centos")
log.Println("See also https://lwn.net/Articles/446528/")
err = fmt.Errorf("vsyscall is not available")
return
}
// enable rpms from old minor releases
d += "RUN sed -i 's/enabled=0/enabled=1/' /etc/yum.repos.d/CentOS-Vault.repo\n"
// 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"
if sk.DistroRelease == "8" {
// FIXME CentOS Vault repository list for 8 is empty
// at the time of this fix; check for it and use a
// workaround if it's still empty
d += `RUN grep enabled /etc/yum.repos.d/CentOS-Vault.repo` +
` || echo -e '[8.0.1905]\nbaseurl=http://vault.centos.org/8.0.1905/BaseOS/$basearch/os/'` +
` >> /etc/yum.repos.d/CentOS-Vault.repo` + "\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 drpm grub2-tools-minimal " +
"elfutils-libelf-devel\n"
}
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
}
cmd := exec.Command("docker", "build", "-t", sk.DockerName(), imagePath)
rawOutput, err := cmd.CombinedOutput()
if err != nil {
log.Printf("Base image for %s:%s generating error, see log",
sk.DistroType.String(), sk.DistroRelease)
log.Println(string(rawOutput))
return
}
log.Printf("Base image for %s:%s generating success",
sk.DistroType.String(), sk.DistroRelease)
return
}
func dockerImageAppend(sk config.KernelMask, pkgname string) (err error) {
imagePath, err := dockerImagePath(sk)
if err != nil {
return
}
raw, err := ioutil.ReadFile(imagePath + "/Dockerfile")
if err != nil {
return
}
if strings.Contains(string(raw), pkgname) {
// already installed kernel
log.Printf("kernel %s for %s:%s is already exists",
pkgname, sk.DistroType.String(), sk.DistroRelease)
return
}
var s string
switch sk.DistroType {
case config.Ubuntu:
imagepkg := strings.Replace(pkgname, "headers", "image", -1)
log.Printf("Start adding kernel %s for %s:%s",
imagepkg, sk.DistroType.String(), sk.DistroRelease)
s = fmt.Sprintf("RUN apt-get install -y %s %s\n", imagepkg,
pkgname)
case config.CentOS:
imagepkg := strings.Replace(pkgname, "-devel", "", -1)
log.Printf("Start adding kernel %s for %s:%s",
imagepkg, sk.DistroType.String(), sk.DistroRelease)
version := strings.Replace(pkgname, "kernel-devel-", "", -1)
s = fmt.Sprintf("RUN yum -y install %s %s\n", imagepkg,
pkgname)
s += fmt.Sprintf("RUN dracut --add-drivers 'e1000 ext4' -f "+
"/boot/initramfs-%s.img %s\n", version, version)
default:
err = fmt.Errorf("%s not yet supported", sk.DistroType.String())
return
}
err = ioutil.WriteFile(imagePath+"/Dockerfile",
append(raw, []byte(s)...), 0644)
if err != nil {
return
}
cmd := exec.Command("docker", "build", "-t", sk.DockerName(), imagePath)
rawOutput, err := cmd.CombinedOutput()
if err != nil {
// Fallback to previous state
werr := ioutil.WriteFile(imagePath+"/Dockerfile", raw, 0644)
if werr != nil {
return
}
log.Printf("Add kernel %s for %s:%s error, see log",
pkgname, sk.DistroType.String(), sk.DistroRelease)
log.Println(string(rawOutput))
return
}
log.Printf("Add kernel %s for %s:%s success",
pkgname, sk.DistroType.String(), sk.DistroRelease)
return
}
func kickImage(name string) (err error) {
cmd := exec.Command("docker", "run", name, "bash", "-c", "ls")
_, err = cmd.CombinedOutput()
return
}
func copyKernels(name string) (err error) {
cmd := exec.Command("docker", "ps", "-a")
rawOutput, err := cmd.CombinedOutput()
if err != nil {
log.Println(string(rawOutput))
return
}
r, err := regexp.Compile(".*" + name)
if err != nil {
return
}
var containerID string
what := r.FindAll(rawOutput, -1)
for _, w := range what {
containerID = strings.Fields(string(w))[0]
break
}
usr, err := user.Current()
if err != nil {
return
}
target := usr.HomeDir + "/.out-of-tree/kernels/"
if !exists(target) {
os.MkdirAll(target, os.ModePerm)
}
cmd = exec.Command("docker", "cp", containerID+":/boot/.", target)
rawOutput, err = cmd.CombinedOutput()
if err != nil {
log.Println(string(rawOutput))
return
}
return
}
func genKernelPath(files []os.FileInfo, kname string) string {
for _, file := range files {
if strings.HasPrefix(file.Name(), "vmlinuz") {
if strings.Contains(file.Name(), kname) {
return file.Name()
}
}
}
return "unknown"
}
func genInitrdPath(files []os.FileInfo, kname string) string {
for _, file := range files {
if strings.HasPrefix(file.Name(), "initrd") ||
strings.HasPrefix(file.Name(), "initramfs") {
if strings.Contains(file.Name(), kname) {
return file.Name()
}
}
}
return "unknown"
}
func genRootfsImage(d dockerImageInfo, download bool) (rootfs string, err error) {
usr, err := user.Current()
if err != nil {
return
}
imageFile := d.ContainerName + ".img"
imagesPath := usr.HomeDir + "/.out-of-tree/images/"
os.MkdirAll(imagesPath, os.ModePerm)
rootfs = imagesPath + imageFile
if !exists(rootfs) {
if download {
log.Println(imageFile, "not exists, start downloading...")
err = downloadImage(imagesPath, imageFile)
}
}
return
}
type dockerImageInfo struct {
ContainerName string
DistroType config.DistroType
DistroRelease string // 18.04/7.4.1708/9.1
}
func listDockerImages() (diis []dockerImageInfo, err error) {
cmd := exec.Command("docker", "images")
rawOutput, err := cmd.CombinedOutput()
if err != nil {
return
}
r, err := regexp.Compile("out_of_tree_.*")
if err != nil {
return
}
containers := r.FindAll(rawOutput, -1)
for _, c := range containers {
container := strings.Fields(string(c))[0]
s := strings.Replace(container, "__", ".", -1)
values := strings.Split(s, "_")
distro, ver := values[3], values[4]
dii := dockerImageInfo{
ContainerName: container,
DistroRelease: ver,
}
dii.DistroType, err = config.NewDistroType(distro)
if err != nil {
return
}
diis = append(diis, dii)
}
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 := listDockerImages()
if err != nil {
return
}
for _, d := range dockerImages {
err = genDockerKernels(d, &newkcfg, download)
if err != nil {
log.Println("gen kernels", d.ContainerName, ":", 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.Println(kernelsCfgPath, "is successfully updated")
return
}
func genDockerKernels(dii dockerImageInfo, newkcfg *config.KernelConfig,
download bool) (err error) {
name := dii.ContainerName
cmd := exec.Command("docker", "run", name, "ls", "/lib/modules")
rawOutput, err := cmd.CombinedOutput()
if err != nil {
log.Println(string(rawOutput), err)
return
}
usr, err := user.Current()
if err != nil {
return
}
kernelsBase := usr.HomeDir + "/.out-of-tree/kernels/"
files, err := ioutil.ReadDir(kernelsBase)
if err != nil {
return
}
rootfs, err := genRootfsImage(dii, download)
if err != nil {
return
}
for _, k := range strings.Fields(string(rawOutput)) {
ki := config.KernelInfo{
DistroType: dii.DistroType,
DistroRelease: dii.DistroRelease,
KernelRelease: k,
ContainerName: name,
KernelPath: kernelsBase + genKernelPath(files, k),
InitrdPath: kernelsBase + genInitrdPath(files, k),
RootFS: rootfs,
}
newkcfg.Kernels = append(newkcfg.Kernels, ki)
}
return
}
func hasKernel(ki config.KernelInfo, kcfg config.KernelConfig) bool {
for _, sk := range kcfg.Kernels {
if sk == ki {
return true
}
}
return false
}
func shuffle(a []string) []string {
// FisherYates 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 generateKernels(km config.KernelMask, registry string,
commands []config.DockerCommand, max int64,
download bool) (err error) {
log.Println("Generating for kernel mask", km)
_, err = genRootfsImage(dockerImageInfo{ContainerName: km.DockerName()},
download)
if err != nil {
return
}
err = generateBaseDockerImage(registry, commands, km)
if err != nil {
return
}
var pkgs []string
switch km.DistroType {
case config.Ubuntu:
pkgs, err = matchDebianHeadersPkg(km.DockerName(),
km.ReleaseMask, true)
case config.CentOS:
pkgs, err = matchCentOSDevelPkg(km.DockerName(),
km.ReleaseMask, true)
default:
err = fmt.Errorf("%s not yet supported", km.DistroType.String())
}
if err != nil {
return
}
for i, pkg := range shuffle(pkgs) {
if max <= 0 {
log.Println("Max is reached")
break
}
log.Println(i, "/", len(pkgs), pkg)
err = dockerImageAppend(km, pkg)
if err == nil {
max--
} else {
log.Println("dockerImageAppend", err)
}
}
err = kickImage(km.DockerName())
if err != nil {
log.Println("kick image", km.DockerName(), ":", err)
return
}
err = copyKernels(km.DockerName())
if err != nil {
log.Println("copy kernels", km.DockerName(), ":", err)
return
}
return
}
func kernelAutogenHandler(workPath, registry string,
commands []config.DockerCommand,
max int64, host, download bool) (err error) {
ka, err := config.ReadArtifactConfig(workPath + "/.out-of-tree.toml")
if err != nil {
return
}
for _, sk := range ka.SupportedKernels {
if sk.DistroRelease == "" {
err = errors.New("Please set distro_release")
return
}
err = generateKernels(sk, registry, commands, max, download)
if err != nil {
return
}
}
err = updateKernelsCfg(host, download)
return
}
func kernelDockerRegenHandler(host, download bool) (err error) {
dockerImages, err := listDockerImages()
if err != nil {
return
}
for _, d := range dockerImages {
var imagePath string
imagePath, err = dockerImagePath(config.KernelMask{
DistroType: d.DistroType,
DistroRelease: d.DistroRelease,
})
if err != nil {
return
}
cmd := exec.Command("docker", "build", "-t",
d.ContainerName, imagePath)
var rawOutput []byte
rawOutput, err = cmd.CombinedOutput()
if err != nil {
log.Println("docker build:", string(rawOutput))
return
}
err = kickImage(d.ContainerName)
if err != nil {
log.Println("kick image", d.ContainerName, ":", err)
continue
}
err = copyKernels(d.ContainerName)
if err != nil {
log.Println("copy kernels", d.ContainerName, ":", err)
continue
}
}
return updateKernelsCfg(host, download)
}
func kernelGenallHandler(distro, version, registry string,
commands []config.DockerCommand, host, download bool) (err error) {
distroType, err := config.NewDistroType(distro)
if err != nil {
return
}
km := config.KernelMask{
DistroType: distroType,
DistroRelease: version,
ReleaseMask: ".*",
}
err = generateKernels(km, registry, commands, kernelsAll, download)
if err != nil {
return
}
return updateKernelsCfg(host, download)
}