add grub-efi support

* tests: increase timeout
* ci: split e2e tests

Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
This commit is contained in:
Adphi 2023-09-12 13:59:11 +02:00
parent d4c3476031
commit a41bbdb745
Signed by: adphi
GPG Key ID: F2159213400E50AB
29 changed files with 524 additions and 162 deletions

View File

@ -45,9 +45,17 @@ jobs:
- name: Run tests
run: git --no-pager diff --exit-code HEAD~1 HEAD **/**.go templates/ || make tests
e2e-tests:
name: End to end Tests
templates-tests:
name: Test Templates
runs-on: ubuntu-latest
strategy:
matrix:
image:
- ubuntu
- debian
- kalilinux
- alpine
- centos
steps:
- name: Checkout
@ -70,6 +78,53 @@ jobs:
- name: Setup dependencies
run: sudo apt update && sudo apt install -y util-linux udev parted e2fsprogs mount tar extlinux qemu-utils qemu-system
- name: Share cache with other actions
uses: actions/cache@v2
with:
path: |
~/go/pkg/mod
/tmp/.buildx-cache
key: ${{ runner.os }}-tests-${{ github.sha }}
restore-keys: |
${{ runner.os }}-tests-
- name: Run tests
run: git --no-pager diff --exit-code HEAD~1 HEAD **/**.go templates/ || IMAGE=${{ matrix.image }} make test-templates
e2e-tests:
name: End to end Tests
runs-on: ubuntu-latest
strategy:
matrix:
image:
- alpine:3.17
- ubuntu:20.04
- ubuntu:22.04
- debian:10
- debian:11
- centos:8
steps:
- name: Checkout
uses: actions/checkout@v3
with:
# fetching all tags is required for the Makefile to compute the right version
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: "1.20"
- name: Set up QEMU dependency
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Setup dependencies
run: sudo apt update && sudo apt install -y util-linux udev parted e2fsprogs mount tar extlinux qemu-utils qemu-system ovmf
- name: Share cache with other actions
uses: actions/cache@v2
with:
@ -81,7 +136,7 @@ jobs:
${{ runner.os }}-tests-
- name: Run end-to-end tests
run: make e2e
run: E2E_IMAGES=${{ matrix.image }} make e2e
docs-up-to-date:
name: Docs up to date
@ -224,6 +279,7 @@ jobs:
if: startsWith(github.event.ref, 'refs/tags/v')
needs:
- tests
- templates-tests
- docs-up-to-date
- build
- e2e-tests

View File

@ -39,7 +39,6 @@ RUN apt-get update && \
mount \
tar \
extlinux \
grub2 \
cryptsetup-bin \
qemu-utils && \
apt-get clean && \

View File

@ -64,10 +64,15 @@ docker-run:
.PHONY: tests
tests:
@go generate ./...
@go list .| xargs go test -exec sudo -count=1 -timeout 20m -v
@go list .| xargs go test -exec sudo -count=1 -timeout 60m -v -skip TestConfig
.PHONY: test-templates
test-templates:
@go generate ./...
@go test -exec sudo -count=1 -timeout 60m -v -run TestConfig/$(IMAGE)
e2e: docker-build .build
@go test -v -exec sudo -count=1 -timeout 60m -ldflags "-X '$(MODULE).Version=$(VERSION)' -X '$(MODULE).BuildDate=$(shell date)'" ./e2e
@go test -v -exec sudo -count=1 -timeout 60m -ldflags "-X '$(MODULE).Version=$(VERSION)' -X '$(MODULE).BuildDate=$(shell date)'" ./e2e -args -images $(E2E_IMAGES)
docs-up-to-date:
@$(MAKE) cli-docs

View File

