feat: 添加服务器控制功能

- 新增服务器控制页面,支持查看服务器状态和执行关机、重启操作
- 添加移动端适配页面,优化小屏显示效果
- 实现服务器信息展示、折叠面板、模态框等功能
- 添加 Docker 部署相关文件,支持容器化部署
- 新增测试脚本,验证 SSH 连接和命令执行功能
这个提交包含在:
2025-03-28 08:55:07 +08:00
当前提交 187ecd7d34
共修改 10 个文件,包含 1439 行新增和 0 行删除
+166
查看文件
@@ -0,0 +1,166 @@
### Python template
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python
.pdm-build/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
/.idea/
/.vscode/
+7
查看文件
@@ -0,0 +1,7 @@
FROM python:3.9
WORKDIR /app
COPY . /app
RUN pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
EXPOSE 8090
CMD ["python", "app.py"]
+252
查看文件
@@ -0,0 +1,252 @@
from flask import Flask, render_template, request, redirect, url_for,jsonify
import threading
import time
import logging
from logging.handlers import TimedRotatingFileHandler
import paramiko
# import subprocess
# import shlex
app = Flask(__name__)
# 指定template的目录
app.template_folder = 'templates'
def setup_logging():
# 创建日志记录器
logger = logging.getLogger()
logger.setLevel(logging.DEBUG) # 设置最低日志级别
# 创建文件处理器(按天分割日志)
log_path = "logs/app.log"
file_handler = TimedRotatingFileHandler(
filename=log_path,
when="midnight", # 每天午夜分割
backupCount=7 # 保留7天日志
)
file_handler.setLevel(logging.DEBUG)
# 创建控制台处理器(输出到终端)
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
# 设置日志格式
formatter = logging.Formatter(
"%(asctime)s [%(levelname)s] %(message)s",
datefmt="%Y-%m-%d %H:%M:%S"
)
file_handler.setFormatter(formatter)
console_handler.setFormatter(formatter)
# 添加处理器
logger.addHandler(file_handler)
logger.addHandler(console_handler)
# 初始化日志
setup_logging()
# 模拟的服务器信息,可以根据实际需求进行动态配置
servers = [
{"name": "现场端侧070", "ip": "***REMOVED***", "username": "root", "password": "***REMOVED***"},
{"name": "测试服务器", "ip": "***REMOVED***", "username": "root", "password": "***REMOVED***"},
{"name": "Server2", "ip": "192.168.1.2", "username": "user2", "password": "password2"},
]
# 全局变量存储服务器状态
server_status = {}
# 定时任务函数
def fetch_server_status():
global server_status
logging.info("=== 定时任务开始执行 ===")
results = []
for server in servers:
result = dict
result = server
output, error = execute_ssh_command(server, 'top -b -n1 | head -n5')
if error:
logging.error(f"SSH执行失败 - {server['name']}({server['ip']}) 错误:{error}")
result["status"] = "stopped"
result["error"] = error
else:
try:
result["info"] = parse_top_output(output)
result["status"] = "running"
logging.debug(f"获取状态成功 - {server['name']}({server['ip']})")
except Exception as e:
result["status"] = "stopped"
result["error"] = str(e)
results.append(result)
server_status = results # 更新全局状态
logging.info("=== 定时任务执行完成 ===")
# 递归设置定时器,实现循环执行
threading.Timer(60, fetch_server_status).start() # 每 60 秒执行一次
# 初始化定时任务
def start_scheduler():
fetch_server_status() # 立即执行一次
threading.Thread(target=lambda: time.sleep(0.1)).start() # 避免主线程阻塞
# SSH 连接并执行命令
def execute_ssh_command(server, command,connect_timeout=10, command_timeout=10):
try:
# 创建 SSH 客户端对象
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
# 连接到远程服务器
client.connect(server['ip'],
username=server['username'], password=server['password'],
timeout=connect_timeout)
# 执行命令
stdin, stdout, stderr = client.exec_command(command)
# 设置通道超时(防止命令卡住)
stdout.channel.settimeout(command_timeout) # 命令执行超时时间
# 读取输出(带超时处理)
try:
output = stdout.read().decode()
error = stderr.read().decode()
if error:
logging.error(f"SSH执行失败 - {server['name']}({server['ip']}) 错误:{error}")
else:
logging.debug(f"命令执行成功:{server['name']}({server['ip']}) {command}")
except paramiko.SSHException as e:
# 处理超时或异常
output = ""
error = f"Command execution timeout or error: {str(e)}"
client.close()
return output, error
except Exception as e:
return str(e), None
# def execute_ssh_command(server, command):
# try:
# ssh_command = f"ssh {server['username']}@{server['ip']} '{command}'"
# # 兼容 Python 3.6 的参数配置
# process = subprocess.run(
# shlex.split(ssh_command),
# stdout=subprocess.PIPE,
# stderr=subprocess.PIPE,
# timeout=10,
# check=True
# )
# # 手动解码字节为字符串
# return process.stdout.decode(), process.stderr.decode()
# except subprocess.CalledProcessError as e:
# return "", str(e)
# 根据IP获取server信息
def get_server_info(server_ip):
command = 'top -b -n1 | head -n 5'
for server in servers:
if server['ip'] == server_ip:
return server
return None
def parse_top_output(top_output):
lines = top_output.strip().split('\n') # 按行分割
result = {}
# 解析第1行:系统基础信息
parts = lines[0].split(', ')
# print(parts)
system_info_part = parts[0].split(' ', 3) # 分割为 ['top', '-', '时间', 'up ...']
result['system_time'] = system_info_part[2].strip() # 时间部分
result['uptime'] = system_info_part[3].strip() # 运行时间(如 up 712 days)
# 用户数在 parts[2]
user_str = parts[2]
result['users'] = user_str.strip()[0]
# 负载平均值在 parts[3]
result['load_avg'] = [parts[3].split(': ')[1],parts[4],parts[5]]
# 解析第2行:任务状态
tasks = lines[1].split(', ')
# print(tasks)
# print(tasks[2].strip().split(' '))
result['tasks'] = {
'total': (tasks[0].split()[1]),
'running': (tasks[1].strip()[0]),
'sleeping': (tasks[2].strip().split(' ')[0]),
'stopped': (tasks[3].strip()[0]),
'zombie': (tasks[4].strip()[0])
}
# print( result['tasks'])
# 解析第3行:CPU使用情况
cpu = lines[2].split(', ')
# print(cpu)
result['cpu'] = {
'us': (cpu[0].split()[1]),
'sy': (cpu[1].split()[0]),
'ni': (cpu[2].split()[0]),
'id': (cpu[3].split()[0]),
}
# print( result['cpu'])
# 解析第4行:内存使用情况
mem = lines[3].split(', ')
# print(mem)
# print(mem[0].split())
result['memory'] = {
'total': (mem[0].split()[3]),
'free': (mem[1].split()[0]),
'used': (mem[2].split()[0]),
'buff/cache': (mem[3].split()[0])
}
# print( result['memory'])
# 解析第5行:交换分区使用情况
swap_line = lines[4].split(', ')
# print(swap_line)
result['swap'] = {
'total': (swap_line[0].split()[2]),
'free': (swap_line[1].split()[0])
}
# print(result['swap'])
return result
# ==========================================================================================
# 前端页面展示
# 前端路由
@app.route('/')
def index():
return render_template('index.html', servers=server_status)
@app.route('/m')
def mobile():
return render_template('m.html', servers=server_status)
# 控制开关机
@app.route('/control', methods=['POST'])
def control():
server_ip = request.form['server_ip']
action = request.form['action']
logging.info(f"用户操作请求:{action} {server_ip}")
# 查找服务器信息
server = next((s for s in servers if s['ip'] == server_ip), None)
if not server:
return "Server not found", 404
# 根据操作执行相应命令
if action == "shutdown":
# command = "sudo shutdown -h now"
command = "sudo ls"
elif action == "reboot":
command = "sudo reboot"
else:
return "Invalid action", 400
# 执行命令
output, error = execute_ssh_command(server, command)
if error:
logging.error(f"执行失败:{server_ip} {action} - {error}")
return f"Error: {error}", 500
logging.info(f"操作成功:{server_ip} {action}")
return f"Command executed successfully: {output}"
if __name__ == '__main__':
# 启动定时任务
start_scheduler()
app.run(host='0.0.0.0',port=8090,debug=True)
+10
查看文件
@@ -0,0 +1,10 @@
services:
linux_controller:
build:
context: .
dockerfile: Dockerfile
container_name: linux_controller
ports:
- "8090:8090"
volumes:
- ./:/app
+2
查看文件
@@ -0,0 +1,2 @@
paramiko~=3.5.0
flask~=3.1.0
+1
查看文件
@@ -0,0 +1 @@
scp -r ./* root@***REMOVED***:/data/linux_controller/
+1
查看文件
@@ -0,0 +1 @@
./.venv/bin/python ./app.py
+462
查看文件
@@ -0,0 +1,462 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Server Control</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
}
.container {
padding: 20px;
}
/* 在现有按钮样式后添加 */
#refresh-btn {
background-color: #2196F3; /* 蓝色主题色 */
margin: 15px 0;
}
#refresh-btn:hover {
background-color: #1976D2;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
table, th, td {
border: 1px solid #ddd;
}
th, td {
padding: 10px;
text-align: center;
}
th {
background-color: #f4f4f4;
}
button {
padding: 8px 16px;
font-size: 14px;
cursor: pointer;
}
.control-buttons {
display: flex;
justify-content: space-around;
}
.control-buttons button {
margin: 5px;
}
</style>
<!-- 信息info -->
<style>
.quick-view {
display: flex;
justify-content: space-around;
margin-bottom: 10px;
}
.metric-box {
display: flex;
flex-direction: column;
align-items: center;
width: 100px;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
margin: 0 5px;
}
.metric-label {
font-size: 12px;
color: #666;
}
.metric-value {
font-size: 16px;
font-weight: bold;
margin-top: 5px;
}
.metric-value.green {
color: #4CAF50;
}
.metric-value.red {
color: #f44336;
}
.toggle-details {
background-color: #4CAF50;
color: white;
border: none;
padding: 5px 10px;
cursor: pointer;
margin-bottom: 5px;
}
.collapsed-details {
display: none;
padding: 10px;
border-top: 1px solid #ddd;
}
.resource-row {
display: flex;
gap: 10px;
margin-top: 15px;
}
.resource-card {
flex: 1;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
}
.card-title {
font-weight: bold;
margin-bottom: 5px;
}
.progress-bar {
height: 10px;
background-color: #f1f1f1;
border-radius: 3px;
width: 0;
}
.progress-text {
margin-top: 5px;
font-size: 12px;
}
.metric-table.small {
font-size: 12px;
}
</style>
<!-- 弹框 -->
<style>
/* 模态框样式 */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5);
z-index: 9999;
}
.modal-content {
background-color: white;
margin: 10% auto;
padding: 20px;
border-radius: 8px;
width: 30%;
text-align: center;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.modal-title {
font-size: 1.2em;
margin-bottom: 15px;
color: #2196F3;
}
.modal-message {
font-size: 1em;
margin-bottom: 25px;
color: #333;
}
.modal-buttons .btn {
padding: 10px 25px;
font-size: 14px;
border: none;
border-radius: 4px;
cursor: pointer;
margin: 0 10px;
}
.modal-buttons .confirm-btn {
background-color: #4CAF50;
color: white;
}
.modal-buttons .cancel-btn {
background-color: #f44336;
color: white;
}
.close {
float: right;
font-size: 20px;
cursor: pointer;
transition: color 0.3s;
}
.close:hover {
color: #2196F3;
}
/* 按钮样式优化 */
.custom-btn {
background-color: #4CAF50;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s;
}
.custom-btn:hover {
background-color: #45a049;
}
/* 不同操作的按钮颜色 */
[data-action="shutdown"] {
background-color: #f44336;
}
[data-action="shutdown"]:hover {
background-color: #d32f2f;
}
</style>
</head>
<body>
<div class="container">
<h1>端侧设备控制</h1>
<hr/>
<button id="refresh-btn" class="custom-btn" onclick="location.reload()">
<span class="button-text">刷新</span>
</button>
<table>
<thead>
<tr>
<th>服务器名称</th>
<th>服务器IP</th>
<th>状态</th>
<th>服务器信息</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for server in servers %}
<tr>
<td>{{ server.name }}</td>
<td>{{ server.ip }}</td>
<td>
{% if server.status == 'running' %}
<span style="color: green;">运行中</span>
{% elif server.status == 'stopped' %}
<span style="color: red;">已停止</span>
{% else %}
<span style="color: orange;">未知状态</span>
{% endif %}
</td>
<!-- 在表格的服务器信息列中 -->
<td>
{% if server.status == 'running' %}
<div class="server-details">
<!-- 简化版关键指标 -->
<div class="quick-view">
<div class="metric-box">
<div class="metric-label">CPU 空闲</div>
<div class="metric-value {{ 'green' if server.info.cpu.id|float > 90 else 'yellow' }}">
{{ server.info.cpu.id }}%
</div>
</div>
<div class="metric-box">
<div class="metric-label">内存使用</div>
<div class="metric-value {{ 'red' if server.info.memory.used|float / server.info.memory.total|float > 0.8 }}">
{{ (server.info.memory.used|float / server.info.memory.total|float * 100)|round(1) }}%
</div>
</div>
<div class="metric-box">
<div class="metric-label">负载</div>
<div class="metric-value">
{{ server.info.load_avg[0] }}
</div>
</div>
</div>
<!-- 折叠面板 -->
<button class="toggle-details" onclick="toggleDetails(this)">展开详细信息</button>
<div class="collapsed-details">
<h4>系统信息</h4>
<div class="info-row">
<div class="info-item">
<span class="label">系统时间:</span>
<span class="value">{{ server.info.system_time }}</span>
</div>
<div class="info-item">
<span class="label">运行时间:</span>
<span class="value">{{ server.info.uptime }}</span>
</div>
</div>
<h4>任务状态</h4>
<table class="metric-table small">
<tr><td>总任务数:</td><td>{{ server.info.tasks.total }}</td></tr>
<tr><td>运行中:</td><td>{{ server.info.tasks.running }}</td></tr>
<tr><td>休眠:</td><td>{{ server.info.tasks.sleeping }}</td></tr>
</table>
<h4>资源详情</h4>
<div class="resource-row">
<div class="resource-card">
<div class="card-title">CPU</div>
<div class="card-content">
<div class="progress-bar" style="width: {{ server.info.cpu.id }}%; background-color: #4CAF50;"></div>
<div class="progress-text">{{ server.info.cpu.id }}% 空闲</div>
</div>
</div>
<div class="resource-card">
<div class="card-title">内存</div>
<div class="card-content">
<div class="progress-bar" style="width: {{ (server.info.memory.used|float / server.info.memory.total|float)*100 }}%; background-color: #f44336;"></div>
<div class="progress-text">{{ (server.info.memory.used|float / 1024)|round(1) }}GB / {{ (server.info.memory.total|float / 1024)|round(1) }}GB</div>
</div>
</div>
</div>
</div>
</div>
{% elif server.status == 'stopped' %}
<p style="color: red;">服务器已停止</p>
{% else %}
<p style="color: orange;">状态未知</p>
{% endif %}
</td>
<!-- 操作按钮部分 -->
<td>
<div class="control-buttons">
<!-- 每个服务器的按钮组包裹在表单内 -->
<form action="/control" method="POST">
<input type="hidden" name="server_ip" value="{{ server.ip }}">
<!-- 关机按钮 -->
<button class="custom-btn" type="button" data-action="shutdown">
<span class="button-text">关机</span>
</button>
<!-- 重启按钮 -->
<button class="custom-btn" type="button" data-action="reboot">
<span class="button-text">重启</span>
</button>
</form>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- 在 body 结尾处添加模态框容器 -->
<div id="customModal" class="modal">
<div class="modal-content">
<span class="close">&times;</span>
<h3 class="modal-title"></h3>
<p class="modal-message"></p>
<div class="modal-buttons">
<button class="btn confirm-btn">确定</button>
<button class="btn cancel-btn">取消</button>
</div>
</div>
</div>
<script>
function toggleDetails(button) {
const parent = button.closest('.server-details');
const details = parent.querySelector('.collapsed-details');
details.style.display = details.style.display === 'block' ? 'none' : 'block';
}
</script>
<script>
// 获取模态框元素
const modal = document.getElementById('customModal');
const modalTitle = document.querySelector('.modal-title');
const modalMessage = document.querySelector('.modal-message');
const confirmBtn = document.querySelector('.confirm-btn');
const cancelBtn = document.querySelector('.cancel-btn');
const closeBtn = document.querySelector('.close');
// 显示模态框函数
function showModal(title, message, onConfirm, onCancel) {
modalTitle.textContent = title;
modalMessage.textContent = message;
modal.style.display = 'block';
// 绑定事件
confirmBtn.onclick = () => {
modal.style.display = 'none';
onConfirm();
};
cancelBtn.onclick = () => {
modal.style.display = 'none';
onCancel();
};
closeBtn.onclick = () => {
modal.style.display = 'none';
onCancel();
};
// 点击背景关闭
window.onclick = (event) => {
if (event.target == modal) {
modal.style.display = 'none';
onCancel();
}
};
}
// 重写按钮点击事件
document.querySelectorAll('.control-buttons button').forEach(btn => {
btn.onclick = (e) => {
e.preventDefault();
const form = e.target.closest('form');
const action = form.action;
const formData = new FormData(form);
formData.set('action',btn.dataset.action)
console.log(formData);
showModal(
"操作确认",
e.target.textContent + "操作不可逆,确认继续?",
() => {
// 提交表单
fetch(action, {
method: 'POST',
body: new URLSearchParams(formData),
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
}).then(response => {
if (response.ok) {
location.reload(); // 刷新页面
} else {
alert("操作失败");
}
});
},
() => {} // 取消操作
);
return false;
};
});
</script>
</body>
</html>
+495
查看文件
@@ -0,0 +1,495 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Server Control</title>
<!-- 在 head 引入字体图标 -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
<style>
/* 移动端媒体查询 */
@media (max-width: 768px) {
/* 顶部固定栏 */
.mobile-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
background: white;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
position: sticky;
top: 0;
z-index: 1000;
}
/* 刷新按钮 */
.mobile-refresh-btn {
padding: 8px 16px;
background: #2196F3;
}
/* 服务器卡片布局 */
.server-card {
background: white;
margin: 10px;
padding: 15px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
/* 状态标签 */
.status-badge {
padding: 5px 10px;
border-radius: 16px;
font-size: 12px;
}
.running { background: #e8f5e9; color: #155724; }
.stopped { background: #f8d7da; color: #721c24; }
/* 操作按钮 */
.mobile-controls {
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 15px;
}
.custom-btn {
width: 100%;
}
/* 折叠面板 */
.mobile-toggle {
display: flex;
justify-content: space-between;
width: 100%;
padding: 8px 12px;
background: #f1f1f1;
border: none;
border-radius: 4px;
}
.mobile-collapsed {
padding: 15px;
background: #f8f9fa;
}
}
/* 全局优化 */
.metric-box {
flex: 1 1 30%; /* 等分显示 */
}
.custom-btn {
touch-action: manipulation;
padding: 12px;
font-size: 16px;
}
</style>
<!-- 信息info -->
<style>
.quick-view {
display: flex;
justify-content: space-around;
margin-bottom: 10px;
}
.metric-box {
display: flex;
flex-direction: column;
align-items: center;
width: 100px;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
margin: 0 5px;
}
.metric-label {
font-size: 12px;
color: #666;
}
.metric-value {
font-size: 16px;
font-weight: bold;
margin-top: 5px;
}
.metric-value.green {
color: #4CAF50;
}
.metric-value.red {
color: #f44336;
}
.toggle-details {
background-color: #4CAF50;
color: white;
border: none;
padding: 5px 10px;
cursor: pointer;
margin-bottom: 5px;
}
.collapsed-details {
display: none;
padding: 10px;
border-top: 1px solid #ddd;
}
.resource-row {
display: flex;
gap: 10px;
margin-top: 15px;
}
.resource-card {
flex: 1;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
}
.card-title {
font-weight: bold;
margin-bottom: 5px;
}
.progress-bar {
height: 10px;
background-color: #f1f1f1;
border-radius: 3px;
width: 0;
}
.progress-text {
margin-top: 5px;
font-size: 12px;
}
.metric-table.small {
font-size: 12px;
}
</style>
<!-- 弹框 -->
<style>
/* 模态框样式 */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5);
z-index: 9999;
}
.modal-content {
background-color: white;
margin: 10% auto;
padding: 20px;
border-radius: 8px;
width: 80%;
text-align: center;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.modal-title {
font-size: 1.2em;
margin-bottom: 15px;
color: #2196F3;
}
.modal-message {
font-size: 1em;
margin-bottom: 25px;
color: #333;
}
.modal-buttons .btn {
padding: 10px 25px;
font-size: 14px;
border: none;
border-radius: 4px;
cursor: pointer;
margin: 0 10px;
}
.modal-buttons .confirm-btn {
background-color: #4CAF50;
color: white;
}
.modal-buttons .cancel-btn {
background-color: #f44336;
color: white;
}
.close {
float: right;
font-size: 20px;
cursor: pointer;
transition: color 0.3s;
}
.close:hover {
color: #2196F3;
}
/* 按钮样式优化 */
.custom-btn {
background-color: #4CAF50;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s;
}
.custom-btn:hover {
background-color: #45a049;
}
/* 不同操作的按钮颜色 */
[data-action="shutdown"] {
background-color: #f44336;
}
[data-action="shutdown"]:hover {
background-color: #d32f2f;
}
</style>
</head>
<body>
<div class="container">
<header class="mobile-header">
<h1>端侧设备控制</h1>
<button id="refresh-btn" class="mobile-refresh-btn">
<span class="button-text">刷新</span>
</button>
</header>
<div class="server-list">
{% for server in servers %}
<div class="server-card">
<div class="server-header">
<h2>{{ server.name }}</h2>
<div class="status-badge {{ server.status }}">
{% if server.status == 'running' %}
<span style="color: green;">运行中</span>
{% elif server.status == 'stopped' %}
<span style="color: red;">已停止</span>
{% else %}
<span style="color: orange;">未知状态</span>
{% endif %}
</div>
</div>
{% if server.status == 'running' %}
<div class="server-info">
<!-- 简化版关键指标 -->
<div class="quick-view">
<div class="metric-box">
<div class="metric-label">CPU 空闲</div>
<div class="metric-value {{ 'green' if server.info.cpu.id|float > 90 else 'yellow' }}">
{{ server.info.cpu.id }}%
</div>
</div>
<div class="metric-box">
<div class="metric-label">内存使用</div>
<div class="metric-value {{ 'red' if server.info.memory.used|float / server.info.memory.total|float > 0.8 }}">
{{ (server.info.memory.used|float / server.info.memory.total|float * 100)|round(1) }}%
</div>
</div>
<div class="metric-box">
<div class="metric-label">负载</div>
<div class="metric-value">
{{ server.info.load_avg[0] }}
</div>
</div>
</div>
<!-- 折叠面板 -->
<button class="toggle-details mobile-toggle" onclick="toggleDetails(this)">
<span>展开详细信息</span>
<i class="fas fa-chevron-down"></i>
</button>
<div class="collapsed-details mobile-collapsed">
<h4>系统信息</h4>
<div class="info-row">
<div class="info-item">
<span class="label">系统时间:</span>
<span class="value">{{ server.info.system_time }}</span>
</div>
<div class="info-item">
<span class="label">运行时间:</span>
<span class="value">{{ server.info.uptime }}</span>
</div>
</div>
<h4>任务状态</h4>
<table class="metric-table small">
<tr><td>总任务数:</td><td>{{ server.info.tasks.total }}</td></tr>
<tr><td>运行中:</td><td>{{ server.info.tasks.running }}</td></tr>
<tr><td>休眠:</td><td>{{ server.info.tasks.sleeping }}</td></tr>
</table>
<h4>资源详情</h4>
<div class="resource-row">
<div class="resource-card">
<div class="card-title">CPU</div>
<div class="card-content">
<div class="progress-bar" style="width: {{ server.info.cpu.id }}%; background-color: #4CAF50;"></div>
<div class="progress-text">{{ server.info.cpu.id }}% 空闲</div>
</div>
</div>
<div class="resource-card">
<div class="card-title">内存</div>
<div class="card-content">
<div class="progress-bar" style="width: {{ (server.info.memory.used|float / server.info.memory.total|float)*100 }}%; background-color: #f44336;"></div>
<div class="progress-text">{{ (server.info.memory.used|float / 1024)|round(1) }}GB / {{ (server.info.memory.total|float / 1024)|round(1) }}GB</div>
</div>
</div>
</div>
</div>
</div>
{% endif %}
<div class="control-buttons mobile-controls">
<div class="control-buttons">
<!-- 每个服务器的按钮组包裹在表单内 -->
<form action="/control" method="POST">
<input type="hidden" name="server_ip" value="{{ server.ip }}">
<!-- 关机按钮 -->
<button class="custom-btn" type="button" data-action="shutdown">
<span class="button-text">关机</span>
</button>
<!-- 重启按钮 -->
<button class="custom-btn" type="button" data-action="reboot">
<span class="button-text">重启</span>
</button>
</form>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
<!-- 在 body 结尾处添加模态框容器 -->
<div id="customModal" class="modal">
<div class="modal-content">
<span class="close">&times;</span>
<h3 class="modal-title"></h3>
<p class="modal-message"></p>
<div class="modal-buttons">
<button class="btn confirm-btn">确定</button>
<button class="btn cancel-btn">取消</button>
</div>
</div>
</div>
<script>
// 优化折叠动画
function toggleDetails(button) {
const parent = button.closest('.server-card');
const details = parent.querySelector('.collapsed-details');
details.style.display = details.style.display === 'block' ? 'none' : 'block';
button.querySelector('i').classList.toggle('fa-chevron-up');
}
// 防止频繁刷新
let refreshTimer;
document.getElementById('refresh-btn').addEventListener('click', () => {
clearTimeout(refreshTimer);
refreshTimer = setTimeout(location.reload, 500);
});
</script>
<script>
// 获取模态框元素
const modal = document.getElementById('customModal');
const modalTitle = document.querySelector('.modal-title');
const modalMessage = document.querySelector('.modal-message');
const confirmBtn = document.querySelector('.confirm-btn');
const cancelBtn = document.querySelector('.cancel-btn');
const closeBtn = document.querySelector('.close');
// 显示模态框函数
function showModal(title, message, onConfirm, onCancel) {
modalTitle.textContent = title;
modalMessage.textContent = message;
modal.style.display = 'block';
// 绑定事件
confirmBtn.onclick = () => {
modal.style.display = 'none';
onConfirm();
};
cancelBtn.onclick = () => {
modal.style.display = 'none';
onCancel();
};
closeBtn.onclick = () => {
modal.style.display = 'none';
onCancel();
};
// 点击背景关闭
window.onclick = (event) => {
if (event.target == modal) {
modal.style.display = 'none';
onCancel();
}
};
}
// 重写按钮点击事件
document.querySelectorAll('.control-buttons button').forEach(btn => {
btn.onclick = (e) => {
e.preventDefault();
const form = e.target.closest('form');
const action = form.action;
const formData = new FormData(form);
formData.set('action',btn.dataset.action)
console.log(formData);
showModal(
"操作确认",
e.target.textContent + "操作不可逆,确认继续?",
() => {
// 提交表单
fetch(action, {
method: 'POST',
body: new URLSearchParams(formData),
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
}).then(response => {
if (response.ok) {
location.reload(); // 刷新页面
} else {
alert("操作失败");
}
});
},
() => {} // 取消操作
);
return false;
};
});
</script>
</body>
+43
查看文件
@@ -0,0 +1,43 @@
import paramiko
# 配置连接参数
hostname = "***REMOVED***" # 远程服务器的 IP 地址
port = 22 # SSH 端口,通常是 22
username = "root" # SSH 用户名
password = "***REMOVED***" # SSH 密码,或使用密钥认证
# SSH 连接并执行关机命令
def shutdown_server():
try:
# 创建 SSH 客户端对象
client = paramiko.SSHClient()
# 自动添加主机密钥(如果是首次连接)
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
# 连接到远程服务器
client.connect(hostname, port, username, password)
# 执行关机命令
# stdin, stdout, stderr = client.exec_command('sudo shutdown -h now')
stdin, stdout, stderr = client.exec_command('sudo ls /')
# 如果需要输入 sudo 密码,可以通过 stdin 传递
stdin.write('your_sudo_password\n')
stdin.flush()
# 输出命令执行结果
output = stdout.read().decode()
error = stderr.read().decode()
if output:
print("Output:", output)
if error:
print("Error:", error)
# 关闭 SSH 连接
client.close()
except Exception as e:
print(f"An error occurred: {e}")
if __name__ == "__main__":
shutdown_server()