Skip to content

CLAUDE.md - Python IPAM Project

Claude Code-specific configuration and standards for the Python IPAM project.

Documentation Standards

IMPORTANT LANGUAGE REQUIREMENT: - ALL documentation MUST be written in English - This includes: README.md, API.md, FEATURES.md, REFACTORING.md, CLAUDE.md, code comments, commit messages - German or other languages are NOT permitted in documentation

DOCUMENTATION UPDATE POLICY: - Every code change MUST be evaluated for documentation impact - Before committing ANY change, ask: "Does this require documentation updates?" - Documentation files to check: - README.md - User-facing documentation, setup instructions - API.md - REST API endpoint documentation - FEATURES.md - Feature tracking and roadmap - REFACTORING.md - Architecture and refactoring notes - CLAUDE.md - This file, project standards - Documentation updates should be committed together with code changes - Never commit code without updating related documentation

Release Guardrails

  • Do not tag or create a release unless the latest CI build is successful.
  • Before merging the release-please PR: check GitHub's Security → Code scanning tab for new CRITICAL/HIGH Trivy findings since the last release.
  • Fix available (dependency bump, base image update) → apply as a normal fix/docker commit first, let it flow into the same or next release.
  • No fix available / false positive → dismiss in the GitHub UI with a reason ("no fix available", "not affected") instead of ignoring it, so it doesn't stay open indefinitely.
  • Claude Code must not merge a release PR while an unaddressed CRITICAL finding is open – flag it to the user first.

Testing Expectations

  • Add or update tests for new features and behavior changes.
  • Tests must never touch ./ipam.db. tests/conftest.py sets DATABASE_URL=sqlite:///:memory: before ipam is imported, because the engine is bound inside create_app(); changing app.config["SQLALCHEMY_DATABASE_URI"] afterwards has no effect. Use create_app(name, {"SQLALCHEMY_DATABASE_URI": ...}) when a test needs its own database file.

Project Overview

Python IPAM is a modern IP Address Management (IPAM) system based on: - Flask 3.0 (Python Web Framework) - SQLite/SQLAlchemy (Database) - Bootstrap 5 + DataTables (Frontend) - pytest (Testing Framework) - Docker (Containerization)

Code Style Rules

Python (Google Style Guide)

Formatter/Linter: Ruff, 80 character line length (config in pyproject.toml)

ruff check .
ruff format --check .

Naming Conventions: - Modules/Packages: lowercase_with_underscores - Classes: CapWords (e.g., NetworkForm, HostModel) - Functions/Variables: lowercase_with_underscores - Constants: CAPS_WITH_UNDERSCORES - Private: _leading_underscore

Import Order (separated by blank lines):

from __future__ import annotations

import os
import ipaddress

from flask import Flask, render_template
from flask_sqlalchemy import SQLAlchemy

from ipam import db, Network, Host

Dockerfile (Hadolint Standard)

Follow security best practices:

FROM python:3.15-slim

# Security: Non-root user
RUN useradd -r -s /bin/false ipam
USER ipam

# Layer optimization
RUN apt-get update && \
    apt-get install -y --no-install-recommends gcc && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

# JSON notation for CMD
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]

Git Commits (Angular Convention)

Format:

<type>(<scope>): <short summary>

<body>

<footer>

Types for this project: - feat: New IPAM features (Host/Network management) - fix: Bug fixes in web UI or API - deps: Dependency updates (generated by Renovate) - test: Unit tests for models/routes/forms - docker: Container configuration - docs: README or API documentation

Examples:

feat(networks): add VLAN ID support
fix(hosts): resolve auto-detection for overlapping subnets
test(models): add network cascade delete tests
docker: optimize layer caching in Dockerfile
docs: update pyenv setup instructions

IMPORTANT: Do not use AI signatures in commits!

Shell/Bash

For test scripts and setup automation:

#!/bin/bash

# Functions
function setup_venv() {
  local python_version="$1"
  echo "Setting up Python ${python_version}..."
}

# Prefer [[ ]]
if [[ -d "venv" ]]; then
  source venv/bin/activate
fi

# Quote variables
echo "PATH: ${PATH}"

Project-Specific Standards

Testing

Test Structure:

