Skip to content

Commit

Permalink
Add vmimage dependency runner
Browse files Browse the repository at this point in the history
 VM image dependencies in tests

 * Comprehensive functional tests in `runner_vmimage.py`
 * Documentation section in dependencies guide
 * Example test and recipe JSON
 * Integration with resolver and check systems
 * Setup.py entry points for plugin discovery

Reference: avocado-framework#6043
Signed-off-by: Harvey Lynden <[email protected]>
  • Loading branch information
harvey0100 committed Jan 29, 2025
1 parent 0e6226f commit d1b8ec3
Show file tree
Hide file tree
Showing 9 changed files with 406 additions and 3 deletions.
114 changes: 114 additions & 0 deletions avocado/plugins/runners/vmimage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import sys
import traceback
from multiprocessing import set_start_method

from avocado.core.nrunner.app import BaseRunnerApp
from avocado.core.nrunner.runner import BaseRunner
from avocado.core.utils import messages
from avocado.plugins.vmimage import download_image
from avocado.utils import vmimage


class VMImageRunner(BaseRunner):
"""
Runner for dependencies of type vmimage.
This runner uses the vmimage plugin's download_image function which handles:
1. Checking if the image exists in cache
2. Downloading the image if not in cache
3. Storing the image in the configured cache directory
4. Returning the cached image path
"""

name = "vmimage"
description = "Runner for dependencies of type vmimage"

def run(self, runnable):
try:
yield messages.StartedMessage.get()

provider = runnable.kwargs.get("provider")
version = runnable.kwargs.get("version")
arch = runnable.kwargs.get("arch")

if not all([provider, version, arch]):
stderr = "Missing required parameters: provider, version, and arch"
yield messages.StderrMessage.get(stderr.encode())
yield messages.FinishedMessage.get("error")
return

try:
yield messages.StdoutMessage.get(
f"Getting VM image for {provider} {version} {arch}".encode()
)

try:
provider_normalized = provider.lower()
available_providers = [
p.name.lower() for p in vmimage.IMAGE_PROVIDERS
]
if provider_normalized not in available_providers:
raise ValueError(
f"Provider '{provider}' not found. Available providers: {', '.join(available_providers)}"
)

# download_image will use cache if available, otherwise download
image = download_image(provider_normalized, version, arch)
if not image:
raise RuntimeError("Failed to get image")

yield messages.StdoutMessage.get(
f"Successfully retrieved VM image from cache or downloaded to: {image['file']}".encode()
)
yield messages.FinishedMessage.get("pass")
return

except Exception as e:
yield messages.StderrMessage.get(
f"Failed to download image: {str(e)}".encode()
)
yield messages.FinishedMessage.get(
"error",
fail_reason=str(e),
fail_class=e.__class__.__name__,
traceback=traceback.format_exc(),
)
return

except Exception as e:
yield messages.StderrMessage.get(
f"Failed to get/download VM image: {str(e)}".encode()
)
yield messages.FinishedMessage.get(
"error",
fail_reason=str(e),
fail_class=e.__class__.__name__,
traceback=traceback.format_exc(),
)
return

except Exception as e:
yield messages.StderrMessage.get(traceback.format_exc().encode())
yield messages.FinishedMessage.get(
"error",
fail_reason=str(e),
fail_class=e.__class__.__name__,
traceback=traceback.format_exc(),
)
return


class RunnerApp(BaseRunnerApp):
PROG_NAME = "avocado-runner-vmimage"
PROG_DESCRIPTION = "nrunner application for dependencies of type vmimage"
RUNNABLE_KINDS_CAPABLE = ["vmimage"]


def main():
if sys.platform == "darwin":
set_start_method("fork")
app = RunnerApp(print)
app.run()


if __name__ == "__main__":
main()
39 changes: 39 additions & 0 deletions docs/source/guides/user/chapters/dependencies.rst
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,45 @@ effect on the spawner.
* `uri`: the image reference, in any format supported by ``podman
pull`` itself.

VM Image
++++++++

Support downloading virtual machine images ahead of test execution time.
This allows tests to have their required VM images downloaded and cached
before the test execution begins, preventing timeout issues during the
actual test run.

* `type`: `vmimage`
* `provider`: the VM image provider (e.g., 'Fedora')
* `version`: version of the image
* `arch`: architecture of the image

Following is an example of tests using the VM Image dependency that demonstrates
different use cases including multiple dependencies and different providers:

.. literalinclude:: ../../../../../examples/tests/dependency_vmimage.py

To test the VM Image dependency:

1. Clear the cache first::

$ avocado cache clear

2. Run the tests::

$ avocado run examples/tests/dependency_vmimage.py

3. Check the cache to see downloaded images::

