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

Merged
merged 1 commit into from
Jul 3, 2025
Merged
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.NewDiskUtil()
err = diskUtil.CreateDisk(dataDisk, 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.NewDiskUtil()
err = diskUtil.ResizeDisk(dataDisk, diskSize)
if err != nil {
return fmt.Errorf("failed to resize disk %q: %w", diskName, err)
}
Expand Down
23 changes: 23 additions & 0 deletions pkg/imgutil/manager.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// SPDX-FileCopyrightText: Copyright The Lima Authors
// SPDX-License-Identifier: Apache-2.0

package imgutil

import (
"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 int64) error

// ResizeDisk resizes an existing disk image to the specified size.
ResizeDisk(disk string, size int64) 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
}
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,36 +28,19 @@ 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.ImageDiskManager.
type NativeImageUtil struct{}

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

// 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
}
f, err := os.Create(disk)
if err != nil {
return err
}
defer f.Close()
roundedSize := RoundUp(size)
return f.Truncate(int64(roundedSize))
}

// 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 +89,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 +108,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 +136,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,9 +145,39 @@ func convertRawToRaw(source, dest string, size *int64) error {
return nil
}

func MakeSparse(f *os.File, n int64) error {
if _, err := f.Seek(n, io.SeekStart); err != nil {
func makeSparse(f *os.File, offset int64) error {
if _, err := f.Seek(offset, io.SeekStart); err != nil {
return err
}
return f.Truncate(n)
return f.Truncate(offset)
}

// CreateDisk creates a new disk image with the specified size.
func (n *NativeImageUtil) CreateDisk(disk string, size int64) error {
if _, err := os.Stat(disk); err == nil || !errors.Is(err, fs.ErrNotExist) {
return err
}
f, err := os.Create(disk)
if err != nil {
return err
}
defer f.Close()
roundedSize := roundUp(size)
return f.Truncate(int64(roundedSize))
}

// 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)
}

// ResizeDisk resizes an existing disk image to the specified size.
func (n *NativeImageUtil) ResizeDisk(disk string, size int64) error {
roundedSize := roundUp(size)
return os.Truncate(disk, roundedSize)
}

// MakeSparse makes a file sparse, starting from the specified offset.
func (n *NativeImageUtil) MakeSparse(f *os.File, offset int64) error {
return makeSparse(f, offset)
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ import (

func TestRoundUp(t *testing.T) {
tests := []struct {
Size int
Rounded int
Size int64
Rounded int64
}{
{0, 0},
{1, 512},
Expand All @@ -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
75 changes: 75 additions & 0 deletions pkg/imgutil/proxyimgutil/proxyimgutil.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// 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"
)

// ImageDiskManager is a proxy implementation of imgutil.ImageDiskManager that uses both QEMU and native image utilities.
type ImageDiskManager struct {
qemu imgutil.ImageDiskManager
native imgutil.ImageDiskManager
}

// NewDiskUtil returns a new instance of ImageDiskManager that uses both QEMU and native image utilities.
func NewDiskUtil() imgutil.ImageDiskManager {
return &ImageDiskManager{
qemu: &qemuimgutil.QemuImageUtil{DefaultFormat: qemuimgutil.QemuImgFormat},
native: &nativeimgutil.NativeImageUtil{},
}
}

// CreateDisk creates a new disk image with the specified size.
func (p *ImageDiskManager) CreateDisk(disk string, size int64) error {
err := p.qemu.CreateDisk(disk, size)
if err == nil {
return nil
}
if errors.Is(err, exec.ErrNotFound) {
return p.native.CreateDisk(disk, size)
}
return err
}

// ResizeDisk resizes an existing disk image to the specified size.
func (p *ImageDiskManager) ResizeDisk(disk string, size int64) error {
err := p.qemu.ResizeDisk(disk, size)
if err == nil {
return nil
}
if errors.Is(err, exec.ErrNotFound) {
return p.native.ResizeDisk(disk, size)
}
return err
}

// ConvertToRaw converts a disk image to raw format.
func (p *ImageDiskManager) ConvertToRaw(source, dest string, size *int64, allowSourceWithBackingFile bool) error {
err := p.qemu.ConvertToRaw(source, dest, size, allowSourceWithBackingFile)
if err == nil {
return nil
}
if errors.Is(err, exec.ErrNotFound) {
return p.native.ConvertToRaw(source, dest, size, allowSourceWithBackingFile)
}
return err
}

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