to python
Build and Push Docker Images / build-and-push (push) Successful in 1m8s

This commit is contained in:
2026-08-14 19:40:19 +02:00
parent d5791a4ac5
commit 7a5b2ac3bd
4 changed files with 126 additions and 76 deletions
@@ -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"]
@@ -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`
@@ -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()
@@ -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