$ avocado cache list

4. Run again to verify cache is used::

$ avocado run examples/tests/dependency_vmimage.py

The vmimage runner will download required images on first run and use cached
images on subsequent runs.

Ansible Module
++++++++++++++

Expand Down
1 change: 1 addition & 0 deletions examples/nrunner/recipes/runnable/vmimage_fedora.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"kind": "vmimage", "kwargs": {"provider": "fedora", "version": "41", "arch": "x86_64"}}
85 changes: 85 additions & 0 deletions examples/tests/dependency_vmimage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import os
from avocado import Test


class VmimageTest(Test):
"""
Test demonstrating VM image dependency usage.
The vmimage dependency runner will ensure the required VM image
is downloaded and cached before the test execution begins.
:avocado: dependency={"type": "vmimage", "provider": "fedora", "version": "41", "arch": "s390x"}
"""

def test_vmimage_exists(self):
"""
Verify that the VM image was downloaded by the vmimage runner.
"""
from avocado.core.settings import settings

# Get cache directory from settings
cache_dir = settings.as_dict().get("datadir.paths.cache_dirs")[0]
cache_base = os.path.join(cache_dir, "by_location")

# The image should be in the cache since the runner downloaded it
self.assertTrue(
os.path.exists(cache_base),
f"Cache directory {cache_base} does not exist"
)

# Log cache contents for debugging
self.log.info("Cache directory contents:")
for root, _, files in os.walk(cache_base):
for f in files:
if f.endswith((".qcow2", ".raw")):
self.log.info("Found image: %s", os.path.join(root, f))


class MultiArchVmimageTest(Test):
"""
Test demonstrating multiple VM image dependencies with different architectures.
:avocado: dependency={"type": "vmimage", "provider": "fedora", "version": "41", "arch": "s390x"}
:avocado: dependency={"type": "vmimage", "provider": "fedora", "version": "41", "arch": "x86_64"}
"""

def test_multiple_images(self):
"""
Verify that multiple VM images can be handled by the runner.
"""
self.log.info("Test with multiple VM image dependencies")


class UbuntuVmimageTest(Test):
"""
Test demonstrating VM image dependency with a different provider.
:avocado: dependency={"type": "vmimage", "provider": "ubuntu", "version": "22.04", "arch": "x86_64"}
"""

def test_ubuntu_image(self):
"""
Verify that Ubuntu images can be handled by the runner.
"""
self.log.info("Test with Ubuntu VM image dependency")


# Testing instructions:
#
# 1. Clear the cache first:
# $ avocado cache clear
#
# 2. Run the tests:
# $ avocado run examples/tests/dependency_vmimage.py
#
# 3. Check the cache to see downloaded images:
# $ avocado cache list
#
# 4. Run again to verify cache is used:
# $ avocado run examples/tests/dependency_vmimage.py
#
# The vmimage runner should:
# - Download the required images on first run
# - Use cached images on subsequent runs
# - Handle multiple dependencies per test
# - Support different providers and architectures
1 change: 1 addition & 0 deletions python-avocado.spec
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ PATH=%{buildroot}%{_bindir}:%{buildroot}%{_libexecdir}/avocado:$PATH \
%{_bindir}/avocado-runner-pip
%{_bindir}/avocado-runner-podman-image
%{_bindir}/avocado-runner-sysinfo
%{_bindir}/avocado-runner-vmimage
%{_bindir}/avocado-software-manager
%{_bindir}/avocado-external-runner
%{python3_sitelib}/avocado*
Expand Down
7 changes: 5 additions & 2 deletions selftests/check.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@
"job-api-check-file-exists": 11,
"job-api-check-output-file": 4,
"job-api-check-tmp-directory-exists": 1,
"nrunner-interface": 80,
"nrunner-interface": 90,
"nrunner-requirement": 28,
"unit": 682,
"jobs": 11,
"functional-parallel": 318,
"functional-parallel": 325,
"functional-serial": 7,
"optional-plugins": 0,
"optional-plugins-golang": 2,
Expand Down Expand Up @@ -630,6 +630,9 @@ def create_suites(args): # pylint: disable=W0621
{
"runner": "avocado-runner-pip",
},
{
"runner": "avocado-runner-vmimage",
},
],
}

Expand Down
3 changes: 2 additions & 1 deletion selftests/functional/resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,8 @@ def test_runnables_recipe(self):
package: 1
pip: 1
python-unittest: 1
sysinfo: 1"""
sysinfo: 1
vmimage: 1"""
cmd_line = f"{AVOCADO} -V list {runnables_recipe_path}"
result = process.run(cmd_line)
self.assertIn(
Expand Down
Loading

0 comments on commit d1b8ec3

Please sign in to comment.