Compare commits

...

14 Commits

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

7
.gitignore

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

169
README.md

@ -1,69 +1,96 @@
# 1. 简介
在网页上同时查看多个服务器的信息(CPU、网络、内存、硬盘、显卡)
大致原理是后端的python程序通过ssh连接服务器,定期通过终端解析获取所需数据存在字典中,然后前端网页定期获取字典的内容进行可视化
分为服务端和客户端,客户端向服务端发送本机数据,服务端整理所有客户端的数据,**服务端不再需要保存客户端的密钥**
![](pics/demo.png)
# 2. 安装
## 2.1. 运行环境
即运行后端程序所需的环境,可在conda中安装虚拟环境,linux和windows都可以。
**Tips:** 将鼠标悬浮在`网络`、`CPU占用率`、`GPU使用情况的用户`上时可以查看更详细的信息。
# 2. 开发环境
可在conda中安装虚拟环境,linux和windows都可以。
```bash
pip install flask flask-cors psutil -i https://pypi.tuna.tsinghua.edu.cn/simple
```
## 2.2. 服务器环境
即需要被查看的服务器上所安装的环境。
因为本质上是通过ssh连接服务器,然后通过命令来获取相应的信息,有的命令可能服务器系统上不自带需要另外安装,否则无法获取到对应的数据。
- **ifstat**,用于获取网络数据的工具,可通过apt安装(如果不需要显示网络数据则不用安装)。并且需要在服务器上运行一下命令,查看哪个网卡才是主要的,写到配置文件里去(如果不需要查看网络信息可以不写)。
- **gpustat**,用于获取显卡上用户的使用情况,也可通过apt安装。
- **nvidia驱动**,需要需要安装N卡的驱动,能够通过`nvidia-smi`来获取显卡信息即可(AMD的应该就没办法了)。
# 3. 运行部署
客户端的机器上 **可能** 还需要通过APT安装一下`gpustat`,最好是`1.1.1`版的,`0.6.0`貌似会出问题。
其中这个ifstat查看网卡的步骤如下:通过apt安装完成之后,在终端输入`ifstat`,可以看到类似下面的输出(ctrl+c停止),因为一般会不只一个网卡,而且名称也会不一样。此时可以看一下哪个名称的网卡有数据变化,比如下方的就是`eno2`,可以写到配置文件里。
可以使用`pyinstaller`将python程序打包得到客户端和服务端的可执行程序,则不再需要安装运行环境。如果不打包直接用python执行的话就要安装前面的开发环境。
```bash
pyinstaller --onefile client.py
pyinstaller --onefile server.py
```
eno1 eno2 br-6c8650526aef docker0 veth1d3300f
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
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
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
0.00 0.00 3.66 0.19 0.00 0.00 0.00 0.00 0.00 0.00
0.00 0.00 8.34 8.26 0.00 0.00 0.00 0.00 0.00 0.00
0.00 0.00 8.25 4.78 0.00 0.00 0.00 0.00 0.00 0.00
执行命令之后,可以`dist`目录内找到两个可执行文件,将`client`文件放到客户端的合适的地方,`server`放到服务器的合适的地方。客户端指的就是需要获取数据的机器,服务端就是网页所在的机器。
以及放置对应的`client_config.json`和`server_config.json`。
## 3.1. 服务器
执行以下命令即可,注意server和json要改为实际的路径。可以用screen或者systemctl来保持后台执行,推荐使用systemctl实现开机自启。
```bash
/home/lxb/projects/Tool_CheckGPUsWeb/dist/server --cfg /home/lxb/projects/Tool_CheckGPUsWeb/server_config.json
```
## 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`,具体规则如下:
- title:服务器名称,用于显示。
- ip:服务器ip地址,用于连接。
- port:访问的端口,一般是22,如果访问容器等则按需修改。
- username:用于登录的账户名称。
- password:用于登录的账户密码。
- key_filename:用于登录的账户密钥**路径**。(password和key_filename只需要设置一个即可,如果服务器只能使用密钥登陆则填密钥即可)
- network_interface_name:网卡名称。(非必须项,如果不需要可视化网速则不需要设置)
- storage_list:需要查看存储空间使用情况的路径list。(非必须项,无论有没有设置都会默认检查根目录的使用情况)
其中`client_config.json`中的内容如下,
```json
{
"title": "SERVER_76",
"ip": "123.123.123.76",
"port": 22,
"username": "lxb",
"password": "abcdefg",
"key_filename": "/home/.ssh/id_rsa",
"network_interface_name": "eno2",
"storage_list": [
"server_url": "http://10.1.16.174:15001",
"title": "174",
"interval": 3.0,
"note": "",
"enable": ["gpu", "cpu", "memory", "storage", "network"],
"storage_list":[
"/",
"/media/D",
"/media/E",
"/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,安装完之后可执行命令`docker run -d -p 80:80 -v /home/lxb/nginx_gpus:/usr/share/nginx/html --name nginx_gpus nginx:latest`,注意**按需修改命令**,具体可修改内容如下。
```bash
@ -74,21 +101,53 @@ docker run -d \
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. 其他
- `永辉`帮忙搞了一下顶部checkbox布局的问题。
# 4. 其他
- `永辉`帮忙搞了一下顶部checkbox布局的问题(在这一版中没有加上checkbox)
- 参考`治鹏`的方法加了每张显卡的用户使用的情况。

