Skip to content

Phase 5: Neo4j Graph Database Integration

Objective

Add pre-computed relationship intelligence using Neo4j graph database to enable instant complex queries while maintaining the reliable tool chain foundation as backup for edge cases.

Strategic Approach

Transform network infrastructure data from relational tables into a graph-first data model that mirrors the physical reality of network relationships, enabling sub-second traversal of complex network topologies and dependencies.

graph TD
    A[NetBox Relational Data] --> B[Graph Schema Mapping]
    B --> C[Neo4j Graph Database]
    C --> D[Cypher Query Engine]
    D --> E[Complex Relationship Queries]

    F[Real-time Updates] --> G[NetBox Webhooks]  
    G --> H[Graph Sync Engine]
    H --> C

    I[Query Router] --> J{Query Complexity}
    J -->|Simple| K[Direct NetBox API]
    J -->|Complex| D
    J -->|Hybrid| L[Graph + API Combination]

    style C fill:#4caf50
    style E fill:#e8f5e8

Schema Discovery & Design

NetBox to Graph Schema Mapping

Core Entity Types:

// Primary network entities
CREATE (s:Site {name: 'HQ-DC1', slug: 'hq-dc1'})
CREATE (r:Rack {name: 'A-01', position: 42})  
CREATE (d:Device {name: 'SW-CORE-01', device_type: 'Catalyst-9500'})
CREATE (i:Interface {name: 'GigE1/0/1', type: 'ethernet'})
CREATE (c:Cable {id: 'CAB-001', type: 'cat6'})
CREATE (v:VLAN {vid: 100, name: 'Production'})
CREATE (ip:IPAddress {address: '192.168.100.1/24'})

// Hierarchical relationships
CREATE (s)-[:CONTAINS]->(r)
CREATE (r)-[:HOUSES]->(d)  
CREATE (d)-[:HAS_INTERFACE]->(i)

// Network connectivity
CREATE (i)-[:CONNECTED_VIA]->(c)
CREATE (c)-[:TERMINATES_AT]->(i2:Interface)
CREATE (i)-[:MEMBER_OF]->(v)
CREATE (i)-[:ASSIGNED_IP]->(ip)

// Logical relationships
CREATE (v)-[:SPANS_DEVICE]->(d)
CREATE (ip)-[:ALLOCATED_TO]->(d)

Relationship Pattern Analysis

Identified Key Patterns:

// Device-to-device connectivity (Layer 1)
MATCH (d1:Device)-[:HAS_INTERFACE]->(i1:Interface)-[:CONNECTED_VIA]->
      (c:Cable)<-[:CONNECTED_VIA]-(i2:Interface)<-[:HAS_INTERFACE]-(d2:Device)
RETURN d1.name, d2.name, c.type

// VLAN spanning analysis (Layer 2)  
MATCH (v:VLAN)-[:SPANS_DEVICE]->(d:Device)
RETURN v.name, count(d) as device_count, collect(d.name) as devices

// IP subnet utilization (Layer 3)
MATCH (subnet:Prefix)-[:CONTAINS_IP]->(ip:IPAddress)-[:ALLOCATED_TO]->(d:Device)
RETURN subnet.cidr, count(ip) as used_ips, collect(d.name) as devices

// Power dependency chains  
MATCH (d:Device)-[:POWERED_BY]->(outlet:PowerOutlet)-[:FED_BY]->
      (pdu:PDU)-[:CONNECTED_TO]->(ups:UPS)
RETURN d.name, pdu.name, ups.name

Data Population Pipeline

Initial Graph Construction

import asyncio
from neo4j import AsyncGraphDatabase
from netbox_client import NetBoxAPI

