mirror of
https://github.com/linka-cloud/d2vm.git
synced 2025-07-05 11:02:26 +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:
@ -1,18 +1,13 @@
|
||||
package run
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"go.linka.cloud/d2vm/pkg/qemu"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -37,8 +32,6 @@ var (
|
||||
qemuDetached bool
|
||||
networking string
|
||||
publishFlags MultipleFlag
|
||||
deviceFlags MultipleFlag
|
||||
usbEnabled bool
|
||||
|
||||
QemuCmd = &cobra.Command{
|
||||
Use: "qemu [options] [image-path]",
|
||||
@ -71,7 +64,6 @@ func init() {
|
||||
|
||||
// Paths and settings for disks
|
||||
flags.Var(&disks, "disk", "Disk config, may be repeated. [file=]path[,size=1G][,format=qcow2]")
|
||||
flags.StringVar(&data, "data", "", "String of metadata to pass to VM")
|
||||
|
||||
// VM configuration
|
||||
flags.StringVar(&accel, "accel", defaultAccel, "Choose acceleration mode. Use 'tcg' to disable it.")
|
||||
@ -87,362 +79,45 @@ func init() {
|
||||
flags.StringVar(&networking, "networking", qemuNetworkingDefault, "Networking mode. Valid options are 'default', 'user', 'bridge[,name]', tap[,name] and 'none'. 'user' uses QEMUs userspace networking. 'bridge' connects to a preexisting bridge. 'tap' uses a prexisting tap device. 'none' disables networking.`")
|
||||
|
||||
flags.Var(&publishFlags, "publish", "Publish a vm's port(s) to the host (default [])")
|
||||
|
||||
// USB devices
|
||||
flags.BoolVar(&usbEnabled, "usb", false, "Enable USB controller")
|
||||
|
||||
flags.Var(&deviceFlags, "device", "Add USB host device(s). Format driver[,prop=value][,...] -- add device, like --device on the qemu command line.")
|
||||
|
||||
}
|
||||
|
||||
func Qemu(cmd *cobra.Command, args []string) {
|
||||
|
||||
// Generate UUID, so that /sys/class/dmi/id/product_uuid is populated
|
||||
vmUUID := 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, "")
|
||||
|
||||
path := args[0]
|
||||
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
for i, d := range 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 == "" {
|
||||
log.Fatalf("disk specified with no size or name")
|
||||
}
|
||||
disks[i] = d
|
||||
}
|
||||
|
||||
disks = append(Disks{DiskConfig{Path: path}}, disks...)
|
||||
|
||||
if networking == "" || networking == "default" {
|
||||
dflt := qemuNetworkingDefault
|
||||
networking = dflt
|
||||
}
|
||||
netMode := strings.SplitN(networking, ",", 2)
|
||||
|
||||
var netdevConfig string
|
||||
switch netMode[0] {
|
||||
case qemuNetworkingUser:
|
||||
netdevConfig = "user,id=t0"
|
||||
case qemuNetworkingTap:
|
||||
if len(netMode) != 2 {
|
||||
log.Fatalf("Not enough arguments for %q networking mode", qemuNetworkingTap)
|
||||
}
|
||||
if len(publishFlags) != 0 {
|
||||
log.Fatalf("Port publishing requires %q networking mode", qemuNetworkingUser)
|
||||
}
|
||||
netdevConfig = fmt.Sprintf("tap,id=t0,ifname=%s,script=no,downscript=no", netMode[1])
|
||||
case qemuNetworkingBridge:
|
||||
if len(netMode) != 2 {
|
||||
log.Fatalf("Not enough arguments for %q networking mode", qemuNetworkingBridge)
|
||||
}
|
||||
if len(publishFlags) != 0 {
|
||||
log.Fatalf("Port publishing requires %q networking mode", qemuNetworkingUser)
|
||||
}
|
||||
netdevConfig = fmt.Sprintf("bridge,id=t0,br=%s", netMode[1])
|
||||
case qemuNetworkingNone:
|
||||
if len(publishFlags) != 0 {
|
||||
log.Fatalf("Port publishing requires %q networking mode", qemuNetworkingUser)
|
||||
}
|
||||
netdevConfig = ""
|
||||
default:
|
||||
log.Fatalf("Invalid networking mode: %s", netMode[0])
|
||||
}
|
||||
|
||||
config := QemuConfig{
|
||||
Path: path,
|
||||
GUI: enableGUI,
|
||||
Disks: disks,
|
||||
Arch: arch,
|
||||
CPUs: cpus,
|
||||
Memory: mem,
|
||||
Accel: accel,
|
||||
Detached: qemuDetached,
|
||||
QemuBinPath: qemuCmd,
|
||||
PublishedPorts: publishFlags,
|
||||
NetdevConfig: netdevConfig,
|
||||
UUID: vmUUID,
|
||||
USB: usbEnabled,
|
||||
Devices: deviceFlags,
|
||||
}
|
||||
|
||||
config, err := discoverBinaries(config)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if err = runQemuLocal(config); err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func runQemuLocal(config QemuConfig) error {
|
||||
var args []string
|
||||
config, args = buildQemuCmdline(config)
|
||||
|
||||
for _, d := range config.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(config.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 config.Detached == true {
|
||||
return fmt.Errorf("Detached mode is only supported when running in a container, not locally")
|
||||
}
|
||||
|
||||
qemuCmd := exec.Command(config.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 config.GUI != true {
|
||||
qemuCmd.Stdin = os.Stdin
|
||||
qemuCmd.Stdout = os.Stdout
|
||||
qemuCmd.Stderr = os.Stderr
|
||||
}
|
||||
|
||||
return qemuCmd.Run()
|
||||
}
|
||||
|
||||
func buildQemuCmdline(config QemuConfig) (QemuConfig, []string) {
|
||||
// Iterate through the flags and build arguments
|
||||
var qemuArgs []string
|
||||
qemuArgs = append(qemuArgs, "-smp", fmt.Sprintf("%d", config.CPUs))
|
||||
qemuArgs = append(qemuArgs, "-m", fmt.Sprintf("%d", config.Memory))
|
||||
qemuArgs = append(qemuArgs, "-uuid", config.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 config.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 config.Arch {
|
||||
case "s390x":
|
||||
goArch = "s390x"
|
||||
case "aarch64":
|
||||
goArch = "arm64"
|
||||
case "x86_64":
|
||||
goArch = "amd64"
|
||||
default:
|
||||
log.Fatalf("%s is an unsupported architecture.", config.Arch)
|
||||
}
|
||||
|
||||
if goArch != runtime.GOARCH {
|
||||
log.Infof("Disable acceleration as %s != %s", config.Arch, runtime.GOARCH)
|
||||
config.Accel = ""
|
||||
}
|
||||
|
||||
if config.Accel != "" {
|
||||
switch config.Arch {
|
||||
case "s390x":
|
||||
qemuArgs = append(qemuArgs, "-machine", fmt.Sprintf("s390-ccw-virtio,accel=%s", config.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, config.Accel))
|
||||
default:
|
||||
qemuArgs = append(qemuArgs, "-machine", fmt.Sprintf("q35,accel=%s", config.Accel))
|
||||
}
|
||||
} else {
|
||||
switch config.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 config.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 config.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 config.NetdevConfig == "" {
|
||||
qemuArgs = append(qemuArgs, "-net", "none")
|
||||
} else {
|
||||
mac := generateMAC()
|
||||
if config.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(config.PublishedPorts)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
qemuArgs = append(qemuArgs, "-netdev", config.NetdevConfig+forwardings)
|
||||
}
|
||||
|
||||
if config.GUI != true {
|
||||
qemuArgs = append(qemuArgs, "-nographic")
|
||||
}
|
||||
|
||||
if config.USB == true {
|
||||
qemuArgs = append(qemuArgs, "-usb")
|
||||
}
|
||||
for _, d := range config.Devices {
|
||||
qemuArgs = append(qemuArgs, "-device", d)
|
||||
}
|
||||
|
||||
return config, qemuArgs
|
||||
}
|
||||
|
||||
func discoverBinaries(config QemuConfig) (QemuConfig, error) {
|
||||
if config.QemuImgPath != "" {
|
||||
return config, nil
|
||||
}
|
||||
|
||||
qemuBinPath := "qemu-system-" + config.Arch
|
||||
qemuImgPath := "qemu-img"
|
||||
|
||||
var err error
|
||||
config.QemuBinPath, err = exec.LookPath(qemuBinPath)
|
||||
if err != nil {
|
||||
return config, fmt.Errorf("Unable to find %s within the $PATH", qemuBinPath)
|
||||
}
|
||||
|
||||
config.QemuImgPath, err = exec.LookPath(qemuImgPath)
|
||||
if err != nil {
|
||||
return config, fmt.Errorf("Unable to find %s within the $PATH", qemuImgPath)
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func buildQemuForwardings(publishFlags MultipleFlag) (string, error) {
|
||||
if len(publishFlags) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
var forwardings string
|
||||
var publishedPorts []PublishedPort
|
||||
for _, publish := range publishFlags {
|
||||
p, err := NewPublishedPort(publish)
|
||||
if err != nil {
|
||||
return "", err
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
hostPort := p.Host
|
||||
guestPort := p.Guest
|
||||
|
||||
forwardings = fmt.Sprintf("%s,hostfwd=%s::%d-:%d", forwardings, p.Protocol, hostPort, guestPort)
|
||||
publishedPorts = append(publishedPorts, p)
|
||||
}
|
||||
|
||||
return forwardings, nil
|
||||
}
|
||||
|
||||
func buildDockerForwardings(publishedPorts []string) ([]string, error) {
|
||||
pmap := []string{}
|
||||
for _, port := range publishedPorts {
|
||||
s, err := NewPublishedPort(port)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pmap = append(pmap, "-p", fmt.Sprintf("%d:%d/%s", s.Host, s.Guest, s.Protocol))
|
||||
opts := []qemu.Option{
|
||||
qemu.WithDisks(disks...),
|
||||
qemu.WithAccel(accel),
|
||||
qemu.WithArch(arch),
|
||||
qemu.WithCPUs(cpus),
|
||||
qemu.WithMemory(mem),
|
||||
qemu.WithNetworking(networking),
|
||||
qemu.WithStdin(os.Stdin),
|
||||
qemu.WithStdout(os.Stdout),
|
||||
qemu.WithStderr(os.Stderr),
|
||||
}
|
||||
if enableGUI {
|
||||
opts = append(opts, qemu.WithGUI())
|
||||
}
|
||||
if qemuDetached {
|
||||
opts = append(opts, qemu.WithDetached())
|
||||
}
|
||||
if err := qemu.Run(cmd.Context(), path, opts...); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return pmap, nil
|
||||
}
|
||||
|
||||
// QemuConfig contains the config for Qemu
|
||||
type QemuConfig struct {
|
||||
Path string
|
||||
GUI bool
|
||||
Disks Disks
|
||||
FWPath string
|
||||
Arch string
|
||||
CPUs uint
|
||||
Memory uint
|
||||
Accel string
|
||||
Detached bool
|
||||
QemuBinPath string
|
||||
QemuImgPath string
|
||||
PublishedPorts []string
|
||||
NetdevConfig string
|
||||
UUID uuid.UUID
|
||||
USB bool
|
||||
Devices []string
|
||||
}
|
||||
|
||||
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 net.HardwareAddr(mac)
|
||||
}
|
||||
|
@ -28,6 +28,8 @@ import (
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
"go.linka.cloud/d2vm/pkg/qemu"
|
||||
)
|
||||
|
||||
//go:embed sparsecat-linux-amd64
|
||||
@ -188,15 +190,8 @@ func ConvertMBtoGB(i int) int {
|
||||
return (i + (1024 - i%1024)) / 1024
|
||||
}
|
||||
|
||||
// DiskConfig is the config for a disk
|
||||
type DiskConfig struct {
|
||||
Path string
|
||||
Size int
|
||||
Format string
|
||||
}
|
||||
|
||||
// Disks is the type for a list of DiskConfig
|
||||
type Disks []DiskConfig
|
||||
type Disks []qemu.Disk
|
||||
|
||||
func (l *Disks) String() string {
|
||||
return fmt.Sprint(*l)
|
||||
@ -204,7 +199,7 @@ func (l *Disks) String() string {
|
||||
|
||||
// Set is used by flag to configure value from CLI
|
||||
func (l *Disks) Set(value string) error {
|
||||
d := DiskConfig{}
|
||||
d := qemu.Disk{}
|
||||
s := strings.Split(value, ",")
|
||||
for _, p := range s {
|
||||
c := strings.SplitN(p, "=", 2)
|
||||
|
Reference in New Issue
Block a user