|
| 1 | +from __future__ import annotations |
| 2 | + |
| 3 | +import datetime |
| 4 | +import logging |
| 5 | + |
| 6 | +from fastapi import HTTPException, status |
| 7 | +from pydantic import BaseModel |
| 8 | +from sqlalchemy import select |
| 9 | +from sqlalchemy.exc import NoResultFound |
| 10 | + |
| 11 | +from diracx.core.exceptions import InvalidQueryError |
| 12 | +from diracx.core.properties import OPERATOR, SERVICE_ADMINISTRATOR |
| 13 | +from diracx.db.sql.pilot_agents.schema import PilotAgents |
| 14 | +from diracx.db.sql.utils import BaseSQLDB |
| 15 | + |
| 16 | +from ..dependencies import PilotLogsDB |
| 17 | +from ..fastapi_classes import DiracxRouter |
| 18 | +from ..utils.users import AuthorizedUserInfo |
| 19 | +from .access_policies import ActionType, CheckPilotLogsPolicyCallable |
| 20 | + |
| 21 | +logger = logging.getLogger(__name__) |
| 22 | +router = DiracxRouter() |
| 23 | + |
| 24 | + |
| 25 | +class LogLine(BaseModel): |
| 26 | + line_no: int |
| 27 | + line: str |
| 28 | + |
| 29 | + |
| 30 | +class LogMessage(BaseModel): |
| 31 | + pilot_stamp: str |
| 32 | + lines: list[LogLine] |
| 33 | + vo: str |
| 34 | + |
| 35 | + |
| 36 | +class DateRange(BaseModel): |
| 37 | + min: str | None = None # expects a string in ISO 8601 ("%Y-%m-%dT%H:%M:%S.%f%z") |
| 38 | + max: str | None = None # expects a string in ISO 8601 ("%Y-%m-%dT%H:%M:%S.%f%z") |
| 39 | + |
| 40 | + |
| 41 | +@router.post("/") |
| 42 | +async def send_message( |
| 43 | + data: LogMessage, |
| 44 | + pilot_logs_db: PilotLogsDB, |
| 45 | + check_permissions: CheckPilotLogsPolicyCallable, |
| 46 | +) -> int: |
| 47 | + |
| 48 | + logger.warning(f"Message received '{data}'") |
| 49 | + user_info = await check_permissions(action=ActionType.CREATE) |
| 50 | + pilot_id = 0 # need to get pilot id from pilot_stamp (via PilotAgentsDB) |
| 51 | + # also add a timestamp to be able to select and delete logs based on pilot creation dates, even if corresponding |
| 52 | + # pilots have been already deleted from PilotAgentsDB (so the logs can live longer than pilots). |
| 53 | + submission_time = datetime.datetime.fromtimestamp(0, datetime.timezone.utc) |
| 54 | + pilot_agents_db = BaseSQLDB.available_implementations("PilotAgentsDB")[0] |
| 55 | + url = BaseSQLDB.available_urls()["PilotAgentsDB"] |
| 56 | + db = pilot_agents_db(url) |
| 57 | + |
| 58 | + try: |
| 59 | + async with db.engine_context(): |
| 60 | + async with db: |
| 61 | + stmt = select(PilotAgents.pilot_id, PilotAgents.submission_time).where( |
| 62 | + PilotAgents.pilot_stamp == data.pilot_stamp |
| 63 | + ) |
| 64 | + pilot_id, submission_time = (await db.conn.execute(stmt)).one() |
| 65 | + except NoResultFound as exc: |
| 66 | + logger.error( |
| 67 | + f"Cannot determine PilotID for requested PilotStamp: {data.pilot_stamp}, Error: {exc}." |
| 68 | + ) |
| 69 | + raise HTTPException(status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc |
| 70 | + |
| 71 | + docs = [] |
| 72 | + for line in data.lines: |
| 73 | + docs.append( |
| 74 | + { |
| 75 | + "PilotStamp": data.pilot_stamp, |
| 76 | + "PilotID": pilot_id, |
| 77 | + "SubmissionTime": submission_time, |
| 78 | + "VO": user_info.vo, |
| 79 | + "LineNumber": line.line_no, |
| 80 | + "Message": line.line, |
| 81 | + } |
| 82 | + ) |
| 83 | + await pilot_logs_db.bulk_insert(pilot_logs_db.index_name(pilot_id), docs) |
| 84 | + return pilot_id |
| 85 | + |
| 86 | + |
| 87 | +@router.get("/logs") |
| 88 | +async def get_logs( |
| 89 | + pilot_id: int, |
| 90 | + db: PilotLogsDB, |
| 91 | + check_permissions: CheckPilotLogsPolicyCallable, |
| 92 | +) -> list[dict]: |
| 93 | + |
| 94 | + logger.warning(f"Retrieving logs for pilot ID '{pilot_id}'") |
| 95 | + user_info = await check_permissions(action=ActionType.QUERY) |
| 96 | + |
| 97 | + # here, users with privileged properties will see logs from all VOs. Is it what we want ? |
| 98 | + search_params = [{"parameter": "PilotID", "operator": "eq", "value": pilot_id}] |
| 99 | + if _non_privileged(user_info): |
| 100 | + search_params.append( |
| 101 | + {"parameter": "VO", "operator": "eq", "value": user_info.vo} |
| 102 | + ) |
| 103 | + result = await db.search( |
| 104 | + ["Message"], |
| 105 | + search_params, |
| 106 | + [{"parameter": "LineNumber", "direction": "asc"}], |
| 107 | + ) |
| 108 | + if not result: |
| 109 | + return [{"Message": f"No logs for pilot ID = {pilot_id}"}] |
| 110 | + return result |
| 111 | + |
| 112 | + |
| 113 | +@router.delete("/logs") |
| 114 | +async def delete( |
| 115 | + pilot_id: int, |
| 116 | + data: DateRange, |
| 117 | + db: PilotLogsDB, |
| 118 | + check_permissions: CheckPilotLogsPolicyCallable, |
| 119 | +) -> str: |
| 120 | + """Delete either logs for a specific PilotID or a creation date range. |
| 121 | + Non-privileged users can only delete log files within their own VO. |
| 122 | + """ |
| 123 | + message = "no-op" |
| 124 | + user_info = await check_permissions(action=ActionType.DELETE) |
| 125 | + non_privil_params = {"parameter": "VO", "operator": "eq", "value": user_info.vo} |
| 126 | + |
| 127 | + # id pilot_id is provided we ignore data.min and data.max |
| 128 | + if data.min and data.max and not pilot_id: |
| 129 | + raise InvalidQueryError( |
| 130 | + "This query requires a range operator definition in DiracX" |
| 131 | + ) |
| 132 | + |
| 133 | + if pilot_id: |
| 134 | + search_params = [{"parameter": "PilotID", "operator": "eq", "value": pilot_id}] |
| 135 | + if _non_privileged(user_info): |
| 136 | + search_params.append(non_privil_params) |
| 137 | + await db.delete(search_params) |
| 138 | + message = f"Logs for pilot ID '{pilot_id}' successfully deleted" |
| 139 | + |
| 140 | + elif data.min: |
| 141 | + logger.warning(f"Deleting logs for pilots with submission data >='{data.min}'") |
| 142 | + search_params = [ |
| 143 | + {"parameter": "SubmissionTime", "operator": "gt", "value": data.min} |
| 144 | + ] |
| 145 | + if _non_privileged(user_info): |
| 146 | + search_params.append(non_privil_params) |
| 147 | + await db.delete(search_params) |
| 148 | + message = f"Logs for for pilots with submission data >='{data.min}' successfully deleted" |
| 149 | + |
| 150 | + return message |
| 151 | + |
| 152 | + |
| 153 | +def _non_privileged(user_info: AuthorizedUserInfo): |
| 154 | + return ( |
| 155 | + SERVICE_ADMINISTRATOR not in user_info.properties |
| 156 | + and OPERATOR not in user_info.properties |
| 157 | + ) |
0 commit comments