class GraphPopulationEngine:
    def __init__(self, neo4j_uri: str, netbox_client: NetBoxAPI):
        self.driver = AsyncGraphDatabase.driver(neo4j_uri, auth=('neo4j', 'password'))
        self.netbox = netbox_client

    async def populate_initial_graph(self):
        """
        Parallel extraction and graph population from NetBox
        """
        # Extract all entity types concurrently
        extraction_tasks = [
            self.extract_sites(),
            self.extract_devices(), 
            self.extract_interfaces(),
            self.extract_cables(),
            self.extract_vlans(),
            self.extract_ip_addresses(),
            self.extract_power_infrastructure()
        ]

        extracted_data = await asyncio.gather(*extraction_tasks)

        # Build graph in dependency order
        async with self.driver.session() as session:
            await session.execute_write(self.create_sites, extracted_data[0])
            await session.execute_write(self.create_devices, extracted_data[1])
            await session.execute_write(self.create_interfaces, extracted_data[2])
            await session.execute_write(self.create_relationships, extracted_data)

        print("Initial graph population completed")

    async def create_devices(self, tx, devices_data):
        """
        Batch create device nodes with properties
        """
        cypher = """
        UNWIND $devices as device
        CREATE (d:Device {
            id: device.id,
            name: device.name,
            device_type: device.device_type.model,
            serial: device.serial,
            status: device.status.value,
            site_id: device.site.id,
            rack_id: device.rack.id
        })
        """
        await tx.run(cypher, devices=devices_data)

    async def create_relationships(self, tx, all_data):
        """
        Create relationships between entities in batch operations
        """
        # Site -> Device relationships
        site_device_cypher = """
        MATCH (s:Site {id: $site_id})
        MATCH (d:Device {site_id: $site_id})
        CREATE (s)-[:CONTAINS]->(d)
        """

        # Interface -> Device relationships  
        interface_device_cypher = """
        MATCH (d:Device {id: $device_id})
        MATCH (i:Interface {device_id: $device_id})
        CREATE (d)-[:HAS_INTERFACE]->(i)
        """

        # Cable connections (most complex)
        cable_connection_cypher = """
        MATCH (c:Cable {id: $cable_id})
        MATCH (i1:Interface {id: $termination_a_id})
        MATCH (i2:Interface {id: $termination_b_id})
        CREATE (i1)-[:CONNECTED_VIA]->(c)<-[:CONNECTED_VIA]-(i2)
        """

        # Execute relationship creation in parallel where possible
        relationship_tasks = [
            tx.run(site_device_cypher, site_id=site_id)
            for site_id in extract_site_ids(all_data)
        ]

        await asyncio.gather(*relationship_tasks)

Data Quality Assurance

// Constraint definitions for data integrity
CREATE CONSTRAINT device_name_unique FOR (d:Device) REQUIRE d.name IS UNIQUE;
CREATE CONSTRAINT interface_device_interface_unique FOR (i:Interface) REQUIRE (i.device_id, i.name) IS UNIQUE;
CREATE CONSTRAINT cable_id_unique FOR (c:Cable) REQUIRE c.id IS UNIQUE;

// Data validation queries
// Find devices without sites (orphaned devices)
MATCH (d:Device) 
WHERE NOT (d)<-[:CONTAINS]-(:Site)
RETURN d.name, d.id

// Find cables with missing terminations
MATCH (c:Cable)
WHERE NOT (c)-[:TERMINATES_AT]->(:Interface)
RETURN c.id, c.label

// Find IP addresses without device assignments
MATCH (ip:IPAddress)
WHERE NOT (ip)-[:ALLOCATED_TO]->(:Device)
RETURN ip.address, ip.status

Graph Query Capabilities

Complex Relationship Queries

Network Path Discovery:

// Find all possible paths between two devices
MATCH path = (d1:Device {name: 'SW-ACCESS-01'})-[*1..5]-(d2:Device {name: 'RTR-CORE-01'})
WHERE ALL(r in relationships(path) WHERE type(r) IN ['CONNECTED_VIA', 'HAS_INTERFACE'])
RETURN path, length(path) as hop_count
ORDER BY hop_count ASC
LIMIT 5

Impact Analysis:

// Find all devices affected if a specific device fails
MATCH (failed:Device {name: 'SW-CORE-01'})
MATCH (affected:Device)-[*1..3]-(failed)
WHERE affected <> failed
RETURN DISTINCT affected.name, affected.device_type, 
       shortestPath((affected)-[*]-(failed)) as dependency_path

VLAN Topology Mapping:

