diff --git a/pkg/qemu/imgutil/imgutil.go b/pkg/qemu/imgutil/imgutil.go index 0496618d449..9261ed175bc 100644 --- a/pkg/qemu/imgutil/imgutil.go +++ b/pkg/qemu/imgutil/imgutil.go @@ -5,14 +5,78 @@ import ( "encoding/json" "fmt" "os/exec" - "path/filepath" - "strings" + + "github.com/sirupsen/logrus" ) +type InfoChild struct { + Name string `json:"name,omitempty"` // since QEMU 8.0 + Info Info `json:"info,omitempty"` // since QEMU 8.0 +} + +type InfoFormatSpecific struct { + Type string `json:"type,omitempty"` // since QEMU 1.7 + Data json.RawMessage `json:"data,omitempty"` // since QEMU 1.7 +} + +func (sp *InfoFormatSpecific) Qcow2() *InfoFormatSpecificDataQcow2 { + if sp.Type != "qcow2" { + return nil + } + var x InfoFormatSpecificDataQcow2 + if err := json.Unmarshal(sp.Data, &x); err != nil { + panic(err) + } + return &x +} + +func (sp *InfoFormatSpecific) Vmdk() *InfoFormatSpecificDataVmdk { + if sp.Type != "vmdk" { + return nil + } + var x InfoFormatSpecificDataVmdk + if err := json.Unmarshal(sp.Data, &x); err != nil { + panic(err) + } + return &x +} + +type InfoFormatSpecificDataQcow2 struct { + Compat string `json:"compat,omitempty"` // since QEMU 1.7 + LazyRefcounts bool `json:"lazy-refcounts,omitempty"` // since QEMU 1.7 + Corrupt bool `json:"corrupt,omitempty"` // since QEMU 2.2 + RefcountBits int `json:"refcount-bits,omitempty"` // since QEMU 2.3 + CompressionType string `json:"compression-type,omitempty"` // since QEMU 5.1 + ExtendedL2 bool `json:"extended-l2,omitempty"` // since QEMU 5.2 +} + +type InfoFormatSpecificDataVmdk struct { + CreateType string `json:"create-type,omitempty"` // since QEMU 1.7 + CID int `json:"cid,omitempty"` // since QEMU 1.7 + ParentCID int `json:"parent-cid,omitempty"` // since QEMU 1.7 + Extents []InfoFormatSpecificDataVmdkExtent `json:"extents,omitempty"` // since QEMU 1.7 +} + +type InfoFormatSpecificDataVmdkExtent struct { + Filename string `json:"filename,omitempty"` // since QEMU 1.7 + Format string `json:"format,omitempty"` // since QEMU 1.7 + VSize int64 `json:"virtual-size,omitempty"` // since QEMU 1.7 + ClusterSize int `json:"cluster-size,omitempty"` // since QEMU 1.7 +} + // Info corresponds to the output of `qemu-img info --output=json FILE` type Info struct { - Format string `json:"format,omitempty"` // since QEMU 1.3 - VSize int64 `json:"virtual-size,omitempty"` + Filename string `json:"filename,omitempty"` // since QEMU 1.3 + Format string `json:"format,omitempty"` // since QEMU 1.3 + VSize int64 `json:"virtual-size,omitempty"` // since QEMU 1.3 + ActualSize int64 `json:"actual-size,omitempty"` // since QEMU 1.3 + DirtyFlag bool `json:"dirty-flag,omitempty"` // since QEMU 5.2 + ClusterSize int `json:"cluster-size,omitempty"` // since QEMU 1.3 + BackingFilename string `json:"backing-filename,omitempty"` // since QEMU 1.3 + FullBackingFilename string `json:"full-backing-filename,omitempty"` // since QEMU 1.3 + BackingFilenameFormat string `json:"backing-filename-format,omitempty"` // since QEMU 1.3 + FormatSpecific *InfoFormatSpecific `json:"format-specific,omitempty"` // since QEMU 1.7 + Children []InfoChild `json:"children,omitempty"` // since QEMU 8.0 } func ConvertToRaw(source string, dest string) error { @@ -27,6 +91,14 @@ func ConvertToRaw(source string, dest string) error { return nil } +func ParseInfo(b []byte) (*Info, error) { + var imgInfo Info + if err := json.Unmarshal(b, &imgInfo); err != nil { + return nil, err + } + return &imgInfo, nil +} + func GetInfo(f string) (*Info, error) { var stdout, stderr bytes.Buffer cmd := exec.Command("qemu-img", "info", "--output=json", "--force-share", f) @@ -36,26 +108,45 @@ func GetInfo(f string) (*Info, error) { return nil, fmt.Errorf("failed to run %v: stdout=%q, stderr=%q: %w", cmd.Args, stdout.String(), stderr.String(), err) } - var imgInfo Info - if err := json.Unmarshal(stdout.Bytes(), &imgInfo); err != nil { - return nil, err - } - return &imgInfo, nil + return ParseInfo(stdout.Bytes()) } -func DetectFormat(f string) (string, error) { - switch ext := strings.ToLower(filepath.Ext(f)); ext { - case ".qcow2": - return "qcow2", nil - case ".raw": - return "raw", nil +func AcceptableAsBasedisk(info *Info) error { + switch info.Format { + case "qcow2", "raw": + // NOP + default: + logrus.WithField("filename", info.Filename). + 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) + } + if info.BackingFilename != "" { + return fmt.Errorf("base disk (%q) must not have a backing file (%q)", info.Filename, info.BackingFilename) } - imgInfo, err := GetInfo(f) - if err != nil { - return "", err + if info.FullBackingFilename != "" { + return fmt.Errorf("base disk (%q) must not have a backing file (%q)", info.Filename, info.FullBackingFilename) } - if imgInfo.Format == "" { - return "", fmt.Errorf("failed to detect format of %q", f) + if info.FormatSpecific != nil { + if vmdk := info.FormatSpecific.Vmdk(); vmdk != nil { + for _, e := range vmdk.Extents { + if e.Filename != info.Filename { + return fmt.Errorf("base disk (%q) must not have an extent file (%q)", info.Filename, e.Filename) + } + } + } } - return imgInfo.Format, nil + // info.Children is set since QEMU 8.0 + switch len(info.Children) { + case 0: + // NOP + case 1: + if info.Filename != info.Children[0].Info.Filename { + return fmt.Errorf("base disk (%q) child must not have a different filename (%q)", info.Filename, info.Children[0].Info.Filename) + } + if len(info.Children[0].Info.Children) > 0 { + return fmt.Errorf("base disk (%q) child must not have children of its own", info.Filename) + } + default: + return fmt.Errorf("base disk (%q) must not have multiple children: %+v", info.Filename, info.Children) + } + return nil } diff --git a/pkg/qemu/imgutil/imgutil_test.go b/pkg/qemu/imgutil/imgutil_test.go new file mode 100644 index 00000000000..b9c8dfc68eb --- /dev/null +++ b/pkg/qemu/imgutil/imgutil_test.go @@ -0,0 +1,212 @@ +package imgutil + +import ( + "testing" + + "gotest.tools/v3/assert" +) + +func TestParseInfo(t *testing.T) { + t.Run("qcow2", func(t *testing.T) { + // qemu-img create -f qcow2 foo.qcow2 4G + // (QEMU 8.0) + const s = `{ + "children": [ + { + "name": "file", + "info": { + "children": [ + ], + "virtual-size": 197120, + "filename": "foo.qcow2", + "format": "file", + "actual-size": 200704, + "format-specific": { + "type": "file", + "data": { + } + }, + "dirty-flag": false + } + } + ], + "virtual-size": 4294967296, + "filename": "foo.qcow2", + "cluster-size": 65536, + "format": "qcow2", + "actual-size": 200704, + "format-specific": { + "type": "qcow2", + "data": { + "compat": "1.1", + "compression-type": "zlib", + "lazy-refcounts": false, + "refcount-bits": 16, + "corrupt": false, + "extended-l2": false + } + }, + "dirty-flag": false +}` + + info, err := ParseInfo([]byte(s)) + assert.NilError(t, err) + assert.Equal(t, 1, len(info.Children)) + assert.Check(t, info.FormatSpecific != nil) + qcow2 := info.FormatSpecific.Qcow2() + assert.Check(t, qcow2 != nil) + assert.Equal(t, qcow2.Compat, "1.1") + + t.Run("diff", func(t *testing.T) { + // qemu-img create -f qcow2 -F qcow2 -b foo.qcow2 bar.qcow2 + // (QEMU 8.0) + const s = `{ + "children": [ + { + "name": "file", + "info": { + "children": [ + ], + "virtual-size": 197120, + "filename": "bar.qcow2", + "format": "file", + "actual-size": 200704, + "format-specific": { + "type": "file", + "data": { + } + }, + "dirty-flag": false + } + } + ], + "backing-filename-format": "qcow2", + "virtual-size": 4294967296, + "filename": "bar.qcow2", + "cluster-size": 65536, + "format": "qcow2", + "actual-size": 200704, + "format-specific": { + "type": "qcow2", + "data": { + "compat": "1.1", + "compression-type": "zlib", + "lazy-refcounts": false, + "refcount-bits": 16, + "corrupt": false, + "extended-l2": false + } + }, + "full-backing-filename": "foo.qcow2", + "backing-filename": "foo.qcow2", + "dirty-flag": false +}` + info, err := ParseInfo([]byte(s)) + assert.NilError(t, err) + assert.Equal(t, 1, len(info.Children)) + assert.Equal(t, "foo.qcow2", info.BackingFilename) + assert.Equal(t, "bar.qcow2", info.Filename) + assert.Check(t, info.FormatSpecific != nil) + qcow2 := info.FormatSpecific.Qcow2() + assert.Check(t, qcow2 != nil) + assert.Equal(t, qcow2.Compat, "1.1") + }) + }) + t.Run("vmdk", func(t *testing.T) { + t.Run("twoGbMaxExtentSparse", func(t *testing.T) { + // qemu-img create -f vmdk foo.vmdk 4G -o subformat=twoGbMaxExtentSparse + // (QEMU 8.0) + const s = `{ + "children": [ + { + "name": "extents.1", + "info": { + "children": [ + ], + "virtual-size": 327680, + "filename": "foo-s002.vmdk", + "format": "file", + "actual-size": 327680, + "format-specific": { + "type": "file", + "data": { + } + }, + "dirty-flag": false + } + }, + { + "name": "extents.0", + "info": { + "children": [ + ], + "virtual-size": 327680, + "filename": "foo-s001.vmdk", + "format": "file", + "actual-size": 327680, + "format-specific": { + "type": "file", + "data": { + } + }, + "dirty-flag": false + } + }, + { + "name": "file", + "info": { + "children": [ + ], + "virtual-size": 512, + "filename": "foo.vmdk", + "format": "file", + "actual-size": 4096, + "format-specific": { + "type": "file", + "data": { + } + }, + "dirty-flag": false + } + } + ], + "virtual-size": 4294967296, + "filename": "foo.vmdk", + "cluster-size": 65536, + "format": "vmdk", + "actual-size": 659456, + "format-specific": { + "type": "vmdk", + "data": { + "cid": 918420663, + "parent-cid": 4294967295, + "create-type": "twoGbMaxExtentSparse", + "extents": [ + { + "virtual-size": 2147483648, + "filename": "foo-s001.vmdk", + "cluster-size": 65536, + "format": "SPARSE" + }, + { + "virtual-size": 2147483648, + "filename": "foo-s002.vmdk", + "cluster-size": 65536, + "format": "SPARSE" + } + ] + } + }, + "dirty-flag": false +}` + info, err := ParseInfo([]byte(s)) + assert.NilError(t, err) + assert.Equal(t, 3, len(info.Children)) + assert.Equal(t, "foo.vmdk", info.Filename) + assert.Check(t, info.FormatSpecific != nil) + vmdk := info.FormatSpecific.Vmdk() + assert.Check(t, vmdk != nil) + assert.Equal(t, vmdk.CreateType, "twoGbMaxExtentSparse") + }) + }) +} diff --git a/pkg/qemu/qemu.go b/pkg/qemu/qemu.go index 3c02a01226b..f8fe70acdb7 100644 --- a/pkg/qemu/qemu.go +++ b/pkg/qemu/qemu.go @@ -96,13 +96,19 @@ func EnsureDisk(cfg Config) error { if err != nil { return err } + baseDiskInfo, err := imgutil.GetInfo(baseDisk) + if err != nil { + return fmt.Errorf("failed to get the information of base disk %q: %w", baseDisk, err) + } + if err = imgutil.AcceptableAsBasedisk(baseDiskInfo); err != nil { + return fmt.Errorf("file %q is not acceptable as the base disk: %w", baseDisk, err) + } + if baseDiskInfo.Format == "" { + return fmt.Errorf("failed to inspect the format of %q", baseDisk) + } args := []string{"create", "-f", "qcow2"} if !isBaseDiskISO { - baseDiskFormat, err := imgutil.DetectFormat(baseDisk) - if err != nil { - return err - } - args = append(args, "-F", baseDiskFormat, "-b", baseDisk) + args = append(args, "-F", baseDiskInfo.Format, "-b", baseDisk) } args = append(args, diffDisk, strconv.Itoa(int(diskSize))) cmd := exec.Command("qemu-img", args...) @@ -570,14 +576,24 @@ func Cmdline(cfg Config) (string, []string, error) { } if isBaseDiskCDROM { args = appendArgsIfNoConflict(args, "-boot", "order=d,splash-time=0,menu=on") - args = append(args, "-drive", fmt.Sprintf("file=%s,media=cdrom,readonly=on", baseDisk)) + args = append(args, "-drive", fmt.Sprintf("file=%s,format=raw,media=cdrom,readonly=on", baseDisk)) } 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 { - args = append(args, "-drive", fmt.Sprintf("file=%s,if=virtio,discard=on", baseDisk)) + baseDiskInfo, err := imgutil.GetInfo(baseDisk) + if err != nil { + return "", nil, fmt.Errorf("failed to get the information of %q: %w", baseDisk, err) + } + if err = imgutil.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/vz/disk.go b/pkg/vz/disk.go index 557361273bb..334827a98a5 100644 --- a/pkg/vz/disk.go +++ b/pkg/vz/disk.go @@ -49,13 +49,19 @@ func EnsureDisk(driver *driver.BaseDriver) error { if err != nil { return err } + baseDiskInfo, err := imgutil.GetInfo(baseDisk) + if err != nil { + return fmt.Errorf("failed to get the information of base disk %q: %w", baseDisk, err) + } + if err = imgutil.AcceptableAsBasedisk(baseDiskInfo); err != nil { + return fmt.Errorf("file %q is not acceptable as the base disk: %w", baseDisk, err) + } + if baseDiskInfo.Format == "" { + return fmt.Errorf("failed to inspect the format of %q", baseDisk) + } args := []string{"create", "-f", "qcow2"} if !isBaseDiskISO { - baseDiskFormat, err := imgutil.DetectFormat(baseDisk) - if err != nil { - return err - } - args = append(args, "-F", baseDiskFormat, "-b", baseDisk) + args = append(args, "-F", baseDiskInfo.Format, "-b", baseDisk) } args = append(args, diffDisk, strconv.Itoa(int(diskSize))) cmd := exec.Command("qemu-img", args...) diff --git a/pkg/vz/vm_darwin.go b/pkg/vz/vm_darwin.go index f2ae41cf340..d33718250d2 100644 --- a/pkg/vz/vm_darwin.go +++ b/pkg/vz/vm_darwin.go @@ -373,12 +373,12 @@ func attachNetwork(driver *driver.BaseDriver, vmConfig *vz.VirtualMachineConfigu } func validateDiskFormat(diskPath string) error { - format, err := imgutil.DetectFormat(diskPath) + info, err := imgutil.GetInfo(diskPath) if err != nil { return fmt.Errorf("failed to detect the format of %q: %w", diskPath, err) } - if format != "raw" { - return fmt.Errorf("expected the format of %q to be \"raw\", got %q", diskPath, format) + if info.Format != "raw" { + return fmt.Errorf("expected the format of %q to be \"raw\", got %q", diskPath, info.Format) } // TODO: ensure that the disk is formatted with GPT or ISO9660 return nil @@ -437,23 +437,23 @@ func attachDisks(driver *driver.BaseDriver, vmConfig *vz.VirtualMachineConfigura } extraDiskPath := filepath.Join(d.Dir, filenames.DataDisk) - extraDiskFormat, err := imgutil.DetectFormat(extraDiskPath) + extraDiskInfo, err := imgutil.GetInfo(extraDiskPath) if err != nil { return fmt.Errorf("failed to run detect disk format %q: %q", diskName, err) } - if extraDiskFormat != "raw" { - if strings.Contains(extraDiskFormat, string(os.PathSeparator)) { + if extraDiskInfo.Format != "raw" { + if strings.Contains(extraDiskInfo.Format, string(os.PathSeparator)) { return fmt.Errorf( "failed to convert disk %q to raw for vz driver because extraDiskFormat %q contains a path separator %q", diskName, - extraDiskFormat, + extraDiskInfo.Format, os.PathSeparator, ) } rawPath := fmt.Sprintf("%s.raw", extraDiskPath) - oldFormatPath := fmt.Sprintf("%s.%s", extraDiskPath, extraDiskFormat) + oldFormatPath := fmt.Sprintf("%s.%s", extraDiskPath, extraDiskInfo.Format) if err = imgutil.ConvertToRaw(extraDiskPath, rawPath); err != nil { - return fmt.Errorf("failed to convert %s disk %q to raw for vz driver: %w", extraDiskFormat, diskName, err) + return fmt.Errorf("failed to convert %s disk %q to raw for vz driver: %w", extraDiskInfo.Format, diskName, err) } if err = os.Rename(extraDiskPath, oldFormatPath); err != nil { return fmt.Errorf("failed to rename additional disk for vz driver: %w", err)