commit 187ecd7d34fcf239a1d15b659b7f21fac9d1967d Author: wangchuanli Date: Fri Mar 28 08:55:07 2025 +0800 feat: 添加服务器控制功能 - 新增服务器控制页面,支持查看服务器状态和执行关机、重启操作 - 添加移动端适配页面,优化小屏显示效果 - 实现服务器信息展示、折叠面板、模态框等功能 - 添加 Docker 部署相关文件,支持容器化部署 - 新增测试脚本,验证 SSH 连接和命令执行功能 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7e36f92 --- /dev/null +++ b/.gitignore @@ -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/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..568c91f --- /dev/null +++ b/Dockerfile @@ -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"] + diff --git a/app.py b/app.py new file mode 100644 index 0000000..7c26b6b --- /dev/null +++ b/app.py @@ -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) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..4157032 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,10 @@ +services: + linux_controller: + build: + context: . + dockerfile: Dockerfile + container_name: linux_controller + ports: + - "8090:8090" + volumes: + - ./:/app diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..489d787 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +paramiko~=3.5.0 +flask~=3.1.0 \ No newline at end of file diff --git a/send.sh b/send.sh new file mode 100644 index 0000000..173b36b --- /dev/null +++ b/send.sh @@ -0,0 +1 @@ +scp -r ./* root@***REMOVED***:/data/linux_controller/ \ No newline at end of file diff --git a/start.sh b/start.sh new file mode 100644 index 0000000..5fcda06 --- /dev/null +++ b/start.sh @@ -0,0 +1 @@ +./.venv/bin/python ./app.py \ No newline at end of file diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..8261428 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,462 @@ + + + + + + Server Control + + + + + + + + + + +
+

端侧设备控制

+
+ + + + + + + + + + + + + {% for server in servers %} + + + + + + + + + + + {% endfor %} + +
服务器名称服务器IP状态服务器信息操作
{{ server.name }}{{ server.ip }} + {% if server.status == 'running' %} + 运行中 + {% elif server.status == 'stopped' %} + 已停止 + {% else %} + 未知状态 + {% endif %} + + {% if server.status == 'running' %} +
+ +
+
+
CPU 空闲
+
+ {{ server.info.cpu.id }}% +
+
+
+
内存使用
+
+ {{ (server.info.memory.used|float / server.info.memory.total|float * 100)|round(1) }}% +
+
+
+
负载
+
+ {{ server.info.load_avg[0] }} +
+
+
+ + + +
+

系统信息

+
+
+ 系统时间: + {{ server.info.system_time }} +
+
+ 运行时间: + {{ server.info.uptime }} +
+
+ +

任务状态

+ + + + +
总任务数:{{ server.info.tasks.total }}
运行中:{{ server.info.tasks.running }}
休眠:{{ server.info.tasks.sleeping }}
+ +

资源详情

+
+
+
CPU
+
+
+
{{ server.info.cpu.id }}% 空闲
+
+
+
+
内存
+
+
+
{{ (server.info.memory.used|float / 1024)|round(1) }}GB / {{ (server.info.memory.total|float / 1024)|round(1) }}GB
+
+
+
+
+
+ {% elif server.status == 'stopped' %} +

服务器已停止

+ {% else %} +

状态未知

+ {% endif %} +
+
+ +
+ + + + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/templates/m.html b/templates/m.html new file mode 100644 index 0000000..a538786 --- /dev/null +++ b/templates/m.html @@ -0,0 +1,495 @@ + + + + + + Server Control + + + + + + + + + + + + + + +
+
+

端侧设备控制

+ +
+ +
+ {% for server in servers %} +
+
+

{{ server.name }}

+
+ {% if server.status == 'running' %} + 运行中 + {% elif server.status == 'stopped' %} + 已停止 + {% else %} + 未知状态 + {% endif %} +
+
+ {% if server.status == 'running' %} +
+ +
+
+
CPU 空闲
+
+ {{ server.info.cpu.id }}% +
+
+
+
内存使用
+
+ {{ (server.info.memory.used|float / server.info.memory.total|float * 100)|round(1) }}% +
+
+
+
负载
+
+ {{ server.info.load_avg[0] }} +
+
+
+ + + +
+

系统信息

+
+
+ 系统时间: + {{ server.info.system_time }} +
+
+ 运行时间: + {{ server.info.uptime }} +
+
+ +

任务状态

+ + + + +
总任务数:{{ server.info.tasks.total }}
运行中:{{ server.info.tasks.running }}
休眠:{{ server.info.tasks.sleeping }}
+ +

资源详情

+
+
+
CPU
+
+
+
{{ server.info.cpu.id }}% 空闲
+
+
+
+
内存
+
+
+
{{ (server.info.memory.used|float / 1024)|round(1) }}GB / {{ (server.info.memory.total|float / 1024)|round(1) }}GB
+
+
+
+
+
+ {% endif %} +
+
+ +
+ + + + + +
+
+
+
+ {% endfor %} +
+
+ + + + + + + + + + \ No newline at end of file diff --git a/test.py b/test.py new file mode 100644 index 0000000..227c54d --- /dev/null +++ b/test.py @@ -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() \ No newline at end of file