tests/
├── conftest.py              # Pytest fixtures (Application Factory)
├── test_models.py           # SQLAlchemy Model Tests
├── test_routes.py           # Flask Route Tests
├── test_forms.py            # WTForms Validation Tests
├── test_database.py         # Database initialization tests
├── test_export_import.py    # Export/Import plugin tests
├── test_crud_operations.py  # CRUD operation tests
└── test_address_assignment.py # Assignment/lease/observation model

Test Commands:

# All tests
pytest -v

# With coverage
pytest --cov=ipam --cov-report=html

# Specific tests
pytest tests/test_models.py::TestNetworkModel::test_network_properties

# Database tests
pytest tests/test_database.py -v

Database Models

SQLAlchemy models in ipam/models.py:

from ipam.extensions import db

class Network(db.Model):
    __tablename__ = 'networks'  # Explicit table names

    # Primary Keys as first column
    id = db.Column(db.Integer, primary_key=True)

    # Relationships with cascade definitions
    hosts = db.relationship('Host', backref='network_ref',
                           cascade='all, delete-orphan')

    # Properties for calculated values
    @property
    def total_hosts(self):
        return len(list(ipaddress.IPv4Network(
            f"{self.network}/{self.cidr}", strict=False
        ).hosts()))

Flask Application Structure

Application Factory Pattern in ipam/__init__.py:

from ipam.extensions import db
from ipam.config import config

def create_app(config_name='default'):
    app = Flask(__name__)
    app.config.from_object(config[config_name])

    db.init_app(app)

    from ipam.web import web_bp
    from ipam.api import api_bp

    app.register_blueprint(web_bp)
    app.register_blueprint(api_bp)

    return app

Flask Routes

Web Routes in ipam/web/routes.py:

from ipam.web import web_bp
from ipam.extensions import db
from ipam.models import Network, Host

@web_bp.route('/')
def index():
    """Dashboard."""
    networks = Network.query.all()
    return render_template('index.html', networks=networks)

API Routes in ipam/api/networks.py:

from flask_restx import Namespace, Resource
from ipam.extensions import db
from ipam.models import Network

api = Namespace('networks', description='Network operations')

@api.route('/')
class NetworkList(Resource):
    def get(self):
        """List all networks."""
        networks = Network.query.all()
        return {'data': [network.to_dict() for network in networks]}

Templates

HTML/Jinja2 template standards: - Bootstrap 5 CSS classes - Responsive design - DataTables for tables - Bootstrap Icons (bi-*)

MCP Servers

This project is configured with MCP servers via .mcp.json:

  • context7: fetch up-to-date documentation for Flask, SQLAlchemy, and other libraries instead of relying on potentially outdated training knowledge
  • playwright: browser automation for end-to-end testing of the web UI
  • sqlite: direct inspection of ./ipam.db (schema, ad-hoc debug queries)

On first use in Claude Code, approve the servers when prompted (/mcp shows connection status).

Environment & Tools

Local Development

pyenv Setup (pyenv + pyenv-virtualenv):

./scripts/pyenv.sh
Reads the Python version (requires-python) and project name from pyproject.toml, installs the interpreter, creates/migrates the python-ipam-env virtualenv, runs pyenv local, and installs requirements.txt + requirements-dev.txt. Re-run it after bumping requires-python.

Manual equivalent:

pyenv install 3.15
pyenv virtualenv 3.15 python-ipam-env
pyenv local python-ipam-env   # writes .python-version (the env name), auto-activates
pip install -r requirements.txt -r requirements-dev.txt

Database Initialization:

# Create database (on first start)
python3 -c "from ipam import create_app; from ipam.extensions import db; app = create_app(); app.app_context().push(); db.create_all()"

Development Server:

python app.py  # Flask dev server on :5000

# Available URLs:
# - Web Interface: http://localhost:5000
# - REST API: http://localhost:5000/api/v1
# - Swagger UI: http://localhost:5000/api/v1/docs

Mobile/iPhone Access (for testing responsive UI):

cloudflared tunnel --url http://localhost:5000
Provides a temporary public URL pointing to the local dev server.

Docker Development

Development Container:

docker-compose --profile dev up

Production Container:

docker-compose up -d

Quality Assurance

Linting/Formatting (local):

# Python Code (requirements-dev.txt: ruff, pytest-cov)
ruff check .
ruff format --check .

# Dockerfile
hadolint Dockerfile

# Shell Scripts
shellcheck run_tests.sh

Testing:

