From 614c56b05fb4d56b21ff499f7b37db338f659c48 Mon Sep 17 00:00:00 2001 From: Jakub Kadlcik Date: Tue, 15 Oct 2024 12:17:20 +0200 Subject: [PATCH] rpmbuild, frontend: activate Red Hat subscription on demand Fix #2132 --- .../copr_backend/background_worker_build.py | 24 +++-- backend/tests/test_background_worker_build.py | 12 +-- backend/tests/testlib/__init__.py | 5 +- rpmbuild/bin/copr-builder-ready | 94 +++++++++++++++++++ rpmbuild/copr-rpmbuild.spec | 1 + rpmbuild/copr-rpmbuild.yml | 5 + rpmbuild/copr_rpmbuild/config.py | 3 + 7 files changed, 127 insertions(+), 17 deletions(-) create mode 100755 rpmbuild/bin/copr-builder-ready diff --git a/backend/copr_backend/background_worker_build.py b/backend/copr_backend/background_worker_build.py index 0c3645391..5fbca83cf 100644 --- a/backend/copr_backend/background_worker_build.py +++ b/backend/copr_backend/background_worker_build.py @@ -12,6 +12,8 @@ import json import shlex +from subprocess import PIPE +from tempfile import NamedTemporaryFile from datetime import datetime from packaging import version from cachetools.func import ttl_cache @@ -242,21 +244,25 @@ def _check_copr_builder(self): raise BuildRetry("Minimum version for builder is {}" .format(MIN_BUILDER_VERSION)) - def _check_mock_config(self): - config = "/etc/mock/{}.cfg".format(self.job.chroot) - command = "/usr/bin/test -f " + config - if self.job.chroot == "srpm-builds": - return - if self.ssh.run(command): - raise BuildRetry("Chroot config {} not found".format(config)) - def _check_vm(self): """ Check that the VM is OK to start the build """ self.log.info("Checking that builder machine is OK") self._check_copr_builder() - self._check_mock_config() + + # The output won't be live and will appear only after this command + # finishes. Making it live is nontrivial but we have a good code for + # doing so in `resallocserver.manager.run_command`. Praiskup plans to + # generalize it into a separate package that we could eventually use + # here. + cmd = "copr-builder-ready " + self.job.chroot + rc, stdout, stderr = self.ssh.run_expensive( + cmd, subprocess_timeout=660) + self.log.info(stdout) + if rc: + self.log.info(stderr) + raise BuildRetry("Builder wasn't ready, trying a new one") def _fill_build_info_file(self): """ diff --git a/backend/tests/test_background_worker_build.py b/backend/tests/test_background_worker_build.py index 6955b62ed..e50f0e4e7 100644 --- a/backend/tests/test_background_worker_build.py +++ b/backend/tests/test_background_worker_build.py @@ -695,13 +695,13 @@ class _SideEffect: def __call__(self): self.counter += 1 if self.counter == 1: - return (1, "err stdout", "err stderr") - return (0, "", "") + return (1, b"err stdout", "err stderr") + return (0, b"", "") config = f_build_rpm_case ssh = config.ssh - ssh.set_command("/usr/bin/test -f /etc/mock/fedora-30-x86_64.cfg", - 0, "", "", return_action=_SideEffect()) + ssh.set_command("copr-builder-ready fedora-30-x86_64", + 0, b"", "", return_action=_SideEffect()) worker = config.bw worker.process() assert_logs_exist([ @@ -859,8 +859,8 @@ def test_failed_build_retry(f_build_rpm_case, caplog): hosts[index].hostname = "1.2.3." + str(index) rhf.return_value.get_host.side_effect = hosts ssh = config.ssh - ssh.set_command("/usr/bin/test -f /etc/mock/fedora-30-x86_64.cfg", - 1, "", "not found") + ssh.set_command("copr-builder-ready fedora-30-x86_64", + 1, b"", "not found") config.bw.process() assert_logs_exist([ diff --git a/backend/tests/testlib/__init__.py b/backend/tests/testlib/__init__.py index 97cc05ad5..a5818b3b5 100644 --- a/backend/tests/testlib/__init__.py +++ b/backend/tests/testlib/__init__.py @@ -6,6 +6,7 @@ import os import shutil from unittest.mock import MagicMock, patch +from subprocess import PIPE from copr_backend.background_worker_build import COMMANDS from copr_backend.sshcmd import SSHConnection, SSHConnectionError, DEFAULT_SUBPROCESS_TIMEOUT @@ -130,8 +131,8 @@ def __init__(self, user=None, host=None, config_file=None, log=None): self.commands = {} self.set_command(COMMANDS["rpm_q_builder"], 0, "666\n", "") - self.set_command("/usr/bin/test -f /etc/mock/fedora-30-x86_64.cfg", - 0, "", "") + self.set_command("copr-builder-ready fedora-30-x86_64", 0, b"", "") + self.set_command("copr-builder-ready srpm-builds", 0, b"", "") self.set_command("copr-rpmbuild-log", 0, "build log stdout\n", "build log stderr\n") self.resultdir = "fedora-30-x86_64/00848963-example" diff --git a/rpmbuild/bin/copr-builder-ready b/rpmbuild/bin/copr-builder-ready new file mode 100755 index 000000000..3a6772f35 --- /dev/null +++ b/rpmbuild/bin/copr-builder-ready @@ -0,0 +1,94 @@ +#! /usr/bin/python3 + +""" +Final checks that the builder machine is ready to be used + +Everything printed to STDOUT will be redirected to the copr-backend logs, +STDERR will be ignored. +""" + +import os +import sys +import time +from fnmatch import fnmatch +from copr_rpmbuild.config import Config + + +def check_mock_config(chroot): + """ + Does the mock config for this chroot exist? + """ + if chroot == "srpm-builds": + return + + config = "/etc/mock/{}.cfg".format(chroot) + if os.path.isfile(config): + return + + print("Chroot config {} not found".format(config)) + sys.exit(1) + + +def subscription_required(chroot): + """ + Is subscription required for this task? + """ + config = Config() + config.load_config() + + for pattern in config.rhsm: + if fnmatch(chroot, pattern): + return True + return False + + +def active_subscription(): + """ + Is subscription active on this system? + """ + # There are standard-ish ways for checking whether the subscription is + # active. No silver bullet, all of them have trade-offs. + # - Checking the existence of `/etc/pki/consumer/cert.pem` file + # - Checking the existence of `/etc/pki/entitlement/*.pem` files + # - Exit code from `subscription-manager status` + # - Exit code from `subscription-manager identity` + # We don't want to rely on any of them. We use a custom daemon for + # registering the system on the background. Once it is done, + # it creates a file. + return os.path.exists("/run/copr-builder/rhsm-subscribed") + + +def wait_for_subscription(timeout=600): + """ + Wait until this system has an active subscription + + Activating Red Hat subscription may take a lot of time and historically, the + subscription service used to be unreliable, so we should wait for the + subscription only when necessary. + """ + start = time.time() + attempt = 1 + while True: + print("Checking Red Hat subscription (attempt #{0})".format(attempt)) + if active_subscription(): + print("Red Hat subscription active") + return + if time.time() > start + timeout: + print("Waiting for Red Hat subscription timeouted!") + sys.exit(1) + time.sleep(30) + attempt += 1 + + +def main(): + """ + The entrypoint for this script + """ + chroot = sys.argv[1] + check_mock_config(chroot) + if subscription_required(chroot): + wait_for_subscription() + + +if __name__ == "__main__": + main() diff --git a/rpmbuild/copr-rpmbuild.spec b/rpmbuild/copr-rpmbuild.spec index 4a5482bef..90785779f 100644 --- a/rpmbuild/copr-rpmbuild.spec +++ b/rpmbuild/copr-rpmbuild.spec @@ -286,6 +286,7 @@ EOF %_bindir/copr-builder-cleanup %_bindir/copr-builder-rhsm-subscribe %_bindir/copr-builder-rhsm-subscribe-daemon +%_bindir/copr-builder-ready %_sysconfdir/copr-builder %dir %mock_config_overrides %doc %mock_config_overrides/README diff --git a/rpmbuild/copr-rpmbuild.yml b/rpmbuild/copr-rpmbuild.yml index fcfffc104..4c48609ba 100644 --- a/rpmbuild/copr-rpmbuild.yml +++ b/rpmbuild/copr-rpmbuild.yml @@ -12,3 +12,8 @@ # cute # multiline # snippet +# +# Chroots that require active Red Hat subscription +# rhsm: +# - rhel-* +# - epel-* diff --git a/rpmbuild/copr_rpmbuild/config.py b/rpmbuild/copr_rpmbuild/config.py index ebdcee715..0442b31af 100644 --- a/rpmbuild/copr_rpmbuild/config.py +++ b/rpmbuild/copr_rpmbuild/config.py @@ -12,8 +12,10 @@ class Config: """ Configuration class for copr-rpmbuild """ + def __init__(self): self.tags_to_mock_snippet = [] + self.rhsm = [] def load_config(self): """ @@ -27,3 +29,4 @@ def load_config(self): pass self.tags_to_mock_snippet = config_data.get("tags_to_mock_snippet", []) + self.rhsm = config_data.get("rhsm", [])