A
8672b0de6b
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>
4.3 KiB
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.pyscripts/sync_subscribers.pyscripts/export_subscribers.pyscripts/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:
- Install packages:
hostapd dnsmasq nginx python3 python3-pip - Configure static IP on
wlan0:/etc/dhcpcd.confentryinterface wlan0 static ip_address=192.168.4.1/24 - Write
/etc/hostapd/hostapd.conf(SSID=Emergency-5G, open auth, channel 6) - Write
/etc/dnsmasq.conf(DHCP range 192.168.4.10-250,address=/#/192.168.4.1) - Write nginx site config (proxy
/api/→ Flask, serve portal static) - Enable
net.ipv4.ip_forward=1in/etc/sysctl.conf - Add iptables rules: all port 80 from
wlan0→192.168.4.1:80(captive portal) - Enable and start:
hostapd,dnsmasq,nginx,esim-provisioning - Install Python dependencies:
pip3 install -r /opt/esim-provisioning/requirements.txt - 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