Raw block device passthrough support
This commit is contained in:
parent
0870f8113a
commit
003b562e48
7 changed files with 133 additions and 26 deletions
30
cmd/ls.go
30
cmd/ls.go
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
|
@ -35,7 +36,7 @@ var lsCmd = &cobra.Command{
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func getDevicePassthroughConfig(val string) vm.USBDevicePassthroughConfig {
|
func getDevicePassthroughConfig(val string) vm.PassthroughConfig {
|
||||||
valSplit := strings.Split(val, ":")
|
valSplit := strings.Split(val, ":")
|
||||||
if want, have := 2, len(valSplit); want != have {
|
if want, have := 2, len(valSplit); want != have {
|
||||||
slog.Error("Bad device passthrough syntax", "error", fmt.Errorf("wrong items split by ':' count: want %v, have %v", want, have))
|
slog.Error("Bad device passthrough syntax", "error", fmt.Errorf("wrong items split by ':' count: want %v, have %v", want, have))
|
||||||
|
|
@ -62,14 +63,33 @@ func getDevicePassthroughConfig(val string) vm.USBDevicePassthroughConfig {
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
return vm.USBDevicePassthroughConfig{
|
return vm.PassthroughConfig{
|
||||||
VendorID: uint16(vendorID),
|
USB: []vm.USBDevicePassthroughConfig{{
|
||||||
ProductID: uint16(productID),
|
VendorID: uint16(vendorID),
|
||||||
|
ProductID: uint16(productID),
|
||||||
|
}},
|
||||||
}
|
}
|
||||||
|
case "dev":
|
||||||
|
devPath := filepath.Clean(valSplit[1])
|
||||||
|
stat, err := os.Stat(devPath)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to stat the device path", "error", err.Error(), "path", devPath)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
isDev := stat.Mode()&os.ModeDevice != 0
|
||||||
|
if !isDev {
|
||||||
|
slog.Error("Provided path is not a path to a valid block device", "path", devPath, "file-mode", stat.Mode())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return vm.PassthroughConfig{Block: []vm.BlockDevicePassthroughConfig{{
|
||||||
|
Path: devPath,
|
||||||
|
}}}
|
||||||
default:
|
default:
|
||||||
slog.Error("Unknown device passthrough type", "value", valSplit[0])
|
slog.Error("Unknown device passthrough type", "value", valSplit[0])
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
// This unreachable code is required to compile.
|
// This unreachable code is required to compile.
|
||||||
return vm.USBDevicePassthroughConfig{}
|
return vm.PassthroughConfig{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,10 +51,10 @@ func doUSBRootCheck() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func runVM(passthroughArg string, fn func(context.Context, *vm.VM, *vm.FileManager) int, forwardPortsRules []vm.PortForwardingRule, unrestrictedNetworking bool) int {
|
func runVM(passthroughArg string, fn func(context.Context, *vm.VM, *vm.FileManager) int, forwardPortsRules []vm.PortForwardingRule, unrestrictedNetworking bool) int {
|
||||||
var passthroughConfig []vm.USBDevicePassthroughConfig
|
var passthroughConfig vm.PassthroughConfig
|
||||||
|
|
||||||
if passthroughArg != "" {
|
if passthroughArg != "" {
|
||||||
passthroughConfig = []vm.USBDevicePassthroughConfig{getDevicePassthroughConfig(passthroughArg)}
|
passthroughConfig = getDevicePassthroughConfig(passthroughArg)
|
||||||
doUSBRootCheck()
|
doUSBRootCheck()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -66,7 +66,7 @@ func runVM(passthroughArg string, fn func(context.Context, *vm.VM, *vm.FileManag
|
||||||
|
|
||||||
MemoryAlloc: vmMemAllocFlag,
|
MemoryAlloc: vmMemAllocFlag,
|
||||||
|
|
||||||
USBDevices: passthroughConfig,
|
PassthroughConfig: passthroughConfig,
|
||||||
ExtraPortForwardingRules: forwardPortsRules,
|
ExtraPortForwardingRules: forwardPortsRules,
|
||||||
|
|
||||||
OSUpTimeout: time.Duration(vmOSUpTimeoutFlag) * time.Second,
|
OSUpTimeout: time.Duration(vmOSUpTimeoutFlag) * time.Second,
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,11 @@ package vm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
func prepareVMCmd(cmd *exec.Cmd) {
|
func prepareVMCmd(cmd *exec.Cmd) {
|
||||||
|
|
@ -17,3 +21,26 @@ func prepareVMCmd(cmd *exec.Cmd) {
|
||||||
func terminateProcess(pid int) error {
|
func terminateProcess(pid int) error {
|
||||||
return syscall.Kill(-pid, syscall.SIGTERM)
|
return syscall.Kill(-pid, syscall.SIGTERM)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This is never used except for a band-aid that would check
|
||||||
|
// that there are no double-mounts.
|
||||||
|
func checkDeviceSeemsMounted(devPathPrefix string) (bool, error) {
|
||||||
|
absDevPathPrefix, err := filepath.Abs(devPathPrefix)
|
||||||
|
if err != nil {
|
||||||
|
return false, errors.Wrap(err, "get abs path")
|
||||||
|
}
|
||||||
|
|
||||||
|
mounts, err := exec.Command("mount").Output()
|
||||||
|
if err != nil {
|
||||||
|
return false, errors.Wrap(err, "run mount command")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, line := range strings.Split(string(mounts), "\n") {
|
||||||
|
// I know, I know, this is a rare band-aid.
|
||||||
|
if strings.HasPrefix(line, devPathPrefix) || strings.HasPrefix(line, absDevPathPrefix) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
|
||||||
15
vm/passthrough.go
Normal file
15
vm/passthrough.go
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
package vm
|
||||||
|
|
||||||
|
type USBDevicePassthroughConfig struct {
|
||||||
|
VendorID uint16
|
||||||
|
ProductID uint16
|
||||||
|
}
|
||||||
|
|
||||||
|
type BlockDevicePassthroughConfig struct {
|
||||||
|
Path string
|
||||||
|
}
|
||||||
|
|
||||||
|
type PassthroughConfig struct {
|
||||||
|
USB []USBDevicePassthroughConfig
|
||||||
|
Block []BlockDevicePassthroughConfig
|
||||||
|
}
|
||||||
|
|
@ -103,10 +103,10 @@ func (vm *VM) sshSetup() (ssh.Signer, error) {
|
||||||
|
|
||||||
installSSHDCmd := ""
|
installSSHDCmd := ""
|
||||||
if vm.installSSH {
|
if vm.installSSH {
|
||||||
installSSHDCmd = "apk add openssh; "
|
installSSHDCmd = "ifconfig eth0 up && ifconfig lo up && udhcpc; apk add openssh; "
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := `do_setup () { sh -c "set -ex; setup-alpine -q; ` + installSSHDCmd + `mkdir -p ~/.ssh; echo ` + shellescape.Quote(string(sshPublicKey)) + ` > ~/.ssh/authorized_keys; rc-update add sshd; rc-service sshd start"; echo "SERIAL"" ""STATUS: $?"; }; do_setup` + "\n"
|
cmd := `do_setup () { sh -c "set -ex; ` + installSSHDCmd + `mkdir -p ~/.ssh; echo ` + shellescape.Quote(string(sshPublicKey)) + ` > ~/.ssh/authorized_keys; rc-update add sshd; rc-service sshd start"; echo "SERIAL"" ""STATUS: $?"; }; do_setup` + "\n"
|
||||||
|
|
||||||
err = vm.writeSerial([]byte(cmd))
|
err = vm.writeSerial([]byte(cmd))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,6 @@ import (
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
type USBDevicePassthroughConfig struct {
|
|
||||||
VendorID uint16
|
|
||||||
ProductID uint16
|
|
||||||
}
|
|
||||||
|
|
||||||
type PortForwardingRule struct {
|
type PortForwardingRule struct {
|
||||||
HostIP net.IP
|
HostIP net.IP
|
||||||
HostPort uint16
|
HostPort uint16
|
||||||
|
|
|
||||||
72
vm/vm.go
72
vm/vm.go
|
|
@ -54,6 +54,8 @@ type VM struct {
|
||||||
// These are to be interacted with using `atomic` package
|
// These are to be interacted with using `atomic` package
|
||||||
disposed uint32
|
disposed uint32
|
||||||
canceled uint32
|
canceled uint32
|
||||||
|
|
||||||
|
originalCfg VMConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
type DriveConfig struct {
|
type DriveConfig struct {
|
||||||
|
|
@ -67,7 +69,7 @@ type VMConfig struct {
|
||||||
|
|
||||||
MemoryAlloc uint32 // In KiB.
|
MemoryAlloc uint32 // In KiB.
|
||||||
|
|
||||||
USBDevices []USBDevicePassthroughConfig
|
PassthroughConfig PassthroughConfig
|
||||||
ExtraPortForwardingRules []PortForwardingRule
|
ExtraPortForwardingRules []PortForwardingRule
|
||||||
|
|
||||||
// Timeouts
|
// Timeouts
|
||||||
|
|
@ -149,26 +151,43 @@ func NewVM(logger *slog.Logger, cfg VMConfig) (*VM, error) {
|
||||||
cmdArgs = append(cmdArgs, "-device", "virtio-gpu-device")
|
cmdArgs = append(cmdArgs, "-device", "virtio-gpu-device")
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(cfg.USBDevices) != 0 {
|
|
||||||
cmdArgs = append(cmdArgs, "-device", "nec-usb-xhci")
|
|
||||||
|
|
||||||
for _, dev := range cfg.USBDevices {
|
|
||||||
cmdArgs = append(cmdArgs, "-device", "usb-host,vendorid=0x"+hex.EncodeToString(utils.Uint16ToBytesBE(dev.VendorID))+",productid=0x"+hex.EncodeToString(utils.Uint16ToBytesBE(dev.ProductID)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, extraDrive := range cfg.Drives {
|
for i, extraDrive := range cfg.Drives {
|
||||||
_, err = os.Stat(extraDrive.Path)
|
_, err = os.Stat(extraDrive.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrapf(err, "stat extra drive #%v path", i)
|
return nil, errors.Wrapf(err, "stat extra drive #%v path", i)
|
||||||
}
|
}
|
||||||
|
|
||||||
driveArgs := "file=" + shellescape.Quote(extraDrive.Path) + ",format=qcow2,if=virtio"
|
driveArgs := "file=" + shellescape.Quote(extraDrive.Path) + ",format=qcow2,if=none,id=disk" + fmt.Sprint(i)
|
||||||
if extraDrive.SnapshotMode {
|
if extraDrive.SnapshotMode {
|
||||||
driveArgs += ",snapshot=on"
|
driveArgs += ",snapshot=on"
|
||||||
}
|
}
|
||||||
|
|
||||||
cmdArgs = append(cmdArgs, "-drive", driveArgs)
|
cmdArgs = append(cmdArgs, "-drive", driveArgs, "-device", "virtio-blk-pci,drive=disk"+fmt.Sprint(i)+",bootindex="+fmt.Sprint(i))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cfg.PassthroughConfig.USB) != 0 {
|
||||||
|
cmdArgs = append(cmdArgs, "-device", "nec-usb-xhci")
|
||||||
|
|
||||||
|
for _, dev := range cfg.PassthroughConfig.USB {
|
||||||
|
cmdArgs = append(cmdArgs, "-device", "usb-host,vendorid=0x"+hex.EncodeToString(utils.Uint16ToBytesBE(dev.VendorID))+",productid=0x"+hex.EncodeToString(utils.Uint16ToBytesBE(dev.ProductID)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, dev := range cfg.PassthroughConfig.Block {
|
||||||
|
// It's always a user's responsibility to ensure that no drives are mounted
|
||||||
|
// in both host and guest system. This should serve as the last resort.
|
||||||
|
{
|
||||||
|
seemsMounted, err := checkDeviceSeemsMounted(dev.Path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "check whether device seems to be mounted (path '%v')", dev.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
if seemsMounted {
|
||||||
|
return nil, fmt.Errorf("device '%v' is already mounted in the host system", dev.Path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cmdArgs = append(cmdArgs, "-drive", "file="+shellescape.Quote(dev.Path)+",format=raw,aio=native,cache=none")
|
||||||
}
|
}
|
||||||
|
|
||||||
// We're not using clean `cdromImagePath` here because it is set to "."
|
// We're not using clean `cdromImagePath` here because it is set to "."
|
||||||
|
|
@ -235,6 +254,8 @@ func NewVM(logger *slog.Logger, cfg VMConfig) (*VM, error) {
|
||||||
|
|
||||||
osUpTimeout: osUpTimeout,
|
osUpTimeout: osUpTimeout,
|
||||||
sshUpTimeout: sshUpTimeout,
|
sshUpTimeout: sshUpTimeout,
|
||||||
|
|
||||||
|
originalCfg: cfg,
|
||||||
}
|
}
|
||||||
|
|
||||||
vm.resetSerialStdout()
|
vm.resetSerialStdout()
|
||||||
|
|
@ -252,6 +273,8 @@ func (vm *VM) Run() error {
|
||||||
return errors.Wrap(err, "start qemu cmd")
|
return errors.Wrap(err, "start qemu cmd")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
go vm.runPeriodicHostMountChecker()
|
||||||
|
|
||||||
var globalErrsMu sync.Mutex
|
var globalErrsMu sync.Mutex
|
||||||
var globalErrs []error
|
var globalErrs []error
|
||||||
|
|
||||||
|
|
@ -499,3 +522,30 @@ func (vm *VM) DialSCP() (*scp.Client, error) {
|
||||||
func (vm *VM) SSHUpNotifyChan() chan struct{} {
|
func (vm *VM) SSHUpNotifyChan() chan struct{} {
|
||||||
return vm.sshReadyCh
|
return vm.sshReadyCh
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// It's always a user's responsibility to ensure that no drives are mounted
|
||||||
|
// in both host and guest system. This should serve as the last resort.
|
||||||
|
func (vm *VM) runPeriodicHostMountChecker() {
|
||||||
|
if len(vm.originalCfg.PassthroughConfig.Block) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-vm.ctx.Done():
|
||||||
|
return
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
for _, dev := range vm.originalCfg.PassthroughConfig.Block {
|
||||||
|
seemsMounted, err := checkDeviceSeemsMounted(dev.Path)
|
||||||
|
if err != nil {
|
||||||
|
vm.logger.Warn("Failed to check if a passed device seems to be mounted", "dev-path", dev.Path)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if seemsMounted {
|
||||||
|
panic(fmt.Sprintf("CRITICAL: Passed-through device '%v' appears to have been mounted on the host OS. Forcefully exiting now to prevent data corruption.", dev.Path))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue