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 11*786bc225SDaniel P. Berrangéimport stat 129903217aSDaniel P. Berrangéimport subprocess 13f57213f8SDaniel P. Berrangéimport sys 14f57213f8SDaniel P. Berrangéimport unittest 159903217aSDaniel P. Berrangéimport urllib.request 1634b17c0aSThomas Huthfrom time import sleep 179903217aSDaniel P. Berrangéfrom pathlib import Path 189903217aSDaniel P. Berrangéfrom shutil import copyfileobj 199903217aSDaniel P. Berrangé 209903217aSDaniel P. Berrangé 219903217aSDaniel P. Berrangé# Instances of this class must be declared as class level variables 229903217aSDaniel P. Berrangé# starting with a name "ASSET_". This enables the pre-caching logic 239903217aSDaniel P. Berrangé# to easily find all referenced assets and download them prior to 249903217aSDaniel P. Berrangé# execution of the tests. 259903217aSDaniel P. Berrangéclass Asset: 269903217aSDaniel P. Berrangé 279903217aSDaniel P. Berrangé def __init__(self, url, hashsum): 289903217aSDaniel P. Berrangé self.url = url 299903217aSDaniel P. Berrangé self.hash = hashsum 309903217aSDaniel P. Berrangé cache_dir_env = os.getenv('QEMU_TEST_CACHE_DIR') 319903217aSDaniel P. Berrangé if cache_dir_env: 329903217aSDaniel P. Berrangé self.cache_dir = Path(cache_dir_env, "download") 339903217aSDaniel P. Berrangé else: 349903217aSDaniel P. Berrangé self.cache_dir = Path(Path("~").expanduser(), 359903217aSDaniel P. Berrangé ".cache", "qemu", "download") 369903217aSDaniel P. Berrangé self.cache_file = Path(self.cache_dir, hashsum) 379903217aSDaniel P. Berrangé self.log = logging.getLogger('qemu-test') 389903217aSDaniel P. Berrangé 399903217aSDaniel P. Berrangé def __repr__(self): 409903217aSDaniel P. Berrangé return "Asset: url=%s hash=%s cache=%s" % ( 419903217aSDaniel P. Berrangé self.url, self.hash, self.cache_file) 429903217aSDaniel P. Berrangé 439903217aSDaniel P. Berrangé def _check(self, cache_file): 449903217aSDaniel P. Berrangé if self.hash is None: 459903217aSDaniel P. Berrangé return True 469903217aSDaniel P. Berrangé if len(self.hash) == 64: 4705e30321SThomas Huth hl = hashlib.sha256() 489903217aSDaniel P. Berrangé elif len(self.hash) == 128: 4905e30321SThomas Huth hl = hashlib.sha512() 509903217aSDaniel P. Berrangé else: 519903217aSDaniel P. Berrangé raise Exception("unknown hash type") 529903217aSDaniel P. Berrangé 5305e30321SThomas Huth # Calculate the hash of the file: 5405e30321SThomas Huth with open(cache_file, 'rb') as file: 5505e30321SThomas Huth while True: 5605e30321SThomas Huth chunk = file.read(1 << 20) 5705e30321SThomas Huth if not chunk: 5805e30321SThomas Huth break 5905e30321SThomas Huth hl.update(chunk) 6005e30321SThomas Huth 61db17daf8SThomas Huth return self.hash == hl.hexdigest() 629903217aSDaniel P. Berrangé 639903217aSDaniel P. Berrangé def valid(self): 649903217aSDaniel P. Berrangé return self.cache_file.exists() and self._check(self.cache_file) 659903217aSDaniel P. Berrangé 6634b17c0aSThomas Huth def _wait_for_other_download(self, tmp_cache_file): 6734b17c0aSThomas Huth # Another thread already seems to download the asset, so wait until 6834b17c0aSThomas Huth # it is done, while also checking the size to see whether it is stuck 6934b17c0aSThomas Huth try: 7034b17c0aSThomas Huth current_size = tmp_cache_file.stat().st_size 7134b17c0aSThomas Huth new_size = current_size 7234b17c0aSThomas Huth except: 7334b17c0aSThomas Huth if os.path.exists(self.cache_file): 7434b17c0aSThomas Huth return True 7534b17c0aSThomas Huth raise 7634b17c0aSThomas Huth waittime = lastchange = 600 7734b17c0aSThomas Huth while waittime > 0: 7834b17c0aSThomas Huth sleep(1) 7934b17c0aSThomas Huth waittime -= 1 8034b17c0aSThomas Huth try: 8134b17c0aSThomas Huth new_size = tmp_cache_file.stat().st_size 8234b17c0aSThomas Huth except: 8334b17c0aSThomas Huth if os.path.exists(self.cache_file): 8434b17c0aSThomas Huth return True 8534b17c0aSThomas Huth raise 8634b17c0aSThomas Huth if new_size != current_size: 8734b17c0aSThomas Huth lastchange = waittime 8834b17c0aSThomas Huth current_size = new_size 8934b17c0aSThomas Huth elif lastchange - waittime > 90: 9034b17c0aSThomas Huth return False 9134b17c0aSThomas Huth 9234b17c0aSThomas Huth self.log.debug("Time out while waiting for %s!", tmp_cache_file) 9334b17c0aSThomas Huth raise 9434b17c0aSThomas Huth 959903217aSDaniel P. Berrangé def fetch(self): 969903217aSDaniel P. Berrangé if not self.cache_dir.exists(): 979903217aSDaniel P. Berrangé self.cache_dir.mkdir(parents=True, exist_ok=True) 989903217aSDaniel P. Berrangé 999903217aSDaniel P. Berrangé if self.valid(): 1009903217aSDaniel P. Berrangé self.log.debug("Using cached asset %s for %s", 1019903217aSDaniel P. Berrangé self.cache_file, self.url) 1029903217aSDaniel P. Berrangé return str(self.cache_file) 1039903217aSDaniel P. Berrangé 104f57213f8SDaniel P. Berrangé if os.environ.get("QEMU_TEST_NO_DOWNLOAD", False): 105f57213f8SDaniel P. Berrangé raise Exception("Asset cache is invalid and downloads disabled") 106f57213f8SDaniel P. Berrangé 1079903217aSDaniel P. Berrangé self.log.info("Downloading %s to %s...", self.url, self.cache_file) 1089903217aSDaniel P. Berrangé tmp_cache_file = self.cache_file.with_suffix(".download") 1099903217aSDaniel P. Berrangé 11034b17c0aSThomas Huth for retries in range(3): 1119903217aSDaniel P. Berrangé try: 11234b17c0aSThomas Huth with tmp_cache_file.open("xb") as dst: 11334b17c0aSThomas Huth with urllib.request.urlopen(self.url) as resp: 11434b17c0aSThomas Huth copyfileobj(resp, dst) 11534b17c0aSThomas Huth break 11634b17c0aSThomas Huth except FileExistsError: 11734b17c0aSThomas Huth self.log.debug("%s already exists, " 11834b17c0aSThomas Huth "waiting for other thread to finish...", 11934b17c0aSThomas Huth tmp_cache_file) 12034b17c0aSThomas Huth if self._wait_for_other_download(tmp_cache_file): 12134b17c0aSThomas Huth return str(self.cache_file) 12234b17c0aSThomas Huth self.log.debug("%s seems to be stale, " 12334b17c0aSThomas Huth "deleting and retrying download...", 12434b17c0aSThomas Huth tmp_cache_file) 12534b17c0aSThomas Huth tmp_cache_file.unlink() 12634b17c0aSThomas Huth continue 1279903217aSDaniel P. Berrangé except Exception as e: 1289903217aSDaniel P. Berrangé self.log.error("Unable to download %s: %s", self.url, e) 1299903217aSDaniel P. Berrangé tmp_cache_file.unlink() 1309903217aSDaniel P. Berrangé raise 13134b17c0aSThomas Huth 1329903217aSDaniel P. Berrangé try: 1339903217aSDaniel P. Berrangé # Set these just for informational purposes 1349903217aSDaniel P. Berrangé os.setxattr(str(tmp_cache_file), "user.qemu-asset-url", 1359903217aSDaniel P. Berrangé self.url.encode('utf8')) 1369903217aSDaniel P. Berrangé os.setxattr(str(tmp_cache_file), "user.qemu-asset-hash", 1379903217aSDaniel P. Berrangé self.hash.encode('utf8')) 1389903217aSDaniel P. Berrangé except Exception as e: 1399903217aSDaniel P. Berrangé self.log.debug("Unable to set xattr on %s: %s", tmp_cache_file, e) 1409903217aSDaniel P. Berrangé pass 1419903217aSDaniel P. Berrangé 1429903217aSDaniel P. Berrangé if not self._check(tmp_cache_file): 1439903217aSDaniel P. Berrangé tmp_cache_file.unlink() 1449903217aSDaniel P. Berrangé raise Exception("Hash of %s does not match %s" % 1459903217aSDaniel P. Berrangé (self.url, self.hash)) 1469903217aSDaniel P. Berrangé tmp_cache_file.replace(self.cache_file) 147*786bc225SDaniel P. Berrangé # Remove write perms to stop tests accidentally modifying them 148*786bc225SDaniel P. Berrangé os.chmod(self.cache_file, stat.S_IRUSR | stat.S_IRGRP) 1499903217aSDaniel P. Berrangé 1509903217aSDaniel P. Berrangé self.log.info("Cached %s at %s" % (self.url, self.cache_file)) 1519903217aSDaniel P. Berrangé return str(self.cache_file) 152f57213f8SDaniel P. Berrangé 153f57213f8SDaniel P. Berrangé def precache_test(test): 154f57213f8SDaniel P. Berrangé log = logging.getLogger('qemu-test') 155f57213f8SDaniel P. Berrangé log.setLevel(logging.DEBUG) 156f57213f8SDaniel P. Berrangé handler = logging.StreamHandler(sys.stdout) 157f57213f8SDaniel P. Berrangé handler.setLevel(logging.DEBUG) 158f57213f8SDaniel P. Berrangé formatter = logging.Formatter( 159f57213f8SDaniel P. Berrangé '%(asctime)s - %(name)s - %(levelname)s - %(message)s') 160f57213f8SDaniel P. Berrangé handler.setFormatter(formatter) 161f57213f8SDaniel P. Berrangé log.addHandler(handler) 162f57213f8SDaniel P. Berrangé for name, asset in vars(test.__class__).items(): 163f57213f8SDaniel P. Berrangé if name.startswith("ASSET_") and type(asset) == Asset: 164f57213f8SDaniel P. Berrangé log.info("Attempting to cache '%s'" % asset) 165f57213f8SDaniel P. Berrangé asset.fetch() 166f57213f8SDaniel P. Berrangé log.removeHandler(handler) 167f57213f8SDaniel P. Berrangé 168f57213f8SDaniel P. Berrangé def precache_suite(suite): 169f57213f8SDaniel P. Berrangé for test in suite: 170f57213f8SDaniel P. Berrangé if isinstance(test, unittest.TestSuite): 171f57213f8SDaniel P. Berrangé Asset.precache_suite(test) 172f57213f8SDaniel P. Berrangé elif isinstance(test, unittest.TestCase): 173f57213f8SDaniel P. Berrangé Asset.precache_test(test) 174f57213f8SDaniel P. Berrangé 175f57213f8SDaniel P. Berrangé def precache_suites(path, cacheTstamp): 176f57213f8SDaniel P. Berrangé loader = unittest.loader.defaultTestLoader 177f57213f8SDaniel P. Berrangé tests = loader.loadTestsFromNames([path], None) 178f57213f8SDaniel P. Berrangé 179f57213f8SDaniel P. Berrangé with open(cacheTstamp, "w") as fh: 180f57213f8SDaniel P. Berrangé Asset.precache_suite(tests) 181