diff --git a/go.mod b/go.mod index 62d739b..a541f26 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d github.com/alessio/shellescape v1.4.2 github.com/bramvdbogaerde/go-scp v1.2.1 + github.com/dustin/go-humanize v1.0.1 github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 github.com/pkg/errors v0.9.1 github.com/sethvargo/go-password v0.2.0 diff --git a/go.sum b/go.sum index b1e1739..e7aa11c 100644 --- a/go.sum +++ b/go.sum @@ -7,6 +7,8 @@ github.com/bramvdbogaerde/go-scp v1.2.1/go.mod h1:s4ZldBoRAOgUg8IrRP2Urmq5qqd2yP github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI= diff --git a/storage/storage.go b/storage/storage.go new file mode 100644 index 0000000..4a9c7d0 --- /dev/null +++ b/storage/storage.go @@ -0,0 +1,171 @@ +package storage + +import ( + "bytes" + "crypto/sha512" + "encoding/hex" + "fmt" + "io" + "log/slog" + "math" + "net/http" + "os" + "path/filepath" + + "github.com/dustin/go-humanize" + "github.com/pkg/errors" +) + +const imageURL = "http://localhost:8000/linsk-base.qcow2" + +var imageHash = [64]byte{96, 134, 26, 122, 43, 140, 212, 78, 44, 123, 103, 209, 21, 36, 81, 152, 9, 177, 47, 114, 225, 117, 64, 198, 50, 151, 71, 100, 1, 92, 106, 24, 224, 254, 157, 125, 188, 118, 84, 200, 47, 11, 215, 252, 100, 173, 64, 202, 132, 110, 15, 240, 234, 223, 56, 125, 94, 94, 179, 39, 193, 215, 41, 109} + +type Storage struct { + logger *slog.Logger + + path string +} + +func NewStorage(logger *slog.Logger, dataDir string) (*Storage, error) { + dataDir = filepath.Clean(dataDir) + + err := os.MkdirAll(dataDir, 0700) + if err != nil { + return nil, fmt.Errorf("mkdir all data dir") + } + + return &Storage{ + logger: logger, + + path: dataDir, + }, nil +} + +func (s *Storage) getLocalImagePath() string { + return filepath.Join(s.path, hex.EncodeToString(imageHash[:])+".qcow2") +} + +func (s *Storage) DownloadImage() error { + localImagePath := s.getLocalImagePath() + + var created, success bool + + defer func() { + if created && !success { + _ = os.Remove(localImagePath) + } + }() + + _, err := os.Stat(localImagePath) + if err == nil { + err = os.Remove(localImagePath) + if err != nil { + return errors.Wrap(err, "remove existing image") + } + } else { + if !errors.Is(err, os.ErrNotExist) { + return errors.Wrap(err, "stat local image path") + } + } + + f, err := os.OpenFile(localImagePath, os.O_CREATE|os.O_WRONLY, 0400) + if err != nil { + return errors.Wrap(err, "open file") + } + + created = true + + defer func() { _ = f.Close() }() + + resp, err := http.Get(imageURL) + if err != nil { + return errors.Wrap(err, "http get image") + } + + defer func() { _ = resp.Body.Close() }() + + _, err = copyWithProgress(f, resp.Body, 1024, resp.ContentLength, func(i int, f float64) { + s.logger.Info("Downloading image", "url", imageURL, "percent", math.Round(f*100*100)/100, "content-length", humanize.Bytes(uint64(resp.ContentLength))) + }) + if err != nil { + return errors.Wrap(err, "copy resp to file") + } + + err = s.ValidateImageHash() + if err != nil { + return errors.Wrap(err, "validate image hash") + } + + success = true + + return nil +} + +func (s *Storage) ValidateImageHash() error { + localImagePath := s.getLocalImagePath() + + f, err := os.OpenFile(localImagePath, os.O_RDONLY, 0400) + if err != nil { + return errors.Wrap(err, "open file") + } + + defer func() { _ = f.Close() }() + + h := sha512.New() + block := make([]byte, 1024) + for { + read, err := f.Read(block) + if read > 0 { + h.Write(block[:read]) + } + if err != nil { + if errors.Is(err, io.EOF) { + break + } + + return errors.Wrap(err, "read file block") + } + } + + sum := h.Sum(nil) + + if !bytes.Equal(sum, imageHash[:]) { + return fmt.Errorf("hash mismatch: want '%v', have '%v'", hex.EncodeToString(imageHash[:]), hex.EncodeToString(sum)) + } + + return nil +} + +func copyWithProgress(dst io.Writer, src io.Reader, blockSize int, length int64, report func(int, float64)) (int, error) { + block := make([]byte, blockSize) + + var progress int + + for { + read, err := src.Read(block) + if read > 0 { + written, err := dst.Write(block[:read]) + if err != nil { + return progress, errors.Wrap(err, "write") + } + progress += written + } + if err != nil { + if errors.Is(err, io.EOF) { + break + } + + return progress, errors.Wrap(err, "read") + } + + if progress%1000000 == 0 { + var percent float64 + if length != 0 { + percent = float64(progress) / float64(length) + } + report(progress, percent) + } + } + + return progress, nil +} diff --git a/vm/vm.go b/vm/vm.go index 8322529..5e15de6 100644 --- a/vm/vm.go +++ b/vm/vm.go @@ -144,7 +144,6 @@ func NewVM(logger *slog.Logger, cfg VMConfig) (*VM, error) { if !cfg.ShowDisplay { cmdArgs = append(cmdArgs, "-display", "none") - } else if runtime.GOARCH == "arm64" { // No video is configured by default in ARM. This will enable it. // TODO: This doesn't really work on arm64. It just shows a blank viewer. @@ -174,7 +173,7 @@ func NewVM(logger *slog.Logger, cfg VMConfig) (*VM, error) { } if len(cfg.PassthroughConfig.Block) != 0 { - logger.Warn("Detected raw block device passthrough, please note that it's YOUR responsibility to ensure that no device is mounted in your OS and the VM at the same time") + logger.Warn("Detected raw block device passthrough. Please note that it's YOUR responsibility to ensure that no device is mounted in your OS and the VM at the same time. Otherwise, you run serious risks. No further warnings will be issued.") } for _, dev := range cfg.PassthroughConfig.Block {