commit 7be4036bed89c83a2fd15f68f728614fded30e6a Author: Thies Mueller Date: Thu Jun 11 17:47:58 2026 +0200 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d4f369f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +config.yaml +.venv/ \ No newline at end of file diff --git a/app.py b/app.py new file mode 100644 index 0000000..0354f9f --- /dev/null +++ b/app.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python3 + +import logging +import signal +import sys +from typing import List, Dict, Any +from concurrent.futures import ThreadPoolExecutor, as_completed + +import requests +import yaml +from flask import Flask, request, Response + +app = Flask(__name__) + +CONFIG_FILE = "config.yaml" + + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(message)s" +) + +logger = logging.getLogger("pager-api") + + +def load_config() -> Dict[str, Any]: + with open(CONFIG_FILE, "r") as f: + return yaml.safe_load(f) + +config = load_config() + +PARALLEL_TARGETS = config.get("parallelism", {}).get("targets", 10) +PARALLEL_PAGERS = config.get("parallelism", {}).get("all_pagers", 10) + +def get_all_valid_ids() -> List[str]: + ids = [] + for group in config.get("pager_ranges", []): + for i in range(int(group["start"]), int(group["end"]) + 1): + ids.append(str(i)) + return ids + + +def pager_exists(pager_id: str) -> bool: + return pager_id in get_all_valid_ids() + + +def validate_token(req) -> bool: + auth_header = req.headers.get("Authorization", "") + if not auth_header.startswith("Bearer "): + return False + token = auth_header.replace("Bearer ", "", 1).strip() + return token == config.get("bearer_token") + + +def call_target(target: str, path: str) -> bool: + url = target.rstrip("/") + path + + try: + logger.info(f"CALL TARGET: {url}") + + response = requests.get(url, timeout=10) + + logger.info( + f"TARGET RESPONSE: {url} -> " + f"{response.status_code} {response.text}" + ) + + return response.status_code < 400 + + except Exception as e: + logger.exception(f"TARGET ERROR: {url} -> {e}") + return False + + +def notify_targets(path: str) -> bool: + + targets = config.get("targets", []) + success = True + + with ThreadPoolExecutor(max_workers=PARALLEL_TARGETS) as executor: + futures = [ + executor.submit(call_target, target, path) + for target in targets + ] + + for f in as_completed(futures): + if not f.result(): + success = False + + return success + + +def notify_all_pagers_parallel() -> bool: + pager_ids = get_all_valid_ids() + success = True + + with ThreadPoolExecutor(max_workers=PARALLEL_PAGERS) as executor: + futures = [ + executor.submit(notify_targets, f"/call/P{pid}") + for pid in pager_ids + ] + + for f in as_completed(futures): + if not f.result(): + success = False + + return success + + +@app.route("/api/page", methods=["POST"]) +def api_page(): + + logger.info(f"REQUEST FROM {request.remote_addr}") + + if not validate_token(request): + return Response("ERROR", status=401, mimetype="text/plain") + + body = request.get_data(as_text=True).strip() + + if not body: + body = request.form.get("id", "").strip() + + logger.info(f"INPUT: {body}") + + if not body: + return Response("ERROR", status=400, mimetype="text/plain") + + try: + + if body.upper() == "ALL": + + success = notify_all_pagers_parallel() + + return Response( + "PAGERNOTIFIED" if success else "ERROR", + status=200 if success else 500, + mimetype="text/plain" + ) + + if not pager_exists(body): + logger.warning(f"PAGER NOT FOUND: {body}") + return Response("PAGERNOTFOUND", status=404, mimetype="text/plain") + + success = notify_targets(f"/call/P{body}") + + return Response( + "PAGERNOTIFIED" if success else "ERROR", + status=200 if success else 500, + mimetype="text/plain" + ) + + except Exception as e: + logger.exception(f"GENERAL ERROR: {e}") + return Response("ERROR", status=500, mimetype="text/plain") + + +@app.route("/api/shutdown", methods=["POST"]) +def api_shutdown(): + + logger.info(f"SHUTDOWN REQUEST FROM {request.remote_addr}") + + if not validate_token(request): + return Response("ERROR", status=401, mimetype="text/plain") + + try: + success = notify_targets("/call/P999") + + return Response( + "PAGERNOTIFIED" if success else "ERROR", + status=200 if success else 500, + mimetype="text/plain" + ) + + except Exception as e: + logger.exception(f"SHUTDOWN ERROR: {e}") + return Response("ERROR", status=500, mimetype="text/plain") + + + +def signal_handler(sig, frame): + logger.info("SIGTERM/SIGINT received") + sys.exit(0) + + +signal.signal(signal.SIGINT, signal_handler) +signal.signal(signal.SIGTERM, signal_handler) + + +if __name__ == "__main__": + logger.info("Starting pager API") + + app.run( + host="0.0.0.0", + port=5500, + threaded=True + ) \ No newline at end of file diff --git a/config.example.yaml b/config.example.yaml new file mode 100644 index 0000000..6159679 --- /dev/null +++ b/config.example.yaml @@ -0,0 +1,16 @@ +bearer_token: "SUPERSECRET" + +targets: + - "http://10.10.10.1" + - "http://10.10.10.2" + +pager_ranges: + - start: 101 + end: 116 + + - start: 201 + end: 216 + +parallelism: + targets: 1 + all_pagers: 1 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..09a313b --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +flask +requests +pyyaml