Compare commits

..

10 Commits

16 changed files with 3013 additions and 890 deletions

99
PERSISTENCE.md Normal file
View File

@ -0,0 +1,99 @@
# IPMI Controller - Persistence Setup
## Data Persistence
All configuration and user data is stored in the `data/` directory:
- `data/config.json` - All settings, fan curves, IPMI config
- `data/users.json` - User accounts and passwords
**IMPORTANT:** The `data/` directory is committed to git for version control of your settings.
## Backup Your Settings
```bash
# Create backup
cd ~/ipmi-controller
cp -r data data.backup.$(date +%Y%m%d)
# Or backup to external location
cp data/config.json /mnt/backup/ipmi-controller-config.json
```
## Auto-Start on Boot (systemd)
1. **Create service file:**
```bash
sudo tee /etc/systemd/system/ipmi-controller.service << 'EOF'
[Unit]
Description=IPMI Controller
After=network.target
[Service]
Type=simple
User=devmatrix
WorkingDirectory=/home/devmatrix/ipmi-controller
ExecStart=/usr/bin/python3 /home/devmatrix/ipmi-controller/web_server.py
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
EOF
```
2. **Enable and start:**
```bash
sudo systemctl daemon-reload
sudo systemctl enable ipmi-controller
sudo systemctl start ipmi-controller
```
3. **Check status:**
```bash
sudo systemctl status ipmi-controller
sudo journalctl -u ipmi-controller -f
```
## Docker Deployment (Persistent)
```bash
# Using docker-compose
cd ~/ipmi-controller
docker-compose up -d
# Data is persisted in ./data directory
```
## Updating Without Losing Settings
```bash
cd ~/ipmi-controller
# Backup first
cp -r data data.backup
# Pull updates
git pull
# Restart
sudo systemctl restart ipmi-controller
# OR if using docker:
docker-compose restart
```
## What Gets Persisted
✅ IPMI connection settings
✅ HTTP sensor configuration
✅ Fan curves (Balanced, Silent, Performance, etc.)
✅ User accounts and passwords
✅ Theme preference (dark/light)
✅ Fan groups and custom names
✅ All control settings (poll interval, panic temps, etc.)
## Migration to New Server
1. Copy `data/config.json` and `data/users.json` to new server
2. Install ipmitool: `sudo apt-get install ipmitool`
3. Install Python deps: `pip install -r requirements.txt`
4. Start server: `python3 web_server.py`

206
README.md
View File

@ -1,138 +1,156 @@
# IPMI Fan Controller v2 # IPMI Controller
A simpler, more robust fan controller for Dell T710 and compatible servers using IPMI. Advanced IPMI fan control for Dell servers with web interface, HTTP sensor support, multiple fan curves, and persistent configuration.
## What's Different from v1? ## Features
- **Direct host execution** - No Docker networking complications - 🌡️ **Dual Sensor Support** - IPMI + HTTP (lm-sensors from Proxmox/host)
- **Better error recovery** - Automatically reconnects on IPMI failures - 🌬️ **Smart Fan Control** - Automatic curves, manual control, panic mode
- **Simpler codebase** - Easier to debug and modify - 📊 **3 Preset Curves** - Balanced (default), Silent, Performance
- **Working web UI** - Clean, responsive dashboard - 👥 **Fan Groups** - Organize and control fans individually or in groups
- **CLI testing mode** - Test without starting the web server - 🔍 **Fan Identify** - Visual fan identification
- 🎨 **Themes** - Dark and Light mode
- 📱 **Responsive Web UI** - Works on desktop and mobile
- 🔌 **Public API** - For external integrations
- 💾 **Persistent Settings** - Survives restarts and updates
## Quick Start ## Quick Start
### 1. Install ### Automated Install (Recommended)
```bash ```bash
cd ~/projects/fan-controller-v2 git clone https://github.com/yourusername/ipmi-controller.git
cd ipmi-controller
chmod +x install.sh chmod +x install.sh
sudo ./install.sh sudo ./install.sh
``` ```
This will: This will:
- Install Python dependencies - Install all dependencies
- Create systemd service - Create systemd service for auto-start
- Set up config in `/etc/ipmi-fan-controller/` - Set up persistent data directory
- Start the controller on boot
### 2. Configure ### Manual Install
Edit the configuration file:
```bash ```bash
sudo nano /etc/ipmi-fan-controller/config.json # Install dependencies
sudo apt-get install -y ipmitool python3-pip
pip3 install -r requirements.txt
# Run
python3 web_server.py
``` ```
Set your IPMI credentials: Access at `http://your-server:8000`
```json
{ ## Initial Setup
"host": "192.168.1.100",
"username": "root", 1. Complete the setup wizard (create admin + IPMI config)
"password": "your-password", 2. Login with your admin credentials
"port": 623 3. (Optional) Set up HTTP sensor on your Proxmox host:
} ```bash
# On Proxmox server
curl -O https://raw.githubusercontent.com/yourusername/ipmi-controller/main/setup-sensors-server.sh
sudo ./setup-sensors-server.sh
```
4. Enable auto control and enjoy automatic fan management!
## Persistence
All your settings are automatically saved to `data/config.json`:
✅ IPMI configuration
✅ HTTP sensor settings
✅ Fan curves (Balanced, Silent, Performance)
✅ User accounts
✅ Theme preference
✅ All control settings
**Backups:**
```bash
./backup.sh backup # Create backup
./backup.sh list # List backups
./backup.sh restore [file] # Restore from backup
``` ```
### 3. Start **Auto-backup via cron:**
```bash
# Add to crontab (keeps 30 days of backups)
0 2 * * * /opt/ipmi-controller/backup.sh auto
```
## Updating
```bash ```bash
sudo systemctl start ipmi-fan-controller cd ipmi-controller
git pull
# Settings are preserved automatically
sudo systemctl restart ipmi-controller
``` ```
Open the web UI at `http://your-server:8000` ## Fan Curves
## CLI Testing **Balanced** (Default) - Best for most users:
```
30°C → 10% | 40°C → 15% | 50°C → 30% | 60°C → 55% | 70°C → 85% | 80°C → 100%
```
Test the IPMI connection without the web server: **Silent** - Noise-sensitive environments:
```
30°C → 5% | 40°C → 10% | 50°C → 15% | 60°C → 35% | 70°C → 70% | 80°C → 100%
```
**Performance** - Heavy workloads:
```
30°C → 20% | 40°C → 35% | 50°C → 55% | 60°C → 85% | 70°C → 100%
```
## Documentation
- [Setup Guide](SETUP.md) - Full installation instructions
- [Persistence Guide](PERSISTENCE.md) - Backup and restore
- [API Reference](API.md) - Public API documentation
## Docker
```bash ```bash
python3 fan_controller.py 192.168.1.100 root password docker-compose up -d
``` ```
This will: Data persists in `./data` directory.
1. Test the connection
2. Show temperatures and fan speeds
3. Try manual fan control (30% → 50% → auto)
## Features ## Management Commands
### Automatic Control ```bash
- Adjusts fan speed based on CPU temperature # Status
- Configurable fan curve (temp → speed mapping) sudo systemctl status ipmi-controller
- Panic mode: sets fans to 100% if temp exceeds threshold
### Manual Control # Logs
- Set any fan speed from 0-100% sudo journalctl -u ipmi-controller -f
- Override automatic control temporarily
### Safety Features # Restart
- Returns to automatic control on shutdown sudo systemctl restart ipmi-controller
- Reconnects automatically if IPMI connection drops
- Panic temperature protection
## Configuration Options # Stop
sudo systemctl stop ipmi-controller
```json
{
"host": "192.168.1.100", // IPMI IP address
"username": "root", // IPMI username
"password": "secret", // IPMI password
"port": 623, // IPMI port (default: 623)
"enabled": false, // Start automatic control on boot
"interval": 10, // Check interval in seconds
"min_speed": 10, // Minimum fan speed (%)
"max_speed": 100, // Maximum fan speed (%)
"panic_temp": 85, // Panic mode trigger (°C)
"panic_speed": 100, // Panic mode fan speed (%)
"fan_curve": [ // Temp (°C) → Speed (%) mapping
{"temp": 30, "speed": 15},
{"temp": 40, "speed": 25},
{"temp": 50, "speed": 40},
{"temp": 60, "speed": 60},
{"temp": 70, "speed": 80},
{"temp": 80, "speed": 100}
]
}
``` ```
## Troubleshooting ## Troubleshooting
### Connection Failed **IPMI Connection Failed:**
1. Verify IPMI is enabled in BIOS/iDRAC - Verify IPMI IP, username, password
2. Test manually: `ipmitool -I lanplus -H <ip> -U <user> -P <pass> mc info` - Test: `ipmitool -I lanplus -H <ip> -U <user> -P <pass> mc info`
3. Check firewall allows port 623
### Fans Not Responding **No Temperature Data:**
1. Some Dell servers need 3rd party PCIe response disabled - Check HTTP sensor: `curl http://proxmox-ip:8888`
2. Try enabling manual mode first via web UI - Verify `sensor_preference` is set to "auto" or "http"
3. Check IPMI user has admin privileges
### Service Won't Start **Settings Lost After Update:**
```bash - Ensure `data/` directory is not deleted
# Check logs - Check file permissions: `ls -la data/`
sudo journalctl -u ipmi-fan-controller -f
# Check config is valid JSON
sudo python3 -c "import json; json.load(open('/etc/ipmi-fan-controller/config.json'))"
```
## Files
- `fan_controller.py` - Core IPMI control logic
- `web_server.py` - FastAPI web interface
- `install.sh` - Installation script
- `requirements.txt` - Python dependencies
## License ## License
MIT License - Feel free to modify and distribute. MIT License

