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

160 lines
4.3 KiB
Markdown

# 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`
```python
#!/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`
```python
#!/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`
```python
#!/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 `wlan0``192.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:
```bash
#!/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
```bash
# 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
```