Skip to content

Commit 0862979

Browse files
committed
VMImage Caching Fix
Improve VMImage caching and asset handling functionality. - Enhanced asset.py to handle direct file paths and normal asset names - Better error handling and logging in get_metadata() - Refactored _find_cached_image() to use Asset functionality - Fixed multiple Pylint warnings (too-many-branches, protected-access, etc.) - Improved network error handling in provider selection Signed-off-by: Harvey Lynden <[email protected]>
1 parent 4fad5c6 commit 0862979

File tree

2 files changed

+134
-5
lines changed

2 files changed

+134
-5
lines changed

avocado/utils/asset.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -443,7 +443,9 @@ def fetch(self, timeout=None):
443443

444444
def find_asset_file(self, create_metadata=False):
445445
"""
446-
Search for the asset file in each one of the cache locations
446+
Search for the asset file in each one of the cache locations.
447+
448+
It handles both normal asset names and direct file paths.
447449
448450
:param bool create_metadata: Should this method create the
449451
metadata in case asset file found
@@ -454,6 +456,11 @@ def find_asset_file(self, create_metadata=False):
454456
:raises: OSError
455457
"""
456458

459+
# Try 1: Check if self.name is already a direct file path
460+
if self.name and os.path.isfile(self.name):
461+
return self.name
462+
463+
# Try 2: Search in cache directories using relative_dir
457464
for cache_dir in self.cache_dirs:
458465
cache_dir = os.path.expanduser(cache_dir)
459466
asset_file = os.path.join(cache_dir, self.relative_dir)
@@ -482,20 +489,28 @@ def get_metadata(self):
482489
"""
483490
Returns metadata of the asset if it exists or None.
484491
492+
It handles both normal asset names and direct file paths.
493+
485494
:return: metadata
486495
:rtype: dict or None
487496
"""
488497
try:
489498
asset_file = self.find_asset_file()
490499
except OSError as exc:
500+
LOG.debug("Asset file not found, metadata not available for %s", self.name)
491501
raise OSError("Metadata not available.") from exc
492502

493503
basename = os.path.splitext(asset_file)[0]
494504
metadata_file = f"{basename}_metadata.json"
495505
if os.path.isfile(metadata_file):
496-
with open(metadata_file, "r", encoding="utf-8") as f:
497-
metadata = json.load(f)
498-
return metadata
506+
try:
507+
with open(metadata_file, "r", encoding="utf-8") as f:
508+
metadata = json.load(f)
509+
return metadata
510+
except json.JSONDecodeError as e:
511+
LOG.warning("Invalid JSON in metadata file %s: %s", metadata_file, e)
512+
except OSError as e:
513+
LOG.warning("Cannot read metadata file %s: %s", metadata_file, e)
499514
return None
500515

501516
@property

avocado/utils/vmimage.py

Lines changed: 115 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ def _feed_html_parser(self, url, parser):
119119
data = u.read()
120120
parser.feed(astring.to_text(data, self.HTML_ENCODING))
121121
except HTTPError as exc:
122-
raise ImageProviderError(f"Cannot open {url}") from exc
122+
raise ImageProviderError(f"Network error: Cannot open {url}") from exc
123123

124124
@staticmethod
125125
def get_best_version(versions):
@@ -633,6 +633,109 @@ def _take_snapshot(self):
633633
process.run(cmd)
634634
return new_image
635635

