Compare commits

..

No commits in common. "a100327769eb71c8a98156ef6b5bab266eae896a" and "5b9ec7b3515f900ac304a3040aa496026b319922" have entirely different histories.

16 changed files with 886 additions and 3009 deletions

View File

@ -1,99 +0,0 @@
# 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,156 +1,138 @@
# IPMI Controller # IPMI Fan Controller v2
Advanced IPMI fan control for Dell servers with web interface, HTTP sensor support, multiple fan curves, and persistent configuration. A simpler, more robust fan controller for Dell T710 and compatible servers using IPMI.
## Features ## What's Different from v1?
- 🌡️ **Dual Sensor Support** - IPMI + HTTP (lm-sensors from Proxmox/host) - **Direct host execution** - No Docker networking complications
- 🌬️ **Smart Fan Control** - Automatic curves, manual control, panic mode - **Better error recovery** - Automatically reconnects on IPMI failures
- 📊 **3 Preset Curves** - Balanced (default), Silent, Performance - **Simpler codebase** - Easier to debug and modify
- 👥 **Fan Groups** - Organize and control fans individually or in groups - **Working web UI** - Clean, responsive dashboard
- 🔍 **Fan Identify** - Visual fan identification - **CLI testing mode** - Test without starting the web server
- 🎨 **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
### Automated Install (Recommended) ### 1. Install
```bash ```bash
git clone https://github.com/yourusername/ipmi-controller.git cd ~/projects/fan-controller-v2
cd ipmi-controller
chmod +x install.sh chmod +x install.sh
sudo ./install.sh sudo ./install.sh
``` ```
This will: This will:
- Install all dependencies - Install Python dependencies
- Create systemd service for auto-start - Create systemd service
- Set up persistent data directory - Set up config in `/etc/ipmi-fan-controller/`
- Start the controller on boot
### Manual Install ### 2. Configure
Edit the configuration file:
```bash ```bash
# Install dependencies sudo nano /etc/ipmi-fan-controller/config.json
sudo apt-get install -y ipmitool python3-pip
pip3 install -r requirements.txt
# Run
python3 web_server.py
``` ```
Access at `http://your-server:8000` Set your IPMI credentials:
```json
## Initial Setup {
"host": "192.168.1.100",
1. Complete the setup wizard (create admin + IPMI config) "username": "root",
2. Login with your admin credentials "password": "your-password",
3. (Optional) Set up HTTP sensor on your Proxmox host: "port": 623
```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
``` ```
**Auto-backup via cron:** ### 3. Start
```bash
# Add to crontab (keeps 30 days of backups)
0 2 * * * /opt/ipmi-controller/backup.sh auto
```
## Updating
```bash ```bash
cd ipmi-controller sudo systemctl start ipmi-fan-controller
git pull
# Settings are preserved automatically
sudo systemctl restart ipmi-controller
``` ```
## Fan Curves Open the web UI at `http://your-server:8000`
**Balanced** (Default) - Best for most users: ## CLI Testing
```
30°C → 10% | 40°C → 15% | 50°C → 30% | 60°C → 55% | 70°C → 85% | 80°C → 100%
```
**Silent** - Noise-sensitive environments: Test the IPMI connection without the web server:
```
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
docker-compose up -d python3 fan_controller.py 192.168.1.100 root password
``` ```
Data persists in `./data` directory. This will:
1. Test the connection
2. Show temperatures and fan speeds
3. Try manual fan control (30% → 50% → auto)
## Management Commands ## Features
```bash ### Automatic Control
# Status - Adjusts fan speed based on CPU temperature
sudo systemctl status ipmi-controller - Configurable fan curve (temp → speed mapping)
- Panic mode: sets fans to 100% if temp exceeds threshold
# Logs ### Manual Control
sudo journalctl -u ipmi-controller -f - Set any fan speed from 0-100%
- Override automatic control temporarily
# Restart ### Safety Features
sudo systemctl restart ipmi-controller - Returns to automatic control on shutdown
- Reconnects automatically if IPMI connection drops
- Panic temperature protection
# Stop ## Configuration Options
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
**IPMI Connection Failed:** ### Connection Failed
- Verify IPMI IP, username, password 1. Verify IPMI is enabled in BIOS/iDRAC
- Test: `ipmitool -I lanplus -H <ip> -U <user> -P <pass> mc info` 2. Test manually: `ipmitool -I lanplus -H <ip> -U <user> -P <pass> mc info`
3. Check firewall allows port 623
**No Temperature Data:** ### Fans Not Responding
- Check HTTP sensor: `curl http://proxmox-ip:8888` 1. Some Dell servers need 3rd party PCIe response disabled
- Verify `sensor_preference` is set to "auto" or "http" 2. Try enabling manual mode first via web UI
3. Check IPMI user has admin privileges
**Settings Lost After Update:** ### Service Won't Start
- Ensure `data/` directory is not deleted ```bash
- Check file permissions: `ls -la data/` # Check logs
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 MIT License - Feel free to modify and distribute.

183
SETUP.md
View File

@ -1,183 +0,0 @@
# 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
```

