Skip to content

Commit ab9fd04

Browse files
committed
defined a common interface for nativeimgutil & qemuimgutil
Signed-off-by: Horiodino <[email protected]>
1 parent dbf3cd9 commit ab9fd04

File tree

15 files changed

+968
-27
lines changed

15 files changed

+968
-27
lines changed

cmd/limactl/disk.go

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@ import (
1818
"github.com/sirupsen/logrus"
1919
"github.com/spf13/cobra"
2020

21-
"github.com/lima-vm/lima/pkg/nativeimgutil"
22-
"github.com/lima-vm/lima/pkg/qemu/imgutil"
21+
"github.com/lima-vm/lima/pkg/imgutil"
2322
"github.com/lima-vm/lima/pkg/store"
2423
"github.com/lima-vm/lima/pkg/store/filenames"
2524
)
@@ -113,11 +112,11 @@ func diskCreateAction(cmd *cobra.Command, args []string) error {
113112

114113
// qemu may not be available, use it only if needed.
115114
dataDisk := filepath.Join(diskDir, filenames.DataDisk)
116-
if format == "raw" {
117-
err = nativeimgutil.CreateRawDisk(dataDisk, int(diskSize))
118-
} else {
119-
err = imgutil.CreateDisk(dataDisk, format, int(diskSize))
115+
diskUtil, _, err := imgutil.NewImageUtil(format)
116+
if err != nil {
117+
return err
120118
}
119+
err = diskUtil.CreateDisk(dataDisk, int(diskSize))
121120
if err != nil {
122121
rerr := os.RemoveAll(diskDir)
123122
if rerr != nil {
@@ -410,11 +409,11 @@ func diskResizeAction(cmd *cobra.Command, args []string) error {
410409

411410
// qemu may not be available, use it only if needed.
412411
dataDisk := filepath.Join(disk.Dir, filenames.DataDisk)
413-
if disk.Format == "raw" {
414-
err = nativeimgutil.ResizeRawDisk(dataDisk, int(diskSize))
415-
} else {
416-
err = imgutil.ResizeDisk(dataDisk, disk.Format, int(diskSize))
412+
diskUtil, _, err := imgutil.NewImageUtil(disk.Format)
413+
if err != nil {
414+
return err
417415
}
416+
err = diskUtil.ResizeDisk(dataDisk, int(diskSize))
418417
if err != nil {
419418
return fmt.Errorf("failed to resize disk %q: %w", diskName, err)
420419
}

pkg/imgutil/factory.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// SPDX-FileCopyrightText: Copyright The Lima Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package imgutil
5+
6+
import (
7+
"github.com/lima-vm/lima/pkg/imgutil/proxyimgutil"
8+
)
9+
10+
// NewImageUtil returns an appropriate implementation of the Interface
11+
// based on system capabilities. If qemu-img is available, it returns
12+
// a QEMU-based implementation, otherwise it falls back to the native
13+
// implementation.
14+
func NewImageUtil(format string) (proxyimgutil.Interface, proxyimgutil.InfoProvider, error) {
15+
return proxyimgutil.NewProxyImageUtil(format)
16+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// SPDX-FileCopyrightText: Copyright The Lima Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package nativeimgutil
5+
6+
import (
7+
"os"
8+
"path/filepath"
9+
"testing"
10+
11+
"gotest.tools/v3/assert"
12+
)
13+
14+
func FuzzConvertToRaw(f *testing.F) {
15+
f.Fuzz(func(t *testing.T, imgData []byte, withBacking bool, size int64) {
16+
srcPath := filepath.Join(t.TempDir(), "src.img")
17+
destPath := filepath.Join(t.TempDir(), "dest.img")
18+
err := os.WriteFile(srcPath, imgData, 0o600)
19+
assert.NilError(t, err)
20+
_ = convertToRaw(srcPath, destPath, &size, withBacking)
21+
})
22+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// SPDX-FileCopyrightText: Copyright The Lima Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package nativeimgutil
5+
6+
import (
7+
"os"
8+
9+
imgutil "github.com/lima-vm/lima/pkg/imgutil/qemuimgutil"
10+
)
11+
12+
// NativeImageUtil is the native implementation of the proxyimgutil.Interface
13+
type NativeImageUtil struct{}
14+
15+
// NewNativeImageUtil returns a new NativeImageUtil instance
16+
func NewNativeImageUtil() *NativeImageUtil {
17+
return &NativeImageUtil{}
18+
}
19+
20+
// CreateDisk creates a new raw disk image with the specified size
21+
func (n *NativeImageUtil) CreateDisk(disk string, size int) error {
22+
return createRawDisk(disk, size)
23+
}
24+
25+
// ResizeDisk resizes an existing raw disk image to the specified size
26+
func (n *NativeImageUtil) ResizeDisk(disk string, size int) error {
27+
return resizeRawDisk(disk, size)
28+
}
29+
30+
// ConvertToRaw converts a disk image to raw format
31+
func (n *NativeImageUtil) ConvertToRaw(source, dest string, size *int64, allowSourceWithBackingFile bool) error {
32+
return convertToRaw(source, dest, size, allowSourceWithBackingFile)
33+
}
34+
35+
func (n *NativeImageUtil) MakeSparse(f *os.File, offset int64) error {
36+
return makeSparse(f, offset)
37+
}
38+
39+
// NativeInfoProvider is the native implementation of the proxyimgutil.InfoProvider
40+
type NativeInfoProvider struct{}
41+
42+
// NewNativeInfoProvider returns a new NativeInfoProvider instance
43+
func NewNativeInfoProvider() *NativeInfoProvider {
44+
return &NativeInfoProvider{}
45+
}
46+
47+
// Just a placeholder for the interface
48+
// GetInfo retrieves information about a disk image
49+
// This is a stub implementation as the native package doesn't provide this functionality
50+
func (n *NativeInfoProvider) GetInfo(path string) (*imgutil.Info, error) {
51+
return nil, nil
52+
}
53+
54+
// AcceptableAsBasedisk checks if a disk image is acceptable as a base disk
55+
// This is a stub implementation as the native package doesn't provide this functionality
56+
func (n *NativeInfoProvider) AcceptableAsBasedisk(info *imgutil.Info) error {
57+
return nil
58+
}
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
// SPDX-FileCopyrightText: Copyright The Lima Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
// Package nativeimgutil provides image utilities that do not depend on `qemu-img` binary.
5+
package nativeimgutil
6+
7+
import (
8+
"errors"
9+
"fmt"
10+
"io"
11+
"io/fs"
12+
"os"
13+
"path/filepath"
14+
15+
containerdfs "github.com/containerd/continuity/fs"
16+
"github.com/docker/go-units"
17+
"github.com/lima-vm/go-qcow2reader"
18+
"github.com/lima-vm/go-qcow2reader/convert"
19+
"github.com/lima-vm/go-qcow2reader/image/qcow2"
20+
"github.com/lima-vm/go-qcow2reader/image/raw"
21+
"github.com/sirupsen/logrus"
22+
23+
"github.com/lima-vm/lima/pkg/progressbar"
24+
)
25+
26+
// Disk image size must be aligned to sector size. Qemu block layer is rounding
27+
// up the size to 512 bytes. Apple virtualization framework reject disks not
28+
// aligned to 512 bytes.
29+
const sectorSize = 512
30+
31+
// roundUp rounds size up to sectorSize.
32+
func roundUp(size int) int {
33+
sectors := (size + sectorSize - 1) / sectorSize
34+
return sectors * sectorSize
35+
}
36+
37+
// createRawDisk creates an empty raw data disk.
38+
func createRawDisk(disk string, size int) error {
39+
if _, err := os.Stat(disk); err == nil || !errors.Is(err, fs.ErrNotExist) {
40+
return err
41+
}
42+
f, err := os.Create(disk)
43+
if err != nil {
44+
return err
45+
}
46+
defer f.Close()
47+
roundedSize := roundUp(size)
48+
return f.Truncate(int64(roundedSize))
49+
}
50+
51+
// resizeRawDisk resizes a raw data disk.
52+
func resizeRawDisk(disk string, size int) error {
53+
roundedSize := roundUp(size)
54+
return os.Truncate(disk, int64(roundedSize))
55+
}
56+
57+
// convertToRaw converts a source disk into a raw disk.
58+
// source and dest may be same.
59+
// convertToRaw is a NOP if source == dest, and no resizing is needed.
60+
func convertToRaw(source, dest string, size *int64, allowSourceWithBackingFile bool) error {
61+
srcF, err := os.Open(source)
62+
if err != nil {
63+
return err
64+
}
65+
defer srcF.Close()
66+
srcImg, err := qcow2reader.Open(srcF)
67+
if err != nil {
68+
return fmt.Errorf("failed to detect the format of %q: %w", source, err)
69+
}
70+
if size != nil && *size < srcImg.Size() {
71+
return fmt.Errorf("specified size %d is smaller than the original image size (%d) of %q", *size, srcImg.Size(), source)
72+
}
73+
logrus.Infof("Converting %q (%s) to a raw disk %q", source, srcImg.Type(), dest)
74+
switch t := srcImg.Type(); t {
75+
case raw.Type:
76+
if err = srcF.Close(); err != nil {
77+
return err
78+
}
79+
return convertRawToRaw(source, dest, size)
80+
case qcow2.Type:
81+
if !allowSourceWithBackingFile {
82+
q, ok := srcImg.(*qcow2.Qcow2)
83+
if !ok {
84+
return fmt.Errorf("unexpected qcow2 image %T", srcImg)
85+
}
86+
if q.BackingFile != "" {
87+
return fmt.Errorf("qcow2 image %q has an unexpected backing file: %q", source, q.BackingFile)
88+
}
89+
}
90+
default:
91+
logrus.Warnf("image %q has an unexpected format: %q", source, t)
92+
}
93+
if err = srcImg.Readable(); err != nil {
94+
return fmt.Errorf("image %q is not readable: %w", source, err)
95+
}
96+
97+
// Create a tmp file because source and dest can be same.
98+
destTmpF, err := os.CreateTemp(filepath.Dir(dest), filepath.Base(dest)+".lima-*.tmp")
99+
if err != nil {
100+
return err
101+
}
102+
destTmp := destTmpF.Name()
103+
defer os.RemoveAll(destTmp)
104+
defer destTmpF.Close()
105+
106+
// Truncating before copy eliminates the seeks during copy and provide a
107+
// hint to the file system that may minimize allocations and fragmentation
108+
// of the file.
109+
if err := makeSparse(destTmpF, srcImg.Size()); err != nil {
110+
return err
111+
}
112+
113+
// Copy
114+
bar, err := progressbar.New(srcImg.Size())
115+
if err != nil {
116+
return err
117+
}
118+
bar.Start()
119+
err = convert.Convert(destTmpF, srcImg, convert.Options{Progress: bar})
120+
bar.Finish()
121+
if err != nil {
122+
return fmt.Errorf("failed to convert image: %w", err)
123+
}
124+
125+
// Resize
126+
if size != nil {
127+
logrus.Infof("Expanding to %s", units.BytesSize(float64(*size)))
128+
if err = makeSparse(destTmpF, *size); err != nil {
129+
return err
130+
}
131+
}
132+
if err = destTmpF.Close(); err != nil {
133+
return err
134+
}
135+
136+
// Rename destTmp into dest
137+
if err = os.RemoveAll(dest); err != nil {
138+
return err
139+
}
140+
return os.Rename(destTmp, dest)
141+
}
142+
143+
func convertRawToRaw(source, dest string, size *int64) error {
144+
if source != dest {
145+
// continuity attempts clonefile
146+
if err := containerdfs.CopyFile(dest, source); err != nil {
147+
return fmt.Errorf("failed to copy %q into %q: %w", source, dest, err)
148+
}
149+
}
150+
if size != nil {
151+
logrus.Infof("Expanding to %s", units.BytesSize(float64(*size)))
152+
destF, err := os.OpenFile(dest, os.O_RDWR, 0o644)
153+
if err != nil {
154+
return err
155+
}
156+
if err = makeSparse(destF, *size); err != nil {
157+
_ = destF.Close()
158+
return err
159+
}
160+
return destF.Close()
161+
}
162+
return nil
163+
}
164+
165+
func makeSparse(f *os.File, n int64) error {
166+
if _, err := f.Seek(n, io.SeekStart); err != nil {
167+
return err
168+
}
169+
return f.Truncate(n)
170+
}

0 commit comments

Comments
 (0)