2023-08-25 15:12:19 +01:00
package cmd
import (
2023-08-25 16:54:58 +01:00
"context"
2023-08-26 16:26:35 +01:00
"fmt"
2023-08-25 15:12:19 +01:00
"os"
2023-08-25 19:55:11 +01:00
"os/signal"
2023-08-30 09:36:33 +01:00
"path/filepath"
"strconv"
"strings"
2023-08-25 16:54:58 +01:00
"sync"
2023-08-25 19:55:11 +01:00
"syscall"
2023-08-29 11:51:06 +01:00
"time"
2023-08-25 15:12:19 +01:00
2023-08-25 16:54:58 +01:00
"log/slog"
2023-08-31 16:23:40 +01:00
"github.com/AlexSSD7/linsk/nettap"
2023-09-01 11:44:49 +01:00
"github.com/AlexSSD7/linsk/osspecifics"
2023-08-31 16:23:40 +01:00
"github.com/AlexSSD7/linsk/share"
2023-08-30 09:19:02 +01:00
"github.com/AlexSSD7/linsk/storage"
2023-08-26 09:16:52 +01:00
"github.com/AlexSSD7/linsk/vm"
2023-08-25 15:12:19 +01:00
"github.com/pkg/errors"
)
2023-08-31 20:10:26 +01:00
func createStoreOrExit ( ) * storage . Storage {
2023-08-30 09:19:02 +01:00
store , err := storage . NewStorage ( slog . With ( "caller" , "storage" ) , dataDirFlag )
if err != nil {
slog . Error ( "Failed to create Linsk data storage" , "error" , err . Error ( ) , "data-dir" , dataDirFlag )
os . Exit ( 1 )
}
2023-08-30 09:43:41 +01:00
return store
}
2023-09-01 18:15:32 +01:00
type runVMFunc func ( context . Context , * vm . VM , * vm . FileManager , * share . NetTapRuntimeContext ) int
func runVM ( passthroughArg string , fn runVMFunc , forwardPortsRules [ ] vm . PortForwardingRule , unrestrictedNetworking bool , withNetTap bool ) int {
2023-08-31 20:10:26 +01:00
store := createStoreOrExit ( )
2023-08-30 12:39:38 +01:00
vmImagePath , err := store . CheckVMImageExists ( )
2023-08-30 09:19:02 +01:00
if err != nil {
2023-08-30 12:39:38 +01:00
slog . Error ( "Failed to check whether VM image exists" , "error" , err . Error ( ) )
2023-08-31 20:10:26 +01:00
return 1
2023-08-30 12:39:38 +01:00
}
if vmImagePath == "" {
slog . Error ( "VM image does not exist. You need to build it first before attempting to start Linsk. Please run `linsk build` first." )
2023-08-31 20:10:26 +01:00
return 1
2023-08-30 09:19:02 +01:00
}
2023-08-30 13:32:00 +01:00
biosPath , err := store . CheckDownloadVMBIOS ( )
if err != nil {
2023-08-31 16:23:40 +01:00
slog . Error ( "Failed to check/download VM BIOS" , "error" , err . Error ( ) )
2023-08-31 20:10:26 +01:00
return 1
2023-08-30 13:32:00 +01:00
}
2023-08-29 15:31:17 +01:00
var passthroughConfig vm . PassthroughConfig
2023-08-26 11:27:38 +01:00
if passthroughArg != "" {
2023-08-31 20:10:26 +01:00
passthroughConfigPtr , err := getDevicePassthroughConfig ( passthroughArg )
if err != nil {
slog . Error ( "Failed to get device passthrough config" , "error" , err . Error ( ) )
return 1
}
passthroughConfig = * passthroughConfigPtr
2023-09-01 11:44:49 +01:00
}
if len ( passthroughConfig . USB ) != 0 {
// Log USB-related warnings.
2023-08-31 20:10:26 +01:00
2023-09-01 16:29:01 +01:00
// Unfortunately USB passthrough is unstable in macOS and Windows. On Windows, you also need to install external
// libusbK driver, which nullifies the UX. This is a problem with how QEMU works, and unfortunately there isn't
// much we can do about it from our side.
2023-09-02 11:28:23 +01:00
switch {
case osspecifics . IsWindows ( ) :
2023-09-01 11:44:49 +01:00
// TODO: To document: installation of libusbK driver with Zadig utility.
slog . Warn ( "USB passthrough is unstable on Windows and requires installation of libusbK driver. Please consider using raw block device passthrough instead." )
2023-09-02 11:28:23 +01:00
case osspecifics . IsMacOS ( ) :
2023-09-01 11:44:49 +01:00
slog . Warn ( "USB passthrough is unstable on macOS. Please consider using raw block device passthrough instead." )
}
2023-08-26 11:27:38 +01:00
}
2023-08-25 16:54:58 +01:00
2023-08-31 16:23:40 +01:00
var tapRuntimeCtx * share . NetTapRuntimeContext
var tapsConfig [ ] vm . TapConfig
if withNetTap {
tapManager , err := nettap . NewTapManager ( slog . With ( "caller" , "nettap-manager" ) )
if err != nil {
slog . Error ( "Failed to create new network tap manager" , "error" , err . Error ( ) )
2023-08-31 20:10:26 +01:00
return 1
2023-08-31 16:23:40 +01:00
}
2023-09-02 10:07:17 +01:00
tapNameToUse , err := nettap . NewUniqueTapName ( )
2023-09-01 14:41:19 +01:00
if err != nil {
slog . Error ( "Failed to generate new network tap name" , "error" , err . Error ( ) )
return 1
}
2023-08-31 16:23:40 +01:00
knownAllocs , err := store . ListNetTapAllocations ( )
if err != nil {
slog . Error ( "Failed to list net tap allocations" , "error" , err . Error ( ) )
2023-08-31 20:10:26 +01:00
return 1
2023-08-31 16:23:40 +01:00
}
removedTaps , err := tapManager . PruneTaps ( knownAllocs )
if err != nil {
slog . Error ( "Failed to prune dangling network taps" , "error" , err . Error ( ) )
} else {
// This is optional, meaning that we won't exit in panic if this fails.
for _ , removedTap := range removedTaps {
err = store . ReleaseNetTapAllocation ( removedTap )
if err != nil {
slog . Error ( "Failed to release a danging net tap allocation" , "error" , err . Error ( ) )
}
}
}
err = store . SaveNetTapAllocation ( tapNameToUse , os . Getpid ( ) )
if err != nil {
slog . Error ( "Failed to save net tap allocation" , "error" , err . Error ( ) )
2023-08-31 20:10:26 +01:00
return 1
2023-08-31 16:23:40 +01:00
}
tapManager , err = nettap . NewTapManager ( slog . Default ( ) )
if err != nil {
slog . Error ( "Failed to create net tap manager" , "error" , err . Error ( ) )
2023-08-31 20:10:26 +01:00
return 1
2023-08-31 16:23:40 +01:00
}
err = tapManager . CreateNewTap ( tapNameToUse )
if err != nil {
releaseErr := store . ReleaseNetTapAllocation ( tapNameToUse )
if releaseErr != nil {
2023-08-31 20:10:26 +01:00
slog . Error ( "Failed to release net tap allocation" , "error" , releaseErr . Error ( ) , "tap-name" , tapNameToUse )
// Non-critical error.
2023-08-31 16:23:40 +01:00
}
slog . Error ( "Failed to create new tap" , "error" , err . Error ( ) )
2023-08-31 20:10:26 +01:00
return 1
2023-08-31 16:23:40 +01:00
}
2023-08-31 20:10:26 +01:00
defer func ( ) {
err := tapManager . DeleteTap ( tapNameToUse )
if err != nil {
slog . Error ( "Failed to clean up net tap" , "error" , err . Error ( ) , "tap-name" , tapNameToUse )
} else {
err = store . ReleaseNetTapAllocation ( tapNameToUse )
if err != nil {
slog . Error ( "Failed to release net tap allocation" , "error" , err . Error ( ) , "tap-name" , tapNameToUse )
}
}
} ( )
2023-08-31 16:23:40 +01:00
tapNet , err := nettap . GenerateNet ( )
if err != nil {
slog . Error ( "Failed to generate tap net plan" , "error" , err . Error ( ) )
2023-08-31 20:10:26 +01:00
return 1
2023-08-31 16:23:40 +01:00
}
err = tapManager . ConfigureNet ( tapNameToUse , tapNet . HostCIDR )
if err != nil {
slog . Error ( "Failed to configure tap net" , "error" , err . Error ( ) )
2023-08-31 20:10:26 +01:00
return 1
2023-08-31 16:23:40 +01:00
}
tapRuntimeCtx = & share . NetTapRuntimeContext {
Manager : tapManager ,
Name : tapNameToUse ,
Net : tapNet ,
}
tapsConfig = [ ] vm . TapConfig { {
Name : tapNameToUse ,
} }
}
2023-08-27 13:44:57 +01:00
vmCfg := vm . VMConfig {
2023-08-27 15:53:44 +01:00
Drives : [ ] vm . DriveConfig { {
2023-08-30 12:39:38 +01:00
Path : vmImagePath ,
2023-08-27 15:53:44 +01:00
SnapshotMode : true ,
} } ,
2023-08-27 13:44:57 +01:00
2023-08-29 10:59:50 +01:00
MemoryAlloc : vmMemAllocFlag ,
2023-08-30 13:32:00 +01:00
BIOSPath : biosPath ,
2023-08-29 10:59:50 +01:00
2023-08-29 15:31:17 +01:00
PassthroughConfig : passthroughConfig ,
2023-08-27 13:44:57 +01:00
ExtraPortForwardingRules : forwardPortsRules ,
2023-08-31 16:23:40 +01:00
UnrestrictedNetworking : unrestrictedNetworking ,
Taps : tapsConfig ,
2023-08-29 11:51:06 +01:00
OSUpTimeout : time . Duration ( vmOSUpTimeoutFlag ) * time . Second ,
SSHUpTimeout : time . Duration ( vmSSHSetupTimeoutFlag ) * time . Second ,
2023-08-31 16:23:40 +01:00
ShowDisplay : vmDebugFlag ,
2023-08-27 13:44:57 +01:00
}
2023-09-01 18:15:32 +01:00
return innerRunVM ( vmCfg , tapRuntimeCtx , fn )
}
func innerRunVM ( vmCfg vm . VMConfig , tapRuntimeCtx * share . NetTapRuntimeContext , fn runVMFunc ) int {
2023-08-27 13:44:57 +01:00
vi , err := vm . NewVM ( slog . Default ( ) . With ( "caller" , "vm" ) , vmCfg )
2023-08-25 16:54:58 +01:00
if err != nil {
2023-08-29 10:59:50 +01:00
slog . Error ( "Failed to create vm instance" , "error" , err . Error ( ) )
2023-08-31 20:10:26 +01:00
return 1
2023-08-25 15:12:19 +01:00
}
2023-08-25 16:54:58 +01:00
runErrCh := make ( chan error , 1 )
var wg sync . WaitGroup
ctx , ctxCancel := context . WithCancel ( context . Background ( ) )
2023-08-25 19:55:11 +01:00
defer ctxCancel ( )
interrupt := make ( chan os . Signal , 2 )
signal . Notify ( interrupt , syscall . SIGTERM , syscall . SIGINT )
2023-08-25 16:54:58 +01:00
wg . Add ( 1 )
go func ( ) {
defer wg . Done ( )
err := vi . Run ( )
ctxCancel ( )
runErrCh <- err
} ( )
2023-08-25 19:55:11 +01:00
go func ( ) {
for i := 0 ; ; i ++ {
select {
case <- ctx . Done ( ) :
signal . Reset ( )
return
case sig := <- interrupt :
lg := slog . With ( "signal" , sig )
2023-09-02 11:28:23 +01:00
switch {
case i == 0 :
2023-08-25 19:55:11 +01:00
lg . Warn ( "Caught interrupt, safely shutting down" )
2023-09-02 11:28:23 +01:00
case i < 10 :
2023-08-25 19:55:11 +01:00
lg . Warn ( "Caught subsequent interrupt, please interrupt n more times to panic" , "n" , 10 - i )
2023-09-02 11:28:23 +01:00
default :
2023-08-25 19:55:11 +01:00
panic ( "force interrupt" )
}
err := vi . Cancel ( )
if err != nil {
2023-08-29 10:59:50 +01:00
lg . Warn ( "Failed to cancel VM context" , "error" , err . Error ( ) )
2023-08-25 19:55:11 +01:00
}
}
}
} ( )
fm := vm . NewFileManager ( slog . Default ( ) . With ( "caller" , "file-manager" ) , vi )
2023-08-25 16:54:58 +01:00
for {
select {
case err := <- runErrCh :
2023-08-27 15:53:44 +01:00
if err == nil {
err = fmt . Errorf ( "operation canceled by user" )
}
2023-08-29 10:59:50 +01:00
slog . Error ( "Failed to start the VM" , "error" , err . Error ( ) )
2023-08-31 20:10:26 +01:00
return 1
2023-08-25 16:54:58 +01:00
case <- vi . SSHUpNotifyChan ( ) :
err := fm . Init ( )
if err != nil {
2023-08-29 10:59:50 +01:00
slog . Error ( "Failed to initialize File Manager" , "error" , err . Error ( ) )
2023-08-31 20:10:26 +01:00
return 1
2023-08-25 16:54:58 +01:00
}
2023-08-31 16:23:40 +01:00
startupFailed := false
if tapRuntimeCtx != nil {
err := vi . ConfigureInterfaceStaticNet ( context . Background ( ) , "eth1" , tapRuntimeCtx . Net . GuestCIDR )
if err != nil {
slog . Error ( "Failed to configure tag interface network" , "error" , err . Error ( ) )
startupFailed = true
}
}
var exitCode int
if ! startupFailed {
exitCode = fn ( ctx , vi , fm , tapRuntimeCtx )
} else {
exitCode = 1
}
2023-08-25 16:54:58 +01:00
err = vi . Cancel ( )
if err != nil {
2023-08-29 10:59:50 +01:00
slog . Error ( "Failed to cancel VM context" , "error" , err . Error ( ) )
2023-08-31 20:10:26 +01:00
return 1
2023-08-25 16:54:58 +01:00
}
wg . Wait ( )
select {
case err := <- runErrCh :
if err != nil {
2023-08-29 10:59:50 +01:00
slog . Error ( "Failed to run the VM" , "error" , err . Error ( ) )
2023-08-31 20:10:26 +01:00
return 1
2023-08-25 16:54:58 +01:00
}
default :
}
2023-08-26 11:27:38 +01:00
return exitCode
2023-08-25 16:54:58 +01:00
}
}
2023-08-25 15:12:19 +01:00
}
2023-08-26 16:26:35 +01:00
2023-08-31 20:10:26 +01:00
func getDevicePassthroughConfig ( val string ) ( * vm . PassthroughConfig , error ) {
2023-09-01 11:44:49 +01:00
isRoot , err := osspecifics . CheckRunAsRoot ( )
if err != nil {
return nil , errors . Wrap ( err , "check whether the program is run as root" )
}
if ! isRoot {
return nil , fmt . Errorf ( "device passthrough of any type requires root (admin) privileges" )
}
2023-08-30 09:36:33 +01:00
valSplit := strings . Split ( val , ":" )
if want , have := 2 , len ( valSplit ) ; want != have {
2023-08-31 20:10:26 +01:00
return nil , fmt . Errorf ( "bad device passthrough syntax: wrong items split by ':' count: want %v, have %v" , want , have )
2023-08-30 09:36:33 +01:00
}
switch valSplit [ 0 ] {
case "usb" :
usbValsSplit := strings . Split ( valSplit [ 1 ] , "," )
if want , have := 2 , len ( usbValsSplit ) ; want != have {
2023-08-31 20:10:26 +01:00
return nil , fmt . Errorf ( "bad usb device passthrough syntax: wrong args split by ',' count: want %v, have %v" , want , have )
2023-08-30 09:36:33 +01:00
}
vendorID , err := strconv . ParseUint ( usbValsSplit [ 0 ] , 16 , 32 )
if err != nil {
2023-08-31 20:10:26 +01:00
return nil , fmt . Errorf ( "bad usb vendor id '%v'" , usbValsSplit [ 0 ] )
2023-08-30 09:36:33 +01:00
}
productID , err := strconv . ParseUint ( usbValsSplit [ 1 ] , 16 , 32 )
if err != nil {
2023-08-31 20:10:26 +01:00
return nil , fmt . Errorf ( "bad usb product id '%v'" , usbValsSplit [ 1 ] )
2023-08-30 09:36:33 +01:00
}
2023-08-31 20:10:26 +01:00
return & vm . PassthroughConfig {
2023-08-30 09:36:33 +01:00
USB : [ ] vm . USBDevicePassthroughConfig { {
VendorID : uint16 ( vendorID ) ,
ProductID : uint16 ( productID ) ,
} } ,
2023-08-31 20:10:26 +01:00
} , nil
2023-08-30 09:36:33 +01:00
case "dev" :
devPath := filepath . Clean ( valSplit [ 1 ] )
2023-09-01 11:44:49 +01:00
err := osspecifics . CheckValidDevicePath ( devPath )
if err != nil {
return nil , errors . Wrapf ( err , "check whether device path is valid '%v'" , devPath )
}
2023-08-30 09:36:33 +01:00
2023-08-31 20:10:26 +01:00
return & vm . PassthroughConfig { Block : [ ] vm . BlockDevicePassthroughConfig { {
2023-08-30 09:36:33 +01:00
Path : devPath ,
2023-08-31 20:10:26 +01:00
} } } , nil
2023-08-30 09:36:33 +01:00
default :
2023-08-31 20:10:26 +01:00
return nil , fmt . Errorf ( "unknown device passthrough type '%v'" , val )
2023-08-30 09:36:33 +01:00
}
}