Skip to content

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

  1. Install pyenv (if not already installed):

macOS with Homebrew:

brew install pyenv

Linux/macOS with curl:

curl https://pyenv.run | bash

  1. Shell Configuration (for bash/zsh):
    echo 'export PATH="$HOME/.pyenv/bin:$PATH"' >> ~/.bashrc
    echo 'eval "$(pyenv init -)"' >> ~/.bashrc
    echo 'eval "$(pyenv virtualenv-init -)"' >> ~/.bashrc
    source ~/.bashrc
    

Setup

  1. Clone repository:

    git clone <repository-url>
    cd ipam
    

  2. Install Python and create the project environment (pyenv + pyenv-virtualenv):

    ./scripts/pyenv.sh
    
    Reads the target version and env name from pyproject.toml, installs Python, creates python-ipam-env, runs pyenv 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

  1. Alternative without pyenv-virtualenv – plain venv:

    python -m venv venv
    source venv/bin/activate  # Linux/macOS
    # or
    venv\\Scripts\\activate  # Windows
    

  2. Install dependencies:

    pip install -r requirements.txt -r requirements-dev.txt
    
    requirements.txt holds runtime dependencies only (it is what the production image installs); test and lint tools live in requirements-dev.txt.

  3. Configure environment variables:

    cp .env.example .env
    # Edit .env as needed
    
    Optional host assignment behavior:

  4. HOST_ASSIGN_ON_CREATE=true marks new hosts as assigned by default. API authentication and rate limiting:
  5. LEASE_RETIRE_MIN_AGE_SECONDS=3600 sets the minimum grace period after lease expiry for conditional retirement (nonnegative integer; no cleanup scheduler is enabled).
  6. API_TOKENS=token1,token2 enables token auth for /api/v1 (empty disables).
  7. API_RATE_LIMIT=200 per minute sets the global API limit.
  8. RATELIMIT_ENABLED=true toggles rate limiting.
  9. RATELIMIT_STORAGE_URI=memory:// sets the Flask-Limiter backend. Version display:
  10. The UI footer shows the version from version.txt (maintained by release-please). APP_VERSION=... overrides it; without either, dev is shown.

  11. Initialize database (migrations):

    export FLASK_APP=app.py
    flask db upgrade
    
    For new schema changes, create a migration first:
    flask db migrate -m "describe change"
    flask db upgrade
    

  12. Start application:

    python app.py
    

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 Key
  • network - Network address (e.g., "192.168.1.0")
  • cidr - CIDR suffix (e.g., 24)
  • broadcast_address - Broadcast address
  • name - Network name (optional)
  • domain - DNS domain (optional)
  • vlan_id - VLAN ID (optional)
  • description - Description (optional)
  • location - Location (optional)

Hosts Table

  • id - Primary Key
  • ip_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 identifier
  • network_id - Foreign Key to Networks
  • assignment_type - How the address is assigned: reservation, static, dynamic or unknown (nullable)
  • assignment_source, assignment_last_confirmed_at - Provenance of the assignment
  • lease_expires_at, lease_source, lease_observed_at - Explicitly observed DHCP lease and its provenance
  • last_seen_source - Provenance of last_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 Key
  • network_id - Foreign Key to Networks
  • start_ip - Range start IP address
  • end_ip - Range end IP address
  • description - 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):

0 3 * * * cd /path/to/python-ipam && FLASK_APP=app.py flask backup create

Development Guidelines

  1. Code Style: Follow PEP 8
  2. Tests: Write tests for new features
  3. Commits: Use meaningful commit messages
  4. 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.

IP Address,Hostname,Description
10.20.1.10,server01,Updated description

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.