mirror of
https://github.com/linka-cloud/d2vm.git
synced 2024-11-22 15:56:24 +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:
parent
29d953c14d
commit
62d8a1019d
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,6 +1,10 @@
|
|||||||
.idea
|
.idea
|
||||||
tests
|
tests
|
||||||
|
scratch
|
||||||
*.qcow2
|
*.qcow2
|
||||||
|
*.vmdk
|
||||||
|
*.vdi
|
||||||
|
|
||||||
dist/
|
dist/
|
||||||
|
/d2vm
|
||||||
.goreleaser.yaml
|
.goreleaser.yaml
|
||||||
|
5
Makefile
5
Makefile
@ -29,7 +29,8 @@ docker-push:
|
|||||||
@docker image push -a $(DOCKER_IMAGE)
|
@docker image push -a $(DOCKER_IMAGE)
|
||||||
|
|
||||||
docker-build:
|
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:
|
||||||
@docker run --rm -i -t \
|
@docker run --rm -i -t \
|
||||||
@ -40,4 +41,4 @@ docker-run:
|
|||||||
$(DOCKER_IMAGE) bash
|
$(DOCKER_IMAGE) bash
|
||||||
|
|
||||||
build:
|
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
|
||||||
|
32
README.md
32
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'
|
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:
|
## Supported VM Linux distributions:
|
||||||
|
|
||||||
Working and tested:
|
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:
|
### Converting an existing Docker Image to VM image:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
b2vm convert --help
|
d2vm convert --help
|
||||||
```
|
```
|
||||||
```
|
```
|
||||||
Convert Docker image to vm image
|
Convert Docker image to vm image
|
||||||
@ -84,14 +86,13 @@ Usage:
|
|||||||
d2vm convert [docker image] [flags]
|
d2vm convert [docker image] [flags]
|
||||||
|
|
||||||
Flags:
|
Flags:
|
||||||
-d, --debug Enable Debug output
|
-d, --debug Enable Debug output
|
||||||
-f, --force Override output qcow2 image
|
-f, --force Override output qcow2 image
|
||||||
-h, --help help for convert
|
-h, --help help for convert
|
||||||
-o, --output string The output image (default "disk0.qcow2")
|
-o, --output string The output image, the extension determine the image format. Supported formats: qcow2 qed raw vdi vhd vmdk (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")
|
||||||
-p, --password string The Root user password (default "root")
|
--pull Always pull docker image
|
||||||
--pull Always pull docker image
|
-s, --size string The output image size (default "10G")
|
||||||
-s, --size string The output image size (default "10G")
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -232,11 +233,10 @@ Usage:
|
|||||||
Flags:
|
Flags:
|
||||||
--build-arg stringArray Set build-time variables
|
--build-arg stringArray Set build-time variables
|
||||||
-d, --debug Enable Debug output
|
-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
|
--force Override output image
|
||||||
-h, --help help for build
|
-h, --help help for build
|
||||||
-o, --output string The output image (default "disk0.qcow2")
|
-o, --output string The output image, the extension determine the image format. Supported formats: qcow2 qed raw vdi vhd vmdk (default "disk0.qcow2")
|
||||||
-O, --output-format string The output image format, supported formats: qcow2 qed raw vdi vhd vmdk (default "qcow2")
|
|
||||||
-p, --password string Root user password (default "root")
|
-p, --password string Root user password (default "root")
|
||||||
-s, --size string The output image size (default "10G")
|
-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:
|
Or if you want to create a VirtualBox image:
|
||||||
|
|
||||||
```bash
|
```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
|
### 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 ?
|
- [ ] Create service from `ENTRYPOINT` `CMD` `WORKDIR` and `ENV` instructions ?
|
||||||
- [ ] Inject Image `ENV` variables into `.bashrc` or other service environment file ?
|
- [ ] 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).
|
||||||
|
@ -154,7 +154,7 @@ func NewBuilder(ctx context.Context, workdir, imgTag, disk string, size int64, o
|
|||||||
osRelease: osRelease,
|
osRelease: osRelease,
|
||||||
img: img,
|
img: img,
|
||||||
diskRaw: filepath.Join(workdir, disk+".raw"),
|
diskRaw: filepath.Join(workdir, disk+".raw"),
|
||||||
diskOut: filepath.Join(workdir, disk+".qcow2"),
|
diskOut: filepath.Join(workdir, disk+"."+format),
|
||||||
format: f,
|
format: f,
|
||||||
size: size,
|
size: size,
|
||||||
mbrPath: mbrBin,
|
mbrPath: mbrBin,
|
||||||
|
@ -15,6 +15,9 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
@ -35,16 +38,23 @@ var (
|
|||||||
Short: "Build a vm image from Dockerfile",
|
Short: "Build a vm image from Dockerfile",
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
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)
|
size, err := parseSize(size)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
exec.SetDebug(debug)
|
exec.SetDebug(debug)
|
||||||
logrus.Infof("building docker image from %s", file)
|
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 {
|
if err := docker.Build(cmd.Context(), tag, file, args[0], buildArgs...); err != nil {
|
||||||
return err
|
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() {
|
func init() {
|
||||||
rootCmd.AddCommand(buildCmd)
|
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().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, the extension determine the image format. Supported formats: "+strings.Join(d2vm.OutputFormats(), " "))
|
||||||
buildCmd.Flags().StringVarP(&output, "output", "o", output, "The output image")
|
|
||||||
buildCmd.Flags().StringVarP(&password, "password", "p", "root", "Root user password")
|
buildCmd.Flags().StringVarP(&password, "password", "p", "root", "Root user password")
|
||||||
buildCmd.Flags().StringVarP(&size, "size", "s", "10G", "The output image size")
|
buildCmd.Flags().StringVarP(&size, "size", "s", "10G", "The output image size")
|
||||||
buildCmd.Flags().BoolVarP(&debug, "debug", "d", false, "Enable Debug output")
|
buildCmd.Flags().BoolVarP(&debug, "debug", "d", false, "Enable Debug output")
|
||||||
|
@ -17,6 +17,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/c2h5oh/datasize"
|
"github.com/c2h5oh/datasize"
|
||||||
@ -37,6 +38,9 @@ var (
|
|||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
SilenceUsage: true,
|
SilenceUsage: true,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
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]
|
img := args[0]
|
||||||
tag := "latest"
|
tag := "latest"
|
||||||
if parts := strings.Split(img, ":"); len(parts) > 1 {
|
if parts := strings.Split(img, ":"); len(parts) > 1 {
|
||||||
@ -74,7 +78,7 @@ var (
|
|||||||
return err
|
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() {
|
func init() {
|
||||||
convertCmd.Flags().BoolVar(&pull, "pull", false, "Always pull docker image")
|
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, the extension determine the image format. Supported formats: "+strings.Join(d2vm.OutputFormats(), " "))
|
||||||
convertCmd.Flags().StringVarP(&output, "output", "o", output, "The output image")
|
|
||||||
convertCmd.Flags().StringVarP(&password, "password", "p", "root", "The Root user password")
|
convertCmd.Flags().StringVarP(&password, "password", "p", "root", "The Root user password")
|
||||||
convertCmd.Flags().StringVarP(&size, "size", "s", "10G", "The output image size")
|
convertCmd.Flags().StringVarP(&size, "size", "s", "10G", "The output image size")
|
||||||
convertCmd.Flags().BoolVarP(&debug, "debug", "d", false, "Enable Debug output")
|
convertCmd.Flags().BoolVarP(&debug, "debug", "d", false, "Enable Debug output")
|
||||||
|
42
cmd/d2vm/run.go
Normal file
42
cmd/d2vm/run.go
Normal 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
77
cmd/d2vm/run/metadata.go
Normal 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
454
cmd/d2vm/run/qemu.go
Normal 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
305
cmd/d2vm/run/util.go
Normal 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
331
cmd/d2vm/run/vbox.go
Normal 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
|
||||||
|
}
|
@ -20,6 +20,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
@ -27,7 +28,7 @@ import (
|
|||||||
"go.linka.cloud/d2vm/pkg/docker"
|
"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()
|
imgUUID := uuid.New().String()
|
||||||
tmpPath := filepath.Join(os.TempDir(), "d2vm", imgUUID)
|
tmpPath := filepath.Join(os.TempDir(), "d2vm", imgUUID)
|
||||||
if err := os.MkdirAll(tmpPath, os.ModePerm); err != nil {
|
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)
|
defer docker.Remove(ctx, imgUUID)
|
||||||
|
|
||||||
logrus.Infof("creating vm image")
|
logrus.Infof("creating vm image")
|
||||||
|
format := strings.TrimPrefix(filepath.Ext(output), ".")
|
||||||
b, err := NewBuilder(ctx, tmpPath, imgUUID, "", size, r, format)
|
b, err := NewBuilder(ctx, tmpPath, imgUUID, "", size, r, format)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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 {
|
if err := os.RemoveAll(output); err != nil {
|
||||||
return err
|
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 err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
@ -59,15 +59,15 @@ RUN sudo sed -i "s|ExecStart=.*|ExecStart=-/sbin/agetty --autologin ${USER} --ke
|
|||||||
*00-netconf.yaml*
|
*00-netconf.yaml*
|
||||||
```yaml
|
```yaml
|
||||||
network:
|
network:
|
||||||
version: 2
|
version: 2
|
||||||
renderer: networkd
|
renderer: networkd
|
||||||
ethernets:
|
ethernets:
|
||||||
eth0:
|
eth0:
|
||||||
dhcp4: true
|
dhcp4: true
|
||||||
nameservers:
|
nameservers:
|
||||||
addresses:
|
addresses:
|
||||||
- 8.8.8.8
|
- 8.8.8.8
|
||||||
- 8.8.4.4
|
- 8.8.4.4
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
5
go.mod
5
go.mod
@ -4,9 +4,11 @@ go 1.17
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/c2h5oh/datasize v0.0.0-20200825124411-48ed595a09d2
|
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/go-containerregistry v0.8.0
|
||||||
github.com/google/uuid v1.3.0
|
github.com/google/uuid v1.3.0
|
||||||
github.com/joho/godotenv v1.4.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/sirupsen/logrus v1.8.1
|
||||||
github.com/spf13/cobra v1.4.0
|
github.com/spf13/cobra v1.4.0
|
||||||
github.com/stretchr/testify v1.7.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/distribution v2.7.1+incompatible // indirect
|
||||||
github.com/docker/docker v20.10.12+incompatible // indirect
|
github.com/docker/docker v20.10.12+incompatible // indirect
|
||||||
github.com/docker/docker-credential-helpers v0.6.4 // 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/docker/go-units v0.4.0 // indirect
|
||||||
github.com/gogo/protobuf v1.3.2 // indirect
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
github.com/golang/protobuf v1.5.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/spf13/pflag v1.0.5 // indirect
|
||||||
github.com/vbatts/tar-split v0.11.2 // indirect
|
github.com/vbatts/tar-split v0.11.2 // indirect
|
||||||
go.uber.org/atomic v1.7.0 // 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/sync v0.0.0-20210220032951-036812b2e83c // indirect
|
||||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
|
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
|
||||||
google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa // indirect
|
google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa // indirect
|
||||||
|
7
go.sum
7
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 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.1/go.mod h1:XUsP6YE/mKtz6bxc+I8UiKKTP04qjQL4qcS3XoQ5xkw=
|
||||||
github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ=
|
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.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-beta.2.0.20190828155532-0293cbd26c69/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
|
||||||
github.com/containerd/containerd v1.3.0/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 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 h1:axCks+yV+2MR3/kZhAmy07yC56WZ2Pwu/fKWtKuZB0o=
|
||||||
github.com/docker/docker-credential-helpers v0.6.4/go.mod h1:ofX3UI0Gz1TteYBjtgs07O36Pyasyp66D2uKT7H8W1c=
|
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.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-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-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=
|
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.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||||
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
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/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 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/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
|
@ -18,12 +18,24 @@ import (
|
|||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
_ "embed"
|
_ "embed"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"go.linka.cloud/d2vm/pkg/exec"
|
"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 {
|
func FormatImgName(name string) string {
|
||||||
s := strings.Replace(name, ":", "-", -1)
|
s := strings.Replace(name, ":", "-", -1)
|
||||||
s = strings.Replace(s, "/", "_", -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 {
|
func Pull(ctx context.Context, tag string) error {
|
||||||
return Cmd(ctx, "image", "pull", tag)
|
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...)...)
|
||||||
|
}
|
||||||
|
@ -3,4 +3,5 @@ package d2vm
|
|||||||
var (
|
var (
|
||||||
Version = ""
|
Version = ""
|
||||||
BuildDate = ""
|
BuildDate = ""
|
||||||
|
Image = ""
|
||||||
)
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user