Compare commits
10 Commits
5b9ec7b351
...
a100327769
| Author | SHA1 | Date |
|---|---|---|
|
|
a100327769 | |
|
|
009131099a | |
|
|
f3a2fd0392 | |
|
|
0c20af8023 | |
|
|
3dfdcd1865 | |
|
|
1d258875a8 | |
|
|
bd99b80aab | |
|
|
63af1e883b | |
|
|
2c91d6f100 | |
|
|
b1c2264cc6 |
|
|
@ -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
206
README.md
|
|
@ -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
|
||||
- **Better error recovery** - Automatically reconnects on IPMI failures
|
||||
- **Simpler codebase** - Easier to debug and modify
|
||||
- **Working web UI** - Clean, responsive dashboard
|
||||
- **CLI testing mode** - Test without starting the web server
|
||||
- 🌡️ **Dual Sensor Support** - IPMI + HTTP (lm-sensors from Proxmox/host)
|
||||
- 🌬️ **Smart Fan Control** - Automatic curves, manual control, panic mode
|
||||
- 📊 **3 Preset Curves** - Balanced (default), Silent, Performance
|
||||
- 👥 **Fan Groups** - Organize and control fans individually or in groups
|
||||
- 🔍 **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
|
||||
|
||||
### 1. Install
|
||||
### Automated Install (Recommended)
|
||||
|
||||
```bash
|
||||
cd ~/projects/fan-controller-v2
|
||||
git clone https://github.com/yourusername/ipmi-controller.git
|
||||
cd ipmi-controller
|
||||
chmod +x install.sh
|
||||
sudo ./install.sh
|
||||
```
|
||||
|
||||
This will:
|
||||
- Install Python dependencies
|
||||
- Create systemd service
|
||||
- Set up config in `/etc/ipmi-fan-controller/`
|
||||
- Install all dependencies
|
||||
- Create systemd service for auto-start
|
||||
- Set up persistent data directory
|
||||
- Start the controller on boot
|
||||
|
||||
### 2. Configure
|
||||
|
||||
Edit the configuration file:
|
||||
### Manual Install
|
||||
|
||||
```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:
|
||||
```json
|
||||
{
|
||||
"host": "192.168.1.100",
|
||||
"username": "root",
|
||||
"password": "your-password",
|
||||
"port": 623
|
||||
}
|
||||
Access at `http://your-server:8000`
|
||||
|
||||
## Initial Setup
|
||||
|
||||
1. Complete the setup wizard (create admin + IPMI config)
|
||||
2. Login with your admin credentials
|
||||
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
|
||||
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
|
||||
python3 fan_controller.py 192.168.1.100 root password
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
This will:
|
||||
1. Test the connection
|
||||
2. Show temperatures and fan speeds
|
||||
3. Try manual fan control (30% → 50% → auto)
|
||||
Data persists in `./data` directory.
|
||||
|
||||
## Features
|
||||
## Management Commands
|
||||
|
||||
### Automatic Control
|
||||
- Adjusts fan speed based on CPU temperature
|
||||
- Configurable fan curve (temp → speed mapping)
|
||||
- Panic mode: sets fans to 100% if temp exceeds threshold
|
||||
```bash
|
||||
# Status
|
||||
sudo systemctl status ipmi-controller
|
||||
|
||||
### Manual Control
|
||||
- Set any fan speed from 0-100%
|
||||
- Override automatic control temporarily
|
||||
# Logs
|
||||
sudo journalctl -u ipmi-controller -f
|
||||
|
||||
### Safety Features
|
||||
- Returns to automatic control on shutdown
|
||||
- Reconnects automatically if IPMI connection drops
|
||||
- Panic temperature protection
|
||||
# Restart
|
||||
sudo systemctl restart ipmi-controller
|
||||
|
||||
## Configuration Options
|
||||
|
||||
```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}
|
||||
]
|
||||
}
|
||||
# Stop
|
||||
sudo systemctl stop ipmi-controller
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Connection Failed
|
||||
1. Verify IPMI is enabled in BIOS/iDRAC
|
||||
2. Test manually: `ipmitool -I lanplus -H <ip> -U <user> -P <pass> mc info`
|
||||
3. Check firewall allows port 623
|
||||
**IPMI Connection Failed:**
|
||||
- Verify IPMI IP, username, password
|
||||
- Test: `ipmitool -I lanplus -H <ip> -U <user> -P <pass> mc info`
|
||||
|
||||
### Fans Not Responding
|
||||
1. Some Dell servers need 3rd party PCIe response disabled
|
||||
2. Try enabling manual mode first via web UI
|
||||
3. Check IPMI user has admin privileges
|
||||
**No Temperature Data:**
|
||||
- Check HTTP sensor: `curl http://proxmox-ip:8888`
|
||||
- Verify `sensor_preference` is set to "auto" or "http"
|
||||
|
||||
### Service Won't Start
|
||||
```bash
|
||||
# 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
|
||||
**Settings Lost After Update:**
|
||||
- Ensure `data/` directory is not deleted
|
||||
- Check file permissions: `ls -la data/`
|
||||
|
||||
## License
|
||||
|
||||
MIT License - Feel free to modify and distribute.
|
||||
MIT License
|
||||
|
|
|
|||
|
|
@ -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.
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
"""
|
||||
IPMI Fan Controller v2 - Simpler, More Robust
|
||||
For Dell T710 and compatible servers
|
||||
IPMI Controller - Advanced Fan Control for Dell Servers
|
||||
Features: Fan groups, multiple curves, HTTP sensors, panic mode
|
||||
"""
|
||||
import subprocess
|
||||
import re
|
||||
|
|
@ -8,6 +8,7 @@ import time
|
|||
import json
|
||||
import logging
|
||||
import threading
|
||||
import requests
|
||||
from dataclasses import dataclass, asdict
|
||||
from typing import List, Dict, Optional, Tuple
|
||||
from datetime import datetime
|
||||
|
|
@ -19,24 +20,19 @@ logging.basicConfig(
|
|||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.StreamHandler(),
|
||||
logging.FileHandler('/tmp/ipmi-fan-controller.log')
|
||||
logging.FileHandler('/tmp/ipmi-controller.log')
|
||||
]
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class FanCurvePoint:
|
||||
temp: float
|
||||
speed: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class TemperatureReading:
|
||||
name: str
|
||||
location: str
|
||||
value: float
|
||||
status: str
|
||||
source: str = "ipmi" # ipmi, http, ssh
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -45,20 +41,116 @@ class FanReading:
|
|||
fan_number: int
|
||||
speed_rpm: 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:
|
||||
"""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),
|
||||
]
|
||||
"""IPMI fan controller with advanced features."""
|
||||
|
||||
def __init__(self, host: str, username: str, password: str, port: int = 623):
|
||||
self.host = host
|
||||
|
|
@ -104,13 +196,12 @@ class IPMIFanController:
|
|||
return False, str(e)
|
||||
|
||||
def test_connection(self) -> bool:
|
||||
"""Test if we can connect to the server."""
|
||||
"""Test IPMI connection."""
|
||||
success, _ = self._run_ipmi(["mc", "info"], timeout=10)
|
||||
return success
|
||||
|
||||
def enable_manual_fan_control(self) -> bool:
|
||||
"""Enable manual fan control mode."""
|
||||
# Dell: raw 0x30 0x30 0x01 0x00
|
||||
success, _ = self._run_ipmi(["raw", "0x30", "0x30", "0x01", "0x00"])
|
||||
if success:
|
||||
self.manual_mode = True
|
||||
|
|
@ -119,7 +210,6 @@ class IPMIFanController:
|
|||
|
||||
def disable_manual_fan_control(self) -> bool:
|
||||
"""Return to automatic fan control."""
|
||||
# Dell: raw 0x30 0x30 0x01 0x01
|
||||
success, _ = self._run_ipmi(["raw", "0x30", "0x30", "0x01", "0x01"])
|
||||
if success:
|
||||
self.manual_mode = False
|
||||
|
|
@ -128,18 +218,14 @@ class IPMIFanController:
|
|||
|
||||
def set_fan_speed(self, speed_percent: int, fan_id: str = "0xff") -> bool:
|
||||
"""Set fan speed (0-100%). fan_id 0xff = all fans."""
|
||||
if speed_percent < 0:
|
||||
speed_percent = 0
|
||||
if speed_percent > 100:
|
||||
speed_percent = 100
|
||||
|
||||
speed_percent = max(0, min(100, speed_percent))
|
||||
hex_speed = f"0x{speed_percent:02x}"
|
||||
success, _ = self._run_ipmi([
|
||||
"raw", "0x30", "0x30", "0x02", fan_id, hex_speed
|
||||
])
|
||||
|
||||
if success:
|
||||
logger.info(f"Fan speed set to {speed_percent}%")
|
||||
logger.info(f"Fan {fan_id} speed set to {speed_percent}%")
|
||||
return success
|
||||
|
||||
def get_temperatures(self) -> List[TemperatureReading]:
|
||||
|
|
@ -150,7 +236,6 @@ class IPMIFanController:
|
|||
|
||||
temps = []
|
||||
for line in output.splitlines():
|
||||
# Parse: Sensor Name | 01h | ok | 3.1 | 45 degrees C
|
||||
parts = [p.strip() for p in line.split("|")]
|
||||
if len(parts) >= 5:
|
||||
name = parts[0]
|
||||
|
|
@ -165,7 +250,8 @@ class IPMIFanController:
|
|||
name=name,
|
||||
location=location,
|
||||
value=value,
|
||||
status=status
|
||||
status=status,
|
||||
source="ipmi"
|
||||
))
|
||||
return temps
|
||||
|
||||
|
|
@ -183,12 +269,10 @@ class IPMIFanController:
|
|||
name = parts[0]
|
||||
reading = parts[4]
|
||||
|
||||
# Extract fan number
|
||||
match = re.search(r'fan\s*(\d+)', name, re.IGNORECASE)
|
||||
fan_number = int(match.group(1)) if match else 0
|
||||
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 = int(rpm_match.group(1)) if rpm_match else None
|
||||
|
||||
|
|
@ -217,100 +301,144 @@ class IPMIFanController:
|
|||
return "memory"
|
||||
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:
|
||||
"""Check if controller is working properly."""
|
||||
return self.consecutive_failures < self.max_failures
|
||||
|
||||
|
||||
class FanControlService:
|
||||
"""Background service for automatic fan control."""
|
||||
class IPMIControllerService:
|
||||
"""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.controller: Optional[IPMIFanController] = None
|
||||
self.http_client: Optional[HTTPSensorClient] = None
|
||||
self.running = False
|
||||
self.thread: Optional[threading.Thread] = None
|
||||
self.current_speed = 0
|
||||
self.target_speed = 0
|
||||
self.current_speeds: Dict[str, int] = {} # fan_id -> speed
|
||||
self.target_speeds: Dict[str, int] = {}
|
||||
self.last_temps: List[TemperatureReading] = []
|
||||
self.last_fans: List[FanReading] = []
|
||||
self.lock = threading.Lock()
|
||||
self.in_identify_mode = False
|
||||
|
||||
# Default config
|
||||
self.config = {
|
||||
"host": "",
|
||||
"username": "",
|
||||
"password": "",
|
||||
"port": 623,
|
||||
# IPMI Settings
|
||||
"ipmi_host": "",
|
||||
"ipmi_username": "",
|
||||
"ipmi_password": "",
|
||||
"ipmi_port": 623,
|
||||
|
||||
# HTTP Sensor Settings
|
||||
"http_sensor_enabled": False,
|
||||
"http_sensor_url": "",
|
||||
"http_sensor_timeout": 10,
|
||||
|
||||
# Fan Control Settings
|
||||
"enabled": False,
|
||||
"interval": 10, # seconds
|
||||
"poll_interval": 10,
|
||||
"fan_update_interval": 10,
|
||||
"min_speed": 10,
|
||||
"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_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._last_data_time = datetime.utcnow()
|
||||
|
||||
def _load_config(self):
|
||||
"""Load configuration from file."""
|
||||
try:
|
||||
if Path(self.config_path).exists():
|
||||
with open(self.config_path, 'r') as f:
|
||||
config_file = Path(self.config_path)
|
||||
if config_file.exists():
|
||||
with open(config_file) as f:
|
||||
loaded = json.load(f)
|
||||
self.config.update(loaded)
|
||||
self._deep_update(self.config, loaded)
|
||||
logger.info(f"Loaded config from {self.config_path}")
|
||||
except Exception as 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):
|
||||
"""Save configuration to file."""
|
||||
try:
|
||||
Path(self.config_path).parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(self.config_path, 'w') as f:
|
||||
config_file = Path(self.config_path)
|
||||
config_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(config_file, 'w') as f:
|
||||
json.dump(self.config, f, indent=2)
|
||||
logger.info(f"Saved config to {self.config_path}")
|
||||
except Exception as e:
|
||||
|
|
@ -318,149 +446,280 @@ class FanControlService:
|
|||
|
||||
def update_config(self, **kwargs):
|
||||
"""Update configuration values."""
|
||||
self.config.update(kwargs)
|
||||
self._deep_update(self.config, kwargs)
|
||||
self._save_config()
|
||||
|
||||
# Reinitialize controller if connection params changed
|
||||
if any(k in kwargs for k in ['host', 'username', 'password', 'port']):
|
||||
# Reinitialize if needed
|
||||
if any(k in kwargs for k in ['ipmi_host', 'ipmi_username', 'ipmi_password', 'ipmi_port']):
|
||||
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."""
|
||||
if not all([self.config.get('host'), self.config.get('username'), self.config.get('password')]):
|
||||
logger.warning("Missing IPMI credentials")
|
||||
if not all([self.config.get('ipmi_host'), self.config.get('ipmi_username')]):
|
||||
return False
|
||||
|
||||
self.controller = IPMIFanController(
|
||||
host=self.config['host'],
|
||||
username=self.config['username'],
|
||||
password=self.config['password'],
|
||||
port=self.config.get('port', 623)
|
||||
host=self.config['ipmi_host'],
|
||||
username=self.config['ipmi_username'],
|
||||
password=self.config.get('ipmi_password', ''),
|
||||
port=self.config.get('ipmi_port', 623)
|
||||
)
|
||||
|
||||
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
|
||||
else:
|
||||
logger.error(f"Failed to connect to IPMI at {self.config['host']}")
|
||||
logger.error(f"Failed to connect to IPMI")
|
||||
self.controller = None
|
||||
return False
|
||||
|
||||
def start(self):
|
||||
"""Start the fan control service."""
|
||||
def _init_http_client(self) -> bool:
|
||||
"""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:
|
||||
return
|
||||
return True
|
||||
|
||||
if not self._init_controller():
|
||||
logger.error("Cannot start service - IPMI connection failed")
|
||||
logger.error("Cannot start - IPMI connection failed")
|
||||
return False
|
||||
|
||||
if self.config.get('http_sensor_enabled'):
|
||||
self._init_http_client()
|
||||
|
||||
self.running = True
|
||||
self.thread = threading.Thread(target=self._control_loop, daemon=True)
|
||||
self.thread.start()
|
||||
logger.info("Fan control service started")
|
||||
logger.info("IPMI Controller service started")
|
||||
return True
|
||||
|
||||
def stop(self):
|
||||
"""Stop the fan control service."""
|
||||
"""Stop the controller service."""
|
||||
self.running = False
|
||||
if self.thread:
|
||||
self.thread.join(timeout=5)
|
||||
|
||||
# Return to automatic control
|
||||
if self.controller:
|
||||
self.controller.disable_manual_fan_control()
|
||||
|
||||
logger.info("Fan control service stopped")
|
||||
logger.info("IPMI Controller service stopped")
|
||||
|
||||
def _control_loop(self):
|
||||
"""Main control loop running in background thread."""
|
||||
# Enable manual control on startup
|
||||
"""Main control loop."""
|
||||
if self.controller:
|
||||
self.controller.enable_manual_fan_control()
|
||||
|
||||
poll_counter = 0
|
||||
|
||||
while self.running:
|
||||
try:
|
||||
if not self.config.get('enabled', False):
|
||||
time.sleep(1)
|
||||
continue
|
||||
|
||||
# Ensure 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():
|
||||
time.sleep(30)
|
||||
continue
|
||||
self.controller.enable_manual_fan_control()
|
||||
|
||||
# Get sensor data
|
||||
temps = self.controller.get_temperatures()
|
||||
fans = self.controller.get_fan_speeds()
|
||||
# Poll temperatures at configured interval
|
||||
poll_interval = self.config.get('poll_interval', 10)
|
||||
if poll_counter % poll_interval == 0:
|
||||
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)
|
||||
|
||||
with self.lock:
|
||||
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))
|
||||
poll_counter += 1
|
||||
time.sleep(1)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Control loop error: {e}")
|
||||
time.sleep(10)
|
||||
|
||||
def get_status(self) -> Dict:
|
||||
"""Get current status."""
|
||||
with self.lock:
|
||||
return {
|
||||
"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 _get_temperatures(self) -> List[TemperatureReading]:
|
||||
"""Get temperatures from all sources."""
|
||||
temps = []
|
||||
preference = self.config.get('sensor_preference', 'ipmi')
|
||||
|
||||
# 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 set_manual_speed(self, speed: int) -> bool:
|
||||
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."""
|
||||
if not self.controller:
|
||||
return False
|
||||
|
||||
self.config['enabled'] = False
|
||||
self._save_config()
|
||||
speed = max(0, min(100, speed))
|
||||
|
||||
if self.controller.set_fan_speed(speed):
|
||||
self.current_speed = speed
|
||||
return True
|
||||
return False
|
||||
return self.controller.set_fan_speed(speed, fan_id)
|
||||
|
||||
def set_auto_mode(self, enabled: bool):
|
||||
"""Enable or disable automatic control."""
|
||||
|
|
@ -471,64 +730,61 @@ class FanControlService:
|
|||
self.controller.enable_manual_fan_control()
|
||||
elif not enabled and self.controller:
|
||||
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 instance
|
||||
_service: Optional[FanControlService] = None
|
||||
# Global service instances
|
||||
_service_instances: Dict[str, IPMIControllerService] = {}
|
||||
|
||||
|
||||
def get_service(config_path: str = "/etc/ipmi-fan-controller/config.json") -> FanControlService:
|
||||
"""Get or create the global service instance."""
|
||||
global _service
|
||||
if _service is None:
|
||||
_service = FanControlService(config_path)
|
||||
return _service
|
||||
def get_service(config_path: str = "/etc/ipmi-controller/config.json") -> IPMIControllerService:
|
||||
"""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__":
|
||||
# Simple CLI test
|
||||
# CLI test
|
||||
import sys
|
||||
|
||||
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)
|
||||
|
||||
host = sys.argv[1]
|
||||
username = sys.argv[2]
|
||||
password = sys.argv[3]
|
||||
host, user, pwd = sys.argv[1:4]
|
||||
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}...")
|
||||
if controller.test_connection():
|
||||
print("✓ Connected successfully")
|
||||
|
||||
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")
|
||||
print(f"Testing {host}...")
|
||||
if ctrl.test_connection():
|
||||
print("✓ Connected")
|
||||
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()])
|
||||
else:
|
||||
print("✗ Connection failed")
|
||||
sys.exit(1)
|
||||
print("✗ Failed")
|
||||
|
|
|
|||
278
install.sh
278
install.sh
|
|
@ -1,147 +1,215 @@
|
|||
#!/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
|
||||
|
||||
echo "🌬️ IPMI Fan Controller v2 - Setup"
|
||||
echo "=================================="
|
||||
INSTALL_DIR="${1:-/opt/ipmi-controller}"
|
||||
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
|
||||
if [ "$EUID" -eq 0 ]; then
|
||||
INSTALL_SYSTEM=true
|
||||
INSTALL_DIR="/opt/ipmi-fan-controller"
|
||||
CONFIG_DIR="/etc/ipmi-fan-controller"
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
echo "⚠️ Not running as root. Installing to $HOME/ipmi-controller instead."
|
||||
INSTALL_DIR="$HOME/ipmi-controller"
|
||||
DATA_DIR="$INSTALL_DIR/data"
|
||||
SYSTEM_INSTALL=false
|
||||
else
|
||||
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 ""
|
||||
SYSTEM_INSTALL=true
|
||||
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
|
||||
echo ""
|
||||
echo "📁 Creating directories..."
|
||||
mkdir -p "$INSTALL_DIR"
|
||||
mkdir -p "$CONFIG_DIR"
|
||||
mkdir -p "$INSTALL_DIR/logs"
|
||||
mkdir -p "$DATA_DIR"
|
||||
|
||||
# Copy files
|
||||
echo ""
|
||||
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/"
|
||||
echo "📋 Copying files..."
|
||||
cp -r . "$INSTALL_DIR/" 2>/dev/null || true
|
||||
|
||||
# Install Python dependencies
|
||||
echo ""
|
||||
echo "🐍 Installing Python dependencies..."
|
||||
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'
|
||||
# Ensure data directory exists with proper files
|
||||
if [ ! -f "$DATA_DIR/config.json" ]; then
|
||||
echo "⚙️ Creating default config..."
|
||||
cat > "$DATA_DIR/config.json" << 'EOF'
|
||||
{
|
||||
"host": "",
|
||||
"username": "root",
|
||||
"password": "",
|
||||
"port": 623,
|
||||
"ipmi_host": "",
|
||||
"ipmi_username": "",
|
||||
"ipmi_password": "",
|
||||
"ipmi_port": 623,
|
||||
"http_sensor_enabled": false,
|
||||
"http_sensor_url": "",
|
||||
"http_sensor_timeout": 10,
|
||||
"enabled": false,
|
||||
"interval": 10,
|
||||
"poll_interval": 10,
|
||||
"min_speed": 10,
|
||||
"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_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
|
||||
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)
|
||||
if [ "$INSTALL_SYSTEM" = true ]; then
|
||||
echo ""
|
||||
echo "🔧 Creating systemd service..."
|
||||
cat > /etc/systemd/system/ipmi-fan-controller.service << EOF
|
||||
if [ "$SYSTEM_INSTALL" = true ]; then
|
||||
echo "🔧 Creating systemd service..."
|
||||
cat > "/etc/systemd/system/$SERVICE_NAME.service" << EOF
|
||||
[Unit]
|
||||
Description=IPMI Fan Controller v2
|
||||
Description=IPMI Controller
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
User=$USER
|
||||
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
|
||||
ExecStop=/usr/bin/python3 -c "import requests; requests.post('http://localhost:8000/api/shutdown')"
|
||||
Restart=on-failure
|
||||
ExecStop=/bin/kill -TERM \$MAINPID
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable ipmi-fan-controller.service
|
||||
|
||||
echo ""
|
||||
echo "✅ Installation complete!"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo " 1. Edit config: sudo nano $CONFIG_DIR/config.json"
|
||||
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 "Or test from CLI:"
|
||||
echo " python3 $INSTALL_DIR/fan_controller.py <host> <user> <pass>"
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable "$SERVICE_NAME"
|
||||
|
||||
# Set proper ownership
|
||||
chown -R "$USER:$USER" "$INSTALL_DIR"
|
||||
|
||||
echo ""
|
||||
echo "✅ Installation complete!"
|
||||
echo ""
|
||||
echo "Start the service:"
|
||||
echo " sudo systemctl start $SERVICE_NAME"
|
||||
echo ""
|
||||
echo "Check status:"
|
||||
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
|
||||
|
||||
echo ""
|
||||
echo "✅ User installation complete!"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo " 1. Edit config: nano $CONFIG_DIR/config.json"
|
||||
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 "Or test from CLI:"
|
||||
echo " python3 $INSTALL_DIR/fan_controller.py <host> <user> <pass>"
|
||||
|
||||
# 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 "✅ 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
|
||||
|
||||
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!"
|
||||
|
|
|
|||
|
|
@ -2,3 +2,5 @@ fastapi==0.109.0
|
|||
uvicorn[standard]==0.27.0
|
||||
pydantic==2.5.3
|
||||
pydantic-settings==2.1.0
|
||||
python-multipart==0.0.6
|
||||
paramiko==3.4.0
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
|
|
@ -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
|
||||
|
|
@ -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!"
|
||||
1700
web_server.py
1700
web_server.py
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue