2
0
mirror of https://github.com/linka-cloud/d2vm.git synced 2025-06-27 23:52:27 +00:00

remove -O option, use output extension instead

add run command to execute vm in qemu or virtualbox

Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
This commit is contained in:
2022-05-20 16:36:36 +02:00
parent 29d953c14d
commit 62d8a1019d
17 changed files with 1320 additions and 38 deletions

View File

@ -15,6 +15,9 @@
package main
import (
"os"
"path/filepath"
"runtime"
"strings"
"github.com/google/uuid"
@ -35,16 +38,23 @@ var (
Short: "Build a vm image from Dockerfile",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
// TODO(adphi): resolve context path
if runtime.GOOS != "linux" {
return docker.RunD2VM(cmd.Context(), d2vm.Image, d2vm.Version, cmd.Name(), os.Args[2:]...)
}
size, err := parseSize(size)
if err != nil {
return err
}
exec.SetDebug(debug)
logrus.Infof("building docker image from %s", file)
if file == "" {
file = filepath.Join(args[0], "Dockerfile")
}
if err := docker.Build(cmd.Context(), tag, file, args[0], buildArgs...); err != nil {
return err
}
return d2vm.Convert(cmd.Context(), tag, size, password, output, format)
return d2vm.Convert(cmd.Context(), tag, size, password, output)
},
}
)
@ -52,11 +62,10 @@ var (
func init() {
rootCmd.AddCommand(buildCmd)
buildCmd.Flags().StringVarP(&file, "file", "f", "Dockerfile", "Name of the Dockerfile (Default is 'PATH/Dockerfile')")
buildCmd.Flags().StringVarP(&file, "file", "f", "", "Name of the Dockerfile")
buildCmd.Flags().StringArrayVar(&buildArgs, "build-arg", nil, "Set build-time variables")
buildCmd.Flags().StringVarP(&format, "output-format", "O", format, "The output image format, supported formats: "+strings.Join(d2vm.OutputFormats(), " "))
buildCmd.Flags().StringVarP(&output, "output", "o", output, "The output image")
buildCmd.Flags().StringVarP(&output, "output", "o", output, "The output image, the extension determine the image format. Supported formats: "+strings.Join(d2vm.OutputFormats(), " "))
buildCmd.Flags().StringVarP(&password, "password", "p", "root", "Root user password")
buildCmd.Flags().StringVarP(&size, "size", "s", "10G", "The output image size")
buildCmd.Flags().BoolVarP(&debug, "debug", "d", false, "Enable Debug output")

View File

@ -17,6 +17,7 @@ package main
import (
"fmt"
"os"
"runtime"
"strings"
"github.com/c2h5oh/datasize"
@ -37,6 +38,9 @@ var (
Args: cobra.ExactArgs(1),
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
if runtime.GOOS != "linux" {
return docker.RunD2VM(cmd.Context(), d2vm.Image, d2vm.Version, cmd.Name(), os.Args[2:]...)
}
img := args[0]
tag := "latest"
if parts := strings.Split(img, ":"); len(parts) > 1 {
@ -74,7 +78,7 @@ var (
return err
}
}
return d2vm.Convert(cmd.Context(), img, size, password, output, format)
return d2vm.Convert(cmd.Context(), img, size, password, output)
},
}
)
@ -89,8 +93,7 @@ func parseSize(s string) (int64, error) {
func init() {
convertCmd.Flags().BoolVar(&pull, "pull", false, "Always pull docker image")
convertCmd.Flags().StringVarP(&format, "output-format", "O", format, "The output image format, supported formats: "+strings.Join(d2vm.OutputFormats(), " "))
convertCmd.Flags().StringVarP(&output, "output", "o", output, "The output image")
convertCmd.Flags().StringVarP(&output, "output", "o", output, "The output image, the extension determine the image format. Supported formats: "+strings.Join(d2vm.OutputFormats(), " "))
convertCmd.Flags().StringVarP(&password, "password", "p", "root", "The Root user password")
convertCmd.Flags().StringVarP(&size, "size", "s", "10G", "The output image size")
convertCmd.Flags().BoolVarP(&debug, "debug", "d", false, "Enable Debug output")

42
cmd/d2vm/run.go Normal file
View File

@ -0,0 +1,42 @@
// 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 main
import (
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"go.linka.cloud/d2vm/cmd/d2vm/run"
)
var (
runCmd = &cobra.Command{
Use: "run",
Short: "run the converted virtual machine",
PersistentPreRun: func(cmd *cobra.Command, args []string) {
if debug {
logrus.SetLevel(logrus.DebugLevel)
}
},
}
)
func init() {
rootCmd.AddCommand(runCmd)
runCmd.AddCommand(run.VboxCmd)
runCmd.AddCommand(run.QemuCmd)
runCmd.PersistentFlags().BoolVarP(&debug, "debug", "d", false, "Enable Debug output")
}

77
cmd/d2vm/run/metadata.go Normal file
View File

@ -0,0 +1,77 @@
package run
import (
"fmt"
"os"
"path/filepath"
"github.com/rn/iso9660wrap"
log "github.com/sirupsen/logrus"
)
// WriteMetadataISO writes a metadata ISO file in a format usable by pkg/metadata
func WriteMetadataISO(path string, content []byte) error {
outfh, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
defer outfh.Close()
return iso9660wrap.WriteBuffer(outfh, content, "config")
}
func metadataCreateUsage() {
invoked := filepath.Base(os.Args[0])
fmt.Printf("USAGE: %s metadata create [file.iso] [metadata]\n\n", invoked)
fmt.Printf("'file.iso' is the file to create.\n")
fmt.Printf("'metadata' will be written to '/config' in the ISO.\n")
fmt.Printf("This is compatible with the d2vm/metadata package\n")
}
func metadataCreate(args []string) {
if len(args) != 2 {
metadataCreateUsage()
os.Exit(1)
}
switch args[0] {
case "help", "-h", "-help", "--help":
metadataCreateUsage()
os.Exit(0)
}
isoImage := args[0]
metadata := args[1]
if err := WriteMetadataISO(isoImage, []byte(metadata)); err != nil {
log.Fatal("Failed to write user data ISO: ", err)
}
}
func metadataUsage() {
invoked := filepath.Base(os.Args[0])
fmt.Printf("USAGE: %s metadata COMMAND [options]\n\n", invoked)
fmt.Printf("Commands:\n")
fmt.Printf(" create Create a metadata ISO\n")
}
func metadata(args []string) {
if len(args) < 1 {
metadataUsage()
os.Exit(1)
}
switch args[0] {
case "help", "-h", "-help", "--help":
metadataUsage()
os.Exit(0)
}
switch args[0] {
case "create":
metadataCreate(args[1:])
default:
fmt.Printf("%q is not a valid metadata command.\n\n", args[0])
metadataUsage()
os.Exit(1)
}
}

454
cmd/d2vm/run/qemu.go Normal file
View File

@ -0,0 +1,454 @@
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"
)
const (
qemuNetworkingNone string = "none"
qemuNetworkingUser = "user"
qemuNetworkingTap = "tap"
qemuNetworkingBridge = "bridge"
qemuNetworkingDefault = qemuNetworkingUser
)
var (
defaultArch string
defaultAccel string
enableGUI *bool
disks Disks
data *string
accel *string
arch *string
cpus *uint
mem *uint
qemuCmd *string
qemuDetached *bool
networking *string
publishFlags MultipleFlag
deviceFlags MultipleFlag
usbEnabled *bool
QemuCmd = &cobra.Command{
Use: "qemu [options] [image-path]",
Args: cobra.ExactArgs(1),
Run: Qemu,
}
)
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"
}
flags := QemuCmd.Flags()
// flags.Usage = func() {
// fmt.Printf("Options:")
// flags.PrintDefaults()
// fmt.Printf("")
// fmt.Printf("If not running as root note that '--networking bridge,br0' requires a")
// fmt.Printf("setuid network helper and appropriate host configuration, see")
// fmt.Printf("https://wiki.qemu.org/Features/HelperNetworking")
// }
enableGUI = flags.Bool("gui", false, "Set qemu to use video output instead of stdio")
// Paths and settings for disks
flags.Var(&disks, "disk", "Disk config, may be repeated. [file=]path[,size=1G][,format=qcow2]")
data = flags.String("data", "", "String of metadata to pass to VM; error to specify both -data and -data-file")
// VM configuration
accel = flags.String("accel", defaultAccel, "Choose acceleration mode. Use 'tcg' to disable it.")
arch = flags.String("arch", defaultArch, "Type of architecture to use, e.g. x86_64, aarch64, s390x")
cpus = flags.Uint("cpus", 1, "Number of CPUs")
mem = flags.Uint("mem", 1024, "Amount of memory in MB")
// Backend configuration
qemuCmd = flags.String("qemu", "", "Path to the qemu binary (otherwise look in $PATH)")
qemuDetached = flags.Bool("detached", false, "Set qemu container to run in the background")
// Networking
networking = flags.String("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
usbEnabled = flags.Bool("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
for _, publish := range publishFlags {
p, err := NewPublishedPort(publish)
if err != nil {
return "", err
}
hostPort := p.Host
guestPort := p.Guest
forwardings = fmt.Sprintf("%s,hostfwd=%s::%d-:%d", forwardings, p.Protocol, hostPort, guestPort)
}
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))
}
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)
}

