Compare commits

...

14 Commits

  1. 5
      .gitignore
  2. 169
      README.md
  3. BIN
      __pycache__/version.cpython-312.pyc
  4. 30
      client.py
  5. 5
      client_config.json
  6. 317
      index.html
  7. BIN
      pics/demo.png
  8. 24
      server.py
  9. 6
      server_config.json
  10. 1
      version.py
  11. 52
      web/css/style_1.css
  12. 9
      web/index.html
  13. 60
      web/js/script.js

5
.gitignore

@ -1 +1,6 @@
serverList.json serverList.json
# build
build/
dist/
*.spec

169
README.md

@ -1,69 +1,96 @@
# 1. 简介 # 1. 简介
在网页上同时查看多个服务器的信息(CPU、网络、内存、硬盘、显卡) 在网页上同时查看多个服务器的信息(CPU、网络、内存、硬盘、显卡)
大致原理是后端的python程序通过ssh连接服务器,定期通过终端解析获取所需数据存在字典中,然后前端网页定期获取字典的内容进行可视化 分为服务端和客户端,客户端向服务端发送本机数据,服务端整理所有客户端的数据,**服务端不再需要保存客户端的密钥**
![](pics/demo.png) ![](pics/demo.png)
# 2. 安装
## 2.1. 运行环境 **Tips:** 将鼠标悬浮在`网络`、`CPU占用率`、`GPU使用情况的用户`上时可以查看更详细的信息。
即运行后端程序所需的环境,可在conda中安装虚拟环境,linux和windows都可以。 # 2. 开发环境
可在conda中安装虚拟环境,linux和windows都可以。
```bash ```bash
pip install flask flask-cors psutil -i https://pypi.tuna.tsinghua.edu.cn/simple pip install flask flask-cors psutil -i https://pypi.tuna.tsinghua.edu.cn/simple
``` ```
## 2.2. 服务器环境
即需要被查看的服务器上所安装的环境。
因为本质上是通过ssh连接服务器,然后通过命令来获取相应的信息,有的命令可能服务器系统上不自带需要另外安装,否则无法获取到对应的数据。 # 3. 运行部署
- **ifstat**,用于获取网络数据的工具,可通过apt安装(如果不需要显示网络数据则不用安装)。并且需要在服务器上运行一下命令,查看哪个网卡才是主要的,写到配置文件里去(如果不需要查看网络信息可以不写)。 客户端的机器上 **可能** 还需要通过APT安装一下`gpustat`,最好是`1.1.1`版的,`0.6.0`貌似会出问题。
- **gpustat**,用于获取显卡上用户的使用情况,也可通过apt安装。
- **nvidia驱动**,需要需要安装N卡的驱动,能够通过`nvidia-smi`来获取显卡信息即可(AMD的应该就没办法了)。
其中这个ifstat查看网卡的步骤如下:通过apt安装完成之后,在终端输入`ifstat`,可以看到类似下面的输出(ctrl+c停止),因为一般会不只一个网卡,而且名称也会不一样。此时可以看一下哪个名称的网卡有数据变化,比如下方的就是`eno2`,可以写到配置文件里。 可以使用`pyinstaller`将python程序打包得到客户端和服务端的可执行程序,则不再需要安装运行环境。如果不打包直接用python执行的话就要安装前面的开发环境。
```bash
pyinstaller --onefile client.py
pyinstaller --onefile server.py
``` ```
eno1 eno2 br-6c8650526aef docker0 veth1d3300f 执行命令之后,可以`dist`目录内找到两个可执行文件,将`client`文件放到客户端的合适的地方,`server`放到服务器的合适的地方。客户端指的就是需要获取数据的机器,服务端就是网页所在的机器。
KB/s in KB/s out KB/s in KB/s out KB/s in KB/s out KB/s in KB/s out KB/s in KB/s out 以及放置对应的`client_config.json`和`server_config.json`。
0.00 0.00 3.31 1.96 0.00 0.00 0.00 0.00 0.00 0.00
0.00 0.00 2.23 1.52 0.00 0.00 0.00 0.00 0.00 0.00 ## 3.1. 服务器
0.00 0.00 7.56 8.03 0.00 0.00 0.00 0.00 0.00 0.00
0.00 0.00 4.00 4.55 0.00 0.00 0.00 0.00 0.00 0.00 执行以下命令即可,注意server和json要改为实际的路径。可以用screen或者systemctl来保持后台执行,推荐使用systemctl实现开机自启。
0.00 0.00 3.66 0.19 0.00 0.00 0.00 0.00 0.00 0.00 ```bash
0.00 0.00 8.34 8.26 0.00 0.00 0.00 0.00 0.00 0.00 /home/lxb/projects/Tool_CheckGPUsWeb/dist/server --cfg /home/lxb/projects/Tool_CheckGPUsWeb/server_config.json
0.00 0.00 8.25 4.78 0.00 0.00 0.00 0.00 0.00 0.00
``` ```
## 2.3. 后端部署
安装好运行环境且设置好配置文件后,直接开一个screen,然后在目录下运行`python app.py`即可。(需要确保当前机器能够访问到所需要监视的服务器)
需要注意的是,app.py内最后几行可以找到`app.run(debug=True, host='127.0.0.1', port=port)`这行代码,可以将debug改为`False`,host可以改为`0.0.0.0`(在云服务器上部署时貌似需要改为这个),port可以按需修改。并且需要在防火墙上打开对应端口。可修改`check_interval`变量,默认为2,代表检测一次服务器信息的间隔。 其中`server_config.json`的内容如下:
```json
{
"host": "0.0.0.0",
"port": 15002,
"server_list":["76", "174", "233", "222"],
"note_dict":{
"76" : "这是一个公告内容"
},
"api_name": "api"
}
```
- host:不用改。
- port:改成合适的端口,服务器记得要开放这个端口。
- server_list:所查询的服务器名称列表,客户端访问的时候只有下列对应的名称才会被处理。
- note_dict:公告字典,可以给对应服务器显示公告。
- api_name:api的名称,保持服务器、客户端和nginx的设置统一即可。
修改配置文件之后需要**重启**程序才能生效。
## 3.2. 客户端
执行以下命令即可,注意client和json要改为实际的路径。可以用screen或者systemctl来保持后台执行,推荐使用systemctl实现开机自启。
```bash
/home/lxb/projects/ServerInfo-client/client --cfg /home/lxb/projects/ServerInfo-client/client_config.json
```
其中配置文件默认名称为`serverList.json`,需要自己创建,格式参考`serverList_example.json`,具体规则如下: 其中`client_config.json`中的内容如下,
- title:服务器名称,用于显示。
- ip:服务器ip地址,用于连接。
- port:访问的端口,一般是22,如果访问容器等则按需修改。
- username:用于登录的账户名称。
- password:用于登录的账户密码。
- key_filename:用于登录的账户密钥**路径**。(password和key_filename只需要设置一个即可,如果服务器只能使用密钥登陆则填密钥即可)
- network_interface_name:网卡名称。(非必须项,如果不需要可视化网速则不需要设置)
- storage_list:需要查看存储空间使用情况的路径list。(非必须项,无论有没有设置都会默认检查根目录的使用情况)
```json ```json
{ {
"title": "SERVER_76", "server_url": "http://10.1.16.174:15001",
"ip": "123.123.123.76", "title": "174",
"port": 22, "interval": 3.0,
"username": "lxb", "note": "",
"password": "abcdefg", "enable": ["gpu", "cpu", "memory", "storage", "network"],
"key_filename": "/home/.ssh/id_rsa", "storage_list":[
"network_interface_name": "eno2", "/",
"storage_list": [
"/media/D", "/media/D",
"/media/E",
"/media/F" "/media/F"
] ],
"api_name": "api"
} }
``` ```
- server_url:访问服务器的路径,即IP+端口,根据情况修改。(按理来说是直接15002,但是用了nginx之后会根据设置将api的请求转发到15002上,nginx监听的15001所以这里就是15001)
- title:客户端本机的名称,只有这个名称在服务器的server_list中才会被处理。(那是不是任一个客户端程序设置到其他的title就会干扰其他服务器数据的显示?**是的**,除了这个title没有做其他验证,只能是人工确保一下每个客户端有单独的title)
- interval:获取信息的间隔
- note:公告,会与在服务器那边设置的公告合并显示。
- enable:开启检测的内容,如果不需要检测某个数据删除掉即可,目前只支持`gpu`、`cpu`、`memory`、`storage`、`network`。
- storage_list:检测的硬盘的路径,需要检测哪条路径就加到这个上面。
- api_name:与服务器、nginx的设置保持一致即可,不然无法正常访问。
开启运行之后,如果`serverList.json`有修改,需要重新启动app.py才能生效。 修改配置文件之后需要**重启**程序才能生效。
## 3.3. 网页部署
> **注意**,网页部署这一块的文档可能还存在着不少问题,主要是当时是用GPT等工具辅助的搞的,现在再写这个文档已经过了好久细节记不清了。还有nginx这里写的也是用容器,但是好像不用容器的话会更好一些。
>
> 这部分文档还需要优化。
## 2.4. 网页部署
可以使用docker运行一个nginx的容器来简单的部署这个网页。 可以使用docker运行一个nginx的容器来简单的部署这个网页。
首先安装docker,安装完之后可执行命令`docker run -d -p 80:80 -v /home/lxb/nginx_gpus:/usr/share/nginx/html --name nginx_gpus nginx:latest`,注意**按需修改命令**,具体可修改内容如下。 首先安装docker,安装完之后可执行命令`docker run -d -p 80:80 -v /home/lxb/nginx_gpus:/usr/share/nginx/html --name nginx_gpus nginx:latest`,注意**按需修改命令**,具体可修改内容如下。
```bash ```bash
@ -74,21 +101,53 @@ docker run -d \
nginx:latest nginx:latest
``` ```
**另外需要**将index.html中的fetchData函数内的地址替换为对应后端的ip+端口。(`fetch('<替换这里>/all_data')`) 将web目录下的内容放到数据卷的对应位置。
另外nginx的配置如下:
```
server {
listen 15001;
listen [::]:15001;
# server_name *;
location / {
root /usr/share/nginx/html/serverInfo_web;
index index.html index.htm;
try_files $uri $uri/ =404;
}
location /api/ {
proxy_pass http://localhost:15002;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
然后把`index.html`放入数据卷中,替换掉原来的。然后访问主机`ip:映射的端口号`,如`123.123.123.123:80`(默认8080的话可以不输入)即可打开网页。 set $env "internal";
add_header X-Environment $env;
另外可以修改setInterval的时间,即多久访问一次后端,建议时间不要小于后端的check_interval,不然经常获取的也是没有更新的数据浪费了。 }
```javascript
// 页面加载时获取数据并定时刷新
document.addEventListener('DOMContentLoaded', function() {
fetchData();
setInterval(fetchData, 3000); // 每3秒刷新一次数据
});
``` ```
也就是直接访问服务器的15001端口可以访问网页,然后通过访问对应的`api`接口会转发到15002端口上,也就是flask的端口。
最后,有域名的话也可以搞一个反向代理,可参考 [服务器上使用Nginx部署网页+反向代理](http://blog.lxblxb.top/archives/1723257245091)(现在看来这个博客可能写的是有些问题的)。
有域名的话也可以搞一个反向代理,可参考 [服务器上使用Nginx部署网页+反向代理](http://blog.lxblxb.top/archives/1723257245091)。 **特别的**,`web/js/script.js`中有如下的代码,是通过`internal`来修改访问的URL,以实现访问内网的时候就采用内网的地址访问服务器,避免在内网的时候访问公网。这部分也要按需的修改一下。如果只是在内网访问则直接让`apiURL`等于内网地址即可。
```js
// 根据环境变量设置API URL
if (environment === 'internal') {
apiURL = 'http://10.1.16.174:15001';
} else {
apiURL = 'http://gpus.lxblxb.top';
}
```
以及反向代理的nginx那边也需要一些配置,大概是设置下面的内容。对应内网nginx设置的`internal`。
```
set $env "public";
add_header X-Environment $env;
```
# 3. 其他 # 4. 其他
- `永辉`帮忙搞了一下顶部checkbox布局的问题。 - `永辉`帮忙搞了一下顶部checkbox布局的问题(在这一版中没有加上checkbox)
- 参考`治鹏`的方法加了每张显卡的用户使用的情况。 - 参考`治鹏`的方法加了每张显卡的用户使用的情况。

BIN
__pycache__/version.cpython-312.pyc

Binary file not shown.

30
client.py

@ -2,8 +2,10 @@ import os
import json import json
import time import time
import psutil import psutil
import argparse
import requests import requests
import subprocess import subprocess
from version import version
# region get data # region get data
@ -19,10 +21,13 @@ def get_gpus_info(error_dict):
gpu_name = gpu_name.replace('NVIDIA ', '').replace('GeForce ', '') gpu_name = gpu_name.replace('NVIDIA ', '').replace('GeForce ', '')
process_list = list() process_list = list()
for process_info in gpu_info['processes']: for process_info in gpu_info['processes']:
cmd = process_info['command']
if 'full_command' in process_info:
cmd = ' '.join(process_info["full_command"])
process_list.append({ process_list.append({
"user": process_info['username'], "user": process_info['username'],
"memory": process_info['gpu_memory_usage'], "memory": process_info['gpu_memory_usage'],
"cmd": ' '.join(process_info["full_command"]) "cmd": cmd
}) })
# 加到list中 # 加到list中
@ -36,7 +41,7 @@ def get_gpus_info(error_dict):
"process_list": process_list "process_list": process_list
}) })
except Exception as e: except Exception as e:
error_dict['gpu'] = e error_dict['gpu'] = f'{e}'
return result_list return result_list
@ -85,7 +90,7 @@ def get_cpu_info(error_dict):
result_dict["core_occupy_list"] = psutil.cpu_percent(interval=None, percpu=True) result_dict["core_occupy_list"] = psutil.cpu_percent(interval=None, percpu=True)
except Exception as e: except Exception as e:
error_dict['cpu'] = e error_dict['cpu'] = f'{e}'
return result_dict return result_dict
@ -103,7 +108,7 @@ def get_storages_info(error_dict, path_list):
} }
result_list.append(tmp_res) result_list.append(tmp_res)
except Exception as e: except Exception as e:
error_dict['storage'] = e error_dict['storage'] = f'{e}'
return result_list return result_list
@ -115,7 +120,7 @@ def get_memory_info(error_dict):
result_dict["total"] = mem.total / 1024 result_dict["total"] = mem.total / 1024
result_dict["used"] = mem.used / 1024 result_dict["used"] = mem.used / 1024
except Exception as e: except Exception as e:
error_dict['memory'] = e error_dict['memory'] = f'{e}'
return result_dict return result_dict
@ -124,7 +129,7 @@ last_network_stats = None
last_network_time = None last_network_time = None
def get_networks_info(error_dict): def get_networks_info(error_dict):
result_list = list() result_list = list()
# try: try:
global last_network_stats global last_network_stats
global last_network_time global last_network_time
current_stats = psutil.net_io_counters(pernic=True) current_stats = psutil.net_io_counters(pernic=True)
@ -155,6 +160,8 @@ def get_networks_info(error_dict):
# 记录信息下次用 # 记录信息下次用
last_network_stats = current_stats last_network_stats = current_stats
last_network_time = time.time() last_network_time = time.time()
except Exception as e:
error_dict['network'] = f'{e}'
return result_list return result_list
@ -184,23 +191,28 @@ def collect_data():
result_dict['note'] = client_cfg['note'] result_dict['note'] = client_cfg['note']
result_dict['title'] = client_cfg['title'] result_dict['title'] = client_cfg['title']
result_dict['interval'] = client_cfg['interval'] result_dict['interval'] = client_cfg['interval']
result_dict['version'] = version
return result_dict return result_dict
def main(): def main():
parser = argparse.ArgumentParser()
parser.add_argument('--cfg', default='client_config.json', type=str, help='the path of config json.')
args = parser.parse_args()
# 加载配置文件 # 加载配置文件
cfg_path = "client_config.json" cfg_path = args.cfg
global client_cfg global client_cfg
with open(cfg_path, 'r') as f: with open(cfg_path, 'r') as f:
client_cfg = json.load(f) client_cfg = json.load(f)
# 持续发送 # 持续发送
send_interval = client_cfg['interval'] send_interval = client_cfg['interval']
api_url = client_cfg['server_url'] + '/api/update_data' api_name = client_cfg['api_name']
api_url = client_cfg['server_url'] + f'/{api_name}/update_data'
while True: while True:
data = collect_data() data = collect_data()
try: try:
requests.post(api_url, json=data) result = requests.post(api_url, json=data)
except Exception as e: except Exception as e:
print(e) print(e)
time.sleep(send_interval) time.sleep(send_interval)

