[libvirt PATCH 1/4] tests: introduce lavocado: initial code structure

Beraldo Leal bleal at redhat.com
Wed Jun 30 16:36:31 UTC 2021


lavocado aims to be an alternative test framework for the libvirt
project using Python, python-libvirt and Avocado. This can be used to
write unit, functional and integration tests and it is inspired by the
libvirt-tck framework.

Documentation, helper classes and templates will be provided to speed
up common test writing scenarios.

Signed-off-by: Beraldo Leal <bleal at redhat.com>
---
 tests/lavocado/lavocado/__init__.py         |   0
 tests/lavocado/lavocado/defaults.py         |  11 ++
 tests/lavocado/lavocado/exceptions.py       |  20 +++
 tests/lavocado/lavocado/helpers/__init__.py |   0
 tests/lavocado/lavocado/helpers/domains.py  |  75 ++++++++++
 tests/lavocado/lavocado/test.py             | 144 ++++++++++++++++++++
 tests/lavocado/requirements.txt             |   3 +
 tests/lavocado/templates/domain.xml.jinja   |  20 +++
 8 files changed, 273 insertions(+)
 create mode 100644 tests/lavocado/lavocado/__init__.py
 create mode 100644 tests/lavocado/lavocado/defaults.py
 create mode 100644 tests/lavocado/lavocado/exceptions.py
 create mode 100644 tests/lavocado/lavocado/helpers/__init__.py
 create mode 100644 tests/lavocado/lavocado/helpers/domains.py
 create mode 100644 tests/lavocado/lavocado/test.py
 create mode 100644 tests/lavocado/requirements.txt
 create mode 100644 tests/lavocado/templates/domain.xml.jinja

