Skip to content
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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ require (
github.com/google/go-cmp v0.7.0
github.com/google/yamlfmt v0.20.0
github.com/invopop/jsonschema v0.13.0
github.com/lima-vm/go-qcow2reader v0.6.0
github.com/lima-vm/go-qcow2reader v0.7.0
github.com/lima-vm/sshocker v0.3.8 // gomodjail:unconfined
github.com/mattn/go-isatty v0.0.20
github.com/mattn/go-shellwords v1.0.12
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,8 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lima-vm/go-qcow2reader v0.6.0 h1:dNstUGQxEUPbmiiVnu/cek2x7scrHe2VJy5JseLLflo=
github.com/lima-vm/go-qcow2reader v0.6.0/go.mod h1:ay45SlGOzU+2Vc21g5/lmQgPn7Hmf0JpPhm8cuOK1FI=
github.com/lima-vm/go-qcow2reader v0.7.0 h1:p+t0U7aRyEl5cc7t5Z54XzbaYeLTy4s3bn+Wl6+j4iA=
github.com/lima-vm/go-qcow2reader v0.7.0/go.mod h1:ay45SlGOzU+2Vc21g5/lmQgPn7Hmf0JpPhm8cuOK1FI=
github.com/lima-vm/sshocker v0.3.8 h1:nnIaqi1G1hvWihm++53YBXeI8/x7CQNzhEswPC0M2E0=
github.com/lima-vm/sshocker v0.3.8/go.mod h1:sO9dTE0i+I0BHHVpyoKZWkPFtHtTkrqcfCkQguRMsx8=
github.com/linuxkit/virtsock v0.0.0-20220523201153-1a23e78aa7a2 h1:DZMFueDbfz6PNc1GwDRA8+6lBx1TB9UnxDQliCqR73Y=
Expand Down
45 changes: 43 additions & 2 deletions pkg/driver/vz/disk.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,17 @@ import (
"fmt"
"os"
"path/filepath"
"strconv"

"github.com/coreos/go-semver/semver"
"github.com/docker/go-units"
"github.com/sirupsen/logrus"

"github.com/lima-vm/lima/v2/pkg/imgutil/proxyimgutil"
"github.com/lima-vm/lima/v2/pkg/iso9660util"
"github.com/lima-vm/lima/v2/pkg/limatype"
"github.com/lima-vm/lima/v2/pkg/limatype/filenames"
"github.com/lima-vm/lima/v2/pkg/osutil"
)

func EnsureDisk(ctx context.Context, inst *limatype.Instance) error {
Expand Down Expand Up @@ -51,8 +55,45 @@ func EnsureDisk(ctx context.Context, inst *limatype.Instance) error {
}
return diffDiskF.Close()
}
if err = diskUtil.ConvertToRaw(ctx, baseDisk, diffDisk, &diskSize, false); err != nil {
return fmt.Errorf("failed to convert %q to a raw disk %q: %w", baseDisk, diffDisk, err)
// Check whether to use ASIF format
converter := diskUtil.ConvertToASIF
if !determineUseASIF() {
converter = diskUtil.ConvertToRaw
}
if err = converter(ctx, baseDisk, diffDisk, &diskSize, false); err != nil {
return fmt.Errorf("failed to convert %q to a disk %q: %w", baseDisk, diffDisk, err)
}
return err
}

func determineUseASIF() bool {
var useASIF bool
if macOSProductVersion, err := osutil.ProductVersion(); err != nil {
logrus.WithError(err).Warn("Failed to get macOS product version; using raw format instead of ASIF")
} else if macOSProductVersion.LessThan(*semver.New("26.0.0")) {
logrus.Infof("macOS version %q does not support ASIF format; using raw format instead", macOSProductVersion)
} else {
// TODO: change default to true,
// if the conversion from ASIF to raw while preserving sparsity is implemented,
// or if enough testing is done to confirm that interoperability issues won't happen with ASIF.
useASIF = false
// allow overriding via LIMA_VZ_ASIF environment variable
if envVar := os.Getenv("LIMA_VZ_ASIF"); envVar != "" {
if b, err := strconv.ParseBool(envVar); err != nil {
logrus.WithError(err).Warnf("invalid LIMA_VZ_ASIF value %q", envVar)
} else {
useASIF = b
uses := "ASIF"
if !useASIF {
uses = "raw"
}
logrus.Infof("LIMA_VZ_ASIF=%s; using %s format to diff disk", envVar, uses)
}
} else if useASIF {
logrus.Info("using ASIF format for the disk image")
} else {
logrus.Info("using raw format for the disk image")
}
}
return useASIF
}
8 changes: 6 additions & 2 deletions pkg/driver/vz/vm_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"os"
"path/filepath"
"runtime"
"slices"
"strconv"
"sync"
"syscall"
Expand All @@ -22,6 +23,8 @@ import (
"github.com/coreos/go-semver/semver"
"github.com/docker/go-units"
"github.com/lima-vm/go-qcow2reader"
"github.com/lima-vm/go-qcow2reader/image"
"github.com/lima-vm/go-qcow2reader/image/asif"
"github.com/lima-vm/go-qcow2reader/image/raw"
"github.com/sirupsen/logrus"

Expand Down Expand Up @@ -451,8 +454,9 @@ func validateDiskFormat(diskPath string) error {
if err != nil {
return fmt.Errorf("failed to detect the format of %q: %w", diskPath, err)
}
if t := img.Type(); t != raw.Type {
return fmt.Errorf("expected the format of %q to be %q, got %q", diskPath, raw.Type, t)
supportedDiskTypes := []image.Type{raw.Type, asif.Type}
if t := img.Type(); !slices.Contains(supportedDiskTypes, t) {
return fmt.Errorf("expected the format of %q to be one of %v, got %q", diskPath, supportedDiskTypes, t)
}
// TODO: ensure that the disk is formatted with GPT or ISO9660
return nil
Expand Down
3 changes: 3 additions & 0 deletions pkg/imgutil/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,7 @@ type ImageDiskManager interface {

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

// ConvertToASIF converts a disk image to ASIF format.
ConvertToASIF(ctx context.Context, source, dest string, size *int64, allowSourceWithBackingFile bool) error
}
51 changes: 51 additions & 0 deletions pkg/imgutil/nativeimgutil/asifutil/asif_darwin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// SPDX-FileCopyrightText: Copyright The Lima Authors
// SPDX-License-Identifier: Apache-2.0

package asifutil

import (
"context"
"fmt"
"os"
"os/exec"
"strings"
)

// NewAttachedASIF creates a new ASIF image file at the specified path with the given size
// and attaches it, returning the attached device path and an open file handle.
// The caller is responsible for detaching the ASIF image device when done.
func NewAttachedASIF(path string, size int64) (string, *os.File, error) {
createArgs := []string{"image", "create", "blank", "--fs", "none", "--format", "ASIF", "--size", fmt.Sprintf("%d", size), path}
if err := exec.CommandContext(context.Background(), "diskutil", createArgs...).Run(); err != nil {
return "", nil, fmt.Errorf("failed to create ASIF image %q: %w", path, err)
}
attachArgs := []string{"image", "attach", "--noMount", path}
out, err := exec.CommandContext(context.Background(), "diskutil", attachArgs...).Output()
if err != nil {
return "", nil, fmt.Errorf("failed to attach ASIF image %q: %w", path, err)
}
devicePath := strings.TrimSpace(string(out))
f, err := os.OpenFile(devicePath, os.O_RDWR, 0o644)
if err != nil {
_ = DetachASIF(devicePath)
return "", nil, fmt.Errorf("failed to open ASIF device %q: %w", devicePath, err)
}
return devicePath, f, err
}

// DetachASIF detaches the ASIF image device at the specified path.
func DetachASIF(devicePath string) error {
if output, err := exec.CommandContext(context.Background(), "hdiutil", "detach", devicePath).CombinedOutput(); err != nil {
return fmt.Errorf("failed to detach ASIF image %q: %w: %s", devicePath, err, output)
}
return nil
}

// ResizeASIF resizes the ASIF image at the specified path to the given size.
func ResizeASIF(path string, size int64) error {
resizeArgs := []string{"image", "resize", "--size", fmt.Sprintf("%d", size), path}
if output, err := exec.CommandContext(context.Background(), "diskutil", resizeArgs...).CombinedOutput(); err != nil {
return fmt.Errorf("failed to resize ASIF image %q: %w: %s", path, err, output)
}
return nil
}
25 changes: 25 additions & 0 deletions pkg/imgutil/nativeimgutil/asifutil/asif_others.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//go:build !darwin

// SPDX-FileCopyrightText: Copyright The Lima Authors
// SPDX-License-Identifier: Apache-2.0

package asifutil

import (
"errors"
"os"
)

var ErrASIFNotSupported = errors.New("ASIF is only supported on macOS")

func NewAttachedASIF(_ string, _ int64) (string, *os.File, error) {
return "", nil, ErrASIFNotSupported
}

func DetachASIF(_ string) error {
return ErrASIFNotSupported
}

func ResizeASIF(_ string, _ int64) error {
return ErrASIFNotSupported
}
2 changes: 1 addition & 1 deletion pkg/imgutil/nativeimgutil/fuzz_test.go
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)
_ = convertTo(imageRaw, srcPath, destPath, &size, withBacking)
})
}
79 changes: 71 additions & 8 deletions pkg/imgutil/nativeimgutil/nativeimgutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,21 @@ import (
"fmt"
"io"
"io/fs"
"math"
"math/rand/v2"
"os"
"path/filepath"

containerdfs "github.com/containerd/continuity/fs"
"github.com/docker/go-units"
"github.com/lima-vm/go-qcow2reader"
"github.com/lima-vm/go-qcow2reader/convert"
"github.com/lima-vm/go-qcow2reader/image/asif"
"github.com/lima-vm/go-qcow2reader/image/qcow2"
"github.com/lima-vm/go-qcow2reader/image/raw"
"github.com/sirupsen/logrus"

"github.com/lima-vm/lima/v2/pkg/imgutil/nativeimgutil/asifutil"
"github.com/lima-vm/lima/v2/pkg/progressbar"
)

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

