diff --git a/cmd/build.go b/cmd/build.go index 5a6fccb..d08fd8b 100644 --- a/cmd/build.go +++ b/cmd/build.go @@ -13,10 +13,9 @@ var buildCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { store := createStoreOrExit() - err := store.BuildVMImageWithInterruptHandler(vmDebugFlag, buildOverwriteFlag) - if err != nil { - slog.Error("Failed to build VM image", "error", err.Error()) - os.Exit(1) + exitCode := store.RunCLIImageBuild(vmDebugFlag, buildOverwriteFlag) + if exitCode != 0 { + os.Exit(exitCode) } slog.Info("VM image built successfully", "path", store.GetVMImagePath()) diff --git a/cmd/runvm/runvm.go b/cmd/runvm/runvm.go new file mode 100644 index 0000000..4ca5e41 --- /dev/null +++ b/cmd/runvm/runvm.go @@ -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 + } + } +} diff --git a/cmd/utils.go b/cmd/utils.go index a7e8c44..a0cf5f8 100644 --- a/cmd/utils.go +++ b/cmd/utils.go @@ -1,19 +1,16 @@ package cmd import ( - "context" "fmt" "os" - "os/signal" "path/filepath" "strconv" "strings" - "sync" - "syscall" "time" "log/slog" + "github.com/AlexSSD7/linsk/cmd/runvm" "github.com/AlexSSD7/linsk/nettap" "github.com/AlexSSD7/linsk/osspecifics" "github.com/AlexSSD7/linsk/share" @@ -32,9 +29,7 @@ func createStoreOrExit() *storage.Storage { return store } -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 { +func runVM(passthroughArg string, fn runvm.RunVMFunc, forwardPortsRules []vm.PortForwardingRule, unrestrictedNetworking bool, withNetTap bool) int { store := createStoreOrExit() vmImagePath, err := store.CheckVMImageExists() @@ -197,116 +192,13 @@ func runVM(passthroughArg string, fn runVMFunc, forwardPortsRules []vm.PortForwa 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) if err != nil { slog.Error("Failed to create vm instance", "error", err.Error()) return 1 } - 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()) - } - } - } - }() - - 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 - } - } + return runvm.RunVM(vi, true, tapRuntimeCtx, fn) } func getDevicePassthroughConfig(val string) (*vm.PassthroughConfig, error) { diff --git a/imgbuilder/imgbuilder.go b/imgbuilder/imgbuilder.go index a4d8725..842b41f 100644 --- a/imgbuilder/imgbuilder.go +++ b/imgbuilder/imgbuilder.go @@ -6,15 +6,14 @@ import ( "fmt" "os" "os/exec" - "os/signal" "path/filepath" "strings" - "sync" - "syscall" "log/slog" + "github.com/AlexSSD7/linsk/cmd/runvm" "github.com/AlexSSD7/linsk/osspecifics" + "github.com/AlexSSD7/linsk/share" "github.com/AlexSSD7/linsk/utils" "github.com/AlexSSD7/linsk/vm" "github.com/alessio/shellescape" @@ -88,99 +87,29 @@ func createQEMUImg(outPath string) error { return nil } -func (bc *BuildContext) BuildWithInterruptHandler() error { - defer func() { - err := bc.vi.Cancel() +func (bc *BuildContext) RunCLIBuild() int { + return runvm.RunVM(bc.vi, false, nil, func(ctx context.Context, v *vm.VM, fm *vm.FileManager, ntrc *share.NetTapRuntimeContext) int { + sc, err := bc.vi.DialSSH() 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) - var wg sync.WaitGroup + defer func() { _ = sc.Close() }() - ctx, ctxCancel := context.WithCancel(context.Background()) - defer ctxCancel() + bc.logger.Info("VM OS installation in progress") - interrupt := make(chan os.Signal, 2) - signal.Notify(interrupt, syscall.SIGTERM, syscall.SIGINT) - defer signal.Reset() - - 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()) - } - } + err = runAlpineSetup(sc, []string{"openssh", "lvm2", "util-linux", "cryptsetup", "vsftpd", "samba", "netatalk"}) + if err != nil { + bc.logger.Error("Failed to set up Alpine Linux", "error", err.Error()) + return 1 } - }() - for { - 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 - } - } + return 0 + }) } -func runAlpineSetupCmd(sc *ssh.Client, pkgs []string) error { +func runAlpineSetup(sc *ssh.Client, pkgs []string) error { sess, err := sc.NewSession() if err != nil { 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, " ") } + //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'` err = sess.Run(cmd) diff --git a/storage/download.go b/storage/download.go index a4913c1..31f1bd9 100644 --- a/storage/download.go +++ b/storage/download.go @@ -27,10 +27,8 @@ func (s *Storage) download(url string, hash []byte, out string, applyReaderMiddl _, err := os.Stat(out) if err == nil { return errors.Wrap(err, "file already exists") - } else { - if !errors.Is(err, os.ErrNotExist) { - return errors.Wrap(err, "stat out path") - } + } else if !errors.Is(err, os.ErrNotExist) { + return errors.Wrap(err, "stat out path") } f, err := os.OpenFile(out, os.O_CREATE|os.O_WRONLY, 0400) diff --git a/storage/hash.go b/storage/hash.go index bcb1a82..6414a69 100644 --- a/storage/hash.go +++ b/storage/hash.go @@ -38,7 +38,7 @@ func validateFileHash(path string, hash []byte) error { sum := h.Sum(nil) 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 diff --git a/storage/storage.go b/storage/storage.go index 7df8bf8..d161fac 100644 --- a/storage/storage.go +++ b/storage/storage.go @@ -3,7 +3,6 @@ package storage import ( "compress/bzip2" "fmt" - "io" "log/slog" "os" "path/filepath" @@ -69,33 +68,37 @@ func (s *Storage) GetAarch64EFIImagePath() string { 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() removed, err := checkExistsOrRemove(vmImagePath, overwrite) 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() 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() 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) buildCtx, err := imgbuilder.NewBuildContext(s.logger.With("subcaller", "imgbuilder"), baseImagePath, vmImagePath, showBuilderVMDisplay, biosPath) 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() - if err != nil { - return errors.Wrap(err, "do build") + exitCode := buildCtx.RunCLIBuild() + if exitCode != 0 { + return exitCode } err = os.Remove(baseImagePath) @@ -105,7 +108,7 @@ func (s *Storage) BuildVMImageWithInterruptHandler(showBuilderVMDisplay bool, ov s.logger.Info("Removed base image", "path", baseImagePath) } - return nil + return 0 } func (s *Storage) CheckVMImageExists() (string, error) { @@ -152,9 +155,7 @@ func (s *Storage) CheckDownloadAarch64EFIImage() (string, error) { } // EFI image doesn't exist. Download one. - err := s.download(constants.GetAarch64EFIImageBZ2URL(), constants.GetAarch64EFIImageHash(), efiImagePath, func(r io.Reader) io.Reader { - return bzip2.NewReader(r) - }) + err := s.download(constants.GetAarch64EFIImageBZ2URL(), constants.GetAarch64EFIImageHash(), efiImagePath, bzip2.NewReader) if err != nil { return "", errors.Wrap(err, "download base alpine image") }