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>
2.9 KiB
2.9 KiB
Unit 01 — Database Layer
Goal
SQLite persistence layer with CRUD operations and MAC-address-based rate limiting.
Files
server/database.pyserver/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)
"