Skip to content

Commit 9903217

Browse files
berrangehuth
authored andcommitted
tests/functional: add a module for handling asset download & caching
The 'Asset' class is a simple module that declares a downloadable asset that can be cached locally. Downloads are stored in the user's home dir at ~/.cache/qemu/download, using a sha256 sum of the URL. [thuth: Drop sha1 support, use hash on file content for naming instead of URL, add the possibility to specify the cache dir via environment variable] Signed-off-by: Daniel P. Berrangé <[email protected]> Reviewed-by: Philippe Mathieu-Daudé <[email protected]> Tested-by: Philippe Mathieu-Daudé <[email protected]> Message-ID: <[email protected]> Signed-off-by: Thomas Huth <[email protected]>
1 parent eeba3d7 commit 9903217

File tree

2 files changed

+98
-0
lines changed

2 files changed

+98
-0
lines changed

tests/functional/qemu_test/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
# later. See the COPYING file in the top-level directory.
77

88

9+
from .asset import Asset
910
from .config import BUILD_DIR
1011
from .cmd import has_cmd, has_cmds, run_cmd, is_readable_executable_file, \
1112
interrupt_interactive_console_until_pattern, wait_for_console_pattern, \
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
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+
8+
import hashlib
9+
import logging
10+
import os
11+
import subprocess
12+
import urllib.request
13+
from pathlib import Path
14+
from 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.
21+
class 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)

0 commit comments

Comments
 (0)