@ -61,7 +61,6 @@ Obviously, **Distroless** images are not supported.
- mount
- tar
- extlinux (when using syslinux)
- grub2 (when using grub)
- qemu-utils
- cryptsetup (when using LUKS)
- [QEMU](https://www.qemu.org/download/#linux) (optional)
@ -161,7 +160,7 @@ Flags:
--append-to-cmdline string Extra kernel cmdline arguments to append to the generated one
--boot-fs string Filesystem to use for the boot partition, ext4 or fat32
--boot-size uint Size of the boot partition in MB (default 100)
--bootloader string Bootloader to use: syslinux, grub (default "syslinux")
--bootloader string Bootloader to use: syslinux, grub, grub-bios, grub-efi (default "syslinux")
--force Override output qcow2 image
-h, --help help for convert
--keep-cache Keep the images after the build
@ -318,7 +317,7 @@ Flags:
--append-to-cmdline string Extra kernel cmdline arguments to append to the generated one
--boot-fs string Filesystem to use for the boot partition, ext4 or fat32
--boot-size uint Size of the boot partition in MB (default 100)
--bootloader string Bootloader to use: syslinux, grub (default "syslinux")
--bootloader string Bootloader to use: syslinux, grub, grub-bios, grub-efi (default "syslinux")
--build-arg stringArray Set build-time variables
-f, --file string Name of the Dockerfile
--force Override output qcow2 image

View File

@ -38,5 +38,6 @@ type BootloaderProvider interface {
}
type Bootloader interface {
Validate(fs BootFS) error
Setup(ctx context.Context, dev, root, cmdline string) error
}

View File

@ -130,7 +130,7 @@ func NewBuilder(ctx context.Context, workdir, imgTag, disk string, size uint64,
}
if bootFS == "" {
bootFS = FSExt4
bootFS = BootFSExt4
}
if err := bootFS.Validate(); err != nil {
@ -146,6 +146,10 @@ func NewBuilder(ctx context.Context, workdir, imgTag, disk string, size uint64,
return nil, err
}
if err := bl.Validate(bootFS); err != nil {
return nil, err
}
if size == 0 {
size = 10 * uint64(datasize.GB)
}

View File

@ -57,6 +57,18 @@ func validateFlags() error {
logrus.Warnf("boot filesystem is set: enabling split boot")
splitBoot = true
}
efi := bootloader == "grub-efi" || bootloader == "grub"
if efi && !splitBoot {
logrus.Warnf("grub-efi bootloader is set: enabling split boot")
splitBoot = true
}
if efi && bootFS != "" && bootFS != "fat32" {
return fmt.Errorf("grub-efi bootloader only supports fat32 boot filesystem")
}
if efi && bootFS == "" {
logrus.Warnf("grub-efi bootloader is set: enabling fat32 boot filesystem")
bootFS = "fat32"
}
if push && tag == "" {
return fmt.Errorf("tag is required when pushing container disk image")
}
@ -82,7 +94,7 @@ func buildFlags() *pflag.FlagSet {
flags.BoolVar(&splitBoot, "split-boot", false, "Split the boot partition from the root partition")
flags.Uint64Var(&bootSize, "boot-size", 100, "Size of the boot partition in MB")
flags.StringVar(&bootFS, "boot-fs", "", "Filesystem to use for the boot partition, ext4 or fat32")
flags.StringVar(&bootloader, "bootloader", "syslinux", "Bootloader to use: syslinux, grub")
flags.StringVar(&bootloader, "bootloader", "syslinux", "Bootloader to use: syslinux, grub, grub-bios, grub-efi")
flags.StringVar(&luksPassword, "luks-password", "", "Password to use for the LUKS encrypted root partition. If not set, the root partition will not be encrypted")
flags.BoolVar(&keepCache, "keep-cache", false, "Keep the images after the build")
return flags

View File

@ -28,6 +28,7 @@ var (
arch string
cpus uint
mem uint
bios string
qemuCmd string
qemuDetached bool
networking string
@ -71,6 +72,8 @@ func init() {
flags.UintVar(&cpus, "cpus", 1, "Number of CPUs")
flags.UintVar(&mem, "mem", 1024, "Amount of memory in MB")
flags.StringVar(&bios, "bios", "", "Path to the optional bios binary")
// Backend configuration
flags.StringVar(&qemuCmd, "qemu", "", "Path to the qemu binary (otherwise look in $PATH)")
flags.BoolVar(&qemuDetached, "detached", false, "Set qemu container to run in the background")
@ -105,6 +108,7 @@ func Qemu(cmd *cobra.Command, args []string) {
qemu.WithStdin(os.Stdin),
qemu.WithStdout(os.Stdout),
qemu.WithStderr(os.Stderr),
qemu.WithBios(bios),
}
if enableGUI {
opts = append(opts, qemu.WithGUI())

View File

@ -29,19 +29,22 @@ import (
"go.linka.cloud/d2vm/pkg/exec"
)
func testConfig(t *testing.T, ctx context.Context, img string, config Config) {
func testConfig(t *testing.T, ctx context.Context, name, img string, config Config, luks, grubBIOS, grubEFI bool) {
require.NoError(t, docker.Pull(ctx, img))
tmpPath := filepath.Join(os.TempDir(), "d2vm-tests", strings.NewReplacer(":", "-", ".", "-").Replace(img))
tmpPath := filepath.Join(os.TempDir(), "d2vm-tests", strings.NewReplacer(":", "-", ".", "-").Replace(name))
require.NoError(t, os.MkdirAll(tmpPath, 0755))
defer os.RemoveAll(tmpPath)
logrus.Infof("inspecting image %s", img)
r, err := FetchDockerImageOSRelease(ctx, img, tmpPath)
r, err := FetchDockerImageOSRelease(ctx, img)
require.NoError(t, err)
defer docker.Remove(ctx, img)
d, err := NewDockerfile(r, img, "root", "", false, false)
if !r.SupportsLUKS() && luks {
t.Skipf("LUKS not supported for %s", r.Version)
}
d, err := NewDockerfile(r, img, "root", "", luks, grubBIOS, grubEFI)
require.NoError(t, err)
logrus.Infof("docker image based on %s", d.Release.Name)
p := filepath.Join(tmpPath, docker.FormatImgName(img))
p := filepath.Join(tmpPath, docker.FormatImgName(name))
dir := filepath.Dir(p)
f, err := os.Create(p)
require.NoError(t, err)
@ -51,6 +54,10 @@ func testConfig(t *testing.T, ctx context.Context, img string, config Config) {
logrus.Infof("building kernel enabled image")
require.NoError(t, docker.Build(ctx, imgUUID, p, dir))
defer docker.Remove(ctx, imgUUID)
// we don't need to test the kernel location if grub is enabled
if grubBIOS || grubEFI {
return
}
require.NoError(t, docker.RunAndRemove(ctx, imgUUID, "test", "-f", config.Kernel))
require.NoError(t, docker.RunAndRemove(ctx, imgUUID, "test", "-f", config.Initrd))
}
@ -116,13 +123,35 @@ func TestConfig(t *testing.T) {
}
exec.SetDebug(true)
names := []string{"luks", "grub-bios", "grub-efi"}
bools := []bool{false, true}
for _, test := range tests {
test := test
t.Run(test.image, func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
testConfig(t, ctx, test.image, test.config)
for _, luks := range bools {
for _, grubBIOS := range bools {
for _, grubEFI := range bools {
luks := luks
grubBIOS := grubBIOS
grubEFI := grubEFI
n := []string{test.image}
for i, v := range []bool{luks, grubBIOS, grubEFI} {
if v {
n = append(n, names[i])
}
}
name := strings.Join(n, "-")
t.Run(name, func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
testConfig(t, ctx, name, test.image, test.config, luks, grubBIOS, grubEFI)
})
}
}
}
})
}
}

View File

@ -41,7 +41,7 @@ func Convert(ctx context.Context, img string, opts ...ConvertOption) error {
defer os.RemoveAll(tmpPath)
logrus.Infof("inspecting image %s", img)
r, err := FetchDockerImageOSRelease(ctx, img, tmpPath)
r, err := FetchDockerImageOSRelease(ctx, img)
if err != nil {
return err
}
@ -51,7 +51,7 @@ func Convert(ctx context.Context, img string, opts ...ConvertOption) error {
}
if !o.raw {
d, err := NewDockerfile(r, img, o.password, o.networkManager, o.luksPassword != "", o.bootLoader == "grub")
d, err := NewDockerfile(r, img, o.password, o.networkManager, o.luksPassword != "", o.hasGrubBIOS(), o.hasGrubEFI())
if err != nil {
return err
}

View File

@ -34,6 +34,14 @@ type convertOptions struct {
keepCache bool
}
func (o *convertOptions) hasGrubBIOS() bool {
return o.bootLoader == "grub" || o.bootLoader == "grub-bios"
}
func (o *convertOptions) hasGrubEFI() bool {
return o.bootLoader == "grub" || o.bootLoader == "grub-efi"
}
func WithSize(size uint64) ConvertOption {
return func(o *convertOptions) {
o.size = size

View File

@ -65,16 +65,21 @@ type Dockerfile struct {
Release OSRelease
NetworkManager NetworkManager
Luks bool
Grub bool
GrubBIOS bool
GrubEFI bool
tmpl *template.Template
}
func (d Dockerfile) Grub() bool {
return d.GrubBIOS || d.GrubEFI
}
func (d Dockerfile) Render(w io.Writer) error {
return d.tmpl.Execute(w, d)
}
func NewDockerfile(release OSRelease, img, password string, networkManager NetworkManager, luks, grub bool) (Dockerfile, error) {
d := Dockerfile{Release: release, Image: img, Password: password, NetworkManager: networkManager, Luks: luks, Grub: grub}
func NewDockerfile(release OSRelease, img, password string, networkManager NetworkManager, luks, grubBIOS, grubEFI bool) (Dockerfile, error) {
d := Dockerfile{Release: release, Image: img, Password: password, NetworkManager: networkManager, Luks: luks, GrubBIOS: grubBIOS, GrubEFI: grubEFI}
var net NetworkManager
switch release.ID {
case ReleaseDebian:

View File

@ -12,7 +12,7 @@ d2vm build [context directory] [flags]
--append-to-cmdline string Extra kernel cmdline arguments to append to the generated one
--boot-fs string Filesystem to use for the boot partition, ext4 or fat32
--boot-size uint Size of the boot partition in MB (default 100)
--bootloader string Bootloader to use: syslinux, grub (default "syslinux")
--bootloader string Bootloader to use: syslinux, grub, grub-bios, grub-efi (default "syslinux")
--build-arg stringArray Set build-time variables
-f, --file string Name of the Dockerfile
--force Override output qcow2 image

View File

@ -12,7 +12,7 @@ d2vm convert [docker image] [flags]
--append-to-cmdline string Extra kernel cmdline arguments to append to the generated one
--boot-fs string Filesystem to use for the boot partition, ext4 or fat32
--boot-size uint Size of the boot partition in MB (default 100)
--bootloader string Bootloader to use: syslinux, grub (default "syslinux")
--bootloader string Bootloader to use: syslinux, grub, grub-bios, grub-efi (default "syslinux")
--force Override output qcow2 image
-h, --help help for convert
--keep-cache Keep the images after the build

View File

@ -11,6 +11,7 @@ d2vm run qemu [options] [image-path] [flags]
```
--accel string Choose acceleration mode. Use 'tcg' to disable it. (default "kvm:tcg")
--arch string Type of architecture to use, e.g. x86_64, aarch64, s390x (default "x86_64")
--bios string Path to the optional bios binary
--cpus uint Number of CPUs (default 1)
--detached Set qemu container to run in the background
--disk disk Disk config, may be repeated. [file=]path[,size=1G][,format=qcow2] (default [])

View File

@ -18,6 +18,7 @@ import (
"bufio"
"bytes"
"context"
"flag"
"io"
"os"
"path/filepath"
@ -36,6 +37,7 @@ import (
type test struct {
name string
args []string
efi bool
}
type img struct {
@ -43,14 +45,24 @@ type img struct {
luks string
}
var images = []img{
{name: "alpine:3.17", luks: "Enter passphrase for /dev/sda2:"},
{name: "ubuntu:20.04", luks: "Please unlock disk root:"},
{name: "ubuntu:22.04", luks: "Please unlock disk root:"},
{name: "debian:10", luks: "Please unlock disk root:"},
{name: "debian:11", luks: "Please unlock disk root:"},
{name: "centos:8", luks: "Please enter passphrase for disk"},
}
var (
images = []img{
{name: "alpine:3.17", luks: "Enter passphrase for /dev/sda2:"},
{name: "ubuntu:20.04", luks: "Please unlock disk root:"},
{name: "ubuntu:22.04", luks: "Please unlock disk root:"},
{name: "debian:10", luks: "Please unlock disk root:"},
{name: "debian:11", luks: "Please unlock disk root:"},
{name: "centos:8", luks: "Please enter passphrase for disk"},
}
imgNames = func() []string {
var imgs []string
for _, img := range images {
imgs = append(imgs, img.name)
}
return imgs
}()
imgs = flag.String("images", "", "comma separated list of images to test, must be one of: "+strings.Join(imgNames, ","))
)
func TestConvert(t *testing.T) {
require := require2.New(t)
@ -62,6 +74,10 @@ func TestConvert(t *testing.T) {
name: "split-boot",
args: []string{"--split-boot"},
},
{
name: "fat32",
args: []string{"--split-boot", "--boot-fs=fat32"},
},
{
name: "luks",
args: []string{"--luks-password=root"},
@ -69,13 +85,30 @@ func TestConvert(t *testing.T) {
{
name: "grub",
args: []string{"--bootloader=grub"},
efi: true,
},
{
name: "grub-luks",
args: []string{"--bootloader=grub", "--luks-password=root"},
efi: true,
},
}
var testImgs []img
imgs:
for _, v := range strings.Split(*imgs, ",") {
for _, img := range images {
if img.name == v {
testImgs = append(testImgs, img)
continue imgs
}
}
t.Fatalf("invalid image: %q, valid images: %s", v, strings.Join(imgNames, ","))
}
if len(testImgs) == 0 {
testImgs = images
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@ -83,7 +116,7 @@ func TestConvert(t *testing.T) {
require.NoError(os.MkdirAll(dir, os.ModePerm))
defer os.RemoveAll(dir)
for _, img := range images {
for _, img := range testImgs {
t.Run(img.name, func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
@ -161,7 +194,11 @@ func TestConvert(t *testing.T) {
cancel()
}
}()
if err := qemu.Run(ctx, out, qemu.WithStdin(inr), qemu.WithStdout(io.MultiWriter(outw, os.Stdout)), qemu.WithStderr(io.Discard), qemu.WithMemory(2048)); err != nil && !success.Load() {
opts := []qemu.Option{qemu.WithStdin(inr), qemu.WithStdout(io.MultiWriter(outw, os.Stdout)), qemu.WithStderr(io.Discard), qemu.WithMemory(2048), qemu.WithCPUs(2)}
if tt.efi {
opts = append(opts, qemu.WithBios("/usr/share/ovmf/OVMF.fd"))
}
if err := qemu.Run(ctx, out, opts...); err != nil && !success.Load() {
t.Fatalf("failed to run qemu: %v", err)
}
})

10
fs.go
View File

@ -21,8 +21,8 @@ import (
type BootFS string
const (
FSExt4 BootFS = "ext4"
FSFat32 BootFS = "fat32"
BootFSExt4 BootFS = "ext4"
BootFSFat32 BootFS = "fat32"
)
func (f BootFS) String() string {
@ -30,11 +30,11 @@ func (f BootFS) String() string {
}
func (f BootFS) IsExt() bool {
return f == FSExt4
return f == BootFSExt4
}
func (f BootFS) IsFat() bool {
return f == FSFat32
return f == BootFSFat32
}
func (f BootFS) IsSupported() bool {
@ -50,7 +50,7 @@ func (f BootFS) Validate() error {
func (f BootFS) linux() string {
switch f {
case FSFat32:
case BootFSFat32:
return "vfat"
default:
return "ext4"

77
grub.go
View File

@ -17,89 +17,54 @@ package d2vm
import (
"context"
"fmt"
"os"
exec2 "os/exec"
"path/filepath"
"github.com/sirupsen/logrus"
"go.linka.cloud/d2vm/pkg/exec"
)
const grubCfg = `GRUB_DEFAULT=0
GRUB_HIDDEN_TIMEOUT=0
GRUB_HIDDEN_TIMEOUT_QUIET=true
GRUB_TIMEOUT=0
GRUB_CMDLINE_LINUX_DEFAULT="%s"
GRUB_CMDLINE_LINUX=""
GRUB_TERMINAL=console
`
type grub struct {
name string
c Config
r OSRelease
*grubCommon
}
func (g grub) Validate(fs BootFS) error {
switch fs {
case BootFSFat32:
return nil
default:
return fmt.Errorf("grub only supports fat32 boot filesystem due to grub-efi")
}
}
func (g grub) Setup(ctx context.Context, dev, root string, cmdline string) error {
logrus.Infof("setting up grub bootloader")
if err := os.WriteFile(filepath.Join(root, "etc", "default", "grub"), []byte(fmt.Sprintf(grubCfg, cmdline)), perm); err != nil {
clean, err := g.prepare(ctx, dev, root, cmdline)
if err != nil {
return err
}
if err := os.MkdirAll(filepath.Join(root, "boot", g.name), os.ModePerm); err != nil {
defer clean()
if err := g.install(ctx, "--target=x86_64-efi", "--efi-directory=/boot", "--no-nvram", "--removable", "--no-floppy"); err != nil {
return err
}
mounts := []string{"dev", "proc", "sys"}
var unmounts []string
defer func() {
for _, v := range unmounts {
if err := exec.Run(ctx, "umount", filepath.Join(root, v)); err != nil {
logrus.Errorf("failed to unmount /%s: %s", v, err)
}
}
}()
for _, v := range mounts {
if err := exec.Run(ctx, "mount", "-o", "bind", "/"+v, filepath.Join(root, v)); err != nil {
return err
}
unmounts = append(unmounts, v)
}
if err := exec.Run(ctx, "chroot", root, g.name+"-install", "--target=i386-pc", "--boot-directory", "/boot", dev); err != nil {
if err := g.install(ctx, "--target=i386-pc", "--boot-directory=/boot", dev); err != nil {
return err
}
if err := exec.Run(ctx, "chroot", root, g.name+"-mkconfig", "-o", "/boot/"+g.name+"/grub.cfg"); err != nil {
if err := g.mkconfig(ctx); err != nil {
return err
}
return nil
}
type grubBootloaderProvider struct {
type grubProvider struct {
config Config
}
func (g grubBootloaderProvider) New(c Config, r OSRelease) (Bootloader, error) {
name := "grub"
if r.ID == "centos" {
name = "grub2"
}
if _, err := exec2.LookPath("grub-install"); err != nil {
return nil, err
}
if _, err := exec2.LookPath("grub-mkconfig"); err != nil {
return nil, err
}
return grub{
name: name,
c: c,
r: r,
}, nil
func (g grubProvider) New(c Config, r OSRelease) (Bootloader, error) {
return grub{grubCommon: newGrubCommon(c, r)}, nil
}
func (g grubBootloaderProvider) Name() string {
func (g grubProvider) Name() string {
return "grub"
}
func init() {
RegisterBootloaderProvider(grubBootloaderProvider{})
RegisterBootloaderProvider(grubProvider{})
}

61
grub_bios.go Normal file
View File

@ -0,0 +1,61 @@
// Copyright 2023 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"
"github.com/sirupsen/logrus"
)
type grubBios struct {
*grubCommon
}
func (g grubBios) Validate(_ BootFS) error {
return nil
}
func (g grubBios) Setup(ctx context.Context, dev, root string, cmdline string) error {
logrus.Infof("setting up grub bootloader")
clean, err := g.prepare(ctx, dev, root, cmdline)
if err != nil {
return err
}
defer clean()
if err := g.install(ctx, "--target=i386-pc", "--boot-directory=/boot", dev); err != nil {
return err
}
if err := g.mkconfig(ctx); err != nil {
return err
}
return nil
}
type grubBiosProvider struct {
config Config
}
func (g grubBiosProvider) New(c Config, r OSRelease) (Bootloader, error) {
return grubBios{grubCommon: newGrubCommon(c, r)}, nil
}
func (g grubBiosProvider) Name() string {
return "grub-bios"
}
func init() {
RegisterBootloaderProvider(grubBiosProvider{})
}

102
grub_common.go Normal file
View File

@ -0,0 +1,102 @@
// Copyright 2023 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"
"path/filepath"
"github.com/sirupsen/logrus"
"go.linka.cloud/d2vm/pkg/exec"
)
const grubCfg = `GRUB_DEFAULT=0
GRUB_HIDDEN_TIMEOUT=0
GRUB_HIDDEN_TIMEOUT_QUIET=true
GRUB_TIMEOUT=0
GRUB_CMDLINE_LINUX_DEFAULT="%s"
GRUB_CMDLINE_LINUX=""
GRUB_TERMINAL=console
`
type grubCommon struct {
name string
c Config
r OSRelease
root string
dev string
}
func newGrubCommon(c Config, r OSRelease) *grubCommon {
name := "grub"
if r.ID == "centos" {
name = "grub2"
}
return &grubCommon{
name: name,
c: c,
r: r,
}
}
func (g *grubCommon) prepare(ctx context.Context, dev, root, cmdline string) (clean func(), err error) {
g.dev = dev
g.root = root
if err = os.WriteFile(filepath.Join(root, "etc", "default", "grub"), []byte(fmt.Sprintf(grubCfg, cmdline)), perm); err != nil {
return
}
if err = os.MkdirAll(filepath.Join(root, "boot", g.name), os.ModePerm); err != nil {
return
}
mounts := []string{"dev", "proc", "sys"}
var unmounts []string
clean = func() {
for _, v := range unmounts {
if err := exec.Run(ctx, "umount", filepath.Join(root, v)); err != nil {
logrus.Errorf("failed to unmount /%s: %s", v, err)
}
}
}
defer func() {
if err != nil {
clean()
}
}()
for _, v := range mounts {
if err = exec.Run(ctx, "mount", "-o", "bind", "/"+v, filepath.Join(root, v)); err != nil {
return
}
unmounts = append(unmounts, v)
}
return
}
func (g *grubCommon) install(ctx context.Context, args ...string) error {
if g.dev == "" || g.root == "" {
return fmt.Errorf("grubCommon not prepared")
}
args = append([]string{g.root, g.name + "-install"}, args...)
return exec.Run(ctx, "chroot", args...)
}
func (g *grubCommon) mkconfig(ctx context.Context) error {
if g.dev == "" || g.root == "" {
return fmt.Errorf("grubCommon not prepared")
}
return exec.Run(ctx, "chroot", g.root, g.name+"-mkconfig", "-o", "/boot/"+g.name+"/grub.cfg")
}

67
grub_efi.go Normal file
View File

@ -0,0 +1,67 @@
// Copyright 2023 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"
"github.com/sirupsen/logrus"
)
type grubEFI struct {
*grubCommon
}
func (g grubEFI) Validate(fs BootFS) error {
switch fs {
case BootFSFat32:
return nil
default:
return fmt.Errorf("grub-efi only supports fat32 boot filesystem")
}
}
func (g grubEFI) Setup(ctx context.Context, dev, root string, cmdline string) error {
logrus.Infof("setting up grub-efi bootloader")
clean, err := g.prepare(ctx, dev, root, cmdline)
if err != nil {
return err
}
defer clean()
if err := g.install(ctx, "--target=x86_64-efi", "--efi-directory=/boot", "--no-nvram", "--removable", "--no-floppy"); err != nil {
return err
}
if err := g.mkconfig(ctx); err != nil {
return err
}
return nil
}
type grubEFIProvider struct {
config Config
}
func (g grubEFIProvider) New(c Config, r OSRelease) (Bootloader, error) {
return grubEFI{grubCommon: newGrubCommon(c, r)}, nil
}
func (g grubEFIProvider) Name() string {
return "grub-efi"
}
func init() {
RegisterBootloaderProvider(grubEFIProvider{})
}

View File

@ -16,12 +16,8 @@ package d2vm
import (
"context"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"text/template"
"github.com/joho/godotenv"
"github.com/sirupsen/logrus"
@ -107,40 +103,8 @@ func ParseOSRelease(s string) (OSRelease, error) {
return o, nil
}
const (
osReleaseDockerfile = `
FROM {{ . }}
ENTRYPOINT [""]
CMD ["/bin/cat", "/etc/os-release"]
`
)
var (
osReleaseDockerfileTemplate = template.Must(template.New("osrelease.Dockerfile").Parse(osReleaseDockerfile))
)
func FetchDockerImageOSRelease(ctx context.Context, img string, tmpPath string) (OSRelease, error) {
d := filepath.Join(tmpPath, "osrelease.Dockerfile")
f, err := os.Create(d)
if err != nil {
return OSRelease{}, err
}
defer f.Close()
if err := osReleaseDockerfileTemplate.Execute(f, img); err != nil {
return OSRelease{}, err
}
imgTag := fmt.Sprintf("os-release-%s", img)
if err := docker.Cmd(ctx, "image", "build", "-t", imgTag, "-f", d, tmpPath); err != nil {
return OSRelease{}, err
}
defer func() {
if err := docker.Cmd(ctx, "image", "rm", imgTag); err != nil {
logrus.WithError(err).Error("failed to cleanup OSRelease Docker Image")
}
}()
o, _, err := docker.CmdOut(ctx, "run", "--rm", "-i", imgTag)
func FetchDockerImageOSRelease(ctx context.Context, img string) (OSRelease, error) {
o, _, err := docker.CmdOut(ctx, "run", "--rm", "-i", "--entrypoint", "cat", img, "/etc/os-release")
if err != nil {
return OSRelease{}, err
}

View File

@ -44,6 +44,7 @@ type config struct {
arch string
cpus uint
memory uint
bios string
accel string
detached bool
qemuBinPath string
@ -92,6 +93,12 @@ func WithMemory(memory uint) Option {
}
}
func WithBios(bios string) Option {
return func(c *config) {
c.bios = bios
}
}
func WithAccel(accel string) Option {
return func(c *config) {
c.accel = accel

View File

@ -197,6 +197,10 @@ func (c *config) buildQemuCmdline() ([]string, error) {
qemuArgs = append(qemuArgs, "-m", fmt.Sprintf("%d", c.memory))
qemuArgs = append(qemuArgs, "-uuid", c.uuid.String())
if c.bios != "" {
qemuArgs = append(qemuArgs, "-bios", c.bios)
}
// Need to specify the vcpu type when running qemu on arm64 platform, for security reason,
// the vcpu should be "host" instead of other names such as "cortex-a53"...
if c.arch == "aarch64" {

View File

@ -50,6 +50,10 @@ type syslinux struct {
mbrBin string
}
func (s syslinux) Validate(_ BootFS) error {
return nil
}
func (s syslinux) Setup(ctx context.Context, dev, root string, cmdline string) error {
logrus.Infof("setting up syslinux bootloader")
if err := exec.Run(ctx, "extlinux", "--install", filepath.Join(root, "boot")); err != nil {

View File

@ -2,8 +2,7 @@ FROM {{ .Image }}
USER root
RUN apk update --no-cache && \
apk add \
RUN apk add --no-cache \
util-linux \
linux-virt \
{{- if ge .Release.VersionID "3.17" }}
@ -31,13 +30,21 @@ iface eth0 inet dhcp\n\
' > /etc/network/interfaces
{{ end }}
{{- if .Luks }}
{{ if .Luks }}
RUN apk add --no-cache cryptsetup && \
source /etc/mkinitfs/mkinitfs.conf && \
echo "features=\"${features} cryptsetup\"" > /etc/mkinitfs/mkinitfs.conf && \
mkinitfs $(ls /lib/modules)
{{- end }}
{{- if .Grub }} \
RUN apk add --no-cache grub grub-bios
# we need to keep that at the end, because after it, we can't install packages without error anymore due to grub hooks
{{- if .Grub }}
RUN apk add --no-cache \
{{- if .GrubBIOS }}
grub-bios \
{{- end }}
{{- if .GrubEFI }}
grub-efi \
{{- end }}
grub
{{- end }}

View File

@ -7,24 +7,23 @@ RUN sed -i 's/mirrorlist/#mirrorlist/g' /etc/yum.repos.d/CentOS-* && \
RUN yum update -y
# See https://bugzilla.redhat.com/show_bug.cgi?id=1917213
RUN yum install -y \
kernel \
systemd \
NetworkManager \
{{- if .Grub }}
{{- if .GrubBIOS }}
grub2 \
{{- end }}
{{- if .GrubEFI }}
grub2 grub2-efi-x64 grub2-efi-x64-modules \
{{- end }}
e2fsprogs \
sudo && \
systemctl enable NetworkManager && \
systemctl unmask systemd-remount-fs.service && \
systemctl unmask getty.target
{{- if not .Grub }}
RUN cd /boot && \
mv $(find . -name 'vmlinuz-*') /boot/vmlinuz && \
mv $(find . -name 'initramfs-*.img') /boot/initrd.img
{{- end }}
systemctl unmask getty.target && \
find /boot -type l -exec rm {} \;
{{ if .Luks }}
RUN yum install -y cryptsetup && \
@ -34,3 +33,9 @@ RUN dracut --no-hostonly --regenerate-all --force
{{ end }}
{{ if .Password }}RUN echo "root:{{ .Password }}" | chpasswd {{ end }}
{{- if not .Grub }}
RUN cd /boot && \
mv $(find . -name 'vmlinuz-*') /boot/vmlinuz && \
mv $(find . -name 'initramfs-*.img') /boot/initrd.img
{{- end }}

View File

@ -10,21 +10,23 @@ RUN echo "deb http://archive.debian.org/debian stretch main" > /etc/apt/sources.
echo "deb-src http://archive.debian.org/debian-security stretch/updates main" >> /etc/apt/sources.list
{{- end }}
RUN apt-get -y update && \
RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends \
linux-image-amd64 && \
find /boot -type l -exec rm {} \;
{{- if not .Grub }}
RUN mv $(find /boot -name 'vmlinuz-*') /boot/vmlinuz && \
mv $(find /boot -name 'initrd.img-*') /boot/initrd.img
{{- end }}
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
systemd-sysv \
systemd \
{{- if .Grub }}
grub2 \
grub-common \
grub2-common \
{{- end }}
{{- if .GrubBIOS }}
grub-pc-bin \
{{- end }}
{{- if .GrubEFI }}
grub-efi-amd64-bin \
{{- end }}
dbus \
iproute2 \
@ -65,3 +67,9 @@ RUN DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends cr
echo "CRYPTSETUP=y" >> /etc/cryptsetup-initramfs/conf-hook && \
update-initramfs -u -v
{{- end }}
# needs to be after update-initramfs
{{- if not .Grub }}
RUN mv $(find /boot -name 'vmlinuz-*') /boot/vmlinuz && \
mv $(find /boot -name 'initrd.img-*') /boot/initrd.img
{{- end }}

View File

@ -2,14 +2,21 @@ FROM {{ .Image }}
USER root
RUN apt-get update -y && \
RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends \
linux-image-virtual \
initramfs-tools \
systemd-sysv \
systemd \
{{- if .Grub }}
grub2 \
grub-common \
grub2-common \
{{- end }}
{{- if .GrubBIOS }}
grub-pc-bin \
{{- end }}
{{- if .GrubEFI }}
grub-efi-amd64-bin \
{{- end }}
dbus \
isc-dhcp-client \
@ -17,11 +24,6 @@ RUN apt-get update -y && \
iputils-ping && \
find /boot -type l -exec rm {} \;
{{- if not .Grub }}
RUN mv $(find /boot -name 'vmlinuz-*') /boot/vmlinuz && \
mv $(find /boot -name 'initrd.img-*') /boot/initrd.img
{{- end }}
RUN systemctl preset-all
{{ if .Password }}RUN echo "root:{{ .Password }}" | chpasswd {{ end }}
@ -54,3 +56,9 @@ iface eth0 inet dhcp\n\
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends cryptsetup-initramfs && \
update-initramfs -u -v
{{- end }}
# needs to be after update-initramfs
{{- if not .Grub }}
RUN mv $(find /boot -name 'vmlinuz-*') /boot/vmlinuz && \
mv $(find /boot -name 'initrd.img-*') /boot/initrd.img
{{- end }}