From a63030fd00475737473c2408855a382a84aab4f2 Mon Sep 17 00:00:00 2001 From: AlexSSD7 Date: Fri, 25 Aug 2023 16:54:58 +0100 Subject: [PATCH] Better logging + runVM impl --- cmd/ls.go | 80 ++++++++++------------------------------------- cmd/root.go | 5 +++ cmd/run.go | 34 ++++++++++++++++++++ cmd/utils.go | 74 +++++++++++++++++++++++++++++++++++++++++-- utils/utils.go | 10 ++++++ vm/filemanager.go | 47 ++++++++++++++++++++++++++-- vm/vm.go | 16 +++++++--- 7 files changed, 192 insertions(+), 74 deletions(-) create mode 100644 cmd/run.go diff --git a/cmd/ls.go b/cmd/ls.go index 5076060..8f147bc 100644 --- a/cmd/ls.go +++ b/cmd/ls.go @@ -1,14 +1,14 @@ package cmd import ( + "context" "fmt" + "log/slog" "os" "strconv" "strings" - "sync" "github.com/AlexSSD7/vldisk/vm" - "github.com/inconshreveable/log15" "github.com/spf13/cobra" ) @@ -18,72 +18,24 @@ var lsCmd = &cobra.Command{ // Short: "", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - doRootCheck() - - passthroughConfig := getDevicePassthroughConfig(args[0]) - - // TODO: We should download alpine image ourselves. - // TODO: ALSO, we need make it usable offline. We can't always download packages from the web. - - // TODO: CLI-friendly logging. - vi, err := vm.NewInstance(log15.New(), "alpine-img/alpine.qcow2", []vm.USBDevicePassthroughConfig{passthroughConfig}, true) - if err != nil { - fmt.Printf("Failed to create VM instance: %v.\n", err) - os.Exit(1) - } - - runErrCh := make(chan error, 1) - - var wg sync.WaitGroup - - wg.Add(1) - go func() { - defer wg.Done() - - err := vi.Run() - runErrCh <- err - }() - - fm := vm.NewFileManager(vi) - - for { - select { - case err := <-runErrCh: - fmt.Printf("Failed to run the VM: %v.\n", err) + runVM(args[0], func(ctx context.Context, i *vm.Instance, fm *vm.FileManager) { + lsblkOut, err := fm.Lsblk() + if err != nil { + slog.Error("Failed to list block devices in the VM", "error", err.Error()) os.Exit(1) - case <-vi.SSHUpNotifyChan(): - err := fm.Init() - if err != nil { - fmt.Printf("Failed to initialize file manager: %v.\n", err) - os.Exit(1) - } - - lsblkOut, err := fm.Lsblk() - if err != nil { - fmt.Printf("Failed to run list block devices in the VM: %v.\n", err) - os.Exit(1) - } - - fmt.Print(string(lsblkOut)) - - err = vi.Cancel() - if err != nil { - fmt.Printf("Failed to cancel VM context: %v.\n", err) - os.Exit(1) - } - - wg.Wait() - - return nil } - } + + fmt.Print(string(lsblkOut)) + }) + + return nil }, } func getDevicePassthroughConfig(val string) vm.USBDevicePassthroughConfig { valSplit := strings.Split(val, ":") if want, have := 2, len(valSplit); want != have { - fmt.Printf("Bad device passthrough syntax (wrong items split by ':' count: want %v, have %v).\n", want, have) + slog.Error("Bad device passthrough syntax", "error", fmt.Errorf("wrong items split by ':' count: want %v, have %v", want, have).Error()) os.Exit(1) } @@ -91,19 +43,19 @@ func getDevicePassthroughConfig(val string) vm.USBDevicePassthroughConfig { case "usb": usbValsSplit := strings.Split(valSplit[1], ",") if want, have := 2, len(usbValsSplit); want != have { - fmt.Printf("Bad USB device passthrough syntax (wrong args split by ',' count: want %v, have %v).\n", want, have) + slog.Error("Bad USB device passthrough syntax", "error", fmt.Errorf("wrong args split by ',' count: want %v, have %v", want, have).Error()) os.Exit(1) } usbBus, err := strconv.ParseUint(usbValsSplit[0], 10, 8) if err != nil { - fmt.Printf("Bad USB device bus number '%v' (%v).\n", usbValsSplit[0], err) + slog.Error("Bad USB device bus number", "value", usbValsSplit[0]) os.Exit(1) } usbPort, err := strconv.ParseUint(usbValsSplit[1], 10, 8) if err != nil { - fmt.Printf("Bad USB device port number '%v' (%v).\n", usbValsSplit[1], err) + slog.Error("Bad USB device port number", "value", usbValsSplit[1]) os.Exit(1) } @@ -112,7 +64,7 @@ func getDevicePassthroughConfig(val string) vm.USBDevicePassthroughConfig { HostPort: uint8(usbPort), } default: - fmt.Printf("Unknown device passthrough type '%v'.\n", valSplit[0]) + slog.Error("Unknown device passthrough type", "value", valSplit[0]) os.Exit(1) // This unreachable code is required to compile. return vm.USBDevicePassthroughConfig{} diff --git a/cmd/root.go b/cmd/root.go index 97525a9..49d9b64 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -3,6 +3,8 @@ package cmd import ( "os" + "log/slog" + "github.com/spf13/cobra" ) @@ -21,5 +23,8 @@ func Execute() { } func init() { + slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, nil))) + rootCmd.AddCommand(lsCmd) + rootCmd.AddCommand(runCmd) } diff --git a/cmd/run.go b/cmd/run.go new file mode 100644 index 0000000..6981d5d --- /dev/null +++ b/cmd/run.go @@ -0,0 +1,34 @@ +package cmd + +import ( + "context" + "fmt" + "log/slog" + + "github.com/AlexSSD7/vldisk/vm" + "github.com/spf13/cobra" +) + +var runCmd = &cobra.Command{ + Use: "run", + // TODO: Fill this + // Short: "", + Args: cobra.ExactArgs(3), + RunE: func(cmd *cobra.Command, args []string) error { + vmMountDevName := args[1] + fsType := args[2] + + runVM(args[0], func(ctx context.Context, i *vm.Instance, fm *vm.FileManager) { + err := fm.Mount(vmMountDevName, vm.MountOptions{FSType: fsType}) + if err != nil { + slog.Error("Failed to mount the disk inside the VM", "error", err) + return + } + + fmt.Println("Mounted! Now sleeping") + <-ctx.Done() + }) + + return nil + }, +} diff --git a/cmd/utils.go b/cmd/utils.go index 50deed5..2845be4 100644 --- a/cmd/utils.go +++ b/cmd/utils.go @@ -1,10 +1,14 @@ package cmd import ( - "fmt" + "context" "os" "os/user" + "sync" + "log/slog" + + "github.com/AlexSSD7/vldisk/vm" "github.com/pkg/errors" ) @@ -19,12 +23,76 @@ func checkIfRoot() (bool, error) { func doRootCheck() { ok, err := checkIfRoot() if err != nil { - fmt.Printf("Failed to check whether the command is ran by root: %v.\n", err) + slog.Error("Failed to check whether the command is ran by root", "error", err.Error()) os.Exit(1) } if !ok { - fmt.Printf("Root permissions are required.\n") + slog.Error("You must run this program as root") os.Exit(1) } } + +func runVM(passthroughArg string, fn func(context.Context, *vm.Instance, *vm.FileManager)) *vm.Instance { + doRootCheck() + + passthroughConfig := getDevicePassthroughConfig(passthroughArg) + + // TODO: Alpine image should be downloaded from somewhere. + vi, err := vm.NewInstance(slog.Default(), "alpine-img/alpine.qcow2", []vm.USBDevicePassthroughConfig{passthroughConfig}, true) + if err != nil { + slog.Error("Failed to create vm instance", "error", err.Error()) + os.Exit(1) + } + + runErrCh := make(chan error, 1) + var wg sync.WaitGroup + + ctx, ctxCancel := context.WithCancel(context.Background()) + + wg.Add(1) + go func() { + defer wg.Done() + + err := vi.Run() + ctxCancel() + runErrCh <- err + }() + + fm := vm.NewFileManager(vi) + + for { + select { + case err := <-runErrCh: + slog.Error("Failed to start the VM", "error", err.Error()) + os.Exit(1) + case <-vi.SSHUpNotifyChan(): + err := fm.Init() + if err != nil { + slog.Error("Failed to initialize File Manager", "error", err.Error()) + os.Exit(1) + } + + fn(ctx, vi, fm) + + err = vi.Cancel() + if err != nil { + slog.Error("Failed to cancel VM context", "error", err.Error()) + os.Exit(1) + } + + wg.Wait() + + select { + case err := <-runErrCh: + if err != nil { + slog.Error("Failed to run the VM", "error", err.Error()) + os.Exit(1) + } + default: + } + + return nil + } + } +} diff --git a/utils/utils.go b/utils/utils.go index d811c5f..3d837c2 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -1,6 +1,7 @@ package utils import ( + "regexp" "strings" "unicode" ) @@ -13,3 +14,12 @@ func ClearUnprintableChars(s string) string { return -1 }, s) } + +var devNameRegexp = regexp.MustCompile("^[0-9a-z_-]+$") + +func ValidateDevName(s string) bool { + // Allow mapped devices. + s = strings.TrimPrefix(s, "mapper/") + + return devNameRegexp.MatchString(s) +} diff --git a/vm/filemanager.go b/vm/filemanager.go index db3a95a..3a6f395 100644 --- a/vm/filemanager.go +++ b/vm/filemanager.go @@ -2,7 +2,11 @@ package vm import ( "bytes" + "fmt" + "path/filepath" + "github.com/AlexSSD7/vldisk/utils" + "github.com/alessio/shellescape" "github.com/pkg/errors" ) @@ -36,12 +40,12 @@ func (fm *FileManager) Init() error { } func (fm *FileManager) Lsblk() ([]byte, error) { - c, err := fm.vi.DialSSH() + sc, err := fm.vi.DialSSH() if err != nil { return nil, errors.Wrap(err, "dial vm ssh") } - sess, err := c.NewSession() + sess, err := sc.NewSession() if err != nil { return nil, errors.Wrap(err, "create new vm ssh session") } @@ -57,3 +61,42 @@ func (fm *FileManager) Lsblk() ([]byte, error) { return ret.Bytes(), nil } + +type MountOptions struct { + FSType string +} + +func (fm *FileManager) Mount(devName string, mo MountOptions) error { + if devName == "" { + return fmt.Errorf("device name is empty") + } + + // It does allow mapper/ prefix for mapped devices. + // This is to enable the support for LVM and LUKS. + if !utils.ValidateDevName(devName) { + return fmt.Errorf("bad device name") + } + + fullDevPath := filepath.Clean("/dev/" + devName) + + if mo.FSType == "" { + return fmt.Errorf("fs type is empty") + } + + sc, err := fm.vi.DialSSH() + if err != nil { + return errors.Wrap(err, "dial vm ssh") + } + + sess, err := sc.NewSession() + if err != nil { + return errors.Wrap(err, "create new vm ssh session") + } + + err = sess.Run("mount -t " + shellescape.Quote(mo.FSType) + " " + shellescape.Quote(fullDevPath) + " /mnt") + if err != nil { + return errors.Wrap(err, "run mount cmd") + } + + return nil +} diff --git a/vm/vm.go b/vm/vm.go index 2956454..09d3290 100644 --- a/vm/vm.go +++ b/vm/vm.go @@ -15,8 +15,9 @@ import ( "sync/atomic" "time" + "log/slog" + "github.com/alessio/shellescape" - "github.com/inconshreveable/log15" "github.com/phayes/freeport" "github.com/pkg/errors" "go.uber.org/multierr" @@ -29,7 +30,7 @@ type USBDevicePassthroughConfig struct { } type Instance struct { - logger log15.Logger + logger *slog.Logger ctx context.Context ctxCancel context.CancelFunc @@ -53,7 +54,7 @@ type Instance struct { canceled uint32 } -func NewInstance(logger log15.Logger, alpineImagePath string, usbDevices []USBDevicePassthroughConfig, debug bool) (*Instance, error) { +func NewInstance(logger *slog.Logger, alpineImagePath string, usbDevices []USBDevicePassthroughConfig, debug bool) (*Instance, error) { alpineImagePath = filepath.Clean(alpineImagePath) _, err := os.Stat(alpineImagePath) if err != nil { @@ -157,6 +158,8 @@ func (vi *Instance) Run() error { return } + vi.logger.Info("Setting the VM up") + sshSigner, err := vi.sshSetup() if err != nil { globalErrFn(errors.Wrap(err, "set up ssh")) @@ -192,7 +195,7 @@ func (vi *Instance) Run() error { // This is to notify everyone waiting for SSH to be up that it's ready to go. close(vi.sshReadyCh) - vi.logger.Info("SSH up, the VM ready for work") + vi.logger.Info("The VM is ready") }() _, err = vi.cmd.Process.Wait() @@ -209,8 +212,11 @@ func (vi *Instance) Run() error { combinedErr := multierr.Combine( append(globalErrs, errors.Wrap(cancelErr, "cancel on exit"))..., ) + if combinedErr != nil { + return fmt.Errorf("%w %v", combinedErr, getLogErrMsg(vi.stderrBuf.String())) + } - return fmt.Errorf("%w %v", combinedErr, getLogErrMsg(vi.stderrBuf.String())) + return nil } func (vi *Instance) Cancel() error {