636+
@classmethod
637+
def _find_cached_image( # pylint: disable=too-many-locals
638+
cls,
639+
cache_dirs,
640+
name=None,
641+
version=None,
642+
build=None,
643+
arch=None,
644+
checksum=None,
645+
algorithm=None,
646+
snapshot_dir=None,
647+
):
648+
"""
649+
Find a cached image using asset.py enhanced built-in functionality.
650+
651+
This version uses the enhanced Asset.get_metadata() method which supports
652+
both normal asset names and direct file paths, maximizing utilization of
653+
existing asset.py caching features.
654+
"""
655+
656+
# pylint: disable-next=invalid-name
657+
ARCH_COMPATIBILITY = {
658+
"x86_64": ["x86_64", "amd64", "64"],
659+
"amd64": ["x86_64", "amd64", "64"],
660+
"aarch64": ["aarch64", "arm64"],
661+
"arm64": ["aarch64", "arm64"],
662+
}
663+
compatible_arches = ARCH_COMPATIBILITY.get(arch, [arch]) if arch else []
664+
665+
def matches_image_criteria(metadata, name, version, build, compatible_arches):
666+
"""
667+
Check if metadata matches the specified image criteria.
668+
669+
:param metadata: Asset metadata dictionary
670+
:param name: Expected image name
671+
:param version: Expected image version
672+
:param build: Expected image build
673+
:param compatible_arches: List of compatible architectures
674+
:return: True if metadata matches criteria, False otherwise
675+
"""
676+
677+
if metadata.get("type") != "vmimage":
678+
return False
679+
680+
if name and metadata.get("name", "").lower() != name.lower():
681+
return False
682+
683+
if version and str(metadata.get("version", "")) != str(version):
684+
return False
685+
686+
if build and str(metadata.get("build", "")) != str(build):
687+
return False
688+
689+
if compatible_arches and metadata.get("arch") not in compatible_arches:
690+
return False
691+
692+
return True
693+
694+
# Use Asset.get_all_assets() to find cached assets
695+
for asset_path in asset.Asset.get_all_assets(cache_dirs, sort=False):
696+
try:
697+
temp_asset = asset.Asset(
698+
name=asset_path,
699+
asset_hash=checksum,
700+
algorithm=algorithm,
701+
cache_dirs=cache_dirs,
702+
)
703+
704+
try:
705+
metadata = temp_asset.get_metadata()
706+
if not metadata:
707+
continue
708+
except OSError:
709+
continue
710+
711+
if matches_image_criteria(
712+
metadata, name, version, build, compatible_arches
713+
):
714+
# pylint: disable-next=W0212
715+
if checksum and not temp_asset._verify_hash(asset_path):
716+
LOG.debug("Hash mismatch for cached image: %s", asset_path)
717+
continue
718+
719+
LOG.info("Found matching cached image: %s", asset_path)
720+
return cls(
721+
name=metadata.get("name", name),
722+
url=asset_path,
723+
version=metadata.get("version", version),
724+
arch=metadata.get("arch", arch),
725+
build=metadata.get("build", build),
726+
checksum=checksum,
727+
algorithm=algorithm,
728+
cache_dir=cache_dirs[0],
729+
snapshot_dir=snapshot_dir,
730+
)
731+
732+
except (OSError, ValueError, KeyError, AttributeError) as e:
733+
# Skip assets that can't be processed (common errors during metadata processing)
734+
LOG.debug("Skipping asset %s due to error: %s", asset_path, e)
735+
continue
736+
737+
return None
738+
636739
@classmethod
637740
# pylint: disable=R0913
638741
def from_parameters(
@@ -739,14 +842,25 @@ def get_best_provider(name=None, version=None, build=None, arch=None):
739842
if name == "fedora" and arch in ("ppc64", "ppc64le", "s390x"):
740843
name = "fedorasecondary"
741844

845+
provider_errors = []
742846
for provider in IMAGE_PROVIDERS:
743847
if name is None or name == provider.name.lower():
744848
try:
745849
return provider(**provider_args)
746850
except ImageProviderError as e:
747851
LOG.debug(e)
852+
provider_errors.append(f"{provider.name}: {e}")
853+
except (HTTPError, OSError) as e:
854+
LOG.debug(
855+
"Network error while creating provider %s: %s", provider.name, e
856+
)
857+
# Network errors should raise immediately
858+
raise ImageProviderError(f"Network error: {e}") from e
748859

749860
LOG.debug("Provider for %s not available", name)
861+
if provider_errors:
862+
error_details = "; ".join(provider_errors)
863+
raise ImageProviderError(f"Providers failed - {error_details}")
750864
raise AttributeError("Provider not available")
751865

752866

0 commit comments

Comments
 (0)