From b228d194e71da20b5830627c722794249a453a17 Mon Sep 17 00:00:00 2001 From: aniket1260 Date: Tue, 1 Sep 2026 23:47:44 +0530 Subject: [PATCH] kbuild: make network readiness checks bounded (#3197) Signed-off-by: aniket1260 --- config/runtime/base/python.jinja2 | 16 ++++++--- kernelci/kbuild.py | 60 ++++++++++++++++++++----------- tests/test_kbuild.py | 40 +++++++++++++++++++++ 3 files changed, 91 insertions(+), 25 deletions(-) diff --git a/config/runtime/base/python.jinja2 b/config/runtime/base/python.jinja2 index 93fe263a42..892f1b35e3 100644 --- a/config/runtime/base/python.jinja2 +++ b/config/runtime/base/python.jinja2 @@ -161,18 +161,24 @@ class Job(BaseJob): {% block python_main -%} def main(args): # Sometimes container DNS is failing if k8s node is still booting - # This is a workaround: - # test www.google.com until it works or retry 10 times - for _ in range(30): + # Workaround: resolve target KernelCI API host until ready (up to 10 retries) + try: + api_yaml = yaml.safe_load(API_CONFIG_YAML) if isinstance(API_CONFIG_YAML, str) else API_CONFIG_YAML + api_url = (api_yaml.get('url') if isinstance(api_yaml, dict) else None) or 'https://api.kernelci.org' + hostname = urllib.parse.urlparse(api_url).hostname or 'api.kernelci.org' + except Exception: + hostname = 'api.kernelci.org' + + for _ in range(10): try: - socket.gethostbyname('www.google.com') + socket.gethostbyname(hostname) break except socket.gaierror: pass # sleep 1 second time.sleep(1) else: - print("DNS resolution failed, exiting", file=sys.stderr) + print(f"DNS resolution failed for {hostname}, exiting", file=sys.stderr) sys.exit(1) job = Job({% block python_job_constr %}api_config=API_CONFIG_YAML, node=NODE, workspace=WORKSPACE{% endblock %}) diff --git a/kernelci/kbuild.py b/kernelci/kbuild.py index 0708fb7354..55a4503173 100644 --- a/kernelci/kbuild.py +++ b/kernelci/kbuild.py @@ -574,24 +574,42 @@ def _getcrosfragment(self, fragment): return (buffer, fragment) - def _verify_network(self): - """Verify network connectivity""" - # TBD: Different URL? pool of urls? - retries = 10 + def _verify_network(self, url=None, max_retries=5, retry_delay=5, timeout=10): + """Verify network connectivity to a KernelCI target service""" + target_url = url + if not target_url: + if hasattr(self, "_api_config") and self._api_config and getattr(self._api_config, "url", None): + target_url = self._api_config.url + elif hasattr(self, "_context") and self._context: + target_url = getattr(self._context, "get_api_url", lambda: None)() + if not target_url: + target_url = "https://api.kernelci.org" + + retries = max_retries + start_time = time.time() while retries > 0: try: - r = requests.get("https://google.com") + r = requests.get(target_url, timeout=timeout) + if r.status_code == 200: + return + print( + f"[_verify_network] Non-200 response ({r.status_code}) " + f"from {target_url}" + ) except Exception as e: - print(f"[_verify_network] Error: {e}") - time.sleep(5) - retries -= 1 - continue - if r.status_code == 200: - return - time.sleep(5) + print(f"[_verify_network] Error reaching {target_url}: {e}") - print("[_verify_network] Network is not available") - sys.exit(1) + retries -= 1 + if retries > 0: + time.sleep(retry_delay) + + elapsed = int(time.time() - start_time) + msg = ( + f"[_verify_network] Network readiness check failed for {target_url} " + f"after {elapsed}s" + ) + print(msg, file=sys.stderr) + raise RuntimeError(msg) def extract_config(self, frag): """Extract config fragments from legacy config file""" @@ -1372,7 +1390,7 @@ def upload_artifacts(self): for file in files: file_rel = os.path.relpath( os.path.join(root, file), self._af_dir - ) + ).replace("\\", "/") artifact_path = os.path.join(self._af_dir, file_rel) upload_tasks.append((file_rel, artifact_path)) else: @@ -1382,7 +1400,8 @@ def upload_artifacts(self): upload_tasks.append((artifact, artifact_path)) def is_dtb_artifact(artifact): - return artifact.startswith("dtbs/") and artifact.endswith(".dtb") + posix_art = artifact.replace("\\", "/") + return posix_art.startswith("dtbs/") and posix_art.endswith(".dtb") dtb_tasks = [task for task in upload_tasks if is_dtb_artifact(task[0])] dtbs_archive_task = next( @@ -1466,22 +1485,23 @@ def process_and_upload_artifact( dtb_urls = storage.upload_archive( dtbs_archive_task[1], [ - (artifact_path, artifact) + (artifact_path, artifact.replace("\\", "/")) for artifact, artifact_path in dtb_tasks ], root_path, archive_name=dtbs_archive_task[0], ) for artifact, _artifact_path in dtb_tasks: - stored_url = dtb_urls.get(artifact) + art_posix = artifact.replace("\\", "/") + stored_url = dtb_urls.get(art_posix) or dtb_urls.get(artifact) if not stored_url: failed_uploads.append( (artifact, "missing URL after archive upload") ) continue successful_uploads += 1 - self._full_artifacts[artifact] = stored_url - artifact_key = self.map_artifact_name(artifact) + self._full_artifacts[art_posix] = stored_url + artifact_key = self.map_artifact_name(art_posix) with node_af_lock: node_af[artifact_key] = stored_url except Exception as e: diff --git a/tests/test_kbuild.py b/tests/test_kbuild.py index c92db7515c..f28b7883b4 100644 --- a/tests/test_kbuild.py +++ b/tests/test_kbuild.py @@ -5,6 +5,10 @@ import os import sys import types +from unittest import mock + +import pytest +import requests from kernelci.kbuild import KBuild @@ -214,3 +218,39 @@ def test_archive_dropped_when_no_dtbs_built(self, tmp_path): kbuild._package_dtbs() kbuild.verify_build() assert "dtbs.tar.xz" not in kbuild._artifacts + + +class TestVerifyNetwork: + def test_verify_network_success_immediate(self, tmp_path): + kbuild = _kbuild(tmp_path) + mock_response = mock.Mock(status_code=200) + with mock.patch("kernelci.kbuild.requests.get", return_value=mock_response) as mock_get: + kbuild._verify_network(url="https://api.staging.kernelci.org", max_retries=3, retry_delay=0) + mock_get.assert_called_once_with("https://api.staging.kernelci.org", timeout=10) + + def test_verify_network_retry_then_success(self, tmp_path): + kbuild = _kbuild(tmp_path) + fail_response = mock.Mock(status_code=503) + success_response = mock.Mock(status_code=200) + with mock.patch("kernelci.kbuild.requests.get", side_effect=[fail_response, success_response]) as mock_get: + with mock.patch("time.sleep"): + kbuild._verify_network(url="https://api.kernelci.org", max_retries=3, retry_delay=0) + assert mock_get.call_count == 2 + + def test_verify_network_timeout_and_exhaustion(self, tmp_path): + kbuild = _kbuild(tmp_path) + with mock.patch("kernelci.kbuild.requests.get", side_effect=requests.exceptions.Timeout("Connection timed out")): + with mock.patch("time.sleep"): + with pytest.raises(RuntimeError) as exc_info: + kbuild._verify_network(url="https://custom.target.org", max_retries=2, retry_delay=0) + assert "Network readiness check failed for https://custom.target.org" in str(exc_info.value) + + def test_verify_network_non_200_exhaustion(self, tmp_path): + kbuild = _kbuild(tmp_path) + mock_response = mock.Mock(status_code=500) + with mock.patch("kernelci.kbuild.requests.get", return_value=mock_response): + with mock.patch("time.sleep"): + with pytest.raises(RuntimeError) as exc_info: + kbuild._verify_network(max_retries=2, retry_delay=0) + assert "https://api.kernelci.org" in str(exc_info.value) +