5
client_config.json

@ -1,5 +1,5 @@
{ {
"server_url": "http://127.0.0.1:15002", "server_url": "http://10.1.16.174:15001",
"title": "174", "title": "174",
"interval": 3.0, "interval": 3.0,
"note": "", "note": "",
@ -9,5 +9,6 @@
"/media/D", "/media/D",
"/media/E", "/media/E",
"/media/F" "/media/F"
] ],
"api_name": "api"
} }

317
index.html

@ -1,317 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Server and GPU Information</title>
<style>
.card {
margin: 10px;
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
width: 300px;
display: inline-block;
vertical-align: top;
}
.server-name {
font-weight: bold;
margin-bottom: 5px;
font-size: 24px; /* 调整字体大小 */
background-color: black; /* 背景色设为黑色 */
color: white; /* 文字颜色设为白色 */
padding: 10px; /* 增加内边距使其更美观 */
border-radius: 5px; /* 可选:增加圆角效果 */
}
.gpu-info {
margin-top: 10px;
border: 1px solid #ccc; /* 边框 */
border-radius: 8px; /* 圆角 */
padding: 10px; /* 内边距 */
margin-bottom: 15px; /* 下边距 */
background-color: #f9f9f9; /* 背景颜色 */
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); /* 阴影 */
}
.user-info {
margin-top: 10px; /* 上边距 */
font-size: 14px; /* 字体大小 */
color: #555; /* 字体颜色 */
}
.user-item {
color: #007bff; /* 用户名颜色 */
font-weight: bold; /* 加粗 */
}
/* 头部样式 */
.head_contrainer{
display: flex;
flex-direction: row;
justify-content: space-between;
height: 90px;
align-items: center;
}
.head_contrainer .checkboxes{
display: flex;
align-items: center;
}
</style>
</head>
<body>
<div class="head_contrainer">
<div>
<h1>Server and GPU Information</h1>
<p id="time"></p>
</div>
<div class="checkboxes">
<div class="sample">
<label for="toggle_network">网络</label>
<input type="checkbox" id="toggle_network" checked onchange="updateDisplay()">
</div>
<div class="sample">
<label for="toggle_memory">内存</label>
<input type="checkbox" id="toggle_memory" checked onchange="updateDisplay()">
</div>
<div class="sample">
<label for="toggle_storage">存储</label>
<input type="checkbox" id="toggle_storage" checked onchange="updateDisplay()">
</div>
<div class="sample">
<label for="toggle_gpus">显卡</label>
<input type="checkbox" id="toggle_gpus" checked onchange="updateDisplay()">
</div>
</div>
</div>
<div id="server-data"></div>
<script>
let lastData = null;
// 请求服务器获取GPus数据
function fetchData() {
fetch('http://127.0.0.1:15002/all_data')
// 获取服务器和显卡数据
.then(response => response.json()) // 解析 JSON 响应
.then(data => {
// 处理 JSON 数据
// console.log(data);
displayServerData(data); // 调用显示数据的函数
})
.catch(error => {
// console.error('Error fetching data:', error);
displayError(error + " (多半是没有正确连接服务器端,可能是没开、网络错误)");
});
}
function displayError(err_info){
let serverDataContainer = document.getElementById('server-data');
serverDataContainer.innerHTML = ''; // 清空容器
let errDiv = document.createElement('div');
errDiv.classList.add('error-info');
errDiv.innerHTML = err_info;
serverDataContainer.appendChild(errDiv);
}
function parse_data_unit(num, fixedLen=2){
if (num < 1024){
return num.toFixed(fixedLen) + " KB";
}
num /= 1024;
if (num < 1024){
return num.toFixed(fixedLen) + " MB";
}
num /= 1024;
if (num < 1024){
return num.toFixed(fixedLen) + " GB";
}
num /= 1024;
if (num < 1024){
return num.toFixed(fixedLen) + " TB";
}
}
function add_bar(serverCard){
let bar = document.createElement('hr');
serverCard.appendChild(bar);
}
function updateDisplay(){
if (lastData != null){
displayServerData(lastData);
}
}
// 页面绑定数据
function displayServerData(data) {
lastData = data;
// 绘制 -------------------
let serverDataContainer = document.getElementById('server-data');
serverDataContainer.innerHTML = ''; // 清空容器
let timeStr = data['time']
let serverData = data['server_data']
let timeDiv = document.getElementById('time')
timeDiv.textContent = "更新时间为:" + timeStr
let greenDot = '<span style="color: green;"> 空闲</span>';
let yellowDot = '<span style="color: orange;"> 占用</span>';
let redDot = '<span style="color: red;"> 占用</span>';
for (let key in serverData){
let serverCard = document.createElement('div');
serverCard.classList.add('card');
// 标题
let serverName = document.createElement('div');
serverName.classList.add('server-name');
let updateFlag = serverData[key].updated ? '' : ' - Not updated -';
serverName.textContent = key + updateFlag;
serverCard.appendChild(serverName);
// 网速
if (document.getElementById('toggle_network').checked && 'network_info' in serverData[key]){
let networkInfo = document.createElement('div');
networkInfo.classList.add('network-info');
let inNum = serverData[key].network_info.in;
let outNum = serverData[key].network_info.out;
inNum = parse_data_unit(inNum)
outNum = parse_data_unit(outNum)
networkInfo.innerHTML += "<strong> 网络 : </strong> in: " + inNum + "/s, out: " + outNum + "/s";
serverCard.appendChild(networkInfo);
// 分割线
add_bar(serverCard);
}
// 内存
if (document.getElementById('toggle_memory').checked && 'memory_info' in serverData[key]){
let memoryInfo = document.createElement('div');
memoryInfo.classList.add('memory-info');
let totalNum = serverData[key].memory_info.total
let usedNum = serverData[key].memory_info.used
let totalMem = parse_data_unit(totalNum);
let usedMem = parse_data_unit(usedNum);
let tmpColor = "green";
if (usedNum / totalNum > 0.8)
tmpColor = "red";
else if (usedNum / totalNum > 0.6)
tmpColor = "orange";
memoryInfo.innerHTML += "<strong> 内存 : </strong> <span style=\"color: " + tmpColor + ";\">" + usedMem + " / " + totalMem + "</span><br>";
serverCard.appendChild(memoryInfo);
// 分割线
add_bar(serverCard);
}
// 存储空间
if (document.getElementById('toggle_storage').checked && 'storage_info_list' in serverData[key]){
let storageInfo = document.createElement('div');
storageInfo.classList.add('storage-info');
for (let i = 0; i < serverData[key].storage_info_list.length; i++) {
let targetPath = serverData[key].storage_info_list[i].path;
let totalNum = serverData[key].storage_info_list[i].total
let availableNum = serverData[key].storage_info_list[i].available
let totalStorage = parse_data_unit(totalNum);
let availableStorage = parse_data_unit(totalNum - availableNum);
let tmpColor = "green";
if (availableNum / totalNum < 0.1)
tmpColor = "red";
else if (availableNum / totalNum < 0.3)
tmpColor = "orange";
storageInfo.innerHTML += '<strong>' + targetPath + " :</strong> <span style=\"color: " + tmpColor
+ ";\">" + availableStorage + " / " + totalStorage + "</span><br>";
}
serverCard.appendChild(storageInfo);
// 分割线
add_bar(serverCard);
}
// gpu
if (document.getElementById('toggle_gpus').checked && 'gpu_info_list' in serverData[key]){
serverData[key].gpu_info_list.forEach(function(gpu){
let gpuInfo = document.createElement('div');
gpuInfo.classList.add('gpu-info');
let colorDot = greenDot;
if (gpu.used_mem < 1000 && gpu.util_gpu < 20){
colorDot = greenDot;
}
else if (gpu.util_mem < 50){
colorDot = yellowDot;
}else{
colorDot = redDot;
}
gpuInfo.innerHTML = '<strong>' + gpu.idx + ' - ' + gpu.gpu_name + colorDot + '</strong><br>'
+ '温度: ' + gpu.temperature + '°C<br>'
+ '显存: ' + gpu.used_mem + ' / ' + gpu.total_mem + " MB" + '<br>'
+ '利用率: ' + gpu.util_gpu + '%';
// 添加用户使用信息
if ('users' in gpu) { // 检查是否有用户信息
let userInfo = document.createElement('div');
userInfo.classList.add('user-info');
userInfo.innerHTML = "<strong>使用情况:</strong>";
// for (const [username, mem] of Object.entries(gpu.users)) {
// userInfo.innerHTML += `<span class="user-item">${username} (${mem}) </span>`;
// }
// 排序
const user_entries = Object.entries(gpu.users);
const sorted_user = user_entries.sort((a, b) => b[1] - a[1]);
sorted_user.forEach(([key, value]) => {
// 过滤小于50MB的
if (value > 40)
userInfo.innerHTML += `<span class="user-item">${key} (${value}) </span>`;
});
gpuInfo.appendChild(userInfo); // 将用户信息添加到GPU信息中
}
serverCard.appendChild(gpuInfo);
});
// 分割线
add_bar(serverCard);
}
// 错误信息
if ('err_info' in serverData[key])
{
let errInfo = document.createElement('div');
errInfo.classList.add('error-info');
errInfo.innerHTML = '<strong>error info</strong><br>' + serverData[key].err_info;
serverCard.appendChild(errInfo);
// 分割线
add_bar(serverCard);
}
// 删除最后的分割线
if (serverCard.lastElementChild && serverCard.lastElementChild.tagName === 'HR') {
serverCard.removeChild(serverCard.lastElementChild);
}
serverDataContainer.appendChild(serverCard);
}
}
// 页面加载时获取数据并定时刷新
document.addEventListener('DOMContentLoaded', function() {
fetchData();
setInterval(fetchData, 3000); // 每3秒刷新一次数据
});
</script>
</body>
</html>

