init commit

This commit is contained in:
joestar817
2024-05-19 22:38:21 +08:00
commit d2b20168c4
10 changed files with 236 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
.idea

19
Dockerfile Normal file
View 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
View 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
View 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
View File

33
controller/client.py Normal file
View 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
View 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
View 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
View 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
View 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()}'
)