Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
lima-vm
GitHub Repository: lima-vm/lima
Path: blob/master/pkg/qemuimgutil/qemuimgutil.go
2667 views
1
// SPDX-FileCopyrightText: Copyright The Lima Authors
2
// SPDX-License-Identifier: Apache-2.0
3
4
package qemuimgutil
5
6
import (
7
"bytes"
8
"context"
9
"encoding/json"
10
"errors"
11
"fmt"
12
"io/fs"
13
"os"
14
"os/exec"
15
"strconv"
16
17
"github.com/lima-vm/go-qcow2reader/image"
18
"github.com/lima-vm/go-qcow2reader/image/raw"
19
"github.com/sirupsen/logrus"
20
)
21
22
const QemuImgFormat = "qcow2"
23
24
// QemuImageUtil is the QEMU implementation of the imgutil Interface.
25
type QemuImageUtil struct {
26
DefaultFormat string // Default disk format, e.g., "qcow2"
27
}
28
29
// Info corresponds to the output of `qemu-img info --output=json FILE`.
30
type Info struct {
31
Filename string `json:"filename,omitempty"` // since QEMU 1.3
32
Format string `json:"format,omitempty"` // since QEMU 1.3
33
VSize int64 `json:"virtual-size,omitempty"` // since QEMU 1.3
34
ActualSize int64 `json:"actual-size,omitempty"` // since QEMU 1.3
35
DirtyFlag bool `json:"dirty-flag,omitempty"` // since QEMU 5.2
36
ClusterSize int `json:"cluster-size,omitempty"` // since QEMU 1.3
37
BackingFilename string `json:"backing-filename,omitempty"` // since QEMU 1.3
38
FullBackingFilename string `json:"full-backing-filename,omitempty"` // since QEMU 1.3
39
BackingFilenameFormat string `json:"backing-filename-format,omitempty"` // since QEMU 1.3
40
FormatSpecific *InfoFormatSpecific `json:"format-specific,omitempty"` // since QEMU 1.7
41
Children []InfoChild `json:"children,omitempty"` // since QEMU 8.0
42
}
43
44
type InfoChild struct {
45
Name string `json:"name,omitempty"` // since QEMU 8.0
46
Info Info `json:"info,omitempty"` // since QEMU 8.0
47
}
48
49
type InfoFormatSpecific struct {
50
Type string `json:"type,omitempty"` // since QEMU 1.7
51
Data json.RawMessage `json:"data,omitempty"` // since QEMU 1.7
52
}
53
54
func resizeDisk(ctx context.Context, disk, format string, size int64) error {
55
args := []string{"resize", "-f", format, disk, strconv.FormatInt(size, 10)}
56
cmd := exec.CommandContext(ctx, "qemu-img", args...)
57
if out, err := cmd.CombinedOutput(); err != nil {
58
return fmt.Errorf("failed to run %v: %q: %w", cmd.Args, string(out), err)
59
}
60
return nil
61
}
62
63
func (sp *InfoFormatSpecific) Qcow2() *InfoFormatSpecificDataQcow2 {
64
if sp.Type != "qcow2" {
65
return nil
66
}
67
var x InfoFormatSpecificDataQcow2
68
if err := json.Unmarshal(sp.Data, &x); err != nil {
69
panic(err)
70
}
71
return &x
72
}
73
74
func (sp *InfoFormatSpecific) Vmdk() *InfoFormatSpecificDataVmdk {
75
if sp.Type != "vmdk" {
76
return nil
77
}
78
var x InfoFormatSpecificDataVmdk
79
if err := json.Unmarshal(sp.Data, &x); err != nil {
80
panic(err)
81
}
82
return &x
83
}
84
85
type InfoFormatSpecificDataQcow2 struct {
86
Compat string `json:"compat,omitempty"` // since QEMU 1.7
87
LazyRefcounts bool `json:"lazy-refcounts,omitempty"` // since QEMU 1.7
88
Corrupt bool `json:"corrupt,omitempty"` // since QEMU 2.2
89
RefcountBits int `json:"refcount-bits,omitempty"` // since QEMU 2.3
90
CompressionType string `json:"compression-type,omitempty"` // since QEMU 5.1
91
ExtendedL2 bool `json:"extended-l2,omitempty"` // since QEMU 5.2
92
}
93
94
type InfoFormatSpecificDataVmdk struct {
95
CreateType string `json:"create-type,omitempty"` // since QEMU 1.7
96
CID int `json:"cid,omitempty"` // since QEMU 1.7
97
ParentCID int `json:"parent-cid,omitempty"` // since QEMU 1.7
98
Extents []InfoFormatSpecificDataVmdkExtent `json:"extents,omitempty"` // since QEMU 1.7
99
}
100
101
type InfoFormatSpecificDataVmdkExtent struct {
102
Filename string `json:"filename,omitempty"` // since QEMU 1.7
103
Format string `json:"format,omitempty"` // since QEMU 1.7
104
VSize int64 `json:"virtual-size,omitempty"` // since QEMU 1.7
105
ClusterSize int `json:"cluster-size,omitempty"` // since QEMU 1.7
106
}
107
108
func convertToRaw(ctx context.Context, source, dest string) error {
109
if source != dest {
110
return execQemuImgConvert(ctx, source, dest)
111
}
112
113
// If source == dest, we need to use a temporary file to avoid file locking issues
114
115
info, err := getInfo(ctx, source)
116
if err != nil {
117
return fmt.Errorf("failed to get info for source disk %q: %w", source, err)
118
}
119
if info.Format == "raw" {
120
return nil
121
}
122
123
tempFile := dest + ".lima-qemu-convert.tmp"
124
defer os.Remove(tempFile)
125
126
if err := execQemuImgConvert(ctx, source, tempFile); err != nil {
127
return err
128
}
129
130
return os.Rename(tempFile, dest)
131
}
132
133
func execQemuImgConvert(ctx context.Context, source, dest string) error {
134
var stdout, stderr bytes.Buffer
135
cmd := exec.CommandContext(ctx, "qemu-img", "convert", "-O", "raw", source, dest)
136
cmd.Stdout = &stdout
137
cmd.Stderr = &stderr
138
if err := cmd.Run(); err != nil {
139
return fmt.Errorf("failed to run %v: stdout=%q, stderr=%q: %w",
140
cmd.Args, stdout.String(), stderr.String(), err)
141
}
142
return nil
143
}
144
145
func parseInfo(b []byte) (*Info, error) {
146
var imgInfo Info
147
if err := json.Unmarshal(b, &imgInfo); err != nil {
148
return nil, err
149
}
150
return &imgInfo, nil
151
}
152
153
func getInfo(ctx context.Context, f string) (*Info, error) {
154
var stdout, stderr bytes.Buffer
155
cmd := exec.CommandContext(ctx, "qemu-img", "info", "--output=json", "--force-share", f)
156
cmd.Stdout = &stdout
157
cmd.Stderr = &stderr
158
if err := cmd.Run(); err != nil {
159
return nil, fmt.Errorf("failed to run %v: stdout=%q, stderr=%q: %w",
160
cmd.Args, stdout.String(), stderr.String(), err)
161
}
162
return parseInfo(stdout.Bytes())
163
}
164
165
// CreateDisk creates a new disk image with the specified size.
166
func (q *QemuImageUtil) CreateDisk(ctx context.Context, disk string, size int64) error {
167
if _, err := os.Stat(disk); err == nil || !errors.Is(err, fs.ErrNotExist) {
168
// disk already exists
169
return err
170
}
171
172
args := []string{"create", "-f", q.DefaultFormat, disk, strconv.FormatInt(size, 10)}
173
cmd := exec.CommandContext(ctx, "qemu-img", args...)
174
if out, err := cmd.CombinedOutput(); err != nil {
175
return fmt.Errorf("failed to run %v: %q: %w", cmd.Args, string(out), err)
176
}
177
return nil
178
}
179
180
// ResizeDisk resizes an existing disk image to the specified size.
181
func (q *QemuImageUtil) ResizeDisk(ctx context.Context, disk string, size int64) error {
182
info, err := getInfo(ctx, disk)
183
if err != nil {
184
return fmt.Errorf("failed to get info for disk %q: %w", disk, err)
185
}
186
return resizeDisk(ctx, disk, info.Format, size)
187
}
188
189
// MakeSparse is a stub implementation as the qemu package doesn't provide this functionality.
190
func (q *QemuImageUtil) MakeSparse(_ context.Context, _ *os.File, _ int64) error {
191
return nil
192
}
193
194
// GetInfo retrieves the information of a disk image.
195
func GetInfo(ctx context.Context, path string) (*Info, error) {
196
qemuInfo, err := getInfo(ctx, path)
197
if err != nil {
198
return nil, err
199
}
200
201
return qemuInfo, nil
202
}
203
204
// Convert converts a disk image to raw format.
205
// Currently only raw.Type is supported.
206
func (q *QemuImageUtil) Convert(ctx context.Context, imageType image.Type, source, dest string, size *int64, allowSourceWithBackingFile bool) error {
207
if imageType != raw.Type {
208
return fmt.Errorf("QemuImageUtil.Convert only supports raw.Type, got %q", imageType)
209
}
210
if !allowSourceWithBackingFile {
211
info, err := getInfo(ctx, source)
212
if err != nil {
213
return fmt.Errorf("failed to get info for source disk %q: %w", source, err)
214
}
215
if info.BackingFilename != "" || info.FullBackingFilename != "" {
216
return fmt.Errorf("qcow2 image %q has an unexpected backing file: %q", source, info.BackingFilename)
217
}
218
}
219
220
if err := convertToRaw(ctx, source, dest); err != nil {
221
return err
222
}
223
224
if size != nil {
225
destInfo, err := getInfo(ctx, dest)
226
if err != nil {
227
return fmt.Errorf("failed to get info for converted disk %q: %w", dest, err)
228
}
229
230
if *size > destInfo.VSize {
231
return resizeDisk(ctx, dest, "raw", *size)
232
}
233
}
234
235
return nil
236
}
237
238
// AcceptableAsBaseDisk checks if a disk image is acceptable as a base disk.
239
func AcceptableAsBaseDisk(info *Info) error {
240
switch info.Format {
241
case "qcow2", "raw":
242
// NOP
243
default:
244
logrus.WithField("filename", info.Filename).
245
Warnf("Unsupported image format %q. The image may not boot, or may have an extra privilege to access the host filesystem. Use with caution.", info.Format)
246
}
247
if info.BackingFilename != "" {
248
return fmt.Errorf("base disk (%q) must not have a backing file (%q)", info.Filename, info.BackingFilename)
249
}
250
if info.FullBackingFilename != "" {
251
return fmt.Errorf("base disk (%q) must not have a backing file (%q)", info.Filename, info.FullBackingFilename)
252
}
253
if info.FormatSpecific != nil {
254
if vmdk := info.FormatSpecific.Vmdk(); vmdk != nil {
255
for _, e := range vmdk.Extents {
256
if e.Filename != info.Filename {
257
return fmt.Errorf("base disk (%q) must not have an extent file (%q)", info.Filename, e.Filename)
258
}
259
}
260
}
261
}
262
// info.Children is set since QEMU 8.0
263
switch len(info.Children) {
264
case 0:
265
// NOP
266
case 1:
267
if info.Filename != info.Children[0].Info.Filename {
268
return fmt.Errorf("base disk (%q) child must not have a different filename (%q)", info.Filename, info.Children[0].Info.Filename)
269
}
270
if len(info.Children[0].Info.Children) > 0 {
271
return fmt.Errorf("base disk (%q) child must not have children of its own", info.Filename)
272
}
273
default:
274
return fmt.Errorf("base disk (%q) must not have multiple children: %+v", info.Filename, info.Children)
275
}
276
return nil
277
}
278
279