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 15*34b17c0aSThomas 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: 469903217aSDaniel P. Berrangé sum_prog = 'sha256sum' 479903217aSDaniel P. Berrangé elif len(self.hash) == 128: 489903217aSDaniel P. Berrangé sum_prog = 'sha512sum' 499903217aSDaniel P. Berrangé else: 509903217aSDaniel P. Berrangé raise Exception("unknown hash type") 519903217aSDaniel P. Berrangé 529903217aSDaniel P. Berrangé checksum = subprocess.check_output( 539903217aSDaniel P. Berrangé [sum_prog, str(cache_file)]).split()[0] 549903217aSDaniel P. Berrangé return self.hash == checksum.decode("utf-8") 559903217aSDaniel P. Berrangé 569903217aSDaniel P. Berrangé def valid(self): 579903217aSDaniel P. Berrangé return self.cache_file.exists() and self._check(self.cache_file) 589903217aSDaniel P. Berrangé 59*34b17c0aSThomas Huth def _wait_for_other_download(self, tmp_cache_file): 60*34b17c0aSThomas Huth # Another thread already seems to download the asset, so wait until 61*34b17c0aSThomas Huth # it is done, while also checking the size to see whether it is stuck 62*34b17c0aSThomas Huth try: 63*34b17c0aSThomas Huth current_size = tmp_cache_file.stat().st_size 64*34b17c0aSThomas Huth new_size = current_size 65*34b17c0aSThomas Huth except: 66*34b17c0aSThomas Huth if os.path.exists(self.cache_file): 67*34b17c0aSThomas Huth return True 68*34b17c0aSThomas Huth raise 69*34b17c0aSThomas Huth waittime = lastchange = 600 70*34b17c0aSThomas Huth while waittime > 0: 71*34b17c0aSThomas Huth sleep(1) 72*34b17c0aSThomas Huth waittime -= 1 73*34b17c0aSThomas Huth try: 74*34b17c0aSThomas Huth new_size = tmp_cache_file.stat().st_size 75*34b17c0aSThomas Huth except: 76*34b17c0aSThomas Huth if os.path.exists(self.cache_file): 77*34b17c0aSThomas Huth return True 78*34b17c0aSThomas Huth raise 79*34b17c0aSThomas Huth if new_size != current_size: 80*34b17c0aSThomas Huth lastchange = waittime 81*34b17c0aSThomas Huth current_size = new_size 82*34b17c0aSThomas Huth elif lastchange - waittime > 90: 83*34b17c0aSThomas Huth return False 84*34b17c0aSThomas Huth 85*34b17c0aSThomas Huth self.log.debug("Time out while waiting for %s!", tmp_cache_file) 86*34b17c0aSThomas Huth raise 87*34b17c0aSThomas Huth 889903217aSDaniel P. Berrangé def fetch(self): 899903217aSDaniel P. Berrangé if not self.cache_dir.exists(): 909903217aSDaniel P. Berrangé self.cache_dir.mkdir(parents=True, exist_ok=True) 919903217aSDaniel P. Berrangé 929903217aSDaniel P. Berrangé if self.valid(): 939903217aSDaniel P. Berrangé self.log.debug("Using cached asset %s for %s", 949903217aSDaniel P. Berrangé self.cache_file, self.url) 959903217aSDaniel P. Berrangé return str(self.cache_file) 969903217aSDaniel P. Berrangé 97f57213f8SDaniel P. Berrangé if os.environ.get("QEMU_TEST_NO_DOWNLOAD", False): 98f57213f8SDaniel P. Berrangé raise Exception("Asset cache is invalid and downloads disabled") 99f57213f8SDaniel P. Berrangé 1009903217aSDaniel P. Berrangé self.log.info("Downloading %s to %s...", self.url, self.cache_file) 1019903217aSDaniel P. Berrangé tmp_cache_file = self.cache_file.with_suffix(".download") 1029903217aSDaniel P. Berrangé 103*34b17c0aSThomas Huth for retries in range(3): 1049903217aSDaniel P. Berrangé try: 105*34b17c0aSThomas Huth with tmp_cache_file.open("xb") as dst: 106*34b17c0aSThomas Huth with urllib.request.urlopen(self.url) as resp: 107*34b17c0aSThomas Huth copyfileobj(resp, dst) 108*34b17c0aSThomas Huth break 109*34b17c0aSThomas Huth except FileExistsError: 110*34b17c0aSThomas Huth self.log.debug("%s already exists, " 111*34b17c0aSThomas Huth "waiting for other thread to finish...", 112*34b17c0aSThomas Huth tmp_cache_file) 113*34b17c0aSThomas Huth if self._wait_for_other_download(tmp_cache_file): 114*34b17c0aSThomas Huth return str(self.cache_file) 115*34b17c0aSThomas Huth self.log.debug("%s seems to be stale, " 116*34b17c0aSThomas Huth "deleting and retrying download...", 117*34b17c0aSThomas Huth tmp_cache_file) 118*34b17c0aSThomas Huth tmp_cache_file.unlink() 119*34b17c0aSThomas Huth continue 1209903217aSDaniel P. Berrangé except Exception as e: 1219903217aSDaniel P. Berrangé self.log.error("Unable to download %s: %s", self.url, e) 1229903217aSDaniel P. Berrangé tmp_cache_file.unlink() 1239903217aSDaniel P. Berrangé raise 124*34b17c0aSThomas Huth 1259903217aSDaniel P. Berrangé try: 1269903217aSDaniel P. Berrangé # Set these just for informational purposes 1279903217aSDaniel P. Berrangé os.setxattr(str(tmp_cache_file), "user.qemu-asset-url", 1289903217aSDaniel P. Berrangé self.url.encode('utf8')) 1299903217aSDaniel P. Berrangé os.setxattr(str(tmp_cache_file), "user.qemu-asset-hash", 1309903217aSDaniel P. Berrangé self.hash.encode('utf8')) 1319903217aSDaniel P. Berrangé except Exception as e: 1329903217aSDaniel P. Berrangé self.log.debug("Unable to set xattr on %s: %s", tmp_cache_file, e) 1339903217aSDaniel P. Berrangé pass 1349903217aSDaniel P. Berrangé 1359903217aSDaniel P. Berrangé if not self._check(tmp_cache_file): 1369903217aSDaniel P. Berrangé tmp_cache_file.unlink() 1379903217aSDaniel P. Berrangé raise Exception("Hash of %s does not match %s" % 1389903217aSDaniel P. Berrangé (self.url, self.hash)) 1399903217aSDaniel P. Berrangé tmp_cache_file.replace(self.cache_file) 1409903217aSDaniel P. Berrangé 1419903217aSDaniel P. Berrangé self.log.info("Cached %s at %s" % (self.url, self.cache_file)) 1429903217aSDaniel P. Berrangé return str(self.cache_file) 143f57213f8SDaniel P. Berrangé 144f57213f8SDaniel P. Berrangé def precache_test(test): 145f57213f8SDaniel P. Berrangé log = logging.getLogger('qemu-test') 146f57213f8SDaniel P. Berrangé log.setLevel(logging.DEBUG) 147f57213f8SDaniel P. Berrangé handler = logging.StreamHandler(sys.stdout) 148f57213f8SDaniel P. Berrangé handler.setLevel(logging.DEBUG) 149f57213f8SDaniel P. Berrangé formatter = logging.Formatter( 150f57213f8SDaniel P. Berrangé '%(asctime)s - %(name)s - %(levelname)s - %(message)s') 151f57213f8SDaniel P. Berrangé handler.setFormatter(formatter) 152f57213f8SDaniel P. Berrangé log.addHandler(handler) 153f57213f8SDaniel P. Berrangé for name, asset in vars(test.__class__).items(): 154f57213f8SDaniel P. Berrangé if name.startswith("ASSET_") and type(asset) == Asset: 155f57213f8SDaniel P. Berrangé log.info("Attempting to cache '%s'" % asset) 156f57213f8SDaniel P. Berrangé asset.fetch() 157f57213f8SDaniel P. Berrangé log.removeHandler(handler) 158f57213f8SDaniel P. Berrangé 159f57213f8SDaniel P. Berrangé def precache_suite(suite): 160f57213f8SDaniel P. Berrangé for test in suite: 161f57213f8SDaniel P. Berrangé if isinstance(test, unittest.TestSuite): 162f57213f8SDaniel P. Berrangé Asset.precache_suite(test) 163f57213f8SDaniel P. Berrangé elif isinstance(test, unittest.TestCase): 164f57213f8SDaniel P. Berrangé Asset.precache_test(test) 165f57213f8SDaniel P. Berrangé 166f57213f8SDaniel P. Berrangé def precache_suites(path, cacheTstamp): 167f57213f8SDaniel P. Berrangé loader = unittest.loader.defaultTestLoader 168f57213f8SDaniel P. Berrangé tests = loader.loadTestsFromNames([path], None) 169f57213f8SDaniel P. Berrangé 170f57213f8SDaniel P. Berrangé with open(cacheTstamp, "w") as fh: 171f57213f8SDaniel P. Berrangé Asset.precache_suite(tests) 172