183
SETUP.md Normal file
View File

@ -0,0 +1,183 @@
# IPMI Controller - Setup Guide
## Prerequisites
- Dell server with IPMI (iDRAC) enabled
- Linux host (Ubuntu/Debian recommended)
- Python 3.10+
- ipmitool
## Installation
### 1. Install IPMI Controller
On your management server (where you run the controller):
```bash
git clone https://github.com/yourusername/ipmi-controller.git
cd ipmi-controller
pip install -r requirements.txt
```
### 2. Install ipmitool
```bash
sudo apt-get update
sudo apt-get install -y ipmitool
```
### 3. Run the Controller
```bash
python3 web_server.py
```
Open `http://your-server:8000` in browser.
## Initial Setup
1. **Complete the Setup Wizard:**
- Create admin account
- Enter IPMI credentials
- IP: Your Dell server's IPMI IP
- Username: Usually "root"
- Password: Your IPMI password
- Port: 623 (default)
2. **Login** with your new admin credentials
## lm-sensors HTTP Server (Optional but Recommended)
For better temperature monitoring (including PCIe cards), set up lm-sensors on your Dell server:
### Option A: Automated Setup
On your **Dell/Proxmox server** (not the controller):
```bash
# Download and run setup script
curl -O https://raw.githubusercontent.com/yourusername/ipmi-controller/main/setup-sensors-server.sh
chmod +x setup-sensors-server.sh
sudo ./setup-sensors-server.sh
```
### Option B: Manual Setup
1. **Install lm-sensors:**
```bash
sudo apt-get install -y lm-sensors netcat-openbsd
```
2. **Detect sensors:**
```bash
sudo sensors-detect --auto
```
3. **Test sensors:**
```bash
sensors
```
4. **Create HTTP server script** (`/usr/local/bin/sensors-http-server.sh`):
```bash
#!/bin/bash
PORT=${1:-8888}
while true; do
{
echo -e "HTTP/1.1 200 OK\r"
echo -e "Content-Type: text/plain\r"
echo -e "Access-Control-Allow-Origin: *\r"
echo -e "\r"
sensors -u 2>/dev/null || echo "Error"
} | nc -l -p "$PORT" -q 1
done
```
5. **Make executable:**
```bash
chmod +x /usr/local/bin/sensors-http-server.sh
```
6. **Create systemd service** (`/etc/systemd/system/sensors-http.service`):
```ini
[Unit]
Description=lm-sensors HTTP Server
After=network.target
[Service]
Type=simple
ExecStart=/usr/local/bin/sensors-http-server.sh 8888
Restart=always
User=root
[Install]
WantedBy=multi-user.target
```
7. **Enable and start:**
```bash
sudo systemctl daemon-reload
sudo systemctl enable sensors-http
sudo systemctl start sensors-http
```
8. **Test:**
```bash
curl http://$(hostname -I | awk '{print $1}'):8888
```
### Configure IPMI Controller
1. Go to **Settings****HTTP** tab
2. Enable "HTTP Sensor"
3. Enter URL: `http://your-dell-server-ip:8888`
4. Save
## Docker Deployment
```bash
docker build -t ipmi-controller .
docker run -d \
-p 8000:8000 \
-v $(pwd)/data:/app/data \
--name ipmi-controller \
ipmi-controller
```
## Troubleshooting
### IPMI Connection Failed
- Verify IPMI IP is correct
- Check IPMI username/password
- Ensure IPMI is enabled in BIOS/iDRAC
- Test manually: `ipmitool -I lanplus -H <ip> -U <user> -P <pass> mc info`
### No Temperature Data
- Check if lm-sensors is installed on Dell server
- Run `sensors` to verify it works
- Check HTTP endpoint: `curl http://dell-ip:8888`
### Service Won't Start
```bash
# Check logs
sudo journalctl -u sensors-http -f
# Check if port is in use
sudo netstat -tlnp | grep 8888
```
## Security Notes
- Change default password after first login
- Use HTTPS/reverse proxy for production
- Firewall port 8000 to internal network only
- HTTP sensor endpoint is read-only
## Updating
```bash
cd ipmi-controller
git pull
pip install -r requirements.txt
# Restart the service
```

Binary file not shown.

Binary file not shown.

141
backup.sh Executable file
View File

