xref: /qemu/tests/functional/qemu_test/asset.py (revision c27f452d61a4741605da2e03c0e6c756dd249f25)
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
11786bc225SDaniel P. Berrangéimport stat
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é
42*c27f452dSDaniel P. Berrangé    def __str__(self):
43*c27f452dSDaniel P. Berrangé        return str(self.cache_file)
44*c27f452dSDaniel P. Berrangé
459903217aSDaniel P. Berrangé    def _check(self, cache_file):
469903217aSDaniel P. Berrangé        if self.hash is None:
479903217aSDaniel P. Berrangé            return True
489903217aSDaniel P. Berrangé        if len(self.hash) == 64:
4905e30321SThomas Huth            hl = hashlib.sha256()
509903217aSDaniel P. Berrangé        elif len(self.hash) == 128:
5105e30321SThomas Huth            hl = hashlib.sha512()
529903217aSDaniel P. Berrangé        else:
539903217aSDaniel P. Berrangé            raise Exception("unknown hash type")
549903217aSDaniel P. Berrangé
5505e30321SThomas Huth        # Calculate the hash of the file:
5605e30321SThomas Huth        with open(cache_file, 'rb') as file:
5705e30321SThomas Huth            while True:
5805e30321SThomas Huth                chunk = file.read(1 << 20)
5905e30321SThomas Huth                if not chunk:
6005e30321SThomas Huth                    break
6105e30321SThomas Huth                hl.update(chunk)
6205e30321SThomas Huth
63db17daf8SThomas Huth        return self.hash == hl.hexdigest()
649903217aSDaniel P. Berrangé
659903217aSDaniel P. Berrangé    def valid(self):
669903217aSDaniel P. Berrangé        return self.cache_file.exists() and self._check(self.cache_file)
679903217aSDaniel P. Berrangé
6834b17c0aSThomas Huth    def _wait_for_other_download(self, tmp_cache_file):
6934b17c0aSThomas Huth        # Another thread already seems to download the asset, so wait until
7034b17c0aSThomas Huth        # it is done, while also checking the size to see whether it is stuck
7134b17c0aSThomas Huth        try:
7234b17c0aSThomas Huth            current_size = tmp_cache_file.stat().st_size
7334b17c0aSThomas Huth            new_size = current_size
7434b17c0aSThomas Huth        except:
7534b17c0aSThomas Huth            if os.path.exists(self.cache_file):
7634b17c0aSThomas Huth                return True
7734b17c0aSThomas Huth            raise
7834b17c0aSThomas Huth        waittime = lastchange = 600
7934b17c0aSThomas Huth        while waittime > 0:
8034b17c0aSThomas Huth            sleep(1)
8134b17c0aSThomas Huth            waittime -= 1
8234b17c0aSThomas Huth            try:
8334b17c0aSThomas Huth                new_size = tmp_cache_file.stat().st_size
8434b17c0aSThomas Huth            except:
8534b17c0aSThomas Huth                if os.path.exists(self.cache_file):
8634b17c0aSThomas Huth                    return True
8734b17c0aSThomas Huth                raise
8834b17c0aSThomas Huth            if new_size != current_size:
8934b17c0aSThomas Huth                lastchange = waittime
9034b17c0aSThomas Huth                current_size = new_size
9134b17c0aSThomas Huth            elif lastchange - waittime > 90:
9234b17c0aSThomas Huth                return False
9334b17c0aSThomas Huth
9434b17c0aSThomas Huth        self.log.debug("Time out while waiting for %s!", tmp_cache_file)
9534b17c0aSThomas Huth        raise
9634b17c0aSThomas Huth
979903217aSDaniel P. Berrangé    def fetch(self):
989903217aSDaniel P. Berrangé        if not self.cache_dir.exists():
999903217aSDaniel P. Berrangé            self.cache_dir.mkdir(parents=True, exist_ok=True)
1009903217aSDaniel P. Berrangé
1019903217aSDaniel P. Berrangé        if self.valid():
1029903217aSDaniel P. Berrangé            self.log.debug("Using cached asset %s for %s",
1039903217aSDaniel P. Berrangé                           self.cache_file, self.url)
1049903217aSDaniel P. Berrangé            return str(self.cache_file)
1059903217aSDaniel P. Berrangé
106f57213f8SDaniel P. Berrangé        if os.environ.get("QEMU_TEST_NO_DOWNLOAD", False):
107f57213f8SDaniel P. Berrangé            raise Exception("Asset cache is invalid and downloads disabled")
108f57213f8SDaniel P. Berrangé
1099903217aSDaniel P. Berrangé        self.log.info("Downloading %s to %s...", self.url, self.cache_file)
1109903217aSDaniel P. Berrangé        tmp_cache_file = self.cache_file.with_suffix(".download")
1119903217aSDaniel P. Berrangé
11234b17c0aSThomas Huth        for retries in range(3):
1139903217aSDaniel P. Berrangé            try:
11434b17c0aSThomas Huth                with tmp_cache_file.open("xb") as dst:
11534b17c0aSThomas Huth                    with urllib.request.urlopen(self.url) as resp:
11634b17c0aSThomas Huth                        copyfileobj(resp, dst)
11734b17c0aSThomas Huth                break
11834b17c0aSThomas Huth            except FileExistsError:
11934b17c0aSThomas Huth                self.log.debug("%s already exists, "
12034b17c0aSThomas Huth                               "waiting for other thread to finish...",
12134b17c0aSThomas Huth                               tmp_cache_file)
12234b17c0aSThomas Huth                if self._wait_for_other_download(tmp_cache_file):
12334b17c0aSThomas Huth                    return str(self.cache_file)
12434b17c0aSThomas Huth                self.log.debug("%s seems to be stale, "
12534b17c0aSThomas Huth                               "deleting and retrying download...",
12634b17c0aSThomas Huth                               tmp_cache_file)
12734b17c0aSThomas Huth                tmp_cache_file.unlink()
12834b17c0aSThomas Huth                continue
1299903217aSDaniel P. Berrangé            except Exception as e:
1309903217aSDaniel P. Berrangé                self.log.error("Unable to download %s: %s", self.url, e)
1319903217aSDaniel P. Berrangé                tmp_cache_file.unlink()
1329903217aSDaniel P. Berrangé                raise
13334b17c0aSThomas Huth
1349903217aSDaniel P. Berrangé        try:
1359903217aSDaniel P. Berrangé            # Set these just for informational purposes
1369903217aSDaniel P. Berrangé            os.setxattr(str(tmp_cache_file), "user.qemu-asset-url",
1379903217aSDaniel P. Berrangé                        self.url.encode('utf8'))
1389903217aSDaniel P. Berrangé            os.setxattr(str(tmp_cache_file), "user.qemu-asset-hash",
1399903217aSDaniel P. Berrangé                        self.hash.encode('utf8'))
1409903217aSDaniel P. Berrangé        except Exception as e:
1419903217aSDaniel P. Berrangé            self.log.debug("Unable to set xattr on %s: %s", tmp_cache_file, e)
1429903217aSDaniel P. Berrangé            pass
1439903217aSDaniel P. Berrangé
1449903217aSDaniel P. Berrangé        if not self._check(tmp_cache_file):
1459903217aSDaniel P. Berrangé            tmp_cache_file.unlink()
1469903217aSDaniel P. Berrangé            raise Exception("Hash of %s does not match %s" %
1479903217aSDaniel P. Berrangé                            (self.url, self.hash))
1489903217aSDaniel P. Berrangé        tmp_cache_file.replace(self.cache_file)
149786bc225SDaniel P. Berrangé        # Remove write perms to stop tests accidentally modifying them
150786bc225SDaniel P. Berrangé        os.chmod(self.cache_file, stat.S_IRUSR | stat.S_IRGRP)
1519903217aSDaniel P. Berrangé
1529903217aSDaniel P. Berrangé        self.log.info("Cached %s at %s" % (self.url, self.cache_file))
1539903217aSDaniel P. Berrangé        return str(self.cache_file)
154f57213f8SDaniel P. Berrangé
155f57213f8SDaniel P. Berrangé    def precache_test(test):
156f57213f8SDaniel P. Berrangé        log = logging.getLogger('qemu-test')
157f57213f8SDaniel P. Berrangé        log.setLevel(logging.DEBUG)
158f57213f8SDaniel P. Berrangé        handler = logging.StreamHandler(sys.stdout)
159f57213f8SDaniel P. Berrangé        handler.setLevel(logging.DEBUG)
160f57213f8SDaniel P. Berrangé        formatter = logging.Formatter(
161f57213f8SDaniel P. Berrangé            '%(asctime)s - %(name)s - %(levelname)s - %(message)s')
162f57213f8SDaniel P. Berrangé        handler.setFormatter(formatter)
163f57213f8SDaniel P. Berrangé        log.addHandler(handler)
164f57213f8SDaniel P. Berrangé        for name, asset in vars(test.__class__).items():
165f57213f8SDaniel P. Berrangé            if name.startswith("ASSET_") and type(asset) == Asset:
166f57213f8SDaniel P. Berrangé                log.info("Attempting to cache '%s'" % asset)
167f57213f8SDaniel P. Berrangé                asset.fetch()
168f57213f8SDaniel P. Berrangé        log.removeHandler(handler)
169f57213f8SDaniel P. Berrangé
170f57213f8SDaniel P. Berrangé    def precache_suite(suite):
171f57213f8SDaniel P. Berrangé        for test in suite:
172f57213f8SDaniel P. Berrangé            if isinstance(test, unittest.TestSuite):
173f57213f8SDaniel P. Berrangé                Asset.precache_suite(test)
174f57213f8SDaniel P. Berrangé            elif isinstance(test, unittest.TestCase):
175f57213f8SDaniel P. Berrangé                Asset.precache_test(test)
176f57213f8SDaniel P. Berrangé
177f57213f8SDaniel P. Berrangé    def precache_suites(path, cacheTstamp):
178f57213f8SDaniel P. Berrangé        loader = unittest.loader.defaultTestLoader
179f57213f8SDaniel P. Berrangé        tests = loader.loadTestsFromNames([path], None)
180f57213f8SDaniel P. Berrangé
181f57213f8SDaniel P. Berrangé        with open(cacheTstamp, "w") as fh:
182f57213f8SDaniel P. Berrangé            Asset.precache_suite(tests)
183