linsk/cmd/utils.go

279 lines
8.2 KiB
Go
Raw Normal View History

2023-09-02 20:03:44 +01:00
// Linsk - A utility to access Linux-native file systems on non-Linux operating systems.
// Copyright (c) 2023 The Linsk Authors.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
2023-08-25 15:12:19 +01:00
package cmd
import (
"context"
2023-08-26 16:26:35 +01:00
"fmt"
2023-08-25 15:12:19 +01:00
"os"
2023-08-30 09:36:33 +01:00
"path/filepath"
"strconv"
"strings"
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-09-02 11:47:58 +01:00
"github.com/AlexSSD7/linsk/cmd/runvm"
"github.com/AlexSSD7/linsk/nettap"
2023-09-01 11:44:49 +01:00
"github.com/AlexSSD7/linsk/osspecifics"
"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"
)
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-02 12:09:26 +01:00
func runVM(passthroughArg string, fn runvm.Func, forwardPortsRules []vm.PortForwardingRule, unrestrictedNetworking bool, withNetTap bool) int {
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())
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.")
return 1
2023-08-30 09:19:02 +01:00
}
biosPath, err := store.CheckDownloadVMBIOS(context.Background())
2023-08-30 13:32:00 +01:00
if err != nil {
slog.Error("Failed to check/download VM BIOS", "error", err.Error())
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 != "" {
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-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
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())
return 1
}
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
}
knownAllocs, err := store.ListNetTapAllocations()
if err != nil {
slog.Error("Failed to list net tap allocations", "error", err.Error())
return 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())
return 1
}
tapManager, err = nettap.NewTapManager(slog.Default())
if err != nil {
slog.Error("Failed to create net tap manager", "error", err.Error())
return 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(), "tap-name", tapNameToUse)
// Non-critical error.
}
slog.Error("Failed to create new tap", "error", err.Error())
return 1
}
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)
}
}
}()
tapNet, err := nettap.GenerateNet()
if err != nil {
slog.Error("Failed to generate tap net plan", "error", err.Error())
return 1
}
err = tapManager.ConfigureNet(tapNameToUse, tapNet.HostCIDR)
if err != nil {
slog.Error("Failed to configure tap net", "error", err.Error())
return 1
}
tapRuntimeCtx = &share.NetTapRuntimeContext{
Manager: tapManager,
Name: tapNameToUse,
Net: tapNet,
}
tapsConfig = []vm.TapConfig{{
Name: tapNameToUse,
}}
}
vmCfg := vm.Config{
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,
UnrestrictedNetworking: unrestrictedNetworking,
Taps: tapsConfig,
2023-08-29 11:51:06 +01:00
OSUpTimeout: time.Duration(vmOSUpTimeoutFlag) * time.Second,
SSHUpTimeout: time.Duration(vmSSHSetupTimeoutFlag) * time.Second,
Debug: vmDebugFlag,
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())
return 1
2023-08-25 15:12:19 +01:00
}
2023-08-25 16:54:58 +01:00
2023-09-02 11:47:58 +01:00
return runvm.RunVM(vi, true, tapRuntimeCtx, fn)
2023-08-25 15:12:19 +01:00
}
2023-08-26 16:26:35 +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 {
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 {
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 {
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 {
return nil, fmt.Errorf("bad usb product id '%v'", usbValsSplit[1])
2023-08-30 09:36:33 +01:00
}
return &vm.PassthroughConfig{
2023-08-30 09:36:33 +01:00
USB: []vm.USBDevicePassthroughConfig{{
VendorID: uint16(vendorID),
ProductID: uint16(productID),
}},
}, 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
blockSize, err := osspecifics.GetDeviceLogicalBlockSize(devPath)
if err != nil {
return nil, errors.Wrapf(err, "get logical block size for device '%v'", devPath)
}
return &vm.PassthroughConfig{Block: []vm.BlockDevicePassthroughConfig{{
Path: devPath,
BlockSize: blockSize,
}}}, nil
2023-08-30 09:36:33 +01:00
default:
return nil, fmt.Errorf("unknown device passthrough type '%v'", val)
2023-08-30 09:36:33 +01:00
}
}