From d2b20168c4d841115a7dae94c3a9e6e2c10021b1 Mon Sep 17 00:00:00 2001 From: joestar817 Date: Sun, 19 May 2024 22:38:21 +0800 Subject: [PATCH] init commit --- .gitignore | 1 + Dockerfile | 19 +++++++++++ Makefile | 9 +++++ README.md | 40 +++++++++++++++++++++++ controller/__init__.py | 0 controller/client.py | 33 +++++++++++++++++++ controller/ipmi.py | 74 ++++++++++++++++++++++++++++++++++++++++++ controller/logger.py | 18 ++++++++++ docker-compose.yml | 11 +++++++ start.py | 31 ++++++++++++++++++ 10 files changed, 236 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 README.md create mode 100644 controller/__init__.py create mode 100644 controller/client.py create mode 100644 controller/ipmi.py create mode 100644 controller/logger.py create mode 100644 docker-compose.yml create mode 100644 start.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..723ef36 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f0e5cb4 --- /dev/null +++ b/Dockerfile @@ -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"] + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..5d5e012 --- /dev/null +++ b/Makefile @@ -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 . + diff --git a/README.md b/README.md new file mode 100644 index 0000000..db60ca0 --- /dev/null +++ b/README.md @@ -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自动调速 | + + + +#### 免责声明 + +手动调整风扇转速有一定的风险导致服务器过热损坏,请谨慎操作,对此使用此项目引发的任何问题,概不负责 diff --git a/controller/__init__.py b/controller/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/controller/client.py b/controller/client.py new file mode 100644 index 0000000..09bca3a --- /dev/null +++ b/controller/client.py @@ -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) diff --git a/controller/ipmi.py b/controller/ipmi.py new file mode 100644 index 0000000..f508795 --- /dev/null +++ b/controller/ipmi.py @@ -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)}') diff --git a/controller/logger.py b/controller/logger.py new file mode 100644 index 0000000..d7265d6 --- /dev/null +++ b/controller/logger.py @@ -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) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..6b937ce --- /dev/null +++ b/docker-compose.yml @@ -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 \ No newline at end of file diff --git a/start.py b/start.py new file mode 100644 index 0000000..c7dcb40 --- /dev/null +++ b/start.py @@ -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()}' + )