BIN
pics/demo.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 754 KiB

After

Width:  |  Height:  |  Size: 754 KiB

24
server.py

@ -1,6 +1,8 @@
from flask import Flask, jsonify, request from flask import Flask, jsonify, request
from flask_cors import CORS from flask_cors import CORS
from version import version
import json import json
import argparse
#region 全局 #region 全局
@ -9,20 +11,29 @@ CORS(app)
server_cfg = None server_cfg = None
data_dict = dict() data_dict = dict()
parser = argparse.ArgumentParser()
parser.add_argument('--cfg', default='server_config.json', type=str, help='the path of config json.')
args = parser.parse_args()
# 加载配置文件
cfg_path = args.cfg
with open(cfg_path, 'r') as f:
server_cfg = json.load(f)
api_name = server_cfg['api_name']
#endregion #endregion
#region 接口 #region 接口
# 测试用 # 测试用
@app.route('/api') @app.route(f'/{api_name}')
def hello(): def hello():
return 'hi. —— CheckGPUsWeb' return 'hi. —— CheckGPUsWeb'
@app.route('/api/get_data', methods=['GET']) @app.route(f'/{api_name}/get_data', methods=['GET'])
def get_data(): def get_data():
return jsonify(data_dict) return jsonify(data_dict)
@app.route('/api/update_data', methods=['POST']) @app.route(f'/{api_name}/update_data', methods=['POST'])
def receive_data(): def receive_data():
data = request.json data = request.json
# 如果存在对应标题则更新记录 # 如果存在对应标题则更新记录
@ -41,16 +52,11 @@ def receive_data():
def init(): def init():
data_dict['server_dict'] = dict() data_dict['server_dict'] = dict()
data_dict['version'] = version
for server_name in server_cfg['server_list']: for server_name in server_cfg['server_list']:
data_dict['server_dict'][server_name] = None data_dict['server_dict'][server_name] = None
def main(): def main():
# 加载配置文件
cfg_path = "server_config.json"
global server_cfg
with open(cfg_path, 'r') as f:
server_cfg = json.load(f)
init() init()
# flask # flask
app.run(debug=False, host=server_cfg['host'], port=server_cfg['port']) app.run(debug=False, host=server_cfg['host'], port=server_cfg['port'])

