Skip to content

Commit a88474b

Browse files
committed
vz: Support ASIF on creating diffdisk
- Add `LIMA_VZ_ASIF` environment variable to use ASIF on creating diffdisk - pkg/imgutil: Add `ImageDiskManager.ConvertToASIF()` - Remove `hack/convert-diffdisk-to-asif.sh` Signed-off-by: Norio Nomura <norio.nomura@gmail.com>
1 parent a92ccaf commit a88474b

File tree

11 files changed

+216
-75
lines changed

11 files changed

+216
-75
lines changed

hack/convert-diffdisk-to-asif.sh

Lines changed: 0 additions & 60 deletions
This file was deleted.

pkg/driver/vz/disk.go

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,17 @@ import (
99
"fmt"
1010
"os"
1111
"path/filepath"
12+
"strconv"
1213

14+
"github.com/coreos/go-semver/semver"
1315
"github.com/docker/go-units"
16+
"github.com/sirupsen/logrus"
1417

1518
"github.com/lima-vm/lima/v2/pkg/imgutil/proxyimgutil"
1619
"github.com/lima-vm/lima/v2/pkg/iso9660util"
1720
"github.com/lima-vm/lima/v2/pkg/limatype"
1821
"github.com/lima-vm/lima/v2/pkg/limatype/filenames"
22+
"github.com/lima-vm/lima/v2/pkg/osutil"
1923
)
2024

2125
func EnsureDisk(ctx context.Context, inst *limatype.Instance) error {
@@ -51,8 +55,45 @@ func EnsureDisk(ctx context.Context, inst *limatype.Instance) error {
5155
}
5256
return diffDiskF.Close()
5357
}
54-
if err = diskUtil.ConvertToRaw(ctx, baseDisk, diffDisk, &diskSize, false); err != nil {
55-
return fmt.Errorf("failed to convert %q to a raw disk %q: %w", baseDisk, diffDisk, err)
58+
// Check whether to use ASIF format
59+
converter := diskUtil.ConvertToASIF
60+
if !determineUseASIF() {
61+
converter = diskUtil.ConvertToRaw
62+
}
63+
if err = converter(ctx, baseDisk, diffDisk, &diskSize, false); err != nil {
64+
return fmt.Errorf("failed to convert %q to a disk %q: %w", baseDisk, diffDisk, err)
5665
}
5766
return err
5867
}
68+
69+
func determineUseASIF() bool {
70+
var useASIF bool
71+
if macOSProductVersion, err := osutil.ProductVersion(); err != nil {
72+
logrus.WithError(err).Warn("Failed to get macOS product version; using raw format instead of ASIF")
73+
} else if macOSProductVersion.LessThan(*semver.New("26.0.0")) {
74+
logrus.Infof("macOS version %q does not support ASIF format; using raw format instead", macOSProductVersion)
75+
} else {
76+
// TODO: change default to true,
77+
// if the conversion from ASIF to raw while preserving sparsity is implemented,
78+
// or if enough testing is done to confirm that interoperability issues won't happen with ASIF.
79+
useASIF = false
80+
// allow overriding via LIMA_VZ_ASIF environment variable
81+
if envVar := os.Getenv("LIMA_VZ_ASIF"); envVar != "" {
82+
if b, err := strconv.ParseBool(envVar); err != nil {
83+
logrus.WithError(err).Warnf("invalid LIMA_VZ_ASIF value %q", envVar)
84+
} else {
85+
useASIF = b
86+
uses := "ASIF"
87+
if !useASIF {
88+
uses = "raw"
89+
}
90+
logrus.Infof("LIMA_VZ_ASIF=%s; using %s format to diff disk", envVar, uses)
91+
}
92+
} else if useASIF {
93+
logrus.Info("using ASIF format for the disk image")
94+
} else {
95+
logrus.Info("using raw format for the disk image")
96+
}
97+
}
98+
return useASIF
99+
}

