Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file removed .DS_Store
Binary file not shown.
24 changes: 24 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
.git
.gitignore
node_modules
frontend/dist
frontend/node_modules
jobs
uploads
outputs
static
*.db
.venv
__pycache__
*.pyc
*.tsbuildinfo
.idea
.claude
tests
.pytest_cache
.ruff_cache
htmlcov
.coverage
.env
*.md
!frontend/README.md
6 changes: 3 additions & 3 deletions .github/workflows/Lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ jobs:
with:
python-version: "3.12"

- name: Install dependencies
- name: Install lint tools
run: |
python -m pip install --upgrade pip
pip install ruff black isort
pip install "ruff~=0.8" "black~=24.0" "isort~=5.13"

- name: Ruff
run: ruff check .
Expand All @@ -29,4 +29,4 @@ jobs:
run: black --check .

- name: isort
run: isort --check-only .
run: isort --check-only --profile black .
47 changes: 35 additions & 12 deletions .github/workflows/github-actions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ name: Test
on: [push]

jobs:
FastAPI-Installation:
backend:
runs-on: ubuntu-latest
steps:
- name: CheckOut Code
- name: Check Out Code
uses: actions/checkout@v4

- name: Set up Python
Expand All @@ -16,22 +16,45 @@ jobs:

- name: Install dependencies
run: |
python -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
pip install uvicorn
pip install -e ".[dev]"

- name: Run tests with coverage
run: python -m pytest tests/ --cov=app --cov=main --cov-fail-under=80

- name: Run FastAPI server and test health endpoint
run: |
source .venv/bin/activate

uvicorn main:app --host 127.0.0.1 --port 8000 &
SERVER_PID=$!

# Wait for server to start
sleep 5
for i in $(seq 1 30); do
curl -sf http://127.0.0.1:8000/live && break || sleep 1
done

# SUMO is absent on the runner: /ready must report 503, /health degraded
test "$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8000/ready)" = "503"
curl --fail http://127.0.0.1:8000/health | grep -q '"degraded"'

kill $SERVER_PID

frontend:
runs-on: ubuntu-latest
steps:
- name: Check Out Code
uses: actions/checkout@v4

# Fail the workflow if /health doesn't return a 2xx response
curl --fail http://127.0.0.1:8000/health
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: frontend/package-lock.json

- name: Install dependencies
working-directory: frontend
run: npm ci

kill $SERVER_PID
- name: Build
working-directory: frontend
run: npm run build
29 changes: 29 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# macOS
.DS_Store

# Python
__pycache__/
*.pyc
.venv/
.venv312/
.pytest_cache/
.ruff_cache/
htmlcov/
.coverage
.env

# Project runtime data
jobs/
uploads/
outputs/
*.db

# Node / frontend
node_modules/
frontend/dist/
*.tsbuildinfo
frontend/vite.config.js
frontend/vite.config.d.ts

# IDE
.idea/
46 changes: 46 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# ── Stage 1: build the frontend ──────────────────────────────────────────────
FROM node:20-slim AS frontend
WORKDIR /fe
COPY frontend/package.json frontend/package-lock.json ./
RUN npm ci
COPY frontend/ ./
RUN npm run build

