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