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 119903217aSDaniel P. Berrangéimport subprocess 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é 429903217aSDaniel P. Berrangé def _check(self, cache_file): 439903217aSDaniel P. Berrangé if self.hash is None: 449903217aSDaniel P. Berrangé return True 459903217aSDaniel P. Berrangé if len(self.hash) == 64: 4605e30321SThomas Huth hl = hashlib.sha256() 479903217aSDaniel P. Berrangé elif len(self.hash) == 128: 4805e30321SThomas Huth hl = hashlib.sha512() 499903217aSDaniel P. Berrangé else: 509903217aSDaniel P. Berrangé raise Exception("unknown hash type") 519903217aSDaniel P. Berrangé 5205e30321SThomas Huth # Calculate the hash of the file: 5305e30321SThomas Huth with open(cache_file, 'rb') as file: 5405e30321SThomas Huth while True: 5505e30321SThomas Huth chunk = file.read(1 << 20) 5605e30321SThomas Huth if not chunk: 5705e30321SThomas Huth break 5805e30321SThomas Huth hl.update(chunk) 5905e30321SThomas Huth 60*db17daf8SThomas Huth return self.hash == hl.hexdigest() 619903217aSDaniel P. Berrangé 629903217aSDaniel P. Berrangé def valid(self): 639903217aSDaniel P. Berrangé return self.cache_file.exists() and self._check(self.cache_file) 649903217aSDaniel P. Berrangé 6534b17c0aSThomas Huth def _wait_for_other_download(self, tmp_cache_file): 6634b17c0aSThomas Huth # Another thread already seems to download the asset, so wait until 6734b17c0aSThomas Huth # it is done, while also checking the size to see whether it is stuck 6834b17c0aSThomas Huth try: 6934b17c0aSThomas Huth current_size = tmp_cache_file.stat().st_size 7034b17c0aSThomas Huth new_size = current_size 7134b17c0aSThomas Huth except: 7234b17c0aSThomas Huth if os.path.exists(self.cache_file): 7334b17c0aSThomas Huth return True 7434b17c0aSThomas Huth raise 7534b17c0aSThomas Huth waittime = lastchange = 600 7634b17c0aSThomas Huth while waittime > 0: 7734b17c0aSThomas Huth sleep(1) 7834b17c0aSThomas Huth waittime -= 1 7934b17c0aSThomas Huth try: 8034b17c0aSThomas Huth new_size = tmp_cache_file.stat().st_size 8134b17c0aSThomas Huth except: 8234b17c0aSThomas Huth if os.path.exists(self.cache_file): 8334b17c0aSThomas Huth return True 8434b17c0aSThomas Huth raise 8534b17c0aSThomas Huth if new_size != current_size: 8634b17c0aSThomas Huth lastchange = waittime 8734b17c0aSThomas Huth current_size = new_size 8834b17c0aSThomas Huth elif lastchange - waittime > 90: 8934b17c0aSThomas Huth return False 9034b17c0aSThomas Huth 9134b17c0aSThomas Huth self.log.debug("Time out while waiting for %s!", tmp_cache_file) 9234b17c0aSThomas Huth raise 9334b17c0aSThomas Huth 949903217aSDaniel P. Berrangé def fetch(self): 959903217aSDaniel P. Berrangé if not self.cache_dir.exists(): 969903217aSDaniel P. Berrangé self.cache_dir.mkdir(parents=True, exist_ok=True) 979903217aSDaniel P. Berrangé 989903217aSDaniel P. Berrangé if self.valid(): 999903217aSDaniel P. Berrangé self.log.debug("Using cached asset %s for %s", 1009903217aSDaniel P. Berrangé self.cache_file, self.url) 1019903217aSDaniel P. Berrangé return str(self.cache_file) 1029903217aSDaniel P. Berrangé 103f57213f8SDaniel P. Berrangé if os.environ.get("QEMU_TEST_NO_DOWNLOAD", False): 104f57213f8SDaniel P. Berrangé raise Exception("Asset cache is invalid and downloads disabled") 105f57213f8SDaniel P. Berrangé 1069903217aSDaniel P. Berrangé self.log.info("Downloading %s to %s...", self.url, self.cache_file) 1079903217aSDaniel P. Berrangé tmp_cache_file = self.cache_file.with_suffix(".download") 1089903217aSDaniel P. Berrangé 10934b17c0aSThomas Huth for retries in range(3): 1109903217aSDaniel P. Berrangé try: 11134b17c0aSThomas Huth with tmp_cache_file.open("xb") as dst: 11234b17c0aSThomas Huth with urllib.request.urlopen(self.url) as resp: 11334b17c0aSThomas Huth copyfileobj(resp, dst) 11434b17c0aSThomas Huth break 11534b17c0aSThomas Huth except FileExistsError: 11634b17c0aSThomas Huth self.log.debug("%s already exists, " 11734b17c0aSThomas Huth "waiting for other thread to finish...", 11834b17c0aSThomas Huth tmp_cache_file) 11934b17c0aSThomas Huth if self._wait_for_other_download(tmp_cache_file): 12034b17c0aSThomas Huth return str(self.cache_file) 12134b17c0aSThomas Huth self.log.debug("%s seems to be stale, " 12234b17c0aSThomas Huth "deleting and retrying download...", 12334b17c0aSThomas Huth tmp_cache_file) 12434b17c0aSThomas Huth tmp_cache_file.unlink() 12534b17c0aSThomas Huth continue 1269903217aSDaniel P. Berrangé except Exception as e: 1279903217aSDaniel P. Berrangé self.log.error("Unable to download %s: %s", self.url, e) 1289903217aSDaniel P. Berrangé tmp_cache_file.unlink() 1299903217aSDaniel P. Berrangé raise 13034b17c0aSThomas Huth 1319903217aSDaniel P. Berrangé try: 1329903217aSDaniel P. Berrangé # Set these just for informational purposes 1339903217aSDaniel P. Berrangé os.setxattr(str(tmp_cache_file), "user.qemu-asset-url", 1349903217aSDaniel P. Berrangé self.url.encode('utf8')) 1359903217aSDaniel P. Berrangé os.setxattr(str(tmp_cache_file), "user.qemu-asset-hash", 1369903217aSDaniel P. Berrangé self.hash.encode('utf8')) 1379903217aSDaniel P. Berrangé except Exception as e: 1389903217aSDaniel P. Berrangé self.log.debug("Unable to set xattr on %s: %s", tmp_cache_file, e) 1399903217aSDaniel P. Berrangé pass 1409903217aSDaniel P. Berrangé 1419903217aSDaniel P. Berrangé if not self._check(tmp_cache_file): 1429903217aSDaniel P. Berrangé tmp_cache_file.unlink() 1439903217aSDaniel P. Berrangé raise Exception("Hash of %s does not match %s" % 1449903217aSDaniel P. Berrangé (self.url, self.hash)) 1459903217aSDaniel P. Berrangé tmp_cache_file.replace(self.cache_file) 1469903217aSDaniel P. Berrangé 1479903217aSDaniel P. Berrangé self.log.info("Cached %s at %s" % (self.url, self.cache_file)) 1489903217aSDaniel P. Berrangé return str(self.cache_file) 149f57213f8SDaniel P. Berrangé 150f57213f8SDaniel P. Berrangé def precache_test(test): 151f57213f8SDaniel P. Berrangé log = logging.getLogger('qemu-test') 152f57213f8SDaniel P. Berrangé log.setLevel(logging.DEBUG) 153f57213f8SDaniel P. Berrangé handler = logging.StreamHandler(sys.stdout) 154f57213f8SDaniel P. Berrangé handler.setLevel(logging.DEBUG) 155f57213f8SDaniel P. Berrangé formatter = logging.Formatter( 156f57213f8SDaniel P. Berrangé '%(asctime)s - %(name)s - %(levelname)s - %(message)s') 157f57213f8SDaniel P. Berrangé handler.setFormatter(formatter) 158f57213f8SDaniel P. Berrangé log.addHandler(handler) 159f57213f8SDaniel P. Berrangé for name, asset in vars(test.__class__).items(): 160f57213f8SDaniel P. Berrangé if name.startswith("ASSET_") and type(asset) == Asset: 161f57213f8SDaniel P. Berrangé log.info("Attempting to cache '%s'" % asset) 162f57213f8SDaniel P. Berrangé asset.fetch() 163f57213f8SDaniel P. Berrangé log.removeHandler(handler) 164f57213f8SDaniel P. Berrangé 165f57213f8SDaniel P. Berrangé def precache_suite(suite): 166f57213f8SDaniel P. Berrangé for test in suite: 167f57213f8SDaniel P. Berrangé if isinstance(test, unittest.TestSuite): 168f57213f8SDaniel P. Berrangé Asset.precache_suite(test) 169f57213f8SDaniel P. Berrangé elif isinstance(test, unittest.TestCase): 170f57213f8SDaniel P. Berrangé Asset.precache_test(test) 171f57213f8SDaniel P. Berrangé 172f57213f8SDaniel P. Berrangé def precache_suites(path, cacheTstamp): 173f57213f8SDaniel P. Berrangé loader = unittest.loader.defaultTestLoader 174f57213f8SDaniel P. Berrangé tests = loader.loadTestsFromNames([path], None) 175f57213f8SDaniel P. Berrangé 176f57213f8SDaniel P. Berrangé with open(cacheTstamp, "w") as fh: 177f57213f8SDaniel P. Berrangé Asset.precache_suite(tests) 178