263 行
8.7 KiB
Python
263 行
8.7 KiB
Python
from flask import Flask, render_template, request, redirect, url_for,jsonify
|
||
import json
|
||
import os
|
||
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_JSON 注入(JSON 数组),避免在代码中硬编码 IP / 账号 / 密码
|
||
# 格式: [{"name": "web1", "ip": "192.168.1.10", "username": "root", "password": "xxx"}]
|
||
# 配置方式参考 .env.example
|
||
_servers_env = os.environ.get("SERVERS_JSON", "").strip()
|
||
if _servers_env:
|
||
try:
|
||
servers = json.loads(_servers_env)
|
||
except json.JSONDecodeError as e:
|
||
raise SystemExit(f"SERVERS_JSON 环境变量不是合法 JSON: {e}")
|
||
else:
|
||
servers = []
|
||
logging.warning("未设置 SERVERS_JSON 环境变量,服务器列表为空,请参考 .env.example 进行配置")
|
||
|
||
# 全局变量存储服务器状态
|
||
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()
|
||
# debug 模式默认关闭:开启后 Werkzeug 调试器会暴露在网络上,存在远程代码执行风险
|
||
debug = os.environ.get("FLASK_DEBUG", "0").lower() in ("1", "true", "yes")
|
||
app.run(host='0.0.0.0', port=8090, debug=debug)
|