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
156 changes: 156 additions & 0 deletions .github/workflows/refresh-embedded-dwarf.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
name: Refresh embedded DWARF for new Ceph releases

# Phase 1 (this file): centos-stream / el9 only. Detects newly-published
# Ceph point releases (quincy / reef / squid / tentacle) on
# download.ceph.com, generates osdtrace + radostrace DWARF JSONs for the
# missing ones inside a disposable centos:stream9 podman container, and
# opens a follow-up PR with the new files.
#
# Phases 2-3 (future): mirror the same detect/generate/PR flow for
# quay.io container-image build-ids, then for Ubuntu / Cloud Archive /
# Debian respin pockets, which need their own discovery and host
# environment. Each phase will be a sibling workflow file in this
# directory so failures are scoped per-distro.

on:
schedule:
# Weekly, Monday 06:00 UTC -- well after Ceph upstream's typical
# Friday/Tuesday point-release cadence.
- cron: '0 6 * * 1'
workflow_dispatch: # also runnable on demand from the Actions UI

permissions:
contents: write
pull-requests: write

jobs:
refresh:
runs-on: ubuntu-24.04
# Worst case: 15 missing versions * ~6 min/version = 90 min for the
# generators alone, plus ~5 min for the host build + ~5 min for the
# final rebuild + PR open. 120 min leaves headroom for slow downloads.
timeout-minutes: 180

steps:
- name: Checkout code and submodules
uses: actions/checkout@v4
with:
submodules: recursive
fetch-depth: 0

- name: Install host build deps + podman
run: |
sudo apt-get update
sudo apt-get install -y g++ clang libelf-dev libc6-dev-i386 \
libdw-dev python3 podman

- name: Detect missing DWARF JSONs (centos-stream / el9)
id: detect
run: |
python3 tools/detect_missing_dwarf.py > /tmp/missing.tsv
echo "==== missing rows ===="
cat /tmp/missing.tsv
echo "===================="
COUNT=$(wc -l < /tmp/missing.tsv | tr -d ' ')
echo "count=$COUNT" >> "$GITHUB_OUTPUT"
echo "Detected $COUNT missing JSON-set(s)."

- name: Exit early if nothing to do
if: steps.detect.outputs.count == '0'
run: |
echo "All upstream RPMs already have embedded DWARF JSONs."
echo "No PR will be opened."

- name: Build cephtrace on the host (used only for the final-rebuild gate)
if: steps.detect.outputs.count != '0'
run: make -j"$(nproc)" all

- name: Generate missing JSONs
if: steps.detect.outputs.count != '0'
id: generate
run: |
: > /tmp/succeeded.tsv
: > /tmp/failed.tsv
while IFS=$'\t' read -r distro tools version pkgver url; do
if ./tools/gen_dwarf_for_version.sh \
"$distro" "$tools" "$version" "$pkgver"; then
printf '%s\t%s\t%s\t%s\n' "$distro" "$tools" "$version" "$pkgver" \
>> /tmp/succeeded.tsv
else
printf '%s\t%s\t%s\t%s\n' "$distro" "$tools" "$version" "$pkgver" \
>> /tmp/failed.tsv
echo "::warning::dwarf generation failed for $version"
fi
done < /tmp/missing.tsv

S=$(wc -l < /tmp/succeeded.tsv | tr -d ' ')
F=$(wc -l < /tmp/failed.tsv | tr -d ' ')
echo "Generation summary: $S succeeded, $F failed."
echo "succeeded=$S" >> "$GITHUB_OUTPUT"
echo "failed=$F" >> "$GITHUB_OUTPUT"

- name: Re-aggregate embedded DWARF header + relink
# This step proves the new JSONs parse cleanly through
# tools/generate_embedded_dwarf.py and that osdtrace + radostrace
# still link with the larger header. Failing here means one of
# the just-generated JSONs is malformed -- we want CI to catch that
# before the PR is opened, not after.
if: steps.generate.outputs.succeeded != '0'
run: |
make clean
make -j"$(nproc)" osdtrace radostrace

- name: Compose pull-request body
if: steps.generate.outputs.succeeded != '0'
run: |
{
echo "## Newly added embedded DWARF JSONs"
echo
echo "| distro | tools | version | pkgver |"
echo "|---|---|---|---|"
while IFS=$'\t' read -r d t v p; do
printf '| %s | %s | %s | `%s` |\n' "$d" "$t" "$v" "$p"
done < /tmp/succeeded.tsv
echo
if [ -s /tmp/failed.tsv ]; then
echo "## Versions that failed to generate"
echo
echo "These will be retried by the next scheduled run."
echo
echo '```'
cat /tmp/failed.tsv
echo '```'
echo
fi
echo "## Verification"
echo "- \`tools/detect_missing_dwarf.py\` identified the rows above"
echo " by probing \`download.ceph.com/rpm-X.Y.Z/el9/x86_64/\`."
echo "- Each JSON was generated inside a disposable"
echo " \`quay.io/centos/centos:stream9\` container with the"
echo " matching ceph-osd + lib*-debuginfo packages installed."
echo "- \`make -j\` re-aggregated the headers and linked"
echo " \`osdtrace\` + \`radostrace\` cleanly."
echo
echo "_Generated by \`.github/workflows/refresh-embedded-dwarf.yaml\` ($(date -u +'%Y-%m-%d %H:%MZ'))._"
} > /tmp/pr_body.md
cat /tmp/pr_body.md

