OpenIPAM is an open-source, self-hosted IP Address Management (IPAM) platform — It is the source of truth for who owns which IP, in which subnet, in which site, with which VLAN, DNS record, and DHCP reservation — exposed through a polished web UI and a first-class REST API built for automation (Ansible, Terraform, CI pipelines).
Built with Python 3.12, FastAPI, PostgreSQL, and a server-rendered HTMX frontend. Every mutating action is audited; every concurrent allocation is safe by design.
| Area | What you get |
|---|---|
| IPAM core | Hierarchical sections/sites, nested subnets with race-safe overlap detection, live utilization, split/merge operations |
| Addresses | Locked "next free IP" allocation (advisory locks, concurrency-tested), states (used/reserved/dhcp/offline), bulk CSV import/export, IP history |
| DNS | Forward + reverse zones and records, pluggable sync backends — PowerDNS API, BIND zone files, RFC2136 dynamic updates — with post-commit auto-sync |
| DHCP | Scopes and MAC reservations, ISC DHCP + Kea config generators, live Kea REST push |
| Discovery | Scheduled/on-demand ping sweeps, remote agent ingestion, reconciliation with an approval step |
| Security | RBAC with section-scoped multi-tenancy, JWT + revocable scoped API tokens, full audit log |
| Automation | Auto-generated OpenAPI docs, cursor pagination, Idempotency-Key support, rate limiting on burst-prone endpoints |
The UI gallery lives at docs/screenshots/ — dashboard, subnet tree, address grid, DNS zones, DHCP scopes, audit log. Screenshots are added as the UI matures; see the gallery's README for what to capture and how to contribute one.
| Concern | Choice |
|---|---|
| Language / Framework | Python 3.12+ · FastAPI (async) |
| Database | PostgreSQL 15+ — native inet/cidr types + GiST indexes |
| ORM / Migrations | SQLAlchemy 2.0 (async) · Alembic |
| Validation | Pydantic v2 |
| Background jobs | arq + Redis |
| Frontend | Jinja2 · HTMX · Alpine.js · Tailwind CSS |
| Quality | ruff · mypy (strict) · import-linter · pytest |
| Tooling | uv · Docker · GitHub Actions |
Prerequisites: Python 3.12+, uv, Docker (for Postgres/Redis), Node 20+ (only to rebuild CSS).
uv sync # install dependencies into .venv
cp .env.example .env # adjust credentials (never commit real secrets)
docker compose up -d db redis
uv run alembic upgrade head
uv run uvicorn app.main:app --reload- Web UI: http://localhost:8000
- API docs (OpenAPI): http://localhost:8000/api/docs
- Health check: http://localhost:8000/health
Optional (local uv path): seed demo data (nested sections, subnets, addresses, DNS zone, DHCP scope, a section-scoped demo role) with uv run python -m scripts.seed_demo — or uv run python -m scripts.bootstrap_admin to provision the admin user. (The Docker path below covers the containerized equivalent.)
Prefer containers end to end? The compose file also runs the application —
web server and arq worker — built from the Dockerfile, alongside
Postgres and Redis. No Python or Node on the host needed to run the stack:
docker compose up -d --build # build app + worker, start all four services
# Re-run --build only when the Dockerfile or dependencies change.
docker compose exec app alembic upgrade head # apply migrations (alembic ships in the image)
docker compose logs -f # follow app/worker logs-
Web UI: http://localhost:8000 · API docs: http://localhost:8000/api/docs · Health: http://localhost:8000/health
-
The
workerservice (python -m app.worker) is included, so scheduled discovery scans and DNS/DHCP auto-sync jobs run out of the box — nothing to start separately. -
The containers get
IPAM_DATABASE_URL/IPAM_REDIS_URLfrom compose (thedb/redisservice names), so no.envis needed on this path. -
The demo/admin scripts (
scripts/) are not baked into the image, and there is no sign-up flow — so on a fresh compose stack, provision the admin user from the host against the compose DB:docker compose up -d db redis uv run python -m scripts.bootstrap_admin # or seed_demo for demo data(The host runs only
uv sync— the DB itself is the container's.)
npm install && npm run build:css # compiled output is committed; node is only needed to rebuildAll configuration is environment-driven via pydantic-settings — copy .env.example to .env and adjust. Key settings: IPAM_DATABASE_URL, IPAM_REDIS_URL, IPAM_JWT_SECRET (set a strong value in production), cookie/security flags, connection pool sizing, and the DNS/DHCP adapter settings (IPAM_DNS_SYNC_BACKEND, IPAM_PDNS_*, IPAM_BIND_*, IPAM_RFC2136_*, IPAM_DHCP_GENERATOR, IPAM_KEA_*).
A modular monolith: one deployable FastAPI service, cleanly separated by domain module, each with its own models, schemas, repository, service, router, and views.
┌──────────────────────── Web UI (Jinja2 + HTMX + Alpine + Tailwind) ─────────────────────┐
│ REST API under /api/v1 · OpenAPI docs │
├────────────────────────────── FastAPI app (async) ───────────────────────────────────────┤
│ modules: auth · users · sections · subnets · addresses · vlans · discovery · dns · │
│ dhcp · audit · custom_fields · dashboard · search │
├────────────────────────────── Service layer (business logic) ────────────────────────────┤
├────────────────────────────── Repository layer (SQLAlchemy 2.0 async) ───────────────────┤
│ PostgreSQL (inet/cidr + GiST) · Redis (jobs · idempotency · rate limits) │
└──────────────────────────── arq worker (discovery scans · DNS/DHCP sync) ─────────────────┘
Key design decisions — soft-delete policy, cursor pagination, error envelope, idempotency keys, API tokens, concurrency guarantees, the DNS/DHCP event-sync model — are documented in docs/architecture.md.
The repository is a single deployable service (modular monolith), laid out so that every domain has one clearly owned directory. The annotated tree:
openipam/
├── app/ # the FastAPI application
│ ├── main.py # app factory: routers, middleware, exception handlers
│ ├── config.py # pydantic-settings, env-driven (IPAM_ prefix)
│ ├── database.py # async engine/session setup, get_db dependency
│ ├── templating.py # Jinja2 template wiring
│ ├── worker.py # arq worker entrypoint (python -m app.worker)
│ ├── core/ # shared infrastructure — no business logic
│ ├── modules/ # one directory per domain module
│ ├── templates/ # Jinja2 templates, mirroring modules/
│ └── static/ # Tailwind CSS sources + vendored JS
├── alembic/ # database migrations
│ ├── env.py # async migration runner
│ ├── script.py.mako # migration template
│ └── versions/ # one file per schema change, in order
├── tests/
│ ├── conftest.py # shared fixtures (DB/Redis gates, client, wipes)
│ ├── unit/ # pure logic, no DB/HTTP
│ ├── integration/ # DB-backed service & repository tests
│ └── e2e/ # full HTTP flows via the ASGI test client
├── scripts/ # one-off admin & demo tooling
├── docs/ # architecture, runbooks, adapter checklists
└── .github/workflows/ # CI: lint, architecture, typecheck, test, release
| File | Purpose |
|---|---|
main.py |
create_app(): registers routers and middleware, wires the central exception handler, mounts static assets |
config.py |
Settings via pydantic-settings — every value comes from env (IPAM_*); .env.example mirrors it |
database.py |
async SQLAlchemy engine + session factory with explicit pool sizing and statement timeout, and the get_db dependency |
templating.py |
Jinja2 environment: template/static dirs, globals, filters |
worker.py |
WorkerSettings for the arq background worker (discovery scans, DNS/DHCP sync jobs); run with python -m app.worker |
core/ is deliberately limited to cross-cutting infrastructure with no business rules of its own:
| File | Purpose |
|---|---|
security.py |
password hashing, JWT encode/decode, get_current_user, API-token authentication |
permissions.py |
RBAC require_permission(...) / require_api_token(...) dependencies |
exceptions.py |
domain exception classes + the single central handler producing the error envelope |
pagination.py |
cursor-based CursorPage[T] helpers (never offset/limit) |
logging.py |
structured JSON logging with request-id correlation |
events.py |
in-process domain event bus (publish/subscribe, used for DNS/DHCP auto-sync) |
idempotency.py |
Idempotency-Key response cache for automation endpoints |
rate_limit.py |
Redis fixed-window rate limiting for burst-prone endpoints |
Each domain owns a self-contained module with the same internal layout —
this is the golden rule of the codebase: cross-module access goes through the
service layer only (import-linter enforces it in CI).
app/modules/<domain>/
├── models.py # SQLAlchemy ORM models
├── schemas.py # Pydantic request/response models
├── repository.py # DB query functions (or handled in service.py for simple modules)
├── service.py # business logic — the only thing routers/views call
├── router.py # JSON REST API routes (/api/v1/...)
├── views.py # HTML/HTMX page routes (only for modules with a UI)
└── exceptions.py # module-specific domain exceptions
| Module | Owns |
|---|---|
auth |
login, JWT access/refresh tokens, revocable scoped API tokens |
users |
users, roles, permissions, RBAC data |
sections |
hierarchical sites/folders; the access-control boundary |
subnets |
CIDR blocks: nesting, overlap detection, split/merge, utilization |
addresses |
IP records: states, locked "next free IP" allocation, bulk CSV import/export |
vlans |
VLANs and subnet↔VLAN association |
discovery |
scan jobs, schedules, reconciliation, remote-agent ingestion |
dns |
zones/records + pluggable sync backends (PowerDNS, BIND, RFC2136) |
dhcp |
scopes/reservations + ISC DHCP/Kea generators and Kea REST push |
audit |
append-only event trail + query API/UI |
custom_fields |
field-definition schema + validation, consumed by subnets/addresses |
dashboard |
home/overview page (aggregates other modules' service reads) |
search |
global subnet/address search |
templates/— Jinja2 templates, one directory per module mirroringapp/modules/;base.htmlis the shared layout (nav, section switcher) andindex.htmlthe landing/dashboard page. HTMX partials are rendered by each module'sviews.py.static/—css/input.css(Tailwind source), the committed compiledcss/main.css(rebuilt withnpm run build:css), andvendor/htmx.min.js.
Every schema change ships a numbered migration in versions/. env.py drives the async engine from IPAM_DATABASE_URL (never hardcoded). Migrations run as an explicit step (uv run alembic upgrade head) — never automatically on app boot.
| Tier | Scope |
|---|---|
tests/unit/ |
pure logic (schemas, subnet math, generators, parser) — no DB/HTTP |
tests/integration/ |
service + repository behavior against a real test Postgres (skips gracefully when unreachable) |
tests/e2e/ |
full HTTP flows through the ASGI client, including auth, permissions, and idempotency/rate-limit behavior |
conftest.py provides the shared fixtures: DB/Redis availability gates, an authenticated test client, and the network-state wipe that keeps the shared test DB repeatable.
| Script | Purpose |
|---|---|
bootstrap_admin.py |
upsert permission codes and provision the admin role/user |
seed_demo.py |
idempotent demo data (sections, subnets, DNS/DHCP, a scoped read-only role) |
discovery_agent.py |
lightweight remote scan agent that pushes results to the ingest API |
Scripts call service-layer functions only — same module-boundary rule as the app.
| Doc | Contents |
|---|---|
architecture.md |
working rules, full architecture, per-module roadmap status |
backup-restore.md |
pg_dump/pg_restore runbook + Alembic-consistency rules |
adapter-smoke-tests.md |
manual verification checklists for DNS/DHCP adapters |
production-checklist.md |
verified go-live checklist: secrets, TLS/cookies, CSRF posture, JWT defaults, ops |
troubleshooting.md |
deep-dive fixes for common uv/compose/migration/runtime issues |
| File | Purpose |
|---|---|
pyproject.toml |
project metadata (name, version, license) + ruff/mypy/import-linter/pytest/semantic-release config |
uv.lock |
locked dependency versions — CI installs with uv sync --frozen |
alembic.ini |
Alembic runtime configuration |
.env.example |
template for every supported environment variable |
docker-compose.yml |
dev stack: db, redis, app, worker |
docker-compose.test.yml |
isolated test Postgres + Redis |
Dockerfile |
image with two targets: app (web) and worker (arq) |
package.json / package-lock.json |
Tailwind CSS build (npm run build:css) |
.pre-commit-config.yaml |
local hooks mirroring CI checks |
.github/workflows/ci.yml |
CI: lint, architecture, typecheck, test, migration-check, semantic-release |
LICENSE, CONTRIBUTING.md, README.md, .gitignore |
project housekeeping |
DB-backed integration/e2e tests need a Postgres; with Docker:
docker compose -f docker-compose.test.yml up -d
TEST_DATABASE_URL=postgresql+asyncpg://ipam:ipam@localhost:5433/ipam uv run pytestWithout a DB reachable, DB-gated tests skip and the rest still runs: uv run pytest.
CI runs the full suite (including DB + Redis-backed tests) against service containers on every push/PR.
uv run ruff check .
uv run ruff format --check .
uv run mypy app/
uv run lint-imports # module-boundary contract
uv run pytest| Common problem | Quick fix |
|---|---|
uv: command not found |
Install uv (astral.sh/uv) and restart your shell. |
uv sync --frozen fails (lockfile mismatch) |
Run uv sync and commit the regenerated uv.lock. |
Port 5432 in use / db won't start |
Another Postgres owns the port; stop it or remap it in docker-compose.yml (the test stack uses 5433). |
alembic upgrade head errors / missing columns |
A migration is missing — see the migrations section of the guide. |
| DB-backed tests skip | The test DB is down — docker compose -f docker-compose.test.yml up -d, then run with TEST_DATABASE_URL=postgresql+asyncpg://ipam:ipam@localhost:5433/ipam uv run pytest. |
| Background jobs (discovery scans, DNS/DHCP sync) never run | Redis or the worker is down — see the worker section of the guide. |
| Can't log in / cookies don't stick | IPAM_COOKIE_SECURE=true needs HTTPS; bootstrap_admin won't reset an existing user's password. |
The full Troubleshooting guide covers every topic in depth — environment, lockfile, Docker/services, migrations, auth, the arq worker, testing, CSS, and pre-commit — with commands for each fix.
Still stuck? Open an issue with the failing command and its output.
Foundation, Auth, Users/Permissions, Sections, Subnets, VLANs, Addresses, Audit, Discovery, DNS, and DHCP milestones are complete, including the dashboard, global search, API tokens, idempotency, and rate limiting. The full per-module status table lives in docs/architecture.md (Part 3).
Contributions are welcome! Read CONTRIBUTING.md for the development setup, coding conventions, and PR checklist. Releases are versioned automatically from Conventional Commits via semantic-release.
OpenIPAM is free software released under the GNU General Public License v3 — see LICENSE. You may redistribute and/or modify it under the terms of the GPLv3.