@ -0,0 +1,141 @@
#!/bin/bash
# IPMI Controller - Backup and Restore
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DATA_DIR="${DATA_DIR:-$SCRIPT_DIR/data}"
BACKUP_DIR="${BACKUP_DIR:-$SCRIPT_DIR/backups}"
show_help() {
echo "IPMI Controller - Backup/Restore Tool"
echo ""
echo "Usage:"
echo " $0 backup - Create backup"
echo " $0 restore [filename] - Restore from backup"
echo " $0 list - List available backups"
echo " $0 auto - Auto-backup (cron-friendly)"
echo ""
}
create_backup() {
mkdir -p "$BACKUP_DIR"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="$BACKUP_DIR/ipmi-controller-backup-$TIMESTAMP.tar.gz"
echo "📦 Creating backup..."
tar -czf "$BACKUP_FILE" -C "$SCRIPT_DIR" data/ 2>/dev/null
if [ $? -eq 0 ]; then
echo "✅ Backup created: $BACKUP_FILE"
echo ""
ls -lh "$BACKUP_FILE"
else
echo "❌ Backup failed"
exit 1
fi
}
restore_backup() {
if [ -z "$1" ]; then
echo "❌ Please specify backup file"
list_backups
exit 1
fi
BACKUP_FILE="$BACKUP_DIR/$1"
if [ ! -f "$BACKUP_FILE" ]; then
BACKUP_FILE="$1"
fi
if [ ! -f "$BACKUP_FILE" ]; then
echo "❌ Backup file not found: $1"
exit 1
fi
echo "⚠️ This will overwrite current settings!"
read -p "Are you sure? (yes/no): " confirm
if [ "$confirm" != "yes" ]; then
echo "Aborted"
exit 0
fi
# Create safety backup first
echo "📦 Creating safety backup..."
mkdir -p "$BACKUP_DIR"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
tar -czf "$BACKUP_DIR/safety-backup-before-restore-$TIMESTAMP.tar.gz" -C "$SCRIPT_DIR" data/ 2>/dev/null
# Stop service if running
if systemctl is-active --quiet ipmi-controller 2>/dev/null; then
echo "🛑 Stopping service..."
sudo systemctl stop ipmi-controller
WAS_RUNNING=true
else
WAS_RUNNING=false
fi
# Restore
echo "📥 Restoring from backup..."
tar -xzf "$BACKUP_FILE" -C "$SCRIPT_DIR"
if [ $? -eq 0 ]; then
echo "✅ Restore complete"
# Restart service if it was running
if [ "$WAS_RUNNING" = true ]; then
echo "🚀 Starting service..."
sudo systemctl start ipmi-controller
fi
else
echo "❌ Restore failed"
exit 1
fi
}
list_backups() {
echo "📂 Available backups:"
echo ""
if [ -d "$BACKUP_DIR" ] && [ "$(ls -A "$BACKUP_DIR")" ]; then
ls -lh "$BACKUP_DIR"/*.tar.gz 2>/dev/null | awk '{print " " $9 " (" $5 ")"}'
else
echo " No backups found"
fi
}
auto_backup() {
# This is cron-friendly - keeps only last 30 days
mkdir -p "$BACKUP_DIR"
# Create backup
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="$BACKUP_DIR/auto-backup-$TIMESTAMP.tar.gz"
tar -czf "$BACKUP_FILE" -C "$SCRIPT_DIR" data/ 2>/dev/null
# Clean old backups (keep last 30 days)
find "$BACKUP_DIR" -name "auto-backup-*.tar.gz" -mtime +30 -delete 2>/dev/null
echo "Auto-backup complete: $BACKUP_FILE"
}
case "${1:-}" in
backup)
create_backup
;;
restore)
restore_backup "$2"
;;
list)
list_backups
;;
auto)
auto_backup
;;
help|--help|-h)
show_help
;;
*)
show_help
exit 1
;;
esac

190
data/config.json Normal file
View File

@ -0,0 +1,190 @@
{
"ipmi_host": "192.168.5.191",
"ipmi_username": "root",
"ipmi_password": "calvin",
"ipmi_port": 623,
"http_sensor_enabled": true,
"http_sensor_url": "http://192.168.5.200:8888",
"http_sensor_timeout": 10,
"enabled": true,
"poll_interval": 10,
"fan_update_interval": 10,
"min_speed": 10,
"max_speed": 100,
"panic_temp": 85.0,
"panic_speed": 100,
"panic_on_no_data": true,
"no_data_timeout": 60,
"primary_sensor": "cpu",
"sensor_preference": "auto",
"fans": {},
"fan_groups": {},
"fan_curves": {
"Default": {
"points": [
{
"temp": 30,
"speed": 15
},
{
"temp": 40,
"speed": 25
},
{
"temp": 50,
"speed": 40
},
{
"temp": 60,
"speed": 60
},
{
"temp": 70,
"speed": 80
},
{
"temp": 80,
"speed": 100
}
],
"sensor_source": "cpu",
"applies_to": "all"
},
"Balanced": {
"points": [
{
"temp": 30,
"speed": 10
},
{
"temp": 35,
"speed": 12
},
{
"temp": 40,
"speed": 15
},
{
"temp": 45,
"speed": 20
},
{
"temp": 50,
"speed": 30
},
{
"temp": 55,
"speed": 40
},
{
"temp": 60,
"speed": 55
},
{
"temp": 65,
"speed": 70
},
{
"temp": 70,
"speed": 85
},
{
"temp": 75,
"speed": 95
},
{
"temp": 80,
"speed": 100
}
],
"sensor_source": "cpu",
"applies_to": "all"
},
"Silent": {
"points": [
{
"temp": 30,
"speed": 5
},
{
"temp": 40,
"speed": 10
},
{
"temp": 50,
"speed": 15
},
{
"temp": 55,
"speed": 25
},
{
"temp": 60,
"speed": 35
},
{
"temp": 65,
"speed": 50
},
{
"temp": 70,
"speed": 70
},
{
"temp": 75,
"speed": 85
},
{
"temp": 80,
"speed": 100
}
],
"sensor_source": "cpu",
"applies_to": "all"
},
"Performance": {
"points": [
{
"temp": 30,
"speed": 20
},
{
"temp": 35,
"speed": 25
},
{
"temp": 40,
"speed": 35
},
{
"temp": 45,
"speed": 45
},
{
"temp": 50,
"speed": 55
},
{
"temp": 55,
"speed": 70
},
{
"temp": 60,
"speed": 85
},
{
"temp": 65,
"speed": 95
},
{
"temp": 70,
"speed": 100
}
],
"sensor_source": "cpu",
"applies_to": "all"
}
},
"theme": "dark",
"active_curve": "Performance"
}

242
deploy-prod.sh Executable file
View File

@ -0,0 +1,242 @@
#!/bin/bash
# IPMI Controller - Production Deployment Script
# Run this on the production server to set up auto-start and persistence
set -e
INSTALL_DIR="/opt/ipmi-controller"
DATA_DIR="$INSTALL_DIR/data"
SERVICE_NAME="ipmi-controller"
USER="devmatrix"
echo "🌡️ IPMI Controller - Production Deployment"
echo "============================================="
echo ""
# Check if running as root
if [ "$EUID" -ne 0 ]; then
echo "❌ Please run as root: sudo ./deploy-prod.sh"
exit 1
fi
# Get the source directory
SOURCE_DIR="${1:-$(pwd)}"
if [ ! -f "$SOURCE_DIR/web_server.py" ]; then
echo "❌ Cannot find web_server.py in $SOURCE_DIR"
echo "Usage: sudo ./deploy-prod.sh [source-directory]"
exit 1
fi
echo "📁 Source: $SOURCE_DIR"
echo "📁 Install: $INSTALL_DIR"
echo ""
# Create directories
echo "📂 Creating directories..."
mkdir -p "$INSTALL_DIR"
mkdir -p "$DATA_DIR"
mkdir -p /var/log/ipmi-controller
# Copy application files
echo "📋 Copying application files..."
cp "$SOURCE_DIR/web_server.py" "$INSTALL_DIR/"
cp "$SOURCE_DIR/fan_controller.py" "$INSTALL_DIR/"
cp "$SOURCE_DIR/requirements.txt" "$INSTALL_DIR/"
# Install dependencies
echo "🐍 Installing Python dependencies..."
pip3 install -q -r "$SOURCE_DIR/requirements.txt" || pip install -q -r "$SOURCE_DIR/requirements.txt"
# Install ipmitool if not present
if ! command -v ipmitool &> /dev/null; then
echo "📦 Installing ipmitool..."
apt-get update -qq
apt-get install -y -qq ipmitool
fi
# Preserve existing config if it exists
if [ -f "$DATA_DIR/config.json" ]; then
echo "💾 Preserving existing configuration..."
cp "$DATA_DIR/config.json" "$DATA_DIR/config.json.backup.$(date +%Y%m%d%H%M%S)"
else
echo "⚙️ Creating default configuration..."
cat > "$DATA_DIR/config.json" << 'EOF'
{
"ipmi_host": "",
"ipmi_username": "",
"ipmi_password": "",
"ipmi_port": 623,
"http_sensor_enabled": false,
"http_sensor_url": "",
"http_sensor_timeout": 10,
"enabled": false,
"poll_interval": 10,
"min_speed": 10,
"max_speed": 100,
"panic_temp": 85,
"panic_speed": 100,
"panic_on_no_data": true,
"no_data_timeout": 60,
"primary_sensor": "cpu",
"sensor_preference": "auto",
"fans": {},
"fan_groups": {},
"fan_curves": {
"Balanced": {
"points": [
{"temp": 30, "speed": 10},
{"temp": 35, "speed": 12},
{"temp": 40, "speed": 15},
{"temp": 45, "speed": 20},
{"temp": 50, "speed": 30},
{"temp": 55, "speed": 40},
{"temp": 60, "speed": 55},
{"temp": 65, "speed": 70},
{"temp": 70, "speed": 85},
{"temp": 75, "speed": 95},
{"temp": 80, "speed": 100}
],
"sensor_source": "cpu",
"applies_to": "all"
},
"Silent": {
"points": [
{"temp": 30, "speed": 5},
{"temp": 40, "speed": 10},
{"temp": 50, "speed": 15},
{"temp": 55, "speed": 25},
{"temp": 60, "speed": 35},
{"temp": 65, "speed": 50},
{"temp": 70, "speed": 70},
{"temp": 75, "speed": 85},
{"temp": 80, "speed": 100}
],
"sensor_source": "cpu",
"applies_to": "all"
},
"Performance": {
"points": [
{"temp": 30, "speed": 20},
{"temp": 35, "speed": 25},
{"temp": 40, "speed": 35},
{"temp": 45, "speed": 45},
{"temp": 50, "speed": 55},
{"temp": 55, "speed": 70},
{"temp": 60, "speed": 85},
{"temp": 65, "speed": 95},
{"temp": 70, "speed": 100}
],
"sensor_source": "cpu",
"applies_to": "all"
}
},
"active_curve": "Balanced",
"theme": "dark"
}
EOF
fi
# Create users file if not exists
if [ ! -f "$DATA_DIR/users.json" ]; then
echo '{"users": {}}' > "$DATA_DIR/users.json"
fi
# Set ownership
chown -R "$USER:$USER" "$INSTALL_DIR"
chown -R "$USER:$USER" /var/log/ipmi-controller
# Create systemd service
echo "🔧 Creating systemd service..."
cat > "/etc/systemd/system/$SERVICE_NAME.service" << EOF
[Unit]
Description=IPMI Controller - Fan Control for Dell Servers
After=network.target
Wants=network.target
[Service]
Type=simple
User=$USER
Group=$USER
WorkingDirectory=$INSTALL_DIR
Environment="PYTHONUNBUFFERED=1"
Environment="DATA_DIR=$DATA_DIR"
Environment="LOG_DIR=/var/log/ipmi-controller"
ExecStart=/usr/bin/python3 $INSTALL_DIR/web_server.py
ExecStop=/bin/kill -TERM \$MAINPID
ExecReload=/bin/kill -HUP \$MAINPID
# Restart policy
Restart=always
RestartSec=10
StartLimitInterval=60s
StartLimitBurst=3
# Logging
StandardOutput=append:/var/log/ipmi-controller/server.log
StandardError=append:/var/log/ipmi-controller/error.log
# Security
NoNewPrivileges=false
ProtectSystem=no
ProtectHome=no
[Install]
WantedBy=multi-user.target
EOF
# Create logrotate config
echo "📝 Creating logrotate config..."
cat > "/etc/logrotate.d/ipmi-controller" << EOF
/var/log/ipmi-controller/*.log {
daily
rotate 14
compress
delaycompress
missingok
notifempty
create 644 $USER $USER
sharedscripts
postrotate
systemctl reload ipmi-controller
endscript
}
EOF
# Reload systemd
echo "🔄 Reloading systemd..."
systemctl daemon-reload
# Enable service
echo "✅ Enabling service to start on boot..."
systemctl enable "$SERVICE_NAME"
echo ""
echo "🚀 Starting IPMI Controller..."
systemctl start "$SERVICE_NAME"
sleep 2
# Check status
if systemctl is-active --quiet "$SERVICE_NAME"; then
echo ""
echo "✅ IPMI Controller is running!"
echo ""
echo "🌐 Access: http://$(hostname -I | awk '{print $1}'):8000"
echo ""
echo "📋 Management Commands:"
echo " Status: sudo systemctl status $SERVICE_NAME"
echo " Logs: sudo journalctl -u $SERVICE_NAME -f"
echo " Restart: sudo systemctl restart $SERVICE_NAME"
echo " Stop: sudo systemctl stop $SERVICE_NAME"
echo ""
echo "💾 Settings: $DATA_DIR/config.json"
echo "📜 Logs: /var/log/ipmi-controller/"
echo ""
echo "✅ Production deployment complete!"
echo " Service will auto-start on boot."
else
echo ""
echo "⚠️ Service failed to start. Check logs:"
echo " sudo journalctl -u $SERVICE_NAME -f"
exit 1
fi

20
docker-compose.yml Normal file
View File

@ -0,0 +1,20 @@
version: '3.8'
services:
ipmi-controller:
build: .
container_name: ipmi-controller
restart: unless-stopped
ports:
- "8000:8000"
volumes:
# Persist data directory
- ./data:/app/data
# Optional: mount ipmitool from host if needed
- /usr/bin/ipmitool:/usr/bin/ipmitool:ro
environment:
- PYTHONUNBUFFERED=1
- DATA_DIR=/app/data
# Required for ipmitool to work in container
privileged: true
network_mode: host

View File

@ -1,6 +1,6 @@
""" """
IPMI Fan Controller v2 - Simpler, More Robust IPMI Controller - Advanced Fan Control for Dell Servers
For Dell T710 and compatible servers Features: Fan groups, multiple curves, HTTP sensors, panic mode
""" """
import subprocess import subprocess
import re import re
@ -8,6 +8,7 @@ import time
import json import json
import logging import logging
import threading import threading
import requests
from dataclasses import dataclass, asdict from dataclasses import dataclass, asdict
from typing import List, Dict, Optional, Tuple from typing import List, Dict, Optional, Tuple
from datetime import datetime from datetime import datetime
@ -19,24 +20,19 @@ logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[ handlers=[
logging.StreamHandler(), logging.StreamHandler(),
logging.FileHandler('/tmp/ipmi-fan-controller.log') logging.FileHandler('/tmp/ipmi-controller.log')
] ]
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@dataclass
class FanCurvePoint:
temp: float
speed: int
@dataclass @dataclass
class TemperatureReading: class TemperatureReading:
name: str name: str
location: str location: str
value: float value: float
status: str status: str
source: str = "ipmi" # ipmi, http, ssh
@dataclass @dataclass
@ -45,20 +41,116 @@ class FanReading:
fan_number: int fan_number: int
speed_rpm: Optional[int] speed_rpm: Optional[int]
speed_percent: Optional[int] speed_percent: Optional[int]
name: Optional[str] = None # Custom name
group: Optional[str] = None # Fan group
@dataclass
class FanCurve:
name: str
points: List[Dict[str, float]] # [{"temp": 30, "speed": 15}, ...]
sensor_source: str = "cpu" # Which sensor to use
applies_to: str = "all" # "all", group name, or fan_id
class HTTPSensorClient:
"""Client for fetching sensor data from HTTP endpoint (lm-sensors over HTTP)."""
def __init__(self, url: str, timeout: int = 10):
self.url = url
self.timeout = timeout
self.last_reading = None
self.consecutive_failures = 0
def fetch_sensors(self) -> List[TemperatureReading]:
"""Fetch sensor data from HTTP endpoint."""
try:
response = requests.get(self.url, timeout=self.timeout)
response.raise_for_status()
# Parse lm-sensors style output
temps = self._parse_sensors_output(response.text)
self.consecutive_failures = 0
return temps
except Exception as e:
logger.error(f"Failed to fetch HTTP sensors from {self.url}: {e}")
self.consecutive_failures += 1
return []
def _parse_sensors_output(self, output: str) -> List[TemperatureReading]:
"""Parse lm-sensors -u style output."""
temps = []
current_chip = ""
for line in output.splitlines():
line = line.strip()
# New chip section - chip names typically don't have spaces or colons at start
if line and not line.startswith("_") and ":" not in line and not line[0].isdigit():
if "Adapter:" not in line and "ERROR" not in line.upper():
current_chip = line
continue
# Temperature reading
if "_input:" in line and "temp" in line.lower():
parts = line.split(":")
if len(parts) >= 2:
name = parts[0].strip()
try:
value = float(parts[1].strip().split()[0]) # Handle "34.000" or "34.000 (high ="
location = self._classify_sensor_name(name, current_chip)
temps.append(TemperatureReading(
name=f"{current_chip}/{name}",
location=location,
value=value,
status="ok",
source="http"
))
except (ValueError, IndexError):
pass
return temps
def _classify_sensor_name(self, name: str, chip: str) -> str:
"""Classify sensor location from name."""
import re
name_lower = name.lower()
chip_lower = chip.lower()
# Check chip name first for CPU identification
if "coretemp" in chip_lower:
# Extract CPU number from chip (coretemp-isa-0000 = cpu1, coretemp-isa-0001 = cpu2)
if "0001" in chip or "isa-0001" in chip_lower:
return "cpu2"
return "cpu1"
# Check sensor name for core temps
if "core" in name_lower:
# Try to determine which CPU based on core number
core_match = re.search(r'core\s*(\d+)', name_lower)
if core_match:
core_num = int(core_match.group(1))
if core_num >= 6:
return "cpu2"
return "cpu1"
return "cpu"
elif "package" in name_lower:
return "cpu"
elif "tdie" in name_lower or "tctl" in name_lower:
return "cpu"
elif "pcie" in name_lower or "nvme" in name_lower or "composite" in name_lower:
return "pcie"
elif "loc1" in name_lower or "loc2" in name_lower:
return "chipset"
return "other"
def is_healthy(self) -> bool:
return self.consecutive_failures < 3
class IPMIFanController: class IPMIFanController:
"""Simplified IPMI fan controller with robust error handling.""" """IPMI fan controller with advanced features."""
# Default fan curve (temp C -> speed %)
DEFAULT_CURVE = [
FanCurvePoint(30, 15),
FanCurvePoint(40, 25),
FanCurvePoint(50, 40),
FanCurvePoint(60, 60),
FanCurvePoint(70, 80),
FanCurvePoint(80, 100),
]
def __init__(self, host: str, username: str, password: str, port: int = 623): def __init__(self, host: str, username: str, password: str, port: int = 623):
self.host = host self.host = host
@ -104,13 +196,12 @@ class IPMIFanController:
return False, str(e) return False, str(e)
def test_connection(self) -> bool: def test_connection(self) -> bool:
"""Test if we can connect to the server.""" """Test IPMI connection."""
success, _ = self._run_ipmi(["mc", "info"], timeout=10) success, _ = self._run_ipmi(["mc", "info"], timeout=10)
return success return success
def enable_manual_fan_control(self) -> bool: def enable_manual_fan_control(self) -> bool:
"""Enable manual fan control mode.""" """Enable manual fan control mode."""
# Dell: raw 0x30 0x30 0x01 0x00
success, _ = self._run_ipmi(["raw", "0x30", "0x30", "0x01", "0x00"]) success, _ = self._run_ipmi(["raw", "0x30", "0x30", "0x01", "0x00"])
if success: if success:
self.manual_mode = True self.manual_mode = True
@ -119,7 +210,6 @@ class IPMIFanController:
def disable_manual_fan_control(self) -> bool: def disable_manual_fan_control(self) -> bool:
"""Return to automatic fan control.""" """Return to automatic fan control."""
# Dell: raw 0x30 0x30 0x01 0x01
success, _ = self._run_ipmi(["raw", "0x30", "0x30", "0x01", "0x01"]) success, _ = self._run_ipmi(["raw", "0x30", "0x30", "0x01", "0x01"])
if success: if success:
self.manual_mode = False self.manual_mode = False
@ -128,18 +218,14 @@ class IPMIFanController:
def set_fan_speed(self, speed_percent: int, fan_id: str = "0xff") -> bool: def set_fan_speed(self, speed_percent: int, fan_id: str = "0xff") -> bool:
"""Set fan speed (0-100%). fan_id 0xff = all fans.""" """Set fan speed (0-100%). fan_id 0xff = all fans."""
if speed_percent < 0: speed_percent = max(0, min(100, speed_percent))
speed_percent = 0
if speed_percent > 100:
speed_percent = 100
hex_speed = f"0x{speed_percent:02x}" hex_speed = f"0x{speed_percent:02x}"
success, _ = self._run_ipmi([ success, _ = self._run_ipmi([
"raw", "0x30", "0x30", "0x02", fan_id, hex_speed "raw", "0x30", "0x30", "0x02", fan_id, hex_speed
]) ])
if success: if success:
logger.info(f"Fan speed set to {speed_percent}%") logger.info(f"Fan {fan_id} speed set to {speed_percent}%")
return success return success
def get_temperatures(self) -> List[TemperatureReading]: def get_temperatures(self) -> List[TemperatureReading]:
@ -150,7 +236,6 @@ class IPMIFanController:
temps = [] temps = []
for line in output.splitlines(): for line in output.splitlines():
# Parse: Sensor Name | 01h | ok | 3.1 | 45 degrees C
parts = [p.strip() for p in line.split("|")] parts = [p.strip() for p in line.split("|")]
if len(parts) >= 5: if len(parts) >= 5:
name = parts[0] name = parts[0]
@ -165,7 +250,8 @@ class IPMIFanController:
name=name, name=name,
location=location, location=location,
value=value, value=value,
status=status status=status,
source="ipmi"
)) ))
return temps return temps
@ -183,12 +269,10 @@ class IPMIFanController:
name = parts[0] name = parts[0]
reading = parts[4] reading = parts[4]
# Extract fan number
match = re.search(r'fan\s*(\d+)', name, re.IGNORECASE) match = re.search(r'fan\s*(\d+)', name, re.IGNORECASE)
fan_number = int(match.group(1)) if match else 0 fan_number = int(match.group(1)) if match else 0
fan_id = f"0x{fan_number-1:02x}" if fan_number > 0 else "0x00" fan_id = f"0x{fan_number-1:02x}" if fan_number > 0 else "0x00"
# Extract RPM
rpm_match = re.search(r'(\d+)\s*RPM', reading, re.IGNORECASE) rpm_match = re.search(r'(\d+)\s*RPM', reading, re.IGNORECASE)
rpm = int(rpm_match.group(1)) if rpm_match else None rpm = int(rpm_match.group(1)) if rpm_match else None
@ -217,100 +301,144 @@ class IPMIFanController:
return "memory" return "memory"
return "other" return "other"
def calculate_fan_speed(self, temps: List[TemperatureReading],
curve: Optional[List[FanCurvePoint]] = None) -> int:
"""Calculate target fan speed based on temperatures."""
if not temps:
return 50 # Default if no temps
if curve is None:
curve = self.DEFAULT_CURVE
# Find max CPU temperature
cpu_temps = [t for t in temps if t.location.startswith("cpu")]
if cpu_temps:
max_temp = max(t.value for t in cpu_temps)
else:
max_temp = max(t.value for t in temps)
# Apply fan curve with linear interpolation
sorted_curve = sorted(curve, key=lambda p: p.temp)
if max_temp <= sorted_curve[0].temp:
return sorted_curve[0].speed
if max_temp >= sorted_curve[-1].temp:
return sorted_curve[-1].speed
for i in range(len(sorted_curve) - 1):
p1, p2 = sorted_curve[i], sorted_curve[i + 1]
if p1.temp <= max_temp <= p2.temp:
if p2.temp == p1.temp:
return p1.speed
ratio = (max_temp - p1.temp) / (p2.temp - p1.temp)
speed = p1.speed + ratio * (p2.speed - p1.speed)
return int(round(speed))
return sorted_curve[-1].speed
def is_healthy(self) -> bool: def is_healthy(self) -> bool:
"""Check if controller is working properly.""" """Check if controller is working properly."""
return self.consecutive_failures < self.max_failures return self.consecutive_failures < self.max_failures
class FanControlService: class IPMIControllerService:
"""Background service for automatic fan control.""" """Main service for IPMI Controller with all advanced features."""
def __init__(self, config_path: str = "/etc/ipmi-fan-controller/config.json"): def __init__(self, config_path: str = "/etc/ipmi-controller/config.json"):
self.config_path = config_path self.config_path = config_path
self.controller: Optional[IPMIFanController] = None self.controller: Optional[IPMIFanController] = None
self.http_client: Optional[HTTPSensorClient] = None
self.running = False self.running = False
self.thread: Optional[threading.Thread] = None self.thread: Optional[threading.Thread] = None
self.current_speed = 0 self.current_speeds: Dict[str, int] = {} # fan_id -> speed
self.target_speed = 0 self.target_speeds: Dict[str, int] = {}
self.last_temps: List[TemperatureReading] = [] self.last_temps: List[TemperatureReading] = []
self.last_fans: List[FanReading] = [] self.last_fans: List[FanReading] = []
self.lock = threading.Lock() self.lock = threading.Lock()
self.in_identify_mode = False
# Default config # Default config
self.config = { self.config = {
"host": "", # IPMI Settings
"username": "", "ipmi_host": "",
"password": "", "ipmi_username": "",
"port": 623, "ipmi_password": "",
"ipmi_port": 623,
# HTTP Sensor Settings
"http_sensor_enabled": False,
"http_sensor_url": "",
"http_sensor_timeout": 10,
# Fan Control Settings
"enabled": False, "enabled": False,
"interval": 10, # seconds "poll_interval": 10,
"fan_update_interval": 10,
"min_speed": 10, "min_speed": 10,
"max_speed": 100, "max_speed": 100,
"fan_curve": [
{"temp": 30, "speed": 15},
{"temp": 40, "speed": 25},
{"temp": 50, "speed": 40},
{"temp": 60, "speed": 60},
{"temp": 70, "speed": 80},
{"temp": 80, "speed": 100},
],
"panic_temp": 85, "panic_temp": 85,
"panic_speed": 100 "panic_speed": 100,
"panic_on_no_data": True,
"no_data_timeout": 60,
# Sensor Selection
"primary_sensor": "cpu", # cpu, cpu1, cpu2, inlet, exhaust, pcie, etc.
"sensor_preference": "auto", # ipmi, http, auto
# Fan Configuration
"fans": {}, # fan_id -> {"name": "Custom Name", "group": "group1"}
"fan_groups": {}, # group_name -> {"fans": ["0x00", "0x01"], "curve": "Default"}
# Fan Curves
"active_curve": "Balanced",
"fan_curves": {
"Balanced": {
"points": [
{"temp": 30, "speed": 10},
{"temp": 35, "speed": 12},
{"temp": 40, "speed": 15},
{"temp": 45, "speed": 20},
{"temp": 50, "speed": 30},
{"temp": 55, "speed": 40},
{"temp": 60, "speed": 55},
{"temp": 65, "speed": 70},
{"temp": 70, "speed": 85},
{"temp": 75, "speed": 95},
{"temp": 80, "speed": 100}
],
"sensor_source": "cpu",
"applies_to": "all"
},
"Silent": {
"points": [
{"temp": 30, "speed": 5},
{"temp": 40, "speed": 10},
{"temp": 50, "speed": 15},
{"temp": 55, "speed": 25},
{"temp": 60, "speed": 35},
{"temp": 65, "speed": 50},
{"temp": 70, "speed": 70},
{"temp": 75, "speed": 85},
{"temp": 80, "speed": 100}
],
"sensor_source": "cpu",
"applies_to": "all"
},
"Performance": {
"points": [
{"temp": 30, "speed": 20},
{"temp": 35, "speed": 25},
{"temp": 40, "speed": 35},
{"temp": 45, "speed": 45},
{"temp": 50, "speed": 55},
{"temp": 55, "speed": 70},
{"temp": 60, "speed": 85},
{"temp": 65, "speed": 95},
{"temp": 70, "speed": 100}
],
"sensor_source": "cpu",
"applies_to": "all"
}
},
# UI Settings
"theme": "dark", # dark, light, auto
} }
self._load_config() self._load_config()
self._last_data_time = datetime.utcnow()
def _load_config(self): def _load_config(self):
"""Load configuration from file.""" """Load configuration from file."""
try: try:
if Path(self.config_path).exists(): config_file = Path(self.config_path)
with open(self.config_path, 'r') as f: if config_file.exists():
with open(config_file) as f:
loaded = json.load(f) loaded = json.load(f)
self.config.update(loaded) self._deep_update(self.config, loaded)
logger.info(f"Loaded config from {self.config_path}") logger.info(f"Loaded config from {self.config_path}")
except Exception as e: except Exception as e:
logger.error(f"Failed to load config: {e}") logger.error(f"Failed to load config: {e}")
def _deep_update(self, d: dict, u: dict):
"""Deep update dictionary."""
for k, v in u.items():
if isinstance(v, dict) and k in d and isinstance(d[k], dict):
self._deep_update(d[k], v)
else:
d[k] = v
def _save_config(self): def _save_config(self):
"""Save configuration to file.""" """Save configuration to file."""
try: try:
Path(self.config_path).parent.mkdir(parents=True, exist_ok=True) config_file = Path(self.config_path)
with open(self.config_path, 'w') as f: config_file.parent.mkdir(parents=True, exist_ok=True)
with open(config_file, 'w') as f:
json.dump(self.config, f, indent=2) json.dump(self.config, f, indent=2)
logger.info(f"Saved config to {self.config_path}") logger.info(f"Saved config to {self.config_path}")
except Exception as e: except Exception as e:
@ -318,149 +446,280 @@ class FanControlService:
def update_config(self, **kwargs): def update_config(self, **kwargs):
"""Update configuration values.""" """Update configuration values."""
self.config.update(kwargs) self._deep_update(self.config, kwargs)
self._save_config() self._save_config()
# Reinitialize controller if connection params changed # Reinitialize if needed
if any(k in kwargs for k in ['host', 'username', 'password', 'port']): if any(k in kwargs for k in ['ipmi_host', 'ipmi_username', 'ipmi_password', 'ipmi_port']):
self._init_controller() self._init_controller()
if any(k in kwargs for k in ['http_sensor_enabled', 'http_sensor_url']):
self._init_http_client()
def _init_controller(self): def _init_controller(self) -> bool:
"""Initialize the IPMI controller.""" """Initialize the IPMI controller."""
if not all([self.config.get('host'), self.config.get('username'), self.config.get('password')]): if not all([self.config.get('ipmi_host'), self.config.get('ipmi_username')]):
logger.warning("Missing IPMI credentials")
return False return False
self.controller = IPMIFanController( self.controller = IPMIFanController(
host=self.config['host'], host=self.config['ipmi_host'],
username=self.config['username'], username=self.config['ipmi_username'],
password=self.config['password'], password=self.config.get('ipmi_password', ''),
port=self.config.get('port', 623) port=self.config.get('ipmi_port', 623)
) )
if self.controller.test_connection(): if self.controller.test_connection():
logger.info(f"Connected to IPMI at {self.config['host']}") logger.info(f"Connected to IPMI at {self.config['ipmi_host']}")
return True return True
else: else:
logger.error(f"Failed to connect to IPMI at {self.config['host']}") logger.error(f"Failed to connect to IPMI")
self.controller = None self.controller = None
return False return False
def start(self): def _init_http_client(self) -> bool:
"""Start the fan control service.""" """Initialize HTTP sensor client."""
if not self.config.get('http_sensor_enabled'):
return False
url = self.config.get('http_sensor_url')
if not url:
return False
self.http_client = HTTPSensorClient(
url=url,
timeout=self.config.get('http_sensor_timeout', 10)
)
logger.info(f"HTTP sensor client initialized for {url}")
return True
def start(self) -> bool:
"""Start the controller service."""
if self.running: if self.running:
return return True
if not self._init_controller(): if not self._init_controller():
logger.error("Cannot start service - IPMI connection failed") logger.error("Cannot start - IPMI connection failed")
return False return False
if self.config.get('http_sensor_enabled'):
self._init_http_client()
self.running = True self.running = True
self.thread = threading.Thread(target=self._control_loop, daemon=True) self.thread = threading.Thread(target=self._control_loop, daemon=True)
self.thread.start() self.thread.start()
logger.info("Fan control service started") logger.info("IPMI Controller service started")
return True return True
def stop(self): def stop(self):
"""Stop the fan control service.""" """Stop the controller service."""
self.running = False self.running = False
if self.thread: if self.thread:
self.thread.join(timeout=5) self.thread.join(timeout=5)
# Return to automatic control
if self.controller: if self.controller:
self.controller.disable_manual_fan_control() self.controller.disable_manual_fan_control()
logger.info("Fan control service stopped") logger.info("IPMI Controller service stopped")
def _control_loop(self): def _control_loop(self):
"""Main control loop running in background thread.""" """Main control loop."""
# Enable manual control on startup
if self.controller: if self.controller:
self.controller.enable_manual_fan_control() self.controller.enable_manual_fan_control()
poll_counter = 0
while self.running: while self.running:
try: try:
if not self.config.get('enabled', False): if not self.config.get('enabled', False):
time.sleep(1) time.sleep(1)
continue continue
# Ensure controller is healthy
if not self.controller or not self.controller.is_healthy(): if not self.controller or not self.controller.is_healthy():
logger.warning("Controller unhealthy, attempting reconnect...") logger.warning("IPMI unhealthy, reconnecting...")
if not self._init_controller(): if not self._init_controller():
time.sleep(30) time.sleep(30)
continue continue
self.controller.enable_manual_fan_control() self.controller.enable_manual_fan_control()
# Get sensor data # Poll temperatures at configured interval
temps = self.controller.get_temperatures() poll_interval = self.config.get('poll_interval', 10)
fans = self.controller.get_fan_speeds() if poll_counter % poll_interval == 0:
temps = self._get_temperatures()
fans = self.controller.get_fan_speeds() if self.controller else []
with self.lock: with self.lock:
self.last_temps = temps self.last_temps = temps
self.last_fans = fans self.last_fans = fans
if not temps: if temps:
logger.warning("No temperature readings received") self._last_data_time = datetime.utcnow()
time.sleep(self.config.get('interval', 10))
continue
# Check for panic temperature # Apply fan curves
max_temp = max((t.value for t in temps if t.location.startswith('cpu')), default=0) if not self.in_identify_mode:
if max_temp >= self.config.get('panic_temp', 85): self._apply_fan_curves(temps)
self.target_speed = self.config.get('panic_speed', 100)
logger.warning(f"PANIC MODE: CPU temp {max_temp}°C, setting fans to {self.target_speed}%")
else:
# Calculate target speed from curve
curve = [FanCurvePoint(p['temp'], p['speed']) for p in self.config.get('fan_curve', [])]
self.target_speed = self.controller.calculate_fan_speed(temps, curve)
# Apply limits poll_counter += 1
self.target_speed = max(self.config.get('min_speed', 10), time.sleep(1)
min(self.config.get('max_speed', 100), self.target_speed))
# Apply fan speed if changed significantly (>= 5%)
if abs(self.target_speed - self.current_speed) >= 5:
if self.controller.set_fan_speed(self.target_speed):
self.current_speed = self.target_speed
logger.info(f"Fan speed adjusted to {self.target_speed}% (CPU temp: {max_temp:.1f}°C)")
time.sleep(self.config.get('interval', 10))
except Exception as e: except Exception as e:
logger.error(f"Control loop error: {e}") logger.error(f"Control loop error: {e}")
time.sleep(10) time.sleep(10)
def get_status(self) -> Dict: def _get_temperatures(self) -> List[TemperatureReading]:
"""Get current status.""" """Get temperatures from all sources."""
with self.lock: temps = []
return { preference = self.config.get('sensor_preference', 'ipmi')
"running": self.running,
"enabled": self.config.get('enabled', False),
"connected": self.controller is not None and self.controller.is_healthy(),
"manual_mode": self.controller.manual_mode if self.controller else False,
"current_speed": self.current_speed,
"target_speed": self.target_speed,
"temperatures": [asdict(t) for t in self.last_temps],
"fans": [asdict(f) for f in self.last_fans],
"config": {
k: v for k, v in self.config.items()
if k != 'password' # Don't expose password
}
}
def set_manual_speed(self, speed: int) -> bool: # Try IPMI
if self.controller and preference in ['ipmi', 'auto']:
temps = self.controller.get_temperatures()
# Try HTTP sensor
if self.http_client and preference in ['http', 'auto']:
http_temps = self.http_client.fetch_sensors()
if http_temps:
if preference == 'http' or not temps:
temps = http_temps
else:
# Merge, preferring HTTP for PCIe sensors
temp_dict = {t.name: t for t in temps}
for ht in http_temps:
if ht.location == 'pcie' or ht.name not in temp_dict:
temps.append(ht)
return temps
def _apply_fan_curves(self, temps: List[TemperatureReading]):
"""Apply fan curves based on temperatures."""
if not temps:
# Check for panic mode on no data
if self.config.get('panic_on_no_data', True):
time_since_data = (datetime.utcnow() - self._last_data_time).total_seconds()
if time_since_data > self.config.get('no_data_timeout', 60):
self._set_all_fans(self.config.get('panic_speed', 100), "PANIC: No data")
return
# Get primary sensor
primary_sensor = self.config.get('primary_sensor', 'cpu')
sensor_temps = [t for t in temps if t.location == primary_sensor]
if not sensor_temps:
sensor_temps = [t for t in temps if t.location.startswith(primary_sensor)]
if not sensor_temps:
sensor_temps = temps # Fallback to any temp
max_temp = max(t.value for t in sensor_temps)
# Check panic temperature
if max_temp >= self.config.get('panic_temp', 85):
self._set_all_fans(self.config.get('panic_speed', 100), f"PANIC: Temp {max_temp}°C")
return
# Get fan curves
curves = self.config.get('fan_curves', {})
active_curve_name = self.config.get('active_curve', 'Balanced')
default_curve = curves.get(active_curve_name, curves.get('Balanced', {'points': [{'temp': 30, 'speed': 15}, {'temp': 80, 'speed': 100}]}))
# Apply curves to fans
fans = self.config.get('fans', {})
groups = self.config.get('fan_groups', {})
# Calculate target speeds per group/individual
fan_speeds = {}
for fan_id, fan_info in fans.items():
group = fan_info.get('group')
curve_name = fan_info.get('curve', 'Default')
if group and group in groups:
curve_name = groups[group].get('curve', 'Default')
curve = curves.get(curve_name, default_curve)
speed = self._calculate_curve_speed(max_temp, curve['points'])
# Apply limits
speed = max(self.config.get('min_speed', 10),
min(self.config.get('max_speed', 100), speed))
fan_speeds[fan_id] = speed
# If no individual fan configs, apply to all
if not fan_speeds:
speed = self._calculate_curve_speed(max_temp, default_curve['points'])
speed = max(self.config.get('min_speed', 10),
min(self.config.get('max_speed', 100), speed))
self._set_all_fans(speed, f"Temp {max_temp}°C")
else:
# Set individual fan speeds
for fan_id, speed in fan_speeds.items():
self._set_fan_speed(fan_id, speed, f"Temp {max_temp}°C")
def _calculate_curve_speed(self, temp: float, points: List[Dict]) -> int:
"""Calculate fan speed from curve points."""
if not points:
return 50
sorted_points = sorted(points, key=lambda p: p['temp'])
if temp <= sorted_points[0]['temp']:
return sorted_points[0]['speed']
if temp >= sorted_points[-1]['temp']:
return sorted_points[-1]['speed']
for i in range(len(sorted_points) - 1):
p1, p2 = sorted_points[i], sorted_points[i + 1]
if p1['temp'] <= temp <= p2['temp']:
if p2['temp'] == p1['temp']:
return p1['speed']
ratio = (temp - p1['temp']) / (p2['temp'] - p1['temp'])
speed = p1['speed'] + ratio * (p2['speed'] - p1['speed'])
return int(round(speed))
return sorted_points[-1]['speed']
def _set_all_fans(self, speed: int, reason: str):
"""Set all fans to a speed."""
if self.controller and speed != self.current_speeds.get('all'):
if self.controller.set_fan_speed(speed, "0xff"):
self.current_speeds['all'] = speed
logger.info(f"All fans set to {speed}% ({reason})")
def _set_fan_speed(self, fan_id: str, speed: int, reason: str):
"""Set specific fan speed."""
if self.controller and speed != self.current_speeds.get(fan_id):
if self.controller.set_fan_speed(speed, fan_id):
self.current_speeds[fan_id] = speed
logger.info(f"Fan {fan_id} set to {speed}% ({reason})")
def identify_fan(self, fan_id: str):
"""Identify a fan by setting it to 100% and others to 0%."""
if not self.controller:
return False
self.in_identify_mode = True
# Set all fans to 0%
self.controller.set_fan_speed(0, "0xff")
time.sleep(0.5)
# Set target fan to 100%
self.controller.set_fan_speed(100, fan_id)
return True
def stop_identify(self):
"""Stop identify mode and resume normal control."""
self.in_identify_mode = False
def set_manual_speed(self, speed: int, fan_id: str = "0xff") -> bool:
"""Set manual fan speed.""" """Set manual fan speed."""
if not self.controller: if not self.controller:
return False return False
self.config['enabled'] = False self.config['enabled'] = False
self._save_config()
speed = max(0, min(100, speed)) speed = max(0, min(100, speed))
if self.controller.set_fan_speed(speed): return self.controller.set_fan_speed(speed, fan_id)
self.current_speed = speed
return True
return False
def set_auto_mode(self, enabled: bool): def set_auto_mode(self, enabled: bool):
"""Enable or disable automatic control.""" """Enable or disable automatic control."""
@ -472,63 +731,60 @@ class FanControlService:
elif not enabled and self.controller: elif not enabled and self.controller:
self.controller.disable_manual_fan_control() self.controller.disable_manual_fan_control()
def get_status(self) -> Dict:
"""Get current controller status."""
with self.lock:
status = {
"running": self.running,
"enabled": self.config.get('enabled', False),
"connected": self.controller is not None and self.controller.is_healthy(),
"manual_mode": self.controller.manual_mode if self.controller else False,
"in_identify_mode": self.in_identify_mode,
"current_speeds": self.current_speeds,
"target_speeds": self.target_speeds,
"temperatures": [asdict(t) for t in self.last_temps],
"fans": [asdict(f) for f in self.last_fans],
"config": self._get_safe_config()
}
return status
# Global service instance def _get_safe_config(self) -> Dict:
_service: Optional[FanControlService] = None """Get config without sensitive data."""
safe = json.loads(json.dumps(self.config))
# Remove passwords
safe.pop('ipmi_password', None)
safe.pop('http_sensor_password', None)
return safe
def get_service(config_path: str = "/etc/ipmi-fan-controller/config.json") -> FanControlService: # Global service instances
"""Get or create the global service instance.""" _service_instances: Dict[str, IPMIControllerService] = {}
global _service
if _service is None:
_service = FanControlService(config_path) def get_service(config_path: str = "/etc/ipmi-controller/config.json") -> IPMIControllerService:
return _service """Get or create the service instance."""
if config_path not in _service_instances:
_service_instances[config_path] = IPMIControllerService(config_path)
return _service_instances[config_path]
if __name__ == "__main__": if __name__ == "__main__":
# Simple CLI test # CLI test
import sys import sys
if len(sys.argv) < 4: if len(sys.argv) < 4:
print("Usage: python fan_controller.py <host> <username> <password> [port]") print("Usage: fan_controller.py <host> <username> <password>")
sys.exit(1) sys.exit(1)
host = sys.argv[1] host, user, pwd = sys.argv[1:4]
username = sys.argv[2]
password = sys.argv[3]
port = int(sys.argv[4]) if len(sys.argv) > 4 else 623 port = int(sys.argv[4]) if len(sys.argv) > 4 else 623
controller = IPMIFanController(host, username, password, port) ctrl = IPMIFanController(host, user, pwd, port)
print(f"Testing connection to {host}...") print(f"Testing {host}...")
if controller.test_connection(): if ctrl.test_connection():
print("✓ Connected successfully") print("✓ Connected")
print("\nTemps:", [(t.name, t.value) for t in ctrl.get_temperatures()])
print("\nTemperatures:") print("\nFans:", [(f.fan_number, f.speed_rpm) for f in ctrl.get_fan_speeds()])
for temp in controller.get_temperatures():
print(f" {temp.name}: {temp.value}°C ({temp.location})")
print("\nFan speeds:")
for fan in controller.get_fan_speeds():
print(f" Fan {fan.fan_number}: {fan.speed_rpm} RPM")
print("\nEnabling manual control...")
if controller.enable_manual_fan_control():
print("✓ Manual control enabled")
print("\nSetting fans to 30%...")
if controller.set_fan_speed(30):
print("✓ Speed set to 30%")
time.sleep(3)
print("\nSetting fans to 50%...")
if controller.set_fan_speed(50):
print("✓ Speed set to 50%")
time.sleep(3)
print("\nReturning to automatic control...")
controller.disable_manual_fan_control()
print("✓ Done")
else: else:
print("✗ Connection failed") print("✗ Failed")
sys.exit(1)

View File

@ -1,111 +1,161 @@
#!/bin/bash #!/bin/bash
# Setup script for IPMI Fan Controller v2 # IPMI Controller - Install Script with Persistence
# This sets up auto-start and ensures settings persist
set -e set -e
echo "🌬️ IPMI Fan Controller v2 - Setup" INSTALL_DIR="${1:-/opt/ipmi-controller}"
echo "==================================" DATA_DIR="$INSTALL_DIR/data"
SERVICE_NAME="ipmi-controller"
USER="${SUDO_USER:-$USER}"
echo "🌡️ IPMI Controller Installation"
echo "================================"
echo "Install dir: $INSTALL_DIR"
echo "Data dir: $DATA_DIR"
echo "Service user: $USER"
echo ""
# Check if running as root for system-wide install # Check if running as root for system-wide install
if [ "$EUID" -eq 0 ]; then if [ "$EUID" -ne 0 ]; then
INSTALL_SYSTEM=true echo "⚠️ Not running as root. Installing to $HOME/ipmi-controller instead."
INSTALL_DIR="/opt/ipmi-fan-controller" INSTALL_DIR="$HOME/ipmi-controller"
CONFIG_DIR="/etc/ipmi-fan-controller" DATA_DIR="$INSTALL_DIR/data"
SYSTEM_INSTALL=false
else else
INSTALL_SYSTEM=false SYSTEM_INSTALL=true
INSTALL_DIR="$HOME/.local/ipmi-fan-controller"
CONFIG_DIR="$HOME/.config/ipmi-fan-controller"
echo "⚠️ Running as user - installing to $INSTALL_DIR"
echo " (Run with sudo for system-wide install)"
echo ""
fi fi
# Check dependencies
echo "📦 Checking dependencies..."
if ! command -v python3 &> /dev/null; then
echo "❌ Python 3 is required but not installed"
exit 1
fi
if ! command -v ipmitool &> /dev/null; then
echo "⚠️ ipmitool not found. Installing..."
if [ "$INSTALL_SYSTEM" = true ]; then
apt-get update && apt-get install -y ipmitool
else
echo "❌ Please install ipmitool: sudo apt-get install ipmitool"
exit 1
fi
fi
echo "✓ Python 3: $(python3 --version)"
echo "✓ ipmitool: $(ipmitool -V)"
# Create directories # Create directories
echo ""
echo "📁 Creating directories..." echo "📁 Creating directories..."
mkdir -p "$INSTALL_DIR" mkdir -p "$INSTALL_DIR"
mkdir -p "$CONFIG_DIR" mkdir -p "$DATA_DIR"
mkdir -p "$INSTALL_DIR/logs"
# Copy files # Copy files
echo "" echo "📋 Copying files..."
echo "📋 Installing files..." cp -r . "$INSTALL_DIR/" 2>/dev/null || true
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cp "$SCRIPT_DIR/fan_controller.py" "$INSTALL_DIR/"
cp "$SCRIPT_DIR/web_server.py" "$INSTALL_DIR/"
cp "$SCRIPT_DIR/requirements.txt" "$INSTALL_DIR/"
# Install Python dependencies # Ensure data directory exists with proper files
echo "" if [ ! -f "$DATA_DIR/config.json" ]; then
echo "🐍 Installing Python dependencies..." echo "⚙️ Creating default config..."
python3 -m pip install -q -r "$INSTALL_DIR/requirements.txt" cat > "$DATA_DIR/config.json" << 'EOF'
# Create default config if not exists
if [ ! -f "$CONFIG_DIR/config.json" ]; then
echo ""
echo "⚙️ Creating default configuration..."
cat > "$CONFIG_DIR/config.json" << 'EOF'
{ {
"host": "", "ipmi_host": "",
"username": "root", "ipmi_username": "",
"password": "", "ipmi_password": "",
"port": 623, "ipmi_port": 623,
"http_sensor_enabled": false,
"http_sensor_url": "",
"http_sensor_timeout": 10,
"enabled": false, "enabled": false,
"interval": 10, "poll_interval": 10,
"min_speed": 10, "min_speed": 10,
"max_speed": 100, "max_speed": 100,
"fan_curve": [ "panic_temp": 85,
{"temp": 30, "speed": 15}, "panic_speed": 100,
{"temp": 40, "speed": 25}, "panic_on_no_data": true,
{"temp": 50, "speed": 40}, "no_data_timeout": 60,
{"temp": 60, "speed": 60}, "primary_sensor": "cpu",
{"temp": 70, "speed": 80}, "sensor_preference": "auto",
"fans": {},
"fan_groups": {},
"fan_curves": {
"Balanced": {
"points": [
{"temp": 30, "speed": 10},
{"temp": 35, "speed": 12},
{"temp": 40, "speed": 15},
{"temp": 45, "speed": 20},
{"temp": 50, "speed": 30},
{"temp": 55, "speed": 40},
{"temp": 60, "speed": 55},
{"temp": 65, "speed": 70},
{"temp": 70, "speed": 85},
{"temp": 75, "speed": 95},
{"temp": 80, "speed": 100} {"temp": 80, "speed": 100}
], ],
"panic_temp": 85, "sensor_source": "cpu",
"panic_speed": 100 "applies_to": "all"
},
"Silent": {
"points": [
{"temp": 30, "speed": 5},
{"temp": 40, "speed": 10},
{"temp": 50, "speed": 15},
{"temp": 55, "speed": 25},
{"temp": 60, "speed": 35},
{"temp": 65, "speed": 50},
{"temp": 70, "speed": 70},
{"temp": 75, "speed": 85},
{"temp": 80, "speed": 100}
],
"sensor_source": "cpu",
"applies_to": "all"
},
"Performance": {
"points": [
{"temp": 30, "speed": 20},
{"temp": 35, "speed": 25},
{"temp": 40, "speed": 35},
{"temp": 45, "speed": 45},
{"temp": 50, "speed": 55},
{"temp": 55, "speed": 70},
{"temp": 60, "speed": 85},
{"temp": 65, "speed": 95},
{"temp": 70, "speed": 100}
],
"sensor_source": "cpu",
"applies_to": "all"
}
},
"active_curve": "Balanced",
"theme": "dark"
} }
EOF EOF
fi fi
if [ ! -f "$DATA_DIR/users.json" ]; then
echo "👤 Creating users file..."
echo '{"users": {}}' > "$DATA_DIR/users.json"
fi
# Install Python dependencies
echo "🐍 Installing dependencies..."
if [ "$SYSTEM_INSTALL" = true ]; then
pip3 install -q -r "$INSTALL_DIR/requirements.txt" || pip install -q -r "$INSTALL_DIR/requirements.txt"
else
pip3 install --user -q -r "$INSTALL_DIR/requirements.txt" 2>/dev/null || true
fi
# Install ipmitool if not present
if ! command -v ipmitool &> /dev/null; then
echo "📦 Installing ipmitool..."
if [ "$SYSTEM_INSTALL" = true ]; then
apt-get update -qq && apt-get install -y -qq ipmitool
else
echo "⚠️ Please install ipmitool manually: sudo apt-get install ipmitool"
fi
else
echo "✓ ipmitool already installed"
fi
# Create systemd service (system-wide only) # Create systemd service (system-wide only)
if [ "$INSTALL_SYSTEM" = true ]; then if [ "$SYSTEM_INSTALL" = true ]; then
echo ""
echo "🔧 Creating systemd service..." echo "🔧 Creating systemd service..."
cat > /etc/systemd/system/ipmi-fan-controller.service << EOF cat > "/etc/systemd/system/$SERVICE_NAME.service" << EOF
[Unit] [Unit]
Description=IPMI Fan Controller v2 Description=IPMI Controller
After=network.target After=network.target
[Service] [Service]
Type=simple Type=simple
User=root User=$USER
WorkingDirectory=$INSTALL_DIR WorkingDirectory=$INSTALL_DIR
Environment="CONFIG_PATH=$CONFIG_DIR/config.json" Environment="PYTHONUNBUFFERED=1"
Environment="DATA_DIR=$DATA_DIR"
ExecStart=/usr/bin/python3 $INSTALL_DIR/web_server.py ExecStart=/usr/bin/python3 $INSTALL_DIR/web_server.py
ExecStop=/usr/bin/python3 -c "import requests; requests.post('http://localhost:8000/api/shutdown')" ExecStop=/bin/kill -TERM \$MAINPID
Restart=on-failure Restart=always
RestartSec=10 RestartSec=10
[Install] [Install]
@ -113,35 +163,53 @@ WantedBy=multi-user.target
EOF EOF
systemctl daemon-reload systemctl daemon-reload
systemctl enable ipmi-fan-controller.service systemctl enable "$SERVICE_NAME"
# Set proper ownership
chown -R "$USER:$USER" "$INSTALL_DIR"
echo "" echo ""
echo "✅ Installation complete!" echo "✅ Installation complete!"
echo "" echo ""
echo "Next steps:" echo "Start the service:"
echo " 1. Edit config: sudo nano $CONFIG_DIR/config.json" echo " sudo systemctl start $SERVICE_NAME"
echo " 2. Start service: sudo systemctl start ipmi-fan-controller"
echo " 3. View status: sudo systemctl status ipmi-fan-controller"
echo " 4. Open web UI: http://$(hostname -I | awk '{print $1}'):8000"
echo "" echo ""
echo "Or test from CLI:" echo "Check status:"
echo " python3 $INSTALL_DIR/fan_controller.py <host> <user> <pass>" echo " sudo systemctl status $SERVICE_NAME"
echo ""
echo "View logs:"
echo " sudo journalctl -u $SERVICE_NAME -f"
echo ""
echo "Access: http://$(hostname -I | awk '{print $1}'):8000"
else else
# User install - create a simple start script
cat > "$INSTALL_DIR/start.sh" << 'EOF'
#!/bin/bash
cd "$(dirname "$0")"
export DATA_DIR="./data"
export PYTHONUNBUFFERED=1
echo "Starting IPMI Controller..."
echo "Data directory: $DATA_DIR"
python3 web_server.py
EOF
chmod +x "$INSTALL_DIR/start.sh"
echo "" echo ""
echo "✅ User installation complete!" echo "✅ User installation complete!"
echo "" echo ""
echo "Next steps:" echo "Start manually:"
echo " 1. Edit config: nano $CONFIG_DIR/config.json" echo " cd $INSTALL_DIR && ./start.sh"
echo " 2. Start manually:"
echo " CONFIG_PATH=$CONFIG_DIR/config.json python3 $INSTALL_DIR/web_server.py"
echo " 3. Open web UI: http://localhost:8000"
echo "" echo ""
echo "Or test from CLI:" echo "Or create a systemd service manually:"
echo " python3 $INSTALL_DIR/fan_controller.py <host> <user> <pass>" echo " nano ~/.config/systemd/user/ipmi-controller.service"
echo ""
echo "Access: http://localhost:8000"
fi fi
echo "" echo ""
echo "📖 Configuration file: $CONFIG_DIR/config.json" echo "📁 Your settings are stored in: $DATA_DIR"
echo " - config.json: All configuration"
echo " - users.json: User accounts"
echo ""
echo "💾 These files persist across restarts and updates!"

View File

@ -2,3 +2,5 @@ fastapi==0.109.0
uvicorn[standard]==0.27.0 uvicorn[standard]==0.27.0
pydantic==2.5.3 pydantic==2.5.3
pydantic-settings==2.1.0 pydantic-settings==2.1.0
python-multipart==0.0.6
paramiko==3.4.0

30
reset_password.py Normal file
View File

@ -0,0 +1,30 @@
#!/usr/bin/env python3
"""Reset password for fan controller"""
import json
import hashlib
import sys
USERS_FILE = "/home/devmatrix/projects/fan-controller-v2/data/users.json"
def hash_password(password):
return hashlib.sha256(password.encode()).hexdigest()
if len(sys.argv) != 3:
print("Usage: reset_password.py <username> <new_password>")
sys.exit(1)
username = sys.argv[1]
password = sys.argv[2]
try:
with open(USERS_FILE) as f:
data = json.load(f)
data["users"][username] = hash_password(password)
with open(USERS_FILE, 'w') as f:
json.dump(data, f)
print(f"Password reset for user: {username}")
except Exception as e:
print(f"Error: {e}")

9
server.log Normal file
View File

@ -0,0 +1,9 @@
INFO: Started server process [33302]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
INFO: 127.0.0.1:44708 - "GET / HTTP/1.1" 200 OK
INFO: 192.168.5.30:64440 - "GET /login HTTP/1.1" 200 OK
INFO: 192.168.5.30:56562 - "GET /favicon.ico HTTP/1.1" 200 OK
INFO: 192.168.5.30:49401 - "POST /api/setup/test-ipmi HTTP/1.1" 200 OK
INFO: 192.168.5.30:49401 - "GET /favicon.ico HTTP/1.1" 200 OK

117
setup-sensors-server.sh Executable file
View File

@ -0,0 +1,117 @@
#!/bin/bash
# IPMI Controller - lm-sensors HTTP Server Setup
# Run this on your Proxmox/Dell server to expose sensors over HTTP
set -e
echo "🌡️ IPMI Controller - lm-sensors HTTP Setup"
echo "============================================"
# Check if running as root
if [ "$EUID" -ne 0 ]; then
echo "❌ Please run as root (sudo)"
exit 1
fi
# Install lm-sensors if not present
echo ""
echo "📦 Checking lm-sensors..."
if ! command -v sensors &> /dev/null; then
echo "Installing lm-sensors..."
apt-get update
apt-get install -y lm-sensors
echo ""
echo "🔧 Running sensors-detect..."
echo "Answer YES to all questions or use default values"
sensors-detect --auto
else
echo "✓ lm-sensors already installed"
fi
# Install netcat if not present
if ! command -v nc &> /dev/null; then
echo "Installing netcat..."
apt-get install -y netcat-openbsd
fi
# Create the HTTP sensors server script
SERVER_SCRIPT="/usr/local/bin/sensors-http-server.sh"
echo ""
echo "📝 Creating HTTP server script..."
cat > "$SERVER_SCRIPT" << 'EOF'
#!/bin/bash
# lm-sensors HTTP Server for IPMI Controller
# Serves sensor data on port 8888
PORT=${1:-8888}
echo "Starting lm-sensors HTTP server on port $PORT..."
echo "Access via: http://$(hostname -I | awk '{print $1}'):$PORT"
while true; do
{
echo -e "HTTP/1.1 200 OK\r"
echo -e "Content-Type: text/plain\r"
echo -e "Access-Control-Allow-Origin: *\r"
echo -e "\r"
sensors -u 2>/dev/null || echo "Error reading sensors"
} | nc -l -p "$PORT" -q 1
done
EOF
chmod +x "$SERVER_SCRIPT"
echo "✓ Created $SERVER_SCRIPT"
# Create systemd service
SERVICE_FILE="/etc/systemd/system/sensors-http.service"
echo ""
echo "🔧 Creating systemd service..."
cat > "$SERVICE_FILE" << EOF
[Unit]
Description=lm-sensors HTTP Server for IPMI Controller
After=network.target
[Service]
Type=simple
ExecStart=$SERVER_SCRIPT 8888
Restart=always
RestartSec=5
User=root
[Install]
WantedBy=multi-user.target
EOF
echo "✓ Created $SERVICE_FILE"
# Reload systemd and enable service
echo ""
echo "🚀 Enabling and starting service..."
systemctl daemon-reload
systemctl enable sensors-http.service
systemctl start sensors-http.service
# Check status
sleep 2
if systemctl is-active --quiet sensors-http.service; then
echo "✓ Service is running!"
echo ""
echo "🌐 HTTP Endpoint: http://$(hostname -I | awk '{print $1}'):8888"
echo ""
echo "Test with: curl http://$(hostname -I | awk '{print $1}'):8888"
else
echo "⚠️ Service failed to start. Check logs:"
echo " journalctl -u sensors-http.service -f"
fi
echo ""
echo "📋 Management Commands:"
echo " Start: sudo systemctl start sensors-http"
echo " Stop: sudo systemctl stop sensors-http"
echo " Status: sudo systemctl status sensors-http"
echo " Logs: sudo journalctl -u sensors-http -f"
echo ""
echo "✅ Setup complete!"

File diff suppressed because it is too large Load Diff