pkg/imgutil/manager.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,7 @@ type ImageDiskManager interface {
2121

2222
// MakeSparse makes a file sparse, starting from the specified offset.
2323
MakeSparse(ctx context.Context, f *os.File, offset int64) error
24+
25+
// ConvertToASIF converts a disk image to ASIF format.
26+
ConvertToASIF(ctx context.Context, source, dest string, size *int64, allowSourceWithBackingFile bool) error
2427
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// SPDX-FileCopyrightText: Copyright The Lima Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package asifutil
5+
6+
import (
7+
"context"
8+
"fmt"
9+
"os"
10+
"os/exec"
11+
"strings"
12+
)
13+
14+
// NewAttachedASIF creates a new ASIF image file at the specified path with the given size
15+
// and attaches it, returning the attached device path and an open file handle.
16+
// The caller is responsible for detaching the ASIF image device when done.
17+
func NewAttachedASIF(path string, size int64) (string, *os.File, error) {
18+
createArgs := []string{"image", "create", "blank", "--fs", "none", "--format", "ASIF", "--size", fmt.Sprintf("%d", size), path}
19+
if err := exec.CommandContext(context.Background(), "diskutil", createArgs...).Run(); err != nil {
20+
return "", nil, fmt.Errorf("failed to create ASIF image %q: %w", path, err)
21+
}
22+
attachArgs := []string{"image", "attach", "--noMount", path}
23+
out, err := exec.CommandContext(context.Background(), "diskutil", attachArgs...).Output()
24+
if err != nil {
25+
return "", nil, fmt.Errorf("failed to attach ASIF image %q: %w", path, err)
26+
}
27+
devicePath := strings.TrimSpace(string(out))
28+
f, err := os.OpenFile(devicePath, os.O_RDWR, 0o644)
29+
if err != nil {
30+
_ = DetachASIF(devicePath)
31+
return "", nil, fmt.Errorf("failed to open ASIF device %q: %w", devicePath, err)
32+
}
33+
return devicePath, f, err
34+
}
35+
36+
// DetachASIF detaches the ASIF image device at the specified path.
37+
func DetachASIF(devicePath string) error {
38+
if output, err := exec.CommandContext(context.Background(), "hdiutil", "detach", devicePath).CombinedOutput(); err != nil {
39+
return fmt.Errorf("failed to detach ASIF image %q: %w: %s", devicePath, err, output)
40+
}
41+
return nil
42+
}
43+
44+
// ResizeASIF resizes the ASIF image at the specified path to the given size.
45+
func ResizeASIF(path string, size int64) error {
46+
resizeArgs := []string{"image", "resize", "--size", fmt.Sprintf("%d", size), path}
47+
if output, err := exec.CommandContext(context.Background(), "diskutil", resizeArgs...).CombinedOutput(); err != nil {
48+
return fmt.Errorf("failed to resize ASIF image %q: %w: %s", path, err, output)
49+
}
50+
return nil
51+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
//go:build !darwin
2+
3+
// SPDX-FileCopyrightText: Copyright The Lima Authors
4+
// SPDX-License-Identifier: Apache-2.0
5+
6+
package asifutil
7+
8+
import (
9+
"errors"
10+
"os"
11+
)
12+
13+
var ErrASIFNotSupported = errors.New("ASIF is only supported on macOS")
14+
15+
func NewAttachedASIF(_ string, _ int64) (string, *os.File, error) {
16+
return "", nil, ErrASIFNotSupported
17+
}
18+
19+
func DetachASIF(_ string) error {
20+
return ErrASIFNotSupported
21+
}
22+
23+
func ResizeASIF(_ string, _ int64) error {
24+
return ErrASIFNotSupported
25+
}

pkg/imgutil/nativeimgutil/fuzz_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,6 @@ func FuzzConvertToRaw(f *testing.F) {
1717
destPath := filepath.Join(t.TempDir(), "dest.img")
1818
err := os.WriteFile(srcPath, imgData, 0o600)
1919
assert.NilError(t, err)
20-
_ = convertToRaw(srcPath, destPath, &size, withBacking)
20+
_ = convertTo(imageRaw, srcPath, destPath, &size, withBacking)
2121
})
2222
}

