mirror of
https://github.com/linka-cloud/d2vm.git
synced 2025-06-23 22:02:25 +00:00
run/qemu: remove usb and device flags
refactor: move qemu to its own package tests: implements end to end tests for the convert command with the following images: alpine:3.17, ubuntu:20.04, debian:11, centos:8 Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
This commit is contained in:
28
pkg/docker/check_term_unix.go
Normal file
28
pkg/docker/check_term_unix.go
Normal file
@ -0,0 +1,28 @@
|
||||
//go:build !windows
|
||||
|
||||
// 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 docker
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func isInteractive() bool {
|
||||
_, err := unix.IoctlGetWinsize(int(os.Stdout.Fd()), unix.TIOCGWINSZ)
|
||||
return err == nil
|
||||
}
|
36
pkg/docker/check_term_windows.go
Normal file
36
pkg/docker/check_term_windows.go
Normal file
@ -0,0 +1,36 @@
|
||||
//go:build windows
|
||||
|
||||
// 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 docker
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func isInteractive() bool {
|
||||
handle := windows.Handle(os.Stdout.Fd())
|
||||
var mode uint32
|
||||
if err := windows.GetConsoleMode(handle, &mode); err != nil {
|
||||
return false
|
||||
}
|
||||
mode |= windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING
|
||||
if err := windows.SetConsoleMode(handle, mode); err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
@ -130,7 +130,14 @@ func RunD2VM(ctx context.Context, image, version, in, out, cmd string, args ...s
|
||||
if version == "" {
|
||||
version = "latest"
|
||||
}
|
||||
a := []string{
|
||||
a := []string{"run", "--rm"}
|
||||
|
||||
interactive := isInteractive()
|
||||
|
||||
if interactive {
|
||||
a = append(a, "-i", "-t")
|
||||
}
|
||||
a = append(a,
|
||||
"--privileged",
|
||||
"-e",
|
||||
// yes... it is kind of a dirty hack
|
||||
@ -145,6 +152,12 @@ func RunD2VM(ctx context.Context, image, version, in, out, cmd string, args ...s
|
||||
"/d2vm",
|
||||
fmt.Sprintf("%s:%s", image, version),
|
||||
cmd,
|
||||
)
|
||||
c := exec.CommandContext(ctx, "docker", append(a, args...)...)
|
||||
if interactive {
|
||||
c.Stdin = os.Stdin
|
||||
}
|
||||
return RunInteractiveAndRemove(ctx, append(a, args...)...)
|
||||
c.Stdout = os.Stdout
|
||||
c.Stderr = os.Stderr
|
||||
return c.Run()
|
||||
}
|
||||
|
141
pkg/qemu/config.go
Normal file
141
pkg/qemu/config.go
Normal file
@ -0,0 +1,141 @@
|
||||
// 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 qemu
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Option func(c *config)
|
||||
|
||||
type Disk struct {
|
||||
Path string
|
||||
Size int
|
||||
Format string
|
||||
}
|
||||
|
||||
type PublishedPort struct {
|
||||
Guest uint16
|
||||
Host uint16
|
||||
Protocol string
|
||||
}
|
||||
|
||||
// config contains the config for Qemu
|
||||
type config struct {
|
||||
path string
|
||||
uuid uuid.UUID
|
||||
gui bool
|
||||
disks []Disk
|
||||
networking string
|
||||
arch string
|
||||
cpus uint
|
||||
memory uint
|
||||
accel string
|
||||
detached bool
|
||||
qemuBinPath string
|
||||
qemuImgPath string
|
||||
publishedPorts []PublishedPort
|
||||
netdevConfig string
|
||||
|
||||
stdin io.Reader
|
||||
stdout io.Writer
|
||||
stderr io.Writer
|
||||
}
|
||||
|
||||
func WithGUI() Option {
|
||||
return func(c *config) {
|
||||
c.gui = true
|
||||
}
|
||||
}
|
||||
|
||||
func WithDisks(disks ...Disk) Option {
|
||||
return func(c *config) {
|
||||
c.disks = disks
|
||||
}
|
||||
}
|
||||
|
||||
func WithNetworking(networking string) Option {
|
||||
return func(c *config) {
|
||||
c.networking = networking
|
||||
}
|
||||
}
|
||||
|
||||
func WithArch(arch string) Option {
|
||||
return func(c *config) {
|
||||
c.arch = arch
|
||||
}
|
||||
}
|
||||
|
||||
func WithCPUs(cpus uint) Option {
|
||||
return func(c *config) {
|
||||
c.cpus = cpus
|
||||
}
|
||||
}
|
||||
|
||||
func WithMemory(memory uint) Option {
|
||||
return func(c *config) {
|
||||
c.memory = memory
|
||||
}
|
||||
}
|
||||
|
||||
func WithAccel(accel string) Option {
|
||||
return func(c *config) {
|
||||
c.accel = accel
|
||||
}
|
||||
}
|
||||
|
||||
func WithDetached() Option {
|
||||
return func(c *config) {
|
||||
c.detached = true
|
||||
}
|
||||
}
|
||||
|
||||
func WithQemuBinPath(path string) Option {
|
||||
return func(c *config) {
|
||||
c.qemuBinPath = path
|
||||
}
|
||||
}
|
||||
|
||||
func WithQemuImgPath(path string) Option {
|
||||
return func(c *config) {
|
||||
c.qemuImgPath = path
|
||||
}
|
||||
}
|
||||
|
||||
func WithPublishedPorts(ports ...PublishedPort) Option {
|
||||
return func(c *config) {
|
||||
c.publishedPorts = ports
|
||||
}
|
||||
}
|
||||
|
||||
func WithStdin(r io.Reader) Option {
|
||||
return func(c *config) {
|
||||
c.stdin = r
|
||||
}
|
||||
}
|
||||
|
||||
func WithStdout(w io.Writer) Option {
|
||||
return func(c *config) {
|
||||
c.stdout = w
|
||||
}
|
||||
}
|
||||
|
||||
func WithStderr(w io.Writer) Option {
|
||||
return func(c *config) {
|
||||
c.stderr = w
|
||||
}
|
||||
}
|
363
pkg/qemu/qemu.go
Normal file
363
pkg/qemu/qemu.go
Normal file
@ -0,0 +1,363 @@
|
||||
package qemu
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
NetworkingNone = "none"
|
||||
NetworkingUser = "user"
|
||||
NetworkingTap = "tap"
|
||||
NetworkingBridge = "bridge"
|
||||
NetworkingDefault = NetworkingUser
|
||||
)
|
||||
|
||||
var (
|
||||
defaultArch string
|
||||
defaultAccel string
|
||||
)
|
||||
|
||||
func init() {
|
||||
switch runtime.GOARCH {
|
||||
case "arm64":
|
||||
defaultArch = "aarch64"
|
||||
case "amd64":
|
||||
defaultArch = "x86_64"
|
||||
case "s390x":
|
||||
defaultArch = "s390x"
|
||||
}
|
||||
switch {
|
||||
case runtime.GOARCH == "s390x":
|
||||
defaultAccel = "kvm"
|
||||
case haveKVM():
|
||||
defaultAccel = "kvm:tcg"
|
||||
case runtime.GOOS == "darwin":
|
||||
defaultAccel = "hvf:tcg"
|
||||
}
|
||||
}
|
||||
|
||||
func Run(ctx context.Context, path string, opts ...Option) error {
|
||||
config := &config{}
|
||||
|
||||
for _, o := range opts {
|
||||
o(config)
|
||||
}
|
||||
|
||||
config.path = path
|
||||
|
||||
// Generate UUID, so that /sys/class/dmi/id/product_uuid is populated
|
||||
config.uuid = uuid.New()
|
||||
// These envvars override the corresponding command line
|
||||
// options. So this must remain after the `flags.Parse` above.
|
||||
// accel = GetStringValue("LINUXKIT_QEMU_ACCEL", accel, "")
|
||||
|
||||
if config.arch == "" {
|
||||
config.arch = defaultArch
|
||||
}
|
||||
|
||||
if config.accel == "" {
|
||||
config.accel = defaultAccel
|
||||
}
|
||||
|
||||
if _, err := os.Stat(config.path); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if config.cpus == 0 {
|
||||
config.cpus = 1
|
||||
}
|
||||
|
||||
if config.memory == 0 {
|
||||
config.memory = 1024
|
||||
}
|
||||
|
||||
for i, d := range config.disks {
|
||||
id := ""
|
||||
if i != 0 {
|
||||
id = strconv.Itoa(i)
|
||||
}
|
||||
if d.Size != 0 && d.Format == "" {
|
||||
d.Format = "qcow2"
|
||||
}
|
||||
if d.Size != 0 && d.Path == "" {
|
||||
d.Path = "disk" + id + ".img"
|
||||
}
|
||||
if d.Path == "" {
|
||||
return fmt.Errorf("disk specified with no size or name")
|
||||
}
|
||||
config.disks[i] = d
|
||||
}
|
||||
|
||||
config.disks = append([]Disk{{Path: config.path}}, config.disks...)
|
||||
|
||||
if config.networking == "" || config.networking == "default" {
|
||||
dflt := NetworkingDefault
|
||||
config.networking = dflt
|
||||
}
|
||||
netMode := strings.SplitN(config.networking, ",", 2)
|
||||
|
||||
switch netMode[0] {
|
||||
case NetworkingUser:
|
||||
config.netdevConfig = "user,id=t0"
|
||||
case NetworkingTap:
|
||||
if len(netMode) != 2 {
|
||||
return fmt.Errorf("Not enough arguments for %q networking mode", NetworkingTap)
|
||||
}
|
||||
if len(config.publishedPorts) != 0 {
|
||||
return fmt.Errorf("Port publishing requires %q networking mode", NetworkingUser)
|
||||
}
|
||||
config.netdevConfig = fmt.Sprintf("tap,id=t0,ifname=%s,script=no,downscript=no", netMode[1])
|
||||
case NetworkingBridge:
|
||||
if len(netMode) != 2 {
|
||||
return fmt.Errorf("Not enough arguments for %q networking mode", NetworkingBridge)
|
||||
}
|
||||
if len(config.publishedPorts) != 0 {
|
||||
return fmt.Errorf("Port publishing requires %q networking mode", NetworkingUser)
|
||||
}
|
||||
config.netdevConfig = fmt.Sprintf("bridge,id=t0,br=%s", netMode[1])
|
||||
case NetworkingNone:
|
||||
if len(config.publishedPorts) != 0 {
|
||||
return fmt.Errorf("Port publishing requires %q networking mode", NetworkingUser)
|
||||
}
|
||||
config.netdevConfig = ""
|
||||
default:
|
||||
return fmt.Errorf("Invalid networking mode: %s", netMode[0])
|
||||
}
|
||||
|
||||
if err := config.discoverBinaries(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
return config.runQemuLocal(ctx)
|
||||
}
|
||||
|
||||
func (c *config) runQemuLocal(ctx context.Context) (err error) {
|
||||
var args []string
|
||||
args, err = c.buildQemuCmdline()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, d := range c.disks {
|
||||
// If disk doesn't exist then create one
|
||||
if _, err := os.Stat(d.Path); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
log.Debugf("Creating new qemu disk [%s] format %s", d.Path, d.Format)
|
||||
qemuImgCmd := exec.Command(c.qemuImgPath, "create", "-f", d.Format, d.Path, fmt.Sprintf("%dM", d.Size))
|
||||
log.Debugf("%v", qemuImgCmd.Args)
|
||||
if err := qemuImgCmd.Run(); err != nil {
|
||||
return fmt.Errorf("Error creating disk [%s] format %s: %s", d.Path, d.Format, err.Error())
|
||||
}
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
log.Infof("Using existing disk [%s] format %s", d.Path, d.Format)
|
||||
}
|
||||
}
|
||||
|
||||
// Detached mode is only supported in a container.
|
||||
if c.detached == true {
|
||||
return fmt.Errorf("Detached mode is only supported when running in a container, not locally")
|
||||
}
|
||||
|
||||
qemuCmd := exec.CommandContext(ctx, c.qemuBinPath, args...)
|
||||
// If verbosity is enabled print out the full path/arguments
|
||||
log.Debugf("%v", qemuCmd.Args)
|
||||
|
||||
// If we're not using a separate window then link the execution to stdin/out
|
||||
if c.gui == true {
|
||||
qemuCmd.Stdin = nil
|
||||
qemuCmd.Stdout = nil
|
||||
qemuCmd.Stderr = nil
|
||||
} else {
|
||||
qemuCmd.Stdin = c.stdin
|
||||
qemuCmd.Stdout = c.stdout
|
||||
qemuCmd.Stderr = c.stderr
|
||||
}
|
||||
|
||||
return qemuCmd.Run()
|
||||
}
|
||||
|
||||
func (c *config) buildQemuCmdline() ([]string, error) {
|
||||
// Iterate through the flags and build arguments
|
||||
var qemuArgs []string
|
||||
qemuArgs = append(qemuArgs, "-smp", fmt.Sprintf("%d", c.cpus))
|
||||
qemuArgs = append(qemuArgs, "-m", fmt.Sprintf("%d", c.memory))
|
||||
qemuArgs = append(qemuArgs, "-uuid", c.uuid.String())
|
||||
|
||||
// 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" {
|
||||
if runtime.GOARCH == "arm64" {
|
||||
qemuArgs = append(qemuArgs, "-cpu", "host")
|
||||
} else {
|
||||
qemuArgs = append(qemuArgs, "-cpu", "cortex-a57")
|
||||
}
|
||||
}
|
||||
|
||||
// goArch is the GOARCH equivalent of config.Arch
|
||||
var goArch string
|
||||
switch c.arch {
|
||||
case "s390x":
|
||||
goArch = "s390x"
|
||||
case "aarch64":
|
||||
goArch = "arm64"
|
||||
case "x86_64":
|
||||
goArch = "amd64"
|
||||
default:
|
||||
return nil, fmt.Errorf("%s is an unsupported architecture.", c.arch)
|
||||
}
|
||||
|
||||
if goArch != runtime.GOARCH {
|
||||
log.Infof("Disable acceleration as %s != %s", c.arch, runtime.GOARCH)
|
||||
c.accel = ""
|
||||
}
|
||||
|
||||
if c.accel != "" {
|
||||
switch c.arch {
|
||||
case "s390x":
|
||||
qemuArgs = append(qemuArgs, "-machine", fmt.Sprintf("s390-ccw-virtio,accel=%s", c.accel))
|
||||
case "aarch64":
|
||||
gic := ""
|
||||
// VCPU supports less PA bits (36) than requested by the memory map (40)
|
||||
highmem := "highmem=off,"
|
||||
if runtime.GOOS == "linux" {
|
||||
// gic-version=host requires KVM, which implies Linux
|
||||
gic = "gic_version=host,"
|
||||
highmem = ""
|
||||
}
|
||||
qemuArgs = append(qemuArgs, "-machine", fmt.Sprintf("virt,%s%saccel=%s", gic, highmem, c.accel))
|
||||
default:
|
||||
qemuArgs = append(qemuArgs, "-machine", fmt.Sprintf("q35,accel=%s", c.accel))
|
||||
}
|
||||
} else {
|
||||
switch c.arch {
|
||||
case "s390x":
|
||||
qemuArgs = append(qemuArgs, "-machine", "s390-ccw-virtio")
|
||||
case "aarch64":
|
||||
qemuArgs = append(qemuArgs, "-machine", "virt")
|
||||
default:
|
||||
qemuArgs = append(qemuArgs, "-machine", "q35")
|
||||
}
|
||||
}
|
||||
|
||||
// rng-random does not work on macOS
|
||||
// Temporarily disable it until fixed upstream.
|
||||
if runtime.GOOS != "darwin" {
|
||||
rng := "rng-random,id=rng0"
|
||||
if runtime.GOOS == "linux" {
|
||||
rng = rng + ",filename=/dev/urandom"
|
||||
}
|
||||
if c.arch == "s390x" {
|
||||
qemuArgs = append(qemuArgs, "-object", rng, "-device", "virtio-rng-ccw,rng=rng0")
|
||||
} else {
|
||||
qemuArgs = append(qemuArgs, "-object", rng, "-device", "virtio-rng-pci,rng=rng0")
|
||||
}
|
||||
}
|
||||
|
||||
var lastDisk int
|
||||
for i, d := range c.disks {
|
||||
index := i
|
||||
if d.Format != "" {
|
||||
qemuArgs = append(qemuArgs, "-drive", "file="+d.Path+",format="+d.Format+",index="+strconv.Itoa(index)+",media=disk")
|
||||
} else {
|
||||
qemuArgs = append(qemuArgs, "-drive", "file="+d.Path+",index="+strconv.Itoa(index)+",media=disk")
|
||||
}
|
||||
lastDisk = index
|
||||
}
|
||||
|
||||
// Ensure CDROMs start from at least hdc
|
||||
if lastDisk < 2 {
|
||||
lastDisk = 2
|
||||
}
|
||||
|
||||
if c.netdevConfig != "" {
|
||||
mac := generateMAC()
|
||||
if c.arch == "s390x" {
|
||||
qemuArgs = append(qemuArgs, "-device", "virtio-net-ccw,netdev=t0,mac="+mac.String())
|
||||
} else {
|
||||
qemuArgs = append(qemuArgs, "-device", "virtio-net-pci,netdev=t0,mac="+mac.String())
|
||||
}
|
||||
forwardings, err := buildQemuForwardings(c.publishedPorts)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
qemuArgs = append(qemuArgs, "-netdev", c.netdevConfig+forwardings)
|
||||
} else {
|
||||
qemuArgs = append(qemuArgs, "-net", "none")
|
||||
}
|
||||
|
||||
if c.gui != true {
|
||||
qemuArgs = append(qemuArgs, "-nographic")
|
||||
}
|
||||
|
||||
return qemuArgs, nil
|
||||
}
|
||||
|
||||
func (c *config) discoverBinaries() error {
|
||||
if c.qemuImgPath != "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
qemuBinPath := "qemu-system-" + c.arch
|
||||
qemuImgPath := "qemu-img"
|
||||
|
||||
var err error
|
||||
c.qemuBinPath, err = exec.LookPath(qemuBinPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to find %s within the $PATH", qemuBinPath)
|
||||
}
|
||||
|
||||
c.qemuImgPath, err = exec.LookPath(qemuImgPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to find %s within the $PATH", qemuImgPath)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildQemuForwardings(publishedPorts []PublishedPort) (string, error) {
|
||||
if len(publishedPorts) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
var forwardings string
|
||||
for _, p := range publishedPorts {
|
||||
hostPort := p.Host
|
||||
guestPort := p.Guest
|
||||
|
||||
forwardings = fmt.Sprintf("%s,hostfwd=%s::%d-:%d", forwardings, p.Protocol, hostPort, guestPort)
|
||||
}
|
||||
|
||||
return forwardings, nil
|
||||
}
|
||||
|
||||
func haveKVM() bool {
|
||||
_, err := os.Stat("/dev/kvm")
|
||||
return !os.IsNotExist(err)
|
||||
}
|
||||
|
||||
func generateMAC() net.HardwareAddr {
|
||||
mac := make([]byte, 6)
|
||||
n, err := rand.Read(mac)
|
||||
if err != nil {
|
||||
log.WithError(err).Fatal("failed to generate random mac address")
|
||||
}
|
||||
if n != 6 {
|
||||
log.WithError(err).Fatalf("generated %d bytes for random mac address", n)
|
||||
}
|
||||
mac[0] &^= 0x01 // Clear multicast bit
|
||||
mac[0] |= 0x2 // Set locally administered bit
|
||||
return mac
|
||||
}
|
Reference in New Issue
Block a user