Better VM runtime logic

This commit is contained in:
AlexSSD7 2023-09-02 11:47:58 +01:00
commit 7b5391f4d8
7 changed files with 165 additions and 220 deletions

View file

@ -13,10 +13,9 @@ var buildCmd = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
store := createStoreOrExit() store := createStoreOrExit()
err := store.BuildVMImageWithInterruptHandler(vmDebugFlag, buildOverwriteFlag) exitCode := store.RunCLIImageBuild(vmDebugFlag, buildOverwriteFlag)
if err != nil { if exitCode != 0 {
slog.Error("Failed to build VM image", "error", err.Error()) os.Exit(exitCode)
os.Exit(1)
} }
slog.Info("VM image built successfully", "path", store.GetVMImagePath()) slog.Info("VM image built successfully", "path", store.GetVMImagePath())

125
cmd/runvm/runvm.go Normal file
View file

@ -0,0 +1,125 @@
package runvm
import (
"context"
"fmt"
"os"
"os/signal"
"sync"
"syscall"
"log/slog"
"github.com/AlexSSD7/linsk/share"
"github.com/AlexSSD7/linsk/vm"
)
type RunVMFunc func(context.Context, *vm.VM, *vm.FileManager, *share.NetTapRuntimeContext) int
func RunVM(vi *vm.VM, initFileManager bool, tapRuntimeCtx *share.NetTapRuntimeContext, fn RunVMFunc) int {
runErrCh := make(chan error, 1)
var wg sync.WaitGroup
ctx, ctxCancel := context.WithCancel(context.Background())
defer ctxCancel()
interrupt := make(chan os.Signal, 2)
signal.Notify(interrupt, syscall.SIGTERM, syscall.SIGINT)
wg.Add(1)
go func() {
defer wg.Done()
err := vi.Run()
ctxCancel()
runErrCh <- err
}()
go func() {
for i := 0; ; i++ {
select {
case <-ctx.Done():
signal.Reset()
return
case sig := <-interrupt:
lg := slog.With("signal", sig)
switch {
case i == 0:
lg.Warn("Caught interrupt, safely shutting down")
case i < 10:
lg.Warn("Caught subsequent interrupt, please interrupt n more times to panic", "n", 10-i)
default:
panic("force interrupt")
}
err := vi.Cancel()
if err != nil {
lg.Warn("Failed to cancel VM context", "error", err.Error())
}
}
}
}()
var fm *vm.FileManager
if initFileManager {
fm = vm.NewFileManager(slog.Default().With("caller", "file-manager"), vi)
}
for {
select {
case err := <-runErrCh:
if err == nil {
err = fmt.Errorf("operation canceled by user")
}
slog.Error("Failed to start the VM", "error", err.Error())
return 1
case <-vi.SSHUpNotifyChan():
if fm != nil {
err := fm.Init()
if err != nil {
slog.Error("Failed to initialize File Manager", "error", err.Error())
return 1
}
}
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 {
slog.Error("Failed to cancel VM context", "error", err.Error())
return 1
}
wg.Wait()
select {
case err := <-runErrCh:
if err != nil {
slog.Error("Failed to run the VM", "error", err.Error())
return 1
}
default:
}
return exitCode
}
}
}

View file

