ipmi-fan-control/backend/database.py

186 lines
6.4 KiB
Python

"""Database models and session management."""
from datetime import datetime
from typing import Optional, List
import json
from sqlalchemy import (
create_engine, Column, Integer, String, Boolean,
DateTime, Float, ForeignKey, Text, event
)
from sqlalchemy.orm import declarative_base, sessionmaker, relationship, Session
from sqlalchemy.pool import StaticPool
from backend.config import settings, DATA_DIR
# Create engine
if settings.DATABASE_URL.startswith("sqlite"):
engine = create_engine(
settings.DATABASE_URL,
connect_args={"check_same_thread": False},
poolclass=StaticPool,
)
else:
engine = create_engine(settings.DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
# Database Models
class User(Base):
"""Admin user model."""
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
username = Column(String(50), unique=True, index=True, nullable=False)
hashed_password = Column(String(255), nullable=False)
is_active = Column(Boolean, default=True)
created_at = Column(DateTime, default=datetime.utcnow)
last_login = Column(DateTime, nullable=True)
class Server(Base):
"""IPMI Server model."""
__tablename__ = "servers"
id = Column(Integer, primary_key=True, index=True)
name = Column(String(100), nullable=False)
host = Column(String(100), nullable=False) # IP address
port = Column(Integer, default=623)
username = Column(String(100), nullable=False)
encrypted_password = Column(String(255), nullable=False) # Encrypted password
# Server type (dell, hpe, etc.)
vendor = Column(String(50), default="dell")
# Fan control settings
manual_control_enabled = Column(Boolean, default=False)
third_party_pcie_response = Column(Boolean, default=True)
fan_curve_data = Column(Text, nullable=True) # JSON string
auto_control_enabled = Column(Boolean, default=False)
# Panic mode settings
panic_mode_enabled = Column(Boolean, default=True)
panic_timeout_seconds = Column(Integer, default=60)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
last_seen = Column(DateTime, nullable=True)
is_active = Column(Boolean, default=True)
# Relationships
sensor_data = relationship("SensorData", back_populates="server", cascade="all, delete-orphan")
fan_data = relationship("FanData", back_populates="server", cascade="all, delete-orphan")
class FanCurve(Base):
"""Fan curve configuration."""
__tablename__ = "fan_curves"
id = Column(Integer, primary_key=True, index=True)
server_id = Column(Integer, ForeignKey("servers.id"), nullable=False)
name = Column(String(100), default="Default")
curve_data = Column(Text, nullable=False) # JSON array of {temp, speed} points
sensor_source = Column(String(50), default="cpu") # cpu, inlet, exhaust, etc.
is_active = Column(Boolean, default=True)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
class SensorData(Base):
"""Historical sensor data."""
__tablename__ = "sensor_data"
id = Column(Integer, primary_key=True, index=True)
server_id = Column(Integer, ForeignKey("servers.id"), nullable=False)
sensor_name = Column(String(100), nullable=False)
sensor_type = Column(String(50), nullable=False) # temperature, voltage, fan, power
value = Column(Float, nullable=False)
unit = Column(String(20), nullable=True)
timestamp = Column(DateTime, default=datetime.utcnow)
server = relationship("Server", back_populates="sensor_data")
class FanData(Base):
"""Historical fan speed data."""
__tablename__ = "fan_data"
id = Column(Integer, primary_key=True, index=True)
server_id = Column(Integer, ForeignKey("servers.id"), nullable=False)
fan_number = Column(Integer, nullable=False)
fan_id = Column(String(20), nullable=False) # IPMI fan ID (0x00, 0x01, etc.)
speed_rpm = Column(Integer, nullable=True)
speed_percent = Column(Integer, nullable=True)
is_manual = Column(Boolean, default=False)
timestamp = Column(DateTime, default=datetime.utcnow)
server = relationship("Server", back_populates="fan_data")
class SystemLog(Base):
"""System event logs."""
__tablename__ = "system_logs"
id = Column(Integer, primary_key=True, index=True)
server_id = Column(Integer, ForeignKey("servers.id"), nullable=True)
event_type = Column(String(50), nullable=False) # panic, fan_change, error, warning, info
message = Column(Text, nullable=False)
details = Column(Text, nullable=True)
timestamp = Column(DateTime, default=datetime.utcnow)
class AppSettings(Base):
"""Application settings storage."""
__tablename__ = "app_settings"
id = Column(Integer, primary_key=True, index=True)
key = Column(String(100), unique=True, nullable=False)
value = Column(Text, nullable=True)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
def get_db() -> Session:
"""Get database session."""
db = SessionLocal()
try:
yield db
finally:
db.close()
def init_db():
"""Initialize database tables."""
Base.metadata.create_all(bind=engine)
# Create default admin if no users exist
db = SessionLocal()
try:
# Check if setup is complete
setup_complete = db.query(AppSettings).filter(AppSettings.key == "setup_complete").first()
if not setup_complete:
AppSettings(key="setup_complete", value="false")
db.add(AppSettings(key="setup_complete", value="false"))
db.commit()
finally:
db.close()
def is_setup_complete(db: Session) -> bool:
"""Check if initial setup is complete."""
setting = db.query(AppSettings).filter(AppSettings.key == "setup_complete").first()
if setting:
return setting.value == "true"
return False
def set_setup_complete(db: Session, complete: bool = True):
"""Mark setup as complete."""
setting = db.query(AppSettings).filter(AppSettings.key == "setup_complete").first()
if setting:
setting.value = "true" if complete else "false"
else:
setting = AppSettings(key="setup_complete", value="true" if complete else "false")
db.add(setting)
db.commit()