141
backup.sh
View File

@ -1,141 +0,0 @@
#!/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

View File

@ -1,190 +0,0 @@
{
"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"
}

View File

@ -1,242 +0,0 @@
#!/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

View File

@ -1,20 +0,0 @@
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 Controller - Advanced Fan Control for Dell Servers IPMI Fan Controller v2 - Simpler, More Robust
Features: Fan groups, multiple curves, HTTP sensors, panic mode For Dell T710 and compatible servers
""" """
import subprocess import subprocess
import re import re
@ -8,7 +8,6 @@ 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
@ -20,19 +19,24 @@ 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-controller.log') logging.FileHandler('/tmp/ipmi-fan-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
@ -41,116 +45,20 @@ 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:
"""IPMI fan controller with advanced features.""" """Simplified IPMI fan controller with robust error handling."""
# 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
@ -196,12 +104,13 @@ class IPMIFanController:
return False, str(e) return False, str(e)
def test_connection(self) -> bool: def test_connection(self) -> bool:
"""Test IPMI connection.""" """Test if we can connect to the server."""
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
@ -210,6 +119,7 @@ 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
@ -218,14 +128,18 @@ 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."""
speed_percent = max(0, min(100, speed_percent)) if speed_percent < 0:
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 {fan_id} speed set to {speed_percent}%") logger.info(f"Fan speed set to {speed_percent}%")
return success return success
def get_temperatures(self) -> List[TemperatureReading]: def get_temperatures(self) -> List[TemperatureReading]:
@ -236,6 +150,7 @@ 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]
@ -250,8 +165,7 @@ class IPMIFanController:
name=name, name=name,
location=location, location=location,
value=value, value=value,
status=status, status=status
source="ipmi"
)) ))
return temps return temps
@ -269,10 +183,12 @@ 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
@ -301,144 +217,100 @@ 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 IPMIControllerService: class FanControlService:
"""Main service for IPMI Controller with all advanced features.""" """Background service for automatic fan control."""
def __init__(self, config_path: str = "/etc/ipmi-controller/config.json"): def __init__(self, config_path: str = "/etc/ipmi-fan-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_speeds: Dict[str, int] = {} # fan_id -> speed self.current_speed = 0
self.target_speeds: Dict[str, int] = {} self.target_speed = 0
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 = {
# IPMI Settings "host": "",
"ipmi_host": "", "username": "",
"ipmi_username": "", "password": "",
"ipmi_password": "", "port": 623,
"ipmi_port": 623,
# HTTP Sensor Settings
"http_sensor_enabled": False,
"http_sensor_url": "",
"http_sensor_timeout": 10,
# Fan Control Settings
"enabled": False, "enabled": False,
"poll_interval": 10, "interval": 10, # seconds
"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:
config_file = Path(self.config_path) if Path(self.config_path).exists():
if config_file.exists(): with open(self.config_path, 'r') as f:
with open(config_file) as f:
loaded = json.load(f) loaded = json.load(f)
self._deep_update(self.config, loaded) self.config.update(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:
config_file = Path(self.config_path) Path(self.config_path).parent.mkdir(parents=True, exist_ok=True)
config_file.parent.mkdir(parents=True, exist_ok=True) with open(self.config_path, 'w') as f:
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:
@ -446,280 +318,149 @@ class IPMIControllerService:
def update_config(self, **kwargs): def update_config(self, **kwargs):
"""Update configuration values.""" """Update configuration values."""
self._deep_update(self.config, kwargs) self.config.update(kwargs)
self._save_config() self._save_config()
# Reinitialize if needed # Reinitialize controller if connection params changed
if any(k in kwargs for k in ['ipmi_host', 'ipmi_username', 'ipmi_password', 'ipmi_port']): if any(k in kwargs for k in ['host', 'username', 'password', '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) -> bool: def _init_controller(self):
"""Initialize the IPMI controller.""" """Initialize the IPMI controller."""
if not all([self.config.get('ipmi_host'), self.config.get('ipmi_username')]): if not all([self.config.get('host'), self.config.get('username'), self.config.get('password')]):
logger.warning("Missing IPMI credentials")
return False return False
self.controller = IPMIFanController( self.controller = IPMIFanController(
host=self.config['ipmi_host'], host=self.config['host'],
username=self.config['ipmi_username'], username=self.config['username'],
password=self.config.get('ipmi_password', ''), password=self.config['password'],
port=self.config.get('ipmi_port', 623) port=self.config.get('port', 623)
) )
if self.controller.test_connection(): if self.controller.test_connection():
logger.info(f"Connected to IPMI at {self.config['ipmi_host']}") logger.info(f"Connected to IPMI at {self.config['host']}")
return True return True
else: else:
logger.error(f"Failed to connect to IPMI") logger.error(f"Failed to connect to IPMI at {self.config['host']}")
self.controller = None self.controller = None
return False return False
def _init_http_client(self) -> bool: def start(self):
"""Initialize HTTP sensor client.""" """Start the fan control service."""
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 True return
if not self._init_controller(): if not self._init_controller():
logger.error("Cannot start - IPMI connection failed") logger.error("Cannot start service - 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("IPMI Controller service started") logger.info("Fan control service started")
return True return True
def stop(self): def stop(self):
"""Stop the controller service.""" """Stop the fan control 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("IPMI Controller service stopped") logger.info("Fan control service stopped")
def _control_loop(self): def _control_loop(self):
"""Main control loop.""" """Main control loop running in background thread."""
# 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("IPMI unhealthy, reconnecting...") logger.warning("Controller unhealthy, attempting reconnect...")
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()
# Poll temperatures at configured interval # Get sensor data
poll_interval = self.config.get('poll_interval', 10) temps = self.controller.get_temperatures()
if poll_counter % poll_interval == 0: fans = self.controller.get_fan_speeds()
temps = self._get_temperatures()
fans = self.controller.get_fan_speeds() if self.controller else []
with self.lock:
self.last_temps = temps
self.last_fans = fans
if temps:
self._last_data_time = datetime.utcnow()
# Apply fan curves
if not self.in_identify_mode:
self._apply_fan_curves(temps)
poll_counter += 1 with self.lock:
time.sleep(1) self.last_temps = temps
self.last_fans = fans
if not temps:
logger.warning("No temperature readings received")
time.sleep(self.config.get('interval', 10))
continue
# Check for panic temperature
max_temp = max((t.value for t in temps if t.location.startswith('cpu')), default=0)
if max_temp >= self.config.get('panic_temp', 85):
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
self.target_speed = max(self.config.get('min_speed', 10),
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_temperatures(self) -> List[TemperatureReading]: def get_status(self) -> Dict:
"""Get temperatures from all sources.""" """Get current status."""
temps = [] with self.lock:
preference = self.config.get('sensor_preference', 'ipmi') return {
"running": self.running,
# Try IPMI "enabled": self.config.get('enabled', False),
if self.controller and preference in ['ipmi', 'auto']: "connected": self.controller is not None and self.controller.is_healthy(),
temps = self.controller.get_temperatures() "manual_mode": self.controller.manual_mode if self.controller else False,
"current_speed": self.current_speed,
# Try HTTP sensor "target_speed": self.target_speed,
if self.http_client and preference in ['http', 'auto']: "temperatures": [asdict(t) for t in self.last_temps],
http_temps = self.http_client.fetch_sensors() "fans": [asdict(f) for f in self.last_fans],
if http_temps: "config": {
if preference == 'http' or not temps: k: v for k, v in self.config.items()
temps = http_temps if k != 'password' # Don't expose password
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]): def set_manual_speed(self, speed: int) -> bool:
"""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))
return self.controller.set_fan_speed(speed, fan_id) if self.controller.set_fan_speed(speed):
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."""
@ -730,61 +471,64 @@ class IPMIControllerService:
self.controller.enable_manual_fan_control() self.controller.enable_manual_fan_control()
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
def _get_safe_config(self) -> Dict:
"""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
# Global service instances # Global service instance
_service_instances: Dict[str, IPMIControllerService] = {} _service: Optional[FanControlService] = None
def get_service(config_path: str = "/etc/ipmi-controller/config.json") -> IPMIControllerService: def get_service(config_path: str = "/etc/ipmi-fan-controller/config.json") -> FanControlService:
"""Get or create the service instance.""" """Get or create the global service instance."""
if config_path not in _service_instances: global _service
_service_instances[config_path] = IPMIControllerService(config_path) if _service is None:
return _service_instances[config_path] _service = FanControlService(config_path)
return _service
if __name__ == "__main__": if __name__ == "__main__":
# CLI test # Simple CLI test
import sys import sys
if len(sys.argv) < 4: if len(sys.argv) < 4:
print("Usage: fan_controller.py <host> <username> <password>") print("Usage: python fan_controller.py <host> <username> <password> [port]")
sys.exit(1) sys.exit(1)
host, user, pwd = sys.argv[1:4] host = sys.argv[1]
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
ctrl = IPMIFanController(host, user, pwd, port) controller = IPMIFanController(host, username, password, port)
print(f"Testing {host}...") print(f"Testing connection to {host}...")
if ctrl.test_connection(): if controller.test_connection():
print("✓ Connected") print("✓ Connected successfully")
print("\nTemps:", [(t.name, t.value) for t in ctrl.get_temperatures()])
print("\nFans:", [(f.fan_number, f.speed_rpm) for f in ctrl.get_fan_speeds()]) print("\nTemperatures:")
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("✗ Failed") print("✗ Connection failed")
sys.exit(1)

View File

@ -1,215 +1,147 @@
#!/bin/bash #!/bin/bash
# IPMI Controller - Install Script with Persistence # Setup script for IPMI Fan Controller v2
# This sets up auto-start and ensures settings persist
set -e set -e
INSTALL_DIR="${1:-/opt/ipmi-controller}" echo "🌬️ IPMI Fan Controller v2 - Setup"
DATA_DIR="$INSTALL_DIR/data" echo "=================================="
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" -ne 0 ]; then if [ "$EUID" -eq 0 ]; then
echo "⚠️ Not running as root. Installing to $HOME/ipmi-controller instead." INSTALL_SYSTEM=true
INSTALL_DIR="$HOME/ipmi-controller" INSTALL_DIR="/opt/ipmi-fan-controller"
DATA_DIR="$INSTALL_DIR/data" CONFIG_DIR="/etc/ipmi-fan-controller"
SYSTEM_INSTALL=false
else else
SYSTEM_INSTALL=true INSTALL_SYSTEM=false
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 "$DATA_DIR" mkdir -p "$CONFIG_DIR"
mkdir -p "$INSTALL_DIR/logs"
# Copy files # Copy files
echo "📋 Copying files..." echo ""
cp -r . "$INSTALL_DIR/" 2>/dev/null || true echo "📋 Installing files..."
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/"
# Ensure data directory exists with proper files # Install Python dependencies
if [ ! -f "$DATA_DIR/config.json" ]; then echo ""
echo "⚙️ Creating default config..." echo "🐍 Installing Python dependencies..."
cat > "$DATA_DIR/config.json" << 'EOF' python3 -m pip install -q -r "$INSTALL_DIR/requirements.txt"
# Create default config if not exists
if [ ! -f "$CONFIG_DIR/config.json" ]; then
echo ""
echo "⚙️ Creating default configuration..."
cat > "$CONFIG_DIR/config.json" << 'EOF'
{ {
"ipmi_host": "", "host": "",
"ipmi_username": "", "username": "root",
"ipmi_password": "", "password": "",
"ipmi_port": 623, "port": 623,
"http_sensor_enabled": false,
"http_sensor_url": "",
"http_sensor_timeout": 10,
"enabled": false, "enabled": false,
"poll_interval": 10, "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,
"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 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 [ "$SYSTEM_INSTALL" = true ]; then if [ "$INSTALL_SYSTEM" = true ]; then
echo "🔧 Creating systemd service..." echo ""
cat > "/etc/systemd/system/$SERVICE_NAME.service" << EOF echo "🔧 Creating systemd service..."
cat > /etc/systemd/system/ipmi-fan-controller.service << EOF
[Unit] [Unit]
Description=IPMI Controller Description=IPMI Fan Controller v2
After=network.target After=network.target
[Service] [Service]
Type=simple Type=simple
User=$USER User=root
WorkingDirectory=$INSTALL_DIR WorkingDirectory=$INSTALL_DIR
Environment="PYTHONUNBUFFERED=1" Environment="CONFIG_PATH=$CONFIG_DIR/config.json"
Environment="DATA_DIR=$DATA_DIR"
ExecStart=/usr/bin/python3 $INSTALL_DIR/web_server.py ExecStart=/usr/bin/python3 $INSTALL_DIR/web_server.py
ExecStop=/bin/kill -TERM \$MAINPID ExecStop=/usr/bin/python3 -c "import requests; requests.post('http://localhost:8000/api/shutdown')"
Restart=always Restart=on-failure
RestartSec=10 RestartSec=10
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target
EOF EOF
systemctl daemon-reload systemctl daemon-reload
systemctl enable "$SERVICE_NAME" systemctl enable ipmi-fan-controller.service
# Set proper ownership echo ""
chown -R "$USER:$USER" "$INSTALL_DIR" echo "✅ Installation complete!"
echo ""
echo "" echo "Next steps:"
echo "✅ Installation complete!" echo " 1. Edit config: sudo nano $CONFIG_DIR/config.json"
echo "" echo " 2. Start service: sudo systemctl start ipmi-fan-controller"
echo "Start the service:" echo " 3. View status: sudo systemctl status ipmi-fan-controller"
echo " sudo systemctl start $SERVICE_NAME" echo " 4. Open web UI: http://$(hostname -I | awk '{print $1}'):8000"
echo "" echo ""
echo "Check status:" echo "Or test from CLI:"
echo " sudo systemctl status $SERVICE_NAME" echo " python3 $INSTALL_DIR/fan_controller.py <host> <user> <pass>"
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' echo ""
#!/bin/bash echo "✅ User installation complete!"
cd "$(dirname "$0")" echo ""
export DATA_DIR="./data" echo "Next steps:"
export PYTHONUNBUFFERED=1 echo " 1. Edit config: nano $CONFIG_DIR/config.json"
echo "Starting IPMI Controller..." echo " 2. Start manually:"
echo "Data directory: $DATA_DIR" echo " CONFIG_PATH=$CONFIG_DIR/config.json python3 $INSTALL_DIR/web_server.py"
python3 web_server.py echo " 3. Open web UI: http://localhost:8000"
EOF echo ""
chmod +x "$INSTALL_DIR/start.sh" echo "Or test from CLI:"
echo " python3 $INSTALL_DIR/fan_controller.py <host> <user> <pass>"
echo ""
echo "✅ User installation complete!"
echo ""
echo "Start manually:"
echo " cd $INSTALL_DIR && ./start.sh"
echo ""
echo "Or create a systemd service manually:"
echo " nano ~/.config/systemd/user/ipmi-controller.service"
echo ""
echo "Access: http://localhost:8000"
fi fi
echo "" echo ""
echo "📁 Your settings are stored in: $DATA_DIR" echo "📖 Configuration file: $CONFIG_DIR/config.json"
echo " - config.json: All configuration"
echo " - users.json: User accounts"
echo ""
echo "💾 These files persist across restarts and updates!"

View File

@ -2,5 +2,3 @@ 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

View File

@ -1,30 +0,0 @@
#!/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}")

View File

@ -1,9 +0,0 @@
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

View File

@ -1,117 +0,0 @@
#!/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