This is an automated email from the ASF dual-hosted git repository.
sbp pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-release.git
The following commit(s) were added to refs/heads/main by this push:
new aca2188 Allow admins to start the key import script, and improve
logging
aca2188 is described below
commit aca2188365983dcfbaaeff13b19eb5a6fbc8daa9
Author: Sean B. Palmer <[email protected]>
AuthorDate: Tue Jun 17 14:53:35 2025 +0100
Allow admins to start the key import script, and improve logging
---
atr/blueprints/admin/admin.py | 102 ++++++++++++------------
atr/blueprints/admin/templates/update-keys.html | 7 +-
scripts/keys_import.py | 57 ++++++++++---
3 files changed, 105 insertions(+), 61 deletions(-)
diff --git a/atr/blueprints/admin/admin.py b/atr/blueprints/admin/admin.py
index 14f769e..eaf36f8 100644
--- a/atr/blueprints/admin/admin.py
+++ b/atr/blueprints/admin/admin.py
@@ -21,6 +21,7 @@ import logging
import os
import pathlib
import statistics
+import sys
import time
from collections.abc import Callable, Mapping
from typing import Any, Final
@@ -359,27 +360,29 @@ async def admin_env() -> quart.wrappers.response.Response:
@admin.BLUEPRINT.route("/keys/update", methods=["GET", "POST"])
async def admin_keys_update() -> str | response.Response | tuple[Mapping[str,
Any], int]:
"""Update keys from remote data."""
- if quart.request.method == "POST":
- try:
- added_count, updated_count = await _update_keys()
- return {
- "message": f"Successfully added {added_count} and updated
{updated_count} keys",
- "category": "success",
- }, 200
- except httpx.RequestError as e:
- return {
- "message": f"Failed to fetch data: {e!s}",
- "category": "error",
- }, 200
- except Exception as e:
- return {
- "message": f"Failed to update projects: {e!s}",
- "category": "error",
- }, 200
+ if quart.request.method != "POST":
+ empty_form = await util.EmptyForm.create_form()
+ # Get the previous output from the log file
+ log_path = pathlib.Path("keys_import.log")
+ if not await aiofiles.os.path.exists(log_path):
+ previous_output = None
+ else:
+ async with aiofiles.open(log_path) as f:
+ previous_output = await f.read()
+ return await template.render("update-keys.html",
empty_form=empty_form, previous_output=previous_output)
- # For GET requests, show the update form
- empty_form = await util.EmptyForm.create_form()
- return await template.render("update-keys.html", empty_form=empty_form)
+ try:
+ pid = await _update_keys()
+ return {
+ "message": f"Successfully started key update process with PID
{pid}",
+ "category": "success",
+ }, 200
+ except Exception as e:
+ _LOGGER.exception("Failed to start key update process")
+ return {
+ "message": f"Failed to update keys: {e!s}",
+ "category": "error",
+ }, 200
@admin.BLUEPRINT.route("/performance")
@@ -797,38 +800,37 @@ async def _update_committees(
return added_count, updated_count
-async def _update_keys() -> tuple[int, int]:
- import httpx
+async def _update_keys() -> int:
+ async def _log_process(process: asyncio.subprocess.Process) -> None:
+ try:
+ stdout, stderr = await process.communicate()
+ if stdout:
+ _LOGGER.info(f"keys_import.py
stdout:\n{stdout.decode('utf-8')[:1000]}")
+ if stderr:
+ _LOGGER.error(f"keys_import.py
stderr:\n{stderr.decode('utf-8')[:1000]}")
+ except Exception:
+ _LOGGER.exception("Error reading from subprocess for
keys_import.py")
- successes = 0
- failures = 0
- async with db.session() as data:
- # Get all committees
- committees = await data.committee().all()
- for committee in committees:
- # Get the KEYS file
- async with httpx.AsyncClient() as client:
- response = await
client.get(f"https://downloads.apache.org/{committee.name}/KEYS")
- try:
- response.raise_for_status()
- except httpx.HTTPStatusError as e:
- _LOGGER.error(f"Failed to fetch KEYS file for
{committee.name}: {e!s}")
- continue
- keys_data = await response.aread()
- keys_text = keys_data.decode("utf-8", errors="replace")
- # results, success_count, error_count, submitted_committees
- try:
- _result, yes, no, _committees = await interaction.upload_keys(
- [committee.name], keys_text, [committee.name]
- )
- except interaction.InteractionError as e:
- _LOGGER.error(f"Failed to update keys for {committee.name}:
{e!s}")
- continue
- _LOGGER.info(f"Updated keys for {committee.name}: {yes} successes,
{no} failures")
- successes += yes
- failures += no
+ app = asfquart.APP
+ if not hasattr(app, "background_tasks"):
+ app.background_tasks = set()
+
+ if await aiofiles.os.path.exists("../Dockerfile.alpine"):
+ # Not in a container, developing locally
+ command = ["poetry", "run", "python3", "scripts/keys_import.py"]
+ else:
+ # In a container
+ command = [sys.executable, "scripts/keys_import.py"]
+
+ process = await asyncio.create_subprocess_exec(
+ *command, stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE, cwd=".."
+ )
+
+ task = asyncio.create_task(_log_process(process))
+ app.background_tasks.add(task)
+ task.add_done_callback(app.background_tasks.discard)
- return successes, failures
+ return process.pid
async def _update_metadata() -> tuple[int, int]:
diff --git a/atr/blueprints/admin/templates/update-keys.html
b/atr/blueprints/admin/templates/update-keys.html
index 6fa65fb..4c23eed 100644
--- a/atr/blueprints/admin/templates/update-keys.html
+++ b/atr/blueprints/admin/templates/update-keys.html
@@ -88,9 +88,14 @@
<form action="javascript:submitForm().then(_ => { return false; })">
{{ empty_form.hidden_tag() }}
- <button type="submit" id="submitButton">Update projects</button>
+ <button type="submit" id="submitButton">Update keys</button>
</form>
+ {% if previous_output %}
+ <h2>Previous output</h2>
+ <pre>{{ previous_output }}</pre>
+ {% endif %}
+
<script>
const submitForm = async () => {
const button = document.getElementById("submitButton");
diff --git a/scripts/keys_import.py b/scripts/keys_import.py
index a86ed74..7e3f73c 100644
--- a/scripts/keys_import.py
+++ b/scripts/keys_import.py
@@ -2,6 +2,8 @@
# Usage: poetry run python3 scripts/keys_import.py
import asyncio
+import contextlib
+import os
import sys
import time
@@ -15,7 +17,7 @@ import atr.ldap as ldap
import atr.util as util
-def get(entry, prop):
+def get(entry: dict, prop: str) -> str | None:
if prop in entry:
values = entry[prop]
if values:
@@ -23,9 +25,35 @@ def get(entry, prop):
return None
-async def amain():
+def write(message: str) -> None:
+ print(message)
+ sys.stdout.flush()
+
+
[email protected]
+def log_to_file(conf: config.AppConfig):
+ log_file_path = os.path.join(conf.STATE_DIR, "keys_import.log")
+ # This should not be required
+ os.makedirs(conf.STATE_DIR, exist_ok=True)
+
+ original_stdout = sys.stdout
+ original_stderr = sys.stderr
+ with open(log_file_path, "a") as f:
+ sys.stdout = f
+ sys.stderr = f
+ try:
+ yield
+ finally:
+ sys.stdout = original_stdout
+ sys.stderr = original_stderr
+
+
+async def keys_import(conf: config.AppConfig) -> None:
# Runs as a standalone script, so we need a worker style database
connection
await db.init_database_for_worker()
+ # Print the time and current PID
+ print(f"--- {time.strftime('%Y-%m-%d %H:%M:%S')} by pid {os.getpid()} ---")
+ sys.stdout.flush()
# Get all email addresses in LDAP
# We'll discard them when we're finished
@@ -41,7 +69,7 @@ async def amain():
start = time.perf_counter_ns()
await asyncio.to_thread(ldap.search, ldap_params)
end = time.perf_counter_ns()
- print("LDAP search took", (end - start) / 1000000, "ms")
+ write(f"LDAP search took {(end - start) / 1000000} ms")
# Map the LDAP addresses to Apache UIDs
email_to_uid = {}
@@ -53,7 +81,7 @@ async def amain():
email_to_uid[alt_email] = uid
if committer_email := get(entry, "asf-committer-email"):
email_to_uid[committer_email] = uid
- print("Email addresses from LDAP:", len(email_to_uid))
+ write(f"Email addresses from LDAP: {len(email_to_uid)}")
# Open an ATR database connection
async with db.session() as data:
@@ -68,7 +96,7 @@ async def amain():
# For each remote KEYS file, check that it responded 200 OK
committee_name = url.rsplit("/", 2)[-2]
if status != 200:
- print(committee_name, "error:", status)
+ write(f"{committee_name} error: {status}")
continue
# Parse the KEYS file and add it to the database
@@ -79,18 +107,27 @@ async def amain():
[committee_name], content, [committee_name],
ldap_data=email_to_uid
)
except Exception as e:
- print(committee_name, "error:", e)
+ write(f"{committee_name} error: {e}")
continue
# Print and record the number of keys that were okay and failed
- print(committee_name, yes, no)
+ write(f"{committee_name} {yes} {no}")
total_yes += yes
total_no += no
- print("Total okay:", total_yes)
- print("Total failed:", total_no)
+ write(f"Total okay: {total_yes}")
+ write(f"Total failed: {total_no}")
+ end = time.perf_counter_ns()
+ write(f"Script took {(end - start) / 1000000} ms")
+ write("")
+
+
+async def amain() -> None:
+ conf = config.AppConfig()
+ with log_to_file(conf):
+ await keys_import(conf)
-def main():
+def main() -> None:
asyncio.run(amain())
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]