// Complete VLAN 100 topology with device interconnections
MATCH (v:VLAN {vid: 100})-[:SPANS_DEVICE]->(d:Device)
OPTIONAL MATCH (d)-[:HAS_INTERFACE]->(i:Interface)-[:MEMBER_OF]->(v)
OPTIONAL MATCH (i)-[:CONNECTED_VIA]->(c:Cable)<-[:CONNECTED_VIA]-(i2:Interface)
OPTIONAL MATCH (i2)<-[:HAS_INTERFACE]-(d2:Device)
WHERE (v)-[:SPANS_DEVICE]->(d2)
RETURN v.name, 
       collect(DISTINCT d.name) as vlan_devices,
       collect(DISTINCT {
           from_device: d.name,
           from_interface: i.name,
           to_device: d2.name, 
           to_interface: i2.name,
           cable: c.id
       }) as inter_device_connections

Advanced Analytical Queries

Capacity Planning Analysis:

// Site utilization analysis with growth projections
MATCH (s:Site)-[:CONTAINS]->(r:Rack)-[:HOUSES]->(d:Device)
WITH s, r, count(d) as device_count, r.u_height as total_units
MATCH (r)-[:HOUSES]->(d2:Device)
WITH s, r, device_count, total_units, sum(d2.device_type.u_height) as used_units
RETURN s.name,
       count(r) as total_racks,
       sum(used_units) as total_used_units,
       sum(total_units) as total_available_units,
       round(100.0 * sum(used_units) / sum(total_units), 2) as utilization_percent,
       sum(total_units) - sum(used_units) as available_capacity
ORDER BY utilization_percent DESC

Single Point of Failure Detection:

// Find critical devices whose failure would isolate other devices
MATCH (critical:Device)
WHERE EXISTS {
    MATCH (isolated:Device)-[*1..2]-(critical)
    WHERE isolated <> critical
    AND NOT EXISTS {
        MATCH (isolated)-[*1..3]-(alternate:Device)-[*1..2]-(target:Device)
        WHERE target <> critical 
        AND target <> isolated
        AND alternate <> critical
        AND alternate <> isolated
    }
}
RETURN critical.name, critical.device_type,
       [(critical)-[*1..2]-(dependent) WHERE dependent <> critical | dependent.name] as dependent_devices

Synchronization System

Real-time Updates via NetBox Webhooks

from fastapi import FastAPI, BackgroundTasks
from neo4j import AsyncGraphDatabase
import json

app = FastAPI()

class GraphSyncEngine:
    def __init__(self, neo4j_uri: str):
        self.driver = AsyncGraphDatabase.driver(neo4j_uri)

    async def handle_webhook_event(self, event_type: str, object_type: str, 
                                 data: dict, snapshot: dict):
        """
        Process NetBox webhook events and sync to graph
        """
        handler_map = {
            ('created', 'device'): self.handle_device_created,
            ('updated', 'device'): self.handle_device_updated, 
            ('deleted', 'device'): self.handle_device_deleted,
            ('created', 'cable'): self.handle_cable_created,
            ('deleted', 'cable'): self.handle_cable_deleted,
        }

        handler = handler_map.get((event_type, object_type))
        if handler:
            await handler(data, snapshot)
        else:
            print(f"Unhandled webhook: {event_type} {object_type}")

    async def handle_device_created(self, data: dict, snapshot: dict):
        """
        Add new device to graph with relationships
        """
        async with self.driver.session() as session:
            # Create device node
            await session.execute_write(self.create_device_node, data)

            # Create site relationship if site exists
            if data.get('site'):
                await session.execute_write(self.link_device_to_site, 
                                          data['id'], data['site']['id'])

            # Create rack relationship if rack assigned
            if data.get('rack'):
                await session.execute_write(self.link_device_to_rack,
                                          data['id'], data['rack']['id'])

    async def handle_cable_created(self, data: dict, snapshot: dict):
        """
        Add cable with termination relationships
        """
        async with self.driver.session() as session:
            cypher = """
            CREATE (c:Cable {
                id: $cable_id,
                label: $label,
                type: $cable_type,
                length: $length,
                status: $status
            })

            WITH c
            MATCH (i1:Interface {id: $term_a_id})
            MATCH (i2:Interface {id: $term_b_id})
            CREATE (i1)-[:CONNECTED_VIA]->(c)<-[:CONNECTED_VIA]-(i2)
            """

            await session.execute_write(
                lambda tx: tx.run(cypher, 
                    cable_id=data['id'],
                    label=data.get('label', ''),
                    cable_type=data.get('type', {}).get('name', ''),
                    length=data.get('length'),
                    status=data.get('status', {}).get('value', ''),
                    term_a_id=data['a_terminations'][0]['object_id'],
                    term_b_id=data['b_terminations'][0]['object_id']
                )
            )

