d2vm/builder.go

512 lines
13 KiB
Go

// Copyright 2022 Linka Cloud All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package d2vm
import (
"context"
"fmt"
"os"
exec2 "os/exec"
"path/filepath"
"strings"
"github.com/c2h5oh/datasize"
"github.com/sirupsen/logrus"
"go.uber.org/multierr"
"go.linka.cloud/d2vm/pkg/exec"
)
const (
hosts = `127.0.0.1 localhost
# The following lines are desirable for IPv6 capable hosts
::1 ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
ff02::3 ip6-allhosts
`
syslinuxCfgUbuntu = `DEFAULT linux
SAY Now booting the kernel from SYSLINUX...
LABEL linux
KERNEL /boot/vmlinuz
APPEND ro root=UUID=%s initrd=/boot/initrd.img net.ifnames=0 console=tty0 console=ttyS0,115200n8 %s
`
syslinuxCfgDebian = `DEFAULT linux
SAY Now booting the kernel from SYSLINUX...
LABEL linux
KERNEL /vmlinuz
APPEND ro root=UUID=%s initrd=/initrd.img net.ifnames=0 console=tty0 console=ttyS0,115200n8 %s
`
syslinuxCfgAlpine = `DEFAULT linux
SAY Now booting the kernel from SYSLINUX...
LABEL linux
KERNEL /boot/vmlinuz-virt
APPEND ro root=UUID=%s rootfstype=ext4 initrd=/boot/initramfs-virt console=ttyS0,115200 %s
`
syslinuxCfgCentOS = `DEFAULT linux
SAY Now booting the kernel from SYSLINUX...
LABEL linux
KERNEL /boot/vmlinuz
APPEND ro root=UUID=%s initrd=/boot/initrd.img net.ifnames=0 console=tty0 console=ttyS0,115200n8 %s
`
)
var (
formats = []string{"qcow2", "qed", "raw", "vdi", "vhd", "vmdk"}
mbrPaths = []string{
// debian path
"/usr/lib/syslinux/mbr/mbr.bin",
// ubuntu path
"/usr/lib/EXTLINUX/mbr.bin",
// alpine path
"/usr/share/syslinux/mbr.bin",
// centos path
"/usr/share/syslinux/mbr.bin",
// archlinux path
"/usr/lib/syslinux/bios/mbr.bin",
}
)
const (
perm os.FileMode = 0644
)
func sysconfig(osRelease OSRelease) (string, error) {
switch osRelease.ID {
case ReleaseUbuntu:
if osRelease.VersionID < "20.04" {
return syslinuxCfgDebian, nil
}
return syslinuxCfgUbuntu, nil
case ReleaseDebian:
return syslinuxCfgDebian, nil
case ReleaseKali:
return syslinuxCfgDebian, nil
case ReleaseAlpine:
return syslinuxCfgAlpine, nil
case ReleaseCentOS:
return syslinuxCfgCentOS, nil
default:
return "", fmt.Errorf("%s: distribution not supported", osRelease.ID)
}
}
type Builder interface {
Build(ctx context.Context) (err error)
Close() error
}
type builder struct {
osRelease OSRelease
src string
img *image
diskRaw string
diskOut string
format string
size uint64
mntPoint string
splitBoot bool
bootSize uint64
mbrPath string
loDevice string
bootPart string
rootPart string
bootUUID string
rootUUID string
cmdLineExtra string
}
func NewBuilder(ctx context.Context, workdir, imgTag, disk string, size uint64, osRelease OSRelease, format string, cmdLineExtra string, splitBoot bool, bootSize uint64) (Builder, error) {
if err := checkDependencies(); err != nil {
return nil, err
}
f := strings.ToLower(format)
valid := false
for _, v := range formats {
if valid = v == f; valid {
break
}
}
if !valid {
return nil, fmt.Errorf("invalid format: %s valid formats are: %s", f, strings.Join(formats, " "))
}
if splitBoot && bootSize < 50 {
return nil, fmt.Errorf("boot partition size must be at least 50MiB")
}
if splitBoot && bootSize >= size {
return nil, fmt.Errorf("boot partition size must be less than the disk size")
}
mbrBin := ""
for _, v := range mbrPaths {
if _, err := os.Stat(v); err == nil {
mbrBin = v
break
}
}
if mbrBin == "" {
return nil, fmt.Errorf("unable to find syslinux's mbr.bin path")
}
if size == 0 {
size = 10 * uint64(datasize.GB)
}
if disk == "" {
disk = "disk0"
}
img, err := NewImage(ctx, imgTag, workdir)
if err != nil {
return nil, err
}
// i, err := os.Stat(imgTar)
// if err != nil {
// return nil, err
// }
// if i.Size() > size {
// s := datasize.ByteSize(math.Ceil(datasize.ByteSize(i.Size()).GBytes())) * datasize.GB
// logrus.Warnf("%s is smaller than rootfs size, using %s", datasize.ByteSize(size), s)
// size = int64(s)
// }
b := &builder{
osRelease: osRelease,
img: img,
diskRaw: filepath.Join(workdir, disk+".d2vm.raw"),
diskOut: filepath.Join(workdir, disk+"."+format),
format: f,
size: size,
mbrPath: mbrBin,
mntPoint: filepath.Join(workdir, "/mnt"),
cmdLineExtra: cmdLineExtra,
splitBoot: splitBoot,
bootSize: bootSize,
}
if err := os.MkdirAll(b.mntPoint, os.ModePerm); err != nil {
return nil, err
}
return b, nil
}
func (b *builder) Build(ctx context.Context) (err error) {
defer func() {
if err == nil {
return
}
logrus.WithError(err).Error("Build failed")
if err := b.unmountImg(context.Background()); err != nil {
logrus.WithError(err).Error("failed to unmount")
}
if err := b.cleanUp(context.Background()); err != nil {
logrus.WithError(err).Error("failed to cleanup")
}
}()
if err = b.cleanUp(ctx); err != nil {
return err
}
if err = b.makeImg(ctx); err != nil {
return err
}
if err = b.mountImg(ctx); err != nil {
return err
}
if err = b.copyRootFS(ctx); err != nil {
return err
}
if err = b.setupRootFS(ctx); err != nil {
return err
}
if err = b.installKernel(ctx); err != nil {
return err
}
if err = b.unmountImg(ctx); err != nil {
return err
}
if err = b.setupMBR(ctx); err != nil {
return err
}
if err = b.convert2Img(ctx); err != nil {
return err
}
if err = b.cleanUp(ctx); err != nil {
return err
}
return nil
}
func (b *builder) cleanUp(ctx context.Context) error {
return os.RemoveAll(b.diskRaw)
}
func (b *builder) makeImg(ctx context.Context) error {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
logrus.Infof("creating raw image")
if err := block(b.diskRaw, b.size); err != nil {
return err
}
var args []string
if b.splitBoot {
args = []string{"-s", b.diskRaw,
"mklabel", "msdos", "mkpart", "primary", "1Mib", fmt.Sprintf("%dMib", b.bootSize),
"mkpart", "primary", fmt.Sprintf("%dMib", b.bootSize), "100%",
"set", "1", "boot", "on",
}
} else {
args = []string{"-s", b.diskRaw, "mklabel", "msdos", "mkpart", "primary", "1Mib", "100%", "set", "1", "boot", "on"}
}
if err := exec.Run(ctx, "parted", args...); err != nil {
return err
}
return nil
}
func (b *builder) mountImg(ctx context.Context) error {
logrus.Infof("mounting raw image")
o, _, err := exec.RunOut(ctx, "losetup", "--show", "-f", b.diskRaw)
if err != nil {
return err
}
b.loDevice = strings.TrimSuffix(o, "\n")
if err := exec.Run(ctx, "kpartx", "-a", b.loDevice); err != nil {
return err
}
b.bootPart = fmt.Sprintf("/dev/mapper/%sp1", filepath.Base(b.loDevice))
if b.splitBoot {
b.rootPart = fmt.Sprintf("/dev/mapper/%sp2", filepath.Base(b.loDevice))
} else {
b.rootPart = b.bootPart
}
logrus.Infof("creating raw image file system")
if err := exec.Run(ctx, "mkfs.ext4", b.rootPart); err != nil {
return err
}
if err := exec.Run(ctx, "mount", b.rootPart, b.mntPoint); err != nil {
return err
}
if !b.splitBoot {
return nil
}
if err := os.MkdirAll(filepath.Join(b.mntPoint, "boot"), os.ModePerm); err != nil {
return err
}
if err := exec.Run(ctx, "mkfs.ext4", b.bootPart); err != nil {
return err
}
if err := exec.Run(ctx, "mount", b.bootPart, filepath.Join(b.mntPoint, "boot")); err != nil {
return err
}
return nil
}
func (b *builder) unmountImg(ctx context.Context) error {
logrus.Infof("unmounting raw image")
var merr error
if b.splitBoot {
if err := exec.Run(ctx, "umount", filepath.Join(b.mntPoint, "boot")); err != nil {
merr = multierr.Append(merr, err)
}
}
if err := exec.Run(ctx, "umount", b.mntPoint); err != nil {
merr = multierr.Append(merr, err)
}
if err := exec.Run(ctx, "kpartx", "-d", b.loDevice); err != nil {
merr = multierr.Append(merr, err)
}
if err := exec.Run(ctx, "losetup", "-d", b.loDevice); err != nil {
merr = multierr.Append(merr, err)
}
return merr
}
func (b *builder) copyRootFS(ctx context.Context) error {
logrus.Infof("copying rootfs to raw image")
if err := b.img.Flatten(ctx, b.mntPoint); err != nil {
return err
}
return nil
}
func diskUUID(ctx context.Context, disk string) (string, error) {
o, _, err := exec.RunOut(ctx, "blkid", "-s", "UUID", "-o", "value", disk)
if err != nil {
return "", err
}
return strings.TrimSuffix(o, "\n"), nil
}
func (b *builder) setupRootFS(ctx context.Context) (err error) {
logrus.Infof("setting up rootfs")
b.rootUUID, err = diskUUID(ctx, b.rootPart)
var fstab string
if b.splitBoot {
b.bootUUID, err = diskUUID(ctx, b.bootPart)
if err != nil {
return err
}
fstab = fmt.Sprintf("UUID=%s / ext4 errors=remount-ro 0 1\nUUID=%s /boot ext4 errors=remount-ro 0 2\n", b.rootUUID, b.bootUUID)
} else {
b.bootUUID = b.rootUUID
fstab = fmt.Sprintf("UUID=%s / ext4 errors=remount-ro 0 1\n", b.bootUUID)
}
if err := b.chWriteFile("/etc/fstab", fstab, perm); err != nil {
return err
}
if err := b.chWriteFileIfNotExist("/etc/resolv.conf", "nameserver 8.8.8.8", 0644); err != nil {
return err
}
if err := b.chWriteFileIfNotExist("/etc/hostname", "localhost", perm); err != nil {
return err
}
if err := b.chWriteFileIfNotExist("/etc/hosts", hosts, perm); err != nil {
return err
}
// TODO(adphi): is it the righ fix ?
if err := os.RemoveAll("/usr/sbin/policy-rc.d"); err != nil {
return err
}
if err := os.RemoveAll(b.chPath("/.dockerenv")); err != nil {
return err
}
// create a symlink to /boot for non-alpine images in order to have a consistent path
// even if the image is not split
if _, err := os.Stat(b.chPath("/boot/boot")); os.IsNotExist(err) {
if err := os.Symlink(".", b.chPath("/boot/boot")); err != nil {
return err
}
}
switch b.osRelease.ID {
case ReleaseAlpine:
by, err := os.ReadFile(b.chPath("/etc/inittab"))
if err != nil {
return err
}
by = append(by, []byte("\n"+"ttyS0::respawn:/sbin/getty -L ttyS0 115200 vt100\n")...)
if err := b.chWriteFile("/etc/inittab", string(by), perm); err != nil {
return err
}
if err := b.chWriteFileIfNotExist("/etc/network/interfaces", "", perm); err != nil {
return err
}
return nil
case ReleaseUbuntu:
if b.osRelease.VersionID >= "20.04" {
return nil
}
fallthrough
case ReleaseDebian, ReleaseKali:
t, err := os.Readlink(b.chPath("/vmlinuz"))
if err != nil {
return err
}
if err := os.Symlink(t, b.chPath("/boot/vmlinuz")); err != nil {
return err
}
t, err = os.Readlink(b.chPath("/initrd.img"))
if err != nil {
return err
}
if err := os.Symlink(t, b.chPath("/boot/initrd.img")); err != nil {
return err
}
return nil
default:
return nil
}
}
func (b *builder) installKernel(ctx context.Context) error {
logrus.Infof("installing linux kernel")
if err := exec.Run(ctx, "extlinux", "--install", b.chPath("/boot")); err != nil {
return err
}
sysconfig, err := sysconfig(b.osRelease)
if err != nil {
return err
}
if err := b.chWriteFile("/boot/syslinux.cfg", fmt.Sprintf(sysconfig, b.rootUUID, b.cmdLineExtra), perm); err != nil {
return err
}
return nil
}
func (b *builder) setupMBR(ctx context.Context) error {
logrus.Infof("writing MBR")
if err := exec.Run(ctx, "dd", fmt.Sprintf("if=%s", b.mbrPath), fmt.Sprintf("of=%s", b.diskRaw), "bs=440", "count=1", "conv=notrunc"); err != nil {
return err
}
return nil
}
func (b *builder) convert2Img(ctx context.Context) error {
logrus.Infof("converting to %s", b.format)
return exec.Run(ctx, "qemu-img", "convert", b.diskRaw, "-O", b.format, b.diskOut)
}
func (b *builder) chWriteFile(path string, content string, perm os.FileMode) error {
return os.WriteFile(b.chPath(path), []byte(content), perm)
}
func (b *builder) chWriteFileIfNotExist(path string, content string, perm os.FileMode) error {
if i, err := os.Stat(b.chPath(path)); err == nil && i.Size() != 0 {
return nil
}
return os.WriteFile(b.chPath(path), []byte(content), perm)
}
func (b *builder) chPath(path string) string {
return fmt.Sprintf("%s%s", b.mntPoint, path)
}
func (b *builder) Close() error {
return b.img.Close()
}
func block(path string, size uint64) error {
f, err := os.Create(path)
if err != nil {
return err
}
defer f.Close()
return f.Truncate(int64(size))
}
func checkDependencies() error {
var merr error
for _, v := range []string{"mount", "blkid", "tar", "losetup", "parted", "partprobe", "qemu-img", "extlinux", "dd", "mkfs"} {
if _, err := exec2.LookPath(v); err != nil {
merr = multierr.Append(merr, err)
}
}
return merr
}
func OutputFormats() []string {
return formats[:]
}