From 06b9e46cd234d083f4e9559907291a936b4b8b2c Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 5 Apr 2016 12:20:37 +0200 Subject: [PATCH] Create Qcow2 class for reading Qcow2 and rebase it Ref #466 --- gns3server/modules/qemu/qcow2.py | 100 +++++++++++++++++++++++++++++++ tests/modules/qemu/test_qcow2.py | 68 +++++++++++++++++++++ tests/resources/empty8G.qcow2 | Bin 0 -> 197120 bytes tests/resources/linked.qcow2 | Bin 0 -> 197120 bytes 4 files changed, 168 insertions(+) create mode 100644 gns3server/modules/qemu/qcow2.py create mode 100644 tests/modules/qemu/test_qcow2.py create mode 100644 tests/resources/empty8G.qcow2 create mode 100644 tests/resources/linked.qcow2 diff --git a/gns3server/modules/qemu/qcow2.py b/gns3server/modules/qemu/qcow2.py new file mode 100644 index 00000000..9f0fc462 --- /dev/null +++ b/gns3server/modules/qemu/qcow2.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python +# +# Copyright (C) 2016 GNS3 Technologies Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import asyncio +import struct + + +class Qcow2Error(Exception): + pass + + +class Qcow2: + """ + Allow to parse a Qcow2 file + """ + + def __init__(self, path): + + self.path = path + self._reload() + + def _reload(self): + # Each QCOW2 file begins with a header, in big endian format, as follows: + # + # typedef struct QCowHeader { + # uint32_t magic; + # uint32_t version; + # + # uint64_t backing_file_offset; + # uint32_t backing_file_size; + # + # uint32_t cluster_bits; + # uint64_t size; /* in bytes */ + # uint32_t crypt_method; + # + # uint32_t l1_size; + # uint64_t l1_table_offset; + # + # uint64_t refcount_table_offset; + # uint32_t refcount_table_clusters; + # + # uint32_t nb_snapshots; + # uint64_t snapshots_offset; + # } QCowHeader; + struct_format = ">IIQi" + + with open(self.path, 'rb') as f: + content = f.read(struct.calcsize(struct_format)) + + self.magic, self.version, self.backing_file_offset, self.backing_file_size = struct.unpack_from(struct_format, content) + + if self.magic != 1363560955: # The first 4 bytes contain the characters 'Q', 'F', 'I' followed by 0xfb. + raise Qcow2Error("Invalid magic for {}".format(self.path)) + + @property + def backing_file(self): + """ + When using linked clone this will return the path to the base image + + :returns: None if it's not a linked clone, the path otherwise + """ + with open(self.path, 'rb') as f: + f.seek(self.backing_file_offset) + content = f.read(self.backing_file_size) + path = content.decode() + if len(path) == 0: + return None + return path + + @asyncio.coroutine + def rebase(self, qemu_img, base_image): + """ + Rebase a linked clone in order to use the correct disk + + :param qemu_img: Path to the qemu-img binary + :param base_image: Path to the base image + """ + if not os.path.exists(base_image): + raise FileNotFoundError(base_image) + command = [qemu_img, "rebase", "-u", "-b", base_image, self.path] + process = yield from asyncio.create_subprocess_exec(*command) + retcode = yield from process.wait() + if retcode != 0: + raise Qcow2Error("Could not rebase the image") + self._reload() diff --git a/tests/modules/qemu/test_qcow2.py b/tests/modules/qemu/test_qcow2.py new file mode 100644 index 00000000..d193c625 --- /dev/null +++ b/tests/modules/qemu/test_qcow2.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python +# +# Copyright (C) 2016 GNS3 Technologies Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import pytest +import shutil +import asyncio + +from gns3server.modules.qemu.qcow2 import Qcow2,Qcow2Error + + +def qemu_img(): + """ + Return the path of qemu-img on system. + We can't use shutil.which because for safety reason we break + the PATH to avoid test interacting with real binaries + """ + paths = [ + "/usr/bin/qemu-img", + "/usr/local/bin/qemu-img" + ] + for path in paths: + if os.path.exists(path): + return path + return None + + +def test_valid_base_file(): + qcow2 = Qcow2("tests/resources/empty8G.qcow2") + assert qcow2.version == 3 + assert qcow2.backing_file is None + + +def test_valid_linked_file(): + qcow2 = Qcow2("tests/resources/linked.qcow2") + assert qcow2.version == 3 + assert qcow2.backing_file == "empty8G.qcow2" + + +def test_invalid_file(): + with pytest.raises(Qcow2Error): + Qcow2("tests/resources/nvram_iou") + + +@pytest.mark.skipif(qemu_img() is None, reason="qemu-img is not available") +def test_rebase(tmpdir, loop): + shutil.copy("tests/resources/empty8G.qcow2", str(tmpdir / "empty16G.qcow2")) + shutil.copy("tests/resources/linked.qcow2", str(tmpdir / "linked.qcow2")) + qcow2 = Qcow2(str(tmpdir / "linked.qcow2")) + assert qcow2.version == 3 + assert qcow2.backing_file == "empty8G.qcow2" + loop.run_until_complete(asyncio.async(qcow2.rebase(qemu_img(), str(tmpdir / "empty16G.qcow2")))) + assert qcow2.backing_file == str(tmpdir / "empty16G.qcow2") + diff --git a/tests/resources/empty8G.qcow2 b/tests/resources/empty8G.qcow2 new file mode 100644 index 0000000000000000000000000000000000000000..beec669a27d59f726faf6c523cd713038e151b75 GIT binary patch literal 197120 zcmeIuF%Ezr3;@7_KEPL*IQk3!zq*Jp$i#}nL1-v#LUZSS+!3*q)32>0xqEo?uPfE8 z#d;lvwN$TRhav$21PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72>e4J|6>eO5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7e?M1d4-PaMJ51PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfWTaV2P$L& AcmMzZ literal 0 HcmV?d00001 diff --git a/tests/resources/linked.qcow2 b/tests/resources/linked.qcow2 new file mode 100644 index 0000000000000000000000000000000000000000..8fad54876141b5d0f2fcf682dff153304443385b GIT binary patch literal 197120 zcmeIuF>b;z6aY{gI&`VjBX9zSPMn}sM5|Xeo9Q7}*X!ZmEU&*IEH+hYMjxAW z^HN)kMOjt#E_J*S!*+gXM%8jvl=WBIx9{B9EI)UtnY@j^i}L5?POmZn0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1bQnl=