diff --git a/.gitignore b/.gitignore index 2e90faf..9b8164a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,10 @@ .idea tests +scratch *.qcow2 +*.vmdk +*.vdi dist/ +/d2vm .goreleaser.yaml diff --git a/Makefile b/Makefile index c63645d..377e0e8 100644 --- a/Makefile +++ b/Makefile @@ -29,7 +29,8 @@ docker-push: @docker image push -a $(DOCKER_IMAGE) docker-build: - @docker image build -t $(DOCKER_IMAGE):$(VERSION) -t $(DOCKER_IMAGE):latest . + @docker image build -t $(DOCKER_IMAGE):$(VERSION) . + @echo $(VERSION)|grep -q '-' || docker image tag $(DOCKER_IMAGE):latest $(DOCKER_IMAGE):$(VERSION) docker-run: @docker run --rm -i -t \ @@ -40,4 +41,4 @@ docker-run: $(DOCKER_IMAGE) bash build: - @go build -o d2vm -ldflags "-s -w -X '$(MODULE).Version=$(VERSION)' -X '$(MODULE).BuildDate=$(shell date)'" ./cmd/d2vm + @go build -o d2vm -ldflags "-s -w -X '$(MODULE).Image=$(DOCKER_IMAGE)' -X '$(MODULE).Version=$(VERSION)' -X '$(MODULE).BuildDate=$(shell date)'" ./cmd/d2vm diff --git a/README.md b/README.md index d891910..f873a5d 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,8 @@ If you want to run it on **OSX** or **Windows** (the last one is totally unteste alias d2vm='docker run --rm -i -t --privileged -v /var/run/docker.sock:/var/run/docker.sock -v $PWD:/build -w /build linkacloud/d2vm' ``` +**Starting from v0.1.0, d2vm automatically run build and convert commands inside Docker when not running on linux**. + ## Supported VM Linux distributions: Working and tested: @@ -75,7 +77,7 @@ d2vm: aliased to docker run --rm -i -t --privileged -v /var/run/docker.sock:/var ### Converting an existing Docker Image to VM image: ```bash -b2vm convert --help +d2vm convert --help ``` ``` Convert Docker image to vm image @@ -84,14 +86,13 @@ Usage: d2vm convert [docker image] [flags] Flags: - -d, --debug Enable Debug output - -f, --force Override output qcow2 image - -h, --help help for convert - -o, --output string The output image (default "disk0.qcow2") - -O, --output-format string The output image format, supported formats: qcow2 qed raw vdi vhd vmdk (default "qcow2") - -p, --password string The Root user password (default "root") - --pull Always pull docker image - -s, --size string The output image size (default "10G") + -d, --debug Enable Debug output + -f, --force Override output qcow2 image + -h, --help help for convert + -o, --output string The output image, the extension determine the image format. Supported formats: qcow2 qed raw vdi vhd vmdk (default "disk0.qcow2") + -p, --password string The Root user password (default "root") + --pull Always pull docker image + -s, --size string The output image size (default "10G") ``` @@ -232,11 +233,10 @@ Usage: Flags: --build-arg stringArray Set build-time variables -d, --debug Enable Debug output - -f, --file string Name of the Dockerfile (Default is 'PATH/Dockerfile') (default "Dockerfile") + -f, --file string Name of the Dockerfile --force Override output image -h, --help help for build - -o, --output string The output image (default "disk0.qcow2") - -O, --output-format string The output image format, supported formats: qcow2 qed raw vdi vhd vmdk (default "qcow2") + -o, --output string The output image, the extension determine the image format. Supported formats: qcow2 qed raw vdi vhd vmdk (default "disk0.qcow2") -p, --password string Root user password (default "root") -s, --size string The output image size (default "10G") @@ -249,7 +249,7 @@ sudo d2vm build -p MyP4Ssw0rd -f ubuntu.Dockerfile -o ubuntu.qcow2 . Or if you want to create a VirtualBox image: ```bash -sudo d2vm build -p MyP4Ssw0rd -f ubuntu.Dockerfile -O vdi -o ubuntu.vdi . +sudo d2vm build -p MyP4Ssw0rd -f ubuntu.Dockerfile -o ubuntu.vdi . ``` ### Complete example @@ -265,4 +265,8 @@ You can find the Dockerfiles used to install the Kernel in the [templates](templ - [ ] Create service from `ENTRYPOINT` `CMD` `WORKDIR` and `ENV` instructions ? - [ ] Inject Image `ENV` variables into `.bashrc` or other service environment file ? -- [ ] Use image layers to create *rootfs* instead of container ? +- [x] Use image layers to create *rootfs* instead of container ? + +### Acknowledgments + +The *run* commands are adapted from [linuxkit](https://github.com/docker/linuxkit). diff --git a/builder.go b/builder.go index dd321bb..8b599e4 100644 --- a/builder.go +++ b/builder.go @@ -154,7 +154,7 @@ func NewBuilder(ctx context.Context, workdir, imgTag, disk string, size int64, o osRelease: osRelease, img: img, diskRaw: filepath.Join(workdir, disk+".raw"), - diskOut: filepath.Join(workdir, disk+".qcow2"), + diskOut: filepath.Join(workdir, disk+"."+format), format: f, size: size, mbrPath: mbrBin, diff --git a/cmd/d2vm/build.go b/cmd/d2vm/build.go index 521d035..06cd361 100644 --- a/cmd/d2vm/build.go +++ b/cmd/d2vm/build.go @@ -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") diff --git a/cmd/d2vm/convert.go b/cmd/d2vm/convert.go index 9d309ea..0500d61 100644 --- a/cmd/d2vm/convert.go +++ b/cmd/d2vm/convert.go @@ -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") diff --git a/cmd/d2vm/run.go b/cmd/d2vm/run.go new file mode 100644 index 0000000..9643f9e --- /dev/null +++ b/cmd/d2vm/run.go @@ -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") +} diff --git a/cmd/d2vm/run/metadata.go b/cmd/d2vm/run/metadata.go new file mode 100644 index 0000000..8ff4371 --- /dev/null +++ b/cmd/d2vm/run/metadata.go @@ -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) + } +} diff --git a/cmd/d2vm/run/qemu.go b/cmd/d2vm/run/qemu.go new file mode 100644 index 0000000..f975e58 --- /dev/null +++ b/cmd/d2vm/run/qemu.go @@ -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) +} diff --git a/cmd/d2vm/run/util.go b/cmd/d2vm/run/util.go new file mode 100644 index 0000000..9215fba --- /dev/null +++ b/cmd/d2vm/run/util.go @@ -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 :[/] 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 : or :/") + } + + 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 +} diff --git a/cmd/d2vm/run/vbox.go b/cmd/d2vm/run/vbox.go new file mode 100644 index 0000000..9d2bdc4 --- /dev/null +++ b/cmd/d2vm/run/vbox.go @@ -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[])[,[bridge|host]adapter=]") + + 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 +} diff --git a/convert.go b/convert.go index 8852979..d18c9c9 100644 --- a/convert.go +++ b/convert.go @@ -20,6 +20,7 @@ import ( "io" "os" "path/filepath" + "strings" "github.com/google/uuid" "github.com/sirupsen/logrus" @@ -27,7 +28,7 @@ import ( "go.linka.cloud/d2vm/pkg/docker" ) -func Convert(ctx context.Context, img string, size int64, password string, output string, format string) error { +func Convert(ctx context.Context, img string, size int64, password string, output string) error { imgUUID := uuid.New().String() tmpPath := filepath.Join(os.TempDir(), "d2vm", imgUUID) if err := os.MkdirAll(tmpPath, os.ModePerm); err != nil { @@ -62,6 +63,7 @@ func Convert(ctx context.Context, img string, size int64, password string, outpu defer docker.Remove(ctx, imgUUID) logrus.Infof("creating vm image") + format := strings.TrimPrefix(filepath.Ext(output), ".") b, err := NewBuilder(ctx, tmpPath, imgUUID, "", size, r, format) if err != nil { return err @@ -73,7 +75,7 @@ func Convert(ctx context.Context, img string, size int64, password string, outpu if err := os.RemoveAll(output); err != nil { return err } - if err := MoveFile(filepath.Join(tmpPath, "disk0.qcow2"), output); err != nil { + if err := MoveFile(filepath.Join(tmpPath, "disk0."+format), output); err != nil { return err } return nil diff --git a/examples/full/README.md b/examples/full/README.md index 3f990e4..9275779 100644 --- a/examples/full/README.md +++ b/examples/full/README.md @@ -59,15 +59,15 @@ RUN sudo sed -i "s|ExecStart=.*|ExecStart=-/sbin/agetty --autologin ${USER} --ke *00-netconf.yaml* ```yaml network: - version: 2 - renderer: networkd - ethernets: - eth0: - dhcp4: true - nameservers: - addresses: - - 8.8.8.8 - - 8.8.4.4 + version: 2 + renderer: networkd + ethernets: + eth0: + dhcp4: true + nameservers: + addresses: + - 8.8.8.8 + - 8.8.4.4 ``` diff --git a/go.mod b/go.mod index 942b114..e82d403 100644 --- a/go.mod +++ b/go.mod @@ -4,9 +4,11 @@ go 1.17 require ( github.com/c2h5oh/datasize v0.0.0-20200825124411-48ed595a09d2 + github.com/containerd/console v1.0.3 github.com/google/go-containerregistry v0.8.0 github.com/google/uuid v1.3.0 github.com/joho/godotenv v1.4.0 + github.com/rn/iso9660wrap v0.0.0-20171120145750-baf8d62ad315 github.com/sirupsen/logrus v1.8.1 github.com/spf13/cobra v1.4.0 github.com/stretchr/testify v1.7.0 @@ -22,7 +24,7 @@ require ( github.com/docker/distribution v2.7.1+incompatible // indirect github.com/docker/docker v20.10.12+incompatible // indirect github.com/docker/docker-credential-helpers v0.6.4 // indirect - github.com/docker/go-connections v0.4.0 // indirect + github.com/docker/go-connections v0.4.1-0.20190612165340-fd1b1942c4d5 // indirect github.com/docker/go-units v0.4.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.2 // indirect @@ -37,7 +39,6 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/vbatts/tar-split v0.11.2 // indirect go.uber.org/atomic v1.7.0 // indirect - golang.org/x/net v0.0.0-20211216030914-fe4d6282115f // indirect golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa // indirect diff --git a/go.sum b/go.sum index a31e079..fc832e2 100644 --- a/go.sum +++ b/go.sum @@ -168,6 +168,8 @@ github.com/containerd/console v0.0.0-20181022165439-0650fd9eeb50/go.mod h1:Tj/on github.com/containerd/console v0.0.0-20191206165004-02ecf6a7291e/go.mod h1:8Pf4gM6VEbTNRIT26AyyU7hxdQU3MvAvxVI0sc00XBE= github.com/containerd/console v1.0.1/go.mod h1:XUsP6YE/mKtz6bxc+I8UiKKTP04qjQL4qcS3XoQ5xkw= github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ= +github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= +github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= github.com/containerd/containerd v1.2.10/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= github.com/containerd/containerd v1.3.0-beta.2.0.20190828155532-0293cbd26c69/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= github.com/containerd/containerd v1.3.0/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= @@ -280,8 +282,9 @@ github.com/docker/docker v20.10.12+incompatible h1:CEeNmFM0QZIsJCZKMkZx0ZcahTiew github.com/docker/docker v20.10.12+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.6.4 h1:axCks+yV+2MR3/kZhAmy07yC56WZ2Pwu/fKWtKuZB0o= github.com/docker/docker-credential-helpers v0.6.4/go.mod h1:ofX3UI0Gz1TteYBjtgs07O36Pyasyp66D2uKT7H8W1c= -github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-connections v0.4.1-0.20190612165340-fd1b1942c4d5 h1:2o8D0hdBky229bNnc7a8bAZkeVMpH4qsp2Rmt4g/+Zk= +github.com/docker/go-connections v0.4.1-0.20190612165340-fd1b1942c4d5/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-events v0.0.0-20170721190031-9461782956ad/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI= @@ -677,6 +680,8 @@ github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rn/iso9660wrap v0.0.0-20171120145750-baf8d62ad315 h1:DjbO/+j3556fy07xoEM/MyLYN3WwwYyt4dHRC5U+KN8= +github.com/rn/iso9660wrap v0.0.0-20171120145750-baf8d62ad315/go.mod h1:qrZfINtl+sTGgS3elQWqWsD2Ke4Il5jDzBr2Q+lzuuE= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= diff --git a/pkg/docker/docker.go b/pkg/docker/docker.go index 6b44a62..cc02294 100644 --- a/pkg/docker/docker.go +++ b/pkg/docker/docker.go @@ -18,12 +18,24 @@ import ( "bufio" "context" _ "embed" + "fmt" + "os" "path/filepath" + "runtime" "strings" + "github.com/sirupsen/logrus" + "go.linka.cloud/d2vm/pkg/exec" ) +func dockerSocket() string { + if runtime.GOOS == "windows" { + return "//var/run/docker.sock" + } + return "/var/run/docker.sock" +} + func FormatImgName(name string) string { s := strings.Replace(name, ":", "-", -1) s = strings.Replace(s, "/", "_", -1) @@ -70,3 +82,34 @@ func ImageList(ctx context.Context, tag string) ([]string, error) { func Pull(ctx context.Context, tag string) error { return Cmd(ctx, "image", "pull", tag) } + +func RunInteractiveAndRemove(ctx context.Context, args ...string) error { + logrus.Tracef("running 'docker run --rm -i -t %s'", strings.Join(args, " ")) + cmd := exec.CommandContext(ctx, "docker", append([]string{"run", "--rm", "-it"}, args...)...) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() +} + +func RunD2VM(ctx context.Context, image, version, cmd string, args ...string) error { + pwd, err := os.Getwd() + if err != nil { + return err + } + if version == "" { + version = "latest" + } + a := []string{ + "--privileged", + "-v", + fmt.Sprintf("%s:/var/run/docker.sock", dockerSocket()), + "-v", + fmt.Sprintf("%s:/d2vm", pwd), + "-w", + "/d2vm", + fmt.Sprintf("%s:%s", image, version), + cmd, + } + return RunInteractiveAndRemove(ctx, append(a, args...)...) +} diff --git a/version.go b/version.go index 2594d30..ed3a300 100644 --- a/version.go +++ b/version.go @@ -3,4 +3,5 @@ package d2vm var ( Version = "" BuildDate = "" + Image = "" )