# ── Stage 2: runtime (Ubuntu + SUMO + backend + built frontend) ─────────────
FROM ubuntu:22.04

ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
python3 python3-pip python3-venv \
sumo sumo-tools \
&& rm -rf /var/lib/apt/lists/*

WORKDIR /app

# Python deps in a venv.
RUN python3 -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
COPY requirements.txt ./
RUN pip install --no-cache-dir --upgrade pip \
&& pip install --no-cache-dir -r requirements.txt

# Backend + built SPA served by FastAPI's StaticFiles mount.
COPY main.py ./
COPY app/ ./app/
COPY --from=frontend /fe/dist ./static

# Non-root user; data (jobs.db + uploads/outputs) lives on a volume.
RUN useradd --create-home --uid 10001 appuser \
&& mkdir -p /data \
&& chown -R appuser:appuser /data /app
USER appuser

VOLUME /data
ENV OSM2NS3_DATA_DIR=/data \
OSM2NS3_STATIC_DIR=/app/static \
SUMO_HOME=/usr/share/sumo

EXPOSE 8000

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
85 changes: 57 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,7 @@ OSM file → netconvert → randomTrips → duarouter → SUMO → tra

**Python:** 3.10+

**Python packages:**
```
fastapi
uvicorn[standard]
python-multipart
aiofiles
pydantic>=2.0
```
**Python packages:** pinned in `requirements.txt` (`fastapi`, `uvicorn[standard]`, `python-multipart`, `aiofiles`, `pydantic`, `pydantic-settings`).

**SUMO tools** (must be on `PATH` or set `SUMO_HOME`):
| Tool | Purpose |
Expand All @@ -58,34 +51,44 @@ sudo apt update && sudo apt install sumo sumo-tools
```bash
git clone https://github.com/your-username/osm2ns3.git
cd osm2ns3
pip install -r requirements.txt
```

**`requirements.txt`**
```
fastapi>=0.110.0
uvicorn[standard]>=0.29.0
python-multipart>=0.0.9
aiofiles>=23.0.0
pydantic>=2.0.0
pip install -r requirements.txt # runtime
pip install -e ".[dev]" # + tests, lint, coverage
```

---

## Running

```bash
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
uvicorn main:app --reload --host 0.0.0.0 --port 8000
```

Open **http://localhost:8000/docs** for the interactive Swagger UI.

If SUMO is installed at a non-standard path:
```bash
export SUMO_HOME=/opt/sumo
uvicorn app.main:app --reload
uvicorn main:app --reload
```

### Configuration

All runtime configuration is via `OSM2NS3_*` environment variables (or a `.env` file):

| Variable | Default | Purpose |
|----------|---------|---------|
| `OSM2NS3_API_KEY` | *(unset)* | When set, every `/jobs/*` route requires an `X-API-Key` header. Auth is **off by default**. |
| `OSM2NS3_CORS_ORIGINS` | `["http://localhost:5173","http://localhost:8000"]` | JSON list of allowed CORS origins (`allow_credentials=True`). |
| `OSM2NS3_DATA_DIR` | repo root | Where `jobs/`, `uploads/`, `outputs/`, and `jobs.db` live. |
| `OSM2NS3_STATIC_DIR` | `static/` | Built frontend served as an SPA with deep-link fallback. |
| `OSM2NS3_MAX_UPLOAD_MB` | `100` | Upload size cap (413 above it). |
| `OSM2NS3_MAX_CONCURRENT_JOBS` | `2` | Pipeline concurrency (semaphore). |
| `OSM2NS3_JOB_RETENTION_MAX` | `200` | Keep at most N finished job dirs. |
| `OSM2NS3_JOB_RETENTION_DAYS` | `7` | Prune finished jobs older than N days. |
| `OSM2NS3_TIMEOUT_SUMO_S` | `1200` | Hard timeout for the `sumo` stage. |
| `OSM2NS3_LOG_LEVEL` | `INFO` | Root log level. |
| `SUMO_HOME` | *(unset)* | Extra directory searched first for `randomTrips.py`/`traceExporter.py`. |

---

## Quick Start
Expand Down Expand Up @@ -182,18 +185,23 @@ curl -O http://localhost:8000/jobs/3f2a1c7e-.../download/mobility.tcl

| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/health` | SUMO tool availability check |
| `GET` | `/live` | Liveness probe — always ok while the process serves |
| `GET` | `/ready` | Readiness: 200 when all SUMO tools found, 503 otherwise |
| `GET` | `/health` | `/ready` shape that never returns 5xx (backcompat) |
| `GET` | `/schema` | Full JSON Schema for all 74 parameters |
| `GET` | `/defaults` | Default values for every parameter |
| `POST` | `/jobs` | Submit OSM file + config, returns `job_id` |
| `GET` | `/jobs` | List all jobs, newest first |
| `GET` | `/jobs/{id}` | Job status + per-stage progress |
| `POST` | `/jobs/{id}/cancel` | Cancel a pending/running job (kills its subprocess tree) |
| `GET` | `/jobs/{id}/config` | Exact config used for a job |
| `GET` | `/jobs/{id}/files` | List downloadable output files |
| `GET` | `/jobs/{id}/download/{file}` | Download an output file |
| `GET` | `/jobs/{id}/logs` | Per-stage stdout/stderr logs |
| `DELETE` | `/jobs/{id}` | Delete job and all its files |

`/live`, `/ready`, `/health`, `/schema`, `/defaults`, and the SPA are public. Every `/jobs/*` route sits behind the API-key check when `OSM2NS3_API_KEY` is set.

---

## Parameters
Expand Down Expand Up @@ -368,18 +376,39 @@ mobility.Install(nodes);

```
osm2ns3/
├── main.py # FastAPI app factory + route handlers
├── app/
│ ├── main.py # FastAPI app, all route handlers
│ ├── models.py # Pydantic models: 74 parameters, 14 validators
│ └── pipeline.py # Async pipeline: subprocess + aiofiles throughout
├── jobs/ # Per-job workdirs with intermediate files and .log files
├── uploads/ # Uploaded OSM files (named by job UUID)
├── outputs/ # Final output files served for download
└── requirements.txt
│ ├── settings.py # pydantic-settings (OSM2NS3_* env, derived dirs)
│ ├── models.py # Pydantic models: 74 parameters, cross-stage validators
│ ├── store.py # SQLite job store (WAL, retention pruning, restart recovery)
│ └── worker.py # PipelineRunner: async subprocess pipeline + cancellation
├── frontend/ # React/TS SPA (schema-driven form from GET /schema)
├── static/ # Built SPA served by FastAPI (present in the Docker image)
├── jobs/ # Per-job workdirs with intermediate files and .log files
├── uploads/ # Uploaded OSM files (named by job UUID)
├── outputs/ # Final output files served for download
├── tests/ # 118 tests: models, command builders, store, API, cancel
├── pyproject.toml # project metadata + pytest/black/isort/ruff/coverage config
├── requirements.txt
├── Dockerfile # multi-stage: node build → ubuntu+SUMO+sumo-tools runtime
└── docker-compose.yml
```

---

## Docker

A single all-in-one image builds the frontend and runs the backend on an Ubuntu + SUMO base, persisting state to a `/data` volume:

```bash
docker build -t osm2ns3 .
docker run -p 8000:8000 -v osm2ns3-data:/data osm2ns3
```

or `docker compose up --build`. The SPA is served at `/` with deep links (`/jobs/{id}`) working; set `OSM2NS3_API_KEY` to enable auth inside the container.

---

## Use Case: VANET Research

This tool was built for VANET (Vehicular Ad-hoc Network) simulation research where NS-3 is used to evaluate routing protocols (AODV, GPSR, etc.) over realistic urban mobility. A typical research workflow:
Expand Down
Empty file added app/__init__.py
Empty file.
Loading
Loading