forked from avocado-framework/avocado
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
1 parent
2337945
commit 7c314dc
Showing
9 changed files
with
419 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,176 @@ | ||
import sys | ||
import traceback | ||
from multiprocessing import set_start_method | ||
import hashlib | ||
import os | ||
|
||
from avocado.core.nrunner.app import BaseRunnerApp | ||
from avocado.core.nrunner.runner import BaseRunner | ||
from avocado.core.utils import messages | ||
from avocado.utils import vmimage | ||
|
||
|
||
class VMImageRunner(BaseRunner): | ||
"""Runner for dependencies of type vmimage | ||
This runner handles downloading and caching of VM images. | ||
Runnable attributes usage: | ||
* kind: 'vmimage' | ||
* uri: not used | ||
* args: not used | ||
* kwargs: | ||
- provider: VM image provider (e.g., 'Fedora') (required) | ||
- version: version of the image (required) | ||
- arch: architecture of the image (required) | ||
""" | ||
|
||
name = "vmimage" | ||
description = "Runner for dependencies of type vmimage" | ||
|
||
def run(self, runnable): | ||
try: | ||
yield messages.StartedMessage.get() | ||
|
||
# Retrieve parameters from kwargs | ||
provider = runnable.kwargs.get("provider") | ||
version = runnable.kwargs.get("version") | ||
arch = runnable.kwargs.get("arch") | ||
|
||
# Validate required parameters | ||
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: | ||
# Get the image object | ||
yield messages.StdoutMessage.get( | ||
f"Getting VM image for {provider} {version} {arch}".encode() | ||
) | ||
|
||
# Download the image | ||
yield messages.StdoutMessage.get( | ||
f"Downloading VM image for: provider={provider}, version={version}, arch={arch}".encode() | ||
) | ||
try: | ||
# Get the image using vmimage utility with configured cache dir | ||
from avocado.core.settings import settings | ||
|
||
cache_dirs = settings.as_dict().get("datadir.paths.cache_dirs") | ||
if not cache_dirs: | ||
raise ValueError("No cache directories configured in settings.") | ||
cache_dir = cache_dirs[0] | ||
|
||
# Validate provider exists | ||
provider_normalized = provider.lower() # Normalize case | ||
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)}" | ||
) | ||
|
||
image = vmimage.Image.from_parameters( | ||
name=provider_normalized, | ||
version=version, | ||
arch=arch, | ||
cache_dir=cache_dir, | ||
) | ||
|
||
# Download and get the image path | ||
image_path = image.get() | ||
if not image_path or not os.path.exists(image_path): | ||
raise RuntimeError( | ||
f"Downloaded image not found at {image_path}" | ||
) | ||
|
||
# Verify the downloaded image | ||
if not os.path.isfile(image_path): | ||
raise RuntimeError(f"Image path is not a file: {image_path}") | ||
if os.path.getsize(image_path) == 0: | ||
raise RuntimeError(f"Image file is empty: {image_path}") | ||
if not image_path.endswith((".qcow2", ".raw")): | ||
raise RuntimeError(f"Unexpected image format: {image_path}") | ||
|
||
# Verify checksum if available | ||
checksum_file = image_path + ".CHECKSUM" | ||
if os.path.exists(checksum_file): | ||
with open(checksum_file, "r", encoding="utf-8") as f: | ||
expected_checksum = ( | ||
f.read().strip().split()[0] | ||
) # Handle 'sha256:...' format | ||
hasher = hashlib.sha256() | ||
with open(image_path, "rb") as f: | ||
while chunk := f.read(4096): | ||
hasher.update(chunk) | ||
actual_checksum = hasher.hexdigest() | ||
if actual_checksum != expected_checksum: | ||
raise RuntimeError( | ||
f"Checksum mismatch for {image_path}\n" | ||
f"Expected: {expected_checksum}\n" | ||
f"Actual: {actual_checksum}" | ||
) | ||
|
||
yield messages.StdoutMessage.get( | ||
f"Successfully downloaded VM image to: {image_path}".encode() | ||
) | ||
yield messages.FinishedMessage.get("pass") | ||
return # Ensure the generator exits after yielding "pass" | ||
|
||
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 # Ensure the generator exits after yielding "error" | ||
|
||
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 # Ensure the generator exits after yielding "error" | ||
|
||
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 # Ensure the generator exits after yielding "error" | ||
|
||
|
||
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
{"kind": "vmimage", "kwargs": {"provider": "fedora", "version": "41", "arch": "x86_64"}} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
#!/usr/bin/env python3 | ||
|
||
import glob | ||
import os | ||
|
||
from avocado import Test | ||
|
||
|
||
class VmimageTest(Test): | ||
""" | ||
Example test using vmimage dependency | ||
:avocado: dependency={"type": "vmimage", "provider": "Fedora", "version": "41", "arch": "x86_64"} | ||
""" | ||
|
||
def test(self): | ||
""" | ||
A test that requires a Fedora VM image | ||
""" | ||
# Reconstruct cache path using same logic as vmimage runner | ||
from avocado.core.settings import settings | ||
|
||
# Get settings | ||
cache_dir = settings.as_dict().get("datadir.paths.cache_dirs")[0] | ||
# Match runner's normalization exactly | ||
version = "41" | ||
arch = "x86_64" | ||
|
||
# Match actual filename pattern with build number | ||
image_filename = f"Fedora-Cloud-Base-Generic-{version}-*.{arch}.qcow2" | ||
|
||
# Search all hash directories for matching image | ||
cache_base = os.path.join(cache_dir, "by_location") | ||
found_files = [] | ||
searched_dirs = [] | ||
|
||
if os.path.exists(cache_base): | ||
for dir_name in os.listdir(cache_base): | ||
dir_path = os.path.join(cache_base, dir_name) | ||
if os.path.isdir(dir_path): | ||
pattern = os.path.join(dir_path, image_filename) | ||
matches = glob.glob(pattern) | ||
found_files.extend(matches) | ||
searched_dirs.append(dir_path) | ||
|
||
self.log.info( | ||
"Searched %d cache directories: %s", | ||
len(searched_dirs), | ||
", ".join(searched_dirs), | ||
) | ||
|
||
self.assertTrue( | ||
len(found_files) > 0, | ||
f"No VM images found matching pattern: {image_filename}\n" | ||
f"Searched directories: {searched_dirs}\n" | ||
f"Cache base path: {cache_base}", | ||
) | ||
self.log.info("VM image validation successful") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.