diff --git a/.github/workflows/compile.yml b/.github/workflows/compile.yml index 624f80e6..f8cdf407 100644 --- a/.github/workflows/compile.yml +++ b/.github/workflows/compile.yml @@ -43,6 +43,14 @@ jobs: go-version-file: go.mod - name: Build run: make + + - name: Install qemu-img + run: | + brew install qemu + + - name: Verify qemu-img is installed + run: qemu-img --version + - name: Test if: matrix.os != 'macOS-14' run: make test diff --git a/OWNERS b/OWNERS index 1882ebd1..5bcbf550 100644 --- a/OWNERS +++ b/OWNERS @@ -8,4 +8,5 @@ reviewers: - baude - cfergeau - lstocchi + - nirs - praveenkumar diff --git a/README.md b/README.md index f1819ec6..b523749b 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,11 @@ -vfkit - Simple command line tool to start VMs through the macOS Virtualization framework +vfkit - Command-line tool to start VMs on macOS ==== ### Introduction vfkit offers a command-line interface to start virtual machines using the [macOS Virtualization framework](https://developer.apple.com/documentation/virtualization). It also provides a `github.com/crc-org/vfkit/pkg/config` go package. -This package provides a native Go API to generate the vfkit command line. - - -### Installation - -vfkit is available in brew: - -``` -brew install vfkit -``` - -### Building - -From the root direction of this repository, run `make`. +This package implements a native Go API to generate the vfkit command line. ### Usage @@ -31,26 +18,21 @@ See https://github.com/crc-org/vfkit/blob/main/doc/usage.md - [Containers Plumbing 2023](https://crc.dev/blog/posts/2023-03-22-containers-plumbing/) - [FOSDEM 2023](https://fosdem.org/2023/schedule/event/govfkit/) +### Adopters -### Background - -The work in this repository makes use of https://github.com/Code-Hex/vz which provides go bindings for macOS virtualization framework. -The lifetime of virtual machines created using the virtualization framework is tied to the filetime of the process where they were created. -When using `Code-Hex/vz`, this means the virtual machine will be terminated at the end of the go process using these bindings. -Spawning a `vfkit` process gives more flexibility and more control over the lifetime of the virtual machine. +- [minikube](https://minikube.sigs.k8s.io/) 1.35.0 and newer - minikube quickly sets up a local Kubernetes cluster +- [podman](https://podman.io/) 5.0 and newer - podman is a free software CLI tool to manage containers, pods and images +- [crc](https://crc.dev/) - crc sets up local OpenShift or MicroShift clusters for development and testing purposes +- [ovm](https://github.com/oomol-lab/ovm) - ovm is used by Oomol Studio to run linux containers on macOS +### Installation -The kernel must be uncompressed before use as no bootloader is used, as -documented in https://www.kernel.org/doc/Documentation/arm64/booting.txt +vfkit is available in brew: ``` -3. Decompress the kernel image ------------------------------- +brew install vfkit +``` -Requirement: OPTIONAL +### Building -The AArch64 kernel does not currently provide a decompressor and therefore -requires decompression (gzip etc.) to be performed by the boot loader if a -compressed Image target (e.g. Image.gz) is used. For bootloaders that do not -implement this requirement, the uncompressed Image target is available instead. -``` +From the root direction of this repository, run `make`. diff --git a/cmd/vfkit/main.go b/cmd/vfkit/main.go index 6ec95d0f..773b02b5 100644 --- a/cmd/vfkit/main.go +++ b/cmd/vfkit/main.go @@ -136,8 +136,6 @@ func runVFKit(vmConfig *config.VirtualMachine, opts *cmdline.Options) error { runtime.LockOSThread() defer runtime.UnlockOSThread() - util.SetupExitSignalHandling() - gpuDevs := vmConfig.VirtioGPUDevices() if opts.UseGUI && len(gpuDevs) > 0 { gpuDevs[0].UsesGUI = true @@ -157,6 +155,26 @@ func runVFKit(vmConfig *config.VirtualMachine, opts *cmdline.Options) error { } srv.Start() } + + shutdownFunc := func() { + log.Debugf("shutting down...") + stopped, err := vfVM.RequestStop() + if err != nil { + log.Errorf("failed to shutdown VM: %v", err) + } else if !stopped { + log.Warnf("VM did not acknowledge stop request") + } + if err := waitForVMState(vfVM, vz.VirtualMachineStateStopped, time.After(5*time.Second)); err != nil { + log.Warnf("failed to wait for VM stop: %v, forcing stop", err) + if forceErr := vfVM.Stop(); forceErr != nil { + log.Errorf("failed to force stop VM: %v", forceErr) + } + } else { + log.Debugf("VM stopped gracefully") + } + + } + util.SetupExitSignalHandling(shutdownFunc) return runVirtualMachine(vmConfig, vfVM) } @@ -289,8 +307,9 @@ func generateCloudInitImage(files []string) (string, error) { } configFiles := map[string]io.Reader{ - "user-data": nil, - "meta-data": nil, + "user-data": nil, + "meta-data": nil, + "network-config": nil, } hasConfigFile := false @@ -306,7 +325,9 @@ func generateCloudInitImage(files []string) (string, error) { filename := filepath.Base(path) if _, ok := configFiles[filename]; ok { - hasConfigFile = true + if filename == "user-data" || filename == "meta-data" { + hasConfigFile = true + } configFiles[filename] = file } } diff --git a/cmd/vfkit/main_test.go b/cmd/vfkit/main_test.go index fb9e9d17..4b4ee3b1 100644 --- a/cmd/vfkit/main_test.go +++ b/cmd/vfkit/main_test.go @@ -70,6 +70,7 @@ func TestGenerateCloudInitImage(t *testing.T) { iso, err := generateCloudInitImage([]string{ filepath.Join(assetsDir, "user-data"), filepath.Join(assetsDir, "meta-data"), + filepath.Join(assetsDir, "network-config"), }) require.NoError(t, err) diff --git a/contrib/scripts/start-gvproxy.sh b/contrib/scripts/start-gvproxy.sh new file mode 100755 index 00000000..70706c31 --- /dev/null +++ b/contrib/scripts/start-gvproxy.sh @@ -0,0 +1,38 @@ +#!/bin/sh + +# SPDX-FileCopyrightText: The vfkit authors +# SPDX-License-Identifier: Apache-2.0 +# +# This script can be used to start a raw disk image with vfkit using gvproxy +# for usermode networking. +# The mac address must be 5a:94:ef:e4:0c:ee as this is the address expected by gvproxy. +# +# After the VM is running, its ssh port is reachable on port 2223 on localhost (127.0.0.1). +# +# If the path to `--listen-vfkit` is too long (more than ~100 characters), then +# gvproxy/vfkit will fail to start as a unix socket filename must be less than +# that. +# +# This script creates an overlay for the disk image, the disk image is not modified. +# +set -exuo pipefail + +: "${GVPROXY:=gvproxy}" +: "${VFKIT:=./out/vfkit}" + +DISK_IMAGE="${1?Usage: $0 diskimage}" +VM_NAME="$(basename ${DISK_IMAGE})" + +${GVPROXY} -mtu 1500 -ssh-port 2223 -listen-vfkit unixgram://$(pwd)/${VM_NAME}.sock -log-file ${VM_NAME}.gvproxy.log --pid-file ${VM_NAME}.gvproxy.pid & + +TO_REMOVE="${VM_NAME}.sock ${VM_NAME}.gvproxy.pid ${VM_NAME}.overlay.img ${VM_NAME}.efistore.nvram" +trap 'if [[ -f "${VM_NAME}.gvproxy.pid" ]]; then kill $(cat ${VM_NAME}.gvproxy.pid); fi; rm -f ${TO_REMOVE}' EXIT + +cp -c ${DISK_IMAGE} "${VM_NAME}".overlay.img + +${VFKIT} --cpus 2 --memory 2048 \ + --bootloader efi,variable-store=${VM_NAME}.efistore.nvram,create \ + --device virtio-blk,path=${VM_NAME}.overlay.img \ + --device virtio-serial,logFilePath=${VM_NAME}.log \ + --device virtio-net,unixSocketPath=$(pwd)/${VM_NAME}.sock,mac=5a:94:ef:e4:0c:ee \ + --device virtio-rng diff --git a/contrib/scripts/start-with-cloud-init.sh b/contrib/scripts/start-with-cloud-init.sh index 2953e17e..5da9582f 100755 --- a/contrib/scripts/start-with-cloud-init.sh +++ b/contrib/scripts/start-with-cloud-init.sh @@ -12,13 +12,16 @@ # The $DISK_IMG variable needs to be set by the user to a # valid image path for the VM. # +# The script has optional support for network-config. The user +# can specify a network-config file using $NETWORK_CONFIG variable. +# # Once the VM is running, the user can connect to it using their # provided key. The VM IP can be found in `/var/db/dhcpd_leases` # by searching for the HOST_NAME or MAC address (72:20:43:d4:38:62). # # Example: # $ SSH_USER=test HOST_NAME=vm1 DISK_IMG=Fedora-Cloud-Base-AmazonEC2-41-1.4.aarch64.raw \ -# SSH_PUB_KEY=id_rsa.pub ./contrib/scripts/start-with-cloud-init.sh +# SSH_PUB_KEY=id_rsa.pub NETWORK_CONFIG=network-config ./contrib/scripts/start-with-cloud-init.sh # # $ ssh -i id_rsa test@192.168.64.14 @@ -26,6 +29,7 @@ set -exu HOST_NAME=${HOST_NAME:-"vfkit-vm"} SSH_USER=${SSH_USER:-"test"} +NETWORK_CONFIG=${NETWORK_CONFIG:-} if [ ! -f "$SSH_PUB_KEY" ]; then echo "Error: '$SSH_PUB_KEY' does not exist" @@ -37,6 +41,11 @@ if [ ! -f "$DISK_IMG" ]; then exit 1 fi +if [ -n "$NETWORK_CONFIG" ] && [ ! -f "$NETWORK_CONFIG" ]; then + echo "Error: '$NETWORK_CONFIG' does not exist" + exit 1 +fi + PUBLIC_KEY=$(cat "$SSH_PUB_KEY") mkdir -p out @@ -61,9 +70,14 @@ instance-id: $HOST_NAME local-hostname: $HOST_NAME EOF +CLOUD_INIT_ARGS="out/user-data,out/meta-data" +if [ -n "$NETWORK_CONFIG" ]; then + CLOUD_INIT_ARGS="$CLOUD_INIT_ARGS,$NETWORK_CONFIG" +fi + ./out/vfkit --cpus 2 --memory 2048 \ --bootloader efi,variable-store=out/efistore.nvram,create \ - --cloud-init out/user-data,out/meta-data \ + --cloud-init "$CLOUD_INIT_ARGS" \ --device virtio-blk,path="$DISK_IMG" \ --device virtio-serial,logFilePath=out/cloud-init.log \ --device virtio-net,nat,mac=72:20:43:d4:38:62 \ diff --git a/doc/usage.md b/doc/usage.md index d7ae888e..8ffbf77d 100644 --- a/doc/usage.md +++ b/doc/usage.md @@ -1,7 +1,7 @@ # vfkit Command Line The `vfkit` executable can be used to create a virtual machine (VM) using macOS virtualization framework. -The virtual machine will be terminated as soon as the `vfkit` process exits. +The virtual machine will be started when `vfkit` starts and will be terminated as soon as the `vfkit` process exits. Its configuration can be specified through command line options. Specifying VM bootloader configuration is mandatory. @@ -55,6 +55,21 @@ A bootloader is required to tell vfkit _how_ it should start the guest OS. `--bootloader linux` replaces the legacy `--kernel`, `--kernel-cmdline` and `--initrd` options. It allows to specify which kernel and initrd should be used when starting the VM. +On Apple Silicon hardware (M1 CPUs and newer), when using `--bootloader linux`, the kernel must be uncompressed before use as documented in https://www.kernel.org/doc/Documentation/arm64/booting.txt. `vfkit` will exit with an error if it detects a compressed kernel when running on Apple silicon. There are no such requirements when using `--bootloader efi`. + +Excerpt from the kernel’s `booting.txt`: +``` +3. Decompress the kernel image +------------------------------ + +Requirement: OPTIONAL + +The AArch64 kernel does not currently provide a decompressor and therefore +requires decompression (gzip etc.) to be performed by the boot loader if a +compressed Image target (e.g. Image.gz) is used. For bootloaders that do not +implement this requirement, the uncompressed Image target is available instead. +``` + #### Arguments - `kernel`: path to the kernel to use to start the virtual machine. The kernel *must* be uncompressed or the VM will hang when trying to start. See [the kernel documentation](https://www.kernel.org/doc/Documentation/arm64/booting.txt) for more details. @@ -164,7 +179,7 @@ Vfkit can create this ISO image automatically, or you can provide a pre-made ISO ##### Automatic ISO Creation -Vfkit allows you to pass the file paths of your `user-data` and `meta-data` files directly as arguments. +Vfkit allows you to pass the file paths of your `user-data`, `meta-data`, and `network-config` files directly as arguments. It will then handle the creation of the ISO image and the virtio-blk device internally. Example @@ -273,6 +288,8 @@ This allows to connect to the export of the remote NBD server: The `--device virtio-net` option adds a network interface to the virtual machine. If it gets its IP address through DHCP, its IP can be found in `/var/db/dhcpd_leases` on the host. +vfkit only supports NAT networking on its own. However, it integrates with [gvisor-tap-vsock](https://github.com/containers/gvisor-tap-vsock) for a user-mode networking stack, and [vmnet-helper](https://github.com/nirs/vmnet-helper) for shared/bridged/host networking through vmnet. + #### Arguments - `mac`: optional argument to specify the MAC address of the VM. If it's omitted, a random MAC address will be used. - `fd`: file descriptor to attach to the guest network interface. The file descriptor must be a connected datagram socket. See [VZFileHandleNetworkDeviceAttachment](https://developer.apple.com/documentation/virtualization/vzfilehandlenetworkdeviceattachment?language=objc) for more details. @@ -294,6 +311,9 @@ This adds a virtio-net device to the VM, and redirects all the network traffic o ``` This is useful in combination with usermode networking stacks such as [gvisor-tap-vsock](https://github.com/containers/gvisor-tap-vsock). +See [this shell script](https://github.com/nirs/vmnet-helper/blob/main/examples/vfkit.sh) for an example of networking using `vmnet-helper`. +See [this shell script](https://github.com/crc-org/vfkit/blob/main/contrib/scripts/start-gvproxy.sh) for an example of networking using `gvproxy`. + ### Serial Port diff --git a/go.mod b/go.mod index ed9a0a3a..675b830b 100644 --- a/go.mod +++ b/go.mod @@ -5,11 +5,12 @@ go 1.23.0 toolchain go1.23.6 require ( - github.com/Code-Hex/vz/v3 v3.6.0 + github.com/Code-Hex/vz/v3 v3.7.0 github.com/cavaliergopher/grab/v3 v3.0.1 github.com/containers/common v0.62.3 - github.com/crc-org/crc/v2 v2.49.0 - github.com/gin-gonic/gin v1.10.0 + github.com/crc-org/crc/v2 v2.51.0 + github.com/gin-gonic/gin v1.10.1 + github.com/inetaf/tcpproxy v0.0.0-20250222171855-c4b9df066048 github.com/kdomanski/iso9660 v0.4.0 github.com/pkg/term v1.1.0 github.com/prashantgupta24/mac-sleep-notifier v1.0.1 @@ -18,10 +19,9 @@ require ( github.com/spf13/pflag v1.0.6 github.com/stretchr/testify v1.10.0 github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 - golang.org/x/crypto v0.38.0 - golang.org/x/mod v0.24.0 + golang.org/x/crypto v0.39.0 + golang.org/x/mod v0.25.0 golang.org/x/sys v0.33.0 - inet.af/tcpproxy v0.0.0-20231102063150-2862066fc2a9 ) require ( @@ -35,7 +35,7 @@ require ( github.com/cloudwego/iasm v0.2.0 // indirect github.com/crc-org/machine v0.0.0-20240926103419-a943b47fd48b // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/ebitengine/purego v0.8.2 // indirect + github.com/ebitengine/purego v0.8.3 // indirect github.com/fatih/color v1.18.0 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/gin-contrib/sse v0.1.0 // indirect @@ -61,16 +61,16 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/rivo/uniseg v0.4.7 // indirect - github.com/shirou/gopsutil/v4 v4.25.2 // indirect + github.com/shirou/gopsutil/v4 v4.25.4 // indirect github.com/tklauser/go-sysconf v0.3.14 // indirect github.com/tklauser/numcpus v0.9.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect golang.org/x/arch v0.8.0 // indirect - golang.org/x/net v0.37.0 // indirect + golang.org/x/net v0.40.0 // indirect golang.org/x/term v0.32.0 // indirect - golang.org/x/text v0.25.0 // indirect + golang.org/x/text v0.26.0 // indirect google.golang.org/protobuf v1.36.5 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 23bf9bf5..edd24ac0 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ github.com/Code-Hex/go-infinity-channel v1.0.0 h1:M8BWlfDOxq9or9yvF9+YkceoTkDI1pFAqvnP87Zh0Nw= github.com/Code-Hex/go-infinity-channel v1.0.0/go.mod h1:5yUVg/Fqao9dAjcpzoQ33WwfdMWmISOrQloDRn3bsvY= -github.com/Code-Hex/vz/v3 v3.6.0 h1:S79dokzXmaLgC2yR0l0drRTGO/iFL3xwiCNVF80lJ5k= -github.com/Code-Hex/vz/v3 v3.6.0/go.mod h1:1LsW0jqW0r0cQ+IeR4hHbjdqOtSidNCVMWhStMHGho8= +github.com/Code-Hex/vz/v3 v3.7.0 h1:VEkfq5TVKnv85M81gQVPzLH9JzHrUJN/QQMpDZ+odPA= +github.com/Code-Hex/vz/v3 v3.7.0/go.mod h1:1LsW0jqW0r0cQ+IeR4hHbjdqOtSidNCVMWhStMHGho8= github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4= github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= @@ -22,24 +22,24 @@ github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQ github.com/containers/common v0.62.3 h1:aOGryqXfW6aKBbHbqOveH7zB+ihavUN03X/2pUSvWFI= github.com/containers/common v0.62.3/go.mod h1:3R8kDox2prC9uj/a2hmXj/YjZz5sBEUNrcDiw51S0Lo= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/crc-org/crc/v2 v2.49.0 h1:V0N8TS5k1XMZF61Dw/P7yq8SFuCkHUAO2wwlfDuJI3I= -github.com/crc-org/crc/v2 v2.49.0/go.mod h1:uAWz+FS48HtNov/jLTUVQhBLacgZ+Gbv0IyRnQyjadM= +github.com/crc-org/crc/v2 v2.51.0 h1:Alk+nSg3bgkTUfFtuojsqd3MgaENGyqV/QO9zXx9ALg= +github.com/crc-org/crc/v2 v2.51.0/go.mod h1:aX+kLWe5nYlQlMa5PKPG4QXDiIj5eBlfgxwa+l/UXSg= github.com/crc-org/machine v0.0.0-20240926103419-a943b47fd48b h1:5577tKzQcPfd/i0dCekY32R9DUi677sNfhVLYKulBGM= github.com/crc-org/machine v0.0.0-20240926103419-a943b47fd48b/go.mod h1:trWeQimjfE3dJ8qWOxI4ePtYm13aecK42bf01s6h/Nc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I= -github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/ebitengine/purego v0.8.3 h1:K+0AjQp63JEZTEMZiwsI9g0+hAMNohwUOtY0RPGexmc= +github.com/ebitengine/purego v0.8.3/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= -github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= +github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= @@ -61,6 +61,8 @@ github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg= github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/inetaf/tcpproxy v0.0.0-20250222171855-c4b9df066048 h1:jaqViOFFlZtkAwqvwZN+id37fosQqR5l3Oki9Dk4hz8= +github.com/inetaf/tcpproxy v0.0.0-20250222171855-c4b9df066048/go.mod h1:Di7LXRyUcnvAcLicFhtM9/MlZl/TNgRSDHORM2c6CMI= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kdomanski/iso9660 v0.4.0 h1:BPKKdcINz3m0MdjIMwS0wx1nofsOjxOq8TOr45WGHFg= @@ -103,8 +105,8 @@ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/shirou/gopsutil/v4 v4.25.2 h1:NMscG3l2CqtWFS86kj3vP7soOczqrQYIEhO/pMvvQkk= -github.com/shirou/gopsutil/v4 v4.25.2/go.mod h1:34gBYJzyqCDT11b6bMHP0XCvWeU3J61XRT7a2EmCRTA= +github.com/shirou/gopsutil/v4 v4.25.4 h1:cdtFO363VEOOFrUCjZRh4XVJkb548lyF0q0uTeMqYPw= +github.com/shirou/gopsutil/v4 v4.25.4/go.mod h1:xbuxyoZj+UsgnZrENu3lQivsngRR5BdjbJwf2fv4szA= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= @@ -136,12 +138,12 @@ github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQ golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= -golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= -golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= -golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= -golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= -golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= -golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= +golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= +golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= +golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= +golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -152,8 +154,8 @@ golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= -golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= -golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= @@ -163,7 +165,5 @@ gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYs gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -inet.af/tcpproxy v0.0.0-20231102063150-2862066fc2a9 h1:zomTWJvjwLbKRgGameQtpK6DNFUbZ2oNJuWhgUkGp3M= -inet.af/tcpproxy v0.0.0-20231102063150-2862066fc2a9/go.mod h1:Tojt5kmHpDIR2jMojxzZK2w2ZR7OILODmUo2gaSwjrk= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 4b5763c1..d863e338 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -1,6 +1,9 @@ package config import ( + "os" + "os/exec" + "path/filepath" "testing" "github.com/stretchr/testify/assert" @@ -44,3 +47,26 @@ func TestNetworkBlockDevice_NoDevice(t *testing.T) { nbdItem := vm.NetworkBlockDevice("nbd2") require.Nil(t, nbdItem) } + +func TestVirtualMachine_ValidateBlockDevices(t *testing.T) { + vm := &VirtualMachine{} + + tmpDir := t.TempDir() + imagePath := filepath.Join(tmpDir, "disk.qcow2") + size := "1G" + + cmd := exec.Command("qemu-img", "create", "-f", "qcow2", imagePath, size) + err := cmd.Run() + + require.NoError(t, err) + defer os.Remove(imagePath) + + dev, err := VirtioBlkNew(imagePath) + require.NoError(t, err) + vm.Devices = append(vm.Devices, dev) + + err = dev.validate() + require.Error(t, err) + + require.ErrorContains(t, err, "vfkit does not support qcow2 image format") +} diff --git a/pkg/config/virtio.go b/pkg/config/virtio.go index 2f7e451f..0e7ab32a 100644 --- a/pkg/config/virtio.go +++ b/pkg/config/virtio.go @@ -1,6 +1,7 @@ package config import ( + "bytes" "fmt" "math" "net" @@ -25,6 +26,7 @@ const ( // Default VirtioGPU Resolution defaultVirtioGPUResolutionWidth = 800 defaultVirtioGPUResolutionHeight = 600 + qcow2Header = "QFI\xfb" ) // VirtioInput configures an input device, such as a keyboard or pointing device @@ -571,7 +573,10 @@ func (dev *VirtioBlk) FromOptions(options []option) error { } } - return dev.DiskStorageConfig.FromOptions(unhandledOpts) + if err := dev.DiskStorageConfig.FromOptions(unhandledOpts); err != nil { + return err + } + return dev.validate() } func (dev *VirtioBlk) ToCmdLine() ([]string, error) { @@ -588,6 +593,24 @@ func (dev *VirtioBlk) ToCmdLine() ([]string, error) { return cmdLine, nil } +func (dev *VirtioBlk) validate() error { + imgPath := dev.ImagePath + file, err := os.Open(imgPath) + if err != nil { + return fmt.Errorf("failed to open file %s: %v", imgPath, err) + } + defer file.Close() + header := make([]byte, 4) + _, err = file.Read(header) + if err != nil { + return fmt.Errorf("failed to read the header of file %s: %v", imgPath, err) + } + if bytes.Equal(header, []byte(qcow2Header)) { + return fmt.Errorf("vfkit does not support qcow2 image format") + } + return nil +} + // VirtioVsockNew creates a new virtio-vsock device for 2-way communication // between the host and the virtual machine. The communication will happen on // vsock port, and on the host it will use the unix socket at socketURL. diff --git a/pkg/config/virtio_test.go b/pkg/config/virtio_test.go index b6305581..caca6128 100644 --- a/pkg/config/virtio_test.go +++ b/pkg/config/virtio_test.go @@ -1,6 +1,9 @@ package config import ( + "fmt" + "os" + "path/filepath" "testing" "time" @@ -16,247 +19,12 @@ type virtioDevTest struct { errorMsg string } -var virtioDevTests = map[string]virtioDevTest{ - "NewVirtioBlk": { - newDev: func() (VirtioDevice, error) { return VirtioBlkNew("/foo/bar") }, - expectedDev: &VirtioBlk{ - DiskStorageConfig: DiskStorageConfig{ - StorageConfig: StorageConfig{ - DevName: "virtio-blk", - }, - ImagePath: "/foo/bar", - }, - DeviceIdentifier: "", - }, - expectedCmdLine: []string{"--device", "virtio-blk,path=/foo/bar"}, - }, - "NewVirtioBlkWithDevId": { - newDev: func() (VirtioDevice, error) { - dev, err := VirtioBlkNew("/foo/bar") - if err != nil { - return nil, err - } - dev.SetDeviceIdentifier("test") - return dev, nil - }, - expectedDev: &VirtioBlk{ - DiskStorageConfig: DiskStorageConfig{ - StorageConfig: StorageConfig{ - DevName: "virtio-blk", - }, - ImagePath: "/foo/bar", - }, - DeviceIdentifier: "test", - }, - expectedCmdLine: []string{"--device", "virtio-blk,path=/foo/bar,deviceId=test"}, - alternateCmdLine: []string{"--device", "virtio-blk,deviceId=test,path=/foo/bar"}, - }, - "NewNVMe": { - newDev: func() (VirtioDevice, error) { return NVMExpressControllerNew("/foo/bar") }, - expectedDev: &NVMExpressController{ - DiskStorageConfig: DiskStorageConfig{ - StorageConfig: StorageConfig{ - DevName: "nvme", - }, - ImagePath: "/foo/bar", - }, - }, - expectedCmdLine: []string{"--device", "nvme,path=/foo/bar"}, - }, - "NewVirtioFs": { - newDev: func() (VirtioDevice, error) { return VirtioFsNew("/foo/bar", "") }, - expectedDev: &VirtioFs{ - SharedDir: "/foo/bar", - }, - expectedCmdLine: []string{"--device", "virtio-fs,sharedDir=/foo/bar"}, - }, - "NewVirtioFsWithTag": { - newDev: func() (VirtioDevice, error) { return VirtioFsNew("/foo/bar", "myTag") }, - expectedDev: &VirtioFs{ - SharedDir: "/foo/bar", - DirectorySharingConfig: DirectorySharingConfig{ - MountTag: "myTag", - }, - }, - expectedCmdLine: []string{"--device", "virtio-fs,sharedDir=/foo/bar,mountTag=myTag"}, - alternateCmdLine: []string{"--device", "virtio-fs,mountTag=myTag,sharedDir=/foo/bar"}, - }, - "NewRosettaShare": { - newDev: func() (VirtioDevice, error) { return RosettaShareNew("myTag") }, - expectedDev: &RosettaShare{ - DirectorySharingConfig: DirectorySharingConfig{ - MountTag: "myTag", - }, - }, - expectedCmdLine: []string{"--device", "rosetta,mountTag=myTag"}, - }, - "NewVirtioVsock": { - newDev: func() (VirtioDevice, error) { return VirtioVsockNew(1234, "/foo/bar.unix", false) }, - expectedDev: &VirtioVsock{ - Port: 1234, - SocketURL: "/foo/bar.unix", - }, - expectedCmdLine: []string{"--device", "virtio-vsock,port=1234,socketURL=/foo/bar.unix,connect"}, - alternateCmdLine: []string{"--device", "virtio-vsock,socketURL=/foo/bar.unix,connect,port=1234"}, - }, - "NewVirtioVsockWithListen": { - newDev: func() (VirtioDevice, error) { return VirtioVsockNew(1234, "/foo/bar.unix", true) }, - expectedDev: &VirtioVsock{ - Port: 1234, - SocketURL: "/foo/bar.unix", - Listen: true, - }, - expectedCmdLine: []string{"--device", "virtio-vsock,port=1234,socketURL=/foo/bar.unix,listen"}, - alternateCmdLine: []string{"--device", "virtio-vsock,socketURL=/foo/bar.unix,listen,port=1234"}, - }, - "NewVirtioRng": { - newDev: VirtioRngNew, - expectedDev: &VirtioRng{}, - expectedCmdLine: []string{"--device", "virtio-rng"}, - }, - "NewVirtioSerial": { - newDev: func() (VirtioDevice, error) { return VirtioSerialNew("/foo/bar.log") }, - expectedDev: &VirtioSerial{ - LogFile: "/foo/bar.log", - }, - expectedCmdLine: []string{"--device", "virtio-serial,logFilePath=/foo/bar.log"}, - }, - "NewVirtioSerialStdio": { - newDev: VirtioSerialNewStdio, - expectedDev: &VirtioSerial{ - UsesStdio: true, - }, - expectedCmdLine: []string{"--device", "virtio-serial,stdio"}, - }, - "NewVirtioSerialPty": { - newDev: VirtioSerialNewPty, - expectedDev: &VirtioSerial{ - UsesPty: true, - }, - expectedCmdLine: []string{"--device", "virtio-serial,pty"}, - }, - "NewVirtioNet": { - newDev: func() (VirtioDevice, error) { return VirtioNetNew("") }, - expectedDev: &VirtioNet{ - Nat: true, - }, - expectedCmdLine: []string{"--device", "virtio-net,nat"}, - }, - "NewVirtioNetWithPath": { - newDev: func() (VirtioDevice, error) { - dev, err := VirtioNetNew("") - if err != nil { - return nil, err - } - dev.SetUnixSocketPath("/tmp/unix.sock") - return dev, nil - }, - expectedDev: &VirtioNet{ - Nat: false, - UnixSocketPath: "/tmp/unix.sock", - }, - expectedCmdLine: []string{"--device", "virtio-net,unixSocketPath=/tmp/unix.sock"}, - }, - "NewVirtioNetWithMacAddress": { - newDev: func() (VirtioDevice, error) { return VirtioNetNew("00:11:22:33:44:55") }, - expectedDev: &VirtioNet{ - Nat: true, - MacAddress: []byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55}, - }, - expectedCmdLine: []string{"--device", "virtio-net,nat,mac=00:11:22:33:44:55"}, - alternateCmdLine: []string{"--device", "virtio-net,mac=00:11:22:33:44:55,nat"}, - }, - "NewUSBMassStorage": { - newDev: func() (VirtioDevice, error) { return USBMassStorageNew("/foo/bar") }, - expectedDev: &USBMassStorage{ - DiskStorageConfig: DiskStorageConfig{ - StorageConfig: StorageConfig{ - DevName: "usb-mass-storage", - }, - ImagePath: "/foo/bar", - }, - }, - expectedCmdLine: []string{"--device", "usb-mass-storage,path=/foo/bar"}, - }, - "NewUSBMassStorageReadOnly": { - newDev: func() (VirtioDevice, error) { - dev, err := USBMassStorageNew("/foo/bar") - if err != nil { - return nil, err - } - dev.SetReadOnly(true) - return dev, err - }, - expectedDev: &USBMassStorage{ - DiskStorageConfig: DiskStorageConfig{ - StorageConfig: StorageConfig{ - DevName: "usb-mass-storage", - ReadOnly: true, - }, - ImagePath: "/foo/bar", - }, - }, - expectedCmdLine: []string{"--device", "usb-mass-storage,path=/foo/bar,readonly"}, - }, - "NewVirtioInputWithPointingDevice": { - newDev: func() (VirtioDevice, error) { return VirtioInputNew("pointing") }, - expectedDev: &VirtioInput{ - InputType: "pointing", - }, - expectedCmdLine: []string{"--device", "virtio-input,pointing"}, - }, - "NewVirtioInputWithKeyboardDevice": { - newDev: func() (VirtioDevice, error) { return VirtioInputNew("keyboard") }, - expectedDev: &VirtioInput{ - InputType: "keyboard", - }, - expectedCmdLine: []string{"--device", "virtio-input,keyboard"}, - }, - "NewVirtioGPUDevice": { - newDev: VirtioGPUNew, - expectedDev: &VirtioGPU{ - false, - VirtioGPUResolution{Width: 800, Height: 600}, - }, - expectedCmdLine: []string{"--device", "virtio-gpu,width=800,height=600"}, - }, - "NewVirtioGPUDeviceWithDimensions": { - newDev: func() (VirtioDevice, error) { - dev, err := VirtioGPUNew() - if err != nil { - return nil, err - } - dev.(*VirtioGPU).VirtioGPUResolution = VirtioGPUResolution{Width: 1920, Height: 1080} - return dev, nil - }, - expectedDev: &VirtioGPU{ - false, - VirtioGPUResolution{Width: 1920, Height: 1080}, - }, - expectedCmdLine: []string{"--device", "virtio-gpu,width=1920,height=1080"}, - }, - "NetworkBlockDeviceNew": { - newDev: func() (VirtioDevice, error) { - return NetworkBlockDeviceNew("nbd://1.1.1.1:10000", 1000, SynchronizationNoneMode) - }, - expectedDev: &NetworkBlockDevice{ - NetworkBlockStorageConfig: NetworkBlockStorageConfig{ - StorageConfig: StorageConfig{ - DevName: "nbd", - }, - URI: "nbd://1.1.1.1:10000", - }, - DeviceIdentifier: "", - Timeout: time.Duration(1000 * time.Millisecond), - SynchronizationMode: SynchronizationNoneMode, - }, - expectedCmdLine: []string{"--device", "nbd,uri=nbd://1.1.1.1:10000,timeout=1000,sync=none"}, - }, - "NewVirtioBalloon": { - newDev: VirtioBalloonNew, - expectedDev: &VirtioBalloon{}, - expectedCmdLine: []string{"--device", "virtio-balloon"}, - }, +func getTestVirtioBlkDevice(testImagePath string) (*VirtioBlk, error) { + err := os.WriteFile(testImagePath, []byte{'0', '0', '0', '0'}, 0600) + if err != nil { + return nil, fmt.Errorf("failed to write test image: %v", err) + } + return VirtioBlkNew(testImagePath) } func testVirtioDev(t *testing.T, test *virtioDevTest) { @@ -296,6 +64,251 @@ func testErrorVirtioDev(t *testing.T, test *virtioDevTest) { } func TestVirtioDevices(t *testing.T) { + testImagePath := filepath.Join(t.TempDir(), "test.img") + var virtioDevTests = map[string]virtioDevTest{ + "NewVirtioBlk": { + newDev: func() (VirtioDevice, error) { + return getTestVirtioBlkDevice(testImagePath) + }, + expectedDev: &VirtioBlk{ + DiskStorageConfig: DiskStorageConfig{ + StorageConfig: StorageConfig{ + DevName: "virtio-blk", + }, + ImagePath: testImagePath, + }, + DeviceIdentifier: "", + }, + expectedCmdLine: []string{"--device", fmt.Sprintf("virtio-blk,path=%s", testImagePath)}, + }, + "NewVirtioBlkWithDevId": { + newDev: func() (VirtioDevice, error) { + dev, err := getTestVirtioBlkDevice(testImagePath) + if err != nil { + return nil, err + } + dev.SetDeviceIdentifier("test") + return dev, nil + }, + expectedDev: &VirtioBlk{ + DiskStorageConfig: DiskStorageConfig{ + StorageConfig: StorageConfig{ + DevName: "virtio-blk", + }, + ImagePath: testImagePath, + }, + DeviceIdentifier: "test", + }, + expectedCmdLine: []string{"--device", fmt.Sprintf("virtio-blk,path=%s,deviceId=test", testImagePath)}, + alternateCmdLine: []string{"--device", fmt.Sprintf("virtio-blk,deviceId=test,path=%s", testImagePath)}, + }, + "NewNVMe": { + newDev: func() (VirtioDevice, error) { return NVMExpressControllerNew("/foo/bar") }, + expectedDev: &NVMExpressController{ + DiskStorageConfig: DiskStorageConfig{ + StorageConfig: StorageConfig{ + DevName: "nvme", + }, + ImagePath: "/foo/bar", + }, + }, + expectedCmdLine: []string{"--device", "nvme,path=/foo/bar"}, + }, + "NewVirtioFs": { + newDev: func() (VirtioDevice, error) { return VirtioFsNew("/foo/bar", "") }, + expectedDev: &VirtioFs{ + SharedDir: "/foo/bar", + }, + expectedCmdLine: []string{"--device", "virtio-fs,sharedDir=/foo/bar"}, + }, + "NewVirtioFsWithTag": { + newDev: func() (VirtioDevice, error) { return VirtioFsNew("/foo/bar", "myTag") }, + expectedDev: &VirtioFs{ + SharedDir: "/foo/bar", + DirectorySharingConfig: DirectorySharingConfig{ + MountTag: "myTag", + }, + }, + expectedCmdLine: []string{"--device", "virtio-fs,sharedDir=/foo/bar,mountTag=myTag"}, + alternateCmdLine: []string{"--device", "virtio-fs,mountTag=myTag,sharedDir=/foo/bar"}, + }, + "NewRosettaShare": { + newDev: func() (VirtioDevice, error) { return RosettaShareNew("myTag") }, + expectedDev: &RosettaShare{ + DirectorySharingConfig: DirectorySharingConfig{ + MountTag: "myTag", + }, + }, + expectedCmdLine: []string{"--device", "rosetta,mountTag=myTag"}, + }, + "NewVirtioVsock": { + newDev: func() (VirtioDevice, error) { return VirtioVsockNew(1234, "/foo/bar.unix", false) }, + expectedDev: &VirtioVsock{ + Port: 1234, + SocketURL: "/foo/bar.unix", + }, + expectedCmdLine: []string{"--device", "virtio-vsock,port=1234,socketURL=/foo/bar.unix,connect"}, + alternateCmdLine: []string{"--device", "virtio-vsock,socketURL=/foo/bar.unix,connect,port=1234"}, + }, + "NewVirtioVsockWithListen": { + newDev: func() (VirtioDevice, error) { return VirtioVsockNew(1234, "/foo/bar.unix", true) }, + expectedDev: &VirtioVsock{ + Port: 1234, + SocketURL: "/foo/bar.unix", + Listen: true, + }, + expectedCmdLine: []string{"--device", "virtio-vsock,port=1234,socketURL=/foo/bar.unix,listen"}, + alternateCmdLine: []string{"--device", "virtio-vsock,socketURL=/foo/bar.unix,listen,port=1234"}, + }, + "NewVirtioRng": { + newDev: VirtioRngNew, + expectedDev: &VirtioRng{}, + expectedCmdLine: []string{"--device", "virtio-rng"}, + }, + "NewVirtioSerial": { + newDev: func() (VirtioDevice, error) { return VirtioSerialNew("/foo/bar.log") }, + expectedDev: &VirtioSerial{ + LogFile: "/foo/bar.log", + }, + expectedCmdLine: []string{"--device", "virtio-serial,logFilePath=/foo/bar.log"}, + }, + "NewVirtioSerialStdio": { + newDev: VirtioSerialNewStdio, + expectedDev: &VirtioSerial{ + UsesStdio: true, + }, + expectedCmdLine: []string{"--device", "virtio-serial,stdio"}, + }, + "NewVirtioSerialPty": { + newDev: VirtioSerialNewPty, + expectedDev: &VirtioSerial{ + UsesPty: true, + }, + expectedCmdLine: []string{"--device", "virtio-serial,pty"}, + }, + "NewVirtioNet": { + newDev: func() (VirtioDevice, error) { return VirtioNetNew("") }, + expectedDev: &VirtioNet{ + Nat: true, + }, + expectedCmdLine: []string{"--device", "virtio-net,nat"}, + }, + "NewVirtioNetWithPath": { + newDev: func() (VirtioDevice, error) { + dev, err := VirtioNetNew("") + if err != nil { + return nil, err + } + dev.SetUnixSocketPath("/tmp/unix.sock") + return dev, nil + }, + expectedDev: &VirtioNet{ + Nat: false, + UnixSocketPath: "/tmp/unix.sock", + }, + expectedCmdLine: []string{"--device", "virtio-net,unixSocketPath=/tmp/unix.sock"}, + }, + "NewVirtioNetWithMacAddress": { + newDev: func() (VirtioDevice, error) { return VirtioNetNew("00:11:22:33:44:55") }, + expectedDev: &VirtioNet{ + Nat: true, + MacAddress: []byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55}, + }, + expectedCmdLine: []string{"--device", "virtio-net,nat,mac=00:11:22:33:44:55"}, + alternateCmdLine: []string{"--device", "virtio-net,mac=00:11:22:33:44:55,nat"}, + }, + "NewUSBMassStorage": { + newDev: func() (VirtioDevice, error) { return USBMassStorageNew("/foo/bar") }, + expectedDev: &USBMassStorage{ + DiskStorageConfig: DiskStorageConfig{ + StorageConfig: StorageConfig{ + DevName: "usb-mass-storage", + }, + ImagePath: "/foo/bar", + }, + }, + expectedCmdLine: []string{"--device", "usb-mass-storage,path=/foo/bar"}, + }, + "NewUSBMassStorageReadOnly": { + newDev: func() (VirtioDevice, error) { + dev, err := USBMassStorageNew("/foo/bar") + if err != nil { + return nil, err + } + dev.SetReadOnly(true) + return dev, err + }, + expectedDev: &USBMassStorage{ + DiskStorageConfig: DiskStorageConfig{ + StorageConfig: StorageConfig{ + DevName: "usb-mass-storage", + ReadOnly: true, + }, + ImagePath: "/foo/bar", + }, + }, + expectedCmdLine: []string{"--device", "usb-mass-storage,path=/foo/bar,readonly"}, + }, + "NewVirtioInputWithPointingDevice": { + newDev: func() (VirtioDevice, error) { return VirtioInputNew("pointing") }, + expectedDev: &VirtioInput{ + InputType: "pointing", + }, + expectedCmdLine: []string{"--device", "virtio-input,pointing"}, + }, + "NewVirtioInputWithKeyboardDevice": { + newDev: func() (VirtioDevice, error) { return VirtioInputNew("keyboard") }, + expectedDev: &VirtioInput{ + InputType: "keyboard", + }, + expectedCmdLine: []string{"--device", "virtio-input,keyboard"}, + }, + "NewVirtioGPUDevice": { + newDev: VirtioGPUNew, + expectedDev: &VirtioGPU{ + false, + VirtioGPUResolution{Width: 800, Height: 600}, + }, + expectedCmdLine: []string{"--device", "virtio-gpu,width=800,height=600"}, + }, + "NewVirtioGPUDeviceWithDimensions": { + newDev: func() (VirtioDevice, error) { + dev, err := VirtioGPUNew() + if err != nil { + return nil, err + } + dev.(*VirtioGPU).VirtioGPUResolution = VirtioGPUResolution{Width: 1920, Height: 1080} + return dev, nil + }, + expectedDev: &VirtioGPU{ + false, + VirtioGPUResolution{Width: 1920, Height: 1080}, + }, + expectedCmdLine: []string{"--device", "virtio-gpu,width=1920,height=1080"}, + }, + "NetworkBlockDeviceNew": { + newDev: func() (VirtioDevice, error) { + return NetworkBlockDeviceNew("nbd://1.1.1.1:10000", 1000, SynchronizationNoneMode) + }, + expectedDev: &NetworkBlockDevice{ + NetworkBlockStorageConfig: NetworkBlockStorageConfig{ + StorageConfig: StorageConfig{ + DevName: "nbd", + }, + URI: "nbd://1.1.1.1:10000", + }, + DeviceIdentifier: "", + Timeout: time.Duration(1000 * time.Millisecond), + SynchronizationMode: SynchronizationNoneMode, + }, + expectedCmdLine: []string{"--device", "nbd,uri=nbd://1.1.1.1:10000,timeout=1000,sync=none"}, + }, + "NewVirtioBalloon": { + newDev: VirtioBalloonNew, + expectedDev: &VirtioBalloon{}, + expectedCmdLine: []string{"--device", "virtio-balloon"}, + }, + } t.Run("virtio-devices", func(t *testing.T) { for name := range virtioDevTests { t.Run(name, func(t *testing.T) { @@ -307,6 +320,5 @@ func TestVirtioDevices(t *testing.T) { } }) } - }) } diff --git a/pkg/util/exithandler.go b/pkg/util/exithandler.go index dd4c30a6..f23dde78 100644 --- a/pkg/util/exithandler.go +++ b/pkg/util/exithandler.go @@ -28,22 +28,24 @@ func RegisterExitHandler(handler func()) { // SetupExitSignalHandling sets up a signal channel to listen for termination or interruption signals. // When one of these signals is received, all the registered exit handlers will be invoked, just // before terminating the program. -func SetupExitSignalHandling() { - setupExitSignalHandling(true) +func SetupExitSignalHandling(shutdownFunc func()) { + setupExitSignalHandling(shutdownFunc) } // setupExitSignalHandling sets up a signal channel to listen for termination or interruption signals. // When one of these signals is received, all the registered exit handlers will be invoked. // It is possible to prevent the program from exiting by setting the doExit param to false (used for testing) -func setupExitSignalHandling(doExit bool) { +func setupExitSignalHandling(shutdownFunc func()) { sigChan := make(chan os.Signal, 2) signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM, syscall.SIGINT) go func() { + defer func() { + signal.Stop(sigChan) + }() for sig := range sigChan { log.Printf("captured %v, calling exit handlers and exiting..", sig) - ExecuteExitHandlers() - if doExit { - os.Exit(1) + if shutdownFunc != nil { + shutdownFunc() } } }() diff --git a/pkg/util/exithandler_test.go b/pkg/util/exithandler_test.go index 97c92d58..6428f890 100644 --- a/pkg/util/exithandler_test.go +++ b/pkg/util/exithandler_test.go @@ -7,7 +7,12 @@ import ( ) func TestExitHandlerCalled(t *testing.T) { - setupExitSignalHandling(false) + shutdownCalled := false + shutDownFunc := func() { + shutdownCalled = true + ExecuteExitHandlers() + } + setupExitSignalHandling(shutDownFunc) ch := make(chan struct{}) RegisterExitHandler(func() { @@ -23,6 +28,9 @@ func TestExitHandlerCalled(t *testing.T) { select { case <-ch: // exit handler was called + if !shutdownCalled { + t.Errorf("exit handler was not called") + } case <-time.After(5 * time.Second): t.Errorf("Exit handler not called - timed out") } diff --git a/pkg/vf/vsock.go b/pkg/vf/vsock.go index 3d03934f..8ed9538e 100644 --- a/pkg/vf/vsock.go +++ b/pkg/vf/vsock.go @@ -8,7 +8,7 @@ import ( "net/url" "strconv" - "inet.af/tcpproxy" + "github.com/inetaf/tcpproxy" ) func ExposeVsock(vm *VirtualMachine, port uint32, vsockPath string, listen bool) (io.Closer, error) { diff --git a/test/assets/network-config b/test/assets/network-config new file mode 100644 index 00000000..c8daf8df --- /dev/null +++ b/test/assets/network-config @@ -0,0 +1,6 @@ +version: 2 +ethernets: + enp0s1: + dhcp4: false + addresses: + - 192.168.64.50/24 diff --git a/test/vm_helpers.go b/test/vm_helpers.go index 15eb3c45..f36a8891 100644 --- a/test/vm_helpers.go +++ b/test/vm_helpers.go @@ -26,6 +26,8 @@ func retryIPFromMAC(errCh chan error, macAddress string) (string, error) { ip string ) + timeout := time.After(10 * time.Second) + for { select { case err := <-errCh: @@ -36,7 +38,7 @@ func retryIPFromMAC(errCh chan error, macAddress string) (string, error) { log.Infof("found IP address %s for MAC %s", ip, macAddress) return ip, nil } - case <-time.After(10 * time.Second): + case <-timeout: return "", fmt.Errorf("timeout getting IP from MAC: %w", err) } } @@ -47,6 +49,9 @@ func retrySSHDial(errCh chan error, scheme string, address string, sshConfig *ss sshClient *ssh.Client err error ) + + timeout := time.After(10 * time.Second) + for { select { case err := <-errCh: @@ -59,7 +64,7 @@ func retrySSHDial(errCh chan error, scheme string, address string, sshConfig *ss return sshClient, nil } log.Debugf("ssh failed: %v", err) - case <-time.After(10 * time.Second): + case <-timeout: return nil, fmt.Errorf("timeout waiting for SSH: %w", err) } }