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@xxxxxxxxxx> --- 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@xxxxxxxxxx> +# +# 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@xxxxxxxxxx> +# +# 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@xxxxxxxxxx> +# +# 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