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 12*f57213f8SDaniel P. Berrangéimport sys 13*f57213f8SDaniel P. Berrangéimport unittest 149903217aSDaniel P. Berrangéimport urllib.request 159903217aSDaniel P. Berrangéfrom pathlib import Path 169903217aSDaniel P. Berrangéfrom shutil import copyfileobj 179903217aSDaniel P. Berrangé 189903217aSDaniel P. Berrangé 199903217aSDaniel P. Berrangé# Instances of this class must be declared as class level variables 209903217aSDaniel P. Berrangé# starting with a name "ASSET_". This enables the pre-caching logic 219903217aSDaniel P. Berrangé# to easily find all referenced assets and download them prior to 229903217aSDaniel P. Berrangé# execution of the tests. 239903217aSDaniel P. Berrangéclass Asset: 249903217aSDaniel P. Berrangé 259903217aSDaniel P. Berrangé def __init__(self, url, hashsum): 269903217aSDaniel P. Berrangé self.url = url 279903217aSDaniel P. Berrangé self.hash = hashsum 289903217aSDaniel P. Berrangé cache_dir_env = os.getenv('QEMU_TEST_CACHE_DIR') 299903217aSDaniel P. Berrangé if cache_dir_env: 309903217aSDaniel P. Berrangé self.cache_dir = Path(cache_dir_env, "download") 319903217aSDaniel P. Berrangé else: 329903217aSDaniel P. Berrangé self.cache_dir = Path(Path("~").expanduser(), 339903217aSDaniel P. Berrangé ".cache", "qemu", "download") 349903217aSDaniel P. Berrangé self.cache_file = Path(self.cache_dir, hashsum) 359903217aSDaniel P. Berrangé self.log = logging.getLogger('qemu-test') 369903217aSDaniel P. Berrangé 379903217aSDaniel P. Berrangé def __repr__(self): 389903217aSDaniel P. Berrangé return "Asset: url=%s hash=%s cache=%s" % ( 399903217aSDaniel P. Berrangé self.url, self.hash, self.cache_file) 409903217aSDaniel P. Berrangé 419903217aSDaniel P. Berrangé def _check(self, cache_file): 429903217aSDaniel P. Berrangé if self.hash is None: 439903217aSDaniel P. Berrangé return True 449903217aSDaniel P. Berrangé if len(self.hash) == 64: 459903217aSDaniel P. Berrangé sum_prog = 'sha256sum' 469903217aSDaniel P. Berrangé elif len(self.hash) == 128: 479903217aSDaniel P. Berrangé sum_prog = 'sha512sum' 489903217aSDaniel P. Berrangé else: 499903217aSDaniel P. Berrangé raise Exception("unknown hash type") 509903217aSDaniel P. Berrangé 519903217aSDaniel P. Berrangé checksum = subprocess.check_output( 529903217aSDaniel P. Berrangé [sum_prog, str(cache_file)]).split()[0] 539903217aSDaniel P. Berrangé return self.hash == checksum.decode("utf-8") 549903217aSDaniel P. Berrangé 559903217aSDaniel P. Berrangé def valid(self): 569903217aSDaniel P. Berrangé return self.cache_file.exists() and self._check(self.cache_file) 579903217aSDaniel P. Berrangé 589903217aSDaniel P. Berrangé def fetch(self): 599903217aSDaniel P. Berrangé if not self.cache_dir.exists(): 609903217aSDaniel P. Berrangé self.cache_dir.mkdir(parents=True, exist_ok=True) 619903217aSDaniel P. Berrangé 629903217aSDaniel P. Berrangé if self.valid(): 639903217aSDaniel P. Berrangé self.log.debug("Using cached asset %s for %s", 649903217aSDaniel P. Berrangé self.cache_file, self.url) 659903217aSDaniel P. Berrangé return str(self.cache_file) 669903217aSDaniel P. Berrangé 67*f57213f8SDaniel P. Berrangé if os.environ.get("QEMU_TEST_NO_DOWNLOAD", False): 68*f57213f8SDaniel P. Berrangé raise Exception("Asset cache is invalid and downloads disabled") 69*f57213f8SDaniel P. Berrangé 709903217aSDaniel P. Berrangé self.log.info("Downloading %s to %s...", self.url, self.cache_file) 719903217aSDaniel P. Berrangé tmp_cache_file = self.cache_file.with_suffix(".download") 729903217aSDaniel P. Berrangé 739903217aSDaniel P. Berrangé try: 749903217aSDaniel P. Berrangé resp = urllib.request.urlopen(self.url) 759903217aSDaniel P. Berrangé except Exception as e: 769903217aSDaniel P. Berrangé self.log.error("Unable to download %s: %s", self.url, e) 779903217aSDaniel P. Berrangé raise 789903217aSDaniel P. Berrangé 799903217aSDaniel P. Berrangé try: 809903217aSDaniel P. Berrangé with tmp_cache_file.open("wb+") as dst: 819903217aSDaniel P. Berrangé copyfileobj(resp, dst) 829903217aSDaniel P. Berrangé except: 839903217aSDaniel P. Berrangé tmp_cache_file.unlink() 849903217aSDaniel P. Berrangé raise 859903217aSDaniel P. Berrangé try: 869903217aSDaniel P. Berrangé # Set these just for informational purposes 879903217aSDaniel P. Berrangé os.setxattr(str(tmp_cache_file), "user.qemu-asset-url", 889903217aSDaniel P. Berrangé self.url.encode('utf8')) 899903217aSDaniel P. Berrangé os.setxattr(str(tmp_cache_file), "user.qemu-asset-hash", 909903217aSDaniel P. Berrangé self.hash.encode('utf8')) 919903217aSDaniel P. Berrangé except Exception as e: 929903217aSDaniel P. Berrangé self.log.debug("Unable to set xattr on %s: %s", tmp_cache_file, e) 939903217aSDaniel P. Berrangé pass 949903217aSDaniel P. Berrangé 959903217aSDaniel P. Berrangé if not self._check(tmp_cache_file): 969903217aSDaniel P. Berrangé tmp_cache_file.unlink() 979903217aSDaniel P. Berrangé raise Exception("Hash of %s does not match %s" % 989903217aSDaniel P. Berrangé (self.url, self.hash)) 999903217aSDaniel P. Berrangé tmp_cache_file.replace(self.cache_file) 1009903217aSDaniel P. Berrangé 1019903217aSDaniel P. Berrangé self.log.info("Cached %s at %s" % (self.url, self.cache_file)) 1029903217aSDaniel P. Berrangé return str(self.cache_file) 103*f57213f8SDaniel P. Berrangé 104*f57213f8SDaniel P. Berrangé def precache_test(test): 105*f57213f8SDaniel P. Berrangé log = logging.getLogger('qemu-test') 106*f57213f8SDaniel P. Berrangé log.setLevel(logging.DEBUG) 107*f57213f8SDaniel P. Berrangé handler = logging.StreamHandler(sys.stdout) 108*f57213f8SDaniel P. Berrangé handler.setLevel(logging.DEBUG) 109*f57213f8SDaniel P. Berrangé formatter = logging.Formatter( 110*f57213f8SDaniel P. Berrangé '%(asctime)s - %(name)s - %(levelname)s - %(message)s') 111*f57213f8SDaniel P. Berrangé handler.setFormatter(formatter) 112*f57213f8SDaniel P. Berrangé log.addHandler(handler) 113*f57213f8SDaniel P. Berrangé for name, asset in vars(test.__class__).items(): 114*f57213f8SDaniel P. Berrangé if name.startswith("ASSET_") and type(asset) == Asset: 115*f57213f8SDaniel P. Berrangé log.info("Attempting to cache '%s'" % asset) 116*f57213f8SDaniel P. Berrangé asset.fetch() 117*f57213f8SDaniel P. Berrangé log.removeHandler(handler) 118*f57213f8SDaniel P. Berrangé 119*f57213f8SDaniel P. Berrangé def precache_suite(suite): 120*f57213f8SDaniel P. Berrangé for test in suite: 121*f57213f8SDaniel P. Berrangé if isinstance(test, unittest.TestSuite): 122*f57213f8SDaniel P. Berrangé Asset.precache_suite(test) 123*f57213f8SDaniel P. Berrangé elif isinstance(test, unittest.TestCase): 124*f57213f8SDaniel P. Berrangé Asset.precache_test(test) 125*f57213f8SDaniel P. Berrangé 126*f57213f8SDaniel P. Berrangé def precache_suites(path, cacheTstamp): 127*f57213f8SDaniel P. Berrangé loader = unittest.loader.defaultTestLoader 128*f57213f8SDaniel P. Berrangé tests = loader.loadTestsFromNames([path], None) 129*f57213f8SDaniel P. Berrangé 130*f57213f8SDaniel P. Berrangé with open(cacheTstamp, "w") as fh: 131*f57213f8SDaniel P. Berrangé Asset.precache_suite(tests) 132