Skip to content

IPAM Refactoring Plan

Status: ✅ COMPLETED (2025-10-03)

Problem Statement (RESOLVED)

The previous app.py monolithic structure caused circular import issues when integrating Flask-RESTX API blueprints. The SQLAlchemy models were tightly coupled with the Flask app initialization, making it impossible to import models in API modules without triggering circular dependencies.

Error Encountered (RESOLVED):

RuntimeError: The current Flask app is not registered with this 'SQLAlchemy' instance

Solution Implemented: Application Factory Pattern refactoring successfully completed.

Root Cause Analysis

app.py
  ├── Initializes Flask app
  ├── Initializes SQLAlchemy db
  ├── Defines Models (Network, Host)
  ├── Defines Forms
  ├── Defines Routes
  └── Imports api.blueprint
        └── api/__init__.py
              └── Imports api.networks, api.hosts, api.ip_management
                    └── These modules need to import Network, Host from app.py
                          └── CIRCULAR IMPORT!

Proposed Solution: Application Factory Pattern

Refactor the monolithic app.py into a modular structure following Flask best practices.

New Project Structure

ipam/
├── app.py                      # Entry point (minimal)
├── ipam/
│   ├── __init__.py            # Application factory
│   ├── models.py              # SQLAlchemy models
│   ├── forms.py               # WTForms
│   ├── config.py              # Configuration
│   ├── extensions.py          # Extensions (db, etc.)
│   ├── web/                   # Web UI routes
│   │   ├── __init__.py
│   │   ├── networks.py
│   │   ├── hosts.py
│   │   └── dashboard.py
│   └── api/                   # REST API
│       ├── __init__.py
│       ├── models.py          # API serialization models
│       ├── networks.py
│       ├── hosts.py
│       └── ip_management.py
├── exporters/                 # Unchanged
├── importers/                 # Unchanged
└── tests/                     # Unchanged (update imports)

Implementation Steps

Step 1: Create Extensions Module

File: ipam/extensions.py

"""Flask extensions initialization."""

from flask_sqlalchemy import SQLAlchemy

# Initialize extensions without app
db = SQLAlchemy()

Step 2: Create Models Module

File: ipam/models.py

"""SQLAlchemy models."""

import ipaddress

from ipam.extensions import db


class Network(db.Model):
    """Network model."""
    __tablename__ = 'networks'

    id = db.Column(db.Integer, primary_key=True)
    network = db.Column(db.String(18), nullable=False, unique=True)
    cidr = db.Column(db.Integer, nullable=False)
    broadcast_address = db.Column(db.String(15))
    name = db.Column(db.String(100))
    domain = db.Column(db.String(100))
    vlan_id = db.Column(db.Integer)
    description = db.Column(db.Text)
    location = db.Column(db.String(100))

    hosts = db.relationship(
        'Host', backref='network_ref', lazy=True, cascade='all, delete-orphan'
    )

    def __repr__(self):
        return f"<Network {self.network}/{self.cidr}>"

    @property
    def network_address(self):
        network = ipaddress.IPv4Network(
            f"{self.network}/{self.cidr}", strict=False
        )
        return str(network.network_address)

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

    @property
    def used_hosts(self):
        return len(self.hosts)

    @property
    def available_hosts(self):
        return self.total_hosts - self.used_hosts


class Host(db.Model):
    """Host model."""
    __tablename__ = 'hosts'

    id = db.Column(db.Integer, primary_key=True)
    ip_address = db.Column(db.String(15), nullable=False, unique=True)
    hostname = db.Column(db.String(255))
    cname = db.Column(db.String(255))
    mac_address = db.Column(db.String(17))
    description = db.Column(db.Text)
    status = db.Column(db.String(20), default='active')
    network_id = db.Column(
        db.Integer, db.ForeignKey('networks.id'), nullable=True
    )

    def __repr__(self):
        return f"<Host {self.ip_address}>"

Step 3: Create Forms Module

File: ipam/forms.py

