From 2fb4d08e03deaef972d99a482b255c49468419c1 Mon Sep 17 00:00:00 2001 From: Jan Dubois Date: Mon, 16 Feb 2026 13:58:56 -0800 Subject: [PATCH] Introduce stable disk/iso filenames with rename-based setup Replace the legacy basedisk/diffdisk naming with self-documenting filenames. Downloads land in the ephemeral "image" file, which EnsureDisk then renames to "disk" (non-ISO) or "iso" (ISO images). QEMU renames directly; the vz driver converts to a new "disk" file and removes the original. Existing instances are migrated by MigrateDiskLayout, which creates symlinks from the new names to the legacy files. Boot paths now check for the "iso" file via osutil.FileExists instead of calling iso9660util.IsISO9660 at startup. Signed-off-by: Jan Dubois --- pkg/driver/krunkit/krunkit_darwin_arm64.go | 2 +- pkg/driver/qemu/qemu.go | 86 +++++------ pkg/driver/vz/vm_darwin.go | 45 +++--- pkg/driver/wsl2/fs.go | 2 +- pkg/driver/wsl2/vm_windows.go | 2 +- pkg/driverutil/disk.go | 93 ++++++++---- pkg/driverutil/disk_test.go | 165 ++++++++++++++++----- pkg/instance/start.go | 50 +++---- pkg/limatype/filenames/filenames.go | 7 +- pkg/limayaml/defaults.go | 2 +- pkg/osutil/file.go | 16 ++ website/content/en/docs/dev/internals.md | 7 +- 12 files changed, 312 insertions(+), 165 deletions(-) create mode 100644 pkg/osutil/file.go 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