Wire most of the work from today together
This commit is contained in:
parent
40aa08c86c
commit
e57519e58d
12 changed files with 328 additions and 132 deletions
|
|
@ -7,6 +7,7 @@ import (
|
|||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/AlexSSD7/linsk/utils"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
|
@ -26,7 +27,7 @@ var cleanCmd = &cobra.Command{
|
|||
os.Exit(1)
|
||||
}
|
||||
|
||||
if strings.ToLower(string(answer)) != "y\n" {
|
||||
if utils.ClearUnprintableChars(strings.ToLower(string(answer)), false) != "y" {
|
||||
fmt.Fprintf(os.Stderr, "Aborted.\n")
|
||||
os.Exit(2)
|
||||
}
|
||||
|
|
@ -37,6 +38,8 @@ var cleanCmd = &cobra.Command{
|
|||
os.Exit(1)
|
||||
}
|
||||
|
||||
// TODO: Clean network tap allocations, if any.
|
||||
|
||||
slog.Info("Deleted data directory", "path", rmPath)
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"log/slog"
|
||||
"os"
|
||||
|
||||
"github.com/AlexSSD7/linsk/share"
|
||||
"github.com/AlexSSD7/linsk/vm"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
|
@ -15,7 +16,7 @@ var lsCmd = &cobra.Command{
|
|||
Short: "Start a VM and list all user drives within the VM. Uses lsblk command under the hood.",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
os.Exit(runVM(args[0], func(ctx context.Context, i *vm.VM, fm *vm.FileManager) int {
|
||||
os.Exit(runVM(args[0], func(ctx context.Context, i *vm.VM, fm *vm.FileManager, trc *share.NetTapRuntimeContext) int {
|
||||
lsblkOut, err := fm.Lsblk()
|
||||
if err != nil {
|
||||
slog.Error("Failed to list block devices in the VM", "error", err.Error())
|
||||
|
|
@ -29,6 +30,6 @@ var lsCmd = &cobra.Command{
|
|||
}
|
||||
|
||||
return 0
|
||||
}, nil, false))
|
||||
}, nil, false, false))
|
||||
},
|
||||
}
|
||||
|
|
|
|||
95
cmd/run.go
95
cmd/run.go
|
|
@ -4,9 +4,11 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/AlexSSD7/linsk/share"
|
||||
"github.com/AlexSSD7/linsk/vm"
|
||||
"github.com/sethvargo/go-password/password"
|
||||
"github.com/spf13/cobra"
|
||||
|
|
@ -20,46 +22,30 @@ var runCmd = &cobra.Command{
|
|||
vmMountDevName := args[1]
|
||||
fsType := args[2]
|
||||
|
||||
ftpPassivePortCount := uint16(9)
|
||||
newBackendFunc := share.GetBackend(shareBackendFlag)
|
||||
if newBackendFunc == nil {
|
||||
slog.Error("Unknown file share backend", "type", shareBackendFlag)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
networkSharePort, err := getClosestAvailPortWithSubsequent(9000, 10)
|
||||
cfg, err := share.RawUserConfiguration{
|
||||
ListenIP: shareListenIPFlag,
|
||||
|
||||
FTPExtIP: ftpExtIPFlag,
|
||||
SMBExtMode: smbUseExternAddrFlag,
|
||||
}.Process(shareBackendFlag, slog.With("caller", "share-config"))
|
||||
if err != nil {
|
||||
slog.Error("Failed to get closest available host port for network file share", "error", err.Error())
|
||||
slog.Error("Failed to process raw configuration", "error", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
ftpListenIP := net.ParseIP(ftpListenAddrFlag)
|
||||
if ftpListenIP == nil {
|
||||
slog.Error("Invalid FTP listen address specified", "value", ftpListenAddrFlag)
|
||||
backend, vmOpts, err := newBackendFunc(cfg)
|
||||
if err != nil {
|
||||
slog.Error("Failed to initialize share backend", "backend", shareBackendFlag, "error", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
ftpExtIP := net.ParseIP(ftpExtIPFlag)
|
||||
if ftpExtIP == nil {
|
||||
slog.Error("Invalid FTP external IP specified", "value", ftpExtIPFlag)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if ftpListenAddrFlag != defaultFTPListenAddr && ftpExtIPFlag == defaultFTPListenAddr {
|
||||
slog.Warn("No external FTP IP address via --ftp-extip was configured. This is a requirement in almost all scenarios if you want to connect remotely.")
|
||||
}
|
||||
|
||||
ports := []vm.PortForwardingRule{{
|
||||
HostIP: ftpListenIP,
|
||||
HostPort: networkSharePort,
|
||||
VMPort: 21,
|
||||
}}
|
||||
|
||||
for i := uint16(0); i < ftpPassivePortCount; i++ {
|
||||
p := networkSharePort + 1 + i
|
||||
ports = append(ports, vm.PortForwardingRule{
|
||||
HostIP: ftpListenIP,
|
||||
HostPort: p,
|
||||
VMPort: p,
|
||||
})
|
||||
}
|
||||
|
||||
os.Exit(runVM(args[0], func(ctx context.Context, i *vm.VM, fm *vm.FileManager) int {
|
||||
os.Exit(runVM(args[0], func(ctx context.Context, i *vm.VM, fm *vm.FileManager, tapCtx *share.NetTapRuntimeContext) int {
|
||||
slog.Info("Mounting the device", "dev", vmMountDevName, "fs", fsType, "luks", luksFlag)
|
||||
|
||||
err := fm.Mount(vmMountDevName, vm.MountOptions{
|
||||
|
|
@ -77,32 +63,53 @@ var runCmd = &cobra.Command{
|
|||
return 1
|
||||
}
|
||||
|
||||
err = fm.StartFTP(sharePWD, networkSharePort+1, ftpPassivePortCount, ftpExtIP)
|
||||
shareURI, err := backend.Apply(ctx, sharePWD, &share.VMShareContext{
|
||||
Instance: i,
|
||||
FileManager: fm,
|
||||
NetTapCtx: tapCtx,
|
||||
})
|
||||
if err != nil {
|
||||
slog.Error("Failed to start FTP server", "error", err.Error())
|
||||
slog.Error("Failed to apply (start) file share backend", "backend", shareBackendFlag, "error", err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
slog.Info("Started the network share successfully", "type", "ftp")
|
||||
|
||||
shareURI := "ftp://linsk:" + sharePWD + "@" + ftpExtIP.String() + ":" + fmt.Sprint(networkSharePort)
|
||||
|
||||
fmt.Fprintf(os.Stderr, "================\n[Network File Share Config]\nThe network file share was started. Please use the credentials below to connect to the file server.\n\nType: FTP\nServer Address: ftp://%v:%v\nUsername: linsk\nPassword: %v\n\nShare URI: %v\n================\n", ftpExtIP.String(), networkSharePort, sharePWD, shareURI)
|
||||
fmt.Fprintf(os.Stderr, "============================\n[Network File Share Config]\nThe network file share was started. Please use the credentials below to connect to the file server.\n\nType: "+strings.ToUpper(shareBackendFlag)+"\nURL: %v\nUsername: linsk\nPassword: %v\n===========================\n", shareURI, sharePWD)
|
||||
|
||||
<-ctx.Done()
|
||||
return 0
|
||||
}, ports, unrestrictedNetworkingFlag))
|
||||
}, vmOpts.Ports, unrestrictedNetworkingFlag, vmOpts.EnableTap))
|
||||
},
|
||||
}
|
||||
|
||||
var luksFlag bool
|
||||
var ftpListenAddrFlag string
|
||||
var shareListenIPFlag string
|
||||
var ftpExtIPFlag string
|
||||
|
||||
const defaultFTPListenAddr = "127.0.0.1"
|
||||
var shareBackendFlag string
|
||||
var smbUseExternAddrFlag bool
|
||||
|
||||
func init() {
|
||||
runCmd.Flags().BoolVarP(&luksFlag, "luks", "l", false, "Use cryptsetup to open a LUKS volume (password will be prompted).")
|
||||
runCmd.Flags().StringVar(&ftpListenAddrFlag, "ftp-listen", defaultFTPListenAddr, "Specifies the address to bind the FTP ports to. NOTE: Changing bind address is not enough to connect remotely. You should also specify --ftp-extip.")
|
||||
runCmd.Flags().StringVar(&ftpExtIPFlag, "ftp-extip", defaultFTPListenAddr, "Specifies the external IP the FTP server should advertise.")
|
||||
|
||||
var defaultShareType string
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
defaultShareType = "smb"
|
||||
default:
|
||||
defaultShareType = "ftp"
|
||||
}
|
||||
|
||||
runCmd.Flags().StringVar(&shareBackendFlag, "share-backend", defaultShareType, "Specifies the file share backend to use. The default value is OS-specific.")
|
||||
runCmd.Flags().StringVar(&shareListenIPFlag, "share-listen", share.GetDefaultListenIPStr(), "Specifies the IP to bind the network share port to. NOTE: For FTP, changing the bind address is not enough to connect remotely. You should also specify --ftp-extip.")
|
||||
|
||||
smbExternDefault := false
|
||||
if runtime.GOOS == "windows" {
|
||||
smbExternDefault = true
|
||||
}
|
||||
|
||||
runCmd.Flags().StringVar(&ftpExtIPFlag, "ftp-extip", share.GetDefaultListenIPStr(), "Specifies the external IP the FTP server should advertise.")
|
||||
runCmd.Flags().BoolVar(&smbUseExternAddrFlag, "smb-extern", smbExternDefault, "Specifies whether Linsk emulate external networking for the VM's SMB server. This is the default for Windows as there is no way to specify ports in Windows SMB client.")
|
||||
|
||||
// TODO: log the use of smbUseExternAddrFlag when SMB is not enabled.
|
||||
}
|
||||
|
|
|
|||
10
cmd/shell.go
10
cmd/shell.go
|
|
@ -7,6 +7,7 @@ import (
|
|||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/AlexSSD7/linsk/share"
|
||||
"github.com/AlexSSD7/linsk/vm"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
|
@ -39,11 +40,7 @@ var shellCmd = &cobra.Command{
|
|||
forwardPortRules = append(forwardPortRules, fpr)
|
||||
}
|
||||
|
||||
if !unrestrictedNetworkingFlag {
|
||||
slog.Warn("IMPORTANT: By default, Linsk shell starts a VM with restricted networking as it's done with `run` command. This means that you will have no internet access in the shell. If you want to have access to the internet, please add `--vm-unrestricted-networking` flag to your `linsk shell` command.")
|
||||
}
|
||||
|
||||
os.Exit(runVM(passthroughArg, func(ctx context.Context, i *vm.VM, fm *vm.FileManager) int {
|
||||
os.Exit(runVM(passthroughArg, func(ctx context.Context, i *vm.VM, fm *vm.FileManager, trc *share.NetTapRuntimeContext) int {
|
||||
sc, err := i.DialSSH()
|
||||
if err != nil {
|
||||
slog.Error("Failed to dial VM SSH", "error", err.Error())
|
||||
|
|
@ -130,7 +127,8 @@ var shellCmd = &cobra.Command{
|
|||
}
|
||||
|
||||
return 0
|
||||
}, forwardPortRules, unrestrictedNetworkingFlag))
|
||||
}, forwardPortRules, true, false))
|
||||
// TODO: Enable tap option.
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
189
cmd/utils.go
189
cmd/utils.go
|
|
@ -3,7 +3,6 @@ package cmd
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
"os/user"
|
||||
|
|
@ -17,6 +16,8 @@ import (
|
|||
|
||||
"log/slog"
|
||||
|
||||
"github.com/AlexSSD7/linsk/nettap"
|
||||
"github.com/AlexSSD7/linsk/share"
|
||||
"github.com/AlexSSD7/linsk/storage"
|
||||
"github.com/AlexSSD7/linsk/vm"
|
||||
"github.com/pkg/errors"
|
||||
|
|
@ -64,7 +65,7 @@ func createStore() *storage.Storage {
|
|||
return store
|
||||
}
|
||||
|
||||
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, *share.NetTapRuntimeContext) int, forwardPortsRules []vm.PortForwardingRule, unrestrictedNetworking bool, withNetTap bool) int {
|
||||
store := createStore()
|
||||
|
||||
vmImagePath, err := store.CheckVMImageExists()
|
||||
|
|
@ -80,7 +81,7 @@ func runVM(passthroughArg string, fn func(context.Context, *vm.VM, *vm.FileManag
|
|||
|
||||
biosPath, err := store.CheckDownloadVMBIOS()
|
||||
if err != nil {
|
||||
slog.Error("Failed to check/download VM BIOS", "error", err)
|
||||
slog.Error("Failed to check/download VM BIOS", "error", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
|
|
@ -91,6 +92,85 @@ func runVM(passthroughArg string, fn func(context.Context, *vm.VM, *vm.FileManag
|
|||
doUSBRootCheck()
|
||||
}
|
||||
|
||||
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())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
tapNameToUse := nettap.NewRandomTapName()
|
||||
// TODO: Run two instances at the same time and check whether nothing is wrongfully pruned.
|
||||
knownAllocs, err := store.ListNetTapAllocations()
|
||||
if err != nil {
|
||||
slog.Error("Failed to list net tap allocations", "error", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
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())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
tapManager, err = nettap.NewTapManager(slog.Default())
|
||||
if err != nil {
|
||||
slog.Error("Failed to create net tap manager", "error", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
err = tapManager.CreateNewTap(tapNameToUse)
|
||||
if err != nil {
|
||||
releaseErr := store.ReleaseNetTapAllocation(tapNameToUse)
|
||||
if releaseErr != nil {
|
||||
slog.Error("Failed to release net tap allocation", "error", releaseErr.Error(), "tapname", tapNameToUse)
|
||||
}
|
||||
|
||||
slog.Error("Failed to create new tap", "error", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
tapNet, err := nettap.GenerateNet()
|
||||
if err != nil {
|
||||
slog.Error("Failed to generate tap net plan", "error", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
err = tapManager.ConfigureNet(tapNameToUse, tapNet.HostCIDR)
|
||||
if err != nil {
|
||||
slog.Error("Failed to configure tap net", "error", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
tapRuntimeCtx = &share.NetTapRuntimeContext{
|
||||
Manager: tapManager,
|
||||
Name: tapNameToUse,
|
||||
Net: tapNet,
|
||||
}
|
||||
|
||||
tapsConfig = []vm.TapConfig{{
|
||||
Name: tapNameToUse,
|
||||
}}
|
||||
|
||||
// TODO: Clean the tap up before exiting.
|
||||
}
|
||||
|
||||
vmCfg := vm.VMConfig{
|
||||
Drives: []vm.DriveConfig{{
|
||||
Path: vmImagePath,
|
||||
|
|
@ -103,11 +183,13 @@ func runVM(passthroughArg string, fn func(context.Context, *vm.VM, *vm.FileManag
|
|||
PassthroughConfig: passthroughConfig,
|
||||
ExtraPortForwardingRules: forwardPortsRules,
|
||||
|
||||
UnrestrictedNetworking: unrestrictedNetworking,
|
||||
Taps: tapsConfig,
|
||||
|
||||
OSUpTimeout: time.Duration(vmOSUpTimeoutFlag) * time.Second,
|
||||
SSHUpTimeout: time.Duration(vmSSHSetupTimeoutFlag) * time.Second,
|
||||
|
||||
UnrestrictedNetworking: unrestrictedNetworking,
|
||||
ShowDisplay: vmDebugFlag,
|
||||
ShowDisplay: vmDebugFlag,
|
||||
}
|
||||
|
||||
vi, err := vm.NewVM(slog.Default().With("caller", "vm"), vmCfg)
|
||||
|
|
@ -177,7 +259,23 @@ func runVM(passthroughArg string, fn func(context.Context, *vm.VM, *vm.FileManag
|
|||
os.Exit(1)
|
||||
}
|
||||
|
||||
exitCode := fn(ctx, vi, fm)
|
||||
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
|
||||
}
|
||||
|
||||
err = vi.Cancel()
|
||||
if err != nil {
|
||||
|
|
@ -201,64 +299,6 @@ func runVM(passthroughArg string, fn func(context.Context, *vm.VM, *vm.FileManag
|
|||
}
|
||||
}
|
||||
|
||||
func checkPortAvailable(port uint16, subsequent uint16) (bool, error) {
|
||||
if port+subsequent < port {
|
||||
return false, fmt.Errorf("subsequent ports exceed allowed port range")
|
||||
}
|
||||
|
||||
if subsequent == 0 {
|
||||
ln, err := net.Listen("tcp", ":"+fmt.Sprint(port))
|
||||
if err != nil {
|
||||
if opErr, ok := err.(*net.OpError); ok {
|
||||
if sysErr, ok := opErr.Err.(*os.SyscallError); ok {
|
||||
if sysErr.Err == syscall.EADDRINUSE {
|
||||
// The port is in use.
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false, errors.Wrapf(err, "net listen (port %v)", port)
|
||||
}
|
||||
|
||||
err = ln.Close()
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "close ephemeral listener")
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
for i := uint16(0); i < subsequent; i++ {
|
||||
ok, err := checkPortAvailable(port+i, 0)
|
||||
if err != nil {
|
||||
return false, errors.Wrapf(err, "check subsequent port available (base: %v, seq: %v)", port, i)
|
||||
}
|
||||
|
||||
if !ok {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func getClosestAvailPortWithSubsequent(port uint16, subsequent uint16) (uint16, error) {
|
||||
// We use 10 as port range
|
||||
for i := port; i < 65535; i += subsequent {
|
||||
ok, err := checkPortAvailable(i, subsequent)
|
||||
if err != nil {
|
||||
return 0, errors.Wrapf(err, "check port available (%v)", i)
|
||||
}
|
||||
|
||||
if ok {
|
||||
return i, nil
|
||||
}
|
||||
}
|
||||
|
||||
return 0, fmt.Errorf("no available port (with %v subsequent ones) found", subsequent)
|
||||
}
|
||||
|
||||
func getDevicePassthroughConfig(val string) vm.PassthroughConfig {
|
||||
valSplit := strings.Split(val, ":")
|
||||
if want, have := 2, len(valSplit); want != have {
|
||||
|
|
@ -294,17 +334,18 @@ func getDevicePassthroughConfig(val string) vm.PassthroughConfig {
|
|||
}
|
||||
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)
|
||||
}
|
||||
// TODO: This is for Linux only. Should support Windows as well.
|
||||
// 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)
|
||||
}
|
||||
// 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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue