Push Tracker
ria-ran--sim-drop/plan/01-database.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

2.9 KiB

Unit 01 — Database Layer

Goal

SQLite persistence layer with CRUD operations and MAC-address-based rate limiting.

Files

  • server/database.py
  • server/models.py

server/models.py

from dataclasses import dataclass
from datetime import datetime
from typing import Optional

@dataclass
class Subscriber:
    id: Optional[int]
    imsi: str
    iccid: str
    ki: str           # hex string
    opc: str          # hex string
    mac_address: Optional[str]
    created_at: datetime
    synced_to_core: bool

server/database.py

Schema

CREATE TABLE IF NOT EXISTS subscribers (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    imsi TEXT UNIQUE NOT NULL,
    iccid TEXT UNIQUE NOT NULL,
    ki TEXT NOT NULL,
    opc TEXT NOT NULL,
    mac_address TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    synced_to_core BOOLEAN DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_imsi ON subscribers(imsi);
CREATE INDEX IF NOT EXISTS idx_mac_address ON subscribers(mac_address);

Functions

Function Signature Returns Notes
init_db (db_path: str) -> None Creates schema idempotently
get_connection (db_path: str) -> sqlite3.Connection connection row_factory = sqlite3.Row
add_subscriber (conn, imsi, iccid, ki, opc, mac_address) -> int row id Raises ValueError on duplicate IMSI/ICCID
get_subscriber_by_imsi (conn, imsi: str) -> Optional[Subscriber] Subscriber or None
get_subscriber_by_id (conn, sub_id: int) -> Optional[Subscriber] Subscriber or None
list_unsynced (conn) -> list[Subscriber] list WHERE synced_to_core = 0
mark_synced (conn, imsi: str) -> None UPDATE synced_to_core = 1
check_rate_limit (conn, mac_address: str, window_hours: int) -> bool True = allowed Counts rows for mac in last N hours
export_all (conn) -> list[dict] list of dicts All rows as serializable dicts

Rate Limiting Logic

def check_rate_limit(conn, mac_address, window_hours):
    cutoff = datetime.utcnow() - timedelta(hours=window_hours)
    cur = conn.execute(
        "SELECT COUNT(*) FROM subscribers WHERE mac_address = ? AND created_at > ?",
        (mac_address, cutoff.isoformat())
    )
    count = cur.fetchone()[0]
    return count == 0  # True = allowed (no prior provisioning in window)

Verification

python -c "
from server.database import init_db, get_connection, add_subscriber, check_rate_limit
import tempfile, os
db = tempfile.mktemp(suffix='.db')
init_db(db)
conn = get_connection(db)
sid = add_subscriber(conn, '302720000000001', '8910302720000000001', 'aa'*16, 'bb'*16, 'aa:bb:cc:dd:ee:ff')
print('inserted id:', sid)
print('rate limit (should be False now):', check_rate_limit(conn, 'aa:bb:cc:dd:ee:ff', 1))
conn.close(); os.unlink(db)
"