sync_engine = GraphSyncEngine("bolt://localhost:7687")

@app.post("/netbox-webhook")
async def netbox_webhook(webhook_data: dict, background_tasks: BackgroundTasks):
    """
    Receive NetBox webhook and queue graph sync
    """
    background_tasks.add_task(
        sync_engine.handle_webhook_event,
        webhook_data['event'],
        webhook_data['model'],  
        webhook_data['data'],
        webhook_data['snapshots']
    )

    return {"status": "accepted"}

Batch Synchronization Safety Net

import schedule
import time
from datetime import datetime, timedelta

class GraphConsistencyChecker:
    def __init__(self, netbox_client, neo4j_driver):
        self.netbox = netbox_client
        self.driver = neo4j_driver

    async def daily_consistency_check(self):
        """
        Compare NetBox and Neo4j data for inconsistencies
        """
        print(f"Starting consistency check at {datetime.now()}")

        inconsistencies = []

        # Check device count consistency
        netbox_device_count = len(await self.netbox.dcim.devices.all())

        async with self.driver.session() as session:
            neo4j_result = await session.run("MATCH (d:Device) RETURN count(d) as device_count")
            neo4j_device_count = (await neo4j_result.single())['device_count']

        if netbox_device_count != neo4j_device_count:
            inconsistencies.append({
                'type': 'device_count_mismatch',
                'netbox_count': netbox_device_count,
                'neo4j_count': neo4j_device_count,
                'severity': 'high'
            })

        # Check for orphaned relationships
        async with self.driver.session() as session:
            orphaned_query = """
            MATCH (c:Cable)-[:CONNECTED_VIA]-(i:Interface)
            WHERE NOT EXISTS {
                MATCH (i)<-[:HAS_INTERFACE]-(:Device)
            }
            RETURN count(DISTINCT c) as orphaned_cables
            """
            orphaned_result = await session.run(orphaned_query)
            orphaned_count = (await orphaned_result.single())['orphaned_cables']

        if orphaned_count > 0:
            inconsistencies.append({
                'type': 'orphaned_cable_relationships',
                'count': orphaned_count,
                'severity': 'medium'
            })

        if inconsistencies:
            await self.handle_inconsistencies(inconsistencies)
        else:
            print("Graph consistency check passed")

    async def handle_inconsistencies(self, inconsistencies):
        """
        Handle detected inconsistencies with automated repair
        """
        for issue in inconsistencies:
            if issue['severity'] == 'high':
                print(f"CRITICAL: {issue}")
                await self.trigger_full_resync()
            else:
                print(f"WARNING: {issue}")
                await self.repair_specific_issue(issue)

# Schedule daily consistency checks
schedule.every().day.at("02:00").do(lambda: asyncio.run(
    GraphConsistencyChecker(netbox_client, neo4j_driver).daily_consistency_check()
))

Hybrid System Integration

Smart Query Routing

