From 009bd2abab58baeed5828a737fb44a8150d3e371 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=B3nio=20Meireles?= Date: Mon, 18 Jul 2016 12:23:51 +0100 Subject: [PATCH] add support for QCow2 volumes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - it's now the default / expected disk backend format - closes #42 Signed-off-by: António Meireles --- Godeps/Godeps.json | 4 + README.md | 35 +++-- components/server/qcow2.go | 100 ++++++++++++ components/server/run.go | 67 ++++++-- vendor/github.com/vbatts/qcow2/LICENSE | 19 +++ vendor/github.com/vbatts/qcow2/README.md | 13 ++ .../vbatts/qcow2/cmd/qcow2-info/main.go | 114 ++++++++++++++ vendor/github.com/vbatts/qcow2/qcow2.go | 69 +++++++++ vendor/github.com/vbatts/qcow2/qcow2_test.go | 146 ++++++++++++++++++ .../vbatts/qcow2/testdata/file.qcow2.gz | Bin 0 -> 15494 bytes 10 files changed, 535 insertions(+), 32 deletions(-) create mode 100644 components/server/qcow2.go create mode 100644 vendor/github.com/vbatts/qcow2/LICENSE create mode 100644 vendor/github.com/vbatts/qcow2/README.md create mode 100644 vendor/github.com/vbatts/qcow2/cmd/qcow2-info/main.go create mode 100644 vendor/github.com/vbatts/qcow2/qcow2.go create mode 100644 vendor/github.com/vbatts/qcow2/qcow2_test.go create mode 100644 vendor/github.com/vbatts/qcow2/testdata/file.qcow2.gz diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index d6fd7a1..e83dc1b 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -467,6 +467,10 @@ "ImportPath": "github.com/ugorji/go/codec", "Rev": "b94837a2404ab90efe9289e77a70694c355739cb" }, + { + "ImportPath": "github.com/vbatts/qcow2", + "Rev": "cbec6a0f73bd869d047ef2fdf6afa464df574d23" + }, { "ImportPath": "github.com/vincent-petithory/dataurl", "Rev": "9a301d65acbb728fcc3ace14f45f511a4cfeea9c" diff --git a/README.md b/README.md index e016235..3d45e0d 100644 --- a/README.md +++ b/README.md @@ -138,28 +138,33 @@ Accessing the newly created CoreOS instance is just a few more clicks away... ## simple usage recipe: a **docker** and **rkt** playground ### create a volume to store your persistent data - + > the step bellow requires `qemu-img`to be present in you macOS host + > one way to achieve that is having it installed through + > [homebrew's](http://brew.sh) by issuing `❯❯❯ brew install qemu` ``` - ❯❯❯ dd if=/dev/zero of=var_lib_docker.img bs=1G count=16 + ❯❯❯ qemu-img create -f qcow2 var_lib_docker.img.qcow2 16G ``` -> will become `/var/lib/{docker|rkt}`. in this example case we created a volume -> with 16GB. + > will become `/var/lib/{docker|rkt}`. in this example case we created a + > **QCow2** volume with 16GB. -### *format* it + **Raw** volumes were the default until version + **[0.7.12](https://github.com/TheNewNormal/corectl/releases/tag/v0.7.12)**. + They are still supported but become a deprecated feature that may disappear + some point in the future. - ``` - ❯❯❯ /usr/local/Cellar/e2fsprogs/1.42.12/sbin/mke2fs -b 1024 -i 1024 -t ext4 -m0 -F var_lib_docker.img - ``` - > requires [homebrew's](http://brew.sh) e2fsprogs package installed. - > - > `❯❯❯ brew install e2fsprogs` - -### *label* it +### *format* and label it + > we'll format and label the newly create volume from within a transient VM + > as it's the simplest way. We're formatting it with `ext4` but you can choose + > any filesystem you like assuming it is a CoreOS supported one. ``` - ❯❯❯ /usr/local/Cellar/e2fsprogs/1.42.12/sbin/e2label var_lib_docker.img rkthdd + ❯❯❯ corectl run --name foo --volume=var_lib_docker.img.qcow2 + ❯❯❯ corectl ssh foo "sudo mke2fs -b 1024 -i 1024 -t ext4 -m0 /dev/vda && \ + sudo e2label /dev/vda rkthdd " + ❯❯❯ corectl halt foo ``` - here, we labeled our volume `rkthdd` which is the *signature* that our + + above, we labeled our volume `rkthdd` which is the *signature* that our [*recipe*](cloud-init/docker-only-with-persistent-storage.txt) expects. >by relying in *labels* for volume identification we get around the issues we'd diff --git a/components/server/qcow2.go b/components/server/qcow2.go new file mode 100644 index 0000000..697d3b0 --- /dev/null +++ b/components/server/qcow2.go @@ -0,0 +1,100 @@ +// Copyright (c) 2016 by António Meireles . +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package server + +import ( + "bytes" + "encoding/binary" + "fmt" + "os" + + "github.com/helm/helm/log" + "github.com/vbatts/qcow2" +) + +var ErrFileIsNotQCOW2 = fmt.Errorf("File doesn't appear to be a qcow one") + +// adapted from github.com/vbatts/qcow2/cmd/qcow2-info/main.go +func ValidateQcow2(fh *os.File) (err error) { + var size int + + buf := make([]byte, qcow2.V2HeaderSize) + + if size, err = fh.Read(buf); err != nil { + return + } + + if size >= qcow2.V2HeaderSize && bytes.Compare(buf[:4], qcow2.Magic) != 0 { + log.Debug("%q: Does not appear to be qcow file %#v %#v", + fh.Name(), buf[:4], qcow2.Magic) + return ErrFileIsNotQCOW2 + } + + q := qcow2.Header{ + Version: qcow2.Version(be32(buf[4:8])), + BackingFileOffset: be64(buf[8:16]), + BackingFileSize: be32(buf[16:20]), + ClusterBits: be32(buf[20:24]), + Size: be64(buf[24:32]), + CryptMethod: qcow2.CryptMethod(be32(buf[32:36])), + L1Size: be32(buf[36:40]), + L1TableOffset: be64(buf[40:48]), + RefcountTableOffset: be64(buf[48:56]), + RefcountTableClusters: be32(buf[56:60]), + NbSnapshots: be32(buf[60:64]), + SnapshotsOffset: be64(buf[64:72]), + HeaderLength: 72, // v2 this is a standard length + } + + if q.Version == 3 { + if size, err = fh.Read(buf[:qcow2.V3HeaderSize]); err != nil { + return fmt.Errorf("(qcow2) error validating %q: %s", + fh.Name(), err) + } + if size < qcow2.V3HeaderSize { + return fmt.Errorf("(qcow2) error validating %q: short read", + fh.Name()) + } + + q.IncompatibleFeatures = be32(buf[0:8]) + q.CompatibleFeatures = be32(buf[8:16]) + q.AutoclearFeatures = be32(buf[16:24]) + q.RefcountOrder = be32(buf[24:28]) + q.HeaderLength = be32(buf[28:32]) + } + if log.IsDebugging { + log.Info("%#v\n", q) + log.Info("IncompatibleFeatures: %b\n", q.IncompatibleFeatures) + log.Info("CompatibleFeatures: %b\n", q.CompatibleFeatures) + } + // Process the extension header data + buf = make([]byte, q.HeaderLength) + if size, err = fh.Read(buf); err != nil { + return fmt.Errorf("(qcow2) error validating %q: %s", fh.Name(), err) + } + if size < q.HeaderLength { + return fmt.Errorf("(qcow2) error validating %q: short read", fh.Name()) + } + return +} + +func be32(b []byte) int { + return int(binary.BigEndian.Uint32(b)) +} + +func be64(b []byte) int64 { + return int64(binary.BigEndian.Uint64(b)) +} diff --git a/components/server/run.go b/components/server/run.go index 113c12a..cfe7bec 100644 --- a/components/server/run.go +++ b/components/server/run.go @@ -66,8 +66,8 @@ type ( } // StorageDevice ... StorageDevice struct { - Slot int - Type, Path string + Slot, Format int + Type, Path string } // StorageAssets ... StorageAssets struct { @@ -78,13 +78,12 @@ type ( const ( _ = iota Raw + Qcow2 Tap - HDD = "HDD" - CDROM = "CDROM" - Local = "localfs" - Remote = "URL" - Attached = true - Detached = false + HDD = "HDD" + CDROM = "CDROM" + Local = "localfs" + Remote = "URL" ) var ServerTimeout = 25 * time.Second @@ -114,7 +113,11 @@ func (vm *VMInfo) ValidateCDROM(path string) (err error) { // ValidateVolumes ... func (vm *VMInfo) ValidateVolumes(volumes []string, root bool) (err error) { - var abs string + var ( + abs string + fh *os.File + format = Qcow2 + ) for _, j := range volumes { if j != "" { @@ -124,9 +127,26 @@ func (vm *VMInfo) ValidateVolumes(volumes []string, root bool) (err error) { if abs, err = filepath.Abs(j); err != nil { return } - if !strings.HasSuffix(j, ".img") { - return fmt.Errorf("Aborting: --volume payload MUST end"+ - " in '.img' ('%s' doesn't)", j) + if fh, err = os.Open(j); err != nil { + return + } + defer fh.Close() + if err = ValidateQcow2(fh); err != nil { + if err != ErrFileIsNotQCOW2 { + return + } + log.Warn("using Raw formated volumes is a deprecated feature " + + "that may become unsupported in the future. Please " + + "consider moving to QCOW2 ones") + format = Raw + err = nil + } + if format == Raw { + // to be consistent with previous behaviour + if !strings.HasSuffix(j, ".img") { + return fmt.Errorf("Aborting: --volume payload MUST end"+ + " in '.img' ('%s' doesn't)", j) + } } // check atomicity reply := &RPCreply{} @@ -157,7 +177,7 @@ func (vm *VMInfo) ValidateVolumes(volumes []string, root bool) (err error) { } } vm.Storage.HardDrives[strconv.Itoa(slot)] = - StorageDevice{Type: HDD, Slot: slot, Path: abs} + StorageDevice{Type: HDD, Format: format, Slot: slot, Path: abs} if root { vm.Root = slot } @@ -295,8 +315,15 @@ func (vm *VMInfo) assembleBootPayload() (xArgs []string, err error) { } for _, v := range vm.Storage.HardDrives { - instr = append(instr, "-s", fmt.Sprintf("4:%d,virtio-blk,%s", - v.Slot, v.Path)) + switch v.Format { + case Raw: + instr = append(instr, "-s", fmt.Sprintf("4:%d,virtio-blk,%s", + v.Slot, v.Path)) + case Qcow2: + instr = append(instr, "-s", + fmt.Sprintf("4:%d,virtio-blk,file://%s,format=qcow", + v.Slot, v.Path)) + } } return []string{strings.Join(instr, " "), @@ -412,11 +439,17 @@ func (volumes *StorageAssets) PrettyPrint(root int) { fmt.Printf(" /dev/cdrom%v\t%s\n", a, b.Path) } for a, b := range volumes.HardDrives { + format := "raw" i, _ := strconv.Atoi(a) + if b.Format == Qcow2 { + format = "qcow2" + } if i != root { - fmt.Printf(" /dev/vd%v\t%s\n", string(i+'a'), b.Path) + fmt.Printf(" /dev/vd%v\t%s,format=%s\n", string(i+'a'), + b.Path, format) } else { - fmt.Printf(" /,/dev/vd%v\t%s\n", string(i+'a'), b.Path) + fmt.Printf(" /,/dev/vd%v\t%s,format=%s\n", string(i+'a'), + b.Path, format) } } } diff --git a/vendor/github.com/vbatts/qcow2/LICENSE b/vendor/github.com/vbatts/qcow2/LICENSE new file mode 100644 index 0000000..8ba5491 --- /dev/null +++ b/vendor/github.com/vbatts/qcow2/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2015 Vincent Batts, Raleigh, NC, USA + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/github.com/vbatts/qcow2/README.md b/vendor/github.com/vbatts/qcow2/README.md new file mode 100644 index 0000000..2e69515 --- /dev/null +++ b/vendor/github.com/vbatts/qcow2/README.md @@ -0,0 +1,13 @@ +# qcow2 + +WIP bindings for qcow2 file format + +## installing + +```bash +go get github.com/vbatts/qcow2/cmd/qcow2-info +``` + +## License + +See [LICENSE](./LICENSE) diff --git a/vendor/github.com/vbatts/qcow2/cmd/qcow2-info/main.go b/vendor/github.com/vbatts/qcow2/cmd/qcow2-info/main.go new file mode 100644 index 0000000..6a31f62 --- /dev/null +++ b/vendor/github.com/vbatts/qcow2/cmd/qcow2-info/main.go @@ -0,0 +1,114 @@ +package main + +import ( + "bytes" + "encoding/binary" + "flag" + "fmt" + "os" + + "github.com/vbatts/qcow2" +) + +func main() { + flag.Parse() + + for _, arg := range flag.Args() { + fh, err := os.Open(arg) + if err != nil { + fmt.Fprintf(os.Stderr, "[ERR] %q: %s\n", arg, err) + os.Exit(1) + } + defer fh.Close() + + buf := make([]byte, qcow2.V2HeaderSize) + size, err := fh.Read(buf) + if err != nil { + fmt.Fprintf(os.Stderr, "[ERR] %q: %s\n", arg, err) + os.Exit(1) + } + if size < qcow2.V2HeaderSize { + fmt.Fprintf(os.Stderr, "[ERR] %q: short read\n", arg) + os.Exit(1) + } + + if bytes.Compare(buf[:4], qcow2.Magic) != 0 { + fmt.Fprintf(os.Stderr, "[ERR] %q: Does not appear to be qcow file %#v %#v\n", arg, buf[:4], qcow2.Magic) + os.Exit(1) + } + + q := qcow2.Header{ + Version: qcow2.Version(be32(buf[4:8])), + BackingFileOffset: be64(buf[8:16]), + BackingFileSize: be32(buf[16:20]), + ClusterBits: be32(buf[20:24]), + Size: be64(buf[24:32]), + CryptMethod: qcow2.CryptMethod(be32(buf[32:36])), + L1Size: be32(buf[36:40]), + L1TableOffset: be64(buf[40:48]), + RefcountTableOffset: be64(buf[48:56]), + RefcountTableClusters: be32(buf[56:60]), + NbSnapshots: be32(buf[60:64]), + SnapshotsOffset: be64(buf[64:72]), + HeaderLength: 72, // v2 this is a standard length + } + + if q.Version == 3 { + size, err := fh.Read(buf[:qcow2.V3HeaderSize]) + if err != nil { + fmt.Fprintf(os.Stderr, "[ERR] %q: %s\n", arg, err) + os.Exit(1) + } + if size < qcow2.V3HeaderSize { + fmt.Fprintf(os.Stderr, "[ERR] %q: short read\n", arg) + os.Exit(1) + } + + q.IncompatibleFeatures = be32(buf[0:8]) + q.CompatibleFeatures = be32(buf[8:16]) + q.AutoclearFeatures = be32(buf[16:24]) + q.RefcountOrder = be32(buf[24:28]) + q.HeaderLength = be32(buf[28:32]) + } + fmt.Printf("%#v\n", q) + fmt.Printf("IncompatibleFeatures: %b\n", q.IncompatibleFeatures) + fmt.Printf("CompatibleFeatures: %b\n", q.CompatibleFeatures) + + // Process the extension header data + buf = make([]byte, q.HeaderLength) + size, err = fh.Read(buf) + if err != nil { + fmt.Fprintf(os.Stderr, "[ERR] %q: %s\n", arg, err) + os.Exit(1) + } + if size < q.HeaderLength { + fmt.Fprintf(os.Stderr, "[ERR] %q: short read\n", arg) + os.Exit(1) + } + for { + t := qcow2.HeaderExtensionType(be32(buf[:4])) + if t == qcow2.HdrExtEndOfArea { + break + } + exthdr := qcow2.ExtHeader{ + Type: t, + Size: be32(buf[4:8]), + } + // XXX this may need a copy(), so the slice resuse doesn't corrupt + exthdr.Data = buf[8 : 8+exthdr.Size] + q.ExtHeaders = append(q.ExtHeaders, exthdr) + + round := exthdr.Size % 8 + buf = buf[8+exthdr.Size+round:] + } + + } +} + +func be32(b []byte) int { + return int(binary.BigEndian.Uint32(b)) +} + +func be64(b []byte) int64 { + return int64(binary.BigEndian.Uint64(b)) +} diff --git a/vendor/github.com/vbatts/qcow2/qcow2.go b/vendor/github.com/vbatts/qcow2/qcow2.go new file mode 100644 index 0000000..fbe8c04 --- /dev/null +++ b/vendor/github.com/vbatts/qcow2/qcow2.go @@ -0,0 +1,69 @@ +package qcow2 + +var ( + // Magic is the front of the file fingerprint + Magic = []byte{0x51, 0x46, 0x49, 0xFB} + + // V2HeaderSize is the image header at the beginning of the file + V2HeaderSize = 72 + + // V3HeaderSize is directly following the v2 header, up to 104 + V3HeaderSize = 104 - V2HeaderSize +) + +type ( + // Version number of this image. Valid versions are 2 or 3 + Version int + + // CryptMethod is whether no encryption (0), or AES encryption (1) + CryptMethod int + + // HeaderExtensionType indicators the the entries in the optional header area + HeaderExtensionType int +) + +const ( + HdrExtEndOfArea HeaderExtensionType = 0x00000000 + HdrExtBackingFileFormat HeaderExtensionType = 0xE2792ACA + HdrExtFeatureNameTable HeaderExtensionType = 0x6803f857 // TODO needs processing for feature name table + // any thing else is "other" and can be ignored +) + +func (qcm CryptMethod) String() string { + if qcm == 1 { + return "AES" + } + return "none" +} + +type Header struct { + // magic [:4] + Version Version // [4:8] + BackingFileOffset int64 // [8:16] + BackingFileSize int // [16:20] + ClusterBits int // [20:24] + Size int64 // [24:32] + CryptMethod CryptMethod // [32:36] + L1Size int // [36:40] + L1TableOffset int64 // [40:48] + RefcountTableOffset int64 // [48:56] + RefcountTableClusters int // [56:60] + NbSnapshots int // [60:64] + SnapshotsOffset int64 // [64:72] + + // v3 + IncompatibleFeatures int // [72:80] bitmask + CompatibleFeatures int // [80:88] bitmask + AutoclearFeatures int // [88:96] bitmask + RefcountOrder int // [96:100] + HeaderLength int // [100:104] + + // Header extensions + ExtHeaders []ExtHeader +} + +type ExtHeader struct { + Type HeaderExtensionType + Size int + Data []byte +} diff --git a/vendor/github.com/vbatts/qcow2/qcow2_test.go b/vendor/github.com/vbatts/qcow2/qcow2_test.go new file mode 100644 index 0000000..57a37fc --- /dev/null +++ b/vendor/github.com/vbatts/qcow2/qcow2_test.go @@ -0,0 +1,146 @@ +package qcow2 + +import ( + "bufio" + "bytes" + "compress/gzip" + "encoding/binary" + "os" + "testing" +) + +/* +qemu-img create -f qcow2 file.qcow2 100m +sudo modprobe nbd max_part=63 +sudo qemu-nbd -f qcow2 -c /dev/nbd0 file.qcow2 +sudo mkfs.ext2 /dev/nbd0 +mkdir -p file/ +sudo mount /dev/nbd0 file/ +sudo umount file/ +sudo qemu-nbd -d /dev/nbd0 +sudo qemu-img snapshot -c base file.qcow2 +sudo qemu-nbd -f qcow2 -c /dev/nbd0 file.qcow2 +sudo mount /dev/nbd0 file +echo Howdy | sudo dd of=file/hello.txt +sudo umount file/ +sudo qemu-nbd -d /dev/nbd0 +sudo qemu-img snapshot -c hello file.qcow2 +sudo qemu-nbd -f qcow2 -c /dev/nbd0 file.qcow2 +sudo mount /dev/nbd0 file +sudo rm file/hello.txt +sudo umount file/ +sudo qemu-nbd -d /dev/nbd0 +ls -lsh ./file.qcow2 +# 4.9M -rw-r--r--. 1 vbatts vbatts 5.1M Sep 3 13:38 file.qcow2 +qcow2-info ./file.qcow2 +# image: ./file.qcow2 +# file format: qcow2 +# virtual size: 100M (104857600 bytes) +# disk size: 4.8M +# cluster_size: 65536 +# Snapshot list: +# ID TAG VM SIZE DATE VM CLOCK +# 1 base 0 2015-09-03 13:36:55 00:00:00.000 +# 2 hello 0 2015-09-03 13:38:07 00:00:00.000 +# Format specific information: +# compat: 1.1 +# lazy refcounts: false +# refcount bits: 16 +# corrupt: false +gzip -9 file.qcow2 > file.qcow2.gz +*/ +var testQcowFile = "./testdata/file.qcow2.gz" + +func TestHeader(t *testing.T) { + f, err := os.Open(testQcowFile) + if err != nil { + t.Fatal(err) + } + gz, err := gzip.NewReader(f) + if err != nil { + t.Fatal(err) + } + rdr := bufio.NewReader(gz) + + buf := make([]byte, V2HeaderSize) + size, err := rdr.Read(buf) + if err != nil { + t.Fatal(err) + } + if size < V2HeaderSize { + t.Fatal(err) + } + + if bytes.Compare(buf[:4], Magic) != 0 { + t.Fatalf("[ERR] Does not appear to be qcow file %#v %#v\n", buf[:4], Magic) + } + + q := Header{ + Version: Version(be32(buf[4:8])), + BackingFileOffset: be64(buf[8:16]), + BackingFileSize: be32(buf[16:20]), + ClusterBits: be32(buf[20:24]), + Size: be64(buf[24:32]), + CryptMethod: CryptMethod(be32(buf[32:36])), + L1Size: be32(buf[36:40]), + L1TableOffset: be64(buf[40:48]), + RefcountTableOffset: be64(buf[48:56]), + RefcountTableClusters: be32(buf[56:60]), + NbSnapshots: be32(buf[60:64]), + SnapshotsOffset: be64(buf[64:72]), + HeaderLength: 72, // v2 this is a standard length + } + + if q.Version == 3 { + size, err := rdr.Read(buf[:V3HeaderSize]) + if err != nil { + t.Fatal(err) + } + if size < V3HeaderSize { + t.Fatalf("short read") + } + + q.IncompatibleFeatures = be32(buf[0:8]) + q.CompatibleFeatures = be32(buf[8:16]) + q.AutoclearFeatures = be32(buf[16:24]) + q.RefcountOrder = be32(buf[24:28]) + q.HeaderLength = be32(buf[28:32]) + } + t.Logf("%#v", q) + + // Process the extension header data + buf = make([]byte, q.HeaderLength) + size, err = rdr.Read(buf) + if err != nil { + t.Fatal(err) + } + if size < q.HeaderLength { + t.Fatalf("short read") + } + for { + t := HeaderExtensionType(be32(buf[:4])) + if t == HdrExtEndOfArea { + break + } + exthdr := ExtHeader{ + Type: t, + Size: be32(buf[4:8]), + } + // XXX this may need a copy(), so the slice resuse doesn't corrupt + exthdr.Data = buf[8 : 8+exthdr.Size] + q.ExtHeaders = append(q.ExtHeaders, exthdr) + + round := exthdr.Size % 8 + buf = buf[8+exthdr.Size+round:] + } + + // TODO at this point we can do some assertions on the `q` values +} + +func be32(b []byte) int { + return int(binary.BigEndian.Uint32(b)) +} + +func be64(b []byte) int64 { + return int64(binary.BigEndian.Uint64(b)) +} diff --git a/vendor/github.com/vbatts/qcow2/testdata/file.qcow2.gz b/vendor/github.com/vbatts/qcow2/testdata/file.qcow2.gz new file mode 100644 index 0000000000000000000000000000000000000000..1ad0b02b73594b5a5714b0cbe42d628250b9d28b GIT binary patch literal 15494 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 QM6g$G8uRIZ0gDIxFH)~&#Q*>R literal 0 HcmV?d00001