BIN
__pycache__/version.cpython-312.pyc

Binary file not shown.

90
client.py

@ -2,8 +2,10 @@ import os
import json
import time
import psutil
import argparse
import requests
import subprocess
from version import version
# region get data
@ -19,10 +21,13 @@ def get_gpus_info(error_dict):
gpu_name = gpu_name.replace('NVIDIA ', '').replace('GeForce ', '')
process_list = list()
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({
"user": process_info['username'],
"memory": process_info['gpu_memory_usage'],
"cmd": ' '.join(process_info["full_command"])
"cmd": cmd
})
# 加到list中
@ -36,7 +41,7 @@ def get_gpus_info(error_dict):
"process_list": process_list
})
except Exception as e:
error_dict['gpu'] = e
error_dict['gpu'] = f'{e}'
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)
except Exception as e:
error_dict['cpu'] = e
error_dict['cpu'] = f'{e}'
return result_dict
@ -103,7 +108,7 @@ def get_storages_info(error_dict, path_list):
}
result_list.append(tmp_res)
except Exception as e:
error_dict['storage'] = e
error_dict['storage'] = f'{e}'
return result_list
@ -115,7 +120,7 @@ def get_memory_info(error_dict):
result_dict["total"] = mem.total / 1024
result_dict["used"] = mem.used / 1024
except Exception as e:
error_dict['memory'] = e
error_dict['memory'] = f'{e}'
return result_dict
@ -124,37 +129,39 @@ last_network_stats = None
last_network_time = None
def get_networks_info(error_dict):
result_list = list()
# try:
global last_network_stats
global last_network_time
current_stats = psutil.net_io_counters(pernic=True)
if last_network_stats is None:
# 第一次检测
for k in current_stats.keys():
if k == 'lo':
continue
result_list.append({
"name": k,
"default": False,
"in": 0,
"out": 0
})
else:
time_interval = time.time() - last_network_time
for k in current_stats.keys():
if k == 'lo':
continue
result_list.append({
"name": k,
"default": False,
"in": (current_stats[k].bytes_recv - last_network_stats[k].bytes_recv) / time_interval / 1000,
"out": (current_stats[k].bytes_sent - last_network_stats[k].bytes_sent) / time_interval / 1000
})
# 记录信息下次用
last_network_stats = current_stats
last_network_time = time.time()
try:
global last_network_stats
global last_network_time
current_stats = psutil.net_io_counters(pernic=True)
if last_network_stats is None:
# 第一次检测
for k in current_stats.keys():
if k == 'lo':
continue
result_list.append({
"name": k,
"default": False,
"in": 0,
"out": 0
})
else:
time_interval = time.time() - last_network_time
for k in current_stats.keys():
if k == 'lo':
continue
result_list.append({
"name": k,
"default": False,
"in": (current_stats[k].bytes_recv - last_network_stats[k].bytes_recv) / time_interval / 1000,
"out": (current_stats[k].bytes_sent - last_network_stats[k].bytes_sent) / time_interval / 1000
})
# 记录信息下次用
last_network_stats = current_stats
last_network_time = time.time()
except Exception as e:
error_dict['network'] = f'{e}'
return result_list
@ -184,23 +191,28 @@ def collect_data():
result_dict['note'] = client_cfg['note']
result_dict['title'] = client_cfg['title']
result_dict['interval'] = client_cfg['interval']
result_dict['version'] = version
return result_dict
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
with open(cfg_path, 'r') as f:
client_cfg = json.load(f)
# 持续发送
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:
data = collect_data()
try:
requests.post(api_url, json=data)
result = requests.post(api_url, json=data)
except Exception as e:
print(e)
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",
"interval": 3.0,
"note": "",
@ -9,5 +9,6 @@
"/media/D",
"/media/E",
"/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_cors import CORS
from version import version
import json
import argparse
#region 全局
@ -9,20 +11,29 @@ CORS(app)
server_cfg = None
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
#region 接口
# 测试用
@app.route('/api')
@app.route(f'/{api_name}')
def hello():
return 'hi. —— CheckGPUsWeb'
@app.route('/api/get_data', methods=['GET'])
@app.route(f'/{api_name}/get_data', methods=['GET'])
def get_data():
return jsonify(data_dict)
@app.route('/api/update_data', methods=['POST'])
@app.route(f'/{api_name}/update_data', methods=['POST'])
def receive_data():
data = request.json
# 如果存在对应标题则更新记录
@ -41,16 +52,11 @@ def receive_data():
def init():
data_dict['server_dict'] = dict()
data_dict['version'] = version
for server_name in server_cfg['server_list']:
data_dict['server_dict'][server_name] = None
def main():
# 加载配置文件
cfg_path = "server_config.json"
global server_cfg
with open(cfg_path, 'r') as f:
server_cfg = json.load(f)
init()
# flask
app.run(debug=False, host=server_cfg['host'], port=server_cfg['port'])