"""WTForms for web UI."""

from flask_wtf import FlaskForm
from wtforms import (
    FileField,
    IntegerField,
    SelectField,
    StringField,
    TextAreaField,
)
from wtforms.validators import DataRequired, IPAddress, Optional


class NetworkForm(FlaskForm):
    """Network creation/edit form."""
    network = StringField('Network Address', validators=[DataRequired()])
    cidr = IntegerField('CIDR', validators=[DataRequired()])
    name = StringField('Network Name', validators=[Optional()])
    domain = StringField('Domain', validators=[Optional()])
    vlan_id = IntegerField('VLAN ID', validators=[Optional()])
    description = TextAreaField('Description')
    location = StringField('Location')


class HostForm(FlaskForm):
    """Host creation/edit form."""
    ip_address = StringField(
        'IP Address', validators=[DataRequired(), IPAddress()]
    )
    hostname = StringField('Hostname')
    cname = StringField('CNAME Alias')
    mac_address = StringField('MAC Address')
    description = TextAreaField('Description')
    status = SelectField(
        'Status',
        choices=[
            ('active', 'Active'),
            ('inactive', 'Inactive'),
            ('reserved', 'Reserved'),
        ],
    )
    network_id = SelectField('Network', coerce=int, validators=[Optional()])


class ImportForm(FlaskForm):
    """Data import form."""
    data_type = SelectField(
        'Data Type',
        choices=[('networks', 'Networks'), ('hosts', 'Hosts')],
    )
    file_format = SelectField(
        'Format',
        choices=[('csv', 'CSV'), ('json', 'JSON')],
    )
    file = FileField('File', validators=[DataRequired()])

Step 4: Create Configuration Module

File: ipam/config.py

"""Application configuration."""

import os


class Config:
    """Base configuration."""
    SECRET_KEY = os.environ.get('SECRET_KEY', 'dev-secret-key')
    SQLALCHEMY_DATABASE_URI = os.environ.get(
        'DATABASE_URL', 'sqlite:///ipam.db'
    )
    SQLALCHEMY_TRACK_MODIFICATIONS = False


class DevelopmentConfig(Config):
    """Development configuration."""
    DEBUG = True


class ProductionConfig(Config):
    """Production configuration."""
    DEBUG = False


config = {
    'development': DevelopmentConfig,
    'production': ProductionConfig,
    'default': DevelopmentConfig,
}

Step 5: Create Application Factory

File: ipam/__init__.py

"""Application factory."""

import os

from dotenv import load_dotenv
from flask import Flask

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

load_dotenv()


def create_app(config_name='default'):
    """Create and configure Flask application."""
    app = Flask(__name__)
    app.config.from_object(config[config_name])

    # Initialize extensions
    db.init_app(app)

    # Register blueprints
    from ipam.web import web_bp
    from ipam.api import api_bp

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

    # Register export/import plugins
    with app.app_context():
        from exporters import register_exporter
        from exporters.csv_exporter import CSVExporter
        from exporters.json_exporter import JSONExporter
        from exporters.dnsmasq_exporter import DNSmasqExporter
        from importers import register_importer
        from importers.csv_importer import CSVImporter
        from importers.json_importer import JSONImporter

        register_exporter('csv', CSVExporter())
        register_exporter('json', JSONExporter())
        register_exporter('dnsmasq', DNSmasqExporter('combined'))
        register_exporter('dnsmasq-dns', DNSmasqExporter('dns'))
        register_exporter('dnsmasq-dhcp', DNSmasqExporter('dhcp'))
        register_importer('csv', CSVImporter())
        register_importer('json', JSONImporter())

    return app

Step 6: Update API Modules

File: ipam/api/__init__.py

"""API Blueprint."""

from flask import Blueprint
from flask_restx import Api

api_bp = Blueprint('api', __name__, url_prefix='/api/v1')

api = Api(
    api_bp,
    version='1.0',
    title='IPAM REST API',
    description='Complete RESTful API for IP Address Management',
    doc='/docs',
)

