diff --git a/pkg/driver/krunkit/krunkit_darwin_arm64.go b/pkg/driver/krunkit/krunkit_darwin_arm64.go index ae97215d3b8..31cd7fffad5 100644 --- a/pkg/driver/krunkit/krunkit_darwin_arm64.go +++ b/pkg/driver/krunkit/krunkit_darwin_arm64.go @@ -45,7 +45,7 @@ func Cmdline(inst *limatype.Instance) (*exec.Cmd, error) { "--restful-uri", "none://", // First virtio-blk device is the boot disk - "--device", fmt.Sprintf("virtio-blk,path=%s,format=raw", filepath.Join(inst.Dir, filenames.DiffDisk)), + "--device", fmt.Sprintf("virtio-blk,path=%s,format=raw", filepath.Join(inst.Dir, filenames.Disk)), "--device", fmt.Sprintf("virtio-blk,path=%s", filepath.Join(inst.Dir, filenames.CIDataISO)), } diff --git a/pkg/driver/qemu/qemu.go b/pkg/driver/qemu/qemu.go index b08fc61ebff..beb07067a88 100644 --- a/pkg/driver/qemu/qemu.go +++ b/pkg/driver/qemu/qemu.go @@ -87,42 +87,46 @@ func minimumQemuVersion() (hardMin, softMin semver.Version) { return hardMin, softMin } -// EnsureDisk also ensures the kernel and the initrd. +// EnsureDisk creates the VM disk from the downloaded image. +// For ISO images, it renames the image to "iso" and creates an empty qcow2 disk. +// For non-ISO images, it validates and renames the image to "disk". func EnsureDisk(ctx context.Context, cfg Config) error { - diffDisk := filepath.Join(cfg.InstanceDir, filenames.DiffDisk) - if _, err := os.Stat(diffDisk); err == nil || !errors.Is(err, os.ErrNotExist) { - // disk is already ensured + diskPath := filepath.Join(cfg.InstanceDir, filenames.Disk) + if _, err := os.Stat(diskPath); err == nil || !errors.Is(err, os.ErrNotExist) { return err } - baseDisk := filepath.Join(cfg.InstanceDir, filenames.BaseDisk) - - diskSize, _ := units.RAMInBytes(*cfg.LimaYAML.Disk) - if diskSize == 0 { - return nil - } - isBaseDiskISO, err := iso9660util.IsISO9660(baseDisk) + imagePath := filepath.Join(cfg.InstanceDir, filenames.Image) + isISO, err := iso9660util.IsISO9660(imagePath) if err != nil { return err } - baseDiskInfo, err := qemuimgutil.GetInfo(ctx, baseDisk) + imageInfo, err := qemuimgutil.GetInfo(ctx, imagePath) if err != nil { - return fmt.Errorf("failed to get the information of base disk %q: %w", baseDisk, err) + return fmt.Errorf("failed to get the information of %q: %w", imagePath, err) } - if err = qemuimgutil.AcceptableAsBaseDisk(baseDiskInfo); err != nil { - return fmt.Errorf("file %q is not acceptable as the base disk: %w", baseDisk, err) + if err = qemuimgutil.AcceptableAsBaseDisk(imageInfo); err != nil { + return fmt.Errorf("file %q is not acceptable as a disk image: %w", imagePath, err) } - if baseDiskInfo.Format == "" { - return fmt.Errorf("failed to inspect the format of %q", baseDisk) + if imageInfo.Format == "" { + return fmt.Errorf("failed to inspect the format of %q", imagePath) } - args := []string{"create", "-f", "qcow2"} - if !isBaseDiskISO { - args = append(args, "-F", baseDiskInfo.Format, "-b", baseDisk) - } - args = append(args, diffDisk, strconv.Itoa(int(diskSize))) - cmd := exec.CommandContext(ctx, "qemu-img", args...) - if out, err := cmd.CombinedOutput(); err != nil { - return fmt.Errorf("failed to run %v: %q: %w", cmd.Args, string(out), err) + if isISO { + isoPath := filepath.Join(cfg.InstanceDir, filenames.ISO) + if err = os.Rename(imagePath, isoPath); err != nil { + return err + } + diskSize, _ := units.RAMInBytes(*cfg.LimaYAML.Disk) + args := []string{"create", "-f", "qcow2", diskPath, strconv.Itoa(int(diskSize))} + cmd := exec.CommandContext(ctx, "qemu-img", args...) + if out, err := cmd.CombinedOutput(); err != nil { + _ = os.Rename(isoPath, imagePath) + return fmt.Errorf("failed to run %v: %q: %w", cmd.Args, string(out), err) + } + } else { + if err = os.Rename(imagePath, diskPath); err != nil { + return err + } } return nil } @@ -152,8 +156,8 @@ func sendHmpCommand(cfg Config, cmd, tag string) (string, error) { } func execImgCommand(ctx context.Context, cfg Config, args ...string) (string, error) { - diffDisk := filepath.Join(cfg.InstanceDir, filenames.DiffDisk) - args = append(args, diffDisk) + diskPath := filepath.Join(cfg.InstanceDir, filenames.Disk) + args = append(args, diskPath) logrus.Debugf("Running qemu-img %v command", args) cmd := exec.CommandContext(ctx, "qemu-img", args...) b, err := cmd.Output() @@ -687,8 +691,8 @@ func Cmdline(ctx context.Context, cfg Config) (exe string, args []string, err er } // Disk - baseDisk := filepath.Join(cfg.InstanceDir, filenames.BaseDisk) - diffDisk := filepath.Join(cfg.InstanceDir, filenames.DiffDisk) + diskPath := filepath.Join(cfg.InstanceDir, filenames.Disk) + isoPath := filepath.Join(cfg.InstanceDir, filenames.ISO) extraDisks := []string{} for _, d := range y.AdditionalDisks { diskName := d.Name @@ -719,31 +723,15 @@ func Cmdline(ctx context.Context, cfg Config) (exe string, args []string, err er extraDisks = append(extraDisks, dataDisk) } - isBaseDiskCDROM, err := iso9660util.IsISO9660(baseDisk) - if err != nil { - return "", nil, err + if osutil.FileExists(diskPath) { + args = append(args, "-drive", fmt.Sprintf("file=%s,if=virtio,discard=on", diskPath)) } - if isBaseDiskCDROM { + if osutil.FileExists(isoPath) { args = appendArgsIfNoConflict(args, "-boot", "order=d,splash-time=0,menu=on") - args = append(args, "-drive", fmt.Sprintf("file=%s,format=raw,media=cdrom,readonly=on", baseDisk)) + args = append(args, "-drive", fmt.Sprintf("file=%s,format=raw,media=cdrom,readonly=on", isoPath)) } else { args = appendArgsIfNoConflict(args, "-boot", "order=c,splash-time=0,menu=on") } - if diskSize, _ := units.RAMInBytes(*cfg.LimaYAML.Disk); diskSize > 0 { - args = append(args, "-drive", fmt.Sprintf("file=%s,if=virtio,discard=on", diffDisk)) - } else if !isBaseDiskCDROM { - baseDiskInfo, err := qemuimgutil.GetInfo(ctx, baseDisk) - if err != nil { - return "", nil, fmt.Errorf("failed to get the information of %q: %w", baseDisk, err) - } - if err = qemuimgutil.AcceptableAsBaseDisk(baseDiskInfo); err != nil { - return "", nil, fmt.Errorf("file %q is not acceptable as the base disk: %w", baseDisk, err) - } - if baseDiskInfo.Format == "" { - return "", nil, fmt.Errorf("failed to inspect the format of %q", baseDisk) - } - args = append(args, "-drive", fmt.Sprintf("file=%s,format=%s,if=virtio,discard=on", baseDisk, baseDiskInfo.Format)) - } for _, extraDisk := range extraDisks { args = append(args, "-drive", fmt.Sprintf("file=%s,if=virtio,discard=on", extraDisk)) } diff --git a/pkg/driver/vz/vm_darwin.go b/pkg/driver/vz/vm_darwin.go index fc7bafdf33d..13a3bca96f6 100644 --- a/pkg/driver/vz/vm_darwin.go +++ b/pkg/driver/vz/vm_darwin.go @@ -30,7 +30,6 @@ import ( "github.com/lima-vm/lima/v2/pkg/hostagent/events" "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/limayaml" @@ -486,41 +485,39 @@ func validateDiskFormat(diskPath string) error { } func attachDisks(ctx context.Context, inst *limatype.Instance, vmConfig *vz.VirtualMachineConfiguration) error { - baseDiskPath := filepath.Join(inst.Dir, filenames.BaseDisk) - diffDiskPath := filepath.Join(inst.Dir, filenames.DiffDisk) + diskPath := filepath.Join(inst.Dir, filenames.Disk) + isoPath := filepath.Join(inst.Dir, filenames.ISO) ciDataPath := filepath.Join(inst.Dir, filenames.CIDataISO) - isBaseDiskCDROM, err := iso9660util.IsISO9660(baseDiskPath) - if err != nil { - return err - } var configurations []vz.StorageDeviceConfiguration - if isBaseDiskCDROM { - if err = validateDiskFormat(baseDiskPath); err != nil { + if osutil.FileExists(diskPath) { + if err := validateDiskFormat(diskPath); err != nil { return err } - baseDiskAttachment, err := vz.NewDiskImageStorageDeviceAttachment(baseDiskPath, true) + diskAttachment, err := vz.NewDiskImageStorageDeviceAttachmentWithCacheAndSync(diskPath, false, diskImageCachingMode, vz.DiskImageSynchronizationModeFsync) if err != nil { return err } - baseDisk, err := vz.NewUSBMassStorageDeviceConfiguration(baseDiskAttachment) + diskDev, err := vz.NewVirtioBlockDeviceConfiguration(diskAttachment) if err != nil { return err } - configurations = append(configurations, baseDisk) - } - if err = validateDiskFormat(diffDiskPath); err != nil { - return err + configurations = append(configurations, diskDev) } - diffDiskAttachment, err := vz.NewDiskImageStorageDeviceAttachmentWithCacheAndSync(diffDiskPath, false, diskImageCachingMode, vz.DiskImageSynchronizationModeFsync) - if err != nil { - return err - } - diffDisk, err := vz.NewVirtioBlockDeviceConfiguration(diffDiskAttachment) - if err != nil { - return err + if osutil.FileExists(isoPath) { + if err := validateDiskFormat(isoPath); err != nil { + return err + } + isoAttachment, err := vz.NewDiskImageStorageDeviceAttachment(isoPath, true) + if err != nil { + return err + } + isoDev, err := vz.NewUSBMassStorageDeviceConfiguration(isoAttachment) + if err != nil { + return err + } + configurations = append(configurations, isoDev) } - configurations = append(configurations, diffDisk) diskUtil := proxyimgutil.NewDiskUtil(ctx) @@ -557,7 +554,7 @@ func attachDisks(ctx context.Context, inst *limatype.Instance, vmConfig *vz.Virt configurations = append(configurations, extraDisk) } - if err = validateDiskFormat(ciDataPath); err != nil { + if err := validateDiskFormat(ciDataPath); err != nil { return err } ciDataAttachment, err := vz.NewDiskImageStorageDeviceAttachment(ciDataPath, true) diff --git a/pkg/driver/wsl2/fs.go b/pkg/driver/wsl2/fs.go index b247b29d663..953bde5fe9e 100644 --- a/pkg/driver/wsl2/fs.go +++ b/pkg/driver/wsl2/fs.go @@ -18,7 +18,7 @@ import ( // EnsureFs downloads the root fs. func EnsureFs(ctx context.Context, inst *limatype.Instance) error { - baseDisk := filepath.Join(inst.Dir, filenames.BaseDisk) + baseDisk := filepath.Join(inst.Dir, filenames.BaseDiskLegacy) if _, err := os.Stat(baseDisk); errors.Is(err, os.ErrNotExist) { var ensuredBaseDisk bool errs := make([]error, len(inst.Config.Images)) diff --git a/pkg/driver/wsl2/vm_windows.go b/pkg/driver/wsl2/vm_windows.go index 19ca85fa737..7069df96ed7 100644 --- a/pkg/driver/wsl2/vm_windows.go +++ b/pkg/driver/wsl2/vm_windows.go @@ -39,7 +39,7 @@ func startVM(ctx context.Context, distroName string) error { // initVM calls WSL to import a new VM specifically for Lima. func initVM(ctx context.Context, instanceDir, distroName string) error { - baseDisk := filepath.Join(instanceDir, filenames.BaseDisk) + baseDisk := filepath.Join(instanceDir, filenames.BaseDiskLegacy) logrus.Infof("Importing distro from %q to %q", baseDisk, instanceDir) out, err := executil.RunUTF16leCommand([]string{ "wsl.exe", diff --git a/pkg/driverutil/disk.go b/pkg/driverutil/disk.go index 7ccd5644c11..55a6da6c48d 100644 --- a/pkg/driverutil/disk.go +++ b/pkg/driverutil/disk.go @@ -12,50 +12,93 @@ import ( "github.com/docker/go-units" "github.com/lima-vm/go-qcow2reader/image" + "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/filenames" + "github.com/lima-vm/lima/v2/pkg/osutil" ) -// EnsureDisk ensures that the diff disk exists with the specified size and format. -func EnsureDisk(ctx context.Context, instDir, diskSize string, diskImageFormat image.Type) error { - diffDisk := filepath.Join(instDir, filenames.DiffDisk) - if _, err := os.Stat(diffDisk); err == nil || !errors.Is(err, os.ErrNotExist) { - // disk is already ensured - return err +// MigrateDiskLayout creates symlinks from the current filenames (disk, iso) to +// the legacy filenames (diffdisk, basedisk) used by older Lima versions. +// The original files are left in place so older Lima versions can still use them. +func MigrateDiskLayout(instDir string) error { + diskPath := filepath.Join(instDir, filenames.Disk) + if osutil.FileExists(diskPath) { + return nil // already migrated or new instance } - diskUtil := proxyimgutil.NewDiskUtil(ctx) + diffDiskPath := filepath.Join(instDir, filenames.DiffDiskLegacy) + if osutil.FileExists(diffDiskPath) { + logrus.Infof("Creating symlink %q -> %q", filenames.Disk, filenames.DiffDiskLegacy) + if err := os.Symlink(filenames.DiffDiskLegacy, diskPath); err != nil { + return fmt.Errorf("failed to symlink %q to %q: %w", filenames.Disk, filenames.DiffDiskLegacy, err) + } + } - baseDisk := filepath.Join(instDir, filenames.BaseDisk) + baseDiskPath := filepath.Join(instDir, filenames.BaseDiskLegacy) + isoPath := filepath.Join(instDir, filenames.ISO) + if osutil.FileExists(baseDiskPath) && !osutil.FileExists(isoPath) { + isISO, err := iso9660util.IsISO9660(baseDiskPath) + if err != nil { + return err + } + if isISO { + logrus.Infof("Creating symlink %q -> %q", filenames.ISO, filenames.BaseDiskLegacy) + if err := os.Symlink(filenames.BaseDiskLegacy, isoPath); err != nil { + return fmt.Errorf("failed to symlink %q to %q: %w", filenames.ISO, filenames.BaseDiskLegacy, err) + } + } + // Non-ISO basedisk is a legacy qcow2 backing file; leave it for QEMU to resolve. + } - diskSizeInBytes, _ := units.RAMInBytes(diskSize) - if diskSizeInBytes == 0 { - return nil + return nil +} + +// EnsureDisk creates the VM disk from the downloaded image. +// For ISO images, it renames the image to "iso" and creates an empty disk. +// For non-ISO images, it converts the image to "disk" and removes the original. +func EnsureDisk(ctx context.Context, instDir, diskSize string, diskImageFormat image.Type) error { + diskPath := filepath.Join(instDir, filenames.Disk) + if _, err := os.Stat(diskPath); err == nil || !errors.Is(err, os.ErrNotExist) { + return err } - isBaseDiskISO, err := iso9660util.IsISO9660(baseDisk) + + imagePath := filepath.Join(instDir, filenames.Image) + isISO, err := iso9660util.IsISO9660(imagePath) if err != nil { return err } - srcDisk := baseDisk - if isBaseDiskISO { - srcDisk = diffDisk - // Create an empty data volume for the diff disk - diffDiskF, err := os.Create(diffDisk) + diskSizeInBytes, _ := units.RAMInBytes(diskSize) + diskUtil := proxyimgutil.NewDiskUtil(ctx) + + if isISO { + isoPath := filepath.Join(instDir, filenames.ISO) + if err := os.Rename(imagePath, isoPath); err != nil { + return err + } + f, err := os.Create(diskPath) if err != nil { + _ = os.Rename(isoPath, imagePath) return err } - - if err = diffDiskF.Close(); err != nil { + if err := f.Close(); err != nil { + os.Remove(diskPath) + _ = os.Rename(isoPath, imagePath) return err } + if err := diskUtil.Convert(ctx, diskImageFormat, diskPath, diskPath, &diskSizeInBytes, false); err != nil { + os.Remove(diskPath) + _ = os.Rename(isoPath, imagePath) + return fmt.Errorf("failed to create disk %q: %w", diskPath, err) + } + } else { + if err := diskUtil.Convert(ctx, diskImageFormat, imagePath, diskPath, &diskSizeInBytes, false); err != nil { + return fmt.Errorf("failed to convert %q to %q: %w", imagePath, diskPath, err) + } + os.Remove(imagePath) } - // Check whether to use ASIF format - - if err = diskUtil.Convert(ctx, diskImageFormat, srcDisk, diffDisk, &diskSizeInBytes, false); err != nil { - return fmt.Errorf("failed to convert %q to a disk %q: %w", srcDisk, diffDisk, err) - } - return err + return nil } diff --git a/pkg/driverutil/disk_test.go b/pkg/driverutil/disk_test.go index 583feb19c62..6e4c5696175 100644 --- a/pkg/driverutil/disk_test.go +++ b/pkg/driverutil/disk_test.go @@ -60,12 +60,19 @@ func detectImageType(t *testing.T, path string) image.Type { return img.Type() } -func checkDisk(t *testing.T, diff string, expectedType image.Type) { +func checkDisk(t *testing.T, diskPath string, expectedType image.Type) { t.Helper() - fi, err := os.Stat(diff) + fi, err := os.Stat(diskPath) assert.NilError(t, err) assert.Assert(t, fi.Size() > 0) - assert.Equal(t, detectImageType(t, diff), expectedType) + assert.Equal(t, detectImageType(t, diskPath), expectedType) +} + +func assertSymlink(t *testing.T, path, expectedTarget string) { + t.Helper() + target, err := os.Readlink(path) + assert.NilError(t, err) + assert.Equal(t, target, expectedTarget) } func isMacOS26OrHigher() bool { @@ -79,16 +86,11 @@ func isMacOS26OrHigher() bool { return version.Major >= 26 } -func TestEnsureDisk_WithISOBaseImage(t *testing.T) { +func TestEnsureDisk_WithISOImage(t *testing.T) { instDir := t.TempDir() - base := filepath.Join(instDir, filenames.BaseDisk) - diff := filepath.Join(instDir, filenames.DiffDisk) - - writeMinimalISO(t, base) - isISO, err := iso9660util.IsISO9660(base) - assert.NilError(t, err) - assert.Assert(t, isISO) - baseHashBefore := sha256File(t, base) + imagePath := filepath.Join(instDir, filenames.Image) + diskPath := filepath.Join(instDir, filenames.Disk) + isoPath := filepath.Join(instDir, filenames.ISO) formats := []image.Type{typeRAW} if isMacOS26OrHigher() { @@ -96,25 +98,30 @@ func TestEnsureDisk_WithISOBaseImage(t *testing.T) { } for _, format := range formats { + writeMinimalISO(t, imagePath) + imageHashBefore := sha256File(t, imagePath) + assert.NilError(t, EnsureDisk(t.Context(), instDir, "2MiB", format)) - isISO, err = iso9660util.IsISO9660(base) + + // image should have been renamed to iso + assert.Assert(t, !osutil.FileExists(imagePath)) + assert.Equal(t, imageHashBefore, sha256File(t, isoPath)) + isISO, err := iso9660util.IsISO9660(isoPath) assert.NilError(t, err) assert.Assert(t, isISO) - assert.Equal(t, baseHashBefore, sha256File(t, base)) - checkDisk(t, diff, format) - assert.NilError(t, os.Remove(diff)) + + // disk should be a real file (empty data disk) + checkDisk(t, diskPath, format) + + assert.NilError(t, os.Remove(diskPath)) + assert.NilError(t, os.Remove(isoPath)) } } -func TestEnsureDisk_WithNonISOBaseImage(t *testing.T) { +func TestEnsureDisk_WithNonISOImage(t *testing.T) { instDir := t.TempDir() - base := filepath.Join(instDir, filenames.BaseDisk) - diff := filepath.Join(instDir, filenames.DiffDisk) - - writeNonISO(t, base) - isISO, err := iso9660util.IsISO9660(base) - assert.NilError(t, err) - assert.Assert(t, !isISO) + imagePath := filepath.Join(instDir, filenames.Image) + diskPath := filepath.Join(instDir, filenames.Disk) formats := []image.Type{typeRAW} if isMacOS26OrHigher() { @@ -122,18 +129,26 @@ func TestEnsureDisk_WithNonISOBaseImage(t *testing.T) { } for _, format := range formats { + writeNonISO(t, imagePath) + assert.NilError(t, EnsureDisk(t.Context(), instDir, "2MiB", format)) - checkDisk(t, diff, format) - assert.NilError(t, os.Remove(diff)) + + // image should have been consumed + assert.Assert(t, !osutil.FileExists(imagePath)) + + // disk should be the converted image + checkDisk(t, diskPath, format) + + assert.NilError(t, os.Remove(diskPath)) } } -func TestEnsureDisk_ExistingDiffDisk(t *testing.T) { +func TestEnsureDisk_ExistingDisk(t *testing.T) { instDir := t.TempDir() - base := filepath.Join(instDir, filenames.BaseDisk) - diff := filepath.Join(instDir, filenames.DiffDisk) + imagePath := filepath.Join(instDir, filenames.Image) + diskPath := filepath.Join(instDir, filenames.Disk) - writeNonISO(t, base) + writeNonISO(t, imagePath) formats := []image.Type{typeRAW} if isMacOS26OrHigher() { @@ -141,10 +156,92 @@ func TestEnsureDisk_ExistingDiffDisk(t *testing.T) { } for _, format := range formats { - assert.NilError(t, os.WriteFile(diff, []byte("preexisting"), 0o644)) - origHash := sha256File(t, diff) + assert.NilError(t, os.WriteFile(diskPath, []byte("preexisting"), 0o644)) + origHash := sha256File(t, diskPath) assert.NilError(t, EnsureDisk(t.Context(), instDir, "2MiB", format)) - assert.Equal(t, sha256File(t, diff), origHash) - assert.NilError(t, os.Remove(diff)) + assert.Equal(t, sha256File(t, diskPath), origHash) + assert.NilError(t, os.Remove(diskPath)) } } + +func TestMigrateDiskLayout_LegacyDiffDisk(t *testing.T) { + instDir := t.TempDir() + diffDiskPath := filepath.Join(instDir, filenames.DiffDiskLegacy) + + assert.NilError(t, os.WriteFile(diffDiskPath, []byte("legacy-disk"), 0o644)) + origHash := sha256File(t, diffDiskPath) + + assert.NilError(t, MigrateDiskLayout(instDir)) + + // disk should be a symlink to diffdisk + diskPath := filepath.Join(instDir, filenames.Disk) + assertSymlink(t, diskPath, filenames.DiffDiskLegacy) + assert.Equal(t, sha256File(t, diskPath), origHash) + + // diffdisk should still exist (untouched) + assert.Assert(t, osutil.FileExists(diffDiskPath)) +} + +func TestMigrateDiskLayout_LegacyISOBaseDisk(t *testing.T) { + instDir := t.TempDir() + baseDiskPath := filepath.Join(instDir, filenames.BaseDiskLegacy) + diffDiskPath := filepath.Join(instDir, filenames.DiffDiskLegacy) + + writeMinimalISO(t, baseDiskPath) + baseHash := sha256File(t, baseDiskPath) + assert.NilError(t, os.WriteFile(diffDiskPath, []byte("legacy-disk"), 0o644)) + diffHash := sha256File(t, diffDiskPath) + + assert.NilError(t, MigrateDiskLayout(instDir)) + + // disk should be a symlink to diffdisk + diskPath := filepath.Join(instDir, filenames.Disk) + assertSymlink(t, diskPath, filenames.DiffDiskLegacy) + assert.Equal(t, sha256File(t, diskPath), diffHash) + + // iso should be a symlink to basedisk + isoPath := filepath.Join(instDir, filenames.ISO) + assertSymlink(t, isoPath, filenames.BaseDiskLegacy) + assert.Equal(t, sha256File(t, isoPath), baseHash) + + // original files should still exist + assert.Assert(t, osutil.FileExists(diffDiskPath)) + assert.Assert(t, osutil.FileExists(baseDiskPath)) +} + +func TestMigrateDiskLayout_LegacyNonISOBaseDisk(t *testing.T) { + instDir := t.TempDir() + baseDiskPath := filepath.Join(instDir, filenames.BaseDiskLegacy) + diffDiskPath := filepath.Join(instDir, filenames.DiffDiskLegacy) + + writeNonISO(t, baseDiskPath) + baseHash := sha256File(t, baseDiskPath) + assert.NilError(t, os.WriteFile(diffDiskPath, []byte("legacy-disk"), 0o644)) + + assert.NilError(t, MigrateDiskLayout(instDir)) + + // disk should be a symlink to diffdisk + diskPath := filepath.Join(instDir, filenames.Disk) + assertSymlink(t, diskPath, filenames.DiffDiskLegacy) + + // non-ISO basedisk should remain unchanged (qcow2 backing file) + assert.Equal(t, sha256File(t, baseDiskPath), baseHash) + + // no iso symlink should be created + isoPath := filepath.Join(instDir, filenames.ISO) + _, err := os.Lstat(isoPath) + assert.Assert(t, os.IsNotExist(err)) +} + +func TestMigrateDiskLayout_AlreadyMigrated(t *testing.T) { + instDir := t.TempDir() + diskPath := filepath.Join(instDir, filenames.Disk) + + assert.NilError(t, os.WriteFile(diskPath, []byte("current-disk"), 0o644)) + origHash := sha256File(t, diskPath) + + assert.NilError(t, MigrateDiskLayout(instDir)) + + // disk should be unchanged + assert.Equal(t, sha256File(t, diskPath), origHash) +} diff --git a/pkg/instance/start.go b/pkg/instance/start.go index ac547c732e1..e327501b5ed 100644 --- a/pkg/instance/start.go +++ b/pkg/instance/start.go @@ -29,6 +29,8 @@ import ( "github.com/lima-vm/lima/v2/pkg/imgutil/proxyimgutil" "github.com/lima-vm/lima/v2/pkg/limatype" "github.com/lima-vm/lima/v2/pkg/limatype/filenames" + "github.com/lima-vm/lima/v2/pkg/limayaml" + "github.com/lima-vm/lima/v2/pkg/osutil" "github.com/lima-vm/lima/v2/pkg/registry" "github.com/lima-vm/lima/v2/pkg/store" "github.com/lima-vm/lima/v2/pkg/usrlocal" @@ -66,19 +68,23 @@ func Prepare(ctx context.Context, inst *limatype.Instance, guestAgent string) (* return nil, err } - // Check if the instance has been created (the base disk already exists) - baseDisk := filepath.Join(inst.Dir, filenames.BaseDisk) - _, err = os.Stat(baseDisk) - created := err == nil + // Migrate legacy disk layout (diffdisk → disk, ISO basedisk → iso) + if err := driverutil.MigrateDiskLayout(inst.Dir); err != nil { + return nil, err + } + created := limayaml.IsExistingInstanceDir(inst.Dir) + + imagePath := filepath.Join(inst.Dir, filenames.Image) + disk := filepath.Join(inst.Dir, filenames.Disk) kernel := filepath.Join(inst.Dir, filenames.Kernel) kernelCmdline := filepath.Join(inst.Dir, filenames.KernelCmdline) initrd := filepath.Join(inst.Dir, filenames.Initrd) - if _, err := os.Stat(baseDisk); errors.Is(err, os.ErrNotExist) { - var ensuredBaseDisk bool + if !osutil.FileExists(imagePath) && !osutil.FileExists(disk) { + var ensuredImage bool errs := make([]error, len(inst.Config.Images)) for i, f := range inst.Config.Images { - if _, err := fileutils.DownloadFile(ctx, baseDisk, f.File, true, "the image", *inst.Config.Arch); err != nil { + if _, err := fileutils.DownloadFile(ctx, imagePath, f.File, true, "the image", *inst.Config.Arch); err != nil { errs[i] = err continue } @@ -102,10 +108,10 @@ func Prepare(ctx context.Context, inst *limatype.Instance, guestAgent string) (* continue } } - ensuredBaseDisk = true + ensuredImage = true break } - if !ensuredBaseDisk { + if !ensuredImage { return nil, fileutils.Errors(errs) } } @@ -114,8 +120,8 @@ func Prepare(ctx context.Context, inst *limatype.Instance, guestAgent string) (* return nil, err } - // Ensure diffDisk size matches the store - if err := prepareDiffDisk(ctx, inst); err != nil { + // Ensure disk size matches the configured value + if err := prepareDisk(ctx, inst); err != nil { return nil, err } @@ -435,13 +441,12 @@ func ShowMessage(inst *limatype.Instance) error { return scanner.Err() } -// prepareDiffDisk checks the disk size difference between inst.Disk and yaml.Disk. -// If there is no diffDisk, return nil (the instance has not been initialized or started yet). -func prepareDiffDisk(ctx context.Context, inst *limatype.Instance) error { - diffDisk := filepath.Join(inst.Dir, filenames.DiffDisk) +// prepareDisk resizes the VM disk if its size differs from the configured size. +// Returns nil if the disk does not yet exist (instance not yet initialized). +func prepareDisk(ctx context.Context, inst *limatype.Instance) error { + disk := filepath.Join(inst.Dir, filenames.Disk) - // Handle the instance initialization - _, err := os.Stat(diffDisk) + _, err := os.Stat(disk) if err != nil { if os.IsNotExist(err) { return nil @@ -449,7 +454,7 @@ func prepareDiffDisk(ctx context.Context, inst *limatype.Instance) error { return err } - f, err := os.Open(diffDisk) + f, err := os.Open(disk) if err != nil { return err } @@ -470,15 +475,10 @@ func prepareDiffDisk(ctx context.Context, inst *limatype.Instance) error { if inst.Disk < diskSize { inst.Disk = diskSize - return errors.New("diffDisk: Shrinking is currently unavailable") + return errors.New("disk shrinking is not supported") } diskUtil := proxyimgutil.NewDiskUtil(ctx) - err = diskUtil.ResizeDisk(ctx, diffDisk, inst.Disk) - if err != nil { - return err - } - - return nil + return diskUtil.ResizeDisk(ctx, disk, inst.Disk) } diff --git a/pkg/limatype/filenames/filenames.go b/pkg/limatype/filenames/filenames.go index a8459d77888..218dd54331b 100644 --- a/pkg/limatype/filenames/filenames.go +++ b/pkg/limatype/filenames/filenames.go @@ -36,8 +36,11 @@ const ( CIDataISO = "cidata.iso" CIDataISODir = "cidata" CloudConfig = "cloud-config.yaml" - BaseDisk = "basedisk" - DiffDisk = "diffdisk" + Image = "image" // downloaded VM image; renamed to Disk or ISO during setup + Disk = "disk" // VM disk (or symlink to DiffDiskLegacy for migrated instances) + ISO = "iso" // optional CDROM image (or symlink to BaseDiskLegacy for migrated instances) + BaseDiskLegacy = "basedisk" // legacy name for Image; may remain as qcow2 backing file + DiffDiskLegacy = "diffdisk" // legacy name for Disk Kernel = "kernel" KernelCmdline = "kernel.cmdline" Initrd = "initrd" diff --git a/pkg/limayaml/defaults.go b/pkg/limayaml/defaults.go index 0902c6c3f8f..7916fabf7a4 100644 --- a/pkg/limayaml/defaults.go +++ b/pkg/limayaml/defaults.go @@ -992,7 +992,7 @@ func IsExistingInstanceDir(dir string) bool { // because the file is created during the initialization of the instance. for _, f := range []string{ filenames.HostAgentStdoutLog, filenames.HostAgentStderrLog, - filenames.VzIdentifier, filenames.BaseDisk, filenames.DiffDisk, filenames.CIDataISO, + filenames.VzIdentifier, filenames.Image, filenames.Disk, filenames.BaseDiskLegacy, filenames.DiffDiskLegacy, filenames.CIDataISO, } { file := filepath.Join(dir, f) if _, err := os.Lstat(file); !errors.Is(err, os.ErrNotExist) { diff --git a/pkg/osutil/file.go b/pkg/osutil/file.go new file mode 100644 index 00000000000..25cdb8503f9 --- /dev/null +++ b/pkg/osutil/file.go @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: Copyright The Lima Authors +// SPDX-License-Identifier: Apache-2.0 + +package osutil + +import ( + "errors" + "os" +) + +// FileExists reports whether path exists and is accessible. +// It returns true for any non-ErrNotExist stat result, including permission errors. +func FileExists(path string) bool { + _, err := os.Stat(path) + return !errors.Is(err, os.ErrNotExist) +} diff --git a/website/content/en/docs/dev/internals.md b/website/content/en/docs/dev/internals.md index 559f9ed75bd..047e78ef79d 100644 --- a/website/content/en/docs/dev/internals.md +++ b/website/content/en/docs/dev/internals.md @@ -44,8 +44,11 @@ Ansible: - `ansible-inventory.yaml`: the Ansible node inventory. See [ansible](#ansible). disk: -- `basedisk`: the base image -- `diffdisk`: the diff image (QCOW2) +- `image`: the downloaded VM image; renamed to `disk` or `iso` during setup +- `disk`: the VM disk (can be a symlink to legacy `diffdisk`) +- `iso`: optional CDROM image for ISO-based installations (can be a symlink to legacy `basedisk`) +- `basedisk`: legacy name for the downloaded image (pre-v2.1 instances; may remain as a qcow2 backing file) +- `diffdisk`: legacy name for `disk` (pre-v2.1 instances) kernel: - `kernel`: the kernel