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(a)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(a)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(a)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(a)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