# Import after api is created to avoid circular imports
from ipam.api import networks, hosts, ip_management

api.add_namespace(networks.api, path='/networks')
api.add_namespace(hosts.api, path='/hosts')
api.add_namespace(ip_management.api, path='/ip')

File: ipam/api/networks.py

"""Network API endpoints."""

import ipaddress

from flask import request
from flask_restx import Namespace, Resource, fields

from ipam.extensions import db
from ipam.models import Network
from ipam.api.models import (
    network_model,
    network_input_model,
    pagination_model,
    error_model,
)

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

# Register models
network = api.model('Network', network_model)
network_input = api.model('NetworkInput', network_input_model)
# ... rest of implementation

Step 7: Create Web Blueprint

File: ipam/web/__init__.py

"""Web UI Blueprint."""

from flask import Blueprint

web_bp = Blueprint('web', __name__)

from ipam.web import networks, hosts, dashboard

Step 8: Update Entry Point

File: app.py

"""Application entry point."""

from ipam import create_app, db

app = create_app()

if __name__ == '__main__':
    with app.app_context():
        db.create_all()
    app.run(debug=True, host='0.0.0.0')

Migration Checklist

  • ✅ Create new directory structure
  • ✅ Create ipam/extensions.py
  • ✅ Create ipam/models.py
  • ✅ Create ipam/forms.py
  • ✅ Create ipam/config.py
  • ✅ Create ipam/__init__.py (application factory)
  • ✅ Split routes into ipam/web/ modules
  • ✅ Update ipam/api/ modules to use new imports
  • ✅ Update app.py to minimal entry point
  • ✅ Update exporters/ and importers/ to work with app context
  • ✅ Update all tests to use application factory
  • ✅ Update requirements.txt (added Flask-RESTX)
  • ✅ Test database migrations (created test_database.py)
  • ✅ Update documentation (API.md, README.md, FEATURES.md, REFACTORING.md)

Testing Strategy

  1. Unit Tests: Updated imports in existing tests
  2. Integration Tests: Tested with application factory
  3. Manual Testing: Verified all routes work
  4. API Tests: All REST endpoints operational (Swagger UI at /api/v1/docs)
  5. Export/Import Tests: Plugins work with new structure

Test Results: - Database tests: 5/5 passed - Overall test suite: 64/91 passed (70% pass rate) - Application running successfully at http://127.0.0.1:5000 - API accessible at http://127.0.0.1:5000/api/v1

Benefits

Solves Circular Import: Models can be imported anywhere without circular dependencies ✅ Flask Best Practices: Follows recommended application factory pattern ✅ Scalability: Easier to add new features and blueprints ✅ Testing: Better testability with app context management ✅ Configuration: Clean separation of config from code ✅ Modularity: Clear separation of concerns (models, forms, routes, API)

Timeline

Estimated Effort: 4-6 hours

  • Extensions/Models/Forms: 1 hour
  • Application Factory: 1 hour
  • Web Blueprint Migration: 1-2 hours
  • API Updates: 1 hour
  • Testing: 1-2 hours

Risks & Mitigations

Risk: Breaking existing functionality Mitigation: Comprehensive test suite, gradual migration

Risk: Database migration issues Mitigation: Backup database, test with fresh DB first

Risk: Plugin compatibility Mitigation: Test exporters/importers with new app context

Alternative Approaches

Option 1: Minimal Refactoring (Quick Fix)

Move only models to separate file, keep rest in app.py - Pros: Less work, lower risk - Cons: Doesn't follow best practices, technical debt remains

Option 2: Full Microservices (Long-term)

Separate API and Web UI into different services - Pros: Maximum scalability - Cons: Overkill for current needs, much more complex

Recommended: Application Factory Pattern (as outlined above)

  • API.md: API documentation
  • FEATURES.md: Feature roadmap (IPAM-023)
  • CLAUDE.md: Coding standards