class HybridQueryRouter:
    def __init__(self, neo4j_driver, netbox_client):
        self.graph_db = neo4j_driver
        self.netbox = netbox_client
        self.performance_tracker = QueryPerformanceTracker()

    async def route_query(self, classified_intent: Dict[str, Any]) -> ExecutionStrategy:
        """
        Decide whether to use graph database or direct API calls
        """
        query_characteristics = self.analyze_query_characteristics(classified_intent)

        # Graph database is optimal for:
        if (query_characteristics['relationship_depth'] > 2 or
            query_characteristics['involves_path_finding'] or
            query_characteristics['requires_aggregation'] or
            query_characteristics['involves_multiple_object_types'] > 3):

            # Check graph data freshness
            graph_freshness = await self.check_graph_freshness(classified_intent['target_objects'])

            if graph_freshness['is_acceptable']:
                return ExecutionStrategy(
                    primary='neo4j_graph',
                    fallback='netbox_api',
                    estimated_performance='sub_second'
                )
            else:
                return ExecutionStrategy(
                    primary='hybrid_refresh',
                    steps=['refresh_graph_subset', 'execute_graph_query'],
                    estimated_performance='fast'
                )

        # Direct API is better for:
        elif (query_characteristics['single_object_lookup'] or
              query_characteristics['requires_realtime_data'] or
              query_characteristics['simple_list_query']):

            return ExecutionStrategy(
                primary='netbox_api',
                optimization='use_cache',
                estimated_performance='fast'
            )

        # Hybrid approach for complex edge cases
        else:
            return ExecutionStrategy(
                primary='hybrid_combination',
                steps=['graph_analysis', 'api_enrichment', 'combine_results'],
                estimated_performance='moderate'
            )

    async def execute_graph_query(self, cypher_query: str, parameters: dict) -> dict:
        """
        Execute Cypher query with performance monitoring
        """
        start_time = time.time()

        async with self.graph_db.session() as session:
            result = await session.run(cypher_query, parameters)
            data = await result.data()

        execution_time = time.time() - start_time

        self.performance_tracker.record_query_performance(
            query_type='graph',
            execution_time=execution_time,
            result_count=len(data)
        )

        return {
            'data': data,
            'execution_time': execution_time,
            'source': 'neo4j_graph',
            'cache_eligible': True
        }

Performance Optimization

Query Performance Tuning

-- Create indexes for common query patterns
CREATE INDEX device_name_idx FOR (d:Device) ON (d.name);
CREATE INDEX interface_device_idx FOR (i:Interface) ON (i.device_id);
CREATE INDEX vlan_vid_idx FOR (v:VLAN) ON (v.vid);
CREATE INDEX cable_termination_idx FOR (c:Cable) ON (c.termination_a_id, c.termination_b_id);

-- Composite indexes for complex queries
CREATE INDEX device_site_type_idx FOR (d:Device) ON (d.site_id, d.device_type);
CREATE INDEX interface_vlan_idx FOR (i:Interface) ON (i.device_id, i.vlan_memberships);

Query Optimization Examples:

-- BEFORE: Inefficient path finding (takes 30+ seconds)
MATCH path = (d1:Device {name: 'SW-001'})-[*]-(d2:Device {name: 'RTR-001'})
RETURN path

-- AFTER: Optimized with direction and relationship type constraints (< 1 second)
MATCH path = (d1:Device {name: 'SW-001'})-[:HAS_INTERFACE]->
             (:Interface)-[:CONNECTED_VIA*1..5]-
             (:Interface)<-[:HAS_INTERFACE]-(d2:Device {name: 'RTR-001'})
RETURN path
LIMIT 1

Success Metrics

Performance Targets

  • Relationship queries: Complete in under 1 second using graph traversal
  • Complex infrastructure audits: Complete in under 3 seconds using optimized Cypher
  • Graph synchronization: Stay within 5 minutes of NetBox state using webhooks
  • Query success rate: Handle previously impossible multi-hop relationship queries

Technical Metrics

  • Graph database size: Efficiently store 10K+ devices, 100K+ interfaces, 50K+ cables
  • Sync reliability: 99.9% webhook processing success rate
  • Memory usage: Graph database footprint under 4GB for typical enterprise deployment
  • Concurrent query performance: Support 50+ concurrent users without degradation

User Experience Impact

  • New query capabilities: Enable questions previously impossible with relational approaches
  • Response time consistency: Eliminate variable performance based on relationship complexity
  • Visual topology: Support for graph visualization and network mapping tools
  • Predictable performance: Sub-second response times regardless of network size

This graph integration phase transforms the system from a basic API interface into a true network topology intelligence platform capable of answering sophisticated infrastructure questions in real-time.