6
server_config.json

@ -1,8 +1,8 @@
{ {
"host": "127.0.0.1", "host": "0.0.0.0",
"port": 15002, "port": 15002,
"server_list":["76", "174", "233", "222"], "server_list":["76", "174", "233", "222"],
"note_dict":{ "note_dict":{
"174": "test note" },
} "api_name": "api"
} }

1
version.py

@ -0,0 +1 @@
version = "0.1.1.20250317_beta"

52
web/css/style_1.css

@ -1,31 +1,67 @@
#header-container { #header-container {
/* background-color: beige; */
font-size: 32px; font-size: 32px;
font-weight: bold; font-weight: bold;
padding-left: 20px;
padding-top: 10px;
/* padding-bottom: 5px; */
} }
.header-container { /* 设置html和body的高度为100% */
/* display: grid; html, body {
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); height: 100%;
gap: 10px; */ margin: 0;
padding: 0;
}
/* 使用flexbox布局 */
body {
display: flex;
flex-direction: column;
}
/* 主要内容区域,flex-grow: 1使其占据剩余空间 */
.content {
flex-grow: 1;
/* padding: 20px; */
}
/* Footer样式 */
footer {
background-color: #f1f1f1;
color: rgb(172, 172, 172);
text-align: center;
padding: 10px 0;
} }
.card { .card {
padding: 5px 10px; padding: 5px 10px;
margin: 5px; margin: 5px;
border-radius: 8px; border-radius: 8px;
box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); box-shadow: 0px 1px 10px rgba(0, 0, 0, 0.3);
width: 300px; width: 300px;
display: inline-block; display: inline-block;
vertical-align: top; vertical-align: top;
margin: 12px;
}
.note-info {
border-style: solid;
border-width: 4px;
border-color: #a10000;
border-radius: 8px;
padding: 6px 10px;
margin-top: 4px;
margin-bottom: 6px;
} }
.server-name { .server-name {
background-color: rgb(0, 0, 0); background-color: rgb(0, 0, 0);
color: white; color: white;
border-radius: 8px; border-radius: 8px;
padding: 4px 10px; padding: 6px 10px;
font-size: 26px; font-size: 26px;
margin-top: 4px;
margin-bottom: 6px; margin-bottom: 6px;
} }
@ -37,7 +73,7 @@
border-radius: 8px; border-radius: 8px;
margin-top: 5px; margin-top: 5px;
padding: 4px 8px; padding: 4px 8px;
margin-bottom: 5px; margin-bottom: 12px;
background-color: #f9f9f9; background-color: #f9f9f9;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
} }

