91 lines
3.9 KiB
Bash
91 lines
3.9 KiB
Bash
|
|
#!/usr/bin/env bash
|
||
|
|
# Refresh the IP-geolocation database used by the approximate-location fallback
|
||
|
|
# (backend/data/geoip.py). Runs on the HOST (prod/beta), not in a container, on
|
||
|
|
# a systemd timer — see geoip-refresh.timer.
|
||
|
|
#
|
||
|
|
# Why on the host and not baked into the image:
|
||
|
|
# * it is a 100-200 MB data file on its own monthly release cadence, not code;
|
||
|
|
# baking it in adds that to every image pull and freezes its freshness to
|
||
|
|
# whenever the last deploy happened;
|
||
|
|
# * MaxMind's GeoLite EULA *requires* deleting a database within 30 days of a
|
||
|
|
# newer release, which a build-time copy cannot honour on its own;
|
||
|
|
# * geoip.py memory-maps the file and re-opens it when (mtime, size) changes,
|
||
|
|
# so an atomic swap here is picked up with no restart and no deploy.
|
||
|
|
#
|
||
|
|
# Default source: DB-IP's IP to City Lite, CC BY 4.0, no account and no licence
|
||
|
|
# key — which is why this script needs no secret from the vault. Point
|
||
|
|
# GEOIP_URL elsewhere (e.g. a MaxMind permalink carrying a licence key from
|
||
|
|
# /etc/thermograph.env) to use a different database; geoip.py reads the file's
|
||
|
|
# own metadata for the attribution line, so nothing else changes.
|
||
|
|
#
|
||
|
|
# sudo /opt/thermograph/infra/deploy/geoip-refresh.sh
|
||
|
|
#
|
||
|
|
# Exit status: 0 on a successful swap AND on "already current" (so the timer
|
||
|
|
# doesn't alarm); non-zero only on a real failure, leaving any existing
|
||
|
|
# database untouched — a failed refresh must never be worse than a stale one.
|
||
|
|
set -euo pipefail
|
||
|
|
|
||
|
|
DEST_DIR="${THERMOGRAPH_GEOIP_HOST_DIR:-/var/lib/thermograph/geoip}"
|
||
|
|
DEST="$DEST_DIR/city.mmdb"
|
||
|
|
# DB-IP publishes one file per month at a predictable URL. Try this month, then
|
||
|
|
# last month: the new file appears at some point on the 1st, so a timer firing
|
||
|
|
# early would otherwise fail for a day.
|
||
|
|
BASE="${GEOIP_BASE:-https://download.db-ip.com/free}"
|
||
|
|
THIS_MONTH="$(date -u +%Y-%m)"
|
||
|
|
LAST_MONTH="$(date -u -d "$(date -u +%Y-%m-01) -1 month" +%Y-%m)"
|
||
|
|
|
||
|
|
log() { echo "[geoip-refresh] $*"; }
|
||
|
|
|
||
|
|
mkdir -p "$DEST_DIR"
|
||
|
|
tmp="$(mktemp -d "${TMPDIR:-/tmp}/geoip.XXXXXX")"
|
||
|
|
trap 'rm -rf "$tmp"' EXIT
|
||
|
|
|
||
|
|
url=""
|
||
|
|
if [ -n "${GEOIP_URL:-}" ]; then
|
||
|
|
url="$GEOIP_URL"
|
||
|
|
else
|
||
|
|
for m in "$THIS_MONTH" "$LAST_MONTH"; do
|
||
|
|
candidate="$BASE/dbip-city-lite-$m.mmdb.gz"
|
||
|
|
if curl -fsSI --max-time 30 "$candidate" >/dev/null 2>&1; then
|
||
|
|
url="$candidate"; break
|
||
|
|
fi
|
||
|
|
done
|
||
|
|
fi
|
||
|
|
[ -n "$url" ] || { log "no published database found at $BASE for $THIS_MONTH or $LAST_MONTH"; exit 1; }
|
||
|
|
log "source: $url"
|
||
|
|
|
||
|
|
curl -fsSL --max-time 900 --retry 3 --retry-delay 10 -o "$tmp/db.gz" "$url"
|
||
|
|
gunzip -c "$tmp/db.gz" > "$tmp/city.mmdb"
|
||
|
|
|
||
|
|
# Prove the file actually opens and answers before it can replace a working
|
||
|
|
# one. A truncated download that merely *exists* would otherwise silently
|
||
|
|
# disable lookups for a month.
|
||
|
|
python3 - "$tmp/city.mmdb" <<'PY'
|
||
|
|
import sys
|
||
|
|
import maxminddb
|
||
|
|
with maxminddb.open_database(sys.argv[1]) as r:
|
||
|
|
meta = r.metadata()
|
||
|
|
if "city" not in (meta.database_type or "").lower():
|
||
|
|
sys.exit(f"unexpected database_type: {meta.database_type}")
|
||
|
|
if r.get("8.8.8.8") is None and r.get("1.1.1.1") is None:
|
||
|
|
sys.exit("database opened but answered nothing for known public addresses")
|
||
|
|
print(f"[geoip-refresh] verified {meta.database_type} "
|
||
|
|
f"built {meta.build_epoch} nodes={meta.node_count}")
|
||
|
|
PY
|
||
|
|
|
||
|
|
if [ -f "$DEST" ] && cmp -s "$tmp/city.mmdb" "$DEST"; then
|
||
|
|
log "already current: $DEST unchanged"
|
||
|
|
exit 0
|
||
|
|
fi
|
||
|
|
|
||
|
|
# Atomic swap on the same filesystem: readers either see the whole old file or
|
||
|
|
# the whole new one, never a half-written mmap. The app's open handle keeps the
|
||
|
|
# unlinked inode alive until it re-opens on the mtime change.
|
||
|
|
install -m 0444 "$tmp/city.mmdb" "$DEST.new"
|
||
|
|
mv -f "$DEST.new" "$DEST"
|
||
|
|
log "installed $DEST ($(stat -c %s "$DEST") bytes)"
|
||
|
|
|
||
|
|
# MaxMind's EULA requires deleting superseded copies within 30 days; keeping no
|
||
|
|
# copies at all satisfies that trivially and is the right default for DB-IP too.
|
||
|
|
find "$DEST_DIR" -maxdepth 1 -name 'city.mmdb.*' -type f -delete 2>/dev/null || true
|