// convertToRaw converts a source disk into a raw disk.
type targetImageType string

const (
imageRaw targetImageType = "raw"
imageASIF targetImageType = "ASIF"
)

// convertTo converts a source disk into a raw or ASIF 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 {
// convertTo is a NOP if source == dest, and no resizing is needed.
func convertTo(destType targetImageType, source, dest string, size *int64, allowSourceWithBackingFile bool) error {
srcF, err := os.Open(source)
if err != nil {
return err
Expand All @@ -54,13 +65,15 @@ func convertToRaw(source, dest string, size *int64, allowSourceWithBackingFile b
if size != nil && *size < srcImg.Size() {
return fmt.Errorf("specified size %d is smaller than the original image size (%d) of %q", *size, srcImg.Size(), source)
}
logrus.Infof("Converting %q (%s) to a raw disk %q", source, srcImg.Type(), dest)
logrus.Infof("Converting %q (%s) to a %s disk %q", source, srcImg.Type(), destType, dest)
switch t := srcImg.Type(); t {
case raw.Type:
if err = srcF.Close(); err != nil {
return err
}
return convertRawToRaw(source, dest, size)
if destType == imageRaw {
return convertRawToRaw(source, dest, size)
}
case qcow2.Type:
if !allowSourceWithBackingFile {
q, ok := srcImg.(*qcow2.Qcow2)
Expand All @@ -71,6 +84,11 @@ func convertToRaw(source, dest string, size *int64, allowSourceWithBackingFile b
return fmt.Errorf("qcow2 image %q has an unexpected backing file: %q", source, q.BackingFile)
}
}
case asif.Type:
if destType == imageASIF {
return convertASIFToASIF(source, dest, size)
}
return fmt.Errorf("conversion from ASIF to %q is not supported", destType)
default:
logrus.Warnf("image %q has an unexpected format: %q", source, t)
}
Expand All @@ -79,11 +97,26 @@ func convertToRaw(source, dest string, size *int64, allowSourceWithBackingFile b
}

// Create a tmp file because source and dest can be same.
destTmpF, err := os.CreateTemp(filepath.Dir(dest), filepath.Base(dest)+".lima-*.tmp")
var (
destTmpF *os.File
destTmp string
attachedDevice string
)
switch destType {
case imageRaw:
destTmpF, err = os.CreateTemp(filepath.Dir(dest), filepath.Base(dest)+".lima-*.tmp")
destTmp = destTmpF.Name()
case imageASIF:
// destTmp != destTmpF.Name() because destTmpF is mounted ASIF device file.
randomBase := fmt.Sprintf("%s.lima-%d.tmp.asif", filepath.Base(dest), rand.UintN(math.MaxUint))
destTmp = filepath.Join(filepath.Dir(dest), randomBase)
attachedDevice, destTmpF, err = asifutil.NewAttachedASIF(destTmp, srcImg.Size())
default:
return fmt.Errorf("unsupported target image type: %q", destType)
}
if err != nil {
return err
}
destTmp := destTmpF.Name()
defer os.RemoveAll(destTmp)
defer destTmpF.Close()

Expand Down Expand Up @@ -116,6 +149,13 @@ func convertToRaw(source, dest string, size *int64, allowSourceWithBackingFile b
if err = destTmpF.Close(); err != nil {
return err
}
// Detach ASIF device
if destType == imageASIF {
err := asifutil.DetachASIF(attachedDevice)
if err != nil {
return fmt.Errorf("failed to detach ASIF image %q: %w", attachedDevice, err)
}
}

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

func convertASIFToASIF(source, dest string, size *int64) error {
if source != dest {
if err := containerdfs.CopyFile(dest, source); err != nil {
return fmt.Errorf("failed to copy %q into %q: %w", source, dest, err)
}
if err := os.Chmod(dest, 0o644); err != nil {
return fmt.Errorf("failed to set permissions on %q: %w", dest, err)
}
}
if size != nil {
logrus.Infof("Resizing to %s", units.BytesSize(float64(*size)))
if err := asifutil.ResizeASIF(dest, *size); err != nil {
return fmt.Errorf("failed to resize ASIF image %q: %w", dest, err)
}
}
return nil
}

func makeSparse(f *os.File, offset int64) error {
if _, err := f.Seek(offset, io.SeekStart); err != nil {
return err
Expand All @@ -172,7 +230,7 @@ func (n *NativeImageUtil) CreateDisk(_ context.Context, disk string, size int64)

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

// ResizeDisk resizes an existing disk image to the specified size.
Expand All @@ -185,3 +243,8 @@ func (n *NativeImageUtil) ResizeDisk(_ context.Context, disk string, size int64)
func (n *NativeImageUtil) MakeSparse(_ context.Context, f *os.File, offset int64) error {
return makeSparse(f, offset)
}

// ConvertToASIF converts a disk image to ASIF format.
func (n *NativeImageUtil) ConvertToASIF(_ context.Context, source, dest string, size *int64, allowSourceWithBackingFile bool) error {
return convertTo(imageASIF, source, dest, size, allowSourceWithBackingFile)
}
Loading
Loading