305
cmd/d2vm/run/util.go Normal file
View File

@ -0,0 +1,305 @@
// 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 run
import (
"bufio"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"strings"
)
// Handle flags with multiple occurrences
type MultipleFlag []string
func (f *MultipleFlag) String() string {
return "A multiple flag is a type of flag that can be repeated any number of times"
}
func (f *MultipleFlag) Set(value string) error {
*f = append(*f, value)
return nil
}
func (f *MultipleFlag) Type() string {
return "multiple-flag"
}
func GetStringValue(envKey string, flagVal string, defaultVal string) string {
var res string
// If defined, take the env variable
if _, ok := os.LookupEnv(envKey); ok {
res = os.Getenv(envKey)
}
// If a flag is specified, this value takes precedence
// Ignore cases where the flag carries the default value
if flagVal != "" && flagVal != defaultVal {
res = flagVal
}
// if we still don't have a value, use the default
if res == "" {
res = defaultVal
}
return res
}
func GetIntValue(envKey string, flagVal int, defaultVal int) int {
var res int
// If defined, take the env variable
if _, ok := os.LookupEnv(envKey); ok {
var err error
res, err = strconv.Atoi(os.Getenv(envKey))
if err != nil {
res = 0
}
}
// If a flag is specified, this value takes precedence
// Ignore cases where the flag carries the default value
if flagVal > 0 {
res = flagVal
}
// if we still don't have a value, use the default
if res == 0 {
res = defaultVal
}
return res
}
func GetBoolValue(envKey string, flagVal bool) bool {
var res bool
// If defined, take the env variable
if _, ok := os.LookupEnv(envKey); ok {
switch os.Getenv(envKey) {
case "":
res = false
case "0":
res = false
case "false":
res = false
case "FALSE":
res = false
case "1":
res = true
default:
// catches "true", "TRUE" or anything else
res = true
}
}
// If a flag is specified, this value takes precedence
if res != flagVal {
res = flagVal
}
return res
}
func StringToIntArray(l string, sep string) ([]int, error) {
var err error
if l == "" {
return []int{}, err
}
s := strings.Split(l, sep)
i := make([]int, len(s))
for idx := range s {
if i[idx], err = strconv.Atoi(s[idx]); err != nil {
return nil, err
}
}
return i, nil
}
// Convert a multi-line string into an array of strings
func SplitLines(in string) []string {
res := []string{}
s := bufio.NewScanner(strings.NewReader(in))
for s.Scan() {
res = append(res, s.Text())
}
return res
}
// This function parses the "size" parameter of a disk specification
// and returns the size in MB. The "size" parameter defaults to GB, but
// the unit can be explicitly set with either a G (for GB) or M (for
// MB). It returns the disk size in MB.
func GetDiskSizeMB(s string) (int, error) {
if s == "" {
return 0, nil
}
sz := len(s)
if strings.HasSuffix(s, "M") {
return strconv.Atoi(s[:sz-1])
}
if strings.HasSuffix(s, "G") {
s = s[:sz-1]
}
i, err := strconv.Atoi(s)
if err != nil {
return 0, err
}
return 1024 * i, nil
}
func ConvertMBtoGB(i int) int {
if i < 1024 {
return 1
}
if i%1024 == 0 {
return i / 1024
}
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
func (l *Disks) String() string {
return fmt.Sprint(*l)
}
// Set is used by flag to configure value from CLI
func (l *Disks) Set(value string) error {
d := DiskConfig{}
s := strings.Split(value, ",")
for _, p := range s {
c := strings.SplitN(p, "=", 2)
switch len(c) {
case 1:
// assume it is a filename even if no file=x
d.Path = c[0]
case 2:
switch c[0] {
case "file":
d.Path = c[1]
case "size":
size, err := GetDiskSizeMB(c[1])
if err != nil {
return err
}
d.Size = size
case "format":
d.Format = c[1]
default:
return fmt.Errorf("Unknown disk config: %s", c[0])
}
}
}
*l = append(*l, d)
return nil
}
func (l *Disks) Type() string {
return "disk"
}
// PublishedPort is used by some backends to expose a VMs port on the host
type PublishedPort struct {
Guest uint16
Host uint16
Protocol string
}
// NewPublishedPort parses a string of the form <host>:<guest>[/<tcp|udp>] and returns a PublishedPort structure
func NewPublishedPort(publish string) (PublishedPort, error) {
p := PublishedPort{}
slice := strings.Split(publish, ":")
if len(slice) < 2 {
return p, fmt.Errorf("Unable to parse the ports to be published, should be in format <host>:<guest> or <host>:<guest>/<tcp|udp>")
}
hostPort, err := strconv.ParseUint(slice[0], 10, 16)
if err != nil {
return p, fmt.Errorf("The provided hostPort can't be converted to uint16")
}
right := strings.Split(slice[1], "/")
protocol := "tcp"
if len(right) == 2 {
protocol = strings.TrimSpace(strings.ToLower(right[1]))
}
if protocol != "tcp" && protocol != "udp" {
return p, fmt.Errorf("Provided protocol is not valid, valid options are: udp and tcp")
}
guestPort, err := strconv.ParseUint(right[0], 10, 16)
if err != nil {
return p, fmt.Errorf("The provided guestPort can't be converted to uint16")
}
if hostPort < 1 || hostPort > 65535 {
return p, fmt.Errorf("Invalid hostPort: %d", hostPort)
}
if guestPort < 1 || guestPort > 65535 {
return p, fmt.Errorf("Invalid guestPort: %d", guestPort)
}
p.Guest = uint16(guestPort)
p.Host = uint16(hostPort)
p.Protocol = protocol
return p, nil
}
// CreateMetadataISO writes the provided meta data to an iso file in the given state directory
func CreateMetadataISO(state, data string, dataPath string) ([]string, error) {
var d []byte
// if we have neither data nor dataPath, nothing to return
switch {
case data != "" && dataPath != "":
return nil, fmt.Errorf("Cannot specify options for both data and dataPath")
case data == "" && dataPath == "":
return []string{}, nil
case data != "":
d = []byte(data)
case dataPath != "":
var err error
d, err = ioutil.ReadFile(dataPath)
if err != nil {
return nil, fmt.Errorf("Cannot read user data from path %s: %v", dataPath, err)
}
}
isoPath := filepath.Join(state, "data.iso")
if err := WriteMetadataISO(isoPath, d); err != nil {
return nil, fmt.Errorf("Cannot write user data ISO: %v", err)
}
return []string{isoPath}, nil
}

331
cmd/d2vm/run/vbox.go Normal file
View File

@ -0,0 +1,331 @@
package run
import (
"bytes"
"fmt"
"io"
"net"
"os"
"os/exec"
"os/signal"
"path/filepath"
"runtime"
"strconv"
"strings"
"github.com/containerd/console"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
var (
VboxCmd = &cobra.Command{
Use: "vbox [options] image-path",
Args: cobra.ExactArgs(1),
Run: Vbox,
}
vboxmanageFlag *string
vmName *string
networks VBNetworks
)
func init() {
flags := VboxCmd.Flags()
// Display flags
enableGUI = flags.Bool("gui", false, "Show the VM GUI")
// vbox options
vboxmanageFlag = flags.String("vboxmanage", "VBoxManage", "VBoxManage binary to use")
vmName = flags.String("name", "", "Name of the Virtualbox VM")
// Paths and settings for disks
flags.Var(&disks, "disk", "Disk config, may be repeated. [file=]path[,size=1G][,format=raw]")
// VM configuration
cpus = flags.Uint("cpus", 1, "Number of CPUs")
mem = flags.Uint("mem", 1024, "Amount of memory in MB")
// networking
flags.Var(&networks, "networking", "Network config, may be repeated. [type=](null|nat|bridged|intnet|hostonly|generic|natnetwork[<devicename>])[,[bridge|host]adapter=<interface>]")
if runtime.GOOS == "windows" {
log.Fatalf("TODO: Windows is not yet supported")
}
}
func Vbox(cmd *cobra.Command, args []string) {
path := args[0]
vboxmanage, err := exec.LookPath(*vboxmanageFlag)
if err != nil {
log.Fatalf("Cannot find management binary %s: %v", *vboxmanageFlag, err)
}
name := *vmName
if name == "" {
name = strings.TrimSuffix(filepath.Base(path), filepath.Ext(path))
}
// remove machine in case it already exists
cleanup(vboxmanage, name)
_, out, err := manage(vboxmanage, "createvm", "--name", name, "--register")
if err != nil {
log.Fatalf("createvm error: %v\n%s", err, out)
}
_, out, err = manage(vboxmanage, "modifyvm", name, "--acpi", "on")
if err != nil {
log.Fatalf("modifyvm --acpi error: %v\n%s", err, out)
}
_, out, err = manage(vboxmanage, "modifyvm", name, "--memory", fmt.Sprintf("%d", *mem))
if err != nil {
log.Fatalf("modifyvm --memory error: %v\n%s", err, out)
}
_, out, err = manage(vboxmanage, "modifyvm", name, "--cpus", fmt.Sprintf("%d", *cpus))
if err != nil {
log.Fatalf("modifyvm --cpus error: %v\n%s", err, out)
}
_, out, err = manage(vboxmanage, "modifyvm", name, "--firmware", "bios")
if err != nil {
log.Fatalf("modifyvm --firmware error: %v\n%s", err, out)
}
// set up serial console
_, out, err = manage(vboxmanage, "modifyvm", name, "--uart1", "0x3F8", "4")
if err != nil {
log.Fatalf("modifyvm --uart error: %v\n%s", err, out)
}
consolePath := filepath.Join(os.TempDir(), "d2vm-vb", name, "console")
if err := os.MkdirAll(filepath.Dir(consolePath), os.ModePerm); err != nil {
log.Fatal(err)
}
if runtime.GOOS != "windows" {
consolePath, err = filepath.Abs(consolePath)
if err != nil {
log.Fatalf("Bad path: %v", err)
}
} else {
// TODO use a named pipe on Windows
}
_, out, err = manage(vboxmanage, "modifyvm", name, "--uartmode1", "client", consolePath)
if err != nil {
log.Fatalf("modifyvm --uartmode error: %v\n%s", err, out)
}
_, out, err = manage(vboxmanage, "storagectl", name, "--name", "IDE Controller", "--add", "ide")
if err != nil {
log.Fatalf("storagectl error: %v\n%s", err, out)
}
_, out, err = manage(vboxmanage, "storageattach", name, "--storagectl", "IDE Controller", "--port", "1", "--device", "0", "--type", "hdd", "--medium", path)
if err != nil {
log.Fatalf("storageattach error: %v\n%s", err, out)
}
_, out, err = manage(vboxmanage, "modifyvm", name, "--boot1", "disk")
if err != nil {
log.Fatalf("modifyvm --boot error: %v\n%s", err, out)
}
if len(disks) > 0 {
_, out, err = manage(vboxmanage, "storagectl", name, "--name", "SATA", "--add", "sata")
if err != nil {
log.Fatalf("storagectl error: %v\n%s", err, out)
}
}
for i, d := range disks {
id := strconv.Itoa(i)
if d.Size != 0 && d.Format == "" {
d.Format = "raw"
}
if d.Format != "raw" && d.Path == "" {
log.Fatal("vbox currently can only create raw disks")
}
if d.Path == "" && d.Size == 0 {
log.Fatal("please specify an existing disk file or a size")
}
if d.Path == "" {
d.Path = "disk" + id + ".img"
if err := os.Truncate(d.Path, int64(d.Size)*int64(1048576)); err != nil {
log.Fatalf("Cannot create disk: %v", err)
}
}
_, out, err = manage(vboxmanage, "storageattach", name, "--storagectl", "SATA", "--port", "0", "--device", id, "--type", "hdd", "--medium", d.Path)
if err != nil {
log.Fatalf("storageattach error: %v\n%s", err, out)
}
}
for i, d := range networks {
nic := i + 1
_, out, err = manage(vboxmanage, "modifyvm", name, fmt.Sprintf("--nictype%d", nic), "virtio")
if err != nil {
log.Fatalf("modifyvm --nictype error: %v\n%s", err, out)
}
_, out, err = manage(vboxmanage, "modifyvm", name, fmt.Sprintf("--nic%d", nic), d.Type)
if err != nil {
log.Fatalf("modifyvm --nic error: %v\n%s", err, out)
}
if d.Type == "hostonly" {
_, out, err = manage(vboxmanage, "modifyvm", name, fmt.Sprintf("--hostonlyadapter%d", nic), d.Adapter)
if err != nil {
log.Fatalf("modifyvm --hostonlyadapter error: %v\n%s", err, out)
}
} else if d.Type == "bridged" {
_, out, err = manage(vboxmanage, "modifyvm", name, fmt.Sprintf("--bridgeadapter%d", nic), d.Adapter)
if err != nil {
log.Fatalf("modifyvm --bridgeadapter error: %v\n%s", err, out)
}
}
_, out, err = manage(vboxmanage, "modifyvm", name, fmt.Sprintf("--cableconnected%d", nic), "on")
if err != nil {
log.Fatalf("modifyvm --cableconnected error: %v\n%s", err, out)
}
}
// create socket
_ = os.Remove(consolePath)
ln, err := net.Listen("unix", consolePath)
if err != nil {
log.Fatalf("Cannot listen on console socket %s: %v", consolePath, err)
}
var vmType string
if *enableGUI {
vmType = "gui"
} else {
vmType = "headless"
}
term := console.Current()
ws, err := term.Size()
if err != nil {
log.Fatal(err)
}
if err := term.Resize(ws); err != nil {
log.Fatal(err)
}
if err := term.SetRaw(); err != nil {
log.Fatal(err)
}
defer term.Close()
_, out, err = manage(vboxmanage, "startvm", name, "--type", vmType)
if err != nil {
log.Fatalf("startvm error: %v\n%s", err, out)
}
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
go func() {
<-c
cleanup(vboxmanage, name)
os.Exit(1)
}()
socket, err := ln.Accept()
if err != nil {
log.Fatalf("Accept error: %v", err)
}
go func() {
if _, err := io.Copy(socket, term); err != nil {
cleanup(vboxmanage, name)
log.Fatalf("Copy error: %v", err)
}
cleanup(vboxmanage, name)
os.Exit(0)
}()
go func() {
if _, err := io.Copy(term, socket); err != nil {
cleanup(vboxmanage, name)
log.Fatalf("Copy error: %v", err)
}
cleanup(vboxmanage, name)
os.Exit(0)
}()
// wait forever
select {}
}
func cleanup(vboxmanage string, name string) {
if _, _, err := manage(vboxmanage, "controlvm", name, "poweroff"); err != nil {
return
}
_, out, err := manage(vboxmanage, "storageattach", name, "--storagectl", "IDE Controller", "--port", "1", "--device", "0", "--type", "hdd", "--medium", "emptydrive")
if err != nil {
log.Errorf("storageattach error: %v\n%s", err, out)
}
for i := range disks {
id := strconv.Itoa(i)
_, out, err := manage(vboxmanage, "storageattach", name, "--storagectl", "SATA", "--port", "0", "--device", id, "--type", "hdd", "--medium", "emptydrive")
if err != nil {
log.Errorf("storageattach error: %v\n%s", err, out)
}
}
if _, out, err = manage(vboxmanage, "unregistervm", name, "--delete"); err != nil {
log.Errorf("unregistervm error: %v\n%s", err, out)
}
}
func manage(vboxmanage string, args ...string) (string, string, error) {
cmd := exec.Command(vboxmanage, args...)
log.Debugf("[VBOX]: %s %s", vboxmanage, strings.Join(args, " "))
var stdout bytes.Buffer
var stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
return stdout.String(), stderr.String(), err
}
// VBNetwork is the config for a Virtual Box network
type VBNetwork struct {
Type string
Adapter string
}
// VBNetworks is the type for a list of VBNetwork
type VBNetworks []VBNetwork
func (l *VBNetworks) String() string {
return fmt.Sprint(*l)
}
func (l *VBNetworks) Type() string {
return "vbnetworks"
}
// Set is used by flag to configure value from CLI
func (l *VBNetworks) Set(value string) error {
d := VBNetwork{}
s := strings.Split(value, ",")
for _, p := range s {
c := strings.SplitN(p, "=", 2)
switch len(c) {
case 1:
d.Type = c[0]
case 2:
switch c[0] {
case "type":
d.Type = c[1]
case "adapter", "bridgeadapter", "hostadapter":
d.Adapter = c[1]
default:
return fmt.Errorf("Unknown network config: %s", c[0])
}
}
}
*l = append(*l, d)
return nil
}