Path: blob/master/pkg/imgutil/nativeimgutil/nativeimgutil.go
2621 views
// SPDX-FileCopyrightText: Copyright The Lima Authors1// SPDX-License-Identifier: Apache-2.023// Package nativeimgutil provides image utilities that do not depend on `qemu-img` binary.4package nativeimgutil56import (7"context"8"errors"9"fmt"10"io"11"io/fs"12"math"13"math/rand/v2"14"os"15"path/filepath"1617containerdfs "github.com/containerd/continuity/fs"18"github.com/docker/go-units"19"github.com/lima-vm/go-qcow2reader"20"github.com/lima-vm/go-qcow2reader/convert"21"github.com/lima-vm/go-qcow2reader/image"22"github.com/lima-vm/go-qcow2reader/image/asif"23"github.com/lima-vm/go-qcow2reader/image/qcow2"24"github.com/lima-vm/go-qcow2reader/image/raw"25"github.com/sirupsen/logrus"2627"github.com/lima-vm/lima/v2/pkg/imgutil/nativeimgutil/asifutil"28"github.com/lima-vm/lima/v2/pkg/progressbar"29)3031// Disk image size must be aligned to sector size. Qemu block layer is rounding32// up the size to 512 bytes. Apple virtualization framework reject disks not33// aligned to 512 bytes.34const sectorSize = 5123536// NativeImageUtil is the native implementation of the imgutil.ImageDiskManager.37type NativeImageUtil struct{}3839// roundUp rounds size up to sectorSize.40func roundUp(size int64) int64 {41sectors := (size + sectorSize - 1) / sectorSize42return sectors * sectorSize43}4445// convertTo converts a source disk into a raw or ASIF disk.46// source and dest may be same.47// convertTo is a NOP if source == dest, and no resizing is needed.48func convertTo(destType image.Type, source, dest string, size *int64, allowSourceWithBackingFile bool) error {49srcF, err := os.Open(source)50if err != nil {51return err52}53defer srcF.Close()54srcImg, err := qcow2reader.Open(srcF)55if err != nil {56return fmt.Errorf("failed to detect the format of %q: %w", source, err)57}58if size != nil && *size < srcImg.Size() {59return fmt.Errorf("specified size %d is smaller than the original image size (%d) of %q", *size, srcImg.Size(), source)60}61logrus.Infof("Converting %q (%s) to a %s disk %q", source, srcImg.Type(), destType, dest)62switch t := srcImg.Type(); t {63case raw.Type:64if destType == raw.Type {65if err = srcF.Close(); err != nil {66return err67}68return convertRawToRaw(source, dest, size)69}70case qcow2.Type:71if !allowSourceWithBackingFile {72q, ok := srcImg.(*qcow2.Qcow2)73if !ok {74return fmt.Errorf("unexpected qcow2 image %T", srcImg)75}76if q.BackingFile != "" {77return fmt.Errorf("qcow2 image %q has an unexpected backing file: %q", source, q.BackingFile)78}79}80case asif.Type:81if destType == asif.Type {82return convertASIFToASIF(source, dest, size)83}84return fmt.Errorf("conversion from ASIF to %q is not supported", destType)85default:86logrus.Warnf("image %q has an unexpected format: %q", source, t)87}88if err = srcImg.Readable(); err != nil {89return fmt.Errorf("image %q is not readable: %w", source, err)90}9192// Create a tmp file because source and dest can be same.93var (94destTmpF *os.File95destTmp string96attachedDevice string97)98switch destType {99case raw.Type:100destTmpF, err = os.CreateTemp(filepath.Dir(dest), filepath.Base(dest)+".lima-*.tmp")101destTmp = destTmpF.Name()102case asif.Type:103// destTmp != destTmpF.Name() because destTmpF is mounted ASIF device file.104randomBase := fmt.Sprintf("%s.lima-%d.tmp.asif", filepath.Base(dest), rand.UintN(math.MaxUint))105destTmp = filepath.Join(filepath.Dir(dest), randomBase)106// Since qcow2 image is smaller than expected size, we need to specify expected size to avoid resize later.107// Resizing ASIF image is not supported by qemu-img which recognizes ASIF format as raw.108var newSize int64109if size != nil {110newSize = *size111} else {112newSize = srcImg.Size()113}114attachedDevice, destTmpF, err = asifutil.NewAttachedASIF(destTmp, newSize)115default:116return fmt.Errorf("unsupported target image type: %q", destType)117}118if err != nil {119return err120}121defer os.RemoveAll(destTmp)122defer destTmpF.Close()123124// Truncating before copy eliminates the seeks during copy and provide a125// hint to the file system that may minimize allocations and fragmentation126// of the file.127if err := makeSparse(destTmpF, srcImg.Size()); err != nil {128return err129}130131// Copy132bar, err := progressbar.New(srcImg.Size())133if err != nil {134return err135}136bar.Start()137err = convert.Convert(destTmpF, srcImg, convert.Options{Progress: bar})138bar.Finish()139if err != nil {140return fmt.Errorf("failed to convert image: %w", err)141}142143// Resize144if size != nil {145logrus.Infof("Expanding to %s", units.BytesSize(float64(*size)))146if err = makeSparse(destTmpF, *size); err != nil {147return err148}149}150if err = destTmpF.Close(); err != nil {151return err152}153// Detach ASIF device154if destType == asif.Type {155err := asifutil.DetachASIF(attachedDevice)156if err != nil {157return fmt.Errorf("failed to detach ASIF image %q: %w", attachedDevice, err)158}159}160161// Rename destTmp into dest162if err = os.RemoveAll(dest); err != nil {163return err164}165return os.Rename(destTmp, dest)166}167168func convertRawToRaw(source, dest string, size *int64) error {169if source != dest {170// continuity attempts clonefile171if err := containerdfs.CopyFile(dest, source); err != nil {172return fmt.Errorf("failed to copy %q into %q: %w", source, dest, err)173}174if err := os.Chmod(dest, 0o644); err != nil {175return fmt.Errorf("failed to set permissions on %q: %w", dest, err)176}177}178if size != nil {179logrus.Infof("Expanding to %s", units.BytesSize(float64(*size)))180destF, err := os.OpenFile(dest, os.O_RDWR, 0o644)181if err != nil {182return err183}184if err = makeSparse(destF, *size); err != nil {185_ = destF.Close()186return err187}188return destF.Close()189}190return nil191}192193func convertASIFToASIF(source, dest string, size *int64) error {194if source != dest {195if err := containerdfs.CopyFile(dest, source); err != nil {196return fmt.Errorf("failed to copy %q into %q: %w", source, dest, err)197}198if err := os.Chmod(dest, 0o644); err != nil {199return fmt.Errorf("failed to set permissions on %q: %w", dest, err)200}201}202if size != nil {203logrus.Infof("Resizing to %s", units.BytesSize(float64(*size)))204if err := asifutil.ResizeASIF(dest, *size); err != nil {205return fmt.Errorf("failed to resize ASIF image %q: %w", dest, err)206}207}208return nil209}210211func makeSparse(f *os.File, offset int64) error {212if _, err := f.Seek(offset, io.SeekStart); err != nil {213return err214}215return f.Truncate(offset)216}217218// CreateDisk creates a new disk image with the specified size.219func (n *NativeImageUtil) CreateDisk(_ context.Context, disk string, size int64) error {220if _, err := os.Stat(disk); err == nil || !errors.Is(err, fs.ErrNotExist) {221return err222}223f, err := os.Create(disk)224if err != nil {225return err226}227defer f.Close()228roundedSize := roundUp(size)229return f.Truncate(roundedSize)230}231232// Convert converts a disk image to the specified format.233// Currently supported formats are raw.Type and asif.Type.234func (n *NativeImageUtil) Convert(_ context.Context, imageType image.Type, source, dest string, size *int64, allowSourceWithBackingFile bool) error {235return convertTo(imageType, source, dest, size, allowSourceWithBackingFile)236}237238// ResizeDisk resizes an existing disk image to the specified size.239func (n *NativeImageUtil) ResizeDisk(_ context.Context, disk string, size int64) error {240roundedSize := roundUp(size)241return os.Truncate(disk, roundedSize)242}243244// MakeSparse makes a file sparse, starting from the specified offset.245func (n *NativeImageUtil) MakeSparse(_ context.Context, f *os.File, offset int64) error {246return makeSparse(f, offset)247}248249250