./run_tests.sh  # Complete test suite with coverage
make test       # Makefile target

Pre-Commit Quality Checklist

  • [ ] ruff check . clean?
  • [ ] ruff format --check . clean?
  • [ ] pytest -v – all tests passing?
  • [ ] No *.db file committed?
  • [ ] No API key / secret in code?
  • [ ] Conventional Commit format followed?
  • [ ] No AI signature in the commit?
  • [ ] Documentation checked: does README.md, API.md, FEATURES.md, or CLAUDE.md/AGENTS.md need updating for this change?

API Documentation

REST API Base URL: /api/v1 Swagger UI: http://localhost:5000/api/v1/docs

Main Endpoints:

Networks: - GET /api/v1/networks - List all networks (with filtering/pagination) - POST /api/v1/networks - Create network - GET /api/v1/networks/{id} - Get 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/pagination) - POST /api/v1/hosts - Create host - GET /api/v1/hosts/{id} - Get host - PUT /api/v1/hosts/{id} - Update host - DELETE /api/v1/hosts/{id} - Delete host (unconditional manual operation) - POST /api/v1/hosts/{id}/retire-lease - Atomic conditional retirement of an expired dynamic collector record; full expected snapshot required, see API.md. Server minimum grace: LEASE_RETIRE_MIN_AGE_SECONDS (3600 default). No scheduler enabled. - POST /api/v1/hosts/{id}/release-reservation - Atomic conditional delete or conversion to dynamic of a reservation removed at its source; full expected snapshot required, see API.md.

IP Management: - GET /api/v1/ip/networks/{id}/next-ip - Get next free IP - GET /api/v1/ip/networks/{id}/available-ips - List all free IPs - GET /api/v1/ip/{ip_address} - Query IP status

Response Format:

{
  "data": [...],
  "pagination": {
    "page": 1,
    "per_page": 50,
    "total_items": 100,
    "total_pages": 2
  }
}

Complete documentation: API.md

Deployment

Container Images

Production Images (Chainguard distroless): - Registry: ghcr.io/tuxpeople/python-ipam - Base: cgr.dev/chainguard/python:latest (distroless; versioned tags need a Chainguard subscription, so we track latest) - Security: 0 CRITICAL/HIGH vulnerabilities (Trivy scanned) - Size: ~50-100MB (multi-stage build) - User: nonroot (UID 65532) - Python: 3.15 (from latest; keep pyproject.toml requires-python in step)

Tags: - latest - Always points to latest stable release - 1.0.0 - Specific patch version - 1.0 - Minor version (updates with new patches) - 1 - Major version (updates with new minors/patches)

