Skip to content

Commit

Permalink
Use libdnf5 python bindings for repo sanity check if available
Browse files Browse the repository at this point in the history
Signed-off-by: Mattia Verga <[email protected]>
  • Loading branch information
mattiaverga committed Dec 16, 2024
1 parent 18fa4e6 commit 5055e8c
Show file tree
Hide file tree
Showing 8 changed files with 104 additions and 29 deletions.
16 changes: 12 additions & 4 deletions bodhi-server/bodhi-server.spec
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
%global client_min_version 8.3.0
%global messages_min_version 8.1.1

%bcond libdnf5 %[0%{?fedora} >= 41]

Name: %{pypi_name}
Version: %{pypi_version}
Release: 0%{?dist}
Expand Down Expand Up @@ -66,7 +68,11 @@ updates for a software distribution.
Summary: Bodhi composer backend

Requires: %{py3_dist jinja2}
%if %{with libdnf5}
Requires: python3-bodhi-server+libdnf5 == %{version}-%{release}
%else
Requires: bodhi-server == %{version}-%{release}
%endif
Requires: pungi >= 4.1.20
Requires: python3-createrepo_c
Requires: skopeo
Expand All @@ -75,14 +81,18 @@ Requires: skopeo
The Bodhi composer is the component that publishes Bodhi artifacts to
repositories.

%if %{with libdnf5}
%pyproject_extras_subpkg -n python3-bodhi-server libdnf5
%endif


%prep
%autosetup -n %{src_name}-%{pypi_version}
# Remove bundled egg-info
rm -rf %{pypi_name}.egg-info

%generate_buildrequires
%pyproject_buildrequires
%pyproject_buildrequires %{?_with_libdnf5:-x libdnf5}