- name: Open pull request
if: steps.generate.outputs.succeeded != '0'
uses: peter-evans/create-pull-request@v6
with:
branch: chore/refresh-embedded-dwarf-${{ github.run_id }}
delete-branch: true
title: "chore: refresh embedded DWARF for new Ceph releases"
commit-message: |
chore: refresh embedded DWARF for new Ceph point releases

Auto-generated by the refresh-embedded-dwarf workflow.
See PR body for the list of versions added.
body-path: /tmp/pr_body.md
labels: |
dwarf-refresh
automated
add-paths: |
files/centos-stream/osdtrace/*.json
files/centos-stream/radostrace/*.json
135 changes: 135 additions & 0 deletions tools/detect_missing_dwarf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
#!/usr/bin/env python3
"""Detect Ceph point releases for which we don't yet ship embedded DWARF JSONs.

Phase 1: centos-stream el9 only. Probes download.ceph.com for the ceph-osd
RPMs of each (major.2.patch) candidate version and diffs against the JSONs
already present under files/centos-stream/{osdtrace,radostrace}/.

Output (one row per (version, missing-tool-list)) is TSV on stdout so the
companion shell driver can read it line-by-line:

centos-stream osdtrace,radostrace 17.2.4 2:17.2.4-0.el9 https://download.ceph.com/rpm-17.2.4/el9/x86_64/ceph-osd-17.2.4-0.el9.x86_64.rpm

The columns are: distro, comma-joined-tool-list, upstream-version,
package-version-string (matches what `osdtrace -j` records as the JSON's
`version` field), and the RPM URL the row's existence was inferred from
(included for traceability / debuggability of CI runs).
"""

from __future__ import annotations

import sys
import urllib.request
from pathlib import Path

REPO_ROOT = Path(__file__).resolve().parent.parent
CENTOS_DIR = REPO_ROOT / "files" / "centos-stream"
DOWNLOAD_BASE = "https://download.ceph.com"

# We only care about the modern lines (quincy, reef, squid, tentacle).
# Each major has a single (X.2) minor line.
MAJOR_VERSIONS = [17, 18, 19, 20]

# Centos-stream el9 ceph-osd RPM URL template.
RPM_URL_TMPL = "{base}/rpm-{ver}/el9/x86_64/ceph-osd-{ver}-0.el9.x86_64.rpm"

# Probe up to this many patch releases per major. 20 is generous; quincy
# topped out at 17.2.9 and the longest historical Ceph line (octopus) ran
# through 15.2.17 so this leaves plenty of headroom.
CANDIDATE_PATCHES = list(range(0, 20))


def head(url: str, *, timeout: float = 15.0) -> int:
"""HTTP HEAD returning the status code, or 0 on network error.

Used to probe whether a given RPM URL exists; HEAD is much cheaper than
GET and download.ceph.com supports it. A 0 return means we treat the
URL as unavailable -- safer than retrying the workflow with a partial
discovery on a flaky run.
"""
req = urllib.request.Request(url, method="HEAD")
try:
with urllib.request.urlopen(req, timeout=timeout) as r:
return r.status
except Exception:
return 0
Comment on lines +50 to +55


def upstream_el9_versions() -> list[str]:
"""Versions that have an el9 ceph-osd RPM published upstream.

Probes every (major.2.0 .. major.2.19) combination; cheap (~80 HEAD
requests, ~10 s total) and avoids fragile HTML scraping of the directory
index. Returns a sorted (version-tuple-ascending) list.
"""
out: list[str] = []
for maj in MAJOR_VERSIONS:
for patch in CANDIDATE_PATCHES:
ver = f"{maj}.2.{patch}"
url = RPM_URL_TMPL.format(base=DOWNLOAD_BASE, ver=ver)
if head(url) == 200:
out.append(ver)
return out


def existing_versions(tool: str) -> set[str]:
"""Versions already covered by JSONs under files/centos-stream/<tool>/."""
d = CENTOS_DIR / tool
if not d.is_dir():
return set()
prefix = {"osdtrace": "osd-2:", "radostrace": "rados-2:"}[tool]
suffix = "-0.el9_dwarf.json"
return {
name[len(prefix):-len(suffix)]
for name in (p.name for p in d.iterdir())
if name.startswith(prefix) and name.endswith(suffix)
}


def version_key(v: str) -> tuple[int, ...]:
return tuple(int(x) for x in v.split("."))


def main() -> None:
upstream = upstream_el9_versions()
if not upstream:
# Treat a fully-empty probe set as a hard error: it almost always
# means download.ceph.com is unreachable from the runner, and
# opening a PR that deletes nothing is harmless but auto-merging
# against an empty diff would be misleading.
print("ERROR: no upstream RPMs detected; aborting", file=sys.stderr)
sys.exit(1)
Comment on lines +93 to +101

osd_have = existing_versions("osdtrace")
rados_have = existing_versions("radostrace")

# Group missing-tool sets by version so one container session can
# generate both JSONs for the same version.
missing: dict[str, list[str]] = {}
for ver in upstream:
tools: list[str] = []
if ver not in osd_have:
tools.append("osdtrace")
if ver not in rados_have:
tools.append("radostrace")
if tools:
missing[ver] = tools

for ver in sorted(missing, key=version_key):
tools = missing[ver]
url = RPM_URL_TMPL.format(base=DOWNLOAD_BASE, ver=ver)
print(
"\t".join(
[
"centos-stream",
",".join(sorted(tools)),
ver,
f"2:{ver}-0.el9",
url,
]
)
)


if __name__ == "__main__":
main()
Loading
Loading