8
server_config.json

@ -1,8 +1,8 @@
{
"host": "127.0.0.1",
"host": "0.0.0.0",
"port": 15002,
"server_list":["76", "174", "233", "222"],
"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 {
/* background-color: beige; */
font-size: 32px;
font-weight: bold;
padding-left: 20px;
padding-top: 10px;
/* padding-bottom: 5px; */
}
.header-container {
/* display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 10px; */
/* 设置html和body的高度为100% */
html, body {
height: 100%;
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 {
padding: 5px 10px;
margin: 5px;
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;
display: inline-block;
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 {
background-color: rgb(0, 0, 0);
color: white;
border-radius: 8px;
padding: 4px 10px;
padding: 6px 10px;
font-size: 26px;
margin-top: 4px;
margin-bottom: 6px;
}
@ -37,7 +73,7 @@
border-radius: 8px;
margin-top: 5px;
padding: 4px 8px;
margin-bottom: 5px;
margin-bottom: 12px;
background-color: #f9f9f9;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}

17
web/index.html

@ -1,18 +1,27 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>服务器信息</title>
<link rel="stylesheet" href="./css/style_1.css">
</head>
<body>
<div id="header-container">
服务器信息
</div>
<div id="server-data">
<div class="content">
<div id="header-container">
服务器信息
</div>
<div id="server-data">
</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>
</body>
</html>

62
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() {
fetch('http://127.0.0.1:15002/api/get_data')
fetch(apiURL + '/api/get_data')
// 获取服务器和显卡数据
.then(response => response.json()) // 解析 JSON 响应
.then(data => {
@ -82,17 +99,27 @@ function displayServerData(data){
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 timeFromUpdate = Date.now() - lastTime;
if (timeFromUpdate > serverData['interval'] * 1000 * 3){
if (timeFromUpdate > serverData['interval'] * 1000 * 4){
let errText = document.createElement('div');
errText.className = 'error-text';
errText.textContent = "长时间未更新,上次更新时间: " + lastTime.toLocaleString();
serverCard.appendChild(errText);
serverDataContainer.appendChild(serverCard);
continue;
}else if (timeFromUpdate > serverData['interval'] * 1000 * 1.5){
}else if (timeFromUpdate > serverData['interval'] * 1000 * 2.5){
serverName.textContent = serverTitle + " - Not update -";
}
@ -101,18 +128,20 @@ function displayServerData(data){
let networkInfo = document.createElement('div');
networkInfo.className = 'network-info';
// todo 暂时采用所有网卡均值的方法
// todo 暂时采用所有网卡总和的方法
let inSum = 0;
let outSum = 0;
let tmpTitle = "";
serverData.network_list.forEach(function(network){
inSum += network['in'];
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 outStr = parse_data_unit(outSum / serverData.network_list.length);
let inStr = parse_data_unit(inSum);
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);
// 分割线
@ -128,9 +157,9 @@ function displayServerData(data){
serverData.cpu['temperature_list'].forEach(function(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>" + serverData.cpu['core_avg_occupy'] + "%";
"<strong>占用率 : </strong><span title=\"" + serverData.cpu['core_occupy_list'] + "\">" + serverData.cpu['core_avg_occupy'] + "%";
serverCard.appendChild(cpuInfo);
// 分割线
@ -176,7 +205,7 @@ function displayServerData(data){
tmpClass = "state-occupy";
else if (availableNum / totalNum < 0.3)
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>";
}
@ -195,13 +224,14 @@ function displayServerData(data){
let markLightOccupy = '<span class="state-light-occupy"> 占用</span>';
let markOccupy = '<span class="state-occupy"> 占用</span>';
let tmpMark = markFree;
if (gpu.used_memory < 1000 && gpu.utilization < 20){
tmpMark = markFree;
let memory_used_ratio = gpu.used_memory / gpu.total_memory;
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;
}else{
tmpMark = markOccupy;
tmpMark = markFree;
}
gpuInfo.innerHTML = '<strong>' + gpu.idx + ' - ' + gpu.name + tmpMark + '</strong><br>'
+ '温度: ' + gpu.temperature + '°C<br>'
@ -257,5 +287,5 @@ function displayServerData(data){
// 页面加载时获取数据并定时刷新
document.addEventListener('DOMContentLoaded', function() {
fetchData();
setInterval(fetchData, 3500); // 每3.5秒刷新一次数据
});
setInterval(fetchData, 4000); // 每4秒刷新一次数据
});

Loading…
Cancel
Save