diff --git a/tests/lavocado/lavocado/__init__.py b/tests/lavocado/lavocado/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/tests/lavocado/lavocado/defaults.py b/tests/lavocado/lavocado/defaults.py
new file mode 100644
index 0000000000..47f1299cf4
--- /dev/null
+++ b/tests/lavocado/lavocado/defaults.py
@@ -0,0 +1,11 @@
+LIBVIRT_URI = "qemu:///system"
+
+TEMPLATE_PATH = "./templates/domain.xml.jinja"
+
+VMIMAGE = {
+        'provider': 'fedora',
+        'version': '33',
+        'checksum': '67daa956d8c82ef799f8b0a191c1753c9bda3bca'
+        }
+
+CACHE_DIR = '/tmp/lavocado-cache'
diff --git a/tests/lavocado/lavocado/exceptions.py b/tests/lavocado/lavocado/exceptions.py
new file mode 100644
index 0000000000..d89cbb3eef
--- /dev/null
+++ b/tests/lavocado/lavocado/exceptions.py
@@ -0,0 +1,20 @@
+# Copyright (C) 2021 Red Hat, Inc.
+# Author: Beraldo Leal <bleal at redhat.com>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library.  If not, see
+# <http://www.gnu.org/licenses/>.
+
+
+class TestSetupException(Exception):
+    pass
diff --git a/tests/lavocado/lavocado/helpers/__init__.py b/tests/lavocado/lavocado/helpers/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/tests/lavocado/lavocado/helpers/domains.py b/tests/lavocado/lavocado/helpers/domains.py
new file mode 100644
index 0000000000..cddee1b4b7
--- /dev/null
+++ b/tests/lavocado/lavocado/helpers/domains.py
@@ -0,0 +1,75 @@
+# Copyright (C) 2021 Red Hat, Inc.
+# Author: Beraldo Leal <bleal at redhat.com>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library.  If not, see
+# <http://www.gnu.org/licenses/>.
+
+
+import os
+
+from avocado.utils.genio import read_file
+from jinja2 import Template
+
+from lavocado import defaults
+from lavocado.exceptions import TestSetupException
+
+
+class Domain:
+    @classmethod
+    def from_xml_path(cls, conn, xml_path):
+        """Create a new domain from a XML file.
+
+        :param conn: a connection object to the Hypervisor.
+        :type conn: libvirt.virConnect
+        :param xml_path: XML file path.
+        :type xml_path: str
+        :returns: : the created domain object
+        :rtype: libvirt.virDomain
+        """
+        xml_content = read_file(xml_path)
+        return conn.createXML(xml_content)
+
+    @classmethod
+    def from_xml_template(cls, conn, suffix, arguments=None):
+        """Create a new domain from the default XML template.
+
+        This will use the `defaults.TEMPLATE_PATH` file, parsing some arguments
+        defined there.
+
+        :param conn: a connection object to the Hypervisor.
+        :type conn: libvirt.virConnect
+        :param suffix: A suffix string to be added to the domain domain.
+        :type suffix: str
+        :param arguments: a key/value dict to be used during
+                          template parse. currently supported keys are: name,
+                          memory, vcpus, arch, machine and image. Visit the
+                          template file for details.
+        :rtype arguments: dict
+        :returns: : the created domain object
+        :rtype: libvirt.virDomain
+        """
+        template_path = defaults.TEMPLATE_PATH
+        arguments = arguments or {}
+
+        if not os.path.isfile(template_path):
+            error = f"Template {template_path} not found."
+            raise TestSetupException(error)
+
+        # Adding a suffix to the name
+        name = arguments.get('name', 'lavocado-test')
+        arguments['name'] = f"{name}-{suffix}"
+
+        template = Template(read_file(template_path))
+        xml_content = template.render(**arguments)
+        return conn.createXML(xml_content)
diff --git a/tests/lavocado/lavocado/test.py b/tests/lavocado/lavocado/test.py
new file mode 100644
index 0000000000..b77ecaed58
--- /dev/null
+++ b/tests/lavocado/lavocado/test.py
@@ -0,0 +1,144 @@
+# Copyright (C) 2021 Red Hat, Inc.
+# Author: Beraldo Leal <bleal at redhat.com>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library.  If not, see
+# <http://www.gnu.org/licenses/>.
+
+"""Basic test helper module to avoid code redundancy."""
+
+import os
+import libvirt
+
+from avocado import Test
+from avocado.utils import vmimage
+
+from lavocado import defaults
+from lavocado.exceptions import TestSetupException
+from lavocado.helpers.domains import Domain
+
+
+class LibvirtTest(Test):
+    """Main class helper for tests.
+
+    Any test that inherits from this class, will have some methods and
+    properties to assist on their jobs.
+    """
+    def setUp(self):
+        """Setup to be executed before each test.
+
+        Currently, this method is creating just a basic hypervisor connection.
+        Please, extend this method when writing your tests for your own needs.
+
+        Any error that happens here *will not* flag the test as "FAIL", instead
+        tests will be flagged as "ERROR", meaning that some bootstrap error has
+        happened.
+        """
+        self.defaults = defaults
+        self.conn = self.connect()
+
+    def connect(self):
+        """Try to open a new connection with the hypervisor.
+
+        This method uses the value defined at `defaults.LIBVIRT_URI` as URI.
+
+        :returns: a libvirt connection.
+        :rtype: libvirt.virConnect
+        """
+        try:
+            return libvirt.open(self.defaults.LIBVIRT_URI)
+        except libvirt.libvirtError:
+            msg = ("Failed to open connection with the hypervisor using "
+                   + self.defaults.LIBVIRT_URI)
+            self.cancel(msg)
+
+    def create_domain(self, arguments=None):
+        """Creates a libvirt domain based on the default template.
+
+        This will receive some arguments that will be rendered on the
+        template.  For more information about the arguments, see
+        templates/domain.xml.jinja. For now, at least the 'image' argument must
+        be informed, with the path for the image to boot.
+
+        If you are using this method from a test method (different from
+        setUp()), AND you would like to count its call as a "setup/bootstrap"
+        stage, consider using the following Avocado decorator:
+
+        from avocado.core.decorators import cancel_on
+
+        @cancel_on(TestSetupException)
+        def test_foo(self):
+          ...
+
+        In that way, your test will not FAIL, instead it will be cancelled in
+        case of any problem during this bootstrap.
+
+        :param dict arguments: A key,value dictionary with the arguments
+                               to be replaced on the template. If
+                               any missing argument, template will be
+                               rendered with default values.
+        """
+        try:
+            return Domain.from_xml_template(self.conn, self.id(), arguments)
+        # This will catch any avocado exception plus any os error
+        except Exception as ex:
+            msg = f"Failed to create domain: {ex}"
+            raise TestSetupException(msg) from ex
+
+    def get_generic_image(self):
+        """Ask Avocado to fetch an VM image snapshot.
+
+        Avocado will handle if image is already downloaded into the
+        cache dir and also will make sure the checksum is matching.
+
+        This will return an Image object pointing to a snapshot file. So
+        multiple calls of this method will never return the same object.
+
+        If you are using this method from a test method (different from
+        setUp()), AND you would like to count its call as a "setup/bootstrap"
+        stage, consider using the following Avocado decorator:
+
+        from avocado.core.decorators import cancel_on
+
+        @cancel_on(TestSetupException)
+        def test_foo(self):
+          ...
+
+        In that way, your test will not FAIL, instead it will be cancelled in
+        case of any problem during this bootstrap.
+        """
+        image = self.defaults.VMIMAGE
+        try:
+            return vmimage.get(name=image.get('provider'),
+                               version=image.get('version'),
+                               cache_dir=self.defaults.CACHE_DIR,
+                               checksum=image.get('checksum'))
+        # This will catch any error, including avocado exceptions + OS errors
+        except Exception as ex:
+            msg = f"Failed to get a generic image: {ex}"
+            raise TestSetupException(msg) from ex
+
+    def tearDown(self):
+        """Shutdown after each test.
+
+        This will destroy all previously created domains by this test, and
+        remove any image snapshot if created.
+        """
+        for domain in self.conn.listAllDomains():
+            if domain.name().endswith(self.id()):
+                domain.destroy()
+
+        if hasattr(self, 'image') and isinstance(self.image, vmimage.Image):
+            if os.path.exists(self.image.path):
+                os.remove(self.image.path)
+        self.conn.close()
diff --git a/tests/lavocado/requirements.txt b/tests/lavocado/requirements.txt
new file mode 100644
index 0000000000..6927528323
--- /dev/null
+++ b/tests/lavocado/requirements.txt
@@ -0,0 +1,3 @@
+git+https://github.com/avocado-framework/avocado@8c87bfe5e8a1895d77226064433453f158a3ce56#egg=avocado_framework
+libvirt-python
+Jinja2
diff --git a/tests/lavocado/templates/domain.xml.jinja b/tests/lavocado/templates/domain.xml.jinja
new file mode 100644
index 0000000000..a7bd57e5b0
--- /dev/null
+++ b/tests/lavocado/templates/domain.xml.jinja
@@ -0,0 +1,20 @@
+<domain type='kvm'>
+  <name>{{name|default("lavocado-test")}}</name>
+  <memory unit='KiB'>{{memory|default(4194304)}}</memory>
+  <vcpu placement='static'>{{vcpus|default(4)}}</vcpu>
+  <os>
+    <type arch='{{arch|default("x86_64")}}' machine='{{machine|default("q35")}}'>hvm</type>
+    <boot dev='hd'/>
+  </os>
+  <devices>
+    <emulator>/usr/bin/qemu-system-x86_64</emulator>
+    <disk type='file' device='disk'>
+      <driver name='qemu' type='qcow2'/>
+      <source file='{{image}}'/>
+      <target dev='vda' bus='virtio'/>
+    </disk>
+    <console type='pty'>
+      <target type='serial' port='0'/>
+    </console>
+  </devices>
+</domain>
-- 
2.26.3




More information about the libvir-list mailing list