All tags are created by the release.yml image job after a release-please PR merge – a regular main push does not produce a new image (see ## Releases above).

Multi-stage Build:

# Stage 1: Build dependencies in -dev image
FROM cgr.dev/chainguard/python:latest-dev AS build
RUN python -m venv /tmp/venv && \
    /tmp/venv/bin/pip install --upgrade 'pip<25.2' setuptools wheel

# Stage 2: Install Python packages
FROM build AS build-venv
COPY requirements.txt /tmp/requirements.txt
RUN /tmp/venv/bin/pip install -r /tmp/requirements.txt

# Stage 3: Minimal runtime image (distroless)
FROM cgr.dev/chainguard/python:latest
COPY --from=build-venv /tmp/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
RUN mkdir -p /tmp && chmod 1777 /tmp

Kubernetes (if used):

# Follow Kubernetes best practices
metadata:
  labels:
    app.kubernetes.io/name: python-ipam
    app.kubernetes.io/version: "1.0.0"
    app.kubernetes.io/component: backend
spec:
  securityContext:
    runAsNonRoot: true
    runAsUser: 65532
    fsGroup: 65532

Troubleshooting

Common Issues:

  1. SQLite Lock Issues:

    # Use context manager
    with app.app_context():
        db.session.commit()
    

  2. pyenv Python not found:

    pyenv rehash
    which python  # Should show pyenv version
    

  3. Docker Build Failures:

    docker system prune  # Cleanup
    docker build --no-cache .
    

Maintenance

Dependencies Update:

pip list --outdated
pip install -U package_name
Update the pinned version in requirements.txt (runtime, shipped in the image) or requirements-dev.txt (tests/lint) by hand. Do not use pip freeze > requirements.txt: it would copy dev tools into the image. Renovate normally proposes these bumps.

Database Migrations (on schema changes):

# In Python Console
from ipam import create_app
from ipam.extensions import db

app = create_app()
with app.app_context():
    db.drop_all()  # Caution in production!
    db.create_all()

Database Initialization (Command line):

python3 -c "from ipam import create_app; from ipam.extensions import db; app = create_app(); app.app_context().push(); db.create_all()"

Release Process

Semantic Versioning

This project follows Semantic Versioning 2.0.0:

Version Format: MAJOR.MINOR.PATCH

  • MAJOR: Breaking changes (API changes, database schema changes)
  • MINOR: New features (backwards compatible)
  • PATCH: Bug fixes (backwards compatible)

Examples: - v1.0.0 - First production release - v1.1.0 - New features (e.g., subnet calculator) - v1.1.1 - Bug fixes - v2.0.0 - Breaking changes (e.g., database schema migration)

Creating a Release

Automated via release-please – no manual git tag or gh release create anymore.

Flow: 1. Merge Conventional-Commit-formatted commits (feat, fix, ...) to main as usual (PR or direct commit). 2. release-please opens/updates a release PR (chore(main): release X.Y.Z) with an auto-generated changelog preview. 3. Merge that PR when you want to ship a release. This automatically: - Creates the Git tag vX.Y.Z and a GitHub Release with categorized changelog - Triggers the image job in release.yml (Docker image + SBOM + Trivy scan, pushed to GHCR with X.Y.Z, X.Y, X, latest tags) 4. No manual tag, no manual release notes, no manual Docker verification step – all of the above happens automatically from the merge.

One-time setup (already done for this repo, see below): - Repo-Setting Settings → Actions → General → Workflow permissions → "Allow GitHub Actions to create and approve pull requests" enabled - Settings → General → Pull Requests → "Allow merge commits" disabled (squash-merge only) – otherwise the release-please changelog gets duplicate entries - release-please runs with a GitHub App token (actions/create-github-app-token), not GITHUB_TOKEN: release PRs opened with GITHUB_TOKEN need manual approval before their checks run. The App (Contents, Pull requests, Issues: read & write; no webhook) is installed only on the repos that use release-please (python-ipam, dorflade-mhd); each workflow's token is scoped to its own repository. Repo variable RELEASE_APP_CLIENT_ID and secret RELEASE_APP_PRIVATE_KEY (a key used only by this repo). To rotate: create a new key in the App settings, update the secret, delete the old key.

Version bump mapping: feat → Minor, fix/deps/docker/perf → Patch, feat! or BREAKING CHANGE: footer → Major. Config: release-please-config.json, current version tracked in .release-please-manifest.json.

Hotfix Release Process

No special process needed – a fix: commit on main is picked up by release-please like any other change. Merge the resulting release PR to ship it immediately.

CI/CD Integration

Two workflows, split by cost – cheap checks on every push, the expensive build only on release:

  • tests.yml (every push/PR to main/develop): lint (Ruff check+format), pytest (tests+coverage), hadolint (Dockerfile), image-scan (fast, single-arch Trivy scan, no push) – all four run in parallel.
  • release.yml calls the reusable workflows release-please.yml and container-release.yml from tuxpeople/shared, pinned by SHA (push to main maintains the release-please PR; only merging it, or manual workflow_dispatch, runs the tests and image jobs):
  • tests: the complete tests.yml as safety net; a failure blocks the image
  • Multi-arch Docker build (linux/amd64,linux/arm64), pushed to GHCR
  • Image signed with cosign (keyless, by digest)
  • Cosign keyless signature, verified right after signing. The certificate names container-release.yml in tuxpeople/shared, see the header of that file for the cosign verify command
  • SBOM generation (Trivy, SPDX) – signed attestation in the registry (actions/attest, together with the build provenance) and uploaded to the GitHub Release; verify with gh attestation verify oci://ghcr.io/tuxpeople/python-ipam:latest --owner tuxpeople
  • Trivy scan: SARIF upload to Code scanning (informational); the blocking scan is image-scan in tests.yml (only findings with an available fix)
  • Tags: X.Y.Z, X.Y, X, latest (read from version.txt, which release-please's simple release-type maintains)

A regular main push therefore does not produce a new image – only a merged release PR does. See ## Releases above for the full flow.


Last Update: 2025-10-03 Maintainer: Python IPAM Team