# https://docs.fedoraproject.org/en-US/packaging-guidelines/UsersAndGroups/#_dynamic_allocation
cat > %{name}.sysusers << EOF
Expand Down Expand Up @@ -117,9 +127,7 @@ install -pm0644 docs/_build/*.1 %{buildroot}%{_mandir}/man1/
install -p -D -m 0644 %{name}.sysusers %{buildroot}%{_sysusersdir}/%{name}.sysusers

%check
# sanity_checks tests rely on dnf command, but system's dnf cache is not accessible
# from koji
%{pytest} -v -k 'not sanity_check and not TestSanityCheckRepodata'
%{pytest} -v

%pre -n %{pypi_name}
%sysusers_create_compat %{name}.sysusers
Expand Down
87 changes: 63 additions & 24 deletions bodhi-server/bodhi/server/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@
from bodhi.server.config import config
from bodhi.server.exceptions import RepodataException

try:
import libdnf5
use_libdnf5 = True
except ImportError:
use_libdnf5 = False

_ = TranslationStringFactory('bodhi')

Expand Down Expand Up @@ -354,29 +359,36 @@ def sanity_check_repodata(myurl, repo_type, drpms=True):
if not ret:
raise RepodataException('updateinfo.xml.gz contains empty ID tags')

# Now call out to DNF to check if the repo is usable
# "tests" is a list of tuples with (dnf args, expected output) to run.
# For every test, DNF is run with the arguments, and if the expected output is not found,
# an error is raised.
tests = []

if repo_type in ('yum', 'source'):
tests.append((['list', '--available'], 'testrepo'))
else: # repo_type == 'module', verified above
tests.append((['module', 'list'], '.*'))

for test in tests:
dnfargs, expout = test

# Make sure every DNF test runs in a new temp dir
testdir = tempfile.mkdtemp(dir=tmpdir)
output = sanity_check_repodata_dnf(testdir, myurl, *dnfargs)
if (expout == ".*" and len(output.strip()) != 0) or (expout in output):
continue
else:
raise RepodataException(
"DNF did not return expected output when running test!"
+ f" Test: {dnfargs}, expected: {expout}, output: {output}")
if use_libdnf5:
try:
testdir = tempfile.mkdtemp(dir=tmpdir)
load_repo_libdnf5(testdir, myurl)
except Exception as e:
raise RepodataException(f'Error loading the repository: {e}')
else:
# Now call out to DNF to check if the repo is usable
# "tests" is a list of tuples with (dnf args, expected output) to run.
# For every test, DNF is run with the arguments, and if the expected output
# is not found, an error is raised.
tests = []

if repo_type in ('yum', 'source'):
tests.append((['list', '--available'], 'testrepo'))
else: # repo_type == 'module', verified above
tests.append((['module', 'list'], '.*'))

for test in tests:
dnfargs, expout = test

# Make sure every DNF test runs in a new temp dir
testdir = tempfile.mkdtemp(dir=tmpdir)
output = sanity_check_repodata_dnf(testdir, myurl, *dnfargs)
if (expout == ".*" and len(output.strip()) != 0) or (expout in output):
continue
else:
raise RepodataException(
"DNF did not return expected output when running test!"
+ f" Test: {dnfargs}, expected: {expout}, output: {output}")


def sanity_check_repodata_dnf(tempdir, myurl, *dnf_args):
Expand All @@ -394,7 +406,7 @@ def sanity_check_repodata_dnf(tempdir, myurl, *dnf_args):
Raises:
Exception: If the repodata is not valid or does not exist.
"""
cmd = ['dnf',
cmd = ['dnf4',
'--disablerepo=*',
f'--repofrompath=testrepo,{myurl}',
'--enablerepo=testrepo',
Expand All @@ -406,6 +418,33 @@ def sanity_check_repodata_dnf(tempdir, myurl, *dnf_args):
return subprocess.check_output(cmd, encoding='utf-8', stderr=subprocess.STDOUT)


def load_repo_libdnf5(tempdir, myurl):
"""
Use libdnf5 python bindings to try to load a repository.
Args:
tempdir (str): Temporary directory for libdnf cache.
myurl (str): A path to a repodata directory.
Raises:
Exception: If the repodata is not valid or does not exist.
"""
base = libdnf5.base.Base()
base_config = base.get_config()
base_config.plugins = False
base_config.cachedir = tempdir
base.setup()
repo_sack = base.get_repo_sack()
repo = repo_sack.create_repo("testrepo")
repo.get_config().baseurl = myurl
repo_sack.load_repos(libdnf5.repo.Repo.Type_AVAILABLE)
query = libdnf5.repo.RepoQuery(base)
query.filter_enabled(True)
repos = [r.get_id() for r in query]
assert len(repos) == 1
assert repos[0] == 'testrepo'
return True


def age(context, date, only_distance=False):
"""
Return a human readable age since the given date.
Expand Down
4 changes: 4 additions & 0 deletions bodhi-server/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ Markdown = ">=3.3.6"
munch = ">=2.5.0"
koji = ">=1.27.1"
libcomps ="^0.1.20"
libdnf5 = {version = "^5.2", optional = true}
packaging = ">=21.3"
prometheus-client = ">=0.13.1"
psycopg2 = ">=2.8.6"
Expand All @@ -117,6 +118,9 @@ SQLAlchemy = ">=1.4, <2.1"
waitress = ">=1.4.4"
zstandard = "^0.21 || ^0.22.0 || ^0.23.0"

[tool.poetry.extras]
libdnf5 = ["libdnf5"]

[tool.pytest.ini_options]
addopts = "--cov-config .coveragerc --cov=bodhi --cov-report term --cov-report xml --cov-report html"
testpaths = ["tests"]
Expand Down
22 changes: 21 additions & 1 deletion bodhi-server/tests/test_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import os
import shutil
import subprocess
import sys
import tempfile

from munch import munchify
Expand Down Expand Up @@ -465,6 +466,17 @@ def test_correct_yum_repo_with_xz_compress(self):
# No exception should be raised here.
util.sanity_check_repodata(self.tempdir, repo_type='yum', drpms=True)

@mock.patch('bodhi.server.util.load_repo_libdnf5', side_effect=Exception("Exception message"))
def test_invalid_repo_exception(self, *args):
"""An exception should be raised if repo data is corrupted."""
pytest.importorskip('libdnf5', reason='This tests correct behavior with libdnf5 '
'which is not installed')
base.mkmetadatadir(self.tempdir, compress_type='xz')

with pytest.raises(util.RepodataException) as exc:
util.sanity_check_repodata(self.tempdir, repo_type='yum', drpms=True)
assert str(exc.value) == "Error loading the repository: Exception message"

def test_correct_yum_repo_with_gz_compress(self):
"""No Exception should be raised if the repo is normal.
Expand Down Expand Up @@ -535,16 +547,24 @@ def _mkmetadatadir_w_modules(self):
root.remove(data)
repomd_tree.write(repomd_path, encoding='UTF-8', xml_declaration=True)

@pytest.mark.skipif(
"libdnf5" in sys.modules,
reason='This can only be tested if lidnf5 is not installed'
)
@mock.patch('subprocess.check_output', return_value='Some output')
def test_correct_module_repo(self, *args):
"""No Exception should be raised if the repo is a normal module repo."""
self._mkmetadatadir_w_modules()
# No exception should be raised here.
util.sanity_check_repodata(self.tempdir, repo_type='module', drpms=True)

@pytest.mark.skipif(
"libdnf5" in sys.modules,
reason='This can only be tested if lidnf5 is not installed'
)
@mock.patch('subprocess.check_output', return_value='')
def test_module_repo_no_dnf_output(self, *args):
"""No Exception should be raised if the repo is a normal module repo."""
"""An Exception should be raised if the repo is invalid module repo."""
self._mkmetadatadir_w_modules()

with pytest.raises(util.RepodataException) as exc:
Expand Down
1 change: 1 addition & 0 deletions devel/ci/Dockerfile-f41
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ RUN dnf --best install -y \
python3-jinja2 \
python3-koji \
python3-libcomps \
python3-libdnf5 \
python3-librepo \
python3-markdown \
python3-munch \
Expand Down
1 change: 1 addition & 0 deletions devel/ci/Dockerfile-pip
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ RUN dnf install -y \
poetry \
postgresql-devel \
python3-devel \
python3-libdnf5 \
python3-librepo \
redhat-rpm-config \
python3-libcomps \
Expand Down
1 change: 1 addition & 0 deletions devel/ci/Dockerfile-rawhide
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ RUN dnf --best install -y \
python3-jinja2 \
python3-koji \
python3-libcomps \
python3-libdnf5 \
python3-librepo \
python3-markdown \
python3-munch \
Expand Down
1 change: 1 addition & 0 deletions news/5820.bug
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Where available, libdnf5 Python bindings are now used in repository sanity checks, otherwise we're forcing dnf-4 usage with the old method

0 comments on commit 5055e8c

Please sign in to comment.