19903217aSDaniel P. Berrangé# Test utilities for fetching & caching assets 29903217aSDaniel P. Berrangé# 39903217aSDaniel P. Berrangé# Copyright 2024 Red Hat, Inc. 49903217aSDaniel P. Berrangé# 59903217aSDaniel P. Berrangé# This work is licensed under the terms of the GNU GPL, version 2 or 69903217aSDaniel P. Berrangé# later. See the COPYING file in the top-level directory. 79903217aSDaniel P. Berrangé 89903217aSDaniel P. Berrangéimport hashlib 99903217aSDaniel P. Berrangéimport logging 109903217aSDaniel P. Berrangéimport os 11786bc225SDaniel P. Berrangéimport stat 12f57213f8SDaniel P. Berrangéimport sys 13f57213f8SDaniel P. Berrangéimport unittest 149903217aSDaniel P. Berrangéimport urllib.request 1534b17c0aSThomas Huthfrom time import sleep 169903217aSDaniel P. Berrangéfrom pathlib import Path 179903217aSDaniel P. Berrangéfrom shutil import copyfileobj 189903217aSDaniel P. Berrangé 199903217aSDaniel P. Berrangé 209903217aSDaniel P. Berrangé# Instances of this class must be declared as class level variables 219903217aSDaniel P. Berrangé# starting with a name "ASSET_". This enables the pre-caching logic 229903217aSDaniel P. Berrangé# to easily find all referenced assets and download them prior to 239903217aSDaniel P. Berrangé# execution of the tests. 249903217aSDaniel P. Berrangéclass Asset: 259903217aSDaniel P. Berrangé 269903217aSDaniel P. Berrangé def __init__(self, url, hashsum): 279903217aSDaniel P. Berrangé self.url = url 289903217aSDaniel P. Berrangé self.hash = hashsum 299903217aSDaniel P. Berrangé cache_dir_env = os.getenv('QEMU_TEST_CACHE_DIR') 309903217aSDaniel P. Berrangé if cache_dir_env: 319903217aSDaniel P. Berrangé self.cache_dir = Path(cache_dir_env, "download") 329903217aSDaniel P. Berrangé else: 339903217aSDaniel P. Berrangé self.cache_dir = Path(Path("~").expanduser(), 349903217aSDaniel P. Berrangé ".cache", "qemu", "download") 359903217aSDaniel P. Berrangé self.cache_file = Path(self.cache_dir, hashsum) 369903217aSDaniel P. Berrangé self.log = logging.getLogger('qemu-test') 379903217aSDaniel P. Berrangé 389903217aSDaniel P. Berrangé def __repr__(self): 399903217aSDaniel P. Berrangé return "Asset: url=%s hash=%s cache=%s" % ( 409903217aSDaniel P. Berrangé self.url, self.hash, self.cache_file) 419903217aSDaniel P. Berrangé 42*c27f452dSDaniel P. Berrangé def __str__(self): 43*c27f452dSDaniel P. Berrangé return str(self.cache_file) 44*c27f452dSDaniel P. Berrangé 459903217aSDaniel P. Berrangé def _check(self, cache_file): 469903217aSDaniel P. Berrangé if self.hash is None: 479903217aSDaniel P. Berrangé return True 489903217aSDaniel P. Berrangé if len(self.hash) == 64: 4905e30321SThomas Huth hl = hashlib.sha256() 509903217aSDaniel P. Berrangé elif len(self.hash) == 128: 5105e30321SThomas Huth hl = hashlib.sha512() 529903217aSDaniel P. Berrangé else: 539903217aSDaniel P. Berrangé raise Exception("unknown hash type") 549903217aSDaniel P. Berrangé 5505e30321SThomas Huth # Calculate the hash of the file: 5605e30321SThomas Huth with open(cache_file, 'rb') as file: 5705e30321SThomas Huth while True: 5805e30321SThomas Huth chunk = file.read(1 << 20) 5905e30321SThomas Huth if not chunk: 6005e30321SThomas Huth break 6105e30321SThomas Huth hl.update(chunk) 6205e30321SThomas Huth 63db17daf8SThomas Huth return self.hash == hl.hexdigest() 649903217aSDaniel P. Berrangé 659903217aSDaniel P. Berrangé def valid(self): 669903217aSDaniel P. Berrangé return self.cache_file.exists() and self._check(self.cache_file) 679903217aSDaniel P. Berrangé 6834b17c0aSThomas Huth def _wait_for_other_download(self, tmp_cache_file): 6934b17c0aSThomas Huth # Another thread already seems to download the asset, so wait until 7034b17c0aSThomas Huth # it is done, while also checking the size to see whether it is stuck 7134b17c0aSThomas Huth try: 7234b17c0aSThomas Huth current_size = tmp_cache_file.stat().st_size 7334b17c0aSThomas Huth new_size = current_size 7434b17c0aSThomas Huth except: 7534b17c0aSThomas Huth if os.path.exists(self.cache_file): 7634b17c0aSThomas Huth return True 7734b17c0aSThomas Huth raise 7834b17c0aSThomas Huth waittime = lastchange = 600 7934b17c0aSThomas Huth while waittime > 0: 8034b17c0aSThomas Huth sleep(1) 8134b17c0aSThomas Huth waittime -= 1 8234b17c0aSThomas Huth try: 8334b17c0aSThomas Huth new_size = tmp_cache_file.stat().st_size 8434b17c0aSThomas Huth except: 8534b17c0aSThomas Huth if os.path.exists(self.cache_file): 8634b17c0aSThomas Huth return True 8734b17c0aSThomas Huth raise 8834b17c0aSThomas Huth if new_size != current_size: 8934b17c0aSThomas Huth lastchange = waittime 9034b17c0aSThomas Huth current_size = new_size 9134b17c0aSThomas Huth elif lastchange - waittime > 90: 9234b17c0aSThomas Huth return False 9334b17c0aSThomas Huth 9434b17c0aSThomas Huth self.log.debug("Time out while waiting for %s!", tmp_cache_file) 9534b17c0aSThomas Huth raise 9634b17c0aSThomas Huth 979903217aSDaniel P. Berrangé def fetch(self): 989903217aSDaniel P. Berrangé if not self.cache_dir.exists(): 999903217aSDaniel P. Berrangé self.cache_dir.mkdir(parents=True, exist_ok=True) 1009903217aSDaniel P. Berrangé 1019903217aSDaniel P. Berrangé if self.valid(): 1029903217aSDaniel P. Berrangé self.log.debug("Using cached asset %s for %s", 1039903217aSDaniel P. Berrangé self.cache_file, self.url) 1049903217aSDaniel P. Berrangé return str(self.cache_file) 1059903217aSDaniel P. Berrangé 106f57213f8SDaniel P. Berrangé if os.environ.get("QEMU_TEST_NO_DOWNLOAD", False): 107f57213f8SDaniel P. Berrangé raise Exception("Asset cache is invalid and downloads disabled") 108f57213f8SDaniel P. Berrangé 1099903217aSDaniel P. Berrangé self.log.info("Downloading %s to %s...", self.url, self.cache_file) 1109903217aSDaniel P. Berrangé tmp_cache_file = self.cache_file.with_suffix(".download") 1119903217aSDaniel P. Berrangé 11234b17c0aSThomas Huth for retries in range(3): 1139903217aSDaniel P. Berrangé try: 11434b17c0aSThomas Huth with tmp_cache_file.open("xb") as dst: 11534b17c0aSThomas Huth with urllib.request.urlopen(self.url) as resp: 11634b17c0aSThomas Huth copyfileobj(resp, dst) 11734b17c0aSThomas Huth break 11834b17c0aSThomas Huth except FileExistsError: 11934b17c0aSThomas Huth self.log.debug("%s already exists, " 12034b17c0aSThomas Huth "waiting for other thread to finish...", 12134b17c0aSThomas Huth tmp_cache_file) 12234b17c0aSThomas Huth if self._wait_for_other_download(tmp_cache_file): 12334b17c0aSThomas Huth return str(self.cache_file) 12434b17c0aSThomas Huth self.log.debug("%s seems to be stale, " 12534b17c0aSThomas Huth "deleting and retrying download...", 12634b17c0aSThomas Huth tmp_cache_file) 12734b17c0aSThomas Huth tmp_cache_file.unlink() 12834b17c0aSThomas Huth continue 1299903217aSDaniel P. Berrangé except Exception as e: 1309903217aSDaniel P. Berrangé self.log.error("Unable to download %s: %s", self.url, e) 1319903217aSDaniel P. Berrangé tmp_cache_file.unlink() 1329903217aSDaniel P. Berrangé raise 13334b17c0aSThomas Huth 1349903217aSDaniel P. Berrangé try: 1359903217aSDaniel P. Berrangé # Set these just for informational purposes 1369903217aSDaniel P. Berrangé os.setxattr(str(tmp_cache_file), "user.qemu-asset-url", 1379903217aSDaniel P. Berrangé self.url.encode('utf8')) 1389903217aSDaniel P. Berrangé os.setxattr(str(tmp_cache_file), "user.qemu-asset-hash", 1399903217aSDaniel P. Berrangé self.hash.encode('utf8')) 1409903217aSDaniel P. Berrangé except Exception as e: 1419903217aSDaniel P. Berrangé self.log.debug("Unable to set xattr on %s: %s", tmp_cache_file, e) 1429903217aSDaniel P. Berrangé pass 1439903217aSDaniel P. Berrangé 1449903217aSDaniel P. Berrangé if not self._check(tmp_cache_file): 1459903217aSDaniel P. Berrangé tmp_cache_file.unlink() 1469903217aSDaniel P. Berrangé raise Exception("Hash of %s does not match %s" % 1479903217aSDaniel P. Berrangé (self.url, self.hash)) 1489903217aSDaniel P. Berrangé tmp_cache_file.replace(self.cache_file) 149786bc225SDaniel P. Berrangé # Remove write perms to stop tests accidentally modifying them 150786bc225SDaniel P. Berrangé os.chmod(self.cache_file, stat.S_IRUSR | stat.S_IRGRP) 1519903217aSDaniel P. Berrangé 1529903217aSDaniel P. Berrangé self.log.info("Cached %s at %s" % (self.url, self.cache_file)) 1539903217aSDaniel P. Berrangé return str(self.cache_file) 154f57213f8SDaniel P. Berrangé 155f57213f8SDaniel P. Berrangé def precache_test(test): 156f57213f8SDaniel P. Berrangé log = logging.getLogger('qemu-test') 157f57213f8SDaniel P. Berrangé log.setLevel(logging.DEBUG) 158f57213f8SDaniel P. Berrangé handler = logging.StreamHandler(sys.stdout) 159f57213f8SDaniel P. Berrangé handler.setLevel(logging.DEBUG) 160f57213f8SDaniel P. Berrangé formatter = logging.Formatter( 161f57213f8SDaniel P. Berrangé '%(asctime)s - %(name)s - %(levelname)s - %(message)s') 162f57213f8SDaniel P. Berrangé handler.setFormatter(formatter) 163f57213f8SDaniel P. Berrangé log.addHandler(handler) 164f57213f8SDaniel P. Berrangé for name, asset in vars(test.__class__).items(): 165f57213f8SDaniel P. Berrangé if name.startswith("ASSET_") and type(asset) == Asset: 166f57213f8SDaniel P. Berrangé log.info("Attempting to cache '%s'" % asset) 167f57213f8SDaniel P. Berrangé asset.fetch() 168f57213f8SDaniel P. Berrangé log.removeHandler(handler) 169f57213f8SDaniel P. Berrangé 170f57213f8SDaniel P. Berrangé def precache_suite(suite): 171f57213f8SDaniel P. Berrangé for test in suite: 172f57213f8SDaniel P. Berrangé if isinstance(test, unittest.TestSuite): 173f57213f8SDaniel P. Berrangé Asset.precache_suite(test) 174f57213f8SDaniel P. Berrangé elif isinstance(test, unittest.TestCase): 175f57213f8SDaniel P. Berrangé Asset.precache_test(test) 176f57213f8SDaniel P. Berrangé 177f57213f8SDaniel P. Berrangé def precache_suites(path, cacheTstamp): 178f57213f8SDaniel P. Berrangé loader = unittest.loader.defaultTestLoader 179f57213f8SDaniel P. Berrangé tests = loader.loadTestsFromNames([path], None) 180f57213f8SDaniel P. Berrangé 181f57213f8SDaniel P. Berrangé with open(cacheTstamp, "w") as fh: 182f57213f8SDaniel P. Berrangé Asset.precache_suite(tests) 183