mirror of
https://github.com/lkddi/dell-fans-controller-docker.git
synced 2026-05-18 13:47:27 +08:00
完善开源发布配置和使用文档
This commit is contained in:
@@ -0,0 +1,14 @@
|
|||||||
|
.git
|
||||||
|
.github
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
__pycache__
|
||||||
|
*.py[cod]
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
.DS_Store
|
||||||
|
README.md
|
||||||
|
CHANGES.md
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
HOST=192.168.1.100
|
||||||
|
USERNAME=root
|
||||||
|
PASSWORD=your_idrac_password
|
||||||
@@ -1,58 +1,58 @@
|
|||||||
name: Build and Push Docker Image
|
name: Docker Image
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ master ]
|
branches:
|
||||||
|
- master
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ master ]
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
docker:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v2
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Login to DockerHub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v2
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
with:
|
uses: docker/login-action@v3
|
||||||
username: ${{ secrets.DOCKER_USER }}
|
with:
|
||||||
password: ${{ secrets.DOCKER_PASS }}
|
username: ${{ secrets.DOCKER_USER }}
|
||||||
|
password: ${{ secrets.DOCKER_PASS }}
|
||||||
|
|
||||||
- name: Login to Harbor Registry
|
- name: Extract Docker metadata
|
||||||
uses: docker/login-action@v2
|
id: meta
|
||||||
with:
|
uses: docker/metadata-action@v5
|
||||||
registry: harbor.ay.lc
|
with:
|
||||||
username: ${{ secrets.HARBOR_USER }}
|
images: lkddi/dell-fans-controller
|
||||||
password: ${{ secrets.HARBOR_PASS }}
|
tags: |
|
||||||
|
type=ref,event=tag
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
type=semver,pattern={{major}}
|
||||||
|
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||||
|
|
||||||
- name: Extract metadata (tags, labels) for Docker
|
- name: Build Docker image
|
||||||
id: meta
|
uses: docker/build-push-action@v7
|
||||||
uses: docker/metadata-action@v4
|
with:
|
||||||
with:
|
context: .
|
||||||
images: |
|
platforms: linux/amd64,linux/arm64
|
||||||
lkddi/dell-fans-controller-docker
|
push: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||||
harbor.ay.lc/library/dell-fans-controller
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
tags: |
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
type=ref,event=branch
|
cache-from: type=gha
|
||||||
type=ref,event=pr
|
cache-to: type=gha,mode=max
|
||||||
type=sha,prefix={{branch}}-
|
|
||||||
latest
|
|
||||||
|
|
||||||
- name: Build and push Docker image
|
|
||||||
uses: docker/build-push-action@v4
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
push: true
|
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
|
||||||
cache-from: type=gha
|
|
||||||
cache-to: type=gha,mode=max
|
|
||||||
|
|||||||
+8
-1
@@ -1 +1,8 @@
|
|||||||
.idea
|
.env
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
.DS_Store
|
||||||
|
|||||||
+14
-18
@@ -1,21 +1,17 @@
|
|||||||
# 修复日志
|
# Changelog / 更新日志
|
||||||
|
|
||||||
## 问题1:温度读取不准确
|
## Unreleased
|
||||||
- **问题**:之前的代码无法正确解析IPMI传感器输出中的温度值
|
|
||||||
- **解决方案**:
|
|
||||||
- 修改了`sensor()`方法,使用`ipmitool sdr`命令获取更准确的传感器数据
|
|
||||||
- 更新了`temperature()`方法,使用正则表达式正确提取温度值
|
|
||||||
- **结果**:现在能够准确读取所有温度传感器数据
|
|
||||||
|
|
||||||
## 问题2:风扇转速读取不准确
|
- 准备公开开源发布流程,Docker Hub 镜像统一为 `lkddi/dell-fans-controller`。
|
||||||
- **问题**:IPMI原始命令无法返回设置的风扇占空比值
|
- GitHub Actions 改为 PR/master 构建验证,`v*` tag 才发布 Docker 镜像。
|
||||||
- **解决方案**:
|
- 新增 Docker、Docker Compose 和本机 Python 三种运行说明。
|
||||||
- 通过校准实验确定了RPM与百分比的转换关系:20%设置对应4800 RPM
|
- 移除代码中的默认 iDRAC 地址、账号和密码,改为必须通过环境变量配置。
|
||||||
- 实现了基于RPM的百分比估算算法
|
- 增加 `.env.example`、`.dockerignore` 和 MIT License。
|
||||||
- 添加了适当的四舍五入逻辑以匹配典型的5%步进
|
- 清理 Python 缓存文件,避免将运行产物提交到仓库。
|
||||||
- **结果**:现在能够准确估算当前风扇转速百分比
|
|
||||||
|
|
||||||
## 技术细节
|
## Previous Improvements
|
||||||
- Dell服务器的IPMI系统在手动风扇模式下,可通过`ipmitool sdr`命令获取准确的RPM值
|
|
||||||
- 风扇转速百分比通过公式计算:`(current_rpm / theoretical_max_rpm) * 100`
|
- 使用 `ipmitool sdr` 读取温度和风扇传感器数据。
|
||||||
- 理论最大RPM基于校准数据:`4800 RPM * (100/20) = 24000 RPM`
|
- 根据最高温度自动选择风扇转速,高温时交还 iDRAC 自动模式。
|
||||||
|
- 对 IPMI 会话失败和命令超时增加重试和日志。
|
||||||
|
- 使用 RPM 估算风扇百分比,降低部分 Dell 机型 raw 命令返回 0 时的影响。
|
||||||
|
|||||||
+3
-4
@@ -1,5 +1,5 @@
|
|||||||
FROM ubuntu:22.04
|
FROM ubuntu:22.04
|
||||||
LABEL maintainer="joestar817@foxmail.com"
|
LABEL maintainer="lkddi"
|
||||||
|
|
||||||
# 安装依赖并设置时区
|
# 安装依赖并设置时区
|
||||||
RUN apt update && apt install -y \
|
RUN apt update && apt install -y \
|
||||||
@@ -11,8 +11,8 @@ RUN apt update && apt install -y \
|
|||||||
&& echo 'Asia/Shanghai' > /etc/timezone
|
&& echo 'Asia/Shanghai' > /etc/timezone
|
||||||
|
|
||||||
# 复制应用文件
|
# 复制应用文件
|
||||||
COPY . /dell-fans-controller-docker
|
COPY . /dell-fans-controller
|
||||||
WORKDIR /dell-fans-controller-docker
|
WORKDIR /dell-fans-controller
|
||||||
|
|
||||||
# 如果有requirements.txt则安装Python依赖
|
# 如果有requirements.txt则安装Python依赖
|
||||||
# COPY requirements.txt .
|
# COPY requirements.txt .
|
||||||
@@ -23,4 +23,3 @@ WORKDIR /dell-fans-controller-docker
|
|||||||
|
|
||||||
# 设置启动命令
|
# 设置启动命令
|
||||||
CMD ["python3", "start.py"]
|
CMD ["python3", "start.py"]
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 lkddi
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -5,5 +5,4 @@ help: ## show help message
|
|||||||
|
|
||||||
|
|
||||||
build: ## build docker image
|
build: ## build docker image
|
||||||
docker build -t dell-fans-controller-docker .
|
docker build -t lkddi/dell-fans-controller:test .
|
||||||
|
|
||||||
|
|||||||
@@ -1,153 +1,176 @@
|
|||||||
# Dell风扇控制器
|
# Dell Fans Controller / Dell 风扇控制器
|
||||||
|
|
||||||
## 项目简介
|
Dockerized fan controller for Dell iDRAC/IPMI servers. It reads temperature sensors through `ipmitool` and adjusts fan speed automatically to reduce noise while keeping the server safe.
|
||||||
|
|
||||||
Dell风扇控制器是一个自动化工具,通过IPMI接口监控Dell服务器的温度并自动调节风扇转速。本项目基于原项目 joestar817/dell-fans-controller-docker 进行了大量改进和功能增强。
|
通过 iDRAC/IPMI 自动读取 Dell 服务器温度,并根据温度调整风扇转速。适合希望降低噪音、但仍保留高温自动保护的家庭实验室、机房和 NAS 场景。
|
||||||
|
|
||||||
### 主要特性
|
> Warning: manual fan control can cause overheating if configured incorrectly. Test with care and keep iDRAC automatic mode available as fallback.
|
||||||
|
>
|
||||||
|
> 警告:手动风扇控制配置不当可能导致过热。请先确认服务器散热状态,并保留 iDRAC 自动模式作为兜底。
|
||||||
|
|
||||||
- **精准温度监控**:通过IPMI接口获取服务器进出口、CPU等关键温度数据
|
## Features / 功能
|
||||||
- **智能转速控制**:根据温度自动调节风扇转速,平衡散热效果和噪音
|
|
||||||
- **网络容错能力**:具备强大的网络连接容错机制,能处理网络波动
|
|
||||||
- **多架构支持**:支持 AMD64 和 ARM64 架构,兼容多种平台
|
|
||||||
- **自动构建部署**:通过 GitHub Actions 自动构建和推送 Docker 镜像
|
|
||||||
|
|
||||||
## 使用方法
|
- Reads Dell iDRAC sensor data with IPMI LANPlus.
|
||||||
|
- Automatically switches between manual fan speed and iDRAC automatic mode.
|
||||||
|
- Reuses sensor data within one control cycle to reduce iDRAC session pressure.
|
||||||
|
- Retries transient IPMI session failures and backs off after controller errors.
|
||||||
|
- Supports Docker multi-arch images: `linux/amd64` and `linux/arm64`.
|
||||||
|
|
||||||
### 1. 准备工作
|
- 通过 IPMI LANPlus 读取 Dell iDRAC 传感器数据。
|
||||||
|
- 根据最高温度自动设置风扇转速,超过阈值时交还 iDRAC 自动控制。
|
||||||
|
- 单次控制周期复用传感器数据,减少 iDRAC 会话压力。
|
||||||
|
- 对临时 IPMI 会话失败进行重试,并在控制器异常后等待下一轮。
|
||||||
|
- Docker 镜像支持 `linux/amd64` 和 `linux/arm64`。
|
||||||
|
|
||||||
在开始使用之前,请确保:
|
## Quick Start / 快速开始
|
||||||
|
|
||||||
1. 登录iDRAC管理界面并启用IPMI服务
|
Before running, enable IPMI over LAN in iDRAC and make sure the host running this container can reach the iDRAC management IP.
|
||||||
2. 确保网络能够访问iDRAC管理接口
|
|
||||||
3. 准备好iDRAC的用户名和密码
|
|
||||||
|
|
||||||
### 2. Docker运行
|
运行前请先在 iDRAC 中启用 IPMI over LAN,并确认运行容器的主机可以访问 iDRAC 管理 IP。
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 基本运行命令
|
docker run -d --name dell-fans-controller \
|
||||||
docker run -d --name=dell-fans-controller \
|
|
||||||
-e HOST=YOUR_IDRAC_IP \
|
|
||||||
-e USERNAME=YOUR_USERNAME \
|
|
||||||
-e PASSWORD=YOUR_PASSWORD \
|
|
||||||
--restart always \
|
--restart always \
|
||||||
lkddi/dell-fans-controller:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 配置参数
|
|
||||||
|
|
||||||
| 环境变量 | 说明 | 默认值 |
|
|
||||||
|---------|------|--------|
|
|
||||||
| HOST | iDRAC管理接口IP地址 | 192.168.1.100 |
|
|
||||||
| USERNAME | iDRAC用户名 | root |
|
|
||||||
| PASSWORD | iDRAC密码 | your_idrac_password |
|
|
||||||
|
|
||||||
## 工作原理
|
|
||||||
|
|
||||||
### 温度控制策略
|
|
||||||
|
|
||||||
系统会监控服务器的多个温度传感器,取最高温度值作为控制依据:
|
|
||||||
|
|
||||||
| 温度范围(℃) | 风扇转速(%) | 说明 |
|
|
||||||
|------------|------------|------|
|
|
||||||
| 0-50 | 15 | 静音模式 |
|
|
||||||
| 50-55 | 20 | 低速运行 |
|
|
||||||
| 55-60 | 30 | 中速运行 |
|
|
||||||
| 60-65 | 40 | 高速运行 |
|
|
||||||
| >65 | 自动模式 | 由iDRAC自动调节 |
|
|
||||||
|
|
||||||
### 智能控制机制
|
|
||||||
|
|
||||||
- **模式切换**:系统在手动模式和自动模式间智能切换
|
|
||||||
- **转速监测**:实时监测当前风扇转速,避免不必要的调整
|
|
||||||
- **状态跟踪**:记录和跟踪风扇模式和转速设置历史
|
|
||||||
|
|
||||||
## Docker镜像
|
|
||||||
|
|
||||||
### 支持的架构
|
|
||||||
|
|
||||||
- AMD64 (x86_64)
|
|
||||||
- ARM64 (aarch64)
|
|
||||||
|
|
||||||
### 镜像仓库
|
|
||||||
|
|
||||||
- Docker Hub: `lkddi/dell-fans-controller:latest`
|
|
||||||
|
|
||||||
### 自动构建
|
|
||||||
|
|
||||||
项目通过 GitHub Actions 实现了自动构建和部署:
|
|
||||||
|
|
||||||
- 每次推送代码到master分支时自动构建新镜像
|
|
||||||
- 支持多架构镜像构建
|
|
||||||
- 自动推送至Docker Hub和Harbor仓库
|
|
||||||
|
|
||||||
## 技术实现
|
|
||||||
|
|
||||||
### 核心功能
|
|
||||||
|
|
||||||
1. **精确的温度读取**:使用正则表达式解析IPMI传感器数据,准确提取温度值
|
|
||||||
2. **RPM到百分比转换**:通过校准数据建立RPM与风扇转速百分比的准确转换关系(20% = 4800 RPM)
|
|
||||||
3. **网络容错机制**:
|
|
||||||
- 5次重试机制
|
|
||||||
- 60秒超时设置
|
|
||||||
- IPMI会话建立失败的特殊处理
|
|
||||||
- 10秒重试间隔
|
|
||||||
|
|
||||||
### 配置选项
|
|
||||||
|
|
||||||
- 运行间隔:每60秒检查一次温度并调整风扇转速
|
|
||||||
- 网络超时:60秒超时限制
|
|
||||||
- 重试机制:5次失败后停止
|
|
||||||
|
|
||||||
## 部署示例
|
|
||||||
|
|
||||||
### Proxmox VE (PVE)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run -d --name=dell-fans-controller \
|
|
||||||
-e HOST=192.168.1.100 \
|
-e HOST=192.168.1.100 \
|
||||||
-e USERNAME=root \
|
-e USERNAME=root \
|
||||||
-e PASSWORD=calvin \
|
-e PASSWORD=your_idrac_password \
|
||||||
--restart always \
|
|
||||||
lkddi/dell-fans-controller:latest
|
lkddi/dell-fans-controller:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
### 群晖NAS
|
View logs / 查看日志:
|
||||||
|
|
||||||
在群晖的Docker应用中:
|
|
||||||
|
|
||||||
1. 搜索并下载 `lkddi/dell-fans-controller` 镜像
|
|
||||||
2. 创建容器并设置环境变量
|
|
||||||
3. 启用自动重启选项
|
|
||||||
|
|
||||||
## 安全说明
|
|
||||||
|
|
||||||
- 项目使用IPMI协议与服务器通信,需要相应的管理权限
|
|
||||||
- 建议使用专用的IPMI管理账户
|
|
||||||
- 定期更新镜像以获得安全补丁
|
|
||||||
|
|
||||||
## 故障排除
|
|
||||||
|
|
||||||
### 常见问题
|
|
||||||
|
|
||||||
1. **连接失败**:检查网络是否能访问iDRAC接口
|
|
||||||
2. **认证失败**:确认用户名和密码正确
|
|
||||||
3. **权限不足**:确保账户有IPMI控制权限
|
|
||||||
|
|
||||||
### 日志查看
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker logs dell-fans-controller
|
docker logs -f dell-fans-controller
|
||||||
```
|
```
|
||||||
|
|
||||||
## 免责声明
|
### Docker Compose
|
||||||
|
|
||||||
手动调整服务器风扇转速可能带来过热风险,使用本项目前请充分了解风险并做好数据备份。对于使用本项目引发的任何问题,作者概不负责。
|
Copy the example environment file and edit the iDRAC settings:
|
||||||
|
|
||||||
## 贡献
|
复制示例环境变量文件,并修改 iDRAC 配置:
|
||||||
|
|
||||||
欢迎提交 Issue 和 Pull Request 来改进项目。
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
## 许可证
|
`.env` example / 示例:
|
||||||
|
|
||||||
该项目遵循原项目的开源协议。
|
```env
|
||||||
|
HOST=192.168.1.100
|
||||||
|
USERNAME=root
|
||||||
|
PASSWORD=your_idrac_password
|
||||||
|
```
|
||||||
|
|
||||||
|
Start the service / 启动服务:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run with Python / 直接使用 Python 运行
|
||||||
|
|
||||||
|
Install dependencies / 安装依赖:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Debian / Ubuntu
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install -y ipmitool python3
|
||||||
|
|
||||||
|
# macOS with Homebrew
|
||||||
|
brew install ipmitool python
|
||||||
|
```
|
||||||
|
|
||||||
|
Run / 运行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export HOST=192.168.1.100
|
||||||
|
export USERNAME=root
|
||||||
|
export PASSWORD=your_idrac_password
|
||||||
|
python3 start.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration / 配置
|
||||||
|
|
||||||
|
| Variable | Required | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `HOST` | Yes | iDRAC management IP address. / iDRAC 管理 IP。 |
|
||||||
|
| `USERNAME` | Yes | iDRAC username with IPMI permission. / 有 IPMI 权限的 iDRAC 用户名。 |
|
||||||
|
| `PASSWORD` | Yes | iDRAC password. / iDRAC 密码。 |
|
||||||
|
|
||||||
|
The application does not include default credentials. Missing variables will stop startup with a clear error.
|
||||||
|
|
||||||
|
程序不内置默认地址、账号或密码。缺少环境变量时会直接停止并输出明确错误。
|
||||||
|
|
||||||
|
## Temperature Policy / 温控策略
|
||||||
|
|
||||||
|
The controller reads all temperature sensors and uses the highest value.
|
||||||
|
|
||||||
|
控制器读取所有温度传感器,并使用最高温度作为控制依据。
|
||||||
|
|
||||||
|
| Temperature / 温度 | Fan Speed / 风扇转速 |
|
||||||
|
| --- | --- |
|
||||||
|
| `0-50 C` | `20%` |
|
||||||
|
| `50-55 C` | `25%` |
|
||||||
|
| `55-60 C` | `30%` |
|
||||||
|
| `60-65 C` | `40%` |
|
||||||
|
| `>65 C` | iDRAC automatic mode / iDRAC 自动模式 |
|
||||||
|
|
||||||
|
## Troubleshooting / 故障排查
|
||||||
|
|
||||||
|
Test IPMI manually first / 先手动测试 IPMI:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ipmitool -H 192.168.1.100 -I lanplus -U root -P your_idrac_password mc info
|
||||||
|
ipmitool -H 192.168.1.100 -I lanplus -U root -P your_idrac_password sdr
|
||||||
|
```
|
||||||
|
|
||||||
|
Common issues / 常见问题:
|
||||||
|
|
||||||
|
- `Unable to establish IPMI v2 / RMCP+ session`: iDRAC IPMI service may be busy or unstable. Check network latency, duplicate monitoring scripts, and consider resetting iDRAC with `mc reset cold`.
|
||||||
|
- Connection failed / 连接失败:确认容器主机能访问 iDRAC 管理 IP。
|
||||||
|
- Authentication failed / 认证失败:确认用户名、密码和 IPMI 权限。
|
||||||
|
- Permission denied / 权限不足:建议使用专用 iDRAC 用户,并授予 IPMI 控制权限。
|
||||||
|
|
||||||
|
## Release / 发布新版本
|
||||||
|
|
||||||
|
Docker images are published only from Git tags matching `v*`. Pull requests and `master` pushes only build and verify the image.
|
||||||
|
|
||||||
|
Docker 镜像只在推送 `v*` tag 时发布。PR 和 `master` 推送只做构建验证,不覆盖 `latest`。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git tag v1.0.0
|
||||||
|
git push origin v1.0.0
|
||||||
|
```
|
||||||
|
|
||||||
|
The tag workflow publishes:
|
||||||
|
|
||||||
|
- `lkddi/dell-fans-controller:latest`
|
||||||
|
- `lkddi/dell-fans-controller:v1.0.0`
|
||||||
|
- `lkddi/dell-fans-controller:1.0.0`
|
||||||
|
- `lkddi/dell-fans-controller:1.0`
|
||||||
|
- `lkddi/dell-fans-controller:1`
|
||||||
|
|
||||||
|
GitHub Actions requires these repository secrets:
|
||||||
|
|
||||||
|
- `DOCKER_USER`: Docker Hub username.
|
||||||
|
- `DOCKER_PASS`: Docker Hub access token. Do not use your account password.
|
||||||
|
|
||||||
|
## Development / 开发
|
||||||
|
|
||||||
|
Syntax check without generating tracked cache files:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -c "import ast, pathlib; [ast.parse(pathlib.Path(p).read_text(), filename=p) for p in ['start.py','controller/client.py','controller/ipmi.py']]"
|
||||||
|
```
|
||||||
|
|
||||||
|
Build locally:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t lkddi/dell-fans-controller:test .
|
||||||
|
```
|
||||||
|
|
||||||
|
## License / 许可证
|
||||||
|
|
||||||
|
MIT
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ from controller.logger import logger
|
|||||||
from controller.ipmi import IpmiTool
|
from controller.ipmi import IpmiTool
|
||||||
|
|
||||||
|
|
||||||
|
# 风扇控制器:根据iDRAC温度传感器结果自动切换风扇模式和转速
|
||||||
class FanController:
|
class FanController:
|
||||||
|
|
||||||
|
# 初始化控制器并记录iDRAC连接信息
|
||||||
def __init__(self, host: str, username: str, password: str):
|
def __init__(self, host: str, username: str, password: str):
|
||||||
self.host = host
|
self.host = host
|
||||||
self.username = username
|
self.username = username
|
||||||
@@ -14,10 +16,12 @@ class FanController:
|
|||||||
self.last_set_speed = None # 记录最后设置的风扇速度
|
self.last_set_speed = None # 记录最后设置的风扇速度
|
||||||
self.is_auto_mode = False # 记录当前是否为自动模式
|
self.is_auto_mode = False # 记录当前是否为自动模式
|
||||||
|
|
||||||
|
# 设置手动风扇速度
|
||||||
def set_fan_speed(self, speed: int):
|
def set_fan_speed(self, speed: int):
|
||||||
logger.info(f'设置风扇速度: {speed}%')
|
logger.info(f'设置风扇速度: {speed}%')
|
||||||
self.ipmi.set_fan_speed(speed)
|
self.ipmi.set_fan_speed(speed)
|
||||||
|
|
||||||
|
# 根据最高温度计算目标风扇转速
|
||||||
def get_required_fan_speed(self, temperature: int) -> int:
|
def get_required_fan_speed(self, temperature: int) -> int:
|
||||||
"""
|
"""
|
||||||
根据温度确定所需的风扇转速
|
根据温度确定所需的风扇转速
|
||||||
@@ -35,7 +39,9 @@ class FanController:
|
|||||||
else:
|
else:
|
||||||
return -1 # 表示应切换到自动模式
|
return -1 # 表示应切换到自动模式
|
||||||
|
|
||||||
|
# 执行一次完整的温度读取和风扇控制周期
|
||||||
def run(self):
|
def run(self):
|
||||||
|
# 同一轮控制周期复用一次SDR结果,减少iDRAC会话压力
|
||||||
sensor_data = self.ipmi.sensor()
|
sensor_data = self.ipmi.sensor()
|
||||||
temperature: int = max(self.ipmi.temperature(sensor_data))
|
temperature: int = max(self.ipmi.temperature(sensor_data))
|
||||||
logger.info(f'当前最高温度: {temperature}')
|
logger.info(f'当前最高温度: {temperature}')
|
||||||
|
|||||||
+18
-11
@@ -3,7 +3,10 @@ import time
|
|||||||
import re
|
import re
|
||||||
from controller.logger import logger
|
from controller.logger import logger
|
||||||
|
|
||||||
|
|
||||||
|
# IPMI命令封装器:负责调用ipmitool读取传感器并设置Dell风扇
|
||||||
class IpmiTool:
|
class IpmiTool:
|
||||||
|
# 初始化iDRAC连接参数
|
||||||
def __init__(self, host: str, username: str, password: str):
|
def __init__(self, host: str, username: str, password: str):
|
||||||
if not host or not username or not password:
|
if not host or not username or not password:
|
||||||
raise ValueError("host, username and password must be provided")
|
raise ValueError("host, username and password must be provided")
|
||||||
@@ -11,6 +14,7 @@ class IpmiTool:
|
|||||||
self.username = username
|
self.username = username
|
||||||
self.password = password
|
self.password = password
|
||||||
|
|
||||||
|
# 执行ipmitool命令并处理重试、超时和会话异常
|
||||||
def run_cmd(self, cmd: str) -> str:
|
def run_cmd(self, cmd: str) -> str:
|
||||||
basecmd = f'ipmitool -H {self.host} -I lanplus -U {self.username} -P {self.password}'
|
basecmd = f'ipmitool -H {self.host} -I lanplus -U {self.username} -P {self.password}'
|
||||||
command = f'{basecmd} {cmd}'
|
command = f'{basecmd} {cmd}'
|
||||||
@@ -47,20 +51,23 @@ class IpmiTool:
|
|||||||
else:
|
else:
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
# 读取iDRAC控制器基本信息
|
||||||
def mc_info(self) -> str:
|
def mc_info(self) -> str:
|
||||||
"""
|
"""
|
||||||
execute ipmitool command mc info
|
执行ipmitool mc info命令
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return self.run_cmd(cmd='mc info')
|
return self.run_cmd(cmd='mc info')
|
||||||
|
|
||||||
|
# 读取完整SDR传感器数据
|
||||||
def sensor(self) -> str:
|
def sensor(self) -> str:
|
||||||
"""
|
"""
|
||||||
execute ipmitool command sdr to get sensor data
|
执行ipmitool sdr命令获取传感器数据
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return self.run_cmd(cmd='sdr')
|
return self.run_cmd(cmd='sdr')
|
||||||
|
|
||||||
|
# 从SDR数据中解析温度传感器列表
|
||||||
def temperature(self, data: str = None) -> list:
|
def temperature(self, data: str = None) -> list:
|
||||||
"""
|
"""
|
||||||
获取当前温度传感器列表
|
获取当前温度传感器列表
|
||||||
@@ -84,32 +91,30 @@ class IpmiTool:
|
|||||||
|
|
||||||
return temperatures
|
return temperatures
|
||||||
|
|
||||||
|
# 从SDR数据中解析风扇RPM列表
|
||||||
def fan_speeds(self) -> list:
|
def fan_speeds(self) -> list:
|
||||||
"""
|
"""
|
||||||
get current fan speeds
|
获取当前风扇RPM列表
|
||||||
:return: list of fan speeds in percentage
|
:return: 风扇RPM列表
|
||||||
"""
|
"""
|
||||||
data = self.sensor()
|
data = self.sensor()
|
||||||
fan_speeds = []
|
fan_speeds = []
|
||||||
|
|
||||||
for line in data.splitlines():
|
for line in data.splitlines():
|
||||||
if 'Fan' in line and 'RPM' in line:
|
if 'Fan' in line and 'RPM' in line:
|
||||||
# Extract numeric value from line - format is typically "Fan1 | 1234 | RPM |"
|
# 从传感器行中提取RPM数值,典型格式为 "Fan1 RPM | 4800 RPM | ok"
|
||||||
parts = line.split('|')
|
parts = line.split('|')
|
||||||
if len(parts) >= 2:
|
if len(parts) >= 2:
|
||||||
try:
|
try:
|
||||||
# Extract the value and convert RPM to percentage if possible
|
|
||||||
# For Dell servers, we may need to get duty cycle instead
|
|
||||||
value_str = parts[1].strip()
|
value_str = parts[1].strip()
|
||||||
if value_str.isdigit():
|
if value_str.isdigit():
|
||||||
rpm = int(value_str)
|
rpm = int(value_str)
|
||||||
# Placeholder: we might need to use raw commands to get duty cycle
|
|
||||||
# For now, return the raw value
|
|
||||||
fan_speeds.append(rpm)
|
fan_speeds.append(rpm)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
continue
|
continue
|
||||||
return fan_speeds
|
return fan_speeds
|
||||||
|
|
||||||
|
# 获取当前风扇占空比,raw命令不可用时用RPM估算
|
||||||
def get_fan_duty_cycle(self, sensor_data: str = None) -> int:
|
def get_fan_duty_cycle(self, sensor_data: str = None) -> int:
|
||||||
"""
|
"""
|
||||||
获取当前风扇占空比/百分比
|
获取当前风扇占空比/百分比
|
||||||
@@ -177,9 +182,10 @@ class IpmiTool:
|
|||||||
|
|
||||||
return -1 # Return -1 if unable to determine
|
return -1 # Return -1 if unable to determine
|
||||||
|
|
||||||
|
# 切换风扇自动/手动模式
|
||||||
def switch_fan_mode(self, auto: bool):
|
def switch_fan_mode(self, auto: bool):
|
||||||
"""
|
"""
|
||||||
switch the fan mode
|
切换风扇模式
|
||||||
:param auto:
|
:param auto:
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
@@ -187,9 +193,10 @@ class IpmiTool:
|
|||||||
auto_cmd = 'raw 0x30 0x30 0x01 0x01'
|
auto_cmd = 'raw 0x30 0x30 0x01 0x01'
|
||||||
return self.run_cmd(cmd=auto_cmd) if auto else self.run_cmd(cmd=manual_cmd)
|
return self.run_cmd(cmd=auto_cmd) if auto else self.run_cmd(cmd=manual_cmd)
|
||||||
|
|
||||||
|
# 设置手动风扇速度百分比
|
||||||
def set_fan_speed(self, speed: int):
|
def set_fan_speed(self, speed: int):
|
||||||
"""
|
"""
|
||||||
set fan speed
|
设置风扇速度
|
||||||
:param speed:
|
:param speed:
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ logger = logging.getLogger(__name__)
|
|||||||
logger.setLevel(logging.DEBUG)
|
logger.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
|
||||||
|
# 自定义日志格式器:统一输出北京时间,方便容器日志排查
|
||||||
class CustomFormatter(logging.Formatter):
|
class CustomFormatter(logging.Formatter):
|
||||||
|
# 格式化日志记录并注入北京时间字段
|
||||||
def format(self, record):
|
def format(self, record):
|
||||||
desired_timezone = timezone(timedelta(hours=8))
|
desired_timezone = timezone(timedelta(hours=8))
|
||||||
current_time = datetime.now(desired_timezone).strftime('%Y-%m-%d %H:%M:%S')
|
current_time = datetime.now(desired_timezone).strftime('%Y-%m-%d %H:%M:%S')
|
||||||
@@ -13,6 +15,7 @@ class CustomFormatter(logging.Formatter):
|
|||||||
return super().format(record)
|
return super().format(record)
|
||||||
|
|
||||||
|
|
||||||
|
# 标准输出日志处理器:让Docker日志直接显示控制器运行状态
|
||||||
stream_handler = logging.StreamHandler()
|
stream_handler = logging.StreamHandler()
|
||||||
stream_handler.setFormatter(CustomFormatter(' %(customtime)s [%(levelname)s] %(message)s'))
|
stream_handler.setFormatter(CustomFormatter(' %(customtime)s [%(levelname)s] %(message)s'))
|
||||||
logger.addHandler(stream_handler)
|
logger.addHandler(stream_handler)
|
||||||
|
|||||||
+4
-8
@@ -1,11 +1,7 @@
|
|||||||
version: "3"
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
dell-fans-controller-docker:
|
dell-fans-controller:
|
||||||
image: lkddi/dell-fans-controller:latest
|
image: lkddi/dell-fans-controller:latest
|
||||||
container_name: dell-fans-controller-docker
|
container_name: dell-fans-controller
|
||||||
restart: always
|
restart: always
|
||||||
environment:
|
env_file:
|
||||||
HOST: 192.168.1.1
|
- .env
|
||||||
USERNAME: root
|
|
||||||
PASSWORD: password
|
|
||||||
|
|||||||
@@ -7,9 +7,10 @@ from controller.logger import logger
|
|||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
||||||
host = os.getenv('HOST', "192.168.1.100")
|
# 从环境变量读取iDRAC连接信息,开源版本不内置任何真实默认凭据
|
||||||
username = os.getenv('USERNAME', "root")
|
host = os.getenv('HOST')
|
||||||
password = os.getenv('PASSWORD', "your_idrac_password")
|
username = os.getenv('USERNAME')
|
||||||
|
password = os.getenv('PASSWORD')
|
||||||
if not host:
|
if not host:
|
||||||
raise RuntimeError('未设置 HOST 环境变量')
|
raise RuntimeError('未设置 HOST 环境变量')
|
||||||
|
|
||||||
@@ -19,14 +20,17 @@ if __name__ == '__main__':
|
|||||||
if not password:
|
if not password:
|
||||||
raise RuntimeError('未设置 PASSWORD 环境变量')
|
raise RuntimeError('未设置 PASSWORD 环境变量')
|
||||||
|
|
||||||
|
# 复用控制器实例,避免每轮循环丢失上次设置状态
|
||||||
client = FanController(host=host, username=username, password=password)
|
client = FanController(host=host, username=username, password=password)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
|
# 执行一次温度读取和风扇控制周期
|
||||||
client.run()
|
client.run()
|
||||||
time.sleep(60)
|
time.sleep(60)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
logger.error(
|
logger.error(
|
||||||
f'运行控制器失败 {err}. {traceback.format_exc()}'
|
f'运行控制器失败 {err}. {traceback.format_exc()}'
|
||||||
)
|
)
|
||||||
|
# iDRAC会话异常时等待下一轮,避免连续请求压垮IPMI服务
|
||||||
time.sleep(60)
|
time.sleep(60)
|
||||||
|
|||||||
Reference in New Issue
Block a user