diff --git a/pkg/qemu/imgutil/imgutil.go b/pkg/qemu/imgutil/imgutil.go index 0496618d449..a3bfb92350f 100644 --- a/pkg/qemu/imgutil/imgutil.go +++ b/pkg/qemu/imgutil/imgutil.go @@ -9,10 +9,74 @@ import ( "strings" ) +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,11 +108,7 @@ 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) { 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") + }) + }) +}