Raw block device passthrough support
This commit is contained in:
parent
0870f8113a
commit
003b562e48
7 changed files with 133 additions and 26 deletions
|
|
@ -4,7 +4,11 @@ package vm
|
|||
|
||||
import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func prepareVMCmd(cmd *exec.Cmd) {
|
||||
|
|
@ -17,3 +21,26 @@ func prepareVMCmd(cmd *exec.Cmd) {
|
|||
func terminateProcess(pid int) error {
|
||||
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 := ""
|
||||
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))
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -9,11 +9,6 @@ import (
|
|||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type USBDevicePassthroughConfig struct {
|
||||
VendorID uint16
|
||||
ProductID uint16
|
||||
}
|
||||
|
||||
type PortForwardingRule struct {
|
||||
HostIP net.IP
|
||||
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
|
||||
disposed uint32
|
||||
canceled uint32
|
||||
|
||||
originalCfg VMConfig
|
||||
}
|
||||
|
||||
type DriveConfig struct {
|
||||
|
|
@ -67,7 +69,7 @@ type VMConfig struct {
|
|||
|
||||
MemoryAlloc uint32 // In KiB.
|
||||
|
||||
USBDevices []USBDevicePassthroughConfig
|
||||
PassthroughConfig PassthroughConfig
|
||||
ExtraPortForwardingRules []PortForwardingRule
|
||||
|
||||
// Timeouts
|
||||
|
|
@ -149,26 +151,43 @@ func NewVM(logger *slog.Logger, cfg VMConfig) (*VM, error) {
|
|||
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 {
|
||||
_, err = os.Stat(extraDrive.Path)
|
||||
if err != nil {
|
||||
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 {
|
||||
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 "."
|
||||
|
|
@ -235,6 +254,8 @@ func NewVM(logger *slog.Logger, cfg VMConfig) (*VM, error) {
|
|||
|
||||
osUpTimeout: osUpTimeout,
|
||||
sshUpTimeout: sshUpTimeout,
|
||||
|
||||
originalCfg: cfg,
|
||||
}
|
||||
|
||||
vm.resetSerialStdout()
|
||||
|
|
@ -252,6 +273,8 @@ func (vm *VM) Run() error {
|
|||
return errors.Wrap(err, "start qemu cmd")
|
||||
}
|
||||
|
||||
go vm.runPeriodicHostMountChecker()
|
||||
|
||||
var globalErrsMu sync.Mutex
|
||||
var globalErrs []error
|
||||
|
||||
|
|
@ -499,3 +522,30 @@ func (vm *VM) DialSCP() (*scp.Client, error) {
|
|||
func (vm *VM) SSHUpNotifyChan() chan struct{} {
|
||||
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