Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
lima-vm
GitHub Repository: lima-vm/lima
Path: blob/master/pkg/imgutil/nativeimgutil/nativeimgutil.go
2621 views
1
// SPDX-FileCopyrightText: Copyright The Lima Authors
2
// SPDX-License-Identifier: Apache-2.0
3
4
// Package nativeimgutil provides image utilities that do not depend on `qemu-img` binary.
5
package nativeimgutil
6
7
import (
8
"context"
9
"errors"
10
"fmt"
11
"io"
12
"io/fs"
13
"math"
14
"math/rand/v2"
15
"os"
16
"path/filepath"
17
18
containerdfs "github.com/containerd/continuity/fs"
19
"github.com/docker/go-units"
20
"github.com/lima-vm/go-qcow2reader"
21
"github.com/lima-vm/go-qcow2reader/convert"
22
"github.com/lima-vm/go-qcow2reader/image"
23
"github.com/lima-vm/go-qcow2reader/image/asif"
24
"github.com/lima-vm/go-qcow2reader/image/qcow2"
25
"github.com/lima-vm/go-qcow2reader/image/raw"
26
"github.com/sirupsen/logrus"
27
28
"github.com/lima-vm/lima/v2/pkg/imgutil/nativeimgutil/asifutil"
29
"github.com/lima-vm/lima/v2/pkg/progressbar"
30
)
31
32
// Disk image size must be aligned to sector size. Qemu block layer is rounding
33
// up the size to 512 bytes. Apple virtualization framework reject disks not
34
// aligned to 512 bytes.
35
const sectorSize = 512
36
37
// NativeImageUtil is the native implementation of the imgutil.ImageDiskManager.
38
type NativeImageUtil struct{}
39
40
// roundUp rounds size up to sectorSize.
41
func roundUp(size int64) int64 {
42
sectors := (size + sectorSize - 1) / sectorSize
43
return sectors * sectorSize
44
}
45
46
// convertTo converts a source disk into a raw or ASIF disk.
47
// source and dest may be same.
48
// convertTo is a NOP if source == dest, and no resizing is needed.
49
func convertTo(destType image.Type, source, dest string, size *int64, allowSourceWithBackingFile bool) error {
50
srcF, err := os.Open(source)
51
if err != nil {
52
return err
53
}
54
defer srcF.Close()
55
srcImg, err := qcow2reader.Open(srcF)
56
if err != nil {
57
return fmt.Errorf("failed to detect the format of %q: %w", source, err)
58
}
59
if size != nil && *size < srcImg.Size() {
60
return fmt.Errorf("specified size %d is smaller than the original image size (%d) of %q", *size, srcImg.Size(), source)
61
}
62
logrus.Infof("Converting %q (%s) to a %s disk %q", source, srcImg.Type(), destType, dest)
63
switch t := srcImg.Type(); t {
64
case raw.Type:
65
if destType == raw.Type {
66
if err = srcF.Close(); err != nil {
67
return err
68
}
69
return convertRawToRaw(source, dest, size)
70
}
71
case qcow2.Type:
72
if !allowSourceWithBackingFile {
73
q, ok := srcImg.(*qcow2.Qcow2)
74
if !ok {
75
return fmt.Errorf("unexpected qcow2 image %T", srcImg)
76
}
77
if q.BackingFile != "" {
78
return fmt.Errorf("qcow2 image %q has an unexpected backing file: %q", source, q.BackingFile)
79
}
80
}
81
case asif.Type:
82
if destType == asif.Type {
83
return convertASIFToASIF(source, dest, size)
84
}
85
return fmt.Errorf("conversion from ASIF to %q is not supported", destType)
86
default:
87
logrus.Warnf("image %q has an unexpected format: %q", source, t)
88
}
89
if err = srcImg.Readable(); err != nil {
90
return fmt.Errorf("image %q is not readable: %w", source, err)
91
}
92
93
// Create a tmp file because source and dest can be same.
94
var (
95
destTmpF *os.File
96
destTmp string
97
attachedDevice string
98
)
99
switch destType {
100
case raw.Type:
101
destTmpF, err = os.CreateTemp(filepath.Dir(dest), filepath.Base(dest)+".lima-*.tmp")
102
destTmp = destTmpF.Name()
103
case asif.Type:
104
// destTmp != destTmpF.Name() because destTmpF is mounted ASIF device file.
105
randomBase := fmt.Sprintf("%s.lima-%d.tmp.asif", filepath.Base(dest), rand.UintN(math.MaxUint))
106
destTmp = filepath.Join(filepath.Dir(dest), randomBase)
107
// Since qcow2 image is smaller than expected size, we need to specify expected size to avoid resize later.
108
// Resizing ASIF image is not supported by qemu-img which recognizes ASIF format as raw.
109
var newSize int64
110
if size != nil {
111
newSize = *size
112
} else {
113
newSize = srcImg.Size()
114
}
115
attachedDevice, destTmpF, err = asifutil.NewAttachedASIF(destTmp, newSize)
116
default:
117
return fmt.Errorf("unsupported target image type: %q", destType)
118
}
119
if err != nil {
120
return err
121
}
122
defer os.RemoveAll(destTmp)
123
defer destTmpF.Close()
124
125
// Truncating before copy eliminates the seeks during copy and provide a
126
// hint to the file system that may minimize allocations and fragmentation
127
// of the file.
128
if err := makeSparse(destTmpF, srcImg.Size()); err != nil {
129
return err
130
}
131
132
// Copy
133
bar, err := progressbar.New(srcImg.Size())
134
if err != nil {
135
return err
136
}
137
bar.Start()
138
err = convert.Convert(destTmpF, srcImg, convert.Options{Progress: bar})
139
bar.Finish()
140
if err != nil {
141
return fmt.Errorf("failed to convert image: %w", err)
142
}
143
144
// Resize
145
if size != nil {
146
logrus.Infof("Expanding to %s", units.BytesSize(float64(*size)))
147
if err = makeSparse(destTmpF, *size); err != nil {
148
return err
149
}
150
}
151
if err = destTmpF.Close(); err != nil {
152
return err
153
}
154
// Detach ASIF device
155
if destType == asif.Type {
156
err := asifutil.DetachASIF(attachedDevice)
157
if err != nil {
158
return fmt.Errorf("failed to detach ASIF image %q: %w", attachedDevice, err)
159
}
160
}
161
162
// Rename destTmp into dest
163
if err = os.RemoveAll(dest); err != nil {
164
return err
165
}
166
return os.Rename(destTmp, dest)
167
}
168
169
func convertRawToRaw(source, dest string, size *int64) error {
170
if source != dest {
171
// continuity attempts clonefile
172
if err := containerdfs.CopyFile(dest, source); err != nil {
173
return fmt.Errorf("failed to copy %q into %q: %w", source, dest, err)
174
}
175
if err := os.Chmod(dest, 0o644); err != nil {
176
return fmt.Errorf("failed to set permissions on %q: %w", dest, err)
177
}
178
}
179
if size != nil {
180
logrus.Infof("Expanding to %s", units.BytesSize(float64(*size)))
181
destF, err := os.OpenFile(dest, os.O_RDWR, 0o644)
182
if err != nil {
183
return err
184
}
185
if err = makeSparse(destF, *size); err != nil {
186
_ = destF.Close()
187
return err
188
}
189
return destF.Close()
190
}
191
return nil
192
}
193
194
func convertASIFToASIF(source, dest string, size *int64) error {
195
if source != dest {
196
if err := containerdfs.CopyFile(dest, source); err != nil {
197
return fmt.Errorf("failed to copy %q into %q: %w", source, dest, err)
198
}
199
if err := os.Chmod(dest, 0o644); err != nil {
200
return fmt.Errorf("failed to set permissions on %q: %w", dest, err)
201
}
202
}
203
if size != nil {
204
logrus.Infof("Resizing to %s", units.BytesSize(float64(*size)))
205
if err := asifutil.ResizeASIF(dest, *size); err != nil {
206
return fmt.Errorf("failed to resize ASIF image %q: %w", dest, err)
207
}
208
}
209
return nil
210
}
211
212
func makeSparse(f *os.File, offset int64) error {
213
if _, err := f.Seek(offset, io.SeekStart); err != nil {
214
return err
215
}
216
return f.Truncate(offset)
217
}
218
219
// CreateDisk creates a new disk image with the specified size.
220
func (n *NativeImageUtil) CreateDisk(_ context.Context, disk string, size int64) error {
221
if _, err := os.Stat(disk); err == nil || !errors.Is(err, fs.ErrNotExist) {
222
return err
223
}
224
f, err := os.Create(disk)
225
if err != nil {
226
return err
227
}
228
defer f.Close()
229
roundedSize := roundUp(size)
230
return f.Truncate(roundedSize)
231
}
232
233
// Convert converts a disk image to the specified format.
234
// Currently supported formats are raw.Type and asif.Type.
235
func (n *NativeImageUtil) Convert(_ context.Context, imageType image.Type, source, dest string, size *int64, allowSourceWithBackingFile bool) error {
236
return convertTo(imageType, source, dest, size, allowSourceWithBackingFile)
237
}
238
239
// ResizeDisk resizes an existing disk image to the specified size.
240
func (n *NativeImageUtil) ResizeDisk(_ context.Context, disk string, size int64) error {
241
roundedSize := roundUp(size)
242
return os.Truncate(disk, roundedSize)
243
}
244
245
// MakeSparse makes a file sparse, starting from the specified offset.
246
func (n *NativeImageUtil) MakeSparse(_ context.Context, f *os.File, offset int64) error {
247
return makeSparse(f, offset)
248
}
249
250