Push Tracker
ria-ran--sim-drop/plan/07-scripts.md
A ashkan@beigi.net 8672b0de6b Add Phase-1 eSIM provisioning implementation
Full working closed-loop eSIM provisioning stack (spec in claude.md / plan/):
Flask API, SQLite subscriber store, credential/profile generator, Open5GS
and free5gc core adapters, captive portal, operational scripts, systemd
units, network configs, and a 44-test suite.

Profile generation currently returns a mock JSON profile; real SGP.22
generation via pySim is the planned Phase-2 upgrade.

Fixes two bugs found while getting the suite green:
- generate_imsi hardcoded a 10-digit MSIN, producing 16-digit IMSIs for a
  3-digit MNC; MSIN length is now 15 - len(mcc) - len(mnc).
- check_rate_limit compared timestamps as strings across mismatched formats
  (SQLite CURRENT_TIMESTAMP vs Python isoformat); both sides now normalized
  via SQLite datetime() so the window check is chronologically correct.

Add .gitignore for venv, caches, runtime config.yaml, and databases.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 05:56:31 +00:00

4.3 KiB

Unit 07 — Operational Scripts

Goal

CLI scripts for database initialization, subscriber sync, JSON export, and Raspberry Pi WiFi AP setup.

Files

  • scripts/init_db.py
  • scripts/sync_subscribers.py
  • scripts/export_subscribers.py
  • scripts/setup_wifi_ap.sh

scripts/init_db.py

#!/usr/bin/env python3
"""Initialize the SQLite subscriber database."""
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))

from server.utils import load_config
from server.database import init_db

def main():
    config_path = os.environ.get("CONFIG_PATH", "config.yaml")
    cfg = load_config(config_path)
    db_path = cfg["database"]["path"]
    os.makedirs(os.path.dirname(db_path), exist_ok=True)
    init_db(db_path)
    print(f"Database initialized at {db_path}")

if __name__ == "__main__":
    main()

scripts/sync_subscribers.py

#!/usr/bin/env python3
"""Sync unsynced subscribers to the configured core network."""
import sys, os, logging
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))

from server.utils import load_config
from server.database import get_connection, list_unsynced, mark_synced
from server.core_adapters import get_adapter

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

def main():
    config_path = os.environ.get("CONFIG_PATH", "config.yaml")
    cfg = load_config(config_path)
    adapter = get_adapter(cfg)
    if adapter is None:
        logger.error("No core adapter configured. Set core.type in config.")
        sys.exit(1)

    conn = get_connection(cfg["database"]["path"])
    unsynced = list_unsynced(conn)
    logger.info(f"Found {len(unsynced)} unsynced subscribers")

    success = 0
    for sub in unsynced:
        try:
            adapter.add_subscriber(sub.imsi, sub.ki, sub.opc)
            mark_synced(conn, sub.imsi)
            success += 1
            logger.info(f"Synced {sub.imsi}")
        except Exception as e:
            logger.error(f"Failed to sync {sub.imsi}: {e}")

    conn.close()
    logger.info(f"Sync complete: {success}/{len(unsynced)} succeeded")

if __name__ == "__main__":
    main()

scripts/export_subscribers.py

#!/usr/bin/env python3
"""Export all subscribers to JSON on stdout."""
import sys, os, json
from datetime import datetime
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))

from server.utils import load_config
from server.database import get_connection, export_all

def main():
    config_path = os.environ.get("CONFIG_PATH", "config.yaml")
    cfg = load_config(config_path)
    conn = get_connection(cfg["database"]["path"])
    subscribers = export_all(conn)
    conn.close()

    output = {
        "exported_at": datetime.utcnow().isoformat() + "Z",
        "count": len(subscribers),
        "subscribers": subscribers
    }
    print(json.dumps(output, indent=2, default=str))

if __name__ == "__main__":
    main()

scripts/setup_wifi_ap.sh

Raspberry Pi WiFi AP setup. Must be run as root.

Steps:

  1. Install packages: hostapd dnsmasq nginx python3 python3-pip
  2. Configure static IP on wlan0: /etc/dhcpcd.conf entry interface wlan0 static ip_address=192.168.4.1/24
  3. Write /etc/hostapd/hostapd.conf (SSID=Emergency-5G, open auth, channel 6)
  4. Write /etc/dnsmasq.conf (DHCP range 192.168.4.10-250, address=/#/192.168.4.1)
  5. Write nginx site config (proxy /api/ → Flask, serve portal static)
  6. Enable net.ipv4.ip_forward=1 in /etc/sysctl.conf
  7. Add iptables rules: all port 80 from wlan0192.168.4.1:80 (captive portal)
  8. Enable and start: hostapd, dnsmasq, nginx, esim-provisioning
  9. Install Python dependencies: pip3 install -r /opt/esim-provisioning/requirements.txt
  10. Print success message and next steps

Script structure:

#!/bin/bash
set -euo pipefail
[ "$(id -u)" = "0" ] || { echo "Must run as root"; exit 1; }

SSID="Emergency-5G"
CHANNEL=6
AP_IP="192.168.4.1"
DHCP_RANGE="192.168.4.10,192.168.4.250,24h"
INSTALL_DIR="/opt/esim-provisioning"
...

Verification

# init_db
CONFIG_PATH=config.yaml python scripts/init_db.py

# export (after some provisioning)
CONFIG_PATH=config.yaml python scripts/export_subscribers.py | python -m json.tool

# sync (requires core adapter running)
CONFIG_PATH=config.yaml python scripts/sync_subscribers.py