mirror of
https://github.com/lkddi/dell-fans-controller-docker.git
synced 2026-04-03 09:55:11 +08:00
init commit
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.idea
|
||||
19
Dockerfile
Normal file
19
Dockerfile
Normal file
@@ -0,0 +1,19 @@
|
||||
FROM ubuntu:22.04
|
||||
LABEL maintainer="joestar817@foxmail.com"
|
||||
|
||||
|
||||
RUN apt update && apt install -y \
|
||||
ipmitool \
|
||||
python3 \
|
||||
python3-pip && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY . /dell-fans-controller-docker
|
||||
WORKDIR /dell-fans-controller-docker
|
||||
|
||||
ENV TZ=Asia/Shanghai
|
||||
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
|
||||
RUN echo 'Asia/Shanghai' >/etc/timezone
|
||||
|
||||
CMD ["python3","start.py"]
|
||||
|
||||
9
Makefile
Normal file
9
Makefile
Normal file
@@ -0,0 +1,9 @@
|
||||
.PHONY: help
|
||||
|
||||
help: ## show help message
|
||||
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[$$()% a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
|
||||
|
||||
|
||||
build: ## build docker image
|
||||
docker build -t dell-fans-controller-docker .
|
||||
|
||||
40
README.md
Normal file
40
README.md
Normal file
@@ -0,0 +1,40 @@
|
||||
### dell-fans-controller-docker
|
||||
|
||||
|
||||
|
||||
#### 项目说明
|
||||
|
||||
本项目通过python脚本调用ipmitool,来自动调整Dell R730服务器的风扇转速,并打包为docker镜像。
|
||||
|
||||
适用于在R730上搭建all in one服务场景,docker镜像可运行在pve、群晖,或其它支持docker的环境中
|
||||
|
||||
|
||||
|
||||
#### 使用说明
|
||||
|
||||
1. 请登录idrac打开ipmi服务
|
||||
|
||||
2. 运行以下命令
|
||||
```
|
||||
docker run -d --name=dell-fans-controller-docker -e HOST=192.168.1.1 -e USERNAME=root -e PASSWORD=password --restart always joestar817/dell-fans-controller-docker:latest
|
||||
```
|
||||
|
||||
#### 代码说明
|
||||
|
||||
脚本首先通过ipmitool来获取 **进出口温度和CPU核心温度**,再通过其中的最大值来判断调整服务器的风扇转速
|
||||
|
||||
运行间隔为每60秒运行一次
|
||||
|
||||
| 温度(℃) | 风扇转速(%) |
|
||||
|-------|--------------------|
|
||||
| 0-50 | 10 |
|
||||
| 50-55 | 20 |
|
||||
| 55-60 | 30 |
|
||||
| 60-65 | 40 |
|
||||
| >65℃ | 设置为自动模式,由idrac自动调速 |
|
||||
|
||||
|
||||
|
||||
#### 免责声明
|
||||
|
||||
手动调整风扇转速有一定的风险导致服务器过热损坏,请谨慎操作,对此使用此项目引发的任何问题,概不负责
|
||||
0
controller/__init__.py
Normal file
0
controller/__init__.py
Normal file
33
controller/client.py
Normal file
33
controller/client.py
Normal file
@@ -0,0 +1,33 @@
|
||||
from controller.logger import logger
|
||||
|
||||
from controller.ipmi import IpmiTool
|
||||
|
||||
|
||||
class FanController:
|
||||
|
||||
def __init__(self, host: str, username: str, password: str):
|
||||
self.host = host
|
||||
self.username = username
|
||||
self.password = password
|
||||
|
||||
self.ipmi = IpmiTool(self.host, self.username, self.password)
|
||||
|
||||
def set_fan_speed(self, speed: int):
|
||||
logger.info(f'Set fan speed: {speed}%')
|
||||
self.ipmi.set_fan_speed(speed)
|
||||
|
||||
def run(self):
|
||||
temperature: int = max(self.ipmi.temperature())
|
||||
logger.info(f'Current maximum temperature: {temperature}')
|
||||
|
||||
if 0 < temperature < 50:
|
||||
self.set_fan_speed(10)
|
||||
elif 50 < temperature < 55:
|
||||
self.set_fan_speed(20)
|
||||
elif 55 < temperature < 60:
|
||||
self.set_fan_speed(30)
|
||||
elif 60 < temperature < 65:
|
||||
self.set_fan_speed(40)
|
||||
else:
|
||||
logger.info(f'Switch fan control to auto mode')
|
||||
self.ipmi.switch_fan_mode(auto=True)
|
||||
74
controller/ipmi.py
Normal file
74
controller/ipmi.py
Normal file
@@ -0,0 +1,74 @@
|
||||
import subprocess
|
||||
|
||||
|
||||
class IpmiTool:
|
||||
|
||||
def __init__(self, host: str, username: str, password: str):
|
||||
self.host = host
|
||||
self.username = username
|
||||
self.password = password
|
||||
|
||||
def run_cmd(self, cmd: str) -> str:
|
||||
basecmd = f'ipmitool -H {self.host} -I lanplus -U {self.username} -P {self.password}'
|
||||
command = f'{basecmd} {cmd}'
|
||||
result = subprocess.run(command, shell=True, capture_output=True, text=True)
|
||||
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(
|
||||
f'execute command {cmd} failed:{result.stderr}'
|
||||
)
|
||||
|
||||
return result.stdout
|
||||
|
||||
def mc_info(self) -> str:
|
||||
"""
|
||||
execute ipmitool command mc info
|
||||
:return:
|
||||
"""
|
||||
return self.run_cmd(cmd='mc info')
|
||||
|
||||
def sensor(self) -> str:
|
||||
"""
|
||||
execute ipmitool command sensor
|
||||
:return:
|
||||
"""
|
||||
return self.run_cmd(cmd='sensor')
|
||||
|
||||
def temperature(self) -> list:
|
||||
"""
|
||||
get current temperature
|
||||
:return:
|
||||
"""
|
||||
data = self.sensor()
|
||||
temperatures = []
|
||||
|
||||
for line in data.splitlines():
|
||||
if 'Temp' in line:
|
||||
temperatures.append(float(line.split('|')[1].strip()))
|
||||
|
||||
return temperatures
|
||||
|
||||
def switch_fan_mode(self, auto: bool):
|
||||
"""
|
||||
switch the fan mode
|
||||
:param auto:
|
||||
:return:
|
||||
"""
|
||||
manual_cmd = 'raw 0x30 0x30 0x01 0x00'
|
||||
auto_cmd = 'raw 0x30 0x30 0x01 0x01'
|
||||
return self.run_cmd(cmd=auto_cmd) if auto else self.run_cmd(cmd=manual_cmd)
|
||||
|
||||
def set_fan_speed(self, speed: int):
|
||||
"""
|
||||
set fan speed
|
||||
:param speed:
|
||||
:return:
|
||||
"""
|
||||
if speed < 10 or speed > 100:
|
||||
raise ValueError(
|
||||
'speed must be between 10 and 100'
|
||||
)
|
||||
|
||||
self.switch_fan_mode(auto=False)
|
||||
base_cmd = 'raw 0x30 0x30 0x02 0xff'
|
||||
return self.run_cmd(cmd=f'{base_cmd} {hex(speed)}')
|
||||
18
controller/logger.py
Normal file
18
controller/logger.py
Normal file
@@ -0,0 +1,18 @@
|
||||
import logging
|
||||
from datetime import datetime, timezone, timedelta
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
class CustomFormatter(logging.Formatter):
|
||||
def format(self, record):
|
||||
desired_timezone = timezone(timedelta(hours=8))
|
||||
current_time = datetime.now(desired_timezone).strftime('%Y-%m-%d %H:%M:%S')
|
||||
record.customtime = current_time
|
||||
return super().format(record)
|
||||
|
||||
|
||||
stream_handler = logging.StreamHandler()
|
||||
stream_handler.setFormatter(CustomFormatter(' %(customtime)s [%(levelname)s] %(message)s'))
|
||||
logger.addHandler(stream_handler)
|
||||
11
docker-compose.yml
Normal file
11
docker-compose.yml
Normal file
@@ -0,0 +1,11 @@
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
dell-fans-controller-docker:
|
||||
image: joestar817/dell-fans-controller-docker:latest
|
||||
container_name: dell-fans-controller-docker
|
||||
restart: always
|
||||
environment:
|
||||
HOST: 192.168.1.1
|
||||
USERNAME: root
|
||||
PASSWORD: password
|
||||
31
start.py
Normal file
31
start.py
Normal file
@@ -0,0 +1,31 @@
|
||||
import os
|
||||
import time
|
||||
import traceback
|
||||
|
||||
from controller.client import FanController
|
||||
from controller.logger import logger
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
host = os.getenv('HOST')
|
||||
username = os.getenv('USERNAME')
|
||||
password = os.getenv('PASSWORD')
|
||||
|
||||
if host is None:
|
||||
raise RuntimeError('HOST environment variable not set')
|
||||
|
||||
if username is None:
|
||||
raise RuntimeError('USERNAME environment variable not set')
|
||||
|
||||
if password is None:
|
||||
raise RuntimeError('PASSWORD environment variable not set')
|
||||
|
||||
while True:
|
||||
try:
|
||||
client = FanController(host=host, username=username, password=password)
|
||||
client.run()
|
||||
time.sleep(60)
|
||||
except Exception as err:
|
||||
logger.error(
|
||||
f'run controller failed {err}. {traceback.format_exc()}'
|
||||
)
|
||||
Reference in New Issue
Block a user