Skip to content

defined a common interface for nativeimgutil & qemuimgutil #3650

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 5 additions & 12 deletions cmd/limactl/disk.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@ import (
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"

"github.com/lima-vm/lima/pkg/nativeimgutil"
"github.com/lima-vm/lima/pkg/qemu/imgutil"
"github.com/lima-vm/lima/pkg/imgutil/proxyimgutil"
"github.com/lima-vm/lima/pkg/store"
"github.com/lima-vm/lima/pkg/store/filenames"
)
Expand Down Expand Up @@ -113,11 +112,8 @@ func diskCreateAction(cmd *cobra.Command, args []string) error {

// qemu may not be available, use it only if needed.
dataDisk := filepath.Join(diskDir, filenames.DataDisk)
if format == "raw" {
err = nativeimgutil.CreateRawDisk(dataDisk, int(diskSize))
} else {
err = imgutil.CreateDisk(dataDisk, format, int(diskSize))
}
diskUtil, _ := proxyimgutil.NewProxyImageUtil()
err = diskUtil.CreateDisk(dataDisk, int(diskSize))
if err != nil {
rerr := os.RemoveAll(diskDir)
if rerr != nil {
Expand Down Expand Up @@ -410,11 +406,8 @@ func diskResizeAction(cmd *cobra.Command, args []string) error {

// qemu may not be available, use it only if needed.
dataDisk := filepath.Join(disk.Dir, filenames.DataDisk)
if disk.Format == "raw" {
err = nativeimgutil.ResizeRawDisk(dataDisk, int(diskSize))
} else {
err = imgutil.ResizeDisk(dataDisk, disk.Format, int(diskSize))
}
diskUtil, _ := proxyimgutil.NewProxyImageUtil()
err = diskUtil.ResizeDisk(dataDisk, int(diskSize))
if err != nil {
return fmt.Errorf("failed to resize disk %q: %w", diskName, err)
}
Expand Down
94 changes: 94 additions & 0 deletions pkg/imgutil/interface.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// SPDX-FileCopyrightText: Copyright The Lima Authors
// SPDX-License-Identifier: Apache-2.0

package imgutil

import (
"encoding/json"
"os"
)

// ImageDiskManager defines the common operations for disk image utilities.
type ImageDiskManager interface {
// CreateDisk creates a new disk image with the specified size.
CreateDisk(disk string, size int) error

// ResizeDisk resizes an existing disk image to the specified size.
ResizeDisk(disk string, size int) error

// ConvertToRaw converts a disk image to raw format.
ConvertToRaw(source, dest string, size *int64, allowSourceWithBackingFile bool) error

// MakeSparse makes a file sparse, starting from the specified offset.
MakeSparse(f *os.File, offset int64) error
}

type InfoChild struct {
Name string `json:"name,omitempty"` // since QEMU 8.0
Info Info `json:"info,omitempty"` // since QEMU 8.0
}

type InfoFormatSpecific struct {
Type string `json:"type,omitempty"` // since QEMU 1.7
Data json.RawMessage `json:"data,omitempty"` // since QEMU 1.7
}

func (sp *InfoFormatSpecific) Qcow2() *InfoFormatSpecificDataQcow2 {
if sp.Type != "qcow2" {
return nil
}
var x InfoFormatSpecificDataQcow2
if err := json.Unmarshal(sp.Data, &x); err != nil {
panic(err)
}
return &x
}

func (sp *InfoFormatSpecific) Vmdk() *InfoFormatSpecificDataVmdk {
if sp.Type != "vmdk" {
return nil
}
var x InfoFormatSpecificDataVmdk
if err := json.Unmarshal(sp.Data, &x); err != nil {
panic(err)
}
return &x
}

type InfoFormatSpecificDataQcow2 struct {
Compat string `json:"compat,omitempty"` // since QEMU 1.7
LazyRefcounts bool `json:"lazy-refcounts,omitempty"` // since QEMU 1.7
Corrupt bool `json:"corrupt,omitempty"` // since QEMU 2.2
RefcountBits int `json:"refcount-bits,omitempty"` // since QEMU 2.3
CompressionType string `json:"compression-type,omitempty"` // since QEMU 5.1
ExtendedL2 bool `json:"extended-l2,omitempty"` // since QEMU 5.2
}

type InfoFormatSpecificDataVmdk struct {
CreateType string `json:"create-type,omitempty"` // since QEMU 1.7
CID int `json:"cid,omitempty"` // since QEMU 1.7
ParentCID int `json:"parent-cid,omitempty"` // since QEMU 1.7
Extents []InfoFormatSpecificDataVmdkExtent `json:"extents,omitempty"` // since QEMU 1.7
}

type InfoFormatSpecificDataVmdkExtent struct {
Filename string `json:"filename,omitempty"` // since QEMU 1.7
Format string `json:"format,omitempty"` // since QEMU 1.7
VSize int64 `json:"virtual-size,omitempty"` // since QEMU 1.7
ClusterSize int `json:"cluster-size,omitempty"` // since QEMU 1.7
}

// Info corresponds to the output of `qemu-img info --output=json FILE`.
type Info struct {
Filename string `json:"filename,omitempty"` // since QEMU 1.3
Format string `json:"format,omitempty"` // since QEMU 1.3
VSize int64 `json:"virtual-size,omitempty"` // since QEMU 1.3
ActualSize int64 `json:"actual-size,omitempty"` // since QEMU 1.3
DirtyFlag bool `json:"dirty-flag,omitempty"` // since QEMU 5.2
ClusterSize int `json:"cluster-size,omitempty"` // since QEMU 1.3
BackingFilename string `json:"backing-filename,omitempty"` // since QEMU 1.3
FullBackingFilename string `json:"full-backing-filename,omitempty"` // since QEMU 1.3
BackingFilenameFormat string `json:"backing-filename-format,omitempty"` // since QEMU 1.3
FormatSpecific *InfoFormatSpecific `json:"format-specific,omitempty"` // since QEMU 1.7
Children []InfoChild `json:"children,omitempty"` // since QEMU 8.0
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@ func FuzzConvertToRaw(f *testing.F) {
destPath := filepath.Join(t.TempDir(), "dest.img")
err := os.WriteFile(srcPath, imgData, 0o600)
assert.NilError(t, err)
_ = ConvertToRaw(srcPath, destPath, &size, withBacking)
_ = convertToRaw(srcPath, destPath, &size, withBacking)
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,17 @@ import (
// aligned to 512 bytes.
const sectorSize = 512

// RoundUp rounds size up to sectorSize.
func RoundUp(size int) int {
// NativeImageUtil is the native implementation of the imgutil Interface.
type NativeImageUtil struct{}

// roundUp rounds size up to sectorSize.
func roundUp(size int) int {
sectors := (size + sectorSize - 1) / sectorSize
return sectors * sectorSize
}

// CreateRawDisk creates an empty raw data disk.
func CreateRawDisk(disk string, size int) error {
// createRawDisk creates an empty raw data disk.
func createRawDisk(disk string, size int) error {
if _, err := os.Stat(disk); err == nil || !errors.Is(err, fs.ErrNotExist) {
return err
}
Expand All @@ -44,20 +47,20 @@ func CreateRawDisk(disk string, size int) error {
return err
}
defer f.Close()
roundedSize := RoundUp(size)
roundedSize := roundUp(size)
return f.Truncate(int64(roundedSize))
}

// ResizeRawDisk resizes a raw data disk.
func ResizeRawDisk(disk string, size int) error {
roundedSize := RoundUp(size)
// resizeRawDisk resizes a raw data disk.
func resizeRawDisk(disk string, size int) error {
roundedSize := roundUp(size)
return os.Truncate(disk, int64(roundedSize))
}

// ConvertToRaw converts a source disk into a raw disk.
// convertToRaw converts a source disk into a raw disk.
// source and dest may be same.
// ConvertToRaw is a NOP if source == dest, and no resizing is needed.
func ConvertToRaw(source, dest string, size *int64, allowSourceWithBackingFile bool) error {
// convertToRaw is a NOP if source == dest, and no resizing is needed.
func convertToRaw(source, dest string, size *int64, allowSourceWithBackingFile bool) error {
srcF, err := os.Open(source)
if err != nil {
return err
Expand Down Expand Up @@ -106,7 +109,7 @@ func ConvertToRaw(source, dest string, size *int64, allowSourceWithBackingFile b
// Truncating before copy eliminates the seeks during copy and provide a
// hint to the file system that may minimize allocations and fragmentation
// of the file.
if err := MakeSparse(destTmpF, srcImg.Size()); err != nil {
if err := makeSparse(destTmpF, srcImg.Size()); err != nil {
return err
}

Expand All @@ -125,7 +128,7 @@ func ConvertToRaw(source, dest string, size *int64, allowSourceWithBackingFile b
// Resize
if size != nil {
logrus.Infof("Expanding to %s", units.BytesSize(float64(*size)))
if err = MakeSparse(destTmpF, *size); err != nil {
if err = makeSparse(destTmpF, *size); err != nil {
return err
}
}
Expand Down Expand Up @@ -153,7 +156,7 @@ func convertRawToRaw(source, dest string, size *int64) error {
if err != nil {
return err
}
if err = MakeSparse(destF, *size); err != nil {
if err = makeSparse(destF, *size); err != nil {
_ = destF.Close()
return err
}
Expand All @@ -162,7 +165,7 @@ func convertRawToRaw(source, dest string, size *int64) error {
return nil
}

func MakeSparse(f *os.File, n int64) error {
func makeSparse(f *os.File, n int64) error {
if _, err := f.Seek(n, io.SeekStart); err != nil {
return err
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ func TestRoundUp(t *testing.T) {
{123456789, 123457024},
}
for _, tc := range tests {
if RoundUp(tc.Size) != tc.Rounded {
if roundUp(tc.Size) != tc.Rounded {
t.Errorf("expected %d, got %d", tc.Rounded, tc.Size)
}
}
Expand Down Expand Up @@ -63,15 +63,15 @@ func TestConvertToRaw(t *testing.T) {
t.Run("qcow without backing file", func(t *testing.T) {
resultImage := filepath.Join(tmpDir, strings.ReplaceAll(strings.ReplaceAll(t.Name(), string(os.PathSeparator), "_"), "/", "_"))

err = ConvertToRaw(qcowImage.Name(), resultImage, nil, false)
err = convertToRaw(qcowImage.Name(), resultImage, nil, false)
assert.NilError(t, err)
assertFileEquals(t, rawImage.Name(), resultImage)
})

t.Run("qcow with backing file", func(t *testing.T) {
resultImage := filepath.Join(tmpDir, strings.ReplaceAll(strings.ReplaceAll(t.Name(), string(os.PathSeparator), "_"), "/", "_"))

err = ConvertToRaw(qcowImage.Name(), resultImage, nil, true)
err = convertToRaw(qcowImage.Name(), resultImage, nil, true)
assert.NilError(t, err)
assertFileEquals(t, rawImage.Name(), resultImage)
})
Expand All @@ -80,15 +80,15 @@ func TestConvertToRaw(t *testing.T) {
resultImage := filepath.Join(tmpDir, strings.ReplaceAll(strings.ReplaceAll(t.Name(), string(os.PathSeparator), "_"), "/", "_"))

size := int64(2_097_152) // 2mb
err = ConvertToRaw(qcowImage.Name(), resultImage, &size, false)
err = convertToRaw(qcowImage.Name(), resultImage, &size, false)
assert.NilError(t, err)
assertFileEquals(t, rawImageExtended.Name(), resultImage)
})

t.Run("raw", func(t *testing.T) {
resultImage := filepath.Join(tmpDir, strings.ReplaceAll(strings.ReplaceAll(t.Name(), string(os.PathSeparator), "_"), "/", "_"))

err = ConvertToRaw(rawImage.Name(), resultImage, nil, false)
err = convertToRaw(rawImage.Name(), resultImage, nil, false)
assert.NilError(t, err)
assertFileEquals(t, rawImage.Name(), resultImage)
})
Expand Down
32 changes: 32 additions & 0 deletions pkg/imgutil/nativeimgutil/util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// SPDX-FileCopyrightText: Copyright The Lima Authors
// SPDX-License-Identifier: Apache-2.0

package nativeimgutil

import (
"os"
)

// NewNativeImageUtil returns a new NativeImageUtil instance.
func NewNativeImageUtil() *NativeImageUtil {
return &NativeImageUtil{}
}

// CreateDisk creates a new raw disk image with the specified size.
func (n *NativeImageUtil) CreateDisk(disk string, size int) error {
return createRawDisk(disk, size)
}

// ResizeDisk resizes an existing raw disk image to the specified size.
func (n *NativeImageUtil) ResizeDisk(disk string, size int) error {
return resizeRawDisk(disk, size)
}

// ConvertToRaw converts a disk image to raw format.
func (n *NativeImageUtil) ConvertToRaw(source, dest string, size *int64, allowSourceWithBackingFile bool) error {
return convertToRaw(source, dest, size, allowSourceWithBackingFile)
}

func (n *NativeImageUtil) MakeSparse(f *os.File, offset int64) error {
return makeSparse(f, offset)
}
66 changes: 66 additions & 0 deletions pkg/imgutil/proxyimgutil/proxyimgutil.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// SPDX-FileCopyrightText: Copyright The Lima Authors
// SPDX-License-Identifier: Apache-2.0

package proxyimgutil

import (
"errors"
"os"
"os/exec"

"github.com/lima-vm/lima/pkg/imgutil"
"github.com/lima-vm/lima/pkg/imgutil/nativeimgutil"
"github.com/lima-vm/lima/pkg/imgutil/qemuimgutil"
)

type ProxyImageDiskManager struct {
qemu imgutil.ImageDiskManager
native imgutil.ImageDiskManager
}

func NewProxyImageUtil() (imgutil.ImageDiskManager, *qemuimgutil.QemuInfoProvider) {
return &ProxyImageDiskManager{
qemu: qemuimgutil.NewQemuImageUtil(),
native: nativeimgutil.NewNativeImageUtil(),
}, qemuimgutil.NewQemuInfoProvider()
}

func (p *ProxyImageDiskManager) CreateDisk(disk string, size int) error {
if err := p.qemu.CreateDisk(disk, size); err != nil {
if errors.Is(err, exec.ErrNotFound) {
return p.native.CreateDisk(disk, size)
}
return err
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When qemu.CreateDisk returns an error other than exec.ErrNotFound, the error should be returned immediately without falling back to native.CreateDisk?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same applies to other functions too

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exitError.ExitCode() == 127

How does this happen?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok heres the explanation , see in go exec.Command("non existent command")
This results in exec.ErrNotFound because Go cannot find the command itself. It never even starts running because the binary doesn't exist.

exec.Command("sh", "-c", "non existent command"):
Here, sh exists, but the command inside the shell (nonexistentcommand) doesn't exist. So, the shell runs but exits with exit code 127, which means "command not found inside the shell."

exec.ErrNotFound for missing executables.

Exit code 127 when the executable runs but the command inside it is missing.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is sh relevant here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used sh just as a common example to illustrate the behavior.

The 127 exit code is simply a valid, standardized way to indicate "command not found" it's a safe and predictable way to signal failure without causing unexpected behavior.

In this case, yeah the failure scenario I described doesn't really apply here. I included it more as an extra precaution.

return nil
}

func (p *ProxyImageDiskManager) ResizeDisk(disk string, size int) error {
if err := p.qemu.ResizeDisk(disk, size); err != nil {
if errors.Is(err, exec.ErrNotFound) {
return p.native.ResizeDisk(disk, size)
}
return err
}
return nil
}

func (p *ProxyImageDiskManager) ConvertToRaw(source, dest string, size *int64, allowSourceWithBackingFile bool) error {
if err := p.qemu.ConvertToRaw(source, dest, size, allowSourceWithBackingFile); err != nil {
if errors.Is(err, exec.ErrNotFound) {
return p.native.ConvertToRaw(source, dest, size, allowSourceWithBackingFile)
}
return err
}
return nil
}

func (p *ProxyImageDiskManager) MakeSparse(f *os.File, offset int64) error {
if err := p.qemu.MakeSparse(f, offset); err != nil {
if errors.Is(err, exec.ErrNotFound) {
return p.native.MakeSparse(f, offset)
}
return err
}
return nil
}
Loading
Loading