- PostgreSQL schema: 14 tables, JSONB attributes, audit triggers, soft delete - FastAPI backend: CRUD, search/filter, relationship graph, bulk import, JWT RBAC - React frontend: CI table, detail card, force-graph, dashboard - Seed data: homelab scenario (Proxmox, Mikrotik, VMs, services) - Docker Compose + Kubernetes manifests - 20 backend tests (pytest + httpx)
9.2 KiB
9.2 KiB
CMDB — Configuration Management Database
Full-stack CMDB application: FastAPI + PostgreSQL + React (MUI)
Architecture
┌──────────┐ ┌────────────┐ ┌────────────┐
│ React │────▶│ Nginx │────▶│ FastAPI │
│ (MUI) │ │ (reverse │ │ (async) │
│ :3000 │ │ proxy) │ │ :8000 │
└──────────┘ │ :80 │ └─────┬──────┘
└────────────┘ │
┌──────▼──────┐
│ PostgreSQL │
│ :5432 │
└─────────────┘
Quick Start (Docker Compose)
# Clone and start
cd cmdb-app
docker-compose up -d
# Apply migrations (if not auto-applied)
docker exec -i cmdb-postgres psql -U cmdb -d cmdb < backend/migrations/001_initial_schema.sql
docker exec -i cmdb-postgres psql -U cmdb -d cmdb < backend/migrations/002_seed_data.sql
# Open
# API docs: http://localhost/api/docs
# Frontend: http://localhost
Local Development
Backend
cd backend
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
# Start PostgreSQL (Docker)
docker run -d --name cmdb-pg -p 5432:5432 \
-e POSTGRES_DB=cmdb -e POSTGRES_USER=cmdb -e POSTGRES_PASSWORD=cmdb_secret \
postgres:16-alpine
# Run migrations
psql -h localhost -U cmdb -d cmdb < migrations/001_initial_schema.sql
psql -h localhost -U cmdb -d cmdb < migrations/002_seed_data.sql
# Start backend
uvicorn app.main:app --reload --port 8000
Frontend
cd frontend
npm install
npm run dev
# → http://localhost:5173
API Reference
Authentication
# Login
curl -X POST http://localhost:8000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"admin123"}'
# → {"access_token":"eyJ...","token_type":"bearer"}
# Get current user
curl -H "Authorization: Bearer <token>" http://localhost:8000/api/auth/me
Configuration Items
# List CIs (paginated, filtered)
curl -H "Authorization: Bearer <token>" \
"http://localhost:8000/api/ci?page=1&page_size=10&status=active&search=proxmox"
# Get single CI with all details
curl -H "Authorization: Bearer <token>" \
http://localhost:8000/api/ci/<ci_id>
# Create CI
curl -X POST http://localhost:8000/api/ci \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"name": "new-server",
"ci_type_id": "<type_uuid>",
"status": "active",
"tags": ["new", "production"],
"attributes": {"cpu": "Xeon", "ram_gb": 32}
}'
# Update CI
curl -X PATCH http://localhost:8000/api/ci/<ci_id> \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"status": "maintenance"}'
# Delete (soft)
curl -X DELETE -H "Authorization: Bearer <token>" \
http://localhost:8000/api/ci/<ci_id>
# Export CSV
curl -H "Authorization: Bearer <token>" \
http://localhost:8000/api/ci/export?format=csv > cmdb_export.csv
Relationships & Graph
# Add relationship
curl -X POST http://localhost:8000/api/ci/<ci_id>/relationships \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"source_ci_id": "<ci_id>",
"target_ci_id": "<other_ci_id>",
"relationship": "depends_on",
"description": "Service depends on server"
}'
# Get relationship graph
curl -H "Authorization: Bearer <token>" \
"http://localhost:8000/api/ci/graph/visualize?depth=2"
Bulk Import
curl -X POST http://localhost:8000/api/ci/bulk/import \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"items": [
{"name": "server-1", "ci_type_name": "PhysicalServer", "status": "active"},
{"name": "vm-web", "ci_type_name": "VirtualMachine", "status": "active"}
]
}'
Dashboard
curl -H "Authorization: Bearer <token>" \
http://localhost:8000/api/dashboard/stats
Query Parameters (CI List)
| Parameter | Type | Description |
|---|---|---|
| page | int | Page number (default: 1) |
| page_size | int | Items per page (1-100, default: 20) |
| search | string | Full-text search on name/desc/serial |
| status | string | Filter by status |
| ci_type_id | UUID | Filter by CI type |
| location_id | UUID | Filter by location |
| tag | string | Filter by tag |
| owner_id | UUID | Filter by owner |
| sort_by | string | Sort field (default: name) |
| sort_order | string | asc/desc (default: asc) |
Testing
cd backend
pytest tests/ -v
Deployment
Docker Compose (production)
# Set secrets
export JWT_SECRET=$(openssl rand -hex 32)
export POSTGRES_PASSWORD=$(openssl rand -hex 32)
docker-compose -f docker-compose.yml up -d
Kubernetes
# Create namespace
kubectl create namespace cmdb
# Create secrets
kubectl -n cmdb create secret generic cmdb-secrets \
--from-literal=db-user=cmdb \
--from-literal=db-password=$(openssl rand -hex 16) \
--from-literal=jwt-secret=$(openssl rand -hex 32)
# Deploy
kubectl apply -f k8s/postgres.yaml
kubectl apply -f k8s/backend.yaml
kubectl apply -f k8s/frontend.yaml
# Check
kubectl -n cmdb get pods
Security Checklist
- Change
JWT_SECRETin production - Change PostgreSQL password
- Enable SSL/TLS for PostgreSQL (
sslmode=require) - Run backend as non-root user
- Configure CORS for production domain only
- Set up
pg_hba.confto restrict DB access - Enable rate limiting (configured: 120 req/min)
- Run
pg_dumpbackups daily - Review audit trail in
changelogtable
Database Schema
ER Diagram (simplified)
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ ci_classes │────▶│ ci_types │────▶│ cis │
└──────────────┘ └──────────────┘ └──────┬───────┘
│
┌─────────────────────────────┼───────────────────────┐
│ │ │ │ │
┌─────▼─────┐ ┌────▼─────┐ ┌──────▼──────┐ ┌───▼────┐ ┌──▼──────────┐
│ip_addresses│ │ nics │ │ hw_details │ │sw_inst │ │relationships│
└───────────┘ └──────────┘ └─────────────┘ └────────┘ └─────────────┘
Key Design Decisions
- Soft delete everywhere —
deleted_atcolumn, never hard delete - JSONB attributes — extensible key-value store for class-specific fields
- Audit trail —
changelogtable + PostgreSQL triggers - Versioning — CI
versioncolumn incremented on every update - UUID primary keys — safe for distributed/multi-instance
- INET type — native PostgreSQL IP address handling
Ansible Integration
# playbooks/cmdb-import.yml
- name: Import Ansible facts into CMDB
hosts: all
tasks:
- name: Get system facts
set_fact:
ci_data:
name: "{{ inventory_hostname }}"
status: active
attributes:
os: "{{ ansible_distribution }} {{ ansible_distribution_version }}"
cpu_cores: "{{ ansible_processor_vcpus }}"
ram_gb: "{{ (ansible_memtotal_mb / 1024) | round(1) }}"
ip: "{{ ansible_default_ipv4.address }}"
- name: Register in CMDB
uri:
url: "http://cmdb-host:8000/api/ci"
method: POST
headers:
Authorization: "Bearer {{ cmdb_token }}"
body_format: json
body: "{{ ci_data }}"
status_code: [201, 409]
Roadmap
- Discovery integration — nmap, arp-scan, SNMP polling
- CMDB reconciliation — compare discovered vs. recorded state
- Change management — RFC workflow, approval chain
- Dependency impact analysis — cascade failure simulation
- SSO/LDAP — corporate directory integration
- Webhook notifications — Slack/Teams alerts on CI changes
- API versioning —
/api/v2/with backward compatibility - GraphQL — alternative API layer for complex queries
- RBAC per CI type — fine-grained access control
- Terraform/Pulumi integration — import IaC resources as CIs