156 lines
5.1 KiB
Python
156 lines
5.1 KiB
Python
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import sys
|
|
import time
|
|
|
|
import httpx
|
|
from sqlalchemy import create_engine, text
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
if str(ROOT) not in sys.path:
|
|
sys.path.insert(0, str(ROOT))
|
|
|
|
from services.shared.security import issue_app_token
|
|
|
|
|
|
def _load_env_file(path: str) -> None:
|
|
env_path = Path(path).expanduser().resolve()
|
|
if not env_path.exists() or not env_path.is_file():
|
|
return
|
|
for line in env_path.read_text(encoding="utf-8").splitlines():
|
|
stripped = line.strip()
|
|
if not stripped or stripped.startswith("#") or "=" not in stripped:
|
|
continue
|
|
key, value = stripped.split("=", 1)
|
|
key = key.strip()
|
|
if key and key not in os.environ:
|
|
os.environ[key] = value.strip()
|
|
|
|
|
|
def _bool_env(name: str, default: bool) -> bool:
|
|
raw = os.getenv(name)
|
|
if raw is None:
|
|
return default
|
|
return raw.strip().lower() in {"1", "true", "yes", "on"}
|
|
|
|
|
|
def _ops_headers() -> dict[str, str]:
|
|
if _bool_env("ALLOW_LEGACY_HEADER_AUTH", True):
|
|
return {"X-User": "admin", "X-Role": "admin"}
|
|
token = issue_app_token(
|
|
subject="ops:track9-check",
|
|
username="track9-check",
|
|
role="admin",
|
|
auth_source="service",
|
|
provider="track9-script",
|
|
ttl_seconds=300,
|
|
)
|
|
return {"Authorization": f"Bearer {token}"}
|
|
|
|
|
|
def _get_json(url: str) -> dict:
|
|
with httpx.Client(timeout=10, trust_env=False) as client:
|
|
response = client.get(url, headers=_ops_headers())
|
|
response.raise_for_status()
|
|
return response.json()
|
|
|
|
|
|
def _wait_for_ami_connected(base_url: str, retries: int = 8, delay_seconds: float = 1.0) -> dict:
|
|
last_status: dict = {}
|
|
for _ in range(retries):
|
|
status = _get_json(f"{base_url.rstrip('/')}/proxy/asterisk-bridge/asterisk/status")
|
|
last_status = status
|
|
if status.get("ami_connected"):
|
|
return status
|
|
time.sleep(delay_seconds)
|
|
return last_status
|
|
|
|
|
|
def _table_exists(conn, name: str) -> bool:
|
|
try:
|
|
conn.execute(text(f"SELECT 1 FROM {name} LIMIT 1"))
|
|
return True
|
|
except Exception: # noqa: BLE001
|
|
return False
|
|
|
|
|
|
def _scalar(conn, sql: str) -> int:
|
|
value = conn.execute(text(sql)).scalar()
|
|
return int(value or 0)
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description="Track 9 Asterisk integration acceptance validator")
|
|
parser.add_argument("--base-url", default="http://127.0.0.1:8080")
|
|
parser.add_argument("--env-file", default=".env.production")
|
|
parser.add_argument("--database-url", required=True)
|
|
parser.add_argument("--require-recording", action="store_true")
|
|
args = parser.parse_args()
|
|
|
|
_load_env_file(args.env_file)
|
|
|
|
failures: list[str] = []
|
|
|
|
health = _get_json(f"{args.base_url.rstrip('/')}/proxy/asterisk-bridge/health")
|
|
status = _wait_for_ami_connected(args.base_url)
|
|
if health.get("status") != "ok":
|
|
failures.append("Bridge health is not ok")
|
|
if not status.get("ami_connected"):
|
|
failures.append("AMI is not connected")
|
|
|
|
engine = create_engine(args.database_url, future=True)
|
|
with engine.begin() as conn:
|
|
for table in ("asterisk_event_log", "asterisk_call_links", "voice_events"):
|
|
if not _table_exists(conn, table):
|
|
failures.append(f"Missing table: {table}")
|
|
|
|
started = _scalar(
|
|
conn,
|
|
"SELECT COUNT(*) FROM asterisk_event_log WHERE ami_event_name = 'MVPCCCallStarted' AND forward_status = 'forwarded'",
|
|
)
|
|
call_started = _scalar(
|
|
conn,
|
|
"SELECT COUNT(*) FROM voice_events WHERE event_type = 'call.started' AND payload_json LIKE '%\"source\": \"asterisk\"%'",
|
|
)
|
|
links = _scalar(conn, "SELECT COUNT(*) FROM asterisk_call_links")
|
|
stale_failures = _scalar(
|
|
conn,
|
|
"SELECT COUNT(*) FROM asterisk_event_log WHERE forward_status = 'failed'",
|
|
)
|
|
recordings = _scalar(
|
|
conn,
|
|
"SELECT COUNT(*) FROM asterisk_event_log WHERE ami_event_name = 'MVPCCRecordingReady' AND recording_id IS NOT NULL",
|
|
)
|
|
|
|
if started < 1:
|
|
failures.append("No forwarded MVPCCCallStarted events")
|
|
if call_started < 1:
|
|
failures.append("No platform call.started events from Asterisk source")
|
|
if links < 1:
|
|
failures.append("No linked Asterisk calls")
|
|
if stale_failures > 0:
|
|
failures.append(f"Failed bridge events present: {stale_failures}")
|
|
if args.require_recording and recordings < 1:
|
|
failures.append("No uploaded recordings linked to Asterisk events")
|
|
|
|
if failures:
|
|
print("[FAIL] Track 9 validation failed")
|
|
for item in failures:
|
|
print(f"- {item}")
|
|
return 1
|
|
|
|
print("[PASS] Track 9 validation checks passed")
|
|
print(f"- started_events: {started}")
|
|
print(f"- platform_call_started: {call_started}")
|
|
print(f"- call_links: {links}")
|
|
print(f"- recording_events_with_upload: {recordings}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|