1# Test utilities for fetching & caching assets 2# 3# Copyright 2024 Red Hat, Inc. 4# 5# This work is licensed under the terms of the GNU GPL, version 2 or 6# later. See the COPYING file in the top-level directory. 7 8import hashlib 9import logging 10import os 11import subprocess 12import urllib.request 13from pathlib import Path 14from shutil import copyfileobj 15 16 17# Instances of this class must be declared as class level variables 18# starting with a name "ASSET_". This enables the pre-caching logic 19# to easily find all referenced assets and download them prior to 20# execution of the tests. 21class Asset: 22 23 def __init__(self, url, hashsum): 24 self.url = url 25 self.hash = hashsum 26 cache_dir_env = os.getenv('QEMU_TEST_CACHE_DIR') 27 if cache_dir_env: 28 self.cache_dir = Path(cache_dir_env, "download") 29 else: 30 self.cache_dir = Path(Path("~").expanduser(), 31 ".cache", "qemu", "download") 32 self.cache_file = Path(self.cache_dir, hashsum) 33 self.log = logging.getLogger('qemu-test') 34 35 def __repr__(self): 36 return "Asset: url=%s hash=%s cache=%s" % ( 37 self.url, self.hash, self.cache_file) 38 39 def _check(self, cache_file): 40 if self.hash is None: 41 return True 42 if len(self.hash) == 64: 43 sum_prog = 'sha256sum' 44 elif len(self.hash) == 128: 45 sum_prog = 'sha512sum' 46 else: 47 raise Exception("unknown hash type") 48 49 checksum = subprocess.check_output( 50 [sum_prog, str(cache_file)]).split()[0] 51 return self.hash == checksum.decode("utf-8") 52 53 def valid(self): 54 return self.cache_file.exists() and self._check(self.cache_file) 55 56 def fetch(self): 57 if not self.cache_dir.exists(): 58 self.cache_dir.mkdir(parents=True, exist_ok=True) 59 60 if self.valid(): 61 self.log.debug("Using cached asset %s for %s", 62 self.cache_file, self.url) 63 return str(self.cache_file) 64 65 self.log.info("Downloading %s to %s...", self.url, self.cache_file) 66 tmp_cache_file = self.cache_file.with_suffix(".download") 67 68 try: 69 resp = urllib.request.urlopen(self.url) 70 except Exception as e: 71 self.log.error("Unable to download %s: %s", self.url, e) 72 raise 73 74 try: 75 with tmp_cache_file.open("wb+") as dst: 76 copyfileobj(resp, dst) 77 except: 78 tmp_cache_file.unlink() 79 raise 80 try: 81 # Set these just for informational purposes 82 os.setxattr(str(tmp_cache_file), "user.qemu-asset-url", 83 self.url.encode('utf8')) 84 os.setxattr(str(tmp_cache_file), "user.qemu-asset-hash", 85 self.hash.encode('utf8')) 86 except Exception as e: 87 self.log.debug("Unable to set xattr on %s: %s", tmp_cache_file, e) 88 pass 89 90 if not self._check(tmp_cache_file): 91 tmp_cache_file.unlink() 92 raise Exception("Hash of %s does not match %s" % 93 (self.url, self.hash)) 94 tmp_cache_file.replace(self.cache_file) 95 96 self.log.info("Cached %s at %s" % (self.url, self.cache_file)) 97 return str(self.cache_file) 98