9
web/index.html

@ -1,18 +1,27 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>服务器信息</title> <title>服务器信息</title>
<link rel="stylesheet" href="./css/style_1.css"> <link rel="stylesheet" href="./css/style_1.css">
</head> </head>
<body> <body>
<div class="content">
<div id="header-container"> <div id="header-container">
服务器信息 服务器信息
</div> </div>
<div id="server-data"> <div id="server-data">
</div> </div>
</div>
<footer>
<p>项目源码在<a href="http://git.lxblxb.top/lxb/Tool_CheckGPUsWeb/src/branch/v2">这里</a>,有问题可联系<span title="xiongbin_lin@163.com">lxb</span></p>
</footer>
<script src="./js/script.js"></script> <script src="./js/script.js"></script>
</body> </body>
</html> </html>

60
web/js/script.js

@ -1,6 +1,23 @@
// 判断内网还是公网
let apiURL = '';
fetch('/index.html') // 随便请求一个资源
.then(response => {
// 获取X-Environment响应头
const environment = response.headers.get('X-Environment');
// 根据环境变量设置API URL
if (environment === 'internal') {
apiURL = 'http://10.1.16.174:15001';
} else {
apiURL = 'http://gpus.lxblxb.top';
}
console.log('访问地址: ' + apiURL);
})
// 请求服务器获取数据 // 请求服务器获取数据
function fetchData() { function fetchData() {
fetch('http://127.0.0.1:15002/api/get_data') fetch(apiURL + '/api/get_data')
// 获取服务器和显卡数据 // 获取服务器和显卡数据
.then(response => response.json()) // 解析 JSON 响应 .then(response => response.json()) // 解析 JSON 响应
.then(data => { .then(data => {
@ -82,17 +99,27 @@ function displayServerData(data){
continue; continue;
} }
// 添加公告
if ('note' in serverData && serverData['note'] != ''){
let noteInfo = document.createElement('div');
noteInfo.className = 'note-info';
noteInfo.innerHTML = '<div style="text-align: center;"><strong>公告</strong></div>' + serverData['note'];
serverCard.appendChild(noteInfo);
}
// 判断时间 // 判断时间
let lastTime = new Date(serverData['update_time_stamp'] * 1000); let lastTime = new Date(serverData['update_time_stamp'] * 1000);
let timeFromUpdate = Date.now() - lastTime; let timeFromUpdate = Date.now() - lastTime;
if (timeFromUpdate > serverData['interval'] * 1000 * 3){ if (timeFromUpdate > serverData['interval'] * 1000 * 4){
let errText = document.createElement('div'); let errText = document.createElement('div');
errText.className = 'error-text'; errText.className = 'error-text';
errText.textContent = "长时间未更新,上次更新时间: " + lastTime.toLocaleString(); errText.textContent = "长时间未更新,上次更新时间: " + lastTime.toLocaleString();
serverCard.appendChild(errText); serverCard.appendChild(errText);
serverDataContainer.appendChild(serverCard); serverDataContainer.appendChild(serverCard);
continue; continue;
}else if (timeFromUpdate > serverData['interval'] * 1000 * 1.5){ }else if (timeFromUpdate > serverData['interval'] * 1000 * 2.5){
serverName.textContent = serverTitle + " - Not update -"; serverName.textContent = serverTitle + " - Not update -";
} }
@ -101,18 +128,20 @@ function displayServerData(data){
let networkInfo = document.createElement('div'); let networkInfo = document.createElement('div');
networkInfo.className = 'network-info'; networkInfo.className = 'network-info';
// todo 暂时采用所有网卡均值的方法 // todo 暂时采用所有网卡总和的方法
let inSum = 0; let inSum = 0;
let outSum = 0; let outSum = 0;
let tmpTitle = "";
serverData.network_list.forEach(function(network){ serverData.network_list.forEach(function(network){
inSum += network['in']; inSum += network['in'];
outSum += network['out']; outSum += network['out'];
tmpTitle += network['name'] + " in: " + parse_data_unit(network['in']) + "/s out: " + parse_data_unit(network['out']) + "/s\n";
}); });
let inStr = parse_data_unit(inSum / serverData.network_list.length); let inStr = parse_data_unit(inSum);
let outStr = parse_data_unit(outSum / serverData.network_list.length); let outStr = parse_data_unit(outSum);
networkInfo.innerHTML += "<strong> 网络 : </strong> in:" + inStr + "/s out:" + outStr + "/s</span><br>"; networkInfo.innerHTML += "<strong> 网络 : </strong> <span title=\"" + tmpTitle + "\">in:" + inStr + "/s out:" + outStr + "/s</span><br>";
serverCard.appendChild(networkInfo); serverCard.appendChild(networkInfo);
// 分割线 // 分割线
@ -128,9 +157,9 @@ function displayServerData(data){
serverData.cpu['temperature_list'].forEach(function(v){ serverData.cpu['temperature_list'].forEach(function(v){
temperature_list_str += v + " ℃ "; temperature_list_str += v + " ℃ ";
}); });
cpuInfo.innerHTML = "<strong>CPU型号 : </strong>" + serverData.cpu['name'] + "<br>" + cpuInfo.innerHTML = "<strong>" + serverData.cpu['name'] + "</strong><br>" +
"<strong>温度 : </strong>" + temperature_list_str + "<br>" + "<strong>温度 : </strong>" + temperature_list_str + "<br>" +
"<strong>占用率 : </strong>" + serverData.cpu['core_avg_occupy'] + "%"; "<strong>占用率 : </strong><span title=\"" + serverData.cpu['core_occupy_list'] + "\">" + serverData.cpu['core_avg_occupy'] + "%";
serverCard.appendChild(cpuInfo); serverCard.appendChild(cpuInfo);
// 分割线 // 分割线
@ -176,7 +205,7 @@ function displayServerData(data){
tmpClass = "state-occupy"; tmpClass = "state-occupy";
else if (availableNum / totalNum < 0.3) else if (availableNum / totalNum < 0.3)
tmpClass = "state-light-occupy"; tmpClass = "state-light-occupy";
storageInfo.innerHTML += '<strong>' + targetPath + " :</strong> <span class=\"" + tmpClass storageInfo.innerHTML += '<strong>' + targetPath + " :</strong> <span title=\"剩余可用:" + parse_data_unit(availableNum) + "\" class=\"" + tmpClass
+ "\">" + availableStorage + " / " + totalStorage + "</span><br>"; + "\">" + availableStorage + " / " + totalStorage + "</span><br>";
} }
@ -195,13 +224,14 @@ function displayServerData(data){
let markLightOccupy = '<span class="state-light-occupy"> 占用</span>'; let markLightOccupy = '<span class="state-light-occupy"> 占用</span>';
let markOccupy = '<span class="state-occupy"> 占用</span>'; let markOccupy = '<span class="state-occupy"> 占用</span>';
let tmpMark = markFree; let tmpMark = markFree;
if (gpu.used_memory < 1000 && gpu.utilization < 20){ let memory_used_ratio = gpu.used_memory / gpu.total_memory;
tmpMark = markFree; if (memory_used_ratio > 0.25 && gpu.utilization > 50){
tmpMark = markOccupy;
} }
else if (gpu.util_mem < 50){ else if (memory_used_ratio > 0.25 || gpu.utilization > 50){
tmpMark = markLightOccupy; tmpMark = markLightOccupy;
}else{ }else{
tmpMark = markOccupy; tmpMark = markFree;
} }
gpuInfo.innerHTML = '<strong>' + gpu.idx + ' - ' + gpu.name + tmpMark + '</strong><br>' gpuInfo.innerHTML = '<strong>' + gpu.idx + ' - ' + gpu.name + tmpMark + '</strong><br>'
+ '温度: ' + gpu.temperature + '°C<br>' + '温度: ' + gpu.temperature + '°C<br>'
@ -257,5 +287,5 @@ function displayServerData(data){
// 页面加载时获取数据并定时刷新 // 页面加载时获取数据并定时刷新
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
fetchData(); fetchData();
setInterval(fetchData, 3500); // 每3.5秒刷新一次数据 setInterval(fetchData, 4000); // 每4秒刷新一次数据
}); });
Loading…
Cancel
Save