pkg/imgutil/nativeimgutil/nativeimgutil.go

Lines changed: 71 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,21 @@ import (
1010
"fmt"
1111
"io"
1212
"io/fs"
13+
"math"
14+
"math/rand/v2"
1315
"os"
1416
"path/filepath"
1517

1618
containerdfs "github.com/containerd/continuity/fs"
1719
"github.com/docker/go-units"
1820
"github.com/lima-vm/go-qcow2reader"
1921
"github.com/lima-vm/go-qcow2reader/convert"
22+
"github.com/lima-vm/go-qcow2reader/image/asif"
2023
"github.com/lima-vm/go-qcow2reader/image/qcow2"
2124
"github.com/lima-vm/go-qcow2reader/image/raw"
2225
"github.com/sirupsen/logrus"
2326

27+
"github.com/lima-vm/lima/v2/pkg/imgutil/nativeimgutil/asifutil"
2428
"github.com/lima-vm/lima/v2/pkg/progressbar"
2529
)
2630

@@ -38,10 +42,17 @@ func roundUp(size int64) int64 {
3842
return sectors * sectorSize
3943
}
4044

41-
// convertToRaw converts a source disk into a raw disk.
45+
type targetImageType string
46+
47+
const (
48+
imageRaw targetImageType = "raw"
49+
imageASIF targetImageType = "ASIF"
50+
)
51+
52+
// convertTo converts a source disk into a raw or ASIF disk.
4253
// source and dest may be same.
43-
// convertToRaw is a NOP if source == dest, and no resizing is needed.
44-
func convertToRaw(source, dest string, size *int64, allowSourceWithBackingFile bool) error {
54+
// convertTo is a NOP if source == dest, and no resizing is needed.
55+
func convertTo(destType targetImageType, source, dest string, size *int64, allowSourceWithBackingFile bool) error {
4556
srcF, err := os.Open(source)
4657
if err != nil {
4758
return err
@@ -54,13 +65,15 @@ func convertToRaw(source, dest string, size *int64, allowSourceWithBackingFile b
5465
if size != nil && *size < srcImg.Size() {
5566
return fmt.Errorf("specified size %d is smaller than the original image size (%d) of %q", *size, srcImg.Size(), source)
5667
}
57-
logrus.Infof("Converting %q (%s) to a raw disk %q", source, srcImg.Type(), dest)
68+
logrus.Infof("Converting %q (%s) to a %s disk %q", source, srcImg.Type(), destType, dest)
5869
switch t := srcImg.Type(); t {
5970
case raw.Type:
6071
if err = srcF.Close(); err != nil {
6172
return err
6273
}
63-
return convertRawToRaw(source, dest, size)
74+
if destType == imageRaw {
75+
return convertRawToRaw(source, dest, size)
76+
}
6477
case qcow2.Type:
6578
if !allowSourceWithBackingFile {
6679
q, ok := srcImg.(*qcow2.Qcow2)
@@ -71,6 +84,11 @@ func convertToRaw(source, dest string, size *int64, allowSourceWithBackingFile b
7184
return fmt.Errorf("qcow2 image %q has an unexpected backing file: %q", source, q.BackingFile)
7285
}
7386
}
87+
case asif.Type:
88+
if destType == imageASIF {
89+
return convertASIFToASIF(source, dest, size)
90+
}
91+
return fmt.Errorf("conversion from ASIF to %q is not supported", destType)
7492
default:
7593
logrus.Warnf("image %q has an unexpected format: %q", source, t)
7694
}
@@ -79,11 +97,26 @@ func convertToRaw(source, dest string, size *int64, allowSourceWithBackingFile b
7997
}
8098

8199
// Create a tmp file because source and dest can be same.
82-
destTmpF, err := os.CreateTemp(filepath.Dir(dest), filepath.Base(dest)+".lima-*.tmp")
100+
var (
101+
destTmpF *os.File
102+
destTmp string
103+
attachedDevice string
104+
)
105+
switch destType {
106+
case imageRaw:
107+
destTmpF, err = os.CreateTemp(filepath.Dir(dest), filepath.Base(dest)+".lima-*.tmp")
108+
destTmp = destTmpF.Name()
109+
case imageASIF:
110+
// destTmp != destTmpF.Name() because destTmpF is mounted ASIF device file.
111+
randomBase := fmt.Sprintf("%s.lima-%d.tmp.asif", filepath.Base(dest), rand.UintN(math.MaxUint))
112+
destTmp = filepath.Join(filepath.Dir(dest), randomBase)
113+
attachedDevice, destTmpF, err = asifutil.NewAttachedASIF(destTmp, srcImg.Size())
114+
default:
115+
return fmt.Errorf("unsupported target image type: %q", destType)
116+
}
83117
if err != nil {
84118
return err
85119
}
86-
destTmp := destTmpF.Name()
87120
defer os.RemoveAll(destTmp)
88121
defer destTmpF.Close()
89122

@@ -116,6 +149,13 @@ func convertToRaw(source, dest string, size *int64, allowSourceWithBackingFile b
116149
if err = destTmpF.Close(); err != nil {
117150
return err
118151
}
152+
// Detach ASIF device
153+
if destType == imageASIF {
154+
err := asifutil.DetachASIF(attachedDevice)
155+
if err != nil {
156+
return fmt.Errorf("failed to detach ASIF image %q: %w", attachedDevice, err)
157+
}
158+
}
119159

120160
// Rename destTmp into dest
121161
if err = os.RemoveAll(dest); err != nil {
@@ -149,6 +189,24 @@ func convertRawToRaw(source, dest string, size *int64) error {
149189
return nil
150190
}
151191

192+
func convertASIFToASIF(source, dest string, size *int64) error {
193+
if source != dest {
194+
if err := containerdfs.CopyFile(dest, source); err != nil {
195+
return fmt.Errorf("failed to copy %q into %q: %w", source, dest, err)
196+
}
197+
if err := os.Chmod(dest, 0o644); err != nil {
198+
return fmt.Errorf("failed to set permissions on %q: %w", dest, err)
199+
}
200+
}
201+
if size != nil {
202+
logrus.Infof("Resizing to %s", units.BytesSize(float64(*size)))
203+
if err := asifutil.ResizeASIF(dest, *size); err != nil {
204+
return fmt.Errorf("failed to resize ASIF image %q: %w", dest, err)
205+
}
206+
}
207+
return nil
208+
}
209+
152210
func makeSparse(f *os.File, offset int64) error {
153211
if _, err := f.Seek(offset, io.SeekStart); err != nil {
154212
return err
@@ -172,7 +230,7 @@ func (n *NativeImageUtil) CreateDisk(_ context.Context, disk string, size int64)
172230

173231
// ConvertToRaw converts a disk image to raw format.
174232
func (n *NativeImageUtil) ConvertToRaw(_ context.Context, source, dest string, size *int64, allowSourceWithBackingFile bool) error {
175-
return convertToRaw(source, dest, size, allowSourceWithBackingFile)
233+
return convertTo(imageRaw, source, dest, size, allowSourceWithBackingFile)
176234
}
177235

178236
// ResizeDisk resizes an existing disk image to the specified size.
@@ -185,3 +243,8 @@ func (n *NativeImageUtil) ResizeDisk(_ context.Context, disk string, size int64)
185243
func (n *NativeImageUtil) MakeSparse(_ context.Context, f *os.File, offset int64) error {
186244
return makeSparse(f, offset)
187245
}
246+
247+
// ConvertToASIF converts a disk image to ASIF format.
248+
func (n *NativeImageUtil) ConvertToASIF(_ context.Context, source, dest string, size *int64, allowSourceWithBackingFile bool) error {
249+
return convertTo(imageASIF, source, dest, size, allowSourceWithBackingFile)
250+
}

0 commit comments

Comments
 (0)