Python IPAM - IP Address Management System¶
A modern, web-based IP Address Management (IPAM) system built with Flask, SQLite, Bootstrap, and DataTables.
Features¶
- π Network Management: Manage IP networks with CIDR notation
- π₯οΈ Host Management: Track IP addresses, hostnames, and MAC addresses
- π Detail Views: Click a network or IP address to see all details, related hosts/DHCP ranges, and allocation state
- πΊοΈ Address Map: Per-address grid on each network page, colour-coded by allocation state, with block navigation for large subnets
- π§ Assignment Tracking: DHCP reservations, static and dynamic assignments, leases and observations as independent facts with per-aspect provenance
- π§Ή Guarded lease retirement: Atomic, snapshot-checked deletion of expired collector-owned dynamic records; see API contract. Reservations removed at the source are deleted or converted to dynamic the same way; see release contract.
- π REST API: Complete RESTful API with Swagger UI documentation
- π Dashboard: Clear overview of network utilization
- π Advanced Search: DataTables integration for efficient data filtering
- π± Responsive Design: Bootstrap 5 for modern, mobile-friendly UI
- π³ Container-ready: Docker support for easy deployment
- β Fully Tested: Comprehensive unit tests with pytest
Local Development with pyenv¶
Prerequisites¶
- Install pyenv (if not already installed):
macOS with Homebrew:
Linux/macOS with curl:
- Shell Configuration (for bash/zsh):
Setup¶
-
Clone repository:
-
Install Python and create the project environment (pyenv + pyenv-virtualenv):
Reads the target version and env name frompyproject.toml, installs Python, createspython-ipam-env, runspyenv local, and installs the dependencies. Re-run it after a Python version bump to migrate the env.
Manual equivalent:
pyenv install 3.15
pyenv virtualenv 3.15 python-ipam-env
pyenv local python-ipam-env # writes .python-version, auto-activates
-
Alternative without pyenv-virtualenv β plain venv:
-
Install dependencies:
requirements.txtholds runtime dependencies only (it is what the production image installs); test and lint tools live inrequirements-dev.txt. -
Configure environment variables:
Optional host assignment behavior: HOST_ASSIGN_ON_CREATE=truemarks new hosts as assigned by default. API authentication and rate limiting:LEASE_RETIRE_MIN_AGE_SECONDS=3600sets the minimum grace period after lease expiry for conditional retirement (nonnegative integer; no cleanup scheduler is enabled).API_TOKENS=token1,token2enables token auth for/api/v1(empty disables).API_RATE_LIMIT=200 per minutesets the global API limit.RATELIMIT_ENABLED=truetoggles rate limiting.RATELIMIT_STORAGE_URI=memory://sets the Flask-Limiter backend. Version display:-
The UI footer shows the version from
version.txt(maintained by release-please).APP_VERSION=...overrides it; without either,devis shown. -
Initialize database (migrations):
For new schema changes, create a migration first: -
Start application:
The application will be available at: - Web Interface: http://localhost:5000 - REST API: http://localhost:5000/api/v1 - API Documentation (Swagger UI): http://localhost:5000/api/v1/docs
Running Tests¶
# Run all tests
pytest
# Tests with coverage report
pytest --cov=app --cov-report=html
# Run specific tests
pytest tests/test_models.py
# Tests in watch mode (with pytest-watch)
pip install pytest-watch
ptw
Docker Deployment¶
Production Container (Chainguard Distroless)¶
The production Docker image is built on Chainguard distroless Python images for maximum security:
Security Features: - β 0 CRITICAL/HIGH vulnerabilities (Trivy scanned) - β Multi-stage build with minimal attack surface - β Distroless runtime (no shell, package manager) - β Runs as nonroot user (UID 65532) - β Includes SBOM (Software Bill of Materials) - β Python 3.15
Image Details: - Size: ~50-100MB (vs 200-300MB for standard Python images) - Base: cgr.dev/chainguard/python:latest (distroless; currently Python 3.15) - Registry: ghcr.io/tuxpeople/python-ipam
# Pull and run production image
docker pull ghcr.io/tuxpeople/python-ipam:latest
docker run -d -p 5000:5000 \
-v $(pwd)/ipam.db:/app/ipam.db \
ghcr.io/tuxpeople/python-ipam:latest
# The container runs migrations automatically on startup.
# Set IPAM_RUN_MIGRATIONS=false to disable.
# Or use Docker Compose
docker-compose up -d
# With custom .env file
cp .env.example .env
# Edit .env for production settings
docker-compose up -d
Web UI access¶
The web UI has no built-in login yet (planned: IPAM-015). Anyone who can
reach it can change or delete data and restore backups, so put it behind
an authenticating reverse proxy (e.g. Authelia forward auth) or restrict
network access. Exclude /api/v1/ from the proxy login: the REST API
authenticates with API_TOKENS itself.
All state-changing web UI requests are CSRF-protected (Flask-WTF). Set a
random SECRET_KEY in production; it signs the session and CSRF tokens.
With FLASK_ENV=production the app logs a warning at startup while
SECRET_KEY is still one of the placeholders published in this repository.
Development¶
# Development environment with hot-reload
docker-compose --profile dev up
# Or build locally
docker build -t python-ipam:dev .
docker run -p 5000:5000 python-ipam:dev
REST API¶
The complete REST API is available at /api/v1. Interactive API documentation (Swagger UI) can be found at http://localhost:5000/api/v1/docs
API endpoints require a token when API_TOKENS is configured. Use
Authorization: Bearer <token> or X-API-Key: <token>. Rate limiting
can be enabled with RATELIMIT_ENABLED=true and configured via
API_RATE_LIMIT and RATELIMIT_STORAGE_URI.
Main Endpoints:¶
Networks:
- GET /api/v1/networks - List all networks with filtering and pagination
- GET /api/v1/networks/{id} - Get specific network
- POST /api/v1/networks - Create new network
- PUT /api/v1/networks/{id} - Update network
- DELETE /api/v1/networks/{id} - Delete network
Hosts:
- GET /api/v1/hosts - List all hosts with filtering and pagination
- GET /api/v1/hosts/{id} - Get specific host
- POST /api/v1/hosts - Create new host
- PUT /api/v1/hosts/{id} - Update host
- DELETE /api/v1/hosts/{id} - Delete host
IP Management:
- GET /api/v1/ip/networks/{id}/next-ip - Get next available IP
- GET /api/v1/ip/networks/{id}/available-ips - List all available IPs
- GET /api/v1/ip/{ip_address} - Query IP address status
See API.md for complete documentation
Project Structure¶
ipam/
βββ app.py # Flask application entry point
βββ requirements.txt # Python dependencies
βββ pytest.ini # Pytest configuration
βββ Dockerfile # Docker container definition
βββ docker-compose.yml # Docker Compose configuration
βββ .env.example # Example environment variables
βββ ipam/ # Main application package
β βββ __init__.py # Application Factory
β βββ extensions.py # Flask extensions (SQLAlchemy)
β βββ models.py # Database models
β βββ forms.py # WTForms
β βββ config.py # Configuration
β βββ api/ # REST API Blueprint
β β βββ __init__.py # API Blueprint and Swagger
β β βββ models.py # API serialization models
β β βββ networks.py # Network endpoints
β β βββ hosts.py # Host endpoints
β β βββ ip_management.py # IP management endpoints
β βββ web/ # Web Interface Blueprint
β βββ __init__.py # Web Blueprint
β βββ routes.py # Web routes
βββ templates/ # HTML Templates (Jinja2)
β βββ base.html # Base template
β βββ index.html # Dashboard
β βββ networks.html # Network overview
β βββ hosts.html # Host overview
β βββ add_network.html # Add network
β βββ add_host.html # Add host
β βββ network_detail.html # Network detail
β βββ host_detail.html # Host detail
β βββ _host_allocation.html # Allocation card (shared partial)
β βββ _subnet_map.html # Address map (network detail partial)
β βββ edit_network.html # Edit network
β βββ edit_host.html # Edit host
βββ exporters/ # Export plugins
β βββ base_exporter.py # Base exporter class
β βββ csv_exporter.py # CSV export
β βββ json_exporter.py # JSON export
β βββ dnsmasq_exporter.py # DNSmasq config export
βββ importers/ # Import plugins
β βββ base_importer.py # Base importer class
β βββ csv_importer.py # CSV import
β βββ json_importer.py # JSON import
βββ tests/ # Test suite
βββ conftest.py # Pytest fixtures
βββ test_models.py # Model tests
βββ test_routes.py # Route tests
βββ test_forms.py # Form tests
βββ test_database.py # Database tests
βββ test_export_import.py # Export/Import tests
βββ test_crud_operations.py # CRUD tests
Database Schema¶
Networks Table¶
id- Primary Keynetwork- Network address (e.g., "192.168.1.0")cidr- CIDR suffix (e.g., 24)broadcast_address- Broadcast addressname- Network name (optional)domain- DNS domain (optional)vlan_id- VLAN ID (optional)description- Description (optional)location- Location (optional)
Hosts Table¶
id- Primary Keyip_address- IP address (unique)hostname- Hostname (optional)cname- DNS alias/CNAME (optional)mac_address- MAC address (optional)description- Description (optional)status- Status (active/inactive/reserved)is_assigned- Legacy curation flag (not authoritative for allocation)last_seen- Latest actual observation of the device (UTC)discovery_source- Discovery source identifiernetwork_id- Foreign Key to Networksassignment_type- How the address is assigned: reservation, static, dynamic or unknown (nullable)assignment_source,assignment_last_confirmed_at- Provenance of the assignmentlease_expires_at,lease_source,lease_observed_at- Explicitly observed DHCP lease and its provenancelast_seen_source- Provenance oflast_seen
Assignment, lease and observation are independent; the displayed allocation state is derived from them and never stored. See API.md and ADDRESS_ASSIGNMENT_MODEL.md.
Note: After upgrading, run flask db upgrade to apply schema changes. If
your database predates Alembic migrations, run
flask db stamp 7b4a1d0f9b2a before flask db upgrade.
DHCP Ranges Table¶
id- Primary Keynetwork_id- Foreign Key to Networksstart_ip- Range start IP addressend_ip- Range end IP addressdescription- Description (optional)is_active- Whether the range is active
Backups & Restore¶
Backups are stored in BACKUP_DIR (default: ./backups).
CLI commands:
export FLASK_APP=app.py
flask backup create
flask backup list
flask backup verify ipam-backup-YYYYmmdd-HHMMSSZ.db
flask backup restore ipam-backup-YYYYmmdd-HHMMSSZ.db
Scheduled backups (cron example):
Development Guidelines¶
- Code Style: Follow PEP 8
- Tests: Write tests for new features
- Commits: Use meaningful commit messages
- Branches: Use feature branches for new development
Technology Stack¶
- Backend: Flask 3.1, SQLAlchemy 2.0, Flask-RESTX
- Frontend: Bootstrap 5, jQuery, DataTables
- Database: SQLite (production-ready for small to medium deployments)
- Testing: pytest, pytest-flask (96 tests)
- Containerization: Docker (Chainguard distroless), Docker Compose
- Security: Trivy scanning, SBOM generation, multi-stage builds
- CI/CD: GitHub Actions (tests, security scans, docs deployment)
License¶
[Specify license here]
Contributing¶
Contributions are welcome! Please create issues for bug reports or feature requests.
Network Import¶
Use the web interface's Import page to upload networks as CSV or JSON.
CSV requires Network and CIDR; optional columns include Name, Domain,
VLAN ID, Location, and Description. Column names are case-sensitive.
Network,CIDR,Name,Domain,VLAN ID,Location,Description
192.168.1.0,24,Office LAN,office.example.com,100,Office,Main office network
10.0.0.0,16,,,,Datacenter,Production servers
JSON accepts either an array of network objects or an object containing a
data array. Required keys are network and cidr; optional keys are name,
domain, vlan_id, location, and description.
{
"data": [
{
"network": "10.20.1.0",
"cidr": 24,
"vlan_id": 1,
"location": "Sennhof",
"name": "Default",
"domain": "office.example.com"
}
]
}
Names and domains have surrounding whitespace removed. For new networks,
omitted, empty, or JSON null values leave these fields unset. Existing files without these
fields remain supported. Existing networks are skipped by default.
Enable Update existing networks or hosts to update entries matched by their normalized
Network address (network in JSON), preserving their database IDs and linked
hosts/DHCP ranges. Both Network and CIDR are still required. CIDR changes
are rejected and the entire entry is skipped with a warning. Omitted optional
columns/JSON keys preserve existing values; empty values (including JSON null)
clear them. This applies to name, domain, VLAN ID, location, and description.
New addresses create new networks. Results report created, updated, and skipped
entries. The option also supports host imports (see below).
Network addresses are normalized using the supplied CIDR before matching or
saving in both formats: 10.20.1.42 with CIDR 24 becomes 10.20.1.0.
This also applies to duplicate detection and updates. Existing stored entries
with non-network addresses are not automatically corrected.
Host Import Updates¶
Enable Update existing networks or hosts when importing hosts as CSV or
JSON to update records matched by IP Address (ip_address in JSON).
Existing IDs and network associations are preserved. Without this option,
existing hosts are skipped. New IP addresses create new hosts as before.
Optional CSV columns are Hostname, MAC Address, Status, Description,
Last Seen, Discovery Source, and Is Assigned. JSON uses hostname,
mac_address, status, description, last_seen, discovery_source, and
is_assigned. Omitted fields preserve existing values. Empty values or JSON
null clear optional values; empty status resets to active, and empty
assignment resets to false on updates. Use explicit false/true to change
assignment. New hosts retain the configured assignment default when omitted
or empty. Invalid status values retain the existing import behavior of
falling back to active; invalid assignment values or timestamps skip the
entry with an error. Results report created, updated, and skipped entries.
This updates the hostname and description while preserving other host fields.
Hostnames have a matching network domain suffix removed before saving:
server01.example.com becomes server01 when the associated network's
domain is example.com. Matching is case-insensitive and accepts a
trailing DNS dot (for example, server01.example.com.). Only the complete
domain suffix, including its separating dot, is removed; remaining hostname
labels and their case are preserved. Unmatched names, names equal to the
domain, and names without an associated network/domain remain unchanged.
New hosts use the auto-detected network; updates use the existing
association and only normalize explicitly supplied hostnames. Omitted
hostnames and skipped existing hosts are not modified. This applies
consistently across CSV/JSON import, the web UI's Add/Edit Host forms, and
the REST API (POST/PUT/upsert).
Similarly, network addresses accept any host IP within the network, not
just the base address, and are normalized before matching or saving (for
example, 10.20.1.42/24 becomes 10.20.1.0/24). This also applies
consistently across import, the web UI, and the REST API. Updating a
network's CIDR is not allowed anywhere (web UI, REST API, or import); the
network address itself may still be corrected. Delete and recreate the
network if it needs a different CIDR.
Networks may not overlap. Creating or moving a network onto a range that overlaps an existing network -- as a subnet, a supernet, or any other partial overlap -- is rejected consistently across the web UI, the REST API, and import. There is currently no concept of nested/hierarchical subnets; every network is an independent, non-overlapping range.
Host List¶
The Hosts table includes a sortable Last Seen column showing the stored
last_seen timestamp as YYYY-MM-DD HH:MM:SS. Hosts without a timestamp show
-. The displayed value is not converted to the browser's local timezone.