From 7a5b2ac3bdc4918d13e7551487c4e60361778453 Mon Sep 17 00:00:00 2001 From: Bram Date: Fri, 14 Aug 2026 19:40:19 +0200 Subject: [PATCH] to python --- .../gluetun-pia-wireguard-rotator/Dockerfile | 15 ++- .../gluetun-pia-wireguard-rotator/README.md | 10 +- .../entrypoint.py | 110 ++++++++++++++++++ .../entrypoint.sh | 67 ----------- 4 files changed, 126 insertions(+), 76 deletions(-) create mode 100644 Dockers/gluetun-pia-wireguard-rotator/entrypoint.py delete mode 100644 Dockers/gluetun-pia-wireguard-rotator/entrypoint.sh diff --git a/Dockers/gluetun-pia-wireguard-rotator/Dockerfile b/Dockers/gluetun-pia-wireguard-rotator/Dockerfile index eb2e8b8..74dbdc1 100644 --- a/Dockers/gluetun-pia-wireguard-rotator/Dockerfile +++ b/Dockers/gluetun-pia-wireguard-rotator/Dockerfile @@ -7,17 +7,22 @@ RUN apk add --no-cache git ca-certificates \ FROM docker:cli -RUN apk add --no-cache bash ca-certificates tzdata jq +RUN apk add --no-cache bash ca-certificates tzdata jq python3 py3-pip \ + && python3 -m venv /opt/venv \ + && /opt/venv/bin/pip install --no-cache-dir 'croniter==6.2.4' \ + && apk del py3-pip + +ENV PATH="/opt/venv/bin:$PATH" COPY --from=builder /go/bin/pia-wg-config /usr/local/bin/pia-wg-config -COPY rotate.sh entrypoint.sh /usr/local/bin/ +COPY rotate.sh entrypoint.py /usr/local/bin/ -RUN chmod +x /usr/local/bin/rotate.sh /usr/local/bin/entrypoint.sh +RUN chmod +x /usr/local/bin/rotate.sh /usr/local/bin/entrypoint.py ENV TZ=Europe/Brussels ENV WG_CONFIG_PATH=/config/wireguard/wg0.conf ENV ROTATOR_STATE_PATH=/config/rotator-state.json ENV GLUETUN_CONTAINER=m3u-filter-vpn -ENV ROTATE_AT=03:00 +ENV ROTATE_CRON="0 3 * * *" -ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] +ENTRYPOINT ["/opt/venv/bin/python", "/usr/local/bin/entrypoint.py"] diff --git a/Dockers/gluetun-pia-wireguard-rotator/README.md b/Dockers/gluetun-pia-wireguard-rotator/README.md index 4e1c306..f99dbd1 100644 --- a/Dockers/gluetun-pia-wireguard-rotator/README.md +++ b/Dockers/gluetun-pia-wireguard-rotator/README.md @@ -1,6 +1,6 @@ # gluetun-pia-wireguard-rotator -Sidecar die dagelijks (en bij container-start) een willekeurige PIA WireGuard-region kiest via [pia-wg-config](https://github.com/ccarpinteri/pia-wg-config), `wg0.conf` op het gedeelde gluetun-volume schrijft, en een configureerbare lijst containers herstart. +Sidecar die op een cron-schema (en bij container-start) een willekeurige PIA WireGuard-region kiest via [pia-wg-config](https://github.com/ccarpinteri/pia-wg-config), `wg0.conf` op het gedeelde gluetun-volume schrijft, en een configureerbare lijst containers herstart. ## Vereisten @@ -35,10 +35,12 @@ docker run --rm --entrypoint pia-wg-config bramkel/gluetun-pia-wireguard-rotator | `GLUETUN_CONTAINER` | `m3u-filter-vpn` | Gluetun-container; wordt **altijd als eerste** herstart | | `WG_CONFIG_PATH` | `/config/wireguard/wg0.conf` | Pad waar `wg0.conf` wordt geschreven | | `ROTATOR_STATE_PATH` | `/config/rotator-state.json` | Laatste rotatie-metadata | -| `ROTATE_AT` | `03:00` | Dagelijks rotatietijdstip (`HH:MM`, in `TZ`) | +| `ROTATE_CRON` | `0 3 * * *` | 5-veld cron-expressie (minuut uur dag-van-maand maand dag-van-week), in `TZ`. Macros: `@hourly`, `@daily`, `@weekly`, `@monthly`, `@yearly` | | `RATE_LIMIT_WAIT_SECONDS` | `3600` | Wachttijd bij PIA rate-limit (`429` / `too_many_attempts`) vóór retry | | `TZ` | `Europe/Brussels` | Tijdzone voor scheduling | +`ROTATE_CRON` voorbeelden: `0 */6 * * *` (elke 6 uur), `0 3 * * 1-5` (weekdagen 03:00), `@hourly`. Quote de waarde in Compose (`"0 3 * * *"`) zodat YAML `*` niet speciaal interpreteert. Oude `ROTATE_AT=HH:MM` werkt nog als `ROTATE_CRON` leeg is. + ## Output - `wireguard/wg0.conf` op het gedeelde volume — Gluetun leest dit als `/gluetun/wireguard/wg0.conf` en dit **overschrijft** `WIREGUARD_*` environment variables @@ -62,7 +64,7 @@ Zie [`docker-compose.example.yml`](docker-compose.example.yml) voor een volledig - PIA_REGIONS=netherlands,france,belgium - GLUETUN_CONTAINER=downloaders-vpn - RESTART_CONTAINERS=SabNZBd,qbittorrent,nzbhydra2,Spotweb - - ROTATE_AT=03:00 + - 'ROTATE_CRON=0 3 * * *' volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - /var/dockers/m3u-filter-pia:/config @@ -100,7 +102,7 @@ Optioneel host-`.env`-keys (`WIREGUARD_*`) opruimen als die niet meer gebruikt w ## Gedrag 1. Bij start: direct roteren (nieuwe config + container-restarts) -2. Daarna: elke dag om `ROTATE_AT` opnieuw +2. Daarna: volgens `ROTATE_CRON` (standaard dagelijks om 03:00) 3. Willekeurige region uit `PIA_REGIONS`, nooit dezelfde als de vorige (uit `rotator-state.json`; uitzondering: maar één region geconfigureerd) 4. `GLUETUN_CONTAINER` wordt altijd als eerste herstart, daarna containers uit `RESTART_CONTAINERS` diff --git a/Dockers/gluetun-pia-wireguard-rotator/entrypoint.py b/Dockers/gluetun-pia-wireguard-rotator/entrypoint.py new file mode 100644 index 0000000..eb2d86d --- /dev/null +++ b/Dockers/gluetun-pia-wireguard-rotator/entrypoint.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +"""Schedule PIA WireGuard rotations via ROTATE_CRON (croniter).""" + +from __future__ import annotations + +import os +import re +import subprocess +import sys +import time +from datetime import datetime +from zoneinfo import ZoneInfo + +from croniter import croniter + +# Matches rotate.sh EXIT_RATE_LIMITED (EX_TEMPFAIL) +EXIT_RATE_LIMITED = 75 + +CRON_MACROS = { + "@yearly": "0 0 1 1 *", + "@annually": "0 0 1 1 *", + "@monthly": "0 0 1 * *", + "@weekly": "0 0 * * 0", + "@daily": "0 0 * * *", + "@midnight": "0 0 * * *", + "@hourly": "0 * * * *", +} + + +def log(msg: str) -> None: + print(f"[{datetime.now().astimezone().isoformat(timespec='seconds')}] {msg}", file=sys.stderr) + + +def zone() -> ZoneInfo: + name = os.environ.get("TZ") or "UTC" + try: + return ZoneInfo(name) + except Exception as exc: + raise SystemExit(f"Invalid TZ '{name}': {exc}") from exc + + +def hhmm_to_cron(value: str) -> str: + match = re.fullmatch(r"([0-9]{1,2}):([0-9]{2})", value.strip()) + if not match: + raise ValueError(f"Invalid ROTATE_AT '{value}' (expected HH:MM)") + hour = int(match.group(1)) + minute = int(match.group(2)) + if not (0 <= hour <= 23 and 0 <= minute <= 59): + raise ValueError(f"Invalid ROTATE_AT '{value}' (expected HH:MM)") + return f"{minute} {hour} * * *" + + +def resolve_cron_expr() -> str: + cron = os.environ.get("ROTATE_CRON", "").strip() + at = os.environ.get("ROTATE_AT", "").strip() + if cron: + expr = cron + elif at: + expr = hhmm_to_cron(at) + else: + expr = "0 3 * * *" + + expr = CRON_MACROS.get(expr.lower(), expr) + if not croniter.is_valid(expr): + raise SystemExit(f"Invalid ROTATE_CRON '{expr}' (expected a 5-field cron expression)") + return expr + + +def next_run(expr: str, after: datetime) -> datetime: + return croniter(expr, after).get_next(datetime) + + +def sleep_until_next_rotate(expr: str) -> None: + tz = zone() + now = datetime.now(tz) + nxt = next_run(expr, now) + wait_s = max(0.0, (nxt - now).total_seconds()) + log(f"Next rotation at {nxt.isoformat(timespec='seconds')} (cron '{expr}', TZ={tz.key}) in {int(wait_s)}s") + time.sleep(wait_s) + + +def run_rotation(reason: str) -> None: + wait_s = int(os.environ.get("RATE_LIMIT_WAIT_SECONDS", "3600")) + while True: + log(reason) + result = subprocess.run(["/usr/local/bin/rotate.sh"], check=False) + if result.returncode == 0: + return + if result.returncode == EXIT_RATE_LIMITED: + log(f"PIA rate-limited (too many attempts); waiting {wait_s}s before retry") + time.sleep(wait_s) + reason = "Retrying rotation after rate-limit wait" + continue + raise SystemExit(f"Rotation failed with exit code {result.returncode}") + + +def main() -> None: + expr = resolve_cron_expr() + # Fail fast on bad cron / TZ before rotating. + next_run(expr, datetime.now(zone())) + log(f"Starting gluetun PIA WireGuard rotator (TZ={zone().key}, ROTATE_CRON='{expr}')") + + run_rotation("Running rotation on startup") + while True: + sleep_until_next_rotate(expr) + run_rotation("Running scheduled rotation") + + +if __name__ == "__main__": + main() diff --git a/Dockers/gluetun-pia-wireguard-rotator/entrypoint.sh b/Dockers/gluetun-pia-wireguard-rotator/entrypoint.sh deleted file mode 100644 index ce9bb23..0000000 --- a/Dockers/gluetun-pia-wireguard-rotator/entrypoint.sh +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -log() { echo "[$(date -Is)] $*" >&2; } - -sleep_until_next_rotate() { - local rotate_at="${ROTATE_AT:-03:00}" - local target_h target_m - IFS=: read -r target_h target_m <<<"$rotate_at" - target_h=$((10#$target_h)) - target_m=$((10#$target_m)) - - local now_h now_m now_s target_s wait_s - now_h=$(date +%H) - now_m=$(date +%M) - now_h=$((10#$now_h)) - now_m=$((10#$now_m)) - now_s=$((now_h * 3600 + now_m * 60)) - target_s=$((target_h * 3600 + target_m * 60)) - - if ((now_s < target_s)); then - wait_s=$((target_s - now_s)) - else - wait_s=$((86400 - now_s + target_s)) - fi - - log "Next rotation at ${rotate_at} (${TZ:-UTC}) in ${wait_s}s" - sleep "$wait_s" -} - -# Matches rotate.sh EXIT_RATE_LIMITED (EX_TEMPFAIL) -readonly EXIT_RATE_LIMITED=75 - -run_rotation() { - local reason="$1" - local wait_s="${RATE_LIMIT_WAIT_SECONDS:-3600}" - local rc - - while true; do - log "$reason" - rc=0 - /usr/local/bin/rotate.sh || rc=$? - case "$rc" in - 0) - return 0 - ;; - "$EXIT_RATE_LIMITED") - log "PIA rate-limited (too many attempts); waiting ${wait_s}s before retry" - sleep "$wait_s" - reason="Retrying rotation after rate-limit wait" - ;; - *) - >&2 echo "Rotation failed with exit code $rc" - return "$rc" - ;; - esac - done -} - -log "Starting gluetun PIA WireGuard rotator (TZ=${TZ:-UTC}, ROTATE_AT=${ROTATE_AT:-03:00})" - -run_rotation "Running rotation on startup" - -while true; do - sleep_until_next_rotate - run_rotation "Running scheduled daily rotation" -done