@ -1,19 +1,16 @@
package cmd package cmd
import ( import (
"context"
"fmt" "fmt"
"os" "os"
"os/signal"
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
"sync"
"syscall"
"time" "time"
"log/slog" "log/slog"
"github.com/AlexSSD7/linsk/cmd/runvm"
"github.com/AlexSSD7/linsk/nettap" "github.com/AlexSSD7/linsk/nettap"
"github.com/AlexSSD7/linsk/osspecifics" "github.com/AlexSSD7/linsk/osspecifics"
"github.com/AlexSSD7/linsk/share" "github.com/AlexSSD7/linsk/share"
@ -32,9 +29,7 @@ func createStoreOrExit() *storage.Storage {
return store return store
} }
type runVMFunc func(context.Context, *vm.VM, *vm.FileManager, *share.NetTapRuntimeContext) int func runVM(passthroughArg string, fn runvm.RunVMFunc, forwardPortsRules []vm.PortForwardingRule, unrestrictedNetworking bool, withNetTap bool) int {
func runVM(passthroughArg string, fn runVMFunc, forwardPortsRules []vm.PortForwardingRule, unrestrictedNetworking bool, withNetTap bool) int {
store := createStoreOrExit() store := createStoreOrExit()
vmImagePath, err := store.CheckVMImageExists() vmImagePath, err := store.CheckVMImageExists()
@ -197,116 +192,13 @@ func runVM(passthroughArg string, fn runVMFunc, forwardPortsRules []vm.PortForwa
ShowDisplay: vmDebugFlag, ShowDisplay: vmDebugFlag,
} }
return innerRunVM(vmCfg, tapRuntimeCtx, fn)
}
func innerRunVM(vmCfg vm.VMConfig, tapRuntimeCtx *share.NetTapRuntimeContext, fn runVMFunc) int {
vi, err := vm.NewVM(slog.Default().With("caller", "vm"), vmCfg) vi, err := vm.NewVM(slog.Default().With("caller", "vm"), vmCfg)
if err != nil { if err != nil {
slog.Error("Failed to create vm instance", "error", err.Error()) slog.Error("Failed to create vm instance", "error", err.Error())
return 1 return 1
} }
runErrCh := make(chan error, 1) return runvm.RunVM(vi, true, tapRuntimeCtx, fn)
var wg sync.WaitGroup
ctx, ctxCancel := context.WithCancel(context.Background())
defer ctxCancel()
interrupt := make(chan os.Signal, 2)
signal.Notify(interrupt, syscall.SIGTERM, syscall.SIGINT)
wg.Add(1)
go func() {
defer wg.Done()
err := vi.Run()
ctxCancel()
runErrCh <- err
}()
go func() {
for i := 0; ; i++ {
select {
case <-ctx.Done():
signal.Reset()
return
case sig := <-interrupt:
lg := slog.With("signal", sig)
switch {
case i == 0:
lg.Warn("Caught interrupt, safely shutting down")
case i < 10:
lg.Warn("Caught subsequent interrupt, please interrupt n more times to panic", "n", 10-i)
default:
panic("force interrupt")
}
err := vi.Cancel()
if err != nil {
lg.Warn("Failed to cancel VM context", "error", err.Error())
}
}
}
}()
fm := vm.NewFileManager(slog.Default().With("caller", "file-manager"), vi)
for {
select {
case err := <-runErrCh:
if err == nil {
err = fmt.Errorf("operation canceled by user")
}
slog.Error("Failed to start the VM", "error", err.Error())
return 1
case <-vi.SSHUpNotifyChan():
err := fm.Init()
if err != nil {
slog.Error("Failed to initialize File Manager", "error", err.Error())
return 1
}
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 {
slog.Error("Failed to cancel VM context", "error", err.Error())
return 1
}
wg.Wait()
select {
case err := <-runErrCh:
if err != nil {
slog.Error("Failed to run the VM", "error", err.Error())
return 1
}
default:
}
return exitCode
}
}
} }
func getDevicePassthroughConfig(val string) (*vm.PassthroughConfig, error) { func getDevicePassthroughConfig(val string) (*vm.PassthroughConfig, error) {

View file

@ -6,15 +6,14 @@ import (
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
"os/signal"
"path/filepath" "path/filepath"
"strings" "strings"
"sync"
"syscall"
"log/slog" "log/slog"
"github.com/AlexSSD7/linsk/cmd/runvm"
"github.com/AlexSSD7/linsk/osspecifics" "github.com/AlexSSD7/linsk/osspecifics"
"github.com/AlexSSD7/linsk/share"
"github.com/AlexSSD7/linsk/utils" "github.com/AlexSSD7/linsk/utils"
"github.com/AlexSSD7/linsk/vm" "github.com/AlexSSD7/linsk/vm"
"github.com/alessio/shellescape" "github.com/alessio/shellescape"
@ -88,99 +87,29 @@ func createQEMUImg(outPath string) error {
return nil return nil
} }
func (bc *BuildContext) BuildWithInterruptHandler() error { func (bc *BuildContext) RunCLIBuild() int {
defer func() { return runvm.RunVM(bc.vi, false, nil, func(ctx context.Context, v *vm.VM, fm *vm.FileManager, ntrc *share.NetTapRuntimeContext) int {
err := bc.vi.Cancel() sc, err := bc.vi.DialSSH()
if err != nil { if err != nil {
bc.logger.Error("Failed to cancel VM context", "error", err.Error()) bc.logger.Error("Failed to dial VM SSH", "error", err.Error())
return 1
} }
}()
runErrCh := make(chan error, 1) defer func() { _ = sc.Close() }()
var wg sync.WaitGroup
ctx, ctxCancel := context.WithCancel(context.Background()) bc.logger.Info("VM OS installation in progress")
defer ctxCancel()
interrupt := make(chan os.Signal, 2) err = runAlpineSetup(sc, []string{"openssh", "lvm2", "util-linux", "cryptsetup", "vsftpd", "samba", "netatalk"})
signal.Notify(interrupt, syscall.SIGTERM, syscall.SIGINT) if err != nil {
defer signal.Reset() bc.logger.Error("Failed to set up Alpine Linux", "error", err.Error())
return 1
wg.Add(1)
go func() {
defer wg.Done()
err := bc.vi.Run()
ctxCancel()
runErrCh <- err
}()
go func() {
for i := 0; ; i++ {
select {
case <-ctx.Done():
return
case sig := <-interrupt:
lg := slog.With("signal", sig)
if i == 0 {
lg.Warn("Caught interrupt, safely shutting down")
} else if i < 10 {
lg.Warn("Caught subsequent interrupt, please interrupt n more times to panic", "n", 10-i)
} else {
panic("force interrupt")
}
err := bc.vi.Cancel()
if err != nil {
lg.Warn("Failed to cancel VM context", "error", err.Error())
}
}
} }
}()
for { return 0
select { })
case err := <-runErrCh:
if err == nil {
return fmt.Errorf("operation canceled by user")
}
return errors.Wrap(err, "start vm")
case <-bc.vi.SSHUpNotifyChan():
sc, err := bc.vi.DialSSH()
if err != nil {
return errors.Wrap(err, "dial vm ssh")
}
defer func() { _ = sc.Close() }()
bc.logger.Info("VM OS installation in progress")
err = runAlpineSetupCmd(sc, []string{"openssh", "lvm2", "util-linux", "cryptsetup", "vsftpd", "samba", "netatalk"})
if err != nil {
return errors.Wrap(err, "run alpine setup cmd")
}
err = bc.vi.Cancel()
if err != nil {
return errors.Wrap(err, "cancel vm context")
}
select {
case err := <-runErrCh:
if err != nil {
return errors.Wrap(err, "run vm")
}
default:
}
return nil
}
}
} }
func runAlpineSetupCmd(sc *ssh.Client, pkgs []string) error { func runAlpineSetup(sc *ssh.Client, pkgs []string) error {
sess, err := sc.NewSession() sess, err := sc.NewSession()
if err != nil { if err != nil {
return errors.Wrap(err, "new session") return errors.Wrap(err, "new session")
@ -204,6 +133,7 @@ func runAlpineSetupCmd(sc *ssh.Client, pkgs []string) error {
cmd += " && mount /dev/vda3 /mnt && chroot /mnt apk add " + strings.Join(pkgsQuoted, " ") cmd += " && mount /dev/vda3 /mnt && chroot /mnt apk add " + strings.Join(pkgsQuoted, " ")
} }
//nolint:dupword
cmd += `&& chroot /mnt ash -c 'echo "PasswordAuthentication no" >> /etc/ssh/sshd_config && addgroup -g 1000 linsk && adduser -D -h /mnt -G linsk linsk -u 1000 && touch /etc/network/interfaces'` cmd += `&& chroot /mnt ash -c 'echo "PasswordAuthentication no" >> /etc/ssh/sshd_config && addgroup -g 1000 linsk && adduser -D -h /mnt -G linsk linsk -u 1000 && touch /etc/network/interfaces'`
err = sess.Run(cmd) err = sess.Run(cmd)

View file

@ -27,10 +27,8 @@ func (s *Storage) download(url string, hash []byte, out string, applyReaderMiddl
_, err := os.Stat(out) _, err := os.Stat(out)
if err == nil { if err == nil {
return errors.Wrap(err, "file already exists") return errors.Wrap(err, "file already exists")
} else { } else if !errors.Is(err, os.ErrNotExist) {
if !errors.Is(err, os.ErrNotExist) { return errors.Wrap(err, "stat out path")
return errors.Wrap(err, "stat out path")
}
} }
f, err := os.OpenFile(out, os.O_CREATE|os.O_WRONLY, 0400) f, err := os.OpenFile(out, os.O_CREATE|os.O_WRONLY, 0400)

View file

@ -38,7 +38,7 @@ func validateFileHash(path string, hash []byte) error {
sum := h.Sum(nil) sum := h.Sum(nil)
if !bytes.Equal(sum, hash) { if !bytes.Equal(sum, hash) {
return fmt.Errorf("hash mismatch: want '%v', have '%v'", hex.EncodeToString(hash), hex.EncodeToString(sum)) return fmt.Errorf("hash mismatch: want '%v', have '%v' (path '%v')", hex.EncodeToString(hash), hex.EncodeToString(sum), path)
} }
return nil return nil

View file

@ -3,7 +3,6 @@ package storage
import ( import (
"compress/bzip2" "compress/bzip2"
"fmt" "fmt"
"io"
"log/slog" "log/slog"
"os" "os"
"path/filepath" "path/filepath"
@ -69,33 +68,37 @@ func (s *Storage) GetAarch64EFIImagePath() string {
return filepath.Join(s.path, constants.GetAarch64EFIImageName()) return filepath.Join(s.path, constants.GetAarch64EFIImageName())
} }
func (s *Storage) BuildVMImageWithInterruptHandler(showBuilderVMDisplay bool, overwrite bool) error { func (s *Storage) RunCLIImageBuild(showBuilderVMDisplay bool, overwrite bool) int {
vmImagePath := s.GetVMImagePath() vmImagePath := s.GetVMImagePath()
removed, err := checkExistsOrRemove(vmImagePath, overwrite) removed, err := checkExistsOrRemove(vmImagePath, overwrite)
if err != nil { if err != nil {
return errors.Wrap(err, "check exists or remove") slog.Error("Failed to check for (and remove) existing VM image", "error", err.Error())
return 1
} }
baseImagePath, err := s.CheckDownloadBaseImage() baseImagePath, err := s.CheckDownloadBaseImage()
if err != nil { if err != nil {
return errors.Wrap(err, "check/download base image") slog.Error("Failed to check or download base VM image", "error", err.Error())
return 1
} }
biosPath, err := s.CheckDownloadVMBIOS() biosPath, err := s.CheckDownloadVMBIOS()
if err != nil { if err != nil {
return errors.Wrap(err, "check/download vm bios") slog.Error("Failed to check or download VM BIOS", "error", err.Error())
return 1
} }
s.logger.Info("Building VM image", "tags", constants.GetAlpineBaseImageTags(), "overwriting", removed, "dst", vmImagePath) s.logger.Info("Building VM image", "tags", constants.GetAlpineBaseImageTags(), "overwriting", removed, "dst", vmImagePath)
buildCtx, err := imgbuilder.NewBuildContext(s.logger.With("subcaller", "imgbuilder"), baseImagePath, vmImagePath, showBuilderVMDisplay, biosPath) buildCtx, err := imgbuilder.NewBuildContext(s.logger.With("subcaller", "imgbuilder"), baseImagePath, vmImagePath, showBuilderVMDisplay, biosPath)
if err != nil { if err != nil {
return errors.Wrap(err, "create new img build context") slog.Error("Failed to create new image build context", "error", err.Error())
return 1
} }
err = buildCtx.BuildWithInterruptHandler() exitCode := buildCtx.RunCLIBuild()
if err != nil { if exitCode != 0 {
return errors.Wrap(err, "do build") return exitCode
} }
err = os.Remove(baseImagePath) err = os.Remove(baseImagePath)
@ -105,7 +108,7 @@ func (s *Storage) BuildVMImageWithInterruptHandler(showBuilderVMDisplay bool, ov
s.logger.Info("Removed base image", "path", baseImagePath) s.logger.Info("Removed base image", "path", baseImagePath)
} }
return nil return 0
} }
func (s *Storage) CheckVMImageExists() (string, error) { func (s *Storage) CheckVMImageExists() (string, error) {
@ -152,9 +155,7 @@ func (s *Storage) CheckDownloadAarch64EFIImage() (string, error) {
} }
// EFI image doesn't exist. Download one. // EFI image doesn't exist. Download one.
err := s.download(constants.GetAarch64EFIImageBZ2URL(), constants.GetAarch64EFIImageHash(), efiImagePath, func(r io.Reader) io.Reader { err := s.download(constants.GetAarch64EFIImageBZ2URL(), constants.GetAarch64EFIImageHash(), efiImagePath, bzip2.NewReader)
return bzip2.NewReader(r)
})
if err != nil { if err != nil {
return "", errors.Wrap(err, "download base alpine image") return "", errors.Wrap(err, "download base alpine image")
} }