Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions config/runtime/base/python.jinja2
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

socket.gethostbyname() has no deadline, so the ten-iteration loop cannot interrupt a stuck DNS lookup

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 %})
Expand Down
60 changes: 40 additions & 20 deletions kernelci/kbuild.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_verify_network() still catches every Exception; malformed URLs and programming/configuration errors become misleading network-readiness failures

"""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)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

timeout=10 is an inactivity timeout, not a total wall-clock deadline

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"""
Expand Down Expand Up @@ -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:
Expand All @@ -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")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The DTB path-normalization changes are unrelated to #3197 and are not explained in the PR description. Please remove them from this PR or submit them separately with their motivation and tests.

Their inclusion also raises concern that the complete patch may not have been carefully reviewed before submission. KernelCI’s contribution policy requires contributors to understand and take responsibility for all submitted changes, and to disclose meaningful AI assistance with an Assisted-by tag. Please review the full diff and disclose any applicable assistance.


dtb_tasks = [task for task in upload_tasks if is_dtb_artifact(task[0])]
dtbs_archive_task = next(
Expand Down Expand Up @@ -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:
Expand Down
40 changes: 40 additions & 0 deletions tests/test_kbuild.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
import os
import sys
import types
from unittest import mock

import pytest
import requests

from kernelci.kbuild import KBuild

Expand Down Expand Up @@ -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)